平均も分散も相関も同じまま、恐竜をサーモンに変えてみた。焼きなまし法でDatasaurusを再現する
約9分で読めます

平均も分散も相関も同じまま、恐竜をサーモンに変えてみた。焼きなまし法でDatasaurusを再現する


「平均・分散・相関係数がすべて同じなのに、散布図を描くと恐竜や星や円になる」——Datasaurus dozenという有名なデータセット群がある。「要約統計量だけ見て可視化をサボるな」という教訓として、データ分析の入門でよく紹介される。

Datasaurus dozenの13個のデータセットすべての散布図。恐竜・円・星・X・水平線・バラバラの雲など全く違う形をしているが、すべて平均(54.3, 47.8)・標準偏差(16.8, 26.9)・相関−0.06で小数2桁まで同一

手元で全13データセットの統計量を計算してみると、確かに平均x・平均y・標準偏差x・y・相関係数の5つが小数2桁まで一致していた。

ところでこのデータ群、どうやって作ったのだろう。調べてみると、原論文(Matejka & Fitzmaurice 2017, “Same Stats, Different Graphs”)の生成アルゴリズムは焼きなまし法だった。前回の記事で「悪化をあえて受け入れる最適化」として実装したばかりのあれだ。せっかくなので鑑賞で終わらせず、自分のコードで再現してみることにした。ついでに、原論文にはない形——サーモン——も作ってみる。

アルゴリズム: 点を1個ずつ動かし、統計量がずれたら差し戻す

仕組みは意外なほどシンプルだった。

for it in range(1_500_000):
    T     = 0.6 * (0.002 / 0.6) ** (it / ITERS)   # 温度: 徐々に冷やす
    i     = rng.integers(n)                        # 点を1個選ぶ
    new   = pts[i] + rng.normal(0, sigma, 2)       # 少しだけ動かす
    d_new = dist_to_shape(new)                     # 目標の形までの距離
    if d_new >= dists[i]:                          # 形から遠ざかる移動は
        if rng.random() >= np.exp(-(d_new - dists[i]) / T):
            continue                               # 温度に応じた確率でのみ許す
    pts[i] = new
    if np.any(np.abs(stats_of(pts) - init) >= 0.01):
        pts[i] = old                               # 統計量が小数2桁を超えてずれたら差し戻し

「形に近づく移動を基本は受け入れ、遠ざかる移動も温度が高いうちは確率的に許す。ただし5つの統計量のどれかが開始時から0.01以上ずれたら即差し戻し」。この2つのルールだけで、統計量を人質に取ったまま形だけが変わっていく。

まず失敗した: 恐竜は「作る」より「壊す」ほうが簡単

最初は逆向きに、「何の変哲もないガウス雲から恐竜を浮かび上がらせる」を狙った。統計量を厳密に恐竜と一致させた正規分布の雲を用意して300万イテレーション回したが、結果はこの通り。

ガウス雲から恐竜を目指した失敗例。点は恐竜の輪郭の近くに集まっているが、頭や脚などの細部が埋まらず、何の形かは判別できない

なんとなく密度の濃淡はできるが、恐竜には見えない。「最寄りの目標点に近づく」だけでは点が一部の目標に群がって輪郭が埋まらず、かといって1点ずつ専用の行き先を割り当てると、今度は統計量の制約と個々の移動が衝突して動けなくなった(60万イテレーションで受理された移動はわずか2,184回)。

考えてみれば、原論文は誰も恐竜を「作って」いない。恐竜は手描きの出発点で、そこから単純な形へ壊していく方向にだけ変形している。細部の多い形を目標にするのは、この単純なアルゴリズムでは難しいのだ。というわけで以降は原論文と同じ向きで行く。

恐竜 → 円・星・X・サーモン

目標を「点の集合」ではなく「連続な輪郭線」にして(点が輪郭上を自由に滑れるようになる)、恐竜から4つの形へそれぞれ150万イテレーション変形させた。

恐竜から円・星・X・サーモンへの変形結果。4つとも形がはっきり分かる。すべて平均(54.26,47.83)・SD(16.77,26.94)・相関−0.06のまま

4つの変形が同時に進むアニメーション。恐竜の点群が徐々にほどけて、円・星・X・サーモンの輪郭に再配置されていく

今度は全部成功した。星は輪郭までの平均距離0.002とほぼ完璧に乗り、サーモンも尾びれの二又までくっきり出た。そして肝心の統計量は、150万イテレーションの間ずっと固定されたままだ。サーモンへの変形で、統計量の値を横に並べて監視するとこうなる。

恐竜がサーモンに変形していくアニメーション。右側に平均x・平均y・SDx・SDy・相関の5つの数値がリアルタイム表示されているが、形が完全に変わる間、5つとも小数2桁で一切動かない

形は完全に別物になるのに、右側の5つの数字は1フレームも動かない。「要約統計量は分布の形をほとんど拘束しない」を、これ以上ないほど直接的に見た気がする。

発見: 統計量は「形」は許すが「小ささ」は許さない

実はサーモンは一度失敗している。最初に描いた横向きの魚(縦の広がりが±21程度)では、何度回しても点が輪郭に乗り切らなかった。原因は単純で、標準偏差が固定されているからだ。SD_y=26.9を保つには点が縦に十分散らばっている必要があり、縦に狭い形の上に全点を置くことは数学的に不可能になる。魚の大きさを変えて確かめた。

同じ魚の輪郭を45%・70%・100%の大きさで目標にした結果。45%では点の大半が輪郭に乗れず周囲に飛び散り(平均距離10.67)、70%でも尾びれ周辺に乗れない点が残る(3.86)。100%でほぼ全点が輪郭に乗る(0.00)

小さい魚では、輪郭に乗れなかった点たちが標準偏差のノルマを果たすために外側に飛び散って残る。原論文の目標形状がどれもキャンバス一杯に大きいのは、美的な選択ではなく数学的な必然だったわけだ。ちなみに円の上下が濃くなっているのも同じ理由(円周上だけでSD_yのノルマを満たすには上下に密度が要る)で、よく見るとオリジナルのcircleデータセットも同じ濃淡を持っている。

焼きなまし vs greedy: 今回は僅差だった

前回のAckley関数では「greedy 0% vs 焼きなまし100%」の圧勝だったので、今回も差がつくと予想していた。結果は少し違った。

円ターゲットでの焼きなまし(最終1.39)とgreedy(最終1.53)の比較。収束曲線はほぼ並走し、最終形はどちらも円に見えるが、greedyの円は点の分布にムラがあり、ところどころ隙間や塊がある

greedy(悪化を一切許さない)でも円は円になった。ただし受理された移動は焼きなましの60,092回に対してgreedyは3,277回と18分の1で、出来上がった円は点の配置にムラが残る。悪化を許せないと、統計量の制約と衝突した点は早々に身動きが取れなくなり、他の点がその分を埋め合わせた歪な配置で固まるようだ。前回ほど劇的な差にならなかったのは、この問題の地形がAckley関数ほど多峰的ではないからだろう。「焼きなましはいつでも圧勝」ではなく、地形の険しさ次第——これも動かしたから分かる相場観だった。

手を動かして意外だったこと

一番の驚きは、「平均・分散・相関」という5つの数字が、分布の形をほとんど何も拘束していなかったことだ。恐竜がサーモンに変わっても数字は1つも動かない。仕事で「平均値」「相関」だけを見て分かった気になっている場面は多いが、その裏では恐竜がサーモンに変わっていても気づけない。

一方で、統計量は形には無力なのに、大きさには厳格だった。SDを固定した瞬間、「これより小さくまとまることは許さない」という下限が生まれる。要約統計量は「中身」については何も語らないが、「散らばりの規模」だけは確かに語っている——何を保証していて何を保証していないのか、指標の守備範囲を正確に知っておくことが大事なのだと思う。

数字が同じでも、中身は恐竜かもしれないしサーモンかもしれない。散布図を描く1行を惜しまないことにする。

まとめ

  • Datasaurus dozen(要約統計量が同一で形が全く違う13データセット)の生成アルゴリズム(焼きなまし法)をnumpyで再実装し、恐竜→円・星・X・サーモンの変形を再現した
  • 統計量5つ(平均x,y・SD x,y・相関)は150万イテレーションの変形中、小数2桁で固定されたまま(最大ずれ0.0097)
  • ガウス雲→恐竜の逆向きは失敗。細部の多い形は目標にできず、原論文が恐竜を「出発点」にしているのは必然だった
  • 目標の形が小さすぎるとSD固定と数学的に両立できず、点が輪郭に乗れない(魚45%サイズで平均距離10.67、100%で0.00)。統計量は形を拘束しないが、散らばりの規模は拘束する
  • greedyでも今回は概ね成功(前回のAckleyと違い地形が穏やか)だが、受理移動数は18分の1で、点配置にムラが残った

実験に使ったデータは datasauRus パッケージ由来のDatasaurusDozen.tsv。