Analyse de données

Introduction à pandas

Combien de tirs à 3 points un joueur tente-t-il sur un match typique ? Avant de répondre, il faut charger 32 000 box scores, regarder leur forme et repérer les valeurs manquantes, sous peine de fausser le calcul de presque 20 %.

Le problème

Tu veux raconter une histoire qui revient chaque saison : la révolution du tir à 3 points. Combien de tentatives à 3 points un joueur prend-il sur un match « normal » ? Est-ce que la plupart des joueurs en tentent quelques-unes, ou est-ce que quelques bombardiers (Stephen Curry, Luka Dončić) tirent la moyenne vers le haut pendant que les autres n'en prennent presque jamais ?

La question a l'air simple. Mais avant d'y répondre, il y a un fichier de plus de 32 000 lignes à ouvrir : chaque match, chaque joueur, chaque statistique d'une saison entière. Personne ne lit ça à la main. Et surtout, un piège t'attend : si tu calcules la moyenne des tentatives à 3 points sur tout le fichier sans regarder ce qu'il contient, tu vas te tromper. Pas de peu. La bonne réponse et la fausse diffèrent de presque 20 %, à cause de lignes que tu n'as même pas vues.

Ce module, c'est apprendre à ouvrir, regarder et vérifier un tableau de données avant de calculer quoi que ce soit. C'est le geste numéro un de tout analyste, et celui qu'on saute le plus souvent, à ses dépens.

Le concept (et ce que tu vas gagner)

L'outil, c'est pandas : la bibliothèque Python qui manipule des données en tableau. Son objet central s'appelle un DataFrame.

À la fin de ce module, tu sauras :

  • charger un fichier CSV dans un DataFrame avec read_csv ;
  • inspecter sa forme et son contenu avec .head(), .info(), .dtypes et .describe() ;
  • comprendre pourquoi le type d'une colonne change tout pour les calculs qui suivent ;
  • détecter les valeurs manquantes avec .isna().sum() et, surtout, comprendre pourquoi tu dois le faire avant de calculer ;
  • sélectionner les colonnes qui t'intéressent.

Autrement dit : prendre un fichier inconnu et, en cinq commandes, savoir ce qu'il y a dedans et s'il est fiable.

La théorie en profondeur

Un DataFrame, c'est une grille typée par colonne

Retiens cette intuition, elle ne te quittera plus de tout le parcours. Un DataFrame, c'est un tableau, comme une feuille de calcul, mais piloté par du code :

  • chaque ligne est une observation : ici, la performance d'un joueur sur un match précis ;
  • chaque colonne est une variable : le nom du joueur, les points, les tentatives à 3 points… ;
  • une Series, c'est une seule colonne extraite du tableau. Quand tu écris df['fg3a'], tu obtiens une Series. Tout DataFrame est un assemblage de Series qui partagent le même index de lignes.

Le point essentiel, celui que les débutants ratent : une colonne entière partage un seul type. Pas une case sur deux. Toute la colonne pts est en entiers, toute la colonne player_name est en texte. C'est ce qui rend les calculs rapides, et c'est aussi ce qui te tend des pièges quand le type n'est pas celui que tu crois.

Pourquoi le type d'une colonne décide de tout

pandas devine le type de chaque colonne au moment de la lecture, en regardant les valeurs. Trois types reviennent tout le temps :

  • int64 : un entier (les points marqués sur un match, jamais de virgule) ;
  • float64 : un décimal (le plus_minus, qui peut valoir −27,0, et qui peut aussi être vide) ;
  • object (souvent du texte) : le nom du joueur, le code de l'équipe.

Pourquoi c'est crucial ? Parce que les opérations dépendent du type. Sur une colonne int64, df['pts'].mean() te donne une vraie moyenne. Sur une colonne object, .mean() n'a aucun sens et plante ou renvoie n'importe quoi. Et le piège classique : une colonne censée être numérique, mais lue comme du texte parce qu'une seule case contient un caractère parasite (un tiret, un « DNP », un symbole). À l'œil, le tableau a l'air normal. Au calcul, tout se casse.

Tu vas le voir en vrai dans ce fichier : la colonne min (le temps de jeu) est écrite 25:42, c'est-à-dire « 25 minutes 42 secondes ». Pour pandas, ce n'est pas un nombre, c'est du texte. Tu ne pourras pas en faire la moyenne tant que tu ne l'auras pas convertie. Repérer ça dès le départ, c'est exactement le rôle de .dtypes.

Détecter les valeurs manquantes, et comprendre ce qu'elles veulent dire

Une valeur manquante (notée NaN, pour Not a Number) apparaît quand une case est vide. pandas a une commande pour les compter : df.isna() renvoie une grille de True/False (manquant ou non), et .sum() additionne les True par colonne. Comme True vaut 1 et False vaut 0, tu obtiens directement le nombre de trous dans chaque colonne.

Mais le vrai enseignement n'est pas la commande, c'est ce que tu fais de son résultat. Une valeur manquante n'est presque jamais du hasard pur. Elle a une cause, et cette cause est de l'information.

Dans notre fichier, la colonne min a plus de 5 000 cases vides. Ce ne sont pas des erreurs de saisie : ce sont des joueurs qui n'ont pas joué ce soir-là (blessés, laissés au repos, en fin de banc). Le fichier les liste quand même, avec une ligne entière à zéro : 0 point, 0 passe, 0 tentative à 3 points. Et c'est là que se cache le piège du début de module.

Si tu calcules la moyenne de tentatives à 3 points sur toutes les lignes, tu fais entrer dans ton calcul des milliers de zéros qui ne correspondent à aucun match réellement joué. Le résultat : une moyenne tirée vers le bas, qui sous-estime la réalité du jeu. Si tu retires d'abord les lignes sans temps de jeu, la moyenne remonte nettement. Même fichier, même question, deux réponses différentes, et c'est uniquement la décision sur les valeurs manquantes qui fait la différence.

C'est pour ça qu'on regarde avant de calculer. Le .isna().sum() ne sert pas à faire joli : il te force à décider, en connaissance de cause, ce que tu fais des trous. Les ignorer (et fausser le calcul), les écarter (et changer la population étudiée), ou les remplir. Il n'y a pas de réponse universelle, mais il y a une faute universelle : ne pas avoir regardé.

Quand utiliser quoi, et dans quel ordre

Les quatre commandes d'inspection ne se valent pas, chacune répond à une question :

  • .head() : « à quoi ça ressemble ? » Un coup d'œil aux premières lignes pour vérifier que les colonnes sont là.
  • .info() : « combien de lignes, quels types, combien de valeurs non vides par colonne ? » C'est le bilan de santé complet, et il révèle les valeurs manquantes au passage.
  • .dtypes : « de quel type est chaque colonne ? » Le détail qui décide quels calculs sont possibles.
  • .describe() : « quels ordres de grandeur ? » Moyenne, écart-type, minimum, maximum, quartiles, pour toutes les colonnes numériques d'un coup. Attention : .describe() ignore d'office les colonnes texte. La colonne min, lue comme du texte, n'apparaîtra donc pas. Encore une raison de connaître ses types.

L'ordre naturel : charger, .head() pour voir, .dtypes ou .info() pour les types et les trous, .describe() pour les ordres de grandeur. Et seulement après, calculer.

Le code

Première étape : charger le fichier et le regarder. pd.read_csv lit le CSV et le transforme en DataFrame. .shape te donne (nombre de lignes, nombre de colonnes) et .head() affiche les premières lignes.

Loading...
Maj + Entrée

df.shape répond (32179, 20) : 32 179 lignes (une par performance individuelle sur un match) et 20 colonnes. La sortie de .head() montre les premières lignes : tu y vois player_name, l'équipe tm, l'adversaire opp, le temps de jeu min, les points pts, les tentatives à 3 points fg3a… Tu sais de quoi le tableau est fait, sans avoir lu une seule ligne à la main.

Deuxième étape : les types. On liste les colonnes et le type de chacune.

Loading...
Maj + Entrée

Regarde bien deux lignes de cette sortie. fg3a est en int64 : un entier, tu pourras en faire la moyenne. Mais min est en object, c'est-à-dire du texte. Logique : un temps de jeu est écrit 25:42, ce n'est pas un nombre pour pandas. Si tu tentais df['min'].mean(), tu obtiendrais une erreur. Le type t'a prévenu avant que tu te plantes. (Convertir cette colonne, ce sera le sujet du module suivant.)

Troisième étape : les ordres de grandeur, avec .describe().

Loading...
Maj + Entrée

Surprise utile : la colonne min n'apparaît pas dans le tableau de résumé, alors qu'on l'a demandée. C'est normal, .describe() ignore le texte. Confirmation directe de ce que .dtypes nous disait. Pour le reste, lis la ligne mean : un joueur tente en moyenne 2,8 tirs à 3 points par ligne du fichier. Garde ce chiffre en tête, on va le remettre en question. Et regarde la ligne max de pts : 83. L'écart entre la moyenne (autour de 9) et ce maximum te dit que la distribution est très étirée vers le haut, et signale au passage une valeur extrême à inspecter de près.

Quatrième étape, la plus importante : les valeurs manquantes.

Loading...
Maj + Entrée

Le verdict tombe : une seule colonne a des trous, min, avec 5 528 valeurs manquantes, soit 17,2 % des lignes. Toutes les colonnes de statistiques (pts, ast, fg3a…) sont pleines. Donc le problème n'est pas une donnée perdue : c'est que 17 % des lignes correspondent à des joueurs qui n'ont pas joué. Vérifions ce que ça implique pour notre calcul.

Loading...
Maj + Entrée

C'est le cœur du module. Les 5 528 lignes sans temps de jeu sont à 0 point à 100 % : ce sont bien des matchs non joués. Et le résultat parle de lui-même : la moyenne de tentatives à 3 points est de 2,83 sur tout le fichier, mais de 3,41 si on garde seulement les joueurs qui ont vraiment joué. Près de 20 % d'écart, uniquement parce que des milliers de zéros parasites traînaient dans le calcul. Voilà pourquoi on regarde les valeurs manquantes avant de calculer : la même question avait deux réponses, et seul le contrôle de propreté permettait de choisir la bonne.

Cinquième étape : la visualisation. On répond enfin à la question de départ, sur les joueurs qui ont réellement joué, en regardant la forme de la distribution des tentatives à 3 points.

Loading...
Maj + Entrée

La forme raconte tout. La barre la plus haute est à zéro : 19,7 % des matchs joués se passent sans la moindre tentative à 3 points (les intérieurs classiques, les fins de banc). Puis la courbe décroît régulièrement vers la droite, avec une longue queue : seulement 4,3 % des matchs comptent 10 tentatives ou plus, et une poignée vont jusqu'à plus de 20. La moyenne (3,4, trait rouge) est tirée à droite de la médiane (3, trait jaune) par cette queue : c'est la signature d'une distribution asymétrique, exactement ce que tu as appris à lire dans le parcours Fondamentaux des statistiques. Et ta réponse à la question de départ est là, dessinée : la majorité des joueurs en tentent peu, quelques bombardiers tirent la moyenne vers le haut.

À toi de jouer

Tu as la distribution d'ensemble. Maintenant, qui sont ces bombardiers de la queue de droite ? Complète et lance la cellule : elle regroupe par joueur, garde ceux qui ont disputé au moins 20 matchs, et classe par tentatives moyennes à 3 points. Tu retrouveras les noms attendus en haut du classement.

Loading...
Maj + Entrée

Conclusion

Tu es parti d'une question d'analyste, « à quoi ressemble le volume de tirs à 3 points ? », et tu y as répondu proprement. Au passage, tu as évité le piège qui guette tout débutant : calculer sur un fichier sans l'avoir regardé. Les 17 % de lignes de joueurs absents changeaient ta moyenne de presque 20 %.

Ce que tu as appris

  • Un DataFrame est un tableau où chaque ligne est une observation et chaque colonne une variable d'un seul type ; une colonne isolée est une Series.
  • pd.read_csv() charge un CSV, .shape donne sa taille, .head() montre les premières lignes.
  • .dtypes (et .info()) révèle le type de chaque colonne, et pourquoi ça compte : on ne calcule pas sur du texte, et un type inattendu trahit une donnée à nettoyer (ici, min écrit 25:42).
  • .describe() résume d'un coup toutes les colonnes numériques, et ignore le texte.
  • .isna().sum() compte les valeurs manquantes, et le réflexe est de regarder avant de calculer : un trou n'est pas du bruit, il porte une information (ici, un match non joué).
  • Sélectionner des colonnes avec df[['col1', 'col2']] et des lignes avec un filtre comme df[df['min'].notna()].

Ce que ça change pour le basket

Tout analyste de front office commence sa journée exactement comme ça. Avant de dire à un GM « ce joueur shoote plus en déplacement » ou « cette équipe s'effondre en fin de match », il charge les box scores, regarde la forme du tableau, vérifie les types et chasse les valeurs manquantes. Notre exemple le montre concrètement : compter les soirs sans jeu dans une moyenne de tirs, c'est sous-estimer le volume réel d'un joueur, et donc mal évaluer son profil. Dans une ligue où un contrat se chiffre en dizaines de millions, une moyenne calculée sur des données mal regardées, c'est une recommandation fausse.

Ce qui vient ensuite

Tu sais charger et inspecter un tableau. Mais ici, une colonne te résistait déjà : min, écrite 25:42, illisible pour un calcul. Et ce n'était qu'un avant-goût. La plupart des fichiers réels sont pleins de ces colonnes mal typées : un salaire écrit $55 761 216, une taille 6-9, une université vide. Avant d'analyser, il faut réparer ces données : convertir les colonnes mal typées, traiter les valeurs manquantes. C'est l'objet du prochain module, le nettoyage des données.

Quiz

1.Sur tout le fichier, la moyenne de tentatives à 3 points est de 2,83. En ne gardant que les joueurs ayant réellement joué, elle passe à 3,41. Comment expliques-tu cet écart ?

2.Tu lances df.dtypes et la colonne min (temps de jeu) apparaît en object alors que tu la croyais numérique. Que fais-tu ?

3.Tu demandes df[['min', 'pts', 'fg3a']].describe() mais la colonne min n'apparaît pas dans le résultat. Pourquoi ?

4.L'histogramme des tentatives à 3 points décroît de la gauche vers la droite, avec une longue queue : la moyenne (3,4) est à droite de la médiane (3). Qu'est-ce que ça révèle ?

5.Vrai ou faux : df.isna().sum() ayant affiché 0 sur la colonne pts, tu peux ignorer le contrôle des valeurs manquantes sur les autres colonnes et calculer directement.

Téléchargements

Récupère le script Python complet de ce module et le jeu de données utilisé, pour rejouer l'analyse chez toi.