SOLUTION COMMENTÉE
A N N E X E P É D A G O G I Q U E · F I C H I E R D ' A C C O M PA G N E M E N T
Explication détaillée
de la solution
Lecture ligne par ligne du code du mini-projet
TextBlob
Ce document accompagne le code de référence situé dans
solution/ . Il explique en profondeur la logique de chaque
fonction, le choix de chaque structure de données, et la manière
dont les modules collaborent pour produire un pipeline NLP
complet.
📖 utils_solution.py 📖 sentiment_solution.py 📖 main_solution.py
Document pour l'enseignant Niveau : 3ème année
et l'élève curieux Annexe à l'énoncé
Sommaire
1. Vue d'ensemble du projet 3
2. Module utils_solution.py 3
3. Module sentiment_solution.py 6
4. Module main_solution.py 9
5. Flux d'exécution complet 12
6. Concepts Python mis en œuvre 13
7. Comprendre l'analyse TextBlob 14
8. Bonnes pratiques appliquées 15
9. Pistes d'amélioration 16
1. Vue d'ensemble du projet
Le projet se décompose en trois modules qui collaborent entre eux. Cette architecture suit le
principe « Separation of Concerns » : chaque fichier a une responsabilité claire.
Schéma de dépendance
┌─────────────────────────────┐
│ main_solution.py │ ← point d'entrée (orchestrateur)
│ • executer_analyse() │
│ • afficher_tableau() │
│ • afficher_resume() │
└──────────┬──────────────────┘
import
▼
┌──────────────────────────┐ ┌────────────────────────────┐
│ sentiment_solution.py │ │ utils_solution.py │
│ • analyser_texte() │ │ • lire_avis() │
│ • analyser_liste() │◀───│ • classifier_sentiment() │
│ • compter_sentiments() │ │ • sauvegarder_rapport() │
└──────────┬───────────────┘ │ • construire_rapport() │
│ │ • pourcentage() │
▼ └────────────────────────────┘
TextBlob (lib externe)
🧠 Concept clé : la modularité
Un module Python = un fichier .py . Importer un module via from utils import lire_avis
permet de réutiliser une fonction sans la réécrire. Cela évite la duplication de code et facilite la
maintenance : si classifier_sentiment change, on la modifie une seule fois, et tous les
modules qui l'utilisent sont automatiquement mis à jour.
2. Module utils_solution.py
Ce module regroupe les fonctions pures (sans logique métier NLP) : lecture/écriture de fichiers,
formatage, comptage sécurisé. Il peut être réutilisé tel quel dans d'autres projets.
2.1 Imports & constantes
from datetime import datetime
SEUIL_POSITIF = 0.05
SEUIL_NEGATIF = -0.05
datetime est la seule dépendance : on l'utilise pour horodater le rapport.
Les seuils sont des constantes en MAJUSCULES (PEP 8) : on les centralise pour pouvoir les
modifier en un seul endroit.
2.2 Fonction lire_avis()
lire_avis(chemin_fichier: str) -> list[str]
📂 Lecture d'un fichier UTF-8, gestion d'erreur complète.
def lire_avis(chemin_fichier):
try:
with open(chemin_fichier, "r", encoding="utf-8") as f:
lignes = [Link]()
avis = [[Link]() for ligne in lignes if [Link]()]
return avis
except FileNotFoundError:
print(f" ❌
Fichier introuvable : {chemin_fichier}")
return []
except UnicodeDecodeError:
print(f" ❌ Encodage incorrect pour : {chemin_fichier}")
return []
except OSError as exc:
print(f" ❌ Erreur d'ouverture : {exc}")
return []
Pourquoi ce code ?
1 with open(...) as f est la bonne pratique : le fichier est automatiquement fermé à
la fin du bloc, même en cas d'exception. Plus besoin de [Link]() explicite.
2 encoding="utf-8" est indispensable pour les accents français ; sans cela, le système
utilise l'encodage par défaut (souvent CP1252 sous Windows) et déclenche des erreurs
sur les caractères accentués.
3 Liste en compréhension [[Link]() for ligne in lignes if
[Link]()] : en une seule ligne, on supprime les espaces et on ignore les lignes
vides. Plus concis qu'une boucle de 5 lignes.
4 Trois except séparés : on distingue les erreurs (fichier introuvable, encodage, OS
général) pour donner un message clair à l'utilisateur. Le programme ne plante pas ; il
renvoie une liste vide et l'appelant affichera un message.
2.3 Fonction classifier_sentiment()
classifier_sentiment(polarite: float) -> str
🎯 Le cœur algorithmique du projet — structure conditionnelle.
def classifier_sentiment(polarite):
if polarite > SEUIL_POSITIF:
return "positif"
elif polarite < SEUIL_NEGATIF:
return "negatif"
return "neutre"
Lecture ligne par ligne
1 Si la polarité est strictement supérieure à 0.05 , c'est positif .
2 Sinon, si elle est strictement inférieure à -0.05 , c'est negatif .
3 Sinon, dans l'intervalle [-0.05, +0.05] , c'est neutre .
🧠 Pourquoi des seuils à ±0.05 et pas 0.0 ?
TextBlob produit souvent des scores très proches de 0 (par ex. 0.02 ) qui ne sont pas
vraiment positifs. En exigeant un écart de 0.05 par rapport à zéro, on évite de classer
comme « positif » un avis à 0.02 . Ce seuil est réglable en haut du fichier.
2.4 Fonction sauvegarder_rapport()
sauvegarder_rapport(nom_fichier: str, contenu: str) -> None
💾 Écriture sécurisée d'un rapport UTF-8.
def sauvegarder_rapport(nom_fichier, contenu):
try:
with open(nom_fichier, "w", encoding="utf-8") as f:
[Link](contenu)
print(f" ✓ Rapport enregistré dans : {nom_fichier}")
except (OSError, PermissionError) as exc:
print(f" ❌ Impossible d'écrire le rapport : {exc}")
except Exception as exc:
print(f"❌ Erreur inattendue : {exc}")
Points clés
1 Mode "w" (write) : crée le fichier s'il n'existe pas, l'écrase sinon.
2 On capture deux exceptions : OSError (disque plein, fichier protégé) et
PermissionError (droits insuffisants).
3 Un except Exception en DERNIER rempart intercepte toute autre erreur. En
production, on le combinerait avec un logger, mais pour un projet scolaire print suffit.
2.5 Fonction construire_rapport()
construire_rapport(stats: dict, lignes_analyse: list[tuple]) -> str
🧱 Construit le texte du rapport, ligne par ligne.
La fonction collecte des chaînes de caractères dans une list puis les joint avec
"\n".join(...) . C'est plus lisible que des += successifs (qui recréent une chaîne à chaque
fois en mémoire).
parties = []
[Link]("=" * 78)
[Link](f" RAPPORT D'ANALYSE DE SENTIMENT - {horodatage}")
...
for numero, avis, polarite, categorie in lignes_analyse:
[Link](
f" #{numero:>3} polarité = {polarite:+.2f} ({categorie})"
)
[Link](f" « {avis} »")
[Link]("")
return "\n".join(parties)
Détails f-string
Format Effet Exemple
{var:>3} aligne à droite, 3 caractères minimum " 1" pour 1
{var:+.2f} flottant avec signe forcé, 2 décimales "+0.50" ou "-0.90"
2.6 Fonction pourcentage()
pourcentage(part, total) -> float
🛡️ Division sécurisée par zéro.
def pourcentage(part, total):
if not total: # équivaut à if total == 0
return 0.0
return round(part / total * 100, 1)
Sans la garde if not total , une division par zéro lèverait ZeroDivisionError . On préfère
retourner 0.0 dans ce cas : cela reste cohérent (0 avis → 0 %).
3. Module sentiment_solution.py
C'est le cœur métier du projet : on y manipule TextBlob pour transformer un texte brut en un objet
structuré (polarité, subjectivité, catégorie).
3.1 Imports & import défensif
from textblob import TextBlob
try:
from utils_solution import classifier_sentiment
except ImportError:
# Mode dégradé : redéfinir localement le classificateur
SEUIL_POSITIF = 0.05
SEUIL_NEGATIF = -0.05
def classifier_sentiment(polarite):
if polarite > SEUIL_POSITIF:
return "positif"
elif polarite < SEUIL_NEGATIF:
return "negatif"
return "neutre"
🛡️ Import défensif — pourquoi ?
Si quelqu'un lance directement python sentiment_solution.py (sans passer par main ),
Python ne trouve pas utils_solution dans le PYTHONPATH. Le try/except ImportError
permet de redéfinir localement le classificateur. C'est une graceful degradation : le module
reste fonctionnel seul.
3.2 Fonction analyser_texte()
analyser_texte(texte: str) -> dict
🤖 Enveloppe TextBlob dans une API simple et sûre.
def analyser_texte(texte):
try:
blob = TextBlob(texte)
sentiment = [Link]
polarite = [Link]
subjectivite = [Link]
categorie = classifier_sentiment(polarite)
return {
"polarite": polarite,
"subjectivite": subjectivite,
"categorie": categorie,
}
except Exception as exc:
print(f"⚠ Texte non analysé ({texte!r:.40}): {exc}")
return {"polarite": 0.0,
"subjectivite": 0.0,
"categorie": "neutre"}
Anatomie de la fonction
1 TextBlob(texte) instancie un objet blob qui encapsule le texte et tous ses traitements
(tokenisation, étiquetage, analyse de sentiment, traduction…).
2 [Link] retourne un namedtuple Sentiment(polarity, subjectivity) . On
accède aux deux champs par .polarity et .subjectivity .
3 Retour d'un dict plutôt que d'un tuple : cela rend l'API plus lisible côté appelant
( d["polarite"] vs t[0] ).
4 try/except Exception : filet de sécurité universel. Si TextBlob plante (texte bizarre,
mémoire, etc.), on NE FAIT PAS planter tout le programme : on renvoie un neutre et on
log l'erreur.
3.3 Fonction analyser_liste()
analyser_liste(liste_avis: list[str]) -> list[tuple]
🔁 Boucle + enumerate pour numéroter.
def analyser_liste(liste_avis):
resultats = []
for numero, avis in enumerate(liste_avis, start=1):
infos = analyser_texte(avis)
[Link](
(numero, avis, infos["polarite"], infos["categorie"])
)
return resultats
Décortiquons
1 enumerate(liste_avis, start=1) génère des paires (0, "avis1"), (1,
"avis2"), ... , mais comme on a précisé start=1 , on obtient (1, "avis1"), (2,
"avis2"), ... — pratique pour l'affichage côté humain.
2 Tuple à 4 champs : on garde une structure légère et ordonnée. Pour des données plus
complexes, on utiliserait un dataclass ou un dict .
3.4 Fonction compter_sentiments()
compter_sentiments(resultats: list[tuple]) -> dict
📊 Comptage par catégorie avec un dictionnaire.
def compter_sentiments(resultats):
compteurs = {"positif": 0, "neutre": 0, "negatif": 0}
for _, _, _, categorie in resultats:
if categorie in compteurs:
compteurs[categorie] += 1
compteurs["total"] = (compteurs["positif"]
+ compteurs["neutre"]
+ compteurs["negatif"])
return compteurs
Pourquoi _ ?
On déballe le tuple de 4 champs en _, _, _, categorie . La convention Python veut qu'on
utilise _ pour les variables qu'on n'utilisera pas : cela rend le code intentionnellement lisible.
4. Module main_solution.py
Le module principal a un rôle d'orchestrateur : il ne contient presque pas de logique, mais il fait
collaborer les fonctions des autres modules. C'est un schéma classique en génie logiciel.
4.1 Chemins relatifs via [Link]
DOSSIER_DONNEES = [Link]("..", "donnees")
FICHIER_AVIS = [Link](DOSSIER_DONNEES, "avis_clients.txt")
FICHIER_RAPPORT = [Link]("..", "[Link]")
[Link] construit le chemin de manière portable : il utilise \ sur Windows et / sur
Linux/macOS. Sans cela, on devrait gérer manuellement les séparateurs.
4.2 Affichage du tableau détaillé
afficher_tableau(resultats: list[tuple]) -> None
🖨️ Affichage console en colonnes alignées.
for numero, avis, polarite, categorie in resultats:
avis_tronque = avis[:48] + ("..." if len(avis) > 48 else "")
print(
f"{numero:>3} {avis_tronque:<48} {polarite:>+6.2f} {categorie:<10}"
)
Décryptage des f-strings
Format Sens Effet visuel
{numero:>3} entier, aligné à droite, 3 car. 42
{avis_tronque: chaîne, alignée à gauche, 48 car. Service catastrophique
<48} …
{polarite:>+6.2f} flottant, signe forcé, 6 car. total, 2 -0.90
décimales
{categorie:<10} chaîne, alignée à gauche, 10 car. negatif
4.3 Affichage du résumé statistique
afficher_resume(stats: dict) -> None
📈 Affichage formaté des totaux et pourcentages.
total = stats["total"]
nb_pos = stats["positif"]
pct_pos = pourcentage(nb_pos, total)
print(f" Avis positifs : {nb_pos:>4} ({pct_pos:>5.1f} %)")
Le format :>5.1f réserve 5 caractères (largeur fixe) pour que les colonnes s'alignent même si
on a 9.5 % ou 100.0 %.
4.4 Pipeline executer_analyse()
C'est la fonction principale qui orchestre tout :
def executer_analyse():
# 1) Lecture
avis = lire_avis(FICHIER_AVIS)
if not avis:
print("Aucun avis à analyser. Vérifiez le fichier d'entrée.")
return
# 2) Analyse
resultats = analyser_liste(avis)
# 3) Affichage du tableau
afficher_tableau(resultats)
# 4) Statistiques
stats = compter_sentiments(resultats)
afficher_resume(stats)
# 5) Sauvegarde
rapport = construire_rapport(stats, resultats)
sauvegarder_rapport(FICHIER_RAPPORT, rapport)
Pourquoi return après if not avis ?
Si le fichier est vide ou inexistant, on évite d'appeler analyser_liste([]) qui produirait des
statistiques vides. Le return arrête proprement la fonction — l'utilisateur voit un message clair et
c'est tout.
4.5 Bloc if __name__ == "__main__"
if __name__ == "__main__":
try:
executer_analyse()
except KeyboardInterrupt:
print("\nInterrompu par l'utilisateur (Ctrl+C).")
[Link](0)
except Exception as exc:
print(f"\n ❌ Erreur inattendue : {exc}")
[Link](1)
Le test if __name__ == "__main__" garantit que le bloc n'est exécuté que si on lance
directement main_solution.py . Si on importe ce module ( from main_solution import ... ), ce
bloc n'est pas exécuté. C'est une convention Python universelle.
5. Flux d'exécution complet
Quand l'utilisateur tape python main_solution.py dans son terminal, voici l'ordre exact des appels
:
5.1 Diagramme de séquence
utilisateur main_solution.py sentiment_solution.py utils_solution
─────────── ──────────────── ───────────────────── ──────────────
│ │ │ │
│ python [Link] │ │ │
├────────────────────────▶│ │ │
│ │ lire_avis(chemin) │ │
│ ├─────────────────────────────────────────────────────▶│
│ │◀───────── list[str] ───────────────────────────────┤
│ │ │ │
│ │ analyser_liste(avis) │ │
│ ├─────────────────────────▶│ │
│ │ │ analyser_texte(avis) │
│ │ │ ×N │
│ │ │ (TextBlob) │
│ │◀───── list[tuple] ──────┤ │
│ │ │ │
│ │ afficher_tableau │ │
│ │ afficher_resume │ │
│ │ │ │
│ │ compter_sentiments │ │
│ ├─────────────────────────▶│ │
│ │◀───── dict ─────────────┤ │
│ │ │ │
│ │ construire_rapport │ │
│ ├─────────────────────────────────────────────────────▶│
│ │◀───────── str ───────────────────────────────────────┤
│ │ │ │
│ │ sauvegarder_rapport │ │
│ ├─────────────────────────────────────────────────────▶│
│ │ │ │
│ "Analyse terminée" │ │ │
│◀────────────────────────┤ │ │
5.2 Schéma des données transformées
Étape Type en entrée Type en sortie
lire_avis str (chemin) list[str]
analyser_texte str dict {polarite, subjectivite, categorie}
analyser_liste list[str] list[tuple]
compter_sentiments list[tuple] dict {positif, neutre, negatif, total}
construire_rapport dict + list str
sauvegarder_rapport str (contenu) None (effet de bord : fichier créé)
6. Concepts Python mis en œuvre
Récapitulatif de toutes les notions du programme officiel que ce projet mobilise.
Notion Exemple dans le code
Variables & types polarite: float , avis: str
Listes & tuples liste_avis , (numero, avis, polarite, categorie)
Dictionnaires {"positif": 0, "neutre": 0, ...}
Fonctions def lire_avis(chemin): ...
Paramètres & retour def pourcentage(part, total) -> float
Docstrings PEP 257 chaque fonction est documentée
Modules & imports from utils import classifier_sentiment
Import défensif try: from utils_solution except ImportError:
Conditions if polarite > 0.05: ... elif ... else
Boucles for for numero, avis in enumerate(liste, 1):
Compréhensions de liste [[Link]() for l in lignes if [Link]()]
Lecture/écriture de fichiers open(chemin, "r", encoding="utf-8")
Gestionnaire de contexte with open(...) as f:
Gestion des exceptions try / except / else / finally
f-strings f"Total : {total:>4} avis"
Constantes (PEP 8) SEUIL_POSITIF = 0.05
Bloc __main__ if __name__ == "__main__":
[Link] code de retour propre
7. Comprendre l'analyse TextBlob
7.1 Comment TextBlob calcule-t-il la polarité ?
TextBlob utilise l'algorithme PatternAnalyzer par défaut. Pour chaque mot, il consulte un lexique de
scores : "good" = +0.7, "bad" = -0.5, "love" = +0.5, "hate" = -0.6, etc. Le score final d'une phrase est
la moyenne pondérée des scores des mots qui la composent, modulée par des négations ("not
good" → négatif).
Le lexique a été construit sur des avis Amazon en anglais, ce qui explique pourquoi les phrases
françaises sont sous-estimées : les mots français ne sont pas dans le lexique, donc TextBlob ne
trouve pas de signal et renvoie 0.0.
7.2 Pourquoi les avis anglais du fichier sont-ils mieux classés ?
TextBlob("This phone is absolutely amazing").sentiment
# Sentiment(polarity=0.80, subjectivity=0.90)
TextBlob("Terrible customer service, awful experience").sentiment
# Sentiment(polarity=-1.00, subjectivity=1.00)
Mots amazing, terrible, awful sont dans le lexique anglais. Pour le français, c'est plus compliqué :
TextBlob ne traduit pas, il ne tokenise pas non plus correctement les phrases françaises. D'où des
scores proches de 0.
7.3 Subjectivité vs polarité
Score Question à laquelle il répond
polarity « L'avis est-il positif ou négatif ? »
subjectivity « L'avis est-il subjectif (opinion) ou objectif (fait) ? »
"The package arrived on time" a une subjectivité ≈ 0 (c'est un fait) mais peut avoir une polarité 0. "I
love it" a une subjectivité ≈ 1 et une polarité positive.
⚠️ Limites pédagogiques à discuter en classe
1. Langue : TextBlob = anglais. Pour le français → CamemBERT, Flaubert, ou spacy avec
un modèle fr_core_news.
2. Sarcasme & ironie : TextBlob ne les détecte pas. "Super, encore une panne" sera noté
positif à cause de super.
3. Contexte : "pas mal" (positif en français oral) sera noté négatif à cause de pas.
4. Phrases longues : un même texte qui mélange du positif et du négatif sera moyenné vers
le neutre.
8. Bonnes pratiques appliquées
Ce projet respecte plusieurs règles de la PEP 8 (style guide officiel de Python) et du génie logiciel :
✅ Noms explicites ✅ Docstrings
Les fonctions et variables ont des noms longs et Chaque fonction publique est documentée :
parlants : analyser_texte , description, paramètres, retour, exemple.
compter_sentiments , polarite …
✅ with open ✅ Exceptions typées
Pas d'ouverture « orpheline » : on utilise On capture des exceptions précises
systématiquement le gestionnaire de contexte. ( FileNotFoundError , OSError ) plutôt qu'un
except nu.
✅ Modularité ✅ Constantes nommées
Trois fichiers distincts, un main qui orchestre, des Aucun magic number : les seuils sont centralisés
modules qui ne dépendent que du strict en haut de [Link] .
nécessaire.
9. Pistes d'amélioration
Pour les élèves qui veulent aller plus loin, voici des pistes concrètes de refactorisation :
9.1 Utiliser un dataclass au lieu d'un tuple
from dataclasses import dataclass
@dataclass
class AvisAnalyse:
numero: int
texte: str
polarite: float
subjectivite: float
categorie: str
Un dataclass rend le code plus lisible : [Link] au lieu de avis[2] . C'est une
amélioration simple mais puissante.
9.2 Passer à argparse
import argparse
parser = [Link](description="Analyse de sentiment")
parser.add_argument("--input", default="../donnees/avis_clients.txt")
parser.add_argument("--output", default="../[Link]")
parser.add_argument("--format", choices=["txt", "csv", "json"], default="txt")
args = parser.parse_args()
L'utilisateur peut alors lancer : python [Link] --input mes_avis.txt --format csv .
9.3 Ajouter un modèle français
# pip install transformers torch
from transformers import pipeline
sentiment_fr = pipeline(
"sentiment-analysis",
model="tblard/tf-allocine"" # modèle entraîné sur des critiques FR
)
print(sentiment_fr("Ce produit est absolument fantastique !"))
# [{'label': 'POSITIVE', 'score': 0.9987}]
9.4 Tests unitaires avec pytest
# test_sentiment.py
from sentiment_solution import analyser_texte
def test_avis_positif():
d = analyser_texte("I love this product !")
assert d["categorie"] == "positif"
def test_avis_negatif():
d = analyser_texte("I hate this.")
assert d["categorie"] == "negatif"
🎓 En résumé
Ce projet, bien qu'abordable, met en place les fondamentaux du génie logiciel : découpage
modulaire, gestion d'erreurs, bonnes pratiques Python, et première approche d'une bibliothèque
d'IA. Tous ces réflexes vous serviront dans tout projet de développement futur.
Explication détaillée de la solution — Annexe à l'énoncé © Équipe pédagogique — Lycée Tunisien