Evaluating models

Dans cette partie, on propose un exercice intéressant pour appréhender une fonctionnalité majeure de Dash, le partage de données entre callbacks. L’exercice consiste à changer l’icône de marqueur des vols dans l’application en avion et à faire en sorte que chaque avion soit correctement orienté en fonction de son cap de vol.

Comme nous avons pu le voir dans la première partie de ce sujet, l’API FlightRadar permet de récupérer pour chaque vol une variable heading qui contient le cap de vol en degrés. Nous pourrions utiliser cette variable pour donner une orientation aux icônes des marqueurs sur la carte, mais nous allons plutôt essayer de la recalculer au vol. D’ailleurs, ce n’est pas proposé dans l’exercice mais vous pouvez comparer les résultats de ce calcul avec la donnée brute disponible !

Astuce Exercice 4: Calcul en direct du cap des vols

Cet exercice est à réaliser dans le répertoire better_app, qui contient encore une fois deux fichiers main.py et utils.py.

  1. Comparer le fichier better_app/main.py avec la correction du fichier intermediate_app/main.py. Identifier les nouveaux éléments et rechercher à quoi ils correspondent dans la documentation Dash.
Cliquer pour voir la réponse

On a plusieurs nouveaux éléments. D’abord, la mise en page de l’application intègre 3 nouveaux composants dcc.Store. Ce composant permet de stocker de la donnée JSON dans le navigateur de l’utilisateur. On va s’en servir pour communiquer la position précédente des vols en activité à chaque callback.

L’attribut storage_type du composant détermine le type de stockage utilisé: - memory: les données sont effacées lors d’un rafraichissement de la page; - local: les données sont conservées après la sortie du navigateur; - session: les données sont effacées après la sortie du navigateur.

Le décorateur de callback a également changé:

@app.callback(
    [Output('map', 'children'), Output('memory', 'data')],
    [Input('interval-component', 'n_intervals')],
    [State('memory', 'data')]
)
  • Premièrement, le callback met maintenant à jour deux attributs, l’attribut children du dl.MapContainer mais aussi l’attribut data d’un dcc.Store. C’est cet attribut qui est utilisé pour transmettre des données entre callbacks. Notez bien la présence d’une liste dans le Output du callback, qui permet de changer plusieurs éléments en une seule fois;
  • Deuxièmement, dash.dependencies.State permet de transmettre des données sans déclencher de callback lorsque l’attribut indiqué est modifié. Ici, on transmet les données du dcc.Store 'memory'.

La fonction update_graph_live prend ainsi un argument supplémentaire en entrée, previous_data. Nous souhaitons ajouter aux dictionnaires des vols de la liste data une clé rotation_angle qui contient le cap de vol en degrés. Un bloc conditionnel a donc été ajouté pour:

  • Initialiser cette clé à 0 pour chaque vol (lorsque le dcc.Store a son attribut data vide, donc à l’ouverture de la page);
  • Calculer un nouveau cap à chaque callback à partir des données de position au temps \(t\) et au temps \(t-1\).
Enfin, la liste children renvoyée par la fonction update_graph_live est légèrement modifiée. Chaque marqueur a maintenant un icône spécifique récupéré grâce aux fonctions get_custom_icon et get_closest_round_angle.
  1. La fonction update_rotation_angles de better_app/utils.py prend en entrée la liste des dictionnaires de vols récupérer via l’API FlightRadar et ajoute à chaque dictionnaire une clé 'rotation_angle' associée au cap de vol de l’aéronef. En regardant cette fonction, on voit que pour chaque vol de la liste, on récupère les positions à \(t\) et \(t-1\) si elles existent (les cas particuliers sont gérés à part). Si elles diffèrent, on calcule un nouveau cap avec la fonction bearing_from_positions. Implémenter cette fonction.
Cliquer pour voir un indice Pour calculer un cap à partir de positions différentes à \(t\) et \(t-1\) en degrés, vous pouvez suivre la méthode décrite sur cette page, paragraphe Bearing.
Cliquer pour voir la réponse
def bearing_from_positions(
    longitude: float,
    latitude: float,
    previous_longitude: float,
    previous_latitude: float,
) -> float:
    """
    Compute bearing from two sets of coordinates, at
    t-1 and t. Bearing is measured clockwise from the north.

    Args:
        longitude (float): Longitude (in degrees).
        latitude (float): Latitude (in degrees).
        previous_longitude (float): Previous longitude (in degrees).
        previous_latitude (float): Previous latitude (in degrees).

    Returns:
        float: Bearing in degrees.
    """
    # Convert to radians
    lat1, lon1, lat2, lon2 = map(
        math.radians,
        [previous_latitude, previous_longitude, latitude, longitude]
    )
    # Compute the difference between the two longitudes
    dlon = lon2 - lon1
    # Compute the initial bearing
    y = math.sin(dlon) * math.cos(lat2)
    x = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dlon)
    bearing_rad = math.atan2(y, x)
    # Convert the bearing to degrees
    bearing_deg = math.degrees(bearing_rad)
    # Ensure the bearing is between 0 and 360 degrees
    bearing_deg = (bearing_deg + 360) % 360
    return bearing_deg
  1. La fonction get_custom_icon renvoie un icône d’avion orienté dans une certaine direction (voir le répertoire img du dépôt) en fonction de l’angle round_angle donné en entrée. Cette angle doit avoir une valeur multiple de 15 comprise entre 0 et 345. Implémenter la fonction get_closest_round_angle qui prend en entrée un angle en degrés (compris entre 0 et 360) et qui renvoie l’angle le plus proche parmi les valeurs autorisées pour get_custom_icon.
Cliquer pour voir la réponse
def get_closest_round_angle(angle: float) -> int:
    """
    Get closest round angle (multiple of 15 degrees)
    to the given angle.

    Args:
        angle (float): Given angle.

    Returns:
        int: Closest angle among the following values:
            0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150,
            165, 180, 195, 210, 225, 240, 255, 270,
            285, 300, 315, 330, 345.
    """
    round_angles = [
        0, 15, 30, 45, 60, 75, 90,
        105, 120, 135, 150, 165, 180,
        195, 210, 225, 240, 255, 270,
        285, 300, 315, 330, 345, 360,
    ]
    differences = np.array([
        angle - round_angle for round_angle in round_angles
    ])
    abs_differences = np.abs(differences)
    closest_angle_idx = abs_differences.argmin()
    closest_angle = round_angles[closest_angle_idx]
    if closest_angle == 360:
        return 0
    else:
        return closest_angle
  1. Exécuter l’application en lançant depuis la racine du projet dans un Terminal la commande
python better_app/main.py

Vous devriez observer une application similaire à celle présentée en page d’accueil du sujet !

Dans l’étape suivante, on propose d’améliorer une nouvelle fois l’application en autorisant l’utilisateur à appliquer des filtres différents sur les vols qu’il souhaite observer.