diff --git a/README.md b/README.md index 8d3282b..abe3a1e 100644 --- a/README.md +++ b/README.md @@ -30,14 +30,18 @@ Average silhouette width is a widely-used measure of clustering quality. Higher 2. How well-separated each cluster is from its nearest neighboring cluster. +Although the coefficient is mathematically bounded by [−1, 1], cosine distance between high-dimensional text embeddings is compressed: unrelated notes sit close to orthogonal, so both the within-cluster and nearest-cluster distances land near 0.8. Because silhouette divides the gap between them by the larger of the two, the practical range on embedding data is roughly [−0.05, 0.10] rather than the full interval. + +The bands below are therefore calibrated against that observed range. On the seed board, the five ideal thematic clusters score 0.09; swapping a few notes between clusters drops it to 0.06; a scrambled assignment falls below zero. + The score appears above the results with a plain-language band: -- **0.70 and above** — Strong -- **0.40 to 0.69** — Moderate -- **0.10 to 0.39** — Weak -- **Below 0.10** — Poor +- **0.07 and above** — Strong +- **0.04 to 0.06** — Moderate +- **0.01 to 0.03** — Weak +- **Below 0.01** — Poor -Silhouette values are archetypically bounded below 1.0 for real-world data, so the number is best read as a relative measure. See Hugo Sträng, Tai Dinh. An upper bound on the silhouette evaluation metric for clustering. Pattern Recognition, Volume 178, 2026, 113402, ISSN 0031-3203. +A score near 0.00 means the grouping is no better than chance. Bands are specific to `voyage-3` cosine distance and would need recalibration behind a different embedding model. See Hugo Sträng, Tai Dinh. An upper bound on the silhouette evaluation metric for clustering. Pattern Recognition, Volume 178, 2026, 113402, ISSN 0031-3203. ## Organizing clusters, exporting to workflow software diff --git a/frontend/src/components/stickies.tsx b/frontend/src/components/stickies.tsx index 05551c8..0d3604f 100644 --- a/frontend/src/components/stickies.tsx +++ b/frontend/src/components/stickies.tsx @@ -6,132 +6,136 @@ import Sticky from './sticky'; import Button from './button'; import '../styles/stickies.css'; +// In practice, silhouette on cosine distance between text embeddings occupies roughly +// [-0.05, 0.10], not strict theoretical [-1, 1]: near-orthogonal vectors put both the within- and +// nearest-cluster distances close to 0.8, and the coefficient divides their gap +// by the larger. These bands are calibrated to that range for voyage-3. see README, Reading the cohesion score const scoreLabel = (score: number): string => { - if (score >= 0.7) return 'Strong'; - if (score >= 0.4) return 'Moderate'; - if (score >= 0.1) return 'Weak'; - return 'Poor'; + if (score >= 0.07) return 'Strong'; + if (score >= 0.04) return 'Moderate'; + if (score >= 0.01) return 'Weak'; + return 'Poor'; }; const Stickies = () => { - const { data: stickies, isLoading, error } = useGetStickies(); - const { mutate: cluster, data: clusterResponse, isPending } = useClusterStickies(); + const { data: stickies, isLoading, error } = useGetStickies(); + const { mutate: cluster, data: clusterResponse, isPending } = useClusterStickies(); - const [rankedClusters, setRankedClusters] = useState([]); - const dragIndex = useRef(null); - const [dragOverIndex, setDragOverIndex] = useState(null); + const [rankedClusters, setRankedClusters] = useState([]); + const dragIndex = useRef(null); + const [dragOverIndex, setDragOverIndex] = useState(null); - useEffect(() => { - if (clusterResponse?.clusters) { - setRankedClusters( - clusterResponse.clusters.map((c, i) => ({ ...c, rank: i + 1 })) - ); - } - }, [clusterResponse]); + useEffect(() => { + if (clusterResponse?.clusters) { + setRankedClusters( + clusterResponse.clusters.map((c, i) => ({ ...c, rank: i + 1 })) + ); + } + }, [clusterResponse]); - if (isLoading) return
Loading...
; - if (error) return
Error: {error.message}
; + if (isLoading) return
Loading...
; + if (error) return
Error: {error.message}
; - const score = clusterResponse?.score; + const score = clusterResponse?.score; - const handleCluster = () => { - cluster(); - }; + const handleCluster = () => { + cluster(); + }; - const buildStickyMap = (): Map => { - const map = new Map(); - stickies?.forEach((s) => map.set(s.id, s)); - return map; - }; + const buildStickyMap = (): Map => { + const map = new Map(); + stickies?.forEach((s) => map.set(s.id, s)); + return map; + }; - const stickyMap = buildStickyMap(); + const stickyMap = buildStickyMap(); - const renderStickies = (items: StickyType[]) => - items?.map((sticky) => ); + const renderStickies = (items: StickyType[]) => + items?.map((sticky) => ); - const handleDragStart = (index: number) => { - dragIndex.current = index; - }; + const handleDragStart = (index: number) => { + dragIndex.current = index; + }; - const handleDragOver = (e: DragEvent, index: number) => { - e.preventDefault(); - setDragOverIndex(index); - }; + const handleDragOver = (e: DragEvent, index: number) => { + e.preventDefault(); + setDragOverIndex(index); + }; - const handleDragLeave = () => { - setDragOverIndex(null); - }; + const handleDragLeave = () => { + setDragOverIndex(null); + }; - const handleDrop = (targetIndex: number) => { - const sourceIndex = dragIndex.current; - if (sourceIndex === null || sourceIndex === targetIndex) { - dragIndex.current = null; - setDragOverIndex(null); - return; - } + const handleDrop = (targetIndex: number) => { + const sourceIndex = dragIndex.current; + if (sourceIndex === null || sourceIndex === targetIndex) { + dragIndex.current = null; + setDragOverIndex(null); + return; + } - const reordered = [...rankedClusters]; - const [moved] = reordered.splice(sourceIndex, 1); - reordered.splice(targetIndex, 0, moved); + const reordered = [...rankedClusters]; + const [moved] = reordered.splice(sourceIndex, 1); + reordered.splice(targetIndex, 0, moved); - setRankedClusters(reordered.map((c, i) => ({ ...c, rank: i + 1 }))); - dragIndex.current = null; - setDragOverIndex(null); - }; + setRankedClusters(reordered.map((c, i) => ({ ...c, rank: i + 1 }))); + dragIndex.current = null; + setDragOverIndex(null); + }; - const handleDragEnd = () => { - dragIndex.current = null; - setDragOverIndex(null); - }; + const handleDragEnd = () => { + dragIndex.current = null; + setDragOverIndex(null); + }; - return ( -
-
- ) : ( -
- {renderStickies(stickies ?? [])} -
- )} - - ); + ); }; export default Stickies; diff --git a/frontend/src/tests/stickies.test.tsx b/frontend/src/tests/stickies.test.tsx index 8f1d881..d595c78 100644 --- a/frontend/src/tests/stickies.test.tsx +++ b/frontend/src/tests/stickies.test.tsx @@ -14,7 +14,7 @@ const MOCK_CLUSTER_RESPONSE = { { label: 'Auth Issues', noteIds: ['note_001'] }, { label: 'Export Issues', noteIds: ['note_002'] }, ], - score: 0.74, + score: 0.09, }; let fetchMock: ReturnType;