score 教程
本教程介绍如何根据依赖方、下载量和发布日期计算 MoonBit 包的影响力分数,如何把分数转换成 Web 应用显示的等级和势头标签,以及如何对一组已评分的包排序并解释其分数。这里的数学保持简单;score 设计给出了推导。
快速开始
把模块添加到你的项目:
moon add Luna-Flow/mooncake-impact-factor@0.1.2
在使用它的包的 moon.pkg 中导入该包:
import {
"Luna-Flow/mooncake-impact-factor/score",
}
为一个有 20 个依赖方(其中 4 个为近期)、300 次下载、40 天前发布的包评分:
test "quick start" {
let score = @score.compute_score(20, 4, 300, 40)
println("score = \{score}, rank = \{@score.rank_label(score)}")
}
score = 301.7852882193072, rank = S
分数是一个普通的 Double。 及以上即为等级 S。
日常任务
为一组包排名
不同包的分数可以直接比较,因为每个分数只依赖于该包自己的信号。按分数从高到低排序,并用名称打破并列,使顺序可复现。这正是 Web 应用的 top feed 所用的顺序。
test "rank a list" {
let packages = [
("alice/json", 40, 6, 1200, 20),
("bob/http", 12, 1, 90, 400),
("carol/csv", 12, 1, 90, 400),
("dave/math", 3, 0, 0, 15),
]
let scored = packages.map(p => {
let (name, deps, recent, downloads, days) = p
(name, @score.compute_score(deps, recent, downloads, days))
})
scored.sort_by((a, b) => {
let by_score = b.1.compare(a.1)
if by_score != 0 { by_score } else { a.0.compare(b.0) }
})
for entry in scored {
let (name, score) = entry
println("\{name} \{@score.rank_label(score)} \{score}")
}
}
alice/json S 391.6139680824188
bob/http A 189.57132356978428
carol/csv A 189.57132356978428
dave/math C 59.00068800926255
bob/http 和 carol/csv 的信号相同,因此分数相同;它们的顺序由名称决定。
解释分数的来源
基础分是每个信号各一项之和,发布乘数缩放整个和。因此单独为每个信号评分即可把分数拆成各个部分:
test "explain a score" {
let (deps, recent, downloads, days) = (20, 4, 300, 40)
let from_deps = @score.compute_score(deps, 0, 0, days)
let from_recent = @score.compute_score(0, recent, 0, days)
let from_downloads = @score.compute_score(0, 0, downloads, days)
let total = @score.compute_score(deps, recent, downloads, days)
println("dependents \{from_deps}")
println("recent \{from_recent}")
println("downloads \{from_downloads}")
println("total \{total}")
assert_true((from_deps + from_recent + from_downloads - total).abs() < 1.0e-9)
}
dependents 122.63336379149948
recent 46.06211305386395
downloads 133.08981137394377
total 301.7852882193072
各部分之和在末位舍入误差范围内等于总分,这就是检查使用容差而不是 == 的原因。
查看发布年龄的影响
activity_multiplier 对近期发布最多奖励 12 %,对陈旧发布最多惩罚 12 %:
test "release age" {
for days in [10, 60, 120, 300, 500] {
println("\{days} \{@score.compute_score(20, 4, 300, days)}")
}
}
10 318.8674743449284
60 301.7852882193072
120 284.703102093686
300 267.6209159680649
500 250.5387298424437
同样的信号在最新发布不足一年时为等级 S,之后为等级 A。
衡量 30 天内的增长
compute_score_snapshot 两次接收四个信号:一次是现在的,一次是 30 天前的。30 天前尚不存在的包,其历史信号为 0:
test "new package snapshot" {
let snapshot = @score.compute_score_snapshot(8, 2, 120, 12, 0, 0, 0, 0)
println(snapshot.score_growth_ratio_30d)
println(snapshot.rank_label)
println(snapshot.momentum_label)
}
1
A
Hot
分数从 开始增长,所以增长率报告为 (100 %)。该包是 Hot 而不是 Rising,因为 Rising 至少需要三个近期依赖方,而它只有两个。
把快照交给其他工具
ScoreSnapshot 用 Json(...) 转换为 JSON。这正是 cli 命令的输出:
test "snapshot as JSON" {
let snapshot = @score.compute_score_snapshot(20, 4, 300, 40, 10, 2, 0, 10)
println(Json(snapshot).stringify())
}
{"score":301.7852882193072,"score_30d_ago":135.2764584196223,"score_growth_30d":166.5088297996849,"score_growth_ratio_30d":1.2308780976744749,"rank_label":"S","momentum_label":"Rising","activity_multiplier":1.06}
用 @json.from_json 读回,这需要在导入中包含 moonbitlang/core/json:
test "snapshot from JSON" {
let snapshot = @score.compute_score_snapshot(20, 4, 300, 40, 10, 2, 0, 10)
let back : @score.ScoreSnapshot = @json.from_json(Json(snapshot))
inspect(back.rank_label, content="S")
assert_eq(Json(back), Json(snapshot))
}
ScoreSnapshot 没有实现 Eq,所以检查比较的是 JSON 形式。
进阶
像索引构建器那样准备信号。 这些函数接受任意整数,但 Web 应用中的分数来自特定定义:dependents 统计至少有一个版本依赖本包的包,recent_dependents 统计其中首个此类版本不超过 180 天的包,历史信号则是 30 天前的同样计数,且下载量设为 0。架构指南描述了完整流程。若要把自己的分数与发布的分数比较,请使用相同的定义。
让标签与分数保持在一起。 rank_label 和 compute_momentum_label 是独立的函数,以便你为别处计算出的分数打标签,但只有 compute_score_snapshot 能保证标签与数字一致。请存储快照,而不是根据四舍五入后的数字重新计算标签。
在边界处防止溢出。 没有真实的包拥有 2147483647 个依赖方,但如果信号来自不可信的输入,请先截断:恰好为 2147483647 的计数会使分数变为 NaN,其等级为 D。
test "clamp untrusted counts" {
let raw = 2147483647
let safe = if raw > 1000000000 { 1000000000 } else { raw }
assert_false(@score.compute_score(safe, 0, 0, 0).is_nan())
}
常见陷阱
- 参数顺序颠倒。 所有信号都是
Int,编译器无法区分downloads和dependents。请遵守信号表的顺序。 - 天数而非日期。
days_since_release是整天数,而不是时间戳。负值按0处理,并获得最高乘数。 - 新包的增长率。 比值为
1可能表示”翻倍”,也可能表示”从无到有”。检查score_30d_ago == 0.0来区分二者。 - 用
==比较。 分数是浮点数;比较计算出的和时请使用容差。 - 势头需要快照中的比值。
compute_momentum_label信任你传入的比值。传入百分数(35.0)而不是小数(0.35)会使比值条件对任何实际增长都成立。
下一步
- score API:所有函数和阈值。
- score 设计:公式的推导、以计数表示的等级阈值以及精度界限。
- cli 教程:从 Python 或 shell 计算快照。
- static_search 教程:在浏览器中搜索已评分的包。