时间戳#
Arrow/Pandas 时间戳#
Arrow 时间戳存储为 64 位整数,并带有列元数据以关联时间单位(例如毫秒、微秒或纳秒),以及一个可选的时区。Pandas (Timestamp) 使用表示纳秒的 64 位整数和一个可选的时区。没有关联时区的 Python/Pandas 时间戳类型被称为“时区原生”(Time Zone Naive)。具有关联时区的 Python/Pandas 时间戳类型被称为“时区感知”(Time Zone Aware)。
时间戳转换#
Pandas/Arrow ⇄ Spark#
Spark 将时间戳存储为 64 位整数,表示自 UNIX 纪元以来的微秒数。它不会在其时间戳中存储任何关于时区的元数据。
Spark 使用会话本地时区(即 spark.sql.session.timeZone)来解释时间戳。如果该时区未定义,Spark 将转向系统默认时区。为简便起见,下文假设会话本地时区始终已定义。
在往返转换时间戳时,这意味着以下几点:
时区信息会丢失(所有从 Spark 转换为 Arrow/Pandas 得到的时间戳都是“时区原生”的)。
时间戳会被截断为微秒精度。
会话时区可能会对时间戳值的转换产生非直观的影响。
Spark 到 Pandas(通过 Apache Arrow)#
以下情况假设 Spark 配置 spark.sql.execution.arrow.enabled 设置为 "true"。
>>> 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:00
>>> 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|
+-------------------+-------------------+
请注意,感知型时间戳的转换会进行偏移,以反映假设为 UTC 时的相同时间(它代表时间轴上的同一时刻)。对于原生时间戳,Spark 将其视为处于系统本地时区并将其转换为 UTC。回想一下,在内部,Spark 数据帧的架构不会随时间戳存储任何时区信息。
现在,如果会话时区设置为美国太平洋时间 (PST),我们在感知型时区的显示中不会看到任何偏移(它仍然代表时间轴上的同一时刻)。
>>> 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|
+-------------------+-------------------+
再次查看 utc_df.show(),我们可以看到会话时区的影响之一。原生时间戳最初是假设为 UTC 进行转换的,它所反映的时刻实际上比从 PST 转换来的数据帧中的原生时间戳要早。
>>> utc_df.show()
+-------------------+-------------------+
| naive| aware|
+-------------------+-------------------+
|2018-12-31 16:00:00|2019-01-01 00:00:00|
+-------------------+-------------------+
Spark 到 Pandas#
我们可以观察转换回 Arrow/Pandas 时会发生什么。假设会话时区仍然是 PST。
>>> pst_df.show()
+-------------------+-------------------+
| naive| aware|
+-------------------+-------------------+
|2019-01-01 00:00:00|2019-01-01 00:00:00|
+-------------------+-------------------+
>>> pst_df.toPandas()
naive aware
0 2019-01-01 2019-01-01
>>> 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: datetime64[ns](2)
memory usage: ... bytes
注意,除了成为“时区原生”时间戳外,“感知”值在转换为纪元偏移量时现在会有所不同。Spark 的转换过程是先转换为会话时区(如果未设置会话时区,则为系统本地时区),然后进行本地化以移除时区信息。这导致时间戳比原始时间早 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')
>>> (pst_df.toPandas()['aware'][0].timestamp()-pdf['aware'][0].timestamp())/3600
-8.0
当在会话时区为 UTC 时转换数据帧,也会发生同样类型的转换。在这种情况下,原生和感知型时间戳代表了时间轴上不同的时刻(原生时刻的差异是由于创建数据帧时会话时区的变化所致)。
>>> utc_df.show()
+-------------------+-------------------+
| naive| aware|
+-------------------+-------------------+
|2018-12-31 16:00:00|2019-01-01 00:00:00|
+-------------------+-------------------+
>>> utc_df.toPandas()
naive aware
0 2018-12-31 16:00:00 2019-01-01
注意,当会话时区为 UTC 时,不会发生感知型时间戳那种令人惊讶的偏移(但时间戳仍然会变成“时区原生”)。
>>> spark.conf.set("spark.sql.session.timeZone", "UTC")
>>> pst_df.show()
+-------------------+-------------------+
| naive| aware|
+-------------------+-------------------+
|2019-01-01 08:00:00|2019-01-01 08:00:00|
+-------------------+-------------------+
>>> pst_df.toPandas()['aware'][0]
Timestamp('2019-01-01 08:00:00')
>>> pdf['aware'][0]
Timestamp('2019-01-01 00:00:00.000000500-0800', tz='UTC-08:00')
>>> (pst_df.toPandas()['aware'][0].timestamp()-pdf['aware'][0].timestamp())/3600
0.0