Fix rerate logic (DMOJ)
This commit is contained in:
parent
297b8a2a36
commit
b45de198ba
10 changed files with 77 additions and 78 deletions
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -91,6 +91,7 @@ class AtCoderContestFormat(DefaultContestFormat):
|
|||
|
||||
participation.cumtime = cumtime + penalty
|
||||
participation.score = points
|
||||
participation.tiebreaker = 0
|
||||
participation.format_data = format_data
|
||||
participation.save()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -92,6 +92,7 @@ class ECOOContestFormat(DefaultContestFormat):
|
|||
|
||||
participation.cumtime = cumtime
|
||||
participation.score = points
|
||||
participation.tiebreaker = 0
|
||||
participation.format_data = format_data
|
||||
participation.save()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue