rank method
Rank a list of match results and assign conformal confidence.
Implementation
RankedResults rank(List<MatchResult> results) {
final count = results.length;
if (count == 0) {
return const RankedResults(
items: [],
summary: RankingSummary(
count: 0,
stableCount: 0,
tieGroupCount: 0,
medianGap: 0,
),
);
}
// Tag with original index, sort descending by score then match type
final indexed = <(int, MatchResult)>[];
for (var i = 0; i < results.length; i++) {
indexed.add((i, results[i]));
}
indexed.sort((a, b) {
final scoreCmp = b.$2.score.compareTo(a.$2.score);
if (scoreCmp != 0) return scoreCmp;
return b.$2.matchType.index.compareTo(a.$2.matchType.index);
});
// Compute gaps
final gaps = <double>[];
for (var i = 0; i < indexed.length - 1; i++) {
gaps.add(
(indexed[i].$2.score - indexed[i + 1].$2.score).clamp(
0,
double.infinity,
),
);
}
final sortedGaps = List<double>.from(gaps)..sort();
// Compute confidence per position
final items = <RankedItem>[];
var stableCount = 0;
var tieGroupCount = 0;
var inTieGroup = false;
for (var rank = 0; rank < indexed.length; rank++) {
final (origIdx, result) = indexed[rank];
final gapToNext = rank < gaps.length ? gaps[rank] : 0.0;
double confidence;
if (sortedGaps.isEmpty) {
confidence = 1.0;
} else {
final leqCount = sortedGaps
.where((g) => g <= gapToNext + tieEpsilon * 0.5)
.length;
confidence = leqCount / sortedGaps.length;
}
final isTie = gaps.isNotEmpty && gapToNext < tieEpsilon;
RankStability stability;
if (isTie) {
if (!inTieGroup) {
tieGroupCount++;
inTieGroup = true;
}
stability = RankStability.unstable;
} else {
inTieGroup = false;
if (confidence >= stableThreshold) {
stableCount++;
stability = RankStability.stable;
} else if (confidence >= marginalThreshold) {
stability = RankStability.marginal;
} else {
stability = RankStability.unstable;
}
}
items.add(
RankedItem(
originalIndex: origIdx,
result: result,
rankConfidence: RankConfidence(
confidence: confidence,
gapToNext: gapToNext,
stability: stability,
),
),
);
}
// Median gap
double medianGap;
if (sortedGaps.isEmpty) {
medianGap = 0;
} else {
final mid = sortedGaps.length ~/ 2;
if (sortedGaps.length.isEven) {
medianGap = (sortedGaps[mid - 1] + sortedGaps[mid]) / 2;
} else {
medianGap = sortedGaps[mid];
}
}
return RankedResults(
items: items,
summary: RankingSummary(
count: count,
stableCount: stableCount,
tieGroupCount: tieGroupCount,
medianGap: medianGap,
),
);
}