Кладовочка

О разном, чтобы было под рукой онлайн

Шпаргалка по 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

Ресурсы

Что дальше


Связанные шпаргалки: Python, NumPy, Matplotlib, Seaborn, SQL для начинающих