← Назад к вопросам

Data Cleaning: Очистка датасета

2.0 Middle🔥 191 комментариев
#Python#Машинное обучение

Условие

Дан грязный датасет с:

  • Пропущенными значениями
  • Дубликатами
  • Выбросами
  • Некорректными типами данных

Напишите пайплайн очистки данных на pandas. Объясните выбор каждого метода обработки.

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Пайплайн очистки данных на 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")

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

  1. Порядок важен: Сначала типы, потом пропуски, потом дубликаты, потом выбросы
  2. Контекст матчас: Методы зависят от типа данных и домена
  3. Документация: Всегда логируй какие строки удалены и почему
  4. Баланс: Удаление vs заполнение — выбирается по доле пропусков
  5. Валидация: Финальная проверка на логические ошибки

Этот пайплайн обеспечивает высокое качество данных для последующего анализа.

Data Cleaning: Очистка датасета | PrepBro