pandas: устройство, выборка и изменение таблиц

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