Analyse de données

Introduction à pandas

Combien de tirs à 3 points un joueur tente-t-il sur un match typique ? La réponse est dans 32 179 lignes de box score : une ligne par joueur et par match, pour les 592 joueurs des 30 équipes, sur toute la saison régulière 2025-26. Avant de calculer, il faut regarder ce fichier et repérer ses valeurs manquantes, sous peine de fausser le résultat de presque 20 %.

Le problème

Tu veux raconter une histoire qui revient chaque saison : la révolution du tir à 3 points. Combien de tirs à 3 points un joueur prend-il sur un match « normal » ? Est-ce que presque tout le monde en tente quelques-uns, ou est-ce qu'une minorité de très gros tireurs, les Stephen Curry et les Luka Dončić, tire la moyenne vers le haut pendant que le reste de la ligue n'en prend presque jamais ?

La question a l'air simple. Mais avant d'y répondre, il y a une table de 32 179 lignes à explorer. Une ligne par joueur et par match : les 592 joueurs qui sont entrés sur un terrain cette saison-là, répartis dans les 30 équipes de la ligue, sur une saison régulière complète, d'octobre 2025 à avril 2026. Chaque ligne porte les statistiques d'un joueur sur un match : minutes, points, passes, tirs tentés. Ça ne se lit pas à la main. Et surtout, un piège t'attend : si tu calcules la moyenne des tirs à 3 points sur toute la table sans regarder ce qu'elle contient, tu vas te tromper. Et l'écart n'est pas anecdotique : la vraie réponse est presque 20 % plus haute que celle que tu obtiendrais, parce que des milliers de lignes de la table ne correspondent à aucun match réellement joué.

Voici à quoi ressemblent quatre lignes de cette table, telles quelles :

player_namegame_dateoppminptsfg3a
Andrew Wiggins2026-04-12ATL25:4244
Bam Adebayo2026-04-12ATL29:14254
Kel'el Ware2026-04-12ATL29:10165
Pelle Larsson2026-04-12ATL(vide)00

Une ligne par joueur et par match. Regarde déjà deux choses : le temps de jeu est écrit 25:42, comme sur une feuille de match et non comme un nombre, et la dernière ligne n'a pas de temps de jeu du tout, avec des statistiques entièrement à zéro.

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 apprendre

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 une table inconnue et, en cinq commandes, savoir ce qu'il y a dedans et s'il est fiable.

La théorie en détail

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

Garde cette image en tête, 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, et celui qu'on oublie le plus souvent : 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. C'est le type du plus_minus, alors que ses valeurs sont entières : il suffit qu'une case soit vide pour que pandas bascule toute la colonne en décimal ;
  • 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 cette table : 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 cases vides dans chaque colonne.

Mais la commande n'est que la moitié du travail : ce qui compte, 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 table, 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). La table 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 table, 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() n'est pas une formalité : il t'oblige à décider, en connaissance de cause, ce que tu fais des cases vides. 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 la vue la plus complète des trois : en une commande, tu obtiens la taille du tableau, le type de chaque colonne et le nombre de cases remplies, ce dernier chiffre révélant du même coup les valeurs manquantes.
  • .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 cases vides, .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...

df.shape répond (32179, 20) : 32 179 lignes (une par performance individuelle sur un match) et 20 colonnes. Les deux lignes suivantes confirment le périmètre annoncé, 592 joueurs du 21 octobre 2025 au 12 avril 2026. 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...

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

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 de la table. 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...

Une seule colonne a des cases vides, 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...

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 toute la table, 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...

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 le record de la saison culmine à 22 tentatives en un seul match. 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, une minorité de très gros tireurs tire la moyenne vers le haut.

À toi de jouer

Tu as la distribution d'ensemble. Maintenant, qui sont ces très gros tireurs 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...

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 le plus courant du métier : calculer sur une table sans l'avoir regardée. 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 : une case vide n'est pas du bruit, elle 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 tables réelles sont pleines de ces colonnes mal typées : un salaire écrit $11,808,240, une taille 7-0, une université vide (les trois cas d'Alex Sarr). 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.

Introduction à pandas (Analyse de données) · Malyaipfy