Fix rerate logic (DMOJ)

This commit is contained in:
cuom1999 2021-05-24 15:18:39 -05:00
parent 297b8a2a36
commit b45de198ba
10 changed files with 77 additions and 78 deletions

View file

@ -282,7 +282,7 @@ class ContestParticipationForm(ModelForm):
class ContestParticipationAdmin(admin.ModelAdmin): class ContestParticipationAdmin(admin.ModelAdmin):
fields = ('contest', 'user', 'real_start', 'virtual', 'is_disqualified') fields = ('contest', 'user', 'real_start', 'virtual', 'is_disqualified')
list_display = ('contest', 'username', 'show_virtual', 'real_start', 'score', 'cumtime') list_display = ('contest', 'username', 'show_virtual', 'real_start', 'score', 'cumtime', 'tiebreaker')
actions = ['recalculate_results'] actions = ['recalculate_results']
actions_on_bottom = actions_on_top = True actions_on_bottom = actions_on_top = True
search_fields = ('contest__key', 'contest__name', 'user__user__username') search_fields = ('contest__key', 'contest__name', 'user__user__username')
@ -292,7 +292,7 @@ class ContestParticipationAdmin(admin.ModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
return super(ContestParticipationAdmin, self).get_queryset(request).only( return super(ContestParticipationAdmin, self).get_queryset(request).only(
'contest__name', 'contest__format_name', 'contest__format_config', 'contest__name', 'contest__format_name', 'contest__format_config',
'user__user__username', 'real_start', 'score', 'cumtime', 'virtual', 'user__user__username', 'real_start', 'score', 'cumtime', 'tiebreaker', 'virtual',
) )
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):

View file

@ -1,5 +1,6 @@
from judge.contest_format.atcoder import AtCoderContestFormat from judge.contest_format.atcoder import AtCoderContestFormat
from judge.contest_format.default import DefaultContestFormat from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.ecoo import ECOOContestFormat from judge.contest_format.ecoo import ECOOContestFormat
from judge.contest_format.icpc import ICPCContestFormat
from judge.contest_format.ioi import IOIContestFormat from judge.contest_format.ioi import IOIContestFormat
from judge.contest_format.registry import choices, formats from judge.contest_format.registry import choices, formats

View file

@ -91,6 +91,7 @@ class AtCoderContestFormat(DefaultContestFormat):
participation.cumtime = cumtime + penalty participation.cumtime = cumtime + penalty
participation.score = points participation.score = points
participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()

View file

@ -41,6 +41,7 @@ class DefaultContestFormat(BaseContestFormat):
participation.cumtime = max(cumtime, 0) participation.cumtime = max(cumtime, 0)
participation.score = points participation.score = points
participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()

View file

@ -92,6 +92,7 @@ class ECOOContestFormat(DefaultContestFormat):
participation.cumtime = cumtime participation.cumtime = cumtime
participation.score = points participation.score = points
participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()

View file

@ -73,6 +73,7 @@ class IOIContestFormat(DefaultContestFormat):
participation.cumtime = max(cumtime, 0) participation.cumtime = max(cumtime, 0)
participation.score = points participation.score = points
participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()

View file

@ -1,12 +1,31 @@
import math import math
from bisect import bisect from bisect import bisect
from operator import itemgetter from operator import attrgetter, itemgetter
from django.db import connection, transaction from django.db import connection, transaction
from django.db.models import Count from django.db.models import Count, OuterRef, Subquery
from django.db.models.functions import Coalesce
from django.utils import timezone from django.utils import timezone
from judge.utils.ranker import tie_ranker
def tie_ranker(iterable, key=attrgetter('points')):
rank = 0
delta = 1
last = None
buf = []
for item in iterable:
new = key(item)
if new != last:
for _ in buf:
yield rank + (delta - 1) / 2.0
rank += delta
delta = 0
buf = []
delta += 1
buf.append(item)
last = key(item)
for _ in buf:
yield rank + (delta - 1) / 2.0
def rational_approximation(t): def rational_approximation(t):
@ -35,7 +54,7 @@ def WP(RA, RB, VA, VB):
return (math.erf((RB - RA) / math.sqrt(2 * (VA * VA + VB * VB))) + 1) / 2.0 return (math.erf((RB - RA) / math.sqrt(2 * (VA * VA + VB * VB))) + 1) / 2.0
def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated): def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated, is_disqualified):
# actual_rank: 1 is first place, N is last place # actual_rank: 1 is first place, N is last place
# if there are ties, use the average of places (if places 2, 3, 4, 5 tie, use 3.5 for all of them) # if there are ties, use the average of places (if places 2, 3, 4, 5 tie, use 3.5 for all of them)
@ -76,6 +95,12 @@ def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated):
else: else:
new_volatility[i] = math.sqrt(((new_rating[i] - old_rating[i]) ** 2) / Weight + new_volatility[i] = math.sqrt(((new_rating[i] - old_rating[i]) ** 2) / Weight +
(old_volatility[i] ** 2) / (Weight + 1)) (old_volatility[i] ** 2) / (Weight + 1))
if is_disqualified[i]:
# DQed users can manipulate TopCoder ratings to get higher volatility in order to increase their rating
# later on, prohibit this by ensuring their volatility never increases in this situation
new_volatility[i] = min(new_volatility[i], old_volatility[i])
if abs(old_rating[i] - new_rating[i]) > Cap: if abs(old_rating[i] - new_rating[i]) > Cap:
if old_rating[i] < new_rating[i]: if old_rating[i] < new_rating[i]:
new_rating[i] = old_rating[i] + Cap new_rating[i] = old_rating[i] + Cap
@ -96,49 +121,37 @@ def recalculate_ratings(old_rating, old_volatility, actual_rank, times_rated):
def rate_contest(contest): def rate_contest(contest):
from judge.models import Rating, Profile from judge.models import Rating, Profile
cursor = connection.cursor() rating_subquery = Rating.objects.filter(user=OuterRef('user'))
cursor.execute(''' rating_sorted = rating_subquery.order_by('-contest__end_time')
SELECT judge_rating.user_id, judge_rating.rating, judge_rating.volatility, r.times users = contest.users.order_by('is_disqualified', '-score', 'cumtime', 'tiebreaker') \
FROM judge_rating INNER JOIN .annotate(submissions=Count('submission'),
judge_contest ON (judge_contest.id = judge_rating.contest_id) INNER JOIN ( last_rating=Coalesce(Subquery(rating_sorted.values('rating')[:1]), 1200),
SELECT judge_rating.user_id AS id, MAX(judge_contest.end_time) AS last_time, volatility=Coalesce(Subquery(rating_sorted.values('volatility')[:1]), 535),
COUNT(judge_rating.user_id) AS times times=Coalesce(Subquery(rating_subquery.order_by().values('user_id')
FROM judge_contestparticipation INNER JOIN .annotate(count=Count('id')).values('count')), 0)) \
judge_rating ON (judge_rating.user_id = judge_contestparticipation.user_id) INNER JOIN .exclude(user_id__in=contest.rate_exclude.all()) \
judge_contest ON (judge_contest.id = judge_rating.contest_id) .filter(virtual=0).values('id', 'user_id', 'score', 'cumtime', 'tiebreaker', 'is_disqualified',
WHERE judge_contestparticipation.contest_id = %s AND judge_contest.end_time < %s AND 'last_rating', 'volatility', 'times')
judge_contestparticipation.user_id NOT IN (
SELECT profile_id FROM judge_contest_rate_exclude WHERE contest_id = %s
) AND judge_contestparticipation.virtual = 0
GROUP BY judge_rating.user_id
ORDER BY judge_contestparticipation.score DESC, judge_contestparticipation.cumtime ASC
) AS r ON (judge_rating.user_id = r.id AND judge_contest.end_time = r.last_time)
''', (contest.id, contest.end_time, contest.id))
data = {user: (rating, volatility, times) for user, rating, volatility, times in cursor.fetchall()}
cursor.close()
users = contest.users.order_by('is_disqualified', '-score', 'cumtime').annotate(submissions=Count('submission')) \
.exclude(user_id__in=contest.rate_exclude.all()).filter(virtual=0, user__is_unlisted=False) \
.values_list('id', 'user_id', 'score', 'cumtime')
if not contest.rate_all: if not contest.rate_all:
users = users.filter(submissions__gt=0) users = users.filter(submissions__gt=0)
if contest.rating_floor is not None: if contest.rating_floor is not None:
users = users.exclude(user__rating__lt=contest.rating_floor) users = users.exclude(last_rating__lt=contest.rating_floor)
if contest.rating_ceiling is not None: if contest.rating_ceiling is not None:
users = users.exclude(user__rating__gt=contest.rating_ceiling) users = users.exclude(last_rating__gt=contest.rating_ceiling)
users = list(tie_ranker(users, key=itemgetter(2, 3)))
participation_ids = [user[1][0] for user in users] users = list(users)
user_ids = [user[1][1] for user in users] participation_ids = list(map(itemgetter('id'), users))
ranking = list(map(itemgetter(0), users)) user_ids = list(map(itemgetter('user_id'), users))
old_data = [data.get(user, (1200, 535, 0)) for user in user_ids] is_disqualified = list(map(itemgetter('is_disqualified'), users))
old_rating = list(map(itemgetter(0), old_data)) ranking = list(tie_ranker(users, key=itemgetter('score', 'cumtime', 'tiebreaker')))
old_volatility = list(map(itemgetter(1), old_data)) old_rating = list(map(itemgetter('last_rating'), users))
times_ranked = list(map(itemgetter(2), old_data)) old_volatility = list(map(itemgetter('volatility'), users))
rating, volatility = recalculate_ratings(old_rating, old_volatility, ranking, times_ranked) times_ranked = list(map(itemgetter('times'), users))
rating, volatility = recalculate_ratings(old_rating, old_volatility, ranking, times_ranked, is_disqualified)
now = timezone.now() now = timezone.now()
ratings = [Rating(user_id=id, contest=contest, rating=r, volatility=v, last_rated=now, participation_id=p, rank=z) ratings = [Rating(user_id=i, contest=contest, rating=r, volatility=v, last_rated=now, participation_id=p, rank=z)
for id, p, r, v, z in zip(user_ids, participation_ids, rating, volatility, ranking)] for i, p, r, v, z in zip(user_ids, participation_ids, rating, volatility, ranking)]
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute('CREATE TEMPORARY TABLE _profile_rating_update(id integer, rating integer)') cursor.execute('CREATE TEMPORARY TABLE _profile_rating_update(id integer, rating integer)')
cursor.executemany('INSERT INTO _profile_rating_update VALUES (%s, %s)', list(zip(user_ids, rating))) cursor.executemany('INSERT INTO _profile_rating_update VALUES (%s, %s)', list(zip(user_ids, rating)))
@ -178,4 +191,4 @@ def rating_progress(rating):
return 1.0 return 1.0
prev = 0 if not level else RATING_VALUES[level - 1] prev = 0 if not level else RATING_VALUES[level - 1]
next = RATING_VALUES[level] next = RATING_VALUES[level]
return (rating - prev + 0.0) / (next - prev) return (rating - prev + 0.0) / (next - prev)

View file

@ -13,22 +13,3 @@ def ranker(iterable, key=attrgetter('points'), rank=0):
yield rank, item yield rank, item
last = key(item) last = key(item)
def tie_ranker(iterable, key=attrgetter('points')):
rank = 0
delta = 1
last = None
buf = []
for item in iterable:
new = key(item)
if new != last:
for i in buf:
yield rank + (delta - 1) / 2.0, i
rank += delta
delta = 0
buf = []
delta += 1
buf.append(item)
last = key(item)
for i in buf:
yield rank + (delta - 1) / 2.0, i

View file

@ -35,7 +35,7 @@ def api_v1_contest_detail(request, contest):
can_see_rankings = contest.can_see_full_scoreboard(request.user) can_see_rankings = contest.can_see_full_scoreboard(request.user)
problems = list(contest.contest_problems.select_related('problem') problems = list(contest.contest_problems.select_related('problem')
.defer('problem__description').order_by('order')) .defer('problem__description').order_by('order'))
participations = (contest.users.filter(virtual=0, user__is_unlisted=False) participations = (contest.users.filter(virtual=0)
.prefetch_related('user__organizations') .prefetch_related('user__organizations')
.annotate(username=F('user__user__username')) .annotate(username=F('user__user__username'))
.order_by('-score', 'cumtime') if can_see_rankings else []) .order_by('-score', 'cumtime') if can_see_rankings else [])
@ -134,16 +134,15 @@ def api_v1_user_info(request, user):
last_rating = profile.ratings.last() last_rating = profile.ratings.last()
contest_history = {} contest_history = {}
if not profile.is_unlisted: participations = ContestParticipation.objects.filter(user=profile, virtual=0, contest__is_visible=True,
participations = ContestParticipation.objects.filter(user=profile, virtual=0, contest__is_visible=True, contest__is_private=False,
contest__is_private=False, contest__is_organization_private=False)
contest__is_organization_private=False) for contest_key, rating, volatility in participations.values_list('contest__key', 'rating__rating',
for contest_key, rating, volatility in participations.values_list('contest__key', 'rating__rating', 'rating__volatility'):
'rating__volatility'): contest_history[contest_key] = {
contest_history[contest_key] = { 'rating': rating,
'rating': rating, 'volatility': volatility,
'volatility': volatility, }
}
resp['contests'] = { resp['contests'] = {
'current_rating': last_rating.rating if last_rating else None, 'current_rating': last_rating.rating if last_rating else None,

View file

@ -591,7 +591,7 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
ContestRankingProfile = namedtuple( ContestRankingProfile = namedtuple(
'ContestRankingProfile', 'ContestRankingProfile',
'id user css_class username points cumtime organization participation ' 'id user css_class username points cumtime tiebreaker organization participation '
'participation_rating problem_cells result_cell', 'participation_rating problem_cells result_cell',
) )
@ -607,6 +607,7 @@ def make_contest_ranking_profile(contest, participation, contest_problems):
username=user.username, username=user.username,
points=participation.score, points=participation.score,
cumtime=participation.cumtime, cumtime=participation.cumtime,
tiebreaker=participation.tiebreaker,
organization=user.organization, organization=user.organization,
participation_rating=participation.rating.rating if hasattr(participation, 'rating') else None, participation_rating=participation.rating.rating if hasattr(participation, 'rating') else None,
problem_cells=[contest.format.display_user_problem(participation, contest_problem) problem_cells=[contest.format.display_user_problem(participation, contest_problem)
@ -622,17 +623,17 @@ def base_contest_ranking_list(contest, problems, queryset):
def contest_ranking_list(contest, problems): def contest_ranking_list(contest, problems):
return base_contest_ranking_list(contest, problems, contest.users.filter(virtual=0, user__is_unlisted=False) return base_contest_ranking_list(contest, problems, contest.users.filter(virtual=0)
.prefetch_related('user__organizations') .prefetch_related('user__organizations')
.extra(select={'round_score': 'round(score, 6)'}) .extra(select={'round_score': 'round(score, 6)'})
.order_by('is_disqualified', '-round_score', 'cumtime')) .order_by('is_disqualified', '-round_score', 'cumtime', 'tiebreaker'))
def get_contest_ranking_list(request, contest, participation=None, ranking_list=contest_ranking_list, def get_contest_ranking_list(request, contest, participation=None, ranking_list=contest_ranking_list,
show_current_virtual=True, ranker=ranker): show_current_virtual=True, ranker=ranker):
problems = list(contest.contest_problems.select_related('problem').defer('problem__description').order_by('order')) problems = list(contest.contest_problems.select_related('problem').defer('problem__description').order_by('order'))
users = ranker(ranking_list(contest, problems), key=attrgetter('points', 'cumtime')) users = ranker(ranking_list(contest, problems), key=attrgetter('points', 'cumtime', 'tiebreaker'))
if show_current_virtual: if show_current_virtual:
if participation is None and request.user.is_authenticated: if participation is None and request.user.is_authenticated: