Все материалы
Продуктовая аналитикагайдстарт

Pandas с нуля: DataFrame, Series и чтение CSV

Первый практический урок по pandas: как читать CSV, понимать DataFrame и Series, фильтровать строки, выбирать столбцы и проверять пропуски.

КейсПрактика29 июля 2026 г.обновлено 15 сентября 2026 г.10 мин

Pandas нужен не для того, чтобы заменить SQL любой ценой. Он удобен, когда нужно быстро взять файл или результат запроса, проверить структуру, почистить данные и собрать небольшой воспроизводимый анализ рядом с графиком. Начнём с главной модели: DataFrame — таблица, Series — один её столбец.

Коротко

Хороший первый сценарий в pandas выглядит так: прочитать данные, проверить форму и типы, выбрать нужные строки и столбцы, обработать пропуски, получить сводку и сохранить результат. Каждое действие лучше делать явно, чтобы через неделю можно было восстановить, откуда взялась цифра.

  • DataFrame хранит строки и столбцы, Series — одну именованную колонку.
  • head, shape, info и describe — обязательная проверка перед расчётами.
  • Фильтр должен отвечать на вопрос аналитика, а не просто оставлять «интересные» строки.
  • Пропуск, пустая строка и ноль — разные состояния.

DataFrame и Series на одной картинке

Возьмём маленькую таблицу заказов. В ней одна строка — один заказ, user_id — покупатель, channel — канал, revenue — сумма заказа. Когда пишем orders["revenue"], получаем Series. Когда выбираем несколько колонок, остаёмся в DataFrame.

Базовые объекты pandas
ОбъектЧто хранитПример
DataFrameтаблицу со строками и колонкамиorders
Seriesодну колонку с индексомorders["revenue"]
Indexметки строкorders.index
dtypeтип данных колонкиorders["revenue"].dtype
pythonСоздать небольшой DataFrame
import pandas as pd

orders = pd.DataFrame({
    'order_id': [101, 102, 103, 104],
    'user_id': [1, 2, 1, 3],
    'channel': ['organic', 'paid', 'organic', 'email'],
    'revenue': [1200, 800, 450, 1500],
})

revenue = orders['revenue']       # Series
order_slice = orders[['channel', 'revenue']]  # DataFrame

Прочитать CSV и сразу проверить данные

read_csv загружает файл, но не сообщает, правильно ли вы поняли его структуру. Поэтому чтение всегда заканчивайте коротким профилем: количество строк и колонок, типы, пример строк, пропуски и диапазон дат. Это занимает минуту и ловит ошибки разделителя, кодировки и типов.

pythonЗагрузка и первичная проверка файла
orders = pd.read_csv(
    'data/orders.csv',
    parse_dates=['created_at'],
)

print(orders.shape)
print(orders.dtypes)
display(orders.head())
display(orders.isna().mean().sort_values(ascending=False).head())
Не доверяй расширению файла

CSV может быть разделён запятой, точкой с запятой или табуляцией. Если таблица выглядит как один широкий столбец, сначала проверьте sep, encoding и кавычки, а не начинайте чистить строки вручную.

Выбрать строки и колонки

Для понятного анализа разделяйте выборку на два шага: какие строки относятся к вопросу и какие поля нужны для ответа. loc работает с условиями и именами колонок, query иногда делает длинный фильтр читабельнее. Не меняйте исходный DataFrame без явного присваивания или copy() — так меньше неожиданных предупреждений.

pythonФильтр оплаченных заказов за период
paid_orders = orders.loc[
    (orders['status'] == 'paid')
    & (orders['created_at'] >= '2026-07-01')
    & (orders['created_at'] < '2026-08-01'),
    ['order_id', 'user_id', 'channel', 'revenue'],
].copy()

paid_orders = paid_orders.sort_values('revenue', ascending=False)
  • Для нескольких условий используйте круглые скобки вокруг каждого сравнения.
  • В pandas нужен & для «и» и | для «или», а не обычные and и or.
  • Правая граница периода < next_start не включает следующий месяц.

Пропуски: сначала смысл, потом fillna

Пропуск в channel может означать неизвестный источник, а пропуск в discount — отсутствие скидки. В первом случае нужна явная категория unknown, во втором ноль может быть корректен. Универсальная замена всех пропусков нулём делает таблицу аккуратной только на вид.

pythonЯвно обработать разные типы пропусков
orders['channel'] = orders['channel'].fillna('unknown')
orders['discount'] = orders['discount'].fillna(0)

missing_revenue = orders['revenue'].isna().sum()
if missing_revenue > 0:
    raise ValueError('Revenue is missing: check source data')

Мини-профиль перед первой метрикой

До группировки проверьте, что дата действительно дата, сумма — число, а идентификатор заказа уникален там, где это ожидается. Если одна строка — позиция, нельзя считать количество строк количеством заказов. Python не знает смысл бизнес-полей без вашей проверки.

  • orders.shape — размер таблицы.
  • orders.nunique() — количество уникальных значений по колонкам.
  • orders.duplicated("order_id").sum() — контроль повторных заказов.
  • orders["created_at"].min() и .max() — фактическое окно дат.
  • orders.describe(include="all") — быстрый обзор распределений.

Индексы, копии и предупреждения pandas

Индекс pandas — это не обязательно идентификатор строки из базы. Он помогает выровнять Series и DataFrame по меткам, но после фильтра может сохранять старые номера. Для экспорта или сравнения иногда нужен reset_index(drop=True), а для присоединения по бизнес-ключу — явный merge. Не полагайтесь на индекс как на order_id, если вы не создавали такой контракт.

После фильтра используйте .copy(), когда собираете независимый слой. Это делает намерение явным и уменьшает риск SettingWithCopyWarning. Предупреждение нельзя просто отключить: сначала решите, хотите ли изменить исходную таблицу или создать новую. Большинство непонятных результатов в учебных ноутбуках начинается именно с неявного изменения среза.

Проверяйте результат присваивания на двух-трёх строках. После нормализации канала выведите исходное и новое значение, после создания даты — тип и границы. Маленькая визуальная проверка быстрее, чем искать ошибку после десяти последующих операций.

pythonЯвно создать независимый срез
paid_orders = orders.loc[orders['status'].eq('paid')].copy()
paid_orders['revenue'] = pd.to_numeric(
    paid_orders['revenue'], errors='coerce'
)

print(paid_orders.index[:3].tolist())
paid_orders = paid_orders.reset_index(drop=True)

loc меняет выбранный объект, а индекс выравнивает значения по меткам

Вернёмся к четырём заказам 101–104. После фильтра revenue >= 1000 останутся строки с исходными метками 0 и 3. Вызов iloc[1] выберет вторую оставшуюся строку, то есть заказ 104. Вызов loc[1] будет искать метку 1, которой в этом срезе нет. Разница особенно заметна после сортировки: позиция строки меняется, а метка сохраняется. Для просмотра по порядку используйте iloc, для изменения по условию или известному ключу — loc.

Присваиваемая Series тоже имеет метки. Если срезу с индексом [0, 3] присвоить pd.Series([10, 20]), pandas совместит метку 0 с 0, а для метки 3 не найдёт значения. Вместо ожидаемых двух скидок появится пропуск. Создайте Series с индексом выбранных заказов или вычислите скидку непосредственно из их revenue. Список и массив присваиваются по позиции: это допустимо только тогда, когда их порядок действительно совпадает с порядком строк.

Индекс по order_id удобен для адресной проверки заказа. Создайте его через set_index("order_id", verify_integrity=True): повторный ключ остановит операцию. Тогда loc[104] обращается к заказу, а не к четвёртой строке. Если таблица хранит несколько позиций заказа, такой индекс уже не уникален и одна метка может выбрать несколько строк. Для соединения этих сущностей понятнее явно указать бизнес-ключи и ожидаемую кратность merge.

Упоминание SettingWithCopyWarning выше относится к прежнему поведению pandas. В pandas 3.0 Copy-on-Write стал единственным режимом: изменение полученного среза не должно менять родительскую таблицу. Цепочка orders[mask]["revenue"] = 0 не является способом обновить orders. Если цель — исходная таблица, запишите orders.loc[mask, "revenue"] = 0 одним присваиванием. Если цель — самостоятельная выборка, работайте с ней явно; copy остаётся понятным указанием намерения и совместимым приёмом для старого кода.

Проверяйте именно тот объект, который должен измениться. В примере скидки добавляются только к large, а суммы orders остаются прежними. Затем одна запись через loc намеренно меняет исходный заказ 104. Проверка обоих состояний помогает различить ошибку выравнивания и ошибку адресата присваивания. Исчезнувшее предупреждение само по себе не доказывает, что нужная таблица получила новые значения.

pythonПозиции, метки и явное изменение заказа
import pandas as pd
orders = pd.DataFrame({'order_id': [101, 102, 103, 104],
                       'revenue': [1200, 800, 450, 1500]})
large = orders.loc[orders['revenue'].ge(1000)].copy()
assert large.index.tolist() == [0, 3]
assert large.iloc[1]['order_id'] == 104
large['discount'] = pd.Series([10, 20], index=large.index)
assert large['discount'].tolist() == [10, 20]
assert orders['revenue'].tolist() == [1200, 800, 450, 1500]
orders.loc[orders['order_id'].eq(104), 'revenue'] = 0
by_order = orders.set_index('order_id', verify_integrity=True)
assert by_order.loc[104, 'revenue'] == 0

Фильтр должен быть переводом вопроса в код

Перед loc сформулируйте выборку словами: «оплаченные заказы в полном июле, кроме тестовых аккаунтов». Затем превратите каждую часть в отдельное условие. Такой код легче проверить и изменить, чем одна строка с несколькими вложенными скобками. Имена is_paid, is_in_period, is_real_user показывают бизнес-логику прямо в ноутбуке.

Границы периода задавай полуинтервалом: больше или равно началу и меньше следующего начала. Это работает для timestamp и не зависит от того, сколько секунд в последнем дне месяца. Если написать <= 31 июля 23:59:59, события с миллисекундами можно потерять.

После фильтра сравните размер базы и долю каждой причины исключения. Если добавление условия уменьшило таблицу в десять раз, это может быть реальный сигнал, а может быть неправильный статус или часовой пояс. Аналитик должен увидеть изменение до первой агрегации.

Диагностика фильтра
ПроверкаЧто она показывает
len(orders)размер исходной базы
is_paid.mean()доля оплаченных строк
is_in_period.mean()доля строк в окне дат
is_real_user.mean()доля после исключения тестовых аккаунтов
len(filtered)итоговый размер выборки

Чтение CSV: кодировка и разделитель — часть контракта

Если CSV открылся одной широкой колонкой, проблема обычно в sep, а не в pandas. В русскоязычных выгрузках встречается точка с запятой, десятичная запятая и разные кодировки. Перед расчётами проверьте несколько сырых строк и явно укажите параметры чтения. Не исправляйте разделитель заменой символов внутри строк: так можно повредить текстовые поля.

Денежное поле иногда приходит как "1 234,50". Сначала зафиксируйте локаль и очистите пробелы, затем приведите значение к числу. Если в одном файле смешаны форматы, не скрывайте ошибку через errors=coerce: посчитайте число непарсящихся строк и покажите их владельцу источника.

Для повторных загрузок сохраняйте список колонок и типы в одном месте. Тогда новый столбец не сломает код, а исчезнувший обязательный столбец вызовет понятную ошибку до расчёта. Это первый маленький шаг от ноутбука к воспроизводимому pipeline.

pythonПрочитать CSV с явным контрактом
orders = pd.read_csv(
    'data/orders.csv',
    sep=';',
    encoding='utf-8-sig',
    usecols=['order_id', 'status', 'created_at', 'revenue'],
)
orders['revenue'] = (
    orders['revenue'].astype('string')
    .str.replace(' ', '', regex=False)
    .str.replace(',', '.', regex=False)
    .pipe(pd.to_numeric, errors='coerce')
)

dtype сохраняет ведущие нули до первой очистки

У заказов из учебной таблицы короткие числовые ключи, но выгрузка может прислать order_id как 00101. Если read_csv распознает это поле числом, получится 101, а последующее astype("string") не вернёт потерянные нули. Задайте строковый dtype прямо при чтении. Это относится и к user_id, если источник определяет его как код, а не величину для арифметики. Одинаково выглядящие ключи должны иметь согласованный тип ещё до первого merge.

В примере ниже разделитель — точка с запятой, дробная часть отделена запятой, а тысячи — обычным пробелом. Параметры decimal и thousands позволяют распознать 1 234,50 числом без ручной замены символов. Это работает для заранее известного формата. Неразрывный пробел или смесь разных локалей потребуют другого правила и отдельного контроля ошибок; нельзя считать любой пробел равнозначным только потому, что строки визуально похожи.

Строковое поле тоже может неожиданно стать пропуском. По умолчанию read_csv распознаёт ряд текстовых маркеров отсутствия, включая NA. Если NA — настоящий код канала, настрой keep_default_na=False и укажите допустимые маркеры через na_values, при необходимости отдельно по колонкам. В учебном CSV пустая строка считается пропуском, а NA сохраняется как текст. Это осознанный контракт конкретного источника, а не настройка для всех файлов компании.

parse_dates просит reader разобрать дату, но после чтения всё равно проверьте dtype. Нераспознанные значения могут оставить колонку строковой. Для регулярного формата удобно указать date_format, а затем проверить, что все обязательные даты присутствуют. В примере календарная дата не содержит часового пояса: не объявляйте её UTC без договорённости об источнике. Если нужны моменты операций, формат timestamp и зона становятся отдельной частью контракта.

Кодировка решает, как байты превращаются в символы, и не определяет ни тип ключа, ни локаль суммы. utf-8-sig полезна для UTF-8 с BOM, но не исправляет файл в Windows-1251. При неверных буквах выясни кодировку у отправителя; режим игнорирования ошибок способен удалить часть идентификатора. После чтения сверяйте контрольный заказ по ключу, его сумму и дату с исходной строкой. Такой пример проверяет смысл параметров лучше, чем отсутствие исключения при загрузке.

pythonПолностью воспроизводимый CSV с ключом 00101 и каналом NA
from io import StringIO
import pandas as pd
raw = StringIO('order_id;channel;created_at;revenue\n00101;NA;2026-07-01;1 234,50\n')
orders = pd.read_csv(
    raw, sep=';', dtype={'order_id': 'string', 'channel': 'string'},
    parse_dates=['created_at'], date_format='%Y-%m-%d',
    thousands=' ', decimal=',', keep_default_na=False, na_values=[''],
)
assert orders.loc[0, 'order_id'] == '00101'
assert orders.loc[0, 'channel'] == 'NA'
assert orders.loc[0, 'revenue'] == 1234.5
assert pd.api.types.is_datetime64_any_dtype(orders['created_at'])

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

Возьмите CSV с заказами и пройдите короткий цикл: осмотрите первые строки, проверьте форму и типы, опишите зерно, выберите период, обработайте пропуски и посчитайте три контрольных показателя. Для старта подойдут количество заказов, уникальные покупатели и общая выручка. Сохраните их до и после очистки, чтобы видеть цену каждого решения.

Затем измените один параметр намеренно: подставьте другой месяц или уберите фильтр статуса. Хороший ноутбук должен показать предсказуемое изменение, а не требовать ручной перестановки ячеек. Если результат меняется неожиданно, вернитесь к индексу, типам и границам дат.

После этого переходите к groupby и merge. У вас уже будет таблица с понятным контрактом, а не случайный набор строк. Так основы pandas связываются с реальной работой аналитика: сначала понять данные, потом считать.

  • Сначала проверить разделитель, кодировку и заголовок.
  • Записать, что означает одна строка.
  • Проверить типы, пропуски, дубли и даты.
  • Собрать фильтр из именованных условий.
  • Сохранить контрольные числа до первой метрики.
Продолжить чтение
Вся библиотека