Exercice 03 — Valider les stats
Partie 2 —
.Filter()+.RemoveOutliers()+.HasAny()/.AllMatch()+ évaluation paresseuse
Concepts théoriques
- Thématique 02 — Filter et fonctions d'ordre supérieur
- Fonctions d'ordre supérieur
- Filter et prédicats
- Closures
- Évaluation paresseuse
Contexte
Le dataset contient maintenant les données réelles (CSV) et les données générées (exercice 02). Avant toute analyse, il faut valider que les contraintes sont respectées dans les trois sources.
Étape 1 — Implémenter .Filter(predicate)
Avant de coder : Filter doit retourner une nouvelle DataSeries<T>, pas modifier l'existante. Quelle méthode LINQ applique un prédicat à une séquence ?
Indice
Where(predicate) filtre une IEnumerable<T> sans modifier la source. Il suffit d'envelopper le résultat dans une nouvelle DataSeries<T>.
public DataSeries<T> Filter(Func<T, bool> predicate)
{
// retourner une nouvelle DataSeries contenant seulement les éléments qui satisfont le prédicat
// ...
}Voir la solution
public DataSeries<T> Filter(Func<T, bool> predicate)
=> DataSeries<T>.From(_data.Where(dp => predicate(dp.Value)));Observer que Filter retourne une nouvelle DataSeries<T> — la source _data n'est jamais modifiée. C'est l'immuabilité : chaque appel produit un nouvel objet.
Vérifier dans Program.cs :
var wins = valorant.Filter(m => m.Won);
Console.WriteLine(valorant.Count); // 25 — inchangé
Console.WriteLine(wins.Count); // sous-ensembleLes prédicats sont des valeurs. Plutôt que d'écrire les lambdas en ligne, les déclarer, les nommer et les combiner comme n'importe quelle variable :
Func<ValorantMatch, bool> isWin = m => m.Won;
Func<ValorantMatch, bool> isHighScore = m => m.Kills > 20;
// Combinaison : un nouveau prédicat (victoire éclatante) construit à partir des deux autres
Func<ValorantMatch, bool> isCrushingWin = m => isWin(m) && isHighScore(m);
var top = valorant.Filter(isCrushingWin);Une fonction stockée dans une variable se passe, se combine, se réutilise — → Fonctions comme valeurs
Étape 2 — Implémenter .RemoveOutliers(isValid)
Avant de coder : quelle est la différence entre Filter et RemoveOutliers ? Peut-on éviter de dupliquer du code ?
Indice
RemoveOutliers(isValid) garde les éléments valides — c'est exactement Filter(isValid). Une méthode peut déléguer à une autre méthode de la même classe.
public DataSeries<T> RemoveOutliers(Func<T, bool> isValid)
{
// ...
}Voir la solution
public DataSeries<T> RemoveOutliers(Func<T, bool> isValid)
=> Filter(isValid);Appliquer à chaque jeu pour éliminer les valeurs impossibles :
// Valorant : kills plausibles pour un match compétitif
var valorantValid = valorant.RemoveOutliers(m =>
m.Kills >= 0 && m.Kills <= 50 &&
m.Deaths >= 1 && m.Deaths <= 30 &&
m.Assists >= 0
);
// CS2 : contraintes similaires
var cs2Valid = cs2.RemoveOutliers(m =>
m.Kills + m.Assists <= 50 &&
m.Deaths >= 1
);
// LoL : le support a structurellement peu de kills
var lolValid = lol.RemoveOutliers(m =>
m.Kills <= 10 &&
m.Deaths >= 1 &&
m.Assists >= 0 &&
m.Cs >= 0
);Étape 3 — .HasAny() et .AllMatch()
Avant de coder : quelles méthodes LINQ répondent à "au moins un" et "tous" ?
Indice
Any(predicate) et All(predicate) — elles retournent un bool.
public bool HasAny(Func<T, bool> predicate) => // ...
public bool AllMatch(Func<T, bool> predicate) => // ...Voir la solution
public bool HasAny(Func<T, bool> predicate)
=> _data.Any(dp => predicate(dp.Value));
public bool AllMatch(Func<T, bool> predicate)
=> _data.All(dp => predicate(dp.Value));Utilisation :
Console.WriteLine(valorantValid.HasAny(m => m.Kills > 20));
// → Léa a-t-elle au moins un match avec plus de 20 kills ?
Console.WriteLine(lolValid.AllMatch(m => m.Deaths >= 1));
// → Tous les matchs de Noé ont-ils au moins 1 mort ?→ Théorie : Any et All — prédicats HOF
Évaluation paresseuse — observation
Where (LINQ) est paresseux : il ne parcourt rien à l'appel, il décrit l'opération. Mais DataSeries<T> modifie ce comportement. Deux expériences pour l'observer.
Expérience 1 — Quand le prédicat s'exécute-t-il ?
var filtered = valorant.Filter(m =>
{
Console.WriteLine($"Évaluation de {m.Player}");
return m.Won;
});
Console.WriteLine("--- après Filter ---");
_ = filtered.Count;
Console.WriteLine("--- après Count ---");Résultat attendu (intuition LINQ pure) : les lignes "Évaluation de…" apparaissent après Count.
Résultat réel — pourquoi ?
Les lignes s'affichent avant "--- après Filter ---". Le constructeur de DataSeries<T> appelle ToList() en interne : la matérialisation est immédiate. filtered.Count n'exécute rien de plus — la série est déjà calculée.
Expérience 2 — Mutation de la source après construction
var source = valorant.ToList();
var series = DataSeries<ValorantMatch>.From(source);
var filtered = series.Filter(m => m.Won);
int countBefore = filtered.Count;
source.Clear();
int countAfter = filtered.Count;
Console.WriteLine(countBefore == countAfter); // vrai ou faux ?Résultat attendu (intuition LINQ pure) : false — la série refléterait la source vidée.
Résultat réel — pourquoi ?
true — filtered est un instantané figé à sa création. La mutation de source est sans effet sur la série. C'est un choix délibéré : l'immuabilité est garantie dès la construction.
Comment préserver l'exécution différée ?
Voir
Stocker IEnumerable<DataPoint<T>> au lieu de List<DataPoint<T>> en interne suffirait :
// Version paresseuse — chaque accès réévalue le prédicat
private readonly IEnumerable<DataPoint<T>> _data; // au lieu de List<DataPoint<T>>Le pipeline resterait lazy mais la série ne serait plus un instantané : toute mutation de la source se répercuterait à chaque accès. Deux stratégies légitimes selon le contexte.
Étape 4 — Interface CLI
Ajouter les flags --player <nom> et --filter wins|losses|all. Ces deux flags se combinent avec --game introduit en exercice 01.
Avant de coder : Si --player est absent, que filtrer ? Si --filter vaut "all", faut-il appliquer un prédicat ? Plutôt qu'un if/else par mode, que gagne-t-on à stocker les prédicats dans un dictionnaire ? Que faut-il faire pour ajouter un critère --filter close (matchs serrés) ?
Indice — dispatch fonctionnel
Une fonction est une valeur : elle peut être la valeur d'un dictionnaire. Dictionary<string, Func<ValorantMatch, bool>> associe chaque mode CLI à son prédicat — le if/else disparaît.
Voir la solution
string? player = args.Contains("--player")
? args[Array.IndexOf(args, "--player") + 1]
: null;
string filterMode = args.Contains("--filter")
? args[Array.IndexOf(args, "--filter") + 1]
: "all";
// Table de prédicats — le mode CLI sélectionne une fonction
var filters = new Dictionary<string, Func<ValorantMatch, bool>>
{
["wins"] = m => m.Won,
["losses"] = m => !m.Won,
["all"] = m => true,
};
var result = valorant.Filter(filters[filterMode]);Ajouter un critère = ajouter une ligne dans la table, zéro if. La fonction choisie à l'exécution est une valeur comme une autre.
--player et --filter s'enchaînent naturellement : Filter retourne une DataSeries — composabilité des flags = composabilité du pipeline.
Vérification
valorant.Countreste 25 aprèsFilter(immuabilité)RemoveOutlierssur les données réelles ne retire aucun match (données déjà propres)RemoveOutlierssur les données générées (exercice 02) retire quelques matchs impossibles- Les deux expériences confirment que
DataSeries<T>matérialise immédiatement (snapshot) — contrairement à un pipeline LINQ pur