2012-06-08 64 views
49

¿Hay alguna 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 agregan? Los casos de uso típicos serían los funcionamientos de desviación estándar ponderados y promedio ponderado.Función agregada de Pandas DataFrame utilizando varias columnas

Me gustaría ser capaz de 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 ... 

Respuesta

68

Sí; use la función .apply(...), que se invocará 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) 
+0

Puede ser más eficiente dividir esto en unas pocas operaciones de la siguiente manera: (1) crear una columna de ponderaciones, (2) normalizar las observaciones por sus ponderaciones, (3) calcular la suma agrupada de observaciones ponderadas y una agrupada suma de ponderaciones, (4) normaliza la suma ponderada de observaciones por la suma de ponderaciones. – kalu

+3

¿Qué sucede si queremos calcular los wavg de muchas variables (columnas), p. todo excepto df ['pesos']? – CPBL

+2

@Wes, ¿hay alguna manera en que una vez pueda hacer esto con 'agg()' y una 'lambda' construida alrededor de' np.average (... weights = ...) ', o cualquier soporte nativo nuevo en pandas para ponderar significa que esta publicación apareció por primera vez? –

3

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 hacerlo dentro del 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 una trama de datos que se agrupa 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éricas, la función min() se usa para la agregación.

3

hago esto mucho y encontré la siguiente bastante práctico:

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) 

Esta calculará la media ponderada de todas las columnas numéricas en el df y soltar los no numéricos.

+0

¡Esto es increíblemente rápido! ¡Gran trabajo! –

+0

Esto es realmente bueno si tienes varias columnas. ¡Bonito! – Chris

+0

@santon, gracias por la respuesta. ¿Podría dar un ejemplo de su solución? Recibí un error 'KeyError': 'COUNT' mientras trataba de usar su solución. – Allen

1

Logrando esto a través de groupby(...).apply(...) no está funcionando. 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 
+0

Cuando dice no-rendimiento. ¿Cuánto es la diferencia? ¿Lo has medido? – Bouncner

1

Mi solución es similar a la solución de Nathaniel, sólo que es para una sola columna y no profunda copiar toda la trama de datos cada vez, lo que podría ser prohibitivamente lento. La ganancia de rendimiento sobre el GroupBy solución (...). Aplicar (...) es de aproximadamente 100 veces (!)

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 
0

Es posible devolver cualquier número de los 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 

definir una función personalizada que se pasará a apply. Admite implícitamente un DataFrame, lo que significa que el parámetro data es un DataFrame. Note cómo se utiliza varias columnas, lo cual no es posible con el método agg 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 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 

Usted puede obtener un mejor rendimiento al calcular previamente el ponderada suma en nuevas columnas de DataFrame como se explica en otras respuestas y evite usar apply en total.

Cuestiones relacionadas