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
3 Réponses :
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() )
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.
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
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
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
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.