diff --git a/_quarto.yml b/_quarto.yml
index 0543d4bed..b0b970e27 100644
--- a/_quarto.yml
+++ b/_quarto.yml
@@ -4,53 +4,8 @@ project:
render:
- index.qmd
- content/getting-started/index.qmd
- - content/getting-started/01_installation.qmd
- - content/getting-started/02_DS_environment.qmd
- - content/getting-started/03_data_analysis.qmd
- - content/getting-started/04_python_practice.qmd
- - content/getting-started/05_rappels_types.qmd
- - content/getting-started/06_rappels_fonctions.qmd
- - content/getting-started/07_rappels_classes.qmd
- - content/manipulation/index.qmd
- - content/manipulation/01_numpy.qmd
- - content/manipulation/02a_pandas_tutorial.qmd
- - content/manipulation/02b_pandas_TP.qmd
- - content/manipulation/03_geopandas_tutorial.qmd
- - content/manipulation/03_geopandas_TP.qmd
- - content/manipulation/04a_webscraping_TP.qmd
- - content/manipulation/04c_API_TP.qmd
- - content/manipulation/04b_regex_TP.qmd
- - content/manipulation/07_dask.qmd
- - content/visualisation/index.qmd
- - content/visualisation/matplotlib.qmd
- - content/visualisation/maps.qmd
- - content/modelisation/index.qmd
- - content/modelisation/0_preprocessing.qmd
- - content/modelisation/1_modelevaluation.qmd
- - content/modelisation/2_SVM.qmd
- - content/modelisation/3_regression.qmd
- - content/modelisation/4_featureselection.qmd
- - content/modelisation/5_clustering.qmd
- content/modelisation/6_pipeline.qmd
- - content/NLP/index.qmd
- - content/NLP/01_intro.qmd
- - content/NLP/02_exoclean.qmd
- - content/NLP/03_lda.qmd
- - content/NLP/04_word2vec.qmd
- - content/NLP/05_exo_supp.qmd
- - content/modern-ds/index.qmd
- - content/modern-ds/continuous_integration.qmd
- - content/modern-ds/dallE.qmd
- - content/modern-ds/s3.qmd
- - content/modern-ds/elastic_approfondissement.qmd
- - content/modern-ds/elastic_intro.qmd
- - content/git/index.qmd
- - content/git/introgit.qmd
- - content/git/exogit.qmd
- - content/annexes/evaluation.qmd
- - content/annexes/corrections.qmd
- - content/annexes/evaluation.qmd
- - content/annexes/corrections.qmd
+ - content/modelisation/7_mlapi.qmd
website:
title: "Python pour la data science"
@@ -148,6 +103,7 @@ website:
- content/modelisation/4_featureselection.qmd
- content/modelisation/5_clustering.qmd
- content/modelisation/6_pipeline.qmd
+ - content/modelisation/7_mlapi.qmd
- id: NLP
title: "NLP"
#collapse-level: 2
diff --git a/content/modelisation/6_pipeline.qmd b/content/modelisation/6_pipeline.qmd
index 0363f90a8..60c1f3602 100644
--- a/content/modelisation/6_pipeline.qmd
+++ b/content/modelisation/6_pipeline.qmd
@@ -18,7 +18,6 @@ description: |
dans une chaîne d'opérations. Il s'agit d'une approche particulièrement
appropriée pour réduire la difficulté à changer d'algorithme ou pour
faciliter la ré-application d'un code à de nouvelles données.
-eval: false
echo: false
image: featured.png
bibliography: ../../reference.bib
@@ -30,7 +29,6 @@ bibliography: ../../reference.bib
#| echo: false
#| output: 'asis'
#| include: true
-#| eval: true
import sys
sys.path.insert(1, '../../') #insert the utils module
@@ -41,12 +39,31 @@ print_badges("content/modelisation/6_pipeline.qmd")
```
:::
+
+Ce chapitre présente la première application
+d'une journée de cours que j'ai
+donné à l'Université Dauphine dans le cadre
+des _PSL Data Week_.
+
+
+
+
+Dérouler les _slides_ associées ci-dessous ou [cliquer ici](https://linogaliana.github.io/dauphine-week-data/#/title-slide)
+pour les afficher en plein écran.
+
+
+
+
+
+
+
+
+
Pour lire les données de manière efficace, nous
proposons d'utiliser le _package_ `duckdb`.
Pour l'installer, voici la commande:
```{python}
-#| eval: true
#| output: false
#| echo: true
!pip install duckdb
@@ -87,7 +104,7 @@ projets utilisant des techniques de _machine learning_.
```{python}
#| echo: true
-#| eval: true
+
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
@@ -119,7 +136,7 @@ Ils présentent de nombreux intérêts, parmi lesquels:
Un des intérêts des *pipelines* scikit est qu'ils fonctionnent aussi avec
des méthodes qui ne sont pas issues de `scikit`.
-Il est possible d'introduire un modèle de réseau de neurones `Keras` dans
+Il est possible d'introduire un modèle de réseau de neurone `Keras` dans
un pipeline `scikit`.
Pour introduire un modèle économétrique `statsmodels`
c'est un peu plus coûteux mais nous allons proposer des exemples
@@ -148,7 +165,7 @@ transformation inverse).
```{python}
#| echo: true
-#| eval: true
+
from sklearn.pipeline import Pipeline
from sklearn.svm import SVC
from sklearn.decomposition import PCA
@@ -169,7 +186,7 @@ pour la *grid search*:
```{python}
#| echo: true
-#| eval: true
+
from sklearn.model_selection import GridSearchCV
param_grid = {"reduce_dim__n_components":[2, 5, 10], "clf__C":[0.1, 10, 100]}
grid_search = GridSearchCV(pipe, param_grid=param_grid)
@@ -231,7 +248,7 @@ une vue:
```{python}
#| echo: false
-#| eval: true
+
import duckdb
# version remote
url = "https://www.data.gouv.fr/fr/datasets/r/56bde1e9-e214-408b-888d-34c57ff005c4"
@@ -246,11 +263,11 @@ import duckdb
duckdb.sql(f'CREATE OR REPLACE VIEW dvf AS SELECT * FROM read_parquet("dvf.parquet")')
```
-Les données prennent la forme suivante :
+Les données prennent la forme suivante:
```{python}
#| echo: true
-#| eval: true
+
duckdb.sql(f"SELECT * FROM dvf LIMIT 5")
```
@@ -260,7 +277,7 @@ pour la suite de l'exercice
```{python}
#| echo: true
-#| eval: true
+
xvars = [
"Date mutation", "Valeur fonciere",
'Nombre de lots', 'Code type local',
@@ -271,7 +288,7 @@ xvars = ", ".join([f'"{s}"' for s in xvars])
```{python}
#| echo: true
-#| eval: true
+
mutations = duckdb.sql(
f'''
SELECT
@@ -307,7 +324,7 @@ Le code ci-dessus effectue la conversion adéquate au niveau de `Pandas`.
```{python}
#| echo: true
-#| eval: true
+
mutations.head(2)
```
@@ -359,7 +376,6 @@ exceptions près (@fig-corr-surface):
```{python}
#| echo: true
-#| eval: true
#| output: false
corr = mutations.loc[
@@ -376,7 +392,6 @@ cmap = sns.diverging_palette(230, 20, as_cmap=True)
```{python}
#| echo: true
-#| eval: true
#| fig-cap: Matrice de corrélation des variables de surface
#| label: fig-corr-surface
@@ -394,7 +409,7 @@ g
```{python}
#| echo: true
-#| eval: true
+
mutations['lprix'] = np.log(mutations["Valeur fonciere"])
mutations['surface'] = mutations.loc[:, colonnes_surface].sum(axis = 1).astype(int)
```
@@ -403,7 +418,7 @@ mutations['surface'] = mutations.loc[:, colonnes_surface].sum(axis = 1).astype(i
```{python}
#| echo: true
-#| eval: true
+
mutations['surface'] = mutations.loc[:, mutations.columns[mutations.columns.str.startswith('Surface Carrez')].tolist()].sum(axis = 1)
```
@@ -469,7 +484,7 @@ exceptionnelles dont le mécanisme de fixation du prix diffère)
```{python}
#| echo: true
-#| eval: true
+
mutations2 = mutations.drop(
colonnes_surface.tolist() + ["Date mutation", "lprix"], # ajouter "confinement" si données 2020
axis = "columns"
@@ -487,7 +502,7 @@ Ces différents types vont bénéficier d'étapes de _preprocessing_
différentes.
```{python}
-#| eval: true
+#| echo: true
numeric_features = mutations2.columns[~mutations2.columns.isin(['dep','Code_type_local', 'month', 'Valeur_fonciere'])].tolist()
categorical_features = ['dep','Code_type_local','month']
```
@@ -519,7 +534,7 @@ mutations2 = mutations2.groupby('dep').sample(frac = 0.1, random_state = 123)
Avec la fonction adéquate de `Scikit`, faire un découpage de `mutations2`
en _train_ et _test sets_
-en suivant les consignes suivantes :
+en suivant les consignes suivantes:
- 20% des données dans l'échantillon de _test_ ;
- L'échantillonnage est stratifié par départements ;
@@ -532,7 +547,6 @@ en suivant les consignes suivantes :
```{python}
-#| eval: true
from sklearn.model_selection import train_test_split
mutations2 = mutations2.groupby('dep').sample(frac = 0.1, random_state = 123)
@@ -582,7 +596,6 @@ _💡 Il est recommandé de s'aider de la documentation de `Scikit`. Si vous ave
```{python}
-#| eval: true
#| label: exo2-q1
# Question 1
from sklearn.ensemble import RandomForestRegressor
@@ -592,7 +605,6 @@ regr = RandomForestRegressor(max_depth=2, random_state=123)
```{python}
-#| eval: true
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.pipeline import make_pipeline, Pipeline
@@ -612,7 +624,6 @@ pipe = Pipeline(steps=[('preprocessor', transformer),
A l'issue de cet exercice, nous devrions obtenir le _pipeline_ suivant.
```{python}
-#| eval: true
pipe
```
@@ -651,26 +662,22 @@ quel semble être le problème ?
```{python}
-#| eval: true
pipe.fit(X_train, y_train)
```
```{python}
-#| eval: true
#| output: false
# Question 2
pipe[:-1].transform(X_train.sample(4))
```
```{python}
-#| eval: true
# Question 4
pipe.predict(X_test)
```
```{python}
-#| eval: true
# Question 5
X_fictif = pd.DataFrame(
{
@@ -688,7 +695,6 @@ pipe.predict(X_fictif)
```{python}
-#| eval: true
from sklearn.metrics import mean_squared_error
np.sqrt(
@@ -742,7 +748,6 @@ de cet exercice est le suivant.
```{python}
-#| eval: true
features_names = pipe[:-1].get_feature_names_out()
importances = pipe['randomforest'].feature_importances_
std = np.std([tree.feature_importances_ for tree in pipe['randomforest'].estimators_], axis=0)
@@ -796,7 +801,6 @@ Les prédictions peuvent nous suggérer également
qu'il y a un problème:
```{python}
-#| eval: true
#| echo: true
compar = pd.DataFrame([y_test, pipe.predict(X_test)]).T
compar.columns = ['obs','pred']
@@ -813,12 +817,11 @@ g.ax.axline(xy1=(0, 0), slope=1, color="red", dashes=(5, 2))
## Restriction du champ du modèle
Mettre en oeuvre un bon modèle de prix au niveau France entière
-est complexe. Nous allons donc nous restreindre au champ suivant :
+est complexe. Nous allons donc nous restreindre au champ suivant:
les appartements dans Paris.
```{python}
#| echo: true
-#| eval: true
mutations_paris = mutations.drop(
colonnes_surface.tolist() + ["Date mutation", "lprix"], # ajouter "confinement" si données 2020
axis = "columns"
@@ -853,7 +856,6 @@ de celles-ci:
```{python}
-#| eval: true
#| label: estim-model-paris
from sklearn.ensemble import GradientBoostingRegressor
@@ -914,7 +916,6 @@ la profondeur de l'arbre mais c'était un choix au doigt mouillé.
❓️ Quels sont les hyperparamètres qu'on peut essayer d'optimiser ?
```{python}
-#| eval: true
pipe['boosting'].get_params()
```
@@ -924,7 +925,6 @@ absurde de jouer sur le paramètre `random_state` qui est la racine du générat
pseudo-aléatoire.
```{python}
-#| eval: true
X = pd.concat((X_train, X_test), axis=0)
Y = np.concatenate([y_train,y_test])
```
@@ -971,7 +971,6 @@ Pour gagner en performance, il est recommandé d'utiliser l'argument
```{python}
#| output: false
-#| eval: true
#| label: grid-search
import numpy as np
@@ -994,7 +993,6 @@ print(f"Elapsed time : {int(end_time - start_time)} seconds")
```
```{python}
-#| eval: true
grid_search
```
@@ -1002,7 +1000,6 @@ grid_search
```{python}
-#| eval: true
grid_search.best_params_
grid_search.best_estimator_
```
@@ -1012,14 +1009,12 @@ Toutes les performances sur les ensembles d'échantillons et de test sur la gril
d'hyperparamètres sont disponibles dans l'attribut:
```{python}
-#| eval: true
perf_random_forest = pd.DataFrame(grid_search.cv_results_)
```
Regardons les résultats moyens pour chaque valeur des hyperparamètres:
```{python}
-#| eval: true
fig, ax = plt.subplots(1)
g = sns.lineplot(data = perf_random_forest, ax = ax,
x = "param_boosting__n_estimators",
@@ -1043,7 +1038,6 @@ du jeu de *test*:
```{python}
#| echo: true
#| output: false
-#| eval: true
pipe_optimal = grid_search.best_estimator_
pipe_optimal.fit(X_train, y_train)
@@ -1055,18 +1049,18 @@ compar['diff'] = compar.obs - compar.pred
On obtient le RMSE suivant :
```{python}
-#| eval: true
+
print("Le RMSE sur le jeu de test est {:,}".format(
int(np.sqrt(mean_squared_error(y_test, pipe_optimal.predict(X_test))))
))
```
-Et si on regarde la qualité en prédiction :
+Et si on regarde la qualité en prédiction:
```{python}
#| include: false
#| echo: true
-#| eval: true
+
g = sns.relplot(data = compar, x = 'obs', y = 'pred', color = "royalblue", alpha = 0.8)
g.set(ylim=(0, 2e6), xlim=(0, 2e6),
title='Evaluating estimation error on test sample',
@@ -1085,6 +1079,17 @@ pertinente était le prix ou une transformation de celle-ci
(par exemple le prix au $m^2$)
+## Prochaine étape
+
+Nous avons un modèle certes perfectible mais fonctionnel.
+La question qui se pose maintenant c'est d'essayer d'en faire
+quelque chose au service des utilisateurs. Cela nous amène vers
+la question de la __mise en production__.
+
+Ceci est l'objet du prochain chapitre. Il s'agira d'une version introductive
+des enjeux évoqués dans le cadre du cours de
+3e année de [mise en production de projets de _data science_](https://ensae-reproductibilite.github.io/website/).
+
## Références
diff --git a/content/modelisation/7_mlapi.qmd b/content/modelisation/7_mlapi.qmd
new file mode 100644
index 000000000..2d988ed2e
--- /dev/null
+++ b/content/modelisation/7_mlapi.qmd
@@ -0,0 +1,335 @@
+---
+title: "Mettre à disposition un modèle par le biais d'une API"
+date: 2023-10-20T13:00:00Z
+weight: 70
+slug: ml-api
+tags:
+ - scikit
+ - Machine Learning
+ - Pipeline
+ - Modelisation
+ - Tutorial
+categories:
+ - Modélisation
+ - Tutoriel
+description: |
+ TO BE COMPLETED
+image: featured.png
+bibliography: ../../reference.bib
+---
+
+:::{.cell .markdown}
+```{python}
+#| echo: false
+#| output: 'asis'
+#| include: true
+import sys
+sys.path.insert(1, '../../') #insert the utils module
+from utils import print_badges
+#print_badges(__file__)
+print_badges("content/modelisation/7_mlapi.qmd")
+```
+:::
+
+
+Ce chapitre présente la deuxième application
+d'une journée de cours que j'ai
+donné à l'Université Dauphine dans le cadre
+des _PSL Data Week_.
+
+L'objectif de ce chapitre est d'amener à développer
+une API du type de [celle-ci](https://dvf-simple-api.lab.sspcloud.fr).
+
+
+
+
+Dérouler les _slides_ associées ci-dessous ou [cliquer ici](https://linogaliana.github.io/dauphine-week-data/#/title-slide)
+pour les afficher en plein écran.
+
+
+
+
+
+
+
+Le chapitre précédent constituait une introduction à la création
+de _pipelines_ de _machine learning_.
+Ce chapitre va aller plus loin en montrant la démarche pour le rendre
+disponible à plus grande échelle par le biais d'une API pouvant
+être consommée avec de nouvelles données. L'objectif de celle-ci est
+de ne pas contraindre les réutilisateurs d'un modèle
+à disposer d'un environnement technique complexe
+pour pouvoir utiliser le même modèle que celui entraîné précédemment.
+
+
+
+## Exemple de réutilisation d'un modèle sous forme d'API
+
+Un exemple d'API obtenue à l'issue de ce chapitre est
+mis à disposition sur [https://dvf-simple-api.lab.sspcloud.fr/](https://dvf-simple-api.lab.sspcloud.fr/).
+La documentation de l'API est disponible [ici](https://dvf-simple-api.lab.sspcloud.fr/docs).
+
+Cette API est utilisable dans plusieurs langages.
+
+En `Python`, par exemple, cela donnera:
+
+```{python}
+import requests
+
+pieces_principales = 6
+surface = 50
+url = f"https://dvf-simple-api.lab.sspcloud.fr/predict?month=4&nombre_lots=1&code_type_local=2&nombre_pieces_principales={pieces_principales}&surface={surface}"
+requests.get(url).json()
+```
+
+Néanmoins, l'un des intérêts de proposer
+une API est que les utilisateurs du modèle
+ne sont pas obligés d'être des pythonistes.
+Cela accroît grandement la cible des ré-utilisateurs
+potentiels.
+
+Cette approche ouvre notamment la possibilité de
+faire des applications interactives qui utilisent,
+en arrière plan, notre modèle entraîné avec `Python`.
+
+::::: {.content-visible when-format="html"}
+
+Voici un exemple, minimaliste, d'une réutilisation
+de notre modèle avec deux sélecteurs Javascript
+qui mettent à jour le prix estimé du bien.
+
+:::: {.columns}
+
+::: {.column width="50%"}
+```{ojs}
+//| echo: false
+html`
Nombre de pièces
${viewof pieces_principales}
`
+```
+:::
+
+::: {.column width="50%"}
+```{ojs}
+//| echo: false
+html`Surface de l'appartement
${surface}
`
+```
+:::
+
+::::
+
+```{ojs}
+//| echo: false
+viewof pieces_principales = Inputs.range([1, 12], {step: 1, value: 6})
+```
+
+```{ojs}
+//| echo: false
+viewof surface = Inputs.range([1, 300], {step: 1, value: 50})
+```
+
+
+```{ojs}
+//| echo: false
+md`${return_message}`
+```
+
+```{ojs}
+//| echo: false
+html`${url_api_print}`
+```
+
+
+
+```{ojs}
+//| echo: false
+url_api_dvf = `https://corsproxy.io/?https://dvf-simple-api.lab.sspcloud.fr/predict?month=4&nombre_lots=1&code_type_local=2&nombre_pieces_principales=${pieces_principales}&surface=${surface}`
+```
+
+```{ojs}
+//| echo: false
+url_api_print = md`[https://dvf-simple-api.lab.sspcloud.fr/predict?month=4&nombre_lots=1&code_type_local=2&nombre_pieces_principales=${pieces_principales}&surface=${surface}](${url_api_dvf})`
+```
+
+
+```{ojs}
+//| echo: false
+value = d3.json(url_api_dvf).then(data => {
+ // Access the 'value' property from the object
+ let originalNumber = data;
+
+ // Convert it to a floating-point number
+ let numericValue = parseFloat(originalNumber);
+
+ // Round the number
+ let roundedNumber = Math.round(numericValue).toLocaleString();
+
+ return roundedNumber;
+}).catch(error => console.error('Error:', error));
+```
+
+```{ojs}
+//| echo: false
+return_message = `Valeur estimée de l'appartement: __${value} €__`
+```
+
+
+:::::
+
+
+## Etape 1: créer une application en local
+
+Mettre en place une API consiste à gravir une marche
+dans l'échelle de la reproductibilité par rapport
+à fournir un _notebook_. Ces derniers
+ne sont pas les outils les plus adaptés
+pour partager autre chose que du code, à faire tourner
+de son côté.
+
+Il est donc naturel de sortir des _notebooks_
+lorsqu'on commence à aller vers ce niveau de mise à
+disposition.
+Par le biais de
+scripts `Python` lancés en ligne de commande,
+construits en exportant le code du chapitre précédent
+de nos notebooks, on pourra
+créer une base de départ propre.
+
+Il est plus naturel de privilégier une interface de développement
+généraliste comme VSCode à Jupyter lorsqu'on franchit
+ce rubicon. L'exercice suivant permettra donc
+de créer cette première application minimale, à
+exécuter en ligne de commande.
+
+
+
+::: {.cell .markdown}
+```{=html}
+
+
Exercice 1: créer des scripts pour entraîner le modèle
+```
+
+Le dépôt `Github` qui permet de construire l'API _from scratch_
+est [disponible ici](https://github.com/linogaliana/api-dvf).
+Nous allons emprunter quelques éléments, par-ci par-là,
+pour faire notre application en local.
+
+- Créer un nouveau service `VSCode` sur le `SSPCloud` en paramétrant dans l'onglet
+`Networking` le port 5000 ;
+- Utiliser la commande suivante depuis le terminal:
+
+```shell
+mkdir app
+cd app
+```
+
+Depuis le menu des fichiers, créer quatre fichiers dont le contenu
+suit:
+
+- `requirements.txt`: récupérer le contenu sur [cette page](https://raw.githubusercontent.com/linogaliana/api-dvf/main/requirements.txt) ;
+- `getdvf.py`: récupérer le contenu sur [cette page](https://raw.githubusercontent.com/linogaliana/api-dvf/main/getdvf.py) ;
+- `train.py`: récupérer le contenu sur [cette page](https://raw.githubusercontent.com/linogaliana/api-dvf/main/train.py) ;
+- `api.py`: récupérer le contenu sur [cette page](https://raw.githubusercontent.com/linogaliana/api-dvf/main/main.py).
+
+
+- Exécuter `getdvf.py` puis `train.py` pour stocker en local le modèle entraîné
+- Ajouter `model.joblib` au `.gitignore` _(si vous utilisez `Git`)_
+- Créer un script `test.py` qui contient la fonction suivante et la teste après avoir importé votre modèle (`load('pipe.joblib')` en n'oubliant pas `from joblib import load`):
+
+```{python}
+import pandas as pd
+
+def predict(
+ month: int = 3,
+ nombre_lots: int = 1,
+ code_type_local: int = 2,
+ nombre_pieces_principales: int = 3,
+ surface: float = 75
+) -> float:
+ """
+ """
+
+ df = pd.DataFrame(
+ {
+ "month": [month],
+ "Nombre_de_lots": [nombre_lots],
+ "Code_type_local": [code_type_local],
+ "Nombre_pieces_principales": [nombre_pieces_principales],
+ "surface": [surface]
+ }
+ )
+
+ prediction = model.predict(df)
+
+ return prediction
+```
+
+```{=html}
+
+```
+:::
+
+## Etape 2: créer une API en local
+
+Le script précédent constitue déjà un progrès dans
+la reproductibilité. Il rend plus facile le réentraînement
+d'un modèle sur le même jeu de données. Néanmoins,
+il reste tributaire du fait que la personne désirant
+utiliser du modèle utilise `Python` et sache réentrainer
+le modèle dans les mêmes conditions que vous.
+
+Avec `FastAPI`, nous allons très facilement pouvoir
+transformer cette application `Python` en une API.
+
+::: {.cell .markdown}
+```{=html}
+
+
Exercice 2: créer des scripts pour entraîner le modèle
+```
+
+- La ligne ci-dessous du script `api.py` récupère un modèle pré-entraîné enregistré sur un espace de stockage
+
+```python
+download_file("https://minio.lab.sspcloud.fr/projet-formation/diffusion/python-datascientist/pipe.joblib", 'pipe.joblib')
+```
+
+Retirer cette ligne de votre script, pour utiliser
+le modèle que vous venez d'entraîner.
+
+- Déployer en local l'API avec la commande
+
+```shell
+uvicorn api:app --reload --host "0.0.0.0" --port 5000
+```
+
+- A partir du `README` du [service VSCode](https://datalab.sspcloud.fr/my-services),
+se rendre sur l'URL de déploiement,
+ajouter `/docs/` à celui-ci et observer la documentation de l'API
+- Se servir de la documentation pour tester les requêtes `/predict`
+- Récupérer l'URL d'une des requêtes proposées. La tester dans le navigateur
+et depuis `Python` avec `Requests` (`requests.get(url).json()`)
+- Optionnel: faire tourner le même code dans un autre environnement que le SSPCloud (par exemple une installation de `Python` en local) pour voir que ça fonctionne de manière identique.
+```{=html}
+
+```
+:::
+
+
+## Aller plus loin: mettre à disposition cette API de manière pérenne
+
+L'étape précédente permettait de créer un point d'accès
+à votre modèle depuis n'importe quel type de client. A chaque
+requête de l'API, le script `api.py` était exécuté et
+renvoyait son _output_.
+
+Ceci est déjà un saut de géant dans l'échelle de la
+reproductibilité. Néanmoins, cela reste artisanal: si votre
+serveur local connait un problème (par exemple, vous _killez_ l'application), les clients ne recevront plus de réponse,
+sans comprendre pourquoi.
+
+Il est donc plus fiable de mettre en production sur des
+serveurs dédiés, qui tournent 24h/24 et qui peuvent
+également se répartir la charge de travail s'il y a
+beaucoup de demandes instantanées.
+
+Ceci dépasse néanmoins
+le cadre de ce cours et sera l'objet
+d'un cours dédié en 3e année de l'ENSAE: ["Mise en production de projets _data science_"](https://ensae-reproductibilite.github.io/website/) donné par Romain Avouac et moi.
\ No newline at end of file
diff --git a/content/modelisation/app/api.py b/content/modelisation/app/api.py
new file mode 100644
index 000000000..5fd4fb15b
--- /dev/null
+++ b/content/modelisation/app/api.py
@@ -0,0 +1,55 @@
+"""A simple API to expose our trained RandomForest model for Tutanic survival."""
+from fastapi import FastAPI
+from joblib import load
+
+import pandas as pd
+
+model = load('pipe.joblib')
+
+app = FastAPI(
+ title="Quel est le prix de ce logement ?",
+ description=
+ "Application du boosting sur les données DVF 🏡
Une version par API pour faciliter la réutilisation du modèle 🚀" +\
+ "
"
+ )
+
+
+@app.get("/", tags=["Welcome"])
+def show_welcome_page():
+ """
+ Show welcome page with model name and version.
+ """
+
+ return {
+ "Message": "API de prédiction des prix de l'immobilier",
+ "Model_name": 'DVF ML',
+ "Model_version": "0.1",
+ }
+
+
+@app.get("/predict", tags=["Predict"])
+async def predict(
+ month: int = 3,
+ nombre_lots: int = 1,
+ code_type_local: int = 2,
+ nombre_pieces_principales: int = 3,
+ surface: float = 75
+) -> float:
+ """
+ """
+
+ df = pd.DataFrame(
+ {
+ "month": [month],
+ "Nombre_de_lots": [nombre_lots],
+ "Code_type_local": [code_type_local],
+ "Nombre_pieces_principales": [nombre_pieces_principales],
+ "surface": [surface]
+ }
+ )
+
+ prediction = model.predict(df)
+
+ return prediction
+
+
diff --git a/content/modelisation/app/getdvf.py b/content/modelisation/app/getdvf.py
new file mode 100644
index 000000000..868b20563
--- /dev/null
+++ b/content/modelisation/app/getdvf.py
@@ -0,0 +1,124 @@
+import os
+import requests
+import duckdb
+
+URL = "https://www.data.gouv.fr/fr/datasets/r/56bde1e9-e214-408b-888d-34c57ff005c4"
+FILENAME_DVF_LOCAL = "dvf.parquet"
+
+
+def download_file(url, file_name):
+ """
+ Download a file from a given URL and save it locally.
+
+ Args:
+ url (str): The URL of the file to be downloaded.
+ file_name (str): The local file name to save the downloaded content.
+
+ Returns:
+ None
+ """
+ # Check if the file already exists
+ if not os.path.exists(file_name):
+ response = requests.get(url)
+
+ if response.status_code == 200:
+ with open(file_name, "wb") as f:
+ f.write(response.content)
+ print("Téléchargement réussi.")
+ else:
+ print(f"Échec du téléchargement. Code d'état : {response.status_code}")
+ else:
+ print(f"Le fichier '{file_name}' existe déjà. Aucun téléchargement nécessaire.")
+
+
+def create_dataset_paris(duckdb_session, xvars):
+ """
+ Create a Paris dataset based on specified variables.
+
+ Args:
+ duckdb_session: The DuckDB session object.
+ xvars (list): List of variable names to include in the dataset.
+
+ Returns:
+ pandas.DataFrame: The created Paris dataset.
+ """
+ xvars = ", ".join([f'"{s}"' for s in xvars])
+
+ mutations = duckdb.sql(
+ f"""
+ SELECT
+ date_part('month', "Date mutation") AS month,
+ substring("Code postal", 1, 2) AS dep,
+ {xvars},
+ COLUMNS('Surface Carrez.*')
+ FROM dvf
+ """
+ ).to_df()
+
+ colonnes_surface = mutations.columns[
+ mutations.columns.str.startswith("Surface Carrez")
+ ]
+ mutations.loc[:, colonnes_surface] = (
+ mutations.loc[:, colonnes_surface]
+ .replace({",": "."}, regex=True)
+ .astype(float)
+ .fillna(0)
+ )
+ mutations["surface"] = mutations.loc[:, colonnes_surface].sum(axis=1).astype(int)
+
+ mutations_paris = mutations.drop(
+ colonnes_surface.tolist()
+ + ["Date mutation"], # ajouter "confinement" si données 2020
+ axis="columns",
+ ).copy()
+
+ mutations_paris = mutations_paris.loc[
+ mutations_paris["Valeur fonciere"] < 5e6
+ ] # keep only values below 5 millions
+
+ mutations_paris.columns = mutations_paris.columns.str.replace(" ", "_")
+ mutations_paris = mutations_paris.dropna(subset=["dep", "Code_type_local", "month"])
+ mutations_paris = mutations_paris.loc[mutations_paris["dep"] == "75"]
+ mutations_paris = mutations_paris.loc[mutations_paris["Code_type_local"] == 2].drop(
+ ["dep", "Code_type_local", "Nombre_de_lots"], axis="columns"
+ )
+ mutations_paris.loc[mutations_paris["surface"] > 0]
+
+ return mutations_paris
+
+
+def pipeline_fetch_data(url=URL, file_name=FILENAME_DVF_LOCAL):
+ """
+ Fetch data, create a DuckDB view, and save the processed data as a Parquet file.
+
+ Args:
+ url (str): The URL to download the initial data.
+ file_name (str): The local file name to save the downloaded content.
+
+ Returns:
+ None
+ """
+ download_file(url, file_name)
+ duckdb.sql(
+ f'CREATE OR REPLACE VIEW dvf AS SELECT * FROM read_parquet("dvf.parquet")'
+ )
+
+ xvars = [
+ "Date mutation",
+ "Valeur fonciere",
+ "Nombre de lots",
+ "Code type local",
+ "Nombre pieces principales",
+ ]
+
+ data = create_dataset_paris(duckdb, xvars)
+
+ data.to_parquet("input.parquet")
+
+ print(f"Data fetching done, {data.shape[0]} rows available")
+
+ return data
+
+
+if __name__ == "__main__":
+ pipeline_fetch_data()
diff --git a/content/modelisation/app/main_before_api.py b/content/modelisation/app/main_before_api.py
new file mode 100644
index 000000000..feef8db58
--- /dev/null
+++ b/content/modelisation/app/main_before_api.py
@@ -0,0 +1,18 @@
+import joblib
+import pandas as pd
+
+model = joblib.load("pipe.joblib")
+
+X_fictif = pd.DataFrame(
+ {
+ "month": [3, 12],
+ "Nombre_de_lots": [1, 2],
+ "Code_type_local": [2, 1],
+ "Nombre_pieces_principales": [3., 6.],
+ "surface": [75., 180.]
+ }
+)
+
+print(
+ model.predict(X_fictif)
+)
\ No newline at end of file
diff --git a/content/modelisation/app/requirements_api.txt b/content/modelisation/app/requirements_api.txt
new file mode 100644
index 000000000..6d6930bee
--- /dev/null
+++ b/content/modelisation/app/requirements_api.txt
@@ -0,0 +1,3 @@
+duckdb
+uvicorn
+fastapi
\ No newline at end of file
diff --git a/content/modelisation/app/train.py b/content/modelisation/app/train.py
new file mode 100644
index 000000000..1c24bcaee
--- /dev/null
+++ b/content/modelisation/app/train.py
@@ -0,0 +1,76 @@
+from joblib import dump
+import numpy as np
+import pandas as pd
+
+from sklearn.model_selection import GridSearchCV
+from sklearn.ensemble import GradientBoostingRegressor
+from sklearn.impute import SimpleImputer
+from sklearn.preprocessing import StandardScaler, OneHotEncoder
+from sklearn.pipeline import make_pipeline, Pipeline
+from sklearn.compose import make_column_transformer
+from sklearn.model_selection import train_test_split
+
+from getdvf import pipeline_fetch_data
+
+
+def pipeline_define(data):
+
+ numeric_features = data.columns[~data.columns.isin(['month', 'Valeur_fonciere'])].tolist()
+ categorical_features = ['month']
+
+ reg = GradientBoostingRegressor(random_state=0)
+
+ numeric_pipeline = make_pipeline(
+ SimpleImputer(),
+ StandardScaler()
+ )
+ transformer = make_column_transformer(
+ (numeric_pipeline, numeric_features),
+ (OneHotEncoder(sparse = False, handle_unknown = "ignore"), categorical_features))
+
+ pipe = Pipeline(steps=[('preprocessor', transformer),
+ ('boosting', reg)])
+
+ return pipe
+
+
+def create_data_cv(data):
+
+ pipe = pipeline_define(data)
+
+ X_train, X_test, y_train, y_test = train_test_split(
+ data.drop("Valeur_fonciere", axis = 1),
+ data[["Valeur_fonciere"]].values.ravel(),
+ test_size = 0.2, random_state = 123
+ )
+
+ X = pd.concat((X_train, X_test), axis=0)
+ Y = np.concatenate([y_train,y_test])
+
+ param_grid = {
+ "boosting__n_estimators": np.linspace(5,25, 5).astype(int),
+ "boosting__max_depth": [2,4]
+ }
+ grid_search = GridSearchCV(pipe, param_grid=param_grid)
+ grid_search.fit(X, Y)
+
+ return grid_search, X_train, y_train, X_test, y_test
+
+
+def train_best_model_cv(data):
+
+ grid_search, X_train, y_train, X_test, y_test = create_data_cv(data)
+
+ pipe_optimal = grid_search.best_estimator_
+ pipe_optimal.fit(X_train, y_train)
+
+ return pipe_optimal
+
+def dump_boosting_cv(data, filename='pipe.joblib'):
+ pipe_optimal = train_best_model_cv(data)
+ dump(pipe_optimal, filename)
+ return pipe_optimal
+
+if __name__ == "__main__":
+ mutations_paris = pipeline_fetch_data()
+ dump_boosting_cv(mutations_paris)
diff --git a/styles.css b/styles.css
index 2a7f8c6ef..627b46a08 100644
--- a/styles.css
+++ b/styles.css
@@ -76,3 +76,14 @@
text-decoration: none; /* Remove underline */
transition: transform 0.2s ease-in-out; /* Add transition for smooth effect */
}
+
+
+
+.blue-underlined {
+ color: #4758AB;
+ text-decoration: underline;
+}
+.red-underlined {
+ color: red;
+ text-decoration: underline;
+}
\ No newline at end of file