オフラインアプリに「世界ランキング」を実装する — Box-Muller 法と決定論的シード
| 開発記録 | アルゴリズム
タグ: #アルゴリズム #統計 #Flutter #個人開発
サーバーを持たない完全オフラインのアプリで「あなたは上位◯%」を表示したい。平均100・標準偏差15の正規分布に従う10万人の擬似母集団を Box-Muller 法で決定論的に生成し、統計モデルとしてのランキング体験を作った話です。
サーバーなしで「上位◯%」を出したいという無茶
新作アプリを完全オフラインの MVP として設計しました。ネットワーク依存ゼロ、データはすべて端末内で完結。ところが企画段階でどうしても入れたい機能がありました。「世界ランキング」です。スコアを出したあとに「上位 12%」と表示されるのと、ただ数字が出るだけとでは、続けるモチベーションがまるで違います。
でもサーバーはありません。実ユーザーのスコアを集める手段がない。そこで発想を変えました。ランキングの母集団を、統計モデルとして端末内で生成することにしたのです。
正規分布に従う「10万人」を作る
この種のスコアは平均 100・標準偏差 15 の正規分布に従うよう設計するのが定番です。つまり母集団の分布は最初から分かっている。であれば、その分布に従う擬似的なスコア集団を作って、自分のスコアを挿入した位置から順位を計算すればいい。
正規乱数の生成には Box-Muller 法を使いました。一様乱数 u1, u2 から
z = sqrt(-2 ln u1) * cos(2π u2)
score = 100 + 15z
で標準正規分布に従う値が得られます。これで 10 万人分の擬似スコアを生成し、ソートして自分の挿入位置を求めれば「上位◯%」と偏差値が出ます。
「決定論的」であることが大事
ここで重要なのが乱数のシードです。起動のたびにランキングの顔ぶれが変わったら、ユーザーは即座に違和感を持ちます。そこで端末固有のシードから生成することで、同じ端末では常に同じ母集団が再現されるようにしました。乱数なのに決定論的、というのがこの設計の肝です。
同じ技法は「今日の問題」の生成にも使っています。日付文字列(yyyy-MM-dd)を FNV-1a ハッシュで 32bit 整数に変換し、それを問題ジェネレータのシードにする。これですべての端末で「今日の問題」が一致します。サーバーから配信しているように見えて、実は各端末が同じ数式から同じ問題を導出しているだけです。
実装メモ: 10万件をどう持つか
「10 万人分のスコアを毎回生成してソート」は一見重そうですが、実測すると最近の端末では起動時に一度計算してメモリに持つだけで十分でした。それでも気になる場合は、母集団を実際に生成せず、正規分布の累積分布関数(CDF)から直接パーセンタイルを計算する近似式に置き換えられます。母集団方式の利点は「ランキング画面に並ぶ他者のスコア」まで一貫した世界として描けることで、体験の説得力を取るか計算の軽さを取るかの選択になります。うちは体験を取りました。
誠実さとのバランス
もちろんこれは実ユーザーの順位ではありません。統計モデル上の推定順位です。実在の他人と競っていると誤認させる見せ方は避けるべきで、そこは表現に気を使うポイントです。ただ「あなたのスコアは分布上ここに位置する」という情報自体は統計的に誠実で、偏差値と同じ考え方です。
サーバーレスでもランキング体験は作れる。Box-Muller と決定論的シード、覚えておくと個人開発の武器になります。