Prédire la Cybersickness
par Machine Learning

Documentation technique complète — problème, code ligne par ligne, analyse des plots et interprétation scientifique des résultats.

🐍 Python · scikit-learn 📊 300 participants VR ⚡ 96.7% accuracy 🔬 Supervisé + Non Supervisé ✍️ Sarah Mahmoudi
🎯 Section 0

La Problématique — Pourquoi ce projet ?

Avant tout code et toute technique, il faut comprendre le problème réel qu'on cherche à résoudre. Cette section explique le contexte, l'enjeu scientifique et pourquoi le Machine Learning est la bonne approche.

Le contexte : la VR face à un problème de santé ignoré

La réalité virtuelle est aujourd'hui déployée dans des domaines critiques : thérapie comportementale, rééducation post-AVC, formation médicale, éducation et industrie. Pourtant, une proportion importante des utilisateurs développe des symptômes invalidants dès les premières minutes d'immersion.

🥽

La Réalité Virtuelle

Technologie immersive à fort potentiel thérapeutique et éducatif. Des millions d'utilisateurs y sont exposés sans évaluation préalable de leur tolérance ou vulnérabilité.

🤢

La Cybersickness

Vertiges (80%), désorientation (88%), nausées (30%), maux de tête (30%). Dans les cas extrêmes : panique et chutes physiques observées lors de scènes à haute hauteur.

🚫

L'absence d'outils

Les systèmes VR n'intègrent aucun mécanisme d'adaptation au profil de l'utilisateur. Aucun standard clinique n'existe pour évaluer le risque avant une session immersive.

🤖

Pourquoi le ML ?

Le Machine Learning peut apprendre les patterns complexes entre profil utilisateur et symptômes, pour prédire le niveau de cybersickness et identifier les profils à risque automatiquement.

Observations terrain réelles — base de ce projet

Données collectées lors de la Journée de la Santé Citée & Porte de Versailles, ~100 participants en session VR réelle :

88%
Désorientation
80%
Vertiges
30%
Nausées
30%
Maux de tête
5%
Peur / panique
~0%
Sueurs

Les 4 objectifs ML du projet

#ObjectifTechniqueType d'apprentissageBut concret
1Prédire le niveau de cybersicknessClassificationSuperviséFaible / Modéré / Sévère
2Estimer le score SSQ exactRégressionSuperviséUn nombre continu ex: 6.75
3Visualiser les données 13D en 2DPCANon superviséComprendre la structure des données
4Découvrir des profils utilisateursK-MeansNon superviséGroupes naturels sans labels
💡Supervisé vs Non supervisé : En apprentissage supervisé, on donne au modèle des exemples avec les réponses (X + y) et il apprend la relation. En apprentissage non supervisé, on ne donne que les données (X) et le modèle découvre lui-même des structures cachées — sans savoir à l'avance ce qu'il cherche.
📦 Section 1

Génération du Dataset

Les ~30 questionnaires papier collectés sur le terrain sont insuffisants pour entraîner un modèle ML fiable (minimum recommandé : 150–300 exemples). On génère un dataset synthétique dont les probabilités reproduisent fidèlement les taux observés sur le terrain.

Pourquoi un dataset synthétique ?

ℹ️Un modèle ML apprend des patterns statistiques. Avec seulement 30 exemples, il n'y a pas assez de données pour apprendre de manière fiable. On génère 300 participants en utilisant les probabilités réelles observées (vertige=80%, désorientation=88%...) comme paramètres du générateur. C'est une approche standard en recherche quand les données terrain sont limitées.

Code — Initialisation et démographie

Pythonimport numpy as np import pandas as pd np.random.seed(42) # Graine fixée : même résultat à chaque exécution (reproductibilité) N = 300 # 300 participants simulés # Âge : on crée 4 tranches séparées pour refléter la vraie distribution terrain age = np.concatenate([ np.random.randint(10, 18, 40), # 40 enfants / ados np.random.randint(18, 35, 130), # 130 jeunes adultes (majorité sur le terrain) np.random.randint(35, 55, 90), # 90 adultes np.random.randint(55, 80, 40), # 40 seniors ]) np.random.shuffle(age) # Mélange pour éviter tout ordre artificiel dans le CSV # Genre : 52/48 ratio observé sur le terrain genre = np.random.choice(['M', 'F'], N, p=[0.52, 0.48])
🔍 np.random.seed(42) — Reproductibilité

Sans seed, les nombres aléatoires changent à chaque exécution → résultats différents à chaque run. Avec seed(42), on fixe la séquence aléatoire. Le 42 est une convention (référence culturelle à The Hitchhiker's Guide to the Galaxy), mais n'importe quelle valeur fonctionne. Essentiel en recherche pour que d'autres puissent reproduire exactement tes résultats.

Code — Génération des scores de symptômes

Python# Fonction clé : génère un score de symptôme (0–10) basé sur les taux observés def gen_score(prob_base, sensible_age=True): scores = [] for i in range(N): p = prob_base if sensible_age and (age[i] < 15 or age[i] > 60): p = min(p + 0.15, 1.0) # Enfants et seniors : +15% de probabilité (vulnérabilité connue) if duree[i] > 10: p = min(p + 0.10, 1.0) # Longue exposition : +10% (fatigue cumulative) scores.append(np.random.binomial(10, p)) # Score entre 0 et 10 return np.array(scores) # Application avec les taux RÉELLEMENT observés sur le terrain vertige_score = gen_score(0.80) # 80/100 personnes sur le terrain desorientation_score = gen_score(0.88) # 88/100 personnes sur le terrain nausee_score = gen_score(0.30) # 30/100 personnes sur le terrain sueurs_score = gen_score(0.05, sensible_age=False) # Quasi-absent # Score SSQ global : pondération scientifique Kennedy (1993) ssq_score = ( vertige_score * 0.25 + # Symptôme dominant desorientation_score * 0.25 + # Symptôme dominant nausee_score * 0.20 + # Symptôme important maux_tete_score * 0.10 + fatigue_oculaire_score * 0.10 + sueurs_score * 0.05 + peur_panique * 5.00 # Événement critique = malus fort ).round(2)
🔍 np.random.binomial(10, p) — La distribution des scores

binomial(n=10, p) simule 10 "tirages" indépendants, chacun avec probabilité p de succès. Le résultat est le nombre de succès : un entier entre 0 et 10. Avec p=0.80 (vertige), on obtiendra en moyenne 8/10. Cette distribution est réaliste car les symptômes ont une intensité variable autour d'une moyenne — certains ressentent peu, d'autres beaucoup, même avec la même probabilité de base.

🔍 Section 2

Analyse Exploratoire (EDA)

Avant tout modèle ML, on explore les données visuellement. L'EDA permet de comprendre les distributions, détecter les anomalies, identifier les corrélations et formuler des hypothèses. Un expert ML passe 60 à 70% de son temps ici.

Distributions
📊

Figure 1 — Distributions des scores de symptômes (histogrammes + KDE)

Comment lire ce graphe

Chaque subplot montre la distribution d'un symptôme (0–10). Les barres bleues = histogramme (combien de participants ont chaque score). La courbe lissée = KDE (Kernel Density Estimation), une version continue de l'histogramme. La ligne rouge pointillée = moyenne du symptôme.

Un histogramme étalé vers la droite = symptôme fréquent et intense. Concentré à gauche = rare ou faible.

Ce qu'on observe et ce que ça signifie
  • Vertige (~8.5/10) et Désorientation (~9.2/10) : distributions très décalées à droite. Confirme tes observations terrain (80% et 88%). Ce sont les symptômes dominants de la cybersickness.
  • Nausée et Maux de tête (~3/10) : distribution plus centrée, symétrique. Présents mais moins intenses — cohérent avec tes 30%.
  • Sueurs (~0.5/10) : fortement concentré à 0. Quasi-absent, comme observé sur le terrain.
  • Fatigue oculaire (~4.5/10) : distribution intermédiaire — symptôme secondaire mais non négligeable.
💡La grande différence entre les symptômes (vertige vs sueurs) justifie la pondération différentielle dans le score SSQ. On ne peut pas traiter tous les symptômes de la même façon.
Corrélation
🔗

Figure 2 — Matrice de corrélation (heatmap)

Comment lire ce graphe

Chaque cellule montre la corrélation de Pearson entre deux variables, de -1 à +1. Rouge foncé = forte corrélation positive (les deux montent ensemble). Bleu = corrélation négative. Blanc = aucune relation linéaire. On masque le triangle supérieur car il est identique (symétrie).

La diagonale vaudrait 1.0 (chaque variable est corrélée à elle-même) — on l'ignore.

Analyse et interprétations clés
  • Vertige ↔ Désorientation : très fortement corrélés (~0.7–0.8). Ces deux symptômes apparaissent ensemble — ils partagent probablement le même mécanisme vestibulaire.
  • Symptômes ↔ ssq_score : tous positifs et significatifs. Le score SSQ capte bien l'ensemble des symptômes.
  • Âge : corrélation modérée avec la plupart des symptômes — les tranches extrêmes (seniors/enfants) sont plus touchées.
  • Durée exposition : corrélé positivement — plus on est exposé longtemps, plus les symptômes s'intensifient.
⚠️Des variables très corrélées entre elles = redondance d'information. C'est ce qui explique que la PCA peut réduire de 13 à 11 composantes sans perdre grand-chose.
SSQ par âge et genre
📦

Figure 3 — Score SSQ par tranche d'âge (boxplot) et par genre (violinplot)

Comment lire ces graphes

Boxplot (gauche) : La boîte centrale = 50% des participants (Q1 à Q3). La ligne centrale = médiane. Les moustaches = valeurs min/max hors outliers. Les points au-delà = valeurs aberrantes (outliers).

Violinplot (droite) : Combine boxplot + densité de distribution. Plus la forme est large à une hauteur, plus il y a de participants avec ce score.

Ce qu'on observe et pourquoi c'est important
  • Seniors (55–80 ans) : médiane SSQ plus haute et boîte plus large = plus touchés ET plus de variabilité. Cohérent avec la littérature (système vestibulaire fragilisé).
  • Enfants/ados : scores élevés également — susceptibilité neurologique liée au développement.
  • Jeunes adultes : scores généralement plus bas — tolérance VR plus haute.
  • Genre : distribution similaire M/F — le genre seul n'est pas un prédicteur fort de la cybersickness dans notre dataset.
💡Ces observations justifient l'inclusion de l'âge et de la tranche d'âge comme features dans nos modèles ML — ce sont des variables potentiellement prédictives.
⚙️ Section 3

Preprocessing — Préparer les données

Les algorithmes ML sont des formules mathématiques. Ils ne comprennent que des nombres normalisés. Cette étape transforme les données brutes en un format exploitable — c'est le fondement de tout pipeline ML.

Données brutes
Encodage
Standardisation
Train / Test Split
✅ ML Ready

Étape 1 — LabelEncoder : texte → nombres

Pythonfrom sklearn.preprocessing import LabelEncoder le_genre = LabelEncoder() # Crée un encodeur vide le_casque = LabelEncoder() # fit_transform : apprend les catégories (fit) ET encode (transform) en une étape df['genre_enc'] = le_genre.fit_transform(df['genre']) # 'F' → 0, 'M' → 1 (ordre alphabétique) df['casque_enc'] = le_casque.fit_transform(df['type_casque']) # 'HTC Vive'→0, 'Meta Quest 2'→1, 'Meta Quest 3'→2, 'PlayStation VR'→3, 'Valve Index'→4
🔍 Pourquoi encoder ? Et quelles limites ?

Un algorithme calcule des distances et des produits. Il ne peut pas faire "Meta Quest 2" × 0.5. LabelEncoder attribue un entier par catégorie. Limite importante : l'encodage crée un ordre artificiel (Valve Index = 4 > Meta Quest 2 = 1). Pour de vrais projets de production, on préférerait OneHotEncoder qui crée une colonne binaire par catégorie. Ici, c'est acceptable car les modèles ensemblistes (Random Forest, Gradient Boosting) sont robustes à cet artefact.

Étape 2 — StandardScaler : normalisation

Pythonfrom sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # Formule : X_new = (X - μ) / σ → résultat : μ=0, σ=1 pour chaque variable # Avant : age=[10–80], sueurs=[0–10] → échelles incompatibles # Après : age≈[-1.4 à +2.7], sueurs≈[-0.6 à +4.1] → même ordre de grandeur
⚠️Data Leakage — l'erreur classique : Il faut toujours fit() le scaler sur X_train uniquement, puis transform() sur train ET test séparément. Si on fit sur tout le dataset, la moyenne et l'écart-type calculés "voient" des données du test set, donnant un avantage artificiel au modèle lors de l'évaluation — c'est comme donner les réponses d'un examen à l'avance.

Étape 3 — Train / Test Split

Pythonfrom sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X_scaled, y_class, test_size=0.2, # 20% → 60 participants pour tester le modèle random_state=42, # Même split à chaque run stratify=y_class # Garantit les mêmes proportions dans train et test ) # Train: 240 participants | Test: 60 participants | Features: 13
🔍 Pourquoi stratify=y_class ?

Sans stratification, le split aléatoire pourrait mettre 90% des "sévère" dans le train set et seulement 10% dans le test — le modèle serait mal évalué sur les cas sévères. stratify=y_class garantit que la proportion faible/modéré/sévère est identique dans train et test, rendant l'évaluation équitable et représentative.

🤖 Section 4 — Supervisé

Classification — Prédire le niveau de cybersickness

Objectif : à partir des symptômes et du profil d'un participant, prédire automatiquement si son niveau de cybersickness est faible, modéré ou sévère. On compare 5 algorithmes différents.

Pourquoi 5 algorithmes ?

ℹ️Aucun algorithme n'est universellement meilleur. Chacun fait des hypothèses différentes sur la structure des données (linéarité, localité, ensembles...). Comparer plusieurs algorithmes sur les mêmes données permet d'identifier lequel correspond le mieux au problème. C'est la pratique standard en ML.
📐

Logistic Regression

Trouve une frontière linéaire (hyperplan) entre les classes. Calcule une probabilité par classe via la fonction sigmoïde. Très interprétable : les coefficients indiquent le poids de chaque feature.

🏆 96.7% — Meilleur
👥

KNN (k=5)

Prédit en cherchant les 5 participants les plus similaires dans les données d'entraînement et en votant à la majorité. Aucun paramètre appris — la "mémoire" est le dataset entier.

81.7%
🎯

SVM (kernel RBF)

Cherche la frontière qui maximise la marge entre les classes. Le kernel RBF projette les données dans un espace de dimension supérieure pour créer des frontières non-linéaires.

95.0%
🌲

Random Forest

Entraîne 100 arbres de décision sur des sous-échantillons aléatoires. Vote majoritaire final. Robuste, peu sensible au surapprentissage et donne la feature importance.

90.0%
📈

Gradient Boosting

Construit les arbres en séquence — chaque arbre se concentre sur les erreurs du précédent. Très performant mais plus lent à entraîner et plus sensible aux hyperparamètres.

93.3%

Code — Boucle d'entraînement et évaluation

Pythonfrom sklearn.metrics import accuracy_score, classification_report from sklearn.model_selection import cross_val_score for name, clf in classifiers.items(): # APPRENTISSAGE : le modèle voit (X_train, y_train) et ajuste ses paramètres clf.fit(X_train, yc_train) # PRÉDICTION : le modèle applique ce qu'il a appris sur des données jamais vues y_pred = clf.predict(X_test) # ACCURACY : % de bonnes prédictions sur le test set acc = accuracy_score(yc_test, y_pred) # CROSS-VALIDATION : répète 5 fois le split → score plus fiable et moins biaisé cv = cross_val_score(clf, X_scaled, y_class, cv=5) # cv est un tableau de 5 scores → on garde mean ± std
🔍 Cross-validation (CV) — Pourquoi c'est plus fiable qu'un seul split ?

Un seul train/test split peut être "chanceux" ou "malchanceux" selon comment les données tombent. La CV avec cv=5 découpe le dataset en 5 parts égales. Elle entraîne 5 fois : chaque fois une part différente est le test set et les 4 autres sont le train set. Score final = moyenne des 5 → beaucoup plus stable et représentatif de la vraie performance.

Classification results
📊

Figure 4 — Résultats Classification : Matrice de confusion, comparaison des modèles, feature importance

Matrice de confusion (gauche) — Lecture

Chaque ligne = classe réelle. Chaque colonne = classe prédite. La diagonale = bonnes prédictions. Hors-diagonale = erreurs (confusion entre classes).

Exemple : si la cellule (sévère, modéré) = 2, cela signifie que le modèle a classé 2 participants "réellement sévères" comme "modéré" — une erreur potentiellement importante en contexte clinique.

96.7%
Accuracy — Logistic Regression

Le modèle prédit correctement 58 participants sur 60 dans le test set. F1-score modéré = 0.97, F1-score sévère = 0.97. Excellent et cohérent avec l'accuracy globale.

Feature Importance (droite) — Analyse

Le Random Forest révèle quelles variables sont les plus déterminantes pour prédire le niveau de cybersickness :

  • vertige_score (15.8%) — Le symptôme le plus prédictif. Cohérent avec son taux terrain de 80%.
  • nausee_score (14.5%) — Deuxième variable clé, malgré son taux plus faible (30%). Son intensité discrimine fortement modéré vs sévère.
  • desorientation_score (13.2%) — Troisième variable. Très fréquente (88%) mais sa variabilité capte bien les différences.
  • Variables démographiques : age, durée, genre ont des importances plus faibles — ils influencent mais ne déterminent pas seuls le niveau.
💡La feature importance est une contribution scientifique directe : elle permet de dire "ces 3 symptômes sont les signaux d'alerte prioritaires à surveiller lors d'une session VR."
Comparaison des modèles (centre) — Pourquoi la Logistic Regression gagne ?

Le graphe central montre que la Logistic Regression surpasse tous les autres modèles, y compris les plus complexes (Random Forest, Gradient Boosting). Cela s'explique par la nature de nos données : la frontière entre faible/modéré/sévère est linéaire dans l'espace des features standardisées. Le score SSQ étant une combinaison linéaire des symptômes, les classes sont séparées par des hyperplans — exactement ce que la Logistic Regression modélise. Les modèles non-linéaires (SVM RBF, Random Forest) introduisent de la complexité inutile et surapprennnent légèrement. Leçon : la simplicité gagne quand le problème est intrinsèquement simple.

Résultats détaillés

🏆 BEST

Logistic Regression

96.7%
CV: 97.7% ± 2.5%

SVM (RBF)

95.0%
CV: 94.3% ± 2.5%

Gradient Boosting

93.3%
CV: 93.3% ± 3.0%

Random Forest

90.0%
CV: 91.3% ± 2.2%

KNN (k=5)

81.7%
CV: 87.0% ± 3.2%
📉 Section 5 — Supervisé

Régression — Prédire le score SSQ exact

Là où la classification prédit une catégorie, la régression prédit un nombre réel continu — ici le score SSQ (ex: 6.75). Même pipeline, objectif différent, métriques différentes.

Code — 4 régresseurs comparés

Pythonfrom sklearn.linear_model import LinearRegression, Ridge, Lasso from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error regressors = { 'Linear Regression': LinearRegression(), # Y = a1*vertige + a2*nausee + ... + b (combinaison linéaire) 'Ridge (L2)': Ridge(alpha=1.0), # Linéaire + pénalité L2 = force les coefficients à rester petits # Évite l'overfitting quand des features sont corrélées 'Lasso (L1)': Lasso(alpha=0.1), # Pénalité L1 = peut annuler certains coefficients à exactement 0 # → sélection automatique de features 'Random Forest Reg': RandomForestRegressor(n_estimators=100), # Capture les relations non-linéaires complexes } for name, reg in regressors.items(): reg.fit(X_train, yr_train) y_pred = reg.predict(X_test) r2 = r2_score(yr_test, y_pred) # % variance expliquée rmse = np.sqrt(mean_squared_error(yr_test, y_pred)) # Erreur en unité SSQ mae = mean_absolute_error(yr_test, y_pred) # Erreur absolue moyenne
Regression results
📉

Figure 5 — Résultats Régression : Prédit vs Réel et Graphe des Résidus

Graphe Prédit vs Réel (gauche) — Lecture

Chaque point = un participant du test set. L'axe X = score SSQ réel, l'axe Y = score SSQ prédit par le modèle. La ligne rouge pointillée = prédiction parfaite (prédit = réel).

Si le modèle était parfait, tous les points seraient sur cette diagonale. Plus les points s'en écartent, plus les erreurs sont grandes.

⚠️On observe que les points ne suivent pas du tout la diagonale — R² négatif (-0.02). Le modèle prédit des valeurs autour de la moyenne sans capturer la variabilité réelle.
Graphe des Résidus (droite) — Analyse

Résidu = valeur réelle − valeur prédite. Un bon modèle a des résidus :

  • Centrés sur 0 (pas de biais systématique)
  • Distribués aléatoirement (pas de pattern)
  • De variance constante

Explication scientifique du R² négatif : Le score SSQ est défini mathématiquement comme une combinaison linéaire des features (vertige×0.25 + désorientation×0.25 + ...). Il reste donc très peu de variance "cachée" et non-expliquée pour qu'un modèle externe l'apprenne. La régression n'apporte pas de valeur ajoutée ici — c'est un résultat attendu et scientifiquement honnête à rapporter.

MétriqueFormuleNotre résultatInterprétation
1 − SS_rés / SS_tot≈ −0.02Le modèle n'explique pas la variance — attendu car SSQ = f(features)
RMSE√ moy(y−ŷ)²1.304 pointsErreur moyenne de ±1.3 points de SSQ sur 60 participants
MAEmoy |y−ŷ|0.842 pointsEn moyenne, le modèle se trompe de 0.84 point SSQ
🔬 Section 6 — Non Supervisé

PCA — Voir l'invisible en 2D

On a 13 variables. Impossible de les visualiser simultanément. La PCA compresse ces 13 dimensions en quelques composantes qui capturent l'essentiel de l'information — sans aucun label.

Code — PCA complète et projection 2D

Pythonfrom sklearn.decomposition import PCA # Étape 1 : PCA complète — calcule toutes les composantes pca_full = PCA() # Sans n_components = garde tout pca_full.fit(X_scaled) # explained_variance_ratio_ : tableau du % de variance capté par chaque composante # ex: [0.12, 0.10, 0.09, 0.08, ...] → PC1 = 12%, PC2 = 10%... var_ind = pca_full.explained_variance_ratio_ # np.cumsum : somme cumulative → [0.12, 0.22, 0.31, 0.39, ...] var_cumul = np.cumsum(var_ind) # Trouver combien de composantes conservent 90% de l'information n_comp_90 = np.argmax(var_cumul >= 0.90) + 1 # Résultat : 11 # Étape 2 : Projection 2D pour visualisation pca_2d = PCA(n_components=2) # On force 2 composantes X_2d = pca_2d.fit_transform(X_scaled) # 300×13 → 300×2
PCA
🔬

Figure 6 — PCA : Scree Plot (variance) et Projection 2D colorée

Scree Plot (gauche) — Lecture et analyse

Barres bleues = variance individuelle de chaque composante principale. Courbe rouge = variance cumulée. Les lignes pointillées orange (90%) et rouge (95%) indiquent les seuils.

Résultat : 11 composantes sur 13 pour 90% de l'information.

Pas de "coude" très prononcé → les features contribuent de manière relativement équilibrée, sans qu'une ou deux dominent totalement. C'est cohérent avec la heatmap de corrélation qui montrait des corrélations modérées.

27.65%
Variance expliquée par PC1 + PC2 seulement

PC1 et PC2 ensemble ne capturent que 27.65% de l'information totale — c'est pourquoi la projection 2D montre des groupes peu séparés.

Projection 2D (droite) — Lecture et analyse

Chaque point = un participant projeté dans l'espace 2D (PC1, PC2). Coloré par niveau réel de cybersickness.

Observation : Les trois couleurs (vert=faible, orange=modéré, rouge=sévère) se mélangent considérablement dans l'espace PC1/PC2. Il n'y a pas de séparation nette et visuelle.

  • Cela confirme que 2D ne suffit pas pour bien séparer les classes
  • Les modèles supervisés travaillent dans les 13 dimensions originales — bien plus discriminantes
  • C'est pour ça que la classification à 96.7% est possible même si la projection 2D semble mélangée
💡La PCA ici est un outil d'exploration et de compréhension, pas un outil de prédiction. Elle montre que le problème nécessite plus de 2 dimensions pour être bien résolu — ce que nos classifieurs font.
🔵 Section 7 — Non Supervisé

K-Means — Découvrir des profils utilisateurs

Sans utiliser les labels, K-Means découvre automatiquement 4 groupes naturels dans les données. Résultat : 4 profils d'utilisateurs VR distincts, identifiés sans aucun a priori.

Code — Trouver le meilleur k et clustering final

Pythonfrom sklearn.cluster import KMeans from sklearn.metrics import silhouette_score # Tester k de 2 à 9 — on ne sait pas a priori combien de groupes existent for k in range(2, 10): km = KMeans(n_clusters=k, random_state=42, n_init=10) km.fit(X_scaled) # Inertie = somme des distances² entre points et leur centroïde # Plus k est grand, plus l'inertie est petite (mais risque d'overfitting) inertias.append(km.inertia_) # Silhouette : mesure cohérence interne des clusters (-1 à +1) # +1 = point bien dans son cluster, loin des autres # 0 = à la frontière, -1 = probablement dans le mauvais cluster silhouettes.append(silhouette_score(X_scaled, km.labels_)) best_k = 4 # k qui maximise le score silhouette km_final = KMeans(n_clusters=best_k, random_state=42, n_init=10) df['cluster'] = km_final.fit_predict(X_scaled)
K-Means
🔵

Figure 7 — K-Means : Elbow, Silhouette et Clusters projetés en PCA 2D

Méthode Elbow (gauche) — Lecture

L'axe Y = inertie (dispersion intra-cluster). Elle diminue toujours quand k augmente — plus on crée de groupes, plus ils sont petits et serrés. On cherche le "coude" où les gains deviennent marginaux.

Ici la courbe décroît de façon assez régulière sans coude très marqué → les données n'ont pas de structure en clusters parfaitement distincts. C'est cohérent avec un phénomène continu comme la cybersickness.

Score Silhouette (centre) et Clusters 2D (droite)

Le score silhouette k=4 (0.136) est le plus élevé — c'est le k optimal. Le score est relativement faible car les clusters se chevauchent : les profils de cybersickness sont des gradients continus, pas des groupes discretement séparés.

La projection 2D confirme : les 4 clusters se chevauchent en 2D. Rappel : les clusters sont définis dans les 13 dimensions originales — le chevauchement en 2D ne remet pas en cause leur validité.

Radar clusters
🕸️

Figure 8 — Radar Chart : Profil symptômes moyen par cluster

Comment lire un radar chart

Chaque axe représente un symptôme. La valeur sur l'axe = score moyen du cluster pour ce symptôme (0–10). Plus la surface colorée est grande, plus les symptômes sont intenses dans ce groupe.

Comparer les formes des clusters permet d'identifier les profils différentiels — quel groupe souffre de quoi spécifiquement.

Les 4 profils découverts et leur interprétation
  • Cluster 0 — "Profil à risque maximal" : Âge moyen 37 ans, SSQ 10.59, peur_panique = 1.0 (tous en panique). Ce groupe inclut tous les participants ayant vécu un événement de peur intense (scène de hauteur). Le malus SSQ de peur_panique×5 les pousse en sévère.
  • Cluster 1 — "Profil modéré stable" : Âge moyen 34 ans, SSQ 5.48, exposition courte (5.75 min), sans panique. Profil de tolérance standard — les utilisateurs VR occasionnels typiques.
  • Cluster 2 — "Jeunes longue exposition" : Âge moyen 24 ans, exposition longue (10.45 min), SSQ 6.63. Les jeunes tiennent plus longtemps mais finissent par développer des symptômes notables.
  • Cluster 3 — "Seniors sensibles" : Âge moyen 69 ans, vertige 9.81/10, désorientation 10/10. Profil à surveiller en priorité — tolérance VR très faible.

Profils détaillés par cluster

🔴 Cluster 0 — Profil à risque maximal

Âge moyen 37 ans · Panique systématique
Score SSQ10.59 / ~12 max
Vertige7.73 / 10
Désorientation8.93 / 10
Peur/panique100% du groupe
Durée exposition8.2 min

🔵 Cluster 1 — Profil modéré stable

Âge moyen 34 ans · Tolérance standard
Score SSQ5.48 / ~12 max
Vertige7.92 / 10
Désorientation8.72 / 10
Peur/panique0%
Durée exposition5.75 min

🟢 Cluster 2 — Jeunes longue exposition

Âge moyen 24 ans · Sessions longues
Score SSQ6.63 / ~12 max
Vertige9.16 / 10
Désorientation9.83 / 10
Peur/panique0%
Durée exposition10.45 min

🟡 Cluster 3 — Seniors sensibles

Âge moyen 69 ans · Vulnérabilité élevée
Score SSQ7.01 / ~12 max
Vertige9.81 / 10
Désorientation10.00 / 10
Peur/panique0%
Durée exposition9.29 min
💡Valeur pratique des clusters : Ces 4 profils permettent de recommander des protocoles VR adaptés. Pour le Cluster 3 (seniors), limiter la durée à 5 min et éviter les scènes de hauteur. Pour le Cluster 0, un screening préalable sur la sensibilité à la hauteur est indispensable. Ces recommandations sont directement actionnables pour les développeurs VR.
📊 Section 8

Synthèse — Résultats et conclusions

Vue d'ensemble de tous les résultats, leur signification scientifique et les conclusions qu'on peut tirer pour la prévention de la cybersickness en VR.

Tableau de bord complet

TechniqueMeilleur modèleScore cléConclusion
ClassificationLogistic Regression96.7% accuracyLa séparation faible/modéré/sévère est linéairement apprise. Modèle fiable.
RégressionRidge (L2)R² = −0.02Normal : SSQ = f(features). Pas de valeur ajoutée attendue ici.
PCA11/13 composantes90% info conservée. Légère redondance entre vertige/désorientation.
K-Meansk = 44 profils identifiésProfils actionnables : seniors, jeunes longue expo, modéré, panique.

Ce que ce projet démontre scientifiquement

La cybersickness est prédictible

96.7% de précision prouve que les symptômes rapportés permettent de classifier fiablement le niveau de cybersickness. Une application préventive est techniquement faisable.

🎯

3 symptômes sont prioritaires

La feature importance identifie vertige, nausée et désorientation comme les signaux d'alerte les plus discriminants — ces 3 variables suffisent à la plupart des décisions cliniques.

👥

4 profils à risque existent

Le clustering découvre des groupes naturels d'utilisateurs avec des profils de symptômes distincts. Ces profils orientent des recommandations concrètes pour les développeurs VR.

📊

La régression a des limites ici

Le R² négatif n'est pas un échec — c'est une observation valide : quand la cible est définie par les features, la régression externe n'apporte pas de gain prédictif.

💡Ce projet illustre l'ensemble du workflow ML professionnel : collecte terrain → génération de données → EDA → preprocessing → supervisé (classification + régression) → non supervisé (PCA + clustering) → interprétation scientifique.