将大熊猫时区感知的 DateTimeIndex 转换为初始时间戳,但在特定时区

您可以使用函数 tz_localize使 Timestamp 或 DateTimeIndex 时区可感知,但是如何做到相反的事情: 如何将可感知时区的 Timestamp 转换为初始时区,同时保留其时区?

举个例子:

In [82]: t = pd.date_range(start="2013-05-18 12:00:00", periods=10, freq='s', tz="Europe/Brussels")


In [83]: t
Out[83]:
<class 'pandas.tseries.index.DatetimeIndex'>
[2013-05-18 12:00:00, ..., 2013-05-18 12:00:09]
Length: 10, Freq: S, Timezone: Europe/Brussels

我可以通过将时区设置为 Nothing 来删除它,但是结果会被转换为 UTC (12点钟变成10点钟) :

In [86]: t.tz = None


In [87]: t
Out[87]:
<class 'pandas.tseries.index.DatetimeIndex'>
[2013-05-18 10:00:00, ..., 2013-05-18 10:00:09]
Length: 10, Freq: S, Timezone: None

有没有其他方法可以将 DateTimeIndex 转换为时区幼稚,但同时保留它所设置的时区?


一些 背景的原因,我问这个: 我想与时区幼稚的时间序列(以避免额外的麻烦与时区,我不需要他们的情况下,我正在工作)。
但由于某些原因,我必须处理本地时区(欧洲/布鲁塞尔)中的时区感知时间序列。由于我所有的其他数据都是时区天真(但表示在我的本地时区) ,我想转换这个时间序列天真到进一步的工作,但它也必须表示在我的本地时区(所以只是删除时区信息,而不是转换 用户可见时间到 UTC)。

我知道时间实际上是内部存储为 UTC 的,只有当您表示它时才会转换为另一个时区,所以当我想要“去本地化”它时,必须进行某种转换。例如,使用 python datetime 模块,您可以像下面这样“删除”时区:

In [119]: d = pd.Timestamp("2013-05-18 12:00:00", tz="Europe/Brussels")


In [120]: d
Out[120]: <Timestamp: 2013-05-18 12:00:00+0200 CEST, tz=Europe/Brussels>


In [121]: d.replace(tzinfo=None)
Out[121]: <Timestamp: 2013-05-18 12:00:00>

因此,基于此,我可以执行以下操作,但我认为在处理更大的时间序列时,这样做效率不会很高:

In [124]: t
Out[124]:
<class 'pandas.tseries.index.DatetimeIndex'>
[2013-05-18 12:00:00, ..., 2013-05-18 12:00:09]
Length: 10, Freq: S, Timezone: Europe/Brussels


In [125]: pd.DatetimeIndex([i.replace(tzinfo=None) for i in t])
Out[125]:
<class 'pandas.tseries.index.DatetimeIndex'>
[2013-05-18 12:00:00, ..., 2013-05-18 12:00:09]
Length: 10, Freq: None, Timezone: None
190358 次浏览

我认为你不可能以比你提议的更有效的方式达到你的目的。

潜在的问题是时间戳(您似乎已经意识到了)由两部分组成。表示 UTC 时间的数据和时区 tz _ info。当将时区打印到屏幕上时,时区信息仅用于显示目的。在显示时,数据被适当地偏移,并且 + 01:00(或类似的)被添加到字符串中。去掉 tz _ info 值(使用 tz _ Convert (tz = Nothing))实际上并不会改变表示时间戳初始部分的数据。

因此,唯一的方法是修改底层数据(熊猫不允许这样做... ... DatetimeIndex 是不可变的——请参阅 DatetimeIndex 上的帮助) ,或者创建一组新的时间戳对象并将它们包装在一个新的 DatetimeIndex 中。你的解决方案是后者:

pd.DatetimeIndex([i.replace(tzinfo=None) for i in t])

作为参考,以下是 Timestampreplace方法(见 tslib.pyx) :

def replace(self, **kwds):
return Timestamp(datetime.replace(self, **kwds),
offset=self.offset)

您可以参考 datetime.datetime上的文档,看到 datetime.datetime.replace也创建了一个新对象。

如果可以,为了提高效率,最好的办法是修改数据源,以便它(不正确地)报告没有时区的时间戳。你提到:

我想工作与时区幼稚的时间序列(以避免额外的麻烦与时区,我不需要他们的情况下,我正在工作)

我很好奇你说的额外麻烦是什么。作为所有软件开发的一般规则,我建议使用 UTC 表示时间戳“初始值”。没有什么比看到两个不同的 int64值想知道它们属于哪个时区更糟糕的了。如果您始终、始终、始终使用 UTC 作为内部存储,那么您将避免无数令人头疼的问题。我的口头禅是 时区仅用于人工 I/O

根据地方检察官的建议“ 唯一的方法就是修改基础数据”并使用 numpy 修改基础数据..。

这对我有用,而且很快:

def tz_to_naive(datetime_index):
"""Converts a tz-aware DatetimeIndex into a tz-naive DatetimeIndex,
effectively baking the timezone into the internal representation.


Parameters
----------
datetime_index : pandas.DatetimeIndex, tz-aware


Returns
-------
pandas.DatetimeIndex, tz-naive
"""
# Calculate timezone offset relative to UTC
timestamp = datetime_index[0]
tz_offset = (timestamp.replace(tzinfo=None) -
timestamp.tz_convert('UTC').replace(tzinfo=None))
tz_offset_td64 = np.timedelta64(tz_offset)


# Now convert to naive DatetimeIndex
return pd.DatetimeIndex(datetime_index.values + tz_offset_td64)

显式地设置索引的 tz属性似乎有效:

ts_utc = ts.tz_convert("UTC")
ts_utc.index.tz = None

为了回答我自己的问题,这个功能已经被添加到熊猫同时。从 从熊猫0.15.0开始,可以使用 tz_localize(None)删除导致本地时间的时区。
参见 whatsnew 条目: 《时区处理改进》 http://pandas.pydata.org/pandas-docs/stable/whatsnew.html#timezone-handling-improvements

以上面的例子为例:

In [4]: t = pd.date_range(start="2013-05-18 12:00:00", periods=2, freq='H',
tz= "Europe/Brussels")


In [5]: t
Out[5]: DatetimeIndex(['2013-05-18 12:00:00+02:00', '2013-05-18 13:00:00+02:00'],
dtype='datetime64[ns, Europe/Brussels]', freq='H')

使用 tz_localize(None)删除导致 天真的当地时间的时区信息:

In [6]: t.tz_localize(None)
Out[6]: DatetimeIndex(['2013-05-18 12:00:00', '2013-05-18 13:00:00'],
dtype='datetime64[ns]', freq='H')

此外,您还可以使用 tz_convert(None)删除时区信息,但是可以转换为 UTC,因此产生 天真的协调世界时时间:

In [7]: t.tz_convert(None)
Out[7]: DatetimeIndex(['2013-05-18 10:00:00', '2013-05-18 11:00:00'],
dtype='datetime64[ns]', freq='H')

这是 更有表现力datetime.replace解决方案多得多:

In [31]: t = pd.date_range(start="2013-05-18 12:00:00", periods=10000, freq='H',
tz="Europe/Brussels")


In [32]: %timeit t.tz_localize(None)
1000 loops, best of 3: 233 µs per loop


In [33]: %timeit pd.DatetimeIndex([i.replace(tzinfo=None) for i in t])
10 loops, best of 3: 99.7 ms per loop

最重要的是在定义 datetime 对象时添加 tzinfo

from datetime import datetime, timezone
from tzinfo_examples import HOUR, Eastern
u0 = datetime(2016, 3, 13, 5, tzinfo=timezone.utc)
for i in range(4):
u = u0 + i*HOUR
t = u.astimezone(Eastern)
print(u.time(), 'UTC =', t.time(), t.tzname())

因为我总是很难记住,一个快速的总结,其中每一个做什么:

>>> pd.Timestamp.now()  # naive local time
Timestamp('2019-10-07 10:30:19.428748')


>>> pd.Timestamp.utcnow()  # tz aware UTC
Timestamp('2019-10-07 08:30:19.428748+0000', tz='UTC')


>>> pd.Timestamp.now(tz='Europe/Brussels')  # tz aware local time
Timestamp('2019-10-07 10:30:19.428748+0200', tz='Europe/Brussels')


>>> pd.Timestamp.now(tz='Europe/Brussels').tz_localize(None)  # naive local time
Timestamp('2019-10-07 10:30:19.428748')


>>> pd.Timestamp.now(tz='Europe/Brussels').tz_convert(None)  # naive UTC
Timestamp('2019-10-07 08:30:19.428748')


>>> pd.Timestamp.utcnow().tz_localize(None)  # naive UTC
Timestamp('2019-10-07 08:30:19.428748')


>>> pd.Timestamp.utcnow().tz_convert(None)  # naive UTC
Timestamp('2019-10-07 08:30:19.428748')

当系列中有多个不同的时区时,公认的解决方案不起作用。它抛出 ValueError: Tz-aware datetime.datetime cannot be converted to datetime64 unless utc=True

解决方案是使用 apply方法。

请参阅以下例子:

# Let's have a series `a` with different multiple timezones.
> a
0    2019-10-04 16:30:00+02:00
1    2019-10-07 16:00:00-04:00
2    2019-09-24 08:30:00-07:00
Name: localized, dtype: object


> a.iloc[0]
Timestamp('2019-10-04 16:30:00+0200', tz='Europe/Amsterdam')


# trying the accepted solution
> a.dt.tz_localize(None)
ValueError: Tz-aware datetime.datetime cannot be converted to datetime64 unless utc=True


# Make it tz-naive. This is the solution:
> a.apply(lambda x:x.tz_localize(None))
0   2019-10-04 16:30:00
1   2019-10-07 16:00:00
2   2019-09-24 08:30:00
Name: localized, dtype: datetime64[ns]


# a.tz_convert() also does not work with multiple timezones, but this works:
> a.apply(lambda x:x.tz_convert('America/Los_Angeles'))
0   2019-10-04 07:30:00-07:00
1   2019-10-07 13:00:00-07:00
2   2019-09-24 08:30:00-07:00
Name: localized, dtype: datetime64[ns, America/Los_Angeles]

后期的贡献,但只是偶然发现类似的 Python datetime 和 Pandas 为同一日期提供不同的时间戳

如果你在 pandas中有时区感知的日期时间,那么在这个上下文中 从技术上讲,tz_localize(None)更改 POSIX 时间戳(在内部使用) ,就好像时间戳的本地时间是 UTC 一样。 本地就是 在指定时区的本地。例如:

import pandas as pd


t = pd.date_range(start="2013-05-18 12:00:00", periods=2, freq='H', tz="US/Central")
# DatetimeIndex(['2013-05-18 12:00:00-05:00', '2013-05-18 13:00:00-05:00'], dtype='datetime64[ns, US/Central]', freq='H')


t_loc = t.tz_localize(None)
# DatetimeIndex(['2013-05-18 12:00:00', '2013-05-18 13:00:00'], dtype='datetime64[ns]', freq='H')


# offset in seconds according to timezone:
(t_loc.values-t.values)//1e9
# array([-18000, -18000], dtype='timedelta64[ns]')

请注意,这将留给您 在夏令时转换期间发生的奇怪事情,例如。

t = pd.date_range(start="2020-03-08 01:00:00", periods=2, freq='H', tz="US/Central")
(t.values[1]-t.values[0])//1e9
# numpy.timedelta64(3600,'ns')


t_loc = t.tz_localize(None)
(t_loc.values[1]-t_loc.values[0])//1e9
# numpy.timedelta64(7200,'ns')

相反,tz_convert(None)不修改内部时间戳,它只是删除 tzinfo

t_utc = t.tz_convert(None)
(t_utc.values-t.values)//1e9
# array([0, 0], dtype='timedelta64[ns]')

我的底线是: 如果你能够或者只能使用不修改底层 POSIX 时间戳的 t.tz_convert(None),那就坚持使用时区感知的日期时间。请记住,那时您实际上是在与 UTC 一起工作。

(Python 3.8.2 x64 on Windows 10,pandas v1.0.5.)

我是如何在欧洲用15分钟的频率日期时间索引来处理这个问题的。

如果您有一个 知道时区(在我的例子中是 Europe/Amsterdam)索引,并希望通过将所有内容转换为本地时间来将其转换为 时区天真索引,那么您将遇到 dst 问题,即

  • 三月的最后一个星期日(欧洲转为夏季时间)将缺少一个小时
  • 10月最后一个星期日(欧洲转为夏季时间)将有1小时的复制

你可以这样处理:

# make index tz naive
df.index = df.index.tz_localize(None)


# handle dst
if df.index[0].month == 3:
# last sunday of march, one hour is lost
df = df.resample("15min").pad()


if df.index[0].month == 10:
# in october, one hour is added
df = df[~df.index.duplicated(keep='last')]

注意: 在我的例子中,我在仅包含一个月的 df上运行上面的代码,因此我使用 df.index[0].month来查找月份。如果你的包含更多的月份,你可能应该索引它不同,以知道什么时候做 DST。

它包括从3月份的最后一个有效值重新采样,以避免丢失1小时(在我的例子中,所有数据都是以15分钟的间隔进行的,因此我就这样重新采样。不管你的时间间隔是多少,重新采样)。十月份,我放弃复制品。