Мне интересно узнать о странном поведении, которое я наблюдал при использовании 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, где вместо атомарных значений возвращается вся серия.
После нескольких тестов выяснилось, что это происходит при выполнении двух условий:
Если я переключусь на x.notna()
в своем условии, возникнет та же проблема, когда столбец A содержит только допустимые значения, а ошибка всегда появляется в следующих столбцах.
Я понимаю, что есть и другие способы получить желаемый результат в Pandas, поэтому не стесняйтесь вносить предложения, но мне бы очень хотелось понять, что здесь происходит: ошибочен ли мой код в чем-то, можно ли его легко исправить или это что-то вроде ошибки?
Спасибо за вашу помощь и извините, если мой английский немного неуклюж :)
Обновлено: я исправил несоответствие между данными игрушки, указанными в коде, и исходными данными.
Потому что, работая с 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
Большое спасибо, ваш тест отдельных столбцов, кажется, подтверждает мои подозрения, я рассмотрю возможность сообщить о потенциальной ошибке. Я также ценю ваше решение: группа transform
должна передавать скалярные значения, но, возможно, моя проблема связана с разной длиной потенциальных выходных данных моей лямбда-функции.
Действительно, того факта, что он ведет себя по-разному в нескольких столбцах и в отдельных столбцах, не должно быть, по моему мнению. Дайте мне знать, если сообщите об ошибке.
Самая большая проблема заключается в том, что функция, переданная в 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. Несогласованность/ошибка заключается в том, что несколько столбцов обрабатываются вместе. Смотрите мой ответ для демонстрации.
Судя по тому, что я вижу, DataFrame, инициализированный в коде, и тот, который показан в разделе «Исходные данные», отличаются, поэтому код из моего ответа выполняет то, что было его первоначальной целью.
«DataFrameGroupBy.transform действительно работает с DataFrames» -> это тоже неверно. DataFrameGroupBy.transform
определенно работает для каждого столбца независимо (именно поэтому сообщаемое поведение содержит ошибки). DataFrameGroupBy.apply
будет работать со всем DataFrame.
Спасибо за ваш ответ, я скоро проверю ваше рекомендованное решение. Мне некуда сказать, кто из вас прав, но что касается второй проблемы, мне кажется, что df.groupby("C").transform(lambda x : x.isna().mean())
возвращает процент NA для каждой группы и каждого столбца, а не для всех столбцов.
@TimDdckr предлагаемое решение: этот ответ правильный (на самом деле он похож на то, что я предложил в своем), но есть много неверных утверждений о причинах и поведении groupby
.
Я согласен, что реализация pandas непоследовательна и содержит ошибки, но, насколько я понимаю из документации, она делает то, что говорит. Я согласен, что он должен работать так же, как и при непосредственном вызове преобразования в DataFrame. Я занизил ваш ответ, потому что он не объяснял, как реализовать желаемое поведение, а просто демонстрировал, что происходит, когда вы делаете это в цикле for. Мой ответ на самом деле делает то же самое: фактически преобразует каждую группу отдельно, с дополнительным преимуществом объединения результатов в один DataFrame.
@Grinjero Думаю, вы пропустили фразу «А пока вы можете избежать этого, установив правильное количество NaN в качестве вывода:» с соответствующим кодом из моего ответа. Он действительно обеспечивает функциональное решение, позволяющее избежать возникновения ошибки… и фактически основан на том же принципе, который вы предложили в своем (использование списка вместо серии) после того, как я ответил.
Честно, кажется, пропустил это
Простое и элегантное альтернативное решение моей проблемы, большое спасибо.