Learning
Troisième jalon : Apprentissage¶
Nous sommes prêts à passer à l’apprentissage à proprement parler. Pour ce faire, nous allons utiliser la bibliothèque scikit-learn.
Préparation de données d’apprentissage et de test¶
Le but est d’estimer le prix d’un produit à partir des attributs. La colonne “Prix” correspond ainsi aux étiquettes ("targets"), tandis que les autres colonnes sont les attributs ("features").
%matplotlib inline
%load_ext autoreload
%autoreload 2
import matplotlib.pyplot as plt
import numpy as np
import os
import sys
sys.path.append(os.path.abspath(".."))
from IPython.core.display_functions import DisplayHandle
from pandas import read_csv
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import MinMaxScaler, StandardScaler
from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LinearRegression
from IPython.display import Markdown, display
from src.cleaning import main as cleaning
from src.scraper import main as scraper
from sklearn.tree import DecisionTreeRegressor
from sklearn.neighbors import KNeighborsRegressor
from sklearn.decomposition import PCA
from seaborn import heatmap
from sklearn.ensemble import RandomForestRegressor
from sklearn.base import clone
class Tableau:
"""
classe pour afficher le tableau des scores automatiquements.
"""
def __init__(self, score_title: str = "R²") -> None:
self._table: dict[str, float] = dict[str, float]()
self._score_title: str = score_title
def ajoutligne(self, methode: str, score: float) -> None:
self._table[methode] = score
def dessiner(self) -> DisplayHandle | None:
table = f"| Méthode | {self._score_title} |\n| :---: | :---: |\n"
for key, value in self._table.items():
table+= f"|{key}|{value:0.6f}|\n"
return display(Markdown(table))
_filename_scraped = "../data.csv"
_filename_cleaned = "../clean.csv"
_url = "bordeaux.html"
try:
if not os.path.exists(_filename_scraped):
print(f"{_filename_scraped} est absent. Lancement du scraper (~10min):")
scraper(_filename_scraped, _url)
cleaning(_filename_scraped, _filename_cleaned)
except Exception as e:
print(f"ERREUR: {e}")
dataset = read_csv(_filename_cleaned)
Question 15¶
Préparez la matrice de données et le vecteur des étiquettes pour votre jeu de données. Séparez les données en un jeu d’entraînement et un jeu de test, sur un base 75%/25%, en utilisant le paramètre random_state = 49.
Nous utiliserons les notations suivantes :
X_train (resp. y_train) fait référence aux données d’apprentissage (resp. à leur étiquette),
X_test (resp. y_test) fait référence aux données de test (resp. à leur étiquette).
X = dataset.drop("Prix", axis=1).values
y = dataset["Prix"].values
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=49
)
Premier modèle : Régression Linéaire¶
Dans un premier temps, nous allons appliquer une régression linéaire LinearRegression sur nos données.
lr_table: Tableau = Tableau()
def get_model_name(model) -> str:
return (
model.steps[-2][1].__class__.__name__
if hasattr(model, "steps")
else model.__class__.__name__
)
def dessiner(lr, X_test, y_test):
x_test_pred = lr.predict(X_test)
plt.figure(figsize=(8, 6))
plt.scatter(x_test_pred, y_test, label="Vins")
mn = min(x_test_pred.min(), y_test.min())
mx = max(x_test_pred.max(), y_test.max())
plt.plot([mn, mx], [mn, mx], color="red", linestyle="--", label="estim_LR=y_test")
plt.xlabel("estim_LR")
plt.ylabel("y_test")
plt.legend()
plt.grid(True, linestyle=":", alpha=0.6)
name = get_model_name(lr)
plt.title(name)
plt.show()
Question 16¶
Évaluez ce modèle sur le jeu de test par r2_score. Rappelons qu’il s’agit de la mesure par défaut pour la fonction score() des méthodes de régression.
lr = LinearRegression()
lr.fit(X_train, y_train)
score: float = lr.score(X_test, y_test)
# lr_table.ajoutligne("LR", score)
print(f"LR score[r^2]: {score:.6f}")
LR score[r^2]: 0.278382
Question 17¶
Nous allons Faire une comparaison entre les estimations (estim_LR) faites par le modèle pour les données de test et les prix y_test. Dessinez une figure où l’axe x représente estim_LR et l’axe y est y_test. Pensez à utiliser la fonction scatter pour représenter les points. Nous appellerons dans la suite figure de visualisation cette représentation. Rappelons que si votre modèle fonctionne bien pour ce jeu de données, vous devriez avoir les points autour de la diagonale où estim_LR est égal à y_test. Que constatez-vous ?
dessiner(lr, X_test, y_test)
On constate des bouteilles hors de prix et donc l'algorithme de regression linéaire ne prend pas suffisamment en compte ces valeurs là.
Question 18¶
Nous allons maintenant évaluer l’impact du pré-traitement sur nos données. En utilisant la commande make_pipeline, normalisez d’abord les données avant de lancer l’apprentissage, et évaluez le score sur le jeu de test. Répétez l’expérience, cette fois-ci en standardisant les données avant l’apprentissage. Dessinez les figures de visualisation pour les deux cas. Observez-vous les mêmes choses qu’à la question précédente ?
scalers = {
"Normalisation + LR": MinMaxScaler,
"Standardisation + LR": StandardScaler,
}
for name, scaler in scalers.items():
pipeline = make_pipeline(scaler(), LinearRegression())
pipeline.fit(X_train, y_train)
x_test_pred = pipeline.predict(X_test)
score: float = pipeline.score(X_test, y_test)
# lr_table.ajoutligne(f"{name} + LR", score)
print(f"{name} + LR score[r^2]: {score:.6f}")
dessiner(pipeline, X_test, y_test)
Normalisation + LR + LR score[r^2]: 0.278382
Standardisation + LR + LR score[r^2]: 0.278382
On constate aucune différence entre la normalisation, la standardisation et sans prétraitement.
Question 19¶
Trouvez les valeurs minimales et maximales pour les prix dans votre dataset. Que peut-on dire de la dispersion des prix dans votre dataset ? Dans le cas où les étiquettes sont trop dispersées, une solution pourrait être de modifier l’échelle des étiquettes. Pour ce faire, on pourrait transformer les prix de sorte que les prix utilisés dans l’apprentissage soient ln(y[i]) à l’aide des fonctions ln et exp de la bibliothèque numpy. Quelles sont les nouvelles valeurs pour le prix minimum et maximum de votre dataset ?
print(f"Prix Min : {y.min():.2f}€")
print(f"Prix Max : {y.max():.2f}€")
y_log = np.log(y)
print(f"Log Min : {y_log.min():.4f}")
print(f"Log Max : {y_log.max():.4f}")
Prix Min : 6.20€ Prix Max : 17280.00€ Log Min : 1.8245 Log Max : 9.7573
Question 20¶
Après avoir éventuellement transformé les prix, remplissez le tableau suivant avec les résultats obtenus grâce à la régression linéaire, avec ou sans pré-traitement. Constatez-vous une amélioration des prédictions due au pré-traitement pour ce jeu de données et cette méthode ? Dessinez la figure de visualisation pour la méthode qui donne le meilleur score. Que constatez-vous ?
X_train, X_test, y_train, y_test = train_test_split(
X, y_log, test_size=0.25, random_state=49
)
scalers = {
"LR": None,
"Normalisation + LR": MinMaxScaler,
"Standardisation + LR": StandardScaler,
}
# liste de tout les scalers
lr_models = []
# apprentissage et récupération des valeurs
for name, scaler in scalers.items():
model = (
LinearRegression()
if scaler is None
else make_pipeline(scaler(), LinearRegression())
)
model.fit(X_train, y_train)
score = model.score(X_test, y_test)
lr_table.ajoutligne(f"{name}", score)
lr_models.append((model, score))
# dessiner la table + le meilleur score
lr_table.dessiner()
# recherche du plus grand
best_model_lr, best_score_lr = max(lr_models, key=lambda x: x[1])
dessiner(best_model_lr, X_test, y_test)
| Méthode | R² |
|---|---|
| LR | 0.493431 |
| Normalisation + LR | 0.493431 |
| Standardisation + LR | 0.493431 |
L'application d'une transformation logarithmique sur les prix ont permis d'obtenir une prédiction bien plus précise.
Deuxième modèle : Arbre de Décision¶
Dans la suite, vous utiliserez les prix transformés ou non en fonction de votre dataset. Le deuxième modèle d’apprentissage que nous allons considérer sont les arbres de décision DecisionTreeRegressor.
Question 21¶
En utilisant la méthode de validation croisée (cv) avec cv = 5, déterminez la valeur de la meilleure profondeur maximale (h) parmi max_depth= {3, 4, 5} sur les données X_train (sans pré-traitement).
Remplissez à nouveau un tableau de trois lignes (une pour la méthode “AD”, une pour “Normalisation + AD” et une pour “Standardisation + AD”) avec les résultats que vous avez obtenus grâce aux arbres de décision avec la profondeur maximale, h, avec ou sans pré-traitement pour les données de test.
Constatez-vous une amélioration significative suite au pré-traitement ? Les scores obtenus sont-ils satisfaisants ?
depths = [3, 4, 5]
scalers = {
"AD": None,
"Normalisation + AD": MinMaxScaler,
"Standardisation + AD": StandardScaler,
}
# trouver la meilleure profondeur par cross-validation
depth_table = Tableau("cross_val_score")
depth_list = []
for depth in depths:
model = DecisionTreeRegressor(max_depth=depth)
# faire la moyenne des valeurs qui ont fait de la cross-validation
score = cross_val_score(model, X, y_log, cv=5).mean()
depth_list.append((depth, score))
depth_table.ajoutligne(f"AD depth={depth}", score)
depth_table.dessiner()
# récupérer la profondeur avec le meilleur score cv
best_depth, _ = max(depth_list, key=lambda x: x[1])
# faire le tableau avec pretraitement
ad_table = Tableau()
ad_models = []
for name, scaler in scalers.items():
# créer le modèle avec ou sans prétraitement
model = (
DecisionTreeRegressor(max_depth=best_depth)
if scaler is None
else make_pipeline(scaler(), DecisionTreeRegressor(max_depth=best_depth))
)
model.fit(X_train, y_train)
# évaluer sur les données de test
score = model.score(X_test, y_test)
ad_table.ajoutligne(f"{name}", score)
ad_models.append((model, score))
ad_table.dessiner()
# recherche du plus grand
best_model_ad, best_score_ad = max(ad_models, key=lambda x: x[1])
| Méthode | cross_val_score |
|---|---|
| AD depth=3 | 0.396810 |
| AD depth=4 | 0.405879 |
| AD depth=5 | 0.411351 |
| Méthode | R² |
|---|---|
| AD | 0.493632 |
| Normalisation + AD | 0.493632 |
| Standardisation + AD | 0.493632 |
Les scores avec KNN sans prétraitement, avec normalisation et standardisation sont satisfaisant, cependant aucune amélioration significative entre les profondeur.
Troisième modèle : Nplus proches voisins¶
En guise de troisième méthode d’apprentissage, nous allons nous pencher sur la méthode des plus proches voisins KNeighborsRegressor
Question 22¶
Évaluez ce modèle sur le jeu de test, en employant dans une premier temps le paramètre n_neighbors = 4. Testez ensuite l’impact du pré-traitement, comme pour les modèles précédents. Constatez-vous une amélioration significative suite au pré-traitement ? Les scores obtenus sont-ils satisfaisants ?
Refaites l’expérience avec un paramètre n_neigbors=5. L’augmentation du nombre de voisins considérés permet-elle de meilleurs résultats ? Si cette amélioration n’est pas significative, nous conserverons n_neighbors = 4 pour la suite, par souci de simplicité.
[Question 23]: Aucune amélioration signification par rapport au pré et post traitement
Question 23¶
Remplissez à nouveau un tableau de trois lignes (une pour la méthode “KNN”, une pour “Normalisation + KNN” et une pour “Standardisation + KNN”) avec les résultats que vous avez obtenus grâce à la méthode des plus proches voisins, avec ou sans pré-traitement.
m_neighbors = [4, 5]
scalers = {
"KNN": None,
"Normalisation + KNN": MinMaxScaler,
"Standardisation + KNN": StandardScaler,
}
knn_models = []
for n in m_neighbors:
knn_table = Tableau()
for name, scaler in scalers.items():
model = (
KNeighborsRegressor(n_neighbors=n)
if scaler is None
else make_pipeline(scaler(), KNeighborsRegressor(n_neighbors=n))
)
model.fit(X_train, y_train)
score = model.score(X_test, y_test)
knn_table.ajoutligne(f"{name}[{n}]", score)
knn_models.append((model, score))
knn_table.dessiner()
# recherche du plus grand
best_model_knn, best_score_knn = max(knn_models, key=lambda x: x[1])
| Méthode | R² |
|---|---|
| KNN[4] | 0.477806 |
| Normalisation + KNN[4] | 0.469166 |
| Standardisation + KNN[4] | 0.473708 |
| Méthode | R² |
|---|---|
| KNN[5] | 0.512624 |
| Normalisation + KNN[5] | 0.497191 |
| Standardisation + KNN[5] | 0.490254 |
Discussions sur le jeu de données¶
Nous avons considéré à ce stade trois méthodes d’apprentissage, à savoir la régression linéaire (LR), les arbres de décision (AD) et la méthode des plus proches voisins (KNN). En gardant uniquement le meilleur score obtenu pour chacune de ces trois méthodes (avec pré-traitement ou non), nous obtenons un tableau de 3 lignes :
| Méthode | R² |
|---|---|
| LR | |
| AD | |
| KNN |
Question 24¶
Replissez le tableau, et comparez les scores obtenus pour ce jeu de données grâce à ces différentes méthodes. Que constatez-vous ? Dans la suite, nous noterons M la méthode qui donne le meilleur score pour les données de test.
comparaison = [
(best_score_lr, best_model_lr, "LR"),
(best_score_ad, best_model_ad, "AD"),
(best_score_knn, best_model_knn, "KNN"),
]
tableau = Tableau()
for score, _, name in comparaison:
tableau.ajoutligne(name, score)
tableau.dessiner()
best_score, M, best_name = max(comparaison, key=lambda x: x[0])
print("best_model=", best_name, "best_scaler=", get_model_name(M))
| Méthode | R² |
|---|---|
| LR | 0.493431 |
| AD | 0.493632 |
| KNN | 0.512624 |
best_model= KNN best_scaler= KNeighborsRegressor
Question 25¶
Réduisez le nombre de composantes à 5. La modélisation avec 5 composantes principales est-elle satisfaisante pour ce jeu de données ? Justifiez.
pca = PCA(n_components=5)
pca.fit(X_train)
X_train_pca = pca.transform(X_train)
X_test_pca = pca.transform(X_test)
print(X_train_pca[0])
print(X_test_pca[0])
print("explained_variance_ratio {} ".format(pca.explained_variance_ratio_))
sum_ratio = pca.explained_variance_ratio_.sum()
print("explained_variance_ratio_sum {} ".format(sum_ratio))
print("SATISFAISANT" if sum_ratio >= 0.80 else "PAS SATISFAISANT")
[ 1.11100337 3.16573198 -0.3719893 -0.25706822 -0.09063923] [ 2.51509226 0.66058572 -0.52987514 -0.15125367 -0.03348169] explained_variance_ratio [0.65405014 0.27609326 0.05190792 0.00374865 0.00226865] explained_variance_ratio_sum 0.9880686227341204 SATISFAISANT
Question 26¶
Lancez la méthode M sur les données obtenues après réduction des composantes, et comparez les nouveaux scores aux scores précédents. Que constatez-vous ?
best_model = clone(M)
best_model.fit(X_train_pca, y_train)
best_model.score(X_test_pca, y_test)
0.47283214331249235
On constate que le score avec PCA avec 5 éléments est inférieur au score obtenu précédemment avec M.
Matrice de corrélation¶
Nous allons tenter de comprendre dans quelle mesure chaque attribut permet de prédire l’étiquette
correlation_matrix= dataset.corr()
# taille du heatmap
n = len(correlation_matrix.columns)
plt.figure(figsize=(n, n))
heatmap(correlation_matrix, annot=True)
<Axes: >
Question 28¶
Quels sont les attributs dont la connaissance nous renseigne le plus sur le prix ? Pour déterminer cela, on regardera la colonne (ou la ligne) “Prix” de la matrice, qui indique la corrélation entre le prix et chacun des autres attributs, considérés individuellement. Plus le résultat est grand, plus la corrélation entre le prix et cet attribut est grande. À l’inverse, plus ce résultat est grand dans les négatifs, plus la corrélation inverse est grande. Enfin, un résultat proche de zéro signifie qu’il y a une faible corrélation entre le prix et cet attribut.
correlation_matrix_price = (
correlation_matrix[["Prix"]].drop("Prix").sort_values(by="Prix", ascending=False)
)
correlation_matrix_price
| Prix | |
|---|---|
| Robert | 0.210094 |
| Robinson | 0.161463 |
| Suckling | 0.105276 |
| App_Pauillac | 0.096240 |
| App_Sauternes | 0.050067 |
| App_Graves | 0.034605 |
| App_Margaux | 0.030996 |
| App_Saint-Emilion | 0.025579 |
| App_Saint-Julien | 0.002595 |
| App_Côtes de Bourg | -0.003555 |
| App_Saint-Estèphe | -0.004447 |
| App_Saint-Georges-Saint-Emilion | -0.007938 |
| App_Bordeaux Supérieur | -0.009086 |
| App_Pessac-Léognan | -0.010756 |
| App_Francs Côtes de Bordeaux | -0.013608 |
| App_Vin de France | -0.015707 |
| App_Canon-Fronsac | -0.016113 |
| App_Puisseguin-Saint-Emilion | -0.016460 |
| App_Bordeaux Blancs Secs | -0.017848 |
| App_Montagne-Saint-Emilion | -0.019995 |
| App_Bordeaux | -0.022425 |
| App_Listrac | -0.023119 |
| App_Lalande de Pomerol | -0.024662 |
| App_Barsac | -0.025593 |
| App_Fronsac | -0.026566 |
| App_Médoc | -0.028850 |
| App_Moulis | -0.029784 |
| App_Castillon Côtes de Bordeaux | -0.034061 |
| App_Pomerol | -0.041008 |
| App_Haut-Médoc | -0.069917 |
Question 29¶
Modifiez le jeu de données en ne conservant que les cinq attributs qui sont le plus corrélés (positivement ou négativement) au prix. Appliquez la méthode M à ce nouveau jeu de données, et comparez le score de prédiction aux résultats obtenus précédemment. Que constatez-vous ?
best_score_correlation_5 = correlation_matrix_price.abs().head(5).index.to_list()
print(best_score_correlation_5)
best_score_X_train, best_score_X_test, best_score_y_train, best_score_y_test = (
train_test_split(
dataset[best_score_correlation_5].values, y_log, test_size=0.25, random_state=49
)
)
best_model = clone(M)
best_model.fit(best_score_X_train, best_score_y_train)
best_model.score(best_score_X_test, best_score_y_test)
['Robert', 'Robinson', 'Suckling', 'App_Pauillac', 'App_Sauternes']
0.47041917132486233
On constate que le score avec seulement 5 attributs est très proche au score obtenu précédemment avec l'ensemble des données.
Quatrième modèle : une méthode ensembliste¶
Pour terminer, vous allez explorer une méthode qui n’a pas été étudiée dans le cours.
Question 30¶
On se penche sur l’apprentissage via une méthode ensembliste, comme le Gradient Boosting ou les Random Forests. Choisissez une de ces méthodes, et refaites l’apprentissage en l’utilisant. Remplissez à nouveau un tableau de trois lignes (une pour la méthode “ens”, une pour “Normalisation + ens” et une pour “Standardisation + ens”) avec les résultats que vous avez obtenus grâce à la méthode ensembliste que vous avez choisie, avec ou sans pré-traitement. Que constatez-vous ?
# Random Forests
scalers = {
"Random Forest": None,
"Normalisation + RF": MinMaxScaler,
"Standardisation + RF": StandardScaler,
}
rf_table = Tableau()
for name, scaler in scalers.items():
rf_model = RandomForestRegressor()
model = (
rf_model if scaler is None
else make_pipeline(scaler(), rf_model)
)
model.fit(X_train, y_train)
score = model.score(X_test, y_test)
rf_table.ajoutligne(name, score)
rf_table.dessiner()
| Méthode | R² |
|---|---|
| Random Forest | 0.519810 |
| Normalisation + RF | 0.522608 |
| Standardisation + RF | 0.516279 |
On constate qu'entre le modèle M et le random forest, le modèle random forest surpasse le modèle M dans cette ensemble de donnée.