92 votes

Fonction d'agrégation de DataFrame Pandas utilisant plusieurs colonnes

Existe-t-il un moyen d'écrire une fonction d'agrégation telle qu'elle est utilisée dans le document DataFrame.agg qui aurait accès à plus d'une colonne des données qui sont agrégées ? Les cas d'utilisation typiques sont les fonctions de moyenne pondérée et d'écart type pondéré.

Je voudrais pouvoir écrire quelque chose comme

def wAvg(c, w):
    return ((c * w).sum() / w.sum())

df = DataFrame(....) # df has columns c and w, i want weighted average
                     # of c using w as weight.
df.aggregate ({"c": wAvg}) # and somehow tell it to use w column as weights ...

114voto

Wes McKinney Points 17545

Oui, utilisez le .apply(...) qui sera appelée sur chaque sous-produit DataFrame . Par exemple :

grouped = df.groupby(keys)

def wavg(group):
    d = group['data']
    w = group['weights']
    return (d * w).sum() / w.sum()

grouped.apply(wavg)

13voto

Ted Petrou Points 20559

Il est possible de renvoyer n'importe quel nombre de valeurs agrégées à partir d'un objet groupby avec apply . Il suffit de renvoyer une série et les valeurs d'index deviendront les nouveaux noms de colonnes.

Voyons un exemple rapide :

df = pd.DataFrame({'group':['a','a','b','b'],
                   'd1':[5,10,100,30],
                   'd2':[7,1,3,20],
                   'weights':[.2,.8, .4, .6]},
                 columns=['group', 'd1', 'd2', 'weights'])
df

  group   d1  d2  weights
0     a    5   7      0.2
1     a   10   1      0.8
2     b  100   3      0.4
3     b   30  20      0.6

Définir une fonction personnalisée qui sera transmise à apply . Il accepte implicitement un DataFrame - ce qui signifie que l'élément data est un DataFrame. Remarquez qu'il utilise plusieurs colonnes, ce qui n'est pas possible avec la fonction agg méthode groupby :

def weighted_average(data):
    d = {}
    d['d1_wa'] = np.average(data['d1'], weights=data['weights'])
    d['d2_wa'] = np.average(data['d2'], weights=data['weights'])
    return pd.Series(d)

Appelez le groupby apply avec notre fonction personnalisée :

df.groupby('group').apply(weighted_average)

       d1_wa  d2_wa
group              
a        9.0    2.2
b       58.0   13.2

Vous pouvez obtenir de meilleures performances en précalculant les totaux pondérés dans de nouvelles colonnes de DataFrame, comme expliqué dans d'autres réponses, et en évitant d'utiliser la fonction apply tout à fait.

9voto

ErnestScribbler Points 436

Ma solution est similaire à celle de Nathaniel, sauf qu'elle ne concerne qu'une seule colonne et que je n'effectue pas de copie profonde de l'ensemble du cadre de données à chaque fois, ce qui pourrait être excessivement lent. Le gain de performance par rapport à la solution groupby(...).apply(...) est d'environ 100x( !).

def weighted_average(df, data_col, weight_col, by_col):
    df['_data_times_weight'] = df[data_col] * df[weight_col]
    df['_weight_where_notnull'] = df[weight_col] * pd.notnull(df[data_col])
    g = df.groupby(by_col)
    result = g['_data_times_weight'].sum() / g['_weight_where_notnull'].sum()
    del df['_data_times_weight'], df['_weight_where_notnull']
    return result

5voto

santon Points 1658

Je le fais souvent et j'ai trouvé ce qui suit très pratique :

def weighed_average(grp):
    return grp._get_numeric_data().multiply(grp['COUNT'], axis=0).sum()/grp['COUNT'].sum()
df.groupby('SOME_COL').apply(weighed_average)

Cela permettra de calculer la moyenne pondérée de toutes les colonnes numériques dans le fichier df et de laisser tomber ceux qui ne sont pas numériques.

4voto

Nathaniel Points 51

Pour y parvenir, il faut groupby(...).apply(...) n'est pas performant. Voici une solution que j'utilise tout le temps (en utilisant essentiellement la logique de kalu).

def grouped_weighted_average(self, values, weights, *groupby_args, **groupby_kwargs):
   """
    :param values: column(s) to take the average of
    :param weights_col: column to weight on
    :param group_args: args to pass into groupby (e.g. the level you want to group on)
    :param group_kwargs: kwargs to pass into groupby
    :return: pandas.Series or pandas.DataFrame
    """

    if isinstance(values, str):
        values = [values]

    ss = []
    for value_col in values:
        df = self.copy()
        prod_name = 'prod_{v}_{w}'.format(v=value_col, w=weights)
        weights_name = 'weights_{w}'.format(w=weights)

        df[prod_name] = df[value_col] * df[weights]
        df[weights_name] = df[weights].where(~df[prod_name].isnull())
        df = df.groupby(*groupby_args, **groupby_kwargs).sum()
        s = df[prod_name] / df[weights_name]
        s.name = value_col
        ss.append(s)
    df = pd.concat(ss, axis=1) if len(ss) > 1 else ss[0]
    return df

pandas.DataFrame.grouped_weighted_average = grouped_weighted_average

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X