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


99

Вы можете использовать эту функцию, tz_localizeчтобы сделать отметку времени или 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

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

Timezone = None означает UTC ... Я не уверен, что понимаю, о чем вы здесь спрашиваете.
Энди Хайден

Я добавил некоторые пояснения. Я хочу сохранить время, которое вы «видите» как пользователь. Надеюсь, это немного проясняет ситуацию.
joris

Ага, это так, я не понимал, что ты можешь это сделать replace.
Энди Хайден

@AndyHayden Итак, на самом деле я хочу, чтобы это было точным обратным tz_localizeтому, что replace(tzinfo=None)делает для datetime, но это действительно не очень очевидный способ.
joris

Ответы:


123

Чтобы ответить на мой собственный вопрос, эта функциональность тем временем была добавлена ​​в pandas. Начиная с pandas 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')

using 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

1
В случае , если вы работаете с чем - то , что уже находится UTC и необходимости преобразовать его в местное время и затем уронить часовой пояс: from tzlocal import get_localzone, tz_here = get_localzone(),<datetime object>.tz_convert(tz_here).tz_localize(None)
Натан Ллойд

3
Если у вас нет полезного индекса, вам может понадобиться t.dt.tz_localize(None)или t.dt.tz_convert(None). Обратите внимание на расширение .dt.
Acumenus

2
Это решение работает только тогда, когда в Серии есть один уникальный tz. Если у вас есть несколько разных tz в одной и той же серии, посмотрите (и проголосуйте за) решение здесь :-): stackoverflow.com/a/59204751/1054154
tozCSS 05

14

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

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

Итак, единственный способ сделать то, что вы хотите, - это изменить базовые данные (pandas не позволяет этого ... 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 для внутреннего хранилища, вы избежите бесчисленных головных болей. Моя мантра - часовые пояса предназначены только для ввода-вывода людей .


3
Спасибо за ответ и поздний ответ: мой случай - это не приложение, а просто научный анализ моей собственной работы (например, не делиться с коллегами по всему миру). И в этом случае может быть проще просто работать с наивными метками времени, но в вашем местном времени. Так что мне не нужно беспокоиться о часовых поясах, и я просто могу интерпретировать временную метку как местное время (дополнительные «хлопоты» могут заключаться, например, в том, что все должно быть в часовых поясах, иначе вы получите такие вещи, как «невозможно сравнить смещение- наивные и учитывающие смещение даты и времени "). Но я полностью согласен с вами при работе с более сложными приложениями.
joris

13

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

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

7

tzКажется, что установка атрибута индекса явно работает:

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

3
Поздний комментарий, но я хочу, чтобы результатом было время, представленное в местном часовом поясе, а не в UTC. И, как я показываю в вопросе, установка значения tzNone также преобразует его в UTC.
joris 08

Кроме того, временной ряд уже знает часовой пояс, поэтому его вызов tz_convertвызовет ошибку.
joris 08

4

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

3

Основываясь на предположении 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)

Спасибо за Ваш ответ! Однако я думаю, что это будет работать только в том случае, если в период набора данных нет перехода на летнее / зимнее время.
joris

@joris Ах, хороший улов! Я не думал об этом! Я изменю свое решение, чтобы справиться с этой ситуацией как можно скорее.
Джек Келли

Я считаю, что это все еще неправильно, поскольку вы рассчитываете смещение только в первый раз, а не по мере его продвижения во времени. Это приведет к тому, что вы пропустите переход на летнее время и не настроитесь соответствующим образом на эту дату и далее.
Пьер-Люк Бертран

2

Поздний вклад, но только что наткнулся на что-то похожее в 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]')

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

(Python 3.8.2 x64 в Windows 10, pandasv1.0.5.)


0

Самое важное - добавить, 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())
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.