Skip to content

Exercice 06 — Dashboard ESL ​

Partie 5 — Extensions fluentes + ToCsv() + WithFallback() + PairWith() + DSL

Concepts théoriques ​

Contexte ​

L'équipe présente ses stats à un tournoi ESL. Le data analyst prépare un rapport hebdomadaire : export CSV pour l'organisateur, gestion des données manquantes, comparaison kills vs assists côte à côte.

Un pipeline fluent lisible comme du langage naturel est l'objectif :

csharp
kdaLea
    .Smooth(windowSize: 3)
    .WithFallback(0.0, v => double.IsNaN(v))
    .PairWith(kdaDylan)
    .ToCsv("report.csv");

Avant de coder :

  • Normalize(), Smooth() (exercice 04) et Statistics() (exercice 05) étaient des fonctions static isolées dans un utilitaire provisoire MathHelpers, appelées explicitement (MathHelpers.Normalize(series)) faute de mécanisme pour les rattacher à DataSeries<double>. Quel mot-clé C# permet d'ajouter une méthode à un type existant sans modifier sa classe — et pourquoi DataSeries<T> (générique) en a besoin plutôt que d'écrire des méthodes d'instance ?
  • Quel contrat chaque méthode doit-elle respecter pour que le chaînage reste possible ?
Indice

Le contrat de la composition : chaque méthode retourne le même type qu'elle reçoit (ou un type compatible). C'est ce qui permet x.g().f() — l'équivalent C# de (f ∘ g)(x). → Le contrat de la composition


Étape 1 — ToCsv() dans DataSeriesExtensions.cs ​

Renommer le fichier provisoire de l'exercice 04 (DataSeries/MathHelpers.cs) en DataSeries/DataSeriesExtensions.cs, et sa classe MathHelpers en DataSeriesExtensions. Ajouter le mot-clé this devant le premier paramètre de Normalize, Smooth et Statistics (exercice 05) — elles deviennent enfin de vraies méthodes d'extension, appelables en series.Normalize(), series.Smooth(windowSize) et series.Statistics() plutôt que via MathHelpers.Normalize(series). Ajouter ToCsv à côté, directement comme méthode d'extension :

csharp
public static class DataSeriesExtensions
{
    public static void ToCsv(this DataSeries<double> series, string path)
    {
        // générer les lignes "date,valeur" depuis DataPoints et écrire dans le fichier
        // ...
    }
}
Voir la solution
csharp
public static void ToCsv(this DataSeries<double> series, string path)
{
    var lines = series.DataPoints.Select(dp => $"{dp.Timestamp:yyyy-MM-dd},{dp.Value:F4}");
    File.WriteAllLines(path, lines.Prepend("date,value"));
}

Chaque ligne porte la vraie date du match — le fichier est importable et triable dans Excel.


Étape 2 — WithFallback(fallback, isMissing) ​

WithFallback n'est qu'un Transform conditionnel :

csharp
public static DataSeries<T> WithFallback<T>(
    this DataSeries<T> series, T fallback, Func<T, bool> isMissing)
{
    // ...
}
Voir la solution
csharp
public static DataSeries<T> WithFallback<T>(
    this DataSeries<T> series, T fallback, Func<T, bool> isMissing)
    => series.Transform(v => isMissing(v) ? fallback : v);

Étape 3 — PairWith() — kills vs assists côte à côte ​

Zip (ici PairWith) combine deux listes élément par élément — pattern fondamental pour traiter des séries corrélées (kills + assists, température + humidité...). → Zip — combiner deux séquences en parallèle

csharp
public static DataSeries<(double Left, double Right)> PairWith(
    this DataSeries<double> left, DataSeries<double> right)
{
    // Hint : Zip combine deux séquences élément par élément
    // ...
}
Voir la solution
csharp
public static DataSeries<(double Left, double Right)> PairWith(
    this DataSeries<double> left, DataSeries<double> right)
    => DataSeries<(double, double)>.From(
        left.DataPoints.Zip(right.DataPoints, (l, r) =>
            new DataPoint<(double, double)>(l.Timestamp, (l.Value, r.Value)))
    );

Les timestamps de la série gauche sont préservés dans le résultat — le CSV exporté via ToCsv reste daté.

Comparer kills normalisés et assists normalisés de Léa :

csharp
var kills   = valorant.Filter(m => m.Player == "Léa").Transform(m => (double)m.Kills);
var assists = valorant.Filter(m => m.Player == "Léa").Transform(m => (double)m.Assists);

var report = kills.Normalize().PairWith(assists.Normalize());
foreach (var (k, a) in report.Values)
    Console.WriteLine($"kills={k:F2}  assists={a:F2}");

DSL final — rapport hebdomadaire en une expression ​

Les extensions définissent le vocabulaire du domaine. Un bon DSL se lit sans avoir besoin de connaître l'implémentation : → DSL

csharp
valorant
    .Filter(m => m.Player == "Léa")
    .Transform(m => (double)m.Kills)
    .Normalize()
    .Smooth(3)
    .WithFallback(0.0, v => double.IsNaN(v))
    .ToCsv("lea_kills_smoothed.csv");

Un pipeline est aussi une valeur. La préparation commune (lisser puis combler les trous) se répète pour chaque série du rapport — la stocker dans une variable :

csharp
// Préparation commune : lisser puis combler les trous
Func<DataSeries<double>, DataSeries<double>> prepare =
    s => s.Smooth(3).WithFallback(0.0, v => double.IsNaN(v));

// Réutilisé sur chaque série du rapport hebdomadaire
prepare(killsLea).ToCsv("lea_kills.csv");
prepare(assistsLea).ToCsv("lea_assists.csv");
prepare(kdaRaphael).ToCsv("raphael_kda.csv");

Composer deux pipelines stockés donne un nouveau pipeline — c'est exactement f ∘ g : → Composition explicite avec Func · Fonctions comme valeurs


Étape 4 — Interface CLI ​

Ajouter --export <fichier> pour déclencher l'export CSV depuis la ligne de commande.

Avant de coder : À quel endroit du pipeline appeler ToCsv — avant ou après Smooth ? Comment récupérer le nom du fichier cible depuis args ?

dotnet run -- --game valorant --player Léa --stat kda --export lea_kda.csv
Voir la solution
csharp
if (args.Contains("--export"))
{
    var file = args[Array.IndexOf(args, "--export") + 1];
    values.ToCsv(file);
    Console.WriteLine($"Exporté : {file}");
}

Vérification ​

  • ToCsv produit un fichier importable dans Excel
  • WithFallback sur des données propres ne modifie aucun élément
  • PairWith sur deux séries de longueur différente → Zip s'arrête à la plus courte
  • Le pipeline DSL compile et produit un résultat identique aux étapes intermédiaires