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}