2012-06-08 70 views
49

C'è un modo per scrivere una funzione di aggregazione come viene utilizzata nel metodo DataFrame.agg, che avrebbe accesso a più di una colonna dei dati che vengono aggregati? I casi d'uso tipici sarebbero la media ponderata, le funzioni di deviazione standard ponderate.Pandas DataFrame funzione di aggregazione utilizzando più colonne

mi piacerebbe essere in grado di scrivere qualcosa di simile

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

risposta

68

Sì; utilizzare la funzione .apply(...), che verrà chiamata su ogni sub-DataFrame. Ad esempio:

grouped = df.groupby(keys) 

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

grouped.apply(wavg) 
+0

Potrebbe essere più efficiente suddividerlo in poche operazioni come segue: (1) creare una colonna di pesi, (2) normalizzare le osservazioni con i loro pesi, (3) calcolare la somma raggruppata di osservazioni ponderate e raggruppate somma dei pesi, (4) normalizza la somma ponderata delle osservazioni per la somma dei pesi. – kalu

+3

E se vogliamo calcolare le variabili di molte variabili (colonne), ad es. tutto tranne che per df ['pesi']? – CPBL

+2

@Wes, c'è un modo per farlo una volta con 'agg()' e un 'lambda' costruito attorno a' np.average (... weights = ...) ', o qualsiasi nuovo supporto nativo in panda per ponderato significa da quando questo post è apparso per la prima volta? –

3

Quanto segue (basato sulla risposta di Wes McKinney) realizza esattamente ciò che stavo cercando. Sarei felice di sapere se esiste un modo più semplice per farlo entro 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 funzione df_wavg() restituisce un dataframe che è raggruppato dalla colonna "GroupBy", e che restituisce la somma dei pesi per la colonna pesi. Altre colonne sono le medie ponderate o, se non numeriche, la funzione min() viene utilizzata per l'aggregazione.

3

Lo faccio molto e ho trovato il seguente abbastanza comoda:

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) 

consente di calcolare la media ponderata di tutte le colonne numeriche nel df e rilasciare quelli non numerici.

+0

Questo è velocissimo! Ottimo lavoro! –

+0

Questo è davvero dolce se si hanno più colonne. Bello! – Chris

+0

@santon, grazie per la risposta. Potresti dare un esempio della tua soluzione? Ho ricevuto un errore "KeyError:" COUNT "durante il tentativo di utilizzare la soluzione. – Allen

1

L'utilizzo di questo numero via groupby(...).apply(...) non è performante. Ecco una soluzione che uso sempre (essenzialmente usando la logica di 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

Quando dici "non performante". Quanto costa la differenza? L'hanno misurato? – Bouncner

1

mia soluzione è simile alla soluzione di Nathaniel, solo che è per una singola colonna E non profonde copiare l'intero frame di dati ogni volta, che potrebbe essere proibitivo lento. Il guadagno di prestazioni sopra la groupby soluzione di (...). Applicare (...) è di circa 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 
0

E 'possibile restituire qualsiasi numero di valori aggregati da un oggetto groupby con apply. Semplicemente, restituisci una serie ei valori dell'indice diventeranno i nuovi nomi di colonna.

Vediamo un esempio veloce:

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 

Definire una funzione personalizzata che verrà passato a apply. Accetta implicitamente un DataFrame, il che significa che il parametro data è un DataFrame. Notate come si usa più colonne, che non è possibile con il metodo 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) 

chiamate il metodo GroupBy apply con la nostra funzione personalizzata:

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

     d1_wa d2_wa 
group    
a  9.0 2.2 
b  58.0 13.2 

È possibile ottenere prestazioni migliori dal ricalcolo dei ponderata totalizza in nuove colonne DataFrame come spiegato in altre risposte ed evita di utilizzare completamente apply.

Problemi correlati