Как очистить данные в Pandas

Зачем чистить данные

80% времени аналитика — чистка. Реальные данные всегда грязные:

  • Пропуски (NaN).
  • Дубликаты.
  • Неправильные типы (числа как строки).
  • Разные форматы строк ('Moscow' vs 'moscow' vs 'MOSCOW').
  • Выбросы.

Чистка — обязательный этап до любого анализа.

1. Посмотреть, что в данных

df.info()           # типы, non-null count
df.describe()       # статистика числовых
df.head()           # первые 5 строк
df.isna().sum()     # сколько NaN в каждой колонке
df.dtypes           # типы колонок
df.nunique()        # количество уникальных в каждой колонке

Всегда начинайте с этого.

2. Обработка пропусков (NaN)

Проверить процент NaN

(df.isna().sum() / len(df) * 100).sort_values(ascending=False)

Удалить строки

df = df.dropna()               # где любой NaN
df = df.dropna(subset=['email'])  # где NaN в email
df = df.dropna(thresh=3)       # где < 3 не-NaN

Заполнить

df['amount'] = df['amount'].fillna(0)
df['status'] = df['status'].fillna('unknown')

# Средним по группе
df['amount'] = df.groupby('city')['amount'].transform(
    lambda x: x.fillna(x.mean())
)

# Forward fill для временных рядов
df['price'] = df.sort_values('date')['price'].ffill()

Оставить NaN и работать с ними

Иногда лучше сохранить:

# Аналитические агрегаты pandas игнорируют NaN по умолчанию
df['amount'].mean()   # не учитывает NaN

# Для явного подсчёта
df['amount'].notna().sum()

3. Дубликаты

Найти

df.duplicated().sum()             # сколько полных дублей
df.duplicated(subset=['email']).sum()  # по email

# Показать
df[df.duplicated(keep=False)]     # все дубликаты

Удалить

df = df.drop_duplicates()
df = df.drop_duplicates(subset=['email'], keep='first')
df = df.drop_duplicates(subset=['user_id', 'date'])

Если хочется сразу закрепить тему на практике — открой тренажёр в Telegram. 10 минут в день — и синтаксис в пальцах.

4. Приведение типов

# Строка → число
df['amount'] = pd.to_numeric(df['amount'], errors='coerce')

# Строка → дата
df['date'] = pd.to_datetime(df['date'], errors='coerce')
df['date'] = pd.to_datetime(df['date'], format='%d.%m.%Y')

# Число → строка
df['id'] = df['id'].astype(str)

# Bool из строки
df['is_active'] = df['is_active'].map({'yes': True, 'no': False})

# Категориальный (экономит память)
df['country'] = df['country'].astype('category')

errors='coerce' — превращает невалидные значения в NaN вместо падения.

5. Очистка строк

Нормализация

df['email'] = df['email'].str.lower().str.strip()
df['name'] = df['name'].str.title()  # 'john doe' → 'John Doe'

Удаление символов

df['phone'] = df['phone'].str.replace(r'[^\d]', '', regex=True)
# '+7 (999) 123-45-67' → '79991234567'

Замена значений

df['status'] = df['status'].replace({
    'paid': 'оплачен',
    'cancelled': 'отменён',
    'refunded': 'возврат'
})

Извлечение через regex

df['domain'] = df['email'].str.extract(r'@(.+)$')

Разделение одной колонки на несколько

df[['first', 'last']] = df['full_name'].str.split(' ', n=1, expand=True)

6. Выбросы

Визуальная проверка

import seaborn as sns
sns.boxplot(data=df, x='amount')
# Точки вне «усов» — выбросы

Z-score

from scipy import stats
df['z_score'] = stats.zscore(df['amount'])
df_clean = df[df['z_score'].abs() < 3]

IQR

q1 = df['amount'].quantile(0.25)
q3 = df['amount'].quantile(0.75)
iqr = q3 - q1
df_clean = df[(df['amount'] >= q1 - 1.5 * iqr) & (df['amount'] <= q3 + 1.5 * iqr)]

Winsorization (подрезать, не удалять)

from scipy.stats import mstats
df['amount_clipped'] = mstats.winsorize(df['amount'], limits=[0.01, 0.01])
# Обрезает 1% снизу и сверху

7. Стандартизация категорий

Часто 'Moscow', 'moscow', 'МОСКВА', 'Москва' — одно и то же:

city_mapping = {
    'moscow': 'Москва', 'МОСКВА': 'Москва', 'Москва': 'Москва',
    'spb': 'Санкт-Петербург', 'СПб': 'Санкт-Петербург',
}
df['city'] = df['city'].str.lower().map(city_mapping).fillna(df['city'])

8. Работа с датами

# Стандартизация формата
df['date'] = pd.to_datetime(df['date'], errors='coerce')

# Проверка диапазона
df[df['date'] < '2020-01-01']        # слишком старые
df[df['date'] > pd.Timestamp.now()]   # из будущего (ошибка)

# Извлечь компоненты
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['dayofweek'] = df['date'].dt.dayofweek

Чтобы не только читать теорию, но и решать реальные задачи — загляните в бот Карьерника. Там по каждой теме подборка вопросов с разборами.

9. Проверка целостности

# Есть ли отрицательные значения в положительном поле?
assert (df['amount'] >= 0).all()

# Есть ли дубликаты по PK?
assert df['id'].is_unique

# Все FK ссылаются на существующие user_id?
assert df['user_id'].isin(users['id']).all()

10. Итоговый пайплайн

def clean_data(df):
    df = df.drop_duplicates()
    df['email'] = df['email'].str.lower().str.strip()
    df['amount'] = pd.to_numeric(df['amount'], errors='coerce')
    df['date'] = pd.to_datetime(df['date'], errors='coerce')
    df = df.dropna(subset=['email', 'amount', 'date'])
    df = df[df['amount'] > 0]
    return df.reset_index(drop=True)

df_clean = clean_data(df)

Пайплайн-функция — всегда чистая обработка.

Частые ошибки

1. Удалять NaN без анализа

50% NaN в важной колонке — не удалять всё, а разбираться в причине.

2. Использовать mean для скошенных данных

Median устойчивее к выбросам.

3. Игнорировать timezone

Dates в разных TZ → разные результаты. Приводите к одному.

4. Забыть reset_index

После dropna / filter индекс сохраняется с дырками. reset_index(drop=True) лечит.

5. Мутировать оригинал

# Плохо
df = pd.read_csv(...)
df_clean = df  # это не копия!
df_clean = df_clean.drop(...)  # мутирует df

# Хорошо
df_clean = df.copy()

Читайте также

FAQ

Сколько времени на чистку?

50–80% всего аналитического проекта. Грязные данные — главный стоппер.

Где чистить — в SQL или pandas?

Чем раньше, тем лучше. Если данные всегда грязные из источника — на ETL-уровне. Разовый случай — pandas.

Как понять, что данные «достаточно чистые»?

Когда любые дальнейшие операции дают ожидаемые результаты без сюрпризов. В идеале — автоматические assert-ы.

Нужна ли автоматизация чистки?

Обязательно, если данные приходят регулярно. Функция или класс с unit-тестами.