October 2016

Volume 31 Number 10

テストの実行 - C# による ANOVA

James McCaffrey

James McCaffrey分散分析 (ANOVA) とは、昔からある統計手法で、サンプル データしかない状況で 3 つ以上のグループの平均が等しいかどうかを推測するのに使用されます。たとえば、ある大学に初級コンピューター サイエンスのクラスが 3 クラスあるとします。各クラスは同じ教授の指導を受けますが、使う教科書と指導方法が違います。この状況で、各クラスの学生の成績が同じになるかどうかを調べます。

コンピューター サイエンスの理解度を評価する試験があり、結果が 1 ~ 15 の段階で示されます。しかし、この試験は非常に高価で時間もかかるため、各クラスから無作為に 6 人だけ選び、この試験を実施することにします。試験を行い、得られたサンプルで ANOVA を実行して、3 つのクラスの平均がすべて同じになるかどうかを推測します。

目標はデータ セットの平均を分析することです。しかし、ANOVA をご存じない方にはこの名前が少しわかりにくいかもしれません。ANOVA と名付けられた理由は、平均を推測するために分散分析 (ANalyze Of VAriance) を行うためです。

ANOVA の考え方と今回の目標を理解するには、図 1 のデモ プログラムをご覧いただくのが一番です。デモでは、3 つのグループのそれぞれの成績をハードコーディングしています。Group1 には成績が 4 人分しかありません。Group3 にも 5 人分しかありません。学生が試験を欠席したり、成績データを紛失または破損することがあり、サンプルの数が等しくならない状況はよく起こります。

C# デモ プログラムによる ANOVA
図 1 C# デモ プログラムによる ANOVA

ANOVA には大きく 2 つの手順があります。最初の手順では、サンプル データを使って、F-statistic (F) と自由度 (df) というペア値を計算します。2 番目の手順では、F 値と df 値を使って、すべての母平均が同じになる確率 (p-value) を決定します。最初の手順は比較的簡単です。2 番目はかなり難しい手順です。

デモで求めた F 値は 15.884 になります。一般に、F が大きくなるほど、すべてのグループの母平均が等しくなる可能性は低くなります。df = (2, 12) となる理由については後ほど説明します。今回の F と df を使うと、p-value は 0.000425 になります。この値は非常に小さいので、母平均は大きく異なる可能性があることになります。ここで、追加の統計テストを実施して、母平均が互いにどの程度異なるかを判断します。今回のデモ データの場合、Group1 (サンプル平均 = 4.50) は、Group2 (平均 = 9.67) と Group3 (平均 = 10.60) よりも成績が低いことがわかります。

デモ プログラム

デモ プログラムを作成するには、Visual Studio を起動し、[ファイル] メニューの [新規作成] をポイントし、[プロジェクト] をクリックして、C# コンソール アプリケーション オプションを選択します。このデモ プログラムは .NET との大きな依存関係がないため、どのバージョンの Visual Studio でも機能します。テンプレート コードがソリューション ウィンドウに読み込まれたら、Program.cs ファイルを右クリックし、名前を「AnovaProgram.cs」に変更します。Program クラスの名前が Visual Studio によって自動的に変更されます。

エディター ウィンドウの上部で、不要な using ステートメントを、最上位レベルの System 名前空間を参照するステートメントを除いてすべて削除します。プログラムの全体の構造は、図 2 のようになります。デモ プログラムは長すぎてここにすべて掲載することはできませんが、完全なデモ ソース コードは本稿付属のコード ダウンロードから入手できます。

図 2 デモ プログラムの構造

using System;
namespace Anova
{
  class AnovaProgram
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Begin ANOVA using C# demo");
      // Set up sample data
      // Use data to calculate F and df
      // Use F and df to calculate p-value
      Console.WriteLine("End ANOVA demo");
    }
    static double Fstat(double[][] data, out int[] df) { . . }
    static double LogGamma(double z) { . . }
    static double BetaIncCf(double a, double b, double x) { . . }
    static double BetaInc(double a, double b, double x) { . . }
    static double PF(double a, double b, double x) { . . }
    static double QF(double a, double b, double x) { . . }
    static void ShowData(double[][] data, string[] colNames) { . . }
  }
}

静的メソッド Fstat は、配列の配列オブジェクトに格納されているデータを基に、F-statistic を計算して返します。また、2 つの df 値も計算して out パラメーターの配列に返します。関数 ShowData は、サンプルの平均を表示する簡単なヘルパー関数です。

残りの 5 つのメソッドは、すべて p-value を計算するために使用しています。中心となるメソッドは QF です。QF がメソッド PF を呼び出し、PF がメソッド BetaInc を呼び出し、BetaInc がメソッド BetaIncCf とメソッド LogGamma を呼び出します。

いくつか WriteLine メッセージを表示した後、Main メソッドはサンプル データを準備して表示します。

double[][] data = new double[3][]; // 3 groups
data[0] = new double[] { 3, 4, 6, 5 };
data[1] = new double[] { 8, 12, 9, 11, 10, 8 };
data[2] = new double[] { 13, 9, 11, 8, 12 };
string[] colNames = new string[] {  "Group1", "Group2", "Group3" };
ShowData(data, colNames);

実際のシナリオでは、データはテキスト ファイルに保存されている可能性があるため、ヘルパー関数を作成して、ファイルからデータを読み取り、配列の配列にデータを読み込むことになります。

F-statistic と df は次のように計算します。

int[] df = null;
double F = Fstat(data, out df);

ANOVA では、データ セットの df は値のペアになります。ペアの最初の値は K - 1 で、K はグループの数を表します。ペアの 2 番目の値は N - K で、N はサンプル値の総数を表します。今回デモ データの場合は、df = (K-1, N-K) = (3-1, 15-3) = (2, 12) になります。

p-value は次のように計算して表示します。

double pValue = QF(df[0], df[1], F);
Console.Write("p-value = ");

要するに、ANOVA を実行する際に呼び出しているステートメントは非常にシンプルです。ですが、その背後では多くの処理が行われています。

F-statistic の計算

F-statistic 値の計算には、複数の補助手順があります。ここではサンプル データの値としてデモの値を使用します。

Group1: 3.00, 4.00, 6.00, 5.00
Group2: 8.00, 12.00, 9.00, 11.00, 10.00, 8.00
Group3: 13.00, 9.00, 11.00, 8.00, 12.00

最初の補助手順では、各グループの平均と、サンプル全体の平均を計算します。デモ データの場合、計算は以下のようになります。

means[0] = (3.0 + 4.0 + 6.0 + 5.0) / 4 = 4.50
means[1] = (8.0 + 12.0 + 9.0 + 11.0 + 10.0 + 8.0) / 6 = 9.67
means[2] = (13.0 + 9.0 + 11.0 + 8.0 + 12.0) / 5 = 10.60
gMean = (3.0 + 4.0 + . . . + 12.0) / 15 = 8.60

メソッド Fstat は、次の定義から始めます。

static double Fstat(double[][] data, out int[] df)
{
  int K = data.Length; // Number groups
  int[] n = new int[K]; // Number items each group
  int N = 0; // total number data points
  for (int i = 0; i < K; ++i) {
    n[i] = data[i].Length;
    N += data[i].Length;
  }
...

ここで、ローカル配列 n には各グループのそれぞれの成績が含まれています。K はグループの数です。N はすべてのグループに含まれる成績データの総数です。次に、グループの平均を計算して配列 means に格納してから、全グループの総平均を計算して変数 gMean に格納します。

double[] means = new double[K];
double gMean = 0.0;
for (int i = 0; i < K; ++i) {
  for (int j = 0; j < data[i].Length; ++j) {
    means[i] += data[i][j];
    gMean += data[i][j];
  }
  means[i] /= n[i];
}
gMean /= N;

次の補助手順では、「グループ間の平方和」 (SSb) と「グループ間の平均平方」 (MSb) を計算します。SSb は、各グループの平均と全体平均の差を 2 乗し、それに重みを掛けて加算した総和です。Msb は「MSb = SSb / (K-1)」で求めます。K はグループの数です。デモ データの場合、計算は以下のようになります。

SSb = (4 * (4.50 - 8.60)^2) +
 (6 * (9.67 - 8.60)^2) +
 (5 * (10.60 - 8.60)^2) = 94.07
MSb = 94.07 / (3-1) = 47.03

SSb と MSb を計算するコードは次のとおりです。

double SSb = 0.0;
for (int i = 0; i < K; ++i)
  SSb += n[i] * (means[i] - gMean) * (means[i] - gMean);
double MSb = SSb / (K - 1);

次の補助手順では、「グループ内の平方和」 (SSw) と「グループ内の平均平方」 (MSw) を計算します。SSw は、グループ内の各成績値とそのグループの平均の差を求め、2 乗して合計した値です。Msw は「MSw = SSw / (N-K)」で求めます。デモ データの場合、計算は以下のようになります。

SSw = (3.0 - 4.50)^2 + . . + (8.0 - 9.67)^2 +
 . . + (12.0 - 10.60)^2 = 35.53
MSw = 35.53 / (15-3) = 2.96

SSw と MSw を計算するコードは次のとおりです。

double SSw = 0.0;
for (int i = 0; i < K; ++i)
  for (int j = 0; j < data[i].Length; ++j)
    SSw += (data[i][j] - means[i]) * (data[i][j] - means[i]);
double MSw = SSw / (N - K);

最後の補助手順では、2 つの df 値と F-statistic を計算します。2 つの df 値は K - 1 と N - K で、F-statistic は「F = MSb / MSw」で求めます。デモ データの場合、計算は以下のようになります。

df = (K-1, N-K) = (3-1, 15-3) = (2, 12)
F = 47.03 / 2.96 = 15.88.

df と F を計算するコードは次のとおりです。

...
  df = new int[2];
  df[0] = K - 1;
  df[1] = N - K;
  double F = MSb / MSw;
  return F;
} // Fstat

F-statistic 値と df 値はデータのセットから機械的に求められるため、数式を理解すれば比較的簡単だと思います。

p-value の計算

F-statistic 値と df 値から p-value を換算します。p-value は、F-statistic 値と df 値を求めたサンプル データを基に、すべての母平均が等しくなる確率です。この換算の原理はシンプルですが、実際の計算はきわめて困難です。できる限り簡単にするため、長々とした補足説明が必要な細部には触れません。図 3 のグラフをご覧ください。

F-Statistic 値と df 値からの p-Value の計算
図 3 F-Statistic 値と df 値からの p-Value の計算

df 値の各ペアが取り得る値は、F-distribution というグラフを決定します。F-distribution の形状は、df の値に応じて大きく異なる可能性があります。図 3 のグラフは、df = (4, 12) の F-distribution を示しています。df = (2, 12) の F-distribution の形状は非常に変則的になるため、デモ データの df = (2, 12) ではなく、df = (4, 12) を使用しています。

F-distribution グラフの下の領域の合計は正確に 1.0 になります。F-statistic 値がわかっている場合、F-statistic 値から正の無限大までの F-distribution グラフの下の領域が p-value になります。少し分かりづらい説明かもしれませんが、一般に、F-distribution グラフの下の領域のうち、0 から F-statistic までの領域を PF と呼び、F-statistic から正の無限大までの領域 (p-value) を QF と呼びます。F-distribution グラフの下の領域の合計は 1 になるため、「PF + QF = 1」が成り立ちます。QF を計算するよりも PF を計算する方がやや簡単です。そこで、p-value (QF) を求める場合は PF を計算後、1 から PF を減算します。

PF の計算は非常に難解ですが、さいわい、数十年間にわたって知られている優れた推定式があります。このような数式など、多くの数式は、M. Abramowitz と I.A. Stegun の共著による有名な文献『Handbook of Mathematical Functions』(Dover Publications、1965 年) に掲載されています。科学プログラマの間で、この書籍は通常「A&S」と呼ばれています。A&S の各式には ID 番号が付けられています。

今回のデモのメソッド PF は、メソッド BetaInc を囲む単なるラッパーです。

static double PF(double a, double b, double x)
{
  double z = (a * x) / (a * x + b);
  return BetaInc(a / 2, b / 2, z);
}

メソッド BetaInc の名前は「incomplete Beta」(不完全ベータ関数) を表します。 メソッド BetaInc では、A&S の式 6.6.2 と 26.5.8 を使用します。これらの方程式は LogGamma 関数と BetaIncCf 関数を呼び出します。LogGamma 関数の説明や実装は非常に困難です。簡単に説明すると、数学のガンマ関数は階乗の概念を実数値に一般化します。ガンマ関数の値は、階乗のように天文学的に大きくなる可能性があるため、ガンマの対数を計算して値を小さくするのが一般的です。

LogGamma の計算は、非常に難解で、使用できるアルゴリズムも複数あります。デモ プログラムでは、(g=5, n=7) を使った Lanczos 近似アルゴリズムを使用しています。A&S には、LogGamma を計算できるさまざまなアルゴリズムが掲載されていますが、A&S の出版時には知られていなかった Lanczos 近似を使用すると、より正確な結果が得られます。

メソッド BetaIncCf の名前は「incomplete Beta computed by continued fraction」(連分数によって求められる不完全ベータ) を表します。 今回のデモ プログラムのメソッド BetaIncCf では A&S の式 26.5.8 を使用しています。

まとめ

ANOVA テストでは、グループのデータ項目が数学的に独立していて、母集団のデータ セットが (ガウス分布のように) 正規分布になり、母集団のデータ セットは等分散であるということから、数学的な推測を行います。

このような推測をテストする方法は複数ありますが、テストの結果を解釈するのは困難です。問題は、実際のデータが正確に正規分布し、等分散になるとは到底考えられません。ただし、データが多少正規分布から逸脱したり、等分散でなかったとしても、ANOVA は機能します。最終的には、ANOVA の推測を証明するのは非常に難しいため、結果の解釈には慎重さが求められます。

ANOVA は t-test と密接な関係があります。t-test は、サンプル データしかない状況で、2 つのグループの母平均が正確に等しいかどうかを判断します。そのため、デモのように 3 つのグループがあり、ANOVA を使用しない場合は、グループ 1 とグループ 2 を比較し、グループ 1 と 3 を比較し、グループ 2 とグループ 3 を比較するという、3 つの t-tests を実行することもできます。ただし、第 1 種過誤 (誤検知) が発生するため、このアプローチはお勧めしません。

今回取り上げた ANOVA の種類は、一元配置 (または一要素) ANOVA と呼ばれます。二元配置 ANOVA という別の手法は、要素が 2 つある場合に使用します。

ANOVA は、データ セットの F-statistic の計算値に基づきます。F-statistic を使用する統計テストは他にもあります。たとえば、F-statistic を使用して、データの 2 つのグループの分散が同じかどうかを推測できます。


Dr.James McCaffrey は、ワシントン州レドモンドの Microsoft Research に勤務しています。これまでに、Internet Explorer、Bing などの複数のマイクロソフト製品にも携わってきました。Dr.McCaffrey の連絡先は jammc@microsoft.com (英語のみ) です。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Chris Lee および Kirk Olynyk に心より感謝いたします。