
当 Polars 从 Parquet 文件读取含极小年份(如 0200)的日期时间数据时,若原始数据以纳秒时间戳存储但未正确对齐纪元起始,可能导致严重年份偏移(如 0200-03-01 被误解析为 1953-10-28),本文详解成因、验证方法及生产环境应对策略。
当 polars 从 parquet 文件读取含极小年份(如 `0200`)的日期时间数据时,若原始数据以纳秒时间戳存储但未正确对齐纪元起始,可能导致严重年份偏移(如 `0200-03-01` 被误解析为 `1953-10-28`),本文详解成因、验证方法及生产环境应对策略。
Parquet 文件中的时间戳本质上是整数(自 Unix 纪元 1970-01-01T00:00:00Z 起经过的单位时间数),其语义高度依赖时间单位(time unit) 和 时区上下文。问题中 0200-03-01 被解析为 1953-10-28,并非 Polars 的“错误”,而是底层时间戳数值在纳秒单位下发生了负向整数溢出:
- 0200-03-01 00:00:00 UTC 对应的纳秒级时间戳约为 -55620224640000000000(即约 -55.62 万亿纳秒);
- 若写入 Parquet 时错误地将该值作为无符号或截断的 64 位整数处理,或使用了不兼容的序列化逻辑(如 Java/Spark 默认用纳秒但未保留符号完整性),则实际存入文件的数值可能被扭曲;
- Polars 按标准 int64 解析该扭曲值,并将其解释为自 1970-01-01 起的纳秒偏移,最终映射到 1953-10-28 这一看似“随机”实则可复现的时间点。
✅ 验证是否为单位错配问题:
可通过以下最小复现代码确认行为边界:
import polars as pl
# 正确:显式指定微秒单位 + 极小年份(支持 ISO 格式字符串直读)
s = pl.Series("dt", ["0200-03-01 00:00:00"]).str.to_datetime(time_unit="us")
df = s.to_frame()
df.write_parquet("test_correct.parquet")
assert pl.read_parquet("test_correct.parquet").item(0, 0) == pl.datetime(200, 3, 1)
# 错误模拟:手动构造一个被截断的纳秒时间戳(演示偏移)
# (真实场景中此值来自上游写入缺陷)
corrupted_ns = -55620224640000000000 & 0xFFFFFFFFFFFFFFFF # 模拟无符号截断
df_corrupt = pl.DataFrame({"dt": [corrupted_ns]}).with_columns(
pl.col("dt").cast(pl.Datetime("ns"))
)
print(df_corrupt) # 输出接近 1953-10-28 的时间 → 即问题现象
⚠️ 关键事实澄清:
- 0200 年在 ISO 8601 和 Polars 中完全合法,Polars 原生支持 0001–9999 年范围(甚至更广);
- 问题根源几乎总在上游写入端:Spark、PyArrow 或其他 Parquet 写入器若配置 timestamp_unit="ns" 但未妥善处理远古时间,易触发整数溢出或符号丢失;
- Pandas 表现一致,印证这是 Parquet 格式层的通用约束,非 Polars 特有缺陷。
? 生产环境建议方案:
- 源头修复(首选):要求上游系统改用 time_unit="us"(微秒)写入 Parquet——微秒时间戳对 0001–9999 年覆盖更安全,且主流库默认兼容性更好;
-
读取时绕过自动解析:若无法修改上游,强制将日期列读为 pl.String,再按需解析(牺牲性能换取准确性):
df = pl.read_parquet( "s3://bucket/file.parquet", storage_options=storage_options, columns=["start_date"], # 只读关键列 dtypes={"start_date": pl.String} # 强制字符串类型 ).with_columns( pl.col("start_date").str.to_datetime(strict=False) # strict=False 容忍格式异常 ) -
校验与告警:在 ETL 流程中加入年份范围检查:
df = pl.read_parquet(...) invalid_years = df.filter(~pl.col("start_date").dt.year().is_between(1, 9999)) if len(invalid_years) > 0: raise ValueError(f"Found {len(invalid_years)} records with out-of-range years")
总结:此类问题本质是跨系统时间语义对齐失败。与其在读取侧“修复”已损坏的时间戳,不如推动上游统一使用 microsecond 时间单位并启用 allow_negative 等健壮选项。Polars 提供了足够的类型控制能力,但数据契约的可靠性永远始于写入端。











