Variables numériques et catégorielles : types, unités et valeurs autorisées

Variables numériques et catégorielles : mesurer une quantité ou décrire une catégorie.

Les variables numériques et catégorielles décrivent deux sortes d’informations : une quantité que l’on mesure ou compte, et une catégorie à laquelle une observation appartient. Pour préparer des données, il faut aussi connaître leurs unités et leurs valeurs autorisées. Une colonne remplie de nombres ne suffit donc pas à garantir que l’on peut en calculer une moyenne. Nous allons reconnaître ces différences, vérifier quelques colonnes avec pandas et déclarer un ordre sans effacer les informations à contrôler.

Avant de commencer. Vous pouvez lire les explications sans exécuter Python. Pour reproduire les cellules, utilisez le guide Python, uv, VS Code et Jupyter et sélectionnez le noyau de votre environnement .venv. Les blocs ci-dessous présentent le code et ses sorties vérifiées ; les calculs s’exécutent dans le notebook téléchargé.

1. Variables numériques et catégorielles : reconnaître leur sens

Une variable numérique, ou quantitative, représente une quantité. Une variable discrète prend des valeurs séparées : Tutoring_Sessions compte des séances de tutorat, sans demi-séance dans ce dénombrement. Une variable continue représente une mesure qui peut prendre des valeurs intermédiaires : une durée de sommeil peut s’exprimer en heures et en fractions d’heure.

Une variable catégorielle, ou qualitative, décrit une catégorie, appelée aussi modalité. Elle est nominale lorsqu’aucun ordre ne s’impose : School_Type distingue Public et Private. Elle est ordinale lorsque ses catégories ont un ordre : Motivation_Level va de Low à Medium, puis à High. Cet ordre n’indique pas de distance mesurable entre les niveaux.

Deux familles de variables : numérique discrète ou continue, catégorielle nominale ou ordinale, avec les exemples du tableau.
Le sens de la variable détermine les comparaisons pertinentes. Une durée reste une mesure, même lorsqu’elle est enregistrée en heures entières.

Une variable binaire possède deux modalités. Par exemple, Internet_Access contient Yes ou No. Ces réponses décrivent la présence ou l’absence d’un accès ; elles ne mesurent ni sa vitesse ni sa qualité. Si l’on remplace des catégories par des codes entiers, ces codes restent des étiquettes tant qu’aucune quantité n’est définie.

2. Distinguer le sens statistique du type pandas

Le type de stockage indique comment pandas représente une colonne en mémoire. L’attribut dtypes permet de le consulter. Il ne précise pas, à lui seul, l’unité d’une durée, le sens d’un code ou l’ordre d’une série de catégories.

Dans le notebook, commencez par la préparation des bibliothèques. Cette cellule appelle uv uniquement si pandas ou openpyxl manque, et cible le Python du noyau sélectionné.

from importlib.util import find_spec
import subprocess
import sys

manquants = [nom for nom in ("pandas", "openpyxl")
            if find_spec(nom) is None]
if manquants:
    subprocess.run(
        ["uv", "pip", "install", "--python", sys.executable, *manquants],
        check=True,
    )
else:
    print("pandas et openpyxl sont déjà installés.")
pandas et openpyxl sont déjà installés.

Nous chargeons ensuite la vue de régression : sa seule cible est Exam_Score et Result est exclu dès la lecture. df désigne le DataFrame principal, c’est-à-dire le tableau pandas. Pour une tâche de classification, il faudrait constituer une vue distincte contenant les étiquettes Result déjà fournies et excluant Exam_Score ; cette autre tâche n’intervient pas dans les calculs ci-dessous.

import pandas as pd

fichier = "StudentPerformanceFactors_Reg_Classif.xlsx"
df = pd.read_excel(
    fichier, sheet_name="Données", engine="openpyxl",
    usecols=lambda nom: nom != "Result",
)
print("Observations :", len(df))
print("Colonnes :", df.shape[1])
print("pandas :", pd.__version__)
colonnes = ["Hours_Studied", "Tutoring_Sessions",
            "Motivation_Level", "School_Type"]
print(df[colonnes].dtypes.to_string())
Observations : 6607
Colonnes : 20
pandas : 3.0.6
Hours_Studied        int64
Tutoring_Sessions    int64
Motivation_Level       str
School_Type            str

Le tableau compte 6 607 lignes et 20 colonnes. Avec pandas 3.0.6, les deux champs textuels affichent le type str et les deux champs numériques le type int64. Des versions ou options de lecture différentes peuvent produire object ou string pour le texte. Le changement introduit avec pandas 3.0 explique notamment le type str par défaut. La variable motivation reste une catégorie ordinale, quel que soit ce type de stockage initial.

Hours_Studied décrit une durée en heures par semaine, prend le type int64 après lecture dans pandas et sert d’entrée dans la tâche de régression.
La même colonne se décrit par son sens, sa représentation informatique et son rôle dans la tâche choisie. Ces trois informations se complètent.

3. Documenter les unités et les domaines autorisés

Une unité donne du sens à une quantité : heures par semaine, pourcentage ou séances par mois. Un domaine autorisé précise quelles valeurs sont acceptées pour un champ : un intervalle numérique, des entiers non négatifs ou une liste de catégories. Ces règles proviennent du dictionnaire des données et du contexte de collecte. Elles doivent être explicites avant de corriger une valeur.

VariableSens et unitéRègle ou information à conserver
Hours_StudiedDurée d’étude en heures par semaine.Durée non négative ; 168 heures est un plafond de cohérence sur sept jours.
AttendancePart des cours suivis, en pourcentage.Valeur comprise entre 0 et 100, bornes incluses.
Sleep_HoursDurée moyenne de sommeil en heures par nuit.Mesure enregistrée en heures entières ; une valeur négative ou supérieure à 24 nécessite une vérification.
Tutoring_SessionsNombre de séances par mois.Entier supérieur ou égal à zéro ; aucune borne maximale n’est déduite du maximum observé.
Family_IncomeNiveau de revenu, sans montant monétaire fourni.Low, Medium, High, dans cet ordre.
Distance_from_HomeÉloignement du domicile, sans distance en kilomètres fournie.Near, Moderate, Far, dans cet ordre ; absence à traiter séparément.
Exemples de définitions et de contrôles à distinguer des valeurs simplement observées.

Ces plafonds temporels sont des contrôles de cohérence choisis ici à partir des unités, pas des seuils validés de plausibilité pour chaque élève. Une durée de sommeil inférieure à 24 heures peut encore demander une investigation. De même, les modalités Near et Far ne permettent pas de calculer une distance moyenne en kilomètres.

numeriques = ["Hours_Studied", "Attendance", "Sleep_Hours",
              "Tutoring_Sessions", "Physical_Activity", "Exam_Score"]
print(df[numeriques].agg(["min", "max"]).T.to_string())
                   min  max
Hours_Studied        1   44
Attendance          60  100
Sleep_Hours          4   10
Tutoring_Sessions    0    8
Physical_Activity    0    6
Exam_Score          55  101

Dans ce fichier, la présence varie de 60 à 100 et le tutorat de 0 à 8 séances. Cela ne rend pas une présence de 55 % ou un total de 9 séances automatiquement invalide dans un nouveau fichier. Les minimums et maximums observés décrivent un échantillon ; les valeurs autorisées expriment une règle.

Attendance : les valeurs observées vont de 60 à 100 %, tandis que les pourcentages autorisés vont de 0 à 100 %.
Les deux intervalles partagent la même échelle. La présence observée dans le classeur ne représente qu’une partie du domaine défini pour un pourcentage.

La cible Exam_Score comporte une valeur de 101. Nous la conservons : la description « note à l’examen final » ne suffit pas à établir une règle universelle imposant un maximum de 100. Il faut vérifier le barème ou la source avant toute correction. Une valeur inhabituelle constitue un point à examiner, pas une autorisation de modifier la donnée.

4. Vérifier les nombres et les catégories sans les remplacer

Une vérification doit distinguer une valeur présente hors règle d’une information absente. Le code suivant teste quatre colonnes numériques. between contrôle un intervalle ; mod(1).eq(0) vérifie que le nombre de séances n’a pas de partie fractionnaire. notna() réserve le diagnostic « hors règle » aux valeurs renseignées.

regles = {
    "Attendance": df["Attendance"].between(0, 100),
    "Hours_Studied": df["Hours_Studied"].between(0, 168),
    "Sleep_Hours": df["Sleep_Hours"].between(0, 24),
    "Tutoring_Sessions": (
        df["Tutoring_Sessions"].ge(0)
        & df["Tutoring_Sessions"].mod(1).eq(0)
    ),
}
for nom, conforme in regles.items():
    hors_regle = df[nom].notna() & ~conforme
    print(nom, "| absences :", int(df[nom].isna().sum()),
          "| hors règle :", int(hors_regle.sum()))
    if hors_regle.any():
        print(df.loc[hors_regle, [nom]].to_string())
Attendance | absences : 0 | hors règle : 0
Hours_Studied | absences : 0 | hors règle : 0
Sleep_Hours | absences : 0 | hors règle : 0
Tutoring_Sessions | absences : 0 | hors règle : 0

Ces quatre colonnes ne contiennent ici ni valeurs manquantes ni valeurs hors des règles testées. Ce diagnostic porte uniquement sur les quatre règles testées et sur ce fichier. Il ne démontre pas que toutes les colonnes sont correctes ni que les observations sont représentatives d’une population.

Pour les catégories, la méthode isin teste l’appartenance à une liste. La comparaison respecte les libellés exacts : Low et une variante contenant un espace final ne correspondent pas à la même chaîne. Les listes ci-dessous reprennent la description des champs, plutôt que de déclarer autorisés tous les éléments apparaissant dans la colonne.

autorisees = {
    "Motivation_Level": ["Low", "Medium", "High"],
    "School_Type": ["Public", "Private"],
    "Teacher_Quality": ["Low", "Medium", "High"],
    "Distance_from_Home": ["Near", "Moderate", "Far"],
}
for nom, valeurs in autorisees.items():
    serie = df[nom]
    inattendues = serie.notna() & ~serie.isin(valeurs)
    print(nom, "| absences :", int(serie.isna().sum()),
          "| inattendues :", int(inattendues.sum()))
    if inattendues.any():
        print(serie.loc[inattendues].value_counts().to_string())
Motivation_Level | absences : 0 | inattendues : 0
School_Type | absences : 0 | inattendues : 0
Teacher_Quality | absences : 78 | inattendues : 0
Distance_from_Home | absences : 67 | inattendues : 0

Les quatre colonnes ne présentent aucun libellé inattendu. En revanche, Teacher_Quality compte 78 absences et Distance_from_Home en compte 67.

5. Déclarer une catégorie ordonnée dans pandas

Le type category permet à pandas de conserver une liste de modalités et un indicateur d’ordre. Avec CategoricalDtype, nous indiquons explicitement Low, Medium, High, puis ordered=True. L’ordre alphabétique ne correspond pas à cette progression. La documentation des données catégorielles précise comment ces informations servent au tri et aux comparaisons.

ordre = ["Low", "Medium", "High"]
serie = df["Motivation_Level"]
inattendues = serie.notna() & ~serie.isin(ordre)
if inattendues.any():
    raise ValueError("Catégories de motivation à vérifier avant conversion.")

df_prepare = df.copy()
type_motivation = pd.CategoricalDtype(categories=ordre, ordered=True)
df_prepare["Motivation_Level"] = serie.astype(type_motivation)
print("Type :", df_prepare["Motivation_Level"].dtype)
print("Ordre :", df_prepare["Motivation_Level"].cat.categories.tolist())
print("Ordonné :", df_prepare["Motivation_Level"].cat.ordered)
print("Absences avant / après :", int(serie.isna().sum()),
      int(df_prepare["Motivation_Level"].isna().sum()))
Type : category
Ordre : ['Low', 'Medium', 'High']
Ordonné : True
Absences avant / après : 0 0

La sortie confirme le type category, les trois modalités dans l’ordre voulu et aucune nouvelle absence. Pour School_Type, on déclarerait au contraire une catégorie non ordonnée. Aucune hiérarchie n’est établie entre Public et Private.

Déclarer un ordre ne signifie pas que l’écart entre Low et Medium égale celui entre Medium et High. Cela ne prépare pas non plus automatiquement les entrées de tous les modèles. Le choix de l’encodage dépendra de l’algorithme et du sens du champ. Enfin, un export CSV ne conserve pas le type category ni cet ordre : gardez leur déclaration dans le notebook ou dans un dictionnaire séparé.

6. Mise en pratique : une distance sans kilomètres

Reprenez Distance_from_Home. Déterminez sa famille, ses modalités autorisées et leur ordre. Expliquez pourquoi une moyenne en kilomètres serait injustifiée. À l’aide des contrôles précédents, indiquez le nombre de valeurs manquantes et de libellés inattendus, puis vérifiez les effectifs de chaque modalité sans remplacer les valeurs manquantes.

Afficher la correction et le code

Distance_from_Home est une variable catégorielle ordinale : Near, Moderate, Far décrivent un éloignement croissant. Aucune distance numérique ni frontière en kilomètres n’est fournie. Les convertir en kilomètres inventerait une mesure.

serie = df["Distance_from_Home"]
ordre_distance = ["Near", "Moderate", "Far"]
print(serie.value_counts().reindex(ordre_distance, fill_value=0).to_string())
print("Absences :", int(serie.isna().sum()))
print("Libellés inattendus :", int(
    (serie.notna() & ~serie.isin(ordre_distance)).sum()
))
Distance_from_Home
Near        3884
Moderate    1998
Far          658
Absences : 67
Libellés inattendus : 0

On retrouve 3 884 valeurs Near, 1 998 valeurs Moderate, 658 valeurs Far et 67 absences, soit les 6 607 observations. Aucun libellé présent n’est hors de la liste autorisée. La méthode reindex présente les effectifs dans l’ordre choisi. Son argument fill_value=0 indique un effectif nul pour une modalité qui n’apparaîtrait pas dans le comptage ; il ne remplace aucune valeur manquante dans df.

7. Erreurs fréquentes

  • Déduire le sens du seul type pandas. Lisez la définition du champ : un entier peut représenter un nombre de séances, une durée enregistrée à l’heure près ou un code.
  • Oublier la période de mesure. Des heures par nuit et des heures par semaine ne se comparent pas directement. Vérifiez la période et la définition de la mesure avant toute comparaison : une durée moyenne par nuit ne donne pas à elle seule le total de sommeil d’une semaine donnée.
  • Confondre intervalle observé et domaine autorisé. Le maximum de huit séances décrit le fichier, sans définir une limite générale.
  • Confondre absence et modalité. No est une réponse connue ; une case vide ne renseigne pas cette réponse.
  • Convertir avant d’inspecter. Une catégorie non déclarée peut devenir manquante lors d’une conversion. Identifiez-la avant de changer le type.
  • Calculer une moyenne sur des codes ordinaux. Un ordre de niveaux ne garantit pas des écarts numériques comparables.

8. À retenir

Le sens statistique, le type pandas et le rôle d’une colonne répondent à des questions différentes. La durée de sommeil est une quantité, elle est représentée ici par des entiers après lecture dans pandas et elle peut servir d’entrée dans une tâche donnée. La cible se choisit séparément. Pour chaque champ, conservez sa définition, son unité éventuelle, ses valeurs autorisées et sa règle de gestion des absences.

Vérifiez les valeurs avant de convertir ou de corriger. Une liste explicite protège les catégories attendues ; un ordre explicite évite de laisser le tri alphabétique décider du sens. Le notebook garde ces décisions reproductibles sans écraser les données d’origine.

Sources et documentation