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} + +``` +::: + +## 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} + +``` +::: + + +## 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