Konwertuj DateTimeIndex uwzględniający strefę czasową pandy na naiwny znacznik czasu, ale w określonej strefie czasowej

107

Możesz użyć tej funkcji, tz_localizeaby wskazać strefę czasową Timestamp lub DateTimeIndex, ale jak możesz zrobić coś przeciwnego: jak przekonwertować sygnaturę czasową uwzględniającą strefę czasową na naiwną, zachowując jej strefę czasową?

Przykład:

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

Mogłem usunąć strefę czasową, ustawiając ją na Brak, ale wynik jest konwertowany na UTC (godzina 12 stała się 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

Czy jest inny sposób na przekonwertowanie DateTimeIndex na naiwną strefę czasową, ale zachowując strefę czasową, w której została ustawiona?


Pewien kontekst na temat powodu, dla którego pytam: chcę pracować z naiwnymi seriami czasowymi dotyczącymi stref czasowych (aby uniknąć dodatkowych problemów ze strefami czasowymi i nie potrzebuję ich w przypadku, nad którym pracuję).
Ale z jakiegoś powodu mam do czynienia z seriami czasowymi uwzględniającymi strefę czasową w mojej lokalnej strefie czasowej (Europa / Bruksela). Ponieważ wszystkie moje inne dane są naiwne dla strefy czasowej (ale reprezentowane w mojej lokalnej strefie czasowej), chcę przekonwertować tę serię czasową na naiwną, aby dalej z nią pracować, ale musi również być reprezentowana w mojej lokalnej strefie czasowej (więc po prostu usuń informacje o strefie czasowej, bez konwersji czasu widocznego dla użytkownika na UTC).

Wiem, że czas jest w rzeczywistości przechowywany wewnętrznie jako UTC i jest konwertowany na inną strefę czasową tylko wtedy, gdy go reprezentujesz, więc musi nastąpić jakaś konwersja, gdy chcę go „zdelokalizować”. Na przykład za pomocą modułu datetime w języku python można „usunąć” strefę czasową w następujący sposób:

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> 

Na tej podstawie mógłbym więc wykonać następujące czynności, ale przypuszczam, że nie będzie to zbyt wydajne podczas pracy z większymi seriami czasu:

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
joris
źródło
Strefa czasowa = brak oznacza UTC ... Nie jestem pewien, czy rozumiem, o co pytasz.
Andy Hayden
Dodałem wyjaśnienie. Chcę zachować czas, który „widzisz” jako użytkownik. Mam nadzieję, że to trochę to wyjaśnia.
joris
Aha, tak, nie zdawałem sobie sprawy, że możesz to zrobić replace.
Andy Hayden
@AndyHayden Tak naprawdę to, czego chcę, to dokładna odwrotność tego, tz_localizeco replace(tzinfo=None)robi w przypadku dat, ale w rzeczywistości nie jest to bardzo oczywisty sposób.
joris

Odpowiedzi:

133

Odpowiadając na moje własne pytanie, w międzyczasie ta funkcja została dodana do pand. Począwszy od pandy 0.15.0 , możesz użyć, tz_localize(None)aby usunąć strefę czasową skutkującą czasem lokalnym.
Zobacz wpis whatsnew: http://pandas.pydata.org/pandas-docs/stable/whatsnew.html#timezone-handling-improvements

A więc z moim przykładem z góry:

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

użycie tz_localize(None)usuwa informacje o strefie czasowej, powodując naiwny czas lokalny :

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

Ponadto możesz również użyć tz_convert(None)do usunięcia informacji o strefie czasowej, ale konwertując na UTC, uzyskując naiwny czas 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')

Jest to znacznie wydajniejsze niż datetime.replacerozwiązanie:

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
joris
źródło
1
W przypadku, gdy pracujemy z czegoś, co jest już UTC i trzeba konwertować go na czas lokalny, a następnie upuścić czasowej: from tzlocal import get_localzone, tz_here = get_localzone(),<datetime object>.tz_convert(tz_here).tz_localize(None)
Nathan Lloyd
3
Jeśli nie masz przydatnego indeksu, możesz potrzebować t.dt.tz_localize(None)lub t.dt.tz_convert(None). Zwróć uwagę na .dt.
Acumenus
2
To rozwiązanie działa tylko wtedy, gdy w serii występuje jeden unikalny tz. Jeśli masz wiele różnych tz z tej samej serii, zobacz (i zagłosuj) tutaj :-): stackoverflow.com/a/59204751/1054154
tozCSS
14

Myślę, że nie możesz osiągnąć tego, co chcesz, w bardziej efektywny sposób niż proponowałeś.

Podstawowy problem polega na tym, że znaczniki czasu (jak wydajesz się świadomy) składają się z dwóch części. Dane reprezentujące czas UTC i strefę czasową tz_info. Informacje o strefie czasowej są używane tylko do celów wyświetlania podczas drukowania strefy czasowej na ekranie. W czasie wyświetlania dane są odpowiednio przesunięte i do ciągu dodawane jest +01: 00 (lub podobne). Usunięcie wartości tz_info (przy użyciu tz_convert (tz = None)) nie zmienia w rzeczywistości danych, które reprezentują naiwną część znacznika czasu.

Tak więc jedynym sposobem na zrobienie tego, co chcesz, jest zmodyfikowanie podstawowych danych (pandy nie pozwalają na to ... DatetimeIndex są niezmienne - zobacz pomoc dotyczącą DatetimeIndex) lub utworzenie nowego zestawu obiektów ze znacznikami czasu i zawijanie ich w nowym DatetimeIndex. Twoje rozwiązanie robi to drugie:

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

Dla odniesienia, oto replacemetoda Timestamp(patrz tslib.pyx):

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

Możesz odnieść się do dokumentów, datetime.datetimeaby zobaczyć, że datetime.datetime.replacerównież tworzy nowy obiekt.

Jeśli możesz, najlepszym sposobem na zwiększenie wydajności jest zmodyfikowanie źródła danych tak, aby (nieprawidłowo) zgłaszało sygnatury czasowe bez ich strefy czasowej. Wspomniałeś:

Chcę pracować z naiwnymi seriami czasowymi dla stref czasowych (aby uniknąć dodatkowych problemów ze strefami czasowymi i nie potrzebuję ich w przypadku, nad którym pracuję)

Byłbym ciekawy, o jakim dodatkowym kłopocie masz na myśli. Generalnie zalecam, aby w przypadku tworzenia oprogramowania zachować „naiwne wartości” sygnatury czasowej w UTC. Nie ma nic gorszego niż przyjrzenie się dwóm różnym wartościom int64 zastanawianie się, do której strefy czasowej należą. Jeśli zawsze, zawsze, zawsze używasz UTC jako pamięci wewnętrznej, unikniesz niezliczonych bólów głowy. Moja mantra brzmi: Strefy czasowe są przeznaczone wyłącznie dla ludzkich wejść / wyjść .

DA
źródło
3
Dziękuję za odpowiedź i spóźnioną odpowiedź: mój przypadek nie jest aplikacją, tylko naukową analizą mojej własnej pracy (a więc np. Brak udostępniania współpracownikom z całego świata). W takim przypadku łatwiej jest po prostu pracować z naiwnymi znacznikami czasu, ale w czasie lokalnym. Więc nie muszę martwić się o strefy czasowe i po prostu mogę zinterpretować znacznik czasu jako czas lokalny (dodatkowym „kłopotem” może być np. naiwne i uwzględniające przesunięcia czasy danych ”). Ale całkowicie się z tobą zgadzam, gdy mam do czynienia z bardziej złożonymi aplikacjami.
joris,
14

Ponieważ zawsze staram się zapamiętać, krótkie podsumowanie tego, co robią:

>>> 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')
Juan A. Navarro
źródło
7

tzWydaje się, że ustawienie atrybutu indeksu wyraźnie działa:

ts_utc = ts.tz_convert("UTC")
ts_utc.index.tz = None
filmor
źródło
3
Spóźniony komentarz, ale chcę, aby wynikiem był czas reprezentowany w lokalnej strefie czasowej, a nie w UTC. I jak pokazuję w pytaniu, ustawienie na tzBrak również konwertuje je na UTC.
joris
Co więcej, seria czasów jest już świadoma strefy czasowej, więc wywołanie tz_convertjej spowoduje zgłoszenie błędu.
joris
4

Opierając się na sugestii DA, że „ jedynym sposobem na zrobienie tego, co chcesz, jest modyfikacja danych bazowych ” i użycie numpy do zmodyfikowania danych bazowych ...

To działa dla mnie i jest dość szybkie:

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)
Jack Kelly
źródło
Dzięki za odpowiedź! Myślę jednak, że zadziała to tylko wtedy, gdy w okresie zbioru danych nie będzie przejścia z czasu letniego na zimowy.
joris
@joris Ah, dobry chwyt! Nie brałem tego pod uwagę! Zmodyfikuję swoje rozwiązanie, aby jak najszybciej poradzić sobie z tą sytuacją.
Jack Kelly
Uważam, że nadal jest to błędne, ponieważ obliczasz przesunięcie tylko za pierwszym razem, a nie w miarę postępu w czasie. Spowoduje to, że przegapisz czas letni i nie dostosujesz się odpowiednio w tym dniu i później.
Pierre-Luc Bertrand
4

Przyjęte rozwiązanie nie działa, gdy w serii jest wiele różnych stref czasowych. RzucaValueError: Tz-aware datetime.datetime cannot be converted to datetime64 unless utc=True

Rozwiązaniem jest użycie applymetody.

Zobacz poniższe przykłady:

# 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]
tozCSS
źródło
2

Późny wkład, ale właśnie natknąłem się na coś podobnego w Pythonie data i godzina, a pandy podają różne sygnatury czasowe dla tej samej daty .

Jeśli masz datę i godzinę uwzględniającą strefę czasową w pandas, technicznie rzecz biorąc, tz_localize(None)zmienia znacznik czasu POSIX (który jest używany wewnętrznie), tak jakby lokalny czas z sygnatury czasowej był UTC. Lokalny w tym kontekście oznacza lokalny w określonej strefie czasowej . Dawny:

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

Zauważ, że spowoduje to dziwne rzeczy podczas przejścia na czas letni , np

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

W przeciwieństwie do tz_convert(None)tego nie modyfikuje wewnętrznego znacznika czasu, po prostu usuwa rozszerzenie tzinfo.

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

Mój wniosek byłby następujący: trzymaj się daty i godziny uwzględniającej strefę czasową, jeśli możesz lub używaj tylko tego, t.tz_convert(None)co nie modyfikuje bazowego znacznika czasu POSIX. Pamiętaj tylko, że wtedy praktycznie pracujesz z UTC.

(Python 3.8.2 x64 w systemie Windows 10, pandaswersja 1.0.5.)

MrFuppes
źródło
0

Najważniejszą rzeczą jest dodanie tzinfopodczas definiowania obiektu daty i godziny.

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())
Yuchao Jiang
źródło