Очистка данных и разработка признаков — важные шаги в конвейере предварительной обработки данных, которые существенно влияют на качество и эффективность аналитических моделей.

Ссылка на конкурс / Ссылка на набор данных

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

['PassengerId', 'HomePlanet', 'CryoSleep', 'Cabin', 'Destination', 'Age',
       'VIP', 'RoomService', 'FoodCourt', 'ShoppingMall', 'Spa', 'VRDeck',
       'Name', 'Transported']
df['Transported'].value_counts() # balanced dataset

# True     4378
# False    4315

Целевой класс здесь хорошо сбалансирован..! поскольку это набор данных для практики, в противном случае в реальных данных это часто неверно.

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

def showDetails(column):
    print('------------------------------------------')
    print(column +' & TRANSPORTED')
    print(df[column].value_counts())
    tempDict = dict(df[column][df['Transported'] == True].value_counts())
    for i in tempDict.keys():
        tempDict[i] = (tempDict[i]/len(df[df[column] == i]))*100
    print(tempDict)
    print('------------------------------------------')

showDetails('HomePlanet')
showDetails('CryoSleep')
showDetails('Destination')
showDetails('VIP')
Output :

HomePlanet & TRANSPORTED
Earth     4602
Europa    2131
Mars      1759
Name: HomePlanet, dtype: int64
{'Earth': 42.39461103867884, 'Europa': 65.884561238855, 'Mars': 52.30244457077885}
------------------------------------------
------------------------------------------
CryoSleep & TRANSPORTED
False    5439
True     3037
Name: CryoSleep, dtype: int64
{True: 81.75831412578202, False: 32.892075749218606}
------------------------------------------
------------------------------------------
Destination & TRANSPORTED
TRAPPIST-1e      5915
55 Cancri e      1800
PSO J318.5-22     796
Name: Destination, dtype: int64
{'TRAPPIST-1e': 47.11749788672866, '55 Cancri e': 61.0, 'PSO J318.5-22': 50.37688442211056}
------------------------------------------
------------------------------------------
VIP & TRANSPORTED
False    8291
True      199
Name: VIP, dtype: int64
{False: 50.63321674104451, True: 38.19095477386934}

Пример: {"Земля": 42,39461103867884, "Европа": 65,884561238855, "Марс": 52,30244457077885}

Итак, здесь успешно перевезено 42,4% пассажиров, прибывших с Земли.

Давайте посмотрим, существует ли в данных какой-либо геометрический паттерн, который может дать нам представление о том, какой тип разработки признаков или какой тип моделей будет лучше работать для этого набора данных. Буду использовать t-sne для визуализации.

def plot_tsne(X,y,p=30,step=1000):
    """
    X-features number (pandas dataframe)
    y-targets int (pandas series)
    p-perplexity - points in neighourhood
    step-Maximum number of iterations for the optimization. Should be at least 250.
    """
    targets = y.astype(int)
    tsne = manifold.TSNE(n_components=2,perplexity=p,n_iter=step ,random_state=42)
    transformed_data = tsne.fit_transform(X.iloc[:100,:])
    tsne_df = pd.DataFrame(np.column_stack((transformed_data, targets[:100])),columns=["x", "y", "targets"])
    tsne_df.loc[:, "targets"] = tsne_df.targets.astype(int)
    grid = sns.FacetGrid(tsne_df, hue="targets", size=8)
    grid.map(plt.scatter, "x", "y").add_legend()

Глядя на распределение точек данных на 2D-плоскости, разделение точек выглядит хорошо, и похоже, что мы можем легко провести границу логистической регрессии для классификации точек. Таким образом, логистическая регрессия может хорошо работать с этим набором данных, мы рассмотрим его при построении модели.

Также здесь есть подробная статья о t-sne, чтобы узнать больше.



Итак, это были некоторые сведения о данных, кроме этого, можно провести однофакторный анализ, но это выходит за рамки этой статьи.

Давайте займемся разработкой функций,

Давайте займемся разработкой функций.

PassengerId — уникальный идентификатор для каждого пассажира. Каждый идентификатор принимает форму gggg_pp, где gggg указывает на группу, с которой путешествует пассажир, а pp — его номер в группе. Люди в группе часто являются членами семьи, но не всегда.

поэтому, используя эту функцию, мы можем создать функцию размера группы,

df['Group'] = [x.split('_')[0] for x in list(df['PassengerId'])]
df['Group'] = df['Group'].astype(int)
test_data['Group'] = [x.split('_')[0] for x in list(test_data['PassengerId'])]
test_data['Group'] = test_data['Group'].astype(int)

# New feature - Group size
df['Group_size']=df['Group'].map(lambda x: df['Group'].value_counts()[x])
test_data['Group_size']=test_data['Group'].map(lambda x: test_data['Group'].value_counts()[x])

plt.figure(figsize=(20,4))
sns.countplot(data=df, x='Group_size', hue='Transported')
plt.title('Group size')

У нас есть имя путешественника в качестве функции, поэтому здесь мы можем разделить их на 3 типа: Индивидуальный путешественник / Семья / Группа, чтобы узнать,если путешественник путешествует с семьей, мы будем использовать фамилию путешественника.

def add_traveller_type(df):

    checkdict ={

    }
    newColumnTravellerType = []

    for index in range(len(df)):
        gggg =df.iloc[index]['PassengerId'].split('_')[0]
        if df.iloc[index]['Name'] != df.iloc[index]['Name'] :
            lastname = 'NA'
        else :
            lastname =df.iloc[index]['Name'].split(' ')[1]
        if gggg in checkdict:
            checkdict[gggg].append(lastname)
        else :
            checkdict[gggg] = [lastname]

    for index in range(len(df)):
        gggg =df.iloc[index]['PassengerId'].split('_')[0]
        lastnames = checkdict[gggg]
        lastname = 'NA'
        if df.iloc[index]['Name'] == df.iloc[index]['Name'] :
            lastname =df.iloc[index]['Name'].split(' ')[1]

        if len(lastnames) == 1:
            newColumnTravellerType.append('INDIVIDUAL')
        elif len(lastnames) > 1:
            if lastname != 'NA' and lastnames.count(lastname) > 1:
                newColumnTravellerType.append('FAMILY')
            else :
                newColumnTravellerType.append('GROUP')

    df['TravellerType'] = newColumnTravellerType
    return df

df=  add_traveller_type(df)
test_data = add_traveller_type(test_data)
# for now deleting the group feature 
# as it was used as a helper to create group size feature

df.drop(columns=['Group','Name'],inplace=True)
test_data.drop(columns=['Group','Name'],inplace=True)

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

df['Age'].fillna(value=df['Age'].mean(),inplace=True)
df['RoomService'].fillna(value=df['RoomService'].median(),inplace=True)
df['FoodCourt'].fillna(value=df['FoodCourt'].median(),inplace=True)
df['ShoppingMall'].fillna(value=df['ShoppingMall'].median(),inplace=True)
df['Spa'].fillna(value=df['Spa'].median(),inplace=True)
df['VRDeck'].fillna(value=df['VRDeck'].median(),inplace=True)

df.isna().sum()

# Output :
# PassengerId        0
# HomePlanet       201
# CryoSleep        217
# Cabin            199
# Destination      182
# Age                0
# VIP              203
# RoomService        0
# FoodCourt          0
# ShoppingMall       0
# Spa                0
# VRDeck             0
# Transported        0
# Group_size         0
# TravellerType      0
# Custom logic if person is with family than some information 
# should be common like homeplanet destination ect.

def customImputation(column,index):
    if df.iloc[index][column] != df.iloc[index][column]:
        if df.iloc[index]['TravellerType'] == 'FAMILY':
            try:
                if df.iloc[index-1]['TravellerType'] == 'FAMILY' or df.iloc[index+1]['TravellerType'] == 'FAMILY':
                    df.at[index,column] = df.iloc[index-1][column]
                else:
                    df.at[index,column] = df[column].mode().values[0]

            except:
                pass
        else:
            df.at[index,column] = df[column].mode().values[0]

columns = ['HomePlanet','CryoSleep','Destination','VIP']
for index in range(len(df)):
    for column in columns:
        customImputation(column,index)
        
        
def customImputation(column,index):
    if test_data.iloc[index][column] != test_data.iloc[index][column]:
        if test_data.iloc[index]['TravellerType'] == 'FAMILY':
            try:
                if test_data.iloc[index-1]['TravellerType'] == 'FAMILY' or test_data.iloc[index+1]['TravellerType'] == 'FAMILY':
                    test_data.at[index,column] = test_data.iloc[index-1][column]
                else:
                    test_data.at[index,column] = test_data[column].mode().values[0]

            except:
                pass
        else:
            test_data.at[index,column] = test_data[column].mode().values[0]
        
for index in range(len(test_data)):
    for column in columns:
        customImputation(column,index)

"RoomService", "FoodCourt", "ShoppingMall", "Spa", "VRDeck" — категории расходов пассажиров. Таким образом, мы можем использовать его для создания двух новых функций «Расходы» — это общие расходы, сделанные пассажиром, и «has_expenditure» — которые означают, что пассажир сделал какие-либо расходы во время путешествия.

#Get the total amount the passenger expend
df['Expenditure']= df[['RoomService','FoodCourt','ShoppingMall','Spa','VRDeck']].sum(axis=1)
test_data['Expenditure']= test_data[['RoomService','FoodCourt','ShoppingMall','Spa','VRDeck']].sum(axis=1)

df['has_expenditure'] = pd.DataFrame(df['Expenditure'] > 0).astype(int)
test_data['has_expenditure'] = pd.DataFrame(test_data['Expenditure'] > 0).astype(int)

df.groupby('has_expenditure').Transported.mean().plot(kind='barh').set_xlabel('% transported')

У нас есть возрастная функция, мы можем создать корзину из этой непрерывной функции.

Cabin - Номер каюты, в которой находится пассажир. Принимает форму deck/num/side, где side может быть либо P для порта, либо S для правого борта.

В соответствии с определением функции мы разделим ее на 3 части, а также создадим номер группы и номер пассажира из функции PassengerId.

df[['Deck', 'Num', 'Side']] = df['Cabin'].str.split('/', expand=True)
test_data[['Deck', 'Num', 'Side']] = test_data['Cabin'].str.split('/', expand=True)

def feature_engineering(cat_df):

    nums = []
    for num in list(cat_df['Cabin'].values):
        num =str(num)
        if '/' in num:
            nums.append(num.split('/')[1])
        else:
            nums.append(None)

    gggg = []
    for num in list(cat_df['PassengerId'].values):
        num =str(num)
        if '_' in num:
            gggg.append(num.split('_')[0])
        else:
            gggg.append(None)

    cat_df['gggg'] = gggg

    pp = []
    for num in list(cat_df['PassengerId'].values):
        num =str(num)
        if '_' in num:
            pp.append(num.split('_')[1])
        else:
            pp.append(None)
            
    cat_df['pp'] = pp


    cat_df['Cabin_num'] = nums

    bool_ = {
        False : 0,
        True  : 1
    }
    cat_df['VIP']=cat_df['VIP'].map(bool_)
    cat_df['CryoSleep']=cat_df['CryoSleep'].map(bool_)

    cat_df = cat_df.drop(columns=['Cabin','PassengerId'],axis=1)
    
   
    age_0_15 = []
    age_15_30 = []
    age_30_plus = []
    
    for age in cat_df['Age'].values:
        if int(age) <= 15:
            age_0_15.append(1)
            age_15_30.append(0)
            age_30_plus.append(0)
        elif int(age) >15 and int(age) <=30:
            age_0_15.append(0)
            age_15_30.append(1)
            age_30_plus.append(0)
        elif int(age) > 30:
            age_0_15.append(0)
            age_15_30.append(0)
            age_30_plus.append(1)
            
    cat_df['age_0_15'] = age_0_15
    cat_df['age_15_30'] = age_15_30
    cat_df['age_30_plus'] = age_30_plus
    
    
#     bins = np.arange(0,80,18)
#     cat_df['Age_bin']  = pd.cut(cat_df['Age'],bins=bins)
    
    cat_df = cat_df.drop(['Age','VIP','AgeGroup'],axis=1)
    
    
    X = pd.get_dummies(cat_df,columns=['HomePlanet','Destination','Deck','Side'],drop_first=False)
    return X

df = feature_engineering(df)

test_data['Age'].fillna(0,inplace=True) # age feature contains nan values
test_data = feature_engineering(test_data)

Наконец, сохранение предварительно обработанных обучающих и тестовых данных

df.to_csv('./finalTrain.csv')
test_data.to_csv('./finalTest.csv')

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