feat: factor balanced pass rate into composite rankings
The dashboard now computes its own composite ranking score on the client, blending (balanced-or-raw) average score with (balanced-or-raw) advance pass rate via the existing scoreWeight / passRateWeight sliders. Both inputs are toggled independently: - 'Balance juror grading style (score)' — existing useBalancedRanking - 'Balance juror approval rate (advance vote)' — new useBalancedPassRate Both default to true and persist per-round. The pass rate is balanced the same way scores are: each juror's personal yes-rate gives them a Bernoulli stddev, each vote is z-normalized against that, and the project's mean z is rescaled to the round's overall yes rate. A 'yes' from a juror who rarely says yes counts more than a 'yes' from a lenient juror. List rows now show two chips — score (Bal/Raw X.XX) and pass rate (Bal Yes% / Yes% N%) — so admins can see what's driving the order. The threshold cutoff and live re-sort effect both use the same composite formula. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,14 @@ import {
|
||||
} from '../services/ai-ranking'
|
||||
import { logAudit } from '../utils/audit'
|
||||
import type { EvaluationConfig } from '@/types/competition-configs'
|
||||
import { computeBalanceContext, computeBalancedProjectScores, type ScorePoint } from '../services/juror-balance'
|
||||
import {
|
||||
computeBalanceContext,
|
||||
computeBalancedProjectScores,
|
||||
computePassRateContext,
|
||||
computeBalancedPassRates,
|
||||
type ScorePoint,
|
||||
type VotePoint,
|
||||
} from '../services/juror-balance'
|
||||
|
||||
// ─── Local Types ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -492,6 +499,7 @@ export const rankingRouter = router({
|
||||
}>> = {}
|
||||
|
||||
const balancePoints: ScorePoint[] = []
|
||||
const votePoints: VotePoint[] = []
|
||||
|
||||
for (const a of assignments) {
|
||||
if (!a.evaluation) continue
|
||||
@@ -523,19 +531,45 @@ export const rankingRouter = router({
|
||||
rawScore: a.evaluation.globalScore,
|
||||
})
|
||||
}
|
||||
|
||||
if (decision !== null) {
|
||||
votePoints.push({
|
||||
projectId: a.projectId,
|
||||
userId: a.userId,
|
||||
vote: decision,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const balanceCtx = computeBalanceContext(balancePoints)
|
||||
const balancedByProject = computeBalancedProjectScores(balancePoints, balanceCtx)
|
||||
|
||||
// Per-project balanced average on the 1-10 scale, comparable to raw avgs.
|
||||
const balanced: Record<string, { rawAverage: number | null; balancedAverage: number | null }> = {}
|
||||
const passRateCtx = computePassRateContext(votePoints)
|
||||
const balancedPassRateByProject = computeBalancedPassRates(votePoints, passRateCtx)
|
||||
|
||||
// Per-project: balanced score (1-10) + balanced pass rate (0-1).
|
||||
const balanced: Record<string, {
|
||||
rawAverage: number | null
|
||||
balancedAverage: number | null
|
||||
rawPassRate: number | null
|
||||
balancedPassRate: number | null
|
||||
}> = {}
|
||||
for (const [projectId, result] of balancedByProject.entries()) {
|
||||
balanced[projectId] = {
|
||||
rawAverage: result.rawAverage,
|
||||
balancedAverage: result.balancedAverage,
|
||||
rawPassRate: null,
|
||||
balancedPassRate: null,
|
||||
}
|
||||
}
|
||||
for (const [projectId, result] of balancedPassRateByProject.entries()) {
|
||||
const existing = balanced[projectId] ?? {
|
||||
rawAverage: null, balancedAverage: null, rawPassRate: null, balancedPassRate: null,
|
||||
}
|
||||
existing.rawPassRate = result.rawPassRate
|
||||
existing.balancedPassRate = result.balancedPassRate
|
||||
balanced[projectId] = existing
|
||||
}
|
||||
|
||||
// Per-juror grading stats so the side panel can render each juror's
|
||||
// personal baseline and rescaled contribution.
|
||||
@@ -544,12 +578,20 @@ export const rankingRouter = router({
|
||||
jurorStats[userId] = { mean: s.mean, stddev: s.stddev, count: s.count }
|
||||
}
|
||||
|
||||
const jurorYesRates: Record<string, { yesRate: number; stddev: number; count: number }> = {}
|
||||
for (const [userId, s] of passRateCtx.jurorYesRates.entries()) {
|
||||
jurorYesRates[userId] = { yesRate: s.yesRate, stddev: s.stddev, count: s.count }
|
||||
}
|
||||
|
||||
return {
|
||||
byProject,
|
||||
balanced,
|
||||
jurorStats,
|
||||
overallMean: balanceCtx.overallMean,
|
||||
overallStddev: balanceCtx.overallStddev,
|
||||
jurorYesRates,
|
||||
overallYesRate: passRateCtx.overallYesRate,
|
||||
overallYesStddev: passRateCtx.overallStddev,
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user