les données covid#

pour travailler localement sur votre PC

commencez par télécharger les artefacts

import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

ce sujet vise à acquérir et mettre en forme les données du COVID pour pouvoir produire facilement des diagrammes comme celui-ci
comme vous le voyez on a choisi:

  • une liste de pays,

  • une liste de mesures - ici: deaths & confirmed,

  • et une plage de temps spécifique

les données de Johns Hopkins#

les données sur le corona virus sont publiées par le département Center for Systems Science and Engineering (CSSE), de l’Université Johns Hopkins

  • sur le dépôt github CSSEGISandData/COVID-19

  • dans un format brut, détaillé et touffu - un peu trop compliqué pour l’utiliser ici

# le repo github - si vous êtes curieux

official_url = "https://github.com/CSSEGISandData/COVID-19"

autre jeu de données intéressant#

un dépôt de seconde main pomber/covid19

  • consolide les données du CSSE en une unique source

  • mis à jour quotidiennement

  • le fichier timeseries.json est en format JSON (JavaScript Object Notation)

# le repo qui va vraiment vous servir, avec le fichier json

json_url = "https://pomber.github.io/covid19/timeseries.json"

le format json ?#

vous connaissez sans doute le csv:

  • un format de données très simple décrivant une table

  • les éléments séparés par un caractère (, ou ;…)

  • pouvant contenir des identificateurs, des chaînes de caractères et des nombres

json est un format de données bien plus structuré; avec lui on peut sauver les types suivants

  • nombre, str (attention, utiliser seulement le "), false, true, null

  • mais aussi les listes, et les objets (en Python: un dictionnaire, de type dict)


format json pour le covid#

revenons au covid
le fichier https://pomber.github.io/covid19/timeseries.json contient un objet dict dont

  • les clés sont les pays du monde

  • chaque valeur est une liste de dict
    chacun décrivant une mesure de covid avec les 4 clés: date, confirmed, deaths et recovered

{
  "Afghanistan": [
    {
      "date": "2020-1-22",
      "confirmed": 0,
      "deaths": 0,
      "recovered": 0
    },
    {
      "date": "2020-1-23",
      "confirmed": 0,
      "deaths": 0,
      "recovered": 0
    },
    {
      "date": "2020-1-24",
      "confirmed": 0,
      "deaths": 0,
      "recovered": 0
    }, ...

acquisition des données json#

avec pd.read_json#

on ne va pas faire comme ça ici, mais sachez que c’est la méthode la plus rapide (à écrire):

data = pd.read_json(json_url)

par contre ça peut être franchement long, surtout si votre connexion réseau n’est pas au top
c’est pourquoi on va voir aussi une autre méthode - que vous pouvez sauter si vous êtes pressés de voir le traitement des données per se

caching avec requests#

en utilisant la librairie requests on peut implémenter un caching pour nos données

caching ?

dans le cas présent le terme caching suggère que l’on sauverait le fichier sur disque après l’avoir download depuis Internet; de cette façon on n’attend qu’une seule fois la durée du download
par contre, c’est bien d’être malin et de, par exemple, considérer que les fichiers qui ont plus de 1 jour ne sont plus valides et qu’il faut retourner les chercher; mais bon, let’s keep it simple, on ne va pas aller jusque là…

# pensez à bien installer le module requests

import requests

json_url = "https://pomber.github.io/covid19/timeseries.json"
by_country = None
# mettez cette variable à True si vous avez une bonne connexion

good_connection = False
#good_connection = True
# le code UNIQUEMENT SI VOUS AVEZ UNE BONNE CONNEXION INTERNET

if good_connection:
    response = requests.get(json_url)
    print(response.ok)

    by_country = response.json()

    print(type(by_country))

else:
    print('pas bonne connexion - pas grave...')
pas bonne connexion - pas grave...

chargement avec la lib json#

si l’accès Internet n’est pas possible, sachez que nous exposons une copie des données
faite il y a quelque temps, dans le fichier data/covid-frozen.json

le module json de la librairie standard permet de lire des fichiers en format JSON;
on l’importe comme d’habitude avec

import json

après avoir ouvert un fichier en lecture, la fonction json.load lit le contenu dans un objet Python

json_file = 'data/covid-frozen.json'

with open(json_file) as f:
    by_country = json.load(f)

et on obtient une structure Python de dict et de list

by_country
-> {'Afghanistan': [
      {'date': '2020-1-22', 'confirmed': 0, 'deaths': 0, 'recovered': 0},
      {'date': '2020-1-23', 'confirmed': 0, 'deaths': 0, 'recovered': 0},
      {'date': '2020-1-24', 'confirmed': 0, 'deaths': 0, 'recovered': 0},
    ...
# le code

if by_country is not None:
    print('on utilise les données déjà chargées')
else:
    import json
    json_file = 'data/covid-frozen.json'

    with open(json_file) as f:
        by_country = json.load(f)

    print(type(by_country))
<class 'dict'>
# regardons un peu la première clé de ce dictionnaire

# les 4 premières clés
list(by_country.keys())[:4]
['Afghanistan', 'Albania', 'Algeria', 'Andorra']

une dataframe globale#

qui consolide les données covid monde

exercice (version avancé)#

il s’agit de construire une unique dataframe contenant toutes les données covid monde
à partir de l’objet python by_country

vous devez obtenir quelque chose comme cela

          date  confirmed  deaths  recovered      country
0    2020-1-22          0       0          0  Afghanistan
1    2020-1-23          0       0          0  Afghanistan
2    2020-1-24          0       0          0  Afghanistan
3    2020-1-25          0       0          0  Afghanistan
4    2020-1-26          0       0          0  Afghanistan
..         ...        ...     ...        ...          ...
???  2021-8-29     124437    4401          0     Zimbabwe
???  2021-8-30     124581    4416          0     Zimbabwe
???  2021-8-31     124773    4419          0     Zimbabwe
???   2021-9-1     124960    4438          0     Zimbabwe
???   2021-9-2     125118    4449          0     Zimbabwe

[115050 rows x 5 columns]

attention

  • les ??? peuvent être différents suivant ce que vous faites

  • 115050 dépend de la date à laquelle le fichier a été récupéré
    (et de combien de données étaient alors disponibles)

indications

  • les élèves avancés peuvent travailler sans indications supplémentaires

  • pour les autres élèves, on vous propose une méthode pas-à-pas

# votre code
# rangez votre résultat dans la variable global_df

# global_df = ...

exercice (méthode pas-à-pas)#

exercice (méthode pas-à-pas) de construction de la dataframe globale

nous allons commencer par créer les dataframes de 2 pays 'France' et 'Italy'
puis les concaténer en une unique dataframe globale
et ensuite généraliser à tous les pays

rappel l’objet Python by_country est un dict dont:

  • les clés keys() sont les noms des pays

  • les valeurs values() sont des séries temporelles (list) d’observations sur le covid

  • chaque observation est un objet exprimé sous la forme d’un dict
    avec 4 mesures indiquées par les attributs 'date', 'confirmed', 'deaths' et 'recovered'
    en 2021-8-31 au Zimbabwe on a 124773 cas confirmés, 4419 morts et 0 guéris

exo

  1. prenez la clé 'France'
    construisez la dataframe à partie de la valeur de cette clé
    (la liste des enregistrements temporels de cas de covid)

# votre code

exo

  1. quelles sont les colonnes de cette dataframe ?
    combien y-a-t-il d’entrées (de mesures différentes)

# votre code

exo

  1. vous remarquez que cette dataframe ne contient plus l’information sur le pays
    ajoutez à cette dataframe une colonne de nom 'country' contenant 'France' à chaque ligne

# votre code

exo

  1. faites de même avec la clé 'Italy'
    et utilisez la fonction pandas.concat pour concaténer les deux dataframes

# votre code

exo

  1. généralisez et construisez une dataframe avec tous les pays
    vous aurez sans doute besoin d’utiliser un for python

# votre code

index de la dataframe globale#

les index ne sont pas forcément uniques#

si vous avez appelé pd.concat() sans paramètre particulier, vous pouvez sans doute observer ceci:

# si on essaie d'accéder à la ligne d'index 0
# on remarque qu'en fait on obtient .. plein de lignes
global_df.loc[0]
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[15], line 3
      1 # si on essaie d'accéder à la ligne d'index 0
      2 # on remarque qu'en fait on obtient .. plein de lignes
----> 3 global_df.loc[0]

NameError: name 'global_df' is not defined

→ les index ne sont pas toujours uniques

ce qui s’est passé c’est que :
chacune de nos dataframe par pays a été construite à partir d’un index séquentiel
i.e. un RangeIndex qui commence à chaque fois à 0
et lors du concat on a conservé ces valeurs
ce qui crée une multitude de lignes indexées par 0 (un par pays)

c’est un trait de pandas
contrairement aux dictionnaires Python - où une clé est forcément unique
il est possible de dupliquer plusieurs entrées dans les index
ligne ou colonne - d’une dataframe

même si ça n’est en général pas souhaitable
c’est souvent commode de pouvoir le faire
pendant la phase de construction / mise au point de la dataframe
quitte à adopter par la suite un index plus approprié
(comme on va le faire bientôt)


les dates en pandas#

si les valeurs des dates sont de simples str - chaînes de caractères
vous ne pourrez pas leur appliquer de fonctionnalités spécifiques aux dates

la fonction pandas.to_datetime permet de transformer
une chaîne de caratères contenant une date en un objet de type date

d = pd.to_datetime('2020-12-22')

sur lequel vous pouvez appliquer des fonctions spécifiques aux dates

d.year # 2020
d.month # 12
d.day # 22
# le code
d = pd.to_datetime('2020-12-22')
print(d.year,  # 2020
      d.month, # 12
      d.day)   # 22
2020 12 22

les formats de dates en pandas#

sans indications précises, pandas a inféré le format de la date
ainsi '2020-1-2' sera-t-il compris comme le 2 janvier, et non le 1er février

il est beaucoup plus sûr de passer à pandas.to_datetime le format de vos dates

en utilisant '%Y' pour l’année, %m pour le mois et '%d' pour le jour
on exprime le format des dates dans une chaîne de caractères

'2020-1-2' avec le format '%Y-%m-%d' donnera le 2 janvier 2020
'2020-1-2' avec le format '%Y-%d-%m' donnera le 1 février 2020

pd.to_datetime('2020-1-2', format='%Y-%d-%m').day
    -> 1
# sans indication ça peut être ambigu

pd.to_datetime('2020-1-2').day
2
# c'est parfois nécessaire de bien préciser le format

pd.to_datetime('2020-1-2', format='%Y-%d-%m').day
1
# mais sinon c'est très flexible

pd.to_datetime('2021'), pd.to_datetime('aug 2021'), 
(Timestamp('2021-01-01 00:00:00'), Timestamp('2021-08-01 00:00:00'))
# .. très flexible

pd.to_datetime('15 july 2021'), pd.to_datetime('15 july 2021 08:00')
(Timestamp('2021-07-15 00:00:00'), Timestamp('2021-07-15 08:00:00'))

convertissons nos dates#

reprenons à partir de la dataframe globale

exo

  1. quel est le type des colonnes ?

# votre code

exo

  1. que pensez-vous du type de la 'date' ?
    pensez-vous que ce soit adapté pour trier ?
    même question pour calculer la durée entre 2 événements ?
    comment pourrait-on s’y prendre pour améliorer ça ?

# votre code

exo

  1. regardez la fonction pandas.to_datetime
    sachant que l’année s’écrit %Y, le mois %m et le jour %d
    écrivez le format qui décrirait une date comme '2020-1-22'

# votre code

exo

  1. créez une nouvelle Series déduite de la colonne date
    et qui utilise un type plus adapté aux calculs sur les dates quel est le type de la nouvelle colonne ?

# votre code

exo

  1. remplacez dans la dataframe globale la colonne date par la précédente
    (le mieux est sans doute de conserver le même nom, mais ce n’est pas indispensable)

# votre code

un index plus idoine#

à présent on va pouvoir choisir un index un peu plus adapté à nos données

exo

  1. nous avons vu la notion de MultiIndex
    quel serait d’aprés vous un bon choix pour indexer la dataframe globale ?

# votre réponse

exo

  1. voyez-vous un moyen d’utiliser pivot_table() pour construire une nouvelle
    dataframe qui contienne essentiellement les mêmes informations
    mais avec un multi-index qui soit pertinent dans le contexte
    variante on peut aussi utiliser set_index()
    pour aboutir au même résultat

rangez votre résultat dans une variable clean_df

# votre code

accéder via un MultiIndex#

exo

  1. extrayez de la dataframe la série des 3 mesures
    faites en France le 1er Janvier 2021

# votre code

exo

  1. (avancé - pas vu en cours)
    essayez de trouver/deviner comment extraire de cette dataframe toutes les données relatives à la France

# votre code

exo

  1. même question pour la France et l’Italie

# votre code

un exemple de slicing (très) avancé#

pour illustrer la puissance de pandas, et la pertinence de notre choix d’index
voyons comment utiliser du slicing (très très avancé)
pour extraire cette fois les données relatives à

  • deux pays au hasard - disons France et Italy

  • à la période 1er Juillet - 15 Août 2021 inclus

pour ça on va tirer profit de la structure de l’index
et aussi de la puissance du type datetime64

on va fabriquer :

  • countries: une liste de pays - c’est facile

  • time_slice: un slice sur le temps
    qui en temps normal pourrait s’écrire 'july 2021' : '15 august 2021'
    (bornes inclusives puisque .loc[])

  • un slice sur les colonnes
    mais au fait on les veut toutes, on peut utiliser :

l’idée serait ensuite d’écrire simplement

clean_df.loc [ (countries, time_slice), :]

tout ça fonctionne presque très bien,
sauf pour la création de time_slice qui, pour de sombres raisons de syntaxe,
ne peut pas se faire ici avec la notation start:stop
(parce que pas dans des [])
et du coup on utilise la fonction builtin slice() pour créer time_slice

# ce qui nous donne le code suivant
# plutôt subtil, mais vraiment puissant

### pour slicer sur les deux composantes de l'index

# NB: si on voulait tous les pays on pourrait faire
# countries = slice(None)
# qui est équivalent à utiliser ::
# sauf qu'à nouveau ce n'est pas possible syntaxiquement ici
countries = ['France', 'Italy']
time_slice = slice('july 2021', '15 aug 2021')

clean_df.loc[
    # les lignes: c'est un 2-index donc on peut passer 2 slices
    (countries, time_slice),
    # les colonnes: on les veut toutes
    :]
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[31], line 13
     10 countries = ['France', 'Italy']
     11 time_slice = slice('july 2021', '15 aug 2021')
---> 13 clean_df.loc[
     14     # les lignes: c'est un 2-index donc on peut passer 2 slices
     15     (countries, time_slice),
     16     # les colonnes: on les veut toutes
     17     :]

NameError: name 'clean_df' is not defined

dessinons#

plot d’une dataframe#

plutôt que d’utiliser directement la mécanique de matplotlib.pyplot (tendance à être fastidieux)
il est préférable d’utiliser les méthodes comme plot() mais directement sur la dataframe

la logique de df.plot() est de dessiner autant de courbes que de colonnes
et de plus pandas se charge de tous les labels! bref c’est recommandé, car plus rapide

# illustration

# 3 colonnes donc 3 courbes
# 4 lignes donc 4 points sur chaque courbe

df = pd.DataFrame(
    {'a': [0, 10, 20, 30], 'b': [5, 10, 15, 25], 'c': [30, 15, 5, 0]},
    index = ['early', 'before', 'now', 'predicted'],
)
df
a b c
early 0 5 30
before 10 10 15
now 20 15 5
predicted 30 25 0
# remarquez que pour toutes les courbes,
# c'est toujours l'index qui sert d'abscisse

df.plot();
../../_images/e2cbf919a49a8cef411ef0cd4d7dfbaaf3ccbe6b9c760253b7e2b5e47490f0be.png

sur un pays#

du coup on a souvent seulement besoin de mettre en forme les données pour
qu’elles puissent être directement plottées par cette logique simple

imaginons que dans notre cas on veuille comparer sur un graphique l’évolution de

  • 2 mesures : deaths, confirmed

  • entre 3 pays: France, Italy et Germany

il nous faut donc construire une dataframe qui a:

  • six colonnes - le produit cartésien des 2 mesures et 3 pays

  • et autant de lignes que de dates - indexé par les dates

mais avant de réfléchir à comment faire ça, commençons par le cas simple d’un seul pays, au moins pour valider l’idée générale

exo

  1. affichez sur un graphique les 3 mesures pour la France au cours du temps

# votre code

exo

  1. idem avec seulement 2 mesures deaths et confirmed

# votre code

plusieurs pays#

il nous reste maintenant à traiter le cas de plusieurs pays

exo

  1. extrayez les données pour les 2 mesures et les 3 pays (appelons là df3)

# votre code

exo

  1. essayez de plotter la dataframe (je vous signale le paramètre rot=45
    qu’on peut passer à df.plot() pour améliorer la lisibilité)
    qu’est ce qui ne va pas malgré cela ?

# votre code

mise en forme des données#

quelle est la forme de df3 ?
à ce stade vous devriez avoir 2 colonnes,
et en gros 3 fois plus de lignes que dans un pays

alors qu’on avait dit qu’on voulait 6 colonnes, et autant de lignes que dans un pays (autant que de jours de mesure)

pour obtenir cette forme (qui bien sûr contient toujours autant de données)
on veut faire un découpage qui ressemble à ceci

../../_images/unstacking1.png

df.unstack()#

c’est justement le propos de la méthode unstack() sur la dataframe, qui fonctionne en déplaçant un niveau d’index de l’index des lignes vers l’index des colonnes

dans notre cas précis nous avons
. en lignes un multi-index à deux niveaux country et date
. et en colonnes un index simple (un niveau) de 2 colonnes

et nous pourrions obtenir ce qu’on cherche si on pouvait
en quelque sorte “faire passer” le niveau d’index country
de la direction des lignes à celle des colonnes

comme le niveau d’index country est le premier
donc d’indice 0, on va appeller

df3.unstack(0)

et vous pouvez constater que nous avons à présent
. en lignes un seul niveau d’index - les dates
. en colonnes deux niveaux, les 2 mesures x les 3 pays

# le code du unstack

# df6 = df3.unstack(0)
# df6

ne reste qu’à plotter#

# que du coup il n'y a plus qu'à plotter
# 
# à vous

bonus#

les rapides peuvent écrire une fonction extract() qui prend en paramètres

  • les pays concernés

  • les mesures concernées

  • et en option pour les plus forts, les dates de début et de fin

et qui retourne une dataframe prête à être affichée comme on l’a fait plus haut