时间戳#

Arrow/Pandas 时间戳#

Arrow 时间戳存储为一个 64 位整数,带有列元数据以关联时间单位(例如毫秒、微秒或纳秒)以及可选的时区。Pandas (Timestamp) 使用一个 64 位整数来表示纳秒,以及一个可选的时区。没有关联时区的 Python/Pandas 时间戳类型称为“时区天真”。具有关联时区的 Python/Pandas 时间戳类型称为“时区感知”。

时间戳转换#

Pandas/Arrow ⇄ Spark#

Spark 将时间戳存储为自 UNIX 纪元以来的微秒为单位的 64 位整数。它不会在其时间戳中存储有关时区的任何元数据。

Spark 使用会话本地时区解释时间戳(即 spark.sql.session.timeZone)。如果该时区未定义,Spark 会转而使用默认系统时区。为了简单起见,以下会话本地时区始终已定义。

这意味着在时间戳的往返处理中存在一些事项

  1. 时区信息会丢失(从 Spark 转换为 Arrow/Pandas 的所有时间戳都是“时区天真”)。

  2. 时间戳会被截断为微秒。

  3. 会话时区可能会对时间戳值的转换产生不直观的影響。

Spark 到 Pandas(通过 Apache Arrow)#

以下情况假设 Spark 配置 spark.sql.execution.arrow.enabled 设置为 "true"

>>> pdf = pd.DataFrame({'naive': [datetime(2019, 1, 1, 0)],
...                     'aware': [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

>>> spark.conf.set("spark.sql.session.timeZone", "UTC")
>>> utc_df = sqlContext.createDataFrame(pdf)
>>> utf_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 = sqlContext.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):
 naive    1 non-null datetime64[ns]
 aware    1 non-null datetime64[ns]
 dtypes: datetime64[ns](2)
 memory usage: 96.0 bytes

请注意,除了成为“没有时间区信息”的日期时间戳外,当转换为 epoch 偏移量时,“有时间区信息”的值现在也会不同。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