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):
|
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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue