Convierta el DateTimeIndex compatible con la zona horaria de pandas en una marca de tiempo ingenua, pero en cierta zona horaria

99

Puede usar la función tz_localizepara hacer que una marca de tiempo o una zona horaria DateTimeIndex sea consciente, pero ¿cómo puede hacer lo contrario?

Un ejemplo:

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

Podría eliminar la zona horaria configurándola en Ninguno, pero luego el resultado se convierte a UTC (las 12 en punto se convirtieron en 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

¿Hay otra forma de convertir un DateTimeIndex en una zona horaria ingenua, pero conservando la zona horaria en la que se estableció?


Un poco de contexto sobre la razón por la que pregunto esto: quiero trabajar con series de tiempo ingenuas de zonas horarias (para evitar la molestia adicional con las zonas horarias, y no las necesito para el caso en el que estoy trabajando).
Pero por alguna razón, tengo que lidiar con una serie horaria consciente de la zona horaria en mi zona horaria local (Europa / Bruselas). Como todos mis otros datos son ingenuos de zona horaria (pero están representados en mi zona horaria local), quiero convertir esta serie temporal en ingenuo para seguir trabajando con ellos, pero también tiene que estar representado en mi zona horaria local (así que simplemente elimine la información de la zona horaria, sin convertir la hora visible para el usuario a UTC).

Sé que la hora está almacenada internamente como UTC y solo se convierte a otra zona horaria cuando la representa, por lo que tiene que haber algún tipo de conversión cuando quiero "deslocalizarla". Por ejemplo, con el módulo de fecha y hora de Python puede "eliminar" la zona horaria de esta manera:

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> 

Entonces, basado en esto, podría hacer lo siguiente, pero supongo que esto no será muy eficiente cuando trabaje con una serie temporal más grande:

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
fuente
Zona horaria = Ninguno significa UTC ... No estoy seguro de entender lo que estás preguntando aquí.
Andy Hayden
Agregué alguna explicación. Quiero mantener el tiempo que 'ves' como usuario. Espero que esto lo aclare un poco.
joris
Ah, sí, no me di cuenta de que podrías hacer eso replace.
Andy Hayden
@AndyHayden Así que en realidad lo que quiero es el inverso exacto de tz_localizeque es lo que la replace(tzinfo=None)hace por datetimes, pero de hecho no es una manera muy obvia.
joris

Respuestas:

123

Para responder a mi propia pregunta, esta funcionalidad se ha agregado a los pandas mientras tanto. A partir de pandas 0.15.0 , puede usar tz_localize(None)para eliminar la zona horaria que da como resultado la hora local.
Vea la nueva entrada: http://pandas.pydata.org/pandas-docs/stable/whatsnew.html#timezone-handling-improvements

Entonces, con mi ejemplo de arriba:

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

el uso tz_localize(None)elimina la información de la zona horaria, lo que da como resultado una hora local ingenua :

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

Además, también puede usar tz_convert(None)para eliminar la información de la zona horaria pero convirtiéndola a UTC, lo que produce una hora UTC ingenua :

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

Esto es mucho más eficaz que la datetime.replacesolución:

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
fuente
1
En caso de que usted está trabajando con algo que ya está UTC y la necesidad de convertirla en hora local y luego dejar caer la zona horaria: from tzlocal import get_localzone, tz_here = get_localzone(),<datetime object>.tz_convert(tz_here).tz_localize(None)
Nathan Lloyd
3
Si no tiene un índice útil, es posible que necesite t.dt.tz_localize(None)o t.dt.tz_convert(None). Tenga en cuenta el .dt.
Acumenus
2
Esta solución solo funciona cuando hay un tz único en la Serie. Si tiene varios tz diferentes en la misma serie, consulte (y
vote a favor
14

Creo que no puede lograr lo que quiere de una manera más eficiente de lo que propuso.

El problema subyacente es que las marcas de tiempo (como parece saber) se componen de dos partes. Los datos que representan la hora UTC y la zona horaria, tz_info. La información de la zona horaria se utiliza solo con fines de visualización cuando se imprime la zona horaria en la pantalla. En el momento de la visualización, los datos se compensan adecuadamente y se agrega +01: 00 (o similar) a la cadena. Eliminar el valor tz_info (usando tz_convert (tz = None)) no cambia en realidad los datos que representan la parte ingenua de la marca de tiempo.

Entonces, la única forma de hacer lo que desea es modificar los datos subyacentes (los pandas no permiten esto ... DatetimeIndex son inmutables; consulte la ayuda en DatetimeIndex), o crear un nuevo conjunto de objetos de marca de tiempo y envolverlos en un nuevo DatetimeIndex. Tu solución hace lo último:

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

Como referencia, aquí está el replacemétodo de Timestamp(ver tslib.pyx):

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

Puede consultar los documentos en datetime.datetime para ver que datetime.datetime.replacetambién crea un nuevo objeto.

Si puede, su mejor apuesta por la eficiencia es modificar la fuente de los datos para que informe (incorrectamente) las marcas de tiempo sin su zona horaria. Mencionaste:

Quiero trabajar con series de tiempo ingenuas de zonas horarias (para evitar la molestia adicional con las zonas horarias, y no las necesito para el caso en el que estoy trabajando)

Tendría curiosidad a qué molestia adicional te refieres. Recomiendo, como regla general para todo el desarrollo de software, mantener los 'valores ingenuos' de la marca de tiempo en UTC. No hay nada peor que mirar dos valores int64 diferentes preguntándose a qué zona horaria pertenecen. Si siempre, siempre, siempre utiliza UTC para el almacenamiento interno, evitará innumerables dolores de cabeza. Mi mantra es que las zonas horarias son solo para E / S humanas .

DA
fuente
3
Gracias por la respuesta y una respuesta tardía: mi caso no es una aplicación, solo un análisis científico de mi propio trabajo (por ejemplo, no compartir con colaboradores de todo el mundo). Y en ese caso, puede ser más fácil trabajar con marcas de tiempo ingenuas, pero en su hora local. Así que no tengo que preocuparme por las zonas horarias y solo puedo interpretar la marca de tiempo como la hora local (la 'molestia' adicional puede ser, por ejemplo, que todo debe estar en zonas horarias, de lo contrario, obtienes cosas como "no se puede comparar el desplazamiento fechas ingenuas y con reconocimiento de compensaciones "). Pero estoy completamente de acuerdo con usted cuando se trata de aplicaciones más complejas.
joris
13

Porque siempre me cuesta recordar, un resumen rápido de lo que hace cada uno de estos:

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

Establecer el tzatributo del índice explícitamente parece funcionar:

ts_utc = ts.tz_convert("UTC")
ts_utc.index.tz = None
filmar
fuente
3
Comentario tardío, pero quiero que el resultado sea la hora representada en la zona horaria local, no en UTC. Y como muestro en la pregunta, configurar el tzen Ninguno también lo convierte a UTC.
joris
Además, la serie temporal ya es consciente de la zona horaria, por lo que llamarla tz_convertgenerará un error.
joris
4

La solución aceptada no funciona cuando hay varias zonas horarias diferentes en una serie. LanzaValueError: Tz-aware datetime.datetime cannot be converted to datetime64 unless utc=True

La solución es utilizar el applymétodo.

Consulte los ejemplos siguientes:

# 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
fuente
3

Sobre la base de la sugerencia de DA de que " la única forma de hacer lo que quiere es modificar los datos subyacentes " y usar numpy para modificar los datos subyacentes ...

Esto funciona para mí y es bastante 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
fuente
¡Gracias por tu respuesta! Sin embargo, creo que esto solo funcionará si no hay una transición de verano / invierno en el período del conjunto de datos.
joris
@joris ¡Ah, buen partido! ¡No lo había considerado! Modificaré mi solución para manejar esta situación lo antes posible.
Jack Kelly
Creo que esto sigue siendo incorrecto ya que solo está calculando el desplazamiento de la primera vez y no a medida que avanza a lo largo del tiempo. Esto hará que pierda el horario de verano y no se ajuste en consecuencia en esa fecha determinada y en adelante.
Pierre-Luc Bertrand
2

Contribución tardía, pero acaba de encontrar algo similar en Python datetime y los pandas dan diferentes marcas de tiempo para la misma fecha .

Si tiene una fecha y hora consciente de la zona horaria pandas, técnicamente, tz_localize(None)cambia la marca de tiempo POSIX (que se usa internamente) como si la hora local de la marca de tiempo fuera UTC. Local en este contexto significa local en la zona horaria especificada . 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]')

Tenga en cuenta que esto lo dejará con cosas extrañas durante las transiciones de DST , por ejemplo

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

Por el contrario, tz_convert(None)no modifica la marca de tiempo interna, solo elimina el tzinfo.

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

Mi conclusión sería: quédese con la fecha y hora consciente de la zona horaria si puede o solo use la t.tz_convert(None)que no modifique la marca de tiempo POSIX subyacente. Solo tenga en cuenta que prácticamente está trabajando con UTC entonces.

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

MrFuppes
fuente
0

Lo más importante es agregar tzinfocuando define un objeto de fecha y hora.

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
fuente