Панды преобразовывают противоречивое поведение для списка

У меня есть образец фрагмента, который работает, как и ожидалось:

import pandas as pd

df = pd.DataFrame(data={'label': ['a', 'b', 'b', 'c'], 'wave': [1, 2, 3, 4], 'y': [0,0,0,0]})
df['new'] = df.groupby(['label'])[['wave']].transform(tuple)

Результат:

  label  wave  y     new
0     a     1  0    (1,)
1     b     2  0  (2, 3)
2     b     3  0  (2, 3)
3     c     4  0    (4,)

Это работает аналогично, если вместо tuple в преобразовании я даю set, frozenset, dict, но если я даю list, я получаю совершенно неожиданный результат:

df['new'] = df.groupby(['label'])[['wave']].transform(list)

  label  wave  y  new
0     a     1  0    1
1     b     2  0    2
2     b     3  0    3
3     c     4  0    4

Существует обходной путь для получения ожидаемого результата:

df['new'] = df.groupby(['label'])[['wave']].transform(tuple)['wave'].apply(list)

  label  wave  y     new
0     a     1  0     [1]
1     b     2  0  [2, 3]
2     b     3  0  [2, 3]
3     c     4  0     [4]

Я думал об изменчивости/неизменности (list/tuple), но для set/frozenset это согласуется.

Вопрос в том, почему это работает таким образом?

Ответы

Ответ 1

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

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

df.groupby(['label'])[['wave']].transform(lambda x: list(x))
    wave
0   1
1   2
2   3
3   4

Однако, если длина списка не совпадает с каждой группой, вы получите желаемое поведение:

df.groupby(['label'])[['wave']].transform(lambda x: list(x)+[0])

    wave
0   [1, 0]
1   [2, 3, 0]
2   [2, 3, 0]
3   [4, 0]

Я думаю, что это побочный эффект от функции распаковки списка.

Ответ 2

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

Сначала я подумал, что это может быть потому, что list просто неправильно обрабатывается как аргумент для .transform, но если я это сделаю:

def create_list(obj):
    print(type(obj))
    return obj.to_list()

df.groupby(['label'])[['wave']].transform(create_list)

Я получаю тот же неожиданный результат. Однако если используется метод agg, он работает напрямую:

df.groupby(['label'])['wave'].agg(list)
Out[179]: 
label
a       [1]
b    [2, 3]
c       [4]
Name: wave, dtype: object

Я не могу представить, что это намеренное поведение.

Btw. Я также нахожу подозрительным другое поведение, которое проявляется, если применить кортеж к сгруппированному ряду и сгруппированному фрейму данных. Например. если transform применяется к серии вместо DataFrame, результатом также является не серия, содержащая списки, а серия, содержащая ints (не забывайте о [['wave']], который создает одноколонный фрейм данных transform(tuple) действительно возвращал кортежи ):

df.groupby(['label'])['wave'].transform(tuple)
Out[177]: 
0    1
1    2
2    3
3    4
Name: wave, dtype: int64

Если я сделаю это снова с agg вместо transform, это сработает как для ['wave'], так и для [['wave']]

Я использовал версию 0.25.0 в системе Ubuntu X86_64 для своих тестов.

Ответ 3

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

pd.DataFrame.trasnform изначально реализован поверх .agg:

# pandas/core/generic.py
@Appender(_shared_docs["transform"] % dict(axis="", **_shared_doc_kwargs))
def transform(self, func, *args, **kwargs):
    result = self.agg(func, *args, **kwargs)
    if is_scalar(result) or len(result) != len(self):
        raise ValueError("transforms cannot produce " "aggregated results")

    return result

Тем не менее, transform всегда возвращает DataFrame, который должен иметь ту же длину, что и self, что по сути является входом.

Когда вы выполняете функцию .agg на DataFrame, она отлично работает:

df.groupby('label')['wave'].agg(list)
label
a       [1]
b    [2, 3]
c       [4]
Name: wave, dtype: object

Проблема возникает, когда transform пытается вернуть Series такой же длины.

В процессе преобразования элемента groupby, который представляет собой фрагмент из self, и последующей его конкатенации, списки распаковываются с индексом той же длины, что и упомянутый @Allen.

Однако, когда они не выровнены, не распаковываются:

df.groupby(['label'])[['wave']].transform(lambda x: list(x) + [1])
    wave
0   [1, 1]
1   [2, 3, 1]
2   [2, 3, 1]
3   [4, 1]

Обходного решения этой проблемы можно избежать transform:

df = pd.DataFrame(data={'label': ['a', 'b', 'b', 'c'], 'wave': [1, 2, 3, 4], 'y': [0,0,0,0]})
df = df.merge(df.groupby('label')['wave'].agg(list).rename('new'), on='label')
df
    label   wave    y   new
0   a         1     0   [1]
1   b         2     0   [2, 3]
2   b         3     0   [2, 3]
3   c         4     0   [4]