Converta DateTimeIndex de fuso horário do pandas em timestamp ingênuo, mas em determinado fuso horário

107

Você pode usar a função tz_localizepara tornar um timestamp ou DateTimeIndex ciente do fuso horário, mas como você pode fazer o oposto: como você pode converter um timestamp que reconhece o fuso horário em um ingênuo, preservando seu fuso horário?

Um exemplo:

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

Eu poderia remover o fuso horário definindo-o como Nenhum, mas o resultado é convertido para UTC (12 horas se tornaram 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

Existe outra maneira de converter um DateTimeIndex em fuso horário ingênuo, mas preservando o fuso horário em que foi definido?


Algum contexto sobre o motivo pelo qual estou perguntando isso: quero trabalhar com séries de tempo ingênuas de fuso horário (para evitar o incômodo extra com fusos horários, e não preciso deles para o caso em que estou trabalhando).
Mas, por alguma razão, tenho que lidar com uma série de tempo ciente de fuso horário no meu fuso horário local (Europa / Bruxelas). Como todos os meus outros dados são ingênuos de fuso horário (mas representados em meu fuso horário local), quero converter esta série de tempo em ingênuo para trabalhar mais com ele, mas também deve ser representado em meu fuso horário local (então, basta remover as informações de fuso horário, sem converter a hora visível ao usuário em UTC).

Eu sei que a hora é armazenada internamente como UTC e só é convertida para outro fuso horário quando você a representa, então deve haver algum tipo de conversão quando eu quiser "deslocalizá-la". Por exemplo, com o módulo Python datetime, você pode "remover" o fuso horário assim:

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> 

Portanto, com base nisso, eu poderia fazer o seguinte, mas suponho que não seja muito eficiente ao trabalhar com uma série do tempo maior:

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
fonte
Timezone = None significa UTC ... Não tenho certeza se entendi o que você está perguntando aqui.
Andy Hayden
Eu adicionei alguma explicação. Eu quero manter o tempo que você 'vê' como um usuário. Espero que isso esclareça um pouco.
joris
Ah, sim, eu não sabia que você poderia fazer isso com replace.
Andy Hayden
@AndyHayden Então, na verdade, o que eu quero é o inverso exato do tz_localizeque o replace(tzinfo=None)faz para os datetimes, mas de fato não é uma maneira muito óbvia.
joris

Respostas:

133

Para responder à minha própria pergunta, esta funcionalidade foi adicionada aos pandas entretanto. A partir do pandas 0.15.0 , você pode usar tz_localize(None)para remover o fuso horário resultante da hora local.
Veja a nova entrada: http://pandas.pydata.org/pandas-docs/stable/whatsnew.html#timezone-handling-improvements

Então, com meu exemplo acima:

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

o uso tz_localize(None)remove as informações de fuso horário, resultando em horário local ingênuo :

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

Além disso, você também pode usar tz_convert(None) para remover as informações de fuso horário, mas convertendo para UTC, gerando um tempo UTC ingênuo :

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

Isso é muito mais eficiente do que a datetime.replacesolução:

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
fonte
1
No caso você está trabalhando com algo que já é UTC e necessidade de convertê-lo para a hora local e , em seguida, soltar o fuso horário: from tzlocal import get_localzone, tz_here = get_localzone(),<datetime object>.tz_convert(tz_here).tz_localize(None)
Nathan Lloyd
3
Se você não tiver um índice útil, pode precisar de t.dt.tz_localize(None)ou t.dt.tz_convert(None). Observe o .dt.
Acumenus
2
Esta solução só funciona quando há um tz exclusivo na série. Se você tiver vários tz diferentes na mesma série, veja (e
vote positivamente
14

Acho que você não pode conseguir o que deseja de uma maneira mais eficiente do que propôs.

O problema subjacente é que os carimbos de data / hora (como você parece saber) são compostos de duas partes. Os dados que representam a hora UTC e o fuso horário, tz_info. As informações de fuso horário são usadas apenas para fins de exibição ao imprimir o fuso horário na tela. No tempo de exibição, os dados são deslocados apropriadamente e +01: 00 (ou similar) é adicionado à string. Retirar o valor tz_info (usando tz_convert (tz = None)) não altera realmente os dados que representam a parte ingênua do carimbo de data / hora.

Portanto, a única maneira de fazer o que você deseja é modificar os dados subjacentes (o pandas não permite isso ... DatetimeIndex são imutáveis ​​- consulte a ajuda em DatetimeIndex) ou criar um novo conjunto de objetos de carimbo de data / hora e agrupá-los em um novo DatetimeIndex. Sua solução faz o último:

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

Para referência, aqui está o replacemétodo de Timestamp(consulte tslib.pyx):

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

Você pode consultar a documentação datetime.datetimepara ver que datetime.datetime.replacetambém cria um novo objeto.

Se você puder, sua melhor aposta para eficiência é modificar a fonte dos dados de forma que (incorretamente) relate os carimbos de data / hora sem seu fuso horário. Você mencionou:

Eu quero trabalhar com séries de tempo ingênuas de fuso horário (para evitar o incômodo extra com fusos horários, e não preciso delas para o caso em que estou trabalhando)

Ficaria curioso em saber a que incômodo extra você está se referindo. Eu recomendo como regra geral para todo desenvolvimento de software, manter seu carimbo de data / hora 'valores ingênuos' em UTC. Não há nada pior do que olhar para dois valores int64 diferentes e imaginar a qual fuso horário eles pertencem. Se você sempre, sempre, sempre usar UTC para armazenamento interno, evitará inúmeras dores de cabeça. Meu mantra é Fusos horários são apenas para E / S humana .

DA
fonte
3
Obrigado pela resposta, e uma resposta tardia: meu caso não é um aplicativo, apenas uma análise científica do meu próprio trabalho (por exemplo, não há compartilhamento com colaboradores em todo o mundo). E, nesse caso, pode ser mais fácil trabalhar apenas com carimbos de data / hora ingênuos, mas em seu horário local. Então, eu não preciso me preocupar com fusos horários e apenas posso interpretar o carimbo de data / hora como hora local (o 'incômodo' extra pode ser, por exemplo, que tudo então tem que estar em fusos horários, caso contrário, você obterá coisas como "não pode comparar o deslocamento- datetimes ingênuos e com reconhecimento de deslocamento "). Mas concordo totalmente com você ao lidar com aplicativos mais complexos.
joris
14

Porque sempre me esforço para lembrar, um rápido resumo do que cada um deles faz:

>>> 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
fonte
7

Definir o tzatributo do índice explicitamente parece funcionar:

ts_utc = ts.tz_convert("UTC")
ts_utc.index.tz = None
filmor
fonte
3
Comentário atrasado, mas quero que o resultado seja a hora representada no fuso horário local, não em UTC. E, como mostro na pergunta, definir o tzcomo Nenhum também o converte em UTC.
joris
Além disso, a série de tempo já reconhece o fuso horário, portanto, chamá tz_convert-la gerará um erro.
joris
4

Com base na sugestão do DA de que " a única maneira de fazer o que você quer é modificar os dados subjacentes " e usando o numpy para modificar os dados subjacentes ...

Isso funciona para mim e é muito rápido:

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
fonte
Obrigado pela sua resposta! No entanto, acho que isso só funcionará se não houver transição de verão / inverno no período do conjunto de dados.
joris
@joris Ah, boa pegada! Eu não tinha considerado isso! Vou modificar minha solução para lidar com essa situação o mais rápido possível.
Jack Kelly
Acredito que isso ainda esteja errado, pois você está apenas calculando o deslocamento da primeira vez e não à medida que avança ao longo do tempo. Isso fará com que você perca o horário de verão e não se ajuste de acordo com essa data e em diante.
Pierre-Luc Bertrand
4

A solução aceita não funciona quando há vários fusos horários diferentes em uma série. JogaValueError: Tz-aware datetime.datetime cannot be converted to datetime64 unless utc=True

A solução é usar o applymétodo.

Veja os exemplos abaixo:

# 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
fonte
2

Contribuição tardia, mas acabei de encontrar algo semelhante no datetime do Python e o pandas fornece carimbos de data / hora diferentes para a mesma data .

Se você tiver uma data e hora ciente de fuso horário em pandas, tecnicamente, tz_localize(None)altera o carimbo de data / hora POSIX (que é usado internamente) como se a hora local do carimbo de data / hora fosse UTC. Local neste contexto significa local no fuso horário especificado . Ex:

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

Observe que isso vai deixar você com coisas estranhas durante as transições do horário de verão , por exemplo

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

Em contraste, tz_convert(None)não modifica o carimbo de data / hora interno, apenas remove o tzinfo.

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

Minha linha de fundo seria: mantenha-se com a data e hora com reconhecimento de fuso horário se você puder ou apenas usar o t.tz_convert(None)que não modifica o carimbo de data / hora POSIX subjacente. Lembre-se de que você está praticamente trabalhando com a UTC.

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

MrFuppes
fonte
0

O mais importante é adicionar tzinfoao definir um objeto 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())
Yuchao Jiang
fonte