5.3 字符串、分类、日期与单位
实体解析说明了为什么表面形式与含义必须分开。" North "、"NORTH" 和全角 Unicode 拼写可能代表同一区域,而两个相似的客户姓名仍可能属于不同的人。本节将建立可逆规范化管道:保留原始证据,创建比较键,应用经过批准的业务映射,再验证结果。
分阶段、可观察地规范化文本
首先使用专门的字符串 dtype,不要假设 object 就一定表示文本:
zone_raw = orders["zone"].astype("string")pandas 3.x 的默认字符串推断与旧版本不同。在边界处显式声明 dtype,并使用 pd.api.types.is_string_dtype,能跨版本更清楚地表达意图。
一条机械规范化管道可以写成:
orders["zone_raw"] = orders["zone"]
orders["zone_key"] = (
orders["zone_raw"]
.astype("string")
.str.normalize("NFKC")
.str.strip()
.str.replace(r"\s+", " ", regex=True)
.str.casefold()
)每个阶段只有一个窄而明确的目的:
- Unicode NFKC 规范化让全角拉丁字母等兼容形式可以比较。
strip删除首尾空白,正则表达式压缩内部连续空白。
casefold提供比简单小写转换更强的 Unicode 无大小写比较。
这些是比较策略,并非普遍安全的显示转换。对于某些标识符、语言或科学记号,NFKC 可能折叠有意义的区别。因此要保存原值,并检查碰撞:
collisions = pd.crosstab(
orders["zone_key"],
orders["zone_raw"],
dropna=False,
)不要不加区分地删除标点、重音符号或非 ASCII 字符。这样可能抹去重要区别、破坏姓名,并制造虚假匹配。
显式应用业务映射
机械规范化无法知道当前数据集中的 "n." 表示 North。应使用由领域负责人批准、带版本的映射:
zone_mapping = {
"north": "North",
"n.": "North",
"south": "South",
"s.": "South",
}
orders["zone_clean"] = (
orders["zone_key"]
.map(zone_mapping)
.astype("string")
)对于未知键,.map 返回缺失值。这很有用:意外类别保持可见,不会被悄悄塞进“其他”。
unmapped = (
orders.loc[orders["zone_clean"].isna(), ["zone_raw", "zone_key"]]
.drop_duplicates()
)应复核未映射值,更新受控映射,并记录映射版本。除非已经测量模糊纠错的错误率且结果可撤销,否则不要用模糊匹配自动修正类别。
分类 dtype 编码受控业务域
当列来自一个较小的已定义集合,或遵循非字典序时,分类数据类型(categorical dtype)很有用:
service_type = pd.CategoricalDtype(
categories=["standard", "priority", "express"],
ordered=True,
)
orders["service_level"] = orders["service_level"].astype(service_type)现在排序会遵循声明的服务顺序,而不是字母顺序。类别集合以外的值在转换时会变成缺失,因此信任该列之前应检查失败:
failed = (
orders["service_level_raw"].notna()
& orders["service_level"].isna()
)分类类型不只是一项压缩技巧,它属于模式。业务增加合法等级时,类别定义也必须更新。删除“未使用”类别也可能错误,因为模式允许的类别可能只是没有出现在当前样本中。
使用声明的格式和错误策略解析日期
把原始时间戳保存在解析结果旁边:
orders["ordered_at_raw"] = orders["ordered_at"]
orders["ordered_at_utc"] = pd.to_datetime(
orders["ordered_at_raw"],
format="mixed",
errors="coerce",
utc=True,
)如果预期只有一种确定格式,应优先显式声明,而不是使用 format="mixed":
parsed_date = pd.to_datetime(
orders["order_date_raw"],
format="%Y-%m-%d",
errors="coerce",
)errors="coerce" 会把无效值转换为 NaT,便于定位;它不会让错误值变得有效,也无法解释失败原因:
parse_failed = (
orders["order_date_raw"].notna()
& parsed_date.isna()
)
failed_examples = orders.loc[parse_failed, "order_date_raw"]当严格导入边界要求任意畸形值都停止管道时,应使用 errors="raise";当契约包含隔离与复核流程时,可以使用强制转换。
03/04/2026 这样的文本具有歧义,需要来源特定约定。不要根据哪个解释可以成功,就猜测日月顺序。交换时应优先采用 ISO 8601 格式和显式 format。
utc=True 会把带时区输入转换为统一 UTC 表示。无时区时间戳仍需要先明确来源时区,才能转换。若跨日统计依赖本地时间,还要单独保存本地业务日期。
根据标签转换单位,不要根据数值大小猜测
混合单位会制造看似异常的数值。应保留原始值和单位,再映射明确转换因子:
to_km = {
"km": 1.0,
"m": 0.001,
"mi": 1.609344,
}
factor = shipments["distance_unit"].map(to_km)
unknown_unit = factor.isna() & shipments["distance_unit"].notna()
if unknown_unit.any():
raise ValueError(
shipments.loc[unknown_unit, "distance_unit"].unique().tolist()
)
shipments["distance_km"] = shipments["distance_value"] * factor不要假设大于 100 的数值必然是米。120 公里配送完全可能存在,而 3 英里并不等于 3 公里。单位含义来自元数据。
对于温度,转换可能同时需要比例与偏移:
对于货币,转换还需要汇率来源和生效时间戳。没有来源的单个转换因子不具备可复现性。
验证规范结果,同时保留原始证据
规范化后,应检查:
- 原本非缺失、转换后却变成缺失的值。
- 未知类别和单位。
- 多个原始文本折叠为同一规范文本的碰撞。
- 日期范围、时区一致性与解析失败。
- 单位转换后的范围,以及在声明舍入容差内的可逆性。
- 映射与转换规则版本。
下一节会使用这些规范表示调查异常值。一个值可能因为本身错误、单位被误读,或记录了罕见但重要事件,而表现为统计极端。