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¶
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.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¶
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¶
- NumPy Cheatsheet — массивы, broadcasting, линал
- PyTorch Cheatsheet — DataLoader, Dataset
- sklearn Cheatsheet — Pipeline, preprocessing
- Classical ML Interview Q&A — feature engineering вопросы