001/* 002 * Copyright (c) 2009 The openGion Project. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 013 * either express or implied. See the License for the specific language 014 * governing permissions and limitations under the License. 015 */ 016package org.opengion.plugin.table; 017 018import java.util.List; 019import java.util.ArrayList; 020import java.math.BigDecimal; // 6.9.2.0 (2018/03/05) 021import java.math.RoundingMode; // 6.9.2.0 (2018/03/05) 022 023/** 024 * StandardDeviation は、登録されたデータから、標準偏差等の値を求めます。 025 * 026 * このプログラムは、0データを無視する特殊な計算をしています。 027 * これは、成形条件ミドルウエアが、0をデータなしとして扱っているためです。 028 * よって、一般的な標準偏差等の値を求めることは出来ません。 029 * 030 * ここではデータを追加していき、取り出すときに、計算した値を文字列配列で返します。 031 * 作成するカラムは、CNT,SUM,AVG,(STDEVS or STDEVP),COEFF,M3S,M2S,M1S,M0S,P0S,P1S,P2S,P3S です。 032 * 033 * CNT(個数),SUM(合計),AVG(平均), 034 * STDEVS(標本標準偏差:n-1) または、STDEVP(母標準偏差:n) を、useDEVP(trueで、母標準偏差) で選択します。 035 * COEFF(変動係数) は、標準偏差(σ)を算術平均で、割ったものの百分率 036 * M3S(~-3σ),M2S(-3σ~-2σ),M1S(-2σ~-σ),M0S(-σ~0),P0S(0~σ),P1S(σ~2σ),P2S(2σ~3σ),P3S(3σ~) 037 * FILTERは、1:(-2σ~-σ or σ~2σ) , 2:(-3σ~-2σ or 2σ~3σ) , 3:(~-3σ or 3σ~) のみピックアップします。 038 * 初期値の 0 は、フィルターなしです。 039 * 040 * 6.9.9.2 (2018/09/18) 041 * COEFF(変動係数)の最小値でフィルターするためのキーワード MIN_CV を追加します。 042 * これは、単位(%)で、指定の値以下の変動係数のレコードを出力しません。 043 * 044 * @og.rev 6.7.7.0 (2017/03/31) 新規追加 045 * @og.rev 6.9.3.0 (2018/03/26) 標本標準偏差と母標準偏差は、一つだけにし、変動係数を追加します。 046 * 047 * @version 6.7.7 2017/03/31 048 * @author Kazuhiko Hasegawa 049 * @since JDK1.8, 050 */ 051class StandardDeviation { 052 /** このプログラムのVERSION文字列を設定します。 {@value} */ 053 private static final String VERSION = "7.3.0.0 (2021/01/06)" ; 054 055 // 7.3.0.0 (2021/01/06) SpotBugs:null ではなく長さが0の配列を返すことを検討する 056 private static final String[] ZERO_ARY = new String[0]; // null ではなく長さが0の配列を返すことを検討する 057 058// public static final String[] ADD_CLMS = new String[] { "CNT","SUM","AVG","STDEVS","STDEVP","M3S","M2S","M1S","M0S","P0S","P1S","P2S","P3S" }; 059 /** 追加カラム列 */ 060 public static final String[] ADD_CLMS = new String[] { "CNT","SUM","AVG","STDEV","COEFF","M3S","M2S","M1S","M0S","P0S","P1S","P2S","P3S" }; // 6.9.3.0 (2018/03/26) 061 private static final int HIST_SU = 8; // "M3S","M2S","M1S","M0S","P0S","P1S","P2S","P3S" の個数 062 063 private final List<Double> data = new ArrayList<>(); 064 065 private final int ftype ; // フィルタータイプ(0,1,2,3) 066 private final boolean useDEVP ; // 初期値が、"P" (母標準偏差) 067 private final String format ; // 初期値が、"%.3f" 068 private final double minCV ; // 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターする 069 070 private double sum ; 071 private double pow ; // 6.9.2.0 (2018/03/05) 分散の計算方法を変更 072 073 /** 074 * 各種条件を指定した標準偏差計算用のインスタンスを作成します。 075 * 076 * @og.rev 6.7.7.0 (2017/03/31) 新規追加。 077 * @og.rev 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターするためのキーワード MIN_CV を追加。 078 * 079 * @param ftype フィルタータイプ(0,1,2,3) 080 * @param useDEVP 初期値が、"P" (母標準偏差) 081 * @param format 初期値が、"%.3f" 082 * @param minCV 変動係数の最小値(%) 083 */ 084// public StandardDeviation( final int ftype , final boolean useDEVP , final String format ) { 085 public StandardDeviation( final int ftype , final boolean useDEVP , final String format , final String minCV ) { 086 this.ftype = ftype; 087 this.useDEVP= useDEVP; 088 this.format = format; 089 this.minCV = parseDouble( minCV ); // 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターする 090 } 091 092 /** 093 * 内部情報を、初期化します。 094 * 095 * @og.rev 6.7.7.0 (2017/03/31) 新規追加。 096 * 097 */ 098 public void clear() { 099 data.clear(); 100 sum = 0d; 101 pow = 0d; 102 } 103 104 /** 105 * データを追加します。 106 * 107 * 引数の文字列を、double に変換して使用します。 108 * 変換できない場合は、エラーにはなりませんが、警告を出します。 109 * ただし、値が、0.0 の場合は、対象外にします。 110 * 111 * @og.rev 6.7.7.0 (2017/03/31) 新規追加。 112 * @og.rev 6.9.2.0 (2018/03/05) 分散の計算方法を変更 113 * 114 * @param strVal データ 115 */ 116 public void addData( final String strVal ) { 117 final double val = parseDouble( strVal ); 118 if( val != 0d ) { 119 data.add( val ); 120 sum += val; 121 pow += val * val ; // 6.9.2.0 (2018/03/05) 122 } 123 } 124 125 /** 126 * データから計算した結果を、文字列に変換して、返します。 127 * 128 * 標準偏差の式を 129 * σ=sqrt(Σ(Xi - Xave)^2 / n) 130 * から 131 * σ=sqrt(Σ(Xi^2) / n - Xave^2)) 132 * に変形します。 133 * 参考:http://imagingsolution.blog107.fc2.com/blog-entry-62.html 134 * 135 * @og.rev 6.7.7.0 (2017/03/31) 新規追加。 136 * @og.rev 6.9.2.0 (2018/03/05) 分散の計算方法を変更 137 * @og.rev 6.9.3.0 (2018/03/26) 標本標準偏差と母標準偏差は、一つだけにし、変動係数を追加します。 138 * @og.rev 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターするためのキーワード MIN_CV を追加。 139 * @og.rev 7.3.0.0 (2021/01/06) SpotBugs:null ではなく長さが0の配列を返すことを検討する 140 * 141 * @return データから計算した結果(無い場合は、長さゼロの配列) 142 * @og.rtnNotNull 143 */ 144 public String[] getData() { 145 final int cnt = data.size(); 146// if( cnt == 0 ) { return null; } 147 if( cnt == 0 ) { return ZERO_ARY; } // 7.3.0.0 (2021/01/06) 148 149 final double avg = sum/cnt; // 平均 150 // double sa1 = 0d; 151 152 // // 標準偏差の計算のために一度回す 153 // for( final double val : data ) { 154 // sa1 += Math.pow( val - avg , 2 ) ; 155 // } 156 157 // final double stdevs = cnt==1 ? 0d : Math.sqrt( sa1/(cnt-1) ); // 母集団の標本の標準偏差(標本標準偏差) 158 // final double stdevp = Math.sqrt( sa1/cnt ); // 母集団全ての標準偏差(母標準偏差) 159 160 // 6.9.2.0 (2018/03/05) 分散の計算方法を変更 161 final double vari = Math.abs( pow/cnt - avg * avg ); // マイナスはありえない(計算誤差) 162 // 6.9.3.0 (2018/03/26) 標本標準偏差と母標準偏差は、一つだけにし、変動係数を追加します。 163// final double stdevp = Math.sqrt( vari ); // 母集団全ての標準偏差(母標準偏差) 164// final double stdevs = cnt==1 ? 0d : Math.sqrt( vari * cnt / (cnt-1) ); // 誤差があるので、掛け算してから、SQRTします。 165 final double stdev = useDEVP ? Math.sqrt( vari ) 166 : cnt==1 ? 0d : Math.sqrt( vari * cnt / (cnt-1) ); 167 168 // 6.9.3.0 (2018/03/26) 変動係数(標準偏差/平均 の百分率) 169 final double coeff = stdev / avg * 100 ; 170 171 // 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターするためのキーワード MIN_CV を追加。 172// if( coeff < minCV ) { return null; } // minCV より小さい場合は、null(レコードを追加しない) 173 if( coeff < minCV ) { return ZERO_ARY; } // minCV より小さい場合は、null(レコードを追加しない) 174 175 // 6.9.2.0 (2018/03/05) 毎回計算ではなく固定値を使用します。 176// final double sa2 = useDEVP ? stdevp : stdevs ; // useDEVP == true の場合、母標準偏差 を使用します。 177// final double SA1 = halfUp( useDEVP ? stdevp : stdevs ) ; // useDEVP == true の場合、母標準偏差 を使用します。 178 final double SA1 = halfUp( stdev ) ; // useDEVP == true の場合、母標準偏差 を使用します。 179 final double SA2 = SA1 * 2 ; // 2σ 180 final double SA3 = SA1 * 3 ; // 3σ 181 182 // 確率分布の合計グラフを作成するためにもう一度回す 183 final int[] dtCnt = new int[HIST_SU]; 184 for( final double val : data ) { 185 final double val2 = halfUp( val - avg ); 186 187 // 6.9.2.0 (2018/03/05) 毎回計算ではなく固定値を使用します。 188// if( 0.0d == val2 || cnt == 1 ) { dtCnt[4]++ ; } // 0 ・・・データが1件の場合 189// else if( val2 < -sa2*3 ) { dtCnt[0]++ ; } // -3σ< 190// else if( -sa2*3 <= val2 && val2 < -sa2*2 ) { dtCnt[1]++ ; } // -2σ< 191// else if( -sa2*2 <= val2 && val2 < -sa2*1 ) { dtCnt[2]++ ; } // -1σ< 192// else if( -sa2*1 <= val2 && val2 < 0.0d ) { dtCnt[3]++ ; } // 0< 193// else if( 0.0d <= val2 && val2 < sa2*1 ) { dtCnt[4]++ ; } // 0≦ 194// else if( sa2*1 <= val2 && val2 < sa2*2 ) { dtCnt[5]++ ; } // 1σ≦ 195// else if( sa2*2 <= val2 && val2 < sa2*3 ) { dtCnt[6]++ ; } // 2σ≦ 196// else if( sa2*3 <= val2 ) { dtCnt[7]++ ; } // 3σ≦ 197 198 // 標準偏差等が0に近い場合の誤差を考慮して、比較順を変更します。 199 if( cnt == 1 || 0d == val2 || 0d == SA1 ) { dtCnt[4]++ ; } // 0 ・・・データが1件、平均との差がゼロ、標準偏差がゼロ 200 else if( 0d <= val2 && val2 < SA1 ) { dtCnt[4]++ ; } // 0≦ 201 else if( -0d == val2 ) { dtCnt[3]++ ; } // 0< 平均との差がマイナスゼロの場合 202 else if( -SA1 <= val2 && val2 < 0d ) { dtCnt[3]++ ; } // 0< 203 else if( SA1 <= val2 && val2 < SA2 ) { dtCnt[5]++ ; } // 1σ≦ 204 else if( -SA2 <= val2 && val2 < -SA1 ) { dtCnt[2]++ ; } // -1σ< 205 else if( SA2 <= val2 && val2 < SA3 ) { dtCnt[6]++ ; } // 2σ≦ 206 else if( -SA3 <= val2 && val2 < -SA2 ) { dtCnt[1]++ ; } // -2σ< 207 else if( SA3 <= val2 ) { dtCnt[7]++ ; } // 3σ≦ 208 else if( val2 < -SA3 ) { dtCnt[0]++ ; } // -3σ< 209 } 210 211 // 6.7.2.0 (2017/01/16) FILTERパラメータ追加。 212 // ここで、フィルター処理を行います。 213 final boolean useValue ; 214 switch( ftype ) { 215 case 1 : useValue = ( dtCnt[0] + dtCnt[1] + dtCnt[2] + dtCnt[5] + dtCnt[6] + dtCnt[7] ) > 0 ; break ; 216 case 2 : useValue = ( dtCnt[0] + dtCnt[1] + dtCnt[6] + dtCnt[7] ) > 0 ; break ; 217 case 3 : useValue = ( dtCnt[0] + dtCnt[7] ) > 0 ; break ; 218 default : useValue = true ; break; 219 } 220 221 if( useValue ) { 222 final String[] vals = new String[ADD_CLMS.length]; // CNT,SUM,AVG,STDEVS,STDEVP,M3S,M2S,M1S,M0S,P0S,P1S,P2S,P3S の個数 223 224 vals[0] = String.valueOf( cnt ); // CNT 225 vals[1] = String.format( format , sum ); // SUM 226 vals[2] = String.format( format , avg ); // AVG 227 // 6.9.3.0 (2018/03/26) 標本標準偏差と母標準偏差は、一つだけにし、変動係数を追加します。 228// vals[3] = String.format( format , stdevs ); // STDEVS(標本標準偏差) 229// vals[4] = String.format( format , stdevp ); // STDEVP(母標準偏差) 230 vals[3] = String.format( format , stdev ); // useDEVP=true で、STDEVP(母標準偏差) , false で、STDEVS(標本標準偏差) 231 vals[4] = String.format( "%.2f" , coeff ); // 6.9.3.0 (2018/03/26) 変動係数は、小数第二位で四捨五入します。 232 vals[5] = String.valueOf( dtCnt[0] ); // M3S 233 vals[6] = String.valueOf( dtCnt[1] ); // M2S 234 vals[7] = String.valueOf( dtCnt[2] ); // M1S 235 vals[8] = String.valueOf( dtCnt[3] ); // M0S 236 vals[9] = String.valueOf( dtCnt[4] ); // P0S 237 vals[10] = String.valueOf( dtCnt[5] ); // P1S 238 vals[11] = String.valueOf( dtCnt[6] ); // P2S 239 vals[12] = String.valueOf( dtCnt[7] ); // P3S 240 241 return vals; 242 } 243// return null; 244 return ZERO_ARY; // 7.3.0.0 (2021/01/06) 245 } 246 247 /** 248 * 引数の文字列を、double に変換して返します。 249 * 250 * 処理が止まらないように、null や、変換ミスの場合は、ゼロを返します。 251 * 252 * @param val 変換する元の文字列 253 * 254 * @return 変換後のdouble 255 * @og.rtnNotNull 256 */ 257 private double parseDouble( final String val ) { 258 double rtn = 0.0d; 259 if( val != null && !val.trim().isEmpty() ) { 260 try { 261 rtn = Double.parseDouble( val.trim() ); 262 } 263 catch( final NumberFormatException ex ) { 264 final String errMsg = "文字列を数値に変換できません。val=[" + val + "]" + ex.getMessage(); 265 System.out.println( errMsg ); 266 } 267 } 268 269 return rtn ; 270 } 271 272 /** 273 * 引数のdoubleを、少数点3桁で、四捨五入(HALF_UP)します。 274 * 275 * 長い処理式を、短くすることが目的のメソッドです。 276 * 277 * @param val 変換する元のdouble 278 * 279 * @return 変換後のdouble 280 * @og.rtnNotNull 281 */ 282 private double halfUp( final double val ) { 283 return BigDecimal.valueOf( val ).setScale( 3 , RoundingMode.HALF_UP ).doubleValue(); 284 } 285}