2025, Nov 12 09:03
Почему pandas Series.case_when «игнорирует» условия: выравнивание индексов и позиционная логика
Как работает pandas Series.case_when: выравнивание индексов, mask и позиционная логика. Советы, примеры и решение через .values для избежания неожиданных замен.
Когда кажется, что Series.case_when в pandas «игнорирует» ваши булевы условия, почти всегда дело в выравнивании индексов. Если передаваемая вами Series с условиями не имеет тех же меток индекса, что и Series, которую вы преобразуете, pandas сначала выравнивает их, а уже затем применяет маску. Результат может удивить, если вы ожидаете позиционного поведения.
Как воспроизвести поведение
Ниже — точный шаблон, который сбивает с толку многих: две Series с разными метками индекса и несколько вызовов case_when, которые на первый взгляд будто бы противоречат булевой логике.
import pandas as pd
print(pd.__version__)
# 2.3.0
s_main = pd.Series([1, 2, 3, 4, 5], index=['a', 'b', 'c', 'd', 'e'], dtype='int')
s_alt = pd.Series([1, 2, 3, 4, 5], index=['A', 'B', 'C', 'D', 'E'], dtype='int')
out1 = s_main.case_when([
(s_main.gt(3), 'greater than 3'),
(s_main.lt(3), 'less than 3')
])
print(out1)
# a меньше 3
# b меньше 3
# c 3
# d больше 3
# e больше 3
out2 = s_main.case_when([
(s_main.gt(3), 'greater than 3'),
(s_alt.lt(3), 'less than 3')
])
print(out2)
# a меньше 3
# b меньше 3
# c меньше 3 <- почему это не 3?
# d больше 3
# e больше 3
out3 = s_main.case_when([
(s_alt.gt(3), 'greater than 3'),
(s_alt.lt(3), 'less than 3')
])
print(out3)
# a больше 3 <- почему это не меньше 3?
# b больше 3 <- почему это не меньше 3?
# c больше 3 <- почему это не 3?
# d больше 3
# e больше 3
out4 = s_main.case_when([
(s_alt.gt(3).to_list(), 'greater than 3'),
(s_alt.lt(3).to_list(), 'less than 3')
])
print(out4)
# a меньше 3
# b меньше 3
# c 3
# d больше 3
# e больше 3
Что на самом деле происходит: сначала выравнивание, потом маскирование
Pandas выравнивает по меткам. Прежде чем применять условную маску, pandas сопоставляет условие с целевой Series по меткам индекса. Это мощная возможность для «грязных» данных, но она меняет то, как булевы массивы взаимодействуют с данными, если их индексы не совпадают.
Короткое напоминание о выравнивании. Две Series с одинаковыми метками, но в другом порядке, при арифметике сопоставляются по меткам, а не по позициям:
import pandas as pd
s_x = pd.Series([1, 2, 3], index=['a', 'b', 'c'], dtype='int')
s_y = pd.Series([3, 2, 1], index=['c', 'b', 'a'], dtype='int')
print(s_x + s_y)
# a 2
# b 4
# c 6
# dtype: int64
Если индексы не совпадают, выравнивание вводит отсутствующие позиции. Это можно явно увидеть через align:
import pandas as pd
s_left = pd.Series([1, 2, 3], index=['a', 'b', 'c'], dtype='int')
s_right = pd.Series([1], index=['a'], dtype='int')
print(s_right.align(s_left)[0])
# a 1.0
# b NaN
# c NaN
# dtype: float64
Теперь свяжем это с case_when. Series.case_when реализована через Series.mask. Документация по маскированию объясняет, как обрабатывается несовпадение:
Метод mask — это применение идиомы if-then. Для каждого элемента вызывающего DataFrame, если cond равен False, используется исходный элемент; иначе берётся соответствующий элемент из DataFrame other. Если ось other не выравнивается с осью cond Series/DataFrame, несовпадающие позиции индекса будут заполнены значениями True.
В case_when каждое условие выравнивается с целевой Series. Там, где в индексе условия нет соответствующей метки, mask рассматривает такие позиции как подлежащие замене. Поскольку case_when последовательно «нанизывает» вызовы mask поверх текущих «значений по умолчанию», любая строка, отсутствующая в индексе условия, фактически считается совпадающей с этим условием и заменяется на указанное значение.
Внутренний вызов выглядит так, где default — формирующаяся результирующая Series:
default = default.mask(
condition, other=replacement, axis=0, inplace=False, level=None
)
Отсюда и кажущиеся странности. Когда вы передаёте условия, построенные из s_alt с метками A, B, C, D, E, они не выравниваются с a, b, c, d, e. Несовпадающие элементы считаются заменяемыми, поэтому подстановка срабатывает, даже если булевы значения, вычисленные на s_alt, True или False в их собственной индексной области. Преобразовав условия в обычные массивы или списки, вы исключаете выравнивание по меткам — и маски применяются позиционно.
Есть ещё один важный нюанс. case_when выполняет замены векторизованно, проходя условия в обратном порядке, так что более ранние замены «закрепляются» последними. Это даёт тот же эффект, что и «первое совпадение побеждает» в случае одиночной замены, при этом остаётся векторизованным.
Как добиться позиционного поведения
Если вы хотите, чтобы case_when игнорировала индексы и работала по позициям, передавайте условия как массивы NumPy. Самый простой путь — использовать .values у булевых условий: так вы избегаете шага выравнивания. .to_list даёт тот же эффект, но дороже, чем .values.
fixed = s_main.case_when([
(s_alt.gt(3).values, 'greater than 3'),
(s_alt.lt(3).values, 'less than 3')
])
print(fixed)
# a меньше 3
# b меньше 3
# c 3
# d больше 3
# e больше 3
Если условия получены из самой целевой Series, этого делать не нужно — индексы уже выровнены. Предостережение актуально, когда условия приходят из Series с иным индексом.
Почему это важно
Смешение логики по меткам и по позициям в одном выражении может незаметно перевернуть результат. В ETL-конвейерах, при построении признаков или быстрых правках данных, передача невыравненной булевой Series в case_when приведёт к замене значений, которых вы не хотели касаться. Понимание того, что pandas в первую очередь выравнивает по меткам, помогает избежать тонких ошибок «продакшен-класса».
Для дополнительного контекста есть связанное обсуждение: github.com/pandas-dev/pandas/issues/61781. Приведённая выше документация к Series.mask — ключ к расшифровке наблюдаемого поведения.
Итоги
Думайте в терминах меток, когда передаёте одну Series в операции над другой Series. Нужна позиционная семантика в case_when — конвертируйте условия в массивы через .values. Если действительно требуется выравнивание по меткам, оставляйте условия Series с совпадающими индексами. И помните: case_when собирает результат цепочкой вызовов mask, из-за чего невыравненные условия фактически трактуются как совпадения.