美国每个卫生部门都会公布餐厅检查结果,也有一长串应用试图在你挑选餐厅的那一刻把这些结果摆在你面前。它们的前提是:违规条数能告诉你一些关于后厨的信息。

我本想比较几个辖区,看看各县标准到底差多少。结果发现这种比较根本做不了,而原因比比较本身更有意思。

下面的一切都来自公开的实时 API。代码在这里,只用标准库、不需要 API key 就能重新生成每一个数字。

两个县,一张表,相差 2.65 倍

Boulder County 和 Tri-County(Adams、Arapahoe、Douglas)都向科罗拉多州的开放数据门户发布数据,都使用同一份 56 项的 FDA 食品法典检查表。相同的工具,相同的违规代码 FC01 到 FC56,相同的州。

两个数据集重叠的部分,即 2019 年至 2022 年 10 月的例行检查:

  每次检查的违规条数 n
Boulder County 2.10 1,984
Tri-County 5.57 7,886

我最初的想法是 Tri-County 更严。数据似乎也支持这一点。Tri-County 是覆盖三个县的单一机构,这就把地理与监管者分开了;在其内部,三个县分别是 5.83、5.36 和 5.54。同一机构、跨越两条县界,差距约 9%。而机构之间是 165%。

接着我固定住餐厅。Subway 就是 Subway:相同的企业流程,相同的设备,相同的审核制度。

连锁品牌 Boulder Tri-County
Subway 2.42 4.41
Starbucks 0.65 2.18
Chipotle 0.69 1.94
McDonald’s 1.47 3.11
Taco Bell 1.00 3.36
Wendy’s 0.11 4.47
合计 1.15 3.16

九个连锁品牌,九个都指向同一方向,合计 2.76 倍。那时我确信,科罗拉多州两个相邻卫生部门之间的执法力度存在真实差异。

这个数字是个分数,而我只拿到了分子

Boulder 为检查表的每一项各发布一行,每行带一个状态。Tri-County 每次检查发布一行,带 56 个二元标记。

差别在于每种格式能表达什么。

检查员记录了什么 Boulder Tri-County
已开具违规 OutOut-HighOut-MediumOut-Low 标记 = 1
看过,没问题 In 无法区分
没看 Not Observed 无法区分
此处不适用 Not Applicable 无法区分

Boulder 能说「我查过这一项,没问题」和「我根本没查这一项」。Tri-County 的格式把两者都压成了零。

于是我统计了 Boulder 的检查员实际评估了表格的多大比例:

  表格项数 已评估项数 违规条数 每个已评估项的违规率
Boulder 56.8 24.1 2.10 8.7%
Tri-County 56 无法得知 5.57 若按全部 56 项计则为 9.9%

Boulder 的检查员有 58% 的表格内容根本没碰。用实际查看过的项数作分母,2.65 倍就变成了约 1.14 倍。

即便如此,这仍然夸大了残余差异,因为 Tri-County 的分母无法复原。如果他们的检查员同样只评估约 24 项,他们的真实违规率大约是 23%,是 Boulder 的两倍多,效应的方向就反过来了。Boulder 的比率有上下界,Tri-County 的没有。

马里兰州 Montgomery County 使用一份 15 项的表格,采用同样的三分类词汇,标为「Not Observed」的项占 2% 到 6%,而 Boulder 是 58%。没有全国统一惯例可供归一化,这正是为什么任何只比较已公布条数的人都看不见这个问题。

同样的问题也出现在时间维度上

如果这个数字会随记录惯例而变动,那么只要某个辖区改变了记录方式,它在这个辖区内部也应当变动。

加州 Marin County 公布了 12 年的检查数据。按年份统计的每次例行检查违规条数:

Marin County 按年份的每次例行检查违规条数,以及同一批八名检查员在 2019 年前后的表现 左:序列在 2019 年断裂,此后再未恢复。右:跨越断点两侧工作的每一位检查员都下降了。

2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025
4.08 3.73 4.97 5.02 3.63 1.42 1.21 0.74 0.95 0.93 1.01 1.07

在单个年度边界上从 4.30 降到 1.11。同期检查量还上升了,从 2018 年的 1,351 次例行检查增加到 2019 年的 1,514 次。违规代码词汇没有变化。评级牌项目自 2015 年起就已在运行。

Marin 在每一行都标注了检查员姓名,几乎没有别的辖区这样做。所以我能核查人员是否更换过。在断点两侧各至少有 40 次例行检查的检查员:

检查员 2019 年之前 2019+ 变化
A 7.90 1.42 −6.48
B 5.17 1.27 −3.90
C 4.32 1.10 −3.22
D 3.89 2.09 −1.79
E 3.60 0.91 −2.68
F 2.65 0.90 −1.75
G 1.43 0.81 −0.62
H 1.02 0.93 −0.09

八个人全部下降。合计 −69%。检查员 A 是同一个人做同一份工作,从每次检查 7.90 条违规降到 1.42 条。

再固定住餐厅:在断点两侧都被检查过的 1,059 家商户,从 4.58 降到 1.11。

同样的人。同样的餐厅。同样的代码。违规条数只剩四分之一。

真正变化的东西体现在检查如何被分类上。「未发现问题」的比例几乎没动,从 11-16% 变为 13-26%。被归为重大问题的比例从 23-39% 降到 8-15%,差额被轻微问题吸收,而每次开具违规的检查中逐条列出的违规数从约 4.1-5.8 降到 1.1-2.1。一次检查发现问题的概率大致稳定。每次检查被写下来的问题数量则崩塌了。

核查我看到的是不是一次数据迁移

能推翻这一结论的解释是数据来源。如果 2019 年之前的行来自另一个系统并被迁移进来,那这个断点就是数据集的假象,而不是实务的变化。

三项检验,全部为否:

记录 ID 是连续的。全部 13 年是一个单调递增的序列,且在边界处范围有重叠:2018 年跨 25,039 到 37,081,2019 年跨 24,518 到 42,338。没有重新计数,也没有缺口。

表结构没有变动。inspectorplacardcorrect_by_datecorrected_on_siteinspector_commentsinspection_frequencylicense_number 在 2014 到 2026 的每一年都是 100% 填充。唯一变化的字段是违规代码,从 96-97% 降到 67-90%,而这本身就是结论,不是表结构变化。

抽取指纹变化发生在别处。这个数据集中有些字符串值被字面引号包裹。这一假象在 2024 年及之前占 0% 的行,2025 年占 83%,2026 年占 100%。

最后一点是这里最强的证据。数据管道确实变过,但是在 2025 年,不是 2019 年。如果那 74% 的下降是迁移造成的,格式特征也会随之移动。

它也造成了我遇到的第一个 bug。按检查员分组时若不剥掉这些引号,会得到 46 名检查员。实际有 40 名。其中六人是同一个人出现了两次,一次带引号,一次不带。

像 Marin 这样的断点有多常见

此时一个合理的质疑是:我是专门去找不连续点的,所以找到了;而卫生部门本来就不断修改自己的文书。于是我用同样的方法核查了另外三个长时间序列。

辖区 指标 时间跨度 最大单年变动
科罗拉多州 Boulder County 每次例行检查的违规条数 2013-2025 13 年间从 1.60 到 2.35,无断点
芝加哥 检查不合格比例 2010-2026 16.0% 到 24.9%,缓慢漂移,无断点
纽约市 被标为关键的违规占比 2022-2026 52.4% 到 56.7%,无断点

Marin 是异常值。Boulder 是干净的对照,因为它是同一指标、跨度更长,且逐年变动从未超过约 20%。

关于这张表有两点说明。芝加哥的通过/不通过和纽约市的关键违规占比都是比违规条数更粗的指标,一个部门完全可以改变逐条列举的程度而这两者都不动。另外纽约市公布的序列只能回溯到 2022 年,这几乎算不上检验。

综合起来:像 Marin 这样的断点不是这类数据的常态,但也没有稀少到可以忽略。我手里有长序列的四个辖区中,有一个存在这样的断点,而公布的数据里没有任何东西提示它的存在。

这也加强了科罗拉多州的那组比较。Boulder 在 13 年里保持平稳,意味着 2.65 倍差距中它那一侧不是我恰好选了哪些年份造成的假象。

谁来检查,几乎与是哪家餐厅一样有预测力

芝加哥把它的检查优先级排序模型开源了,一项独立评审发现最大的单一影响来源是由哪位卫生检查员执行检查。那用的是内部数据。Marin 的检查员字段让这一点在公开数据上也可检验。

在 15,888 次例行检查、1,645 家商户和 40 名检查员的范围内,检查员的原始均值从 0.74 到 5.99。其中大部分来自时代因素,因为检查员在不同时间窗工作,而 2019 年的断点正好落在中间。

于是我减去了商户均值,使用被两名或更多不同检查员检查过的 1,453 家商户,然后再减去年份均值:

控制项 检查员之间的差距 标准差
原始 8.1 倍 1.41
减去商户均值 5.18 条违规 1.27
减去商户和年份 3.81 条违规,为 2.22 均值的 172% 0.86

在年份控制下排序被打乱,这正好排除了纯粹的时代解释。控制后最严格的那位检查员是 2025 年上岗的,处在断点的宽松一侧。最宽松的那位工作于 2018-2019 年,处在严格一侧。

对原始序列做粗略的方差分解:商户身份约占 24%,检查员身份约占 21%。

这只是算术去均值,不是拟合模型,所以请把它当作方向和量级,而不是标准误。检查员与年份部分共线,识别力由跨越断点的那八名检查员承担。另外两项发现没有这个问题,因为它们是在精确控制下的原始均值。

结论落在哪里

三个维度,这个计数在三个维度上全部失效。跨辖区,它因一项记录惯例而变动 2.65 倍。跨时间,在同一批人检查同一批餐厅的情况下变动 74%。跨检查员,控制后变动幅度达均值的 172%。

这些都不衡量检查制度是否有效。检查员确实发现真实问题、确实关停真实的后厨,这里的内容对此没有任何正反判断。我衡量的是那个数字,而这个数字恰恰在你把一个与另一个相比时失效,而这正是下游所有人唯一会拿它做的事。

真正的风险位于查询类应用的下游:用违规结果训练的城市预测模型,以及跨地区或跨政策时点比较评级制度的研究。芝加哥的模型就是用这类结果训练的,它自己的审计发现检查员是首要预测因子,而当预测目标的一部分本质上是文书工作时,这正是你会预期的结果。一项跨越某辖区自身记录方式变更的研究会直接撞上 Marin 问题,因为现实静止不动而序列却在移动。

实用版本:违规条数只有在单一辖区内、单一时代内,并针对由谁上门检查做过调整之后,才是可解读的。其他任何情况下都不行。如果你要在这类数据上做开发,第一件要核查的不是那个数字,而是你的数据源能否表达「未观察」,以及这一惯例在你的时间窗内是否保持不变。

这一切都不是餐厅独有的。一个公布的计数记录的是一个机构写下了什么,而我看过的四个部门中就有一个改变了自己的记录方式,却没有对外说明。

如果你知道 Marin County 在 2019 年发生了什么变化,欢迎告诉我,我会更新本文。这里的每个数字都能用代码对实时 API 重新生成,欢迎核查我的工作。