¿Hay alguna forma de escribir una función de agregación como se usa en el DataFrame.agg
método, 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 funciones de desviación estándar ponderada, promedio ponderado.
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 ...
Respuestas:
Si; use la
.apply(...)
función, que será llamada 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)
fuente
agg()
ylambda
construido alrededornp.average(...weights=...)
, o cualquier nuevo soporte nativo en pandas para medios ponderados desde que apareció esta publicación?get_wavg = lambda g: np.average(g['data'], weights = g['weights'])
;grouped.apply(wavg)
¿Son los dos intercambiables?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 marco de datos 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
fuente
del
línea superflua .del
línea en realidad no es superflua, ya que cambio el DataFrame de entrada en el lugar para mejorar el rendimiento, así que tengo que limpiar.df = something
), permanece como una copia superficial y se cambia en el lugar. En este caso, las columnas se agregarían al DataFrame. Intente copiar y pegar esta función y ejecutarla sin ladel
línea, y vea que cambia el DataFrame dado agregando columnas.Es posible devolver cualquier número de valores agregados de un objeto groupby con
apply
. Simplemente, devuelva una Serie y los valores del í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 a la que se le pasará
apply
. Acepta implícitamente un DataFrame, lo que significa que eldata
parámetro es un DataFrame. Observe cómo usa múltiples columnas, lo cual no es posible con elagg
método 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)
Llame al
apply
método groupby 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 calculando previamente los totales ponderados en nuevas columnas DataFrame como se explica en otras respuestas y evitar el uso por
apply
completo.fuente
Lo siguiente (basado en la respuesta de Wes McKinney) logra exactamente lo que estaba buscando. Me encantaría saber si hay una forma más sencilla de hacer esto dentro
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 marco de datos que está agrupado por la columna "groupby" y que devuelve la suma de los pesos de la columna de pesos. Otras columnas son los promedios ponderados o, si no son numéricos, lamin()
función se utiliza para la agregación.fuente
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 el
df
y eliminará las no numéricas.fuente
Lograr esta vía
groupby(...).apply(...)
no es efectivo. 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 (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
fuente