fix: compute z-context per-round in edition-mode rankings rollup
Previously the edition-level branch of analytics.getProjectRankings (programId mode) pooled every juror's evaluations across every round into a single z-normalization context. A juror's mean and stddev are not stable across round types — quick intake screening produces a very different grading profile than a deep evaluation round, and mixing them yields a meaningless personal calibration. The rollup now groups points by roundId, computes one balance context per round, and aggregates per-project as the unweighted mean of the per-round balanced averages. roundId mode is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -118,3 +118,71 @@ export function computeBalancedProjectScores(
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-round balanced rollup: groups points by roundId, computes a balance
|
||||
* context per round, then averages the per-round balanced averages for each
|
||||
* project. Use when surfacing edition-level rankings — never pool z-contexts
|
||||
* across rounds, because a juror's grading profile differs by round type.
|
||||
*/
|
||||
export type RoundScopedScorePoint = ScorePoint & { roundId: string }
|
||||
|
||||
export type EditionRollupResult = {
|
||||
projectId: string
|
||||
rawAverage: number | null
|
||||
balancedAverage: number | null
|
||||
count: number
|
||||
roundCount: number
|
||||
}
|
||||
|
||||
export function computePerRoundBalanced(
|
||||
points: RoundScopedScorePoint[],
|
||||
): Map<string, EditionRollupResult> {
|
||||
const byRound = new Map<string, ScorePoint[]>()
|
||||
for (const p of points) {
|
||||
const arr = byRound.get(p.roundId) ?? []
|
||||
arr.push({ projectId: p.projectId, userId: p.userId, rawScore: p.rawScore })
|
||||
byRound.set(p.roundId, arr)
|
||||
}
|
||||
|
||||
const perRoundResults: Array<Map<string, BalancedProjectResult>> = []
|
||||
for (const roundPoints of byRound.values()) {
|
||||
const ctx = computeBalanceContext(roundPoints)
|
||||
perRoundResults.push(computeBalancedProjectScores(roundPoints, ctx))
|
||||
}
|
||||
|
||||
const accumulator = new Map<
|
||||
string,
|
||||
{ rawSum: number; rawCount: number; balancedSum: number; balancedCount: number; count: number; roundCount: number }
|
||||
>()
|
||||
for (const roundMap of perRoundResults) {
|
||||
for (const [projectId, result] of roundMap.entries()) {
|
||||
const acc = accumulator.get(projectId) ?? {
|
||||
rawSum: 0, rawCount: 0, balancedSum: 0, balancedCount: 0, count: 0, roundCount: 0,
|
||||
}
|
||||
if (result.rawAverage != null) {
|
||||
acc.rawSum += result.rawAverage
|
||||
acc.rawCount += 1
|
||||
}
|
||||
if (result.balancedAverage != null) {
|
||||
acc.balancedSum += result.balancedAverage
|
||||
acc.balancedCount += 1
|
||||
}
|
||||
acc.count += result.count
|
||||
acc.roundCount += 1
|
||||
accumulator.set(projectId, acc)
|
||||
}
|
||||
}
|
||||
|
||||
const out = new Map<string, EditionRollupResult>()
|
||||
for (const [projectId, acc] of accumulator.entries()) {
|
||||
out.set(projectId, {
|
||||
projectId,
|
||||
rawAverage: acc.rawCount > 0 ? acc.rawSum / acc.rawCount : null,
|
||||
balancedAverage: acc.balancedCount > 0 ? acc.balancedSum / acc.balancedCount : null,
|
||||
count: acc.count,
|
||||
roundCount: acc.roundCount,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user