feat(clustering): dégradé HSL multi-stop basé sur le score de non-humanité

Remplace la palette par index par un dégradé continu HSL :
- risk=0.0  → hue 220° (bleu froid   — humain légitime)
- risk=0.25 → hue 165° (cyan-vert    — légèrement suspect)
- risk=0.50 → hue 110° (vert-jaune   — comportement mixte)
- risk=0.75 → hue  55° (jaune-orange — probable bot)
- risk=1.0  → hue   0° (rouge vif    — bot confirmé)

Saturation : 70%→90%, Lightness : 58%→48% (couleur plus intense = plus alarmant)

Légende : barre de dégradé 'Humain ← → Bot' avec stops HSL alignés

Suppression de spread_clusters (chevauchement des zones autorisé)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
SOC Analyst
2026-03-19 14:23:27 +01:00
parent 08d003a050
commit 2f73860cc8
3 changed files with 49 additions and 108 deletions

View File

@ -24,7 +24,7 @@ from ..services.clustering_engine import (
FEATURE_KEYS, FEATURE_NAMES, FEATURE_NORMS, N_FEATURES,
build_feature_vector, kmeans_pp, pca_2d, compute_hulls,
name_cluster, risk_score_from_centroid, standardize,
cluster_color, spread_clusters,
risk_to_gradient_color,
)
log = logging.getLogger(__name__)
@ -196,11 +196,7 @@ def _run_clustering_job(k: int, hours: int, sensitivity: float = 1.0) -> None:
# ── 5. PCA-2D sur les features ORIGINALES (normalisées [0,1]) ────
coords = pca_2d(X64) # (n, 2), normalisé [0,1]
# ── 5b. Dispersion — repousse les clusters trop proches ──────────
coords = spread_clusters(coords, km.labels, k_actual,
n_iter=60, min_dist=0.16)
# ── 5c. Enveloppes convexes par cluster ──────────────────────────
# ── 5b. Enveloppes convexes par cluster ──────────────────────────
hulls = compute_hulls(coords, km.labels, k_actual)
# ── 6. Agrégation par cluster ─────────────────────────────────────
@ -237,7 +233,7 @@ def _run_clustering_job(k: int, hours: int, sensitivity: float = 1.0) -> None:
raw_stats = {"mean_ttl": mean_ttl, "mean_mss": mean_mss, "mean_scale": mean_scale}
label_name = name_cluster(centroids_orig[j], raw_stats)
risk = float(risk_score_from_centroid(centroids_orig[j]))
color = cluster_color(j)
color = risk_to_gradient_color(risk)
# Centroïde 2D = moyenne des coords du cluster
cxy = np.mean(cluster_coords[j], axis=0).tolist() if cluster_coords[j] else [0.5, 0.5]