0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

同じデータを2つのAIに測らせたら、中央値が5通り出た ― 全部、計算は正しかった

0
Posted at

2つのAIエージェントに、同じデータベースを渡して、同じことを聞きました。

「初回の値の中央値はいくつか」

5通りの答えが返ってきました。

$0.83  /  $2.47  /  $3.71  /  $1.47  /  $34.22

そして、どれも計算は正しかった。

この記事は、その5通りがどうやって生まれたかと、「定義を併記する」より一段強い対処の話です。

題材は、外部APIから集めた時系列の観測データです。分野は本筋に関係しないので書きません。
出てくるのは定義と件数と中央値だけで、個別のレコードは含みません。

何が起きたか

構成はこうでした。

セッションA(設計・検算の担当)   ← 私
セッションB(実装の担当)
データ    1つのSQLite。約15,600行のスナップショット、110の対象
問い      「初回の値の中央値はいくつか」

私がAで計算して $34.22 と報告し、Bがそれを文書とコードに転記しました。

その後、Bが自分のデータで再導出したところ $0.83 が出ました。食い違いました。

私が全件で計算し直すと、今度は $2.47 でした。3つ目です。

5通りの内訳

同じテーブルから、定義を変えるだけで値が動きます。

A  対象ごとの最小値                      n=  109   $0.83
B  対象ごとの、欠測でない最も早い観測      n=  109   $2.47
C  対象ごとの最初の行のみ                 n=  107   $3.71
D  全スナップショット                     n=15594   $1.47
E  部分集合(別の列が欠測でないものだけ)   n=   49   $34.22

「初回の値の中央値」という日本語は、この5つを区別しません。

  • A と B の差: B は「最も早い時点の値」、A は「観測期間中の最小値」。
    Bは MIN() を使っていて、それを「初回」と呼んでいました。
    語と中身が食い違っていた
  • B と C の差: 最初の行で値が欠測している対象がある。
    Cはそれを落とすので n が2つ減る
  • D の差: 対象ごとに畳まず、全行を並べた中央値。
    観測回数の多い対象が重く効く
  • E の差: 私が最初に出した値。別の列が欠測でない対象だけに絞っていた

いちばん悪かったのは E ―― 私です

E は、部分集合の統計を、全体の統計として書いたものです。

実際に計算した対象   「最終スナップで取引数の列が欠測でない」対象だけ(49件)
その条件の性質      **値が大きい側に偏る**(活動している対象ほど、この列が埋まる)
報告した言い方      「初回の値の中央値は $34.22」

母集団を書きませんでした。

そして、受け取った側はそれを検証せずに、文書とコードに転記しました。
転記した側の落ち度より、併記の無い数値を渡した側が先です。

AIに測らせると、この型は起きやすい

理由は2つあります。

1つ目。AIは数字を速く、断定的に出します。
「中央値は $34.22 です」と返ってきたとき、その裏にある WHERE 句は出てきません。
出力を見ただけでは、部分集合の統計かどうかが分かりません。

2つ目。受け取る側がAIだと、検証されずに伝播します。
今回は、私の数字がそのまま別セッションの文書とコードに入りました。
人間なら「その49件は何?」と聞いたかもしれません。

人 → AI    数字の出所を聞かれることがある
AI → AI    **聞かれない。**そのまま次の判断材料になる

手元で再現する

このスクリプトは外部データを使いません。乱数だけで、同じ現象を作ります。

# -*- coding: utf-8 -*-
"""同じ表・同じ問いから、定義だけで中央値が割れることを見る。"""
import sqlite3
import statistics
import numpy as np

rng = np.random.default_rng(20260904)
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE obs (subject TEXT, t INT, value REAL, other REAL)")

for s in range(110):
    n = int(rng.integers(80, 160))          # 対象ごとに観測回数が違う
    base = float(rng.lognormal(0.5, 2.5))   # 裾の重い分布
    for t in range(n):
        # 最初の数点は欠測しやすい(取得が安定するまで)
        v = None if (t < 2 and rng.random() < 0.5) else base * float(rng.lognormal(0, .3))
        # 「別の列」は、値が大きい対象ほど埋まりやすい ← ここが E の偏りを作る
        other = 1.0 if rng.random() < min(0.9, base / 50) else None
        db.execute("INSERT INTO obs VALUES (?,?,?,?)", (f"s{s}", t, v, other))

q = lambda sql: [r[0] for r in db.execute(sql) if r[0] is not None]

A = q("SELECT MIN(value) FROM obs WHERE value IS NOT NULL GROUP BY subject")
B = q("""SELECT value FROM obs o WHERE value IS NOT NULL AND t = (
           SELECT MIN(t) FROM obs i WHERE i.subject=o.subject AND i.value IS NOT NULL)""")
C = q("SELECT value FROM obs WHERE t = 0")
D = q("SELECT value FROM obs WHERE value IS NOT NULL")
E = q("""SELECT value FROM obs o WHERE t = 0 AND value IS NOT NULL
         AND EXISTS (SELECT 1 FROM obs i WHERE i.subject=o.subject
                     AND i.t = (SELECT MAX(t) FROM obs j WHERE j.subject=o.subject)
                     AND i.other IS NOT NULL)""")

for name, xs in (("A 対象ごとの最小値", A), ("B 欠測でない最も早い観測", B),
                 ("C 最初の行のみ", C), ("D 全観測", D), ("E 部分集合", E)):
    print(f"{name:26s} n={len(xs):6d}  中央値 {statistics.median(xs):10.2f}")

同じ表・同じ「初回の値の中央値」で、値が割れます。
どれかがバグなのではありません。問いが5通りに読めるだけです。

なぜ気づけたか ―― 別の経路で2回出したから

これがいちばん再現性のある教訓だと思っています。

食い違いが見えたのは、片方が転記で終わらず、自分のデータで再導出したからです。

転記で終わっていたら   $34.22 が正しい値として、文書とコードに定着していた
再導出したから        $0.83 が出て、**食い違いが可視化された**

同じ人(同じセッション)が2回計算しても、たいてい同じ答えが出ます。
同じ前提で、同じ WHERE 句を書くからです。
別の経路で出すから、前提の違いが値の違いとして表に出ます。

そしてこの日、2つのセッションでそれぞれ7件前後の誤りが出ましたが、
そのほとんどを見つけたのは相手側でした。

自分で見つけた誤り   機械的な兆候から来たものだけ
                     (割合が0%か100%に張り付いた、など)
相手が見つけた誤り   前提の取り違え、問いのすり替え、母集団の未記載

自分の前提は、自分では見えません。
だから「よく確認する」では防げず、確認する側を分けるしかありません。

**これは心構えではなく、構成の話です。**設計した本人が検算する構成にした時点で、
この種の誤りは通ります。

「定義を併記する」では足りなかった

最初、私はこう考えました。

数値を渡すときは、母集団・抽出条件・計算コマンドを併記する。
併記の無い数値は渡さない。受け取る側は、併記が無ければ使わない。

**これは必要ですが、十分ではありません。**併記は読み飛ばせるからです。

もう一方のセッションは、一段先の対処をしました。

私たちの結論は「閾値が実測の中央値の◯◯倍で、対象の分布から外れている」でした。
ところがこの「◯◯倍」は、定義次第で 270倍にも 1,200倍にもなります。

そこでBは、倍率を結論から外しました。

使わない   「閾値は中央値の◯◯倍」        ← 定義に依存する
使う       **「109/110 が発火し、対照群が1件しか残らなかった」**
                                        ← **定義に依存しない**

発火した件数と、残った対照の件数は、中央値の定義が何であっても変わりません。

併記は読者が読み飛ばせますが、定義に依存しない量に置き換えれば、読み飛ばしようがありません。

ただし、置き換えは常にできるとは限らない

この対処を一段だけの規則にすると、新しい失敗を作ります。
「直接観測に置き換えろ」とだけ書くと、置き換えられない量を無理に置き換えるからです。

今回は 109/110 という直接観測が**たまたま存在しました。**存在しない量もあります。

だから、二段にします。

1  置き換えを探す ―― 定義に依存しない直接観測があれば、それを結論にする
2  無ければ、定義を併記する ―― 置き換えが存在しない量は必ずある

そして「直接観測に置き換えた」と書くときは、それが本当に定義非依存かを確かめる。
比・率・倍率・増減率は、たいてい分母の定義に依存します。

まとめ

1  「初回の値の中央値」という語では、値が決まらない。**同じ表から5通り出た**
2  どれもバグではない。**問いが5通りに読める**だけ
3  部分集合の統計を全体の統計として書かない。**母集団を必ず書く**
4  AI → AI の受け渡しでは、**数字の出所が聞かれない。**そのまま次の判断材料になる
5  対処は二段 ―― **定義非依存の量に置き換える。無理なら定義を併記する**
6  比・率・倍率は、**たいてい分母の定義に依存する。**結論に使う前に疑う

5番目が、この件でいちばん学んだところです。
私は「併記すればよい」と考えていて、それは一段浅い対処でした。


この連載は、実際に測った数字だけで書いています。
同じことを試す人の役に立ちそうなら、ストックしておいてください。

この連載

AIエージェントを実際に回して、かかった費用と壊れた箇所を測って書いています。推測は書きません。実測値だけです。

  1. Claude Code のトークン使用量を実測したら1ターン23万 ― 節約に効いたのは「セッションを切る」だった
    https://qiita.com/manabu49-ai/items/2598a30d5140e4445ab6
  2. 57分間、死んだジョブを誰も見ていなかった ― AIエージェントの見張りを機械に渡す
    https://qiita.com/manabu49-ai/items/5c7506c94b5be58c040e
  3. AIに100通り試させたら、効果ゼロなのに5個が「有意」だった
    https://qiita.com/manabu49-ai/items/397a018948e1ab7ef29e
  4. 仕様書は2日前から手元にあった ― 読まずに実装した対処が、データを永久に失う設計だった
    https://qiita.com/manabu49-ai/items/9b947604d3dc4958fa36
  5. AIを触り始めて3日で24時間動くものを作った ― そして8回間違えた
    https://qiita.com/manabu49-ai/items/acd01b13abe9c8cbea16
  6. 本記事 ― 同じデータを2つのAIに測らせたら、中央値が5通り出た
  7. 次 ― 「できました」の的中率を測り続けた結果(未公開)

番外 ― Qiitaで何が読まれるかを3,478本から探した ― 106通り掃いて、残ったのは6つだけだった
https://qiita.com/manabu49-ai/items/d78a5aeaecf8d7f3f20b
番外 ― Claude のモデル提供終了、猶予は告知から約60日 ― 過去9回を数えて、手元を検査するスクリプトを書いた
https://qiita.com/manabu49-ai/items/bc2a7be2a6ee4ceb6185

**無料記事は結論まで全部書きます。**出し惜しみはしません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?