Función agregada de Pandas DataFrame usando múltiples columnas

¿Hay una manera de escribir una función de agregación como se usa en el método DataFrame.agg , que tendría acceso a más de una columna de los datos que se están agregando? Los casos de uso típicos serían el promedio ponderado, las funciones de desviación estándar ponderada.

Me gustaría poder escribir algo como

 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 ... 

Sí; use la función .apply(...) , que se DataFrame en cada sub- DataFrame . Por ejemplo:

 grouped = df.groupby(keys) def wavg(group): d = group['data'] w = group['weights'] return (d * w).sum() / w.sum() grouped.apply(wavg) 

Lo siguiente (basado en la respuesta de Wes McKinney) cumple exactamente lo que estaba buscando. Me encantaría saber si hay una forma más sencilla de hacerlo dentro de los 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 función df_wavg() devuelve un dataframe que está agrupado por la columna “groupby”, y que devuelve la sum de los pesos para la columna de pesos. Otras columnas son los promedios ponderados o, si no son numéricos, la función min() se usa para la agregación.

Hago esto mucho y encontré lo siguiente bastante útil:

 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) 

Esto calculará el promedio ponderado de todas las columnas numéricas en las columnas df y dropeará las no numéricas.

Es posible devolver cualquier número de valores agregados de un objeto groupby con apply . Simplemente, devuelva una Serie y los valores de índice se convertirán en los nuevos nombres de columna.

Veamos un ejemplo rápido:

 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 

Defina una función personalizada que se pasará a apply . Acepta implícitamente un DataFrame, lo que significa que el parámetro data es un DataFrame. Observe cómo utiliza varias columnas, lo que no es posible con el método AG por grupo:

 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) 

Llame al método groupby Apply con nuestra función personalizada:

 df.groupby('group').apply(weighted_average) d1_wa d2_wa group a 9.0 2.2 b 58.0 13.2 

Puede obtener un mejor rendimiento al calcular previamente los totales ponderados en nuevas columnas de DataFrame como se explica en otras respuestas y evitar el uso de la apply completo.

Mi solución es similar a la solución de Nathaniel, solo que es para una sola columna y no copio en profundidad todo el dataframe cada vez, lo que podría ser prohibitivamente lento. La ganancia de rendimiento sobre la solución groupby (…). Apply (…) es aproximadamente 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 

Lograr esto a través de groupby(...).apply(...) no se realiza. Aquí hay una solución que uso todo el tiempo (esencialmente usando la lógica 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 (eg 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