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.
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.
# 2024: le site original semble être *down*
# URL = "http://www.xavierdupre.fr/enseignement/complements/marathon.txt"
DATA = "data/marathon.txt"
# 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.
# votre cellule de code
# qu'on va faire descendre
# et raffiner au fur et à mesure
df0 = pd.read_csv(DATA)
df0.head()
| 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.
# pd.read_csv?
Pour commencer, je vous invite à préciser le séparateur :
# à vous de modifier cette première approche
df1 = pd.read_csv(DATA)
# prune-cell
df1 = pd.read_csv(DATA, sep='\t')
# pour vérifier, ceci doit afficher True
df1.shape == (358, 4) and df1.iloc[0, 0] == 'PARIS' and df1.columns[0] == 'PARIS'
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.
NAMES = ["city", "year", "duration", "seconds"]
# à vous de créer une donnée bien propre
df = ...
# 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)
# pour vérifier, ceci doit afficher True
df.shape == (359, 4) and df.iloc[0, 0] == 'PARIS' and df.columns[0] == 'city'
True
# ce qui maintenant nous donne ceci
df.head(2)
| 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.
# df.to_csv?
Par ex., je crée ici un fichier qu'on pourra relire sous Excel :
loop = "marathon-loop.csv"
df.to_csv(loop, sep=";", index=False)
# 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¶
# à vous de calculer les éditions de 1971
df_1971 = ...
# prune-cell
df_1971 = df[df.year == 1971]
# ceci doit retourner True
df_1971.shape == (3, 4) and df_1971.seconds.max() == 8574
np.True_
l'édition de 1981 à Londres¶
# à vous
df_london_1981 = ...
# prune-cell
df_london_1981 = df[(df.city == 'LONDON') & (df.year == 1981)]
# ceci doit retourner True
df_london_1981.shape == (1, 4) and df_london_1981.iloc[0].seconds == 7908
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.
# à vous
cities = ...
# prune-cell
cities = df.city.unique()
cities
<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 ?
# 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)
('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.
# à vous
df_10_to_12 = ...
# prune-cell
df_10_to_12 = df.iloc[9:12]
df_10_to_12
| 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 |
# 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
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.
# à vous
s_paris_2000 = ...
# 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
s_paris_2000
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
# ceci doit retourner True
isinstance(s_paris_2000, pd.Series) and len(s_paris_2000) == 12 and s_paris_2000.iloc[-1] == 2000
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.
df_paris_2000_ys = ...
# prune-cell
# on peut repartir de paris_2000
df_paris_2000_ys = df_paris_2000[['year', 'seconds']]
# 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)
np.True_
agrégats¶
moyenne¶
Pour commencer, que vaudrait la moyenne de la colonne seconds ?
# calculer la moyenne de la colonne 'seconds'
seconds_average = ...
# prune-cell
# more on this later
seconds_average = df.seconds.mean()
# pour vérifier
import math
math.isclose(seconds_average, 7933.660167130919)
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 !
# à vous
count_by_year = ...
# prune-cell
count_by_year = df.groupby(by='year').size()
# 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
# 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()
# prune-cell
# on peut appeler .agg('count')
count_by_year = df.groupby(by='year').agg('count')['city']
# 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)
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 :
datetime64pour modéliser un instant particulier ;timedelta64pour 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 :
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(
| 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()¶
# repartons des données de départ
df = pd.read_csv(DATA, sep="\t", names=NAMES)
df.dtypes
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.
# à vous
# prune-cell
df['duration'] = pd.to_timedelta(df.duration)
# pour vérifier - doit retourner True
df.duration.dtype.kind == 'm' # timedelta64 (ns ou us selon la version de pandas)
True
# et maintenant ça devrait être beaucoup mieux
df.head(2)
| city | year | duration | seconds | |
|---|---|---|---|---|
| 0 | PARIS | 2011 | 0 days 02:06:29 | 7589 |
| 1 | PARIS | 2010 | 0 days 02:06:41 | 7601 |
# une fois que vous avez bien converti vous pourrez faire ceci
# df.duration.dt.components
# prune-cell
df.duration.dt.components
| 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.dtpour, en quelque sorte, se projeter dans l'espace des durées ; c'est exactement le même mécanisme que le.strque 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)
# à vous
# 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)
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.
# 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
(np.int64(2), np.timedelta64(25,'m'))
Maintenant que nous savons faire tout cela, à vous de calculer les colonnes hour, minute et second.
# à vous
# 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()
| 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 |
# 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)
True
# 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.
# 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)
# à vous
# 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
)
# 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)
# 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)
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)