Funkcja agregująca Pandas DataFrame korzystająca z wielu kolumn

80

Czy istnieje sposób na napisanie funkcji agregującej, która jest używana w DataFrame.aggmetodzie, która miałaby dostęp do więcej niż jednej kolumny danych, które są agregowane? Typowymi przypadkami użycia byłyby średnie ważone funkcje odchylenia standardowego.

Chciałbym móc napisać coś takiego

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 ...
user1444817
źródło
Ładny artykuł dotyczący tego konkretnego pytania SO: pbpython.com/weighted-average.html
ptim,

Odpowiedzi:

104

Tak; użyj .apply(...)funkcji, która zostanie wywołana na każdym pod- DataFrame. Na przykład:

grouped = df.groupby(keys)

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

grouped.apply(wavg)
Wes McKinney
źródło
Bardziej efektywne może być rozbicie tego na kilka operacji w następujący sposób: (1) utwórz kolumnę wag, (2) znormalizuj obserwacje według ich wag, (3) oblicz zgrupowaną sumę ważonych obserwacji i zgrupowaną sumę wag , (4) znormalizować ważoną sumę obserwacji przez sumę wag.
kalu
4
A co jeśli chcemy obliczyć wavg wielu zmiennych (kolumn), np. Wszystkiego poza df ['wagi']?
CPBL
2
@Wes, czy jest jakiś sposób, aby kiedyś to zrobić za pomocą agg()i lambdawbudowanego np.average(...weights=...)lub jakiegokolwiek nowego natywnego wsparcia w pandach dla średnich ważonych od czasu pierwszego pojawienia się tego postu?
sparc_spread
4
@Wes McKinney: W swojej książce sugerują, że to podejście: get_wavg = lambda g: np.average(g['data'], weights = g['weights']); grouped.apply(wavg) Czy te dwa są wymienne?
robroc
9

Moje rozwiązanie jest podobne do rozwiązania Nathaniela, tylko że dotyczy pojedynczej kolumny i nie kopiuję głęboko całej ramki danych za każdym razem, co może być zbyt wolne. Wzrost wydajności w stosunku do grupy rozwiązań według (...). Zastosuj (...) wynosi około 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
ErnestScribbler
źródło
Byłoby bardziej czytelne, gdybyś konsekwentnie używał PEP8 i usuwał zbędną dellinię.
MERose
Dzięki! delLinia nie jest właściwie zbędny, bo zmienić DataFrame wejściowego na miejscu w celu zwiększenia wydajności, więc muszę posprzątać.
ErnestScribbler
Ale zwracasz wynik w następnej linii, która kończy funkcję. Po zakończeniu funkcji wszystkie wewnętrzne obiekty i tak są usuwane.
MERose
1
Ale zauważ, że df nie jest obiektem wewnętrznym. Jest argumentem funkcji i tak długo, jak nigdy jej nie przypisujesz ( df = something), pozostaje płytką kopią i jest zmieniany w miejscu. W takim przypadku kolumny zostaną dodane do DataFrame. Spróbuj skopiować i wkleić tę funkcję i uruchomić ją bez dellinii i zobacz, że zmienia ona daną ramkę DataFrame, dodając kolumny.
ErnestScribbler
To nie daje odpowiedzi na pytanie, ponieważ średnia ważona służy jedynie jako przykład dla dowolnego agregatu w wielu kolumnach.
user__42
8

Możliwe jest zwrócenie dowolnej liczby zagregowanych wartości z obiektu grupowania według apply. Po prostu zwróć serię, a wartości indeksu staną się nowymi nazwami kolumn.

Zobaczmy szybki przykład:

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

Zdefiniuj funkcję niestandardową, do której zostanie przekazana apply. Niejawnie akceptuje DataFrame - co oznacza, że dataparametr jest DataFrame. Zwróć uwagę, jak używa wielu kolumn, co nie jest możliwe w aggprzypadku metody grupowania:

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)

Wywołaj applymetodę grupowania za pomocą naszej funkcji niestandardowej:

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

       d1_wa  d2_wa
group              
a        9.0    2.2
b       58.0   13.2

Możesz uzyskać lepszą wydajność, wstępnie obliczając sumy ważone w nowych kolumnach DataFrame, jak wyjaśniono w innych odpowiedziach, i applycałkowicie unikać używania .

Ted Petrou
źródło
4

Poniższe (oparte na odpowiedzi Wesa McKinneya) spełnia dokładnie to, czego szukałem. Z przyjemnością dowiem się, czy istnieje prostszy sposób na zrobienie tego w ramach 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

Funkcja df_wavg()zwraca ramkę danych, która jest pogrupowana według kolumny „groupby” i zwraca sumę wag dla kolumny wag. Inne kolumny są albo średnimi ważonymi, albo, jeśli nie są liczbami, min()funkcja jest używana do agregacji.

dslack
źródło
4

Robię to często i uważam, że bardzo przydatne:

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)

Spowoduje to obliczenie średniej ważonej wszystkich kolumn liczbowych w dfi pominie kolumny nienumeryczne.

Santon
źródło
To jest błyskawiczne! Dobra robota!
Shay Ben-Sasson
To naprawdę fajne, jeśli masz wiele kolumn. Miły!
Chris,
@santon, dzięki za odpowiedź. Czy mógłbyś podać przykład swojego rozwiązania? Podczas próby użycia rozwiązania wystąpił błąd „KeyError:„ COUNT ”.
Allen
@Allen Należy użyć dowolnej nazwy kolumny zawierającej liczby, których chcesz użyć jako średniej ważonej.
santon
4

Osiągnięcie tego za pośrednictwem groupby(...).apply(...)jest niewykonalne. Oto rozwiązanie, którego używam cały czas (zasadniczo używając logiki 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
Nathaniel
źródło
1
Kiedy mówisz, że nie działa. Jaka jest różnica? Zmierzyłem to?
Bouncner