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

Add finer ranking levels #2762

Merged
merged 2 commits into from
Jun 12, 2023
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
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Change the `?username=` value to your GitHub username.
```

> **Note**
> 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).
> 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.

### Hiding individual stats

Expand Down
54 changes: 25 additions & 29 deletions src/calculateRank.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
function expsf(x, lambda = 1) {
return 2 ** (-lambda * x);
function exponential_cdf(x) {
return 1 - 2 ** -x;
}

function log_normal_cdf(x) {
// approximation
return x / (1 + x);
}

/**
Expand All @@ -13,7 +18,7 @@ function expsf(x, lambda = 1) {
* @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.
* @returns {{level: string, percentile: number}}} The users rank.
*/
function calculateRank({
all_commits,
Expand All @@ -24,15 +29,15 @@ function calculateRank({
stars,
followers,
}) {
const COMMITS_MEAN = all_commits ? 1000 : 250,
const COMMITS_MEDIAN = all_commits ? 1000 : 250,
COMMITS_WEIGHT = 2;
const PRS_MEAN = 50,
const PRS_MEDIAN = 50,
PRS_WEIGHT = 3;
const ISSUES_MEAN = 25,
const ISSUES_MEDIAN = 25,
ISSUES_WEIGHT = 1;
const STARS_MEAN = 250,
const STARS_MEDIAN = 50,
STARS_WEIGHT = 4;
const FOLLOWERS_MEAN = 25,
const FOLLOWERS_MEDIAN = 10,
FOLLOWERS_WEIGHT = 1;

const TOTAL_WEIGHT =
Expand All @@ -42,30 +47,21 @@ function calculateRank({
STARS_WEIGHT +
FOLLOWERS_WEIGHT;

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 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_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 rank =
1 -
(COMMITS_WEIGHT * exponential_cdf(commits / COMMITS_MEDIAN) +
PRS_WEIGHT * exponential_cdf(prs / PRS_MEDIAN) +
ISSUES_WEIGHT * exponential_cdf(issues / ISSUES_MEDIAN) +
STARS_WEIGHT * log_normal_cdf(stars / STARS_MEDIAN) +
FOLLOWERS_WEIGHT * log_normal_cdf(followers / FOLLOWERS_MEDIAN)) /
TOTAL_WEIGHT;

const level = (() => {
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";
})();
const level = LEVELS[THRESHOLDS.findIndex((t) => rank * 100 <= t)];

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

export { calculateRank };
Expand Down
5 changes: 2 additions & 3 deletions src/cards/stats-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,8 @@ const renderStatsCard = (stats = {}, options = { hide: [] }) => {
hide_rank ? 0 : 150,
);

// the better user's score the the rank will be closer to zero so
// subtracting 100 to get the progress in 100%
const progress = 100 - rank.score;
// the lower the user's percentile the better
const progress = 100 - rank.percentile;
const cssStyles = getStyles({
titleColor,
ringColor,
Expand Down
2 changes: 1 addition & 1 deletion src/fetchers/stats-fetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ const fetchStats = async (
totalIssues: 0,
totalStars: 0,
contributedTo: 0,
rank: { level: "B", score: 0 },
rank: { level: "C", percentile: 100 },
};

let res = await statsFetcher(username);
Expand Down
2 changes: 1 addition & 1 deletion src/fetchers/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type StatsData = {
totalIssues: number;
totalStars: number;
contributedTo: number;
rank: { level: string; score: number };
rank: { level: string; percentile: number };
};

export type Lang = {
Expand Down
64 changes: 39 additions & 25 deletions tests/calculateRank.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import "@testing-library/jest-dom";
import { calculateRank } from "../src/calculateRank.js";

describe("Test calculateRank", () => {
it("new user gets B rank", () => {
it("new user gets C rank", () => {
expect(
calculateRank({
all_commits: false,
Expand All @@ -13,76 +13,90 @@ describe("Test calculateRank", () => {
stars: 0,
followers: 0,
}),
).toStrictEqual({ level: "B", score: 100 });
).toStrictEqual({ level: "C", percentile: 100 });
});

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

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

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

it("more than average user gets A+ rank", () => {
it("advanced user gets A rank", () => {
expect(
calculateRank({
all_commits: false,
commits: 500,
prs: 100,
issues: 50,
repos: 0,
stars: 500,
followers: 50,
stars: 200,
followers: 40,
}),
).toStrictEqual({ level: "A+", score: 25 });
).toStrictEqual({ level: "A", percentile: 22.72727272727273 });
});

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

it("ezyang gets S+ rank", () => {
it("sindresorhus gets S rank", () => {
expect(
calculateRank({
all_commits: false,
commits: 1000,
prs: 4000,
issues: 2000,
commits: 1300,
prs: 1500,
issues: 4500,
repos: 0,
stars: 5000,
followers: 2000,
stars: 600000,
followers: 50000,
}),
).toStrictEqual({ level: "S+", score: 1.1363983154296875 });
).toStrictEqual({ level: "S", percentile: 0.49947889605312934 });
});
});