web-dev-qa-db-fra.com

Pandas Fonction d'agrégation DataFrame utilisant plusieurs colonnes

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

J'aimerais 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 ...
62
user1444817

Oui; utilisez la fonction .apply(...), qui sera appelée sur chaque sous -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)
84
Wes McKinney

Ma solution est similaire à la solution de Nathaniel, seulement pour une seule colonne et je ne copie pas en profondeur la trame de données entière à chaque fois, ce qui pourrait être excessivement lent. Le gain de performances par rapport à la solution groupby (...). Apply (...) est 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
5
ErnestScribbler

Ce qui suit (basé sur la réponse de Wes McKinney) accomplit exactement ce que je cherchais. Je serais heureux de savoir s'il existe un moyen plus simple de le faire dans pandas.

def wavg_func(datacol, weightscol):
    def wavg(group):
        dd = group[datacol]
        ww = group[weightscol] * 1.0
        return (dd * ww).sum() / ww.sum()
    return wavg


def df_wavg(df, groupbycol, weightscol):
    grouped = df.groupby(groupbycol)
    df_ret = grouped.agg({weightscol:sum})
    datacols = [cc for cc in df.columns if cc not in [groupbycol, weightscol]]
    for dcol in datacols:
        try:
            wavg_f = wavg_func(dcol, weightscol)
            df_ret[dcol] = grouped.apply(wavg_f)
        except TypeError:  # handle non-numeric columns
            df_ret[dcol] = grouped.agg({dcol:min})
    return df_ret

La fonction df_wavg() renvoie une trame de données qui est regroupée par la colonne "groupby" et qui renvoie la somme des poids pour la colonne des poids. Les autres colonnes sont soit les moyennes pondérées ou, si elles ne sont pas numériques, la fonction min() est utilisée pour l'agrégation.

4
dslack

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

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éfinissez une fonction personnalisée qui sera transmise à apply. Il accepte implicitement un DataFrame - ce qui signifie que le paramètre data est un DataFrame. Remarquez comment il utilise plusieurs colonnes, ce qui n'est pas possible avec la méthode groupby agg:

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 la méthode 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 DataFrame comme expliqué dans d'autres réponses et en évitant d'utiliser apply tout à fait.

4
Ted Petrou

Je fais beaucoup cela et j'ai trouvé les éléments suivants très pratiques:

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 calculera la moyenne pondérée de toutes les colonnes numériques dans les df et supprimera celles non numériques.

3
santon

Accomplir ceci via groupby(...).apply(...) n'est pas performant. Voici une solution que j'utilise tout le temps (essentiellement en utilisant 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
3
Nathaniel