🎯 最終演習

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、温度なら℃。

普通の言語ならifswitchで分けるところです。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で桁を揃え、型ごとの単位を自動で切り替える

最後の仕上げです。ここまでの部品をsummarizereportにまとめます

function summarize(kind::Measurement, xs)——第1引数の型をMeasurementと書いているのが要点です。

これで「Measurementの下にある型なら何でも受け取る」という意味になります。LengthでもWeightでもTemperatureでも通ります。

そして中でunit(kind)と呼べば、渡された型に応じた単位が返ってきますこの関数の中にはifが1つもありません——ステップ4で型ごとに定義しておいたおかげです。

温度の測定を足したくなったら、summarizeは1行も変えずに済みます。これが多重ディスパッチの効き目です。

表示は@printfです。@で始まるのはマクロ——コンパイルされる前にコードそのものを書き換える仕組みです。

だから書式指定に間違いがあると、実行時ではなくコンパイル時に見つかります。普通の関数ではできない検査です。

%.3f小数第3位まで。測定値は桁を揃えないと比べられません12.4112.4が並ぶと、同じ精度で測ったのか分かりません。

"-"^46^は文字列の繰り返しです。数値の累乗と同じ記号——Juliaでは「繰り返す」という意味で統一されています

完成です。OUTLIER_SIGMA3.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ではmeanmediansdとして最初から用意されています。中身を知ったうえで使うと、結果の意味が読み取れます。Rカテゴリの最終演習では、アンケートの集計を作ります。

Rのレッスンへ進む
広告スペース(バナー)
広告スペース(記事内)