Преобразование DateTimeIndex с учетом часового пояса панд в наивную временную метку, но в определенном часовом поясе

Вы можете использовать функцию tz_localize чтобы сделать отметку Timestamp или DateTimeIndex, но как вы можете сделать обратное: как вы можете преобразовать отметку времени с отметкой часового пояса в наивную, сохранив при этом ее часовой пояс?

Пример:

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

Я мог удалить часовой пояс, установив для него значение Нет, но затем результат конвертируется в 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

9 ответов

Решение

Чтобы ответить на мой собственный вопрос, эта функциональность была добавлена ​​к пандам. Начиная с панды 0.15.0, вы можете использовать tz_localize(None) удалить часовой пояс, приводящий к местному времени.
Смотрите запись whatsnew: http://pandas.pydata.org/pandas-docs/stable/whatsnew.html

Итак, с моим примером сверху:

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, получая наивное время 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

Поскольку мне всегда трудно вспомнить, краткое изложение того, что делает каждый из них:

>>> 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')

Я думаю, что вы не можете достичь того, что вы хотите, более эффективным способом, чем вы предлагали.

Основная проблема заключается в том, что временные метки (как вы, кажется, знаете) состоят из двух частей. Данные, которые представляют время UTC и часовой пояс, tz_info. Информация о часовом поясе используется только для целей отображения при печати часового пояса на экране. Во время отображения данные смещаются соответствующим образом и в строку добавляется +01:00 (или аналогичное значение). Удаление значения tz_info (с использованием tz_convert(tz=None)) фактически не меняет данные, которые представляют наивную часть временной метки.

Таким образом, единственный способ сделать то, что вы хотите, это изменить базовые данные (панды не позволяют этого... DatetimeIndex неизменны - см. Справку по DatetimeIndex), или создать новый набор объектов отметок времени и обернуть их в новом DatetimeIndex. Ваше решение делает последнее:

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

Для справки, вот replace метод Timestamp (см. tslib.pyx):

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

Вы можете обратиться к документации на datetime.datetime чтобы увидеть это datetime.datetime.replace также создает новый объект.

Если вы можете, лучше всего повысить эффективность, чтобы изменить источник данных таким образом, чтобы он (неверно) сообщал метки времени без их часового пояса. Ты упомянул:

Я хочу работать с наивными временными рядами часовых поясов (чтобы избежать лишних хлопот с часовыми поясами, и они мне не нужны для случая, над которым я работаю)

Мне было бы любопытно, о каких дополнительных хлопотах вы говорите. В качестве общего правила для всех программных разработок я рекомендую сохранять в "UTC" временные метки времени. Есть немного хуже, чем смотреть на два разных значения int64, задаваясь вопросом, к какому часовому поясу они относятся. Если вы всегда, всегда, всегда используете UTC для внутреннего хранения, то вы избежите бесчисленных головных болей. Моя мантра " Часовые пояса" предназначены только для ввода / вывода.

Принятое решение не работает, когда в серии есть несколько разных часовых поясов. Это бросает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]

Настройка tz Атрибут индекса явно работает:

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

Поздний вклад, но только что наткнулся на что-то похожее в Python datetime, а панды дают разные временные метки для одной и той же даты.

Если у вас есть дата-время с учетом часового пояса в 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]')

Мой итог: придерживайтесь даты и времени с учетом часового пояса, если вы можете или только используете t.tz_convert(None)который не изменяет базовую метку времени POSIX. Просто имейте в виду, что тогда вы практически работаете с UTC.

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

Основываясь на предположении DA, что "единственный способ сделать то, что вы хотите, это изменить базовые данные" и использовать 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)

Как я решил эту проблему с 15-минутным индексом даты и времени в Европе.

Если вы находитесь в ситуации, когда вы знаете часовой пояс ( Europe/Amsterdamв моем случае) index и хотите преобразовать его в наивный индекс часового пояса, преобразовав все в местное время, у вас будут проблемы с dst, а именно

  • в последнее воскресенье марта будет отсутствовать 1 час (когда Европа перейдет на летнее время)
  • будет 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узнать месяц. Если у вас больше месяцев, вам, вероятно, следует индексировать его по-другому, чтобы знать, когда переходить на летнее время.

Он состоит из повторной выборки из последнего действительного значения в марте, чтобы не потерять 1 час (в моем случае все мои данные находятся с 15-минутными интервалами, поэтому я передискретизирую таким образом. Передискретизируйте для любого вашего интервала). А на октябрь сбрасываю дубликаты.

Самое главное это добавить tzinfo когда вы определяете объект datetime.

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())
Другие вопросы по тегам