2015-03-01

Commonality Analysis - Java 編

R で、一元配置分散分析を利用した Commonality Analysis の例を紹介しましたが、今回は同じことを Java で実施して、結果を検証したいと思います。

R による Commonality Analysis の記事を下記に示しました。

前回のおさらい

STAGE_AYIELD
A0194.08
A0192.84
A0296.52
A0296.34
A0294.11
A0194.36
A0393.14
A0294.94
A0396.33
A0198.81
A0396.13
A0294.26
A0192.06
A0395.94
A0398.62
A0293.24
A0194.23
A0194.32
A0296.16
A0391.78

Commonality Analysis は、複数の工程 (STAGE) において、工程内の複数の装置と、アウトプットである収率 (YIELD) との関係で、使用した装置との共通性(依存性)を見つけ出すことが目的でした。YIELD に対する装置の依存性を調べるために、工程(STAGE_A など)それぞれについて、一元配置分散分析をして、その p-Value から、装置の違いが YIELD の違いに対して有意かどうかを検定しました(右表は STAGE_A の例)。

R 上では、右の表を tbl というデータフレームにした場合、次のようにして一元配置分散分析をして p-Value を確認できます。

result <- oneway.test(YIELD ~ STAGE, data = tbl, var.equal = TRUE)
print(result$p.value)
[1] 0.6785116

この方法は、複数の工程における複数の装置の組み合わせによって、検定の結果が正しいかどうかが左右されるという不確かさがあります 1.。しかし現実的な対応としては、まず分散分析をして有意なシグナルが見つかれば、そこからドリルダウンして、確かに正しい検定結果かどうかを検証することによって不確かさを補うことができます。

例えば、半導体デバイスの製造工程のように長大な工程の中から、問題解決のとっかかりとなるシグナルを見つけ出すような場合に、強力な手法となります。

そういうわけで、R ばかりでなく、他のプログラミング言語でも同じ処理ができるようにしておいた方が良いだろうと考え、計算結果の検証も兼ねて、Java でも同等の処理をするサンプルを作成しました。

Java による計算

Java では、分散分析の計算の部分に Apache Commons Math の一元配置分散分析 (One-Way ANOVA tests) を利用することにします。

Math - The Commons Math User Guide - Statistics にある一元配置分散分析の部分を下記に引用しました。

One-Way ANOVA tests
double[] classA =
   {93.0, 103.0, 95.0, 101.0, 91.0, 105.0, 96.0, 94.0, 101.0 };
double[] classB =
   {99.0, 92.0, 102.0, 100.0, 102.0, 89.0 };
double[] classC =
   {110.0, 115.0, 111.0, 117.0, 128.0, 117.0 };
List classes = new ArrayList();
classes.add(classA);
classes.add(classB);
classes.add(classC);

Then you can compute ANOVA F- or p-values associated with the null hypothesis that the class means are all the same using a OneWayAnova instance or TestUtils methods:

double fStatistic = TestUtils.oneWayAnovaFValue(classes); // F-value
double pValue = TestUtils.oneWayAnovaPValue(classes);     // P-value

To test perform a One-Way ANOVA test with significance level set at 0.01 (so the test will, assuming assumptions are met, reject the null hypothesis incorrectly only about one in 100 times), use

TestUtils.oneWayAnovaTest(classes, 0.01); // returns a boolean
                                          // true means reject null hypothesis
STAGE_A
A01A02A03
94.0896.5293.14
92.8496.3496.33
94.3694.1196.13
98.8194.9495.94
92.0694.2698.62
94.2393.2491.78
94.3296.16

一元配置分散分析における p-Value の算出は、TestUtils.oneWayAnovaPValue メソッドを利用すればできそうです。

public double anovaPValue(Collection<double[]> categoryData)
                   throws NullArgumentException,
                          DimensionMismatchException,
                          ConvergenceException,
                          MaxCountExceededException

しかし、データ (YIELD) は、左の表のようなグループになるように整理しなければなりません。

R のデータフレームの使い方に慣れてしまうと、Java のような汎用的なプログラミング言語でこういう処理を記述しようとするとなにかと面倒ですが、なるべくシンプルになるように配慮しながら記述してみました。しかし、データの並び替えやら型変換などに手間取り、コーディングが結構長くなってしまいました。

なお、このサンプルで使用している CSV ファイルは、前述の R の記事で使用したファイルと同じです shared sample から sample_commonality.csv.zip をダウンロードできるようにしてあります。

サンプルファイルの格納に使っていたストレージサービス 4shared が、あまりに使いにくいので Google Drive へ変更しました。

リスト:ToolCommonalityAnalysis.java 
package onewayanova;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.commons.math3.stat.inference.TestUtils;

public class ToolCommonalityAnalysis {
    // CSV ファイルを格納するリスト
    List<List<String>> content = new ArrayList<List<String>>();
    // STAGE 毎に分散分析をした p-Value を格納するリスト
    List<ResultData> result = new ArrayList<ResultData>();
    // 有意レベル
    double significant = 0.02;

    ToolCommonalityAnalysis(String fileName) {
        readCSV(fileName);
        calcANOVA();
    }

    /**
     * readCSV - CSV ファイルの読み込み処理
     * 
     * @param fileName
     *            name of CSV file to read
     */
    void readCSV(String fileName) {
        try {
            if (checkBeforeReadfile(fileName)) {
                FileInputStream in = new FileInputStream(fileName);
                InputStreamReader sr = new InputStreamReader(in, "UTF-8");
                BufferedReader br = new BufferedReader(sr);

                String line;
                boolean header = true;
                while ((line = br.readLine()) != null) {
                    StringTokenizer tokenizer = new StringTokenizer(line, ",");
                    // 後の処理の都合で、行単位ではなく、列単位で読み込んでいる
                    if (header) {
                        readColHeader(tokenizer);
                        header = false;
                    } else {
                        readColValues(tokenizer);
                    }
                }
                br.close();
            } else {
                System.out.println("ファイルが無いか、開くことができません。");
                return;
            }
        } catch (FileNotFoundException e) {
            System.out.println("FileNotFoundException : " + e.getMessage());
            return;
        } catch (IOException e) {
            System.out.println("IOException : " + e.getMessage());
            return;
        }
    }

    /**
     * checkBeforeReadfile - CSV ファイルの読み込み可チェック
     * 
     * @param file
     *            file instance to read
     * @return true if OK otherwide false
     */
    boolean checkBeforeReadfile(String fileName) {
        File file = new File(fileName);
        if (file.exists()) {
            if (file.isFile() && file.canRead()) {
                return true;
            }
        }
        return false;
    }

    /**
     * readColHeader - CSV ファイルのヘッダーを読み込む処理
     * 
     * @param token
     *            token of row
     */
    void readColHeader(StringTokenizer token) {
        while (token.hasMoreTokens()) {
            String value = token.nextToken();
            List<String> col = new ArrayList<String>();
            col.add(value);
            content.add(col);
        }
    }

    /**
     * readColValues - CSV ファイルの行順で列毎に読み込む処理
     * 
     * @param token
     *            token of row
     */
    void readColValues(StringTokenizer token) {
        int i = 0;
        while (token.hasMoreTokens()) {
            String value = token.nextToken();
            List<String> colArr = content.get(i);
            colArr.add(value);
            i++;
        }
    }

    /**
     * calcANOVA - 一元配置分析
     */
    void calcANOVA() {
        int cols = content.size();

        for (int i = 1; i < cols - 1; i++) {
            // 水準列 (STAGE)
            List<String> colCond = new ArrayList<String>(content.get(i));
            // YIELD
            List<String> colValue = new ArrayList<String>(content.get(cols - 1));

            // 水準列のヘッダー名の取得
            String condHeader = colCond.get(0);
            // リストからヘッダー名を削除
            colCond.remove(0);
            colValue.remove(0);
            // Commons Math で処理できるようデータグループを整理
            List<double[]> dataGroup = arrangeDataGroup(colCond, colValue);

            // Commons Math で 一元配置分散分析を実施して p-Value を算出
            double pValue = TestUtils.oneWayAnovaPValue(dataGroup); // P-value
            ResultData resultStage = new ResultData(condHeader, pValue);
            result.add(resultStage);
        }
    }

    /**
     * arrangeDataGroup
     *
     * @param levelList 水準のリスト
     * @param valueList 水準に対応する値 (String) のリスト
     * @return Commons Math/oneWayAnovaPValue に渡すデータ
     */
    List<double[]> arrangeDataGroup(List<String> levelList, List<String> valueList) {
        // ユニークな水準数を TreeSet で調査
        List<String> levelSet = new ArrayList<String>(new TreeSet<String>(levelList));
        int levelNum = levelSet.size();

        // 水準名と整数値を連想配列にして、それぞれの条件群にYIELDを振り分ける
        TreeMap<String, Integer> levelKey = new TreeMap<String, Integer>();

        // Double 型の水準別リストのインスタンス
        List<List<Double>> groupDList = new ArrayList<List<Double>>();

        // 水準数でループ
        for (int i = 0; i < levelNum; i++) {
            // 水準名と整数値のペアを定義
            levelKey.put((String) levelSet.get(i), (Integer) i);
            // 水準別に Double 型の Yield を格納する空のインスタンスを準備
            List<Double> groupDouble = new ArrayList<Double>();
            groupDList.add(groupDouble);
        }

        // String を Double の配列リストへ変換して準備したリストに格納
        for (int i = 0; i < levelList.size(); i++) {
            String level = levelList.get(i);
            Double valueDouble = Double.parseDouble(valueList.get(i));
            List<Double> groupDouble = groupDList.get(levelKey.get(level));
            groupDouble.add(valueDouble);
        }

        // Common Math に渡せる double 型の配列/リストへ変換
        List<double[]> groupList = new ArrayList<double[]>();
        for (int i = 0; i < levelNum; i++) {
            List<Double> group = new ArrayList<Double>(groupDList.get(i));
            // 水準iの要素の数
            int groupSize = group.size();
            // 水準iの配列を確保
            double[] groupArr = new double[groupSize];
            // Double から double 型に変換して配列に格納するループ
            for (int j = 0; j < groupSize; j++) {
                groupArr[j] = (group.get(j)).doubleValue();
            }
            groupList.add(groupArr);
        }
        return groupList;
    }

    /**
     * showTable - 読み込んだファイルを表示
     */
    void showTable() {
        for (int i = 0; i < content.size(); i++) {
            System.out.println(content.get(i));
        }
    }

    /**
     * showResult - 分散分析の結果 (p-Value) を表示
     */
    void showResult() {
        DecimalFormat df = new DecimalFormat("0.000000000");
        System.out.println("\nSTAGE    P-VALUE     SIG.");
        for (ResultData data : result) {
            String stage = data.getStage();
            double pval = data.getPvalue();
            String sigStr;
            if (pval < significant) {
                sigStr = new String("*");
            } else {
                sigStr = new String("");
            }
            System.out.println(stage + "  " + df.format(pval) + "  " + sigStr);

        }
    }

    // 結果を格納するリストの要素の構造を定義
    class ResultData {
        String stage;
        Double pvalue;

        ResultData(String stage, Double pvalue) {
            this.stage = stage;
            this.pvalue = pvalue;
        }

        String getStage() {
            return stage;
        }

        Double getPvalue() {
            return pvalue;
        }
    }

    public static void main(String[] args) {
        String fileName = "D:/Users/bitwalk/Documents/data/sample_commonality.csv";
        ToolCommonalityAnalysis tca = new ToolCommonalityAnalysis(fileName);
        tca.showTable();
        tca.showResult();
    }
}

実行結果は以下のようになります。R による算出結果と同じになることが確認できました。

[LOTID, P01, P02, P03, P04, P05, P06, P07, P08, P09, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20]
[STAGE_A, A01, A01, A02, A02, A02, A01, A03, A02, A03, A01, A03, A02, A01, A03, A03, A02, A01, A01, A02, A03]
[STAGE_B, B01, B02, B03, B01, B03, B01, B02, B02, B03, B03, B01, B02, B02, B01, B03, B02, B01, B03, B03, B02]
[STAGE_C, C01, C04, C01, C03, C02, C03, C02, C01, C02, C03, C01, C01, C04, C03, C02, C03, C04, C03, C02, C01]
[STAGE_D, D01, D02, D01, D03, D02, D01, D02, D01, D03, D03, D01, D01, D02, D02, D02, D01, D03, D03, D02, D03]
[STAGE_E, E01, E03, E01, E02, E03, E03, E01, E02, E03, E02, E01, E01, E01, E02, E03, E01, E01, E02, E03, E02]
[STAGE_F, F01, F01, F03, F04, F01, F03, F04, F02, F04, F03, F02, F02, F02, F01, F04, F03, F03, F04, F01, F02]
[YIELD, 94.08, 92.84, 96.52, 96.34, 94.11, 94.36, 93.14, 94.94, 96.33, 98.81, 96.13, 94.26, 92.06, 95.94, 98.62, 93.24, 94.23, 94.32, 96.16, 91.78]

STAGE    P-VALUE     SIG.
STAGE_A  0.678511601  
STAGE_B  0.001784216  *
STAGE_C  0.239092024  
STAGE_D  0.849674707  
STAGE_E  0.435107778  
STAGE_F  0.422098879  

長期の出張中に記事を投稿しています。先日、この出張のために買った廉価な Windows のノート PC 上で動作確認をしていますので、Linux 固有の話題からはしばらく遠ざかります。

参考サイト

  1. 交絡 - Wikipedia

 

0 件のコメント: