From 13399885bb3f1f8f2f3b56f6a29a2ad7286bd80d Mon Sep 17 00:00:00 2001 From: boomzero Date: Sun, 18 Oct 2026 21:14:04 +0800 Subject: [PATCH] Add formula charts to the rating page Six inline SVG charts, one per part of the formula plus an overall rating-vs-solved chart over the username colour bands. They are generated by scripts/rating_charts.py and inlined via ?raw + v-html so they pick up the theme's --vp-* variables and follow the light/dark toggle. Co-Authored-By: Claude Opus 5.5 --- .vitepress/theme/style.css | 11 ++ features/charts/rating-accuracy.svg | 4 + features/charts/rating-confidence.svg | 4 + features/charts/rating-daily-ac.svg | 4 + features/charts/rating-overall.svg | 4 + features/charts/rating-persistence.svg | 4 + features/charts/rating-volume.svg | 4 + features/rating.md | 23 +++ scripts/rating_charts.py | 202 +++++++++++++++++++++++++ 9 files changed, 260 insertions(+) create mode 100644 features/charts/rating-accuracy.svg create mode 100644 features/charts/rating-confidence.svg create mode 100644 features/charts/rating-daily-ac.svg create mode 100644 features/charts/rating-overall.svg create mode 100644 features/charts/rating-persistence.svg create mode 100644 features/charts/rating-volume.svg create mode 100644 scripts/rating_charts.py diff --git a/.vitepress/theme/style.css b/.vitepress/theme/style.css index 18d375e..7adac7d 100644 --- a/.vitepress/theme/style.css +++ b/.vitepress/theme/style.css @@ -382,3 +382,14 @@ kbd { .vp-doc img { border: 1px solid var(--mono-gray-300); } + +/* 评分页的图表(内联 SVG,颜色取主题变量,随深浅色切换) */ +.rating-chart-figure { + margin: 24px 0; +} + +.rating-chart-figure svg { + display: block; + width: 100%; + height: auto; +} diff --git a/features/charts/rating-accuracy.svg b/features/charts/rating-accuracy.svg new file mode 100644 index 0000000..8212da7 --- /dev/null +++ b/features/charts/rating-accuracy.svg @@ -0,0 +1,4 @@ +准确度随 P 的变化:P 在 40%–55% 时满分,过高反而减分0%30%50%100%0%15%40%55%90%100%评测过的提交中解决新题的比例 P准确度满分最低 30%几乎不失败,更像复制的代码53210.50≈ 平均每道已解决的题失败几次(题数很多时) diff --git a/features/charts/rating-confidence.svg b/features/charts/rating-confidence.svg new file mode 100644 index 0000000..871b22b --- /dev/null +++ b/features/charts/rating-confidence.svg @@ -0,0 +1,4 @@ +准确度的可信度 K = S / (S + 80)0%25%50%75%100%02004006008001000解决的题数 S可信度 K80 题 → 50%320 题 → 80%720 题 → 90% diff --git a/features/charts/rating-daily-ac.svg b/features/charts/rating-daily-ac.svg new file mode 100644 index 0000000..1e8f03a --- /dev/null +++ b/features/charts/rating-daily-ac.svg @@ -0,0 +1,4 @@ +每天 AC 数与折算后计入的次数:超过 8 次后按对数增长0102030405060020406080100120当天的 AC 数 a折算后 g(a)≤ 8 次:原样计入不打折AC 14 次 → 约计 12.5 次AC 120 次 → 约计 29.7 次 diff --git a/features/charts/rating-overall.svg b/features/charts/rating-overall.svg new file mode 100644 index 0000000..ce8d939 --- /dev/null +++ b/features/charts/rating-overall.svg @@ -0,0 +1,4 @@ +评分随解决题数的变化,按每题平均失败次数分四条线,背景为用户名颜色红橙蓝绿青灰0800120016002000240030000500100015002000解决的题数 S评分每题失败 1 次每题失败 3 次从不失败每题失败 7 次假设每天 AC 不超过 8 次,平均每天解决 3 题(D = S/3) diff --git a/features/charts/rating-persistence.svg b/features/charts/rating-persistence.svg new file mode 100644 index 0000000..3ce69db --- /dev/null +++ b/features/charts/rating-persistence.svg @@ -0,0 +1,4 @@ +坚持度随有提交天数的变化:1 − e^(−D/250)0%25%50%75%100%02004006008001000有提交的天数 D坚持度100 天 → 33%250 天 → 63%500 天 → 86% diff --git a/features/charts/rating-volume.svg b/features/charts/rating-volume.svg new file mode 100644 index 0000000..3b9683e --- /dev/null +++ b/features/charts/rating-volume.svg @@ -0,0 +1,4 @@ +做题量随解决题数的变化:1 − e^(−S/350)0%25%50%75%100%0500100015002000解决的题数 S(B = 1 时)做题量350 题 → 63%700 题 → 86%1400 题 → 98% diff --git a/features/rating.md b/features/rating.md index 8ca4f6a..94780a6 100644 --- a/features/rating.md +++ b/features/rating.md @@ -1,3 +1,14 @@ + + + + # 用户评分 开启「添加用户评分和用户名颜色」后,用户名会按评分显示颜色,个人主页上也会显示具体分数。 @@ -37,8 +48,12 @@ $$g(a) = \begin{cases} a & a \le 8 \\ 8 + 8 \ln\left(1 + \dfrac{a - 8}{8}\right) 没有 AC 记录时 $B = 1$。$B$ 是一个 0–1 之间的折扣,正常练习和比赛时基本等于 1。 +
+ $$\text{做题量} = 1 - e^{-S B / 350}$$ +
+ 做到几百题以后,继续刷题的收益会越来越小。要拿高分,还得靠准确度。 ### 准确度 @@ -53,14 +68,20 @@ AC 之后重复提交 AC 代码既不增加 $S$ 也不增加 $X$,对评分没 $$\text{准确度} = \begin{cases} 0 & P < 0.15 \\ \dfrac{P - 0.15}{0.25} & 0.15 \le P < 0.4 \\ 1 & 0.4 \le P < 0.55 \\ \max\left(0.3,\ 1 - 2(P - 0.55)\right) & P \ge 0.55 \end{cases}$$ +
+ 题做得越多,准确度越可信,所以它还要乘上可信度: $$K = \frac{S}{S + 80}$$ +
+ ### 坚持度 $$\text{坚持度} = 1 - e^{-D/250}$$ +
+ ### 合在一起 $$\text{评分} = \operatorname{round}\left(3000 \times \left(0.57 \cdot \text{做题量} + 0.33 \cdot \text{准确度} \cdot K + 0.10 \cdot \text{坚持度}\right)^{0.9}\right)$$ @@ -86,6 +107,8 @@ $$\text{评分} = \operatorname{round}\left(3000 \times \left(0.57 \cdot \text{ | F | 和 D 一样,但每天 AC 50 题 | 905 | | G | 新用户,解决 20 题,平均每题失败 3 次 | 246 | +
+ 评分在浏览器里缓存一天,所以今天的提交通常明天才会反映出来。 ## 颜色 diff --git a/scripts/rating_charts.py b/scripts/rating_charts.py new file mode 100644 index 0000000..a19c25d --- /dev/null +++ b/scripts/rating_charts.py @@ -0,0 +1,202 @@ +# 生成 features/rating.md 中的图表:python3 scripts/rating_charts.py +# 公式必须和 features/rating.md 保持一致,改动评分算法时请同步这里并重新生成。 +import math, os + +OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "features", "charts") +os.makedirs(OUT, exist_ok=True) + +# ---------- the formula, exactly as documented ---------- +def g(a): return a if a <= 8 else 8 + 8 * math.log(1 + (a - 8) / 8) +def acc(P): + if P < 0.15: return 0.0 + if P < 0.4: return (P - 0.15) / 0.25 + if P < 0.55: return 1.0 + return max(0.3, 1 - 2 * (P - 0.55)) +def rating(S, X, D, B=1.0): + if S == 0: return 0 + vol = 1 - math.exp(-S * B / 350) + P = (S + 10) / (S + X + 40) + K = S / (S + 80) + per = 1 - math.exp(-D / 250) + return 3000 * (0.57 * vol + 0.33 * acc(P) * K + 0.10 * per) ** 0.9 + +# ---------- theming ---------- +# Each colour is var(--vp-…, var(--local)). Inlined into the page the --vp vars win and +# follow the site's light/dark toggle; opened on its own the --vp vars are undefined, so +# the local vars from """ + +W, H = 600, 360 + +def esc(s): return s.replace("&", "&").replace("<", "<").replace(">", ">") + +class Chart: + def __init__(self, name, label, xr, yr, xt, yt, xlab, ylab, ml=56, mr=24, mt=24, mb=52, ytick_fmt=None, xtick_fmt=None): + self.name, self.label = name, label + self.clip = f"{name}-plot" # ids must be unique: all charts share one page + self.x0, self.x1 = ml, W - mr + self.y0, self.y1 = H - mb, mt + self.xr, self.yr = xr, yr + self.parts = [] + self.xt, self.yt = xt, yt + self.xlab, self.ylab = xlab, ylab + self.yfmt = ytick_fmt or (lambda v: f"{v:g}") + self.xfmt = xtick_fmt or (lambda v: f"{v:g}") + def X(self, v): return self.x0 + (v - self.xr[0]) / (self.xr[1] - self.xr[0]) * (self.x1 - self.x0) + def Y(self, v): return self.y0 - (v - self.yr[0]) / (self.yr[1] - self.yr[0]) * (self.y0 - self.y1) + def add(self, s): self.parts.append(s) + def text(self, x, y, s, fill=FG2, size=13, anchor="start", weight=400, extra=""): + self.add(f'{esc(s)}') + def frame(self): + out = [] + for v in self.yt: + y = self.Y(v) + out.append(f'') + out.append(f'{esc(self.yfmt(v))}') + for v in self.xt: + x = self.X(v) + out.append(f'') + out.append(f'{esc(self.xfmt(v))}') + out.append(f'') + out.append(f'{esc(self.xlab)}') + out.append(f'{esc(self.ylab)}') + return out + def path(self, f, a, b, n=400, stroke=BR, width=2.5, dash=None, clip=True): + pts = [] + for i in range(n + 1): + v = a + (b - a) * i / n + pts.append(f"{self.X(v):.2f},{self.Y(f(v)):.2f}") + d = f' stroke-dasharray="{dash}"' if dash else "" + cp = f' clip-path="url(#{self.clip})"' if clip else "" + self.add(f'') + def dot(self, xv, yv, r=4.5): + self.add(f'') + def guide(self, xv, yv): + x, y = self.X(xv), self.Y(yv) + self.add(f'') + def save(self, before=()): + body = [f''] + body.append(f'') + body += list(before) + body += self.frame() + body += self.parts + svg = (f'' + f'{esc(self.label)}{STYLE}' + "".join(body) + "\n") + with open(f"{OUT}/{self.name}.svg", "w") as fh: + fh.write(svg) + +pct = lambda v: f"{v*100:.0f}%" + +# ---------- 1. daily AC discount g(a) ---------- +c = Chart("rating-daily-ac", "每天 AC 数与折算后计入的次数:超过 8 次后按对数增长", + (0, 120), (0, 60), range(0, 121, 20), range(0, 61, 10), + "当天的 AC 数 a", "折算后 g(a)") +c.add(f'') +c.text(c.X(8) + 6, c.y1 + 16, "≤ 8 次:原样计入", fill=FG2, size=12) +c.path(lambda a: a, 0, 120, n=2, stroke=FG2, width=1.5, dash="5 4") +c.text(c.X(44) - 6, c.Y(44) - 4, "不打折", fill=FG2, size=12, anchor="end") +c.path(g, 0, 120) +for a, dx, dy, anc in [(14, 10, 24, "start"), (120, -8, -14, "end")]: + c.dot(a, g(a)) + c.text(c.X(a) + dx, c.Y(g(a)) + dy, f"AC {a} 次 → 约计 {g(a):.1f} 次", fill=FG, size=13, anchor=anc) +c.save() + +# ---------- 2. volume ---------- +c = Chart("rating-volume", "做题量随解决题数的变化:1 − e^(−S/350)", + (0, 2000), (0, 1), range(0, 2001, 500), [0, .25, .5, .75, 1], + "解决的题数 S(B = 1 时)", "做题量", ytick_fmt=pct) +f = lambda s: 1 - math.exp(-s / 350) +c.path(f, 0, 2000) +for s, dx, dy in [(350, 10, 18), (700, 10, 18), (1400, 0, 22)]: + c.guide(s, f(s)); c.dot(s, f(s)) + c.text(c.X(s) + dx, c.Y(f(s)) + dy, f"{s} 题 → {f(s)*100:.0f}%", fill=FG, size=13) +c.save() + +# ---------- 3. accuracy vs P ---------- +c = Chart("rating-accuracy", "准确度随 P 的变化:P 在 40%–55% 时满分,过高反而减分", + (0, 1), (0, 1), [0, .15, .4, .55, .9, 1], [0, .3, .5, 1], + "评测过的提交中解决新题的比例 P", "准确度", ytick_fmt=pct, xtick_fmt=pct, mt=60) +c.add(f'') +c.path(acc, 0, 1, n=1000) +c.text((c.X(.4) + c.X(.55)) / 2, c.Y(1) + 20, "满分", fill=BR, size=13, anchor="middle", weight=600) +c.text(c.x1, c.Y(.3) + 20, "最低 30%", fill=FG, size=13, anchor="end") +c.text(c.X(.6), c.Y(.44), "几乎不失败,", fill=FG2, size=12) +c.text(c.X(.6), c.Y(.44) + 16, "更像复制的代码", fill=FG2, size=12) +# top axis: fails per solved problem when S is large (P ≈ 1 / (1 + f)) +ty = c.y1 +c.add(f'') +for fpp in [5, 3, 2, 1, .5, 0]: + x = c.X(1 / (1 + fpp)) + c.add(f'') + c.text(x, ty - 10, f"{fpp:g}", fill=FG2, size=12, anchor="middle") +c.text((c.x0 + c.x1) / 2, ty - 30, "≈ 平均每道已解决的题失败几次(题数很多时)", fill=FG2, size=12, anchor="middle") +c.save() + +# ---------- 4. confidence K ---------- +c = Chart("rating-confidence", "准确度的可信度 K = S / (S + 80)", + (0, 1000), (0, 1), range(0, 1001, 200), [0, .25, .5, .75, 1], + "解决的题数 S", "可信度 K", ytick_fmt=pct) +f = lambda s: s / (s + 80) +c.path(f, 0, 1000) +for s, dx, dy in [(80, 10, 18), (320, 6, 22), (720, 0, 22)]: + c.guide(s, f(s)); c.dot(s, f(s)) + c.text(c.X(s) + dx, c.Y(f(s)) + dy, f"{s} 题 → {f(s)*100:.0f}%", fill=FG, size=13) +c.save() + +# ---------- 5. persistence ---------- +c = Chart("rating-persistence", "坚持度随有提交天数的变化:1 − e^(−D/250)", + (0, 1000), (0, 1), range(0, 1001, 200), [0, .25, .5, .75, 1], + "有提交的天数 D", "坚持度", ytick_fmt=pct) +f = lambda d: 1 - math.exp(-d / 250) +c.path(f, 0, 1000) +for d, dx, dy in [(100, 10, 18), (250, 10, 18), (500, 10, 20)]: + c.guide(d, f(d)); c.dot(d, f(d)) + c.text(c.X(d) + dx, c.Y(f(d)) + dy, f"{d} 天 → {f(d)*100:.0f}%", fill=FG, size=13) +c.save() + +# ---------- 6. overall ---------- +BANDS = [ # (lo, hi, name, colour) — same colours as .rating.* in .vitepress/theme/style.css + (2400, 3000, "红", "#dc2f5a"), (2000, 2400, "橙", "#e8590c"), (1600, 2000, "蓝", "#337cf2"), + (1200, 1600, "绿", "#2b9f4f"), (800, 1200, "青", "#1798b0"), (0, 800, "灰", "#78828c")] +c = Chart("rating-overall", "评分随解决题数的变化,按每题平均失败次数分四条线,背景为用户名颜色", + (0, 2000), (0, 3000), range(0, 2001, 500), [0, 800, 1200, 1600, 2000, 2400, 3000], + "解决的题数 S", "评分", mr=132, ml=64) +bg = [] +for lo, hi, name, col in BANDS: + y_hi, y_lo = c.Y(hi), c.Y(lo) + bg.append(f'') + bg.append(f'') + bg.append(f'{name}') +# y tick labels sit on the band boundaries; nudge them so they don't collide with band names +c.yfmt = lambda v: "" +for v in [0, 800, 1200, 1600, 2000, 2400, 3000]: + bg.append(f'{v}') +LINES = [(1, "每题失败 1 次", None, 2.75), (3, "每题失败 3 次", "8 4", 2.25), (7, "每题失败 7 次", "2 4", 2.25), (0, "从不失败", "10 3 2 3", 2.25)] +ends = [] +for fpp, lab, dash, w in LINES: + fn = lambda s, fpp=fpp: rating(s, fpp * s, s / 3) + c.path(fn, 1, 2000, n=600, stroke=FG, width=w, dash=dash) + ends.append([c.Y(fn(2000)), lab, dash, w]) +ends.sort() +for i in range(1, len(ends)): # spread labels at least 17px apart + ends[i][0] = max(ends[i][0], ends[i - 1][0] + 17) +for y, lab, dash, w in ends: + d = f' stroke-dasharray="{dash}"' if dash else "" + c.add(f'') + c.text(c.x1 + 36, y + 4.5, lab, fill=FG, size=12) +c.text(c.x1 - 8, c.y0 - 28, "假设每天 AC 不超过 8 次,", fill=FG2, size=11, anchor="end") +c.text(c.x1 - 8, c.y0 - 12, "平均每天解决 3 题(D = S/3)", fill=FG2, size=11, anchor="end") +c.save(before=bg)