Skip to content

Commit 091e304

Browse files
Switch to data-driven percentiles
1 parent b56689b commit 091e304

File tree

2 files changed

+138
-86
lines changed

2 files changed

+138
-86
lines changed

src/calculateRank.js

Lines changed: 107 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,91 @@
1-
function exponential_cdf(x) {
2-
return 1 - 2 ** -x;
3-
}
1+
function score(x, quantiles) {
2+
const i = quantiles.findIndex((q) => x < q);
3+
4+
if (i == 0) {
5+
return 0.0;
6+
} else if (i == -1) {
7+
return 1.0;
8+
}
49

5-
function log_normal_cdf(x) {
6-
// approximation
7-
return x / (1 + x);
10+
const a = quantiles[i - 1];
11+
const b = quantiles[i];
12+
13+
return ((x - a) / (b - a) + i - 1) / (quantiles.length - 1);
814
}
915

16+
const QUANTILES = {
17+
commits: [
18+
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,
19+
4, 4, 5, 6, 7, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 19, 20, 22, 23, 25, 27,
20+
29, 31, 33, 35, 38, 40, 43, 45, 48, 51, 54, 57, 60, 64, 67, 71, 76, 80, 85,
21+
89, 94, 99, 105, 111, 118, 125, 132, 140, 147, 155, 164, 173, 184, 195, 207,
22+
220, 233, 249, 265, 284, 304, 326, 353, 380, 411, 451, 495, 545, 611, 691,
23+
794, 933, 1195, 1704, 9722,
24+
],
25+
all_commits: [
26+
0, 0, 0, 0, 2, 4, 8, 11, 15, 19, 23, 27, 32, 36, 41, 45, 50, 55, 60, 65, 71,
27+
76, 82, 87, 93, 99, 105, 111, 117, 124, 131, 137, 145, 151, 159, 166, 174,
28+
182, 190, 198, 207, 215, 225, 234, 244, 253, 264, 274, 285, 296, 306, 318,
29+
330, 342, 355, 368, 382, 396, 409, 424, 440, 457, 475, 493, 512, 531, 551,
30+
570, 593, 618, 643, 667, 695, 723, 752, 784, 815, 857, 893, 934, 984, 1037,
31+
1094, 1152, 1217, 1289, 1379, 1475, 1576, 1696, 1851, 2023, 2232, 2480,
32+
2835, 3242, 3885, 4868, 6614, 11801, 792319,
33+
],
34+
prs: [
35+
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,
36+
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,
37+
8, 8, 9, 10, 10, 11, 11, 12, 13, 14, 14, 15, 16, 17, 18, 19, 20, 21, 23, 24,
38+
25, 27, 28, 30, 32, 34, 36, 38, 40, 43, 46, 50, 53, 57, 61, 65, 70, 76, 83,
39+
90, 99, 110, 123, 139, 159, 185, 219, 273, 360, 562, 2291,
40+
],
41+
issues: [
42+
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,
43+
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,
44+
4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 8, 8, 9, 10, 10, 11, 12, 12, 13, 14, 15,
45+
16, 17, 19, 20, 21, 23, 24, 26, 28, 30, 32, 35, 38, 41, 45, 49, 54, 59, 66,
46+
73, 82, 92, 106, 123, 150, 186, 255, 409, 1590,
47+
],
48+
reviews: [
49+
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,
50+
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,
51+
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,
52+
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 3, 4, 5, 6, 8, 11, 15, 23, 36, 61,
53+
129, 764,
54+
],
55+
repos: [
56+
0, 0, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 6, 7, 7, 8, 8, 8, 9, 9, 10, 10, 11,
57+
11, 12, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20,
58+
20, 21, 21, 22, 22, 23, 24, 24, 25, 25, 26, 27, 28, 28, 29, 30, 30, 31, 32,
59+
33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 45, 46, 47, 49, 50, 52, 54, 56,
60+
58, 60, 62, 65, 68, 70, 74, 77, 82, 86, 92, 98, 105, 115, 127, 144, 170,
61+
211, 316, 2002,
62+
],
63+
stars: [
64+
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,
65+
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,
66+
7, 7, 8, 8, 9, 9, 10, 11, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 24,
67+
26, 28, 30, 31, 33, 35, 38, 41, 44, 48, 52, 56, 61, 67, 74, 83, 93, 104,
68+
117, 134, 154, 181, 215, 257, 321, 417, 565, 818, 1298, 2599, 18304,
69+
],
70+
followers: [
71+
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,
72+
5, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 12, 12,
73+
13, 13, 14, 14, 15, 15, 16, 16, 17, 18, 18, 19, 20, 20, 21, 22, 23, 24, 25,
74+
26, 27, 28, 29, 30, 32, 33, 35, 36, 38, 40, 42, 44, 46, 49, 51, 54, 57, 61,
75+
65, 70, 75, 81, 88, 97, 108, 121, 139, 161, 193, 240, 334, 569, 3583,
76+
],
77+
};
78+
79+
const WEIGHT = {
80+
commits: 2.0,
81+
prs: 3.0,
82+
issues: 1.0,
83+
reviews: 0.5,
84+
repos: 0.0,
85+
stars: 4.0,
86+
followers: 1.0,
87+
};
88+
1089
/**
1190
* Calculates the users rank.
1291
*
@@ -27,48 +106,36 @@ function calculateRank({
27106
prs,
28107
issues,
29108
reviews,
30-
// eslint-disable-next-line no-unused-vars
31-
repos, // unused
109+
repos,
32110
stars,
33111
followers,
34112
}) {
35-
const COMMITS_MEDIAN = all_commits ? 1000 : 250,
36-
COMMITS_WEIGHT = 2;
37-
const PRS_MEDIAN = 50,
38-
PRS_WEIGHT = 3;
39-
const ISSUES_MEDIAN = 25,
40-
ISSUES_WEIGHT = 1;
41-
const REVIEWS_MEDIAN = 2,
42-
REVIEWS_WEIGHT = 1;
43-
const STARS_MEDIAN = 50,
44-
STARS_WEIGHT = 4;
45-
const FOLLOWERS_MEDIAN = 10,
46-
FOLLOWERS_WEIGHT = 1;
47-
48-
const TOTAL_WEIGHT =
49-
COMMITS_WEIGHT +
50-
PRS_WEIGHT +
51-
ISSUES_WEIGHT +
52-
REVIEWS_WEIGHT +
53-
STARS_WEIGHT +
54-
FOLLOWERS_WEIGHT;
55-
56113
const THRESHOLDS = [1, 12.5, 25, 37.5, 50, 62.5, 75, 87.5, 100];
57114
const LEVELS = ["S", "A+", "A", "A-", "B+", "B", "B-", "C+", "C"];
58115

59-
const rank =
60-
1 -
61-
(COMMITS_WEIGHT * exponential_cdf(commits / COMMITS_MEDIAN) +
62-
PRS_WEIGHT * exponential_cdf(prs / PRS_MEDIAN) +
63-
ISSUES_WEIGHT * exponential_cdf(issues / ISSUES_MEDIAN) +
64-
REVIEWS_WEIGHT * exponential_cdf(reviews / REVIEWS_MEDIAN) +
65-
STARS_WEIGHT * log_normal_cdf(stars / STARS_MEDIAN) +
66-
FOLLOWERS_WEIGHT * log_normal_cdf(followers / FOLLOWERS_MEDIAN)) /
67-
TOTAL_WEIGHT;
116+
const total_weight =
117+
WEIGHT.commits +
118+
WEIGHT.prs +
119+
WEIGHT.issues +
120+
WEIGHT.reviews +
121+
WEIGHT.repos +
122+
WEIGHT.stars +
123+
WEIGHT.followers;
124+
125+
const total_score =
126+
WEIGHT.commits *
127+
score(commits, all_commits ? QUANTILES.all_commits : QUANTILES.commits) +
128+
WEIGHT.prs * score(prs, QUANTILES.prs) +
129+
WEIGHT.issues * score(issues, QUANTILES.issues) +
130+
WEIGHT.reviews * score(reviews, QUANTILES.reviews) +
131+
WEIGHT.repos * score(repos, QUANTILES.repos) +
132+
WEIGHT.stars * score(stars, QUANTILES.stars) +
133+
WEIGHT.followers * score(followers, QUANTILES.followers);
68134

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

71-
return { level, percentile: rank * 100 };
138+
return { level, percentile };
72139
}
73140

74141
export { calculateRank };

tests/calculateRank.test.js

Lines changed: 31 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { calculateRank } from "../src/calculateRank.js";
33
import { expect, it, describe } from "@jest/globals";
44

55
describe("Test calculateRank", () => {
6-
it("new user gets C rank", () => {
6+
it("new user gets C+ rank", () => {
77
expect(
88
calculateRank({
99
all_commits: false,
@@ -15,82 +15,67 @@ describe("Test calculateRank", () => {
1515
stars: 0,
1616
followers: 0,
1717
}),
18-
).toStrictEqual({ level: "C", percentile: 100 });
18+
).toStrictEqual({ level: "C+", percentile: 78.26086956521738 });
1919
});
2020

21-
it("beginner user gets B- rank", () => {
21+
it("beginner user gets B rank", () => {
2222
expect(
2323
calculateRank({
2424
all_commits: false,
25-
commits: 125,
26-
prs: 25,
27-
issues: 10,
28-
reviews: 5,
29-
repos: 0,
30-
stars: 25,
25+
commits: 50,
26+
prs: 5,
27+
issues: 5,
28+
reviews: 0,
29+
repos: 5,
30+
stars: 5,
3131
followers: 5,
3232
}),
33-
).toStrictEqual({ level: "B-", percentile: 65.02918514848255 });
33+
).toStrictEqual({ level: "B", percentile: 51.97101449275363 });
3434
});
3535

36-
it("median user gets B+ rank", () => {
36+
it("advanced user gets A- rank", () => {
3737
expect(
3838
calculateRank({
3939
all_commits: false,
4040
commits: 250,
41-
prs: 50,
41+
prs: 25,
4242
issues: 25,
43-
reviews: 10,
44-
repos: 0,
45-
stars: 50,
46-
followers: 10,
43+
reviews: 0,
44+
repos: 25,
45+
stars: 25,
46+
followers: 25,
4747
}),
48-
).toStrictEqual({ level: "B+", percentile: 46.09375 });
48+
).toStrictEqual({ level: "A-", percentile: 27.07608695652174 });
4949
});
5050

51-
it("average user gets B+ rank (include_all_commits)", () => {
51+
it("advanced user gets A- rank (include_all_commits)", () => {
5252
expect(
5353
calculateRank({
5454
all_commits: true,
5555
commits: 1000,
56-
prs: 50,
56+
prs: 25,
5757
issues: 25,
58-
reviews: 10,
59-
repos: 0,
60-
stars: 50,
61-
followers: 10,
58+
reviews: 0,
59+
repos: 25,
60+
stars: 25,
61+
followers: 25,
6262
}),
63-
).toStrictEqual({ level: "B+", percentile: 46.09375 });
63+
).toStrictEqual({ level: "A-", percentile: 27.55619360131255 });
6464
});
6565

66-
it("advanced user gets A rank", () => {
66+
it("expert user gets A+ rank", () => {
6767
expect(
6868
calculateRank({
6969
all_commits: false,
7070
commits: 500,
7171
prs: 100,
72-
issues: 50,
73-
reviews: 20,
74-
repos: 0,
75-
stars: 200,
76-
followers: 40,
77-
}),
78-
).toStrictEqual({ level: "A", percentile: 20.841471354166664 });
79-
});
80-
81-
it("expert user gets A+ rank", () => {
82-
expect(
83-
calculateRank({
84-
all_commits: false,
85-
commits: 1000,
86-
prs: 200,
8772
issues: 100,
88-
reviews: 40,
89-
repos: 0,
90-
stars: 800,
91-
followers: 160,
73+
reviews: 0,
74+
repos: 100,
75+
stars: 100,
76+
followers: 100,
9277
}),
93-
).toStrictEqual({ level: "A+", percentile: 5.575988339442828 });
78+
).toStrictEqual({ level: "A+", percentile: 10.794579333709752 });
9479
});
9580

9681
it("sindresorhus gets S rank", () => {
@@ -105,6 +90,6 @@ describe("Test calculateRank", () => {
10590
stars: 600000,
10691
followers: 50000,
10792
}),
108-
).toStrictEqual({ level: "S", percentile: 0.4578556547153667 });
93+
).toStrictEqual({ level: "S", percentile: 0.4312953010223719 });
10994
});
11095
});

0 commit comments

Comments
 (0)