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):
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_on_bottom = actions_on_top = True
search_fields = ('contest__key', 'contest__name', 'user__user__username')
@ -292,7 +292,7 @@ class ContestParticipationAdmin(admin.ModelAdmin):
def get_queryset(self, request):
return super(ContestParticipationAdmin, self).get_queryset(request).only(
'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):

View file

@ -1,5 +1,6 @@
from judge.contest_format.atcoder import AtCoderContestFormat
from judge.contest_format.default import DefaultContestFormat
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.registry import choices, formats

View file

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

View file

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

View file

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

View file

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

View file

@ -1,12 +1,31 @@
import math
from bisect import bisect
from operator import itemgetter
from operator import attrgetter, itemgetter
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 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):
@ -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
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
# 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:
new_volatility[i] = math.sqrt(((new_rating[i] - old_rating[i]) ** 2) / Weight +
(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 old_rating[i] < new_rating[i]:
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):
from judge.models import Rating, Profile
cursor = connection.cursor()
cursor.execute('''
SELECT judge_rating.user_id, judge_rating.rating, judge_rating.volatility, r.times
FROM judge_rating INNER JOIN
judge_contest ON (judge_contest.id = judge_rating.contest_id) INNER JOIN (
SELECT judge_rating.user_id AS id, MAX(judge_contest.end_time) AS last_time,
COUNT(judge_rating.user_id) AS times
FROM judge_contestparticipation INNER JOIN
judge_rating ON (judge_rating.user_id = judge_contestparticipation.user_id) INNER JOIN
judge_contest ON (judge_contest.id = judge_rating.contest_id)
WHERE judge_contestparticipation.contest_id = %s AND judge_contest.end_time < %s AND
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')
rating_subquery = Rating.objects.filter(user=OuterRef('user'))
rating_sorted = rating_subquery.order_by('-contest__end_time')
users = contest.users.order_by('is_disqualified', '-score', 'cumtime', 'tiebreaker') \
.annotate(submissions=Count('submission'),
last_rating=Coalesce(Subquery(rating_sorted.values('rating')[:1]), 1200),
volatility=Coalesce(Subquery(rating_sorted.values('volatility')[:1]), 535),
times=Coalesce(Subquery(rating_subquery.order_by().values('user_id')
.annotate(count=Count('id')).values('count')), 0)) \
.exclude(user_id__in=contest.rate_exclude.all()) \
.filter(virtual=0).values('id', 'user_id', 'score', 'cumtime', 'tiebreaker', 'is_disqualified',
'last_rating', 'volatility', 'times')
if not contest.rate_all:
users = users.filter(submissions__gt=0)
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:
users = users.exclude(user__rating__gt=contest.rating_ceiling)
users = list(tie_ranker(users, key=itemgetter(2, 3)))
participation_ids = [user[1][0] for user in users]
user_ids = [user[1][1] for user in users]
ranking = list(map(itemgetter(0), users))
old_data = [data.get(user, (1200, 535, 0)) for user in user_ids]
old_rating = list(map(itemgetter(0), old_data))
old_volatility = list(map(itemgetter(1), old_data))
times_ranked = list(map(itemgetter(2), old_data))
rating, volatility = recalculate_ratings(old_rating, old_volatility, ranking, times_ranked)
users = users.exclude(last_rating__gt=contest.rating_ceiling)
users = list(users)
participation_ids = list(map(itemgetter('id'), users))
user_ids = list(map(itemgetter('user_id'), users))
is_disqualified = list(map(itemgetter('is_disqualified'), users))
ranking = list(tie_ranker(users, key=itemgetter('score', 'cumtime', 'tiebreaker')))
old_rating = list(map(itemgetter('last_rating'), users))
old_volatility = list(map(itemgetter('volatility'), users))
times_ranked = list(map(itemgetter('times'), users))
rating, volatility = recalculate_ratings(old_rating, old_volatility, ranking, times_ranked, is_disqualified)
now = timezone.now()
ratings = [Rating(user_id=id, 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)]
ratings = [Rating(user_id=i, contest=contest, rating=r, volatility=v, last_rated=now, participation_id=p, rank=z)
for i, p, r, v, z in zip(user_ids, participation_ids, rating, volatility, ranking)]
cursor = connection.cursor()
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)))
@ -178,4 +191,4 @@ def rating_progress(rating):
return 1.0
prev = 0 if not level else RATING_VALUES[level - 1]
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
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)
problems = list(contest.contest_problems.select_related('problem')
.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')
.annotate(username=F('user__user__username'))
.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()
contest_history = {}
if not profile.is_unlisted:
participations = ContestParticipation.objects.filter(user=profile, virtual=0, contest__is_visible=True,
contest__is_private=False,
contest__is_organization_private=False)
for contest_key, rating, volatility in participations.values_list('contest__key', 'rating__rating',
'rating__volatility'):
contest_history[contest_key] = {
'rating': rating,
'volatility': volatility,
}
participations = ContestParticipation.objects.filter(user=profile, virtual=0, contest__is_visible=True,
contest__is_private=False,
contest__is_organization_private=False)
for contest_key, rating, volatility in participations.values_list('contest__key', 'rating__rating',
'rating__volatility'):
contest_history[contest_key] = {
'rating': rating,
'volatility': volatility,
}
resp['contests'] = {
'current_rating': last_rating.rating if last_rating else None,

View file

@ -591,7 +591,7 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
ContestRankingProfile = namedtuple(
'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',
)
@ -607,6 +607,7 @@ def make_contest_ranking_profile(contest, participation, contest_problems):
username=user.username,
points=participation.score,
cumtime=participation.cumtime,
tiebreaker=participation.tiebreaker,
organization=user.organization,
participation_rating=participation.rating.rating if hasattr(participation, 'rating') else None,
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):
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')
.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,
show_current_virtual=True, ranker=ranker):
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 participation is None and request.user.is_authenticated: