Зачем pandas в карьере Junior DE
Каждый день инженер данных получает табличку. CSV из выгрузки, JSON Lines из лога, ответ SQL-запроса, Parquet из data lake. Затем эту табличку нужно посмотреть глазами, отфильтровать, склеить с другой, посчитать агрегат, выгрузить дальше. Можно делать это голым Python — open, csv.reader, циклы, словари — но на 200 тысячах строк это занимает минуты вместо миллисекунд. И в код заглядывать стыдно: вместо одной строки df.groupby("user_id")["amount"].sum() будет двадцать строк с defaultdict.
groupby уверенно и понимать, где у pandas границы.
Курс не превратит вас в эксперта по pandas — для этого нужны месяцы практики. Но после четырёх уроков вы спокойно прочитаете чужой код с pandas и напишете свой ETL-скрипт, который не упадёт на проде.
DataFrame и Series — что есть что
В pandas две главные структуры.
import pandas as pd
# Series — одномерная штука
prices = pd.Series([100, 250, 99], index=["a", "b", "c"], name="price")
print(prices)
# a 100
# b 250
# c 99
# Name: price, dtype: int64
# DataFrame — таблица из Series
df = pd.DataFrame({
"product": ["apple", "banana", "cherry"],
"price": [100, 250, 99],
"in_stock": [True, False, True],
})
print(df)
# product price in_stock
# 0 apple 100 True
# 1 banana 250 False
# 2 cherry 99 True
У DataFrame две оси:
DataFrame = index + набор Series-колонок. Каждая Series со своим dtype.
Почему это важно. На голом Python таблица — это list of dicts: [{"product": "apple", "price": 100}, ...]. Чтобы посчитать сумму цен, вы пишете цикл. В pandas цена — это df["price"], Series типа int64, и сумма — df["price"].sum(). Под капотом это
Первый просмотр данных
Допустим, вам прислали CSV transactions.csv. Стандартный набор команд для знакомства:
import pandas as pd
df = pd.read_csv("transactions.csv")
df.shape # (число_строк, число_колонок)
df.head() # первые 5 строк (head(10) — первые 10)
df.tail(3) # последние 3 строки
df.columns # имена колонок
df.dtypes # тип каждой колонки
df.info() # размер в памяти, типы, non-null counts
df.describe() # min/max/mean/std/quartiles для числовых колонок
Это первое, что вы делаете с любым датасетом. Не идите в groupby, пока не выяснили: сколько строк, какие типы, есть ли пропуски, разумные ли значения. Тридцать секунд на info() и describe() спасают часы отладки.
df.info()
# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 1000 entries, 0 to 999
# Data columns (total 4 columns):
# # Column Non-Null Count Dtype
# --- ------ -------------- -----
# 0 transaction_id 1000 non-null object
# 1 amount 997 non-null float64
# 2 currency 1000 non-null object
# 3 created_at 1000 non-null object
# dtypes: float64(1), object(3)
# memory usage: 31.4+ KB
Здесь сразу видно две проблемы. Первая: created_at — это object, то есть строка, хотя там даты. Вторая: amount имеет 997 non-null из 1000 — три пропуска. Обе проблемы хорошо решать на этапе чтения файла, а не после.
Чтение CSV без боли
pd.read_csv — это монстр-функция с пятьюдесятью параметрами. Junior DE использует ровно эти:
df = pd.read_csv(
"transactions.csv",
dtype={
"transaction_id": "string",
"currency": "category",
},
parse_dates=["created_at"],
na_values=["", "-", "N/A", "NULL"],
encoding="utf-8",
)
Что делает каждый параметр:
dtype— задаёт типы колонок явно. Без этого pandas угадывает по содержимому, и догадка бывает неточной.для колонок с малым числом уникальных значений (валюты, статусы, страны) экономит память в десятки раз.categoryparse_dates— список колонок, которые pandas сразу превратит вdatetime64. Без этого даты остаются строкой, и вы не сможете делатьdt.yearили сравнения с датой.na_values— что считать пропуском. По умолчанию pandas понимает пустую строку иNaN, но реальные CSV содержат-,N/A,NULL,unknown,n/a, прочерки в разных регистрах. Лучше перечислить явно.encoding— кодировка файла. По умолчаниюutf-8, но если файл из Excel — обычноcp1251(Windows-1251) илиcp1252. См. урок 02 модуля 3 про кодировки.
После такого чтения dtypes уже правильные:
df.dtypes
# transaction_id string[python]
# amount float64
# currency category
# created_at datetime64[ns]
DE-кейс: грязные числа
Реальный кейс: вам прислали выгрузку из старой ERP-системы, и в колонке amount встречается такое:
amount
1234.56
2 500.00
1 000,75
-
N/A
12345
То есть числа с пробелами как разделителями тысяч (русский формат), запятая вместо точки, прочерк вместо null. По умолчанию pandas прочитает всю колонку как object (строки) — потому что цифры с пробелами не парсятся в float.
Что делать. Вариант 1 — препроцессинг строк, потом to_numeric:
df = pd.read_csv("erp_export.csv", na_values=["-", "N/A"])
# чистим строку, потом конвертируем
df["amount"] = (
df["amount"]
.str.replace(" ", "", regex=False) # убираем пробелы тысяч
.str.replace(",", ".", regex=False) # запятую → точку
.pipe(pd.to_numeric, errors="coerce") # парсим в float, мусор → NaN
)
errors="coerce"df["amount"].isna().sum() — сколько строк потеряли, и решить, выкинуть их или попытаться спасти.
Типы dtypes — какой когда
| dtype | Что хранит | Когда брать |
|---|---|---|
int64 | целые числа без пропусков | счётчики, ID, без NaN |
Int64 (с большой) | целые с поддержкой pd.NA | счётчики, где бывают пропуски |
float64 | вещественные с NaN | суммы, дроби, метрики |
bool | True/False без пропусков | флаги, без NaN |
boolean | True/False с pd.NA | флаги, где может быть unknown |
object | Python-объекты (обычно строки) | по умолчанию для текста; медленно |
string | строки nullable | предпочтительнее object для текста |
category | строки с малым cardinality | страны, валюты, статусы |
datetime64[ns] | datetime | даты-времена |
timedelta64[ns] | разница времени | длительности |
Главное правило:
objectdtype="string" или dtype="category". Колонка чисел? Принудительно astype("Int64") или pd.to_numeric. Object — это медленные Python-объекты в Python-цикле, в pandas от такого нет ускорения.
PyArrow backend — производительность 2.x
С версии 2.0 pandas умеет хранить данные не только в
dtype_backend="pyarrow":
df = pd.read_csv("data.csv", dtype_backend="pyarrow")
df.dtypes
# transaction_id string[pyarrow]
# amount double[pyarrow]
# created_at timestamp[ns][pyarrow]
Зачем. Три причины:
- Скорость. Особенно для строк и категориальных колонок — arrow быстрее NumPy в 2-10 раз.
- Память. Arrow эффективнее упаковывает строки и nullable-числа.
- Nullable everywhere. В NumPy-backend
int64не поддерживает NaN — нужно использоватьInt64отдельно. В Arrow-backend все типы nullable из коробки. Колонка целых с пропусками — обычное дело, ничего особенного.
import numpy as np
# Старый стиль: смесь
df["count"] # int64, без NaN
df["amount"] # float64, с NaN
# Новый стиль с PyArrow
df["count"] # int64[pyarrow], с pd.NA
df["amount"] # double[pyarrow], с pd.NA
В новых проектах берите PyArrow backend сразу. В легаси-проектах оставьте классику — поведение чуть отличается, и менять backend в готовом коде опасно.
pd.NA vs np.nan
Маленький, но важный момент. В NumPy null — это np.nan (float). Поэтому колонка целых не может иметь пропусков — np.nan это float, а в int64 он не помещается. Старая pandas решала это превращением колонки в float (int64 → float64), а недостающее значение становилось NaN.
В pandas 2.x появился
pd.NApd.NA.
import pandas as pd
import numpy as np
# Старый стиль с NumPy
s = pd.Series([1, 2, np.nan])
print(s.dtype) # float64 — целые превратились в float
print(s[2]) # nan
# Новый стиль с nullable Int64
s = pd.Series([1, 2, pd.NA], dtype="Int64")
print(s.dtype) # Int64 — остаются целые
print(s[2]) # <NA>
Поведение pd.NA в логике: pd.NA == pd.NA возвращает pd.NA, а не True. Это правильно (как в SQL: NULL = NULL → NULL). Учитывайте при условиях — df[df["x"] == 1] корректно отбрасывает строки с NA.
Конвертация типов после чтения
Иногда dtype нельзя поставить при чтении (например, числа нужно сначала почистить). Тогда конвертим после:
# Строка → число (можно с coerce для грязных данных)
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
# Строка → datetime
df["created_at"] = pd.to_datetime(df["created_at"], format="%Y-%m-%d", utc=True)
# object → category для экономии памяти
df["currency"] = df["currency"].astype("category")
# float64 → Int64 (nullable целые)
df["count"] = df["count"].astype("Int64")
pd.to_datetime с utc=True — важная привычка. Все timestamps хранить в UTC, переводить в локальную зону только для отображения. Подробнее в уроке 03 этого модуля.
Когда задаёте dtype при read_csv, pandas умеет конвертировать на лету, не загружая колонку как object и потом не пересчитывая. Это быстрее. Но не все конвертации возможны при чтении — особенно для грязных чисел. Иногда сначала читаем как object, потом чистим, потом конвертим.
Маленький DE-чеклист после чтения файла
Сразу после pd.read_csv всегда:
df.shape— столько ли строк, сколько ждали?df.dtypes— нет ли всё-object?df.isna().sum()— где пропуски, сколько?df.head()— данные похожи на правду?df.duplicated().sum()— есть ли полные дубли строк?
Это шаги, которые делает каждый DE первым делом. Привыкайте.
Упражнение
Возьмите этот CSV (создайте файл dirty.csv с содержимым):
id,amount,currency,created
1,1234.56,USD,2025-01-15
2,2 500.00,USD,2025-01-16
3,1 000,75,EUR,2025-01-17
4,-,USD,2025-01-18
5,N/A,EUR,2025-01-19
6,9999.00,RUB,2025-01-20
Напишите скрипт clean.py, который:
- Читает файл через
pd.read_csvсna_values. - Конвертирует
amountвfloat64, разбираясь с пробелами и запятыми. Грязные значения → NaN. - Конвертирует
createdвdatetime64. - Конвертирует
currencyвcategory. - Печатает
df.info()иdf.head().
Критерии приёмки:
- После очистки
df["amount"].dtype == "float64". df["created"].dtypeначинается сdatetime64.df["currency"].dtype.name == "category".- В колонке
amountровно 2 NaN (строки 4 и 5). df["amount"].sum()примерно равна14734.31(если не учитываете строку 3 — потому что1 000,75после правил должно дать1000.75).
В следующем уроке мы возьмём этот же DataFrame и будем его фильтровать, группировать и склеивать с другими таблицами.