Kendall tau-b rank correlation over the intersection of two top-K lists. * Returns null when the intersection is < 2 items (undefined correlation).
(oldIds: number[], newIds: number[])
| 60 | /** Kendall tau-b rank correlation over the intersection of two top-K lists. |
| 61 | * Returns null when the intersection is < 2 items (undefined correlation). */ |
| 62 | function kendallTau(oldIds: number[], newIds: number[]): number | null { |
| 63 | const common = oldIds.filter((id) => newIds.includes(id)); |
| 64 | if (common.length < 2) return null; |
| 65 | |
| 66 | const oldRank = new Map(oldIds.map((id, idx) => [id, idx])); |
| 67 | const newRank = new Map(newIds.map((id, idx) => [id, idx])); |
| 68 | |
| 69 | let concordant = 0; |
| 70 | let discordant = 0; |
| 71 | for (let i = 0; i < common.length; i++) { |
| 72 | for (let j = i + 1; j < common.length; j++) { |
| 73 | const a = common[i]; |
| 74 | const b = common[j]; |
| 75 | const oldOrder = (oldRank.get(a) ?? 0) - (oldRank.get(b) ?? 0); |
| 76 | const newOrder = (newRank.get(a) ?? 0) - (newRank.get(b) ?? 0); |
| 77 | if (oldOrder * newOrder > 0) concordant++; |
| 78 | else if (oldOrder * newOrder < 0) discordant++; |
| 79 | } |
| 80 | } |
| 81 | const total = concordant + discordant; |
| 82 | return total === 0 ? null : (concordant - discordant) / total; |
| 83 | } |
| 84 | |
| 85 | function formatPreview(text: string, max = 90): string { |
| 86 | const cleaned = text.replace(/\s+/g, " ").trim(); |