Проблема с использованием 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 можно создавать веб-скраперы для извлечения данных из веб-сайтов.


1
64
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

Решено

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

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

Самая большая проблема заключается в том, что функция, переданная в 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