Как очистить данные в 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()Читайте также
- Пропуски в Pandas шпаргалка
- Pandas шпаргалка
- Как найти выбросы
- Выбросы в данных
- Propuski v dannyh pandas
FAQ
Сколько времени на чистку?
50–80% всего аналитического проекта. Грязные данные — главный стоппер.
Где чистить — в SQL или pandas?
Чем раньше, тем лучше. Если данные всегда грязные из источника — на ETL-уровне. Разовый случай — pandas.
Как понять, что данные «достаточно чистые»?
Когда любые дальнейшие операции дают ожидаемые результаты без сюрпризов. В идеале — автоматические assert-ы.
Нужна ли автоматизация чистки?
Обязательно, если данные приходят регулярно. Функция или класс с unit-тестами.