Python Débutant

Python pour la Data Science & l'Intelligence Artificielle : Maîtrisez les Fondations qui Font la Différence

Python est aujourd'hui le langage le plus utilisé au monde en Intelligence Artificielle et en Data Science — non pas par hasard, mais parce qu'il transforme des problèmes complexes en solutions lisibles. Ce cours vous amène de la toute première ligne de code jusqu'aux structures de données et aux outils analytiques utilisés quotidiennement par les ingénieurs en IA. Vous construirez des réflexes solides, comprendrez le pourquoi derrière chaque concept, et manipulerez de vraies données dès les premières sections. À la fin, vous serez capable de lire, écrire et déboguer du code Python autonomement, et de vous lancer dans votre premier projet d'analyse de données.

Preparetoi.academy 83 min

Python pour la Data Science & l'Intelligence Artificielle : Maîtrisez les Fondations qui Font la Différence

Python est aujourd'hui le langage le plus utilisé au monde en Intelligence Artificielle et en Data Science — non pas par hasard, mais parce qu'il transforme des problèmes complexes en solutions lisibles. Ce cours vous amène de la toute première ligne de code jusqu'aux structures de données et aux outils analytiques utilisés quotidiennement par les ingénieurs en IA. Vous construirez des réflexes solides, comprendrez le pourquoi derrière chaque concept, et manipulerez de vraies données dès les premières sections. À la fin, vous serez capable de lire, écrire et déboguer du code Python autonomement, et de vous lancer dans votre premier projet d'analyse de données.


Sommaire

  1. L'Environnement Python : Installer, Comprendre, Exécuter
  2. Variables et Types de Données : Les Conteneurs de l'Information
  3. Opérateurs : Calculer, Comparer, Décider
  4. Chaînes de Caractères : Manipuler le Texte comme un Pro
  5. Structures Conditionnelles : Enseigner à Python à Prendre des Décisions
  6. Boucles for : Répéter sans se Fatiguer
  7. Boucles while : Continuer jusqu'à la Condition
  8. Fonctions : Fabriquer ses Propres Outils
  9. Listes : La Structure de Données la Plus Polyvalente
  10. Tuples et Sets : Données Immuables et Ensembles Uniques
  11. Dictionnaires : La Carte d'Identité de vos Données
  12. Compréhensions : Écrire Plus en Disant Moins
  13. Gestion des Fichiers : Lire et Écrire des Données Réelles
  14. Gestion des Erreurs et Exceptions : Rendre le Code Robuste
  15. Modules et Packages : Utiliser l'Écosystème Python
  16. NumPy : Le Moteur Mathématique de la Data Science
  17. Pandas : Analyser des Tableaux de Données comme un Data Analyst
  18. Visualisation avec Matplotlib : Transformer les Chiffres en Images
  19. Introduction à Scikit-learn : Votre Premier Modèle de Machine Learning
  20. Bonnes Pratiques et Code Pythonique : Écrire du Code Professionnel

1. L'Environnement Python : Installer, Comprendre, Exécuter

Définition approfondie

L'environnement Python est l'ensemble des outils, fichiers et configurations qui permettent d'écrire du code Python et de l'exécuter sur une machine. Il comprend trois composants essentiels : l'interpréteur Python (le moteur qui lit et exécute votre code), l'éditeur de code ou IDE (l'interface dans laquelle vous écrivez), et le gestionnaire de paquets pip (l'outil qui installe des bibliothèques supplémentaires). Contrairement à d'autres langages comme Java ou C++, Python n'a pas besoin d'être "compilé" avant d'être lancé : il est interprété ligne par ligne, ce qui facilite énormément les tests et l'apprentissage.

En Data Science, l'environnement de référence est Anaconda + Jupyter Notebook. Anaconda est une distribution Python qui préinstalle des centaines de bibliothèques scientifiques (NumPy, Pandas, Matplotlib, Scikit-learn). Jupyter Notebook est un environnement interactif où chaque "cellule" de code peut être exécutée indépendamment, ce qui est idéal pour l'exploration de données.

Analogie : la cuisine équipée

Imaginez que Python est comme une cuisine. L'interpréteur Python est le fourneau (sans lui, vous ne pouvez rien cuire). pip est le supermarché où vous allez chercher des ingrédients spéciaux (bibliothèques). Anaconda est le kit de cuisine complet tout-en-un : vous arrivez dans une cuisine déjà équipée de couteaux, casseroles et épices de base. Et Jupyter Notebook, c'est le plan de travail où vous préparez vos recettes étape par étape, en goûtant à chaque ajout.

Bloc de code commenté

# =============================================================
# Vérification et prise en main de l'environnement Python
# =============================================================

# Afficher la version de Python utilisée
import sys
print(sys.version)
# Résultat attendu : 3.11.x ou similaire

# Vérifier les bibliothèques installées
import pkg_resources
# Lister les 5 premières bibliothèques installées
installed = sorted(pkg_resources.working_set, key=lambda x: x.project_name)
for lib in installed[:5]:
    print(f"{lib.project_name} == {lib.version}")

# Premier programme classique revisité pour la Data Science
# On calcule le nombre de jours de données qu'on veut analyser
jours_de_donnees = 365  # une année complète
print(f"Prêt à analyser {jours_de_donnees} jours de données !")

# Vérifier si une bibliothèque clé est disponible
try:
    import pandas          # tentative d'import de pandas
    print("Pandas est disponible ✓")
except ImportError:        # si pandas n'est pas installé
    print("Pandas manquant — lancez : pip install pandas")

Cas d'usage réel

Un data analyst qui rejoint une nouvelle entreprise doit d'abord configurer son environnement. Il installe Anaconda, crée un environnement virtuel dédié au projet (conda create -n projet_ventes python=3.11), puis installe les dépendances spécifiques de l'entreprise. Cette isolation garantit que les versions des bibliothèques du projet ne conflictuent pas avec d'autres projets sur la même machine.

Tableau comparatif : Environnements de développement Python

Outil Usage principal Idéal pour Inconvénient
IDLE Édition simple Débutants absolus Très limité
VS Code Développement complet Scripts, projets Configuration manuelle
Jupyter Notebook Exploration interactive Data Science, analyse Pas adapté aux gros projets
PyCharm Développement professionnel Applications complètes Lourd en ressources
Google Colab Cloud, sans installation Premiers pas rapides Limité sans compte payant
Anaconda + JupyterLab Data Science complète ML, analyse de données Installation volumineuse

Astuce : Pour la Data Science, commencez directement avec Google Colab (colab.research.google.com) — pas d'installation, Python et toutes les bibliothèques ML déjà disponibles, et vous pouvez partager vos notebooks comme un Google Doc.

Attention : N'installez jamais des bibliothèques directement dans l'environnement Python "de base" de votre système. Utilisez toujours des environnements virtuels (conda env ou venv) pour chaque projet. Mélanger les dépendances de projets différents est l'une des causes les plus fréquentes de bugs impossibles à diagnostiquer.


2. Variables et Types de Données : Les Conteneurs de l'Information

Définition approfondie

Une variable en Python est un nom symbolique qui pointe vers une valeur stockée en mémoire. Contrairement à d'autres langages comme Java, Python est dynamiquement typé : vous n'avez pas besoin de déclarer le type d'une variable avant de l'utiliser, Python le détermine automatiquement à l'exécution. Les types de données fondamentaux sont : int (entiers), float (décimaux), str (chaînes de caractères), bool (booléens vrai/faux), et NoneType (absence de valeur). En Data Science, s'y ajoutent des types complexes comme les tableaux NumPy et les DataFrames Pandas — mais tous reposent sur ces types primitifs.

La manière dont Python gère la mémoire est unique : quand vous écrivez a = 5, Python crée un objet de valeur 5 en mémoire, puis fait pointer le nom a vers cet objet. Si vous écrivez ensuite b = a, les deux noms pointent vers le même objet. Cette distinction entre "nom" et "objet" est capitale pour comprendre les comportements parfois surprenants des listes et dictionnaires.

Analogie : les étiquettes dans un entrepôt

Imaginez un immense entrepôt de données. Chaque boîte dans cet entrepôt contient une valeur (le chiffre 42, le texte "Alice", etc.). Une variable est simplement une étiquette que vous collez sur une boîte pour la retrouver facilement. Vous pouvez enlever l'étiquette d'une boîte et la coller sur une autre — la boîte originale existe toujours, mais votre étiquette pointe maintenant ailleurs. C'est exactement ce qui se passe quand vous réassignez une variable.

Bloc de code commenté

# =============================================================
# Variables et types de données dans un contexte Data Science
# =============================================================

# --- Types numériques ---
nb_observations = 150000       # int : nombre entier d'enregistrements
taux_precision = 0.9423        # float : taux de précision d'un modèle ML
nombre_features = 28           # int : nombre de colonnes dans notre dataset

# --- Types textuels ---
nom_dataset = "MNIST Digits"   # str : nom du jeu de données
chemin_fichier = "/data/train.csv"  # str : chemin vers un fichier

# --- Booléens ---
modele_entraine = False        # bool : le modèle n'est pas encore entraîné
donnees_nettoyees = True       # bool : les données ont été prétraitées

# --- NoneType : absence de valeur ---
resultat_prediction = None     # NoneType : on n'a pas encore de prédiction

# --- Vérification des types avec type() ---
print(type(nb_observations))   # <class 'int'>
print(type(taux_precision))    # <class 'float'>
print(type(nom_dataset))       # <class 'str'>
print(type(modele_entraine))   # <class 'bool'>

# --- Conversion de types (casting) ---
score_en_texte = str(taux_precision)     # 0.9423 → "0.9423"
score_en_pourcentage = int(taux_precision * 100)  # 0.9423 → 94

# --- Affectation multiple — utile pour initialiser plusieurs compteurs ---
vrais_positifs, faux_positifs, faux_negatifs = 120, 15, 8

# --- Vérification de l'existence d'une valeur ---
if resultat_prediction is None:
    print("Le modèle n'a pas encore été appliqué")

Cas d'usage réel

Dans un pipeline de traitement de données de santé, chaque variable a un type précis : le nombre de patients est un int, le taux de cholestérol est un float, le nom du service hospitalier est un str, et un indicateur "patient à risque" est un bool. Si une donnée manque dans le dossier patient, elle est représentée par None (pas par 0 ou "" qui sont des valeurs significatives). Cette distinction rigoureuse des types évite des erreurs de calcul catastrophiques dans un contexte médical.

Tableau comparatif : Types de données Python fondamentaux

Type Exemple Taille mémoire Usage en Data Science
int 42, -7, 1_000_000 Variable Compteurs, indices, IDs
float 3.14, 0.001, 1e-5 64 bits Métriques, probabilités, mesures
str "Paris", '2024' Variable Labels, catégories, texte brut
bool True, False 28 bytes Filtres, flags, masques binaires
NoneType None 16 bytes Valeurs manquantes, résultats vides
complex 3+4j 32 bytes Signal processing, transformée de Fourier

Astuce : Pour écrire de grands nombres entiers sans les confondre, utilisez le séparateur underscore de Python : population = 8_100_000_000 est parfaitement valide et bien plus lisible que population = 8100000000.

Attention : En Python, 0.1 + 0.2 ne donne pas exactement 0.3 à cause de la représentation binaire des flottants. Ce n'est pas un bug Python, c'est une caractéristique universelle de l'informatique. En analyse financière ou scientifique, utilisez le module decimal pour des calculs exacts : from decimal import Decimal.


3. Opérateurs : Calculer, Comparer, Décider

Définition approfondie

Les opérateurs sont les symboles qui permettent d'effectuer des opérations sur les valeurs et variables. Python distingue cinq familles d'opérateurs : arithmétiques (calculs mathématiques), de comparaison (produisent un booléen), logiques (combinent des conditions), d'appartenance (vérifient la présence d'un élément), et d'identité (vérifient si deux noms pointent vers le même objet). En Data Science, les opérateurs arithmétiques sont utilisés constamment pour normaliser des données, calculer des métriques, ou dériver de nouvelles colonnes. Les opérateurs de comparaison servent à filtrer des lignes d'un dataset.

Un opérateur souvent méconnu mais essentiel est // (division entière) et % (modulo). Le modulo retourne le reste d'une division : 17 % 5 donne 2 car 17 = 5×3 + 2. En Machine Learning, % est utilisé pour diviser un dataset en batches de taille fixe.

Analogie : les règles du jeu de cartes

Les opérateurs arithmétiques sont comme les règles de calcul de score dans un jeu de cartes : addition des points, multiplication des bonus. Les opérateurs de comparaison sont les arbitres : "est-ce que ma main est meilleure que la tienne ?" (>, <, ==). Les opérateurs logiques sont les stratèges : "je joue cette carte SI j'ai plus de 10 points ET qu'il me reste au moins 3 cartes" (and, or).

Bloc de code commenté

# =============================================================
# Opérateurs dans le contexte d'un pipeline d'analyse de données
# =============================================================

# --- Opérateurs arithmétiques ---
nb_total = 10000         # total d'échantillons dans le dataset
nb_train = 8000          # échantillons d'entraînement
nb_test = nb_total - nb_train   # soustraction : 2000 échantillons de test

ratio_train = nb_train / nb_total   # division classique : 0.8 (80%)
ratio_arrondi = nb_train // nb_total  # division entière : 0 (pas utile ici)

# Calcul de normalisation Min-Max (très courant en ML)
valeur = 75.0
valeur_min = 20.0
valeur_max = 100.0
valeur_normalisee = (valeur - valeur_min) / (valeur_max - valeur_min)
print(f"Valeur normalisée : {valeur_normalisee:.4f}")  # 0.6875

# Puissance : calcul de l'erreur quadratique moyenne (MSE)
prediction = 82.5
reel = 80.0
erreur_carree = (prediction - reel) ** 2   # ** = puissance
print(f"Erreur au carré : {erreur_carree}")  # 6.25

# Modulo : découper un dataset en batches de 32
nb_echantillons = 1050
taille_batch = 32
nb_batches_complets = nb_echantillons // taille_batch    # 32 batches
reste = nb_echantillons % taille_batch                    # 26 échantillons orphelins
print(f"{nb_batches_complets} batches complets, {reste} restants")

# --- Opérateurs de comparaison ---
precision = 0.87
seuil_production = 0.90
print(precision >= seuil_production)   # False : modèle pas prêt pour la prod

# --- Opérateurs logiques ---
donnees_completes = True
modele_valide = False
# AND : les deux conditions doivent être vraies
pret_pour_deploy = donnees_completes and modele_valide  # False
# OR : au moins une condition doit être vraie
continuer_pipeline = donnees_completes or modele_valide  # True
# NOT : inversion
print(not modele_valide)   # True

# --- Opérateurs d'appartenance ---
langages_supportes = ["Python", "R", "Julia", "SQL"]
print("Python" in langages_supportes)   # True
print("MATLAB" not in langages_supportes)  # True

Cas d'usage réel

Dans un système de scoring de crédit bancaire, les opérateurs permettent de calculer un score composite : score = (revenus * 0.3) + (historique_credit * 0.4) + (anciennete_emploi * 0.3). Puis des opérateurs de comparaison filtrent les dossiers : accord_credit = score >= 650 and ratio_endettement < 0.35. Ces deux lignes de code traduisent directement une politique de risque en logique exécutable.

Tableau comparatif : Opérateurs Python essentiels

Famille Opérateur Exemple Résultat Usage ML/Data
Arithmétique +, -, *, / 100 * 0.8 80.0 Calculs de métriques
Arithmétique ** 2 ** 8 256 Puissances, normes
Arithmétique //, % 100 // 32 3 Batches, pagination
Comparaison ==, != val == None True/False Filtres de données
Comparaison >, <, >=, <= score >= 0.9 True/False Seuils de décision
Logique and, or, not a and b True/False Conditions multiples
Appartenance in, not in "x" in liste True/False Validation de valeurs

Astuce : Python possède un opérateur puissant souvent ignoré : l'opérateur ternaire (ou expression conditionnelle). Plutôt que d'écrire un if/else sur 4 lignes, vous pouvez écrire : label = "positif" if score > 0.5 else "négatif". Très utilisé dans les pipelines de transformation de données.

Attention : Ne confondez jamais = (affectation) avec == (comparaison d'égalité). a = 5 stocke 5 dans a. a == 5 teste si a vaut 5 et renvoie True ou False. Cette confusion est l'erreur de syntaxe la plus fréquente chez les débutants.


4. Chaînes de Caractères : Manipuler le Texte comme un Pro

Définition approfondie

En Python, une chaîne de caractères (str) est une séquence immuable de caractères Unicode. "Immuable" signifie que vous ne pouvez pas modifier un caractère d'une chaîne existante — vous en créez une nouvelle à chaque transformation. Python propose une richesse exceptionnelle de méthodes natives pour manipuler les chaînes : découpage, recherche, remplacement, formatage, division. En Data Science et NLP (Traitement du Langage Naturel), les chaînes sont partout : noms de colonnes, labels de catégories, données textuelles brutes, chemins de fichiers, requêtes SQL générées dynamiquement.

Python offre trois syntaxes de formatage. Les f-strings (depuis Python 3.6) sont aujourd'hui le standard recommandé : f"La précision est {score:.2%}". Elles permettent d'intégrer des expressions Python directement dans le texte, avec un contrôle fin du format d'affichage.

Analogie : le moulinage du tissu

Une chaîne de caractères est comme un tissu : vous pouvez le couper (slicing), le mesurer (len()), chercher un motif dedans (find()), remplacer une couleur (replace()), ou le plier/assembler avec d'autres morceaux (+, join()). Mais vous ne pouvez pas modifier un fil individuellement — il faut recréer le tissu entier avec le changement intégré.

Bloc de code commenté

# =============================================================
# Manipulation de chaînes — Prétraitement de données textuelles
# =============================================================

# Données brutes telles qu'elles arrivent d'une API ou d'un CSV
entree_brute = "  Paris, France  |  48.8566° N, 2.3522° E  "

# --- Nettoyage de base ---
propre = entree_brute.strip()          # supprime les espaces en début/fin
print(propre)                           # "Paris, France  |  48.8566° N, 2.3522° E"

# --- Séparation sur un délimiteur ---
parties = propre.split(" | ")          # découpe sur " | "
ville_pays = parties[0]                # "Paris, France"
coordonnees = parties[1]               # "48.8566° N, 2.3522° E"

# --- Extraction de sous-informations ---
ville, pays = ville_pays.split(", ")   # découpe sur ", "
print(ville)    # "Paris"
print(pays)     # "France"

# --- Normalisation (uniformiser la casse) ---
pays_normalise = pays.strip().lower().replace(" ", "_")  # "france"
print(pays_normalise)   # utile pour créer des clés de dictionnaire cohérentes

# --- Vérification de contenu ---
print("Paris" in entree_brute)          # True
print(entree_brute.startswith("  Par")) # True — commence par des espaces puis "Par"
print(ville.upper())                    # "PARIS"

# --- F-string avec formatage avancé ---
lat = 48.8566
lon = 2.3522
rapport = (
    f"Ville        : {ville}\n"
    f"Pays         : {pays}\n"
    f"Latitude     : {lat:.4f}°\n"  # 4 décimales
    f"Longitude    : {lon:.4f}°\n"
    f"Clé dataset  : {pays_normalise}_{ville.lower()}"
)
print(rapport)

# --- Opérations utiles en data cleaning ---
colonne_brute = "  Revenue_Q3 (USD)  "
# Nettoyer un nom de colonne pour l'utiliser comme clé
colonne_propre = (
    colonne_brute
    .strip()            # supprime espaces
    .lower()            # minuscules
    .replace(" ", "_")  # espaces → underscores
    .replace("(", "")   # supprime parenthèses
    .replace(")", "")
)
print(colonne_propre)  # "revenue_q3_usd"

Cas d'usage réel

Dans un projet de sentiment analysis sur des avis clients d'e-commerce, chaque commentaire brut doit être prétraité avant d'être passé à un modèle : suppression des balises HTML résiduelles (replace("<br>", " ")), normalisation de la casse (lower()), suppression des URLs (re.sub(r"https?://\S+", "", texte)), et tokenisation (split()). Ce prétraitement textuel, souvent sous-estimé, représente 40 à 60% du temps de travail réel en NLP.

Tableau comparatif : Méthodes de chaînes les plus utiles en Data Science

Méthode Syntaxe Résultat Cas d'usage
strip() " x ".strip() "x" Nettoyage de données CSV
split() "a,b,c".split(",") ["a","b","c"] Parsing de données délimitées
replace() "a_b".replace("_"," ") "a b" Nettoyage de noms de colonnes
lower()/upper() "ABC".lower() "abc" Normalisation de catégories
startswith() "ID_123".startswith("ID") True Filtrage par préfixe
join() ",".join(["a","b"]) "a,b" Reconstruction de chaînes
find() "hello".find("ll") 2 Détection de sous-chaînes
format()/f-string f"{val:.2f}" "3.14" Rapports formatés

Astuce : La méthode join() est bien plus efficace que la concaténation en boucle (+) pour assembler une grande liste de chaînes. "".join(liste_de_textes) est jusqu'à 10 fois plus rapide que texte += element dans une boucle, car chaque + crée un nouvel objet en mémoire.

Attention : Les chaînes Python sont immuables. mon_texte[0] = "X" lève une TypeError. Pour "modifier" une chaîne, créez-en une nouvelle : mon_texte = "X" + mon_texte[1:]. Si vous effectuez des milliers de modifications textuelles, cette immutabilité peut impacter les performances — utilisez alors io.StringIO comme tampon.


5. Structures Conditionnelles : Enseigner à Python à Prendre des Décisions

Définition approfondie

Les structures conditionnelles permettent à un programme d'exécuter différents blocs de code selon des conditions. Python utilise les mots-clés if, elif (contraction de "else if"), et else. La syntaxe Python est distinctive : pas de parenthèses obligatoires, pas d'accolades — l'indentation (4 espaces) délimite les blocs. Cette règle d'indentation n'est pas stylistique, elle est syntaxiquement obligatoire. Un manque ou un excès d'indentation provoque une erreur immédiate.

En Data Science, les conditionnelles structurent des règles métier : qualifier un modèle pour la production, classer des données dans des catégories, gérer des cas particuliers dans un pipeline (données manquantes, valeurs aberrantes, formats inattendus).

Analogie : le triage aux urgences

Un médecin aux urgences pratique un triage conditionnel : if fréquence cardiaque > 120 OU saturation O2 < 90 → priorité critique ; elif douleur thoracique → priorité haute ; elif fièvre > 39° → priorité moyenne ; else → salle d'attente normale. Ce triage médical est exactement la logique d'un if/elif/else Python : chaque patient suit un seul chemin selon les conditions évaluées dans l'ordre.

Bloc de code commenté

# =============================================================
# Conditionnelles — Système de qualification de modèles ML
# =============================================================

# Métriques d'un modèle de classification binaire
precision = 0.91          # proportion de prédictions positives correctes
rappel = 0.78             # proportion de vrais positifs détectés
f1_score = 0.843          # moyenne harmonique de précision et rappel
taille_dataset = 500      # nombre d'échantillons de test

# --- Décision de déploiement multi-critères ---
if taille_dataset < 100:
    # Les statistiques ne sont pas fiables avec moins de 100 échantillons
    decision = "INVALIDE"
    raison = "Dataset de test trop petit pour être fiable"

elif precision >= 0.95 and rappel >= 0.90:
    # Modèle excellent sur les deux métriques clés
    decision = "DÉPLOIEMENT IMMÉDIAT"
    raison = "Performances remarquables en précision ET rappel"

elif precision >= 0.90 and f1_score >= 0.85:
    # Bon équilibre global — cas le plus courant
    decision = "DÉPLOIEMENT AVEC MONITORING"
    raison = "Bonnes performances globales, surveiller la dérive"

elif precision >= 0.85 or f1_score >= 0.80:
    # Performances acceptables mais perfectibles
    decision = "TEST A/B RECOMMANDÉ"
    raison = "Performances acceptables, valider en production partielle"

else:
    # Performances insuffisantes
    decision = "RETOUR EN ENTRAÎNEMENT"
    raison = f"Métriques insuffisantes (F1={f1_score:.3f})"

# Affichage structuré du rapport de décision
print(f"{'='*45}")
print(f"DÉCISION    : {decision}")
print(f"JUSTIFICATION : {raison}")
print(f"{'='*45}")

# --- Condition imbriquée pour un cas particulier ---
if decision == "DÉPLOIEMENT AVEC MONITORING":
    # Vérification supplémentaire sur le rappel
    if rappel < 0.80:
        print("⚠ Rappel faible — risque de faux négatifs élevé")
        print("  Envisager d'ajuster le seuil de classification")
    else:
        print("✓ Rappel acceptable — déploiement autorisé")

Cas d'usage réel

Dans un système de détection de fraude bancaire, les conditionnelles implémentent les règles de scoring en temps réel : si le montant dépasse 5× la moyenne habituelle du client and le pays est inhabituel and l'heure est entre 2h et 5h du matin, alors la transaction est automatiquement bloquée et envoyée à l'équipe de révision. Ces règles conditionnelles explicites sont souvent combinées avec des modèles ML pour créer des systèmes hybrides plus robustes.

Tableau comparatif : Structures conditionnelles

Structure Quand l'utiliser Avantage Limite
if simple Une seule condition possible Lisible Un seul chemin possible
if/else Deux alternatives Couvre tous les cas Seulement 2 branches
if/elif/else 3+ alternatives mutuellement exclusives Précis, lisible Peut devenir long
Ternaire x if c else y Assignation simple Compact Peu lisible si complexe
match/case (Python 3.10+) Correspondance de patterns Très lisible Python 3.10+ seulement
Dict de dispatch Nombreuses alternatives fixes Très rapide Logique non triviale

Astuce : Python évalue les conditions and et or en court-circuit. Dans a and b, si a est False, b n'est jamais évalué. Exploitez cela pour éviter des erreurs : if liste and liste[0] > 0 — la seconde condition n'est vérifiée que si la liste n'est pas vide.

Attention : En Python, plusieurs valeurs sont considérées comme "fausses" (falsy) : 0, 0.0, "", [], {}, None, False. Ainsi, if ma_liste: est équivalent à if len(ma_liste) > 0:. C'est pratique mais peut induire en erreur si vous testez if valeur: et que valeur = 0 est un résultat légitime — dans ce cas, testez if valeur is not None: explicitement.


6. Boucles for : Répéter sans se Fatiguer

Définition approfondie

La boucle for en Python est une structure d'itération qui parcourt séquentiellement chaque élément d'un itérable (liste, chaîne, plage de nombres, dictionnaire, fichier...) et exécute un bloc de code pour chacun. Python se distingue des autres langages : la boucle for ne gère pas un compteur, elle consomme des éléments d'un itérable. C'est une différence conceptuelle importante : on ne dit pas "répète 10 fois" mais "pour chaque élément de cette collection, fais...".

La fonction range() génère une séquence d'entiers à la demande (sans les stocker tous en mémoire), ce qui est crucial quand on itère sur des millions d'éléments. enumerate() ajoute automatiquement un index à chaque élément, et zip() synchronise deux itérables en parallèle — ces deux fonctions sont indispensables en Data Science.

Analogie : la chaîne de montage

Une boucle for est une chaîne de montage automatique. Chaque élément de votre liste arrive sur le tapis roulant, passe par les mêmes opérations (vérification, transformation, assemblage), puis repart. L'usine ne sait pas à l'avance combien de pièces vont arriver — elle traite simplement chacune selon le même processus. C'est exactement ce que fait for element in ma_liste.

Bloc de code commenté

# =============================================================
# Boucles for — Traitement d'un pipeline de données météo
# =============================================================

# Dataset simulé : températures horaires d'une journée (24h)
temperatures_celsius = [
    -2, -3, -1, 0, 2, 5, 9, 13, 16, 19, 21, 23,
    24, 25, 24, 22, 20, 18, 15, 12, 9, 6, 4, 1
]

# --- Itération simple sur une liste ---
total = 0
for temp in temperatures_celsius:
    total += temp      # accumulation pour calculer la moyenne

moyenne = total / len(temperatures_celsius)
print(f"Température moyenne : {moyenne:.1f}°C")

# --- enumerate() : avoir l'index ET la valeur ---
print("\nHeures avec température négative :")
for heure, temp in enumerate(temperatures_celsius):
    if temp < 0:
        # heure représente l'index (0 = minuit, 23 = 23h)
        print(f"  {heure:02d}h00 → {temp}°C")

# --- range() : générer des indices ---
# Calculer les variations de température heure par heure
print("\nVariations horaires :")
for i in range(1, len(temperatures_celsius)):
    # Attention : on commence à 1 pour pouvoir accéder à i-1
    variation = temperatures_celsius[i] - temperatures_celsius[i - 1]
    direction = "↑" if variation > 0 else "↓" if variation < 0 else "→"
    print(f"  {i-1:02d}h→{i:02d}h : {direction} {abs(variation)}°C")

# --- zip() : synchroniser deux listes ---
stations = ["Paris", "Lyon", "Bordeaux", "Lille"]
temperatures_max = [24, 27, 22, 19]
temperatures_min = [-2, 1, 0, -5]

print("\nBilan par station :")
for station, t_max, t_min in zip(stations, temperatures_max, temperatures_min):
    amplitude = t_max - t_min   # écart thermique journalier
    print(f"  {station:<10} : min={t_min:>4}°C, max={t_max:>3}°C, amplitude={amplitude}°C")

# --- Conversion Celsius → Fahrenheit avec accumulation ---
temperatures_fahrenheit = []                # liste résultat vide
for temp_c in temperatures_celsius:
    temp_f = (temp_c * 9/5) + 32           # formule de conversion
    temperatures_fahrenheit.append(temp_f)  # ajout à la liste résultat

Cas d'usage réel

Dans un pipeline de traitement de fichiers CSV de données financières, une boucle for parcourt chaque ligne d'un fichier de transactions, convertit les devises selon le taux du jour, et alimente une base de données de reporting. Sur 500 000 transactions mensuelles, cette boucle est souvent remplacée par des opérations vectorisées NumPy/Pandas pour des raisons de performance — mais comprendre la boucle for est prérequis pour comprendre la vectorisation.

Tableau comparatif : Fonctions utilitaires des boucles for

Fonction Syntaxe Ce qu'elle apporte Exemple
range() range(0, 10, 2) Séquence d'entiers sans mémoire Indices, batches
enumerate() enumerate(liste) Index + valeur simultanément Position dans un dataset
zip() zip(l1, l2) Itération parallèle Deux colonnes en même temps
reversed() reversed(liste) Itération inversée Données chronologiques
sorted() sorted(liste) Itération triée Traitement ordonné
filter() filter(fn, liste) Itération filtrée Sélection conditionnelle
map() map(fn, liste) Transformation à la volée Conversion de types

Astuce : La fonction enumerate() accepte un argument start qui définit la valeur de départ du compteur : enumerate(liste, start=1) commence à compter à 1 plutôt que 0. Très pratique pour générer des IDs ou des numéros de ligne lisibles par un humain.

Attention : Ne modifiez jamais une liste pendant que vous itérez dessus avec for. Supprimer ou ajouter des éléments pendant la boucle crée des comportements imprévisibles (éléments sautés ou boucle infinie). Travaillez toujours sur une copie, ou construisez une nouvelle liste en parallèle.


7. Boucles while : Continuer jusqu'à la Condition

Définition approfondie

La boucle while répète un bloc de code tant qu'une condition reste vraie. Contrairement à for qui consomme un itérable fini, while peut théoriquement tourner indéfiniment si la condition ne devient jamais fausse — d'où l'importance d'une condition de sortie clairement définie. Elle est indispensable dans les algorithmes d'optimisation (gradient descent, algorithmes itératifs), les systèmes interactifs, et les boucles d'entraînement de réseaux de neurones où l'on continue jusqu'à convergence.

Les mots-clés break (sortie immédiate), continue (passer à l'itération suivante) et else (exécuté si la boucle se termine normalement sans break) donnent un contrôle fin sur le flux d'exécution.

Analogie : la levée du pain

Faire lever du pain est un processus while : tant que la pâte n'a pas doublé de volume, vous attendez et vérifiez toutes les 15 minutes. Vous ne savez pas à l'avance combien de temps cela prendra (1h30 ? 2h ? selon la température ambiante). Vous vérifiez la condition (volume doublé ?), et si c'est vrai, vous sortez de la boucle. Si jamais vous avez oublié de mettre la levure (condition jamais vraie), vous attendriez éternellement — c'est la boucle infinie.

Bloc de code commenté

# =============================================================
# Boucle while — Simulation d'entraînement de modèle ML
# (descente de gradient simplifiée)
# =============================================================

import random
random.seed(42)    # fixe le hasard pour la reproductibilité

# --- Paramètres de simulation ---
poids = 0.5              # paramètre du modèle à optimiser
taux_apprentissage = 0.1 # à quel point on ajuste à chaque étape
seuil_convergence = 0.001 # on s'arrête quand l'erreur est inférieure à ceci
max_iterations = 1000    # sécurité contre la boucle infinie
iteration = 0            # compteur d'itérations

valeur_cible = 2.0       # valeur que le modèle doit apprendre à prédire

print("Démarrage de l'entraînement...")
print(f"{'Itération':<12} {'Prédiction':<14} {'Erreur':<12} {'Poids'}")
print("-" * 55)

# --- Boucle d'entraînement ---
while True:
    # Simulation d'une prédiction avec le poids actuel
    prediction = poids * 3.0   # modèle linéaire simplifié : y = w * x

    # Calcul de l'erreur (différence entre prédiction et valeur réelle)
    erreur = valeur_cible - prediction

    # Affichage toutes les 10 itérations pour ne pas surcharger la console
    if iteration % 10 == 0:
        print(f"{iteration:<12} {prediction:<14.6f} {abs(erreur):<12.6f} {poids:.6f}")

    # Condition de convergence : erreur suffisamment petite
    if abs(erreur) < seuil_convergence:
        print(f"\n✓ Convergence atteinte à l'itération {iteration}")
        break   # sortie propre de la boucle

    # Condition de sécurité : trop d'itérations (modèle diverge)
    if iteration >= max_iterations:
        print(f"\n⚠ Arrêt de sécurité : max {max_iterations} itérations atteint")
        break

    # Mise à jour du poids (règle de mise à jour du gradient)
    poids += taux_apprentissage * erreur
    iteration += 1

print(f"\nRésultat final : poids={poids:.4f}, erreur={abs(erreur):.6f}")

# --- while avec continue : ignorer certaines valeurs ---
donnees_brutes = [1.2, None, 3.4, None, None, 5.6, 2.1, None]
donnees_valides = []
index = 0

while index < len(donnees_brutes):
    valeur = donnees_brutes[index]
    index += 1              # incrémentation AVANT continue (évite la boucle infinie)

    if valeur is None:
        continue            # saute les valeurs manquantes
    donnees_valides.append(valeur)

print(f"Données valides : {donnees_valides}")

Cas d'usage réel

Dans l'entraînement d'un réseau de neurones, la boucle d'optimisation principale est un while : tant que la perte (loss) ne converge pas et que le nombre d'époques maximum n'est pas atteint, continuer à ajuster les poids du réseau. Les frameworks comme TensorFlow et PyTorch abstraient cette boucle, mais elle est fondamentalement un while avec des conditions de sortie multiples.

Tableau comparatif : for vs while

Critère for while
Cas d'usage Itérables finis et connus Conditions de sortie variables
Risque de boucle infinie Aucun Réel si mal conçu
Lisibilité Très haute Moyenne (dépend de la complexité)
Contrôle d'index Via enumerate() Direct avec variable compteur
Usage ML typique Parcourir un dataset Boucle d'entraînement, convergence
Performance Équivalente Équivalente

Astuce : Ajoutez systématiquement une condition de sécurité max_iterations dans toute boucle while algorithmique. Les algorithmes d'optimisation peuvent diverger sur certains jeux de données — sans filet de sécurité, votre programme tournera indéfiniment sans message d'erreur.

Attention : L'oubli d'incrémenter le compteur avant un continue est le piège classique de la boucle infinie silencieuse. Si votre programme "freeze" sans erreur, c'est souvent un while qui ne progresse plus. Ajoutez un print(iteration) temporaire pour diagnostiquer.


8. Fonctions : Fabriquer ses Propres Outils

Définition approfondie

Une fonction est un bloc de code nommé, réutilisable, qui encapsule une tâche spécifique. On la définit avec le mot-clé def, suivi du nom, des paramètres entre parenthèses, et d'un bloc indenté. Elle peut retourner une valeur avec return. Le principe fondamental est DRY (Don't Repeat Yourself) : toute logique qu'on utilise plus d'une fois mérite d'être encapsulée dans une fonction.

Python offre une grande flexibilité dans la définition des paramètres : valeurs par défaut, arguments nommés (kwargs), arguments positionnels variables (*args). En Data Science, on structure son code en fonctions dès le départ pour faciliter les tests, la réutilisation dans d'autres notebooks, et la lisibilité des pipelines.

Analogie : la recette de cuisine standardisée

Dans un restaurant industriel, chaque plat a une fiche technique (la fonction) : liste d'ingrédients (paramètres), étapes de préparation (corps de la fonction), et plat final (valeur retournée). Le cuisinier n'a pas à réinventer la sauce béchamel à chaque fois — il appelle la fiche technique avec les ingrédients du jour. Les paramètres par défaut, c'est quand la fiche dit "400g de farine sauf indication contraire".

Bloc de code commenté

# =============================================================
# Fonctions — Construction d'un pipeline de Feature Engineering
# =============================================================

# --- Fonction simple avec valeur de retour ---
def normaliser_minmax(valeur, minimum, maximum):
    """
    Normalise une valeur dans l'intervalle [0, 1].

    Args:
        valeur  : float — la valeur à normaliser
        minimum : float — la valeur minimale du dataset
        maximum : float — la valeur maximale du dataset

    Returns:
        float : valeur normalisée entre 0 et 1, ou None si invalide
    """
    if maximum == minimum:
        # Évite la division par zéro si toutes les valeurs sont identiques
        return 0.0

    return (valeur - minimum) / (maximum - minimum)

# Utilisation
score_brut = 73.5
score_normalise = normaliser_minmax(score_brut, minimum=0, maximum=100)
print(f"Score normalisé : {score_normalise:.4f}")   # 0.7350

# --- Fonction avec paramètres par défaut ---
def calculer_zscore(valeurs, ddof=1):
    """
    Calcule le Z-score de chaque valeur (standardisation).
    ddof : degrés de liberté (1 pour données d'échantillon, 0 pour population)
    """
    n = len(valeurs)
    if n < 2:
        return [0.0] * n    # impossible de calculer l'écart-type sur 1 valeur

    moyenne = sum(valeurs) / n
    variance = sum((x - moyenne) ** 2 for x in valeurs) / (n - ddof)
    ecart_type = variance ** 0.5

    return [(x - moyenne) / ecart_type for x in valeurs]

revenus = [25000, 45000, 38000, 92000, 31000, 67000]
z_scores = calculer_zscore(revenus)   # utilise ddof=1 par défaut
for rev, z in zip(revenus, z_scores):
    print(f"  {rev:>7}€ → z-score = {z:+.3f}")

# --- Fonction retournant plusieurs valeurs ---
def statistiques_descriptives(donnees):
    """Calcule min, max, moyenne et médiane d'une liste de nombres."""
    tri = sorted(donnees)
    n = len(tri)
    moyenne = sum(tri) / n

    # Calcul de la médiane selon parité
    if n % 2 == 0:
        mediane = (tri[n//2 - 1] + tri[n//2]) / 2
    else:
        mediane = tri[n // 2]

    return tri[0], tri[-1], moyenne, mediane   # retour multiple (tuple)

minimum, maximum, moy, med = statistiques_descriptives(revenus)
print(f"\nMin={minimum}€  Max={maximum}€  Moy={moy:.0f}€  Med={med:.0f}€")

# --- Fonction d'ordre supérieur : accepte une autre fonction en paramètre ---
def appliquer_transformation(donnees, fonction_transform):
    """Applique une transformation à chaque élément d'une liste."""
    return [fonction_transform(x) for x in donnees]

import math
log_revenus = appliquer_transformation(revenus, math.log)  # log de chaque revenu
print(f"\nLog des revenus : {[f'{x:.2f}' for x in log_revenus]}")

Cas d'usage réel

Dans un projet de recommandation de films, chaque étape du pipeline est une fonction : charger_donnees(), nettoyer_ratings(), encoder_utilisateurs(), calculer_similarite_cosinus(), generer_recommandations(user_id, k=10). Cette organisation permet de tester chaque étape indépendamment, de remplacer une transformation sans toucher aux autres, et de réutiliser les fonctions dans d'autres projets de recommandation.

Tableau comparatif : Types de paramètres en Python

Type Syntaxe Exemple Quand l'utiliser
Positionnel def f(a, b) f(1, 2) Arguments toujours requis et ordonnés
Avec défaut def f(a, b=0) f(5)f(5, 0) Arguments optionnels avec valeur standard
Nommé à l'appel def f(a, b) f(b=2, a=1) Clarté à l'appel, ordre libre
*args def f(*vals) f(1,2,3) Nombre variable d'arguments positionnels
**kwargs def f(**opts) f(lr=0.01) Options nommées arbitraires
Positional-only def f(a, /, b) f(1, b=2) API strictes (Python 3.8+)

Astuce : Écrivez toujours une docstring pour chaque fonction (le texte entre triple guillemets juste après def). Les éditeurs comme VS Code et PyCharm l'affichent automatiquement quand vous utilisez la fonction, et des outils comme Sphinx peuvent générer une documentation HTML complète depuis vos docstrings.

Attention : N'utilisez jamais une liste ou un dictionnaire comme valeur par défaut de paramètre : def f(data=[]) est un piège classique. La liste par défaut est créée une seule fois lors de la définition de la fonction et partagée entre tous les appels. Utilisez None à la place : def f(data=None): if data is None: data = [].


9. Listes : La Structure de Données la Plus Polyvalente

Définition approfondie

Une liste Python est une séquence ordonnée et mutable d'éléments de n'importe quel type. "Ordonnée" signifie que l'ordre des éléments est préservé et que chaque élément est accessible par son index (position). "Mutable" signifie qu'on peut modifier, ajouter ou supprimer des éléments après création. Les listes sont la colonne vertébrale du traitement de données en Python pur, avant d'utiliser des structures spécialisées comme les arrays NumPy.

Le slicing (découpage) est une syntaxe puissante : liste[start:stop:step] extrait une sous-liste. liste[::-1] inverse une liste. liste[::2] prend un élément sur deux. Ces notations se retrouvent identiques dans NumPy et Pandas, ce qui rend la maîtrise du slicing doublement précieuse.

Analogie : le wagon de train numéroté

Une liste est un train de wagons. Chaque wagon a un numéro (index) et contient une valeur. Vous pouvez ajouter un wagon en queue (append()), insérer un wagon n'importe où (insert()), décrocher un wagon (remove()), ou lire tous les wagons dans l'ordre. Le premier wagon est numéro 0 (pas 1 !), et vous pouvez compter depuis la fin avec des indices négatifs : -1 est le dernier wagon, -2 l'avant-dernier.

Bloc de code commenté

# =============================================================
# Listes — Gestion d'un historique de performances de modèle
# =============================================================

# --- Création de listes ---
historique_loss = []             # liste vide pour l'historique d'entraînement
metriques = [0.7, 0.75, 0.82]   # liste avec valeurs initiales
labels_classes = ["chat", "chien", "lapin", "oiseau"]  # labels de classification

# --- Ajout d'éléments ---
historique_loss.append(0.9241)    # ajout en fin de liste
historique_loss.append(0.8103)
historique_loss.append(0.7234)
historique_loss.append(0.6891)
historique_loss.append(0.6102)
historique_loss.append(0.5843)
print(f"Historique loss : {historique_loss}")

# --- Accès par index ---
premiere_loss = historique_loss[0]    # premier élément (index 0)
derniere_loss = historique_loss[-1]   # dernier élément (index -1)
print(f"Loss initiale : {premiere_loss:.4f} → Loss finale : {derniere_loss:.4f}")

# --- Slicing : extraire des sous-listes ---
# Les 3 premières valeurs
debut_entrainement = historique_loss[:3]   # [0.9241, 0.8103, 0.7234]

# Les 3 dernières valeurs
fin_entrainement = historique_loss[-3:]    # [0.6102, 0.5843, ...]

# Une valeur sur deux (sous-échantillonnage)
echantillon = historique_loss[::2]

# --- Modification ---
labels_classes[2] = "lapin nain"    # remplacement à l'index 2
labels_classes.insert(0, "serpent") # insertion en position 0

# --- Suppression ---
labels_classes.remove("serpent")    # supprime par valeur (premier trouvé)
valeur_retiree = historique_loss.pop()  # retire et retourne le dernier élément

# --- Informations sur la liste ---
print(f"Nombre d'époques : {len(historique_loss)}")
print(f"Loss minimale : {min(historique_loss):.4f}")
print(f"Loss maximale : {max(historique_loss):.4f}")
print(f"Meilleure époque : index {historique_loss.index(min(historique_loss))}")

# --- Tri ---
scores_validation = [0.82, 0.91, 0.78, 0.88, 0.94, 0.85]
scores_tries = sorted(scores_validation, reverse=True)   # tri décroissant (sans modifier l'original)
print(f"\nTop 3 scores : {scores_tries[:3]}")

# --- Copie sûre d'une liste ---
original = [1, 2, 3, 4, 5]
copie_superficielle = original.copy()  # ou original[:]
copie_superficielle.append(99)
print(f"Original intact : {original}")   # [1, 2, 3, 4, 5] — non modifié

Cas d'usage réel

Dans un système de streaming de données financières en temps réel, une liste queue_transactions accumule les nouvelles transactions. Un traitement par batch extrait les 100 dernières (queue[-100:]), les traite, puis les retire. La liste sert de buffer dynamique entre l'acquisition des données et leur traitement.

Tableau comparatif : Méthodes de liste essentielles

Méthode Syntaxe Modifie la liste ? Complexité
append() lst.append(x) Oui O(1)
insert() lst.insert(i, x) Oui O(n)
remove() lst.remove(x) Oui O(n)
pop() lst.pop(i) Oui O(1) fin, O(n) ailleurs
sort() lst.sort() Oui O(n log n)
sorted() sorted(lst) Non (nouvelle liste) O(n log n)
index() lst.index(x) Non O(n)
count() lst.count(x) Non O(n)
extend() lst.extend(l2) Oui O(k)
copy() lst.copy() Non (nouvelle liste) O(n)

Astuce : Pour construire une grande liste depuis un calcul, la compréhension de liste [expr for x in iterable] est entre 1.5 et 3× plus rapide qu'une boucle for avec append(), car Python optimise le code de compréhension au niveau de l'interpréteur.

Attention : copie = original ne copie pas une liste — les deux noms pointent vers le même objet. Modifier copie modifie aussi original. Utilisez toujours original.copy() ou original[:] pour une copie indépendante. Pour des listes imbriquées (listes de listes), utilisez copy.deepcopy().


10. Tuples et Sets : Données Immuables et Ensembles Uniques

Définition approfondie

Le tuple est une séquence ordonnée et immuable : une fois créé, ses éléments ne peuvent ni être modifiés, ni ajoutés, ni supprimés. Cette immuabilité en fait le conteneur idéal pour des données qui ne doivent jamais changer (coordonnées GPS, configuration système, retours multiples de fonctions). Les tuples sont également plus rapides que les listes et peuvent servir de clés de dictionnaire.

Le set (ensemble) est une collection non ordonnée de valeurs uniques. Il implémente la théorie des ensembles : union (|), intersection (&), différence (-), différence symétrique (^). En Data Science, les sets sont précieux pour détecter les doublons, trouver des valeurs communes entre deux colonnes, ou filtrer rapidement des identifiants déjà traités.

Analogie : les pièces d'identité et le vestiaire

Un tuple est comme votre date de naissance sur votre carte d'identité : (15, 6, 1990) — elle est fixée une fois pour toutes et ne peut pas être modifiée. Un set est comme un vestiaire : chaque veste a un numéro unique, l'ordre n'a pas d'importance (le vestiaire n'est pas "trié"), et impossible d'avoir deux vestes avec le même numéro. Si vous déposez la veste numéro 42 deux fois, il n'y en aura toujours qu'une.

Bloc de code commenté

# =============================================================
# Tuples et Sets — Analyse de données e-commerce
# =============================================================

# ==================== TUPLES ====================

# Tuple : coordonnées GPS des entrepôts (immutables par nature)
entrepot_paris = (48.8566, 2.3522, "Paris-Nord")   # (lat, lon, nom)
entrepot_lyon = (45.7640, 4.8357, "Lyon-Est")
entrepot_bordeaux = (44.8378, -0.5792, "Bordeaux-Centre")

# Déballage (unpacking) — très utilisé en Data Science
lat, lon, nom = entrepot_paris
print(f"Entrepôt : {nom} ({lat:.4f}°N, {lon:.4f}°E)")

# Retour de fonction multiple (en pratique, c'est un tuple)
def calculer_stats_ventes(ventes):
    return min(ventes), max(ventes), sum(ventes) / len(ventes)

ventes_mensuelles = [12400, 18500, 9200, 23100, 15600, 20300]
vente_min, vente_max, vente_moy = calculer_stats_ventes(ventes_mensuelles)

# Les tuples peuvent servir de clés de dictionnaire (pas les listes !)
cache_distances = {}
cache_distances[(entrepot_paris[:2], entrepot_lyon[:2])] = 465.3   # 465 km
print(f"Distance Paris-Lyon en cache : {cache_distances[(entrepot_paris[:2], entrepot_lyon[:2])]} km")

# ==================== SETS ====================

# Clients ayant commandé en janvier vs février
clients_janvier = {"C001", "C002", "C003", "C005", "C007", "C010"}
clients_fevrier = {"C002", "C004", "C005", "C006", "C008", "C010"}

# Clients actifs les deux mois (intersection)
clients_reguliers = clients_janvier & clients_fevrier
print(f"\nClients réguliers : {clients_reguliers}")

# Clients nouveaux en février (différence)
nouveaux_clients = clients_fevrier - clients_janvier
print(f"Nouveaux clients en fév : {nouveaux_clients}")

# Clients actifs au moins un mois (union)
tous_clients_actifs = clients_janvier | clients_fevrier
print(f"Total clients actifs : {len(tous_clients_actifs)}")

# Dédoublonnage rapide d'une liste de catégories de produits
categories_brutes = ["électronique", "vêtements", "électronique", "livres",
                     "vêtements", "cuisine", "électronique", "livres"]
categories_uniques = list(set(categories_brutes))   # conversion set → list
print(f"\nCatégories uniques ({len(categories_uniques)}) : {sorted(categories_uniques)}")

# Test d'appartenance ultra-rapide avec set (O(1) vs O(n) pour list)
ids_blackliste = {"C999", "C888", "C777"}  # set de fraudes connues
id_a_verifier = "C888"
if id_a_verifier in ids_blackliste:
    print(f"⚠ Client {id_a_verifier} blacklisté !")

Cas d'usage réel

Dans un moteur de recommandation de musique, les sets permettent de calculer la similarité de Jaccard entre deux playlists : items_communs = playlist_alice & playlist_bob, items_total = playlist_alice | playlist_bob, similarite = len(items_communs) / len(items_total). Cette métrique de similarité est calculée des millions de fois par seconde — la rapidité des opérations ensemblistes (O(1) pour les tests d'appartenance) est critique.

Tableau comparatif : List vs Tuple vs Set

Critère List [] Tuple () Set {}
Ordonné
Mutable ✓ (contenu)
Doublons autorisés
Clé de dict
Test appartenance O(n) O(n) O(1)
Usage Data Science Séquences de valeurs Coordonnées, configs Déduplication, intersection

Astuce : Pour tester si un élément appartient à une grande collection (des milliers d'éléments ou plus), convertissez toujours votre liste en set d'abord. element in ma_liste est O(n) — il parcourt la liste entière. element in mon_set est O(1) — instantané quelle que soit la taille.

Attention : Un set ne préserve pas l'ordre d'insertion. list(set(["b", "a", "c"])) peut donner ["a", "c", "b"] ou n'importe quel ordre. Si vous dédoublonnez une liste mais avez besoin de conserver l'ordre, utilisez list(dict.fromkeys(ma_liste)) — les dictionnaires Python 3.7+ préservent l'ordre d'insertion.


11. Dictionnaires : La Carte d'Identité de vos Données

Définition approfondie

Un dictionnaire Python (dict) est une structure de données qui associe des clés à des valeurs : {"clé": valeur}. L'accès à une valeur par sa clé est instantané (complexité O(1)), quelle que soit la taille du dictionnaire — grâce à une table de hachage interne. Les clés doivent être immutables (strings, numbers, tuples), les valeurs peuvent être de n'importe quel type, y compris d'autres dictionnaires (imbrication).

En Data Science, les dictionnaires sont partout : représentation d'un enregistrement JSON, mapping de labels vers des entiers pour l'encodage, cache de résultats coûteux, paramètres d'hypertuning de modèles, configuration d'expériences ML.

Analogie : l'annuaire téléphonique intelligent

Un dictionnaire est un annuaire : vous cherchez "Dupont Marie" (clé) et obtenez immédiatement son numéro (valeur), sans parcourir toute la liste de A à Z. La magie est que vous pouvez chercher n'importe quelle entrée en temps constant, que l'annuaire ait 100 ou 10 millions d'entrées. Et contrairement à un vrai annuaire, vous pouvez mettre n'importe quel type d'objet comme "numéro" : un autre dictionnaire, une liste, une fonction.

Bloc de code commenté

# =============================================================
# Dictionnaires — Configuration et résultats d'expériences ML
# =============================================================

# --- Dictionnaire de configuration d'expérience ---
config_experience = {
    "nom": "ResNet50_finetune_v3",
    "modele": "ResNet50",
    "dataset": "CIFAR-100",
    "hyperparametres": {              # dictionnaire imbriqué
        "taux_apprentissage": 0.001,
        "batch_size": 64,
        "nb_epoques": 50,
        "optimizer": "Adam",
        "dropout": 0.3
    },
    "metriques_cibles": ["accuracy", "f1_macro"],
    "gpu": True
}

# --- Accès aux valeurs ---
print(config_experience["nom"])       # accès direct par clé
print(config_experience["hyperparametres"]["batch_size"])  # clé imbriquée

# --- Accès sécurisé avec get() (ne lève pas d'erreur si clé absente) ---
val_regularisation = config_experience.get("l2_regularisation", 0.0)
print(f"L2 régularisation : {val_regularisation}")  # 0.0 (valeur par défaut)

# --- Modification et ajout ---
config_experience["statut"] = "en_cours"                    # ajout
config_experience["hyperparametres"]["taux_apprentissage"] = 0.0005  # modification
del config_experience["gpu"]                                 # suppression

# --- Résultats d'expériences multiples ---
historique_experiences = {}

for version in ["v1", "v2", "v3"]:
    # Simulation de résultats (en réalité, lus depuis des logs)
    historique_experiences[version] = {
        "accuracy": round(0.72 + len(version) * 0.02, 3),
        "f1_score": round(0.69 + len(version) * 0.02, 3),
        "temps_entrainement_min": 45 + len(version) * 10
    }

# --- Itération sur un dictionnaire ---
print("\nComparaison des versions :")
print(f"{'Version':<10} {'Accuracy':<12} {'F1-Score':<12} {'Durée'}")
print("-" * 45)

for version, resultats in historique_experiences.items():
    print(f"{version:<10} {resultats['accuracy']:<12.3f} "
          f"{resultats['f1_score']:<12.3f} {resultats['temps_entrainement_min']}min")

# --- Trouver la meilleure version ---
meilleure_version = max(
    historique_experiences,
    key=lambda v: historique_experiences[v]["accuracy"]
)
print(f"\nMeilleure version : {meilleure_version}")

# --- Fusionner deux dictionnaires (Python 3.9+) ---
params_base = {"lr": 0.001, "batch": 32, "epochs": 10}
params_override = {"lr": 0.01, "epochs": 50}   # valeurs à écraser
params_final = params_base | params_override     # fusion, override gagne
print(f"\nParams final : {params_final}")

Cas d'usage réel

Dans un système de gestion d'expériences MLflow, chaque expérience est un dictionnaire : paramètres d'entrée, métriques de sortie, artefacts produits, tags de classification. La capacité à accéder instantanément à n'importe quelle expérience par son ID (experiences[exp_id]) et à les comparer en itérant sur .items() est fondamentale pour l'industrialisation du Machine Learning.

Tableau comparatif : Méthodes de dictionnaire essentielles

Méthode Syntaxe Résultat Cas d'usage
get(k, défaut) d.get("k", 0) Valeur ou défaut Accès sécurisé
keys() d.keys() Vue des clés Itérer sur les clés
values() d.values() Vue des valeurs Itérer sur les valeurs
items() d.items() Vue clé-valeur Itérer sur tout
update() d.update(d2) Fusion sur place Mise à jour de config
pop(k) d.pop("k") Valeur + suppression Extraction destructive
setdefault() d.setdefault("k", []) Init si absent Compteurs, groupements
fromkeys() dict.fromkeys(liste, 0) Dict initialisé Compteurs initialisés

Astuce : collections.defaultdict est une version améliorée du dictionnaire qui initialise automatiquement les valeurs absentes. from collections import defaultdict; compteur = defaultdict(int) permet de faire compteur["clé"] += 1 sans vérifier si la clé existe. Idéal pour compter des occurrences dans un corpus de texte.

Attention : Itérer sur un dictionnaire tout en le modifiant (ajout/suppression de clés) lève une RuntimeError. Faites d'abord for cle in list(mon_dict.keys()): pour itérer sur une copie des clés si vous devez modifier le dictionnaire pendant l'itération.


12. Compréhensions : Écrire Plus en Disant Moins

Définition approfondie

Les compréhensions sont une syntaxe Python concise pour créer des collections à partir d'un itérable, avec un filtrage optionnel, en une seule expression. On distingue les compréhensions de listes [expr for x in iter if cond], de sets {expr for x in iter}, de dictionnaires {k: v for x in iter}, et les expressions génératrices (expr for x in iter) — ces dernières ne créent pas de liste en mémoire mais produisent les valeurs à la demande (économie de RAM).

Les compréhensions ne sont pas du simple "sucre syntaxique" (joli mais inutile) : elles sont plus rapides qu'une boucle for équivalente car Python les optimise au niveau du bytecode. En Data Science, elles permettent de transformer, filtrer et restructurer des données de manière expressive et performante.

Analogie : la commande au restaurant avec filtre

Imaginez que vous commandez dans un restaurant à buffet : "Je veux une assiette de tout ce qui est chaud, en doublant la portion des plats épicés." En Python : [portion*2 if epice else portion for plat, portion, epice in buffet if chaud]. C'est exactement ce qu'une compréhension exprime : sélection, transformation et filtrage en une seule déclaration d'intention.

Bloc de code commenté

# =============================================================
# Compréhensions — Transformation de données en Data Science
# =============================================================

# Données brutes simulant des logs de prédictions
logs_predictions = [
    {"id": "P001", "score": 0.92, "label": "positif", "valide": True},
    {"id": "P002", "score": 0.34, "label": "négatif", "valide": True},
    {"id": "P003", "score": 0.71, "label": "positif", "valide": False},  # données corrompues
    {"id": "P004", "score": 0.88, "label": "positif", "valide": True},
    {"id": "P005", "score": 0.12, "label": "négatif", "valide": True},
    {"id": "P006", "score": 0.65, "label": "positif", "valide": True},
]

# --- Compréhension de liste simple ---
# Extraire tous les scores valides
scores_valides = [log["score"] for log in logs_predictions if log["valide"]]
print(f"Scores valides : {scores_valides}")

# --- Compréhension avec transformation conditionnelle ---
# Convertir les labels en entiers (0/1) — encodage binaire
labels_encoded = [
    1 if log["label"] == "positif" else 0
    for log in logs_predictions
    if log["valide"]
]
print(f"Labels encodés : {labels_encoded}")

# --- Compréhension de dictionnaire ---
# Créer un index {id: score} pour accès rapide
index_scores = {
    log["id"]: log["score"]
    for log in logs_predictions
    if log["valide"]
}
print(f"\nIndex scores : {index_scores}")

# Accès instantané
print(f"Score P004 : {index_scores.get('P004', 'N/A')}")

# --- Compréhension de set ---
# Labels uniques dans les données valides
labels_uniques = {log["label"] for log in logs_predictions if log["valide"]}
print(f"\nLabels uniques : {labels_uniques}")

# --- Compréhension imbriquée ---
# Matrice de confusion : toutes les paires (vrai_label, pred_label)
vrais_labels = ["chat", "chien", "chat", "lapin"]
pred_labels  = ["chat", "chat",  "lapin","lapin"]

paires = [(vrai, pred) for vrai, pred in zip(vrais_labels, pred_labels)]
erreurs = [(vrai, pred) for vrai, pred in paires if vrai != pred]
print(f"\nErreurs de classification : {erreurs}")

# --- Expression génératrice (économise la RAM) ---
# Somme des carrés des scores — pas besoin de stocker la liste intermédiaire
somme_carres = sum(score ** 2 for score in scores_valides)
print(f"\nSomme des carrés : {somme_carres:.4f}")

# --- Aplatissement d'une liste de listes (compréhension imbriquée) ---
batches_features = [[1.2, 0.5, 3.1], [0.8, 2.3, 1.1], [2.1, 0.9, 1.7]]
features_aplaties = [feat for batch in batches_features for feat in batch]
print(f"\nFeatures aplaties : {features_aplaties}")

Cas d'usage réel

Dans un pipeline de Feature Engineering, les compréhensions de dictionnaires génèrent des features dérivées de manière déclarative : features_log = {f"log_{col}": math.log(df[col] + 1) for col in colonnes_numeriques if df[col].min() >= 0}. Cette ligne crée des dizaines de nouvelles features en une seule expression lisible.

Tableau comparatif : Types de compréhensions

Type Syntaxe Résultat Mémoire Usage
Liste [x*2 for x in l] list En mémoire Transformation de séquences
Dict {k: v for k,v in d} dict En mémoire Index, mapping
Set {x for x in l} set En mémoire Déduplication
Génératrice (x*2 for x in l) generator À la demande Grandes données, sum/max
Imbriquée [x for l in ll for x in l] list En mémoire Aplatissement
Conditionnelle [x if c else y for x in l] list En mémoire Remplacement conditionnel

Astuce : Utilisez une expression génératrice plutôt qu'une compréhension de liste quand vous passez le résultat directement à sum(), max(), min(), any() ou all(). sum(x**2 for x in million_de_valeurs) consomme quelques Ko de RAM. sum([x**2 for x in million_de_valeurs]) crée d'abord une liste d'un million d'éléments en mémoire.

Attention : Une compréhension trop complexe devient illisible. Si votre compréhension nécessite deux if, deux boucles for imbriquées ET une condition ternaire, reformulez-la en boucle for classique. La lisibilité prime sur la concision — un code compréhensible par un collègue vaut mieux qu'un one-liner incompréhensible.


13. Gestion des Fichiers : Lire et Écrire des Données Réelles

Définition approfondie

La gestion des fichiers en Python permet de lire, écrire et manipuler des données persistantes sur le disque. Python utilise le gestionnaire de contexte with open(...) as f: comme pattern recommandé : il garantit la fermeture automatique du fichier même en cas d'erreur, évitant les fuites de ressources. Les modes d'ouverture clés sont "r" (lecture), "w" (écriture, écrase), "a" (ajout en fin), "rb" et "wb" pour les fichiers binaires.

En Data Science, les formats de fichiers les plus courants sont CSV (données tabulaires), JSON (données hiérarchiques, APIs), TXT (logs, corpus de texte), et des formats binaires spécialisés comme Parquet ou HDF5 pour les très grands datasets. Maîtriser la lecture/écriture native de CSV et JSON est fondamental avant d'utiliser Pandas.

Analogie : le classeur d'archives

Travailler avec des fichiers, c'est comme accéder à un classeur d'archives physique. open() c'est ouvrir le classeur, le mode ("r", "w") c'est préciser si vous venez consulter ou rédiger de nouveaux documents, read() ou write() c'est feuilleter ou noter des informations, et close() (géré automatiquement par with) c'est remettre le classeur en rayon après usage. Sans remettre le classeur en rayon, il reste inaccessible aux autres — c'est la fuite de ressource.

Bloc de code commenté

# =============================================================
# Gestion des fichiers — Pipeline de logs d'entraînement ML
# =============================================================

import csv
import json
from pathlib import Path

# --- Écriture d'un fichier CSV de résultats ---
resultats_entrainement = [
    {"epoque": 1, "loss_train": 0.9241, "loss_val": 0.9558, "accuracy": 0.6821},
    {"epoque": 2, "loss_train": 0.8103, "loss_val": 0.8421, "accuracy": 0.7234},
    {"epoque": 3, "loss_train": 0.7234, "loss_val": 0.7891, "accuracy": 0.7651},
    {"epoque": 4, "loss_train": 0.6891, "loss_val": 0.7412, "accuracy": 0.7923},
    {"epoque": 5, "loss_train": 0.6102, "loss_val": 0.7108, "accuracy": 0.8134},
]

chemin_csv = Path("logs_entrainement.csv")

# Écriture du CSV avec le module csv (gestion correcte des virgules/guillemets)
with open(chemin_csv, "w", newline="", encoding="utf-8") as f:
    # DictWriter : chaque ligne est un dictionnaire
    champs = ["epoque", "loss_train", "loss_val", "accuracy"]
    writer = csv.DictWriter(f, fieldnames=champs)

    writer.writeheader()           # écrit la ligne d'en-tête
    writer.writerows(resultats_entrainement)  # écrit toutes les lignes

print(f"✓ Fichier CSV créé : {chemin_csv}")

# --- Lecture du CSV ---
donnees_lues = []
with open(chemin_csv, "r", encoding="utf-8") as f:
    reader = csv.DictReader(f)     # chaque ligne devient un dictionnaire
    for ligne in reader:
        # Les valeurs CSV sont des strings — conversion de type nécessaire
        donnees_lues.append({
            "epoque": int(ligne["epoque"]),
            "loss_train": float(ligne["loss_train"]),
            "loss_val": float(ligne["loss_val"]),
            "accuracy": float(ligne["accuracy"])
        })

meilleure = max(donnees_lues, key=lambda x: x["accuracy"])
print(f"Meilleure époque : {meilleure['epoque']} (accuracy={meilleure['accuracy']:.4f})")

# --- Lecture/Écriture JSON ---
config_modele = {
    "architecture": "CNN",
    "couches": [{"type": "Conv2D", "filtres": 32}, {"type": "Dense", "unites": 128}],
    "optimiseur": {"nom": "Adam", "lr": 0.001, "beta1": 0.9},
    "date_creation": "2025-03-15"
}

chemin_json = Path("config_modele.json")

# Écriture JSON avec indentation pour la lisibilité
with open(chemin_json, "w", encoding="utf-8") as f:
    json.dump(config_modele, f, indent=2, ensure_ascii=False)

# Lecture JSON
with open(chemin_json, "r", encoding="utf-8") as f:
    config_chargee = json.load(f)

print(f"\nArchitecture chargée : {config_chargee['architecture']}")
print(f"Nombre de couches : {len(config_chargee['couches'])}")

# --- Écriture de logs en mode append ---
chemin_log = Path("application.log")
import datetime

def log_evenement(message, niveau="INFO"):
    """Ajoute une ligne de log horodatée sans écraser le fichier."""
    timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    with open(chemin_log, "a", encoding="utf-8") as f:  # mode "a" = append
        f.write(f"[{timestamp}] [{niveau}] {message}\n")

log_evenement("Démarrage du pipeline")
log_evenement("Données chargées : 50000 exemples")
log_evenement("Modèle non convergé après 10 époques", "WARNING")

Cas d'usage réel

Dans un pipeline de traitement de données IoT industriel, des capteurs génèrent 10 000 lignes de données par heure dans des fichiers CSV horodatés. Un script Python lit chaque fichier, valide les plages de valeurs, détecte les anomalies, et écrit un fichier JSON de rapport par heure. Ce pattern lecture-traitement-écriture est la base de tous les pipelines ETL (Extract, Transform, Load).

Tableau comparatif : Modes d'ouverture de fichiers

Mode Signification Fichier existant Fichier absent
"r" Lecture seule Lit depuis le début FileNotFoundError
"w" Écriture seule Écrase le contenu Crée le fichier
"a" Ajout Écrit à la fin Crée le fichier
"x" Création exclusive FileExistsError Crée le fichier
"r+" Lecture + écriture Lit/écrit sans écraser FileNotFoundError
"rb" / "wb" Binaire lecture/écriture Contenu brut bytes Selon r/w

Astuce : Utilisez pathlib.Path plutôt que les chaînes de caractères pour les chemins de fichiers. Path("data") / "2024" / "janvier.csv" fonctionne identiquement sur Windows (\) et Linux/Mac (/), évitant les bugs de portabilité. Path.exists(), Path.mkdir(parents=True), Path.glob("*.csv") sont des outils précieux.

Attention : Le mode "w" détruit silencieusement le contenu existant d'un fichier sans avertissement. Avant d'écrire dans un fichier important, vérifiez son existence avec Path(chemin).exists() et prenez une décision explicite : écraser, appender, ou sauvegarder avec un nom horodaté.


14. Gestion des Erreurs et Exceptions : Rendre le Code Robuste

Définition approfondie

Une exception est un événement qui interrompt le flux normal d'exécution quand Python rencontre une situation anormale. Sans gestion des exceptions, la moindre erreur (fichier manquant, valeur inattendue, réseau indisponible) fait planter tout le programme. La structure try/except/else/finally permet de capturer ces situations, de les gérer gracieusement, et de continuer ou de s'arrêter proprement. try contient le code risqué, except ExceptionType capture les erreurs spécifiques, else s'exécute si aucune erreur n'est survenue, et finally s'exécute toujours (idéal pour libérer des ressources).

En Data Science, la gestion des erreurs est cruciale dans les pipelines en production : une erreur sur un enregistrement ne doit pas bloquer le traitement des 999 999 suivants. On logue les erreurs, on comptabilise les échecs, et on continue.

Analogie : le filet de sécurité du funambule

Exécuter du code sans gestion d'erreurs, c'est marcher sur un câble à 50 mètres de hauteur sans filet. Une seule erreur (valeur nulle inattendue, fichier corrompu) et tout s'arrête brutalement. Le try/except est le filet de sécurité : si vous tombez (erreur), le filet vous rattrape (except), vous pouvez évaluer la situation (logger l'erreur), et décider de continuer ou de remonter. Le finally est la corde de sécurité fixée à votre harnais : elle vous retient quoi qu'il arrive.

Bloc de code commenté

# =============================================================
# Gestion des exceptions — Pipeline de validation de données
# =============================================================

import logging

# Configuration du système de logging (mieux que print pour la production)
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger("pipeline_validation")

# --- Exception personnalisée pour le domaine métier ---
class DonneeInvalideError(Exception):
    """Exception levée quand une donnée ne passe pas la validation métier."""
    def __init__(self, champ, valeur, raison):
        self.champ = champ
        self.valeur = valeur
        self.raison = raison
        super().__init__(f"Champ '{champ}' invalide (valeur={valeur}): {raison}")

# --- Fonction de validation avec gestion d'erreurs ---
def valider_enregistrement_patient(enregistrement):
    """
    Valide un enregistrement patient et retourne les données nettoyées.
    Lève DonneeInvalideError si une règle métier est violée.
    """
    donnees_validees = {}

    # Validation de l'âge
    try:
        age = float(enregistrement.get("age"))    # conversion peut échouer
        if not (0 <= age <= 130):
            raise DonneeInvalideError("age", age, "doit être entre 0 et 130")
        donnees_validees["age"] = age
    except (TypeError, ValueError):
        raise DonneeInvalideError("age", enregistrement.get("age"), "valeur non numérique")

    # Validation de l'IMC
    try:
        imc = float(enregistrement.get("imc"))
        if not (10.0 <= imc <= 80.0):
            raise DonneeInvalideError("imc", imc, "IMC hors plage physiologique")
        donnees_validees["imc"] = imc
    except (TypeError, ValueError):
        raise DonneeInvalideError("imc", enregistrement.get("imc"), "valeur non numérique")

    return donnees_validees

# --- Traitement en batch avec gestion des erreurs ---
patients_bruts = [
    {"age": "45", "imc": "24.5"},     # valide
    {"age": "abc", "imc": "28.1"},    # age non numérique
    {"age": "32", "imc": "150.0"},    # imc aberrant
    {"age": None, "imc": "22.3"},     # age manquant
    {"age": "67", "imc": "26.8"},     # valide
]

resultats = []
erreurs = []

for i, patient in enumerate(patients_bruts):
    try:
        # Tentative de validation
        patient_valide = valider_enregistrement_patient(patient)
        resultats.append(patient_valide)
        logger.info(f"Patient {i+1} validé avec succès")

    except DonneeInvalideError as e:
        # Erreur métier connue : on loggue et on continue
        erreurs.append({"index": i, "erreur": str(e), "donnees": patient})
        logger.warning(f"Patient {i+1} rejeté : {e}")

    except Exception as e:
        # Erreur inattendue : on loggue mais on n'interrompt pas le pipeline
        erreurs.append({"index": i, "erreur": f"Erreur inattendue: {e}"})
        logger.error(f"Erreur inattendue sur patient {i+1} : {type(e).__name__}: {e}")

# --- Rapport final ---
print(f"\n{'='*45}")
print(f"Traités  : {len(patients_bruts)}")
print(f"Valides  : {len(resultats)}")
print(f"Rejetés  : {len(erreurs)}")
print(f"Taux OK  : {len(resultats)/len(patients_bruts):.0%}")

Cas d'usage réel

Dans un pipeline d'ingestion de données de réseaux sociaux, les appels API Twitter/X peuvent échouer pour de multiples raisons : rate limiting, timeout réseau, JSON malformé, token expiré. Chaque type d'erreur nécessite une réponse différente : attendre 15 minutes pour le rate limit (RateLimitError), réessayer 3 fois pour un timeout réseau, et alerter l'équipe pour un token expiré. La gestion fine des exceptions est ce qui distingue un script de data scientist d'un vrai système de production.

Tableau comparatif : Exceptions Python courantes en Data Science

Exception Cause typique Comment l'éviter
FileNotFoundError Chemin de fichier incorrect Vérifier Path.exists() avant
ValueError int("abc"), conversion impossible Valider le type avant conversion
KeyError dict["clé_absente"] Utiliser .get(clé, défaut)
IndexError liste[index_hors_bornes] Vérifier len(liste) avant
ZeroDivisionError Division par zéro Vérifier dénominateur ≠ 0
TypeError Opération sur mauvais type Valider les types en entrée
AttributeError Méthode inexistante sur objet Vérifier le type de l'objet
MemoryError Dataset trop grand pour la RAM Traitement par chunks

Astuce : En production, ne capturez jamais except Exception ou except: sans logger l'erreur et sans action corrective. Capturer silencieusement toutes les exceptions (le fameux "bare except") masque des bugs critiques et rend le débogage cauchemardesque. Loggez toujours au minimum logger.error(f"Erreur inattendue: {type(e).__name__}: {e}").

Attention : L'ordre des clauses except est crucial : Python parcourt les except dans l'ordre et exécute le premier qui correspond. Mettez toujours les exceptions spécifiques avant les générales. except Exception avant except ValueError n'atteindra jamais le except ValueError.


15. Modules et Packages : Utiliser l'Écosystème Python

Définition approfondie

Un module Python est simplement un fichier .py contenant des fonctions, classes et variables réutilisables. Un package est un répertoire de modules organisé hiérarchiquement avec un fichier __init__.py. Python est fourni avec une bibliothèque standard riche (modules intégrés), et pip permet d'installer des packages tiers de PyPI (Python Package Index) qui en compte plus de 500 000.

L'instruction import charge un module en mémoire. from module import fonction importe directement une fonction sans préfixe. import numpy as np crée un alias (convention universelle en Data Science). La création de ses propres modules structure un projet Python et évite les fichiers de milliers de lignes.

Analogie : la boîte à outils professionnelle

La bibliothèque standard Python est la boîte à outils livrée avec votre atelier : clés, tournevis, marteau — des outils universels toujours disponibles. pip install c'est commander des outils spécialisés chez un fournisseur professionnel (PyPI) : une scie laser (NumPy), une perceuse CNC (Pandas), un robot soudeur (TensorFlow). Créer vos propres modules, c'est fabriquer vos propres outils sur mesure et les ranger dans votre atelier pour les réutiliser sur tous vos projets.

Bloc de code commenté

# =============================================================
# Modules et packages — Utilisation de la bibliothèque standard
# et création d'un module utilitaire
# =============================================================

# --- Bibliothèque standard : modules essentiels en Data Science ---
import os           # opérations système (chemins, variables d'env)
import sys          # informations sur l'interpréteur Python
import math         # fonctions mathématiques avancées
import random       # génération de nombres aléatoires
import statistics   # statistiques descriptives (standard Python 3.4+)
import datetime     # manipulation de dates et temps
import itertools    # outils d'itération avancés
import collections  # structures de données spécialisées
import json         # sérialisation JSON

# --- Utilisation pratique de statistics ---
notes_etudiants = [14.5, 18.0, 12.0, 16.5, 9.5, 17.0, 13.0, 15.5, 11.0, 18.5]

print("=== Statistiques descriptives (module statistics) ===")
print(f"Moyenne       : {statistics.mean(notes_etudiants):.2f}")
print(f"Médiane       : {statistics.median(notes_etudiants):.2f}")
print(f"Écart-type    : {statistics.stdev(notes_etudiants):.2f}")
print(f"Variance      : {statistics.variance(notes_etudiants):.2f}")

# --- itertools : combinatoire pour la sélection de features ---
from itertools import combinations, product

features_disponibles = ["age", "revenu", "anciennete", "score_credit"]

# Toutes les paires de features (pour tester les interactions)
paires_features = list(combinations(features_disponibles, 2))
print(f"\n{len(paires_features)} paires de features possibles :")
for paire in paires_features:
    print(f"  {paire[0]} × {paire[1]}")

# --- collections.Counter : comptage automatique ---
from collections import Counter

corpus_tags = ["machine-learning", "python", "deep-learning", "python",
               "nlp", "machine-learning", "python", "computer-vision",
               "nlp", "python", "deep-learning"]

compteur = Counter(corpus_tags)
print(f"\nTop 3 tags : {compteur.most_common(3)}")

# --- datetime : manipulation de dates pour les séries temporelles ---
from datetime import datetime, timedelta

debut_collecte = datetime(2024, 1, 1)
fin_collecte = datetime(2024, 3, 31)
duree = fin_collecte - debut_collecte
print(f"\nPériode de collecte : {duree.days} jours")

# Générer toutes les dates d'une semaine
semaine = [debut_collecte + timedelta(days=i) for i in range(7)]
for jour in semaine:
    print(f"  {jour.strftime('%A %d/%m/%Y')}")

# --- os.environ : lire des variables d'environnement (bonnes pratiques sécurité) ---
api_key = os.environ.get("OPENAI_API_KEY", "non_configuree")
if api_key == "non_configuree":
    print("\n⚠ Variable OPENAI_API_KEY non définie dans l'environnement")

Cas d'usage réel

Dans un projet de traitement de données de santé, os.environ est utilisé pour lire les credentials de base de données sans les écrire dans le code source (sécurité). datetime convertit les timestamps UNIX des capteurs en dates lisibles. collections.defaultdict groupe les patients par diagnostic sans initialisation laborieuse. itertools.chain concatène des générateurs de fichiers CSV sans tout charger en RAM.

Tableau comparatif : Modules standards utiles en Data Science

Module Fonctionnalité clé Import recommandé Remplacé en production par
statistics Stats descriptives simples import statistics NumPy, Pandas
math Fonctions mathématiques import math NumPy (vectorisé)
random Aléatoire import random numpy.random
datetime Dates et temps from datetime import datetime Pandas Timestamp
csv Fichiers CSV import csv Pandas read_csv
json Sérialisation JSON import json Pandas, requests
os Système d'exploitation import os pathlib
itertools Itération avancée from itertools import ... Aucun équivalent direct
collections Structures spécialisées from collections import ... Pandas parfois
logging Journalisation import logging loguru (package tiers)

Astuce : Créez un fichier utils.py dans chaque projet pour y centraliser vos fonctions utilitaires réutilisables (normalisation, validation, formatage). Importez-les avec from utils import ma_fonction. Cette habitude transforme un notebook monolithique en un projet structuré et maintenable.

Attention : Ne nommez jamais vos fichiers Python comme des modules existants : random.py, statistics.py, math.py. Python chercherait votre fichier en priorité et échouerait à importer le vrai module. C'est un bug silencieux très difficile à diagnostiquer.


16. NumPy : Le Moteur Mathématique de la Data Science

Définition approfondie

NumPy (Numerical Python) est la bibliothèque fondamentale du calcul scientifique Python. Sa structure centrale est le ndarray (N-dimensional array) : un tableau multidimensionnel d'éléments du même type, stockés de manière contiguë en mémoire. Cette homogénéité et cette contiguïté permettent des opérations vectorisées — calculées en C ou Fortran sous le capot — jusqu'à 100× plus rapides qu'une boucle Python équivalente. NumPy est la base sur laquelle reposent Pandas, Scikit-learn, TensorFlow et PyTorch.

La broadcasting est une règle NumPy qui permet d'effectuer des opérations entre arrays de formes différentes sans copier les données. Elle est omniprésente en Machine Learning : normaliser une matrice d'observations par un vecteur de moyennes, ou ajouter un biais à chaque neurone d'une couche.

Analogie : la chaîne de calcul industrielle

Une liste Python traitée en boucle for est comme un artisan qui lime chaque pièce métallique une par une. Un array NumPy est une chaîne de fabrication industrielle CNC : toutes les pièces sont traitées simultanément par des machines spécialisées. 10 000 pièces à la main prennent 10 000 fois plus longtemps qu'une seule. 10 000 éléments NumPy prennent presque le même temps qu'un seul. C'est la vectorisation.

Bloc de code commenté

# =============================================================
# NumPy — Traitement vectorisé de données de capteurs industriels
# =============================================================

import numpy as np

np.random.seed(2024)   # reproductibilité des résultats aléatoires

# --- Création d'arrays ---
# Simulation de mesures de température de 5 capteurs sur 24h
# Shape (5, 24) : 5 lignes (capteurs), 24 colonnes (heures)
temperatures = np.random.normal(loc=22.0, scale=3.5, size=(5, 24))
print(f"Shape : {temperatures.shape}")  # (5, 24)
print(f"Type  : {temperatures.dtype}")  # float64
print(f"RAM   : {temperatures.nbytes} bytes")  # 5*24*8 = 960 bytes

# --- Opérations vectorisées (sans boucle for) ---
# Conversion Celsius → Fahrenheit pour TOUS les capteurs simultanément
temperatures_f = (temperatures * 9/5) + 32   # opération sur tout le tableau d'un coup

# Normalisation Z-score par capteur (axe=1 : opération sur chaque ligne)
moyennes = temperatures.mean(axis=1, keepdims=True)    # (5, 1)
ecarts_types = temperatures.std(axis=1, keepdims=True) # (5, 1)
temperatures_normalisees = (temperatures - moyennes) / ecarts_types  # broadcasting!

print(f"\nMoyennes par capteur : {moyennes.flatten().round(2)}")

# --- Slicing multi-dimensionnel ---
capteur_0 = temperatures[0, :]          # première ligne (capteur 0), toutes les heures
heures_diurnes = temperatures[:, 8:18]  # tous les capteurs, heures 8h à 17h
print(f"\nTempératures diurnes — shape : {heures_diurnes.shape}")  # (5, 10)

# --- Masques booléens (filtrage conditionnel) ---
# Détecter les anomalies : température > 3 écarts-types de la moyenne
seuil_alerte = temperatures.mean() + 3 * temperatures.std()
masque_anomalies = temperatures > seuil_alerte
nb_anomalies = masque_anomalies.sum()
print(f"\nAnomalies détectées : {nb_anomalies}")
print(f"Valeurs aberrantes : {temperatures[masque_anomalies].round(2)}")

# --- Algèbre linéaire (fondamental pour le ML) ---
# Matrice de corrélation entre capteurs
matrice_corr = np.corrcoef(temperatures)  # (5, 5) matrice de corrélation
print(f"\nMatrice de corrélation shape : {matrice_corr.shape}")
print(f"Corrélation capteur 0-1 : {matrice_corr[0, 1]:.4f}")

# Produit matriciel : exemple de multiplication poids × features
poids = np.random.randn(3, 5)    # 3 neurones, 5 features d'entrée
features = np.random.randn(5, 100) # 5 features, 100 observations
sortie_couche = poids @ features   # @ : produit matriciel Python 3.5+
print(f"\nSortie couche neuronale : {sortie_couche.shape}")  # (3, 100)

# --- Statistiques par axe ---
print(f"\nStats sur 24h :")
print(f"  Température max    : {temperatures.max(axis=1).round(2)}")
print(f"  Heure la plus chaude : {temperatures.argmax(axis=1)}")  # indice du max

Cas d'usage réel

Dans un système de traitement d'images médicales (IRM cérébrales), chaque image est un array NumPy 3D (hauteur, largeur, profondeur). Un batch de 32 images devient un array 4D (32, 256, 256, 128). Toutes les opérations de preprocessing (normalisation, augmentation, redimensionnement) sont effectuées vectorisées sur ce batch en une seule opération, permettant de traiter des milliers d'images par seconde.

Tableau comparatif : List Python vs Array NumPy

Critère Liste Python Array NumPy
Types d'éléments Mixtes Homogènes
Vitesse calcul 1× (référence) 10×–100×
Mémoire ~56 bytes/élément 8 bytes/float64
Opérations vecto. Non Oui
Dimensions 1D seulement natif N dimensions
Slicing avancé Limité Complet (masques, fancy indexing)
Usage recommandé Données hétérogènes Calcul numérique, ML

Astuce : Utilisez np.float32 plutôt que np.float64 (par défaut) pour les grands datasets d'entraînement ML. Vous réduisez la consommation mémoire de moitié avec une perte de précision négligeable pour la plupart des algorithmes d'apprentissage. Sur GPU, float32 est aussi 2× plus rapide que float64.

Attention : Ne confondez pas array.shape (tuple des dimensions) avec array.size (nombre total d'éléments) et len(array) (taille de la première dimension). Pour un array (3, 4, 5) : .shape = (3, 4, 5), .size = 60, len() = 3. Cette confusion est source de nombreux bugs en traitement d'images.


17. Pandas : Analyser des Tableaux de Données comme un Data Analyst

Définition approfondie

Pandas est la bibliothèque d'analyse de données tabulaires de référence en Python. Sa structure centrale, le DataFrame, est un tableau à deux dimensions avec des lignes indexées et des colonnes nommées — exactement comme une feuille Excel, mais manipulable programmatiquement avec une puissance incomparable. La Series est une colonne unique avec son index. Pandas intègre des fonctionnalités de nettoyage, transformation, agrégation, fusion et visualisation de données.

Pandas s'appuie sur NumPy pour les calculs, ce qui le rend très performant. Il gère nativement les valeurs manquantes (NaN — Not a Number), les types de données temporelles, et supporte des fichiers CSV, Excel, JSON, SQL, Parquet et plus.

Analogie : Excel programmable sous stéroïdes

Un DataFrame Pandas est une feuille Excel que vous pouvez manipuler avec du code : filtrer des lignes (df[df["age"] > 30]), créer des colonnes calculées (df["imc"] = df["poids"] / df["taille"]**2), grouper et agréger (df.groupby("region").mean()), fusionner deux tables (pd.merge(df1, df2, on="id")). La différence avec Excel : ces opérations s'exécutent sur des millions de lignes en secondes, sont reproductibles, et ne nécessitent pas de clic.

Bloc de code commenté

# =============================================================
# Pandas — Analyse de données de ventes e-commerce
# =============================================================

import pandas as pd
import numpy as np

np.random.seed(42)
pd.set_option("display.float_format", "{:.2f}".format)

# --- Création d'un DataFrame ---
nb_commandes = 1000

df = pd.DataFrame({
    "commande_id": range(1, nb_commandes + 1),
    "client_id": np.random.randint(1, 201, nb_commandes),
    "produit": np.random.choice(["Laptop", "Smartphone", "Tablette", "Casque", "Montre"], nb_commandes),
    "categorie": np.random.choice(["Informatique", "Mobile", "Audio", "Wearable"], nb_commandes),
    "montant": np.round(np.random.uniform(29.99, 1499.99, nb_commandes), 2),
    "date_commande": pd.date_range("2024-01-01", periods=nb_commandes, freq="8H"),
    "note_client": np.random.choice([1,2,3,4,5, np.nan], nb_commandes, p=[0.05,0.1,0.15,0.35,0.30,0.05])
})

print("=== Aperçu du dataset ===")
print(df.head(3).to_string())

# --- Exploration initiale ---
print(f"\nDimensions : {df.shape}")   # (1000, 7)
print(df.dtypes)
print(f"\nValeurs manquantes :\n{df.isnull().sum()}")

# --- Nettoyage des données ---
# Remplacer les notes manquantes par la médiane
mediane_notes = df["note_client"].median()
df["note_client"] = df["note_client"].fillna(mediane_notes)

# Extraire des features temporelles depuis la date
df["mois"] = df["date_commande"].dt.month
df["jour_semaine"] = df["date_commande"].dt.day_name()
df["heure"] = df["date_commande"].dt.hour

# --- Filtrage ---
# Commandes haute valeur (>500€) avec bonne note (≥4)
commandes_premium = df[(df["montant"] > 500) & (df["note_client"] >= 4)]
print(f"\nCommandes premium : {len(commandes_premium)} ({len(commandes_premium)/len(df):.1%})")

# --- Agrégation avec groupby ---
print("\n=== Chiffre d'affaires par catégorie ===")
ca_par_categorie = (
    df.groupby("categorie")
    .agg(
        ca_total=("montant", "sum"),
        nb_commandes=("commande_id", "count"),
        note_moyenne=("note_client", "mean"),
        panier_moyen=("montant", "mean")
    )
    .sort_values("ca_total", ascending=False)
    .round(2)
)
print(ca_par_categorie.to_string())

# --- Pivot table : analyse croisée mois × catégorie ---
pivot_mensuel = df.pivot_table(
    values="montant",
    index="mois",
    columns="categorie",
    aggfunc="sum"
).round(0)

print(f"\n=== CA mensuel par catégorie (€) ===")
print(pivot_mensuel.to_string())

# --- Feature Engineering ---
# Score composite de valeur client (RFM simplifié)
valeur_client = (
    df.groupby("client_id")
    .agg(
        nb_achats=("commande_id", "count"),
        ca_total=("montant", "sum"),
        note_moy=("note_client", "mean")
    )
)
# Normalisation pour créer un score 0-100
valeur_client["score_valeur"] = (
    (valeur_client["nb_achats"] / valeur_client["nb_achats"].max() * 40) +
    (valeur_client["ca_total"] / valeur_client["ca_total"].max() * 40) +
    (valeur_client["note_moy"] / 5 * 20)
).round(1)

print(f"\nTop 5 clients par score de valeur :")
print(valeur_client.nlargest(5, "score_valeur").to_string())

Cas d'usage réel

Un data analyst en grande distribution utilise Pandas quotidiennement pour analyser les ventes par magasin : chargement de fichiers CSV hebdomadaires (pd.read_csv()), fusion avec le référentiel produits (pd.merge()), calcul des ruptures de stock (df[df["stock"] == 0]), et génération de rapports Excel automatisés (df.to_excel()). Ce travail qui prenait une journée avec Excel est réduit à 20 minutes avec un script Pandas reproductible.

Tableau comparatif : Fonctions Pandas essentielles

Opération Pandas Équivalent SQL
Sélection colonnes df[["col1","col2"]] SELECT col1, col2
Filtrage lignes df[df["val"] > 0] WHERE val > 0
Tri df.sort_values("col") ORDER BY col
Agrégation df.groupby("col").sum() GROUP BY col
Jointure pd.merge(df1, df2, on="id") JOIN ON id
Nouvelles colonnes df["new"] = df["a"] + df["b"] SELECT a+b AS new
Valeurs uniques df["col"].nunique() COUNT(DISTINCT col)
Top N df.nlargest(10, "col") ORDER BY col DESC LIMIT 10

Astuce : La méthode .query() de Pandas permet d'écrire des filtres dans une syntaxe proche du SQL, particulièrement lisible : df.query("montant > 500 and note_client >= 4 and categorie == 'Informatique'"). Elle est aussi légèrement plus performante sur les grands DataFrames.

Attention : Évitez d'utiliser des boucles for sur les lignes d'un DataFrame (for index, row in df.iterrows()). Sur un DataFrame de 100 000 lignes, c'est 10 à 100× plus lent que les opérations vectorisées Pandas. Utilisez toujours .apply(), .map(), .groupby(), ou des opérations vectorisées directes.


18. Visualisation avec Matplotlib : Transformer les Chiffres en Images

Définition approfondie

Matplotlib est la bibliothèque de visualisation de données fondatrice de l'écosystème Python scientifique. Elle permet de créer des graphiques de publication de qualité : courbes, histogrammes, nuages de points, cartes de chaleur, diagrammes en barres, et bien plus. Son interface pyplot (importée comme plt) offre une API de haut niveau inspirée de MATLAB. Pour des graphiques plus esthétiques, Seaborn est un complément qui s'appuie sur Matplotlib avec une syntaxe simplifiée.

La structure de Matplotlib repose sur deux objets : la Figure (la "toile" entière, qui peut contenir plusieurs graphiques) et les Axes (un graphique individuel avec ses axes x et y). La maîtrise de cette architecture figure/axes est ce qui permet de créer des tableaux de bord multi-graphiques complexes.

Analogie : l'atelier du cartographe

Matplotlib est l'atelier d'un cartographe. La Figure est la table de travail où vous déroulez votre parchemin vierge. Chaque Axes est une carte individuelle que vous dessinez sur ce parchemin. Vous pouvez placer plusieurs cartes côte à côte sur la même table (subplot). plt.plot() est le stylet avec lequel vous tracez les routes, plt.xlabel() ajoute les légendes des axes, et plt.savefig() plastifie votre carte pour la distribution.

Bloc de code commenté

# =============================================================
# Matplotlib — Tableau de bord d'analyse de performance ML
# =============================================================

import matplotlib.pyplot as plt
import numpy as np

# Configuration globale du style
plt.style.use("seaborn-v0_8-whitegrid")   # style épuré et professionnel
plt.rcParams["figure.dpi"] = 120
plt.rcParams["font.size"] = 10

# Données simulées de courbes d'apprentissage
np.random.seed(2024)
epoques = np.arange(1, 51)

# Deux modèles à comparer
loss_modele_A = 1.2 * np.exp(-0.08 * epoques) + 0.05 * np.random.randn(50)
loss_modele_B = 1.0 * np.exp(-0.05 * epoques) + 0.08 * np.random.randn(50)
loss_val_A = loss_modele_A + 0.08 + 0.03 * np.random.randn(50)
loss_val_B = loss_modele_B + 0.12 + 0.05 * np.random.randn(50)

accuracy_A = 1 - loss_val_A / 1.5
accuracy_B = 1 - loss_val_B / 1.2

scores_classes = np.array([0.91, 0.84, 0.88, 0.72, 0.95])
classes = ["Chat", "Chien", "Lapin", "Oiseau", "Poisson"]

# --- Création d'une figure avec 4 sous-graphiques ---
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle("Tableau de Bord — Comparaison Modèles A vs B", fontsize=14, fontweight="bold")

# --- Graphique 1 : Courbes de loss ---
ax1 = axes[0, 0]                                  # premier sous-graphique
ax1.plot(epoques, loss_modele_A, "b-", label="Modèle A — train", linewidth=2)
ax1.plot(epoques, loss_val_A, "b--", label="Modèle A — val", linewidth=1.5, alpha=0.8)
ax1.plot(epoques, loss_modele_B, "r-", label="Modèle B — train", linewidth=2)
ax1.plot(epoques, loss_val_B, "r--", label="Modèle B — val", linewidth=1.5, alpha=0.8)
ax1.set_xlabel("Époque")
ax1.set_ylabel("Loss")
ax1.set_title("Courbes d'apprentissage")
ax1.legend(fontsize=8)
ax1.set_xlim(1, 50)

# --- Graphique 2 : Évolution de l'accuracy ---
ax2 = axes[0, 1]
ax2.plot(epoques, accuracy_A, color="steelblue", linewidth=2, label="Modèle A")
ax2.plot(epoques, accuracy_B, color="coral", linewidth=2, label="Modèle B")
ax2.axhline(y=0.90, color="green", linestyle=":", linewidth=1.5, label="Seuil prod (90%)")
ax2.fill_between(epoques, accuracy_A, accuracy_B,
                 alpha=0.15, color="purple", label="Écart")
ax2.set_xlabel("Époque")
ax2.set_ylabel("Accuracy")
ax2.set_title("Évolution de l'accuracy")
ax2.legend(fontsize=8)
ax2.set_ylim(0, 1.05)

# --- Graphique 3 : F1-score par classe (barplot) ---
ax3 = axes[1, 0]
couleurs = ["#2196F3", "#4CAF50", "#FF9800", "#F44336", "#9C27B0"]
barres = ax3.bar(classes, scores_classes, color=couleurs, width=0.6, edgecolor="white")

# Annoter chaque barre avec sa valeur
for barre, score in zip(barres, scores_classes):
    ax3.text(barre.get_x() + barre.get_width()/2., barre.get_height() + 0.01,
             f"{score:.0%}", ha="center", va="bottom", fontsize=9, fontweight="bold")

ax3.set_ylim(0, 1.10)
ax3.set_xlabel("Classe")
ax3.set_ylabel("F1-Score")
ax3.set_title("F1-Score par classe (Modèle A final)")
ax3.axhline(y=0.85, color="red", linestyle="--", linewidth=1, label="Seuil min")
ax3.legend(fontsize=8)

# --- Graphique 4 : Distribution des erreurs (histogramme) ---
ax4 = axes[1, 1]
erreurs_A = accuracy_A - np.sort(accuracy_A)   # simulation d'erreurs résiduelles
erreurs_A = np.random.normal(0, 0.03, 500)
ax4.hist(erreurs_A, bins=30, color="steelblue", edgecolor="white", alpha=0.8)
ax4.axvline(x=0, color="red", linestyle="--", linewidth=1.5, label="Erreur nulle")
ax4.set_xlabel("Erreur résiduelle")
ax4.set_ylabel("Fréquence")
ax4.set_title("Distribution des erreurs de prédiction")
ax4.legend(fontsize=8)

plt.tight_layout()                  # ajuste automatiquement les espacements
plt.savefig("tableau_bord_ml.png", dpi=150, bbox_inches="tight")
plt.show()
print("✓ Figure sauvegardée : tableau_bord_ml.png")

Cas d'usage réel

Dans un rapport mensuel automatisé pour la direction d'une startup FinTech, un script Python génère un fichier PDF de 15 pages avec des graphiques Matplotlib : évolution du MRR (Monthly Recurring Revenue), cohortes de rétention clients, distribution des scores de crédit, carte de chaleur des transactions par heure/jour. Ce rapport est généré automatiquement chaque 1er du mois par un cron job, sans intervention humaine.

Tableau comparatif : Bibliothèques de visualisation Python

Bibliothèque Forces Faiblesses Usage idéal
Matplotlib Contrôle total, publications Verbeux, peu esthétique par défaut Graphiques scientifiques précis
Seaborn Beau par défaut, stats Moins flexible EDA, distributions, corrélations
Plotly Interactif, zoom/survol Plus lourd Dashboards, présentations web
Altair Syntaxe déclarative Peu de types de graphiques Données tabulaires, grammaire
Bokeh Interactivité avancée Courbe d'apprentissage Applications web analytiques

Astuce : Pour les analyses exploratoires rapides, df.plot() de Pandas génère directement un graphique Matplotlib depuis un DataFrame en une ligne : df.groupby("mois")["montant"].sum().plot(kind="bar", color="steelblue", figsize=(10,4), title="CA mensuel"). Parfait pour une exploration rapide sans configuration.

Attention : Évitez les graphiques en camembert (pie charts) pour comparer plus de 4 catégories — l'œil humain compare très mal les angles. Préférez un graphique en barres horizontales, bien plus lisible. Cette règle est fondamentale en data storytelling et est enseignée dans tous les cours de communication par les données.


19. Introduction à Scikit-learn : Votre Premier Modèle de Machine Learning

Définition approfondie

Scikit-learn est la bibliothèque de Machine Learning la plus utilisée au monde pour Python. Elle offre une interface unifiée et cohérente pour des dizaines d'algorithmes : classification, régression, clustering, réduction de dimensionnalité, et preprocessing. Son API est basée sur trois méthodes : fit(X, y) (entraîner le modèle), predict(X) (faire des prédictions), et transform(X) (transformer des données). Cette uniformité signifie qu'une fois que vous maîtrisez un algorithme, vous maîtrisez l'interface de tous les autres.

Les concepts fondamentaux sont : features (X — les colonnes d'entrée), target (y — la colonne à prédire), train/test split (séparation des données pour évaluer les performances honnêtement), et les métriques d'évaluation (accuracy, F1, RMSE selon le type de problème).

Analogie : apprendre à un enfant à reconnaître les fruits

Entraîner un modèle ML, c'est comme apprendre à un enfant à reconnaître les fruits. Vous lui montrez des centaines de pommes avec le label "pomme", des oranges avec "orange" — c'est la phase fit(). Ensuite, vous lui montrez un nouveau fruit et demandez "c'est quoi ?" — c'est predict(). Le test sur des fruits qu'il n'a jamais vus (test set) mesure s'il a vraiment appris, ou s'il a juste mémorisé les exemples montrés (overfitting).

Bloc de code commenté

# =============================================================
# Scikit-learn — Classification de risque de diabète
# (basé sur des indicateurs de santé anonymisés)
# =============================================================

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score
import warnings
warnings.filterwarnings("ignore")

np.random.seed(42)

# --- Simulation d'un dataset médical ---
n = 800    # 800 patients

df = pd.DataFrame({
    "age": np.random.randint(25, 75, n),
    "imc": np.round(np.random.normal(28.5, 6.0, n), 1),
    "glucose_jeun": np.random.randint(70, 200, n),
    "tension_systolique": np.random.randint(100, 180, n),
    "historique_familial": np.random.randint(0, 2, n),
    "activite_physique": np.random.randint(0, 5, n),  # jours/semaine
    "nb_medicaments": np.random.randint(0, 6, n),
})

# Création de la cible (diabétique = 1) basée sur des règles médicales simplifiées
prob_diabete = (
    (df["glucose_jeun"] > 126).astype(float) * 0.4 +
    (df["imc"] > 30).astype(float) * 0.2 +
    (df["age"] > 50).astype(float) * 0.15 +
    df["historique_familial"] * 0.15 +
    np.random.random(n) * 0.1
)
df["diabetique"] = (prob_diabete > 0.45).astype(int)

print(f"Distribution : {df['diabetique'].value_counts().to_dict()}")
print(f"Prévalence : {df['diabetique'].mean():.1%}")

# --- Préparation des données ---
# Séparation features / target
X = df.drop("diabetique", axis=1)    # toutes les colonnes sauf la cible
y = df["diabetique"]                 # la colonne cible

# Division train (80%) / test (20%) — TOUJOURS avant tout preprocessing
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=42,
    stratify=y        # préserve la proportion de classes
)

print(f"\nTrain: {X_train.shape}, Test: {X_test.shape}")

# --- Preprocessing : standardisation des features ---
# IMPORTANT : fit() uniquement sur X_train, transform() sur les deux
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)   # apprend les stats ET transforme
X_test_scaled = scaler.transform(X_test)          # applique les stats du train SEULEMENT

# --- Entraînement et comparaison de plusieurs modèles ---
modeles = {
    "Régression Logistique": LogisticRegression(max_iter=1000),
    "Random Forest": RandomForestClassifier(n_estimators=100, random_state=42),
    "Gradient Boosting": GradientBoostingClassifier(n_estimators=100, random_state=42),
}

print(f"\n{'Modèle':<28} {'Accuracy':<12} {'AUC-ROC':<12} {'F1-Score'}")
print("-" * 65)

meilleurs_resultats = {}

for nom, modele in modeles.items():
    # Entraînement
    modele.fit(X_train_scaled, y_train)

    # Prédiction
    y_pred = modele.predict(X_test_scaled)
    y_proba = modele.predict_proba(X_test_scaled)[:, 1]  # probabilité classe positive

    # Métriques
    from sklearn.metrics import accuracy_score, f1_score
    acc = accuracy_score(y_test, y_pred)
    auc = roc_auc_score(y_test, y_proba)
    f1 = f1_score(y_test, y_pred)

    print(f"{nom:<28} {acc:<12.4f} {auc:<12.4f} {f1:.4f}")
    meilleurs_resultats[nom] = {"modele": modele, "auc": auc}

# --- Rapport détaillé du meilleur modèle ---
meilleur_nom = max(meilleurs_resultats, key=lambda k: meilleurs_resultats[k]["auc"])
meilleur_modele = meilleurs_resultats[meilleur_nom]["modele"]

print(f"\n=== Rapport détaillé : {meilleur_nom} ===")
y_pred_final = meilleur_modele.predict(X_test_scaled)
print(classification_report(y_test, y_pred_final, target_names=["Non-diabétique", "Diabétique"]))

# Importance des features (pour Random Forest et Gradient Boosting)
if hasattr(meilleur_modele, "feature_importances_"):
    importances = pd.Series(
        meilleur_modele.feature_importances_,
        index=X.columns
    ).sort_values(ascending=False)
    print("Importance des features :")
    for feat, imp in importances.items():
        print(f"  {feat:<25} : {'█' * int(imp*50)} {imp:.4f}")

Cas d'usage réel

Dans une assurance santé, un modèle RandomForest entraîné sur l'historique médical des assurés prédit le risque de sinistres coûteux l'année suivante. Ce modèle permet à l'assureur de proposer des programmes de prévention ciblés aux personnes à haut risque, réduisant les coûts de 15% tout en améliorant la santé des clients. Le pipeline Scikit-learn complet (preprocessing + modèle + évaluation) est encapsulé dans un Pipeline Scikit-learn et déployé via une API FastAPI.

Tableau comparatif : Algorithmes Scikit-learn pour la classification

Algorithme Forces Faiblesses Données idéales
Régression Logistique Interprétable, rapide Linéaire seulement Relations linéaires, baseline
Random Forest Robuste, peu de tuning Lent sur de très gros datasets Données tabulaires générales
Gradient Boosting Très performant Sensible aux hyperparamètres Compétitions Kaggle, tabular
SVM Efficace petits datasets Lent sur grands Données haute dimension
KNN Intuitif, pas d'entraînement Lent en prédiction Petits datasets
Naive Bayes Très rapide, NLP Hypothèse d'indépendance Texte, classification de spam

Astuce : Commencez toujours par une baseline simple (Régression Logistique ou DummyClassifier) avant d'essayer des modèles complexes. Si votre modèle "sophistiqué" n'est pas significativement meilleur que la baseline, vos features ou votre formulation du problème sont à revoir — pas l'algorithme.

Attention : Ne faites jamais scaler.fit_transform(X) sur tout le dataset avant de le diviser train/test. Cela constitue une data leakage (fuite de données) : les statistiques de normalisation calculées incluent des informations du test set, ce qui donne des performances artificiellement gonflées. Divisez d'abord, normalisez ensuite, sur le train uniquement.


20. Bonnes Pratiques et Code Pythonique : Écrire du Code Professionnel

Définition approfondie

Le code "pythonique" est du code qui exploite pleinement les idiomes, conventions et forces du langage Python pour être lisible, concis et efficace. Le guide PEP 8 (Python Enhancement Proposal 8) est la référence de style universellement adoptée : nommage des variables (snake_case), longueur des lignes (79 caractères), espacement autour des opérateurs, organisation des imports. Le Zen de Python (import this) résume la philosophie en 19 aphorismes — "Beau vaut mieux que laid. Explicite vaut mieux qu'implicite. Simple vaut mieux que complexe."

En Data Science professionnelle, la qualité du code est aussi importante que la qualité des modèles. Un notebook illisible avec des variables nommées a, b, tmp1 est un passif technique — il ne peut pas être relu, réutilisé, ni maintenu. Les conventions de nommage, la documentation inline, et la structure modulaire transforment un script one-shot en un actif durable.

Analogie : la rédaction académique vs les notes brouillon

Un code non pythonique est comme des notes de brouillon griffonnées sur un coin de serviette : vous comprenez ce que vous avez écrit le jour même, mais impossible à relire une semaine plus tard. Le code pythonique est comme un article académique structuré : titre, sous-parties, raisonnement clair, références explicites. N'importe quel lecteur compétent peut le comprendre, le critiquer, et le faire progresser — même vous, six mois plus tard.

Bloc de code commenté

# =============================================================
# Code Pythonique — Refactoring d'un pipeline Data Science
# AVANT vs APRÈS : illustration des bonnes pratiques
# =============================================================

# ==================== VERSION NON PYTHONIQUE ====================
# (code typique d'un débutant qui fonctionne mais est illisible)

def f(d, t=0.5):
    r = []
    for i in range(len(d)):
        if d[i] > t:
            r.append(1)
        else:
            r.append(0)
    return r

# ==================== VERSION PYTHONIQUE ====================
# Même fonctionnalité, mais propre, lisible et maintenable

def binariser_scores(scores: list[float], seuil: float = 0.5) -> list[int]:
    """
    Convertit une liste de scores continus en labels binaires.

    Args:
        scores : Liste de scores entre 0 et 1 (ex: probabilités de sortie d'un modèle)
        seuil  : Valeur de coupure (défaut=0.5). Score > seuil → 1, sinon → 0.

    Returns:
        Liste d'entiers 0 ou 1 de même longueur que scores.

    Example:
        >>> binariser_scores([0.3, 0.7, 0.5, 0.9], seuil=0.5)
        [0, 1, 0, 1]
    """
    return [1 if score > seuil else 0 for score in scores]

# --- Conventions de nommage PEP 8 ---

# Variables et fonctions : snake_case
taux_apprentissage = 0.001          # ✓ snake_case
nombreIterations = 100              # ✗ camelCase (non pythonique)

# Constantes : MAJUSCULES
TAILLE_BATCH = 32                   # ✓ SCREAMING_SNAKE_CASE pour les constantes
CHEMIN_DONNEES = "/data/train.csv"
CLASSES_CIBLES = ["chat", "chien", "lapin"]

# Classes : PascalCase
class PipelineTraitement:
    """Pipeline de traitement et validation des données."""

    def __init__(self, nom_projet: str, version: str = "1.0"):
        self.nom_projet = nom_projet
        self.version = version
        self._donnees = None    # attribut "privé" (convention : préfixe _)

    def charger_donnees(self, chemin: str) -> None:
        """Charge les données depuis un fichier CSV."""
        print(f"[{self.nom_projet} v{self.version}] Chargement : {chemin}")

# --- Context managers (gestionnaires de contexte) ---
# ✗ Version fragile sans with
fichier = open("data.csv", "r")
contenu = fichier.read()
fichier.close()   # oublié si exception entre open() et close()

# ✓ Version robuste avec with
with open("data.csv", "r", encoding="utf-8") as fichier:
    contenu = fichier.read()   # fichier fermé automatiquement, même si exception

# --- Unpacking expressif ---
# ✗ Non pythonique
coord = (48.8566, 2.3522, "Paris")
lat = coord[0]
lon = coord[1]
nom = coord[2]

# ✓ Pythonique
lat, lon, nom = (48.8566, 2.3522, "Paris")
# ✓ Encore mieux avec * pour "le reste"
premier, *milieu, dernier = [10, 20, 30, 40, 50]

# --- Walrus operator := (Python 3.8+) ---
# Assigner ET utiliser dans la même expression
donnees = [0.3, 0.8, 0.1, 0.9, 0.5, 0.7]
if (n = len(donnees)) > 5:    # assigne n ET teste la condition
    print(f"Dataset suffisant : {n} éléments")

# --- f-strings avancées ---
metriques = {"accuracy": 0.9134, "f1": 0.8923, "auc": 0.9451}
for nom_metrique, valeur in metriques.items():
    # Formatage aligné avec f-strings
    print(f"  {nom_metrique:<12} : {valeur:.4f} ({valeur:.1%})")

# --- Type hints (annotations de type) ---
from typing import Optional, Union

def calculer_f1(precision: float, rappel: float,
                beta: float = 1.0) -> Optional[float]:
    """Calcule le F-bêta score. Retourne None si precision + rappel == 0."""
    denominateur = (beta**2 * precision) + rappel
    if denominateur == 0:
        return None
    return (1 + beta**2) * precision * rappel / denominateur

# --- Gestion de configuration avec dataclass (Python 3.7+) ---
from dataclasses import dataclass, field

@dataclass
class ConfigModele:
    """Configuration immuable d'un modèle ML."""
    nom: str
    taux_apprentissage: float = 0.001
    batch_size: int = 32
    nb_epoques: int = 100
    classes: list = field(default_factory=lambda: ["cat", "dog"])

    def __post_init__(self):
        """Validation après initialisation."""
        if not (0 < self.taux_apprentissage < 1):
            raise ValueError(f"Taux d'apprentissage invalide : {self.taux_apprentissage}")

config = ConfigModele(nom="CNN_v2", taux_apprentissage=0.0005, nb_epoques=50)
print(f"\nConfig : {config}")

Cas d'usage réel

Dans une équipe de Data Scientists chez une banque, les revues de code (code reviews) évaluent systématiquement le respect des bonnes pratiques : docstrings complètes, type hints sur toutes les fonctions publiques, absence de "magic numbers" non documentés, et couverture de tests unitaires > 80%. Un code non pythonique est renvoyé en révision, même si les résultats du modèle sont bons — car le code non maintenable est un risque opérationnel.

Tableau comparatif : Code pythonique vs non pythonique

Pratique ✗ Non pythonique ✓ Pythonique Bénéfice
Test de vérité if len(liste) != 0: if liste: Lisibilité
Nommage a, tmp, data2 taux_rappel, scores_valides Maintenabilité
Compréhension for x in l: r.append(f(x)) [f(x) for x in l] Performance + lisibilité
Échange tmp=a; a=b; b=tmp a, b = b, a Concision
None check if x == None: if x is None: Sémantique correcte
Contexte fichier f=open(); ...; f.close() with open() as f: Robustesse
F-string "val=" + str(val) f"val={val}" Lisibilité + performance
Enumerate for i in range(len(l)): l[i] for i, x in enumerate(l) Clarté

Astuce : Installez flake8 et black dans votre environnement de développement : pip install flake8 black. black reformate automatiquement votre code selon PEP 8 en une commande (black mon_script.py). flake8 détecte les violations de style et les erreurs potentielles. Ces deux outils, combinés à un linter IDE, élèvent la qualité du code sans effort conscient.

Attention : Les bonnes pratiques ne doivent pas devenir une obsession paralysante. "Perfect is the enemy of good" : un notebook fonctionnel légèrement imparfait vaut mieux qu'un code parfait jamais terminé. Appliquez les bonnes pratiques progressivement, commencez par les nommages et les docstrings, et refactorisez le reste quand le code est stabilisé.


Conclusion

Tu as maintenant maîtrisé :

  • ✅ Configurer un environnement Python professionnel pour la Data Science (Anaconda, Jupyter, pip, environnements virtuels)
  • ✅ Manipuler les types de données fondamentaux (int, float, str, bool, None) avec rigueur
  • ✅ Utiliser l'ensemble des opérateurs Python pour les calculs, comparaisons et logique conditionnelle
  • ✅ Traiter et nettoyer des chaînes de caractères pour le preprocessing de données textuelles
  • ✅ Structurer des décisions complexes avec des conditions if/elif/else multi-critères
  • ✅ Itérer sur des collections avec les boucles for et toutes leurs fonctions utilitaires (enumerate, zip, range)
  • ✅ Implémenter des algorithmes itératifs et des boucles de convergence avec while
  • ✅ Concevoir des fonctions réutilisables, documentées et testables pour structurer un pipeline
  • ✅ Exploiter les listes Python pour le traitement séquentiel de données avec slicing avancé
  • ✅ Utiliser tuples et sets pour des besoins spécifiques (immuabilité, unicité, opérations ensemblistes)
  • ✅ Modéliser des données structurées et des configurations avec les dictionnaires
  • ✅ Écrire des transformations de données concises et performantes avec les compréhensions
  • ✅ Lire et écrire des fichiers CSV, JSON et logs dans des pipelines de données réels
  • ✅ Rendre un pipeline robuste et maintenable avec la gestion des exceptions
  • ✅ Naviguer dans l'écosystème de modules Python standard et packages tiers
  • ✅ Manipuler des arrays multidimensionnels et effectuer des calculs vectorisés avec NumPy
  • ✅ Analyser, nettoyer et agréger des datasets tabulaires avec Pandas
  • ✅ Créer des visualisations de données professionnelles et multi-graphiques avec Matplotlib
  • ✅ Entraîner, évaluer et comparer des modèles de Machine Learning avec Scikit-learn
  • ✅ Écrire du code pythonique, documenté et conforme aux standards professionnels PEP 8

Étape suivante recommandée : Approfondissez le Machine Learning en travaillant sur le projet complet Kaggle "Titanic - Machine Learning from Disaster" — il combine tout ce que vous avez appris (Pandas pour le nettoyage, NumPy pour les calculs, Scikit-learn pour la modélisation, Matplotlib pour la visualisation) dans un contexte compétitif réel avec une communauté active de 50 000+ participants et des solutions commentées pour comparer vos approches.

Accédez à des centaines d'examens QCM — Découvrir les offres Premium