import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns import scipy import missingno as msno from sklearn.impute import SimpleImputer # импортируем датасет Титаник titanic = pd.read_csv('train.csv') sns.set() titanic.info() titanic.Age.astype('int') titanic.isna().sum() (titanic.isna().sum() / len(titanic)).round(4) * 100 msno.bar(titanic) msno.matrix(titanic) titanic[['Age', 'Cabin', 'Embarked']].isnull().corr() df = titanic.iloc[:, [i for i, n in enumerate(np.var(titanic.isnull(), axis = 'rows')) if n > 0]] df.isnull().corr() msno.heatmap(titanic) # удаление строк обозначим через axis = 'index' # subset = ['Embarked'] говорит о том, что мы ищем пропуски только в столбце Embarked titanic.dropna(axis = 'index', subset = ['Embarked'], inplace = True) # убедимся, что в Embarked действительно не осталось пропусков titanic.Embarked.isna().sum() # передадим в параметр columns тот столбец, который хотим удалить titanic.drop(columns = ['Cabin'], inplace = True) # убедимся, что такого столбца больше нет titanic.columns sex_g = titanic.groupby('Sex').count() sex_g # сравним количество пассажиров в столбце Age и столбце PassengerId sex_g['PassengerId'].sum(), sex_g['Age'].sum() # метод .mean() игнорирует пропуски и не выдает ошибки titanic['Age'].mean() # то же можно сказать про метод .corr() titanic[['Age', 'Fare']].corr() # еще раз загрузим датасет "Титаник", в котором снова будут пропущенные значения titanic = pd.read_csv('train.csv') # возьмем лишь некоторые из столбцов titanic = titanic[['Pclass', 'Sex', 'Survived', 'SibSp', 'Parch', 'Fare', 'Age', 'Embarked']] # закодируем столбец Sex с помощью числовых значений map_dict = {'male' : 0, 'female' : 1} titanic['Sex'] = titanic['Sex'].map(map_dict) # посмотрим на результат titanic.head() # вначале сделаем копию датасета fillna_const = titanic.copy() # заполним пропуски в столбце Age нулями, передав методу .fillna() словарь, # где ключами будут названия столбцов, а значениями - константы для заполнения пропусков fillna_const.fillna({'Age' : 0}, inplace = True) titanic.Age.median(), fillna_const.Age.median() # найдем пассажиров с неизвестным портом посадки # для этого создадим маску по столбцу Embarked и применим ее к исходным данным missing_embarked = pd.read_csv('train.csv') missing_embarked[missing_embarked.Embarked.isnull()] # метод .fillna() можно применить к одному столбцу # два пропущенных значения в столбце Embarked заполним буквой S (Southampton) fillna_const.Embarked.fillna('S', inplace = True) # убедимся, что в столбцах Age и Embarked не осталось пропущенных значений fillna_const[['Age', 'Embarked']].isna().sum() # сделаем копию датасета const_imputer = titanic.copy() # импортируем класс SimpleImputer из модуля impute библиотеки sklearn from sklearn.impute import SimpleImputer # создадим объект этого класса, указав, # что мы будем заполнять константой strategy = 'constant', а именно нулем fill_value = 0 imp_const = SimpleImputer(strategy = 'constant', fill_value = 0) # и обучим модель на столбце Age # мы используем двойные скобки, потому что метод .fit() на вход принимает двумерный массив imp_const.fit(const_imputer[['Age']]) # также используем двойные скобки с методом .transform() const_imputer['Age'] = imp_const.transform(const_imputer[['Age']]) # убедимся, что пропусков не осталось и посчитаем количество нулевых значений const_imputer.Age.isna().sum(), (const_imputer['Age'] == 0).sum() # удалим его const_imputer.drop(columns = ['Embarked'], inplace = True) # и посмотрим на размер получившегося датафрейма const_imputer.shape # посмотрим на результат const_imputer.head(3) # сделаем копию датафрейма fillna_median = titanic.copy() # заполним пропуски в столбце Age медианным значением возраста, # можно заполнить и средним арифметическим через метод .mean() fillna_median.Age.fillna(fillna_median.Age.median(), inplace = True) # убедимся, что пропусков не осталось fillna_median.Age.isna().sum() # изменим размер последующих графиков sns.set(rc = {'figure.figsize' : (10, 6)}) # скопируем датафрейм median_imputer = titanic.copy() # посмотрим на распределение возраста до заполнения пропусков sns.histplot(median_imputer['Age'], bins = 20) plt.title('Распределение Age до заполнения пропусков'); median_imputer['Age'].mean().round(1), median_imputer['Age'].median() # часть 2 # создадим объект класса SimpleImputer с параметром strategy = 'median' # (для заполнения средним арифметическим используйте strategy = 'mean') imp_median = SimpleImputer(strategy = 'median') # применим метод .fit_transform() для одновременного обучения модели и заполнения пропусков median_imputer['Age'] = imp_median.fit_transform(median_imputer[['Age']]) # убедимся, что пропущенных значений не осталось median_imputer.Age.isna().sum() # посмотрим на распределение после заполнения пропусков sns.histplot(median_imputer['Age'], bins = 20) plt.title('Распределение Age после заполнения медианой'); # посмотрим на метрики после заполнения медианой median_imputer['Age'].mean().round(1), median_imputer['Age'].median() # столбец Embarked нам опять же не понадобится median_imputer.drop(columns = ['Embarked'], inplace = True) # посмотрим на размеры получившегося датафрейма median_imputer.shape #заполнеие внутригрупповым значением ===================== # скопируем датафрейм median_imputer_bins = titanic.copy() # сгруппируем пассажиров по полу и классу каюты Age_bins = median_imputer_bins.groupby(['Sex', 'Pclass']) # найдем медианный возраст с учетом получившихся групп Age_bins.Age.median() # объект SeriesGroupBy находится в переменной Age_bins.Age, # применим к нему lambda-функцию через метод .apply() median_imputer_bins.Age = Age_bins.Age.apply(lambda x: x.fillna(x.median())) # проверим пропуски в столбце Age median_imputer_bins.Age.isna().sum() sns.histplot(median_imputer_bins['Age'], bins = 20) plt.title('Распределение Age после заполнения внутригрупповой медианой'); plt.show() # столбец Embarked нам не понадобится median_imputer_bins.drop(columns = ['Embarked'], inplace = True) # посмотрим на размеры получившегося датафрейма median_imputer_bins.shape # Заполение частотным значением ========================= # скопируем датафрейм titanic_mode = titanic.copy() # посмотрим на распределение пассажиров по порту посадки до заполнения пропусков titanic_mode.groupby('Embarked')['Survived'].count() # создадим объект класса SimpleImputer с параметром strategy = 'most_frequent' imp_most_freq = SimpleImputer(strategy = 'most_frequent') # применим метод .fit_transform() к столбцу Embarked titanic_mode['Embarked'] = imp_most_freq.fit_transform(titanic_mode[['Embarked']]) # убедимся, что пропусков не осталось titanic_mode.Embarked.isna().sum() # количество пассажиров в категории S должно увеличиться на два titanic_mode.groupby('Embarked')['Survived'].count() titanic.Embarked.value_counts().index[0] # для работы с последующими методами столбец Embarked нам уже не нужен titanic.drop(columns = ['Embarked'], inplace = True) # часть 3 #Детерминированный подход ================================= #Подготовка данных # сделаем копию датасета lr = titanic.copy() # импортируем класс StandardScaler модуля Preprocessing библиотеки sklearn from sklearn.preprocessing import StandardScaler # создаем объект этого класса scaler = StandardScaler() # применяем метод .fit_transform() и сразу помещаем результат в датафрейм lr = pd.DataFrame(scaler.fit_transform(lr), columns = lr.columns) # посмотрим на результат lr.head(3) # создадим маску из пустых значений в столбце Age с помощью метода .isnull() test = lr[lr['Age'].isnull()].copy() test.head(3) # посмотрим на количество таких строк test.shape # используем метод .dropna(), чтобы избавиться от пропусков train = lr.dropna().copy() # оценим количество строк без пропусков train.shape len(train) + len(test) # целевая переменная может быть в формате Series y_train = train['Age'] # также не забудем удалить столбец Age из датафрейма признаков X_train = train.drop('Age', axis = 1) # в test столбец Age не нужен в принципе X_test = test.drop('Age', axis = 1) # на этих признаках мы будем учить нашу модель X_train.head(3) # импортируем класс LinearRegression from sklearn.linear_model import LinearRegression # создадим объект этого класса lr_model = LinearRegression() # обучим модель lr_model.fit(X_train, y_train) # применим обученную модель к данным, в которых были пропуски в столбце Age y_pred = lr_model.predict(X_test) # посмотрим на первые три прогнозных значения y_pred[:3] test['Age'] = y_pred test.head(3) # еще раз взглянем на датафрейм train train.head(3) lr = pd.concat([train, test]) lr.head(7) # восстановим изначальный порядок строк, отсортировав их по индексу lr.sort_index(inplace = True) lr.head(7) # вернем исходный масштаб с помощью метода .inverse_transform() lr = pd.DataFrame(scaler.inverse_transform(lr), columns = lr.columns) # округлим столбец Age и выведем результат lr.Age = lr.Age.round(1) lr.head(7) # Проверим на наши данные. Подставим в формулу выше отмасштабированное # значение возраста первого наблюдения (индекс 0). (-0.530377 * titanic.Age.std() + titanic.Age.mean()).round() # проверим пропуски и размеры DF lr.Age.isna().sum(), lr.shape #Оценка результата # посмотрим на распределение возраста после заполнения пропусков sns.histplot(lr['Age'], bins = 20) plt.title('Распределение Age после заполнения с помощью линейной регрессии (дет.)'); # установим минимальное значение на уровне 0,5 (полгода) lr.Age.clip(lower = 0.5, inplace = True) lr.Age.mean().round(1), lr.Age.median() #Воспользуемся методом .clip(), который установит минимальную границу значений столбца. # установим минимальное значение на уровне 0,5 (полгода) lr.Age.clip(lower = 0.5, inplace = True) #Остается посмотреть на новые средние показатели. lr.Age.mean().round(1), lr.Age.median() #Особенность детерминированного подхода # сделаем копию датафрейма, которую используем для визуализации lr_viz = lr.copy() # создадим столбец Age_type, в который запишем actual, если индекс наблюдения есть в train, # и imputed, если нет (т.е. он есть в test) lr_viz['Age_type'] = np.where(lr.index.isin(train.index), 'actual', 'imputed') # вновь "обрежем" нулевые значения lr_viz.Age.clip(lower = 0.5, inplace = True) # посмотрим на результат lr_viz.head(7) #создадим точечную диаграмму, где по оси x будет индекс датафрейма, по оси y — возраст, #а цветом обозначим изначальное это значение, или заполненное. sns.scatterplot(data = lr_viz, x = lr_viz.index, y = 'Age', hue = 'Age_type') plt.title('Распределение изначальных и заполненных значений (лин. регрессия, дет. подход)'); lr_viz[lr_viz['Age_type'] == 'actual'].Age.std().round(2), lr_viz[lr_viz['Age_type'] == 'imputed'].Age.std().round(2) #Стохастический подход # объявим функцию для создания гауссовского шума # на входе эта функция будет принимать некоторый массив значений x, # среднее значение mu, СКО std и точку отсчета для воспроизводимости результата def gaussian_noise(x, mu = 0, std = 1, random_state = 42): # вначале создадим объект, который позволит получать воспроизводимые результаты rs = np.random.RandomState(random_state) # применим метод .normal() к этому объекту для создания гауссовского шума noise = rs.normal(mu, std, size = x.shape) # добавим шум к исходному массиву return x + noise test['Age'] = gaussian_noise(x = test['Age']) # посмотрим, как изменились заполненные значения test.head(3) # соединим датасеты и обновим индекс lr_stochastic = pd.concat([train, test]) lr_stochastic.sort_index(inplace = True) # вернем исходный масштаб с помощью метода .inverse_transform() lr_stochastic = pd.DataFrame(scaler.inverse_transform(lr_stochastic), columns = lr_stochastic.columns) # округлим столбец Age и выведем результат lr_stochastic.Age = lr_stochastic.Age.round(1) lr_stochastic.head(7) # посмотрим на распределение возраста # после заполнения пропусков с помощью стохастического подхода sns.histplot(lr_stochastic['Age'], bins = 20) plt.title('Распределение Age после заполнения с помощью линейной регрессии (стох.)'); # обрежем нулевые и отрицательные значения lr_stochastic.Age.clip(lower = 0.5, inplace = True) lr_stochastic.Age.mean().round(1), lr_stochastic.Age.median() # сделаем копию датафрейма, которую используем для визуализации lr_st_viz = lr_stochastic.copy() # создадим столбец Age_type, в который запишем actual, если индекс наблюдения есть в train, # и imputed, если нет (т.е. он есть в test) lr_st_viz['Age_type'] = np.where(lr_stochastic.index.isin(train.index), 'actual', 'imputed') # вновь "обрежем" нулевые значения lr_st_viz.Age.clip(lower = 0.5, inplace = True) # создадим график, где по оси x будет индекс датафрейма, # по оси y - возраст, а цветом мы обозначим изначальное это значение, или заполненное sns.scatterplot(data = lr_st_viz, x = lr_st_viz.index, y = 'Age', hue = 'Age_type') plt.title('Распределение изначальных и заполненных значений (лин. регрессия')