Лабораторная работа №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-й перцентиль Age ≈ 56 лет. Пассажиры старше 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) часто даёт больший прирост качества модели, чем подбор алгоритмов. - Корреляционный анализ новых признаков подтвердил их независимость и информативность.