Очистка данных и разработка признаков — важные шаги в конвейере предварительной обработки данных, которые существенно влияют на качество и эффективность аналитических моделей.
Ссылка на конкурс / Ссылка на набор данных
Начнем с изучения набора данных. Ниже перечислены функции, которые у нас есть, определение каждой функции вы можете найти здесь.
['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')
Это все для статьи, теперь следующим шагом будет выбор лучших функций и построение модели классификации машинного обучения для прогнозирования погоды с учетом того, был ли пассажир в тестовых данных перевезен или нет.