Skip to content

Commit 66e5492

Browse files
Add finer ranking levels (#2762)
* Add finer ranking levels * Update rank description
1 parent 768721f commit 66e5492

File tree

6 files changed

+69
-60
lines changed

6 files changed

+69
-60
lines changed

readme.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ Change the `?username=` value to your GitHub username.
137137
> By default, the stats card only shows statistics like stars, commits and pull requests from public repositories. To show private statistics on the stats card, you should [deploy your own instance](#deploy-on-your-own) using your own GitHub API token.
138138
139139
> **Note**
140-
> Available ranks are S+ (top 1%), S (top 25%), A++ (top 45%), A+ (top 60%), and B+ (everyone). The values are calculated by using the [cumulative distribution function](https://en.wikipedia.org/wiki/Cumulative_distribution_function) using commits, contributions, issues, stars, pull requests, followers, and owned repositories. The implementation can be investigated at [src/calculateRank.js](./src/calculateRank.js).
140+
> Available ranks are S (top 1%), A+ (12.5%), A (25%), A- (37.5%), B+ (50%), B (62.5%), B- (75%), C+ (87.5%) and C (everyone). This ranking scheme is based on the [Japanese academic grading](https://wikipedia.org/wiki/Academic_grading_in_Japan) system. The global percentile is calculated as a weighted sum of percentiles for each statistic (number of commits, pull requests, issues, stars and followers), based on the cumulative distribution function of the [exponential](https://wikipedia.org/wiki/exponential_distribution) and the [log-normal](https://wikipedia.org/wiki/Log-normal_distribution) distributions. The implementation can be investigated at [src/calculateRank.js](./src/calculateRank.js). The circle around the rank shows 100 minus the global percentile.
141141
142142
### Hiding individual stats
143143

src/calculateRank.js

+25-29
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
function expsf(x, lambda = 1) {
2-
return 2 ** (-lambda * x);
1+
function exponential_cdf(x) {
2+
return 1 - 2 ** -x;
3+
}
4+
5+
function log_normal_cdf(x) {
6+
// approximation
7+
return x / (1 + x);
38
}
49

510
/**
@@ -13,7 +18,7 @@ function expsf(x, lambda = 1) {
1318
* @param {number} params.repos Total number of repos.
1419
* @param {number} params.stars The number of stars.
1520
* @param {number} params.followers The number of followers.
16-
* @returns {{level: string, score: number}}} The users rank.
21+
* @returns {{level: string, percentile: number}}} The users rank.
1722
*/
1823
function calculateRank({
1924
all_commits,
@@ -24,15 +29,15 @@ function calculateRank({
2429
stars,
2530
followers,
2631
}) {
27-
const COMMITS_MEAN = all_commits ? 1000 : 250,
32+
const COMMITS_MEDIAN = all_commits ? 1000 : 250,
2833
COMMITS_WEIGHT = 2;
29-
const PRS_MEAN = 50,
34+
const PRS_MEDIAN = 50,
3035
PRS_WEIGHT = 3;
31-
const ISSUES_MEAN = 25,
36+
const ISSUES_MEDIAN = 25,
3237
ISSUES_WEIGHT = 1;
33-
const STARS_MEAN = 250,
38+
const STARS_MEDIAN = 50,
3439
STARS_WEIGHT = 4;
35-
const FOLLOWERS_MEAN = 25,
40+
const FOLLOWERS_MEDIAN = 10,
3641
FOLLOWERS_WEIGHT = 1;
3742

3843
const TOTAL_WEIGHT =
@@ -42,30 +47,21 @@ function calculateRank({
4247
STARS_WEIGHT +
4348
FOLLOWERS_WEIGHT;
4449

45-
const rank =
46-
(COMMITS_WEIGHT * expsf(commits, 1 / COMMITS_MEAN) +
47-
PRS_WEIGHT * expsf(prs, 1 / PRS_MEAN) +
48-
ISSUES_WEIGHT * expsf(issues, 1 / ISSUES_MEAN) +
49-
STARS_WEIGHT * expsf(stars, 1 / STARS_MEAN) +
50-
FOLLOWERS_WEIGHT * expsf(followers, 1 / FOLLOWERS_MEAN)) /
51-
TOTAL_WEIGHT;
50+
const THRESHOLDS = [1, 12.5, 25, 37.5, 50, 62.5, 75, 87.5, 100];
51+
const LEVELS = ["S", "A+", "A", "A-", "B+", "B", "B-", "C+", "C"];
5252

53-
const RANK_S_PLUS = 0.025;
54-
const RANK_S = 0.1;
55-
const RANK_A_PLUS = 0.25;
56-
const RANK_A = 0.5;
57-
const RANK_B_PLUS = 0.75;
53+
const rank =
54+
1 -
55+
(COMMITS_WEIGHT * exponential_cdf(commits / COMMITS_MEDIAN) +
56+
PRS_WEIGHT * exponential_cdf(prs / PRS_MEDIAN) +
57+
ISSUES_WEIGHT * exponential_cdf(issues / ISSUES_MEDIAN) +
58+
STARS_WEIGHT * log_normal_cdf(stars / STARS_MEDIAN) +
59+
FOLLOWERS_WEIGHT * log_normal_cdf(followers / FOLLOWERS_MEDIAN)) /
60+
TOTAL_WEIGHT;
5861

59-
const level = (() => {
60-
if (rank <= RANK_S_PLUS) return "S+";
61-
if (rank <= RANK_S) return "S";
62-
if (rank <= RANK_A_PLUS) return "A+";
63-
if (rank <= RANK_A) return "A";
64-
if (rank <= RANK_B_PLUS) return "B+";
65-
return "B";
66-
})();
62+
const level = LEVELS[THRESHOLDS.findIndex((t) => rank * 100 <= t)];
6763

68-
return { level, score: rank * 100 };
64+
return { level: level, percentile: rank * 100 };
6965
}
7066

7167
export { calculateRank };

src/cards/stats-card.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,8 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
209209
hide_rank ? 0 : 150,
210210
);
211211

212-
// the better user's score the the rank will be closer to zero so
213-
// subtracting 100 to get the progress in 100%
214-
const progress = 100 - rank.score;
212+
// the lower the user's percentile the better
213+
const progress = 100 - rank.percentile;
215214
const cssStyles = getStyles({
216215
titleColor,
217216
ringColor,

src/fetchers/stats-fetcher.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ const fetchStats = async (
189189
totalIssues: 0,
190190
totalStars: 0,
191191
contributedTo: 0,
192-
rank: { level: "B", score: 0 },
192+
rank: { level: "C", percentile: 100 },
193193
};
194194

195195
let res = await statsFetcher(username);

src/fetchers/types.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export type StatsData = {
2222
totalIssues: number;
2323
totalStars: number;
2424
contributedTo: number;
25-
rank: { level: string; score: number };
25+
rank: { level: string; percentile: number };
2626
};
2727

2828
export type Lang = {

tests/calculateRank.test.js

+39-25
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import "@testing-library/jest-dom";
22
import { calculateRank } from "../src/calculateRank.js";
33

44
describe("Test calculateRank", () => {
5-
it("new user gets B rank", () => {
5+
it("new user gets C rank", () => {
66
expect(
77
calculateRank({
88
all_commits: false,
@@ -13,76 +13,90 @@ describe("Test calculateRank", () => {
1313
stars: 0,
1414
followers: 0,
1515
}),
16-
).toStrictEqual({ level: "B", score: 100 });
16+
).toStrictEqual({ level: "C", percentile: 100 });
1717
});
1818

19-
it("average user gets A rank", () => {
19+
it("beginner user gets B- rank", () => {
20+
expect(
21+
calculateRank({
22+
all_commits: false,
23+
commits: 125,
24+
prs: 25,
25+
issues: 10,
26+
repos: 0,
27+
stars: 25,
28+
followers: 5,
29+
}),
30+
).toStrictEqual({ level: "B-", percentile: 69.333868386557 });
31+
});
32+
33+
it("median user gets B+ rank", () => {
2034
expect(
2135
calculateRank({
2236
all_commits: false,
2337
commits: 250,
2438
prs: 50,
2539
issues: 25,
2640
repos: 0,
27-
stars: 250,
28-
followers: 25,
41+
stars: 50,
42+
followers: 10,
2943
}),
30-
).toStrictEqual({ level: "A", score: 50 });
44+
).toStrictEqual({ level: "B+", percentile: 50 });
3145
});
3246

33-
it("average user gets A rank (include_all_commits)", () => {
47+
it("average user gets B+ rank (include_all_commits)", () => {
3448
expect(
3549
calculateRank({
3650
all_commits: true,
3751
commits: 1000,
3852
prs: 50,
3953
issues: 25,
4054
repos: 0,
41-
stars: 250,
42-
followers: 25,
55+
stars: 50,
56+
followers: 10,
4357
}),
44-
).toStrictEqual({ level: "A", score: 50 });
58+
).toStrictEqual({ level: "B+", percentile: 50 });
4559
});
4660

47-
it("more than average user gets A+ rank", () => {
61+
it("advanced user gets A rank", () => {
4862
expect(
4963
calculateRank({
5064
all_commits: false,
5165
commits: 500,
5266
prs: 100,
5367
issues: 50,
5468
repos: 0,
55-
stars: 500,
56-
followers: 50,
69+
stars: 200,
70+
followers: 40,
5771
}),
58-
).toStrictEqual({ level: "A+", score: 25 });
72+
).toStrictEqual({ level: "A", percentile: 22.72727272727273 });
5973
});
6074

61-
it("expert user gets S rank", () => {
75+
it("expert user gets A+ rank", () => {
6276
expect(
6377
calculateRank({
6478
all_commits: false,
6579
commits: 1000,
6680
prs: 200,
6781
issues: 100,
6882
repos: 0,
69-
stars: 1000,
70-
followers: 100,
83+
stars: 800,
84+
followers: 160,
7185
}),
72-
).toStrictEqual({ level: "S", score: 6.25 });
86+
).toStrictEqual({ level: "A+", percentile: 6.082887700534744 });
7387
});
7488

75-
it("ezyang gets S+ rank", () => {
89+
it("sindresorhus gets S rank", () => {
7690
expect(
7791
calculateRank({
7892
all_commits: false,
79-
commits: 1000,
80-
prs: 4000,
81-
issues: 2000,
93+
commits: 1300,
94+
prs: 1500,
95+
issues: 4500,
8296
repos: 0,
83-
stars: 5000,
84-
followers: 2000,
97+
stars: 600000,
98+
followers: 50000,
8599
}),
86-
).toStrictEqual({ level: "S+", score: 1.1363983154296875 });
100+
).toStrictEqual({ level: "S", percentile: 0.49947889605312934 });
87101
});
88102
});

0 commit comments

Comments
 (0)