大数据文摘出品
编译:李雷、小蒋、钱天培
如果你经常坐公交车,相信下面这一场景对你绝不陌生。
你到了车站,准备搭乘声称每10分钟一班的公交车。你盯着你的手表留意着时间,结果公交车终于在11分钟后到来。
这时你不由得感叹:为什么今天运气这么差!
想想也是。如果公交车每10分钟一班,而你到达的时间是随机的,那么你的平均等待时间难道不是5分钟嘛?
但实际上,等待公交车的时间似乎永远要比你预估的久。
究竟是你错了?还是公交运营系统出了问题?
事实证明,在一些合理的假设下,你可以得出一个惊人的结论:
在等待平均10分钟一班的公交车时,你的平均等待时间将为10分钟。
这就叫等待时间悖论。
等待时间悖论
如果公交车精确每10分钟来一辆,那么你的平均等待时间就是这个间隔的一半:5分钟。
可是,如果我们给这个10分钟加上一点随机成分呢?
这时,等待时间悖论就出现了。
等待时间悖是检验悖论的一种。那么,什么是检验悖论呢?
简言之,只要观察量的概率与观察量有关,就会出现检验悖论。比如说,我们做了一个调查大学生班级平均人数的调查。虽然学校确实保证每班平均有30名学生,但实际调查下来的平均班级规模通常会大得多。
原因是,较大的班级中就有更多的学生,因此在计算学生的平均体验时,你会对大班进行过度地抽样。极端得讲,如果有一个班一个学生也没有,那你压根不会抽样到这个班级的学生。
对于通常10分钟一班的公交线路,有时两班车的间隔会超过10分钟,有时则短点。如果你在随机时间到达,那你会有更多机会遇到更长的等待间隔,而不是较短的。
因此,乘客所经历的平均等待时间间隔将比公交车之间的平均到达时间间隔更长,因为较长的间隔是被过度采样了的。
但等待时间悖论提出了一个比这更震撼的主张。
当两班车的平均间隔是N分钟时,搭乘者所经历的平均等待时间也是N分钟,而非N/2分钟。
这是真的吗?
模拟等待时间
为了证明等待时间悖论的合理性,让我们首先模拟平均每10分钟到达一班的公交车流。
我们将模拟大量的公交车到达的情况:100万辆(或大约19年中全天不间断的10分钟来一辆车的间隔),以保证实验的准确性。
import numpy as np
N = 1000000 # number of buses
tau = 10 # average minutes between arrivals
rand = np.random.RandomState(42) # universal random seed
bus_arrival_times = N * tau * np.sort(rand.rand(N))
为了确认我们做的是对的,让我们检查一下平均间隔是否接近τ= 10:
intervals = np.diff(bus_arrival_times)
intervals.mean()
输出:
9.9999879601518398
通过模拟这些公交车到达,我们现在可以模拟大量乘客在此期间到达公交车站,并计算他们每个人经历的等待时间。让我们将它封装在一个函数中供以后使用:
def simulate_wait_times(arrival_times,
rseed=8675309, # Jenny's random seed
n_passengers=1000000):
rand = np.random.RandomState(rseed)
arrival_times = np.asarray(arrival_times)
passenger_times = arrival_times.max() * rand.rand(n_passengers)
# find the index of the next bus for each simulated passenger
i = np.searchsorted(arrival_times, passenger_times, side='right')
return arrival_times[i] - passenger_times
然后我们可以模拟一些等待时间并计算平均值:
wait_times = simulate_wait_times(bus_arrival_times)
wait_times.mean()
输出:
10.001584206227317
平均等待时间接近10分钟。正如等待时间悖论预测的那样。
深入挖掘:概率和泊松过程
我们如何理解这一现象呢?
从本质上说,这是检验悖论的一个例子,其中观察值的概率与观察值本身有关。 让我们用p(T)表示公交车到达车站时间隔T的分布。 在这种表示法中,到达时间的期望值是:
在上面的模拟中,我们选择了E [T] =τ= 10分钟。
当乘客随机到达公交车站时,他们所经历的时间间隔的概率将受到p(T)的影响,但也受到T本身的影响:间隔时间越长,乘客遇到这一间隔的概率就越大。
所以我们可以得出乘客所经历的到达时间分布:
比例常数来自正态化分布:
与上面相比,我们可以将它简化为
预计等待时间E [W]将是乘客所经历的预期间隔的一半,所以我们可以写作
或者可以写得更清楚一点:
现在,让我们为p(T)选择一个表格并计算积分。
如果我们这种公式推导可行,那用于p(T)的合理分布是什么?
我们可以通过绘制两班车间隔的直方图来获得模拟到达中的p(T)分布的图片:
%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn')
plt.hist(intervals, bins=np.arange(80), density=True)
plt.axvline(intervals.mean(), color='black', linestyle='dotted')
plt.xlabel('Interval between arrivals (minutes)')
plt.ylabel('Probability density');
这里的垂直虚线表示平均的间隔大约为10分钟。这看起来非常像指数分布,而且并非偶然:我们将公交车的到达时间模拟为均匀随机数,这非常接近于泊松过程,对于这样的过程,可以证明到达之间的间隔分布是呈指数分布的。
注:实际上,在区间Nτ内均匀采样N个点,点之间的间隔T遵循β分布:T /(Nτ)〜Bet [1,N],当N很大的时候这个极限趋于T~Exp [1 /τ]。
区间的指数分布意味着到达时间遵循泊松过程。
通过再次检查这个推断,我们可以确认它与泊松过程的另一个属性的相匹配:在固定时间范围内到达公交的数量将是泊松分布的。让我们将模拟到达的时间按小时分桶检查一下:
from scipy.stats import poisson
# count the number of arrivals in 1-hour bins
binsize = 60
binned_arrivals = np.bincount((bus_arrival_times // binsize).astype(int))
x = np.arange(20)
# plot the results
plt.hist(binned_arrivals, bins=x - 0.5, density=True, alpha=0.5, label='simulation')
plt.plot(x, poisson(binsize / tau).pmf(x), 'ok', label='Poisson prediction')
plt.xlabel('Number of arrivals per hour')
plt.ylabel('frequency')
plt.legend();
经验值和理论值紧密匹配,这让我们相信我们的解释是正确:对于大N,柏松过程可以很好地描述我们模拟的公交到达时间,其到达间隔是指数分布的。
这意味着概率分布如下:
将此概率分布代入上面的公式,我们发现一个人的平均等待时间为
乘客的预期等待时间与公交到达的平均间隔相同!
一种补充的推断方式是:泊松过程是一个无记忆过程,这意味着事件发生的历史情况与下一个事件的预期时间无关。所以当你到达公交站后,等到下一班公交的平均等待时间总是一样的:在我们的案例中,它是10分钟,这与上一班车走了多久无关!
同样的原理,你已经等待了多久并不重要:下一辆公交预计的到达时间总是10分钟:对泊松过程来说,你花费在等待的时间没用。
实际的等待时间
如果通过泊松过程确实描述了真实世界的公交到达时间,上述分析是正确的,但事实真的如此吗?
为了确定等待时间悖论是否描述了现实情况,我们深入研究了一些可供下载的数据:arrival_times.csv(3MB的CSV文件)
https://gist.githubusercontent.com/jakevdp/82409002fcc5142a2add0168c274a869/raw/1bbabf78333306dbc45b9f33662500957b2b6dc3/arrival_times.csv
该数据集包含2016年第二季度记录的西雅图市中心3rd & Pike公交站的西雅图Rapid Ride C、D、E线的预定和实际到达时间。
import pandas as pd
df = pd.read_csv('arrival_times.csv')
df = df.dropna(axis=0, how='any')
df.head()
我特意选择Rapid Ride路线的数据是因为,在一天的大部分时间里,公交车的间隔很规律,通常在10到15分钟之间。
首先,让我们进行一下数据清洗,将其转换为更易于使用的表单:
# combine date and time into a single timestamp
df['scheduled'] = pd.to_datetime(df['OPD_DATE'] + ' ' + df['SCH_STOP_TM'])
df['actual'] = pd.to_datetime(df['OPD_DATE'] + ' ' + df['ACT_STOP_TM'])
# if scheduled & actual span midnight, then the actual day needs to be adjusted
minute = np.timedelta64(1, 'm')
hour = 60 * minute
diff_hrs = (df['actual'] - df['scheduled']) / hour
df.loc[diff_hrs > 20, 'actual'] -= 24 * hour
df.loc[diff_hrs < -20, 'actual'] += 24 * hour
df['minutes_late'] = (df['actual'] - df['scheduled']) / minute
# map internal route codes to external route letters
df['route'] = df['RTE'].replace({673: 'C', 674: 'D', 675: 'E'}).astype('category')
df['direction'] = df['DIR'].replace({'N': 'northbound', 'S': 'southbound'}).astype('category')
# extract useful columns
df = df[['route', 'direction', 'scheduled', 'actual', 'minutes_late']].copy()
df.head()
该表中主要有六个不同的数据集:C、D和E线的北行和南行。为了了解它们的特性,让我们绘制这六条线路的实际与预定到达时间差的直方图:
import seaborn as sns
g = sns.FacetGrid(df, row="direction", col="route")
g.map(plt.hist, "minutes_late", bins=np.arange(-10, 20))
g.set_titles('{col_name} {row_name}')
g.set_axis_labels('minutes late', 'number of buses');
你可能会认为公交车每次在行程开始时与其时间表更接近,并且在快结束时有更多的差异,这在数据中得到了证实:南行(southbound)C线和北行(northbound) D线、E线都在各自路线的起点接近时间表,而其反方向在终点时更接近。
接下来让我们来看看这六条路线观察和预计的到达时间间隔。我们首先使用Pandas 的groupby功能分别计算这些间隔:
def compute_headway(scheduled):
minute = np.timedelta64(1, 'm')
return scheduled.sort_values().diff() / minute
grouped = df.groupby(['route', 'direction'])
df['actual_interval'] = grouped['actual'].transform(compute_headway)
df['scheduled_interval'] = grouped['scheduled'].transform(compute_headway)
g = sns.FacetGrid(df.dropna(), row="direction", col="route")
g.map(plt.hist, "actual_interval", bins=np.arange(50) + 0.5)
g.set_titles('{col_name} {row_name}')
g.set_axis_labels('actual interval (minutes)', 'number of buses');
可以很清楚看出,这并不像我们模型的指数分布形式,此外,分布可能受到非恒定的预定到达间隔的影响。
让我们重复上面的图表,查看预定到达间隔的分布:
这表明公交车在整个星期都有不同的到达时间间隔,所以我们无法从原始到达时间数据的分布来评估等待时间悖论的准确性。
g = sns.FacetGrid(df.dropna(), row="direction", col="route")
g.map(plt.hist, "scheduled_interval", bins=np.arange(20) - 0.5)
g.set_titles('{col_name} {row_name}')
g.set_axis_labels('scheduled interval (minutes)', 'frequency');
构建均匀分布的时间表
即使预定的到达间隔不均匀,也有一些特定的间隔有大量到达的数据:例如,有近2000个北行E线的预定间隔为10分钟。为了探索等待时间悖论是否适用,让我们按路线、方向和预定间隔对数据进行分组,然后将这些近似的到达时间重新堆叠在一起,就像它们按顺序发生的一样。这应该保持了原始数据所有的相关特征,同时更容易直接与等待时间悖论的预测比较。
def stack_sequence(data):
# first, sort by scheduled time
data = data.sort_values('scheduled')
# re-stack data & recompute relevant quantities
data['scheduled'] = data['scheduled_interval'].cumsum()
data['actual'] = data['scheduled'] + data['minutes_late']
data['actual_interval'] = data['actual'].sort_values().diff()
return data
subset = df[df.scheduled_interval.isin([10, 12, 15])]
grouped = subset.groupby(['route', 'direction', 'scheduled_interval'])
sequenced = grouped.apply(stack_sequence).reset_index(drop=True)
sequenced.head()
使用这些清理过的数据,我们可以绘制不同路线、方向和到达频率的“实际”到达间隔的分布:
for route in ['C', 'D', 'E']:
g = sns.FacetGrid(sequenced.query(f"route == '{route}'"),
row="direction", col="scheduled_interval")
g.map(plt.hist, "actual_interval", bins=np.arange(40) + 0.5)
g.set_titles('{row_name} ({col_name:.0f} min)')
g.set_axis_labels('actual interval (min)', 'count')
g.fig.set_size_inches(8, 4)
g.fig.suptitle(f'{route} line', y=1.05, fontsize=14)
我们看到,每条路线和时间表的观测到达间隔的分布接近高斯分布,在预定的到达间隔附近达到峰值,并且在路线开始附近具有较小的标准差(C的南行(southbound),D / E的北行(northbound)),以及在路线结束附近有更大的标准差。
即使不经过统计测试,我们也可以清楚地看到,实际的到达时间间隔肯定不是指数分布的,因而等待时间悖论所依赖的基本假设并不成立。
我们可以利用上面使用的等待时间模拟功能来找到每条公交路线、方向和时间表的平均等待时间:
grouped = sequenced.groupby(['route', 'direction', 'scheduled_interval'])
sims = grouped['actual'].apply(simulate_wait_times)
sims.apply(lambda times: "{0:.1f} +/- {1:.1f}".format(times.mean(), times.std()))
输出:
平均等待时间可能比预定时间间隔的一半长上一两分钟,但不等于等待时间悖论所暗示的预定时间间隔。换句话说,检验悖论得到了证实,但等待时间悖论似乎与现实不符。
结论
等待时间悖论是个非常有趣的现象。它涵盖了模拟、概率以及统计假设与现实的比较。
虽然我们确认了,现实世界的公交线路确实遵循了一些版本的检验悖论,但上面的分析非常明确地显示,等待时间悖论背后的核心假设(公交车的到达时间遵循泊松过程)并不是很有根据。
回想起来,这也并不令人惊讶:泊松过程是一个无记忆过程,它假设到达的概率完全独立于自上次到达的时间。实际上,一个运行良好的公交系统将有一个有意安排的时间表,用以避免这种行为:公交车不会在一天中的随机时间开始他们的路线,而是按照选择能够最佳服务公众的时间表开始他们的路线。
这里更大的教训是,你应该谨慎对待任何数据分析工作的假设。泊松过程可以良好地描述到达时间的数据 – 但只是在某些特定情况下。
仅仅因为一种类型的数据看起来像另一种类型的数据,并不能推导出对一种数据有效的假设必然对另一种有效。
通常那些看似正确的假设可能会导致与现实不符的结论。
最后,你可以在这里下载本文全部代码
http://jakevdp.github.io/downloads/notebooks/WaitingTimeParadox.ipynb
相关报道:
http://jakevdp.github.io/blog/2018/09/13/waiting-time-paradox/?utm_source=mybridge&utm_medium=blog&utm_campaign=read_more