web-dev-qa-db-fra.com

Alternative plus rapide pour effectuer des opérations groupby de pandas

J'ai un ensemble de données avec le nom (nom_personne), le jour et la couleur (chemise_couleur) sous forme de colonnes.

Chaque personne porte une chemise d'une certaine couleur un jour donné. Le nombre de jours peut être arbitraire.

Par exemple. contribution:

name  day  color
----------------
John   1   White
John   2   White
John   3   Blue
John   4   Blue
John   5   White
Tom    2   White
Tom    3   Blue
Tom    4   Blue
Tom    5   Black
Jerry  1   Black
Jerry  2   Black
Jerry  4   Black
Jerry  5   White

Je dois trouver la couleur la plus fréquemment utilisée par chaque personne.

Par exemple. résultat:

name    color
-------------
Jerry   Black
John    White
Tom     Blue

J'effectue l'opération suivante pour obtenir les résultats, ce qui fonctionne bien mais est assez lent:

most_frquent_list = [[name, group.color.mode()[0]] 
                        for name, group in data.groupby('name')]
most_frquent_df = pd.DataFrame(most_frquent_list, columns=['name', 'color'])

Supposons maintenant que j'ai un jeu de données avec 5 millions de noms uniques. Quel est le meilleur/le plus rapide moyen d'effectuer l'opération ci-dessus?

5
astrobiologist

numpy.add.at et pandas.factorize de Numpy

Ceci est destiné à être rapide. Cependant, j'ai essayé de l'organiser pour qu'il soit lisible aussi.

i, r = pd.factorize(df.name)
j, c = pd.factorize(df.color)
n, m = len(r), len(c)

b = np.zeros((n, m), dtype=np.int64)

np.add.at(b, (i, j), 1)
pd.Series(c[b.argmax(1)], r)

John     White
Tom       Blue
Jerry    Black
dtype: object

groupby, size et idxmax

df.groupby(['name', 'color']).size().unstack().idxmax(1)

name
Jerry    Black
John     White
Tom       Blue
dtype: object

name
Jerry    Black
John     White
Tom       Blue
Name: color, dtype: object

Counter

¯\_(ツ)_/¯

from collections import Counter

df.groupby('name').color.apply(lambda c: Counter(c).most_common(1)[0][0])

name
Jerry    Black
John     White
Tom       Blue
Name: color, dtype: object
5
piRSquared

Solution depd.Series.mode

df.groupby('name').color.apply(pd.Series.mode).reset_index(level=1,drop=True)
Out[281]: 
name
Jerry    Black
John     White
Tom       Blue
Name: color, dtype: object
4
Wen-Ben

METTRE À JOUR

Cela doit être difficile à battre (environ 10 fois plus rapide sur le daraframe échantillon que toute solution de pandas proposée et 1,5 fois plus rapide que la solution numpy proposée). Le Gist consiste à rester à l’écart des pandas et à utiliser itertools.groupby, qui fait un bien meilleur travail quand il s’agit de données non numériques.

from itertools import groupby
from collections import Counter

pd.Series({x: Counter(z[-1] for z in y).most_common(1)[0][0] for x,y 
          in groupby(sorted(df.values.tolist()), 
                            key=lambda x: x[0])})
# Jerry    Black
# John     White
# Tom       Blue

Old Answer

Voici une autre méthode. Il est en réalité plus lent que l'original, mais je le garde ici:

data.groupby('name')['color']\
    .apply(pd.Series.value_counts)\
    .unstack().idxmax(axis=1)
# name
# Jerry    Black
# John     White
# Tom       Blue
4
DYZ

Pourquoi ne pas faire deux groupements avec transform(max)?

df = df.groupby(["name", "color"], as_index=False, sort=False).count()
idx = df.groupby("name", sort=False).transform(max)["day"] == df["day"]
df = df[idx][["name", "color"]].reset_index(drop=True)

Sortie:

    name  color
0   John  White
1    Tom   Blue
2  Jerry  Black
2
André C. Andersen

Similaire aux pd.factorize et np.add.at ans de @ piRSquared.

Nous encodons les piqûres dans les colonnes en utilisant 

i, r = pd.factorize(df.name)
j, c = pd.factorize(df.color)
n, m = len(r), len(c)
b = np.zeros((n, m), dtype=np.int64)

Mais alors, au lieu de faire ceci:

np.add.at(b, (i, j), 1)
max_columns_after_add_at = b.argmax(1)

Nous obtenons le max_columns_after_add_at en utilisant une fonction jited, pour ajouter et trouver le maximum dans la même boucle:

@nb.jit(nopython=True, cache=True)
def add_at(x, rows, cols, val):
    max_vals = np.zeros((x.shape[0], ), np.int64)
    max_inds = np.zeros((x.shape[0], ), np.int64)
    for i in range(len(rows)):
        r = rows[i]
        c = cols[i]
        x[r, c]+=1
        if(x[r, c] > max_vals[r]):
            max_vals[r] = x[r, c]
            max_inds[r] = c
    return max_inds

Et puis obtenir le dataframe à la fin, 

ans = pd.Series(c[max_columns_after_add_at], r)

La différence est donc comment nous fonctionnons argmax(axis=1) after np.add.at().

Analyse temporelle

import numpy as np
import numba as nb
m = 100000
n = 100000
rows = np.random.randint(low = 0, high = m, size=10000)
cols = np.random.randint(low = 0, high = n, size=10000)

Donc ça:

%%time
x = np.zeros((m,n))
np.add.at(x, (rows, cols), 1)
maxs = x.argmax(1)

donne:

Temps CPU: utilisateur 12,4 s, sys: 38 s, total: 50,4 s Temps mural: 50,5 s

Et ça

%%time
x = np.zeros((m,n))
maxs2 = add_at(x, rows, cols, 1)

donne

Temps CPU: utilisateur 108 ms, sys: 39.4 s, total: 39.5 s Temps mural: 38.4 s

0
Deepak Saini