    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 * このﾌﾟﾛｸﾞﾗﾑは、０データを無視する特殊な計算をしています。
    027 * これは、成形条件ﾐﾄﾞﾙｳｴｱが、０をﾃﾞｰﾀなしとして扱っているためです。
    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 = "6.9.9.2 (2018/09/18)" ;
    054
    055//      public  static final String[] ADD_CLMS = new String[] { "CNT","SUM","AVG","STDEVS","STDEVP","M3S","M2S","M1S","M0S","P0S","P1S","P2S","P3S" };
    056        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)
    057        private static final int      HIST_SU  = 8;             // "M3S","M2S","M1S","M0S","P0S","P1S","P2S","P3S" の個数
    058
    059        private final List<Double> data = new ArrayList<>();
    060
    061        private final int               ftype   ;       // フィルタータイプ(0,1,2,3)
    062        private final boolean   useDEVP ;       // 初期値が、"P" (母標準偏差)
    063        private final String    format  ;       // 初期値が、"%.3f"
    064        private final double    minCV   ;       // 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターする
    065
    066        private double sum ;
    067        private double pow ;                            // 6.9.2.0 (2018/03/05) 分散の計算方法を変更
    068
    069        /**
    070         * 各種条件を指定した標準偏差計算用のインスタンスを作成します。
    071         *
    072         * @og.rev 6.7.7.0 (2017/03/31) 新規追加。
    073         * @og.rev 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターするためのｷｰﾜｰﾄﾞ MIN_CV を追加。
    074         *
    075         * @param ftype         フィルタータイプ(0,1,2,3)
    076         * @param useDEVP       初期値が、"P" (母標準偏差)
    077         * @param format        初期値が、"%.3f"
    078         * @param minCV         変動係数の最小値(%)
    079         */
    080//      public StandardDeviation( final int ftype , final boolean useDEVP , final String format ) {
    081        public StandardDeviation( final int ftype , final boolean useDEVP , final String format , final String minCV ) {
    082                this.ftype      = ftype;
    083                this.useDEVP= useDEVP;
    084                this.format     = format;
    085                this.minCV      = parseDouble( minCV );         // 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターする
    086        }
    087
    088        /**
    089         * 内部情報を、初期化します。
    090         *
    091         * @og.rev 6.7.7.0 (2017/03/31) 新規追加。
    092         *
    093         */
    094        public void clear() {
    095                data.clear();
    096                sum = 0d;
    097                pow = 0d;
    098        }
    099
    100        /**
    101         * データを追加します。
    102         *
    103         * 引数の文字列を、double に変換して使用します。
    104         * 変換できない場合は、エラーにはなりませんが、警告を出します。
    105         * ただし、値が、0.0 の場合は、対象外にします。
    106         *
    107         * @og.rev 6.7.7.0 (2017/03/31) 新規追加。
    108         * @og.rev 6.9.2.0 (2018/03/05) 分散の計算方法を変更
    109         *
    110         * @param strVal        データ
    111         */
    112        public void addData( final String strVal ) {
    113                final double val = parseDouble( strVal );
    114                if( val != 0d ) {
    115                        data.add( val );
    116                        sum += val;
    117                        pow += val * val ;              // 6.9.2.0 (2018/03/05)
    118                }
    119        }
    120
    121        /**
    122         * データから計算した結果を、文字列に変換して、返します。
    123         *
    124         * 標準偏差の式を
    125         *    σ＝sqrt(Σ(Xi - Xave)^2 / n)
    126         * から
    127         *    σ＝sqrt(Σ(Xi^2) / n - Xave^2))
    128         * に変形します。
    129         * 参考：http://imagingsolution.blog107.fc2.com/blog-entry-62.html
    130         *
    131         * @og.rev 6.7.7.0 (2017/03/31) 新規追加。
    132         * @og.rev 6.9.2.0 (2018/03/05) 分散の計算方法を変更
    133         * @og.rev 6.9.3.0 (2018/03/26) 標本標準偏差と母標準偏差は、一つだけにし、変動係数を追加します。
    134         * @og.rev 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターするためのｷｰﾜｰﾄﾞ MIN_CV を追加。
    135         *
    136         * @return データから計算した結果
    137         */
    138        public String[] getData() {
    139                final int cnt = data.size();
    140                if( cnt == 0 ) { return null; }
    141                final double avg = sum/cnt;                     // 平均
    142        //      double sa1 = 0d;
    143
    144        //      // 標準偏差の計算のために一度回す
    145        //      for( final double val : data ) {
    146        //              sa1 += Math.pow( val - avg , 2 ) ;
    147        //      }
    148
    149        //      final double stdevs = cnt==1 ? 0d : Math.sqrt( sa1/(cnt-1) );           // 母集団の標本の標準偏差(標本標準偏差)
    150        //      final double stdevp = Math.sqrt( sa1/cnt );                                                     // 母集団全ての標準偏差(母標準偏差)
    151
    152                // 6.9.2.0 (2018/03/05) 分散の計算方法を変更
    153                final double vari = Math.abs( pow/cnt - avg * avg );                                    // マイナスはありえない（計算誤差）
    154                // 6.9.3.0 (2018/03/26) 標本標準偏差と母標準偏差は、一つだけにし、変動係数を追加します。
    155//              final double stdevp = Math.sqrt( vari );                                                                // 母集団全ての標準偏差(母標準偏差)
    156//              final double stdevs = cnt==1 ? 0d : Math.sqrt( vari * cnt / (cnt-1) );  // 誤差があるので、掛け算してから、SQRTします。
    157                final double stdev  = useDEVP ? Math.sqrt( vari )
    158                                                                          : cnt==1 ? 0d : Math.sqrt( vari * cnt / (cnt-1) );
    159
    160                // 6.9.3.0 (2018/03/26) 変動係数(標準偏差/平均 の百分率)
    161                final double coeff = stdev / avg * 100 ;
    162
    163                // 6.9.9.2 (2018/09/18) COEFF(変動係数)の最小値でフィルターするためのｷｰﾜｰﾄﾞ MIN_CV を追加。
    164                if( coeff < minCV ) { return null; }    // minCV より小さい場合は、null(ﾚｺｰﾄﾞを追加しない)
    165
    166                // 6.9.2.0 (2018/03/05) 毎回計算ではなく固定値を使用します。
    167//              final double sa2 = useDEVP ? stdevp : stdevs ;                                          // useDEVP == true の場合、母標準偏差 を使用します。
    168//              final double SA1 = halfUp( useDEVP ? stdevp : stdevs ) ;                        // useDEVP == true の場合、母標準偏差 を使用します。
    169                final double SA1 = halfUp( stdev ) ;                                                            // useDEVP == true の場合、母標準偏差 を使用します。
    170                final double SA2 = SA1 * 2 ;                                                                            // 2σ
    171                final double SA3 = SA1 * 3 ;                                                                            // 3σ
    172
    173                // 確率分布の合計グラフを作成するためにもう一度回す
    174                final int[] dtCnt = new int[HIST_SU];
    175                for( final double val : data ) {
    176                        final double val2 = halfUp( val - avg );
    177
    178                        // 6.9.2.0 (2018/03/05) 毎回計算ではなく固定値を使用します。
    179//                      if(        0.0d == val2 || cnt == 1      ) { dtCnt[4]++ ; }             //   0  ･･･データが１件の場合
    180//                      else if(                   val2 < -sa2*3 ) { dtCnt[0]++ ; }             // -3σ＜
    181//                      else if( -sa2*3 <= val2 && val2 < -sa2*2 ) { dtCnt[1]++ ; }             // -2σ＜
    182//                      else if( -sa2*2 <= val2 && val2 < -sa2*1 ) { dtCnt[2]++ ; }             // -1σ＜
    183//                      else if( -sa2*1 <= val2 && val2 <  0.0d  ) { dtCnt[3]++ ; }             //   0＜
    184//                      else if(   0.0d <= val2 && val2 <  sa2*1 ) { dtCnt[4]++ ; }             //   0≦
    185//                      else if(  sa2*1 <= val2 && val2 <  sa2*2 ) { dtCnt[5]++ ; }             //  1σ≦
    186//                      else if(  sa2*2 <= val2 && val2 <  sa2*3 ) { dtCnt[6]++ ; }             //  2σ≦
    187//                      else if(  sa2*3 <= val2                  ) { dtCnt[7]++ ; }             //  3σ≦
    188
    189                        // 標準偏差等が０に近い場合の誤差を考慮して、比較順を変更します。
    190                        if( cnt == 1 || 0d == val2 || 0d == SA1 ) { dtCnt[4]++ ; }              //   0  ･･･データが１件、平均との差がゼロ、標準偏差がゼロ
    191                        else if(  0d  <= val2 && val2 <  SA1  ) { dtCnt[4]++ ; }                //   0≦
    192                        else if( -0d  == val2                 ) { dtCnt[3]++ ; }                //   0＜ 平均との差がマイナスゼロの場合
    193                        else if( -SA1 <= val2 && val2 <  0d   ) { dtCnt[3]++ ; }                //   0＜
    194                        else if(  SA1 <= val2 && val2 <  SA2  ) { dtCnt[5]++ ; }                //  1σ≦
    195                        else if( -SA2 <= val2 && val2 < -SA1  ) { dtCnt[2]++ ; }                // -1σ＜
    196                        else if(  SA2 <= val2 && val2 <  SA3  ) { dtCnt[6]++ ; }                //  2σ≦
    197                        else if( -SA3 <= val2 && val2 < -SA2  ) { dtCnt[1]++ ; }                // -2σ＜
    198                        else if(  SA3 <= val2                 ) { dtCnt[7]++ ; }                //  3σ≦
    199                        else if(                 val2 < -SA3  ) { dtCnt[0]++ ; }                // -3σ＜
    200                }
    201
    202                // 6.7.2.0 (2017/01/16) FILTERパラメータ追加。
    203                // ここで、フィルター処理を行います。
    204                final boolean useValue ;
    205                switch( ftype ) {
    206                        case 1  : useValue = ( dtCnt[0] + dtCnt[1] + dtCnt[2] + dtCnt[5] + dtCnt[6] + dtCnt[7] ) > 0 ; break ;
    207                        case 2  : useValue = ( dtCnt[0] + dtCnt[1] +                       dtCnt[6] + dtCnt[7] ) > 0 ; break ;
    208                        case 3  : useValue = ( dtCnt[0] +                                             dtCnt[7] ) > 0 ; break ;
    209                        default : useValue = true ; break;
    210                }
    211
    212                if( useValue ) {
    213                        final String[] vals = new String[ADD_CLMS.length];      // CNT,SUM,AVG,STDEVS,STDEVP,M3S,M2S,M1S,M0S,P0S,P1S,P2S,P3S の個数
    214
    215                        vals[0]  = String.valueOf( cnt );                               // CNT
    216                        vals[1]  = String.format( format , sum );               // SUM
    217                        vals[2]  = String.format( format , avg );               // AVG
    218                        // 6.9.3.0 (2018/03/26) 標本標準偏差と母標準偏差は、一つだけにし、変動係数を追加します。
    219//                      vals[3]  = String.format( format , stdevs );    // STDEVS(標本標準偏差)
    220//                      vals[4]  = String.format( format , stdevp );    // STDEVP(母標準偏差)
    221                        vals[3]  = String.format( format , stdev );             // useDEVP=true で、STDEVP(母標準偏差) , false で、STDEVS(標本標準偏差)
    222                        vals[4]  = String.format( "%.2f" , coeff );             // 6.9.3.0 (2018/03/26) 変動係数は、小数第二位で四捨五入します。
    223                        vals[5]  = String.valueOf( dtCnt[0] );                  // M3S
    224                        vals[6]  = String.valueOf( dtCnt[1] );                  // M2S
    225                        vals[7]  = String.valueOf( dtCnt[2] );                  // M1S
    226                        vals[8]  = String.valueOf( dtCnt[3] );                  // M0S
    227                        vals[9]  = String.valueOf( dtCnt[4] );                  // P0S
    228                        vals[10] = String.valueOf( dtCnt[5] );                  // P1S
    229                        vals[11] = String.valueOf( dtCnt[6] );                  // P2S
    230                        vals[12] = String.valueOf( dtCnt[7] );                  // P3S
    231
    232                        return vals;
    233                }
    234                return null;
    235        }
    236
    237        /**
    238         * 引数の文字列を、double に変換して返します。
    239         *
    240         * 処理が止まらないように、null や、変換ミスの場合は、ゼロを返します。
    241         *
    242         * @param       val     変換する元の文字列
    243         *
    244         * @return      変換後のdouble
    245         * @og.rtnNotNull
    246         */
    247        private double parseDouble( final String val ) {
    248                double rtn = 0.0d;
    249                if( val != null && !val.trim().isEmpty() ) {
    250                        try {
    251                                rtn = Double.parseDouble( val.trim() );
    252                        }
    253                        catch( final NumberFormatException ex ) {
    254                                final String errMsg = "文字列を数値に変換できません。val=[" + val + "]" + ex.getMessage(); ;
    255                                System.out.println( errMsg );
    256                        }
    257                }
    258
    259                return rtn ;
    260        }
    261
    262        /**
    263         * 引数のdoubleを、少数点３桁で、四捨五入(HALF_UP)します。
    264         *
    265         * 長い処理式を、短くすることが目的のメソッドです。
    266         *
    267         * @param       val     変換する元のdouble
    268         *
    269         * @return      変換後のdouble
    270         * @og.rtnNotNull
    271         */
    272        private double halfUp( final double val ) {
    273                return BigDecimal.valueOf( val ).setScale( 3 , RoundingMode.HALF_UP ).doubleValue();
    274        }
    275}