<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>WZH</title><description>An engineer wielding AI.</description><link>https://zhihao.wang</link><item><title>Building Emoji Search with Claude Code in Half an Hour</title><link>https://zhihao.wang/blog/emoji-search</link><guid isPermaLink="true">https://zhihao.wang/blog/emoji-search</guid><description>A note on an e5 distribution bias I bumped into.</description><pubDate>Sat, 25 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Built a semantic emoji search with Claude Code. Fully static, the model runs in the browser. A short writeup below.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/tool/emoji-search&quot;&gt;demo&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;1. Requirements&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Search emoji by text in Chinese or English; pasting an emoji should also surface similar ones.&lt;/li&gt;
&lt;li&gt;Show emoji glosses in both languages.&lt;/li&gt;
&lt;li&gt;Skin-tone toggle.&lt;/li&gt;
&lt;li&gt;Tunable retrieval params: top-K, similarity threshold, etc.&lt;/li&gt;
&lt;li&gt;Click to copy; Enter copies the first result.&lt;/li&gt;
&lt;li&gt;Fully static and local-only — privacy first.&lt;/li&gt;
&lt;li&gt;The model loads on first visit and is cached afterwards; offline-capable from the second visit, with first load under 30s.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;2. Overall Design&lt;/h2&gt;
&lt;p&gt;The model is &lt;code&gt;Xenova/multilingual-e5-small&lt;/code&gt;: multilingual, 384-dim, ~30 MB after int8 quantization — small enough to live in the browser&apos;s IndexedDB. e5-base would be better quality but isn&apos;t worth the cost in the browser.&lt;/p&gt;
&lt;p&gt;1914 emoji, each annotated with Chinese and English names plus CLDR keywords (flag entries derive from &lt;code&gt;cldr-annotations-derived-full&lt;/code&gt;). At build time, embeddings are computed once in Node, centered, and quantized — totaling roughly 1.1 MB of artifacts.&lt;/p&gt;
&lt;p&gt;At runtime the main thread only handles UI; model inference and vector math live in a Web Worker. Queries take one of three paths:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pure keyword queries hit a dictionary lookup.&lt;/li&gt;
&lt;li&gt;Text-semantic queries go through the model embedding + cosine.&lt;/li&gt;
&lt;li&gt;Pasted-emoji reverse lookups skip the model entirely and just dot-product against the prebuilt vectors.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. Notable Bits&lt;/h2&gt;
&lt;h3&gt;3.1 Emoji Reverse Search&lt;/h3&gt;
&lt;p&gt;The tool needs to support pasting an emoji to find similar ones (e.g. paste 🐼, get back 🦊 🐵 🐻). There&apos;s a free optimization here: since the query &lt;em&gt;is&lt;/em&gt; an emoji, its vector was already computed at build time and lives in &lt;code&gt;emoji-vectors.i8.bin&lt;/code&gt;. At runtime we just look up the index and use it directly — no need to run the model again.&lt;/p&gt;
&lt;p&gt;The implementation is straightforward: strip skin-tone modifiers (&lt;code&gt;U+1F3FB&lt;/code&gt; to &lt;code&gt;U+1F3FF&lt;/code&gt;) from the input, use &lt;code&gt;Intl.Segmenter&lt;/code&gt; to extract the first grapheme cluster (which handles ZWJ sequences like 🤦‍♂️), look up &lt;code&gt;Map&amp;#x3C;emoji, index&gt;&lt;/code&gt;, and dot-product directly against the 1914 stored int8 vectors.&lt;/p&gt;
&lt;p&gt;Text search runs the model every query (~tens of ms with q8). Reverse search has none of that overhead — it&apos;s just dot products, and noticeably faster.&lt;/p&gt;
&lt;h3&gt;3.2 The e5 Distribution Bias&lt;/h3&gt;
&lt;p&gt;Right after wiring up reverse search, dragging the similarity slider had no visible effect. I checked the cosine distribution from 🐼 to every other emoji:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;top-1   0.972  (🦊)
top-10  0.955  (🐭)
p50     0.901
p90     0.886  ← 90% of emoji sit above this
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A 0.84 threshold basically lets every one of the 1913 other emoji through. Likely cause: e5 is trained with a &lt;code&gt;passage:&lt;/code&gt; prefix on every passage, so all doc vectors get pulled toward a shared direction by that common context. Computing the mean confirms it: &lt;code&gt;||mean||&lt;/code&gt; ≈ 0.7, far from negligible.&lt;/p&gt;
&lt;p&gt;This isn&apos;t a new problem. The fix has been around for years: &lt;a href=&quot;https://arxiv.org/abs/1702.01417&quot;&gt;All-but-the-Top (Mu &amp;#x26; Viswanath, ICLR 2018)&lt;/a&gt; discusses how a few common directions dominate distance in word embeddings. The remedy is to subtract the mean and renormalize:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;for (let i = 0; i &amp;#x3C; n; i++) {
  let n2 = 0;
  for (let j = 0; j &amp;#x3C; dim; j++) {
    vec[i * dim + j] -= mean[j];
    n2 += vec[i * dim + j] ** 2;
  }
  const nrm = Math.sqrt(n2);
  for (let j = 0; j &amp;#x3C; dim; j++) vec[i * dim + j] /= nrm;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Subtract the mean at build time, store the centered vectors, and at runtime subtract the same mean from query embeddings — keeping the feature spaces aligned. After:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;🐼 vs 🐻     0.963  →   0.570
🐼 vs 🍜     0.909  →  -0.011
🐼 vs 😀     0.920  →   0.201
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The distribution opens up. The same threshold now works for both text and reverse search.&lt;/p&gt;
&lt;p&gt;The All-but-the-Top paper actually recommends going further: subtract the mean &lt;em&gt;and&lt;/em&gt; remove the top K = D/100 principal components (D=384 → K≈4). I tried it:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;right&quot;&gt;K&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;🐼 top-1&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;🐼 vs 🐻&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;🐼 vs 🍜&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;0 (mean only)&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;&lt;strong&gt;0.628&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;&lt;strong&gt;0.572&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;-0.009&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;4 (paper default)&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.531&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.478&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;-0.090&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;right&quot;&gt;8&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.376&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.343&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.004&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;But the result is counterintuitive: as K grows, the cosine to &lt;em&gt;related&lt;/em&gt; emoji (🐼-🐻) gets squashed alongside everything else — the distribution narrows instead of widening. My guess is the paper targeted word2vec / GloVe — Zipfian-frequency-dominated word vectors with many shared directions — whereas e5 is a &lt;strong&gt;contrastively trained sentence encoder&lt;/strong&gt; whose training objective already does a lot of the work compressing shared directions. After removing the mean, what&apos;s left is mostly real semantics, and removing more is just dropping signal.&lt;/p&gt;
&lt;p&gt;Final pick: K=0, first-order is enough.&lt;/p&gt;</content:encoded></item><item><title>站点更新日志</title><link>https://zhihao.wang/blog/website-update-log</link><guid isPermaLink="true">https://zhihao.wang/blog/website-update-log</guid><description>记录本网站的搭建过程和功能更新历程</description><pubDate>Sun, 19 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://wzh.cc/img/3523dc53.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;(Photo by Volodymyr Dobrovolskyy on Unsplash
)&lt;/p&gt;
&lt;h2&gt;更新日志&lt;/h2&gt;
&lt;h3&gt;2025-11&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;优化了 Cards 瀑布流排序算法，优化图片加载性能。&lt;/li&gt;
&lt;li&gt;处理和规范相关域名使用。&lt;/li&gt;
&lt;li&gt;解决了一些已知问题；内容样式与交互优化。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2025-10&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;搭建网站初版（&lt;a href=&quot;https://astro.build/&quot;&gt;Astro 框架&lt;/a&gt;、&lt;a href=&quot;https://astro-pure.js.org/&quot;&gt;Pure 主题&lt;/a&gt;，大量参考 &lt;a href=&quot;https://joeytoday.com/&quot;&gt;joeytoday 博客样式&lt;/a&gt;）。&lt;/li&gt;
&lt;li&gt;打通部署链路（&lt;a href=&quot;https://workers.cloudflare.com/&quot;&gt;Cloudflare Workers&lt;/a&gt;）。&lt;/li&gt;
&lt;li&gt;搭建对象存储链路（&lt;a href=&quot;https://www.cloudflare.com/developer-platform/products/r2/&quot;&gt;Cloudflare R2&lt;/a&gt;）。&lt;/li&gt;
&lt;li&gt;集成图像处理功能（&lt;a href=&quot;https://developers.cloudflare.com/images/transform-images/&quot;&gt;Cloudflare Transform Images&lt;/a&gt;）。&lt;/li&gt;
&lt;li&gt;搭建发图自动上传图床机器人（&lt;a href=&quot;https://core.telegram.org/bots&quot;&gt;Telegram Bots&lt;/a&gt;）。&lt;/li&gt;
&lt;li&gt;新增 Cards 内容类型。&lt;/li&gt;
&lt;li&gt;移除 Comments 模块、文章转发模块、阅读时间提示模块等。&lt;/li&gt;
&lt;li&gt;内容样式与交互优化。&lt;/li&gt;
&lt;/ol&gt;</content:encoded></item><item><title>公司的坑位多少才足够？</title><link>https://zhihao.wang/blog/waiting-time-paradox</link><guid isPermaLink="true">https://zhihao.wang/blog/waiting-time-paradox</guid><description>还要等多久啊？？？</description><pubDate>Sun, 11 Apr 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 等坑位问题&lt;/h2&gt;
&lt;p&gt;公司每层只有 4 个坑位，需要供至少两百人使用，每次需要解决生理问题的时候都需要等待一段时间，在等待的过程中想到了这个问题：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果 4 个坑位供 200 个人使用，假设每人平均需要使用 20 分钟，每天上班时间的坑位使用都不会断（即每出来一个人必定有一个人进去），并且每天在正式上班之前就已经有 4 个人在任意时间开始使用了。请问，我在上班时候的任意时刻去到厕所，发现 4 个坑位满员且前面没有人排队，如果我等的话我需要等待多长时间？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;回到原来这道题，因为设定了每个人蹲 20 分钟（实际上是数学期望为 20 分钟，这里为了简化问题，直接定死为 20 分钟），因此在最好情况下，我到的时候刚好有人出来，需要等待 0 分钟；在最差情况下，我到的时候刚好 4 个人同时入坑，我需要等待 20 分钟，那么我们只需要考虑在这 20 分钟以内，第一个人出来的时间的期望。
又因为我们只需要考虑我们到厕所的时刻的前 20 分钟，因此问题转化为了：在 0~20 之间随机取 4 个数，这 4 个数里的最小值就是我需要等待的时间。&lt;/p&gt;
&lt;h3&gt;1.1 蒙特卡罗方法&lt;/h3&gt;
&lt;p&gt;身为一个程序猿，我们先用百试不爽的蒙特卡罗方法模拟一下，假设每人蹲坑时间 $T = 20$，坑位数量 $N = 4$，模拟 100 万次：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import random

NUM_TESTS = 1000000  # Num of tests
NUM_TOILETS = 4      # Num of toilets
TOILET_TIME = 20     # Average toilet time

total_time = 0
for _ in range(NUM_TESTS):
    total_time += min([random.random() * TOILET_TIME for _ in range(NUM_TOILETS)])

print(f&apos;Num of tests: {NUM_TESTS:,}&apos;)
print(f&apos;Num of toilets: {NUM_TOILETS}&apos;)
print(f&apos;Average toilet time: {TOILET_TIME} mins&apos;)
print(f&apos;You need to wait for {total_time / NUM_TESTS:.3f} mins.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模拟结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Num of tests: 1,000,000
Num of toilets: 4
Average toilet time: 20 mins
You need to wait for 3.996 mins.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这种情况下，我们需要等待约 4 分钟时间。&lt;/p&gt;
&lt;p&gt;如果你多试几次不同的值，很快就会发现实际上我们要等待的平均时间是 $\frac{T}{N + 1}$。&lt;/p&gt;
&lt;h3&gt;1.2 解析方法&lt;/h3&gt;
&lt;p&gt;下面我们尝试分析。&lt;/p&gt;
&lt;p&gt;假设每人蹲坑时间为 $T$，坑位数量为 $N$，每个坑位在上一个 $T$ 分钟的时间周期内进去的时刻为 $t_i$，$i \in {1, 2, \cdots, n}$。那么根据上面的分析，我们要等待的时长应该是 $x = \min(t_1, t_2, \cdots, t_N)$，现在要求 $x$ 的期望 $E[x]$。&lt;/p&gt;
&lt;p&gt;我们试着算一下 $x$ 的累积分布函数（Cumulative Distribution Function，CDF）：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\operatorname{CDF}(x)
&amp;#x26;= P(x \le t) \
&amp;#x26;= 1 - P(x &gt; t) \
&amp;#x26;= 1 - \prod_{i = 1}^{N} p(x &gt; t_i) \
&amp;#x26;= 1 - (1 - \frac{x}{T})^N
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;对累积分布函数求导，可以得到 $x$ 的概率密度函数（Probability Density Function，PDF）：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\operatorname{PDF}(x)
&amp;#x26;= (-1) \cdot (N) \cdot (1 - \frac{x}{T})^{N - 1} \cdot (-\frac{1}{T}) \
&amp;#x26;= \frac{N}{T} \cdot (1 - \frac{x}{T})^{N - 1}
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;那么 $x$ 的数学期望 $E[x]$ 计算如下：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
E[x]
&amp;#x26;= \int_0^T x \cdot \operatorname{PDF}(x) ~ dx \
&amp;#x26;= \int_0^T x \cdot \frac{N}{T} \cdot (1 - \frac{x}{T})^{N - 1} ~ dx
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;我们采用换元积分法：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
t(x) &amp;#x26;= 1 - \frac{x}{T} \
t&apos;(x) &amp;#x26;= -\frac{1}{T} \
x &amp;#x26;= (1 - t) \cdot T \
E[x]
&amp;#x26;= \int_0^T x \cdot \frac{N}{T} \cdot (1 - \frac{x}{T})^{N - 1} ~ dx \
&amp;#x26;= - N \int_0^T x \cdot (1 - \frac{x}{T})^{N - 1} \cdot (-\frac{1}{T}) ~ dx \
&amp;#x26;= - N \int_1^0 (1 - t) \cdot T \cdot t^{N - 1} ~ dt \
&amp;#x26;= - NT \int_1^0 (t^{N - 1} - t^N) ~ dt \
&amp;#x26;= - NT \int_1^0 (\frac{t^N}{N} - \frac{t^{N + 1}}{N + 1})&apos; ~ dt \
&amp;#x26;= - NT \cdot (0 - \frac{1}{N (N + 1)}) \
&amp;#x26;= \frac{T}{N + 1}
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;结论与蒙特卡罗方法模拟的结果一致。&lt;/p&gt;
&lt;h2&gt;2. 感知错觉：检查悖论&lt;/h2&gt;
&lt;p&gt;回顾一下上一节的结论：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;假设每人蹲坑时间为 $T$，坑位数量为 $N$，每个坑位在上一个 $T$ 分钟的时间周期内进去的时刻为 $t_i$，$i \in {1, 2, \cdots, n}$。那么我们在任意时刻进入，发现坑位满员且无人等待，那么我们需要等待的时长 $x$ 的数学期望为：&lt;/p&gt;
&lt;p&gt;$$E[x] = \frac{T}{N + 1}$$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然而在现实生活中，我们往往会觉得等了远远不止这个时间。以我司坑位举例，坑位数量 $N=4$，假设每人蹲坑时间 $T=20$，那么这种情况下我需要等待的时间应该是 $\frac{T}{N + 1} = \frac{20}{4 + 1} = 4$ 分钟，但是我感觉经常需要等待 10 分钟以上，记忆中最长的一次能到 15 分钟以上，这是为什么？&lt;/p&gt;
&lt;p&gt;事实上，这是&lt;strong&gt;采样偏差&lt;/strong&gt;所导致的问题。&lt;/p&gt;
&lt;p&gt;按照我们前面的设定，每人的蹲坑时间固定，每个坑位的初始开始时间随机且独立，我们在任意时刻到达厕所（视为一次采样），那么需要等待时间的采样应该是一个均匀分布。而实际上，每人的蹲坑时间有长有短，更有可能是一个均值为 $T$ 的高斯分布，而不是一个均匀分布，这就导致了每个坑位在时间轴上被使用的情况的「区间划分」是不均匀的，那么如果我们依然是在均匀的任意时刻到达厕所（视为一次采样），那么我们会&lt;strong&gt;对等待时间较长的区间采样得更多，而对等待时间较短的区间采样得更少&lt;/strong&gt;，因此导致了我们实际上确实等了更长的时间（而不仅仅是在感受上）！&lt;/p&gt;
&lt;p&gt;下面再基于蒙特卡罗方法重新模拟。我们将每人的蹲坑时间从固定的 $T=20$，改为符合均值为 $T=20$、方差为 $5$ 的正态分布，其余变量不变，模拟 100 万次：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import random

NUM_TESTS = 1000000    # Num of tests
NUM_TOILETS = 4        # Num of toilets
TOILET_TIME = 20       # Average toilet time
TOILET_TIME_SIGMA = 5  # Standard deviation of toilet time

total_time = 0
for _ in range(NUM_TESTS):
    waiting_time = []
    while len(waiting_time) &amp;#x3C; NUM_TOILETS:
        t = random.normalvariate(TOILET_TIME, TOILET_TIME_SIGMA)
        if t &gt; 0:
            waiting_time.append(t)
    total_time += min(waiting_time)

print(f&apos;Num of tests: {NUM_TESTS:,}&apos;)
print(f&apos;Num of toilets: {NUM_TOILETS}&apos;)
print(f&apos;Average toilet time: {TOILET_TIME} mins&apos;)
print(f&apos;You need to wait for {total_time / NUM_TESTS:.3f} mins.&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模拟结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Num of tests: 1,000,000
Num of toilets: 4
Average toilet time: 20 mins
You need to wait for 14.857 mins.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Amazing，需要等待的时间从 4 分钟涨到了 15 分钟！😂&lt;/p&gt;
&lt;p&gt;当然，这里出于习惯假设了高斯分布，并且指定了到达时间的方差，并不一定正确，目的只是为了在直观上理解这种现象。事实上，这个现象也有科学家进行了相关研究，例如，美国计算机学家 Allen Downey 对普渡大学的班级平均人数进行统计，发现通过统计得出的平均人数是 90 人，而教务处给出的真正的平均人数是 35 人，从而提出了 &lt;code&gt;检查悖论&lt;/code&gt;（Inspection Paradox，&lt;a href=&quot;https://en.wikipedia.org/wiki/Renewal_theory#Inspection_paradox&quot;&gt;维基百科&lt;/a&gt;），大意如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果我们等待一些预先定义好的时间 $t$，然后观察包含 $t$ 的更新区间有多大，我们应该期望它比平均大小的更新区间大。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有关准确的数学描述与理论证明，请参考 &lt;a href=&quot;https://en.wikipedia.org/wiki/Renewal_theory#Inspection_paradox&quot;&gt;维基百科&lt;/a&gt; 或相关论文。&lt;/p&gt;
&lt;p&gt;这里再举一些跟&lt;strong&gt;采样偏差&lt;/strong&gt;相关的现象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;等公交的时候，明明站牌上写每 10 分钟一趟，却感觉等了远不止 10 分钟。&lt;/li&gt;
&lt;li&gt;开车的时候，无论走哪条路，总是感觉经常碰上绿灯。&lt;/li&gt;
&lt;li&gt;总感觉明星挣钱、职业电竞选手挣钱、直播带货挣钱，那是因为挣钱的人曝光量大。&lt;/li&gt;
&lt;li&gt;航空公司经常表示航班上座率不够，公司亏损，可坐飞机时候却经常感觉飞机拥挤。&lt;/li&gt;
&lt;li&gt;Allen Downey：在社交网络上，每名用户拥有 44 名好友，而每名用户的好友平均拥有 104 名好友。&lt;/li&gt;
&lt;li&gt;Allen Downey：联邦监狱每名囚犯的平均判决时间是 3.6 年，但正在服刑的所有囚犯的平均判决时间是 13 年。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有时，我们也会用 &lt;code&gt;幸存者偏差&lt;/code&gt;（&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E5%80%96%E5%AD%98%E8%80%85%E5%81%8F%E8%AA%A4&quot;&gt;维基百科&lt;/a&gt;）来解释这些现象，实际上不管是幸存者偏差还是这里的检查悖论，其实都是因为&lt;strong&gt;统计或采样的方式不同&lt;/strong&gt;。这里就不再展开了。&lt;/p&gt;
&lt;h2&gt;3. 启示&lt;/h2&gt;
&lt;p&gt;这对我们有什么启示？&lt;/p&gt;
&lt;p&gt;首先，从个人角度出发，等车、等蹲坑时间比平均时间确实是客观世界的规律，当遇到这些事情的时候，想开点，放宽心。🙂&lt;/p&gt;
&lt;p&gt;其次，如果作为公司「如厕机制」的设计人，有什么措施能够有效降低等待时间？我们复用前面基于高斯分布的蒙特卡罗模拟方法，计算一下真实等待时间与厕坑数量的关系，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import random

NUM_TESTS = 1000000    # Num of tests
TOILET_TIME = 20       # Average toilet time
TOILET_TIME_SIGMA = 5  # Standard deviation of toilet time

for num_toilets in range(2, 22, 2):

    total_time1 = 0
    for _ in range(NUM_TESTS):
        total_time1 += min([random.random() * TOILET_TIME for _ in range(num_toilets)])

    total_time2 = 0
    for _ in range(NUM_TESTS):
        waiting_time = []
        while len(waiting_time) &amp;#x3C; num_toilets:
            t = random.normalvariate(TOILET_TIME, TOILET_TIME_SIGMA)
            if t &gt; 0:
                waiting_time.append(t)
        total_time2 += min(waiting_time)

    print(f&apos;| {num_toilets} | {total_time1 / NUM_TESTS:.3f} | {total_time2 / NUM_TESTS:.3f} |&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模拟结果如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;center&quot;&gt;厕坑数量&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;均匀蹲坑等待时间（min）&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;高斯蹲坑等待时间（min）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;2&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;6.664&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;17.181&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;4&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;3.999&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;14.858&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;6&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;2.854&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;13.664&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;8&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;2.224&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;12.881&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;10&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;1.823&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;12.311&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;12&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;1.538&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;11.857&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;14&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;1.332&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;11.491&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;16&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;1.177&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;11.178&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;18&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;1.052&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;10.908&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;20&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;0.951&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;10.667&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;均匀蹲坑等待时间降低明显，但高斯蹲坑等待时间似乎降低不多，这里面的差距在哪？多装了坑位，理论上总的蹲坑时长肯定是按比例增加，但是似乎对我们的心理感受提升得却不是很明显？这里其实还忽略了我们的一个很重要的假设：&lt;strong&gt;每次到厕所的时候坑位用满，且无人排队&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;事实上，随着坑位的增加，坑位用满的情况也会降低，举个比较极端的例子，如果坑位从 4 个扩展到 20 个，事实上我们到达的时候往往是有坑位的，等待时间几乎可以变成 0，而这部分人群是无法被采样到的。&lt;/p&gt;
&lt;p&gt;如果要考虑更细致的话，应该将公司楼层总人数、如厕流行时间段等等因素考虑进来，那就变成了一个复杂的规划问题了。不过无论如何，在坑位只有 4 个、等待时间经常要超过 10 分钟甚至 15 分钟来说，增加坑位总是好的。&lt;/p&gt;
&lt;h2&gt;4. 参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://math.stackexchange.com/questions/786392/expectation-of-minimum-of-n-i-i-d-uniform-random-variables&quot;&gt;Mathematics Stack Exchange：Expectation of Minimum of $n$ i.i.d. uniform random variables&lt;/a&gt;（2014，StackExchange）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/29826413/answer/252235849&quot;&gt;你在生活中用过最高端的数学知识是什么？&lt;/a&gt;（2017，王赟 Maigo）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s?__biz=MjM5MTQzNzU2NA==&amp;#x26;mid=2651665006&amp;#x26;idx=1&amp;#x26;sn=050bfa5ae8f5fc7bc9636145dfcfe41b&quot;&gt;公交车总迟到？你大概掉进了等待时间悖论&lt;/a&gt;（2018，李雷 et al.）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s?__biz=MzIwMzg0OTAyOQ==&amp;#x26;mid=2247493862&amp;#x26;idx=1&amp;#x26;sn=f122825a9dd997d031a950376a3e817b&quot;&gt;直播带货能赚大钱吗？公交车为什啥总不来？&lt;/a&gt;（2020，李永乐）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.zhihu.com/question/288093713/answer/585969980&quot;&gt;有哪些数学上的事实，没有一定数学知识的人不会相信？&lt;/a&gt;（2020，知乎回答，有对检查悖论理论证明的截图）&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>