20 人の学生の英語の試験の点が以下のようであった。各学生の偏差値を求めよ。
元資料 6 枚目の式です。n 人の各学生の点を xi、平均を x̄、標準偏差を σ とすると
偏差値の意味。 平均をとった人がちょうど 50。 平均より標準偏差 1 つぶん上なら 60、2 つぶんなら 70 です。 つまり「平均から何σ離れているか」を 50 を中心に 10 倍して読みやすくした値です。 だから 100 を超えることも、0 を下回ることもあります (下のデータでは 9 点の人が 29.7、100 点の人が 62.2 になります ── 点差が大きいわりに偏差値の差が小さいのは、そもそも点のばらつき(σ ≒ 28)が大きいためです)。
元資料 9 枚目のプログラムです。まずそのまま実行してみます。
Sub Macro()
Dim I, S, SS As Integer
Dim sc(21) As Integer
Dim a, d, sd, sv As Double
For i = 1 To 20
sc(i) = Cells(i + 1, 2).Value
S = S + sc(i)
Next i
a = S / 20
For i = 1 To 20
SS = SS + (sc(i) - a) ^ 2
Next
d = SS / 20
sd = Math.Sqr(d)
For i = 1 To 20
sv = 10 * (sc(i) - a) / sd + 50
Cells(i + 1, 3).Value = Format(sv, "###.##")
Next
' 途中の値も見てみる(この 3 行は元資料にはありません)
Cells(1, 5) = "平均": Cells(2, 5) = a
Cells(3, 5) = "偏差の2乗和": Cells(4, 5) = SS
Cells(5, 5) = "標準偏差": Cells(6, 5) = sd
End Sub
元資料 1 行目の Dim I, S, SS As Integer は、
VBA では SS だけが Integer 型になり、
I と S は Variant 型になります(第 3 章 §1.1)。
そして SS が Integer であることが、答えを狂わせます。
SS = SS + (sc(i) - a) ^ 2 の右辺は小数です
(平均が 65.85 なので、たとえば (64 − 65.85)² = 3.4225)。
これを Integer の箱に入れると毎回四捨五入されます。
20 回足すあいだに丸めが積み上がり、偏差の 2 乗和がずれるのです。
| 宣言のしかた | 偏差の 2 乗和 | 標準偏差 | 千葉(9 点)の偏差値 |
|---|---|---|---|
| 正しい値(厳密に計算) | 15676.55 | 27.996919 | 29.69 |
Dim I, S, SS As Integer(元資料) |
15678 | 27.998214 | 29.7 |
Dim I As Integer, S As Integer, SS As Integer(「1 つずつ As を書く」と直しただけ) |
15678 | 27.998214 | 29.7 |
Dim SS As Double にする |
15676.55 | 27.996919 | 29.69 |
ここが大事なところです。
第 3 章で「Dim a, b As Integer は 1 つずつ書きなさい」と言いました。
しかしこの場合、1 つずつ書き直すだけでは答えは直りません ──
SS はもともと Integer だったのですから。
本当の直し方は SS As Double にすることです。
「小数を足し込む変数は Double にする」 ── 型は書式の問題ではなく、
答えの正しさの問題だという実例です。
Sub Macro520()
Dim i As Integer
Dim n As Integer
Dim S As Long ' 点の合計(整数のままでよい)
Dim SS As Double ' ← ここが要点。小数を足し込むので Double
Dim sc(1 To 20) As Integer ' 1 To 20 と書いて意図をはっきりさせる
Dim a As Double, sd As Double, sv As Double
n = 20
' 平均を求める
S = 0
For i = 1 To n
sc(i) = Cells(i + 1, 2).Value
S = S + sc(i)
Next i
a = S / n
' 標準偏差を求める
SS = 0
For i = 1 To n
SS = SS + (sc(i) - a) ^ 2
Next i
sd = Sqr(SS / n)
' 偏差値を書き出して、色をつける
For i = 1 To n
sv = 50 + (sc(i) - a) / sd * 10
Cells(i + 1, 3) = Format(sv, "###.##")
If sv >= 60 Then
Cells(i + 1, 3).Interior.Color = RGB(198, 239, 206)
ElseIf sv < 40 Then
Cells(i + 1, 3).Interior.Color = RGB(255, 199, 206)
End If
Next i
Cells(1, 5) = "平均": Cells(2, 5) = a
Cells(3, 5) = "偏差の2乗和": Cells(4, 5) = SS
Cells(5, 5) = "標準偏差": Cells(6, 5) = sd
End Sub
合計 S は Long でよい理由。
点は整数なので合計も整数です。ただし Integer(上限 32,767)だと、
100 点満点で 328 人を超えるとあふれます。
人数が増えても大丈夫なように Long にしておきます。
「整数だけれど大きくなりうるものは Long」「小数が入るものは Double」
── これが型の選び方の基本です。
点数の分布を見るには、10 点刻みで何人いるかを数えます。 「数える箱を並べる」のは配列がいちばん得意な仕事です。
考え方。
点数を 10 で割って切り捨てると、そのまま箱の番号になります ──
64 点 → 64 \ 10 = 6 → 「60 点台」の箱。
100 点だけは 10 になってしまうので、9 の箱(90 点以上)に入れます。
Sub Macro2()
Dim d(0 To 9) As Integer ' 度数(0〜9 点台 … 90 点以上)
Dim i As Integer, j As Integer
Dim n As Integer
Dim t As Integer
Dim saidai As Integer
n = 20
' ① 数える
For i = 1 To n
t = Cells(i + 1, 1).Value \ 10 ' 10 で割って切り捨て
If t > 9 Then t = 9 ' 100 点は 9 の箱へ
d(t) = d(t) + 1
Next i
' ② 表にする
Cells(1, 3) = "階級": Cells(1, 4) = "人数": Cells(1, 5) = "グラフ"
For i = 0 To 9
Cells(i + 2, 3) = i * 10 & "〜" & (i * 10 + 9)
Cells(i + 2, 4) = d(i)
' ③ ■ を人数ぶん並べる(横棒グラフ)
For j = 1 To d(i)
Cells(i + 2, 5 + j - 1) = "■"
Cells(i + 2, 5 + j - 1).Font.Color = RGB(0, 112, 192)
Next j
If d(i) > saidai Then saidai = d(i)
Next i
Cells(13, 3) = "最も多い階級の人数"
Cells(13, 4) = saidai
Cells(14, 3) = "合計"
For i = 0 To 9
Cells(14, 4) = Cells(14, 4).Value + d(i)
Next i
End Sub
この分布は「ふたこぶ」になっています。 80 点台が 4 人、90 点以上が 4 人と上位に固まり、 一方で 10 点台・30 点台にも人がいます。 平均 65.85 のあたり(60 点台)は 2 人しかいません ── 平均は「いちばん多いところ」を表しているわけではないのです。 ヒストグラムを描くと、平均だけでは見えないことが分かります。 これがデータを「見る」ということです。
√2 を求めたいとします。Sqr(2) と書けば済みますが、
その Sqr の中で何が行われているのかを作ってみます。
考え方。 √2 は y = x2 − 2 が 0 になる x です。 グラフ上の点 (xn, y) で接線を引き、 それが横軸と交わるところを次の x にする ── これを繰り返すと、 答えにどんどん近づきます。式にすると
右の形は「x と 2/x の平均をとる」と読めます。 x が √2 より大きければ 2/x は小さいので、平均をとると間にはさまるのです。
Sub Macro3()
Dim x As Double, xn As Double, sa As Double
Dim i As Integer
x = Cells(1, 2).Value ' 元資料は 100 から始めている
Cells(2, 1) = "回": Cells(2, 2) = "x": Cells(2, 3) = "変化量": Cells(2, 4) = "誤差"
For i = 1 To 12
xn = (x + 2 / x) / 2 ' 次の値
sa = x - xn ' どれだけ動いたか
Cells(i + 2, 1) = i
Cells(i + 2, 2) = xn
Cells(i + 2, 3) = sa
Cells(i + 2, 4) = Abs(xn - Sqr(2))
x = xn
If Abs(sa) < 0.0000000001 Then Exit For ' ほとんど動かなくなったら終わり
Next i
Cells(15, 1) = "Sqr(2)"
Cells(15, 2) = Sqr(2)
Cells(16, 1) = i - 1 & " 回で収束"
End Sub
4 列目の「誤差」を見てください。
最初はゆっくり近づき、答えの近くまで来ると桁数が一気に倍々で増えます
(0.07 → 0.002 → 0.000002 → 0.000000000002)。
これを2 次収束といい、ニュートン法の強みです。
初期値を 100 のような遠い値にしても、だいたい半分ずつ縮んでいって
やがて 2 次収束の領域に入ります。B1 を 1.5 にすると、
4 回ほどで 15 桁合うことが確かめられます。
止め方に注意。
If xn = Sqr(2) Then Exit For のように等しいかで比べてはいけません
(第 3 章・第 6 章の誤差の話)。
「ほとんど動かなくなったら終わり」(Abs(sa) < 0.0000000001)
という止め方をします。
また、念のため For i = 1 To 12 と回数の上限も付けています ──
収束しない場合に無限ループにならないようにするためです。
「収束したら抜ける + 回数の上限」の二重の備えが定石です。
元資料には整列そのものの節はありませんが、 第 9 章で「3 つの整数を小さい順に並べる」をやり、 第 11 章で「順位の計算は n² 回の比較になる」と触れました。 その続きとして、何件でも並べられるやり方を扱います。
バブルソートは、第 9 章の「隣どうしを比べて、逆なら交換」を 端から端まで、何周も繰り返すだけです。 1 周するごとに、いちばん大きい値が右端に「浮かび上がる」のでこの名前です。
Sub Macro4()
Dim n As Integer
Dim i As Integer, j As Integer
Dim ten(1 To 20) As Integer
Dim nm(1 To 20) As String
Dim hikaku As Long, koukan As Long
n = 8
' 読み込む
For i = 1 To n
nm(i) = Cells(i + 1, 1).Value
ten(i) = Cells(i + 1, 2).Value
Next i
' バブルソート(高い順)
For i = 1 To n - 1
For j = 1 To n - i
hikaku = hikaku + 1
If ten(j) < ten(j + 1) Then ' 順が逆なら交換
Call swapI(ten(j), ten(j + 1))
Call swapS(nm(j), nm(j + 1))
koukan = koukan + 1
End If
Next j
Next i
' 書き出す
Cells(1, 4) = "順位": Cells(1, 5) = "氏名": Cells(1, 6) = "点"
For i = 1 To n
Cells(i + 1, 4) = i
Cells(i + 1, 5) = nm(i)
Cells(i + 1, 6) = ten(i)
Next i
Cells(11, 4) = "比較した回数": Cells(11, 6) = hikaku
Cells(12, 4) = "交換した回数": Cells(12, 6) = koukan
Cells(13, 4) = "n(n-1)/2 =": Cells(13, 6) = n * (n - 1) / 2
End Sub
Sub swapI(ByRef x As Integer, ByRef y As Integer)
Dim t As Integer
t = x: x = y: y = t
End Sub
Sub swapS(ByRef x As String, ByRef y As String)
Dim t As String
t = x: x = y: y = t
End Sub
点数だけでなく氏名も一緒に動かすところが要点です。
片方だけ並べ替えると、名前と点の対応が崩れます ──
実務でいちばん怖い種類の不具合です。
上のプログラムで Call swapS(nm(j), nm(j + 1)) の行を
コメントにして実行してみてください。点は正しく並ぶのに、名前が合わなくなります。
比較の回数は n(n−1)/2 です。 8 人なら 28 回、100 人なら 4950 回、1000 人なら約 50 万回 ── n² に比例して増えます。 だから件数が多いときは、もっと速い並べ方(クイックソートやマージソート。 比較回数は n log n に比例)が使われます。 1000 件なら約 1 万回で済み、50 倍の差です。 第 8 章の「引き算版と余り版の互除法」と同じで、 同じ答えを出す手順でも、速さは桁で違うのです。
Double。
Integer だと毎回丸められて答えがずれる ── 型は答えの正しさの問題Long点 \ 10 をそのまま箱の番号にする。
平均だけでは分布は見えないもとにしたのは「情報学入門(VBA)165-2019」全 22 枚です。
| 元資料 | この教材 | 種別 |
|---|---|---|
9 枚目のプログラム Dim I, S, SS As Integer |
SS が Integer なので、偏差の 2 乗和が毎回丸められて答えがずれる。
偏差の 2 乗和が 15676.55 ではなく 15678 になり、標準偏差が 27.9969 → 27.9982 に。
「1 つずつ As を書く」だけでは直らず、SS As Double にする必要がある
(この教材の処理系で両方を実行して確認しました。
丸めの規則が Excel とわずかに違うため最下位の桁は一致しないことがありますが、
ずれること自体は同じ理由で起こります) | 誤り |
Dim a, d, sd, sv As Double |
同じ形。sv 以外は Variant になる。
この場合は Variant でも小数が入るので答えは狂わないが、意図が伝わらない | 表記 |
合計 S の型 |
Integer だと 100 点満点で 328 人を超えるとあふれる。Long にした | 不足 |
Dim sc(21) As Integer(20 人ぶん) |
箱は 0〜21 の 22 個で 2 つ余る。Dim sc(1 To 20) と書いた(第 11 章) | 不足 |
Sub Macro() という名前 |
動くが、何をするマクロか分からない。Macro520 のように区別できる名前に | 表記 |
| 偏差値が何を意味するのかの説明がない | 「平均から何σ離れているか」であること、100 超や 0 未満もありうることを追記 | 不足 |
| ヒストグラムの階級の決め方(点 ÷ 10 が箱の番号)が書かれていない | 整数の割り算 \ をそのまま添字に使う書き方を示し、
100 点だけ別扱いが必要なことも明記 | 不足 |
| ニュートン法の止め方と収束の速さに触れていない | 「ほとんど動かなくなったら」+回数の上限という二重の備え、 誤差の桁が倍々に増える2 次収束を表で見せた | 不足 |
| フィボナッチ数列(3〜5 枚目)が資料 155 と重複 | 第 10 章に集約 | 重複 |
元資料で正しかったこと。
偏差値の式(x̄、σ、Oi = 50 + (xi−x̄)/σ × 10)は正確で、
プログラムの計算の流れ(合計 → 平均 → 偏差の 2 乗和 → 標準偏差 → 偏差値)も正しく、
Format(sv, "###.##") の使い方、
Math.Sqr(VBA でも書ける形)も問題ありません。
20 人の点数から求めた平均 65.85 も一致しました。
狂うのは SS の型だけで、そこを直せば完全に正しくなります。