Seleziona le righe DataFrame tra due date


198

Sto creando un DataFrame da un CSV come segue:

stock = pd.read_csv('data_in/' + filename + '.csv', skipinitialspace=True)

DataFrame ha una colonna di data. C'è un modo per creare un nuovo DataFrame (o semplicemente sovrascrivere quello esistente) che contiene solo righe con valori di data che rientrano in un intervallo di date specificato o tra due valori di data specificati?

Risposte:


404

Esistono due possibili soluzioni:

  • Usa una maschera booleana, quindi usa df.loc[mask]
  • Impostare la colonna della data come DatetimeIndex, quindi utilizzare df[start_date : end_date]

Usando una maschera booleana :

Assicurarsi che df['date']sia una serie con tipo datetime64[ns]:

df['date'] = pd.to_datetime(df['date'])  

Crea una maschera booleana. start_datee end_datepossono essere stringhe datetime.datetimes, np.datetime64s, pd.Timestamps o persino datetime:

#greater than the start date and smaller than the end date
mask = (df['date'] > start_date) & (df['date'] <= end_date)

Seleziona il sub-DataFrame:

df.loc[mask]

o riassegnare a df

df = df.loc[mask]

Per esempio,

import numpy as np
import pandas as pd

df = pd.DataFrame(np.random.random((200,3)))
df['date'] = pd.date_range('2000-1-1', periods=200, freq='D')
mask = (df['date'] > '2000-6-1') & (df['date'] <= '2000-6-10')
print(df.loc[mask])

i rendimenti

            0         1         2       date
153  0.208875  0.727656  0.037787 2000-06-02
154  0.750800  0.776498  0.237716 2000-06-03
155  0.812008  0.127338  0.397240 2000-06-04
156  0.639937  0.207359  0.533527 2000-06-05
157  0.416998  0.845658  0.872826 2000-06-06
158  0.440069  0.338690  0.847545 2000-06-07
159  0.202354  0.624833  0.740254 2000-06-08
160  0.465746  0.080888  0.155452 2000-06-09
161  0.858232  0.190321  0.432574 2000-06-10

Utilizzando un DatetimeIndex :

Se hai intenzione di fare molte selezioni per data, potrebbe essere più veloce impostare prima la datecolonna come indice. Quindi puoi selezionare le righe per data usando df.loc[start_date:end_date].

import numpy as np
import pandas as pd

df = pd.DataFrame(np.random.random((200,3)))
df['date'] = pd.date_range('2000-1-1', periods=200, freq='D')
df = df.set_index(['date'])
print(df.loc['2000-6-1':'2000-6-10'])

i rendimenti

                   0         1         2
date                                    
2000-06-01  0.040457  0.326594  0.492136    # <- includes start_date
2000-06-02  0.279323  0.877446  0.464523
2000-06-03  0.328068  0.837669  0.608559
2000-06-04  0.107959  0.678297  0.517435
2000-06-05  0.131555  0.418380  0.025725
2000-06-06  0.999961  0.619517  0.206108
2000-06-07  0.129270  0.024533  0.154769
2000-06-08  0.441010  0.741781  0.470402
2000-06-09  0.682101  0.375660  0.009916
2000-06-10  0.754488  0.352293  0.339337

Mentre l'indicizzazione dell'elenco Python, ad esempio seq[start:end]include, startma non end, al contrario, Pandas df.loc[start_date : end_date]include entrambi i punti finali nel risultato se si trovano nell'indice. Né start_dateend_datedeve essere nell'indice comunque.


Si noti inoltre che pd.read_csvha un parse_datesparametro che è possibile utilizzare per analizzare la datecolonna come datetime64s. Pertanto, se si utilizza parse_dates, non è necessario utilizzare df['date'] = pd.to_datetime(df['date']).


L'impostazione della colonna della data come indice funziona bene, ma non è chiaro dalla documentazione che ho visto che uno può farlo. Grazie.
Faheem Mitha,

@FaheemMitha: ho aggiunto un link qui sopra dove è documentata la "indicizzazione parziale delle stringhe".
unutbu,

La parte forse meno chiara è che un indice deve essere creato esplicitamente. E senza creare esplicitamente l'indice, un intervallo limitato restituisce un set vuoto, non un errore.
Faheem Mitha,

8
Dopo il df = df.set_index(['date'])passaggio, ho scoperto che anche l'indice deve essere ordinato (tramite df.sort_index(inplace=True, ascending=True)), poiché altrimenti è possibile ottenere risultati DataFrame meno che completi o addirittura vuoti df.loc['2000-6-1':'2000-6-10']. E se lo usi ascending=False, non funzionerà affatto, anche se lo invertirai condf.loc['2000-6-10':'2000-6-1']
bgoodr

1
Se vuoi mantenere la colonna 'date' mentre dai ancora il suo valore all'indice del dataframe, puoi farlo df.index = df ['date']
Richard Liang,

64

Penso che l'opzione migliore sarà quella di utilizzare i controlli diretti anziché utilizzare la funzione loc:

df = df[(df['date'] > '2000-6-1') & (df['date'] <= '2000-6-10')]

Per me funziona.

Il problema principale con la funzione loc con una sezione è che i limiti dovrebbero essere presenti nei valori effettivi, altrimenti ciò comporterà KeyError.


1
Penso che le sezioni via locsiano fantastiche. E mi sembra che, come dice unutbu, Né start_date né end_date devono essere nell'indice .
nealmcb,

come filtrare la data come (14 giorni prima della data corrente) .. se la data odierna è il 15-01-2019 ... ho bisogno dei dati dal (01-01-2019 al 15-01-2019)
Praveen Snowy

Semplice ed elegante. Grazie Christin, questo è quello che stavo cercando di fare. Per me va bene.
brohjoe,


19

È possibile utilizzare il isinmetodo sulla datecolonna in questo modo df[df["date"].isin(pd.date_range(start_date, end_date))]

Nota: funziona solo con le date (come richiesto dalla domanda) e non con i timestamp.

Esempio:

import numpy as np   
import pandas as pd

# Make a DataFrame with dates and random numbers
df = pd.DataFrame(np.random.random((30, 3)))
df['date'] = pd.date_range('2017-1-1', periods=30, freq='D')

# Select the rows between two dates
in_range_df = df[df["date"].isin(pd.date_range("2017-01-15", "2017-01-20"))]

print(in_range_df)  # print result

che dà

           0         1         2       date
14  0.960974  0.144271  0.839593 2017-01-15
15  0.814376  0.723757  0.047840 2017-01-16
16  0.911854  0.123130  0.120995 2017-01-17
17  0.505804  0.416935  0.928514 2017-01-18
18  0.204869  0.708258  0.170792 2017-01-19
19  0.014389  0.214510  0.045201 2017-01-20

9

Mantenendo la soluzione semplice e pitonica, ti suggerirei di provare questo.

Nel caso in cui lo farai frequentemente, la soluzione migliore sarebbe innanzitutto impostare la colonna della data come indice che convertirà la colonna in DateTimeIndex e utilizzare la seguente condizione per dividere qualsiasi intervallo di date.

import pandas as pd

data_frame = data_frame.set_index('date')

df = data_frame[(data_frame.index > '2017-08-10') & (data_frame.index <= '2017-08-15')]

4

Con il mio test della pandasversione 0.22.0ora puoi rispondere a questa domanda più facilmente con un codice più leggibile semplicemente usando between.

# create a single column DataFrame with dates going from Jan 1st 2018 to Jan 1st 2019
df = pd.DataFrame({'dates':pd.date_range('2018-01-01','2019-01-01')})

Supponiamo che tu voglia prendere le date tra il 27 novembre 2018 e il 15 gennaio 2019:

# use the between statement to get a boolean mask
df['dates'].between('2018-11-27','2019-01-15', inclusive=False)

0    False
1    False
2    False
3    False
4    False

# you can pass this boolean mask straight to loc
df.loc[df['dates'].between('2018-11-27','2019-01-15', inclusive=False)]

    dates
331 2018-11-28
332 2018-11-29
333 2018-11-30
334 2018-12-01
335 2018-12-02

Nota l'argomento inclusivo. molto utile quando vuoi essere esplicito sulla tua gamma. notare che quando impostato su True torniamo anche il 27 novembre 2018:

df.loc[df['dates'].between('2018-11-27','2019-01-15', inclusive=True)]

    dates
330 2018-11-27
331 2018-11-28
332 2018-11-29
333 2018-11-30
334 2018-12-01

Questo metodo è anche più veloce del isinmetodo precedentemente citato :

%%timeit -n 5
df.loc[df['dates'].between('2018-11-27','2019-01-15', inclusive=True)]
868 µs ± 164 µs per loop (mean ± std. dev. of 7 runs, 5 loops each)


%%timeit -n 5

df.loc[df['dates'].isin(pd.date_range('2018-01-01','2019-01-01'))]
1.53 ms ± 305 µs per loop (mean ± std. dev. of 7 runs, 5 loops each)

Tuttavia, non è più veloce della risposta attualmente accettata, fornita da unutbu, solo se la maschera è già stata creata . ma se la maschera è dinamica e deve essere riassegnata più volte, il mio metodo potrebbe essere più efficiente:

# already create the mask THEN time the function

start_date = dt.datetime(2018,11,27)
end_date = dt.datetime(2019,1,15)
mask = (df['dates'] > start_date) & (df['dates'] <= end_date)

%%timeit -n 5
df.loc[mask]
191 µs ± 28.5 µs per loop (mean ± std. dev. of 7 runs, 5 loops each)

4

Un'altra opzione, come raggiungere questo obiettivo, è utilizzando il pandas.DataFrame.query()metodo. Lascia che ti mostri un esempio nel seguente frame di dati chiamato df.

>>> df = pd.DataFrame(np.random.random((5, 1)), columns=['col_1'])
>>> df['date'] = pd.date_range('2020-1-1', periods=5, freq='D')
>>> print(df)
      col_1       date
0  0.015198 2020-01-01
1  0.638600 2020-01-02
2  0.348485 2020-01-03
3  0.247583 2020-01-04
4  0.581835 2020-01-05

Come argomento, usa la condizione per filtrare in questo modo:

>>> start_date, end_date = '2020-01-02', '2020-01-04'
>>> print(df.query('date >= @start_date and date <= @end_date'))
      col_1       date
1  0.244104 2020-01-02
2  0.374775 2020-01-03
3  0.510053 2020-01-04

Se non si desidera includere i limiti, è sufficiente modificare la condizione come segue:

>>> print(df.query('date > @start_date and date < @end_date'))
      col_1       date
2  0.374775 2020-01-03

3

Preferisco non modificare il df.

Una possibilità è quella di recuperare il indexdella starte enddate:

import numpy as np   
import pandas as pd

#Dummy DataFrame
df = pd.DataFrame(np.random.random((30, 3)))
df['date'] = pd.date_range('2017-1-1', periods=30, freq='D')

#Get the index of the start and end dates respectively
start = df[df['date']=='2017-01-07'].index[0]
end = df[df['date']=='2017-01-14'].index[0]

#Show the sliced df (from 2017-01-07 to 2017-01-14)
df.loc[start:end]

che si traduce in:

     0   1   2       date
6  0.5 0.8 0.8 2017-01-07
7  0.0 0.7 0.3 2017-01-08
8  0.8 0.9 0.0 2017-01-09
9  0.0 0.2 1.0 2017-01-10
10 0.6 0.1 0.9 2017-01-11
11 0.5 0.3 0.9 2017-01-12
12 0.5 0.4 0.3 2017-01-13
13 0.4 0.9 0.9 2017-01-14
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.