Skip to content

Commit c15d023

Browse files
committed
Rating fixes for issues noted during MM beta test
1 parent 5966842 commit c15d023

2 files changed

Lines changed: 102 additions & 18 deletions

File tree

src/ratings/mmRatingEngine.js

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -603,24 +603,40 @@ function resolveRatingPathSource (challenge) {
603603
return null
604604
}
605605

606+
/**
607+
* Determine whether a challenge has complete placement data for score ordering.
608+
* Partial placement backfills are common in older MM imports; if only some rows
609+
* have placements, aggregate scores are the only comparable value across all
610+
* participants.
611+
* @param {Array<Object>} participantRows challenge participant rows
612+
* @returns {boolean} true when every participant has a positive placement
613+
*/
614+
function shouldUsePlacementScores (participantRows) {
615+
return (participantRows || []).length > 0 &&
616+
(participantRows || []).every(row => !!toOptionalPlacement(row && row.placement))
617+
}
618+
606619
/**
607620
* Normalize one Marathon Match score for Qubits ordering.
608-
* Source placements are authoritative final standings when present. Otherwise,
621+
* Complete source placements are authoritative final standings. Otherwise,
609622
* relative-scoring aggregates are treated as higher-is-better, while
610623
* non-relative MINIMIZE challenges need inversion to match standings order.
624+
* Placement is only a fallback when the row does not have an aggregate score.
611625
* @param {Object} row participant result row
612626
* @param {Object} scoringConfig relative scoring configuration for the challenge
627+
* @param {Object} [options] score normalization options
628+
* @param {boolean} [options.usePlacementScore=false] whether to rank by placement
613629
* @returns {number} normalized Qubits score
614630
*/
615-
function normalizeScore (row, scoringConfig) {
631+
function normalizeScore (row, scoringConfig, options = {}) {
616632
const placement = toOptionalPlacement(row && row.placement)
617-
if (placement) {
633+
if (options.usePlacementScore && placement) {
618634
return -placement
619635
}
620636

621637
const aggregateScore = Number(row.aggregateScore)
622638
if (!Number.isFinite(aggregateScore)) {
623-
return 0
639+
return placement ? -placement : 0
624640
}
625641

626642
if (
@@ -834,24 +850,18 @@ function computePlacementByCoderId (participants) {
834850

835851
/**
836852
* Build optional history metadata for the target participant.
837-
* Source-provided placements are preferred; score ordering is the fallback for
838-
* historical MM submissions that do not expose a stored placement.
853+
* Placement is computed from the same normalized scores used by the rating run,
854+
* with the source placement kept as a fallback when computed placement is absent.
839855
* @param {Array<Object>} participants rated participant states for one challenge
840856
* @param {string} targetUserKey normalized target user id
841857
* @param {Object} sourceRow source participant row for the target user
842858
* @returns {Object} persisted memberStatsHistory metadata
843859
*/
844860
function buildHistoryResultFields (participants, targetUserKey, sourceRow) {
845-
const sourcePlacement = toOptionalPlacement(sourceRow && sourceRow.placement)
846-
if (sourcePlacement) {
847-
return {
848-
placement: sourcePlacement
849-
}
850-
}
851-
852861
const computedPlacement = computePlacementByCoderId(participants).get(String(targetUserKey))
862+
const sourcePlacement = toOptionalPlacement(sourceRow && sourceRow.placement)
853863
return omitUndefinedFields({
854-
placement: computedPlacement
864+
placement: computedPlacement || sourcePlacement
855865
})
856866
}
857867

@@ -1650,14 +1660,15 @@ function resolveRatingPathParticipantId (row, source) {
16501660
* @param {Object} row participant source row
16511661
* @param {string} source rating path source
16521662
* @param {Object} scoringConfig MM scoring config when source is Marathon Match
1663+
* @param {Object} [options] score normalization options
16531664
* @returns {number} normalized Qubits score
16541665
*/
1655-
function normalizeRatingPathScore (row, source, scoringConfig) {
1666+
function normalizeRatingPathScore (row, source, scoringConfig, options = {}) {
16561667
if (source === RATING_PATH_SOURCE_DEVELOPMENT) {
16571668
return normalizeDevelopmentScore(row)
16581669
}
16591670

1660-
return normalizeScore(row, scoringConfig)
1671+
return normalizeScore(row, scoringConfig, options)
16611672
}
16621673

16631674
/**
@@ -1729,6 +1740,8 @@ async function rerateMmRatingPath (membersClient, challengeClient, mmDbClient, r
17291740

17301741
ratingPathChallengesProcessed += 1
17311742

1743+
const usePlacementScore = historyEntry.source === RATING_PATH_SOURCE_MARATHON_MATCH &&
1744+
shouldUsePlacementScores(participantRows)
17321745
const targetStateBeforeRun = cloneState(stateByUserId.get(targetUserKey))
17331746
const participantRowsByUserId = new Map()
17341747
const participants = participantRows.map((row) => {
@@ -1741,7 +1754,7 @@ async function rerateMmRatingPath (membersClient, challengeClient, mmDbClient, r
17411754
rating: participantState.rating,
17421755
volatility: participantState.volatility,
17431756
numRatings: participantState.numRatings,
1744-
score: normalizeRatingPathScore(row, historyEntry.source, scoringConfig)
1757+
score: normalizeRatingPathScore(row, historyEntry.source, scoringConfig, { usePlacementScore })
17451758
}
17461759
})
17471760

@@ -1952,6 +1965,7 @@ async function rerateMmTrack (membersClient, challengeClient, mmDbClient, review
19521965
}
19531966

19541967
const participantIds = participantRows.map((row) => toBigIntUserId(row.memberId))
1968+
const usePlacementScore = shouldUsePlacementScores(participantRows)
19551969
const missingParticipantIds = participantIds.filter((participantId) => !stateByUserId.has(buildUserStateKey(participantId)))
19561970
if (missingParticipantIds.length > 0) {
19571971
await loadParticipantHistoryCache(membersClient, missingParticipantIds, participantHistoryByUserId, dimensionIds)
@@ -1975,7 +1989,7 @@ async function rerateMmTrack (membersClient, challengeClient, mmDbClient, review
19751989
rating: participantState.rating,
19761990
volatility: participantState.volatility,
19771991
numRatings: participantState.numRatings,
1978-
score: normalizeScore(row, scoringConfig)
1992+
score: normalizeScore(row, scoringConfig, { usePlacementScore })
19791993
}
19801994
})
19811995

test/unit/MmRatingEngine.test.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,76 @@ describe('marathon match rating engine unit tests', () => {
972972
should.equal(historyRow.placement, 1)
973973
})
974974

975+
it('rerateMmTrack should use aggregate scores when MM placement data is partial', async () => {
976+
const thirdUserId = toBigInt(7007)
977+
const { client: membersClient, state } = createMembersClient({
978+
historyRows: [],
979+
statsRows: [],
980+
maxRatingRows: []
981+
})
982+
const mixedPlacementRows = [
983+
{
984+
submissionId: 'submission-target',
985+
memberId: targetUserId,
986+
challengeId,
987+
aggregateScore: 80,
988+
reviewedDate: new Date('2024-06-01T10:00:00.000Z'),
989+
createdAt: new Date('2024-06-01T10:00:00.000Z'),
990+
submissionCreatedAt: new Date('2024-06-01T09:00:00.000Z')
991+
},
992+
{
993+
submissionId: 'submission-opponent',
994+
memberId: opponentUserId,
995+
challengeId,
996+
placement: 1,
997+
aggregateScore: 100,
998+
reviewedDate: new Date('2024-06-01T10:05:00.000Z'),
999+
createdAt: new Date('2024-06-01T10:05:00.000Z'),
1000+
submissionCreatedAt: new Date('2024-06-01T09:05:00.000Z')
1001+
},
1002+
{
1003+
submissionId: 'submission-third',
1004+
memberId: thirdUserId,
1005+
challengeId,
1006+
placement: 2,
1007+
aggregateScore: 90,
1008+
reviewedDate: new Date('2024-06-01T10:10:00.000Z'),
1009+
createdAt: new Date('2024-06-01T10:10:00.000Z'),
1010+
submissionCreatedAt: new Date('2024-06-01T09:10:00.000Z')
1011+
}
1012+
]
1013+
const expectedParticipants = [
1014+
createParticipant(targetUserId, 0, 0, 0, 80),
1015+
createParticipant(opponentUserId, 0, 0, 0, 100),
1016+
createParticipant(thirdUserId, 0, 0, 0, 90)
1017+
]
1018+
runQubitsRating(expectedParticipants)
1019+
const expectedTargetState = expectedParticipants.find((participant) => participant.coderId === String(targetUserId))
1020+
1021+
const result = await rerateMmTrack(
1022+
membersClient,
1023+
createChallengeClient(challengeMetadata),
1024+
null,
1025+
createMmReviewDbClient(mixedPlacementRows),
1026+
targetUserId,
1027+
challengeId
1028+
)
1029+
1030+
should.equal(result.challengesProcessed, 1)
1031+
should.equal(result.ratingsUpdated, 1)
1032+
1033+
const statsRow = state.statsRows.find((row) =>
1034+
String(row.userId) === String(targetUserId) &&
1035+
row.trackId === DATA_SCIENCE_TRACK_ID &&
1036+
row.typeId === MARATHON_MATCH_TYPE_ID
1037+
)
1038+
const historyRow = findHistoryRow(state.historyRows, targetUserId, challengeId)
1039+
1040+
should.equal(statsRow.rating, expectedTargetState.rating)
1041+
should.equal(historyRow.newRating, expectedTargetState.rating)
1042+
should.equal(historyRow.placement, 3)
1043+
})
1044+
9751045
it('rerateMmTrack should skip MM challenges with challenge metadata isRated false', async () => {
9761046
const { client: membersClient, state } = createMembersClient({
9771047
historyRows: [],

0 commit comments

Comments
 (0)