Devoir 3 — TP marathon¶

Bienvenue dans votre troisième devoir ! Changement de décor : après numpy, nous passons à pandas, découvert en séance 3. Ce devoir prend la forme d'un TP guidé, dans lequel nous allons charger un jeu de données de marathons puis l'interroger : sélections, premiers agrégats, et une première rencontre avec les durées.

{admonition}
:class: important

- Ce notebook reste dans le dossier `D3/` de votre dépôt `pe-homework`, puis il doit être **commité et poussé** (`git add`, `git commit`, `git push`) avant la deadline. Attention, c'est la **date du commit** qui fait foi, pas celle du push ; le nom du fichier, lui, n'a pas d'importance (`sujet.ipynb` convient très bien).
- Juste avant de rendre, **pensez à ré-exécuter toutes les cellules** via *Kernel → Restart Kernel and Run All Cells* : toutes doivent s'exécuter dans l'ordre et sans erreur.
- Le fichier `data/marathon.txt` doit rester dans un sous-dossier `data/` à côté du notebook.
- La documentation de `pandas` et les notebooks du cours sont autorisés ; les assistants IA sont tolérés, à condition que vous soyez **capable d'expliquer chaque ligne que vous rendez** — je pourrai vous le demander à l'oral en séance.
{admonition}
:class: note

| Partie | Points |
|--------|--------|
| chargement propre (`sep`, `names`) + sauvegarde csv | 4 |
| recherches (1971, Londres 1981, villes) | 4 |
| extraits #1, #2, #3 (`iloc` / `loc` / masques, bons types de retour) | 5 |
| agrégats (moyenne, marathons par an) | 3 |
| durées (`to_timedelta`, `.dt`, colonnes `hour/minute/second`) | 4 |
| bonus : la section `dt.components` **et** une figure du record de Paris au fil des ans (`df.plot`) | +2 |

Les cellules « pour vérifier » doivent afficher `True` : elles jouent le rôle des cellules de test des devoirs précédents, ne les modifiez pas !
{admonition}
:class: note

Une `Series` ou une `DataFrame` sait se dessiner toute seule : `s.plot()` trace une courbe avec l'index en abscisse, `s.plot(marker="o")` ajoute des marqueurs, `df.plot.bar()` fait un diagramme en barres. C'est le sujet de la séance 5 ; pour le bonus, une seule ligne suffit une fois la série des records de Paris construite.

Ce TP est repris de https://numerique-exos.info-mines.paris/pandas-tps/marathon/readme-marathon-nb/ (Thierry Parmentelat, CC BY-NC-ND). Le corrigé existe donc en ligne ; je compte sur vous pour ne pas aller le regarder avant d'avoir rendu !

marathon (divers basique)¶

Voici un petit TP pour travailler trois briques de pandas :

  • le chargement et la sélection ;
  • un peu de groupby ;
  • un peu de gestion du temps et des durées.
In [1]:
import pandas as pd

les données¶

Nous allons étudier un jeu de données trouvé sur Internet : les temps records de plusieurs grands marathons, au fil des ans.

In [2]:
# 2024: le site original semble être *down*
# URL = "http://www.xavierdupre.fr/enseignement/complements/marathon.txt"

DATA = "data/marathon.txt"
In [3]:
# regardons les 5 premières lignes du fichier de données
# (ou bien ouvrez-le dans vs-code)

with open(DATA) as f:
    for _ in range(5):
        print(next(f), end="")
PARIS	2011	02:06:29	7589
PARIS	2010	02:06:41	7601
PARIS	2009	02:05:47	7547
PARIS	2008	02:06:40	7600
PARIS	2007	02:07:17	7637

chargement¶

Pour charger un fichier de ce genre, le premier réflexe est d'utiliser la fonction read_csv de pandas.

In [4]:
# votre cellule de code
# qu'on va faire descendre
# et raffiner au fur et à mesure

df0 = pd.read_csv(DATA)
df0.head()
Out[4]:
PARIS\t2011\t02:06:29\t7589
0 PARIS\t2010\t02:06:41\t7601
1 PARIS\t2009\t02:05:47\t7547
2 PARIS\t2008\t02:06:40\t7600
3 PARIS\t2007\t02:07:17\t7637
4 PARIS\t2006\t02:08:03\t7683

C'est un début, mais le résultat ne ressemble pas franchement à ce qu'on espérait !

Il faut donc regarder la documentation d'un peu plus près.

In [5]:
# pd.read_csv?

Pour commencer, je vous invite à préciser le séparateur :

In [6]:
# à vous de modifier cette première approche

df1 = pd.read_csv(DATA)
In [7]:
# prune-cell

df1 = pd.read_csv(DATA, sep='\t')
In [8]:
# pour vérifier, ceci doit afficher True

df1.shape == (358, 4) and df1.iloc[0, 0] == 'PARIS' and df1.columns[0] == 'PARIS'
Out[8]:
True

C'est mieux, mais les noms des colonnes ne sont pas corrects. En effet, par défaut, read_csv utilise la première ligne du fichier pour déterminer les noms des colonnes ; or notre fichier texte ne contient pas de ligne d'en-tête (voyez ci-dessus) !

Il serait donc pertinent de donner nous-mêmes un nom aux colonnes.

In [9]:
NAMES = ["city", "year", "duration", "seconds"]
In [10]:
# à vous de créer une donnée bien propre
df = ...
In [11]:
# prune-cell
# si en plus on précise le nom des colonnes 
# ça commence à être franchement mieux

df = pd.read_csv(DATA, sep="\t", names=NAMES)
In [12]:
# pour vérifier, ceci doit afficher True

df.shape == (359, 4) and df.iloc[0, 0] == 'PARIS' and df.columns[0] == 'city'
Out[12]:
True
In [13]:
# ce qui maintenant nous donne ceci

df.head(2)
Out[13]:
city year duration seconds
0 PARIS 2011 02:06:29 7589
1 PARIS 2010 02:06:41 7601

sauvegarde dans un fichier csv¶

Dans l'autre sens, quand on a produit une dataframe, on veut souvent sauver le résultat dans un fichier texte ; c'est le rôle de to_csv.

In [14]:
# df.to_csv?

Par ex., je crée ici un fichier qu'on pourra relire sous Excel :

In [15]:
loop = "marathon-loop.csv"
df.to_csv(loop, sep=";", index=False)
In [16]:
# pour voir un aperçu
#  nouveau vous pouvez regarder le fichier avec vs-code 
# ou encore dans le terminal avec $ less marathon-loop.csv (sortir avec 'q')

%cat marathon-loop.csv
city;year;duration;seconds
PARIS;2011;02:06:29;7589
PARIS;2010;02:06:41;7601
PARIS;2009;02:05:47;7547
PARIS;2008;02:06:40;7600
PARIS;2007;02:07:17;7637
PARIS;2006;02:08:03;7683
PARIS;2005;02:08:02;7682
PARIS;2004;02:08:56;7736
PARIS;2003;02:06:33;7593
PARIS;2002;02:08:18;7698
PARIS;2001;02:09:40;7780
PARIS;2000;02:08:49;7729
PARIS;1999;02:08:10;7690
PARIS;1998;02:09:37;7777
PARIS;1997;02:10:14;7814
PARIS;1996;02:12:18;7938
PARIS;1995;02:10:06;7806
PARIS;1994;02:10:56;7856
PARIS;1993;02:10:46;7846
PARIS;1992;02:10:03;7803
PARIS;1990;02:13:10;7990
PARIS;1989;02:13:03;7983
PARIS;1988;02:13:53;8033
PARIS;1987;02:11:09;7869
PARIS;1986;02:12:44;7964
PARIS;1985;02:10:49;7849
PARIS;1984;02:11:58;7918
PARIS;1983;02:12:38;7958
PARIS;1982;02:14:07;8047
PARIS;1981;02:11:44;7904
PARIS;1980;02:25:50;8750
PARIS;1979;02:18:53;8333
PARIS;1978;02:34:55;9295
PARIS;1977;02:30:41;9041
PARIS;1976;02:20:57;8457
BERLIN;2011;02:03:38;7418
BERLIN;2010;02:05:08;7508
BERLIN;2009;02:06:08;7568
BERLIN;2008;02:03:59;7439
BERLIN;2007;02:04:26;7466
BERLIN;2006;02:05:56;7556
BERLIN;2005;02:07:41;7661
BERLIN;2004;02:06:44;7604
BERLIN;2003;02:04:55;7495
BERLIN;2002;02:06:47;7607
BERLIN;2001;02:08:47;7727
BERLIN;2000;02:07:42;7662
BERLIN;1999;02:06:44;7604
BERLIN;1998;02:06:05;7565
BERLIN;1997;02:07:41;7661
BERLIN;1996;02:09:15;7755
BERLIN;1995;02:07:02;7622
BERLIN;1994;02:08:31;7711
BERLIN;1993;02:10:57;7857
BERLIN;1992;02:08:07;7687
BERLIN;1991;02:10:57;7857
BERLIN;1990;02:08:16;7696
BERLIN;1989;02:10:11;7811
BERLIN;1988;02:11:45;7905
BERLIN;1987;02:11:11;7871
BERLIN;1986;02:11:03;7863
BERLIN;1985;02:11:43;7903
BERLIN;1984;02:13:35;8015
BERLIN;1983;02:13:37;8017
BERLIN;1982;02:14:47;8087
BERLIN;1981;02:15:42;8142
BERLIN;1980;02:16:48;8208
BERLIN;1979;02:21:09;8469
BERLIN;1978;02:20:03;8403
BERLIN;1977;02:15:19;8119
BERLIN;1976;02:23:08;8588
BERLIN;1975;02:47:08;10028
BERLIN;1974;02:44:53;9893
FUKUOKA;2010;02:08:24;7704
FUKUOKA;2009;02:05:18;7518
FUKUOKA;2008;02:06:10;7570
FUKUOKA;2007;02:06:39;7599
FUKUOKA;2006;02:06:52;7612
FUKUOKA;2005;02:08:29;7709
FUKUOKA;2004;02:09:10;7750
FUKUOKA;2003;02:07:52;7672
FUKUOKA;2002;02:09:13;7753
FUKUOKA;2001;02:09:25;7765
FUKUOKA;2000;02:06:51;7611
FUKUOKA;1999;02:07:54;7674
FUKUOKA;1998;02:08:42;7722
FUKUOKA;1997;02:07:28;7648
FUKUOKA;1996;02:10:48;7848
FUKUOKA;1995;02:09:30;7770
FUKUOKA;1994;02:09:45;7785
FUKUOKA;1993;02:08:51;7731
FUKUOKA;1992;02:09:04;7744
FUKUOKA;1991;02:10:58;7858
FUKUOKA;1990;02:11:35;7895
FUKUOKA;1989;02:12:54;7974
FUKUOKA;1988;02:11:04;7864
FUKUOKA;1987;02:08:18;7698
FUKUOKA;1986;02:10:06;7806
FUKUOKA;1985;02:09:51;7791
FUKUOKA;1984;02:10:00;7800
FUKUOKA;1983;02:08:52;7732
FUKUOKA;1982;02:10:15;7815
FUKUOKA;1981;02:08:18;7698
FUKUOKA;1980;02:09:45;7785
FUKUOKA;1979;02:10:35;7835
FUKUOKA;1978;02:10:21;7821
FUKUOKA;1977;02:10:56;7856
FUKUOKA;1976;02:12:35;7955
FUKUOKA;1975;02:10:09;7809
FUKUOKA;1974;02:11:32;7892
FUKUOKA;1973;02:11:45;7905
FUKUOKA;1972;02:10:30;7830
FUKUOKA;1971;02:12:51;7971
FUKUOKA;1970;02:10:38;7838
FUKUOKA;1969;02:11:13;7873
FUKUOKA;1968;02:10:48;7848
FUKUOKA;1967;02:09:37;7777
FUKUOKA;1966;02:14:05;8045
FUKUOKA;1965;02:18:36;8316
FUKUOKA;1964;02:14:49;8089
FUKUOKA;1963;02:18:01;8281
FUKUOKA;1962;02:16:19;8179
FUKUOKA;1961;02:22:05;8525
FUKUOKA;1960;02:19:04;8344
FUKUOKA;1959;02:29:34;8974
FUKUOKA;1958;02:24:01;8641
FUKUOKA;1957;02:21:40;8500
FUKUOKA;1956;02:25:15;8715
FUKUOKA;1955;02:23:16;8596
FUKUOKA;1954;02:24:55;8695
FUKUOKA;1953;02:27:26;8846
FUKUOKA;1952;02:27:59;8879
FUKUOKA;1951;02:30:13;9013
FUKUOKA;1950;02:30:47;9047
FUKUOKA;1949;02:40:26;9626
FUKUOKA;1948;02:37:25;9445
FUKUOKA;1947;02:45:45;9945
AMSTERDAM;1975;02:16:51;8211
AMSTERDAM;1976;02:18:48;8328
AMSTERDAM;1977;02:12:47;7967
AMSTERDAM;1979;02:14:46;8086
AMSTERDAM;1980;02:09:01;7741
AMSTERDAM;1981;02:18:11;8291
AMSTERDAM;1982;02:12:15;7935
AMSTERDAM;1983;02:13:41;8021
AMSTERDAM;1984;02:14:28;8068
AMSTERDAM;1985;02:19:24;8364
AMSTERDAM;1986;02:14:46;8086
AMSTERDAM;1987;02:12:40;7960
AMSTERDAM;1988;02:12:38;7958
AMSTERDAM;1989;02:13:52;8032
AMSTERDAM;1990;02:11:52;7912
AMSTERDAM;1991;02:13:26;8006
AMSTERDAM;1992;02:14:58;8098
AMSTERDAM;1993;02:11:56;7916
AMSTERDAM;1994;02:15:56;8156
AMSTERDAM;1995;02:14:00;8040
AMSTERDAM;1996;02:10:57;7857
AMSTERDAM;1997;02:08:24;7704
AMSTERDAM;1998;02:08:13;7693
AMSTERDAM;1999;02:06:47;7607
AMSTERDAM;2000;02:08:57;7737
AMSTERDAM;2001;02:07:02;7622
AMSTERDAM;2002;02:07:26;7646
AMSTERDAM;2003;02:06:39;7599
AMSTERDAM;2004;02:06:22;7582
AMSTERDAM;2005;02:06:20;7580
AMSTERDAM;2006;02:08:52;7732
AMSTERDAM;2007;02:06:29;7589
AMSTERDAM;2008;02:07:52;7672
AMSTERDAM;2009;02:06:18;7578
AMSTERDAM;2010;02:05:44;7544
LONDON;1981;02:11:48;7908
LONDON;1982;02:09:24;7764
LONDON;1983;02:09:43;7783
LONDON;1984;02:09:57;7797
LONDON;1985;02:08:16;7696
LONDON;1986;02:10:02;7802
LONDON;1987;02:09:50;7790
LONDON;1988;02:10:20;7820
LONDON;1989;02:09:03;7743
LONDON;1990;02:10:10;7810
LONDON;1991;02:09:17;7757
LONDON;1992;02:10:02;7802
LONDON;1993;02:10:50;7850
LONDON;1994;02:08:53;7733
LONDON;1995;02:08:30;7710
LONDON;1996;02:10:00;7800
LONDON;1997;02:07:55;7675
LONDON;1998;02:07:57;7677
LONDON;1999;02:07:57;7677
LONDON;2000;02:06:36;7596
LONDON;2001;02:07:09;7629
LONDON;2002;02:05:38;7538
LONDON;2003;02:07:56;7676
LONDON;2004;02:06:18;7578
LONDON;2005;02:07:35;7655
LONDON;2006;02:06:39;7599
LONDON;2007;02:07:41;7661
LONDON;2008;02:05:15;7515
LONDON;2009;02:05:10;7510
LONDON;2010;02:05:19;7519
LONDON;2011;02:04:40;7480
NEW YORK;1970;02:31:38;9098
NEW YORK;1971;02:22:54;8574
NEW YORK;1972;02:27:52;8872
NEW YORK;1973;02:21:54;8514
NEW YORK;1974;02:26:30;8790
NEW YORK;1975;02:19:27;8367
NEW YORK;1976;02:10:10;7810
NEW YORK;1977;02:11:28;7888
NEW YORK;1978;02:12:12;7932
NEW YORK;1979;02:11:42;7902
NEW YORK;1980;02:09:41;7781
NEW YORK;1981;02:08:13;7693
NEW YORK;1982;02:09:29;7769
NEW YORK;1983;02:08:59;7739
NEW YORK;1984;02:14:53;8093
NEW YORK;1985;02:11:34;7894
NEW YORK;1986;02:11:06;7866
NEW YORK;1987;02:11:01;7861
NEW YORK;1988;02:08:20;7700
NEW YORK;1989;02:08:01;7681
NEW YORK;1990;02:12:39;7959
NEW YORK;1991;02:09:28;7768
NEW YORK;1992;02:09:29;7769
NEW YORK;1993;02:10:04;7804
NEW YORK;1994;02:11:21;7881
NEW YORK;1995;02:11:00;7860
NEW YORK;1996;02:09:54;7794
NEW YORK;1997;02:08:12;7692
NEW YORK;1998;02:08:45;7725
NEW YORK;1999;02:09:14;7754
NEW YORK;2000;02:10:09;7809
NEW YORK;2001;02:07:43;7663
NEW YORK;2002;02:08:07;7687
NEW YORK;2003;02:10:30;7830
NEW YORK;2004;02:09:28;7768
NEW YORK;2005;02:09:30;7770
NEW YORK;2006;02:09:58;7798
NEW YORK;2007;02:09:04;7744
NEW YORK;2008;02:08:43;7723
NEW YORK;2009;02:09:15;7755
NEW YORK;2010;02:08:14;7694
STOCKOLM;1979;02:17:35;8255
STOCKOLM;1980;02:15:49;8149
STOCKOLM;1981;02:13:26;8006
STOCKOLM;1982;02:19:20;8360
STOCKOLM;1983;02:11:37;7897
STOCKOLM;1984;02:13:47;8027
STOCKOLM;1985;02:17:18;8238
STOCKOLM;1986;02:12:33;7953
STOCKOLM;1987;02:13:52;8032
STOCKOLM;1988;02:14:26;8066
STOCKOLM;1989;02:13:34;8014
STOCKOLM;1990;02:13:04;7984
STOCKOLM;1991;02:12:38;7958
STOCKOLM;1992;02:15:58;8158
STOCKOLM;1993;02:16:30;8190
STOCKOLM;1994;02:14:06;8046
STOCKOLM;1995;02:14:29;8069
STOCKOLM;1996;02:15:05;8105
STOCKOLM;1997;02:17:22;8242
STOCKOLM;1998;02:16:12;8172
STOCKOLM;1999;02:14:52;8092
STOCKOLM;2000;02:18:49;8329
STOCKOLM;2001;02:18:17;8297
STOCKOLM;2002;02:18:20;8300
STOCKOLM;2003;02:18:14;8294
STOCKOLM;2004;02:16:12;8172
STOCKOLM;2005;02:13:28;8008
STOCKOLM;2006;02:17:01;8221
STOCKOLM;2007;02:20:56;8456
STOCKOLM;2008;02:16:03;8163
STOCKOLM;2009;02:15:34;8134
STOCKOLM;2010;02:12:48;7968
STOCKOLM;2011;02:14:07;8047
BOSTON;1963;02:18:58;8338
BOSTON;1964;02:19:59;8399
BOSTON;1965;02:16:33;8193
BOSTON;1966;02:17:11;8231
BOSTON;1967;02:15:45;8145
BOSTON;1968;02:22:17;8537
BOSTON;1969;02:13:49;8029
BOSTON;1970;02:10:30;7830
BOSTON;1971;02:18:45;8325
BOSTON;1972;02:15:39;8139
BOSTON;1973;02:16:03;8163
BOSTON;1974;02:13:39;8019
BOSTON;1975;02:09:55;7795
BOSTON;1976;02:20:19;8419
BOSTON;1977;02:14:46;8086
BOSTON;1978;02:10:13;7813
BOSTON;1979;02:09:27;7767
BOSTON;1980;02:12:11;7931
BOSTON;1981;02:09:26;7766
BOSTON;1982;02:08:52;7732
BOSTON;1983;02:09:00;7740
BOSTON;1984;02:10:34;7834
BOSTON;1985;02:14:05;8045
BOSTON;1986;02:07:51;7671
BOSTON;1987;02:11:50;7910
BOSTON;1988;02:08:43;7723
BOSTON;1989;02:09:06;7746
BOSTON;1990;02:08:19;7699
BOSTON;1991;02:11:06;7866
BOSTON;1992;02:08:14;7694
BOSTON;1993;02:09:33;7773
BOSTON;1994;02:07:15;7635
BOSTON;1995;02:09:22;7762
BOSTON;1996;02:09:15;7755
BOSTON;1997;02:10:34;7834
BOSTON;1998;02:07:34;7654
BOSTON;1999;02:09:52;7792
BOSTON;2000;02:09:47;7787
BOSTON;2001;02:09:43;7783
BOSTON;2002;02:09:02;7742
BOSTON;2003;02:10:11;7811
BOSTON;2004;02:10:37;7837
BOSTON;2005;02:11:44;7904
BOSTON;2006;02:07:14;7634
BOSTON;2007;02:14:13;8053
BOSTON;2008;02:07:45;7665
BOSTON;2009;02:08:42;7722
BOSTON;2010;02:05:52;7552
BOSTON;2011;02:03:02;7382
CHICAGO;1977;02:17:52;8272
CHICAGO;1978;02:19:20;8360
CHICAGO;1979;02:23:20;8600
CHICAGO;1980;02:14:04;8044
CHICAGO;1981;02:16:13;8173
CHICAGO;1982;02:10:59;7859
CHICAGO;1983;02:09:44;7784
CHICAGO;1984;02:08:05;7685
CHICAGO;1985;02:07:13;7633
CHICAGO;1986;02:08:27;7707
CHICAGO;1988;02:08:57;7737
CHICAGO;1989;02:11:25;7885
CHICAGO;1990;02:09:41;7781
CHICAGO;1991;02:14:33;8073
CHICAGO;1992;02:16:14;8174
CHICAGO;1993;02:13:14;7994
CHICAGO;1994;02:11:16;7876
CHICAGO;1995;02:11:18;7878
CHICAGO;1996;02:08:52;7732
CHICAGO;1997;02:07:10;7630
CHICAGO;1998;02:06:54;7614
CHICAGO;1999;02:05:42;7542
CHICAGO;2000;02:07:01;7621
CHICAGO;2001;02:08:52;7732
CHICAGO;2002;02:05:56;7556
CHICAGO;2003;02:05:50;7550
CHICAGO;2004;02:06:16;7576
CHICAGO;2005;02:07:02;7622
CHICAGO;2006;02:07:35;7655
CHICAGO;2007;02:11:11;7871
CHICAGO;2008;02:06:25;7585
CHICAGO;2009;02:05:41;7541
CHICAGO;2010;02:06:23;7583

des recherches¶

les éditions de 1971¶

In [17]:
# à vous de calculer les éditions de 1971

df_1971 = ...
In [18]:
# prune-cell

df_1971 = df[df.year == 1971]
In [19]:
# ceci doit retourner True

df_1971.shape == (3, 4) and df_1971.seconds.max() == 8574
Out[19]:
np.True_

l'édition de 1981 à Londres¶

In [20]:
# à vous

df_london_1981 = ...
In [21]:
# prune-cell

df_london_1981 = df[(df.city == 'LONDON') & (df.year == 1981)]
In [22]:
# ceci doit retourner True

df_london_1981.shape == (1, 4) and df_london_1981.iloc[0].seconds == 7908
Out[22]:
np.True_

trouver toutes les villes¶

On veut maintenant construire une collection de toutes les villes qui apparaissent au moins une fois dans la table.

In [23]:
# à vous

cities = ...
In [24]:
# prune-cell

cities = df.city.unique()
cities
Out[24]:
<StringArray>
[    'PARIS',    'BERLIN',   'FUKUOKA', 'AMSTERDAM',    'LONDON',  'NEW YORK',
  'STOCKOLM',    'BOSTON',   'CHICAGO']
Length: 9, dtype: str

Intéressez-vous au type du résultat : est-ce une dataframe, une series, un ndarray, une liste ?

In [25]:
# prune-cell

# attention, la réponse dépend de la version de pandas :
# jusqu'à pandas 2, `unique()` sur une colonne texte renvoyait un ndarray ;
# avec pandas 3 (dtype `str`), c'est un StringArray — dans les deux cas,
# ni une Series ni une DataFrame, et on peut faire len(...) dessus.
import numpy as np

type(cities).__name__, isinstance(cities, np.ndarray)
Out[25]:
('StringArray', False)

des extraits¶

Attention, dans les consignes qui suivent, les numéros de ligne commencent à 1.

extrait #1¶

Extrayez les entrées correspondant aux lignes 10 à 12 incluses.

In [26]:
# à vous

df_10_to_12 = ...
In [27]:
# prune-cell

df_10_to_12 = df.iloc[9:12]
df_10_to_12
Out[27]:
city year duration seconds
9 PARIS 2002 02:08:18 7698
10 PARIS 2001 02:09:40 7780
11 PARIS 2000 02:08:49 7729
In [28]:
# ceci doit retourner True

df_10_to_12.shape == (3, 4) and df_10_to_12.iloc[0].year == 2002 and df_10_to_12.iloc[-1].year == 2000
Out[28]:
np.True_

extrait #2¶

Construisez une Series correspondant aux événements à Paris après 2000 (inclus), dans laquelle on ne garde que l'année.

In [29]:
# à vous
s_paris_2000 = ...
In [30]:
# prune-cell

# pour bien décortiquer, en deux temps

df_paris_2000 = df[(df.city == 'PARIS') & (df.year >= 2000)]

# plusieurs possibilités
#paris_2000['year']
#paris_2000.loc[:, 'year']
# le plus simple ici est le mieux
s_paris_2000 = df_paris_2000.year
In [31]:
s_paris_2000
Out[31]:
0     2011
1     2010
2     2009
3     2008
4     2007
5     2006
6     2005
7     2004
8     2003
9     2002
10    2001
11    2000
Name: year, dtype: int64
In [32]:
# ceci doit retourner True

isinstance(s_paris_2000, pd.Series) and len(s_paris_2000) == 12 and s_paris_2000.iloc[-1] == 2000
Out[32]:
np.True_

extrait #3¶

Construisez maintenant une DataFrame correspondant aux mêmes événements — Paris après 2000 — dans laquelle on ne garde que les deux colonnes year et seconds.

In [33]:
df_paris_2000_ys = ...
In [34]:
# prune-cell

# on peut repartir de paris_2000
df_paris_2000_ys = df_paris_2000[['year', 'seconds']]
In [35]:
# ceci doit retourner True

(isinstance(df_paris_2000_ys, pd.DataFrame)
 and df_paris_2000_ys.shape == (12, 2) 
 and df_paris_2000_ys.iloc[-2].seconds == 7780)
Out[35]:
np.True_

agrégats¶

moyenne¶

Pour commencer, que vaudrait la moyenne de la colonne seconds ?

In [36]:
# calculer la moyenne de la colonne 'seconds'

seconds_average = ...
In [37]:
# prune-cell

# more on this later

seconds_average = df.seconds.mean()
In [38]:
# pour vérifier

import math
math.isclose(seconds_average, 7933.660167130919)
Out[38]:
True
{admonition}
:class: note

`df.groupby("colonne")` découpe la table en groupes — un par valeur distincte de la colonne — puis applique une opération à chaque groupe : `df.groupby("city").size()` compte les lignes de chaque ville, `df.groupby("city")["seconds"].mean()` en donne la durée moyenne. Le résultat est une `Series` indexée par les valeurs du critère. On étudiera `groupby` en détail en séance 4.

combien de marathons par an¶

Je veux maintenant produire une série qui compte, année par année, combien de marathons ont été courus.

Il y a plusieurs façons d'y arriver ; si vous en voyez plus d'une, n'hésitez pas à toutes les essayer !

In [39]:
# à vous

count_by_year = ...
In [40]:
# prune-cell

count_by_year = df.groupby(by='year').size()
In [41]:
# prune-cell

# toutes les colonnes vont contenir les mêmes infos, on peut en prendre une au hasard
count_by_year = df.groupby(by='year').count().city
In [42]:
# prune-cell 

# il y a plein de variantes, on peut inverser le count() et le city
count_by_year = df.groupby(by='year').city.count()
In [43]:
# prune-cell

# on peut appeler .agg('count')
count_by_year = df.groupby(by='year').agg('count')['city']
In [44]:
# pour vérifier

(isinstance(count_by_year, pd.Series)
 and len(count_by_year) == 65
 and count_by_year.loc[1947] == 1
 and count_by_year.loc[2007] == 9
 and count_by_year.loc[2011] == 5)
Out[44]:
np.True_

les durées¶

Dans cette partie, notre but est simplement de vérifier que la colonne seconds contient bien le nombre de secondes correspondant à la colonne duration.

Pour ce faire, nous allons commencer par convertir la colonne duration en quelque chose d'un peu plus utilisable. En effet, numpy expose deux types particulièrement bien adaptés à la gestion du temps :

  • datetime64 pour modéliser un instant particulier ;
  • timedelta64 pour modéliser une durée entre deux instants.

Au besoin, vous trouverez plus de détails ici : https://numpy.org/doc/stable/reference/arrays.datetime.html

read_csv(parse_dates=)¶

Commençons par écarter une "fausse bonne idée" : read_csv propose une option parse_dates. Regardez ce que cela donne ici :

In [45]:
df_broken = pd.read_csv(
    DATA, sep='\t', 
    names=['city', 'year', 'duration', 'seconds'], 
    parse_dates=['duration'])
df_broken
/tmp/ipykernel_2511026/3523436968.py:1: UserWarning: Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format.
  df_broken = pd.read_csv(
Out[45]:
city year duration seconds
0 PARIS 2011 2026-10-05 02:06:29 7589
1 PARIS 2010 2026-10-05 02:06:41 7601
2 PARIS 2009 2026-10-05 02:05:47 7547
3 PARIS 2008 2026-10-05 02:06:40 7600
4 PARIS 2007 2026-10-05 02:07:17 7637
... ... ... ... ...
354 CHICAGO 2006 2026-10-05 02:07:35 7655
355 CHICAGO 2007 2026-10-05 02:11:11 7871
356 CHICAGO 2008 2026-10-05 02:06:25 7585
357 CHICAGO 2009 2026-10-05 02:05:41 7541
358 CHICAGO 2010 2026-10-05 02:06:23 7583

359 rows × 4 columns

Ça ne va pas du tout ! Pouvez-vous deviner pourquoi ? Le souci, c'est qu'ici nous n'avons pas une date : ce que nous avons, c'est une durée — et ce n'est pas du tout la même chose.

{admonition}
:class: note

`pd.to_timedelta(serie)` convertit une colonne de textes comme `"2:06:29"` en vraies durées (type `timedelta64`), sur lesquelles additions, soustractions et comparaisons ont un sens. Une fois la colonne convertie, l'accesseur `.dt` donne accès aux méthodes propres aux durées, exactement comme `.str` pour les chaînes de caractères : `serie.dt.total_seconds()`. Les dates et les durées sont le sujet de la séance 5.

pd.to_timedelta()¶

In [46]:
# repartons des données de départ

df = pd.read_csv(DATA, sep="\t", names=NAMES)

df.dtypes
Out[46]:
city          str
year        int64
duration      str
seconds     int64
dtype: object

Pour convertir proprement la colonne, nous allons plutôt utiliser pd.to_timedelta().

Voyez la documentation de cette fonction, puis modifiez la dataframe df pour que la colonne duration soit désormais du type timedelta64.

In [47]:
# à vous
In [48]:
# prune-cell

df['duration'] = pd.to_timedelta(df.duration)
In [49]:
# pour vérifier - doit retourner True

df.duration.dtype.kind == 'm'   # timedelta64 (ns ou us selon la version de pandas)
Out[49]:
True
In [50]:
# et maintenant ça devrait être beaucoup mieux

df.head(2)
Out[50]:
city year duration seconds
0 PARIS 2011 0 days 02:06:29 7589
1 PARIS 2010 0 days 02:06:41 7601
In [51]:
# une fois que vous avez bien converti vous pourrez faire ceci
# df.duration.dt.components
In [52]:
# prune-cell
df.duration.dt.components
Out[52]:
days hours minutes seconds milliseconds microseconds nanoseconds
0 0 2 6 29 0 0 0
1 0 2 6 41 0 0 0
2 0 2 5 47 0 0 0
3 0 2 6 40 0 0 0
4 0 2 7 17 0 0 0
... ... ... ... ... ... ... ...
354 0 2 7 35 0 0 0
355 0 2 11 11 0 0 0
356 0 2 6 25 0 0 0
357 0 2 5 41 0 0 0
358 0 2 6 23 0 0 0

359 rows × 7 columns

duration == seconds ?¶

À présent que duration est dans le bon type, nous pouvons utiliser toutes les fonctions disponibles sur ce type. À l'usage, cela se fait en deux temps :

  • sur l'objet Series, on applique l'accesseur .dt pour, en quelque sorte, se projeter dans l'espace des durées ; c'est exactement le même mécanisme que le .str que nous avons utilisé pour appliquer des méthodes comme .lower() ou .replace() aux chaînes, et non à la série (plus de détails ici : https://pandas.pydata.org/docs/reference/api/pandas.Series.dt.html)
  • de là, on peut appeler toutes les méthodes disponibles sur les objets timedelta ; intéressez-vous en particulier à total_seconds.

Ainsi, pour vérifier que la colonne seconds correspond bien à duration, quel code écririez-vous ? (il doit afficher True)

In [53]:
# à vous
In [54]:
# prune-cell 

# il faut convertir en int parce que total_seconds retourne des flottants
total_seconds = df.duration.dt.total_seconds().astype(int)

np.all(total_seconds == df.seconds)
Out[54]:
np.True_

colonnes hour minute et second¶

Nous nous proposons maintenant d'ajouter des colonnes hour, minute et second, qui doivent être de type entier. Pour ce faire, deux approches :

  • "à la main" : on fait les calculs nous-mêmes ;
  • puis la version "paresseuse" : on découvre au détour d'une question StackOverflow que l'information est disponible directement dans la colonne duration — mais elle est bien cachée !

à la main¶

Un indice : on peut calculer le quotient et le reste entre deux objets de type "durée" avec les opérateurs usuels // et %, comme illustré ci-dessous.

In [55]:
# par exemple
import numpy as np

# une durée de 1h
one_hour = np.timedelta64(1, 'h')
# guess what...
one_minute = np.timedelta64(1, 'm')
one_second = np.timedelta64(1, 's')

# une durée de 2h25
random_duration = 2*one_hour + np.timedelta64(25, 'm')


# eh bien on peut faire comme avec des entiers

quotient, reste = random_duration // one_hour, random_duration % one_hour

quotient, reste
Out[55]:
(np.int64(2), np.timedelta64(25,'m'))

Maintenant que nous savons faire tout cela, à vous de calculer les colonnes hour, minute et second.

In [56]:
# à vous
In [57]:
# prune-cell

df['hour'] = df.duration // one_hour
df['minute'] = df.duration % one_hour // one_minute
df['second'] = df.duration % one_minute // one_second

df.head()
Out[57]:
city year duration seconds hour minute second
0 PARIS 2011 0 days 02:06:29 7589 2 6 29
1 PARIS 2010 0 days 02:06:41 7601 2 6 41
2 PARIS 2009 0 days 02:05:47 7547 2 5 47
3 PARIS 2008 0 days 02:06:40 7600 2 6 40
4 PARIS 2007 0 days 02:07:17 7637 2 7 17
In [58]:
# prune-cell
# pour vérifier, vous décommentez tout ceci et ça doit afficher True
(    np.all(df.loc[0, ['hour', 'minute', 'second']] == [2, 6, 29])
 and df.hour.dtype == int
 and df.minute.dtype == int 
 and df.second.dtype == int)
Out[58]:
True
In [59]:
# pour vérifier, vous décommentez tout ceci et ça doit afficher True
# (    np.all(df.loc[0, ['hour', 'minute', 'second']] == [2, 6, 29])
#  and df.hour.dtype == int
#  and df.minute.dtype == int 
#  and df.second.dtype == int)

version paresseuse avec dt.components¶

Il se trouve qu'on peut faire le même travail sans s'embêter autant, une fois qu'on a découvert que l'accesseur .dt possède un attribut qui donne accès à ce genre de détails.

In [60]:
# on défait le travail de la section précédente, si nécessaire

for col in 'hour', 'minute', 'second':
    if col in df.columns:
        df.drop(columns=col, inplace=True)
In [61]:
# à vous
In [62]:
# prune-cell

# on pourrait aussi faire en 3 lignes
# df['hour'] = df.duration.dt.components.hours
# df['minute'] = df.duration.dt.components.minutes
# df['second'] = df.duration.dt.components.seconds

# mais c'est aussi un merge en fait, juste que les noms ne correspondent pas.
df = df.merge(
    df.duration.dt.components
    [['hours', 'minutes', 'seconds']]
    .rename(
        columns={'hours': 'hour', 'minutes': 'minute', 'seconds': 'second'}),
    left_index=True, right_index=True
)
In [63]:
# pour vérifier: même consigne
# (    np.all(df.loc[0, ['hour', 'minute', 'second']] == [2, 6, 29])
#  and df.hour.dtype == int
#  and df.minute.dtype == int 
#  and df.second.dtype == int)
In [64]:
# prune-cell 
# pour vérifier: même consigne
(    np.all(df.loc[0, ['hour', 'minute', 'second']] == [2, 6, 29])
 and df.hour.dtype == int
 and df.minute.dtype == int 
 and df.second.dtype == int)
Out[64]:
True

Bon courage à tous ! Pour toute question sur le sujet, vous pouvez me contacter par email : raphael@imagine-app.fr.

Au plaisir de vous lire,

votre responsable de cours, Raphael Prasquier (sujet conçu par Aurélien Noce)