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