Chapitre 14 — Les spécialisations : automatisation, web, data, IA et jeux¶
⏱️ Durée estimée : 8 à 12 heures pour tout parcourir (tu peux aussi ne lire en détail que les domaines qui t'attirent)
📋 Prérequis : avoir terminé le Chapitre 13 — Tests avec pytest et bonnes pratiques, savoir créer un venv et installer des paquets (Chapitre 12), être à l'aise avec les fichiers (Chapitre 07) et les classes (Chapitre 10).
🎯 Objectifs — À la fin de ce chapitre, tu sauras…
- automatiser des tâches répétitives sur tes fichiers avec
pathlib,shutiletargparse;- créer une petite application web avec Flask et une API avec FastAPI ;
- charger, filtrer, résumer des données avec pandas et les représenter avec matplotlib ;
- entraîner et évaluer un premier modèle de machine learning avec scikit-learn ;
- programmer la boucle de jeu d'un mini-jeu avec pygame ;
- choisir une spécialisation et suivre un parcours d'apprentissage pour progresser.
Cours¶
Félicitations ! 🎉 Tu maîtrises maintenant les bases de Python : variables, conditions, boucles, structures de données, fonctions, fichiers, exceptions, modules, classes, tests et outils. C'est comme si tu avais appris à lire, écrire et compter.
La question devient : qu'est-ce que tu veux construire ? Python est un couteau suisse. Le même langage sert à ranger tes photos, faire tourner un site web, analyser des millions de lignes de données, entraîner une intelligence artificielle ou créer un jeu. Chaque domaine a ses propres bibliothèques, ses habitudes et ses outils.
Ce chapitre est une visite guidée de cinq grandes spécialisations. Pour chacune :
- à quoi ça sert, concrètement ;
- les bibliothèques clés ;
- un exemple complet et fonctionnel à recopier et lancer ;
- un mini-parcours pour aller plus loin.
💡 Conseil d'organisation : crée un dossier
specialisations/avec un venv par domaine (automatisation/,web/,data/,ia/,jeux/). Les bibliothèques de data et d'IA sont volumineuses : inutile de les installer là où tu fais du web.
mkdir -p specialisations/web && cd specialisations/web
python3 -m venv .venv # Windows : py -m venv .venv
source .venv/bin/activate # Windows : .venv\Scripts\Activate.ps1
1. Automatisation : faire travailler l'ordinateur à ta place¶
1.1 À quoi ça sert ?¶
Toute tâche répétitive sur un ordinateur peut probablement être automatisée : renommer 300 photos, trier un dossier « Téléchargements » en désordre, fusionner des fichiers Excel chaque lundi, envoyer un e-mail de rappel, récupérer une information sur un site web tous les matins, sauvegarder un dossier…
C'est souvent la spécialisation la plus rentable pour un débutant : quelques dizaines de lignes de Python te font gagner des heures, dès maintenant, sans rien installer d'autre que Python.
🧠 Règle empirique : si tu fais la même manipulation plus de trois fois, ou si elle prend plus de dix minutes, écris un script.
1.2 Bibliothèques clés¶
| Bibliothèque | Rôle | Installation |
|---|---|---|
pathlib, shutil, os |
parcourir, copier, déplacer, renommer des fichiers | incluses dans Python |
argparse |
créer des commandes avec options (--aide, --executer…) |
incluse |
csv, json, zipfile |
lire/écrire des formats courants, compresser | incluses |
datetime, time, sched |
dates, attentes, planification simple | incluses |
subprocess |
lancer d'autres programmes | incluse |
requests |
télécharger des pages web, appeler des API | pip install requests |
beautifulsoup4 |
extraire des informations d'une page HTML (scraping) | pip install beautifulsoup4 |
openpyxl |
lire/écrire des fichiers Excel .xlsx |
pip install openpyxl |
pypdf |
lire, fusionner, découper des PDF | pip install pypdf |
Pillow |
redimensionner, convertir des images | pip install pillow |
schedule |
lancer une fonction à heure fixe | pip install schedule |
playwright / selenium |
piloter un vrai navigateur | pip install playwright |
1.3 Exemple complet : trier un dossier par type de fichier¶
Ce script range chaque fichier d'un dossier dans un sous-dossier selon son extension (Images/, Documents/, Audio/…). Par sécurité, il fonctionne d'abord en mode simulation : il affiche ce qu'il ferait sans rien toucher. Il faut ajouter --executer pour qu'il déplace vraiment les fichiers.
Enregistre-le sous trier_fichiers.py :
"""Range les fichiers d'un dossier dans des sous-dossiers selon leur type.
Usage :
python trier_fichiers.py chemin/du/dossier # simulation
python trier_fichiers.py chemin/du/dossier --executer # pour de vrai
"""
import argparse
import shutil
from pathlib import Path
CATEGORIES = {
"Images": {".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"},
"Documents": {".pdf", ".docx", ".odt", ".txt", ".md", ".xlsx", ".csv"},
"Audio": {".mp3", ".wav", ".flac", ".ogg"},
"Videos": {".mp4", ".mkv", ".avi", ".mov"},
"Archives": {".zip", ".tar", ".gz", ".7z", ".rar"},
"Code": {".py", ".js", ".html", ".css", ".json"},
}
def trouver_categorie(fichier: Path) -> str:
"""Renvoie le nom de la catégorie d'un fichier d'après son extension."""
extension = fichier.suffix.lower()
for categorie, extensions in CATEGORIES.items():
if extension in extensions:
return categorie
return "Autres"
def destination_libre(destination: Path) -> Path:
"""Ajoute un numéro au nom si un fichier du même nom existe déjà."""
if not destination.exists():
return destination
compteur = 1
while True:
candidat = destination.with_stem(f"{destination.stem}_{compteur}")
if not candidat.exists():
return candidat
compteur += 1
def trier_dossier(dossier: Path, executer: bool = False) -> dict[str, int]:
"""Trie les fichiers de dossier. Renvoie le nombre de fichiers par catégorie."""
bilan: dict[str, int] = {}
for fichier in sorted(dossier.iterdir()):
if not fichier.is_file() or fichier.name.startswith("."):
continue # on ignore les sous-dossiers et les fichiers cachés
categorie = trouver_categorie(fichier)
destination = destination_libre(dossier / categorie / fichier.name)
if executer:
destination.parent.mkdir(exist_ok=True)
shutil.move(fichier, destination)
action = "Déplacé" if executer else "[simulation]"
print(f"{action} {fichier.name} → {categorie}/")
bilan[categorie] = bilan.get(categorie, 0) + 1
return bilan
def main() -> None:
analyseur = argparse.ArgumentParser(description="Range un dossier par type.")
analyseur.add_argument("dossier", type=Path, help="dossier à ranger")
analyseur.add_argument(
"--executer", action="store_true", help="déplace vraiment les fichiers"
)
arguments = analyseur.parse_args()
if not arguments.dossier.is_dir():
analyseur.error(f"{arguments.dossier} n'est pas un dossier.")
bilan = trier_dossier(arguments.dossier, arguments.executer)
print("\nBilan :")
for categorie, nombre in sorted(bilan.items()):
print(f" {categorie:<10} {nombre} fichier(s)")
if __name__ == "__main__":
main()
Essaie-le sur un dossier de test, jamais directement sur tes vrais fichiers la première fois :
mkdir bazar
# crée quelques fichiers vides pour tester (macOS / Linux) :
touch bazar/vacances.JPG bazar/cv.pdf bazar/chanson.mp3 bazar/script.py bazar/archive.zip bazar/notes.txt bazar/mystere.xyz
# Windows PowerShell :
mkdir bazar
"vacances.JPG","cv.pdf","chanson.mp3","script.py","archive.zip","notes.txt","mystere.xyz" | ForEach-Object { New-Item "bazar\$_" }
python trier_fichiers.py bazar # simulation
python trier_fichiers.py bazar --executer # pour de vrai
python trier_fichiers.py --help # aide générée automatiquement par argparse
[simulation] archive.zip → Archives/
[simulation] chanson.mp3 → Audio/
[simulation] cv.pdf → Documents/
[simulation] mystere.xyz → Autres/
[simulation] notes.txt → Documents/
[simulation] script.py → Code/
[simulation] vacances.JPG → Images/
Bilan :
Archives 1 fichier(s)
Audio 1 fichier(s)
Autres 1 fichier(s)
Code 1 fichier(s)
Documents 2 fichier(s)
Images 1 fichier(s)
Ce qu'il faut remarquer :
CATEGORIESest un dictionnaire d'ensembles (chapitre 04) : la rechercheextension in extensionsest instantanée. Pour ajouter un type, tu modifies les données, pas le code.fichier.suffix.lower():.JPGet.jpgsont traités pareil.destination_libreévite d'écraser un fichier existant (photo.jpgdevientphoto_1.jpg). Avec l'automatisation, une erreur se répète sur des centaines de fichiers : sois prudent !argparsetransforme ton script en véritable outil en ligne de commande, avec une aide--helpgratuite.- Les fonctions
trouver_categorieettrier_dossiersont faciles à tester avec pytest ettmp_path(chapitre 13).
⚠️ Les trois règles de l'automatisation sereine : 1) toujours un mode simulation ; 2) tester sur une copie ; 3) ne jamais supprimer quand on peut déplacer.
1.4 Aller plus loin en automatisation¶
- Fichiers : renommage en lot, sauvegarde datée d'un dossier dans un
.zip(shutil.make_archive), recherche de doublons (en comparant des empreintes avechashlib). - Bureautique : fusionner des fichiers Excel avec
openpyxl(ou pandas), assembler des PDF avecpypdf, redimensionner des photos avecPillow. - Web : télécharger des données avec
requests, interroger des API publiques (météo, transports…), extraire des informations avecBeautifulSoup. Respecte les conditions d'utilisation des sites et ne les surcharge pas de requêtes. - Planification : lancer tes scripts automatiquement avec le Planificateur de tâches (Windows),
cron(macOS/Linux) ou la bibliothèqueschedule. - Lecture recommandée : le livre Automate the Boring Stuff with Python d'Al Sweigart (gratuit en ligne, en anglais) est la référence du domaine.
2. Web : créer des sites et des API¶
2.1 À quoi ça sert ?¶
Quand tu ouvres une page web, ton navigateur (le client) envoie une requête HTTP à un serveur, qui renvoie une réponse : une page HTML, une image, ou des données au format JSON.
Navigateur Serveur Python
───────── ──────────────
GET /api/taches ───────▶ cherche les tâches
◀─────── 200 OK [{"id": 1, "titre": "..."}]
Python est très utilisé côté serveur (le back-end) : Instagram, Pinterest ou de nombreux services de grandes entreprises reposent sur Python. Deux types de projets :
- un site web : le serveur renvoie des pages HTML que l'humain lit dans son navigateur ;
- une API (Application Programming Interface) : le serveur renvoie des données (JSON) que d'autres programmes utilisent (une application mobile, un site en JavaScript, un autre script Python…).
Vocabulaire indispensable :
| Terme | Signification |
|---|---|
| Route (ou endpoint) | une adresse du site, comme /, /contact, /api/taches/3 |
| Méthode HTTP | l'intention : GET (lire), POST (créer), PUT/PATCH (modifier), DELETE (supprimer) |
| Code de statut | le résultat : 200 OK, 201 créé, 404 introuvable, 422 données invalides, 500 erreur du serveur |
| JSON | le format de données du web, très proche des dictionnaires Python (chapitre 07) |
| Template | un modèle HTML avec des « trous » remplis par Python |
2.2 Bibliothèques clés¶
| Framework | Points forts | Pour qui ? |
|---|---|---|
| Flask | minimaliste, simple, très bien documenté | premier site, petits projets, apprendre les bases |
| FastAPI | moderne, rapide, validation automatique, documentation d'API générée | API, projets qui utilisent les annotations de type (chapitre 11) |
| Django | « piles incluses » : base de données, comptes utilisateurs, interface d'administration | gros sites complets |
Autour : Jinja2 (templates, utilisé par Flask), SQLAlchemy ou l'ORM de Django (bases de données), pydantic (validation, utilisé par FastAPI), requests/httpx (appeler des API).
2.3 Exemple complet 1 : un site de citations avec Flask¶
Enregistre app_flask.py :
"""Une mini application web de citations avec Flask."""
import random
from flask import Flask, jsonify, render_template_string, request
app = Flask(__name__)
CITATIONS = [
{"auteur": "Victor Hugo", "texte": "La musique, c'est du bruit qui pense."},
{
"auteur": "Marie Curie",
"texte": "Rien dans la vie n'est à craindre, tout est à comprendre.",
},
{
"auteur": "Albert Camus",
"texte": "Au milieu de l'hiver, j'ai découvert en moi un invincible été.",
},
]
PAGE = """
<!doctype html>
<html lang="fr">
<head><meta charset="utf-8"><title>Citation du jour</title></head>
<body style="font-family: sans-serif; max-width: 40rem; margin: 3rem auto;">
<h1>💬 Citation du jour</h1>
<blockquote>« {{ citation.texte }} »</blockquote>
<p>— <em>{{ citation.auteur }}</em></p>
<p><a href="/">Une autre !</a> · <a href="/api/citations">Version JSON</a></p>
<h2>Proposer une citation</h2>
<form method="post" action="/ajouter">
<input name="auteur" placeholder="Auteur" required>
<input name="texte" placeholder="Citation" required size="40">
<button type="submit">Ajouter</button>
</form>
</body>
</html>
"""
@app.route("/")
def accueil():
return render_template_string(PAGE, citation=random.choice(CITATIONS))
@app.route("/api/citations")
def api_citations():
return jsonify(CITATIONS)
@app.route("/ajouter", methods=["POST"])
def ajouter():
CITATIONS.append({"auteur": request.form["auteur"], "texte": request.form["texte"]})
return render_template_string(PAGE, citation=CITATIONS[-1])
if __name__ == "__main__":
app.run(debug=True)
Lance le serveur :
Ouvre http://127.0.0.1:5000 dans ton navigateur. Rafraîchis : la citation change. Clique sur « Version JSON » : tu vois les données brutes. Ajoute une citation avec le formulaire. Pour arrêter le serveur : Ctrl+C dans le terminal.
Décryptage :
@app.route("/")est un décorateur : il associe l'adresse/à la fonctionaccueil. Quand le navigateur demande/, Flask appelleaccueil()et renvoie ce qu'elle retourne.{{ citation.texte }}est un « trou » du template Jinja2, rempli parrender_template_string(PAGE, citation=...). Jinja2 échappe automatiquement les caractères spéciaux : si quelqu'un tape<script>dans le formulaire, il s'affiche comme du texte au lieu de s'exécuter. C'est une protection de sécurité importante.methods=["POST"]: la route/ajoutern'accepte que l'envoi de formulaire.request.form["auteur"]lit le champname="auteur".jsonify(...)transforme une liste/dictionnaire Python en réponse JSON.debug=Truerecharge le serveur à chaque modification du code et affiche des erreurs détaillées. Uniquement en développement, jamais sur un vrai serveur public.- Les citations ajoutées sont perdues à l'arrêt du serveur (elles sont en mémoire). Pour les garder, il faudrait un fichier JSON ou une base de données.
💡 Dans un vrai projet Flask, les templates sont dans des fichiers séparés (
templates/accueil.html) et on utiliserender_template("accueil.html", ...). On a tout mis dans un seul fichier pour que l'exemple soit facile à copier.
Tester une application Flask : Flask fournit un faux navigateur, app.test_client(), parfait avec pytest :
from app_flask import app
def test_accueil():
client = app.test_client()
reponse = client.get("/")
assert reponse.status_code == 200
assert "Citation du jour" in reponse.get_data(as_text=True)
2.4 Exemple complet 2 : une API de tâches avec FastAPI¶
(Les guillemets sont nécessaires à cause des crochets ; [standard] installe aussi le serveur uvicorn et la commande fastapi.)
Enregistre api_taches.py :
"""Une mini API de liste de tâches avec FastAPI."""
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI(title="API des tâches")
class NouvelleTache(BaseModel):
titre: str
priorite: int = 1
class Tache(NouvelleTache):
id: int
terminee: bool = False
taches: dict[int, Tache] = {}
prochain_id = 1
@app.get("/")
def accueil() -> dict[str, str]:
return {"message": "Bienvenue ! Documentation sur /docs"}
@app.get("/taches")
def lister_taches(terminee: bool | None = None) -> list[Tache]:
resultat = list(taches.values())
if terminee is not None:
resultat = [t for t in resultat if t.terminee == terminee]
return resultat
@app.post("/taches", status_code=201)
def creer_tache(nouvelle: NouvelleTache) -> Tache:
global prochain_id
tache = Tache(id=prochain_id, **nouvelle.model_dump())
taches[tache.id] = tache
prochain_id += 1
return tache
@app.get("/taches/{tache_id}")
def lire_tache(tache_id: int) -> Tache:
if tache_id not in taches:
raise HTTPException(status_code=404, detail="Tâche introuvable")
return taches[tache_id]
@app.patch("/taches/{tache_id}/terminer")
def terminer_tache(tache_id: int) -> Tache:
tache = lire_tache(tache_id)
tache.terminee = True
return tache
@app.delete("/taches/{tache_id}", status_code=204)
def supprimer_tache(tache_id: int) -> None:
if taches.pop(tache_id, None) is None:
raise HTTPException(status_code=404, detail="Tâche introuvable")
Lance le serveur de développement :
Puis ouvre http://127.0.0.1:8000/docs : FastAPI a généré tout seul une documentation interactive de ton API ! Clique sur POST /taches → Try it out → modifie le JSON → Execute. Tu peux tester chaque route sans écrire une ligne de code client. 🤩
Tu peux aussi l'interroger depuis un terminal avec curl :
curl -X POST http://127.0.0.1:8000/taches -H "Content-Type: application/json" -d '{"titre": "Tester curl"}'
# {"titre":"Tester curl","priorite":1,"id":1,"terminee":false}
curl http://127.0.0.1:8000/taches
# Windows PowerShell :
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:8000/taches -ContentType "application/json" -Body '{"titre": "Tester"}'
Invoke-RestMethod http://127.0.0.1:8000/taches
Décryptage :
- Les classes
BaseModelde pydantic décrivent la forme des données. Si un client envoie{"priorite": "haute"}(sans titre, et un texte au lieu d'un nombre), FastAPI répond automatiquement422avec un message précis : Field required, Input should be a valid integer. Tu n'écris aucunifde validation. - Les annotations de type (chapitre 11) ne sont plus décoratives :
tache_id: intconvertit et vérifie le paramètre de l'URL ;terminee: bool | None = Nonecrée un paramètre facultatif?terminee=true. -> Tacheindique le format de la réponse, utilisé pour la documentation et le filtrage des données.HTTPException(status_code=404, ...)renvoie une erreur HTTP propre.Tache(NouvelleTache): de l'héritage ! Une tâche, c'est une nouvelle tâche avec unidet un état en plus.
Tester une API FastAPI avec pytest (pip install httpx si besoin) :
from fastapi.testclient import TestClient
from api_taches import app
client = TestClient(app)
def test_scenario_complet():
reponse = client.post("/taches", json={"titre": "Apprendre FastAPI", "priorite": 2})
assert reponse.status_code == 201
assert reponse.json()["id"] == 1
assert client.patch("/taches/1/terminer").json()["terminee"] is True
assert client.delete("/taches/1").status_code == 204
assert client.get("/taches/1").status_code == 404
def test_donnees_invalides():
reponse = client.post("/taches", json={"priorite": "haute"})
assert reponse.status_code == 422
2.5 Aller plus loin en web¶
- Les bases du web : HTML et CSS (indispensables, même côté serveur ; MDN Web Docs est la référence, en français), puis un peu de JavaScript.
- Flask : le tutoriel officiel « Flaskr » (un blog avec base de données et comptes), les templates dans des fichiers, les formulaires, les sessions.
- Bases de données : SQL avec le module
sqlite3(inclus dans Python), puis SQLAlchemy. - FastAPI : le tutoriel officiel (excellent, disponible en partie en français), l'authentification, les bases de données avec SQLModel.
- Django : le tutoriel officiel « Écriture de votre première application Django » (traduit en français) ; le Django Girls Tutorial (en français) est parfait pour débuter.
- Mettre en ligne : héberger ton application sur un service comme PythonAnywhere, Render ou Fly.io ; apprendre les bases de la sécurité (mots de passe hachés, variables d'environnement pour les secrets, HTTPS).
3. Data : analyser et visualiser des données¶
3.1 À quoi ça sert ?¶
Les entreprises, les laboratoires, les associations et les administrations croulent sous les données : ventes, mesures de capteurs, résultats d'enquêtes, statistiques publiques… L'analyse de données consiste à les nettoyer, les explorer, les résumer et les visualiser pour répondre à des questions : quel produit se vend le mieux ? les ventes augmentent-elles ? y a-t-il une différence entre les villes ?
Python est le langage de référence de la data, grâce à un écosystème exceptionnel. C'est aussi la porte d'entrée vers l'IA (section 4).
3.2 Bibliothèques clés¶
| Bibliothèque | Rôle |
|---|---|
| pandas | le tableur de Python : charger (CSV, Excel, JSON, SQL), filtrer, grouper, calculer |
| NumPy | calcul numérique rapide sur des tableaux de nombres (la fondation de tout l'écosystème) |
| matplotlib | la bibliothèque de graphiques historique, très complète |
| seaborn | graphiques statistiques élégants, basés sur matplotlib |
| plotly | graphiques interactifs (zoom, survol) dans le navigateur |
| Jupyter (JupyterLab, ou les notebooks de VS Code) | carnets interactifs : code, résultats et graphiques mélangés, idéal pour explorer |
| polars | alternative moderne et très rapide à pandas |
L'objet central de pandas est le DataFrame : un tableau avec des lignes et des colonnes nommées, comme une feuille de calcul. Chaque colonne est une Series.
3.3 Exemple complet : analyser les ventes d'une boulangerie¶
Crée d'abord le fichier de données ventes.csv (copie-colle ce contenu dans VS Code et enregistre-le en UTF-8) :
date,produit,categorie,quantite,prix_unitaire,ville
2026-01-05,Croissant,Viennoiserie,120,1.10,Lyon
2026-01-05,Baguette,Pain,200,1.20,Lyon
2026-01-12,Pain au chocolat,Viennoiserie,95,1.30,Paris
2026-01-19,Baguette,Pain,260,1.20,Paris
2026-01-26,Éclair,Pâtisserie,40,2.80,Lyon
2026-02-02,Croissant,Viennoiserie,140,1.10,Paris
2026-02-09,Baguette,Pain,230,1.20,Lyon
2026-02-16,Tarte aux pommes,Pâtisserie,25,3.50,Paris
2026-02-23,Pain au chocolat,Viennoiserie,110,1.30,Lyon
2026-03-02,Éclair,Pâtisserie,55,2.80,Paris
2026-03-09,Baguette,Pain,280,1.20,Paris
2026-03-16,Croissant,Viennoiserie,150,1.10,Lyon
2026-03-23,Pain complet,Pain,70,2.10,Lyon
2026-03-30,Tarte aux pommes,Pâtisserie,30,3.50,Lyon
2026-04-06,Pain au chocolat,Viennoiserie,130,1.30,Paris
2026-04-13,Baguette,Pain,300,1.20,Lyon
Puis analyse_ventes.py, dans le même dossier :
"""Analyse des ventes d'une boulangerie avec pandas et matplotlib."""
import matplotlib.pyplot as plt
import pandas as pd
# 1. Charger les données (la colonne date est convertie en vraies dates)
ventes = pd.read_csv("ventes.csv", parse_dates=["date"])
# 2. Découvrir le tableau
print(ventes.head()) # les 5 premières lignes
print(ventes.shape) # (nombre de lignes, nombre de colonnes)
ventes.info() # types des colonnes, valeurs manquantes
# 3. Créer une nouvelle colonne calculée
ventes["chiffre_affaires"] = ventes["quantite"] * ventes["prix_unitaire"]
# 4. Statistiques simples
print(f"\nChiffre d'affaires total : {ventes['chiffre_affaires'].sum():.2f} €")
print(f"Quantité moyenne par vente : {ventes['quantite'].mean():.1f}")
# 5. Filtrer : seulement Paris
paris = ventes[ventes["ville"] == "Paris"]
print(f"Nombre de ventes à Paris : {len(paris)}")
# 6. Regrouper : chiffre d'affaires par catégorie
par_categorie = (
ventes.groupby("categorie")["chiffre_affaires"].sum().sort_values(ascending=False)
)
print("\nChiffre d'affaires par catégorie :")
print(par_categorie)
# 7. Regrouper par mois et par ville (tableau croisé)
ventes["mois"] = ventes["date"].dt.to_period("M")
par_mois = ventes.pivot_table(
index="mois", columns="ville", values="chiffre_affaires", aggfunc="sum"
)
print("\nChiffre d'affaires par mois et par ville :")
print(par_mois)
# 8. Visualiser : deux graphiques côte à côte
figure, (axe1, axe2) = plt.subplots(1, 2, figsize=(12, 5))
par_categorie.plot(kind="bar", ax=axe1, color="#4C72B0")
axe1.set_title("Chiffre d'affaires par catégorie")
axe1.set_xlabel("")
axe1.set_ylabel("Euros")
axe1.tick_params(axis="x", rotation=0)
par_mois.plot(kind="line", marker="o", ax=axe2)
axe2.set_title("Évolution mensuelle par ville")
axe2.set_xlabel("Mois")
axe2.set_ylabel("Euros")
figure.tight_layout()
figure.savefig("ventes.png", dpi=100)
print("\nGraphique enregistré dans ventes.png")
plt.show()
Extrait de la sortie :
date produit categorie quantite prix_unitaire ville
0 2026-01-05 Croissant Viennoiserie 120 1.1 Lyon
1 2026-01-05 Baguette Pain 200 1.2 Lyon
2 2026-01-12 Pain au chocolat Viennoiserie 95 1.3 Paris
...
(16, 6)
...
Chiffre d'affaires total : 3016.00 €
Quantité moyenne par vente : 139.7
Nombre de ventes à Paris : 7
Chiffre d'affaires par catégorie :
categorie
Pain 1671.0
Viennoiserie 886.5
Pâtisserie 458.5
Name: chiffre_affaires, dtype: float64
Chiffre d'affaires par mois et par ville :
ville Lyon Paris
mois
2026-01 484.0 435.5
2026-02 419.0 241.5
2026-03 417.0 490.0
2026-04 360.0 169.0
Graphique enregistré dans ventes.png
Une fenêtre s'ouvre avec deux graphiques : un diagramme en barres (le pain domine largement) et deux courbes mensuelles (Lyon est plus régulière que Paris). L'image est aussi enregistrée dans ventes.png.
Décryptage des opérations pandas essentielles :
| Code | Effet | Équivalent « Python pur » |
|---|---|---|
ventes["quantite"] |
une colonne (Series) | [ligne["quantite"] for ligne in lignes] |
ventes["quantite"] * ventes["prix_unitaire"] |
calcul ligne par ligne, sans boucle | une boucle for |
ventes[ventes["ville"] == "Paris"] |
filtre les lignes | compréhension avec if |
ventes.groupby("categorie")[...].sum() |
somme par groupe | dictionnaire d'accumulation |
.sort_values(ascending=False) |
trie | sorted(..., reverse=True) |
pivot_table(index=..., columns=..., values=..., aggfunc=...) |
tableau croisé dynamique | double dictionnaire |
ventes["date"].dt.to_period("M") |
extrait le mois d'une date | date.strftime("%Y-%m") |
💡 Combiner des conditions : avec pandas, on n'utilise pas
and/ormais&/|, et chaque condition entre parenthèses :ventes[(ventes["ville"] == "Lyon") & (ventes["quantite"] > 100)].💡 Jupyter : pour explorer des données, les notebooks sont bien plus confortables qu'un script : tu exécutes cellule par cellule et les tableaux s'affichent joliment. Dans VS Code, crée un fichier
.ipynb(installe le paquetipykerneldans ton venv), ou installe JupyterLab (pip install jupyterlabpuisjupyter lab).
3.4 Aller plus loin en data¶
- pandas en profondeur : le guide « 10 minutes to pandas » de la documentation officielle, puis la gestion des valeurs manquantes (
isna,fillna,dropna), les jointures (merge), les dates. - Vraies données : explore les jeux de données ouverts de data.gouv.fr (au format CSV, en français), l'INSEE, ou Kaggle.
- Visualisation : seaborn pour les graphiques statistiques, plotly pour l'interactif ; apprends à choisir le bon graphique (barres pour comparer, courbe pour une évolution, nuage de points pour une relation).
- Statistiques : moyenne, médiane, écart-type, corrélation… La data sans statistiques, c'est comme la cuisine sans goûter.
- SQL : le langage des bases de données, incontournable pour tout poste en data.
- Tableaux de bord : Streamlit permet de transformer un script d'analyse en application web interactive en quelques lignes.
4. Intelligence artificielle et machine learning¶
4.1 À quoi ça sert ?¶
Le machine learning (apprentissage automatique) consiste à laisser un programme apprendre des règles à partir d'exemples, au lieu de les écrire à la main.
Analogie : pour apprendre à un enfant à reconnaître les chiens et les chats, tu ne lui donnes pas une liste de règles (« si les oreilles sont pointues et que… »). Tu lui montres des centaines d'exemples étiquetés, et il finit par généraliser. Le machine learning fait pareil avec des données.
Exemples d'applications : filtre anti-spam, recommandation de films, détection de fraude bancaire, reconnaissance d'images, traduction automatique, prévision de ventes… et les grands modèles de langage (les assistants conversationnels), qui sont l'aboutissement (très) avancé de ces idées.
Le vocabulaire de base :
| Terme | Signification | Dans l'exemple ci-dessous |
|---|---|---|
Caractéristiques (features, souvent X) |
les informations d'entrée | 4 mesures d'une fleur |
Étiquette / cible (label, y) |
ce qu'on veut prédire | l'espèce de la fleur |
| Classification | prédire une catégorie | setosa, versicolor ou virginica |
| Régression | prédire un nombre | (ex. le prix d'une maison) |
| Entraînement (fit) | le modèle apprend sur des exemples | modele.fit(X, y) |
| Prédiction (predict) | le modèle devine pour de nouvelles données | modele.predict(X_nouveau) |
| Jeu de test | des exemples cachés pendant l'entraînement, pour évaluer honnêtement | 25 % des fleurs |
⚠️ La règle d'or : on évalue toujours un modèle sur des données qu'il n'a jamais vues. Sinon, c'est comme donner à un élève l'interrogation avec les réponses la veille : il aura 20/20 sans avoir rien compris.
4.2 Bibliothèques clés¶
| Bibliothèque | Rôle |
|---|---|
| scikit-learn | le machine learning « classique » : classification, régression, regroupement, évaluation. Idéal pour débuter. |
| NumPy / pandas | préparer les données (voir section 3) |
| PyTorch | réseaux de neurones et deep learning (recherche et industrie) |
| TensorFlow / Keras | autre grande famille de deep learning ; Keras est très accessible |
| Hugging Face Transformers | utiliser des modèles pré-entraînés (texte, images, audio) |
| SDK des fournisseurs d'IA | appeler de grands modèles de langage via leurs API depuis Python |
4.3 Exemple complet : reconnaître des espèces d'iris¶
Le jeu de données iris est le « Hello, World! » du machine learning : 150 fleurs, pour chacune 4 mesures (longueur et largeur des sépales et des pétales, en cm) et son espèce (setosa, versicolor ou virginica). Il est inclus dans scikit-learn : rien à télécharger.
Enregistre iris_classification.py :
"""Classer des fleurs d'iris avec scikit-learn."""
from sklearn.datasets import load_iris
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
# 1. Charger les données : 150 fleurs, 4 mesures chacune, 3 espèces
iris = load_iris()
X = iris.data # les mesures (entrées)
y = iris.target # l'espèce, codée 0, 1 ou 2 (sortie à prédire)
print("Mesures :", iris.feature_names)
print("Espèces :", iris.target_names.tolist())
print("Première fleur :", X[0], "→", iris.target_names[y[0]])
# 2. Séparer : 75 % pour apprendre, 25 % pour évaluer
X_entrainement, X_test, y_entrainement, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y
)
print(f"\n{len(X_entrainement)} fleurs pour l'entraînement, {len(X_test)} pour le test")
# 3. Choisir un modèle et l'entraîner
modele = KNeighborsClassifier(n_neighbors=5)
modele.fit(X_entrainement, y_entrainement)
# 4. Prédire sur des fleurs jamais vues et mesurer la précision
predictions = modele.predict(X_test)
precision = accuracy_score(y_test, predictions)
print(f"\nPrécision des k plus proches voisins : {precision:.1%}")
print("Matrice de confusion :")
print(confusion_matrix(y_test, predictions))
print(classification_report(y_test, predictions, target_names=iris.target_names))
# 5. Comparer avec un autre modèle : même interface fit / predict !
arbre = DecisionTreeClassifier(max_depth=3, random_state=42)
arbre.fit(X_entrainement, y_entrainement)
print(f"Précision de l'arbre de décision : {arbre.score(X_test, y_test):.1%}")
# 6. Utiliser le modèle sur une nouvelle fleur cueillie dans le jardin
nouvelle_fleur = [[6.1, 2.9, 4.7, 1.4]] # longueur/largeur sépale, pétale (cm)
espece = iris.target_names[modele.predict(nouvelle_fleur)[0]]
print(f"\nLa nouvelle fleur est probablement : {espece}")
Mesures : ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
Espèces : ['setosa', 'versicolor', 'virginica']
Première fleur : [5.1 3.5 1.4 0.2] → setosa
112 fleurs pour l'entraînement, 38 pour le test
Précision des k plus proches voisins : 97.4%
Matrice de confusion :
[[12 0 0]
[ 0 13 0]
[ 0 1 12]]
precision recall f1-score support
setosa 1.00 1.00 1.00 12
versicolor 0.93 1.00 0.96 13
virginica 1.00 0.92 0.96 13
accuracy 0.97 38
macro avg 0.98 0.97 0.97 38
weighted avg 0.98 0.97 0.97 38
Précision de l'arbre de décision : 89.5%
La nouvelle fleur est probablement : versicolor
(Les chiffres exacts peuvent légèrement varier selon ta version de scikit-learn.)
Décryptage :
train_test_splitmélange et coupe les données.random_state=42fixe le hasard pour obtenir le même découpage à chaque exécution (reproductibilité).stratify=ygarde la même proportion de chaque espèce dans les deux parties.- k plus proches voisins (k-NN) : pour classer une nouvelle fleur, le modèle cherche les 5 fleurs d'entraînement les plus ressemblantes (les plus proches en mesures) et vote pour l'espèce majoritaire. Simple et intuitif !
- Arbre de décision : le modèle apprend une suite de questions (« longueur du pétale < 2,45 cm ? »), comme un jeu des 20 questions.
- Tous les modèles de scikit-learn ont la même interface :
fitpour apprendre,predictpour prédire,scorepour évaluer. Changer de modèle = changer une ligne. - Matrice de confusion : ligne = vraie espèce, colonne = espèce prédite. La diagonale = bonnes réponses. Ici, une seule erreur : une virginica prise pour une versicolor (ces deux espèces se ressemblent).
- Précision 97,4 % : 37 fleurs bien classées sur 38. Sur un jeu de test de 38 exemples, une erreur de plus ou de moins change beaucoup le pourcentage : ne tire pas de conclusions hâtives sur de petits jeux de données (voir la validation croisée dans l'exercice 6).
🧭 Et ChatGPT, Claude, etc. ? Les grands modèles de langage sont des réseaux de neurones gigantesques entraînés sur d'énormes quantités de texte. Tu ne vas pas en entraîner un, mais tu peux les utiliser depuis Python via l'API de leur fournisseur (une requête HTTP, comme en section 2), ou faire tourner de petits modèles ouverts avec Hugging Face. Comprendre les bases vues ici (données, entraînement, évaluation, surapprentissage) t'aidera à les utiliser avec discernement.
4.4 Aller plus loin en IA¶
- Consolider la data (section 3) : 80 % du travail en machine learning consiste à préparer les données.
- scikit-learn : les tutoriels officiels ; la régression linéaire, la normalisation des données (
StandardScaler), la validation croisée, les pipelines ; le surapprentissage et comment l'éviter. - Un peu de mathématiques : statistiques, probabilités, algèbre linéaire de base (vecteurs, matrices). Pas besoin d'être un génie, mais l'intuition aide énormément.
- Compétitions pour débutants : Kaggle propose des problèmes guidés (le Titanic est un classique) et des mini-cours gratuits.
- Deep learning : le cours gratuit Practical Deep Learning for Coders (fast.ai), ou les tutoriels de PyTorch ; reconnaissance d'images, traitement du texte.
- Les grands modèles de langage : appeler une API d'IA depuis Python, apprendre à rédiger de bonnes consignes, découvrir les « agents » et la recherche documentaire augmentée (RAG).
- Éthique : biais dans les données, vie privée, impact environnemental. Un bon praticien de l'IA se pose ces questions.
5. Jeux vidéo avec pygame¶
5.1 À quoi ça sert ?¶
Programmer un jeu est la façon la plus amusante de consolider tout ce que tu as appris : boucles, conditions, listes, classes, fichiers… et un peu de géométrie. Le résultat est immédiatement visible et partageable. Python n'est pas le langage des jeux AAA (trop lent pour cela), mais il est parfait pour des jeux 2D, des prototypes, des game jams et pour apprendre les concepts du jeu vidéo.
5.2 Bibliothèques clés¶
| Bibliothèque | Points forts |
|---|---|
| pygame (ou sa variante communautaire pygame-ce) | la référence historique : fenêtre, dessin, images, sons, clavier/souris. Énormément de tutoriels. |
| Arcade | plus moderne, orientée objet, bien documentée |
| Pyxel | moteur rétro « pixel art » avec éditeur intégré, très ludique |
| Ursina | jeux en 3D simples |
| Godot (langage GDScript, proche de Python) | un vrai moteur de jeu complet, si tu veux aller plus loin |
5.3 Le concept clé : la boucle de jeu¶
Un jeu vidéo est un dessin animé interactif. Comme un dessin animé, il affiche des images successives très rapidement (60 par seconde). Entre chaque image, le programme fait toujours les trois mêmes choses :
┌──────────────────────────────────────────────────┐
│ tant que le jeu tourne : │
│ 1. ÉVÉNEMENTS → touches, souris, fermeture │
│ 2. MISE À JOUR → déplacer, collisions, score │
│ 3. AFFICHAGE → effacer, tout redessiner │
│ (attendre pour tenir 60 images par seconde) │
└──────────────────────────────────────────────────┘
C'est une simple boucle while (chapitre 03) ! Tout jeu, du Pong à un jeu moderne, repose sur ce squelette.
Autre notion : le repère de l'écran. L'origine (0, 0) est en haut à gauche ; x augmente vers la droite et y vers le bas (l'inverse des maths du collège !).
5.4 Exemple complet : Attrape-étoiles¶
Des étoiles tombent du ciel ; déplace le panier avec les flèches pour les attraper. Chaque étoile ratée coûte une vie.
Enregistre attrape_etoiles.py :
"""Attrape-étoiles : un mini-jeu avec pygame.
Déplace le panier avec les flèches gauche/droite pour attraper les étoiles.
Tu as 3 vies : chaque étoile ratée en coûte une.
"""
import random
import pygame
LARGEUR, HAUTEUR = 640, 480
IPS = 60 # images par seconde
BLANC = (255, 255, 255)
NUIT = (15, 20, 45)
JAUNE = (255, 215, 0)
ORANGE = (240, 130, 40)
class Panier:
def __init__(self) -> None:
self.rect = pygame.Rect(0, 0, 100, 20)
self.rect.midbottom = (LARGEUR // 2, HAUTEUR - 20)
self.vitesse = 7
def deplacer(self, touches) -> None:
if touches[pygame.K_LEFT]:
self.rect.x -= self.vitesse
if touches[pygame.K_RIGHT]:
self.rect.x += self.vitesse
self.rect.clamp_ip(pygame.Rect(0, 0, LARGEUR, HAUTEUR)) # reste à l'écran
def dessiner(self, ecran) -> None:
pygame.draw.rect(ecran, ORANGE, self.rect, border_radius=6)
class Etoile:
def __init__(self) -> None:
self.rect = pygame.Rect(random.randint(0, LARGEUR - 20), -20, 20, 20)
self.vitesse = random.uniform(2, 5)
self.y = float(self.rect.y)
def tomber(self) -> None:
self.y += self.vitesse
self.rect.y = int(self.y)
def dessiner(self, ecran) -> None:
pygame.draw.circle(ecran, JAUNE, self.rect.center, 10)
def main() -> None:
pygame.init()
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption("Attrape-étoiles")
horloge = pygame.time.Clock()
police = pygame.font.Font(None, 36)
panier = Panier()
etoiles: list[Etoile] = []
score = 0
vies = 3
en_cours = True
while en_cours:
# 1. ÉVÉNEMENTS : clavier, souris, fermeture de la fenêtre
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
en_cours = False
# 2. MISE À JOUR : on fait évoluer le monde du jeu
if vies > 0:
panier.deplacer(pygame.key.get_pressed())
if random.random() < 0.03: # environ 2 nouvelles étoiles par seconde
etoiles.append(Etoile())
for etoile in etoiles[:]: # on parcourt une copie pour pouvoir retirer
etoile.tomber()
if etoile.rect.colliderect(panier.rect):
etoiles.remove(etoile)
score += 1
elif etoile.rect.top > HAUTEUR:
etoiles.remove(etoile)
vies -= 1
# 3. AFFICHAGE : on redessine tout, puis on montre l'image
ecran.fill(NUIT)
for etoile in etoiles:
etoile.dessiner(ecran)
panier.dessiner(ecran)
texte = police.render(f"Score : {score} Vies : {vies}", True, BLANC)
ecran.blit(texte, (10, 10))
if vies <= 0:
fin = police.render("Perdu ! Ferme la fenêtre pour quitter.", True, BLANC)
ecran.blit(fin, fin.get_rect(center=(LARGEUR // 2, HAUTEUR // 2)))
pygame.display.flip()
horloge.tick(IPS) # limite à 60 images par seconde
pygame.quit()
if __name__ == "__main__":
main()
Décryptage :
pygame.Rectest un rectangle (position + taille) avec des outils géniaux :midbottom,center,colliderect(détection de collision entre deux rectangles),clamp_ip(garder dans l'écran).pygame.event.get()vide la file des événements. Indispensable : sans cette ligne, le système croit que le programme ne répond plus.pygame.key.get_pressed()donne l'état de toutes les touches en ce moment : idéal pour un mouvement continu tant qu'on garde la touche enfoncée. (Pour une action unique, comme sauter ou tirer, on écoute plutôt l'événementpygame.KEYDOWN.)ecran.fill(NUIT)efface tout à chaque image : on ne déplace pas les dessins, on redessine tout à la nouvelle position. C'est le principe du dessin animé.pygame.display.flip()affiche l'image qu'on vient de dessiner (on dessine en coulisses, puis on montre d'un coup, pour éviter le scintillement).horloge.tick(60)fait une petite pause pour ne pas dépasser 60 images par seconde : le jeu tourne à la même vitesse sur tous les ordinateurs.for etoile in etoiles[:]: on parcourt une copie de la liste, car on retire des éléments pendant le parcours (le modifier directement ferait sauter des éléments).- On garde
self.yenfloatcar unRectne stocke que des entiers : sans cela, une vitesse de 2,7 serait arrondie à chaque image. - Les classes (chapitre 10) organisent naturellement un jeu : chaque objet du jeu sait se mettre à jour et se dessiner.
5.5 Aller plus loin en jeux¶
- Classiques à recréer (dans cet ordre de difficulté) : Pong, Snake, Casse-briques, Space Invaders, Flappy Bird, un jeu de plateforme.
- Images et sons : charge des images (
pygame.image.load), joue des sons (pygame.mixer). Des ressources libres de droits existent, par exemple sur Kenney.nl ou OpenGameArt.org. - Organisation : les classes
pygame.sprite.Spriteetpygame.sprite.Group, les états du jeu (menu, partie, pause, fin), le temps écoulé (dt) pour des mouvements indépendants de la vitesse de la machine. - Physique simple : gravité, saut, rebonds (un peu de vecteurs avec
pygame.math.Vector2). - Partage : participe à une game jam (sur itch.io), publie ton jeu sur GitHub, ou empaquette-le avec
pyinstaller, voire pour le navigateur avecpygbag. - Changer d'outil : quand tu voudras des jeux plus ambitieux, essaie Arcade, ou passe à Godot (dont le langage GDScript te paraîtra très familier).
6. Tableau comparatif : quelle spécialisation choisir ?¶
| Critère | 🤖 Automatisation | 🌐 Web | 📊 Data | 🧠 IA / ML | 🎮 Jeux |
|---|---|---|---|---|---|
| Tu aimes… | gagner du temps, l'efficacité | créer des sites, des services utilisés par d'autres | comprendre, chercher des tendances, convaincre avec des chiffres | les défis scientifiques, les prédictions | créer, le visuel, l'interactivité |
| Bibliothèques phares | pathlib, shutil, requests, openpyxl | Flask, FastAPI, Django | pandas, matplotlib, Jupyter | scikit-learn, PyTorch | pygame, Arcade |
| Difficulté pour débuter | ⭐ très accessible | ⭐⭐ (il faut aussi HTML/CSS) | ⭐⭐ | ⭐⭐⭐ (maths, statistiques) | ⭐⭐ |
| Premier résultat utile | en une soirée | en un week-end | en une soirée | en une soirée (mais comprendre prend du temps) | en un week-end |
| Autres connaissances utiles | système de fichiers, ligne de commande | HTML, CSS, HTTP, SQL, un peu de JavaScript | statistiques, SQL, Excel | maths (stats, algèbre linéaire), data | un peu de géométrie, game design, graphisme |
| Débouchés | utile dans tous les métiers ; DevOps, administration système | développeur back-end, full-stack | data analyst, data engineer | data scientist, ingénieur ML | développement de jeux (souvent avec d'autres langages ensuite), prototypage |
| Projet type pour débuter | ranger ses Téléchargements | une API de tâches | analyser ses dépenses | prédire le prix d'un logement | Snake |
🧭 Comment choisir ? Pars de ce que tu as envie de construire, pas de ce qui est « à la mode ». La motivation est ton meilleur carburant. Et rien ne t'empêche de combiner : un script qui automatise la collecte de données, les analyse avec pandas et publie un tableau de bord sur le web, c'est exactement ce que fait le mini-projet de ce chapitre. Si tu hésites vraiment, commence par l'automatisation : c'est utile tout de suite et ça renforce tes bases pour tout le reste.
Pièges fréquents¶
1. Installer les bibliothèques hors du venv (ou dans le mauvais)¶
Symptôme : ModuleNotFoundError: No module named 'pandas' alors que tu viens de l'installer ; ou VS Code souligne import flask en jaune.
Correction : active le bon venv avant pip install (le (.venv) doit être visible), et sélectionne le même interpréteur dans VS Code (Python: Select Interpreter). Rappel : pip install scikit-learn mais import sklearn ; pip install pillow mais import PIL ; pip install beautifulsoup4 mais import bs4. Le nom du paquet et le nom du module ne sont pas toujours identiques.
2. Nommer son fichier comme la bibliothèque¶
Symptôme : ImportError: cannot import name 'Flask' from partially initialized module 'flask' ou AttributeError: module 'pandas' has no attribute 'read_csv'.
Correction : ton fichier s'appelle flask.py, pandas.py, pygame.py… et masque la vraie bibliothèque. Renomme-le (mon_app.py, analyse.py, jeu.py) et supprime le dossier __pycache__.
3. Lancer un script d'automatisation directement sur ses vrais fichiers¶
Symptôme : 500 photos renommées n'importe comment, des fichiers écrasés ou perdus.
Correction : prévois toujours un mode simulation qui affiche les actions sans les faire ; teste sur une copie ou un dossier factice ; vérifie qu'aucun fichier n'est écrasé (exists()) ; préfère déplacer plutôt que supprimer. Avec Git, tu peux aussi versionner les fichiers texte avant une transformation.
4. Le serveur web « ne marche pas »¶
Symptôme : le navigateur affiche « Impossible de se connecter », ou les modifications du code ne s'affichent pas, ou Address already in use.
Correction : le serveur doit tourner dans un terminal pendant que tu utilises le navigateur (ne ferme pas ce terminal). Vérifie l'adresse et le port (5000 pour Flask, 8000 pour FastAPI). Si le port est occupé, un autre serveur tourne déjà : arrête-le avec Ctrl+C, ou change de port (app.run(port=5001), fastapi dev api.py --port 8001). Active debug=True (Flask) ou --reload (uvicorn) pour le rechargement automatique.
5. Le graphique matplotlib ne s'affiche pas (ou bloque le programme)¶
Symptôme : rien ne s'affiche ; ou le programme semble figé après le graphique.
Correction : il faut appeler plt.show() à la fin. Ce dernier bloque le script jusqu'à la fermeture de la fenêtre : c'est normal. Sur un serveur (ou si aucune fenêtre ne peut s'ouvrir), utilise figure.savefig("graphique.png") et ouvre l'image.
6. Évaluer un modèle sur ses données d'entraînement¶
Symptôme : ton modèle affiche 100 % de précision… puis se trompe tout le temps sur de nouvelles données.
Correction : c'est du surapprentissage (le modèle a appris par cœur). Sépare toujours tes données avec train_test_split avant l'entraînement, et n'évalue que sur le jeu de test. Pour des résultats plus fiables, utilise la validation croisée (cross_val_score).
7. Oublier de traiter les événements ou de rafraîchir l'écran dans pygame¶
Symptôme : la fenêtre est noire, ou « ne répond pas », ou ne se ferme pas.
Correction : dans chaque tour de boucle, appelle pygame.event.get() (et gère pygame.QUIT), dessine, puis pygame.display.flip(). Sans horloge.tick(60), le jeu tourne trop vite et utilise 100 % du processeur.
8. Vouloir tout apprendre en même temps¶
Symptôme : tu enchaînes les tutoriels Django, PyTorch et pygame en une semaine… et tu as l'impression de ne rien maîtriser.
Correction : choisis un domaine pour les prochains mois et réalise un projet concret de bout en bout (chapitre 15). La profondeur vaut mieux que la largeur au début. Les autres domaines seront beaucoup plus faciles ensuite : les bases de Python sont les mêmes partout.
Exercices¶
Chaque exercice correspond à un domaine. Fais au moins ceux de la spécialisation qui t'attire, et un ou deux autres par curiosité.
Exercice 1 ⭐ — Automatisation : inventaire d'un dossier¶
Écris un script inventaire.py qui parcourt un dossier et tous ses sous-dossiers (passé en argument, ou le dossier courant par défaut) et affiche, pour chaque extension, le nombre de fichiers et la taille totale, triés du plus fréquent au moins fréquent, avec un total à la fin.
Écris une fonction taille_lisible(octets) qui renvoie "500 octets", "2.0 Ko", "5.0 Mo", "3.0 Go"…
Indices : Path.rglob("*") parcourt récursivement ; fichier.stat().st_size donne la taille en octets ; collections.Counter (chapitre 09) est parfait pour compter.
Exercice 2 ⭐ — Web : nouvelles routes Flask¶
Pars de l'application de citations Flask (section 2.3) et ajoute :
- une route
/citation/<int:numero>qui affiche la citation n°numero(en commençant à 1), et renvoie une erreur 404 si le numéro n'existe pas ; - une route
/auteursqui affiche la liste HTML des auteurs, sans doublon et triés par ordre alphabétique, avec leur nombre dans le titre ; - une route
/api/auteursqui renvoie cette même liste en JSON.
Écris quelques tests avec app.test_client().
Indices : <int:numero> dans la route passe un entier à la fonction ; abort(404) (à importer de flask) renvoie une erreur ; dans un template Jinja2, {% for x in liste %}...{% endfor %} fait une boucle et {{ liste|length }} donne la longueur.
Exercice 3 ⭐⭐ — Web : enrichir l'API FastAPI¶
Améliore l'API des tâches (section 2.4) :
- la validation : le titre doit faire entre 1 et 100 caractères et la priorité être comprise entre 1 et 3 (sinon, erreur 422 automatique) ;
GET /tachesaccepte un paramètre facultatifrecherche(recherche insensible à la casse dans le titre) et renvoie les tâches triées par priorité décroissante, puis parid;- une route
GET /statsqui renvoie{"total": ..., "terminees": ..., "restantes": ..., "pourcentage": ...}.
Écris des tests avec TestClient. Problème : les tâches sont stockées dans une variable globale partagée entre les tests. Écris une fixture qui la réinitialise avant chaque test.
Indices : from pydantic import Field puis titre: str = Field(min_length=1, max_length=100) et priorite: int = Field(default=1, ge=1, le=3) (ge = greater or equal, le = less or equal) ; @pytest.fixture(autouse=True) applique une fixture à tous les tests du fichier.
Exercice 4 ⭐⭐ — Data : enquête à la boulangerie¶
Avec ventes.csv (section 3.3) et pandas, réponds par un script aux questions suivantes :
- Quel produit s'est le plus vendu en quantité ? (et combien d'unités ?)
- Quel est le chiffre d'affaires de chaque ville ?
- Quelle est la vente (ligne) qui a rapporté le plus ? Affiche le produit, la date au format
jj/mm/aaaa, la ville et le montant. - Affiche les ventes de viennoiseries à Lyon de plus de 100 unités (colonnes date, produit, quantité).
- Quel est le prix moyen d'un article dans chaque catégorie (chiffre d'affaires total ÷ quantité totale) ?
- Exporte un résumé par produit (quantité totale, chiffre d'affaires, nombre de ventes) dans
resume_produits.csv, trié par chiffre d'affaires décroissant.
Indices : Series.idxmax() renvoie l'étiquette (ou l'index) de la valeur maximale ; df.loc[index] renvoie une ligne ; groupby(...).agg(nom=("colonne", "sum"), ...) crée plusieurs colonnes résumées d'un coup ; df.to_csv("fichier.csv").
Exercice 5 ⭐⭐ — Data : un graphique soigné¶
Réalise un diagramme en barres horizontales du chiffre d'affaires par produit :
- barres triées (le plus gros chiffre d'affaires en haut) ;
- montant affiché au bout de chaque barre (
"1524 €") ; - un titre, un nom d'axe, sans les bordures du haut et de droite ;
- enregistré dans
ca_par_produit.png.
Indices : axe.barh(...) renvoie les barres ; axe.bar_label(barres, labels=[...], padding=4) ajoute les étiquettes ; axe.spines[["top", "right"]].set_visible(False) masque des bordures.
Exercice 6 ⭐⭐⭐ — IA : classer des vins et choisir ses réglages¶
scikit-learn contient un autre jeu de données : load_wine() (178 vins, 13 mesures chimiques, 3 cépages).
- Sépare les données (25 % de test,
random_state=42, stratifié) et entraîne un k-NN (k=5). Note la précision. - Recommence en ajoutant une mise à l'échelle des données avec
StandardScaler, dans un pipeline. Compare. Pourquoi une telle différence ? - Pour
kallant de 1 à 15 (impairs), calcule la précision moyenne par validation croisée à 5 plis sur les données d'entraînement uniquement. Choisis le meilleurk, entraîne le modèle final et évalue-le une seule fois sur le jeu de test.
Indices : from sklearn.pipeline import make_pipeline, make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=k)) ; from sklearn.model_selection import cross_val_score, cross_val_score(modele, X, y, cv=5) renvoie 5 scores.
Exercice 7 ⭐⭐⭐ — Automatisation : renommage en lot sécurisé¶
Écris un outil en ligne de commande renommer_lot.py :
python renommer_lot.py photos vacances --extension .jpg # simulation
python renommer_lot.py photos vacances --extension .jpg --executer
qui renomme les fichiers (triés par nom) en vacances_001.jpg, vacances_002.jpg… avec les exigences suivantes :
- seuls les fichiers de l'extension demandée sont traités (tous si l'option est absente), en ignorant la casse ; l'extension est mise en minuscules ;
- les fichiers cachés (commençant par
.) sont ignorés ; - mode simulation par défaut ;
- aucun fichier ne doit jamais être écrasé, y compris dans le cas piégeux où un fichier porte déjà un des noms cibles (ex.
vacances_002.jpgexiste et fait partie des fichiers à renommer) ; - relancer le script une deuxième fois ne doit rien casser.
Exercice 8 ⭐⭐⭐ — Jeux : Attrape-étoiles, le retour¶
Améliore le jeu de la section 5.4 :
- un niveau qui augmente toutes les 10 étoiles attrapées, rendant les étoiles plus rapides et plus fréquentes ;
- des étoiles rouges piégées (1 sur 5) : les attraper fait perdre une vie, les laisser tomber ne coûte rien ;
- en fin de partie :
Rpour rejouer,Échappour quitter ; - un meilleur score sauvegardé dans un fichier
record.txtet affiché en permanence.
Organise le code avec une classe Jeu qui contient l'état de la partie (score, vies, panier, étoiles) et des méthodes recommencer, mettre_a_jour et dessiner. Utilise des @property (chapitre 11) pour niveau et termine.
Mini-projet¶
🌦️ Ma station météo : automatisation + data + web¶
Tu vas combiner trois spécialisations dans un seul petit projet : un script automatise la production d'un fichier de relevés météo, un module analyse ces données avec pandas et produit un graphique, et une application web Flask affiche le tout dans un tableau de bord.
Cahier des charges¶
- Génération des données (
generer_donnees.py) : produit un fichierreleves.csvavec une ligne par jour et par ville (Brest, Lyon, Marseille) sur une année, et les colonnesdate, ville, temp_min, temp_max, pluie_mm. Les valeurs sont fictives mais réalistes : plus froid en hiver qu'en été, Marseille plus chaude que Brest, environ un jour de pluie sur trois, ettemp_min <= temp_max. Avec la même « graine », le fichier doit être identique à chaque exécution. - Analyse (
analyse.py), trois fonctions : charger(chemin)→ DataFrame avec une colonnetemp_moyenneen plus ;resume_par_ville(releves)→ par ville : température moyenne, record de chaleur, record de froid, nombre de jours de pluie, cumul de pluie ;tracer_mensuel(releves, chemin_image)→ graphique de la température moyenne par mois, une courbe par ville, enregistré en PNG.- Tableau de bord (
app.py) : une page/qui affiche le nombre de relevés, le tableau résumé et le graphique ; une route/api/resumequi renvoie le résumé en JSON. - Tests (
test_meteo.py) : au moins 5 tests pytest (génération, cohérence des données, résumé, graphique, application web), qui n'utilisent que des fichiers temporaires. - Qualité : venv,
requirements.txt,.gitignore,ruff checketruff format --checksans erreur, projet sous Git (chapitre 12).
Étapes suggérées¶
- Prépare le projet : dossier, venv,
pip install pandas matplotlib flask pytest ruff,pip freeze > requirements.txt,git init. - Écris le générateur. Commence simplement (températures aléatoires), puis ajoute l'effet des saisons. Ouvre le CSV pour vérifier.
- Écris
analyse.pyet teste chaque fonction dans un petit script ou dans un notebook. - Écris l'application Flask. Commence par afficher juste le nombre de relevés, puis le tableau, puis l'image.
- Écris les tests, lance ruff, committe.
Indices :
- Utilise
random.Random(graine)pour un générateur de hasard reproductible, ethasard.gauss(moyenne, ecart_type)pour des variations naturelles. - L'effet des saisons peut être imité par un cosinus :
-8 * math.cos(2 * math.pi * (jour_de_l_annee - 15) / 365)vaut environ -8 mi-janvier et +8 mi-juillet.date.timetuple().tm_ydaydonne le numéro du jour dans l'année. DataFrame.to_html()produit un tableau HTML ; dans un template Jinja2,{{ tableau|safe }}l'insère sans l'échapper (uniquement pour du HTML que tu as généré).- Flask sert automatiquement les fichiers du dossier
static/à l'adresse/static/.... - Sur un serveur web, utilise
matplotlib.use("Agg")(dessin sans fenêtre) etplt.close(figure)aprèssavefigpour libérer la mémoire.
Pistes d'amélioration¶
- Remplace les données fictives par de vraies données : l'API gratuite Open-Meteo (
requests) fournit l'historique météo de n'importe quelle ville. - Ajoute un formulaire pour choisir la ville et la période affichées.
- Ajoute un modèle scikit-learn de régression qui prédit la température du lendemain à partir des jours précédents (la quatrième spécialisation !).
- Planifie la mise à jour quotidienne des données.
- Transforme le tableau de bord en application Streamlit et compare les deux approches.
Corrigés¶
Corrigé de l'exercice 1
"""Inventaire d'un dossier : nombre de fichiers et taille totale par extension."""
import sys
from collections import Counter
from pathlib import Path
def taille_lisible(octets: float) -> str:
"""Convertit un nombre d'octets en texte lisible (Ko, Mo, Go)."""
if octets < 1024:
return f"{octets} octets"
for unite in ("Ko", "Mo", "Go"):
octets /= 1024
if octets < 1024:
return f"{octets:.1f} {unite}"
return f"{octets / 1024:.1f} To"
def inventorier(dossier: Path) -> tuple[Counter, Counter]:
nombres: Counter = Counter()
tailles: Counter = Counter()
for fichier in dossier.rglob("*"): # rglob : parcourt aussi les sous-dossiers
if fichier.is_file():
extension = fichier.suffix.lower() or "(sans extension)"
nombres[extension] += 1
tailles[extension] += fichier.stat().st_size
return nombres, tailles
def main() -> None:
dossier = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
nombres, tailles = inventorier(dossier)
print(f"Inventaire de {dossier.resolve()}\n")
print(f"{'Extension':<18}{'Fichiers':>9}{'Taille':>14}")
for extension, nombre in nombres.most_common():
print(f"{extension:<18}{nombre:>9}{taille_lisible(tailles[extension]):>14}")
print(f"\nTotal : {nombres.total()} fichiers, {taille_lisible(tailles.total())}")
if __name__ == "__main__":
main()
Corrigé de l'exercice 2
`app_flask_ex.py` :"""Exercice 2 : l'application de citations, avec deux nouvelles routes."""
import random
from flask import Flask, abort, jsonify, render_template_string
app = Flask(__name__)
CITATIONS = [
{"auteur": "Victor Hugo", "texte": "La musique, c'est du bruit qui pense."},
{
"auteur": "Marie Curie",
"texte": "Rien dans la vie n'est à craindre, tout est à comprendre.",
},
{
"auteur": "Albert Camus",
"texte": "Au milieu de l'hiver, j'ai découvert en moi un invincible été.",
},
]
PAGE_CITATION = """
<h1>Citation n° {{ numero }}</h1>
<blockquote>« {{ citation.texte }} »</blockquote>
<p>— {{ citation.auteur }}</p>
"""
PAGE_AUTEURS = """
<h1>Auteurs ({{ auteurs|length }})</h1>
<ul>
{% for auteur in auteurs %}
<li>{{ auteur }}</li>
{% endfor %}
</ul>
"""
@app.route("/")
def accueil():
citation = random.choice(CITATIONS)
return f"« {citation['texte']} » — {citation['auteur']}"
@app.route("/citation/<int:numero>")
def citation_numero(numero: int):
if not 1 <= numero <= len(CITATIONS):
abort(404)
return render_template_string(
PAGE_CITATION, numero=numero, citation=CITATIONS[numero - 1]
)
@app.route("/auteurs")
def auteurs():
noms = sorted({c["auteur"] for c in CITATIONS})
return render_template_string(PAGE_AUTEURS, auteurs=noms)
@app.route("/api/auteurs")
def api_auteurs():
return jsonify(sorted({c["auteur"] for c in CITATIONS}))
if __name__ == "__main__":
app.run(debug=True)
from app_flask_ex import app
client = app.test_client()
def test_citation_existante():
reponse = client.get("/citation/2")
assert reponse.status_code == 200
assert "Marie Curie" in reponse.get_data(as_text=True)
def test_citation_inexistante():
assert client.get("/citation/99").status_code == 404
assert client.get("/citation/0").status_code == 404
def test_auteurs():
html = client.get("/auteurs").get_data(as_text=True)
assert "Auteurs (3)" in html
assert html.index("Albert Camus") < html.index("Victor Hugo") # ordre alphabétique
def test_api_auteurs():
assert client.get("/api/auteurs").get_json() == [
"Albert Camus",
"Marie Curie",
"Victor Hugo",
]
Corrigé de l'exercice 3
`api_taches_ex.py` :"""Exercice 3 : l'API des tâches, avec recherche, tri et statistiques."""
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
app = FastAPI(title="API des tâches")
class NouvelleTache(BaseModel):
titre: str = Field(min_length=1, max_length=100)
priorite: int = Field(default=1, ge=1, le=3)
class Tache(NouvelleTache):
id: int
terminee: bool = False
taches: dict[int, Tache] = {}
prochain_id = 1
@app.get("/taches")
def lister_taches(
terminee: bool | None = None, recherche: str | None = None
) -> list[Tache]:
resultat = list(taches.values())
if terminee is not None:
resultat = [t for t in resultat if t.terminee == terminee]
if recherche:
resultat = [t for t in resultat if recherche.lower() in t.titre.lower()]
# les plus prioritaires d'abord (3 = haute), puis par ordre de création
return sorted(resultat, key=lambda t: (-t.priorite, t.id))
@app.post("/taches", status_code=201)
def creer_tache(nouvelle: NouvelleTache) -> Tache:
global prochain_id
tache = Tache(id=prochain_id, **nouvelle.model_dump())
taches[tache.id] = tache
prochain_id += 1
return tache
@app.patch("/taches/{tache_id}/terminer")
def terminer_tache(tache_id: int) -> Tache:
if tache_id not in taches:
raise HTTPException(status_code=404, detail="Tâche introuvable")
taches[tache_id].terminee = True
return taches[tache_id]
@app.get("/stats")
def statistiques() -> dict[str, int | float]:
total = len(taches)
terminees = sum(1 for t in taches.values() if t.terminee)
return {
"total": total,
"terminees": terminees,
"restantes": total - terminees,
"pourcentage": round(100 * terminees / total, 1) if total else 0.0,
}
import pytest
from fastapi.testclient import TestClient
import api_taches_ex
from api_taches_ex import app
client = TestClient(app)
@pytest.fixture(autouse=True)
def reinitialiser():
"""Vide les tâches avant chaque test (autouse : appliquée automatiquement)."""
api_taches_ex.taches.clear()
api_taches_ex.prochain_id = 1
def creer(titre, priorite=1):
return client.post("/taches", json={"titre": titre, "priorite": priorite})
def test_stats_vides():
assert client.get("/stats").json() == {
"total": 0,
"terminees": 0,
"restantes": 0,
"pourcentage": 0.0,
}
def test_tri_et_recherche():
creer("Faire les courses", 1)
creer("Réviser Python", 3)
creer("Courses de Noël", 2)
titres = [t["titre"] for t in client.get("/taches").json()]
assert titres == ["Réviser Python", "Courses de Noël", "Faire les courses"]
trouvees = client.get("/taches", params={"recherche": "COURSES"}).json()
assert len(trouvees) == 2
def test_stats():
creer("A")
creer("B")
creer("C")
client.patch("/taches/2/terminer")
stats = client.get("/stats").json()
assert stats == {"total": 3, "terminees": 1, "restantes": 2, "pourcentage": 33.3}
@pytest.mark.parametrize(
"donnees",
[{"titre": ""}, {"titre": "Ok", "priorite": 5}, {"titre": "Ok", "priorite": 0}],
)
def test_validation(donnees):
assert client.post("/taches", json=donnees).status_code == 422
Corrigé de l'exercice 4
"""Exercice 4 : répondre à des questions avec pandas."""
import pandas as pd
ventes = pd.read_csv("ventes.csv", parse_dates=["date"])
ventes["chiffre_affaires"] = ventes["quantite"] * ventes["prix_unitaire"]
# 1. Le produit le plus vendu en quantité
quantites = ventes.groupby("produit")["quantite"].sum()
print(f"1. Produit le plus vendu : {quantites.idxmax()} ({quantites.max()} unités)")
# 2. Chiffre d'affaires de chaque ville
print("2. Chiffre d'affaires par ville :")
print(ventes.groupby("ville")["chiffre_affaires"].sum().round(2).to_string())
# 3. La vente (ligne) qui a rapporté le plus
meilleure = ventes.loc[ventes["chiffre_affaires"].idxmax()]
print(
f"3. Meilleure vente : {meilleure['produit']} le {meilleure['date']:%d/%m/%Y} "
f"à {meilleure['ville']} ({meilleure['chiffre_affaires']:.2f} €)"
)
# 4. Les ventes de viennoiseries à Lyon de plus de 100 unités
filtre = (
(ventes["categorie"] == "Viennoiserie")
& (ventes["ville"] == "Lyon")
& (ventes["quantite"] > 100)
)
print("4. Viennoiseries à Lyon, plus de 100 unités :")
print(ventes.loc[filtre, ["date", "produit", "quantite"]].to_string(index=False))
# 5. Prix moyen pondéré par catégorie (CA total / quantité totale)
resume = ventes.groupby("categorie")[["chiffre_affaires", "quantite"]].sum()
resume["prix_moyen"] = (resume["chiffre_affaires"] / resume["quantite"]).round(2)
print("5. Prix moyen par catégorie :")
print(resume.sort_values("prix_moyen", ascending=False).to_string())
# 6. Exporter un résumé par produit
resume_produits = ventes.groupby("produit").agg(
quantite_totale=("quantite", "sum"),
chiffre_affaires=("chiffre_affaires", "sum"),
nombre_ventes=("date", "count"),
)
resume_produits.sort_values("chiffre_affaires", ascending=False).to_csv(
"resume_produits.csv"
)
print("6. Résumé exporté dans resume_produits.csv")
1. Produit le plus vendu : Baguette (1270 unités)
2. Chiffre d'affaires par ville :
ville
Lyon 1680.0
Paris 1336.0
3. Meilleure vente : Baguette le 13/04/2026 à Lyon (360.00 €)
4. Viennoiseries à Lyon, plus de 100 unités :
date produit quantite
2026-01-05 Croissant 120
2026-02-23 Pain au chocolat 110
2026-03-16 Croissant 150
5. Prix moyen par catégorie :
chiffre_affaires quantite prix_moyen
categorie
Pâtisserie 458.5 150 3.06
Pain 1671.0 1340 1.25
Viennoiserie 886.5 745 1.19
6. Résumé exporté dans resume_produits.csv
Corrigé de l'exercice 5
"""Exercice 5 : un graphique soigné du chiffre d'affaires par produit."""
import matplotlib.pyplot as plt
import pandas as pd
ventes = pd.read_csv("ventes.csv", parse_dates=["date"])
ventes["chiffre_affaires"] = ventes["quantite"] * ventes["prix_unitaire"]
par_produit = ventes.groupby("produit")["chiffre_affaires"].sum().sort_values()
figure, axe = plt.subplots(figsize=(8, 4.5))
barres = axe.barh(par_produit.index, par_produit.values, color="#4C72B0")
axe.bar_label(barres, labels=[f"{v:.0f} €" for v in par_produit.values], padding=4)
axe.set_title("Chiffre d'affaires par produit (janvier à avril 2026)")
axe.set_xlabel("Euros")
axe.spines[["top", "right"]].set_visible(False) # allège le cadre
axe.set_xlim(0, par_produit.max() * 1.15) # laisse de la place aux étiquettes
figure.tight_layout()
figure.savefig("ca_par_produit.png", dpi=100)
print("Graphique enregistré dans ca_par_produit.png")
Corrigé de l'exercice 6
"""Exercice 6 : classer des vins, comparer des modèles et choisir k."""
from sklearn.datasets import load_wine
from sklearn.model_selection import cross_val_score, train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
vins = load_wine()
X, y = vins.data, vins.target
print(f"{X.shape[0]} vins, {X.shape[1]} mesures, {len(vins.target_names)} classes")
X_entrainement, X_test, y_entrainement, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y
)
# 1. Sans mise à l'échelle
brut = KNeighborsClassifier(n_neighbors=5)
brut.fit(X_entrainement, y_entrainement)
print(f"k-NN sans mise à l'échelle : {brut.score(X_test, y_test):.1%}")
# 2. Avec mise à l'échelle : toutes les mesures ramenées à la même échelle
normalise = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=5))
normalise.fit(X_entrainement, y_entrainement)
print(f"k-NN avec StandardScaler : {normalise.score(X_test, y_test):.1%}")
# 3. Choisir k par validation croisée (sur les données d'entraînement seulement)
print("\nValidation croisée (5 plis) :")
meilleur_k, meilleur_score = None, 0.0
for k in range(1, 16, 2):
modele = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=k))
scores = cross_val_score(modele, X_entrainement, y_entrainement, cv=5)
print(f" k = {k:2d} : {scores.mean():.1%} (± {scores.std():.1%})")
if scores.mean() > meilleur_score:
meilleur_k, meilleur_score = k, scores.mean()
final = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=meilleur_k))
final.fit(X_entrainement, y_entrainement)
precision_finale = final.score(X_test, y_test)
print(f"\nMeilleur k = {meilleur_k} → précision sur le test : {precision_finale:.1%}")
178 vins, 13 mesures, 3 classes
k-NN sans mise à l'échelle : 77.8%
k-NN avec StandardScaler : 93.3%
Validation croisée (5 plis) :
k = 1 : 95.4% (± 4.4%)
k = 3 : 96.2% (± 2.4%)
k = 5 : 95.5% (± 3.6%)
k = 7 : 96.2% (± 2.3%)
k = 9 : 96.2% (± 2.3%)
k = 11 : 97.0% (± 1.5%)
k = 13 : 97.0% (± 1.5%)
k = 15 : 97.7% (± 1.9%)
Meilleur k = 15 → précision sur le test : 100.0%
Corrigé de l'exercice 7
"""Renomme en lot les fichiers d'un dossier : prefixe_001.ext, prefixe_002.ext…
Usage : python renommer_lot.py dossier prefixe [--extension .jpg] [--executer]
"""
import argparse
from pathlib import Path
def planifier(
dossier: Path, prefixe: str, extension: str | None
) -> list[tuple[Path, Path]]:
"""Renvoie la liste des renommages (ancien, nouveau) sans rien modifier."""
fichiers = sorted(
f
for f in dossier.iterdir()
if f.is_file()
and not f.name.startswith(".")
and (extension is None or f.suffix.lower() == extension.lower())
)
largeur = max(3, len(str(len(fichiers)))) # 001, 002… ou 0001 si > 999 fichiers
plan = []
for numero, fichier in enumerate(fichiers, start=1):
nouveau_nom = f"{prefixe}_{numero:0{largeur}d}{fichier.suffix.lower()}"
plan.append((fichier, fichier.with_name(nouveau_nom)))
return plan
def main() -> None:
analyseur = argparse.ArgumentParser(description="Renommage en lot.")
analyseur.add_argument("dossier", type=Path)
analyseur.add_argument("prefixe")
analyseur.add_argument("--extension", help="ne traiter que cette extension")
analyseur.add_argument("--executer", action="store_true")
args = analyseur.parse_args()
plan = planifier(args.dossier, args.prefixe, args.extension)
nouveaux = {nouveau for _, nouveau in plan}
anciens = {ancien for ancien, _ in plan}
conflits = [n for n in nouveaux if n.exists() and n not in anciens]
if conflits:
analyseur.error(f"ces fichiers existent déjà : {[c.name for c in conflits]}")
etiquette = "✔" if args.executer else "[simulation]"
for ancien, nouveau in plan:
print(f"{etiquette} {ancien.name} → {nouveau.name}")
if args.executer:
# Deux passes : d'abord des noms temporaires, pour éviter d'écraser
# un fichier qui porterait déjà un des nouveaux noms (ex. photo_002 ↔ photo_001)
temporaires = []
for ancien, nouveau in plan:
temporaire = ancien.with_name(f".tmp_{nouveau.name}")
ancien.rename(temporaire)
temporaires.append((temporaire, nouveau))
for temporaire, nouveau in temporaires:
temporaire.rename(nouveau)
print(f"{len(plan)} fichier(s) concerné(s).")
if __name__ == "__main__":
main()
Corrigé de l'exercice 8
"""Attrape-étoiles, version 2 : niveaux, étoiles piégées, rejouer, meilleur score."""
import random
from pathlib import Path
import pygame
LARGEUR, HAUTEUR = 640, 480
IPS = 60
BLANC = (255, 255, 255)
NUIT = (15, 20, 45)
JAUNE = (255, 215, 0)
ROUGE = (230, 60, 60)
ORANGE = (240, 130, 40)
FICHIER_RECORD = Path("record.txt")
def lire_record() -> int:
try:
return int(FICHIER_RECORD.read_text(encoding="utf-8"))
except (FileNotFoundError, ValueError):
return 0
def ecrire_record(score: int) -> None:
FICHIER_RECORD.write_text(str(score), encoding="utf-8")
class Panier:
def __init__(self) -> None:
self.rect = pygame.Rect(0, 0, 100, 20)
self.rect.midbottom = (LARGEUR // 2, HAUTEUR - 20)
self.vitesse = 7
def deplacer(self, touches) -> None:
if touches[pygame.K_LEFT]:
self.rect.x -= self.vitesse
if touches[pygame.K_RIGHT]:
self.rect.x += self.vitesse
self.rect.clamp_ip(pygame.Rect(0, 0, LARGEUR, HAUTEUR))
def dessiner(self, ecran) -> None:
pygame.draw.rect(ecran, ORANGE, self.rect, border_radius=6)
class Etoile:
def __init__(self, niveau: int) -> None:
self.rect = pygame.Rect(random.randint(0, LARGEUR - 20), -20, 20, 20)
# plus le niveau est élevé, plus les étoiles tombent vite
self.vitesse = random.uniform(2, 4) + 0.5 * niveau
self.y = float(self.rect.y)
self.piegee = random.random() < 0.2 # 1 étoile sur 5 est rouge
def tomber(self) -> None:
self.y += self.vitesse
self.rect.y = int(self.y)
def dessiner(self, ecran) -> None:
couleur = ROUGE if self.piegee else JAUNE
pygame.draw.circle(ecran, couleur, self.rect.center, 10)
class Jeu:
def __init__(self) -> None:
self.record = lire_record()
self.recommencer()
def recommencer(self) -> None:
self.panier = Panier()
self.etoiles: list[Etoile] = []
self.score = 0
self.vies = 3
@property
def niveau(self) -> int:
return 1 + self.score // 10 # un niveau de plus toutes les 10 étoiles
@property
def termine(self) -> bool:
return self.vies <= 0
def perdre_une_vie(self) -> None:
self.vies -= 1
if self.termine and self.score > self.record:
self.record = self.score
ecrire_record(self.record)
def mettre_a_jour(self, touches) -> None:
if self.termine:
return
self.panier.deplacer(touches)
if random.random() < 0.02 + 0.005 * self.niveau:
self.etoiles.append(Etoile(self.niveau))
for etoile in self.etoiles[:]:
etoile.tomber()
if etoile.rect.colliderect(self.panier.rect):
self.etoiles.remove(etoile)
if etoile.piegee:
self.perdre_une_vie()
else:
self.score += 1
elif etoile.rect.top > HAUTEUR:
self.etoiles.remove(etoile)
if not etoile.piegee: # rater une étoile rouge, c'est bien !
self.perdre_une_vie()
def dessiner(self, ecran, police) -> None:
ecran.fill(NUIT)
for etoile in self.etoiles:
etoile.dessiner(ecran)
self.panier.dessiner(ecran)
infos = (
f"Score : {self.score} Vies : {self.vies} "
f"Niveau : {self.niveau} Record : {self.record}"
)
ecran.blit(police.render(infos, True, BLANC), (10, 10))
if self.termine:
fin = police.render(
"Perdu ! R pour rejouer, Échap pour quitter", True, BLANC
)
ecran.blit(fin, fin.get_rect(center=(LARGEUR // 2, HAUTEUR // 2)))
def main() -> None:
pygame.init()
ecran = pygame.display.set_mode((LARGEUR, HAUTEUR))
pygame.display.set_caption("Attrape-étoiles v2")
horloge = pygame.time.Clock()
police = pygame.font.Font(None, 30)
jeu = Jeu()
en_cours = True
while en_cours:
for evenement in pygame.event.get():
if evenement.type == pygame.QUIT:
en_cours = False
elif evenement.type == pygame.KEYDOWN:
if evenement.key == pygame.K_ESCAPE:
en_cours = False
elif evenement.key == pygame.K_r and jeu.termine:
jeu.recommencer()
jeu.mettre_a_jour(pygame.key.get_pressed())
jeu.dessiner(ecran, police)
pygame.display.flip()
horloge.tick(IPS)
pygame.quit()
if __name__ == "__main__":
main()
Corrigé du mini-projet
Structure du projet :station-meteo/
├── .gitignore
├── requirements.txt
├── generer_donnees.py
├── analyse.py
├── app.py
├── test_meteo.py
├── releves.csv (généré)
└── static/
└── mensuel.png (généré)
"""Étape 1 (automatisation) : génère un fichier CSV de relevés météo fictifs."""
import csv
import math
import random
from datetime import date, timedelta
from pathlib import Path
VILLES = {"Brest": 12.0, "Lyon": 13.0, "Marseille": 16.0} # température moyenne
def generer(chemin: Path, debut: date, nombre_jours: int, graine: int = 1) -> None:
hasard = random.Random(graine) # graine fixe : mêmes données à chaque fois
with open(chemin, "w", newline="", encoding="utf-8") as fichier:
ecrivain = csv.writer(fichier)
ecrivain.writerow(["date", "ville", "temp_min", "temp_max", "pluie_mm"])
for i in range(nombre_jours):
jour = debut + timedelta(days=i)
# une sinusoïde imite les saisons : froid en janvier, chaud en juillet
saison = -8 * math.cos(2 * math.pi * (jour.timetuple().tm_yday - 15) / 365)
for ville, moyenne in VILLES.items():
centre = moyenne + saison + hasard.gauss(0, 2)
ecart = hasard.uniform(4, 10) # écart entre le min et le max du jour
temp_min = round(centre - ecart / 2, 1)
temp_max = round(centre + ecart / 2, 1)
il_pleut = hasard.random() < 0.35 # environ 1 jour sur 3
pluie = round(hasard.expovariate(0.4), 1) if il_pleut else 0
ecrivain.writerow([jour.isoformat(), ville, temp_min, temp_max, pluie])
if __name__ == "__main__":
generer(Path("releves.csv"), date(2025, 1, 1), 365)
print("releves.csv créé.")
"""Étape 2 (data) : analyse des relevés avec pandas et graphique matplotlib."""
from pathlib import Path
import matplotlib
matplotlib.use("Agg") # dessine dans un fichier, sans ouvrir de fenêtre
import matplotlib.pyplot as plt # noqa: E402
import pandas as pd # noqa: E402
def charger(chemin: Path) -> pd.DataFrame:
releves = pd.read_csv(chemin, parse_dates=["date"])
releves["temp_moyenne"] = (releves["temp_min"] + releves["temp_max"]) / 2
return releves
def resume_par_ville(releves: pd.DataFrame) -> pd.DataFrame:
return (
releves.groupby("ville")
.agg(
temp_moyenne=("temp_moyenne", "mean"),
record_chaleur=("temp_max", "max"),
record_froid=("temp_min", "min"),
jours_de_pluie=("pluie_mm", lambda pluie: int((pluie > 0).sum())),
cumul_pluie_mm=("pluie_mm", "sum"),
)
.round(1)
)
def tracer_mensuel(releves: pd.DataFrame, chemin_image: Path) -> None:
mensuel = releves.pivot_table(
index=releves["date"].dt.month,
columns="ville",
values="temp_moyenne",
aggfunc="mean",
)
figure, axe = plt.subplots(figsize=(8, 4))
mensuel.plot(ax=axe, marker="o")
axe.set_title("Température moyenne par mois")
axe.set_xlabel("Mois")
axe.set_ylabel("°C")
axe.set_xticks(range(1, 13))
axe.grid(alpha=0.3)
figure.tight_layout()
chemin_image.parent.mkdir(exist_ok=True)
figure.savefig(chemin_image, dpi=100)
plt.close(figure)
"""Étape 3 (web) : un tableau de bord Flask qui affiche l'analyse."""
from pathlib import Path
from flask import Flask, jsonify, render_template_string
from analyse import charger, resume_par_ville, tracer_mensuel
DOSSIER = Path(__file__).parent
FICHIER_CSV = DOSSIER / "releves.csv"
IMAGE = DOSSIER / "static" / "mensuel.png"
app = Flask(__name__)
PAGE = """
<!doctype html>
<html lang="fr">
<head><meta charset="utf-8"><title>Ma station météo</title></head>
<body style="font-family: sans-serif; max-width: 50rem; margin: 2rem auto;">
<h1>🌦️ Ma station météo</h1>
<p>{{ nombre }} relevés analysés.</p>
{{ tableau|safe }}
<img src="/static/mensuel.png" alt="Température moyenne par mois"
style="max-width: 100%;">
<p><a href="/api/resume">Données au format JSON</a></p>
</body>
</html>
"""
@app.route("/")
def tableau_de_bord():
releves = charger(FICHIER_CSV)
tracer_mensuel(releves, IMAGE)
tableau = resume_par_ville(releves).to_html(border=0)
return render_template_string(PAGE, nombre=len(releves), tableau=tableau)
@app.route("/api/resume")
def api_resume():
resume = resume_par_ville(charger(FICHIER_CSV))
return jsonify(resume.to_dict(orient="index"))
if __name__ == "__main__":
app.run(debug=True)
from datetime import date
import pytest
from analyse import charger, resume_par_ville, tracer_mensuel
from generer_donnees import generer
@pytest.fixture
def fichier_csv(tmp_path):
chemin = tmp_path / "releves.csv"
generer(chemin, date(2025, 1, 1), 60)
return chemin
def test_generer_nombre_de_lignes(fichier_csv):
lignes = fichier_csv.read_text(encoding="utf-8").splitlines()
assert len(lignes) == 1 + 60 * 3 # en-tête + 60 jours × 3 villes
def test_min_inferieur_au_max(fichier_csv):
releves = charger(fichier_csv)
assert (releves["temp_min"] <= releves["temp_max"]).all()
def test_resume(fichier_csv):
resume = resume_par_ville(charger(fichier_csv))
assert sorted(resume.index) == ["Brest", "Lyon", "Marseille"]
assert (resume["jours_de_pluie"] <= 60).all()
assert (resume["record_froid"] <= resume["record_chaleur"]).all()
def test_graphique_cree(fichier_csv, tmp_path):
image = tmp_path / "images" / "mensuel.png"
tracer_mensuel(charger(fichier_csv), image)
assert image.exists() and image.stat().st_size > 0
def test_application_web(fichier_csv, monkeypatch):
import app as module_app
monkeypatch.setattr(module_app, "FICHIER_CSV", fichier_csv)
client = module_app.app.test_client()
assert "Ma station météo" in client.get("/").get_data(as_text=True)
assert set(client.get("/api/resume").get_json()) == {"Brest", "Lyon", "Marseille"}
python generer_donnees.py # crée releves.csv (365 jours × 3 villes)
pytest # 5 passed
python app.py # puis ouvre http://127.0.0.1:5000
Récapitulatif¶
Points clés à retenir :
- Automatisation :
pathlib+shutil+argparsetransforment une corvée en commande. Toujours un mode simulation, tester sur une copie, ne jamais écraser. - Web : un serveur associe des routes à des fonctions. Flask est simple et parfait pour débuter ; FastAPI valide les données grâce aux annotations de type et génère la documentation
/docs. Les deux se testent avec un client de test. - Data : pandas manipule des tableaux (
read_csv, filtres,groupby,pivot_table) sans boucles ; matplotlib dessine les graphiques (plt.subplots,savefig,plt.show). - IA / ML : scikit-learn offre une interface unique
fit/predict/score. On sépare toujours les données (train_test_split) et on évalue sur des exemples jamais vus ; la validation croisée rend l'évaluation fiable. - Jeux : tout jeu repose sur la boucle de jeu — événements, mise à jour, affichage — à 60 images par seconde. pygame fournit
Rect, les collisions, le clavier et le dessin. - Chaque domaine a ses bibliothèques, mais les bases de Python sont les mêmes partout : fonctions, classes, fichiers, tests, venv et Git te servent dans tous les cas.
- Choisis un domaine qui te motive et approfondis-le par des projets concrets.
Checklist :
- J'ai créé un venv dédié pour chaque domaine testé.
- J'ai lancé le script de tri de fichiers en mode simulation, puis pour de vrai sur un dossier de test.
- J'ai lancé l'application Flask et ouvert la page dans mon navigateur.
- J'ai exploré la documentation automatique
/docsde l'API FastAPI. - J'ai analysé
ventes.csvavec pandas et obtenu un graphique matplotlib. - J'ai entraîné et évalué un modèle scikit-learn sur le jeu iris.
- J'ai joué à Attrape-étoiles et je comprends la boucle de jeu.
- J'ai consulté le tableau comparatif et choisi le domaine que je veux approfondir.
- J'ai noté une idée de projet pour le chapitre suivant.