3
votes

Comment trouver le nombre de lignes qui tombent dans une période de temps de chaque ligne, tout en satisfaisant les critères dans d'autres colonnes?

J'ai un exemple de dataframe avec une colonne de noms et une colonne de datetimes.

import time
import random
import datetime

Rows = []
Rik_Kraan = []
Willacya = []

for i in range(1000,50000,1000):
    
    Rows.append(i)
    
    # Creates Dataframe where number of names is 20% the length of the Dataframe.
    numberList = ["Name_"+str(j) for j in range(1,int(i*.2))]
    df_test = pd.DataFrame({'Date':pd.date_range(start='1/1/2020', freq='S', periods=i),
                 'Name':[random.choice(numberList) for x in range(i)]})
    
    # Rik_Kraan solution using masking
    start = time.time() 
    dates = df_test['Date'].values
    name = df_test['Name'].values
    df_test.assign(Total=np.sum((dates[:, None] <= (dates+pd.Timedelta(10, 'H'))) & (dates[:, None] >= dates) & (name[:, None] == name), axis=0))
    end = time.time()
    Rik_Kraan.append(end-start)

    # Original Solution
    start = time.time()
    for j in df_test.Name.unique():
        df2 = df_test[df_test.Name == j].copy()
        total = df2['Date'].apply(lambda x: len(df2[(df2.Date<=x) & (df2.Date>x - datetime.timedelta(hours = 1))]))
        df_test.loc[total.index,'Total'] = total.values
    end = time.time()
    Willacya.append(end-start)   
    
pd.DataFrame({'Num_Rows':Rows,'Rik_Kraan':Rik_Kraan,'Willacya':Willacya}).set_index('Num_Rows').plot()

Pour chaque ligne, j'essaie de trouver le nombre total de lignes qui ont une date dans les 10 heures après, et le nom correspond.

J'ai réussi à le faire avec le code ci-dessous, mais sur un ensemble de données beaucoup plus volumineux, cela prend une éternité. Y a-t-il mieux pour accomplir cela?

    Date                Name    Total
0   2020-01-01 09:00:00 James   3
1   2020-01-01 10:00:00 Sarah   3
2   2020-01-01 11:00:00 Sarah   2
3   2020-01-01 12:00:00 James   2
4   2020-01-01 13:00:00 Mark    2
5   2020-01-01 14:00:00 James   1
6   2020-01-01 15:00:00 Mark    1
7   2020-01-01 16:00:00 Sarah   1
8   2020-01-02 09:00:00 Mark    4
9   2020-01-02 10:00:00 Sarah   4
10  2020-01-02 11:00:00 Sarah   3
11  2020-01-02 12:00:00 Mark    3
12  2020-01-02 13:00:00 Sarah   2
13  2020-01-02 14:00:00 Sarah   1
14  2020-01-02 15:00:00 Mark    2
15  2020-01-02 16:00:00 Mark    1
16  2020-01-03 09:00:00 Sarah   3
17  2020-01-03 10:00:00 Sarah   2
18  2020-01-03 11:00:00 Mark    1
19  2020-01-03 12:00:00 Sarah   1

Résultat:

df['Total'] = 0
for i in df.Name.unique():
    df2 = df[df.Name == i]
    total = df2['Date'].apply(lambda x: len(df2[(df2.Date>=x) & (df2.Date<x + datetime.timedelta(hours = 10))]))
    df.loc[total.index,'Total'] = total.values
df

EDIT: Les données réelles sont d'au moins 80000 lignes et avec plus de 200 noms. La colonne Date est spécifique jusqu'à la seconde., La colonne Date contient des entrées en double où deux noms différents peuvent avoir la même date / heure, mais aucun nom unique n'aura plus d'une des mêmes entrées de date / heure.

ÉDITER-----------------------------------------------

J'ai noté la réponse de Rik Kraan bien qu'elle ait produit des résultats plus lents lors de l'utilisation de mes propres données. Pour cette raison, je voulais comparer les performances des deux méthodes. Ci-dessous exécute un test des deux comparant des tailles d'échantillon jusqu'à 50000 lignes par incréments de 1000 lignes. Pour mon cas d'utilisation spécifique, il semble que la solution de Rik soit plus rapide jusqu'à 48/49 mille lignes, après quoi la solution d'origine semble meilleure.

import random
np.random.seed(1)

numberList = ['Mark','James','Sarah']
df = pd.DataFrame({'Date':pd.date_range(start='1/1/2020', freq='BH', periods=20),
             'Name':[random.choice(numberList) for x in range(20)]})

    Date                Name
0   2020-01-01 09:00:00 James
1   2020-01-01 10:00:00 Sarah
2   2020-01-01 11:00:00 Sarah
3   2020-01-01 12:00:00 James
4   2020-01-01 13:00:00 Mark
5   2020-01-01 14:00:00 James
6   2020-01-01 15:00:00 Mark
7   2020-01-01 16:00:00 Sarah
8   2020-01-02 09:00:00 Mark
9   2020-01-02 10:00:00 Sarah
10  2020-01-02 11:00:00 Sarah
11  2020-01-02 12:00:00 Mark
12  2020-01-02 13:00:00 Sarah
13  2020-01-02 14:00:00 Sarah
14  2020-01-02 15:00:00 Mark
15  2020-01-02 16:00:00 Mark
16  2020-01-03 09:00:00 Sarah
17  2020-01-03 10:00:00 Sarah
18  2020-01-03 11:00:00 Mark
19  2020-01-03 12:00:00 Sarah

Comparaison d'algorithmes


0 commentaires

3 Réponses :


2
votes

Si vos données ne sont pas trop volumineuses, effectuez une fusion automatique sur le Name et la requête:

                  Date   Name  Total
0  2020-01-01 09:00:00  James      3
1  2020-01-01 10:00:00  Sarah      3
2  2020-01-01 11:00:00  Sarah      2
3  2020-01-01 12:00:00  James      2
4  2020-01-01 13:00:00   Mark      2
5  2020-01-01 14:00:00  James      1
6  2020-01-01 15:00:00   Mark      1
7  2020-01-01 16:00:00  Sarah      1
8  2020-01-02 09:00:00   Mark      4
9  2020-01-02 10:00:00  Sarah      4
10 2020-01-02 11:00:00  Sarah      3
11 2020-01-02 12:00:00   Mark      3
12 2020-01-02 13:00:00  Sarah      2
13 2020-01-02 14:00:00  Sarah      1
14 2020-01-02 15:00:00   Mark      2
15 2020-01-02 16:00:00   Mark      1
16 2020-01-03 09:00:00  Sarah      3
17 2020-01-03 10:00:00  Sarah      2
18 2020-01-03 11:00:00   Mark      1
19 2020-01-03 12:00:00  Sarah      1

Production:

df['Total'] = (df.reset_index().merge(df, on='Name')
   .loc[lambda x: (x.Date_y-x.Date_x<thresh) & (x.Date_x <= x.Date_y)]
   .groupby('index').size()
)


1 commentaires

Excellente solution, mais mes données de cas réel sont de 80000 x 2 avec 200 noms uniques et font que mon ordinateur portable manque de mémoire.



1
votes

Vous pouvez créer des colonnes décalées qui incluent les noms dans les 10 heures suivantes. Si nous comparons ces colonnes avec le Name origine, nous obtenons plusieurs colonnes booléennes indiquant si le nom dans la colonne Name est présent dans les lignes suivantes. Une simple somme sur les lignes obtient alors la colonne Total prévue.

CPU times: user 9.76 ms, sys: 4.41 ms, total: 14.2 ms
Wall time: 12.1 ms

Votre solution avait un temps CPU de 24,9 ms

CPU times: user 24.9 ms, sys: 0 ns, total: 24.9 ms
Wall time: 21.8 ms

Ma solution proposée est un peu plus rapide:

# Make copy of the original dataframe and set the Date column as index
df_shifted = df.set_index('Date')

# Loop over the coming 10 hours and create shifted columns
for i in range(1,10):
    df_shifted[i] = df_shifted.shift(periods=-i, freq='H')['Name']
    # Compare with the original Name column
    df_shifted[i] = df_shifted[i] == df_shifted['Name']

# Set the original Name column to True (as we want to count these names as well)
df_shifted['Name'] = True

# Assign new total column to the original dataframe
df.assign(Total=df2.sum(axis=1).values)

J'espère que c'est utile


3 commentaires

Merci, c'est une excellente solution à mon problème proposé, mais la colonne datetime dans mes données de cas réel a de nombreuses entrées en double. L'index devrait être groupé par nom et date pour que chaque ligne soit unique, sinon j'obtiens l'erreur ne peut pas réindexer à partir d'un axe en double.


Edit: Il convient également de noter que les données réelles sont spécifiques jusqu'à la seconde, ce qui pourrait ne pas être efficace pour déplacer et créer une colonne pour la variable de temps.


Ah, je vois, tu as raison. Permettez-moi de publier un autre exemple qui pourrait fonctionner avec la diffusion Numpy



1
votes

Nous pouvons également utiliser la diffusion numpy . Essentiellement, pour chaque ligne, nous voulons compter le nombre de lignes dans un intervalle de temps de 10 heures qui ont le même name .

Commencez par créer des tableaux numpy des colonnes d'intérêt

df.assign(Total=np.sum((dates[:, None] <= (dates+pd.Timedelta(10, 'H'))) & (dates[:, None] >= dates) & (name[:, None] == name), axis=0))

Ensuite, créez un masque en comparant les lignes entre elles. Cela donne un tableau de forme number_of_rows * number_of_rows

(dates[:, None] <= (dates+pd.Timedelta(10, 'H'))) & (dates[:, None] >= dates) & (name[:, None] == name)

Enfin, nous pouvons prendre la somme de chaque colonne, qui nous fournit le nombre total de noms identiques dans les 10 prochaines heures, et l'affecter à une nouvelle colonne.

dates = df['Date'].values
name = df['Name'].values


2 commentaires

Merci, j'aime cette solution, elle s'exécute beaucoup plus rapidement sur les données ci-dessus, bien qu'elle soit beaucoup plus lente lors de l'utilisation d'un plus grand ensemble de données. Je viens de tester les deux sur un ensemble de données de taille 80,800,8000,80000 lignes. Votre solution est plus de deux fois plus rapide sur chaque ensemble de données en plus du dernier où elle est entre 4 et 6 fois plus lente que la taille des masques augmente de façon exponentielle.


Je dois noter que j'ai utilisé ces ensembles de données de taille car celui que j'ai fait au moins 80000 lignes. Je marquerai votre réponse correcte s'il n'y a pas d'autres solutions.