Première NSI — Découvrir k-NN avec pandas

Activité de 2 h au format page Web, avec codes copiables pour Thonny.
Utilisation : chaque bloc Python peut être copié puis collé directement dans Thonny. Les bibliothèques utilisées sont 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 h

Accè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 :

Le notebook est autonome : il crée son propre fichier CSV.

0. Mise en situation

On dispose de mesures faites sur des fruits :

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.
Code Python à tester dans Thonny
# 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 inline

1. Création du jeu de données

Exécutez la cellule suivante. Elle crée un fichier fruits_knn.csv directement dans Basthon.

Code Python à tester dans Thonny
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.

Code Python à tester dans Thonny
# À compléter

<details> <summary>Aide</summary>

Utilisez pd.read_csv("fruits_knn.csv"), puis df.head(). </details>

Question 2

Affichez :

  1. le nombre de lignes et de colonnes ;
  2. les noms des colonnes ;
  3. le nombre de fruits de chaque type.
Code Python à tester dans Thonny
# À compléter

<details> <summary>Correction possible</summary>

Code python
df.shape
df.columns
df["type"].value_counts()

</details>

Question 3

Affichez les statistiques descriptives du tableau.

Puis répondez à l’oral :

Code Python à tester dans Thonny
# À compléter

3. Visualiser les données

Pour commencer, on utilise seulement deux caractéristiques :

Question 4

Exécutez la cellule suivante. Que remarquez-vous ? Les trois types de fruits semblent-ils faciles à séparer ?

Code Python à tester dans Thonny
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 :

Code python
fruit_inconnu = {"masse_g": 150, "diametre_mm": 73}

Ajoutez ce fruit sur le graphique précédent avec un marqueur différent.

Code Python à tester dans Thonny
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 :

Code python
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 :

Question 6

Complétez la fonction distance_2d.

Code Python à tester dans Thonny
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>

Code python
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.

Code Python à tester dans Thonny
# À compléter

<details> <summary>Aide</summary>

Une ligne de DataFrame peut être utilisée comme un dictionnaire :

Code python
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.

Code Python à tester dans Thonny
# À compléter

5. 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 ?

Code Python à tester dans Thonny
k = 3

# À compléter

<details> <summary>Indice</summary>

Vous pouvez utiliser :

Code python
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.

Code Python à tester dans Thonny
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>

Code python
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 ?

Code Python à tester dans Thonny
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éter

7. 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 :

Exécutez la cellule suivante.

Code Python à tester dans Thonny
# 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.

Code Python à tester dans Thonny
# À compléter

<details> <summary>Aide</summary>

On peut écrire une fonction qui prend une ligne du tableau :

Code python
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.

Code Python à tester dans Thonny
# À compléter

<details> <summary>Correction possible</summary>

Code python
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 :

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.

Code Python à tester dans Thonny
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>

Code python
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.

Code Python à tester dans Thonny
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 :

Code Python à tester dans Thonny
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.

  1. Que signifie le k dans k-NN ?
  2. Pourquoi doit-on parfois normaliser les données ?
  3. Pourquoi faut-il séparer les données en apprentissage et en test ?
  4. Donnez un avantage de k-NN.
  5. 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 :

Défi C — Interface simple

Écrivez une fonction classifier_fruit(masse, diametre, couleur, k) qui affiche une phrase du type :

Code text
Le fruit est probablement une orange.
Code Python à tester dans Thonny
# Mini-projet : votre code ici

---

Annexe professeur — éléments de correction rapide

Cette section peut être supprimée avant distribution aux élèves.

Code Python à tester dans Thonny
# 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