pandas, matplotlib, math et io.Sommaire
0. Mise en situation 1. Création du jeu de données 2. Charger et explorer les données avec pandas 3. Visualiser les données 4. Calculer une distance 5. Premier classificateur k-NN 6. Programmer une fonction knn 7. Comprendre le rôle de k 8. Évaluer l’algorithme sur des données de test 9. Aller plus loin : utiliser trois caractéristiques 10. Point important : les échelles de valeurs 11. Bilan 12. Mini-projet final, 20 à 30 minutes Proposition de découpage des 2 hAccès au corrigé professeur
Entrez le code fourni par l’enseignant pour ouvrir la page corrigée.
Première NSI — Découvrir et comprendre l’algorithme k-NN avec pandas
Durée conseillée : 2 h Pré-requis : tableaux de données avec pandas, boucles, fonctions, listes, dictionnaires, coordonnées dans le plan. Objectif : comprendre le principe d’un algorithme de classification supervisée : les k plus proches voisins, ou k-NN.
À la fin de l’activité, vous saurez :
- charger et explorer un jeu de données avec
pandas; - représenter des données en deux dimensions ;
- calculer une distance entre deux objets ;
- programmer une version simple de k-NN ;
- tester l’influence du paramètre
k; - mesurer la qualité d’un classificateur sur des données de test.
Le notebook est autonome : il crée son propre fichier CSV.
0. Mise en situation
On dispose de mesures faites sur des fruits :
- leur masse en grammes ;
- leur diamètre en millimètres ;
- leur couleur moyenne codée par un nombre entre 0 et 10 ;
- leur type :
pomme,orangeoubanane.
On veut entraîner un programme à prédire le type d’un fruit inconnu à partir de ses mesures.
La méthode k-NN repose sur une idée simple :
Pour classer un nouvel objet, on regarde les k objets connus qui lui ressemblent le plus. La classe majoritaire parmi ces voisins devient la prédiction.
# Cellule à exécuter au début du notebook
import pandas as pd
import matplotlib.pyplot as plt
from math import sqrt
from collections import Counter
# Pour que les graphiques s'affichent dans le notebook
%matplotlib inline1. Création du jeu de données
Exécutez la cellule suivante. Elle crée un fichier fruits_knn.csv directement dans Basthon.
csv = """masse_g,diametre_mm,couleur,type
145,72,3.1,pomme
152,74,3.4,pomme
160,76,3.2,pomme
138,70,2.8,pomme
170,79,3.6,pomme
155,75,3.5,pomme
149,73,3.0,pomme
165,78,3.7,pomme
142,71,2.9,pomme
158,76,3.3,pomme
180,80,7.5,orange
190,84,7.8,orange
175,78,7.2,orange
200,86,8.0,orange
185,82,7.6,orange
195,85,8.1,orange
178,79,7.1,orange
188,83,7.9,orange
205,88,8.3,orange
182,81,7.4,orange
115,38,9.0,banane
120,40,9.2,banane
110,36,8.8,banane
130,42,9.5,banane
125,41,9.3,banane
118,39,9.1,banane
108,35,8.7,banane
132,43,9.6,banane
122,40,9.4,banane
112,37,8.9,banane
151,74,3.2,pomme
186,82,7.7,orange
117,39,9.0,banane
168,78,3.8,pomme
198,86,8.2,orange
128,42,9.5,banane
"""
with open("fruits_knn.csv", "w", encoding="utf-8") as f:
f.write(csv)
print("Fichier fruits_knn.csv créé.")2. Charger et explorer les données avec pandas
Question 1
Chargez le fichier CSV dans une variable df, puis affichez les cinq premières lignes.
# À compléter<details> <summary>Aide</summary>
Utilisez pd.read_csv("fruits_knn.csv"), puis df.head(). </details>
Question 2
Affichez :
- le nombre de lignes et de colonnes ;
- les noms des colonnes ;
- le nombre de fruits de chaque type.
# À compléter<details> <summary>Correction possible</summary>
df.shape
df.columns
df["type"].value_counts()</details>
Question 3
Affichez les statistiques descriptives du tableau.
Puis répondez à l’oral :
- quelle colonne contient les classes à prédire ?
- quelles colonnes peuvent servir à faire une prédiction ?
# À compléter3. Visualiser les données
Pour commencer, on utilise seulement deux caractéristiques :
- la masse ;
- le diamètre.
Question 4
Exécutez la cellule suivante. Que remarquez-vous ? Les trois types de fruits semblent-ils faciles à séparer ?
for fruit in df["type"].unique():
sous_table = df[df["type"] == fruit]
plt.scatter(sous_table["masse_g"], sous_table["diametre_mm"], label=fruit)
plt.xlabel("Masse en grammes")
plt.ylabel("Diamètre en millimètres")
plt.title("Représentation des fruits selon deux mesures")
plt.legend()
plt.grid()
plt.show()Question 5
On observe maintenant un fruit inconnu :
fruit_inconnu = {"masse_g": 150, "diametre_mm": 73}Ajoutez ce fruit sur le graphique précédent avec un marqueur différent.
fruit_inconnu = {"masse_g": 150, "diametre_mm": 73}
# À compléter : refaire le graphique et ajouter le fruit inconnu<details> <summary>Indice</summary>
Pour le fruit inconnu, vous pouvez utiliser :
plt.scatter(fruit_inconnu["masse_g"], fruit_inconnu["diametre_mm"], marker="x", s=150, label="inconnu")</details>
4. Calculer une distance
Pour déterminer les fruits les plus proches, il faut mesurer une distance.
On utilise ici la distance euclidienne dans le plan :
\[ distance(A,B)=\sqrt{(x_A-x_B)^2+(y_A-y_B)^2} \]
Dans notre cas :
xcorrespond à la masse ;ycorrespond au diamètre.
Question 6
Complétez la fonction distance_2d.
def distance_2d(fruit1, fruit2):
"""Renvoie la distance entre deux fruits décrits par masse_g et diametre_mm."""
# À compléter
# Test attendu : la distance entre un point et lui-même vaut 0
print(distance_2d({"masse_g": 150, "diametre_mm": 73}, {"masse_g": 150, "diametre_mm": 73}))<details> <summary>Correction possible</summary>
def distance_2d(fruit1, fruit2):
dm = fruit1["masse_g"] - fruit2["masse_g"]
dd = fruit1["diametre_mm"] - fruit2["diametre_mm"]
return sqrt(dm**2 + dd**2)</details>
Question 7
Calculez la distance entre le fruit inconnu et chaque fruit du tableau.
Ajoutez une colonne distance à df.
# À compléter<details> <summary>Aide</summary>
Une ligne de DataFrame peut être utilisée comme un dictionnaire :
df["distance"] = df.apply(lambda ligne: distance_2d(fruit_inconnu, ligne), axis=1)</details>
Question 8
Triez le tableau par distance croissante et affichez les 5 fruits les plus proches du fruit inconnu.
# À compléter5. Premier classificateur k-NN
On choisit un entier k. On regarde les k lignes les plus proches. La classe la plus fréquente devient la prédiction.
Question 9
Avec k = 3, quelle classe prédisez-vous pour le fruit inconnu ?
k = 3
# À compléter<details> <summary>Indice</summary>
Vous pouvez utiliser :
voisins = df.sort_values("distance").head(k)
voisins["type"].value_counts()</details>
6. Programmer une fonction knn
Question 10
Complétez la fonction suivante. Elle doit renvoyer la classe prédite pour un fruit inconnu.
def knn_2d(table, fruit, k):
"""
table : DataFrame contenant masse_g, diametre_mm et type
fruit : dictionnaire contenant masse_g et diametre_mm
k : nombre de voisins utilisés
renvoie : classe prédite
"""
table = table.copy()
# 1. Calculer les distances
# À compléter
# 2. Garder les k plus proches voisins
# À compléter
# 3. Trouver la classe majoritaire
# À compléter
return None
print(knn_2d(df, fruit_inconnu, 3))<details> <summary>Correction possible</summary>
def knn_2d(table, fruit, k):
table = table.copy()
table["distance"] = table.apply(lambda ligne: distance_2d(fruit, ligne), axis=1)
voisins = table.sort_values("distance").head(k)
prediction = voisins["type"].value_counts().idxmax()
return prediction</details>
Question 11
Testez la fonction avec les fruits inconnus suivants et plusieurs valeurs de k : 1, 3, 5, 7.
Que remarquez-vous ?
fruits_inconnus = [
{"masse_g": 150, "diametre_mm": 73},
{"masse_g": 190, "diametre_mm": 84},
{"masse_g": 119, "diametre_mm": 39},
{"masse_g": 165, "diametre_mm": 82}, # cas possiblement ambigu
]
# À compléter7. Comprendre le rôle de k
Question 12
Pourquoi k = 1 peut-il être risqué ? Pourquoi choisir un k trop grand peut-il aussi poser problème ?
Répondez en quelques lignes dans la cellule Markdown ci-dessous.
Votre réponse :
8. Évaluer l’algorithme sur des données de test
Un modèle n’est pas seulement évalué sur les données qu’il connaît déjà.
On sépare donc le tableau en deux parties :
- un ensemble d’apprentissage : données connues par l’algorithme ;
- un ensemble de test : données gardées à part pour vérifier les prédictions.
Exécutez la cellule suivante.
# On mélange les lignes avec random_state pour obtenir toujours le même résultat
melange = df.sample(frac=1, random_state=42).reset_index(drop=True)
# 75 % apprentissage, 25 % test
limite = int(0.75 * len(melange))
train = melange.iloc[:limite].copy()
test = melange.iloc[limite:].copy()
print("Taille apprentissage :", len(train))
print("Taille test :", len(test))Question 13
Pour chaque ligne de test, prédisez le type avec knn_2d(train, fruit, 3).
Ajoutez une colonne prediction à test, puis affichez les colonnes masse_g, diametre_mm, type et prediction.
# À compléter<details> <summary>Aide</summary>
On peut écrire une fonction qui prend une ligne du tableau :
def predire_ligne(ligne):
fruit = {"masse_g": ligne["masse_g"], "diametre_mm": ligne["diametre_mm"]}
return knn_2d(train, fruit, 3)
test["prediction"] = test.apply(predire_ligne, axis=1)</details>
Question 14
Calculez le pourcentage de bonnes prédictions.
Ce pourcentage s’appelle l’accuracy, ou exactitude.
# À compléter<details> <summary>Correction possible</summary>
bonnes_reponses = test["type"] == test["prediction"]
accuracy = bonnes_reponses.mean()
print("Accuracy :", accuracy)
print("Accuracy en % :", round(accuracy * 100, 1), "%")</details>
9. Aller plus loin : utiliser trois caractéristiques
Jusqu’ici, on n’a utilisé que :
- la masse ;
- le diamètre.
Mais notre tableau contient aussi la couleur moyenne. On peut donc calculer une distance en trois dimensions.
Question 15
Complétez la fonction distance_3d, puis programmez une fonction knn_3d.
def distance_3d(fruit1, fruit2):
"""Distance utilisant masse_g, diametre_mm et couleur."""
# À compléter
def knn_3d(table, fruit, k):
table = table.copy()
# À compléter
return None
fruit_inconnu_3d = {"masse_g": 165, "diametre_mm": 82, "couleur": 7.5}
print(knn_3d(df, fruit_inconnu_3d, 3))<details> <summary>Correction possible</summary>
def distance_3d(fruit1, fruit2):
dm = fruit1["masse_g"] - fruit2["masse_g"]
dd = fruit1["diametre_mm"] - fruit2["diametre_mm"]
dc = fruit1["couleur"] - fruit2["couleur"]
return sqrt(dm**2 + dd**2 + dc**2)
def knn_3d(table, fruit, k):
table = table.copy()
table["distance"] = table.apply(lambda ligne: distance_3d(fruit, ligne), axis=1)
voisins = table.sort_values("distance").head(k)
return voisins["type"].value_counts().idxmax()</details>
10. Point important : les échelles de valeurs
La masse varie environ de 100 à 200. Le diamètre varie environ de 35 à 90. La couleur varie seulement de 0 à 10.
Dans une distance, une colonne avec de grands nombres peut avoir trop d’influence.
Une solution fréquente consiste à normaliser les données : transformer chaque colonne numérique pour qu’elle soit comprise entre 0 et 1.
Question 16
Exécutez la cellule suivante, puis observez les nouvelles colonnes.
df_norm = df.copy()
for colonne in ["masse_g", "diametre_mm", "couleur"]:
mini = df_norm[colonne].min()
maxi = df_norm[colonne].max()
df_norm[colonne + "_norm"] = (df_norm[colonne] - mini) / (maxi - mini)
df_norm.head()Question 17
Complétez une distance normalisée qui utilise :
masse_g_norm;diametre_mm_norm;couleur_norm.
def distance_norm(fruit1, fruit2):
# À compléter
def knn_norm(table, fruit, k):
table = table.copy()
# À compléter
return None
# Exemple : on doit normaliser aussi le fruit inconnu
fruit = {"masse_g": 165, "diametre_mm": 82, "couleur": 7.5}
fruit_norm = fruit.copy()
for colonne in ["masse_g", "diametre_mm", "couleur"]:
mini = df[colonne].min()
maxi = df[colonne].max()
fruit_norm[colonne + "_norm"] = (fruit[colonne] - mini) / (maxi - mini)
print(fruit_norm)
print(knn_norm(df_norm, fruit_norm, 3))11. Bilan
Répondez aux questions suivantes.
- Que signifie le
kdans k-NN ? - Pourquoi doit-on parfois normaliser les données ?
- Pourquoi faut-il séparer les données en apprentissage et en test ?
- Donnez un avantage de k-NN.
- Donnez une limite de k-NN.
Votre bilan :
12. Mini-projet final, 20 à 30 minutes
Choisissez l’un des défis suivants.
Défi A — Tableau de comparaison
Créez un tableau qui compare l’accuracy obtenue pour k = 1, k = 3, k = 5 et k = 7.
Défi B — Visualisation des erreurs
Affichez les fruits de test sur un graphique :
- une forme ou une couleur pour le vrai type ;
- un marqueur particulier pour les erreurs.
Défi C — Interface simple
Écrivez une fonction classifier_fruit(masse, diametre, couleur, k) qui affiche une phrase du type :
Le fruit est probablement une orange.# Mini-projet : votre code ici---
Annexe professeur — éléments de correction rapide
Cette section peut être supprimée avant distribution aux élèves.
# Correction compacte des fonctions principales
def distance_2d(fruit1, fruit2):
dm = fruit1["masse_g"] - fruit2["masse_g"]
dd = fruit1["diametre_mm"] - fruit2["diametre_mm"]
return sqrt(dm**2 + dd**2)
def knn_2d(table, fruit, k):
table = table.copy()
table["distance"] = table.apply(lambda ligne: distance_2d(fruit, ligne), axis=1)
voisins = table.sort_values("distance").head(k)
return voisins["type"].value_counts().idxmax()
def distance_3d(fruit1, fruit2):
dm = fruit1["masse_g"] - fruit2["masse_g"]
dd = fruit1["diametre_mm"] - fruit2["diametre_mm"]
dc = fruit1["couleur"] - fruit2["couleur"]
return sqrt(dm**2 + dd**2 + dc**2)
def knn_3d(table, fruit, k):
table = table.copy()
table["distance"] = table.apply(lambda ligne: distance_3d(fruit, ligne), axis=1)
voisins = table.sort_values("distance").head(k)
return voisins["type"].value_counts().idxmax()
def distance_norm(fruit1, fruit2):
dm = fruit1["masse_g_norm"] - fruit2["masse_g_norm"]
dd = fruit1["diametre_mm_norm"] - fruit2["diametre_mm_norm"]
dc = fruit1["couleur_norm"] - fruit2["couleur_norm"]
return sqrt(dm**2 + dd**2 + dc**2)
def knn_norm(table, fruit, k):
table = table.copy()
table["distance"] = table.apply(lambda ligne: distance_norm(fruit, ligne), axis=1)
voisins = table.sort_values("distance").head(k)
return voisins["type"].value_counts().idxmax()Proposition de découpage des 2 h
- 0 à 15 min : rappel
pandas, chargement, exploration. - 15 à 35 min : visualisation et intuition de proximité.
- 35 à 60 min : distance et voisins les plus proches.
- 60 à 85 min : programmation de
knn_2d. - 85 à 105 min : ensemble de test, accuracy.
- 105 à 120 min : normalisation ou mini-projet selon l’avancement.