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