Выборка столбцов и строк: [], loc и iloc без цепочной индексации
Содержание курса
Цепочное присваивание: почему df[col][mask] = value ненадёжно и как его заменить
Умение читать данные через loc и iloc — это одно. Записывать через них — немного другое, и здесь есть конкретная ловушка, которую легко не заметить.
Предположим, нужно изменить значение city для строк с высоким баллом. Интуитивный, но ненадёжный способ:
mask = df['score'] >= 90
df['city'][mask] = 'Новосибирск' # так делать не надо
Выражение df['city'][mask] = 'Новосибирск' — это два последовательных индексирования. Сначала df['city'] возвращает объект — Series или его представление (view). Потом по этому объекту пытаемся присвоить значение через [mask]. Проблема в том, что pandas не гарантирует, вернул ли первый вызов представление оригинала или копию. Это зависит от внутренней структуры данных, версии pandas и режима работы.
Что происходит в зависимости от режима
В legacy-режиме (pandas 2.x без CoW) поведение непредсказуемо: иногда запись попадает в исходный DataFrame, иногда — только в промежуточный объект, который тут же отбрасывается. pandas предупреждает об этом через SettingWithCopyWarning, но запись формально завершается без исключения — просто данные могут не измениться.
Copy-on-Write (CoW) — режим, который в pandas 2.x включается явно (pd.options.mode.copy_on_write = True) и становится поведением по умолчанию начиная с pandas 3.0. При CoW цепочное присваивание не обновляет исходный DataFrame, а pandas диагностирует это предупреждением или ошибкой класса ChainedAssignmentError — в зависимости от версии и настроек предупреждений. В любом случае рассчитывать на то, что такая запись сработает, нельзя.
Версионно устойчивый способ — один вызов loc
Есть единственный паттерн, который работает одинаково в обоих режимах: присваивать через один вызов loc с двумя аргументами — маска строк и метка столбца.
import pandas as pd
df = pd.DataFrame({
'name': ['Анна', 'Борис', 'Вера', 'Григорий'],
'score': [85, 92, 78, 95],
'city': ['Москва', 'Казань', 'Москва', 'Уфа']
}, index=['a', 'b', 'c', 'd'])
mask = df['score'] >= 90
# Правильно: один вызов loc, строки и столбец в одном выражении
df.loc[mask, 'city'] = 'Новосибирск'
print(df['city'])
# a Москва
# b Новосибирск
# c Москва
# d Новосибирск
# Name: city, dtype: object
df.loc[mask, 'city'] = 'Новосибирск' — это одно индексирование, которое напрямую адресует нужные ячейки в исходном DataFrame. Никакого промежуточного объекта, никакой неопределённости. Результат одинаков в legacy-режиме и при CoW.
Аналогично работает iloc, когда нужно изменить конкретную ячейку по позиции:
df.iloc[0, 2] = 'Санкт-Петербург' # строка 0, столбец 2 (city)
print(df.loc['a', 'city']) # 'Санкт-Петербург'
Что именно запрещать, а что нет
Опасна именно цепочка df[col][mask] = value — условное изменение подмножества строк через два последовательных индексирования. Прямое присваивание целого столбца — df['new_col'] = ... — это другая операция: одно индексирование, которое корректно создаёт или заменяет столбец целиком. Такой способ не является цепочным и безопасен; подробнее он разбирается в следующем уроке.
Булева маска: метки против порядка строк
С масками различие между loc и iloc особенно заметно после перестановки строк. Пусть индекс таблицы — ['a', 'b', 'c'], а маска создана в обратном порядке: метка c помечена как истинная, остальные — как ложные.
import pandas as pd
df = pd.DataFrame({"score": [10, 20, 30]}, index=["a", "b", "c"])
mask = pd.Series([True, False, False], index=["c", "b", "a"])
print(df.loc[mask, "score"].tolist()) # [30]
print(df.iloc[mask.to_numpy(), 0].tolist()) # [10]loc сопоставляет значения Series с метками строк: истинная метка c выбирает 30. У iloc булев массив применяется по позиции: первое True выбирает 10. Если маска получена из того же df, обычно используйте df.loc[df["score"] > 15]. Передавая маску из другой таблицы, сначала проверьте её индекс: неправильное выравнивание меняет смысл фильтра. Для отдельной проверки позиций есть задача про нестандартный индекс.
Правило для условной записи простое: изменение выбранных строк или ячеек — через один вызов loc или iloc, а не через цепочку [][].
