Шпаргалка по Pandas: DataFrame, groupby, временные ряды
Pandas — главный инструмент анализа данных в Python. Две основные структуры: Series (одномерная) и DataFrame (двумерная таблица). Построен на NumPy, добавляет метки, пропуски, группировки и работу с временными рядами.
Импорт и первые шаги
import pandas as pd
import numpy as np
# Настройки отображения
pd.set_option('display.max_columns', None) # показывать все столбцы
pd.set_option('display.max_rows', 100) # до 100 строк
pd.set_option('display.width', None) # не обрезать по ширине
Создание DataFrame
# Из словаря
df = pd.DataFrame({
'name': ['Анна', 'Борис', 'Вика'],
'age': [25, 32, 28],
'city': ['Москва', 'Питер', 'Казань']
})
# Из списка списков
df = pd.DataFrame(
[[1, 'x', 1.5], [2, 'y', 2.5]],
columns=['A', 'B', 'C']
)
# Из NumPy массива
df = pd.DataFrame(np.random.randn(100, 4), columns=['A', 'B', 'C', 'D'])
Базовая информация
df.info() # типы, память, non-null
df.describe() # count, mean, std, min, 25%, 50%, 75%, max
df.describe(include='object') # для строковых: count, unique, top, freq
df.head(10) # первые 10 строк
df.tail(5) # последние 5
df.shape # (строки, столбцы)
df.columns # список названий столбцов
df.index # индекс строк
df.dtypes # типы данных
df.memory_usage(deep=True) # память (deep — учесть строки)
Загрузка и сохранение данных
# CSV
df = pd.read_csv('file.csv')
df = pd.read_csv('file.csv', sep=';', encoding='utf-8')
df = pd.read_csv('file.csv', index_col=0, parse_dates=['date'])
df = pd.read_csv('file.csv', usecols=['name', 'age']) # только нужные
df = pd.read_csv('file.csv', chunksize=10000) # чанками
df.to_csv('output.csv', index=False)
# Excel
df = pd.read_excel('file.xlsx', sheet_name='Лист1')
df.to_excel('output.xlsx', index=False)
# Parquet (быстрее CSV, хранит типы)
df.to_parquet('output.parquet')
df = pd.read_parquet('output.parquet')
# JSON
df = pd.read_json('file.json')
df.to_json('output.json', orient='records')
# SQL
from sqlalchemy import create_engine
engine = create_engine('sqlite:///db.sqlite')
df = pd.read_sql('SELECT * FROM users', engine)
df.to_sql('users', engine, if_exists='replace', index=False)
Индексация и выборка
Выбор столбцов и строк
# Столбцы
df['name'] # Series
df[['name', 'age']] # DataFrame (двойные скобки!)
# Строки по позиции
df[0:5] # первые 5 строк (позиционный срез)
df.iloc[0:5] # то же самое
df.iloc[[0, 2, 4]] # строки 0, 2, 4 по позиции
df.iloc[-5:] # последние 5 строк
# Строки по метке индекса
df.loc[0:5] # метки 0..5 ВКЛЮЧИТЕЛЬНО (в отличие от iloc!)
df.loc[[0, 2, 4]] # строки с метками 0, 2, 4
# Строки и столбцы одновременно
df.loc[0:5, 'name':'age'] # строки 0..5, столбцы name..age
df.iloc[0:5, 0:3] # строки 0..4, столбцы 0..2
df.loc[[1, 3], ['name', 'city']] # конкретные строки и столбцы
Ключевые различия .loc vs .iloc vs []
df[0:5] |
.loc |
.iloc |
|
|---|---|---|---|
| Основа | Позиция (только строки) | Метка (label) | Позиция (integer) |
| Верхняя граница | Не включена | Включена | Не включена |
| Строки + столбцы | ❌ | ✅ | ✅ |
Условная фильтрация
# Простые условия
df[df['age'] > 25]
df[df['city'] == 'Москва']
df[df['salary'].isna()] # строки с пропущенной зарплатой
# Множественные условия — & | ~ (не and/or!)
df[(df['age'] > 25) & (df['city'] == 'Москва')]
df[(df['dept'] == 'IT') | (df['dept'] == 'HR')]
df[~df['name'].isin(['Анна', 'Борис'])] # НЕ в списке
# query() — SQL-подобный синтаксис
df.query('age > 25 and salary > 50000')
df.query('city in ["Москва", "Питер"]')
# Специальные методы
df[df['age'].between(20, 40)] # возраст в диапазоне
df[df['email'].str.contains('@gmail')] # строковый поиск
df.nlargest(10, 'salary') # топ-10 по зарплате
df.nsmallest(5, 'price') # 5 самых дешёвых
df.sample(10) # случайные 10 строк
df.sample(frac=0.1) # 10% случайных строк
Сортировка
df.sort_values('age') # по возрастанию
df.sort_values('age', ascending=False) # по убыванию
df.sort_values(['city', 'age'], ascending=[True, False]) # по двум полям
df.sort_index() # по индексу
# NaN в конец (по умолчанию) или в начало
df.sort_values('col', na_position='first')
Пропущенные значения
df.isna() # булева маска
df.isna().sum() # количество пропусков по столбцам
df.isna().sum().sum() # общее число пропусков
# Удаление
df.dropna() # удалить строки с любым NaN
df.dropna(how='all') # удалить только если все NaN
df.dropna(subset=['email']) # удалить где NaN в email
df.dropna(thresh=3) # оставить если >= 3 не-NaN
df.dropna(axis=1) # удалить столбцы с NaN
# Заполнение
df.fillna(0) # нулями
df.fillna({'age': 0, 'city': 'Н/Д'}) # разными значениями
df.fillna(df.mean(numeric_only=True)) # средними по столбцам
df.fillna(method='ffill') # предыдущим значением (forward fill)
df.fillna(method='bfill') # следующим значением (backward fill)
df.interpolate() # линейная интерполяция
Операции со столбцами
# Добавление/изменение
df['new_col'] = df['a'] + df['b']
df['category'] = np.where(df['score'] >= 80, 'high', 'low')
df['category'] = np.select(
[df['score'] >= 80, df['score'] >= 60],
['high', 'medium'],
default='low'
)
# Переименование
df.rename(columns={'old': 'new'})
df.rename(columns=str.lower) # все в нижний регистр
# Удаление
df.drop('col', axis=1) # удалить столбец
df.drop(['col1', 'col2'], axis=1) # удалить несколько
df.drop([0, 1, 2]) # удалить строки
# Типы данных
df['col'].astype('int32') # преобразование
pd.to_numeric(df['col'], errors='coerce') # строка → число (NaN если ошибка)
df['col'].astype('category') # категориальный (экономит память)
Группировка и агрегация
groupby — split-apply-combine
Основной механизм агрегации в Pandas. Данные разбиваются на группы по значению столбца (split), к каждой группе применяется функция (apply), результаты собираются обратно (combine). Аналог GROUP BY в SQL. Поддерживает несколько агрегаций одновременно через agg().
# Базовая группировка
df.groupby('city')['salary'].mean()
df.groupby('city').agg({'salary': 'mean', 'age': 'max'})
# Именованная агрегация
df.groupby('dept').agg(
avg_salary=('salary', 'mean'),
max_salary=('salary', 'max'),
count=('salary', 'count'),
unique_cities=('city', 'nunique')
)
# Несколько агрегаций
df.groupby('city')['salary'].agg(['mean', 'std', 'min', 'max', 'count'])
# Группировка по нескольким полям
df.groupby(['city', 'dept'])['salary'].mean().unstack()
# transform — сохраняет размер (для добавления в df)
df['dept_avg'] = df.groupby('dept')['salary'].transform('mean')
df['z_score'] = df.groupby('dept')['salary'].transform(
lambda x: (x - x.mean()) / x.std()
)
# filter — отфильтровать группы
df.groupby('dept').filter(lambda x: x['salary'].mean() > 70000)
Сводные таблицы
pivot_table превращает «длинные» данные в «широкие»: строки становятся одним индексом, столбцы — другим, значения — агрегируются. Аналог сводных таблиц Excel. pivot — без агрегации (каждая комбинация индекса и столбца должна быть уникальна). crosstab — быстрый подсчёт частот.
# pivot_table
pd.pivot_table(df, values='sales', index='region', columns='product', aggfunc='sum')
# Без агрегации — pivot (reshape)
df.pivot(index='date', columns='city', values='temp')
# Кросс-таблица
pd.crosstab(df['city'], df['dept'])
pd.crosstab(df['city'], df['dept'], values=df['salary'], aggfunc='mean')
pd.crosstab(df['city'], df['dept'], normalize='index') # доли по строкам
Объединение данных
# merge (аналог SQL JOIN)
pd.merge(users, orders, on='user_id') # inner
pd.merge(users, orders, on='user_id', how='left') # все пользователи
pd.merge(users, orders, on='user_id', how='outer') # все строки
pd.merge(users, orders, left_on='id', right_on='user_id') # разные имена
pd.merge(users, orders, on='id', indicator=True) # колонка _merge
# concat (склеивание)
pd.concat([df1, df2]) # по строкам
pd.concat([df1, df2], axis=1) # по столбцам
pd.concat([df1, df2], ignore_index=True) # сбросить индекс
# join (по индексу)
df1.join(df2) # left join по индексу
df1.join(df2, how='inner')
Временные ряды
# Создание дат
dates = pd.date_range('2024-01-01', periods=365, freq='D')
dates = pd.date_range('2024-01-01', '2024-12-31', freq='W') # понедельники
dates = pd.date_range('2024-01-01', periods=12, freq='MS') # начало месяца
dates = pd.date_range('2024-01-01', periods=4, freq='QS') # начало квартала
# Частоты: D (день), W (неделя), M (конец месяца), MS (начало месяца),
# Q (конец квартала), QS (начало), A (год), H (час), T/min (минута)
# Парсинг дат
df['date'] = pd.to_datetime(df['date_str'])
df['date'] = pd.to_datetime(df['date_str'], format='%d.%m.%Y')
# Установка индекса
df = df.set_index('date')
df = df.sort_index() # обязательно отсортировать!
# Срезы по датам (работают только с DatetimeIndex!)
df['2024-01'] # январь 2024
df['2024-01':'2024-03'] # Q1 2024
df['2024-01-15':'2024-01-31'] # вторая половина января
# Доступ к компонентам даты
df.index.year # год
df.index.month # месяц (1-12)
df.index.day # день (1-31)
df.index.dayofweek # день недели (0=Пн, 6=Вс)
df.index.day_name() # 'Monday', 'Tuesday'...
df.index.quarter # квартал (1-4)
df.index.is_month_start # начало месяца?
Операции с временными рядами
После установки datetime-индекса Pandas даёт богатый набор операций: сдвиги (shift, diff), ресемплинг (изменение частоты — дневные в месячные), скользящие и расширяющиеся окна, экспоненциально взвешенные средние. Всё это — стандартные инструменты анализа временных рядов.
# Сдвиг
df['prev'] = df['value'].shift(1) # предыдущее значение
df['next'] = df['value'].shift(-1) # следующее
df['diff'] = df['value'].diff() # разница с предыдущим
df['pct'] = df['value'].pct_change() # % изменение
# Ресемплинг (изменение частоты)
df.resample('W').mean() # дневные → недельные
df.resample('M').agg(['mean', 'sum', 'count']) # дневные → месячные
df.resample('D').ffill() # заполнить предыдущим
df.resample('H').interpolate() # интерполяция
# Скользящие окна
df['SMA_7'] = df['value'].rolling(7).mean() # простое скользящее
df['SMA_30'] = df['value'].rolling(30, min_periods=15).mean() # мин 15 точек
df['roll_std'] = df['value'].rolling(7).std() # скользящее std
# Bollinger Bands
df['BB_mid'] = df['value'].rolling(20).mean()
df['BB_upper'] = df['BB_mid'] + 2 * df['value'].rolling(20).std()
df['BB_lower'] = df['BB_mid'] - 2 * df['value'].rolling(20).std()
# Расширяющиеся окна (накопительные)
df['expanding_mean'] = df['value'].expanding().mean()
# Экспоненциально взвешенное (EMA)
df['EMA_12'] = df['value'].ewm(span=12).mean()
apply, map, transform
# map — для Series (простые преобразования)
df['name'] = df['name'].map(str.title)
df['dept_code'] = df['dept'].map({'IT': 1, 'HR': 2, 'Sales': 3})
# apply — для Series (любые функции)
df['name_len'] = df['name'].apply(len)
df['salary_k'] = df['salary'].apply(lambda x: x / 1000)
# apply — для DataFrame (по строкам или столбцам)
df.apply(np.sum, axis=0) # сумма по столбцам
df.apply(lambda row: row['a'] + row['b'], axis=1) # сумма по строке
# transform — как apply, но сохраняет размер
df.groupby('dept')['salary'].transform('mean') # среднее по отделу в каждой строке
Работа со строками (.str)
df['name'].str.lower() # в нижний регистр
df['name'].str.upper() # в верхний
df['name'].str.title() # каждое слово с большой
df['name'].str.strip() # убрать пробелы
df['name'].str.replace('old', 'new') # замена
df['name'].str.contains('pattern') # булева маска
df['name'].str.startswith('А') # начинается с
df['name'].str.len() # длина строки
df['name'].str.split(' ') # разбить → список
df['email'].str.split('@', expand=True) # разбить → DataFrame (два столбца)
df['text'].str.extract(r'(\d+)') # извлечь по regex
df['text'].str.findall(r'\d+') # найти все совпадения
Memory optimization
# Показать использование памяти
df.info(memory_usage='deep')
# Downcast числовых типов
for col in df.select_dtypes('float').columns:
df[col] = pd.to_numeric(df[col], downcast='float')
for col in df.select_dtypes('int').columns:
df[col] = pd.to_numeric(df[col], downcast='integer')
# Категориальный тип (экономия 10-50x для повторяющихся строк)
df['city'] = df['city'].astype('category')
df['status'] = df['status'].astype('category')
# Sparse — для разреженных данных
df['sparse_col'] = df['sparse_col'].astype(pd.SparseDtype('float64', 0))
Method chaining (цепочки методов)
# Вместо:
df_temp = df[df['age'] > 18]
df_temp = df_temp.groupby('city').agg({'salary': 'mean'})
df_temp = df_temp.sort_values('salary', ascending=False)
# Пишем цепочкой (чище, быстрее):
result = (
df
.query('age > 18')
.groupby('city')
.agg(avg_salary=('salary', 'mean'))
.sort_values('avg_salary', ascending=False)
.reset_index()
)
# pipe — вставить пользовательскую функцию в цепочку
def add_age_group(df):
df = df.copy()
df['age_group'] = pd.cut(df['age'], bins=[0, 25, 40, 60, 100], labels=['<25', '25-40', '40-60', '60+'])
return df
result = (
df
.pipe(add_age_group)
.groupby('age_group')['salary'].mean()
)
Визуализация (встроенная)
# Pandas-обёртка над matplotlib
df['salary'].plot(kind='hist', bins=20)
df.plot(kind='scatter', x='age', y='salary')
df['city'].value_counts().plot(kind='bar')
df.groupby('city')['salary'].mean().plot(kind='barh')
df['category'].value_counts().plot(kind='pie', autopct='%1.1f%%')
df.plot(kind='box', column='salary', by='city')
df[['sales', 'profit']].plot(kind='line') # несколько линий
# Сохранение графика
ax = df.plot(kind='bar')
ax.figure.savefig('plot.png', dpi=150, bbox_inches='tight')
Приёмы и паттерны
# Дедубликация
df.duplicated() # булева маска дубликатов
df.drop_duplicates() # удалить дубли
df.drop_duplicates(subset=['name', 'email'])
# Уникальные значения
df['city'].unique() # список уникальных
df['city'].value_counts() # + частоты
df['city'].value_counts(normalize=True) # доли
# Корреляция
df.corr(numeric_only=True) # матрица корреляций
df.corrwith(df['target']) # корреляция с конкретным столбцом
# Условные столбцы через assign
df = df.assign(
age_group=lambda x: pd.cut(x['age'], bins=[0, 25, 40, 60, 100], labels=['<25', '25-40', '40-60', '60+']),
salary_k=lambda x: x['salary'] / 1000
)
# Обработка выбросов (IQR метод)
Q1 = df['salary'].quantile(0.25)
Q3 = df['salary'].quantile(0.75)
IQR = Q3 - Q1
df_no_outliers = df[(df['salary'] >= Q1 - 1.5*IQR) & (df['salary'] <= Q3 + 1.5*IQR)]
Частые ошибки
| Ошибка | Правильно |
|---|---|
df[df.age > 25]['salary'] = 0 |
df.loc[df.age > 25, 'salary'] = 0 |
df[cond1 and cond2] |
df[(cond1) & (cond2)] — скобки обязательны! |
.loc[0:5] как .iloc[0:5] |
.loc включает правую границу, .iloc — нет |
merge без on — по всем общим |
Явно указывай on= |
inplace=True в цепочке |
Не работает! Используй присваивание |
float сравнение == |
Используй np.isclose() или df.isna() |
Группировка без observed=True |
С категориями: groupby(..., observed=True) |
query() и eval() — быстрые фильтры и вычисления
query() — фильтрация строкой
query() позволяет фильтровать DataFrame, используя строковое выражение вместо булевых масок. Код короче, читается как SQL WHERE, и работает быстрее на больших данных за счёт внутренней оптимизации (numexpr).
# Вместо:
df[(df['age'] > 30) & (df['salary'] > 50000) & (df['dept'].isin(['IT', 'HR']))]
# query() — чище и быстрее:
df.query('age > 30 and salary > 50000 and dept in ["IT", "HR"]')
# Переменные через @
min_age: int = 30
min_salary: float = 50000.0
df.query('age > @min_age and salary > @min_salary')
# Строковые методы внутри query()
df.query('name.str.startswith("А")')
df.query('email.str.contains("@gmail.com")')
# Работа с индексом
df.query('index >= "2024-01-01"')
eval() — вычисления строкой
eval() вычисляет арифметические и логические выражения над столбцами как строку. На больших DataFrame (100k+ строк) может быть быстрее обычных операций за счёт numexpr и экономии памяти на промежуточных массивах.
# Вместо:
df['result'] = df['a'] + df['b'] * df['c']
# eval() — иногда быстрее на больших данных:
df['result'] = df.eval('a + b * c')
# Несколько выражений за раз
df.eval('''
total = a + b
ratio = a / b
flag = total > 100
''', inplace=True)
# Когда eval() НЕ нужен: простые операции (a + b), малые DataFrame (<10k строк).
# Используй eval() для: сложных выражений, больших данных, нескольких колонок за раз.
Сравнение производительности
query() и eval() дают выигрыш только на больших DataFrame (≥500k строк) и сложных выражениях. На малых данных оверхед парсинга выражения нивелирует преимущество.
import pandas as pd
import numpy as np
big_df: pd.DataFrame = pd.DataFrame({
'age': np.random.randint(18, 65, size=1_000_000),
'salary': np.random.randint(30_000, 200_000, size=1_000_000),
'dept': np.random.choice(['IT', 'HR', 'Sales', 'Ops'], size=1_000_000),
'a': np.random.randn(1_000_000),
'b': np.random.randn(1_000_000),
'c': np.random.randn(1_000_000),
})
# 1. Булева маска vs query() — фильтрация
mask_direct = big_df[(big_df['age'] > 30) & (big_df['salary'] > 50000)]
mask_query = big_df.query('age > 30 and salary > 50000')
# %timeit в Jupyter: 8.5 мс ± 200 мкс — булева маска
# %timeit в Jupyter: 5.1 мс ± 150 мкс — query() (~1.7x быстрее)
# 2. Арифметика vs eval() — вычисление
calc_direct = big_df['a'] + big_df['b'] * big_df['c']
calc_eval = big_df.eval('a + b * c')
# %timeit в Jupyter: 6.2 мс ± 100 мкс — обычное выражение
# %timeit в Jupyter: 4.8 мс ± 80 мкс — eval() (~1.3x быстрее)
# Вердикт
def benchmark(name: str, expr) -> None:
"""Усреднённый запуск для воспроизводимости."""
import time
times: list[float] = []
for _ in range(5):
t0 = time.perf_counter()
_ = expr()
times.append(time.perf_counter() - t0)
avg_ms = sum(times) / len(times) * 1000
print(f'{name:30s} {avg_ms:6.2f} мс (avg of 5)')
benchmark('bool mask', lambda: big_df[(big_df['age'] > 30) & (big_df['salary'] > 50000)])
benchmark('query()', lambda: big_df.query('age > 30 and salary > 50000'))
benchmark('arithmetic', lambda: big_df['a'] + big_df['b'] * big_df['c'])
benchmark('eval()', lambda: big_df.eval('a + b * c'))
Когда выигрыш реальный:
- DataFrame ≥ 500k строк
- Сложные выражения (3+ операций, много
and/or) - Узкое место — CPU, а не I/O
Когда НЕ использовать:
- DataFrame < 50k строк (оверхед парсинга)
- Простые операции (
a + b) - Уже используется
numba/cython— прирост будет незначительным
Частые ошибки:
| Ошибка | Правильно |
|---|---|
query('dept == IT') — IT без кавычек |
query('dept == "IT"') |
query('a > @x and @y') — две переменные без оператора |
query('a > @x and b > @y') |
eval('a + b', inplace=True) без присваивания |
eval() не меняет df, если нет = в выражении |
Использовать eval() для a + b на 100 строках |
Не нужно — оверхед парсинга больше выигрыша |
Категориальные данные (pd.Categorical)
Категориальный тип (category) — один из самых мощных инструментов Pandas для экономии памяти и ускорения операций. Подходит для столбцов с малым числом уникальных значений (пол, статус, город, тип продукта).
Экономия памяти зависит от длины строк и числа категорий:
| Тип до | Тип после | Типичная экономия | Когда |
|---|---|---|---|
object (города: 5–15 символов) |
category |
5–15x | Мало уникальных значений (≤100), короткие строки |
object (длинные тексты >100 символов) |
category |
10–50x | Много одинаковых длинных строк |
int64 (любые целые) |
category |
2–8x | Много строк, мало уникальных значений |
Не используйте category, если:
- Уникальных значений почти столько же, сколько строк (нет экономии)
- Нужна попарная уникальность — например, ID пользователей, UUID, email
- Столбец будет часто конкатенироваться с другими DF (теряется тип)
# Создание категориального типа
df['city'] = df['city'].astype('category')
df['status'] = pd.Categorical(df['status'], categories=['new', 'active', 'closed'], ordered=True)
# Доступ к внутренним кодам (нужно для ML-моделей: sklearn не принимает строки)
df['status'].cat.codes # int8/int16 — внутренние коды категорий
df['status'].cat.categories # Index — список всех категорий
# Сравнение памяти
print(df['city'].memory_usage(deep=True)) # object — например, 800 KB
df['city'] = df['city'].astype('category')
print(df['city'].memory_usage(deep=True)) # category — например, 20 KB
# Упорядоченные категории (важно для сортировки!)
size_order = pd.CategoricalDtype(categories=['S', 'M', 'L', 'XL'], ordered=True)
df['size'] = df['size'].astype(size_order)
df.sort_values('size') # S < M < L < XL — правильный порядок!
# Добавление новых категорий
df['status'] = df['status'].cat.add_categories(['archived'])
# Удаление неиспользуемых
df['status'] = df['status'].cat.remove_unused_categories()
# Переименование категорий
df['status'] = df['status'].cat.rename_categories({'new': 'Новый', 'active': 'Активный'})
Важные нюансы
# groupby с категориями: always use observed=True!
# Без observed Pandas показывает ВСЕ категории, даже пустые
df.groupby('status', observed=True)['amount'].sum()
# Преобразование обратно
df['city'] = df['city'].astype(str) # category → string
# Сериализация: Parquet сохраняет категории «из коробки», CSV — нет
df.to_parquet('data.parquet') # типы сохраняются
df.to_csv('data.csv') # категории станут строками!
Частые ошибки:
| Ошибка | Правильно |
|---|---|
groupby('cat_col') без observed=True |
Всегда groupby('cat_col', observed=True) |
| Сохранение категорий в CSV → потеря типа | Используй Parquet или восстанавливай astype('category') |
pd.concat двух df с разными категориями |
Категории приведутся к object — объедини категории сначала |
| Присвоение значения не из категорий | Будет NaN, если значение не в списке categories |
MultiIndex — иерархические индексы
MultiIndex (или иерархический индекс) — это два и более уровня индексации строк и/или столбцов. Позволяет представлять многомерные данные в плоской таблице. Часто возникает после groupby() с несколькими ключами или pivot_table().
# Создание MultiIndex
df_multi = df.set_index(['region', 'city'])
df_multi = df.groupby(['region', 'city']).agg(
total_sales=('sales', 'sum'),
avg_price=('price', 'mean')
)
# Индексация по уровням
df_multi.loc['Москва'] # все города региона «Москва»
df_multi.loc[('Москва', 'Химки')] # конкретный город
df_multi.loc[('Москва', slice(None))] # все города Москвы (slice)
df_multi.loc[(slice(None), 'Химки'), :] # Химки во всех регионах
df_multi.xs('Химки', level='city') # кросс-секция по уровню
# Работа с уровнями
df_multi.index.get_level_values(0) # значения первого уровня
df_multi.index.get_level_values('region') # по имени
df_multi.swaplevel() # поменять уровни местами
df_multi.sort_index(level='city') # сортировка по конкретному уровню
# «Разворот» уровня индекса в столбцы
df_multi.unstack(level='city') # city → столбцы
df_multi.unstack().stack() # stack/unstack — преобразование туда-обратно
# Сброс MultiIndex обратно в столбцы
df_flat = df_multi.reset_index()
# MultiIndex в колонках (после pivot_table)
df_pivot = df.pivot_table(values='sales', index='date', columns=['region', 'product'])
df_pivot.loc[:, ('Москва', 'Товар А')] # доступ к мульти-колонкам
Частые ошибки:
| Ошибка | Правильно |
|---|---|
df_multi.loc['region'] — забыл про второй уровень |
df_multi.loc[('region', 'city')] или xs() |
.loc[] по неотсортированному MultiIndex |
df.sort_index() перед .loc со срезами |
Забыть скобки в кортеже ('a', 'b') |
df.loc[('a', 'b')], не df.loc['a', 'b'] |
reset_index() без drop=False → теряет данные |
Явно указывай: reset_index(drop=False) |
SettingWithCopyWarning — ловушка цепочечной индексации
Одна из самых частых проблем новичков. Возникает, когда Pandas не может определить, работаете вы с оригиналом или копией данных.
# ❌ ПЛОХО: цепочечная индексация — Pandas не знает, менять ли view или copy
df[df['age'] > 30]['salary'] = 100000 # SettingWithCopyWarning!
# ✅ ХОРОШО: используй .loc сразу
df.loc[df['age'] > 30, 'salary'] = 100000
# Почему это происходит:
# df[df['age'] > 30] → может вернуть view ИЛИ copy (Pandas сам решает)
# [...]['salary'] = X → присваивание в copy, оригинал не меняется!
# Ещё примеры
# ❌ Плохо
filtered = df[df['dept'] == 'IT']
filtered['bonus'] = filtered['salary'] * 0.1 # Warning на этой строке
# ✅ Хорошо — вариант 1: .loc на оригинале
df.loc[df['dept'] == 'IT', 'bonus'] = df['salary'] * 0.1
# ✅ Хорошо — вариант 2: явная копия (если нужен промежуточный df)
filtered = df[df['dept'] == 'IT'].copy()
filtered['bonus'] = filtered['salary'] * 0.1 # Без warning
Правило: если планируешь менять данные — используй .loc[] с условием и столбцом сразу. Если нужен промежуточный DataFrame для анализа — делай .copy().
Pandas vs SQL — таблица соответствий
Если ты знаешь SQL, эта таблица поможет перевести запросы на Pandas.
| SQL | Pandas | Примечание |
|---|---|---|
SELECT col1, col2 FROM df |
df[['col1', 'col2']] |
Двойные скобки |
SELECT * FROM df LIMIT 10 |
df.head(10) |
|
WHERE col > 100 |
df[df['col'] > 100] или df.query('col > 100') |
|
WHERE col IN ('A', 'B') |
df[df['col'].isin(['A', 'B'])] |
|
WHERE col BETWEEN 10 AND 20 |
df[df['col'].between(10, 20)] |
|
WHERE col LIKE '%pattern%' |
df[df['col'].str.contains('pattern')] |
|
WHERE col IS NULL |
df[df['col'].isna()] |
|
WHERE col IS NOT NULL |
df[df['col'].notna()] |
|
AND / OR |
& / | |
Скобки обязательны! |
SELECT DISTINCT col |
df['col'].unique() |
|
SELECT DISTINCT col1, col2 |
df[['col1', 'col2']].drop_duplicates() |
|
ORDER BY col ASC |
df.sort_values('col') |
|
ORDER BY col DESC |
df.sort_values('col', ascending=False) |
|
GROUP BY col |
df.groupby('col') |
|
GROUP BY ... HAVING |
df.groupby('col').filter(...) |
|
SELECT col, COUNT(*) |
df.groupby('col').size() |
|
SELECT col, SUM(x), AVG(y) |
df.groupby('col').agg({'x': 'sum', 'y': 'mean'}) |
|
INNER JOIN ... ON |
pd.merge(df1, df2, on='key') |
|
LEFT JOIN ... ON |
pd.merge(df1, df2, on='key', how='left') |
|
UNION ALL |
pd.concat([df1, df2]) |
|
CREATE TABLE AS SELECT ... |
df.copy() или df[cols].copy() |
|
UPDATE ... SET ... WHERE |
df.loc[cond, 'col'] = value |
|
DELETE FROM ... WHERE |
df.drop(df[cond].index) |
|
COUNT(*) |
len(df) |
|
COUNT(DISTINCT col) |
df['col'].nunique() |
|
Оконные функции OVER (PARTITION BY ...) |
df.groupby('col')['x'].transform(...) |
|
CASE WHEN ... THEN ... END |
np.select() или pd.cut() |
Продвинутая работа с памятью
Дополнение к базовым приёмам из раздела Memory optimization. Здесь — продвинутые техники.
# Автоматический downcast всех числовых столбцов
def reduce_memory_usage(df: pd.DataFrame) -> pd.DataFrame:
"""Downcast numeric columns to smallest viable dtype."""
for col in df.select_dtypes('integer').columns:
df[col] = pd.to_numeric(df[col], downcast='integer')
for col in df.select_dtypes('float').columns:
df[col] = pd.to_numeric(df[col], downcast='float')
for col in df.select_dtypes('object').columns:
# Если уникальных значений < 50% строк — в category
if df[col].nunique() / len(df) < 0.5:
df[col] = df[col].astype('category')
return df
# Чтение CSV чанками (файл не влезает в память)
chunks: list[pd.DataFrame] = []
for chunk in pd.read_csv('big_file.csv', chunksize=100_000):
# Обрабатываем чанк: фильтруем, агрегируем, уменьшаем
chunk = chunk.query('date >= "2024-01-01"')
chunk = reduce_memory_usage(chunk)
chunks.append(chunk)
df = pd.concat(chunks, ignore_index=True)
# Чтение только нужных столбцов + с типами
dtypes: dict[str, str] = {'id': 'int32', 'amount': 'float32', 'status': 'category'}
df = pd.read_csv('file.csv', usecols=['id', 'amount', 'status'], dtype=dtypes)
# Оценка экономии
before: int = df.memory_usage(deep=True).sum()
df_reduced: pd.DataFrame = reduce_memory_usage(df.copy())
after: int = df_reduced.memory_usage(deep=True).sum()
print(f'Память: {before/1024**2:.1f} MB → {after/1024**2:.1f} MB ({(1-after/before)*100:.0f}% экономии)')
Стилизация таблиц (DataFrame.style)
DataFrame.style — встроенный механизм форматирования таблиц для Jupyter-ноутбуков и экспорта в HTML. Позволяет подсвечивать ячейки, добавлять цветовые шкалы (heatmap'ы), форматировать числа — без изменения самих данных.
# Подсветка экстремумов (max — зелёным, min — красным)
styled: pd.io.formats.style.Styler = (
df.style
.highlight_max(color='#d4edda')
.highlight_min(color='#f8d7da')
)
# Цветовая карта (heatmap) — градиент по значениям
styled = df.style.background_gradient(cmap='viridis')
# Градиент по конкретному столбцу
styled = df.style.background_gradient(subset=['salary', 'bonus'], cmap='Blues')
# Форматирование чисел
styled = df.style.format({
'price': '${:,.2f}',
'percentage': '{:.1%}',
'date': '{:%d.%m.%Y}',
'int_col': '{:,}'.replace(',', ' ') # пробел как разделитель тысяч
})
# Гистограммы внутри ячеек (bar charts)
styled = df.style.bar(subset=['sales'], color='#5cb85c')
# Комбинирование нескольких стилей
styled = (
df.style
.background_gradient(subset=['score'], cmap='RdYlGn')
.highlight_max(subset=['score'], color='#28a745')
.format({'score': '{:.2f}', 'pct': '{:.1%}'})
.set_caption('Результаты за Q1 2024')
)
# Экспорт стилизованной таблицы в Excel (требует openpyxl)
# styled.to_excel('report.xlsx', engine='openpyxl')
# Экспорт в HTML
html: str = styled.to_html()
with open('report.html', 'w') as f:
f.write(html)
Работа с Parquet и Feather
Parquet и Feather — бинарные форматы, которые быстрее CSV в 5–50x и сохраняют типы данных (datetime, category, int32 — не теряются). Parquet — для долгосрочного хранения и Big Data, Feather — для быстрого обмена между Python и R.
# === Parquet ===
# Запись
df.to_parquet('data.parquet')
df.to_parquet('data.parquet', compression='snappy') # быстрое сжатие
df.to_parquet('data.parquet', compression='gzip') # сильнее, но медленнее
df.to_parquet('data.parquet', index=False) # без индекса
# Партиционирование — сохранение по папкам (ключевой признак Parquet!)
df.to_parquet('sales/', partition_cols=['year', 'month'])
# Чтение
df = pd.read_parquet('data.parquet')
df = pd.read_parquet('sales/') # чтение всех партиций
df = pd.read_parquet('sales/', filters=[('year', '==', 2024)]) # читать только 2024
# Чтение только нужных столбцов (экономия I/O!)
df = pd.read_parquet('data.parquet', columns=['id', 'amount', 'date'])
# === Feather ===
# Запись (быстрее Parquet, но файл больше)
df.to_feather('data.feather')
# Чтение
df = pd.read_feather('data.feather')
df = pd.read_feather('data.feather', columns=['id', 'amount'])
# === Сравнение форматов ===
# | Формат | Скорость | Размер | Типы данных | Партиции | Экосистема |
# |----------|----------|---------|-------------|----------|----------------------|
# | CSV | ★☆☆ | ★★★ | Нет | Нет | Везде |
# | JSON | ★★☆ | ★★☆ | Частично | Нет | Web API |
# | Parquet | ★★★ | ★★☆ | Да | Да | Spark, Hive, BigQuery |
# | Feather | ★★★ | ★☆☆ | Да | Нет | Python ↔ R |
Когда что использовать:
- CSV — обмен с не-Python системами, маленькие файлы, человекочитаемость
- Parquet — основное хранилище данных, большие объёмы (>100 MB), аналитика
- Feather — быстрый временный обмен между скриптами/Python/R
Ресурсы
- Сайт Pandas — полная документация и руководства
- Pandas на GitHub — исходный код
Что дальше
- Шпаргалка по Python: от основ до профи
- Шпаргалка по NumPy: массивы, операции, линейная алгебра
- Шпаргалка по Matplotlib: графики от простых до публикационных
- Шпаргалка по Seaborn: статистическая визуализация
- SQL шпаргалка для начинающих
- Шпаргалка по Scikit-learn: машинное обучение на Python
- Data Science инструменты: стек, roadmap, с чего начать
Связанные шпаргалки: Python, NumPy, Matplotlib, Seaborn, SQL для начинающих