Exercice 05 — Classement de saison
Partie 4 —
.Fold()+.Statistics()+.SlidingWindow()
Concepts théoriques
Contexte
Établir le classement officiel des 5 joueurs de Team Helvetia pour la saison. Le coaching staff veut savoir : qui est le plus régulier ? qui progresse le plus vite ?
.Fold() est l'outil fondamental qui permet de répondre à toutes ces questions en une seule abstraction.
Concept FP : Fold — l'agrégation universelle
Sum, Count, Max, Any, All sont tous des cas particuliers de Fold. Implémenter Fold une seule fois suffit à exprimer n'importe quelle agrégation.
[a, b, c, d] avec seed s et f :
s → f(s, a) → f(f(s,a), b) → f(f(f(s,a),b), c) → résultat final→ Théorie : Reduce / Aggregate · Fold — l'agrégation universelle
Étape 1 — Implémenter .Fold()
Avant de coder : quelle méthode LINQ fait exactement ce que décrit le schéma ci-dessus ?
Indice
Aggregate(seed, combiner) — c'est le Fold de LINQ. La méthode de la bibliothèque n'a qu'à déléguer à Aggregate.
public double Fold(double seed, Func<double, double, double> combiner)
{
// ...
}Voir la solution
public double Fold(double seed, Func<double, double, double> combiner)
=> _data.Aggregate(seed, (acc, d) => combiner(acc, d.Value));_data stocke des (DateTime, double) — le combinateur ne travaille que sur les doubles.
Différence avec esport :
DataSeries<T>.Fold<TResult>est générique sur le type du résultat :TResult Fold<TResult>(TResult seed, Func<TResult, T, TResult> combiner). Cela permet, par exemple, d'accumuler dans unstringou un objet personnalisé. Ici, le seed et le résultat sont toujours desdouble— plus simple, mais moins flexible.
Réécrire les agrégations classiques avec Fold sur les KDA de Léa :
var kdaValues = kdaLea; // StatSeries
var sum = kdaValues.Fold(0.0, (acc, val) => acc + val);
var count = kdaValues.Fold(0.0, (acc, _) => acc + 1);
var best = kdaValues.Fold(double.MinValue, (acc, val) => val > acc ? val : acc);
var mean = sum / count;
Console.WriteLine($"KDA moyen de Léa : {mean:F2}");
Console.WriteLine($"KDA max de Léa : {best:F2}");Reproduire pour les 4 autres joueurs et afficher le classement.
Étape 2 — .SlidingWindow(size) — progression mensuelle
Avant de coder : une fenêtre glissante de taille 5 à partir d'une liste de 13 éléments produit combien de fenêtres ? Quelle formule générale ?
Indice
count - size + 1 fenêtres. Pour 13 éléments avec taille 5 : 13 - 5 + 1 = 9 fenêtres.
public IEnumerable<StatSeries> SlidingWindow(int size)
{
var points = _data.ToList();
return Enumerable.Range(0, Math.Max(0, points.Count - size + 1))
.Select(i => // extraire une fenêtre de `size` éléments à partir de l'indice i
);
}Voir la solution
public IEnumerable<StatSeries> SlidingWindow(int size)
{
var points = _data.ToList();
return Enumerable.Range(0, Math.Max(0, points.Count - size + 1))
.Select(i => new StatSeries(points.Skip(i).Take(size)));
}Calculer la moyenne KDA par fenêtre de 5 matchs pour Léa :
var progression = kdaLea
.SlidingWindow(5)
.Select(window => window.Fold(0.0, (acc, v) => acc + v) / 5);
Console.WriteLine("Progression KDA Léa (fenêtres de 5 matchs) :");
foreach (var avg in progression)
Console.WriteLine($" {avg:F2}");Étape 3 — .Statistics() — qui est le plus régulier ?
public class SeriesStats
{
public double Min { get; }
public double Max { get; }
public double Mean { get; }
public double StdDev { get; }
public SeriesStats(double min, double max, double mean, double stdDev)
{
Min = min;
Max = max;
Mean = mean;
StdDev = stdDev;
}
}
public SeriesStats Statistics()
{
var nums = _data.Select(d => d.Value).ToList();
var mean = // ...
var variance = // ...
return new SeriesStats(min: /* ... */, max: /* ... */, mean: mean, stdDev: /* ... */);
}Voir la solution
public SeriesStats Statistics()
{
var nums = _data.Select(d => d.Value).ToList();
var mean = nums.Aggregate(0.0, (acc, v) => acc + v) / nums.Count;
var variance = nums.Aggregate(0.0, (acc, v) => acc + Math.Pow(v - mean, 2)) / nums.Count;
return new SeriesStats(
min: nums.Min(),
max: nums.Max(),
mean: mean,
stdDev: Math.Sqrt(variance)
);
}Comparer les profils — un écart-type faible = joueur régulier :
var statsLea = kdaLea.Statistics();
var statsRaphael = kdaRaphael.Statistics();
Console.WriteLine($"Léa — KDA moy : {statsLea.Mean:F2}, écart-type : {statsLea.StdDev:F2}");
Console.WriteLine($"Raphaël — KDA moy : {statsRaphael.Mean:F2}, écart-type : {statsRaphael.StdDev:F2}");Qui mérite la place de titulaire aux playoffs ?
Étape 4 — Interface CLI
Ajouter --rank pour afficher le classement des joueurs par KDA moyen, et --window <n> pour afficher la progression sur des fenêtres glissantes.
Avant de coder : Comment trier une collection de tuples (nom, kdaMoyen) par valeur décroissante ? Pour --window, comment récupérer n sous forme d'entier depuis args ?
dotnet run -- --rank
dotnet run -- --game valorant --player Léa --stat kda --window 3Voir la solution
if (args.Contains("--rank"))
{
var players = new[]
{
("Léa", kdaLea.Fold(0.0, (a, v) => a + v) / kdaLea.Count),
("Raphaël", kdaRaphael.Fold(0.0, (a, v) => a + v) / kdaRaphael.Count),
("Noé", kdaNoe.Fold(0.0, (a, v) => a + v) / kdaNoe.Count),
("Dylan", kdaDylan.Fold(0.0, (a, v) => a + v) / kdaDylan.Count),
("Kiara", kdaKiara.Fold(0.0, (a, v) => a + v) / kdaKiara.Count),
};
foreach (var (name, kda) in players.OrderByDescending(p => p.Item2))
Console.WriteLine($"{name,-10} KDA moy : {kda:F2}");
}
int window = args.Contains("--window")
? int.Parse(args[Array.IndexOf(args, "--window") + 1])
: 5;Étape bonus (avancé) — GroupBy
Étape optionnelle — pour aller plus loin.
Le classement de l'étape 4 construit les moyennes joueur par joueur, à la main. GroupBy fait le partitionnement automatiquement :
// Tous les matchs Valorant (Léa + Dylan) — stats par joueur en un pipeline
var ranking = valorant.Matches
.GroupBy(m => m.Player)
.Select(g => new
{
Player = g.Key,
Matches = g.Count(),
AvgKda = g.Aggregate(0.0, (acc, m) =>
acc + (m.Kills + m.Assists) / (double)(m.Deaths == 0 ? 1 : m.Deaths))
/ g.Count()
})
.OrderByDescending(s => s.AvgKda);
foreach (var s in ranking)
Console.WriteLine($"{s.Player,-10} {s.Matches} matchs — KDA moy : {s.AvgKda:F2}");Le motif GroupBy(clé).Select(g => g.Aggregate(...)) = partitionner, puis réduire chaque partition — un Fold par clé. → GroupBy — agréger par clé
Vérification
Foldsur liste vide retourneseedSlidingWindow(5)sur 13 matchs produit 9 fenêtres (13 - 5 + 1 = 9)Statistics().Meancorrespond àFold(0.0, (acc,v)=>acc+v) / Count- Les écarts-types permettent de distinguer les profils réguliers des profils variables