Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added main script that allows for Calendar integration, and misc. changes #37

Merged
merged 5 commits into from
Jul 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ pip-delete-this-directory.txt
.pytest_cache/

# Mypy (static type checking)
.mypy_cache/
.mypy_cache/

# Credentials for calendar
credentials.json
token.pickle
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,28 @@

[![Build Status](https://travis-ci.com/varunvora/alcoding.svg?branch=master)](https://travis-ci.com/varunvora/alcoding)

Alcoding Club of [PES University](https://pes.edu/) maintains ratings of its students who are active in [competitive programming](https://en.wikipedia.org/wiki/Competitive_programming). This repository contains the ratings and the code which generates it.
Alcoding Club of [PES University](https://pes.edu/) maintains ratings of its students who are active in [competitive programming](https://en.wikipedia.org/wiki/Competitive_programming). This repository contains the ratings and the code that generates it.

## Purpose
An intra-college rating is maintained so that the club can identify good coders. The club will group these students and help them improve at competitive programming by organizing meet-ups, providing resources, arranging contests and develop a coding community in the University.
An intra-college rating is maintained to aid the club in identifying good coders. The club aims to help these students improve their competitive programming skills by organizing meet-ups, providing resources, arranging contests and developing a coding community in the University.


## Ratings
The ratings are calculated by students' performances in [specified contests](database/README.md).
The ratings are calculated using students' performances in [specified contests](database/README.md).

### Mechanism
A [rank list](database/contest_ranks) of registered students is generated at the end of each contest. A rating is computed from the rank list, which indicates their relative performance. The implementation is almost the same as [Codechef's Rating Mechanism](https://www.codechef.com/ratings) which is a modified version of [Elo rating system](https://en.wikipedia.org/wiki/Elo_rating_system). To avoid students from [protecting their ratings](https://en.wikipedia.org/wiki/Elo_rating_system#Game_activity_versus_protecting_one's_rating) and encourage participation, a decay rule is also added which decrements a student's rating by 1% if she does not take part in 5 consecutive contests.
A [rank list](database/contest_ranks) of registered students is generated at the end of each contest. A rating is computed from the rank list, which indicates their relative performance. The implementation is very similar to [Codechef's Rating Mechanism](https://www.codechef.com/ratings) which is a modified version of the [Elo rating system](https://en.wikipedia.org/wiki/Elo_rating_system). To prevent students from [protecting their ratings](https://en.wikipedia.org/wiki/Elo_rating_system#Game_activity_versus_protecting_one's_rating) and encourage participation, a decay rule, which decrements a student's rating by 1% if they do not take part in 5 consecutive rated contests, is also added.


### Verification
The [code that generates the rating](ratings/processor.py) is open. Along with that we have provided [a script with which you can verify](executor.sh) that the displayed ratings are correct. This script resets all students' ratings, and computes the ratings after all the contest ranks are considered. You may [report an issue](https://github.com/varunvora/alcoding/issues) if you find any discrepancy.
The [code that generates the rating](ratings/processor.py) is open. Further, we also provide [a method with which you can verify](run.py) the displayed ratings. This method resets all students' ratings, and recomputes the ratings of every student after considering all contest ranks. Please do [report an issue](https://github.com/pes-alcoding-club/student-ratings/issues) if you find any discrepancy.

## Calendar
Alcoding Club maintains a [Google calendar for competitive programming](https://calendar.google.com/calendar?cid=N3RsZGt1dXEwcW1mOW9ub2Jxb3ByZ2Z1cDRAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ). Contests that are marked as "Rated" will be considered for these ratings.
Alcoding Club maintains a [Google Calendar for competitive programming](https://calendar.google.com/calendar?cid=N3RsZGt1dXEwcW1mOW9ub2Jxb3ByZ2Z1cDRAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ). Contests that are marked "Rated" will be considered for these ratings.
## Contribute
This project is still very small so there are no strict guidelines for contribution. For now we are following [PEP 8 -- Style Guide for Python Code](https://www.python.org/dev/peps/pep-0008/).
At the moment, there are no strict guidelines for contribution. As a standard, we follow the [PEP 8 -- Style Guide for Python Code](https://www.python.org/dev/peps/pep-0008/).

You can [report an issue](https://github.com/varunvora/alcoding/issues) if you find a bug or any other change you would like to make. You may also make a [pull request](https://github.com/varunvora/alcoding/pulls). It would be helpful if you use [our Github labels](https://github.com/varunvora/alcoding/labels) for all issues and pull requests. Be sure to clearly document and describe any issues or changes.
Feel free to [report an issue](https://github.com/pes-alcoding-club/student-ratings/issues) if you find a bug, or have any other change you would like to see. You may also create a [pull request](https://github.com/pes-alcoding-club/student-ratings/pulls). It would be helpful if you use [our Github labels](https://github.com/pes-alcoding-club/student-ratings/labels) for all issues and pull requests. Be sure to clearly document and describe any issues or changes.

## FAQ

Expand All @@ -37,11 +37,11 @@ You can [report an issue](https://github.com/varunvora/alcoding/issues) if you f
1. Which contests are taken into account for rating?

Contests in ['Competitive Programming PESU' Calendar](https://calendar.google.com/calendar?cid=N3RsZGt1dXEwcW1mOW9ub2Jxb3ByZ2Z1cDRAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ) are considered for ratings.
1. How can I tell if these ratings are legitimate?
1. How can I tell whether these ratings are legitimate?

You can verify the ratings yourself by running [this script](executor.sh). It resets all students' ratings to default values and recomputes it for all contests so far in chronological order.

1. How can I get the scoreboard only for some particular contest(s)?

Clone this repository, open [executor.sh](executor.sh) and remove the contests you do not want the scoreboard for. Run this script and check [scoreboard.csv](scoreboard.csv).
You can verify the ratings yourself by calling the [make_scoreboard] function in [run.py](run.py). It resets all students' ratings to default values and recomputes it for all contests so far in chronological order.

1. How can I make a scoreboard for a few particular contests?
Firstly, clone this repository.
Create your own [contest_names_file.in](database/contest_names_file.in) and add the contest names in the format [platform]-[month]-[contest_code]. In [run.py](run.py), change the [contest_names_file_path] variable's value to your file's path.
Now call the [make_scoreboard] function in [run.py](run.py) with the required parameters and check [scoreboard.csv](scoreboard.csv).
File renamed without changes.
28 changes: 15 additions & 13 deletions database/db_tools.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import re
import sys
import csv
import logging
from os import listdir
from os.path import join
from collections import Counter
from functools import lru_cache
from typing import List, Set, Tuple, Dict, Callable, Any
from tinydb import TinyDB, where
from ratings import elo
from utils import log

DB_FILE: str = 'database/db.json'
CONTEST_RANKS_DIR: str = 'database/contest_ranks'
Expand Down Expand Up @@ -57,7 +57,7 @@ def reset_database(db_file: str = DB_FILE) -> None:
BEST: elo.DEFAULT_RATING,
TIMES_PLAYED: 0,
LAST_FIVE: 5})
logging.info(f'Successfully reset database and stored in {db_file}')
log.info(f'Successfully reset database and stored in {db_file}')


def get_site_name_from_file_name(file_name: str) -> str:
Expand All @@ -68,8 +68,8 @@ def get_site_name_from_file_name(file_name: str) -> str:
"""
file_name_parts = file_name.split("-")
if len(file_name_parts) < 2 or file_name_parts[0] not in SITES:
logging.error(f"Invalid filename '{file_name}' in contest ranks. File name convention is"
f"'site-contest-details.in'")
log.error(f"Invalid filename '{file_name}' in contest ranks. File name convention is"
f"'site-month-contestCode.in'")
quit()
return file_name_parts[0]

Expand Down Expand Up @@ -144,18 +144,18 @@ def log_unmapped_handles(site_username_tuple_list: List[Tuple[str, str]]) -> Non

log_unmapped_handles(site_handle_tuple_list)

logging.info('Mapped ')
log.info('Mapped usernames to SRNs')


def remove_unmapped_handles_from_rank_file(file_name: str) -> None:
"""
Removes unmapped handles from outdated rank files
to reduce space and time it takes for the script to run
"""
with open(join(CONTEST_RANKS_DIR, file_name), 'r') as rank_file:
with open(file_name, 'r') as rank_file:
input_data: str = rank_file.read()

with open(join(CONTEST_RANKS_DIR, file_name), 'w') as rank_file:
count = 0
with open(file_name, 'w') as rank_file:
for user_name_line in input_data.split("\n"):
check_occurrence_in_line: bool = False
for user_name in user_name_line.split():
Expand All @@ -164,7 +164,9 @@ def remove_unmapped_handles_from_rank_file(file_name: str) -> None:
rank_file.write(user_name + " ")
if check_occurrence_in_line:
rank_file.write("\n")
logging.info(f'Cleaned {file_name}')
count+=1
loginfo = file_name.split('/')[2]
log.info(f'Cleaned {loginfo}')


def export_to_csv(db_file: str = DB_FILE, scoreboard_file: str = SCOREBOARD_FILE) -> None:
Expand All @@ -190,7 +192,7 @@ def export_to_csv(db_file: str = DB_FILE, scoreboard_file: str = SCOREBOARD_FILE
wr = csv.writer(fp)
wr.writerows(csv_table)

logging.info(f'Successfully exported database from {db_file} to {scoreboard_file}')
log.info(f'Successfully exported database from {db_file} to {scoreboard_file}')


def prettify(db_file: str = DB_FILE) -> None:
Expand All @@ -201,11 +203,11 @@ def prettify(db_file: str = DB_FILE) -> None:
fp.write_back(fp.all())


if __name__ == "__main__":
'''if __name__ == "__main__":
# While executing this script, you can specify which function to execute
func_str: str = sys.argv[1]
try:
func_obj: Callable = globals()[func_str]
func_obj(*sys.argv[2:]) # Arguments to specified function can be passed
except KeyError:
logging.error(f'Provided invalid argument. No function {func_str}')
except KeyError:'
log.error(f'Provided invalid argument. No function {func_str}')'''
58 changes: 36 additions & 22 deletions ratings/processor.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import sys
import logging
from time import time
from ratings import elo
from database import db_tools as db
from tinydb import TinyDB, where

from utils import log

class RatingProcessor:

def __init__(self, database: TinyDB, rank_file):
def __init__(self, database: TinyDB, rank_file_path):
self.database: TinyDB = database

self.N: int = 0
self.Cf: float = 0.0
self.Rb_Vb_list: list = []
self.usn_rank_dict: dict = {}
self.rank_file_path = rank_file_path
self.rank_file = open(rank_file_path)

self.read_contest_ranks(rank_file) # sets usn_rank_dict
self.read_contest_ranks(self.rank_file) # sets usn_rank_dict
self.set_contest_details() # sets N, Cf and Rb_Vb_list
self.process_competition() # uses the set attributes to compute new ratings

Expand All @@ -35,9 +36,9 @@ def read_contest_ranks(self, rank_file) -> None:
self.usn_rank_dict[usn] = current_rank
same_rank_count += 1
else:
logging.info(f'Ignoring usn {usn}')
log.info(f'Ignoring SRN {usn}')
current_rank += same_rank_count # ranks are not 1, 1, 1, 2 but 1, 1, 1, 4
logging.debug(self.usn_rank_dict)
log.debug(self.usn_rank_dict)

def set_contest_details(self) -> None:
"""
Expand All @@ -54,12 +55,12 @@ def set_contest_details(self) -> None:
self.N = len(self.usn_rank_dict)
self.Cf = elo.Cf(rating_list, vol_list, self.N)
self.Rb_Vb_list = list(zip(rating_list, vol_list))
logging.debug(f'Contest: {rank_file_path}\nPlayers: {self.N}\nCompetition Factor: {self.Cf}')
log.debug(f'Contest: {self.rank_file_path}\nPlayers: {self.N}\nCompetition Factor: {self.Cf}')

@staticmethod
def _decay_player(player_dict: dict) -> None:
"""
Reduces ratings by 10% for those who have competed at least once
Reduces ratings by 1% for those who have competed at least once
but have not taken part in the past 5 contests
:param player_dict: dict with all details of a player
"""
Expand All @@ -76,7 +77,7 @@ def _decay_player(player_dict: dict) -> None:
player_dict[db.RATING] = rating
player_dict[db.LAST_FIVE] = max(1, last_five)

logging.debug('Successfully decayed ratings')
log.debug('Successfully decayed ratings')

def _update_player(self, player_dict: dict, actual_rank: int) -> None:
"""
Expand All @@ -99,27 +100,27 @@ def _update_player(self, player_dict: dict, actual_rank: int) -> None:
player_dict[db.BEST] = max(old_best, new_rating)
player_dict[db.LAST_FIVE] = 5

logging.debug('Successfully updated ratings')
log.debug('Successfully updated ratings')

def process_competition(self) -> None:

rows = self.database.all()
for row in rows:
logging.debug(f'Before: {row}')
log.debug(f'Before: {row}')
if row[db.USN] in self.usn_rank_dict:
actual_rank = self.usn_rank_dict[row[db.USN]]
self._update_player(row, actual_rank)
else:
self._decay_player(row)
logging.debug(f'After: {row}')
log.debug(f'After: {row}')
self.database.write_back(rows)


def read_argv(argv_format_alert: str):
"""
"""def read_argv(argv_format_alert: str):
'''
:param argv_format_alert: An error message on what the command line arguments should be
:return: rank file if argv is valid
"""
'''
try:
assert len(sys.argv) == 2
rank_file = sys.argv[1]
Expand All @@ -128,15 +129,28 @@ def read_argv(argv_format_alert: str):
return rank_file

except IOError or FileNotFoundError:
logging.error(f'Invalid file path for rank file: {rank_file}\n{argv_format_alert}')
error(f'Invalid file path for rank file: {rank_file}\n{argv_format_alert}')
quit()

except AssertionError:
logging.error(f'Invalid command line arguments.\n{argv_format_alert}')
quit()
error(f'Invalid command line arguments.\n{argv_format_alert}')
quit()"""

def process(rank_file_path):
start_time = time()
# Main logic starts here
database_obj = TinyDB(db.DB_FILE)
RatingProcessor(database_obj, rank_file_path)
database_obj.close()

if __name__ == "__main__":
duration = time()-start_time
log.debug(f'Updated ratings for {rank_file_path}')
if duration > 10:
log.critical(f'Ratings update for {rank_file_path} took {duration} seconds.\n'
f'Consider removing unnecessary handles or optimize ratings algorithm')


'''if __name__ == "__main__":
start_time = time()

argv_format = 'processor.py rank_file_path'
Expand All @@ -149,7 +163,7 @@ def read_argv(argv_format_alert: str):
database_obj.close()

duration = time()-start_time
logging.debug(f'Updated ratings for {rank_file_path}')
log.debug(f'Updated ratings for {rank_file_path}')
if duration > 10:
logging.critical(f'Ratings update for {rank_file_path} took {duration} seconds.\n'
f'Consider removing unnecessary handles or optimize ratings algorithm')
logging.log.critical(f'Ratings update for {rank_file_path} took {duration} seconds.\n'
f'Consider removing unnecessary handles or optimize ratings algorithm')'''
10 changes: 6 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
tinydb
requests
bs4
selenium
requests==2.22.0
beautifulsoup4==4.9.1
google_api_python_client==1.10.0
google_auth_oauthlib==0.4.1
selenium==3.141.0
tinydb==3.15.2
Loading