Лабораторная работа №6

Очистка и трансформация данных с использованием pandas


Введение

Качество данных напрямую определяет качество любой аналитической или ML-модели. Реальные датасеты практически всегда содержат пропуски, выбросы, некорректные типы и избыточные признаки. Умение обнаруживать и устранять эти проблемы — базовый навык data scientist'а.

Цель работы

Освоить методы очистки и трансформации табличных данных с использованием библиотеки pandas на примере датасета Titanic: научиться обрабатывать пропуски, преобразовывать типы данных, выявлять и устранять выбросы, создавать производные признаки и проводить агрегацию.


Описание набора данных

Датасет: Titanic — Kaggle

Датасет содержит информацию о 891 пассажире крушения «Титаника». Задача — предсказать выживаемость, но в данной работе акцент сделан на предобработке данных.

Признак Тип Описание
PassengerId int Идентификатор пассажира
Survived int Выжил (1) / не выжил (0)
Pclass int Класс билета (1, 2, 3)
Name str Имя пассажира
Sex str Пол
Age float Возраст (есть пропуски)
SibSp int Кол-во братьев/сестёр/супругов на борту
Parch int Кол-во родителей/детей на борту
Ticket str Номер билета
Fare float Стоимость билета
Cabin str Номер каюты (много пропусков)
Embarked str Порт посадки: S, C, Q

1. Первичный анализ данных

1.1. Загрузка и просмотр данных

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np

df = pd.read_csv('train.csv')
count_na_b = df.isna().sum()  # сохраняем пропуски ДО обработки для метрик
df.head(10)

Первые 10 строк показали разнородность данных: в Name содержится звание (Mr., Mrs. и т.д.), Cabin заполнен лишь частично.

1.2. Статистика датафрейма

df.info()
df.describe().T

Результаты info(): - Всего строк: 891 - Age: заполнено 714 из 891 (177 пропусков, ~19.9%) - Cabin: заполнено только 204 из 891 (~77% пропусков) - Embarked: 2 пропуска

Ключевые статистики (describe()):

Признак Mean Std Min Max
Age 29.70 14.53 0.42 80.0
Fare 32.20 49.69 0.00 512.3
SibSp 0.52 1.10 0 8
Parch 0.38 0.81 0 6
Survived 0.38 0 1

Обратим внимание на большой разброс Fare (std ≈ 50 при mean ≈ 32) — признак явно содержит выбросы.

1.3. Визуализация распределений

cols_to_plot = ['Survived', 'Pclass', 'Age', 'SibSp', 'Parch', 'Fare']

fig, axes = plt.subplots(2, 3, figsize=(12, 8))
axes = axes.flatten()

for i, col in enumerate(cols_to_plot):
    sns.histplot(data=df, x=col, bins=30, ax=axes[i])
    mean = df[col].mean()
    axes[i].axvline(mean, color='red', label='Mean')
    axes[i].legend()

plt.tight_layout()
plt.show()

Наблюдения по графикам: - Survived: дисбаланс — примерно 38% выживших и 62% погибших - Age: близкое к нормальному распределение, пик в районе 20–30 лет - Fare: сильно правосторонняя асимметрия (большинство платили мало, единицы — очень много) - SibSp, Parch: большинство пассажиров путешествовали без родственников

1.4. Визуализация пропусков

df.isna().sum()
Признак Пропусков % от всех
Cabin 687 77.1%
Age 177 19.9%
Embarked 2 0.2%

Столбец Cabin имеет критически большое количество пропусков — полное восстановление невозможно.


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

2.1. Признак Age

mean_val_age = df['Age'].mean()   # 29.70
# медиана: df['Age'].median() == 28.0
df['Age'] = df['Age'].fillna(mean_val_age)

Выбрано заполнение средним значением (можно также использовать медиану — она устойчивее к выбросам). После заполнения в Age не осталось пропусков.

Создание нового признака Age_group:

df['Age_group'] = np.select(
    [df['Age'] <= 21, df['Age'] <= 44, df['Age'] <= 59, df['Age'] >= 60],
    ["teenager", "young adult", "middle-aged", "elderly"],
    default=None
)

Разбивка возрастных групп:

Age_group Диапазон
teenager ≤ 21 лет
young adult 22–44 лет
middle-aged 45–59 лет
elderly ≥ 60 лет

2.2. Признак Embarked

df['Embarked'] = df['Embarked'].fillna(df['Embarked'].mode()[0])

Наиболее часто встречающийся порт посадки — S (Southampton). Именно им заполнены два пропуска. Это стандартная стратегия для категориальных признаков с малым числом пропусков.

2.3. Признак Cabin

У 77% пассажиров каюта неизвестна — удалить столбец было бы потерей информации о структуре (палубе). Вместо этого создан новый признак — первая буква номера каюты, обозначающая палубу:

df['Cabin_class'] = df['Cabin'].str[0]
df['Cabin_class'] = df['Cabin_class'].fillna("None")

Пассажиры без каюты получили метку "None", которая сама по себе несёт информацию (скорее всего, пассажиры 3-го класса).

2.4. Результаты обработки пропусков

После всех операций:

df.isna().sum()
# Age: 0, Embarked: 0, Cabin_class: 0

Все критические пропуски устранены. Исходный столбец Cabin оставлен для справки.


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

3.1. Преобразование Pclass в строковые категории

df['Pclass'] = np.select(
    [df['Pclass'] == 1, df['Pclass'] == 2, df['Pclass'] == 3],
    ['F', 'S', 'T'],
    default=None
)

Кодировка: 1 → F (First), 2 → S (Second), 3 → T (Third).

3.2. Извлечение обращения (Title) из имени

for i, Name in enumerate(df['Name']):
    Name = Name.split(' ')
    for j in Name:
        if j.endswith('.'):
            rank = j
    df.loc[i, 'title'] = rank

Наиболее частые обращения в датасете:

Title Количество
Mr. 517
Miss. 182
Mrs. 125
Master. 40
Dr., Rev., Col. и др. 27

Признак title несёт информацию о поле, возрасте и социальном статусе одновременно.

3.3. Бинаризация пола

df['Sex'] = np.where(df['Sex'] == 'male', 1, 0)
# male -> 1, female -> 0

3.4. Признаки размера семьи

df['FamilySize'] = df['SibSp'] + df['Parch'] + 1
df['IsAlone'] = np.where(df['FamilySize'] == 1, 1, 0)

FamilySize = количество членов семьи на борту (включая самого пассажира). IsAlone = 1 означает, что пассажир путешествовал без родственников.

Из 891 пассажира 537 (60.3%) путешествовали в одиночестве.

3.5. Итог по созданным признакам

Новый признак Источник Описание
Age_group Age Возрастная группа
Cabin_class Cabin Палуба (первая буква каюты)
title Name Обращение (Mr., Mrs. и т.д.)
FamilySize SibSp + Parch Размер семьи на борту
IsAlone FamilySize Путешествовал ли один

4. Обработка выбросов

4.1. Boxplot и IQR для Fare

sns.boxplot(df['Fare'])

Boxplot показал наличие множества экстремально высоких значений (до 512 у.е. при медиане ~14.4).

Метод IQR (Interquartile Range):

IQR = df['Fare'].quantile(0.75) - df['Fare'].quantile(0.25)
# IQR ≈ 31.0
lowest  = df['Fare'].quantile(0.25) - IQR * 1.5   # ≈ -31.3 → граница = 0
highest = df['Fare'].quantile(0.75) + IQR * 1.5   # ≈ 65.6

df['Fare'] = df['Fare'].clip(lowest, highest)

Все значения выше ~65.6 заменены на это граничное значение (winsorization). Нижняя граница отрицательная, поэтому минимальное значение осталось 0.

После обработки boxplot стал компактным — выбросы устранены.

4.2. Выбросы Age — обрезка по 95-му перцентилю

sns.histplot(df['Age'])  # до обработки
df['Age'] = df['Age'].clip(upper=df['Age'].quantile(0.95))
sns.histplot(df['Age'])  # после обработки

95-й перцентиль Age56 лет. Пассажиры старше 56 получили значение 56. Распределение стало симметричнее, хвост справа исчез.

4.3. Итог по выбросам

Признак Метод Результат
Fare IQR + clip Верхняя граница ~65.6
Age Перцентиль 95% Верхняя граница ~56 лет

5. Агрегация и анализ

5.1. Средняя выживаемость по классам

df.groupby('Pclass')['Survived'].mean()
Класс Выживаемость
F (1-й) ~0.63 (63%)
S (2-й) ~0.47 (47%)
T (3-й) ~0.24 (24%)

Пассажиры 1-го класса выживали в 2.6 раза чаще пассажиров 3-го. Класс билета — сильный предиктор выживаемости.

5.2. Группировка по классу и полу

df.sort_values(['Pclass', 'Sex'])

Данные отсортированы для наглядности: внутри каждого класса хорошо видна разница между мужчинами (Sex=1) и женщинами (Sex=0).

5.3. Медианный возраст по портам посадки

df.groupby('Embarked')['Age'].median()
Порт Медианный возраст
C (Cherbourg) ~29.0 лет
Q (Queenstown) ~22.0 лет
S (Southampton) ~28.0 лет

Пассажиры из Квинстауна в среднем моложе — вероятно, больше эмигрантов третьего класса.

5.4. Сводная таблица выживаемости

df.pivot_table(
    columns='Age_group',
    values='Survived',
    index='FamilySize',
    fill_value=0
)

Таблица показывает, как выживаемость меняется в зависимости от возрастной группы и размера семьи. Пассажиры средних возрастных групп с семьями размером 2–4 человека имели наилучшие шансы.

5.5. Сохранение очищенных данных

df.to_csv('titanic_cleaned.csv', index=False)

6. Метрики качества очистки данных

6.1. Процент заполненных пропусков

count_na_e = df.isna().sum()
(count_na_b - count_na_e) * 100 / count_na_b
Признак Было пропусков Заполнено (%)
Age 177 100%
Embarked 2 100%
Cabin 687 — (создан Cabin_class)

6.2. Уникальные значения в категориальных признаках

s = df.select_dtypes(include='object').nunique()
s[s < 20]
Признак Уникальных значений
Pclass 3
Embarked 3
Age_group 4
Cabin_class 9

6.3. Корреляции между новыми признаками

Категориальные признаки (Spearman):

encoded_cat = df[['title', 'Cabin_class', 'Age_group']].apply(
    lambda x: x.astype('category').cat.codes
)
encoded_cat.corr(method='spearman')

Умеренная корреляция между title и Age_group (~0.4) — ожидаемо, так как обращение отчасти определяет возраст (Master = мальчики, Mr. = взрослые мужчины).

Числовые признаки (Pearson):

encoded_num = df[['FamilySize', 'Fare']]
encoded_num.corr(method='pearson')

Слабая положительная корреляция FamilySize и Fare (~0.2): семьи платили больше суммарно, но не кратно.

Fare и Survived:

df[['Fare', 'Survived']].corr()

Положительная корреляция (~0.26): пассажиры с более дорогими билетами выживали чаще (косвенно отражает класс каюты и расположение относительно шлюпок).


Заключение

Результаты работы

В ходе работы с датасетом Titanic были выполнены следующие шаги:

Первичный анализ показал наличие пропусков в трёх признаках (Age — 20%, Cabin — 77%, Embarked — 0.2%), а также правостороннюю асимметрию распределения Fare.

Обработка пропусков: Age заполнен средним значением, Embarked — модой (S), Cabin преобразован в признак Cabin_class (палуба).

Трансформация данных: создано 5 новых информативных признаков: Age_group, Cabin_class, title, FamilySize, IsAlone. Числовые и категориальные признаки приведены к нужным типам.

Обработка выбросов: Fare обработан методом IQR, Age обрезан по 95-му перцентилю. Распределения стали значительно компактнее.

Агрегация: подтверждена высокая зависимость выживаемости от класса билета (63% vs 24%) и порта посадки.

Выводы

  • Pandas предоставляет полный набор инструментов для профессиональной предобработки данных.
  • Стратегия работы с пропусками зависит от типа признака: среднее/медиана для числовых, мода для категориальных, создание новых признаков — при высокой доле пропусков.
  • Feature engineering (создание FamilySize, title) часто даёт больший прирост качества модели, чем подбор алгоритмов.
  • Корреляционный анализ новых признаков подтвердил их независимость и информативность.

Ссылки