fix(cluster): exclude shed noise points from cluster membership#15
fix(cluster): exclude shed noise points from cluster membership#15rdmmf wants to merge 4 commits into
Conversation
Function and binary clustering stored a condensed-tree node's members as *every* descendant leaf, including points that HDBSCAN sheds as singleton noise before the cluster dissolves. That diluted tight cores: two 100%-similar binaries with one loosely-attached neighbour showed up as a single ~80% cohesion cluster of 3 instead of a precise pair, and member_count/cohesion were inflated by the shed points. Membership now follows HDBSCAN semantics: a leaf belongs to an ancestor cluster C only if it survives to C's death lambda (fallout >= death(C)). Shed points drop out and become noise. The full hierarchy is preserved when genuine sub-structure exists (parent + tight children still emitted). Also excludes the synthetic global root (which only stitches unrelated components) and drops degenerate <min_cluster_size survivor nodes. Shared logic extracted to cluster_common.hierarchical_membership with a Redis-free self-check (python -m ... cluster_common). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ring dev added run_pool_bin_clustering with its own inline membership walk that had the same bug (all descendants counted as members). Route it through the shared cluster_common.hierarchical_membership so pool-based binary clustering gets the same precise membership as the main path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HDBSCAN's single-linkage buries high-cohesion sub-groups inside low-density clusters: a cluster of 14 near-identical stub functions (cohesion 0.54) had 14 leaf children and zero sub-clusters, so the 100% pairs/groups inside were invisible. No HDBSCAN parameter surfaces them (the condensed tree genuinely has no sub-nodes), because loose members peel off one-at-a-time as singleton noise rather than splitting. Add a selectable Leiden backend (clustering.algorithm="leiden") that runs recursive community detection on the sparse similarity graph: - keeps low-cohesion parents as dendrogram levels (insight the user wants), - surfaces tight high-cohesion sub-groups as their children, - stops recursing at stop_cohesion so already-tight groups aren't over-split, - graph-native: missing pairs are absent, not treated as distance-1. Verified read-only on real data: the 0.54 blob is preserved as a parent with 0.96-1.0 sub-groups surfaced beneath it; aggregate 221 clusters @ avg cohesion 0.982. Faster than HDBSCAN and re-clusters a single touched component in ~3ms (incremental-friendly). Implementation: build_leiden_tree emits the same condensed-tree shape (tree_df) that flows through the existing persistence/metadata path unchanged. The HDBSCAN tree-build is extracted verbatim into _hdbscan_tree; run_clustering now dispatches on algorithm. HDBSCAN remains the default. Redis-free self-check in cluster_common covers both the split and no-over-split cases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Added: recursive-Leiden backend (scope A)Beyond the shed-noise membership fix, this branch now adds a selectable Leiden clustering backend for the function path. Why: HDBSCAN's single-linkage buries high-cohesion sub-groups inside low-density clusters — a 14-func cluster @ cohesion 0.54 had 14 leaf children and zero sub-clusters, so the 100% groups inside were invisible. No HDBSCAN param surfaces them (the tree genuinely has no sub-nodes). On the real What: Verified read-only on real data: the 0.54 blob preserved as a parent with 0.96–1.0 sub-groups surfaced beneath it; aggregate 221 clusters @ avg cohesion 0.982; re-clusters one touched component in ~3ms. Implementation: Not yet: full write-path run on live data (would clear existing clusters — recommend a throwaway-collection re-cluster before undrafting); binary path still HDBSCAN-only (follow-up). |
… phantom file-level algo Method selection was unreachable: pool config stored `cluster_algo` but run_pool_clustering never read it, and the direct build routes didn't forward an algorithm. Now: - run_pool_clustering reads cluster_algo/resolution/stop_cohesion from the pool's func_cluster_params and passes them through. - build_cluster / rebuild_cluster / rebuild_all forward algorithm + leiden knobs. - swagger ClusterBuild + PoolFuncClusterParams expose algorithm/resolution/ stop_cohesion. - pool-creation UI gets an Algorithm dropdown (HDBSCAN | Leiden) with resolution + stop_cohesion shown only for Leiden; pool detail view displays the chosen algorithm. Also removes the phantom file-level similarity algorithm: the pool form offered unweighted_cosine/weighted_cosine/jaccard for "file similarity", but file/binary similarity is derived from shared function clusters and inherits the function algorithm — `file_sim_params.algo` was stored and never consumed. Replaced the selector with an explanatory note and dropped the dead field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebased onto current
dev(supersedes #14, which was branched off a stale dev).Problem
Two complaints, one root cause:
member_count,cohesion_scoreinflated.Root cause
HDBSCAN keeps a tight core under one condensed-tree node while loosely-attached members peel off one-at-a-time as singleton noise (each <
min_cluster_size), so no split node is created. The code then built each node's membership by walking every descendant leaf, including those shed singletons — diluting the tight core.Not a parameter problem:
min_samples=1(needed to keep identical binaries tight) is already the default.Fix
A leaf belongs to an ancestor cluster
Conly if it survives toC's death lambda (fallout(leaf) >= death(C)). Shed points become noise.min_cluster_sizesurvivors.direct_membersderived from each leaf's deepest surviving cluster.Shared logic in
cluster_common.hierarchical_membership, applied to all hierarchical callers:run_clustering(function + binary) andrun_pool_bin_clustering. Redis-free self-check:uv run python bsimvis/app/services/cluster_common.py.Testing
🤖 Generated with Claude Code