2017-05-08 1 views
2

J'ai un grand dataframe tenant des utilisateurs de cartographie (index) pour compte d'éléments (colonnes):Comment vectoriser le calcul de pandas impliquant un groupement personnalisé?

users_items = pd.DataFrame(np.array([[0, 1, 1, 0],  # user 0 
            [1, 0, 0, 0],  # user 1 
            [5, 0, 0, 9],  # user 2 
            [0, 3, 5, 0],  # user 3 
            [0, 2, 2, 0],  # user 4 
            [7, 0, 0, 1],  # user 5 
            [3, 5, 0, 4]]), # user 6 
          columns=list('ABCD')) 

Pour chaque utilisateur, je veux trouver tous les utilisateurs qui ont compte non nuls pour au moins la même articles et additionner leurs comptes. Donc, pour l'utilisateur 1, ce serait les utilisateurs 1, 2, 5 et 6 et la somme des comptes est égale à [16, 5, 0, 14]. Cela peut être utilisé pour suggérer de nouveaux éléments aux utilisateurs en fonction des éléments que les utilisateurs "similaires" ont obtenus.

Cette implémentation naïve utilise une signature comme une expression régulière pour filtrer les lignes pertinentes et une boucle en boucle sur toutes les signatures:

def create_signature(request_counts): 
    return ''.join('x' if count else '.' for count in request_counts) 

users_items['signature'] = users_items.apply(create_signature, axis=1).astype('category') 

current_items = users_items.groupby('signature').sum() 

similar_items = pd.DataFrame(index=current_items.index, 
          columns=current_items.columns) 

for signature in current_items.index: 
    row = current_items.filter(regex=signature, axis='index').sum() 
    similar_items.loc[signature] = row 

Le résultat est:

  A B C D 
signature    
.xx.  0 6 8 0 
x...  16 5 0 14 
x..x  15 5 0 14 
xx.x  3 5 0 4 

Cela fonctionne bien, mais il est trop lent pour l'ensemble de données réel qui se compose de 100 000 utilisateurs et quelque 600 articles. La génération des signatures ne prend que 10 secondes, mais le bouclage de toutes les signatures (40k) prend plusieurs heures.

La vectorisation de la boucle devrait offrir une énorme amélioration des performances, mais mon expérience avec Pandas est limitée, donc je ne sais pas comment s'y prendre. Il est même possible de vectoriser ce type de calcul? Peut-être en utilisant des masques?

+0

Pourquoi y at-il seulement 4 signatures pour l'exemple? Avez-vous besoin d'autres combinaisons comme .x ..,. Xxx, x.xx etc? Pourquoi ces 4 sont-ils choisis? – Allen

+0

@Allen L'ensemble des signatures résulte simplement des données qui se trouvent dans la structure de données users_items. Toutes les signatures possibles ne se produiront pas. –

+0

donc 'x ...' devrait être la somme de toutes les lignes dont il est un sous-ensemble –

Répondre

0

Au lieu d'une string comme signature, vous pouvez utiliser un frozenset

def create_signature(request_counts): 
    return frozenset(request_counts[request_counts != 0].index) 

une alternative est

def create_signature(request_counts): 
    return frozenset(request_counts.replace({0: None}).dropna().index) 

Je n'ai pas un ensemble de données assez grand pour voir si l'on est plus rapide que la autre.

Si vous avez des colonnes en double, insérez un appel à reset_index() avant la .index

Cela vous permet de vectoriser votre filtre à la fin

for signature in current_items.index: 
    row = current_items[signature <= current_items.index].sum() 
    similar_items.loc[signature] = row 

résultats dans

signature     A B C D 
frozenset({'B', 'C'})  0 6 8 0 
frozenset({'A'})   16 5 0 14 
frozenset({'A', 'D'})  15 5 0 14 
frozenset({'B', 'A', 'D'}) 3 5 0 4 
+0

J'aurais pensé qu'utiliser frozensets (je pensais à un peu de tableaux moi-même) au lieu de chaînes pour les signatures serait beaucoup plus rapide. Malheureusement, générer les signatures frozenset prend beaucoup plus de temps (2+ minutes contre 10 secondes). Cela dit, en utilisant fronzesets la boucle ne prendra que 6 heures au lieu de 8, donc il y a une nette amélioration ici. Cependant, je pense que seule la vectorisation de la boucle for pourra accélérer suffisamment cette opération. –

+0

est-ce que la boucle for prend toujours la longueur de la chaîne, car je m'attendrais à ce que les comparaisons 'set' soient plus rapides que' regex'. Les tableaux de bits pourraient être encore plus rapides –

+0

Je l'ai fait après avoir soumis mon commentaire. Je l'ai mis à jour maintenant. –