================================================================================
                    PRÉREQUIS PYTHON POUR LA DATA SCIENCE
                     CHEAT SHEET ULTRA-COMPLET POUR DÉBUTANTS
================================================================================

[IMPORTANT] OBJECTIF: Maîtriser les fondamentaux Python nécessaires avant d'aborder
             l'analyse de données, le machine learning et la data science.

================================================================================
                         1. SYNTAXE DE BASE PYTHON
================================================================================

1.1 VARIABLES ET TYPES DE DONNÉES
----------------------------------

# Déclaration de variables (pas besoin de type explicite)
nom = "Alice"              # String (chaîne de caractères)
age = 25                   # Integer (entier)
taille = 1.65              # Float (nombre à virgule)
est_etudiant = True        # Boolean (True ou False)

# Vérifier le type d'une variable
type(age)                  # Retourne: <class 'int'>

# Conversion de types (casting)
nombre_str = "123"
nombre_int = int(nombre_str)      # "123" -> 123
nombre_float = float(nombre_str)  # "123" -> 123.0
texte = str(456)                  # 456 -> "456"


1.2 OPÉRATEURS ARITHMÉTIQUES
-----------------------------

a = 10
b = 3

addition = a + b           # 13
soustraction = a - b       # 7
multiplication = a * b     # 30
division = a / b           # 3.333... (toujours un float)
division_entiere = a // b  # 3 (partie entière)
modulo = a % b            # 1 (reste de la division)
puissance = a ** b        # 1000 (10³)


1.3 OPÉRATEURS DE COMPARAISON
------------------------------

x = 5
y = 10

x == y    # False (égal à)
x != y    # True (différent de)
x < y     # True (inférieur à)
x > y     # False (supérieur à)
x <= y    # True (inférieur ou égal)
x >= y    # False (supérieur ou égal)


1.4 OPÉRATEURS LOGIQUES
------------------------

a = True
b = False

a and b   # False (ET logique - les deux doivent être vrais)
a or b    # True (OU logique - au moins un doit être vrai)
not a     # False (NON logique - inverse)


================================================================================
                         2. STRUCTURES DE DONNÉES
================================================================================

2.1 LISTES (List) - Collection ordonnée et modifiable
------------------------------------------------------

# Création
ma_liste = [1, 2, 3, 4, 5]
liste_mixte = [1, "deux", 3.0, True]

# Accès aux éléments (index commence à 0)
premier = ma_liste[0]      # 1
dernier = ma_liste[-1]     # 5
portion = ma_liste[1:4]    # [2, 3, 4] (slicing)

# Modification
ma_liste[0] = 10           # [10, 2, 3, 4, 5]
ma_liste.append(6)         # Ajoute 6 à la fin
ma_liste.insert(1, 15)     # Insère 15 à l'index 1
ma_liste.remove(3)         # Supprime la première occurrence de 3
element = ma_liste.pop()   # Retire et retourne le dernier élément
ma_liste.extend([7, 8])    # Ajoute plusieurs éléments

# Opérations utiles
longueur = len(ma_liste)   # Nombre d'éléments
ma_liste.sort()            # Trie la liste
ma_liste.reverse()         # Inverse l'ordre
ma_liste.clear()           # Vide la liste


2.2 TUPLES - Collection ordonnée et IMMUTABLE (non modifiable)
---------------------------------------------------------------

# Création
mon_tuple = (1, 2, 3)
tuple_single = (5,)        # Virgule obligatoire pour un seul élément

# Accès (comme les listes)
premier = mon_tuple[0]     # 1

# IMPOSSIBLE de modifier
# mon_tuple[0] = 10        # [X] TypeError!

# Unpacking (déballage)
a, b, c = mon_tuple        # a=1, b=2, c=3


2.3 DICTIONNAIRES (Dict) - Paires clé-valeur
---------------------------------------------

# Création
personne = {
    "nom": "Alice",
    "age": 25,
    "ville": "Paris"
}

# Accès
nom = personne["nom"]              # "Alice"
nom = personne.get("nom")          # "Alice" (méthode plus sûre)
nom = personne.get("prenom", "?")  # "?" si la clé n'existe pas

# Modification
personne["age"] = 26               # Modifie la valeur
personne["email"] = "a@mail.com"   # Ajoute une nouvelle clé
del personne["ville"]              # Supprime une clé

# Parcourir un dictionnaire
for cle, valeur in personne.items():
    print(f"{cle}: {valeur}")

# Méthodes utiles
cles = personne.keys()             # Liste des clés
valeurs = personne.values()        # Liste des valeurs
personne.update({"age": 27})       # Met à jour


2.4 SETS - Collection non ordonnée sans doublons
-------------------------------------------------

# Création
mon_set = {1, 2, 3, 4, 5}
mon_set = set([1, 2, 2, 3])  # {1, 2, 3} - les doublons sont éliminés

# Opérations
mon_set.add(6)                # Ajoute un élément
mon_set.remove(3)             # Supprime (erreur si absent)
mon_set.discard(3)            # Supprime (pas d'erreur si absent)

# Opérations ensemblistes
a = {1, 2, 3}
b = {3, 4, 5}
union = a | b                 # {1, 2, 3, 4, 5}
intersection = a & b          # {3}
difference = a - b            # {1, 2}


================================================================================
                         3. STRUCTURES DE CONTRÔLE
================================================================================

3.1 CONDITIONS (if, elif, else)
--------------------------------

age = 18

if age < 18:
    print("Mineur")
elif age == 18:
    print("Tout juste majeur")
else:
    print("Majeur")

# Condition ternaire (sur une ligne)
statut = "Majeur" if age >= 18 else "Mineur"


3.2 BOUCLES FOR
---------------

# Boucle sur une liste
fruits = ["pomme", "banane", "orange"]
for fruit in fruits:
    print(fruit)

# Boucle avec range()
for i in range(5):         # 0, 1, 2, 3, 4
    print(i)

for i in range(2, 10):     # 2, 3, 4, 5, 6, 7, 8, 9
    print(i)

for i in range(0, 10, 2):  # 0, 2, 4, 6, 8 (pas de 2)
    print(i)

# Boucle avec index et valeur
for index, fruit in enumerate(fruits):
    print(f"{index}: {fruit}")

# Parcourir un dictionnaire
personne = {"nom": "Alice", "age": 25}
for cle, valeur in personne.items():
    print(f"{cle} = {valeur}")


3.3 BOUCLES WHILE
-----------------

compteur = 0
while compteur < 5:
    print(compteur)
    compteur += 1

# Boucle infinie avec condition de sortie
while True:
    reponse = input("Continuer? (o/n): ")
    if reponse == 'n':
        break  # Sort de la boucle


3.4 CONTRÔLE DE FLUX (break, continue, pass)
---------------------------------------------

# break: Sort complètement de la boucle
for i in range(10):
    if i == 5:
        break
    print(i)  # Affiche 0, 1, 2, 3, 4

# continue: Passe à l'itération suivante
for i in range(5):
    if i == 2:
        continue
    print(i)  # Affiche 0, 1, 3, 4

# pass: Ne fait rien (placeholder)
for i in range(5):
    if i == 2:
        pass  # À compléter plus tard
    print(i)


================================================================================
                         4. FONCTIONS
================================================================================

4.1 DÉFINITION ET APPEL DE FONCTION
------------------------------------

# Fonction simple
def saluer():
    print("Bonjour!")

saluer()  # Appel de la fonction

# Fonction avec paramètres
def saluer_personne(nom):
    print(f"Bonjour {nom}!")

saluer_personne("Alice")

# Fonction avec valeur de retour
def addition(a, b):
    return a + b

resultat = addition(5, 3)  # 8

# Fonction avec paramètres par défaut
def saluer_avec_titre(nom, titre="M."):
    return f"Bonjour {titre} {nom}"

print(saluer_avec_titre("Dupont"))           # "Bonjour M. Dupont"
print(saluer_avec_titre("Martin", "Mme"))    # "Bonjour Mme Martin"


4.2 ARGUMENTS VARIABLES
-----------------------

# *args: nombre variable d'arguments positionnels
def somme(*nombres):
    total = 0
    for nombre in nombres:
        total += nombre
    return total

print(somme(1, 2, 3))        # 6
print(somme(1, 2, 3, 4, 5))  # 15

# **kwargs: nombre variable d'arguments nommés
def afficher_infos(**infos):
    for cle, valeur in infos.items():
        print(f"{cle}: {valeur}")

afficher_infos(nom="Alice", age=25, ville="Paris")


4.3 FONCTIONS LAMBDA (ANONYMES)
-------------------------------

# Fonction lambda (fonction courte sur une ligne)
carre = lambda x: x ** 2
print(carre(5))  # 25

# Très utilisées avec map, filter, sorted
nombres = [1, 2, 3, 4, 5]
carres = list(map(lambda x: x**2, nombres))  # [1, 4, 9, 16, 25]


================================================================================
                         5. COMPRÉHENSIONS DE LISTE
================================================================================

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

# Créer une liste de carrés
nombres = [1, 2, 3, 4, 5]
carres = [x**2 for x in nombres]  # [1, 4, 9, 16, 25]

# Avec condition
pairs = [x for x in nombres if x % 2 == 0]  # [2, 4]

# Compréhension de dictionnaire
nombres = [1, 2, 3, 4, 5]
carre_dict = {x: x**2 for x in nombres}  # {1:1, 2:4, 3:9, 4:16, 5:25}

# Compréhension de set
lettres = [c for c in "bonjour"]
set_lettres = {c for c in "bonjour"}  # {'b', 'o', 'n', 'j', 'u', 'r'}


================================================================================
                         6. GESTION DES FICHIERS
================================================================================

6.1 LECTURE DE FICHIERS
------------------------

# Méthode 1: Avec fermeture manuelle
fichier = open("data.txt", "r")  # "r" = read (lecture)
contenu = fichier.read()
fichier.close()

# Méthode 2: Avec context manager (RECOMMANDÉ)
with open("data.txt", "r") as fichier:
    contenu = fichier.read()
# Le fichier est automatiquement fermé

# Lire ligne par ligne
with open("data.txt", "r") as fichier:
    for ligne in fichier:
        print(ligne.strip())  # strip() enlève les espaces/sauts de ligne

# Lire toutes les lignes dans une liste
with open("data.txt", "r") as fichier:
    lignes = fichier.readlines()


6.2 ÉCRITURE DE FICHIERS
-------------------------

# Mode "w" (write): Écrase le fichier s'il existe
with open("output.txt", "w") as fichier:
    fichier.write("Première ligne\n")
    fichier.write("Deuxième ligne\n")

# Mode "a" (append): Ajoute à la fin du fichier
with open("output.txt", "a") as fichier:
    fichier.write("Ligne ajoutée\n")

# Écrire plusieurs lignes
lignes = ["ligne 1\n", "ligne 2\n", "ligne 3\n"]
with open("output.txt", "w") as fichier:
    fichier.writelines(lignes)


================================================================================
                         7. GESTION DES ERREURS
================================================================================

7.1 TRY-EXCEPT
--------------

# Gérer les erreurs possibles
try:
    resultat = 10 / 0
except ZeroDivisionError:
    print("Erreur: Division par zéro!")

# Plusieurs types d'erreurs
try:
    nombre = int("abc")
except ValueError:
    print("Erreur: Conversion impossible")
except Exception as e:
    print(f"Autre erreur: {e}")

# Avec finally (toujours exécuté)
try:
    fichier = open("data.txt", "r")
    contenu = fichier.read()
except FileNotFoundError:
    print("Fichier non trouvé")
finally:
    fichier.close()  # Toujours exécuté


================================================================================
                         8. MODULES ET IMPORTS
================================================================================

8.1 IMPORTER DES MODULES
------------------------

# Import complet
import math
resultat = math.sqrt(16)  # 4.0

# Import spécifique
from math import sqrt, pi
resultat = sqrt(16)       # 4.0
print(pi)                 # 3.14159...

# Import avec alias
import numpy as np
import pandas as pd

# Import tout (À ÉVITER)
from math import *


8.2 MODULES STANDARDS UTILES POUR LA DATA SCIENCE
--------------------------------------------------

import math        # Fonctions mathématiques
import random      # Génération de nombres aléatoires
import statistics  # Statistiques de base
import datetime    # Manipulation de dates
import os          # Interaction avec le système d'exploitation
import json        # Manipulation de fichiers JSON
import csv         # Manipulation de fichiers CSV


================================================================================
                         9. FONCTIONS UTILES BUILT-IN
================================================================================

# len(): Longueur d'une séquence
len([1, 2, 3])        # 3
len("bonjour")        # 7

# sum(): Somme d'une séquence numérique
sum([1, 2, 3, 4, 5])  # 15

# min() et max(): Minimum et maximum
min([3, 1, 4, 1, 5])  # 1
max([3, 1, 4, 1, 5])  # 5

# sorted(): Trier une séquence (retourne une nouvelle liste)
sorted([3, 1, 4, 1, 5])        # [1, 1, 3, 4, 5]
sorted([3, 1, 4], reverse=True) # [4, 3, 1]

# enumerate(): Index + valeur
for i, valeur in enumerate(['a', 'b', 'c']):
    print(f"{i}: {valeur}")

# zip(): Combine plusieurs itérables
noms = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
for nom, age in zip(noms, ages):
    print(f"{nom} a {age} ans")

# map(): Applique une fonction à chaque élément
nombres = [1, 2, 3, 4, 5]
carres = list(map(lambda x: x**2, nombres))  # [1, 4, 9, 16, 25]

# filter(): Filtre les éléments
nombres = [1, 2, 3, 4, 5, 6]
pairs = list(filter(lambda x: x % 2 == 0, nombres))  # [2, 4, 6]

# all(): Vérifie si tous les éléments sont True
all([True, True, True])   # True
all([True, False, True])  # False

# any(): Vérifie si au moins un élément est True
any([False, False, True]) # True
any([False, False])       # False


================================================================================
                         10. STRINGS (CHAÎNES DE CARACTÈRES)
================================================================================

10.1 OPÉRATIONS DE BASE
------------------------

texte = "Data Science"

# Longueur
len(texte)           # 12

# Accès aux caractères
texte[0]             # 'D'
texte[-1]            # 'e' (dernier caractère)
texte[0:4]           # 'Data' (slicing)

# Concaténation
prenom = "Alice"
nom = "Dupont"
nom_complet = prenom + " " + nom  # "Alice Dupont"

# Répétition
print("Ha" * 3)      # "HaHaHa"

# Vérification
"Data" in texte      # True
"Python" in texte    # False


10.2 MÉTHODES DE STRING
------------------------

texte = "  Data Science  "

# Casse
texte.upper()        # "  DATA SCIENCE  "
texte.lower()        # "  data science  "
texte.capitalize()   # "  data science  " (première lettre en maj)
texte.title()        # "  Data Science  " (chaque mot en maj)

# Nettoyage
texte.strip()        # "Data Science" (enlève espaces début/fin)
texte.lstrip()       # "Data Science  " (enlève à gauche)
texte.rstrip()       # "  Data Science" (enlève à droite)

# Remplacement
texte.replace("Data", "Big Data")  # "  Big Data Science  "

# Division et jonction
mots = "Data Science Python".split()  # ['Data', 'Science', 'Python']
" ".join(mots)                        # "Data Science Python"
"-".join(mots)                        # "Data-Science-Python"

# Recherche
texte.find("Science")    # 7 (index de début, -1 si non trouvé)
texte.count("a")         # 2 (nombre d'occurrences)
texte.startswith("Data") # True
texte.endswith("ce")     # True


10.3 FORMATAGE DE STRINGS
--------------------------

# Méthode 1: f-strings (RECOMMANDÉ - Python 3.6+)
nom = "Alice"
age = 25
message = f"Je m'appelle {nom} et j'ai {age} ans"

# Avec expressions
message = f"Dans 5 ans, j'aurai {age + 5} ans"

# Formatage de nombres
pi = 3.14159265359
print(f"Pi = {pi:.2f}")           # "Pi = 3.14" (2 décimales)
print(f"Pi = {pi:.4f}")           # "Pi = 3.1416"
nombre = 1234567
print(f"Nombre: {nombre:,}")      # "Nombre: 1,234,567"

# Méthode 2: .format()
message = "Je m'appelle {} et j'ai {} ans".format(nom, age)
message = "Je m'appelle {0} et j'ai {1} ans".format(nom, age)
message = "Je m'appelle {n} et j'ai {a} ans".format(n=nom, a=age)

# Méthode 3: % (ancien style, moins utilisé)
message = "Je m'appelle %s et j'ai %d ans" % (nom, age)


================================================================================
                         11. CLASSES ET OBJETS (BASES)
================================================================================

11.1 DÉFINITION D'UNE CLASSE
----------------------------

class Personne:
    # Constructeur
    def __init__(self, nom, age):
        self.nom = nom      # Attribut d'instance
        self.age = age
    
    # Méthode
    def se_presenter(self):
        return f"Je m'appelle {self.nom} et j'ai {self.age} ans"
    
    # Méthode qui modifie l'état
    def vieillir(self):
        self.age += 1

# Création d'objets (instances)
alice = Personne("Alice", 25)
bob = Personne("Bob", 30)

# Accès aux attributs
print(alice.nom)         # "Alice"
print(bob.age)           # 30

# Appel de méthodes
print(alice.se_presenter())  # "Je m'appelle Alice et j'ai 25 ans"
alice.vieillir()
print(alice.age)         # 26


11.2 HÉRITAGE
-------------

class Etudiant(Personne):
    def __init__(self, nom, age, universite):
        super().__init__(nom, age)  # Appel du constructeur parent
        self.universite = universite
    
    def se_presenter(self):
        return f"{super().se_presenter()} et j'étudie à {self.universite}"

etudiant = Etudiant("Charlie", 20, "Sorbonne")
print(etudiant.se_presenter())


================================================================================
                         12. BIBLIOTHÈQUES ESSENTIELLES
================================================================================

12.1 NUMPY - CALCUL NUMÉRIQUE
------------------------------

import numpy as np

# Créer des arrays
arr = np.array([1, 2, 3, 4, 5])
arr2d = np.array([[1, 2, 3], [4, 5, 6]])

# Opérations vectorisées
arr * 2              # Multiplie chaque élément par 2
arr + 10             # Ajoute 10 à chaque élément
np.sqrt(arr)         # Racine carrée de chaque élément

# Fonctions utiles
np.mean(arr)         # Moyenne
np.median(arr)       # Médiane
np.std(arr)          # Écart-type
np.min(arr)          # Minimum
np.max(arr)          # Maximum
np.sum(arr)          # Somme


12.2 PANDAS - MANIPULATION DE DONNÉES
--------------------------------------

import pandas as pd

# Créer un DataFrame
data = {
    'nom': ['Alice', 'Bob', 'Charlie'],
    'age': [25, 30, 35],
    'ville': ['Paris', 'Lyon', 'Marseille']
}
df = pd.DataFrame(data)

# Lire des fichiers
df = pd.read_csv('data.csv')
df = pd.read_excel('data.xlsx')

# Afficher les données
df.head()            # 5 premières lignes
df.tail()            # 5 dernières lignes
df.info()            # Informations sur le DataFrame
df.describe()        # Statistiques descriptives

# Sélection
df['nom']            # Sélectionner une colonne
df[['nom', 'age']]   # Plusieurs colonnes
df.loc[0]            # Sélectionner une ligne par index
df.iloc[0]           # Sélectionner par position

# Filtrage
df[df['age'] > 25]   # Filtrer les lignes


12.3 MATPLOTLIB - VISUALISATION
--------------------------------

import matplotlib.pyplot as plt

# Graphique simple
x = [1, 2, 3, 4, 5]
y = [2, 4, 6, 8, 10]

plt.plot(x, y)
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Mon graphique')
plt.show()

# Histogramme
data = [1, 2, 2, 3, 3, 3, 4, 4, 5]
plt.hist(data, bins=5)
plt.show()


================================================================================
                         13. BONNES PRATIQUES
================================================================================

13.1 CONVENTIONS DE NOMMAGE (PEP 8)
------------------------------------

# Variables et fonctions: snake_case (minuscules avec underscores)
mon_age = 25
def calculer_moyenne(valeurs):
    pass

# Classes: PascalCase (chaque mot commence par une majuscule)
class MaClasse:
    pass

# Constantes: MAJUSCULES avec underscores
PI = 3.14159
TAUX_TVA = 0.20


13.2 COMMENTAIRES
-----------------

# Commentaire sur une ligne

"""
Commentaire
sur plusieurs
lignes (docstring)
"""

# Les docstrings pour documenter les fonctions
def calculer_moyenne(valeurs):
    """
    Calcule la moyenne d'une liste de valeurs.
    
    Args:
        valeurs (list): Liste de nombres
    
    Returns:
        float: La moyenne des valeurs
    """
    return sum(valeurs) / len(valeurs)


13.3 CONSEILS
-------------

1. Utilisez des noms de variables explicites
   [OK] nombre_etudiants = 30
   [X] n = 30

2. Évitez les variables globales

3. Une fonction = une tâche

4. Utilisez les list comprehensions quand c'est lisible

5. Préférez "with" pour les fichiers

6. Gérez les exceptions avec try-except

7. Commentez le "pourquoi", pas le "quoi"


================================================================================
                         14. ASTUCES PYTHON POUR DATA SCIENCE
================================================================================

14.1 UNPACKING
--------------

# Déballage de listes
a, b, c = [1, 2, 3]

# Avec *
first, *middle, last = [1, 2, 3, 4, 5]
# first=1, middle=[2, 3, 4], last=5


14.2 OPÉRATEUR TERNAIRE
------------------------

# Syntaxe: valeur_si_vrai if condition else valeur_si_faux
age = 18
statut = "Majeur" if age >= 18 else "Mineur"


14.3 ENUMERATE ET ZIP
---------------------

# enumerate(): obtenir index et valeur
for i, valeur in enumerate(['a', 'b', 'c'], start=1):
    print(f"{i}: {valeur}")

# zip(): combiner plusieurs listes
noms = ['Alice', 'Bob']
ages = [25, 30]
for nom, age in zip(noms, ages):
    print(f"{nom}: {age}")


14.4 DICTIONNAIRES PAR DÉFAUT
------------------------------

from collections import defaultdict

# Compteur avec defaultdict
compteur = defaultdict(int)
for lettre in "bonjour":
    compteur[lettre] += 1


14.5 COLLECTIONS UTILES
------------------------

from collections import Counter

# Compter les occurrences
mots = ['a', 'b', 'a', 'c', 'b', 'a']
compteur = Counter(mots)  # Counter({'a': 3, 'b': 2, 'c': 1})
compteur.most_common(2)   # [('a', 3), ('b', 2)]


================================================================================
                         15. EXERCICE PRATIQUE COMPLET
================================================================================

ÉNONCÉ:
-------
Créez un programme de gestion de notes d'étudiants avec les fonctionnalités:

1. Une classe Etudiant avec:
   - Attributs: nom, prenom, notes (liste)
   - Méthode pour ajouter une note
   - Méthode pour calculer la moyenne
   - Méthode pour afficher les informations

2. Une fonction pour créer plusieurs étudiants

3. Une fonction pour:
   - Calculer la moyenne de la classe
   - Trouver le meilleur étudiant
   - Afficher les statistiques

4. Sauvegarde des données dans un fichier JSON

5. Gestion des erreurs

SOLUTION COMPLÈTE:
------------------
"""

# Import des modules nécessaires
import json
import statistics

# 1. CLASSE ETUDIANT
class Etudiant:
    """Classe représentant un étudiant avec ses notes"""
    
    def __init__(self, nom, prenom):
        """
        Initialise un étudiant
        
        Args:
            nom (str): Nom de famille
            prenom (str): Prénom
        """
        self.nom = nom
        self.prenom = prenom
        self.notes = []
    
    def ajouter_note(self, note):
        """
        Ajoute une note à l'étudiant
        
        Args:
            note (float): Note entre 0 et 20
        
        Raises:
            ValueError: Si la note n'est pas entre 0 et 20
        """
        if not 0 <= note <= 20:
            raise ValueError("La note doit être entre 0 et 20")
        self.notes.append(note)
    
    def calculer_moyenne(self):
        """
        Calcule la moyenne des notes
        
        Returns:
            float: Moyenne ou None si pas de notes
        """
        if not self.notes:
            return None
        return sum(self.notes) / len(self.notes)
    
    def afficher_infos(self):
        """Affiche les informations de l'étudiant"""
        moyenne = self.calculer_moyenne()
        print(f"\n--- {self.prenom} {self.nom} ---")
        print(f"Notes: {self.notes}")
        if moyenne is not None:
            print(f"Moyenne: {moyenne:.2f}/20")
        else:
            print("Pas de notes enregistrées")
    
    def to_dict(self):
        """
        Convertit l'étudiant en dictionnaire pour JSON
        
        Returns:
            dict: Représentation dictionnaire
        """
        return {
            'nom': self.nom,
            'prenom': self.prenom,
            'notes': self.notes
        }
    
    @classmethod
    def from_dict(cls, data):
        """
        Crée un étudiant depuis un dictionnaire
        
        Args:
            data (dict): Données de l'étudiant
        
        Returns:
            Etudiant: Instance d'étudiant
        """
        etudiant = cls(data['nom'], data['prenom'])
        etudiant.notes = data['notes']
        return etudiant


# 2. FONCTION POUR CRÉER DES ÉTUDIANTS
def creer_etudiants():
    """
    Crée et retourne une liste d'étudiants avec des notes
    
    Returns:
        list: Liste d'objets Etudiant
    """
    etudiants = []
    
    # Création de quelques étudiants
    alice = Etudiant("Dupont", "Alice")
    alice.ajouter_note(15)
    alice.ajouter_note(17)
    alice.ajouter_note(14)
    etudiants.append(alice)
    
    bob = Etudiant("Martin", "Bob")
    bob.ajouter_note(12)
    bob.ajouter_note(13)
    bob.ajouter_note(11)
    etudiants.append(bob)
    
    charlie = Etudiant("Dubois", "Charlie")
    charlie.ajouter_note(18)
    charlie.ajouter_note(19)
    charlie.ajouter_note(17)
    etudiants.append(charlie)
    
    return etudiants


# 3. FONCTION DE STATISTIQUES
def analyser_classe(etudiants):
    """
    Analyse et affiche les statistiques de la classe
    
    Args:
        etudiants (list): Liste d'étudiants
    """
    if not etudiants:
        print("Aucun étudiant dans la classe")
        return
    
    print("\n" + "="*60)
    print("STATISTIQUES DE LA CLASSE")
    print("="*60)
    
    # Calculer toutes les moyennes
    moyennes = []
    for etudiant in etudiants:
        moyenne = etudiant.calculer_moyenne()
        if moyenne is not None:
            moyennes.append(moyenne)
    
    if not moyennes:
        print("Aucune note enregistrée")
        return
    
    # Moyenne de la classe
    moyenne_classe = sum(moyennes) / len(moyennes)
    print(f"\nMoyenne de la classe: {moyenne_classe:.2f}/20")
    
    # Médiane
    mediane = statistics.median(moyennes)
    print(f"Médiane de la classe: {mediane:.2f}/20")
    
    # Écart-type
    if len(moyennes) > 1:
        ecart_type = statistics.stdev(moyennes)
        print(f"Écart-type: {ecart_type:.2f}")
    
    # Meilleur et moins bon
    print(f"Meilleure moyenne: {max(moyennes):.2f}/20")
    print(f"Moins bonne moyenne: {min(moyennes):.2f}/20")
    
    # Meilleur étudiant
    meilleur_etudiant = max(etudiants, key=lambda e: e.calculer_moyenne() or 0)
    print(f"\nMeilleur étudiant: {meilleur_etudiant.prenom} {meilleur_etudiant.nom}")
    print(f"Moyenne: {meilleur_etudiant.calculer_moyenne():.2f}/20")
    
    # Distribution des notes
    print("\n--- Distribution ---")
    excellents = sum(1 for m in moyennes if m >= 16)
    bons = sum(1 for m in moyennes if 14 <= m < 16)
    moyens = sum(1 for m in moyennes if 10 <= m < 14)
    faibles = sum(1 for m in moyennes if m < 10)
    
    print(f"Excellents (≥16): {excellents} étudiant(s)")
    print(f"Bons (14-16): {bons} étudiant(s)")
    print(f"Moyens (10-14): {moyens} étudiant(s)")
    print(f"Faibles (<10): {faibles} étudiant(s)")


# 4. SAUVEGARDE ET CHARGEMENT JSON
def sauvegarder_etudiants(etudiants, fichier="etudiants.json"):
    """
    Sauvegarde les étudiants dans un fichier JSON
    
    Args:
        etudiants (list): Liste d'étudiants
        fichier (str): Nom du fichier
    """
    try:
        donnees = [etudiant.to_dict() for etudiant in etudiants]
        with open(fichier, 'w', encoding='utf-8') as f:
            json.dump(donnees, f, indent=4, ensure_ascii=False)
        print(f"\n[OK] Données sauvegardées dans {fichier}")
    except Exception as e:
        print(f"[X] Erreur lors de la sauvegarde: {e}")


def charger_etudiants(fichier="etudiants.json"):
    """
    Charge les étudiants depuis un fichier JSON
    
    Args:
        fichier (str): Nom du fichier
    
    Returns:
        list: Liste d'étudiants ou liste vide si erreur
    """
    try:
        with open(fichier, 'r', encoding='utf-8') as f:
            donnees = json.load(f)
        etudiants = [Etudiant.from_dict(data) for data in donnees]
        print(f"[OK] {len(etudiants)} étudiant(s) chargé(s) depuis {fichier}")
        return etudiants
    except FileNotFoundError:
        print(f"[ATTENTION] Fichier {fichier} non trouvé")
        return []
    except Exception as e:
        print(f"[X] Erreur lors du chargement: {e}")
        return []


# 5. PROGRAMME PRINCIPAL
def main():
    """Fonction principale du programme"""
    print("="*60)
    print("SYSTÈME DE GESTION DES NOTES")
    print("="*60)
    
    # Créer les étudiants
    etudiants = creer_etudiants()
    
    # Afficher les informations de chaque étudiant
    print("\n[DOCS] INFORMATIONS DES ÉTUDIANTS")
    for etudiant in etudiants:
        etudiant.afficher_infos()
    
    # Analyser la classe
    analyser_classe(etudiants)
    
    # Sauvegarder les données
    sauvegarder_etudiants(etudiants)
    
    # Test de chargement
    print("\n--- Test de chargement ---")
    etudiants_charges = charger_etudiants()
    if etudiants_charges:
        print(f"Nombre d'étudiants chargés: {len(etudiants_charges)}")
    
    # Démonstration de gestion d'erreur
    print("\n--- Test de gestion d'erreur ---")
    try:
        etudiant_test = Etudiant("Test", "Etudiant")
        etudiant_test.ajouter_note(25)  # Note invalide
    except ValueError as e:
        print(f"[OK] Erreur capturée: {e}")


# Point d'entrée du programme
if __name__ == "__main__":
    main()

"""
================================================================================
                         EXPLICATION DU CODE
================================================================================

CONCEPTS PYTHON UTILISÉS:

1. CLASSES ET POO:
   - __init__: Constructeur de classe
   - self: Référence à l'instance
   - Méthodes d'instance
   - @classmethod: Méthode de classe
   - Encapsulation des données

2. GESTION DES EXCEPTIONS:
   - try-except pour capturer les erreurs
   - raise pour lever des exceptions
   - Exceptions personnalisées

3. MANIPULATION DE FICHIERS:
   - with open() pour gérer les fichiers
   - json.dump() et json.load()
   - Encodage UTF-8

4. FONCTIONS AVANCÉES:
   - Paramètres par défaut
   - Docstrings pour la documentation
   - Lambda functions
   - max() avec key

5. COMPRÉHENSIONS DE LISTE:
   - [etudiant.to_dict() for etudiant in etudiants]
   - Conversion liste en dictionnaires

6. MODULE STATISTICS:
   - Calcul de médiane
   - Calcul d'écart-type

7. STRUCTURES DE DONNÉES:
   - Listes pour stocker les étudiants
   - Dictionnaires pour JSON
   - Filtrage avec conditions

RÉSULTAT ATTENDU:
-----------------
Le programme affiche:
1. Les informations de chaque étudiant
2. Les statistiques de la classe
3. La distribution des performances
4. Sauvegarde les données en JSON
5. Peut recharger les données

FICHIER JSON CRÉÉ (etudiants.json):
[
    {
        "nom": "Dupont",
        "prenom": "Alice",
        "notes": [15, 17, 14]
    },
    ...
]

================================================================================
                         POINTS CLÉS À RETENIR
================================================================================

[OK] Utilisez les classes pour structurer votre code
[OK] Gérez toujours les exceptions possibles
[OK] Documentez vos fonctions avec des docstrings
[OK] Utilisez des noms de variables explicites
[OK] Séparez la logique en fonctions réutilisables
[OK] Validez les données en entrée
[OK] Utilisez with pour les fichiers
[OK] Sauvegardez les données dans des formats standards (JSON, CSV)

================================================================================
FIN DU COURS PRÉREQUIS - VOUS ÊTES PRÊT POUR LA DATA SCIENCE! [RAPIDE]
================================================================================
"""

================================================================================
           CHAPITRE 1: VISUALISATION DE DONNÉES AVEC PYTHON
            Matplotlib, Seaborn, Plotly - Guide Ultra-Complet
================================================================================

[IMPORTANT] OBJECTIFS DU CHAPITRE:
   - Maîtriser Matplotlib pour créer tous types de graphiques
   - Utiliser Seaborn pour des visualisations statistiques avancées
   - Créer des graphiques interactifs avec Plotly
   - Comprendre les bonnes pratiques de visualisation
   - Personnaliser complètement vos graphiques

================================================================================
                    PARTIE 1: INTRODUCTION À LA VISUALISATION
================================================================================

1.1 POURQUOI VISUALISER LES DONNÉES?
-------------------------------------

La visualisation de données est CRUCIALE en Data Science car:

[OK] Communication: Transmet des insights complexes simplement
[OK] Exploration: Révèle des patterns cachés dans les données
[OK] Détection d'anomalies: Identifie les outliers visuellement
[OK] Validation: Vérifie les hypothèses et résultats
[OK] Storytelling: Raconte l'histoire derrière les chiffres

Principe: "Une image vaut mille mots" (et mille lignes de code!)


1.2 LES TROIS BIBLIOTHÈQUES PRINCIPALES
----------------------------------------

MATPLOTLIB:
- La bibliothèque de base, très flexible
- Contrôle total sur chaque élément
- Style "bas niveau" mais puissant
- Base de nombreuses autres bibliothèques

SEABORN:
- Basée sur Matplotlib, plus haut niveau
- Esthétique moderne par défaut
- Intégration native avec Pandas
- Spécialisée en visualisations statistiques

PLOTLY:
- Graphiques interactifs (zoom, hover, etc.)
- Parfait pour le web et les dashboards
- Rendu HTML
- Très moderne et professionnel


1.3 INSTALLATION
----------------

# Installation des bibliothèques
pip install matplotlib seaborn plotly pandas numpy --break-system-packages

# Imports standards
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np


================================================================================
                    PARTIE 2: MATPLOTLIB - LA BASE
================================================================================

2.1 ANATOMIE D'UN GRAPHIQUE MATPLOTLIB
---------------------------------------

Un graphique Matplotlib est composé de:

FIGURE: Le conteneur principal (la fenêtre)
AXES: La zone de dessin (le graphique lui-même)
AXIS: Les axes X et Y
TITLE: Le titre
LABELS: Les étiquettes des axes
LEGEND: La légende
TICKS: Les graduations
GRID: La grille

        Title
         |
    _____|_____
   |           |
Y  |   AXES    |  Legend
   |  (plot)   |
   |___________|
        X


2.2 DEUX INTERFACES MATPLOTLIB
-------------------------------

INTERFACE 1: pyplot (style MATLAB - simple)
-------------------------------------------
import matplotlib.pyplot as plt

# Création automatique de figure et axes
plt.plot([1, 2, 3], [1, 4, 9])
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Mon graphique')
plt.show()

INTERFACE 2: Orientée objet (recommandée pour complexe)
--------------------------------------------------------
import matplotlib.pyplot as plt

# Création explicite de figure et axes
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [1, 4, 9])
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_title('Mon graphique')
plt.show()


2.3 GRAPHIQUES DE BASE MATPLOTLIB
----------------------------------

A. GRAPHIQUE EN LIGNE (plot)
-----------------------------
"""
import matplotlib.pyplot as plt
import numpy as np

# Données
x = np.linspace(0, 10, 100)  # 100 points de 0 à 10
y = np.sin(x)

# Graphique simple
plt.figure(figsize=(10, 6))  # Taille: 10x6 pouces
plt.plot(x, y)
plt.xlabel('X', fontsize=12)
plt.ylabel('sin(X)', fontsize=12)
plt.title('Fonction Sinus', fontsize=14, fontweight='bold')
plt.grid(True, alpha=0.3)  # Grille avec transparence
plt.show()

# Plusieurs lignes
y2 = np.cos(x)
y3 = np.tan(x)

plt.figure(figsize=(10, 6))
plt.plot(x, y, label='sin(x)', color='blue', linewidth=2)
plt.plot(x, y2, label='cos(x)', color='red', linewidth=2, linestyle='--')
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Fonctions Trigonométriques')
plt.legend(loc='upper right')  # Position de la légende
plt.grid(True)
plt.ylim(-2, 2)  # Limites de l'axe Y
plt.show()
"""

PARAMÈTRES IMPORTANTS DE plot():
--------------------------------
color ou c:     Couleur ('r', 'blue', '#FF5733', (0.1, 0.2, 0.5))
linestyle ou ls: Style de ligne ('-', '--', '-.', ':')
linewidth ou lw: Épaisseur de ligne (nombre)
marker:         Marqueur ('o', 's', '^', '*', 'D')
markersize ou ms: Taille du marqueur
label:          Étiquette pour la légende
alpha:          Transparence (0 à 1)


B. NUAGE DE POINTS (scatter)
-----------------------------
"""
import matplotlib.pyplot as plt
import numpy as np

# Données aléatoires
np.random.seed(42)
x = np.random.randn(100)
y = 2*x + np.random.randn(100)  # Y corrélé avec X

# Scatter plot simple
plt.figure(figsize=(10, 6))
plt.scatter(x, y, alpha=0.6)
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Nuage de Points')
plt.grid(True, alpha=0.3)
plt.show()

# Scatter avec couleurs et tailles variables
sizes = np.random.randint(10, 200, 100)  # Tailles aléatoires
colors = np.random.rand(100)  # Couleurs aléatoires

plt.figure(figsize=(10, 6))
scatter = plt.scatter(x, y, c=colors, s=sizes, alpha=0.6, 
                     cmap='viridis', edgecolors='black', linewidth=0.5)
plt.colorbar(scatter, label='Valeur de couleur')  # Barre de couleur
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Nuage de Points Avancé')
plt.grid(True, alpha=0.3)
plt.show()
"""


C. HISTOGRAMME (hist)
---------------------
"""
import matplotlib.pyplot as plt
import numpy as np

# Données: distribution normale
data = np.random.randn(1000)

# Histogramme simple
plt.figure(figsize=(10, 6))
plt.hist(data, bins=30, edgecolor='black', alpha=0.7)
plt.xlabel('Valeur')
plt.ylabel('Fréquence')
plt.title('Distribution Normale')
plt.grid(True, alpha=0.3, axis='y')
plt.show()

# Histogramme avec densité de probabilité
plt.figure(figsize=(10, 6))
plt.hist(data, bins=30, density=True, alpha=0.7, 
         color='skyblue', edgecolor='black', label='Données')

# Ajouter la courbe théorique
from scipy import stats
mu, sigma = stats.norm.fit(data)
x = np.linspace(data.min(), data.max(), 100)
plt.plot(x, stats.norm.pdf(x, mu, sigma), 
         'r-', linewidth=2, label=f'Normale(μ={mu:.2f}, σ={sigma:.2f})')

plt.xlabel('Valeur')
plt.ylabel('Densité')
plt.title('Distribution avec Courbe Théorique')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
"""


D. DIAGRAMME EN BARRES (bar)
-----------------------------
"""
import matplotlib.pyplot as plt

# Données
categories = ['A', 'B', 'C', 'D', 'E']
valeurs = [23, 45, 56, 78, 32]

# Barres verticales
plt.figure(figsize=(10, 6))
plt.bar(categories, valeurs, color='steelblue', edgecolor='black', alpha=0.7)
plt.xlabel('Catégories')
plt.ylabel('Valeurs')
plt.title('Diagramme en Barres')
plt.grid(True, alpha=0.3, axis='y')
plt.show()

# Barres horizontales
plt.figure(figsize=(10, 6))
plt.barh(categories, valeurs, color='coral', edgecolor='black', alpha=0.7)
plt.xlabel('Valeurs')
plt.ylabel('Catégories')
plt.title('Diagramme en Barres Horizontales')
plt.grid(True, alpha=0.3, axis='x')
plt.show()

# Barres groupées
categories = ['A', 'B', 'C', 'D']
groupe1 = [20, 35, 30, 35]
groupe2 = [25, 32, 34, 20]
groupe3 = [22, 28, 30, 25]

x = np.arange(len(categories))  # Positions des groupes
width = 0.25  # Largeur des barres

fig, ax = plt.subplots(figsize=(10, 6))
ax.bar(x - width, groupe1, width, label='Groupe 1', color='skyblue')
ax.bar(x, groupe2, width, label='Groupe 2', color='lightcoral')
ax.bar(x + width, groupe3, width, label='Groupe 3', color='lightgreen')

ax.set_xlabel('Catégories')
ax.set_ylabel('Valeurs')
ax.set_title('Barres Groupées')
ax.set_xticks(x)
ax.set_xticklabels(categories)
ax.legend()
ax.grid(True, alpha=0.3, axis='y')
plt.show()
"""


E. CAMEMBERT (pie)
------------------
"""
import matplotlib.pyplot as plt

# Données
labels = ['Python', 'R', 'SQL', 'Java', 'Autres']
sizes = [35, 25, 20, 10, 10]
colors = ['#ff9999', '#66b3ff', '#99ff99', '#ffcc99', '#ff99cc']
explode = (0.1, 0, 0, 0, 0)  # "Exploser" la 1ère part

plt.figure(figsize=(10, 8))
plt.pie(sizes, explode=explode, labels=labels, colors=colors,
        autopct='%1.1f%%',  # Affiche les pourcentages
        shadow=True, startangle=90)
plt.title('Langages Utilisés en Data Science')
plt.axis('equal')  # Assure que le cercle est rond
plt.show()

# Donut chart (variante moderne)
plt.figure(figsize=(10, 8))
plt.pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%',
        startangle=90, pctdistance=0.85)
# Créer le trou au centre
centre_circle = plt.Circle((0,0), 0.70, fc='white')
fig = plt.gcf()
fig.gca().add_artist(centre_circle)
plt.title('Donut Chart')
plt.axis('equal')
plt.show()
"""


F. BOÎTE À MOUSTACHES (boxplot)
--------------------------------
"""
import matplotlib.pyplot as plt
import numpy as np

# Générer des données
np.random.seed(42)
data1 = np.random.normal(100, 10, 200)
data2 = np.random.normal(90, 20, 200)
data3 = np.random.normal(110, 15, 200)

data = [data1, data2, data3]

plt.figure(figsize=(10, 6))
box = plt.boxplot(data, labels=['Groupe A', 'Groupe B', 'Groupe C'],
                  patch_artist=True,  # Pour colorer les boîtes
                  notch=True,  # Encoche pour l'IC de la médiane
                  showmeans=True)  # Afficher la moyenne

# Personnaliser les couleurs
colors = ['lightblue', 'lightgreen', 'lightcoral']
for patch, color in zip(box['boxes'], colors):
    patch.set_facecolor(color)

plt.ylabel('Valeurs')
plt.title('Boîtes à Moustaches Comparatives')
plt.grid(True, alpha=0.3, axis='y')
plt.show()
"""

EXPLICATION BOXPLOT:
-------------------
La boîte représente l'intervalle interquartile (IQR = Q3 - Q1)
- Ligne du bas: Q1 (25e percentile)
- Ligne du milieu: Médiane (50e percentile)
- Ligne du haut: Q3 (75e percentile)
- Moustaches: s'étendent jusqu'à 1.5*IQR
- Points: outliers (valeurs au-delà des moustaches)


G. CARTE DE CHALEUR (imshow/matshow)
-------------------------------------
"""
import matplotlib.pyplot as plt
import numpy as np

# Créer une matrice de corrélation
data = np.random.rand(10, 10)

plt.figure(figsize=(10, 8))
plt.imshow(data, cmap='hot', interpolation='nearest')
plt.colorbar(label='Valeur')
plt.title('Carte de Chaleur')
plt.xlabel('X')
plt.ylabel('Y')
plt.show()

# Avec annotations
fig, ax = plt.subplots(figsize=(10, 8))
im = ax.imshow(data, cmap='YlOrRd')

# Ajouter les valeurs dans chaque cellule
for i in range(data.shape[0]):
    for j in range(data.shape[1]):
        text = ax.text(j, i, f'{data[i, j]:.2f}',
                      ha="center", va="center", color="black", fontsize=8)

ax.set_title('Carte de Chaleur avec Annotations')
plt.colorbar(im, ax=ax)
plt.show()
"""


2.4 SUBPLOTS: PLUSIEURS GRAPHIQUES
-----------------------------------

A. GRILLE SIMPLE
----------------
"""
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)

# Créer une grille 2x2
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# Premier graphique (en haut à gauche)
axes[0, 0].plot(x, np.sin(x))
axes[0, 0].set_title('Sinus')
axes[0, 0].grid(True)

# Deuxième graphique (en haut à droite)
axes[0, 1].plot(x, np.cos(x), color='red')
axes[0, 1].set_title('Cosinus')
axes[0, 1].grid(True)

# Troisième graphique (en bas à gauche)
axes[1, 0].plot(x, x**2, color='green')
axes[1, 0].set_title('Carré')
axes[1, 0].grid(True)

# Quatrième graphique (en bas à droite)
axes[1, 1].plot(x, np.exp(x/10), color='orange')
axes[1, 1].set_title('Exponentielle')
axes[1, 1].grid(True)

plt.tight_layout()  # Ajuste l'espacement
plt.show()
"""

B. GRILLE PERSONNALISÉE
------------------------
"""
import matplotlib.pyplot as plt
import numpy as np

# GridSpec pour des layouts complexes
from matplotlib.gridspec import GridSpec

fig = plt.figure(figsize=(12, 8))
gs = GridSpec(3, 3, figure=fig)

# Grand graphique en haut
ax1 = fig.add_subplot(gs[0, :])  # Première ligne, toutes les colonnes
ax1.plot(np.random.randn(100))
ax1.set_title('Graphique Principal')

# Deux graphiques moyens
ax2 = fig.add_subplot(gs[1, :2])  # Deuxième ligne, 2 premières colonnes
ax2.scatter(np.random.randn(50), np.random.randn(50))
ax2.set_title('Scatter')

ax3 = fig.add_subplot(gs[1:, 2])  # Lignes 2-3, dernière colonne
ax3.hist(np.random.randn(100), orientation='horizontal')
ax3.set_title('Histogramme')

# Deux petits graphiques en bas
ax4 = fig.add_subplot(gs[2, 0])
ax4.bar(['A', 'B', 'C'], [1, 2, 3])
ax4.set_title('Barres 1')

ax5 = fig.add_subplot(gs[2, 1])
ax5.bar(['X', 'Y', 'Z'], [3, 2, 1])
ax5.set_title('Barres 2')

plt.tight_layout()
plt.show()
"""


2.5 PERSONNALISATION AVANCÉE
-----------------------------

A. STYLES ET THÈMES
-------------------
"""
import matplotlib.pyplot as plt

# Voir tous les styles disponibles
print(plt.style.available)

# Utiliser un style
plt.style.use('seaborn-v0_8-darkgrid')  # ou 'ggplot', 'fivethirtyeight', etc.

# Réinitialiser
plt.style.use('default')
"""

B. COULEURS ET COLORMAPS
-------------------------
"""
import matplotlib.pyplot as plt
import numpy as np

# Couleurs nommées
colors_named = ['red', 'blue', 'green', 'orange', 'purple']

# Couleurs hexadécimales
colors_hex = ['#FF5733', '#33FF57', '#3357FF']

# Couleurs RGB (0 à 1)
colors_rgb = [(0.1, 0.2, 0.3), (0.9, 0.5, 0.1)]

# Colormaps courantes:
# Sequential: 'viridis', 'plasma', 'inferno', 'magma', 'cividis'
# Diverging: 'RdYlBu', 'RdYlGn', 'Spectral', 'coolwarm'
# Qualitative: 'Set1', 'Set2', 'Set3', 'Paired', 'tab10'

# Exemple avec colormap
data = np.random.rand(10, 10)
plt.imshow(data, cmap='viridis')
plt.colorbar()
plt.show()
"""

C. ANNOTATIONS ET TEXTE
------------------------
"""
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)
y = np.sin(x)

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

# Ajouter du texte simple
ax.text(5, 0.5, 'Point important', fontsize=12, color='red')

# Ajouter une annotation avec flèche
ax.annotate('Maximum', xy=(np.pi/2, 1), xytext=(2, 0.8),
            arrowprops=dict(arrowstyle='->', color='red', lw=2),
            fontsize=12, color='red')

# Ajouter une ligne horizontale/verticale
ax.axhline(y=0, color='black', linestyle='--', linewidth=0.5, alpha=0.5)
ax.axvline(x=5, color='green', linestyle='--', linewidth=0.5, alpha=0.5)

# Ajouter une zone colorée
ax.axvspan(3, 7, alpha=0.2, color='yellow', label='Zone importante')

ax.set_title('Graphique avec Annotations')
ax.legend()
ax.grid(True)
plt.show()
"""

D. CONFIGURATION DES AXES
--------------------------
"""
import matplotlib.pyplot as plt
import numpy as np

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

x = np.linspace(0, 10, 100)
y = np.sin(x)
ax.plot(x, y)

# Limites des axes
ax.set_xlim(0, 10)
ax.set_ylim(-1.5, 1.5)

# Échelle logarithmique
# ax.set_xscale('log')
# ax.set_yscale('log')

# Ticks personnalisés
ax.set_xticks([0, 2, 4, 6, 8, 10])
ax.set_xticklabels(['Zéro', 'Deux', 'Quatre', 'Six', 'Huit', 'Dix'])

# Rotation des labels
ax.tick_params(axis='x', rotation=45)

# Taille des labels
ax.tick_params(labelsize=10)

# Grille personnalisée
ax.grid(True, which='major', linestyle='-', linewidth=0.5, alpha=0.7)
ax.grid(True, which='minor', linestyle=':', linewidth=0.5, alpha=0.4)
ax.minorticks_on()

# Épines (spines) - cadre du graphique
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_linewidth(2)
ax.spines['bottom'].set_linewidth(2)

plt.show()
"""


2.6 SAUVEGARDER LES GRAPHIQUES
-------------------------------
"""
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([1, 2, 3], [1, 4, 9])

# Sauvegarder en différents formats
plt.savefig('graphique.png', dpi=300, bbox_inches='tight')  # PNG haute résolution
plt.savefig('graphique.pdf', bbox_inches='tight')  # PDF vectoriel
plt.savefig('graphique.svg', bbox_inches='tight')  # SVG vectoriel
plt.savefig('graphique.jpg', dpi=300, bbox_inches='tight')  # JPEG

# Avec fond transparent
plt.savefig('graphique_transparent.png', transparent=True, bbox_inches='tight')
"""

Paramètres importants de savefig():
-----------------------------------
dpi: Résolution (300 pour publication, 100 pour web)
bbox_inches='tight': Rogne l'espace blanc
transparent: Fond transparent (True/False)
facecolor: Couleur de fond
edgecolor: Couleur du bord


================================================================================
                    PARTIE 3: SEABORN - VISUALISATIONS STATISTIQUES
================================================================================

3.1 INTRODUCTION À SEABORN
---------------------------

Seaborn = Matplotlib + Esthétique + Statistiques

Avantages:
[OK] Graphiques plus beaux par défaut
[OK] Intégration parfaite avec Pandas DataFrames
[OK] Visualisations statistiques en une ligne
[OK] Palettes de couleurs cohérentes
[OK] Facetting (sous-graphiques) facile

"""
import seaborn as sns
import matplotlib.pyplot as plt

# Configurer le style
sns.set_theme(style="whitegrid")  # Styles: darkgrid, whitegrid, dark, white, ticks

# Configurer le contexte (taille des éléments)
sns.set_context("notebook")  # Contextes: paper, notebook, talk, poster
"""


3.2 DATASETS D'EXEMPLE SEABORN
-------------------------------
"""
import seaborn as sns

# Charger des datasets d'exemple
tips = sns.load_dataset('tips')  # Pourboires restaurant
iris = sns.load_dataset('iris')  # Fleurs iris
titanic = sns.load_dataset('titanic')  # Passagers Titanic
planets = sns.load_dataset('planets')  # Exoplanètes
flights = sns.load_dataset('flights')  # Vols aériens

# Voir les datasets disponibles
print(sns.get_dataset_names())

# Explorer un dataset
print(tips.head())
print(tips.info())
"""


3.3 GRAPHIQUES RELATIONNELS
----------------------------

A. SCATTER PLOT (relplot, scatterplot)
---------------------------------------
"""
import seaborn as sns
import matplotlib.pyplot as plt

tips = sns.load_dataset('tips')

# Scatter plot simple
plt.figure(figsize=(10, 6))
sns.scatterplot(data=tips, x='total_bill', y='tip')
plt.title('Pourboire vs Addition Totale')
plt.show()

# Avec couleur selon une variable
plt.figure(figsize=(10, 6))
sns.scatterplot(data=tips, x='total_bill', y='tip', 
                hue='time',  # Couleur selon l'heure
                style='sex',  # Forme selon le sexe
                size='size')  # Taille selon la taille du groupe
plt.title('Analyse Multivariée des Pourboires')
plt.show()

# Avec relplot (FacetGrid automatique)
g = sns.relplot(data=tips, x='total_bill', y='tip',
                col='time',  # Créer des colonnes
                hue='sex',
                style='smoker',
                height=5, aspect=1.2)
g.set_axis_labels("Addition Totale ($)", "Pourboire ($)")
plt.show()
"""

B. LINE PLOT (lineplot)
------------------------
"""
import seaborn as sns
import matplotlib.pyplot as plt

flights = sns.load_dataset('flights')

# Line plot avec intervalle de confiance
plt.figure(figsize=(12, 6))
sns.lineplot(data=flights, x='year', y='passengers', 
             hue='month', errorbar=('ci', 95))  # IC à 95%
plt.title('Évolution du Nombre de Passagers')
plt.show()

# Agrégation automatique
fmri = sns.load_dataset('fmri')
plt.figure(figsize=(12, 6))
sns.lineplot(data=fmri, x='timepoint', y='signal',
             hue='region', style='event',
             markers=True, dashes=False)
plt.title('Signal fMRI')
plt.show()
"""


3.4 GRAPHIQUES DE DISTRIBUTION
-------------------------------

A. HISTOGRAMME (histplot)
--------------------------
"""
import seaborn as sns
import matplotlib.pyplot as plt

tips = sns.load_dataset('tips')

# Histogramme simple
plt.figure(figsize=(10, 6))
sns.histplot(data=tips, x='total_bill', bins=30)
plt.title('Distribution des Additions')
plt.show()

# Avec KDE (Kernel Density Estimation)
plt.figure(figsize=(10, 6))
sns.histplot(data=tips, x='total_bill', bins=30, kde=True)
plt.title('Distribution avec Estimation de Densité')
plt.show()

# Histogrammes empilés
plt.figure(figsize=(10, 6))
sns.histplot(data=tips, x='total_bill', hue='time', 
             multiple='stack', bins=20)
plt.title('Distribution par Moment de la Journée')
plt.show()

# Histogramme 2D (heatmap de densité)
plt.figure(figsize=(10, 6))
sns.histplot(data=tips, x='total_bill', y='tip', bins=20, cmap='YlOrRd')
plt.title('Distribution Bivariée')
plt.show()
"""

B. KDE PLOT (Kernel Density Estimation)
----------------------------------------
"""
import seaborn as sns
import matplotlib.pyplot as plt

tips = sns.load_dataset('tips')

# KDE simple
plt.figure(figsize=(10, 6))
sns.kdeplot(data=tips, x='total_bill')
plt.title('Estimation de Densité')
plt.show()

# KDE avec plusieurs groupes
plt.figure(figsize=(10, 6))
sns.kdeplot(data=tips, x='total_bill', hue='time', 
            fill=True, alpha=0.5)
plt.title('Densités Comparées')
plt.show()

# KDE bivariée
plt.figure(figsize=(10, 6))
sns.kdeplot(data=tips, x='total_bill', y='tip', 
            fill=True, thresh=0, levels=10, cmap='mako')
plt.title('Densité Bivariée')
plt.show()
"""

C. DISTRIBUTION PLOT (displot)
-------------------------------
"""
import seaborn as sns

tips = sns.load_dataset('tips')

# Combine hist, kde, et rug plot
g = sns.displot(data=tips, x='total_bill', kind='hist',
                kde=True, rug=True, height=6, aspect=1.5)
g.set_axis_labels("Addition Totale ($)", "Fréquence")
plt.show()

# Avec facetting
g = sns.displot(data=tips, x='total_bill', col='time',
                row='sex', kind='kde', fill=True)
plt.show()
"""


3.5 GRAPHIQUES CATÉGORIELS
---------------------------

A. BOX PLOT (boxplot)
---------------------
"""
import seaborn as sns
import matplotlib.pyplot as plt

tips = sns.load_dataset('tips')

# Box plot simple
plt.figure(figsize=(10, 6))
sns.boxplot(data=tips, x='day', y='total_bill')
plt.title('Distribution des Additions par Jour')
plt.show()

# Box plot avec hue
plt.figure(figsize=(10, 6))
sns.boxplot(data=tips, x='day', y='total_bill', hue='sex')
plt.title('Additions par Jour et Sexe')
plt.show()

# Box plot horizontal avec ordre
plt.figure(figsize=(10, 6))
sns.boxplot(data=tips, x='total_bill', y='day', 
            order=['Thur', 'Fri', 'Sat', 'Sun'])
plt.title('Box Plot Horizontal')
plt.show()
"""

B. VIOLIN PLOT (violinplot)
----------------------------
"""
import seaborn as sns
import matplotlib.pyplot as plt

tips = sns.load_dataset('tips')

# Violin plot (combine boxplot et KDE)
plt.figure(figsize=(10, 6))
sns.violinplot(data=tips, x='day', y='total_bill')
plt.title('Distribution en Violon')
plt.show()

# Violin plot splitté
plt.figure(figsize=(10, 6))
sns.violinplot(data=tips, x='day', y='total_bill', 
               hue='sex', split=True)
plt.title('Violin Plot Splitté')
plt.show()

# Avec swarm plot superposé
plt.figure(figsize=(10, 6))
sns.violinplot(data=tips, x='day', y='total_bill', color='lightgray')
sns.swarmplot(data=tips, x='day', y='total_bill', color='black', alpha=0.5)
plt.title('Violin + Swarm Plot')
plt.show()
"""

C. STRIP PLOT ET SWARM PLOT
----------------------------
"""
import seaborn as sns
import matplotlib.pyplot as plt

tips = sns.load_dataset('tips')

# Strip plot (points avec jitter)
plt.figure(figsize=(10, 6))
sns.stripplot(data=tips, x='day', y='total_bill', jitter=True, alpha=0.5)
plt.title('Strip Plot')
plt.show()

# Swarm plot (points non superposés)
plt.figure(figsize=(10, 6))
sns.swarmplot(data=tips, x='day', y='total_bill', hue='sex')
plt.title('Swarm Plot')
plt.show()
"""

D. BAR PLOT (barplot)
---------------------
"""
import seaborn as sns
import matplotlib.pyplot as plt

tips = sns.load_dataset('tips')

# Bar plot avec IC (agrège automatiquement)
plt.figure(figsize=(10, 6))
sns.barplot(data=tips, x='day', y='total_bill', 
            errorbar=('ci', 95))  # Intervalle de confiance
plt.title('Addition Moyenne par Jour')
plt.show()

# Avec hue
plt.figure(figsize=(10, 6))
sns.barplot(data=tips, x='day', y='total_bill', hue='sex')
plt.title('Addition Moyenne par Jour et Sexe')
plt.show()
"""

E. COUNT PLOT (countplot)
-------------------------
"""
import seaborn as sns
import matplotlib.pyplot as plt

tips = sns.load_dataset('tips')

# Compte le nombre d'observations
plt.figure(figsize=(10, 6))
sns.countplot(data=tips, x='day')
plt.title('Nombre de Clients par Jour')
plt.show()

# Avec hue
plt.figure(figsize=(10, 6))
sns.countplot(data=tips, x='day', hue='sex')
plt.title('Nombre de Clients par Jour et Sexe')
plt.show()
"""


3.6 GRAPHIQUES MATRICIELS
--------------------------

A. HEATMAP (heatmap)
--------------------
"""
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd

# Créer une matrice de corrélation
tips = sns.load_dataset('tips')
# Sélectionner seulement les colonnes numériques
numeric_data = tips.select_dtypes(include=['float64', 'int64'])
corr = numeric_data.corr()

# Heatmap simple
plt.figure(figsize=(10, 8))
sns.heatmap(corr, annot=True, cmap='coolwarm', center=0,
            square=True, linewidths=1, cbar_kws={"shrink": 0.8})
plt.title('Matrice de Corrélation')
plt.show()

# Heatmap avec masque (triangle supérieur)
import numpy as np
mask = np.triu(np.ones_like(corr, dtype=bool))

plt.figure(figsize=(10, 8))
sns.heatmap(corr, mask=mask, annot=True, cmap='coolwarm', 
            center=0, square=True, linewidths=1)
plt.title('Matrice de Corrélation (Triangle Inférieur)')
plt.show()
"""

B. CLUSTERMAP (clustermap)
---------------------------
"""
import seaborn as sns
import matplotlib.pyplot as plt

iris = sns.load_dataset('iris')
iris_numeric = iris.drop('species', axis=1)

# Heatmap avec clustering hiérarchique
g = sns.clustermap(iris_numeric, standard_scale=1, 
                   cmap='viridis', figsize=(10, 10))
plt.show()

# Avec annotations de ligne
species = iris['species']
lut = dict(zip(species.unique(), sns.color_palette("Set2", 3)))
row_colors = species.map(lut)

g = sns.clustermap(iris_numeric, row_colors=row_colors,
                   standard_scale=1, cmap='mako', figsize=(10, 10))
plt.show()
"""


3.7 PAIRPLOT ET JOINTPLOT
--------------------------

A. PAIRPLOT (pairplot)
----------------------
"""
import seaborn as sns

iris = sns.load_dataset('iris')

# Matrice de scatter plots
g = sns.pairplot(iris, hue='species', height=2.5)
plt.show()

# Avec KDE sur la diagonale
g = sns.pairplot(iris, hue='species', diag_kind='kde', height=2.5)
plt.show()

# Personnalisé
g = sns.pairplot(iris, hue='species',
                 plot_kws={'alpha': 0.6, 's': 80, 'edgecolor': 'k'},
                 height=3)
plt.show()
"""

B. JOINTPLOT (jointplot)
------------------------
"""
import seaborn as sns

tips = sns.load_dataset('tips')

# Joint plot (scatter + distributions marginales)
g = sns.jointplot(data=tips, x='total_bill', y='tip', kind='scatter')
plt.show()

# Avec regression
g = sns.jointplot(data=tips, x='total_bill', y='tip', kind='reg')
plt.show()

# Avec KDE
g = sns.jointplot(data=tips, x='total_bill', y='tip', kind='kde')
plt.show()

# Hexbin
g = sns.jointplot(data=tips, x='total_bill', y='tip', kind='hex')
plt.show()
"""


3.8 REGRESSION PLOTS
--------------------

A. REGPLOT (regplot)
--------------------
"""
import seaborn as sns
import matplotlib.pyplot as plt

tips = sns.load_dataset('tips')

# Regression linéaire simple
plt.figure(figsize=(10, 6))
sns.regplot(data=tips, x='total_bill', y='tip')
plt.title('Régression Linéaire: Pourboire vs Addition')
plt.show()

# Sans intervalle de confiance
plt.figure(figsize=(10, 6))
sns.regplot(data=tips, x='total_bill', y='tip', ci=None)
plt.title('Régression sans IC')
plt.show()

# Regression polynomiale
plt.figure(figsize=(10, 6))
sns.regplot(data=tips, x='total_bill', y='tip', order=2)
plt.title('Régression Polynomiale (degré 2)')
plt.show()

# Regression robuste
plt.figure(figsize=(10, 6))
sns.regplot(data=tips, x='total_bill', y='tip', robust=True)
plt.title('Régression Robuste')
plt.show()
"""

B. LMPLOT (lmplot)
------------------
"""
import seaborn as sns

tips = sns.load_dataset('tips')

# Linear model plot avec facetting
g = sns.lmplot(data=tips, x='total_bill', y='tip', 
               col='time', hue='sex', height=5)
plt.show()

# Avec regression logistique
tips['big_tip'] = (tips['tip'] / tips['total_bill'] > 0.2).astype(int)
g = sns.lmplot(data=tips, x='total_bill', y='big_tip',
               logistic=True, height=6, aspect=1.5)
plt.show()
"""


3.9 FACETGRID - GRILLES DE GRAPHIQUES
--------------------------------------
"""
import seaborn as sns
import matplotlib.pyplot as plt

tips = sns.load_dataset('tips')

# Créer une FacetGrid
g = sns.FacetGrid(tips, col='time', row='sex', height=4)

# Appliquer un plot à chaque facette
g.map(sns.scatterplot, 'total_bill', 'tip')
g.add_legend()
plt.show()

# Avec différentes fonctions
g = sns.FacetGrid(tips, col='day', col_wrap=2, height=4)
g.map(sns.histplot, 'total_bill', kde=True)
plt.show()

# Personnalisé
g = sns.FacetGrid(tips, col='time', hue='sex', height=5, aspect=1.2)
g.map(sns.scatterplot, 'total_bill', 'tip', alpha=0.7)
g.add_legend()
g.set_axis_labels("Addition Totale ($)", "Pourboire ($)")
g.set_titles("{col_name}")
plt.show()
"""


3.10 PALETTES DE COULEURS SEABORN
----------------------------------
"""
import seaborn as sns
import matplotlib.pyplot as plt

# Palettes qualitatives (catégories distinctes)
sns.set_palette("Set2")
sns.set_palette("Paired")
sns.set_palette("tab10")

# Palettes séquentielles (gradients)
sns.set_palette("Blues")
sns.set_palette("YlOrRd")
sns.set_palette("viridis")

# Palettes divergentes (centré sur une valeur)
sns.set_palette("RdBu")
sns.set_palette("coolwarm")
sns.set_palette("Spectral")

# Créer une palette personnalisée
custom_palette = sns.color_palette(["#FF5733", "#33FF57", "#3357FF"])
sns.set_palette(custom_palette)

# Afficher une palette
sns.palplot(sns.color_palette("husl", 10))
plt.show()

# Utiliser dans un plot
tips = sns.load_dataset('tips')
plt.figure(figsize=(10, 6))
sns.barplot(data=tips, x='day', y='total_bill', 
            palette='viridis')
plt.show()
"""


================================================================================
                    PARTIE 4: PLOTLY - VISUALISATIONS INTERACTIVES
================================================================================

4.1 INTRODUCTION À PLOTLY
--------------------------

Plotly crée des graphiques INTERACTIFS:
[OK] Zoom et pan
[OK] Hover pour voir les valeurs
[OK] Sélection de données
[OK] Export en images
[OK] Parfait pour dashboards et web

Deux interfaces principales:
1. Plotly Express (haut niveau, simple)
2. Graph Objects (bas niveau, flexible)

"""
import plotly.express as px
import plotly.graph_objects as go
"""


4.2 PLOTLY EXPRESS - GRAPHIQUES RAPIDES
----------------------------------------

A. SCATTER PLOT
---------------
"""
import plotly.express as px

tips = px.data.tips()  # Dataset d'exemple

# Scatter plot simple
fig = px.scatter(tips, x='total_bill', y='tip',
                 title='Pourboire vs Addition')
fig.show()  # Ouvre dans le navigateur

# Avec couleur et taille
fig = px.scatter(tips, x='total_bill', y='tip',
                 color='time',  # Couleur selon la variable
                 size='size',   # Taille selon la variable
                 hover_data=['day', 'sex'],  # Infos au survol
                 title='Analyse Interactive des Pourboires')
fig.show()

# Avec animation
gapminder = px.data.gapminder()
fig = px.scatter(gapminder, x='gdpPercap', y='lifeExp',
                 animation_frame='year',  # Animation temporelle
                 animation_group='country',
                 size='pop', color='continent',
                 hover_name='country',
                 log_x=True,
                 range_x=[100, 100000],
                 range_y=[25, 90],
                 title='Évolution Mondiale (1952-2007)')
fig.show()
"""

B. LINE CHART
-------------
"""
import plotly.express as px

# Dataset de séries temporelles
df = px.data.stocks()

fig = px.line(df, x='date', y=['GOOG', 'AAPL', 'AMZN'],
              title='Prix des Actions Tech')
fig.show()

# Personnalisé
fig = px.line(df, x='date', y='GOOG',
              title='Google Stock Price')
fig.update_traces(line_color='#4285F4', line_width=3)
fig.update_layout(
    xaxis_title='Date',
    yaxis_title='Prix ($)',
    hovermode='x unified'
)
fig.show()
"""

C. BAR CHART
------------
"""
import plotly.express as px

tips = px.data.tips()

# Bar chart simple
fig = px.bar(tips, x='day', y='total_bill',
             title='Addition Totale par Jour')
fig.show()

# Barres groupées
fig = px.bar(tips, x='day', y='total_bill', color='sex',
             barmode='group',  # ou 'stack' pour empilé
             title='Addition par Jour et Sexe')
fig.show()

# Barres horizontales
fig = px.bar(tips, x='total_bill', y='day', orientation='h',
             color='time',
             title='Barres Horizontales')
fig.show()
"""

D. HISTOGRAMME
--------------
"""
import plotly.express as px

tips = px.data.tips()

# Histogramme simple
fig = px.histogram(tips, x='total_bill', nbins=30,
                   title='Distribution des Additions')
fig.show()

# Avec overlay
fig = px.histogram(tips, x='total_bill', color='sex',
                   marginal='box',  # Ajoute un boxplot en marge
                   barmode='overlay',
                   opacity=0.7,
                   title='Distribution Comparative')
fig.show()
"""

E. BOX ET VIOLIN PLOTS
----------------------
"""
import plotly.express as px

tips = px.data.tips()

# Box plot
fig = px.box(tips, x='day', y='total_bill', color='sex',
             title='Distribution par Jour')
fig.show()

# Violin plot
fig = px.violin(tips, x='day', y='total_bill', color='sex',
                box=True,  # Ajoute un boxplot inside
                points='all',  # Affiche tous les points
                title='Violin Plot Interactive')
fig.show()
"""

F. HEATMAP
----------
"""
import plotly.express as px
import numpy as np

# Matrice de corrélation
tips = px.data.tips()
numeric_cols = tips.select_dtypes(include=['float64', 'int64'])
corr = numeric_cols.corr()

fig = px.imshow(corr, 
                text_auto=True,  # Affiche les valeurs
                color_continuous_scale='RdBu_r',
                title='Matrice de Corrélation Interactive')
fig.show()

# Heatmap personnalisée
z = np.random.randn(20, 20)
fig = px.imshow(z, color_continuous_scale='Viridis',
                title='Heatmap')
fig.show()
"""

G. PIE CHART
------------
"""
import plotly.express as px

tips = px.data.tips()

# Pie chart
fig = px.pie(tips, names='day', values='total_bill',
             title='Répartition des Additions par Jour')
fig.show()

# Donut chart
fig = px.pie(tips, names='day', values='total_bill',
             hole=0.4,  # Crée le trou central
             title='Donut Chart')
fig.show()
"""

H. SUNBURST ET TREEMAP
-----------------------
"""
import plotly.express as px

tips = px.data.tips()

# Sunburst (diagramme en soleil)
fig = px.sunburst(tips, path=['day', 'time', 'sex'],
                  values='total_bill',
                  title='Hiérarchie des Additions')
fig.show()

# Treemap
fig = px.treemap(tips, path=['day', 'time', 'sex'],
                 values='total_bill',
                 color='tip',
                 color_continuous_scale='RdYlGn',
                 title='Treemap des Pourboires')
fig.show()
"""


4.3 GRAPH OBJECTS - CONTRÔLE TOTAL
-----------------------------------

A. CRÉATION BASIQUE
-------------------
"""
import plotly.graph_objects as go
import numpy as np

# Créer un graphique vide
fig = go.Figure()

# Ajouter une trace (ligne, scatter, etc.)
x = np.linspace(0, 10, 100)
y = np.sin(x)

fig.add_trace(go.Scatter(x=x, y=y, mode='lines', name='sin(x)'))

# Ajouter une deuxième trace
y2 = np.cos(x)
fig.add_trace(go.Scatter(x=x, y=y2, mode='lines', name='cos(x)'))

# Mise à jour du layout
fig.update_layout(
    title='Fonctions Trigonométriques',
    xaxis_title='X',
    yaxis_title='Y',
    hovermode='x unified'
)

fig.show()
"""

B. SCATTER 3D
-------------
"""
import plotly.graph_objects as go
import numpy as np

# Données 3D
t = np.linspace(0, 10, 100)
x = np.sin(t)
y = np.cos(t)
z = t

fig = go.Figure(data=[go.Scatter3d(
    x=x, y=y, z=z,
    mode='markers+lines',
    marker=dict(
        size=5,
        color=z,
        colorscale='Viridis',
        showscale=True
    ),
    line=dict(color='darkblue', width=2)
)])

fig.update_layout(
    title='Spirale 3D',
    scene=dict(
        xaxis_title='X',
        yaxis_title='Y',
        zaxis_title='Z'
    )
)

fig.show()
"""

C. SURFACE PLOT
---------------
"""
import plotly.graph_objects as go
import numpy as np

# Créer une grille
x = np.linspace(-5, 5, 50)
y = np.linspace(-5, 5, 50)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

fig = go.Figure(data=[go.Surface(x=X, y=Y, z=Z, colorscale='Viridis')])

fig.update_layout(
    title='Surface 3D',
    scene=dict(
        xaxis_title='X',
        yaxis_title='Y',
        zaxis_title='Z'
    )
)

fig.show()
"""

D. SUBPLOTS
-----------
"""
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np

# Créer des subplots
fig = make_subplots(
    rows=2, cols=2,
    subplot_titles=('Plot 1', 'Plot 2', 'Plot 3', 'Plot 4'),
    specs=[[{'type': 'scatter'}, {'type': 'bar'}],
           [{'type': 'scatter3d'}, {'type': 'surface'}]]
)

# Ajouter des traces à chaque subplot
x = np.linspace(0, 10, 100)

# Subplot 1,1
fig.add_trace(go.Scatter(x=x, y=np.sin(x), name='sin'), row=1, col=1)

# Subplot 1,2
fig.add_trace(go.Bar(x=['A', 'B', 'C'], y=[1, 3, 2], name='bar'), row=1, col=2)

# Subplot 2,1
fig.add_trace(go.Scatter3d(x=x, y=np.sin(x), z=x, mode='lines'), row=2, col=1)

# Subplot 2,2
X, Y = np.meshgrid(np.linspace(-5, 5, 20), np.linspace(-5, 5, 20))
Z = np.sin(np.sqrt(X**2 + Y**2))
fig.add_trace(go.Surface(z=Z, showscale=False), row=2, col=2)

fig.update_layout(height=800, showlegend=True, title_text="Subplots Variés")
fig.show()
"""


4.4 PERSONNALISATION AVANCÉE
-----------------------------

A. ANNOTATIONS
--------------
"""
import plotly.graph_objects as go
import numpy as np

x = np.linspace(0, 10, 100)
y = np.sin(x)

fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=y, mode='lines'))

# Ajouter une annotation
fig.add_annotation(
    x=np.pi/2, y=1,
    text="Maximum",
    showarrow=True,
    arrowhead=2,
    arrowsize=1,
    arrowwidth=2,
    arrowcolor="red",
    ax=40, ay=-40,
    font=dict(size=14, color="red"),
    bgcolor="yellow",
    opacity=0.8
)

# Ajouter une ligne verticale
fig.add_vline(x=5, line_dash="dash", line_color="green",
              annotation_text="Ligne verticale")

# Ajouter une ligne horizontale
fig.add_hline(y=0, line_dash="dot", line_color="blue")

# Ajouter une région
fig.add_vrect(x0=3, x1=7, fillcolor="red", opacity=0.2,
              annotation_text="Zone importante")

fig.update_layout(title="Graphique avec Annotations")
fig.show()
"""

B. BOUTONS ET MENUS
-------------------
"""
import plotly.graph_objects as go
import numpy as np

x = np.linspace(0, 10, 100)

fig = go.Figure()

# Ajouter plusieurs traces
fig.add_trace(go.Scatter(x=x, y=np.sin(x), name='sin(x)', visible=True))
fig.add_trace(go.Scatter(x=x, y=np.cos(x), name='cos(x)', visible=False))
fig.add_trace(go.Scatter(x=x, y=np.tan(x), name='tan(x)', visible=False))

# Créer des boutons
fig.update_layout(
    updatemenus=[
        dict(
            type="buttons",
            direction="left",
            buttons=list([
                dict(label="Sinus",
                     method="update",
                     args=[{"visible": [True, False, False]}]),
                dict(label="Cosinus",
                     method="update",
                     args=[{"visible": [False, True, False]}]),
                dict(label="Tangente",
                     method="update",
                     args=[{"visible": [False, False, True]}]),
                dict(label="Tout",
                     method="update",
                     args=[{"visible": [True, True, True]}])
            ]),
            pad={"r": 10, "t": 10},
            showactive=True,
            x=0.11,
            xanchor="left",
            y=1.1,
            yanchor="top"
        ),
    ]
)

fig.update_layout(title="Graphique avec Boutons")
fig.show()
"""

C. SLIDER
---------
"""
import plotly.graph_objects as go
import numpy as np

# Créer des données pour différentes valeurs
x = np.linspace(0, 10, 100)
frames = []

for k in range(1, 11):
    frames.append(go.Frame(data=[go.Scatter(x=x, y=np.sin(k*x))],
                          name=str(k)))

fig = go.Figure(
    data=[go.Scatter(x=x, y=np.sin(x))],
    frames=frames
)

# Ajouter un slider
fig.update_layout(
    updatemenus=[dict(
        type="buttons",
        buttons=[dict(label="Play",
                     method="animate",
                     args=[None, {"frame": {"duration": 500}}])]
    )],
    sliders=[dict(
        steps=[dict(method='animate',
                   args=[[f.name], {"frame": {"duration": 0}}],
                   label=f.name) for f in frames]
    )]
)

fig.show()
"""


4.5 GRAPHIQUES FINANCIERS
--------------------------
"""
import plotly.graph_objects as go
import pandas as pd
import numpy as np

# Créer des données OHLC (Open, High, Low, Close)
dates = pd.date_range('2023-01-01', periods=100)
np.random.seed(42)
open_prices = 100 + np.cumsum(np.random.randn(100))
high_prices = open_prices + np.random.rand(100) * 5
low_prices = open_prices - np.random.rand(100) * 5
close_prices = (high_prices + low_prices) / 2 + np.random.randn(100)

# Candlestick chart
fig = go.Figure(data=[go.Candlestick(
    x=dates,
    open=open_prices,
    high=high_prices,
    low=low_prices,
    close=close_prices
)])

fig.update_layout(title='Candlestick Chart', xaxis_rangeslider_visible=False)
fig.show()

# OHLC chart
fig = go.Figure(data=[go.Ohlc(
    x=dates,
    open=open_prices,
    high=high_prices,
    low=low_prices,
    close=close_prices
)])

fig.update_layout(title='OHLC Chart')
fig.show()
"""


4.6 CARTES GÉOGRAPHIQUES
-------------------------

A. SCATTER GEO
--------------
"""
import plotly.express as px

# Dataset avec coordonnées
gapminder = px.data.gapminder().query("year == 2007")

fig = px.scatter_geo(gapminder, 
                     locations="iso_alpha",
                     color="continent",
                     hover_name="country",
                     size="pop",
                     projection="natural earth",
                     title="Population Mondiale 2007")
fig.show()
"""

B. CHOROPLETH (CARTE THÉMATIQUE)
--------------------------------
"""
import plotly.express as px

gapminder = px.data.gapminder().query("year == 2007")

fig = px.choropleth(gapminder,
                    locations="iso_alpha",
                    color="lifeExp",
                    hover_name="country",
                    color_continuous_scale=px.colors.sequential.Plasma,
                    title="Espérance de Vie Mondiale")
fig.show()
"""


4.7 SAUVEGARDER LES GRAPHIQUES
-------------------------------
"""
import plotly.express as px

fig = px.scatter(px.data.iris(), x='sepal_width', y='sepal_length')

# Sauvegarder en HTML (interactif)
fig.write_html("graphique.html")

# Sauvegarder en image statique (nécessite kaleido)
# pip install kaleido --break-system-packages
fig.write_image("graphique.png")
fig.write_image("graphique.pdf")
fig.write_image("graphique.svg")
"""


================================================================================
                    PARTIE 5: BONNES PRATIQUES DE VISUALISATION
================================================================================

5.1 CHOISIR LE BON TYPE DE GRAPHIQUE
-------------------------------------

COMPARAISON:
- Bar chart (catégories)
- Box plot (distributions)

DISTRIBUTION:
- Histogram
- KDE plot
- Violin plot

RELATION:
- Scatter plot (2 variables continues)
- Line plot (temporel)
- Heatmap (corrélation)

COMPOSITION:
- Pie chart (parties d'un tout)
- Stacked bar (évolution de composition)
- Treemap (hiérarchie)

ÉVOLUTION:
- Line chart (tendance)
- Area chart (cumul)


5.2 PRINCIPES DE DESIGN
------------------------

1. SIMPLICITÉ:
   - Un graphique = un message
   - Éviter le chartjunk (décorations inutiles)
   - Ratio data-ink élevé

2. CLARTÉ:
   - Labels lisibles
   - Légendes explicites
   - Titre descriptif

3. COULEURS:
   - Maximum 5-7 couleurs distinctes
   - Contraste suffisant
   - Colorblind-friendly

4. ÉCHELLES:
   - Commencer l'axe Y à 0 pour les bars
   - Échelle appropriée (log si nécessaire)
   - Éviter les axes tronqués trompeurs

5. ANNOTATIONS:
   - Mettre en évidence les insights
   - Guider le lecteur
   - Contextualiser


5.3 PALETTE DE COULEURS RECOMMANDÉES
-------------------------------------

ACCESSIBLE (Colorblind-friendly):
- viridis, plasma, inferno, cividis
- ColorBrewer palettes

DIVERGENTE (pour données centrées):
- RdBu, RdYlBu, Spectral

SÉQUENTIELLE (pour gradients):
- Blues, YlOrRd, Greens


================================================================================
                    EXERCICE PRATIQUE ULTRA-DÉTAILLÉ
================================================================================

ÉNONCÉ:
-------
Vous êtes Data Analyst pour une chaîne de restaurants. On vous fournit le
dataset "tips" contenant les informations sur les additions et pourboires.

MISSIONS:
1. Explorer visuellement le dataset
2. Analyser la relation entre addition et pourboire
3. Comparer les comportements selon le jour et l'heure
4. Créer un dashboard interactif avec Plotly
5. Présenter vos insights avec des visualisations professionnelles

SOLUTION COMPLÈTE:
==================
"""

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots

# Configuration
sns.set_theme(style="whitegrid")
plt.rcParams['figure.figsize'] = (12, 8)

# ============================================================================
# 1. CHARGEMENT ET EXPLORATION DES DONNÉES
# ============================================================================

# Charger le dataset
tips = sns.load_dataset('tips')

print("=" * 80)
print("ANALYSE DU DATASET TIPS")
print("=" * 80)
print("\n[GRAPHIQUE] Aperçu des données:")
print(tips.head(10))

print("\n[HAUSSE] Informations générales:")
print(tips.info())

print("\n[BAISSE] Statistiques descriptives:")
print(tips.describe())

print("\n[RECHERCHE] Valeurs manquantes:")
print(tips.isnull().sum())

# ============================================================================
# 2. DISTRIBUTION DES VARIABLES (MATPLOTLIB)
# ============================================================================

fig, axes = plt.subplots(2, 2, figsize=(15, 12))

# Distribution de l'addition totale
axes[0, 0].hist(tips['total_bill'], bins=30, edgecolor='black', 
                color='steelblue', alpha=0.7)
axes[0, 0].set_xlabel('Addition Totale ($)', fontsize=12)
axes[0, 0].set_ylabel('Fréquence', fontsize=12)
axes[0, 0].set_title('Distribution des Additions', fontsize=14, fontweight='bold')
axes[0, 0].axvline(tips['total_bill'].mean(), color='red', linestyle='--', 
                   linewidth=2, label=f'Moyenne: ${tips["total_bill"].mean():.2f}')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)

# Distribution des pourboires
axes[0, 1].hist(tips['tip'], bins=30, edgecolor='black', 
                color='coral', alpha=0.7)
axes[0, 1].set_xlabel('Pourboire ($)', fontsize=12)
axes[0, 1].set_ylabel('Fréquence', fontsize=12)
axes[0, 1].set_title('Distribution des Pourboires', fontsize=14, fontweight='bold')
axes[0, 1].axvline(tips['tip'].mean(), color='red', linestyle='--', 
                   linewidth=2, label=f'Moyenne: ${tips["tip"].mean():.2f}')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# Nombre de clients par jour
day_counts = tips['day'].value_counts()
axes[1, 0].bar(day_counts.index, day_counts.values, color='lightgreen', 
               edgecolor='black', alpha=0.7)
axes[1, 0].set_xlabel('Jour', fontsize=12)
axes[1, 0].set_ylabel('Nombre de clients', fontsize=12)
axes[1, 0].set_title('Fréquentation par Jour', fontsize=14, fontweight='bold')
axes[1, 0].grid(True, alpha=0.3, axis='y')

# Taux de pourboire (%)
tips['tip_percent'] = (tips['tip'] / tips['total_bill']) * 100
axes[1, 1].hist(tips['tip_percent'], bins=30, edgecolor='black', 
                color='gold', alpha=0.7)
axes[1, 1].set_xlabel('Taux de Pourboire (%)', fontsize=12)
axes[1, 1].set_ylabel('Fréquence', fontsize=12)
axes[1, 1].set_title('Distribution du Taux de Pourboire', fontsize=14, fontweight='bold')
axes[1, 1].axvline(tips['tip_percent'].mean(), color='red', linestyle='--', 
                   linewidth=2, label=f'Moyenne: {tips["tip_percent"].mean():.1f}%')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('/home/claude/visualisation_distributions.png', dpi=300, bbox_inches='tight')
plt.show()

# ============================================================================
# 3. ANALYSE DES RELATIONS (SEABORN)
# ============================================================================

# 3.1 Relation Addition-Pourboire
fig, axes = plt.subplots(1, 2, figsize=(16, 6))

# Scatter avec regression
sns.regplot(data=tips, x='total_bill', y='tip', ax=axes[0],
            scatter_kws={'alpha': 0.6, 's': 50}, line_kws={'color': 'red', 'linewidth': 2})
axes[0].set_xlabel('Addition Totale ($)', fontsize=12)
axes[0].set_ylabel('Pourboire ($)', fontsize=12)
axes[0].set_title('Relation Addition-Pourboire', fontsize=14, fontweight='bold')
axes[0].grid(True, alpha=0.3)

# Calculer et afficher la corrélation
correlation = tips['total_bill'].corr(tips['tip'])
axes[0].text(0.05, 0.95, f'Corrélation: {correlation:.3f}', 
             transform=axes[0].transAxes, fontsize=12,
             verticalalignment='top', bbox=dict(boxstyle='round', 
             facecolor='wheat', alpha=0.5))

# Joint plot style avec hexbin
sns.histplot(data=tips, x='total_bill', y='tip', bins=20, cmap='YlOrRd', ax=axes[1])
axes[1].set_xlabel('Addition Totale ($)', fontsize=12)
axes[1].set_ylabel('Pourboire ($)', fontsize=12)
axes[1].set_title('Densité Bivariée', fontsize=14, fontweight='bold')

plt.tight_layout()
plt.savefig('/home/claude/visualisation_relation.png', dpi=300, bbox_inches='tight')
plt.show()

# 3.2 Comparaisons par catégories
fig, axes = plt.subplots(2, 2, figsize=(16, 12))

# Box plot par jour
sns.boxplot(data=tips, x='day', y='total_bill', palette='Set2', ax=axes[0, 0])
axes[0, 0].set_xlabel('Jour', fontsize=12)
axes[0, 0].set_ylabel('Addition Totale ($)', fontsize=12)
axes[0, 0].set_title('Distribution des Additions par Jour', fontsize=14, fontweight='bold')
axes[0, 0].grid(True, alpha=0.3, axis='y')

# Violin plot par moment de la journée
sns.violinplot(data=tips, x='time', y='tip', hue='sex', split=True, 
               palette='muted', ax=axes[0, 1])
axes[0, 1].set_xlabel('Moment', fontsize=12)
axes[0, 1].set_ylabel('Pourboire ($)', fontsize=12)
axes[0, 1].set_title('Pourboires par Moment et Sexe', fontsize=14, fontweight='bold')
axes[0, 1].grid(True, alpha=0.3, axis='y')

# Swarm plot: Taille du groupe vs Addition
sns.swarmplot(data=tips, x='size', y='total_bill', hue='time', 
              palette='coolwarm', ax=axes[1, 0])
axes[1, 0].set_xlabel('Taille du Groupe', fontsize=12)
axes[1, 0].set_ylabel('Addition Totale ($)', fontsize=12)
axes[1, 0].set_title('Addition selon la Taille du Groupe', fontsize=14, fontweight='bold')
axes[1, 0].grid(True, alpha=0.3, axis='y')

# Heatmap de la moyenne des additions
pivot = tips.pivot_table(values='total_bill', index='day', columns='time', aggfunc='mean')
sns.heatmap(pivot, annot=True, fmt='.2f', cmap='YlGnBu', 
            cbar_kws={'label': 'Addition Moyenne ($)'}, ax=axes[1, 1])
axes[1, 1].set_title('Addition Moyenne par Jour et Moment', fontsize=14, fontweight='bold')

plt.tight_layout()
plt.savefig('/home/claude/visualisation_comparaisons.png', dpi=300, bbox_inches='tight')
plt.show()

# ============================================================================
# 4. PAIRPLOT POUR VUE D'ENSEMBLE
# ============================================================================

g = sns.pairplot(tips, hue='time', diag_kind='kde', corner=True,
                 plot_kws={'alpha': 0.6}, height=3)
g.fig.suptitle('Matrice de Relations - Dataset Tips', y=1.02, fontsize=16, fontweight='bold')
plt.savefig('/home/claude/visualisation_pairplot.png', dpi=300, bbox_inches='tight')
plt.show()

# ============================================================================
# 5. DASHBOARD INTERACTIF PLOTLY
# ============================================================================

# Créer un dashboard avec subplots
fig = make_subplots(
    rows=3, cols=2,
    subplot_titles=(
        'Relation Addition-Pourboire',
        'Distribution des Additions',
        'Pourboires par Jour',
        'Évolution Temporelle',
        'Taux de Pourboire',
        'Composition par Moment'
    ),
    specs=[
        [{'type': 'scatter'}, {'type': 'histogram'}],
        [{'type': 'bar'}, {'type': 'scatter'}],
        [{'type': 'box'}, {'type': 'pie'}]
    ],
    vertical_spacing=0.12,
    horizontal_spacing=0.1
)

# 1. Scatter plot
for time in tips['time'].unique():
    data = tips[tips['time'] == time]
    fig.add_trace(
        go.Scatter(
            x=data['total_bill'],
            y=data['tip'],
            mode='markers',
            name=time,
            marker=dict(size=8, opacity=0.6)
        ),
        row=1, col=1
    )

# 2. Histogram
fig.add_trace(
    go.Histogram(
        x=tips['total_bill'],
        nbinsx=30,
        name='Additions',
        marker_color='steelblue'
    ),
    row=1, col=2
)

# 3. Bar chart par jour
day_avg = tips.groupby('day')['tip'].mean().reset_index()
fig.add_trace(
    go.Bar(
        x=day_avg['day'],
        y=day_avg['tip'],
        name='Pourboire Moyen',
        marker_color='coral'
    ),
    row=2, col=1
)

# 4. Scatter temporel (simulation)
tips_sorted = tips.sort_values('total_bill').reset_index()
tips_sorted['index'] = range(len(tips_sorted))
fig.add_trace(
    go.Scatter(
        x=tips_sorted['index'],
        y=tips_sorted['total_bill'],
        mode='lines+markers',
        name='Additions',
        line=dict(color='green', width=2)
    ),
    row=2, col=2
)

# 5. Box plot du taux de pourboire
for day in tips['day'].unique():
    data = tips[tips['day'] == day]
    fig.add_trace(
        go.Box(
            y=data['tip_percent'],
            name=day,
            boxmean='sd'
        ),
        row=3, col=1
    )

# 6. Pie chart
time_counts = tips['time'].value_counts()
fig.add_trace(
    go.Pie(
        labels=time_counts.index,
        values=time_counts.values,
        hole=0.3
    ),
    row=3, col=2
)

# Mise à jour du layout
fig.update_layout(
    title_text="Dashboard d'Analyse des Pourboires",
    title_font_size=20,
    height=1200,
    showlegend=True,
    hovermode='closest'
)

# Axes labels
fig.update_xaxes(title_text="Addition Totale ($)", row=1, col=1)
fig.update_yaxes(title_text="Pourboire ($)", row=1, col=1)
fig.update_xaxes(title_text="Addition ($)", row=1, col=2)
fig.update_xaxes(title_text="Jour", row=2, col=1)
fig.update_yaxes(title_text="Pourboire Moyen ($)", row=2, col=1)
fig.update_xaxes(title_text="Index", row=2, col=2)
fig.update_yaxes(title_text="Addition ($)", row=2, col=2)
fig.update_yaxes(title_text="Taux de Pourboire (%)", row=3, col=1)

# Sauvegarder le dashboard
fig.write_html('/home/claude/dashboard_interactif.html')
fig.show()

# ============================================================================
# 6. ANALYSE STATISTIQUE ET INSIGHTS
# ============================================================================

print("\n" + "=" * 80)
print("[GRAPHIQUE] INSIGHTS ET STATISTIQUES CLÉS")
print("=" * 80)

# Statistiques générales
print("\n1. STATISTIQUES GÉNÉRALES:")
print(f"   - Addition moyenne: ${tips['total_bill'].mean():.2f}")
print(f"   - Pourboire moyen: ${tips['tip'].mean():.2f}")
print(f"   - Taux de pourboire moyen: {tips['tip_percent'].mean():.2f}%")
print(f"   - Nombre total de clients: {len(tips)}")

# Analyse par jour
print("\n2. ANALYSE PAR JOUR:")
day_stats = tips.groupby('day').agg({
    'total_bill': ['mean', 'count'],
    'tip': 'mean',
    'tip_percent': 'mean'
}).round(2)
print(day_stats)

# Analyse par moment
print("\n3. ANALYSE PAR MOMENT DE LA JOURNÉE:")
time_stats = tips.groupby('time').agg({
    'total_bill': ['mean', 'count'],
    'tip': 'mean',
    'tip_percent': 'mean'
}).round(2)
print(time_stats)

# Meilleurs pourboires
print("\n4. TOP 5 DES MEILLEURS POURBOIRES:")
top_tips = tips.nlargest(5, 'tip')[['total_bill', 'tip', 'tip_percent', 'day', 'time']]
print(top_tips)

# Analyse fumeurs vs non-fumeurs
print("\n5. FUMEURS VS NON-FUMEURS:")
smoker_stats = tips.groupby('smoker').agg({
    'total_bill': 'mean',
    'tip': 'mean',
    'tip_percent': 'mean'
}).round(2)
print(smoker_stats)

print("\n" + "=" * 80)
print("[OK] ANALYSE TERMINÉE - FICHIERS SAUVEGARDÉS")
print("=" * 80)

"""
FICHIERS GÉNÉRÉS:
-----------------
1. visualisation_distributions.png - Distributions des variables principales
2. visualisation_relation.png - Analyse de la relation addition-pourboire
3. visualisation_comparaisons.png - Comparaisons par catégories
4. visualisation_pairplot.png - Vue d'ensemble des relations
5. dashboard_interactif.html - Dashboard Plotly interactif

INSIGHTS PRINCIPAUX:
--------------------
1. Forte corrélation positive entre addition et pourboire (r ≈ 0.68)
2. Le dîner génère des additions plus élevées que le déjeuner
3. Les weekends (Sam-Dim) sont les jours les plus fréquentés
4. Le taux de pourboire moyen est d'environ 16%
5. Les groupes plus grands tendent à avoir des additions plus élevées

RECOMMANDATIONS:
----------------
1. Optimiser le staff pour les weekends
2. Promouvoir les services du dîner
3. Former le personnel sur le service pour maximiser les pourboires
4. Analyser les outliers (pourboires exceptionnellement bas/élevés)
"""

print("\n[BRAVO] Exercice terminé avec succès!")

================================================================================
                         FIN DU CHAPITRE 1
================================================================================

RÉSUMÉ DES COMPÉTENCES ACQUISES:
[OK] Maîtrise de Matplotlib (plots, subplots, personnalisation)
[OK] Utilisation avancée de Seaborn (distributions, relations, catégories)
[OK] Création de graphiques interactifs avec Plotly
[OK] Choix du bon type de visualisation selon les données
[OK] Application des bonnes pratiques de data visualization
[OK] Création de dashboards professionnels

PROCHAINES ÉTAPES:
[DOCS] Chapitre 2: Machine Learning avec Scikit-learn
[DOCS] Chapitre 3: Deep Learning (TensorFlow/PyTorch)

================================================================================