<jupyter_start><jupyter_text>Data Science For Suppy Chain & Operations  * Après les travaux d'analyse exploratoire des données et de création et préparation des variables, nous nous intéressons à l'implémentation et l'évaluation de modèles **d'apprentissage supervisé*** A noter que ce Notebook contient quelques exemples de modèles auxquels on s'intéressera. La liste de ces modèles n'est pas du tout exhaustive.  0. Rappel - Problématique et jeu de donnée préparés<jupyter_code>import numpy as np
import pandas as pd
## On peut choisir le nombre max de lignes/colonnes à voir
pd.set_option('display.max_columns', 500)
pd.set_option("display.max_rows", 500)
pd.options.display.float_format = '{:,.2f}'.format

import datetime

import matplotlib.pyplot as plt
%matplotlib inline
from matplotlib.dates import DateFormatter
import seaborn as sns
sns.set(color_codes = True)
sns.set_style("whitegrid")

import warnings
warnings.filterwarnings("ignore")

from sklearn.preprocessing import OneHotEncoder,  FunctionTransformer, OrdinalEncoder
#scaler
from sklearn.preprocessing import StandardScaler
#column transformer  and pipeline
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline, make_pipeline
#model selection 
from sklearn.model_selection import cross_val_score, KFold, GroupShuffleSplit
from sklearn.model_selection import GridSearchCV
#linear models
from sklearn.linear_model import LogisticRegressionCV, LassoCV
#ensemble models
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor, GradientBoostingClassifier, GradientBoostingRegressor
#evaluation 
from sklearn.metrics import mean_squared_error, r2_score<jupyter_output><empty_output><jupyter_text>Charger le fichier de données obtenu de l'autre Notebook<jupyter_code># Lire les données du dataset
path = 'dataset/data.csv'
df = pd.read_csv(path, parse_dates = ["Date"])<jupyter_output><empty_output><jupyter_text>**Rappel de l'objectif** : Prédire les ventes sur une période de $6$  semaines (ou autres) en construisant un modèle de prédiction basé sur une sélection de variables les plus pertinentes pour permettre également d'expliquer l'évolution des ventes. **Intérêt métier**:- Une entreprise qui estime mal ses ventes peut :    - produire trop et perdre de l'argent en coûts de stockage, (même si elle peut limiter les pertes en procédant à des discounts sur son surplus)     - Produire trop peu et dans ce cas là il y a un manque à gagner dû à la demande non satisfaite - De plus, un modèle de prévision des ventes peut permettre d'identifier les facteurs qui influent sur les ventes et peut donc permettre d'identifier des leviers d'actions booster les ventes Pour la modélisation, on peut penser à différents scénarios ou différentes stratégies, par exemple :    - Un modèle de séries temporelles    - Un modèle de Machine learning où l'on créé des features temporelles     - etc.- Dans la suite, nous allons aborder la **deuxième approche** (Machine Learning) à travers différents modèles. La première approche nécessiterait de construire un modèle par store et il se peut que la profondeur historique des données ne soit pas suffisante pour créer des modèles performants. Lors du cours précédent, nous avons réalisé des premiers travaux d'analyses des données et de préparation d'un data set agrégé. Nous continuerons cette préparation dans ce Notebook avant d'enchainer sur les modèles.<jupyter_code>df.head()
# Affichage d'informations sur les données 
df.info()
# Affichage de la taille du dataset (n_lignes et n_colonnes) 
print("Data's shape : ", df.shape)
# La fonction housing.isna().sum() de Pandas est utilisée pour détecter et compter 
# les valeurs manquantes en fonction de la colonne 
missing = df.isna().sum() 
print(missing) 
# Autrement
missing = df.isnull().sum().sort_values(ascending=False)
print(missing)
# Affichage d'informations statistiques sur les données 
# La méthode describe() ignore les valeurs NULL 
df.describe()
## Quelques fonctions utiles implémentées précédemment
def show_distribution(df, var, n_bins=50, stat="count"):
    """
    Args:
        df : data frame
        var : nom de la colonne de df, pour laquelle on souhaite afficher les graphes de distribution
        n_bins : nombre de bins (barres verticales) dans l'histogramme
        stat : "frequency", "count", "density", or "probability", pour l'histogramme
    Return:
        None
    """
    ## Calcul de statistiques pour les afficher sur l'histogramme en barres verticales
    mean_val = df[var].mean()
    med_val = df[var].median()
    mode_val = df[var].mode()[0]     ## La valeur la plus fréquente
    max_val = df[var].max()

    # Une figure pour 2 subplots : 2 ligne, 1 colonne. Subplot 1 sera un histogramme, subplot 2 un box plot
    fig, ax = plt.subplots(2, 1, figsize = (15,10))

    # Plot histogram   
    sns.histplot(
        data = df,
        x=var, 
        bins=n_bins,
        stat = stat,
        ax = ax[0]
    )

    # Ajouter des lignes verticales pour la moyenne, la mediane, mode, et la valeur max
    ax[0].axvline(x=mean_val, color = 'green', linestyle='dashed', linewidth = 2)
    ax[0].axvline(x=med_val, color = 'red', linestyle='dashed', linewidth = 2)
    ax[0].axvline(x=mode_val, color = 'gray', linestyle='dashed', linewidth = 2)
    ax[0].axvline(x=max_val, color = 'black', linestyle='dashed', linewidth = 2)

    # boxplot   
    sns.boxplot(
        x=df[var], 
        ax= ax[1]
    )
    ax[1].set_xlabel(var + 'Value')

    # Ajouter un titre
    fig.suptitle('Distribution de la variable ' + var)

    # Show the figure
    fig.show()
# Visualisation des distributions des variables numériques 
show_distribution(df, 'Sales')
# Visualisation des distributions des variables numériques 
show_distribution(df, 'Customers')<jupyter_output><empty_output><jupyter_text>1. Suite - transformation du jeu de données  1.1 Transformations des variables<jupyter_code># Pour garder une copie de df si on veut refaire des traitements différents
data = df.copy()<jupyter_output><empty_output><jupyter_text>Création de nouvelles variables continues à partir des différentes dates si cela peut être pertinent<jupyter_code># Nombre de jours/mois depuis la participation à Promo2
# Transformer la colonne Promo2Since en datetime 
# Pour les erreurs (cas Promo2Since = 0), les ignorer => Cela va créer des valeurs NaN, à traiter !
data["Promo2Since"] = pd.to_datetime(data["Promo2Since"], errors='coerce')
# La différence en jours, (peut créer des NaN)
data["DaysSincePromo2"] = (data["Date"] - data["Promo2Since"]).dt.days
# La différence en mois, (peut créer des NaN)
data["MonthsSincePromo2"] = (data["Date"] - data["Promo2Since"])/np.timedelta64(1, 'M')
# Remplacer les NaN dans ces deux nouvelles colonnes
data['DaysSincePromo2'].fillna('0', inplace=True)
data['MonthsSincePromo2'].fillna('0', inplace=True)
# Nombre de jours/mois depuis l'ouverture de la compétition
data["CompetitionOpenSince"] = pd.to_datetime(data["CompetitionOpenSince"], errors='coerce')
# La différence en jours, (peut créer des NaN)
data["DaysSinceCompetition"] = (data["Date"] - data["CompetitionOpenSince"]).dt.days
# La différence en mois, (peut créer des NaN)
data["MonthsSinceCompetition"] = (data["Date"] - data["CompetitionOpenSince"])/np.timedelta64(1, 'M')
## Remplacer les NaN dans ces deux nouvelles colonnes
data['DaysSinceCompetition'].fillna('0', inplace=True)
data['MonthsSinceCompetition'].fillna('0', inplace=True)
data['DaysSincePromo2'] = pd.to_numeric(data['DaysSincePromo2'])
data['MonthsSincePromo2'] = pd.to_numeric(data['MonthsSincePromo2'])
data['DaysSinceCompetition'] = pd.to_numeric(data['DaysSinceCompetition'])
data['MonthsSinceCompetition'] = pd.to_numeric(data['MonthsSinceCompetition'])
data.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
RangeIndex: 1017209 entries, 0 to 1017208
Data columns (total 28 columns):
 #   Column                     Non-Null Count    Dtype         
---  ------                     --------------    -----         
 0   Store                      1017209 non-null  int64         
 1   DayOfWeek                  1017209 non-null  int64         
 2   Date                       1017209 non-null  datetime64[ns]
 3   Sales                      1017209 non-null  int64         
 4   Customers                  1017209 non-null  int64         
 5   Open                       1017209 non-null  int64         
 6   Promo                      1017209 non-null  int64         
 7   StateHoliday               1017209 non-null  object        
 8   SchoolHoliday              1017209 non-null  int64         
 9   StoreType                  1017209 non-null  object        
 10  Assortment                 1017209 non-null  object        
 11  CompetitionDistance        1017209 no[...]<jupyter_text>One hot encoding des variables catégorielles (ou autres encoding)<jupyter_code>data["StateHoliday"] = data["StateHoliday"].replace({0 : '0'})
# Afficher les variables numériques
numerical_features = data.select_dtypes(include=[np.number]).columns 
print(numerical_features)
# Afficher les variables catégorielles
categorical_features = data.select_dtypes(include=[np.object]).columns 
print(categorical_features)
# Une grande partie du prétraitement est le codage 
# On doit représenter chaque élément de données d'une manière compréhensible par l'ordinateur => convertir en langage machine
# Dans de nombreuses branches de l'informatique, en particulier l'apprentissage automatique, 
# One hot encoding est largement utilisé
data = pd.get_dummies(data, columns=['StateHoliday', 'StoreType', 'Assortment', 'PromoInterval'])
data.head()<jupyter_output><empty_output><jupyter_text>D'autres variables à créer ou à transformer (log transform, ...) ? Filtres sur le jeu de données (éliminer les jours de fermeture, les jours avec ventes nulles, ...)<jupyter_code>data = data[(data["Open"]==1) & (data["Sales"]>0)]
## Rajoutez d'autres filtres si cela vous semble pertinent<jupyter_output><empty_output><jupyter_text>Garder que les colonnes utiles<jupyter_code>## Par exemple, les colonnes Open et Customers (leak de la target) ne seront pas gardées
transformed_data = data.drop(['Open', 'Customers', 'CompetitionOpenSince', 'Promo2Since'], axis=1)
transformed_data.head()<jupyter_output><empty_output><jupyter_text>1.2 Stratégie de validation et split du jeu de données Quelle(s) différentes métrique(s) peut-on utiliser ? La documentation sklearn contient la liste des métriques implémentées et leurs explications :  **Lien ICI**  - ... Pour la suite, on vous demandera d'inclure le RMSE dans vos résultats. Comment réaliser la validation des modèles entrainés ? (split temporel, split aléatoire, ...) Proposition : un split temporel - entrainer sur les données du 01/01/2013 au 31/12/2014 et évaluer sur les données à partir du 01/01/2015<jupyter_code>## Séparation train/test (nh'ésitez pas à tester plusieurs splits)
train_date = (transformed_data['Date'] >= '2013-01-01') & (transformed_data['Date'] <= '2017-12-31')
test_date = (transformed_data['Date'] >= '2015-01-01')
data_train = transformed_data[train_date]
data_test = transformed_data[test_date]
X_train = data_train.drop(['Sales','Date'], axis=1).values
y_train = data_train.Sales.values
X_test = data_test.drop(['Sales','Date'], axis=1).values
y_test = data_test.Sales.values
print('X_train shape :', X_train.shape)
print('y_train shape :', y_train.shape)
print('X_test shape :', X_test.shape)
print('y_test shape :', y_test.shape)
print("The training dataset has {} examples and {} features.".format(X_train.shape[0], X_train.shape[1]))
print("The testing dataset has {} examples and {} features.".format(X_test.shape[0], X_test.shape[1]))<jupyter_output>X_train shape : (844338, 33)
y_train shape : (844338,)
X_test shape : (196029, 33)
y_test shape : (196029,)
The training dataset has 844338 examples and 33 features.
The testing dataset has 196029 examples and 33 features.<jupyter_text>Normalisation des données Essayer différentes normalisations (standard scaler, etc.) si ça vous semble pertinents. La documentation sklearn contient la liste des métriques implémentées et leurs explications : **Lien ICI****Remarque 1** : la méthode fit() pour la normalisation doit être appliquée sur le jeu de train uniquement. La même transformation sera appliquée (fit_transform()) ensuite sur les jeux de train et test. **Remarque 2** : vous pouvez évaluer un même modèle avant / après normalisation pour comparer la résultats.<jupyter_code>from sklearn.preprocessing import StandardScaler
feature_scaler = StandardScaler()
feature_scaler = feature_scaler.fit(X_train)
X_training = feature_scaler.fit_transform(X_train)
X_testing = feature_scaler.transform(X_test)<jupyter_output><empty_output><jupyter_text>2. Régression Linéaire Pour les différents modèles, la documentation sklearn détaille les différentes fonctions et méthodes disponibles que vous pourrez utiliser. Vous pourrez également vous baser sur le Notebook du cours 0, partie 3. (Introduction Python). Documentation SKLearn Régression Linéaire : **Lien ICI**  Evaluation du modèle avant normalisation<jupyter_code># Commençons par le modèle de base 
from sklearn.linear_model import LinearRegression 
from sklearn.metrics import mean_squared_error, r2_score  
# Création d'une instance (le modèle lin_reg) par le constructeur LinearRegression()  
lin_reg = LinearRegression() 
# Apprentissage du modèle par la méthode fit() : Il s'agit d'une approche d'apprentissage 
# supervisée puisqu'on utilise lin_reg.fit(X_train, y_train) 
lin_reg.fit(X_train, y_train) 
# Prédiction des les données d'apprentissage X_train par la méthode .predict() 
y_pred = lin_reg.predict(X_train) 
# Evaluation de la prédiction obtenue avec les deux métriques R2 et RMSE 
rmse = np.sqrt(mean_squared_error(y_train, y_pred)) 
r2 = r2_score(y_train, y_pred) 
print("Training: R2=", r2, " et RMSE=", rmse) 
# Prédiction sur les données de test X_test par la méthode .predict() 
y_pred = lin_reg.predict(X_test) 
# Evaluation de la prédiction obtenue avec les deux métriques R2 et RMSE 
rmse = np.sqrt(mean_squared_error(y_test, y_pred)) 
r2 = r2_score(y_test, y_pred) 
print("Testing: R2=", r2, " et RMSE=", rmse)<jupyter_output>Training: R2= 0.2514710548155372  et RMSE= 2685.344087334176
Testing: R2= 0.2625513967847568  et RMSE= 2605.5506203394425<jupyter_text>Evaluation du modèle après normalisation<jupyter_code>from sklearn.linear_model import LinearRegression 
from sklearn.metrics import mean_squared_error, r2_score  
lin_reg = LinearRegression() 
lin_reg.fit(X_training, y_train)  
y_pred = lin_reg.predict(X_training) 
rmse = np.sqrt(mean_squared_error(y_train, y_pred)) 
r2 = r2_score(y_train, y_pred) 
print("Training: R2=", r2, " et RMSE=", rmse) 
y_pred = lin_reg.predict(X_testing) 
rmse = np.sqrt(mean_squared_error(y_test, y_pred)) 
r2 = r2_score(y_test, y_pred) 
print("Testing: R2=", r2, " et RMSE=", rmse)<jupyter_output>Training: R2= 0.2514660453394555  et RMSE= 2685.35307305514
Testing: R2= 0.2626241184660568  et RMSE= 2605.422147187209<jupyter_text>3. Decision Tree *Même démarche pour la régression linéaire...* Documentation Decision Tree : **Lien ICI**<jupyter_code>from sklearn.tree import DecisionTreeRegressor 
# Création d'une instance dt_reg par le constructeur DecisionTreeRegressor() 
dt_reg = DecisionTreeRegressor() 
# Apprentissage du modèle dt_reg par fit 
dt_reg.fit(X_train, y_train) 
# Pédiction sur X_train par la méthode predict() 
y_pred = dt_reg.predict(X_train) 
# Evaluation en calculant les métriques R2 et RMSE 
rmse = np.sqrt(mean_squared_error(y_train, y_pred)) 
r2 = r2_score(y_train, y_pred) 
print("Training: R2=", r2, " et RMSE=", rmse) 
# Prédiction sur X_test par la méthode predict() 
y_pred = dt_reg.predict(X_test) 
# Evaluation en calculant les métriques R2 et RMSE 
rmse = np.sqrt(mean_squared_error(y_test, y_pred)) 
r2 = r2_score(y_test, y_pred) 
print("Testing: R2=", r2, " et RMSE=", rmse)<jupyter_output>Training: R2= 1.0  et RMSE= 0.0
Testing: R2= 1.0  et RMSE= 0.0<jupyter_text>Conclusion : > Pour DecisionTreeRegressor, plus les valeurs de RMSE et de R-carré (R2) sont proches de 0 et 1, respectivement, plus les résultats du modèle seront précis.> Une valeur RMSE >= 0,5 et une valeur R2 =< 0.5 reflète la faible capacité du modèle à prédire avec précision les données.> Dans notre cas notre modèle est bien précis.  4. Random Forest  Documentation Random Forest : **Lien ICI**<jupyter_code>from sklearn.ensemble import RandomForestRegressor 
# Création d'une instance par le constructeur RandomForestRegressor() 
rf_reg = RandomForestRegressor() 
# Apprentissage du modèle rf_reg par la méthode fit() 
rf_reg.fit(X_train, y_train) 
# Prédiction sur X_train par la méthode predict() 
y_pred = rf_reg.predict(X_train) 
# Evaluation e calculant R2 et RMSE 
rmse = np.sqrt(mean_squared_error(y_train, y_pred)) 
r2 = r2_score(y_train, y_pred) 
print("Training: R2=", r2, " et RMSE=", rmse) 
# Prédiction sur X_test par la méthode predict() 
y_pred = rf_reg.predict(X_test) 
#Evaluation e calculant R2 et RMSE 
rmse = np.sqrt(mean_squared_error(y_test, y_pred)) 
r2 = r2_score(y_test, y_pred) 
print("Testing: R2=", r2, " et RMSE=", rmse)<jupyter_output><empty_output><jupyter_text>5. [Optional] Autres modèles à explorer - Si vous le souhaitez, vous pouvez explorer d'autres modèles : XGBoost, SVM, ...- A part les modèles deep learning, vous trouverez des implémentations des modèles ML dans sklearn  Conclusions - Présentez et comparer les différents résultats obtenus avec les différents modèles. Quel modèle vous retenez et pourquoi ?- Interprétabilité métier des résultats<jupyter_code># Les modèles testés from sklearn.pipeline 
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LinearRegression 
from sklearn.tree import DecisionTreeRegressor 
from sklearn.ensemble import RandomForestRegressor 
 
print(LinearRegression().get_params()) 
print(DecisionTreeRegressor().get_params()) 
print(RandomForestRegressor().get_params()
# K-fold cross-validation et GridSearchCV 
pipelines = [] 
params = [] 
names = [] 
 
# Ajouter LinearRegression 
pipelines.append(Pipeline([('clf', LinearRegression())])) 
## LinearRegressionRegressor 
params.append({'clf__normalize':[True]}) 
names.append('LinearRegression') 
# Ajouter DecisionTreeRegressor 
pipelines.append(Pipeline([('clf', DecisionTreeRegressor())])) 
## DecisionTreeRegressor 
params.append({'clf__max_depth':np.linspace(5, 15, 5)}) 
names.append('DecisionTreeRegressor') 
# Ajouter RandomForestRegressor 
pipelines.append(Pipeline([('clf', RandomForestRegressor())])) 
## RandomForestRegressor 
params.append({'clf__n_estimators': [50,100,200]}) 
names.append('RandomForestRegressor')
# L'enraînement avec cross-validation 
# n_jobs = -1 signifie que le calcul sera distribué sur tous les CPU de l'ordinateur 
from sklearn.model_selection import KFold, GridSearchCV, cross_val_score 
def model(pipeline, parameters, name, X, y):        
    cv = KFold(n_splits=3, shuffle=True, random_state=32)    
    grid_obj = GridSearchCV(estimator=pipeline, param_grid=parameters, cv=cv, scoring='r2', n_jobs=-1)    
    grid_obj.fit(X,y)      
    print(name, 'R2:', grid_obj.best_score_)    
    estimator = grid_obj.best_estimator_    
    estimator.fit(X,y) # Training sur tout training data   
    return estimator   
estimators = [] 
for i in range(len(pipelines)):
    estimators.append(model(pipelines[i], params[i], names[i], X_train, y_train))<jupyter_output>LinearRegression R2: 0.25138422362072194
DecisionTreeRegressor R2: 0.7145989671276786
{'fraction_non_alphanumeric': 0.09582397386154788, 'fraction_numerical': 0.02853788033489892, 'mean_word_length': 4.889657245941071, 'pattern_counts': {'":': 0, '<': 58, '<?xml version=': 0, '>': 65, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '196445', 'license': 'MIT', 'n_tokens_mistral': 6966, 'n_tokens_neox': 6526, 'n_words': 1911, 'path': 'Data analysis with supervised learning.ipynb', 'repo_name': 'RaniaAbid-GH/Data_Science_for_Suppy_Chain_and_Operations', 'size': '19588'}