столбцы pandas GroupBy со значениями NaN (отсутствующими)

162

У меня есть DataFrame со многими пропущенными значениями в столбцах, которые я хочу сгруппировать:

import pandas as pd
import numpy as np
df = pd.DataFrame({'a': ['1', '2', '3'], 'b': ['4', np.NaN, '6']})

In [4]: df.groupby('b').groups
Out[4]: {'4': [0], '6': [2]}

видите, что Pandas сбросил строки с целевыми значениями NaN. (Я хочу включить эти строки!)

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

Какие-либо предложения? Стоит ли писать для этого функцию или есть простое решение?

Дьюла Самуэль Карли
источник
1
@PhillipCloud Я отредактировал этот вопрос, чтобы включить только вопрос, который на самом деле неплохой, касающийся улучшения открытых панд Джеффа.
Энди Хайден
1
Отсутствие возможности включать (и размножать) NaN в группы довольно неприятно. Цитирование R неубедительно, так как такое поведение не согласуется с множеством других вещей. В любом случае, фиктивный хак тоже довольно плохой. Однако размер (включая NaN) и количество (без учета NaN) группы будут отличаться, если есть NaN. dfgrouped = df.groupby (['b']). a.agg (['sum', 'size', 'count']) dfgrouped ['sum'] [dfgrouped ['size']! = dfgrouped ['count ']] = None
Можете ли вы резюмировать, чего конкретно вы пытаетесь достичь? т.е. мы видим результат, но что это за «желаемый» результат?
приблизительно
4
С пандами 1.1 вы скоро сможете указать dropna=Falseв , groupby()чтобы получить желаемый результат. Подробнее
cs95

Ответы:

139

Это упоминается в разделе «Отсутствующие данные» документации :

Группы NA в GroupBy автоматически исключаются. Такое поведение согласуется с R

Один из обходных путей - использовать заполнитель перед выполнением группировки (например, -1):

In [11]: df.fillna(-1)
Out[11]: 
   a   b
0  1   4
1  2  -1
2  3   6

In [12]: df.fillna(-1).groupby('b').sum()
Out[12]: 
    a
b    
-1  2
4   1
6   3

Тем не менее, это выглядит довольно ужасным взломом ... возможно, должна быть возможность включить NaN в groupby (см. Эту проблему с github - в которой используется тот же хак-заполнитель).

Однако, как описано в другом ответе, из pandas 1.1 вы лучше контролируете это поведение, значения NA теперь разрешены в группировщике с использованием dropna = False

Энди Хайден
источник
4
Это логичное, но своего рода забавное решение, о котором я подумал ранее: Pandas создает поля NaN из пустых, и мы должны их изменить. Это причина того, что я думаю о поиске других решений, таких как запуск SQL-сервера и запрос таблиц оттуда (выглядит слишком сложно), или поиск другой библиотеки, несмотря на Pandas, или использование моей собственной (что я хочу избавиться). Спасибо
Дьюла Самуэль Карли
@ GyulaSámuelKarli Мне это кажется небольшой ошибкой (см. Отчет об ошибке выше), и мое решение - обходной путь. Мне кажется странным, что вы списываете со счетов всю библиотеку.
Энди Хайден
1
Я не хочу записывать Pandas, просто ищите инструмент, который больше всего соответствует моим запросам.
Дьюла Самуэль Карли
1
Взгляните на мой ответ ниже, я считаю, что нашел довольно хорошее (более чистое и, возможно, более быстрое) решение. stackoverflow.com/a/43375020/408853
ча
4
Нет, это не согласуется с R. df%>% group_by также выдаст сводки NA с предупреждением, которого можно избежать, передав столбец группировки через fct_explicit_na, а затем будет создан уровень (Отсутствует).
Ravaging Care
65

панды> = 1.1

Начиная с pandas 1.1, вы лучше контролируете это поведение, значения NA теперь разрешены в группировщике, используя dropna=False:

pd.__version__
# '1.1.0.dev0+2004.g8d10bfb6f'

# Example from the docs
df

   a    b  c
0  1  2.0  3
1  1  NaN  4
2  2  1.0  3
3  1  2.0  2

# without NA (the default)
df.groupby('b').sum()

     a  c
b        
1.0  2  3
2.0  2  5
# with NA
df.groupby('b', dropna=False).sum()

     a  c
b        
1.0  2  3
2.0  2  5
NaN  1  4
cs95
источник
9
Надеюсь, этот ответ постепенно поднимется к вершине. Это правильный подход.
kdbanman
43

Древняя тема, если кто-то все еще спотыкается, другой обходной путь - преобразовать через .astype (str) в строку перед группировкой. Это сохранит NaN.

df = pd.DataFrame({'a': ['1', '2', '3'], 'b': ['4', np.NaN, '6']})
df['b'] = df['b'].astype(str)
df.groupby(['b']).sum()
    a
b   
4   1
6   3
nan 2
М. Киевиш
источник
@ K3 --- rnc: Смотрите комментарий к вашей ссылке - автор сообщения по вашей ссылке что-то сделал не так.
Thomas
@Thomas, да, именно так, как в примере выше. Пожалуйста, отредактируйте, если вы можете сделать пример безопасным (и таким же тривиальным).
K3 --- rnc
sumИз aявляется конкатенация здесь, а не цифровая сумма. Это «работает» только потому, что 'b' состоит из разных записей. Вы должны «а» быть числовыми и «б» быть строка
BallpointBen
10

Я не могу добавить комментарий к M. Kiewisch, так как у меня недостаточно очков репутации (их всего 41, но мне нужно больше 50 для комментариев).

В любом случае, просто хочу отметить, что решение M. Kiewisch не работает как есть и, возможно, потребуется дополнительная настройка. Рассмотрим, например,

>>> df = pd.DataFrame({'a': [1, 2, 3, 5], 'b': [4, np.NaN, 6, 4]})
>>> df
   a    b
0  1  4.0
1  2  NaN
2  3  6.0
3  5  4.0
>>> df.groupby(['b']).sum()
     a
b
4.0  6
6.0  3
>>> df.astype(str).groupby(['b']).sum()
      a
b
4.0  15
6.0   3
nan   2

который показывает, что для группы b = 4.0 соответствующее значение равно 15 вместо 6. Здесь 1 и 5 просто объединяются как строки, а не складываются как числа.

Камараджу Кусуманчи
источник
13
Это потому , что вы превратили всю DF на ул, а не только bколонки
Корем
Обратите внимание, что теперь это было исправлено в упомянутом ответе.
Шайдо
1
Новое решение, на мой взгляд, лучше, но все же небезопасно. Рассмотрим случай, когда одна из записей в столбце «b» такая же, как строковый np.NaN. Затем эти вещи складываются вместе. df = pd.DataFrame ({'a': [1, 2, 3, 5, 6], 'b': ['foo', np.NaN, 'bar', 'foo', 'nan']}) ; df ['b'] = df ['b']. astype (строка); df.groupby (['b']). sum ()
Камараджу Кусуманчи
6

Одно небольшое замечание по поводу решения Энди Хайдена - оно не работает (больше?), Потому что np.nan == np.nanуступает False, поэтому replaceфункция на самом деле ничего не делает.

Для меня сработало следующее:

df['b'] = df['b'].apply(lambda x: x if not np.isnan(x) else -1)

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

Tuetschek
источник
13
Также есть df['b'].fillna(-1).
K3 ---
6

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

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

def safe_groupby(df, group_cols, agg_dict):
    # set name of group col to unique value
    group_id = 'group_id'
    while group_id in df.columns:
        group_id += 'x'
    # get final order of columns
    agg_col_order = (group_cols + list(agg_dict.keys()))
    # create unique index of grouped values
    group_idx = df[group_cols].drop_duplicates()
    group_idx[group_id] = np.arange(group_idx.shape[0])
    # merge unique index on dataframe
    df = df.merge(group_idx, on=group_cols)
    # group dataframe on group id and aggregate values
    df_agg = df.groupby(group_id, as_index=True)\
               .agg(agg_dict)
    # merge grouped value index to results of aggregation
    df_agg = group_idx.set_index(group_id).join(df_agg)
    # rename index
    df_agg.index.name = None
    # return reordered columns
    return df_agg[agg_col_order]

Обратите внимание, что теперь вы можете просто сделать следующее:

data_block = [np.tile([None, 'A'], 3),
              np.repeat(['B', 'C'], 3),
              [1] * (2 * 3)]

col_names = ['col_a', 'col_b', 'value']

test_df = pd.DataFrame(data_block, index=col_names).T

grouped_df = safe_groupby(test_df, ['col_a', 'col_b'],
                          OrderedDict([('value', 'sum')]))

Это вернет успешный результат, не беспокоясь о перезаписи реальных данных, которые были ошибочно приняты за фиктивное значение.

Грант Лангсет
источник
Это лучшее решение для общего случая, но в случаях, когда я знаю недопустимую строку / число, которое я могу использовать вместо этого, я, вероятно, собираюсь пойти с ответом Энди Хайдена ниже ... Я надеюсь, что панды скоро исправят это поведение.
Сара Мессер
4

Я уже ответил на это, но почему-то ответ был преобразован в комментарий. Тем не менее, это наиболее эффективное решение:

Неспособность включать (и размножать) NaN в группы довольно неприятно. Цитирование R неубедительно, так как такое поведение не согласуется с множеством других вещей. В любом случае, фиктивный хак тоже довольно плохой. Однако размер (включая NaN) и количество (без учета NaN) группы будут отличаться, если есть NaN.

dfgrouped = df.groupby(['b']).a.agg(['sum','size','count'])

dfgrouped['sum'][dfgrouped['size']!=dfgrouped['count']] = None

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


источник
1
Это было очень полезно для меня, но оно отвечает на несколько иной вопрос, чем исходный. IIUC, ваше решение распространяет NaN при суммировании, но элементы NaN в столбце «b» по-прежнему отбрасываются как строки.
Эндрю