Набор данных Titanic — один из лучших наборов данных для практики очистки данных и разработки функций. Это простой набор данных с очень богатой историей. Процесс очистки данных очень важен и является одним из самых трудоемких в анализе данных. Этот набор данных изначально был получен с конкурса Kaggle (Титаник — машинное обучение от катастрофы). Здесь я продемонстрирую некоторые методы очистки набора данных Титаника и выполнения проектирования признаков с целью применить его к машинному обучению с помощью древовидных моделей. Поскольку этапы обработки данных длинные, я расскажу о применении машинного обучения к этой проблеме в другой статье (ссылку добавлю сюда). Легче начать изучение машинного обучения с чистым набором данных, готовым к входным правилам большинства моделей. В реальном мире нам приходится выполнять большую обработку данных, прежде чем применять алгоритмы машинного обучения, и обычно обработка данных занимает больше времени, чем часть моделирования машинного обучения. Разработка признаков может улучшить набор данных, выявив новые переменные, которые могут помочь в исследовательском анализе, понять отношения между переменными и повлиять на окончательные прогнозы с помощью любого алгоритма ML. Чем больше мы погружаемся в набор данных Титаника, чтобы очистить его, тем больше интересной информации мы находим скрытой в грязных неструктурированных столбцах таблицы. коды и наборы данных, используемые в этой статье, размещены в моем репозитории GitHub.

Начнем с импорта необходимых библиотек.

# Basic imports
import numpy as np
import pandas as pd
import re
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
import plotly.graph_objs as go
from ipywidgets import interact

Загрузить данные

train = pd.read_csv("titanic_datasets/train.csv")
test = pd.read_csv("titanic_datasets/test.csv")

# Contatenate dataset
df = pd.concat([train, test], ignore_index = True, sort = False)
df.head()

Краткое описание переменных в этом наборе данных представлено ниже. Для получения более подробной информации обратитесь к источнику на Kaggle: Titanic-Machine Learning from Disaster. Эти переменные предоставляют информацию о различных аспектах пассажиров Титаника, таких как их демографические данные, семейные отношения, детали билетов и результат выживания.

Проверьте отсутствующие значения и типы данных

nan_counts = df.isna().sum().sort_values(ascending = False)
summary = pd.concat([df.info(), nan_counts], axis=0)
print(summary)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1309 entries, 0 to 1308
Data columns (total 12 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   PassengerId  1309 non-null   int64  
 1   Survived     891 non-null    float64
 2   Pclass       1309 non-null   int64  
 3   Name         1309 non-null   object 
 4   Sex          1309 non-null   object 
 5   Age          1046 non-null   float64
 6   SibSp        1309 non-null   int64  
 7   Parch        1309 non-null   int64  
 8   Ticket       1309 non-null   object 
 9   Fare         1308 non-null   float64
 10  Cabin        295 non-null    object 
 11  Embarked     1307 non-null   object 
dtypes: float64(3), int64(4), object(5)
memory usage: 122.8+ KB
Cabin          1014
Survived        418
Age             263
Embarked          2
Fare              1
PassengerId       0
Pclass            0
Name              0
Sex               0
SibSp             0
Parch             0
Ticket            0
dtype: int64

Этапы обработки данных

  1. Сопоставьте «Sex» с «is_male», чтобы использовать эту переменную на графиках корреляции и в моделях ML.
  2. Сопоставьте Embarqued с Embarqued_code{1,2,3}. зарезервируйте ноль для NaN.
  3. Разделите переменную «Ticket» на «Ticket_preffix» и «Ticket_n».
  4. Сгруппируйте повторяющиеся значения Ticket и подсчитайте количество людей, которые путешествовали вместе, включая друзей, горничных и нянь, и создайте переменную Companies.
  5. Создайте переменную «FamilySize», объединив переменные «SibSp» и «Parch».
  6. Извлеките переменную «Название» из переменной «Имя» и классифицируйте.
  7. Сопоставьте «Fare» с «Fare_level»: {1,2,3,4,5,6,7,8,9,10}
  8. Самая грязная переменная «Cabin» -> разбить ее на [‘Cabin_label’, ‘Cabin_n’, ‘n_of_Cabins’].
  9. Сопоставьте переменную «Возраст» с «Возрастной_группой».

Секс

Закодируйте переменную Пол в Is_male, 1 для мужчины, 0 для женщины. Этот шаг необходим, потому что мы планируем использовать алгоритмы машинного обучения на основе дерева решений scikit-learn, и он требует кодирования категориальных переменных (см. scikit-learn 1.3.0: Дерево решений).

# Mapping Sex bolean
df['Is_male'] = df['Sex'].map( {'female': 0, 'male': 1} ).astype(int)

Отправился

Очистите переменную для порта посадки «Embarked». Сначала мы заполняем только два значения NaN истинными значениями, которые мы нашли в Google, а затем кодируем переменную в числовые значения. Значения C = Шербур, Q = Квинстаун и S = ​​Саутгемптон будут сопоставлены с 1 = C, 2 = Q и 3 = S.

# Check the entries with NaN vales.
df.loc[df['Embarked'].isna(),:]

Мы видим, что оба пассажира имеют общие значения переменных, Пол = женский, Pclass = 1, они были в одной и той же кабине = B28, и номер билета один и тот же Ticket = 113572. Как вы думаете, это совпадение? Думаю, нет. Миссис Стоун села на Титаник в Саутгемптоне 10 апреля 1912 года и путешествовала первым классом со своей горничной Амели Икард. Она занимала каюту Б-28 (см. Энциклопедию Титаника).

# Fill the NaN and map Embarked to numerical codes
df.loc[df['Embarked'].isna(), 'Embarked'] = 'S'  # (Encyclopedia titanica)
df['Embarked_code'] = df['Embarked'].map( {'C': 1, 'Q': 2, 'S': 3} ).astype(int)

Билет

Разделите «Билет» на переменные «Префикс_Билета» и «Номер_Билета».

# split the column 'Ticket'
Ticket_split = []
Ticket_len = []
for i in df['Ticket'].index:
    splitted = df.loc[i,'Ticket'].split(' ')
    Ticket_split.append(splitted)
    length = len(splitted)
    Ticket_len.append(length)
    
df['Ticket_split'] = Ticket_split
df['Ticket_len'] = Ticket_len

# df[['Ticket_len']].value_counts()
# df.loc[df['Ticket_len'] > 2, :] We can see tome typo errors.

# Create columns Ticket_preffix and Ticket_number
ticket_preffix = []
ticket_number = []
for i in df['Ticket'].index:
    ticket_parts = df.loc[i, 'Ticket_split']
    number = ticket_parts[-1]
    ticket_number.append(number)
    length = df.loc[i, 'Ticket_len'].item()
    if length > 1:
        preffix = ''.join(ticket_parts[0:-1])
        ticket_preffix.append(preffix)
    else:
        ticket_preffix.append('blanck')
        
df['Ticket_preffix'] = ticket_preffix
df['Ticket_number'] = ticket_number
# drop unnecessary columns created
df.drop(columns=['Ticket_split', 'Ticket_len'], inplace=True)

df.Ticket_preffix.unique()
array(['A/5', 'PC', 'STON/O2.', 'blanck', 'PP', 'A/5.', 'C.A.', 'A./5.',
       'SC/Paris', 'S.C./A.4.', 'A/4.', 'CA', 'S.P.', 'S.O.C.', 'SO/C',
       'W./C.', 'SOTON/OQ', 'W.E.P.', 'A4.', 'C', 'SOTON/O.Q.',
       'SC/PARIS', 'S.O.P.', 'A.5.', 'Fa', 'CA.', 'F.C.C.', 'W/C',
       'SW/PP', 'SCO/W', 'P/PP', 'SC', 'SC/AH', 'A/S', 'SC/AHBasle',
       'A/4', 'WE/P', 'S.W./PP', 'S.O./P.P.', 'F.C.', 'SOTON/O2',
       'S.C./PARIS', 'C.A./SOTON', 'SC/A.3', 'STON/OQ.', 'SC/A4', 'AQ/4',
       'A.2.', 'LP', 'AQ/3.'], dtype=object)

Мы можем визуализировать разнообразие значений префиксов билетов и попытаться найти связь с портом посадки.

df_grouped = df.groupby(['Ticket_preffix', 'Embarked']).agg({'Survived': ['sum', lambda x: x.count() - x.sum()]})
df_grouped.columns = ['Survived', 'Non-Survived']
df_grouped['Count'] = df.groupby(['Ticket_preffix', 'Embarked']).size()
print(df_grouped)
                            Survived  Non-Survived  Count
Ticket_preffix Embarked                               
A./5.          S              0.0           2.0      3
A.2.           S              0.0           0.0      1
A.5.           S              0.0           2.0      3
A/4            S              0.0           3.0      6
A/4.           S              0.0           3.0      3
A/5            S              1.0           9.0     12
A/5.           Q              0.0           1.0      1
               S              1.0           5.0      9
A/S            S              0.0           1.0      1
A4.            S              0.0           1.0      1
AQ/3.          Q              0.0           0.0      1
AQ/4           Q              0.0           0.0      1
C              S              2.0           3.0      8
C.A.           C              0.0           0.0      1
               S             13.0          14.0     45
C.A./SOTON     S              0.0           1.0      1
CA             S              0.0           6.0     10
CA.            S              1.0           7.0     12
F.C.           C              0.0           0.0      1
               S              0.0           1.0      2
F.C.C.         S              4.0           1.0      9
Fa             S              0.0           1.0      1
LP             S              0.0           0.0      1
P/PP           C              1.0           1.0      2
PC             C             29.0          17.0     72
               S             10.0           4.0     20
PP             S              2.0           1.0      4
S.C./A.4.      S              0.0           1.0      1
S.C./PARIS     C              1.0           1.0      3
S.O./P.P.      S              0.0           3.0      7
S.O.C.         S              0.0           5.0      7
S.O.P.         S              0.0           1.0      1
S.P.           S              0.0           1.0      1
S.W./PP        S              1.0           0.0      1
SC             C              1.0           0.0      1
               S              0.0           0.0      1
SC/A.3         C              0.0           0.0      1
SC/A4          S              0.0           0.0      1
SC/AH          S              1.0           1.0      4
SC/AHBasle     C              1.0           0.0      1
SC/PARIS       C              2.0           3.0     10
               S              0.0           0.0      1
SC/Paris       C              2.0           2.0      5
SCO/W          S              0.0           1.0      1
SO/C           S              1.0           0.0      1
SOTON/O.Q.     S              1.0           7.0     16
SOTON/O2       S              0.0           2.0      3
SOTON/OQ       S              1.0           6.0      8
STON/O2.       S              8.0          10.0     21
STON/OQ.       S              0.0           0.0      1
SW/PP          S              1.0           0.0      1
W./C.          S              1.0           8.0     14
W.E.P.         S              0.0           1.0      2
W/C            S              0.0           1.0      1
WE/P           S              1.0           1.0      2
blanck         C             56.0          51.0    173
               Q             30.0          46.0    120
               S            169.0         313.0    668

Столбец «Tiket_prefix» содержит некоторые опечатки и, похоже, связан с информацией «Отправлено». Большинство значений пустые, и, возможно, не стоит анализировать вероятность выживания с помощью модели ML.

Компаньоны

Более того, мы уже заметили, что в переменной Ticket_number есть дублирующиеся записи. Некоторые пассажиры делят номер билета, потому что они вместе. У них могут быть отношения с сотрудником или службой, такие как горничная или няня, или они могут быть близкими друзьями. Не все эти отношения были зафиксированы в переменных SibSp и Parch. См., например, случай с миссис Стоун, которая путешествовала со своей служанкой Амели Икард, и обе выжили. (Энциклопедия Титаника).

Мы можем извлечь неявную информацию из повторяющихся значений в переменной «Билет» и создать новую переменную для ее описания. Здесь мы создаем новую переменную Companies, описывающую количество спутников или попутчиков для каждого человека. Возможно, это число оказывает некоторое влияние на вероятность выживания, когда мы используем алгоритм ML для прогнозирования значений «выживших» в тестовом наборе данных.

n_alone , n_duplicates = df['Ticket'].duplicated(keep=False).astype(int).value_counts()
print("Number of duplicated ticket values:", n_duplicates)
print("Number of non-duplicated ticket values:", n_alone)
Number of duplicated ticket values: 596
Number of non-duplicated ticket values: 713
df['Companions'] = df['Ticket'].duplicated(keep=False).astype(int) * df.groupby('Ticket')['Ticket'].transform('count') -1 
df.loc[df['Companions'] == -1, 'Companions'] = 0

Мы можем проверить распределение количества компаньонов.

df['Companions'].value_counts()
0     713
1     264
2     147
3      64
4      35
6      35
5      24
7      16
10     11
Name: Companions, dtype: int64df['Ticket_number'].duplicated(keep=False).astype(int).value_counts()
# (df.groupby('Ticket')['Ticket'].transform('count') - 1)

Семейный размер

Новая переменная «Размер семьи» создается из суммы переменных «SibSp» и «Parch» для описания структуры семьи на борту для каждого пассажира. Мы создаем коррелированную переменную, почему? Мы можем исследовать частоту «выживших» в зависимости от общего числа членов семьи. Более того, древовидные модели имеют внутренний алгоритм выбора признаков, который выбирает наиболее релевантные переменные для предсказания целевой переменной. Возможно, переменная «Численность семьи» может быть важнее, чем переменные «СибСп» и «Парч».

df['FamilySize'] = df['SibSp'] + df['Parch']
print(df[['SibSp','Parch','FamilySize', 'Survived']].groupby(['FamilySize'], as_index=False).mean().sort_values(by = 'Survived'))
FamilySize     SibSp     Parch  Survived
7           7  4.000000  3.000000  0.000000
8          10  6.727273  3.272727  0.000000
5           5  2.800000  2.200000  0.136364
4           4  2.000000  2.000000  0.200000
0           0  0.000000  0.000000  0.303538
6           6  3.250000  2.750000  0.333333
1           1  0.778723  0.221277  0.552795
2           2  0.893082  1.106918  0.578431
3           3  1.302326  1.697674  0.7241383.5

Новая переменная «Заголовок»

Теперь мы извлечем информацию о заголовке пассажира из переменной Имя. Мы можем создать функцию Заголовок, извлекая из столбца Имя заголовки: Мистер Мисс, Миссис Доктор и другие. (см. также Сина — лучший рабочий классификатор Тинатика)

# get the 'Title'
def get_title(name):
 title_search = re.search(' ([A-Za-z]+)\.', name)
 # If the title exists, extract and return it.
 if title_search:
  return title_search.group(1)
 return ""

df['Title'] = df['Name'].apply(get_title)

df_grouped = df.groupby(['Title', 'Sex']).agg({'Survived': ['sum', lambda x: x.count() - x.sum()]})
df_grouped.columns = ['Survived', 'Non-Survived']
df_grouped['Count'] = df.groupby(['Title', 'Sex']).size()
print(df_grouped)
Survived  Non-Survived  Count
Title    Sex                                  
Capt     male         0.0           1.0      1
Col      male         1.0           1.0      4
Countess female       1.0           0.0      1
Don      male         0.0           1.0      1
Dona     female       0.0           0.0      1
Dr       female       1.0           0.0      1
         male         2.0           4.0      7
Jonkheer male         0.0           1.0      1
Lady     female       1.0           0.0      1
Major    male         1.0           1.0      2
Master   male        23.0          17.0     61
Miss     female     127.0          55.0    260
Mlle     female       2.0           0.0      2
Mme      female       1.0           0.0      1
Mr       male        81.0         436.0    757
Mrs      female      99.0          26.0    197
Ms       female       1.0           0.0      2
Rev      male         0.0           6.0      8
Sir      male         1.0           0.0      1

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

# Clean the variable Title.
df['Title'] = df['Title'].replace(['Lady', 'Countess','Capt', 'Col',
  'Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'noble')
df['Title'] = df['Title'].replace('Mlle', 'Miss')
df['Title'] = df['Title'].replace('Ms', 'Mrs')
df['Title'] = df['Title'].replace('Mme', 'Mrs')
# Check the survival rate for the title cathegories obtained
df_grouped = df.groupby(['Title', 'Sex']).agg({'Survived': ['sum', lambda x: x.count() - x.sum()]})
df_grouped.columns = ['Survived', 'Non-Survived']
df_grouped['Count'] = df.groupby(['Title', 'Sex']).size()
# Calculate the survived/non-survived rate
df_grouped['Survival Rate'] = df_grouped['Survived'] / (df_grouped['Survived'] + df_grouped['Non-Survived'])
# Order the DataFrame by the survival rate in descending order
df_grouped = df_grouped.sort_values('Survival Rate', ascending=False)

print(df_grouped)
Title   Sex     Survived   Non-Survived  Count  Survival Rate
                                                 
noble  female       3.0           0.0      4       1.000000
Mrs    female     101.0          26.0    200       0.795276
Miss   female     129.0          55.0    262       0.701087
Master male        23.0          17.0     61       0.575000
noble  male         5.0          15.0     25       0.250000
Mr     male        81.0         436.0    757       0.156673

Мы видим, что дворянство сильно повлияло на вероятность выживания Титаника. Остальные названия отражают пол и возраст пассажира. Мы можем заметить, что выживаемость мужчины с титулом «мастер» выше, чем у мужчин с титулом «мистер», хотя это можно интерпретировать по возрасту, тогда как у более молодого человека больше шансов выжить. Если мы сравним выживаемость женщин, то пассажиры со званием Мисс имеют более низкие шансы на выживание по сравнению с женщинами со званием Миссис, тогда как это может указывать на то, замужем женщина или нет, может иметь значение. Но все мы согласны с тем, что информация о семье и возрасте имеет свои особенности для описания. Исключительная информация, которую мы можем получить из функции титула, - это знать, владеет ли пассажир дворянским титулом или нет.

df['noble'] = df['Title'].apply(lambda x: 1  if x =='noble'  else 0)
df['noble'].value_counts()
0    1280
1      29
Name: noble, dtype: int64

Плата за проезд

Мы можем заменить отсутствующее значение «Тариф» (есть только одно) медианным значением. Справедливости ради мы заменим отсутствующее значение тарифа медианой значений тарифа для пола = мужчины и Pкласса = 3.

# fill with the median Fare for the Pclass = 3 and Sex= male.
Fare_Pclass3_male = df.loc[(df['Pclass'] == 3) & (df['Sex'] == 'male'), 'Fare'].median()
df.loc[df['PassengerId'] == 1044, 'Fare'] = df.loc[(df['Pclass'] == 3) & (df['Sex'] == 'male'), 'Fare'].median()
print("medium Fare for Sex = male and Pclass=3: ", Fare_Pclass3_male)
medium Fare for Sex = male and Pclass=3:  7.8958

Мы можем классифицировать Тарифы по группам с одинаковыми диапазонами или уровнями Тарифов. Если мы классифицируем его с помощью интерквартиля, распределение бинов возвращает странное представление, потому что переменная «Плата за проезд» не является непрерывной.

# Divide Fare in 10 levels
n = 10
df['Fare_level'] = pd.cut(df['Fare'], n, labels=np.arange(1,n+1))
df['Fare_range'] = pd.cut(df['Fare'], n)

Мы можем визуализировать, как вероятность выживания меняется с помощью «Fare_level».

# Calculate survival probability for each fare level
fare_survival_grouped = df.groupby('Fare_level').agg({'Fare_range': ['first', 'count'], 'Survived': 'mean'}).reset_index()
fare_survival_grouped.columns = ['Fare_level', 'Fare_range', 'Count', 'Survival_proba']
fare_survival_grouped = fare_survival_grouped.reset_index(drop=True)
fare_survival_grouped

В приведенной выше таблице четыре пассажира с вероятностью выживания 100% имеют стоимость проезда 512,33 фунтов стерлингов. Это случай мистера Томаса Дрейка Мартинеса Кардесы, богатого банкира из Джермантауна, штат Пенсильвания. Он сел на Титаник со своей матерью Шарлоттой Кардеза, слугой Гюставом Лесуэром и мисс Энни Мур Уорд, горничной семьи Кардеза. (см. Энциклопедию Титаника и Титаник Фэндом). Они заняли один из двух самых роскошных кают на борту (B51, B53 и B55) и оплатили еще один костюм для слуги и, вероятно, еще один для их горничной Энни.

Давайте построим зависимость вероятности выживания с новой функцией «Fare_level». На графике ниже мы видим, как вероятность выживания быстро увеличивается с «Fare_level» и колеблется для значений выше 4-го уровня.

# Calculate the counts
variable = 'Fare_level'
counts = df[variable].value_counts()
# Calculate the probability of survival
survived = df[df['Survived'] == 1][variable].value_counts().sort_index()
not_survived = df[df['Survived'] == 0][variable].value_counts().sort_index()
survival_prob = survived / (survived + not_survived)
# Create subplots with shared x-axis
fig, ax1 = plt.subplots()
# Plot the survival probability using scatter plot markers and lines on the left y-axis
ax1.plot(survival_prob.index, survival_prob.values, marker='o', linestyle='-', color='red')
ax1.set_ylabel('Survival Probability', color='black')
ax1.tick_params(axis='y', labelcolor='red')
# Create a twin y-axis for the left side
ax2 = ax1.twinx()
#Plot the count bars on the right y-axis
ax2.bar(counts.index, counts.values, alpha=0.4, width=0.2, color='royalblue')
ax2.set_ylabel('Counts', color='black')
ax2.tick_params(axis='y', labelcolor='blue')
# Set the x-axis label and title
ax1.set_xlabel(variable)
plt.title('Count and Survival Probability')
# Adjust the layout of subplots
plt.tight_layout()

Кабина

Это самый грязный столбец, большинство значений отсутствует, а некоторые строки содержат более одного значения в каждом столбце (более одной кабины для некоторых пассажиров). Наблюдались только 22% значений «Кабины» (295/1309). Информация о каюте может быть очень информативной в качестве косвенного показателя расстояния от шлюпочной палубы, где расположены спасательные шлюпки, что оказывает большое влияние на шансы на выживание. Но этот столбец не структурирован и в основном содержит пропущенные значения. Чтобы помочь решить эту проблему, мы начинаем с разделения этого столбца на три столбца: ["Cabin_label", "Cabin_number", "N_of_Cabins Reserved"]. Вот некоторые подходы к вменению данных в переменную «Каюта»:

  • Проанализируйте эти три столбца вместе со значениями «Fare» и количеством членов семьи на борту, «FamilySize», таким образом, чтобы мы могли попытаться предсказать недостающие значения, по крайней мере, для отсутствующей метки Cabin_label.
  • Мы могли бы применить простое правило и оценить отсутствующую метку Cabin_label, сопоставив переменную «Fare».
  • Можем поискать дублирующиеся значения переменной «Билет», при которых наблюдается «Каюта» у одного из попутчиков.
  • Мы можем использовать контролируемый алгоритм стратегии вменения на основе машинного обучения, такой как MISS Forest.
  • В древовидных моделях есть некоторые методы, позволяющие делать прогнозы даже в сценариях, в которых у нас есть пропущенные значения. Автономные версии XGBoost и LightGBM имеют встроенную поддержку пропущенных значений. Суррогатное разделение — это метод, при котором дерево находит другую переменную для выполнения разделения, если первичная переменная разделения отсутствует. суррогатная переменная имеет самую высокую корреляцию с первичной переменной и имеет тенденцию выполнять разделение так же, как и первичная. Однако я не нашел реализации дерева решений в Python, поддерживающей использование суррогатных разбиений.
  • Мы можем создать новую категориальную переменную для отсутствующего значения.

Существует множество подходов к вменению данных и стратегий использования столбцов «Каюта», чтобы помочь в прогнозировании вероятности выживания. Некоторые общие записные книжки в Kaggle пытаются выполнить вменение метки салона, используя простые правила, основанные на переменной «Fare» или «Pclass», но переменная «Fare» не обязательно соответствует пропорциональному значению для «Pclass» в случаях, когда код билета одинаков для группы пассажиров. Некоторые пассажиры отправились на борт с семьей и сотрудниками и заплатили за несколько кают. В этих случаях код билета один и тот же, и тариф один и тот же, соответствующий сумме кают и сопровождающих. Это идеальное определение неструктурированного набора данных, в котором переменные содержат более одной части информации. Отсутствующие значения в переменной «Cabin_number» невозможно выполнить вменение. Здесь я выбираю последний метод, и в новой переменной «Cabin_label» отсутствующие значения вменяются путем создания новой категориальной переменной «N».

# get the last cabin if exists
df['Last_cabin'] = df['Cabin'].apply(lambda x: str(x).split()[-1] if pd.notnull(x) else 'N')
# get the Cabin_label of the last cabin
pattern = r'([A-Za-z])'
df['Cabin_label'] = df['Last_cabin'].apply(lambda x: re.search(pattern, str(x)).group(1) if x != 'N' else 'N')
# Get the number of the last cabin if exists
pattern = r'(\d+)'
df['Cabin_number'] = df['Last_cabin'].apply(lambda x: re.search(pattern, str(x)) if x != 'N' else -1)
# Get the number of distinct cabins reserved by the passenger.
df['Cabin_count'] = df['Cabin'].apply(lambda x: len(str(x).split()) if pd.notnull(x) else 0)
# drop not used columns and columns with NaN values.
df.drop(columns = ['Cabin','Last_cabin', 'Cabin_number'], inplace = True)

Отображение возраста

Переменная «Возраст» содержит 1046 наблюдений из 1309 записей, соответствующих 80% набора данных (20% неизвестных, 263 пропущенных значения). Можно использовать вменение правила регрессии с использованием аналогичных стратегий, перечисленных в предыдущем разделе. Здесь мы создаем новую переменную «Возрастная_группа», разделив интервал значений на почти равные интервалы. Эта новая переменная может быть гораздо более информативной, чем точные значения «Возраст».

Женщины и дети в первую очередь!

Переменная Возрастная_группа категоризирована со значимыми возрастными диапазонами для каждой группы до 60 лет. Продолжительность жизни в 1900 г. составляла около 50 лет по данным u.demog. Беркли. Здесь я выбрал диапазоны 0–13 для детей, 13–30 для молодых, диапазон 30–45 для зрелой группы, 45–60 для пожилых. С 60 лет мы можем классифицировать как очень старые. Эта произвольная категоризация была выбрана для получения хорошего распределения значений для каждой категории (сбалансированного). Если мы выберем меньший возрастной диапазон (например, 10 лет), количество представителей в классе уменьшится и станет несбалансированным. В следующей статье я рассмотрю некоторые алгоритмы на основе ML для импутации данных и сделаю импутацию для переменных Возраст и Возрастная группа.

# Mapping Age
df.loc[df['Age'] <= 13, 'Age_group'] = 0 # kids
df.loc[(df['Age'] > 13) & (df['Age'] <= 30), 'Age_group'] = 1 # young
df.loc[(df['Age'] > 30) & (df['Age'] <= 45), 'Age_group'] = 2 # mature1
df.loc[(df['Age'] > 45) & (df['Age'] <= 60), 'Age_group'] = 3 # old
df.loc[(df['Age'] > 60) & (df['Age'] <= 100), 'Age_group'] = 4 # very old

# Calculate survival probability for each fare level
age_survival_grouped = df.groupby('Age_group').agg({'Age': ['min', 'max'], 'Survived': ['mean', 'count']}).reset_index()
age_survival_grouped.columns = ['Age_group', 'Age_min' , 'Age_max', 'Survival_proba', 'Count']
age_survival_grouped

# Calculate the counts
variable = 'Age_group'
counts = df[variable].value_counts()

# Calculate the probability of survival
survived = df[df['Survived'] == 1][variable].value_counts().sort_index()
not_survived = df[df['Survived'] == 0][variable].value_counts().sort_index()
survival_prob = survived / (survived + not_survived)

# Create subplots with shared x-axis
fig, ax1 = plt.subplots()   

# Plot the survival probability using scatter plot markers and lines on the left y-axis
ax1.plot(survival_prob.index, survival_prob.values, marker='o', linestyle='-', color='red')
ax1.set_ylabel('Survival Probability', color='black')
ax1.tick_params(axis='y', labelcolor='red')    

# Create a twin y-axis for the left side
ax2 = ax1.twinx()

#Plot the count bars on the right y-axis
ax2.bar(counts.index, counts.values, alpha=0.4, width=0.2, color='royalblue')
ax2.set_ylabel('Counts', color='black')
ax2.tick_params(axis='y', labelcolor='blue')

# Set the x-axis label and title
ax1.set_xlabel(variable)
plt.title('Count and Survival Probability')

# Adjust the layout of subplots
plt.tight_layout()

До этого момента мы занимались только очисткой данных и проектированием признаков. Некоторые пропущенные значения были заполнены значениями, полученными из внешних источников, или мы применили условную медианную операцию для заполнения небольшого количества значений NaN. Эта операция немного изменяет набор данных, но я буду считать набор данных только что очищенным и сконструированным. В столбце Выжил недостающие значения соответствуют тестовой части (см. Kaggle Titanic — Machine Learning from Disaster). В столбцах Возраст и Возрастная_группа много пропущенных значений, и мы не можем заполнить их, используя простую условную медиану или правило сопоставления. Затем я сохраню набор данных до этого момента как titanic_eng.csv. Вы можете скачать этот набор данных из репозитория GitHub, который я создал для этой статьи.

# Plot the missing values matrix
import missingno as msno

dataframe = df

plt.figure(figsize=(10, 6))
msno.matrix(dataframe, color=(0.29803922, 0.44705882, 0.69019608), fontsize=20)
plt.xlabel('Variables')
plt.ylabel('Rows', fontsize=20)
plt.title('Missing Values in Titanic_eng dataset', fontsize=20)
plt.show()

# export the dataset_eng
df.to_csv('titanic_datasets/titanic_eng.csv', index = False)

Исследовательский анализ данных

Мы можем провести некоторый исследовательский анализ с помощью недавно очищенного и модифицированного набора данных Titanic.

print(df.columns)
Index(['PassengerId', 'Survived', 'Pclass', 'Name', 'Sex', 'SibSp', 'Parch',
       'Ticket', 'Fare', 'Embarked', 'Sex_bool', 'Embarked_code',
       'Ticket_preffix', 'Ticket_number', 'Companions', 'FamilySize', 'Title',
       'noble', 'Fare_level', 'Fare_range', 'Cabin_label', 'Cabin_count',
       'Age_group', 'Title_Master', 'Title_Miss', 'Title_Mr', 'Title_Mrs',
       'Title_noble'],
      dtype='object')


Проверим корреляции между переменными.

# print(train_data.corr())
corrMatrix = df[~pd.isna(df["Survived"])].loc[:, ['Pclass', 'Is_male', 'Age_group', 'FamilySize', 'Survived', 'Fare']].corr()
fig, ax = plt.subplots(figsize=(8,7))         # Sample figsize in inches
sns.heatmap(corrMatrix, annot=True, linewidths=.5, ax=ax)

Ниже я встраиваю приложение Dash с интерактивным графиком вероятности выжившего и подсчета.



Пассажиры с отсутствующей информацией о салоне

Каков профиль пассажиров, у которых нет информации о кабине, или Cabin_label = N. Анализируя распределения характеристик пассажиров, которые были в каждой метке кабины, мы видим, что профиль пассажиров с отсутствующей информацией о кабине очень близок к профилю всех пассажиров, независимо от того, в какой кабине они находились. Это можно понять, поскольку пассажиры в группе с Cabin_label = N не принадлежали в основном к низшему социальному статусу (Pclass = 3). Есть пассажиры из трех значений Pclass с отсутствующими значениями Cabin. Отсутствующая информация о салоне не связана с данным P-классом, полом или возрастной группой, она распределяется случайным образом. Вот почему очень сложно осмысленно использовать эту функцию с такой структурой.

# Filter the DataFrame for passengers with cabin label 'N'
cabin_N_passengers = df[df['Cabin_label'] == 'N']

fig, axes = plt.subplots(2, 4, figsize=(20, 8))

# Plot the distribution of classes
sns.countplot(x='Pclass', data=cabin_N_passengers, ax=axes[0,0], width=0.5)
axes[0,0].set_title('Passenger Class Distribution for Cabin Label N')
# Plot the distribution of sex
sns.countplot(x='Sex', data=cabin_N_passengers, ax=axes[0,1], width=0.3)
axes[0,1].set_title('Sex Distribution for Cabin Label N')
# Plot the distribution of fare
sns.countplot(x='Fare_range', data=cabin_N_passengers, ax=axes[0,2], width=0.5)
axes[0,2].set_title('Fare Distribution for Cabin Label N')
# Plot the distribution of fare
sns.countplot(x='Age_group', data=cabin_N_passengers, ax=axes[0,3], width=0.5)
axes[0,3].set_title('Age_group Distribution for Cabin Label N')
# Plot the distribution of fare
sns.countplot(x='Embarked_code', data=cabin_N_passengers, ax=axes[1,0], width=0.5)
axes[1,0].set_title('Embarked_code Distribution for Cabin Label N')
# Plot the distribution of fare
sns.countplot(x='FamilySize', data=cabin_N_passengers, ax=axes[1,1], width=0.5)
axes[1,1].set_title('Familysize Distribution for Cabin Label N')
# Plot the distribution of fare
sns.countplot(x='Companions', data=cabin_N_passengers, ax=axes[1,2], width=0.5)
axes[1,2].set_title('Companions Distribution for Cabin Label N')
# Plot the distribution of fare
sns.countplot(x='Title', data=cabin_N_passengers, ax=axes[1,3], width=0.5)
axes[1,3].set_title('Title Distribution for Cabin Label N')

# Adjust the spacing between subplots
plt.tight_layout()
# Display the plots
plt.show()

Ниже я вставляю приложение Dash с интерактивным графиком для проверки профилей столбцов всех переменных, отфильтрованных по значению Cabin_label.



Все коды, используемые в этой статье, находятся в моем репозитории GitHub.



Вопросы? Комментарии? Вы можете оставить свой отзыв в разделе комментариев или связаться со мной напрямую по электронной почте: [email protected].

Если вам понравилась эта статья, нажмите 👏 🙂.

Если вы хотите поддержать меня, купите мне кофе:

Контакты:[email protected],LinkedIn, CV-Lattes, ORCID.