Проблема с использованием groupby и преобразования с условной лямбда-выражением для нескольких столбцов в Pandas

Мне интересно узнать о странном поведении, которое я наблюдал при использовании Pandas.

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

Для этого я попробовал использовать groupby и transform с лямбдой, содержащей условный оператор для x.isna().mean(). По большей части это работает, но дает странные результаты при выполнении некоторых определенных условий.

Вот воспроизводимый пример с игрушечным фреймом данных; порог установлен на 60% отсутствующих значений:

import pandas as pd
import numpy as np

df = pd.DataFrame(
    {
    "A" : [np.nan, 4.7, 6.6, np.nan, np.nan, 5.4, 6., 5.3],
    "B" : [np.nan, np.nan, 7.2, 15., np.nan, 5.5, np.nan, np.nan],
    "C" : ["D", "D", "D", "E", "E", "F", "F", "F"]
    }
 )

df.groupby("C").transform(lambda x : x if x.isna().mean() < .6 else np.nan)

Исходные данные:

     A     B  C
0  NaN   NaN  D
1  4.7   NaN  D
2  6.6   7.2  D
3  NaN  15.0  E
4  NaN   NaN  E
5  5.4   5.5  F
6  6.0   NaN  F
7  5.3   NaN  F

Чего я ожидаю:

     A     B
0  NaN   NaN
1  4.7   NaN
2  6.6   NaN
3  NaN  15.0
4  NaN   NaN
5  5.4   NaN
6  6.0   NaN
7  5.3   NaN

Что я получаю:

     A                                       B
0  NaN                                     NaN
1  4.7                                     NaN
2  6.6                                     NaN
3  NaN    3 15.0 4 NaN Name: B, dtype: float64
4  NaN    3 15.0 4 NaN Name: B, dtype: float64
5  5.4                                     NaN
6  6.0                                     Nan
7  5.3                                     NaN

Моя проблема заключается в строках 3 и 4, где вместо атомарных значений возвращается вся серия.

После нескольких тестов выяснилось, что это происходит при выполнении двух условий:

  1. сгруппированным значениям в первом столбце присваивается значение NaN;
  2. значения во втором столбце должны оставаться прежними.

Если я переключусь на x.notna() в своем условии, возникнет та же проблема, когда столбец A содержит только допустимые значения, а ошибка всегда появляется в следующих столбцах.

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

Спасибо за вашу помощь и извините, если мой английский немного неуклюж :)

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

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
0
64
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Потому что, работая с Series, вы можете создать маску и установить недостающие значения в DataFrame.where с фильтрацией только необходимых столбцов:

m = df.groupby("C").transform(lambda x : x.isna().mean()) < .6

#alternative
m = df.isna().groupby(df["C"]).transform('mean') < .6

df[m.columns] = df[m.columns].where(m)
print (df)
     A     B  C
0  NaN   NaN  D
1  4.7   NaN  D
2  6.6   NaN  D
3  NaN  15.0  E
4  NaN   NaN  E
5  5.4   NaN  F
6  6.0   NaN  F
7  5.3   NaN  F

Простое и элегантное альтернативное решение моей проблемы, большое спасибо.

TimDdckr 10.07.2024 16:47
Ответ принят как подходящий

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

g = df.groupby('C')
for col in ['A', 'B']:
    print(col)
    print(g[col].transform(lambda x : x if x.isna().mean() < .6 else np.nan))

A
0    NaN
1    4.7
2    6.6
3    NaN
4    NaN
5    5.4
6    6.0
7    5.3
Name: A, dtype: float64
B
0     NaN
1     NaN
2     NaN
3    15.0
4     NaN
5     NaN
6     NaN
7     NaN
Name: B, dtype: float64

Вам следует сообщить об этом

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

df.groupby('C').transform(lambda x : x if x.isna().mean() < .6
                          else [np.nan]*len(x))

Выход:

     A     B
0  NaN   NaN
1  4.7   NaN
2  6.6   NaN
3  NaN  15.0
4  NaN   NaN
5  5.4   NaN
6  6.0   NaN
7  5.3   NaN

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

TimDdckr 10.07.2024 16:43

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

mozway 10.07.2024 16:46

Самая большая проблема заключается в том, что функция, переданная в DataFrameGroupBy.transform, должна получить в качестве аргумента DataFrame, содержащий группу, и вернуть преобразованный DataFrame с тем же индексом. Это отличается от метода DataFrame.transform. Для получения дополнительной информации ознакомьтесь с их документацией.

Вторая проблема заключается в том, что, поскольку DataFrameGroupBy.transform на самом деле работает с DataFrames, а не с объектами pd.Series, то x.isna().mean() фактически вычисляет процент NAN во всей группе по всем столбцам. См. DataFrame.mean для получения дополнительной информации.

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

df = pd.DataFrame(
    {
        "A": [np.nan, 4.7, 6.6, np.nan, np.nan, 5.4, 6.0, 5.3],
        "B": [np.nan, np.nan, 7.2, 15.0, np.nan, 5.5, np.nan, np.nan],
        "C": ["D", "D", "D", "E", "E", "F", "F", "F"],
    }
)

def groupby_transform(sub_df: pd.DataFrame) -> pd.DataFrame:
    def process_column(series: pd.Series) -> pd.Series:
        percent_nan = series.isna().mean()
        if percent_nan < 0.6:
            return series
        else:
            return pd.Series(np.nan, index=series.index, dtype=series.dtype)

    return sub_df.transform(process_column, axis=0)

df.groupby("C").transform(groupby_transform)

Это выводит

     A     B
0  NaN   NaN
1  4.7   NaN
2  6.6   NaN
3  NaN  15.0
4  NaN   NaN
5  5.4   NaN
6  6.0   NaN
7  5.3   NaN

Частично это правда, df.groupby('C')['B'].transform(lambda x : x if x.isna().mean() < .6 else np.nan) дает ожидаемый результат OP. Несогласованность/ошибка заключается в том, что несколько столбцов обрабатываются вместе. Смотрите мой ответ для демонстрации.

mozway 10.07.2024 15:49

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

Grinjero 10.07.2024 16:06

«DataFrameGroupBy.transform действительно работает с DataFrames» -> это тоже неверно. DataFrameGroupBy.transform определенно работает для каждого столбца независимо (именно поэтому сообщаемое поведение содержит ошибки). DataFrameGroupBy.apply будет работать со всем DataFrame.

mozway 10.07.2024 16:27

Спасибо за ваш ответ, я скоро проверю ваше рекомендованное решение. Мне некуда сказать, кто из вас прав, но что касается второй проблемы, мне кажется, что df.groupby("C").transform(lambda x : x.isna().mean()) возвращает процент NA для каждой группы и каждого столбца, а не для всех столбцов.

TimDdckr 10.07.2024 16:38

@TimDdckr предлагаемое решение: этот ответ правильный (на самом деле он похож на то, что я предложил в своем), но есть много неверных утверждений о причинах и поведении groupby.

mozway 10.07.2024 16:42

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

Grinjero 11.07.2024 10:22

@Grinjero Думаю, вы пропустили фразу «А пока вы можете избежать этого, установив правильное количество NaN в качестве вывода:» с соответствующим кодом из моего ответа. Он действительно обеспечивает функциональное решение, позволяющее избежать возникновения ошибки… и фактически основан на том же принципе, который вы предложили в своем (использование списка вместо серии) после того, как я ответил.

mozway 11.07.2024 10:25

Честно, кажется, пропустил это

Grinjero 11.07.2024 11:19

Другие вопросы по теме