Pandas: Устарение перемаркированных dicts в groupby.agg вызывает множество проблем

Созданный на 19 нояб. 2017  ·  37Комментарии  ·  Источник: pandas-dev/pandas

Эта проблема создана на основе обсуждения в # 15931 после прекращения поддержки перемаркированных слов в groupby.agg . Многое из того, что резюмируется ниже, уже обсуждалось в предыдущем обсуждении. Я бы порекомендовал, в частности, https://github.com/pandas-dev/pandas/pull/15931#issuecomment -336139085, где также четко указаны проблемы.

Мотивация, стоящая за устареванием # 15931, была в основном связана с обеспечением согласованного интерфейса для agg() между Series и Dataframe (см. Также # 14668 для контекста).

Функциональность перемаркировки с помощью вложенного dict была описана некоторыми как слишком сложная и / или непоследовательная и поэтому устарела.

Однако за это приходится платить: невозможность агрегировать и переименовывать одновременно приводит к очень неприятным проблемам и некоторой обратной несовместимости, когда нет разумного обходного пути:

  • _ [раздражает] _ больше нет контроля над именами результирующих столбцов
  • _ [раздражает] _ вам нужно найти способ переименовать MultiIndex _ после_ выполнения агрегации, требуя отслеживать порядок столбцов в двух местах кода .... не практично вообще, а иногда совершенно невозможно (примеры ниже ).
  • ⚠️ _ [break] _ не может применять более одного вызываемого объекта с одним и тем же внутренним именем к одному и тому же столбцу ввода. Это приводит к двум частям случая:

    • _ [break] _ вы больше не можете применять два или более лямбда-агрегатора в одном столбце

    • _ [нарушение] _ вы больше не можете применять два или более агрегатора из частичных функций, если не измените их скрытый атрибут __name__

Пример

_ (обратите внимание, это созданный пример с целью демонстрации проблемы в как можно более коротком коде, но все продемонстрированные здесь проблемы укусили меня в реальной жизни после изменения, а также в ситуациях, не таких простых, как здесь ) _

Фреймворк входных данных

mydf = pd.DataFrame(
    {
        'cat': ['A', 'A', 'A', 'B', 'B', 'C'],
        'energy': [1.8, 1.95, 2.04, 1.25, 1.6, 1.01],
        'distance': [1.2, 1.5, 1.74, 0.82, 1.01, 0.6]
    },
    index=range(6)
)
  cat  distance  energy
0   A      1.20    1.80
1   A      1.50    1.95
2   A      1.74    2.04
3   B      0.82    1.25
4   B      1.01    1.60
5   C      0.60    1.01

До:

легко писать и читать, работает должным образом

import numpy as np
import statsmodels.robust as smrb
from functools import partial

# median absolute deviation as a partial function
# in order to demonstrate the issue with partial functions as aggregators
mad_c1 = partial(smrb.mad, c=1)

# renaming and specifying the aggregators at the same time
# note that I want to choose the resulting column names myself
# for example "total_xxxx" instead of just "sum"
mydf_agg = mydf.groupby('cat').agg({
    'energy': {
        'total_energy': 'sum',
        'energy_p98': lambda x: np.percentile(x, 98),  # lambda
        'energy_p17': lambda x: np.percentile(x, 17),  # lambda
    },
    'distance': {
        'total_distance': 'sum',
        'average_distance': 'mean',
        'distance_mad': smrb.mad,   # original function
        'distance_mad_c1': mad_c1,  # partial function wrapping the original function
    },
})

приводит к

          energy                             distance
    total_energy energy_p98 energy_p17 total_distance average_distance distance_mad distance_mad_c1
cat
A           5.79     2.0364     1.8510           4.44            1.480     0.355825           0.240
B           2.85     1.5930     1.3095           1.83            0.915     0.140847           0.095
C           1.01     1.0100     1.0100           0.60            0.600     0.000000           0.000

и все осталось:

# get rid of the first MultiIndex level in a pretty straightforward way
mydf_agg.columns = mydf_agg.columns.droplevel(level=0)

Счастливого танца, восхваляющего панд 💃 🕺!

После

import numpy as np
import statsmodels.robust as smrb
from functools import partial

# median absolute deviation as a partial function
# in order to demonstrate the issue with partial functions as aggregators
mad_c1 = partial(smrb.mad, c=1)

# no way of choosing the destination's column names...
mydf_agg = mydf.groupby('cat').agg({
    'energy': [
        'sum',
        lambda x: np.percentile(x, 98), # lambda
        lambda x: np.percentile(x, 17), # lambda
    ],
    'distance': [
        'sum',
        'mean',
        smrb.mad, # original function
        mad_c1,   # partial function wrapping the original function
    ],
})

Вышеуказанное прерывается, потому что все лямбда-функции приводят к столбцам с именем <lambda> что приводит к

SpecificationError: Function names must be unique, found multiple named <lambda>

Обратно несовместимая регрессия: больше нельзя применять две разные лямбды к одному и тому же исходному столбцу.

Если удалить lambda x: np.percentile(x, 98) сверху, мы получим ту же проблему с частичной функцией, которая наследует имя функции от исходной функции:

SpecificationError: Function names must be unique, found multiple named mad

Наконец, после перезаписи атрибута __name__ партиала (например, с помощью mad_c1.__name__ = 'mad_c1' ) мы получаем:

    energy          distance
       sum <lambda>      sum   mean       mad mad_c1
cat
A     5.79   1.8510     4.44  1.480  0.355825  0.240
B     2.85   1.3095     1.83  0.915  0.140847  0.095
C     1.01   1.0100     0.60  0.600  0.000000  0.000

с еще

  • один столбец отсутствует (98-й процентиль)
  • обработка столбцов MultiIndex
  • и переименование столбцов

разбираться на отдельном этапе.

После агрегации невозможно контролировать имена столбцов, лучшее, что мы можем получить автоматизированным способом, - это некоторая комбинация исходного имени столбца и имени функции _aggregate_, например:

mydf_agg.columns = ['_'.join(col) for col in mydf_agg.columns]

что приводит к:

     energy_sum  energy_<lambda>  distance_sum  distance_mean  distance_mad distance_mad_c1
cat
A          5.79           1.8510          4.44          1.480      0.355825           0.240
B          2.85           1.3095          1.83          0.915      0.140847           0.095
C          1.01           1.0100          0.60          0.600      0.000000           0.000

и если вам действительно нужно иметь разные имена, вы можете сделать это так:

mydf_agg.rename({
    "energy_sum": "total_energy",
    "energy_<lambda>": "energy_p17",
    "distance_sum": "total_distance",
    "distance_mean": "average_distance"
    }, inplace=True)

но это означает, что вам нужно быть осторожным, чтобы код переименования (который теперь должен располагаться в другом месте кода) синхронизировался с кодом, в котором определена агрегация ...

Пользователь Sad pandas 😢 (который, конечно, все еще любит панд)


Я стремлюсь к согласованности и в то же время глубоко сожалею об устаревании функций агрегирования и переименования. Я надеюсь, что приведенные выше примеры прояснят болевые точки.


Возможные решения

  • Отмените устаревание функции перемаркировки dict-of-dict
  • Предоставьте другой API, чтобы иметь возможность это сделать (но почему должны быть два метода для одной и той же основной цели, а именно для агрегации?)
  • ??? (открыт для предложений)

_Дополнительное чтение: _

Что касается вышеупомянутого обсуждения в запросе на перенос, которое продолжается уже несколько месяцев, я только недавно осознал одну из причин, по которой меня так беспокоит это устаревание: «объединить и переименовать» - это естественная вещь, связанная с Агрегирование GROUP BY в SQL, поскольку в SQL вы обычно указываете имя столбца назначения непосредственно рядом с выражением агрегации, например SELECT col1, avg(col2) AS col2_mean, stddev(col2) AS col2_var FROM mytable GROUP BY col1 .

Я _не_ говорю, что Pandas, конечно, обязательно должны обеспечивать те же функции, что и SQL. Но приведенные выше примеры демонстрируют, почему API dict-of-dict, на мой взгляд, был чистым и простым решением для многих случаев использования.

(* Я лично не согласен с тем, что подход диктата сложен.)

API Design Groupby

Самый полезный комментарий

Как бы то ни было, я также категорически за то, чтобы не обесценивать функциональность.

Для меня большая причина заключается в том, что есть что-то глубоко странное в смешивании пространства имен функций Python (что-то связанное с конкретной реализацией) с данными в именах столбцов (то, что наверняка не должно знать о реализации). Тот факт, что мы видим столбцы (потенциально несколько столбцов) с именем '<lambda>' , вызывает у меня серьезный когнитивный диссонанс.

Подход переименования раздражает, потому что есть этот промежуточный шаг, на котором переносятся ненужные (и открытые) имена столбцов. Кроме того, их трудно надежно и систематически переименовывать, поскольку есть потенциальные зависимости от реализации.

Помимо этого, функциональность вложенного dict, по общему признанию, сложна, но это сложная операция, которая выполняется.

TL; DR Пожалуйста, не обесценивайте. :)

Все 37 Комментарий

@zertrin : Спасибо, что

@jreback @jorisvandenbossche @TomAugspurger @ chris-b1

Я согласен с тем, что переименование с текущей реализацией agg очень неуклюже и не работает в этом примере. Вложенные dicts несколько сложны, но написание их в том виде, в котором вы это сделали, очень ясно дает понять, что происходит.

Я предполагаю , что может быть names параметр добавлен в agg , который бы СЛОВАРЬ КАРТОГРАФИРОВАНИЯ на общую столбцы с их новыми именами. Вы даже можете добавить еще один параметр drop_index в качестве логического, чтобы определить, сохранять ли верхний уровень индекса.

Таким образом, синтаксис превратился бы в:

agg_dict = {'energy': ['sum',
                       lambda x: np.percentile(x, 98), # lambda
                       lambda x: np.percentile(x, 17), # lambda
                      ],
            'distance': ['sum',
                         'mean',
                         smrb.mad, # original function
                         mad_c1,   # partial function wrapping the original function
                        ]
           }

name_dict = {'energy':['energy_sum', 'energy_p98', 'energy_p17'],
             'distance':['distance_sum', 'distance_mean', 'distance_mad', 'distance_mad_c1']}


mydf.groupby('cat').agg(agg_dict, names=name_dict, drop_index=True)

Или, может быть, можно создать совершенно новый метод agg_assign , который будет работать аналогично DataFrame.assign :

mydf.groupby('cat').agg_assign(energy_sum=lambda x: x.energy.sum(),
                               energy_p98=lambda x: np.percentile(x.energy, 98),
                               energy_p17=lambda x: np.percentile(x.energy, 17),
                               distance_sum=lambda x: x.distance.sum(),
                               distance_mean=lambda x: x.distance.mean(),
                               distance_mad=lambda x: smrb.mad(x.distance),
                               distance_mad_c1=lambda x: mad_c1(x.distance))

На самом деле мне этот вариант больше нравится.

Как бы то ни было, я также категорически за то, чтобы не обесценивать функциональность.

Для меня большая причина заключается в том, что есть что-то глубоко странное в смешивании пространства имен функций Python (что-то связанное с конкретной реализацией) с данными в именах столбцов (то, что наверняка не должно знать о реализации). Тот факт, что мы видим столбцы (потенциально несколько столбцов) с именем '<lambda>' , вызывает у меня серьезный когнитивный диссонанс.

Подход переименования раздражает, потому что есть этот промежуточный шаг, на котором переносятся ненужные (и открытые) имена столбцов. Кроме того, их трудно надежно и систематически переименовывать, поскольку есть потенциальные зависимости от реализации.

Помимо этого, функциональность вложенного dict, по общему признанию, сложна, но это сложная операция, которая выполняется.

TL; DR Пожалуйста, не обесценивайте. :)

Мой вклад мотивирован двумя вещами.

  1. Я знаю и согласен с мотивацией уменьшить раздутый API Pandas. Даже если я заблуждаюсь в отношении предполагаемой мотивации уменьшить «раздутые» элементы API, я все же считаю, что API Pandas можно упростить.
  2. Я думаю, что лучше иметь хорошую кулинарную книгу с хорошими рецептами, чем предоставлять API, удовлетворяющий все желания и желания. Я не утверждаю, что переименование через вложенные словари квалифицируется как удовлетворение прихоти, поскольку оно уже существовало, и мы обсуждаем его устаревание. Но это действительно находится в диапазоне между упрощенным API и чем-то ... еще.

Кроме того, у объектов Pandas Series и DataFrame есть методы pipe для облегчения конвейерной обработки. В этом сегменте документации обсуждается, что мы могли бы использовать pipe для прокси для методов вместо создания подклассов. В том же духе мы могли бы использовать новый GroupBy.pipe для выполнения аналогичной роли и позволить нам создавать прокси-методы для объектов groupby.

Я воспользуюсь примером @zertrin

import numpy as np
import statsmodels.robust as smrb
from functools import partial

# The DataFrame offered up above
mydf = pd.DataFrame(
    {
        'cat': ['A', 'A', 'A', 'B', 'B', 'C'],
        'energy': [1.8, 1.95, 2.04, 1.25, 1.6, 1.01],
        'distance': [1.2, 1.5, 1.74, 0.82, 1.01, 0.6]
    },
    index=range(6)
)

# Identical dictionary passed to `agg`
funcs = {
    'energy': {
        'total_energy': 'sum',
        'energy_p98': lambda x: np.percentile(x, 98),  # lambda
        'energy_p17': lambda x: np.percentile(x, 17),  # lambda
    },
    'distance': {
        'total_distance': 'sum',
        'average_distance': 'mean',
        'distance_mad': smrb.mad,   # original function
        'distance_mad_c1': mad_c1,  # partial function wrapping the original function
    },
}

# Write a proxy method to be passed to `pipe`
def agg_assign(gb, fdict):
    data = {
        (cl, nm): gb[cl].agg(fn)
        for cl, d in fdict.items()
        for nm, fn in d.items()
    }
    return pd.DataFrame(data)

# All the API we need already exists with `pipe`
mydf.groupby('cat').pipe(agg_assign, fdict=funcs)

Что приводит к

            distance                                                 energy                        
    average_distance distance_mad distance_mad_c1 total_distance energy_p17 energy_p98 total_energy
cat                                                                                                
A              1.480     0.355825           0.240           4.44     1.8510     2.0364         5.79
B              0.915     0.140847           0.095           1.83     1.3095     1.5930         2.85
C              0.600     0.000000           0.000           0.60     1.0100     1.0100         1.01

Метод pipe во многих случаях делает ненужным добавление нового API. Он также предоставляет средства для замены обсуждаемых устаревших функций. Поэтому я был бы склонен отказаться от поддержки.

Мне очень нравится идея tdpetrou - использовать: names=name_dict .

Это может сделать всех счастливыми. Это дает нам возможность легко переименовывать столбцы по

Не совсем так, как упоминалось в моем первоначальном сообщении, это не решит проблему отделения места, где агрегатная операция определена, от имени результирующего столбца, что потребует дополнительных усилий, чтобы убедиться, что оба они «синхронизированы».

Я не говорю, что это плохое решение (в конце концов, оно решает другие проблемы), но это было бы не так просто и ясно, как диктантный подход. Я имею в виду, что во время написания вам необходимо синхронизировать оба словаря списков, а при чтении источника читатель должен приложить усилия, чтобы сопоставить имена во втором слове списков с совокупным определением в первом слове списков. В каждом случае это вдвое больше усилий.

Вложенные dicts несколько сложны, но написание их в том виде, в котором вы это сделали, очень ясно дает понять, что происходит.

Я до сих пор не понимаю, почему все говорят, что dict of dict сложный. Для меня это самый ясный способ сделать это.

Тем не менее, если ключевое слово names является единственным решением, которое устраивает команда pandas, это все равно будет улучшением по сравнению с текущей ситуацией.

@pirsquared интересное решение с текущим API. Хотя, на мой взгляд, это не совсем легко понять (я действительно не понимаю, как это работает: confused:)

Я начал ветку в сабреддите datascience - Что вы ненавидите в пандах groupby и указал на глагол dplyr do который реализован в plydata . Это работает точно так же, как agg_assign так что это было довольно интересно.

@zertrin agg_assign будет превосходить ваш подход dict of dict и будет идентичен агрегациям sql, а также позволит нескольким столбцам взаимодействовать друг с другом в агрегации. Он также будет работать идентично DataFrame.assign .

Есть мысли @jreback @TomAugspurger ?

...
mydf.groupby ('кошка'). agg (agg_dict, names = name_dict, drop_index = True)

Хотя это решает проблему, необходимо выровнять ключи и значения в двух местах. Я думаю, что API (как предлагается для .agg_assign ), не требующий такого бухгалтерского кода, менее подвержен ошибкам.

Также существует проблема кода очистки после использования API. Когда операции groupby возвращают MultiIndex dataframe, в большинстве случаев пользователь отменяет MultiIndex . Прямой декларативный способ использования .agg_assign не предполагает иерархии, вывода MultiIndex и последующей очистки.

Основываясь на шаблонах использования, я думаю, что для многоиндексных выходов следует строго указывать, а не отказываться.

Изначально я скептически относился к предложению agg_assign , но последние два комментария убедили меня, что это может быть хорошим решением.

Особенно думая о возможности использовать его в форме agg_assign(**relabeling_dict) и, таким образом, иметь возможность определять мой relabeling_dict следующим образом:

relabeling_dict = {
    'energy_sum': lambda x: x.energy.sum(),
    'energy_p98': lambda x: np.percentile(x.energy, 98),
    'energy_p17': lambda x: np.percentile(x.energy, 17),
    'distance_sum': lambda x: x.distance.sum(),
    'distance_mean': lambda x: x.distance.mean(),
    'distance_mad': lambda x: smrb.mad(x.distance),
    'distance_mad_c1': lambda x: mad_c1(x.distance)
}

Это было бы довольно гибко и решило бы все проблемы, упомянутые в моем OP.

@zertrin @ has2k1

Я думал об этом еще немного, и эта функция уже существует с apply . Вы просто возвращаете серию с индексом как новые имена столбцов и значения как агрегацию. Это позволяет использовать пробелы в имени и дает вам возможность упорядочивать столбцы именно так, как вы хотите:

def my_agg(x):
    data = {'energy_sum': x.energy.sum(),
            'energy_p98': np.percentile(x.energy, 98),
            'energy_p17': np.percentile(x.energy, 17),
            'distance sum' : x.distance.sum(),
            'distance mean': x.distance.mean(),
            'distance MAD': smrb.mad(x.distance),
            'distance MAD C1': mad_c1(x.distance)}
    return pd.Series(data, index=list_of_column_order)

mydf.groupby('cat').apply(my_agg)

Таким образом, может не быть необходимости в новом методе, а вместо этого просто лучший пример в документации.

@tdpetrou , вы правы. Я забыл, как работает apply как использую свою версию из-за двойного выполнения в процессе выбора быстро-медленного пути.

Хм, в самом деле, нет никаких шансов, что я бы подумал об использовании этого в контексте агрегации, просто прочитав документ, однако ...
Более того, я все еще считаю решение с apply слишком запутанным. Подход agg_assign казался более простым и понятным.

Поскольку на самом деле об этом никогда не было заявлений, действительно ли исключен подход dict-of-dict (который, хотя и устарел, но уже реализован и решает все эти проблемы)?

За исключением подхода agg_assign , dict-of-dict прежнему кажется наиболее простым и не требует никакого кодирования, просто не рекомендуется.

Преимущество и недостаток подхода agg_assign том, что он подталкивает выбор столбца к методу агрегации . Во всех примерах x переданный lambda представляет собой что-то вроде self.get_group(group) для каждой группы в self , объект DataFrameGroupBy . Это хорошо, потому что оно четко отделяет именование , которое находится в **kwargs , от выделения , которое находится в функции.

Недостатком является то, что ваши приятные общие функции агрегирования теперь должны заниматься выбором столбцов. Бесплатного обеда нет! Это означает, что у вас будет много помощников, таких как lambda x: x[col].min . Вам также нужно быть осторожным с такими вещами, как np.min который уменьшает по всем измерениям, по сравнению с pd.DataFrame.min , который уменьшает более axis=0 . Вот почему что-то вроде agg_assign не будет эквивалентно apply . apply прежнему работает по столбцам для определенных методов.

Я не уверен насчет этих компромиссов по сравнению с методом диктата, но мне любопытно услышать мысли других людей. Вот примерный набросок agg_assign , который я назвал и который я назвал agg_table чтобы подчеркнуть, что функции передаются в таблицы, а не в столбцы:

from collections import defaultdict

import pandas as pd
import numpy as np
from pandas.core.groupby import DataFrameGroupBy

mydf = pd.DataFrame(
    {
        'cat': ['A', 'A', 'A', 'B', 'B', 'C'],
        'energy': [1.8, 1.95, 2.04, 1.25, 1.6, 1.01],
        'distance': [1.2, 1.5, 1.74, 0.82, 1.01, 0.6]
    },
    index=range(6)
)


def agg_table(self, **kwargs):
    output = defaultdict(dict)
    for group in self.groups:
        for k, v in kwargs.items():
            output[k][group] = v(self.get_group(group))

    return pd.concat([pd.Series(output[k]) for k in output],
                     keys=list(output),
                     axis=1)

DataFrameGroupBy.agg_table = agg_table

использование

>>> gr = mydf.groupby("cat")
>>> gr.agg_table(n=len,
                 foo=lambda x: x.energy.min(),
                 bar=lambda y: y.distance.min())

   n   foo   bar
A  3  1.80  1.20
B  2  1.25  0.82
C  1  1.01  0.60

Я подозреваю, что мы могли бы немного сделать производительность этого менее ужасного, но не так сильно, как .agg ...

Не мог бы кто-нибудь из основной команды Pandas объяснить, в чем основная причина отказа от перемаркировки слов в groupby.agg ?

Я мог бы легко понять, вызывает ли это слишком много проблем для поддержки кода, но если это связано со сложностью для конечного пользователя - я бы также предпочел вернуть его, поскольку это довольно ясно по сравнению с необходимыми обходными путями ...

Спасибо!

Может ли кто-нибудь из основной команды Pandas объяснить, в чем основная причина отказа от перемаркировки слов в groupby.agg?

Вы видели https://github.com/pandas-dev/pandas/pull/15931/files#diff -52364fb643114f3349390ad6bcf24d8fR461?

Основная причина заключалась в том, что dict-ключи были перегружены для выполнения двух задач. Для Series / SeriesGroupBy они предназначены для именования. Для DataFrame / DataFrameGroupBy они предназначены для выбора столбца.

In [32]: mydf.aggregate({"distance": "min"})
Out[32]:
distance    0.6
dtype: float64

In [33]: mydf.aggregate({"distance": {"foo": "min"}})
/Users/taugspurger/Envs/pandas-dev/bin/ipython:1: FutureWarning: using a dict with renaming is deprecated and will be removed in a future version
  #!/Users/taugspurger/Envs/pandas-dev/bin/python3.6
Out[33]:
     distance
foo       0.6

In [34]: mydf.distance.agg({"foo": "min"})
Out[34]:
foo    0.6
Name: distance, dtype: float64

In [35]: mydf.groupby("cat").agg({"distance": {"foo": "min"}})
/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/core/groupby.py:4201: FutureWarning: using a dict with renaming is deprecated and will be removed in a future version
  return super(DataFrameGroupBy, self).aggregate(arg, *args, **kwargs)
Out[35]:
    distance
         foo
cat
A       1.20
B       0.82
C       0.60

In [36]: mydf.groupby("cat").distance.agg({"foo": "min"})
/Users/taugspurger/Envs/pandas-dev/bin/ipython:1: FutureWarning: using a dict on a Series for aggregation
is deprecated and will be removed in a future version
  #!/Users/taugspurger/Envs/pandas-dev/bin/python3.6
Out[36]:
      foo
cat
A    1.20
B    0.82
C    0.60

Вероятно, это не самая запутанная вещь в пандах, так что, возможно, мы могли бы вернуться к этому :) Я, вероятно, упускаю некоторые крайние случаи. Но даже если мы удалим агрегаты dict-of-dicts, у нас все равно будет несоответствие между именованием и выбором столбца:

Для Series / SeriesGroupBy ключи словаря всегда используются для наименования вывода.

Для DataFrame / DataFrameGroupby ключи dict всегда доступны для выбора. С помощью dict-of-dicts мы выбираем столбец, а затем внутренний dict предназначен для наименования вывода, как и Series / SeriesGroupBy.

Мы кратко обсуждали это раньше (где-то в ходе долгого обсуждения устаревания), и я предложил нечто подобное здесь: https://github.com/pandas-dev/pandas/pull/14668#issuecomment -274508089. Но в итоге была реализована только устаревшая версия, а не идеи по упрощению других функций использования dicts (функция «переименования»).

Проблема заключалась в том, что dicts использовались как для «выбора» (в каком столбце вы хотите применить эту функцию), так и для «переименования» (каким должно быть полученное имя столбца при применении этой функции). Альтернативным синтаксисом, помимо dicts, могут быть аргументы ключевого слова, как это обсуждается здесь в предложении agg_assign .
Я по-прежнему за то, чтобы изучить эту возможность, будь то в самом agg или в новом методе, таком как agg_assign .

То, что я тогда предложил, было чем-то похожим на agg_assign но с использованием dict для каждого ключевого слова вместо лямбда-функции. В переводе на приведенный здесь пример это будет примерно так:

mydf.groupby('cat').agg(
    energy_sum={'energy': 'sum'},
    energy_p98={'energy': lambda x: np.percentile(x, 98)},
    energy_p17={'energy': lambda x: np.percentile(x, 17)},
    distance_sum={'distance': 'sum'},
    distance_mean={'distance': 'mean'},
    distance_mad={'distance': smrb.mad},
    distance_mad_c1={'distance': mad_c1})

Я не уверен, что это обязательно более читабельно или проще писать как версию со всеми лямбдами, но эта потенциально может быть более производительной, поскольку панды все еще могут использовать оптимизированные реализации для суммы, среднего и т. Д. В тех столбцах, где вы это делаете не имеют лямбда-выражения или функции, указанной пользователем.

Большой вопрос при таком подходе: что будет означать df.groupby('cat').agg(foo='mean') ? Это логически применило бы «среднее значение» ко всем столбцам, поскольку вы не делали никакого выбора (аналогично {'col1' : {'foo': 'mean'}, 'col2': {'foo':'mean'}, 'col3': ...} раньше). Но это приведет к появлению многоиндексированных столбцов, хотя в приведенном выше примере я думаю, было бы неплохо не заканчиваться столбцами MI.

Я думаю, что вышеизложенное можно сделать обратно совместимым внутри существующего agg , но вопрос в том, нужно ли это.
Я также думаю, что это было бы хорошо распространено на случай series например:

mydf.groupby('cat').distance.agg(
    distance_sum='sum',
    distance_mean='mean',
    distance_mad=smrb.mad,
    distance_mad_c1=mad_c1)

(и вы даже можете подумать о том, чтобы сделать вышеупомянутый один раз для «расстояния» и один раз для «энергии» и объединить результат, если вам не нравятся все dicts / lambda)

@TomAugspurger В вашем примере простая реализация agg_table , не лучше ли было бы перебрать разные функции, которые будут применяться, вместо итерации групп, и, в конце концов, объединить новые столбцы по оси = 1 вместо объединения вновь образованных строк по оси = 0?

Кстати, @zertrin @tdpetrou @smcateer @pirsquared и другие, большое спасибо за то, что подняли эту проблему и дали такой подробный отзыв. Такая обратная связь и участие сообщества очень важны!

Мне действительно очень нравится шаблон, предложенный @tdpetrou (использование apply с функцией, возвращающей серию) - возможно, даже лучше, чем dict of dicts.

Если функция возвращает pd.Series(data, index=data.keys()) , гарантированно ли мы получим индексы в правильном порядке? (Просто думаю о том, как лучше всего реализовать шаблон в моем коде - рискуя отклониться от темы).

Изменить: извините, я неправильно понял смысл аргумента индекса (здесь он необязателен, нужен только в том случае, если вы хотите указать порядок столбцов - возврат pd.Series(data) выполняет эту работу за меня).

Будет ли пример @tdpetrou работать с first & last ?

Мне пришлось прибегнуть к такому типу головы / хвоста

def agg_funcs(x):
    data = {'start':x['DATE_TIME'].head(1).values[0],
           'finish':x['DATE_TIME'].tail(1).values[0],
           'events':len(x['DATE_TIME'])}
    return pd.Series(data, index = list(data.keys()))

results = df.groupby('col').apply(agg_funcs)

Я все еще хотел бы заняться этим, но не думаю, что это будет сделано для версии 0.23.

Может ли подход @tdpetrou работать без определения функции, которую мы больше никогда не будем использовать в нашем коде? Исходя из мира Q / Kdb + (похожего на SQL), я не понимаю, почему нам нужно создавать любую временную переменную / функцию для простого оператора выбора.

ОП здесь.

Честно говоря, после всего этого времени и множества обсуждений в # 15931 и здесь я все еще не убежден, что это хорошая идея отказаться от перемаркировки диктовок.

В конце концов, ни одна из предложенных здесь альтернатив не является более интуитивной для пользователей, чем нынешний подход к перемаркировке, ИМХО. Когда это было в документации, всего на одном примере было ясно, как это работает, и это очень гибко.

Конечно, разработчики pandas все еще могут думать иначе, просто соглашаясь с точкой зрения пользователя.

Даже подход, основанный на переименовании, не очень интуитивен. На мой взгляд, синтаксис должен быть похож на SQL - func(column_name) as new_column_name . В Python мы можем сделать это с помощью кортежа из трех элементов. (func, column_name, new_column_name) . Вот как dexplo выполняет группировку по агрегации.

dexplo

@zertrin, есть ли у вас отзывы о моем предложении выше: https://github.com/pandas-dev/pandas/issues/18366/#issuecomment -349089667
В конце концов, он как бы инвертирует порядок словаря: вместо "{col: {name: func}}" это будет вроде "** {name: {col: func}}"

@jorisvandenbossche Я рассмотрел ваш подход. Дело в том, что я не совсем понимаю, какие дополнительные преимущества он дает по сравнению с текущим подходом.

Проще говоря, учитывая следующие варианты:

  1. Неустойчивое текущее поведение, которое работает хорошо (несколько строк устаревшего кода для удаления, повторно добавить часть документации, которая была удалена)
  2. Реализуйте свое предложение (в код должны быть внесены значительные изменения, следует отказаться от текущего подхода, всем пользователям необходимо адаптировать свой код)

Я не понимаю, почему мы должны выбирать 2, если это не приносит значимых и ощутимых преимуществ с точки зрения разработчика и пользователя.

Чтобы ответить на некоторые вопросы в вашем предложении выше:

Проблема заключалась в том, что dicts использовались как для «выбора» (в каком столбце вы хотите применить эту функцию), так и для «переименования» (каким должно быть полученное имя столбца при применении этой функции).

Поскольку раньше это было хорошо задокументировано, я не думаю, что это было проблемой для пользователей . Лично я сразу понял суть, глядя на примеры в документации. (РЕДАКТИРОВАТЬ: и я подумал: _ "ура! Очень полезная конструкция, она точно соответствует тому, что я искал. Хорошо." _)

Альтернативным синтаксисом, помимо dicts, могут быть аргументы ключевого слова.

Одним из привлекательных моментов использования подхода dict-of-dict является то, что пользователи могут легко генерировать его динамически с помощью другого кода. Как вы указали в комментарии чуть выше этого, переход к аргументам ключевого слова, как в вашем предложении, все равно позволит это с помощью конструкции **{name: {col: func}} . Так что я не против вашего предложения. Я просто не вижу добавленной стоимости и необходимости таких изменений, когда мы уже достигли того же уровня функциональности, что и текущая внедренная система.

В конце концов, ваше предложение было бы _OKay_, если разработчики ядра pandas категорически против нынешнего подхода. Я просто не вижу никакой выгоды в качестве _пользователя_. (на самом деле я вижу недостаток в изменении всего существующего пользовательского кода, чтобы он снова работал с новым предложением).

@zertrin, мы обсуждали это вчера с некоторыми основными разработчиками, но не прежде чем отвечать на ваш комментарий, чтобы отразить только наши мысли вчерашнего дня.


Итак, прежде всего, мы полностью согласны с идеей о том, что базовая функциональность, такая как SQL «SELECT avg (col2) as col2_avg», должна работать и быть простой, с чем мы полностью согласны, и мы действительно хотим найти решение для этого.

Помимо первоначальных причины , почему мы решили принизить этот (который может или не может быть, сильными), ток (устаревшее) dicts из dicts также не то, что идеально подходит, так как это создает мультииндексное , что вы на самом деле не хотите:

In [1]: df = pd.DataFrame({'A': ['a', 'b', 'a'], 'B': range(3), 'C': [.1, .2, .3]})

In [3]: gr = df.groupby('A')

In [4]: gr.agg({'B': {'b_sum': 'sum'}, 'C': {'c_mean': 'mean', 'c_count': 'count'}})
Out[4]: 
        C            B
  c_count c_mean b_sum
A                     
a       2    0.2     2
b       1    0.2     1

В приведенном выше примере первый уровень MultiIndex является излишним, поскольку вы уже специально переименовали столбцы (в примере в OP за этим также непосредственно следует удаление первого уровня столбцов).
Однако это сложно изменить, потому что вы также можете делать такие вещи, как gr.agg(['sum', 'mean']) или (смешанный) gr.agg({'B': ['sum', 'mean'], 'C': {'c_mean': 'mean', 'c_count': 'count'}}) если MultiIndex необходим и имеет смысл.

Таким образом, одно из предложений, которое было упомянуто в обсуждении выше, заключалось в том, чтобы иметь способ указать окончательные имена столбцов отдельно (например, https://github.com/pandas-dev/pandas/issues/18366#issuecomment-346683449).
Добавление, например, дополнительного ключевого слова в aggregate для указания имен столбцов, например

gr.agg({'B': 'sum', 'C': ['mean', 'count']}, columns=['b_sum', 'c_mean', 'c_count'])

было бы возможно.
Однако, если мы разделим спецификацию столбца / функции и новые имена столбцов, мы также можем сделать это более общим, чем новое ключевое слово, и сделать что-то вроде:

gr.agg({'B': 'sum', 'C': ['mean', 'count']}).rename(columns=['b_sum', 'c_mean', 'c_count'])

Это требует решения https://github.com/pandas-dev/pandas/issues/14829 (что мы хотим сделать для 0.24.0).
(Важное примечание: для этого нам нужно исправить повторяющиеся имена проблемы функций лямбды, поэтому мы должны сделать какое - то автоматическая дедупликацию имен , если мы хотим поддержать это решение.)


Кроме того, нам все еще нравится способ аргументов ключевого слова для переименования. Причины этого:

  • это похоже на то, как assign работает в пандах, а также согласуется с тем, как groupby().aggregate() работает в ibis (а также похоже на то, как это выглядит, например, dplyr в R)
  • он напрямую дает вам неиерархические имена столбцов, которые вы хотите (без MultiIndex)
  • для простых случаев (также, например, для случая серии), я думаю, что это проще, чем диктат диктата

Мы все еще немного обсуждали, как это может выглядеть. Я предложил выше (использовать эквивалентный выбор столбца / функции, как в моих первых примерах):

gr.agg(b_sum={'B': 'sum'}, c_mean={'C': 'mean'}, c_count={'C': 'count'})

Вы все еще можете создать эту спецификацию как диктовку, но с заменой внутреннего и внешнего уровней по сравнению с текущей (устаревшей) версией:

gr.agg(**{'b_sum': {'B': 'sum'}, 'c_mean': {'C': 'mean'}, 'c_count': {'C': 'count'})

(у нас может быть пример вспомогательной функции, которая преобразует существующие диктовки в эту версию)

Однако dict всегда представляет собой только один {col: func} , и эти многократные одноэлементные dicts выглядят немного странно. Итак, мы подумали об альтернативе - использовать кортежи:

gr.agg(b_sum=('B', 'sum'), c_mean=('C', 'mean'), c_count=('C', 'count'))

Это выглядит немного лучше, но, с другой стороны, {'B': 'sum'} dict согласуется с другими API-интерфейсами для указания столбца, к которому следует применить функцию.


Оба приведенных выше предложения (более легкое переименование впоследствии и именование на основе ключевых слов) в принципе ортогональны, но было бы неплохо иметь и то, и другое (или еще что-то на основе дальнейшего обсуждения)

Спасибо, что отправили сюда текущие мысли разработчиков 😃

Я признаю (на мой взгляд, единственный) недостаток устаревшего подхода dict-of-dict с результирующим MultiIndex. Может быть сглажено, если пользователь передаст дополнительную опцию (да, YAO: - /).

Как уже упоминалось, я не против второй версии, если это возможно:

  • каким-то образом динамически генерировать вещи и распаковывать их (благодаря конструкции **{} , ура, Python!)
  • держите переименование и спецификацию агрегации близко друг к другу (необходимость отслеживать два списка, чтобы их порядок оставался неизменным, просто раздражает как пользователя ИМХО)
  • использовать лямбда-функции или частичные функции без необходимости обходных путей из-за (потенциально отсутствия или конфликта) имен функций.

Таким образом, последнее предложение (с dicts или кортежами для сопоставления col> func), я думаю, в порядке.

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

Обсуждалось сегодня на встрече разработчиков.

Краткое содержание

  1. @jorisvandenbossche попытается реализовать gr.agg(b_sum=("B", "sum), ...) , т.е. когда arg передано *GroupBy.agg , интерпретировать kwargs как <output_name>=(<selection>, <aggfunc>)
  2. Ортогонально к этим проблемам, мы хотели бы реализовать MutliIndex.flatten и предоставить ключевое слово flatten=True для .agg

Может быть, это поможет: мой обходной путь для устаревания - это вспомогательные функции, которые заменяют карты alias-> aggr списком правильно названных функций:

def aliased_aggr(aggr, name):
    if isinstance(aggr,str):
        def f(data):
            return data.agg(aggr)
    else:
        def f(data):
            return aggr(data)
    f.__name__ = name
    return f

def convert_aggr_spec(aggr_spec):
    return {
        col : [ 
            aliased_aggr(aggr,alias) for alias, aggr in aggr_map.items() 
        ]  
        for col, aggr_map in aggr_spec.items() 
    }

что дает старое поведение с:

mydf_agg = mydf.groupby('cat').agg(convert_aggr_spec{
    'energy': {
        'total_energy': 'sum',
        'energy_p98': lambda x: np.percentile(x, 98),  # lambda
        'energy_p17': lambda x: np.percentile(x, 17),  # lambda
    },
    'distance': {
        'total_distance': 'sum',
        'average_distance': 'mean',
        'distance_mad': smrb.mad,   # original function
        'distance_mad_c1': mad_c1,  # partial function wrapping the original function
    },
}))

который совпадает с

mydf_agg = mydf.groupby('cat').agg({
    'energy': [ 
        aliased_aggr('sum', 'total_energy'),
        aliased_aggr(lambda x: np.percentile(x, 98), 'energy_p98'),
        aliased_aggr(lambda x: np.percentile(x, 17), 'energy_p17')
    ],
    'distance': [
         aliased_aggr('sum', 'total_distance'),
         aliased_aggr('mean', 'average_distance'),
         aliased_aggr(smrb.mad, 'distance_mad'),
         aliased_aggr(mad_c1, 'distance_mad_c1'),
    ]
})

Это работает для меня, но, вероятно, не будет работать в некоторых угловых случаях ...

Обновление : обнаружено, что переименование не требуется, поскольку кортежи в спецификации агрегирования интерпретируются как (псевдоним, aggr). Таким образом, функция alias_aggr не нужна, и преобразование выглядит следующим образом:

def convert_aggr_spec(aggr_spec):
    return {
        col : [ 
           (alias,aggr) for alias, aggr in aggr_map.items() 
        ]  
        for col, aggr_map in aggr_spec.items() 
    }

Я просто хочу присоединиться к этому как еще одному пользователю, которому действительно очень не хватает функциональности агрегирования столбца для любой функции и немедленного переименования его в той же строке. Я _ никогда_ обнаружил, что использую MultiIndex, возвращаемый pandas - я либо сразу его сглаживаю, либо я действительно хочу вручную указать имена столбцов, потому что они на самом деле означают что-то конкретное.

Я был бы доволен любым из предложенных здесь подходов: синтаксис, подобный SQL (я на самом деле обнаружил, что уже много использую .query() в пандах), возврат к устаревшему поведению, любые другие предложения. Нынешний подход уже вызвал насмешки со стороны коллег, использующих R.

Недавно я даже обнаружил, что использую PySpark вместо pandas, хотя в этом нет необходимости, просто потому, что мне гораздо больше нравится синтаксис:

df.groupby("whatever").agg(
    F.max("col1").alias("my_max_col"),
    F.avg("age_col").alias("average_age"),
    F.sum("col2").alias("total_yearly_payments")
)

Кроме того, PySpark в большинстве случаев более запутан в написании, чем pandas, это просто выглядит намного чище! Так что я определенно ценю, что работа над этим еще продолжается :-)

Я думаю, у нас есть согласованный синтаксис для этой функции; нам нужен кто-то
реализовать это.

В среду, 27 марта 2019 г., в 9:01 Thomas Kastl [email protected]
написал:

Я просто хочу присоединиться к этому как еще один пользователь, который действительно, очень
отсутствует функция агрегирования столбца для любой функции и
сразу переименовав его в том же ряду. Я никогда не находил себя
используя MultiIndex, возвращаемый pandas - я либо сразу его сглаживаю,
или я действительно хочу вручную указать имена столбцов, потому что они
на самом деле означает что-то конкретное.

Я был бы доволен любым из предложенных здесь подходов: SQL-подобный синтаксис
(На самом деле я уже часто использую .query () в пандах),
возврат к режиму амортизации, любые другие предложения. В
Текущий подход уже вызвал насмешки со стороны коллег, использующих R.

Недавно я даже обнаружил, что использую PySpark вместо pandas, хотя
в этом не было необходимости просто потому, что мне гораздо больше нравится синтаксис:

df.groupby ("что угодно"). agg (F.max ("col1"). alias ("my_max_col"),
F.avg ("age_col"). Alias ​​("average_age"),
F.sum ("col2"). Alias ​​("total_yearly_payments"))

Кроме того, PySpark в большинстве случаев более запутан в написании, чем pandas,
это выглядит намного чище! Так что я определенно ценю эту работу над
это еще нужно сделать :-)

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/pandas-dev/pandas/issues/18366#issuecomment-477168767 ,
или отключить поток
https://github.com/notifications/unsubscribe-auth/ABQHIkCYYsah5siYA4_z0oop_ufIB3h8ks5va3nJgaJpZM4QjSLL
.

Я попытаюсь добраться до этого в версии 0.25.0.

Я разместил PR на https://github.com/pandas-dev/pandas/pull/26399. Основная идея состоит в том, чтобы разрешить эту смесь переименования и агрегации для конкретного столбца с помощью **kwargs с пониманием того, что значения должны быть кортежами из (selection, aggfunc) .

In [2]: df = pd.DataFrame({'kind': ['cat', 'dog', 'cat', 'dog'],
   ...:                    'height': [9.1, 6.0, 9.5, 34.0],
   ...:                    'weight': [7.9, 7.5, 9.9, 198.0]})

In [3]: df
Out[3]:
  kind  height  weight
0  cat     9.1     7.9
1  dog     6.0     7.5
2  cat     9.5     9.9
3  dog    34.0   198.0

In [4]: df.groupby('kind').agg(min_height=('height', 'min'), max_weight=('weight', 'max'))
Out[4]:
      min_height  max_weight
kind
cat          9.1         9.9
dog          6.0       198.0

У этого есть несколько ограничений

  • Это несколько своеобразно для остальных панд. Sytanx (output_name=(selection, aggfunc)) самом деле больше нигде не появляется (хотя .assign действительно использует шаблон output_name=... )
  • Написание имен выходных данных, не являющихся идентификаторами Python, некрасиво: .agg(**{'output name': (col, func)})
  • Это только Python 3.6+, или нам нужны уродливые хаки для 3.5 и ранее, поскольку порядок **kwargs ранее не сохранялся
  • Аггфункция должна быть унарной функцией. Если вашему пользовательскому агфунку требуются дополнительные аргументы, вам нужно сначала частично применить его.

И есть деталь реализации: несколько lambda aggfuncs для одного и того же столбца еще не поддерживаются, хотя это можно исправить позже.


Я подозреваю, что большинство людей, подписавшихся здесь, поддержат некоторую альтернативу устаревшему поведению. Что люди думают об этом конкретно?

cc @WillAyd, если я пропустил какие-либо ваши опасения.

Привет @TomAugspurger!

Спасибо, что сделали этот шаг вперед.

У этого есть несколько ограничений

  • Это несколько своеобразно для остальных панд. Sytanx (output_name=(selection, aggfunc)) самом деле больше нигде не появляется (хотя .assign действительно использует шаблон output_name=... )

Я не могу не чувствовать, что такого рода аргументы очень похожи на те, которые в первую очередь мотивировали отказаться от существующей реализации.

Не могли бы вы рассказать, почему мы получаем больше преимуществ от этого нового способа по сравнению со старым _ в отношении этого конкретного аргумента_?

Одно из преимуществ, о котором я уже мог подумать, заключается в том, что (для py3.6 +) мы можем выбирать порядок вывода столбцов индивидуально.

  • Написание имен выходных данных, не являющихся идентификаторами Python, некрасиво: .agg(**{'output name': (col, func)})

Почему-то в этом отношении старый способ был лучше. Но, как я сказал ранее, если можно использовать конструкцию **{...} для динамического построения агрегации, я был бы достаточно счастлив.

  • Это только Python 3.6+, или нам нужны уродливые хаки для 3.5 и ранее, поскольку порядок **kwargs ранее не сохранялся

Как это работало раньше (существующая функция dict-of-dict)? был ли заказ каким-то образом гарантирован?

  • Аггфункция должна быть унарной функцией. Если вашему пользовательскому агфунку требуются дополнительные аргументы, вам нужно сначала частично применить его.

Просто чтобы подтвердить мое понимание: agfunc может быть любым вызываемым, который возвращает допустимое значение, верно? (в дополнение к "часто используемым" строковым агфунгам типа 'min' , 'max' и т. д.). Есть ли разница по сравнению с тем, что было раньше? (т.е. разве не было унарного ограничения?)

И есть деталь реализации: несколько lambda aggfuncs для одного и того же столбца еще не поддерживаются, хотя это можно исправить позже.

Да, это немного раздражает, но пока это всего лишь временное ограничение и его можно исправить, это может сработать.

Я подозреваю, что большинство людей, подписавшихся здесь, поддержат некоторую альтернативу устаревшему поведению. Что люди думают об этом конкретно?

Что ж, в любом случае я думаю, что агрегирование и переименование за один шаг действительно важно сохранить. Если прежнее поведение действительно не подходит, тогда подойдет и этот вариант.

Не могли бы вы поделиться, почему мы получаем больше преимуществ от этого нового способа по сравнению со старым в отношении этого конкретного аргумента.

Возможно, я неправильно запомнил, но я считаю, что SeriesGroupby.agg и DataFrameGroupby.agg имеют разные значения между внешним ключом в словаре (это выбор столбца или имя вывода?). С помощью этого синтаксиса мы можем постоянно иметь ключевое слово, означающее имя вывода.

Почему-то в этом отношении старый способ был лучше.

Разница только в ** ? В остальном я думаю, что у всех одни и те же ограничения.

Как это работало раньше (существующая функция dict-of-dict)? был ли заказ каким-то образом гарантирован?

Сортировка ключей, чем я сейчас занимаюсь в PR.

Просто чтобы подтвердить мое понимание: agfunc может быть любым вызываемым, который возвращает допустимое значение, верно?

Вот разница

In [21]: df = pd.DataFrame({"A": ['a', 'a'], 'B': [1, 2], 'C': [3, 4]})

In [22]: def aggfunc(x, myarg=None):
    ...:     print(myarg)
    ...:     return sum(x)
    ...:

In [23]: df.groupby("A").agg({'B': {'foo': aggfunc}}, myarg='bar')
/Users/taugspurger/sandbox/pandas/pandas/core/groupby/generic.py:1308: FutureWarning: using a dict with renaming is deprecated and will be removed in a future version
  return super().aggregate(arg, *args, **kwargs)
None
Out[23]:
    B
  foo
A
a   3

с альтернативным предложением мы резервируем **kwargs для имен выходных столбцов. Итак, вам понадобится functools.partitial(aggfunc, myarg='bar') .

Хорошо, спасибо, я думаю, что предлагаемый подход 👍 для первой итерации (и действительно будет хорош в качестве замены, как только будет снято ограничение на множественную реализацию лямбда)

Была ли эта страница полезной?
0 / 5 - 0 рейтинги