数据管道中的模式漂移与语义漂移
当从源数据库或数据集中删除某列时,下一次管道运行通常会失败,因此这个问题很容易被发现。但当某个字段的名称和数据类型保持不变,但其含义发生了变化(这种现象被称为“语义漂移”)时,管道仍可能正常运行,从而导致问题被忽视。 之所以会出现这种情况,是因为数据库模式仅描述了数据的结构,并没有说明这些数据的实际含义。它告诉我们哪些字段存在、它们的数据类型是什么,以及它们是否可以包含空值,但却无法说明时间戳是否使用UTC格式、持久性得分的范围应该是多少,或者空字符串与空值是否有不同的含义。 因此,大多数管道检查机制都是为了检测那些显而易见的错误,比如缺失的列或错误的数据类型,但往往无法发现那些数据表面上看似
当从源数据库或数据集中删除某列时,下一次管道运行通常会失败,因此这个问题很容易被发现。但当某个字段的名称和数据类型保持不变,但其含义发生了变化(这种现象被称为“语义漂移”)时,管道仍可能正常运行,从而导致问题被忽视。
之所以会出现这种情况,是因为数据库模式仅描述了数据的结构,并没有说明这些数据的实际含义。它告诉我们哪些字段存在、它们的数据类型是什么,以及它们是否可以包含空值,但却无法说明时间戳是否使用UTC格式、持久性得分的范围应该是多少,或者空字符串与空值是否有不同的含义。
因此,大多数管道检查机制都是为了检测那些显而易见的错误,比如缺失的列或错误的数据类型,但往往无法发现那些数据表面上看似正确,但其含义或实际值已经发生变化的隐蔽问题。
我想了解这种局限性在实践中究竟会达到什么程度,于是建立了一个小型实验环境,在其中人为制造了九种不同的管道故障情况,并测试了四种检测工具在这些情况下的表现。本文将探讨这些检测工具能够发现哪些问题,以及当某些变化被忽视从而导致数据损坏时会发生什么。
你可以在这里获取相关代码,只需执行几条命令就可以重现这个实验过程。
我们将会讨论的内容:
先决条件
若想自行完成这个实验,你需要具备以下条件:
- Python 3.10或更高版本,以及阅读Python代码的基本能力
- Pydantic v2,通过命令`pip install pydantic`进行安装
- Git,用于克隆代码仓库
- 终端环境,用于运行脚本程序
- 基本的SQL知识,例如`GROUP BY`和`WHERE ... IS NULL`等语句。SQLite已随Python一同提供,因此无需额外安装其他软件
- 对数据管道的基本了解:知道数据管道是从哪里读取数据、如何对这些数据进行处理,以及最终将处理结果存储到哪里
实验环境的运作原理
我故意将这个处理流程设计得非常简单。它从一个JSONL文件中读取1,000条游戏物品清单记录,展开其中嵌套的stats和attributes对象,然后将这些数据加载到SQLite数据库中,并计算出六项指标:总记录数、未装备的物品数量、按稀有度分类的物品数量、各类物品的平均攻击力、每天获得的物品数量以及平均耐久度。我还故意省略了一些验证步骤,这样任何修改都会以与在认为源数据可靠的情况下实际运行该流程时相同的方式影响其结果。
每个修改步骤都会应用一个确定的处理函数,这个函数会接收一条记录并返回修改后的记录,因此所有的处理结果都是可重复的。例如,时区调整功能会将所有UTC时间戳转换为UTC+4时区的时间,并去掉时区偏移量:
def local_time_without_offset(row):
timestamp = row["acquired_at"]
if not timestamp.endswith("Z"):
raise ValueError(f"预期的UTC时间戳应以‘Z’结尾,但实际上是:{timestamp!r}")
utc_time = datetime.fromisoformat(timestamp.removesuffix("Z") + "+00:00")
local_time = utc_time + timedelta(hours=4)
row["acquired_at"] = local_time.replace(tzinfo=None).isoformat()
return row
在应用了每一项修改之后,我都会运行整个处理流程,并将其输出结果保存到另一个SQLite数据库中。然后我会将这个新数据库与使用原始数据生成的数据库进行对比,检查每一行和每一列的数据是否存在差异。这个步骤有助于确认处理流程是否真的产生了正确的结果,而不仅仅是为了验证它是否成功运行了。
通过这种对比方式,我们可以将各种修改效果归为以下四类:
- 明显失败:错误信息会明确指出哪些字段受到了修改的影响。
- 隐藏性失败:处理流程虽然完成了运行,但出现了故障,而错误信息并没有说明具体原因。
- 结果有误但流程仍成功执行:处理流程最终完成了,但计算出的数据与基准值不一致。
- 结果完全正确:新数据库中的数据与基准数据完全一致。
对于每一个经过修改的文件,我都使用了四种检测工具来进行验证。这些检测工具的结构都是相同的:它们会接收基准文件和修改后的文件作为输入,检查其中是否存在差异,并会返回是否发现了问题以及具体的错误信息。这种统一的接口设计使得设置过程更加简单,也便于后续添加更多的检测工具。
这四种检测工具分别是:
- 模式结构对比:比较修改后的文件与基准文件中的字段及数据类型,以检测模式结构是否发生了变化。
- 简单的Pydantic模型验证:根据一些基本规则来验证数据的有效性,并确保
rarity字段只取五个已知的数值。 - 严格的Pydantic模型验证:应用更复杂的验证规则,同样要求
rarity只能取五个已知值,同时检查时间戳是否考虑了时区因素,限制equipped_slot的值也只能是五个已知的数值,并且会拒绝任何未知的顶层字段。这里指的是模型中定义的验证规则,而不是Pydantic框架中的strict=True>模式。 - 统计数据分析:比较修改后的数据与基准数据的统计特征,通过检查各字段的平均值、在不同类别中的分布情况以及空值的出现率等指标,来发现其他检测工具可能遗漏的差异。
理解数据的形态与含义
在本文的后续部分,我们将使用两个术语:数据形态和数据含义。数据形态指的是数据集中的字段、这些字段的路径以及它们所包含的值类型。模式检测器通过检查每个字段及其对应的值类型来获取这些信息。
def walk(obj, prefix, schema):
for key, value in obj.items():
field = f"{prefix}{key}"
if isinstance(value, dict):
walk(value, field + ".", schema)
else:
schema.setdefault(field, set()).add(type(value).__name__)
对于基线情况,这种检测方法会返回如下形式的结果:acquired_at: {'str'}、equipped_slot: {'NoneType', 'str'}以及attributes.durability: {'int'}。
然而,数据含义还包括字典中未包含的所有信息,例如数值的单位、时区、预期取值范围、有效的字符串集合,以及空值的代表意义。由此可见,任何仅关注数据形态的检测工具都存在局限性。由于模式检测器是根据字段路径和数据类型的字典来生成结果的,因此它无法区分那些虽然生成了相同的字典,但实际包含的意义不同的数据集。
在九种变化情况中,有三种并没有改变这个字典的结构:时区调整、新的枚举值以及耐用性的变化。因此,模式检测器并没有对这些变化作出任何标记。
检测工具可以在不同层面上对数据进行分析:
- 形态层面:检查哪些字段存在,以及它们所包含的值类型。
- 值约束层面:检查单个数值是否遵循特定的规则,例如是否需要提供时区信息,或者某个字段的取值范围是否被限定在特定范围内。
- 分布层面:将各种统计指标(如平均值、各类别所占的比例、空值的出现频率)与基线数据进行比较,从而发现数据中的变化。这种方法无需明确规定每个数值的具体含义,就能揭示数据的变化趋势。
- 语义层面:检查数据实际代表的意义,包括单位、时区规则以及空值的含义。这些规则的确定需要基于对数据的深入理解,而且必须由生成和使用这些数据的人员或系统来制定。
前四种检测工具涵盖了前三种分析层次。然而,第四种分析层次超出了这些工具的能力范围——因为一个数据集可能在结构、单个数值以及统计分布方面都符合检测要求,但其实际含义却发生了变化。要真正理解数据的意义,我们需要一些仅凭数据本身无法获得的上下文信息。
结果矩阵
结果矩阵总结了四种检测工具对九种变化情况的反应情况。
for drift in ALL_DRIFTS:
dst = ROOT / "data" / "drifted" / f"{drift.name}.jsonl"
drifted_db = DB_DIR / f"{drift.name}.db"
apply_drift(drift, BASELINE, dst)
result = run_pipeline(dst, db_path=str(drifted_db))
outcome = classify(result, baseline_db, drifted_db, drift.fields)
detected = {
det.name: det.check(str(BASELINE), str(dst))["detected"]
for det in DETECTORS
}
rows.append((drift, outcome, detected, result.get("error")))
它显示了这些变更中哪些被检测到了,哪些被遗漏了,以及处理流程是失败了还是继续使用了错误的数据。
| 变化类型 | 发生了什么变化 | 处理结果 | 。受影响的行数 | >模式差异 | )Pydantic(宽松检查模式) | >Pydantic(严格检查模式) | >统计分析结果 |
|---|---|---|---|---|---|---|---|
rename_rarity |
rarity字段的名称被更改了 |
处理失败,并产生了明显的错误提示 | n/a | 是 | 是 | 是 | 否 |
drop_equipped_slot |
equipped_slot字段被删除了 |
处理失败,并产生了明显的错误提示 | n/a | 是 | 是 | 是 | 否 |
stats_become_list |
stats对象被转换成了数组 |
处理失败,且错误比较复杂 | n/a | 是 | 是 | 是 | 否 |
add_column |
新增了guild字段 |
处理成功,没有出现问题 | 0 | 是 | 否 | 是 | 否 |
type_change_level |
level字段的类型从整数变为了字符串 |
处理成功,没有出现问题 | 0 | 是(仅此一项出现错误) | 否 | 否 | 否 |
empty_equipped_slot |
空字段被替换成了"" |
处理成功,但部分数据发生了损坏 | 205 | 是 | 否 | 是 | 是 |
new_enum_value |
有20行数据的mythic字段被重新标记了 |
处理成功,但部分数据发生了损坏 | 20 | 否 | 是 | 是 | 否 |
local_time_without_offset |
时间戳被调整为UTC+4时区,偏移量被删除了 | 处理成功,但部分数据发生了损坏 | 1,000 | 否 | 否 | 是(仅此一项出现错误) | 否 |
durability_scaled |
耐久性数值被乘以了10 | 处理成功,但部分数据发生了损坏 | 1,000 | 否 | 否 | 否 | 是(仅此一项出现错误) |
| 总共检测到的变更数量 | 6 / 9 | 4 / 9 | 7 / 9 | 2 / 9 |
“是(仅此一项出现错误)”表示只有该检测工具发现了这一变更;“n/a”表示处理流程崩溃,因此没有数据被加载。
处理流程错误
| 变化类型 | 错误描述 | 错误原因 |
|---|---|---|
rename_rarity |
处理失败,并产生了明显的错误提示 | KeyError: 'rarity' |
drop_equipped_slot |
处理失败,并产生了明显的错误提示 | KeyError: 'equipped_slot' |
stats_become_list |
处理失败,且错误比较复杂 | TypeError: list indices must be integers or slices, not str |
严格检查模式的检测结果最好(9项变更中有7项被检测到),其次是模式差异检测(6项),宽松检查模式排在第三位(4项),统计分析模式的检测效果最差(2项)。
但是,每个检测工具都遗漏了一些变化。有三种变化仅被其中一个检测工具发现:严格模式的检测工具发现了时区变更,模式差异检测工具发现了level字段的变化,而统计分析工具则察觉到了数据持久性的调整。
检测数据结构的变化
在九种变化中,有五种改变了数据的结构。这些变化包括将rarity字段的名称更改、删除equipped_slot字段、添加guild字段、用数组替换stats对象,以及将level字段的类型从整数改为字符串。
模式差异检测工具发现了这五种变化中的全部五种;严格模式的检测工具发现了四种,而宽松模式的检测工具只发现了三种。这一事实表明,模式差异检测工具在检测结构变化方面最为有效。
在三种变化中,数据处理流程出现了故障,但其中一些错误信息比其他信息更有用。将rarity字段的名称更改时,会引发KeyError: 'rarity'错误,这个错误信息明确指出了哪个字段缺失了;而将stats对象替换为[attack, defense, speed]数组时,则会出现TypeError: list indices must be integers or slices, not str错误,这个错误信息虽然说明了错误的类型,但并没有指出具体是哪个字段出了问题。要想找出问题的根源,就需要仔细分析相关的处理代码。
分类器会特别突出显示第一种错误;而confusing.It工具则通过检查错误信息中是否提到了受到变化的字段来区分这两种错误。
if result["status"] == "error":
error_text = result["error"].lower()
names_cause = any(f.lower() in error_text for f in drift_fields)
return {
"label": "failed_loudly" if names_cause else "failed_confusingly",
"rows_affected": None,
"loss_cells": 0,
"corrupt_cells": 0,
"extra_rows": 0,
}
Pydantic检测工具也在同一个文件上进行了检测,它们报告的错误信息是stats: Input should be a valid dictionary,这个错误信息直接指出了出问题的字段。
level字段类型的变更再次说明了为什么某些结构变化会无法被检测出来。两种Pydantic模型都接受了这种变化,因为Pydantic的默认设置会将字符串"42"自动转换为整数42;数据处理流程也接受了这种变化,因为SQLite在将数据插入INTEGER类型的列时,也会自动将看起来像数字的文本转换为整数。因此,数据库认为这些数据是符合要求的,分类器也就将这次处理结果标记为passed_clean。
只有模式差异检测工具发现了这种变化,因为它会在任何转换操作发生之前就检查原始数据的类型。这一事实再次说明了在数据被强制转换之后再进行检查的局限性:一旦数据类型发生了改变,原本的变化就无法再被察觉到了。在这个例子中,最简单的检测工具反而发现了一种两种Pydantic模型都忽略了的变化。
实验还表明,并非所有被检测到的变化都会引发问题。添加guild字段,或者将level的值从整数改为字符串,这两种操作都会触发警报。模式差异检测器能够识别出这两种变化,而严格模型则只能检测到添加guild这一变化。不过,这两种变化都没有损坏任何数据行,因为加载步骤只会读取它预期会存在的字段。
这凸显出了区分那些应该中断数据处理流程的变化与那些只需被记录下来的变化的必要性。如果检测器错误地标记了太多无害的变化,用户很可能会开始忽略这些警告信息。
语义变化:错误数据的具体表现形式
其余那些变化并没有改变字段的内容或数据类型,而是改变了这些数值所代表的含义。这类变化更难以被检测出来,但它们确实会对最终结果产生影响。下表展示了每种变化所带来的影响,以及这些变化是否会在数据处理流程的输出结果中体现出来。
| 变化类型 | )受影响的数据行数 | 发生变化的指标 | 变化前 | 变化后 |
|---|---|---|---|---|
| 时间戳调整为UTC+4时区,时差消失 | 1,000 | 每日获取的物品数量 | 1月2日:9件;1月3日:3件 | 1月2日:6件;1月3日:6件 |
空值字段被替换为"" |
205 | 未装备的物品数量 | 205 | 0 |
有20行数据的稀有度被设置为mythic |
20 | 每种稀有度的物品数量 | 普通稀有度:619件;神话稀有度:20件 | 普通稀有度:609件;神话稀有度:20件 |
| 物品的耐久性数值被乘以了10 | 1,000 | 平均耐久性 | 54.78 | 547.85 |
我注意到的一点是,大多数数据的数值看起来仍然很正常。例如,将时间戳向前推进4小时,只会使某些物品的记录日期发生变化;由于4小时相当于1天的六分之一,因此大约有六分之一的物品会出现在不同的日期上。每一天都会有一些物品从当前日期转移到下一天,同时也会从前一天获得一些物品。
物品的总数仍然是1,000件,每日记录的数量变化也非常小。考虑到平均每天会有5.5件物品被记录下来,数量的变化幅度通常也就两三件而已。因此,从9件减少到6件,这种变化很容易被视为正常的波动,而不会被认为是数据问题。
由于没有物品丢失,数据处理流程的总行数检查仍然通过了。任何只比较总数的检查方法也都无法发现这个问题。虽然总数是正确的,但有些物品现在被记录在了错误的日期下。
我还研究了为什么数据处理流程没有报告这个问题。在发生这些变化之后,原始的时间戳2026-01-02T22:30:00+00:00变为了2026-01-03T02:30:00。新的时间戳没有时区偏移,因此datetime.fromisoformat会将其视为普通的日期时间格式进行处理。isoformat()方法在输出结果时也不会加上时区偏移。而SQLite的DATE()>函数会将这种没有时区偏移的时间戳视为UTC时间,从而导致物品的记录日期变成了1月3日,而不是1月2日。
该处理流程在处理时间戳时没有出现任何错误,但将相关数据归入了错误的日期。如果生产者原本输入的是2026-01-03T02:30:00+04:00,那么这个时间戳所表示的时间其实与原始数据相同。SQLite会将其转换为UTC时间,但相关数据仍然会被计入1月2日这一天。
问题的根源在于缺少时区偏移量信息。由于缺乏这一信息,处理流程根本无法判断该时间戳所属的时区,因此就将其视为UTC时间进行处理了。
null被替换为""这一操作也引发了另一个问题:虽然这种替换使得该字段仍然被视为字符串类型,但那些仅用于验证数据类型的检查依然能够通过。
equipped_slot的字段类型从{'NoneType', 'str'}变为了{'str'},这一变化被数据库模式对比工具检测到了。虽然处理流程没有报错,但查询WHERE equipped_slot IS NULL时却没有任何结果返回。因此,仪表板上显示的未装备装备的玩家数量变成了0,而不是205人,这让人误以为所有玩家都已经装备好了装备。
<统计分析结果显示了实际发生的情况:equipped_slot为null的比例从20%降到了0%;equipped_slot为空字符串的比例则从0%上升到了20%。实际上,那些原本为null的值被替换成了空字符串,并非真正的零值。
<新的mythic稀有度等级在处理流程中也没有出现任何问题。有20条记录的稀有度被错误地更改为mythic:其中10条记录的稀有度从common变为mythic(总数从619减少到609),9条记录的稀有度从uncommon变为mythic(总数从236减少到227),还有1条记录的稀有度从rare变为mythic(总数从98减少到97)。查询GROUP BY rarity时,系统也没有对这些变化报错。它只是为mythic这个稀有度等级创建了一行新数据,同时减少了其他稀有度等级的记录数量。对于那些只认识五种原始稀有度等级的仪表板来说,可能会将mythic这一等级忽略掉,或者将其归类到“其他”类别中。
<耐久度的变化最清楚地说明了这样一个问题:数据的结构可能没有发生变化,但其含义却可能完全不同。在原始数据中,耐久度的取值范围是10到100,因此平均值547.85显然是不正确的。但由于该字段的名称和数据类型都没有改变,所以数据库模式对比工具并没有发现这一变化。
<处理流程之所以能够接受这些数值,是因为没有任何机制会检查这些数值是否在预期的范围内。只有那些能够判断耐久度是否应该在10到100之间的检查机制,才能发现这个问题。为了验证这一点,我在Pydantic模型中添加了范围限制条件:
class RangedAttributes(Attributes):
durability: int = Field(ge=10, le=100)
class StrictWithRange(StrictItem):
attributes: RangedAttributes
flagged = 0
for row in drifted:
try:
StrictWithRange(**row)
except ValidationError:
flagged += 1
<这种检查方法共检测出了1,000条异常记录中的985条。剩下的15条记录的原始耐久度值确实是10,但在经过乘以10的缩放处理后,其数值变成了100。由于100仍在允许的范围内,因此这些记录也通过了验证。损害与可见性
那四种数据偏差分别影响了1,000行、205行、20行和1,000行数据,但每种偏差都只改变了管道计算出的六项指标中的其中一项。其余五项指标则保持不变。因此,任何监控该管道的人,除非特意检查受到影响的指标并了解其正常值,否则很可能会忽略这些变化。
主要结论是:数据在很多行中发生变化,但人们所监控的大部分结果并不会因此而改变。只有当人们检查那些受到偏差影响的指标时,才会发现这些问题。
检测数据含义的变化
为了能够发现那些超出数据结构本身变化的范围,我们需要进行一些能够直接检测数值变化的检查。
严格的检测模型会检查这些数值是否遵循特定的规则。它成功发现了时区设置错误,因为AwareDatetime会拒绝那些没有时区信息的时间戳(acquired_at: 输入的值必须包含时区信息)。此外,它还发现了那些被设置为空字符串的字段,因为其Literal规则只允许使用五种预定义的字段名,而""并不符合这些规则。
而宽松的检测模型则没有这么多限制:acquired_at可以接受任何形式的字符串值,equipped_slot可以接受任何字符串或None,甚至也可以接受未知字段。因此,这种模型未能发现时区设置错误、那些被设置为空字符串的字段,以及新添加的列。
严格的检测模型也遗漏了耐用性变化这一情况,因为我没有为该字段定义允许的变化范围。在另一次测试中,我在模型中添加了Field(ge=10, le=100)这个限制条件,这样它就成功发现了那1,000行数据中的985行发生了变化。不过仍有15行被遗漏了,因为这些数据的原始耐用性值就是10,在经过比例调整后仍然为10,因此没有触发检测规则。
统计分析工具通过将数据分布与基准值进行比较来发现异常变化。它是唯一一个成功检测出耐用性变化情况的工具,因为它不需要预先定义变化范围,只需判断平均耐用性值是从54.8增加到了547.9即可。
不过,这种工具也可能会将一些正常的数据变化误认为是异常。为了验证这一点,我将耐用性数值调整了1.02到1.3倍,并设置了四种不同的阈值来检测变化情况。我还用200个通过不同随机数生成的干净数据文件来测试这个工具的准确性,看看它会产生多少次误报。由于基准数据是用相同的随机数生成的,所以我没有将其包含在测试范围内。
| 平均变化阈值 | 仅针对耐用性字段的误报情况 | 针对所有数值型字段的误报情况 | 被检测到的变化情况 |
|---|---|---|---|
| 25% | 0 / 200 | 0 / 200 | 仅1.3倍的变化被检测到 |
| 10% | 0 / 200 | 0 / 200 | 1.1倍及以上的变化被检测到 |
| 5% | 0 / 200 | 23 / 200 (11.5%) | 1.05倍及以上的变化被检测到 |
| 2% | 37 / 200 (18.5%) | 188 / 200 (94%) | 所有测试范围内的变化都被检测到 |
实验结果也说明了为什么并非所有的数值字段都适用于同一个阈值。与“耐久度”这一字段相比,stats.attack、level和player_id这些字段的数值变化幅度要大得多。当使用2%的阈值时,在200个正常的文件中,有188个文件的其中至少一个字段触发了警报;而当阈值设为5%时,这3个字段共导致了23次误报,而“耐久度”这一字段则完全没有引发任何警报。stats.attack这一字段就导致了其中的15次误报。就连player_id这个字段也会触发警报,尽管其平均值本身并没有实际意义。
使用统一的阈值来处理所有数值字段其实是一种简单化的做法,但事实上有些字段的数值变化幅度确实比其他字段大得多,因此根本不应该用它们的平均值来进行检测。
category-share这一检测机制使用了固定的5个百分点作为阈值,因此调整平均值的阈值并不会影响它的检测结果。在200个正常的文件中,有22个文件的检测结果出现了26次警报(占11%)。其中17次警报与equipped_slot这个字段有关,而另外16次警报则与chest或off_hand这些字段有关。
出现这些误报的原因主要有两个。首先,该检测机制在计算每个栏位的占比时,只使用了795条非空数据记录,而不是全部1000条记录,因此每项数据的占比数值的标准误差约为1.4个百分点。
chest>这一字段在基准数据中所占的比例为17.2%,而非预定的20%。因此,只要一个文件的chest字段占比达到22.2%就会触发警报,而这种情况在实际数据中大约发生了6%。相反,off_hand这一字段的基准占比值偏高,为22.3%,这也说明基准数据本身可能包含抽样误差,而这些误差会影响到后续的所有检测结果。
还有一个限制因素源于检测机制的设计本身,并非测量结果本身造成的。虽然我没有针对这种情况进行测试,但代码显示为什么这种错误会被忽略:严格的检测模型确实能够捕捉到时区偏移的问题,因为时间戳会丢失其时区偏移信息;但是数据生成者完全可以通过修改时间戳的值来使其保持Z后缀的形式,从而让所有检测机制都认为这些数据是有效的。严格模型只检查时间戳是否带有时区偏移信息,并不验证时间本身是否正确;而统计分析工具也会忽略时间戳字段,因为这些字段的数值种类太多,无法被当作分类数据进行处理。
这一限制也反映了第四层检测机制的局限性:虽然时间戳看起来是有效的,但它实际上代表的是错误的时间。没有任何一种检测机制会去验证时间戳是否与它实际应该代表的时间相符。
这项实验得出的结论
实验结果表明,将这四种检测机制结合起来使用,能够获得更全面的检测效果,因为每种机制都能捕捉到其他机制所遗漏的变化。
schema diff这种检测方法操作简单,而且在每次数据加载时都可以轻松执行。它成功检测出了9种时间偏移情况中的6种,其中包括level字段的变化——这两种Pydantic模型都因为在进行比较之前先对数值进行了转换,从而错过了这一变化。此外,该检测方法还识别出了那些会导致错误出现的字段,而这些错误在管道系统自身的错误提示中并没有被提及。不过,它只检测出了4种会破坏数据完整性的时间偏移情况中的1种。这种严格的模型及统计分析方法能够有效识别出这四种有害的变化趋势。对于那些规则明确的字段,比如需要考虑时区差异的时间戳、允许的取值范围以及数值区间等,我会使用这种严格模型来进行检测;而对于那些没有明确验证规则的重要指标,则会采用统计检验方法,并根据过去的数据为每个字段设定不同的阈值。
实验结果证明了:对于不同的字段来说,使用同一个阈值是行不通的。在200个数据正常的样本中,统一设定5%的阈值导致了23次误报,而这些误报都发生在与“耐久性”无关的字段上。因此,只有那些平均值具有实际意义的字段,才适合应用统计检验方法,并且阈值应该根据该字段的正常变化范围来设定。
第四层要求数据的生成方和接收方必须对数据的含义有共同的认知。如果生成方在未通知任何人的情况下更改了某个字段的计量单位、时区或允许取值范围,那么接收方就无法正确解读这些数据。因此,双方应该就测量单位、时区以及允许的取值范围等关键规则达成一致。
这个建议是基于我的检测工具无法发现的一些问题而提出的。目前我还没有在实验室中测试过这种方法的实际效果。
局限性
由于这些数据是人工生成的、彼此独立的,因此它们比现实世界中的数据要简单得多——后者往往存在季节性变化或自然增长的趋势,因此误报率可能会比我测量的结果更高。此外,我在实验中使用了一个固定的基准值,因此该基准值中的任何抽样误差都会影响到所有的对比结果;而在实际的生产系统中,通常会使用滚动基准值来进行分析。
我在所有字段上都使用了相同的阈值。虽然根据每个字段的正常变化范围来设定不同的阈值可能会更有效,但我并没有进行这样的测试。另外,我仅仅测试了众多可用工具中的四种检测方法,这些结果只能反映出这些检测方法各自的局限性,并不能说明哪种工具才是最好的。
最后,我将各种数据变更分为“格式上的变化”和“含义上的变化”两类,但实际上有些变更,比如将null替换为空字符串,其实可以归入这两类中的任何一类。
在我的第一次阈值测试中还存在一个错误:我在数据生成器中使用了错误的变量名(用SEED代替了seed),这导致所有20个样本的数据都与基准值完全一致,因此没有产生任何误报。在2%的阈值下出现这样的结果似乎不太可能,所以我检查了代码并找出了这个错误。修正了这个错误后,我还排除了基准值本身所使用的种子值42,因为使用这个值生成的数据总是相同的,永远不可能触发误报。本文中所有的测试结果都是基于修正后的测试结果得出的。
结论
实验结果表明,模式结构的变更并不是导致管道系统出现故障的唯一原因。虽然模式差异分析能够检测出所有五种结构上的变化,但却无法发现那些仅改变字段名称或类型而不影响数据含义的变更。例如,时间戳被错误地设置到了错误的日期,空值被替换成了空字符串,或者耐久性数值增加了十倍——在这些情况下,管道系统虽然能够正常运行,但最终产生的结果却是错误的。
<这四种检测器能够捕捉到不同类型的变化,将它们结合起来使用确实可以提高检测的覆盖范围。然而,仅仅检测到这些变化还不够——还需要进一步验证这些数据是否落在预期的范围内,以及它们是否仍然符合下游计算所依赖的前提条件。
<即使某个数据处理流程顺利完成,也不意味着数据就是正确的。同样重要的是要检查这些数据是否依然保持着其原本的含义,以及下游的计算过程是否仍能产生可靠的结果。 相关文章
如何利用知识图谱来检测金融欺诈行为:一份实用指南
在企业支付工作中,大多数反欺诈架构都是孤立地评估每笔交易的。 当一张发票到达系统时,自动规则引擎会依据静态阈值来检查它:确认金额是否在正常范围内,供应商是否在批准的供应商名单上,以及发送请求的IP地址是否可疑。如果这些单独的属性看起来都符合要求,支付流程就会继续进行。 但这种方法的本质缺陷在于其结构设计。像资金转移链条、循环开票机制或虚假供应商这类复杂的欺诈手段,很少会生成那些能触发单一阈值警报的交易。例如,一家空壳公司开具4,800美元的发票,从而低于5,000美元的双重审批门槛,单独来看这个交易似乎完全合法。但当25家看似独立的供应商都将资金汇入同一个银行账户时,这种协同作案的模式就会暴露
阅读全文
当怀孕改变了用户的正常生理状态时,可穿戴设备该如何做出相应的调整?
你的智能手环或手表具备许多实用功能,比如能够记录你平时的生理数据,并在这些数据发生变化时及时提醒你。 静息心率、心率变异性、体温以及呼吸频率等因素都能帮助形成这些生理数据的画像,不过不同的设备会对这些测量结果进行不同的解读。 在怀孕之前,这些指标通常会保持稳定的状态,但怀孕会改变其中一些指标的正常范围。 我每天早上第一个打开的应用程序就是Oura,因此我很清楚一个评估结果的准确性会对我的一天产生多大的影响。如果“准备程度得分”过低,我会觉得自己做错了什么,但实际上并没有。现在想象一下,在你的身体正在经历一系列正常的生理变化期间,连续几周都看到这样的评估结果会是什么感觉…… 这就是我想在这里讨论
阅读全文
没有屏幕是否意味着数据质量更高?关于无屏幕可穿戴设备的基础知识
智能手表让人们更轻松地获取健康数据。只需快速看一下手腕,就能知道自己的心率、步数、睡眠时长,甚至还能估算出身体的恢复情况。 但显示这些信息仅仅是健康监测的一部分。更重要的是要准确且持续地收集这些数据,而且通常不能干扰人们的日常活动。 正是这一区别使得无屏幕的可穿戴设备得以进入消费市场。智能手环和无屏幕健身追踪器能够在不显示通知或不断要求用户查看手腕的情况下,记录各种健康数据和活动指标。它们会在后台收集信息,而最详细的数据分析结果则可以通过配套的智能手机应用程序获取。 然而,去掉屏幕并不会自动让可穿戴设备变得更准确。如果设备的传感器在应对运动、皮肤接触不良等因素时存在问题,即使它收集了数千次数据
阅读全文
Dropbox介绍了其内部开发的Cookie审计工具是如何在200多个网页平台上检测用户是否同意被收集这些数据的行为的。
Dropbox的工程师们介绍了一种基于Playwright技术的审计工具,该工具能够检测超过200个网页上的用户cookie同意设置相关行为。该工具会针对每个页面执行三项测试:针对美国用户的测试、针对欧盟用户的测试,以及检测全球隐私控制相关设置的测试。Cookie相关的规则并未被编写在代码中;此外还有一款辅助工具可以用于查找当前正在使用的URL地址。每周发布的报告会将可能存在的违规行为与误报区分开来。 作者:丹尼尔·柯蒂斯
阅读全文