score API

パッケージ Luna-Flow/mooncake-impact-factor/score は、4 つの整数シグナルから MoonBit パッケージのインパクトスコアを計算し、スコアにランクとモメンタムの区分を付け、すべてを 1 つの ScoreSnapshot にまとめます。どの関数も純粋かつ全域的で、中断することも隠れた状態を持つこともありません。

ソース: src/score/impact_factor.mbt。式の背後にある数学は score の設計 にあります。

moon.pkg でパッケージをインポートします。

import {
  "Luna-Flow/mooncake-impact-factor/score",
}

シグナル

どのスコア関数も、同じ 4 つのシグナルを次の順で受け取ります。

パラメーター意味
dependentsこのパッケージに依存しているパッケージの数。
recent_dependentsそれらのうち、最近のウィンドウ内で初めて現れたものの数。
downloadsダウンロード数。不明な場合は 0。
days_since_release最新リリースからの経過日数(整数)。

これらの関数はシグナル同士の整合性(たとえば recent_dependents <= dependents)を確認しません。負の値は 0 に切り上げられます。シグナルはインデックスビルダーがローカルのレジストリスナップショットから算出します。アーキテクチャガイド を参照してください。

スコア

compute_score

compute_score はパッケージのインパクトスコアを返します。

pub fn compute_score(Int, Int, Int, Int) -> Double

σ(n)=ln⁡(1+max⁡(n,0))\sigma(n) = \ln(1 + \max(n, 0)) とし、被依存数を DD、最近の被依存数を RR、ダウンロード数を WW、リリースからの日数を tt とすると、結果は次のとおりです。

S=m(t) (38 σ(D)+27 σ(R)+22 σ(W))S = m(t)\,\bigl(38\,\sigma(D) + 27\,\sigma(R) + 22\,\sigma(W)\bigr)

ここで m(t)m(t) は activity_multiplier(t) です。スコアが 00 になるのは 3 つのカウントがすべて 00 以下のときに限られ、どのカウントが増えてもスコアが減ることはありません。設計上スコアに上限はありませんが、カウントが 231−12^{31} - 1 未満であればおよそ 20942094 を下回ります。

test "compute_score" {
  let score = @score.compute_score(20, 4, 300, 40)
  inspect(score, content="301.7852882193072")
  inspect(@score.compute_score(0, 0, 0, 10), content="0")
}

activity_multiplier

activity_multiplier は、スコア全体に掛かるリリース鮮度係数 m(t)m(t) を返します。

pub fn activity_multiplier(Int) -> Double

負の days_since_release は 0 として扱われます。

リリースからの日数乗数
0 〜 301.12
31 〜 901.06
91 〜 1801.0
181 〜 3650.94
366 以上0.88
test "activity_multiplier" {
  inspect(@score.activity_multiplier(-3), content="1.12")
  inspect(@score.activity_multiplier(90), content="1.06")
  inspect(@score.activity_multiplier(400), content="0.88")
}

clamp_non_negative

clamp_non_negative は value が非負ならそれを、そうでなければ 0 を返します。

pub fn clamp_non_negative(Int) -> Int

スコア関数はすべてのシグナルにこれを適用します。呼び出し側が同じ方法でシグナルを準備できるよう公開されています。

test "clamp_non_negative" {
  inspect(@score.clamp_non_negative(-7), content="0")
  inspect(@score.clamp_non_negative(12), content="12")
}

ラベル

rank_label

rank_label はスコアをランク区分 S、A、B、C、D のいずれかに対応付けます。

pub fn rank_label(Double) -> String
ランク条件
Sscore >= 260.0
A180.0 <= score < 260.0
B110.0 <= score < 180.0
C50.0 <= score < 110.0
Dscore < 50.0、または score が NaN
test "rank_label" {
  inspect(@score.rank_label(260.0), content="S")
  inspect(@score.rank_label(259.99), content="A")
  inspect(@score.rank_label(12.0), content="D")
}

compute_momentum_label

compute_momentum_label はスコアの伸びの速さを Rising、Hot、Stable に分類します。

pub fn compute_momentum_label(Double, Double, Double, Int) -> String

引数は現在のスコア SS、30 日前のスコア S30S_{30}、成長率 rr、最近の被依存数 RR です。成長量を G=S−S30G = S - S_{30} とすると、次のようになります。

ラベル条件
RisingG≥35G \ge 35 かつ r≥0.35r \ge 0.35 かつ R≥3R \ge 3
HotRising ではなく、かつ G≥18G \ge 18、r≥0.18r \ge 0.18、R≥2R \ge 2
Stableそれ以外(いずれかの引数が NaN の場合を含む)

この関数は growth_ratio が 2 つのスコアと一致するかを確認しません。compute_score_snapshot を使えば自動的に計算されます。

test "compute_momentum_label" {
  inspect(@score.compute_momentum_label(140.0, 100.0, 0.4, 3), content="Rising")
  inspect(@score.compute_momentum_label(140.0, 100.0, 0.4, 2), content="Hot")
  inspect(@score.compute_momentum_label(105.0, 100.0, 0.05, 1), content="Stable")
}

スナップショット

ScoreSnapshot

ScoreSnapshot は、ある時点での 1 つのパッケージの評価結果一式です。

pub struct ScoreSnapshot {
  score : Double
  score_30d_ago : Double
  score_growth_30d : Double
  score_growth_ratio_30d : Double
  rank_label : String
  momentum_label : String
  activity_multiplier : Double
} derive(ToJson, @debug.Debug, @json.FromJson)
フィールド意味
score現在のスコア SS。
score_30d_ago過去のシグナルから計算したスコア S30S_{30}。
score_growth_30dG=S−S30G = S - S_{30}。スコアが下がった場合は負になります。
score_growth_ratio_30dS30>0S_{30} > 0 のときは G/S30G / S_{30}。それ以外では G>0G > 0 なら 11、G≤0G \le 0 なら 00。
rank_labelrank_label(score).
momentum_labelcompute_momentum_label(score, score_30d_ago, score_growth_ratio_30d, recent_dependents).
activity_multiplier現在のシグナルに対する activity_multiplier(days_since_release)。

フィールドはパッケージ外からは読み取り専用です。スナップショットは compute_score_snapshot で作成してください。ToJson はフィールド名をキーとするオブジェクトを書き出し、これが cli コマンド の出力形式です。FromJson は同じ形式を読み戻し、フィールドが欠けていたり型が違ったりすると @json.JsonDecodeError を送出します。Debug により debug_inspect と Repr(...) が使えます。この構造体は Eq を実装していないため、スナップショットはフィールドか JSON 形式で比較してください。

test "ScoreSnapshot JSON round trip" {
  let snapshot = @score.compute_score_snapshot(8, 2, 120, 12, 0, 0, 0, 0)
  let json = Json(snapshot)
  let back : @score.ScoreSnapshot = @json.from_json(json)
  assert_eq(Json(back), json)
  inspect(back.momentum_label, content="Hot")
}

compute_score_snapshot

compute_score_snapshot はパッケージの現在と 30 日前のスコアを計算し、成長量、2 つのラベル、乗数を 1 回の呼び出しで導出します。

pub fn compute_score_snapshot(Int, Int, Int, Int, Int, Int, Int, Int) -> ScoreSnapshot

最初の 4 つの引数は現在のシグナル、残りの 4 つは 30 日前のシグナルで、どちらもシグナル表の順に並べます。

compute_score_snapshot(
  dependents, recent_dependents, downloads, days_since_release,
  historical_dependents, historical_recent_dependents,
  historical_downloads, historical_days_since_release,
)

モメンタムラベルには現在の recent_dependents が使われます。30 日前に存在しなかったパッケージは過去のシグナルをすべて 0 として表します。このとき score_30d_ago は 00、成長率は 11 になります。

test "compute_score_snapshot" {
  let snapshot = @score.compute_score_snapshot(20, 4, 300, 40, 10, 2, 0, 10)
  debug_inspect(
    snapshot,
    content=(
      #|{
      #|  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,
      #|}
    ),
  )
}

非推奨

ScoreSnapshot には、以前の MoonBit が派生トレイトから暗黙に生成していたメソッドが 3 つ残っています。これらはインターフェースファイルから隠されており、使うと警告が出ます。

非推奨代わりに使うもの
ScoreSnapshot::to_json(s)Json(s)
ScoreSnapshot::from_json(json, path)@json.from_json(json)
ScoreSnapshot::to_repr(s)Repr(s)、debug_inspect、または @debug.to_string