NBA stats 🏀

TP5
Author

Ludovic Deneuville

Introduction

Vous allez travailler sur des données de la nba depuis les années 2000.

Les donnĂ©es sont brutes i.e. elles ont Ă©tĂ© rĂ©cupĂ©rĂ©e via l’api stats nba et elles n’ont quasiment pas Ă©tĂ© retouchĂ©es.

Le modĂšle de donnĂ©es n’est pas fourni ! Ce sera Ă  vous d’explorer vous mĂȘme la base de donnĂ©es.

Tip

Dans votre vie professionnelle, dans 95 % des cas vous allez hĂ©riter de missions oĂč la documentation est obsolĂšte voire inexistante.

Il faut donc apprendre Ă  vous dĂ©brouiller dĂšs maintenant. Et au passage, vous convaincre que faire et maintenir la doc ça aide et c’est bien !

1 Importer la base de données

Aujourd’hui, pas de script sql pour crĂ©er les tables et importer les donnĂ©es, vous allez utiliser un dump.

NoteDéfinition

Un dump de base de donnĂ©es est un fichier qui contient une copie complĂšte d’une base de donnĂ©es.

Il sert Ă  :

  • Sauvegarder une base de donnĂ©es
  • TransfĂ©rer une base vers un autre serveur
  • Reconstituer la base Ă  l’identique (tables, donnĂ©es, index, etc.)

En bref : c’est une photographie de la base de donnĂ©es Ă  un instant donnĂ©, utilisable pour la recrĂ©er ailleurs.

Le fichier nba.dump est disponible sur le stockage S3 de l’enseignant.

Pour rĂ©cupĂ©rer ce fichier et l’utiliser pour votre base de donnĂ©es :

    • ⚠ Si vous ĂȘtes sur le datalab du GENES
    • â†Ș Avant de lancer le script 5_import_for_students
    • â†Ș Dans le FILE_PATH, remplacez ludo2ne par ldeneuville-ensai
NoteExplications

Le script 5_import_for_students.py :

  • Se connecte au S3 de l’enseignant pour rĂ©cupĂ©rer le fichier nba.dump
    • Vous remarquerez que le fichier est dans un dossier nommĂ© diffusion
    • Ce dossier est accessible en lecture aux autres utilisateurs
  • Utilise les variables d’envionnement pour se connecter Ă  votre base de donnĂ©es
    • Celles que vous avez renseignĂ© dans le fichier .env
  • Utilise l’utilitaire pg_restore pour crĂ©er la base de donnĂ©es Ă  partir du dump

2 Rendez-vous en base inconnue

Je n’ai pas rĂ©ussi Ă  inviter Laury T. pour vous accompagner dans cette aventure mais voici quelques requĂȘtes utiles pour explorer une base de donnĂ©es PostgreSQL.

-- Liste des tables du schema
SELECT table_name
  FROM information_schema.tables
 WHERE table_schema = '<schema_name>'
   AND table_type = 'BASE TABLE'
 ORDER BY table_name;

-- Colonnes d une table
SELECT column_name,
       data_type,
       is_nullable,
       column_default
  FROM information_schema.columns
 WHERE table_schema = '<...>'
   AND table_name   = '<...>'
 ORDER BY ordinal_position;

-- Colonnes commencant ou terminant par id
SELECT table_name, 
       column_name,
       data_type
  FROM information_schema.columns
 WHERE table_schema = '<...>'
   AND (column_name ILIKE 'id%' OR column_name ILIKE '%id')
ORDER BY table_name, column_name;

-- Generer les CREATE TABLE basiques
SELECT 'CREATE TABLE ' || table_schema || '.' || table_name || ' ( ' || E'\n' ||
       STRING_AGG(
           '  ' || column_name || ' ' || UPPER(data_type),
           ',' || E'\n'
           ORDER BY ordinal_position
       )
       || E'\n);' AS create_table_script
  FROM information_schema.columns
 WHERE table_schema = '<...>'
 GROUP BY table_schema, table_name
 ORDER BY table_name;

Liste des colonnes des tables d’un schĂ©ma

WITH
params AS (
    SELECT
        '<...>' AS schemaname
),
cols AS (
    SELECT
        c.table_schema,
        c.table_name,
        c.column_name,
        c.ordinal_position,
        c.is_nullable,
        c.udt_name,
        c.character_maximum_length,
        c.column_default
    FROM information_schema.columns c
    JOIN params p ON c.table_schema = p.schemaname
),
pks AS (
    SELECT DISTINCT
        kcu.table_schema,
        kcu.table_name,
        kcu.column_name
    FROM information_schema.table_constraints tc
    JOIN information_schema.key_column_usage kcu
         ON tc.constraint_name = kcu.constraint_name
        AND tc.table_schema = kcu.table_schema
    JOIN params p ON tc.table_schema = p.schemaname
    WHERE tc.constraint_type = 'PRIMARY KEY'
),
uniques AS (
    SELECT DISTINCT
        kcu.table_schema,
        kcu.table_name,
        kcu.column_name
    FROM information_schema.table_constraints tc
    JOIN information_schema.key_column_usage kcu
         ON tc.constraint_name = kcu.constraint_name
        AND tc.table_schema = kcu.table_schema
    JOIN params p ON tc.table_schema = p.schemaname
    WHERE tc.constraint_type = 'UNIQUE'
),
fks AS (
    SELECT DISTINCT ON (kcu.table_schema, kcu.table_name, kcu.column_name)
        kcu.table_schema,
        kcu.table_name,
        kcu.column_name,
        ccu.table_name AS ref_table,
        ccu.column_name AS ref_column
    FROM information_schema.table_constraints tc
    JOIN information_schema.key_column_usage kcu
         ON tc.constraint_name = kcu.constraint_name
        AND tc.table_schema = kcu.table_schema
    JOIN information_schema.constraint_column_usage ccu
         ON tc.constraint_name = ccu.constraint_name
    JOIN params p ON tc.table_schema = p.schemaname
    WHERE tc.constraint_type = 'FOREIGN KEY'
    ORDER BY kcu.table_schema, kcu.table_name, kcu.column_name
)
SELECT
    c.table_name AS "Table",
    c.column_name AS "Column",
    c.udt_name || COALESCE('(' || c.character_maximum_length::text || ')', '') AS "Type",
    CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END AS "PK",
    CASE WHEN fk.ref_table IS NOT NULL THEN fk.ref_table || '.' || fk.ref_column END AS "FK",
    CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END OR CASE WHEN u.column_name IS NOT NULL THEN true ELSE false END AS "Unique",
    CASE WHEN c.is_nullable ='YES' THEN true ELSE false END AS "Nullable",
    c.column_default AS "Default value"
FROM cols c
LEFT JOIN pks pk
       ON pk.table_schema = c.table_schema
      AND pk.table_name = c.table_name
      AND pk.column_name = c.column_name
LEFT JOIN uniques u
       ON u.table_schema = c.table_schema
      AND u.table_name = c.table_name
      AND u.column_name = c.column_name
LEFT JOIN fks fk
       ON fk.table_schema = c.table_schema
      AND fk.table_name = c.table_name
      AND fk.column_name = c.column_name
ORDER BY
    c.table_name,
    c.ordinal_position;

Une requĂȘte quasi complĂšte pour crĂ©er les tables d’un schĂ©ma :

WITH 
params AS (
    SELECT '<...>' AS schema_name
),
tables AS (
    SELECT table_name
    FROM information_schema.tables t
    JOIN params p ON t.table_schema = p.schema_name
    WHERE t.table_type = 'BASE TABLE'
),
cols AS (
    SELECT
        c.table_schema,
        c.table_name,
        c.ordinal_position,
        c.column_name,
        c.udt_name,
        c.character_maximum_length,
        c.is_nullable,
        c.column_default
    FROM information_schema.columns c
    JOIN params p ON c.table_schema = p.schema_name
    JOIN tables t ON c.table_name = t.table_name
),
pks AS (
    SELECT DISTINCT
        tc.table_schema,
        tc.table_name,
        kcu.column_name
    FROM information_schema.table_constraints tc
    JOIN information_schema.key_column_usage kcu
         ON tc.constraint_name = kcu.constraint_name
    JOIN params p ON tc.table_schema = p.schema_name
    WHERE tc.constraint_type = 'PRIMARY KEY'
),
uniques AS (
    SELECT DISTINCT
        tc.table_schema,
        tc.table_name,
        tc.constraint_name,
        STRING_AGG(kcu.column_name, ', ' ORDER BY kcu.ordinal_position) AS cols
    FROM information_schema.table_constraints tc
    JOIN information_schema.key_column_usage kcu
         ON tc.constraint_name = kcu.constraint_name
    JOIN params p ON tc.table_schema = p.schema_name
    WHERE tc.constraint_type = 'UNIQUE'
    GROUP BY tc.table_schema, tc.table_name, tc.constraint_name
),
fks AS (
    SELECT DISTINCT
        tc.table_schema,
        tc.table_name,
        tc.constraint_name,
        kcu.column_name,
        ccu.table_schema AS foreign_table_schema,
        ccu.table_name   AS foreign_table_name,
        ccu.column_name  AS foreign_column_name
    FROM information_schema.table_constraints tc
    JOIN information_schema.key_column_usage kcu
         ON tc.constraint_name = kcu.constraint_name
    JOIN information_schema.referential_constraints rc
         ON tc.constraint_name = rc.constraint_name
    JOIN information_schema.constraint_column_usage ccu
         ON rc.constraint_name = ccu.constraint_name
    JOIN params p ON tc.table_schema = p.schema_name
    WHERE tc.constraint_type = 'FOREIGN KEY'
)
SELECT
    'CREATE TABLE ' || c.table_schema || '.' || c.table_name || ' (' || E'\n' ||
    STRING_AGG(
        '  ' || c.column_name || ' ' || c.udt_name ||
        CASE WHEN c.character_maximum_length IS NOT NULL
             THEN '(' || c.character_maximum_length || ')' ELSE '' END ||
        CASE WHEN c.column_default IS NOT NULL
             THEN ' DEFAULT ' || c.column_default ELSE '' END ||
        CASE WHEN c.is_nullable = 'NO'
             THEN ' NOT NULL' ELSE '' END,
        ',' || E'\n'
        ORDER BY c.ordinal_position
    )
    -- PRIMARY KEY
    ||
    COALESCE((
        SELECT ',' || E'\n  PRIMARY KEY (' ||
               STRING_AGG(DISTINCT column_name, ', ') ||
               ')'
        FROM pks pk
        WHERE pk.table_name = c.table_name
    ), '')
    -- UNIQUE constraints
    ||
    COALESCE((
        SELECT STRING_AGG(
            ',' || E'\n  CONSTRAINT ' || constraint_name ||
            ' UNIQUE (' || cols || ')'
        , '')
        FROM uniques u
        WHERE u.table_name = c.table_name
    ), '')
    -- FOREIGN KEYS
    ||
    COALESCE((
        SELECT STRING_AGG(
            ',' || E'\n  CONSTRAINT ' || constraint_name ||
            ' FOREIGN KEY (' || column_name || ')' ||
            ' REFERENCES ' || foreign_table_schema || '.' || foreign_table_name ||
            ' (' || foreign_column_name || ')'
        , '')
        FROM fks fk
        WHERE fk.table_name = c.table_name
    ), '')
    || E'\n);\n' AS create_table_script
FROM cols c
GROUP BY c.table_schema, c.table_name
ORDER BY c.table_name;
);
Colonne Signification
matchup Description du match : ex. LAL @ BOS (Ă  l’extĂ©rieur) ou LAL vs BOS (Ă  domicile).
wl RĂ©sultat pour l’équipe : W (win), L (loss).
min Minutes totales jouĂ©es par l’équipe (toujours 240 sauf prolongation).
pts Total des points de l’équipe.
fgm Field goals made — tirs rĂ©ussis (2 pts + 3 pts).
fga Field goals attempted — tirs tentĂ©s.
fg_pct Taux de réussite : fgm / fga.
fg3m 3-points réussis.
fg3a 3-points tentés.
fg3_pct Pourcentage Ă  3 points.
ftm Free throws made — lancers francs rĂ©ussis.
fta Free throws attempted — lancers francs tentĂ©s.
ft_pct Pourcentage aux lancers.
oreb Rebonds offensifs.
dreb Rebonds défensifs.
reb Rebonds totaux (oreb + dreb).
ast Passes décisives.
stl Interceptions.
blk Contres.
tov Ballons perdus (turnovers).
pf Fautes personnelles.

Si un modÚle de données est nécessaire pour vous, vous pouvez par exemple :

  • GĂ©nĂ©rer les CREATE TABLE
  • Demander Ă  une IAgen de vous gĂ©nĂ©rer le modĂšle
  • Par exemple au format utilsĂ© par ce site : https://dbdiagram.io/

3 Explorons avec les Spurs

Tip

Vous allez avoir parfois des incompatibilités de types.

Vous pouvez utiliser par exemple <colname>::int ou <colname>::str pour caster.

    • classez ces matchs par date
    • Choisissez des exemples de game_id et vĂ©rifiez
    • Ou mieux Ă©crivez une requĂȘte qui vĂ©rifie l’hypothĂšse pour tous les game_id

Ce choix de conception a des avantages et des inconvénients. Nous y reviendrons par la suite lorsque nous afficherons les scores de chaque match.

Regardons maintenant les stats des joueurs.

4 Afficher les matchs

Dans toute cette partie nous nous interesserons Ă  la saison 2020-2021.

Interessons-nous la saison réguliÚre.

    • ajoutez le nombre de matchs jouĂ©s
    • classez par nombre de victoires dĂ©croissant

Passons maintenant aux Playoffs.

Une derniĂšre question pour la route :

Tout simplement :

  • Trouvez le dernier match des playoffs i.e. celui qui a le plus grand id
  • DĂ©duisez l’équipe ayant gagnĂ© le dernier match et la finaliste
  • Enfin listez tous les matchs de la finales pour avoir le score
WITH 
last_playoff_games AS (
SELECT season_id, MAX(game_id) AS last_game_id
  FROM nba.game
 WHERE season_type = 'Playoffs'
 GROUP BY season_id
),
final_teams AS (
SELECT g.season_id, 
       g.team_id AS team_id_win, 
       g2.team_id AS team_id_los
  FROM nba.game g
  JOIN last_playoff_games lpg ON g.season_id = lpg.season_id AND g.game_id = lpg.last_game_id
  JOIN nba.game g2 ON g2.game_id = g.game_id AND g2.team_id <> g.team_id
 WHERE g.wl = 'W'
)
SELECT gwin.season_id,
       twin.full_name AS equipe,
       COUNT(*) FILTER (WHERE gwin.wl = 'W') || ' - ' || COUNT(*) FILTER (WHERE gwin.wl = 'L') AS score,
       tlos.full_name AS adversaire
  FROM nba.game gwin
  JOIN final_teams ft ON ft.team_id_win = gwin.team_id AND ft.season_id = gwin.season_id
  JOIN nba.team twin ON twin.id = gwin.team_id
  JOIN nba.game glos ON glos.game_id = gwin.game_id AND glos.team_id = ft.team_id_los
  JOIN nba.team tlos ON tlos.id = glos.team_id
 WHERE gwin.season_type = 'Playoffs'
 GROUP BY gwin.season_id, twin.full_name, tlos.full_name
 ORDER BY gwin.season_id DESC;

5 Triple zéro

Lors d’un match, les principales statistiques utilisĂ©es pour mesurer les performances des joueurs sont : les points, les rebonds, les passes dĂ©cisives, les interceptions et les contres.

Un joueur réalise alors un :

  • double-double : s’il atteint au moins 10 dans deux de ces statistiques
  • triple-double : au moins 10 dans trois de ces statistiques (grosse perf)
  • quadruple-double : au moins 10 dans quatre de ces statistiques (trĂšs rare)

Il existe Ă©galement d’autres performances trĂšs rares :

  • double triple-double : s’il atteint au moins 20 dans trois de ces statistiques
  • Five-by-five : au moins 5 dans les cinq catĂ©gories

Vous aussi en avez assez d’entendre toujours les mĂȘmes noms rĂ©aliser ce genre de performance : Wemby, Curry, Gilgeous-Alexander, Doncic, Jokic


L’heure de la revanche a sonnĂ©. Mettons aujourd’hui en lumiĂšre les vrais champions, ceux qui rĂ©alisent des 0 dans ces stats, font des fautes, perdent des balles.

Tip
  • Commencez par retirer les lignes ou la colonne minutes est vide
  • Puis utilisez CAST(SPLIT_PART(minutes, ':', 1) AS integer)
    • quadruple zĂ©ro : 0 points et 3 autres stats Ă  0
    • Le CASE WHEN est votre ami ici

Quelques autres titres à décerner (toujours avec au moins 10min de jeu):

    • i.e. qui n’a marquĂ© aucun point, et a tentĂ© le plus grand nombre de tirs, lancers francs inclus

La colonne plusminuspoints est la diffĂ©rence entre le nombre de points marquĂ©s par l’équipe et le nombre de points encaissĂ©s lorsque le joueur est sur le terrain.

Cela donne une idĂ©e de l’influence et l’impact du joueur lorsqu’il est sur le terrain.

Ils ont essayĂ© de faire perdre leur Ă©quipe mais ils n’ont pas rĂ©ussi :

6 Classement par saison

Pour occuper les plus rapides 🚀

    • Triez par pourcentage de victoires
    • En cas d’égalitĂ©, vous pouvez si vous le souhaitez trouver les autres critĂšres de dĂ©paratage mais cela complique grandement la requĂȘte
    • ProcĂ©dez par Ă©tapes

Pour chaque saison réguliÚre donnez le match :

ArrĂȘtez vos services

C’est la fin du TP, vous pouvez maintenant sauvegarder votre travail et libĂ©rer les ressources rĂ©servĂ©es :