# ============================================================================
# [LIVRE] ANALYSE DE DONNÉES AVEC PYTHON - GUIDE ULTRA-DÉTAILLÉ POUR DÉBUTANTS
# ============================================================================
#
# [OBJECTIF] GUIDE COMPLET POUR MAÎTRISER L'ANALYSE DE DONNÉES DE ZÉRO À EXPERT
#
# PARTIE 0  : INTRODUCTION & CONFIGURATION (ce fichier)
# PARTIE 1  : PYTHON DE BASE POUR LA DATA
# PARTIE 2  : NUMPY - TABLEAUX NUMÉRIQUES
# PARTIE 3  : PANDAS - MANIPULATION DE DONNÉES
# PARTIE 4  : VISUALISATION (Matplotlib & Seaborn)
# PARTIE 5  : STATISTIQUES DESCRIPTIVES
# PARTIE 6  : NETTOYAGE ET PRÉPARATION DES DONNÉES
# PARTIE 7  : ANALYSE EXPLORATOIRE (EDA)
# PARTIE 8  : CORRÉLATIONS ET RELATIONS
# PARTIE 9  : INTRODUCTION AU MACHINE LEARNING (Scikit-learn)
# PARTIE 10 : PROJETS COMPLETS
#
# [TEMPS] TEMPS TOTAL : ~40-50 heures
# [DOCS] PRÉREQUIS : Python de base (variables, fonctions, listes)
# ============================================================================

"""
[OBJECTIF] PHILOSOPHIE DE CE GUIDE

COMMENT ?  -> Explications pas à pas
POURQUOI ? -> Raisons et contexte
QUAND ?    -> Cas d'usage concrets
PRATIQUE   -> Exemples réels et exercices

Ce guide vise à être VOTRE SEULE RÉFÉRENCE pour l'analyse de données !
"""

# ============================================================================
# [GUIDE] CHAPITRE 0.1 : QU'EST-CE QUE L'ANALYSE DE DONNÉES ?
# ============================================================================

"""
DÉFINITION SIMPLE

L'analyse de données = processus d'inspecter, nettoyer, transformer
et modéliser des données pour DÉCOUVRIR des informations utiles,
tirer des CONCLUSIONS et prendre des DÉCISIONS.

ANALOGIE [RECHERCHE]
Imaginez que vous êtes un détective :
- Les données = les indices
- L'analyse = l'enquête
- Les résultats = la solution du mystère


POURQUOI PYTHON POUR LA DATA ?

[OK] GRATUIT et open-source
[OK] Syntaxe simple et lisible
[OK] Bibliothèques spécialisées puissantes (NumPy, Pandas, Matplotlib...)
[OK] Grande communauté
[OK] Utilisé par Google, Netflix, NASA, CERN...
[OK] Machine Learning et IA intégrés


CYCLE D'ANALYSE DE DONNÉES

1. [ENTREE] COLLECTE      -> Obtenir les données
2. [NETTOYAGE] NETTOYAGE     -> Traiter les données manquantes/erronées
3. [RECHERCHE] EXPLORATION   -> Comprendre la structure
4. [GRAPHIQUE] VISUALISATION -> Créer des graphiques
5. [HAUSSE] ANALYSE       -> Appliquer des statistiques
6. [IDEE] CONCLUSIONS   -> Interpréter et communiquer

Vous apprendrez CHAQUE étape dans ce guide !
"""


# ============================================================================
# [GUIDE] CHAPITRE 0.2 : L'ÉCOSYSTÈME DATA PYTHON
# ============================================================================

"""
LES BIBLIOTHÈQUES ESSENTIELLES

┌─────────────────────────────────────────────────────────┐
│                ÉCOSYSTÈME DATA PYTHON                    │
├──────────────┬──────────────────────────────────────────┤
│  NumPy       │ Tableaux numériques ultra-rapides         │
│  Pandas      │ Manipulation de données (tableaux/séries) │
│  Matplotlib  │ Visualisation de base                     │
│  Seaborn     │ Visualisation statistique avancée         │
│  SciPy       │ Statistiques et math scientifique         │
│  Scikit-learn│ Machine Learning                          │
│  Plotly      │ Graphiques interactifs                    │
│  Statsmodels │ Modèles statistiques avancés              │
└──────────────┴──────────────────────────────────────────┘


ANALOGIE DES BIBLIOTHÈQUES

NumPy    = La calculatrice scientifique (calculs rapides)
Pandas   = Excel sous stéroïdes (tableaux, filtres, groupes)
Matplotlib = Le crayon et le papier (dessiner n'importe quoi)
Seaborn  = L'artiste (beaux graphiques automatiques)
Scikit-learn = Le professeur (apprendre des modèles)
"""


# ============================================================================
# [GUIDE] CHAPITRE 0.3 : INSTALLATION ET CONFIGURATION
# ============================================================================

"""
OPTION 1 : ANACONDA (RECOMMANDÉ POUR DÉBUTANTS)

Anaconda = Distribution Python complète pour la science des données
- Inclut Python + toutes les bibliothèques data
- Interface graphique (Anaconda Navigator)
- Jupyter Notebook inclus

Télécharger : https://www.anaconda.com/download

OPTION 2 : INSTALLATION MANUELLE

1. Installer Python (python.org)
2. Créer environnement virtuel
3. Installer bibliothèques
"""

# Étape par étape :

# 1. Créer environnement virtuel
python -m venv data_env

# 2. Activer (Mac/Linux)
source data_env/bin/activate

# 2. Activer (Windows)
data_env\Scripts\activate

# 3. Installer toutes les bibliothèques
pip install numpy pandas matplotlib seaborn scipy scikit-learn jupyter plotly openpyxl xlrd

# 4. Créer requirements.txt
pip freeze > requirements.txt

# 5. Vérifier installation
python -c "import numpy, pandas, matplotlib, seaborn; print('Tout installé !')"

"""
JUPYTER NOTEBOOK vs VS CODE vs SCRIPTS .py

JUPYTER NOTEBOOK [OK] (Recommandé pour débuter)
- Interface web interactive
- Voir résultats cellule par cellule
- Graphiques inline
- Parfait pour exploration

VS CODE
- IDE complet
- Supporte Jupyter
- Bon pour projets

SCRIPTS .py
- Fichiers Python classiques
- Automatisation
- Production
"""


# ============================================================================
# [GUIDE] CHAPITRE 0.4 : LANCER JUPYTER NOTEBOOK
# ============================================================================

"""
DÉMARRER JUPYTER
"""

# Terminal (avec environnement activé)
jupyter notebook

# Ou Jupyter Lab (interface améliorée)
jupyter lab

"""
RACCOURCIS JUPYTER ESSENTIELS

MODE COMMANDE (touche Esc) :
  A  = Insérer cellule AU-DESSUS
  B  = Insérer cellule EN-DESSOUS
  D,D = Supprimer cellule
  M  = Convertir en Markdown
  Y  = Convertir en Code
  Z  = Annuler
  Shift+Enter = Exécuter et passer à la suivante

MODE ÉDITION (touche Enter) :
  Shift+Enter = Exécuter cellule
  Ctrl+Enter  = Exécuter sans avancer
  Tab         = Autocomplétion
  Shift+Tab   = Documentation


STRUCTURE D'UN NOTEBOOK

# Cellule Markdown (documentation)
## Titre
Description de ce que vous faites

# Cellule Code
import pandas as pd
df = pd.read_csv('data.csv')
df.head()  # Affiche les 5 premières lignes
"""


# ============================================================================
# [GUIDE] CHAPITRE 0.5 : STRUCTURE D'UN PROJET DATA
# ============================================================================

"""
ORGANISATION RECOMMANDÉE
"""

mon_projet_data/
├── data/
│   ├── raw/              # Données brutes (ne JAMAIS modifier)
│   │   └── dataset.csv
│   ├── processed/        # Données nettoyées
│   │   └── clean_data.csv
│   └── external/         # Données externes
├── notebooks/            # Jupyter Notebooks
│   ├── 01_exploration.ipynb
│   ├── 02_cleaning.ipynb
│   ├── 03_analysis.ipynb
│   └── 04_visualisation.ipynb
├── scripts/              # Scripts Python
│   ├── data_loading.py
│   └── utils.py
├── outputs/              # Résultats
│   ├── figures/
│   └── reports/
├── requirements.txt
└── README.md

"""
RÈGLES D'OR [LISTE]

1. NE JAMAIS modifier les données brutes (raw/)
2. Documenter chaque étape
3. Nommer fichiers avec numéros (01_, 02_...)
4. Sauvegarder souvent (Git recommandé)
5. Reproduire vos analyses (seed aléatoire)
"""


# ============================================================================
# [GUIDE] CHAPITRE 0.6 : PREMIERS PAS - VOTRE PREMIER PROGRAMME DATA
# ============================================================================

"""
VOTRE PREMIER PROGRAMME D'ANALYSE

Exécutez ce code cellule par cellule dans Jupyter :
"""

# Cellule 1 : Imports (toujours en premier)
import numpy as np          # Alias np (convention)
import pandas as pd         # Alias pd (convention)
import matplotlib.pyplot as plt  # Alias plt (convention)
import seaborn as sns       # Alias sns (convention)

# Cellule 2 : Configuration de base
plt.style.use('seaborn-v0_8')  # Style des graphiques
sns.set_palette("husl")         # Palette de couleurs
pd.set_option('display.max_columns', 20)  # Afficher plus de colonnes

print("[OK] Bibliothèques chargées avec succès !")
print(f"NumPy version: {np.__version__}")
print(f"Pandas version: {pd.__version__}")

# Cellule 3 : Créer des données simples
notes = pd.DataFrame({
    'Étudiant': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'],
    'Maths': [18, 14, 16, 12, 19],
    'Physique': [15, 16, 13, 18, 14],
    'Info': [20, 17, 15, 16, 18]
})

print(notes)

# Cellule 4 : Analyse rapide
print("\n[GRAPHIQUE] STATISTIQUES DESCRIPTIVES :")
print(notes.describe())

print("\n[HAUSSE] MOYENNES PAR MATIÈRE :")
print(notes[['Maths', 'Physique', 'Info']].mean())

# Cellule 5 : Visualisation
notes.set_index('Étudiant').plot(kind='bar', figsize=(10, 6))
plt.title('Notes par Étudiant et Matière')
plt.ylabel('Note sur 20')
plt.xlabel('Étudiant')
plt.legend(title='Matière')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

print("[BRAVO] Votre premier graphique d'analyse !")

"""
[IDEE] FÉLICITATIONS !

Vous venez de faire votre PREMIÈRE analyse de données :
[OK] Importé des bibliothèques
[OK] Créé un tableau de données
[OK] Calculé des statistiques
[OK] Visualisé les résultats

C'est exactement ce que font les data scientists au quotidien !
"""


# ============================================================================
# [COURS] EXERCICE PRATIQUE 0 : MISE EN PLACE ET PREMIERS PAS
# ============================================================================

"""
OBJECTIF : Installer l'environnement et tester les bases

EXERCICE :
1. Installer Python et les bibliothèques (ou Anaconda)
2. Lancer Jupyter Notebook
3. Créer un nouveau notebook "exercice_00.ipynb"
4. Compléter le code ci-dessous

DONNÉES : Ventes d'une boutique sur 6 mois
"""

# ---- EXERCICE À COMPLÉTER ----

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# Données de ventes
ventes = pd.DataFrame({
    'Mois': ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin'],
    'Electronique': [12500, 13200, 11800, 14500, 15200, 16800],
    'Vêtements': [8400, 7900, 9200, 10500, 11200, 9800],
    'Alimentation': [22000, 21500, 23000, 22800, 24500, 25200]
})

# TODO 1 : Afficher le tableau
print("=== TABLEAU DES VENTES ===")
# Votre code ici...

# TODO 2 : Calculer le total des ventes par mois
# (somme des 3 catégories pour chaque mois)
# Votre code ici...

# TODO 3 : Trouver le mois avec les meilleures ventes total
# Votre code ici...

# TODO 4 : Créer un graphique en ligne des ventes
# plt.figure(figsize=(12, 6))
# Votre code ici...


# ---- CORRIGÉ COMPLET ----

print("=" * 50)
print("        CORRIGÉ EXERCICE 0")
print("=" * 50)

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# Données
ventes = pd.DataFrame({
    'Mois': ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin'],
    'Electronique': [12500, 13200, 11800, 14500, 15200, 16800],
    'Vêtements': [8400, 7900, 9200, 10500, 11200, 9800],
    'Alimentation': [22000, 21500, 23000, 22800, 24500, 25200]
})

# CORRIGÉ 1 : Afficher
print("=== TABLEAU DES VENTES ===")
print(ventes)
print(f"\nDimensions : {ventes.shape[0]} lignes × {ventes.shape[1]} colonnes")

# CORRIGÉ 2 : Total par mois
ventes['Total'] = ventes['Electronique'] + ventes['Vêtements'] + ventes['Alimentation']
print("\n=== TOTAL PAR MOIS ===")
print(ventes[['Mois', 'Total']])

# CORRIGÉ 3 : Meilleur mois
idx_max = ventes['Total'].idxmax()
meilleur_mois = ventes.loc[idx_max, 'Mois']
meilleur_total = ventes.loc[idx_max, 'Total']
print(f"\n[TROPHEE] Meilleur mois : {meilleur_mois} avec {meilleur_total:,} € de ventes")

# CORRIGÉ 4 : Graphique
fig, axes = plt.subplots(1, 2, figsize=(15, 6))

# Graphique 1 : Lignes
for col in ['Electronique', 'Vêtements', 'Alimentation']:
    axes[0].plot(ventes['Mois'], ventes[col], marker='o', linewidth=2, label=col)
axes[0].set_title('Ventes par Catégorie (Jan-Juin)', fontsize=14)
axes[0].set_ylabel('Ventes (€)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# Graphique 2 : Barres empilées
axes[1].bar(ventes['Mois'], ventes['Electronique'], label='Electronique')
axes[1].bar(ventes['Mois'], ventes['Vêtements'], 
            bottom=ventes['Electronique'], label='Vêtements')
axes[1].bar(ventes['Mois'], ventes['Alimentation'], 
            bottom=ventes['Electronique']+ventes['Vêtements'], label='Alimentation')
axes[1].set_title('Répartition des Ventes', fontsize=14)
axes[1].set_ylabel('Ventes (€)')
axes[1].legend()

plt.tight_layout()
plt.savefig('exercice_00_ventes.png', dpi=150, bbox_inches='tight')
plt.show()
print("\n[OK] Graphique sauvegardé : exercice_00_ventes.png")

# Statistiques finales
print("\n=== STATISTIQUES ===")
print(f"Ventes totales sur 6 mois : {ventes['Total'].sum():,} €")
print(f"Moyenne mensuelle : {ventes['Total'].mean():,.0f} €")
print(f"Meilleur mois : {meilleur_mois}")
print(f"Moins bon mois : {ventes.loc[ventes['Total'].idxmin(), 'Mois']}")

"""
POINTS CLÉS APPRIS :
[OK] Import des bibliothèques essentielles
[OK] Création d'un DataFrame Pandas
[OK] Opérations sur colonnes
[OK] Statistiques de base (idxmax, sum, mean)
[OK] Graphiques avec Matplotlib
[OK] Organisation du code

PROCHAINE ÉTAPE -> PARTIE 1 : Python de Base pour la Data
"""

# ============================================================================
# [LIVRE] ANALYSE DE DONNÉES - PARTIE 1 : PYTHON DE BASE POUR LA DATA
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 1.1 : Structures de données Python essentielles
# - Chapitre 1.2 : Compréhensions de listes et dictionnaires
# - Chapitre 1.3 : Fonctions utiles pour la data
# - Chapitre 1.4 : Gestion de fichiers (CSV, JSON, Excel)
# - Chapitre 1.5 : Gestion des erreurs et types
# - Chapitre 1.6 : Dates et heures
#
# [TEMPS] TEMPS : ~4-5 heures
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 1.1 : STRUCTURES DE DONNÉES ESSENTIELLES
# ============================================================================

"""
[REFLEXION] POURQUOI CE CHAPITRE ?

Avant d'utiliser NumPy et Pandas, vous devez MAÎTRISER les
structures de données Python de base. Elles sont la fondation !

STRUCTURES DISPONIBLES :
- list       -> Séquence ordonnée, modifiable
- tuple      -> Séquence ordonnée, IMMUABLE
- dict       -> Paires clé-valeur
- set        -> Ensemble (valeurs uniques)
"""

# -------------------------------------------------------------------------
# LISTES (list) - Structure la plus utilisée en data
# -------------------------------------------------------------------------

"""
[IDEE] LISTE = Tableau de valeurs ordonnées
QUAND ? Stocker données séquentielles (notes, prix, mesures)
"""

# Créer des listes
notes = [14, 18, 12, 16, 20, 15]          # Liste d'entiers
prenoms = ['Alice', 'Bob', 'Charlie']      # Liste de strings
mixte = [42, 'hello', 3.14, True]          # Types mixtes

# Accéder aux éléments (indexation commence à 0)
print(notes[0])     # -> 14  (premier)
print(notes[-1])    # -> 15  (dernier)
print(notes[1:4])   # -> [18, 12, 16]  (tranche/slice)
print(notes[::2])   # -> [14, 12, 20]  (un sur deux)

# Modifier
notes[0] = 15
notes.append(19)       # Ajouter à la fin
notes.insert(2, 17)    # Insérer à l'index 2
notes.remove(20)       # Supprimer première occurrence
del notes[0]           # Supprimer à l'index

# Opérations utiles pour la data
print(len(notes))      # Longueur
print(sum(notes))      # Somme
print(min(notes))      # Minimum
print(max(notes))      # Maximum
print(sorted(notes))   # Trié (retourne nouvelle liste)

# Vérifier présence
print(18 in notes)     # True/False

# Compter
print(notes.count(15)) # Combien de fois 15 apparaît

# -------------------------------------------------------------------------
# TUPLES (tuple) - Listes immuables
# -------------------------------------------------------------------------

"""
[IDEE] TUPLE = Liste qu'on NE peut PAS modifier
QUAND ? Coordonnées GPS, couleurs RGB, données qui ne changent pas

POURQUOI PAS TOUJOURS LISTE ?
[OK] Tuples plus rapides
[OK] Protection contre modification accidentelle
[OK] Utilisés comme clés de dictionnaire
"""

coordonnees = (48.8566, 2.3522)     # Latitude, Longitude Paris
couleur_rouge = (255, 0, 0)         # RGB
dimensions = (1920, 1080)           # Résolution

# Accéder (comme liste)
print(coordonnees[0])   # 48.8566 (latitude)
print(coordonnees[1])   # 2.3522  (longitude)

# Déballage (très utile !)
latitude, longitude = coordonnees
print(f"Lat: {latitude}, Lon: {longitude}")

# En data : souvent retourné par des fonctions
valeur_min, valeur_max = min(notes), max(notes)
print(f"Min: {valeur_min}, Max: {valeur_max}")

# -------------------------------------------------------------------------
# DICTIONNAIRES (dict) - Données structurées
# -------------------------------------------------------------------------

"""
[IDEE] DICT = Paires clé:valeur (comme JSON)
QUAND ? Données structurées, configuration, résultats
COMMENT ? clé -> valeur (recherche O(1) = ultra-rapide)
"""

# Créer
etudiant = {
    'nom': 'Alice',
    'age': 22,
    'notes': [15, 18, 14, 19],
    'actif': True
}

# Accéder
print(etudiant['nom'])          # 'Alice'
print(etudiant.get('age'))      # 22
print(etudiant.get('email', 'N/A'))  # 'N/A' si absent (sécurisé)

# Modifier
etudiant['age'] = 23
etudiant['email'] = 'alice@example.com'  # Ajouter clé

# Supprimer
del etudiant['actif']
etudiant.pop('email', None)    # pop() avec défaut (sécurisé)

# Parcourir
for cle in etudiant:
    print(cle)

for cle, valeur in etudiant.items():  # <- Plus courant en data
    print(f"  {cle}: {valeur}")

# Clés et valeurs
print(list(etudiant.keys()))    # ['nom', 'age', 'notes']
print(list(etudiant.values()))  # ['Alice', 23, [15, 18, 14, 19]]

# Fusionner dictionnaires
dict1 = {'a': 1, 'b': 2}
dict2 = {'c': 3, 'd': 4}
merged = {**dict1, **dict2}     # <- Syntaxe moderne Python 3.5+
# ou
merged = dict1 | dict2          # <- Python 3.9+

# -------------------------------------------------------------------------
# ENSEMBLES (set) - Valeurs uniques
# -------------------------------------------------------------------------

"""
[IDEE] SET = Ensemble mathématique (valeurs UNIQUES)
QUAND ? Trouver valeurs uniques, enlever doublons, comparer ensembles
"""

pays_europeens = {'France', 'Espagne', 'Italie', 'Allemagne'}
pays_g7 = {'France', 'Allemagne', 'Italie', 'USA', 'UK', 'Canada', 'Japon'}

# Opérations d'ensemble (très utiles en data !)
intersection = pays_europeens & pays_g7      # Commun aux deux
union = pays_europeens | pays_g7             # Tous les pays
difference = pays_g7 - pays_europeens        # Dans G7 mais pas européens

print("EU ∩ G7:", intersection)
print("EU ∪ G7:", union)
print("G7 non-EU:", difference)

# Utilisation pratique : enlever doublons
notes_avec_doublons = [15, 18, 15, 12, 18, 20, 15]
notes_uniques = list(set(notes_avec_doublons))
print(notes_uniques)  # -> [12, 15, 18, 20] (ordre non garanti)

# Vérifier si sous-ensemble
print({'France', 'Italie'}.issubset(pays_europeens))  # True


# ============================================================================
# [GUIDE] CHAPITRE 1.2 : COMPRÉHENSIONS
# ============================================================================

"""
[REFLEXION] QU'EST-CE QU'UNE COMPRÉHENSION ?

Syntaxe COMPACTE pour créer des listes/dicts/sets
À la place de boucles longues -> Code plus PYTHONIQUE

POURQUOI APPRENDRE ÇA ?
-> Très courant dans le code data/ML
-> Plus rapide qu'une boucle for classique
-> Plus lisible une fois maîtrisé
"""

# -------------------------------------------------------------------------
# COMPRÉHENSION DE LISTE
# -------------------------------------------------------------------------

# Syntaxe : [expression for element in iterable if condition]

# SANS compréhension (ancienne façon)
carres = []
for n in range(1, 11):
    carres.append(n ** 2)

# AVEC compréhension (façon pythonique)
carres = [n ** 2 for n in range(1, 11)]
print(carres)   # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# AVEC condition
carres_pairs = [n ** 2 for n in range(1, 11) if n % 2 == 0]
print(carres_pairs)  # [4, 16, 36, 64, 100]

# Exemple data : nettoyer des prix
prix_bruts = ['12.5€', '8.99€', '25.00€', '3.75€']
prix_floats = [float(p.replace('€', '')) for p in prix_bruts]
print(prix_floats)   # [12.5, 8.99, 25.0, 3.75]

# Exemple data : filtrer valeurs invalides
mesures = [15.2, -1, 18.5, None, 12.1, -999, 14.8]
mesures_valides = [m for m in mesures if m is not None and m > 0]
print(mesures_valides)  # [15.2, 18.5, 12.1, 14.8]

# Transformation complexe
# Normaliser notes sur 100
notes_sur_20 = [12, 15, 18, 14, 16]
notes_sur_100 = [n * 5 for n in notes_sur_20]
print(notes_sur_100)  # [60, 75, 90, 70, 80]

# -------------------------------------------------------------------------
# COMPRÉHENSION DE DICTIONNAIRE
# -------------------------------------------------------------------------

# Syntaxe : {cle: valeur for element in iterable}

# Carré de chaque nombre comme dict
carres_dict = {n: n**2 for n in range(1, 6)}
print(carres_dict)  # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# Exemple data : créer index
etudiants = ['Alice', 'Bob', 'Charlie', 'Diana']
index_etudiants = {nom: i for i, nom in enumerate(etudiants)}
print(index_etudiants)  # {'Alice': 0, 'Bob': 1, ...}

# Inverser un dictionnaire
notes_lettres = {'A': 18, 'B': 14, 'C': 10, 'D': 7}
notes_inversees = {v: k for k, v in notes_lettres.items()}
print(notes_inversees)  # {18: 'A', 14: 'B', ...}

# Filtrer un dictionnaire
notes_reussies = {etud: note for etud, note in 
                  {'Alice': 15, 'Bob': 8, 'Charlie': 12}.items() 
                  if note >= 10}
print(notes_reussies)  # {'Alice': 15, 'Charlie': 12}

# -------------------------------------------------------------------------
# COMPRÉHENSION DE SET
# -------------------------------------------------------------------------

# Trouver toutes les premières lettres uniques
noms = ['Alice', 'Bob', 'André', 'Béatrice', 'Arthur']
premieres_lettres = {nom[0] for nom in noms}
print(premieres_lettres)  # {'A', 'B'}


# ============================================================================
# [GUIDE] CHAPITRE 1.3 : FONCTIONS UTILES POUR LA DATA
# ============================================================================

"""
FONCTIONS BUILT-IN ESSENTIELLES POUR LA DATA
"""

# ---- zip() : Combiner plusieurs listes ----
"""
[IDEE] zip() : Combine plusieurs itérables en paires
QUAND ? Combiner deux listes liées (noms + notes, etc.)
"""

etudiants = ['Alice', 'Bob', 'Charlie']
notes = [16, 14, 18]
villes = ['Paris', 'Lyon', 'Marseille']

# Combiner deux listes
for etudiant, note in zip(etudiants, notes):
    print(f"{etudiant}: {note}/20")

# Créer dictionnaire depuis deux listes
notes_dict = dict(zip(etudiants, notes))
print(notes_dict)  # {'Alice': 16, 'Bob': 14, 'Charlie': 18}

# Combiner trois listes
for nom, note, ville in zip(etudiants, notes, villes):
    print(f"{nom} ({ville}): {note}/20")

# Transposer (utile en data !)
coordonnees = [(1, 4), (2, 5), (3, 6)]
x_vals, y_vals = zip(*coordonnees)
print(f"X: {x_vals}")   # (1, 2, 3)
print(f"Y: {y_vals}")   # (4, 5, 6)


# ---- enumerate() : Index + valeur ----
"""
[IDEE] enumerate() : Itérer avec index ET valeur
QUAND ? Besoin de l'index ET de la valeur
"""

fruits = ['pomme', 'banane', 'cerise', 'datte']

for i, fruit in enumerate(fruits):
    print(f"Index {i}: {fruit}")

# Commencer à 1
for rang, fruit in enumerate(fruits, start=1):
    print(f"Rang {rang}: {fruit}")

# Créer dictionnaire rang:valeur
classement = {rang: etud for rang, etud in enumerate(
    sorted(etudiants, key=lambda e: notes[etudiants.index(e)], reverse=True), 1
)}


# ---- map() : Appliquer fonction sur itérable ----
"""
[IDEE] map() : Appliquer une fonction à chaque élément
QUAND ? Transformer des données
ALTERNATIVE : Compréhension de liste (plus lisible)
"""

# Convertir strings en nombres
chiffres_str = ['1', '2', '3', '4', '5']
chiffres_int = list(map(int, chiffres_str))
print(chiffres_int)  # [1, 2, 3, 4, 5]

# Avec fonction lambda
prix = [100, 250, 75, 320]
prix_avec_tva = list(map(lambda p: p * 1.2, prix))
print(prix_avec_tva)  # [120.0, 300.0, 90.0, 384.0]

# Équivalent avec compréhension (préféré)
prix_avec_tva = [p * 1.2 for p in prix]


# ---- filter() : Filtrer éléments ----
"""
[IDEE] filter() : Garder éléments qui satisfont condition
ALTERNATIVE : Compréhension de liste avec condition
"""

notes = [8, 15, 12, 6, 18, 9, 14]
notes_passees = list(filter(lambda n: n >= 10, notes))
print(notes_passees)  # [15, 12, 18, 14]

# Équivalent préféré
notes_passees = [n for n in notes if n >= 10]


# ---- sorted() avec key ----
"""
[IDEE] sorted() : Trier avec critère personnalisé
QUAND ? Trier par attribut, longueur, valeur calculée
"""

# Trier par longueur de nom
noms = ['Charlie', 'Ali', 'Béatrice', 'Bob', 'Véronique']
noms_par_longueur = sorted(noms, key=len)
print(noms_par_longueur)  # ['Ali', 'Bob', 'Charlie', 'Béatrice', 'Véronique']

# Trier liste de dictionnaires
etudiants_data = [
    {'nom': 'Alice', 'note': 16},
    {'nom': 'Bob', 'note': 14},
    {'nom': 'Charlie', 'note': 18}
]

# Trier par note (décroissant)
par_note = sorted(etudiants_data, key=lambda e: e['note'], reverse=True)
for e in par_note:
    print(f"{e['nom']}: {e['note']}")


# ---- any() et all() ----
"""
[IDEE] any() : Au moins un True | all() : Tous True
QUAND ? Vérifications sur des listes de conditions
"""

notes = [15, 12, 18, 9, 14]

print(any(n > 17 for n in notes))   # True (18 > 17)
print(all(n >= 10 for n in notes))  # False (9 < 10)
print(any(n < 10 for n in notes))   # True (9 < 10)


# ============================================================================
# [GUIDE] CHAPITRE 1.4 : GESTION DE FICHIERS POUR LA DATA
# ============================================================================

"""
[REFLEXION] POURQUOI LIRE DES FICHIERS ?

En data science, les données viennent de :
- Fichiers CSV (données tabulaires)
- JSON (APIs, configs)
- Excel (entreprises)
- Bases de données

On verra Pandas plus en détail en Partie 3, mais voici les bases Python
"""

import csv
import json
import os

# -------------------------------------------------------------------------
# LIRE/ÉCRIRE DES FICHIERS CSV
# -------------------------------------------------------------------------

"""
[IDEE] CSV = Comma-Separated Values
Format le plus commun pour les données tabulaires
"""

# Écrire un CSV
donnees = [
    ['Nom', 'Age', 'Ville', 'Salaire'],
    ['Alice', 28, 'Paris', 45000],
    ['Bob', 35, 'Lyon', 52000],
    ['Charlie', 22, 'Marseille', 38000]
]

with open('employes.csv', 'w', newline='', encoding='utf-8') as f:
    writer = csv.writer(f)
    writer.writerows(donnees)

print("[OK] CSV créé : employes.csv")

# Lire un CSV
with open('employes.csv', 'r', encoding='utf-8') as f:
    reader = csv.reader(f)
    for ligne in reader:
        print(ligne)

# Lire avec DictReader (clé = nom de colonne)
with open('employes.csv', 'r', encoding='utf-8') as f:
    reader = csv.DictReader(f)
    for employe in reader:
        print(f"{employe['Nom']} ({employe['Age']} ans) -> {employe['Salaire']} €")

# -------------------------------------------------------------------------
# LIRE/ÉCRIRE DES FICHIERS JSON
# -------------------------------------------------------------------------

"""
[IDEE] JSON = JavaScript Object Notation
Format universel pour données structurées et APIs
"""

# Données Python -> JSON
config_analyse = {
    'version': '1.0',
    'auteur': 'Alice Martin',
    'dataset': 'employes.csv',
    'colonnes_cibles': ['Salaire', 'Age'],
    'filtres': {
        'age_min': 18,
        'age_max': 65,
        'villes': ['Paris', 'Lyon']
    }
}

# Écrire JSON
with open('config.json', 'w', encoding='utf-8') as f:
    json.dump(config_analyse, f, indent=4, ensure_ascii=False)

print("[OK] JSON créé : config.json")

# Lire JSON
with open('config.json', 'r', encoding='utf-8') as f:
    config = json.load(f)

print(f"Dataset : {config['dataset']}")
print(f"Auteur  : {config['auteur']}")
print(f"Villes  : {config['filtres']['villes']}")

# JSON depuis/vers string (APIs)
json_string = json.dumps({'x': 1, 'y': 2})       # Dict -> String
data_dict = json.loads('{"x": 1, "y": 2}')        # String -> Dict

# -------------------------------------------------------------------------
# GESTION DES CHEMINS DE FICHIERS
# -------------------------------------------------------------------------

import os
from pathlib import Path  # <- Python 3.6+ (RECOMMANDÉ)

"""
[IDEE] pathlib : Module moderne pour gérer les chemins
Plus propre que os.path
"""

# Chemin du fichier courant
dossier_courant = Path('.')
print(dossier_courant.absolute())

# Créer chemins (cross-platform)
dossier_data = Path('data') / 'raw'         # <- Fonctionne sur Windows et Linux
fichier_csv = Path('data') / 'employes.csv'

# Vérifications
print(fichier_csv.exists())          # Existe ?
print(fichier_csv.is_file())         # C'est un fichier ?
print(fichier_csv.suffix)            # -> '.csv'
print(fichier_csv.stem)              # -> 'employes' (sans extension)
print(fichier_csv.name)              # -> 'employes.csv'
print(fichier_csv.parent)            # -> PosixPath('data')

# Créer dossiers
Path('data/raw').mkdir(parents=True, exist_ok=True)
Path('data/processed').mkdir(parents=True, exist_ok=True)
Path('outputs/figures').mkdir(parents=True, exist_ok=True)

# Lister fichiers
for fichier in Path('.').iterdir():
    if fichier.suffix == '.csv':
        print(f"CSV trouvé : {fichier}")

# Trouver tous les CSV récursivement
for csv_file in Path('.').rglob('*.csv'):
    print(csv_file)


# ============================================================================
# [GUIDE] CHAPITRE 1.5 : TYPES ET CONVERSIONS
# ============================================================================

"""
[REFLEXION] POURQUOI IMPORTANT EN DATA ?

Les données "sales" arrivent souvent avec mauvais types :
- Prix stockés comme string "12.5€"
- Dates comme string "2024-01-15"
- Booléens comme "Oui"/"Non"

Il faut CONVERTIR avant d'analyser !
"""

# ---- Vérifier les types ----
valeurs = [42, 3.14, "hello", True, None, [1, 2]]
for v in valeurs:
    print(f"  {v!r:15} -> type: {type(v).__name__}")

# isinstance() = tester le type
x = 42
print(isinstance(x, int))           # True
print(isinstance(x, (int, float)))  # True (l'un ou l'autre)

# ---- Conversions de base ----
# int()
print(int("42"))          # 42    (string -> int)
print(int(3.9))           # 3     (float -> int, tronqué !)
print(int(True))          # 1
print(int(False))         # 0

# float()
print(float("3.14"))      # 3.14
print(float("1e3"))       # 1000.0 (notation scientifique)
print(float(42))          # 42.0

# str()
print(str(42))            # "42"
print(str(3.14))          # "3.14"

# bool()
# [ATTENTION] IMPORTANT à connaître !
print(bool(0))            # False
print(bool(0.0))          # False
print(bool(""))           # False
print(bool([]))           # False
print(bool(None))         # False
print(bool(1))            # True (tout autre nombre)
print(bool("a"))          # True (string non vide)
print(bool([1]))          # True (liste non vide)

# ---- Conversions avancées pour la data ----
# Données sales -> Nettoyage
def convertir_prix(valeur):
    """Convertir '12.5€' ou '12,5' en float"""
    if isinstance(valeur, (int, float)):
        return float(valeur)
    if isinstance(valeur, str):
        valeur = valeur.strip()                  # Enlever espaces
        valeur = valeur.replace('€', '')         # Enlever €
        valeur = valeur.replace(',', '.')        # Virgule -> Point
        valeur = valeur.replace(' ', '')         # Enlever espaces
        try:
            return float(valeur)
        except ValueError:
            return None  # Valeur invalide

# Test
prix_sales = ['12.5€', '8,99', '  25.00 €', 'N/A', 100, 3.99]
prix_propres = [convertir_prix(p) for p in prix_sales]
print(prix_propres)  # [12.5, 8.99, 25.0, None, 100.0, 3.99]


# ============================================================================
# [GUIDE] CHAPITRE 1.6 : DATES ET HEURES
# ============================================================================

"""
[IDEE] DATES EN DATA = Sujet crucial !

Les séries temporelles (time series) sont partout :
- Cours de bourse
- Températures quotidiennes
- Ventes par heure
- Logs de serveur

Python a datetime, mais on utilisera souvent Pandas pour les dates
"""

from datetime import datetime, date, timedelta

# ---- Créer des dates ----
# Aujourd'hui
aujourd_hui = date.today()
maintenant = datetime.now()

print(f"Aujourd'hui : {aujourd_hui}")      # 2024-01-15
print(f"Maintenant  : {maintenant}")       # 2024-01-15 14:30:45.123456

# Créer date spécifique
naissance = date(1995, 3, 15)
evenement = datetime(2024, 6, 1, 9, 30, 0)  # 1 juin 2024 à 09:30

# ---- Opérations sur dates ----
# Différence
age_jours = (aujourd_hui - naissance).days
age_ans = age_jours // 365
print(f"Âge : {age_ans} ans ({age_jours} jours)")

# Ajouter/soustraire du temps
dans_30_jours = aujourd_hui + timedelta(days=30)
il_y_a_1_semaine = aujourd_hui - timedelta(weeks=1)
print(f"Dans 30 jours : {dans_30_jours}")

# ---- Formater les dates ----
# strftime : date -> string
maintenant_format = maintenant.strftime("%d/%m/%Y à %H:%M")
print(f"Formaté : {maintenant_format}")   # "15/01/2024 à 14:30"

# strptime : string -> date
date_string = "15/01/2024"
date_parsee = datetime.strptime(date_string, "%d/%m/%Y")
print(type(date_parsee))   # <class 'datetime.datetime'>

# Codes de format essentiels
"""
%Y -> Année 4 chiffres (2024)
%y -> Année 2 chiffres (24)
%m -> Mois (01-12)
%d -> Jour (01-31)
%H -> Heure 24h (00-23)
%M -> Minute (00-59)
%S -> Seconde (00-59)
%A -> Nom du jour (Lundi)
%B -> Nom du mois (Janvier)
"""

# ---- Cas pratique : Analyser des séries temporelles ----
dates_ventes = ['2024-01-15', '2024-01-16', '2024-01-17']
montants = [1250, 980, 1540]

# Convertir strings en dates
dates_obj = [datetime.strptime(d, '%Y-%m-%d') for d in dates_ventes]

# Filtrer par période
debut = datetime(2024, 1, 16)
ventes_filtrees = [
    (d, m) for d, m in zip(dates_obj, montants)
    if d >= debut
]
print("Ventes après le 16/01:", ventes_filtrees)


# ============================================================================
# [COURS] EXERCICE PRATIQUE 1 : MANIPULATION DE DONNÉES PYTHON
# ============================================================================

"""
SCÉNARIO : Vous êtes analyste dans une école.
Vous devez analyser les résultats d'une promotion d'étudiants.

DONNÉES :
"""

# ---- DONNÉES BRUTES ----
etudiants_bruts = [
    {'nom': 'Alice Martin', 'naissance': '15/03/2001', 'notes': [15, 18, 14, 16, 19], 'ville': 'paris'},
    {'nom': 'Bob Dupont', 'naissance': '22/07/2000', 'notes': [12, 10, 8, 14, 11], 'ville': 'lyon'},
    {'nom': 'Charlie Moreau', 'naissance': '05/11/2001', 'notes': [18, 20, 17, 19, 18], 'ville': 'marseille'},
    {'nom': 'Diana Petit', 'naissance': '30/01/2002', 'notes': [9, 7, 11, 8, 10], 'ville': 'paris'},
    {'nom': 'Eve Lambert', 'naissance': '14/09/2001', 'notes': [16, 15, 17, 14, 18], 'ville': 'bordeaux'},
    {'nom': 'Frank Dubois', 'naissance': '28/04/2000', 'notes': [13, 12, 15, 11, 14], 'ville': 'lyon'},
]

# ---- EXERCICES ----
"""
EXERCICE 1 : Calculer la moyenne de chaque étudiant
EXERCICE 2 : Déterminer si chaque étudiant est reçu (moyenne >= 10)
EXERCICE 3 : Classer les étudiants par moyenne décroissante
EXERCICE 4 : Calculer l'âge de chaque étudiant
EXERCICE 5 : Grouper les étudiants par ville
EXERCICE 6 : Trouver la ville avec le plus d'étudiants
EXERCICE 7 : Afficher un bulletin récapitulatif
"""

# TODO : Votre code ici

# ---- CORRIGÉ COMPLET ----

print("=" * 60)
print("     CORRIGÉ - EXERCICE PRATIQUE 1")
print("=" * 60)

from datetime import date, datetime

etudiants_bruts = [
    {'nom': 'Alice Martin', 'naissance': '15/03/2001', 'notes': [15, 18, 14, 16, 19], 'ville': 'paris'},
    {'nom': 'Bob Dupont', 'naissance': '22/07/2000', 'notes': [12, 10, 8, 14, 11], 'ville': 'lyon'},
    {'nom': 'Charlie Moreau', 'naissance': '05/11/2001', 'notes': [18, 20, 17, 19, 18], 'ville': 'marseille'},
    {'nom': 'Diana Petit', 'naissance': '30/01/2002', 'notes': [9, 7, 11, 8, 10], 'ville': 'paris'},
    {'nom': 'Eve Lambert', 'naissance': '14/09/2001', 'notes': [16, 15, 17, 14, 18], 'ville': 'bordeaux'},
    {'nom': 'Frank Dubois', 'naissance': '28/04/2000', 'notes': [13, 12, 15, 11, 14], 'ville': 'lyon'},
]

# CORRIGÉ 1 : Moyenne et enrichissement des données
def enrichir_etudiant(etud):
    """Ajouter moyenne, statut, âge, ville formatée"""
    moyenne = sum(etud['notes']) / len(etud['notes'])
    naissance = datetime.strptime(etud['naissance'], '%d/%m/%Y').date()
    aujourd_hui = date.today()
    age = (aujourd_hui - naissance).days // 365
    
    return {
        **etud,
        'moyenne': round(moyenne, 2),
        'recu': moyenne >= 10,
        'age': age,
        'ville': etud['ville'].capitalize(),
        'mention': (
            'Très Bien' if moyenne >= 16 else
            'Bien' if moyenne >= 14 else
            'Assez Bien' if moyenne >= 12 else
            'Passable' if moyenne >= 10 else
            'Ajourné'
        )
    }

etudiants = [enrichir_etudiant(e) for e in etudiants_bruts]

# CORRIGÉ 2 : Statut reçu
print("\n[LISTE] RÉSULTATS INDIVIDUELS :")
print("-" * 55)
for e in etudiants:
    statut = "[OK] REÇU" if e['recu'] else "[X] AJOURNÉ"
    print(f"  {e['nom']:<20} {e['moyenne']:>5.2f}/20  {e['mention']:<12} {statut}")

# CORRIGÉ 3 : Classement par moyenne
print("\n[TROPHEE] CLASSEMENT PAR MOYENNE :")
print("-" * 55)
classement = sorted(etudiants, key=lambda e: e['moyenne'], reverse=True)
for rang, e in enumerate(classement, 1):
    print(f"  {rang}. {e['nom']:<20} -> {e['moyenne']}/20")

# CORRIGÉ 4 : Âges
print("\n[BIRTHDAY_CAKE] AGES :")
for e in etudiants:
    print(f"  {e['nom']:<20} -> {e['age']} ans (né(e) le {e['naissance']})")

# CORRIGÉ 5 : Grouper par ville
from collections import defaultdict
par_ville = defaultdict(list)
for e in etudiants:
    par_ville[e['ville']].append(e['nom'])

print("\n[CITYSCAPE] ÉTUDIANTS PAR VILLE :")
for ville, noms in sorted(par_ville.items()):
    print(f"  {ville:<15} : {', '.join(noms)}")

# CORRIGÉ 6 : Ville la plus représentée
ville_max = max(par_ville, key=lambda v: len(par_ville[v]))
print(f"\n[IMPORTANT] Ville la plus représentée : {ville_max} ({len(par_ville[ville_max])} étudiants)")

# CORRIGÉ 7 : Bulletin récapitulatif
moyennes = [e['moyenne'] for e in etudiants]
recus = [e for e in etudiants if e['recu']]

print("\n" + "=" * 55)
print("           [GRAPHIQUE] BULLETIN DE LA PROMOTION")
print("=" * 55)
print(f"  Effectif total     : {len(etudiants)} étudiants")
print(f"  Reçus              : {len(recus)} ({len(recus)/len(etudiants)*100:.0f}%)")
print(f"  Moyenne promo      : {sum(moyennes)/len(moyennes):.2f}/20")
print(f"  Meilleure moyenne  : {max(moyennes):.2f} ({classement[0]['nom']})")
print(f"  Plus basse moyenne : {min(moyennes):.2f} ({classement[-1]['nom']})")

# Compte par mention
from collections import Counter
mentions = Counter(e['mention'] for e in etudiants)
print(f"\n  Mentions obtenues :")
for mention, count in sorted(mentions.items()):
    print(f"    {mention:<15} : {count} étudiant(s)")

"""
CONCEPTS CLÉS MAÎTRISÉS :
[OK] Compréhension de listes
[OK] Fonctions de transformation
[OK] sorted() avec key=lambda
[OK] defaultdict pour grouper
[OK] Counter pour compter
[OK] Manipulation de dates
[OK] F-strings avec formatage

PROCHAINE ÉTAPE -> PARTIE 2 : NumPy
"""

# ============================================================================
# [LIVRE] ANALYSE DE DONNÉES - PARTIE 2 : NUMPY
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 2.1 : Introduction et Création de tableaux
# - Chapitre 2.2 : Indexation et Slicing
# - Chapitre 2.3 : Opérations mathématiques
# - Chapitre 2.4 : Statistiques avec NumPy
# - Chapitre 2.5 : Manipulation de tableaux (reshape, stack, etc.)
# - Chapitre 2.6 : Tableaux aléatoires
# - Chapitre 2.7 : Algèbre linéaire de base
#
# [TEMPS] TEMPS : ~5-6 heures
# ============================================================================

import numpy as np

# ============================================================================
# [GUIDE] CHAPITRE 2.1 : INTRODUCTION À NUMPY
# ============================================================================

"""
[REFLEXION] QU'EST-CE QUE NUMPY ?

NumPy = Numerical Python
Bibliothèque de calcul numérique ultra-performante

POURQUOI NUMPY PLUTÔT QUE DES LISTES PYTHON ?

Performance ! Exemple :
"""
import time

# Créer 1 million de nombres
n = 1_000_000

# AVEC LISTE PYTHON
liste = list(range(n))
debut = time.time()
resultat = [x * 2 for x in liste]
print(f"Liste Python : {time.time() - debut:.4f}s")

# AVEC NUMPY
array = np.arange(n)
debut = time.time()
resultat = array * 2
print(f"NumPy        : {time.time() - debut:.4f}s")

# -> NumPy est 10-100x plus rapide !

"""
POURQUOI NUMPY EST PLUS RAPIDE ?

1. CONTIGUÏTÉ MÉMOIRE
   Éléments stockés côte à côte en mémoire
   -> Accès ultra-rapide (cache CPU)

2. TYPE UNIFORME
   Tous les éléments ont le MÊME type
   -> Pas de vérification de type à chaque opération

3. VECTORISATION
   Opérations sur tout le tableau en une instruction
   -> Boucle en C compilé, pas Python

4. BROADCASTING
   Opérations intelligentes sur tableaux de tailles différentes


NUMPY vs LISTE

              LISTE PYTHON    NUMPY ARRAY
Type éléments   Mixtes          Uniforme
Taille          Dynamique       Fixe
Performance     Lente           Ultra-rapide
Mémoire         Beaucoup        Peu
Opérations      Manuelles       Vectorisées
"""

# ============================================================================
# [GUIDE] CHAPITRE 2.1 (suite) : CRÉER DES TABLEAUX
# ============================================================================

# -------------------------------------------------------------------------
# CRÉATION DE BASE
# -------------------------------------------------------------------------

# Depuis une liste Python
a1 = np.array([1, 2, 3, 4, 5])
print(a1)           # [1 2 3 4 5]
print(type(a1))     # <class 'numpy.ndarray'>
print(a1.dtype)     # int64

# Depuis liste imbriquée (2D)
matrice = np.array([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
])
print(matrice)
print(matrice.shape)   # (3, 3) -> 3 lignes, 3 colonnes

# Spécifier le type
floats = np.array([1, 2, 3], dtype=float)     # [1. 2. 3.]
entiers = np.array([1.5, 2.7, 3.9], dtype=int)  # [1 2 3] (tronqué)
bools = np.array([0, 1, 0, 1], dtype=bool)   # [F T F T]

# -------------------------------------------------------------------------
# TABLEAUX SPÉCIAUX (très utilisés !)
# -------------------------------------------------------------------------

"""
[IDEE] CES FONCTIONS CRÉENT DES TABLEAUX COURANTS
"""

# Zeros (initialisé à 0)
zeros = np.zeros(5)                  # [0. 0. 0. 0. 0.]
zeros_2d = np.zeros((3, 4))          # Matrice 3×4 de zéros

# Uns (initialisé à 1)
uns = np.ones(4)                     # [1. 1. 1. 1.]
uns_int = np.ones((2, 3), dtype=int) # Matrice 2×3 de 1 entiers

# Vide (mémoire non initialisée, très rapide)
vide = np.empty((2, 2))              # [ATTENTION] Valeurs aléatoires (garbage)

# Identité (matrice diagonale de 1)
identite = np.eye(3)
# [[1. 0. 0.]
#  [0. 1. 0.]
#  [0. 0. 1.]]

# Constante
constante = np.full((3, 3), 7)       # Matrice 3×3 de 7

# -------------------------------------------------------------------------
# SÉQUENCES NUMÉRIQUES
# -------------------------------------------------------------------------

"""
[IDEE] 3 FAÇONS DIFFÉRENTES DE CRÉER UNE SÉQUENCE
"""

# arange : comme range() mais pour numpy
seq1 = np.arange(10)          # [0 1 2 3 4 5 6 7 8 9]
seq2 = np.arange(1, 11)       # [1 2 3 4 5 6 7 8 9 10]
seq3 = np.arange(0, 1, 0.1)   # [0.  0.1 0.2 ... 0.9]

# linspace : N points équidistants ENTRE deux valeurs
# QUAND ? Créer axe X pour graphiques, intégration numérique
seq4 = np.linspace(0, 1, 5)   # [0.   0.25 0.5  0.75 1.  ]
seq5 = np.linspace(0, 10, 11) # [0. 1. 2. 3. ... 10.]

# logspace : N points en échelle logarithmique
seq6 = np.logspace(0, 3, 4)   # [1. 10. 100. 1000.]

# Comparaison arange vs linspace
print("arange(0, 1, 0.3):", np.arange(0, 1, 0.3))    # Pas fixe -> peut varier
print("linspace(0, 1, 4):", np.linspace(0, 1, 4))    # Nb points fixe <- PRÉFÉRÉ

# -------------------------------------------------------------------------
# PROPRIÉTÉS D'UN TABLEAU
# -------------------------------------------------------------------------

arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

print(f"shape   : {arr.shape}")     # (3, 3) - dimensions
print(f"ndim    : {arr.ndim}")      # 2 - nombre de dimensions
print(f"size    : {arr.size}")      # 9 - nombre total d'éléments
print(f"dtype   : {arr.dtype}")     # int64 - type des éléments
print(f"itemsize: {arr.itemsize}")  # 8 - octets par élément
print(f"nbytes  : {arr.nbytes}")    # 72 - octets totaux


# ============================================================================
# [GUIDE] CHAPITRE 2.2 : INDEXATION ET SLICING
# ============================================================================

"""
[REFLEXION] POURQUOI MAÎTRISER L'INDEXATION ?

Accéder aux bonnes données = Compétence fondamentale
Sélectionner lignes, colonnes, plages de valeurs
"""

# -------------------------------------------------------------------------
# TABLEAUX 1D
# -------------------------------------------------------------------------

arr = np.array([10, 20, 30, 40, 50, 60, 70])

# Index positif (commence à 0)
print(arr[0])     # 10 (premier)
print(arr[3])     # 40

# Index négatif (depuis la fin)
print(arr[-1])    # 70 (dernier)
print(arr[-2])    # 60

# Slicing : arr[start:stop:step]
print(arr[1:5])   # [20 30 40 50] (de 1 inclus à 5 exclus)
print(arr[::2])   # [10 30 50 70] (un sur deux)
print(arr[::-1])  # [70 60 50 40 30 20 10] (inversé)
print(arr[2:])    # [30 40 50 60 70] (de 2 jusqu'à la fin)
print(arr[:4])    # [10 20 30 40] (jusqu'à 4 exclus)

# [ATTENTION] ATTENTION : La slice retourne une VUE (pas une copie !)
vue = arr[1:4]
vue[0] = 999
print(arr)  # -> [10 999 30 40 50 60 70] !! arr EST modifié

# Pour copier :
copie = arr[1:4].copy()
copie[0] = 888
print(arr)  # -> Non modifié

# -------------------------------------------------------------------------
# TABLEAUX 2D (Matrices)
# -------------------------------------------------------------------------

matrice = np.array([
    [10, 20, 30, 40],
    [50, 60, 70, 80],
    [90, 100, 110, 120]
])

"""
Indices 2D : matrice[ligne, colonne]

       col0 col1 col2 col3
ligne0 [ 10,  20,  30,  40]
ligne1 [ 50,  60,  70,  80]
ligne2 [ 90, 100, 110, 120]
"""

# Élément unique
print(matrice[0, 0])    # 10  (ligne 0, col 0)
print(matrice[1, 2])    # 70  (ligne 1, col 2)
print(matrice[-1, -1])  # 120 (dernière ligne, dernière col)

# Lignes entières
print(matrice[0])       # [10 20 30 40] (première ligne)
print(matrice[-1])      # [90 100 110 120] (dernière ligne)
print(matrice[0:2])     # Lignes 0 et 1

# Colonnes entières
print(matrice[:, 0])    # [10 50 90] (première colonne)
print(matrice[:, -1])   # [40 80 120] (dernière colonne)
print(matrice[:, 1:3])  # Colonnes 1 et 2

# Sous-matrice (plage de lignes et colonnes)
print(matrice[0:2, 1:3])
# [[20 30]
#  [60 70]]

# -------------------------------------------------------------------------
# INDEXATION AVANCÉE
# -------------------------------------------------------------------------

# Indexation par liste
arr = np.array([10, 20, 30, 40, 50, 60, 70])
indices = [0, 2, 5]
print(arr[indices])  # [10 30 60]

# Indexation booléenne (TRÈS UTILISÉE EN DATA !)
"""
[IDEE] Indexation booléenne = Filtrer des données selon une condition
C'est l'équivalent du WHERE en SQL !
"""

notes = np.array([15, 8, 12, 18, 6, 14, 10])

# Créer masque booléen
masque = notes >= 10
print(masque)          # [True False True True False True True]

# Filtrer
notes_recues = notes[masque]
print(notes_recues)    # [15 12 18 14 10]

# Version condensée (TRÈS COURANTE)
print(notes[notes >= 10])  # Même résultat

# Conditions multiples
print(notes[(notes >= 10) & (notes < 16)])  # Entre 10 et 15
print(notes[(notes < 8) | (notes > 17)])    # < 8 ou > 17

# Modifier éléments avec condition
notes_copy = notes.copy()
notes_copy[notes_copy < 10] = 10  # Mettre la barre à 10 minimum
print(notes_copy)  # [15 10 12 18 10 14 10]

# np.where : si-alors pour tableaux
mentions = np.where(notes >= 10, "Reçu", "Ajourné")
print(mentions)  # ['Reçu' 'Ajourné' 'Reçu' 'Reçu' 'Ajourné' 'Reçu' 'Reçu']

# np.where avec plusieurs conditions
notes = np.array([15, 8, 12, 18, 6, 14, 10])
categories = np.select(
    [notes >= 16, notes >= 12, notes >= 10],
    ['Très Bien', 'Bien', 'Passable'],
    default='Ajourné'
)
print(categories)


# ============================================================================
# [GUIDE] CHAPITRE 2.3 : OPÉRATIONS MATHÉMATIQUES
# ============================================================================

"""
[IDEE] OPÉRATIONS VECTORISÉES = Magie NumPy !

Au lieu de : for element in liste: element * 2
On fait    : array * 2
-> Appliqué sur TOUS les éléments automatiquement
"""

a = np.array([1, 2, 3, 4, 5])
b = np.array([10, 20, 30, 40, 50])

# Opérations élément par élément
print(a + b)      # [11 22 33 44 55]
print(a - b)      # [-9 -18 -27 -36 -45]
print(a * b)      # [10 40 90 160 250]
print(a / b)      # [0.1 0.1 0.1 0.1 0.1]
print(a ** 2)     # [1 4 9 16 25]
print(a % 2)      # [1 0 1 0 1] (modulo)

# Avec scalaire
print(a * 3)      # [3 6 9 12 15]
print(a + 10)     # [11 12 13 14 15]

# Fonctions mathématiques NumPy (sur tout le tableau)
angles = np.array([0, np.pi/6, np.pi/4, np.pi/3, np.pi/2])
print(np.sin(angles))     # [0.  0.5  0.707  0.866  1.]
print(np.cos(angles))

prix = np.array([100, 200, 50, 1000])
print(np.sqrt(prix))      # Racine carrée
print(np.log(prix))       # Logarithme naturel
print(np.log10(prix))     # Log base 10
print(np.exp(np.array([1, 2, 3])))  # Exponentielle

# Arrondi
vals = np.array([1.234, 5.678, 9.012])
print(np.round(vals, 1))   # [1.2 5.7 9.0]
print(np.floor(vals))      # [1. 5. 9.] (arrondi bas)
print(np.ceil(vals))       # [2. 6. 10.] (arrondi haut)

# Valeur absolue
negatifs = np.array([-3, 4, -1, 2, -5])
print(np.abs(negatifs))   # [3 4 1 2 5]

# -------------------------------------------------------------------------
# BROADCASTING (Opérations intelligentes)
# -------------------------------------------------------------------------

"""
[IDEE] BROADCASTING = Étendre automatiquement les tableaux
pour les rendre compatibles

RÈGLES :
1. Si dimensionnalités différentes, aligner depuis la droite
2. Dimensions compatibles si égales OU l'une est 1
"""

# Exemple 1 : Scalaire × Tableau
matrice = np.array([[1, 2], [3, 4], [5, 6]])
print(matrice * 10)
# [[10 20]
#  [30 40]
#  [50 60]]

# Exemple 2 : Vecteur ligne × Matrice
ligne = np.array([10, 20])     # shape: (2,)
print(matrice + ligne)         # shape: (3,2) + (2,) -> broadcast
# [[11 22]
#  [13 24]
#  [15 26]]

# Exemple data : Normaliser chaque colonne
data = np.array([[100, 2, 0.5],
                 [200, 4, 1.0],
                 [150, 3, 0.75]])

minimum = data.min(axis=0)    # Min de chaque colonne
maximum = data.max(axis=0)    # Max de chaque colonne
normalise = (data - minimum) / (maximum - minimum)
print("Données normalisées [0-1]:")
print(np.round(normalise, 3))


# ============================================================================
# [GUIDE] CHAPITRE 2.4 : STATISTIQUES AVEC NUMPY
# ============================================================================

"""
[IDEE] NUMPY FOURNIT TOUTES LES STATISTIQUES DE BASE
Ces fonctions sont ultra-rapides (calcul C/Fortran en arrière-plan)
"""

# Données simulées : températures sur 12 mois (en °C)
temperatures = np.array([3.2, 5.1, 8.4, 12.7, 17.3, 21.5, 
                          24.1, 23.8, 19.2, 13.6, 7.8, 4.1])

# Mesures de tendance centrale
print(f"Moyenne  : {np.mean(temperatures):.1f}°C")       # Arithmétique
print(f"Médiane  : {np.median(temperatures):.1f}°C")     # Valeur du milieu
print(f"Variance : {np.var(temperatures):.1f}")           # Dispersion
print(f"Écart-type: {np.std(temperatures):.1f}°C")       # √Variance

# Extrêmes
print(f"Min      : {np.min(temperatures):.1f}°C")
print(f"Max      : {np.max(temperatures):.1f}°C")
print(f"Étendue  : {np.ptp(temperatures):.1f}°C")        # max - min

# Index des extrêmes
idx_min = np.argmin(temperatures)
idx_max = np.argmax(temperatures)
mois = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin', 
        'Juil', 'Août', 'Sep', 'Oct', 'Nov', 'Déc']
print(f"Mois le plus froid : {mois[idx_min]} ({temperatures[idx_min]}°C)")
print(f"Mois le plus chaud : {mois[idx_max]} ({temperatures[idx_max]}°C)")

# Percentiles / Quantiles
print(f"25e percentile : {np.percentile(temperatures, 25):.1f}°C")
print(f"50e percentile : {np.percentile(temperatures, 50):.1f}°C")
print(f"75e percentile : {np.percentile(temperatures, 75):.1f}°C")

# IQR (Interquartile Range)
q1 = np.percentile(temperatures, 25)
q3 = np.percentile(temperatures, 75)
iqr = q3 - q1
print(f"IQR : {iqr:.1f}°C")

# Somme et produit
print(f"Somme    : {np.sum(temperatures):.1f}")
print(f"Somme cumulée : {np.cumsum(temperatures)}")  # Cumul croissant

# ---- Statistiques sur axes (tableaux 2D) ----
notes_classe = np.array([
    [15, 18, 12, 16],   # Étudiant 1
    [14, 13, 17, 15],   # Étudiant 2
    [18, 20, 16, 19],   # Étudiant 3
    [10, 12, 11, 13],   # Étudiant 4
    [16, 14, 18, 15],   # Étudiant 5
])

# axis=0 : le long des lignes -> résultat par COLONNE
print("Moyenne par matière :", np.mean(notes_classe, axis=0))
# -> [14.6 15.4 14.8 15.6]

# axis=1 : le long des colonnes -> résultat par LIGNE
print("Moyenne par étudiant:", np.mean(notes_classe, axis=1))
# -> [15.25 14.75 18.25 11.5 15.75]


# ============================================================================
# [GUIDE] CHAPITRE 2.5 : MANIPULATION DE TABLEAUX
# ============================================================================

# ---- Reshape : Changer la forme ----
arr = np.arange(12)
print(arr)               # [0 1 2 3 4 5 6 7 8 9 10 11]
print(arr.reshape(3, 4)) # 3 lignes × 4 colonnes
print(arr.reshape(4, 3)) # 4 lignes × 3 colonnes
print(arr.reshape(2, 2, 3)) # 3D : 2×2×3

# -1 : NumPy calcule automatiquement la dimension
print(arr.reshape(-1, 4))  # Nb colonnes=4, lignes calculées
print(arr.reshape(3, -1))  # Nb lignes=3, colonnes calculées

# Aplatir (mettre en 1D)
matrice = np.array([[1, 2], [3, 4], [5, 6]])
print(matrice.flatten())  # [1 2 3 4 5 6] (copie)
print(matrice.ravel())    # [1 2 3 4 5 6] (vue si possible)

# Transposer
print(matrice.T)          # Lignes <-> Colonnes

# ---- Combiner des tableaux ----
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])

# Vertical (empiler)
vertical = np.vstack([a, b])    # np.concatenate([a,b], axis=0)
print("vstack:\n", vertical)
# [[1 2]
#  [3 4]
#  [5 6]
#  [7 8]]

# Horizontal (côte à côte)
horizontal = np.hstack([a, b])  # np.concatenate([a,b], axis=1)
print("hstack:\n", horizontal)
# [[1 2 5 6]
#  [3 4 7 8]]

# ---- Diviser des tableaux ----
arr = np.arange(12)
parties = np.split(arr, 3)          # En 3 parties égales
print(parties)  # [array([0,1,2,3]), array([4,5,6,7]), array([8,9,10,11])]

# Diviser à positions précises
a, b, c = np.split(arr, [3, 7])    # Couper à index 3 et 7
print(a, b, c)  # [0 1 2] [3 4 5 6] [7 8 9 10 11]

# ---- Trier ----
arr = np.array([3, 1, 4, 1, 5, 9, 2, 6])
print(np.sort(arr))           # [1 1 2 3 4 5 6 9] (copie triée)
print(np.argsort(arr))        # indices pour trier : [1 3 6 0 2 4 7 5]

# Tri de matrice
matrice = np.array([[3, 1, 4], [1, 5, 9], [2, 6, 5]])
print(np.sort(matrice, axis=1))   # Trier chaque ligne
print(np.sort(matrice, axis=0))   # Trier chaque colonne

# ---- Opérations ensemblistes ----
a = np.array([1, 2, 3, 4, 5])
b = np.array([3, 4, 5, 6, 7])

print(np.intersect1d(a, b))  # [3 4 5]
print(np.union1d(a, b))      # [1 2 3 4 5 6 7]
print(np.setdiff1d(a, b))    # [1 2] (dans a mais pas b)
print(np.unique(np.array([1, 2, 2, 3, 3, 3])))  # [1 2 3]


# ============================================================================
# [GUIDE] CHAPITRE 2.6 : TABLEAUX ALÉATOIRES
# ============================================================================

"""
[IDEE] GÉNÉRATION ALÉATOIRE EN DATA SCIENCE

Utilisation :
- Simulation de données
- Tests statistiques
- Machine Learning (initialisation)
- Validation croisée

SEED : Pour reproductibilité !
"""

# Fixer le générateur aléatoire
rng = np.random.default_rng(seed=42)   # <- Moderne (NumPy 1.17+)
# np.random.seed(42)                    # <- Ancienne façon (fonctionne encore)

# Entiers aléatoires
entiers = rng.integers(0, 100, size=10)     # 10 entiers entre 0 et 99
print("Entiers:", entiers)

# Flottants entre 0 et 1
floats = rng.random(size=(3, 4))            # Matrice 3×4
print("Floats:\n", np.round(floats, 3))

# Distribution normale (Gaussienne)
# mean=0, std=1 par défaut (distribution standard)
normale = rng.normal(loc=10, scale=2, size=1000)  # Moyenne=10, Écart-type=2
print(f"Normale : mean={normale.mean():.2f}, std={normale.std():.2f}")

# Distribution uniforme entre a et b
uniforme = rng.uniform(low=0, high=100, size=50)

# Distribution de Poisson (événements rares)
poisson = rng.poisson(lam=5, size=100)    # Lambda = taux moyen

# Distribution Bernoulli / Binomiale
binomiale = rng.binomial(n=10, p=0.5, size=100)  # 10 essais, p=0.5

# Choix aléatoire
fruits = np.array(['pomme', 'banane', 'cerise', 'datte'])
choix = rng.choice(fruits, size=6, replace=True)   # Avec remise
print("Choix:", choix)

# Mélanger un tableau
numeros = np.arange(1, 11)
rng.shuffle(numeros)                              # In-place
print("Mélangé:", numeros)

permutation = rng.permutation(np.arange(10))      # Retourne copie


# ============================================================================
# [GUIDE] CHAPITRE 2.7 : ALGÈBRE LINÉAIRE
# ============================================================================

"""
[IDEE] POURQUOI L'ALGÈBRE LINÉAIRE EN DATA ?

- Machine Learning = Algèbre linéaire !
- Régression linéaire, PCA, réseaux de neurones...
- Comprendre np.linalg est essentiel
"""

A = np.array([[2, 1], [5, 3]])
B = np.array([[1, 0], [0, 1]])  # Identité

# Produit matriciel (NE PAS confondre avec *)
print("A @ B:\n", A @ B)           # Matrice × Matrice
print("A.dot(B):\n", A.dot(B))     # Identique

# Résoudre système d'équations Ax = b
# 2x + y = 8
# 5x + 3y = 21
A = np.array([[2, 1], [5, 3]])
b = np.array([8, 21])
x = np.linalg.solve(A, b)          # Résout Ax = b
print(f"Solution : x={x[0]:.1f}, y={x[1]:.1f}")

# Déterminant
print(f"Déterminant : {np.linalg.det(A):.1f}")

# Inverse
print("Inverse de A:\n", np.linalg.inv(A))

# Valeurs propres et vecteurs propres
valeurs, vecteurs = np.linalg.eig(A)
print("Valeurs propres:", valeurs)

# Norme d'un vecteur
v = np.array([3, 4])
print(f"Norme euclidienne : {np.linalg.norm(v):.1f}")   # 5.0

# Moindres carrés (régression linéaire simple)
# y = ax + b
x_data = np.array([[1, 1], [2, 1], [3, 1], [4, 1], [5, 1]])  # [x, 1]
y_data = np.array([2.1, 4.2, 5.9, 8.1, 10.0])
coefficients, residuels, rang, valeurs_sing = np.linalg.lstsq(x_data, y_data, rcond=None)
a, b = coefficients
print(f"Droite de régression : y = {a:.2f}x + {b:.2f}")


# ============================================================================
# [COURS] EXERCICE PRATIQUE 2 : ANALYSE AVEC NUMPY
# ============================================================================

"""
SCÉNARIO : Capteur météo -> Analyser données de température sur 1 an

DONNÉES : Températures journalières simulées
"""

# ---- DONNÉES ----
np.random.seed(42)

# Simuler températures sur 365 jours (avec variations saisonnières)
jours = np.arange(365)
temp_base = 15 + 10 * np.sin(2 * np.pi * jours / 365 - np.pi/2)
bruit = np.random.normal(0, 3, 365)
temperatures_annuelles = temp_base + bruit

# Simuler quelques données manquantes (NaN)
indices_manquants = np.random.choice(365, size=20, replace=False)
temperatures_avec_nan = temperatures_annuelles.copy().astype(float)
temperatures_avec_nan[indices_manquants] = np.nan

# ---- EXERCICES ----
"""
1. Calculer les statistiques globales (ignorer NaN)
2. Séparer en 4 saisons et calculer moyenne par saison
3. Trouver les 10 jours les plus chauds
4. Calculer le nombre de jours de gel (< 0°C)
5. Calculer moyenne mobile sur 7 jours
6. Détecter les valeurs aberrantes (> 3 écarts-types)
"""

# ---- CORRIGÉ ----

print("=" * 60)
print("     CORRIGÉ - EXERCICE PRATIQUE 2 (NumPy)")
print("=" * 60)

# 1. Statistiques globales (ignorer NaN)
print("\n[GRAPHIQUE] STATISTIQUES ANNUELLES :")
print(f"  Température moyenne : {np.nanmean(temperatures_avec_nan):.1f}°C")
print(f"  Température médiane : {np.nanmedian(temperatures_avec_nan):.1f}°C")
print(f"  Écart-type          : {np.nanstd(temperatures_avec_nan):.1f}°C")
print(f"  Min                 : {np.nanmin(temperatures_avec_nan):.1f}°C")
print(f"  Max                 : {np.nanmax(temperatures_avec_nan):.1f}°C")
print(f"  Données manquantes  : {np.sum(np.isnan(temperatures_avec_nan))}")

# 2. Par saison
saisons_def = {
    'Hiver (Jan-Mar)': slice(0, 90),
    'Printemps (Avr-Juin)': slice(90, 181),
    'Été (Juil-Sep)': slice(181, 274),
    'Automne (Oct-Déc)': slice(274, 365)
}

print("\n[WHITE_SUN_WITH_SMALL_CLOUD] MOYENNES PAR SAISON :")
for saison, periode in saisons_def.items():
    temp_saison = temperatures_avec_nan[periode]
    moy = np.nanmean(temp_saison)
    print(f"  {saison:<30} : {moy:.1f}°C")

# 3. 10 jours les plus chauds
temp_sans_nan = temperatures_avec_nan.copy()
temp_sans_nan[np.isnan(temp_sans_nan)] = -999  # Remplacer NaN temporairement

indices_chauds = np.argsort(temperatures_annuelles)[-10:][::-1]
print("\n[HOT] 10 JOURS LES PLUS CHAUDS :")
for i, idx in enumerate(indices_chauds, 1):
    mois_noms = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin',
                 'Juil', 'Août', 'Sep', 'Oct', 'Nov', 'Déc']
    mois = mois_noms[int(idx // 30.5)]
    jour_mois = int(idx % 30.5) + 1
    print(f"  {i:2}. Jour {idx+1:3d} ({mois} ~{jour_mois:2d}) : {temperatures_annuelles[idx]:.1f}°C")

# 4. Jours de gel
jours_gel = np.sum(temperatures_annuelles < 0)
print(f"\n[SNOWFLAKE]  JOURS DE GEL : {jours_gel} jours ({jours_gel/365*100:.1f}%)")

# 5. Moyenne mobile 7 jours
def moyenne_mobile(arr, fenetre):
    """Calculer moyenne mobile"""
    n = len(arr)
    result = np.full(n, np.nan)
    for i in range(fenetre - 1, n):
        fenetre_data = arr[i - fenetre + 1:i + 1]
        if not np.any(np.isnan(fenetre_data)):
            result[i] = np.mean(fenetre_data)
    return result

temp_lissee = moyenne_mobile(temperatures_avec_nan, 7)
print(f"\n[HAUSSE] MOYENNE MOBILE 7 JOURS :")
print(f"  Premiers 14 jours disponibles: {np.round(temp_lissee[6:14], 1)}")

# 6. Valeurs aberrantes
moyenne = np.nanmean(temperatures_annuelles)
ecart = np.nanstd(temperatures_annuelles)
seuil = 3

aberrantes = np.where(
    np.abs(temperatures_annuelles - moyenne) > seuil * ecart
)[0]
print(f"\n[ATTENTION]  VALEURS ABERRANTES (>{seuil} écarts-types) :")
if len(aberrantes) == 0:
    print("  Aucune valeur aberrante détectée")
else:
    for idx in aberrantes:
        print(f"  Jour {idx+1}: {temperatures_annuelles[idx]:.1f}°C")

# Visualisation rapide (texte)
print("\n[GRAPHIQUE] HISTOGRAMME DES TEMPÉRATURES :")
bins = np.arange(-10, 40, 5)
hist, bords = np.histogram(temperatures_annuelles, bins=bins)
for i, (debut, fin, nb) in enumerate(zip(bords, bords[1:], hist)):
    barre = '█' * (nb // 5)
    print(f"  [{debut:3.0f}°C, {fin:3.0f}°C[ : {barre} ({nb} jours)")

"""
CONCEPTS CLÉS MAÎTRISÉS :
[OK] Création et manipulation de tableaux NumPy
[OK] Indexation booléenne
[OK] Fonctions nan-safe (nanmean, nanstd...)
[OK] argsort pour classer
[OK] np.where pour conditions
[OK] np.histogram pour distributions
[OK] Moyennes mobiles

PROCHAINE ÉTAPE -> PARTIE 3 : Pandas
"""

# ============================================================================
# [LIVRE] ANALYSE DE DONNÉES - PARTIE 3 : PANDAS
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 3.1 : Series et DataFrame
# - Chapitre 3.2 : Lire et écrire des données
# - Chapitre 3.3 : Exploration du DataFrame
# - Chapitre 3.4 : Sélection et filtrage
# - Chapitre 3.5 : Modification et création de colonnes
# - Chapitre 3.6 : Regroupement (groupby)
# - Chapitre 3.7 : Fusion de DataFrames
# - Chapitre 3.8 : Dates dans Pandas
# - Chapitre 3.9 : Apply, Map et transformations
#
# [TEMPS] TEMPS : ~8-10 heures
# ============================================================================

import pandas as pd
import numpy as np

# ============================================================================
# [GUIDE] CHAPITRE 3.1 : SERIES ET DATAFRAME
# ============================================================================

"""
[REFLEXION] QU'EST-CE QUE PANDAS ?

Pandas = Pan(el) D(ata)S -> Données de panel
La bibliothèque la PLUS UTILISÉE en analyse de données Python

ANALOGIE :
Pandas = Excel sous stéroïdes
- DataFrame = Feuille de calcul
- Series = Colonne

DEUX STRUCTURES PRINCIPALES :
- pd.Series  : Tableau 1D avec index
- pd.DataFrame : Tableau 2D avec index et colonnes
"""

# -------------------------------------------------------------------------
# pd.Series : Tableau 1D avec étiquettes
# -------------------------------------------------------------------------

"""
ANALOGIE : Series = Colonne d'Excel avec des étiquettes de ligne
"""

# Créer Series
s1 = pd.Series([10, 20, 30, 40, 50])
print(s1)
# 0    10
# 1    20
# ...
# dtype: int64

# Avec index personnalisé
notes = pd.Series(
    [15, 18, 12, 16, 19],
    index=['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'],
    name='Notes de Maths'
)
print(notes)

# Accéder aux éléments
print(notes['Alice'])       # 15  (par étiquette)
print(notes[0])             # 15  (par position)
print(notes[['Alice', 'Eve']])  # Plusieurs

# Opérations vectorisées
print(notes * 5)             # Convertir en /100
print(notes.mean())          # Moyenne
print(notes[notes >= 15])    # Filtre

# Depuis dictionnaire
prix_dict = {'pomme': 1.20, 'banane': 0.85, 'cerise': 3.50}
prix = pd.Series(prix_dict)
print(prix)

# Informations sur la Series
print(f"Taille  : {len(notes)}")
print(f"Type    : {notes.dtype}")
print(f"Index   : {list(notes.index)}")
print(f"Valeurs : {notes.values}")          # -> array numpy

# -------------------------------------------------------------------------
# pd.DataFrame : Tableau 2D
# -------------------------------------------------------------------------

"""
ANALOGIE : DataFrame = Feuille Excel complète
Lignes = Observations/enregistrements
Colonnes = Variables/attributs
"""

# Créer depuis dictionnaire de listes (MÉTHODE PRINCIPALE)
df = pd.DataFrame({
    'Nom': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'],
    'Age': [28, 35, 22, 31, 25],
    'Ville': ['Paris', 'Lyon', 'Paris', 'Marseille', 'Lyon'],
    'Salaire': [45000, 62000, 38000, 54000, 47000],
    'Departement': ['RH', 'Tech', 'Marketing', 'Tech', 'RH']
})

print(df)
print(f"\nShape : {df.shape}")    # (5, 5) -> 5 lignes, 5 colonnes
print(f"Taille : {df.size}")     # 25 éléments total

# Depuis liste de dictionnaires
employes = [
    {'nom': 'Alice', 'age': 28, 'salaire': 45000},
    {'nom': 'Bob', 'age': 35, 'salaire': 62000},
    {'nom': 'Charlie', 'age': 22, 'salaire': 38000}
]
df2 = pd.DataFrame(employes)

# Depuis numpy array
arr = np.random.randint(0, 100, size=(4, 3))
df3 = pd.DataFrame(arr, columns=['A', 'B', 'C'])

# Index personnalisé
df4 = pd.DataFrame(
    {'x': [1, 2, 3], 'y': [4, 5, 6]},
    index=['a', 'b', 'c']
)


# ============================================================================
# [GUIDE] CHAPITRE 3.2 : LIRE ET ÉCRIRE DES DONNÉES
# ============================================================================

"""
[IDEE] PANDAS SUPPORTE TOUS LES FORMATS DATA
"""

# ---- CRÉER DONNÉES DE TEST ----
import io

csv_data = """Nom,Age,Ville,Salaire,Departement,Date_embauche
Alice Martin,28,Paris,45000,RH,2022-03-15
Bob Dupont,35,Lyon,62000,Tech,2019-06-01
Charlie Moreau,22,Paris,38000,Marketing,2023-01-10
Diana Petit,31,Marseille,54000,Tech,2020-09-22
Eve Lambert,25,Lyon,47000,RH,2021-11-30
Frank Dubois,40,Paris,75000,Tech,2015-04-18
Grace Martin,29,Bordeaux,51000,Marketing,2022-07-05
Henry Leroy,33,Paris,68000,Tech,2018-02-14
"""

# ---- LIRE CSV ----
# Depuis fichier
# df = pd.read_csv('employes.csv')

# Depuis string (pour exemples)
df = pd.read_csv(io.StringIO(csv_data))
print("CSV chargé :", df.shape)

# Options importantes de read_csv
"""
pd.read_csv(
    'fichier.csv',
    sep=',',                    # Séparateur (';' pour Excel européen)
    header=0,                   # Ligne de l'en-tête (0=première)
    index_col=None,             # Colonne à utiliser comme index
    usecols=['col1', 'col2'],   # Seulement ces colonnes
    dtype={'Age': int, 'Salaire': float},  # Forcer les types
    parse_dates=['Date_embauche'],  # Parser comme dates
    encoding='utf-8',           # Encodage
    na_values=['N/A', 'null', '-'],  # Valeurs à traiter comme NaN
    skiprows=[1, 2],            # Sauter des lignes
    nrows=100,                  # Lire seulement N lignes (gros fichiers)
    chunksize=1000,             # Lire par morceaux (gros fichiers)
)
"""

# ---- LIRE EXCEL ----
# pip install openpyxl
# df = pd.read_excel('fichier.xlsx', sheet_name='Feuil1')
# df = pd.read_excel('fichier.xlsx', sheet_name=0)  # Première feuille

# ---- LIRE JSON ----
# df = pd.read_json('data.json')

# ---- LIRE BASE DE DONNÉES ----
# import sqlite3
# conn = sqlite3.connect('database.db')
# df = pd.read_sql('SELECT * FROM employes', conn)

# ---- ÉCRIRE DES DONNÉES ----
# CSV
df.to_csv('sortie.csv', index=False)        # index=False : ne pas écrire index
df.to_csv('sortie_sep.csv', sep=';', index=False)  # Point-virgule

# Excel
# df.to_excel('sortie.xlsx', index=False, sheet_name='Employes')

# JSON
df.to_json('sortie.json', orient='records', indent=2)

# Markdown (pour rapports)
print(df.to_markdown(index=False))          # Nécessite tabulate


# ============================================================================
# [GUIDE] CHAPITRE 3.3 : EXPLORATION DU DATAFRAME
# ============================================================================

"""
[IDEE] PREMIÈRE CHOSE À FAIRE avec un nouveau dataset !
Ces fonctions vous donnent un aperçu rapide de vos données.
"""

# Recréer le DataFrame
df = pd.read_csv(io.StringIO(csv_data))

print("=" * 50)
print("EXPLORATION DE BASE")
print("=" * 50)

# Aperçu des premières/dernières lignes
print("\nhead(5) :")
print(df.head())           # 5 premières lignes (défaut)

print("\ntail(3) :")
print(df.tail(3))          # 3 dernières lignes

print("\nsample(3) :")
print(df.sample(3))        # 3 lignes aléatoires

# Informations générales (TRÈS UTILE !)
print("\ninfo() :")
print(df.info())
"""
Output info() :
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8 entries, 0 to 7
Data columns (total 6 columns):
 #   Column          Non-Null Count  Dtype 
---  ------          --------------  ----- 
 0   Nom             8 non-null      object
 1   Age             8 non-null      int64 
 2   Ville           8 non-null      object
 3   Salaire         8 non-null      int64 
 4   Departement     8 non-null      object
 5   Date_embauche   8 non-null      object
"""

# Statistiques descriptives
print("\ndescribe() :")
print(df.describe())           # Variables numériques
print(df.describe(include='all'))  # Toutes les variables

# Types de données
print("\ndtypes :")
print(df.dtypes)

# Valeurs manquantes
print("\nValeurs manquantes :")
print(df.isnull().sum())   # Nombre de NaN par colonne
print(f"\nTotal NaN : {df.isnull().sum().sum()}")

# Dimensions
print(f"\nDimensions : {df.shape}")
print(f"Lignes     : {df.shape[0]}")
print(f"Colonnes   : {df.shape[1]}")
print(f"Noms colonnes : {list(df.columns)}")
print(f"Index : {df.index}")

# Valeurs uniques
print("\nValeurs uniques par colonne :")
for col in df.columns:
    print(f"  {col:<20} : {df[col].nunique()} valeurs uniques")

# Distribution d'une variable catégorielle
print("\nDistribution des départements :")
print(df['Departement'].value_counts())
print(df['Departement'].value_counts(normalize=True))  # En %


# ============================================================================
# [GUIDE] CHAPITRE 3.4 : SÉLECTION ET FILTRAGE
# ============================================================================

"""
[REFLEXION] COMMENT ACCÉDER AUX DONNÉES ?

Trois méthodes principales :
1. [] (crochets) -> Colonnes et lignes
2. .loc[] -> Index/étiquettes
3. .iloc[] -> Position (numérique)
"""

df = pd.read_csv(io.StringIO(csv_data))

# -------------------------------------------------------------------------
# SÉLECTIONNER DES COLONNES
# -------------------------------------------------------------------------

# Une colonne -> Series
noms = df['Nom']
salaires = df['Salaire']

# Plusieurs colonnes -> DataFrame
sous_df = df[['Nom', 'Salaire', 'Departement']]

# Raccourci (si pas d'espace et pas de conflit)
# ages = df.Age     <- Fonctionne mais NON RECOMMANDÉ

# -------------------------------------------------------------------------
# .loc[] : Sélection par ÉTIQUETTES
# -------------------------------------------------------------------------

"""
df.loc[selection_lignes, selection_colonnes]
"""

# Une ligne par index
print(df.loc[0])               # Ligne d'index 0
print(df.loc[0, 'Nom'])        # Cellule spécifique

# Plage de lignes (INCLUS les deux bornes !)
print(df.loc[0:3])             # Lignes 0, 1, 2, 3 (4 lignes !)

# Lignes et colonnes spécifiques
print(df.loc[0:2, ['Nom', 'Salaire']])

# Toutes les lignes, colonnes spécifiques
print(df.loc[:, 'Age':'Ville'])   # Colonnes de Age à Ville inclus

# -------------------------------------------------------------------------
# .iloc[] : Sélection par POSITION numérique
# -------------------------------------------------------------------------

"""
df.iloc[position_lignes, position_colonnes]
Comme indexation NumPy (0-based, bornes EXCLUSIVES)
"""

print(df.iloc[0])               # Première ligne
print(df.iloc[-1])              # Dernière ligne
print(df.iloc[0:3])             # Lignes 0, 1, 2 (pas 3)
print(df.iloc[:, 0:3])          # 3 premières colonnes
print(df.iloc[0:3, 0:3])        # 3×3 coin supérieur gauche

# -------------------------------------------------------------------------
# FILTRAGE (Sélection par condition)
# -------------------------------------------------------------------------

"""
[IDEE] LE FILTRAGE EST LA COMPÉTENCE LA PLUS UTILISÉE EN DATA !
Équivalent du WHERE en SQL
"""

# Condition simple
parisiens = df[df['Ville'] == 'Paris']
print("Parisiens:", parisiens[['Nom', 'Ville', 'Salaire']])

# Condition numérique
bien_payes = df[df['Salaire'] > 50000]
print("Bien payés:", bien_payes[['Nom', 'Salaire']])

# Conditions multiples (& = ET, | = OU)
# [ATTENTION] Toujours mettre parenthèses autour de chaque condition !
tech_paris = df[(df['Departement'] == 'Tech') & (df['Ville'] == 'Paris')]
print("Tech à Paris:", tech_paris[['Nom', 'Departement', 'Ville']])

parisiens_ou_lyonnais = df[(df['Ville'] == 'Paris') | (df['Ville'] == 'Lyon')]
print("Paris ou Lyon:", parisiens_ou_lyonnais[['Nom', 'Ville']])

# isin() : Appartient à une liste (équivalent IN en SQL)
grandes_villes = df[df['Ville'].isin(['Paris', 'Lyon', 'Marseille'])]
print("Grandes villes:", grandes_villes[['Nom', 'Ville']])

# NOT : ~
pas_paris = df[~(df['Ville'] == 'Paris')]
# ou
pas_paris = df[df['Ville'] != 'Paris']

# between() : Intervalle
age_moyen = df[df['Age'].between(25, 35)]
print("Age 25-35:", age_moyen[['Nom', 'Age']])

# str.contains() : Recherche dans texte
martin_famille = df[df['Nom'].str.contains('Martin')]
print("Famille Martin:", martin_famille)

# isnull() / notna() : Données manquantes
df_test = df.copy()
df_test.loc[0, 'Salaire'] = None
avec_salaire = df_test[df_test['Salaire'].notna()]
sans_salaire = df_test[df_test['Salaire'].isnull()]

# query() : Syntaxe SQL-like (plus lisible)
result = df.query('Salaire > 50000 and Ville == "Paris"')
result2 = df.query('Age >= 30 and Departement in ["Tech", "RH"]')


# ============================================================================
# [GUIDE] CHAPITRE 3.5 : MODIFICATION ET CRÉATION DE COLONNES
# ============================================================================

"""
[IDEE] TRANSFORMER LES DONNÉES = Cœur de la préparation data
"""

df = pd.read_csv(io.StringIO(csv_data))

# ---- Créer une nouvelle colonne ----
# Calcul simple
df['Salaire_mensuel'] = df['Salaire'] / 12

# Condition (np.where équivalent)
df['Senior'] = df['Age'] >= 30

# Catégorie
df['Tranche_age'] = pd.cut(
    df['Age'],
    bins=[0, 25, 35, 50, 100],
    labels=['Jeune', 'Actif', 'Sénior', 'Expert']
)

# Rang/Quantile
df['Rang_salaire'] = df['Salaire'].rank(ascending=False).astype(int)

# ---- Modifier une colonne existante ----
df['Nom'] = df['Nom'].str.upper()        # Mettre en majuscules
df['Ville'] = df['Ville'].str.title()   # Première lettre majuscule

# ---- Parser les dates ----
df['Date_embauche'] = pd.to_datetime(df['Date_embauche'])
df['Annee_embauche'] = df['Date_embauche'].dt.year
df['Anciennete_jours'] = (pd.Timestamp.now() - df['Date_embauche']).dt.days
df['Anciennete_ans'] = df['Anciennete_jours'] / 365.25

# ---- Supprimer des colonnes ----
df = df.drop(columns=['Salaire_mensuel'])  # Supprimer une colonne
df = df.drop(columns=['Col1', 'Col2'], errors='ignore')  # Ignorer si absent

# ---- Renommer des colonnes ----
df = df.rename(columns={
    'Nom': 'Prenom_Nom',
    'Departement': 'Dept'
})

# Renommer toutes les colonnes en minuscules
df.columns = df.columns.str.lower()

# ---- Changer les types ----
df['age'] = df['age'].astype(int)
df['salaire'] = df['salaire'].astype(float)
df['dept'] = df['dept'].astype('category')  # Économie mémoire

# ---- Réordonner les colonnes ----
nouvelles_colonnes = ['prenom_nom', 'age', 'ville', 'dept', 'salaire']
df_reordonne = df[nouvelles_colonnes]

# ---- Réinitialiser l'index ----
df = df.reset_index(drop=True)     # drop=True : ne pas garder l'ancien index

# ---- Définir une colonne comme index ----
df_indexe = df.set_index('prenom_nom')
print(df_indexe)


# ============================================================================
# [GUIDE] CHAPITRE 3.6 : GROUPBY - AGRÉGER LES DONNÉES
# ============================================================================

"""
[IDEE] groupby() = Équivalent du GROUP BY en SQL

CONCEPT : Diviser -> Appliquer -> Combiner (Split-Apply-Combine)
1. DIVISER   : Grouper les données selon une/plusieurs colonnes
2. APPLIQUER : Calculer une statistique par groupe
3. COMBINER  : Assembler les résultats

C'est l'une des fonctions LES PLUS PUISSANTES de Pandas !
"""

df = pd.read_csv(io.StringIO(csv_data))

# ---- groupby simple ----
# Salaire moyen par département
par_dept = df.groupby('Departement')['Salaire'].mean()
print("Salaire moyen par département:")
print(par_dept)

# Plusieurs colonnes en même temps
stats_dept = df.groupby('Departement')['Salaire'].agg(['mean', 'min', 'max', 'count'])
print("\nStatistiques par département:")
print(stats_dept)

# Grouper sur plusieurs colonnes
par_dept_ville = df.groupby(['Departement', 'Ville'])['Salaire'].mean()
print("\nSalaire moyen par département et ville:")
print(par_dept_ville)

# ---- agg() : Agrégation multiple ----
# Différentes fonctions pour différentes colonnes
agregats = df.groupby('Departement').agg({
    'Salaire': ['mean', 'max', 'min', 'std'],
    'Age': ['mean', 'count'],
    'Nom': 'count'
})
print("\nAgrégats multiples:")
print(agregats)

# Avec noms personnalisés (NamedAgg)
agregats_propres = df.groupby('Departement').agg(
    salaire_moyen=('Salaire', 'mean'),
    salaire_max=('Salaire', 'max'),
    age_moyen=('Age', 'mean'),
    effectif=('Nom', 'count')
)
print("\nAgrégats avec noms:")
print(agregats_propres)

# ---- Fonctions d'agrégation disponibles ----
"""
sum()   -> Somme
mean()  -> Moyenne
median()-> Médiane
min()   -> Minimum
max()   -> Maximum
count() -> Nombre de non-NaN
size()  -> Nombre total (avec NaN)
std()   -> Écart-type
var()   -> Variance
first() -> Premier élément
last()  -> Dernier élément
nunique()-> Nombre de valeurs uniques
"""

# ---- transform() : Ajouter résultat agrégé sans réduire ----
# TRÈS UTILE pour comparer chaque valeur à la moyenne de son groupe
df['Salaire_moyen_dept'] = df.groupby('Departement')['Salaire'].transform('mean')
df['Ecart_vs_dept'] = df['Salaire'] - df['Salaire_moyen_dept']
print("\nÉcart vs moyenne du département:")
print(df[['Nom', 'Departement', 'Salaire', 'Salaire_moyen_dept', 'Ecart_vs_dept']])

# ---- filter() avec groupby : Garder groupes selon condition ----
# Garder seulement les départements avec > 2 employés
dept_grands = df.groupby('Departement').filter(lambda g: len(g) > 2)
print("\nDépartements > 2 personnes:")
print(dept_grands[['Nom', 'Departement']])

# ---- Pivot Table (résumé croisé) ----
pivot = df.pivot_table(
    values='Salaire',
    index='Departement',
    columns='Ville',
    aggfunc='mean',
    fill_value=0
)
print("\nPivot - Salaire moyen par Dept × Ville:")
print(pivot)


# ============================================================================
# [GUIDE] CHAPITRE 3.7 : FUSION DE DATAFRAMES
# ============================================================================

"""
[IDEE] MERGE = Équivalent du JOIN en SQL

Combiner deux DataFrames selon une/des colonnes communes
"""

# Créer deux DataFrames à fusionner
employes = pd.DataFrame({
    'emp_id': [1, 2, 3, 4, 5],
    'nom': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'],
    'dept_id': [1, 2, 1, 3, 2]
})

departements = pd.DataFrame({
    'dept_id': [1, 2, 3, 4],
    'dept_nom': ['RH', 'Tech', 'Marketing', 'Finance'],
    'budget': [100000, 500000, 150000, 300000]
})

print("Employés:", employes)
print("Départements:", departements)

# ---- INNER JOIN : Seulement les correspondances ----
inner = pd.merge(employes, departements, on='dept_id', how='inner')
print("\nINNER JOIN:")
print(inner)

# ---- LEFT JOIN : Tous de gauche + correspondances droite ----
left = pd.merge(employes, departements, on='dept_id', how='left')
print("\nLEFT JOIN (tous les employés):")
print(left)

# ---- RIGHT JOIN : Correspondances gauche + tous de droite ----
right = pd.merge(employes, departements, on='dept_id', how='right')
print("\nRIGHT JOIN (tous les départements):")
print(right)

# ---- OUTER JOIN : Tous les enregistrements ----
outer = pd.merge(employes, departements, on='dept_id', how='outer')
print("\nOUTER JOIN (tout):")
print(outer)

# ---- Colonnes de jointure différentes ----
df_gauche = pd.DataFrame({'id_emp': [1, 2, 3], 'nom': ['A', 'B', 'C']})
df_droite = pd.DataFrame({'emp_ref': [1, 2, 4], 'score': [90, 85, 92]})
merged = pd.merge(df_gauche, df_droite, left_on='id_emp', right_on='emp_ref')

# ---- concat() : Empiler des DataFrames ----
# Vertical (empiler lignes)
df1 = employes.iloc[:3]
df2 = employes.iloc[3:]
combine = pd.concat([df1, df2], ignore_index=True)

# Horizontal (ajouter colonnes)
scores = pd.DataFrame({'score': [90, 85, 78, 92, 88]})
combine_h = pd.concat([employes, scores], axis=1)


# ============================================================================
# [GUIDE] CHAPITRE 3.8 : DATES DANS PANDAS
# ============================================================================

"""
[IDEE] PANDAS EXCELLE DANS LA MANIPULATION DE DATES
Séries temporelles, analyse par période, groupes temporels
"""

# Créer des dates
dates = pd.date_range(start='2024-01-01', end='2024-12-31', freq='D')
print(f"365 jours : {len(dates)} dates")

# Depuis string
date_obj = pd.to_datetime('2024-03-15')
dates_liste = pd.to_datetime(['2024-01-15', '2024-06-20', '2024-12-01'])

# Créer série temporelle
np.random.seed(42)
ventes = pd.Series(
    np.random.randint(100, 500, size=365),
    index=pd.date_range('2024-01-01', periods=365, freq='D'),
    name='Ventes'
)

# Accéder aux composants de date
df_dates = pd.DataFrame({'date': pd.date_range('2024-01-01', periods=12, freq='ME')})
df_dates['annee'] = df_dates['date'].dt.year
df_dates['mois'] = df_dates['date'].dt.month
df_dates['nom_mois'] = df_dates['date'].dt.month_name()
df_dates['trimestre'] = df_dates['date'].dt.quarter
df_dates['semaine'] = df_dates['date'].dt.isocalendar().week
df_dates['jour_semaine'] = df_dates['date'].dt.day_name()
df_dates['est_weekend'] = df_dates['date'].dt.dayofweek >= 5
print(df_dates)

# Resample : Agréger par période
# Monthly
monthly = ventes.resample('ME').sum()     # Total mensuel
monthly_mean = ventes.resample('ME').mean()  # Moyenne mensuelle

# Weekly
weekly = ventes.resample('W').sum()

# Quarterly
quarterly = ventes.resample('QE').sum()

print("\nVentes mensuelles :")
print(monthly)

# Groupby temporel
ventes_df = ventes.reset_index()
ventes_df.columns = ['date', 'montant']
ventes_df['mois'] = ventes_df['date'].dt.month
ventes_df['jour_semaine'] = ventes_df['date'].dt.day_name()

par_mois = ventes_df.groupby('mois')['montant'].mean()
print("\nMoyenne par mois :")
print(par_mois)

# Rolling (Moyenne mobile)
ventes_smooth = ventes.rolling(window=7).mean()   # Fenêtre 7 jours

# Décaler (shift)
ventes_j1 = ventes.shift(1)     # Valeur du jour précédent
croissance = (ventes - ventes_j1) / ventes_j1 * 100  # Croissance %


# ============================================================================
# [GUIDE] CHAPITRE 3.9 : APPLY ET TRANSFORMATIONS
# ============================================================================

"""
[IDEE] apply() = Appliquer UNE FONCTION CUSTOM sur chaque ligne/colonne

QUAND ?
- Transformation complexe pas disponible directement
- Logique métier custom
- Traitement de strings complexe
"""

df = pd.read_csv(io.StringIO(csv_data))

# ---- apply sur colonne (Series) ----
# Fonction simple
df['Nom_majuscule'] = df['Nom'].apply(str.upper)

# Lambda
df['Salaire_net'] = df['Salaire'].apply(lambda x: x * 0.75)  # -25% charges

# Fonction personnalisée
def categoriser_salaire(salaire):
    if salaire < 40000:
        return 'Junior'
    elif salaire < 60000:
        return 'Intermédiaire'
    else:
        return 'Senior'

df['Categorie_sal'] = df['Salaire'].apply(categoriser_salaire)
print(df[['Nom', 'Salaire', 'Categorie_sal']])

# ---- apply sur DataFrame (ligne entière, axis=1) ----
def score_employe(row):
    """Calculer un score basé sur plusieurs colonnes"""
    score = 0
    score += min(row['Age'] / 10, 5)           # Max 5 pts pour âge
    score += min(row['Salaire'] / 10000, 7)     # Max 7 pts pour salaire
    return round(score, 1)

df['Score'] = df.apply(score_employe, axis=1)  # axis=1 = par ligne
print(df[['Nom', 'Age', 'Salaire', 'Score']])

# ---- map() et replace() pour Series ----
# Mapper des valeurs
dept_code = {'RH': 'HR', 'Tech': 'IT', 'Marketing': 'MKT'}
df['Dept_code'] = df['Departement'].map(dept_code)

# replace()
df['Ville_court'] = df['Ville'].replace({
    'Paris': 'PAR',
    'Lyon': 'LYO',
    'Marseille': 'MAR'
})

# ---- Traitement de strings (str accessor) ----
df['Prenom'] = df['Nom'].str.split(' ').str[0]   # Premier mot
df['Nom_fam'] = df['Nom'].str.split(' ').str[1]  # Deuxième mot
df['Email'] = (df['Prenom'].str.lower() + '.' + 
               df['Nom_fam'].str.lower() + '@company.com')

# Méthodes string courantes
serie_str = pd.Series(['  Hello  ', 'WORLD', '  Python  Data  '])
print(serie_str.str.strip())         # Enlever espaces
print(serie_str.str.lower())         # Minuscules
print(serie_str.str.upper())         # Majuscules
print(serie_str.str.title())         # Première lettre maj
print(serie_str.str.len())           # Longueur
print(serie_str.str.contains('o'))   # Contient 'o' ?
print(serie_str.str.replace('  ', ' '))  # Remplacer
print(serie_str.str.split(' '))      # Diviser


# ============================================================================
# [COURS] EXERCICE PRATIQUE 3 : ANALYSE PANDAS COMPLÈTE
# ============================================================================

"""
SCÉNARIO : Vous êtes data analyst dans une chaîne de magasins.
Vous devez analyser les ventes et fournir un rapport complet.
"""

# ---- DONNÉES ----
np.random.seed(42)
n = 500

categories = ['Électronique', 'Vêtements', 'Alimentation', 'Sport', 'Maison']
villes = ['Paris', 'Lyon', 'Marseille', 'Bordeaux', 'Toulouse']
vendeurs = [f'Vendeur_{i}' for i in range(1, 21)]

ventes_data = pd.DataFrame({
    'Date': pd.date_range('2024-01-01', periods=n, freq='D').repeat(1)[:n],
    'Categorie': np.random.choice(categories, n),
    'Ville': np.random.choice(villes, n),
    'Vendeur': np.random.choice(vendeurs, n),
    'Montant': np.round(np.random.exponential(scale=150, size=n) + 20, 2),
    'Quantite': np.random.randint(1, 10, n),
    'Client_nouveau': np.random.choice([True, False], n, p=[0.3, 0.7])
})

# Introduire quelques valeurs manquantes
indices_nan = np.random.choice(n, 20, replace=False)
ventes_data.loc[indices_nan[:10], 'Montant'] = np.nan
ventes_data.loc[indices_nan[10:], 'Quantite'] = np.nan

print("Données créées:", ventes_data.shape)
print(ventes_data.head())

# ---- EXERCICES ----
"""
1. Explorer les données (shape, types, manquants)
2. Nettoyer les données manquantes
3. Ajouter colonne 'Chiffre_affaires' = Montant × Quantite
4. CA total par catégorie (trié décroissant)
5. CA moyen par ville et catégorie (pivot table)
6. Top 5 vendeurs par CA total
7. Évolution mensuelle du CA
8. Taux de nouveaux clients par ville
"""

print("=" * 60)
print("     CORRIGÉ - EXERCICE PRATIQUE 3 (Pandas)")
print("=" * 60)

# 1. Exploration
print("\n1. EXPLORATION :")
print(f"  Shape : {ventes_data.shape}")
print(f"  Colonnes : {list(ventes_data.columns)}")
print(f"  Types :\n{ventes_data.dtypes}")
print(f"  Valeurs manquantes :\n{ventes_data.isnull().sum()}")

# 2. Nettoyage
ventes = ventes_data.copy()
ventes['Montant'] = ventes['Montant'].fillna(ventes['Montant'].median())
ventes['Quantite'] = ventes['Quantite'].fillna(ventes['Quantite'].median()).astype(int)
print(f"\n2. APRÈS NETTOYAGE : {ventes.isnull().sum().sum()} valeurs manquantes")

# 3. Chiffre d'affaires
ventes['Chiffre_affaires'] = ventes['Montant'] * ventes['Quantite']

# 4. CA par catégorie
print("\n4. CA PAR CATÉGORIE :")
ca_categorie = ventes.groupby('Categorie')['Chiffre_affaires'].sum().sort_values(ascending=False)
for cat, ca in ca_categorie.items():
    print(f"  {cat:<15} : {ca:>12,.0f} €")

# 5. Pivot table
print("\n5. CA MOYEN PAR VILLE × CATÉGORIE :")
pivot = ventes.pivot_table(
    values='Chiffre_affaires',
    index='Ville',
    columns='Categorie',
    aggfunc='sum',
    fill_value=0
).round(0)
print(pivot.to_string())

# 6. Top 5 vendeurs
print("\n6. TOP 5 VENDEURS :")
top_vendeurs = (ventes.groupby('Vendeur')['Chiffre_affaires']
                .sum()
                .sort_values(ascending=False)
                .head(5))
for rang, (vendeur, ca) in enumerate(top_vendeurs.items(), 1):
    print(f"  {rang}. {vendeur:<12} : {ca:>10,.0f} €")

# 7. Évolution mensuelle
print("\n7. ÉVOLUTION MENSUELLE :")
ventes['Mois'] = ventes['Date'].dt.to_period('M')
ca_mensuel = ventes.groupby('Mois')['Chiffre_affaires'].sum()
for mois, ca in ca_mensuel.head(6).items():
    print(f"  {mois} : {ca:>10,.0f} €")

# 8. Taux nouveaux clients
print("\n8. TAUX NOUVEAUX CLIENTS PAR VILLE :")
taux_nouveaux = ventes.groupby('Ville')['Client_nouveau'].mean() * 100
for ville, taux in taux_nouveaux.sort_values(ascending=False).items():
    print(f"  {ville:<15} : {taux:.1f}%")

"""
CONCEPTS CLÉS MAÎTRISÉS :
[OK] Lecture et exploration de DataFrame
[OK] Nettoyage (fillna)
[OK] Création de colonnes calculées
[OK] groupby + agg
[OK] pivot_table
[OK] Manipulation de dates (dt accessor)
[OK] Filtrage et tri

PROCHAINE ÉTAPE -> PARTIE 4 : Visualisation
"""

# ============================================================================
# [LIVRE] ANALYSE DE DONNÉES - PARTIE 4 : VISUALISATION
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 4.1 : Matplotlib - Bases et anatomie d'un graphique
# - Chapitre 4.2 : Types de graphiques Matplotlib
# - Chapitre 4.3 : Personnalisation avancée
# - Chapitre 4.4 : Subplots (graphiques multiples)
# - Chapitre 4.5 : Seaborn - Visualisation statistique
# - Chapitre 4.6 : Pandas .plot() intégré
# - Chapitre 4.7 : Bonnes pratiques de visualisation
#
# [TEMPS] TEMPS : ~6-8 heures
# ============================================================================

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
from datetime import datetime, timedelta

# Configuration globale
plt.rcParams['figure.dpi'] = 100
plt.rcParams['font.size'] = 11
sns.set_theme(style='whitegrid')

# ============================================================================
# [GUIDE] CHAPITRE 4.1 : ANATOMIE D'UN GRAPHIQUE MATPLOTLIB
# ============================================================================

"""
[REFLEXION] POURQUOI APPRENDRE MATPLOTLIB ?

"Un graphique vaut mille mots"
La visualisation est ESSENTIELLE pour :
- Comprendre vos données (exploration)
- Communiquer vos résultats
- Détecter anomalies et patterns
- Raconter une histoire avec les données

ANATOMIE D'UN GRAPHIQUE MATPLOTLIB :

Figure = La "toile" complète (peut contenir plusieurs graphiques)
  └── Axes = Un graphique individuel (contient les données)
        ├── Title : Titre
        ├── X-axis : Axe horizontal (xlabel, xticks)
        ├── Y-axis : Axe vertical (ylabel, yticks)
        ├── Legend : Légende
        ├── Grid : Grille
        └── Data : Les données (lignes, barres, points...)
"""

# ---- Deux interfaces ----

# INTERFACE 1 : Orientée État (simple, pour débutants)
plt.figure(figsize=(8, 5))
plt.plot([1, 2, 3], [1, 4, 9])
plt.title('Interface État')
plt.xlabel('X')
plt.ylabel('Y')
plt.show()

# INTERFACE 2 : Orientée Objet (recommandée, plus contrôle)
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot([1, 2, 3], [1, 4, 9])
ax.set_title('Interface Objet')
ax.set_xlabel('X')
ax.set_ylabel('Y')
plt.tight_layout()
plt.show()

"""
[IDEE] UTILISER L'INTERFACE OBJET !
Elle permet de contrôler précisément chaque élément
Indispensable pour graphiques multiples (subplots)
"""

# ============================================================================
# [GUIDE] CHAPITRE 4.2 : TYPES DE GRAPHIQUES MATPLOTLIB
# ============================================================================

# Données de démonstration
np.random.seed(42)
x = np.linspace(0, 10, 100)
y1 = np.sin(x)
y2 = np.cos(x)

# ---- 1. GRAPHIQUE EN LIGNE (plot) ----
"""
QUAND ? Séries temporelles, évolutions, fonctions continues
"""
fig, ax = plt.subplots(figsize=(10, 5))

ax.plot(x, y1, label='sin(x)', color='royalblue', linewidth=2)
ax.plot(x, y2, label='cos(x)', color='crimson', linewidth=2, 
        linestyle='--')  # Pointillés

ax.set_title('Fonctions Trigonométriques', fontsize=16, fontweight='bold')
ax.set_xlabel('Angle (radians)')
ax.set_ylabel('Valeur')
ax.legend(fontsize=12)
ax.grid(True, alpha=0.3)
ax.axhline(y=0, color='black', linewidth=0.5)  # Ligne y=0
ax.set_xlim(0, 10)
ax.set_ylim(-1.5, 1.5)
plt.tight_layout()
plt.savefig('01_lignes.png', dpi=150, bbox_inches='tight')
plt.show()

# ---- 2. NUAGE DE POINTS (scatter) ----
"""
QUAND ? Relation entre deux variables numériques
"""
n = 200
x_scatter = np.random.normal(0, 1, n)
y_scatter = x_scatter * 1.5 + np.random.normal(0, 0.5, n)
couleurs = np.abs(x_scatter)  # Couleur selon valeur

fig, ax = plt.subplots(figsize=(8, 6))
scatter = ax.scatter(
    x_scatter, y_scatter,
    c=couleurs,             # Couleur selon troisième variable
    cmap='coolwarm',        # Palette de couleurs
    alpha=0.6,              # Transparence
    s=50,                   # Taille des points
    edgecolors='white',     # Contour blanc
    linewidth=0.5
)

plt.colorbar(scatter, ax=ax, label='Valeur absolue de X')
ax.set_title('Nuage de Points avec Couleur', fontsize=14)
ax.set_xlabel('X')
ax.set_ylabel('Y')
plt.tight_layout()
plt.savefig('02_scatter.png', dpi=150, bbox_inches='tight')
plt.show()

# ---- 3. DIAGRAMME EN BARRES (bar) ----
"""
QUAND ? Comparer des catégories
"""
categories = ['RH', 'Tech', 'Marketing', 'Finance', 'Opérations']
valeurs = [45000, 68000, 52000, 71000, 48000]
couleurs_bar = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6']

fig, ax = plt.subplots(figsize=(10, 6))

barres = ax.bar(categories, valeurs, color=couleurs_bar, 
                edgecolor='white', linewidth=1.5, width=0.6)

# Ajouter valeurs sur les barres
for barre in barres:
    hauteur = barre.get_height()
    ax.text(
        barre.get_x() + barre.get_width() / 2.,  # Position X
        hauteur + 500,                             # Position Y (légèrement au-dessus)
        f'{hauteur:,} €',                          # Texte
        ha='center', va='bottom', fontweight='bold'
    )

ax.set_title('Salaire Moyen par Département', fontsize=14, fontweight='bold')
ax.set_xlabel('Département')
ax.set_ylabel('Salaire Annuel (€)')
ax.set_ylim(0, max(valeurs) * 1.15)  # Espace pour les étiquettes
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'{x:,.0f}'))
plt.tight_layout()
plt.savefig('03_barres.png', dpi=150, bbox_inches='tight')
plt.show()

# ---- 4. HISTOGRAMME ----
"""
QUAND ? Distribution d'une variable continue
"""
data_hist = np.concatenate([
    np.random.normal(50, 10, 300),   # Groupe 1
    np.random.normal(75, 8, 200),    # Groupe 2
])

fig, ax = plt.subplots(figsize=(10, 6))
n_count, bins, patches = ax.hist(
    data_hist,
    bins=30,
    color='steelblue',
    edgecolor='white',
    alpha=0.7,
    density=False       # True pour probabilité
)

# Ligne de distribution normale
from scipy import stats
mu, sigma = data_hist.mean(), data_hist.std()
x_norm = np.linspace(data_hist.min(), data_hist.max(), 100)
# Adapter à la hauteur de l'histogramme
ax_twin = ax.twinx()
ax_twin.plot(x_norm, stats.norm.pdf(x_norm, mu, sigma), 
             'r-', linewidth=2, label='Distribution normale')
ax_twin.set_ylabel('Densité', color='red')

ax.axvline(mu, color='orange', linewidth=2, linestyle='--', label=f'Moyenne: {mu:.1f}')
ax.set_title('Histogramme de Distribution', fontsize=14)
ax.set_xlabel('Valeur')
ax.set_ylabel('Fréquence')
ax.legend()
plt.tight_layout()
plt.savefig('04_histogramme.png', dpi=150, bbox_inches='tight')
plt.show()

# ---- 5. BOXPLOT (Boîte à moustaches) ----
"""
QUAND ? Comparer distributions, détecter outliers
"""
data_box = {
    'Groupe A': np.random.normal(70, 15, 100),
    'Groupe B': np.random.normal(80, 10, 100),
    'Groupe C': np.random.normal(65, 20, 100),
}
# Ajouter outliers manuellement
data_box['Groupe A'] = np.append(data_box['Groupe A'], [10, 5, 130])

fig, ax = plt.subplots(figsize=(10, 6))

bp = ax.boxplot(
    data_box.values(),
    labels=data_box.keys(),
    notch=False,            # True pour intervalle de confiance médiane
    patch_artist=True,      # Remplir les boîtes avec couleur
    showfliers=True,        # Afficher outliers
    medianprops={'color': 'red', 'linewidth': 2}
)

# Colorier les boîtes
couleurs_box = ['lightblue', 'lightgreen', 'lightyellow']
for patch, couleur in zip(bp['boxes'], couleurs_box):
    patch.set_facecolor(couleur)

ax.set_title('Comparaison des Distributions', fontsize=14)
ax.set_xlabel('Groupe')
ax.set_ylabel('Valeur')
ax.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.savefig('05_boxplot.png', dpi=150, bbox_inches='tight')
plt.show()

# ---- 6. CAMEMBERT (pie) ----
"""
QUAND ? Proportions d'un tout (max 5-6 catégories)
Attention : Souvent critiqué -> préférer barres horizontales !
"""
parts = [35, 28, 20, 12, 5]
labels = ['Électronique', 'Vêtements', 'Alimentation', 'Sport', 'Autre']
explode = (0.1, 0, 0, 0, 0)  # Décoller première part

fig, ax = plt.subplots(figsize=(8, 8))
ax.pie(
    parts, labels=labels, explode=explode,
    autopct='%1.1f%%',          # Format pourcentage
    startangle=90,              # Commencer à 12h
    shadow=True,
    colors=plt.cm.Set3.colors[:5]
)
ax.set_title('Répartition des Ventes par Catégorie', fontsize=14)
plt.tight_layout()
plt.savefig('06_camembert.png', dpi=150, bbox_inches='tight')
plt.show()


# ============================================================================
# [GUIDE] CHAPITRE 4.3 : PERSONNALISATION AVANCÉE
# ============================================================================

"""
[IDEE] UN BON GRAPHIQUE = DONNÉES CLAIRES + DESIGN PROPRE
"""

# Styles disponibles
print("Styles matplotlib:")
print(plt.style.available[:10])  # Quelques styles

# Utiliser un style
plt.style.use('seaborn-v0_8-whitegrid')

# ---- Couleurs ----
# Couleurs nommées
couleur_1 = 'royalblue'
couleur_2 = 'crimson'

# Hex
couleur_hex = '#3498db'

# RGB normalisé
couleur_rgb = (0.2, 0.4, 0.8)

# Palettes
palette_blues = plt.cm.Blues(np.linspace(0.3, 1, 5))
palette_viridis = plt.cm.viridis(np.linspace(0, 1, 5))

# ---- Annotations ----
fig, ax = plt.subplots(figsize=(10, 6))

x = np.linspace(0, 10, 100)
y = np.sin(x) * np.exp(-x/5)
ax.plot(x, y, 'royalblue', linewidth=2)

# Trouver le maximum
idx_max = np.argmax(y)
ax.annotate(
    f'Maximum\n({x[idx_max]:.1f}, {y[idx_max]:.2f})',
    xy=(x[idx_max], y[idx_max]),           # Point pointé
    xytext=(x[idx_max]+1, y[idx_max]+0.1), # Position du texte
    arrowprops=dict(arrowstyle='->', color='red', lw=2),
    fontsize=11, color='red'
)

# Texte libre
ax.text(7, 0.3, 'Zone\nstabilisée', fontsize=10, 
        ha='center', style='italic', color='gray',
        bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))

# Zone colorée
ax.axvspan(0, 2, alpha=0.1, color='green', label='Zone active')
ax.axhline(0, color='black', linewidth=0.5, linestyle='--')

ax.set_title('Oscillation Amortie', fontsize=16)
ax.set_xlabel('Temps')
ax.set_ylabel('Amplitude')
ax.legend()
plt.tight_layout()
plt.savefig('07_annotations.png', dpi=150, bbox_inches='tight')
plt.show()

# ---- Formatage des axes ----
import matplotlib.ticker as mticker

fig, ax = plt.subplots(figsize=(10, 5))
dates = pd.date_range('2024-01', periods=12, freq='ME')
valeurs_m = np.random.randint(10000, 100000, 12)

ax.plot(dates, valeurs_m, 'o-', color='steelblue', linewidth=2)

# Formater axe Y en milliers
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, p: f'{x/1000:.0f}k'))

# Formater axe X (dates)
import matplotlib.dates as mdates
ax.xaxis.set_major_formatter(mdates.DateFormatter('%b\n%Y'))
ax.xaxis.set_major_locator(mdates.MonthLocator())

ax.set_title("Chiffre d'Affaires Mensuel 2024")
ax.set_ylabel("CA (milliers €)")
plt.tight_layout()
plt.show()


# ============================================================================
# [GUIDE] CHAPITRE 4.4 : SUBPLOTS (GRAPHIQUES MULTIPLES)
# ============================================================================

"""
[IDEE] SUBPLOTS = Plusieurs graphiques dans une même figure
Essentiel pour comparaisons et tableaux de bord !
"""

np.random.seed(42)

# ---- Grille simple 2×2 ----
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle('Tableau de Bord des Ventes', fontsize=18, fontweight='bold')

# Données
mois = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin']
ca = [48000, 52000, 45000, 61000, 58000, 67000]
clients = [120, 135, 115, 155, 148, 172]
categories = ['Électro', 'Vêtements', 'Alim', 'Sport']
parts_cat = [35, 28, 22, 15]

# Graphique 1 : CA mensuel (barre)
axes[0, 0].bar(mois, ca, color='steelblue', edgecolor='white')
axes[0, 0].set_title("CA Mensuel (€)")
axes[0, 0].set_ylabel("Montant (€)")

# Graphique 2 : Clients (ligne)
axes[0, 1].plot(mois, clients, 'o-', color='orangered', linewidth=2, markersize=8)
axes[0, 1].fill_between(range(len(mois)), clients, alpha=0.1, color='orangered')
axes[0, 1].set_title("Nombre de Clients")
axes[0, 1].set_ylabel("Clients")
axes[0, 1].set_xticklabels(mois)

# Graphique 3 : Camembert
axes[1, 0].pie(parts_cat, labels=categories, autopct='%1.1f%%')
axes[1, 0].set_title("Répartition par Catégorie")

# Graphique 4 : Corrélation CA/Clients
axes[1, 1].scatter(clients, ca, color='purple', alpha=0.7, s=100)
for i, m in enumerate(mois):
    axes[1, 1].annotate(m, (clients[i], ca[i]), 
                         xytext=(5, 5), textcoords='offset points')
axes[1, 1].set_title("Corrélation Clients <-> CA")
axes[1, 1].set_xlabel("Clients")
axes[1, 1].set_ylabel("CA (€)")

plt.tight_layout()
plt.savefig('08_dashboard.png', dpi=150, bbox_inches='tight')
plt.show()

# ---- Grille asymétrique avec gridspec ----
from matplotlib.gridspec import GridSpec

fig = plt.figure(figsize=(14, 8))
gs = GridSpec(2, 3, figure=fig, hspace=0.35, wspace=0.35)

ax1 = fig.add_subplot(gs[0, :])       # Ligne 0, toutes les colonnes
ax2 = fig.add_subplot(gs[1, 0])       # Ligne 1, colonne 0
ax3 = fig.add_subplot(gs[1, 1])       # Ligne 1, colonne 1
ax4 = fig.add_subplot(gs[1, 2])       # Ligne 1, colonne 2

# Graphique principal (en haut)
x = pd.date_range('2024-01', periods=365, freq='D')
y = np.cumsum(np.random.randn(365) * 100 + 10)
ax1.plot(x, y, color='royalblue', linewidth=1.5)
ax1.set_title('Évolution Annuelle du CA Cumulé', fontsize=14)
ax1.set_ylabel('CA Cumulé (€)')

# Petits graphiques en bas
for ax, mois_label in zip([ax2, ax3, ax4], ['T1', 'T2', 'T3']):
    data_t = np.random.randint(20000, 80000, 3)
    ax.bar(['Mois 1', 'Mois 2', 'Mois 3'], data_t, color='steelblue')
    ax.set_title(f'Trimestre {mois_label}')

fig.suptitle('Rapport Annuel des Ventes', fontsize=16, fontweight='bold', y=1.02)
plt.savefig('09_gridspec.png', dpi=150, bbox_inches='tight')
plt.show()


# ============================================================================
# [GUIDE] CHAPITRE 4.5 : SEABORN - VISUALISATION STATISTIQUE
# ============================================================================

"""
[IDEE] SEABORN = Matplotlib avec des statistiques intégrées

AVANTAGES vs Matplotlib :
[OK] Beaux graphiques par défaut
[OK] Intégration directe avec Pandas DataFrames
[OK] Graphiques statistiques prêts à l'emploi
[OK] Couleurs et styles soignés
[OK] Moins de code pour plus de résultats

QUAND UTILISER SEABORN ?
-> Analyse exploratoire
-> Comparaisons statistiques
-> Visualisation de distributions
"""

# Créer dataset pour Seaborn
np.random.seed(42)
df_employees = pd.DataFrame({
    'Salaire': np.concatenate([
        np.random.normal(45000, 8000, 100),   # Junior
        np.random.normal(65000, 10000, 80),   # Senior
        np.random.normal(90000, 15000, 50),   # Expert
    ]),
    'Niveau': (['Junior'] * 100 + ['Senior'] * 80 + ['Expert'] * 50),
    'Age': np.concatenate([
        np.random.normal(26, 3, 100),
        np.random.normal(35, 4, 80),
        np.random.normal(45, 5, 50),
    ]),
    'Experience': np.concatenate([
        np.random.normal(2, 1, 100),
        np.random.normal(8, 2, 80),
        np.random.normal(18, 4, 50),
    ]),
    'Departement': np.random.choice(['Tech', 'RH', 'Marketing', 'Finance'], 230),
    'Satisfaction': np.random.uniform(1, 10, 230)
})
df_employees['Salaire'] = df_employees['Salaire'].clip(25000, 150000)

# ---- 1. Histplot (Distribution) ----
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

sns.histplot(df_employees['Salaire'], kde=True, ax=axes[0], 
             color='steelblue', bins=30)
axes[0].set_title('Distribution des Salaires')
axes[0].set_xlabel('Salaire (€)')

sns.histplot(data=df_employees, x='Salaire', hue='Niveau', 
             kde=True, ax=axes[1], bins=20)
axes[1].set_title('Distribution par Niveau')

plt.tight_layout()
plt.savefig('10_histplot.png', dpi=150, bbox_inches='tight')
plt.show()

# ---- 2. Boxplot et Violinplot ----
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

sns.boxplot(data=df_employees, x='Niveau', y='Salaire', 
            ax=axes[0], palette='Set2',
            order=['Junior', 'Senior', 'Expert'])
axes[0].set_title('Boxplot des Salaires par Niveau')

sns.violinplot(data=df_employees, x='Niveau', y='Salaire', 
               ax=axes[1], palette='Set2',
               order=['Junior', 'Senior', 'Expert'],
               inner='box')     # Afficher boxplot à l'intérieur
axes[1].set_title('Violinplot des Salaires par Niveau')

plt.tight_layout()
plt.savefig('11_boxviolin.png', dpi=150, bbox_inches='tight')
plt.show()

# ---- 3. Scatterplot avec régression ----
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# Scatter avec hue (couleur par catégorie)
sns.scatterplot(data=df_employees, x='Age', y='Salaire', 
                hue='Niveau', ax=axes[0], alpha=0.6,
                palette='Set1')
axes[0].set_title('Age vs Salaire par Niveau')

# Régression avec lmplot/regplot
sns.regplot(data=df_employees, x='Experience', y='Salaire',
            ax=axes[1], scatter_kws={'alpha': 0.3},
            line_kws={'color': 'red', 'linewidth': 2})
axes[1].set_title('Régression Expérience -> Salaire')

plt.tight_layout()
plt.savefig('12_scatter_reg.png', dpi=150, bbox_inches='tight')
plt.show()

# ---- 4. Heatmap (Matrice de corrélation) ----
corr_matrix = df_employees[['Salaire', 'Age', 'Experience', 'Satisfaction']].corr()

fig, ax = plt.subplots(figsize=(8, 6))
sns.heatmap(
    corr_matrix,
    annot=True,          # Afficher valeurs
    fmt='.2f',           # Format décimales
    cmap='RdYlGn',       # Palette rouge-jaune-vert
    vmin=-1, vmax=1,     # Limites
    center=0,            # Centrer sur 0
    square=True,         # Carré
    linewidths=0.5
)
ax.set_title('Matrice de Corrélation', fontsize=14)
plt.tight_layout()
plt.savefig('13_heatmap.png', dpi=150, bbox_inches='tight')
plt.show()

# ---- 5. Pairplot (Toutes les combinaisons) ----
"""
[IDEE] pairplot = Graphique de PAIRES
Montre toutes les relations entre variables d'un coup
Très utile pour EDA (exploration) !
"""
g = sns.pairplot(
    df_employees[['Salaire', 'Age', 'Experience', 'Satisfaction', 'Niveau']],
    hue='Niveau',
    diag_kind='kde',     # Diagonale : distribution
    plot_kws={'alpha': 0.5}
)
g.fig.suptitle('Pairplot des Variables', y=1.02)
plt.savefig('14_pairplot.png', dpi=150, bbox_inches='tight')
plt.show()

# ---- 6. Barplot et Countplot ----
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

sns.barplot(data=df_employees, x='Departement', y='Salaire',
            ax=axes[0], estimator='mean', palette='Blues_d',
            errorbar='sd')     # Afficher écart-type
axes[0].set_title('Salaire Moyen par Département')

sns.countplot(data=df_employees, x='Departement',
              ax=axes[1], palette='Greens_d',
              order=df_employees['Departement'].value_counts().index)
axes[1].set_title('Effectif par Département')

plt.tight_layout()
plt.savefig('15_barcount.png', dpi=150, bbox_inches='tight')
plt.show()

# ---- 7. FacetGrid (Graphiques par groupe) ----
g = sns.FacetGrid(
    df_employees, 
    col='Departement', 
    col_wrap=2,          # 2 colonnes
    height=4, aspect=1.5
)
g.map(sns.histplot, 'Salaire', kde=True, bins=15)
g.set_titles(col_template='{col_name}')
g.set_xlabels('Salaire (€)')
g.set_ylabels('Fréquence')
g.fig.suptitle('Distribution des Salaires par Département', 
               y=1.02, fontsize=14)
plt.savefig('16_facetgrid.png', dpi=150, bbox_inches='tight')
plt.show()


# ============================================================================
# [GUIDE] CHAPITRE 4.6 : PANDAS .PLOT() INTÉGRÉ
# ============================================================================

"""
[IDEE] Pandas intègre Matplotlib -> Graphiques rapides !
Parfait pour exploration rapide
"""

# Données
df_ventes = pd.DataFrame({
    'Mois': pd.date_range('2024-01', periods=12, freq='ME'),
    'Electronique': np.random.randint(30000, 80000, 12),
    'Vetements': np.random.randint(15000, 40000, 12),
    'Alimentation': np.random.randint(20000, 50000, 12)
}).set_index('Mois')

# Ligne
df_ventes.plot(figsize=(12, 5), title='Évolution des Ventes')
plt.ylabel('Montant (€)')
plt.tight_layout()
plt.show()

# Barres
df_ventes.plot(kind='bar', figsize=(12, 5), title='Ventes par Mois')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

# Barres empilées
df_ventes.plot(kind='bar', stacked=True, figsize=(12, 5), 
               title='Ventes Empilées')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

# Histogramme
df_employees['Salaire'].plot(kind='hist', bins=30, figsize=(10, 5),
                              title='Distribution des Salaires', color='steelblue')
plt.xlabel('Salaire (€)')
plt.tight_layout()
plt.show()

# Box
df_employees.boxplot(column='Salaire', by='Niveau', figsize=(10, 6))
plt.title('Salaires par Niveau')
plt.tight_layout()
plt.show()


# ============================================================================
# [GUIDE] CHAPITRE 4.7 : BONNES PRATIQUES DE VISUALISATION
# ============================================================================

"""
[OK] RÈGLES D'OR DE LA VISUALISATION

1. TITRE CLAIR ET DESCRIPTIF
   - "Distribution des Salaires par Département en 2024"
   - PAS seulement "Graphique 1"

2. AXES ÉTIQUETÉS (TOUJOURS !)
   - Avec UNITÉS si nécessaire (€, %, cm, kg...)
   
3. LÉGENDE LISIBLE
   - Seulement si nécessaire
   - Placement optimal

4. PALETTE DE COULEURS ADAPTÉE
   - Données catégorielles -> Qualitative (Set1, Set2, Paired)
   - Données ordonnées -> Séquentielle (Blues, Viridis, YlOrRd)
   - Données divergentes -> Divergente (RdYlGn, RdBu, coolwarm)
   - Daltonisme -> viridis, cividis, plasma

5. CHOIX DU BON TYPE DE GRAPHIQUE
   - Comparaison catégories -> Barres
   - Évolution temporelle -> Lignes
   - Relation 2 variables -> Scatter
   - Distribution -> Histogramme ou Boxplot
   - Proportions -> Camembert (max 5-6) ou barres horizontales
   - Corrélation multiple -> Heatmap

6. SIMPLICITÉ
   - Supprimer éléments superflus
   - Grid légère (alpha=0.3)
   - Fond blanc

7. COHÉRENCE
   - Même palette dans tout le rapport
   - Même style

8. ACCESSIBILITÉ
   - Pas seulement la couleur pour distinguer
   - Annotations pour valeurs importantes
   - Contraste suffisant
"""

# Exemple : Mauvais vs Bon graphique

fig, axes = plt.subplots(1, 2, figsize=(16, 6))

categories = ['A', 'B', 'C', 'D', 'E']
valeurs = [42, 67, 31, 58, 49]

# [X] MAUVAIS
axes[0].bar(categories, valeurs)
axes[0].set_title('Données')

# [OK] BON
couleurs_bien = ['#e74c3c' if v == max(valeurs) else '#3498db' for v in valeurs]
barres = axes[1].barh(categories, valeurs, color=couleurs_bien, 
                       edgecolor='white', height=0.6)
for barre in barres:
    w = barre.get_width()
    axes[1].text(w + 0.5, barre.get_y() + barre.get_height()/2,
                  f'{w}%', ha='left', va='center', fontweight='bold')
axes[1].set_title('Taux de Satisfaction par Équipe (2024)', 
                   fontsize=13, fontweight='bold')
axes[1].set_xlabel('Taux de Satisfaction (%)')
axes[1].set_xlim(0, 80)
axes[1].axvline(x=50, color='gray', linestyle='--', alpha=0.5, label='Objectif 50%')
axes[1].legend()
axes[1].grid(axis='x', alpha=0.3)
axes[1].set_frame_on(False)  # Supprimer le cadre

plt.tight_layout()
plt.savefig('17_bon_mauvais.png', dpi=150, bbox_inches='tight')
plt.show()


# ============================================================================
# [COURS] EXERCICE PRATIQUE 4 : TABLEAU DE BORD COMPLET
# ============================================================================

"""
SCÉNARIO : Créer un tableau de bord des ventes pour la direction
DONNÉES : Ventes sur 12 mois, 4 catégories, 5 régions
"""

# Données
np.random.seed(42)
mois_list = pd.date_range('2024-01', periods=12, freq='ME')
categories = ['Électronique', 'Vêtements', 'Alimentation', 'Sport']
regions = ['Nord', 'Sud', 'Est', 'Ouest', 'Centre']

# CA par mois et catégorie
ca_data = {}
for cat in categories:
    base = np.random.randint(30000, 80000)
    tendance = np.linspace(0, base * 0.3, 12)  # Croissance
    bruit = np.random.randint(-5000, 5000, 12)
    saisonnalite = np.sin(np.linspace(0, 2*np.pi, 12)) * base * 0.2
    ca_data[cat] = base + tendance + bruit + saisonnalite

df_ca = pd.DataFrame(ca_data, index=mois_list)

# CA par région
ca_region = pd.Series(
    np.random.randint(100000, 500000, 5),
    index=regions
).sort_values()

# Distribution satisfaction
satisfaction = np.concatenate([
    np.random.normal(7.5, 1.2, 150),
    np.random.normal(5.0, 0.8, 50)
])

# Données temporelles cumulées
ca_mensuel = df_ca.sum(axis=1)
ca_cumule = ca_mensuel.cumsum()
objectif_cumule = pd.Series(
    np.linspace(50000*12, 60000*12, 12),
    index=mois_list
).cumsum() / 12 * np.arange(1, 13)

# ---- TABLEAU DE BORD ----
fig = plt.figure(figsize=(18, 12))
fig.patch.set_facecolor('#f8f9fa')

gs = GridSpec(3, 3, figure=fig, hspace=0.4, wspace=0.3)

# Titre principal
fig.text(0.5, 0.98, 'TABLEAU DE BORD DES VENTES 2024',
         ha='center', va='top', fontsize=20, fontweight='bold', color='#2c3e50')
fig.text(0.5, 0.95, f'Mis à jour le {datetime.now().strftime("%d/%m/%Y")}',
         ha='center', va='top', fontsize=11, color='#7f8c8d')

# KPIs (indicateurs clés en haut)
ax_kpi1 = fig.add_subplot(gs[0, 0])
ax_kpi2 = fig.add_subplot(gs[0, 1])
ax_kpi3 = fig.add_subplot(gs[0, 2])

for ax_kpi in [ax_kpi1, ax_kpi2, ax_kpi3]:
    ax_kpi.set_facecolor('#2c3e50')
    ax_kpi.axis('off')

ax_kpi1.text(0.5, 0.7, f'{ca_mensuel.sum():,.0f} €',
              ha='center', fontsize=20, fontweight='bold', color='#2ecc71',
              transform=ax_kpi1.transAxes)
ax_kpi1.text(0.5, 0.3, 'CA Total 2024',
              ha='center', fontsize=12, color='white',
              transform=ax_kpi1.transAxes)

ax_kpi2.text(0.5, 0.7, f'{ca_mensuel.mean():,.0f} €',
              ha='center', fontsize=20, fontweight='bold', color='#3498db',
              transform=ax_kpi2.transAxes)
ax_kpi2.text(0.5, 0.3, 'CA Mensuel Moyen',
              ha='center', fontsize=12, color='white',
              transform=ax_kpi2.transAxes)

croissance = (ca_mensuel.iloc[-1] - ca_mensuel.iloc[0]) / ca_mensuel.iloc[0] * 100
couleur_croiss = '#2ecc71' if croissance > 0 else '#e74c3c'
ax_kpi3.text(0.5, 0.7, f'+{croissance:.1f}%' if croissance > 0 else f'{croissance:.1f}%',
              ha='center', fontsize=20, fontweight='bold', color=couleur_croiss,
              transform=ax_kpi3.transAxes)
ax_kpi3.text(0.5, 0.3, 'Croissance Jan->Déc',
              ha='center', fontsize=12, color='white',
              transform=ax_kpi3.transAxes)

# Graphique 1 : CA mensuel + Objectif (ligne 1, col 0-1)
ax1 = fig.add_subplot(gs[1, :2])
ax1.plot(mois_list, ca_mensuel/1000, 'o-', color='#3498db', 
          linewidth=2, markersize=6, label='CA Réel')
ax1.fill_between(mois_list, ca_mensuel/1000, alpha=0.1, color='#3498db')
ax1.set_title('Évolution du CA Mensuel', fontweight='bold')
ax1.set_ylabel('CA (k€)')
ax1.xaxis.set_major_formatter(mdates.DateFormatter('%b'))
ax1.legend()
ax1.grid(alpha=0.3)

# Graphique 2 : Répartition par catégorie (ligne 1, col 2)
ax2 = fig.add_subplot(gs[1, 2])
ca_par_cat = df_ca.sum()
couleurs_cat = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12']
wedges, texts, autotexts = ax2.pie(
    ca_par_cat, labels=ca_par_cat.index, 
    colors=couleurs_cat, autopct='%1.1f%%',
    textprops={'fontsize': 8}
)
ax2.set_title('Répartition par Catégorie', fontweight='bold')

# Graphique 3 : CA par région (ligne 2, col 0)
ax3 = fig.add_subplot(gs[2, 0])
colors_reg = ['#27ae60' if v == ca_region.max() else '#3498db' for v in ca_region.values]
ax3.barh(ca_region.index, ca_region.values/1000, color=colors_reg)
ax3.set_title('CA par Région (k€)', fontweight='bold')
ax3.set_xlabel('k€')

# Graphique 4 : Distribution satisfaction (ligne 2, col 1)
ax4 = fig.add_subplot(gs[2, 1])
ax4.hist(satisfaction.clip(1, 10), bins=25, color='#9b59b6', 
          edgecolor='white', alpha=0.8)
ax4.axvline(satisfaction.mean(), color='red', linewidth=2, 
             label=f'Moy: {satisfaction.mean():.1f}')
ax4.set_title('Distribution Satisfaction', fontweight='bold')
ax4.set_xlabel('Score (1-10)')
ax4.legend()

# Graphique 5 : Évolution par catégorie (ligne 2, col 2)
ax5 = fig.add_subplot(gs[2, 2])
for cat, couleur in zip(categories, couleurs_cat):
    ax5.plot(range(12), df_ca[cat]/1000, 
              linewidth=1.5, label=cat, color=couleur, alpha=0.8)
ax5.set_title('Tendances par Catégorie', fontweight='bold')
ax5.set_xlabel('Mois')
ax5.set_ylabel('k€')
ax5.legend(fontsize=8)
ax5.grid(alpha=0.3)

plt.savefig('18_tableau_de_bord.png', dpi=150, bbox_inches='tight',
            facecolor=fig.get_facecolor())
plt.show()

print("[OK] Tableau de bord sauvegardé : 18_tableau_de_bord.png")

"""
CONCEPTS CLÉS MAÎTRISÉS :
[OK] Matplotlib : figure, axes, subplots, gridspec
[OK] Types de graphiques (ligne, barre, scatter, hist, pie, box)
[OK] Personnalisation (couleurs, annotations, formatage)
[OK] Seaborn : distributions, corrélations, pairplot
[OK] Pandas .plot() pour exploration rapide
[OK] Tableau de bord complet
[OK] Bonnes pratiques de visualisation

PROCHAINE ÉTAPE -> PARTIE 5 : Statistiques Descriptives
"""

# ============================================================================
# [LIVRE] ANALYSE DE DONNÉES - PARTIES 5 à 9
# ============================================================================
#
# PARTIE 5 : STATISTIQUES DESCRIPTIVES
# PARTIE 6 : NETTOYAGE ET PRÉPARATION DES DONNÉES
# PARTIE 7 : ANALYSE EXPLORATOIRE (EDA)
# PARTIE 8 : CORRÉLATIONS ET RELATIONS
# PARTIE 9 : INTRODUCTION AU MACHINE LEARNING
#
# [TEMPS] TEMPS TOTAL : ~15-20 heures
# ============================================================================

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats

# ============================================================================
# [NOMBRE] PARTIE 5 : STATISTIQUES DESCRIPTIVES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS :
- Mesures de tendance centrale (moyenne, médiane, mode)
- Mesures de dispersion (variance, écart-type, IQR)
- Distribution et forme (asymétrie, kurtosis)
- Intervalles de confiance
"""

# ============================================================================
# [GUIDE] CHAPITRE 5.1 : MESURES DE TENDANCE CENTRALE
# ============================================================================

"""
TENDANCE CENTRALE = Valeur typique/représentative d'un jeu de données

TROIS MESURES PRINCIPALES :
1. Moyenne    -> Sensible aux valeurs extrêmes
2. Médiane    -> Robuste (non sensible aux outliers)
3. Mode       -> La valeur la plus fréquente
"""

# Données d'exemple
salaires = np.array([35000, 40000, 42000, 45000, 48000, 50000,
                      52000, 55000, 60000, 65000, 150000])  # 150k = outlier

# ---- Moyenne (Mean) ----
"""
COMMENT : Somme / Nombre d'éléments
QUAND : Distribution symétrique, sans outliers
PROBLÈME : Très sensible aux valeurs extrêmes
"""
moyenne = np.mean(salaires)
print(f"Moyenne   : {moyenne:,.0f} €")    # Tirée vers le haut par 150k

# ---- Médiane (Median) ----
"""
COMMENT : Valeur du milieu (après tri)
QUAND : Présence d'outliers, distribution asymétrique
AVANTAGE : Robuste (peu sensible aux extrêmes)
"""
mediane = np.median(salaires)
print(f"Médiane   : {mediane:,.0f} €")    # Valeur du milieu

# ---- Mode ----
"""
COMMENT : Valeur la plus fréquente
QUAND : Variables catégorielles ou données discrètes
"""
from scipy.stats import mode
notes_classe = [12, 15, 15, 18, 12, 14, 15, 12, 15, 10]
mode_result = mode(notes_classe)
print(f"Mode      : {mode_result.mode}")   # -> 15 (apparaît 4 fois)

# En Pandas
serie = pd.Series(notes_classe)
print(f"Mode Pandas: {serie.mode().values}")

# ---- Comparaison Moyenne vs Médiane ----
print(f"\n[IDEE] IMPACT DES OUTLIERS :")
print(f"Avec 150k  : Moyenne = {np.mean(salaires):,.0f}€, Médiane = {np.median(salaires):,.0f}€")
salaires_sans_outlier = salaires[salaires < 100000]
print(f"Sans 150k  : Moyenne = {np.mean(salaires_sans_outlier):,.0f}€, Médiane = {np.median(salaires_sans_outlier):,.0f}€")
print("-> Préférer la médiane quand outliers présents !")

# ============================================================================
# [GUIDE] CHAPITRE 5.2 : MESURES DE DISPERSION
# ============================================================================

"""
DISPERSION = Étalement des données autour du centre

MESURES :
1. Variance     -> Écart moyen au carré
2. Écart-type   -> Racine de la variance (même unité que les données)
3. IQR          -> Intervalle interquartile (Q3 - Q1)
4. Étendue      -> Max - Min
5. CV           -> Coefficient de Variation (relatif)
"""

notes = np.array([12, 15, 18, 10, 14, 16, 13, 17, 11, 15])

# ---- Variance ----
"""
FORMULE : moyenne((xi - μ)²)
POPULATION  -> divisé par N (np.var avec ddof=0)
ÉCHANTILLON -> divisé par N-1 (np.var avec ddof=1)
QUAND utiliser ddof=1 ? -> Toujours en pratique (estimateur non biaisé)
"""
variance_pop = np.var(notes, ddof=0)       # Population
variance_ech = np.var(notes, ddof=1)       # Échantillon
print(f"Variance pop : {variance_pop:.2f}")
print(f"Variance éch : {variance_ech:.2f}")

# ---- Écart-type (Standard Deviation) ----
"""
INTERPRÉTATION :
Petit écart-type -> Données regroupées près de la moyenne
Grand écart-type -> Données dispersées
"""
ecart_type = np.std(notes, ddof=1)
print(f"Écart-type : {ecart_type:.2f}")
print(f"-> La plupart des notes entre {np.mean(notes)-ecart_type:.1f} et {np.mean(notes)+ecart_type:.1f}")

# ---- Règle des 68-95-99.7 (distribution normale) ----
"""
Dans une distribution normale :
68% des données dans [μ - σ, μ + σ]
95% des données dans [μ - 2σ, μ + 2σ]
99.7% des données dans [μ - 3σ, μ + 3σ]
"""
mu = np.mean(notes)
sigma = np.std(notes, ddof=1)
print(f"\n68-95-99.7 :")
print(f"68% entre {mu-sigma:.1f} et {mu+sigma:.1f}")
print(f"95% entre {mu-2*sigma:.1f} et {mu+2*sigma:.1f}")

# ---- IQR (Interquartile Range) ----
"""
Q1 = 25ème percentile
Q3 = 75ème percentile
IQR = Q3 - Q1

AVANTAGE : Robuste aux outliers (comme médiane)
UTILISATION : Détecter les outliers (rule de Tukey)
"""
q1 = np.percentile(notes, 25)
q3 = np.percentile(notes, 75)
iqr = q3 - q1
print(f"\nQ1={q1:.1f}, Q3={q3:.1f}, IQR={iqr:.1f}")

# Bornes pour outliers (règle de Tukey)
borne_basse = q1 - 1.5 * iqr
borne_haute = q3 + 1.5 * iqr
print(f"Outliers si < {borne_basse:.1f} ou > {borne_haute:.1f}")

# ---- Coefficient de Variation (CV) ----
"""
CV = (Écart-type / Moyenne) × 100
UTILITÉ : Comparer la dispersion de variables différentes

CV < 15%  -> Faible variabilité
CV 15-35% -> Variabilité modérée
CV > 35%  -> Forte variabilité
"""
cv = (np.std(notes, ddof=1) / np.mean(notes)) * 100
print(f"\nCV = {cv:.1f}%")

# ============================================================================
# [GUIDE] CHAPITRE 5.3 : FORME DE LA DISTRIBUTION
# ============================================================================

"""
FORME = Caractéristiques visuelles de la distribution

Métriques :
1. Asymétrie (Skewness) : Distribution penchée à gauche ou droite
2. Aplatissement (Kurtosis) : Distribution pointue ou plate
"""

# Créer différentes distributions
np.random.seed(42)
dist_normale = np.random.normal(50, 10, 1000)
dist_asym_droite = np.random.exponential(scale=20, size=1000)  # Queue à droite
dist_asym_gauche = 100 - dist_asym_droite                       # Queue à gauche

# ---- Asymétrie (Skewness) ----
"""
Skewness = 0    -> Distribution symétrique (Moyenne ≈ Médiane)
Skewness > 0    -> Queue à droite (positive) : Médiane < Moyenne
Skewness < 0    -> Queue à gauche (négative) : Médiane > Moyenne

EXEMPLE : Salaires -> Asymétrie positive (quelques très riches tirent la moyenne)
"""
from scipy.stats import skew, kurtosis

for nom, dist in [('Normale', dist_normale), 
                   ('Asym. droite', dist_asym_droite),
                   ('Asym. gauche', dist_asym_gauche)]:
    print(f"{nom:<15}: skewness={skew(dist):+.2f}")

# ---- Kurtosis ----
"""
Kurtosis = 3    -> Distribution normale (mésokurtique)
Kurtosis > 3    -> Queue lourde, pic pointu (leptokurtique)
Kurtosis < 3    -> Queue légère, pic plat (platykurtique)
"""
# scipy retourne kurtosis - 3 (excess kurtosis, normalisé à 0)
print(f"\nKurtosis (excess): {kurtosis(dist_normale):.2f}")  # ~0

# ============================================================================
# [GUIDE] CHAPITRE 5.4 : STATISTIQUES AVANCÉES
# ============================================================================

"""
TESTS DE NORMALITÉ
Tester si des données suivent une distribution normale
"""

from scipy.stats import shapiro, normaltest, anderson

donnees = np.random.normal(0, 1, 100)

# Test de Shapiro-Wilk (recommandé pour n < 5000)
stat, p_shapiro = shapiro(donnees)
print(f"Shapiro-Wilk: statistic={stat:.4f}, p={p_shapiro:.4f}")
print(f"Normal ? {'OUI' if p_shapiro > 0.05 else 'NON'} (seuil 5%)")

# Test de D'Agostino-Pearson
stat, p_normal = normaltest(donnees)
print(f"D'Agostino:  statistic={stat:.4f}, p={p_normal:.4f}")

"""
INTERVALLES DE CONFIANCE
Estimer l'incertitude d'une statistique

IC à 95% : On est sûr à 95% que le vrai paramètre est dans cet intervalle
"""

echantillon = np.random.normal(50, 10, 30)
n = len(echantillon)
moyenne = np.mean(echantillon)
se = np.std(echantillon, ddof=1) / np.sqrt(n)  # Erreur standard

# IC à 95% avec distribution t (pour petit échantillon)
from scipy.stats import t
t_critique = t.ppf(0.975, df=n-1)   # 0.975 pour IC bilatéral
marge = t_critique * se

print(f"\nIC 95% : [{moyenne - marge:.1f}, {moyenne + marge:.1f}]")
print(f"-> La vraie moyenne est dans cet intervalle avec 95% de confiance")

# Avec scipy directement
ic = stats.t.interval(0.95, df=n-1, loc=moyenne, scale=se)
print(f"IC 95% (scipy): {ic[0]:.1f} à {ic[1]:.1f}")


# ============================================================================
# [COURS] EXERCICE PRATIQUE 5 : STATISTIQUES DESCRIPTIVES COMPLÈTES
# ============================================================================

"""
SCÉNARIO : Étude sur les performances de 3 algorithmes de tri
Comparer les temps d'exécution sur 100 essais chacun
"""

np.random.seed(42)

# Données
algo_A = np.random.gamma(2, 15, 100)      # Asymétrie positive
algo_B = np.random.normal(35, 5, 100)     # Normale
algo_C = np.random.uniform(5, 70, 100)   # Uniforme

# ---- CORRIGÉ ----
print("=" * 60)
print("     CORRIGÉ - EXERCICE PRATIQUE 5")
print("=" * 60)

algos = {'Algo A': algo_A, 'Algo B': algo_B, 'Algo C': algo_C}

print(f"\n{'Statistique':<20} {'Algo A':>10} {'Algo B':>10} {'Algo C':>10}")
print("-" * 53)

stats_list = [
    ('Moyenne', lambda x: np.mean(x)),
    ('Médiane', lambda x: np.median(x)),
    ('Écart-type', lambda x: np.std(x, ddof=1)),
    ('Min', np.min), ('Max', np.max),
    ('Q1 (25%)', lambda x: np.percentile(x, 25)),
    ('Q3 (75%)', lambda x: np.percentile(x, 75)),
    ('IQR', lambda x: np.percentile(x,75)-np.percentile(x,25)),
    ('Asymétrie', skew),
    ('CV (%)', lambda x: np.std(x,ddof=1)/np.mean(x)*100),
]

for nom, fn in stats_list:
    vals = [fn(d) for d in algos.values()]
    print(f"{nom:<20} {vals[0]:>10.1f} {vals[1]:>10.1f} {vals[2]:>10.1f}")

# Tests de normalité
print("\n--- Tests de Normalité (Shapiro-Wilk) ---")
for nom, data in algos.items():
    _, p = shapiro(data)
    print(f"{nom}: p={p:.4f} -> {'Normale' if p > 0.05 else 'Non-normale'}")

# Visualisation
fig, axes = plt.subplots(2, 3, figsize=(15, 8))

for i, (nom, data) in enumerate(algos.items()):
    # Histogramme
    axes[0, i].hist(data, bins=20, color=f'C{i}', edgecolor='white', alpha=0.7)
    axes[0, i].axvline(np.mean(data), color='red', label=f'Moy: {np.mean(data):.1f}')
    axes[0, i].axvline(np.median(data), color='blue', linestyle='--', label=f'Méd: {np.median(data):.1f}')
    axes[0, i].set_title(f'{nom} - Histogramme')
    axes[0, i].legend(fontsize=8)
    
    # Boxplot
    axes[1, i].boxplot(data, vert=True, patch_artist=True,
                        boxprops={'facecolor': f'C{i}', 'alpha': 0.5})
    axes[1, i].set_title(f'{nom} - Boxplot')
    axes[1, i].set_ylabel('Temps (ms)')

plt.suptitle("Comparaison des Algorithmes de Tri", fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig('exo5_stats.png', dpi=150, bbox_inches='tight')
plt.show()


# ============================================================================
# [NETTOYAGE] PARTIE 6 : NETTOYAGE ET PRÉPARATION DES DONNÉES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS :
- Gérer les valeurs manquantes (NaN)
- Traiter les doublons
- Corriger les types de données
- Détecter et traiter les outliers
- Normaliser et standardiser

[IDEE] RÈGLE D'OR :
"Les données sales donnent des analyses fausses"
En pratique, 80% du temps d'un data scientist = nettoyage !
"""

# ============================================================================
# [GUIDE] CHAPITRE 6.1 : VALEURS MANQUANTES
# ============================================================================

"""
TYPES DE DONNÉES MANQUANTES :
1. MCAR (Missing Completely At Random) -> Manquant par hasard pur
2. MAR (Missing At Random) -> Manquant selon d'autres variables
3. MNAR (Missing Not At Random) -> Manquant selon sa propre valeur

STRATÉGIES :
1. Supprimer les lignes/colonnes
2. Imputer (remplacer par une valeur calculée)
3. Garder (certains algorithmes gèrent les NaN)
"""

# Créer données avec valeurs manquantes
np.random.seed(42)
n = 200
df_propre = pd.DataFrame({
    'Age': np.random.randint(18, 65, n),
    'Salaire': np.random.normal(50000, 15000, n),
    'Experience': np.random.randint(0, 30, n),
    'Ville': np.random.choice(['Paris', 'Lyon', 'Marseille', None], n, p=[0.4, 0.3, 0.2, 0.1]),
    'Score': np.random.uniform(0, 100, n),
    'Email': [f'user{i}@test.com' for i in range(n)]
})

# Introduire des NaN
mask_age = np.random.random(n) < 0.05
mask_salaire = np.random.random(n) < 0.15
mask_experience = np.random.random(n) < 0.1
mask_score = np.random.random(n) < 0.2

df_propre.loc[mask_age, 'Age'] = np.nan
df_propre.loc[mask_salaire, 'Salaire'] = np.nan
df_propre.loc[mask_experience, 'Experience'] = np.nan
df_propre.loc[mask_score, 'Score'] = np.nan

df = df_propre.copy()

# ---- Détecter les valeurs manquantes ----
print("=== ANALYSE DES VALEURS MANQUANTES ===")
manquants = df.isnull().sum()
pct_manquants = df.isnull().mean() * 100

rapport_manquants = pd.DataFrame({
    'Nb_manquants': manquants,
    'Pct_manquants': pct_manquants.round(1),
    'Nb_presents': df.notna().sum()
}).sort_values('Nb_manquants', ascending=False)

print(rapport_manquants)

# Visualiser les manquants
import missingno  # pip install missingno (optionnel)
# missingno.matrix(df)

# ---- Stratégies de traitement ----

# 1. SUPPRIMER (dropna)
"""
QUAND ? Peu de données manquantes (< 5%), MCAR
"""
df_complet = df.dropna()                # Supprimer toute ligne avec NaN
df_complet_col = df.dropna(axis=1)     # Supprimer colonnes avec NaN
df_seuil = df.dropna(thresh=4)        # Garder si au moins 4 non-NaN
print(f"\nApres dropna: {df_complet.shape} (était {df.shape})")

# 2. IMPUTER (fillna)
df_impute = df.copy()

# Imputation simple
df_impute['Age'] = df_impute['Age'].fillna(df_impute['Age'].median())
df_impute['Salaire'] = df_impute['Salaire'].fillna(df_impute['Salaire'].mean())
df_impute['Experience'] = df_impute['Experience'].fillna(0)  # Constante
df_impute['Ville'] = df_impute['Ville'].fillna('Inconnue')  # Catégorie spéciale
df_impute['Score'] = df_impute['Score'].fillna(df_impute['Score'].median())

# Imputation par propagation
# ffill : Prendre valeur précédente
# bfill : Prendre valeur suivante
df_time = pd.DataFrame({'val': [1, np.nan, np.nan, 4, 5]})
print("\nPropagation:")
print(df_time['val'].ffill())    # [1, 1, 1, 4, 5]

# 3. IMPUTATION STATISTIQUE AVANCÉE (KNN, Iterative)
from sklearn.impute import KNNImputer, SimpleImputer

# SimpleImputer
imputer = SimpleImputer(strategy='median')
cols_numeriques = ['Age', 'Salaire', 'Experience', 'Score']
df_sklearn = df.copy()
df_sklearn[cols_numeriques] = imputer.fit_transform(df[cols_numeriques])

# KNN Imputer (utilise valeurs voisines)
knn_imputer = KNNImputer(n_neighbors=5)
df_knn = df.copy()
df_knn[cols_numeriques] = knn_imputer.fit_transform(df[cols_numeriques])

print("\nComparaison des imputations :")
print(f"SimpleImputer: pas de NaN = {df_sklearn[cols_numeriques].isnull().sum().sum() == 0}")

# ============================================================================
# [GUIDE] CHAPITRE 6.2 : DOUBLONS
# ============================================================================

# Créer données avec doublons
df_doublons = pd.DataFrame({
    'Nom': ['Alice', 'Bob', 'Alice', 'Charlie', 'Bob', 'Diana'],
    'Email': ['a@t.com', 'b@t.com', 'a@t.com', 'c@t.com', 'b@test.com', 'd@t.com'],
    'Score': [85, 92, 85, 78, 88, 95]
})

# Détecter doublons
print("\n=== DOUBLONS ===")
print(f"Doublons exacts : {df_doublons.duplicated().sum()}")
print(f"Doublons nom    : {df_doublons.duplicated('Nom').sum()}")

# Afficher les doublons
print(df_doublons[df_doublons.duplicated(keep=False)])

# Supprimer doublons
df_sans_doublons = df_doublons.drop_duplicates()
df_sans_doublons_nom = df_doublons.drop_duplicates(subset='Nom', keep='first')

# ============================================================================
# [GUIDE] CHAPITRE 6.3 : OUTLIERS (VALEURS ABERRANTES)
# ============================================================================

"""
MÉTHODES DE DÉTECTION :
1. Z-score   : Valeur > 3 écarts-types de la moyenne
2. IQR       : Valeur hors [Q1 - 1.5×IQR, Q3 + 1.5×IQR]
3. Isolation Forest (ML)
"""

salaires = pd.Series([35000, 40000, 42000, 45000, 47000, 48000, 50000,
                       51000, 53000, 55000, 60000, 65000, 200000, 5000])

# ---- Méthode Z-score ----
z_scores = np.abs(stats.zscore(salaires))
outliers_z = salaires[z_scores > 3]
print(f"\nOutliers Z-score (>3): {outliers_z.values}")

# ---- Méthode IQR (Tukey) ----
q1 = salaires.quantile(0.25)
q3 = salaires.quantile(0.75)
iqr = q3 - q1
borne_basse = q1 - 1.5 * iqr
borne_haute = q3 + 1.5 * iqr
outliers_iqr = salaires[(salaires < borne_basse) | (salaires > borne_haute)]
print(f"Outliers IQR: {outliers_iqr.values}")

# ---- Traitement des outliers ----
# Option 1 : Supprimer
salaires_clean = salaires[(salaires >= borne_basse) & (salaires <= borne_haute)]

# Option 2 : Winsoriser (caper aux bornes)
salaires_wins = salaires.clip(lower=borne_basse, upper=borne_haute)

# Option 3 : Transformer (log)
salaires_log = np.log1p(salaires)  # log(1+x)

print(f"\nSans outliers: mean={salaires_clean.mean():,.0f}€")
print(f"Winsorisé:     mean={salaires_wins.mean():,.0f}€")

# ============================================================================
# [GUIDE] CHAPITRE 6.4 : NORMALISATION ET STANDARDISATION
# ============================================================================

"""
POURQUOI ?
Certains algorithmes ML sont sensibles à l'échelle des données
Ex: Régression Ridge, SVM, KNN, PCA

MÉTHODES :
1. Standardisation (Z-score) : µ=0, σ=1
2. Normalisation Min-Max     : Valeurs entre [0, 1]
3. Normalisation Max-Abs     : Valeurs entre [-1, 1]
4. Robuste                   : Utilise médiane et IQR
"""

from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler

data_original = pd.DataFrame({
    'Salaire': [25000, 45000, 65000, 100000, 200000],
    'Age': [22, 30, 38, 45, 55],
    'Exp': [0, 5, 12, 20, 32]
})

# StandardScaler : (x - µ) / σ
scaler_std = StandardScaler()
data_std = pd.DataFrame(
    scaler_std.fit_transform(data_original),
    columns=data_original.columns
)

# MinMaxScaler : (x - min) / (max - min)
scaler_mm = MinMaxScaler()
data_mm = pd.DataFrame(
    scaler_mm.fit_transform(data_original),
    columns=data_original.columns
)

# RobustScaler : (x - médiane) / IQR -> robuste aux outliers
scaler_rob = RobustScaler()
data_rob = pd.DataFrame(
    scaler_rob.fit_transform(data_original),
    columns=data_original.columns
)

print("\nOriginal:\n", data_original)
print("\nStandardisé (µ=0, σ=1):\n", data_std.round(2))
print("\nNormalisé [0,1]:\n", data_mm.round(2))

# ============================================================================
# [COURS] EXERCICE PRATIQUE 6 : PIPELINE DE NETTOYAGE COMPLET
# ============================================================================

"""
SCÉNARIO : Nettoyer un dataset de clients d'une banque
"""

# Données sales
donnees_clients_sales = {
    'ID': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 3],  # Doublon: 3
    'Nom': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 
             'Frank', 'Grace', None, 'Henry', 'Iris', 'Charlie'],
    'Age': [25, 35, np.nan, 28, 45, -5, 200, 30, 22, 38, np.nan],  # -5 et 200 aberrants
    'Revenu': [30000, 50000, 45000, np.nan, 85000, 
                32000, 48000, 60000, 25000, 95000, 45000],
    'Score_credit': ['750', '820', '680', '710', '900',
                     '630', '770', 'N/A', '695', '855', '680'],  # Mal typé
    'Ville': ['paris', 'LYON', 'marseille', 'Paris', 'lyon',
               'MARSEILLE', 'paris', 'bordeaux', 'Paris', 'Lyon', 'marseille'],
    'Defaut': [0, 0, 1, 0, 0, 1, 0, np.nan, 0, 0, 1]
}
df_sale = pd.DataFrame(donnees_clients_sales)

print("=== DONNÉES SALES ===")
print(df_sale)
print(f"\nShape : {df_sale.shape}")

# ---- CORRIGÉ : Pipeline de nettoyage ----
def nettoyer_clients(df):
    """Pipeline complet de nettoyage"""
    df = df.copy()
    
    # 1. Supprimer doublons
    n_avant = len(df)
    df = df.drop_duplicates(subset='ID')
    print(f"1. Doublons supprimés: {n_avant - len(df)}")
    
    # 2. Corriger les noms
    df['Nom'] = df['Nom'].fillna('Inconnu').str.title()
    
    # 3. Corriger les villes (normaliser casse)
    df['Ville'] = df['Ville'].str.title()
    
    # 4. Convertir Score_credit (string -> numérique)
    df['Score_credit'] = pd.to_numeric(df['Score_credit'], errors='coerce')
    
    # 5. Traiter les âges aberrants
    df['Age'] = pd.to_numeric(df['Age'], errors='coerce')
    df.loc[(df['Age'] < 18) | (df['Age'] > 100), 'Age'] = np.nan
    
    # 6. Imputer valeurs manquantes numériques
    for col in ['Age', 'Revenu', 'Score_credit']:
        df[col] = df[col].fillna(df[col].median())
    
    # 7. Imputer Defaut (variable cible)
    df['Defaut'] = df['Defaut'].fillna(0).astype(int)
    
    # 8. Rapport final
    print(f"2. Valeurs manquantes restantes: {df.isnull().sum().sum()}")
    print(f"3. Shape finale: {df.shape}")
    
    return df

df_propre = nettoyer_clients(df_sale)
print("\n=== DONNÉES PROPRES ===")
print(df_propre)


# ============================================================================
# [RECHERCHE] PARTIE 7 : ANALYSE EXPLORATOIRE (EDA)
# ============================================================================

"""
EDA = Exploratory Data Analysis
OBJECTIF : Comprendre vos données AVANT de modéliser

ÉTAPES EDA :
1. Aperçu général (shape, types, manquants)
2. Distribution de chaque variable
3. Relations entre variables
4. Patterns temporels (si dates)
5. Hypothèses et insights

CITATION : "Les graphiques révèlent ce que les statistiques cachent"
"""

# Dataset : Iris (classique)
from sklearn.datasets import load_iris, load_boston

iris = load_iris()
df_iris = pd.DataFrame(iris.data, columns=iris.feature_names)
df_iris['espece'] = pd.Categorical.from_codes(iris.target, iris.target_names)

print("=== EDA DU DATASET IRIS ===")
print(df_iris.head())
print(f"\nShape: {df_iris.shape}")
print(f"Espèces: {df_iris['espece'].value_counts().to_dict()}")

# ---- 1. Statistiques par groupe ----
print("\n--- Statistiques par Espèce ---")
print(df_iris.groupby('espece').agg({
    'sepal length (cm)': ['mean', 'std'],
    'petal length (cm)': ['mean', 'std']
}).round(2))

# ---- 2. Matrice de corrélation ----
correlation = df_iris.drop('espece', axis=1).corr()
fig, ax = plt.subplots(figsize=(8, 6))
sns.heatmap(correlation, annot=True, fmt='.2f', cmap='coolwarm', 
            center=0, square=True)
ax.set_title('Matrice de Corrélation - Iris')
plt.tight_layout()
plt.savefig('eda_heatmap.png', dpi=150, bbox_inches='tight')
plt.show()

# ---- 3. Distributions ----
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
for i, col in enumerate(iris.feature_names):
    ax = axes[i//2, i%2]
    for espece in df_iris['espece'].unique():
        data = df_iris[df_iris['espece']==espece][col]
        ax.hist(data, alpha=0.6, label=espece, bins=15)
    ax.set_title(f'Distribution : {col}')
    ax.legend()
    ax.set_xlabel(col)
plt.suptitle('EDA - Distributions par Espèce', fontsize=14)
plt.tight_layout()
plt.savefig('eda_distributions.png', dpi=150, bbox_inches='tight')
plt.show()

# ---- 4. Pairplot complet ----
g = sns.pairplot(df_iris, hue='espece', diag_kind='kde')
g.fig.suptitle('Pairplot - Dataset Iris', y=1.02)
plt.savefig('eda_pairplot.png', dpi=150, bbox_inches='tight')
plt.show()


# ============================================================================
# [LIEN] PARTIE 8 : CORRÉLATIONS ET RELATIONS
# ============================================================================

"""
CORRÉLATION = Mesure de la relation linéaire entre deux variables
Valeur entre -1 et +1

+1  -> Corrélation positive parfaite
 0  -> Pas de corrélation linéaire
-1  -> Corrélation négative parfaite

ATTENTION : Corrélation ≠ Causalité !
"""

# Générer données corrélées
np.random.seed(42)
n = 100
x = np.random.normal(0, 1, n)
y_forte_pos = x * 0.9 + np.random.normal(0, 0.3, n)   # Forte positive
y_faible_pos = x * 0.3 + np.random.normal(0, 0.8, n)  # Faible positive
y_negative = -x * 0.8 + np.random.normal(0, 0.4, n)   # Négative
y_nulle = np.random.normal(0, 1, n)                     # Nulle

# ---- Coefficient de Pearson ----
"""
QUAND : Relations linéaires, données continues normales
FORMULE : Cov(X,Y) / (σX × σY)
"""
r_forte, p_forte = stats.pearsonr(x, y_forte_pos)
r_faible, p_faible = stats.pearsonr(x, y_faible_pos)
r_neg, p_neg = stats.pearsonr(x, y_negative)
r_nulle, p_nulle = stats.pearsonr(x, y_nulle)

print("=== CORRÉLATIONS DE PEARSON ===")
print(f"Forte positive : r={r_forte:.2f}, p={p_forte:.4f}")
print(f"Faible positive: r={r_faible:.2f}, p={p_faible:.4f}")
print(f"Négative       : r={r_neg:.2f}, p={p_neg:.4f}")
print(f"Nulle          : r={r_nulle:.2f}, p={p_nulle:.4f}")

# ---- Coefficient de Spearman ----
"""
QUAND : Relations monotones, données ordinales, outliers présents
AVANTAGE : Plus robuste que Pearson
"""
r_spearman, p_spearman = stats.spearmanr(x, y_forte_pos)
print(f"\nSpearman (forte pos): r={r_spearman:.2f}")

# ---- Visualisation ----
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

donnees = [
    (x, y_forte_pos, f'Pearson r={r_forte:.2f}', 'Forte Positive'),
    (x, y_faible_pos, f'Pearson r={r_faible:.2f}', 'Faible Positive'),
    (x, y_negative, f'Pearson r={r_neg:.2f}', 'Négative'),
    (x, y_nulle, f'Pearson r={r_nulle:.2f}', 'Nulle'),
]

for ax, (xi, yi, r_label, titre) in zip(axes.flat, donnees):
    ax.scatter(xi, yi, alpha=0.5, color='steelblue', s=30)
    # Droite de régression
    z = np.polyfit(xi, yi, 1)
    p = np.poly1d(z)
    x_line = np.linspace(xi.min(), xi.max(), 100)
    ax.plot(x_line, p(x_line), 'r-', linewidth=2)
    ax.set_title(f'{titre}\n{r_label}')
    ax.set_xlabel('X')
    ax.set_ylabel('Y')

plt.suptitle('Types de Corrélations', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig('correlations.png', dpi=150, bbox_inches='tight')
plt.show()


# ============================================================================
# [BOT] PARTIE 9 : INTRODUCTION AU MACHINE LEARNING
# ============================================================================

"""
[OBJECTIF] OBJECTIFS PARTIE 9 :
- Comprendre les concepts ML de base
- Régression linéaire
- Classification simple
- Validation croisée
- Évaluation des modèles

ML = Algorithmes qui apprennent des patterns dans les données
"""

from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import (mean_squared_error, r2_score, 
                              accuracy_score, classification_report,
                              confusion_matrix)

# ============================================================================
# [GUIDE] CHAPITRE 9.1 : RÉGRESSION LINÉAIRE
# ============================================================================

"""
RÉGRESSION = Prédire une valeur NUMÉRIQUE continue

EXEMPLE : Prédire le salaire selon l'expérience
"""

# Données
np.random.seed(42)
n = 200
experience = np.random.uniform(0, 30, n)
salaire = 25000 + 2500 * experience + np.random.normal(0, 5000, n)

df_reg = pd.DataFrame({'Experience': experience, 'Salaire': salaire})

# ---- 1. Préparer les données ----
X = df_reg[['Experience']]  # Features (variables explicatives)
y = df_reg['Salaire']       # Target (variable à prédire)

# Diviser en train et test
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)
print(f"Train: {X_train.shape}, Test: {X_test.shape}")

# ---- 2. Entraîner le modèle ----
modele = LinearRegression()
modele.fit(X_train, y_train)  # <- L'algorithme "apprend"

print(f"\nModèle appris :")
print(f"Pente (β₁)    : {modele.coef_[0]:,.0f} €/an d'expérience")
print(f"Intercept (β₀): {modele.intercept_:,.0f} €")
print(f"Équation      : Salaire = {modele.intercept_:,.0f} + {modele.coef_[0]:,.0f} × Expérience")

# ---- 3. Prédire ----
y_pred = modele.predict(X_test)

# Prédire pour de nouvelles valeurs
nouvelle_experience = pd.DataFrame({'Experience': [5, 10, 15, 20]})
predictions = modele.predict(nouvelle_experience)
for exp, sal in zip([5, 10, 15, 20], predictions):
    print(f"  {exp} ans -> {sal:,.0f} €")

# ---- 4. Évaluer ----
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
r2 = r2_score(y_test, y_pred)

print(f"\nÉvaluation du modèle :")
print(f"RMSE : {rmse:,.0f} € (erreur typique)")
print(f"R²   : {r2:.3f} ({r2*100:.1f}% de variance expliquée)")

# ---- 5. Visualiser ----
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Données + droite de régression
axes[0].scatter(X_train, y_train, alpha=0.4, label='Données train', s=20)
axes[0].scatter(X_test, y_test, alpha=0.6, label='Données test', s=30, color='orange')
x_line = np.linspace(0, 30, 100).reshape(-1, 1)
axes[0].plot(x_line, modele.predict(x_line), 'r-', linewidth=2, label='Régression')
axes[0].set_title(f'Régression Linéaire (R²={r2:.2f})')
axes[0].set_xlabel('Expérience (années)')
axes[0].set_ylabel('Salaire (€)')
axes[0].legend()

# Valeurs prédites vs réelles
axes[1].scatter(y_test, y_pred, alpha=0.6, color='purple', s=30)
axes[1].plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 
              'r--', linewidth=2, label='Parfait')
axes[1].set_title('Prédits vs Réels')
axes[1].set_xlabel('Valeurs Réelles (€)')
axes[1].set_ylabel('Valeurs Prédites (€)')
axes[1].legend()

plt.tight_layout()
plt.savefig('ml_regression.png', dpi=150, bbox_inches='tight')
plt.show()

# ============================================================================
# [GUIDE] CHAPITRE 9.2 : CLASSIFICATION
# ============================================================================

"""
CLASSIFICATION = Prédire une CATÉGORIE (classe)

EXEMPLE : Prédire si un client fera défaut (Oui/Non)
"""

# Dataset Iris pour classification
X_iris = df_iris.drop('espece', axis=1)
y_iris = df_iris['espece']

# Diviser
X_train_i, X_test_i, y_train_i, y_test_i = train_test_split(
    X_iris, y_iris, test_size=0.3, random_state=42, stratify=y_iris
)

# Standardiser (important pour classification !)
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train_i)
X_test_s = scaler.transform(X_test_i)

# Régression Logistique
clf = LogisticRegression(max_iter=1000)
clf.fit(X_train_s, y_train_i)

y_pred_i = clf.predict(X_test_s)

# Évaluation
print("=== CLASSIFICATION - DATASET IRIS ===")
print(f"Accuracy : {accuracy_score(y_test_i, y_pred_i):.3f}")
print("\nRapport de classification :")
print(classification_report(y_test_i, y_pred_i))

# Matrice de confusion
cm = confusion_matrix(y_test_i, y_pred_i)
fig, ax = plt.subplots(figsize=(7, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=clf.classes_, yticklabels=clf.classes_)
ax.set_title('Matrice de Confusion')
ax.set_ylabel('Vraie Classe')
ax.set_xlabel('Classe Prédite')
plt.tight_layout()
plt.savefig('ml_confusion_matrix.png', dpi=150, bbox_inches='tight')
plt.show()

# ============================================================================
# [GUIDE] CHAPITRE 9.3 : VALIDATION CROISÉE
# ============================================================================

"""
[IDEE] VALIDATION CROISÉE = Évaluation plus robuste

PROBLÈME : Une seule division train/test peut être biaisée
SOLUTION : K-Fold Cross Validation

Diviser en K parties -> Entraîner K fois -> Moyenne des scores
"""

from sklearn.model_selection import cross_val_score, KFold

# K-Fold CV
kf = KFold(n_splits=5, shuffle=True, random_state=42)
scores_cv = cross_val_score(clf, 
                             scaler.fit_transform(X_iris), 
                             y_iris, 
                             cv=5, 
                             scoring='accuracy')

print(f"\n=== VALIDATION CROISÉE (5-Fold) ===")
print(f"Scores : {np.round(scores_cv, 3)}")
print(f"Moyenne : {scores_cv.mean():.3f} ± {scores_cv.std():.3f}")


# ============================================================================
# [COURS] EXERCICE PRATIQUE FINAL : PROJET D'ANALYSE COMPLET
# ============================================================================

"""
PROJET : Analyse complète d'un dataset de logements
Objectif : Prédire le prix d'un logement

ÉTAPES :
1. Chargement et exploration
2. Nettoyage
3. EDA + Visualisation
4. Modélisation
5. Évaluation
"""

from sklearn.datasets import fetch_california_housing
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline

# 1. Charger données
housing = fetch_california_housing()
df_h = pd.DataFrame(housing.data, columns=housing.feature_names)
df_h['Prix'] = housing.target * 100000  # En $

print("=" * 60)
print("     CORRIGÉ - PROJET FINAL")
print("=" * 60)

# 2. Exploration
print(f"\nShape: {df_h.shape}")
print(df_h.describe().round(2))

# 3. EDA
fig, axes = plt.subplots(2, 4, figsize=(18, 8))

for i, col in enumerate(housing.feature_names[:4]):
    axes[0, i].hist(df_h[col], bins=30, color=f'C{i}', edgecolor='white')
    axes[0, i].set_title(col, fontsize=9)

for i, col in enumerate(housing.feature_names[4:8]):
    axes[1, i].scatter(df_h[col], df_h['Prix']/100000, 
                        alpha=0.1, s=5, color=f'C{i+4}')
    axes[1, i].set_xlabel(col, fontsize=8)
    axes[1, i].set_ylabel('Prix (100k$)')

plt.suptitle("EDA - California Housing", fontsize=14)
plt.tight_layout()
plt.savefig('projet_eda.png', dpi=150, bbox_inches='tight')
plt.show()

# 4. Corrélation avec le prix
correlations_prix = df_h.corr()['Prix'].drop('Prix').sort_values()
print("\n--- Corrélations avec le Prix ---")
print(correlations_prix.round(2))

# 5. Modélisation
X = df_h[housing.feature_names]
y = df_h['Prix']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Pipeline : Standardisation + Régression
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('regressor', LinearRegression())
])

pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)

# 6. Évaluation
r2 = r2_score(y_test, y_pred)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))

print(f"\n=== RÉSULTATS DU MODÈLE ===")
print(f"R²   : {r2:.3f}")
print(f"RMSE : ${rmse:,.0f}")

# Graphique final
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

axes[0].scatter(y_test/100000, y_pred/100000, alpha=0.1, s=5, color='steelblue')
m = min(y_test.min(), y_pred.min()) / 100000
M = max(y_test.max(), y_pred.max()) / 100000
axes[0].plot([m, M], [m, M], 'r--', linewidth=2)
axes[0].set_title(f'Prédictions vs Réels\nR²={r2:.3f}')
axes[0].set_xlabel('Vrai Prix (100k$)')
axes[0].set_ylabel('Prix Prédit (100k$)')

residus = y_test - y_pred
axes[1].hist(residus/100000, bins=50, color='orange', edgecolor='white')
axes[1].axvline(0, color='red', linewidth=2)
axes[1].set_title('Distribution des Résidus')
axes[1].set_xlabel('Résidu (100k$)')

plt.tight_layout()
plt.savefig('projet_resultats.png', dpi=150, bbox_inches='tight')
plt.show()

print("\n[BRAVO] FÉLICITATIONS ! Vous avez complété le guide complet !")
print("""
COMPÉTENCES ACQUISES :
[OK] Python de base pour la data
[OK] NumPy : calculs numériques vectorisés
[OK] Pandas : manipulation de données
[OK] Matplotlib + Seaborn : visualisation
[OK] Statistiques descriptives
[OK] Nettoyage de données
[OK] Analyse exploratoire (EDA)
[OK] Corrélations
[OK] Machine Learning de base

PROCHAINES ÉTAPES :
-> Pratiquer avec des datasets réels (Kaggle.com)
-> Approfondir Scikit-learn (classification, clustering)
-> Séries temporelles (Prophet, statsmodels)
-> Deep Learning (TensorFlow, PyTorch)
""")