# 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
"""
## Steps I followed:
1. Importing data
2. Cleaning data and data exploration
3. Feature engineering (choosing required features)
4. Data visualization
5. Finding if there exists corr among variables
6. train test split
7. Model building and Hyper-parameter tuning
8. using various sklearn.metrics : mean_absolute_error, mean_squared_error, np.sqrt(mean_squared_error)
9. converting the model to pickle file for future use
"""
# Importing Dependencies
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import Lasso
from sklearn.ensemble import RandomForestRegressor, ExtraTreesRegressor
from sklearn.preprocessing import MinMaxScaler
from sklearn.preprocessing import OneHotEncoder
from sklearn.preprocessing import LabelEncoder
from sklearn.feature_selection import SelectFromModel
import xgboost as xgb
from sklearn.model_selection import RandomizedSearchCV
from sklearn import metrics
# Importing data
dataset = pd.read_csv("../input/vehicle-dataset-from-cardekho/car data.csv")
"""
# About features
1. **name** - Name of the cars
2. **year** - Year of the car when it was bought
3. **selling_price** - Price at which the car is being sold
4. **km_driven** - Number of Kilometres the car is driven
5. **fuel** - Fuel type of car
6. **seller_type** - tells if a seller is individual or a dealer
7. **transmission** - Gear transmission fo the car
8. **owner** - Number of previous owners of the car
"""
# top 5-rows of dataset
dataset.head()
# getting more familier with the dataset
dataset.info()
"""
### Selling_Price is the target feature
"""
"""
# Missing Values
"""
# Checking if there is any missing value
dataset.isnull().sum()
"""
# Categorical Variable Encoding
"""
cat_features = [feature for feature in dataset.columns if dataset[feature].dtypes in ['object']]
cat_features
"""
### Categorical feature distribution with 'Selling_Price'
"""
dataset.shape
dataset.Car_Name.nunique()
# There are 98 different features
# Mean prices per category of categorical variables
plt.figure(figsize=(15,8))
for feature in cat_features:
    dataset.groupby(feature)['Selling_Price'].mean().plot.bar()
    plt.title(feature)
    plt.show()
"""
#### Observation of categorical features
1. Few car names have higher prices, like 'fortuner', 'innova', 'land cruiser', etc.
2. In Fuel_Type, 'Diesel' type cars have higher prices as compared to CNG or Petrol driven cars
3. In seller_type, 'Dealer' are selling cars with higher prices than 'Individual'
4. And finally, 'Automatic' cars are being sold for higher prices than 'Manual' cars
"""
# count of prices per category of categorical variables
plt.figure(figsize=(15,8))
for feature in cat_features:
    dataset.groupby(feature)['Selling_Price'].count().plot.bar()
    plt.title(feature)
    plt.show()
"""
# Temporal Feature: Features releted to time, date or year
"""
temporal_feature = [feature for feature in dataset.columns if 'year' in feature.lower() or 'yr' in feature.lower()]
temporal_feature
"""
##### Updating the temporal feature with number years since the car was bought
"""
for feature in temporal_feature:
    dataset[feature] = 2021 - dataset[feature]
dataset.head()
"""
#### Now 'year' variable represents the number of years passed since the car was bought
"""
"""
# Numerical Variables
"""
"""
##### finding correlation between all the numerical values with target variable Selling_Price
"""
dataset.corr()['Selling_Price'].drop('Selling_Price', axis=0) # pandas.core.frame.DataFrame
"""
##### Observation: The Selling_Price is linearly directly correlated with Present_Price
"""
dataset[['Year', 'Present_Price',  'Kms_Driven', 'Owner']].head()
# Checking the number of unique values present per numerical variable
dataset[['Year', 'Present_Price',  'Kms_Driven', 'Owner']].nunique()
dataset[['Year', 'Present_Price',  'Kms_Driven', 'Owner']].info()
"""
##### Although 'Kms_Driven' is of type int64, but it should be continuous numerical variable
"""
# Changing the datatype of Kms_driven
dataset.Kms_Driven = dataset.Kms_Driven.astype('float64')
dataset[['Year', 'Present_Price',  'Kms_Driven', 'Owner']].info()
"""
### Discrete Numerical Variables
"""
discrete_num_features = [feature for feature in dataset.columns if dataset[feature].dtypes in ['int64']]
discrete_num_features
# Let's see the relationship of discrete numerical variables with target variable ('SalePrice')
for feature in discrete_num_features:
    dataset.groupby(feature)['Selling_Price'].mean().plot.bar()

    plt.xlabel(feature)
    plt.ylabel('Selling_Price')
    plt.title(feature)
    plt.show()
"""
##### Observations: 
1. With the increase in the number of years, we can see a clear decline in Selling Price
2. In case of Owner, if there was no previous (0 owner) the selling price is highest, but the 3 owner mean > 1 onwer; we need to check the median value here
"""
# Let's see the relationship of discrete numerical variables with target variable ('SalePrice')
for feature in discrete_num_features:
    dataset.groupby(feature)['Selling_Price'].median().plot.bar()

    plt.xlabel(feature)
    plt.ylabel('Selling_Price')
    plt.title(feature)
    plt.show()
"""
##### Observations: 
1. With the increase in the number of years, we can see a clear decline in Selling Price
2. In case of Owner, if there was no previous (0 owner) the selling price is highest, but the 3 owner meadian > 1 onwer median.
"""
"""
### Continuous Numerical Variables
"""
continuous_num_features = [feature for feature in dataset.columns if dataset[feature].dtypes == 'float64']
continuous_num_features
# dropping target variable from continuous_num_features list
continuous_num_features.remove('Selling_Price')
continuous_num_features
# Let's analyse the continuous values by creating histograms to understand the distribution
for feature in continuous_num_features:
    dataset[feature].hist(bins=30)
    plt.xlabel(feature)
    plt.ylabel('count')
    plt.title(feature)
    plt.show()
# Let's analyse the continuous values by creating histograms to understand the distribution
for feature in continuous_num_features:
    sns.histplot(data=dataset, x=feature, kde=True)
    plt.xlabel(feature)
    plt.ylabel('count')
    plt.title(feature)
    plt.show()
"""
##### Obseravation: Continuous variables are right skewed (mode < median < mean)
"""
"""
### Outliers : Checking outliers in numerical variables
"""
# boxplot to visualize outliers

for feature in discrete_num_features + continuous_num_features:
    dataset.boxplot(column = feature)
    plt.ylabel(feature)
    plt.title(feature)
    plt.show()
"""
##### Observations: All the numerical variables have outliers. Had there been any missing values, we would have replaced with median() instead of mean()
"""
"""
# Splitting Data
"""
X = dataset.drop('Selling_Price', axis=1)
y = dataset.Selling_Price
X.head()
"""
# Categorical variable Encoding
### Label encoding and One-hot encoding
"""
"""
#### Label Encoding for Fuel_Type, Seller_Type, Transmission
"""
# label encoding
car_dataset = X.copy()
car_dataset.replace({'Fuel_Type':{'Petrol':3, 'Diesel':1, 'CNG':2}}, inplace=True)
car_dataset.replace({'Seller_Type':{'Dealer':1, 'Individual':2}}, inplace=True)
car_dataset.replace({'Transmission':{'Manual':2, 'Automatic':1}}, inplace=True)
"""
# Train - Test Split
"""
x_train, x_test, y_train, y_test = train_test_split(car_dataset, y, random_state=1, test_size=0.1)
"""
### One-hot encoding for Car-Name
"""
x_train.head()
# 'handle_unknown' helps to discard categories not seen during fit
encoder_OH = OneHotEncoder(handle_unknown = 'ignore', sparse=False)

train_encoded = pd.DataFrame(encoder_OH.fit_transform(x_train[['Car_Name']]), index = x_train.index)
test_encoded = pd.DataFrame(encoder_OH.transform(x_test[['Car_Name']]), index = x_test.index)

train_OH = pd.concat([x_train.select_dtypes(include = ['int64', 'float64']), train_encoded], axis = 1)
test_OH = pd.concat([x_test.select_dtypes(include = ['int64', 'float64']), test_encoded], axis = 1)
print(train_OH.shape)
print(test_OH.shape)
# total number of features = 101
"""
### Feature Selection
"""
# Next setps:
#     1. feature scaling 
#     2. feature selection using lasso and select from model
#     3. model building using treebase ensemble models randomforest, extra tree, xgboost
#     4. use of randomized search cv for hyperparameter tuning
#     5. selecting the best model out of them
#     6. printing the test score
train_cols = train_OH.columns

# MinMax scaler object
scaler = MinMaxScaler()

# transforming train data
x_train_encoded_scaled = pd.DataFrame(scaler.fit_transform(train_OH), columns = train_cols)

# transforming test data
x_test_encoded_scaled = pd.DataFrame(scaler.transform(test_OH), columns = train_cols)
"""
## Feature Selection using Lasso regression to select important features
#### This is the reason why I performed feature scaling
"""
# The bigger the alpha for Lasso, less features gets selected
# SelectFromModel selects features whose coefficients are non-zero
# feature selection using training data
feature_sel = SelectFromModel(Lasso(alpha = 0.01, random_state=1, max_iter=10000))
feature_sel.fit(x_train_encoded_scaled, y_train)

selected_features = x_train_encoded_scaled.columns[feature_sel.get_support()]
len(selected_features)
"""
##### Out of 101 features, only 24 got selected
"""
np.array(selected_features)
"""
# Model building
"""
"""
### Refer below articles to know more about Tree based algorithms
### [Decision trees and ensemble methods do not require feature scaling to be performed as they are not sensitive to the the variance in the data](https://towardsdatascience.com/do-decision-trees-need-feature-scaling-97809eaa60c6)

### [ExtraTreesRegressor vs RandomForestRegressor](https://quantdare.com/what-is-the-difference-between-extra-trees-and-random-forest/)
"""
# # ExtraTreesRegressor and RandomForestRegressor have exactly same set of parameters,

# Important differences among them exists as below:
    # 1. Random forest uses bootstrap replicas (bootstrap == True), i.e., it subsamples the input data with replacement. 
    # But Extra Trees uses the whole original samples (bootstrap == False).

    # 2. Random forest chooses optimum split for splitting nodes (computationally costly), but ExtraTrees chooses splits randomly
# Considering features selected using lasso regression
x_train_final = train_OH[selected_features]
x_test_final = test_OH[selected_features]
x_train_final.head()
"""
#### Hyper-parameter Tuning
"""
params_ET_RF = {
'n_jobs' : [-1],
'n_estimators' : [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200],
'criterion' : ['mse', 'mae'],
'max_depth' : [5, 10, 15, 20, 25, 30],
'max_features' : ['auto', 'sqrt'],
'min_samples_split' : [2, 5, 10, 15, 100],
'min_samples_leaf' : [1, 2, 5, 10]
}

params_XGBRegressor = {
'n_jobs' : [-1],
'learning_rate' : [0.05, 0.1, 0.15, 0.2, 0.25, 0.3],
'max_depth' : [3,4,5,6,8,10,12,15],
'n_estimators' : [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200],
"min_child_weight" : [1,3,5,7],
"gamma" : [0.0, 0.1, 0.2, 0.3, 0.4],
"colsample_bytree" : [0.3, 0.4, 0.5, 0.6]
}

regressors = [ExtraTreesRegressor(), RandomForestRegressor(), xgb.XGBRegressor()]
params = [params_ET_RF, params_ET_RF, params_XGBRegressor]
names = ['ExtraTreesRegressor', 'RandomForestRegressor', 'XGBRegressor']
# looping through each regressor
for i in range(3):
    cv_regressor = RandomizedSearchCV(regressors[i], param_distributions = params[i], n_iter = 5, scoring = 'neg_mean_squared_error', n_jobs=-1, cv = 5)
    # scorings='roc_auc' for classification problems
    # scorings='neg_mean_squared_error' for regression problems

    cv_regressor.fit(x_train_final,y_train)

    print("************",names[i],"************")
    print("Best estimators: \n {}".format(cv_regressor.best_estimator_))
    print()
    print("Best score: \n {}".format(cv_regressor.best_score_))
    print()
    print("Best parameters: \n {}".format(cv_regressor.best_params_))
    print()
    print()
"""
##### XGBRegressor performed best
"""
"""
### Final Prediction
"""
# defination of final model
final_model = xgb.XGBRegressor(base_score=0.5, booster='gbtree', colsample_bylevel=1,
             colsample_bynode=1, colsample_bytree=0.3, gamma=0.3, gpu_id=-1,
             importance_type='gain', interaction_constraints='',
             learning_rate=0.05, max_delta_step=0, max_depth=3,
             min_child_weight=5,  monotone_constraints='()',
             n_estimators=1100, n_jobs=-1, num_parallel_tree=1, random_state=0,
             reg_alpha=0, reg_lambda=1, scale_pos_weight=1, subsample=1,
             tree_method='exact', validate_parameters=1, verbosity=None)

# fitting and prediction
final_model.fit(x_train_final, y_train)
predictions = final_model.predict(x_test_final)
"""
## Performance Metrics
"""
from sklearn import metrics

# performance metrices
print('MAE:', metrics.mean_absolute_error(y_test, predictions))
print('MSE:', metrics.mean_squared_error(y_test, predictions))
print('RMSE:', np.sqrt(metrics.mean_squared_error(y_test, predictions)))


import pickle
# open a file, where you ant to store the data
file = open('final_model.pkl', 'wb')

# dump information to that file
pickle.dump(final_model, file)