Работа с грязными данными: как чистить, нормализовать и не потерять важную информацию

Работа с грязными данными: как чистить, нормализовать и не потерять важную информацию

Представьте: вы получили датасет для анализа. 100 тысяч строк. Клиентская база. Нужно сделать сегментацию, построить модель, найти инсайты. Открываете файл и видите:

Email: "ivan@gmailcom" (без точки), "MARIA@YANDEX.RU" (капс), "null", пустые ячейки. Телефоны: "+7 (912) 345-67-89", "89123456789", "912-345-6789" — три разных формата. Даты: "12.03.2024", "2024-03-12", "12 марта 2024". Имена: "Иванов Иван", "ПЕТРОВА МАРИЯ", "сидоров", " Алексей Смирнов " (с пробелами).

Дубликаты: один клиент записан трижды с разными ID. Пропуски: 30% ячеек пустые. Outliers: возраст "250 лет", доход "$99999999". Вы тратите 2 часа на анализ, получаете странные результаты, понимаете — данные грязные. Нужна чистка.

Вы начинаете чистить. Удаляете строки с пропусками. Убираете дубликаты. Через 4 часа датасет уменьшился со 100K до 65K строк. Вы потеряли 35% данных. И вдруг понимаете: а вдруг среди удалённых были ценные клиенты? Важная информация потеряна безвозвратно.

По данным IBM (2023), компании тратят 40-60% времени аналитиков на очистку данных. Gartner оценивает средний ущерб от плохого качества данных в $12.9M на компанию в год. Data scientists говорят: 80% работы — это подготовка данных, только 20% — сами модели.

Проблема не только во времени. Грязные данные приводят к неправильным выводам, плохим бизнес-решениям, провальным ML-моделям. А агрессивная очистка уничтожает паттерны и важную информацию.

Эта статья — практическое руководство. Какие бывают проблемы с данными, как их находить, какие стратегии очистки применять, когда удалять, когда заполнять, когда оставлять как есть. С конкретными примерами кода (Python/SQL), техниками, инструментами, чек-листами. Чтобы данные были чистыми, но информация не терялась.

1. Что такое грязные данные

Определение

Грязные данные (dirty data) — это данные с ошибками, несогласованностью, дубликатами, пропусками или некорректным форматированием, которые делают их непригодными для анализа без предварительной обработки.

Почему данные становятся грязными

  • Человеческий фактор: Опечатки при вводе, разные стандарты ввода у разных операторов

  • Слияние источников: Объединили 3 базы с разными форматами — получили хаос

  • Миграция систем: Переезд со старой CRM на новую, потеря данных при экспорте/импорте

  • Эволюция схемы данных: Поля менялись со временем, старые записи не обновлялись

  • Отсутствие валидации: Форма позволяет ввести что угодно, никто не проверяет

  • Технические сбои: Ошибки записи, неполные транзакции, дублирование при повторных попытках

  • Устаревание: Клиент сменил телефон 2 года назад, в базе старый

Типы грязных данных

Тип проблемыПримерЧастота
Missing values (пропуски)NULL, NA, пустые ячейки30-50%
ДубликатыОдин клиент с 3 записями5-15%
Несогласованность"Москва", "москва", "MSK"20-40%
OutliersВозраст 250 лет1-5%
Неправильный типЧисло как строка "123"10-20%
Неправильный форматРазные форматы дат15-30%
Опечатки"gmailcom" вместо "gmail.com"5-10%

Грязные данные vs Плохие данные

Грязные данные: Правильная информация в неправильном формате. Можно починить.

Плохие данные: Неправильная информация. Нельзя починить, только удалить или заменить.

Пример грязных: "+7 912 345-67-89" → легко очистить до "79123456789"

Пример плохих: email = "asdfgh" → это не email, информация бесполезна

«80% проблем в data science — это не алгоритмы и модели. Это грязные данные» — DJ Patil, бывший Chief Data Scientist США

2. Стоимость грязных данных

Почему это не просто "неудобство", а серьёзная проблема.

Стоимость 1: Потерянное время

Data scientist с зарплатой $100K/год тратит 60% времени на очистку данных.

Расчёт: $100K × 0.6 = $60K в год на одного человека только на очистку

Команда из 5 data scientists = $300K/год только на борьбу с грязными данными.

Стоимость 2: Неправильные решения

Грязные данные → неверный анализ → плохие бизнес-решения.

Пример: Дубликаты клиентов не удалили. Анализ показал: "У нас 100K клиентов". На самом деле 70K. Планировали маркетинг на 100K бюджет, потратили впустую 30%.

Стоимость 3: Провальные ML-модели

Garbage in = garbage out. Модель обученная на грязных данных делает плохие предсказания.

Пример: Outliers не удалили (доход $99999999). Модель кредитного скоринга одобряет кредиты неплатёжеспособным клиентам. Потери в миллионах.

Стоимость 4: Репутационные риски

Отправили рассылку с ошибками в именах/адресах. Клиент получил "Уважаемый NULL!". Репутация компании страдает.

Стоимость 5: Compliance проблемы

GDPR требует правильности персональных данных. Грязные данные = штрафы до €20M или 4% оборота.

Индустриальная статистика

МетрикаЗначениеИсточник
Средний ущерб от плохого качества данных$12.9M/годGartner, 2023
% времени аналитиков на очистку40-60%IBM, 2023
Стоимость bad data в США$3.1 триллиона/годIBM, 2020
% проектов ML проваливающихся из-за данных85%Gartner, 2022

3. Оценка качества данных

Прежде чем чистить — нужно оценить масштаб проблемы.

Dimension 1: Completeness (полнота)

Сколько данных отсутствует?

Формула: Completeness = (Заполненные ячейки / Всего ячеек) × 100%

Пример: Датасет 10K строк × 20 колонок = 200K ячеек. Пустых 50K. Completeness = 75%.

Бенчмарки:

  • 95-100% — Отлично

  • 80-95% — Хорошо

  • 60-80% — Удовлетворительно

  • <60% — Плохо

Dimension 2: Accuracy (точность)

Насколько данные соответствуют реальности?

Сложно измерить без ground truth. Методы оценки:

  • Сэмплинг + ручная проверка (100-200 записей)

  • Сравнение с внешними источниками

  • Логические проверки (возраст 0-120, email содержит @)

Dimension 3: Consistency (согласованность)

Одинаковые значения записаны одинаково?

Проверка: Количество уникальных значений для категориальных полей.

Пример: Поле "Город". Ожидаем 100 городов. На самом деле 847 уникальных значений (из-за опечаток, разных написаний).

Dimension 4: Uniqueness (уникальность)

Есть ли дубликаты?

Формула: Duplicate rate = (Дубликаты / Всего записей) × 100%

Dimension 5: Timeliness (актуальность)

Насколько свежие данные?

Проверка: Дата последнего обновления записей. Если 50% записей старше 2 лет — возможно устарели.

Dimension 6: Validity (валидность)

Данные соответствуют формату и ограничениям?

Проверки:

  • Email содержит @ и домен

  • Телефон имеет правильную длину

  • Дата в будущем для DoB (некорректно)

  • Отрицательная цена (некорректно)

Пример Python для оценки качества:

import pandas as pd

df = pd.read_csv('data.csv')

# Полнота
completeness = (1 - df.isnull().sum().sum() / (len(df) * len(df.columns))) * 100
print(f"Completeness: {completeness:.2f}%")

# Дубликаты
duplicates = df.duplicated().sum()
duplicate_rate = (duplicates / len(df)) * 100
print(f"Duplicate rate: {duplicate_rate:.2f}%")

# Уникальность категориальных
for col in df.select_dtypes(include='object').columns:
print(f"{col}: {df[col].nunique()} уникальных значений")

4. Принципы очистки данных

Не существует универсального способа. Но есть принципы.

Принцип 1: Никогда не модифицируйте исходные данные

Создайте копию. Работайте с копией. Оригинал — это страховка.

df_clean = df.copy() # Всегда

Принцип 2: Документируйте каждый шаг

Что делали, почему, сколько строк удалили/изменили.

Формат:

  • Шаг 1: Удалили строки где email=NULL (5,234 строк)

  • Шаг 2: Заменили пропуски в возрасте на median (12,456 строк)

  • Шаг 3: Удалили дубликаты по email+phone (3,891 строк)

Принцип 3: Понимайте доменную область

Нельзя механически чистить без понимания бизнеса.

Пример: Возраст = 0. Это ошибка? Или это новорожденный? Нужен контекст.

Принцип 4: Начинайте с EDA (Exploratory Data Analysis)

Прежде чем чистить — изучите данные:

  • Распределения (гистограммы)

  • Корреляции

  • Выбросы (boxplots)

  • Паттерны пропусков (missingno library)

Принцип 5: Итеративный процесс

Очистка → Анализ → Обнаружили новую проблему → Очистка → Анализ...

Не пытайтесь сделать всё за один проход.

Принцип 6: Автоматизируйте, но проверяйте

Пишите скрипты для очистки. Но всегда сэмплируйте результат вручную (100-200 строк). Автоматизация может скрывать ошибки.

Принцип 7: Баланс между чистотой и потерей информации

Идеально чистые данные = пустой датасет (всё удалили).

Нулевая очистка = грязный анализ.

Нужен баланс. Иногда лучше оставить несовершенные данные, чем удалить половину.

5. Работа с пропущенными значениями

Missing values — самая частая проблема.

Типы пропусков

MCAR (Missing Completely At Random): Пропуски случайны, не зависят от других данных.

Пример: Сбой системы привёл к потере данных за 2 часа.

MAR (Missing At Random): Пропуски зависят от других наблюдаемых данных.

Пример: Мужчины реже указывают возраст, чем женщины. Пропуск в возрасте зависит от пола.

MNAR (Missing Not At Random): Пропуск зависит от самого значения.

Пример: Люди с высоким доходом не указывают доход (именно потому что высокий).

Стратегии обработки

Стратегия 1: Удаление

Когда: Пропусков мало (<5%), они MCAR, потеря данных приемлема.

# Удалить строки с любым пропуском
df_clean = df.dropna()

# Удалить строки где пропуск в критичных колонках
df_clean = df.dropna(subset=['email', 'user_id'])

# Удалить колонки где >50% пропусков
threshold = len(df) * 0.5
df_clean = df.dropna(axis=1, thresh=threshold)

Риски: Потеря информации, смещение выборки (если пропуски не MCAR).

Стратегия 2: Заполнение константой

Когда: Пропуск = отсутствие значения (не ошибка).

# Заполнить 0
df['column'].fillna(0)

# Заполнить "Unknown"
df['city'].fillna('Unknown')

# Заполнить -1 (чтобы отличить от настоящих значений)
df['age'].fillna(-1)

Стратегия 3: Заполнение статистикой

Когда: Числовые данные, пропуски MAR.

# Mean (среднее) — чувствителен к outliers
df['age'].fillna(df['age'].mean())

# Median (медиана) — устойчив к outliers
df['salary'].fillna(df['salary'].median())

# Mode (мода) — для категориальных
df['city'].fillna(df['city'].mode()[0])

Стратегия 4: Forward/Backward fill

Когда: Временные ряды, значения стабильны.

# Forward fill (взять предыдущее значение)
df['temperature'].fillna(method='ffill')

# Backward fill (взять следующее)
df['temperature'].fillna(method='bfill')

Стратегия 5: Интерполяция

Когда: Временные ряды, значения меняются плавно.

# Линейная интерполяция
df['temperature'].interpolate(method='linear')

# Полиномиальная
df['sales'].interpolate(method='polynomial', order=2)

Стратегия 6: Заполнение предсказаниями модели

Когда: Сложные зависимости, важные данные.

Обучаете модель (например, KNN, Random Forest) на данных без пропусков, предсказываете пропущенные значения.

Стратегия 7: Оставить как есть

Когда: Модель/алгоритм умеет работать с пропусками (например, XGBoost, LightGBM).

Выбор стратегии: дерево решений

  1. Пропусков <5% в некритичных колонках? → Удалить

  2. Пропусков >50% в колонке? → Удалить колонку

  3. Временной ряд? → Forward/backward fill или интерполяция

  4. Категориальные данные? → Mode или "Unknown"

  5. Числовые с outliers? → Median

  6. Числовые без outliers? → Mean

  7. Сложные зависимости + критичные данные? → Модель

6. Обработка дубликатов

Дубликаты искажают анализ, завышают метрики, ломают модели.

Типы дубликатов

1. Точные дубликаты

Все поля идентичны. Просто обнаружить и удалить.

# Найти
df[df.duplicated()]

# Удалить (оставить первую запись)
df_clean = df.drop_duplicates()

# Удалить по ключевым колонкам
df_clean = df.drop_duplicates(subset=['email', 'phone'])

2. Частичные дубликаты

Часть полей совпадает, но не все.

Пример:

  • Строка 1: email=ivan@mail.ru, phone=+79123456789, name=Иван

  • Строка 2: email=ivan@mail.ru, phone=+79123456780, name=Иван

Email совпадает → вероятно один человек, но телефон разный (обновился?).

Решение: Нужно доменное знание для объединения/выбора.

3. Fuzzy дубликаты

Похожие, но не идентичные из-за опечаток.

Пример:

  • Иван Иванов

  • Иван Иваанов (опечатка)

  • ИВАН ИВАНОВ (капс)

Решение: Fuzzy matching (Levenshtein distance, soundex).

from fuzzywuzzy import fuzz

# Сходство строк (0-100)
similarity = fuzz.ratio("Иван Иванов", "Иван Иваанов")
print(similarity) # 92

# Если >90 — считаем дубликатом

Стратегии удаления дубликатов

Стратегия 1: Оставить первую запись

df.drop_duplicates(keep='first')

Стратегия 2: Оставить последнюю (самую свежую)

df.drop_duplicates(keep='last')

Стратегия 3: Объединение информации

Взять лучшее из обеих записей.

Пример:

  • Строка 1: email=ivan@mail.ru, phone=NULL

  • Строка 2: email=ivan@mail.ru, phone=+7912...

Объединить: email=ivan@mail.ru, phone=+7912...

Стратегия 4: Оставить наиболее заполненную

Если в одной записи больше заполненных полей — она "качественнее".

7. Стандартизация и нормализация

Стандартизация — приведение к единому формату

1. Текст: uppercase/lowercase

# Lowercase
df['email'] = df['email'].str.lower()

# Uppercase для кодов
df['country_code'] = df['country_code'].str.upper()

# Title case для имён
df['name'] = df['name'].str.title()

2. Удаление пробелов

# Убрать пробелы в начале/конце
df['column'] = df['column'].str.strip()

# Множественные пробелы → один
df['column'] = df['column'].str.replace(r'\s+', ' ', regex=True)

3. Даты

# Преобразовать в datetime
df['date'] = pd.to_datetime(df['date'], errors='coerce')

# Форматировать единообразно
df['date'] = df['date'].dt.strftime('%Y-%m-%d')

4. Телефоны

import re

def clean_phone(phone):
# Оставить только цифры
phone = re.sub(r'\D', '', str(phone))
# Привести к формату 79XXXXXXXXX
if phone.startswith('8'):
phone = '7' + phone[1:]
return phone

df['phone'] = df['phone'].apply(clean_phone)

5. Email

# Lowercase + trim
df['email'] = df['email'].str.lower().str.strip()

# Валидация
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
df['email_valid'] = df['email'].str.match(email_pattern)

Нормализация — приведение к единой шкале

Для числовых данных, чтобы фичи были на одной шкале (важно для ML).

1. Min-Max Scaling (0 до 1)

from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
df['age_scaled'] = scaler.fit_transform(df[['age']])

2. Standardization (Z-score)

Среднее = 0, стандартное отклонение = 1

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
df['salary_scaled'] = scaler.fit_transform(df[['salary']])

8. Работа с outliers и аномалиями

Outliers могут быть: (1) ошибками данных, (2) редкими, но валидными значениями.

Методы обнаружения

1. IQR (Interquartile Range)

Q1 = df['column'].quantile(0.25)
Q3 = df['column'].quantile(0.75)
IQR = Q3 - Q1

# Outliers: за пределами Q1-1.5*IQR и Q3+1.5*IQR
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

outliers = df[(df['column'] < lower_bound) | (df['column'] > upper_bound)]

2. Z-score

Если |Z-score| > 3 — outlier (99.7% данных в пределах ±3σ).

from scipy import stats

z_scores = stats.zscore(df['column'])
outliers = df[abs(z_scores) > 3]

3. Isolation Forest

ML алгоритм для обнаружения аномалий.

from sklearn.ensemble import IsolationForest

iso = IsolationForest(contamination=0.05)
df['outlier'] = iso.fit_predict(df[['age', 'salary']])
outliers = df[df['outlier'] == -1]

Стратегии обработки

1. Удаление

Если это явные ошибки (возраст 250, зарплата $99999999).

2. Замена на границы (capping/flooring)

# Заменить на 95-й перцентиль
upper_limit = df['salary'].quantile(0.95)
df['salary'] = df['salary'].clip(upper=upper_limit)

3. Трансформация

Log transform для уменьшения влияния outliers.

import numpy as np
df['salary_log'] = np.log1p(df['salary'])

4. Оставить как есть

Если это редкие, но валидные значения (например, миллионеры в датасете доходов). Используйте robust модели (Random Forest, XGBoost).

Решение: проверка на валидность

Outlier или реальное значение?

  • Доменное знание (возраст 150 лет = ошибка, возраст 95 = редко, но возможно)

  • Проверка других полей (если возраст 150, но дата рождения = 1990 → ошибка)

  • Сэмплинг: вручную проверить 50-100 outliers

9. Инструменты для очистки данных

Python библиотеки

1. Pandas

Базовый инструмент для работы с данными.

import pandas as pd
df = pd.read_csv('data.csv')
df.info() # Обзор типов и пропусков
df.describe() # Статистика
df.isnull().sum() # Подсчёт пропусков

2. Missingno

Визуализация пропущенных данных.

import missingno as msno
msno.matrix(df) # Матрица пропусков
msno.heatmap(df) # Корреляция пропусков

3. PyJanitor

Удобные функции для очистки.

import janitor
df_clean = (df
.clean_names() # Стандартизация имён колонок
.remove_empty() # Удаление пустых строк/колонок
.dropna(subset=['key_column'])
)

4. Great Expectations

Валидация и тестирование качества данных.

import great_expectations as ge
df_ge = ge.from_pandas(df)
df_ge.expect_column_values_to_not_be_null('email')
df_ge.expect_column_values_to_match_regex('email', r'^.+@.+$')

5. Dedupe

ML для обнаружения дубликатов.

SQL для очистки

-- Удаление дубликатов
DELETE FROM users
WHERE id NOT IN (
SELECT MIN(id) FROM users GROUP BY email
);

-- Стандартизация
UPDATE users SET email = LOWER(TRIM(email));

-- Замена NULL
UPDATE users SET city = 'Unknown' WHERE city IS NULL;

Платформы no-code/low-code

  • OpenRefine: Бесплатный инструмент для очистки, трансформации

  • Trifacta Wrangler: Визуальная подготовка данных

  • Alteryx: Enterprise решение

  • DataRobot: Автоматическая очистка для ML

Рекомендуемый стек

Для начала достаточно:

  1. Pandas (основа)

  2. Missingno (визуализация пропусков)

  3. Sklearn (нормализация, outliers)

Для продвинутых:

  1. + Great Expectations (валидация)

  2. + PyJanitor (удобство)

  3. + Dedupe (сложные дубликаты)

10. Документирование процесса очистки

Без документации очистка непрозрачна и невоспроизводима.

Что документировать

1. Исходное состояние данных

  • Количество строк/колонок

  • % пропусков по колонкам

  • Количество дубликатов

  • Типы данных

2. Каждое преобразование

Шаблон записи:

Шаг X: [Название операции]

  • Действие: Что сделали

  • Причина: Почему

  • Результат: Сколько строк затронуто/удалено

  • Код: Фрагмент кода или SQL

Пример:

Шаг 1: Удаление строк без email

  • Действие: df.dropna(subset=['email'])

  • Причина: Email — критичный идентификатор, без него запись бесполезна

  • Результат: Удалено 5,234 строк (5.2% датасета)

  • Было/стало: 100,000 → 94,766 строк

3. Итоговое состояние

  • Финальное количество строк/колонок

  • % пропусков после очистки

  • Сколько всего удалено/изменено

4. Assumptions (предположения)

Какие предположения делались при очистке.

Пример: "Предполагаем, что пропуск в колонке 'income' для молодых пользователей означает низкий доход, а не отсутствие данных".

Формат документации

Вариант 1: Markdown файл

data_cleaning_log.md рядом с кодом.

Вариант 2: Jupyter Notebook с markdown ячейками

Код + объяснения в одном файле.

Вариант 3: Комментарии в коде

# Шаг 3: Заполнение пропусков в age медианой
# Причина: Пропусков 12%, удаление нежелательно
# Median выбран т.к. есть outliers (mean искажён)
median_age = df['age'].median()
df['age'].fillna(median_age, inplace=True)
# Результат: Заполнено 12,456 значений

Воспроизводимость

Документация должна позволять кому-то другому повторить очистку и получить тот же результат.

Для этого:

  • Версионируйте код (Git)

  • Фиксируйте версии библиотек (requirements.txt)

  • Сохраняйте промежуточные состояния (checkpoints)

11. Как не потерять важную информацию

Агрессивная очистка = потеря паттернов и инсайтов.

Проблема 1: Удаление строк с пропусками

Вы удалили все строки где хотя бы одна колонка пустая. Потеряли 40% данных.

Риск: Пропуски могут быть неслучайными (MAR, MNAR). Удаление создаёт selection bias.

Решение:

  • Удаляйте только по критичным колонкам

  • Анализируйте паттерны пропусков (есть ли зависимость?)

  • Создавайте индикаторы пропусков: was_missing=True/False

# Вместо удаления — индикатор
df['age_was_missing'] = df['age'].isnull()
df['age'].fillna(df['age'].median(), inplace=True)

Проблема 2: Удаление outliers

Вы удалили всё, что выходит за 3σ. Но это могли быть редкие, но важные случаи.

Пример: VIP клиенты с высоким доходом — outliers по доходу. Удалили их → потеряли важный сегмент.

Решение:

  • Не удаляйте автоматически

  • Исследуйте outliers: это ошибки или реальные значения?

  • Создавайте отдельный флаг: is_outlier=True

  • Используйте robust методы вместо удаления

Проблема 3: Слишком агрессивная стандартизация

Вы привели "Москва", "moscow", "MSK" к "москва". Но "MSK" могло означать не Москву, а что-то другое (аэропорт, код). Потеряли нюанс.

Решение:

  • Сохраняйте оригинальную колонку

  • Создавайте новую колонку с очищенными данными

  • Проверяйте сэмпл после стандартизации

df['city_original'] = df['city'] # Сохранить оригинал
df['city_clean'] = df['city'].str.lower().str.strip()

Проблема 4: Объединение дубликатов

Два клиента с одинаковым email, но разными адресами. Вы объединили в одну запись, выбрав первый адрес. Но второй адрес мог быть актуальнее.

Решение:

  • Если есть timestamp — выбирайте по дате (последний = актуальнее)

  • Если нет — сохраняйте оба значения через разделитель

  • Создавайте отдельную таблицу для истории изменений

Правило: Сохраняйте сырые данные

Никогда не перезаписывайте исходные файлы. Структура:

  • data/raw/ — оригинальные данные (неприкасаемые)

  • data/interim/ — промежуточные состояния

  • data/processed/ — финальные очищенные данные

Правило: Делайте checkpoints

После каждого крупного шага — сохраняйте промежуточную версию.

df.to_csv('data/interim/01_after_deduplication.csv')
df.to_csv('data/interim/02_after_missing_values.csv')
df.to_csv('data/interim/03_after_standardization.csv')

Если что-то пошло не так — откатываетесь к предыдущему checkpoint.

12. Валидация после очистки

Очистили данные. Как убедиться, что всё правильно?

Проверка 1: Базовые метрики

# До и после
print("Было строк:", len(df_original))
print("Стало строк:", len(df_clean))
print("Удалено:", len(df_original) - len(df_clean))
print("% потерь:", (1 - len(df_clean)/len(df_original)) * 100)

# Пропуски
print("Пропусков до:", df_original.isnull().sum().sum())
print("Пропусков после:", df_clean.isnull().sum().sum())

Проверка 2: Распределения

Сравните распределения до и после. Если они сильно изменились — возможно, вы удалили что-то важное.

import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
df_original['age'].hist(ax=axes[0], bins=50)
axes[0].set_title('До очистки')
df_clean['age'].hist(ax=axes[1], bins=50)
axes[1].set_title('После очистки')

Проверка 3: Сэмплинг

Вручную просмотрите 50-100 случайных строк из очищенных данных.

df_clean.sample(100)

Ищите странности: неожиданные значения, паттерны, которые выглядят неправильно.

Проверка 4: Юнит-тесты на данные

Автоматические проверки инвариантов.

# Проверки
assert df_clean['age'].min() >= 0, "Возраст не может быть отрицательным"
assert df_clean['age'].max() <= 120, "Возраст слишком большой"
assert df_clean['email'].str.contains('@').all(), "Все email должны содержать @"
assert df_clean.duplicated(subset=['user_id']).sum() == 0, "Есть дубликаты user_id"

Проверка 5: Бизнес-логика

Проверьте, что данные соответствуют доменным правилам.

Примеры:

  • Дата регистрации не может быть позже даты первой покупки

  • Сумма заказа >= 0

  • Если status="отменён", то revenue=0

Проверка 6: Сравнение с benchmark

Если у вас есть эталонный датасет (золотой стандарт), сравните:

  • Accuracy = (Правильно очищенные / Всего) × 100%

13. Реальные кейсы

Кейс 1: Клиентская база e-commerce (100K записей)

Проблемы:

  • 35% пропусков в телефонах

  • 12K дубликатов

  • Email в разных форматах (капс, пробелы, опечатки)

  • Адреса несогласованные (сокращения, опечатки в городах)

Решение:

  1. Email: lowercase, trim, валидация regex → удалили 2.3K невалидных

  2. Телефоны: стандартизация до 79XXXXXXXXX → заменили пропуски на "Unknown"

  3. Дубликаты: по email+имя, оставили последнюю запись (самую свежую)

  4. Адреса: fuzzy matching городов → объединили 847 вариантов в 152 города

Результат: Датасет с 100K → 85K строк (качественных). Потеря 15%, но качество выросло с 40% до 92% (по метрике completeness + accuracy).

Кейс 2: Датасет для ML модели предсказания churn (50K пользователей)

Проблемы:

  • 20% пропусков в feature "last_login_days"

  • Outliers в "session_duration" (до 999999 минут)

  • Несбалансированность: churn=1 только у 8%

Решение:

  1. last_login_days: создали флаг was_missing, заполнили медианой

  2. session_duration: capping на 99-м перцентиле (180 минут)

  3. Несбалансированность: использовали SMOTE для oversampling minority class

  4. Не удалили ни одной строки (критично для ML)

Результат: Модель обучена на полном датасете (50K), accuracy 89%, precision/recall для churn класса улучшились за счёт правильной обработки outliers и missing values.

Кейс 3: Логи веб-сервера (10M записей)

Проблемы:

  • IP адреса с bot traffic (30% записей)

  • Timestamps в разных форматах

  • URL с query параметрами (тысячи уникальных)

Решение:

  1. Фильтрация ботов: по user-agent и списку известных bot IP

  2. Timestamps: привели к единому формату UTC

  3. URL: очистили query параметры, оставили только path

  4. Агрегация: group by path + hour → сократили до 500K записей

Результат: Из 10M записей получили 7M валидных (после фильтрации ботов) + агрегировали для анализа. Скорость обработки выросла в 20 раз.

14. Что запомнить

Грязные данные — норма, не исключение. Научиться чистить правильно — критичный навык для любого, кто работает с данными.

Ключевые выводы:

  • 40-60% времени уходит на очистку. Это не баг, это фича профессии data analyst/scientist.

  • Стоимость грязных данных огромна. $12.9M/год средний ущерб на компанию (Gartner). Неправильные решения, провальные модели, потерянное время.

  • Нет универсального рецепта. Каждый датасет уникален. Нужно доменное знание + здравый смысл + итерации.

  • Никогда не модифицируйте оригинал. Работайте с копией. Сохраняйте checkpoints. Структура: raw → interim → processed.

  • Документируйте всё. Что сделали, почему, сколько строк затронуто. Иначе очистка непрозрачна и невоспроизводима.

  • Баланс: чистота vs потеря информации. Идеально чистые данные = пустой датасет. Агрессивная очистка убивает паттерны.

  • Сохраняйте индикаторы. was_missing, was_outlier, original_value — помогают не потерять информацию.

  • Валидация обязательна. Сэмплинг, юнит-тесты, сравнение распределений. Не доверяйте слепо автоматической очистке.

«В data science нет shortcut. Хорошие модели начинаются с хороших данных. Хорошие данные начинаются с правильной очистки» — Andrew Ng, основатель Coursera

Чек-лист: очистка данных

  1. ☐ Сохранить исходные данные (raw/) без изменений

  2. ☐ Провести EDA: распределения, пропуски, outliers

  3. ☐ Оценить качество: completeness, accuracy, consistency

  4. ☐ Создать копию для работы

  5. ☐ Задокументировать каждый шаг очистки

  6. ☐ Обработать пропуски (удалить / заполнить / оставить)

  7. ☐ Найти и обработать дубликаты (точные / fuzzy)

  8. ☐ Стандартизировать форматы (email, телефоны, даты, текст)

  9. ☐ Обработать outliers (исследовать / удалить / cap / оставить)

  10. ☐ Создать индикаторы (was_missing, is_outlier)

  11. ☐ Сохранить промежуточные версии (checkpoints)

  12. ☐ Валидировать результат (сэмплинг, тесты, распределения)

  13. ☐ Сохранить финальную версию (processed/)

  14. ☐ Написать README с описанием процесса

Начните сегодня:

  1. Загрузите свой датасет в Pandas (10 минут)

  2. Запустите df.info(), df.describe(), df.isnull().sum() (5 минут)

  3. Визуализируйте пропуски через missingno (5 минут)

  4. Создайте документ data_cleaning_log.md (2 минуты)

  5. Выберите 1 проблему (пропуски / дубликаты / outliers) (1 минута)

  6. Примените одну технику из статьи (30 минут)

  7. Задокументируйте шаг (10 минут)

Главный урок: Очистка данных — это искусство баланса. С одной стороны — грязь, ошибки, шум. С другой — ценная информация, паттерны, инсайты. Ваша задача не сделать данные идеальными. Ваша задача — сделать их достаточно чистыми для анализа, не потеряв критичную информацию. Это требует внимания, доменного знания и здравого смысла. Но овладев этим навыком, вы сэкономите месяцы работы и создадите основу для качественного анализа.

А лучшие вакансии для аналитиков данных ищите на hirehi.ru