Czy istnieje sposób na napisanie funkcji agregującej, która jest używana w DataFrame.agg
metodzie, 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 ...
Odpowiedzi:
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)
źródło
agg()
ilambda
wbudowanegonp.average(...weights=...)
lub jakiegokolwiek nowego natywnego wsparcia w pandach dla średnich ważonych od czasu pierwszego pojawienia się tego postu?get_wavg = lambda g: np.average(g['data'], weights = g['weights'])
;grouped.apply(wavg)
Czy te dwa są wymienne?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
źródło
del
linię.del
Linia 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ć.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ą bezdel
linii i zobacz, że zmienia ona daną ramkę DataFrame, dodając kolumny.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, żedata
parametr jest DataFrame. Zwróć uwagę, jak używa wielu kolumn, co nie jest możliwe wagg
przypadku 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
apply
metodę 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
apply
całkowicie unikać używania .źródło
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.źródło
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
df
i pominie kolumny nienumeryczne.źródło
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
źródło