Pandas DataFrame: создание, чтение CSV, loc и iloc, фильтрация и groupby

Pandas DataFrame: создание, чтение CSV, loc и iloc, фильтрация и groupby Полезное

DataFrame — это основной объект библиотеки pandas: двумерная таблица, у которой есть подписи строк (индекс) и подписи столбцов, а у каждого столбца свой тип данных (dtype). Ниже на одном наборе данных разберем типовой цикл работы: создать или прочитать таблицу, выбрать нужное, отфильтровать, добавить столбцы, посчитать итоги по группам и разобраться с пропусками.

Все примеры проверены 23.09.2026 на Python 3.14.7 и pandas 3.0.6. Вывод скопирован из реального запуска; в pandas 2.x отдельные детали отличаются, это отмечено по месту.

Три объекта pandas, которые легко перепутать

Объект Что это Пример
Series один столбец: значения + индекс df["city"]
DataFrame таблица из нескольких Series с общим индексом df
Index подписи строк или столбцов df.index, df.columns

Трехмерного Panel из старых учебников больше нет: его удалили из pandas в версии 0.25.0 (2019). Для многомерных данных сейчас используют MultiIndex или библиотеку xarray.

Минимальный пример: создать DataFrame

Самый короткий путь — словарь «имя столбца -> список значений». Установка: pip install pandas.

import pandas as pd

orders = pd.DataFrame({
    "city": ["Москва", "Казань", "Москва", "Пермь"],
    "course": ["Python", "SQL", "SQL", "Python"],
    "price": [12000, 9000, 9000, 12000],
    "paid": [True, False, True, True],
})

print(orders)
print(orders.shape)
print(orders.dtypes)
     city  course  price   paid
0  Москва  Python  12000   True
1  Казань     SQL   9000  False
2  Москва     SQL   9000   True
3   Пермь  Python  12000   True
(4, 4)
city        str
course      str
price     int64
paid       bool
dtype: object

Левый столбец 0-3 — индекс, pandas создал его сам. shape — число строк и столбцов. Тип str у текстовых столбцов — поведение pandas 3.0; в pandas 2.x там будет object.

Чтение CSV: read_csv и разделитель

На практике таблицу чаще читают из файла. Для воспроизводимости текст CSV лежит прямо в коде (io.StringIO), в работе вместо него пишут путь: pd.read_csv("orders.csv").

import io
import pandas as pd

csv_text = """date;city;course;price;paid
2026-09-01;Москва;Python;12000;1
2026-09-01;Казань;SQL;9000;0
2026-09-02;Москва;SQL;;1
2026-09-03;Пермь;Python;12000;1
2026-09-03;Казань;Python;12000;1
"""

bad = pd.read_csv(io.StringIO(csv_text))
print(bad.shape)

df = pd.read_csv(io.StringIO(csv_text), sep=";", parse_dates=["date"])
print(df)
print(df.dtypes)
(5, 1)
        date    city  course    price  paid
0 2026-09-01  Москва  Python  12000.0     1
1 2026-09-01  Казань     SQL   9000.0     0
2 2026-09-02  Москва     SQL      NaN     1
3 2026-09-03   Пермь  Python  12000.0     1
4 2026-09-03  Казань  Python  12000.0     1
date      datetime64[us]
city                 str
course               str
price            float64
paid               int64
dtype: object

Первый вызов — типичная ошибка: по умолчанию разделитель запятая, а в файле точка с запятой, поэтому вся строка ушла в один столбец (форма (5, 1)). Исправление — sep=";". Параметр parse_dates превращает строки дат в тип даты, иначе они останутся текстом.

Пустая ячейка price стала NaN, и столбец из целого превратился в float64. Еще полезны encoding ("cp1251" для старых выгрузок), dtype, usecols и decimal=",".

Выбор данных: [], loc и iloc

Дальше работаем с df из примера выше. Квадратные скобки выбирают столбцы, а строки и срезы надежнее брать через loc и iloc.

Способ По чему выбирает Конец среза Пример
df["col"] имя столбца, вернет Series — df["city"]
df[список] список имен, вернет DataFrame — df[cols], где cols = ["city", "price"]
df.loc[строки, столбцы] метки индекса и имена включается df.loc["b":"d", ["city"]]
df.iloc[строки, столбцы] позиции с нуля не включается df.iloc[1:3, [1, 3]]

Разницу видно, если дать строкам буквенные метки:

df.index = ["a", "b", "c", "d", "e"]
print(df.loc["b":"d", ["city", "price"]])
print(df.iloc[1:3, [1, 3]])
     city    price
b  Казань   9000.0
c  Москва      NaN
d   Пермь  12000.0
     city   price
b  Казань  9000.0
c  Москва     NaN

loc["b":"d"] вернул три строки (метка d включена), iloc[1:3] — две, как обычный срез Python. Частая ошибка — df[0] в надежде получить первую строку: скобки ищут столбец с именем 0, и выходит KeyError: 0. Первая строка по позиции — df.iloc[0] (вернет Series).

Дальше вернем числовой индекс: df = df.reset_index(drop=True).

Фильтрация строк по условию

Условие над столбцом дает логическую маску (Series из True/False), и df[маска] оставляет строки, где True. Несколько условий соединяют через & (и), | (или), ~ (не), каждое условие — в скобках.

df = df.reset_index(drop=True)
try:
    df[df["city"] == "Москва" and df["paid"] == 1]
except ValueError as e:
    print("ValueError:", e)

print(df[(df["city"] == "Москва") & (df["paid"] == 1)])
print(df.query("city == 'Казань' and price >= 10000"))
ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().
        date    city  course    price  paid
0 2026-09-01  Москва  Python  12000.0     1
2 2026-09-02  Москва     SQL      NaN     1
        date    city  course    price  paid
4 2026-09-03  Казань  Python  12000.0     1

Питоновские and/or не работают с масками: они требуют одно значение True или False, а маска содержит много. Внутри строки query() слово and допустимо — там свой синтаксис. Не пробуйте совет a.bool() из текста ошибки: метод Series.bool() удален в pandas 3.0, а сообщение осталось прежним; для маски нужны &/|, для одного значения — .item(), для проверки «хоть одно/все» — .any()/.all(). Для проверки «значение из списка» есть df["course"].isin(["SQL", "Python"]).

Добавление, изменение и удаление столбцов

Способ Что делает Меняет исходный df
df["new"] = ... добавляет столбец в конец да
df.insert(pos, "new", ...) вставляет на позицию да
df.assign(new=...) возвращает новую таблицу нет
df.drop(columns=[...]) возвращает таблицу без столбцов нет, если не присвоить
df["price_k"] = df["price"] / 1000
df.insert(1, "weekday", df["date"].dt.day_name())
df2 = df.assign(vat=lambda d: d["price"] * 0.2)
print(list(df.columns))
print(list(df2.columns))

df = df.drop(columns=["price_k", "weekday"])
print(list(df.columns))
['date', 'weekday', 'city', 'course', 'price', 'paid', 'price_k']
['date', 'weekday', 'city', 'course', 'price', 'paid', 'price_k', 'vat']
['date', 'city', 'course', 'price', 'paid']

Столбец vat появился только в df2: assign исходную таблицу не трогает. Вместо inplace=True у drop проще и нагляднее присваивать результат, как в примере.

Изменять значения по условию нужно одним шагом через loc. Цепочка из двух индексаций в pandas 3.0 исходную таблицу не меняет:

df[df["city"] == "Казань"]["paid"] = 1
print(df["paid"].tolist())

df.loc[df["city"] == "Казань", "paid"] = 1
print(df["paid"].tolist())
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment.
[1, 0, 1, 1, 1]
[1, 1, 1, 1, 1]

Это предупреждение (показана первая строка из нескольких; при запуске скрипта перед ней стоят имя файла и номер строки, и выводится оно в stderr), а не остановка программы, поэтому ошибку легко пропустить: оплата у Казани осталась 0. Причина — режим Copy-on-Write, в pandas 3.0 он включен всегда. В pandas 2.x этот же код давал SettingWithCopyWarning и тоже не менял таблицу, а другие варианты цепочки иногда срабатывали, поэтому полагаться на них нельзя было и раньше.

groupby: итоги по группам

groupby делит таблицу на группы по значениям столбца и считает агрегаты по каждой. Именованная агрегация сразу дает понятные названия итоговых столбцов.

res = (
    df.groupby("course", as_index=False)
      .agg(orders=("city", "size"), revenue=("price", "sum"), avg_price=("price", "mean"))
)
print(res)
print(df.groupby(["course", "city"])["price"].sum())
   course  orders  revenue  avg_price
0  Python       3  36000.0    12000.0
1     SQL       2   9000.0     9000.0
course  city  
Python  Казань    12000.0
        Москва    12000.0
        Пермь     12000.0
SQL     Казань     9000.0
        Москва        0.0
Name: price, dtype: float64

Здесь две ловушки. size считает строки (у SQL их 2), а count посчитал бы только непустые цены (1). И сумма группы, где все значения пустые, равна 0.0 (пара SQL/Москва), а не «нет данных». Если нужно отличать ноль от отсутствия цены, используйте .sum(min_count=1).

Пропуски: найти, удалить, заполнить

print(df.isna().sum())
print(df["price"].mean(), df["price"].count())
print(df.dropna(subset=["price"]).shape)
print(df.fillna({"price": df["price"].median()})["price"].tolist())
print(df["price"].astype("Int64").tolist())
date      0
city      0
course    0
price     1
paid      0
dtype: int64
11250.0 4
(4, 5)
[12000.0, 9000.0, 12000.0, 12000.0, 12000.0]
[12000, 9000, <NA>, 12000, 12000]

isna().sum() показывает число пропусков по столбцам. Среднее 11250.0 посчитано по четырем ценам: статистики pandas по умолчанию пропуски пропускают. Удалить строку (dropna) или заполнить медианой (fillna) — решение по смыслу данных, а не по умолчанию: медиана вместо неизвестной цены исказит выручку. Тип Int64 с большой буквы хранит целые вместе с пропуском <NA>.

Если не получилось: симптом -> причина

Симптом Вероятная причина Что сделать
после read_csv один столбец другой разделитель sep=";" или sep="\t"
UnicodeDecodeError при чтении файл не в UTF-8 encoding="cp1251"
KeyError при выборе опечатка или пробел в имени столбца print(df.columns.tolist())
целые стали float64 в столбце есть пропуск astype("Int64")
The truth value of a Series is ambiguous and/or вместо операторов масок & вместо and, вертикальная черта вместо or, условия в скобках
изменение не сохранилось цепочка df[...][...] = ... df.loc[маска, "столбец"] = ...

Выводы

  • DataFrame — таблица из столбцов-Series с общим индексом; Panel давно удален, для многомерных данных — MultiIndex или xarray.
  • read_csv почти всегда требует явных sep, encoding и parse_dates под конкретный файл.
  • loc работает с метками и включает конец среза, iloc — с позициями и не включает.
  • Фильтры соединяют &, |, ~ со скобками, а менять значения по условию нужно через df.loc[маска, столбец]: в pandas 3.0 цепочки присваивания не работают.
  • В groupby и статистиках пропуски пропускаются по умолчанию; size и count считают разное.

Где применяется / связь с практикой

DataFrame — рабочий инструмент аналитика и data scientist: выгрузки из CRM и баз, отчеты по продажам, подготовка признаков для моделей. Цикл «прочитать -> очистить -> отфильтровать -> сгруппировать» из этой статьи повторяется почти в каждой задаче, меняются только данные и вопросы к ним.

Освойте тему на практике

Системно пройти этот путь на реальных датасетах, вместе с визуализацией и SQL, можно на курсе «Python для анализа данных». Попробовать формат и задать вопросы практикам удобно на бесплатных открытых уроках Otus.

FAQ

Чем DataFrame отличается от таблицы Excel?
Excel хранит данные вместе с формулами и оформлением, а DataFrame — только данные в памяти Python, и все шаги обработки записаны кодом. Поэтому обработку легко повторить на новой выгрузке; прочитать сам файл Excel можно через pd.read_excel (нужен пакет openpyxl).

Как сохранить DataFrame обратно в CSV?
Методом df.to_csv("result.csv", index=False): без index=False в файл попадет лишний столбец с индексом. Разделитель и кодировку задают параметрами sep и encoding, как при чтении.

Что делать, если данные не помещаются в память?
Читать файл частями через pd.read_csv(..., chunksize=100_000) и агрегировать по кускам, загружать только нужные столбцы (usecols) или переходить на форматы и инструменты для больших данных вроде Parquet и DuckDB.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)