Сукупна функція Pandas DataFrame із використанням декількох стовпців


80

Чи є спосіб написати функцію агрегування, як використовується в DataFrame.aggметоді, яка мала б доступ до більш ніж одного стовпця даних, що агрегуються? Типовими випадками використання є середньозважена, зважена функція стандартного відхилення.

Я хотів би мати можливість написати щось подібне

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

Гарна стаття, присвячена цьому конкретному запитанню SO: pbpython.com/weighted-average.html
ptim

Відповіді:


104

Так; використовуйте .apply(...)функцію, яка буде викликана на кожному під- DataFrame. Наприклад:

grouped = df.groupby(keys)

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

grouped.apply(wavg)

Можливо, було б ефективніше розбити це на кілька операцій наступним чином: (1) створити стовпець ваг, (2) нормалізувати спостереження за їх вагами, (3) обчислити згруповану суму зважених спостережень та згруповану суму ваг , (4) нормалізують зважену суму спостережень на суму ваг.
kalu

4
Що робити, якщо ми хочемо обчислити wavg з багатьох змінних (стовпців), наприклад, всього, крім df ['ваги']?
CPBL

2
@Wes, чи є спосіб, який колись міг зробити це за допомогою agg()і lambdaпобудований навколо np.average(...weights=...), або якась нова рідна підтримка в пандах для зважених засобів з моменту появи цієї публікації?
sparc_spread

4
@Wes МакКінні: У своїй книзі ви пропонуєте цей підхід: get_wavg = lambda g: np.average(g['data'], weights = g['weights']); grouped.apply(wavg) Ці два взаємозамінні?
robroc

9

Моє рішення схоже на рішення Натаніеля, лише для одного стовпця, і я не виконую глибоку копію кожного кадру даних щоразу, що може бути надмірно повільним. Приріст продуктивності для групи рішень за (...). Apply (...) становить близько 100 разів (!)

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

Було б зручніше читати, якби ви послідовно використовували PEP8 і видаляли зайву delлінію.
MERose

Дякую! delЛінія насправді не зайва, так як я змінити вхідний DataFrame на місці для підвищення продуктивності, так що я повинен прибирати.
ErnestScribbler

Але ви повертаєте результат у наступному рядку, який закінчує функцію. Після завершення функції всі внутрішні об’єкти все одно очищаються.
MERose

1
Але зверніть увагу, що df не є внутрішнім об'єктом. Це аргумент функції, і поки ви ніколи не присвоюєте їй ( df = something), вона залишається неглибокою копією і змінюється на місці. У цьому випадку стовпці будуть додані до DataFrame. Спробуйте скопіювати цю функцію та запустити її без delрядка, і переконайтеся, що вона змінює заданий DataFrame, додаючи стовпці.
ErnestScribbler

Це не дає відповіді на запитання, оскільки середньозважене середнє просто служить прикладом для будь-якого сукупного матеріалу з кількох стовпців.
user__42

8

Можна повернути будь-яку кількість агрегованих значень з об'єкта groupby за допомогою apply. Просто поверніть Серію, і значення індексу стануть новими іменами стовпців.

Подивимось короткий приклад:

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

Визначте власну функцію, якій буде передано apply. Він неявно приймає DataFrame - тобто dataпараметр є DataFrame. Зверніть увагу, як він використовує кілька стовпців, що неможливо за 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)

Викличте applyметод groupby за допомогою нашої користувацької функції:

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

       d1_wa  d2_wa
group              
a        9.0    2.2
b       58.0   13.2

Ви можете отримати кращу продуктивність, попередньо підрахувавши зважені підсумки в нові стовпці DataFrame, як пояснено в інших відповідях, і уникнути використання applyвзагалі.


4

Наступне (на основі відповіді Уеса МакКінні) виконує саме те, що я шукав. Я був би радий дізнатися, чи існує простіший спосіб зробити це всередині 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

Функція df_wavg()повертає фрейм даних, згрупований за стовпцем "groupby", який повертає суму ваг для стовпця ваг. Інші стовпці - це або зважені середні значення, або, якщо вони не числові, min()функція використовується для агрегування.


4

Я багато роблю це і знайшов наступне досить зручним:

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)

Це обчислить середньозважене середнє для всіх числових стовпців у dfі відкине нечислові стовпці .


Це швидко палає! Чудова робота!
Шей Бен-Сассон,

Це дуже солодко, якщо у вас є кілька стовпців. Приємно!
Кріс

@santon, дякую за відповідь. Не могли б ви навести приклад свого рішення? Під час спроби використати ваше рішення з’явилася помилка „KeyError:„ COUNT ”.
Аллен

@Allen Ви повинні використовувати будь-яку назву стовпця, що містить кількість, яку ви хочете використовувати для середньозваженого.
Сантон

4

Досягнення цього шляхом groupby(...).apply(...)є неефективним. Ось рішення, яке я постійно використовую (по суті, використовуючи логіку Калу).

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

1
Коли ви говорите про неефективність. Скільки різниця? Виміряли?
Bouncner
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.