Skip to content

Commit

Permalink
Switch to data-driven percentiles
Browse files Browse the repository at this point in the history
  • Loading branch information
francois-rozet committed Jul 21, 2023
1 parent b56689b commit 091e304
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 86 deletions.
147 changes: 107 additions & 40 deletions src/calculateRank.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,91 @@
function exponential_cdf(x) {
return 1 - 2 ** -x;
}
function score(x, quantiles) {
const i = quantiles.findIndex((q) => x < q);

if (i == 0) {
return 0.0;
} else if (i == -1) {
return 1.0;
}

function log_normal_cdf(x) {
// approximation
return x / (1 + x);
const a = quantiles[i - 1];
const b = quantiles[i];

return ((x - a) / (b - a) + i - 1) / (quantiles.length - 1);
}

const QUANTILES = {
commits: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3,
4, 4, 5, 6, 7, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 19, 20, 22, 23, 25, 27,
29, 31, 33, 35, 38, 40, 43, 45, 48, 51, 54, 57, 60, 64, 67, 71, 76, 80, 85,
89, 94, 99, 105, 111, 118, 125, 132, 140, 147, 155, 164, 173, 184, 195, 207,
220, 233, 249, 265, 284, 304, 326, 353, 380, 411, 451, 495, 545, 611, 691,
794, 933, 1195, 1704, 9722,
],
all_commits: [
0, 0, 0, 0, 2, 4, 8, 11, 15, 19, 23, 27, 32, 36, 41, 45, 50, 55, 60, 65, 71,
76, 82, 87, 93, 99, 105, 111, 117, 124, 131, 137, 145, 151, 159, 166, 174,
182, 190, 198, 207, 215, 225, 234, 244, 253, 264, 274, 285, 296, 306, 318,
330, 342, 355, 368, 382, 396, 409, 424, 440, 457, 475, 493, 512, 531, 551,
570, 593, 618, 643, 667, 695, 723, 752, 784, 815, 857, 893, 934, 984, 1037,
1094, 1152, 1217, 1289, 1379, 1475, 1576, 1696, 1851, 2023, 2232, 2480,
2835, 3242, 3885, 4868, 6614, 11801, 792319,
],
prs: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7,
8, 8, 9, 10, 10, 11, 11, 12, 13, 14, 14, 15, 16, 17, 18, 19, 20, 21, 23, 24,
25, 27, 28, 30, 32, 34, 36, 38, 40, 43, 46, 50, 53, 57, 61, 65, 70, 76, 83,
90, 99, 110, 123, 139, 159, 185, 219, 273, 360, 562, 2291,
],
issues: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3,
4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 8, 8, 9, 10, 10, 11, 12, 12, 13, 14, 15,
16, 17, 19, 20, 21, 23, 24, 26, 28, 30, 32, 35, 38, 41, 45, 49, 54, 59, 66,
73, 82, 92, 106, 123, 150, 186, 255, 409, 1590,
],
reviews: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 3, 4, 5, 6, 8, 11, 15, 23, 36, 61,
129, 764,
],
repos: [
0, 0, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 6, 7, 7, 8, 8, 8, 9, 9, 10, 10, 11,
11, 12, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20,
20, 21, 21, 22, 22, 23, 24, 24, 25, 25, 26, 27, 28, 28, 29, 30, 30, 31, 32,
33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 45, 46, 47, 49, 50, 52, 54, 56,
58, 60, 62, 65, 68, 70, 74, 77, 82, 86, 92, 98, 105, 115, 127, 144, 170,
211, 316, 2002,
],
stars: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 6, 6,
7, 7, 8, 8, 9, 9, 10, 11, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 24,
26, 28, 30, 31, 33, 35, 38, 41, 44, 48, 52, 56, 61, 67, 74, 83, 93, 104,
117, 134, 154, 181, 215, 257, 321, 417, 565, 818, 1298, 2599, 18304,
],
followers: [
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4,
5, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 12, 12,
13, 13, 14, 14, 15, 15, 16, 16, 17, 18, 18, 19, 20, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 32, 33, 35, 36, 38, 40, 42, 44, 46, 49, 51, 54, 57, 61,
65, 70, 75, 81, 88, 97, 108, 121, 139, 161, 193, 240, 334, 569, 3583,
],
};

const WEIGHT = {
commits: 2.0,
prs: 3.0,
issues: 1.0,
reviews: 0.5,
repos: 0.0,
stars: 4.0,
followers: 1.0,
};

/**
* Calculates the users rank.
*
Expand All @@ -27,48 +106,36 @@ function calculateRank({
prs,
issues,
reviews,
// eslint-disable-next-line no-unused-vars
repos, // unused
repos,
stars,
followers,
}) {
const COMMITS_MEDIAN = all_commits ? 1000 : 250,
COMMITS_WEIGHT = 2;
const PRS_MEDIAN = 50,
PRS_WEIGHT = 3;
const ISSUES_MEDIAN = 25,
ISSUES_WEIGHT = 1;
const REVIEWS_MEDIAN = 2,
REVIEWS_WEIGHT = 1;
const STARS_MEDIAN = 50,
STARS_WEIGHT = 4;
const FOLLOWERS_MEDIAN = 10,
FOLLOWERS_WEIGHT = 1;

const TOTAL_WEIGHT =
COMMITS_WEIGHT +
PRS_WEIGHT +
ISSUES_WEIGHT +
REVIEWS_WEIGHT +
STARS_WEIGHT +
FOLLOWERS_WEIGHT;

const THRESHOLDS = [1, 12.5, 25, 37.5, 50, 62.5, 75, 87.5, 100];
const LEVELS = ["S", "A+", "A", "A-", "B+", "B", "B-", "C+", "C"];

const rank =
1 -
(COMMITS_WEIGHT * exponential_cdf(commits / COMMITS_MEDIAN) +
PRS_WEIGHT * exponential_cdf(prs / PRS_MEDIAN) +
ISSUES_WEIGHT * exponential_cdf(issues / ISSUES_MEDIAN) +
REVIEWS_WEIGHT * exponential_cdf(reviews / REVIEWS_MEDIAN) +
STARS_WEIGHT * log_normal_cdf(stars / STARS_MEDIAN) +
FOLLOWERS_WEIGHT * log_normal_cdf(followers / FOLLOWERS_MEDIAN)) /
TOTAL_WEIGHT;
const total_weight =
WEIGHT.commits +
WEIGHT.prs +
WEIGHT.issues +
WEIGHT.reviews +
WEIGHT.repos +
WEIGHT.stars +
WEIGHT.followers;

const total_score =
WEIGHT.commits *
score(commits, all_commits ? QUANTILES.all_commits : QUANTILES.commits) +
WEIGHT.prs * score(prs, QUANTILES.prs) +
WEIGHT.issues * score(issues, QUANTILES.issues) +
WEIGHT.reviews * score(reviews, QUANTILES.reviews) +
WEIGHT.repos * score(repos, QUANTILES.repos) +
WEIGHT.stars * score(stars, QUANTILES.stars) +
WEIGHT.followers * score(followers, QUANTILES.followers);

const level = LEVELS[THRESHOLDS.findIndex((t) => rank * 100 <= t)];
const percentile = 100 * (1 - total_score / total_weight);
const level = LEVELS[THRESHOLDS.findIndex((t) => percentile <= t)];

return { level, percentile: rank * 100 };
return { level, percentile };
}

export { calculateRank };
Expand Down
77 changes: 31 additions & 46 deletions tests/calculateRank.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { calculateRank } from "../src/calculateRank.js";
import { expect, it, describe } from "@jest/globals";

describe("Test calculateRank", () => {
it("new user gets C rank", () => {
it("new user gets C+ rank", () => {
expect(
calculateRank({
all_commits: false,
Expand All @@ -15,82 +15,67 @@ describe("Test calculateRank", () => {
stars: 0,
followers: 0,
}),
).toStrictEqual({ level: "C", percentile: 100 });
).toStrictEqual({ level: "C+", percentile: 78.26086956521738 });
});

it("beginner user gets B- rank", () => {
it("beginner user gets B rank", () => {
expect(
calculateRank({
all_commits: false,
commits: 125,
prs: 25,
issues: 10,
reviews: 5,
repos: 0,
stars: 25,
commits: 50,
prs: 5,
issues: 5,
reviews: 0,
repos: 5,
stars: 5,
followers: 5,
}),
).toStrictEqual({ level: "B-", percentile: 65.02918514848255 });
).toStrictEqual({ level: "B", percentile: 51.97101449275363 });
});

it("median user gets B+ rank", () => {
it("advanced user gets A- rank", () => {
expect(
calculateRank({
all_commits: false,
commits: 250,
prs: 50,
prs: 25,
issues: 25,
reviews: 10,
repos: 0,
stars: 50,
followers: 10,
reviews: 0,
repos: 25,
stars: 25,
followers: 25,
}),
).toStrictEqual({ level: "B+", percentile: 46.09375 });
).toStrictEqual({ level: "A-", percentile: 27.07608695652174 });
});

it("average user gets B+ rank (include_all_commits)", () => {
it("advanced user gets A- rank (include_all_commits)", () => {
expect(
calculateRank({
all_commits: true,
commits: 1000,
prs: 50,
prs: 25,
issues: 25,
reviews: 10,
repos: 0,
stars: 50,
followers: 10,
reviews: 0,
repos: 25,
stars: 25,
followers: 25,
}),
).toStrictEqual({ level: "B+", percentile: 46.09375 });
).toStrictEqual({ level: "A-", percentile: 27.55619360131255 });
});

it("advanced user gets A rank", () => {
it("expert user gets A+ rank", () => {
expect(
calculateRank({
all_commits: false,
commits: 500,
prs: 100,
issues: 50,
reviews: 20,
repos: 0,
stars: 200,
followers: 40,
}),
).toStrictEqual({ level: "A", percentile: 20.841471354166664 });
});

it("expert user gets A+ rank", () => {
expect(
calculateRank({
all_commits: false,
commits: 1000,
prs: 200,
issues: 100,
reviews: 40,
repos: 0,
stars: 800,
followers: 160,
reviews: 0,
repos: 100,
stars: 100,
followers: 100,
}),
).toStrictEqual({ level: "A+", percentile: 5.575988339442828 });
).toStrictEqual({ level: "A+", percentile: 10.794579333709752 });
});

it("sindresorhus gets S rank", () => {
Expand All @@ -105,6 +90,6 @@ describe("Test calculateRank", () => {
stars: 600000,
followers: 50000,
}),
).toStrictEqual({ level: "S", percentile: 0.4578556547153667 });
).toStrictEqual({ level: "S", percentile: 0.4312953010223719 });
});
});

0 comments on commit 091e304

Please sign in to comment.