Juliaで測定データの統計をとってみよう(総合演習)
ここまでの Julia レッスンで学んだことを全部つないで、動くものを1つ完成させます。新しい構文は出てきません。構造体・抽象型・配列・多重ディスパッチ・高階関数・書式付き出力——すでに練習したものだけで組み立てます。
作るのは測定データの統計です。平均・中央値・標準偏差を出し、外れ値を見つけ、それを除いたらどう変わるかまで見ます。
統計量は自分で書きます。ライブラリを呼ぶだけなら1行ですが、それでは中で何が起きているか分かりません。標準偏差を自分の手で組み立てると、「ばらつき」という言葉が具体的な計算に変わります。
この演習で味わってほしいJuliaらしさは2つです。——ドット演算子(配列全体をまとめて計算する)と、多重ディスパッチ(同じ名前の関数を、引数の型ごとに定義する)。どちらも他の言語から来ると驚く仕組みです。
このページではコードを実行していません。かわりにお手本と同じコードを書けたかを、その場で判定します(判定はあなたのブラウザの中だけで行われ、どこにも送信されません)。
表示している実行結果は本物です。このページを作るときに、実際のJuliaで走らせて得た出力をそのまま載せています。
完成イメージ
最後まで進むと、このコードが完成します。下の実行結果は、このページを作るときに開発機で実際にコードを走らせて得たものです(このページ上では実行していません)。
===== 測定データの統計 ===== 長さ 件数 : 8件 平均 : 12.592mm 中央値 : 12.405mm 標準偏差: 0.537mm 最小-最大: 12.370 〜 13.920mm 外れ値 : 1件 [13.92] ばらつきが許容範囲(0.10mm)を超えています 重さ 件数 : 8件 平均 : 50.050g 中央値 : 50.050g 標準偏差: 0.245g 最小-最大: 49.700 〜 50.400g 外れ値 : なし ばらつきは許容範囲(0.50g)内です ---------------------------------------------- 長さから外れ値1件を除くと 平均 : 12.592 → 12.403mm 標準偏差: 0.537 → 0.027mm
STEP 1測定の種類を抽象型と構造体で表す
🎯 このステップのゴール: 共通の親を作り、その下に具体的な型を並べる
まずデータの形を決めます。測定には「長さ」「重さ」「温度」があり、単位も許容範囲も違います。
abstract type Measurement end——抽象型です。これ自体は実体を作れません。「この下にいくつかの型が並ぶ」という枠だけを用意しています。
その下にstruct Length <: Measurementと並べます。<:が「〜の下に属する」という記号です。
なぜわざわざ型を分けるのか。——長さも重さも中身はFloat641つで、データとしては何も違いません。
分ける理由は、次のステップで見る「多重ディスパッチ」のためです。型が違えば、同じ関数名でも違う処理が呼ばれます。「長さの単位はmm、重さの単位はg」を、ifを1つも書かずに表せます。
value::Float64の::が型の指定です。Juliaは型を書かなくても動きますが、構造体のフィールドには書くのが基本——書かないと性能が大きく落ちます。
Juliaが「書きやすいのに速い」と言われるのは、型が分かるとその型専用の機械語を作るからです。フィールドの型を書かないと、その仕組みが働きません。
constを付けた配列は「この名前を別のものに差し替えない」という意味です。中身の書き換えは防ぎませんが、トップレベルの変数には付けておくと速くなります。
長さの測定: 8件 重さの測定: 8件 Length は Measurement の一種: true Float64 は Measurement ではない: false 構造体の中身: 12.41
STEP 2平均と中央値を自分で書く——添字は1から
🎯 このステップのゴール: 並べ替えて真ん中を取り、偶数個のときも正しく扱う
平均は素直です。全部足して件数で割るだけ。
isempty(xs) && return nothing——「空ならnothingを返す」という書き方です。Juliaでは&&が短絡評価なので、左が真のときだけ右が実行されます。ifを1行に畳む定番の書き方です。
0件の平均は0ではありません。「計算できない」——だからnothingを返します。
中央値でJuliaの最大の特徴に出会います。——添字が1から始まります。
他の多くの言語ではsorted[0]が先頭ですが、Juliaではsorted[1]が先頭です。sorted[0]と書くとBoundsErrorで落ちます。
これは数学の慣習に合わせた設計です。行列の1行目は「1行目」と呼ぶのが自然だから——Juliaが数値計算のために作られた言語であることの現れです。
そのため真ん中を取る式も、他の言語と変わります。7個なら(7+1)÷2 = 4番目が真ん中です。0始まりなら7÷2 = 3番目——式が違うので、他の言語のコードをそのまま持ってくると必ずずれます。
÷は整数の割り算です。/だと4.0という小数になり、添字に使えません。÷はバックスラッシュ付きで\divと打ってTabで変換できます。
偶数個のときは真ん中2つの平均を取ります。8個なら4番目と5番目です。
先頭の要素: 12.41 最後の要素: 12.37 長さの平均: 12.5925 長さの中央値: 12.405000000000001 空のときの平均: nothing
STEP 3ドット演算子で配列をまとめて計算する
🎯 このステップのゴール: 要素ごとの演算を1行で書き、標準偏差を組み立てる
標準偏差を出します。手順は3つ——平均との差を出し、2乗し、その平均の平方根を取る。
ここでJuliaの真骨頂が出ます。
squares = (xs .- m) .^ 2
この.がブロードキャストです。.-は「配列の全要素からmを引く」、.^は「全要素を2乗する」——ループが1つも要りません。
他の言語ならforで回して1つずつ計算するところです。Juliaでは演算子の前に.を置くだけで、配列全体に広がります。
しかも速さは手書きのループと変わりません。Juliaは.の付いた式をまとめて1回のループに畳んでくれます。途中の配列を何度も作ったりしません。
.は自作の関数にも使えます。sqrt.(xs)と書けば全要素の平方根です。「1つの値を扱う関数を書けば、配列にも使える」——これがJuliaの設計です。
length(xs) - 1で割っているのに注目してください。件数ではなく「件数マイナス1」です。
これは標本標準偏差といって、「手元のデータは全体の一部」という前提の計算です。全体をすべて測ったのなら件数で割ります(母標準偏差)。測定データはたいてい一部なので、こちらを使います。
length(xs) < 2で先に弾いています。1件では「ばらつき」が定義できません——n-1が0になり、0で割ることになります。
平均: 12.5925 平均との差: [-0.182, -0.212, -0.143, -0.192, -0.202, 1.328, -0.172, -0.223] 標準偏差: 0.5369690068204255 1件だけのとき: nothing
STEP 4多重ディスパッチで単位を切り替える
🎯 このステップのゴール: 同じ関数名を型の数だけ定義し、ifを書かずに振り分ける
測定の種類によって単位が違います。長さならmm、重さならg、温度なら℃。
普通の言語ならifかswitchで分けるところです。Juliaでは違います。
unit(::Length) = "mm"
unit(::Weight) = "g"
unit(::Temperature) = "℃"
同じ名前の関数を、3回定義しています。違うのは引数の型だけ。
これが多重ディスパッチです。unit(Length(0.0))と呼べば1つ目が、unit(Weight(0.0))なら2つ目が——Juliaが引数の型を見て、自動で選びます。
::Lengthと変数名を書いていないのに注目してください。型だけ分かればよく、中身は使わないときの書き方です。
この設計の何が良いのか。——温度を追加したくなったとき、既存のコードを1行も触らずに済みます。unit(::Temperature) = "℃"と1行足すだけです。
ifで分けていたら、そのifを探して増やすことになります。しかも単位を決めるif、許容範囲を決めるif、表示を決めるif——場所が増えるほど、直し忘れが出ます。
オブジェクト指向のメソッドと似ていますが、決定的に違う点があります。——メソッドは「どのオブジェクトのものか」だけで決まりますが、多重ディスパッチは引数全部の型の組み合わせで決まります。
だから「自分の型に手を入れずに、外から振る舞いを足せます」。他人の作った型に対しても、unit(::SomeoneElsesType) = ...と書けるのです。
Length: 単位=mm 許容=0.1 Weight: 単位=g 許容=0.5 Temperature: 単位=℃ 許容=1.0 長さの単位: mm 重さの単位: g
STEP 5外れ値を見つけて、除いた結果と比べる
🎯 このステップのゴール: filterで絞り込み、除く前と後を並べて示す
測定データには明らかにおかしい値が混じることがあります。この演習のlengthsにも1つだけ大きく外れた値が入っています。
見つけ方は「平均から標準偏差の何倍離れているか」です。
filter(x -> abs(x - m) > OUTLIER_SIGMA * s, xs)——条件に合う要素だけを集めます。
x -> ...がその場で書く小さな関数、absが絶対値です。平均より大きすぎても小さすぎても外れ値なので、符号を落とします。
2倍という基準に絶対的な根拠はありません。3倍を使う分野も、そもそも標準偏差ではなく四分位範囲を使う分野もあります。だから定数にしてあります——変えられることが大事なのです。
そして、いちばん大事な注意です。——外れ値を見つけても、勝手に捨ててはいけません。
測定ミスかもしれませんが、本物の異常かもしれません。装置の不具合を示す最初の兆候だったものを黙って捨てた——実際に起きる話です。
だからこの演習では、除いたデータで上書きしません。cleanedという別の配列を作り、除く前と後を並べて表示します。
「除くとこう変わる」を見せるのが、プログラムの仕事です。除くかどうかを決めるのは人です。
filter(x -> !(x in bad), lengths)の!は否定、inは「含まれているか」です。外れ値に入っていないものだけを残します。
外れ値: [13.92] 除く前: 8件 平均 12.592 除いた後: 7件 平均 12.403 標準偏差: 0.537 → 0.027
STEP 6ひとつの関数にまとめて仕上げる
🎯 このステップのゴール: @printfで桁を揃え、型ごとの単位を自動で切り替える
最後の仕上げです。ここまでの部品をsummarizeとreportにまとめます。
function summarize(kind::Measurement, xs)——第1引数の型をMeasurementと書いているのが要点です。
これで「Measurementの下にある型なら何でも受け取る」という意味になります。LengthでもWeightでもTemperatureでも通ります。
そして中でunit(kind)と呼べば、渡された型に応じた単位が返ってきます。この関数の中にはifが1つもありません——ステップ4で型ごとに定義しておいたおかげです。
温度の測定を足したくなったら、summarizeは1行も変えずに済みます。これが多重ディスパッチの効き目です。
表示は@printfです。@で始まるのはマクロ——コンパイルされる前にコードそのものを書き換える仕組みです。
だから書式指定に間違いがあると、実行時ではなくコンパイル時に見つかります。普通の関数ではできない検査です。
%.3fで小数第3位まで。測定値は桁を揃えないと比べられません。12.41と12.4が並ぶと、同じ精度で測ったのか分かりません。
"-"^46の^は文字列の繰り返しです。数値の累乗と同じ記号——Juliaでは「繰り返す」という意味で統一されています。
完成です。OUTLIER_SIGMAを3.0に変えてみてください。——外れ値が「なし」になります。基準を厳しくするか緩めるかで、見つかるものが変わる。その判断は人がするものだということが、実感できるはずです。
weightsの値を1つ大きく変えれば、そちらにも外れ値が出ます。許容範囲の判定も切り替わります——型ごとにtoleranceを定義しておいた効果が確かめられます。
===== 測定データの統計 ===== 長さ 件数 : 8件 平均 : 12.592mm 中央値 : 12.405mm 標準偏差: 0.537mm 最小-最大: 12.370 〜 13.920mm 外れ値 : 1件 [13.92] ばらつきが許容範囲(0.10mm)を超えています 重さ 件数 : 8件 平均 : 50.050g 中央値 : 50.050g 標準偏差: 0.245g 最小-最大: 49.700 〜 50.400g 外れ値 : なし ばらつきは許容範囲(0.50g)内です ---------------------------------------------- 長さから外れ値1件を除くと 平均 : 12.592 → 12.403mm 標準偏差: 0.537 → 0.027mm
よくある質問
このページではコードを実行しないのですか?
していません。Juliaはブラウザの中では走らせられないためです。このページではお手本と同じコードを書けたかどうかを判定する形にしています(判定はあなたのブラウザの中だけで行われ、コードはどこにも送信されません)。
ただし表示している実行結果は本物です。このページを作るときに実際のJuliaで走らせた出力を、そのまま載せています。
添字が1から始まるのはなぜですか
数学の慣習に合わせているからです。行列の1行目は「1行目」と呼ぶのが自然で、Juliaは数値計算のために作られた言語です。
そのため他の言語のコードをそのまま持ってくるとずれます。真ん中を取る式も、末尾を指す書き方も変わります。Juliaではxs[end]が最後の要素、xs[end-1]がその1つ前です。
xs[0]と書くとBoundsErrorで落ちます。静かに間違った値を返すのではなく、その場で止まるので、気づきやすくはなっています。
ドット演算子は何のためにあるのですか
「1つの値を扱う書き方を、そのまま配列にも使える」ようにするためです。
xs .- mは「全要素からmを引く」、sqrt.(xs)は「全要素の平方根」。ループを書く必要がありません。
しかも速さは手書きのループと変わりません。Juliaは.の付いた式をまとめて1回のループに畳むので、(xs .- m) .^ 2でも途中の配列は作られません。
自作の関数にも使えます。1つの値を扱う関数を書いておけば、.を付けるだけで配列に広がります。
外れ値は除いてしまってよいのですか
プログラムが決めることではありません。
測定ミスなら除くべきですが、本物の異常を捉えていることもあります。装置の不具合を示す最初の兆候だった値を黙って捨てた——実際に起きる話です。
だからこの演習では、除いたデータで上書きしていません。別の配列を作り、除く前と後を並べて表示します。
「除くとこう変わる」を見せるのがプログラムの仕事で、除くかどうかを決めるのは人です。何件除いたかを必ず記録に残してください。
🚩 次はこのカテゴリへ
統計を扱う言語としては、Rが長く使われてきました。この演習で自分で書いた平均・中央値・標準偏差は、Rではmean・median・sdとして最初から用意されています。中身を知ったうえで使うと、結果の意味が読み取れます。Rカテゴリの最終演習では、アンケートの集計を作ります。