2 votes

Python - Différence vectorielle de dates dans un tableau de 1 million de lignes

J'ai le dataframe pandas suivant :

Date                    
2018-04-10 21:05:00        
2018-04-10 21:05:00        
2018-04-10 21:10:00        
2018-04-10 21:15:00     
2018-04-10 21:35:00     

Mon objectif est de calculer le nombre de lignes qui se trouvent 20 minutes avant et 20 minutes après chaque heure (y compris les lignes ayant la même heure avant et après). Quelque chose comme ce qui suit :

Date                   nr_20_min_bef    nr_20_min_after   
2018-04-10 21:05:00          2                 4                                 
2018-04-10 21:05:00          2                 4  
2018-04-10 21:10:00          3                 2
2018-04-10 21:15:00          4                 2
2018-04-10 21:35:00          2                 1

J'ai essayé d'exécuter une boucle for pour itérer sur toutes les lignes, le problème est que la série entière a plus d'un million de lignes, donc je cherchais une solution plus efficace. Mon approche actuelle consiste à utiliser les fonctions pandas :

import datetime
import pandas

df = pd.DataFrame(pd.to_datetime(['2018-04-10 21:05:00',        
'2018-04-10 21:05:00',        
'2018-04-10 21:10:00',        
'2018-04-10 21:15:00',     
'2018-04-10 21:35:00']),columns = ['Date'])

nr_20_min_bef = []
nr_20_min_after = []

for i in range(0, len(df)):
    nr_20_min_bef.append(df.Date.between(df.Date[i] - 
pd.offsets.DateOffset(minutes=20), df.Date[i], inclusive = True).sum())
    nr_20_min_after.append(df.Date.between(df.Date[i], df.Date[i] + 
pd.offsets.DateOffset(minutes=20), inclusive = True).sum())

Une solution vectorielle serait probablement idéale dans ce cas, mais je ne sais pas vraiment comment m'y prendre.

Merci d'avance.

4voto

unutbu Points 222216

La bonne nouvelle est qu'il est possible de vectoriser cela. La mauvaise nouvelle est que... ce n'est pas très simple.

Voici l'analyse comparative perfplot code :

import numpy as np
import pandas as pd
import perfplot

def orig(df):
    nr_20_min_bef = []
    nr_20_min_after = []

    for i in range(0, len(df)):
        nr_20_min_bef.append(df.Date.between(
            df.Date[i] - pd.offsets.DateOffset(minutes=20), df.Date[i], inclusive = True).sum())
        nr_20_min_after.append(df.Date.between(
            df.Date[i], df.Date[i] + pd.offsets.DateOffset(minutes=20), inclusive = True).sum())
    df['nr_20_min_bef'] = nr_20_min_bef
    df['nr_20_min_after'] = nr_20_min_after
    return df

def alt(df):
    df = df.copy()
    df['Date'] = pd.to_datetime(df['Date'])
    df['num'] = 1
    df = df.set_index('Date')

    dup_count = df.groupby(level=0)['num'].count()
    result = dup_count.rolling('20T', closed='both').sum()
    df['nr_20_min_bef'] = result.astype(int)

    max_date = df.index.max()
    min_date = df.index.min()
    dup_count_reversed = df.groupby((max_date - df.index)[::-1] + min_date)['num'].count()
    result = dup_count_reversed.rolling('20T', closed='both').sum()
    result = pd.Series(result.values[::-1], dup_count.index)
    df['nr_20_min_after'] = result.astype(int)
    df = df.drop('num', axis=1)
    df = df.reset_index()
    return df

def make_df(N):
    dates = (np.array(['2018-04-10'], dtype='M8[m]') 
             + (np.random.randint(10, size=N).cumsum()).astype('<i8').astype('<m8[m]'))
    df = pd.DataFrame({'Date': dates})
    return df

def check(df1, df2):
    return df1.equals(df2)

perfplot.show(
    setup=make_df,
    kernels=[orig, alt],
    n_range=[2**k for k in range(4,10)],
    logx=True,
    logy=True,
    xlabel='N',
    equality_check=check)

qui montre alt est significativement plus rapide que orig : enter image description here

En plus de l'analyse comparative orig y alt , perfplot.show vérifie également que les DataFrames renvoyées par orig y alt sont égaux. Compte tenu de la complexité de alt cela nous donne au moins l'assurance qu'il se comporte de la même façon que orig .

Il est un peu difficile de faire un perfplot pour un grand N puisque orig commence prend un temps assez long et chaque repère est répété des centaines de fois. Donc voici quelques points %timeit comparaisons pour les plus grands N :

| N     | orig (ms) | alt (ms) |
|-------+-----------+----------|
| 2**10 |      3040 |     9.32 |
| 2**12 |     12600 |     10.8 |
| 2**20 |         ? |      909 |

In [300]: df = make_df(2**10)
In [301]: %timeit orig(df)
1 loop, best of 3: 3.04 s per loop
In [302]: %timeit alt(df)
100 loops, best of 3: 9.32 ms per loop
In [303]: df = make_df(2**12)
In [304]: %timeit orig(df)
1 loop, best of 3: 12.6 s per loop
In [305]: %timeit alt(df)
100 loops, best of 3: 10.8 ms per loop
In [306]: df = make_df(2**20)
In [307]: %timeit alt(df)
1 loop, best of 3: 909 ms per loop

Maintenant, qu'est-ce que alt faire ? Le plus simple est peut-être d'examiner un petit exemple en utilisant la fonction df vous avez posté :

df = pd.DataFrame(pd.to_datetime(['2018-04-10 21:05:00',        
                                  '2018-04-10 21:05:00',        
                                  '2018-04-10 21:10:00',        
                                  '2018-04-10 21:15:00',     
                                  '2018-04-10 21:35:00']),columns = ['Date'])

L'idée principale est d'utiliser Series.rolling pour effectuer une somme roulante. Lorsque la série a un DatetimeIndex, Series.rolling peut accepter une fréquence temporelle pour le taille de la fenêtre. On peut donc calculer des sommes glissantes avec des fenêtres variables d'une durée fixe. fixe. La première étape consiste donc à faire des dates un DatetimeIndex :

df['Date'] = pd.to_datetime(df['Date'])
df['num'] = 1
df = df.set_index('Date')

Desde df a des dates en double, regroupez par les valeurs DatetimeIndex et comptez le nombre de doublons :

dup_count = df.groupby(level=0)['num'].count()
# Date
# 2018-04-10 21:05:00    2
# 2018-04-10 21:10:00    1
# 2018-04-10 21:15:00    1
# 2018-04-10 21:35:00    1
# Name: num, dtype: int64

Maintenant, calculer la somme roulante sur dup_count :

result = dup_count.rolling('20T', closed='both').sum()
# Date
# 2018-04-10 21:05:00    2.0
# 2018-04-10 21:10:00    3.0
# 2018-04-10 21:15:00    4.0
# 2018-04-10 21:35:00    2.0
# Name: num, dtype: float64

Viola, c'est nr_20_min_bef . 20T spécifie la taille de la fenêtre d'une durée de 20 minutes. closed='both' spécifie que chaque fenêtre comprend à la fois ses points d'extrémité gauche et droite.

Maintenant, si seulement l'informatique nr_20_min_after étaient aussi simples. En théorie, tout ce que nous devons faire est d'inverser l'ordre des lignes dans dup_count et calculez une autre somme mobile. Malheureusement, Series.rolling exige que le DatetimeIndex soit monotoniquement augmentation de :

In [275]: dup_count[::-1].rolling('20T', closed='both').sum()
ValueError: index must be monotonic

Comme le chemin évident est bloqué, nous prenons un détour :

max_date = df.index.max()
min_date = df.index.min()
dup_count_reversed = df.groupby((max_date - df.index)[::-1] + min_date)['num'].count()
# Date
# 2018-04-10 21:05:00    1
# 2018-04-10 21:25:00    1
# 2018-04-10 21:30:00    1
# 2018-04-10 21:35:00    2
# Name: num, dtype: int64

Cela génère un nouveau pseudo-datetime DatetimeIndex pour le regroupement :

In [288]: (max_date - df.index)[::-1] + min_date
Out[288]: 
DatetimeIndex(['2018-04-10 21:05:00', '2018-04-10 21:25:00',
               '2018-04-10 21:30:00', '2018-04-10 21:35:00',
               '2018-04-10 21:35:00'],
              dtype='datetime64[ns]', name='Date', freq=None)

Ces valeurs peuvent ne pas être dans df.index -- mais ça ne fait rien. La seule chose dont nous avons besoin, c'est que les valeurs augmentent de façon monotone et que les différence entre les dates correspondent aux différences dans df.index lorsqu'il est inversé.

Maintenant, en utilisant ce dup_count inversé, nous pouvons profiter de la grande victoire (en performance) en prenant la somme roulante :

result = dup_count_reversed.rolling('20T', closed='both').sum()
# Date
# 2018-04-10 21:05:00    1.0
# 2018-04-10 21:25:00    2.0
# 2018-04-10 21:30:00    2.0
# 2018-04-10 21:35:00    4.0
# Name: num, dtype: float64

result a les valeurs que nous souhaitons pour nr_20_min_after mais dans l'ordre inverse, et avec le mauvais indice. Voici comment nous pouvons corriger cela :

result = pd.Series(result.values[::-1], dup_count.index)
# Date
# 2018-04-10 21:05:00    4.0
# 2018-04-10 21:10:00    2.0
# 2018-04-10 21:15:00    2.0
# 2018-04-10 21:35:00    1.0
# dtype: float64

Et c'est à peu près tout ce qu'il y a à faire alt .

1voto

Ben.T Points 8450

Je pense que vous pouvez utiliser apply même si ce n'est pas de manière vectorielle, cela devrait être plus rapide qu'avec un for boucle comme :

#first create the timedelta of 20 minutes
dt_20 = pd.Timedelta(minutes=20)
# then apply on the first column
df['nr_20_min_bef'] = df['Date'].apply(lambda x: df['Date'][((x - dt_20) <= df['Date'] ) 
                                                            & (x >=df['Date'])].count())

df['nr_20_min_after'] = df['Date'].apply(lambda x: df['Date'][(x <= df['Date'] )& 
                                                              ((x + dt_20) >= df['Date'])].count())

Après avoir fait quelques %timeit il semble que l'utilisation du between est un peu plus rapide qu'avec la méthode mask pour que vous puissiez faire

df['nr_20_min_bef'] = df['Date'].apply(lambda x: df.Date.between(x - dt_20, 
                                                                 x, inclusive = True).sum())

et idem pour l'après.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X