feat(clustering): limites sensibilité et k étendues pour classification maximale
Backend: - k max: 30 → 100 (Query le=100), default: 14 → 20 - sensitivity max: 3.0 → 5.0 (Query le=5.0) - k_actual cap: min(50,...) → min(300,...) — plus de coupure silencieuse - n_init adaptatif: 3 quand k≤60, 1 quand k>60 (maintient performance) - Résultat max effectif: k=100 × sens=5.0 = 500, plafonné à 300 clusters Frontend: - Slider sensibilité: max 3.0 → 5.0, step 0.5 - Libellés: Grossière/Normale/Fine/Très fine/Maximale/Extrême - Label affiche '(N clusters effectifs)' au lieu de '(N clusters)' - Slider k avancé: max 30 → 100 - Label k avancé: 'k → N clusters effectifs' (montre le résultat réel) - Default k: 14 → 20 Test: k=20 × sens=5.0 = 100 clusters, Scanner pur detecté à 0.43, Bot UA simulé 0.38 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@ -152,13 +152,17 @@ _SQL_COLS = [
|
|||||||
|
|
||||||
def _run_clustering_job(k: int, hours: int, sensitivity: float = 1.0) -> None:
|
def _run_clustering_job(k: int, hours: int, sensitivity: float = 1.0) -> None:
|
||||||
"""Exécuté dans le thread pool. Met à jour _CACHE.
|
"""Exécuté dans le thread pool. Met à jour _CACHE.
|
||||||
|
|
||||||
sensitivity : multiplicateur de k [0.5 – 3.0].
|
sensitivity : multiplicateur de k [0.5 – 5.0].
|
||||||
|
0.5 = vue très agrégée (k/2 clusters)
|
||||||
1.0 = comportement par défaut
|
1.0 = comportement par défaut
|
||||||
2.0 = deux fois plus de clusters → groupes plus homogènes
|
2.0 = deux fois plus de clusters → groupes plus homogènes
|
||||||
0.5 = moitié → vue très agrégée
|
5.0 = granularité maximale (classification la plus fine)
|
||||||
|
|
||||||
|
k_actual est plafonné à 300 pour éviter des temps de calcul excessifs.
|
||||||
|
n_init est réduit à 1 quand k_actual > 60 pour rester rapide.
|
||||||
"""
|
"""
|
||||||
k_actual = max(4, min(50, round(k * sensitivity)))
|
k_actual = max(4, min(300, round(k * sensitivity)))
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
with _LOCK:
|
with _LOCK:
|
||||||
_CACHE["status"] = "computing"
|
_CACHE["status"] = "computing"
|
||||||
@ -189,7 +193,9 @@ def _run_clustering_job(k: int, hours: int, sensitivity: float = 1.0) -> None:
|
|||||||
X_std, feat_mean, feat_std = standardize(X64)
|
X_std, feat_mean, feat_std = standardize(X64)
|
||||||
|
|
||||||
# ── 4. K-means++ sur l'espace standardisé ────────────────────────
|
# ── 4. K-means++ sur l'espace standardisé ────────────────────────
|
||||||
km = kmeans_pp(X_std, k=k_actual, max_iter=80, n_init=3, seed=42)
|
# n_init réduit à 1 pour k élevé (> 60) afin de limiter le temps de calcul
|
||||||
|
n_init = 1 if k_actual > 60 else 3
|
||||||
|
km = kmeans_pp(X_std, k=k_actual, max_iter=80, n_init=n_init, seed=42)
|
||||||
log.info(f"[clustering] K-means: {km.n_iter} iters, inertia={km.inertia:.2f}")
|
log.info(f"[clustering] K-means: {km.n_iter} iters, inertia={km.inertia:.2f}")
|
||||||
|
|
||||||
# Centroïdes dans l'espace original [0,1] pour affichage radar
|
# Centroïdes dans l'espace original [0,1] pour affichage radar
|
||||||
@ -411,9 +417,9 @@ async def get_status():
|
|||||||
|
|
||||||
@router.get("/clusters")
|
@router.get("/clusters")
|
||||||
async def get_clusters(
|
async def get_clusters(
|
||||||
k: int = Query(14, ge=4, le=30, description="Nombre de clusters de base"),
|
k: int = Query(20, ge=4, le=100, description="Nombre de clusters de base"),
|
||||||
hours: int = Query(24, ge=1, le=168, description="Fenêtre temporelle (heures)"),
|
hours: int = Query(24, ge=1, le=168, description="Fenêtre temporelle (heures)"),
|
||||||
sensitivity: float = Query(1.0, ge=0.5, le=3.0, description="Sensibilité : multiplicateur de k"),
|
sensitivity: float = Query(1.0, ge=0.5, le=5.0, description="Sensibilité : multiplicateur de k (5.0 = granularité maximale)"),
|
||||||
force: bool = Query(False, description="Forcer le recalcul"),
|
force: bool = Query(False, description="Forcer le recalcul"),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -94,7 +94,7 @@ function hexToRgba(hex: string, alpha = 255): [number, number, number, number] {
|
|||||||
// ─── Composant principal ──────────────────────────────────────────────────────
|
// ─── Composant principal ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function ClusteringView() {
|
export default function ClusteringView() {
|
||||||
const [k, setK] = useState(14);
|
const [k, setK] = useState(20);
|
||||||
const [hours, setHours] = useState(24);
|
const [hours, setHours] = useState(24);
|
||||||
const [sensitivity, setSensitivity] = useState(1.0);
|
const [sensitivity, setSensitivity] = useState(1.0);
|
||||||
const [data, setData] = useState<ClusterResult | null>(null);
|
const [data, setData] = useState<ClusterResult | null>(null);
|
||||||
@ -344,15 +344,15 @@ export default function ClusteringView() {
|
|||||||
<div className="flex justify-between text-xs text-text-secondary">
|
<div className="flex justify-between text-xs text-text-secondary">
|
||||||
<span>Sensibilité</span>
|
<span>Sensibilité</span>
|
||||||
<span className="font-mono text-white">
|
<span className="font-mono text-white">
|
||||||
{sensitivity === 0.5 ? 'Grossière' : sensitivity <= 1.0 ? 'Normale' : sensitivity <= 1.5 ? 'Fine' : sensitivity <= 2.0 ? 'Très fine' : 'Maximum'}
|
{sensitivity <= 0.5 ? 'Grossière' : sensitivity <= 1.0 ? 'Normale' : sensitivity <= 2.0 ? 'Fine' : sensitivity <= 3.5 ? 'Très fine' : sensitivity <= 4.5 ? 'Maximale' : 'Extrême'}
|
||||||
{' '}({Math.round(k * sensitivity)} clusters)
|
{' '}({Math.round(k * sensitivity)} clusters effectifs)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="range" min={0.5} max={3.0} step={0.5} value={sensitivity}
|
<input type="range" min={0.5} max={5.0} step={0.5} value={sensitivity}
|
||||||
onChange={e => setSensitivity(+e.target.value)}
|
onChange={e => setSensitivity(+e.target.value)}
|
||||||
className="w-full accent-accent-primary" />
|
className="w-full accent-accent-primary" />
|
||||||
<div className="flex justify-between text-xs text-text-disabled">
|
<div className="flex justify-between text-xs text-text-disabled">
|
||||||
<span>Grossière</span><span>Maximum</span>
|
<span>Grossière</span><span>Fine</span><span>Extrême</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -362,10 +362,10 @@ export default function ClusteringView() {
|
|||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
Clusters de base (k)
|
Clusters de base (k)
|
||||||
<input type="range" min={4} max={30} value={k}
|
<input type="range" min={4} max={100} value={k}
|
||||||
onChange={e => setK(+e.target.value)}
|
onChange={e => setK(+e.target.value)}
|
||||||
className="w-full mt-1 accent-accent-primary" />
|
className="w-full mt-1 accent-accent-primary" />
|
||||||
<span className="font-mono text-white">{k}</span>
|
<span className="font-mono text-white">{k} → {Math.round(k * sensitivity)} clusters effectifs</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
Fenêtre
|
Fenêtre
|
||||||
|
|||||||
Reference in New Issue
Block a user