web-dev-qa-db-fra.com

Sélectionner des lignes DataFrame entre deux dates

Je crée un DataFrame à partir d'un CSV comme suit:

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

Le DataFrame a une colonne de date. Existe-t-il un moyen de créer un nouveau DataFrame (ou simplement d'écraser celui existant) qui contient uniquement des lignes avec des valeurs de date comprises dans une plage de dates spécifiée ou entre deux valeurs de date spécifiées?

116
darkpool

Il y a deux solutions possibles:

  • Utilisez un masque booléen, puis utilisez df.loc[mask]
  • Définissez la colonne de date comme un DatetimeIndex, puis utilisez df[start_date : end_date]

Utiliser un masque booléen:

Assurez-vous que df['date'] est une série avec dtype datetime64[ns]:

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

Fabriquez un masque booléen. start_date et end_date peuvent être datetime.datetimes, np.datetime64s, pd.Timestamps ou même des chaînes de date/heure:

mask = (df['date'] > start_date) & (df['date'] <= end_date)

Sélectionnez le sous-DataFrame:

df.loc[mask]

ou réaffecter à df

df = df.loc[mask]

Par exemple,

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

les rendements

            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

Utilisation d'un DatetimeIndex:

Si vous allez effectuer beaucoup de sélections par date, il peut être plus rapide de définir d'abord la colonne date comme index. Ensuite, vous pouvez sélectionner des lignes par date à l'aide de 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'])

les rendements

                   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

Lors de l'indexation de liste Python, par exemple, seq[start:end] inclut start mais pas end; en revanche, les pandas df.loc[start_date : end_date] incluent les deux extrémités du résultat si elles figurent dans l'index. Cependant, ni start_date ni end_date ne doivent être dans l'index.


Notez également que pd.read_csv A UN PARAM&EGRAVE;TRE parse_dates que vous pouvez utiliser pour analyser la colonne date en tant que datetime64s. Ainsi, si vous utilisez parse_dates, vous n’auriez pas besoin d’utiliser df['date'] = pd.to_datetime(df['date'])

256
unutbu

Je pense que la meilleure option sera d'utiliser les contrôles directs plutôt que d'utiliser la fonction loc:

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

Ça marche pour moi.

Le problème majeur avec la fonction loc avec une tranche est que les limites doivent être présentes dans les valeurs réelles, sinon KeyError sera généré.

32
Christin Jose

Vous pouvez utiliser la méthode isin sur la colonne date comme suit df[df["date"].isin(pd.date_range(start_date, end_date))]

Remarque: Cela fonctionne uniquement avec les dates (comme le demande la question) et non les horodatages.

Exemple:  

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

qui donne

           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
18
Jonny Brooks

Vous pouvez également utiliser between:

df[df.some_date.between(start_date, end_date)]
13
pomber

Si vous envisagez de le faire fréquemment, la meilleure solution consiste à définir d'abord la colonne de date comme index, qui convertira la colonne en DateTimeIndex et à utiliser la condition suivante pour découper toute plage de dates.

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

Je préfère ne pas modifier le df.

Une option consiste à récupérer la index des dates start et end:

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]

qui se traduit par:

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

Avec mon test de pandas version 0.22.0, vous pouvez maintenant répondre à cette question plus facilement avec un code plus lisible en utilisant simplement 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')})

Disons que vous voulez saisir les dates entre le 27 novembre 2018 et le 15 janvier 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

Remarquez l'argument inclusif. very utile lorsque vous voulez être explicite sur votre gamme. remarquez que, lorsqu'il est défini sur True, nous retournons également le 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

Cette méthode est également plus rapide que la méthode isin mentionnée précédemment:

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

Cependant, il est pas plus rapide que la réponse actuellement acceptée, fournie par unutbu, uniquement si le masque est déjà créé . mais si le masque est dynamique et doit être réaffecté encore et encore, ma méthode peut est plus efficace:

# 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)
0
MattR