web-dev-qa-db-fra.com

GroupBy pandas DataFrame et sélectionnez la valeur la plus courante

J'ai un cadre de données avec trois colonnes de chaîne. Je sais que la seule valeur de la 3ème colonne est valable pour toutes les combinaisons des deux premières. Pour nettoyer les données, je dois grouper par trame de données en fonction des deux premières colonnes et sélectionner la valeur la plus commune de la troisième colonne pour chaque combinaison.

Mon code:

import pandas as pd
from scipy import stats

source = pd.DataFrame({'Country' : ['USA', 'USA', 'Russia','USA'], 
                  'City' : ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
                  'Short name' : ['NY','New','Spb','NY']})

print source.groupby(['Country','City']).agg(lambda x: stats.mode(x['Short name'])[0])

La dernière ligne de code ne fonctionne pas, elle indique "Erreur de clé 'Nom abrégé'" et si j'essaie de ne grouper que par ville, alors j'ai une erreur AssertionError. Que puis-je réparer?

41

Vous pouvez utiliser value_counts() pour obtenir une série de comptes et obtenir la première ligne:

import pandas as pd

source = pd.DataFrame({'Country' : ['USA', 'USA', 'Russia','USA'], 
                  'City' : ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
                  'Short name' : ['NY','New','Spb','NY']})

source.groupby(['Country','City']).agg(lambda x:x.value_counts().index[0])
91
HYRY

Pour agg, la fonction lambba obtient une Series, qui n'a pas d'attribut 'Short name'.

stats.mode renvoie un tuple de deux tableaux, vous devez donc prendre le premier élément du premier tableau de ce tuple.

Avec ces deux simples changements:

source.groupby(['Country','City']).agg(lambda x: stats.mode(x)[0][0])

résultats

                         Short name
Country City                       
Russia  Sankt-Petersburg        Spb
USA     New-York                 NY
15
eumiro

Un peu tard pour le jeu ici, mais je rencontrais des problèmes de performance avec la solution de HYRY, je devais donc en proposer un autre.

Cela fonctionne en recherchant la fréquence de chaque valeur-clé, puis en ne conservant que la valeur qui apparaît le plus souvent.

Il existe également une solution supplémentaire prenant en charge plusieurs modes.

Sur un test à l'échelle représentatif des données sur lesquelles je travaille, cette exécution a été réduite de 37,4 à 0,5 s!

Voici le code de la solution, un exemple d'utilisation et le test de la balance:

import numpy as np
import pandas as pd
import random
import time

test_input = pd.DataFrame(columns=[ 'key',          'value'],
                          data=  [[ 1,              'A'    ],
                                  [ 1,              'B'    ],
                                  [ 1,              'B'    ],
                                  [ 1,              np.nan ],
                                  [ 2,              np.nan ],
                                  [ 3,              'C'    ],
                                  [ 3,              'C'    ],
                                  [ 3,              'D'    ],
                                  [ 3,              'D'    ]])

def mode(df, key_cols, value_col, count_col):
    '''                                                                                                                                                                                                                                                                                                                                                              
    Pandas does not provide a `mode` aggregation function                                                                                                                                                                                                                                                                                                            
    for its `GroupBy` objects. This function is meant to fill                                                                                                                                                                                                                                                                                                        
    that gap, though the semantics are not exactly the same.                                                                                                                                                                                                                                                                                                         

    The input is a DataFrame with the columns `key_cols`                                                                                                                                                                                                                                                                                                             
    that you would like to group on, and the column                                                                                                                                                                                                                                                                                                                  
    `value_col` for which you would like to obtain the mode.                                                                                                                                                                                                                                                                                                         

    The output is a DataFrame with a record per group that has at least one mode                                                                                                                                                                                                                                                                                     
    (null values are not counted). The `key_cols` are included as columns, `value_col`                                                                                                                                                                                                                                                                               
    contains a mode (ties are broken arbitrarily and deterministically) for each                                                                                                                                                                                                                                                                                     
    group, and `count_col` indicates how many times each mode appeared in its group.                                                                                                                                                                                                                                                                                 
    '''
    return df.groupby(key_cols + [value_col]).size() \
             .to_frame(count_col).reset_index() \
             .sort_values(count_col, ascending=False) \
             .drop_duplicates(subset=key_cols)

def modes(df, key_cols, value_col, count_col):
    '''                                                                                                                                                                                                                                                                                                                                                              
    Pandas does not provide a `mode` aggregation function                                                                                                                                                                                                                                                                                                            
    for its `GroupBy` objects. This function is meant to fill                                                                                                                                                                                                                                                                                                        
    that gap, though the semantics are not exactly the same.                                                                                                                                                                                                                                                                                                         

    The input is a DataFrame with the columns `key_cols`                                                                                                                                                                                                                                                                                                             
    that you would like to group on, and the column                                                                                                                                                                                                                                                                                                                  
    `value_col` for which you would like to obtain the modes.                                                                                                                                                                                                                                                                                                        

    The output is a DataFrame with a record per group that has at least                                                                                                                                                                                                                                                                                              
    one mode (null values are not counted). The `key_cols` are included as                                                                                                                                                                                                                                                                                           
    columns, `value_col` contains lists indicating the modes for each group,                                                                                                                                                                                                                                                                                         
    and `count_col` indicates how many times each mode appeared in its group.                                                                                                                                                                                                                                                                                        
    '''
    return df.groupby(key_cols + [value_col]).size() \
             .to_frame(count_col).reset_index() \
             .groupby(key_cols + [count_col])[value_col].unique() \
             .to_frame().reset_index() \
             .sort_values(count_col, ascending=False) \
             .drop_duplicates(subset=key_cols)

print test_input
print mode(test_input, ['key'], 'value', 'count')
print modes(test_input, ['key'], 'value', 'count')

scale_test_data = [[random.randint(1, 100000),
                    str(random.randint(123456789001, 123456789100))] for i in range(1000000)]
scale_test_input = pd.DataFrame(columns=['key', 'value'],
                                data=scale_test_data)

start = time.time()
mode(scale_test_input, ['key'], 'value', 'count')
print time.time() - start

start = time.time()
modes(scale_test_input, ['key'], 'value', 'count')
print time.time() - start

start = time.time()
scale_test_input.groupby(['key']).agg(lambda x: x.value_counts().index[0])
print time.time() - start

L'exécution de ce code imprimera quelque chose comme:

   key value
0    1     A
1    1     B
2    1     B
3    1   NaN
4    2   NaN
5    3     C
6    3     C
7    3     D
8    3     D
   key value  count
1    1     B      2
2    3     C      2
   key  count   value
1    1      2     [B]
2    3      2  [C, D]
0.489614009857
9.19386196136
37.4375009537

J'espère que cela t'aides!

9
abw333

Formellement, la bonne réponse est la solution @eumiro . Le problème de la solution @HYRY est que, lorsque vous avez une suite de nombres comme [1,2,3,4], la solution est fausse: i. e., vous n'avez pas le mode. Exemple:

import pandas as pd
df = pd.DataFrame({'client' : ['A', 'B', 'A', 'B', 'B', 'C', 'A', 'D', 'D', 'E', 'E', 'E','E','E','A'], 'total' : [1, 4, 3, 2, 4, 1, 2, 3, 5, 1, 2, 2, 2, 3, 4], 'bla':[10, 40, 30, 20, 40, 10, 20, 30, 50, 10, 20, 20, 20, 30, 40]})

Si vous calculez comme @HYRY, vous obtenez:

df.groupby(['socio']).agg(lambda x: x.value_counts().index[0])

et vous obtenez:

 enter image description here

Ce qui est clairement faux (voir la valeur A qui devrait être 1 et non pas 4) car il ne peut pas gérer avec des valeurs uniques.

Ainsi, l'autre solution est correcte:

import scipy.stats
df3.groupby(['client']).agg(lambda x: scipy.stats.mode(x)[0][0])

obtenir:

 enter image description here

3
NunodeSousa

La réponse 2019, pd.Series.mode est disponible.

Utilisez groupby , GroupBy.agg , et appliquez la fonction pd.Series.mode à chaque groupe:

source.groupby(['Country','City'])['Short name'].agg(pd.Series.mode)

Country  City            
Russia   Sankt-Petersburg    Spb
USA      New-York             NY
Name: Short name, dtype: object

Si cela est nécessaire en tant que DataFrame, utilisez

source.groupby(['Country','City'])['Short name'].agg(pd.Series.mode).to_frame()

                         Short name
Country City                       
Russia  Sankt-Petersburg        Spb
USA     New-York                 NY

La chose utile à propos de Series.mode est qu’elle renvoie toujours une série, ce qui la rend très compatible avec agg et apply, en particulier lors de la reconstruction de la sortie groupby. C'est aussi plus rapide.

# Accepted answer.
%timeit source.groupby(['Country','City']).agg(lambda x:x.value_counts().index[0])
# Proposed in this post.
%timeit source.groupby(['Country','City'])['Short name'].agg(pd.Series.mode)

5.56 ms ± 343 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.76 ms ± 387 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Series.mode fait aussi du bon travail quand il y a plusieurs} modes:

source2 = source.append(
    pd.Series({'Country': 'USA', 'City': 'New-York', 'Short name': 'New'}),
    ignore_index=True)

# Now `source2` has two modes for the 
# ("USA", "New-York") group, they are "NY" and "New".
source2

  Country              City Short name
0     USA          New-York         NY
1     USA          New-York        New
2  Russia  Sankt-Petersburg        Spb
3     USA          New-York         NY
4     USA          New-York        New

source2.groupby(['Country','City'])['Short name'].agg(pd.Series.mode)

Country  City            
Russia   Sankt-Petersburg          Spb
USA      New-York            [NY, New]
Name: Short name, dtype: object

Ou, si vous souhaitez une ligne distincte pour chaque mode, vous pouvez utiliser GroupBy.apply :

source2.groupby(['Country','City'])['Short name'].apply(pd.Series.mode)

Country  City               
Russia   Sankt-Petersburg  0    Spb
USA      New-York          0     NY
                           1    New
Name: Short name, dtype: object

Si vous ne vous inquiétez pas du mode retourné tant qu'il en est un, vous aurez besoin d'un lambda qui appelle mode et extrait le premier résultat.

source2.groupby(['Country','City'])['Short name'].agg(
    lambda x: pd.Series.mode(x)[0])

Country  City            
Russia   Sankt-Petersburg    Spb
USA      New-York             NY
Name: Short name, dtype: object

Vous pouvez également utiliser statistics.mode from python, mais ...

source.groupby(['Country','City'])['Short name'].apply(statistics.mode)

Country  City            
Russia   Sankt-Petersburg    Spb
USA      New-York             NY
Name: Short name, dtype: object

... cela ne fonctionne pas bien lorsque plusieurs modes sont nécessaires; une StatisticsError est levée. Ceci est mentionné dans la documentation:

Si les données sont vides ou s'il n'y a pas exactement une valeur commune, StatisticsError est déclenché.

Mais vous pouvez voir par vous-même ...

statistics.mode([1, 2])
# ---------------------------------------------------------------------------
# StatisticsError                           Traceback (most recent call last)
# ...
# StatisticsError: no unique mode; found 2 equally common values
3
coldspeed

Le problème ici est la performance, si vous avez beaucoup de lignes ce sera un problème. 

Si c'est votre cas, essayez avec ceci:

import pandas as pd

source = pd.DataFrame({'Country' : ['USA', 'USA', 'Russia','USA'], 
              'City' : ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
              'Short_name' : ['NY','New','Spb','NY']})

source.groupby(['Country','City']).agg(lambda x:x.value_counts().index[0])

source.groupby(['Country','City']).Short_name.value_counts().groupby['Country','City']).first()
0
Diego Perez Sastre

Si vous voulez une autre solution pour résoudre ce problème qui ne dépend pas de value_counts ou scipy.stats, vous pouvez utiliser la collection Counter

from collections import Counter
get_most_common = lambda values: max(Counter(values).items(), key = lambda x: x[1])[0]

Ce qui peut être appliqué à l'exemple ci-dessus comme celui-ci

src = pd.DataFrame({'Country' : ['USA', 'USA', 'Russia','USA'], 
              'City' : ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
              'Short_name' : ['NY','New','Spb','NY']})

src.groupby(['Country','City']).agg(get_most_common)
0
kmader

Une approche légèrement maladroite mais plus rapide pour les grands ensembles de données consiste à obtenir les comptes d'une colonne d'intérêt, à trier les comptes par ordre croissant, puis à dédupliquer un sous-ensemble pour ne conserver que les observations les plus grandes.

import pandas as pd

source = pd.DataFrame({'Country' : ['USA', 'USA', 'Russia','USA'], 
              'City' : ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
              'Short name' : ['NY','New','Spb','NY']})

grouped_df = source.groupby(['Country','City','Short name']
                   )[['Short name']].count().rename(columns={ 
                   'Short name':'count'}).reset_index()
grouped_df = grouped_df.sort_values('count',ascending=False)
grouped_df = grouped_df.drop_duplicates(subset=['Country','City']).drop('count', axis=1)
grouped_df
0
Dimitri

Les deux principales réponses suggèrent:

df.groupby(cols).agg(lambda x:x.value_counts().index[0])

ou, de préférence

df.groupby(cols).agg(pd.Series.mode)

Cependant, ces deux solutions échouent dans les cas Edge simples, comme illustré ici:

df = pd.DataFrame({
    'client_id':['A', 'A', 'A', 'A', 'B', 'B', 'B', 'C'],
    'date':['2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01'],
    'location':['NY', 'NY', 'LA', 'LA', 'DC', 'DC', 'LA', np.NaN]
})

La première:

df.groupby(['client_id', 'date']).agg(lambda x:x.value_counts().index[0])

renvoie IndexError (en raison de la série vide renvoyée par le groupe C). La deuxième:

df.groupby(['client_id', 'date']).agg(pd.Series.mode)

renvoie ValueError: Function does not reduce, puisque le premier groupe renvoie une liste de deux (puisqu'il existe deux modes). (Comme documenté ici , si le premier groupe renvoyait un seul mode, cela fonctionnerait!)

Deux solutions possibles pour ce cas sont:

import scipy
x.groupby(['client_id', 'date']).agg(lambda x: scipy.stats.mode(x)[0])

Et la solution donnée par cs95 dans les commentaires ici :

def foo(x): 
    m = pd.Series.mode(x); 
    return m.values[0] if not m.empty else np.nan
df.groupby(['client_id', 'date']).agg(foo)

Cependant, tous ces éléments sont lents et ne conviennent pas aux grands ensembles de données. Une solution que j'ai finalement utilisée, qui a) peut traiter ces cas et b) est beaucoup, beaucoup plus rapide, est une version légèrement modifiée de la réponse de abw33 (qui devrait être plus élevée):

def get_mode_per_column(dataframe, group_cols, col):
    return (dataframe.fillna(-1)  # NaN placeholder to keep group 
            .groupby(group_cols + [col])
            .size()
            .to_frame('count')
            .reset_index()
            .sort_values('count', ascending=False)
            .drop_duplicates(subset=group_cols)
            .drop(columns=['count'])
            .sort_values(group_cols)
            .replace(-1, np.NaN))  # restore NaNs

group_cols = ['client_id', 'date']    
non_grp_cols = list(set(df).difference(group_cols))
output_df = get_mode_per_column(df, group_cols, non_grp_cols[0]).set_index(group_cols)
for col in non_grp_cols[1:]:
    output_df[col] = get_mode_per_column(df, group_cols, col)[col]

La méthode fonctionne essentiellement sur un col à la fois et génère un df. Ainsi, au lieu de concat, qui est intensif, vous traitez le premier comme un df, puis ajoutez de manière itérative le tableau de sortie (values.flatten() ) sous forme de colonne dans le df.

0
Josh Friedlander