# This Python 3 environment comes with many helpful analytics libraries installed
# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python
# For example, here's several helpful packages to load

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)

# Input data files are available in the read-only "../input/" directory
# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory

import os
for dirname, _, filenames in os.walk('/kaggle/input'):
    for filename in filenames:
        print(os.path.join(dirname, filename))

# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using "Save & Run All" 
# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session
"""
# Содержание
"""
"""
1. [Загрузка данных](#load_data)
2. [Приведение типов](#cast_data)
3. [EDA и предобработка данных](#eda)
    *  [Анализ целевой переменной   ](#target_analysis)
    *  [Анализ признаков](#features_analysis) 
        *  [Количественные признаки](#num_features)
            *  [Rooms](#rooms)
            *  [KitchenSquare](#kitchen_square)    
            *  [Square](#square)   
            *  [LifeSquare](#life_square)
            *  [HouseFloor](#house_floor)
            *  [Floor](#floor)
            *  [HouseYear](#house_year)
            *  [Healthcare_1](#healthcare_1)
        *  [Категориальные признаки](#cat_features) 
            *  [DistrictId](#district_id)
            *  [Ecology_2](#ecology_2)
            *  [Ecology_3](#ecology_3)
            *  [Shops_2](#shops_2)
        *  [Матрица корреляций](#correlation_matrix)
    *  [Анализ влияния признаков на таргет](#feature_target_analysis)
4. [Класс для предобработки данных DataPreprocessing](#data_preprocessing_class)
5. [Построение новых признаков](#feature_generation)
    *  [Dummies](#dummies)
    *  [DistrictSize, IsDistrictLarge](#district_size)
    *  [MedPriceByDistrict](#med_price_by_district)
    *  [MedPriceByFloorYear](#med_price_by_floor_year)
6. [Класс для добавления новых признаков FeatureGenerator](#feature_generator)
7. [Класс для заполнения Healthcare_1](#healthcare_1_model)
8. [Отбор признаков](#features_select)
9. [Разбиение на train и test](#train_test_split)
10. [Построение модели](#model)
    * [Обучение](#fit_model)
    * [Оценка модели](#evaluate_model)
    * [Важность признаков](#feature_importance)
    * [Кросс-валидация](#cv_model)
    * [Поиск по сетке](#grid_search)
11. [Прогнозирование на тестовом датасете](#test_preds)
"""
import numpy as np
import pandas as pd
import random
import xgboost as xgb

from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler, RobustScaler
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.metrics import r2_score as r2
from sklearn.model_selection import KFold, GridSearchCV
from catboost import CatBoostRegressor

from datetime import datetime

import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
# Отключает вывод предупреждений
import warnings
warnings.filterwarnings('ignore')
# Задает размер графиков
matplotlib.rcParams.update({'font.size': 14})
def evaluate_preds(train_true_values, train_pred_values, test_true_values, test_pred_values):
    """Вычисляет метрику r2 для обучающей и тестовой выборки, отображает предсказанные и истинные значения на графиках"""
    
    print("Train R2:\t" + str(round(r2(train_true_values, train_pred_values), 3)))
    print("Test R2:\t" + str(round(r2(test_true_values, test_pred_values), 3)))
    
    plt.figure(figsize=(18,10))
    
    plt.subplot(121)
    sns.scatterplot(x=train_pred_values, y=train_true_values)
    plt.xlabel('Predicted values')
    plt.ylabel('True values')
    plt.title('Train sample prediction')
    
    plt.subplot(122)
    sns.scatterplot(x=test_pred_values, y=test_true_values)
    plt.xlabel('Predicted values')
    plt.ylabel('True values')
    plt.title('Test sample prediction')

    plt.show()
# Пути до тренировочной и тестовой выборки
TRAIN_DATASET_PATH = '../input/real-estate-price-prediction-moscow/train.csv'
TEST_DATASET_PATH = '../input/real-estate-price-prediction-moscow/test.csv'
"""
# Загрузка данных<a class='anchor' id='load_data'>
"""
"""
**Описание датасета**

* **Id** - идентификационный номер квартиры
* **DistrictId** - идентификационный номер района
* **Rooms** - количество комнат
* **Square** - площадь
* **LifeSquare** - жилая площадь
* **KitchenSquare** - площадь кухни
* **Floor** - этаж
* **HouseFloor** - количество этажей в доме
* **HouseYear** - год постройки дома
* **Ecology_1, Ecology_2, Ecology_3** - экологические показатели местности
* **Social_1, Social_2, Social_3** - социальные показатели местности
* **Healthcare_1, Helthcare_2** - показатели местности, связанные с охраной здоровья
* **Shops_1, Shops_2** - показатели, связанные с наличием магазинов, торговых центров
* **Price** - цена квартиры
"""
# Загрузка тренировочной выборки
train_df = pd.read_csv(TRAIN_DATASET_PATH)
train_df.head()
train_df.dtypes
# Загрузка тестовой выборки
test_df = pd.read_csv(TEST_DATASET_PATH)
test_df.head()
test_df.dtypes
print('Число строк в трейне: ', train_df.shape[0])
print('Число строк в тесте: ', test_df.shape[0])
print('Число столбцов в трейне: ', train_df.shape[1])
print('Число столбцов в тесте: ', test_df.shape[1])
"""
# Приведение типов<a class='anchor' id='cast_data'>
"""
train_df.dtypes
"""
Несмотря на то, что признак DistrictId имеет тип int64, он является категориальным. Также признак Id - это просто идентификатор для каждой строки. Чтобы они не мешали при анализе вещественных признаков изменим их тип на str.
"""
train_df['Id'] = train_df['Id'].astype(str)
train_df['DistrictId'] = train_df['DistrictId'].astype(str)
train_df.dtypes
"""
# EDA и предобработка данных<a class='anchor' id='eda'>
"""
"""
## Анализ целевой переменной <a class='anchor' id='target_analysis'>
"""
target_name = 'Price'

# Вывод основных статистик для целевой переменной
train_df[target_name].describe()
"""
Это вещественный признак, поэтому мода не имеет смысла. В этом случае она будет равна случайно выбранному значению целевой переменной. Создадим переменные для средней величины и медианы для отображения на графике распределения.
"""
target_mean = round(train_df[target_name].mean(), 2)
target_median = train_df[target_name].median()

print(f'Среднее: {target_mean}\nМедиана: {target_median}')
# Построение графика распределения целевой переменной
plt.figure(figsize = (16, 8))

sns.distplot(train_df[target_name], bins=50)

y = np.linspace(0, 7e-6, 10)
plt.plot([target_mean] * 10, y, label='mean', linewidth=3)
plt.plot([target_median] * 10, y, label='median', linewidth=3)

plt.title('Distribution of target')
plt.legend()
plt.show()
"""
Из графика распределения видно, что выбросов нет, и дополнительно обрабатывать целевое значение не нужно.
"""
"""
## Анализ признаков<a class='anchor' id='features_analysis'>
"""
"""
### Количественные признаки<a class='anchor' id='num_features'>
"""
"""
Выберем все вещественные признаки, выведем для них основные статистики и построим для них гистограммы.
"""
train_df.dtypes
# Вывод основных статистик для вещественных признаков
train_df.describe()
"""
Видно, что признаки **LifeSquare** и **Healthcare_1** имеют много пропусков, которые также нужно будет обработать.
"""
# Построение гистограмм для вещественных признаков
train_df.select_dtypes(include=['float64', 'int64']).drop(target_name, axis=1).hist(figsize=(16, 16), bins=20, grid=False)
plt.show()
"""
Из гистограмм видно, что признаки **Rooms**, **Square**, **LifeSquare**, **KitchenSquare**, **Floor**, **HouseFloor**, **HouseYear** возможно имеют выбросы. Рассмотрим эти признаки отдельно.
"""
"""
Создадим несколько вспомогательных методов для анализа и обработки признаков.
"""
def plot_feature_hist(feature_name, bins_count=20, ylog=False):
    """Строит гистограмму для заданного признака"""
    
    train_df[feature_name].hist(figsize=(10, 10), bins=bins_count, grid=False)
    
    plt.xlabel(feature_name)
    plt.ylabel('Count')
    plt.title(f'Distribution of {feature_name}')
    
    if ylog:
        plt.yscale('log')
    
    plt.show()
def add_outlier_label_feature(outlier_condition, outlier_label_feature_name):
    """Добавляет новый признак с заданным именем - метка выброса в зависимости от переданного в метод условия"""
    
    train_df[outlier_label_feature_name] = 0
    train_df.loc[condition, outlier_label_feature_name] = 1
def get_and_print_quantile(feature_name):
    """Возвращает 2.5, 97.5 квантиль и выводит значения на экран"""
    
    quantile025 = train_df[feature_name].quantile(.025)
    quantile975 = train_df[feature_name].quantile(.975)
    print(f'2.5 квантиль: {quantile025}')
    print(f'97.5 квантиль: {quantile975}')
    return quantile025, quantile975
"""
#### Rooms <a class='anchor' id='rooms'>
"""
# Построение гистограммы для Rooms
plot_feature_hist('Rooms', ylog=True)
# Вывод основных статистик для Rooms
train_df['Rooms'].describe()
train_df['Rooms'].value_counts()
"""
Есть некорректное значение - 0 комнат.

Также есть слишком большие значения - 6, 10, 19 комнат.
"""
"""
Выведем данные, соответствующие большим значениям.
"""
train_df.loc[(train_df['Rooms'] == 10) | (train_df['Rooms'] == 19) | (train_df['Rooms'] == 6)]
"""
Площадь Square примерно соответствует 2 комнатам, заменим значения Rooms >= 6 медианой.
"""
"""
Выведем данные, соответствующие значению 0.
"""
train_df.loc[train_df['Rooms'] == 0]
"""
Возможно имеется в виду картира-студия, поэтому было указано 0 комнат. Такие значения заменим 1.
"""
# Добавление метки выброса
condition = (train_df['Rooms'] == 0) | (train_df['Rooms'] >= 6)
add_outlier_label_feature(condition, 'Rooms_outlier')

# Проверка
train_df[condition]
"""
Заменим выбросы.
"""
# Замена выбросов
train_df.loc[train_df['Rooms'] == 0, 'Rooms'] = 1
train_df.loc[train_df['Rooms'] >= 6, 'Rooms'] = train_df['Rooms'].median()

# Проверка
train_df.loc[train_df['Rooms_outlier'] == 1]
"""
Еще раз построим гистограмму и выведем основные статистики.
"""
# Построение гистограммы после обработки выбросов
plot_feature_hist('Rooms')
"""
Выбросов нет.
"""
# Вывод основных статистик после обработки выбросов
train_df['Rooms'].describe()
train_df['Rooms'].value_counts()
"""
#### KitchenSquare <a class='anchor' id='kitchen_square'>
"""
# Построение гистограммы для KitchenSquare
plot_feature_hist('KitchenSquare', bins_count=100, ylog=True)
# Вывод основных характеристик для KitchenSquare
train_df['KitchenSquare'].describe()
train_df['KitchenSquare'].value_counts()
"""
Есть некорректные значение - 0, 1, 2 и т.д.

Также есть слишком большие значения - 1970, 2014.
"""
"""
Непонятно, как задать верхнюю и нижнюю границу для выбросов, поэтому вычислим 2.5 и 97.5 квантиль.
"""
quantile025, quantile975 = get_and_print_quantile('KitchenSquare')
"""
В качестве правой границы используем 97.5 квантиль. Значения, которые больше правой границы заменим медианой.

Значение 0 в качестве минимальной границы не подходит. Зададим для минимальной границы примерное значение равное 3. Меньшие значения также заменим 3.
"""
# Добавление метки выброса
KitchenSquare_min = 3
condition = (train_df['KitchenSquare'] < KitchenSquare_min) | (train_df['KitchenSquare'] > quantile975)
add_outlier_label_feature(condition, 'KitchenSquare_outlier')

# Проверка
train_df.head()
"""
Заменим выбросы.
"""
# Замена выбросов
train_df.loc[train_df['KitchenSquare'] < KitchenSquare_min, 'KitchenSquare'] = KitchenSquare_min
train_df.loc[train_df['KitchenSquare'] > quantile975, 'KitchenSquare'] = train_df['KitchenSquare'].median()

# Проверка
train_df[train_df['KitchenSquare_outlier'] == 1]
"""
Еще раз построим гистограмму и выведем основные статистики.
"""
# Построение гистограммы после обработки выбросов
plot_feature_hist('KitchenSquare')
"""
Теперь выбросов нет.
"""
# Вывод основных статистик после обработки выбросов
train_df['KitchenSquare'].describe()
train_df['KitchenSquare'].value_counts()
"""
#### Square <a class='anchor' id='square'>
"""
# Построение гистограммы для Square
plot_feature_hist('Square', ylog=True)
# Вывод основных статистик для Square
train_df['Square'].describe()
"""
Есть слишком маленькие значения. Например, min = 1.136.

Также есть подозрительные значения больше 200.
"""
"""
Вычислим квантили для оценки границ выбросов.
"""
quantile025, quantile975 = get_and_print_quantile('Square')
"""
Посмотрим данные, где Square больше правого квантиля.
"""
train_df[train_df['Square'] > quantile975]
"""
Среди таких данных есть и хорошие значения, не похожие на выбросы. Т.е. квартиры с большой площадью и по высокой цене. Квантиль в качестве правой границы не подойдет. 
"""
"""
Исходя из вида гистограммы, рассмотрим значение 250. 
"""
train_df[train_df['Square'] > 250]
"""
Похоже на выбросы - большая площадь по низкой цене. Пусть правая граница равна 250.
"""
Square_max = 250
"""
Определим левую границу. Сначала посмотрим данные, где Square меньше левого квантиля.
"""
train_df[train_df['Square'] < quantile025]
"""
Есть и хорошие значения, не похожие на выбросы. Например, квартиры с маленькой площадью и по невысокой цене. Попробуем уменьшить значение.
"""
train_df[train_df['Square'] < 20]
"""
Некоторые значения вполне хорошие. Сдвинем границу еще левее.
"""
train_df[train_df['Square'] < 15]
"""
Похоже на выбросы. Для такой площади цены довольно высокие. Пусть левая граница равна 15.
"""
Square_min = 15
"""
Значения вне интервала (Square_min, Square_max) заменим медианой.
"""
# Добавление метки выброса
condition = (train_df['Square'] < Square_min) | (train_df['Square'] > Square_max)
add_outlier_label_feature(condition, 'Square_outlier')

# Проверка
train_df[condition]
"""
Заменим выбросы.
"""
# Замена выбросов
train_df.loc[condition, 'Square'] = train_df['Square'].median()

# Проверка
train_df[train_df['Square_outlier'] == 1]
"""
Еще раз построим гистограмму и выведем основные статистики.
"""
# Построение гистограммы после обработки выбросов
plot_feature_hist('Square', ylog=True)
"""
Выбросов нет.
"""
# Вывод основных статистик после обработки выбросов
train_df['Square'].describe()
"""
#### LifeSquare <a class='anchor' id='life_square'>
"""
# Построение гистограммы для LifeSquare
plot_feature_hist('LifeSquare', ylog=True)
# Вывод основных статистик для LifeSquare
train_df['LifeSquare'].describe()
"""
Есть пропуски, обработаем сначала их. Выведем количество пропусков.
"""
# Вывод количества пропусков
train_df['LifeSquare'].isna().sum()
"""
Добавим признак пропуска.
"""
# Добавление метки пропуска
train_df['LifeSquare_nan'] = train_df['LifeSquare'].isna() * 1

# Проверка
train_df.head()
"""
Заменим пропуски по формуле, используя другие признаки, следующим образом.

Вычислим насколько примерно Square больше, чем LifeSquare + KitchenSquare. Для этого построим серию с разностями и вычислим медиану add_square_median.

Пропуски для LifeSquare вычислим как Square - KitchenSquare - add_square_median.
"""
# Построение серии с разностями между Square и LifeSquare + KitchenSquare
add_square_series = train_df['Square'] - train_df['LifeSquare'] - train_df['KitchenSquare']

# Вычисление медианы этих разностей
add_square_median = add_square_series[(add_square_series > 0) & (~add_square_series.isna())].median()

# Заполнение пропусков для LifeSquare по формуле
condition = (train_df['LifeSquare'].isna()) & (~train_df['Square'].isna()) & (~train_df['KitchenSquare'].isna())
train_df.loc[condition, 'LifeSquare'] = train_df.loc[condition, 'Square'] - train_df.loc[condition, 'KitchenSquare'] - add_square_median

# Проверка
train_df[train_df['LifeSquare_nan'] == 1]
"""
Еще раз построим гистограмму и выведем основные статистики.
"""
# Построение гистограммы после обработки пропусков
plot_feature_hist('LifeSquare', ylog=True)
"""
Разницы с исходным вариантом нет.
"""
# Вывод основных статистик после обработки пропусков
train_df['LifeSquare'].describe()
"""
Пропусков нет.
"""
"""
Теперь обработаем выбросы. Построим более детальную гистограмму.
"""
train_df['LifeSquare'].hist(figsize=(10, 10), bins=100, grid=False)
plt.xlim([0, 250])
"""
Есть слишком маленькие значения. Например, min = 0.37.

Также есть подозрительные значения больше 250.
"""
"""
Вычислим квантили для оценки границ выбросов.
"""
quantile025, quantile975 = get_and_print_quantile('LifeSquare')
"""
Использование правого квантиля возможно удалит и нормальные данные. Исходя из вида гистограммы, исследуем сразу правую границу, равную 150.
"""
train_df[train_df['LifeSquare'] > 150]
"""
Есть и хорошие данные - квартиры с большой площадью и по высокой цене. Сдвинем границу еще правее.
"""
train_df[train_df['LifeSquare'] > 250]
"""
Эти данные похожи на выбросы - большие LifeSquare при маленьких Square и при низкой цене. Пусть правая граница равна 250.
"""
LifeSquare_max = 250
"""
Определим левую границу. Сначала посмотрим данные, где Square меньше левого квантиля.
"""
train_df[train_df['LifeSquare'] < quantile025]
"""
Вроде есть и хорошие значения - небольшие квартиры с маленьким LifeSquare. Сдивнем границу еще левее.
"""
train_df[train_df['LifeSquare'] < 10]
"""
Слишком маленькие значения - похоже на выбросы. Пусть левая граница равна 10.
"""
LifeSquare_min = 10
"""
Значения вне интервала (LifeSquare_min, LifeSquare_max) заменим медианой.
"""
# Добавление метки выброса
condition = (train_df['LifeSquare'] < LifeSquare_min) | (train_df['LifeSquare'] > LifeSquare_max)
add_outlier_label_feature(condition, 'LifeSquare_outlier')

# Проверка
train_df[condition]
"""
Заменим выбросы.
"""
# Замена выбросов
train_df.loc[condition, 'LifeSquare'] = train_df['LifeSquare'].median()

# Проверка
train_df[train_df['LifeSquare_outlier'] == 1]
"""
Еще раз построим гистограмму и выведем основные статистики.
"""
# Построение гистограммы после обработки выбросов
plot_feature_hist('LifeSquare', ylog=True)
"""
Значения больше 200 - это не выбросы.
"""
# Вывод основных статистик после обработки выбросов
train_df['LifeSquare'].describe()
"""
#### HouseFloor <a class='anchor' id='house_floor'>
"""
# Построение гистограммы для HouseFloor
plot_feature_hist('HouseFloor', ylog=True)
# Вывод основных статистик для HouseFloor
train_df['HouseFloor'].describe()
# Вывод отсортированного списка уникальных значений
train_df['HouseFloor'].sort_values().unique()
"""
Максимальное число этажей в Москве равно 95, поэтому значения 0, 99, 117 - это выбросы. Посмотрим какие этажи для квартир в этих данных.
"""
train_df[train_df['HouseFloor'] == 0]
"""
Возможно для таких значений подойдет медиана.
"""
train_df[train_df['HouseFloor'] > 90]
"""
Тут также используем медиану.
"""
"""
В качестве правой границы установим 95.
"""
HouseFloor_max = 95
"""
Значения HouseFloor = 0 и больше HouseFloor_max заменим медианой.
"""
# Добавление метки выброса
condition = (train_df['HouseFloor'] == 0) | (train_df['HouseFloor'] > HouseFloor_max)
add_outlier_label_feature(condition, 'HouseFloor_outlier')

# Проверка
train_df[condition]
"""
Заменим выбросы.
"""
# Замена выбросов
train_df.loc[condition, 'HouseFloor'] = train_df['HouseFloor'].median()

# Проверка
train_df[train_df['HouseFloor_outlier'] == 1]
"""
Еще раз построим гистограмму и выведем основные статистики.
"""
# Построение гистограммы после обработки выбросов
plot_feature_hist('HouseFloor', ylog=True)
"""
Выбросов нет.
"""
# Вывод основных статистик после обработки выбросов
train_df['HouseFloor'].describe()
# Вывод отсортированного списка уникальных значений
train_df['HouseFloor'].sort_values().unique()
train_df['HouseFloor'].value_counts()
"""
#### Floor <a class='anchor' id='floor'>
"""
# Построение гистограммы для Floor
plot_feature_hist('Floor', ylog=True)
# Вывод основных статистик для Floor
train_df['Floor'].describe()
# Вывод отсортированного списка уникальных значений
train_df['Floor'].sort_values().unique()
train_df['Floor'].value_counts()
"""
Данные нормальные. Очевидных выбросов нет, но может быть так, что в данных Floor > HouseFloor. Т.е. этаж, на котором расположена квартира, выше, чем всего этажей в доме. 

Выведем количество таких строк.
"""
(train_df['Floor'] > train_df['HouseFloor']).sum()
"""
Запишем индексы таких строк.
"""
# Индексы строк, где Floor > HouseFloor
floor_outliers = train_df.loc[train_df['Floor'] > train_df['HouseFloor']].index
floor_outliers
"""
Посмотрим подробнее.
"""
train_df.loc[floor_outliers]
"""
Отметим такие выбросы в признаке HouseFloor_outlier.
"""
# Дополнение признака HouseFloor_outlier новыми метками о выбросах
train_df.loc[floor_outliers, 'HouseFloor_outlier'] = 1
"""
Такие данные заменим случайным целым числом от 1 до HouseFloor. Зафиксируем random.seed для воспроизводимости.
"""
random.seed(100)

# Замена значения этаж квартиры случайным целым числом в диапазоне [1, HouseFloor)
train_df.loc[floor_outliers, 'Floor'] = train_df.loc[floor_outliers, 'HouseFloor'].apply(lambda x: random.randint(1, x))

# Проверка
train_df.loc[floor_outliers]
"""
Проверим, что после замены таких данных нет.
"""
(train_df['Floor'] > train_df['HouseFloor']).sum()
"""
Еще раз построим гистограмму и выведем основные статистики.
"""
# Построение гистограммы после обработки выбросов
plot_feature_hist('Floor', ylog=True)
# Вывод основных статистик после обработки выбросов
train_df['Floor'].describe()
train_df['Floor'].value_counts()
"""
#### HouseYear <a class='anchor' id='house_year'>
"""
# Построение гистограммы для HouseYear
plot_feature_hist('HouseYear', ylog=True)
# Вывод основных статистик для HouseYear
train_df['HouseYear'].describe()
train_df['HouseYear'].value_counts()
# Вывод отсортированного списка уникальных значений
train_df['HouseYear'].sort_values().unique()
"""
Есть выбросы справа - слишком большие значения.

Выбросы справа будем обрабатывать так: если год больше текущего года, то заменим его не текущий год.
"""
# Добавление метки выброса
current_year = datetime.now().year
condition = train_df['HouseYear'] > datetime.now().year
add_outlier_label_feature(condition, 'HouseYear_outlier')

# Проверка
train_df[condition]
"""
Заменим выбросы.
"""
# Замена выбросов
train_df.loc[condition, 'HouseYear'] = current_year

# Проверка
train_df[train_df['HouseYear_outlier'] == 1]
"""
Еще раз построим гистограмму и выведем основные статистики.
"""
# Построение гистограммы после обработки выбросов
plot_feature_hist('HouseYear', ylog=True)
"""
Выбросов нет.
"""
# Вывод основных статистик после обработки выбросов
train_df['HouseYear'].describe()
"""
#### Healthcare_1 <a class='anchor' id='healthcare_1'>
"""
# Построение гистограммы для Healthcare_1
plot_feature_hist('Healthcare_1', ylog=True)
# Вывод основных статистик для Healthcare_1
train_df['Healthcare_1'].describe()
"""
Невозможно сказать, есть ли выбросы, однако есть много пропусков.

Выведем количество пропусков.
"""
# Вывод количества пропусков
train_df['Healthcare_1'].isna().sum()
"""
Пропусков очень много - почти половина тренирововчной выборки. Непонятно как заполнять эти пропуски.

Для заполнения пропусков обучим модель [Класс для заполнения Healthcare_1](#healthcare_1_model).
"""
"""
Добавим признак пропуска. При обучении модели на заполнение пропусков будем использовать этот признак для определения пропуска.
"""
# Добавление метки пропуска
train_df['Healthcare_1_nan'] = train_df['Healthcare_1'].isna() * 1

# Проверка
train_df.head()
"""
### Категориальные признаки<a class='anchor' id='cat_features'>
"""
"""
Выведем все номинативные признаки - типа object.
"""
# Вывод всех номинативных признаков
train_df.select_dtypes(include='object').columns.tolist()
"""
Исследуем эти признаки.
"""
"""
#### DistrictId <a class='anchor' id='district_id'>
"""
"""
Несмотря на то, что признак DistrictId изначально имеет тип int64, он является категориальным. Его нужно закодировать.
"""
train_df['DistrictId'].value_counts()
"""
Этот признак закодируем через себя - через value_counts, т.е. используем частотное кодирование. Получим признак DistrictSize - по количеству квартир в этом районе.
"""
"""
#### Ecology_2 <a class='anchor' id='ecology_2'>
"""
train_df['Ecology_2'].value_counts()
"""
Здесь из строкового категориального признака сделаем бинарный признак: 

A заменим на 0, 

B заменим на 1.
"""
"""
#### Ecology_3 <a class='anchor' id='ecology_3'>
"""
train_df['Ecology_3'].value_counts()
"""
Здесь закодируем так же, как для Ecology_2
"""
"""
#### Shops_2 <a class='anchor' id='shops_2'>
"""
train_df['Shops_2'].value_counts()
"""
Здесь закодируем так же, как для Ecology_2
"""
"""
### Матрица корреляций <a class='anchor' id='correlation_matrix'>
"""
"""
Построим матрицу корреляций и изобразим ее графически.
"""
plt.figure(figsize=(20, 15))

sns.set(font_scale=1.4)

# Вычисление и округление значений матрицы корреляции
corr_matrix = train_df.corr()
corr_matrix = np.round(corr_matrix, 2)

# Зануление коэффициента корреляции меньше 0.3, чтобы оставить самые существенные
corr_matrix[np.abs(corr_matrix) < 0.3] = 0

sns.heatmap(corr_matrix, annot=True, linewidths=.5, cmap='coolwarm')

plt.title('Correlation matrix')
plt.show()
"""
Некоторые сильные зависимости понятны и очевидны. Например, зависимость между Rooms и Square или LifeSquare и Square, HouseFloor и Floor.

Интересная прямая зависимость между Shops1 и Social_3. Возможно, это как-то связано с количеством магазинов - чем выше показатель, тем выше некий социальный показатель Social_3.

Видно, что целевая переменная **Price** довольно сильно зависит от признаков **Rooms** и **Square**. Возможно будут полезны новые признаки на их основе.
"""
"""
Также есть очень сильная зависимость между двумя непонятными Social_1 и Social_2.

Посмотрим на график корреляции этих двух признаков.
"""
grid = sns.jointplot(train_df['Social_1'], train_df['Social_2'], kind='reg')
grid.fig.set_figwidth(8)
grid.fig.set_figheight(8)

plt.show()
"""
## Анализ влияния признаков на таргет <a class='anchor' id='feature_target_analysis'>
"""
"""
Изобразим зависимости целевого значения от признаков Rooms и Square, с которыми есть наибольшая зависимость. 
"""
grid = sns.jointplot(train_df['Rooms'], train_df['Price'], kind='reg')
grid.fig.set_figwidth(8)
grid.fig.set_figheight(8)

plt.show()
grid = sns.jointplot(train_df['Square'], train_df['Price'], kind='reg')
grid.fig.set_figwidth(8)
grid.fig.set_figheight(8)

plt.show()
"""
# Класс для предобработки данных DataPreprocessing <a class='anchor' id='data_preprocessing_class'>
"""
class DataPreprocessing:
    """Подготовка исходных данных"""
    
    def __init__(self):
        """Параметры класса"""
        self.medians = None
        self.modas = None
        self.kitchen_square_quantile975 = None
        self.add_square_median = None
    
    def fit(self, X):
        """Сохранение статистик"""
        self.medians = X.median()
        self.modas = X.mode().iloc[0]
        self.kitchen_square_quantile975 = X['KitchenSquare'].quantile(.975)
        
        # Вычисление добавочной площади от общей площади, куда не входит жилая площадь и площадь кухни
        add_square_series = X['Square'] - X['LifeSquare'] - X['KitchenSquare']
        self.add_square_median = add_square_series[(add_square_series > 0) & (~add_square_series.isna())].median()
        
        self.num_features = ['Rooms', 'KitchenSquare', 'Square', 'LifeSquare', 'HouseFloor', 'Floor', 'HouseYear', 'Healthcare_1']
        self.cat_features = ['DistrictId', 'Ecology_2', 'Ecology_3', 'Shops_2']
    
    def transform(self, X):
        """Трансформация данных"""
        
        def add_outlier_label_feature(outlier_condition, outlier_label_feature_name):
            X[outlier_label_feature_name] = 0
            X.loc[condition, outlier_label_feature_name] = 1
        
        # Rooms
        Rooms_max = 6
        condition = (X['Rooms'] == 0) | (X['Rooms'] >= Rooms_max)
        add_outlier_label_feature(condition, 'Rooms_outlier')

        X.loc[X['Rooms'] == 0, 'Rooms'] = 1
        X.loc[X['Rooms'] >= Rooms_max, 'Rooms'] = self.medians['Rooms']
        
        # KitchenSquare
        KitchenSquare_min = 3
        condition = (X['KitchenSquare'] < KitchenSquare_min) | (X['KitchenSquare'] > self.kitchen_square_quantile975)
        add_outlier_label_feature(condition, 'KitchenSquare_outlier')
        
        X.loc[X['KitchenSquare'] < KitchenSquare_min, 'KitchenSquare'] = KitchenSquare_min
        X.loc[X['KitchenSquare'] > self.kitchen_square_quantile975, 'KitchenSquare'] = self.medians['KitchenSquare']
        
        # Square
        Square_min = 15
        Square_max = 250
        condition = (X['Square'] < Square_min) | (X['Square'] > Square_max)
        add_outlier_label_feature(condition, 'Square_outlier')
        
        X.loc[condition, 'Square'] = self.medians['Square']
        
        # LifeSquare
        # Обработка пропусков
        X['LifeSquare_nan'] = X['LifeSquare'].isna() * 1
        
        condition = (X['LifeSquare'].isna()) & (~X['Square'].isna()) & (~X['KitchenSquare'].isna())
        X.loc[condition, 'LifeSquare'] = X.loc[condition, 'Square'] - X.loc[condition, 'KitchenSquare'] - self.add_square_median
        
        # Обработка выбросов
        LifeSquare_min = 10
        LifeSquare_max = 250
        condition = (X['LifeSquare'] < LifeSquare_min) | (X['LifeSquare'] > LifeSquare_max)
        add_outlier_label_feature(condition, 'LifeSquare_outlier')
        
        X.loc[condition, 'LifeSquare'] = self.medians['LifeSquare']
        
        # HouseFloor
        HouseFloor_max = 95
        condition = (X['HouseFloor'] == 0) | (X['HouseFloor'] > HouseFloor_max)
        add_outlier_label_feature(condition, 'HouseFloor_outlier')
        
        X.loc[condition, 'HouseFloor'] = self.medians['HouseFloor']
        
        # Floor
        floor_outliers = X.loc[X['Floor'] > X['HouseFloor']].index
        X.loc[floor_outliers, 'HouseFloor_outlier'] = 1
        
        random.seed(21)
        X.loc[floor_outliers, 'Floor'] = X.loc[floor_outliers, 'HouseFloor'].apply(lambda x: random.randint(1, x))
        
        # HouseYear
        current_year = datetime.now().year
        condition = X['HouseYear'] > current_year
        add_outlier_label_feature(condition, 'HouseYear_outlier')
        
        X.loc[condition, 'HouseYear'] = current_year
        
        # Healthcare_1
        # Обработка пропусков
        X['Healthcare_1_nan'] = X['Healthcare_1'].isna() * 1
        
        X[self.num_features] = X[self.num_features].fillna(self.medians[self.num_features])
        X[self.cat_features] = X[self.cat_features].fillna(self.modas[self.cat_features])
        
        return X
"""
# Построение новых признаков <a class='anchor' id='feature_generation'>
"""
"""
## Dummies <a class='anchor' id='dummies'>
"""
"""
Для преобразования Ecology_2, Ecology_3, Shops_2 используем бинарное кодирование.
"""
binary_to_numbers = {'A': 0, 'B': 1}

# Бинарное кодирование
train_df['Ecology_2'] = train_df['Ecology_2'].replace(binary_to_numbers)
train_df['Ecology_3'] = train_df['Ecology_3'].replace(binary_to_numbers)
train_df['Shops_2'] = train_df['Shops_2'].replace(binary_to_numbers)

# Проверка
train_df[['Ecology_2', 'Ecology_3', 'Shops_2']]
"""
## DistrictSize, IsDistrictLarge <a class='anchor' id='district_size'>
"""
"""
Для преобразования DistrcitId используем частотное кодирование.

Для каждого района DistrictId вычислим количество квартир в этом районе и создадим таблицу, которая содержит DistrictId и DistrictSize - число квартир в этом районе.
"""
# Создание таблицы DistrictId и DistrictSize - число квартир в этом районе
district_size = train_df['DistrictId'].value_counts().reset_index().rename(columns={'index':'DistrictId', 'DistrictId':'DistrictSize'})
district_size.head()
"""
Присоединим полученную таблицу к исходному датафрейму по DistrictId. При этом может быть так, что при преобразовании тестовой выборки появятся DistrictId, которых нет в таблице district_size. В этом случае будем заполнять пропуски модой DistrictSize.
"""
# Присоединение полученной таблицы к тренировочной выборке
train_df = train_df.merge(district_size, on='DistrictId', how='left')
train_df.head()
"""
Добавим новый признак по количеству квартир в этом районе IsDistrictLarge. Границу установим равной 100, т.к. она делит все значения DistrictSize примерно на 2 части.
"""
(train_df['DistrictSize'] > 100).value_counts()
# Добавление нового признака IsDistrictLarge
train_df['IsDistrictLarge'] = (train_df['DistrictSize'] > 100).astype(int)
train_df.head()
"""
## MedPriceByDistrict <a class='anchor' id='med_price_by_district'>
"""
"""
Для создания этого признака используем кодирование через целевую переменную.

Сгруппируем данные по DistrictId и количеству комнат и вычислим медиану цен квартир. Это будет новым признаком для заданных DistrictId и количества комнат.
"""
# Группировка по DistrictId и Rooms и вычисление медианы цены по группе
med_price_by_district = train_df.groupby(['DistrictId', 'Rooms'], as_index=False).agg({'Price':'median'}).rename(columns={'Price':'MedPriceByDistrict'})
med_price_by_district.head()
"""
Присоединим полученную таблицу к исходному датафрейму по DistrictId и Rooms. 
"""
# Присоединение полученной таблицы к тренировочной выборке
train_df = train_df.merge(med_price_by_district, on=['DistrictId', 'Rooms'], how='left')
train_df.head()
"""
При этом может быть так, что при преобразовании тестовой выборки появятся DistrictId, которых нет в таблице med_price_by_district. В этом случае будем заполнять пропуски медианой MedPriceByDistrict.
"""
"""
## MedPriceByFloorYear <a class='anchor' id='med_price_by_floor_year'>
"""
"""
Для создания этого признака используем кодирование через целевую переменную.

Сгруппируем по Floor и HouseYear и вычислим медиану цен квартир. Это будет новым признаком для заданных Floor и HouseYear.
"""
# Группировка по Floor и HouseYear и вычисление медианы цены по группе
med_price_by_floor_year = train_df.groupby(['Floor', 'HouseYear'], as_index=False).agg({'Price':'median'}).rename(columns={'Price':'MedPriceByFloorYear'})
med_price_by_floor_year.head()
"""
Присоединим полученную таблицу к исходному датафрейму по Floor и HouseYear.
"""
# Присоединение полученной таблицы к тренировочной выборке
train_df = train_df.merge(med_price_by_floor_year, on=['Floor', 'HouseYear'], how='left')
train_df.head()
"""
При этом может быть так, что при преобразовании тестовой выборки появятся DistrictId, которых нет в таблице med_price_by_floor_year. В этом случае будем заполнять пропуски медианой MedPriceByFloorYear.
"""
"""
# Класс для добавления новых признаков FeatureGenerator <a class='anchor' id='feature_generator'>
"""
class FeatureGenerator():
    """Генерация новых фич"""
    
    def __init__(self):
        self.binary_to_numbers = None
        self.district_size = None
        self.med_price_by_district = None
        self.med_price_by_floor_year = None
        self.med_price_by_district_median = None
        self.med_price_by_floor_year_median = None
        self.district_size_mode = None
    
    def fit(self, X, y=None):
        # Binary features
        self.binary_to_numbers = {'A': 0, 'B': 1}
        
        # DistrictSize
        self.district_size = X['DistrictId'].value_counts().reset_index().rename(columns={'index':'DistrictId', 'DistrictId':'DistrictSize'})
        self.district_size_mode = self.district_size['DistrictSize'].mode()[0]
        
        # Target encoding
        if y is not None:
            X_copy = X.copy()
            X_copy['Price'] = y.values

            # MedPriceByDistrict
            self.med_price_by_district = X_copy.groupby(['DistrictId', 'Rooms'], as_index=False).agg({'Price':'median'}).rename(columns={'Price':'MedPriceByDistrict'})
            self.med_price_by_district_median = self.med_price_by_district['MedPriceByDistrict'].median()
            
            # MedPriceByFloorYear 
            self.med_price_by_floor_year = X_copy.groupby(['Floor', 'HouseYear'], as_index=False).agg({'Price':'median'}).rename(columns={'Price':'MedPriceByFloorYear'})
            self.med_price_by_floor_year_median = self.med_price_by_floor_year['MedPriceByFloorYear'].median()
    
    def transform(self, X):
        # Binary features
        X['Ecology_2'] = X['Ecology_2'].map(self.binary_to_numbers)
        X['Ecology_3'] = X['Ecology_3'].map(self.binary_to_numbers)
        X['Shops_2'] = X['Shops_2'].map(self.binary_to_numbers)
        
        # DistrictSize
        X = X.merge(self.district_size, on='DistrictId', how='left')
        X['DistrictSize'].fillna(self.district_size_mode, inplace=True)
        
        X['IsDistrictLarge'] = (X['DistrictSize'] > 100).astype(int)
        
        # Target encoding
        # MedPriceByDistrict
        if self.med_price_by_district is not None:
            X = X.merge(self.med_price_by_district, on=['DistrictId', 'Rooms'], how='left')
            X['MedPriceByDistrict'].fillna(self.med_price_by_district_median, inplace=True)
        
        # MedPriceByFloorYear
        if self.med_price_by_floor_year is not None:
            X = X.merge(self.med_price_by_floor_year, on=['Floor', 'HouseYear'], how='left')
            X['MedPriceByFloorYear'].fillna(self.med_price_by_floor_year_median, inplace=True)
        
        return X
"""
# Класс для заполнения Healthcare_1 <a class='anchor' id='healthcare_1_model'>
"""
"""
Признак Healthcare_1 имеет много пропусков, и непонятно как их заполнять.

Для заполнения обучим модель.
"""
# Вывод списка всех признаков
train_df.columns.tolist()
"""
Для обучения будем использовать все признаки, кроме **Id**, **DistrictId**, **Healthcare_1**, **Price**.

Также будем использовать данные, где Healthcare_1 не имеет пропусков.
"""
# Создание выборки для обучения, которая включает такие данные, где Healthcare_1 не имеет пропусков
X = train_df[~train_df['Healthcare_1'].isna()].drop(columns=['Id', 'DistrictId', 'Healthcare_1', 'Price'])
X.head()
# Создание целевого признака - признак Healthcare_1 без пропусков
y = train_df.loc[~train_df['Healthcare_1'].isna(), 'Healthcare_1']
y.head()
# Разбиение на тренировочный и валидационный датасеты
X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.33, shuffle=True, random_state=21)
X_train.shape, X_valid.shape
"""
Параметры модели подберм вручную.
"""
# Обучение модели
model = RandomForestRegressor(criterion='mse',
                              max_depth=10,
                              min_samples_leaf=10,
                              random_state=21, 
                              n_estimators=100)

model.fit(X_train, y_train)
# Предсказание и результаты обучения
y_train_preds = model.predict(X_train)
y_valid_preds = model.predict(X_valid)

evaluate_preds(y_train, y_train_preds, y_valid, y_valid_preds)
"""
Получился хороший результат. Используем полученную модель и создадим класс для заполнения пропусков в Healthcare_1.
"""
class HealthcareFiller():
    """Класс для заполнения пропусков признака Healthcare_1"""
    
    def __init__(self):
        self.model = RandomForestRegressor(criterion='mse',
                                           max_depth=10,
                                           min_samples_leaf=10,
                                           random_state=21, 
                                           n_estimators=100)
        self.feature_names = ['Rooms', 'Square', 'LifeSquare', 'KitchenSquare', 'Floor', 'HouseFloor', 'HouseYear', 
                              'Ecology_1', 'Ecology_2', 'Ecology_3', 'Social_1', 'Social_2', 'Social_3', 'Helthcare_2', 
                              'Shops_1', 'Shops_2', 'Rooms_outlier', 'KitchenSquare_outlier', 'Square_outlier', 
                              'LifeSquare_nan', 'LifeSquare_outlier', 'HouseFloor_outlier', 'HouseYear_outlier', 'Healthcare_1_nan', 
                              'DistrictSize', 'IsDistrictLarge', 'MedPriceByDistrict', 'MedPriceByFloorYear']

        self.target_name = 'Healthcare_1'
    
    def fit(self, X):
        X_train = X[self.feature_names]
        
        # Для обучения выбираются строки, где Healthcare_1 не равен NaN
        X_train = X_train[X_train['Healthcare_1_nan'] == 0]
        y_train = X.loc[X['Healthcare_1_nan'] == 0, self.target_name]
        self.model.fit(X_train, y_train)
    
    def transform(self, X):
        X_preds = X[self.feature_names]
        
        # Для предсказание выбираются строки, где Healthcare_1 = NaN
        X_preds = X_preds[X_preds['Healthcare_1_nan'] == 1]
        
        y_preds = self.model.predict(X_preds)
        
        # Замена NaN-значений для Healthcare_1
        X.loc[X['Healthcare_1_nan'] == 1, self.target_name] = y_preds
        
        return X
"""
# Отбор признаков <a class='anchor' id='features_select'>
"""
"""
Выведем список всех признаков.
"""
# Вывод списка всех признаков
train_df.columns.tolist()
"""
Для обучения будем использовать только некоторые признаки. 

При этом некоторые признаки не используются, т.к. далее при обучении модели выяснилось, что они либо бесполезные, либо ведут к переобучению. Это признаки **Rooms_outlier**, **HouseYear_outlier**, **MedPriceByDistrict**.
"""
feature_names = ['Rooms', 'Square', 'LifeSquare', 'KitchenSquare', 'Floor', 'HouseFloor', 'HouseYear',
                 'Ecology_1', 'Ecology_2', 'Ecology_3', 'Social_1', 'Social_2', 'Social_3',
                 'Healthcare_1', 'Helthcare_2', 'Shops_1', 'Shops_2']

new_feature_names = ['KitchenSquare_outlier', 'Square_outlier', 'LifeSquare_nan', 'LifeSquare_outlier',
                     'HouseFloor_outlier', 'Healthcare_1_nan', 'DistrictSize',
                     'IsDistrictLarge', 'MedPriceByFloorYear']

target_name = 'Price'
"""
# Разбиение на train и test <a class='anchor' id='train_test_split'>
"""
# Считывание обучающего и тестового датасетов
train_df = pd.read_csv(TRAIN_DATASET_PATH)
test_df = pd.read_csv(TEST_DATASET_PATH)

# Создание обучающей выборки и целевой переменной
X = train_df.drop(columns=target_name)
y = train_df[target_name]
# Разбиение обучающей выборки на тренировочный и валидационный датасеты
X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.33, shuffle=True, random_state=21)
# Обработка выбросов и пропусков для всех выборок
preprocessor = DataPreprocessing()
preprocessor.fit(X_train)

X_train = preprocessor.transform(X_train)
X_valid = preprocessor.transform(X_valid)
test_df = preprocessor.transform(test_df)

X_train.shape, X_valid.shape, test_df.shape
# Добавление новых признаков во все выборки
features_gen = FeatureGenerator()
features_gen.fit(X_train, y_train)

X_train = features_gen.transform(X_train)
X_valid = features_gen.transform(X_valid)
test_df = features_gen.transform(test_df)

X_train.shape, X_valid.shape, test_df.shape
# Заполнение пропусков в признаке Healthcare_1 во всех выборках
filler = HealthcareFiller()
filler.fit(X_train)

X_train = filler.transform(X_train)
X_valid = filler.transform(X_valid)
test_df = filler.transform(test_df)

X_train.shape, X_valid.shape, test_df.shape
# Отбор признаков для обучения и предсказания
X_train = X_train[feature_names + new_feature_names]
X_valid = X_valid[feature_names + new_feature_names]
test_df = test_df[feature_names + new_feature_names]

X_train.shape, X_valid.shape, test_df.shape
# Проверка, что нет пропусков во всех выборках
X_train.isna().sum().sum(), X_valid.isna().sum().sum(), test_df.isna().sum().sum()
"""
# Построение модели <a class='anchor' id='model'>
"""
"""
## Обучение <a class='anchor' id='fit_model'>
"""
"""
Для обучения были использованы следующие модели: **RandomForestRegressor**, **GradientBoostingRegressor**, **StackingRegressor** и **CatBoostRegressor**.

Худший результат по метрике и по переобучению показала модель RandomForestRegressor.

Для моделей GradientBoostingRegressor, StackingRegressor результат получился примерно одинаковый. Для GradientBoostingRegressor был проведен поиск параметров по сетке.

Самый лучший результат показала модель **CatBoostRegressor** при использовании параметров по умолчанию. Для этой модели также был проведен поиск по сетке, однако полученные параметры не улучшили результат.
"""
# Создание модели с параметрами по умолчанию
cb_model = CatBoostRegressor(logging_level='Silent')

# Обучение
cb_model.fit(X_train, y_train, verbose=False)
"""
## Оценка модели <a class='anchor' id='evaluate_model'>
"""
# Предсказание на тренировочной и валидационной выборках
y_train_preds = cb_model.predict(X_train)
y_valid_preds = cb_model.predict(X_valid)

# Вывод результатов
evaluate_preds(y_train, y_train_preds, y_valid, y_valid_preds)
"""
Разность метрик около 16%. Значит модель немного переобучена.

Из графика можно сделать вывод, что модель довольно плохо предсказывает цену для квартир с невысокой стоимостью - цена получается завышенной.
"""
"""
## Важность признаков <a class='anchor' id='feature_importance'>
"""
# Вывод списка признаков, отсортированного по важности
feature_importances = pd.DataFrame(zip(X_train.columns, cb_model.feature_importances_), 
                                   columns=['feature_name', 'importance'])

feature_importances.sort_values(by='importance', ascending=False)
"""
Видно, что новый признак MedPriceByFloorYear оказался полезным.
"""
"""
## Кросс-валидация <a class='anchor' id='cv_model'>
"""
cv_score = cross_val_score(cb_model, X_train, y_train, scoring='r2', cv=KFold(n_splits=3, shuffle=True, random_state=21))
cv_score
cv_score.mean()
"""
Метрики отличаются друг от друга не сильно, значит модель скорее всего устойчива.
"""
"""
## Поиск по сетке <a class='anchor' id='grid_search'>
"""
"""
Был проведен поиск по сетке со следующими параметрами. Сейчас в виде комментария, т.к. расчет происходит очень долго.


Полученные параметры:

depth=6, l2_leaf_reg=3, learning_rate=0.03.

Т.е. только depth получился не равным значению по умолчанию.
"""
# search_model = CatBoostRegressor()
# grid = {'learning_rate': [0.03, 0.1],
#         'depth': [4, 6, 10],
#         'l2_leaf_reg': [1, 3, 5, 7, 9],
#         'logging_level':['Silent'],
#         'random_seed': [42]
#        }

# gs = GridSearchCV(search_model, grid, 
#                   scoring='r2',
#                   cv=KFold(n_splits=5,
#                            random_state=21, 
#                            shuffle=True),
#                   n_jobs=-1
#                   )

# gs.fit(X_train, y_train)
# gs.best_params_
"""
# Прогнозирование на тестовом датасете <a class='anchor' id='test_preds'>
"""
test_df.shape
test_df
# Считывание проверочного файла
submit = pd.read_csv('/kaggle/input/real-estate-price-prediction-moscow/sample_submission.csv')
submit.head()
# Предсказание на тестовом датасете
predictions = cb_model.predict(test_df)
predictions
# Заполнение проверочного файла предсказанными значениями цены
submit['Price'] = predictions
submit.head()
# Сохранение результата в файл
submit.to_csv('catboost_submit.csv', index=False)