feat: slider sensibilité + z-score standardization pour clustering plus précis
Sensibilité (0.5x–3.0x) : - Multiplie k : sensibilité=2x avec k=14 → 28 clusters effectifs - Labels UI : Grossière / Normale / Fine / Très fine / Maximum - Paramètres avancés (k, fenêtre) masqués dans un <details> - Cache invalidé si sensibilité change Z-score standardisation (Bishop 2006 PRML §9.1) : - Normalise par variance de chaque feature avant K-means - Features discriminantes (forte std) pèsent plus - Résultat : risque 0→1.00 sur clusters bots vs 0→0.27 avant - Bots détectés : 4 337 IPs vs 1 604 (2.7x plus) - Nouveaux clusters : Bot agressif, Tunnel réseau, UA-CH Mismatch distincts Fix TextLayer deck.gl : - Translittération des accents (é→e, à→a, ç→c…) + strip emojis - Évite les warnings 'Missing character' sur caractères non-ASCII Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@ -96,6 +96,7 @@ function hexToRgba(hex: string, alpha = 255): [number, number, number, number] {
|
||||
export default function ClusteringView() {
|
||||
const [k, setK] = useState(14);
|
||||
const [hours, setHours] = useState(24);
|
||||
const [sensitivity, setSensitivity] = useState(1.0);
|
||||
const [data, setData] = useState<ClusterResult | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [computing, setComputing] = useState(false);
|
||||
@ -123,7 +124,7 @@ export default function ClusteringView() {
|
||||
setError(null);
|
||||
try {
|
||||
const res = await axios.get<ClusterResult>('/api/clustering/clusters', {
|
||||
params: { k, hours, force },
|
||||
params: { k, hours, sensitivity, force },
|
||||
});
|
||||
if (res.data.status === 'computing' || res.data.status === 'idle') {
|
||||
setComputing(true);
|
||||
@ -298,13 +299,17 @@ export default function ClusteringView() {
|
||||
updateTriggers: { getFillColor: [selected?.id], getLineWidth: [selected?.id] },
|
||||
}));
|
||||
|
||||
// 5. Labels (TextLayer) — strip emojis (non supportés par le bitmap font deck.gl)
|
||||
const stripEmoji = (s: string) => s.replace(/[\u{1F000}-\u{1FFFF}\u{2600}-\u{27FF}]/gu, '').trim();
|
||||
const stripNonAscii = (s: string) =>
|
||||
s.replace(/[\u{0080}-\u{FFFF}]/gu, c => {
|
||||
// Translitérations basiques pour la lisibilité
|
||||
const map: Record<string, string> = { é:'e',è:'e',ê:'e',ë:'e',à:'a',â:'a',ô:'o',ù:'u',û:'u',î:'i',ï:'i',ç:'c' };
|
||||
return map[c] ?? '';
|
||||
}).replace(/[\u{1F000}-\u{1FFFF}\u{2600}-\u{27FF}]/gu, '').trim();
|
||||
layerList.push(new TextLayer({
|
||||
id: 'labels',
|
||||
data: nodes,
|
||||
getPosition: (d: ClusterNode) => [toWorld(d.pca_x), toWorld(d.pca_y), 0],
|
||||
getText: (d: ClusterNode) => stripEmoji(d.label),
|
||||
getText: (d: ClusterNode) => stripNonAscii(d.label),
|
||||
getSize: 12,
|
||||
sizeUnits: 'pixels',
|
||||
getColor: [255, 255, 255, 200],
|
||||
@ -334,24 +339,48 @@ export default function ClusteringView() {
|
||||
|
||||
{/* Paramètres */}
|
||||
<div className="bg-background-card rounded-lg p-3 space-y-3">
|
||||
<label className="block text-xs text-text-secondary">
|
||||
Clusters (k)
|
||||
<input type="range" min={4} max={30} value={k}
|
||||
onChange={e => setK(+e.target.value)}
|
||||
className="w-full mt-1 accent-accent-primary" />
|
||||
<span className="font-mono text-white">{k}</span>
|
||||
</label>
|
||||
<label className="block text-xs text-text-secondary">
|
||||
Fenêtre
|
||||
<select value={hours} onChange={e => setHours(+e.target.value)}
|
||||
className="w-full mt-1 bg-background border border-gray-600 rounded px-2 py-1 text-sm">
|
||||
<option value={6}>6h</option>
|
||||
<option value={12}>12h</option>
|
||||
<option value={24}>24h</option>
|
||||
<option value={48}>48h</option>
|
||||
<option value={168}>7j</option>
|
||||
</select>
|
||||
</label>
|
||||
{/* Sensibilité */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs text-text-secondary">
|
||||
<span>Sensibilité</span>
|
||||
<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'}
|
||||
{' '}({Math.round(k * sensitivity)} clusters)
|
||||
</span>
|
||||
</div>
|
||||
<input type="range" min={0.5} max={3.0} step={0.5} value={sensitivity}
|
||||
onChange={e => setSensitivity(+e.target.value)}
|
||||
className="w-full accent-accent-primary" />
|
||||
<div className="flex justify-between text-xs text-text-disabled">
|
||||
<span>Grossière</span><span>Maximum</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* k avancé */}
|
||||
<details className="text-xs text-text-secondary">
|
||||
<summary className="cursor-pointer hover:text-white">Paramètres avancés</summary>
|
||||
<div className="mt-2 space-y-2">
|
||||
<label className="block">
|
||||
Clusters de base (k)
|
||||
<input type="range" min={4} max={30} value={k}
|
||||
onChange={e => setK(+e.target.value)}
|
||||
className="w-full mt-1 accent-accent-primary" />
|
||||
<span className="font-mono text-white">{k}</span>
|
||||
</label>
|
||||
<label className="block">
|
||||
Fenêtre
|
||||
<select value={hours} onChange={e => setHours(+e.target.value)}
|
||||
className="w-full mt-1 bg-background border border-gray-600 rounded px-2 py-1">
|
||||
<option value={6}>6h</option>
|
||||
<option value={12}>12h</option>
|
||||
<option value={24}>24h</option>
|
||||
<option value={48}>48h</option>
|
||||
<option value={168}>7j</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<label className="flex items-center gap-2 text-xs text-text-secondary cursor-pointer">
|
||||
<input type="checkbox" checked={showEdges} onChange={e => setShowEdges(e.target.checked)}
|
||||
className="accent-accent-primary" />
|
||||
|
||||
Reference in New Issue
Block a user