Projet 2 - Prédictions météorologiques

Contexte du projet

Il y a certains jours où l’on serait bien resté en télétravail.. Parmi ceux-là, ces jours à la fois humides et venteux où il est impossible de maintenir une coiffure décente, malgré tous ses efforts. Pourrait-on utiliser Python pour prédire ce que les anglo-saxons nomment des bad hair day (“mauvais jour de cheveux”) ?

L’objectif du projet est de construire un bad hair index (“indice de mauvais jour de cheveux”) à partir des données météorologiques et de représenter graphiquement l’évolution de cette indice afin de déterminer à l’avance les jours où l’on ferait mieux de rester bien au chaud. Afin d’obtenir les données adéquates, nous allons requêter des APIs.

Une API (Interface de Programmation d’Application) est un ensemble de règles et de spécifications que les applications suivent pour communiquer entre elles. Elle permet à votre code d’accéder à des fonctionnalités externes ou à des données, comme celles de bases de données météorologiques ou de services de localisation. Lorsqu’on parle de requêtage d’une API, cela se fait généralement via le protocole HTTP, qui est le même protocole utilisé pour charger des pages web. Dans ce tutoriel, nous utiliserons le package requests, qui simplifie le processus de requêtage et de gestion de réponses HTTP.

Les APIs que nous allons utiliser sont :

  • Nominatim : une API de géocodage proposée par OpenStreetMap qui nous permet de convertir un nom de lieu en coordonnées géographiques.
  • Open-Meteo Weather Forecast : une API qui fournit des prévisions météorologiques détaillées.

Commençons par importer les packages dont nous aurons besoin au cours de ce projet.

import requests
import pandas
import seaborn as sns
import matplotlib.pyplot as plt

import solutions

Partie 1 : récupération des coordonnées géographiques pour une localisation donnée

L’API de prédiction d’open-meteo prend en entrée les coordonnées géographiques (latitude, longitude) du lieu où seront réalisées les prédictions. On pourrait récupérer manuellement les coordonnées du lieu qui nous intéresse, mais cela limiterait la reproductibilité de nos analyses avec d’autres lieux que celui choisi. On va donc utiliser une seconde API, Nominatim, pour obtenir ces coordonnées pour un lieu donné.

Lorsque l’on travaille à partir d’une API, la première étape est toujours de lire sa documentation. C’est elle qui indique à quelle adresse nous devons envoyer nos requêtes, sous quel format, et ce que va nous répondre l’API. Dans notre cas, la docuemntation de Nominatim se trouve à cette adresse. N’hésitez pas à la parcourir rapidement pour évaluer les possibilités de l’API.

Question 1

La première caractéristique essentielle d’une API est le endpoint, c’est à dire l’URL à laquelle on va envoyer des requêtes. Dans notre cas, on va utiliser le endpoint /search dans la mesure où l’on veut trouver un objet géographique (des coordonnées) à partir d’un nom de localisation. La page de documentation associée à ce endpoint nous donne toutes les informations dont nous avons besoin :

  • le format d’une requête est https://nominatim.openstreetmap.org/search?<params><params> doit être remplacé par les paramètres de la requête, séparés par le symbole &
  • dans la section Structured Query, on voit que l’API admet comme paramètres country (pays) et city (ville), que l’on va utiliser pour paramétrer notre requête.

Définissez une fonction build_request_nominatim qui construit le lien de la requête pour un pays et une ville donnée.

Résultat attendu

url_request_nominatim = solutions.build_request_nominatim("France", "Montrouge")
url_request_nominatim

À vous de jouer !

def build_request_nominatim(country, city):
    # Votre code ici
    return url_request
# Vérification du résultat
url_request_nominatim = build_request_nominatim("France", "Montrouge")
url_request_nominatim

Question 2

La prochaine étape est d’envoyer notre requête paramétrisée à l’API. Pour la tester au préalable, on peut simplement mettre l’adresse dans un navigateur et voir ce que nous renvoie l’API. Si les résultats ont l’air cohérent, on peut continuer. Si l’API nous renvoie un code d’erreur, il y a sûrement une erreur à trouver dans la requête.

Pour effectuer cette requête à partir de Python afin d’en récupérer les résultats, on utilise la fonction requests.get() à laquelle on fournit comme seul paramètre l’URL de la requête. On obtient en retour un objet “réponse”, dont on peut extraire le contenu JSON sous forme d’un dictionnaire Python en lui appliquant la méthode .json(). Il faut alors parcourir le dictionnaire pour en extraire les informations pertinentes ; dans notre cas : la latitude et la longitude.

Définissez une fonction get_lat_long qui récupère la latitude et la longitude (centrale) pour un pays et une ville donnée.

Résultat attendu

lat, long = solutions.get_lat_long(query=url_request_nominatim)
print(lat, long)
print(type(lat))
print(type(long))

À vous de jouer !

def get_lat_long(query):
    # Votre code ici
    return latitude, longitude
# Vérification du résultat
lat, long = get_lat_long(query=url_request_nominatim)
print(lat, long)
print(type(lat))
print(type(long))

Partie 2 : récupération des prévisions météorologiques

Maintenant que nous pouvons récupérer les coordonnées associées à une localisation donnée, nous pouvons requêter l’API open-meteo.com pour obtenir les données de prédiction météo associées à ces coordonnées. Là encore, la première étape est de s’intéresser à la documentation (page d’accueil, doc), qui nous fournit plusieurs informations :

  • le endpoint pour l’API de prédiction est https://api.open-meteo.com/v1/forecast
  • l’API attend en entrée une latitude et une latitude, ainsi que les variables météorologiques souhaitées. Pour notre problématique, nous allons récupérer des informations sur le taux d’humidité (relativehumidity_2m) et la vitesse du vent (windspeed_10m)
  • par défaut, l’API renvoie des prédictions à 7 jours

Question 3

Sachant toutes ces informations et en vous aidant de la documentation, définissez une fonction build_request_open_meteo qui construit le lien de la requête pour une latitude et une longitude donnée. Là encore, il est possible de tester la validité de la requête en exécutant le lien dans un navigateur et en vérifiant que les résultats retournés paraissent cohérents.

Résultat attendu

url_request_open_meteo = solutions.build_request_open_meteo(latitude=lat, longitude=long)
url_request_open_meteo

À vous de jouer !

def build_request_open_meteo(latitude, longitude):
    # Votre code ici
    return url_request
# Vérification du résultat
url_request_open_meteo = build_request_open_meteo(latitude=lat, longitude=long)
url_request_open_meteo

Question 4

A nouveau, on utilise la fonction requests.get() pour soumettre la requête à l’API. On obtient en retour un objet “réponse”, dont on peut extraire le contenu JSON sous forme d’un dictionnaire Python en lui appliquant la méthode .json().

Mais que se passe-t-il dans le cas où la requête soumise est invalide (faute de frappe, paramètres inexistants, etc.) ? Dans ce cas, l’API nous renvoie une erreur. L’objet réponse de la requête contient un attribut .status_code qui donne le code de réponse d’une requête. Le code 200 indique la réussite d’une requête ; tout autre code indique une erreur.

Définissez une fonction get_meteo_data qui récupère le dictionnaire complet de données retourné par l’API suite à notre requête. Le comportement de la fonction doit cependant dépendre du code de réponse de la requête :

  • si le code vaut 200, la fonction renvoie le dictionnaire des prédictions ;
  • si le code est différent de 200, la fonction affiche le code d’erreur et renvoie None.

Résultat attendu

predictions = solutions.get_meteo_data(url_request_open_meteo)
type(predictions)
wrong_request = solutions.build_request_open_meteo(latitude=lat, longitude="dix-sept-virgule-quatre")
output = solutions.get_meteo_data(wrong_request)
print(output)

À vous de jouer !

def get_meteo_data(query):
    # Votre code ici
    return response.json()
# Vérification du résultat
predictions = get_meteo_data(url_request_open_meteo)
type(predictions)
# Vérification du résultat
wrong_request = build_request_open_meteo(latitude=lat, longitude="dix-sept-virgule-quatre")
output = get_meteo_data(wrong_request)
print(output)

Question 5

Afin de bien comprendre la structure des données que nous avons récupérées, explorez le dictionnaire des prédictions retourné par l’API (clefs, différents niveaux, format des prédictions, format de la variable indiquant les dates/heures des prédictions, etc.)

Afficher le code
# Exploration des données
print(type(predictions))
print(predictions.keys())
print(type(predictions["hourly"]))
print(predictions["hourly"].keys())
print(type(predictions["hourly"]["time"]))
print()

# Afficher les données
print(predictions['hourly']["time"][:5])
print(predictions['hourly']["time"][-5:])
print()
print(predictions['hourly']["relativehumidity_2m"][:5])
print(predictions['hourly']["windspeed_10m"][:5])

Partie 3 : construction et visualisation d’un bad hair index

L’objectif de cette dernière partie est de calculer et représenter graphiquement le bad hair index. Rappelons que l’on définit cet indice comme le produit de l’humidité relative et de la vitesse du vent. Il s’agit d’une mesure ludique de la probabilité d’avoir une “mauvaise coiffure” en raison des conditions météorologiques.

Question 6

Définissez une fonction preprocess_predictions qui met en forme les prédictions issues de l’API sous forme d’un DataFrame Pandas en vue d’une analyse statistique. Les étapes à implémenter sont les suivantes :

  1. convertir les données prédites en un DataFrame Pandas à 3 colonnes (date et heure de l’observation, humidité, vitesse du vent) ;
  2. convertir la colonne de temps au format datetime (documentation)
  3. ajouter deux nouvelles variables indiquant le jour de l’observation et l’heure de l’observation
  4. ajouter une variable qui calcule le bad hair index

Résultat attendu

df_preds = solutions.preprocess_predictions(predictions)
df_preds.head()

À vous de jouer !

def preprocess_predictions(predictions):
    # Votre code ici
    return df
# Vérification du résultat
df_preds = preprocess_predictions(predictions)
df_preds.head()

Question 7

A des fins de représentation graphique, nous allons représenter le bad hair index agrégé à deux niveaux :

  • moyenne heure par heure. Cela permettra de répondre à la question : “à quel heure sera-t-il généralement préférable de rester à la maison la semaine prochaine ?”
  • moyenne jour par jour. Cela permettra de répondre à la question : “quel jour sera-t-il généralement préférable de rester à la maison la semaine prochaine ?”

Définissez une fonction plot_agg_avg_bhi qui calcule l’indice agrégé dans chaque cas, et représente le résultat sous la forme d’un lineplot.

Résultat attendu

solutions.plot_agg_avg_bhi(df_preds, agg_var="day")
solutions.plot_agg_avg_bhi(df_preds, agg_var="hour")

À vous de jouer !

def plot_agg_avg_bhi(df_preds, agg_var="day"):
    # Votre code ici
    return None
# Vérification du résultat
plot_agg_avg_bhi(df_preds, agg_var="day")
# Vérification du résultat
plot_agg_avg_bhi(df_preds, agg_var="hour")

Qu’en concluez-vous pour la semaine à venir ?

Question 8

Notre outil de prévision des bad hair days fonctionne à merveille. Mais c’est bientôt les vacances, et un voyage à Berlin est prévu. Idéalement, on voudrait pouvoir utiliser notre outil pour n’importe quelle localité. Heureusement, on a défini à chaque étape des fonctions, ce qui va nous permettre de passer facilement à une fonction “chef d’orchestre” qui appelle toutes les autres pour une localité donnée.

Définissez une fonction main qui représente le bad hair index pour un pays, une ville et un niveau d’agrégation donnés.

Résultat attendu

solutions.main(country="Germany", city="Berlin", agg_var="day")
solutions.main(country="Germany", city="Berlin", agg_var="hour")

À vous de jouer !

def main(country, city, agg_var="day"):
    # Votre code ici
    return None
# Vérification du résultat
main(country="Germany", city="Berlin", agg_var="day")
# Vérification du résultat
main(country="Germany", city="Berlin", agg_var="hour")