2025, Nov 01 16:17

Условное заполнение вперед в Pandas: ffill только от строк с colA=3

Как сделать условный forward-fill в Pandas: заполняем colC для colA=4, используя последние значения из строк с colA=3, не трогая colB. Пошаговый пример и код.

Условное заполнение значений по строкам — частая задача при очистке данных. Здесь цель проста: для каждой строки, где colA равно 4, заменить colC на последнее значение colC, встречавшееся в строке, где colA равно 3. Столбец colB должен остаться без изменений.

Минимальный пример, показывающий проблему

Данные представлены в виде DataFrame из Pandas с тремя столбцами. Строки, где colA равен 3, дают значения, которые нужно распространять; строки, где colA равен 4, должны получить это распространённое значение в colC.

from pandas import DataFrame as Frame

source_df = Frame({
    'colA': [3, 4, 4, 4, 3, 4, 4, 3, 4],
    'colB': ['air', 'ground', 'ground', 'ground', 'air', 'ground', 'air', 'ground', 'ground'],
    'colC': ['00JTHYU1', '00JTHYU0', '00JTHYU0', '00JTHYU0', '00JTHYU4', '00JTHYU0', '00JTHYU0', '00JTHYU7', '00JTHYU0']
})

print(source_df)

Ожидаемый результат: в строках, где colA равен 4, столбец colC должен принимать последнее значение colC из строки, где colA был 3, при этом colB остаётся точно таким, как был.

Что на самом деле происходит

Обычный ffill для colC не даст верного результата, потому что он будет протягивать значения и из тех строк, которые не должны служить опорой. Нам нужно заполнять вперёд только из строк, где colA равен 3, игнорируя остальные как источники. Значит, colC нужно замаскировать так, чтобы в роли «опорных» остались лишь значения, соответствующие строкам с colA = 3; всё прочее во время заполнения следует считать пропущенным.

Решение

Подход такой: создать маскированный Series, в котором colC сохраняется только там, где colA равен 3, а в остальных местах — NaN; затем выполнить forward-fill по этому Series и записать полученные значения строго в те строки, где colA равно 4.

from pandas import DataFrame as Frame

data_tbl = Frame({
    'colA': [3, 4, 4, 4, 3, 4, 4, 3, 4],
    'colB': ['air', 'ground', 'ground', 'ground', 'air', 'ground', 'air', 'ground', 'ground'],
    'colC': ['00JTHYU1', '00JTHYU0', '00JTHYU0', '00JTHYU0', '00JTHYU4', '00JTHYU0', '00JTHYU0', '00JTHYU7', '00JTHYU0']
})

anchor_vals = data_tbl['colC'].where(data_tbl['colA'] == 3)
spread_vals = anchor_vals.ffill()

data_tbl.loc[data_tbl['colA'] == 4, 'colC'] = spread_vals

print(data_tbl)

В итоге получаем ожидаемый результат: каждый «блок» четвёрок наследует colC от последней встретившейся тройки, а colB остаётся нетронутым.

   colA    colB      colC
0     3     air  00JTHYU1
1     4  ground  00JTHYU1
2     4  ground  00JTHYU1
3     4  ground  00JTHYU1
4     3     air  00JTHYU4
5     4  ground  00JTHYU4
6     4     air  00JTHYU4
7     3  ground  00JTHYU7
8     4  ground  00JTHYU7

Почему это важно

Заполнение вперёд без маски кажется удобным, но незаметно меняет смысл данных, позволяя не опорным строкам влиять на результат. Маскирование делает намерение явным: только строки, где colA равен 3, определяют, что именно следует распространять. Это и есть разница между быстрым костылём и устойчивым, проверяемым преобразованием.

Выводы

Когда требуется протянуть значения вперёд по условию, сначала сформируйте условный источник, выполните для него forward-fill и запишите обратно только там, где это нужно. Такой подход легко читается, понятен в логике и сохраняет столбцы вроде colB нетронутыми, при этом гарантируя, что у строк с colA, равным 4, в colC всегда будет последнее значение, зафиксированное при colA, равном 3.

Статья основана на вопросе на StackOverflow от Connie Xu и ответе от Grismar.