marathon (divers basique)#

à télécharger

pour réaliser ce TP localement sur votre ordi, commencez par télécharger le zip

un petit TP pour travailler

  • 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#

On va étudier un jeu de données trouvé sur Internet

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

Le premier réflexe pour charger un fichier de ce genre, c’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 ça ne marche pas franchement bien !

il faut donc bien regarder la doc

# pd.read_csv?

et pour commencer je vous invite à préciser le séparateur:

# à vous de modifier cette première approche

df1 = pd.read_csv(DATA)
# pour vérifier, ceci doit afficher True

df1.shape == (358, 4) and df1.iloc[0, 0] == 'PARIS' and df1.columns[0] == 'PARIS'
False

c’est mieux, mais les noms des colonnes ne sont pas corrects
en effet par défaut, read_csv utilise la première ligne pour déterminer les noms des colonnes
or dans le fichier texte il n’y a pas le nom des colonnes ! (voyez ci-dessus)

du coup ce serait pertinent de donner un nom aux colonnes

NAMES = ["city", "year", "duration", "seconds"]
# à vous de créer une donnée bien propre
df = ...
# pour vérifier, ceci doit afficher True

df.shape == (359, 4) and df.iloc[0, 0] == 'PARIS' and df.columns[0] == 'city'
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[10], line 3
      1 # pour vérifier, ceci doit afficher True
----> 3 df.shape == (359, 4) and df.iloc[0, 0] == 'PARIS' and df.columns[0] == 'city'

AttributeError: 'ellipsis' object has no attribute 'shape'
# ce qui maintenant nous donne ceci

df.head(2)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[11], line 3
      1 # ce qui maintenant nous donne ceci
----> 3 df.head(2)

AttributeError: 'ellipsis' object has no attribute 'head'

sauvegarde dans un fichier csv#

dans l’autre sens, quand on a produit une dataframe et qu’on veut sauver le résultat dans un fichier texte

# df.to_csv?

par exemple je crée ici un fichier qu’on peut relire sous excel

loop = "marathon-loop.csv"
df.to_csv(loop, sep=";", index=False)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[13], line 2
      1 loop = "marathon-loop.csv"
----> 2 df.to_csv(loop, sep=";", index=False)

AttributeError: 'ellipsis' object has no attribute 'to_csv'
# 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
cat: marathon-loop.csv: No such file or directory

des recherches#

les éditions de 1971#

# à vous de calculer les éditions de 1971

df_1971 = ...
# ceci doit retourner True

df_1971.shape == (3, 4) and df_1971.seconds.max() == 8574
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[16], line 3
      1 # ceci doit retourner True
----> 3 df_1971.shape == (3, 4) and df_1971.seconds.max() == 8574

AttributeError: 'ellipsis' object has no attribute 'shape'

l’édition de 1981 à Londres#

# à vous

df_london_1981 = ...
# ceci doit retourner True

df_london_1981.shape == (1, 4) and df_london_1981.iloc[0].seconds == 7908
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[18], line 3
      1 # ceci doit retourner True
----> 3 df_london_1981.shape == (1, 4) and df_london_1981.iloc[0].seconds == 7908

AttributeError: 'ellipsis' object has no attribute 'shape'

trouver toutes les villes#

on veut construire une collection de toutes les villes qui apparaissent au moins une fois

# à vous

cities = ...

intéressez-vous au type du résultat (dataframe, series, ndarray, liste ?)

des extraits#

attention ici dans les consignes, les numéros de ligne commencent à 1

extrait #1#

les entrées correspondant aux lignes 10 à 12 inclusivement

# à vous

df_10_to_12 = ...
# 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
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[21], line 3
      1 # ceci doit retourner True
----> 3 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

AttributeError: 'ellipsis' object has no attribute 'shape'

extrait #2#

une Series correspondant aux événements à Paris après 2000 (inclus), dans laquelle on n’a gardé que l’année

# à vous
s_paris_2000 = ...
s_paris_2000
Ellipsis
# ceci doit retourner True

isinstance(s_paris_2000, pd.Series) and len(s_paris_2000) == 12 and s_paris_2000.iloc[-1] == 2000
False

extrait #3#

une DataFrame correspondant aux événements à Paris après 2000, dans laquelle on n’a gardé que les deux colonnes year et seconds

df_paris_2000_ys = ...
# 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)
False

aggrégats#

moyenne#

ce serait quoi la moyenne de la colonne seconds ?

# calculer la moyenne de la colonne 'seconds'

seconds_average = ...
# pour vérifier

import math
math.isclose(seconds_average, 7933.660167130919)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[28], line 4
      1 # pour vérifier
      3 import math
----> 4 math.isclose(seconds_average, 7933.660167130919)

TypeError: must be real number, not ellipsis

combien de marathons par an#

si maintenant je veux produire une série qui compte par année combien il y a eu de marathons

il y a plein de façons de faire, si vous en voyez plusieurs n’hésitez pas…

# à vous

count_by_year = ...
# 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)
False

les durées#

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

pour cela on va commencer par convertir la colonne duration en quelque chose d’un peu plus utilisable

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

voir plus de détails si nécessaire ici: https://numpy.org/doc/stable/reference/arrays.datetime.html

read_csv(parse_dates=)#

commençons par écarter une fausse bonne idée

dans read_csv il y a une option parse_dates; mais regardez ce que ça donne

df_broken = pd.read_csv(
    DATA, sep='\t', 
    names=['city', 'year', 'duration', 'seconds'], 
    parse_dates=['duration'])
df_broken
/tmp/ipykernel_1709/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 2025-09-20 02:06:29 7589
1 PARIS 2010 2025-09-20 02:06:41 7601
2 PARIS 2009 2025-09-20 02:05:47 7547
3 PARIS 2008 2025-09-20 02:06:40 7600
4 PARIS 2007 2025-09-20 02:07:17 7637
... ... ... ... ...
354 CHICAGO 2006 2025-09-20 02:07:35 7655
355 CHICAGO 2007 2025-09-20 02:11:11 7871
356 CHICAGO 2008 2025-09-20 02:06:25 7585
357 CHICAGO 2009 2025-09-20 02:05:41 7541
358 CHICAGO 2010 2025-09-20 02:06:23 7583

359 rows × 4 columns

ça ne va pas !

le truc c’est que ici, on n’a pas une date, ce que nous avons c’est une durée

pd.to_timedelta()#

# repartons des données de départ

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

df.dtypes
city        object
year         int64
duration    object
seconds      int64
dtype: object

non, pour convertir la colonne en datetime64 on va utiliser pd.to_timedelta()

voyez la documentation de cette fonction, et modifiez la dataframe df pour que la colonne duration soit maintenant du type timedelta64

# à vous
# pour vérifier - doit retourner True

df.duration.dtype == 'timedelta64[ns]'
False
# et maintenant ça devrait être beaucoup mieux

df.head(2)
city year duration seconds
0 PARIS 2011 02:06:29 7589
1 PARIS 2010 02:06:41 7601
# une fois que vous avez bien converti vous pourrez faire ceci
# df.duration.dt.components

duration == seconds ?#

à présent qu’on a converti duration dans le bon type, on peut utiliser toutes les fonctions disponibles sur ce type.
en pratique ça se fait en deux temps

  • sur l’objet Series on applique l’attribut dt pour, en quelque sorte, se projeter dans l’espace des ‘date-time’
    c’est exactement comme on l’a vu déjà avec le .str lorsqu’on a eu besoin d’appliquer des méthodes comme .lower() ou replace() sur les chaines et non pas sur 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 - on pourra en particulier s’intéresser à total_seconds

du coup pour vérifier que la colonne seconds correspond bien à duration, on écrirait quoi comme code (qui doit afficher True)

# à vous

colonnes hour minute et second#

on se propose maintenant de rajouter des colonnes hour minute et second - qui doivent être de type entier

pour cela deux approches:

  • “à la main”: on fait les calculs nous-mêmes

  • après quoi on découvre par hasard dans une question SO que c’est disponible directement dans la colonne duration - mais c’est bien caché…

à la main#

indices

  • on peut calculer le quotient et le reste entre deux objets de type “durée” avec les opérateurs usuels // et %

# 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 qu’on sait faire tout ça, on peut calculer les colonnes hour, minute et second

# à vous
# 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 découvre 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
# 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)