如何在 PyArrow、Pandas 与 Spark 之间转换时间戳并理解时区丢失和精度截断
【免费下载链接】arrowApache Arrow is the universal columnar format and multi-language toolbox for fast data interchange and in-memory analytics项目地址: https://gitcode.com/GitHub_Trending/arrow3/arrow
当你在 PyArrow/Pandas 与 PySpark 之间来回传递包含时间列的 DataFrame 时,转换后的时间戳会失去时区信息,并被截断到微秒精度;如果会话时区设置不当,数值本身还会发生偏移。这篇文章基于 Apache Arrow 官方文档 docs/source/python/timestamps.rst 中的完整示例,带你走一遍 Pandas ⇄ Spark(经由 Apache Arrow)的时间戳转换流程,并解释每一步输出为什么是这样。
适用的前提是:你使用 PySpark 的 Arrow 集成(即spark.sql.execution.arrow.enabled配置为"true"),并且关心 aware(带时区)时间戳在往返转换中的行为。
先看三方的时间戳存储模型
理解转换结果的前提是知道各引擎内部怎么存时间:
- Arrow:时间戳存为 64 位整数,靠列元数据关联时间单位(毫秒
ms、微秒us或纳秒ns),并附带一个可选的时区。 - Pandas:
Timestamp使用表示纳秒的 64 位整数,同样带可选时区。 - Spark:时间戳存为表示 UNIX 纪元以来微秒的 64 位整数,不保存任何时区元数据。Spark 按session 时区(即
spark.sql.session.timeZone)解释这些时间戳;如果未设置该配置,则回退到系统默认时区。
由此可以推出往返转换的三个必然结果(文档原文归纳):
- 时区信息丢失——从 Spark 转换回 Arrow/Pandas 的所有时间戳都是 "time zone naive";
- 时间戳被截断到微秒(Spark 内部精度);
- 会话时区的不同设置会对时间戳值的换算产生非直觉的影响。
另外两个术语在文档中反复出现,先对齐一下:不带时区的时间戳类型称为Time Zone Naive,带时区的称为Time Zone Aware。
第一步:准备一个包含 naive 和 aware 两列的 Pandas DataFrame
用下面这段文档中的示例代码构造测试数据:naive列是不带时区的datetime,aware列是带UTC-08:00时区的pandas.Timestamp,且纳秒位为 500(用来观察截断):
import pandas as pd from datetime import datetime, timedelta, timezone pdf = pd.DataFrame({'naive': [datetime(2019, 1, 1, 0)], 'aware': [pd.Timestamp(year=2019, month=1, day=1, nanosecond=500, tz=timezone(timedelta(hours=-8)))]}) pdf文档示例输出:
naive aware 0 2019-01-01 2019-01-01 00:00:00.000000500-08:00Pandas 时间列在转 Arrow 时对应TimestampArray;带时区的时间戳会保留时区信息,例如pandas.rst中的示例里datetime64[us, UTC]会转成timestamp[us, tz=UTC],也就是说 Arrow 侧是能记住时区的——时区丢失发生在进入 Spark 之后。
第二步:设置会话时区并创建 Spark DataFrame
在 Spark 侧创建 DataFrame 前,需要先确认两件事:
spark.sql.execution.arrow.enabled已设为"true"(本文所有 Spark 示例都基于该配置);- 会话时区
spark.sql.session.timeZone已显式设置,否则 Spark 会用系统默认时区解释时间戳,行为取决于运行环境。
先以 UTC 会话时区为例(以下show()输出均为文档示例输出):
from pyspark.sql import SparkSession spark = SparkSession.builder.appName("MyApp").getOrCreate() spark.conf.set("spark.sql.session.timeZone", "UTC") utc_df = spark.createDataFrame(pdf) utc_df.show()+-------------------+-------------------+ | naive| aware| +-------------------+-------------------+ |2019-01-01 00:00:00|2019-01-01 08:00:00| +-------------------+-------------------+注意两点行为:
- aware 列被平移显示:
00:00:00-08:00显示成了08:00:00。这不是数据变了,而是同一时刻在 UTC 下的表示——时区换算到会话时区后,时刻本身不变。 - naive 列按会话时区处理:Spark 把 naive 时间戳当作系统本地时区的时间并换算到 UTC 存储。此时会话时区是 UTC,所以显示不变;这个行为在下一步换时区后会显现出影响。
再切换到美西时区(PST)创建第二个 DataFrame:
spark.conf.set("spark.sql.session.timeZone", "US/Pacific") pst_df = spark.createDataFrame(pdf) pst_df.show()+-------------------+-------------------+ | naive| aware| +-------------------+-------------------+ |2019-01-01 00:00:00|2019-01-01 00:00:00| +-------------------+-------------------+此时 aware 列不再显示平移(同一时刻在 US/Pacific 下就是原始墙钟时间)。但这时如果重新查看刚才在 UTC 会话时区下创建的utc_df,会得到(文档示例输出):
+-------------------+-------------------+ | naive| aware| +-------------------+-------------------+ |2018-12-31 16:00:00|2019-01-01 00:00:00| +-------------------+-------------------+这就是文档强调的"会话时区对值换算的非直觉影响":utc_df的 naive 列当初是按 UTC 解释的,同一数值换到 PST 会话时区下显示时,比pst_df的 naive 时刻早了 8 小时。也就是说,naive 时间戳在不同会话时区下创建的 DataFrame 里,实际代表不同的时刻。
第三步:转回 Pandas 并验证时区丢失
在会话时区仍为 PST 的情况下把pst_df转回 Pandas:
pst_df.toPandas()文档示例输出:
naive aware 0 2019-01-01 2019-01-01用info()检查两列的 dtype:
pst_df.toPandas().info()文档示例输出:
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1 entries, 0 to 0 Data columns (total 2 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 naive 1 non-null datetime64[ns] 1 aware 1 non-null datetime64[ns] dtypes: datetime64ns这就是第一个可核对的结论:两列都变成了不带时区的datetime64[ns]。原始aware列在 Pandas 里是带UTC-08:00的Timestamp,往返后时区信息已经不存在。
进一步对比 epoch 偏移(Spark 先转换到会话时区,再 localise 掉时区信息,结果是该时间戳比原始时刻早了 8 小时):
pst_df.toPandas()['aware'][0]文档示例输出:
Timestamp('2019-01-01 00:00:00')与原始值对比:
pdf['aware'][0]Timestamp('2019-01-01 00:00:00.000000500-0800', tz='UTC-08:00')计算两者 epoch 差值(小时):
(pst_df.toPandas()['aware'][0].timestamp()-pdf['aware'][0].timestamp())/3600文档示例输出:-8.0
也就是说会话时区为 PST 时,aware 时间戳转回 Pandas 后比原始时刻少了 8 小时。
而在会话时区为 UTC 时,同一检查得到的差值是0.0(文档示例输出)——此时 aware 列不会发生这个"意外平移",但注意它同样变成了 time zone naive:
spark.conf.set("spark.sql.session.timeZone", "UTC") pst_df.toPandas()['aware'][0]Timestamp('2019-01-01 08:00:00')所以判断方法可以归纳为:转换后先用info()确认列是否变成不带时区的datetime64[ns],再用上面的 epoch 差值公式确认数值偏移量是否符合会话时区与原始时区的差。
精度截断在哪里发生
示例中aware值带有nanosecond=500(原始值显示为00:00:00.000000500-08:00),但进入 Spark 后show()与转回 Pandas 的结果中都只剩下00:00:00——纳秒部分被丢掉了,这正是"Timestamps are truncated to microseconds"的具体体现:Spark 内部精度是微秒,任何微秒以下的分量在往返后不可恢复。文档对 Spark 往返部分没有提供额外的截断告警或绕过选项,需要纳秒精度时应避免经过 Spark。
如果你不走 Spark 的内存通道,而是用 Parquet 文件在框架间交换数据,PyArrow 文档 parquet_type_handling.rst 对时间戳写入给出了对应的控制项,可以作为可选分支了解:
- 默认写 Parquet 1.0 文件时,纳秒会被 cast 到微秒;用
coerce_timestamps='ms'可指定目标精度:
pq.write_table(table, 'example.parquet', coerce_timestamps='ms')- 低精度 cast 可能丢数据时默认抛异常,传
allow_truncated_timestamps=True可抑制:
pq.write_table(table, 'example.parquet', coerce_timestamps='ms', allow_truncated_timestamps=True)- Parquet 2.6 格式可以不 cast 保存纳秒时间戳,但文档同时提醒许多 Parquet 读取器尚不支持该版本,跨框架兼容时推荐默认的 1.0;
- 部分旧版 Spark(以及 Impala)使用已废弃的
INT96存储时间戳,如需向这些读取器写文件,在write_table中设置use_deprecated_int96_timestamps=True:
pq.write_table(table, 'example.parquet', use_deprecated_int96_timestamps=True)限制与结论
结合 timestamps.rst 的说明,这条集成路径有明确的边界:
- 经 Spark 往返后,时间戳必然变成 time zone naive,且被截断到微秒;这两点无法通过配置规避。
- 数值是否偏移、偏移多少,取决于
spark.sql.session.timeZone与原始时区的关系。示例中 PST 会话时区得到-8.0小时的 epoch 差,UTC 会话时区得到0.0;naive 时间戳在不同会话时区下创建时甚至代表不同时刻,跨时区环境下尤其要小心。 - 若需要保留时区或纳秒精度,应绕过 Spark 的 Arrow 通道,在 PyArrow/Pandas 侧直接处理,或用支持
timestamp[us, tz=...]的 Parquet 2.6 格式做文件级交换(注意读取器支持范围)。
【免费下载链接】arrowApache Arrow is the universal columnar format and multi-language toolbox for fast data interchange and in-memory analytics项目地址: https://gitcode.com/GitHub_Trending/arrow3/arrow
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考