Analyse de points d'intérêts (POI) : Autoencoder, espace latent et embending¶
Le notebook : https://sergelhomme.fr/notebook/Intro_IA_poi.ipynb
En géographie et en aménagement, les principaux exemples mobilisés pour illustrer l'intérêt des réseaux de neurones portent généralement sur le traitement d'images, notamment en télédétection. En matière d'enseignement, les modèles de prédiction économique constituent également des supports pédagogiques intéressants pour appréhender le fonctionnement des réseaux de neurones et développer ses premiers modèles. Néanmoins, depuis la fin des années 2010, l'adaptation de modèles de plongement lexical tels que Word2Vec aux données spatiales a favorisé l'émergence d'un champ de recherche original, encore relativement peu mis en avant : l'analyse spatiale des points d'intérêt (POI). L'objectif est alors de caractériser (de classer) les territoires à partir de ces POIs ou de mieux comprendre la logique d'implantation de ces activités. Dit de manière érudite, il s'agit de saisir la sémantique de l'espace ! On cherche à définir le « sens » d'un lieu — sa fonction urbaine — en analysant les types de commerces, de services ou d'infrastructures qui l'entourent et en traduisant la configuration spatiale d'un territoire en « concepts sémantiques ».
Les recherches récentes sur les POI s'intéressent notamment à la classification des espaces géographiques. Cette problématique n'est toutefois pas nouvelle en géographie, où les méthodes de statistique multivariée diffusées lors de la révolution quantitative des années 1960 occupent une place importante. Les analyses en composantes principales (ACP), parfois associées à des classifications ascendantes hiérarchiques (CAH) ou à des algorithmes de partitionnement tels que le k-means, constituent ainsi des outils classiques de typologie territoriale. Leur principal intérêt réside dans leur caractère exploratoire : les regroupements sont déterminés à partir des régularités observées dans les données plutôt qu'à partir d'une classification définie a priori. Ces approches présentent néanmoins plusieurs limites. Les résultats dépendent fortement des choix méthodologiques effectués (sélection et transformation des variables, métrique de distance, nombre de classes retenues), tandis que certaines méthodes reposent sur des hypothèses de linéarité qui peuvent s'avérer restrictives face à la complexité des phénomènes spatiaux.
Cette dernière limite est particulièrement importante dans la mesure où la géographie s'intéresse à des phénomènes spatiaux complexes dont les relations ne sont pas nécessairement linéaires. Dans cette perspective, les réseaux de neurones apparaissent comme une alternative intéressante grâce à leur capacité à apprendre des représentations non linéaires des données. Toutefois, ces méthodes sont souvent associées à l'apprentissage supervisé et aux tâches de prédiction nécessitant des données préalablement étiquetées. Cette caractéristique semble a priori peu compatible avec l'objectif exploratoire des classifications territoriales, puisqu'elle implique de fournir au modèle des catégories préexistantes servant de référence pour l'apprentissage (ce qui serait possible, car on peut imaginer demander à des experts de classer une partie des lieux d'un territoire, puis l'objectif du réseau sera d'apprendre la logique des experts et de réaliser la classification à grande échelle). Or, l'un des intérêts des démarches de classification en géographie est précisément de faire émerger des regroupements susceptibles de questionner ces catégories expertes ou institutionnelles.
Autoencoder et espace latent : classification non supervisée non linéaire¶
Les travaux menés dans le domaine de l'apprentissage profond ont néanmoins permis de dépasser en partie cette opposition. Parmi les avancées les plus importantes figurent les autoencodeurs, notamment développés et popularisés par Geoffrey Hinton et ses collaborateurs, qui permettent d'apprendre automatiquement des représentations latentes des données sans recourir à des labels. Pour cela, les autoencodeurs vont chercher à transformer les données de telle manière à les simplifier au maximum puis à reconstruire ces données pour retrouver le résultat initial. Un autoencodeur est un réseau de neurones qui prend un objet complexe, le contraint à passer dans une sorte d'entonnoir informationnel, puis tente d'en reconstruire une version aussi fidèle que possible. Il apprend ainsi à partir des données elles-mêmes, en cherchant à reconstruire son entrée. La première partie du réseau, appelée encodeur, est constituée de couches de neurones de taille décroissante jusqu'à atteindre la couche la plus petite, appelée espace latent ou couche latente. À ce stade, l'information a été compressée sous une forme plus compacte. La seconde partie, appelée décodeur, est alors chargée de reconstruire l'objet initial à partir de cette représentation condensée grâce à une succession de couches de neurones de taille croissante.
Pour faire très simple, un réseau de neurones doit être guidé pour apprendre, il doit être supervisé. Il doit pouvoir faire des prédictions dont il peut vérifier la justesse (X -> Y). Si on ne peut pas lui donner en entrée des labels (des Y, des données étiquetées, des prédictions à faire), les autoencodeurs sont une solution. Ils vont alors simplifier (compresser) les données qu'on leur donne, puis ils vont chercher à reconstruire les données à partir de cette simplification pour vérifier s'ils ont bien simplifié, s'ils ont bien compris (X -> Compression (Espace Latent) -> X). On utilisera l'espace latent pour produire Y (une prédiction qui n'en est pas vraiment une, une simple transformation).
Bien que le principe puisse paraître complexe, sa mise en œuvre est aujourd'hui relativement simple grâce aux bibliothèques disponibles en Python. En entrée, l'autoencodeur utilise le même type de tableau de données que celui mobilisé dans une classification non supervisée fondée sur une ACP. Les lignes correspondent aux objets géographiques que l'on souhaite caractériser et les colonnes aux variables descriptives. Dans cet exemple, nous nous appuyons sur les données de la Base Permanente des Équipements (BPE) pour l'Île-de-France. Une jointure spatiale est d'abord réalisée entre les équipements et les communes franciliennes à l'aide de la fonction sjoin(). La fonction crosstab() permet ensuite de construire un tableau de dénombrement croisant les communes et les types d'équipements. Les lignes correspondent alors aux communes, les colonnes aux catégories d'équipements, et chaque cellule indique le nombre d'équipements d'un type donné présents dans une commune. Les données sont téléchargeables ici.
import geopandas as gpd
import pandas as pd
import numpy as np
zones = gpd.read_file("COMMUNE-IDF.shp")
poi = gpd.read_file("BPE_IDF.shp")
poi_dans_zones = gpd.sjoin(poi, zones, how="inner", predicate="within")
matrice_brute = pd.crosstab(poi_dans_zones['INSEE_COM'], poi_dans_zones['Type'])
matrice_brute.head()
| Type | A101 | A104 | A105 | A108 | A109 | A120 | A121 | A122 | A124 | A125 | ... | F307 | F312 | F313 | F314 | F315 | G101 | G102 | G103 | G104 | G105 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| INSEE_COM | |||||||||||||||||||||
| 75101 | 9 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | ... | 1 | 4 | 4 | 0 | 7 | 78 | 81 | 0 | 44 | 3 |
| 75102 | 1 | 0 | 0 | 0 | 0 | 4 | 0 | 1 | 0 | 0 | ... | 1 | 0 | 0 | 0 | 5 | 62 | 37 | 0 | 31 | 0 |
| 75103 | 2 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | ... | 2 | 5 | 0 | 1 | 2 | 34 | 31 | 0 | 34 | 0 |
| 75104 | 5 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | ... | 4 | 6 | 4 | 0 | 6 | 19 | 40 | 0 | 32 | 1 |
| 75105 | 3 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | ... | 4 | 8 | 1 | 0 | 6 | 36 | 80 | 0 | 32 | 2 |
5 rows × 228 columns
Comme dans toute démarche de classification statistique, plusieurs questions se posent concernant le traitement des données : faut-il utiliser les données brutes ou les transformer ? Une normalisation est-elle nécessaire et, si oui, laquelle retenir ? Les réseaux de neurones, bien qu'ils permettent d'apprendre des relations non linéaires, ne dispensent pas de ces choix méthodologiques. Les résultats obtenus demeurent fortement dépendants de la manière dont les données sont préparées. Dans notre cas, il paraît pertinent de travailler sur des données relatives obtenues par une normalisation ligne par ligne. Sans cette étape, le réseau aurait tendance à regrouper les communes principalement en fonction de leur nombre total d'équipements plutôt qu'en fonction de la structure de leur offre d'équipements. Toutefois, une normalisation par ligne présente également un inconvénient : elle conduit à considérer comme identiques deux communes présentant la même répartition d'équipements, même si l'une en possède dix fois plus d'équipements que l'autre. Afin de conserver cette information de taille, nous ajoutons donc une variable supplémentaire correspondant au nombre total d'équipements présents dans chaque commune.
from sklearn.preprocessing import Normalizer
scaler = Normalizer(norm='l1')
X = scaler.fit_transform(matrice_brute)
taille = matrice_brute.sum(axis=1).to_numpy() # Prise en compte de la taille
X = np.column_stack((X, taille))
nb_variables = X.shape[1]
Nous disposons ainsi de 229 variables descriptives, que nous choisissons de projeter dans un espace latent de cinq dimensions. Pourquoi cinq dimensions ? Nous reviendrons en partie sur cette question lors de l'analyse des résultats. Pour réaliser cette compression, l'encodeur comporte une couche intermédiaire de 32 neurones. D'autres architectures auraient pu être envisagées, avec 16, 64 ou 128 neurones, voire plusieurs couches intermédiaires successives. En pratique, ces choix relèvent largement d'une démarche empirique et sont guidés par la qualité des résultats obtenus, notamment en termes de reconstruction et d'interprétation. La fonction d'activation retenue est une fonction ReLU, couramment utilisée dans ce type d'application. L'encodeur étant défini, le décodeur est construit de manière symétrique afin de reconstruire les données initiales à partir de l'espace latent. Seule la couche de sortie diffère et utilise une activation linéaire, adaptée à la reconstruction de variables quantitatives continues. Si le jeu de données n'avait contenu que les variables normalisées comprises entre 0 et 1, une fonction sigmoïde aurait également pu être envisagée pour la couche de sortie.
import tensorflow as tf
from tensorflow.keras import layers, models
dimension_latente = 5
input_layer = layers.Input(shape=(nb_variables,))
# Encodeur : Compresse les données via des fonctions non linéaires (ReLU)
encoded = layers.Dense(32, activation='relu')(input_layer)
bottleneck = layers.Dense(dimension_latente, activation='relu', name="couche_latente")(encoded)
# Décodeur : Tente de reconstruire le vecteur d'origine à partir de la compression
decoded = layers.Dense(32, activation='relu')(bottleneck)
output_layer = layers.Dense(nb_variables, activation='linear')(decoded)
Il convient également de noter que le réseau a été implémenté à l'aide de l'API fonctionnelle de Keras plutôt qu'avec l'API séquentielle. Dans le cas présent, l'autoencodeur aurait pu être défini sous forme séquentielle sans difficulté particulière. Toutefois, l'API fonctionnelle offre davantage de souplesse pour construire des architectures plus complexes, notamment lorsque plusieurs couches, entrées ou sorties doivent être combinées. Une fois le réseau défini, le modèle est compilé puis entraîné de manière classique en minimisant l'erreur de reconstruction entre les données d'entrée et les données reconstruites par le décodeur.
# Assemblage et compilation du modèle complet
autoencoder = models.Model(inputs=input_layer, outputs=output_layer)
autoencoder.compile(optimizer='adam', loss='mse')
# Entraînement : Le modèle apprend à prédire X à partir de X (auto-supervision)
history = autoencoder.fit(X, X, epochs=100, batch_size=32, shuffle=True, verbose=0)
# Extraction de l'encodeur seul pour récupérer les signatures compressées
encoder_modele = models.Model(inputs=input_layer, outputs=autoencoder.get_layer("couche_latente").output)
X_latent = encoder_modele.predict(X)
41/41 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step
Une fois l'apprentissage terminé, nous disposons d'un espace latent que l'on peut explorer et exploiter pour construire une classification. Dans ce cadre non supervisé, le réseau de neurones ne produit pas directement les classes. Son rôle consiste plutôt à apprendre une représentation compacte des données en réduisant la dimensionnalité du problème. Cette démarche est conceptuellement proche de celle de l'ACP ou de l'ACM : dans les deux cas, il s'agit de résumer l'information contenue dans un grand nombre de variables à l'aide d'un nombre réduit de dimensions. La différence essentielle est qu'un autoencodeur est capable d'apprendre des relations non linéaires entre les variables. Pour autant, les questions méthodologiques classiques ne disparaissent pas. Il faut toujours déterminer le nombre de dimensions de l'espace latent, choisir une méthode de classification et définir le nombre de classes à retenir. Dans cet exemple, nous utilisons simplement l'algorithme K-means afin de partitionner l'espace latent en cinq groupes.
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=5, random_state=42)
labels = kmeans.fit_predict(X_latent)
resultats = pd.DataFrame({'INSEE_COM': matrice_brute.index, 'cluster_id': labels})
zones_classifiees = zones.merge(resultats, on='INSEE_COM', how='left')
zones_classifiees.plot('cluster_id')
C:\Users\Serge\anaconda3\Lib\site-packages\sklearn\cluster\_kmeans.py:870: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning warnings.warn( C:\Users\Serge\anaconda3\Lib\site-packages\sklearn\cluster\_kmeans.py:1382: UserWarning: KMeans is known to have a memory leak on Windows with MKL, when there are less chunks than available threads. You can avoid it by setting the environment variable OMP_NUM_THREADS=6. warnings.warn(
<Axes: >
L'évaluation d'une classification obtenue par apprentissage non supervisé n'est pas une tâche simple. Dans un premier temps, il est possible d'examiner les résultats afin d'en apprécier la cohérence géographique et l'interprétabilité. Dans notre cas, les classes obtenues semblent faire apparaître une logique centre-périphérie relativement cohérente. Il est toutefois possible que la prise en compte explicite du nombre total d'équipements ait renforcé cette structure au détriment d'autres dimensions plus fines, hypothèse qu'il serait possible de tester statistiquement. Cette question est d'autant plus complexe à résoudre ici que les données utilisées n'ont fait l'objet d'aucune pondération préalable. Les 228 catégories d'équipements sont traitées de manière équivalente, indépendamment de leur importance fonctionnelle ou de leur rôle dans l'organisation territoriale. Dès lors, le réseau tend naturellement à apprendre les structures les plus saillantes présentes dans les données. Si certaines dimensions jugées importantes par l'analyste ne sont que faiblement représentées, elles risquent d'être moins visibles dans l'espace latent. N'hésitez pas à expérimenter différents paramètres, architectures ou transformations des données afin d'observer l'impact de ces choix sur les classifications produites.
Dit plus simplement, on peut s'attendre naïvement que le réseau retrouve notre logique et produise des classifications attendues, mais ne pas l'aider rend la mission complexe voire impossible pour lui. Lorsqu'il n'est pas supervisé, logiquement il raisonne à sa façon. Si on veut qu'il raisonne comme nous sans données étiquetées, sans données initiales qu’il peut prédire et qui vont guider l'apprentissage, il faudra lui donner des indications dans le tableau de données.
J'espère que cet exemple montre que les réseaux de neurones ne remettent pas fondamentalement en cause les principes des classifications non supervisées. Ils constituent avant tout une alternative permettant d'apprendre des représentations non linéaires des données, tout en conservant la plupart des questionnements méthodologiques classiques. Leur intérêt apparaît notamment lorsque les relations entre variables sont complexes ou lorsque le volume de données devient très important. Comme pour tout modèle, il convient toutefois de vérifier la qualité de l'apprentissage. L'examen de la fonction de perte (loss) permet notamment de s'assurer de la convergence du réseau et de détecter d'éventuelles anomalies. L'analyse de l'espace latent constitue également un outil précieux. Dans notre exemple, l'information semble principalement portée par deux des cinq dimensions latentes. Cela suggère qu'un espace latent plus compact, limité à deux dimensions, pourrait sans doute être testé lors d'expérimentations ultérieures. Ce faible nombre laisse à penser qu'une variable, ici le nombre total d'équipements, écrase les autres.
X_latent[0:5,:]
array([[2143.5483, 0. , 0. , 3118.9468, 0. ],
[1328.3812, 0. , 0. , 1933.0895, 0. ],
[1339.4637, 0. , 0. , 1949.1862, 0. ],
[1361.1276, 0. , 0. , 1980.6943, 0. ],
[1773.9767, 0. , 0. , 2581.3057, 0. ]],
dtype=float32)
import matplotlib.pyplot as plt
loss_valeurs = history.history['loss']
epoches = range(1, len(loss_valeurs) + 1)
plt.figure(figsize=(8, 5))
plt.plot(epoches, loss_valeurs, 'b-')
plt.title("Fonction de Loss pendant l'entraînement")
plt.xlabel('Époques')
plt.ylabel('Erreur (Loss)')
plt.grid(True)
plt.legend()
plt.show()
No artists with labels found to put in legend. Note that artists whose label start with an underscore are ignored when legend() is called with no argument.
Embending : la base des LLM pour créer une sémantique territoriale¶
Nous allons maintenant aborder la notion d'embedding textuel, qui constitue l'un des concepts fondamentaux à l'origine des grands modèles de langage (LLM). Si l'on a compris le principe des autoencodeurs et de l'espace latent, il devient relativement facile de saisir l'intuition derrière les embeddings. Dans les deux cas, l'objectif consiste à représenter des objets complexes à l'aide d'un nombre réduit de dimensions numériques. Un embedding peut ainsi être vu comme une représentation vectorielle apprise automatiquement par un réseau de neurones, autrement dit comme une forme particulière d'espace latent adaptée à un problème donné. L'encodeur est le mécanisme (la fonction) qui transforme les données jusqu'à l'espace latent (l'espace géométrique appris par le modèle). L'embending ce sont les vecteurs finaux (la position d'un objet dans cet espace).
Si l'on applique cette idée aux points d'intérêt, la problématique devient alors : « Dis-moi quels lieux t'entourent et je te dirai quel type de lieu tu es. » Cette approche introduit une dimension nouvelle dans l'analyse spatiale quantitative : elle cherche à faire émerger la sémantique des lieux à partir des configurations spatiales observées. Bien entendu, la géographie et la statistique spatiale disposaient déjà de nombreux outils pour étudier les relations entre objets géographiques. Cependant, ces méthodes n'étaient généralement pas formulées en termes de sémantique spatiale et ne visaient pas explicitement à apprendre une représentation du « sens » des lieux à partir de leur contexte. Selon moi, ces approches nouvelles sont potentiellement disruptives.
L'approche CBOW (Continuous Bag of Words) consiste à prédire le mot central à partir du contexte. L'approche Skip-Gram consiste à prédire le contexte à partir du mot central. Dans les deux cas se pose une question importante : faut-il tenir compte de l'ordre exact des mots ou simplement des mots présents dans le voisinage ? Les modèles de type Word2Vec adoptent une réponse relativement simple. Ils considèrent principalement les mots présents dans une fenêtre de contexte donnée, sans chercher à modéliser finement leur organisation syntaxique. Cette approche s'est révélée remarquablement efficace pour apprendre des représentations sémantiques des mots. Néanmoins, des architectures plus récentes peuvent conserver explicitement l'information de position grâce à des couches supplémentaires ou à des mécanismes dédiés, permettant ainsi au réseau d'apprendre que certains voisins ont une influence différente selon leur position dans la séquence. En tant que géographe dire que la distance n'a pas d'intérêt est une position forte et très discutable. On donc va s'inspirer des approches plus récentes pour tenir compte de la distance, plus exactement ici, de l'ordre de proximité.
On commence par charger les données : 39 types d'équipements répartis dans le Val-de-Marne. On cherche ensuite à déterminer le voisinage des équipements. Pour cela, on va retenir les 10 plus proches voisins par ordre de proximité à l'aide d'un algorithme efficace KDtree(). Chaque équipement possède ainsi une liste ordonnée de 10 voisins correspondant à 10 types d'équipements. A noter qu'un point d'intérêt peut très bien avoir plusieurs voisins de même nature, par exemple ci-dessous le supermarché a deux parfumeries à proximité direct.
import geopandas as gpd
import numpy as np
from scipy.spatial import KDTree
gdf = gpd.read_file("BPE_IDF_39_VDM.shp")
coordonnees = np.array(list(zip(gdf.geometry.x, gdf.geometry.y))) #Extraction des coordonnées X, Y
arbre = KDTree(coordonnees)
distances, indices = arbre.query(coordonnees, k=11) #on cherche les 11 voisins les plus proches
indices_voisins = indices[:, 1:] #Extraction des types de POI à partir des indices trouvés
types_codes = gdf['NOM_EQ'].values
gdf['voisins_types'] = [list(types_codes[idx]) for idx in indices_voisins] # Création de la liste des types des 10 voisins pour chaque ligne
print(gdf[['NOM_EQ', 'voisins_types']].head())
NOM_EQ voisins_types 0 PRESSING-LAVERIE [PAPETERIE_ET_PRESSE, SUPERMARCHE, MAGASIN_OPT... 1 SUPERMARCHE [SUPERMARCHE, PARFUMERIE, VETERINAIRE, PARFUME... 2 EPICERIE [DENTISTE, BANQUE, BOULANGERIE, BANQUE, RECHAR... 3 EPICERIE [CRECHE, EPICERIE, MAGASIN_ELECTROMENAGER, MED... 4 EPICERIE [PRESSING-LAVERIE, MAGASIN_EQUIPEMENTS_DU_FOYE...
Pour créer notre réseau, nous commençons par transformer les mots en identifiants entiers. Cette étape ne produit pas encore de représentation sémantique : elle permet simplement d'obtenir un format exploitable par la couche d'embedding. Celle-ci apprendra ensuite à associer à chaque mot un vecteur numérique reflétant son usage dans les données. Pour réaliser cet encodage initial, nous utilisons le LabelEncoder de sklearn.
import numpy as np
import tensorflow as tf
from tensorflow.keras import layers, models
from sklearn.preprocessing import LabelEncoder
# On initialise l'encodeur de texte
le = LabelEncoder()
# On ajuste l'encodeur sur l'ensemble de tes 39 types de POI possibles
le.fit(gdf['NOM_EQ'])
taille_vocabulaire = len(le.classes_)
# Encodage de la cible (la sortie : ce que le réseau doit deviner)
y = le.transform(gdf['NOM_EQ'])
# Encodage des entrées (les listes de 10 voisins)
# On transforme chaque liste de chaînes de caractères en liste d'entiers
X = np.array([le.transform(voisins) for voisins in gdf['voisins_types']])
Notre réseau débute par une couche d'embedding chargée d'apprendre une représentation vectorielle des mots. La dimension des embeddings est fixée à 16 ; ce choix reste empirique et sera évalué à partir des résultats obtenus. Les vecteurs produits sont ensuite aplatis (Flatten) afin que chaque position du contexte soit représentée par un ensemble distinct de variables dans les couches suivantes. Cette opération permet au réseau de prendre en compte la position des mots dans la séquence. Le modèle comporte ensuite deux couches cachées de respectivement 64 et 32 neurones utilisant une fonction d'activation de type LeakyReLU. Enfin, la couche de sortie possède autant de neurones qu'il existe de types d'activités à prédire et utilise une activation softmax. Cette dernière produit une distribution de probabilités sur l'ensemble des classes possibles ; lors de la prédiction, la classe retenue correspond à celle présentant la probabilité la plus élevée.
# =====================================================================
# ARCHITECTURE DU RÉSEAU DE NEURONES (AVEC EMBEDDING)
# =====================================================================
dim_embedding = 16 # Chaque POI sera représenté par un vecteur de 16 chiffres
longueur_sequence = 10 # On regarde les 10 voisins les plus proches
model = models.Sequential([
# Couche d'Entrée : Reçoit les listes de 10 indices de voisins
layers.Input(shape=(longueur_sequence,)),
# Couche d'Embedding : Transforme (1300, 10) en (1300, 10, 16) elle crée l'espace sémantique de tes types d'équipements
layers.Embedding(input_dim=taille_vocabulaire, output_dim=dim_embedding),
# Couche de Flatten : On "aplatit" la matrice (10 * 16 = 160 neurones) pour la passer aux couches denses suivantes, en préservant l'ordre
layers.Flatten(),
# Couches cachées non linéaires pour capter les synergies de voisinage
layers.Dense(64),
layers.LeakyReLU(negative_slope=0.01),
layers.Dropout(0.2), # Évite le surapprentissage
layers.Dense(32),
layers.LeakyReLU(negative_slope=0.01),
# Couche de Sortie : 39 neurones avec Softmax pour obtenir les probabilités
layers.Dense(taille_vocabulaire, activation='softmax')
])
On compile le modèle, toujours avec l'optimiseur adam.
# 'sparse_categorical_crossentropy' est parfaite car 'y' contient des entiers (0 à 38) et non des vecteurs binaires (One-Hot encoded)
model.compile(
optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy']
)
model.summary()
history = model.fit(
X, y,
epochs=80,
batch_size=32,
validation_split=0.2, # Garde 20% des données pour tester la généralisation
shuffle=True
)
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩ │ embedding (Embedding) │ (None, 10, 16) │ 624 │ ├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤ │ flatten (Flatten) │ (None, 160) │ 0 │ ├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤ │ dense_3 (Dense) │ (None, 64) │ 10,304 │ ├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤ │ leaky_re_lu (LeakyReLU) │ (None, 64) │ 0 │ ├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤ │ dropout (Dropout) │ (None, 64) │ 0 │ ├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤ │ dense_4 (Dense) │ (None, 32) │ 2,080 │ ├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤ │ leaky_re_lu_1 (LeakyReLU) │ (None, 32) │ 0 │ ├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤ │ dense_5 (Dense) │ (None, 39) │ 1,287 │ └──────────────────────────────────────┴─────────────────────────────┴─────────────────┘
Total params: 14,295 (55.84 KB)
Trainable params: 14,295 (55.84 KB)
Non-trainable params: 0 (0.00 B)
Epoch 1/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 2s 3ms/step - accuracy: 0.0965 - loss: 3.4714 - val_accuracy: 0.2203 - val_loss: 2.9502 Epoch 2/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.2255 - loss: 2.9622 - val_accuracy: 0.2464 - val_loss: 2.8439 Epoch 3/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.2432 - loss: 2.8638 - val_accuracy: 0.2639 - val_loss: 2.8053 Epoch 4/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.2732 - loss: 2.7909 - val_accuracy: 0.2680 - val_loss: 2.7879 Epoch 5/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.2796 - loss: 2.7396 - val_accuracy: 0.2726 - val_loss: 2.7870 Epoch 6/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.2899 - loss: 2.6990 - val_accuracy: 0.2827 - val_loss: 2.7730 Epoch 7/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.2981 - loss: 2.6906 - val_accuracy: 0.2845 - val_loss: 2.7792 Epoch 8/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3061 - loss: 2.6516 - val_accuracy: 0.2799 - val_loss: 2.7844 Epoch 9/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.2945 - loss: 2.6471 - val_accuracy: 0.2887 - val_loss: 2.7822 Epoch 10/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3059 - loss: 2.6172 - val_accuracy: 0.2896 - val_loss: 2.7854 Epoch 11/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3155 - loss: 2.5588 - val_accuracy: 0.2900 - val_loss: 2.7905 Epoch 12/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.3138 - loss: 2.5600 - val_accuracy: 0.2887 - val_loss: 2.8060 Epoch 13/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.3178 - loss: 2.5538 - val_accuracy: 0.2919 - val_loss: 2.8158 Epoch 14/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3244 - loss: 2.5331 - val_accuracy: 0.2910 - val_loss: 2.8161 Epoch 15/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3251 - loss: 2.4967 - val_accuracy: 0.2900 - val_loss: 2.8198 Epoch 16/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3244 - loss: 2.4801 - val_accuracy: 0.2896 - val_loss: 2.8287 Epoch 17/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3384 - loss: 2.4759 - val_accuracy: 0.2873 - val_loss: 2.8482 Epoch 18/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3333 - loss: 2.4607 - val_accuracy: 0.2965 - val_loss: 2.8514 Epoch 19/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3397 - loss: 2.4333 - val_accuracy: 0.2919 - val_loss: 2.8545 Epoch 20/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3328 - loss: 2.4510 - val_accuracy: 0.2896 - val_loss: 2.8711 Epoch 21/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3482 - loss: 2.3911 - val_accuracy: 0.2887 - val_loss: 2.8786 Epoch 22/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3479 - loss: 2.3864 - val_accuracy: 0.2887 - val_loss: 2.8987 Epoch 23/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3517 - loss: 2.3284 - val_accuracy: 0.2887 - val_loss: 2.8922 Epoch 24/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3468 - loss: 2.3488 - val_accuracy: 0.2855 - val_loss: 2.9118 Epoch 25/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3512 - loss: 2.3668 - val_accuracy: 0.2786 - val_loss: 2.9232 Epoch 26/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3505 - loss: 2.3386 - val_accuracy: 0.2868 - val_loss: 2.9373 Epoch 27/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3636 - loss: 2.2864 - val_accuracy: 0.2836 - val_loss: 2.9430 Epoch 28/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.3488 - loss: 2.2975 - val_accuracy: 0.2822 - val_loss: 2.9606 Epoch 29/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3584 - loss: 2.2617 - val_accuracy: 0.2804 - val_loss: 2.9782 Epoch 30/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.3588 - loss: 2.2666 - val_accuracy: 0.2804 - val_loss: 2.9726 Epoch 31/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3651 - loss: 2.2498 - val_accuracy: 0.2735 - val_loss: 2.9831 Epoch 32/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.3667 - loss: 2.2410 - val_accuracy: 0.2763 - val_loss: 3.0001 Epoch 33/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.3662 - loss: 2.2340 - val_accuracy: 0.2735 - val_loss: 3.0175 Epoch 34/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3698 - loss: 2.2167 - val_accuracy: 0.2726 - val_loss: 3.0143 Epoch 35/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3657 - loss: 2.2137 - val_accuracy: 0.2749 - val_loss: 3.0218 Epoch 36/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.3680 - loss: 2.1877 - val_accuracy: 0.2772 - val_loss: 3.0317 Epoch 37/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3693 - loss: 2.1901 - val_accuracy: 0.2731 - val_loss: 3.0503 Epoch 38/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3714 - loss: 2.1859 - val_accuracy: 0.2671 - val_loss: 3.0495 Epoch 39/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3685 - loss: 2.1877 - val_accuracy: 0.2740 - val_loss: 3.0703 Epoch 40/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.3692 - loss: 2.1596 - val_accuracy: 0.2676 - val_loss: 3.0818 Epoch 41/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3822 - loss: 2.1425 - val_accuracy: 0.2717 - val_loss: 3.1019 Epoch 42/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3776 - loss: 2.1240 - val_accuracy: 0.2694 - val_loss: 3.0949 Epoch 43/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3863 - loss: 2.1088 - val_accuracy: 0.2754 - val_loss: 3.0972 Epoch 44/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3935 - loss: 2.1023 - val_accuracy: 0.2698 - val_loss: 3.1182 Epoch 45/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3907 - loss: 2.0767 - val_accuracy: 0.2680 - val_loss: 3.1231 Epoch 46/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3791 - loss: 2.1196 - val_accuracy: 0.2694 - val_loss: 3.1355 Epoch 47/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.3849 - loss: 2.1118 - val_accuracy: 0.2657 - val_loss: 3.1419 Epoch 48/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.3897 - loss: 2.0780 - val_accuracy: 0.2735 - val_loss: 3.1559 Epoch 49/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 4ms/step - accuracy: 0.3987 - loss: 2.0493 - val_accuracy: 0.2767 - val_loss: 3.1606 Epoch 50/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.3938 - loss: 2.0710 - val_accuracy: 0.2698 - val_loss: 3.1684 Epoch 51/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.3965 - loss: 2.0745 - val_accuracy: 0.2653 - val_loss: 3.1989 Epoch 52/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.3974 - loss: 2.0601 - val_accuracy: 0.2657 - val_loss: 3.2007 Epoch 53/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.3927 - loss: 2.0504 - val_accuracy: 0.2653 - val_loss: 3.2135 Epoch 54/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.3985 - loss: 2.0331 - val_accuracy: 0.2648 - val_loss: 3.2229 Epoch 55/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.4036 - loss: 2.0310 - val_accuracy: 0.2588 - val_loss: 3.2210 Epoch 56/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.4016 - loss: 2.0156 - val_accuracy: 0.2643 - val_loss: 3.2266 Epoch 57/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.3972 - loss: 2.0318 - val_accuracy: 0.2584 - val_loss: 3.2426 Epoch 58/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.4057 - loss: 2.0146 - val_accuracy: 0.2579 - val_loss: 3.2484 Epoch 59/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.4093 - loss: 2.0158 - val_accuracy: 0.2570 - val_loss: 3.2626 Epoch 60/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.4141 - loss: 1.9742 - val_accuracy: 0.2593 - val_loss: 3.2574 Epoch 61/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.4034 - loss: 2.0067 - val_accuracy: 0.2565 - val_loss: 3.2872 Epoch 62/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.3948 - loss: 2.0064 - val_accuracy: 0.2634 - val_loss: 3.2895 Epoch 63/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.4039 - loss: 1.9841 - val_accuracy: 0.2630 - val_loss: 3.3148 Epoch 64/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.4171 - loss: 1.9755 - val_accuracy: 0.2593 - val_loss: 3.3081 Epoch 65/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.4092 - loss: 1.9735 - val_accuracy: 0.2611 - val_loss: 3.3229 Epoch 66/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.4140 - loss: 1.9557 - val_accuracy: 0.2575 - val_loss: 3.3240 Epoch 67/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.4111 - loss: 1.9604 - val_accuracy: 0.2602 - val_loss: 3.3333 Epoch 68/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.4179 - loss: 1.9623 - val_accuracy: 0.2666 - val_loss: 3.3289 Epoch 69/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.4192 - loss: 1.9498 - val_accuracy: 0.2542 - val_loss: 3.3466 Epoch 70/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.4078 - loss: 1.9655 - val_accuracy: 0.2616 - val_loss: 3.3689 Epoch 71/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.4228 - loss: 1.9100 - val_accuracy: 0.2593 - val_loss: 3.3618 Epoch 72/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.4105 - loss: 1.9538 - val_accuracy: 0.2616 - val_loss: 3.3699 Epoch 73/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.4138 - loss: 1.9237 - val_accuracy: 0.2630 - val_loss: 3.3929 Epoch 74/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.4033 - loss: 1.9628 - val_accuracy: 0.2653 - val_loss: 3.3901 Epoch 75/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.4195 - loss: 1.9256 - val_accuracy: 0.2602 - val_loss: 3.3990 Epoch 76/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.4150 - loss: 1.9377 - val_accuracy: 0.2598 - val_loss: 3.3970 Epoch 77/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.4256 - loss: 1.9156 - val_accuracy: 0.2538 - val_loss: 3.4067 Epoch 78/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.4193 - loss: 1.8860 - val_accuracy: 0.2570 - val_loss: 3.4124 Epoch 79/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.4273 - loss: 1.9008 - val_accuracy: 0.2588 - val_loss: 3.4357 Epoch 80/80 273/273 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - accuracy: 0.4223 - loss: 1.9238 - val_accuracy: 0.2542 - val_loss: 3.4546
Bien entendu, on peut observer l'évolution de la fonction de loss pour vérifier que l'apprentissage s'est bien passé. On peut aussi regarder ce que produit le modèle sur certains exemples :
nouvelle_liste_voisins = gdf['voisins_types'][8]
print(gdf['NOM_EQ'][8])
print(nouvelle_liste_voisins)
probabilites = model.predict(np.array([le.transform(nouvelle_liste_voisins)]))[0]
print(probabilites)
indices_top10 = np.argsort(probabilites)[::-1][:10]
print("Top 10 des prédictions pour ce contexte :")
for idx in indices_top10:
print(f" - {le.classes_[idx]:<20} : {probabilites[idx]*100:.1f}%")
COLLEGE ['TERRAINS_DE_JEUX', 'PRESSING-LAVERIE', 'RECHARGE_VE', 'ECOLE_PRIMAIRE', 'ACCUEIL_DE_LOISIR', 'RECHARGE_VE', 'VETERINAIRE', 'ACCUEIL_DE_LOISIR', 'ACCUEIL_DE_LOISIR', 'RECHARGE_VE'] 1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 94ms/step [8.3928734e-02 3.0777149e-03 2.1206073e-03 2.1306642e-03 6.1625942e-05 2.8441139e-02 6.5922454e-02 3.0475393e-01 8.4460163e-03 1.5319550e-02 7.4578857e-04 8.4470175e-03 2.6921999e-02 1.3197457e-02 2.7266217e-03 3.8608789e-04 1.8879889e-04 2.2408354e-01 1.9120071e-05 2.1077096e-03 2.9752188e-04 8.0991845e-04 8.2608581e-02 1.2611956e-02 1.1769765e-04 5.9316467e-06 4.8209622e-05 2.1286301e-04 6.1705210e-03 5.7809381e-04 3.6112575e-05 8.0829486e-03 7.7409162e-03 4.5228195e-03 3.8374970e-03 4.9597099e-03 5.0288141e-03 6.8905674e-02 3.9757942e-04] Top 10 des prédictions pour ce contexte : - COLLEGE : 30.5% - GYMNASES : 22.4% - ACCUEIL_DE_LOISIR : 8.4% - MAGASIN_DE_VETEMENTS : 8.3% - TERRAINS_DE_JEUX : 6.9% - BOULANGERIE : 6.6% - BOUCHERIE : 2.8% - ECOLE_PRIMAIRE : 2.7% - CRECHE : 1.5% - ELEMENTAIRE : 1.3%
Comme attendu, le réseau identifie correctement un collège à partir des types d'équipements présents dans son voisinage. Le modèle semble avoir appris qu'un environnement associant notamment des terrains de jeu, une école primaire ou encore un accueil de loisirs est fréquemment lié à la présence d'autres équipements d'enseignement. L'examen des autres probabilités prédites est également instructif pour évaluer la cohérence du modèle. En effet, lorsqu'un grand nombre de catégories est considéré, il est souvent plus pertinent d'analyser les prédictions les plus probables que de se limiter à la seule classe majoritaire. Dans notre exemple, le modèle attribue également des probabilités relativement élevées à un gymnase, à un terrain de jeu ou à un accueil de loisir. Ces équipements partagent des caractéristiques fonctionnelles proches et apparaissent fréquemment dans des contextes similaires. À l'inverse, des activités telles qu'une agence de voyage ou un magasin d'électroménager reçoivent des probabilités très faibles. La distribution des probabilités suggère ainsi que le modèle a appris des associations spatiales cohérentes entre les différents types d'équipements. Enfin, les embeddings peuvent être projetés dans un espace de faible dimension à l'aide d'une ACP afin d'explorer les proximités apprises par le réseau. Dans notre cas, l'un des principaux axes semble notamment distinguer des équipements majoritairement publics d'équipements davantage orientés vers les activités privées, ce qui constitue une structure particulièrement plausible au regard de l'organisation des territoires.
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
import pandas as pd
couche_embedding = model.layers[0]
poids_embedding = couche_embedding.get_weights()[0]
noms_poi = le.classes_
df_embedding = pd.DataFrame(poids_embedding, index=noms_poi)
pca = PCA(n_components=4) # ACP
coords_pca = pca.fit_transform(df_embedding)
variance_expliquee = pca.explained_variance_ratio_ * 100
print(f"L'Axe 1 explique {variance_expliquee[0]:.1f}% de la variance.")
print(f"L'Axe 2 explique {variance_expliquee[1]:.1f}% de la variance.")
plt.figure(figsize=(14, 10))
plt.scatter(coords_pca[:, 0], coords_pca[:, 1], color='blue', alpha=0.6, edgecolors='k', s=80)
for i, nom in enumerate(noms_poi):
plt.text(coords_pca[i, 0] + 0.02, coords_pca[i, 1] + 0.02, nom, fontsize=10, alpha=0.8)
plt.title("Espace sémantique des équipements", fontsize=14, fontweight='bold')
plt.xlabel(f"Axe Principal 1 ({variance_expliquee[0]:.1f}%)", fontsize=12)
plt.ylabel(f"Axe Principal 2 ({variance_expliquee[1]:.1f}%)", fontsize=12)
plt.grid(True, linestyle='--', alpha=0.5)
# Centrage des axes sur 0
plt.axhline(0, color='black', linewidth=0.8, linestyle='-')
plt.axvline(0, color='black', linewidth=0.8, linestyle='-')
plt.show()
print(f"L'Axe 3 explique {variance_expliquee[2]:.1f}% de la variance.")
print(f"L'Axe 4 explique {variance_expliquee[3]:.1f}% de la variance.")
plt.figure(figsize=(14, 10))
plt.scatter(coords_pca[:, 2], coords_pca[:, 3], color='blue', alpha=0.6, edgecolors='k', s=80)
for i, nom in enumerate(noms_poi):
plt.text(coords_pca[i, 2] + 0.02, coords_pca[i, 3] + 0.02, nom, fontsize=10, alpha=0.8)
plt.title("Espace sémantique des équipements", fontsize=14, fontweight='bold')
plt.xlabel(f"Axe Principal 3 ({variance_expliquee[2]:.1f}%)", fontsize=12)
plt.ylabel(f"Axe Principal 4 ({variance_expliquee[3]:.1f}%)", fontsize=12)
plt.grid(True, linestyle='--', alpha=0.5)
# Centrage des axes sur 0
plt.axhline(0, color='black', linewidth=0.8, linestyle='-')
plt.axvline(0, color='black', linewidth=0.8, linestyle='-')
plt.show()
L'Axe 1 explique 11.3% de la variance. L'Axe 2 explique 10.4% de la variance.
L'Axe 3 explique 9.7% de la variance. L'Axe 4 explique 8.7% de la variance.