score の設計
このページでは src/score/impact_factor.mbt のスコアモデルの性質を導き、なぜこの形なのかを説明します。関数の一覧は score API に、インデックスビルダーがシグナルをどこから得るかは アーキテクチャガイド にあります。
設計目標
スコアは、ローカルのレジストリインデックスから得られるシグナルをもとに、エコシステムがどれだけ依存しているかでレジストリスナップショットのパッケージを順位付けしなければなりません。計算が安価で、決定的で、説明可能である必要があります。Web ページの読者があるパッケージが別のパッケージより上位にある理由を理解でき、同じシグナルからは MoonBit、インデックスビルダー、ブラウザーのどこでも同じスコアが得られなければなりません。
数学的背景
スコア
被依存数、最近の被依存数、ダウンロード数をそれぞれ 、、、最新リリースからの日数を とします。実装が計算するのは次の値です。
ここで は基本スコア、 は次の階段関数です。
負の は として扱います。スコアに入るのはこれだけで、レジストリのほかの部分に対する正規化はありません。したがって、ほかのパッケージが追加されてもあるパッケージのスコアは変わりません。
対数指数
なので、基本スコアは重み付き積の対数です。
積 はずらしたカウントの Cobb–Douglas 指数11 Cobb–Douglas 形 は生産の経済学に由来します(Cobb と Douglas、1928 年)。その対数は について線形なので、 による順位付けは、ずらしたカウントの重み付き幾何平均による順位付けと同じになります。 であり、重みはその弾力性です: 。ここから 2 つの帰結がすぐに得られます。
倍にすると定数が加わる。 なので、 を倍にすると、 がいくつであっても は 点増えます。同じ一歩は、最近の被依存数では 点、ダウンロード数では 点の価値です。被依存数 1000 のパッケージが次の 1001 件で得る分は、被依存数 10 のパッケージが次の 11 件で得る分と同じです。
収穫逓減。 被依存パッケージが 1 つ増えると、加わる値は
です。ここで を として使いました。被依存パッケージ 1 つの限界的な価値は のように減っていくので、単一のシグナルがランキングを支配することはありません。
カウントで見たランクのしきい値
ランク区分は に対するしきい値で、S は から、A は から、B は から、C は からです。 を逆にたどると、これがカウントで何を意味するかがわかります。 で重み のシグナルだけが 0 でないとき、スコアがしきい値 に達する条件は
です。したがって最小の整数カウントは です。
| しきい値 | 被依存数のみ() | 最近の被依存数のみ() | ダウンロード数のみ() |
|---|---|---|---|
C () | 3 | 6 | 9 |
B () | 18 | 58 | 148 |
A () | 114 | 785 | 3575 |
S () | 936 | 15208 | 135697 |
たとえば に対して なので、被依存数だけで S に届く最初のカウントは 936 です。実際にはシグナルが組み合わさり、被依存数 20、最近の被依存数 4、ダウンロード数 300 で、すでに になります。
成長とモメンタム
スナップショットはスコアを現在のシグナルと 30 日前のシグナルで 2 回評価し、次のように定義します。
なので、 となるのは過去のカウントがすべて のとき、つまり 30 日前に被依存パッケージもダウンロードもなかったときに限られます。そのようなパッケージでは相対成長 が定義されないため、実装は の代わりに (つまり 100 %)を使い、 を有限に保って保存や並べ替えができるようにしています。
モメンタムラベルは 2 段階で 3 つの条件を調べます。
Rising の条件は Hot の条件を含意するので、これらの区分は独立したタグではなく、1 つの尺度の入れ子になった段階です。 のとき は と同じなので、Rising には旧スコアの 倍以上のスコアかつ 点の絶対的な伸びが必要です。絶対的な下限は、ごく小さなパッケージが 点から 点になっただけで上昇扱いされるのを防ぎ、相対的な下限は、大きなパッケージが大きな基数のノイズだけで上昇扱いされるのを防ぎます。
設計上の判断
カウントの対数
課題。 被依存数とダウンロード数は裾の重い分布をしています。数千を持つパッケージはわずかで、大半は 0 です。線形のスコアでは、ランキングが各シグナルで最大のパッケージを並べたリーダーボードになってしまいます。
選択肢。 生のカウント、レジストリ内の順位やパーセンタイル、平方根、対数。
採用。 。1 だけずらすことで が有限に保たれ、 がちょうどシグナルをまったく持たないパッケージに対応します。パーセンタイルはレジストリ全体を必要とし、ほかのパッケージが現れるとスコアが変わるため、CLI の 1 回に 1 パッケージという約束を壊します。平方根は増え続けるうえ、上で導いたスケール不変性を持ちません。
加法的な重み
課題。 3 つのシグナルを 1 つの数にまとめる必要があります。
採用。 重み による対数の重み付き和です。被依存数の合計は確立した採用度を表すので最も重くします。ダウンロード数は外部の人気の目安で、ビルダーが取得できなかったパッケージでは欠けるため最も軽くします。最近の被依存数は合計に上乗せして数えるので、最近のウィンドウ内の被依存パッケージは両方の項に寄与します。最近の項は現在の採用に対するボーナスであって、別の集団ではありません。重みはこのプロジェクトの編集上の選択であり、データに当てはめたパラメーターではありません。
鮮度は項ではなく乗数で
課題。 古く保守されていないパッケージがいつまでもランクを保つべきではありませんが、年数が採用度を上回ってもいけません。
採用。 から の間の乗法的な階段関数です。 に掛けるため、スコアの変化は最大でも で、同じシグナルを持つ最も新しいパッケージと最も古いパッケージの比は です。ランクの境界を 1 つまたぐことはあります(たとえば は なら S、 なら A)が、使われていないパッケージがランク入りすることはありません。 は のままです。加法的な年数の項では、使われていないが最近リリースされたパッケージに正のスコアが付いてしまいます。
ラベルには固定のしきい値
課題。 Web ページには短く安定したラベルが必要です。
採用。 と に対する定数のしきい値です。そのためラベルはどのスナップショットでも同じ意味を持ち、レジストリ全体の統計を必要としません。分位による区分(「上位 5 %」)は、パーセンタイルのスコアと同様にレジストリ全体を必要とします。
整数を入力、Double を出力
課題。 シグナルはカウントですが、スコアは実数値です。
採用。 入力はすべて Int で、負の値は拒否せずに切り上げるので、どの関数も全域的でデータベースの生の値にそのまま適用できます。スコア関数は中断せず Result も返しません。有限でない出力は 数値精度 で説明するオーバーフローだけです。
正しさと不変条件
単調性
のカウントに対して、 は 、、 について単調非減少であり、カウントが 未満である限り狭義単調増加です。 は狭義単調増加、重みは正、 なので、
を超えると、Float を経由する変換(後述)が隣り合うカウントを同じ値に写すことがあり、狭義の増加は非減少に弱まります。 が非増加なので、 は について非増加です。impact_factor_test.mbt のブラックボックステストは、被依存数、ダウンロード数、リリースからの経過についてこの性質の例を確かめています。
値の範囲
で、等号が成り立つのは のときに限られます。カウントが 以下なら なので、
ラベルは全域的
rank_label と compute_momentum_label は、どんな入力にもいずれかのラベルを返します。NaN との比較はすべて偽なので、NaN のスコアはランク D、モメンタム Stable になります。
数値精度
log_signal は を Float に変換してから Double で対数を取ります。 までの整数はすべて Float で正確に表せ、それを超えると変換は最近接丸めとなり、相対誤差は です。すると
となり、この変換による の誤差は最大でも で、これに数回の演算での通常の Double の丸めが加わります。インデックスビルダーには、Float を経由せずに math.log1p を呼ぶ未使用の Python 版の式(scripts/build_index.py の compute_score)がまだ残っており、MoonBit の結果とはこの上限に末尾数単位を加えた範囲で一致します。データベース自体は MoonBit の CLI を通じて埋められます。
加算 は Int で行われます。 では に折り返し、負の数の対数は NaN なので、スコアは NaN になります。
スナップショットの一貫性
compute_score_snapshot はすべてのフィールドを同じ 2 回の compute_score 呼び出しから計算するので、score_growth_30d == score - score_30d_ago は厳密に成り立ち(同じ浮動小数点の引き算です)、rank_label と momentum_label は常に保存された数値のラベルです。
スコアの並べ方
このパッケージはスコアを計算するだけで、並べ替えは利用側が行います。リポジトリ内のどの並び順も同順位を決定的に解決します。ランキングのフィードと既定の検索順は score の降順、次に full_name の昇順で、Hot と Rising のフィードは score_growth_30d の降順、次に score、次に full_name です。ブラウザー検索の並び順は static_search の設計 にあります。
採用しなかった案
- 依存グラフ上の PageRank 型の中心性は重要なパッケージから依存されることを評価しますが、グラフ全体、反復ソルバー、減衰パラメーターが必要で、パッケージページで説明することもできません。直接の被依存数はその反復の第一歩であり、この規模のレジストリには十分です。
- 推移的な被依存パッケージは使っていません。同じ下流パッケージをあらゆる経路で何度も数え、対数で補正できる以上に低レベルのパッケージを優遇してしまいます。
- ラベル付きのランキングから重みを学習するには、存在しないラベルが必要です。固定の重みはコードとこのページに明記されています。
- 負の入力に
Resultを返すと、明白な意味(シグナルなし)を持つ状況のために、すべての呼び出し側にエラー処理を押し付けることになります。
境界
- スコアが測るのは 1 つのレジストリスナップショット内での採用度です。コードの品質、正しさ、安全性、保守の労力は測りません。
- このパッケージはシグナルを与えられたまま受け取ります。シグナルを集め、どの被依存パッケージを最近とみなし、どのダウンロード数を信頼するかを決めるのはインデックスビルダーの役目で、アーキテクチャガイド で説明しています。
- ビルダーは現在、過去のダウンロード数として
0を渡しているため、score_growth_30dには現在のスコアのダウンロード項 が丸ごと含まれます。成長は被依存数と合わせて読んでください。モメンタムラベルは最近の被依存パッケージを要求することでそうしています。 - パッケージ間の正規化も、ウィンドウ内の時間減衰も、信頼区間もありません。スコアは 4 つの整数の決定的な関数です。
- カウント
2147483647には対応していません(スコアがNaNになります)。
Footnotes
-
Cobb–Douglas 形 は生産の経済学に由来します(Cobb と Douglas、1928 年)。その対数は について線形なので、 による順位付けは、ずらしたカウントの重み付き幾何平均による順位付けと同じになります。 ↩