Перейти к содержанию

Pandas: Шпаргалка

~3 минуты чтения

Pandas -- стандартный инструмент для EDA и feature engineering: 95% ML-проектов начинаются с pd.read_csv(). На интервью pandas-вопросы проверяют понимание merge/join (источник #1 data bugs), groupby-агрегаций и работы с пропусками. Три самые частые ошибки практиков: chained assignment, тихое создание дубликатов при merge, и SettingWithCopyWarning.

Быстрый старт

import pandas as pd
import numpy as np

# Чтение данных
df = pd.read_csv('data.csv')
df = pd.read_excel('data.xlsx')
df = pd.read_parquet('data.parquet')
df = pd.read_json('data.json')

# Быстрый обзор
df.head()           # Первые 5 строк
df.tail()           # Последние 5 строк
df.shape            # (rows, columns)
df.info()           # Типы данных, пропуски
df.describe()       # Статистика числовых столбцов
df.dtypes           # Типы данных
df.columns          # Список столбцов
df.index            # Индекс

Создание DataFrame

# Из словаря
df = pd.DataFrame({
    'name': ['Alice', 'Bob', 'Charlie'],
    'age': [25, 30, 35],
    'city': ['NYC', 'LA', 'Chicago']
})

# Из списка списков
df = pd.DataFrame(
    [[1, 2, 3], [4, 5, 6]],
    columns=['a', 'b', 'c'],
    index=['row1', 'row2']
)

# Из numpy array
df = pd.DataFrame(np.random.randn(5, 3), columns=['A', 'B', 'C'])

Выбор данных

Выбор столбцов

df['column']              # Series
df[['col1', 'col2']]      # DataFrame с выбранными столбцами
df.column                 # Dot notation (если имя без пробелов)

Выбор строк

# По индексу
df.loc[0]                 # Строка по метке индекса
df.loc[0:5]               # Срез по меткам (включая 5!)
df.loc[[0, 2, 4]]         # Список индексов

# По позиции
df.iloc[0]                # Первая строка
df.iloc[0:5]              # Первые 5 строк (не включая 5)
df.iloc[[0, 2, 4]]        # По позициям

# Комбинация строки/столбцы
df.loc[0, 'column']       # Конкретная ячейка
df.loc[0:5, ['col1', 'col2']]
df.iloc[0:5, 0:3]

Фильтрация

# Boolean indexing
df[df['age'] > 25]
df[df['city'] == 'NYC']
df[(df['age'] > 25) & (df['city'] == 'NYC')]  # AND
df[(df['age'] > 25) | (df['city'] == 'NYC')]  # OR
df[~(df['age'] > 25)]                          # NOT

# isin
df[df['city'].isin(['NYC', 'LA'])]

# between
df[df['age'].between(25, 35)]

# str методы
df[df['name'].str.contains('Al')]
df[df['name'].str.startswith('A')]

# query (удобнее для сложных условий)
df.query('age > 25 and city == "NYC"')
df.query('city in @cities')  # @variable для внешних переменных

Работа с пропусками

# Проверка
df.isna()                 # True/False для каждой ячейки
df.isna().sum()           # Количество пропусков по столбцам
df.isna().sum().sum()     # Всего пропусков
df.notna()

# Удаление
df.dropna()               # Удалить строки с любыми пропусками
df.dropna(subset=['col1', 'col2'])  # Только по указанным столбцам
df.dropna(how='all')      # Удалить только полностью пустые строки
df.dropna(thresh=3)       # Оставить строки минимум с 3 не-NaN

# Заполнение
df.fillna(0)              # Заполнить нулями
df.fillna(df.mean())      # Заполнить средним
df['col'].fillna(df['col'].median())  # Медианой
df.ffill()                # Forward fill (pandas 2.0+)
df.bfill()                # Backward fill (pandas 2.0+)
df.interpolate()          # Интерполяция

Chained assignment: df[mask]['col'] = value НЕ работает

df[df['age'] > 25]['city'] = 'NYC' -- молча ничего не меняет (или выдаёт SettingWithCopyWarning). Причина: df[mask] может вернуть копию, и присваивание уходит в неё. Правильно: df.loc[df['age'] > 25, 'city'] = 'NYC'. В pandas 3.0+ Copy-on-Write станет дефолтом и поведение изменится, но .loc -- безопасный выбор всегда.


Преобразования

Типы данных

df['col'] = df['col'].astype(int)
df['col'] = df['col'].astype('category')
df['date'] = pd.to_datetime(df['date'])
df['numeric'] = pd.to_numeric(df['col'], errors='coerce')  # Ошибки в NaN

Применение функций

# К столбцу
df['new_col'] = df['col'].apply(lambda x: x * 2)
df['new_col'] = df['col'].map({'A': 1, 'B': 2})

# К нескольким столбцам
df['sum'] = df.apply(lambda row: row['a'] + row['b'], axis=1)

# Vectorized (быстрее!)
df['new_col'] = df['col1'] + df['col2']
df['new_col'] = np.where(df['col'] > 0, 'positive', 'negative')

.apply() с lambda -- это замаскированный Python loop

df.apply(lambda row: ..., axis=1) вызывает Python-функцию ДЛЯ КАЖДОЙ СТРОКИ. На DataFrame с 1M строк это 10-100x медленнее vectorized операции. Пример: df.apply(lambda r: r['a'] + r['b'], axis=1) -- 3 секунды. Vectorized df['a'] + df['b'] -- 5 мс. Используйте .apply() только когда нет vectorized альтернативы. Для условий -- np.where() или np.select().

Строковые операции

df['col'].str.lower()
df['col'].str.upper()
df['col'].str.strip()
df['col'].str.replace('old', 'new')
df['col'].str.split('_')
df['col'].str.split('_', expand=True)  # В отдельные столбцы
df['col'].str.len()
df['col'].str.extract(r'(\d+)')  # Regex группа

Datetime операции

df['date'] = pd.to_datetime(df['date'])
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['day'] = df['date'].dt.day
df['dayofweek'] = df['date'].dt.dayofweek  # 0=Monday
df['hour'] = df['date'].dt.hour
df['date_only'] = df['date'].dt.date
df['time_only'] = df['date'].dt.time

# Разница во времени
df['days_diff'] = (df['date2'] - df['date1']).dt.days

Агрегации

Простые агрегации

df['col'].sum()
df['col'].mean()
df['col'].median()
df['col'].std()
df['col'].min(), df['col'].max()
df['col'].count()           # Не-NaN
df['col'].nunique()         # Уникальные
df['col'].value_counts()    # Частоты

# Несколько сразу
df['col'].agg(['mean', 'std', 'min', 'max'])
df.agg({'col1': 'mean', 'col2': 'sum'})

GroupBy

# Базовый
df.groupby('category')['value'].mean()
df.groupby('category').agg({'value': 'mean', 'count': 'sum'})

# Несколько столбцов группировки
df.groupby(['cat1', 'cat2'])['value'].sum()

# Несколько агрегаций
df.groupby('category').agg(
    mean_value=('value', 'mean'),
    max_value=('value', 'max'),
    count=('value', 'count')
)

# Кастомные функции
df.groupby('category')['value'].apply(lambda x: x.max() - x.min())

# Transform (возвращает той же длины)
df['normalized'] = df.groupby('category')['value'].transform(lambda x: (x - x.mean()) / x.std())

# Filter группы
df.groupby('category').filter(lambda x: len(x) > 5)

Объединение данных

Concat

# Вертикально (стекаем строки)
pd.concat([df1, df2], axis=0, ignore_index=True)

# Горизонтально (стекаем столбцы)
pd.concat([df1, df2], axis=1)

Merge (JOIN)

# Inner join (по умолчанию)
pd.merge(df1, df2, on='key')

# Left join
pd.merge(df1, df2, on='key', how='left')

# Разные названия ключей
pd.merge(df1, df2, left_on='key1', right_on='key2')

# Множественные ключи
pd.merge(df1, df2, on=['key1', 'key2'])

# Все типы join
how='inner'   # Только совпадающие
how='left'    # Все из левой + совпадения из правой
how='right'   # Все из правой + совпадения из левой
how='outer'   # Все из обеих

# Validate для защиты от дубликатов
pd.merge(df1, df2, on='key', validate='one_to_one')
# Опции: 'one_to_one', 'one_to_many', 'many_to_one', 'many_to_many'

merge тихо создаёт дубликаты при non-unique ключах

Если в обеих таблицах есть повторяющиеся значения ключа, merge делает декартово произведение для этих строк. 10 строк слева x 10 справа = 100 строк. Результат может быть в разы больше исходных данных, а ошибки не будет. Используйте validate='one_to_one' (или 'one_to_many') -- pandas кинет MergeError если ключи не уникальны.

Join

# По индексу
df1.join(df2, how='left')
df1.join(df2, on='key')  # df2 индексирован по key

Reshaping

Pivot

# Pivot table
df.pivot_table(
    values='value',
    index='row_category',
    columns='col_category',
    aggfunc='mean',
    fill_value=0
)

# Простой pivot (без агрегации)
df.pivot(index='date', columns='variable', values='value')

Melt (unpivot)

# Wide to long
pd.melt(df,
    id_vars=['id', 'name'],
    value_vars=['jan', 'feb', 'mar'],
    var_name='month',
    value_name='sales'
)

Stack/Unstack

df.stack()      # Columns to index level
df.unstack()    # Index level to columns

Сортировка

df.sort_values('column')
df.sort_values('column', ascending=False)
df.sort_values(['col1', 'col2'], ascending=[True, False])
df.sort_index()  # По индексу

# Top N
df.nlargest(10, 'column')
df.nsmallest(10, 'column')

Дубликаты

df.duplicated()                      # Boolean Series
df.duplicated(subset=['col1'])       # По определённым столбцам
df.drop_duplicates()                 # Удалить дубликаты
df.drop_duplicates(subset=['col1'])
df.drop_duplicates(keep='last')      # Оставить последний
df.drop_duplicates(keep=False)       # Удалить все дубликаты

Добавление/Удаление

# Добавить столбец
df['new_col'] = values
df.insert(0, 'new_col', values)  # В определённую позицию
df = df.assign(new_col=values, another=values2)

# Удалить столбец
df.drop('column', axis=1, inplace=True)
df.drop(['col1', 'col2'], axis=1)
del df['column']

# Удалить строки
df.drop([0, 1, 2], axis=0)
df.drop(df[df['age'] < 18].index)

# Переименовать
df.rename(columns={'old': 'new'})
df.columns = ['a', 'b', 'c']

Сохранение

df.to_csv('output.csv', index=False)
df.to_excel('output.xlsx', index=False)
df.to_parquet('output.parquet')
df.to_json('output.json', orient='records')

Window Functions

# Rolling
df['rolling_mean'] = df['value'].rolling(window=7).mean()
df['rolling_sum'] = df['value'].rolling(window=7).sum()

# Expanding (cumulative)
df['cumsum'] = df['value'].expanding().sum()
df['cummax'] = df['value'].expanding().max()

# Shift
df['prev_value'] = df['value'].shift(1)   # Предыдущее
df['next_value'] = df['value'].shift(-1)  # Следующее
df['pct_change'] = df['value'].pct_change()

# Rank
df['rank'] = df.groupby('category')['value'].rank(ascending=False)

Полезные паттерны

Memory optimization

# Уменьшить размер числовых столбцов
for col in df.select_dtypes(include=['int64']).columns:
    df[col] = pd.to_numeric(df[col], downcast='integer')
for col in df.select_dtypes(include=['float64']).columns:
    df[col] = pd.to_numeric(df[col], downcast='float')

# Категориальные столбцы
df['category_col'] = df['category_col'].astype('category')

# Проверить использование памяти
df.info(memory_usage='deep')

Чтение больших файлов

# Читать по частям
chunks = pd.read_csv('big_file.csv', chunksize=10000)
for chunk in chunks:
    process(chunk)

# Только нужные столбцы
df = pd.read_csv('file.csv', usecols=['col1', 'col2'])

# Указать типы (экономит память)
df = pd.read_csv('file.csv', dtype={'col1': 'int32', 'col2': 'category'})

One-hot encoding

pd.get_dummies(df, columns=['category'], drop_first=True)

Binning

df['age_group'] = pd.cut(df['age'], bins=[0, 18, 35, 50, 100], labels=['child', 'young', 'middle', 'senior'])
df['quantile_group'] = pd.qcut(df['value'], q=4, labels=['Q1', 'Q2', 'Q3', 'Q4'])

Вопросы на интервью

Q: Чем отличаются loc и iloc?

❌ "loc -- по имени, iloc -- по номеру" -- технически верно, но упускает главное

✅ "loc -- label-based, iloc -- position-based. Критическая разница: loc[0:5] ВКЛЮЧАЕТ строку 5 (label-based slice), iloc[0:5] НЕ включает (Python-style). Если index нестандартный (после фильтрации), df[0] может упасть -- используйте iloc[0] для позиционного доступа. Для ячейки: df.at[label, col] (быстрее loc для скаляра)."

Q: Как правильно обработать пропуски в ML-пайплайне?

❌ "fillna(0) или dropna()" -- oversimplification

✅ "Зависит от типа пропуска: MCAR (random) -- dropna безопасен. MAR (зависит от других фичей) -- imputation по группам: df.groupby('category')['value'].transform(lambda x: x.fillna(x.median())). MNAR (зависит от самого значения) -- добавить binary flag is_missing + imputation. Для ML: fit imputer на train, transform на test (sklearn.SimpleImputer), никогда наоборот."

Q: Как оптимизировать memory usage в pandas?

❌ "Использовать chunking" -- один из способов, но не главный

✅ "Три уровня: (1) Типы: int64->int32/int16, float64->float32, строки->category (для < 50% unique). DataFrame 1GB -> 200MB. (2) Чтение: usecols, dtype в read_csv -- не загружать лишнее. (3) Chunking: chunksize=10000 для файлов > RAM. df.info(memory_usage='deep') для диагностики. Для серьезных объемов -- Polars (\(10-100x\) быстрее pandas на агрегациях)."


See Also