Initial commit
This commit is contained in:
41
frontend/.gitignore
vendored
Normal file
41
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
31
frontend/Dockerfile
Normal file
31
frontend/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
# Stage 1: Builder
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Runner
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# Copy necessary files
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
36
frontend/README.md
Normal file
36
frontend/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
18
frontend/eslint.config.mjs
Normal file
18
frontend/eslint.config.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
6
frontend/next.config.mjs
Normal file
6
frontend/next.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
9671
frontend/package-lock.json
generated
Normal file
9671
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
frontend/package.json
Normal file
45
frontend/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.18",
|
||||
"apache-arrow": "^21.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"katex": "^0.16.11",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "16.1.1",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"react-markdown": "^9.0.1",
|
||||
"recharts": "^3.6.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"zundo": "^2.3.0",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.1",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
9
frontend/postcss.config.mjs
Normal file
9
frontend/postcss.config.mjs
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
552
frontend/public/docs/CORRELATION_GUIDE.md
Normal file
552
frontend/public/docs/CORRELATION_GUIDE.md
Normal file
@@ -0,0 +1,552 @@
|
||||
# Guide Complet de la Corrélation
|
||||
|
||||
📊 **Comprendre et maîtriser l'analyse de corrélation** pour identifier les relations entre variables et construire des modèles robustes.
|
||||
|
||||
---
|
||||
|
||||
## Table des Matières
|
||||
|
||||
1. [Qu'est-ce que la Corrélation ?](#quest-ce-que-la-corrélation)
|
||||
2. [Les Trois Méthodes](#les-trois-méthodes)
|
||||
3. [Interpréter la Matrice](#interpréter-la-matrice)
|
||||
4. [Multicolinéarité](#multicolinéarité)
|
||||
5. [P-Values et Significativité](#p-values-et-significativité)
|
||||
6. [Bonnes Pratiques](#bonnes-pratiques)
|
||||
7. [Exemples Concrets](#exemples-concrets)
|
||||
|
||||
---
|
||||
|
||||
## Qu'est-ce que la Corrélation ?
|
||||
|
||||
### Définition
|
||||
|
||||
La **corrélation** mesure la **force** et la **direction** de la relation linéaire entre deux variables quantitatives.
|
||||
|
||||
### Coefficient de Corrélation
|
||||
|
||||
Le coefficient de corrélation **r** varie entre **-1 et +1** :
|
||||
|
||||
```
|
||||
-1 ←----------|----------|----------|----------→ +1
|
||||
négatif positif
|
||||
fort fort
|
||||
```
|
||||
|
||||
### Interprétation Générale
|
||||
|
||||
| Valeur r | Force | Direction | Signification |
|
||||
|----------|-------|-----------|---------------|
|
||||
| **0.9 à 1.0** | Très forte | Positive | X↑ ⇒ Y↑ fortement |
|
||||
| **0.7 à 0.9** | Forte | Positive | X↑ ⇒ Y↑ |
|
||||
| **0.5 à 0.7** | Modérée | Positive | X↑ ⇒ Y↑ modérément |
|
||||
| **0.3 à 0.5** | Faible | Positive | X↑ ⇒ Y↑ légèrement |
|
||||
| **0.0 à 0.3** | Très faible | Positive | Relation quasi nulle |
|
||||
| **0.0** | Aucune | - | Indépendance linéaire |
|
||||
| **-0.3 à 0.0** | Très faible | Négative | X↑ ⇒ Y↓ légèrement |
|
||||
| **-0.5 à -0.3** | Faible | Négative | X↑ ⇒ Y↓ |
|
||||
| **-0.7 à -0.5** | Modérée | Négative | X↑ ⇒ Y↓ modérément |
|
||||
| **-0.9 à -0.7** | Forte | Négative | X↑ ⇒ Y↓ |
|
||||
| **-1.0 à -0.9** | Très forte | Négative | X↑ ⇒ Y↓ fortement |
|
||||
|
||||
### Exemples Visuels
|
||||
|
||||
#### Corrélation Positive Forte (r = 0.95)
|
||||
```
|
||||
┌─────┐
|
||||
│ Y │ •
|
||||
│ │ •
|
||||
│ │ •
|
||||
│ │ •
|
||||
│ │•
|
||||
└─────┴─────→ X
|
||||
```
|
||||
**Exemple** : Taille vs Poids (plus grand = plus lourd)
|
||||
|
||||
#### Corrélation Négative Forte (r = -0.90)
|
||||
```
|
||||
┌─────┐
|
||||
│ Y │•
|
||||
│ │ •
|
||||
│ │ •
|
||||
│ │ •
|
||||
│ │ •
|
||||
└─────┴─────→ X
|
||||
```
|
||||
**Exemple** : Vitesse vs Temps (plus vite = moins de temps)
|
||||
|
||||
#### Corrélation Nulle (r = 0.02)
|
||||
```
|
||||
┌─────┐
|
||||
│ Y │ • • •
|
||||
│ │• •
|
||||
│ │ • •
|
||||
│ │ • •
|
||||
│ │• •
|
||||
└─────┴─────→ X
|
||||
```
|
||||
**Exemple** : Taille vs Note en mathématiques
|
||||
|
||||
---
|
||||
|
||||
## Les Trois Méthodes
|
||||
|
||||
L'application propose **3 méthodes** de calcul de corrélation :
|
||||
|
||||
### 1. Pearson (Défaut)
|
||||
|
||||
**Quand l'utiliser ?**
|
||||
- ✅ Données **normalement distribuées**
|
||||
- ✅ Relation **linéaire**
|
||||
- ✅ Variables **continues**
|
||||
|
||||
**Hypothèses** :
|
||||
- Distribution normale (pas trop asymétrique)
|
||||
- Pas d'outliers extrêmes
|
||||
- Relation linéaire
|
||||
|
||||
**Formule** :
|
||||
```
|
||||
Σ[(Xi - X̄)(Yi - Ȳ)]
|
||||
r = ----------------------------
|
||||
√[Σ(Xi - X̄)²] √[Σ(Yi - Ȳ)²]
|
||||
```
|
||||
|
||||
**Exemple** :
|
||||
```python
|
||||
# Température vs Vent (relation linéaire, normale)
|
||||
# Corrélation de Pearson : r = 0.85 (forte positive)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Spearman
|
||||
|
||||
**Quand l'utiliser ?**
|
||||
- ✅ Données **non-paramétriques** (pas normales)
|
||||
- ✅ Relation **monotone** (pas forcément linéaire)
|
||||
- ✅ Présence d'**outliers**
|
||||
- ✅ Variables **ordinales** (rangs)
|
||||
|
||||
**Hypothèses** :
|
||||
- Relation monotone (toujours dans le même sens)
|
||||
- Moins sensible aux outliers
|
||||
|
||||
**Principe** :
|
||||
Convertit les données en **rangs** avant calcul.
|
||||
|
||||
**Exemple** :
|
||||
```python
|
||||
# Revenu vs Satisfaction (relation monotone mais non linéaire)
|
||||
# Revenu: [1000, 2000, 5000, 10000, 50000]
|
||||
# Rangs: [1, 2, 3, 4, 5]
|
||||
# Corrélation de Spearman : r = 0.90
|
||||
```
|
||||
|
||||
**Différence Pearson vs Spearman** :
|
||||
|
||||
```
|
||||
Pearson (linéaire) :
|
||||
•
|
||||
•
|
||||
•
|
||||
•
|
||||
•
|
||||
|
||||
Spearman (monotone) :
|
||||
•
|
||||
•
|
||||
•
|
||||
•
|
||||
•
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Kendall (Tau)
|
||||
|
||||
**Quand l'utiliser ?**
|
||||
- ✅ **Petits échantillons** (n < 30)
|
||||
- ✅ Beaucoup de **valeurs identiques** (ex-aequo)
|
||||
- ✅ Données **ordinales**
|
||||
|
||||
**Hypothèses** :
|
||||
- Mesure la concordance des paires
|
||||
|
||||
**Principe** :
|
||||
Compte les paires **concordantes** vs **discordantes**.
|
||||
|
||||
**Exemple** :
|
||||
```python
|
||||
# Classement vs Préférence (10 étudiants)
|
||||
# Kendall Tau = 0.67 (modéré)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interpréter la Matrice
|
||||
|
||||
### Structure de la Heatmap
|
||||
|
||||
```
|
||||
Var1 Var2 Var3 Var4
|
||||
┌───────┬───────┬───────┬───────┐
|
||||
Var1 │ 1.00 │ 0.85 │ -0.30 │ 0.15 │
|
||||
├───────┼───────┼───────┼───────┤
|
||||
Var2 │ 0.85 │ 1.00 │ -0.20 │ 0.45 │
|
||||
├───────┼───────┼───────┼───────┤
|
||||
Var3 │ -0.30 │ -0.20 │ 1.00 │ 0.92 │ ← Multicolinéarité
|
||||
├───────┼───────┼───────┼───────┤
|
||||
Var4 │ 0.15 │ 0.45 │ 0.92 │ 1.00 │
|
||||
└───────┴───────┴───────┴───────┘
|
||||
```
|
||||
|
||||
### Lecture de la Heatmap
|
||||
|
||||
#### Couleurs
|
||||
- **Rouge foncé** (r > 0.7) : Forte corrélation positive
|
||||
- **Rouge clair** (0.3 < r < 0.7) : Corrélation modérée/positive
|
||||
- **Gris/Blanc** (|r| < 0.3) : Corrélation faible/nulle
|
||||
- **Bleu clair** (-0.7 < r < -0.3) : Corrélation modérée/négative
|
||||
- **Bleu foncé** (r < -0.7) : Forte corrélation négative
|
||||
|
||||
#### Bordure Rouge ⚠️
|
||||
- Indique une **multicolinéarité problématique** (|r| ≥ 0.7)
|
||||
- Entre deux **prédicteurs** (variables X)
|
||||
- À éviter dans la régression
|
||||
|
||||
### Diagonale
|
||||
- Toujours égale à **1.00** (corrélation de la variable avec elle-même)
|
||||
- Non pertinente pour l'analyse
|
||||
|
||||
---
|
||||
|
||||
## Multicolinéarité
|
||||
|
||||
### Définition
|
||||
|
||||
La **multicolinéarité** se produit lorsque deux ou plusieurs prédicteurs sont **fortement corrélés** entre eux (|r| ≥ 0.7).
|
||||
|
||||
### Pourquoi c'est Problématique ?
|
||||
|
||||
❌ **Effets sur la Régression** :
|
||||
- Coefficients **instables** (varient beaucoup)
|
||||
- **p-values** peu fiables
|
||||
- Difficulté d'interpréter l'effet isolé de chaque variable
|
||||
- **R²** artificiellement élevé
|
||||
|
||||
### Exemple de Multicolinéarité
|
||||
|
||||
```python
|
||||
# Dataset : Prix immobiliers
|
||||
# Variables :
|
||||
# - Surface_m²
|
||||
# - Surface_pieds² (même variable, unité différente)
|
||||
# - Prix
|
||||
|
||||
# Corrélation Surface_m² vs Surface_pieds² : r = 1.0
|
||||
# ⚠️ BORDURE ROUGE sur la heatmap
|
||||
|
||||
# Problème dans la régression :
|
||||
# Prix = 50000 + 1000*Surface_m² - 200*Surface_pieds²
|
||||
# ^^^^^ coefficient positif ^^^^^^^^ coefficient négatif ???
|
||||
# absurde ! (les deux expliquent la même chose)
|
||||
```
|
||||
|
||||
### Détection
|
||||
|
||||
Dans l'application :
|
||||
1. **Ouvrez** la matrice de corrélation
|
||||
2. **Recherchez** les bordures rouges entre prédicteurs
|
||||
3. **Notez** les variables concernées
|
||||
|
||||
### Solution
|
||||
|
||||
**Option 1 : Supprimer une variable**
|
||||
```python
|
||||
# Avant : Surface_m² (r=1.0) vs Surface_pieds²
|
||||
# Solution : Garder Surface_m², supprimer Surface_pieds²
|
||||
```
|
||||
|
||||
**Option 2 : Combiner les variables**
|
||||
```python
|
||||
# Avant : Note_Maths, Note_Physique, Note_Chimie (toutes corrélées)
|
||||
# Solution : Créer "Note_Scientifique" = moyenne des trois
|
||||
```
|
||||
|
||||
**Option 3 : Analyse de corrélation partielle**
|
||||
Utiliser des techniques avancées (hors scope du guide)
|
||||
|
||||
### Checklist Avant Régression
|
||||
|
||||
Avant de lancer votre régression, vérifiez :
|
||||
|
||||
✅ **Aucune bordure rouge** entre les prédicteurs potentiels
|
||||
✅ **Si bordure rouge** : choisissez la variable la plus corrélée avec la cible
|
||||
✅ **Documentez** votre choix de retrait de variable
|
||||
|
||||
---
|
||||
|
||||
## P-Values et Significativité
|
||||
|
||||
### Qu'est-ce que la P-Value ?
|
||||
|
||||
La **p-value** mesure la **probabilité** que la corrélation observée soit due au hasard.
|
||||
|
||||
- **p-value < 0.05** : Corrélation **statistiquement significative** (fiable)
|
||||
- **p-value ≥ 0.05** : Corrélation **non significative** (pourrait être due au hasard)
|
||||
|
||||
### Interprétation
|
||||
|
||||
| Corrélation (r) | P-Value | Interprétation |
|
||||
|-----------------|---------|----------------|
|
||||
| 0.85 | p < 0.001 | ✅ Forte et significative |
|
||||
| 0.60 | p = 0.040 | ✅ Modérée et significative |
|
||||
| 0.75 | p = 0.080 | ⚠️ Forte mais NON significative (échantillon trop petit) |
|
||||
| 0.30 | p = 0.200 | ❌ Faible et non significative |
|
||||
|
||||
### Impact de la Taille d'Échantillon
|
||||
|
||||
```
|
||||
Pour r = 0.30 :
|
||||
|
||||
n = 10 → p = 0.40 (non significatif)
|
||||
n = 50 → p = 0.03 (significatif)
|
||||
n = 100 → p = 0.002 (très significatif)
|
||||
```
|
||||
|
||||
**Règle** : Plus l'échantillon est grand, plus les petites corrélations deviennent significatives.
|
||||
|
||||
### Filtre "Significatif Seulement"
|
||||
|
||||
Dans l'application :
|
||||
- **Cochez** "Significatif seulement (p < 0.05)"
|
||||
- La heatmap n'affiche que les corrélations **statistiquement fiables**
|
||||
- Utile pour filtrer le bruit
|
||||
|
||||
---
|
||||
|
||||
## Bonnes Pratiques
|
||||
|
||||
### 1. Vérifier les Hypothèses
|
||||
|
||||
**Pour Pearson** :
|
||||
```python
|
||||
# Test de normalité (Shapiro-Wilk)
|
||||
from scipy.stats import shapiro
|
||||
|
||||
stat, p = shapiro(data['variable'])
|
||||
if p > 0.05:
|
||||
print("Distribution normale → Pearson OK")
|
||||
else:
|
||||
print("Distribution non normale → Utiliser Spearman")
|
||||
```
|
||||
|
||||
**Vérification visuelle** :
|
||||
```python
|
||||
import matplotlib.pyplot as plt
|
||||
plt.hist(data['variable'])
|
||||
plt.show()
|
||||
# Forme de cloche = normale
|
||||
```
|
||||
|
||||
### 2. Traiter les Outliers
|
||||
|
||||
Les outliers faussent **fortement** la corrélation Pearson :
|
||||
|
||||
```python
|
||||
# Sans outlier : r = 0.95
|
||||
X = [1, 2, 3, 4, 5]
|
||||
Y = [2, 4, 6, 8, 10]
|
||||
|
||||
# Avec outlier : r = 0.40
|
||||
X = [1, 2, 3, 4, 100] # 100 est outlier
|
||||
Y = [2, 4, 6, 8, 10]
|
||||
|
||||
# Solution : utiliser Spearman (robuste aux outliers)
|
||||
# Ou supprimer l'outlier
|
||||
```
|
||||
|
||||
### 3. Corrélation ≠ Causalité
|
||||
|
||||
⚠️ **Attention** : Une corrélation forte n'implique PAS que X cause Y.
|
||||
|
||||
**Exemples classiques** :
|
||||
- 🍦 Ventes de glace vs 🦈 Attaques de requins
|
||||
- Corrélation : r = 0.90
|
||||
- Cause commune : 🌞 Été (chaleur)
|
||||
- Solution : Pas de causalité directe
|
||||
|
||||
- ☕ Café vs 💰 Revenu
|
||||
- Corrélation : r = 0.75
|
||||
- Explication : Les riches boivent plus de café (ou l'inverse ?)
|
||||
- Nécessite une expérience contrôlée
|
||||
|
||||
### 4. Corrélation de Spearman pour les Relations Non-Linéaires
|
||||
|
||||
```python
|
||||
# Relation exponentielle
|
||||
X = [1, 2, 3, 4, 5]
|
||||
Y = [1, 4, 9, 16, 25] # Y = X²
|
||||
|
||||
# Pearson : r = 0.97 (mais PAS linéaire !)
|
||||
# Spearman : r = 1.00 (monotone parfaite)
|
||||
```
|
||||
|
||||
### 5. Matrice de Corrélation Avant Régression
|
||||
|
||||
**Workflow recommandé** :
|
||||
|
||||
```
|
||||
1. Charger les données
|
||||
↓
|
||||
2. Matrice de corrélation
|
||||
↓
|
||||
3. Identifier les variables fortement corrélées avec la cible (|r| > 0.5)
|
||||
↓
|
||||
4. Repérer la multicolinéarité entre prédicteurs (bordure rouge)
|
||||
↓
|
||||
5. Sélectionner les prédicteurs finaux (pas de multicolinéarité)
|
||||
↓
|
||||
6. Régression
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exemples Concrets
|
||||
|
||||
### Exemple 1 : Immobilier
|
||||
|
||||
**Dataset** : Prix, Surface, Chambres, Jardin, Garage, Distance_Centre
|
||||
|
||||
**Matrice de corrélation** :
|
||||
|
||||
```
|
||||
Prix Surface Chambres Jardin Garage Distance
|
||||
Prix 1.00 0.85 0.65 0.70 0.55 -0.75
|
||||
Surface 0.85 1.00 0.72 0.60 0.50 -0.40
|
||||
Chambres 0.65 0.72 1.00 0.45 0.35 -0.25
|
||||
Jardin 0.70 0.60 0.45 1.00 0.30 -0.50
|
||||
Garage 0.55 0.50 0.35 0.30 1.00 -0.20
|
||||
Distance -0.75 -0.40 -0.25 -0.50 -0.20 1.00
|
||||
```
|
||||
|
||||
**Analyse** :
|
||||
1. **Variables corrélées avec le Prix** :
|
||||
- Surface : r = 0.85 ✅ (très forte)
|
||||
- Distance : r = -0.75 ✅ (forte négative)
|
||||
- Jardin : r = 0.70 ✅ (forte)
|
||||
- Chambres : r = 0.65 ✅ (modérée)
|
||||
|
||||
2. **Multicolinéarité** :
|
||||
- Surface vs Chambres : r = 0.72 ⚠️ (proche du seuil)
|
||||
- Si r > 0.7 : choisir une seule variable
|
||||
|
||||
3. **Sélection finale** :
|
||||
- X = Surface, Distance, Jardin
|
||||
- Éviter Chambres (multicolinéarité avec Surface)
|
||||
|
||||
---
|
||||
|
||||
### Exemple 2 : Santé
|
||||
|
||||
**Dataset** : IMC, Tension, Cholestérol, Âge, Sport, Calories
|
||||
|
||||
**Matrice (Spearman)** :
|
||||
|
||||
```
|
||||
IMC Tension Cholestérol Âge Sport Calories
|
||||
IMC 1.00 0.55 0.48 0.30 -0.60 0.70
|
||||
Tension 0.55 1.00 0.65 0.45 -0.40 0.50
|
||||
Cholestérol 0.48 0.65 1.00 0.60 -0.35 0.45
|
||||
Âge 0.30 0.45 0.60 1.00 -0.25 0.20
|
||||
Sport -0.60 -0.40 -0.35 -0.25 1.00 -0.50
|
||||
Calories 0.70 0.50 0.45 0.20 -0.50 1.00
|
||||
```
|
||||
|
||||
**Analyse** :
|
||||
- **Prédicteurs du Tension** : Cholestérol (0.65), IMC (0.55)
|
||||
- **Multicolinéarité** : IMC vs Calories (0.70) ⚠️
|
||||
- **Action** : Garder IMC (plus corrélé avec Tension), exclure Calories
|
||||
|
||||
---
|
||||
|
||||
### Exemple 3 : Marketing
|
||||
|
||||
**Dataset** : Ventes, Budget_TV, Budget_Radio, Budget_Online, Prix, Concurrence
|
||||
|
||||
**Matrice** :
|
||||
|
||||
```
|
||||
Ventes Budget_TV Budget_Radio Budget_Online Prix Concurrence
|
||||
Ventes 1.00 0.80 0.45 0.72 -0.60 -0.40
|
||||
Budget_TV 0.80 1.00 0.30 0.55 -0.20 -0.10
|
||||
Budget_Radio 0.45 0.30 1.00 0.35 -0.15 -0.05
|
||||
Budget_Online 0.72 0.55 0.35 1.00 -0.25 -0.15
|
||||
Prix -0.60 -0.20 -0.15 -0.25 1.00 0.20
|
||||
Concurrence -0.40 -0.10 -0.05 -0.15 0.20 1.00
|
||||
```
|
||||
|
||||
**Décisions** :
|
||||
1. **Meilleur canal** : TV (r = 0.80)
|
||||
2. **Deuxième** : Online (r = 0.72)
|
||||
3. **Radio** : Moins efficace (r = 0.45)
|
||||
4. **Pas de multicolinéarité** sévère entre canaux
|
||||
5. **Sélection** : Budget_TV, Budget_Online, Prix, Concurrence
|
||||
|
||||
---
|
||||
|
||||
## 📊 Fonctionnalités de l'Application
|
||||
|
||||
### Contrôles
|
||||
|
||||
1. **Sélection de la méthode**
|
||||
- Menu déroulant : Pearson / Spearman / Kendall
|
||||
- Défaut : Pearson
|
||||
|
||||
2. **Seuil minimum**
|
||||
- Filtre les corrélations faibles
|
||||
- Exemple : 0.5 n'affiche que |r| ≥ 0.5
|
||||
|
||||
3. **Filtre de significativité**
|
||||
- Cochez "Significatif seulement"
|
||||
- Garantit p < 0.05
|
||||
|
||||
4. **Export CSV**
|
||||
- Téléchargez la matrice complète
|
||||
- Inclut corrélations et p-values
|
||||
|
||||
### Composants Visuels
|
||||
|
||||
#### Cartes de Résumé
|
||||
- **Corrélations les plus fortes** : Top 5 par |r|
|
||||
- **Corrélations les plus faibles** : Bottom 5 non nulles
|
||||
|
||||
#### Tooltip (Survol)
|
||||
Cliquez sur une case pour voir :
|
||||
- Noms des variables
|
||||
- Coefficient de corrélation
|
||||
- P-value
|
||||
- Significativité (✅/❌)
|
||||
- Interprétation (Fort/Moyen/Faible)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Checklist d'Analyse
|
||||
|
||||
Avant de passer à la régression :
|
||||
|
||||
- [ ] **Méthode choisie** selon la distribution des données
|
||||
- [ ] **Corrélations avec la cible** identifiées (|r| > 0.5 souhaité)
|
||||
- [ ] **Multicolinéarité** vérifiée (pas de bordure rouge entre prédicteurs)
|
||||
- [ ] **P-values** significatives (p < 0.05)
|
||||
- [ ] **Outliers** traités (si Pearson)
|
||||
- [ ] **Variables sélectionnées** documentées
|
||||
- [ ] **Résultats exportés** pour référence future
|
||||
|
||||
---
|
||||
|
||||
**Version** : 1.0
|
||||
**Projet** : Application d'Analyse de Données
|
||||
**Auteur** : Documentation Utilisateur
|
||||
|
||||
🔗 **Voir aussi** : [Guide Régression](REGRESSION_GUIDE.md) | [Guide Outliers](OUTLIER_GUIDE.md)
|
||||
438
frontend/public/docs/LOCAL_SETUP_GUIDE.md
Normal file
438
frontend/public/docs/LOCAL_SETUP_GUIDE.md
Normal file
@@ -0,0 +1,438 @@
|
||||
# Local Development Setup Guide
|
||||
|
||||
This guide provides step-by-step instructions for setting up and running the Data Analysis Platform on your local machine.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Prerequisites Installation](#1-prerequisites-installation)
|
||||
2. [Backend Setup](#2-backend-setup)
|
||||
3. [Frontend Setup](#3-frontend-setup)
|
||||
4. [Running Both Services](#4-running-both-services)
|
||||
5. [Verification](#5-verification)
|
||||
6. [Common Issues and Solutions](#6-common-issues-and-solutions)
|
||||
7. [Development Tips](#7-development-tips)
|
||||
|
||||
---
|
||||
|
||||
## 1. Prerequisites Installation
|
||||
|
||||
### Verify Your Installation
|
||||
|
||||
Before starting, verify you have the required tools installed:
|
||||
|
||||
```bash
|
||||
# Check Python version (must be 3.12+)
|
||||
python3.12 --version
|
||||
|
||||
# Check Node.js version (must be 20+)
|
||||
node --version
|
||||
|
||||
# Check npm (comes with Node.js)
|
||||
npm --version
|
||||
|
||||
# Check UV (Python package manager)
|
||||
uv --version
|
||||
```
|
||||
|
||||
### Install Missing Prerequisites
|
||||
|
||||
#### Python 3.12
|
||||
|
||||
**macOS (using Homebrew):**
|
||||
```bash
|
||||
brew install python@3.12
|
||||
```
|
||||
|
||||
**Ubuntu/Debian:**
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install python3.12 python3.12-venv
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
Download from [python.org](https://www.python.org/downloads/)
|
||||
|
||||
#### Node.js 20+
|
||||
|
||||
**macOS (using Homebrew):**
|
||||
```bash
|
||||
brew install node
|
||||
```
|
||||
|
||||
**Ubuntu/Debian:**
|
||||
```bash
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt install -y nodejs
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
Download from [nodejs.org](https://nodejs.org/)
|
||||
|
||||
#### UV (Python Package Manager)
|
||||
|
||||
```bash
|
||||
pip install uv
|
||||
# or
|
||||
pipx install uv
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Backend Setup
|
||||
|
||||
### Step 1: Navigate to Backend Directory
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
```
|
||||
|
||||
### Step 2: Create Virtual Environment
|
||||
|
||||
A virtual environment isolates your project dependencies:
|
||||
|
||||
```bash
|
||||
# Create virtual environment
|
||||
python3.12 -m venv .venv
|
||||
|
||||
# Activate on macOS/Linux
|
||||
source .venv/bin/activate
|
||||
|
||||
# Activate on Windows
|
||||
.venv\Scripts\activate
|
||||
```
|
||||
|
||||
You should see `(.venv)` appear in your terminal prompt.
|
||||
|
||||
### Step 3: Install Dependencies with UV
|
||||
|
||||
UV is a fast Python package installer that resolves dependencies quickly:
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
This reads `pyproject.toml` and `uv.lock` to install exact versions of all dependencies.
|
||||
|
||||
### Step 4: Verify Installation
|
||||
|
||||
```bash
|
||||
python -c "import fastapi; import pandas; import pyarrow; print('All dependencies installed!')"
|
||||
```
|
||||
|
||||
### Step 5: Start the Backend Server
|
||||
|
||||
```bash
|
||||
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
**Command explained:**
|
||||
- `uvicorn` - ASGI server for FastAPI
|
||||
- `main:app` - Points to the FastAPI app instance in `main.py`
|
||||
- `--reload` - Auto-reload on code changes (development mode)
|
||||
- `--host 0.0.0.0` - Listen on all network interfaces
|
||||
- `--port 8000` - Use port 8000
|
||||
|
||||
You should see output like:
|
||||
```
|
||||
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
|
||||
INFO: Started reloader process [12345] using WatchFiles
|
||||
INFO: Started server process [12346]
|
||||
INFO: Waiting for application startup.
|
||||
INFO: Application startup complete.
|
||||
```
|
||||
|
||||
### Step 6: Access Backend Services
|
||||
|
||||
Open your browser and navigate to:
|
||||
|
||||
- **API Root:** http://localhost:8000
|
||||
- **Swagger UI (Interactive API Docs):** http://localhost:8000/docs
|
||||
- **ReDoc (Alternative API Docs):** http://localhost:8000/redoc
|
||||
|
||||
---
|
||||
|
||||
## 3. Frontend Setup
|
||||
|
||||
### Step 1: Open New Terminal
|
||||
|
||||
Keep the backend running in its terminal. Open a **new terminal** for the frontend.
|
||||
|
||||
### Step 2: Navigate to Frontend Directory
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
```
|
||||
|
||||
### Step 3: Install Dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
This downloads all packages listed in `package.json` and `package-lock.json`.
|
||||
|
||||
Expected output includes packages like:
|
||||
- `next` (16.1.1)
|
||||
- `react` (19.2.3)
|
||||
- `typescript` (latest)
|
||||
- `tailwindcss` (4+)
|
||||
- `apache-arrow` (21+)
|
||||
|
||||
### Step 4: Configure Environment (Optional)
|
||||
|
||||
Check if `.env.local` exists:
|
||||
|
||||
```bash
|
||||
ls -la .env.local
|
||||
```
|
||||
|
||||
If not present, create it from the example (if available):
|
||||
|
||||
```bash
|
||||
cp .env.local.example .env.local
|
||||
```
|
||||
|
||||
Edit `.env.local` to configure environment-specific settings like API endpoints.
|
||||
|
||||
### Step 5: Start the Frontend Development Server
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
You should see output like:
|
||||
```
|
||||
▲ Next.js 16.1.1
|
||||
- Local: http://localhost:3000
|
||||
- Network: http://192.168.1.X:3000
|
||||
|
||||
✓ Ready in 2.3s
|
||||
```
|
||||
|
||||
### Step 6: Access Frontend Application
|
||||
|
||||
Open your browser and navigate to:
|
||||
|
||||
**Frontend App:** http://localhost:3000
|
||||
|
||||
---
|
||||
|
||||
## 4. Running Both Services
|
||||
|
||||
For full-stack development, run both services simultaneously.
|
||||
|
||||
### Option 1: Two Terminals (Recommended for Development)
|
||||
|
||||
**Terminal 1 - Backend:**
|
||||
```bash
|
||||
cd backend
|
||||
source .venv/bin/activate # or .venv\Scripts\activate on Windows
|
||||
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
**Terminal 2 - Frontend:**
|
||||
```bash
|
||||
cd frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Option 2: Background Processes
|
||||
|
||||
**Backend:**
|
||||
```bash
|
||||
cd backend
|
||||
source .venv/bin/activate
|
||||
uvicorn main:app --reload --host 0.0.0.0 --port 8000 > ../backend.log 2>&1 &
|
||||
```
|
||||
|
||||
**Frontend:**
|
||||
```bash
|
||||
cd frontend
|
||||
npm run dev > ../frontend.log 2>&1 &
|
||||
```
|
||||
|
||||
View logs:
|
||||
```bash
|
||||
tail -f backend.log
|
||||
tail -f frontend.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Verification
|
||||
|
||||
### Test Backend Health
|
||||
|
||||
```bash
|
||||
curl http://localhost:8000/docs
|
||||
```
|
||||
|
||||
Or open http://localhost:8000/docs in your browser.
|
||||
|
||||
### Test Frontend
|
||||
|
||||
Open http://localhost:3000 in your browser.
|
||||
|
||||
### Check API Connectivity
|
||||
|
||||
From the frontend, you should be able to make requests to the backend at `http://localhost:8000`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Common Issues and Solutions
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
**Error:** `Address already in use`
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Find process using port 8000 (backend)
|
||||
lsof -i :8000
|
||||
# or
|
||||
netstat -tlnp | grep :8000
|
||||
|
||||
# Kill the process
|
||||
kill -9 <PID>
|
||||
|
||||
# Or use a different port
|
||||
uvicorn main:app --reload --port 8001
|
||||
```
|
||||
|
||||
### Virtual Environment Issues
|
||||
|
||||
**Error:** Python command not found or wrong Python version
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Deactivate current environment
|
||||
deactivate
|
||||
|
||||
# Remove and recreate virtual environment
|
||||
rm -rf .venv
|
||||
python3.12 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
uv sync
|
||||
```
|
||||
|
||||
### Module Import Errors
|
||||
|
||||
**Error:** `ModuleNotFoundError: No module named 'fastapi'`
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Ensure virtual environment is activated
|
||||
source .venv/bin/activate
|
||||
|
||||
# Reinstall dependencies
|
||||
uv sync
|
||||
```
|
||||
|
||||
### Frontend Build Errors
|
||||
|
||||
**Error:** `Cannot find module 'X'`
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Clear node_modules and reinstall
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
### CORS Errors (Frontend Cannot Access Backend)
|
||||
|
||||
**Error:** Browser console shows CORS policy errors
|
||||
|
||||
**Solution:**
|
||||
Ensure the backend has CORS middleware configured. Check `backend/main.py` for:
|
||||
|
||||
```python
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:3000"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Development Tips
|
||||
|
||||
### Hot Reload
|
||||
|
||||
- **Backend:** UVicorn's `--reload` flag automatically restarts the server when Python files change
|
||||
- **Frontend:** Next.js dev server automatically refreshes the browser when files change
|
||||
|
||||
### Useful Commands
|
||||
|
||||
**Backend:**
|
||||
```bash
|
||||
# Run tests
|
||||
pytest
|
||||
|
||||
# Run with verbose output
|
||||
uvicorn main:app --reload --log-level debug
|
||||
```
|
||||
|
||||
**Frontend:**
|
||||
```bash
|
||||
# Run tests (when configured)
|
||||
npm test
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Run production build
|
||||
npm start
|
||||
|
||||
# Lint code
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### IDE Recommendations
|
||||
|
||||
- **VS Code** with extensions:
|
||||
- Python (Microsoft)
|
||||
- Pylance
|
||||
- ESLint
|
||||
- Tailwind CSS IntelliSense
|
||||
- Thunder Client (for API testing)
|
||||
|
||||
### Debugging
|
||||
|
||||
**Backend:** Use VS Code's debugger with launch configuration:
|
||||
```json
|
||||
{
|
||||
"name": "Python: FastAPI",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "uvicorn",
|
||||
"args": ["main:app", "--reload", "--host", "0.0.0.0", "--port", "8000"],
|
||||
"cwd": "${workspaceFolder}/backend"
|
||||
}
|
||||
```
|
||||
|
||||
**Frontend:** Use Chrome DevTools or React DevTools browser extension.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
Once both services are running:
|
||||
|
||||
1. Explore the API documentation at http://localhost:8000/docs
|
||||
2. Interact with the frontend at http://localhost:3000
|
||||
3. Review the project context at `_bmad-output/project-context.md`
|
||||
4. Check planning artifacts in `_bmad-output/planning-artifacts/`
|
||||
|
||||
For Docker deployment instructions, see the main README.md (coming soon).
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2026-01-11
|
||||
916
frontend/public/docs/OUTLIER_GUIDE.md
Normal file
916
frontend/public/docs/OUTLIER_GUIDE.md
Normal file
@@ -0,0 +1,916 @@
|
||||
# Guide Complet des Outliers (Valeurs Aberrantes)
|
||||
|
||||
🔍 **Identifier, comprendre et gérer les outliers** pour garantir la qualité de vos analyses statistiques.
|
||||
|
||||
---
|
||||
|
||||
## Table des Matières
|
||||
|
||||
1. [Qu'est-ce qu'un Outlier ?](#quest-ce-quun-outlier)
|
||||
2. [Types d'Outliers](#types-doutliers)
|
||||
3. [Méthodes de Détection](#méthodes-de-détection)
|
||||
4. [Indicateurs Visuels](#indicateurs-visuels)
|
||||
5. [Processus de Gestion](#processus-de-gestion)
|
||||
6. [Impact sur les Analyses](#impact-sur-les-analyses)
|
||||
7. [Bonnes Pratiques](#bonnes-pratiques)
|
||||
8. [Exemples Concrets](#exemples-concrets)
|
||||
|
||||
---
|
||||
|
||||
## Qu'est-ce qu'un Outlier ?
|
||||
|
||||
### Définition
|
||||
|
||||
Un **outlier** (valeur aberrante) est une observation qui s'écarte **significativement** du reste des données.
|
||||
|
||||
### Caractéristiques
|
||||
|
||||
```
|
||||
Données normales :
|
||||
┌─┐
|
||||
┌─┤ ├─┐ ← Distribution normale
|
||||
│ │ │ │
|
||||
└─┤ ├─┘
|
||||
└─┘
|
||||
|
||||
Avec outliers :
|
||||
┌─┐
|
||||
┌─┤ ├─┐
|
||||
│ │ │ │ ● ← Outlier élevé
|
||||
└─┤ ├─┘ ●
|
||||
└─┘ ● ← Outlier faible
|
||||
```
|
||||
|
||||
### Pourquoi les Outliers Sont-Ils Importants ?
|
||||
|
||||
⚠️ **Ils peuvent fausser vos résultats** :
|
||||
|
||||
```python
|
||||
# Moyenne SANS outlier
|
||||
[10, 12, 11, 13, 12] → Moyenne = 11.6
|
||||
|
||||
# Moyenne AVEC outlier
|
||||
[10, 12, 11, 13, 12, 100] → Moyenne = 26.3
|
||||
|
||||
# Impact : La moyenne est plus que doublée !
|
||||
# Solution : Utiliser la médiane (robuste aux outliers)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Types d'Outliers
|
||||
|
||||
L'application détecte **2 types** d'outliers :
|
||||
|
||||
### 1. Outliers Univariés 🔴
|
||||
|
||||
**Définition** : Valeur extrême dans **une seule variable**.
|
||||
|
||||
**Détection** : Méthode **IQR** (Interquartile Range)
|
||||
|
||||
**Exemple** :
|
||||
```python
|
||||
# Surface des appartements (m²)
|
||||
[45, 50, 55, 60, 65, 70, 75, 500] # 500 est outlier
|
||||
|
||||
# Calcul :
|
||||
Q1 = 52.5
|
||||
Q3 = 71.25
|
||||
IQR = Q3 - Q1 = 18.75
|
||||
|
||||
# Limite supérieure
|
||||
Upper = Q3 + 1.5×IQR = 71.25 + 1.5×18.75 = 99.375
|
||||
|
||||
# 500 > 99.375 → OUTLIER 🔴
|
||||
```
|
||||
|
||||
**Indicateur visuel** : **Cercle rouge** sur la valeur dans la table
|
||||
|
||||
---
|
||||
|
||||
### 2. Outliers Multivariés 🟣
|
||||
|
||||
**Définition** : Combinaison **inhabituelle** de plusieurs variables.
|
||||
|
||||
**Détection** : Algorithme **Isolation Forest**
|
||||
|
||||
**Exemple** :
|
||||
```python
|
||||
# Client : Âge=18, Revenu=100000€, Historique=0
|
||||
|
||||
# Individuellement :
|
||||
# - Âge 18 : Normal
|
||||
# - Revenu 100000€ : Normal
|
||||
# - Historique 0 : Normal
|
||||
|
||||
# Mais COMBINÉ :
|
||||
# → Jeune avec très haut revenu et aucun historique
|
||||
# → SUSPECT ! (fraude potentielle, erreur de saisie, etc.)
|
||||
# → OUTLIER MULTIVARIÉ 🟣
|
||||
```
|
||||
|
||||
**Indicateur visuel** : **Cercle violet** sur la ligne dans la table
|
||||
|
||||
---
|
||||
|
||||
## Méthodes de Détection
|
||||
|
||||
### Méthode 1 : IQR (Interquartile Range)
|
||||
|
||||
#### Principe
|
||||
|
||||
L'IQR mesure la **dispersion centrale** des données (50% central).
|
||||
|
||||
```
|
||||
┌────┬──────┬──────┬──────┬────┐
|
||||
Min Q1 Médiane Q3 Max
|
||||
└─────┬─────┘
|
||||
IQR
|
||||
```
|
||||
|
||||
#### Formules
|
||||
|
||||
```python
|
||||
Q1 = 25ème percentile
|
||||
Q3 = 75ème percentile
|
||||
IQR = Q3 - Q1
|
||||
|
||||
Limite inférieure = Q1 - 1.5 × IQR
|
||||
Limite supérieure = Q3 + 1.5 × IQR
|
||||
|
||||
# Outlier si :
|
||||
valeur < Limite inférieure
|
||||
OU
|
||||
valeur > Limite supérieure
|
||||
```
|
||||
|
||||
#### Exemple Pas à Pas
|
||||
|
||||
```python
|
||||
# Données : Prix immobiliers
|
||||
Prix = [150k, 180k, 200k, 220k, 250k, 280k, 300k, 1500k]
|
||||
|
||||
# Étape 1 : Trier
|
||||
[150, 180, 200, 220, 250, 280, 300, 1500]
|
||||
|
||||
# Étape 2 : Calculer Q1 et Q3
|
||||
Q1 = 190 (moyenne de 180 et 200)
|
||||
Q3 = 290 (moyenne de 280 et 300)
|
||||
|
||||
# Étape 3 : Calculer IQR
|
||||
IQR = 290 - 190 = 100
|
||||
|
||||
# Étape 4 : Calculer les limites
|
||||
Limite_inf = 190 - 1.5×100 = 40
|
||||
Limite_sup = 290 + 1.5×100 = 440
|
||||
|
||||
# Étape 5 : Identifier les outliers
|
||||
150k > 40 ✅ Normal
|
||||
180k > 40 ✅ Normal
|
||||
...
|
||||
300k < 440 ✅ Normal
|
||||
1500k > 440 ❌ OUTLIER 🔴
|
||||
```
|
||||
|
||||
#### Avantages et Limites
|
||||
|
||||
✅ **Avantages** :
|
||||
- Simple à comprendre
|
||||
- Robuste aux outliers (utilise des rangs)
|
||||
- Fonctionne avec des données non normales
|
||||
|
||||
❌ **Limites** :
|
||||
- Ne détecte que les extrêmes univariés
|
||||
- Peut manquer des outliers multivariés
|
||||
- Seuil 1.5 arbitraire (peut être ajusté)
|
||||
|
||||
---
|
||||
|
||||
### Méthode 2 : Isolation Forest
|
||||
|
||||
#### Principe
|
||||
|
||||
L'algorithme **isole** les observations en créant des partitions aléatoires. Les outliers sont **plus faciles à isoler** (moins de partitions nécessaires).
|
||||
|
||||
```
|
||||
Forêt aléatoire d'arbres de décision :
|
||||
- Chaque arbre partitionne les données
|
||||
- Les outliers ont des chemins plus courts
|
||||
- Score d'anomalie basé sur la longueur moyenne des chemins
|
||||
```
|
||||
|
||||
#### Algorithme
|
||||
|
||||
```python
|
||||
from sklearn.ensemble import IsolationForest
|
||||
|
||||
# 1. Entraîner le modèle
|
||||
model = IsolationForest(
|
||||
contamination='auto', # Détection automatique du seuil
|
||||
random_state=42
|
||||
)
|
||||
|
||||
# 2. Prédire
|
||||
predictions = model.fit_predict(data_multivariée)
|
||||
|
||||
# Résultats :
|
||||
# 1 → Normal
|
||||
# -1 → Outlier 🟣
|
||||
```
|
||||
|
||||
#### Exemple Visuel
|
||||
|
||||
```
|
||||
Espace 2D (Âge, Revenu) :
|
||||
|
||||
Revenu
|
||||
│
|
||||
│ •
|
||||
│ • •
|
||||
│ • •
|
||||
│ • ● • ← ● = Outlier multivarié
|
||||
│ • • • (combinaison rare)
|
||||
│• •
|
||||
└──────────────→ Âge
|
||||
```
|
||||
|
||||
#### Avantages et Limites
|
||||
|
||||
✅ **Avantages** :
|
||||
- Détecte les outliers **multivariés**
|
||||
- Ne nécessite pas de distribution normale
|
||||
- Fonctionne avec des données de haute dimension
|
||||
|
||||
❌ **Limites** :
|
||||
- Plus complexe à interpréter
|
||||
- Résultats non déterministes (aléatoire)
|
||||
- Peut varier selon le paramètre `contamination`
|
||||
|
||||
---
|
||||
|
||||
### Comparaison des Méthodes
|
||||
|
||||
| Critère | IQR | Isolation Forest |
|
||||
|---------|-----|------------------|
|
||||
| **Type** | Univarié | Multivarié |
|
||||
| **Détection** | Valeurs extrêmes | Combinaisons rares |
|
||||
| **Complexité** | Simple | Complexe |
|
||||
| **Interprétabilité** | Élevée | Faible |
|
||||
| **Robustesse** | Moyenne | Élevée |
|
||||
| **Utilisation** | Première analyse | Analyse approfondie |
|
||||
|
||||
---
|
||||
|
||||
## Indicateurs Visuels
|
||||
|
||||
### Dans l'Application
|
||||
|
||||
#### 🔴 Cercle Rouge (Outlier Univarié)
|
||||
|
||||
**Affichage** : Sur la **cellule** spécifique
|
||||
|
||||
**Exemple** :
|
||||
```
|
||||
┌─────────────┬─────────────┬─────────────┐
|
||||
│ Surface │ Prix │ Chambres │
|
||||
├─────────────┼─────────────┼─────────────┤
|
||||
│ 60 │ 250000 │ 3 │
|
||||
│ 75 │ 300000 │ 4 │
|
||||
│ 500 🔴 │ 350000 │ 5 │ ← Outlier sur Surface
|
||||
│ 55 │ 280000 │ 3 │
|
||||
└─────────────┴─────────────┴─────────────┘
|
||||
```
|
||||
|
||||
**Information au survol** :
|
||||
```
|
||||
Column 'Surface' value 500 is outside IQR bounds [32.5, 115.5]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 🟣 Cercle Violet (Outlier Multivarié)
|
||||
|
||||
**Affichage** : Sur toute la **ligne**
|
||||
|
||||
**Exemple** :
|
||||
```
|
||||
┌─────────────┬─────────────┬─────────────┐
|
||||
│ Âge │ Revenu │ Historique │
|
||||
├─────────────┼─────────────┼─────────────┤
|
||||
│ 35 │ 3000 │ 12 │
|
||||
│ 42 │ 4500 │ 8 │
|
||||
│ 18 🟣 │ 100000 │ 0 🟣 │ ← Outlier multivarié
|
||||
│ 28 │ 2500 │ 5 │
|
||||
└─────────────┴─────────────┴─────────────┘
|
||||
```
|
||||
|
||||
**Information au survol** :
|
||||
```
|
||||
Multivariate anomaly detected by Isolation Forest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 🔴🟣 Les Deux (Outlier Univarié + Multivarié)
|
||||
|
||||
**Exemple** :
|
||||
```
|
||||
┌─────────────┬─────────────┬─────────────┐
|
||||
│ Surface │ Prix │ Chambres │
|
||||
├─────────────┼─────────────┼─────────────┤
|
||||
│ 500 🔴🟣 │ 5000000 🔴 │ 10 🟣 │
|
||||
└─────────────┴─────────────┴─────────────┘
|
||||
```
|
||||
|
||||
**Interprétation** :
|
||||
- 🔴 Surface et Prix sont des valeurs extrêmes
|
||||
- 🟣 La combinaison Surface+Prix+Chambres est anormale
|
||||
|
||||
---
|
||||
|
||||
## Processus de Gestion
|
||||
|
||||
### Workflow Recommandé
|
||||
|
||||
```
|
||||
1. Détection automatique
|
||||
├─ IQR (univarié)
|
||||
└─ Isolation Forest (multivarié)
|
||||
|
||||
2. Investigation manuelle
|
||||
├─ Survoler les indicateurs colorés
|
||||
├─ Lire les raisons détaillées
|
||||
└─ Vérifier la source de données
|
||||
|
||||
3. Décision
|
||||
├─ Conserver (valeurs légitimes)
|
||||
├─ Corriger (erreurs de saisie)
|
||||
└─ Exclure (vraies anomalies)
|
||||
|
||||
4. Ré-analyse
|
||||
├─ Réexécuter la détection
|
||||
├─ Vérifier l'impact sur les résultats
|
||||
└─ Documenter les décisions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Étape 1 : Identification
|
||||
|
||||
**Dans l'application** :
|
||||
1. Importez vos données
|
||||
2. Les outliers sont automatiquement détectés
|
||||
3. Repérez les cercles 🔴 et 🟣
|
||||
|
||||
**Règle de priorité** :
|
||||
```
|
||||
🔴 Univarié → Vérifier en premier (simple)
|
||||
🟣 Multivarié → Vérifier ensuite (complexe)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Étape 2 : Investigation
|
||||
|
||||
#### Questions à se poser
|
||||
|
||||
**1. Est-ce une erreur de saisie ?**
|
||||
```python
|
||||
# Exemple : Âge = 250
|
||||
# → Probablement une erreur (tapez 25 au lieu de 250)
|
||||
# → Vérifier la source de données
|
||||
# → Corriger si possible
|
||||
```
|
||||
|
||||
**2. Est-ce une valeur légitime ?**
|
||||
```python
|
||||
# Exemple : Revenu = 1000000€ pour un CEO
|
||||
# → Valeur extrême mais réelle
|
||||
# → CONSERVER (pas d'erreur)
|
||||
# → Vérifier l'impact sur l'analyse
|
||||
```
|
||||
|
||||
**3. Est-ce un événement rare ?**
|
||||
```python
|
||||
# Exemple : Pic de ventes le Black Friday
|
||||
# → Valeur extrême mais explicable
|
||||
# → CONSERVER ou créer une variable "Black Friday"
|
||||
# → Modéliser séparément si nécessaire
|
||||
```
|
||||
|
||||
**4. Est-ce une mesure défaillante ?**
|
||||
```python
|
||||
# Exemple : Température = -50°C en été
|
||||
# → Erreur de capteur
|
||||
# → EXCLURE ou CORRIGER
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Étape 3 : Action
|
||||
|
||||
#### Option 1 : Corriger
|
||||
|
||||
**Quand** : Erreur de saisie identifiée
|
||||
|
||||
**Exemple** :
|
||||
```python
|
||||
# Avant (erreur)
|
||||
Prix = 15000000 # 15 millions pour un 60 m² ??
|
||||
|
||||
# Après (correction)
|
||||
Prix = 150000 # 150k€ → réaliste
|
||||
|
||||
# Action : Modifier la valeur dans le fichier source
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Option 2 : Conserver
|
||||
|
||||
**Quand** : Valeur légitime mais extrême
|
||||
|
||||
**Stratégies** :
|
||||
|
||||
**A. Transformation**
|
||||
```python
|
||||
# Appliquer une transformation log
|
||||
prix_log = log(Prix)
|
||||
|
||||
# Avantages :
|
||||
# - Réduit l'impact des extrêmes
|
||||
# - Normalise la distribution
|
||||
```
|
||||
|
||||
**B. Modèle robuste**
|
||||
```python
|
||||
# Utiliser des méthodes robustes aux outliers
|
||||
# - Spearman au lieu de Pearson
|
||||
# - Médiane au lieu de moyenne
|
||||
# - Isolation Forest pour la détection
|
||||
```
|
||||
|
||||
**C. Analyse séparée**
|
||||
```python
|
||||
# Créer deux modèles
|
||||
# - Modèle 1 : Données normales
|
||||
# - Modèle 2 : Outliers (cas particuliers)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Option 3 : Exclure
|
||||
|
||||
**Quand** :
|
||||
- Erreur non corrigeable
|
||||
- Valeur influente et non représentative
|
||||
- Mesure défaillante
|
||||
|
||||
**Dans l'application** :
|
||||
1. Cliquez sur l'outlier (🔴 ou 🟣)
|
||||
2. La ligne est marquée pour exclusion
|
||||
3. Les prochaines analyses ignoreront cette ligne
|
||||
4. Les exclusions sont mémorisées
|
||||
|
||||
**⚠️ Précautions** :
|
||||
- Documentez la raison de l'exclusion
|
||||
- Vérifiez l'impact sur la taille de l'échantillon
|
||||
- Assurez-vous que l'exclusion est justifiée
|
||||
|
||||
---
|
||||
|
||||
### Étape 4 : Vérification
|
||||
|
||||
#### Avant vs Après
|
||||
|
||||
```python
|
||||
# AVANT (avec outliers)
|
||||
Moyenne = 26.3
|
||||
Écart-type = 38.5
|
||||
Corrélation = 0.45
|
||||
|
||||
# APRÈS (sans outliers)
|
||||
Moyenne = 11.6
|
||||
Écart-type = 1.1
|
||||
Corrélation = 0.92
|
||||
|
||||
# Impact : L'analyse est plus représentative
|
||||
```
|
||||
|
||||
#### Checklist de validation
|
||||
|
||||
- [ ] Les exclusions sont **justifiées** et documentées
|
||||
- [ ] L'échantillon reste **suffisamment grand** (n > 30)
|
||||
- [ ] Les résultats sont **plus cohérents**
|
||||
- [ ] Pas de **sur-correction** (exclure trop de données)
|
||||
- [ ] Les décisions sont **reproductibles**
|
||||
|
||||
---
|
||||
|
||||
## Impact sur les Analyses
|
||||
|
||||
### 1. Moyenne vs Médiane
|
||||
|
||||
```python
|
||||
# Données avec outlier
|
||||
[10, 12, 11, 13, 12, 100]
|
||||
|
||||
Moyenne = 26.3 📊 Biaisée par l'outlier
|
||||
Médiane = 12 📊 Robuste
|
||||
|
||||
# Règle : Utiliser la médiane en présence d'outliers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Corrélation Pearson vs Spearman
|
||||
|
||||
```python
|
||||
# Avec outlier (Pearson)
|
||||
r_pearson = 0.60 📊 Faible (outlier distord la relation)
|
||||
|
||||
# Avec outlier (Spearman)
|
||||
r_spearman = 0.90 📊 Fort (robuste aux outliers)
|
||||
|
||||
# Sans outlier (Pearson)
|
||||
r_pearson = 0.92 📊 Fort (relation réelle)
|
||||
|
||||
# Règle : Utiliser Spearman si outliers présents
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Régression
|
||||
|
||||
#### Impact sur les Coefficients
|
||||
|
||||
```python
|
||||
# RÉGRESSION SANS OUTLIER
|
||||
Prix = 50000 + 2500×Surface
|
||||
R² = 0.90
|
||||
|
||||
# RÉGRESSION AVEC OUTLIER
|
||||
Prix = 80000 + 1000×Surface
|
||||
R² = 0.60
|
||||
|
||||
# Impact :
|
||||
# - Intercept : 50000 → 80000 (biaisé)
|
||||
# - Surface : 2500 → 1000 (sous-estimé)
|
||||
# - R² : 0.90 → 0.60 (moins précis)
|
||||
```
|
||||
|
||||
#### Influence (Leverage)
|
||||
|
||||
Certains outliers ont un **impact disproportionné** :
|
||||
|
||||
```python
|
||||
# Outlier avec fort leverage
|
||||
Point : (Surface=1000, Prix=1000000)
|
||||
|
||||
# Si exclu :
|
||||
# Coefficient Surface : 2500 → 2800 (+12%)
|
||||
# L'outlier "tire" la droite vers lui
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Tests Statistiques
|
||||
|
||||
```python
|
||||
# Test t (comparaison de moyennes)
|
||||
|
||||
# SANS outlier
|
||||
Groupe A : [10, 12, 11, 13, 12]
|
||||
Groupe B : [15, 17, 16, 18, 17]
|
||||
p-value = 0.0001 ✅ Différence significative
|
||||
|
||||
# AVEC outlier dans Groupe A
|
||||
Groupe A : [10, 12, 11, 13, 12, 100] # outlier ajouté
|
||||
Groupe B : [15, 17, 16, 18, 17]
|
||||
p-value = 0.15 ❌ Différence non significative
|
||||
|
||||
# Impact : L'outlier masque la différence réelle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bonnes Pratiques
|
||||
|
||||
### 1. Toujours Visualiser
|
||||
|
||||
```python
|
||||
# Box plot pour détecter visuellement
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
plt.boxplot(data['Prix'])
|
||||
plt.show()
|
||||
|
||||
# Box plot avec outliers marqués
|
||||
# ┌─┐
|
||||
# │ │ ● ← Outlier
|
||||
# ├─┤
|
||||
# │ │
|
||||
# └─┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Ne Pas Exclure Automatiquement
|
||||
|
||||
```python
|
||||
❌ MAUVAIS
|
||||
if is_outlier(valeur):
|
||||
exclure(valeur) # Automatique
|
||||
|
||||
✅ BON
|
||||
if is_outlier(valeur):
|
||||
investiguer(valeur) # Manuel
|
||||
si erreur:
|
||||
corriger_ou_exclure()
|
||||
sinon:
|
||||
conserver()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Documenter les Décisions
|
||||
|
||||
```python
|
||||
# Journal des exclusions
|
||||
exclusions = {
|
||||
'ligne_42': {
|
||||
'raison': 'Erreur de saisie (âge=250)',
|
||||
'action': 'Corrigé en 25',
|
||||
'date': '2025-01-11'
|
||||
},
|
||||
'ligne_87': {
|
||||
'raison': 'Capteur défaillant (température=-50)',
|
||||
'action': 'Exclu',
|
||||
'date': '2025-01-11'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Comparer Avec/Sans
|
||||
|
||||
```python
|
||||
# Analyse 1 : Tous les données
|
||||
model1 = regression(data_complete)
|
||||
r2_1 = 0.75
|
||||
|
||||
# Analyse 2 : Sans outliers
|
||||
model2 = regression(data_sans_outliers)
|
||||
r2_2 = 0.92
|
||||
|
||||
# Conclusion : Les outliers impactent significativement
|
||||
# → Exclusion justifiée si R² améliore et taille échantillon OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Utiliser des Méthodes Robustes
|
||||
|
||||
```python
|
||||
# Méthodes robustes aux outliers
|
||||
|
||||
Moyenne → Médiane
|
||||
Écart-type → Écart interquartile (IQR)
|
||||
Pearson → Spearman
|
||||
OLS → Régression robuste (Huber, RANSAC)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exemples Concrets
|
||||
|
||||
### Exemple 1 : Immobilier
|
||||
|
||||
**Données** : Prix, Surface, Chambres
|
||||
|
||||
```
|
||||
┌───────┬─────────┬──────────┬──────────┐
|
||||
│ ID │ Surface │ Prix │ Chambres │
|
||||
├───────┼─────────┼──────────┼──────────┤
|
||||
│ 1 │ 60 │ 250000 │ 3 │
|
||||
│ 2 │ 75 │ 300000 │ 4 │
|
||||
│ 3 │ 500 🔴 │ 350000 │ 5 │ ← Outlier univarié
|
||||
│ 4 │ 55 │ 280000 │ 3 │
|
||||
│ 5 │ 80 │ 320000 │ 4 │
|
||||
│ 6 │ 70 │ 15000000 🔴│ 3 │ ← Outlier univarié
|
||||
│ 7 │ 65 │ 290000 │ 3 │
|
||||
└───────┴─────────┴──────────┴──────────┘
|
||||
```
|
||||
|
||||
**Analyse** :
|
||||
- **Ligne 3** : Surface = 500 m² → Outlier (trop grand pour l'échantillon)
|
||||
- **Ligne 6** : Prix = 15M€ → Outlier (prix extrême)
|
||||
|
||||
**Action** :
|
||||
```
|
||||
Investigation :
|
||||
- Ligne 3 : Vérifié → Château luxueux (légitime)
|
||||
- Ligne 6 : Vérifié → Erreur de saisie (15M au lieu de 150k)
|
||||
|
||||
Décision :
|
||||
- Ligne 3 : CONSERVER (valeur réelle)
|
||||
- Ligne 6 : CORRIGER (150k€)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Exemple 2 : E-commerce
|
||||
|
||||
**Données** : Âge, Revenu, Montant_Achat
|
||||
|
||||
```
|
||||
┌───────┬──────┬─────────┬──────────────┐
|
||||
│ ID │ Âge │ Revenu │ Montant_Achat │
|
||||
├───────┼──────┼─────────┼──────────────┤
|
||||
│ 1 │ 35 │ 3000 │ 150 │
|
||||
│ 2 │ 42 │ 4500 │ 200 │
|
||||
│ 3 │ 28 │ 2500 │ 100 │
|
||||
│ 4 │ 18 🟣│ 100000 🟣│ 0 🟣 │ ← Outlier multivarié
|
||||
│ 5 │ 55 │ 5000 │ 250 │
|
||||
└───────┴──────┴─────────┴──────────────┘
|
||||
```
|
||||
|
||||
**Analyse** :
|
||||
- **Ligne 4** :
|
||||
- Âge = 18 (normal)
|
||||
- Revenu = 100k€ (normal)
|
||||
- Montant = 0 (normal)
|
||||
- **MAIS** : Jeune avec très haut revenu et aucun achat → SUSPECT
|
||||
|
||||
**Investigation** :
|
||||
```
|
||||
Cas 1 : Erreur de saisie (revenu=10000€)
|
||||
Cas 2 : Compte frauduleux
|
||||
Cas 3 : Héritage récent (légitime)
|
||||
Cas 4 : Erreur de catégorisation (professionnel vs personnel)
|
||||
```
|
||||
|
||||
**Action** :
|
||||
```
|
||||
Si erreur → Corriger
|
||||
Si fraude → Exclure et signaler
|
||||
Si légitime → Créer une variable "Profil_Riche_Jeune"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Exemple 3 : Santé
|
||||
|
||||
**Données** : IMC, Tension, Cholestérol
|
||||
|
||||
```
|
||||
┌───────┬──────┬─────────┬────────────┐
|
||||
│ ID │ IMC │ Tension │ Cholestérol│
|
||||
├───────┼──────┼─────────┼────────────┤
|
||||
│ 1 │ 22 │ 120 │ 180 │
|
||||
│ 2 │ 25 │ 130 │ 200 │
|
||||
│ 3 │ 60 🔴│ 180 🔴 │ 350 🔴 │ ← Outlier univarié
|
||||
│ 4 │ 23 │ 125 │ 190 │
|
||||
│ 5 │ 28 │ 140 │ 220 │
|
||||
└───────┴──────┴─────────┴────────────┘
|
||||
```
|
||||
|
||||
**Analyse** :
|
||||
- **Ligne 3** : IMC=60 (obésité morbide), Tension=180 (hypertension sévère), Cholestérol=350 (très élevé)
|
||||
|
||||
**Investigation** :
|
||||
```
|
||||
Cas 1 : Patient réel (pathologie sévère)
|
||||
Cas 2 : Erreur de mesure (unités ?)
|
||||
Cas 3 : Données agrégées par erreur
|
||||
```
|
||||
|
||||
**Action** :
|
||||
```
|
||||
Si patient réel :
|
||||
→ CONSERVER et analyser séparément (cas sévères)
|
||||
Si erreur :
|
||||
→ CORRIGER ou EXCLURE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Checklist de Gestion des Outliers
|
||||
|
||||
### Détection
|
||||
- [ ] Identifier les outliers univariés (🔴 cercles rouges)
|
||||
- [ ] Identifier les outliers multivariés (🟣 cercles violets)
|
||||
- [ ] Compter le nombre d'outliers
|
||||
- [ ] Vérifier la proportion (< 5% des données)
|
||||
|
||||
### Investigation
|
||||
- [ ] Vérifier la source de données
|
||||
- [ ] Rechercher des erreurs de saisie
|
||||
- [ ] Consulter un expert du domaine
|
||||
- [ ] Explorer les causes possibles
|
||||
|
||||
### Action
|
||||
- [ ] Corriger les erreurs identifiées
|
||||
- [ ] Conserver les valeurs légitimes
|
||||
- [ ] Exclure les vraies anomalies
|
||||
- [ ] Documenter toutes les décisions
|
||||
|
||||
### Validation
|
||||
- [ ] Comparer les résultats avec/sans
|
||||
- [ ] Vérifier la taille de l'échantillon final
|
||||
- [ ] S'assurer que la distribution est cohérente
|
||||
- [ ] Valider avec des graphiques
|
||||
|
||||
---
|
||||
|
||||
## 📊 Fonctionnalités de l'Application
|
||||
|
||||
### Détection Automatique
|
||||
|
||||
L'application détecte automatiquement les outliers à l'importation :
|
||||
|
||||
```python
|
||||
# Méthode 1 : IQR (univarié)
|
||||
uni_outliers = detect_univariate_outliers(
|
||||
data,
|
||||
columns=numeric_columns
|
||||
)
|
||||
|
||||
# Méthode 2 : Isolation Forest (multivarié)
|
||||
multi_outliers = detect_multivariate_outliers(
|
||||
data,
|
||||
columns=numeric_columns
|
||||
)
|
||||
|
||||
# Fusion structurée
|
||||
result = merge_outliers_structured(uni_outliers, multi_outliers)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Interface Utilisateur
|
||||
|
||||
#### Indicateurs Visuels
|
||||
- 🔴 **Cercle rouge** : Outlier univarié (sur la cellule)
|
||||
- 🟣 **Cercle violet** : Outlier multivarié (sur la ligne)
|
||||
- 🔴🟣 **Les deux** : Les deux types d'outliers
|
||||
|
||||
#### Information au Survol
|
||||
```javascript
|
||||
// Exemple de tooltip
|
||||
{
|
||||
"index": 42,
|
||||
"reasons": [
|
||||
"Column 'Surface' value 500 is outside IQR bounds [32.5, 115.5]",
|
||||
"Multivariate anomaly detected by Isolation Forest"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Exclusion et Mémorisation
|
||||
|
||||
```python
|
||||
# Marquer une ligne pour exclusion
|
||||
excluded_indices = [42, 87, 156]
|
||||
|
||||
# Ré-analyser sans ces lignes
|
||||
new_analysis = analyze_with_exclusions(
|
||||
data,
|
||||
excluded_indices=excluded_indices
|
||||
)
|
||||
|
||||
# Les exclusions sont mémorisées dans la session
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Conclusion
|
||||
|
||||
Les outliers sont à la fois un **défi** et une **opportunité** :
|
||||
|
||||
⚠️ **Défi** :
|
||||
- Peuvent fausser vos analyses
|
||||
- Nécessitent une investigation minutieuse
|
||||
- Exiger des décisions subjectives
|
||||
|
||||
✅ **Opportunité** :
|
||||
- Peuvent révéler des phénomènes intéressants
|
||||
- Signalent des erreurs dans les données
|
||||
- Permettent d'améliorer la qualité des analyses
|
||||
|
||||
**Points clés** :
|
||||
1. **Toujours investiguer** avant d'exclure
|
||||
2. **Documenter** vos décisions
|
||||
3. **Comparer** avec/sans outliers
|
||||
4. **Utiliser** des méthodes robustes
|
||||
5. **Visualiser** pour comprendre
|
||||
|
||||
---
|
||||
|
||||
**Version** : 1.0
|
||||
**Projet** : Application d'Analyse de Données
|
||||
|
||||
🔗 **Voir aussi** : [Guide Corrélation](CORRELATION_GUIDE.md) | [Guide Régression](REGRESSION_GUIDE.md)
|
||||
226
frontend/public/docs/README.md
Normal file
226
frontend/public/docs/README.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# 📚 Documentation Utilisateur
|
||||
|
||||
Bienvenue dans la documentation complète de l'application d'analyse de données statistiques.
|
||||
|
||||
---
|
||||
|
||||
## 📖 Guides Disponibles
|
||||
|
||||
### 🚀 [Guide Utilisateur Principal](USER_GUIDE.md)
|
||||
**Point de départ recommandé**
|
||||
|
||||
- Démarrage rapide
|
||||
- Vue d'ensemble des fonctionnalités
|
||||
- Workflow d'analyse complet
|
||||
- Cas d'usage pratiques
|
||||
- FAQ et glossaire
|
||||
|
||||
**À lire si vous découvrez l'application**
|
||||
|
||||
---
|
||||
|
||||
### 🔗 [Guide de la Corrélation](CORRELATION_GUIDE.md)
|
||||
**Analyser les relations entre variables**
|
||||
|
||||
- Qu'est-ce que la corrélation ?
|
||||
- Les 3 méthodes (Pearson, Spearman, Kendall)
|
||||
- Interpréter la matrice de corrélation
|
||||
- Comprendre la multicolinéarité
|
||||
- P-values et significativité
|
||||
- Bonnes pratiques et exemples
|
||||
|
||||
**À lire pour :** Choisir les bons prédicteurs, éviter la multicolinéarité
|
||||
|
||||
---
|
||||
|
||||
### 📈 [Guide de la Régression](REGRESSION_GUIDE.md)
|
||||
**Modéliser et prédire des phénomènes**
|
||||
|
||||
- Concepts fondamentaux
|
||||
- 4 types de modèles (linéaire, logistique, polynomial, exponentielle)
|
||||
- Configuration du modèle
|
||||
- Interprétation des coefficients
|
||||
- Métriques de qualité (R², AIC, BIC)
|
||||
- Équations exportables (Python, Excel, LaTeX)
|
||||
- Diagnostics graphiques
|
||||
- Exemples détaillés
|
||||
|
||||
**À lire pour :** Construire des modèles prédictifs robustes
|
||||
|
||||
---
|
||||
|
||||
### 🔍 [Guide des Outliers](OUTLIER_GUIDE.md)
|
||||
**Gérer les valeurs aberrantes**
|
||||
|
||||
- Définition et types d'outliers
|
||||
- Méthodes de détection (IQR, Isolation Forest)
|
||||
- Indicateurs visuels (🔴 rouge, 🟣 violet)
|
||||
- Processus de gestion
|
||||
- Impact sur les analyses
|
||||
- Bonnes pratiques
|
||||
- Cas concrets
|
||||
|
||||
**À lire pour :** Garantir la qualité de vos analyses
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Parcours Recommandé
|
||||
|
||||
### Débutant
|
||||
```
|
||||
1. USER_GUIDE.md (Comprendre l'application)
|
||||
2. CORRELATION_GUIDE.md (Explorer les relations)
|
||||
3. REGRESSION_GUIDE.md (Premiers modèles)
|
||||
```
|
||||
|
||||
### Intermédiaire
|
||||
```
|
||||
1. CORRELATION_GUIDE.md (Multicolinéarité)
|
||||
2. REGRESSION_GUIDE.md (Modèles avancés)
|
||||
3. OUTLIER_GUIDE.md (Nettoyage des données)
|
||||
```
|
||||
|
||||
### Expert
|
||||
```
|
||||
1. OUTLIER_GUIDE.md (Gestion avancée)
|
||||
2. REGRESSION_GUIDE.md (Interprétation détaillée)
|
||||
3. CORRELATION_GUIDE.md (Choix des méthodes)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Résumé des Fonctionnalités
|
||||
|
||||
### Corrélation 📊
|
||||
- Matrice de corrélation avec heatmap
|
||||
- 3 méthodes (Pearson, Spearman, Kendall)
|
||||
- Détection de multicolinéarité (≥0.7)
|
||||
- P-values et significativité
|
||||
- Export CSV
|
||||
|
||||
### Régression 📈
|
||||
- 4 types de modèles (linéaire, logistique, polynomial, exponentielle)
|
||||
- Sélection automatique des variables (importance)
|
||||
- Interactions et termes polynomiaux
|
||||
- Équations exportables (Python, Excel, LaTeX)
|
||||
- Graphiques diagnostiques (fit, partial regression, parity plot)
|
||||
- Métriques complètes (R², coefficients, p-values)
|
||||
|
||||
### Outliers 🔍
|
||||
- Détection univariée (IQR)
|
||||
- Détection multivariée (Isolation Forest)
|
||||
- Indicateurs visuels (🔴 rouge, 🟣 violet)
|
||||
- Processus d'exclusion
|
||||
- Raisons détaillées
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Recherche Rapide
|
||||
|
||||
### Je veux...
|
||||
|
||||
**Comprendre mes données**
|
||||
→ [Guide Corrélation](CORRELATION_GUIDE.md)
|
||||
|
||||
**Prédire une variable**
|
||||
→ [Guide Régression](REGRESSION_GUIDE.md)
|
||||
|
||||
**Nettoyer mes données**
|
||||
→ [Guide Outliers](OUTLIER_GUIDE.md)
|
||||
|
||||
**Choisir les bons prédicteurs**
|
||||
→ [Guide Corrélation - Multicolinéarité](CORRELATION_GUIDE.md#multicolinéarité)
|
||||
|
||||
**Interpréter les coefficients**
|
||||
→ [Guide Régression - Interprétation](REGRESSION_GUIDE.md#interprétation-des-résultats)
|
||||
|
||||
**Comprendre le R²**
|
||||
→ [Guide Régression - Métriques](REGRESSION_GUIDE.md#métriques-de-qualité-du-modèle)
|
||||
|
||||
**Exporter l'équation**
|
||||
→ [Guide Régression - Équations](REGRESSION_GUIDE.md#équations-du-modèle)
|
||||
|
||||
**Gérer les valeurs extrêmes**
|
||||
→ [Guide Outliers](OUTLIER_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
## 📝 Glossaire
|
||||
|
||||
### Termes Clés
|
||||
|
||||
- **Corrélation** : Force et direction d'une relation entre deux variables
|
||||
- **Multicolinéarité** : Forte corrélation entre prédicteurs (problématique)
|
||||
- **Outlier** : Valeur aberrante qui s'écarte du reste des données
|
||||
- **R²** : Proportion de variance expliquée par le modèle (0 à 1)
|
||||
- **P-value** : Probabilité que le résultat soit dû au hasard (< 0.05 = significatif)
|
||||
- **Coefficient** : Impact moyen d'une variable sur la cible
|
||||
- **IQR** : Interquartile Range (Q3 - Q1), utilisé pour détecter les outliers
|
||||
- **Isolation Forest** : Algorithme de détection d'anomalies multivariées
|
||||
- **Régression** : Méthode pour modéliser la relation entre variables
|
||||
|
||||
---
|
||||
|
||||
## 🚦 Codes Couleurs
|
||||
|
||||
### Dans la documentation
|
||||
- 📊 **Information** : Définitions et concepts
|
||||
- ✅ **Bon** : Bonne pratique
|
||||
- ❌ **Mauvais** : Erreur à éviter
|
||||
- ⚠️ **Attention** : Point important ou risqué
|
||||
- 🔍 **Investigation** : Analyse nécessaire
|
||||
- 🎯 **Objectif** : But à atteindre
|
||||
|
||||
### Dans l'application
|
||||
- 🔴 **Rouge** : Outlier univarié ou corrélation négative forte
|
||||
- 🟣 **Violet** : Outlier multivarié
|
||||
- 🟢 **Vert** : Significatif ou fiable
|
||||
- 🔵 **Bleu** : Corrélation positive forte
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support et Ressources
|
||||
|
||||
### Documentation Technique
|
||||
- Code source : `/backend/app/core/engine/`
|
||||
- API endpoints : `/backend/app/api/v1/`
|
||||
- Frontend : `/frontend/src/features/`
|
||||
|
||||
### Standards Statistiques
|
||||
- [Pearson Correlation](https://en.wikipedia.org/wiki/Pearson_correlation_coefficient)
|
||||
- [Spearman's Rank](https://en.wikipedia.org/wiki/Spearman%27s_rank_correlation_coefficient)
|
||||
- [Linear Regression](https://en.wikipedia.org/wiki/Linear_regression)
|
||||
- [Isolation Forest](https://en.wikipedia.org/wiki/Isolation_forest)
|
||||
|
||||
---
|
||||
|
||||
## 📅 Mises à Jour
|
||||
|
||||
**Version actuelle** : 1.0
|
||||
**Dernière mise à jour** : Janvier 2026
|
||||
|
||||
### Changements récents
|
||||
- ✅ Documentation complète créée
|
||||
- ✅ Guides utilisateurs détaillés
|
||||
- ✅ Exemples concrets
|
||||
- ✅ Glossaire et parcours recommandés
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Bonnes Analyses !
|
||||
|
||||
Cette documentation est conçue pour vous accompagner à chaque étape de vos analyses statistiques.
|
||||
|
||||
**Rappelez-vous** :
|
||||
- 📊 **Vérifiez toujours** la corrélation avant de régresser
|
||||
- 🔍 **Investiguez** les outliers avant de les exclure
|
||||
- 📈 **Interprétez** les coefficients (pas juste le R²)
|
||||
- ✅ **Documentez** vos décisions
|
||||
|
||||
Bonne découverte de l'application !
|
||||
|
||||
---
|
||||
|
||||
**Version** : 1.0
|
||||
**Projet** : Application Web d'Analyse de Données
|
||||
**Plateforme** : Backend Python + Frontend Next.js
|
||||
1072
frontend/public/docs/REGRESSION_GUIDE.md
Normal file
1072
frontend/public/docs/REGRESSION_GUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
328
frontend/public/docs/USER_GUIDE.md
Normal file
328
frontend/public/docs/USER_GUIDE.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# Guide Utilisateur - Application d'Analyse de Données
|
||||
|
||||
📊 **Bienvenue** dans l'application d'analyse de données statistiques. Ce guide vous accompagne pas à pas dans l'utilisation des fonctionnalités principales.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Démarrage Rapide
|
||||
|
||||
### 1. Importer vos données
|
||||
- **Formats supportés** : CSV, Excel
|
||||
- **Cliquez sur** "Upload File" pour charger votre dataset
|
||||
- Les données sont automatiquement détectées et typées (numérique, catégorique, date)
|
||||
|
||||
### 2. Explorer vos données
|
||||
- Utilisez la **table intelligente** pour visualiser vos données
|
||||
- Les outliers potentiels sont indiqués par des **cercles colorés** :
|
||||
- 🔴 **Rouge** : Outlier univarié (valeur extrême dans une colonne)
|
||||
- 🟣 **Violet** : Outlier multivarié (anomalie globale)
|
||||
|
||||
### 3. Lancer une analyse
|
||||
Cliquez sur le bouton **"Analyse"** dans la barre latérale pour accéder aux outils d'analyse.
|
||||
|
||||
---
|
||||
|
||||
## 📈 Fonctionnalités Principales
|
||||
|
||||
### 1. Matrice de Corrélation 🔗
|
||||
|
||||
**Objectif** : Comprendre les relations entre vos variables numériques.
|
||||
|
||||
#### Comment l'utiliser ?
|
||||
|
||||
1. **Accédez à l'onglet "Corrélation"**
|
||||
2. **Choisissez la méthode** :
|
||||
- **Pearson** : Relations linéaires (données normales)
|
||||
- **Spearman** : Relations monotones (données non-paramétriques)
|
||||
- **Kendall** : Similarité de rang (petits échantillons)
|
||||
|
||||
3. **Interprétez la heatmap** :
|
||||
- **Rouge foncé** : Forte corrélation positive (0.7 à 1.0)
|
||||
- **Bleu foncé** : Forte corrélation négative (-0.7 à -1.0)
|
||||
- **Bordure rouge** ⚠️ : Multicolinéarité détectée (≥0.7)
|
||||
|
||||
4. **Filtres avancés** :
|
||||
- **Seuil minimum** : Affichez seulement les corrélations > X
|
||||
- **Significatif seulement** : p-value < 0.05
|
||||
|
||||
#### ⚠️ Alertes Multicolinéarité
|
||||
Si vous voyez une **bordure rouge** entre deux prédicteurs :
|
||||
- **Ne les utilisez pas ensemble** dans une régression
|
||||
- Choisissez la variable la plus corrélée avec votre cible
|
||||
- Exemple : Si `Taille` et `Poids` sont corrélés à 0.85, gardez-en un seul
|
||||
|
||||
#### Export
|
||||
Cliquez sur **"Exporter CSV"** pour télécharger les résultats.
|
||||
|
||||
---
|
||||
|
||||
### 2. Régression Statistique 📉
|
||||
|
||||
**Objectif** : Modéliser et prédire une variable cible.
|
||||
|
||||
#### Étape 1 : Configuration du modèle
|
||||
|
||||
Dans le panneau de configuration avancée :
|
||||
|
||||
**1. Choisissez votre type de modèle** :
|
||||
- **Linéaire** : Relations linéaires simples
|
||||
- **Logistique** : Cible binaire (oui/non, 0/1)
|
||||
- **Polynomial** : Relations courbes (degrés 2-5)
|
||||
- **Exponentielle** : Croissance/décroissance exponentielle
|
||||
|
||||
**2. Sélectionnez votre Variable Cible (Y)** :
|
||||
- La variable que vous voulez expliquer/prédire
|
||||
- Pour linéaire/polynomial/exponentielle : doit être **numérique continue**
|
||||
- Pour logistique : doit être **catégorique ou binaire**
|
||||
|
||||
**3. Choisissez vos Prédicteurs (X)** :
|
||||
- Les variables qui expliquent Y
|
||||
- L'application recommande automatiquement les **5 meilleures variables** basées sur leur importance
|
||||
- Désélectionnez les variables avec multicolinéarité
|
||||
|
||||
#### Étape 2 : Options avancées
|
||||
|
||||
**Pour Polynomial** :
|
||||
- **Degré du polynôme** : 2 (quadratique) à 5
|
||||
- + degré = + complexité (risque de sur-apprentissage)
|
||||
|
||||
**Pour Linéaire/Polynomial** :
|
||||
- **Inclure interactions** : Crée des termes croisés (x1*x2)
|
||||
- Utile pour capturer les effets combinés de variables
|
||||
|
||||
#### Étape 3 : Lancer l'analyse
|
||||
|
||||
Cliquez sur **"Lancer l'Analyse"** et attendez les résultats.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Interpréter les Résultats de Régression
|
||||
|
||||
### 1. Métriques de Qualité
|
||||
|
||||
| Métrique | Description | Valeur Idéale |
|
||||
|----------|-------------|---------------|
|
||||
| **R-Squared** | Proportion de variance expliquée | 0.7 - 1.0 |
|
||||
| **Adj. R-Squared** | R² ajusté pour le nombre de variables | Proche de R² |
|
||||
| **AIC / BIC** | Critères d'information (plus bas = mieux) | Comparer modèles |
|
||||
|
||||
#### R-Squared Guide
|
||||
- **0.90 - 1.00** : Excellent ajustement
|
||||
- **0.70 - 0.90** : Bon ajustement
|
||||
- **0.50 - 0.70** : Ajustement modéré
|
||||
- **< 0.50** : Faible ajustement
|
||||
|
||||
---
|
||||
|
||||
### 2. Coefficients du Modèle
|
||||
|
||||
Le tableau des coefficients indique l'impact de chaque variable :
|
||||
|
||||
| Colonne | Signification |
|
||||
|---------|---------------|
|
||||
| **Variable** | Nom de la variable ou constante |
|
||||
| **Coefficient** | Impact moyen sur Y (si X augmente de 1) |
|
||||
| **P-Value** | Significativité statistique |
|
||||
| **Fiabilité** | FIABLE si p < 0.05 |
|
||||
|
||||
#### Comment lire les coefficients ?
|
||||
|
||||
**Exemple** : `Y = 10 + 2.5*X1 - 1.3*X2`
|
||||
|
||||
- **Constante (10)** : Valeur de Y quand tous les X = 0
|
||||
- **X1 (+2.5)** : Si X1 augmente de 1, Y augmente de 2.5
|
||||
- **X2 (-1.3)** : Si X2 augmente de 1, Y diminue de 1.3
|
||||
|
||||
⚠️ **Important** : Un coefficient n'est fiable que si **p-value < 0.05** (FIABLE)
|
||||
|
||||
---
|
||||
|
||||
### 3. Équation du Modèle
|
||||
|
||||
L'application génère automatiquement l'équation dans 3 formats :
|
||||
|
||||
#### LaTeX (Mathématique)
|
||||
Pour vos rapports et publications :
|
||||
```
|
||||
y = 1.234567 + 2.345678x_{0} + 3.456789x_{0}^{2}
|
||||
```
|
||||
|
||||
#### Python (Code)
|
||||
Pour implémenter le modèle en Python :
|
||||
```python
|
||||
y = 1.234567 + 2.345678*x0 + 3.456789*x0**2
|
||||
```
|
||||
|
||||
#### Excel (Formule)
|
||||
Pour utiliser dans Excel/Google Sheets :
|
||||
```
|
||||
=1.234567 + 2.345678*A1 + 3.456789*A1^2
|
||||
```
|
||||
|
||||
**Bouton "Copier"** : Copiez l'équation directement dans votre presse-papier !
|
||||
|
||||
---
|
||||
|
||||
### 4. Graphiques de Diagnostic
|
||||
|
||||
#### Fit Plot (Régression univariée)
|
||||
- **Points gris** : Vos données réelles
|
||||
- **Ligne bleue** : Le modèle ajusté
|
||||
- Vérifiez que la ligne suit bien la tendance des points
|
||||
|
||||
#### Partial Regression Plot (Régression multivariée)
|
||||
- Montre l'**effet isolé** de chaque variable
|
||||
- Contrôle l'effet des autres variables
|
||||
- La **pente** = coefficient du modèle
|
||||
- Utilisez le sélecteur pour changer de variable
|
||||
|
||||
#### Parity Plot (Validation)
|
||||
- **Diagonale rouge** : Prédictions parfaites (Y = X)
|
||||
- **Points violets** : Vos observations
|
||||
- Plus les points sont proches de la diagonale = meilleur modèle
|
||||
- **Écart à la diagonale** = erreur de prédiction
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Détection et Gestion des Outliers
|
||||
|
||||
### Types d'Outliers
|
||||
|
||||
#### 1. Outliers Univariés (IQR)
|
||||
Détection basée sur l'écart interquartile :
|
||||
- **Calcul** : Q1 - 1.5×IQR (bas) / Q3 + 1.5×IQR (haut)
|
||||
- **Indicateur** : 🔴 Cercle rouge
|
||||
- **Action** : Vérifiez la valeur, corrigez ou excluez
|
||||
|
||||
#### 2. Outliers Multivariés (Isolation Forest)
|
||||
Détection basée sur les combinaisons de variables :
|
||||
- **Algorithme** : Isolation Forest
|
||||
- **Indicateur** : 🟣 Cercle violet
|
||||
- **Action** : Anomalie globale à investiguer
|
||||
|
||||
### Processus d'Exclusion
|
||||
|
||||
1. **Identifiez** les outliers dans la table
|
||||
2. **Survolez** pour voir le détail (raison)
|
||||
3. **Cliquez sur l'outlier** pour le marquer
|
||||
4. **Réexécutez** l'analyse sans ces points
|
||||
|
||||
**Note** : Les outliers exclus sont mémorisés et ne réapparaissent pas.
|
||||
|
||||
---
|
||||
|
||||
## 💡 Bonnes Pratiques
|
||||
|
||||
### Avant la Régression
|
||||
|
||||
✅ **TOUJOURS** vérifier la matrice de corrélation
|
||||
✅ **Éviter** la multicolinéarité (corrélations ≥ 0.7 entre prédicteurs)
|
||||
✅ **Choisir** des prédicteurs corrélés avec la cible
|
||||
✅ **Exclure** les outliers extrêmes
|
||||
|
||||
### Pendant l'Analyse
|
||||
|
||||
✅ **Commencer** par un modèle linéaire simple
|
||||
✅ **Augmenter** la complexité progressivement (polynomial, interactions)
|
||||
✅ **Surveiller** le R² ajusté (il diminue si variables inutiles)
|
||||
✅ **Vérifier** les p-values (< 0.05 = fiable)
|
||||
|
||||
### Après l'Analyse
|
||||
|
||||
✅ **Valider** avec le Parity Plot (points près de la diagonale)
|
||||
✅ **Interpréter** les coefficients (sens et magnitude)
|
||||
✅ **Exporter** l'équation pour utilisation future
|
||||
✅ **Documenter** les décisions (variables exclues, outliers retirés)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Cas d'Usage
|
||||
|
||||
### Exemple 1 : Prédire le Prix Immobiliers
|
||||
|
||||
**Données** : Prix, Surface, Chambres, Quartier, Année
|
||||
|
||||
1. **Corrélation** : Prix vs Surface (forte corrélation)
|
||||
2. **Régression linéaire** :
|
||||
- Y = Prix
|
||||
- X = Surface, Chambres
|
||||
- Équation : `Prix = 50000 + 2500*Surface + 15000*Chambres`
|
||||
3. **Utilisation** : Prédire le prix d'un appartement de 60m² avec 2 chambres
|
||||
- `Prix = 50000 + 2500*60 + 15000*2 = 215000€`
|
||||
|
||||
### Exemple 2 : Probabilité de Réclamation
|
||||
|
||||
**Données** : Âge, Montant, Historique, Réclamation (oui/non)
|
||||
|
||||
1. **Régression logistique** :
|
||||
- Y = Réclamation (0/1)
|
||||
- X = Âge, Montant, Historique
|
||||
2. **Résultat** : Probabilité de réclamation = f(Âge, Montant, Historique)
|
||||
|
||||
### Exemple 3 : Relation Courbe (Ventes vs Publicité)
|
||||
|
||||
**Données** : Ventes, Budget_Pub, Concurrence
|
||||
|
||||
1. **Corrélation** : Forte mais non-linéaire
|
||||
2. **Régression polynomial (degré 2)** :
|
||||
- Y = Ventes
|
||||
- X = Budget_Pub
|
||||
- Équation : `Ventes = 1000 + 5*Budget - 0.01*Budget²`
|
||||
3. **Interprétation** : Rendements décroissants après un certain budget
|
||||
|
||||
---
|
||||
|
||||
## ❓ Questions Fréquentes
|
||||
|
||||
### Mon R² est faible (< 0.5). Que faire ?
|
||||
|
||||
- **Vérifiez** : Avez-vous les bons prédicteurs ?
|
||||
- **Testez** : Ajoutez des variables ou essayez polynomial
|
||||
- **Nettoyez** : Supprimez les outliers
|
||||
- **Acceptez** : Le modèle n'explique peut-être pas tout (variables manquantes)
|
||||
|
||||
### Une variable a une p-value > 0.05. Je la garde ?
|
||||
|
||||
**Non** en général :
|
||||
- Le coefficient n'est pas statistiquement significatif
|
||||
- Le modèle est plus robuste sans elle
|
||||
- Exception : Avis d'expert justifiant son importance
|
||||
|
||||
### Combien de prédicteurs choisir ?
|
||||
|
||||
- **Règle** : 1 prédicteur pour 10-20 observations
|
||||
- **Maximum** : n/10 (n = taille échantillon)
|
||||
- **Qualité > Quantité** : Préférez 5 variables fiables à 20 variables instables
|
||||
|
||||
### Quand utiliser polynomial vs linéaire ?
|
||||
|
||||
- **Linéaire** : Relation droite (premier choix)
|
||||
- **Polynomial** : Relation courbe évidente sur le scatter plot
|
||||
- **Attention** : Degré trop élevé = sur-apprentissage
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support et Ressources
|
||||
|
||||
### Documentation Détaillée
|
||||
- 📊 **[Corrélation](CORRELATION_GUIDE.md)** : Guide complet des coefficients de corrélation
|
||||
- 📈 **[Régression](REGRESSION_GUIDE.md)** : Modèles, interprétation, équations
|
||||
- 🔍 **[Outliers](OUTLIER_GUIDE.md)** : Méthodes de détection et gestion
|
||||
|
||||
### Glossaire
|
||||
|
||||
- **Corrélation** : Force et direction d'une relation entre deux variables
|
||||
- **Multicolinéarité** : Forte corrélation entre prédicteurs (problématique)
|
||||
- **P-value** : Probabilité que le résultat soit dû au hasard (< 0.05 = significatif)
|
||||
- **R²** : Proportion de variance expliquée par le modèle (0 à 1)
|
||||
- **Outlier** : Observation anormale qui s'écarte du reste des données
|
||||
- **Isolation Forest** : Algorithme de détection d'anomalies multivariées
|
||||
- **IQR** : Interquartile Range (Q3 - Q1), utilisé pour détecter les extrêmes
|
||||
|
||||
---
|
||||
|
||||
**Version** : 1.0
|
||||
**Dernière mise à jour** : Janvier 2026
|
||||
**Plateforme** : Application Web d'Analyse de Données
|
||||
|
||||
🎓 **Bonnes analyses !**
|
||||
1
frontend/public/file.svg
Normal file
1
frontend/public/file.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
frontend/public/globe.svg
Normal file
1
frontend/public/globe.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
frontend/public/next.svg
Normal file
1
frontend/public/next.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
frontend/public/vercel.svg
Normal file
1
frontend/public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
1
frontend/public/window.svg
Normal file
1
frontend/public/window.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
411
frontend/src/app/docs/[...slug]/page.tsx
Normal file
411
frontend/src/app/docs/[...slug]/page.tsx
Normal file
@@ -0,0 +1,411 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft, BookOpen, Home } from "lucide-react";
|
||||
|
||||
// Mapping des fichiers markdown
|
||||
const DOC_FILES = {
|
||||
"readme": "README.md",
|
||||
"user-guide": "USER_GUIDE.md",
|
||||
"correlation-guide": "CORRELATION_GUIDE.md",
|
||||
"regression-guide": "REGRESSION_GUIDE.md",
|
||||
"outlier-guide": "OUTLIER_GUIDE.md",
|
||||
};
|
||||
|
||||
export default function DocsPage() {
|
||||
const params = useParams();
|
||||
const slug = params.slug as string[];
|
||||
const [content, setContent] = useState<string>("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadDoc = async () => {
|
||||
try {
|
||||
const docName = slug[0]?.toLowerCase();
|
||||
const fileName = DOC_FILES[docName as keyof typeof DOC_FILES] || `${docName}.md`;
|
||||
|
||||
const response = await fetch(`/docs/${fileName}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Document non trouvé");
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
setContent(text);
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadDoc();
|
||||
}, [slug]);
|
||||
|
||||
// Gérer l'ancre (hash) pour faire défiler jusqu'à la section
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined" && window.location.hash && content) {
|
||||
setTimeout(() => {
|
||||
const hash = window.location.hash.replace("#", "");
|
||||
let element = document.getElementById(hash);
|
||||
|
||||
if (!element) {
|
||||
const headings = document.querySelectorAll("h2, h3");
|
||||
for (const heading of headings) {
|
||||
const text = heading.textContent || "";
|
||||
const slug = text
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9\s-]/g, "")
|
||||
.trim()
|
||||
.replace(/\s+/g, "-");
|
||||
if (slug === hash) {
|
||||
element = heading as HTMLElement;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin mx-auto mb-4" />
|
||||
<p className="text-slate-500">Chargement de la documentation...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
// Convertisseur Markdown robuste
|
||||
const parseMarkdown = (md: string): string => {
|
||||
const lines = md.split('\n');
|
||||
const output: string[] = [];
|
||||
let inCodeBlock = false;
|
||||
let codeBlockContent: string[] = [];
|
||||
let codeBlockLang = '';
|
||||
let inTable = false;
|
||||
let tableHeader: string[] = [];
|
||||
let tableRows: string[][] = [];
|
||||
let inList = false;
|
||||
let listType: 'ul' | 'ol' = 'ul';
|
||||
let listItems: string[] = [];
|
||||
|
||||
const flushList = () => {
|
||||
if (inList && listItems.length > 0) {
|
||||
const tag = listType;
|
||||
const className = listType === 'ul' ? 'list-disc' : 'list-decimal';
|
||||
output.push(`<ul class="my-4 ml-6 space-y-2 ${className}">${listItems.join('')}</ul>`);
|
||||
listItems = [];
|
||||
}
|
||||
inList = false;
|
||||
};
|
||||
|
||||
const flushTable = () => {
|
||||
if (inTable && tableHeader.length > 0) {
|
||||
let tableHtml = '<div class="overflow-x-auto my-6 rounded-lg border border-slate-300"><table class="min-w-full border-collapse">';
|
||||
tableHtml += '<thead class="bg-slate-50"><tr>';
|
||||
tableHeader.forEach(cell => {
|
||||
tableHtml += `<th class="px-4 py-2 text-left font-semibold text-slate-900 border-b border-slate-300">${parseInline(cell)}</th>`;
|
||||
});
|
||||
tableHtml += '</tr></thead><tbody>';
|
||||
tableRows.forEach(row => {
|
||||
tableHtml += '<tr class="hover:bg-slate-50">';
|
||||
row.forEach(cell => {
|
||||
tableHtml += `<td class="px-4 py-2 text-slate-700 border-b border-slate-200">${parseInline(cell)}</td>`;
|
||||
});
|
||||
tableHtml += '</tr>';
|
||||
});
|
||||
tableHtml += '</tbody></table></div>';
|
||||
output.push(tableHtml);
|
||||
tableHeader = [];
|
||||
tableRows = [];
|
||||
}
|
||||
inTable = false;
|
||||
};
|
||||
|
||||
const parseInline = (text: string): string => {
|
||||
let result = text;
|
||||
|
||||
// Code inline - doit être fait AVANT le bold pour éviter les conflits
|
||||
result = result.replace(/`([^`]+)`/g, '<code class="bg-slate-100 text-rose-600 px-1.5 py-0.5 rounded text-sm font-mono">$1</code>');
|
||||
|
||||
// Bold et italic combiné
|
||||
result = result.replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>');
|
||||
|
||||
// Bold
|
||||
result = result.replace(/\*\*([^*]+)\*\*/g, '<strong class="font-semibold text-slate-900">$1</strong>');
|
||||
|
||||
// Italic - utiliser une approche plus simple pour éviter les lookbehind
|
||||
// Remplacer *text* par <em> seulement si pas déjà dans <strong>
|
||||
result = result.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
||||
|
||||
// Conserver les emojis et caractères spéciaux
|
||||
result = result.replace(/✅/g, '✅');
|
||||
result = result.replace(/❌/g, '❌');
|
||||
result = result.replace(/⚠️/g, '⚠️');
|
||||
result = result.replace(/📊/g, '📊');
|
||||
result = result.replace(/📈/g, '📈');
|
||||
result = result.replace(/🔍/g, '🔍');
|
||||
result = result.replace(/🎯/g, '🎯');
|
||||
result = result.replace(/⭐/g, '⭐');
|
||||
result = result.replace(/📐/g, '📐');
|
||||
result = result.replace(/💡/g, '💡');
|
||||
result = result.replace(/🎓/g, '🎓');
|
||||
result = result.replace(/🔴/g, '🔴');
|
||||
result = result.replace(/🟣/g, '🟣');
|
||||
|
||||
// Liens
|
||||
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
|
||||
if (url.startsWith('#')) {
|
||||
return `<a href="${url}" class="text-indigo-600 hover:text-indigo-700 underline">${text}</a>`;
|
||||
} else if (url.endsWith('.md')) {
|
||||
const docName = url.replace('.md', '').replace('docs/', '').toLowerCase();
|
||||
const mappedName = Object.keys(DOC_FILES).find(
|
||||
key => DOC_FILES[key as keyof typeof DOC_FILES] === url.replace('docs/', '')
|
||||
) || docName;
|
||||
return `<a href="/docs/${mappedName}" class="text-indigo-600 hover:text-indigo-700 underline">${text}</a>`;
|
||||
} else {
|
||||
return `<a href="${url}" class="text-indigo-600 hover:text-indigo-700 underline" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
||||
}
|
||||
});
|
||||
|
||||
// Math inline
|
||||
result = result.replace(/\$([^$]+)\$/g, '<span class="font-mono text-sm bg-slate-100 px-1 rounded">$1</span>');
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const createId = (text: string): string => {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.trim()
|
||||
.replace(/\s+/g, '-');
|
||||
};
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Code blocks
|
||||
if (line.startsWith('```')) {
|
||||
if (inCodeBlock) {
|
||||
// End code block
|
||||
const code = codeBlockContent.join('\n');
|
||||
const escaped = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
output.push(`<pre class="bg-slate-800 text-slate-100 p-4 rounded-lg overflow-x-auto my-4 text-sm"><code>${escaped}</code></pre>`);
|
||||
codeBlockContent = [];
|
||||
inCodeBlock = false;
|
||||
} else {
|
||||
// Start code block
|
||||
flushList();
|
||||
flushTable();
|
||||
codeBlockLang = line.slice(3).trim();
|
||||
inCodeBlock = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
codeBlockContent.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Table separator
|
||||
if (inTable && line.trim().startsWith('|---')) {
|
||||
continue; // Skip separator line
|
||||
}
|
||||
|
||||
// Table rows
|
||||
if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
|
||||
if (!inTable) {
|
||||
flushList();
|
||||
inTable = true;
|
||||
}
|
||||
const cells = line.split('|').slice(1, -1).map(c => c.trim());
|
||||
if (tableHeader.length === 0) {
|
||||
tableHeader = cells;
|
||||
} else {
|
||||
tableRows.push(cells);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not in table anymore
|
||||
if (inTable) {
|
||||
flushTable();
|
||||
}
|
||||
|
||||
// Headers
|
||||
const h1Match = line.match(/^#\s+(.+)$/);
|
||||
if (h1Match) {
|
||||
flushList();
|
||||
const text = parseInline(h1Match[1]);
|
||||
const id = createId(h1Match[1]);
|
||||
output.push(`<h1 id="${id}" class="text-4xl font-bold text-slate-900 mt-8 mb-6">${text}</h1>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const h2Match = line.match(/^##\s+(.+)$/);
|
||||
if (h2Match) {
|
||||
flushList();
|
||||
const text = parseInline(h2Match[1]);
|
||||
const id = createId(h2Match[1]);
|
||||
output.push(`<h2 id="${id}" class="text-2xl font-bold text-slate-900 mt-10 mb-6 pb-2 border-b border-slate-200">${text}</h2>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const h3Match = line.match(/^###\s+(.+)$/);
|
||||
if (h3Match) {
|
||||
flushList();
|
||||
const text = parseInline(h3Match[1]);
|
||||
const id = createId(h3Match[1]);
|
||||
output.push(`<h3 id="${id}" class="text-xl font-bold text-slate-800 mt-8 mb-4">${text}</h3>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Horizontal rule
|
||||
if (line.trim() === '---' || line.trim() === '***') {
|
||||
flushList();
|
||||
output.push('<hr class="my-8 border-slate-200" />');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Blockquote
|
||||
if (line.trim().startsWith('>')) {
|
||||
flushList();
|
||||
const text = parseInline(line.trim().slice(1).trim());
|
||||
output.push(`<blockquote class="border-l-4 border-indigo-400 pl-4 py-2 my-4 bg-indigo-50 text-slate-700 italic">${text}</blockquote>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// List items starting with emojis (✅, ❌, etc.) - doit être AVANT les listes normales
|
||||
const emojiListMatch = line.match(/^[\s]*(✅|❌|⚠️|📊|📈|🔍|🎯|⭐|📐|💡|🎓|🔴|🟣)\s+(.+)$/);
|
||||
if (emojiListMatch) {
|
||||
if (!inList || listType !== 'ul') {
|
||||
flushList();
|
||||
inList = true;
|
||||
listType = 'ul';
|
||||
}
|
||||
listItems.push(`<li class="text-slate-700 flex items-start gap-2"><span>${emojiListMatch[1]}</span><span>${parseInline(emojiListMatch[2])}</span></li>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unordered list
|
||||
const ulMatch = line.match(/^[\s]*[-*]\s+(.+)$/);
|
||||
if (ulMatch) {
|
||||
if (!inList || listType !== 'ul') {
|
||||
flushList();
|
||||
inList = true;
|
||||
listType = 'ul';
|
||||
}
|
||||
listItems.push(`<li class="text-slate-700">${parseInline(ulMatch[1])}</li>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ordered list
|
||||
const olMatch = line.match(/^[\s]*(\d+)\.\s+(.+)$/);
|
||||
if (olMatch) {
|
||||
if (!inList || listType !== 'ol') {
|
||||
flushList();
|
||||
inList = true;
|
||||
listType = 'ol';
|
||||
}
|
||||
listItems.push(`<li class="text-slate-700">${parseInline(olMatch[2])}</li>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not a list item
|
||||
if (inList) {
|
||||
flushList();
|
||||
}
|
||||
|
||||
// Empty line
|
||||
if (line.trim() === '') {
|
||||
output.push('');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular paragraph
|
||||
output.push(`<p class="my-4 text-slate-700 leading-relaxed">${parseInline(line)}</p>`);
|
||||
}
|
||||
|
||||
// Flush remaining content
|
||||
flushList();
|
||||
flushTable();
|
||||
|
||||
// Math blocks
|
||||
let result = output.join('\n');
|
||||
result = result.replace(/\$\$([^$]+)\$\$/g, (match, math) => {
|
||||
return `<div class="my-4 p-4 bg-slate-50 rounded-lg overflow-x-auto"><span class="font-mono text-sm">${math.trim()}</span></div>`;
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50">
|
||||
{/* Header */}
|
||||
<header className="bg-white border-b border-slate-200 sticky top-0 z-50 shadow-sm">
|
||||
<div className="max-w-4xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 text-slate-600 hover:text-indigo-600 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
Retour
|
||||
</Link>
|
||||
<div className="h-6 w-px bg-slate-200" />
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpen className="w-5 h-5 text-indigo-600" />
|
||||
<span className="font-semibold text-slate-900">Documentation</span>
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 text-sm text-slate-500 hover:text-indigo-600 transition-colors"
|
||||
>
|
||||
<Home className="w-4 h-4" />
|
||||
Accueil
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<main className="max-w-4xl mx-auto px-6 py-12">
|
||||
<article
|
||||
className="bg-white rounded-2xl shadow-sm border border-slate-200 p-8 md:p-12 prose prose-slate max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: parseMarkdown(content) }}
|
||||
/>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-8 text-center">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 text-white font-semibold rounded-xl hover:from-indigo-700 hover:to-purple-700 transition-all shadow-md hover:shadow-lg"
|
||||
>
|
||||
<Home className="w-4 h-4" />
|
||||
Retour à l'application
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
frontend/src/app/globals.css
Normal file
46
frontend/src/app/globals.css
Normal file
@@ -0,0 +1,46 @@
|
||||
@import "tailwindcss";
|
||||
@import "katex/dist/katex.min.css";
|
||||
|
||||
@theme {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-inter);
|
||||
--font-mono: var(--font-mono);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #f8fafc; /* Slate-50 */
|
||||
--foreground: #0f172a; /* Slate-900 */
|
||||
|
||||
--font-inter: 'Inter', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #020617; /* Slate-950 */
|
||||
--foreground: #f8fafc; /* Slate-50 */
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
/* Custom Scrollbar for the Grid */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
35
frontend/src/app/layout.tsx
Normal file
35
frontend/src/app/layout.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, JetBrains_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const mono = JetBrains_Mono({
|
||||
variable: "--font-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Data_analysis",
|
||||
description: "Modern Statistical Tool",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css" />
|
||||
</head>
|
||||
<body className={`${inter.variable} ${mono.variable} font-sans antialiased bg-slate-50 text-slate-900`}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
323
frontend/src/app/page.tsx
Normal file
323
frontend/src/app/page.tsx
Normal file
@@ -0,0 +1,323 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { FileUploader } from "@/features/uploader/components/FileUploader";
|
||||
import { SmartGrid } from "@/features/smart-grid/components/SmartGrid";
|
||||
import { InsightPanel } from "@/features/insight-panel/components/InsightPanel";
|
||||
import { CorrelationHeatmap } from "@/features/analysis/components/CorrelationHeatmap";
|
||||
import { AnalysisConfiguration } from "@/features/analysis/components/AnalysisConfiguration";
|
||||
import { AnalysisResults } from "@/features/analysis/components/AnalysisResults";
|
||||
import { HelpButton, HelpPanel, WelcomeTour } from "@/features/help";
|
||||
import { useGridStore } from "@/store/use-grid-store";
|
||||
import { useStore } from "zustand";
|
||||
import { Undo2, Redo2, Wand2, Table as TableIcon, GitBranch, LineChart, Download, AlertTriangle, HelpCircle, BookOpen } from "lucide-react";
|
||||
import { cn, safeJSONStringify } from "@/lib/utils";
|
||||
import { getApiUrl } from "@/lib/api-config";
|
||||
|
||||
export default function Home() {
|
||||
const {
|
||||
data,
|
||||
columns,
|
||||
error,
|
||||
setError,
|
||||
setOutliers,
|
||||
setMultivariateOutliers,
|
||||
getCleanData,
|
||||
targetVariable,
|
||||
selectedFeatures,
|
||||
modelType,
|
||||
polyDegree,
|
||||
includeInteractions,
|
||||
setAnalysisResults,
|
||||
analysisResults,
|
||||
excludedRows
|
||||
} = useGridStore();
|
||||
const [insightCol, setInsightCol] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<"grid" | "correlation" | "results">("grid");
|
||||
const [isConfigOpen, setIsConfigOpen] = useState(false);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
||||
|
||||
const temporalStore = (useGridStore as any).temporal;
|
||||
const { undo, redo, pastStates, futureStates } = useStore(temporalStore, (state: any) => state);
|
||||
|
||||
// Trigger Outlier Detection on data change OR when rows are excluded
|
||||
useEffect(() => {
|
||||
if (data.length > 0 && columns.length > 0) {
|
||||
const triggerDetection = async () => {
|
||||
const numericCols = columns.filter(c => c.type === 'numeric').map(c => c.name);
|
||||
if (numericCols.length === 0) return;
|
||||
|
||||
// Send original data + excluded indices to backend
|
||||
// Backend will filter excluded rows and return outliers with correct original indices
|
||||
const excludedIndicesList = Array.from(excludedRows);
|
||||
|
||||
try {
|
||||
const response = await fetch(getApiUrl("/analysis/detect-outliers"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: safeJSONStringify({
|
||||
data: data,
|
||||
columns: numericCols,
|
||||
method: "both",
|
||||
excluded_indices: excludedIndicesList
|
||||
})
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
// NEW STRUCTURED APPROACH:
|
||||
// 1. Store univariate outliers per column (column-specific)
|
||||
if (result.univariate) {
|
||||
numericCols.forEach(col => {
|
||||
const colOutliers = result.univariate[col] || [];
|
||||
setOutliers(col, colOutliers);
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Store multivariate outliers separately (global anomalies)
|
||||
if (result.multivariate) {
|
||||
setMultivariateOutliers(result.multivariate);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Outlier detection failed", e);
|
||||
}
|
||||
};
|
||||
triggerDetection();
|
||||
}
|
||||
}, [data, columns, excludedRows, setOutliers, setMultivariateOutliers]);
|
||||
|
||||
const onRunRegression = useCallback(async () => {
|
||||
if (!targetVariable || selectedFeatures.length === 0) {
|
||||
setError("Veuillez sélectionner une cible et au moins une feature.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRunning(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
data: getCleanData(),
|
||||
x_features: selectedFeatures,
|
||||
y_target: targetVariable,
|
||||
model_type: modelType,
|
||||
poly_degree: polyDegree,
|
||||
include_interactions: includeInteractions
|
||||
};
|
||||
|
||||
const response = await fetch(getApiUrl("/analysis/run-regression"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: safeJSONStringify(payload)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.detail || "Erreur lors de l'exécution du modèle.");
|
||||
}
|
||||
|
||||
if (result.status === "ok") {
|
||||
setAnalysisResults({ ...result.results, model_type: modelType });
|
||||
setIsConfigOpen(false);
|
||||
setActiveTab("results");
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("Regression failed", e);
|
||||
setError(`Erreur d'analyse: ${e.message}`);
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
}, [getCleanData, targetVariable, selectedFeatures, modelType, setAnalysisResults, setError]);
|
||||
|
||||
const onShowCorrelation = useCallback(() => {
|
||||
setActiveTab("correlation");
|
||||
}, [setActiveTab]);
|
||||
|
||||
const onExportPDF = useCallback(async () => {
|
||||
if (!analysisResults) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(getApiUrl("/reports/export"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: safeJSONStringify({
|
||||
project_name: "Analysis_Result",
|
||||
results: analysisResults,
|
||||
audit_trail: {
|
||||
excluded_rows_count: excludedRows.size
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `Report_Data_analysis_${Date.now()}.pdf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
} catch (e) {
|
||||
console.error("Export failed", e);
|
||||
}
|
||||
}, [analysisResults, excludedRows.size]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-slate-50 font-sans selection:bg-indigo-100 selection:text-indigo-900">
|
||||
|
||||
{/*Navbar with Glassmorphism */}
|
||||
<header className="h-16 border-b border-slate-200 bg-white/80 backdrop-blur-md sticky top-0 z-50 flex items-center justify-between px-8 shadow-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg flex items-center justify-center text-white font-bold shadow-md shadow-indigo-200">
|
||||
DA
|
||||
</div>
|
||||
<h1 className="text-lg font-bold text-slate-800 tracking-tight">Data_analysis</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center bg-slate-100/50 p-1 rounded-lg border border-slate-200/50">
|
||||
<button
|
||||
onClick={() => undo()}
|
||||
disabled={pastStates.length === 0}
|
||||
className="p-2 text-slate-500 hover:text-indigo-600 hover:bg-white disabled:opacity-30 rounded-md transition-all"
|
||||
title="Annuler (Ctrl+Z)"
|
||||
>
|
||||
<Undo2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => redo()}
|
||||
disabled={futureStates.length === 0}
|
||||
className="p-2 text-slate-500 hover:text-indigo-600 hover:bg-white disabled:opacity-30 rounded-md transition-all"
|
||||
title="Rétablir (Ctrl+Y)"
|
||||
>
|
||||
<Redo2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Help Button */}
|
||||
<div className="relative">
|
||||
<button
|
||||
data-tour-target="help-button"
|
||||
onClick={() => setIsHelpOpen(!isHelpOpen)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-bold transition-all border",
|
||||
isHelpOpen
|
||||
? "bg-indigo-100 text-indigo-700 border-indigo-300"
|
||||
: "text-slate-600 hover:text-indigo-600 hover:bg-indigo-50 border-slate-200 bg-white"
|
||||
)}
|
||||
title="Ouvrir l'aide"
|
||||
>
|
||||
<BookOpen className="w-4 h-4" />
|
||||
Aide
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{analysisResults && (
|
||||
<button
|
||||
onClick={onExportPDF}
|
||||
className="flex items-center gap-2 text-slate-600 hover:text-indigo-600 hover:bg-indigo-50 px-4 py-2 rounded-lg text-xs font-bold transition-all border border-slate-200 bg-white shadow-sm"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
Rapport PDF
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
data-tour-target="analyze-button"
|
||||
onClick={() => setIsConfigOpen(!isConfigOpen)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-bold transition-all shadow-lg hover:shadow-xl hover:-translate-y-0.5 active:translate-y-0",
|
||||
isConfigOpen
|
||||
? "bg-slate-800 text-white shadow-slate-200"
|
||||
: "bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-indigo-200"
|
||||
)}
|
||||
>
|
||||
<Wand2 className="w-4 h-4" />
|
||||
{isConfigOpen ? "Fermer Config" : "Lancer Analyse"}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Layout */}
|
||||
<main className="flex-1 flex flex-row overflow-hidden relative bg-slate-50">
|
||||
<div className="flex-1 flex flex-col p-6 overflow-hidden gap-6 max-w-[1600px] mx-auto w-full">
|
||||
|
||||
{/* Top Section: Upload & Tabs */}
|
||||
<div className="flex flex-col md:flex-row items-stretch gap-6">
|
||||
<div className="flex-1 min-w-[300px]" data-tour-target="upload">
|
||||
<FileUploader />
|
||||
</div>
|
||||
|
||||
{/* Elegant Tabs */}
|
||||
<div className="flex bg-white p-1.5 rounded-2xl shadow-sm border border-slate-100 h-fit self-center" data-tour-target="tabs">
|
||||
{[{"id": "grid", "label": "Données", "icon": TableIcon },
|
||||
{"id": "correlation", "label": "Corrélations", "icon": GitBranch },
|
||||
{"id": "results", "label": "Résultats", "icon": LineChart },].map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as any)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-6 py-3 rounded-xl text-xs font-bold transition-all",
|
||||
activeTab === tab.id
|
||||
? "bg-indigo-50 text-indigo-700 shadow-sm ring-1 ring-indigo-100"
|
||||
: "text-slate-500 hover:text-slate-900 hover:bg-slate-50"
|
||||
)}
|
||||
>
|
||||
<tab.icon className="w-4 h-4" />
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Banner */}
|
||||
{error && (
|
||||
<div className="flex items-center gap-3 p-4 bg-rose-50 border border-rose-100 text-rose-700 rounded-xl shadow-sm animate-in slide-in-from-top-2">
|
||||
<AlertTriangle className="w-5 h-5 shrink-0" />
|
||||
<p className="text-sm font-medium">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content Area */}
|
||||
<section className="flex-1 bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden relative">
|
||||
{activeTab === "grid" && (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="h-12 border-b border-slate-100 flex items-center justify-between px-6 bg-slate-50/50">
|
||||
<h2 className="text-xs font-bold text-slate-400 uppercase tracking-widest flex items-center gap-2">
|
||||
<TableIcon className="w-3 h-3" />
|
||||
Vue Tabulaire
|
||||
</h2>
|
||||
{data.length > 0 && (
|
||||
<span className="text-[10px] font-mono font-medium text-slate-500 bg-white px-2 py-1 rounded border border-slate-200 shadow-sm">
|
||||
{data.length.toLocaleString()} rows • {columns.length} cols
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div data-tour-target="grid">
|
||||
<SmartGrid onShowInsights={(col) => setInsightCol(col)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "correlation" && (
|
||||
<div className="p-6 h-full overflow-auto">
|
||||
<CorrelationHeatmap />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "results" && (
|
||||
<div className="p-6 h-full overflow-auto">
|
||||
<AnalysisResults />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Overlays */}
|
||||
{isConfigOpen && <AnalysisConfiguration onRun={onRunRegression} onShowCorrelation={onShowCorrelation} />}
|
||||
{insightCol && <InsightPanel colName={insightCol} onClose={() => setInsightCol(null)} />}
|
||||
<HelpPanel open={isHelpOpen} onOpenChange={setIsHelpOpen} />
|
||||
<WelcomeTour />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState, useMemo } from "react";
|
||||
import { useGridStore, ModelType } from "@/store/use-grid-store";
|
||||
import { Wand2, Target, CheckCircle2, ChevronRight, BarChart3, AlertCircle, Settings2, GitBranch, Lightbulb, Sparkles } from "lucide-react";
|
||||
import { cn, safeJSONStringify } from "@/lib/utils";
|
||||
import { getApiUrl } from "@/lib/api-config";
|
||||
import { analyzeAndRecommendModel, ModelRecommendation } from "@/lib/model-recommendation";
|
||||
|
||||
interface AnalysisConfigurationProps {
|
||||
onRun: () => void;
|
||||
onShowCorrelation?: () => void;
|
||||
}
|
||||
|
||||
export function AnalysisConfiguration({ onRun, onShowCorrelation }: AnalysisConfigurationProps) {
|
||||
const {
|
||||
columns,
|
||||
getCleanData,
|
||||
targetVariable,
|
||||
setTargetVariable,
|
||||
selectedFeatures,
|
||||
setSelectedFeatures,
|
||||
modelType,
|
||||
setModelType,
|
||||
polyDegree,
|
||||
setPolyDegree,
|
||||
includeInteractions,
|
||||
setIncludeInteractions
|
||||
} = useGridStore();
|
||||
|
||||
const [importances, setImportances] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showCorrelationHint, setShowCorrelationHint] = useState(false);
|
||||
const [recommendation, setRecommendation] = useState<ModelRecommendation | null>(null);
|
||||
const [autoApplyRecommendation, setAutoApplyRecommendation] = useState(true);
|
||||
|
||||
const numericCols = columns.filter(c => c.type === 'numeric');
|
||||
const targetCol = columns.find(c => c.name === targetVariable);
|
||||
|
||||
// Validation Logic
|
||||
const validationError = useMemo(() => {
|
||||
if (!targetCol) return null;
|
||||
if (['linear', 'polynomial', 'exponential'].includes(modelType) && targetCol.type !== 'numeric') {
|
||||
return "Ce modèle nécessite une cible numérique continue.";
|
||||
}
|
||||
if (modelType === 'logistic') {
|
||||
if (targetCol.type !== 'categorical' && targetCol.type !== 'boolean') {
|
||||
return "La régression logistique nécessite une cible catégorique ou binaire.";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [targetCol, modelType]);
|
||||
|
||||
const isReady = targetVariable && selectedFeatures.length > 0 && !validationError;
|
||||
|
||||
useEffect(() => {
|
||||
if (!targetVariable || numericCols.length < 2) {
|
||||
setImportances([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchImportance = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const features = numericCols.map(c => c.name).filter(n => n !== targetVariable);
|
||||
const response = await fetch(getApiUrl("/analysis/feature-importance"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: safeJSONStringify({ data: getCleanData(), features, target: targetVariable })
|
||||
});
|
||||
const result = await response.json();
|
||||
setImportances(result.importances);
|
||||
setSelectedFeatures(result.importances.slice(0, 5).map((i: any) => i.feature));
|
||||
} catch (e) {
|
||||
console.error("Feature importance failed", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchImportance();
|
||||
}, [targetVariable, getCleanData, numericCols.length, setSelectedFeatures]);
|
||||
|
||||
// Show correlation hint when target is selected
|
||||
useEffect(() => {
|
||||
if (targetVariable && numericCols.length >= 2) {
|
||||
setShowCorrelationHint(true);
|
||||
}
|
||||
}, [targetVariable, numericCols.length]);
|
||||
|
||||
// Auto-detect best model type when target changes
|
||||
useEffect(() => {
|
||||
if (!targetVariable) {
|
||||
setRecommendation(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Analyser et recommander le meilleur modèle
|
||||
const rec = analyzeAndRecommendModel(
|
||||
useGridStore.getState().data,
|
||||
columns,
|
||||
targetVariable
|
||||
);
|
||||
|
||||
if (rec && autoApplyRecommendation) {
|
||||
// Appliquer automatiquement les recommandations
|
||||
if (rec.recommendedType !== modelType) {
|
||||
setModelType(rec.recommendedType);
|
||||
}
|
||||
if (rec.polynomialDegree && rec.polynomialDegree !== polyDegree) {
|
||||
setPolyDegree(rec.polynomialDegree);
|
||||
}
|
||||
if (rec.recommendedInteractions.length > 0 && !includeInteractions) {
|
||||
setIncludeInteractions(true);
|
||||
}
|
||||
}
|
||||
|
||||
setRecommendation(rec);
|
||||
}, [targetVariable, columns, modelType, polyDegree, includeInteractions, setModelType, setPolyDegree, setIncludeInteractions, autoApplyRecommendation]);
|
||||
|
||||
return (
|
||||
<div className="w-[350px] bg-white border-l border-slate-200 h-full flex flex-col shadow-2xl z-30 animate-in slide-in-from-right duration-300">
|
||||
<header className="p-4 border-b border-slate-100 flex items-center gap-2">
|
||||
<Target className="w-5 h-5 text-indigo-600" />
|
||||
<h3 className="font-bold text-slate-800 text-sm">Configuration Avancée</h3>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-8">
|
||||
{/* Step 0: Model Type */}
|
||||
<section className="space-y-3">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest flex items-center gap-2">
|
||||
<BarChart3 className="w-3 h-3" />
|
||||
Modèle & Complexité
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2 bg-slate-100 p-1 rounded-xl mb-4">
|
||||
{(['linear', 'logistic', 'polynomial', 'exponential'] as const).map((type) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setModelType(type)}
|
||||
className={cn(
|
||||
"py-1.5 rounded-lg text-[11px] font-bold transition-all capitalize",
|
||||
modelType === type ? "bg-white text-indigo-600 shadow-sm" : "text-slate-500 hover:text-slate-700"
|
||||
)}
|
||||
>
|
||||
{type}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Advanced Options for Polynomial/Linear */}
|
||||
{(modelType === 'polynomial' || modelType === 'linear') && (
|
||||
<div className="bg-slate-50 p-3 rounded-lg border border-slate-100 space-y-3 animate-in fade-in">
|
||||
{modelType === 'polynomial' && (
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-medium text-slate-700">Degré du Polynôme</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setPolyDegree(Math.max(2, polyDegree - 1))}
|
||||
className="w-6 h-6 bg-white border rounded text-xs hover:bg-slate-50"
|
||||
>-</button>
|
||||
<span className="text-xs font-mono font-bold w-4 text-center">{polyDegree}</span>
|
||||
<button
|
||||
onClick={() => setPolyDegree(Math.min(5, polyDegree + 1))}
|
||||
className="w-6 h-6 bg-white border rounded text-xs hover:bg-slate-50"
|
||||
>+</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="interactions"
|
||||
checked={includeInteractions}
|
||||
onChange={(e) => setIncludeInteractions(e.target.checked)}
|
||||
className="rounded text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
<label htmlFor="interactions" className="text-xs text-slate-600">
|
||||
Inclure intéractions (x*y)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* AI Recommendation Section */}
|
||||
{recommendation && (
|
||||
<section className="animate-in fade-in slide-in-from-top-2 duration-500">
|
||||
<div className={cn(
|
||||
"p-4 rounded-xl border space-y-3",
|
||||
recommendation.recommendedType === modelType
|
||||
? "bg-gradient-to-br from-emerald-50 to-teal-50 border-emerald-200"
|
||||
: "bg-gradient-to-br from-amber-50 to-orange-50 border-amber-200"
|
||||
)}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={cn(
|
||||
"p-2 rounded-lg",
|
||||
recommendation.recommendedType === modelType
|
||||
? "bg-emerald-100 text-emerald-600"
|
||||
: "bg-amber-100 text-amber-600"
|
||||
)}>
|
||||
<Sparkles className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<p className="text-sm font-bold text-slate-800">
|
||||
{recommendation.recommendedType === modelType
|
||||
? "✅ Configuration optimale détectée"
|
||||
: "💡 Recommandation IA"}
|
||||
</p>
|
||||
{recommendation.confidence >= 0.8 && (
|
||||
<span className="text-[9px] px-2 py-0.5 bg-emerald-100 text-emerald-700 rounded-full font-medium">
|
||||
{Math.round(recommendation.confidence * 100)}% confiant
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{recommendation.recommendedType !== modelType && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setModelType(recommendation.recommendedType);
|
||||
if (recommendation.polynomialDegree) setPolyDegree(recommendation.polynomialDegree);
|
||||
}}
|
||||
className="text-[10px] text-emerald-700 hover:text-emerald-800 font-medium underline mb-2"
|
||||
>
|
||||
Appliquer la recommandation →
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="space-y-1 text-[10px] text-slate-700 leading-relaxed">
|
||||
{recommendation.reasons.map((reason, i) => (
|
||||
<p key={i}>{reason}</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{recommendation.recommendedInteractions.length > 0 && (
|
||||
<div className="mt-2 p-2 bg-white/50 rounded-lg">
|
||||
<p className="text-[9px] font-medium text-slate-600 mb-1">
|
||||
🔗 Interactions suggérées :
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{recommendation.recommendedInteractions.map((pair, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="text-[9px] px-2 py-0.5 bg-indigo-100 text-indigo-700 rounded"
|
||||
>
|
||||
{pair[0]} × {pair[1]}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{!includeInteractions && (
|
||||
<button
|
||||
onClick={() => setIncludeInteractions(true)}
|
||||
className="text-[9px] text-indigo-600 hover:text-indigo-800 underline mt-1"
|
||||
>
|
||||
Activer les interactions →
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{recommendation.warnings.length > 0 && (
|
||||
<div className="mt-2 p-2 bg-amber-100/50 rounded-lg">
|
||||
{recommendation.warnings.map((warning, i) => (
|
||||
<p key={i} className="text-[9px] text-amber-800">{warning}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 mt-2 pt-2 border-t border-black/5">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="auto-apply"
|
||||
checked={autoApplyRecommendation}
|
||||
onChange={(e) => setAutoApplyRecommendation(e.target.checked)}
|
||||
className="rounded text-emerald-600 focus:ring-emerald-500 text-xs"
|
||||
/>
|
||||
<label htmlFor="auto-apply" className="text-[10px] text-slate-600 cursor-pointer">
|
||||
Appliquer automatiquement les recommandations
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Step 1: Select Y */}
|
||||
<section className="space-y-3">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest flex items-center gap-2">
|
||||
<span className="w-4 h-4 bg-slate-100 rounded-full flex items-center justify-center text-slate-500 text-[9px]">1</span>
|
||||
Variable Cible (Y)
|
||||
</p>
|
||||
<select
|
||||
className={cn(
|
||||
"w-full p-2 bg-slate-50 border rounded-lg text-sm outline-none transition-all",
|
||||
validationError ? "border-rose-300 text-rose-600" : "border-slate-200 focus:border-indigo-500"
|
||||
)}
|
||||
value={targetVariable || ""}
|
||||
onChange={(e) => setTargetVariable(e.target.value || null)}
|
||||
>
|
||||
<option value="">Sélectionner une cible...</option>
|
||||
{columns.map(c => (
|
||||
<option key={c.name} value={c.name}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
{validationError && (
|
||||
<div className="flex items-start gap-2 p-2 bg-rose-50 text-[10px] text-rose-600 rounded-lg border border-rose-100">
|
||||
<AlertCircle className="w-3 h-3 shrink-0 mt-0.5" />
|
||||
<span>{validationError}</span>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Step 2: Select X with recommendations */}
|
||||
{targetVariable && (
|
||||
<section className="space-y-4 animate-in fade-in slide-in-from-top-2 duration-500">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest flex items-center gap-2">
|
||||
<span className="w-4 h-4 bg-slate-100 rounded-full flex items-center justify-center text-slate-500 text-[9px]">2</span>
|
||||
Prédicteurs (X)
|
||||
</p>
|
||||
<Wand2 className={cn("w-4 h-4 text-indigo-400", loading && "animate-spin")} />
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="space-y-2">
|
||||
{[1,2,3].map(i => <div key={i} className="h-8 bg-slate-50 animate-pulse rounded-lg" />)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{importances.length > 0 ? importances.map((imp) => (
|
||||
<label
|
||||
key={imp.feature}
|
||||
className={cn(
|
||||
"flex flex-col gap-1 p-2 rounded-lg border cursor-pointer transition-all",
|
||||
selectedFeatures.includes(imp.feature) ? "bg-indigo-50 border-indigo-200" : "bg-white border-slate-100 hover:border-slate-200"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded text-indigo-600 border-slate-300 focus:ring-indigo-500"
|
||||
checked={selectedFeatures.includes(imp.feature)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) setSelectedFeatures([...selectedFeatures, imp.feature]);
|
||||
else setSelectedFeatures(selectedFeatures.filter(f => f !== imp.feature));
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs font-medium text-slate-700 flex-1">{imp.feature}</span>
|
||||
<span className="text-[10px] font-mono text-slate-400">{(imp.score * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="h-1 w-full bg-slate-100 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-indigo-400" style={{ width: `${imp.score * 100}%` }} />
|
||||
</div>
|
||||
</label>
|
||||
)) : (
|
||||
<div className="text-[10px] text-slate-400 italic text-center p-4">
|
||||
Aucune recommandation disponible. Sélectionnez des prédicteurs manuellement.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Correlation Hint - Show when target is selected */}
|
||||
{showCorrelationHint && onShowCorrelation && (
|
||||
<section className="animate-in fade-in slide-in-from-top-2 duration-500">
|
||||
<div className="bg-gradient-to-br from-indigo-50 to-purple-50 p-4 rounded-xl border border-indigo-100 space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<GitBranch className="w-5 h-5 text-indigo-600 shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="text-xs font-bold text-slate-800 mb-1">
|
||||
Analysez les corrélations avant de lancer
|
||||
</p>
|
||||
<p className="text-[10px] text-slate-600 leading-relaxed">
|
||||
Visualisez la matrice de corrélation pour identifier les relations entre variables
|
||||
et éviter la multicolinéarité.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onShowCorrelation}
|
||||
className="w-full py-2.5 bg-white border border-indigo-200 rounded-lg text-xs font-bold text-indigo-700 hover:bg-indigo-50 transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<GitBranch className="w-3.5 h-3.5" />
|
||||
Voir la matrice de corrélation
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<footer className="p-6 border-t border-slate-100 bg-slate-50/50">
|
||||
<button
|
||||
disabled={!isReady}
|
||||
onClick={onRun}
|
||||
className={cn(
|
||||
"w-full py-3 rounded-xl text-sm font-bold shadow-lg flex items-center justify-center gap-2 transition-all",
|
||||
isReady ? "bg-indigo-600 text-white hover:bg-indigo-700 shadow-indigo-100" : "bg-slate-200 text-slate-400 cursor-not-allowed shadow-none"
|
||||
)}
|
||||
>
|
||||
Lancer l'Analyse
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
390
frontend/src/features/analysis/components/AnalysisResults.tsx
Normal file
390
frontend/src/features/analysis/components/AnalysisResults.tsx
Normal file
@@ -0,0 +1,390 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useState, useEffect, useRef } from "react";
|
||||
import { useGridStore } from "@/store/use-grid-store";
|
||||
import {
|
||||
ScatterChart, Scatter, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||
ReferenceLine, Legend, ComposedChart, Line
|
||||
} from "recharts";
|
||||
import { Activity, Percent, Database, TrendingUp, CheckCircle2, XCircle, Info, Copy, Check } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import katex from "katex";
|
||||
|
||||
// Custom tooltip with 2 decimal precision
|
||||
const CustomTooltip = ({ active, payload, label }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-white p-3 border border-slate-200 rounded-lg shadow-lg">
|
||||
<p className="text-xs font-semibold text-slate-700 mb-2">{`X: ${label?.toFixed(2)}`}</p>
|
||||
{payload.map((entry: any, index: number) => (
|
||||
<p key={index} className="text-[10px]" style={{ color: entry.color }}>
|
||||
{`${entry.name}: ${typeof entry.value === 'number' ? entry.value.toFixed(2) : entry.value}`}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// LaTeX Renderer Component using KaTeX
|
||||
function LatexRenderer({ latex, displayMode = true }: { latex: string; displayMode?: boolean }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current && latex) {
|
||||
try {
|
||||
katex.render(latex, containerRef.current, {
|
||||
displayMode,
|
||||
throwOnError: false,
|
||||
output: 'html',
|
||||
trust: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('KaTeX render error:', error);
|
||||
if (containerRef.current) {
|
||||
containerRef.current.textContent = latex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [latex, displayMode]);
|
||||
|
||||
return <div ref={containerRef} className="overflow-x-auto" />;
|
||||
}
|
||||
|
||||
// Helper component for copy button
|
||||
function CopyButton({ text, label }: { text: string; label: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-2 px-3 py-2 text-xs font-semibold text-slate-600 bg-slate-50 hover:bg-slate-100 rounded-lg border border-slate-200 transition-colors"
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4 text-emerald-500" /> : <Copy className="w-4 h-4" />}
|
||||
{copied ? "Copié!" : label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function AnalysisResults() {
|
||||
const { analysisResults, modelType, selectedFeatures } = useGridStore();
|
||||
const [selectedPartialFeature, setSelectedPartialFeature] = useState<string>("");
|
||||
|
||||
const { r_squared, adj_r_squared, coefficients, sample_size, fit_plot, fit_plots_by_feature, partial_regression_plots, diagnostic_plot, equations } = (analysisResults || {}) as any;
|
||||
const primaryFeature = selectedFeatures[0] || "X";
|
||||
|
||||
// Initialize selected partial feature on first load
|
||||
const partialFeatures = useMemo(() => {
|
||||
return partial_regression_plots ? Object.keys(partial_regression_plots) : [];
|
||||
}, [partial_regression_plots]);
|
||||
|
||||
// Auto-select first feature if none selected
|
||||
React.useEffect(() => {
|
||||
if (partialFeatures.length > 0 && !selectedPartialFeature) {
|
||||
setSelectedPartialFeature(partialFeatures[0]);
|
||||
}
|
||||
}, [partialFeatures, selectedPartialFeature]);
|
||||
|
||||
if (!analysisResults) return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-slate-400 gap-4">
|
||||
<TrendingUp className="w-12 h-12 opacity-20" />
|
||||
<p>Lancez une régression pour voir les résultats ici.</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Check if multivariate regression
|
||||
const isMultivariate = selectedFeatures.length > 1;
|
||||
|
||||
// Get equations from backend or generate fallback
|
||||
const pythonEq = equations?.python || "";
|
||||
const excelEq = equations?.excel || "";
|
||||
const latexEqRaw = equations?.latex || "";
|
||||
|
||||
// Data preparation for the Parity Plot (Real vs Pred)
|
||||
// We construct this from fit_plot data
|
||||
const parityData = fit_plot ? fit_plot.map((d: any) => ({
|
||||
real: d.real,
|
||||
pred: d.pred
|
||||
})) : [];
|
||||
|
||||
// Calculate domains for Parity Plot to ensure square aspect ratio logic
|
||||
const allParityValues = parityData.flatMap((d: any) => [d.real, d.pred]);
|
||||
const minParity = Math.min(...allParityValues);
|
||||
const maxParity = Math.max(...allParityValues);
|
||||
const padding = (maxParity - minParity) * 0.05;
|
||||
const parityDomain = [minParity - padding, maxParity + padding];
|
||||
|
||||
// Prepare fit plots for rendering (use multi-feature plots if available)
|
||||
const fitPlotsToRender = fit_plots_by_feature && Object.keys(fit_plots_by_feature).length > 0
|
||||
? Object.entries(fit_plots_by_feature).map(([featureName, plotData]) => ({
|
||||
featureName,
|
||||
plotData: plotData as any[]
|
||||
}))
|
||||
: [{ featureName: primaryFeature, plotData: fit_plot || [] }];
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto space-y-8 p-4 pb-20">
|
||||
{/* 1. Metrics Overview */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">R-Squared</p>
|
||||
<Activity className="w-4 h-4 text-indigo-500" />
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-slate-900">{Number(r_squared).toFixed(4)}</p>
|
||||
<p className="text-[10px] text-slate-500 mt-1">Précision (1.0 = Parfait)</p>
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Adj. R-Squared</p>
|
||||
<Percent className="w-4 h-4 text-indigo-500" />
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-slate-900">{adj_r_squared ? Number(adj_r_squared).toFixed(4) : "N/A"}</p>
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Échantillon</p>
|
||||
<Database className="w-4 h-4 text-indigo-500" />
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-slate-900">{sample_size}</p>
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Modèle</p>
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-500" />
|
||||
</div>
|
||||
<p className="text-lg font-bold text-slate-900 uppercase truncate" title={modelType}>{modelType}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Equation Display and Copy Section */}
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
<header className="p-4 border-b border-slate-100 bg-slate-50/50">
|
||||
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-widest">Équation du Modèle</h3>
|
||||
</header>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* LaTeX Display */}
|
||||
<div className="bg-slate-50 rounded-xl p-6 border border-slate-200">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-3">Format Mathématique</p>
|
||||
<div className="text-center py-4">
|
||||
<LatexRenderer latex={latexEqRaw || "\text{Chargement...}"} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Copy Buttons */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Python Format */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Python / Code</p>
|
||||
<div className="bg-slate-900 rounded-lg p-4 font-mono text-xs text-emerald-400 break-all">
|
||||
{pythonEq}
|
||||
</div>
|
||||
<CopyButton text={pythonEq} label="Copier Python" />
|
||||
</div>
|
||||
|
||||
{/* Excel Format */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Excel / Google Sheets</p>
|
||||
<div className="bg-slate-900 rounded-lg p-4 font-mono text-xs text-blue-400 break-all">
|
||||
{excelEq}
|
||||
</div>
|
||||
<CopyButton text={excelEq} label="Copier Excel" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* Fit Plots - Only for single variable regression */}
|
||||
{!isMultivariate && fitPlotsToRender.length > 0 && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{fitPlotsToRender.map(({ featureName, plotData }) => (
|
||||
<div key={featureName} className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-widest">
|
||||
Ajustement : {featureName} vs Cible
|
||||
</h3>
|
||||
<p className="text-[11px] text-slate-400">
|
||||
La ligne montre la fonction modélisée par rapport aux données réelles (points).
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-80 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart data={plotData} margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
|
||||
<XAxis
|
||||
type="number"
|
||||
dataKey="x"
|
||||
name={featureName}
|
||||
label={{ value: featureName, position: 'bottom', offset: 0, fontSize: 10 }}
|
||||
domain={['auto', 'auto']}
|
||||
tickFormatter={(value) => value.toFixed(2)}
|
||||
tick={{ fontSize: 9 }}
|
||||
/>
|
||||
<YAxis
|
||||
type="number"
|
||||
label={{ value: 'Cible', angle: -90, position: 'left', offset: 0, fontSize: 10 }}
|
||||
domain={['auto', 'auto']}
|
||||
tickFormatter={(value) => value.toFixed(2)}
|
||||
tick={{ fontSize: 9 }}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} cursor={{ strokeDasharray: '3 3' }} />
|
||||
<Legend wrapperStyle={{fontSize: '11px'}} />
|
||||
<Scatter name="Données Réelles" dataKey="real" fill="#94a3b8" opacity={0.5} shape="circle" />
|
||||
<Line name="Modèle (Fit)" type="monotone" dataKey="pred" stroke="#6366f1" strokeWidth={2} dot={false} />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Partial Regression Plot - Only for multivariate regression */}
|
||||
{isMultivariate && partialFeatures.length > 0 && (
|
||||
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div>
|
||||
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-widest">
|
||||
Partial Regression Plot
|
||||
</h3>
|
||||
<p className="text-[11px] text-slate-400 mt-1">
|
||||
Effet isolé de chaque variable, contrôlant les autres. La pente montre l'impact réel de la variable choisie.
|
||||
</p>
|
||||
</div>
|
||||
<select
|
||||
value={selectedPartialFeature}
|
||||
onChange={(e) => setSelectedPartialFeature(e.target.value)}
|
||||
className="px-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent bg-white"
|
||||
>
|
||||
{partialFeatures.map((feature) => (
|
||||
<option key={feature} value={feature}>
|
||||
{feature}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-96 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart
|
||||
data={partial_regression_plots[selectedPartialFeature] || []}
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
|
||||
<XAxis
|
||||
type="number"
|
||||
dataKey="x"
|
||||
label={{ value: `${selectedPartialFeature} (résidus)`, position: 'bottom', offset: 0, fontSize: 10 }}
|
||||
domain={['auto', 'auto']}
|
||||
tickFormatter={(value) => value.toFixed(2)}
|
||||
tick={{ fontSize: 9 }}
|
||||
/>
|
||||
<YAxis
|
||||
type="number"
|
||||
label={{ value: 'Cible (résidus)', angle: -90, position: 'left', offset: 0, fontSize: 10 }}
|
||||
domain={['auto', 'auto']}
|
||||
tickFormatter={(value) => value.toFixed(2)}
|
||||
tick={{ fontSize: 9 }}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} cursor={{ strokeDasharray: '3 3' }} />
|
||||
<Legend wrapperStyle={{fontSize: '11px'}} />
|
||||
<Scatter name="Données" fill="#94a3b8" opacity={0.5} shape="circle" />
|
||||
<Line
|
||||
name="Tendance (pente = coefficient)"
|
||||
type="monotone"
|
||||
dataKey="y"
|
||||
stroke="#6366f1"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="mt-3 p-3 bg-indigo-50 rounded-lg border border-indigo-100">
|
||||
<p className="text-[10px] text-indigo-700">
|
||||
<strong>Interprétation :</strong> Ce graphe montre la relation entre <code>{selectedPartialFeature}</code> et la cible,
|
||||
après avoir retiré l'effet de toutes les autres variables. Une pente positive indique que cette variable
|
||||
augmente la cible, indépendamment des autres facteurs.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3. Parity Plot (Real vs Predicted) - The Diagonal */}
|
||||
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-widest">Validation : Réel vs Prédit</h3>
|
||||
<p className="text-[11px] text-slate-400">
|
||||
Les points doivent s'aligner sur la diagonale rouge (Y = X).
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-80 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
|
||||
<XAxis type="number" dataKey="real" name="Réel" label={{ value: 'Réel', position: 'bottom', offset: 0, fontSize: 10 }} domain={parityDomain} tickFormatter={(value) => value.toFixed(2)} tick={{ fontSize: 9 }} />
|
||||
<YAxis type="number" dataKey="pred" name="Prédit" label={{ value: 'Prédit', angle: -90, position: 'left', offset: 0, fontSize: 10 }} domain={parityDomain} tickFormatter={(value) => value.toFixed(2)} tick={{ fontSize: 9 }} />
|
||||
<Tooltip content={<CustomTooltip />} cursor={{ strokeDasharray: '3 3' }} />
|
||||
<ReferenceLine segment={[{ x: minParity, y: minParity }, { x: maxParity, y: maxParity }]} stroke="#ef4444" strokeDasharray="3 3" />
|
||||
<Scatter name="Observations" data={parityData} fill="#818cf8" opacity={0.6} />
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4. Coefficients Table */}
|
||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
<header className="p-4 border-b border-slate-100 bg-slate-50/50 flex items-center justify-between">
|
||||
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-widest">Coefficients du Modèle</h3>
|
||||
<Info className="w-4 h-4 text-slate-300" />
|
||||
</header>
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-100 text-[10px] uppercase font-bold text-slate-400">
|
||||
<th className="px-6 py-3">Variable</th>
|
||||
<th className="px-6 py-3 text-right">Coefficient</th>
|
||||
<th className="px-6 py-3 text-right">P-Value</th>
|
||||
<th className="px-6 py-3 text-center">Fiabilité</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{Object.entries(coefficients || {}).map(([name, val]) => {
|
||||
const p = (analysisResults as any).p_values[name];
|
||||
const isSignificant = p < 0.05;
|
||||
return (
|
||||
<tr key={name} className="text-sm">
|
||||
<td className="px-6 py-4 font-bold text-slate-700">{name}</td>
|
||||
<td className="px-6 py-4 text-right font-mono">{Number(val).toFixed(4)}</td>
|
||||
<td className={cn("px-6 py-4 text-right font-mono", isSignificant ? "text-emerald-600" : "text-rose-500")}>
|
||||
{Number(p).toFixed(4)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
{isSignificant ? (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-bold text-emerald-600 bg-emerald-50 px-2 py-1 rounded-full">
|
||||
<CheckCircle2 className="w-3 h-3" /> FIABLE
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-bold text-slate-400 bg-slate-100 px-2 py-1 rounded-full">
|
||||
<XCircle className="w-3 h-3" /> INCERTAIN
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
441
frontend/src/features/analysis/components/CorrelationHeatmap.tsx
Normal file
441
frontend/src/features/analysis/components/CorrelationHeatmap.tsx
Normal file
@@ -0,0 +1,441 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState, useMemo } from "react";
|
||||
import { ScatterChart, Scatter, XAxis, YAxis, ZAxis, Tooltip, ResponsiveContainer, Cell, CartesianGrid } from "recharts";
|
||||
import { useGridStore } from "@/store/use-grid-store";
|
||||
import { getApiUrl } from "@/lib/api-config";
|
||||
import { cn, safeJSONStringify } from "@/lib/utils";
|
||||
import { Filter, TrendingUp, Info, Download, CheckCircle2, XCircle } from "lucide-react";
|
||||
|
||||
type CorrelationMethod = 'pearson' | 'spearman' | 'kendall';
|
||||
|
||||
// Multicollinearity threshold: correlations above this between predictors are problematic
|
||||
const MULTICOLLINEARITY_THRESHOLD = 0.7;
|
||||
|
||||
interface CorrelationData {
|
||||
x: string;
|
||||
y: string;
|
||||
value: number;
|
||||
abs_value: number;
|
||||
}
|
||||
|
||||
interface CorrelationResult {
|
||||
matrix: CorrelationData[];
|
||||
pvalues: Array<{ x: string; y: string; pvalue: number | null; significant: boolean }>;
|
||||
metadata: {
|
||||
method: string;
|
||||
n_observations: number;
|
||||
n_variables: number;
|
||||
columns_analyzed: string[];
|
||||
threshold_applied?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CorrelationSummary {
|
||||
strongest: CorrelationData[];
|
||||
weakest: CorrelationData[];
|
||||
total_pairs: number;
|
||||
}
|
||||
|
||||
export function CorrelationHeatmap() {
|
||||
const { columns, getCleanData } = useGridStore();
|
||||
const [result, setResult] = useState<CorrelationResult | null>(null);
|
||||
const [summary, setSummary] = useState<CorrelationSummary | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Controls
|
||||
const [method, setMethod] = useState<CorrelationMethod>('pearson');
|
||||
const [threshold, setThreshold] = useState<number | null>(null);
|
||||
const [showOnlySignificant, setShowOnlySignificant] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const numericCols = columns.filter(c => c.type === 'numeric').map(c => c.name);
|
||||
const cleanData = getCleanData();
|
||||
|
||||
if (numericCols.length < 2 || cleanData.length === 0) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(getApiUrl("/analysis/correlation"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: safeJSONStringify({
|
||||
data: cleanData,
|
||||
columns: numericCols,
|
||||
method,
|
||||
min_threshold: threshold,
|
||||
include_pvalues: true
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.status === "ok") {
|
||||
setResult(data.result);
|
||||
setSummary(data.summary);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Correlation failed", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [columns, getCleanData, method, threshold]);
|
||||
|
||||
// Filter matrix based on significance toggle
|
||||
const filteredMatrix = useMemo(() => {
|
||||
if (!result || !showOnlySignificant) return result?.matrix || [];
|
||||
|
||||
const significantSet = new Set(
|
||||
result.pvalues
|
||||
.filter(p => p.significant)
|
||||
.map(p => `${p.x}-${p.y}`)
|
||||
);
|
||||
|
||||
return result.matrix.filter(m =>
|
||||
significantSet.has(`${m.x}-${m.y}`)
|
||||
);
|
||||
}, [result, showOnlySignificant]);
|
||||
|
||||
if (loading) return (
|
||||
<div className="p-12 text-center text-slate-400 animate-pulse">
|
||||
Calcul de la corrélation ({method})...
|
||||
</div>
|
||||
);
|
||||
|
||||
// Better error messages
|
||||
if (!result || result.matrix.length === 0) {
|
||||
const numericCount = columns.filter(c => c.type === 'numeric').length;
|
||||
const hasData = getCleanData().length > 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-64 border-2 border-dashed border-slate-200 rounded-xl bg-white">
|
||||
<Info className="w-12 h-12 text-slate-300 mb-4" />
|
||||
<p className="text-slate-600 font-medium mb-2">Corrélation non disponible</p>
|
||||
<div className="text-xs text-slate-400 text-center space-y-1 max-w-md">
|
||||
{!hasData && <p>📂 Importez d'abord un fichier de données</p>}
|
||||
{hasData && numericCount < 2 && <p>📊 Il faut au moins <strong>2 colonnes numériques</strong> pour calculer une corrélation. Actuellement : {numericCount} colonne(s) numérique(s)</p>}
|
||||
{hasData && numericCount >= 2 && <p>⚠️ Erreur lors du calcul. Vérifiez que les données contiennent des valeurs numériques valides.</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const exportData = () => {
|
||||
if (!result) return;
|
||||
|
||||
const csv = [
|
||||
['Variable X', 'Variable Y', 'Corrélation', 'P-Value', 'Significatif'].join(','),
|
||||
...result.matrix.map(m => {
|
||||
const pvalue = result.pvalues.find(p => p.x === m.x && p.y === m.y);
|
||||
return [
|
||||
m.x,
|
||||
m.y,
|
||||
m.value.toFixed(6),
|
||||
pvalue?.pvalue?.toFixed(6) || 'N/A',
|
||||
pvalue?.significant ? 'Oui' : 'Non'
|
||||
].join(',');
|
||||
})
|
||||
].join('\n');
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `correlation_${method}_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
a.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Intro Section */}
|
||||
<div className="bg-gradient-to-br from-indigo-50 to-blue-50 p-6 rounded-2xl border border-indigo-100">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 bg-white rounded-xl flex items-center justify-center shadow-sm">
|
||||
<TrendingUp className="w-6 h-6 text-indigo-600" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-sm font-bold text-slate-800 mb-2">Matrice de Corrélation</h2>
|
||||
<p className="text-xs text-slate-600 leading-relaxed mb-3">
|
||||
La corrélation mesure la force et la direction de la relation linéaire entre deux variables.
|
||||
Utilisez cette matrice pour :
|
||||
</p>
|
||||
<ul className="text-[10px] text-slate-600 space-y-1">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="w-3 h-3 text-emerald-500 shrink-0 mt-0.5" />
|
||||
<span><strong>Identifier les prédicteurs pertinents</strong> - corrélations fortes avec votre cible</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<XCircle className="w-3 h-3 text-rose-500 shrink-0 mt-0.5" />
|
||||
<span><strong>Éviter la multicolinéarité</strong> - bordure <strong className="text-rose-600">rouge</strong> = corrélation ≥ {MULTICOLLINEARITY_THRESHOLD} entre prédicteurs (à exclure)</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="w-3 h-3 text-emerald-500 shrink-0 mt-0.5" />
|
||||
<span><strong>Comprendre les relations</strong> - comment les variables interagissent entre elles</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Controls Header */}
|
||||
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-slate-800">Contrôles & Filtres</h3>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
{result.metadata.n_variables} variables • {result.metadata.n_observations} observations
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={exportData}
|
||||
className="flex items-center gap-2 px-4 py-2 text-xs font-semibold text-slate-600 bg-slate-50 hover:bg-slate-100 rounded-lg border border-slate-200 transition-colors"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
Exporter CSV
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* Method Selection */}
|
||||
<div>
|
||||
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-2 block">
|
||||
Méthode
|
||||
</label>
|
||||
<select
|
||||
value={method}
|
||||
onChange={(e) => setMethod(e.target.value as CorrelationMethod)}
|
||||
className="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
>
|
||||
<option value="pearson">Pearson (linéaire)</option>
|
||||
<option value="spearman">Spearman (monotone)</option>
|
||||
<option value="kendall">Kendall (tau)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Threshold Filter */}
|
||||
<div>
|
||||
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-2 block">
|
||||
Seuil minimum (optionnel)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={threshold || ''}
|
||||
onChange={(e) => setThreshold(e.target.value ? parseFloat(e.target.value) : null)}
|
||||
placeholder="Ex: 0.5"
|
||||
className="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Significance Filter */}
|
||||
<div>
|
||||
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-2 block">
|
||||
Filtre
|
||||
</label>
|
||||
<label className="flex items-center gap-2 px-3 py-2 text-sm border border-slate-200 rounded-lg cursor-pointer hover:bg-slate-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showOnlySignificant}
|
||||
onChange={(e) => setShowOnlySignificant(e.target.checked)}
|
||||
className="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
<span className="text-slate-700">Significatif seulement (p < 0.05)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
{summary && (summary.strongest.length > 0 || summary.weakest.length > 0) && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Strongest Correlations */}
|
||||
{summary.strongest.length > 0 && (
|
||||
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<TrendingUp className="w-4 h-4 text-rose-500" />
|
||||
<h4 className="text-xs font-bold text-slate-500 uppercase tracking-widest">
|
||||
Corrélations les Plus Fortes
|
||||
</h4>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{summary.strongest.map((item, idx) => {
|
||||
const pvalue = result.pvalues.find(p => p.x === item.x && p.y === item.y);
|
||||
return (
|
||||
<div key={idx} className="flex items-center justify-between text-xs p-2 bg-slate-50 rounded-lg">
|
||||
<span className="font-medium text-slate-700">
|
||||
{item.x} ↔ {item.y}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn(
|
||||
"font-mono font-bold",
|
||||
item.value > 0 ? "text-rose-500" : "text-blue-500"
|
||||
)}>
|
||||
{item.value.toFixed(4)}
|
||||
</span>
|
||||
{pvalue?.significant && (
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-500" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Weakest Correlations */}
|
||||
{summary.weakest.length > 0 && (
|
||||
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Info className="w-4 h-4 text-slate-400" />
|
||||
<h4 className="text-xs font-bold text-slate-500 uppercase tracking-widest">
|
||||
Corrélations les Plus Faibles
|
||||
</h4>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{summary.weakest.slice(0, 5).map((item, idx) => {
|
||||
const pvalue = result.pvalues.find(p => p.x === item.x && p.y === item.y);
|
||||
return (
|
||||
<div key={idx} className="flex items-center justify-between text-xs p-2 bg-slate-50 rounded-lg">
|
||||
<span className="font-medium text-slate-700">
|
||||
{item.x} ↔ {item.y}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono font-bold text-slate-500">
|
||||
{item.value.toFixed(4)}
|
||||
</span>
|
||||
{!pvalue?.significant && pvalue?.pvalue && (
|
||||
<XCircle className="w-4 h-4 text-slate-300" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Heatmap */}
|
||||
<div className="bg-white p-8 rounded-2xl border border-slate-200 shadow-sm">
|
||||
{filteredMatrix.length === 0 ? (
|
||||
<div className="text-center py-12 text-slate-400">
|
||||
Aucune corrélation ne correspond aux filtres sélectionnés.
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-[500px] w-full relative">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 100 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="x"
|
||||
type="category"
|
||||
name="Variable X"
|
||||
tick={{ fontSize: 10 }}
|
||||
interval={0}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={60}
|
||||
/>
|
||||
<YAxis
|
||||
dataKey="y"
|
||||
type="category"
|
||||
name="Variable Y"
|
||||
tick={{ fontSize: 10 }}
|
||||
width={100}
|
||||
/>
|
||||
<ZAxis type="number" dataKey="value" range={[500, 500]} />
|
||||
<Tooltip
|
||||
cursor={{ strokeDasharray: '3 3' }}
|
||||
content={({ active, payload }) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload as CorrelationData;
|
||||
const pvalue = result.pvalues.find(p => p.x === data.x && p.y === data.y);
|
||||
return (
|
||||
<div className="bg-white p-3 rounded-lg shadow-xl text-xs border border-slate-200 z-50">
|
||||
<p className="font-bold text-slate-800 mb-2">{data.x} ↔ {data.y}</p>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-slate-500">Corrélation:</span>
|
||||
<span className={cn(
|
||||
"font-mono font-bold",
|
||||
data.value > 0 ? "text-rose-500" : "text-blue-500"
|
||||
)}>
|
||||
{data.value.toFixed(6)}
|
||||
</span>
|
||||
</div>
|
||||
{pvalue && pvalue.pvalue && (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-slate-500">P-value:</span>
|
||||
<span className="font-mono">{pvalue.pvalue.toFixed(6)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-slate-500">Significatif:</span>
|
||||
{pvalue.significant ? (
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-500" />
|
||||
) : (
|
||||
<XCircle className="w-4 h-4 text-slate-300" />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="pt-1 border-t border-slate-100">
|
||||
<span className={cn(
|
||||
"italic text-[10px]",
|
||||
Math.abs(data.value) > 0.7 ? "text-rose-500" :
|
||||
Math.abs(data.value) < 0.3 ? "text-slate-400" : "text-amber-500"
|
||||
)}>
|
||||
{Math.abs(data.value) > 0.7 ? "(Fort)" :
|
||||
Math.abs(data.value) < 0.3 ? "(Faible)" : "(Moyen)"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<Scatter data={filteredMatrix} shape="square">
|
||||
{filteredMatrix.map((entry, index) => {
|
||||
const val = entry.value;
|
||||
let color;
|
||||
if (val >= 0) {
|
||||
color = `rgba(244, 63, 94, ${0.1 + val * 0.9})`;
|
||||
} else {
|
||||
color = `rgba(59, 130, 246, ${0.1 + Math.abs(val) * 0.9})`;
|
||||
}
|
||||
|
||||
// Check for problematic multicollinearity (high correlation between predictors)
|
||||
// Exclude diagonal (self-correlation)
|
||||
const isMulticollinear = Math.abs(val) >= MULTICOLLINEARITY_THRESHOLD && entry.x !== entry.y;
|
||||
|
||||
return (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={color}
|
||||
stroke={isMulticollinear ? "#dc2626" : "none"}
|
||||
strokeWidth={isMulticollinear ? 3 : 0}
|
||||
style={{
|
||||
filter: isMulticollinear ? "brightness(0.85)" : "none"
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Scatter>
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
163
frontend/src/features/help/components/HelpButton.tsx
Normal file
163
frontend/src/features/help/components/HelpButton.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { HelpCircle, X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface HelpButtonProps {
|
||||
/**
|
||||
* Contenu de l'aide à afficher
|
||||
*/
|
||||
title: string;
|
||||
content: string | React.ReactNode;
|
||||
/**
|
||||
* Position du tooltip
|
||||
* @default "top"
|
||||
*/
|
||||
position?: "top" | "bottom" | "left" | "right";
|
||||
/**
|
||||
* Variante du bouton
|
||||
* @default "icon"
|
||||
*/
|
||||
variant?: "icon" | "text" | "inline";
|
||||
/**
|
||||
* Classe CSS personnalisée
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Si vrai, le tooltip est toujours visible
|
||||
* @default false
|
||||
*/
|
||||
alwaysOpen?: boolean;
|
||||
}
|
||||
|
||||
export function HelpButton({
|
||||
title,
|
||||
content,
|
||||
position = "top",
|
||||
variant = "icon",
|
||||
className,
|
||||
alwaysOpen = false,
|
||||
}: HelpButtonProps) {
|
||||
const [isOpen, setIsOpen] = useState(alwaysOpen);
|
||||
|
||||
const positionClasses = {
|
||||
top: "bottom-full left-1/2 -translate-x-1/2 mb-3",
|
||||
bottom: "top-full left-1/2 -translate-x-1/2 mt-3",
|
||||
left: "right-full top-1/2 -translate-y-1/2 mr-3",
|
||||
right: "left-full top-1/2 -translate-y-1/2 ml-3",
|
||||
};
|
||||
|
||||
const arrowClasses = {
|
||||
top: "top-full left-1/2 -translate-x-1/2 -mt-1 border-t-slate-900 border-r-transparent border-b-transparent border-l-transparent",
|
||||
bottom: "bottom-full left-1/2 -translate-x-1/2 -mb-1 border-b-slate-900 border-r-transparent border-t-transparent border-l-transparent",
|
||||
left: "left-full top-1/2 -translate-y-1/2 -ml-1 border-l-slate-900 border-t-transparent border-r-transparent border-b-transparent",
|
||||
right: "right-full top-1/2 -translate-y-1/2 -mr-1 border-r-slate-900 border-t-transparent border-l-transparent border-b-transparent",
|
||||
};
|
||||
|
||||
const buttonVariants = {
|
||||
icon: (
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center w-5 h-5 rounded-full transition-all duration-200",
|
||||
"text-slate-400 hover:text-indigo-600 hover:bg-indigo-50",
|
||||
"focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1",
|
||||
isOpen && "bg-indigo-100 text-indigo-700",
|
||||
className
|
||||
)}
|
||||
aria-label={`Aide: ${title}`}
|
||||
>
|
||||
<HelpCircle className="w-4 h-4" />
|
||||
</button>
|
||||
),
|
||||
text: (
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 px-2 py-1 rounded-md transition-all duration-200",
|
||||
"text-xs font-medium text-slate-500 hover:text-indigo-600 hover:bg-indigo-50",
|
||||
"focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1",
|
||||
isOpen && "bg-indigo-100 text-indigo-700",
|
||||
className
|
||||
)}
|
||||
aria-label={`Aide: ${title}`}
|
||||
>
|
||||
<HelpCircle className="w-3.5 h-3.5" />
|
||||
<span>Aide</span>
|
||||
</button>
|
||||
),
|
||||
inline: (
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 text-indigo-600 hover:text-indigo-700 underline underline-dotted underline-offset-2",
|
||||
"transition-all duration-200",
|
||||
"focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1 rounded px-1",
|
||||
className
|
||||
)}
|
||||
aria-label={`Aide: ${title}`}
|
||||
>
|
||||
<span>{title}</span>
|
||||
<HelpCircle className="w-3 h-3" />
|
||||
</button>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative inline-flex items-center">
|
||||
{buttonVariants[variant]}
|
||||
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => setIsOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Tooltip */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute z-50 w-80 max-w-sm",
|
||||
positionClasses[position]
|
||||
)}
|
||||
>
|
||||
<div className="relative bg-slate-900 text-slate-100 rounded-lg shadow-xl p-4">
|
||||
{/* Arrow */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute w-0 h-0 border-8",
|
||||
arrowClasses[position]
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="absolute top-2 right-2 text-slate-400 hover:text-white transition-colors"
|
||||
aria-label="Fermer"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Content */}
|
||||
<div className="pr-6">
|
||||
<h4 className="text-sm font-bold text-white mb-2">{title}</h4>
|
||||
<div className="text-xs text-slate-300 leading-relaxed space-y-2">
|
||||
{typeof content === "string" ? (
|
||||
<p>{content}</p>
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
108
frontend/src/features/help/components/HelpModal.tsx
Normal file
108
frontend/src/features/help/components/HelpModal.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { X } from "lucide-react";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface HelpModalProps {
|
||||
/**
|
||||
* Contrôle l'ouverture du modal
|
||||
*/
|
||||
open?: boolean;
|
||||
/**
|
||||
* Callback lors de la fermeture
|
||||
*/
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
/**
|
||||
* Titre du modal
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Contenu du modal
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
* Classe CSS personnalisée pour le contenu
|
||||
*/
|
||||
contentClassName?: string;
|
||||
/**
|
||||
* Taille du modal
|
||||
* @default "md"
|
||||
*/
|
||||
size?: "sm" | "md" | "lg" | "xl" | "full";
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "max-w-md",
|
||||
md: "max-w-lg",
|
||||
lg: "max-w-2xl",
|
||||
xl: "max-w-4xl",
|
||||
full: "max-w-6xl",
|
||||
};
|
||||
|
||||
export function HelpModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
children,
|
||||
contentClassName,
|
||||
size = "md",
|
||||
}: HelpModalProps) {
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 bg-black/50 backdrop-blur-sm z-50",
|
||||
"transition-opacity duration-200",
|
||||
"data-[state=open]:opacity-100",
|
||||
"data-[state=closed]:opacity-0"
|
||||
)}
|
||||
/>
|
||||
<Dialog.Content
|
||||
className={cn(
|
||||
"fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2",
|
||||
"w-full",
|
||||
sizeClasses[size],
|
||||
"bg-white rounded-2xl shadow-2xl z-50",
|
||||
"border border-slate-200",
|
||||
"transition-all duration-200",
|
||||
"data-[state=open]:animate-in",
|
||||
"data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:fade-out-0",
|
||||
"data-[state=open]:fade-in-0",
|
||||
"data-[state=closed]:zoom-out-95",
|
||||
"data-[state=open]:zoom-in-95",
|
||||
"data-[state=closed]:slide-out-to-left-1/2",
|
||||
"data-[state=closed]:slide-out-to-top-[48%]",
|
||||
"data-[state=open]:slide-in-from-left-1/2",
|
||||
"data-[state=open]:slide-in-from-top-[48%]"
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-200">
|
||||
<Dialog.Title className="text-lg font-bold text-slate-900">
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<Dialog.Close
|
||||
className={cn(
|
||||
"w-8 h-8 rounded-lg flex items-center justify-center",
|
||||
"text-slate-400 hover:text-slate-700 hover:bg-slate-100",
|
||||
"transition-colors duration-150",
|
||||
"focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
)}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className={cn("px-6 py-4 max-h-[60vh] overflow-y-auto", contentClassName)}>
|
||||
{children}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
383
frontend/src/features/help/components/HelpPanel.tsx
Normal file
383
frontend/src/features/help/components/HelpPanel.tsx
Normal file
@@ -0,0 +1,383 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
X,
|
||||
Search,
|
||||
BookOpen,
|
||||
FileText,
|
||||
ArrowRight,
|
||||
ChevronRight,
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
|
||||
// Types pour la structure de documentation
|
||||
interface DocSection {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: React.ReactNode;
|
||||
children?: DocSection[];
|
||||
content?: string;
|
||||
}
|
||||
|
||||
// Structure de documentation basée sur les fichiers markdown
|
||||
const DOCUMENTATION_STRUCTURE: DocSection[] = [
|
||||
{
|
||||
id: "getting-started",
|
||||
title: "Démarrage Rapide",
|
||||
icon: <BookOpen className="w-5 h-5" />,
|
||||
children: [
|
||||
{
|
||||
id: "import-data",
|
||||
title: "Importer vos données",
|
||||
description: "Formats supportés : CSV, Excel",
|
||||
content: "user-guide#importer-vos-données",
|
||||
},
|
||||
{
|
||||
id: "explore-data",
|
||||
title: "Explorer vos données",
|
||||
description: "Table intelligente et détection d'outliers",
|
||||
content: "user-guide#explorer-vos-données",
|
||||
},
|
||||
{
|
||||
id: "start-analysis",
|
||||
title: "Lancer une analyse",
|
||||
description: "Corrélation et régression",
|
||||
content: "user-guide#lancer-une-analyse",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "correlation",
|
||||
title: "Analyse de Corrélation",
|
||||
icon: <FileText className="w-5 h-5" />,
|
||||
children: [
|
||||
{
|
||||
id: "correlation-methods",
|
||||
title: "Méthodes de corrélation",
|
||||
description: "Pearson, Spearman, Kendall",
|
||||
content: "correlation-guide#les-trois-méthodes",
|
||||
},
|
||||
{
|
||||
id: "interpret-heatmap",
|
||||
title: "Interpréter la heatmap",
|
||||
description: "Comprendre les couleurs et valeurs",
|
||||
content: "correlation-guide#interpréter-la-matrice",
|
||||
},
|
||||
{
|
||||
id: "multicollinearity",
|
||||
title: "Multicolinéarité",
|
||||
description: "Détecter et éviter les corrélations entre prédicteurs",
|
||||
content: "correlation-guide#multicolinéarité",
|
||||
},
|
||||
{
|
||||
id: "p-values",
|
||||
title: "P-values et significativité",
|
||||
description: "Comprendre la significativité statistique",
|
||||
content: "correlation-guide#p-values-et-significativité",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "regression",
|
||||
title: "Régression Statistique",
|
||||
icon: <FileText className="w-5 h-5" />,
|
||||
children: [
|
||||
{
|
||||
id: "model-types",
|
||||
title: "Types de modèles",
|
||||
description: "Linéaire, logistique, polynomial, exponentielle",
|
||||
content: "regression-guide#types-de-modèles",
|
||||
},
|
||||
{
|
||||
id: "configure-model",
|
||||
title: "Configuration du modèle",
|
||||
description: "Choisir cible et prédicteurs",
|
||||
content: "regression-guide#configuration-du-modèle",
|
||||
},
|
||||
{
|
||||
id: "interpret-results",
|
||||
title: "Interpréter les résultats",
|
||||
description: "R², coefficients, p-values",
|
||||
content: "regression-guide#interprétation-des-résultats",
|
||||
},
|
||||
{
|
||||
id: "model-equations",
|
||||
title: "Équations du modèle",
|
||||
description: "Exporter en Python, Excel, LaTeX",
|
||||
content: "regression-guide#équations-du-modèle",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "outliers",
|
||||
title: "Gestion des Outliers",
|
||||
icon: <FileText className="w-5 h-5" />,
|
||||
children: [
|
||||
{
|
||||
id: "outlier-types",
|
||||
title: "Types d'outliers",
|
||||
description: "Univariés vs multivariés",
|
||||
content: "outlier-guide#types-doutliers",
|
||||
},
|
||||
{
|
||||
id: "detection-methods",
|
||||
title: "Méthodes de détection",
|
||||
description: "IQR et Isolation Forest",
|
||||
content: "outlier-guide#méthodes-de-détection",
|
||||
},
|
||||
{
|
||||
id: "visual-indicators",
|
||||
title: "Indicateurs visuels",
|
||||
description: "Cercles rouge et violet",
|
||||
content: "outlier-guide#indicateurs-visuels",
|
||||
},
|
||||
{
|
||||
id: "manage-outliers",
|
||||
title: "Gérer les outliers",
|
||||
description: "Processus d'exclusion",
|
||||
content: "outlier-guide#processus-de-gestion",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
interface HelpPanelProps {
|
||||
/**
|
||||
* Contrôle l'ouverture du panel
|
||||
*/
|
||||
open?: boolean;
|
||||
/**
|
||||
* Callback lors de la fermeture
|
||||
*/
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function HelpPanel({ open: controlledOpen, onOpenChange }: HelpPanelProps) {
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(
|
||||
new Set(["getting-started"])
|
||||
);
|
||||
|
||||
const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
|
||||
|
||||
const setOpen = (newOpen: boolean) => {
|
||||
if (controlledOpen === undefined) {
|
||||
setInternalOpen(newOpen);
|
||||
}
|
||||
onOpenChange?.(newOpen);
|
||||
};
|
||||
|
||||
const toggleSection = (sectionId: string) => {
|
||||
setExpandedSections((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(sectionId)) {
|
||||
next.delete(sectionId);
|
||||
} else {
|
||||
next.add(sectionId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// Filtrer les sections basées sur la recherche
|
||||
const filteredStructure = searchQuery
|
||||
? DOCUMENTATION_STRUCTURE.map((section) => ({
|
||||
...section,
|
||||
children: section.children?.filter(
|
||||
(child) =>
|
||||
child.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
child.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
),
|
||||
})).filter((section) => (section.children?.length || 0) > 0)
|
||||
: DOCUMENTATION_STRUCTURE;
|
||||
|
||||
// Ouvrir automatiquement les sections lors de la recherche
|
||||
useEffect(() => {
|
||||
if (searchQuery) {
|
||||
setExpandedSections(new Set(filteredStructure.map((s) => s.id)));
|
||||
}
|
||||
}, [searchQuery, filteredStructure]);
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 bg-black/20 backdrop-blur-sm z-40",
|
||||
"transition-opacity duration-200",
|
||||
open ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
)}
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
<Dialog.Content
|
||||
className={cn(
|
||||
"fixed top-0 right-0 bottom-0 w-[480px] max-w-[90vw] bg-white z-50",
|
||||
"shadow-2xl border-l border-slate-200",
|
||||
"transition-transform duration-200 ease-out",
|
||||
open ? "translate-x-0" : "translate-x-full"
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 bg-white border-b border-slate-200 px-6 py-4 z-10">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-white font-bold shadow-md">
|
||||
?
|
||||
</div>
|
||||
<div>
|
||||
<Dialog.Title className="text-lg font-bold text-slate-900">
|
||||
Centre d'Aide
|
||||
</Dialog.Title>
|
||||
<Dialog.Description className="text-sm text-slate-500">
|
||||
Guides et documentation
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog.Close
|
||||
className={cn(
|
||||
"w-8 h-8 rounded-lg flex items-center justify-center",
|
||||
"text-slate-400 hover:text-slate-700 hover:bg-slate-100",
|
||||
"transition-colors duration-150"
|
||||
)}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher dans les guides..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className={cn(
|
||||
"w-full pl-10 pr-4 py-2.5 rounded-lg",
|
||||
"bg-slate-50 border border-slate-200",
|
||||
"text-sm text-slate-900 placeholder:text-slate-400",
|
||||
"focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent",
|
||||
"transition-all duration-150"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
||||
<div className="space-y-2">
|
||||
{filteredStructure.map((section) => {
|
||||
const isExpanded = expandedSections.has(section.id);
|
||||
const hasChildren = section.children && section.children.length > 0;
|
||||
|
||||
return (
|
||||
<div key={section.id} className="border border-slate-200 rounded-lg overflow-hidden">
|
||||
{/* Section Header */}
|
||||
<button
|
||||
onClick={() => toggleSection(section.id)}
|
||||
disabled={!hasChildren}
|
||||
className={cn(
|
||||
"w-full flex items-center justify-between px-4 py-3",
|
||||
"bg-slate-50 hover:bg-slate-100",
|
||||
"transition-colors duration-150",
|
||||
!hasChildren && "cursor-default"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn(
|
||||
"w-8 h-8 rounded-lg flex items-center justify-center",
|
||||
"bg-indigo-100 text-indigo-600"
|
||||
)}>
|
||||
{section.icon}
|
||||
</div>
|
||||
<span className="font-semibold text-sm text-slate-900">
|
||||
{section.title}
|
||||
</span>
|
||||
</div>
|
||||
{hasChildren && (
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"w-4 h-4 text-slate-400 transition-transform duration-200",
|
||||
isExpanded && "rotate-90"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Section Children */}
|
||||
{isExpanded && hasChildren && (
|
||||
<div className="border-t border-slate-200 bg-white divide-y divide-slate-100">
|
||||
{section.children!.map((child) => (
|
||||
<a
|
||||
key={child.id}
|
||||
href={`/docs/${child.content}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
"flex items-start gap-3 px-4 py-3",
|
||||
"hover:bg-indigo-50/50",
|
||||
"transition-colors duration-150",
|
||||
"group"
|
||||
)}
|
||||
>
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-slate-900 group-hover:text-indigo-700 transition-colors">
|
||||
{child.title}
|
||||
</span>
|
||||
<ExternalLink className="w-3.5 h-3.5 text-slate-400 group-hover:text-indigo-600 transition-colors shrink-0" />
|
||||
</div>
|
||||
{child.description && (
|
||||
<p className="text-xs text-slate-500 leading-relaxed">
|
||||
{child.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{filteredStructure.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<Search className="w-12 h-12 text-slate-300 mx-auto mb-3" />
|
||||
<p className="text-sm text-slate-500">Aucun résultat pour "{searchQuery}"</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="sticky bottom-0 bg-white border-t border-slate-200 px-6 py-4">
|
||||
<a
|
||||
href="/docs/readme"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-2 w-full",
|
||||
"px-4 py-2.5 rounded-lg",
|
||||
"bg-gradient-to-r from-indigo-600 to-purple-600",
|
||||
"text-white text-sm font-semibold",
|
||||
"hover:from-indigo-700 hover:to-purple-700",
|
||||
"transition-all duration-150",
|
||||
"shadow-md hover:shadow-lg"
|
||||
)}
|
||||
>
|
||||
<BookOpen className="w-4 h-4" />
|
||||
<span>Voir toute la documentation</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
120
frontend/src/features/help/components/HelpTooltip.tsx
Normal file
120
frontend/src/features/help/components/HelpTooltip.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Info } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface HelpTooltipProps {
|
||||
/**
|
||||
* Contenu du tooltip
|
||||
*/
|
||||
content: string | React.ReactNode;
|
||||
/**
|
||||
* Position du tooltip
|
||||
* @default "top"
|
||||
*/
|
||||
position?: "top" | "bottom" | "left" | "right";
|
||||
/**
|
||||
* Largeur maximale du tooltip
|
||||
* @default "250px"
|
||||
*/
|
||||
maxWidth?: string;
|
||||
/**
|
||||
* Classe CSS personnalisée pour le wrapper
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Si vrai, affiche une icône d'information
|
||||
* @default true
|
||||
*/
|
||||
showIcon?: boolean;
|
||||
/**
|
||||
* Contenu personnalisé pour déclencher le tooltip
|
||||
*/
|
||||
trigger?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function HelpTooltip({
|
||||
content,
|
||||
position = "top",
|
||||
maxWidth = "250px",
|
||||
className,
|
||||
showIcon = true,
|
||||
trigger,
|
||||
}: HelpTooltipProps) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const positionClasses = {
|
||||
top: "bottom-full left-1/2 -translate-x-1/2 mb-2 pb-1",
|
||||
bottom: "top-full left-1/2 -translate-x-1/2 mt-2 pt-1",
|
||||
left: "right-full top-1/2 -translate-y-1/2 mr-2 pr-1",
|
||||
right: "left-full top-1/2 -translate-y-1/2 ml-2 pl-1",
|
||||
};
|
||||
|
||||
const arrowClasses = {
|
||||
top: "top-full left-1/2 -translate-x-1/2 border-t-slate-800 border-r-transparent border-b-transparent border-l-transparent",
|
||||
bottom: "bottom-full left-1/2 -translate-x-1/2 border-b-slate-800 border-r-transparent border-t-transparent border-l-transparent",
|
||||
left: "left-full top-1/2 -translate-y-1/2 border-l-slate-800 border-t-transparent border-r-transparent border-b-transparent",
|
||||
right: "right-full top-1/2 -translate-y-1/2 border-r-slate-800 border-t-transparent border-l-transparent border-b-transparent",
|
||||
};
|
||||
|
||||
const defaultTrigger = (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center w-4 h-4 rounded-full",
|
||||
"text-slate-400 hover:text-indigo-600 hover:bg-indigo-50",
|
||||
"transition-colors duration-150",
|
||||
"focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1",
|
||||
className
|
||||
)}
|
||||
onMouseEnter={() => setIsVisible(true)}
|
||||
onMouseLeave={() => setIsVisible(false)}
|
||||
onFocus={() => setIsVisible(true)}
|
||||
onBlur={() => setIsVisible(false)}
|
||||
aria-label="Information"
|
||||
>
|
||||
<Info className="w-3 h-3" strokeWidth={2.5} />
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative inline-flex">
|
||||
<div
|
||||
onMouseEnter={() => setIsVisible(true)}
|
||||
onMouseLeave={() => setIsVisible(false)}
|
||||
>
|
||||
{trigger || defaultTrigger}
|
||||
</div>
|
||||
|
||||
{isVisible && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute z-50 pointer-events-none",
|
||||
positionClasses[position]
|
||||
)}
|
||||
style={{ maxWidth }}
|
||||
role="tooltip"
|
||||
aria-hidden={!isVisible}
|
||||
>
|
||||
{/* Arrow */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute w-0 h-0 border-4",
|
||||
arrowClasses[position]
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative bg-slate-800 text-slate-100 text-xs rounded-md shadow-lg px-3 py-2 leading-relaxed">
|
||||
{typeof content === "string" ? (
|
||||
<p>{content}</p>
|
||||
) : (
|
||||
<div>{content}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
376
frontend/src/features/help/components/TourGuide.tsx
Normal file
376
frontend/src/features/help/components/TourGuide.tsx
Normal file
@@ -0,0 +1,376 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { X, ChevronLeft, ChevronRight, Check } from "lucide-react";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface TourStep {
|
||||
/**
|
||||
* Identifiant unique de l'étape
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Titre de l'étape
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Contenu de l'étape
|
||||
*/
|
||||
content: string | React.ReactNode;
|
||||
/**
|
||||
* Sélecteur CSS de l'élément à highlighter
|
||||
* @example "#file-uploader", ".tab-button", "[data-tab='correlation']"
|
||||
*/
|
||||
target?: string;
|
||||
/**
|
||||
* Position du tooltip par rapport à la cible
|
||||
* @default "bottom"
|
||||
*/
|
||||
position?: "top" | "bottom" | "left" | "right" | "center";
|
||||
/**
|
||||
* Image optionnelle pour illustrer l'étape
|
||||
*/
|
||||
image?: string;
|
||||
/**
|
||||
* Action optionnelle à exécuter quand l'étape est affichée
|
||||
*/
|
||||
onEnter?: () => void;
|
||||
/**
|
||||
* Action optionnelle à exécuter quand l'étape est quittée
|
||||
*/
|
||||
onExit?: () => void;
|
||||
}
|
||||
|
||||
interface TourGuideProps {
|
||||
/**
|
||||
* Étapes du tour
|
||||
*/
|
||||
steps: TourStep[];
|
||||
/**
|
||||
* Contrôle l'ouverture du tour
|
||||
*/
|
||||
open?: boolean;
|
||||
/**
|
||||
* Callback lors de la fermeture
|
||||
*/
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
/**
|
||||
* Callback à la fin du tour
|
||||
*/
|
||||
onComplete?: () => void;
|
||||
/**
|
||||
* Callback quand une étape est_skippée
|
||||
*/
|
||||
onSkip?: () => void;
|
||||
/**
|
||||
* Texte du bouton de fin
|
||||
* @default "Compris !"
|
||||
*/
|
||||
completeButtonText?: string;
|
||||
/**
|
||||
* Texte du bouton pour passer
|
||||
* @default "Passer"
|
||||
*/
|
||||
skipButtonText?: string;
|
||||
/**
|
||||
* Si true, affiche une barre de progression
|
||||
* @default true
|
||||
*/
|
||||
showProgress?: boolean;
|
||||
/**
|
||||
* Si true, le tour ne peut pas être fermé en cliquant en dehors
|
||||
* @default false
|
||||
*/
|
||||
closeOnOutsideClick?: boolean;
|
||||
}
|
||||
|
||||
export function TourGuide({
|
||||
steps,
|
||||
open: controlledOpen,
|
||||
onOpenChange,
|
||||
onComplete,
|
||||
onSkip,
|
||||
completeButtonText = "Compris !",
|
||||
skipButtonText = "Passer",
|
||||
showProgress = true,
|
||||
closeOnOutsideClick = false,
|
||||
}: TourGuideProps) {
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||
const [isHighlighted, setIsHighlighted] = useState(false);
|
||||
|
||||
const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
|
||||
const currentStep = steps[currentStepIndex];
|
||||
const isLastStep = currentStepIndex === steps.length - 1;
|
||||
const isFirstStep = currentStepIndex === 0;
|
||||
|
||||
const setOpen = (newOpen: boolean) => {
|
||||
if (controlledOpen === undefined) {
|
||||
setInternalOpen(newOpen);
|
||||
}
|
||||
onOpenChange?.(newOpen);
|
||||
};
|
||||
|
||||
// Gérer l'entrée dans une étape
|
||||
useEffect(() => {
|
||||
if (open && currentStep) {
|
||||
currentStep.onEnter?.();
|
||||
setIsHighlighted(true);
|
||||
|
||||
// Scroll vers l'élément cible si nécessaire
|
||||
if (currentStep.target) {
|
||||
const element = document.querySelector(currentStep.target);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (currentStep) {
|
||||
currentStep.onExit?.();
|
||||
}
|
||||
setIsHighlighted(false);
|
||||
};
|
||||
}, [currentStepIndex, open, currentStep]);
|
||||
|
||||
const nextStep = useCallback(() => {
|
||||
if (isLastStep) {
|
||||
onComplete?.();
|
||||
setOpen(false);
|
||||
setCurrentStepIndex(0);
|
||||
} else {
|
||||
setCurrentStepIndex((prev) => prev + 1);
|
||||
}
|
||||
}, [isLastStep, onComplete, setOpen]);
|
||||
|
||||
const prevStep = useCallback(() => {
|
||||
if (!isFirstStep) {
|
||||
setCurrentStepIndex((prev) => prev - 1);
|
||||
}
|
||||
}, [isFirstStep]);
|
||||
|
||||
const handleSkip = useCallback(() => {
|
||||
onSkip?.();
|
||||
setOpen(false);
|
||||
setCurrentStepIndex(0);
|
||||
}, [onSkip, setOpen]);
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(newOpen: boolean) => {
|
||||
if (newOpen === false && !closeOnOutsideClick) {
|
||||
// Empêcher la fermeture si closeOnOutsideClick est false
|
||||
return;
|
||||
}
|
||||
setOpen(newOpen);
|
||||
if (!newOpen) {
|
||||
setCurrentStepIndex(0);
|
||||
}
|
||||
},
|
||||
[setOpen, closeOnOutsideClick]
|
||||
);
|
||||
|
||||
// Calculer la position du tooltip
|
||||
const getTooltipPosition = () => {
|
||||
if (!currentStep?.target) return "center";
|
||||
|
||||
const element = document.querySelector(currentStep.target);
|
||||
if (!element) return "center";
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Déterminer la meilleure position automatiquement si non spécifiée
|
||||
if (!currentStep.position || currentStep.position === "center") {
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
|
||||
if (centerY < viewportHeight * 0.3) return "bottom";
|
||||
if (centerY > viewportHeight * 0.7) return "top";
|
||||
if (centerX < viewportWidth * 0.3) return "right";
|
||||
if (centerX > viewportWidth * 0.7) return "left";
|
||||
return "bottom";
|
||||
}
|
||||
|
||||
return currentStep.position;
|
||||
};
|
||||
|
||||
const position = getTooltipPosition();
|
||||
|
||||
if (!open || !currentStep) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Overlay avec spotlight */}
|
||||
<div
|
||||
className={cn(
|
||||
"fixed inset-0 z-[60] bg-black/60 transition-opacity duration-300",
|
||||
isHighlighted && "opacity-100"
|
||||
)}
|
||||
onClick={() => handleOpenChange(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Spotlight sur l'élément cible */}
|
||||
{currentStep.target && isHighlighted && (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed z-[61] pointer-events-none transition-all duration-300",
|
||||
"border-4 border-indigo-500 rounded-lg shadow-[0_0_0_9999px_rgba(0,0,0,0.6)]"
|
||||
)}
|
||||
style={{
|
||||
...((() => {
|
||||
const element = document.querySelector(currentStep.target!);
|
||||
if (!element) return {};
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
top: rect.top - 4,
|
||||
left: rect.left - 4,
|
||||
width: rect.width + 8,
|
||||
height: rect.height + 8,
|
||||
};
|
||||
})()),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tooltip du tour */}
|
||||
<Dialog.Root open={open} onOpenChange={handleOpenChange}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Content
|
||||
className={cn(
|
||||
"fixed z-[70] w-[400px] max-w-[90vw]",
|
||||
"bg-white rounded-2xl shadow-2xl border border-slate-200",
|
||||
"transition-all duration-300",
|
||||
position === "center" && "left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2",
|
||||
position === "top" && "left-1/2 -translate-x-1/2 bottom-8",
|
||||
position === "bottom" && "left-1/2 -translate-x-1/2 top-8",
|
||||
position === "left" && "right-8 top-1/2 -translate-y-1/2",
|
||||
position === "right" && "left-8 top-1/2 -translate-y-1/2"
|
||||
)}
|
||||
onPointerDownOutside={(e) => {
|
||||
if (!closeOnOutsideClick) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
onEscapeKeyDown={(e) => {
|
||||
if (!closeOnOutsideClick) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-6 pt-5 pb-3">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<Dialog.Title className="text-lg font-bold text-slate-900">
|
||||
{currentStep.title}
|
||||
</Dialog.Title>
|
||||
<button
|
||||
onClick={() => handleOpenChange(false)}
|
||||
className={cn(
|
||||
"shrink-0 w-6 h-6 rounded flex items-center justify-center",
|
||||
"text-slate-400 hover:text-slate-600 hover:bg-slate-100",
|
||||
"transition-colors duration-150"
|
||||
)}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Progress */}
|
||||
{showProgress && steps.length > 1 && (
|
||||
<div className="mt-3">
|
||||
<div className="flex items-center justify-between text-xs text-slate-500 mb-2">
|
||||
<span>Étape {currentStepIndex + 1} sur {steps.length}</span>
|
||||
<span>{Math.round(((currentStepIndex + 1) / steps.length) * 100)}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-slate-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-indigo-500 to-purple-600 transition-all duration-300 ease-out"
|
||||
style={{
|
||||
width: `${((currentStepIndex + 1) / steps.length) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-6 pb-4">
|
||||
{currentStep.image && (
|
||||
<div className="mb-4 rounded-lg overflow-hidden border border-slate-200">
|
||||
<img
|
||||
src={currentStep.image}
|
||||
alt={currentStep.title}
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm text-slate-600 leading-relaxed space-y-2">
|
||||
{typeof currentStep.content === "string" ? (
|
||||
<p>{currentStep.content}</p>
|
||||
) : (
|
||||
currentStep.content
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between px-6 py-4 bg-slate-50 border-t border-slate-200 rounded-b-2xl">
|
||||
<button
|
||||
onClick={handleSkip}
|
||||
className="text-sm text-slate-500 hover:text-slate-700 transition-colors"
|
||||
>
|
||||
{skipButtonText}
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{!isFirstStep && (
|
||||
<button
|
||||
onClick={prevStep}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-4 py-2 rounded-lg",
|
||||
"text-sm font-medium text-slate-700",
|
||||
"hover:bg-slate-200",
|
||||
"transition-colors duration-150"
|
||||
)}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
Précédent
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={nextStep}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-4 py-2 rounded-lg",
|
||||
"text-sm font-semibold text-white",
|
||||
"bg-gradient-to-r from-indigo-600 to-purple-600",
|
||||
"hover:from-indigo-700 hover:to-purple-700",
|
||||
"shadow-md hover:shadow-lg",
|
||||
"transition-all duration-150"
|
||||
)}
|
||||
>
|
||||
{isLastStep ? (
|
||||
<>
|
||||
<Check className="w-4 h-4" />
|
||||
{completeButtonText}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Suivant
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
72
frontend/src/features/help/components/WelcomeTour.tsx
Normal file
72
frontend/src/features/help/components/WelcomeTour.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { TourGuide } from "./TourGuide";
|
||||
import { useHelpStore } from "../lib/help-store";
|
||||
import { WELCOME_TOUR_STEPS } from "../lib/tours";
|
||||
|
||||
export function WelcomeTour() {
|
||||
const { hasSeenWelcomeTour, autoShowTours, markTourAsSeen } = useHelpStore();
|
||||
|
||||
// Ne pas afficher si déjà vu ou si les tours sont désactivés
|
||||
if (hasSeenWelcomeTour || !autoShowTours) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleComplete = () => {
|
||||
markTourAsSeen("welcome");
|
||||
};
|
||||
|
||||
const handleSkip = () => {
|
||||
markTourAsSeen("welcome");
|
||||
};
|
||||
|
||||
return (
|
||||
<TourGuide
|
||||
steps={WELCOME_TOUR_STEPS}
|
||||
open={!hasSeenWelcomeTour && autoShowTours}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
markTourAsSeen("welcome");
|
||||
}
|
||||
}}
|
||||
onComplete={handleComplete}
|
||||
onSkip={handleSkip}
|
||||
closeOnOutsideClick={false}
|
||||
showProgress={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CorrelationTour() {
|
||||
const { hasSeenCorrelationTour, autoShowTours, markTourAsSeen } = useHelpStore();
|
||||
|
||||
if (hasSeenCorrelationTour || !autoShowTours) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ce tour sera déclenché manuellement depuis l'onglet corrélation
|
||||
return null;
|
||||
}
|
||||
|
||||
export function RegressionTour() {
|
||||
const { hasSeenRegressionTour, autoShowTours, markTourAsSeen } = useHelpStore();
|
||||
|
||||
if (hasSeenRegressionTour || !autoShowTours) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ce tour sera déclenché manuellement depuis la config de régression
|
||||
return null;
|
||||
}
|
||||
|
||||
export function OutlierTour() {
|
||||
const { hasSeenOutlierTour, autoShowTours, markTourAsSeen } = useHelpStore();
|
||||
|
||||
if (hasSeenOutlierTour || !autoShowTours) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ce tour sera déclenché manuellement depuis la grille
|
||||
return null;
|
||||
}
|
||||
11
frontend/src/features/help/index.ts
Normal file
11
frontend/src/features/help/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// Composants
|
||||
export { HelpButton } from "./components/HelpButton";
|
||||
export { HelpTooltip } from "./components/HelpTooltip";
|
||||
export { HelpPanel } from "./components/HelpPanel";
|
||||
export { HelpModal } from "./components/HelpModal";
|
||||
export { TourGuide } from "./components/TourGuide";
|
||||
export type { TourStep } from "./components/TourGuide";
|
||||
export { WelcomeTour } from "./components/WelcomeTour";
|
||||
|
||||
// Store
|
||||
export { useHelpStore } from "./lib/help-store";
|
||||
426
frontend/src/features/insight-panel/components/InsightPanel.tsx
Normal file
426
frontend/src/features/insight-panel/components/InsightPanel.tsx
Normal file
@@ -0,0 +1,426 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useState, useEffect } from "react";
|
||||
import { useGridStore } from "@/store/use-grid-store";
|
||||
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, ReferenceLine, Cell, Tooltip, Tooltip as RechartsTooltip } from "recharts";
|
||||
import { X, AlertTriangle, ShieldCheck, Eye, EyeOff, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface InsightPanelProps {
|
||||
colName: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function InsightPanel({ colName, onClose }: InsightPanelProps) {
|
||||
const { data, detectedOutliers, multivariateOutliers, excludeBatch, toggleRowExclusion, excludedRows, setActiveInsightColumn } = useGridStore();
|
||||
|
||||
// Handle both column-specific and global multivariate outliers
|
||||
const isGlobalView = colName === '__GLOBAL__';
|
||||
const outliers = isGlobalView ? multivariateOutliers : (detectedOutliers[colName] || []);
|
||||
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const [rowsToExclude, setRowsToExclude] = useState<number[]>([]);
|
||||
const [hasExcluded, setHasExcluded] = useState(false);
|
||||
|
||||
// Set active insight column when panel opens, clear when closes
|
||||
useEffect(() => {
|
||||
// Set active column: either specific column or '__GLOBAL__' for multivariate view
|
||||
setActiveInsightColumn(colName);
|
||||
return () => {
|
||||
setActiveInsightColumn(null);
|
||||
};
|
||||
}, [colName, setActiveInsightColumn]);
|
||||
|
||||
// Get full row data for each outlier
|
||||
const outliersWithData = useMemo(() => {
|
||||
return outliers.map((o) => ({
|
||||
...o,
|
||||
rowData: data[o.index] || {},
|
||||
isExcluded: excludedRows.has(o.index)
|
||||
}));
|
||||
}, [outliers, data, excludedRows]);
|
||||
|
||||
// Only non-excluded outliers can be selected
|
||||
const availableOutliers = useMemo(() => {
|
||||
return outliersWithData.filter(o => !o.isExcluded);
|
||||
}, [outliersWithData]);
|
||||
|
||||
// Statistics about what will be excluded
|
||||
const excludeStats = useMemo(() => {
|
||||
const allOutlierIndices = new Set(
|
||||
Object.values(detectedOutliers).flat().map(o => o.index)
|
||||
);
|
||||
const currentColIndices = new Set(outliers.map(o => o.index));
|
||||
|
||||
return {
|
||||
totalInColumn: outliers.length,
|
||||
alreadyExcluded: outliers.filter(o => excludedRows.has(o.index)).length,
|
||||
available: availableOutliers.length,
|
||||
willBeNewlyExcluded: rowsToExclude.filter(i => !excludedRows.has(i)).length,
|
||||
affectedColumns: Object.keys(detectedOutliers).filter(col =>
|
||||
detectedOutliers[col].some(o => rowsToExclude.includes(o.index))
|
||||
).length
|
||||
};
|
||||
}, [detectedOutliers, rowsToExclude, outliers, excludedRows, availableOutliers]);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
const values = data.map(r => Number(r[colName])).filter(v => !isNaN(v));
|
||||
if (values.length === 0) return [];
|
||||
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
const step = (max - min) / 10;
|
||||
|
||||
const bins = Array.from({ length: 10 }, (_, i) => ({
|
||||
range: `${(min + i * step).toFixed(1)}`,
|
||||
count: 0,
|
||||
isOutlier: false
|
||||
}));
|
||||
|
||||
values.forEach(v => {
|
||||
const binIdx = Math.min(Math.floor((v - min) / step), 9);
|
||||
bins[binIdx].count++;
|
||||
});
|
||||
|
||||
return bins;
|
||||
}, [data, colName]);
|
||||
|
||||
const handleToggleRow = (rowIndex: number) => {
|
||||
toggleRowExclusion(rowIndex);
|
||||
setHasExcluded(true);
|
||||
};
|
||||
|
||||
const handleExcludeSelected = () => {
|
||||
if (rowsToExclude.length > 0) {
|
||||
// Only exclude rows that are not already excluded (defensive programming)
|
||||
const newExclusions = rowsToExclude.filter(idx => !excludedRows.has(idx));
|
||||
if (newExclusions.length > 0) {
|
||||
excludeBatch(newExclusions);
|
||||
}
|
||||
setHasExcluded(true);
|
||||
setRowsToExclude([]);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAll = () => {
|
||||
// Toggle selection for available (non-excluded) outliers only
|
||||
const availableIndices = availableOutliers.map(o => o.index);
|
||||
const allAvailableSelected = availableIndices.length > 0 &&
|
||||
availableIndices.every(idx => rowsToExclude.includes(idx));
|
||||
|
||||
if (allAvailableSelected) {
|
||||
// Deselect all available outliers
|
||||
setRowsToExclude(rowsToExclude.filter(idx => !availableIndices.includes(idx)));
|
||||
} else {
|
||||
// Select all available outliers (keep already selected ones)
|
||||
setRowsToExclude([...new Set([...rowsToExclude, ...availableIndices])]);
|
||||
}
|
||||
};
|
||||
|
||||
const allSelected = availableOutliers.length > 0 &&
|
||||
availableOutliers.every(o => rowsToExclude.includes(o.index));
|
||||
|
||||
return (
|
||||
<div className="fixed right-0 top-0 h-screen w-[450px] bg-white border-l border-slate-200 shadow-2xl z-50 flex flex-col animate-in slide-in-from-right duration-300">
|
||||
<header className={`p-4 bg-gradient-to-r text-white flex items-center justify-between ${
|
||||
isGlobalView ? "from-purple-600 to-purple-700" : "from-rose-600 to-rose-700"
|
||||
}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5" />
|
||||
<div>
|
||||
<h3 className="font-bold text-sm">
|
||||
{isGlobalView ? "Anomalies Multivariées" : `Anomalies : ${colName}`}
|
||||
</h3>
|
||||
<p className="text-[10px] opacity-90">
|
||||
{isGlobalView
|
||||
? `${outliers.length} anomalie${outliers.length > 1 ? 's' : ''} globale${outliers.length > 1 ? 's' : ''} détectée${outliers.length > 1 ? 's' : ''}`
|
||||
: `${outliers.length} valeur${outliers.length > 1 ? 's' : ''} détectée${outliers.length > 1 ? 's' : ''}`
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button onClick={onClose} className="hover:bg-white/20 p-1 rounded transition-colors">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
{/* Warning Banner */}
|
||||
<div className={`border p-3 rounded-lg ${
|
||||
isGlobalView
|
||||
? "bg-purple-50 border-purple-200"
|
||||
: "bg-amber-50 border-amber-200"
|
||||
}`}>
|
||||
<p className={`text-[10px] leading-relaxed ${
|
||||
isGlobalView
|
||||
? "text-purple-800"
|
||||
: "text-amber-800"
|
||||
}`}>
|
||||
<strong>
|
||||
{isGlobalView
|
||||
? "🔍 Anomalies Globales :"
|
||||
: "⚠️ Important :"
|
||||
}
|
||||
</strong>{" "}
|
||||
{isGlobalView
|
||||
? "Ces anomalies sont détectées par Isolation Forest en analysant les COMBINAISONS de valeurs across toutes les colonnes numériques. Une ligne peut sembler normale individuellement mais être anormale dans le contexte multivarié."
|
||||
: "Les lignes que vous excluez seront masquées dans toutes les colonnes pour garantir la cohérence des données."
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Summary Stats */}
|
||||
<section className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="bg-slate-50 p-3 rounded-lg border border-slate-100">
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-widest">Total anomalies</p>
|
||||
<p className="text-lg font-bold text-slate-800">{excludeStats.totalInColumn}</p>
|
||||
</div>
|
||||
<div className="bg-slate-50 p-3 rounded-lg border border-slate-100">
|
||||
<p className="text-[10px] text-slate-400 uppercase tracking-widest">Déjà exclues</p>
|
||||
<p className="text-lg font-bold text-rose-600">{excludeStats.alreadyExcluded}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Histogram */}
|
||||
<section className="space-y-3">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Distribution des données</p>
|
||||
<div className="h-40 w-full border border-slate-100 rounded-xl p-2 bg-slate-50">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={chartData}>
|
||||
<XAxis dataKey="range" fontSize={10} hide />
|
||||
<Bar dataKey="count" fill="#818cf8" radius={[2, 2, 0, 0]} />
|
||||
<RechartsTooltip
|
||||
content={({ active, payload }) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-white p-2 border border-slate-200 rounded shadow-lg">
|
||||
<p className="text-xs font-semibold">Valeurs: {payload[0].payload.range}</p>
|
||||
<p className="text-xs">Count: {payload[0].payload.count}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Select / Deselect All */}
|
||||
<section className="flex items-center justify-between p-3 bg-slate-50 rounded-lg border border-slate-100">
|
||||
<span className="text-xs font-medium text-slate-700">
|
||||
{rowsToExclude.length} / {availableOutliers.length} sélectionnée{rowsToExclude.length > 1 ? 's' : ''}
|
||||
{excludeStats.alreadyExcluded > 0 && (
|
||||
<span className="text-slate-400 ml-1">
|
||||
({excludeStats.alreadyExcluded} déjà exclue{excludeStats.alreadyExcluded > 1 ? 's' : ''})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
onClick={toggleAll}
|
||||
disabled={availableOutliers.length === 0}
|
||||
className={cn(
|
||||
"text-xs font-medium",
|
||||
availableOutliers.length === 0
|
||||
? "text-slate-400 cursor-not-allowed"
|
||||
: "text-indigo-600 hover:text-indigo-800"
|
||||
)}
|
||||
>
|
||||
{allSelected ? "Tout désélectionner" : "Tout sélectionner"}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* List of outliers with row data */}
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Détails des anomalies</p>
|
||||
<button
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
className="text-xs text-indigo-600 hover:text-indigo-800 font-medium flex items-center gap-1"
|
||||
>
|
||||
{showDetails ? "Masquer" : "Afficher"} les données
|
||||
{showDetails ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 max-h-[400px] overflow-y-auto">
|
||||
{outliersWithData.length === 0 ? (
|
||||
<div className="text-center py-8 text-slate-400 text-sm">
|
||||
Aucune anomalie détectée dans cette colonne.
|
||||
</div>
|
||||
) : (
|
||||
outliersWithData.map((outlier, i) => {
|
||||
const isSelected = rowsToExclude.includes(outlier.index);
|
||||
const canSelect = !outlier.isExcluded;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
"border rounded-lg p-3 transition-all",
|
||||
canSelect ? "cursor-pointer hover:shadow-md" : "cursor-not-allowed opacity-60",
|
||||
isSelected
|
||||
? "bg-rose-50 border-rose-300 ring-2 ring-rose-200"
|
||||
: outlier.isExcluded
|
||||
? "bg-slate-100 border-slate-200"
|
||||
: "bg-white border-slate-200"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!canSelect) return; // Don't allow selecting already excluded rows
|
||||
|
||||
if (rowsToExclude.includes(outlier.index)) {
|
||||
setRowsToExclude(rowsToExclude.filter(idx => idx !== outlier.index));
|
||||
} else {
|
||||
setRowsToExclude([...rowsToExclude, outlier.index]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
disabled={!canSelect}
|
||||
onChange={() => {}}
|
||||
className={cn(
|
||||
"rounded",
|
||||
canSelect
|
||||
? "border-slate-300 text-rose-600 focus:ring-rose-500"
|
||||
: "border-slate-200 text-slate-400 cursor-not-allowed"
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span className="text-xs font-bold text-slate-700">
|
||||
Ligne {outlier.index + 1}
|
||||
</span>
|
||||
{outlier.isExcluded && (
|
||||
<span className="px-2 py-0.5 bg-slate-200 text-slate-500 text-[9px] rounded font-medium">
|
||||
Déjà exclue
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className={cn(
|
||||
"text-[10px] font-mono px-2 py-0.5 rounded",
|
||||
outlier.isExcluded
|
||||
? "bg-slate-200 text-slate-500"
|
||||
: isGlobalView
|
||||
? "bg-purple-100 text-purple-700"
|
||||
: "bg-rose-100 text-rose-700"
|
||||
)}>
|
||||
{!isGlobalView && outlier.rowData[colName] !== undefined
|
||||
? outlier.rowData[colName].toFixed(4)
|
||||
: isGlobalView
|
||||
? "Multivarié"
|
||||
: 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-slate-500 ml-6 mb-2">
|
||||
{outlier.reasons[0]}
|
||||
</p>
|
||||
|
||||
{showDetails && (
|
||||
<div className="ml-6 p-2 bg-slate-50 rounded border border-slate-100">
|
||||
<p className="text-[9px] text-slate-400 uppercase tracking-wider mb-1">Données complètes de la ligne</p>
|
||||
<div className="grid grid-cols-2 gap-2 text-[10px]">
|
||||
{Object.entries(outlier.rowData).slice(0, 6).map(([key, val]) => (
|
||||
<div key={key} className="truncate" title={String(val)}>
|
||||
<span className="text-slate-400">{key}:</span>
|
||||
<span className="text-slate-700 font-mono ml-1">
|
||||
{typeof val === 'number' ? val.toFixed(2) : String(val)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Selection Summary */}
|
||||
{rowsToExclude.length > 0 && (
|
||||
<section className={`border p-4 rounded-xl space-y-2 ${
|
||||
isGlobalView
|
||||
? "bg-purple-50 border-purple-200"
|
||||
: "bg-indigo-50 border-indigo-200"
|
||||
}`}>
|
||||
<p className={`text-xs font-bold ${
|
||||
isGlobalView
|
||||
? "text-purple-900"
|
||||
: "text-indigo-900"
|
||||
}`}>
|
||||
📋 {excludeStats.willBeNewlyExcluded} nouvelle{excludeStats.willBeNewlyExcluded > 1 ? 's' : ''} ligne{excludeStats.willBeNewlyExcluded > 1 ? 's' : ''} sera{excludeStats.willBeNewlyExcluded > 1 ? 'ont' : ''} exclue{excludeStats.willBeNewlyExcluded > 1 ? 's' : ''}
|
||||
</p>
|
||||
<p className={`text-[10px] ${
|
||||
isGlobalView
|
||||
? "text-purple-700"
|
||||
: "text-indigo-700"
|
||||
}`}>
|
||||
Ces lignes seront masquées dans <strong>toutes</strong> les colonnes.
|
||||
</p>
|
||||
{!isGlobalView && excludeStats.affectedColumns > 1 && (
|
||||
<p className="text-[10px] text-amber-700">
|
||||
⚠️ Affecte aussi {excludeStats.affectedColumns - 1} autre{excludeStats.affectedColumns > 2 ? 's' : ''} colonne{excludeStats.affectedColumns > 2 ? 's' : ''} avec des anomalies.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<footer className="p-6 border-t border-slate-100 bg-slate-50/50 space-y-3">
|
||||
{rowsToExclude.length > 0 && (
|
||||
<div className={`flex items-center gap-2 text-xs font-medium p-2 rounded border ${
|
||||
isGlobalView
|
||||
? "text-purple-600 bg-purple-50 border-purple-100"
|
||||
: "text-emerald-600 bg-emerald-50 border-emerald-100"
|
||||
}`}>
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
<span>
|
||||
{isGlobalView
|
||||
? "L'exclusion des anomalies multivariées améliorera la cohérence globale des données."
|
||||
: "L'exclusion améliorera la qualité de vos analyses."
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleExcludeSelected}
|
||||
disabled={excludeStats.willBeNewlyExcluded === 0}
|
||||
className={cn(
|
||||
"flex-1 py-3 rounded-xl text-sm font-bold shadow-lg transition-all",
|
||||
excludeStats.willBeNewlyExcluded === 0
|
||||
? "bg-slate-200 text-slate-400 cursor-not-allowed"
|
||||
: isGlobalView
|
||||
? "bg-purple-600 hover:bg-purple-700 text-white shadow-purple-100"
|
||||
: "bg-rose-600 hover:bg-rose-700 text-white shadow-rose-100"
|
||||
)}
|
||||
>
|
||||
Exclure ({excludeStats.willBeNewlyExcluded})
|
||||
</button>
|
||||
|
||||
{rowsToExclude.length > 0 && (
|
||||
<button
|
||||
onClick={() => setRowsToExclude([])}
|
||||
className="px-4 py-3 bg-slate-200 hover:bg-slate-300 text-slate-700 rounded-xl text-sm font-bold transition-all"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
475
frontend/src/features/smart-grid/components/SmartGrid.tsx
Normal file
475
frontend/src/features/smart-grid/components/SmartGrid.tsx
Normal file
@@ -0,0 +1,475 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useRef, useState, useEffect } from "react";
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
getFilteredRowModel,
|
||||
flexRender,
|
||||
createColumnHelper,
|
||||
SortingState,
|
||||
ColumnFiltersState,
|
||||
} from "@tanstack/react-table";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useGridStore, ColumnMetadata } from "@/store/use-grid-store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ArrowUp, ArrowDown, ArrowUpDown, Search, AlertCircle, EyeOff, Eye, Info } from "lucide-react";
|
||||
|
||||
const columnHelper = createColumnHelper<any>();
|
||||
|
||||
// --- Header Component ---
|
||||
interface EditableHeaderProps {
|
||||
column: any;
|
||||
metadata: ColumnMetadata;
|
||||
onShowInsights: (colName: string) => void;
|
||||
}
|
||||
|
||||
function EditableHeader({ column, metadata, onShowInsights }: EditableHeaderProps) {
|
||||
const updateColumn = useGridStore((state) => state.updateColumn);
|
||||
const allOutliers = useGridStore((state) => state.detectedOutliers);
|
||||
const multivariateOutliers = useGridStore((state) => state.multivariateOutliers);
|
||||
const outliers = allOutliers[metadata.name] || [];
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [newName, setNewName] = useState(metadata.name);
|
||||
|
||||
const onRename = () => {
|
||||
if (newName !== metadata.name) {
|
||||
updateColumn(metadata.name, { name: newName });
|
||||
}
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const onTypeChange = (type: string) => {
|
||||
updateColumn(metadata.name, { type });
|
||||
};
|
||||
|
||||
const sortDir = column.getIsSorted();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-3 h-full justify-between">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0 overflow-hidden">
|
||||
{isEditing ? (
|
||||
<input
|
||||
autoFocus
|
||||
className="bg-white border border-indigo-300 rounded px-1 text-xs outline-none w-full"
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
onBlur={onRename}
|
||||
onKeyDown={(e) => e.key === 'Enter' && onRename()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="truncate cursor-text font-bold text-slate-700 hover:text-indigo-600 transition-colors text-sm"
|
||||
onClick={(e) => { e.stopPropagation(); setIsEditing(true); }}
|
||||
title={metadata.name}
|
||||
>
|
||||
{metadata.name}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{/* Univariate outlier indicator (column-specific) */}
|
||||
{outliers.length > 0 && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onShowInsights(metadata.name); }}
|
||||
className="bg-rose-500 text-white rounded-full p-0.5 hover:scale-110 transition-transform animate-pulse"
|
||||
title={`${outliers.length} anomalie${outliers.length > 1 ? 's' : ''} détectée${outliers.length > 1 ? 's' : ''} dans cette colonne`}
|
||||
>
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Multivariate outlier indicator (global) - shows for ALL numeric columns when multivariate outliers exist */}
|
||||
{metadata.type === 'numeric' && multivariateOutliers.length > 0 && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onShowInsights('__GLOBAL__'); }}
|
||||
className="bg-purple-500 text-white rounded-full p-0.5 hover:scale-110 transition-transform"
|
||||
title={`${multivariateOutliers.length} anomalie${multivariateOutliers.length > 1 ? 's' : ''} multivariée${multivariateOutliers.length > 1 ? 's' : ''} détectée${multivariateOutliers.length > 1 ? 's' : ''}`}
|
||||
>
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={column.getToggleSortingHandler()}
|
||||
className={cn(
|
||||
"text-slate-400 hover:text-indigo-600 transition-colors p-1 rounded hover:bg-slate-100",
|
||||
sortDir && "text-indigo-600 bg-indigo-50"
|
||||
)}
|
||||
>
|
||||
{sortDir === 'asc' ? <ArrowUp className="w-3 h-3" /> :
|
||||
sortDir === 'desc' ? <ArrowDown className="w-3 h-3" /> :
|
||||
<ArrowUpDown className="w-3 h-3 opacity-50" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
className="text-[10px] bg-slate-100 border border-slate-200 rounded px-1.5 py-0.5 font-semibold uppercase text-slate-500 cursor-pointer hover:bg-slate-200 transition-colors focus:ring-1 focus:ring-indigo-500 outline-none w-16"
|
||||
value={metadata.type}
|
||||
onChange={(e) => onTypeChange(e.target.value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<option value="numeric">Num</option>
|
||||
<option value="categorical">Cat</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="boolean">Bool</option>
|
||||
</select>
|
||||
|
||||
<div className="relative flex-1 min-w-0">
|
||||
<Search className="absolute left-1.5 top-1/2 -translate-y-1/2 w-3 h-3 text-slate-400" />
|
||||
<input
|
||||
value={(column.getFilterValue() as string) ?? ""}
|
||||
onChange={(e) => column.setFilterValue(e.target.value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
placeholder="Filtrer..."
|
||||
className="w-full bg-white border border-slate-200 rounded pl-6 pr-2 py-0.5 text-[10px] outline-none focus:border-indigo-400 transition-all placeholder:text-slate-300"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-1 w-full bg-slate-100 rounded-full overflow-hidden mt-1">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full transition-all",
|
||||
metadata.type === 'numeric' ? "bg-indigo-500 w-full" :
|
||||
metadata.type === 'date' ? "bg-emerald-500 w-full" : "bg-slate-400 w-1/2"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ... (Rest of the file remains same, keeping Cell and SmartGrid logic) ...
|
||||
|
||||
// --- Cell Component ---
|
||||
interface EditableCellProps {
|
||||
initialValue: any;
|
||||
rowIndex: number;
|
||||
colName: string;
|
||||
type: string;
|
||||
isExcluded: boolean;
|
||||
activeInsightColumn: string | null;
|
||||
}
|
||||
|
||||
function EditableCell({ initialValue, rowIndex, colName, type, isExcluded, activeInsightColumn }: EditableCellProps) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const updateCellValue = useGridStore((state) => state.updateCellValue);
|
||||
const isModified = useGridStore((state) => state.modifiedCells.has(`${rowIndex}:${colName}`));
|
||||
const detectedOutliers = useGridStore((state) => state.detectedOutliers);
|
||||
const multivariateOutliers = useGridStore((state) => state.multivariateOutliers);
|
||||
|
||||
// Check if this specific cell is a univariate outlier
|
||||
const columnOutliers = detectedOutliers[colName] || [];
|
||||
const isUnivariateOutlier = columnOutliers.some((o: any) => o.index === rowIndex);
|
||||
|
||||
// Check if this row is a multivariate outlier
|
||||
const isMultivariateOutlier = multivariateOutliers.some((o: any) => o.index === rowIndex);
|
||||
|
||||
// Check if this cell should be highlighted (active insight column AND is outlier)
|
||||
const shouldHighlightRed = activeInsightColumn === colName && isUnivariateOutlier;
|
||||
|
||||
// Check if this cell should be highlighted purple (global multivariate view is active)
|
||||
const shouldHighlightPurple = activeInsightColumn === '__GLOBAL__' && isMultivariateOutlier;
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
const onCommit = () => {
|
||||
if (value !== initialValue) {
|
||||
if (type === 'numeric' && isNaN(Number(value))) {
|
||||
setValue(initialValue);
|
||||
} else {
|
||||
updateCellValue(rowIndex, colName, type === 'numeric' ? Number(value) : value);
|
||||
}
|
||||
}
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<input
|
||||
autoFocus
|
||||
className={cn(
|
||||
"w-full h-full border-2 outline-none px-2 py-1 font-mono text-sm absolute inset-0 z-20",
|
||||
(isUnivariateOutlier || isMultivariateOutlier)
|
||||
? "bg-rose-50 border-rose-500 text-rose-900"
|
||||
: "bg-indigo-50 border-indigo-500 text-indigo-900"
|
||||
)}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={onCommit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') onCommit();
|
||||
if (e.key === 'Escape') {
|
||||
setValue(initialValue);
|
||||
setIsEditing(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"px-3 py-2.5 truncate font-mono text-sm h-full flex items-center relative",
|
||||
type === 'numeric' ? "justify-end text-slate-700" : "justify-start text-slate-600",
|
||||
isModified && !shouldHighlightRed && !shouldHighlightPurple && "bg-amber-50 font-bold text-amber-800",
|
||||
isExcluded && "opacity-30 italic line-through decoration-slate-400 bg-slate-50",
|
||||
// Red highlight: univariate outliers for specific column
|
||||
shouldHighlightRed && !isExcluded && "bg-rose-200 text-rose-900 font-bold border-4 border-rose-500 shadow-lg shadow-rose-200 animate-pulse",
|
||||
// Purple highlight: multivariate outliers (global view)
|
||||
shouldHighlightPurple && !isExcluded && "bg-purple-200 text-purple-900 font-bold border-4 border-purple-500 shadow-lg shadow-purple-200 animate-pulse"
|
||||
)}
|
||||
onDoubleClick={() => setIsEditing(true)}
|
||||
title={
|
||||
shouldHighlightRed && isUnivariateOutlier ? `⚠️ Outlier univarié: ${value?.toString()}` :
|
||||
shouldHighlightPurple && isMultivariateOutlier ? `🔍 Outlier multivarié: ${value?.toString()}` :
|
||||
value?.toString()
|
||||
}
|
||||
>
|
||||
{shouldHighlightRed && isUnivariateOutlier && (
|
||||
<AlertCircle className="w-3 h-3 text-rose-600 absolute top-1 left-1 shrink-0" />
|
||||
)}
|
||||
{shouldHighlightPurple && isMultivariateOutlier && (
|
||||
<AlertCircle className="w-3 h-3 text-purple-600 absolute top-1 left-1 shrink-0" />
|
||||
)}
|
||||
<span className={(shouldHighlightRed || shouldHighlightPurple) ? "ml-4" : ""}>{value?.toString() ?? ""}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Legend Component ---
|
||||
function AnomalyLegend() {
|
||||
const allOutliers = useGridStore((state) => state.detectedOutliers);
|
||||
const multivariateOutliers = useGridStore((state) => state.multivariateOutliers);
|
||||
|
||||
// Check if there are any anomalies to show legend for
|
||||
const hasUnivariate = Object.values(allOutliers).some(outliers => outliers.length > 0);
|
||||
const hasMultivariate = multivariateOutliers.length > 0;
|
||||
|
||||
// Only show legend if there are anomalies
|
||||
if (!hasUnivariate && !hasMultivariate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-4 py-2.5 bg-gradient-to-r from-slate-50 to-indigo-50/30 border-b border-slate-200 rounded-t-xl">
|
||||
<div className="flex items-center gap-1.5 text-slate-600">
|
||||
<Info className="w-4 h-4 text-indigo-500" />
|
||||
<span className="text-xs font-semibold uppercase tracking-wide">Légende des anomalies</span>
|
||||
</div>
|
||||
|
||||
<div className="h-4 w-px bg-slate-300" />
|
||||
|
||||
{hasUnivariate && (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-white rounded-lg border border-slate-200 shadow-sm">
|
||||
<div className="bg-rose-500 text-white rounded-full p-0.5">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
</div>
|
||||
<span className="text-xs text-slate-700 font-medium">
|
||||
Anomalies dans cette colonne
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasMultivariate && (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-white rounded-lg border border-slate-200 shadow-sm">
|
||||
<div className="bg-purple-500 text-white rounded-full p-0.5">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
</div>
|
||||
<span className="text-xs text-slate-700 font-medium">
|
||||
Anomalies globales (multivariées)
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="ml-auto text-[10px] text-slate-400 italic">
|
||||
Cliquez sur un indicateur pour voir les détails
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Main Grid Component ---
|
||||
interface SmartGridProps {
|
||||
onShowInsights: (colName: string) => void;
|
||||
}
|
||||
|
||||
export function SmartGrid({ onShowInsights }: SmartGridProps) {
|
||||
const { data, columns: metadata, isLoading, excludedRows, toggleRowExclusion, detectedOutliers, multivariateOutliers, activeInsightColumn } = useGridStore();
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
|
||||
// Check if there are any anomalies for conditional styling
|
||||
const hasUnivariate = Object.values(detectedOutliers).some(outliers => outliers.length > 0);
|
||||
const hasMultivariate = multivariateOutliers.length > 0;
|
||||
const hasAnomalies = hasUnivariate || hasMultivariate;
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return metadata.map((col) =>
|
||||
columnHelper.accessor(col.name, {
|
||||
header: ({ column }) => <EditableHeader column={column} metadata={col} onShowInsights={onShowInsights} />,
|
||||
cell: ({ getValue, row }) => (
|
||||
<EditableCell
|
||||
initialValue={getValue()}
|
||||
rowIndex={row.index}
|
||||
colName={col.name}
|
||||
type={col.type}
|
||||
isExcluded={excludedRows.has(row.index)}
|
||||
activeInsightColumn={activeInsightColumn}
|
||||
/>
|
||||
),
|
||||
size: 180,
|
||||
minSize: 120,
|
||||
maxSize: 400,
|
||||
})
|
||||
);
|
||||
}, [metadata, excludedRows, onShowInsights, activeInsightColumn]);
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
},
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
defaultColumn: {
|
||||
size: 180,
|
||||
},
|
||||
});
|
||||
|
||||
const { rows } = table.getRowModel();
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: rows.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 40,
|
||||
overscan: 20,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full space-y-4 animate-pulse">
|
||||
<div className="w-12 h-12 bg-slate-200 rounded-full"></div>
|
||||
<p className="text-slate-500 font-medium">Chargement des données...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full border-2 border-dashed border-slate-200 rounded-xl bg-slate-50/50">
|
||||
<p className="text-slate-400 font-medium">Aucune donnée chargée. Veuillez uploader un fichier.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full border border-slate-200 rounded-xl bg-white shadow-sm overflow-hidden">
|
||||
{/* Legend Section */}
|
||||
<AnomalyLegend />
|
||||
|
||||
{/* Table Container */}
|
||||
<div ref={parentRef} className={cn("flex-1 overflow-auto relative", !hasAnomalies && "rounded-t-xl")}>
|
||||
<table className="border-separate border-spacing-0 w-full" style={{ width: table.getTotalSize() }}>
|
||||
<thead className="sticky top-0 z-20 bg-slate-50 shadow-sm">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{/* Fixed Row Number Column Header */}
|
||||
<th className="sticky left-0 z-30 w-12 border-b border-r border-slate-200 bg-slate-100/90 backdrop-blur px-2 text-[10px] font-mono text-slate-500 text-center uppercase tracking-tighter">
|
||||
#
|
||||
</th>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th
|
||||
key={header.id}
|
||||
style={{ width: header.getSize() }}
|
||||
className="border-b border-r border-slate-200 text-left align-top bg-slate-50/90 backdrop-blur p-0"
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const row = rows[virtualRow.index];
|
||||
const isExcluded = excludedRows.has(row.index);
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={cn(
|
||||
"group transition-colors",
|
||||
isExcluded ? "bg-slate-50" : "hover:bg-indigo-50/10"
|
||||
)}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
{/* Fixed Row Number Cell */}
|
||||
<td className="sticky left-0 z-10 w-12 border-b border-r border-slate-100 px-0 bg-white group-hover:bg-slate-50">
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<button
|
||||
onClick={() => toggleRowExclusion(row.index)}
|
||||
className={cn(
|
||||
"p-1 rounded transition-all opacity-0 group-hover:opacity-100 focus:opacity-100",
|
||||
isExcluded ? "text-rose-500 opacity-100" : "text-slate-300 hover:text-slate-500"
|
||||
)}
|
||||
title={isExcluded ? "Inclure" : "Exclure"}
|
||||
>
|
||||
{isExcluded ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
<span className={cn("text-[10px] font-mono text-slate-400 absolute", isExcluded || "group-hover:opacity-0")}>
|
||||
{virtualRow.index + 1}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td
|
||||
key={cell.id}
|
||||
style={{ width: cell.column.getSize() }}
|
||||
className="border-b border-r border-slate-100 p-0 relative"
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
113
frontend/src/features/uploader/components/FileUploader.tsx
Normal file
113
frontend/src/features/uploader/components/FileUploader.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
"use client";
|
||||
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { useGridStore } from "@/store/use-grid-store";
|
||||
import { parseArrowStream } from "@/lib/arrow-client";
|
||||
import { Upload, FileSpreadsheet, Loader2, AlertCircle } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { getApiUrl } from "@/lib/api-config";
|
||||
|
||||
export function FileUploader() {
|
||||
const { setLoading, setData, setError, isLoading } = useGridStore();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const handleFile = async (file: File) => {
|
||||
console.log("=== UPLOAD START ===", file.name, file.size);
|
||||
setLoading(true);
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
try {
|
||||
console.log("Fetching:", getApiUrl("/upload"));
|
||||
const response = await fetch(getApiUrl("/upload"), {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
console.log("Response status:", response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) throw new Error("Backend non trouvé (404). Vérifiez l'URL.");
|
||||
if (response.status === 500) throw new Error("Erreur interne du serveur.");
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
|
||||
const metadataStr = response.headers.get("X-Column-Metadata");
|
||||
console.log("Metadata:", metadataStr);
|
||||
const metadata = metadataStr ? JSON.parse(metadataStr) : [];
|
||||
console.log("Parsing Arrow...");
|
||||
const data = await parseArrowStream(response);
|
||||
console.log("Data parsed, rows:", data.length);
|
||||
|
||||
setData(data, metadata);
|
||||
console.log("=== UPLOAD SUCCESS ===");
|
||||
} catch (err: any) {
|
||||
console.error("Upload Error:", err);
|
||||
// Check for network error (Failed to fetch)
|
||||
if (err.message === "Failed to fetch") {
|
||||
setError("Impossible de contacter le serveur. Le backend est-il lancé sur le port 8000 ?");
|
||||
} else {
|
||||
setError(err.message || "Échec de l'upload");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const onDragLeave = () => setIsDragging(false);
|
||||
|
||||
const onDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (file) handleFile(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative group flex items-center gap-6 p-6 rounded-2xl border-2 border-dashed transition-all duration-300 cursor-pointer overflow-hidden",
|
||||
isDragging
|
||||
? "border-indigo-500 bg-indigo-50/50 scale-[1.02] shadow-xl shadow-indigo-100"
|
||||
: "border-slate-200 bg-white hover:border-indigo-300 hover:shadow-md"
|
||||
)}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"
|
||||
accept=".xlsx,.xls,.csv"
|
||||
onChange={(e) => e.target.files?.[0] && handleFile(e.target.files[0])}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
<div className={cn(
|
||||
"p-4 rounded-xl transition-colors",
|
||||
isDragging ? "bg-indigo-100 text-indigo-600" : "bg-slate-100 text-slate-500 group-hover:bg-indigo-50 group-hover:text-indigo-600"
|
||||
)}>
|
||||
{isLoading ? <Loader2 className="w-8 h-8 animate-spin" /> : <Upload className="w-8 h-8" />}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-1">
|
||||
<h3 className="text-lg font-bold text-slate-900 group-hover:text-indigo-700 transition-colors">
|
||||
{isLoading ? "Traitement en cours..." : "Déposez votre fichier ici"}
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500">
|
||||
Excel (.xlsx) ou CSV. <span className="text-xs bg-slate-100 px-2 py-0.5 rounded text-slate-400">Max 50MB</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:flex items-center gap-2 text-xs font-medium text-slate-400 bg-slate-50 px-3 py-1.5 rounded-lg border border-slate-100">
|
||||
<FileSpreadsheet className="w-4 h-4" />
|
||||
<span>Auto-Detect</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
249
frontend/src/store/use-grid-store.ts
Normal file
249
frontend/src/store/use-grid-store.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { create } from 'zustand';
|
||||
import { temporal } from 'zundo';
|
||||
|
||||
export interface ColumnMetadata {
|
||||
name: string;
|
||||
type: string;
|
||||
native_type: string;
|
||||
}
|
||||
|
||||
export interface Outlier {
|
||||
index: number;
|
||||
reasons: string[];
|
||||
}
|
||||
|
||||
export type ModelType = 'linear' | 'logistic' | 'polynomial' | 'exponential';
|
||||
|
||||
export interface AnalysisResults {
|
||||
r_squared: number;
|
||||
adj_r_squared: number | null;
|
||||
p_values: Record<string, number>;
|
||||
coefficients: Record<string, number>;
|
||||
std_errors: Record<string, number>;
|
||||
sample_size: number;
|
||||
residuals: number[];
|
||||
fit_plot: { real: number, pred: number }[]; // Changed to real vs pred structure
|
||||
}
|
||||
|
||||
interface GridState {
|
||||
data: any[];
|
||||
columns: ColumnMetadata[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
modifiedCells: Set<string>;
|
||||
detectedOutliers: Record<string, Outlier[]>;
|
||||
multivariateOutliers: Outlier[]; // New: Separate storage for multivariate outliers
|
||||
excludedRows: Set<number>;
|
||||
activeInsightColumn: string | null; // Column currently being viewed in InsightPanel
|
||||
|
||||
// Analysis Config
|
||||
targetVariable: string | null;
|
||||
selectedFeatures: string[];
|
||||
modelType: ModelType;
|
||||
polyDegree: number;
|
||||
includeInteractions: boolean;
|
||||
analysisResults: AnalysisResults | null;
|
||||
|
||||
setLoading: (loading: boolean) => void;
|
||||
setData: (data: any[], columns: ColumnMetadata[]) => void;
|
||||
setError: (error: string | null) => void;
|
||||
updateColumn: (oldName: string, newMetadata: Partial<ColumnMetadata>) => void;
|
||||
updateCellValue: (rowIndex: number, colName: string, value: any) => void;
|
||||
setOutliers: (colName: string, outliers: Outlier[]) => void;
|
||||
setMultivariateOutliers: (outliers: Outlier[]) => void; // New: Setter for multivariate outliers
|
||||
toggleRowExclusion: (rowIndex: number) => void;
|
||||
excludeBatch: (rowIndices: number[]) => void;
|
||||
setActiveInsightColumn: (colName: string | null) => void;
|
||||
setTargetVariable: (name: string | null) => void;
|
||||
setSelectedFeatures: (names: string[]) => void;
|
||||
setModelType: (type: ModelType) => void;
|
||||
setPolyDegree: (degree: number) => void;
|
||||
setIncludeInteractions: (include: boolean) => void;
|
||||
setAnalysisResults: (results: AnalysisResults | null) => void;
|
||||
getCleanData: () => any[];
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export const useGridStore = create<GridState>()(
|
||||
temporal(
|
||||
(set, get) => ({
|
||||
data: [],
|
||||
columns: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
modifiedCells: new Set(),
|
||||
detectedOutliers: {},
|
||||
multivariateOutliers: [], // New: Initialize multivariate outliers
|
||||
excludedRows: new Set(),
|
||||
activeInsightColumn: null, // Initialize active insight column
|
||||
targetVariable: null,
|
||||
selectedFeatures: [],
|
||||
modelType: 'linear',
|
||||
polyDegree: 2,
|
||||
includeInteractions: false,
|
||||
analysisResults: null,
|
||||
|
||||
setLoading: (isLoading) => set({ isLoading }),
|
||||
setData: (data, columns) => set({
|
||||
data,
|
||||
columns,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
modifiedCells: new Set(),
|
||||
detectedOutliers: {},
|
||||
multivariateOutliers: [], // Reset multivariate outliers
|
||||
excludedRows: new Set(),
|
||||
activeInsightColumn: null, // Reset active insight column
|
||||
targetVariable: null,
|
||||
selectedFeatures: [],
|
||||
modelType: 'linear',
|
||||
polyDegree: 2,
|
||||
includeInteractions: false,
|
||||
analysisResults: null
|
||||
}),
|
||||
setError: (error) => set({ error, isLoading: false }),
|
||||
|
||||
updateColumn: (oldName, newMetadata) => set((state) => {
|
||||
const newColumns = state.columns.map((col) => col.name === oldName ? { ...col, ...newMetadata } : col);
|
||||
let newData = state.data;
|
||||
if (newMetadata.name && newMetadata.name !== oldName) {
|
||||
newData = state.data.map((row) => {
|
||||
const newRow = { ...row };
|
||||
newRow[newMetadata.name!] = newRow[oldName];
|
||||
delete newRow[oldName];
|
||||
return newRow;
|
||||
});
|
||||
}
|
||||
return { columns: newColumns, data: newData };
|
||||
}),
|
||||
|
||||
updateCellValue: (rowIndex, colName, value) => set((state) => {
|
||||
const newData = [...state.data];
|
||||
newData[rowIndex] = { ...newData[rowIndex], [colName]: value };
|
||||
const newModified = new Set(state.modifiedCells);
|
||||
newModified.add(`${rowIndex}:${colName}`);
|
||||
return { data: newData, modifiedCells: newModified };
|
||||
}),
|
||||
|
||||
setOutliers: (colName, outliers) => set((state) => ({
|
||||
detectedOutliers: { ...state.detectedOutliers, [colName]: outliers }
|
||||
})),
|
||||
|
||||
setMultivariateOutliers: (outliers) => set({ multivariateOutliers: outliers }), // New: Setter for multivariate outliers
|
||||
|
||||
setActiveInsightColumn: (colName) => set({ activeInsightColumn: colName }), // Set which column's insights are being viewed
|
||||
|
||||
toggleRowExclusion: (rowIndex) => set((state) => {
|
||||
const newExcluded = new Set(state.excludedRows);
|
||||
const isRemoving = newExcluded.has(rowIndex);
|
||||
|
||||
if (isRemoving) {
|
||||
newExcluded.delete(rowIndex);
|
||||
} else {
|
||||
newExcluded.add(rowIndex);
|
||||
}
|
||||
|
||||
// Si on exclut une ligne, retirer ses outliers des listes
|
||||
let newDetectedOutliers = state.detectedOutliers;
|
||||
let newMultivariateOutliers = state.multivariateOutliers;
|
||||
|
||||
if (!isRemoving) {
|
||||
// Retirer des detectedOutliers
|
||||
newDetectedOutliers = { ...state.detectedOutliers };
|
||||
Object.keys(newDetectedOutliers).forEach(colName => {
|
||||
newDetectedOutliers[colName] = newDetectedOutliers[colName].filter(
|
||||
outlier => outlier.index !== rowIndex
|
||||
);
|
||||
});
|
||||
|
||||
// Retirer des multivariateOutliers
|
||||
newMultivariateOutliers = state.multivariateOutliers.filter(
|
||||
outlier => outlier.index !== rowIndex
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
excludedRows: newExcluded,
|
||||
detectedOutliers: newDetectedOutliers,
|
||||
multivariateOutliers: newMultivariateOutliers
|
||||
};
|
||||
}),
|
||||
|
||||
excludeBatch: (rowIndices) => set((state) => {
|
||||
const newExcluded = new Set(state.excludedRows);
|
||||
rowIndices.forEach(idx => newExcluded.add(idx));
|
||||
|
||||
// Retirer les outliers exclus de detectedOutliers
|
||||
const newDetectedOutliers = { ...state.detectedOutliers };
|
||||
Object.keys(newDetectedOutliers).forEach(colName => {
|
||||
newDetectedOutliers[colName] = newDetectedOutliers[colName].filter(
|
||||
outlier => !newExcluded.has(outlier.index)
|
||||
);
|
||||
});
|
||||
|
||||
// Retirer les outliers exclus de multivariateOutliers
|
||||
const newMultivariateOutliers = state.multivariateOutliers.filter(
|
||||
outlier => !newExcluded.has(outlier.index)
|
||||
);
|
||||
|
||||
return {
|
||||
excludedRows: newExcluded,
|
||||
detectedOutliers: newDetectedOutliers,
|
||||
multivariateOutliers: newMultivariateOutliers
|
||||
};
|
||||
}),
|
||||
|
||||
setTargetVariable: (targetVariable) => set({ targetVariable }),
|
||||
setSelectedFeatures: (selectedFeatures) => set({ selectedFeatures }),
|
||||
setModelType: (modelType) => set({ modelType }),
|
||||
setPolyDegree: (polyDegree) => set({ polyDegree }),
|
||||
setIncludeInteractions: (includeInteractions) => set({ includeInteractions }),
|
||||
setAnalysisResults: (analysisResults) => set({ analysisResults }),
|
||||
|
||||
getCleanData: () => {
|
||||
const { data, excludedRows } = get();
|
||||
return data.filter((_, index) => !excludedRows.has(index));
|
||||
},
|
||||
|
||||
reset: () => set({
|
||||
data: [], columns: [], isLoading: false, error: null,
|
||||
modifiedCells: new Set(), detectedOutliers: {}, multivariateOutliers: [], excludedRows: new Set(),
|
||||
targetVariable: null, selectedFeatures: [], modelType: 'linear',
|
||||
polyDegree: 2, includeInteractions: false,
|
||||
analysisResults: null
|
||||
}),
|
||||
}),
|
||||
{
|
||||
partialize: (state) => {
|
||||
const {
|
||||
data,
|
||||
columns,
|
||||
modifiedCells,
|
||||
excludedRows,
|
||||
detectedOutliers,
|
||||
multivariateOutliers,
|
||||
activeInsightColumn,
|
||||
targetVariable,
|
||||
selectedFeatures,
|
||||
modelType,
|
||||
polyDegree,
|
||||
includeInteractions
|
||||
} = state;
|
||||
return {
|
||||
data,
|
||||
columns,
|
||||
modifiedCells,
|
||||
excludedRows,
|
||||
detectedOutliers,
|
||||
multivariateOutliers,
|
||||
activeInsightColumn,
|
||||
targetVariable,
|
||||
selectedFeatures,
|
||||
modelType,
|
||||
polyDegree,
|
||||
includeInteractions
|
||||
};
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
34
frontend/tsconfig.json
Normal file
34
frontend/tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user