diff --git a/readme.md b/readme.md index dc60369751a27..90cfb037a9215 100644 --- a/readme.md +++ b/readme.md @@ -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 diff --git a/src/calculateRank.js b/src/calculateRank.js index 7648ad412ed67..c4583da0e804f 100644 --- a/src/calculateRank.js +++ b/src/calculateRank.js @@ -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); } /** @@ -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, @@ -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 = @@ -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 }; diff --git a/src/cards/stats-card.js b/src/cards/stats-card.js index 4761d023e4ab1..d40a2911b49a1 100644 --- a/src/cards/stats-card.js +++ b/src/cards/stats-card.js @@ -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, diff --git a/src/fetchers/stats-fetcher.js b/src/fetchers/stats-fetcher.js index 8fecffa466f8d..45b21854737dd 100644 --- a/src/fetchers/stats-fetcher.js +++ b/src/fetchers/stats-fetcher.js @@ -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); diff --git a/src/fetchers/types.d.ts b/src/fetchers/types.d.ts index 854e1315a04ac..3e7381a7fae0d 100644 --- a/src/fetchers/types.d.ts +++ b/src/fetchers/types.d.ts @@ -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 = { diff --git a/tests/calculateRank.test.js b/tests/calculateRank.test.js index 3bfd7f4376248..4dd29f8ff2a81 100644 --- a/tests/calculateRank.test.js +++ b/tests/calculateRank.test.js @@ -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, @@ -13,10 +13,24 @@ 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, @@ -24,13 +38,13 @@ describe("Test calculateRank", () => { 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, @@ -38,13 +52,13 @@ describe("Test calculateRank", () => { 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, @@ -52,13 +66,13 @@ describe("Test calculateRank", () => { 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, @@ -66,23 +80,23 @@ describe("Test calculateRank", () => { 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 }); }); });