Data Cleaning: Очистка датасета
Условие
Дан грязный датасет с:
- Пропущенными значениями
- Дубликатами
- Выбросами
- Некорректными типами данных
Напишите пайплайн очистки данных на pandas. Объясните выбор каждого метода обработки.
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Пайплайн очистки данных на Pandas
Создам полный пайплайн с объяснением каждого шага обработки грязного датасета. Это критически важный этап, так как данные низкого качества приводят к неверным выводам.
Загрузка и инициальная диагностика
import pandas as pd
import numpy as np
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
# Загрузка датасета
df = pd.read_csv('dirty_data.csv')
# Инициальная диагностика
print(f"Размер датасета: {df.shape}")
print(f"\nТипы данных:\n{df.dtypes}")
print(f"\nПропущенные значения:\n{df.isnull().sum()}")
print(f"\nПроцент пропусков:\n{df.isnull().sum() / len(df) * 100}")
print(f"\nОсновная статистика:\n{df.describe()}")
# Проверка дубликатов
print(f"\nДубликаты: {df.duplicated().sum()}")
Почему это важно: Диагностика показывает масштаб проблем и помогает выбрать оптимальные методы обработки.
Обработка типов данных
def fix_data_types(df):
"""
Исправляет некорректные типы данных
"""
df_clean = df.copy()
# Пример 1: Столбец с числами, сохранёнными как строки
if 'price' in df_clean.columns:
# Удаляем символы валюты и преобразуем в float
df_clean['price'] = (df_clean['price']
.str.replace('$', '')
.str.replace(',', '')
.astype(float))
# Пример 2: Столбец с датами
if 'date' in df_clean.columns:
df_clean['date'] = pd.to_datetime(df_clean['date'],
errors='coerce')
# Пример 3: Категориальные столбцы
if 'category' in df_clean.columns:
df_clean['category'] = df_clean['category'].astype('category')
# Пример 4: Булевы значения
if 'is_active' in df_clean.columns:
df_clean['is_active'] = (df_clean['is_active']
.map({'Yes': True, 'No': False,
'true': True, 'false': False})
.astype(bool))
return df_clean
df = fix_data_types(df)
print("✓ Типы данных исправлены")
Обоснование: Неправильные типы препятствуют анализу. Например, число как строка не поддерживает математические операции.
Обработка пропущенных значений
def handle_missing_values(df):
"""
Обрабатывает пропущенные значения в зависимости от типа столбца
"""
df_clean = df.copy()
# Метод 1: Удаление строк с пропусками в критических столбцах
critical_cols = ['id', 'date', 'amount']
df_clean = df_clean.dropna(subset=critical_cols, how='any')
print(f"Удалено {len(df) - len(df_clean)} строк с пропусками в критических столбцах")
# Метод 2: Заполнение среднего для числовых столбцов
numeric_cols = df_clean.select_dtypes(include=[np.number]).columns
for col in numeric_cols:
if df_clean[col].isnull().sum() > 0:
# Если < 10% пропусков, используем медиану (устойчива к выбросам)
if df_clean[col].isnull().sum() / len(df_clean) < 0.1:
median_val = df_clean[col].median()
df_clean[col].fillna(median_val, inplace=True)
print(f" {col}: заполнено медианой ({median_val})")
else:
# Если > 10%, столбец слишком неполный
print(f" {col}: слишком много пропусков ({df_clean[col].isnull().sum()}), удалить столбец?")
# Метод 3: Заполнение для категориальных столбцов
categorical_cols = df_clean.select_dtypes(include='category').columns
for col in categorical_cols:
if df_clean[col].isnull().sum() > 0:
mode_val = df_clean[col].mode()[0] if len(df_clean[col].mode()) > 0 else 'Unknown'
df_clean[col].fillna(mode_val, inplace=True)
print(f" {col}: заполнено модой ({mode_val})")
# Метод 4: Forward fill для временных рядов
if 'date' in df_clean.columns:
df_clean = df_clean.sort_values('date')
df_clean = df_clean.fillna(method='ffill', limit=3) # Максимум 3 последовательных заполнений
return df_clean
df = handle_missing_values(df)
print("\n✓ Пропущенные значения обработаны\n")
Обоснование:
- Удаление: Для критических столбцов, где пропуск = потеря данных
- Медиана: Для числовых - устойчива к выбросам (vs среднее)
- Мода: Для категориальных - наиболее частое значение
- Forward fill: Для временных рядов - логично заполнить предыдущим значением
Обработка дубликатов
def remove_duplicates(df):
"""
Удаляет дубликаты с сохранением логики
"""
df_clean = df.copy()
# Полные дубликаты всех столбцов
full_dupes = df_clean[df_clean.duplicated(keep='first')]
print(f"Полные дубликаты: {len(full_dupes)}")
df_clean = df_clean.drop_duplicates(keep='first')
# Дубликаты по ID (если есть)
if 'id' in df_clean.columns:
id_dupes = df_clean[df_clean['id'].duplicated(keep='first')]
print(f"Дубликаты по ID: {len(id_dupes)}")
# Оставляем первое появление, считаем остальные опечатками
df_clean = df_clean.drop_duplicates(subset=['id'], keep='first')
# Дубликаты по ключевым полям
key_cols = ['date', 'user_id', 'transaction_id']
available_keys = [col for col in key_cols if col in df_clean.columns]
if available_keys:
df_clean = df_clean.drop_duplicates(subset=available_keys, keep='first')
print(f"Удалено дубликатов по ключевым полям: {len(df) - len(df_clean)}")
return df_clean
df = remove_duplicates(df)
print("\n✓ Дубликаты удалены\n")
Обоснование: Дубликаты смещают статистику. Параметр keep='first' предполагает, что первое появление - оригинал.
Обработка выбросов
def remove_outliers(df):
"""
Обнаруживает и обрабатывает выбросы
"""
df_clean = df.copy()
numeric_cols = df_clean.select_dtypes(include=[np.number]).columns
for col in numeric_cols:
# Метод 1: IQR (Inter-Quartile Range) - для большинства случаев
Q1 = df_clean[col].quantile(0.25)
Q3 = df_clean[col].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = df_clean[(df_clean[col] < lower_bound) | (df_clean[col] > upper_bound)]
if len(outliers) > 0:
print(f"{col}:")
print(f" Границы: [{lower_bound:.2f}, {upper_bound:.2f}]")
print(f" Выбросов: {len(outliers)} ({len(outliers)/len(df_clean)*100:.1f}%)")
# Вариант 1: Удалить (если < 5% выбросов)
if len(outliers) / len(df_clean) < 0.05:
df_clean = df_clean[(df_clean[col] >= lower_bound) & (df_clean[col] <= upper_bound)]
print(f" Действие: удалены")
else:
# Вариант 2: Заменить граничными значениями (winsorize)
df_clean[col] = df_clean[col].clip(lower=lower_bound, upper=upper_bound)
print(f" Действие: обрезаны до границ")
# Метод 2: Z-score для нормально распределённых данных
# z_scores = np.abs(stats.zscore(df_clean[col].dropna()))
# if (z_scores > 3).sum() > 0:
# print(f" Z-score выбросы (>3σ): {(z_scores > 3).sum()}")
return df_clean
df = remove_outliers(df)
print("\n✓ Выбросы обработаны\n")
Обоснование:
- IQR метод: Работает с любым распределением, менее чувствителен к экстремумам
- Z-score: Только для нормального распределения
- Удаление vs обрезка: Удаляем редкие выбросы, обрезаем частые
Дополнительная валидация
def validate_data(df):
"""
Финальная валидация данных
"""
print("=== ФИНАЛЬНАЯ ВАЛИДАЦИЯ ===\n")
# Проверка логических ограничений
if 'price' in df.columns:
negative_prices = (df['price'] < 0).sum()
print(f"Отрицательные цены: {negative_prices}")
if negative_prices > 0:
df = df[df['price'] >= 0]
if 'age' in df.columns:
invalid_ages = ((df['age'] < 0) | (df['age'] > 150)).sum()
print(f"Невалидные возрасты: {invalid_ages}")
if invalid_ages > 0:
df = df[(df['age'] >= 0) & (df['age'] <= 150)]
# Проверка диапазонов дат
if 'date' in df.columns:
future_dates = (df['date'] > pd.Timestamp.now()).sum()
print(f"Даты в будущем: {future_dates}")
# Финальная статистика
print(f"\nФинальный размер: {df.shape[0]} строк, {df.shape[1]} столбцов")
print(f"Удалено: {len(df) - df.shape[0]} строк ({(len(df) - df.shape[0])/len(df)*100:.1f}%)")
print(f"Пропусков: {df.isnull().sum().sum()}")
return df
df = validate_data(df)
Итоговый пайплайн в одной функции
def clean_data_pipeline(df):
"""
Полный пайплайн очистки данных
"""
df = fix_data_types(df)
df = handle_missing_values(df)
df = remove_duplicates(df)
df = remove_outliers(df)
df = validate_data(df)
return df
# Применение пайплайна
df_clean = clean_data_pipeline(df)
# Сохранение
df_clean.to_csv('clean_data.csv', index=False)
print("\n✓ Очищенные данные сохранены в clean_data.csv")
Ключевые выводы
- Порядок важен: Сначала типы, потом пропуски, потом дубликаты, потом выбросы
- Контекст матчас: Методы зависят от типа данных и домена
- Документация: Всегда логируй какие строки удалены и почему
- Баланс: Удаление vs заполнение — выбирается по доле пропусков
- Валидация: Финальная проверка на логические ошибки
Этот пайплайн обеспечивает высокое качество данных для последующего анализа.