Converti DateTimeIndex che riconoscono il fuso orario dei panda in un timestamp ingenuo, ma in un determinato fuso orario


99

Puoi utilizzare la funzione tz_localizeper rendere consapevole un fuso orario Timestamp o DateTimeIndex, ma come puoi fare il contrario: come puoi convertire un Timestamp sensibile al fuso orario in uno ingenuo, preservandone il fuso orario?

Un esempio:

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

Potrei rimuovere il fuso orario impostandolo su Nessuno, ma poi il risultato viene convertito in UTC (le 12 sono diventate 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

C'è un altro modo per convertire un DateTimeIndex in un fuso orario ingenuo, ma preservando il fuso orario in cui è stato impostato?


Un po 'di contesto sul motivo per cui lo chiedo: voglio lavorare con serie temporali ingenue nel fuso orario (per evitare il fastidio extra con i fusi orari, e non ne ho bisogno per il caso su cui sto lavorando).
Ma per qualche motivo, devo gestire una serie temporale che tiene conto del fuso orario nel mio fuso orario locale (Europa / Bruxelles). Poiché tutti gli altri miei dati sono ingenui del fuso orario (ma rappresentati nel mio fuso orario locale), voglio convertire questa serie temporale in ingenuo per lavorarci ulteriormente, ma deve anche essere rappresentato nel mio fuso orario locale (quindi rimuovi le informazioni sul fuso orario, senza convertire l' ora visibile all'utente in UTC).

So che l'ora è effettivamente memorizzata internamente come UTC e convertita in un altro fuso orario solo quando la rappresenti, quindi deve esserci un qualche tipo di conversione quando voglio "delocalizzarla". Ad esempio, con il modulo datetime di python puoi "rimuovere" il fuso orario in questo modo:

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> 

Quindi, in base a questo, potrei fare quanto segue, ma suppongo che non sarà molto efficiente quando si lavora con una serie temporale più 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

Fuso orario = Nessuno significa UTC ... Non sono sicuro di aver capito cosa stai chiedendo qui.
Andy Hayden

Ho aggiunto qualche spiegazione. Voglio mantenere il tempo che "vedi" come utente. Spero che questo lo chiarisca un po '.
joris

Ah ah, lo fa, non sapevo che avresti potuto farlo replace.
Andy Hayden

@ AnddyHayden Quindi in realtà quello che voglio è l'esatto inverso di tz_localizeciò che replace(tzinfo=None)fa per i datetimes, ma in realtà non è un modo molto ovvio.
joris

Risposte:


123

Per rispondere alla mia domanda, nel frattempo questa funzionalità è stata aggiunta ai panda. A partire da panda 0.15.0 , è possibile utilizzare tz_localize(None)per rimuovere il fuso orario risultante nell'ora locale.
Vedi la voce whatsnew: http://pandas.pydata.org/pandas-docs/stable/whatsnew.html#timezone-handling-improvements

Quindi con il mio esempio dall'alto:

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)rimuove le informazioni sul fuso orario con conseguente ingenua ora locale :

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

Inoltre, puoi anche utilizzare tz_convert(None)per rimuovere le informazioni sul fuso orario ma convertirle in UTC, ottenendo così un tempo UTC ingenuo :

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

Questo è molto più performante della datetime.replacesoluzione:

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
Nel caso in cui si sta lavorando con qualcosa che è già UTC e necessità di convertirlo in ora locale e poi cadere il fuso orario: from tzlocal import get_localzone, tz_here = get_localzone(),<datetime object>.tz_convert(tz_here).tz_localize(None)
Nathan Lloyd

3
Se non disponi di un indice utile, potresti aver bisogno di t.dt.tz_localize(None)o t.dt.tz_convert(None). Nota il .dt.
Acumenus

2
Questa soluzione funziona solo quando c'è una tz unica nella serie. Se si dispone di più tz diverso nella stessa serie, quindi vedere (e upvote) la soluzione qui :-): stackoverflow.com/a/59204751/1054154
tozCSS

14

Penso che non puoi ottenere ciò che desideri in modo più efficiente di quanto hai proposto.

Il problema di fondo è che i timestamp (come sembri consapevoli) sono costituiti da due parti. I dati che rappresentano l'ora UTC e il fuso orario, tz_info. Le informazioni sul fuso orario vengono utilizzate solo a scopo di visualizzazione quando si stampa il fuso orario sullo schermo. Al momento della visualizzazione, i dati vengono spostati in modo appropriato e +01: 00 (o simile) viene aggiunto alla stringa. La rimozione del valore tz_info (utilizzando tz_convert (tz = None)) non cambia effettivamente i dati che rappresentano la parte ingenua del timestamp.

Quindi, l'unico modo per fare quello che vuoi è modificare i dati sottostanti (i panda non lo consentono ... DatetimeIndex sono immutabili - vedi la guida su DatetimeIndex), o creare un nuovo set di oggetti timestamp e avvolgerli in un nuovo DatetimeIndex. La tua soluzione fa il secondo:

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

Per riferimento, ecco il replacemetodo di Timestamp(vedi tslib.pyx):

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

Puoi fare riferimento alla documentazione datetime.datetimeper vedere che datetime.datetime.replacecrea anche un nuovo oggetto.

Se puoi, la soluzione migliore per l'efficienza è modificare la fonte dei dati in modo che (in modo errato) riporti i timestamp senza il loro fuso orario. Hai nominato:

Voglio lavorare con le serie temporali ingenui del fuso orario (per evitare il fastidio extra con i fusi orari, e non mi servono per il caso su cui sto lavorando)

Sarei curioso di quale seccatura in più ti riferisci. Come regola generale per tutto lo sviluppo del software, raccomando di mantenere il timestamp "valori ingenui" in UTC. C'è poco peggio che guardare due diversi valori int64 chiedendosi a quale fuso orario appartengono. Se usi sempre, sempre, sempre UTC per la memoria interna, eviterai innumerevoli mal di testa. Il mio mantra è che i fusi orari sono solo per l'I / O umano .


3
Grazie per la risposta, e una risposta in ritardo: il mio caso non è un'applicazione, ma solo un'analisi scientifica per il mio lavoro (quindi ad esempio nessuna condivisione con collaboratori in tutto il mondo). E in tal caso, può essere più semplice lavorare solo con timestamp ingenui, ma nell'ora locale. Quindi non devo preoccuparmi dei fusi orari e posso solo interpretare il timestamp come ora locale (la "seccatura" extra può essere ad esempio che tutto deve essere in fusi orari, altrimenti ottieni cose come "non posso confrontare offset- datetimes ingenuo e offset-consapevole "). Ma sono completamente d'accordo con te quando si tratta di applicazioni più complesse.
joris

13

Poiché faccio sempre fatica a ricordare, un breve riassunto di ciò che ognuno di questi fa:

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

L'impostazione tzdell'attributo dell'indice in modo esplicito sembra funzionare:

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

3
Commento in ritardo, ma voglio che il risultato sia l'ora rappresentata nel fuso orario locale, non in UTC. E come mostro nella domanda, l'impostazione tzdi Nessuno lo converte anche in UTC.
joris

Inoltre, la serie temporale è già consapevole del fuso orario, quindi richiamarla tz_convertgenererà un errore.
joris

4

La soluzione accettata non funziona quando ci sono più fusi orari diversi in una serie. LanciaValueError: Tz-aware datetime.datetime cannot be converted to datetime64 unless utc=True

La soluzione è usare il applymetodo.

Si prega di vedere gli esempi di seguito:

# 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

Basandosi sul suggerimento di DA che " l'unico modo per fare quello che vuoi è modificare i dati sottostanti " e usare numpy per modificare i dati sottostanti ...

Questo funziona per me ed è abbastanza veloce:

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)

Grazie per la tua risposta! Tuttavia, penso che questo funzionerà solo se non è presente alcuna transizione tra ora legale e ora solare nel periodo del set di dati.
joris

@joris Ah, buona cattura! Non l'avevo considerato! Modificherò la mia soluzione per gestire questa situazione al più presto.
Jack Kelly

Credo che questo sia ancora sbagliato poiché stai solo calcolando l'offset della prima volta e non mentre procede nel tempo. Ciò ti farà perdere l'ora legale e non ti adatterai di conseguenza da quella data in poi.
Pierre-Luc Bertrand

2

Contributo tardivo ma ho appena trovato qualcosa di simile in Python datetime ei panda danno timestamp diversi per la stessa data .

Se si dispone di datetime in grado di riconoscere il fuso orario pandas, tecnicamente, tz_localize(None)cambia il timestamp POSIX (che viene utilizzato internamente) come se l'ora locale dal timestamp fosse UTC. Locale in questo contesto significa locale nel fuso orario specificato . 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]')

Nota che questo ti lascerà con cose strane durante le transizioni all'ora legale , ad es

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

Al contrario, tz_convert(None)non modifica il timestamp interno, rimuove solo il file tzinfo.

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

La mia linea di fondo sarebbe: attenersi al datetime sensibile al fuso orario se è possibile o utilizzare solo t.tz_convert(None)che non modifica il timestamp POSIX sottostante. Tieni presente che stai praticamente lavorando con UTC allora.

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


0

La cosa più importante è aggiungere tzinfoquando si definisce un oggetto 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())
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.