Ranking System v2 (#1186)

* Revise rank calculation

* Replace contributions by commits

* Lower average stats and S+ threshold

* Fix calculateRank.test.js

Missing key in dictionary constructor

Co-authored-by: Rick Staa <rick.staa@outlook.com>

* refactor: run prettier

* feat: change star weight to 0.75

* Separate PRs and issues

* Tweak weights

* Add count_private back

* fix: enable 'count_private' again

* test: fix tests

* refactor: improve code formatting

* Higher targets

---------

Co-authored-by: Rick Staa <rick.staa@outlook.com>
This commit is contained in:
François Rozet
2023-05-26 15:39:35 +02:00
committed by GitHub
co-authored by Rick Staa
parent ff2e02ba68
commit c96e84a9ae
7 changed files with 193 additions and 244 deletions
+51 -84
View File
@@ -1,105 +1,72 @@
/**
* Calculates the probability of x taking on x or a value less than x in a normal distribution
* with mean and standard deviation.
*
* @see https://stackoverflow.com/a/5263759/10629172
*
* @param {string} mean The mean of the normal distribution.
* @param {number} sigma The standard deviation of the normal distribution.
* @param {number} to The value to calculate the probability for.
* @returns {number} Probability.
*/
const normalcdf = (mean, sigma, to) => {
var z = (to - mean) / Math.sqrt(2 * sigma * sigma);
var t = 1 / (1 + 0.3275911 * Math.abs(z));
var a1 = 0.254829592;
var a2 = -0.284496736;
var a3 = 1.421413741;
var a4 = -1.453152027;
var a5 = 1.061405429;
var erf =
1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-z * z);
var sign = 1;
if (z < 0) {
sign = -1;
}
return (1 / 2) * (1 + sign * erf);
};
function expsf(x, lambda = 1) {
return 2 ** (-lambda * x);
}
/**
* Calculates the users rank.
*
* @param {object} params Parameters on which the user's rank depends.
* @param {number} params.totalRepos Total number of repos.
* @param {number} params.totalCommits Total number of commits.
* @param {number} params.contributions The number of contributions.
* @param {number} params.followers The number of followers.
* @param {boolean} params.all_commits Whether `include_all_commits` was used.
* @param {number} params.commits Number of commits.
* @param {number} params.prs The number of pull requests.
* @param {number} params.issues The number of issues.
* @param {number} params.stargazers The number of stars.
* @param {number} params.repos Total number of repos.
* @param {number} params.stars The number of stars.
* @param {number} params.followers The number of followers.
* @returns {{level: string, score: number}}} The users rank.
*/
const calculateRank = ({
totalRepos,
totalCommits,
contributions,
followers,
function calculateRank({
all_commits,
commits,
prs,
issues,
stargazers,
}) => {
const COMMITS_OFFSET = 1.65;
const CONTRIBS_OFFSET = 1.65;
const ISSUES_OFFSET = 1;
const STARS_OFFSET = 0.75;
const PRS_OFFSET = 0.5;
const FOLLOWERS_OFFSET = 0.45;
const REPO_OFFSET = 1;
repos, // unused
stars,
followers,
}) {
const COMMITS_MEAN = all_commits ? 1000 : 250,
COMMITS_WEIGHT = 2;
const PRS_MEAN = 50,
PRS_WEIGHT = 3;
const ISSUES_MEAN = 25,
ISSUES_WEIGHT = 1;
const STARS_MEAN = 250,
STARS_WEIGHT = 4;
const FOLLOWERS_MEAN = 25,
FOLLOWERS_WEIGHT = 1;
const ALL_OFFSETS =
CONTRIBS_OFFSET +
ISSUES_OFFSET +
STARS_OFFSET +
PRS_OFFSET +
FOLLOWERS_OFFSET +
REPO_OFFSET;
const TOTAL_WEIGHT =
COMMITS_WEIGHT +
PRS_WEIGHT +
ISSUES_WEIGHT +
STARS_WEIGHT +
FOLLOWERS_WEIGHT;
const RANK_S_VALUE = 1;
const RANK_DOUBLE_A_VALUE = 25;
const RANK_A2_VALUE = 45;
const RANK_A3_VALUE = 60;
const RANK_B_VALUE = 100;
const rank =
(COMMITS_WEIGHT * expsf(commits, 1 / COMMITS_MEAN) +
PRS_WEIGHT * expsf(prs, 1 / PRS_MEAN) +
ISSUES_WEIGHT * expsf(issues, 1 / ISSUES_MEAN) +
STARS_WEIGHT * expsf(stars, 1 / STARS_MEAN) +
FOLLOWERS_WEIGHT * expsf(followers, 1 / FOLLOWERS_MEAN)) /
TOTAL_WEIGHT;
const TOTAL_VALUES =
RANK_S_VALUE +
RANK_DOUBLE_A_VALUE +
RANK_A2_VALUE +
RANK_A3_VALUE +
RANK_B_VALUE;
// prettier-ignore
const score = (
totalCommits * COMMITS_OFFSET +
contributions * CONTRIBS_OFFSET +
issues * ISSUES_OFFSET +
stargazers * STARS_OFFSET +
prs * PRS_OFFSET +
followers * FOLLOWERS_OFFSET +
totalRepos * REPO_OFFSET
) / 100;
const normalizedScore = normalcdf(score, TOTAL_VALUES, ALL_OFFSETS) * 100;
const RANK_S_PLUS = 0.025;
const RANK_S = 0.1;
const RANK_A_PLUS = 0.25;
const RANK_A = 0.5;
const RANK_B_PLUS = 0.75;
const level = (() => {
if (normalizedScore < RANK_S_VALUE) return "S+";
if (normalizedScore < RANK_DOUBLE_A_VALUE) return "S";
if (normalizedScore < RANK_A2_VALUE) return "A++";
if (normalizedScore < RANK_A3_VALUE) return "A+";
return "B+";
if (rank <= RANK_S_PLUS) return "S+";
if (rank <= RANK_S) return "S";
if (rank <= RANK_A_PLUS) return "A+";
if (rank <= RANK_A) return "A";
if (rank <= RANK_B_PLUS) return "B+";
return "B";
})();
return { level, score: normalizedScore };
};
return { level, score: rank * 100 };
}
export { calculateRank };
export default calculateRank;
+15 -24
View File
@@ -192,7 +192,7 @@ const fetchStats = async (
totalIssues: 0,
totalStars: 0,
contributedTo: 0,
rank: { level: "C", score: 0 },
rank: { level: "B", score: 0 },
};
let res = await statsFetcher(username);
@@ -220,53 +220,44 @@ const fetchStats = async (
const user = res.data.data.user;
// populate repoToHide map for quick lookup
// while filtering out
let repoToHide = {};
if (exclude_repo) {
exclude_repo.forEach((repoName) => {
repoToHide[repoName] = true;
});
}
stats.name = user.name || user.login;
stats.totalIssues = user.openIssues.totalCount + user.closedIssues.totalCount;
// normal commits
stats.totalCommits = user.contributionsCollection.totalCommitContributions;
// if include_all_commits then just get that,
// since totalCommitsFetcher already sends totalCommits no need to +=
// if include_all_commits, fetch all commits using the REST API.
if (include_all_commits) {
stats.totalCommits = await totalCommitsFetcher(username);
} else {
stats.totalCommits = user.contributionsCollection.totalCommitContributions;
}
// if count_private then add private commits to totalCommits so far.
// if count_private, add private contributions to totalCommits.
if (count_private) {
stats.totalCommits +=
user.contributionsCollection.restrictedContributionsCount;
}
stats.totalPRs = user.pullRequests.totalCount;
stats.totalIssues = user.openIssues.totalCount + user.closedIssues.totalCount;
stats.contributedTo = user.repositoriesContributedTo.totalCount;
// Retrieve stars while filtering out repositories to be hidden
// Retrieve stars while filtering out repositories to be hidden.
let repoToHide = new Set(exclude_repo);
stats.totalStars = user.repositories.nodes
.filter((data) => {
return !repoToHide[data.name];
return !repoToHide.has(data.name);
})
.reduce((prev, curr) => {
return prev + curr.stargazers.totalCount;
}, 0);
stats.rank = calculateRank({
totalCommits: stats.totalCommits,
totalRepos: user.repositories.totalCount,
followers: user.followers.totalCount,
contributions: stats.contributedTo,
stargazers: stats.totalStars,
all_commits: include_all_commits,
commits: stats.totalCommits,
prs: stats.totalPRs,
issues: stats.totalIssues,
repos: user.repositories.totalCount,
stars: stats.totalStars,
followers: user.followers.totalCount,
});
return stats;