Выборка столбцов и строк: [], 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, а не через цепочку [][].
