DataFrame — это основной объект библиотеки pandas: двумерная таблица, у которой есть подписи строк (индекс) и подписи столбцов, а у каждого столбца свой тип данных (dtype). Ниже на одном наборе данных разберем типовой цикл работы: создать или прочитать таблицу, выбрать нужное, отфильтровать, добавить столбцы, посчитать итоги по группам и разобраться с пропусками.
Содержание
- Три объекта pandas, которые легко перепутать
- Минимальный пример: создать DataFrame
- Чтение CSV: read_csv и разделитель
- Выбор данных: [], loc и iloc
- Фильтрация строк по условию
- Добавление, изменение и удаление столбцов
- groupby: итоги по группам
- Пропуски: найти, удалить, заполнить
- Если не получилось: симптом -> причина
- Выводы
- Где применяется / связь с практикой
- FAQ
Все примеры проверены 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.



