Pandas groupby с категориями с избыточным nan

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

Я иногда использую категории, даже когда есть низкая плотность общих строк, просто потому, что эти строки длинные, и это экономит память / улучшает спектакль. Иногда в каждой колонке тысячи категорий. При группировании по 3 столбцам,pandas заставляет нас удерживать результаты для 1000^3 групп.

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

  • воссоздание всех функций через numpy.
  • постоянное преобразование в строки / коды до groupby, возвращаясь для категории позже.
  • создание столбца кортежа из столбцов группы, затем группирование по столбцу кортежа.

Я надеюсь, что есть способ изменить только этой конкретной pandas идиосинкразия. Простой пример приведен ниже. Вместо 4 категорий, которые я хочу на выходе, я получаю 12.

import pandas as pd

group_cols = ['Group1', 'Group2', 'Group3']

df = pd.DataFrame([['A', 'B', 'C', 54.34],
                   ['A', 'B', 'D', 61.34],
                   ['B', 'A', 'C', 514.5],
                   ['B', 'A', 'A', 765.4],
                   ['A', 'B', 'D', 765.4]],
                  columns=(group_cols+['Value']))

for col in group_cols:
    df[col] = df[col].astype('category')

df.groupby(group_cols, as_index=False).sum()

Group1  Group2  Group3  Value
#   A   A   A   NaN
#   A   A   C   NaN
#   A   A   D   NaN
#   A   B   A   NaN
#   A   B   C   54.34
#   A   B   D   826.74
#   B   A   A   765.40
#   B   A   C   514.50
#   B   A   D   NaN
#   B   B   A   NaN
#   B   B   C   NaN
#   B   B   D   NaN

обновление Bounty

проблема плохо решена командой разработчиков pandas (cf github.com/pandas-dev/pandas/issues/17594). Поэтому я ищу ответы, что адреса следующие:

  1. почему, со ссылкой на исходный код pandas, категориальные данные обрабатываются по-разному в операциях groupby?
  2. почему предпочтительнее текущая реализация? Я понимаю, что это субъективно, но я изо всех сил пытаюсь найти ответ на этот вопрос. Нынешнее поведение является запретительным во многих ситуациях без громоздких, дорогостоящих, обходные пути.
  3. есть ли чистое решение для переопределения обработки панд категориальных данных в операциях groupby? Обратите внимание на 3 маршрута no-go (снижение до numpy; преобразования в/из кодов; создание и группирование по столбцам кортежей). Я бы предпочел решение, которое "соответствует пандам", чтобы минимизировать / избежать потери других категориальных функций панд.
  4. ответ от команды разработчиков pandas для поддержки и уточнения существующих лечение. Кроме того, почему все комбинации категорий не должны настраиваться как логический параметр?

обновление Bounty #2

чтобы быть ясным, я не ожидаю ответов на все вышеперечисленные 4 вопроса. Главный вопрос, который я задаю, заключается в том, можно ли Или желательно перезаписать pandas методы библиотеки, чтобы категории обрабатывались таким образом, чтобы облегчить groupby / set_index операции.

5 ответов


начиная с панды 0.23.0,groupby метод теперь можно взять параметр observed который устраняет эту проблему, если он установлен в True (по умолчанию False). Ниже приведен тот же самый код, что и в вопросе с just observed=True добавил :

import pandas as pd

group_cols = ['Group1', 'Group2', 'Group3']

df = pd.DataFrame([['A', 'B', 'C', 54.34],
                   ['A', 'B', 'D', 61.34],
                   ['B', 'A', 'C', 514.5],
                   ['B', 'A', 'A', 765.4],
                   ['A', 'B', 'D', 765.4]],
                  columns=(group_cols+['Value']))

for col in group_cols:
    df[col] = df[col].astype('category')

df.groupby(group_cols, as_index=False, observed=True).sum()

enter image description here


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

import pandas as pd

group_cols = ['Group1', 'Group2', 'Group3']

df = pd.DataFrame([['A', 'B', 'C', 54.34],
                   ['A', 'B', 'D', 61.34],
                   ['B', 'A', 'C', 514.5],
                   ['B', 'A', 'A', 765.4],
                   ['A', 'B', 'D', 765.4]],
                  columns=(group_cols+['Value']))
for col in group_cols:
    df[col] = df[col].astype('category')


result = df.groupby([df[col].values.codes for col in group_cols]).sum()
result = result.reset_index()
level_to_column_name = {f"level_{i}":col for i,col in enumerate(group_cols)}
result = result.rename(columns=level_to_column_name)
for col in group_cols:
    result[col] = pd.Categorical.from_codes(result[col].values, categories=df[col].values.categories)
result

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

Group1  Group2  Group3  Value
A       B       C       54.34
A       B       D       826.74
B       A       A       765.40
B       A       C       514.50

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

def categorical_groupby(df,group_cols,agg_fuction="sum"):
    "Does a groupby on a number of categorical columns"
    result = df.groupby([df[col].values.codes for col in group_cols]).agg(agg_fuction)
    result = result.reset_index()
    level_to_column_name = {f"level_{i}":col for i,col in enumerate(group_cols)}
    result = result.rename(columns=level_to_column_name)
    for col in group_cols:
        result[col] = pd.Categorical.from_codes(result[col].values, categories=df[col].values.categories)
    return result

назовем это так:

df.pipe(categorical_groupby,group_cols)

Я нашел поведение, подобное тому, что описано в разделе операций Категорийных Данных.

в частности, подобный

In [121]: cats2 = pd.Categorical(["a","a","b","b"], categories=["a","b","c"])

In [122]: df2 = pd.DataFrame({"cats":cats2,"B":["c","d","c","d"], "values":[1,2,3,4]})

In [123]: df2.groupby(["cats","B"]).mean()
Out[123]: 
        values
cats B        
a    c     1.0
     d     2.0
b    c     3.0
     d     4.0
c    c     NaN
     d     NaN

некоторые другие слова, описывающие поведения в Series и groupby. В конце раздела приведен пример сводной таблицы.

кроме серии.min (), Series.Макс() и серии.mode (), следующее операции возможны с категориальными данные:

методы серия как серия.value_counts () будет использовать все категории, даже если некоторые категории отсутствуют в данных:

Groupby также покажет "неиспользуемые" категории:

слова и пример приведены из Категорийных Данных.


есть много вопросы здесь.
Давайте начнем с понимания того, что такое "категория"...

определение категориального dtype

цитирую панды docs для "категорийных данных":

Categoricals-это тип данных pandas,которые соответствуют категориальным переменным в статистике: переменная, которая может принимать только ограниченное, и обычно фиксированное количество возможных значений (категории; уровни в R). Примерами являются пол, социальный класс, группы крови, принадлежность к стране, время наблюдения или рейтинги по шкале Ликерта.

есть два момента, на которых я хочу сосредоточиться здесь:

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

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

    When a factor is first created, all of its levels are stored along with the factor, and if subsets of the factor are extracted, they will retain all of the original levels. This can create problems when constructing model matrices and may or may not be useful when displaying the data using, say, the table function. As an example, consider a random sample from the letters vector, which is part of the base R distribution.
    
    > lets = sample(letters,size=100,replace=TRUE)
    > lets = factor(lets)
    > table(lets[1:5])
    
    a b c d e f g h i j k l m n o p q r s t u v w x y z
    1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 0 1
    
    Even though only five of the levels were actually represented, the table function shows the frequencies for all of the levels of the original factors. To change this, we can simply use another call to factor
    
    > table(factor(lets[1:5]))
    
    a k q s z
    1 1 1 1 1
    

в основном это говорит нам, что отображение/использование всех категорий, даже если они не нужны, не так уж редко. И на самом деле, это поведение по умолчанию!
Это связано с обычными случаями использования категориальных переменных в статистике. Почти во всех случаях do забота обо всех категориях, даже если они не используются. Принимать пример функции pandas вырезать.

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

GroupBy по категориальным переменным

а почему groupby рассмотрим все комбинации категорий: я не могу сказать наверняка, но мое лучшее предположение, основанное на быстром обзоре исходного кода (и проблемы github, о которой Вы упомянули), заключается в том, что они считают groupby по категориальным переменным an взаимодействие между ними. Следовательно, он должен рассматривать все пары / кортежи (например, декартово произведение). AFAIK, это очень помогает, когда вы пытаетесь сделать что-то вроде ANOVA.
Это также означает, что в этом контексте вы не можете думать об этом в обычной SQL-подобной терминологии.

решений?

хорошо, но что, если вы не хотите такого поведения?
Насколько мне известно, и учитывая, что я провел последнюю ночь отслеживая это в исходном коде pandas, вы не можете "отключить" его. Он жестко закодирован на каждом критическом этапе.
Однако из-за пути groupby работает, фактическое "расширение" не происходит, пока оно не понадобится. Например, при вызове sum над группами или пытается их распечатать.
Следовательно, вы можете выполнить любое из следующих действий, чтобы получить только необходимые группы:

df.groupby(group_cols).indices
#{('A', 'B', 'C'): array([0]),
# ('A', 'B', 'D'): array([1, 4]),
# ('B', 'A', 'A'): array([3]),
# ('B', 'A', 'C'): array([2])}

df.groupby(group_cols).groups
#{('A', 'B', 'C'): Int64Index([0], dtype='int64'),
# ('A', 'B', 'D'): Int64Index([1, 4], dtype='int64'),
# ('B', 'A', 'A'): Int64Index([3], dtype='int64'),
# ('B', 'A', 'C'): Int64Index([2], dtype='int64')}

# an example
for g in df.groupby(group_cols).groups:
    print(g, grt.get_group(g).sum()[0])
#('A', 'B', 'C') 54.34
#('A', 'B', 'D') 826.74
#('B', 'A', 'A') 765.4
#('B', 'A', 'C') 514.5

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


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

вот код, который выполняет первоначальную цель:

r = df.groupby(group_cols, as_index=False).agg({'Value': 'sum'})

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

enter image description here

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

Я не знаю, почему методы экземпляра:

df.groupby(group_cols).sum() 
df.groupby(group_cols).mean()
df.groupby(group_cols).stdev()

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

df.groupby(group_cols).agg(['count', 'sum', 'mean', 'std']) 

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

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

в нашем случае разница между декартовым произведением группирующих переменных и естественно встречающимися комбинациями составляет более 1000x (а начальный набор данных-более 1 000 000 строк).

следовательно, я бы проголосовал за то, чтобы сделать observed=True по умолчанию поведение.