Агрегатная функция 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 ...
user1444817
источник
Хорошая статья, посвященная этому конкретному вопросу 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) нормировать взвешенную сумму наблюдений на сумму весов.
калу
4
Что, если мы хотим вычислить wavg многих переменных (столбцов), например, всего, кроме df ['weights']?
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

Мое решение похоже на решение Натаниэля, только для одного столбца, и я не копирую каждый раз глубоко весь фрейм данных, что может быть чрезмерно медленным. Прирост производительности по сравнению с группой решений by (...). Apply (...) составляет примерно 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
источник
Было бы более читабельно, если бы вы использовали PEP8 последовательно и удалили бы лишнюю delстроку.
MERose
Благодаря! delЛиния на самом деле не лишняя, так как я изменить входной DataFrame на месте для повышения производительности, так что я должен убирать.
ErnestScribbler
Но вы возвращаете результат в следующей строке, которая завершает функцию. По завершении функции все внутренние объекты все равно удаляются.
MERose
1
Но обратите внимание, что df не является внутренним объектом. Это аргумент функции, и до тех пор, пока вы не присваиваете ему ( df = something), он остается неглубокой копией и изменяется на месте. В этом случае столбцы будут добавлены в DataFrame. Попробуйте скопировать эту функцию и запустить ее без delстроки, и убедитесь, что она изменяет данный DataFrame, добавляя столбцы.
ErnestScribbler
Это не отвечает на вопрос, потому что средневзвешенное значение просто служит примером для любого агрегирования по нескольким столбцам.
user__42
8

Можно вернуть любое количество агрегированных значений из объекта groupby с помощью apply. Просто верните Series, и значения индекса станут именами новых столбцов.

Давайте посмотрим на быстрый пример:

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()функция используется для агрегирования.

dslack
источник
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 Вы должны использовать любое имя столбца, в котором есть счетчики, которые вы хотите использовать для средневзвешенного значения.
santon
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