From f521c6fdc8088f8ccfd39d51ec36b01751ceca4f Mon Sep 17 00:00:00 2001 From: Cyril Pernet Date: Mon, 29 May 2017 21:19:22 +0100 Subject: [PATCH] all the standard function we had --- Anscombe.mat | Bin 0 -> 429 bytes Anscombe.txt | 11 + Anscombe.xls | Bin 0 -> 13824 bytes HZmvntest.m | 259 ++++ LIBRA/Academic License LIBRA.pdf | Bin 0 -> 40320 bytes LIBRA/adjustedboxplot.m | 653 ++++++++++ LIBRA/adjustedoutlyingness.m | 225 ++++ LIBRA/adm.m | 43 + LIBRA/agnes.m | 221 ++++ LIBRA/agricul.mat | Bin 0 -> 1032 bytes LIBRA/animal.mat | Bin 0 -> 312 bytes LIBRA/bagplot.m | 1959 ++++++++++++++++++++++++++++++ LIBRA/bannerplot.m | 64 + LIBRA/cda.m | 487 ++++++++ LIBRA/cdq.m | 370 ++++++ LIBRA/chiqqplot.m | 41 + LIBRA/clara.m | 194 +++ LIBRA/classSVD.m | 30 + LIBRA/clusplot.m | 239 ++++ LIBRA/country.mat | Bin 0 -> 288 bytes LIBRA/cpca.m | 139 +++ LIBRA/cpcr.m | 159 +++ LIBRA/csimca.m | 447 +++++++ LIBRA/csimpls.m | 223 ++++ LIBRA/cvMcd.m | 167 +++ LIBRA/cvRobpca.m | 206 ++++ LIBRA/cvRpcr.m | 553 +++++++++ LIBRA/cvRsimpls.m | 483 ++++++++ LIBRA/daisy.m | 94 ++ LIBRA/daisyc.mexw32 | Bin 0 -> 8192 bytes LIBRA/daplot.m | 75 ++ LIBRA/ddplot.m | 46 + LIBRA/diana.m | 197 +++ LIBRA/distplot.m | 44 + LIBRA/ellipsplot.m | 76 ++ LIBRA/extractmcdregres.m | 113 ++ LIBRA/fanny.m | 167 +++ LIBRA/fannyc.mexw32 | Bin 0 -> 11776 bytes LIBRA/flower.mat | Bin 0 -> 263 bytes LIBRA/greatsort.m | 23 + LIBRA/halfspacedepth.m | 134 ++ LIBRA/hl.m | 58 + LIBRA/kernelEVD.m | 34 + LIBRA/l1median.m | 89 ++ LIBRA/lmc.m | 42 + LIBRA/lsscatter.m | 53 + LIBRA/ltsregres.m | 1162 ++++++++++++++++++ LIBRA/madc.m | 79 ++ LIBRA/mahalanobis.m | 96 ++ LIBRA/makeplot.m | 1501 +++++++++++++++++++++++ LIBRA/mc.m | 48 + LIBRA/mcdcov.m | 1445 ++++++++++++++++++++++ LIBRA/mcdregres.m | 303 +++++ LIBRA/mcenter.m | 16 + LIBRA/medc.mexw32 | Bin 0 -> 10240 bytes LIBRA/mlochuber.m | 128 ++ LIBRA/mloclogist.m | 124 ++ LIBRA/mlr.m | 149 +++ LIBRA/mona.m | 181 +++ LIBRA/monac.mexw32 | Bin 0 -> 9728 bytes LIBRA/mscalelogist.m | 122 ++ LIBRA/normqqplot.m | 32 + LIBRA/obj200.mat | Bin 0 -> 3392 bytes LIBRA/ols.m | 143 +++ LIBRA/pam.m | 236 ++++ LIBRA/pamc.mexw32 | Bin 0 -> 12288 bytes LIBRA/plotnumbers.m | 94 ++ LIBRA/predict.m | 276 +++++ LIBRA/putlabel.m | 56 + LIBRA/qn.m | 31 + LIBRA/qn.mexw32 | Bin 0 -> 9216 bytes LIBRA/qnm.m | 74 ++ LIBRA/randomset.m | 39 + LIBRA/rapca.m | 430 +++++++ LIBRA/rda.m | 527 ++++++++ LIBRA/regresdiagplot.m | 148 +++ LIBRA/regresdiagplot3d.m | 153 +++ LIBRA/removal.m | 32 + LIBRA/removeObsMcd.m | 91 ++ LIBRA/removeObsRobpca.m | 168 +++ LIBRA/residualplot.m | 52 + LIBRA/rmc.m | 43 + LIBRA/robpca.m | 866 +++++++++++++ LIBRA/robpcaregres.m | 94 ++ LIBRA/robstd.m | 115 ++ LIBRA/rpcr.m | 464 +++++++ LIBRA/rrmse.m | 140 +++ LIBRA/rsimca.m | 610 ++++++++++ LIBRA/rsimpls.m | 519 ++++++++ LIBRA/rsquared.m | 106 ++ LIBRA/rstep.m | 152 +++ LIBRA/ruspini.mat | Bin 0 -> 335 bytes LIBRA/sclprc.mexw32 | Bin 0 -> 7168 bytes LIBRA/scorediagplot.m | 126 ++ LIBRA/screeplot.m | 59 + LIBRA/silhouetteplot.m | 36 + LIBRA/simcaplot.m | 169 +++ LIBRA/spannc.mexw32 | Bin 0 -> 7680 bytes LIBRA/tree.m | 558 +++++++++ LIBRA/twinsc.mexw32 | Bin 0 -> 12800 bytes LIBRA/twopoints.m | 59 + LIBRA/unimcd.m | 71 ++ LIBRA/uniran.m | 12 + LIBRA/updatecov.m | 41 + LIBRA/weightmecov.m | 31 + Pearson.m | 234 ++++ Spearman.m | 222 ++++ bendcorr.m | 308 +++++ compare_correlations.m | 142 +++ conditional.m | 30 + corr_normplot.m | 76 ++ demean.m | 15 + detect_outliers.m | 263 ++++ idealf.m | 18 + iqr_method.m | 59 + joint_density.m | 85 ++ licenses.m | 58 + madmedianrule.m | 92 ++ manual.pdf | Bin 0 -> 506832 bytes pairwise_cleanup.m | 22 + randi.m.zip | Bin 0 -> 376 bytes robust_correlation.m | 175 +++ skipped_correlation.m | 805 ++++++++++++ univar.m | 32 + variance_homogeneity.m | 95 ++ 125 files changed, 23356 insertions(+) create mode 100644 Anscombe.mat create mode 100644 Anscombe.txt create mode 100644 Anscombe.xls create mode 100644 HZmvntest.m create mode 100644 LIBRA/Academic License LIBRA.pdf create mode 100644 LIBRA/adjustedboxplot.m create mode 100644 LIBRA/adjustedoutlyingness.m create mode 100644 LIBRA/adm.m create mode 100644 LIBRA/agnes.m create mode 100644 LIBRA/agricul.mat create mode 100644 LIBRA/animal.mat create mode 100644 LIBRA/bagplot.m create mode 100644 LIBRA/bannerplot.m create mode 100644 LIBRA/cda.m create mode 100644 LIBRA/cdq.m create mode 100644 LIBRA/chiqqplot.m create mode 100644 LIBRA/clara.m create mode 100644 LIBRA/classSVD.m create mode 100644 LIBRA/clusplot.m create mode 100644 LIBRA/country.mat create mode 100644 LIBRA/cpca.m create mode 100644 LIBRA/cpcr.m create mode 100644 LIBRA/csimca.m create mode 100644 LIBRA/csimpls.m create mode 100644 LIBRA/cvMcd.m create mode 100644 LIBRA/cvRobpca.m create mode 100644 LIBRA/cvRpcr.m create mode 100644 LIBRA/cvRsimpls.m create mode 100644 LIBRA/daisy.m create mode 100644 LIBRA/daisyc.mexw32 create mode 100644 LIBRA/daplot.m create mode 100644 LIBRA/ddplot.m create mode 100644 LIBRA/diana.m create mode 100644 LIBRA/distplot.m create mode 100644 LIBRA/ellipsplot.m create mode 100644 LIBRA/extractmcdregres.m create mode 100644 LIBRA/fanny.m create mode 100644 LIBRA/fannyc.mexw32 create mode 100644 LIBRA/flower.mat create mode 100644 LIBRA/greatsort.m create mode 100644 LIBRA/halfspacedepth.m create mode 100644 LIBRA/hl.m create mode 100644 LIBRA/kernelEVD.m create mode 100644 LIBRA/l1median.m create mode 100644 LIBRA/lmc.m create mode 100644 LIBRA/lsscatter.m create mode 100644 LIBRA/ltsregres.m create mode 100644 LIBRA/madc.m create mode 100644 LIBRA/mahalanobis.m create mode 100644 LIBRA/makeplot.m create mode 100644 LIBRA/mc.m create mode 100644 LIBRA/mcdcov.m create mode 100644 LIBRA/mcdregres.m create mode 100644 LIBRA/mcenter.m create mode 100644 LIBRA/medc.mexw32 create mode 100644 LIBRA/mlochuber.m create mode 100644 LIBRA/mloclogist.m create mode 100644 LIBRA/mlr.m create mode 100644 LIBRA/mona.m create mode 100644 LIBRA/monac.mexw32 create mode 100644 LIBRA/mscalelogist.m create mode 100644 LIBRA/normqqplot.m create mode 100644 LIBRA/obj200.mat create mode 100644 LIBRA/ols.m create mode 100644 LIBRA/pam.m create mode 100644 LIBRA/pamc.mexw32 create mode 100644 LIBRA/plotnumbers.m create mode 100644 LIBRA/predict.m create mode 100644 LIBRA/putlabel.m create mode 100644 LIBRA/qn.m create mode 100644 LIBRA/qn.mexw32 create mode 100644 LIBRA/qnm.m create mode 100644 LIBRA/randomset.m create mode 100644 LIBRA/rapca.m create mode 100644 LIBRA/rda.m create mode 100644 LIBRA/regresdiagplot.m create mode 100644 LIBRA/regresdiagplot3d.m create mode 100644 LIBRA/removal.m create mode 100644 LIBRA/removeObsMcd.m create mode 100644 LIBRA/removeObsRobpca.m create mode 100644 LIBRA/residualplot.m create mode 100644 LIBRA/rmc.m create mode 100644 LIBRA/robpca.m create mode 100644 LIBRA/robpcaregres.m create mode 100644 LIBRA/robstd.m create mode 100644 LIBRA/rpcr.m create mode 100644 LIBRA/rrmse.m create mode 100644 LIBRA/rsimca.m create mode 100644 LIBRA/rsimpls.m create mode 100644 LIBRA/rsquared.m create mode 100644 LIBRA/rstep.m create mode 100644 LIBRA/ruspini.mat create mode 100644 LIBRA/sclprc.mexw32 create mode 100644 LIBRA/scorediagplot.m create mode 100644 LIBRA/screeplot.m create mode 100644 LIBRA/silhouetteplot.m create mode 100644 LIBRA/simcaplot.m create mode 100644 LIBRA/spannc.mexw32 create mode 100644 LIBRA/tree.m create mode 100644 LIBRA/twinsc.mexw32 create mode 100644 LIBRA/twopoints.m create mode 100644 LIBRA/unimcd.m create mode 100644 LIBRA/uniran.m create mode 100644 LIBRA/updatecov.m create mode 100644 LIBRA/weightmecov.m create mode 100644 Pearson.m create mode 100644 Spearman.m create mode 100644 bendcorr.m create mode 100644 compare_correlations.m create mode 100644 conditional.m create mode 100644 corr_normplot.m create mode 100644 demean.m create mode 100644 detect_outliers.m create mode 100644 idealf.m create mode 100644 iqr_method.m create mode 100644 joint_density.m create mode 100644 licenses.m create mode 100644 madmedianrule.m create mode 100644 manual.pdf create mode 100644 pairwise_cleanup.m create mode 100644 randi.m.zip create mode 100644 robust_correlation.m create mode 100644 skipped_correlation.m create mode 100644 univar.m create mode 100644 variance_homogeneity.m diff --git a/Anscombe.mat b/Anscombe.mat new file mode 100644 index 0000000000000000000000000000000000000000..01192d318bad422cca85c5b950027e977cf9850e GIT binary patch literal 429 zcmeZu4DoSvQZUssQ1EpO(M`+DN!3vZ$Vn_o%P-2cQgHY2i?A@$QE)CwO)N=GQOM7; zQV1zcRq!g!Q!p}9Ff_F?HnB1?R4_6yG*TcHFfe-h@-r|ns4_AzRLt2rN!$N$fJ7U= zx7)F40(y5fvain*i->BPo%gBfCQDj|2+K)biwnjQ{}|IcR=)IN68rMSgyRgyq~gl< z!#XzwYpbe$J*~dCK32@>k4Q^Ihz_Us-mW|0Cs>RAKYI4%)1N0fo_FoVe{8zZwp6A3 zf|nk*Pe1AKX!C0RZ6D^o zV7EK9_+MyPT&t%1y9J9c#=T{afM5Lgi`>DYfsEU%YeFcRQ_fa_{LH)jfV8 z4b3zD@ji>3J$rqoaaO@Ox!T~E^jino9mR_J1fRcm^3>eC>Z{!poBL17XPgT^cjDUq r=iBP%e!jV&{=@V1J^PpZ)t{7kPHU!-Kd=9;HS$L7?eV`gmYfFw0#CD( literal 0 HcmV?d00001 diff --git a/Anscombe.txt b/Anscombe.txt new file mode 100644 index 0000000..cb024a2 --- /dev/null +++ b/Anscombe.txt @@ -0,0 +1,11 @@ + 1.0000000e+01 8.0400000e+00 1.0000000e+01 9.1400000e+00 1.0000000e+01 7.4600000e+00 8.0000000e+00 6.5800000e+00 + 8.0000000e+00 6.9500000e+00 8.0000000e+00 8.1400000e+00 8.0000000e+00 6.7700000e+00 8.0000000e+00 5.7600000e+00 + 1.3000000e+01 7.5800000e+00 1.3000000e+01 8.7400000e+00 1.3000000e+01 1.2740000e+01 8.0000000e+00 7.7100000e+00 + 9.0000000e+00 8.8100000e+00 9.0000000e+00 8.7700000e+00 9.0000000e+00 7.1100000e+00 8.0000000e+00 8.8400000e+00 + 1.1000000e+01 8.3300000e+00 1.1000000e+01 9.2600000e+00 1.1000000e+01 7.8100000e+00 8.0000000e+00 8.4700000e+00 + 1.4000000e+01 9.9600000e+00 1.4000000e+01 8.1000000e+00 1.4000000e+01 8.8400000e+00 8.0000000e+00 7.0400000e+00 + 6.0000000e+00 7.2400000e+00 6.0000000e+00 6.1300000e+00 6.0000000e+00 6.0800000e+00 8.0000000e+00 5.2500000e+00 + 4.0000000e+00 4.2600000e+00 4.0000000e+00 3.1000000e+00 4.0000000e+00 5.3900000e+00 1.9000000e+01 1.2500000e+01 + 1.2000000e+01 1.0840000e+01 1.2000000e+01 9.1300000e+00 1.2000000e+01 8.1500000e+00 8.0000000e+00 5.5600000e+00 + 7.0000000e+00 4.8200000e+00 7.0000000e+00 7.2600000e+00 7.0000000e+00 6.4200000e+00 8.0000000e+00 7.9100000e+00 + 5.0000000e+00 5.6800000e+00 5.0000000e+00 4.7400000e+00 5.0000000e+00 5.7300000e+00 8.0000000e+00 6.8900000e+00 diff --git a/Anscombe.xls b/Anscombe.xls new file mode 100644 index 0000000000000000000000000000000000000000..73bf5f8206015287c13e917b3e6370c316e657bc GIT binary patch literal 13824 zcmeHOU1(fI6h3#en{1l2*`#TWwOy7?wfWh^#6%4ZX;M+q2QgHo5i9v?N=n)$BoR`f zx@nUd`-71@h*B`cClNFvXdhIPH(!cHC{+?Xckj&JJ@;wx;^}ucT$HS|O>*RVDqnKbVR~D-wSrUY;l~uWQ~_lo%RZ=I?$-%2)>Pcsf@x`yYt?`;T z+B$36I%}fAgjC8F8v1Ix(s;@<^jjn$n7H(T;6T<`M&b|G+UgJ3DAjUj+JK(5JHBVC z!}@lMQmT{wb1J5#$>w8}b2+H|C$H(oa<0#%^p}&f*QNCT&vHj%49`sl&k$x#arh-$ zUb&1sv5dTG8F~Jaa*n3{1pRSm0zru+b5bmelet=!B!gP6P3CDSNzt-28A^5Oon%rX zasf9>!9}(Gz)0`l{+_-Q5=8F_S#ZK2TxlOWauwH7Sha-ro{oBCPcrMciyX6#26D_g z8p$=LWEE^C(0d^)d&BEM9Lc10ZW&kw#@yB;8MpOiV11Q`)WgIOFL5@Y2j;Qv8=BFjD z8V=xa0E*|b$YLuRkQQ4T0#86UEQi!baW>~u3q1$cs0KYJxS#;Ox|G+a5X@ka%6PGc z;fE$y#(1)2_@?`vj1gq(aW=DMd@RbAbkw(;J?>f(3tHYm%kVMPQ2w%sQ~CZW-*W7jYf0Q_nV@CtCDl;A z-NdOp#-WbJEJs~SVoYfnEoY5a%2ymwZRN%Aw1l>6$yLRAL@l4yHs!lUeE##K@(r3- zx|Upfw5)*Cxje0H%1@a%l^;u+p{{Y)l53Ba8)+GPPA!$6GoC9SnNYdLdB&R+u47u3 z(sF)WHI$z{tC1ZP<2hKX_JZ|1L zSp+Ns76FTZMZh9p5wHkY1S|p;0gHe|z#{M$BXFbrf8hMt^QUTWi_E-&{eR-h+uZNZ zfxOdI?)Ps2wS#!b+X0Gzx-bNp?eZ&mT-H2$xH`)$5A#qBls?%c=oMa&`)+lx__fVkiH zUisvnpXUN>zXoDy)4vme9Wsc|sKn#}Ie>Cl4r1n3p4Q{-5ic^Xqga@^$h_wApeZtQ zt_M%D6Qd8yfb^&z1D<%rCH@8S+xYZj#fChnLJG-a@{H`qr(0ux7%1#fpC}o{e?JBx rSw2-@d|pGmepWg@&(HLr3`I!p#|XXZL!ZvTAM>Z_PsFdGyYc@DqHku$ literal 0 HcmV?d00001 diff --git a/HZmvntest.m b/HZmvntest.m new file mode 100644 index 0000000..8d95759 --- /dev/null +++ b/HZmvntest.m @@ -0,0 +1,259 @@ +function [HZmvntest,P] = HZmvntest(X,c,alpha) +%HZMVNTEST. Henze-Zirkler's Multivariate Normality Test. +% Henze and Zirkler (1990) introduce a multivariate version of the univariate +% There are many tests for assessing the multivariate normality in the +% statistical literature (Mecklin and Mundfrom, 2003). Unfortunately, there +% is no known uniformly most powerful test and it is recommended to perform +% several test to assess it. It has been found that the Henze and Zirkler +% test have a good overall power against alternatives to normality. +% +% The Henze-Zirkler test is based on a nonnegative functional distance that +% measures the distance between two distribution functions. This distance +% measure is, +% _ +% / +% D_b(P,Q)K = / |P(T) - Q(t)|^2 f_b(t)dt +% _/ +% R^d +% +% where P(t) [the characteristic function of the multivariate normality +% distribution] and Q(t) [the empirical characteristic function] are the +% Fourier transformations of P and Q, and f_b is the weight or kernel +% function. Its normal density is, +% +% -|t|^2 +% f_b(t) = (2*pi*b^2)^-p/2*exp(--------), t E R^p +% 2*b^2 +% +% where p = number of variables and |t| = (t't)^0.5. +% +% The smoothing parameter b depends on n (sample size) as, +% +% 1 2p + 1 +% b_p(n) = ------ (-------)^1/(p+4)*n^1/(p+4) +% sqrt(2) 4 +% +% The Henze-Zirkler statistic is approximately distributed as a lognormal. +% The lognormal distribution is used to compute the null hypothesis +% probability. +% _n _ _n _ +% 1 \ \ b^2 +% W_n,b = ----- /_ _ /_ _ exp(- ------- Djk) - 2(1+b^2)^-p/2* +% n^2 j=1 k=1 2 +% +% _n _ +% 1 \ b^2 +% --- /_ _ exp(- --------- Dj) + (1+2b^2)^-p/2 +% n j=1 2(1+b^2) +% +% HZ = n*(4 1{S is singular} + W_n,b 1{S is nonsingular}) +% +% where W_n,b = weighted L^2-distance +% Djk = (X_j - X_k)*inv(S)*(X_j - X_k)' +% Dj = (X - MX)*inv(S)*(X - MX)' +% X = data matrix +% MX = sample mean vector +% S = covariance matrix (normalized by n) +% HZ = Henze-Zirkler statistic test (it is known in literature as +% T_n,b) +% 1{.} = stands for the indicator function +% +% According to Henze-Wagner (1997), this test has the desirable properties +% of, +% --affine invariance +% --consistency against each fixed nonnormal alternative distribution +% --asymptotic power against contiguous alternatives of order n^-1/2 +% --feasibility for any dimension and any sample size +% +% If the data is multivariate normality, the test statistic HZ is +% approximately lognormally distributed. It proceeds to calculate the mean, +% variance and smoothness parameter. Then, mean and variance are +% lognormalized and the z P-value is estimated. +% +% Also, for all the interested people we provide the q_b,p(alpha) = +% HZ(1-alpha)-quantile of its lognormal distribution (critical value) +% having expectation E(T_b(p)) and variance var(T_b(p)). +% +% Syntax: function HZmvntest(X,alpha) +% +% Inputs: +% X - data matrix (size of matrix must be n-by-p; data=rows, +% indepent variable=columns) +% c - covariance normalized by n (=1, default)) or n-1 (~=1) +% alpha - significance level (default = 0.05) +% +% Output: +% - Henze-Zirkler's Multivariate Normality Test +% +% Example: From the Table 11.5 (Iris data) of Johnson and Wichern (1992, +% p. 562), we take onlt the Isis setosa data to test if it has a +% multivariate normality distribution using the Doornik-Hansen +% omnibus test. Data has 50 observations on 4 independent +% variables (Var1 (x1) = sepal length; Var2 (x2) = sepal width; +% Var3 (x3) = petal length; Var4 (x4) = petal width. +% +% ------------------------------ +% x1 x2 x3 x4 +% ------------------------------ +% 5.1 3.5 1.4 0.2 +% 4.9 3.0 1.4 0.2 +% 4.7 3.2 1.3 0.2 +% 4.6 3.1 1.5 0.2 +% 5.0 3.6 1.4 0.2 +% 5.4 3.9 1.7 0.4 +% . . . . +% . . . . +% . . . . +% 5.1 3.8 1.6 0.2 +% 4.6 3.2 1.4 0.2 +% 5.3 3.7 1.5 0.2 +% 5.0 3.3 1.4 0.2 +% ------------------------------ +% +% Total data matrix must be: +% You can get the X-matrix by calling to iris data file provided in +% the zip as +% load path-drive:irisetdata +% +% Calling on Matlab the function: +% HZmvntest(X) +% +% Answer is: +% +% Henze-Zirkler's Multivariate Normality Test +% ------------------------------------------------------------------- +% Number of variables: 4 +% Sample size: 50 +% ------------------------------------------------------------------- +% Henze-Zirkler lognormal mean: -0.2794083 +% Henze-Zirkler lognormal variance: 0.1379069 +% Henze-Zirkler statistic: 0.9488453 +% P-value associated to the Henze-Zirkler statistic: 0.0499536 +% With a given significance = 0.050 +% ------------------------------------------------------------------- +% Data analyzed do not have a normal distribution. +% ------------------------------------------------------------------- +% +% Created by A. Trujillo-Ortiz, R. Hernandez-Walls, K. Barba-Rojo, +% and L. Cupul-Magana +% Facultad de Ciencias Marinas +% Universidad Autonoma de Baja California +% Apdo. Postal 453 +% Ensenada, Baja California +% Mexico. +% atrujo@uabc.mx +% +% Copyright. December 5, 2007. +% +% To cite this file, this would be an appropriate format: +% Trujillo-Ortiz, A., R. Hernandez-Walls, K. Barba-Rojo and L. Cupul-Magana. +% (2007). HZmvntest:Henze-Zirkler's Multivariate Normality Test. A MATLAB +% file. [WWW document]. URL http://www.mathworks.com/matlabcentral/ +% fileexchange/loadFile.do?objectId=17931 +% +% References: +% Henze, N. and Zirkler, B. (1990), A Class of Invariant Consistent Tests +% for Multivariate Normality. Commun. Statist.-Theor. Meth., 19(10): +% 3595�3618. +% Henze, N. and Wagner, Th. (1997), A New Approach to the BHEP tests for +% multivariate normality. Journal of Multivariate Analysis, 62:1-23. +% Johnson, R. A. and Wichern, D. W. (1992), Applied Multivariate Statistical +% Analysis. 3rd. ed. New-Jersey:Prentice Hall. +% Mecklin, C. J. and Mundfrom, D. J. (2003), On Using Asymptotic Critical +% Values in Testing for Multivariate Normality. InterStat: Statistics +% on Internet. http://interstat.statjournals.net/YEAR/2003/abstracts/ +% 0301001.php +% + +if nargin < 3 + alpha = 0.05; %default +end + +if alpha <= 0 || alpha >= 1 + fprintf(['Warning: Significance level error; must be 0 <'... + ' alpha < 1 \n']); + return; +end + +if nargin < 2 + c = 1; +end + +if nargin < 1 + error('Requires at least one input argument.'); + return; +end + +% remove NaNs +% ----------- +X(find(sum(isnan(X),2)),:) = []; + +if c == 1 %covariance matrix normalizes by (n) [=default] + S = cov(X,1); +else %covariance matrix normalizes by (n-1) + S = cov(X); +end + +[n,p] = size(X); + +difT = (X - repmat(mean(X),n,1)); + +Dj = diag(difT*inv(S)*difT'); %squared-Mahalanobis' distances + +Y = X*inv(S)*X'; + +Djk = - 2*Y' + diag(Y')*ones(1,n) + ones(n,1)*diag(Y')'; %this procedure was + %taken into account in order to '..avoiding loops and + %it (the file) runs much faster' we thank to Johan + %(J.D.) for it valuabe comment (15/12/08) to improve + %it. + +b = 1/(sqrt(2))*((2*p + 1)/4)^(1/(p + 4))*(n^(1/(p + 4))); %smoothing + %parameter +if (rank(S) == p), + HZ = n * (1/(n^2) * sum(sum(exp( - (b^2)/2 * Djk))) - 2 *... + ((1 + (b^2))^( - p/2)) * (1/n) * (sum(exp( - ((b^2)/(2 *... + (1 + (b^2)))) * Dj))) + ((1 + (2 * (b^2)))^( - p/2))); +else + HZ = n*4; +end + +wb = (1 + b^2)*(1 + 3*b^2); % = 1 + 4*b^2 + 3*b^4 + +a = 1 + 2*b^2; + +mu = 1 - a^(- p/2)*(1 + p*b^2/a + (p*(p + 2)*(b^4))/(2*a^2)); %HZ mean + +si2 = 2*(1 + 4*b^2)^(- p/2) + 2*a^( - p)*(1 + (2*p*b^4)/a^2 + (3*p*... + (p + 2)*b^8)/(4*a^4)) - 4*wb^( - p/2)*(1 + (3*p*b^4)/(2*wb) + (p*... + (p + 2)*b^8)/(2*wb^2)); %HZ variance + +pmu = log(sqrt(mu^4/(si2 + mu^2))); %lognormal HZ mean +psi = sqrt(log((si2 + mu^2)/mu^2)); %lognormal HZ variance + +P = 1 - logncdf(HZ,pmu,psi);%P-value associated to the HZ statistic + +%(1-alpha)-quantile of the lognormal distribution +%q = mu*(1+si2/mu^2)^(-1/2)*exp(norminv(1-alpha)*sqrt(log(1+si2/mu^2))); +HZmvntest = HZ; + +disp(' ') +disp('Henze-Zirkler''s Multivariate Normality Test') +disp('-------------------------------------------------------------------') +fprintf('Number of variables: %i\n', p); +fprintf('Sample size: %i\n', n); +disp('-------------------------------------------------------------------') +fprintf('Henze-Zirkler lognormal mean: %3.7f\n', pmu); +fprintf('Henze-Zirkler lognormal variance: %3.7f\n', psi); +fprintf('Henze-Zirkler statistic: %3.7f\n', HZ); +fprintf('P-value associated to the Henze-Zirkler statistic: %3.7f\n', P); +fprintf('With a given significance = %3.3f\n', alpha); +disp('-------------------------------------------------------------------') +if P >= alpha; + disp('Data analyzed have a normal distribution.'); +else + disp('Data analyzed do not have a normal distribution.'); +end +disp('-------------------------------------------------------------------') + +return, \ No newline at end of file diff --git a/LIBRA/Academic License LIBRA.pdf b/LIBRA/Academic License LIBRA.pdf new file mode 100644 index 0000000000000000000000000000000000000000..edda7bca73d0718a478a9394846095bfdb5905e0 GIT binary patch literal 40320 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 F{{?(cmOcOg literal 0 HcmV?d00001 diff --git a/LIBRA/adjustedboxplot.m b/LIBRA/adjustedboxplot.m new file mode 100644 index 0000000..ca87080 --- /dev/null +++ b/LIBRA/adjustedboxplot.m @@ -0,0 +1,653 @@ +function result=adjustedboxplot(x,varargin) +% +%ADJUSTEDBOXPLOT produces an adjusted box and whisker plot with one box for each column +% of X. +% Typical for this boxplot are its skewness-adjusted whiskers, which are based on +% the medcouple, a robust measure of skewness (see mc.m). At skewed data, the original boxplot +% typically marks too many regular observations as outliers. The adjusted boxplot on the other +% hand makes a better distinction between regular observations and real outliers. +% +% The ADJUSTED BOXPLOT is constructed as follows: +% - a line is put at the height of the sample median. +% - the box is drawn from the first to the third quartile. +% - At right skewed data (MC >= 0), all points outside the interval +% [Q1 - 1.5*e^(a*MC)*IQR ; Q3 + 1.5*e^(b*MC)*IQR] are marked as outliers, +% where Q1 and Q3 denote the first and third quartile respectively, +% IQR stands for the interquartile range and MC is an abbreviation +% for the medcouple (see mc.m). +% At left skewed data (MC < 0), the interval [Q1 - 1.5*e^(-b*MC)*IQR ; +% Q3 + 1.5*e^(-a*MC)*IQR] is used, because of symmetry reasons. +% - finally, the whiskers are drawn, going from the ends of the box to the +% most remote points that are no outliers. +% Note that the standard boxplot has the same box, but other whiskers. In that case, +% all points outside the interval [Q1 - 1.5*IQR ; Q3 + 1.5*IQR] are marked as outliers. +% The adjusted boxplot is thus similar to the original boxplot at symmetric distributions +% where MC=0. +% +% The skewness-adjusted boxplot is introduced in: +% Hubert, M. and Vandervieren, E. (2008), +% "An adjusted boxplot for skewed distributions", +% Computational Statistics and Data Analysis, 52, 5186–5201. +% +% +% Required input arguments: +% x : a data matrix; for each column the adjusted boxplot is +% drawn. When also a grouping vector is given, x must be a +% vector. +% +% Optional input arguments: +% a : number, defining the whisker of the +% adjusted boxplot. The default is to use a = -4. +% b : number, defining the whisker of the +% adjusted boxplot. The default is to use b = 3. +% This means that for right skewed data the interval +% [Q1 - 1.5*e^(-4*MC)*IQR ; Q3 + 1.5*e^(3*MC)*IQR] and +% for left skewed data the interval +% [Q1 - 1.5*e^(-3*MC)*IQR ; Q3 + 1.5*e^(4*MC)*IQR] is +% used. +% groupvalid : Grouping variable defined as a vector, string matrix, +% or cell array of strings. Groupvalid can also be a +% cell array of several grouping variables (such as +% {G1 G2 G3}) to group the values in x by each unique +% combination of grouping variable values. +% classic : If equal to 1, dotted lines are drawn at the height +% of the original whiskers. If equal to 0, only the +% adjusted boxplot is plotted. (default is 0). +% symbol : Symbol and color to use for all outliers (default is 'r+'). +% vert : Box orientation, value 1 for a vertical boxplot (default) +% or value 0 for a horizontal boxplot. +% labels : Character array or cell array of strings containing +% labels for each column of x, or each group in G. +% colors : A string or a three-column matrix of box colors. Each +% box (outline, median line, and whiskers) is drawn in the +% corresponding color. Default is to draw all boxes with +% blue outline, red median, and black whiskers. Colors are +% recycled if mecessary. +% widths : A numeric vector or scalar of box widths. Default is +% 0.5, or slightly smaller for fewer than three boxes. +% Widths are recycled if necessary. +% positions : A numeric vector of box positions. Default is 1:n. +% grouporder : When G is given, a character array or cell array of +% group names, specifying the ordering of the groups in +% G. Ignored when G is not given. +% +% +% I/O: result=adjustedboxplot(x,'a',-4,'b',3,'groupvalid',[],'classic',0,'symbol','r+',... +% 'orientation',1,'labels',[],'colors',[],'widths',1.5,'positions',[],'grouporder',[]); +% The user should only give the input arguments that have to change their default value. +% The name of the input arguments needs to be followed by their value. +% The order of the input arguments is of no importance. +% +% ADJUSTEDBOXPLOT calls ADJUSTEDBOXUTIL to do the actual plotting. +% +% Examples: ADJUSTED BOXPLOT of car mileage grouped by country +% load carsmall +% out=adjustedboxplot(MPG,'groupvalid',Origin,'classic',1) +% out=adjustedboxplot(MPG,'groupvalid',Origin,'symbol','b*','orientation',0) +% out=adjustedboxplot(MPG,'groupvalid',Origin,'widths',0.75,'positions',[1 3 4 7 8 10], ... +% 'grouporder',{'France' 'Germany' 'Italy' 'Japan' 'Sweden' 'USA'},'colors','kbrgym') +% +% The output is a structure containing the following fields: +% result.a : numeric value, used in the definition of the outlier cutoffs +% of the adjusted boxplot. +% result.b : numeric value, used in the definition of the outlier cutoffs +% of the adjusted boxplot. +% result.groupvalid : If there is a grouping vector, this vector +% contains the assigned group numbers for the observations in vector x. +% result.classic : value 1 or 0, indicating if dotted lines have been plot +% at the heigth of the standard whiskers. +% result.symbol : symbol and color that has been used for all outliers. +% result.orientation : If equal to 1, the boxes are vertically oriented. +% If equal to 0, a horizontal orientation has been used. +% result.labels : character array or cell array of strings, containing +% labels for each column of X, or each group in G. +% result.colors : a string or three-column matrix of box colors. +% result.widths : a numeric vector of scalar of box widths. +% result.positions : a numeric vector, containing the box positions. +% result.grouporder : If there is a grouping vector, this character array +% or cell array of group names specifies the ordering of the groups. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Ellen Vandervieren +% Created on: 07/04/2005 +% Last Update: 12/12/2006 +% + +if nargin<1 + error('Input argument ''x'' is undefined.') +end + +whissw = 0; % don't plot whisker inside the box. + +if isvector(x) + % Might have one box, or might have a grouping variable. n will be properly + % set later for the latter. + x = x(:); + n = 1; % +else + % Have a data matrix, use as many boxes as columns. + n = size(x,2); +end + +% Assigning default-values +counter=1; +default=struct('a',-4,'b',3,'groupvalid',[],'classic',0,'symbol','r+','orientation',1,'labels',[],... + 'colors',[],'positions',[],'widths',0.5,'grouporder',[]); +% colors: default is blue box, red median, black whiskers +% positions: default is 1:n +% widths: default is 0.5, smaller for three or fewer boxes +% grouporder: default is 1:n +list=fieldnames(default); +result=default; +IN=length(list); +i=1; +%reading the user's input +if nargin>1 + % + %placing inputfields in array of strings + % + for j=1:nargin-1 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % + %Checking which default parameters have to be changed + % and keep them in the structure 'result'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) %in case of similarity + for j=1:nargin-1 %searching the index of the accompanying field + if rem(j,2)~=0 %fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + result=setfield(result,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end +end + +a=result.a; +b=result.b; +group=result.groupvalid; +classic=result.classic; +symbol=result.symbol; +orientation=result.orientation; +labels=result.labels; +colors=result.colors; +positions=result.positions; +widths=result.widths; +grouporder=result.grouporder; + +% a and b must be numeric scalars. +if isempty(a) + a = -4; +elseif ~isscalar(a) || ~isnumeric(a) + error('LIBRA:adjustedboxplot:BadA',... + 'The ''a'' parameter value must be a numeric scalar.'); +end + +if isempty(b) + b = 3; +elseif ~isscalar(b) || ~isnumeric(b) + error('LIBRA:adjustedboxplot:BadB',... + 'The ''b'' parameter value must be a numeric scalar.'); +end + +% When group is non-empty, x must be a vector. +if (~isempty(group) && ~isvector(x)) + error('LIBRA:adjustedboxplot:VectorRequired',... + 'x must be a vector when there is a grouping variable.'); +end + +% Classic must be equal to 0 or 1. +if isempty(classic) + classic = 0; +elseif ~isscalar(classic) || ~ismember(classic,0:1) + error('LIBRA:adjustedboxplot:InvalidClassic','Invalid value for ''classic'' parameter'); +end + +% Convert wordy inputs to internal codes +if isempty(orientation) + orientation = 1; +elseif ischar(orientation) + orientation = strmatch(orientation,{'horizontal' 'vertical'}) - 1; +end +if isempty(orientation) || ~isscalar(orientation) || ~ismember(orientation,0:1) + error('LIBRA:adjustedboxplot:InvalidOrientation',... + 'Invalid value for ''orientation'' parameter'); +end + +% Deal with grouping variable before processing more inputs +if ~isempty(group) + if orientation, sep = '\n'; + else + sep = ','; + end + [group,glabel,gname,multiline] = mgrp2idx(group,size(x,1),sep); + n = size(gname,1); + if numel(group) ~= numel(x) + error('LIBRA:adjustedboxplot:InputSizeMismatch',... + 'X and G must have the same length.'); + end +else + multiline = false; +end + +% Reorder the groups if necessary +if ~isempty(group) && ~isempty(grouporder) + if iscellstr(grouporder) || ischar(grouporder) + % If we have a grouping vector, grouporder may be a list of group names. + if ischar(grouporder), grouporder = cellstr(grouporder); end + [dum,grouporder] = ismember(grouporder(:),glabel); + % Must be a permutation of the group names + if ~isequal(sort(grouporder),(1:n)') + error('LIBRA:adjustedboxplot:BadGrouporder', ... + 'The ''grouporder'' parameter value must contain all the unique group names in G.'); + end + else + error('LIBRA:adjustedboxplot:BadGrouporder', ... + 'The ''grouporder'' parameter value must be a character array or a cell array of strings.'); + end + group = order(group); + glabel = glabel(grouporder); + gname = gname(grouporder,:); +end + +% Process the rest of the inputs + +if isempty(labels) + if ~isempty(group) + labels = glabel; + end +else + if ~(iscellstr(labels) && numel(labels)==n) && ... + ~(ischar(labels) && size(labels,1)==n) + % Must have one label for each box + error('LIBRA:adjustedboxplot:BadLabels','Incorrect number of box labels.'); + end + if ischar(labels), labels = cellstr(labels); end + multiline = false; +end +dfltLabs = (isempty(labels) && isempty(group)); % box labels are just column numbers + +if isempty(widths) + widths = repmat(min(0.15*n,0.5),n,1); +elseif ~isvector(widths) || ~isnumeric(widths) || any(widths<=0) + error('LIBRA:adjustedboxplot:BadWidths', ... + 'The ''widths'' parameter value must be a numeric vector of positive values.'); +elseif length(widths) < n + % Recycle the widths if necessary. + widths = repmat(widths(:),ceil(n/length(widths)),1); +end + +if isempty(colors) + % Empty colors tells adjustedboxutil to use defaults. + colors = char(zeros(n,0)); +elseif ischar(colors) && isvector(colors) + colors = colors(:); % color spec string, make it a column +elseif isnumeric(colors) && (ndims(colors)==2) && (size(colors,2)==3) + % RGB matrix, that's ok +else + error('LIBRA:adjustedboxplot:BadColors',... + 'The ''colors'' parameter value must be a string or a three-column numeric matrix.'); +end +if size(colors,1) < n + % Recycle the colors if necessary. + colors = repmat(colors,ceil(n/size(colors,1)),1); +end + +if isempty(positions) + positions = 1:n; +elseif ~isvector(positions) || ~isnumeric(positions) + error('LIBRA:adjustedboxplot:BadPositions', ... + 'The ''positions'' parameter value must be a numeric vector.'); +elseif length(positions) ~= n + % Must have one position for each box + error('LIBRA:adjustedboxplot:BadPositions', ... + 'The ''positions'' parameter value must have one element for each box.'); +else + if isempty(group) && isempty(labels) + % If we have matrix data and the positions are not 1:n, we need to + % force the default 1:n tick labels. + labels = cellstr(num2str((1:n)')); + end +end + +% +% Done processing inputs +% + +notch = 0; +whis = 1.5; + +% Put at least the widest box or half narrowest spacing at each margin +if n > 1 + wmax = max(max(widths), 0.5*min(diff(positions))); +else + wmax = 0.5; +end +xlims = [min(positions)-wmax, max(positions)+wmax]; + +ymin = nanmin(x(:)); +ymax = nanmax(x(:)); +if ymax > ymin + dy = (ymax-ymin)/20; +else + dy = 0.5; % no data range, just use a y axis range of 1 +end +ylims = [(ymin-dy) (ymax+dy)]; + +% Scale axis for vertical or horizontal boxes. +newplot +oldstate = get(gca,'NextPlot'); +set(gca,'NextPlot','add','Box','on'); +set(gcf,'Name', 'Adjusted boxplot', 'NumberTitle', 'off'); +if orientation + axis([xlims ylims]); + set(gca,'XTick',positions); + ylabel(gca,'Values'); + if dfltLabs, xlabel(gca, 'Column Number'); end +else + axis([ylims xlims]); + set(gca,'YTick',positions); + xlabel(gca,'Values'); + if dfltLabs, ylabel(gca,'Column Number'); end +end +if nargout>0 + hout = []; +end + +xvisible = NaN(size(x)); +notnans = ~isnan(x); +for i= 1:n + if ~isempty(group) + thisgrp = find((group==i) & notnans); + else + thisgrp = find(notnans(:,i)) + (i-1)*size(x,1); + end + [outliers,hh] = adjustedboxutil(x(thisgrp),a,b,classic,notch,positions(i),widths(i), ... + colors(i,:),symbol,orientation,whis,whissw); + outliers = thisgrp(outliers); + xvisible(outliers) = x(outliers); + if nargout>0 + hout = [hout; hh(:)]; + end +end + +if ~isempty(labels) + if multiline && orientation + % Turn off tick labels and axis label + set(gca, 'XTickLabel',''); + setappdata(gca,'NLines',size(gname,2)); + xlabel(gca,''); + ylim = get(gca, 'YLim'); + + % Place multi-line text approximately where tick labels belong + ypos = repmat(ylim(1),size(positions)); + text(positions,ypos,labels,'HorizontalAlignment','center', ... + 'VerticalAlignment','top', 'UserData','xtick'); + + % Resize function will position text more accurately + set(gcf, 'ResizeFcn', @resizefcn, ... + 'Interruptible','off', 'PaperPositionMode','auto'); + resizefcn(gcf); + elseif orientation + set(gca, 'XTickLabel',labels); + else + set(gca, 'YTickLabel',labels); + end +end +set(gca,'NextPlot',oldstate); + +% Store information for gname function +set(gca, 'UserData', {'adjustedboxplot' xvisible group orientation}); +hold off + +%Output structure +result=struct('a',{a},'b',{b},'groupvalid',{group},'classic',{classic}, ... + 'symbol',{symbol},'orientation',{orientation},'labels',{labels},'colors',{colors},... + 'widths',{widths},'positions',{positions},'grouporder',{grouporder}); + +%============================================================================= + +function [outlier,hout] = adjustedboxutil(x,a,b,classic,notch,lb,lf,clr,symbol,orientation,whis,whissw) +%ADJUSTEDBOXUTIL Produces a single adjusted boxplot. + +% define the median and the quantiles +pctiles = prctile(x,[25;50;75]); +q1 = pctiles(1,:); +med = pctiles(2,:); +q3 = pctiles(3,:); + +% find the extreme values (to determine where whiskers appear) +medc = mc(x); +if medc>=0 + vloadj = q1-whis*exp(a*medc)*(q3-q1); %Lower cutoff value for the adjusted boxplot. + loadj = min(x(x>=vloadj)); + vhiadj = q3+whis*exp(b*medc)*(q3-q1); %Upper cutoff value for the adjusted boxplot. + upadj = max(x(x<=vhiadj)); +else + vloadj = q1-whis*exp(-b*medc)*(q3-q1); %Lower cutoff value for the adjusted boxplot. + loadj = min(x(x>=vloadj)); + vhiadj = q3+whis*exp(-a*medc)*(q3-q1); %Upper cutoff value for the adjusted boxplot. + upadj = max(x(x<=vhiadj)); +end + +if (isempty(loadj)), loadj = q1; end +if (isempty(upadj)), upadj = q3; end + +if (isequal(classic,1)), + vloorig = q1-whis*(q3-q1); %Lower cutoff value for the original boxplot. + loorig = min(x(x>=vloorig)); + if (isempty(loorig)), loorig = q1; end +end + +if (isequal(classic,1)), + vhiorig = q3+whis*(q3-q1); %Upper cutoff value for the original boxplot. + uporig = max(x(x<=vhiorig)); + if (isempty(uporig)), uporig = q3; end +end + +x1 = repmat(lb,1,2); +x2 = x1+[-0.25*lf,0.25*lf]; +outlier = x upadj; +yy = x(outlier); + +xx = repmat(lb,1,length(yy)); +lbp = lb + 0.5*lf; +lbm = lb - 0.5*lf; + +if whissw == 0 + upadj = max(upadj,q3); + loadj = min(loadj,q1); + if (isequal(classic,1)), + uporig = max(uporig,q3); + loorig = min(loorig,q1); + end +end + +% Set up (X,Y) data for notches if desired. +if ~notch + xx2 = [lbm lbp lbp lbm lbm]; + yy2 = [q3 q3 q1 q1 q3]; + xx3 = [lbm lbp]; +else + n1 = med + 1.57*(q3-q1)/sqrt(length(x)); + n2 = med - 1.57*(q3-q1)/sqrt(length(x)); + if n1>q3, n1 = q3; end + if n2=7 + set(hout(7),'Tag','Outliers'); +else + hout(7) = NaN; +end + + +%============================================================================= + +function resizefcn(f)%,dum) + +% Adjust figure layout to make sure labels remain visible +h = findobj(f, 'UserData','xtick'); +if (isempty(h)) + set(f, 'ResizeFcn', ''); + return; +end +ax = get(f, 'CurrentAxes'); +nlines = getappdata(ax, 'NLines'); + +% Position the axes so that the fake X tick labels have room to display +set(ax, 'Units', 'characters'); +p = get(ax, 'Position'); +ptop = p(2) + p(4); +if (p(4) < nlines+1.5) + p(2) = ptop/2; +else + p(2) = nlines + 1; +end +p(4) = ptop - p(2); +set(ax, 'Position', p); +set(ax, 'Units', 'normalized'); + +% Position the labels at the proper place +xl = get(ax, 'XLabel'); +set(xl, 'Units', 'data'); +p = get(xl, 'Position'); +ylim = get(ax, 'YLim'); +p2 = (p(2)+ylim(1))/2; +for j=1:length(h) + p = get(h(j), 'Position') ; + p(2) = p2; + set(h(j), 'Position', p); +end + +%========================================================================== +function [ogroup,glabel,gname,multigroup] = mgrp2idx(group,rows,sep) +%MGRP2IDX Convert multiple grouping variables to index vector +% [OGROUP,GLABEL,GNAME,MULTIGROUP] = MGRP2IDX(GROUP,ROWS) takes +% the inputs GROUP, ROWS, and SEP. GROUP is a grouping variable (numeric +% vector, string matrix, or cell array of strings) or a cell array +% of grouping variables. ROWS is the number of observations. +% SEP is a separator for the grouping variable values. +% +% The output OGROUP is a vector of group indices. GLABEL is a cell +% array of group labels, each label consisting of the values of the +% various grouping variables separated by the characters in SEP. +% GNAME is a cell array containing one column per grouping variable +% and one row for each distinct combination of grouping variable +% values. MULTIGROUP is 1 if there are multiple grouping variables +% or 0 if there are not. + +% Tom Lane, 12-17-99 +% Copyright 1993-2004 The MathWorks, Inc. +% $Revision: 1.4.2.1 $ $Date: 2004/01/24 09:36:20 $ + +multigroup = (iscell(group) & size(group,1)==1); +if (~multigroup) + [ogroup,gname] = grp2idx(group); + glabel = gname; +else + % Group according to each distinct combination of grouping variables + ngrps = size(group,2); + grpmat = zeros(rows,ngrps); + namemat = cell(1,ngrps); + + % Get integer codes and names for each grouping variable + for j=1:ngrps + [g,gn] = grp2idx(group{1,j}); + grpmat(:,j) = g; + namemat{1,j} = gn; + end + + % Find all unique combinations + [urows,ui,uj] = unique(grpmat,'rows'); + + % Create a cell array, one col for each grouping variable value + % and one row for each observation + ogroup = uj; + gname = cell(size(urows)); + for j=1:ngrps + gn = namemat{1,j}; + gname(:,j) = gn(urows(:,j)); + end + + % Create another cell array of multi-line texts to use as labels + glabel = cell(size(gname,1),1); + if (nargin > 2) + nl = sprintf(sep); + else + nl = sprintf('\n'); + end + fmt = sprintf('%%s%s',nl); + lnl = length(fmt)-3; % one less than the length of nl + for j=1:length(glabel) + gn = sprintf(fmt, gname{j,:}); + gn(end-lnl:end) = []; + glabel{j,1} = gn; + end +end diff --git a/LIBRA/adjustedoutlyingness.m b/LIBRA/adjustedoutlyingness.m new file mode 100644 index 0000000..ef781a3 --- /dev/null +++ b/LIBRA/adjustedoutlyingness.m @@ -0,0 +1,225 @@ +function result = adjustedoutlyingness(x,varargin) + +%ADJUSTEDOUTLYINGNESS calculates the 'Skewness-Adjusted Outlyingness'. +% The method searches for outliers in multivariate skewed data +% (thus without assuming elliptical symmetric). +% It is based on the outlyingness measure of Stahel and Donoho (1981, 1982) +% and on the skewness-adjusted boxplot of Hubert and Vandervieren (2008). +% +% The Skewness-Adjusted Outlyingness is described in: +% Hubert, M., and Van der Veeken, S. (2008), +% "Outlier detection for skewed data", +% Journal of Chemometrics, 22, 235-246. +% +% The method can be useful as preprocessing in +% FASTICA (www.cis.hut.fi/projects/ica/fastica/), see +% +% Brys, G., Hubert, M., and Rousseeuw, P.J. (2005), +% "A Robustification of Independent Component Analysis", +% Journal of Chemometrics, 19, 364-375. +% +% +% Required input arguments: +% x : Data matrix (rows=observations, columns=variables) +% +% Optional input arguments: +% ndir : Number of directions in which de outlyingness will be computed +% (default = 250*number of variables) +% classic : If equal to one, the classical Stahel Donoho outlyingness is +% calculated as well (default = 0) +% +% When the number of observations n is sufficiently large compared to the +% dimension p, i.e. when n > 5*p, directions are generated orthogonal to the +% hyperplane through p random observations. The adjusted outlyingness is +% then affine invariant. When n is small compared to p, we take random +% directions through two data points. This procedure is orthogonal +% invariant. +% +% I/O: +% result=adjustedoutlyingness(x,'ndir',250); +% +% Example: +% x = [chi2rnd(2,1000,1) trnd(3,1000,1)]; +% x(1:10,:) = mvnrnd([-2 -3],eye(2)/10,10); +% result = adjustedoutlyingness(x); +% +% The output of ADJUSTEDOUTLYINGNESS is a structure containing: +% result.adjout : skewness-adjusted outlyingness values for all observations +% result.cutoff : cutoff value for the AO-values +% result.flag : The observations whose AO-value exceeds the cutoff value can be +% considered as outliers and receive a flag equal to zero. The regular observations +% receive a flag 1. +% result.classic : If the input argument 'classic' is equal to one, this structure +% contains results of the classical analysis: +% .outl: Stahel-Donoho outlyingness for all observations +% .cutoff: cutoff value for the outlyingness values. +% .flag: The observations whose outlyingness exceeds the cutoff receive flag zero, the others receive flag 1. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust +% +% Written by Guy Brys, Mia Hubert, Stephan Van der Veeken, Tim Verdonck +% Last Update: 09/06/2008 + +if (nargin<1) + error('Input matrix x is required'); +end +[n,p]=size(x); +counter=1; +default=struct('ndir',250*p,'a',-4,'b',3,'classic',0); +list=fieldnames(default); +result=default; +IN=length(list); +i=1; +%reading the user's input +if nargin>1 + % + %placing inputfields in array of strings + % + for j=1:nargin-1 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % + %Checking which default parameters have to be changed + % and keep them in the structure 'result'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) %in case of similarity + for j=1:nargin-1 %searching the index of the accompanying field + if rem(j,2)~=0 %fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + result=setfield(result,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end +end + +ndir=result.ndir; +a=result.a; +b=result.b; +classic=result.classic; +if (ndir<=0) + error('The number of directions should be positive.'); +end + +if isempty(ndir) + ndir=250*p; +end + +% a and b must be numeric scalars, classic should be a binary variable +if isempty(a) + a = -4; +elseif ~isscalar(a) || ~isnumeric(a) + error('The ''a'' parameter value must be a numeric scalar.'); +end + +if isempty(b) + b = 3; +elseif ~isscalar(b) || ~isnumeric(b) + error('The ''b'' parameter value must be a numeric scalar.'); +end + +if isempty(classic) + classic = 0; +elseif ~isscalar(classic) || ~ismember(classic,0:1) + error('Invalid value for ''classic'' parameter.'); +end + +if n>5*p + B=[]; + for i =1:ndir + xx = randperm(n); + P = x(xx(1:p),:); + if (rank(P)==p) + B = [B ; (P\ones(p,1))']; + end + end +else + B=twopoints(x,ndir,0); +end +for i=1:size(B,1) + Bnorm(i)=norm(B(i,:),2); +end +Bnormr=Bnorm(Bnorm > 1.e-12); +B=B(Bnorm > 1.e-12,:); +A=diag(1./Bnormr)*B; + +%Looking in ndir directions for skewness-adjusted outlyingness +Y=x*A'; +[n,p]=size(Y); +YZ=zeros(n,p); + +tmc = mc(Y); +h=find(abs(tmc)==1); +if(sum(h)>1) + error('There are too many ties in the data. The adjusted outlyingness can not be computed.') +end +tme = median(Y); +tiq = iqr(Y); + +s=find(tiq==0); +if(sum(s)>1) + error('There are too many ties in the data. The adjusted outlyingness can not be computed.') +end + +tp1 = prctile(Y,25); +tp3 = prctile(Y,75); +if (tmc>=0) + tup = (tp3+1.5*exp(b*tmc).*tiq)-tme;%3 + tlo = tme-(tp1-1.5*exp(a*tmc).*tiq);%-4 +else + tup = (tp3+1.5*exp(-a*tmc).*tiq)-tme;%4 + tlo = tme-(tp1-1.5*exp(-b*tmc).*tiq);%-3 +end +for k = 1:p + ttup = sort(-Y(Y(:,k)<(tup(k)+tme(k)),k)); + tup(k) = -ttup(1)-tme(k); + ttlo = sort(Y(Y(:,k)>(tme(k)-tlo(k)),k)); + tlo(k) = tme(k)-ttlo(1); +end +tmp = ((Y>=repmat(tme,n,1)).*(repmat(tup,n,1)) + (Y0 + cutoff = prctile(adjout,75)+1.5*exp(b*mcadjout)*iqr(adjout); +else + cutoff = prctile(adjout,75)+1.5*iqr(adjout); +end +ttup=sort(-adjout(adjout1) + error('There are too many ties in the data. The Stahel-Donoho outlyingness can not be computed.') + end + + clYZ=abs((Y-repmat(tme,n,1)))./repmat(umad,n,1); + classout=max(clYZ,[],2); + + %classical chi-square cutoff and flag + quant=chi2inv(0.99,size(x,2)); + classcutoff=sqrt(quant); + classflag=(classout<=classcutoff); + + classic=struct('outl',classout,'cutoff',classcutoff,'flag',classflag); + +end + +%Putting things together +result = struct('adjout',adjout,'cutoff',cutoff,'flag',flag,'classic',classic); \ No newline at end of file diff --git a/LIBRA/adm.m b/LIBRA/adm.m new file mode 100644 index 0000000..0f0f560 --- /dev/null +++ b/LIBRA/adm.m @@ -0,0 +1,43 @@ +function result=adm(x) + +%ADM is a scale estimator given by the Average Distance to the Median. +% It is defined as +% adm(x) = ave(|x_i - med(x)|) +% If x is a matrix, the scale estimate is computed on the columns of x. The +% result is then a row vector. If x is a row or a column vector, +% the output is a scalar. +% +% The ADM is also described in: +% Rousseeuw, P.J. and Verboven, S. (2002), +% "Robust Estimation in Very Small Samples", +% Computational Statistics and Data Analysis, 40, 741-758. +% +% Required input argument: +% x : either a data matrix with n observations in rows, p variables in columns +% or a vector of length n. +% +% I/O: out=adm(x); +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Sabine Verboven +% Last revision 28/08/03 by Nele Smets + +[n,p]=size(x); + +if n==1 & p==1 + result=0; %if X is of size 1x1, the scale estimate is equal to 0 + return +elseif n==1 + x=x'; %if X is row vector, transpose to a column vector + n=p; + p=1; +end + +for i=1:p + X=x(:,i); + result(i)=mean(abs(X-median(X))); +end + diff --git a/LIBRA/agnes.m b/LIBRA/agnes.m new file mode 100644 index 0000000..478fc41 --- /dev/null +++ b/LIBRA/agnes.m @@ -0,0 +1,221 @@ +function result = agnes(x,method,vtype,stdize,metric,plots) + +%AGNES is a agglomerative clustering algorithm. It returns a hierarchy of clusters. +% +%The algorithm is fully described in: +% Kaufman, L. and Rousseeuw, P.J. (1990), +% "Finding groups in data: An introduction to cluster analysis", +% Wiley-Interscience: New York (Series in Applied Probability and +% Statistics), ISBN 0-471-87876-6. +% +% Required input arguments: +% x : Data matrix (rows = observations, columns = variables) +% or Dissimilarity matrix (if number of rows equals 1) +% method : method used +% Possible values are 1 : Group Average +% 2 : Single Linkage +% 3 : Complete Linkage +% 4 : Ward +% 5 : Weighted Average +% vtype : Variable type vector (length equals number of variables) +% Possible values are 1 Asymmetric binary variable (0/1) +% 2 Nominal variable (includes symmetric binary) +% 3 Ordinal variable +% 4 Interval variable +% (if x is a dissimilarity matrix vtype is not required.) +% +% Optional input arguments: +% stdize : standardise the variables given by the x-matrix +% Possible values are 0 : no standardisation (default) +% 1 : standardisation by the mean +% 2 : standardisation by the median +% (if x is a dissimilarity matrix, stdize is ignored) +% metric : Metric to be used +% Possible values are 0 : Mixed (not all interval variables, default) +% 1 : Euclidian (all interval variables, default) +% 2 : Manhattan +% (if x is a dissimilarity matrix, metric is ignored) +% plots : draws figures +% Possible values are 0 : do not create a banner and a cluster tree (default) +% 1 : create a banner and a cluster tree +% +% I/O: +% result=agnes(x,method,vtype,stdize,metric,plots) +% +% Example: +% load agricul.mat +% result=agnes(agricul,1,[4 4],0,0,1); +% +% The output of AGNES is a structure containing: +% result.x : inputmatrix x (only given if x is not a +% dissimilarity matrix) +% result.diss : whether the inputmatrix x is a dissimilarity matrix +% or not +% result.dys : calculated dissimilarities (read row by row from the +% lower dissimilarity matrix, without the elements of +% the diagonal) +% result.metric : metric used +% result.stdize : standardisation used +% result.number : number of observations +% result.method : method used +% result.objectorder : order of objects +% result.heights : dissimilarity between the 2 relative clusters +% (=length of banner) +% result.ac : agglomerative coefficient +% result.merge : a (n-1) by 2 matrix related to the merge +% +% And AGNES will create a banner and a cluster tree if plots equals 1. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Wai Yan Kong +% Created on 05/2006 +% Last Revision: 19/09/2006 + + +%Checking and filling out the inputs +if (nargin<2) + error('Two input arguments required (data or dissimilarity vector and method)') +elseif ((nargin<3) & (size(x,1)~=1)) + error('Three input arguments required (data matrix x, method and vtype)') + % so, only datamatrix x and method as input +elseif (nargin<3) + metri ='unknown'; + metr='unknown'; + stdize = 0; + plots = 0; + % so, only dissim matrix x and method as input +elseif (nargin<4) + stdize = 0; + plots = 0; + if (sum(vtype)~=4*size(x,2)) + metr =0; + metri='mixed'; + else + metr =1; + metri='euclidean'; + end + % so, only datamatrix or dissimilarity matrix x , method and vtype as + % input +elseif (nargin<5) + plots = 0; + if (sum(vtype)~=4*size(x,2)) + metr =0; + metric='mixed'; + else + metr =1; + metric='euclidean'; + end + % so, only datamatrix or dissimilarity matrix x, method, vtype and + % stdize as input +elseif (nargin<6) + plots=0; +elseif (nargin>6) + error('Too many input arguments') +end + +% defining metric (for 4 input arguments) and diss +if (nargin>=5) + if (metric==1) + metr=1; + metric='euclidean'; + elseif (metric==2) + metr=2; + metric='manhattan'; + elseif (metric==0) + metr=0; + metric='mixed'; + else + error('metric must be 0,1 or 2') + end +end + +if ((size(x,1)~=1)) + diss=0; + dissi='x is no dissimilarity matrix'; +else + diss=1; + dissi='x is a dissimilarity matrix'; +end + +%Standardization +if (stdize==1) & (metr==1 | metr==2) & diss==0 + x = ((x - repmat(mean(x),size(x,1),1))./(repmat(std(x),size(x,1),1))); + standardisation='standardisation by mean'; +elseif (stdize==2) & (metr==1|metr==2) & diss==0 + x = ((x - repmat(median(x),size(x,1),1))./(repmat(mad(x),size(x,1),1))); + standardisation='standardisation by median'; +elseif(stdize==0) + standardisation='no standardisation'; +elseif (stdize==1 | stdize==2) + standardisation='no standardisation(not enough num var or X is a dissimilarity matrix)'; +elseif (nargin<=3) + standardisation='no standardisation'; +else + error('stdize must be 0,1 or 2'); +end + +% defining dissimilarity matrix, number and method +if (diss==1) + disv=x; + number=(1+sqrt(1+8*size(x,2)))/2; %number of observations + % checking for missing values in the dissimilarity matrix + if any(isnan(disv)) + error('There are missing value(s) in the dissimilarity matrix!') + end + % checking the dimensions of the dissimilarity matrix + if mod(number,fix(number))~=0 + error(['The dimension of the dissimilarity matrix is not correct!']) + end +else + resl=daisy(x,vtype,metr); + disv=resl.dys; + number=size(x,1); +end + +if (method==1) + namemethod='Group Average'; +elseif (method==2) + namemethod='Single Linkage'; +elseif (method==3) + namemethod='Complete Linkage'; +elseif (method==4) + namemethod='Ward'; +elseif (method==5) + namemethod='Weighted Average'; +else + error('method must be 1,2,3,4, or 5') +end + +%Actual calculations +[ner,ban,coef,merge,dys]=twinsc(number,[0 disv]',method,1); + +% We want ban to be a vector of length n-1 +ban(1)=[]; + +% We want merge to be a (n-1) by 2 matrix +merge2=ones(number-1,2); +for i = 1:(number-1) + merge2(i,:) = merge(2*i-1:2*i); +end + +%Putting things together +result = struct('x',x,'diss',dissi,'dys',dys,'metric',metric,... + 'stdize',standardisation,'number',number,... + 'method',namemethod,'objectorder',ner,'heights',ban,... + 'ac',coef,'merge',merge2,'class','AGNES'); +if diss + result=rmfield(result, 'x'); +end + +% Plots +try + if plots + makeplot(result,'classic',0) + end +catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu +end + diff --git a/LIBRA/agricul.mat b/LIBRA/agricul.mat new file mode 100644 index 0000000000000000000000000000000000000000..5b0ce5ca2575275c848818395f7d123371c887c5 GIT binary patch literal 1032 zcmb7?+e!ja7=^dl5k#>}Q>eC1+QBgJ9kvE^02J?uUX?yK_Zq;-C2%acK; zJ?z}d$A_wHDLn05OXom3hgH|D7F}6%oQnK+h`@OxEQA;Zg_u=1uO1)$m{E_<1M2C4 za|+vcgWkz(|q15jl)H+XiQenbLh6$9mh79BOe5#o?4?~ zn)(uT8|1gxmxu3w8S29DEd5F9GwAE^0{JEMWq1O#Cc5^!=8bKQj>wlZ7nbHp62R#FZ z*q^6A21@e#9fHIb!%|*-KOvr_5J`YBvn`d literal 0 HcmV?d00001 diff --git a/LIBRA/animal.mat b/LIBRA/animal.mat new file mode 100644 index 0000000000000000000000000000000000000000..f571b707d3ccf55487bc5aec0bcfa4107ea0502f GIT binary patch literal 312 zcmb76%L>9U5Zn|LPoDgM{eUWI!3Vk3f>1EhL!sx0DHPHcN)LXPpXhE(KEQ!xW@lzs z7tOu6;_)C+7tOxwolUWJX6dGRP*|63^+>Utty$~~r%_=`e6vrK7tExRXO-ocOL@Y- z0ix1d0Qk_XX*Pk0U={XV@I+uU_JeT%3GHth00DyG6-jS+p91EH*z;vR@G19M(m#@k RfAkAU^PZ5;C*fD@e*p}gA9?@) literal 0 HcmV?d00001 diff --git a/LIBRA/bagplot.m b/LIBRA/bagplot.m new file mode 100644 index 0000000..c0bd9ee --- /dev/null +++ b/LIBRA/bagplot.m @@ -0,0 +1,1959 @@ +function result=bagplot(x,varargin) + +%BAGPLOT draws a bagplot, which is a generalisation of the univariate boxplot +% to bivariate data. The original bagplot is described in +% +% Rousseeuw, P.J., Ruts, I. and Tukey, J.W. (1999), +% "The bagplot: a bivariate boxplot", The American Statistician, 53, 382-387. +% +% The construction of this bagplot is based on the halfspacedepth (see also halfspacedepth.m). +% As the computation of the halfspacedepth is rather time-consuming, it is recommended at large datasets +% to perform the computations on a random subset of the data. The default size of the subset is 200, but this can be +% modified by the user. +% +% The bagplot can also be computed based on the adjusted outlyingness (see also adjustedoutlyingness.m). +% This method is described in: +% +% Hubert, M., and Van der Veeken, S. (2008), +% "Outlier detection for skewed data", Journal of Chemometrics, 22, 235-246. +% +% The bagplot based on the adjusted outlyingness can be obtained by setting the optional input argument 'type' equal to 'ao'. +% +% Required input arguments: +% x : bivariate data matrix (observations in the rows, variables in the +% columns) +% +% Optional input arguments: +% type : To draw the bagplot based on the halfspacedepth, this parameter should be equal to 'hd' (default). +% To draw the bagplot based on the adjusted outlyingness, it should be set to 'ao'. +% sizesubset : When drawing the bagplot based on the halfspacedepth, the size of the subset used to perform +% the main computations. +% plots : If equal to 1, a bagplot is drawn (default). If equal to zero, no plot is made. +% colorbag : The color of the bag. +% colorfence : The color of the fence. +% databag : If this parameter is 1, the data within the bag are +% plotted. If this parameter is set equal to 0, data points +% are not displayed. +% datafence : If this parameter is 1, the data within the fence are +% plotted. If this parameter is set equal to 0, data points +% are not displayed. +% +% I/O: result=bagplot(x,'sizesubset',200,'type','hd','colorbag',[],'colorfence',[],'databag',1,'datafence',1); +% The user should only give the input arguments that have to change their default value. +% The name of the input arguments needs to be followed by their value. +% The order of the input arguments is of no importance. +% +% Examples: +% result=bagplot(x,'colorfence',[0.2 0.2 0.5],'databag',0) +% result=bagplot(x,'datafence',0,'colorbag',[1 0 0]) +% +% +% The output of bagplot is a structure containing +% +% result.center : center of the data. When 'type=hd', this corresponds with the Tukey median. When +% 'type=ao', the point with smallest adjusted outlyingness. +% result.datatype : is 2 for observations in the bag, 1 for the observations in the +% fence and zero for outliers. +% result.flag : is 0 for outliers and equals 1 for regular points +% result.depth : When 'type=hd' this is the halfspacedepth of +% each data point from the subset. The other observations receive +% depth = -1. +% When 'type=ao' this is the adjusted outlyingness (of all data points). +% +% Written by Fabienne Verwerft on 25/05/2005 +% Update and revision by Stephan Van der Veeken 10/12/2007 +% Last revision: 11/02/2008, 25/02/2008, 03/02/2009, 11/05/2009 + +if nargin<1 + error('Input argument is undefined.') +end + +if size(x,1)<10 + error('At least 10 datapoints are needed for the calculations.') +end + +if size(x,2)~=2 + error('The data should be twodimensional.') +end + +S=x; +counter=1; +default=struct('colorbag',[0.6 0.6 1],'colorfence',[0.8 0.8 1],'sizesubset',200,... + 'databag',1,'datafence',1,'plots',1,'type','hd'); +list=fieldnames(default); +result=default; +IN=length(list); +i=1; +%reading the user's input +if nargin>1 + % + %placing inputfields in array of strings + % + for j=1:nargin-1 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % + %Checking which default parameters have to be changed + % and keep them in the structure 'result'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) %in case of similarity + for j=1:nargin-1 %searching the index of the accompanying field + if rem(j,2)~=0 %fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + result=setfield(result,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end +end +colorbag=result.colorbag; +colorfence=result.colorfence; +databag=result.databag; +datafence=result.datafence; +plots=result.plots; +type=result.type; +sizesubset=result.sizesubset; + +switch type + case 'hd' + s1=S(:,1); + s2=S(:,2); + Q=bagp(s1,s2,sizesubset); + bag=Q.interpol; + datatyp=Q.datatyp; + tukm=Q.tukmed; + depth=Q.depth; + case 'ao' + s1=S(:,1); + s2=S(:,2); + n=length(s1); + Q = adjustedoutlyingness(S); + D=[S,Q.adjout,Q.flag,(1:n)']; + P=sortrows(D,3); + L=[P(:,1),P(:,2)]; + n=size(P,1); + f=floor(n/2); + g=sum(Q.flag); + h=n-g; + bag=L((1:f),:); + hulp=[ones(f,1);2*ones(g-f,1);3*ones(h,1)]; + d1=[P,hulp]; + d2=sortrows(d1,5); + datatyp=[d2(:,1),d2(:,2),d2(:,6)]; + tukm=L(1,:); + depth=Q.adjout; +end + +i=find(datatyp(:,3)==1); +data1=datatyp(i,1:2); +i=find(datatyp(:,3)==2); +data2=datatyp(i,1:2); +data=[data1;data2]; +i=find(datatyp(:,3)>=3); +outl=datatyp(i,1:2); +plak=[data;bag]; +k=convhull(plak(:,1),plak(:,2)); +whisk=plak(k,1:2); +if plots==1 + fill(whisk(:,1),whisk(:,2),colorfence,'LineStyle','none') + hold on %This is essential in order to prevent that only the last executed plotting command is executed + q=convhull(bag(:,1),bag(:,2)); + bagq=bag(q,1:2); + fill(bagq(:,1),bagq(:,2),colorbag)%This line should be placed after the command plot(data...) to only %plot %the data outside the bag + axis square + if databag==1 + plot(data1(:,1),data1(:,2),'o','MarkerFaceColor','k','MarkerEdgeColor','k','Markersize',4) + end + if datafence==1 + plot(data2(:,1),data2(:,2),'o','MarkerFaceColor','k','MarkerEdgeColor','k','Markersize',4) + end + plot(outl(:,1),outl(:,2),'hk','MarkerFaceColor','k','Markersize',8) + plot(tukm(1),tukm(2),'o','MarkerFaceColor','w','MarkerEdgeColor','w','MarkerSize',10); + plot(tukm(1),tukm(2),'+k','Markersize',8) + switch type + case 'ao' + title('bagplot based on adjusted outlyingness') + set(gcf,'Name', 'Bagplot based on adjusted outlyingness', 'NumberTitle', 'off'); + case 'hd' + title('bagplot based on halfspacedepth') + set(gcf,'Name', 'Bagplot based on halfspacedepth', 'NumberTitle', 'off'); + end + xmin=min(datatyp(:,1)); + xmax=max(datatyp(:,1)); + small=0.05*(xmax-xmin); + xaxis1=xmin-small; + xaxis2=xmax+small; + ymin=min(datatyp(:,2)); + ymax=max(datatyp(:,2)); + small=0.05*(ymax-ymin); + xaxis3=ymin-small; + xaxis4=ymax+small; + axis([xaxis1 xaxis2 xaxis3 xaxis4]); + box on; + hold off +end +Datatyp=datatyp(:,3); +flag=zeros(size(S,1),1); +for i=1:(size(S,1)) + if Datatyp(i)==3 + flag(i)=0; + else + flag(i)=1; + end +end +datatype=zeros(size(S,1),1); +for i=1:(size(S,1)) + if Datatyp(i)==3 + datatype(i)=0; + else if Datatyp(i)==2 + datatype(i)=1; + else + datatype(i)=2; + end + end +end + +result=struct('center',tukm,'datatype',datatype,'flag',flag,'depth',depth); + +%----------- +function [kount,ADK,empty]=isodepth(x,y,d,varargin) + +% ISODEPTH is an algoritm that computes the depth region of a bivariate dataset +% corresponding to depth d. +% First, we have to check whether the data points are in general position. If not, +% a very small random number is added to each of the data points until the +% dataset comes in general position. All this is done in the m-file dithering. +% Then all special k-dividers must be found. The coordinates of the vertices +% of the depth region we are looking for are intersection points of these +% special k-dividers. So, consequently, every intersection point in turn has +% to be tested, for example by computing its depth (see halfspacedepth.m), +% to check whether it is a vertex of the depth region. +% +% The ISODEPTH algoritm is described in: +% Ruts, I., Rousseeuw, P.J. (1996), +% "Computing depth contours of bivariate point clouds", +% Computational Statistics and Data Analysis, 23, 153-168. +% +% Required input arguments: +% x : vector containing the first coordinates of all the data +% points +% y : vector containing the second coordinates of all the data +% points +% d : the depth of which the depth region has to be constructed +% +% +% I/O: [kount, ADK, empty]= isodepth(x,y,d); +% +% The output of isodepth is given by +% +% kount : the total number of vertices of the depth region +% ADK : the coordinates of the vertices of the depth region +% empty : logical value (1 if the depth region is empty, 0 if not) +% +% This function is part of the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Last Update: 29/04/2005 + +n=length(x); +eps=0.0000001; +% +% Checking input +% +if length(x)==1 + error('x is not a vector') +elseif not(length(x)==length(y)) + error('The vectors x and y must have the same length.') +end +if sum(isnan(x))>=1 || sum(isnan(y))>=1 + error('Missing values are not allowed') +end +if sum(x==x(1))==n + error('All data points ly on a vertical line.') +elseif sum(y==y(1))==n + error('All data points ly on a horizontal line.') +else + R=corrcoef(x,y); + if abs(R(1,2))==1 + error('All data points are collineair.') + end +end +% +% Check whether the data is in general position. If not, add a very small random +% number to each of the data points. +% +[x,y, Index, angl, ind1,ind2]=dithering(x,y); + +% +% Main part +% +if (n==1)&&(d==1) + kount=n; + ADK=[x,y]; + empty=0; + return +end +% +if (d>floor(n/2)) + kount=0; + ADK=0; + empty=1; + return +end +% +if n<=3 + kount=n; + ADK=[x,y]; + empty=1; + return +end +% +nrank(Index)=(1:n); +% +% Let the line rotate from zero to angle(1) +% +ncirq=Index; +kount=1; +halt=0; +M=length(angl); +if angl(1)>(pi/2) + L=1; + D1=ind1(L); + IV1=nrank(D1); + D2=ind2(L); + IV2=nrank(D2); + IV=ncirq(IV1); + ncirq(IV1)=ncirq(IV2); + ncirq(IV2)=IV; + IV=IV1; + nrank(D1)=IV2; + nrank(D2)=IV; + % + if ((IV1==d) && (IV2==(d+1)))||((IV2==d) && (IV1==(d+1)))||((IV1==(n-d)) && (IV2==(n-d+1)))||((IV2==(n-d)) && (IV1==(n-d+1))) + if angl(L)<(pi/2) + dum=angl(L)+(pi/2); + else + dum=angl(L)-(pi/2); + end + if (IV1==d && IV2==(d+1))||(IV2==d && IV1==(d+1)) + if dum<=(pi/2) + alfa(kount)=angl(L)+pi; + else + alfa(kount)=angl(L); + end + end + if or((IV1==(n-d) && IV2==(n-d+1)),(IV2==(n-d) && IV1==(n-d+1))) + if dum<=(pi/2) + alfa(kount)=angl(L); + else + alfa(kount)=angl(L)+pi; + end + end + kand1(kount)=ind1(L); + kand2(kount)=ind2(L); + D(kount)=sin(alfa(kount))*x(ind1(L))-cos(alfa(kount))*y(ind1(L)); + kount=kount+1; + end + halt=1; +end +% +L=2; +stay=1; +% jflag keeps track of which angle we have to test next +while stay==1 + stay=0; + kontrol=0; + if (pi<=(angl(L)+(pi/2))) && ((angl(L)-(pi/2))< angl(1)) + D1=ind1(L); + IV1=nrank(D1); + D2=ind2(L); + IV2=nrank(D2); + IV=ncirq(IV1); + ncirq(IV1)=ncirq(IV2); + ncirq(IV2)=IV; + IV=IV1; + nrank(D1)=IV2; + nrank(D2)=IV; + % + if ((IV1==d) && (IV2==(d+1)))||((IV2==d) && (IV1==(d+1)))||((IV1==(n-d)) && (IV2==(n-d+1)))||((IV2==(n-d)) && (IV1==(n-d+1))) + if angl(L)<(pi/2) + dum=angl(L)+(pi/2); + else + dum=angl(L)-(pi/2); + end + if (IV1==d && IV2==(d+1))||(IV2==d && IV1==(d+1)) + if dum<=(pi/2) + alfa(kount)=angl(L)+pi; + else + alfa(kount)=angl(L); + end + end + if or((IV1==(n-d) && IV2==(n-d+1)),(IV2==(n-d) && IV1==(n-d+1))) + if dum<=(pi/2) + alfa(kount)=angl(L); + else + alfa(kount)=angl(L)+pi; + end + end + kand1(kount)=ind1(L); + kand2(kount)=ind2(L); + D(kount)=sin(alfa(kount))*x(ind1(L))-cos(alfa(kount))*y(ind1(L)); + kount=kount+1; + end + kontrol=1; + end + L=L+1; + if kontrol==1 + halt=1; + end + if (L==(M+1)) && (kontrol==1) + jflag=1; + stay=2; + end + if not(stay==2) + if ((halt==1)&&(kontrol==0))||(L==(M+1)) + stay=3; + else + stay=1; + end + end +end +if not(stay==2) + if (L>1) + jflag=L-1; + else + jflag=M; + end +end +% +halt2=0; +if not(stay==2) + J=0; + % + % If the first switch didnt occur between 0 and the angle angl(1) look for it + % between the following angles. + % + stay2=1; + if (L==M+1) && (kontrol==0) + halt=0; + halt2=0; + J=J+1; + if J==(M+1) + J=1; + end + L=J+1; + if L==(M+1) + L=1; + end + while stay2==1 + stay2=0; + kontrol=0; + if (angl(L)+pi/2)pi + angl(1)=angl(1)-pi; + end + D1=ind1(L); + IV1=nrank(D1); + D2=ind2(L); + IV2=nrank(D2); + IV=ncirq(IV1); + ncirq(IV1)=ncirq(IV2); + ncirq(IV2)=IV; + IV=IV1; + nrank(D1)=IV2; + nrank(D2)=IV; + % + if ((IV1==d) && (IV2==(d+1)))||((IV2==d) && (IV1==(d+1)))||((IV1==(n-d)) && (IV2==(n-d+1)))||((IV2==(n-d)) && (IV1==(n-d+1))) + if angl(L)<(pi/2) + dum=angl(L)+(pi/2); + else + dum=angl(L)-(pi/2); + end + if (IV1==d && IV2==(d+1))||(IV2==d && IV1==(d+1)) + if dum<=(pi/2) + alfa(kount)=angl(L)+pi; + else + alfa(kount)=angl(L); + end + end + if or((IV1==(n-d) && IV2==(n-d+1)),(IV2==(n-d) && IV1==(n-d+1))) + if dum<=(pi/2) + alfa(kount)=angl(L); + else + alfa(kount)=angl(L)+pi; + end + end + kand1(kount)=ind1(L); + kand2(kount)=ind2(L); + D(kount)=sin(alfa(kount))*x(ind1(L))-cos(alfa(kount))*y(ind1(L)); + kount=kount+1; + end + kontrol=1; + end + if angl(1)>pi + angl(1)=angl(1)-pi; + end + if L==M + L=1; + else + L=L+1; + end + if kontrol==1 + halt=1; + end + if (halt==1)&&(kontrol==0) + if halt2==1 + stay2=2; + end + if not(stay2==2) + if L>1 + jflag=L-1; + else + jflag=M; + end + stay2=0; + end + else + if L==jj + if jj==1 + halt2=1; + end + J=J+1; + if J==(M+1) + J=1; + end + L=J+1; + if L==(M+1) + L=1; + end + stay2=1; + else + stay2=1; + end + end + end + end +end +% +if not(stay2==2) + % + % The first switch has occurred. Now start looking for the next ones, + % between the following angles. + % + for i=(J+1):(M-1) + L=jflag; + stay=1; + while stay==1 + stay=0; + kontrol=0; + if ((angl(L)+pi/2)=d)>=1) + NDK=1; + end + if (hdep1IW1)&&(Jfull==0) + kornr(IW1:(IW2-1),1)=kand1(IW1); + kornr(IW1:(IW2-1),2)=kand2(IW1); + kornr(IW1:(IW2-1),3)=kand1(IW2); + kornr(IW1:(IW2-1),4)=kand2(IW2); + else + if IW2>IW1 + i=IW1; + stay3=1; + while stay3==1 + if (kornr(i,1)==kand1(IW1))&&(kornr(i,2)==kand2(IW1))&&(kornr(i,3)==kand1(IW2))&&(kornr(i,4)==kand2(IW2)) + stay3=0; + else + m1=(y(kornr(i,2))-y(kornr(i,1)))/(x(kornr(i,2))-x(kornr(i,1))); + m2=(y(kornr(i,4))-y(kornr(i,3)))/(x(kornr(i,4))-x(kornr(i,3))); + if not(m1==m2) + xcord1=(m1*x(kornr(i,1))-y(kornr(i,1))-m2*x(kornr(i,3))-y(kornr(i,3)))/(m1-m2); + ycord1=(m2*(m1*x(kornr(i,1))-y(kornr(i,1)))-m1*(m2*x(kornr(i,3))-y(kornr(i,3))))/(m1-m2); + end + if (abs(xcord1-xcord)0)&&(stay4>0)&¬(stay2==1) + % + % The intersection point is not the correct one, try the next + % special k-divider. + % + IW2=IW2+1; + if IW2==(num+1) + IW2=1; + end + stay2=1; + end + % + % Look for the next vertex of the convex figure. + % + if (stay3>0)&&(stay4>0)&¬(stay2==1) + IW1=IW2; + IW2=IW2+1; + if IW2==(num+1) + IW2=1; + end + stay2=1; + end +end +% +% Scan kornr and ascribe the coordinates of the vertices to the variable +% ADK. +% +kount=0; +% +if NDK==0 + % + % The requested depth region is empty + % + ADK=0; + empty=1; + return +else + empty=0; +end +% +i=1; +E1=y(kornr(i,2))-y(kornr(i,1)); +F1=x(kornr(i,1))-x(kornr(i,2)); +G1=x(kornr(i,1))*(y(kornr(i,2))-y(kornr(i,1)))-y(kornr(i,1))*(x(kornr(i,2))-x(kornr(i,1))); +E2=y(kornr(i,4))-y(kornr(i,3)); +F2=x(kornr(i,3))-x(kornr(i,4)); +G2=x(kornr(i,3))*(y(kornr(i,4))-y(kornr(i,3)))-y(kornr(i,3))*(x(kornr(i,4))-x(kornr(i,3))); +xcord(i)=(-F2*G1+F1*G2)/(E2*F1-E1*F2); +ycord(i)=(-E2*G1+E1*G2)/(E1*F2-E2*F1); +DK(i,:)=[xcord(i),ycord(i)]; +Juisteind(i)=i; +xcord1=xcord(i); +ycord1=ycord(i); +xcordp=xcord(i); +ycordp=ycord(i); +kount=kount+1; +i=i+1; +% +while not(i==num+1) + if (kornr(i,1)==kornr(i-1,1))&&(kornr(i,2)==kornr(i-1,2))&&(kornr(i,3)==kornr(i-1,3))&&(kornr(i,4)==kornr(i-1,4)) + i=i+1; + else + if (kornr(i,1)==kornr(1,1))&&(kornr(i,2)==kornr(1,2))&&(kornr(i,3)==kornr(1,3))&&(kornr(i,4)==kornr(1,4)) + pp=find(not(Juisteind==0)); + ADK=DK(pp,:); + empty=0; + return + else + E1=y(kornr(i,2))-y(kornr(i,1)); + F1=x(kornr(i,1))-x(kornr(i,2)); + G1=x(kornr(i,1))*(y(kornr(i,2))-y(kornr(i,1)))-y(kornr(i,1))*(x(kornr(i,2))-x(kornr(i,1))); + E2=y(kornr(i,4))-y(kornr(i,3)); + F2=x(kornr(i,3))-x(kornr(i,4)); + G2=x(kornr(i,3))*(y(kornr(i,4))-y(kornr(i,3)))-y(kornr(i,3))*(x(kornr(i,4))-x(kornr(i,3))); + xcord(i)=(-F2*G1+F1*G2)/(E2*F1-E1*F2); + ycord(i)=(-E2*G1+E1*G2)/(E1*F2-E2*F1); + if ((abs(xcord(i)-xcordp)=1 || sum(isnan(y))>=1 + error('Missing values are not allowed') +end +if sum(x==x(1))==n + error('All data points ly on a vertical line.') +elseif sum(y==y(1))==n + error('All data points ly on a horizontal line.') +else + R=corrcoef(x,y); + if abs(R(1,2))==1 + error('All data points are collineair.') + end +end +% +% Looking for optional argument +% +if nargin>2 + if strcmp(varargin{1},'kstar') + kstar=varargin{2}; + if length(kstar)>1 + error('kstar must be a number, not a vector.') + end + else + error('Only kstar can be provided as an optional argument') + end +else + kstar=0; +end +if kstar>=floor(n/2) + error('kstar must be smaller than floor(n/2) because the depth region corresponding with kstar is empty') +end +% +% Check whether the data are in general position. If not, add a very small random +% number to each of the data points. +% +[x,y, Index, angl, ind1,ind2]=dithering(x,y); +% +%Calculation of the Tukey median +% +if n<=3 + xsum=sum(x); + ysum=sum(y); + tukmed=[xsum/n,ysum/n]; + return +end +% +if kstar==0 + ib=ceil(n/3); +else + ib=kstar; +end +ie=floor(n/2); +stay=1; +while stay==1 + le=ie-ib; + if le<0 + le=0; + end + if le==0 + stay=0; + end + if stay==1 + [kou,dk,empty]=isodepth(x,y,ib+ceil(le/2)); + if empty==1 + ie=ib+ceil(le/2); + end + if empty==0 + ib=ib+ceil(le/2); + end + if le==1 + stay=0; + end + end +end +[kount,DK,empty]=isodepth(x,y,ib); +xsum=sum(DK(:,1)); +if not(DK==0) + ysum=sum(DK(:,2)); +else + ysum=0; +end +wx=DK(:,1); +if not(DK==0) + wy=DK(:,2); +else + wy=0; +end +% +% The maximal depth is now ib. +% +% +% Calculation of the center of gravity +% +som=0; +tukmed=0; +if kount>1 + wx=wx-(xsum/kount); + wy=wy-(ysum/kount); + for i=1:(kount-1) + som=som+abs(wx(i)*wy(i+1)-wx(i+1)*wy(i)); + tukmed=tukmed+[(wx(i)+wx(i+1))*abs(wx(i)*wy(i+1)-wx(i+1)*wy(i)),(wy(i)+wy(i+1))*abs(wx(i)*wy(i+1)-wx(i+1)*wy(i))]; + end + som=som+abs(wx(kount)*wy(1)-wx(1)*wy(kount)); + tukmed=tukmed+[(wx(kount)+wx(1))*abs(wx(kount)*wy(1)-wx(1)*wy(kount)),(wy(kount)+wy(1))*abs(wx(kount)*wy(1)-wx(1)*wy(kount))]; + tukmed=(tukmed/(3*som))+[(xsum/kount),(ysum/kount)]; +else + tukmed=[xsum,ysum]; +end + +%-------------- + +function result=bagp(x,y,sizesubset) + +% BAGP is an algoritm that makes the necessary computations to construct +% the bagplot of a bivariate dataset. This function is especially used in +% bagplot.m. First, the data points are standardized. If the dataset is +% too large (n > 200), then a subset is taken. On the other hand, if the +% dataset is too small (n < 10), then no bag can be constructed. The Tukey +% median (see halfmed.m) is also calculated, so that we can center the data. +% Now, the bag lies between two consecutive depth regions. Therefore, one must +% search for the right depth k. Once the k-th and the (k-1)-th depth regions +% are constructed (see isodepth), the bag is the interpolation between the +% vertices of the 2 depth regions. Finally, all the data points are characterized +% by their position compared to the bag with an integer 0,1,2 or 3. +% +% Required input arguments: +% x : vector containing the first coordinates of all the data +% points +% y : vector containing the second coordinates of all the data +% points +% +% I/O: result = bagp(x,y); +% +% The output of bagp is a structure containing +% +% result.tukmed : Tukey median +% result.datatyp : is 2 for observations in the bag, 1 for +% observations in the fence and 0 for outliers +% result.interpol : The coordinates of the bag. +% result.depth : The halfspacedepth of each observation +% +% This function is part of the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Last Update: 29/04/2005 + +n=length(x); +eps=0.0000001; +nointer=0; +ntot=n; +% +% Checking input +% +if length(x)==1 + error('x is not a vector') +elseif not(length(x)==length(y)) + error('The vectors x and y must have the same length.') +end +if sum(isnan(x))>=1 || sum(isnan(y))>=1 + error('Missing values are not allowed') +end +if sum(x==x(1))==n + error('All data points ly on a vertical line.') +elseif sum(y==y(1))==n + error('All data points ly on a horizontal line.') +else + R=corrcoef(x,y); + if abs(R(1,2))==1 + error('All data points are collineair.') + end +end +% +% Standardization of the data set +% +xmean=mean(x); +ymean=mean(y); +xdev=sqrt(var(x)); +ydev=sqrt(var(y)); +if xdev>eps + x=(x-xmean)/xdev; +end +if ydev>eps + y=(y-ymean)/ydev; +end +xoris=x; +yoris=y; +xori=x; +yori=y; +wx=x; +wy=y; +wx1=x; +wy1=y; +% +% If n is large, take a subset +% +nsub=sizesubset; + +if n>nsub + ntot=n; + n=nsub; + rand('state',0); + b=randperm(ntot); + a=b(1:n); + x=wx1(a); + y=wy1(a); + wx=x; + wy=y; +else + a=1:n; +end +% +% Check whether the data is in general position. If not, add a very small random +% number to each of the data points. +% +[x,y, Index, angl, ind1,ind2]=dithering(x,y); +% +depth=zeros(1,ntot)-1; +numdep(1:n)=0; +kstar=0; +for i=1:n + hdep=halfspacedepth(x(i),y(i),x,y); + depth(a(i))=hdep; + if hdep>kstar + kstar=hdep; + end + numdep(hdep)=numdep(hdep)+1; +end +% +% Calculation of the Tukey median +% +tukmed=halfmed(x,y,'kstar',kstar); +% +% Calculation of correct value of k +% +nc=floor(n/2); +j=kstar+1; +stay8=1; +while stay8==1 + stay8=0; + j=j-1; + if numdep(kstar)<=nc + numdep(kstar)=numdep(kstar)+numdep(j-1); + stay8=1; + else + k=j+1; + numk1=numdep(kstar); + numk=numk1-numdep(k-1); + lambdanc=(nc-numk)/(numk1-numk); + end +end +% +% Calculation of the vertices of Dk +% +[kount1,Dk1,empty1]=isodepth(x,y,k); +wx1=Dk1(:,1); +if Dk1==0 + wy1=0; +else + wy1=Dk1(:,2); +end +% +% Calculation of the vertices of Dk-1 +% +[kount2,Dk2,empty2]=isodepth(x,y,k-1); +wx2=Dk2(:,1); +if Dk2==0 + wy2=0; +else + wy2=Dk2(:,2); +end +if corrcoef(wx2,wy2)>0.98 + error('Dk-1 is a line segment. Too many data points coincide.') +end +% +if n>=10 + wx1=wx1-tukmed(1); + wy1=wy1-tukmed(2); + wx2=wx2-tukmed(1); + wy2=wy2-tukmed(2); + x(1:n)=x(1:n)-tukmed(1); + y(1:n)=y(1:n)-tukmed(2); + xori(1:ntot)=xori(1:ntot)-tukmed(1); + yori(1:ntot)=yori(1:ntot)-tukmed(2); + % + % Compute the angles of the data points. + % + numdattm=0; + in=1:n; + for i=1:n + if (abs(x(i))abs(ycord) + if xcord>=0 + angz(i)=asin(ycord); + if angz(i)<0 + angz(i)=angz(i)+pi*2; + end + else + angz(i)=pi-asin(ycord); + end + else + if ycord>=0 + angz(i)=acos(xcord); + else + angz(i)=pi*2-acos(xcord); + end + end + if angz(i)>=(2*pi-eps) + angz(i)=0; + end + end + end + % + % numdattm is the number of datapoints that are equal to the Tukey median + % + [angz,optel]=sort(angz); + in=in(optel); + n=n-numdattm; + % + % Compute the angles of the vertices of the depth region Dk. + % + if not(kount1==1) + ind=1:kount1; + for i=1:kount1 + dist=sqrt(wx1(i)^2+wy1(i)^2); + if dist>eps + xcord=wx1(i)/dist; + ycord=wy1(i)/dist; + if abs(xcord)>abs(ycord) + if xcord>=0 + angy1(i)=asin(ycord); + if angy1(i)<0 + angy1(i)=angy1(i)+pi*2; + end + else + angy1(i)=pi-asin(ycord); + end + else + if ycord>=0 + angy1(i)=acos(xcord); + else + angy1(i)=pi*2-acos(xcord); + end + end + if angy1(i)>=(pi*2-eps) + angy1(i)=0; + end + else + nointer=1; + end + end + [angy1,optel]=sort(angy1); + ind=ind(optel); + end + % + % Compute the angles of the vertices of the depth region Dk-1. + % + jnd=1:kount2; + for i=1:kount2 + dist=sqrt(wx2(i)^2+wy2(i)^2); + if dist>eps + xcord=wx2(i)/dist; + ycord=wy2(i)/dist; + if abs(xcord)>abs(ycord) + if xcord>=0 + angy2(i)=asin(ycord); + if angy2(i)<0 + angy2(i)=angy2(i)+pi*2; + end + else + angy2(i)=pi-asin(ycord); + end + else + if ycord>=0 + angy2(i)=acos(xcord); + else + angy2(i)=pi*2-acos(xcord); + end + end + if angy2(i)>=(pi*2-eps) + angy2(i)=0; + end + else + nointer=1; + end + end + [angy2,optel]=sort(angy2); + jnd=jnd(optel); + % + % Calculation of arrays px and py for Dk + % + if not(kount1==1) + jk=0; + wx1(kount1+1)=wx1(1); + wy1(kount1+1)=wy1(1); + angy1(kount1+1)=angy1(1); + ind(kount1+1)=ind(1); + if angz(1)=(angy1(1)-eps) + j=1; + end + for i=1:n + while (angz(i)>=(angy1(j+1)-eps))&&(jk==0) + j=j+1; + if j==kount1 + jk=1; + end + if j==(kount1+1) + j=1; + end + end + if (abs(wx1(ind(j+1))-wx1(ind(j)))>eps)&&(abs(x(in(i)))>eps) + dist=y(in(i))/x(in(i))-(wy1(ind(j+1))-wy1(ind(j)))/(wx1(ind(j+1))-wx1(ind(j))); + px(i,1)=(wy1(ind(j))*wx1(ind(j+1))-wx1(ind(j))*wy1(ind(j+1)))/(wx1(ind(j+1))-wx1(ind(j))); + px(i,1)=px(i,1)/dist; + py(i,1)=px(i,1)*y(in(i))/x(in(i)); + else + if abs(wx1(ind(j+1))-wx1(ind(j)))<=eps + px(i,1)=wx1(ind(j)); + py(i,1)=px(i,1)*y(in(i))/x(in(i)); + else + px(i,1)=0; + py(i,1)=((wy1(ind(j))-wy1(ind(j+1)))*wx1(ind(j))/(wx1(ind(j+1))-wx1(ind(j))))+wy1(ind(j)); + end + end + end + end + % + % Calculation of arrays px and py for Dk-1 + % + jk=0; + wx2(kount2+1)=wx2(1); + wy2(kount2+1)=wy2(1); + angy2(kount2+1)=angy2(1); + jnd(kount2+1)=jnd(1); + if angz(1)=(angy2(1)-eps) + j=1; + end + for i=1:n + while (angz(i)>=(angy2(j+1)-eps))&&(jk==0) + j=j+1; + if j==kount2 + jk=1; + end + if j==(kount2+1) + j=1; + end + end + if (abs(wx2(jnd(j+1))-wx2(jnd(j)))>eps)&&(abs(x(in(i)))>eps) + dist=(y(in(i))/x(in(i)))-((wy2(jnd(j+1))-wy2(jnd(j)))/(wx2(jnd(j+1))-wx2(jnd(j)))); + px(i,2)=(wy2(jnd(j))*wx2(jnd(j+1))-wx2(jnd(j))*wy2(jnd(j+1)))/(wx2(jnd(j+1))-wx2(jnd(j))); + px(i,2)=px(i,2)/dist; + py(i,2)=px(i,2)*y(in(i))/x(in(i)); + else + if abs(wx2(jnd(j+1))-wx2(jnd(j)))<=eps + px(i,2)=wx2(jnd(j)); + py(i,2)=px(i,2)*y(in(i))/x(in(i)); + else + px(i,2)=0; + py(i,2)=((wy2(jnd(j))-wy2(jnd(j+1)))*wx2(jnd(j))/(wx2(jnd(j+1))-wx2(jnd(j))))+wy2(jnd(j)); + end + end + end + if kount1==1 + px(:,1)=wx1(1); + py(:,1)=wy1(1); + end + % + % Mergesort of angy1 and angy2 to obtain gamma and calculation of arrays px + % and py + % + if kount1==1 + gamma=angy2; + px(:,3)=wx1; + py(:,3)=wy1; + px(:,4)=wx2; + py(:,4)=wy2; + else + ja=1; + jb=1; + i=1; + stay9=1; + while stay9==1 + if angy1(ja)<=angy2(jb) + if ja<=kount1 + gamma(i)=angy1(ja); + px(i,3)=wx1(ind(ja)); + py(i,3)=wy1(ind(ja)); + if jb==1 + wxjb1=wx2(jnd(kount2)); + wyjb1=wy2(jnd(kount2)); + else + wxjb1=wx2(jnd(jb-1)); + wyjb1=wy2(jnd(jb-1)); + end + if (abs(wx2(jnd(jb))-wxjb1)>eps)&&(abs(wx1(ind(ja)))>eps) + dist=(wy1(ind(ja))/wx1(ind(ja)))-((wy2(jnd(jb))-wyjb1)/(wx2(jnd(jb))-wxjb1)); + px(i,4)=(wyjb1*wx2(jnd(jb))-wxjb1*wy2(jnd(jb)))/(wx2(jnd(jb))-wxjb1); + px(i,4)=px(i,4)/dist; + py(i,4)=px(i,4)*wy1(ind(ja))/wx1(ind(ja)); + else + if abs(wx2(jnd(jb))-wxjb1)<=eps + px(i,4)=wxjb1; + py(i,4)=px(i,4)*wy1(ind(ja))/wx1(ind(ja)); + else + px(i,4)=0; + py(i,4)=((wyjb1-wy2(jnd(jb)))*wxjb1/(wx2(jnd(jb))-wxjb1))+wyjb1; + end + end + ja=ja+1; + i=i+1; + else + angy1(ja)=angy1(ja)+10; + end + else + if jb<=kount2 + gamma(i)=angy2(jb); + px(i,4)=wx2(jnd(jb)); + py(i,4)=wy2(jnd(jb)); + if ja==1 + wxja1=wx1(ind(kount1)); + wyja1=wy1(ind(kount1)); + else + wxja1=wx1(ind(ja-1)); + wyja1=wy1(ind(ja-1)); + end + if (abs(wx1(ind(ja))-wxja1)>eps)&&(abs(wx2(jnd(jb)))>eps) + dist=(wy2(jnd(jb))/wx2(jnd(jb)))-((wy1(ind(ja))-wyja1)/(wx1(ind(ja))-wxja1)); + px(i,3)=(wyja1*wx1(ind(ja))-wxja1*wy1(ind(ja)))/(wx1(ind(ja))-wxja1); + px(i,3)=px(i,3)/dist; + py(i,3)=px(i,3)*wy2(jnd(jb))/wx2(jnd(jb)); + else + if abs(wx1(ind(ja))-wxja1)<=eps + px(i,3)=wxja1; + py(i,3)=px(i,3)*wy2(jnd(jb))/wx2(jnd(jb)); + else + px(i,3)=0; + py(i,3)=((wyja1-wy1(ind(ja)))*wxja1/(wx1(ind(ja))-wxja1))+wyja1; + end + end + jb=jb+1; + i=i+1; + else + angy2(jb)=angy2(jb)+10; + end + end + if i<=(kount1+kount2) + stay9=1; + else + stay9=0; + end + end + end + % + % Interpolation of two arrays px and py + % + c=3; + if nointer==1 + kount1=0; + end + if nointer==0 + wx2(1:(kount1+kount2))=lambdanc*px(1:(kount1+kount2),4)+(1-lambdanc)*px(1:(kount1+kount2),3); + wy2(1:(kount1+kount2))=lambdanc*py(1:(kount1+kount2),4)+(1-lambdanc)*py(1:(kount1+kount2),3); + end + if xdev>eps + xcord=(wx2(1:(kount1+kount2))+tukmed(1))*xdev+xmean; + else + xcord=(wx2(1:(kount1+kount2))+tukmed(1)); + end + if ydev>eps + ycord=(wy2(1:(kount1+kount2))+tukmed(2))*ydev+ymean; + else + ycord=wy2(1:(kount1+kount2))+tukmed(2); + end + interpol(1:(kount1+kount2),1)=xcord; + interpol(1:(kount1+kount2),2)=ycord; + % + if nointer==1 + if xdev>eps + xcord=(x(in(1:n))+tukmed(1))*xdev+xmean; + else + xcord=x(in(1:n))+tukmed(1); + end + if ydev>eps + ycord=(y(in(1:n))+tukmed(2))*ydev+ymean; + else + ycord=y(in(1:n))+tukmed(2); + end + datatyp(1:n,1)=xcord; + datatyp(1:n,2)=ycord; + datatyp(1:n,3)=1; + if xdev>eps + xcord=(x(dattm(1:numdattm))+tukmed(1))*xdev+xmean; + else + xcord=x(dattm(1:numdattm))+tukmed(1); + end + if ydev>eps + ycord=(y(dattm(1:numdattm))+tukmed(2))*ydev+ymean; + else + ycord=y(dattm(1:numdattm))+tukmed(2); + end + datatyp((n+1):(n+numdattm),1)=xcord; + datatyp((n+1):(n+numdattm),2)=ycord; + datatyp((n+1):(n+numdattm),3)=0; + return + end + % + % Repeat some calculations for the whole data set + % + if ntot>nsub + n=ntot; + x=xoris; + y=yoris; + % + % Angles of the data points + % + x=x-tukmed(1); + y=y-tukmed(2); + numdattm=0; + in=1:n; + for i=1:n + if (abs(x(i))abs(ycord) + if xcord>=0 + angz(i)=asin(ycord); + if angz(i)<0 + angz(i)=angz(i)+pi*2; + end + else + angz(i)=pi-asin(ycord); + end + else + if ycord>=0 + angz(i)=acos(xcord); + else + angz(i)=pi*2-acos(xcord); + end + end + if angz(i)>=(pi*2-eps) + angz(i)=0; + end + end + end + [angz,optel]=sort(angz); + in=in(optel); + n=n-numdattm; + % + % Calculation of arrays px and py for B + % + jk=0; + wx2(kount2+kount1+1)=wx2(1); + wy2(kount2+kount1+1)=wy2(1); + gamma(kount2+kount1+1)=gamma(1); + if angz(1)=(gamma(1)-eps) + j=1; + end + for i=1:n + while (angz(i)>=gamma(j+1)-eps)&&(jk==0) + j=j+1; + if j==(kount2+kount1) + jk=1; + end + if j==(kount2+kount1+1) + j=1; + end + end + if (abs(wx2(j+1)-wx2(j))>eps)&&(abs(x(in(i)))>eps) + dist=(y(in(i))/x(in(i)))-((wy2(j+1)-wy2(j))/(wx2(j+1)-wx2(j))); + px(i,1)=(wy2(j)*wx2(j+1)-wx2(j)*wy2(j+1))/(wx2(j+1)-wx2(j)); + px(i,1)=px(i,1)/dist; + py(i,1)=px(i,1)*y(in(i))/x(in(i)); + else + if abs(wx2(j+1)-wx2(j))<=eps + px(i,1)=wx2(j); + py(i,1)=px(i,1)*y(in(i))/x(in(i)); + else + px(i,1)=0; + py(i,1)=((wy2(j)-wy2(j+1))*wx2(j)/(wx2(j+1)-wx2(j)))+wy2(j); + end + end + end + end + % + % Decide the type of each data point + % + c=3; + num=0; + num1=0; + num2=0; + num3=0; + lopen=1; + i=1; + while lopen==1 + if ntot<=nsub + px(i,1)=lambdanc*px(i,2)+(1-lambdanc)*px(i,1); + py(i,1)=lambdanc*py(i,2)+(1-lambdanc)*py(i,1); + end + if (px(i,1)^2+py(i,1)^2)>eps + lambda(i)=sqrt(x(in(i))^2+y(in(i))^2)/sqrt(px(i,1)^2+py(i,1)^2); + else + if (abs(x(in(i)))c + num3=num3+1; + typ(i)=3; + end + i=i+1; + if i==(n+1) + lopen=0; + end + end + if xdev>eps + xcord=(x(in(1:n))+tukmed(1))*xdev+xmean; + else + xcord=x(in(1:n))+tukmed(1); + end + if ydev>eps + ycord=(y(in(1:n))+tukmed(2))*ydev+ymean; + else + ycord=y(in(1:n))+tukmed(2); + end + datatyp(1:n,1)=xcord; + datatyp(1:n,2)=ycord; + typ=typ'; + datatyp(1:n,3)=typ; + % + if not(numdattm==0) + num=num+numdattm; + typ(n+(1:numdattm))=0; + lambda(n+(1:numdattm))=0; + px(n+(1:numdattm),1)=x(dattm); + py(n+(1:numdattm),1)=y(dattm); + if xdev>eps + xcord=(x(dattm)+tukmed(1))*xdev+xmean; + else + xcord=x(dattm)+tukmed(1); + end + if ydev>eps + ycord=(y(dattm)+tukmed(2))*ydev+ymean; + else + ycord=y(dattm)+tukmed(2); + end + datatyp(n+(1:numdattm),1)=xcord; + datatyp(n+(1:numdattm),2)=ycord; + datatyp(n+(1:numdattm),3)=typ(n+(1:numdattm)); + end +else + datatyp((1:n),1)=(x+tukmed(1))*xdev+xmean; + datatyp((1:n),2)=(y+tukmed(2))*ydev+ymean; + datatyp((1:n),3)=0; + interpol=0; +end +% +if xdev>eps + tukmed(1)=tukmed(1)*xdev+xmean; +end +if ydev>eps + tukmed(2)=tukmed(2)*ydev+ymean; +end +result=struct('tukmed',tukmed,'datatyp',datatyp,'interpol',interpol,'depth',depth); + +%----------------------------- +function [x,y,Index,angl,ind1,ind2]=dithering(x,y) +%DITHERING is used to check whether the data points are in general position. +% If not, then a very small random number is added to each of +% the data points. Also the angles formed by pairs of data points are +% computed here. This file is used in the functions isodepth.m, halfmed.m +% and bagp.m. +% +% Required input arguments: +% x : vector containing the first coordinates of all the data +% points +% y : vector containing the second coordinates of all the data +% points + +if not(length(x)==length(y)) + error('The vectors x and y must have the same length.') +end +if sum(isnan(x))>=1 || sum(isnan(y))>=1 + error('Missing values are not allowed') +end +n=length(x); +i=0; +blijf3=1; +dith=1; +xorig=x; +yorig=y; +while dith==1 + [xs,Index]=sort(x); + ys=y(Index); + dith=0; + while blijf3==1 + blijf3=0; + i=i+1; + if (i+1)>n + blijf3=2; + else + j=i+1; + if not(xs(i)==xs(j)) + blijf3=1; + else + if ys(i)==ys(j) + dith=1; + blijf3=0; + % two datapoints coincide + else + if ys(i)=1 + spec=find(hoek<=0); + hoek(spec)=hoek(spec)+pi; + end + ind1((p+1):m)=i; + ind2((p+1):m)=rest; + end + % + % Sort all the angles and permute ind1 and ind2 in the same way. + % + [angl,In]=sort(hoek); + % + ind1=ind1(In); + ind2=ind2(In); + % + % Test wether any three datapoints are collinear. + % + ppp=diff(angl); + k=find(ppp==0); + % + if sum(ind1(k)==ind1(k+1))>=1 + % There are 3 or more datapoints collineair. + dith=1; + fac=100000000; + ran=randn(n,2); + x=xorig+ran(:,1)/fac; + wx=x; + y=yorig+ran(:,2)/fac; + wy=y; + else + x=x; + y=y; + end + + end +end + + diff --git a/LIBRA/bannerplot.m b/LIBRA/bannerplot.m new file mode 100644 index 0000000..22b6e14 --- /dev/null +++ b/LIBRA/bannerplot.m @@ -0,0 +1,64 @@ +function bannerplot(usedvars,separationstep,nobs,class,heights,objectorder) + +%BANNERPLOT creates a banner for the output of the clustering algorithms +%agnes, diana or mona. +% +% I/O: bannerplot(out) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +%Note: execute the following command if you have run the bannerplot at the +%commandline to ensure the axes are reset to their original colours for the +%next plot on the same figure window +% > whitebg([1 1 1]) +% +% Last update: 12/02/2009 by S.Verboven + +switch class + case 'MONA' + Y=separationstep; + stepmax=max(Y)+1; + Y(Y==0)=stepmax; + barh(fliplr(Y),1) + set(gcf, 'Name','Banner','numbertitle','off') + whitebg([1 1 1]) + xlabel('Separation Step') + xticks=0:stepmax; + set(gca,'XTick',xticks,'XTickLabel',xticks,'xcolor','k'); + axis([0,stepmax,0.5,nobs-0.5]) + Uvar=num2str(usedvars); + Uvar=fliplr(Uvar); + for k=1:nobs-1 + Uvar2(k)=Uvar(k+((k-1)*2)); + end + for i=1:(usedvars-1) + if Y(i)2 + % + %placing inputfields in array of strings + % + for j=1:nargin-2 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % + %Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) %in case of similarity + for j=1:nargin-2 %searching the index of the accompanying field + if rem(j,2)~=0 %fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end +end +%Checking prior (>0 ) +prior=options.membershipprob; +if size(prior,1)~=1 + prior=prior'; +end +if sum(prior) ~= 0 + epsilon=10^-4; + if (any(prior < 0) | (abs(sum(prior)-1)) > epsilon) + error('Invalid membership probabilities.') + end +end +ng=length(proportions); +if length(prior)~=ng + error('The number of membership probabilities is not the same as the number of groups.') +end + +%%%%%%%%%%%%%%%%%%MAIN FUNCTION %%%%%%%%%%%%%%%%%%%%% +%Checking if a validation set is given +if strmatch(options.misclassif, 'valid','exact') + if options.valid==0 + error(['The misclassification error will be estimated through a validation set',... + 'but no validation set is given!']) + else + validx = options.valid; + validgrouping = options.groupvalid; + if size(validx,1)~=length(validgrouping) + error('The number of observations in the validation set is not the same as the length of its group vector!') + end + if size(validgrouping,1)~=1 + validgrouping = validgrouping'; + end + countsvalidorig=tabulate(validgrouping); + countsvalid=countsvalidorig(countsvalidorig(:,2)~=0,:); + if size(countsvalid,1)==1 + error('The validation set must contain observations from more than one group!') + elseif any(ismember(empty,countsvalid(:,1))) + error(['Group(s)' , num2str(empty(ismember(empty,countsvalid(:,1)))), 'was/were empty in the original dataset.']) + end + if (length(options.weightsvalid) == 1) | (length(options.weightsvalid)~=size(validx,1)) + options.weightsvalid = ones(size(validx,1),1); + end + end +elseif options.valid~=0 + validx = options.valid; + validgrouping = options.groupvalid; + if size(validx,1) ~= length(validgrouping) + error('The number of observations in the validation set is not the same as the length of its group vector!') + end + if size(validgrouping,1)~=1 + validgrouping = validgrouping'; + end + options.misclassif='valid'; + countsvalidorig=tabulate(validgrouping); + countsvalid=countsvalidorig(countsvalidorig(:,2)~=0,:); + if size(countsvalid,1)==1 + error('The validation set must contain more than one group!') + elseif any(ismember(empty,countsvalid(:,1))) + error(['Group(s)' , num2str(empty(ismember(empty,countsvalid(:,1)))), 'was/were empty in the original dataset.']) + end + if (length(options.weightsvalid) == 1) | (length(options.weightsvalid)~=size(validx,1)) + options.weightsvalid = ones(size(validx,1),1); + end +end + +%Discriminant rule based on the training set x +result1 = rawrule(x, g,prior, options.method); + +%Discriminant rule based on reweighted results +if strmatch(options.misclassif,'valid','exact') + result2 = rewrule(validx, result1); + finalgroup = result2.class; +else + result2 = rewrule(x, result1); + finalgroup = result2.class; +end + +%Apply discriminant rule on validation set +switch options.misclassif + case 'valid' + [v,vi,vj]=unique(validgrouping); + %Redefining the group number + if any(v~= (1:length(v))) + v=1:length(v); + validgrouping=v(vj); + elseif size(validgrouping,1)~=1 + validgrouping=validgrouping'; + end + if any(countsvalidorig(:,2)==0) + empty=setdiff(countsvalidorig(find(countsvalidorig(:,2)==0),1), countsorig(find(countsorig(:,2)==0))); + disp(['Warning: the test group(s) ' , num2str(empty'), ' are empty']); + else + empty=[]; + end + misclas=-ones(1,length(lev)); + for i=1:size(validx,1) + if strmatch(options.method,'quadratic','exact') + dist(i) = mahalanobis(validx(i,:), result1.center(vj(i),:),'invcov',result1.invcov{vj(i)}); + else + dist(i) = mahalanobis(validx(i, :), result1.center(vj(i),:),'invcov',result1.invcov); + end + end + weightsvalid=zeros(1,length(dist)); + weightsvalid(dist <= chi2inv(0.975, p))=1; + for i=1:length(v) + if ~isempty(intersect(i,v)) + misclas(i)=sum((validgrouping(options.weightsvalid == 1)==finalgroup(options.weightsvalid == 1)')... + & (validgrouping(options.weightsvalid == 1)==repmat(lev(i),1,sum(options.weightsvalid)))); + ingroup(i) = sum((validgrouping(options.weightsvalid == 1) == repmat(lev(i), 1,sum(options.weightsvalid)))); + misclas(i) = (1 - (misclas(i)./ingroup(i))); + end + end + if any(misclas==-1) + misclas(misclas==-1)=0; + end + misclasprobpergroup=misclas; + misclas=misclas.*result1.prior; + misclasprob=sum(misclas); + case 'training' + for i=1:ng + misclas(i) = sum((g(options.weightstrain==1)==finalgroup(options.weightstrain==1)')... + &(g(options.weightstrain==1)==repmat(lev(i),1,sum(options.weightstrain)))); + ingroup(i) = sum((g(options.weightstrain==1) == repmat(lev(i),1,sum(options.weightstrain)))); + end + misclas = (1 - (misclas./ingroup)); + misclasprobpergroup = misclas; + misclas = misclas.*result1.prior; + misclasprob = sum(misclas); + weightsvalid=0;%only available with validation set + case 'cv' + finalgroup=[]; + for i=1:length(x) + xnew=removal(x,i,0); + groupnew=removal(group,0,i); + functie1res = rawrule(xnew, groupnew,prior, options.method); + functie2res = rewrule(x(i, :), functie1res); + finalgroup = [finalgroup; functie2res.class(1)]; + end + for i=1:ng + misclas(i) = sum((g(options.weightstrain==1) == finalgroup(options.weightstrain==1)')... + & (g(options.weightstrain==1) == repmat(lev(i),1,sum(options.weightstrain)))); + ingroup(i) = sum(g(options.weightstrain==1) == repmat(lev(i),1,sum(options.weightstrain))); + end + misclas = (1 - (misclas./ingroup)); + misclasprobpergroup= misclas; + misclas = misclas.* result1.prior; + misclasprob = sum(misclas); + weightsvalid=0; %only available with validation set +end + +%classify the new observations (predict) +if ~isempty(options.predictset) + resultpredict = rewrule(options.predictset, result1); + finalgrouppredict = resultpredict.class; + for i=1:size(options.predictset,1) + for j = 1:ng + if strmatch(options.method,'quadratic','exact') + distpredict(i,j) = mahalanobis(options.predictset(i,:), result1.center(j,:),'invcov',result1.invcov{j}); + else + distpredict(i,j) = mahalanobis(options.predictset(i, :), result1.center(j,:),'invcov',result1.invcov); + end + end + end + weightspredict = zeros(1,size(distpredict,1)); + weightspredict(min(distpredict,[],2) <= chi2inv(0.975, p))=1; +else + finalgrouppredict = 0; + weightspredict = 0; +end + +%Output structure +result=struct('assignedgroup',{finalgroup'},'scores',{result2.scores'},'method',{result2.method},... + 'cov',{result1.cov},'center',{result1.center},'md',{result1.mahal'}, 'flagtrain',{result1.flag'},... + 'flagvalid',{weightsvalid},'grouppredict',finalgrouppredict,'flagpredict',weightspredict','membershipprob',{result1.prior},... + 'misclassif',{options.misclassif},'groupmisclasprob',{misclasprobpergroup},... + 'avemisclasprob',{misclasprob},'class',{'CDA'},'x',{x},'group',{group}); + +if size(x,2)~=2 + result=rmfield(result,{'x','group'}); +end + +%Plotting the output +try + if options.plots + makeplot(result); + end +catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu +end + +%-------------------------------------------------------------------------- +function result=rawrule(x, grouping,prior, method) + +%calculate the discrimination rule based on the training set x. + +[n,p]=size(x); +g=grouping; +epsilon=10^-4; +[gun,gi,gj]=unique(g); + +ng=length(gun); +switch method + case 'linear' %equal covariances supposed + wsum=0; + for j=1:length(gun) + group.cov{j}=cov(x(g==gun(j),:)); + wsum=wsum+sum(g==gun(j))*group.cov{j}; + group.center(j,:)=mean(x(g==gun(j),:)); %center of all groups, matrix of ng x p + end + covar=wsum/n; + for i=1:n + zgeg(i,:)=x(i,:)-group.center(gj(i),:); + end + dist=zeros(n,1); + for j=1:length(gun) + dist(g==gun(j))=mahalanobis(x(g==gun(j),:),group.center(j,:),'invcov',inv(cov(zgeg))); + end + weights=zeros(n,1); + weights(dist <= chi2inv(0.975,p))=1; + result.cov=covar; %over all group + result.invcov=inv(covar); + result.flag=weights; + result.mahal=dist; + result.center=group.center; %all groups + result.method=method; + case 'quadratic' + for j=1:length(gun) + group.cov{j}=cov(x(g==gun(j),:)); %covariance of group j + group.invcov{j}=inv(group.cov{j}); + group.center(j,:)=mean(x(g==gun(j),:)); %center of all groups + end + for i=1:n + xdist(i)=mahalanobis(x(i,:), group.center(gj(i),:), 'invcov',group.invcov{gj(i)}); + end + weights=zeros(n,1); + weights(xdist <= chi2inv(0.975,p))=1; + result.cov=group.cov; %per group + result.invcov=group.invcov; + result.center = group.center; %all groups + result.mahal = xdist'; + result.flag = weights; + result.method = method; +end +if sum(prior) == 0 + counts = tabulate(g); + if ~any(counts(:,2)) + disp(['Warning: the group(s) ', num2str(counts(counts(:,2) == 0,1)'), 'contain only outliers']); + counts=counts(counts(:,2)~=0,:); + end + result.prior = (counts(:,3)/100)'; +else + result.prior = prior; +end + +%-------------------------------------------------------------------------- +function result=rewrule(x, rawobject) + +epsilon=10^-4; +center=rawobject.center; +covar=rawobject.cov; +invcov=rawobject.invcov; +prior=rawobject.prior; +method=rawobject.method; +if (length(prior) == 0 | length(prior) ~= size(center,1)) + error('invalid prior') +end +if sum(prior)~=0 + if (any(prior < 0) | (abs(sum(prior)-1)) > epsilon) + error('invalid prior') + end +end +ngroup=length(prior); +[n,p]=size(x); +switch method + case 'linear' + for j=1:ngroup + for i=1:n + scores(i,j) = linclassification(x(i,:)', center(j,:)', invcov, prior(j)); + end + end + [maxs,maxsI] = max(scores,[],2); + for i=1:n + maxscore(i,1) = scores(i,maxsI(i)); + end + result.scores = maxscore; + result.class = maxsI; + result.method = method; + case 'quadratic' + for j=1:ngroup + for i=1:n + scores(i, j) = classification(x(i,:)', center(j,:)', covar{j}, invcov{j}, prior(j)); + end + end + [maxs,maxsI] = max(scores,[],2); + for i=1:n + maxscore(i,1) = scores(i,maxsI(i)); + end + result.scores = maxscore; + result.class = maxsI; + result.method = method; +end + + +%--------------make sure the inputvariables are columnvectors! + +function out=classification(x, center, covar,invcov, priorprob) + +out=-0.5*log(abs(det(covar)))-0.5*(x - center)' * invcov *(x - center)+log(priorprob); + +%------------------- +function out=linclassification(x, center, invcov, priorprob) + +out=center'*invcov* x - 0.5*center'*invcov*center+log(priorprob); + diff --git a/LIBRA/cdq.m b/LIBRA/cdq.m new file mode 100644 index 0000000..7c48924 --- /dev/null +++ b/LIBRA/cdq.m @@ -0,0 +1,370 @@ +function result = cdq(x,y,c,varargin) + +%CDQ computes Censored Depth Quantiles for regression, as described in +% +% Debruyne, M., Hubert, M., Portnoy, S., Vanden Branden, K. (2008), +% "Censored depth quantiles", +% Computational Statistics and Data Analysis, 52, 1604-1614. +% +% Required input arguments: +% x : Data matrix of explanatory variables (also called 'regressors'). +% Rows of x represent observations, and columns represent variables. +% If the regression should contain an intercept, the x-matrix should +% contain a column of ones. +% y : A vector with n elements that contains the response variables. +% c : A vector that contains the indices of the censored observations. +% +% Optional input arguments: +% nrCan : Number of candidate hyperplanes to compute objective +% function at first grid point. The default is 500. +% nrCom : The objective function at each grid point after the first one is optimized over +% a set of nrCom hyperplanes having p-1 points in common with +% the previously fitted hyperplane. The default is 100. +% If nrCom = 0, the objective function at each grid point after the first one is optimized over +% a fixed set of 'nrCan' hyperplanes. +% nrLam : Number of hyperplanes used in the computation of the regression depth of each hyperplane. +% The default is 500. +% grid : Vector containing the gridpoints. The default is 0.05:0.05:0.95 +% maxit : Maximum number of iterations at each grid point. The default +% is 4. +% +% I/O: result=cdq(x,y,c,'nrCan',nrCan,'nrCom',nrCom,'nrLam',nrLam,'grid',grid,'maxit',maxit); +% The user should only give the input arguments that have to change their default value. +% The name of the input arguments needs to be followed by their value. +% The order of the input arguments is of no importance. +% +% The output of CDQ is a (g x p) matrix containing the regression quantiles, +% with g the number of grid points and p the number of regression parameters. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Michiel Debruyne +% Last Update: April 27, 2007. + +%Handle defaults and optional user inputs. +nrCan=500; +nrCom=100; +nrLam=500; +grid=0.05:0.05:0.95; +maxit=4; +default=struct('nrCan',nrCan,'nrCom',nrCom,'nrLam',nrLam,'grid',grid,'maxit',maxit); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +% +if nargin==1 + error('At least 3 inputs are required, see help cdq.') +elseif nargin==2 + error('At least 3 inputs are required, see help cdq.') +end + +if nargin>2 + + nrCan=round(options.nrCan); + nrLam=round(options.nrLam); + nrCom=floor(options.nrCom); + grid=sort(options.grid); + maxit=round(options.maxit); + + for j=1:nargin-3 % searching the index of the accompanying field + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp('nrCan',varargin{j}) + nrCan=varargin{j+1}; + if nrCan<1 + error('nrCan should be positive, see help cdq.') + end + end + end + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp('nrCom',varargin{j}) + nrCom=varargin{j+1}; + if nrCom<1 + error('nrCom should be positive, see help cdq.') + end + end + end + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp('nrLam',varargin{j}) + nrLam=varargin{j+1}; + if nrLam<1 + error('nrLam should be positive, see help cdq.') + end + end + end + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp('grid',varargin{j}) + grid=varargin{j+1}; + if size(grid,1)>1 & size(grid,2)>1 + error('grid should be a vector containing the grid points, not a matrix. See help cdq.') + elseif size(grid,1)==1 & size(grid,2)==1 + error('grid should be a vector containing the grid points, not a number. See help cdq.') + elseif sum(grid>=1)+sum(grid<=0) + error('All entries of grid should be between 0 and 1. See help cdq.') + end + end + end + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp('maxit',varargin{j}) + maxit=varargin{j+1}; + if maxit<2 + error('maxit should at least be 2, see help cdq.') + end + end + end + end +end + +%Start main program +n=length(y); +p=size(x,2); +L=length(grid); + +%Construct sets of hyperplanes. +M=nrCan+nrLam; +for i=1:M + ok=0; + while (ok==0) + a=transpose(willcomb(n,p)); + ppu=x(a,:); + if (rank(ppu)==p) + ok=1; + end + end + beti=ppu\y(a); + betakan(:,i)=beti; +end +setCan=betakan(:,1:nrCan); +setLam=betakan(:,nrCan+1 : nrCan+nrLam); + +%tauh contains the tau-hats, defined for every crossed censored observation. +tauh=[]; +crossedobs=[]; + +tau=grid(1); +beta(1,1)=tau; + +%Determine the first estimate +betaPrev=maxObj(x,y,tau,c,crossedobs,tauh,setCan,setLam); +tauPrev=tau; +beta(2:(p+1),1)=betaPrev; +l=2; +iterations=0; + +while l<=L + tau=grid(l); + if nrCom~=0 & iterations==0 + setCom=bepKanU(x,y,betaPrev,nrCom); + [betaNext,mm]=maxObj(x,y,tau,c,crossedobs,tauh,setCom,setLam); + elseif nrCom==0 + betaNext=maxObj(x,y,tau,c,crossedobs,tauh,setCan,setLam); + else + [betaNext,mm]=maxObj(x,y,tau,c,crossedobs,tauh,setCom,setLam); + end + residuals=y-x*betaNext; + + + %Good observations were crossed and still are. + resn=residuals(crossedobs)<=0; + goodObs=crossedobs(resn); + + %Bad observations were crossed but are not anymore. + badObs=crossedobs(residuals(crossedobs)>0); + + %New ones are the ones that are crossed for the first time. + notCross=comp(crossedobs,c); + newObs=notCross(residuals(notCross)<=0); + + + if iterations0)|(length(newObs)>0)) + + %Retain the weights of the good ones. + tauh=tauh(resn); + crossedobs=goodObs; + + %Add the new ones. + nrNew=length(newObs); + crossedobs=[crossedobs newObs]; + + %Their weight equals the previous grid point. + tauh=[tauh repmat(tauPrev,[1,nrNew])]; + + iterations=iterations+1; + + elseif iterations==maxit + %No solution is found (zero in output). + beta(:,end+1)=[tau; zeros(p,1)]; + iterations=0; + residuals=y-x*betaNext; + if ~isempty(tauh) + tauh=tauh(tauh~=tauPrev); + crossedobs=crossedobs(1:length(tauh)); + end + l=l+1; + + %BetaNext is a good solution. + else + beta(:,end+1)=[tau;betaNext]; + iterations=0; + betaPrev=betaNext; + tauPrev=tau; + l=l+1; + end +end + +result=beta(2:end,:)'; + +%function computing the maximum of the objective function + +function[beta,maxobjf]=maxObj(x,y,tau,c,crossedobs,tauh,cand,setLam) + +n=length(y); +nrCan=size(cand,2); +nrLam=size(setLam,2); + +nc=comp(c,1:n); + +i=1; +maxobjf=0; + +while i<=nrCan + + betai=cand(:,i); + ri=y-x*betai; + + minobjBetai=10^20; + j=1; + while j<=nrLam & minobjBetai>maxobjf + + %Take next direction. + setLamj=betai-setLam(:,j); + + if ~isempty(find(abs(setLamj)>0.0000001)) + + %Project. + proj=x*setLamj; + + notCross=comp(crossedobs,c); + Kcomp=[notCross nc]; + + %Part objective function observations in Kcomp. + riKcompos=ri(Kcomp)>=-0.0000001; projKcompos=proj(Kcomp)>0.0000001; + riKcompne=ri(Kcomp)<=0.0000001; projKcompne=proj(Kcomp)<-0.0000001; + obj1V=tau*sum(riKcompos.*projKcompne)+(1-tau)*sum(riKcompne.*projKcompos); + obj1G=tau*sum(riKcompos.*projKcompos)+(1-tau)*sum(riKcompne.*projKcompne); + + if ~isempty(crossedobs) + %Part objective function for pseudo-observations in crossed censured observations. + xcl=x(crossedobs,:)*setLamj; ric=ri(crossedobs); xclp=xcl>0.0000001; + xcln=xcl<-0.0000001; ricn=ric<=0.0000001;ricp=ric>=-0.0000001; + proposResnegV=tauh(xclp & ricn); + pronegResposV=tauh(xcln & ricp); + obj2V=tau*sum((tau-pronegResposV)./(1-pronegResposV))+(1-tau)*sum((tau-proposResnegV)./(1-proposResnegV)); + + proposResnegG=tauh(xcln & ricn); + pronegResposG=tauh(xclp & ricp); + obj2G=tau*sum((tau-pronegResposG)./(1-pronegResposG))+(1-tau)*sum((tau-proposResnegG)./(1-proposResnegG)); + + %Part objective function for pseudo-observations at infinity. + pronegV=tauh(xcl<-0.0000001); + obj3V=tau*sum(1-(tau-pronegV)./(1-pronegV)); + + pronegG=tauh(xcl>0.0000001); + obj3G=tau*sum(1-(tau-pronegG)./(1-pronegG)); + + else + obj2V=0; + obj2G=0; + obj3V=0; + obj3G=0; + end + + %Paste 3 parts. + objG=obj1G+obj2G+obj3G; + objV=obj1V+obj2V+obj3V; + + minobjBetai=min([objG,objV,minobjBetai]); + end + j=j+1; + end + + if minobjBetai>maxobjf + beta=betai; + maxobjf=minobjBetai; + end + i=i+1; +end + +% ------------------------------------------------------------------------ + +function[thetann] = exchangeob(theta,Minv,zu,yu,zi,yi) + +zit=transpose(zi); +u=Minv*zi; +w=-1/(1+zit*u); +thetan=theta-(yi-zit*theta)*w*u; +Mninv=Minv+w*u*transpose(u); + +zut=transpose(zu); +un=Mninv*zu; +wn=-1/(1-zut*un); +thetann=thetan+(yu-zut*thetan)*wn*un; + +% ------------------------------------------------------------------------ + +function [nr] = willcomb(n,p) + +he=randperm(n); +nr=he(1:p); + +% ------------------------------------------------------------------------ + +function[zComp]=comp(z,c) + +n=length(z); +i=1; +while i<=n + c=c(z(i)~=c); + i=i+1; +end +zComp=c; + +% ------------------------------------------------------------------------ + +function[setHyp]=bepKanU(x,y,betaNext,M) + +betaNext; +n=size(x,1); +p=size(x,2); + +residuals=y-x*betaNext; +a=abs(residuals)<=0.00001; + +blq=cumsum(a); +if blq(end)>p + a(blq>p)=0; +end + +xa=x(a,:); +ya=y(a,1); +xb=x(a==0,:); +yb=y(a==0,1); +aa=size(xa); + +setHyp=zeros(p,M); +setHyp(:,1)=betaNext; +minv=inv(transpose(xa)*xa); +for i=2:M + j=ceil(rand(1,1)*p); + k=ceil(rand(1,1)*(n-p)); + bb=size(xb); + aa=size(xa); + if rank([xa(1:(j-1),:);xa((j+1):end,:);xb(k,:)])==p + gre=exchangeob(betaNext,minv,transpose(xa(j,:)),ya(j),transpose(xb(k,:)),yb(k,1)); + setHyp(:,i)=gre; + else + setHyp(:,i)=betaNext; + end +end \ No newline at end of file diff --git a/LIBRA/chiqqplot.m b/LIBRA/chiqqplot.m new file mode 100644 index 0000000..e8acf0b --- /dev/null +++ b/LIBRA/chiqqplot.m @@ -0,0 +1,41 @@ +function chiqqplot(y,p,class) + +%CHIQQPLOT produces a Quantile-Quantile-plot of the vector y +% versus the square root of the quantiles of the chi-squared distribution. +% +% Required input arguments: +% y : row or column vector +% p : degrees of freedom of the chi-squared distribution +% +% Optional input argument: +% class : a string used for the y-label and the title(default: ' ') +% +% I/O: chiqqplot(y,p,class) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +%Written by Nele Smets +% Last update: 23/10/2003 + +set(gcf,'Name', 'Chisquare QQ-plot', 'NumberTitle', 'off') +n=length(y); +if nargin==2 + class=''; +end +for i=1:n + x(i)=chi2inv((i-1/3)/(n+1/3),p); +end +x=sqrt(x); +y=sort(y); +plot(x,y,'o') +xlabel('Square root of the quantiles of the chi-squared distribution'); +if strcmp(class,'MCDCOV') + ylabel('Robust distance'); +elseif strcmp(class,'COV') + ylabel('Mahalanobis distance'); +else + ylabel('Distance'); +end +title(class); diff --git a/LIBRA/clara.m b/LIBRA/clara.m new file mode 100644 index 0000000..89d885d --- /dev/null +++ b/LIBRA/clara.m @@ -0,0 +1,194 @@ +function result = clara(x,kclus,vtype,stdize,metric,plots,nsamp,sampsize) + +%CLARA is the 'Clustering Large Applications' clustering algorithm. +% It returns a list representing a clustering of the data +% into kclus clusters following the clara algorithm which is +% designed for large data sets. +% +%The algorithm is fully described in: +% Kaufman, L. and Rousseeuw, P.J. (1990), +% "Finding groups in data: An introduction to cluster analysis", +% Wiley-Interscience: New York (Series in Applied Probability and +% Statistics), ISBN 0-471-87876-6. +% +% Required input arguments: +% x : Data matrix (rows = observations, columns = variables) +% kclus : The number of desired clusters +% vtype : Variable type vector (length equals number of variables) +% Possible values are 1 Asymmetric binary variable (0/1) +% 2 Nominal variable (includes symmetric binary) +% 3 Ordinal variable +% 4 Interval variable +% +% Optional input arguments: +% stdize : standardise the variables given by the x-matrix +% Possible values are 0 : no standardisation (default) +% 1 : standardisation by the mean +% 2 : standardisation by the median +% metric : Metric to be used +% Possible values are 0: Mixed (not all interval variables, default) +% 1: Euclidean (all interval variables, default) +% 2: Manhattan +% plots : draws figure +% Possible values are 0 : do not create a clusplot (default) +% 1 : create a clusplot +% nsamp : Number of samples to be drawn from the data set +% sampsize : Number of observations in each sample (should be higher +% than the number of clusters and lower than the number of +% observations) +% +% +% I/O: +% result=clara(x,kclus,vtype,stdize,1,5,40+2*kclus) +% +% Example: +% load obj200.mat +% result=clara(obj200,3,[4 4]); +% +% The output of CLARA is a structure containing: +% result.dysobs : dissimilarities for each observation with the medoids +% result.metric : metric used +% result.number : number of observations +% result.idmed : Id of medoid observations +% result.ncluv : A vector with length equal to the number of observations, +% giving for each observation the number of the cluster to +% which it belongs +% result.obj : Objective function for the best subsample +% result.clusinf : Matrix, each row gives numerical information for +% one cluster. These are the cardinality of the cluster +% (number of observations), the maximal and average +% dissimilarity between the observations in the cluster +% and the cluster's medoid, the diameter of the cluster +% (maximal dissimilarity between two observations of the +% cluster), and the separation of the cluster (minimal +% dissimilarity between an observation of the cluster +% and an observation of another cluster). +% result.sylinf : Matrix based on the best subsample, with for each +% observation i of this subsample the cluster to +% which i belongs, as well as the neighbor cluster of i +% (the cluster, not containing i, for which the average +% dissimilarity between its observations and i is minimal), +% and the silhouette width of i. +% result.x : (Standardized) data +% result.class : 'CLARA' +% +% CLARA will create the clusplot if plots equals 1. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Guy Brys (May 2006) +% Last revision: 04 June 2009 by S.Verboven and M. Hubert + +%Checking and filling out the inputs +if (nargin<3) + error('Three input arguments required') +elseif (nargin<4) + stdize = 0; + if (sum(vtype)~=4*size(x,2)) + metri=0; + metric = 'mixed'; + else + metri=1; + metric = 'euclidean'; + end + plots=0; + nsamp=5; + sampsize=40+2*kclus; +elseif (nargin<5) + if (sum(vtype)~=4*size(x,2)) + metri=0; + metric = 'mixed'; + else + metri=1; + metric = 'euclidean'; + end + plots=0; + nsamp=5; + sampsize=40+2*kclus; +elseif (nargin<7) + nsamp=5; + sampsize=40+2*kclus; +elseif (nargin<8) + sampsize=40+2*kclus; +end + +% defining metric (for 4 input arguments) and diss +if (nargin>=5) + if (metric==1) + metri=1; + metric='euclidean'; + elseif (metric==2) + metri=2; + metric='manhattan'; + elseif (metric==0) + metri=0; + metric='mixed'; + else + error('metric must be 0,1 or 2') + end +end + +sampsize = min(sampsize,size(x,1)); + +%Standardization +if (stdize==1) + x = ((x - repmat(mean(x),size(x,1),1))./(repmat(std(x),size(x,1),1))); +elseif (stdize==2) + x = ((x - repmat(median(x),size(x,1),1))./(repmat(mad(x),size(x,1),1))); +end + +%Actual calculations +obj = Inf; +for i=1:nsamp + sampindex = randperm(size(x,1)); + restemp = pam(x(sampindex(1:sampsize),:),kclus,vtype,0,metri); + if (restemp.obj(1) delete(gcf) to get rid of the menu +end + + + + + + + diff --git a/LIBRA/classSVD.m b/LIBRA/classSVD.m new file mode 100644 index 0000000..4144524 --- /dev/null +++ b/LIBRA/classSVD.m @@ -0,0 +1,30 @@ +function [P,T,L,r,centerX,cX]=classSVD(x) + +%CLASSSVD performs the singular value decomposition of a matrix with more +% rows than columns (uses svd.m) +% +% Required input: +% x : data matrix of size n by p where n>p +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Sabine Verboven, Mia Hubert +% Last Update: 06/07/2004 + +[n,p]=size(x); + +if n==1 + error('The sample size is 1. No SVD can be performed.') +end +cX=mean(x); +centerX=x-repmat(cX,n,1); +[U,S,loadings]=svd(centerX./sqrt(n-1),0); +eigenvalues=diag(S).^2; +tol = max([n p])*eigenvalues(1)*eps; +r= sum(eigenvalues>tol); +L=eigenvalues(1:r); +P=loadings(:,1:r); +T=centerX*P; + diff --git a/LIBRA/clusplot.m b/LIBRA/clusplot.m new file mode 100644 index 0000000..2414789 --- /dev/null +++ b/LIBRA/clusplot.m @@ -0,0 +1,239 @@ +function clusplot(x,ncluv,span,xlabels) + +%CLUSPLOT creates a bivariate plot visualizing a partition (clustering) +% of the data. All observations are represented by points in the plot, +% using principal components or multidimensional scaling. Around each +% cluster an ellipse is drawn. +% +%The algorithm is fully described in: +% Pison, G., Struyf, A., and Rousseeuw, P.J. (1999), +% "Displaying a clustering with CLUSPLOT", +% Computational Statistics and Data Analysis, +% 30, 381-392. +% +% Required input arguments: +% x : Data matrix (rows = observations, columns = variables) +% All variables need to be numeric. +% Dissimilarity vector (if input is vector with the dissimilarities read in columnwise) +% ncluv : Cluster membership for each observation (mostly +% as been found by PAM, FANNY or CLARA) +% +% Optional input arguments: +% span : if span=1 (default) the smallest ellipses which encompasse the clustered groups +% will be drawn, otherwise (span=0) ellipses will be plotted. +% xlabels : User-specified labels of the observations to plot on the +% graph. +% +% +% I/O: +% result=clusplot(x,ncluv,span,xlabels) +% +% Example: +% load ruspini.mat +% result=pam(ruspini,4,[4 4]); +% clusplot(ruspini,result.ncluv); +% clusplot(result.dys,result.ncluv); +% +% The output of CLUSPLOT is a figure containing the data +% (possibly plotted in a lower dimension) and ellipses +% surrounding the clusters. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Guy Brys (May 2006) +% Last revision 04 June 2009 by S.Verboven + +if nargin<4 + xlabels=[]; +end +if nargin < 3 + span=1; +end +xgrp=double(ncluv); + +if (isempty(x)) + error('The input matrix must be non-empty'); +elseif (size(x,1)==1) || (size(x,2)==1) + if (size(x,1)==1) + x = x'; + end + %Dissimilarities given + if (sum(isnan(x))~=0) + error('Missing values not allowed in the dissimilarity vector'); + end + %We assume x to be a vector of dissimilarities (read in from the matrix column by column) + n = (1+sqrt(1+8*size(x,1)))/2; + x1 = zeros(n,n); + indexcol = 0; + for i=1:(n-1) + indexcol = [indexcol ; (max(indexcol)-i+1)+repmat(i,(n-i),1)+(i:(n-1))']; + end + indexcol=indexcol(2:length(indexcol))'; + x1(indexcol) = x; %fill out the dissimilarity matrix at the bottom half, column by column + x1 = x1+x1';% full dissimilarity matrix + [points,eiga]= cmdscale(x1); + plotgrp(points(:,1),points(:,2),xgrp); + title(sprintf('CLUSPLOT (the %d components explain %5.2f percent of the point variability)',2,... + max(0,min(100,100*sum(eiga(1:2))/sum(diag(points*points')))))) + hold on + xplot = points(:,1:2); +elseif (size(x,2)==2) + plotgrp(x(:,1),x(:,2),xgrp); + title('CLUSPLOT of a bivariate data set') + hold on + xplot = x; +else + %Real data given + if ((sum((sum(isnan(x))==size(x,1)))>0) || (sum((sum(isnan(x'))==size(x,2)))>0)) + error('A variable or observation contains only missing values'); + end + indexnan = find(isnan(x)); + for i=indexnan + x(i) = median(x(isnan(x(:,ceil(i/size(x,1))))~=1,ceil(i/size(x,1)))); + end + R = cpca(x,'k',2,'plots',0); + R.T(:,2) = -R.T(:,2); + plotgrp(R.T(:,1),R.T(:,2),xgrp); + title(sprintf('CLUSPLOT (the %d components explain %5.2f percent of the point variability)',2,100*sum(R.L)/sum(var(x)))) + hold on + xplot = R.T; +end + +k = double(max(ncluv)); + +for i=1:k + x1 = xplot(ncluv==i,:); + if (rank(cov(x1))==2) + if (span==1) + x2 = [repmat(1,size(x1,1),1) x1]; + [clsqdi,clprob,clstop] = spannc(x2); + [B(i,:) A(:,:,i)] = weightmecov_new(x1,max(0,clprob)); + D(i) = sqrt(weightmecov_new(clsqdi',max(0,clprob))); + elseif (span==0) + B(i,:) = mean(x1); + A(:,:,i) = cov(x1); + D(i) = sqrt(max(mahalanobis(x1,B(i,:),'cov',A(:,:,i)))); + D(i) = D(i)+0.01*D(i); + end + elseif (rank(cov(x1))~=2) + if (span==1) + D(i) = 1; + if ((sum(x1(:,1)~=x1(1,1))~=0) || (sum(x1(:,2)~=x1(1,2))~=0)) + B(i,:) = [min(x1(:,1))+(max(x1(:,1))-min(x1(:,1)))/2 min(x1(:,2))+(max(x1(:,2))-min(x1(:,2)))/2]; + aa = sqrt((B(i,1)-min(x1(:,1)))^2+(B(i,2)-min(x1(:,2)))^2); + if (sum(x1(:,1)~=x1(1,1))~=0) + [maxx1,ind1] = max(x1(:,1)); + [minx1,ind2] = min(x1(:,1)); + qq = atan((x1(ind1,2)-x1(ind2,2))/(maxx1-minx1)); + bb = 0; + else + qq = pi/2; + bb = 0; + end + A(:,:,i) = [cos(qq) sin(qq) ; -sin(qq) cos(qq)]*[aa^2 0 ; 0 bb^2]*[cos(qq) -sin(qq) ; sin(qq) cos(qq)]; + else + aa = (max(x1(:,1))-min(x1(:,1)))/90; + bb = (max(x1(:,2))-min(x1(:,2)))/70; + aa = aa+(aa==0); + bb = bb+(bb==0); + A(:,:,i) = [aa^2 0 ; 0 bb^2]; + B(i,:) = x1(1,:); + end + else + D(i) = 1; + if (((max(x1(:,1))-min(x1(:,1)))>(max(x(:,1))-min(x(:,1)))/70) || ((max(x1(:,2))-min(x1(:,2)))>(max(x(:,2))-min(x(:,2)))/50)) + B(i,:) = [min(x1(:,1))+(max(x1(:,1))-min(x1(:,1)))/2 min(x1(:,2))+(max(x1(:,2))-min(x1(:,2)))/2]; + aa = sqrt((B(i,1)-min(x1(:,1)))^2+(B(i,2)-min(x1(:,2)))^2); + aa = aa+0.05*aa; + if ((max(x1(:,1))-min(x1(:,1)))>(max(x(:,1))-min(x(:,1)))/70) + [maxx1,ind1] = max(x1(:,1)); + [minx1,ind2] = min(x1(:,1)); + qq = atan((x1(ind1,2)-x1(ind2,2))/(maxx1-minx1)); + if (min(x(:,2))==max(x(:,2))) + bb = 1; + else + if ((max(x1(:,2))-min(x1(:,2)))>(max(x(:,2))-min(x(:,2)))/50) + bb = (max(x1(:,2))-min(x1(:,2)))/10; + else + bb = (max(x(:,2))-min(x(:,2)))/40; + end + end + else + if (min(x(:,1))==max(x(:,1))) + bb = 1; + else + bb = (max(x(:,1))-min(x(:,1)))/40; + end + qq = pi/2; + end + A(:,:,i) = [cos(qq) sin(qq) ; -sin(qq) cos(qq)]*[aa^2 0 ; 0 bb^2]*[cos(qq) -sin(qq) ; sin(qq) cos(qq)]; + else + aa = (max(x(:,1))-min(x(:,1)))/90; + bb = (max(x(:,2))-min(x(:,2)))/70; + aa = aa+(aa==0); + bb = bb+(bb==0); + A(:,:,i) = [aa^2 0 ; 0 bb^2]; + B(i,:) = x1(1,:); + end + end + end + posedges = ellipse(A(:,:,i),B(i,:),D(i)); + patch(posedges(:,1),posedges(:,2),[0 0 0],'FaceColor','none') +end +if ~isempty(xlabels) + putlabel(xplot(:,1),xplot(:,2),xlabels) +end +hold off + +%------------ +%SUBFUNCTIONS + +function posedges = ellipse(a,b,d) + +%a: covariance matrix +%b: location +%d: distances + +deta = a(1,1)*a(2,2)-a(1,2)^2; +if deta<0 + deta=0; +end +ylimit = sqrt(a(2,2))*d; +y = -ylimit:(0.01*ylimit):ylimit; +discr = sqrt((deta/(a(2,2)^2))*(a(2,2)*d^2-y.^2)); +discr([1 length(discr)]) = 0; +z = b(1)+(a(1,2)/a(2,2))*y; %the middle of the ellipse +x1 = z-discr; %left of z, on the ellipscontour +x2 = z+discr; %right of z, on the ellipscontour +y = b(2)+y; %position on the y-axis +posedges = [x1' y' ; x2(length(x2):-1:1)' y(length(y):-1:1)']; + +%--- +function plotgrp(x,y,ncluv) + +symb = ['+' 'o' 's' 'd' 'x' '*' '.' 'v']; +k = max(ncluv); +if (k<=8) + for i=1:k + plot(x(ncluv==i),y(ncluv==i),symb(i)); + hold on + end +else + error('Too many groups'); +end + +%--- +function [wmean,wcov]=weightmecov_new(data,weights) + +n = size(data,1); +if (~(isempty(find(weights<0,1)))) + error('The weights are negative'); +end +if size(weights,1)==1 + weights=weights'; +end +wmean=sum(diag(weights/sum(weights))*data); + +wcov=((data - repmat(wmean,n,1))'*diag(weights)*(data - repmat(wmean,n,1)))/(1-sum(weights.^2)); diff --git a/LIBRA/country.mat b/LIBRA/country.mat new file mode 100644 index 0000000000000000000000000000000000000000..3a3f503a7265306223af567f55d1f8b8766037fa GIT binary patch literal 288 zcmeZu4DoSvQZUssQ1EpO(M`+DN!3vZ$Vn_o%P-2cQV4Jk_w>_Ia4t$sEJ;mK$j`G< z2+1f_a4bz%Ffvy#G_*3Zv@$ePFfuT(R3I5JFnap(GcYjB0OE={kCPJ;E+nKRBrso+ z;SrExiC9=ur;x^!v_OchOu_G*{#k9!RVfcxHrX2-XxMo8rm;f8y96G#WdZwSri2wT zXlEO=UErC;w>hn;K`-PU+o9 result.flag.sd) +% or whose orthogonal distance is larger than result.cutoff.od (==> result.flag.od) +% can be considered as outliers and receive a flag equal to zero (result.flag.all). +% The regular observations receive a flag 1. +% result.class : 'CPCA' +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Sabine Verboven +% Last Update: 05/04/2003 + +default=struct('plots',1,'k',0); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +counter=1; +if nargin>1 + % + % placing inputfields in array of strings + % + for j=1:nargin-1 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % Checking which default parameters have to be changed + % and keep them in the structure 'options'. + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) % in case of similarity + for j=1:nargin-2 % searching the index of the accompanying field + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end +end +[n,p]=size(x); +% using the optimal (least time consuming) algorithm +if n

delete(gcf) to get rid of the menu +end \ No newline at end of file diff --git a/LIBRA/cpcr.m b/LIBRA/cpcr.m new file mode 100644 index 0000000..65f1a53 --- /dev/null +++ b/LIBRA/cpcr.m @@ -0,0 +1,159 @@ +function result=cpcr(x,y,varargin) + +%CPCR performs a classical principal components regression. +% First, classical PCA is applied to the predictor variables x (see cpca.m) and +% k components are retained. Then a multiple linear regression method (see mlr.m) +% is performed of the response variable y on the k principal components. +% +% I/O: result=cpcr(x,y,'k',2); +% +% Required input arguments: +% x : Data matrix of the explanatory variables +% (n observations in rows, p variables in columns) +% y : Data matrix of the response variables +% (n observations in rows, q variables in columns) +% +% Optional input argument: +% k : Number of principal components to compute. If k is missing, +% a scree plot is drawn which allows to select +% the number of principal components. +% plots : If equal to one (default), a menu is shown which allows to draw several plots, +% such as a score outlier map and a regression outlier map. +% If 'plots' is equal to zero, all plots are suppressed. +% See also makeplot.m +% +% The output is a structure containing +% +% result.slope : Classical slope +% result.int : Classical intercept +% result.fitted : Classcial prediction vector +% result.res : Classical residuals +% result.sigma : Estimated variance-covariance matrix of the residuals +% result.rsquared : R-squared value +% result.k : Number of components used in the regression +% result.sd : Classical score distances +% result.od : Classical orthogonal distances +% result.resd : Residual distances (when there are several response variables). +% If univariate regression is performed, it contains the standardized residuals. +% result.cutoff : Cutoff values for the score, orthogonal and residual distances. +% result.flag : The observations whose orthogonal distance is larger than result.cutoff.od +% (orthogonal outliers => result.flag.od) and/or whose residual distance is +% larger than result.cutoff.resd (bad leverage points/vertical outliers => result.flag.resd) +% can be considered as outliers and receive a flag equal to zero (=> result.flag.all). +% The regular observations, including the good leverage points, receive a flag 1. +% result.class : 'CPCR' +% result.cpca : Full output of the classical PCA part (see cpca.m) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by S. Verboven +% Last Update: 05/04/2003 + +default=struct('plots',1,'k',0); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +counter=1; +if nargin>2 + % + % placing inputfields in array of strings + % + for j=1:nargin-2 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % Checking which default parameters have to be changed + % and keep them in the structure 'options'. + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) % in case of similarity + for j=1:nargin-2 % searching the index of the accompanying field + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end +end + + +%classical PCA +if options.k==0 + out.cpca=cpca(x,'plots',0); +else + out.cpca=cpca(x,'plots',0,'k',options.k); +end +%model with intercept +T=[out.cpca.T(:,1:out.cpca.k) ones(size(out.cpca.T,1),1)]; +%Multivariate linear regression +out.a=inv(T'*T)*T'*y; +out.fitted=T*out.a; +len=size(out.a,1); +p=size(T,2); +q=size(y,2); +geg=[T,y]; +[n,m]=size(geg); +S=cov(geg); +Sx=S(1:p-1,1:p-1); +Sxy=S(1:p-1,p+1:m); +Syx=Sxy'; +Sy=S(p+1:m,p+1:m); +Se=Sy-out.a(1:p-1,1:q)'*Sx*out.a(1:p-1,1:q); %variance of errors +%regression coefficients slope and intercept ([\beta \alpha]) +out.coeffs=[out.cpca.P(:,1:out.cpca.k)*out.a(1:len-1,:); out.a(len,:)-out.cpca.M*out.cpca.P(:,1:out.cpca.k)*out.a(1:len-1,:)]; %%coefficients in the original space; +out.slope=out.cpca.P(:,1:out.cpca.k)*out.a(1:len-1,:); +out.int=out.a(len,:)-out.cpca.M*out.cpca.P(:,1:out.cpca.k)*out.a(1:len-1,:); +out.res=y-out.fitted; +out.sigma=Se; +if q==1 + out.stdres=out.res./sqrt(diag(out.sigma)); +end +out.k=out.cpca.k; +STTm=sum((y-repmat(mean(y),length(y),1)).^2); +SSE=sum(out.res.^2); +out.rsquared=1-SSE/STTm; +out.class='CPCR'; + +%calculating residual distances +if q>1 + if (-log(det(Se)/(m-1)))>50 + out.resd='singularity'; + else + cen=zeros(q,1); + out.resd=sqrt(mahalanobis(out.res,cen','cov',Se))'; + end +else % q==1 + out.resd=out.stdres; %standardized residuals +end +out.cutoff=out.cpca.cutoff; +out.cutoff.resd=sqrt(chi2inv(0.975,size(y,2))); +%computing flags +out.flag.od=out.cpca.flag.od; +out.flag.sd=out.cpca.flag.sd; +out.flag.resd=abs(out.resd)<=out.cutoff.resd; +out.flag.all=(out.flag.od & out.flag.resd); + +result=struct( 'slope',{out.slope}, 'int',{out.int},'fitted',{out.fitted},'res',{out.res},... + 'cov',{out.sigma},'rsquared',{out.rsquared},... + 'k',{out.k},'sd', {out.cpca.sd},'od',{out.cpca.od},'resd',{out.resd},... + 'cutoff',{out.cutoff},'flag',{out.flag},'class',{out.class},... + 'cpca',{out.cpca}); + +try + if options.plots + makeplot(result) + end +catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu + end + \ No newline at end of file diff --git a/LIBRA/csimca.m b/LIBRA/csimca.m new file mode 100644 index 0000000..a13ec89 --- /dev/null +++ b/LIBRA/csimca.m @@ -0,0 +1,447 @@ +function result = csimca(x,group,varargin); + +%CSIMCA performs the SIMCA method. This is a classification +% method on a data matrix x with a known group structure. On each group a +% robust PCA analysis is performed. Afterwards a classification +% rule is developped to determine the assignment of new observations. +% +% Required input arguments: +% x : training data set (matrix of size n by p). +% group : column vector containing the group numbers of the training +% set x. For the group numbers, any strict positive integer is +% allowed. +% +% Optional input arguments: +% k : Is a vector with size equal to the number of groups, or otherwise 0. Represents the number +% of components to be retained in each group. (default = 0.) +% method : Indicates which classification rule is wanted. `1' results in rule (R1) +% based on the scaled orthogonal and score distances. `2' corresponds with +% (R2) based on the squared scaled orthogonal and score distances. Default is 2. +% gamma : Represents the value(s) used in the classification rule: weight gamma is given to the od's, +% weight (1-gamma) to the sd's. (default = 0.5). +% misclassif : String which indicates how to estimate the probability of +% misclassification. It can be based on the training data ('training'), +% a validation set ('valid'), or cross-validation ('cv'). Default is 'training'. +% membershipprob : Vector which contains the membership probability of each +% group (sorted by increasing group number). These values are used to +% obtain the total misclassification percentage. +% valid : If misclassif was set to 'valid', this field should contain +% the validation set (a matrix of size m by p). +% groupvalid : If misclassif was set to 'valid', this field should contain the group numbers +% of the validation set (a column vector). +% predictset : Contains a new data set (a matrix of size mp by p) from which the +% class memberships are unknown and should be predicted. +% plots : If equal to 1, one figure is created with the training data and the +% boundaries for each group. This plot is +% only available for trivariate (or smaller) data sets. For technical reasons, a maximum +% of 6 groups is allowed. Default is one. +% plotspca : If equal to one, a score diagnostic plot is +% drawn (default). If 'plots' is equal to zero, this plot is suppressed. +% See also makeplot.m +% labsd : The 'labsd' observations with largest score distance are +% labeled on the diagnostic plot. (default = 3) +% labod : The 'labod' observations with largest orthogonal distance are +% labeled on the diagnostic plot. default = 3) +% +% Options for advanced users (input comes from the program RSIMCA.m with option 'classic' = 1): +% +% weightstrain : The weights for the training data. Corresponds to the flagtrain from RSIMCA. (default = 1) +% weightsvalid : The weights for the validation data. Corresponds to the flagvalid from RSIMCA. (default = 1) +% +% I/O: result=csimca(x,group,'method',1,'misclassif','training',... +% 'membershipprob',proportions,'valid',y,'groupvalid',groupy,'plots',0); +% +% The user should only give the input arguments that have to change their default value. +% The name of the input arguments needs to be followed by their value. +% The order of the input arguments is of no importance. +% +% Examples: out=csimca(x,group,'method','1') +% out=csimca(x,group,'plots',0) +% out=csimca(x,group,'valid',y,'groupvalid',groupy) +% +% The output is a structure containing the following fields: +% result.assignedgroup : If there is a validation set, this vector contains the assigned group numbers +% for the observations of the validation set. Otherwise it contains the +% assigned group numbers of the original observations based on the discriminant rules. +% result.pca : A cell containing the results of the different PCA analysis on the training sets. +% result.method : String containing the method used to obtain +% the discriminant rules (either 1 for 'R1' or 2 for 'R2'). This +% corresponds to the input argument method. +% result.flagtrain : Observations from the training set whose score distance and/or orthogonal distance +% exceeds a certain cut-off value can be considered as outliers and receive a flag equal +% to zero. The regular observations receive a flag 1. (See also robpca.m) +% result.flagvalid : Observations from the validation set whose score distance and/or orthogonal distance +% exceeds a certain cut-off value can be considered as outliers and receive a +% flag equal to zero. The regular observations receive a flag 1. +% If there is no validation set, this field is equal to zero. +% result.grouppredict : If there is a prediction set, this vector contains the assigned group numbers +% for the observations of the prediction set. +% result.flagpredict : Observations from the new data set (predict) whose robust distance (to the center of their group) +% exceeds a certain cut-off value can be considered as overall outliers and receive a +% flag equal to zero. The regular observations receive a flag 1. +% If there is no prediction set, this field is +% equal to zero. +% result.membershipprob : A vector with the membership probabilities. If no priors are given, they are estimated +% as the proportions of observations in the training set. +% result.misclassif : String containing the method used to estimate the misclassification probabilities +% (same as the input argument misclassif) +% result.groupmisclasprob : A vector containing the misclassification probabilities for each group. +% result.avemisclasprob : Overall probability of misclassification (weighted average of the misclassification +% probabilities over all groups). +% result.class : 'CSIMCA' +% result.x : The training data set (same as the input argument x) (only in output when p<=3). +% result.group : The group numbers of the training set (same as the input argument group) (only in output when p<=3). +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Karlien Vanden Branden +% Last Update: 05/07/2005 +% + +if nargin<2 + error('There are too few input arguments.') +end + +% assigning default-values +[n,p]=size(x); +if size(group,1)~=1 + group=group'; +end +if n ~= length(group) + error('The number of observations is not the same as the length of the group vector!') +end +g=group; +counts=tabulate(g); %contingency table (outputmatrix with 3 colums): value - number - percentage +[lev,levi,levj]=unique(g); +if ~all(counts(:,2)) %some groups have zero values, omit those groups + disp(['Warning: group(s) ', num2str(counts(counts(:,2)==0,1)'), 'are empty']); + empty=counts(counts(:,2)==0,:); + counts=counts(counts(:,2)~=0,:); +else + empty=[]; +end +ng=size(counts,1); +proportions = zeros(ng,1); +y=0; %initial values of the validation data set and its groupsvector +groupy=0; +labsd = 3; +labod = 3; +counter=1; +gamma = 0.5; +k = zeros(ng,1); +weightstrain = ones(1,n); +weightsvalid = 0; +default=struct('k',k,'method',2,'gamma',0.5,'misclassif','training',... + 'membershipprob',proportions,'valid',y,'groupvalid',groupy,'plots',1,'plotspca',0,'labsd',labsd,... + 'labod',labod,'weightstrain',weightstrain,'weightsvalid',0,'predictset',[]); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +%reading the user's input +if nargin>2 + % + %placing inputfields in array of strings + % + for j=1:nargin-2 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % + %Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) %in case of similarity + for j=1:nargin-2 %searching the index of the accompanying field + if rem(j,2)~=0 %fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end +end + +%Checking gamma +gamma = options.gamma; +if gamma >1 | gamma <0 + error('An inappropriate number for gamma is given. A correct value lies between 0 and 1.'); +end + +%Checking prior (>0 ) +prior=options.membershipprob; +if size(prior,1)~=1 + prior=prior'; +end +epsilon=10^-4; +if sum(prior) ~=0 & (any(prior < 0) | (abs(sum(prior)-1)) > epsilon) + error('Invalid membership probabilities.') +end +if length(prior)~=ng + error('The number of membership probabilities is not the same as the number of groups.') +end + +%%%%%%%%%%%%%%%%%%MAIN FUNCTION %%%%%%%%%%%%%%%%%%%%% +%Checking if a validation set is given +if strmatch(options.misclassif, 'valid','exact') + if options.valid==0 + error(['The misclassification error will be estimated through a validation set',... + 'but no validation set is given!']) + else + validx = options.valid; + validgroup = options.groupvalid; + if size(validx,1)~=length(validgroup) + error('The number of observations in the validation set is not the same as the length of its group vector!') + end + if size(validgroup,1)~=1 + validgroup = validgroup'; + end + countsvalid=tabulate(validgroup); + countsvalid=countsvalid(countsvalid(:,2)~=0,:); + if size(countsvalid,1)==1 + error('The validation set must contain observations from more than one group!') + elseif any(ismember(empty,countsvalid(:,1))) + error(['Group(s) ' ,num2str(empty(ismember(empty,countsvalid(:,1)))), 'was/were empty in the original dataset.']) + end + end + if (length(options.weightsvalid) == 1) | (length(options.weightsvalid)~=size(validx,1)) + options.weightsvalid = ones(size(validx,1),1); + end +elseif options.valid~=0 + validx = options.valid; + validgroup = options.groupvalid; + if size(validx,1) ~= length(validgroup) + error('The number of observations in the validation set is not the same as the length of its group vector!') + end + if size(validgroup,1)~=1 + validgroup = validgroup'; + end + options.misclassif='valid'; + countsvalid=tabulate(validgroup); + countsvalid=countsvalid(countsvalid(:,2)~=0,:); + if size(countsvalid,1)==1 + error('The validation set must contain more than one group!') + elseif any(ismember(empty,countsvalid(:,1))) + error(['Group(s) ' , num2str(empty(ismember(empty,countsvalid(:,1)))), ' was/were empty in the original dataset.']) + end + if (length(options.weightsvalid) == 1) | (length(options.weightsvalid)~=size(validx,1)) + options.weightsvalid = ones(size(validx,1),1); + end +end + +model.counts = counts(:,2); +model.x = x; +model.group = group; + +labsd = floor(max(0,min(options.labsd,n))); +labod = floor(max(0,min(options.labod,n))); + +%CSIMCA: +%PRINCIPAL COMPONENT ANALYSIS +%Perform PCA on each group separately: +% a) if k is not given: decide on the optimal number of components using CV. +% b) if k is given: perform cpca with the optimal number of components. + +for iClass = 1:ng + indexgroup = find(g==iClass); + groupi = x(indexgroup,:); + if options.k(iClass) == 0 + disp(['A scree plot is drawn for group ',num2str(iClass),'.']) + end + model.result{iClass} = cpca(groupi,'k',options.k(iClass),'plots',options.plotspca,... + 'labsd',labsd,'labod',labod); + model.flag(1,indexgroup) = model.result{iClass}.flag.all; +end + +%CLASSIFICATION +%Discriminant rule based on the training set x +[odsc,sdsc] = testmodel(model,x); +finalgrouptrain = assigngroup(odsc,sdsc,options.method,ng,gamma); + +if sum(prior) == 0 + result1.prior = counts(:,3)'./100; +else + result1.prior = prior; +end + +%Compute scaled orthogonal and scaled score distances for the validation set +if strmatch(options.misclassif,'valid','exact') + [odsc,sdsc] = testmodel(model,validx); + finalgroup = assigngroup(odsc,sdsc,options.method,ng,gamma); +elseif strmatch(options.misclassif,'cv','exact') %use cv + model.k = k; + [odsc,sdsc] = leave1out(model); + finalgroup = assigngroup(odsc,sdsc,options.method,ng,gamma); +end + +switch options.misclassif +case 'valid' + [v,vi,vj]=unique(validgroup); + odscgroup = []; + sdscgroup = []; + for iClass = 1:ng + indexgroup = find(validgroup == iClass); + odscgroup = [odscgroup;odsc(indexgroup,iClass)]; + sdscgroup = [sdscgroup;sdsc(indexgroup,iClass)]; + end + weightsvalid=zeros(length(odscgroup),1); + weightsvalid(((odscgroup <= 1) & (sdscgroup <= 1)))=1; + for igamma = 1:length(gamma) + for iClass=1:ng + misclas(iClass)=sum((validgroup(options.weightsvalid==1)==finalgroup(options.weightsvalid==1,igamma)') & ... + (validgroup(options.weightsvalid==1)==repmat(v(iClass),1,sum(options.weightsvalid)))); + ingroup(iClass) = sum((validgroup(options.weightsvalid == 1) == repmat(v(iClass),1,sum(options.weightsvalid)))); + end + misclas = (1 - (misclas./ingroup)); + misclasprobpergroup(igamma,:)=misclas; + misclas=misclas.*result1.prior; + misclasprob(igamma)=sum(misclas); + end +case 'training' + for igamma = 1:length(gamma) + for iClass = 1:ng + result1.misclas(iClass) = sum((group(options.weightstrain==1)==finalgrouptrain(options.weightstrain==1,igamma)')& ... + (group(options.weightstrain==1)==repmat(lev(iClass),1,sum(options.weightstrain)))); + result1.ingroup(iClass) = sum((group(options.weightstrain == 1) == repmat(lev(iClass),1,sum(options.weightstrain)))); + end + misclas = (1 - (result1.misclas./result1.ingroup)); + misclasprobpergroup(igamma,:) = misclas; + misclas = misclas.*result1.prior; + misclasprob(igamma) = sum(misclas); + end + weightsvalid=0;%only available with validation set + finalgroup = finalgrouptrain; +case 'cv' + for igamma = 1:length(gamma) + for iClass=1:ng + misclas(iClass)=sum((group(options.weightstrain==1)==finalgroup(options.weightstrain==1,igamma)') & ... + (group(options.weightstrain==1)==repmat(lev(iClass),1,sum(options.weightstrain)))); + ingroup(iClass) = sum((group(options.weightstrain == 1) == repmat(lev(iClass),1,sum(options.weightstrain)))); + end + misclas = (1 - (misclas./ingroup)); + misclasprobpergroup(igamma,:)=misclas; + misclas=misclas.*result1.prior; + misclasprob(igamma)=sum(misclas); + end + weightsvalid=0; %only available with validation set +end + +if ~isempty(options.predictset) + [odscpredict,sdscpredict] = testmodel(model,options.predictset); + finalgrouppredict = assigngroup(odscpredict,sdscpredict,options.method,ng,gamma)'; + weightspredict = max((odscpredict <= 1) & (sdscpredict <= 1),[],2)'; +else + finalgrouppredict = 0; + weightspredict = 0; +end + +%Output structure +result = struct('assignedgroup',{finalgroup'},'pca',{model.result},'method',options.method,... + 'flagtrain',{model.flag},'flagvalid',{weightsvalid'},'grouppredict',finalgrouppredict,'flagpredict',weightspredict,... + 'membershipprob',{result1.prior},'misclassif',{options.misclassif},'groupmisclasprob',{misclasprobpergroup},... + 'avemisclasprob',{misclasprob},'class',{'CSIMCA'},'x',x,'group',group); + +if size(x,2)>3 + result=rmfield(result,{'x','group'}); +end + +%Plots: +try + if options.plots + makeplot(result) + end +catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu +end + +%--------------------------- +%Leave-One-Out procedure + +function [odsc,sdsc] = leave1out(model) + +nClass = length(model.result); + + for iClass = 1:nClass + index = 1; + indexgroup = find(model.group == iClass); + teller_if_lus = 0; + groupi = model.x(indexgroup,:); + for i = 1:model.counts(iClass) + groupia = removal(groupi,i,0); + GRes = model.result; + GRes{iClass} = cpca(groupia,'k',model.result{iClass}.k,'plots',0); + %Calculate for each class the sd and the od for the observation that was left out: + for jClass = 1:nClass + dataicentered = model.x(indexgroup(index),:)-GRes{jClass}.M; + scorei = dataicentered*GRes{jClass}.P; + dataitilde = scorei*GRes{jClass}.P'; + sd(indexgroup(index),jClass) = sqrt(scorei*(diag(1./GRes{jClass}.L))*scorei'); + od(indexgroup(index),jClass) = norm(dataicentered-dataitilde); + if GRes{jClass}.cutoff.od ~= 0 + odsc(indexgroup(index),jClass) = od(indexgroup(index),jClass)/GRes{jClass}.cutoff.od; + else + odsc(indexgroup(index),jClass) = 0; + end + sdsc(indexgroup(index),jClass) = sd(indexgroup(index),jClass)/GRes{jClass}.cutoff.sd; + end + index = index + 1; + end +end + +%-------------------------------- +function [odsc,sdsc] = testmodel(model,validx); + +%Apply the given model on the test data to obtain different +%orthogonal distances and score distances. + +nClass = length(model.result); +n = size(validx,1); + +for jClass = 1:nClass + for index = 1:n + out{jClass} = model.result{jClass}; + dataicentered = validx(index,:)-out{jClass}.M; + scorei = dataicentered*out{jClass}.P; + dataitilde = scorei*out{jClass}.P'; + sd(index,jClass) = sqrt(scorei*(diag(1./out{jClass}.L))*scorei'); + od(index,jClass) = norm(dataicentered-dataitilde); + if out{jClass}.cutoff.od ~= 0 + odsc(index,jClass) = od(index,jClass)/out{jClass}.cutoff.od; + else + odsc(index,jClass) = 0; + end + sdsc(index,jClass) = sd(index,jClass)/out{jClass}.cutoff.sd; + end +end + +%------------------------- +function result = assigngroup(odsc,sdsc,method,nClass,gamma); + +%Obtain the group assignments for given od's and sd's. + +if method == 1 + sd = sdsc; + od = odsc; +elseif method == 2 + sd = sdsc.^2; + od = odsc.^2; +end + +for igamma = 1:length(gamma) + tdist = gamma(igamma).*od + (1-gamma(igamma)).*sd; + for i = 1:size(od,1) + result(i,igamma) = find(tdist(i,:) == min(tdist(i,:))); + end +end + + diff --git a/LIBRA/csimpls.m b/LIBRA/csimpls.m new file mode 100644 index 0000000..637b910 --- /dev/null +++ b/LIBRA/csimpls.m @@ -0,0 +1,223 @@ +function result=csimpls(x,y,varargin) + +%CSIMPLS performs Partial Least Squares regression using the SIMPLS +% algorithm of de Jong (1993). +% +% Reference: +% de Jong, S. (1993), +% "SIMPLS: an alternative approach to Partial Least Squares Regression", +% Chemometrics and Intelligent Laboratory Systems, 18, 251-263. +% +% Required input arguments: +% x : Data matrix of the explanatory variables +% (n observations in rows, p variables in columns) +% y : Data matrix of the response variables +% (n observations in rows, q variables in columns) +% +% Optional input arguments: +% k : Number of components to be used. +% (default = min([9,rank([x,y]),floor(n/2),p])). +% plots : If equal to one, a menu is shown which allows to draw several plots, +% such as a score outlier map and a regression outlier map. (default) +% If 'plots' is equal to zero, all plots are suppressed. +% See also makeplot.m +% +% I/O: result=csimpls(x,y,'k',10); +% +% The output of CSIMPLS is a structure containing: +% +% result.slope : Classical slope estimate +% result.int : Classical intercept estimate +% result.fitted : Classical prediction vector +% result.res : Classical residuals +% result.cov : Estimated variance-covariance matrix of the residuals +% result.M : Classical center of the matrix [X;Y] +% result.T : Classical scores +% result.weights.r : Classical simpls weights +% result.weights.p : Classical simpls weights +% result.Tcov : Classical covariance matrix of the scores +% result.k : Number of components used in the regression +% result.sd : Classical score distances +% result.od : Classical orthogonal distances +% result.resd : Residual distances (when there are several response variables). +% If univariate regression is performed, it contains the standardized residuals. +% result.cutoff : Cutoff values for the score, orthogonal and residual distances. +% result.flag : The observations whose orthogonal distance is larger than result.cutoff.od +% (orthogonal outliers => result.flag.od) and/or whose residual distance is +% larger than result.cutoff.resd (bad leverage points/vertical outliers => result.flag.resd) +% can be considered as outliers and receive a flag equal to zero (=> result.flag.all). +% The regular observations, including the good leverage points, receive a flag 1. +% result.class : 'CSIMPLS' +% +% This function is part of LIBRA: the Matlab library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +%Written by Karlien Vanden Branden +%Last Update: 05/04/2003 +%Last revision: 20/05/2008 + +[n,p1]=size(x); +[n2,q1]=size(y); +if n~=n2 + error('The response variables and the predictor variables have a different number of observations.') +end +kmin=min([9,rank(x),floor(n/2),p1]); +default=struct('plots',1,'k',kmin); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +counter=1; +if nargin>2 + % + % placing inputfields in array of strings + % + for j=1:nargin-2 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % Checking which default parameters have to be changed + % and keep them in the structure 'options'. + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) % in case of similarity + for j=1:nargin-2 % searching the index of the accompanying field + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end +end + +[xcentr,cx]=mcenter(x); +[ycentr,cy]=mcenter(y); +out.M=[cx cy];%[xcentr ycentr]; +sigmaxy=xcentr'*ycentr; +for i=1:options.k + sigmayx=sigmaxy'; + if q1>p1 + [RR,LL]=eig(sigmaxy*sigmayx); + [LL,I]=greatsort(diag(LL)); + rr=RR(:,I(1)); + qq=sigmayx*rr; + qq=qq/norm(qq); + else + if q1==1 + qq = 1; + rr = sigmaxy; + else + [QQ,LL]=eig(sigmayx*sigmaxy); + [LL,I]=greatsort(diag(LL)); + qq=QQ(:,I(1)); + rr=sigmaxy*qq; + end + end + tt=xcentr*rr; + nttc=norm(tt); + rr=rr/nttc; + tt=tt/nttc; + qq=ycentr'*tt; + uu=ycentr*qq; + pp=xcentr'*tt; + vv=pp; + if i>1 + vv = vv -v*(v'*pp); + end + vv = vv/norm(vv); + sigmaxy = sigmaxy - vv*(vv'*sigmaxy); + v(:,i)=vv; + q(:,i)=qq; + t(:,i)=tt; + u(:,i)=uu; + p(:,i)=pp; + r(:,i)=rr; +end + +%Second Stage : Classical Regression and transformation + +b=r*q'; +int=cy-cx*b; + +%classical output +out.T=t; +out.weights.p=p; +out.weights.r=r; +out.coef=[b; int]; +out.b=b; +out.int=int; +out.yhat=x*b+repmat(int,n,1); +out.res=y-out.yhat; +out.covar=cov(out.res); +if q1==1 + out.stdres=out.res./sqrt(out.covar); +end +out.k=options.k; +STTm=sum((y-repmat(mean(y),length(y),1)).^2); +SSE=sum(out.res.^2); +out.rsquare=1-SSE/STTm; +out.class='CSIMPLS'; + +%calculating classical distances in x-space +out.Tcov=cov(t); +out.sd=sqrt(mahalanobis(t,zeros(1,out.k),'cov',out.Tcov))'; +out.cutoff.sd=sqrt(chi2inv(0.975,out.k)); + +%calculating classical orthogonal distances +xt=t*p'; +tempo=xcentr-xt; +for i=1:n + out.od(i,1)=norm(tempo(i,:)); +end +r=rank(x); +if out.k~=r + m=mean(out.od.^(2/3)); + s=sqrt(var(out.od.^(2/3))); + out.cutoff.od = sqrt(norminv(0.975,m,s)^3); +else + out.cutoff.od=0; +end + +%calculating residual distances +if q1>1 + if (-log(det(out.covar)/(p1+q1-1)))>50 + out.resd='singularity'; + else + cen=zeros(q1,1); + out.resd=sqrt(mahalanobis(out.res,cen','cov',out.covar))'; + end +else %here q==1 + out.resd=out.stdres; %standardized residuals +end +out.cutoff.resd=sqrt(chi2inv(0.975,q1)); + +%Computing flags +out.flag.od=out.od<=out.cutoff.od; +out.flag.resd=abs(out.resd)<=out.cutoff.resd; +out.flag.all=(out.flag.od & out.flag.resd); + + +result=struct('slope',{out.b}, 'int',{out.int},'fitted',{out.yhat},'res',{out.res}, 'cov',{out.covar},... + 'M', {out.M},'T',{out.T} ,'weights', {out.weights},'Tcov',{out.Tcov},'k',{out.k},'sd', {out.sd},'od',{out.od},'resd',{out.resd},... + 'cutoff',{out.cutoff},'flag',{out.flag},'class',{out.class}); + +try + if options.plots + makeplot(result) + end +catch %output must be given even if plots are interrupted +end + + + + + + diff --git a/LIBRA/cvMcd.m b/LIBRA/cvMcd.m new file mode 100644 index 0000000..edfb857 --- /dev/null +++ b/LIBRA/cvMcd.m @@ -0,0 +1,167 @@ +function result = cvMcd(data,kmax,resMCD,h) + +%CVMCD calculates the robust cross-validated PRESS (predicted residual error sum of squares) +% curve for the MCD method in a fast way. +% +% Input arguments: +% data : the full data set +% kmax : the maximal number of components to be considered (mostly kmax = p). +% resMCD : the result of mcdcov(data,'plots',0,'factor',1) +% h : the quantile used in MCD. +% +% output: +% result.press : vector of length kmax with the press values +% result.weights : the weights for all observations +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Sanne Engelen +% Last Update: 01/07/2004 + +% Some initialisations: +n = size(data,1); +p = size(data,2); +r = rank(data); +Pk = []; +Lk = []; +teller_if_lus = 0; + +if nargin < 4 + alfa = 0.75; + h=floor(2*floor((n+p+1)/2)-n+2*(n-floor((n+p+1)/2))*alfa); +end + +outWeights = weightscvMcd(data,r,kmax,resMCD,h); + +w_min = outWeights.w_min; + +Hopt = resMCD.Hsubsets.Hopt; +inputH0.H0 = Hopt; +Tfull = mean(data(Hopt,:)); +Sfull = cov(data(Hopt,:)); + +for i = 1:n + % deciding which index should be removed from H0. + inputH0.same = 0; + if isempty(find(inputH0.H0 == i)) + inputH0.j = h; + if teller_if_lus >= 1 + inputH0.same = 1; + end + teller_if_lus = teller_if_lus + 1; + else + inputH0.j = find(inputH0.H0 == i); + end + + % assigning the input variables: + inputFull.T = Tfull; + inputFull.S = Sfull; + + if ~inputH0.same + res = removeObsMcd(data,i,inputH0,inputFull); + end + if (isempty(find(inputH0.H0 == i))) & (teller_if_lus == 1) + resfixed = res; + end + if isempty(find(inputH0.H0 == i)) & (teller_if_lus ~= 1) + res = resfixed; + end + + P_min_i = res.P_min_i; + L_min_i = res.L_min_i; + mu_min_i = res.mu_min_i; + + for k = 1:kmax + clear Pk Lk; + Pk = P_min_i(:,1:k); + Lk = L_min_i(1:k,1:k); + Xhoedk_min_i(i,(k-1)*p + 1:k*p) = (data(i,:) - mu_min_i)*Pk*Pk' + mu_min_i; + + if k~=r + odk(i,k) = norm(data(i,:) - Xhoedk_min_i(i,(k-1)*p + 1:k*p)); + else + odk(i,k) = 0; + end + end +end + +for k = 1:kmax + press_min(k) = 1/sum(w_min)*w_min*odk(:,k).^2; +end + +result.press = press_min; +result.weights = outWeights; + +%---------------------------------------------------------------------------------- +function out = weightscvMcd(data,r,kmax,resMCD,h) + +% computes the weights used to calculate the robust PRESS values. +% +% input: +% data : the whole data +% r : the rank of the data +% kmax : the maximal number of components to be considered +% resMCD : the result of mcdcov(data,'plots',0,'factor',1) +% h : the number of observations on which the computations are based. +% +% output: +% out.w_min : the weights computed by taken the minimum over all k + +% Some initialisations: +n = size(data,1); +p = size(data,2); +Pk = []; +Lk = []; +Tik = []; + +[P,L] = eig(resMCD.cov); +[L,I] = greatsort(diag(L)); +P = P(:,I); + +for k = 1:kmax + Pk = P(:,1:k); + if h==n + Lk=L(1:k); + else + Lk = chi2inv(h/n,k)/chi2inv(h/n,kmax/2)*L(1:k);% with correction for the factor + end + muk = resMCD.center; + Xhoedk(:,(k-1)*p + 1:k*p) = (data - repmat(muk,n,1))*Pk*Pk' + repmat(muk,n,1); + Tk = (data - repmat(muk,n,1))*Pk; + + for i =1:n + % defining the sd for the observation that is left: + sdk(i,k) = sqrt(mahalanobis(Tk(i,:),zeros(1,k),'cov',diag(Lk))); + + % defining the od for the observation that is left: + if k~=r + odk(i,k) = norm(data(i,:) - Xhoedk(i,(k-1)*p + 1:k*p)); + else + odk(i,k) = 1; + end + end + + % defining weights for odk and sdk: + if k~=r + [m,s]=unimcd(odk(:,k).^(2/3),h); + cutoff(k)=sqrt(norminv(0.975,m,s).^3); + wod(:,k) = (odk(:,k) <= cutoff(k)); + else + cutoff(k)= 0; + wod(:,k) = 1; + end + wsd(:,k) = (sdk(:,k) <= sqrt(chi2inv(0.975,k))); +end + +% determine the weights for every observation: +wk = wsd & wod; + +if size(wk,1) == 1 | size(wk,2) == 1 + w_min = wk'; +else + w_min = min(wk'); +end + +out.w_min = w_min; \ No newline at end of file diff --git a/LIBRA/cvRobpca.m b/LIBRA/cvRobpca.m new file mode 100644 index 0000000..8663362 --- /dev/null +++ b/LIBRA/cvRobpca.m @@ -0,0 +1,206 @@ +function result = cvRobpca(data,kmax,resrob,rawres,h,csteps) + +%CVROBPCA calculates the robust cross-validated PRESS (predicted residual error sum of squares) curve +% for ROBPCA in a fast way. This curve can be used to make a selection of the optimal number of +% components. The function is used in robpca.m. +% +% Input arguments: +% data : the whole data set +% kmax : the maximal number of components to be considered. +% resrob : result of robpca for k = kmax on the whole data set. +% rawres : Optional input parameter. If equal to 1 (default), then the R-PRESS is calculated +% based on the orthogonal distances of the points, that are calculated without performing the +% MCD step within ROBPCA. +% h : the quantile used in ROBPCA. +% csteps : optional : csteps.value : whether c-steps should be performed (default) or not. +% csteps.number: the number of c-steps that must be performed. +% +% Output: +% result.press : the R-PRESS values when the minimum is used to define the weights. +% result.weights : the minimum, median and smooth weights. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Sanne Engelen +% Last Update: 01/07/2004, 03/07/2006 +% Last Revision: 03/07/2006 + + +n = size(data,1); +p = size(data,2); +r = rank(data); +alfa = 0.75; +teller_if_lus = 0; + +% some initialisations +Pk_min_i = []; +Lk_min_i = []; +muk_min_i = []; +Tik = []; + +if nargin < 4 + rawres = 1; + alfa=0.75; + h = min(floor(2*floor((n+kmax+1)/2)-n+2*(n-floor((n+kmax+1)/2))*alfa),n); + csteps.value = 1; + csteps.number = 2; +elseif nargin == 4 + alfa=0.75; + h = min(floor(2*floor((n+kmax+1)/2)-n+2*(n-floor((n+kmax+1)/2))*alfa),n); + csteps.value = 1; + csteps.number = 2; +elseif nargin == 5 + csteps.value = 1; + csteps.number = 2; +else + csteps.value = csteps.value; + if csteps.value + csteps.number = csteps.number; + end +end + +outWeights = weightscvRobpca(data,r,resrob,kmax,h); +result.weights = outWeights; +w_min = outWeights.w_min; + +% inputH0 +H0 = resrob.Hsubsets.H0; +same.value = 0; + +for i = 1:n + same.value = 0; + if isempty(find(H0 == i)) + if teller_if_lus >= 1 + same.value = 1; + end + teller_if_lus = teller_if_lus + 1; + end + + if ~rawres + % inputH1 + H1 = resrob.Hsubsets.H1; + + % Hfreq + Hfreq = resrob.Hsubsets.Hfreq; + + Hsets = [H0;H1;Hfreq]; + factor = 1; + Hsets_min_i = RemoveObsHsets(Hsets,i); + res = removeObsRobpca(data,i,kmax,Hsets_min_i,same,factor,csteps); + else + data_min_i = removal(data,i,0); + H0_min_i = RemoveObsHsets(H0,i); + if ~same.value + [Pk_min_i,TH0,LH0_min_i,rH0_min_i,centerX,muk_min_i]=kernelEVD(data_min_i(H0_min_i,:)); + res.Pk_min_i = Pk_min_i; + res.muk_min_i = muk_min_i; + else + res = same.res; + Pk_min_i = res.Pk_min_i; + muk_min_i = res.muk_min_i; + end + end + + if isempty(find(H0 == i)) + same.res = res; + end + + Pkmax_min_i = res.Pk_min_i; + muk_min_i = res.muk_min_i; + + for k = 1:kmax + xhoed_k(i,(k-1)*p + 1:k*p) = (data(i,:) - muk_min_i)*Pkmax_min_i(:,1:k)*Pkmax_min_i(:,1:k)' + muk_min_i; + if k~=r + odk(i,k) = norm(data(i,:) - xhoed_k(i,(k-1)*p + 1:k*p)); + else + odk(i,k) = 0; + end + end +end + +for k = 1:kmax + press_min(k) = 1/sum(w_min)*w_min*odk(:,k).^2; +end + +result.press = press_min; +%----------------------------------------------------------------------------------------------------------- +function Hsets_min_i = RemoveObsHsets(Hsets,i) + +% removes the right index from the $h$-subsets in Hsets to +% obtain (h - 1)-subsets. +% every h-subset is put as a row in Hsets. +% i is the index of the observation that is removed from the whole data. + +for r = 1:size(Hsets,1) + if ~isempty(find(Hsets(r,:)== i)) + Hsets_min_i(r,:) = removal(Hsets(r,:),0,find(Hsets(r,:) == i)); + else + Hsets_min_i(r,:) = Hsets(r,1:(end-1)); + end + + for j = 1:length(Hsets_min_i(r,:)) + if Hsets_min_i(r,j) > i + Hsets_min_i(r,j) = Hsets_min_i(r,j) - 1; + end + end +end +%----------------------------------------------------------------------------------------------------------- +function out = weightscvRobpca(data,r,resrob,kmax,h) + +% computes the weights needed for the calculation of the R-PRESS. +% +% input: +% data : the whole data set. +% r : the rank of the data set. +% resrob: the result of robpca on the whole data set for k = kmax. +% kmax : the maximal number of components to be considered. +% h : (n-h+1) measures the number of outliers the algorithm should +% resist. Any value between n/2 and n may be specified. +% +% output: +% out.w_min : the minimum weights + + +odk = []; +n = size(data,1); +p = size(data,2); + +% Determine fixed weights: +for k = 1:kmax + muk = resrob.M; + xhoed_k(:,(k-1)*p + 1:k*p) = (data - repmat(muk,n,1))*resrob.P(:,1:k)*resrob.P(:,1:k)' + repmat(muk,n,1); + + for i = 1:n + % defining the od for each k + if k~=r + odk(i,k) = norm(data(i,:) - xhoed_k(i,(k-1)*p + 1:k*p)); + else + odk(i,k) = 0; + end + end + + % defining weights for odk: + if k~=r + [m,s]=unimcd(odk(:,k).^(2/3),h); + cutoff(k)=sqrt(norminv(0.975,m,s).^3); + wod(:,k) = (odk(:,k) <= cutoff(k)); + else + cutoff(k) = 0; + wod(:,k) = 1; + end +end + +% determine the weights for every observation: +wk = wod; + +if size(wk,1) == 1 || size(wk,2) == 1 + w_min = wk'; +else + w_min = min(wk,[],2)'; +end + +out.w_min = w_min; +out.odk = odk; +out.cutoff = cutoff; \ No newline at end of file diff --git a/LIBRA/cvRpcr.m b/LIBRA/cvRpcr.m new file mode 100644 index 0000000..377aca1 --- /dev/null +++ b/LIBRA/cvRpcr.m @@ -0,0 +1,553 @@ +function result = cvRpcr(x,y,kmax,rmsecv,h,k) + +%CVRPCR calculates the robust RMSECV (root mean squared error of cross-validation) curve +% for RPCR or the robust RMSEP (root mean squared error of prediction) value in a fast way. +% The R-RMSECV curve can be used to make a selection of the optimal number of +% components to include in the regression model. The function is used in rpcr.m. +% +% Input arguments: +% x : the explanatory variables +% y : the response variables +% kmax : the maximal number of components to be considered in the model. +% rmsecv : Optional. If equal to 1 (default), the rmsecv is computed. +% Else, rmsecv = 0 and then the rss and R2 are computed. +% h : the quantile used in RPCR +% k : optional, if equal to zero (default), the RMSECV is calculated. +% If different from 0, the RMSEP value is calculated. +% Output: +% If rmsecv = 1 +% result.rmsecv : the r-rmsecv values (only if the RMSECV is computed) +% result.rmsep : the rmsep value (only if RMSEP is computed) +% result.residu : the residuals for every k = 1,...,kmax +% result.outWeights : the fixed weights used in the robust version of the RMSE. +% result.R2 : the coefficient of determination for every k. +% result.rss : the RSS values for every k. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Sanne Engelen +% Last Update: 08/10/2004, 03/07/2006 +% Last Revision: 03/07/2006 + +% some initialisations +n = size(x,1); +p = size(x,2); +q = size(y,2); +teller_if_lus = 0; +cutoffWeights = sqrt(chi2inv(0.975,q)); + +if nargin < 4 + alfa=0.75; + h=floor(2*floor((n+kmax+2)/2)-n+2*(n-floor((n+kmax+2)/2))*alfa); + k = 0; + rmsecv = 1; +elseif nargin == 4 + alfa=0.75; + h=floor(2*floor((n+kmax+2)/2)-n+2*(n-floor((n+kmax+2)/2))*alfa); + k = 0; +elseif nargin == 5 + k = 0; +end + +if q == 1 + FixedWeights = weightscvLtsregres(x,y,kmax,h,k); +else + FixedWeights = weightscvMcdregres(x,y,kmax,h,k,cutoffWeights); +end +kmax=FixedWeights.kmax; + +if rmsecv + weights = FixedWeights.weightsk; + resrob = FixedWeights.resrob; + + if k == 0 + w_min = FixedWeights.w_min; + end + if size(resrob.Hsubsets.H0,2)==1 + resrob.Hsubsets.H0=resrob.Hsubsets.H0'; + end + Hsets = [FixedWeights.Hsets;resrob.Hsubsets.H0;resrob.Hsubsets.H1;resrob.Hsubsets.Hfreq]; + + for i = 1:n + disp(['observation ',num2str(i),' is left out']) + x_min_i = removal(x,i,0); + y_min_i = removal(y,i,0); + + same.value = 0; + if isempty(find(resrob.Hsubsets.H0 == i)) + if teller_if_lus >= 1 + same.value = 1; + end + teller_if_lus = teller_if_lus + 1; + end + + % constructing Hsets of right size: h - 1. + Hsets_min_i = RemoveObsHsets(Hsets,i); + + if k == 0 + res = removeObsRobpca(x,i,kmax,Hsets_min_i,same,1); + else + res = removeObsRobpca(x,i,k,Hsets_min_i,same); + end + + if isempty(find(resrob.Hsubsets.H0 == i)) + same.res = res; + end + + Prob_min_i = res.Pk_min_i; + murob_min_i = res.muk_min_i; + + Tk_min_i = []; + Lk_min_i = []; + coeffk_min_i = []; + + if k == 0 + j_ind = kmax; + else + j_ind = k; + end + + Tkmax_min_i = (x_min_i - repmat(murob_min_i,n-1,1))*Prob_min_i(:,1:j_ind); + + if q == 1 + [resLts2,rawLts2] = ltsregres(Tkmax_min_i,y_min_i,'plots',0,'Hsets',Hsets_min_i,'h',h-1); + else + if k == 0 + Mcdres = mcdcov([Tkmax_min_i,y_min_i],'h',h-1,'plots',0,'Hsets',Hsets_min_i); + % the full mcdregres is not needed, we only need the results of mcdcov. + resMcd2.Mu = Mcdres.center'; + resMcd2.Sigma = Mcdres.cov; + end + end + + if k == 0 + j_start1 = 1; + j_end1 = kmax; + else + j_start1 = k; + j_end1 = k; + end + + for j = j_start1:j_end1 + if k == 0 + if q == 1 + [Bk,intk,weightslts] = extractlts(rawLts2,x_min_i, y_min_i, murob_min_i, Prob_min_i,kmax,j,h-1,n-1); + else + Tk_min_i = (x_min_i - repmat(murob_min_i,n-1,1))*Prob_min_i(:,1:j); + [Bk,intk,sigmayykmaxrew_k,sigmattkmaxrew_k] = extractmcdregres(resMcd2,Tk_min_i,y_min_i,kmax,n-1,q,j,h-1,cutoffWeights); + coeffk = [Bk;intk]; + end + else + if q == 1 + Bk = resLts2.slope; + intk = resLts2.int; + weightslts = resLts2.flag; + else + resMcdregres = mcdregres(Tkmax_min_i,y_min_i,'Hsets',Hsets_min_i,'plots',0,'h',h-1); + Bk = resMcdregres.slope; + intk = resMcdregres.int; + coeffk = [Bk;intk]; + end + end + finalB = Prob_min_i(:,1:j)*Bk; + finalInt = intk - murob_min_i*finalB; + yhat_min_i = x(i,:)*finalB + finalInt; + residk_min_i(i,(j - 1)*q + 1:j*q) = y(i,:) - yhat_min_i; + + % calculation of the resd: + if q > 1 + if k == 0 + rewE2=sigmayykmaxrew_k-coeffk(1:j,1:q)'*sigmattkmaxrew_k*coeffk(1:j,1:q); + cen=zeros(q,1); + resd(i,j)=sqrt(mahalanobis(residk_min_i(i,(j - 1)*q + 1:j*q),cen','cov',rewE2))'; %robust distances of residuals + else + resd(i,j) = sqrt(mahalanobis(residk_min_i(i,(j - 1)*q + 1:j*q),zeros(q,1),'cov',resMcdregres.cov)); + end + else + if k == 0 + scale=sqrt(sum(weightslts.*residk_min_i(i,(j - 1)*q + 1:j*q).^2)/(sum(weightslts)-1)); + resd(i,j) = residk_min_i(i,(j-1)*q + 1:j*q)/scale; + else + resd(i,j) = residk_min_i(i,(j-1)*q + 1:j*q)/resLts2.scale; + end + end + end + end + + if k == 0 + for j = 1:kmax + resk = residk_min_i(:,(j-1)*q + 1:j*q); + if q == 1 + rmsecv_min(j) = sqrt(1/sum(w_min)*w_min*(resk).^2); + else + rmsecv_min(j) = sqrt(1/sum(w_min)*w_min*(mean((resk').^2))'); + end + end + result.rmsecv = rmsecv_min; + result.residu = residk_min_i; + else + weights = weights(:,k); + if q == 1 + rmsep = sqrt(1/sum(weights)*weights'*(residk_min_i(:,k)).^2); + else + rmsep = sqrt(1/sum(weights)*weights'*(mean((residk_min_i(:,(k-1)*q + 1:k*q)').^2))'); + end + result.rmsep = rmsep; + result.residu = residk_min_i(:,(k-1)*q + 1:k*q); + end +end + +result.outWeights = FixedWeights; +result.rss = FixedWeights.rss; +result.R2 = FixedWeights.R2; + +%------------------------------------------------------------------------- +function Hsets_min_i = RemoveObsHsets(Hsets,i) + +% Removes the correct index from the $h$-subsets in Hsets to +% obtain (h - 1)-subsets. +% Every h-set is put as a row in Hsets. +% i is the index of the observation that is removed from the whole data. + +for r = 1:size(Hsets,1) + if ~isempty(find(Hsets(r,:)== i)) + Hsets_min_i(r,:) = removal(Hsets(r,:),0,find(Hsets(r,:) == i)); + else + Hsets_min_i(r,:) = Hsets(r,1:(end-1)); + end + + for j = 1:length(Hsets_min_i(r,:)) + if Hsets_min_i(r,j) > i + Hsets_min_i(r,j) = Hsets_min_i(r,j) - 1; + end + end +end +%------------------------------------------------------------------------ +function out = weightscvLtsregres(x,y,kmax,h,k) + +% Computes the weights needed in the R-RMSECV function. +% +% Input arguments: +% x : the independent variables +% y : the response variables +% kmax : the maximal number of components to be considered. +% h : the number of observations on which the calculations are based. +% k : if zero (default), the kmax approach is used (for RMSECV). +% Else robpca is calculated for a specific k (for RMSEP). +% +% Output arguments: +% if k = 0 (RMSECV): +% out.w_min : the weights obtained by taking the minimum over all k +% out.weightsk : the weights for every observation and every k (n x kmax). +% out.resrob : the results of robpca on x for kmax components. +% if k ~= 0 (RMSEP): +% out.weightsk : the weights for a specific k +% out.resrob : the results of robpca on x for k components. +% out.R2 : the weighted Rsquared for each value of k +% out.rss : the weighted rss for each value of k + + +n = size(x,1); +p = size(x,2); +q = size(y,2); + +if nargin < 5 + k = 0; +end + +if k == 0 + ResRobWhole = robpca(x,'plots',0,'kmax',kmax,'h',h,'k',kmax); +else + ResRobWhole = robpca(x,'plots',0,'kmax',kmax,'h',h,'k',k); +end +kmax = ResRobWhole.kmax; +Trob = ResRobWhole.T; +Mrob = ResRobWhole.M; +Prob = ResRobWhole.P; +HsetsH0 = ResRobWhole.Hsubsets.H0; +HsetsH1 = ResRobWhole.Hsubsets.H1; +HsetsHfreqrob = ResRobWhole.Hsubsets.Hfreq; + +[ResLtsWhole,RawLtsWhole]=ltsregres(Trob,y,'plots',0,'h',h); + +HsetsHfreq = ResLtsWhole.Hsubsets.Hfreq; + +if k == 0 + j_start = 1; + j_end = kmax; +else + j_start = k; + j_end = k; +end + +for j = j_start:j_end + if k == 0 + if j ~= kmax + [Bk,intk,weightslts,HsetsHopt(j,:)] = extractlts(RawLtsWhole,x,y,Mrob,Prob,kmax,j,h,n); + else + [Bk,intk,weightslts] = extractlts(RawLtsWhole,x,y,Mrob,Prob,kmax,j,h,n); + HsetsHopt(kmax,:) = ResLtsWhole.Hsubsets.Hopt'; + end + else + Bk = ResLtsWhole.slope; + intk = ResLtsWhole.int; + weightslts = ResLtsWhole.flag; + HsetsHopt = ResLtsWhole.Hsubsets.Hopt'; + end + coeffk=[Bk;intk]; + finalB = Prob(:,1:j)*Bk; + finalInt = intk - Mrob*finalB; + fitted=x*finalB + repmat(finalInt,n,1); + residuals(:,j)=y-fitted; + scale=sqrt(sum(weightslts.*residuals(:,j).^2)/(sum(weightslts)-1)); + s0=scale; + weightsk(:,j)=abs(residuals(:,j)/scale)<=sqrt(chi2inv(0.975,1)); +end + +% Determining fixed weights. +if k == 0 + if kmax == 1 + w_min = weightsk'; + else + w_min = min(weightsk'); + end + out.w_min = w_min; + + yw = mean(y(w_min == 1,:)); + y2=(y-repmat(yw,n,1)).^2; + R = residuals.^2; + D=sum(y2(w_min==1,:)); + for j = 1:kmax + R1=R(w_min==1,j); + rss(j) = sum(sum(R1)); + R2(j)=1-rss(j)/sum(D); + end + out.rss = 1/sum(w_min)*rss; + out.R2 = R2; +else + yw = mean(y(weightsk(:,k) == 1,:)); + y2=(y-repmat(yw,n,1)).^2; + R = residuals.^2; + D=sum(y2(weightsk(:,k)==1,:)); + R1=R(weightsk(:,k)==1,:); + rss(k) = sum(sum(R1)); + R2(k)=1-rss(k)/sum(D); + out.rss = 1/sum(weightsk(:,k))*rss(k); + out.R2 = R2(k); +end +out.weightsk = weightsk; +out.resrob = ResRobWhole; +if size(HsetsH0,2)==1 + HsetsH0=HsetsH0'; +end +out.kmax=kmax; +out.Hsets = [HsetsHopt;HsetsHfreq;HsetsH0;HsetsH1;HsetsHfreqrob]; +%----------------------------------------------------------------------------- +function [Bk,intk,weightslts,Hopt] = extractlts(rawltskmax, x_min_i, y_min_i, murob_min_i, Prob_min_i,kmax,k,h,n) + +% This function extracts lts-regression coefficients for a certain k based on +% the slope and intercept of the regression with kmax coefficients. +% +% Input arguments: +% rawltskmax : the raw results of ltsregression with kmax components. +% x_min_i : the regressors without observation i. +% y_min_i : the dependent variable without observation i. +% murob_min_i : the center estimated using robpca on the data minus observation i. +% Prob_min_i : the loadings matrix estimated using robpca on the data minus observation i. +% k : the number of components +% +% Output arguments: +% Bk : the slope for k components. +% intk : the intercept for k components. +% weightslts : the weights needed for the reweighting. +% Hopt : the optimal h-subset. Not availabe for kmax. + +j = k; +coeffkraw_min_i = rawltskmax.coefficients; +sloperaw = coeffkraw_min_i(1:j,:); +intraw = coeffkraw_min_i(end,:); +Tk_min_i = (x_min_i - repmat(murob_min_i,n,1))*Prob_min_i(:,1:j); + +% perform csteps on the raw estimates of the parameters: +prevobj = 0; +if j ~= kmax + for noCsteps = 1:10 + residu = y_min_i - Tk_min_i*sloperaw - repmat(intraw,n,1); + [sortresid,indsr] = sort(residu.^2); + obj = sum(sortresid(1:h)); + [Q,R] = qr([Tk_min_i(indsr(1:h),:) ones(h,1)],0); + z = R\(Q'*y_min_i(indsr(1:h),1)); + sloperaw = z(1:j,:); + intraw = z(j+1,:); + if noCsteps >= 20 | abs(obj - prevobj) < 10^(-4) + if noCsteps >= 20 + disp('no convergence in Csteps') + end + break + end + prevobj = obj; + end + Hopt = indsr(1:h)'; +end + +% reweighting: +factor = rawconsfactorlts(h,n); +residu = y_min_i - Tk_min_i*sloperaw - repmat(intraw,n,1); +[sortresid,indsr] = sort(residu.^2); +sh0 = sqrt(1/(h)*sum(sortresid(1:h))); +s0 = sh0*factor; +m = 2*(n)/asvarscalekwad((h),(n)); +quantile = tinv(0.9875,m); +weightslts = abs(residu/s0) <= quantile; +[Q,R] = qr([Tk_min_i(weightslts == 1,:) ones(sum(weightslts),1)],0); +z = R\(Q'*y_min_i(weightslts == 1)); +Bk = z(1:j,:); +intk = z(j+1,:); +%---------------------------------------------------------------------------------------------------------------------- +function out = weightscvMcdregres(x,y,kmax,h,k,cutoffWeights) + +% computes the weights needed in the R-PRESS function. +% +% Input arguments: +% x : the independent variables +% y : the response variables +% kmax : the maximal number of components to be considered. +% h : the number of observations on which the calculations are based. +% k : if zero (default), the kmax approach is used then (for RMSECV). +% Else robpca is calculated for a specific k (for RMSEP). +% +% Output arguments: +% if k = 0 (RMSECV): +% out.w_min : the weights obtained by taking the minimum over all k +% out.weightsk : the weights for every observation and every k (n x kmax). +% out.resrob : the results of robpca on [X,Y] for kmax components. +% if k ~= 0 (RMSEP): +% out.weightsk : the weights for a specific k +% out.resrob : the results of robpca on [X,Y] for k components. +% out.R2 : the weighted Rsquared for each value of k +% out.rss : the weighted rss for each value of k + +n = size(x,1); +p = size(x,2); +q = size(y,2); + +if nargin < 5 + k = 0; +end + +if k == 0 + ResRobWhole = robpca(x,'plots',0,'k',kmax,'kmax',kmax,'h',h); +else + ResRobWhole = robpca(x,'plots',0,'k',k,'kmax',kmax,'h',h); +end +kmax=ResRobWhole.kmax; +Trob = ResRobWhole.T; +Prob = ResRobWhole.P; +Mrob = ResRobWhole.M; +HsetsH0 = ResRobWhole.Hsubsets.H0; +HsetsH1 = ResRobWhole.Hsubsets.H1; +HsetsHfreqrob = ResRobWhole.Hsubsets.Hfreq; + +ResMCDWhole = mcdregres(Trob,y,'h',h,'plots',0); +HsetsHfreq = ResMCDWhole.Hsubsets.Hfreq; + +if k == 0 + for j = 1:kmax + if j ~= kmax + if k == 0 + Tk = (x - repmat(Mrob,n,1))*Prob(:,1:j); + [Bk,intk,sigmayykmaxrew_k,sigmattkmaxrew_k,HsetsHopt(j,:)] = extractmcdregres(ResMCDWhole,Tk,y,kmax,n,q,j,h,cutoffWeights); + else + Tk = (x - repmat(Mrob,n,1))*Prob(:,1:j); + [Bk,intk,sigmayykmaxrew_k,sigmattkmaxrew_k,HsetsHopt(j,:)] = extractmcdregres(ResMCDWhole,Tk,y,j,n,q,j,h,cutoffWeights); + end + else + [Bk,intk,sigmayykmaxrew_k,sigmattkmaxrew_k] = extractmcdregres(ResMCDWhole,Trob,y,kmax,n,q,j,h,cutoffWeights); + HsetsHopt(kmax,:) = ResMCDWhole.Hsubsets.Hopt; + end + coeffk = [Bk;intk]; + finalB = Prob(:,1:j)*Bk; + finalInt = intk - Mrob*finalB; + yhat = x*finalB + repmat(finalInt,n,1); + residu(:,(j-1)*q+1:j*q) = y - yhat; + + cen=zeros(q,1)'; + cov=sigmayykmaxrew_k - coeffk(1:j,1:q)'*sigmattkmaxrew_k*coeffk(1:j,1:q); + [nn,pp]=size(residu(:,(j-1)*q+1:j*q)); + resd = sqrt(mahalanobis(residu(:,(j-1)*q+1:j*q),cen,'cov',cov))'; + weightsk(:,j) = (abs(resd)<=cutoffWeights); + end +else + ResMCDWhole = mcdregres(Trob(:,1:k),y,'h',h,'plots',0); + if size(ResMCDWhole.flag,2)~=1 + ResMCDWhole.flag = ResMCDWhole.flag'; + end + + weightsk(:,k) = ResMCDWhole.flag; + + HsetsHfreq = ResMCDWhole.Hsubsets.Hfreq; + HsetsHopt = ResMCDWhole.Hsubsets.Hopt; +end + + +% Determining fixed weights. + +if k == 0 + if kmax == 1 + w_min = weightsk'; + else + w_min = min(weightsk'); + end + out.w_min = w_min; + + yw = mean(y(w_min == 1,:)); + y2=(y-repmat(yw,n,1)).^2; + R = residu.^2; + D=sum(y2(w_min==1,:)); + for j = 1:kmax + R1=R(w_min==1,(j-1)*q+1:j*q); + rss(j) = sum(sum(R1)); + R2(j)=1-rss(j)/sum(D); + end + out.rss = 1/(q*sum(w_min))*rss; + out.R2 = R2; +else + yw = mean(y(weightsk(:,k) == 1,:)); + y2=(y-repmat(yw,n,1)).^2; + R = ResMCDWhole.res.^2; + D=sum(y2(weightsk(:,k)==1,:)); + R1=R(weightsk(:,k)==1,:); + rss = sum(sum(R1)); + R2=1-rss/sum(D); + out.rss = 1/(q*sum(weightsk(:,k)))*rss; + out.R2 = R2; +end +out.weightsk = weightsk; +out.resrob = ResRobWhole; +if size(HsetsH0,2)==1 + HsetsH0=HsetsH0'; +end +out.kmax=kmax; +out.Hsets = [HsetsHopt;HsetsHfreq;HsetsH0;HsetsH1;HsetsHfreqrob]; + +%--------------------------------------------------------------- +function rawconsfaclts=rawconsfactorlts(quan,n) + +rawconsfaclts=(1/sqrt(1-((2*n)/(quan*(1/norminv((quan+n)/(2*n)))))*... + normpdf(1/(1/(norminv((quan+n)/(2*n))))))); +%-------------------------------------------------------------- + +function asvar=asvarscalekwad(quan,n) + +alfa=quan/n; +alfa=1-alfa; +qalfa=chi2inv(1-alfa,1); +c2=gamcdf(qalfa/2,1/2+1); +c1=1/c2; +c3=3*gamcdf(qalfa/2,1/2+2); +asvar=qalfa*(1-alfa)-c2; +asvar=asvar^2; +asvar=(c3-2*qalfa*c2+(1-alfa)*(qalfa^2))-asvar; +asvar=c1^2*asvar; \ No newline at end of file diff --git a/LIBRA/cvRsimpls.m b/LIBRA/cvRsimpls.m new file mode 100644 index 0000000..68b3281 --- /dev/null +++ b/LIBRA/cvRsimpls.m @@ -0,0 +1,483 @@ +function result = cvRsimpls(x,y,kmax,rmsecv,h,k) + +%CVRIMPLS calculates the robust RMSECV (root mean squared error of cross-validation) curve +% for RSIMPLS or the robust RMSEP(root mean squared error of prediction) value in a fast way. +% The R-RMSECV curve can be used to make a selection of the optimal number of +% components to include in the regression model. The function is used in rsimpls.m. +% +% Input arguments: +% x : the explanatory variables +% y : the rsponse variables +% kmax : maximal number of variables to include in the model. +% rmsecv : Optional. If equal to 1 (default), the rmsecv is computed. +% Else, rmsecv = 0 and then the rss and R2 are computed. +% h : optional input argument. +% k : optional input argument. If k = 0 (default) then RMSECV is calculated. Else the RMSEP wil be computed. +% +% Output: +% if RMSECV is computed: +% result.rmsecv : the R-RMSECV values (obtained with the minimum weights). +% if RMSEP is computed: +% result.rmsep : the R-RMSEP values +% result.rss : the RSS values for every k. +% result.R2 : the coefficient of determination for every k. +% result.residu : the residuals for every k = 1,...,kmax +% result.outWeights : the weights used to compute the robust R-RMSEP values. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Sanne Engelen +% Last Update: 08/10/2004, 03/07/2006 +% Last Revision: 03/07/2006 + + +% some initialisations +n = size(x,1); +p = size(x,2); +q = size(y,2); +r = rank(x); +rz = rank([x,y]); +teller_if_lus = 0; +cutoffWeights = sqrt(chi2inv(0.975,q)); + +if nargin < 4 + alfa = 0.75; + kmaxr=min([kmax+q,rz]); + h=floor(2*floor((n+kmaxr+1)/2)-n+2*(n-floor((n+kmaxr+1)/2))*alfa); + k = 0; + rmsecv = 1; +elseif nargin == 4 + alfa = 0.75; + kmaxr=min([kmax+q,rz]); + h=floor(2*floor((n+kmaxr+1)/2)-n+2*(n-floor((n+kmaxr+1)/2))*alfa); + k = 0; +elseif nargin == 5 + k = 0; +end + +outWeights = weightcvRsimpls(x,y,kmax,h,k,cutoffWeights); + +if rmsecv + if k == 0 + w_min = outWeights.w_min; + end + rob = outWeights.ResRob; + + % Assigning the input variables + if size(rob.Hsubsets.H0,2)==1 + rob.Hsubsets.H0=rob.Hsubsets.H0'; + end + Hsets = [rob.Hsubsets.H0;rob.Hsubsets.H1;rob.Hsubsets.Hfreq]; + same.value = 0; + data = [x,y]; + + for i = 1:n + disp(['observation ',num2str(i),' is left out']) + X_min_i = removal(x,i,0); + Y_min_i = removal(y,i,0); + data_min_i = removal(data,i,0); + + same.value = 0; + if isempty(find(rob.Hsubsets.H0 == i)) + if teller_if_lus >= 1 + same.value = 1; + end + teller_if_lus = teller_if_lus + 1; + end + + % constructing Hsets of right size: h - 1. + Hsets_min_i = RemoveObsHsets(Hsets,i); + + if k == 0 + res = removeObsRobpca(data,i,kmax + q,Hsets_min_i,same,1); + else + res = removeObsRobpca(data,i,k + q,Hsets_min_i,same); + end + + if isempty(find(rob.Hsubsets.H0 == i)) + same.res = res; + end + + Prob_min_i = res.Pk_min_i; + Lrob_min_i = res.Lk_min_i; + murob_min_i = res.muk_min_i; + Trob_min_i = (data_min_i - repmat(murob_min_i,n-1,1))*Prob_min_i; + + % Computing weights corresponding with the ROBPCA results. + sdrob_min_i = sqrt(mahalanobis(Trob_min_i,zeros(1,size(Trob_min_i,2)),'invcov',1./Lrob_min_i))'; + + if k == 0 + cutoff.sd=sqrt(chi2inv(0.975,kmax)); + else + cutoff.sd = sqrt(chi2inv(0.975,k)); + end + + % Orthogonal distances to robust PCA subspace + XRc=data_min_i-repmat(murob_min_i,n-1,1); + Xtilde=Trob_min_i*Prob_min_i'; + Rdiff=XRc-Xtilde; + for j=1:(n-1) + odrob_min_i(j,:)=norm(Rdiff(j,:)); + end + % Robust cutoff-value for the orthogonal distance + if k == 0 + test_k = kmax; + else + test_k = k; + end + + if test_k~=r + [m,s]=unimcd(odrob_min_i.^(2/3),h); + cutoff.od = sqrt(norminv(0.975,m,s).^3); + wrob_min_i = (odrob_min_i<=cutoff.od)&(sdrob_min_i<=cutoff.sd); + else + cutoff.od=0; + wrob_min_i = (sdrob_min_i<=cutoff.sd); + end + + % start the deflation: + xycentr_min_i = []; + sigmax_min_i = []; + xcentr_min_i = []; + sigmaxy_min_i = []; + sigmayx_min_i = []; + + xycentr_min_i = murob_min_i; + sigmax_min_i = Prob_min_i(1:p,:)*diag(Lrob_min_i)*Prob_min_i(1:p,:)'; + xcentr_min_i = X_min_i - repmat(murob_min_i(1:p),n-1,1); + sigmaxy_min_i = Prob_min_i(1:p,:)*diag(Lrob_min_i)*Prob_min_i(p+1:p+q,:)'; + sigmayx_min_i = sigmaxy_min_i'; + + % calculation of the scores. + nScores = 1; + R_min_i = []; + T_min_i = []; + P_min_i = []; + V_min_i = []; + + if k == 0 + countScores = kmax; + else + countScores = k; + end + + while nScores <= countScores + if q == 1 + qq_min_i = 1; + else + [QQ,LL] = eig(sigmayx_min_i*sigmaxy_min_i); + [LL,I] = greatsort(diag(LL)); + qq_min_i = QQ(:,I(1)); + end + + rr_min_i = sigmaxy_min_i*qq_min_i; + rr_min_i = rr_min_i/norm(rr_min_i); + tt_min_i = xcentr_min_i*rr_min_i; + pp_min_i = sigmax_min_i*rr_min_i/(rr_min_i'*sigmax_min_i*rr_min_i); + vv_min_i = pp_min_i; + + if nScores > 1 + vv_min_i = vv_min_i - V_min_i*(V_min_i'*pp_min_i); + end + vv_min_i = vv_min_i./norm(vv_min_i); + sigmaxy_min_i = sigmaxy_min_i - vv_min_i*(vv_min_i'*sigmaxy_min_i); + + V_min_i(:,nScores) = vv_min_i; + T_min_i(:,nScores) = tt_min_i; + R_min_i(:,nScores) = rr_min_i; + P_min_i(:,nScores) = pp_min_i; + + nScores = nScores + 1; + end + + if k == 0 + outRegr = runRegr(x,y,i,T_min_i,Y_min_i,R_min_i,murob_min_i,wrob_min_i,kmax,cutoffWeights); + for j = 1:kmax + Tk_min_i = T_min_i(:,1:j); + geg = [Tk_min_i,Y_min_i]; + + outRegr.Mu = outRegr.center'; + outRegr.Sigma = outRegr.sigma; + [Bk,intk,sigmayykmaxrew_k,sigmattkmaxrew_k] = extractmcdregres(outRegr,Tk_min_i,Y_min_i,kmax,n-1,q,j,h-1,cutoffWeights); + coeffk = [Bk;intk]; + b_min_i = R_min_i(:,1:j)*coeffk(1:j,:); + int_min_i = coeffk(j+1,:) - murob_min_i(1:p)*R_min_i(:,1:j)*coeffk(1:j,:); + Yhat_min_i = x(i,:)*b_min_i + int_min_i; + resid_min_i(i,(j-1)*q + 1:j*q) = y(i,:) - Yhat_min_i; + + % calculation of the resd: + rewE2=sigmayykmaxrew_k- coeffk(1:j,1:q)'*sigmattkmaxrew_k*coeffk(1:j,1:q); + + if q > 1 + cov = rewE2; + cen=zeros(q,1); + resd(i,j)=sqrt(mahalanobis(resid_min_i(i,(j-1)*q + 1:j*q),cen','cov',cov))'; %robust distances of residuals + else + scale = sqrt(rewE2); + resd(i,j) = resid_min_i(i,(j-1)*q + 1:j*q)/scale; + end + end + else + outRegr = runRegr(x,y,i,T_min_i,Y_min_i,R_min_i,murob_min_i,wrob_min_i,k,cutoffWeights); + resid_min_i(i,:) = outRegr.resid_min_i; + resd(i,:) = outRegr.resd'; + end + end + + if k == 0 + for j = 1:kmax + resk = resid_min_i(:,(j-1)*q + 1:j*q); + if q == 1 + rmsecv(j) = sqrt(1/sum(w_min)*w_min*(resk).^2); + else + rmsecv(j) = sqrt(1/sum(w_min)*w_min*(mean((resk').^2))'); + end + end + result.rmsecv = rmsecv; + result.residu = resid_min_i; + else + weights = outWeights.weightsk; + if q == 1 + rmsep = sqrt(1/sum(weights)*weights'*(resid_min_i).^2); + else + rmsep = sqrt(1/sum(weights)*weights'*(mean((resid_min_i').^2))'); + end + result.rmsep = rmsep; + result.residu = resid_min_i; + end +end + +result.outWeights = outWeights; +result.rss = outWeights.rss; +result.R2 = outWeights.R2; + +%------------------------------------------------------------------ +function out = runRegr(x,y,i,T_min_i,Y_min_i,R_min_i,mukmax_min_i,wkmax_min_i,k,cutoffWeights) + +[n,p] = size(x); +[n,q] = size(y); + +% perform the robpca regression: +breg = []; +b_min_i = []; +int_min_i= []; +Yhat_min_i = []; +robpcareg = robpcaregres(T_min_i(:,1:k),Y_min_i,wkmax_min_i',cutoffWeights); +out.center = robpcareg.center; +out.sigma = robpcareg.sigma; +breg = robpcareg.coeffs(1:k,:); +b_min_i = R_min_i(:,1:k)*breg; +int_min_i = robpcareg.coeffs(k+1,:) - mukmax_min_i(1:p)*R_min_i(:,1:k)*breg; +Yhat_min_i = x(i,:)*b_min_i + int_min_i; +resid_min_i = y(i,:) - Yhat_min_i; + +% calculation of the resd: +if q > 1 + cov = robpcareg.cov; + cen=zeros(q,1); + resd=sqrt(mahalanobis(resid_min_i,cen','cov',cov))'; %robust distances of residuals +else + scale = sqrt(robpcareg.cov); + resd = resid_min_i/scale; +end + +out.resid_min_i = resid_min_i; +out.resd = resd; +%---------------------------------------------------------------------------------------- +function out = weightcvRsimpls(x,y,kmax,h,k,cutoffWeights) + +% Computes the weights for the robust RMSECV/RMSEP values. +% +% input: +% x : the independent variables. +% y : the response variables. +% kmax : the maximal number of components to be considered. +% h : the number of observations on which the calculations are based. +% k : if equal to zero, robpca is performed on kmax components (case RMSECV). (default). +% Else, robpca is performed for a certain number of components (case RMSEP) +% +% output: +% out.w_min : the weights obtained by taking the minimum over all k +% out.weightsk : the weights for all observations and all k (n x kmax) +% out.resrob : the results of robpca on [x,y]. +% out.R2 : the weighted Rsquared for each value of k +% out.rss : the weighted rss for each value of k + +n = size(x,1); +p = size(x,2); +q = size(y,2); +r = rank(x); + +if nargin < 5 + k = 0; +end + +if k == 0 + ResRob = robpca([x,y],'plots',0,'k',kmax + q,'kmax',kmax + q,'h',h); +else + ResRob = robpca([x,y],'plots',0,'k',k + q,'kmax',kmax + q,'h',h); +end + +Trob = ResRob.T; +Prob = ResRob.P; +Lrob = ResRob.L; +murob = ResRob.M; +wrob = ResRob.flag.all; + +%deflation + +xycentr = []; +sigmax = []; +xcentr = []; +sigmaxy = []; +sigmayx = []; + +xycentr = murob; +sigmax = Prob(1:p,:)*diag(Lrob)*Prob(1:p,:)'; +xcentr = x - repmat(murob(1:p),n,1); +sigmaxy = Prob(1:p,:)*diag(Lrob)*Prob(p+1:p+q,:)'; +sigmayx = sigmaxy'; + +% calculation of the scores. +nScores = 1; +R = []; +T = []; +P = []; +V = []; + +if k == 0 + countScores = kmax; +else + countScores = k; +end + +while nScores <= countScores + if q == 1 + qq = 1; + else + [QQ,LL] = eig(sigmayx*sigmaxy); + [LL,I] = greatsort(diag(LL)); + qq = QQ(:,I(1)); + end + + rr = sigmaxy*qq; + rr = rr/norm(rr); + tt = xcentr*rr; + pp = sigmax*rr/(rr'*sigmax*rr); + vv = pp; + + if nScores > 1 + vv = vv - V*(V'*pp); + end + vv = vv./norm(vv); + sigmaxy = sigmaxy - vv*(vv'*sigmaxy); + + V(:,nScores) = vv; + T(:,nScores) = tt; + R(:,nScores) = rr; + P(:,nScores) = pp; + + nScores = nScores + 1; +end + +if k == 0 + outRobRegr = robpcaregres(T(:,1:kmax),y,wrob'); + + for j = 1:kmax + outRobRegr.Mu = outRobRegr.center'; + outRobRegr.Sigma = outRobRegr.sigma; + [Bk,intk,sigmayykmaxrew_k,sigmattkmaxrew_k] = extractmcdregres(outRobRegr,T(:,1:j),y,kmax,n,q,j,h,cutoffWeights); + coeffk = [Bk;intk]; + finalB = R(:,1:j)*coeffk(1:j,:); + finalInt = coeffk(j+1,:) - murob(1:p)*R(:,1:j)*coeffk(1:j,:); + Yhat = x*finalB + repmat(finalInt,n,1); + resid(:,(j-1)*q+1:j*q) = y - Yhat; + + % calculation of the rd: + rewE2=sigmayykmaxrew_k- coeffk(1:j,1:q)'*sigmattkmaxrew_k*coeffk(1:j,1:q); + + if q > 1 + cov = rewE2; + cen=zeros(q,1); + resd = sqrt(mahalanobis(resid(:,(j-1)*q+1:j*q),cen','cov',cov))'; %robust distances of residuals + weightsk(:,j) = (abs(resd)<=cutoffWeights); + else + scale = sqrt(rewE2); + resd = resid(:,(j-1)*q+1:j*q)/scale; + weightsk(:,j) = (abs(resd)<=cutoffWeights); + end + end +else + outRobRegr = robpcaregres(T(:,1:k),y,wrob'); + + % robust residual distance: + if q==1 + resd=outRobRegr.resids/sqrt(outRobRegr.cov); + else + resd=sqrt(mahalanobis(outRobRegr.resids,zeros(1,q),'cov',outRobRegr.cov))'; + end + + weightsk = (abs(resd)<=cutoffWeights); +end + +if k == 0 + if kmax == 1 + w_min = weightsk'; + else + w_min = min(weightsk'); + end + + out.w_min = w_min; + out.weightsk = weightsk; + + yw = mean(y(w_min == 1,:)); + y2=(y-repmat(yw,n,1)).^2; + R = resid.^2; + D=sum(y2(w_min==1,:)); + for j = 1:kmax + R1=R(w_min==1,(j-1)*q+1:j*q); + rss(j) = sum(sum(R1)); + R2(j)=1-rss(j)/sum(D); + end + + out.rss = 1/(q*sum(w_min))*rss; + out.R2 = R2; +else + out.weightsk = weightsk; + + s=sum(weightsk); + yw=sum(y(weightsk==1,:))/s; + y2=(y-repmat(yw,n,1)).^2; + R = outRobRegr.resids.^2; + D=sum(y2(weightsk==1,:)); + R1=R(weightsk==1,:); + rss = sum(sum(R1)); + R2=1-rss/sum(D); + + out.rss = 1/(q*sum(weightsk))*rss; + out.R2 = R2; +end +out.ResRob = ResRob; +%--------------------------------------------------------------------------------------- +function Hsets_min_i = RemoveObsHsets(Hsets,i) + +% removes the right index from the $h$-subsets in Hsets to +% obtain (h - 1)-subsets. +% every h-set is put as a row in Hsets. +% i is the index of the observation that is removed from the whole data. + +for r = 1:size(Hsets,1) + if ~isempty(find(Hsets(r,:)== i)) + Hsets_min_i(r,:) = removal(Hsets(r,:),0,find(Hsets(r,:) == i)); + else + Hsets_min_i(r,:) = Hsets(r,1:(end-1)); + end + + for j = 1:length(Hsets_min_i(r,:)) + if Hsets_min_i(r,j) > i + Hsets_min_i(r,j) = Hsets_min_i(r,j) - 1; + end + end +end \ No newline at end of file diff --git a/LIBRA/daisy.m b/LIBRA/daisy.m new file mode 100644 index 0000000..cb86fc6 --- /dev/null +++ b/LIBRA/daisy.m @@ -0,0 +1,94 @@ +function result = daisy(x,vtype,metric) + +%DAISY returns a row vector containing all the pairwise dissimilarities +% (distances) between observations in the dataset. The original +% variables may be of mixed types. +% +% The calculation of dissimilarities is explained in: +% Kaufman, L. and Rousseeuw, P.J. (1990), +% "Finding groups in data: An introduction to cluster analysis", +% Wiley-Interscience: New York (Series in Applied Probability and +% Statistics), ISBN 0-471-87876-6. +% +% Required input arguments: +% x : Data matrix (rows = observations, columns = variables) +% vtype : Variable type vector (length equals number of variables) +% Possible values are 1 Asymmetric binary variable (0/1) +% 2 Nominal variable (includes symmetric binary) +% 3 Ordinal variable +% 4 Interval variable +% +% Optional input arguments: +% metric : Metric to be used +% Possible values are 0: Mixed (not all interval variables, default) +% 1: Euclidean (all interval variables, default) +% 2: Manhattan +% +% I/O: +% result=daisy(x,vtype,1) +% +% Example: +% load flower.mat +% result=daisy(flower,[2 2 1 2 3 3 4]); +% +% The output of DAISY is a structure containing: +% result.dys : dissimilarities vector (read row by row from the +% lower dissimilarity matrix) +% result.metric : metric used +% result.number : number of observations +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Guy Brys and Wai Yan Kong (May 2006) + +%Checking and filling in the inputs +if (nargin<2) + error('Two input arguments required') +elseif (nargin<3) + if (sum(vtype)~=4*size(x,2)) + metric = 'mixed'; + metri = 0; + else + metric = 'euclidean'; + metri = 1; + end +end +% defining metric (for 4 input arguments and diss) +if (nargin==3) + if (metric==1) + metri=1; + metric='euclidean'; + elseif (metric==2) + metri=2; + metric='manhattan'; + elseif (metric==0) + metri=0; + metric='mixed'; + else + error('metric must be 0,1 or 2') + end +end + +%Standardizing in case of mixed metric +if (sum(vtype)~=4*size(x,2)) + colmin = min(x); + colextr = max(x)-colmin; + colextr(colextr==0)=1; + x = (x - repmat(colmin,size(x,1),1))./repmat(colextr,size(x,1),1); +end + +%Replacement of missing values +jtmd = repmat(1,1,size(x,2))-2*(sum(isnan(x))>0); +valmisdat = min(min(x))-0.5; +x(isnan(x)) = valmisdat; +valmd = repmat(valmisdat,1,size(x,2)); + +%Actual calculations +disv=daisyc(size(x,1),size(x,2),x,valmd,jtmd,vtype,metri); + +%Putting things together +result = struct('dys',disv,'metric',metric,'number',size(x,1)); + + diff --git a/LIBRA/daisyc.mexw32 b/LIBRA/daisyc.mexw32 new file mode 100644 index 0000000000000000000000000000000000000000..012ecc00f0e310859490d78ee4370178b88ec40e GIT binary patch literal 8192 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 + error(['Only 6 groups can be drawn with different symbols. Please adapt the code',... + ' of daplot.m if you want to draw more groups.']) +else + legstr=[]; + for i=1:m + data{i}=x(group==lev(i),:); + plot(data{i}(:,1),data{i}(:,2),marking{i}); + hold on + legstr=[legstr; sprintf(['Group',num2str(i)])]; + end + for i=1:m + if strcmp(method,'linear') + Z{i} = ellipse(center(i,:),covar); + elseif strcmp(method,'quadratic') + Z{i} = ellipse(center(i,:),covar{i}); + end + plot(Z{i}(:, 1), Z{i}(:, 2), linecolor{i}) + hold on + end +end +if strmatch('CDA',classic,'exact') & strmatch('linear',method,'exact') + title('Classical linear discriminant analysis') +elseif strmatch('CDA',classic,'exact') & strmatch('quadratic',method,'exact') + title('Classical quadratic discriminant analysis') +elseif strmatch('RDA',classic,'exact') & strmatch('linear',method,'exact') + title('Robust linear discriminant analysis') +elseif strmatch('RDA',classic,'exact') & strmatch('quadratic',method,'exact') + title('Robust quadratic discriminant analysis') +end +hold off +legend(legstr,0) + +%-------------- +function out = ellipse(loc,covar) + +A=covar; +detA = A(1, 1)*A(2, 2) - A(1, 2)^2; +dist = sqrt(chi2inv(0.975, 2)); +ylimit = sqrt(A(2, 2)) * dist; +y = - ylimit:0.01*ylimit:ylimit; +sqroot.discr = sqrt(detA/A(2, 2).^2 * (A(2, 2) * dist.^2 - y.^2)); +sqroot.discr([1, length(sqroot.discr)]) = 0; +b = loc(1) + A(1, 2)/A(2, 2) * y; +x1 = b - sqroot.discr; +x2 = b + sqroot.discr; +y = loc(2) + y; +out= [[x1' y']; flipud([x2' y'])]; + diff --git a/LIBRA/ddplot.m b/LIBRA/ddplot.m new file mode 100644 index 0000000..c09f6e0 --- /dev/null +++ b/LIBRA/ddplot.m @@ -0,0 +1,46 @@ +function ddplot(x,y,cutoff,attrib,nid) + +%DDPLOT is the distance-distance plot as introduced by Rousseeuw and Van +% Zomeren (1990, JASA, 85, 633-639). The Robust distances based on the MCD (mcdcov.m) +% are plotted against the Mahalanobis distances. Cutoff lines permit the +% classification of outliers. +% +% Required input arguments: +% x : a vector containing the mahalanobis distances +% y : a vector containing the robust distances +% cutoff : the cutoff value for the distances +% +% Optional input arguments: +% nid : number of points to be identified in plots +% (Default value: 3) +% +% I/O: ddplot(x,y,cutoff,nid) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Last update: 24/11/2003 + +set(gcf,'Name', 'Distance-distance plot', 'NumberTitle', 'off'); +if nargin==3 + nid=3; +end +ymax=max([max(y),cutoff,2.5])*1.05; +xmax=max([max(x),cutoff,2.5])*1.05; +plot(x,y,'o') +xlabel('Mahalanobis distance'); +ylabel('Robust distance'); +title(attrib) +xlim([-0.01*xmax,xmax]); +ylim([-0.01*ymax,ymax]); +box on +plotnumbers(x,y,0,nid,1); +line(repmat(max([cutoff,2.5]),1,2),[-0.01*ymax,ymax],'Color','r'); +line([-0.01*xmax,xmax],repmat(max([cutoff,2.5]),1,2),'Color','r'); +hold on +plot([-0.01*xmax,min([xmax,ymax])],[-0.01*ymax,min([xmax,ymax])],':','Color','g'); +hold off + + + diff --git a/LIBRA/diana.m b/LIBRA/diana.m new file mode 100644 index 0000000..a9a0e6c --- /dev/null +++ b/LIBRA/diana.m @@ -0,0 +1,197 @@ +function result = diana(x,vtype,stdize,metric,plots) + +%DIANA is a divisive clustering algorithm. It returns a hierarchy of clusters. +% +% The algorithm is fully described in: +% Kaufman, L. and Rousseeuw, P.J. (1990), +% "Finding groups in data: An introduction to cluster analysis", +% Wiley-Interscience: New York (Series in Applied Probability and +% Statistics), ISBN 0-471-87876-6. +% +% Required input arguments: +% x : Data matrix (rows = observations, columns = variables) +% or Dissimilarity matrix (if number of rows equals 1) +% vtype : Variable type vector (length equals number of variables) +% Possible values are 1 Asymmetric binary variable (0/1) +% 2 Nominal variable (includes symmetric binary) +% 3 Ordinal variable +% 4 Interval variable +% (if x is a dissimilarity matrix, vtype is not required) +% +% Optional input arguments: +% stdize : standardise the variables given by the x-matrix +% Possible values are 0 : no standardisation (default) +% 1 : standardisation by the mean +% 2 : standardisation by the median +% (if x is a dissimilarity matrix, stdize is ignored) +% metric : Metric used to calculate the dissimilarity matrix +% Possible values are 0 : Mixed (not all interval variables, default) +% 1 : Euclidian (all interval variables, default) +% 2 : Manhattan +% (if x is a dissimilarity matrix, metric is ignored) +% plots : draws figures +% Possible values are 0 : do not create a banner and a cluster tree (default) +% 1 : create a banner and a cluster tree +% I/O: +% result=diana(x,vtype,metric,stdize,plots) +% +% Example: +% load agricul.mat +% result=diana(agricul,[4 4],0,0,1); +% +% The output of DIANA is a structure containing: +% result.x : inputmatrix x (only given if x is not a +% dissimilarity matrix) +% result.diss : text saying whether the inputmatrix x is a dissimilarity matrix +% or not +% result.dys : calculated dissimilarities (read row by row from the +% lower dissimilarity matrix, without the elements of +% the diagonal) +% result.metric : metric used +% result.stdize : standardisation used +% result.number : number of observations +% result.objectorder : order of objects +% result.heights : diameter of cluster before deviding it +% (= length of banner) +% result.dc : divisive coefficient +% result.merge : a (n-1) by 2 matrix related to the merge +% +% And DIANA will create a banner and a cluster tree if plots equals 1. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Wai Yan Kong +% Created on 05/2006 +% Last Revision: 19/09/2006 + +%Checking and filling in the inputs +if (nargin<1) + error('One input argument required (data or dissimilarity matrix)') +elseif ((nargin<2) & (size(x,1)~=1)) + error('Two input arguments required (datamatrix x and vtype)') + % so, only datamatrix x as input +elseif (nargin<2) + metric ='unknown'; + metri=1; + stdize = 0; + plots = 0; + % so, only dissim matrix x as input +elseif (nargin<3) + stdize = 0; + plots = 0; + if (sum(vtype)~=4*size(x,2)) + metri =0; + metric='mixed'; + else + metri = 1; + metric='euclidean'; + end + % so, only datamatrix or dissimilarity matrix x and vtype + % as input +elseif (nargin<4) + plots = 0; + if (sum(vtype)~=4*size(x,2)) + metri =0; + metric='mixed'; + else + metri =1; + metric='euclidean'; + end + % so, only datamatrix or dissimilarity matrix x, vtype and + % stdize as input +elseif (nargin<5) + plots = 0; +elseif (nargin>5) + error('Too many input arguments') +end + +% defining metric (for 4 input arguments) and diss +if (nargin>=4) + if (metric==1) + metri=1; + metric='euclidean'; + elseif (metric==2) + metri=2; + metric='manhattan'; + elseif (metric==0) + metri=0; + metric='mixed'; + else + error('metric must be 0,1 or 2') + end +end + +if ((size(x,1)~=1)) + diss=0; + dissi='x is no dissimilarity matrix'; +else + diss=1; + dissi='x is a dissimilarity matrix'; +end + +%Standardization +if (stdize==1) & (metri==1 | metri==2)& diss==0 + x = ((x - repmat(mean(x),size(x,1),1))./(repmat(std(x),size(x,1),1))); + standardisation='standardisation by mean'; +elseif (stdize==2) & (metri==1 | metri==2) & diss==0 + x = ((x - repmat(median(x),size(x,1),1))./(repmat(mad(x),size(x,1),1))); + standardisation='standardisation by median'; +elseif(stdize==0) + standardisation='no standardisation'; +elseif (stdize==1 | stdize==2) + standardisation='no standardisation (not enough num var or x is a diss matrix)'; +elseif (nargin<=2) + standardisation='no standardisation'; +else + error('stdize must be 0,1 or 2'); +end + +% defining dissimilarity matrix and number +if (diss==1) + disv=x; + number=(1+sqrt(1+8*size(x,2)))/2; %number of observations + % checking for missing values in the dissimilarity matrix + if any(isnan(disv)) + error('There are missing value(s) in the dissimilarity matrix!') + end + % checking the dimensions of the dissimilarity matrix + if mod(number,fix(number))~=0 + error(['The dimension of the dissimilarity matrix is not correct!']) + end +else + resl=daisy(x,vtype,metri); + disv=resl.dys; + number=size(x,1); +end + +%Actual calculations +[ner,ban,coef,merge,dys]=twinsc(number,[0 disv]',1,2); + +% We want ban to be a vector of length n-1 +ban(1)=[]; + +% We want merge to be a (n-1) by 2 matrix +merge2=ones(number-1,2); +for i = 1:(number-1) + merge2(i,:) = merge(2*i-1:2*i); +end + +%Putting things together +result = struct('x',x,'diss',dissi,'dys',dys,'metric',metric,... + 'stdize',standardisation,'number',number,... + 'objectorder',ner,'heights',ban,'dc',coef,'merge',merge2,... + 'class','DIANA'); +if diss + result=rmfield(result, 'x'); +end + +% Plots +try + if plots + makeplot(result,'classic',0) + end +catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu +end diff --git a/LIBRA/distplot.m b/LIBRA/distplot.m new file mode 100644 index 0000000..1ac2182 --- /dev/null +++ b/LIBRA/distplot.m @@ -0,0 +1,44 @@ +function distplot(y,cutoff,class,nid) + +%DISTPLOT plots the vector y versus the index. +% At the height of the cutoff value a red vertical line is plotted. +% +% Required input arguments: +% y : the vector to be plotted +% cutoff : a cutoff value +% +% Optional input arguments: +% class : the class of the y-vector +% nid : number of points to be identified (Default value: 3) +% +% I/O: distplot(y,cutoff,class,nid) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Last update: 24/11/2003 + +set(gcf,'Name', 'Index plot of the distances', 'NumberTitle', 'off'); +n=length(y); +if nargin==2 + laby='Distance'; + nid=3; +elseif nargin==3 + nid=3; +end +ymax=max([max(y),cutoff,2.5])*1.05; +plot(1:n,y,'o') +box on +xlabel('Index') +if strcmp(class,'MCDCOV') + laby='Robust distance'; +elseif strcmp(class,'COV') + laby='Mahalanobis distance'; +end +ylabel(laby) +xlim([-0.025*n,n*1.05]); +ylim([-0.025*ymax,ymax]); +plotnumbers(1:n,y,0,nid,1) +line([-0.025*n,n*1.05],repmat(max([cutoff,2.5]),1,2),'Color','r'); +title([class]); \ No newline at end of file diff --git a/LIBRA/ellipsplot.m b/LIBRA/ellipsplot.m new file mode 100644 index 0000000..ae15173 --- /dev/null +++ b/LIBRA/ellipsplot.m @@ -0,0 +1,76 @@ +function ellipsplot(center,covar,data,dist,nid,labx,laby) + +%ELLIPSPLOT plots the 97.5% tolerance ellipse of the bivariate data set +% (data). The ellipse is defined by those data points whose distance (dist) +% is equal to the squareroot of the 97.5% chisquare quantile with 2 degrees of +% freedom. +% +% Required input arguments: +% center : estimate of the center of the data set +% covar : estimate of the covariance matrix of the data set +% data : the two-dimensional data matrix +% dist : distance needed to flag data points outside the ellipse +% +% Optional input arguments: +% nid : number of data points with largest distance +% to be identified (default value: 3) +% labx : a label for the x axis (default value: 'X1') +% laby : a label for the y axis (default value: 'X2') +% +% I/O: ellipsplot(center,covar,data,dist,nid,labx,laby) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Last update: 23/10/2003 + +set(gcf,'Name', '97.5% Tolerance ellipse', 'NumberTitle', 'off'); +n=length(dist); +if size(data,2)~=2 + disp('The tolerance ellipse is only drawn for two-dimensional data sets') +else + if nargin==4 + nid=3; + labx='X1'; + laby='X2'; + elseif nargin==5 + labx='X1'; + laby='X2'; + elseif nargin==6 + laby='X2'; + end + deter=covar(1,1)*covar(2,2)-covar(1,2)^2; + ylimit=sqrt(7.37776*covar(2,2)); + y=-ylimit:0.005*ylimit:ylimit; + sqtdi=sqrt(deter*(ylimit^2-y.^2))/covar(2,2); + sqtdi([1,end])=0; + b=center(1)+covar(1,2)/covar(2,2)*y; + x1=b-sqtdi; + x2=b+sqtdi; + y=center(2)+y; + ellip=[x1,x2([end:-1:1]);y,y([end:-1:1])]'; + xmin=min([data(:,1);ellip(:,1)]); + xmax=max([data(:,1);ellip(:,1)]); + ymin=min([data(:,2);ellip(:,2)]); + ymax=max([data(:,2);ellip(:,2)]); + xmarg=0.05*abs(xmax-xmin); + ymarg=0.05*abs(ymax-ymin); + xmin=xmin-xmarg; + xmax=xmax+xmarg; + ymin=ymin-ymarg; + ymax=ymax+ymarg; + x1=data(:,1)'; + x2=data(:,2)'; + plot(x1,x2,'o') + xlabel(labx) + ylabel(laby) + xlim([xmin,xmax]); + ylim([ymin,ymax]); + box on + [dist,ind]=sort(dist); + ind=ind(n-nid+1:n)'; + text(x1(ind),x2(ind),int2str(ind)); + title('Tolerance ellipse (97.5%)'); + line(ellip(:,1),ellip(:,2),'Color','r'); + end \ No newline at end of file diff --git a/LIBRA/extractmcdregres.m b/LIBRA/extractmcdregres.m new file mode 100644 index 0000000..07d7374 --- /dev/null +++ b/LIBRA/extractmcdregres.m @@ -0,0 +1,113 @@ +function [Bk,intk,sigmayykmaxrew_k,sigmattkmaxrew_k,Hopt] = extractmcdregres(resMcd,T,y,kmax,n,q,k,h,cutoffWeights) + +%EXTRACTMCDREGRES is an auxiliary function for cross-validation with RPCR and RSIMPLS. +% It extracts mcd-regression coefficients for a certain k based on +% the raw estimate of the slope and intercept of the regression with kmax coefficients. + +% Input arguments: +% resMCD : the results of the mcdregression on the data minus observation i for kmax components. +% kmax : the maximal number of components +% T : the k scores from ROBPCA +% k : the number of components. +% +% Output : +% Bk : the slope for k components. +% intk : the intercept for k components. +% Hopt : remark that only for k < kmax a hopt is provided. For kmax Hopt needs to be calculated outside this function. +% sigmayykmaxrew_k and sigmattkmaxrew_k : estimates of covariance matrices that are needed in other functions. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Sanne Engelen +% Last Update: 08/10/2004 + + +geg = [T,y]; +j = k; + +mutkmaxraw_k = resMcd.Mu(1:j); +muykmaxraw_k = resMcd.Mu((kmax+1):(kmax + q)); +sigmattkmaxraw_k = resMcd.Sigma(1:j,1:j); +sigmaytkmaxraw_k = resMcd.Sigma((kmax+1):(kmax + q),1:j); +sigmatykmaxraw_k = resMcd.Sigma(1:j,(kmax + 1):(kmax + q)); +sigmayykmaxraw_k = resMcd.Sigma((kmax + 1):(kmax + q),(kmax + 1):(kmax + q)); + +% perform csteps: +Sigmaraw_k = [sigmattkmaxraw_k, sigmatykmaxraw_k ; sigmaytkmaxraw_k, sigmayykmaxraw_k]; +Muraw_k = [mutkmaxraw_k;muykmaxraw_k]'; + +prevdet = 0; +if j ~= kmax + geg_k = [T(:,1:j),y]; + for noCsteps = 1:10 + [sortMD,indsm] = sort(mahalanobis(geg_k,Muraw_k,'cov',Sigmaraw_k)); + Sigmaraw_k = cov(geg_k(indsm(1:h),:)); + Muraw_k = mean(geg_k(indsm(1:h),:)); + obj = det(Sigmaraw_k); + if noCsteps >= 20 |abs(obj - prevdet) < 10^(-4) + if noCsteps >= 20 + disp('no convergence in csteps') + end + break + end + prevdet = obj; + end + Hopt = indsm(1:h); +end + +mukmax_k = Muraw_k; +mutkmax_k = Muraw_k(1:j)'; +muykmax_k = Muraw_k((j+1):(j + q))'; +sigmakmax_k = Sigmaraw_k; +sigmattkmax_k = Sigmaraw_k(1:j,1:j); +sigmaytkmax_k = Sigmaraw_k((j+1):(j + q),1:j); +sigmatykmax_k = Sigmaraw_k(1:j,(j + 1):(j + q)); +sigmayykmax_k = Sigmaraw_k((j + 1):(j + q),(j + 1):(j + q)); + +sigmattinvkmax_k = inv(sigmattkmax_k); +Braw_k = sigmattinvkmax_k*sigmatykmax_k; +intraw_k = (muykmax_k - (sigmaytkmax_k*sigmattinvkmax_k*mutkmax_k))'; + +% reweighting: +rewweights=zeros(n,1); +rewfitted=T(:,1:j)*Braw_k + repmat(intraw_k,n,1); +rewresid=y-rewfitted; +rewE=sigmayykmax_k-Braw_k'*sigmattkmax_k*Braw_k; +for noObs=1:n + if (sqrt(rewresid(noObs,1:q)*inv(rewE)*rewresid(noObs,1:q)')) <= cutoffWeights + rewweights(noObs)=1; + end +end + +rewclasscov=cov(geg(rewweights==1,:)); +rewclasscenter=mean(geg(rewweights==1,:)); + +mukmaxrew_k = rewclasscenter; +mutkmaxrew_k = rewclasscenter(1:j)'; +muykmaxrew_k = rewclasscenter((j+1):(j + q))'; +sigmakmaxrew_k = rewclasscov; +sigmattkmaxrew_k = rewclasscov(1:j,1:j); +sigmaytkmaxrew_k = rewclasscov((j+1):(j + q),1:j); +sigmatykmaxrew_k = rewclasscov(1:j,(j + 1):(j + q)); +sigmayykmaxrew_k = rewclasscov((j + 1):(j + q),(j + 1):(j + q)); + +sigmattinvkmaxrew_k = inv(sigmattkmaxrew_k); +Bk = sigmattinvkmaxrew_k*sigmatykmaxrew_k; +intk = (muykmaxrew_k - (sigmaytkmaxrew_k*sigmattinvkmaxrew_k*mutkmaxrew_k))';% +rewclasscov=cov(geg(rewweights==1,:)); +rewclasscenter=mean(geg(rewweights==1,:)); + +mukmaxrew_k = rewclasscenter; +mutkmaxrew_k = rewclasscenter(1:j)'; +muykmaxrew_k = rewclasscenter((j+1):(j + q))'; +sigmakmaxrew_k = rewclasscov; +sigmattkmaxrew_k = rewclasscov(1:j,1:j); +sigmaytkmaxrew_k = rewclasscov((j+1):(j + q),1:j); +sigmatykmaxrew_k = rewclasscov(1:j,(j + 1):(j + q)); +sigmayykmaxrew_k = rewclasscov((j + 1):(j + q),(j + 1):(j + q)); + +sigmattinvkmaxrew_k = inv(sigmattkmaxrew_k); +Bk = sigmattinvkmaxrew_k*sigmatykmaxrew_k; +intk = (muykmaxrew_k - (sigmaytkmaxrew_k*sigmattinvkmaxrew_k*mutkmaxrew_k))'; \ No newline at end of file diff --git a/LIBRA/fanny.m b/LIBRA/fanny.m new file mode 100644 index 0000000..3f33eb8 --- /dev/null +++ b/LIBRA/fanny.m @@ -0,0 +1,167 @@ +function result = fanny(x,kclus,vtype,metric,plots) + +%FANNY is a fuzzy clustering algorithm. It returns a list representing a fuzzy clustering of the data +% into kclus clusters. +% +% The algorithm is fully described in: +% Kaufman, L. and Rousseeuw, P.J. (1990), +% "Finding groups in data: An introduction to cluster analysis", +% Wiley-Interscience: New York (Series in Applied Probability and +% Statistics), ISBN 0-471-87876-6. +% +% Required input arguments: +% x : Data matrix (rows = observations, columns = variables) +% or Dissimilarity matrix (if number of columns equals 1). The +% dissimilarity vector should be obtained by reading row by row from the +% lower dissimilarity matrix. This can be the result from the +% function 'daisy'. +% kclus : The number of desired clusters +% vtype : Variable type vector (length equals number of variables) +% Possible values are 1 Asymmetric binary variable (0/1) +% 2 Nominal variable (includes symmetric binary) +% 3 Ordinal variable +% 4 Interval variable +% (if x is a dissimilarity matrix vtype is not required.) +% +% Optional input arguments: +% metric : Metric to be used +% Possible values are 0: Mixed (not all interval variables, default) +% 1: Euclidean (all interval variables, default) +% 2: Manhattan +% (if x is a dissimilarity matrix, metric is ignored) +% plots : draws figures +% Possible values are 0 : do not create any plot (default) +% 1 : create a silhouette plot and a clusplot +% +% I/O: +% result=fanny(x,kclus,vtype,metric,plots) +% +% Example: +% load country.mat +% result=fanny(country,2,[4 4],1,0); +% makeplot(result) +% or: +% result=fanny(country,2,[4,4],1,1); +% +% The output of FANNY is a structure containing: +% result.dys : dissimilarities (read row by row from the +% lower dissimilarity matrix) +% result.metric : metric used +% result.number : number of observations +% result.pp : Membership coefficients for each observation +% result.coeff : Dunn's partition coefficient (and normalized version) +% result.ncluv : A vector with length equal to the number of observations, +% giving for each observation the number of the cluster to +% which it has the largest membership +% result.obj : Objective function and the number of iterations the +% fanny algorithm needed to reach this minimal value +% result.sylinf : Matrix, with for each observation i the cluster to +% which i belongs, as well as the neighbor cluster of i +% (the cluster, not containing i, for which the average +% dissimilarity between its observations and i is minimal), +% and the silhouette width of i. The last column +% contains the original object number. +% +% FANNY will create the silhouette plot and the clusplot if plots equals 1 +% (an empty bar indicated by zero in the silhouette plot is a sparse +% between two clusters). +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Guy Brys and Wai Yan Kong (May 2006) +% Last update: March 2009 + +%Checking and filling in the inputs +res1=[]; +if (nargin<2) + error('Two input arguments required') +elseif (nargin<3) && (size(x,2)~=1 & size(x,1)~=1) + error('Three input arguments required') +elseif (nargin<3) + if (size(x,2)==1) + x = x'; + end + res1.metric = 'unknown'; + res1.dys = x; + lookup=seekN(x); + res1.number = lookup.numb; %(1+sqrt(1+8*size(x,1)))/2; + plots = 0; +elseif (nargin<4) + plots = 0; + if (sum(vtype)~=4*size(x,2)) + metri=0; + metric = 'mixed'; + else + metri=1; + metric = 'euclidean'; + end +elseif (nargin<5) + plots = 0; +end + +% defining metric (for 4 input arguments) and diss +if (nargin>=5) + if (metric==1) + metri=1; + metric='euclidean'; + elseif (metric==2) + metri=2; + metric='manhattan'; + elseif (metric==0) + metri=0; + metric='mixed'; + else + error('metric must be 0,1 or 2') + end +end + + +%Calculating the dissimilarities with daisy +%For fanny the second command is also required +if (isempty(res1)) + res1=daisy(x,vtype,metri); +end +res1.dys=res1.dys(lowertouppertrinds(res1.number)); + +%Actual calculations +[pp,coeff,clu,obj,sylinf]=fannyc(res1.number,kclus,[0 res1.dys]'); + +%Putting things together +result = struct('dys',res1.dys,'metric',res1.metric,... + 'number',res1.number,'pp',pp,... + 'coeff',coeff,'ncluv',clu,'obj',obj,'sylinf',sylinf,'x',x,'class','FANNY'); + +% Plots +try + if plots + makeplot(result,'classic',0) + end +catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu +end +%------------ +%SUBFUNCTIONS + +function dv = lowertouppertrinds(n) + +dv=[]; +for i=0:(n-2) + dv = [dv cumsum(i:(n-2))+repmat(1+sum(0:i),1,n-i-1)]; +end + +%--- +function outn = seekN(x) + +ok=0; +numb=0; +k=size(x,2); +sums=cumsum(1:k); +for i=1:k + if(sums(i)==k) + numb=i+1; + ok=1; + end +end +outn=struct('numb',numb,'ok',ok); diff --git a/LIBRA/fannyc.mexw32 b/LIBRA/fannyc.mexw32 new file mode 100644 index 0000000000000000000000000000000000000000..88382385afa3d4d7e384cce4bb0bce5919b171b6 GIT binary patch literal 11776 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 I|0@>w4-}Nj-T(jq literal 0 HcmV?d00001 diff --git a/LIBRA/flower.mat b/LIBRA/flower.mat new file mode 100644 index 0000000000000000000000000000000000000000..f230042c5f51e7939c4eaa65285a37d0c80b7efc GIT binary patch literal 263 zcmeZu4DoSvQZUssQ1EpO(M`+DN!3vZ$Vn_o%P-2cQV4Jk_w>_Ia4t$sEJ;mK$j`G< z2+1f_a4bz%Ffvy#G_*3Zurjq&FfuT(R3I5JFnap(GcYjJ198P1Q?H$Zha7mWyxMp+ z{lMcVRs~K@sz;8rbx5Yo5pa|Y=T;36vzidTxqnMm=Zm*%=J-}L9=&Iot-fgYy?6bm zU5?C)7jfMv`9=E7^a@9bf1Z(7u3Y5_2uy3-!O?t|dxPkkioGkh=89d-y7^Z3>7O_K Vhi4o+ywjd{{+y0|cg}L%005|UUGD$@ literal 0 HcmV?d00001 diff --git a/LIBRA/greatsort.m b/LIBRA/greatsort.m new file mode 100644 index 0000000..200cf70 --- /dev/null +++ b/LIBRA/greatsort.m @@ -0,0 +1,23 @@ +function [vec,I]=greatsort(x); + +%GREATSORT sorts the vector x in descending order. +% +% Required input arguments: +% x : vector to be sorted +% +% Output arguments: +% vec : sorted vector +% I : index => x(I)=vec. +% +% I/O: [vec,I]=greatsort(x); +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +%Written by S. Verboven + +vec=-x; +[svec,I]=sort(vec); +vec=-svec; + diff --git a/LIBRA/halfspacedepth.m b/LIBRA/halfspacedepth.m new file mode 100644 index 0000000..267317c --- /dev/null +++ b/LIBRA/halfspacedepth.m @@ -0,0 +1,134 @@ +function result=halfspacedepth(u,v,x,y) + +%HALFSPACEDEPTH computes the halfspace depth of a (two-dimensional) point theta +% relative to a bivariate data set. +% +% The algoritm is described in: +% Rousseeuw, P., Ruts, I. (1996), +% "AS 307: Bivariate Location Depth", +% Applied Statistics (JRSS-C), 45, 516-526. +% +% Required input arguments: +% u : first coordinate of the point theta +% v : second coordinate of the point theta +% x : vector containing the first coordinates of all the data +% points +% y : vector containing the second coordinates of all the data +% points +% +% I/O: [result] = halfspacedepth(u,v,x,y); +% +% This function is part of the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Fabienne Verwerft +% +% +% Checking input +% +if not(length(x)==length(y)) + error('The vectors x and y must have the same length.') +end +if sum(isnan(x))>=1 || sum(isnan(y))>=1 + error('Missing values are not allowed') +end +% +% m is the number of data points that coincide with theta +% +eps=0.000001; +n=length(x); +norm=sqrt((x-u).^2 + (y-v).^2); +m=sum(norm<=eps); +ll=find(norm>eps); +norm=norm(ll); +xn=(x(ll)-u)./norm; +yn=(y(ll)-v)./norm; +% +% The vector containes the indices of the elements of x and y that satisfy +% the conditions. +% +k=find((abs(x(ll))>abs(y(ll))) & (xn>=0)); +alfa(k)=asin(yn(k)); +t=find(alfa(k)<0); +alfa(k(t))=2*pi+alfa(k(t)); +k=find((abs(x(ll))>abs(y(ll))) & (xn<0)); +alfa(k)=pi-asin(yn(k)); +k=find((abs(x(ll))<=abs(y(ll))) & (yn>=0)); +alfa(k)=acos(xn(k)); +k=find((abs(x(ll))<=abs(y(ll))) & (yn<0)); +alfa(k)=2*pi-acos(xn(k)); +g=find(alfa>=(2*pi-eps)); +alfa(g)=0; +% +nn=n-m; +if nn <= 1 + hdepth=m; + result=hdepth; + return + % all data points coincide with theta +end +% +alfa=sort(alfa); +% +hoek=max(alfa(1)-alfa(nn)+2*pi,max(diff(alfa))); +if hoek > pi+eps + hdepth=m; + result=hdepth; + return + % hdepth=0 because theta lies outside the datacloud +end +% +% rotation around theta +% nu is the number of angles in the upper halfcircle +% +alfa=alfa-alfa(1); +nu=sum(alfa < (pi-eps)); + +if nu >= nn + hdepth=m; + result=hdepth; + return + % hdepth=0 every angle in the upper halfcircle so theta outside the + % datacloud. +end +% +% construction of the array F +% +beta=alfa+pi; +alfatwee=alfa+2*pi; +A=[alfa,alfatwee,beta]; +Aindex(1:2*nn)=1; +Aindex((2*nn+1):3*nn)=0; +[As,Asin]=sort(A); +Aindexs=Aindex(Asin); +pp=cumsum(Aindexs); +juisten=find(Aindexs==0); +F=pp(juisten); +% +% Adjust the array F for the angles that coincide with beta. +% +gelijkab=intersect(find(diff(As)<=eps)+1,juisten); +betagelijka=Asin(gelijkab); +if length(gelijkab)>0 + for i=1:length(gelijkab) + aantal=sum((As(Aindexs==1)+eps)0 + for i=1:length(gindex) + aantal=sum(alfa(1:gindex(i)) p + [P,T,L,r,centerX,cX]=classSVD(X); +else + cX=mean(X); + centerX=X-repmat(cX,n,1); + [P,L]=eig(centerX*centerX'/(n-1)); + [L,I]=greatsort(diag(L)); + P=P(:,I); + tol=n*max(L)*eps; + r=sum(L>tol); + L=L(1:r); + loadings=(centerX/sqrt(n-1))'*P(:,1:r)*diag(1./sqrt(L)); + %normalizing loadings by dividing by the sqrt(eigenvalues) + T=centerX*loadings; + P=loadings; +end \ No newline at end of file diff --git a/LIBRA/l1median.m b/LIBRA/l1median.m new file mode 100644 index 0000000..ed414c4 --- /dev/null +++ b/LIBRA/l1median.m @@ -0,0 +1,89 @@ +function result=L1median(x,tol); + +%L1MEDIAN is an orthogonally equivariant location estimator, +% also known as the spatial median. It is defined as the point which +% minimizes the sum of the Euclidean distances to all observations in the +% data matrix x. It can resist 50% outliers. +% +% Reference (for the algorithm): +% Hossjer, O. and Croux, C. (1995) +% "Generalizing univariate signed rank statistics for testing and estimating +% a multivariate location parameter", Nonparametric Statistics, 4, 293-308. +% +% Required input argument: +% x : either a data matrix with n observations in rows, p variables in columns +% or a vector of length n. +% +% Optional input argument: +% tol : convergence criterium; the iterative process stops when the norm between two solutions < tol. +% (default = 1.e-08). +% +% I/O: result=L1median(x,tol); +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Original Gauss code by C. Croux, translated to MATLAB by Sabine Verboven +% Last updated 06/02/2009 by Mia Hubert + +if nargin <2 + tol=1.e-08; +end; +[n,p]=size(x); +maxstep=200; +%initializing starting value for m +m=median(x); +k=1; +obj=mrobj(x,m); +while (k<=maxstep) + mold=m; + objold=obj; + centerx=x-repmat(m,n,1); + nc=norme(centerx); + w=nc; + ind=1:n; + notzero=ind(nc~=0); + w(notzero)=1./nc(notzero); + delta=sum(centerx.*repmat(w,1,p),1)./sum(w); + nd=norme(delta); + if all(nd=objold)&(nstep<=maxhalf) + nstep=nstep+1; + m=mold+delta./(2^nstep); + end + if (nstep>maxhalf) + mX=mold; + break + end + k=k+1; +end +if k>maxstep + display('Iteration failed') +end +result=m; + +%------------------------------------------------------------------ +function n=norme(x); + +%NORME calculates the Euclidian norm of a matrix x +% the output is a column vector containing the norm of each row +% I/O: n=norme(x); + +n=sqrt(sum(x.^2,2)); + +%------------------------------------------------------------------ +function s=mrobj(x,m) + +%MROBJ computes the objective function in m based on x and a + +xm=norme(x-repmat(m,size(x,1),1)); +s=sum(xm,1)'; + diff --git a/LIBRA/lmc.m b/LIBRA/lmc.m new file mode 100644 index 0000000..1896672 --- /dev/null +++ b/LIBRA/lmc.m @@ -0,0 +1,42 @@ +function [res] = lmc(x) + +%LMC calculates the left medcouple, a robust measure of +%left tail weight +% +% The left medcouple is described in: +% Brys, G., Hubert, M. and Struyf, A. (2006), +% "Robust Measures of Tail Weight", +% Computational Statistics and Data Analysis, +% 50 (No 3), 733-759. +% +% For the up-to-date reference, please consult the website: +% wis.kuleuven.be/stat/robust.html +% +% Required input arguments: +% x : Data matrix (rows=observations, columns=variables) +% +% I/O: +% result=lmc(x); +% +% Example: +% result = lmc([chi2rnd(5,1000,1) trnd(3,1000,1)]); +% +% The output of LMC is a vector containing the left medcouple +% for each column of the data matrix x +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Guy Brys +% Last Update: 17/03/2006 + +if (nargin<1) + error('No input arguments') +end +if (size(x,1)==1) + x = x'; +end +for (i=1:size(x,2)) + res(i) = -mc(x(x(:,i)<=prctile(x(:,i),50),i)); +end diff --git a/LIBRA/lsscatter.m b/LIBRA/lsscatter.m new file mode 100644 index 0000000..d0f9d3d --- /dev/null +++ b/LIBRA/lsscatter.m @@ -0,0 +1,53 @@ +function lsscatter(x,y,fitted,attrib) + +%LSSCATTER makes a scatter plot with regression (LTS/LS) line +% +% Required input arguments: +% x : predictor variabele (without missing values) +% y : response variabele +% fitted : fitted values corresponding with the regression +% attrib : string identifying the used method = 'LS', 'LTS' +% +% I/O: lsscatter(x,y,fitted,attrib) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Nele Smets on : 26/11/2003 +% Last Update: 29/01/2008 + + +set(gcf,'Name', 'Scatter plot', 'NumberTitle', 'off'); +[n,p]=size(x); +if p~=1 + disp(['Scatter plot with regression line ',... + 'is only available for bivariate data']) +else + x1 = x(1); + xn = x(n); + y1 = fitted(1); + yn = fitted(n); + dx = xn - x1; + dy = yn - y1; + slope = dy./dx; + centerx = (x1 + xn)/2; + centery = (y1 + yn)/2; + maxx = max(x); + minx = min(x); + maxy = centery + slope.*(maxx - centerx); + miny = centery - slope.*(centerx - minx); + mx = [minx; maxx]; + my = [miny; maxy]; + plot(x,y,'o'); + hold on + xrange=0.1*(abs(maxx-abs(minx))); + yrange=0.2*(abs(maxy-abs(miny)+5)); + xlim([minx-xrange maxx+xrange]) + ylim([min(y)-yrange max(y)+yrange]) + plot(mx,my,'-'); + title(attrib) + hold off +end + + \ No newline at end of file diff --git a/LIBRA/ltsregres.m b/LIBRA/ltsregres.m new file mode 100644 index 0000000..d1147c6 --- /dev/null +++ b/LIBRA/ltsregres.m @@ -0,0 +1,1162 @@ +function [rew,raw] = ltsregres(x,y,varargin) + +%LTSREGRES carries out least trimmed squares (LTS) regression, introduced in +% +% Rousseeuw, P.J. (1984), "Least Median of Squares Regression," +% Journal of the American Statistical Association, Vol. 79, pp. 871-881. +% +% The LTS regression method minimizes the sum of the h smallest squared +% residuals, where h must be at least half the number of observations. The +% default value of h is roughly 0.75n (n is the total number of observations), +% but the user may choose any value between n/2 and n. +% +% To compute the LTS estimator, the FAST-LTS algorithm is used. +% Reference: +% Rousseeuw, P.J. and Van Driessen, K. (2006), +% "Computing LTS Regression for Large Data Sets", Data Mining and +% Knowledge Discovery, 12, 29-45. +% +% Also available at http://www.agoras.ua.ac.be/ +% +% The LTS regression method is intended for continuous variables, and assumes +% that the number of observations n is at least 2 times the number of +% regression coefficients p. If p is too large with respect to n, it is +% better to first reduce p by variable selection or principal components +% (see rpcr.m, rsimpls.m). The response variable should be univariate, otherwise +% robust multivariate regression should be performed (see mcdregres.m). +% +% The LTS is a robust method in the sense that the estimated regression +% fit is not unduly influenced by outliers in the data, even if there are +% several outliers. Due to this robustness, we can detect outliers by their +% large LTS residuals. +% +% Required input arguments: +% x : Data matrix of explanatory variables (also called 'regressors'). +% Rows of x represent observations, and columns represent variables. +% Missing values (NaN's) and infinite values (Inf's) are allowed, since observations (rows) +% with missing or infinite values will automatically be excluded from the computations. +% y: A vector with n elements that contains the response variables. +% Missing values (NaN's) and infinite values (Inf's) are allowed, since observations (rows) +% with missing or infinite values will automatically be excluded from the computations. +% +% Optional input arguments: +% intercept : If 1, a model with constant term will be fitted (default), +% if 0, no constant term will be included. +% intadjust : If 1, the intercept adjustment will be applied in each step +% of the algorithm. These calculations need substantially more +% computation time than intadjust=0, which is the default value. +% h : The number of observations that have determined the least +% trimmed squares estimator. Any value between n/2 and n may be specified. +% alpha : (1-alpha) measures the fraction of outliers the algorithm should +% resist. Any value between 0.5 and 1 may be specified. (default = 0.75) +% ntrial : Number of initial subsets drawn. Its default value is 500. +% plots : If equal to one, a menu is shown which allows to draw several plots, +% such as residual plots and a regression outlier map. (default) +% If the input argument 'classic' is equal to one, the classical +% plots are drawn as well. +% If 'plots' is equal to zero, all plots are suppressed. +% See also makeplot.m +% classic : If equal to one, classical least squares regression will be performed, +% see ols.m (default = 0). +% +% Input arguments for advanced users: +% Hsets : Instead of random trial h-subsets (default, Hsets = []), Hsets makes it possible to give certain +% h-subsets as input. Hsets is a matrix that contains the indices of the observations of one +% h-subset as a row. +% +% I/O: result=ltsregres(x,y,'plots',0,'intercept',0) +% [rew,raw] = ltsregres(x,y) +% The user should only give the input arguments that have to change their default value. +% The name of the input arguments needs to be followed by their value. +% The order of the input arguments is of no importance. +% +% The output consists of two structures 'rew' and 'raw' containing the +% following fields: +% +% raw.coefficients : Vector of raw LTS coefficient estimates (including the +% intercept, when options.intercept=1). +% raw.fitted : Vector like y containing the raw fitted values of the response. +% raw.res : Vector like y containing the raw residuals from the regression. +% raw.scale : Scale estimate of the raw residuals. +% raw.objective : Objective function of the LTS regression method, i.e. the sum +% of the h smallest squared raw residuals. +% raw.wt : Vector like y containing weights that can be used in a weighted +% least squares. These weights are 1 for points with reasonably +% small raw residuals, and 0 for points with large raw residuals. +% +% rew.slope : Vector of the slope coefficients obtained after reweighting. +% rew.int : The intercept. +% rew.fitted : Vector like y containing the fitted values of the response +% after reweighting. +% rew.res : Vector like y containing the residuals from the weighted +% least squares regression. +% rew.scale : Scale estimate of the reweighted residuals. +% rew.rsquared : Robust version of R squared. This is 1 minus the fraction: +% (sum of the quan smallest squared residuals) over (sum of +% the quan smallest (y-loc)^2), where the denominator +% is minimized over loc. Note that loc is not subtracted from +% y if intercept = 0 in the call to ltsregres. +% rew.h : The number of observations that have determined the LTS estimator, +% i.e. the value of h. +% rew. Hsubsets : A structure that contains Hopt and Hfreq: +% Hopt : The subset of h points whose covariance matrix has minimal determinant, +% ordered following increasing robust distances. +% Hfreq : The subset of h points which are the most frequently selected during the whole +% algorithm. +% rew.alpha : (1-alpha) measures the fraction of outliers the algorithm should +% resist. +% rew.rd : The robust distances for the observations of the design matrix, +% based on the MCD estimator (mcdcov.m) +% rew.resd : Vector like y containing the standardized residuals +% from the weighted least squares regression. +% rew.weights : Vector like y containing weights that have been used in a weighted +% least squares. These weights are 1 for points with reasonably +% small raw residuals, and 0 for points with large raw residuals. +% rew.cutoff : Structure which contains cutoff values for the robust distances computed by mcdcov.m, +% and for the standardized residuals. +% rew.flag : Vector like y containing flags based on the reweighted regression. +% These flags determine which observations can be considered as +% outliers. +% rew.method : Character string naming the method (Least Trimmed Squares). +% rew.class : 'LTS' +% rew.classic : If the input argument 'classic' equals 1, this structure contains the +% results of a classical least squares regression +% rew.X : If x is univariate, same as the input x in the call to ltsregres, +% without rows containing missing or infinite values. +% rew.y : If x is univariate, same as the input y in the call to ltsregres, +% without rows containing missing or infinite values. + +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Version 22/12/2000, +% Written by Katrien Van Driessen and Randy Brenkers +% Revisions by Sabine Verboven, Sanne Engelen, Nele Smets +% Last update: 06/07/2004, 03/07/2006 (profiler tips --> & -> &&, | --> ||) +% Last revision: 03/07/2006 + +%MATLAB: to avoid singularMatrix in line 538 +warning off +%The maximum value for p = number of variables +pmax=20; +%The maximum value for n = number of observations +nmax=50000; +%To change the number of subdatasets and their size, the values of maxgroup +%and nmini can be changed +maxgroup=5; +nmini=300; +%The number of iteration steps in stages 1,2 and 3 can be changed +% by adapting the parameters csteps1, csteps2, and csteps3. +csteps1=2; +csteps2=2; +csteps3=100; + +pmax1=pmax+1; +pmax2=pmax*pmax; +nvm11=pmax*pmax1; +nvm12=pmax1*pmax1; +km10=10*maxgroup; +nmaxi=nmini*maxgroup; +maxmini=fix(((3*nmini-1)/2)+1); +% dtrial : number of subsamples if not all (p+1)-subsets will be considered. +dtrial=500; + +[n,p]=size(x); +[m,q]=size(y); +if q~=1 + if m==1 + y=y'; + else + error('y is not one-dimensional.'); + end +end +na.x=~isfinite(x*ones(p,1)); +na.y=~isfinite(y); +if size(na.x,1)~=size(na.y,1) + error('Number of observations in x and y not equal.'); +end +% Observations with missing or infinite values are ommitted. +ok=~(na.x|na.y); +x=x(ok,:); +y=y(ok,:); +dx=size(x); +dy=size(y,1); +n=dx(1); +% Some checks are now performed. +if n == 0 + error('All observations have missing values!'); +end +if n > nmax + error(['The program allows for at most ' int2str(nmax) ' observations.']); +end +%internal variables and default values +seed=0; +alfa=0.75; +hdef=quanf(alfa,n,p+1); %default value of h +hmin=quanf(0.5,n,p+1); %minmal value of h +default=struct('intercept',1,'intadjust',0,'alpha',alfa,'h',hdef,'plots',1,'ntrial',dtrial,'classic',0,'Hsets',[]); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +counter=1; +%Reading optional inputarguments +if nargin > 3 + % + % placing inputfields in array of strings + % + for j=1:nargin-2 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + dummy=sum(strcmp(chklist,'h')+2*strcmp(chklist,'alpha')); + switch dummy + case 0 % defaultvalues should be taken + alfa=options.alpha; + h=options.h; + case 3 + error('Both inputarguments alpha and h are provided. Only one is required.') + end + % + % Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) % in case of similarity + for j=1:nargin-3 % searching the index of the accompanying field + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end + if dummy==1% checking inputvariable h + if options.h < hmin + error(['The LTS must cover at least ' int2str(hmin) ' observations.']) + elseif options.h > n + error('h is greater than the number of non-missings and non-infinites.') + elseif options.h < p + error(['h should be larger than the dimension ' int2str(p) '.']) + elseif options.h==0 + options.h=h; + end + options.alpha=options.h/n; + elseif dummy==2 + if options.alpha < 0.5 + options.alpha=0.5; + mess=sprintf(['Attention : Alpha should be larger than 0.5. \n',... + 'It is set to 0.5.']); + disp(mess) + end + if options.alpha > 1 + options.alpha=0.75; + mess=sprintf(['Attention : Alpha should be smaller than 1.\n',... + 'It is set to 0.75.']); + disp(mess) + end + if options.alpha == 0 + options.alpha=0.75; + end + options.h=quanf(options.alpha,n,p); + end +end +intercept=options.intercept; %if 1 intercept in the model +h=options.h; %number of regular data points on which estimates are based (=[alpha * n]) +plots=options.plots; %relevant plots if equal to 1 +alfa=options.alpha; %proportion of regular data points +ntrial=options.ntrial; %number of subsets to be taken in the first step +classic=options.classic; %classic least squares regression? +bestobj=inf; %best objective value until 'now' -> goal is to be as small as possible +intadj=options.intadjust; %intercept adjustment needed if set to 1 +Hsets = options.Hsets; +if ~isempty(Hsets) + Hsets_ind = 1; +else + Hsets_ind = 0; +end + +if classic + res.classic=ols(x,y,'plots',0); +else + res.classic=0; +end + +if intercept == 1 + dx=dx+[0 1]; + x=cat(2,x,ones(n,1)); +end +p=dx(2); +if n < p + error('Need more observations than variables.'); +end + +if p > pmax + error(['The program allows for at most ' int2str(pmax) ' variables.']) +end + +rk=rank(x); +if rk < p + error('x is singular'); +end + +if h == n + res.method='Least Squares Regression.'; + [Q,R]=qr(x,0); + z=R\(Q'*y); + raw.coefficients=z; + residuals=y-x*z; + raw.res=residuals; + fitted=x*raw.coefficients; + raw.fitted=fitted; + s0=sqrt(sum(residuals.^2)/(n-p)); + if abs(s0) < 1e-7 + weights=abs(residuals)<=1e-7; + raw.wt=weights; + raw.scale=0; + res.scale=0; + res.coefficients=raw.coefficients; + raw.objective=0; + else + sor=sort(residuals.^2); + raw.objective=sum(sor(1:h)); + raw.scale=s0; + weights=abs(residuals/s0)<=norminv(0.9875); + raw.wt=weights; + [Q,R]=qr(x(weights==1,:),0); + z=R\(Q'*y(weights==1)); + res.coefficients=z; + fitted=x*res.coefficients; + residuals=y-x*z; + res.scale=sqrt(sum(weights.*(residuals.^2))/(sum(weights)-1)); + weights=abs(residuals/res.scale)<=norminv(0.9875); + end + if intercept + s1=sum(residuals.^2); + center=mean(y); + sh=sum((y-center).^2); + res.rsquared=1-s1/sh; + else + s1=sum(residuals.^2); + sh=sum(y.^2); + res.rsquared=1-s1/sh; + end + if res.rsquared > 1 + res.rsquared=1; + elseif res.rsquared < 0 + res.rsquared=0; + end + res.Hsubsets.Hopt=1:n; + res.Hsubsets.Hfreq=1:n; + if abs(s0) < 0 + res.method=strvcat(res.method,'An exact fit was found!'); + end + stdres=residuals/res.scale; + cutoff.resd=sqrt(chi2inv(0.975,1)); + raw=struct('coefficients',{raw.coefficients},'fitted',{raw.fitted},'res',{raw.res},'scale',{raw.scale},... + 'objective',{raw.objective},'wt',{raw.wt}); + rew=struct('slope',{res.coefficients(1:p)},'int',{0},'fitted',{fitted},'res',{residuals},... + 'scale',{res.scale},'rsquared',{res.rsquared},'h',{h},'alpha',{alfa},'resd', {stdres},... + 'rd',{NaN},'cutoff',{cutoff},'flag',{NaN},'weights',{raw.wt},'Hsubsets',{res.Hsubsets},... + 'method',{res.method},'class',{'LTS'},'classic',{res.classic},'X',{x},'y',{y}); + if intercept + rew=setfield(rew,'int',res.coefficients(p)); + rew=setfield(rew,'slope',res.coefficients(1:p-1)); + end + if plots && classic + mcdres=mcdcov(x,'h',h,'plots',0); + if -log(abs(det(mcdres.cov)))/size(data,2)> 50 + res.rd=NaN; + else + res.rd=mcdres.rd; + end + cutoff.rd=mcdres.cutoff.rd; + cutoff.md=mcdres.cutoff.md; + flags=abs(stdres)<=cutoff.resd; + rew=setfield(rew,'rd', res.rd); + rew=setfield(rew,'flag',flags); + rew=setfield(rew,'cutoff',cutoff); + try + makeplot(rew,'classic',1) + catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu + end + elseif plots + mcdres=mcdcov(x,'h',h,'plots',0); + if -log(abs(det(mcdres.cov)))/size(x,2)> 50 + res.rd=NaN; + else + res.rd=mcdres.rd; + end + cutoff.rd=mcdres.cutoff.rd; + cutoff.md=mcdres.cutoff.md; + flags=abs(stdres)<=cutoff.resd; + rew=setfield(rew,'rd', res.rd); + rew=setfield(rew,'cutoff',cutoff); + rew=setfield(rew,'flag',flags); + try + makeplot(rew) + catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu + end + end + return +end + +if p < 5 + eps=1e-12; +elseif p <= 8 + eps=1e-14; +else + eps=1e-16; +end + +% standardization of the data +xorig=x; +yorig=y; +data=[x y]; +if ~intercept + datamed=repmat(0,1,p+1); + datamad=median(abs(data)).*1.4826; + for i=1:p+1 + if abs(datamad(i)) <= eps + datamad(i)=sum(abs(data(:,i))); + datamad(i)=(datamad(i)/n)*1.2533; + if abs(datamad(i)) <= eps; + error('The MAD of some variable is zero'); + end + end + end + x=x./repmat(datamad(1:p),n,1); + y(:,1)=y(:,1)./datamad(p+1); +else + datamed=median(data); + datamed(p)=0; + datamad(p)=1; + for i=1:p+1 + if i ~= p + datamad(i)=median(abs(data(:,i)-datamed(i)))*1.4826; + if abs(datamad(i)) <= eps + datamad(i)=sum(abs(data(:,i)-datamed(i))); + datamad(i)=(datamad(i)/n)*1.2533; + if abs(datamad(i)) <= eps + error('The MAD of some variable is zero'); + end + end + end + end + x=(x-repmat(datamed(1:p),n,1))./repmat(datamad(1:p),n,1); + y(:,1)=(y(:,1)-datamed(p+1))./datamad(p+1); +end + +res.method='Least Trimmed Squares Regression.'; +al=0; +teller = zeros(1,n+1); +if Hsets_ind + csteps = csteps1; + inplane = NaN; + fine = 0; + part = 0; + final = 1; + tottimes = 0; + nsamp = size(Hsets,1); + obsingroup = n; +else + if n >= 2*nmini + maxobs=maxgroup*nmini; + if n >= maxobs + ngroup=maxgroup; + group(1:maxgroup)=nmini; + else + ngroup=floor(n/nmini); + minquan=floor(n/ngroup); + group(1)=minquan; + for s=2:ngroup + group(s)=minquan+double(rem(n,ngroup)>=s-1); + end + end + part=1; + adjh=floor(group(1)*alfa); + nsamp=floor(ntrial/ngroup); + minigr=sum(group); + obsingroup=fillgroup(n,group,ngroup,seed); + totgroup=ngroup; + else + [part,group,ngroup,adjh,minigr,obsingroup]=deal(0,n,1,h,n,n); + replow=[50,22,17,15,14,zeros(1,45)]; + if n < replow(p) + al=1; + perm=[1:p-1,p-1]; + nsamp=nchoosek(n,p); + else + al=0; + nsamp=ntrial; + end + end + + csteps=csteps1; + [tottimes,fine,final]=deal(0); + if part + bobj1=repmat(inf,ngroup,10); + bcoeff1=cell(ngroup,10); + [bcoeff1{:}]=deal(NaN); + end +end +bcoeff=cell(1,10); +bobj=repmat(inf,1,10); +[bcoeff{:}]=deal(NaN); +seed=0; +coeffs=repmat(NaN,p,1); +while final ~= 2 + if fine || (~part && final) + if ~Hsets_ind + nsamp=10; + end + if final + adjh=h; + ngroup=1; + if n*p <= 1e+5 + csteps=csteps3; + elseif n*p <= 1e+6 + csteps=10-(ceil(n*p/1e+5)-2); + else + csteps=1; + end + if n > 5000 + nsamp=1; + end + else + adjh=floor(minigr*alfa); + csteps=csteps2; + end + end + for k=1:ngroup + for i=1:nsamp + tottimes=tottimes+1; + prevobj=0; + if ~Hsets_ind + if final + if ~isinf(bobj(i)) + z=bcoeff{i}; + else + break + end + elseif fine + if ~isinf(bobj1(k,i)) + z=bcoeff1{k,i}; + else + break + end + else + z(1,1)=Inf; + while abs(z(1,1)) == Inf + if ~part + if al + k=p; + perm(k)=perm(k)+1; + while ~(k==1 || perm(k) <= (n-(p-k))) + k=k-1; + perm(k)=perm(k)+1; + for j=(k+1):p + perm(j)=perm(j-1)+1; + end + end + index=perm; + if ~isempty(find(perm>n)) %to avoid indexproblems + break + end + else + [index,seed]=randomset(n,p,seed); + end + else + [index,seed]=randomset(group(k),p,seed); + index=obsingroup{k}(index); + end + if p > 1 + z=x(index,:)\y(index,1); + %problems arise whenever the subsample contains + %equal x-values. To avoid warnings the tests in line + %551 and line 591 are adapted to abs(z(1,1)). + %However, in the first run the matrix will still + %be singular having coefficients [-inf a b ...] or + %[inf inf ...] producing the warning. To avoid + %this we turned off the warnings in this + %function (line 140). + elseif x(index,1) ~= 0 + z(1,1)=y(index,1)/x(index,1); + else + z(1,1)=x(index,1); + end + end + end + if abs(z(1,1)) ~= Inf + if ~part || final + residu=y-x*z; + elseif ~fine + residu=y(obsingroup{k},1)-x(obsingroup{k},:)*z; + else + residu=y(obsingroup{totgroup+1},1)-x(obsingroup{totgroup+1},:)*z; + end + more1=0; + more2=0; + nmore=200; + nmore2=nmore/2; + if intadj %intercept adjustment + [sortres,sortind]=sort(residu); + if ~part %n<600 + [center,cover,loc]=mcduni(sortres,obsingroup,adjh,obsingroup-adjh+1,alfa); + z(p)=z(p)+center; + residu=residu-center; + elseif ~fine %n>600, first step + [center,cover,loc]=mcduni(sortres,size(obsingroup{k},2),adjh,size(obsingroup{k},2)-adjh+1,alfa); + z(p)=z(p)+center; + residu=residu-center; + elseif ~final && size(obsingroup{totgroup+1},2)-adjh <= nmore %fine = merged set + [center,cover,loc]=mcduni(sortres,size(obsingroup{totgroup+1},2),adjh,size(obsingroup{totgroup+1},2)-adjh+1,alfa); + z(p)=z(p)+center; + residu=residu-center; + elseif final & n-adjh <= nmore %final = complete data set + [center,cover,loc]=mcduni(sortres,n,adjh,n-adjh+1,alfa); + z(p)=z(p)+center; + residu=residu-center; + else + [sortres1,sortind1]=sort(abs(sortres)); + [sortres2,sortind2]=sort(sortres(sortind1(1:adjh))); + further = 1; + if final && (sortind1(sortind2(1))+nmore-nmore2+adjh-1 > n || sortind1(sortind2(1))-nmore2< 1) + [center,cover,loc]=mcduni(sortres,n,adjh,n-adjh+1,alfa); + z(p)=z(p)+center; + residu=residu-center; + elseif ~final && fine && (sortind1(sortind2(1))+nmore-nmore2+adjh-1 > size(obsingroup{totgroup+1},2) || sortind1(sortind2(1))-nmore2 < 1) + [center,cover,loc]=mcduni(sortres,size(obsingroup{totgroup+1},2),adjh,size(obsingroup{totgroup+1},2)-adjh+1,alfa); + z(p)=z(p)+center; + residu=residu-center; + else + while further + sortres2(1:adjh+nmore)=sortres(sortind1(sortind2(1))-nmore2:sortind1(sortind2(1))+adjh-1+nmore-nmore2); + [center,cover,loc]=mcduni(sortres2,adjh+nmore,adjh,nmore+1,alfa); + if loc == 1 && ~more1 + if ~more2 + nmore=nmore2; + nmore2=nmore2+nmore2; + more1=1; + if sortind1(sortind2(1))-nmore2 < 1 + further=0; + end + else + further=0; + end + else + if loc == nmore+1 && ~more2 + if ~more1 + nmore=nmore2; + nmore2=-nmore2; + more2=1; + if final && sortind1(sortind2(1))+nmore-nmore2+adjh-1 > n + further=0; + elseif fine && (sortind1(sortind2(1))+nmore-nmore2+adjh-1 > size(obsingroup{totgroup+1},2) || sortind1(sortind2(1))-nmore2<1) + further=0; + end + else + further = 0; + + end + else + if loc == 1 && more1 + if ~more2 + nmore2=nmore2+100; + if sortind1(sortind2(1))-nmore2 < 1 + further=0; + end + else + further = 0; + end + else + if loc == nmore+1 && more2 + if ~more1 + nmore2=nmore2+100; + if final && sortind1(sortind2(1))+nmore-nmore2+adjh-1 > n + further=0; + elseif fine && (sortind1(sortind2(1))+nmore-nmore2+adjh-1 > size(obsingroup{totgroup+1},2) || sortind1(sortind2(1))-nmore2<1) + further=0; + end + else + further=0; + end + else + further=0; + end + end + end + end + end + z(p)=z(p)+center; + residu=residu-center; + end + end + end + end + end + for j=1:csteps %csteps on the subsets + tottimes=tottimes+1; + if ~Hsets_ind + if z(1,1)~=inf + [sortres,sortind]=sort(abs(residu)); + if fine && ~final + sortind=obsingroup{totgroup+1}(sortind); + elseif part && ~final + sortind=obsingroup{k}(sortind); + end + obs_in_set=sort(sortind(1:adjh)); + teller(obs_in_set) = teller(obs_in_set) + 1; + teller(end) = teller(end) + 1; + end + else + obs_in_set = Hsets(i,:); + end + + if Hsets_ind ||(~Hsets_ind && z(1,1)~=inf) + [Q,R]=qr(x(obs_in_set,:),0); + z=R\(Q'*y(obs_in_set,1)); + if ~part || final + residu=y-x*z; + elseif ~fine + residu=y(obsingroup{k},1)-x(obsingroup{k},:)*z; + else + residu=y(obsingroup{totgroup+1},1)-x(obsingroup{totgroup+1},:)*z; + end + more1=0; + more2=0; + nmore=200; + nmore2=nmore/2; + if intadj %intercept adjustment + [sortres,sortind]=sort(residu); + if ~part + [center,cover,loc]=mcduni(sortres,obsingroup,adjh,obsingroup-adjh+1,alfa); + z(p)=z(p)+center; + residu=residu-center; + elseif ~fine + [center,cover,loc]=mcduni(sortres,size(obsingroup{k},2),adjh,size(obsingroup{k},2)-adjh+1,alfa); + z(p)=z(p)+center; + residu=residu-center; + elseif ~final && size(obsingroup{totgroup+1},2)-adjh <= nmore + [center,cover,loc]=mcduni(sortres,size(obsingroup{totgroup+1},2),adjh,size(obsingroup{totgroup+1},2)-adjh+1,alfa); + z(p)=z(p)+center; + residu=residu-center; + elseif final && n-adjh <= nmore + [center,cover,loc]=mcduni(sortres,n,adjh,n-adjh+1,alfa); + z(p)=z(p)+center; + residu=residu-center; + else + [sortres1,sortind1]=sort(abs(sortres)); + [sortres2,sortind2]=sort(sortres(sortind1(1:adjh))); + further = 1; + if final && (sortind1(sortind2(1))+nmore-nmore2+adjh-1 > n || sortind1(sortind2(1))-nmore2< 1) + [center,cover,loc]=mcduni(sortres,n,adjh,n-adjh+1,alfa); + z(p)=z(p)+center; + residu=residu-center; + elseif ~final && fine && (sortind1(sortind2(1))+nmore-nmore2+adjh-1 > size(obsingroup{totgroup+1},2) || sortind1(sortind2(1))-nmore2 < 1) + [center,cover,loc]=mcduni(sortres,size(obsingroup{totgroup+1},2),adjh,size(obsingroup{totgroup+1},2)-adjh+1,alfa); + z(p)=z(p)+center; + residu=residu-center; + else + while further + sortres2(1:adjh+nmore)=sortres(sortind1(sortind2(1))-nmore2:sortind1(sortind2(1))+adjh-1+nmore-nmore2); + [center,cover,loc]=mcduni(sortres2,adjh+nmore,adjh,nmore+1,alfa); + if loc == 1 && ~more1 + if ~more2 + nmore=nmore2; + nmore2=nmore2+nmore2; + more1=1; + if sortind1(sortind2(1))-nmore2 < 1 + further=0; + end + else + further=0; + end + else + if loc == nmore+1 && ~more2 + if ~more1 + nmore=nmore2; + nmore2=-nmore2; + more2=1; + if final && sortind1(sortind2(1))+nmore-nmore2+adjh-1 > n + further=0; + elseif fine && (sortind1(sortind2(1))+nmore-nmore2+adjh-1 > size(obsingroup{totgroup+1},2) | sortind1(sortind2(1))-nmore2<1) + further=0; + end + else further=0; + end + else + if loc == 1 && more1 + if ~more2 + nmore2=nmore2+100; + if sortind1(sortind2(1))-nmore2 < 1 + further=0; + end + else further = 0; + end + else + if loc == nmore+1 && more2 + if ~more1 + nmore2=nmore2+100; + if final && sortind1(sortind2(1))+nmore-nmore2+adjh-1 > n + further=0; + elseif fine && (sortind1(sortind2(1))+nmore-nmore2+adjh-1 > size(obsingroup{totgroup+1},2) || sortind1(sortind2(1))-nmore2<1) + further=0; + end + else further=0; + end + else + further=0; + end + end + end + end + end + z(p)=z(p)+center; + residu=residu-center; + end + end + end + sor=sort(abs(residu)); + obj=sum(sor(1:adjh).^2); %objective function value after the iteration + if j >= 2 && obj == prevobj + break; + end + prevobj=obj; + end %end final + end + if ~final + if fine ||~part %merged set or n<600 + if obj < max(bobj) + [bcoeff,bobj]=insertion(bcoeff,bobj,z,obj,1,eps); + end + else + if obj < max(bobj1(k,:)) + [bcoeff1,bobj1]=insertion(bcoeff1,bobj1,z,obj,k,eps); + end + end + end + if final && obj < bestobj + bestset=obs_in_set; + bestobj=obj; + coeffs=z; + end + end %end for nsamp + end %end for ngroups + if part && ~fine + fine = 1; + elseif (part && fine && ~final) || (~part && ~final) + final = 1; + else + final = 2; + end +end %end while, so final = 2 + +if p <= 1 + coeffs(1)=coeffs(1)*datamad(p+1)/datamad(1); +else + coeffs(1:p-1)=(coeffs(1:p-1)*datamad(p+1))'./datamad(1:p-1); + if ~intercept + coeffs(p)=coeffs(p)*datamad(p+1)/datamad(p); + else + coeffs(p)=coeffs(p)*datamad(p+1); + coeffs(p)=coeffs(p)-sum(coeffs(1:p-1)'.*datamed(1:p-1)); + coeffs(p)=coeffs(p)+datamed(p+1); + end +end +bestobj=bestobj*(datamad(p+1)^2); +x=xorig; +y=yorig; +raw.coefficients=coeffs; +raw.objective=bestobj; +fitted=x*coeffs; +raw.fitted=fitted; +residuals=y-fitted; +raw.residuals=residuals; +sor=sort(residuals.^2); +factor=rawconsfactorlts(h,n); +sh0=sqrt((1/h)*sum(sor(1:h))); +s0=sh0*factor; +cutoff.resd=sqrt(chi2inv(0.975,1)); +if abs(s0) < 1e-7 + weights=abs(residuals)<=1e-7; + raw.wt=weights; + raw.scale=0; + res.scale=0; + res.coefficients=raw.coefficients; + raw.objective=0; +else + raw.scale=s0; + m=2*n/asvarscalekwad(h,n); + quantile=tinv(0.9875,m); + weights=abs(residuals/s0)<=quantile; + raw.wt=weights; + [Q,R]=qr(x(weights==1,:),0); + z=R\(Q'*y(weights==1)); + res.coefficients=z; + fitted=x*res.coefficients; + residuals=y-fitted; + res.scale=sqrt(sum(weights.*residuals.^2)/(sum(weights)-1)); + s0=res.scale; + weights=abs(residuals/res.scale)<=cutoff.resd; +end +res.flag=weights; + +res.Hsubsets.Hopt = bestset; +[telobs,indobs] = greatsort(teller(1:(end - 1))); +res.Hsubsets.Hfreq = indobs(1:(h)); +if size(res.Hsubsets.Hfreq,2) == 1 + res.Hsubsets.Hfreq = res.Hsubsets.Hfreq'; +end + +if intercept + yw=y(raw.wt==1); + cyw=mcenter(yw); + sres=sum(residuals(raw.wt==1).^2); + cwy2=sum(cyw.^2); + res.rsquared=1-sres/cwy2; +else + sor=sort(residuals.^2); + s1=sum(sor(1:h)); + sor=sort(y.^2); + sh=sum(sor(1:h)); + res.rsquared=1-(s1/sh); +end +if res.rsquared > 1 + res.rsquared=1; +elseif res.rsquared < 0 + res.rsquared=0; +end + +if abs(s0) < 1e-7 + res.method=strvcat(res.method,'An exact fit was found!'); + res.Hsubsets.Hopt=1:n; + res.Hsubsets.Hfreq=1:n; + disp('Exact fit was encountered') +end + +if ~intercept + data=x; +else + data=x(:,1:p-1); +end +%calculating residual distances : in case of a univariate analysis they are +%equal to the standardized residuals. +stdres=residuals/res.scale; +cutoff.resd=sqrt(chi2inv(0.975,1)); + +%assigning ouput +raw=struct('coefficients',{raw.coefficients},'fitted',{raw.fitted},'res',{raw.residuals},'scale',{raw.scale},... + 'objective',{raw.objective},'wt',{raw.wt}); +rew=struct('slope',{res.coefficients(1:p)},'int',{0},'fitted',{fitted},'res',{residuals},... + 'scale',{res.scale},'rsquared',{res.rsquared},'h',{h},'Hsubsets',{res.Hsubsets},'alpha',{alfa},'rd',{0},'resd', {stdres},... + 'cutoff',{cutoff},'flag',{res.flag},... + 'method',{res.method},'class',{'LTS'},'classic',{res.classic},'X',{data},'y',{y}); + +if intercept + rew=setfield(rew,'int',res.coefficients(p)); + rew=setfield(rew,'slope',res.coefficients(1:p-1)); +end + +if isfield(rew,'X') && ((size(x,2)-intercept)~=1 || size(y,2)~=1) + rew=rmfield(rew,{'X','y'}); +end +warning on +if plots && classic + mcdres=mcdcov(data,'h',h,'plots',0); + if -log(abs(det(mcdres.cov)))/size(data,2)> 50 + res.rd=NaN; + else + res.rd=mcdres.rd; + end + cutoff.rd=mcdres.cutoff.rd; + cutoff.md=mcdres.cutoff.md; + rew=setfield(rew,'cutoff',cutoff); + rew=setfield(rew,'rd', res.rd); + try + makeplot(rew,'classic',1) + catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu + end +elseif plots + mcdres=mcdcov(data,'h',h,'plots',0); + if -log(abs(det(mcdres.cov)))/size(data,2)> 50 + res.rd=NaN; + else + res.rd=mcdres.rd; + end + cutoff.rd=mcdres.cutoff.rd; + cutoff.md=mcdres.cutoff.md; + rew=setfield(rew,'cutoff',cutoff); + rew=setfield(rew,'rd', res.rd); + try + makeplot(rew) + catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu + end +end +if rew.rd==0 + rew=rmfield(rew,'rd'); +end +% -------------------------------------------------------------------- + +function obsingroup = fillgroup(n,group,ngroup,seed) + +% Creates the subdatasets. + +obsingroup=cell(1,ngroup+1); +jndex=0; +for k = 1:ngroup + for m = 1:group(k) + [random,seed]=uniran(seed); + ran=floor(random*(n-jndex)+1); + jndex=jndex+1; + if jndex == 1 + index(1,jndex)=ran; + index(2,jndex)=k; + else + index(1,jndex)=ran+jndex-1; + index(2,jndex)=k; + ii=min(find(index(1,1:jndex-1) > ran-1+[1:jndex-1])); + if length(ii) + index(1,jndex:-1:ii+1)=index(1,jndex-1:-1:ii); + index(2,jndex:-1:ii+1)=index(2,jndex-1:-1:ii); + index(1,ii)=ran+ii-1; + index(2,ii)=k; + end + end + end + obsingroup{k}=index(1,index(2,:)==k); + obsingroup{ngroup+1}=[obsingroup{ngroup+1},obsingroup{k}]; +end + +% -------------------------------------------------------------------- + +function [ranset,seed] = randomset(tot,nel,seed) + +for j = 1:nel + [random,seed]=uniran(seed); + num=floor(random*tot)+1; + if j > 1 + while any(ranset==num) + [random,seed]=uniran(seed); + num=floor(random*tot)+1; + end + end + ranset(j)=num; +end + + +% -------------------------------------------------------------------- + +function [output] = replow(k,pmax) + +replow=[500,50,22,17,15,14]; +help=zeros(1,pmax-5); +replow=[replow help]; +output=replow(k); + +% -------------------------------------------------------------------- + +function [initmean,initcov,iloc]=mcduni(y,ncas,h,len,alfa) + +% The exact MCD algorithm for the univariate case. + +y=sort(y); +ay(1)=sum(y(1:h)); +factor=rawconsfactormcd(h,ncas); + +for samp=2:len + ay(samp)=ay(samp-1)-y(samp-1)+y(samp+h-1); +end +ay2=ay.^2/h; +sq(1)=sum(y(1:h).^2)-ay2(1); +for samp=2:len + sq(samp)=sq(samp-1)-y(samp-1)^2+y(samp+h-1)^2-ay2(samp)+ay2(samp-1); +end +sqmin=min(sq); +ii=find(sq==sqmin); +ndup=length(ii); +slutn(1:ndup)=ay(ii); +initmean=slutn(floor((ndup+1)/2))/h; +initcov=factor^2*sqmin/h; +iloc=ii(1); + +% ----------------------------------------------------------------------------- + +function [bestmean,bobj]=insertion(bestmean,bobj,z,obj,row,eps) + +insert=1; +equ=find(obj==bobj(row,:)); +for j=equ + if (z==bestmean{row,j}) + insert=0; + end +end +if insert + ins=min(find(obj < bobj(row,:))); + if ins==10 + bestmean{row,ins}=z; + bobj(row,ins)=obj; + else + [bestmean{row,ins+1:10}]=deal(bestmean{row,ins:9}); + bestmean{row,ins}=z; + bobj(row,ins+1:10)=bobj(row,ins:9); + bobj(row,ins)=obj; + end +end + +% -------------------------------------------------------------------------------- + +function quan=quanf(alfa,n,rk) + +quan=floor(2*floor((n+rk+1)/2)-n+2*(n-floor((n+rk+1)/2))*alfa); + + +%--------------------------------------------------------------- + +function rawconsfacmcd=rawconsfactormcd(quan,n) + +qalpha=chi2inv(quan/n,1); +calphainvers=gamcdf(qalpha/2,1/2+1)/(quan/n); +calpha=1/calphainvers; +rawconsfacmcd=calpha; + + +%------------------------------------------------------------- + +function rawconsfaclts=rawconsfactorlts(quan,n) + +rawconsfaclts=(1/sqrt(1-((2*n)/(quan*(1/norminv((quan+n)/(2*n)))))*... + normpdf(1/(1/(norminv((quan+n)/(2*n))))))); + +%-------------------------------------------------------------- + +function asvar=asvarscalekwad(quan,n) + +alfa=quan/n; +alfa=1-alfa; +qalfa=chi2inv(1-alfa,1); +c2=gamcdf(qalfa/2,1/2+1); +c1=1/c2; +c3=3*gamcdf(qalfa/2,1/2+2); +asvar=qalfa*(1-alfa)-c2; +asvar=asvar^2; +asvar=(c3-2*qalfa*c2+(1-alfa)*(qalfa^2))-asvar; +asvar=c1^2*asvar; +%-------------------------------------------------------------- + + + + diff --git a/LIBRA/madc.m b/LIBRA/madc.m new file mode 100644 index 0000000..facd8c5 --- /dev/null +++ b/LIBRA/madc.m @@ -0,0 +1,79 @@ +function result=madc(x) + +%MADC is a scale estimator given by the Median Absolute Deviation +% with finite sample correction factor. +% It is defined as +% mad(x)= b_n 1.4826 med(|x_i - med(x)|) +% with b_n a small sample correction factor to make the mad unbiased at the +% normal distribution. It can resist 50% outliers. +% If x is a matrix, the scale estimate is computed on the columns of x. The +% result is then a row vector. If x is a row or a column vector, +% the output is a scalar. +% +% Required input argument: +% x: either a data matrix with n observations in rows, p variables in columns +% or a columnn vector of length n. +% +% I/O: result=mad(x); +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by S.Verboven +% Last revised: 22/12/03 + +[n,p]=size(x); + +if n==1 & p==1 + result=0; %when X is a one by one matrix, all scale estimators must be equal to 0 + return +elseif n==1 + x=x'; %we only want to work with column vectors + n=p; + p=1; +end +bn=fcorfac(x); %calculating finite sample correction +t_0=median(x); +result=median(abs(x - repmat(median(x),n,1)))*1.4826*bn; +if ~all(result) + result(result==0)=t_0(result==0); +end + +%--------- +function bn=fcorfac(Z) + +[n,p]=size(Z); +switch n +case 2 + bn=1.196; +case 3 + bn=1.495; +case 4 + bn=1.363; +case 5 + bn=1.206; +case 6 + bn=1.200; +case 7 + bn=1.140; +case 8 + bn=1.129; +case 9 + bn=1.107; +end +if n>9 + bn=n/(n-0.8); +end + + + + + + + + + + + + diff --git a/LIBRA/mahalanobis.m b/LIBRA/mahalanobis.m new file mode 100644 index 0000000..c1fc64c --- /dev/null +++ b/LIBRA/mahalanobis.m @@ -0,0 +1,96 @@ +function result=mahalanobis(x,locvct,varargin) + +%MAHALANOBIS computes the (squared) distance of each observation in x +% from the location estimate (locvct) of the data, +% relative to the shape of the data. +% +% Required input arguments: +% x : data matrix (n observations in rows, p variables in columns) +% locvct : location estimate of the data (p-dimensional vector) +% cov or invcov : scatter estimate of the data or the inverse of the scatter estimate (pxp matrix) +% +% I/O: result=mahalanobis(x,locvct,'cov',covmat) +% The user should only give the input arguments that have to change their default value. +% The name of the input arguments needs to be followed by their value. +% The order of the input arguments is of no importance. +% +% Examples: +% result=mahalanobis(x,loc,'cov',covx) +% result=mahalanobis(x,loc,'invcov',invcovx) +% +% Output: +% A row vector containing the squared distances of all the observations to locvct. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Katrien Van Driessen +% Revisions by Sabine Verboven +% Last update on 18/09/2003 +% + +%Initialisation +n = size(x,1); +p = size(x,2); +if nargin<3 + error('Missing a required input variable') +end +counter=1; +default=struct('cov',0,'invcov',NaN); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +%reading the user's input +if nargin>2 + % + %placing inputfields in array of strings + % + for j=1:nargin-2 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % + %Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) %in case of similarity + for j=1:nargin-2 %searching the index of the accompanying field + if rem(j,2)~=0 %fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end +end +if size(locvct,2)==1 + locvct=locvct'; %converting to a rowvector +end +if options.cov==0 & options.invcov==0 + error('The scatter matrix or its inverse is a required input argument.') +end +%%%%%%MAIN%%%%%%%%% +if ~isnan(options.invcov) + covmat=options.invcov; + if min(size(covmat))==1 + covmat=diag(covmat); + end +else + if min(size(options.cov))==1 + options.cov=diag(options.cov); + end + covmat=pinv(options.cov); +end +hlp=x-repmat(locvct,n,1); +dist=sum(hlp*covmat.*hlp,2)'; +result=dist; \ No newline at end of file diff --git a/LIBRA/makeplot.m b/LIBRA/makeplot.m new file mode 100644 index 0000000..7787d4b --- /dev/null +++ b/LIBRA/makeplot.m @@ -0,0 +1,1501 @@ +function makeplot(out,varargin) + +%MAKEPLOT makes plots for the main functions. These figures can also be obtained +% by setting 'plots = 1' in those functions. +% +% Required input: +% out = a structure containing the output of one of the following classes: +% MCDCOV, LS, LTS, MLR, MCDREG, CPCA,CPCR, CSIMPLS, RAPCA, ROBPCA, +% RPCR, RSIMPLS, CDA, RDA, PAM, AGNES, DIANA, FANNY, MONA, CLARA +% +% Optional input: +% nameplot : 0 : menu of plots (default = 0) +% 'all' : all possible plots +% 'scree' : scree plot +% 'pcadiag' : Score outlier map +% 'regdiag' : Regression outlier map +% '3ddiag' : 3D Regression outlier map +% 'robdist' : A plot of the robust distances +% 'qqmcd' : A qq-plot of the robust distances versus the quantiles of the chi-squared distribution +% 'dd' : A DD-plot: Robust distances versus Mahalanobis distances +% 'ellipse' : A tolerance ellipse +% 'resfit' : Standardized LTS Residuals versus fitted values +% 'resindex' : Standardized LTS Residuals versus index +% 'qqlts' : Normal QQ-plot of the LTS residuals +% 'scatter' : Scatterplot with LTS line +% 'da' : Tolerance ellipses of a discrimant analysis +% 'simca' : Scatterplot with boundaries defined by the number of PC's from a simca method +% 'tree' : Tree plot for the clustering methods agnes or diana +% 'silhouet' : Silhouette plot for the clustering methods pam or fanny +% 'banner' : Banner for the clustering methods agnes, diana or mona +% 'clus' : Clusplot for the clustering methods clara, fanny or pam +% labod : number of points to be identified in score plots +% with largest orthogonal distance. +% labsd : number of points to be identified in score and regression plots +% with largest score distance. +% labresd : number of points to be identified in regression plots +% with largest residual distance. +% labmcd : number of points to be identified in MCD plots +% with the largest robust distance. +% lablts : number of points to be identified in LTS plots +% with the largest absolute standardized residual. +% classic : In case of a robust analysis, classic can be set to 0 to avoid classical plots (default). +% If set to one, the inputargument 'out' must contain a field called 'classic' which is a structure. +% +% I/O: makeplot(out,'nameplot',0,'labsd',3,'labod',3,'labresd',3,'classic',0) +% The user should only give the input arguments that have to change their default value. +% The name of the input arguments needs to be followed by their value. The order of +% the input arguments is of no importance. +% +% Example: outrpcr=rpcr(x,y,'plots',0,'classic',1); +% makeplot(outrpcr,'labsd',5, 'classic',1) +% makeplot(outrpcr,'nameplot','scree') +% +% outmcd=mcdcov(x); +% makeplot(outmcd,'labmcd',6) +% makeplot(outmcd,'labmcd',4,'nameplot','dd') +% +% outls=ols(x,y); +% makeplot(outls) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Sabine Verboven 09/04/2003 +% Last Updated : 29/01/2008 +% +% Uses functions: screeplot, scorediagplot, regresdiagplot, +% regresdiagplot3d, plotnumbers, plotnumbers3d, +% distplot, chiqqplot, ddplot, ellipsplot, residualplot, +% normqqplot, lsscatter, tree, bannerplot, +% silhouetteplot, clusplot, +% menureg, menuscoreg, menupls, menucov, menuls,menuda, +% menusimca, menuclus1, menuclus2, menuclus3, menuclus4 +% + +%INTITIALISATION% +if isstruct(out) + innames=fieldnames(out); + if strmatch('class',innames,'exact') + attrib=out.class; + else + error('The method had no class identifier.') + end + if strmatch('classic',innames,'exact') + classic=out.classic; + elseif ismember(attrib,{'CPCA','CPCR','CSIMPLS','MLR','CDA','LS','CSIMCA'}) + classic= 1; + else + classic=0; + end +else + error('The first inputargument is not a structure.') +end +counter=1; +default=struct('nameplot',0,'labsd',3,'labod',3,'labresd',3,'labmcd',3,'lablts',3,'labclus',[],'classic',1); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +% +if nargin>1 + % + %placing inputfields in array of strings + % + for j=1:nargin-1 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % + %Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) %in case of similarity + for j=1:nargin-2 %searching the index of the accompanying field + if rem(j,2)~=0 %fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end + choice=options.nameplot; + if choice~=0 + ask=0; + else + ask=1; %menu of plots + end + labsd=options.labsd; + labod=options.labod; + labresd=options.labresd; + labmcd=options.labmcd; + lablts=options.lablts; + labclus=options.labclus; + classicplots=options.classic; + if ~isstruct(classic) & options.classic==1 & ismember({attrib},{'RAPCA', 'ROBPCA', 'RPCR', 'LTS', 'MCDREG',... + 'RSIMPLS', 'RDA', 'RSIMCA'}) + mess=sprintf(['The classical output is not available. Only robust plots will be shown.\n',... + 'Please rerun the preceeding analysis with the option ''classic'' set to 1',... + '\n if the classical plots are required.']); + disp(mess) + classicplots=0; + end +else + ask=1; %menu of plots + choice=0; + labsd=3; + labod=3; + labresd=3; + labmcd=3; + lablts=3; + labclus=[]; + if ~isstruct(classic) & options.classic==1 & ismember({attrib},{'RAPCA', 'ROBPCA', 'RPCR', 'LTS', 'MCDREG',... + 'RSIMPLS', 'RDA', 'RSIMCA','MCDCOV'}) + mess=sprintf(['The classical output is not available. Only robust plots will be shown.\n',... + 'Please rerun the preceeding analysis with the option ''classic'' set to 1',... + '\n if the classical plots are required.']); + disp(mess) + classicplots=0; + else + classicplots=1; + end +end + +%to initialize the correct menu of plots +exitno=0; +switch attrib + case 'CPCA' + exitno=4; + if ismember({char(choice)},{'regdiag','3ddiag','robdist','qqmcd','dd','ellipse','resfit','resindex','qqlts','diag','scatter',... + 'da','simca'}) + error('That kind of plot is not available for this method.') + end + case 'CPCR' + exitno=6; + if ismember({char(choice)},{'robdist','qqmcd','dd','ellipse','resfit','resindex','qqlts','diag','scatter','da','simca'}) + error('That kind of plot is not available for this method.') + end + case 'LS' + exitno=7; + if ismember({char(choice)},{'scree','pcadiag','3ddiag','robdist','qqmcd','dd','ellipse','simca','da'}) + error('That kind of plot is not available for this method.') + end + case 'MCDREG' + exitno=3; + if ismember({char(choice)},{'scree','pcadiag','3ddiag','robdist','qqmcd','dd','ellipse',... + 'resfit','resindex','qqlts','diag','scatter','da','simca'}) + error('That kind of plot is not available for this method.') + end + case 'MLR' + exitno=3; + if ismember({char(choice)},{'scree','pcadiag','3ddiag','robdist','qqmcd','dd','ellipse',... + 'resfit','resindex','qqlts','diag','scatter','da','simca'}) + error('That kind of plot is not available for this method.') + end + case 'LTS' + exitno=7; + if ismember({char(choice)},{'scree','pcadiag','3ddiag',... + 'robdist','qqmcd','dd','ellipse','da','simca'}) + error('That kind of plot is not available for this method.') + end + if ~isfield(out,'rd') + error('Please rerun the LTS-regression again with the option plots equal to 1.') + end + case {'RAPCA','ROBPCA'} + exitno=4; + if ismember({char(choice)},{'regdiag','3ddiag','robdist','qqmcd','dd','ellipse','resfit',... + 'resindex','qqlts','diag','scatter','da','simca'}) + error('That kind of plot is not available for this method.') + end + case 'RPCR' + exitno=6; + if ismember({char(choice)},{'robdist','qqmcd','dd','ellipse','resfit','resindex','qqlts','diag','scatter',... + 'da','simca'}) + error('That kind of plot is not available for this method.') + end + case {'RSIMPLS','CSIMPLS'} + exitno=5; + if ismember({char(choice)},{'scree','robdist','qqmcd','dd','ellipse','resfit','resindex','qqlts','diag','scatter',... + 'da','simca'}) + error('That kind of plot is not available for this method.') + end + case 'MCDCOV' + if ismember({char(choice)},{'scree','pcadiag','regdiag','3ddiag','resfit','resindex','qqlts','diag','scatter',... + 'da','simca'}) + error('That kind of plot is not available for this method.') + end + exitno=6; + case {'RDA','CDA'} + exitno=3; + if ismember({char(choice)},{'scree','pcadiag','3ddiag','robdist','qqmcd','dd','ellipse',... + 'resfit','resindex','qqlts','diag','scatter','regdiag','simca'}) + error('That kind of plot is not available for this method.') + end + if size(out.center,2)>2 + disp('Warning: Tolerance ellipses are only drawn for two-dimensional data sets.') + return + end + case {'CSIMCA','RSIMCA'} + exitno=3; + if ismember({char(choice)},{'scree','pcadiag','3ddiag','robdist','qqmcd','dd','ellipse',... + 'resfit','resindex','qqlts','diag','scatter','regdiag','da'}) + error('That kind of plot is not available for this method.') + end + if size(out.pca{1}.P,1) > 3 + disp('Warning: The dimension of the dataset is larger than 3.') + return + end + case {'AGNES','DIANA'} + exitno=4; + if ismember({char(choice)},{'scree','pcadiag','3ddiag','robdist','qqmcd','dd','ellipse',... + 'resfit','resindex','qqlts','diag','scatter','regdiag','da','silhouet','clus'}) + error('That kind of plot is not available for this method.') + end + case {'MONA'} + exitno=3; + if ismember({char(choice)},{'scree','pcadiag','3ddiag','robdist','qqmcd','dd','ellipse',... + 'resfit','resindex','qqlts','diag','scatter','regdiag','da','silhouet','clus','tree'}) + error('That kind of plot is not available for this method.') + end + case {'PAM','FANNY'} + exitno=4; + if ismember({char(choice)},{'scree','pcadiag','3ddiag','robdist','qqmcd','dd','ellipse',... + 'resfit','resindex','qqlts','diag','scatter','regdiag','da','tree','banner'}) + error('That kind of plot is not available for this method.') + end + case {'CLARA'} + exitno=3; + if ismember({char(choice)},{'scree','pcadiag','3ddiag','robdist','qqmcd','dd','ellipse',... + 'resfit','resindex','qqlts','diag','scatter','regdiag','da','tree','silhouet','banner'}) + error('That kind of plot is not available for this method.') + end +end +if exitno==0 + error(['Your attribute identifier must be one of the following names:',... + 'CPCA, RAPCA, ROBPCA, CPCR, RPCR, LS, LTS, MCDREG, RSIMPLS, CSIMPLS, MCDCOV,',... + 'CDA, RDA, CSIMCA, RSIMCA, AGNES, DIANA, MONA, PAM, CLARA, FANNY ']) +end + +%plotting what is asked for +if ask==0 + whichplot(out,choice,attrib,exitno,labsd,labod,labresd,labmcd,lablts,labclus,classic,classicplots) +else + %make menu of plots + while (choice~=exitno) + switch attrib + case {'CPCA','ROBPCA','RAPCA'} + choice=menuscore(out,attrib,exitno,labsd,labod,classic,classicplots); + case {'MCDREG','MLR'} + choice=menureg(out,attrib,exitno,labsd,labresd,classic,classicplots); + case {'COV','MCDCOV'} + choice=menucov(out,attrib,exitno,labmcd,classic,classicplots); + case {'RSIMPLS','CSIMPLS'} + choice=menupls(out,attrib,exitno,labsd,labod,labresd,classic,classicplots); + case {'CPCR','RPCR'} + choice=menuscoreg(out,attrib,exitno,labsd,labod,labresd,classic,classicplots); + case {'LTS','LS'} + choice=menuls(out,attrib,exitno,labsd,labresd,lablts,classic,classicplots); + case{'CDA','RDA'} + choice=menuda(out,attrib,exitno,classic,classicplots); + case{'CSIMCA','RSIMCA'} + choice=menusimca(out,attrib,exitno,classic,classicplots); + case{'AGNES','DIANA'} + choice=menuclus1(out,attrib,exitno); + case 'MONA' + choice=menuclus2(out,attrib,exitno); + case{'PAM','FANNY'} + choice=menuclus3(out,attrib,exitno,labclus); + case 'CLARA' + choice=menuclus4(out,attrib,exitno,labclus); + end + end + %whitebg(gca,[1 1 1]) %to ensure the axes in the current figure are reset to their original + %default colours after drawing a bannerplot with AGNES or DIANA +end + +%%%%%%%%%%%%% MAIN FUNCTION %%%%%%%%%%%%%%% +function whichplot(out,plotn,attrib,exitno,labsd,labod,labresd,labmcd,lablts,labclus,classic,classicplots) + +%Initializing variables + +switch attrib + + case {'ROBPCA','RAPCA'} + Xdist=out.sd; + cutoffX=out.cutoff.sd; + cutoffO=out.cutoff.od; + OD=out.od; + k=out.k; + L=out.L; + %classical output + if isstruct(classic) + Lcl=classic.L; + Xdistcl=classic.sd; + ODcl=classic.od; + cutoffXcl=classic.cutoff.sd; + cutoffOcl=classic.cutoff.od; + attribcl='CPCA'; + end + case 'MCDREG' + fitted=out.fitted; + Rdist=out.resd; + cutoffR=out.cutoff.resd; + if size(fitted,2)~=1 + multi=1; + else + multi=0; + end + res=out.res; + Xdist=out.rd; + Rsquared=out.rsquared; + cutoffX=out.cutoff.rd; + Se=out.cov; + k=size(out.slope,1); + %classical output + if isstruct(classic) + attribcl=classic.class; + fittedcl=classic.fitted; + Rdistcl=classic.resd; + standrescl=classic.stdres; + cutoffRcl=classic.cutoff.resd; + rescl=classic.res; + Xdistcl=classic.md; + cutoffXcl=classic.cutoff.md; + Rsquaredcl=classic.rsquared; + Secl=classic.cov; + end + case 'MLR' + fittedcl=out.fitted; + Rdistcl=out.resd; + cutoffRcl=out.cutoff.resd; + if size(fittedcl,2)~=1 + multi=1; + else + multi=0; + end + rescl=out.res; + Xdistcl=out.md; + cutoffXcl=out.cutoff.md; + Rsquaredcl=out.rsquared; + Secl=out.cov; + k=size(out.slope,1); + attribcl=out.class; + case 'RPCR' + fitted=out.fitted; + Rdist=out.resd; + cutoffR=out.cutoff.resd; + if size(fitted,2)~=1 + multi=1; + else + multi=0; + end + res=out.res; + Xdist=out.sd; + Rsquared=out.rsquared; + cutoffX=out.cutoff.sd; + cutoffO=out.cutoff.od; + OD=out.od; + Se=out.cov; + k=out.k; + L=out.robpca.L; + %classical output + if isstruct(classic) + attribcl='CPCR'; + Lcl=classic.cpca.L; + Xdistcl=classic.sd; + ODcl=classic.od; + cutoffOcl=classic.cutoff.od; + cutoffXcl=classic.cutoff.sd; + Rdistcl=classic.resd; + cutoffRcl=classic.cutoff.resd; + end + case 'LTS' + resid=out.res; + fitted=out.fitted; + scale=out.scale; + standres=resid/scale; + n=length(resid); + if isfield(out,'X') + x=out.X; + y=out.y; + else + x=0; + y=0; + end + Xdist=out.rd; + cutoffX=out.cutoff.rd; + cutoffY=out.cutoff.rd; + k=size(out.slope,1); + %classical output + if isstruct(classic) + attribcl='LS'; + residcl=classic.res; + fittedcl=classic.fitted; + scalecl=classic.scale; + standrescl=residcl/scalecl; + Xdistcl=classic.md; + cutoffXcl=classic.cutoff.md; + ncl=length(residcl); + if isfield(classic,'X') + xcl=classic.X; + ycl=classic.y; + else + xcl=0; + ycl=0; + end + end + case 'LS' + attribcl=attrib; + residcl=out.res; + fittedcl=out.fitted; + scalecl=out.scale; + Xdistcl=out.md; + cutoffXcl=out.cutoff.md; + standrescl=out.resd; + ncl=length(residcl); + if isfield(out,'X') + xcl=out.X; + ycl=out.y; + else + xcl=0; + ycl=0; + end + k=size(out.slope,1); + case 'CPCA' + Lcl=out.L; + Xdistcl=out.sd; + ODcl=out.od; + cutoffOcl=out.cutoff.od; + cutoffXcl=out.cutoff.sd; + k=out.k; + attribcl=attrib; + case 'CPCR' + fitted=out.fitted; + Lcl=out.cpca.L; + k=out.k; + Xdistcl=out.sd; + ODcl=out.od; + cutoffOcl=out.cutoff.od; + cutoffXcl=out.cutoff.sd; + Rdistcl=out.resd; + cutoffRcl=out.cutoff.resd; + if size(fitted,2)~=1 + multi=1; %multivariate analysis + else + multi=0; %univariate analysis + stdrescl=out.resd; + cutoffRcl=out.cutoff.resd; + end + attribcl=attrib; + case 'RSIMPLS' + fitted=out.fitted; + Rdist=out.resd; + cutoffR=out.cutoff.resd; + if size(fitted,2)~=1 + multi=1; + else + multi=0; + end + res=out.res; + Xdist=out.sd; + cutoffX=out.cutoff.sd; + cutoffO=out.cutoff.od; + OD=out.od; + Se=out.cov; + k=out.k; + %classical output + if isstruct(classic) + attribcl='CSIMPLS'; + Xdistcl=classic.sd; + ODcl=classic.od; + cutoffOcl=classic.cutoff.od; + cutoffXcl=classic.cutoff.sd; + Rdistcl=classic.resd; + cutoffRcl=classic.cutoff.resd; + end + case 'CSIMPLS' + fittedcl=out.fitted; + if size(fittedcl,2)~=1 + multi=1; + else + multi=0; + end + Xdistcl=out.sd; + ODcl=out.od; + k=out.k; + cutoffXcl=out.cutoff.sd; + cutoffOcl=out.cutoff.od; + attribcl=attrib; + Rdistcl=out.resd; + cutoffRcl=out.cutoff.resd; + case 'MCDCOV' + if ~isempty(out.plane) + disp('Warning (makeplot): The MCD covariance matrix is singular. No plots can be drawn.') + return + end + covar=out.cov; + md=out.md; + rd=out.rd; + if isfield(out,'X') + data=out.X; + else + data=0; + end + center=out.center; + cutoffR=out.cutoff.rd; + cutoffM=out.cutoff.md; + if isstruct(classic) + if isfield(out,'X') + datacl=out.X; + else + datacl=0; + end + centercl=out.classic.center; + covcl=out.classic.cov; + mdcl=out.classic.md; + attribcl=out.classic.class; + end + case 'COV' + %only available inside the mcdcov function. + case 'CDA' + if isfield(out,'x') + xcl=out.x; + groupcl=out.group; + else + xcl=0; + groupcl=0; + end + methodcl=out.method; + centercl=out.center; + covcl=out.cov; + classcl=out.class; + case 'RDA' + if isfield(out,'x') + x=out.x; group=out.group; + else + x=0;group=0; + end + method=out.method; + center=out.center; + covar=out.cov; + class=out.class; + if isstruct(classic) + if isfield(out.classic,'x') + xcl=out.classic.x; + groupcl=out.classic.group; + else + xcl=0; + groupcl=0; + end + methodcl=out.classic.method; + centercl=out.classic.center; + covcl=out.classic.cov; + classcl=out.classic.class; + end + case 'RSIMCA' + if isstruct(classic) + resultcl=out.classic; + else + resultcl=0; + end + case {'DIANA', 'AGNES'} + usedvars=[]; + separationstep=[]; + nobs=out.number; + class=out.class ; + heights=out.heights; + objectorder= out.objectorder; + case 'MONA' + usedvars=out.usedvariable; + separationstep=out.separationstep; + nobs=out.observations; + class=out.class; + heights=[]; + objectorder=out.objectorder; + case {'PAM','FANNY'} + sylinf=out.sylinf; + n=out.number; + ncluv=out.ncluv; + if size(out.x,1)==1 + x=out.dys; + else + x=out.x; + end + class= out.class; + case 'CLARA' + ncluv=out.ncluv; + x=out.x; +end + +%determination of the number of plots (robust or/and classic) +if strcmp(attrib,'LS') | strcmp(attrib,'CPCR') | strcmp(attrib,'CPCA') | strcmp(attrib,'MLR') | strcmp(attrib,'CDA') | strcmp(attrib,'CSIMPLS')|strcmp(attrib,'CSIMCA') + oneplot=1; %only classical plots possible +elseif (strcmp(attrib,'RAPCA') |strcmp(attrib,'MCDREG') | strcmp(attrib,'RPCR')|strcmp(attrib,'ROBPCA')| strcmp(attrib,'RSIMPLS')| strcmp(attrib,'MCDCOV')|strcmp(attrib,'LTS')|strcmp(attrib,'RDA')|strcmp(attrib,'RSIMCA')) & classicplots==0 + oneplot=2; %only robust plots possible +else + oneplot=0; %both robust and classical plot in the middle of the screen + bdwidth=5; + topbdwidth=30; + set(0,'Units','pixels'); + scnsize=get(0,'ScreenSize'); + pos1=[bdwidth, 1/3*scnsize(4)+bdwidth, scnsize(3)/2-2*bdwidth, scnsize(4)/2-(topbdwidth+bdwidth)]; + pos2=[pos1(1)+scnsize(3)/2, pos1(2), pos1(3), pos1(4)]; +end + +switch plotn + case 'all' %all possible plots + close + switch attrib + case {'MCDREG','MLR'} + if oneplot==0 + figure('Position',pos1) + regresdiagplot(Xdist,Rdist,cutoffX,cutoffR,k,multi,attrib,labsd,labresd) + figure('Position',pos2) + regresdiagplot(Xdistcl,Rdistcl,cutoffXcl,cutoffRcl,k,multi,attribcl,labsd,labresd) + elseif oneplot==1 + figure + regresdiagplot(Xdistcl,Rdistcl,cutoffXcl,cutoffRcl,k,multi,attribcl,labsd,labresd) %multi stond op 2 + elseif oneplot==2 + figure + regresdiagplot(Xdist,Rdist,cutoffX,cutoffR,k,multi,attrib,labsd,labresd) + end + case {'CPCA', 'ROBPCA'} + if oneplot==0 + figure('Position',pos1) + screeplot(L,attrib) + figure('Position',pos2) + screeplot(Lcl,attribcl) + pos1(2)=pos1(2)-40; + pos2(2)=pos2(2)-40; + figure('Position',pos1) + scorediagplot(Xdist,OD,k,cutoffX,cutoffO,attrib,labsd,labod) + figure('Position',pos2) + scorediagplot(Xdistcl,ODcl,k,cutoffXcl,cutoffOcl,attribcl,labsd,labod) + elseif oneplot==1 + figure + screeplot(Lcl,attrib) + figure + scorediagplot(Xdistcl,ODcl,k,cutoffXcl,cutoffOcl,attribcl,labsd,labod) + else + figure + screeplot(L,attrib) + figure + scorediagplot(Xdist,OD,k,cutoffX,cutoffO,attrib,labsd,labod) + end + case {'RSIMPLS','CSIMPLS'} + if oneplot==0 + figure('Position',pos1) + scorediagplot(Xdist,OD,k,cutoffX,cutoffO,attrib,labsd,labod) + figure('Position',pos2) + scorediagplot(Xdistcl,ODcl,k,cutoffXcl,cutoffOcl,attribcl,labsd,labod) + pos1(2)=pos1(2)-40; + pos2(2)=pos2(2)-40; + figure('Position',pos1) + regresdiagplot(Xdist,Rdist,cutoffX,cutoffR,k,multi,attribcl,labsd,labresd) + figure('Position',pos2) + regresdiagplot(Xdistcl,Rdistcl,cutoffXcl,cutoffRcl,k,multi,attribcl,labsd,labresd) + %3D plot is still highly memory consuming 08/04/04 + % if exist('OD') + % Nihil=(OD <= 1.e-06); + % OD(Nihil)=0; + % help=OD; + % elseif exist('ODcl') + % Nihil=(ODcl <= 1.e-06); + % ODcl(Nihil)=0; + % helpcl=ODcl; + % end + + % pos1(2)=pos1(2)-80; + % pos2(2)=pos2(2)-80; + % if ~all(help) %in case k=rank(T)=r then OD = zero + % figure('Position',pos1) + % regresdiagplot(Xdist,Rdist,cutoffX,cutoffR,k,multi,attrib,labsd,labresd) + % figure('Position',pos2) + % regresdiagplot(Xdistcl,Rdistcl,cutoffXcl,cutoffRcl,k,multi,attribcl,labsd,labresd) + % else + % figure('Position',pos1) + % regresdiagplot3d(Xdist,OD,Rdist,cutoffX,cutoffO,cutoffR,k,attrib,multi,labsd,labod,labresd,0) + % title(['Robust 3D outlier map based on ',num2str(k),' LV']) + % figure('Position',pos2) + % regresdiagplot3d(Xdistcl,ODcl,Rdistcl,cutoffXcl,cutoffOcl,cutoffRcl,k,attribcl,multi,labsd,labod,labresd,0) + % title(['Classical 3D outlier map based on ',num2str(k),' LV']) + % end + elseif oneplot==1 + figure + scorediagplot(Xdistcl,ODcl,k,cutoffXcl,cutoffOcl,attribcl,labsd,labod) + figure + regresdiagplot(Xdistcl,Rdistcl,cutoffXcl,cutoffRcl,k,multi,attribcl,labsd,labresd) + %3D plot is still highly memory consuming 08/04/04 + % if exist('ODcl') + % Nihil=(ODcl <= 1.e-06); + % ODcl(Nihil)=0; + % helpcl=ODcl; + % end + % figure + % if ~all(helpcl) %in case k=rank(T)=r then OD = zero + % regresdiagplot(Rdistcl,Rdistcl,cutoffXcl,cutoffRcl,k,multi,'CPCR',labsd,labresd) + % else + % regresdiagplot3d(Xdistcl,ODcl,Rdistcl,cutoffXcl,cutoffOcl,cutoffRcl,k,attribcl,multi,labsd,labod,labresd,0) + % title(['Classical 3D outlier map based on ',num2str(k),' LV']) + % end + else + figure + scorediagplot(Xdist,OD,k,cutoffX,cutoffO,attrib,labsd,labod) + figure + regresdiagplot(Xdist,Rdist,cutoffX,cutoffR,k,multi,attrib,labsd,labresd) + %3D plot is still highly memory consuming 08/04/04 + % if exist('OD') + % Nihil=(OD <= 1.e-06); + % OD(Nihil)=0; + % help=OD; + % end + % figure + % if ~all(help) %in case k=rank(T)=r then OD = zero % nog verfijnen is niet volledig nul is zeer klein (<10^-6) + % regresdiagplot(Xdist,Rdist,cutoffX,cutoffR,k,multi,attrib,labsd,labresd) + % else + % regresdiagplot3d(Xdist,OD,Rdist,cutoffX,cutoffO,cutoffR,k,attrib,multi,labsd,labod,labresd,0) + % title(['Robust 3D outlier map based on ',num2str(k),' LV']) + % end + end + case {'CPCR','RPCR'} + if oneplot==0 + figure('Position',pos1) + screeplot(L,attrib) + figure('Position',pos2) + screeplot(Lcl,attrib) + pos1(2)=pos1(2)-40; + pos2(2)=pos2(2)-40; + figure('Position',pos1) + scorediagplot(Xdist,OD,k,cutoffX,cutoffO,attrib,labsd,labod) + figure('Position',pos2) + scorediagplot(Xdistcl,ODcl,k,cutoffXcl,cutoffOcl,attribcl,labsd,labod) + pos1(2)=pos1(2)-80; + pos2(2)=pos2(2)-80; + figure('Position',pos1) + regresdiagplot(Xdist,Rdist,cutoffX,cutoffR,k,multi,attribcl,labsd,labresd) + figure('Position',pos2) + regresdiagplot(Xdistcl,Rdistcl,cutoffXcl,cutoffRcl,k,multi,attribcl,labsd,labresd) + %3D plot is still highly memory consuming 08/04/04 + % if exist('OD') + % Nihil=(OD <= 1.e-06); + % OD(Nihil)=0; + % help=OD; + % elseif exist('ODcl') + % Nihil=(ODcl <= 1.e-06); + % ODcl(Nihil)=0; + % helpcl=ODcl; + % end + % if ~all(help) %in case k=rank(T)=r then OD = zero + % figure('Position',pos1) + % regresdiagplot(Xdist,Rdist,cutoffX,cutoffR,k,multi,attrib,labsd,labresd) + % figure('Position',pos2) + % regresdiagplot(Xdistcl,Rdistcl,cutoffXcl,cutoffRcl,k,multi,attribcl,labsd,labresd) + % else + % figure('Position',pos1) + % regresdiagplot3d(Xdist,OD,Rdist,cutoffX,cutoffO,cutoffR,k,attrib,multi,labsd,labod,labresd,0) + % title(['Robust 3D outlier map based on ',num2str(k),' LV']) + % figure('Position',pos2) + % regresdiagplot3d(Xdistcl,ODcl,Rdistcl,cutoffXcl,cutoffOcl,cutoffRcl,k,attribcl,multi,labsd,labod,labresd,0) + % title(['Classical 3D outlier map based on ',num2str(k),' LV']) + % end + elseif oneplot==1 + figure + screeplot(Lcl,attrib) + figure + scorediagplot(Xdistcl,ODcl,k,cutoffXcl,cutoffOcl,attrib,labsd,labod) + figure + regresdiagplot(Xdistcl,Rdistcl,cutoffXcl,cutoffRcl,k,multi,attribcl,labsd,labresd) + %3D plot is still highly memory consuming 08/04/04 + % if exist('ODcl') + % Nihil=(ODcl <= 1.e-06); + % ODcl(Nihil)=0; + % helpcl=ODcl; + % end + % figure + % if ~all(helpcl) %in case k=rank(T)=r then OD = zero + % regresdiagplot(Xdistcl,Rdistcl,cutoffXcl,cutoffRcl,k,multi,'CPCR',labsd,labresd) + % else + % regresdiagplot3d(Xdistcl,ODcl,Rdistcl,cutoffXcl,cutoffOcl,cutoffRcl,k,attribcl,multi,labsd,labod,labresd,0) + % title(['Classical 3D outlier map based on ',num2str(k),' LV']) + % end + else + figure + screeplot(L,attrib) + figure + scorediagplot(Xdist,OD,k,cutoffX,cutoffO,attrib,labsd,labod) + figure + regresdiagplot(Xdist,Rdist,cutoffX,cutoffR,k,multi,attrib,labsd,labresd) + %3D plot is still highly memory consuming 08/04/04 + % figure + % if exist('OD') + % Nihil=(OD <= 1.e-06); + % OD(Nihil)=0; + % help=OD; + % end + % if ~all(help) %in case k=rank(T)=r then OD = zero + % regresdiagplot(Xdist,Rdist,cutoffX,cutoffR,k,multi,attrib,labsd,labresd) + % else + % regresdiagplot3d(Xdist,OD,Rdist,cutoffX,cutoffO,cutoffR,k,attrib,multi,labsd,labod,labresd,0) + % title(['Robust 3D outlier map based on ',num2str(k),' LV']) + % end + end + case 'MCDCOV' + if oneplot==0 + figure('Position',pos1) + distplot(rd,cutoffR,attrib,labmcd) + figure('Position',pos2) + distplot(md,cutoffM,attribcl,labmcd) + figure('Position',pos1) + chiqqplot(rd,size(data,2),attrib) + figure('Position',pos2) + chiqqplot(md,size(data,2),attribcl) + figure + ddplot(md,rd,cutoffM,attrib,labmcd) + figure + if size(data,2)~=2|data==0 + axes + box on + text(0.05,0.5,'Tolerance ellipse is only available for bivariate data','color','r') + else + ellipsplot(center,covar,data,rd,labmcd) + set(findobj('color','r'),'color','m'); + hold on + ellipsplot(centercl,covcl,data,md,labmcd) + set(findobj('color','r'),'color','b','linestyle',':'); + hold off + legend_handles=[findobj('color','m'); findobj('color','b','linestyle',':')]; + legend(legend_handles,'robust','classical','Location','best') + end + elseif oneplot==2 + figure + distplot(rd,cutoffM,attrib,labmcd) + figure + chiqqplot(rd,size(data,2),attrib) + figure + ddplot(md,rd,cutoffM,attrib,labmcd) + figure + if size(data,2)~=2|data==0 + axes + box on + text(0.05,0.5,'Tolerance ellipse is only available for bivariate data','color','r') + else + ellipsplot(center,covar,data,rd,labmcd,'MCDCOV') + end + end + case {'LTS','LS'} + if oneplot==0 + figure('Position',pos1) + residualplot(fitted,standres,attrib,'Fitted values','Standardized LTS residual',lablts) + figure('Position',pos2) + residualplot(fittedcl,standrescl,attribcl,'Fitted value','Standardized LS residual',lablts) + pos1(2)=pos1(2)-40; + pos2(2)=pos2(2)-40; + figure('Position',pos1) + residualplot(1:n,standres,attrib,'Index','Standardized LTS residual',lablts) + figure('Position',pos2) + residualplot(1:ncl,standrescl,attribcl,'Index','Standardized LS residual',lablts) + pos1(2)=pos1(2)-80; + pos2(2)=pos2(2)-80; + figure('Position',pos1) + normqqplot(resid,attrib) + figure('Position',pos2) + normqqplot(residcl,attribcl) + pos1(2)=pos1(2)-100; + pos2(2)=pos2(2)-100; + figure('Position',pos1) + if strcmp(attrib,'LTS') + regresdiagplot(Xdist,standres,cutoffX,cutoffY,k,0,attrib,labsd,labresd); + else + regresdiagplot(Xdist,Rdist,cutoffX,cutoffR,k,1,attrib,labsd,labresd); + end + figure('Position',pos2) + if strcmp(attribcl,'LS') + regresdiagplot(Xdistcl,standrescl,cutoffXcl,0,k,0,attribcl,labsd,labresd); + else + regresdiagplot(Xdistcl,Rdistcl,cutoffXcl,cutoffRcl,k,1,attribcl,labsd,labresd); + end + pos1(2)=pos1(2)-120; + pos2(2)=pos2(2)-120; + figure('Position',pos1) + if size(x,2)~=1|x==0 + axes + box on + text(0.05,0.5,'Scatter plot is only available for bivariate data','color','r') + else + lsscatter(x,y,fitted,attrib) + end + figure('Position',pos2) + if size(xcl,2)~=1|xcl==0 + axes + box on + text(0.05,0.5,'Scatter plot is only available for bivariate data','color','r') + else + lsscatter(xcl,ycl,fittedcl,attribcl) + end + elseif oneplot ==1 + figure + residualplot(fittedcl,standrescl,attribcl,'Fitted value','Standardized LS residual',lablts) ; + figure + residualplot(1:ncl,standrescl,attribcl,'Index','Standardized LS residual',lablts) ; + figure + normqqplot(residcl,attrib); + figure + if strcmp(attrib,'LS') + regresdiagplot(Xdistcl,standrescl,cutoffXcl,0,k,0,attrib,labsd,labresd); + else + regresdiagplot(Xdistcl,Rdistcl,cutoffXcl,cutoffRcl,k,1,attrib,labsd,labresd); + end + figure + if size(xcl,2)~=1|xcl==0 + axes + box on + text(0.05,0.5,'Scatter plot is only available for bivariate data','color','r') + else + lsscatter(xcl,ycl,fittedcl,attribcl) + end + elseif oneplot==2 + figure + residualplot(fitted,standres,attrib, 'Fitted value','Standardized LTS residual',lablts) + figure + residualplot(1:n,standres,attrib,'Index','Standardized LTS residual',lablts) + figure + normqqplot(resid,attrib) + figure + if strcmp(attrib,'LTS') + regresdiagplot(Xdist,standres,cutoffX,0,k,0,attrib,labsd,labresd); + else + regresdiagplot(Xdist,Rdist,cutoffX,cutoffR,k,1,attrib,labsd,labresd); + end + figure + if size(x,2)~=1|x==0 + axes + box on + text(0.05,0.5,'Scatter plot is only available for bivariate data','color','r') + else + lsscatter(x,y,fitted,attrib) + end + end + case{'RDA','CDA'} + if oneplot==0 + figure('Position',pos1) + daplot(x,group,center,covar,class,method) + figure('Position',pos2) + daplot(xcl,groupcl,centercl,covcl,classcl,methodcl) + elseif oneplot==1 + figure + daplot(xcl,groupcl,centercl,covcl,classcl,methodcl) + elseif oneplot==2 + figure + daplot(x,group,center,covar,class,method) + end + case{'RSIMCA','CSIMCA'} + if oneplot==0 + figure('Position',pos1) + simcaplot(result) + figure('Position',pos2) + simcaplot(resultcl) + elseif oneplot==1 + figure + simcaplot(resultcl) + elseif oneplot==2 + figure + simcaplot(result) + end + case {'DIANA','AGNES'} + bannerplot(usedvars,separationstep,nobs,class,heights,objectorder) + tree(objectorder,heights) + case 'MONA' + bannerplot(usedvars,separationstep,nobs,class,heights,objectorder) + case {'PAM','FANNY'} + silhouetteplot(sylinf,n,class) + clusplot(x,ncluv,labclus) + case 'CLARA' + clusplot(x,ncluv,labclus) + end + choice=exitno; + case 'scree' %screeplot + close + if oneplot==0 + figure('Position',pos1) + screeplot(L,attrib) + figure('Position',pos2) + screeplot(Lcl,attribcl) + elseif oneplot==1 + screeplot(Lcl,attribcl) + else + screeplot(L,attrib) + end + case 'pcadiag' %diagnostic Scoreplot (Xdist-Odist) + %close + if oneplot==0 + figure('Position',pos1) + scorediagplot(Xdist,OD,k,cutoffX,cutoffO,attrib,labsd,labod) + figure('Position',pos2) + scorediagplot(Xdistcl,ODcl,k,cutoffXcl,cutoffOcl,attribcl,labsd,labod) + elseif oneplot==1 + figure + scorediagplot(Xdistcl,ODcl,k,cutoffXcl,cutoffOcl,attribcl,labsd,labod) + else + figure + scorediagplot(Xdist,OD,k,cutoffX,cutoffO,attrib,labsd,labod) + end + case '3ddiag' %3D-plot: highly memory consuming!!!! + close + if oneplot==0 + if exist('OD') + Nihil=(OD <= 1.e-06); + OD(Nihil)=0; + help=OD; + elseif exist('ODcl') + Nihil=(ODcl <= 1.e-06); + ODcl(Nihil)=0; + helpcl=ODcl; + end + if ~all(help)%in case k=rank(T)=r then OD = zero + figure('Position',pos1) + regresdiagplot(Xdist,Rdist,cutoffX,cutoffR,k,multi,attrib,labsd,labresd) + figure('Position',pos2) + regresdiagplot(Xdistcl,Rdistcl,cutoffXcl,cutoffRcl,k,multi,attribcl,labsd,labresd) + else + figure('Position',pos1) + regresdiagplot3d(Xdist,OD,Rdist,cutoffX,cutoffO,cutoffR,k,attrib,multi,labsd,labod,labresd,0) + title(['Robust 3D outlier map based on ',num2str(k),' LV']) + figure('Position',pos2) + regresdiagplot3d(Xdistcl,ODcl,Rdistcl,cutoffXcl,cutoffOcl,cutoffRcl,k,attribcl,multi,labsd,labod,labresd,0) + title(['Classical 3D outlier map based on ',num2str(k),' LV']) + end + elseif oneplot==1 + if exist('ODcl') + Nihil=(ODcl <= 1.e-06); + ODcl(Nihil)=0; + helpcl=ODcl; + end + if ~all(helpcl) %in case k=rank(T)=r then OD = zero + regresdiagplot(Xdistcl,Rdistcl,cutoffXcl,cutoffRcl,k,multi,attribcl,labsd,labresd) + else + regresdiagplot3d(Xdistcl,ODcl,Rdistcl,cutoffXcl,cutoffOcl,cutoffRcl,k,attribcl,multi,labsd,labod,labresd,0) + title(['Classical 3D outlier map based on ',num2str(k),' LV']) + end + else + if exist('OD') + Nihil=(OD <= 1.e-06); + OD(Nihil)=0; + help=OD; + end + if ~all(help) %in case k=rank(T)=r then OD = zero + regresdiagplot(Xdist,Rdist,cutoffX,cutoffR,k,multi,attrib,labsd,labresd) + else + regresdiagplot3d(Xdist,OD,Rdist,cutoffX,cutoffO,cutoffR,k,attrib,multi,labsd,labod,labresd,0) + title(['Robust 3D outlier map based on ',num2str(k),' LV']) + end + end + case {'Idist','robdist'} + close + if oneplot==0 + figure('Position',pos1) + distplot(rd,cutoffM,'MCDCOV',labmcd) + figure('Position',pos2) + distplot(md,cutoffM,'COV',labmcd) + %elseif oneplot==1 not available since 'COV' is only called inside + %the MCDCOV function + elseif oneplot==2 + distplot(rd,cutoffM,'MCDCOV',labmcd) + end + case 'qqmcd' + close + if oneplot==0 + figure('Position',pos1) + chiqqplot(rd,size(data,2),'MCDCOV') + figure('Position',pos2) + chiqqplot(md,size(data,2),'COV') + elseif oneplot==2 + chiqqplot(rd,size(data,2),'MCDCOV') + end + case 'dd' + close + ddplot(md,rd,cutoffM,attrib,labmcd) + case 'ellipse' + close + if oneplot==0 + if size(data,2)~=2|data==0 + axes + box on + text(0.05,0.5,'Tolerance ellipse plot is only available for bivariate data','color','r') + else + ellipsplot(center,covar,data,rd,labmcd) + set(findobj('color','r'),'color','m'); + hold on + ellipsplot(centercl,covcl,data,md,labmcd) + set(findobj('color','r'),'color','b','linestyle',':'); + hold off + legend_handles=[findobj('color','m'); findobj('color','b','linestyle',':')]; + legend(legend_handles,'robust','classical','Location','best') + end + elseif oneplot==2 + if size(data,2)~=2|data==0 + axes + box on + text(0.05,0.5,'Tolerance ellipse plot is only available for bivariate data','color','r') + else + ellipsplot(center,covar,data,rd,labmcd) + end + end + case 'resfit' + close + if oneplot==0 + figure('Position',pos1) + residualplot(fitted,standres,attrib,'Fitted value','Standardized LTS residual',lablts) + figure('Position',pos2) + residualplot(fittedcl,standrescl,attribcl,'Fitted value','Standardized LS residual',lablts) + elseif oneplot==1 + residualplot(fittedcl,standrescl,attribcl,'Fitted value','Standardized LS residual',lablts) + elseif oneplot==2 + residualplot(fitted,standres,attrib,'Fitted value','Standardized LTS residual',lablts) + end + case 'resindex' + close + if oneplot==0 + figure('Position',pos1) + residualplot(1:n,standres,attrib,'Index','Standardized LTS residual',lablts) + figure('Position',pos2) + residualplot(1:ncl,standrescl,attribcl,'Index','Standardized LS residual',lablts) + elseif oneplot==1 + residualplot(1:ncl,standrescl,attribcl,'Index','Standardized LS residual',lablts) + elseif oneplot==2 + residualplot(1:n,standres,attrib,'Index','Standardized LTS residual',lablts) + end + case 'qqlts' + close + if oneplot==0 + figure('Position',pos1) + normqqplot(resid,'LTS') + figure('Position',pos2) + normqqplot(residcl,'LS') + elseif oneplot==1 + normqqplot(residcl,'LS') + elseif oneplot==2 + normqqplot(resid,'LTS') + end + case 'regdiag' + %close + if oneplot==0 + figure('Position',pos1) + if strcmp(attrib,'LTS') + regresdiagplot(Xdist,standres,cutoffX,0,k,0,attrib,labsd,labresd); + else + regresdiagplot(Xdist,Rdist,cutoffX,cutoffR,k,multi,attrib,labsd,labresd); + end + figure('Position',pos2) + if strcmp(attribcl,'LS') + regresdiagplot(Xdistcl,standrescl,cutoffXcl,0,k,0,attribcl,labsd,labresd); + else + regresdiagplot(Xdistcl,Rdistcl,cutoffXcl,cutoffRcl,k,multi,attribcl,labsd,labresd); + end + elseif oneplot==1 + if strcmp(attrib,'LS') + regresdiagplot(Xdistcl,standrescl,cutoffXcl,0,k,0,attrib,labsd,labresd); + else + regresdiagplot(Xdistcl,Rdistcl,cutoffXcl,cutoffRcl,k,multi,attrib,labsd,labresd); + end + elseif oneplot==2 + if strcmp(attrib,'LTS') + regresdiagplot(Xdist,standres,cutoffX,0,k,0,attrib,labsd,labresd); + else + regresdiagplot(Xdist,Rdist,cutoffX,cutoffR,k,multi,attrib,labsd,labresd); + end + end + case 'scatter' + close + if oneplot==0 + figure('Position',pos1) + if size(x,2)~=1|x==0 + axes + box on + text(0.05,0.5,'Scatter plot is only available for bivariate data','color','r') + else + lsscatter(x,y,fitted,attrib) + end + figure('Position',pos2) + if size(xcl,2)~=1|xcl==0 + axes + box on + text(0.05,0.5,'Scatter plot is only available for bivariate data','color','r') + else + lsscatter(xcl,ycl,fittedcl,attribcl) + end + elseif oneplot==1 + if size(xcl,2)~=1|xcl==0 + axes + box on + text(0.05,0.5,'Scatter plot is only available for bivariate data','color','r') + else + lsscatter(xcl,ycl,fittedcl,attribcl) + end + elseif oneplot==2 + if size(x,2)~=1|x==0 + axes + box on + text(0.05,0.5,'Scatter plot is only available for bivariate data','color','r') + else + lsscatter(x,y,fitted,attrib) + end + end + case 'da' + close + if oneplot==0 + figure('Position',pos1) + daplot(x,group,center,covar,class,method) + figure('Position',pos2) + daplot(xcl,groupcl,centercl,covcl,classcl,methodcl) + elseif oneplot==1 + daplot(xcl,groupcl,centercl,covcl,classcl,methodcl) + elseif oneplot==2 + daplot(x,group,center,covar,class,method) + end + case 'simca' + close + if oneplot==0 + figure('Position',pos1) + simcaplot(out) + figure('Position',pos2) + simcaplot(resultcl) + elseif oneplot==1 + simcaplot(out) + elseif oneplot==2 + simcaplot(out) + end + case 'banner' + bannerplot(usedvars,separationstep,nobs,class,heights,objectorder) + case 'tree' + tree(objectorder,heights) + case 'silhouet' + silhouetteplot(sylinf,n,class) + case 'clus' + clusplot(x,ncluv,1,labclus) +end + +%--------------------------------------------------------------------------------------------------------------------------- +function choice= menucov(out,attrib,exitno,labmcd,classic,classicplots) + +choice=menu('Covariance plots: ','All','Index plot of the distances','Quantile-Quantile plot of the distances','Distance-distance plot',... + 'Tolerance ellipse (for bivariate data)','Exit'); +switch choice + case 1 + plotn='all'; + whichplot(out,plotn,attrib,exitno,3,3,3,labmcd,3,[],classic,classicplots) + case 2 + plotn='Idist'; + whichplot(out,'Idist',attrib,exitno,3,3,3,labmcd,3,[],classic,classicplots) + case 3 + plotn='qqmcd'; + whichplot(out,plotn,attrib,exitno,3,3,3,labmcd,3,[],classic,classicplots) + case 4 + plotn='dd'; + whichplot(out,plotn,attrib,exitno,3,3,3,labmcd,3,[],classic,classicplots) + case 5 + plotn='ellipse'; + whichplot(out,plotn,attrib,exitno,3,3,3,labmcd,3,[],classic,classicplots) + case 6 + choice=exitno; +end +%--------------------------------------------------------------------------------------------------------------------------- +function choice = menureg(out,attrib,exitno,labsd,labresd,classic,classicplots) +choice=menu('Regression Plots: ','All ','Regression outlier map', 'Exit '); +switch choice + case 1 + plotn='all'; + whichplot(out,plotn,attrib,exitno,labsd,3,labresd,3,3,[],classic,classicplots) + case 2 + plotn='regdiag'; + whichplot(out,plotn,attrib,exitno,labsd,3,labresd,3,3,[],classic,classicplots) + case 3 + choice=exitno; +end +%--------------------------------------------------------------------------------------------------------------------------- +function choice = menuscore(out,attrib,exitno,labsd,labod,classic,classicplots) +choice=menu('Score Plots: ','All ','Scree plot','Score outlier map', 'Exit '); +switch choice + case 1 + plotn='all'; + whichplot(out,plotn,attrib,exitno,labsd,labod,3,3,3,[],classic,classicplots) + case 2 + plotn='scree'; + whichplot(out,plotn,attrib,exitno,labsd,labod,3,3,3,[],classic,classicplots) + case 3 + plotn='pcadiag'; + whichplot(out,plotn,attrib,exitno,labsd,labod,3,3,3,[],classic,classicplots) + case 4 + choice=exitno; +end + +%--------------------------------------------------------------------------------------------------------------------------- +function choice = menupls(out,attrib,exitno,labsd,labod,labresd,classic,classicplots) +choice=menu('PLS Plots: ','All ','Score outlier map',... + 'Regression outlier map', '3D outlier map (Highly memory consuming)','Exit '); +switch choice + case 1 + plotn='all'; + whichplot(out,plotn,attrib,exitno,labsd,labod,labresd,3,3,[],classic,classicplots) + case 2 + plotn='pcadiag'; + whichplot(out,plotn,attrib,exitno,labsd,labod,labresd,3,3,[],classic,classicplots) + case 3 + plotn='regdiag'; + whichplot(out,plotn,attrib,exitno,labsd,labod,labresd,3,3,[],classic,classicplots) + case 4 + plotn='3ddiag'; + whichplot(out,plotn,attrib,exitno,labsd,labod,labresd,3,3,[],classic,classicplots) + case 5 + choice=exitno; +end +%--------------------------------------------------------------------------------------------------------------------------- +function choice=menuscoreg(out,attrib,exitno,labsd,labod,labresd,classic,classicplots) + +choice=menu('Score and Regression Plots: ','All ','Scree plot','Score outlier map',... + 'Regression outlier map', '3D outlier map (Highly memory consuming)', 'Exit '); + +switch choice + case 1 + plotn='all'; + whichplot(out,plotn,attrib,exitno,labsd,labod,labresd,3,3,[],classic,classicplots) + case 2 + plotn='scree'; + whichplot(out,plotn,attrib,exitno,labsd,labod,labresd,3,3,[],classic,classicplots) + case 3 + plotn='pcadiag'; + whichplot(out,plotn,attrib,exitno,labsd,labod,labresd,3,3,[],classic,classicplots) + case 4 + plotn='regdiag'; + whichplot(out,plotn,attrib,exitno,labsd,labod,labresd,3,3,[],classic,classicplots) + case 5 + plotn='3ddiag'; + whichplot(out,plotn,attrib,exitno,labsd,labod,labresd,3,3,[],classic,classicplots) + case 6 + choice=exitno; +end +%--------------------------------------------------------------------------------------------------------------------------- +function choice= menuls(out,attrib,exitno,labsd,labresd,lablts,classic,classicplots) + +choice=menu('Residual plots: ', 'All', 'Standardized residuals versus fitted values',... + 'Index plot of standardized residuals','Normal QQplot of residuals',... + 'Diagnostic plot of residuals versus robust distances',... + 'Scatter plot with regression line','Exit'); +switch choice + case 1 + plotn='all'; + whichplot(out,plotn,attrib,exitno,labsd,3,labresd,3,lablts,[],classic,classicplots) + case 2 + plotn='resfit'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,lablts,[],classic,classicplots) + case 3 + plotn='resindex'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,lablts,[],classic,classicplots) + case 4 + plotn='qqlts'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,lablts,[],classic,classicplots) + case 5 + plotn='regdiag'; + whichplot(out,plotn,attrib,exitno,labsd,3,labresd,3,lablts,[],classic,classicplots) + case 6 + plotn='scatter'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,lablts,[],classic,classicplots) + case 7 + choice=exitno; +end +%--------------------------------------------------------------------------------------------------------------------------- +function choice = menuda(out,attrib,exitno,classic,classicplots) + +choice=menu('Discriminant analysis: ','All ','Tolerance ellipses (for bivariate data)', 'Exit '); +switch choice + case 1 + plotn='all'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,3,[],classic,classicplots) + case 2 + plotn='da'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,3,[],classic,classicplots) + case 3 + choice=exitno; +end +%------------------------------------------------------------------------------------------------------------------------------ +function choice = menusimca(out,attrib,exitno,classic,classicplots) + +choice=menu('SIMCA analysis: ','All ','Scatter plot', 'Exit '); +switch choice + case 1 + plotn='all'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,3,[],classic,classicplots) + case 2 + plotn='simca'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,3,[],classic,classicplots) + case 3 + choice=exitno; +end +%-------------------------------------------------------------------------- +function choice = menuclus1(out,attrib,exitno) + +choice=menu('Cluster analysis: ','All ','Banner plot', 'Tree plot (n<30)', 'Exit '); +switch choice + case 1 + plotn='all'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,3,[],0,0) + case 2 + plotn='banner'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,3,[],0,0) + case 3 + plotn='tree'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,3,[],0,0) + case 4 + choice=exitno; +end +%-------------------------------------------------------------------------- +function choice = menuclus2(out,attrib,exitno) + +choice=menu('Cluster analysis: ','All ', 'Banner', 'Exit '); +switch choice + case 1 + plotn='all'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,3,[],0,0) + case 2 + plotn='banner'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,3,[],0,0) + case 3 + choice=exitno; +end +%------------------------------------------------------------------------ +function choice = menuclus3(out,attrib,exitno,labclus) +choice=menu('Cluster analysis: ','All ','Silhouette plot', 'Clusplot', 'Exit '); +switch choice + case 1 + plotn='all'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,3,labclus,0,0) + case 2 + plotn='silhouet'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,3,labclus,0,0) + case 3 + plotn='clus'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,3,labclus,0,0) + case 4 + choice=exitno; +end +%-------------------------------------------------------------------------- +function choice = menuclus4(out,attrib,exitno,labclus) +choice=menu('Cluster analysis: ','All ', 'Clusplot', 'Exit '); +switch choice + case 1 + plotn='all'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,3,labclus,0,0) + case 2 + plotn='clus'; + whichplot(out,plotn,attrib,exitno,3,3,3,3,3,labclus,0,0) + case 3 + choice=exitno; +end diff --git a/LIBRA/mc.m b/LIBRA/mc.m new file mode 100644 index 0000000..52d920d --- /dev/null +++ b/LIBRA/mc.m @@ -0,0 +1,48 @@ +function [result] = mc(x) + +%MC calculates the medcouple, a robust measure of skewness. +% +% The medcouple is described in: +% Brys, G., Hubert, M. and Struyf, A. (2004), +% "A Robust Measure of Skewness", +% Journal of Computational and Graphical Statistics, +% 13, 996-1017. +% +% Required input arguments: +% x : Data matrix. +% Rows of x represent observations, and columns represent variables. +% Missing values (NaN's) and infinite values (Inf's) are allowed, since observations (rows) +% with missing or infinite values will automatically be excluded from the computations. +% +% The output of MC is a vector containing the medcouple for each column +% of the data matrix x. +% +% Example: +% result = mc([chi2rnd(5,1000,1) trnd(3,1000,1)]); +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Guy Brys +% Last Update: 31/07/2007 + +if (nargin<1) + error('No input arguments') +end +[n,p] = size(x); +if n==1 + x = x'; + p = 1; +end +x(sum(~isfinite(x),2)>0,:)=[]; +n = size(x,1); +if (n>50000) + error('When there are more than 50000 observations, the MC may be uncomputable due to memory limitations.') +elseif (n>100) + result = medc(x); +elseif n==0 + error('Due to missing values, the data matrix is empty and the MC can not be computed.') +else + result = 0.5*(-medc(repmat(max(x),size(x,1),1)-x)+medc(x)); +end diff --git a/LIBRA/mcdcov.m b/LIBRA/mcdcov.m new file mode 100644 index 0000000..a01c1ae --- /dev/null +++ b/LIBRA/mcdcov.m @@ -0,0 +1,1445 @@ +function [rew,raw]=mcdcov(x,varargin) + +%MCDCOV computes the MCD estimator of a multivariate data set. This +% estimator is given by the subset of h observations with smallest covariance +% determinant. The MCD location estimate is then the mean of those h points, +% and the MCD scatter estimate is their covariance matrix. The default value +% of h is roughly 0.75n (where n is the total number of observations), but the +% user may choose each value between n/2 and n. Based on the raw estimates, +% weights are assigned to the observations such that outliers get zero weight. +% The reweighted MCD estimator is then given by the mean and covariance matrix +% of the cases with non-zero weight. To compute the MCD estimator, +% the FASTMCD algorithm is used. +% +% The MCD method is intended for continuous variables, and assumes that +% the number of observations n is at least 5 times the number of variables p. +% If p is too large relative to n, it would be better to first reduce +% p by variable selection or robust principal components (see the functions +% robpca.m and rapca.m). +% +% The MCD method was introduced in: +% +% Rousseeuw, P.J. (1984), "Least Median of Squares Regression," +% Journal of the American Statistical Association, Vol. 79, pp. 871-881. +% +% The MCD is a robust method in the sense that the estimates are not unduly +% influenced by outliers in the data, even if there are many outliers. +% Due to the MCD's robustness, we can detect outliers by their large +% robust distances. The latter are defined like the usual Mahalanobis +% distance, but based on the MCD location estimate and scatter matrix +% (instead of the nonrobust sample mean and covariance matrix). +% +% The FASTMCD algorithm uses several time-saving techniques which +% make it available as a routine tool to analyze data sets with large n, +% and to detect deviating substructures in them. A full description of the +% algorithm can be found in: +% +% Rousseeuw, P.J. and Van Driessen, K. (1999), "A Fast Algorithm for the +% Minimum Covariance Determinant Estimator," Technometrics, 41, pp. 212-223. +% +% An important feature of the FASTMCD algorithm is that it allows for exact +% fit situations, i.e. when more than h observations lie on a (hyper)plane. +% Then the program still yields the MCD location and scatter matrix, the latter +% being singular (as it should be), as well as the equation of the hyperplane. +% +% +% Required input argument: +% x : a vector or matrix whose columns represent variables, and rows represent observations. +% Missing values (NaN's) and infinite values (Inf's) are allowed, since observations (rows) +% with missing or infinite values will automatically be excluded from the computations. +% +% Optional input arguments: +% cor : If non-zero, the robust correlation matrix will be +% returned. The default value is 0. +% h : The quantile of observations whose covariance determinant will +% be minimized. Any value between n/2 and n may be specified. +% The default value is 0.75*n. +% alpha : (1-alpha) measures the fraction of outliers the algorithm should +% resist. Any value between 0.5 and 1 may be specified. (default = 0.75) +% ntrial : The number of random trial subsamples that are drawn for +% large datasets. The default is 500. +% plots : If equal to one, a menu is shown which allows to draw several plots, +% such as a distance-distance plot. (default) +% If 'plots' is equal to zero, all plots are suppressed. +% See also makeplot.m +% classic : If equal to one, the classical mean and covariance matrix are computed as well. +% (default = 0) +% +% Input arguments for advanced users: +% Hsets : Instead of random trial h-subsets (default, Hsets = []), Hsets makes it possible to give certain +% h-subsets as input. Hsets is a matrix that contains the indices of the observations of one +% h-subset as a row. +% factor : If not equal to 0 (default), the consistency factor is adapted. Only usefull in case of the +% kmax approach. +% +% I/O: result=mcdcov(x,'alpha',0.75,'h',h,'ntrial',500) +% If only one output argument is listed, only the final result ('result') +% is returned. +% The user should only give the input arguments that have to change their default value. +% The name of the input arguments needs to be followed by their value. +% The order of the input arguments is of no importance. +% +% Examples: [rew,raw]=mcdcov(x); +% result=mcdcov(x,'h',20,'plots',0); +% [rew,raw]=mcdcov(x,'alpha',0.8,'cor',0) +% +% The output structure 'raw' contains intermediate results, with the following +% fields : +% +% raw.center : The raw MCD location of the data. +% raw.cov : The raw MCD covariance matrix (multiplied by a consistency factor). +% raw.cor : The raw MCD correlation matrix, if input argument 'cor' was non-zero. +% raw.wt : Weights based on the estimated raw covariance matrix 'raw.cov' and +% the estimated raw location 'raw.center' of the data. These weights determine +% which observations are used to compute the final MCD estimates. +% raw.objective : The determinant of the raw MCD covariance matrix. +% +% The output structure 'rew' contains the final results, namely: +% +% rew.center : The robust location of the data, obtained after reweighting, if +% the raw MCD is not singular. Otherwise the raw MCD center is +% given here. +% rew.cov : The robust covariance matrix, obtained after reweighting, if the raw MCD +% is not singular. Otherwise the raw MCD covariance matrix is given here. +% rew.cor : The robust correlation matrix, obtained after reweighting, if +% options.cor was non-zero. +% rew.h : The number of observations that have determined the MCD estimator, +% i.e. the value of h. +% rew. Hsubsets : A structure that contains Hopt and Hfreq: +% Hopt : The subset of h points whose covariance matrix has minimal determinant, +% ordered following increasing robust distances. +% Hfreq : The subset of h points which are the most frequently selected during the whole +% algorithm. +% rew.alpha : (1-alpha) measures the fraction of outliers the algorithm should +% resist. +% rew.md : The distance of each observation from the classical +% center of the data, relative to the classical shape +% of the data. Often, outlying points fail to have a +% large Mahalanobis distance because of the masking +% effect. +% rew.rd : The distance of each observation to the final, +% reweighted MCD center of the data, relative to the +% reweighted MCD scatter of the data. These distances allow +% us to easily identify the outliers. If the reweighted MCD +% is singular, raw.rd is given here. +% rew.cutoff : Cutoff values for the robust and mahalanobis distances +% rew.flag : Flags based on the reweighted covariance matrix and the +% reweighted location of the data. These flags determine which +% observations can be considered as outliers. If the reweighted +% MCD is singular, raw.wt is given here. +% rew.method : A character string containing information about the method and +% about singular subsamples (if any). +% rew.plane : In case of an exact fit, rew.plane contains the coefficients +% of a (hyper)plane a_1(x_i1-m_1)+...+a_p(x_ip-m_p)=0 +% containing at least h observations, where (m_1,...,m_p) +% is the MCD location of these observations. +% rew.classic : If the input argument 'classic' is equal to one, this structure +% contains results of the classical analysis: center (sample mean), +% cov (sample covariance matrix), md (Mahalanobis distances), class ('COV'). +% rew.class : 'MCDCOV' +% rew.X : If x is bivariate, same as the x in the call to mcdcov, +% without rows containing missing or infinite values. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Katrien Van Driessen and Bjorn Rombouts +% Revisions by Sanne Engelen, Sabine Verboven +% Last Update: 09/04/2004, 01/08/2007 + +% The FASTMCD algorithm works as follows: +% +% The dataset contains n cases and p variables. +% When n < 2*nmini (see below), the algorithm analyzes the dataset as a whole. +% When n >= 2*nmini (see below), the algorithm uses several subdatasets. +% +% When the dataset is analyzed as a whole, a trial subsample of p+1 cases +% is taken, of which the mean and covariance matrix are calculated. +% The h cases with smallest relative distances are used to calculate +% the next mean and covariance matrix, and this cycle is repeated csteps1 +% times. For small n we consider all subsets of p+1 out of n, otherwise +% the algorithm draws 500 random subsets by default. +% Afterwards, the 10 best solutions (means and corresponding covariance +% matrices) are used as starting values for the final iterations. +% These iterations stop when two subsequent determinants become equal. +% (At most csteps3 iteration steps are taken.) The solution with smallest +% determinant is retained. +% +% When the dataset contains more than 2*nmini cases, the algorithm does part +% of the calculations on (at most) maxgroup nonoverlapping subdatasets, of +% (roughly) maxobs cases. +% +% Stage 1: For each trial subsample in each subdataset, csteps1 (see below) iterations are +% carried out in that subdataset. For each subdataset, the 10 best solutions are +% stored. +% +% Stage 2 considers the union of the subdatasets, called the merged set. +% (If n is large, the merged set is a proper subset of the entire dataset.) +% In this merged set, each of the 'best solutions' of stage 1 are used as starting +% values for csteps2 (sse below) iterations. Also here, the 10 best solutions are stored. +% +% Stage 3 depends on n, the total number of cases in the dataset. +% If n <= 5000, all 10 preliminary solutions are iterated. +% If n > 5000, only the best preliminary solution is iterated. +% The number of iterations decreases to 1 according to n*p (If n*p <= 100,000 we +% iterate csteps3 (sse below) times, whereas for n*p > 1,000,000 we take only one iteration step). +% + +if rem(nargin-1,2)~=0 + error('The number of input arguments should be odd!'); +end +% Assigning some input parameters +data = x; +raw.cor = []; +rew.cor = []; +rew.plane = []; +% The maximum value for n (= number of observations) is: +nmax=50000; +% The maximum value for p (= number of variables) is: +pmax=50; +% To change the number of subdatasets and their size, the values of +% maxgroup and nmini can be changed. +maxgroup=5; +nmini=300; +% The number of iteration steps in stages 1,2 and 3 can be changed +% by adapting the parameters csteps1, csteps2, and csteps3. +csteps1=2; +csteps2=2; +csteps3=100; +% dtrial : number of subsamples if not all (p+1)-subsets will be considered. +dtrial=500; + +if size(data,1)==1 + data=data'; +end + +% Observations with missing or infinite values are ommitted. +ok=all(isfinite(data),2); +data=data(ok,:); +xx=data; +[n,p]=size(data); +% Some checks are now performed. +if n==0 + error('All observations have missing or infinite values.') +end +if n > nmax + error(['The program allows for at most ' int2str(nmax) ' observations.']) +end +if p > pmax + error(['The program allows for at most ' int2str(pmax) ' variables.']) +end +if n < p + error('Need at least (number of variables) observations.') +end + +%internal variables +hmin=quanf(0.5,n,p); +%Assiging default values +h=quanf(0.75,n,p); +default=struct('alpha',0.75,'h',h,'plots',1,'ntrial',dtrial,'cor',0,'seed',0,'classic',0,'Hsets',[],'factor',0); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +counter=1; + +%Reading optional inputarguments +if nargin>2 + % + % placing inputfields in array of strings + % + for j=1:nargin-1 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + dummy=sum(strcmp(chklist,'h')+2*strcmp(chklist,'alpha')); + switch dummy + case 0 % defaultvalues should be taken + alfa=options.alpha; + h=options.h; + case 3 + error('Both input arguments alpha and h are provided. Only one is required.') + end + % + % Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) % in case of similarity + for j=1:nargin-2 % searching the index of the accompanying field + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end + if dummy==1% checking inputvariable h + % hmin is the minimum number of observations whose covariance determinant + % will be minimized. + if isempty(options.Hsets) + if options.h < hmin + disp(['Warning: The MCD must cover at least ' int2str(hmin) ' observations.']) + disp(['The value of h is set equal to ' int2str(hmin)]) + options.h = hmin; + elseif options.h > n + error('h is greater than the number of non-missings and non-infinites.') + elseif options.h < p + error(['h should be larger than the dimension ' int2str(p) '.']) + end + end + options.alpha=options.h/n; + elseif dummy==2 + if options.alpha < 0.5 + options.alpha=0.5; + mess=sprintf(['Attention (mcdcov.m): Alpha should be larger than 0.5. \n',... + 'It is set to 0.5.']); + disp(mess) + end + if options.alpha > 1 + options.alpha=0.75; + mess=sprintf(['Attention (mcdcov.m): Alpha should be smaller than 1.\n',... + 'It is set to 0.75.']); + disp(mess) + end + options.h=quanf(options.alpha,n,p); + end +end + +h=options.h; %number of regular datapoints on which estimates are based. h=[alpha*n] +plots=options.plots; %relevant plots are plotted +alfa=options.alpha; %percentage of regular observations +ntrial=options.ntrial; %number of subsets to be taken in the first step +cor=options.cor; %correlation matrix +seed=options.seed; %seed of the random generator +cutoff.rd=sqrt(chi2inv(0.975,p)); %cutoff value for the robust distance +cutoff.md=cutoff.rd; %cutoff value for the mahalanobis distance +Hsets = options.Hsets; +if ~isempty(Hsets) + Hsets_ind = 1; +else + Hsets_ind = 0; +end +factor = options.factor; +if factor == 0 + factor_ind = 0; +else + factor_ind = 1; +end + +% Some initializations. +rew.flag=repmat(NaN,1,length(ok)); +raw.wt=repmat(NaN,1,length(ok)); +raw.rd=repmat(NaN,1,length(ok)); +rew.rd=repmat(NaN,1,length(ok)); +rew.mahalanobis=repmat(NaN,1,length(ok)); +rew.method=sprintf('\nMinimum Covariance Determinant Estimator.'); +correl=NaN; + +% z : if at least h observations lie on a hyperplane, then z contains the +% coefficients of that plane. +% weights : weights of the observations that are not excluded from the computations. +% These are the observations that don't contain missing or infinite values. +% bestobj : best objective value found. +z(1:p)=0; +weights=zeros(1,n); +bestobj=inf; + +% The classical estimates are computed. +[Pcl,Tcl,Lcl,rcl,centerXcl,cXcl] = classSVD(data); +clasmean=cXcl; +clascov=Pcl*diag(Lcl)*Pcl'; + +if p < 5 + eps=1e-12; +elseif p <= 8 + eps=1e-14; +else + eps=1e-16; +end + +% The standardization of the data will now be performed. +med=median(data); +mad=sort(abs(data-repmat(med,n,1))); +mad=mad(h,:); +ii=min(find(mad < eps)); +if length(ii) + % The h-th order statistic is zero for the ii-th variable. The array plane contains + % all the observations which have the same value for the ii-th variable. + plane=find(abs(data(:,ii)-med(ii)) < eps)'; + meanplane=mean(data(plane,:)); + weights(plane)=1; + if p==1 + rew.flag=weights; + raw.wt=weights; + [raw.center,rew.center]=deal(meanplane); + [raw.cov,rew.cov,raw.objective]=deal(0); + if plots + rew.method=sprintf('\nUnivariate location and scale estimation.'); + rew.method=strvcat(rew.method,sprintf('%g of the %g observations are identical.',length(plane),n)); + end + else + z(ii)=1; + rew.plane=z; + covplane=cov(data(plane,:)); + [raw.center,raw.cov,rew.center,rew.cov,raw.objective,raw.wt,rew.flag,... + rew.method]=displ(3,length(plane),weights,n,p,meanplane,covplane,rew.method,z,ok,... + raw.wt,rew.flag,0,NaN,h,ii); + end + rew.Hsubsets.Hopt = plane; + rew.Hsubsets.Hfreq = plane; + %classical analysis? + if options.classic==1 + classic.cov=clascov; + classic.center=clasmean; + classic.class='COV'; + else + classic=0; + end + %assigning the output + rewo=rew;rawo=raw; + rew=struct('center',{rewo.center},'cov',{rewo.cov},'cor',{cor},'h',{h},'Hsubsets',{rewo.Hsubsets},'alpha',{alfa},... + 'flag',{rewo.flag},'plane', {rewo.plane},'method',{rewo.method},'class',{'MCDCOV'},'classic',{classic},'X',{xx}); + raw=struct('center',{rawo.center},'cov',{rawo.cov},'cor',{rawo.cor},'objective',{rawo.objective},... + 'wt',{rawo.wt},'class',{'MCDCOV'},'classic',{classic},'X',{x}); + if size(data,2)~=2 + rew=rmfield(rew,'X'); + raw=rmfield(raw,'X'); + end + return +end +data=(data-repmat(med,n,1))./repmat(mad,n,1); + +% The standardized classical estimates are now computed. +clmean=mean(data); +clcov=cov(data); + +% The univariate non-classical case is now handled. +if p==1 & h~=n + [rew.center, rewsca, weights,raw.center,raw.cov,rawdist,raw.wt,Hopt]=unimcd(data,h); + rew.Hsubsets.Hopt = Hopt'; + rew.Hsubsets.Hfreq = Hopt'; + raw.rd=sqrt(rawdist'); + rew.cov=rewsca^2; + raw.objective=raw.cov*prod(mad)^2; + mah=(data-rew.center).^2/rew.cov; + rew.rd=sqrt(mah'); + rew.flag= rew.rd <= cutoff.rd; + [raw.cov,raw.center]=trafo(raw.cov,raw.center,med,mad,p); + [rew.cov,rew.center]=trafo(rew.cov,rew.center,med,mad,p); + rew.mahalanobis=abs(data'-clmean)/sqrt(clcov); + spec.ask=1; + %classical analysis? + if options.classic==1 + classic.cov=clascov; + classic.center=clasmean; + classic.md=rew.mahalanobis; + classic.class='COV'; + else + classic=0; + end + %assigning the output + rewo=rew;rawo=raw; + rew=struct('center',{rewo.center},'cov',{rewo.cov},'cor',{rewo.cor},'h',{h},'Hsubsets',{rewo.Hsubsets},... + 'alpha',{alfa},'rd',{rewo.rd},'cutoff',{cutoff},'flag',{rewo.flag}, 'plane',{[]},'method',{rewo.method},... + 'class',{'MCDCOV'},'md',{rewo.mahalanobis},'classic',{classic},'X',{xx}); + raw=struct('center',{rawo.center},'cov',{rawo.cov},'cor',{rawo.cor},'objective',{rawo.objective},... + 'rd',{rawo.rd},'cutoff',{cutoff},'wt',{rawo.wt}, 'class',{'MCDCOV'},'classic',{classic},'X',{x}); + if size(data,2)~=2 + rew=rmfield(rew,'X'); + raw=rmfield(raw,'X'); + end + try + if plots & options.classic + makeplot(rew,'classic',1) + elseif plots + makeplot(rew) + end + catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu + return + end + return +end +%exact fit situation +if rcl < p + % all observations lie on a hyperplane. + z = Pcl(:,1); + rew.plane=z; + weights(1:n)=1; + if cor + correl=clcov./(sqrt(diag(clcov))*sqrt(diag(clcov))'); + end + [clcov,clmean]=trafo(clcov,clmean,med,mad,p); + [raw.center,raw.cov,rew.center,rew.cov,raw.objective,raw.wt,rew.flag,... + rew.method]=displ(1,n,weights,n,p,clmean,clcov,rew.method,z./mad',ok,... + raw.wt,rew.flag,cor,correl); + if cor + [rew.cor,raw.cor]=deal(correl); + end + %classical analysis? + if options.classic==1 + classic.cov=clascov; + classic.center=clasmean; + classic.class='COV'; + else + classic=0; + end + rew.Hsubsets.Hopt=1:n; + rew.Hsubsets.Hfreq=1:n; + %assigning the output + rewo=rew;rawo=raw; + rew=struct('center',{rewo.center},'cov',{rewo.cov},'cor',{rewo.cor},'h',{h},'Hsubsets',{rewo.Hsubsets},'alpha',{alfa},... + 'rd',{rewo.rd},'cutoff',{cutoff},'flag',{rewo.flag},'plane',{rewo.plane},'method',{rewo.method},... + 'class',{'MCDCOV'},'classic',{classic},'X',{xx}); + raw=struct('center',{rawo.center},'cov',{rawo.cov},'cor',{rawo.cor},'objective',{rawo.objective},... + 'cutoff',{cutoff},'wt',{rawo.wt}, 'class',{'MCDCOV'},'classic',{classic},'X',{x}); + if size(data,2)~=2 + rew=rmfield(rew,'X'); + raw=rmfield(raw,'X'); + end + return +end + +% The classical case is now handled. +if h==n + if plots + msg=sprintf('The MCD estimates based on %g observations are equal to the classical estimates.\n',h); + rew.method=strvcat(rew.method,msg); + end + raw.center=clmean; + raw.cov=clcov; + raw.objective=det(clcov); + mah=mahalanobis(data,clmean,'cov',clcov); + rew.mahalanobis=sqrt(mah); + raw.rd=rew.mahalanobis; + weights= mah <= cutoff.rd^2; + raw.wt=weights; + [rew.center,rew.cov]=weightmecov(data,weights); + if cor + raw.cor=raw.cov./(sqrt(diag(raw.cov))*sqrt(diag(raw.cov))'); + rew.cor=rew.cov./(sqrt(diag(rew.cov))*sqrt(diag(rew.cov))'); + else + raw.cor=0; + rew.cor=0; + end + if det(rew.cov) < exp(-50*p) + [center,covar,z,correl,plane,count]=fit(data,NaN,med,mad,p,z,cor,rew.center,rew.cov,n); + rew.plane=z; + if cor + correl=covar./(sqrt(diag(cov))*sqrt(diag(covar))'); + end + rew.method=displrw(count,n,p,center,covar,rew.method,z,cor,correl); + [raw.cov,raw.center]=trafo(raw.cov,raw.center,med,mad,p); + [rew.cov,rew.center]=trafo(rew.cov,rew.center,med,mad,p); + rew.rd=raw.rd; + else + mah=mahalanobis(data,rew.center,'cov',rew.cov); + weights = mah <= cutoff.md^2; + [raw.cov,raw.center]=trafo(raw.cov,raw.center,med,mad,p); + [rew.cov,rew.center]=trafo(rew.cov,rew.center,med,mad,p); + rew.rd=sqrt(mah); + end + raw.objective=raw.objective*prod(mad)^2; + rew.flag=weights; + %classical analysis? + if options.classic==1 + classic.cov=clascov; + classic.center=clasmean; + classic.md=rew.mahalanobis; + classic.class='COV'; + else + classic=0; + end + %assigning Hsubsets: + rew.Hsubsets.Hopt = 1:n; + rew.Hsubsets.Hfreq = 1:n; + %assigning the output + rewo=rew;rawo=raw; + rew=struct('center',{rewo.center},'cov',{rewo.cov},'cor',{rewo.cor},'h',{h},'Hsubsets',{rewo.Hsubsets},'alpha',{alfa},... + 'rd',{rewo.rd},'cutoff',{cutoff},'flag',{rewo.flag},'plane',{rewo.plane},... + 'method',{rewo.method},'class',{'MCDCOV'},'md',{rewo.mahalanobis},'classic',{classic},'X',{xx}); + raw=struct('center',{rawo.center},'cov',{rawo.cov},'cor',{rawo.cor},'objective',{rawo.objective},... + 'rd',{rawo.rd},'cutoff',{cutoff},'wt',{rawo.wt}, 'class',{'MCDCOV'},'classic',{classic},'X',{x}); + if size(data,2)~=2 + rew=rmfield(rew,'X'); + raw=rmfield(raw,'X'); + end + try + if plots & options.classic + makeplot(rew,'classic',1) + elseif plots + makeplot(rew) + end + catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu + return + end + return +end +percent=h/n; +teller = zeros(1,n+1); + +if Hsets_ind + csteps = csteps1; + inplane = NaN; + fine = 0; + part = 0; + final = 1; + tottimes = 0; + nsamp = size(Hsets,1); + obsingroup = n; +else + + % If n >= 2*nmini the dataset will be divided into subdatasets. For n < 2*nmini the set + % will be treated as a whole. + + if n >= 2*nmini + maxobs=maxgroup*nmini; + if n >= maxobs + ngroup=maxgroup; + group(1:maxgroup)=nmini; + else + ngroup=floor(n/nmini); + minquan=floor(n/ngroup); + group(1)=minquan; + for s=2:ngroup + group(s)=minquan+double(rem(n,ngroup)>=s-1); + end + end + part=1; + adjh=floor(group(1)*percent); + nsamp=floor(ntrial/ngroup); + minigr=sum(group); + obsingroup=fillgroup(n,group,ngroup,seed); + % obsingroup : i-th row contains the observations of the i-th group. + % The last row (ngroup+1-th) contains the observations for the 2nd stage + % of the algorithm. + else + [part,group,ngroup,adjh,minigr,obsingroup]=deal(0,n,1,h,n,n); + replow=[50,22,17,15,14,zeros(1,45)]; + if n < replow(p) + % All (p+1)-subsets will be considered. + al=1; + perm=[1:p,p]; + nsamp=nchoosek(n,p+1); + else + al=0; + nsamp=ntrial; + end + end + % some further initialisations. + + csteps=csteps1; + inplane=NaN; + % tottimes : the total number of iteration steps. + % fine : becomes 1 when the subdatasets are merged. + % final : becomes 1 for the final stage of the algorithm. + [tottimes,fine,final,prevdet]=deal(0); + + if part + % bmean1 : contains, for the first stage of the algorithm, the means of the ngroup*10 + % best estimates. + % bcov1 : analogous to bmean1, but now for the covariance matrices. + % bobj1 : analogous to bmean1, but now for the objective values. + % coeff1 : if in the k-th subdataset there are at least adjh observations that lie on + % a hyperplane then the coefficients of this plane will be stored in the + % k-th column of coeff1. + coeff1=repmat(NaN,p,ngroup); + bobj1=repmat(inf,ngroup,10); + bmean1=cell(ngroup,10); + bP1 = cell(ngroup,10); + bL1 = cell(ngroup,10); + [bmean1{:}]=deal(NaN); + [bL1{:}]=deal(NaN); + [bP1{:}]=deal(NaN); + end + + % bmean : contains the means of the ten best estimates obtained in the second stage of the + % algorithm. + % bcov : analogous to bmean, but now for the covariance matrices. + % bobj : analogous to bmean, but now for the objective values. + % coeff : analogous to coeff1, but now for the merged subdataset. + % If the data is not split up, the 10 best estimates obtained after csteps1 iterations + % will be stored in bmean, bcov and bobj. + coeff=repmat(NaN,p,1); + bobj=repmat(inf,1,10); + bmean=cell(1,10); + bP = cell(1,10); + bL = cell(1,10); + [bmean{:}]=deal(NaN); + [bP{:}]=deal(NaN); + [bL{:}] = deal(NaN); +end + +seed=0; +tellerwhilelus = 0; + +while final~=2 + if fine | (~part & final) + if ~Hsets_ind + nsamp=10; + end + if final + adjh=h; + ngroup=1; + if n*p <= 1e+5 + csteps=csteps3; + elseif n*p <=1e+6 + csteps=10-(ceil(n*p/1e+5)-2); + else + csteps=1; + end + if n > 5000 + + nsamp=1; + end + else + adjh=floor(minigr*percent); + csteps=csteps2; + end + end + + % found : becomes 1 if we have a singular intermediate MCD estimate. + found=0; + + for k=1:ngroup + if ~fine + found=0; + end + for i=1:nsamp + tottimes=tottimes+1; + % ns becomes 1 if we have a singular trial subsample and if there are at + % least adjh observations in the subdataset that lie on the concerning hyperplane. + % In that case we don't have to take C-steps. The determinant is zero which is + % already the lowest possible value. If ns=1, no C-steps will be taken and we + % start with the next sample. If we, for the considered subdataset, haven't + % already found a singular MCD estimate, then the results must be first stored in + % bmean, bcov, bobj or in bmean1, bcov1 and bobj1. If we, however, already found + % a singular result for that subdataset, then the results won't be stored + % (the hyperplane we just found is probably the same as the one we found earlier. + % We then let adj be zero. This will guarantee us that the results won't be + % stored) and we start immediately with the next sample. + adj=1; + ns=0; + + % For the second and final stage of the algorithm the array sortdist(1:adjh) + % contains the indices of the observations corresponding to the adjh observations + % with minimal relative distances with respect to the best estimates of the + % previous stage. An exception to this, is when the estimate of the previous + % stage is singular. For the second stage we then distinguish two cases : + % + % 1. There aren't adjh observations in the merged set that lie on the hyperplane. + % The observations on the hyperplane are then extended to adjh observations by + % adding the observations of the merged set with smallest orthogonal distances + % to that hyperplane. + % 2. There are adjh or more observations in the merged set that lie on the + % hyperplane. We distinguish two cases. We haven't or have already found such + % a hyperplane. In the first case we start with a new sample. But first, we + % store the results in bmean1, bcov1 and bobj1. In the second case we + % immediately start with a new sample. + % + % For the final stage we do the same as 1. above (if we had h or more observations + % on the hyperplane we would already have found it). + if ~Hsets_ind + + if final + if ~isinf(bobj(i)) + meanvct=bmean{i}; + P=bP{i}; + L = bL{i}; + if bobj(i)==0 + [dis,sortdist]=sort(abs(sum((data-repmat(meanvct,n,1))'.*repmat(coeff,1,n)))); + else + [dis,sortdist]=mahal2((data-repmat(meanvct,n,1))*P,sqrt(L),part,fine,final,k,obsingroup); + end + else + break + end + elseif fine + if ~isinf(bobj1(k,i)) + meanvct=bmean1{k,i}; + P=bP1{k,i}; + L=bL1{k,i}; + if bobj1(k,i)==0 + [dis,ind]=sort(abs(sum((data(obsingroup{end},:)-repmat(meanvct,minigr,1))'.*repmat(coeff1(:,k),1,minigr)))); + sortdist=obsingroup{end}(ind); + if dis(adjh) < 1e-8 + if found==0 + obj=0; + coeff=coeff1(:,k); + found=1; + else + adj=0; + end + ns=1; + end + else + [dis,sortdist]=mahal2((data(obsingroup{end},:)-repmat(meanvct,minigr,1))*P,sqrt(L),part,fine,final,k,obsingroup); + end + else + break; + end + else + if ~part + if al + k=p+1; + perm(k)=perm(k)+1; + while ~(k==1 |perm(k) <=(n-(p+1-k))) + k=k-1; + perm(k)=perm(k)+1; + for j=(k+1):p+1 + perm(j)=perm(j-1)+1; + end + end + index=perm; + else + [index,seed]=randomset(n,p+1,seed); + end + else + [index,seed]=randomset(group(k),p+1,seed); + index=obsingroup{k}(index); + end + [P,T,L,r,centerX,meanvct] = classSVD(data(index,:)); + if r==0 + ns = 1; + rew.center=meanvct; + rew.cov=zeros(p,p); + rew.flag = zeros(1,n); + rew.flag(index) = 1; + rew.Hsubsets.Hopt=1:n; + rew.Hsubsets.Hfreq=1:n; + elseif (r < p) & (r~=0) + % The trial subsample is singular. + % We distinguish two cases : + % + % 1. There are adjh or more observations in the subdataset that lie + % on the hyperplane. If the data is not split up, we have adjh=h and thus + % an exact fit. If the data is split up we distinguish two cases. + % We haven't or have already found such a hyperplane. In the first case + % we check if there are more than h observations in the entire set + % that lie on the hyperplane. If so, we have an exact fit situation. + % If not, we start with a new trial subsample. But first, the + % results must be stored bmean1, bcov1 and bobj1. In the second case + % we immediately start with a new trial subsample. + % + % 2. There aren't adjh observations in the subdataset that lie on the + % hyperplane. We then extend the trial subsample until it isn't singular + % anymore. + + + % The smallest eigenvector belonging to a non-zero eigenvalue contains + %the coefficients of the hyperplane. + eigvct=P(:,r); + + if ~part + dist=abs(sum((data-repmat(meanvct,n,1))'.*repmat(eigvct,1,n))); + else + dist=abs(sum((data(obsingroup{k},:)-repmat(meanvct,group(k),1))'.*repmat(eigvct,1,group(k)))); + end + + obsinplane=find(dist < 1e-8); + % count : number of observations that lie on the hyperplane. + count=length(obsinplane); + + if count >= adjh + if ~part + [center,covar,eigvct,correl]=fit(data,obsinplane,med,mad,p,eigvct,cor); + rew.plane=eigvct; + weights(obsinplane)=1; + [raw.center,raw.cov,rew.center,rew.cov,raw.objective,... + raw.wt,rew.flag,rew.method]=displ(2,count,weights,n,p,center,covar,... + rew.method,eigvct,ok,raw.wt,rew.flag,cor,correl); + if cor + [rew.cor,raw.cor]=deal(correl); + end + rew.Hsubsets.Hopt=obsinplane; + rew.Hsubsets.Hfreq=obsinplane; + return + elseif found==0 + if count>=h + [center,covar,eigvct,correl]=fit(data,obsinplane,med,mad,p,eigvct,cor); + rew.plane=eigvct; + weights(obsinplane)=1; + [raw.center,raw.cov,rew.center,rew.cov,raw.objective,... + raw.wt,rew.flag,rew.method,varargout]=displ(2,count2,weights,n,p,center,covar,... + rew.method,eigvct,ok,raw.wt,rew.flag,cor,correl); + if cor + [rew.cor,raw.cor]=deal(correl); + end + rew.Hsubsets.Hopt=obsinplane; + rew.Hsubsets.Hfreq=obsinplane; + return + end + obj=0; + inplane(k)=count; + coeff1(:,k)=eigvct; + found=1; + ns=1; + else + ns=1; + adj=0; + end + else + covmat = cov(data(index,:)); + meanvct = mean(data(index,:)); + while det(covmat) < exp(-50*p) + [index1,seed]=addobs(index,n,seed); + [covmat,meanvct] = updatecov(data(index,:),covmat,meanvct,data(setdiff(index1,index),:),[],1); + index = index1; + end + end + end + + if ~ns + if ~part + [dis,sortdist] = mahal2((data - repmat(meanvct,n,1))*P,sqrt(L),part,fine,final,k,obsingroup); + else + [dis,sortdist] = mahal2((data(obsingroup{k},:) - repmat(meanvct,group(k),1))*P,sqrt(L),part,fine,final,k,obsingroup); + end + end + end + end + + if ~ns + for j=1:csteps + tottimes=tottimes+1; + if j == 1 + if Hsets_ind + obs_in_set = Hsets(i,:); + else + obs_in_set = sort(sortdist(1:adjh)); + teller(obs_in_set) = teller(obs_in_set) + 1; + teller(end) = teller(end) + 1; + end + else + % The observations correponding to the adjh smallest mahalanobis + % distances determine the subset for the next iteration. + if ~part + [dis2,sortdist] = mahal2((data - repmat(meanvct,n,1))*P,sqrt(L),part,fine,final,k,obsingroup); + else + if final + [dis2,sortdist] = mahal2((data - repmat(meanvct,n,1))*P,sqrt(L),part,fine,final,k,obsingroup); + elseif fine + [dis2,sortdist] = mahal2((data(obsingroup{end},:) - repmat(meanvct,minigr,1))*P,sqrt(L),part,fine,final,k,obsingroup); + else + [dis2,sortdist] = mahal2((data(obsingroup{k},:) - repmat(meanvct,group(k),1))*P,sqrt(L),part,fine,final,k,obsingroup); + end + end + % Creation of a H-subset. + obs_in_set=sort(sortdist(1:adjh)); + teller(obs_in_set) = teller(obs_in_set) + 1; + teller(end) = teller(end) + 1; + end + [P,T,L,r,centerX,meanvct] = classSVD(data(obs_in_set,:)); + if r==0 + rew.center=meanvct; + rew.cov=zeros(p,p); + rew.flag=(data==data(obs_in_set(1),:)); + rew.Hsubset.Hopt=obs_in_set(1); + rew.Hsubset.Hfreq=obs_in_set(1); + else + + obj=prod(L); + + if obj < exp(-50*p) + % The adjh-subset is singular. If adjh=h we have an exact fit situation. + % If adjh < h we distinguish two cases : + % + % 1. We haven't found earlier a singular adjh-subset. We first check if + % in the entire set there are h observations that lie on the hyperplane. + % If so, we have an exact fit situation. If not, we stop taking C-steps + % (the determinant is zero which is the lowest possible value) and + % store the results in the appropriate arrays. We then begin with + % the next trial subsample. + % + % 2. We have, for the concerning subdataset, already found a singular + % adjh-subset. We then immediately begin with the next trial subsample. + + if ~part | final | (fine & n==minigr) + covmat=cov(data(obs_in_set,:)); + [center,covar,z,correl,obsinplane,count]=fit(data,NaN,med,mad,p,NaN,... + cor,meanvct,covmat,n); + rew.plane=z; + weights(obsinplane)=1; + [raw.center,raw.cov,rew.center,rew.cov,raw.objective,... + raw.wt,rew.flag,rew.method]=displ(2,count,weights,n,p,center,covar,... + rew.method,z,ok,raw.wt,rew.flag,cor,correl); + if cor + [rew.cor,raw.cor]=deal(correl); + end + rew.Hsubset.Hopt=obsinplane; + rew.Hsubset.Hfreq=obsinplane; + return + elseif found==0 + eigvct = V(:,1); + dist=abs(sum((data-repmat(meanvct,n,1))'.*repmat(eigvct,1,n))); + obsinplane=find(dist<1e-8); + count=length(obsinplane); + if count >= h + [center,covar,eigvct,correl]=fit(data,obsinplane,med,mad,p,eigvct,cor); + rew.plane=eigvct; + weights(obsinplane)=1; + [raw.center,raw.cov,rew.center,rew.cov,raw.objective,... + raw.wt,rew.flag,rew.method]=displ(2,count,weights,n,p,center,covar,... + rew.method,eigvct,ok,raw.wt,rew.flag,cor,correl); + if cor + [rew.cor,raw.cor]=deal(correl); + end + rew.Hsubset.Hopt=obsinplane; + rew.Hsubset.Hfreq=obsinplane; + return + end + obj=0; + found=1; + if ~fine + coeff1(:,k)=eigvct; + dist=abs(sum((data(obsingroup{k},:)-repmat(meanvct,group(k),1))'.*repmat(eigvct,1,group(k)))); + inplane(k)=length(dist(dist<1e-8)); + else + coeff=eigvct; + dist=abs(sum((data(obsingroup{end},:)-repmat(meanvct,minigr,1))'.*repmat(eigvct,1,minigr))); + inplane=length(dist(dist<1e-8)); + end + break; + else + adj=0; + break; + end + end + end + % We stop taking C-steps when two subsequent determinants become equal. + % We have then reached convergence. + if j >= 2 & obj == prevdet + break; + end + prevdet=obj; + + end % C-steps + + end + + % After each iteration, it has to be checked whether the new solution + % is better than some previous one. A distinction is made between the + % different stages of the algorithm: + % + % - Let us first consider the first (second) stage of the algorithm. + % We distinguish two cases if the objective value is lower than the largest + % value in bobj1 (bobj) : + % + % 1. The new objective value did not yet occur in bobj1 (bobj). We then store + % this value, the corresponding mean and covariance matrix at the right + % place in resp. bobj1 (bobj), bmean1 (bmean) and bcov1 (bcov). + % The objective value is inserted by shifting the greater determinants + % upwards. We perform the same shifting in bmean1 (bmean) and bcov1 (bcov). + % + % 2. The new objective value already occurs in bobj1 (bobj). A comparison is + % made between the new mean vector and covariance matrix and those + % estimates with the same determinant. When for an equal determinant, + % the mean vector or covariance matrix do not correspond, the new results + % will be stored in bobj1 (bobj), bmean1 (bmean) and bcov1 (bcov). + % + % If the objective value is not lower than the largest value in bobj1 (bobj), + % nothing happens. + % + % - For the final stage of the algorithm, only the best solution has to be kept. + % We then check if the objective value is lower than the till then lowest value. + % If so, we have a new best solution. If not, nothing happens. + + + if ~final & adj + if fine | ~part + if obj < max(bobj) & ~ns + [bmean,bP,bL,bobj]=insertion(bmean,bP,bL,bobj,meanvct,P,L,obj,1,eps); + end + else + if obj < max(bobj1(k,:)) & ~ns + [bmean1,bP1,bL1,bobj1]=insertion(bmean1,bP1,bL1,bobj1,meanvct,P,L,obj,k,eps); + end + end + end + + if final & obj< bestobj + % bestset : the best subset for the whole data. + % bestobj : objective value for this set. + % initmean, initcov : resp. the mean and covariance matrix of this set. + bestset=obs_in_set; + bestobj=obj; + initmean=meanvct; + initcov=cov(data(bestset,:)); + raw.initcov=cov(data(bestset,:)); + end + + end % nsamp + end % ngroup + + + if part & ~fine + fine=1; + elseif (part & fine & ~final) | (~part & ~final) + final=1; + else + final=2; + end + +end % while loop + +[P,T,L,r,centerX,cX] = classSVD(data(bestset,:)); +mah=mahalanobis((data - repmat(cX,n,1))*P,zeros(size(P,2),1),'cov',L); +sortmah=sort(mah); + +[sortset,indbestset] = sort(mah(bestset)); +sortbestset = bestset(indbestset); +rew.Hsubsets.Hopt = sortbestset; + +if ~factor_ind + factor = sortmah(h)/chi2inv(h/n,p); +else + factor = sortmah(h)/chi2inv(h/n,p/2); +end + +raw.cov=factor*initcov; +% We express the results in the original units. +[raw.cov,raw.center]=trafo(raw.cov,initmean,med,mad,p); +raw.objective=bestobj*prod(mad)^2; + +if cor + raw.cor=raw.cov./(sqrt(diag(raw.cov))*sqrt(diag(raw.cov))'); +end + +% the mahalanobis distances are computed without the factor, therefore we +% have to correct for it now. +mah=mah/factor; +raw.rd=sqrt(mah); +weights=mah<=cutoff.md^2; +raw.wt=weights; +[rew.center,rew.cov]=weightmecov(data,weights); +[trcov,trcenter]=trafo(rew.cov,rew.center,med,mad,p); + +% determination of Hfreq: +[telobs,indobs] = greatsort(teller(1:(end - 1))); +rew.Hsubsets.Hfreq = indobs(1:(h)); +if size(rew.Hsubsets.Hfreq,2) == 1 + rew.Hsubsets.Hfreq = rew.Hsubsets.Hfreq'; +end + +if cor + rew.cor=rew.cov./(sqrt(diag(rew.cov))*sqrt(diag(rew.cov))'); +end + +if prod(sqrt(L)) < exp(-50*p) + [center,covar,z,correl,plane,count]=fit(data,NaN,med,mad,p,z,cor,rew.center,rew.cov,n); + rew.plane=z; + if cor + correl=covar./(sqrt(diag(covar))*sqrt(diag(covar))'); + [rew.cor,raw.cor] = deal(correl); + end + rew.method=displrw(count,n,p,center,covar,rew.method,z,cor,correl); + rew.flag=weights; + rew.rd=raw.rd; +else + mah=mahalanobis(data,rew.center,'cov',rew.cov); + rew.flag=(mah <= cutoff.md^2); + rew.rd=sqrt(mah); +end + +rew.mahalanobis=sqrt(mahalanobis(data,clmean,'cov',clcov)); +rawo=raw; +reso=rew; +if options.classic==1 + classic.cov=clascov; + classic.center=clasmean; + classic.md=rew.mahalanobis; + classic.flag = (classic.md <= cutoff.md); + if options.cor==1 + classic.cor=clascov./(sqrt(diag(clascov))*sqrt(diag(clascov))'); + end + classic.class='COV'; +else + classic=0; +end +%assigning the output +rew=struct('center',{trcenter},'cov',{trcov},'cor',{reso.cor},'h',{h},'Hsubsets',{reso.Hsubsets},'alpha',{alfa},... + 'rd',{reso.rd},'flag',{reso.flag},'md',{reso.mahalanobis},'cutoff',{cutoff},... + 'plane',{reso.plane},'method',{reso.method},'class',{'MCDCOV'},'classic',{classic},'X',{xx}); +raw=struct('center',{rawo.center},'cov',{rawo.cov},'cor',{rawo.cor},'objective',{rawo.objective},... + 'rd',{rawo.rd},'cutoff',{cutoff},... + 'wt',{rawo.wt},'class',{'MCDCOV'},'classic',{classic},'X',{xx}); + +if size(data,2)~=2 + rew=rmfield(rew,'X'); + raw=rmfield(raw,'X'); +end + +try + if plots & options.classic + makeplot(rew,'classic',1) + elseif plots + makeplot(rew) + end +catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu +end + +%----------------------------------------------------------------------------------------- +function [raw_center,raw_cov,center,covar,raw_objective,raw_wt,mcd_wt,method]=displ(exactfit,... + count,weights,n,p,center,covar,method,z,ok,raw_wt,mcd_wt,cor,correl,varargin) + +% Determines some fields of the output argument REW for the exact fit situation. It also +% displays and writes the messages concerning the exact fit situation. If the raw MCD +% covariance matrix is not singular but the reweighted is, then the function displrw is +% called instead of this function. + +[raw_center,center]=deal(center); +[raw_cov,cover]=deal(covar); +raw_objective=0; +mcd_wt=weights; +raw_wt=weights; + +switch exactfit + case 1 + msg='The covariance matrix of the data is singular.'; + case 2 + msg='The covariance matrix has become singular during the iterations of the MCD algorithm.'; + case 3 + msg=sprintf('The %g-th order statistic of the absolute deviation of variable %g is zero. ',varargin{1},varargin{2}); +end + +msg=sprintf([msg '\nThere are %g observations in the entire dataset of %g observations that lie on the \n'],count,n); +switch p + case 2 + msg=sprintf([msg 'line with equation %g(x_i1-m_1)%+g(x_i2-m_2)=0 \n'],z); + msg=sprintf([msg 'where the mean (m_1,m_2) of these observations is the MCD location']); + case 3 + msg=sprintf([msg 'plane with equation %g(x_i1-m_1)%+g(x_i2-m_2)%+g(x_i3-m_3)=0 \n'],z); + msg=sprintf([msg 'where the mean (m_1,m_2,m_3) of these observations is the MCD location']); + otherwise + msg=sprintf([msg 'hyperplane with equation a_1 (x_i1-m_1) + ... + a_p (x_ip-m_p) = 0 \n']); + msg=sprintf([msg 'with coefficients a_i equal to : \n\n']); + msg=sprintf([msg sprintf('%g ',z)]); + msg=sprintf([msg '\n\nand where the mean (m_1,...,m_p) of these observations is the MCD location']); +end + +method=strvcat(method,[msg '.']); +disp(method) + +%----------------------------------------------------------------------------------------- +function method=displrw(count,n,p,center,covar,method,z,cor,correl) + +% Displays and writes messages in the case the reweighted robust covariance matrix +% is singular. + +msg=sprintf('The reweighted MCD scatter matrix is singular. \n'); +msg=sprintf([msg 'There are %g observations in the entire dataset of %g observations that lie on the\n'],count,n); + +switch p + case 2 + msg=sprintf([msg 'line with equation %g(x_i1-m_1)%+g(x_i2-m_2)=0 \n\n'],z); + msg=sprintf([msg 'where the mean (m_1,m_2) of these observations is : \n\n']); + case 3 + msg=sprintf([msg 'plane with equation %g(x_i1-m_1)%+g(x_i2-m_2)%+g(x_i3-m_3)=0 \n\n'],z); + msg=sprintf([msg 'where the mean (m_1,m_2,m_3) of these observations is : \n\n']); + otherwise + msg=sprintf([msg 'hyperplane with equation a_1 (x_i1-m_1) + ... + a_p (x_ip-m_p) = 0 \n']); + msg=sprintf([msg 'with coefficients a_i equal to : \n\n']); + msg=sprintf([msg sprintf('%g ',z)]); + msg=sprintf([msg '\n\nand where the mean (m_1,...,m_p) of these observations is : \n\n']); +end + +msg=sprintf([msg sprintf('%g ',center)]); +msg=sprintf([msg '\n\nTheir covariance matrix equals : \n\n']); +msg=sprintf([msg sprintf([repmat('% 13.5g ',1,p) '\n'],covar)]); +if cor + msg=sprintf([msg '\n\nand their correlation matrix equals : \n\n']); + msg=sprintf([msg sprintf([repmat('% 13.5g ',1,p) '\n'],correl)]); +end + +method=strvcat(method,msg); + +%----------------------------------------------------------------------------------------- +function [initmean,initcov,z,correl,varargout]=fit(dat,plane,med,mad,p,z,cor,varargin) + +% This function is called in the case of an exact fit. It computes the correlation +% matrix and transforms the coefficients of the hyperplane, the mean, the covariance +% and the correlation matrix to the original units. + +if isnan(plane) + [meanvct,covmat,n]=deal(varargin{:}); + [z, eigvl]=eigs(covmat,1,0,struct('disp',0)); + dist=abs(sum((dat-repmat(meanvct,n,1))'.*repmat(z,1,n))); + plane=find(dist < 1e-8); + varargout{1}=plane; + varargout{2}=length(plane); +end + +z=z./mad'; +[initcov,initmean]=trafo(cov(dat(plane,:)),mean(dat(plane,:)),med,mad,p); +if cor + correl=initcov./(sqrt(diag(initcov))*sqrt(diag(initcov))'); +else + correl=NaN; +end + +%------------------------------------------------------------------------------------------ +function obsingroup=fillgroup(n,group,ngroup,seed) + +% Creates the subdatasets. + +obsingroup=cell(1,ngroup+1); + +jndex=0; +for k=1:ngroup + for m=1:group(k) + [random,seed]=uniran(seed); + ran=floor(random*(n-jndex)+1); + jndex=jndex+1; + if jndex==1 + index(1,jndex)=ran; + index(2,jndex)=k; + else + index(1,jndex)=ran+jndex-1; + index(2,jndex)=k; + ii=min(find(index(1,1:jndex-1) > ran-1+[1:jndex-1])); + if length(ii) + index(1,jndex:-1:ii+1)=index(1,jndex-1:-1:ii); + index(2,jndex:-1:ii+1)=index(2,jndex-1:-1:ii); + index(1,ii)=ran+ii-1; + index(2,ii)=k; + end + end + end + obsingroup{k}=index(1,index(2,:)==k); + obsingroup{ngroup+1}=[obsingroup{ngroup+1},obsingroup{k}]; +end + +%----------------------------------------------------------------------------------------- +function [ranset,seed]=randomset(tot,nel,seed) + +% This function is called if not all (p+1)-subsets out of n will be considered. +% It randomly draws a subsample of nel cases out of tot. + +for j=1:nel + [random,seed]=uniran(seed); + num=floor(random*tot)+1; + if j > 1 + while any(ranset==num) + [random,seed]=uniran(seed); + num=floor(random*tot)+1; + end + end + ranset(j)=num; +end + +%----------------------------------------------------------------------------------------- +function [index,seed]=addobs(index,n,seed) + +% Extends a trial subsample with one observation. + +jndex=length(index); +[random,seed]=uniran(seed); +ran=floor(random*(n-jndex)+1); +jndex=jndex+1; +index(jndex)=ran+jndex-1; +ii=min(find(index(1:jndex-1) > ran-1+[1:jndex-1])); +if length(ii)~=0 + index(jndex:-1:ii+1)=index(jndex-1:-1:ii); + index(ii)=ran+ii-1; +end + +%----------------------------------------------------------------------------------------- + +function mahsort=mahal(dat,meanvct,covmat,part,fine,final,k,obsingroup,group,minigr,n,nvar) + +% Orders the observations according to the mahalanobis distances. + +if ~part | final + [dis,ind]=sort(mahalanobis(dat,meanvct,'cov',covmat)); + mahsort=ind; +elseif fine + [dis,ind]=sort(mahalanobis(dat(obsingroup{end},:),meanvct,'cov',covmat)); + mahsort=obsingroup{end}(ind); +else + [dis,ind]=sort(mahalanobis(dat(obsingroup{k},:),meanvct,'cov',covmat)); + mahsort=obsingroup{k}(ind); +end + +%----------------------------------------------------------------------------------------- + +function [dis,mahsort]=mahal2(score,sca,part,fine,final,k,obsingroup) + +% Orders the observations according to the mahalanobis distances for a diagonal +% covariance matrix and zero mean. sca contains the squareroot of the diagonal elements. + +if ~part | final + [dis,ind]=sort(mahalanobis(score,zeros(size(score,2),1),'cov',sca.^2)); + mahsort=ind; +elseif fine + [dis,ind]=sort(mahalanobis(score,zeros(size(score,2),1),'cov',sca.^2)); + mahsort=obsingroup{end}(ind); +else + [dis,ind]=sort(mahalanobis(score,zeros(size(score,2),1),'cov',sca.^2)); + mahsort=obsingroup{k}(ind); +end + +%------------------------------------------------------------------------------------------ + +function [covmat,meanvct]=trafo(covmat,meanvct,med,mad,nvar) + +% Transforms a mean vector and a covariance matrix to the original units. + +covmat=covmat.*repmat(mad,nvar,1).*repmat(mad',1,nvar); +meanvct=meanvct.*mad+med; + +%----------------------------------------------------------------------------------------- +function [bestmean,bestP,bestL,bobj]=insertion(bestmean,bestP,bestL,bobj,meanvct,P,L,obj,row,eps) + +% Stores, for the first and second stage of the algorithm, the results in the appropriate +% arrays if it belongs to the 10 best results. + +insert=1; + +equ=find(obj==bobj(row,:)); + +for j=equ + if (meanvct==bestmean{row,j}) + if all(P==bestP{row,j}) + if all(L==bestL{row,j}) + insert=0; + end + end + end +end + +if insert + ins=min(find(obj < bobj(row,:))); + + if ins==10 + bestmean{row,ins}=meanvct; + bestP{row,ins} = P; + bestL{row,ins} = L; + bobj(row,ins)=obj; + else + [bestmean{row,ins+1:10}]=deal(bestmean{row,ins:9}); + bestmean{row,ins}=meanvct; + [bestP{row,ins+1:10}] = deal(bestP{row,ins:9}); + bestP{row,ins} = P; + [bestL{row,ins+1:10}] = deal(bestL{row,ins:9}); + bestL{row,ins} = L; + bobj(row,ins+1:10)=bobj(row,ins:9); + bobj(row,ins)=obj; + end + +end + +%----------------------------------------------------------------------------------------- +function quan=quanf(alfa,n,rk) +quan=floor(2*floor((n+rk+1)/2)-n+2*(n-floor((n+rk+1)/2))*alfa); +%-------------------------------------------------------------------------- diff --git a/LIBRA/mcdregres.m b/LIBRA/mcdregres.m new file mode 100644 index 0000000..adbd6d5 --- /dev/null +++ b/LIBRA/mcdregres.m @@ -0,0 +1,303 @@ +function result=mcdregres(x,y,varargin) + +%MCDREGRES is a robust multivariate regression method. It can handle multiple +% response variables. The estimates are based on the robust MCD estimator of +% location and scatter (see mcdcov.m). The explanatory variables should be +% low-dimensional, otherwise robust principal component regression (rpcr.m) +% or robust partial least squares (rsimpls.m) should be applied. +% +% The MCD regression method is described in +% Rousseeuw, P.J., Van Aelst, S., Van Driessen, K, Agullo, J. (2004), +% "Robust multivariate regression", Technometrics, 46, pp 293-305. +% +% Required input arguments: +% x : Data matrix of the explanatory variables +% (n observations in rows, p variables in columns) +% y : Data matrix of the response variables +% (n observations in rows, q variables in columns) +% +% Optional input arguments: +% alpha : (1-alpha) measures the amount of contamination the algorithm should +% resist. Any value between 0.5 and 1 may be specified. (default = 0.75) +% h : The quantile of observations whose covariance determinant will +% be minimized. Any value between n/2 and n may be specified. +% The default value is 0.75*n. +% ntrial : The number of random trial subsamples that are drawn for +% large datasets. (default = 500) +% plots : If equal to one, a menu is shown which allows to draw a regression +% outlier map. (default) +% If the input argument 'classic' is equal to one, the classical +% plot is drawn as well. +% If 'plots' is equal to zero, all plots are suppressed. +% See also makeplot.m +% classic : If equal to one, classical multivariate linear regression +% is performed as well, see mlr.m. (default = 0) +% +% Input arguments for advanced users: +% Hsets : Instead of random trial h-subsets (default, Hsets = []), Hsets makes it possible to give certain +% h-subsets as input. Hsets is a matrix that contains the indices of the observations of one +% h-subset as a row. +% +% I/O: result=mcdregres(x,y,'alpha',0.75,'ntrial',500,'plots',1,'classic',0); +% The user should only give the input arguments that have to change their default value. +% The name of the input arguments needs to be followed by their value. +% The order of the input arguments is of no importance. +% +% Example: result=mcdregres(x,y,'plots',0,'alpha',0.70) +% +% The output is a structure which contains +% result.slope : Robust slope (matrix) +% result.int : Robust intercept (vector) +% result.fitted : Robust prediction matrix +% result.res : Robust residuals +% result.cov : Estimated variance-covariance matrix of the residuals +% result.rsquared : Robust R-squared value +% result.h : The quantile h used throughout the algorithm +% result.Hsubsets : A structure that contains Hopt and Hfreq: +% Hopt : The subset of h points whose covariance matrix has minimal determinant, +% ordered following increasing robust distances. +% Hfreq : The subset of h points which are the most frequently selected during the whole +% algorithm. +% result.rd : Robust scores distances in x-space +% result.resd : Residual distances (when there are several response variables). +% If univariate regression is performed, it contains the standardized residuals. +% result.cutoff : Cutoff values for the score and residual distances +% result.weights : The observations with weight one are used in the reweighting, +% the other observations have zero weight. +% result.flag : The observations whose residual distance is larger than result.cutoff.resd +% (bad leverage points/vertical outliers) can be considered as outliers and receive +% a flag equal to zero. +% The regular observations, including the good leverage points, +% receive a flag 1. +% result.class : 'MCDREG' +% result.classic : If the input argument 'classic' is equal to one, this structure +% contains results of classical multivariate regression (see also mlr.m). +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Original S-PLUS code by Katrien Vandriessen, implemented in MATLAB by Sabine Verboven +% Version date : 12/02/2004 +% Last update: 04/08/2006 + +%%%%%%%%%%%%%%%% +% INITIALIZATION +% +intercept=ones(length(x),1); +geg=[x,y]; +[n,m]=size(geg); +alfa=0.75; +hdefault=min(floor(2*floor((n+m+1)/2)-n+2*(n-floor((n+m+1)/2))*alfa),n); + +if nargin < 3 + options.alpha=alfa; + options.h=hdefault; + options.ntrial=500; + options.plots=1; + options.classic=0; + options.Hsets=[]; +else + default=struct('alpha',alfa,'h',hdefault,'ntrial',500,'plots',1,'classic',0,'Hsets',[]); + list=fieldnames(default); + options=default; + IN=length(list); + i=1; + counter=1; + % + if nargin>2 + % + %placing inputfields in array of strings + % + for j=1:nargin-2 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + dummy=sum(strcmp(chklist,'h')+2*strcmp(chklist,'alpha')); + switch dummy + case 0 %no input for alpha or h so take on the default values + options.alpha=0.75; + options.h=floor(options.alpha*n); + case 3 + error('Both input arguments alpha and h are provided. Only one is required.') + end + % + %Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) %in case of similarity + for j=1:nargin-2 %searching the index of the accompanying field + if rem(j,2)~=0 %fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end + if dummy==1 + options.alpha=options.h/n; + elseif dummy==2 + options.h=floor(options.alpha*n); + end + Hsets = options.Hsets; + end +end +%%%%%%%% +% MAIN % +[mcd_res, mcd_raw]=mcdcov(geg,'h',options.h,'plots',0,'Hsets',options.Hsets); +options.h = mcd_res.h; + +%in case of an exact fit, the calculations stop at this point. +if ~isempty(mcd_res.plane) + disp('Warning (mcdregres): The MCD covariance matrix is singular. See also mcdcov.m.') + result=mcd_res; + return +end + +mcdreg.Hsubsets.Hopt = mcd_res.Hsubsets.Hopt; +mcdreg.Hsubsets.Hfreq = mcd_res.Hsubsets.Hfreq; + +rewcovmcd=mcd_res.cov; %one-step reweighted covariance matrix +rewcenmcd=mcd_res.center; %one-step reweighted location +q=size(y,2); +p=size(x,2); + +%initializing reweighted location and reweighted scatter (paragraph 5.1 of paper) +rewtmcdx=rewcenmcd(1:p)'; %column vectors!!! +rewtmcdy=rewcenmcd((p+1):m)'; + +rewsmcdx=rewcovmcd(1:p,1:p); +rewsmcdxy=rewcovmcd(1:p,(p+1):m); +rewsmcdyx=rewsmcdxy'; +rewsmcdy=rewcovmcd((p+1):m,(p+1):m); + +covyy=rewsmcdy; +covxx = rewsmcdx; +covxy = rewsmcdxy; +covyx = rewsmcdyx; +mcdreg.Sigma = [covxx, covxy ; covyx, covyy]; +mcdreg.Mu = [rewtmcdx;rewtmcdy]; + +%reweighted beta and alpha (the columnvector [\beta^L ; \alpha^L]) +rewbetamcd=[inv(rewsmcdx)*rewsmcdxy; (rewtmcdy-(rewsmcdyx*inv(rewsmcdx)*rewtmcdx))']; + +%calculation of the reweighted weights based on +%the residuals calculated with the reweighted beta-coefficients +rewweights=zeros(n,1); +weights=zeros(n,1); +rewfitted=[x,intercept]*rewbetamcd(1:(p+1),:); +rewresid=y-rewfitted; %(r_i^L) +rewE=rewsmcdy-rewbetamcd(1:p,1:q)'*rewsmcdx*rewbetamcd(1:p,1:q); %reweighted scatter (\Sigma_eps^L) +for j=1:n + if (sqrt(rewresid(j,1:q)*inv(rewE)*rewresid(j,1:q)')) <= sqrt(chi2inv(0.99,q)) + rewweights(j)=1; + end +end + +%regression reweighting part based on the reweighted observations. (paragraph 5.3 of paper) +rewclasscov=cov(geg(rewweights==1,:)); +rewclasscenter=mean(geg(rewweights==1,:)); + +rewtmcdx=rewclasscenter(1:p)'; +rewtmcdy=rewclasscenter((p+1):m)'; + +rewsmcdx=rewclasscov(1:p,1:p); +rewsmcdxy=rewclasscov(1:p,(p+1):m); +rewsmcdyx=rewsmcdxy'; +rewsmcdy=rewclasscov((p+1):m,(p+1):m); + +%regression reweighted coefficients beta and alpha ([\beta^{RL} \alpha^{RL}]) +rewbetamcdrew=[inv(rewsmcdx)*rewsmcdxy; (rewtmcdy-(rewsmcdyx*inv(rewsmcdx)*rewtmcdx))']; +%regression reweighted scatter (\Sigma_eps^{RL}]) +rewE2=rewsmcdy-rewbetamcdrew(1:p,1:q)'*rewsmcdx*rewbetamcdrew(1:p,1:q); +rewfittedrew=[x,intercept]*rewbetamcdrew(1:(p+1),:); +rewresidrew=y-rewfittedrew; + +%%%%%%%%%%%%%%%%% +%OUTPUT STRUCTURE +% +mcdreg.covyy=rewsmcdy; %regression and location reweighted covariance matrix of responses +mcdreg.covxx = rewsmcdx; +mcdreg.covxy = rewsmcdxy; +mcdreg.covyx = rewsmcdyx; +mcdreg.Sigmarew = [mcdreg.covxx, mcdreg.covxy ; mcdreg.covyx, mcdreg.covyy]; +mcdreg.Murew = [rewtmcdx;rewtmcdy]; + +mcdreg.x=x; +mcdreg.y=y; +mcdreg.coeffs=rewbetamcdrew; %regression and location reweighted coefficients +mcdreg.cov=rewE2; %scatter matrix based on location and regression reweighting +mcdreg.fitted=rewfittedrew; %estimated respons(es); +mcdreg.res=rewresidrew; %regression and location reweighted residuals + +if(intercept) + mcdreg.interc=1; +else + mcdreg.interc=0; +end + +% Robust distances in x-space = x-distances (rd) needed in diagnostic regression plot +if (-log(det(mcd_res.cov))/m) > 50 + mcdreg.rd='singularity'; +else + mcdreg.rd=sqrt(mahalanobis(mcdreg.x,rewcenmcd(1:p),'cov',rewcovmcd(1:p,1:p)))'; +end + +% Robust residual distances (resd) needed in diagnostic regression plot +if q>1 + if (-log(det(mcd_res.cov))/m)>50 + mcdreg.resd='singularity'; + disp('Warning (mcdregres): A singularity ') + else + cen=zeros(q,1)'; + [nn,pp]=size(rewresidrew); + mcdreg.resd=sqrt(mahalanobis(rewresidrew,cen,'cov',mcdreg.cov))'; %robust distances of residuals + end +else + mcdreg.covarRes=sqrt(mcdreg.cov); + mcdreg.resd=rewresidrew(:,1)/mcdreg.covarRes; %standardized residuals +end + +% cutoff values +mcdreg.cutoff.rd=sqrt(chi2inv(0.975,p)); +mcdreg.cutoff.resd=sqrt(chi2inv(0.975,q)); +mcdreg.flag=(abs(mcdreg.resd)<=mcdreg.cutoff.resd); + +% robust multivariate Rsquared +mcdreg.weights=rewweights; +Yw=y(mcdreg.weights==1,:); +cYw=mcenter(Yw); +res=rewresidrew(mcdreg.weights==1,:); +mcdreg.rsquared=1-(det(res'*res)/det(cYw'*cYw)); +mcdreg.class='MCDREG'; + + +if options.classic + mcdreg.classic=mlr(x,y,'plots',0); +else + mcdreg.classic=0; +end + +result=struct('slope',{mcdreg.coeffs(1:p,:)}, 'int',{mcdreg.coeffs(p+1,:)}, ... + 'fitted',{mcdreg.fitted},'res',{mcdreg.res},'cov',{mcdreg.cov},'rsquared',{mcdreg.rsquared},... + 'h',{options.h},'Hsubsets',{mcdreg.Hsubsets},'rd', {mcdreg.rd},'resd',{mcdreg.resd},'cutoff',{mcdreg.cutoff},... + 'weights',{mcdreg.weights},'flag',{mcdreg.flag'},'class',{mcdreg.class},'classic',{mcdreg.classic},... + 'Mu',{mcdreg.Mu},'Sigma',{mcdreg.Sigma},'Murew',{mcdreg.Murew},'Sigmarew',{mcdreg.Sigmarew}); + +try + if options.plots & options.classic + makeplot(result,'classic',1) + elseif options.plots + makeplot(result) + end +catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu +end \ No newline at end of file diff --git a/LIBRA/mcenter.m b/LIBRA/mcenter.m new file mode 100644 index 0000000..a3238a3 --- /dev/null +++ b/LIBRA/mcenter.m @@ -0,0 +1,16 @@ +function [mcX,mX]=mcenter(x) + +%MCENTER mean-centers the data matrix x columnwise +% +% Required input arguments: +% x : Data matrix (n observations in rows, p variables in columns) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Sabine Verboven + +[n,p]=size(x); +mX=mean(x); +mcX=x-repmat(mX,n,1); diff --git a/LIBRA/medc.mexw32 b/LIBRA/medc.mexw32 new file mode 100644 index 0000000000000000000000000000000000000000..4523c804e903ce383bd490899dec5d8d01ab0fb4 GIT binary patch literal 10240 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 T(@LmLZVHD4X72Ov$pZfiaghrR literal 0 HcmV?d00001 diff --git a/LIBRA/mlochuber.m b/LIBRA/mlochuber.m new file mode 100644 index 0000000..5cc7ff5 --- /dev/null +++ b/LIBRA/mlochuber.m @@ -0,0 +1,128 @@ +function result=mlochuber(x,varargin) + +%MLOCHUBER is an M-estimator of location with psi-function equal to +% min(1.5,max(-1.5,x)) and with an auxiliary scale estimate. +% It is iteratively computed. It can resist 50% outliers. +% If x is a matrix, the location estimate is computed on the columns of x. The +% result is then a row vector. If x is a row or a column vector, +% the output is a scalar. +% +% The estimator is described in: +% Huber, P. (1981), Robust Statistics, Wiley, New York. +% Its behavior in small samples is discussed in: +% Rousseeuw, P.J. and Verboven, S. (2002), +% "Robust estimation in very small samples", +% Computational Statistics and Data Analysis, 40, 741-758. +% +% Required input argument: +% x: either a data matrix with n observations in rows, p variables in columns +% or a vector of length n. +% +% Optional input arguments: +% k: number of iteration steps (default value = 50) +% loc: a starting value for the location estimate +% default value = 'median' +% other possibilities : 'hl'/'mloclogist'/... +% sca: an auxiliary scale estimate +% default value = 'madc' +% other possibilities: 'qn'/'adm'/'mscalelogist'/... +% +% I/O: result=mlochuber(x,'k',50,'loc','median','sca','mad') +% +% Examples: result=mlochuber(x,'loc','mlochuber','sca','qn'); +% result=mlochuber(x,'sca','mscalelogist','k',10); +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by S. Verboven +% Last update 12/02/04 + +[n,p]=size(x); +% +% initialization with defaults +% +counter=1; +sca='madc'; +loc='median'; +default=struct('k',50,'sca',sca,'loc',loc); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +% +% +if nargin>1 + % + % placing inputfields in array of strings + % + for j=1:nargin-1 + if rem(j,2)~=0 + chklist{i}= varargin{j}; + i=i+1; + end + end + % + % Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) % in case of similarity + for j=1:nargin-1 % searching the index of the accompanying field + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end + options.k=options.k; + options.sca=options.sca; + options.loc=options.loc; +end + +if n==1 & p==1 + out=x; %when X is a one by one matrix, all location estimators must be equal to that matrix + return +elseif n==1 + x=x'; %we only want to work with column vectors + n=p; + p=1; +end + +if n==2 % all location estimators must equal the average for n=2 + out=mean(x,1); + return +end + +alfa=0.866385597462284; %2*normcdf(1.5,0,1)-1; constant denumenator +for i=1:p + X=x(:,i); + t_0=feval(options.loc,X); + tstep=t_0; + s_0=feval(options.sca,X); + if (s_0==0) + out(i)=t_0; + else + j=1; + while j<=options.k + z=(X-tstep)/s_0; + y(abs(z)<=1.5)=z(abs(z)<=1.5); + y(abs(z)>1.5)=1.5*sign(z(abs(z)>1.5)); + tstep=tstep+s_0*(sum(y)/(n*alfa)); %updating location estimate + y=[]; %clear helpvector + j=j+1; + end + out(i)=tstep; + end +end + +result = out; + + diff --git a/LIBRA/mloclogist.m b/LIBRA/mloclogist.m new file mode 100644 index 0000000..9310750 --- /dev/null +++ b/LIBRA/mloclogist.m @@ -0,0 +1,124 @@ +function result=mloclogist(x,varargin) + +%MLOCLOGIST is an M-estimator of location with psi-function equal to +% (exp(x)-1)/(exp(x)+1) and with an auxiliary scale estimate. +% It is iteratively computed. It can resist 50% outliers. +% If x is a matrix, the location estimate is computed on the columns of x. The +% result is then a row vector. If x is a row or a column vector, +% the output is a scalar. +% +% The estimator is introduced in: +% Rousseeuw, P.J. and Verboven, S. (2002), +% "Robust estimation in very small samples", +% Computational Statistics and Data Analysis, 40, 741-758. +% +% Required input argument: +% x: either a data matrix with n observations in rows, p variables in columns +% or a vector of length n. +% +% Optional input arguments: +% k: number of iteration steps (default value = 50) +% loc: a starting value for the location estimate +% default value = 'median' +% other possibilities : 'hl'/'mlochuber'/... +% sca: an auxiliary scale estimate +% default value = 'madc' +% other possibilities: 'qn'/'adm'/'mscalelogist'/... +% +% I/O: result=mloclogist(x,'k',50,'loc','median','sca','madc') +% +% Examples: result=mloclogist(x,'loc','mlochuber','sca','qn'); +% result=mloclogist(x,'sca','mscalelogist','k',10); +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by S. Verboven +%Revisions by N. Smets +% Last update 28/08/03 + +[n,p]=size(x); +% +% initialization with defaults +% +counter=1; +sca='madc'; +loc='median'; +default=struct('k',50,'sca',sca,'loc',loc); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +% +% +if nargin>1 + % + % placing inputfields in array of strings + % + for j=1:nargin-1 + if rem(j,2)~=0 + chklist{i}= varargin{j}; + i=i+1; + end + end + % + % Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) % in case of similarity + for j=1:nargin-1 % searching the index of the accompanying field + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end + options.k=options.k; + options.sca=options.sca; + options.loc=options.loc; +end + +if n==1 & p==1 + out=x; %when X is a one by one matrix, all location estimators must be equal to that matrix + return +elseif n==1 + x=x'; %we only want to work with column vectors + n=p; + p=1; +end + +if n==2 % all location estimators must equal the average for n=2 + out=mean(x,1); + return +end + +alpha=0.413241928283814; %quadl('1/2.*sech(x/2).^2.*normpdf(x,0,1)',-10,10,1.e-15) +for i=1:p + X=x(:,i); + t_0=feval(options.loc,X); + s_0=feval(options.sca,X); + tstep=t_0; + if (s_0==0) + out(i)=t_0; + else + j=1; + while j<=options.k + z=(X-tstep)/s_0; + y=tanh(z/2); + tstep=tstep+s_0*(sum(y)/(n*alpha)); % updating location estimate + y=[]; %clear help vector + j=j+1; + end + out(i)=tstep; + end +end + +result = out; \ No newline at end of file diff --git a/LIBRA/mlr.m b/LIBRA/mlr.m new file mode 100644 index 0000000..d7f3919 --- /dev/null +++ b/LIBRA/mlr.m @@ -0,0 +1,149 @@ +function result = mlr(x,y,varargin) + +%MLR is the classical least squares estimator for multivariate multiple +% linear regression. It can handle both one or several ('multiple') +% predictor variables and one or several ('multivariate') response variables. +% If the regression model contains an intercept, the x-matrix may not contain +% a column with ones. +% If there is only one response variable, the function ols.m is called. +% +% See for example: +% R.A. Johnson and D.W. Wichern, +% "Applied multivariate statistical analysis (Fifth Edition)", +% Prentice Hall, chapter 7. +% +% Required input arguments: +% x : Data matrix of the explanatory variables +% (n observations in rows, p variables in columns) +% y : Data matrix of the response variables +% (n observations in rows, q variables in columns) +% +% Optional input arguments: +% intercept : logical flag: if 1, a model with constant term will be +% fitted; if 0, no constant term will be included. (default: 1) +% plots : If equal to one, a menu is shown which allows to draw several plots, +% such as residual plots and a regression outlier map. (default) +% If 'plots' is equal to zero, all plots are suppressed. +% See also makeplot.m +% +% I/O: result=mlr(x,y,'plots',0); +% The user should only give the input arguments that have to change their default value. +% +% The output is a structure containing: +% +% result.slope : Slope estimate +% result.int : Intercept estimate +% result.fitted : Fitted values +% result.res : Residuals +% result.stdres : Standardized residuals +% result.cov : Estimated variance-covariance matrix of the residuals +% result.rsquared : R-squared value +% result.md : Score distances (Mahalanobis distances in x-space) +% result.resd : Residual distances (when there are several response variables). +% If univariate regression is performed, it contains the standardized residuals. +% result.cutoff : Cutoff values for the score and residual distances +% result.flag : The observations whose residual distance is larger than result.cutoff.resd +% receive a flag equal to zero. The other observations receive a flag 1. +% result.class : 'MLR' (when q > 1) or 'LS' (when q = 1) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Nele Smets on 13/12/2003 +% Last update: 05/04/2004 +% + +q=size(y,2); +p=size(x,2); +geg=[x,y]; +[n,m]=size(geg); +intercept=ones(n,1); +default=struct('plots',1,'intercept',1); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +counter=1; +if nargin>2 + % + % placing inputfields in array of strings + % + for j=1:nargin-2 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % Checking which default parameters have to be changed + % and keep them in the structure 'options'. + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) % in case of similarity + for j=1:nargin-2 % searching the index of the accompanying field + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end +end +%%%%%%%%%Main part%%%%%%% +if q==1 + result=ols(x,y,'intercept', options.intercept,'plots',options.plots); +else + cmean=mean(geg); + ccovar=cov(geg); + meanx=cmean(1:p)'; + meany=cmean((p+1):m)'; + cy=mcenter(y); + covarx=ccovar(1:p,1:p); + covary=ccovar((p+1):m,(p+1):m); + covarxy=ccovar(1:p,(p+1):m); + covaryx=covarxy'; + res.betas=[inv(covarx)*covarxy; (meany-(covaryx*inv(covarx)*meanx))']; + res.fitted=[x intercept]*res.betas; + res.residuals=y-res.fitted; + res.cov=covary-res.betas(1:p,1:q)'*covarx*res.betas(1:p,1:q); + res.stdresid= res.residuals./repmat(diag(res.cov)',n,1); + res.rsquared= 1-(det(res.residuals'*res.residuals)/det(cy'*cy)); + res.class='MLR'; + + % x-distances (md) needed in diagnostic regression plot + if (-log(det(ccovar))/m) > 50 + res.md='singularity'; + res.resd='singularity'; + else + res.md=sqrt(mahalanobis(x,cmean(1:p),'cov',covarx))'; + cen=zeros(q,1)'; + res.resd=sqrt(mahalanobis(res.residuals,cen,'cov',res.cov))'; + end + + %cutoff values + res.cutoff.md=sqrt(chi2inv(0.975,p)); + res.cutoff.resd=sqrt(chi2inv(0.975,q)); + res.flag=(abs(res.resd)<=res.cutoff.resd); + result=struct('slope',{res.betas(1:p,:)},'int',{res.betas(p+1,:)}, ... + 'fitted',{res.fitted},'res',{res.residuals},'stdres',{res.stdresid},... + 'cov',{res.cov},'rsquared',{res.rsquared},'md',{res.md},'resd',{res.resd},... + 'cutoff',{res.cutoff},'flag',{res.flag},'class',{res.class}); + if ~intercept + result=setfield(result, 'int', 0) + end + try + if options.plots==1 + makeplot(result) + end + catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu + end +end + + + + diff --git a/LIBRA/mona.m b/LIBRA/mona.m new file mode 100644 index 0000000..83b4726 --- /dev/null +++ b/LIBRA/mona.m @@ -0,0 +1,181 @@ +function result=mona(x,plots) + +%MONA returns clusters according to mona (monothetic +% clustering) +% +% The algorithm is fully described in: +% Kaufman, L. and Rousseeuw, P.J. (1990), +% "Finding groups in data: An introduction to cluster analysis", +% Wiley-Interscience: New York (Series in Applied Probability and +% Statistics), ISBN 0-471-87876-6. +% +% Required input argument: +% x : Data matrix (rows = observations, columns = variables) +% containing only binary values. +% Missing values are indicated by the value 2. +% +% Optional input argument: +% plots : if equal to 1 a banner is drawn (default = 0) +% +% I/O: +% result=mona(x,1) +% +% Example (subtracted from the referenced book) +% load animal.mat +% result=mona(animal,1); +% +% The output of MONA is a structure containing +% result.matrix : revised inputmatrix (contains only 0 and 1 and missing +% values are estimated) +% result.number : number of observations +% result.var : number of variables +% result.ner : order of objects +% result.lava : variable used for separation +% result.separationstep : separation step +% (The value on the ith index is the stepnumber from +% the seperation of the elements with index 1 to i +% from the elements with index i+1 to result.number) +% If it equals zero, then there was no +% separation. +% +% And MONA will create a banner if plots equals 1. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Wai Yan Kong (May 2006) +% Last Revision: 27 March 2009 S.Verboven + +% Check whether the number of input arguments is correct +if (nargin<1) + error('One input argument required (datamatrix)') +elseif (nargin<2) + plots=0; +elseif (nargin>2) + error('Too many input arguments') +end + +% Define number of observations and variables +[n,p]=size(x); + + +% Check whether x is a matrix containing only binary values +% and whether x has missing values +% We make a revised matrix xx +missing=zeros(1,p); +for j = 1:p + for i= 1:n + if (x(i,j)==0 | x(i,j)==1) + missing(j)=missing(j); + xx(i,j)=x(i,j); + elseif (x(i,j)==2) + missing(j)=missing(j)+1; + for k=1:p + a=0; + b=0; + c=0; + d=0; + + if k~=j + if x(i,k) ~= 2 + + for t=1:n + if (x(t,j)==1 & x(t,k)==1) + a=a+1; + elseif (x(t,j)==1 & x(t,k)==0) + b=b+1; + elseif (x(t,j)==0 & x(t,k)==1) + c=c+1; + elseif (x(t,j)==0 & x(t,k)==0) + d=d+1; + end + end + end + end + association(k)=abs(a*d-b*c); + resinbetween(k)=a*d-b*c; + [C,I]=max(association); + + if resinbetween(I)>0 + xx(i,j)=x(i,I); + elseif resinbetween(I)<0 + xx(i,j)=1-x(i,I); + end + end + else + error('inputmatrix must have binary values or value 2 for missing values') + end + end +end + +TotalMissing=sum(missing); +fprintf(1,'This inputmatrix has %d missing values\n',TotalMissing) + +% Check situations where Mona is not applicable +One=0; +for j=1:p + if missing(j)>=1 + One=One+1; + end +end +if One==p + error('each variable has at least one missing value') +end + + +for j=1:p + + if (missing(j)>= (n/2)) + fprintf(1,'Variable %d has %d missing values\n',j,missing(j)) + error('The number of missing values for some variable equals or is more than half of the number of objects') + end + gelijk=0; + for s=1:n-1 + if xx(s,j)==xx(s+1,j) + gelijk=gelijk+1; + end + end + if gelijk==n-1 + fprintf(1,'Variable %d has identical values\n',j) + error('all values are identical for some variable') + end + +end + +% We make the rowvector kx by reading the revised matrix xx +% row by row +Lengte=n*p; +kx=zeros(1,Lengte); + +for i = 1:n + for j= 1:p + kx((i-1)*p+j) = xx(i,j); + end +end + + +% Actual calculations +[ner,lava,ban]=monac(n,p,kx); + +% We want lava and ban to be vectors of length n-1 +lava(1)=[]; +ban(1)=[]; + + +% Putting things together +result = struct('matrix',xx,'observations',n,'variables',p,... + 'objectorder',ner,'usedvariable',lava,'separationstep',ban, 'class','MONA'); + +% Plots +try + if plots + makeplot(result,'classic',0) + end +catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu +end + + + + diff --git a/LIBRA/monac.mexw32 b/LIBRA/monac.mexw32 new file mode 100644 index 0000000000000000000000000000000000000000..c809da554316571f7c9c9944d8b7f8ccc3e3e568 GIT binary patch literal 9728 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 + % + % placing inputfields in array of strings + % + for j=1:nargin-1 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % + % Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) % in case of similarity + for j=1:nargin-1 % searching the index of the accompanying field + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end + options.k=options.k; + options.sca=options.sca; + options.loc=options.loc; +end + +if n==1 & p==1 + out=0; %when X is a one by one matrix, all scale estimators must equal to 0 + return +elseif n==1 + x=x'; %we only want to work with column vectors + n=p; + p=1; +end + +b=0.3739; +beta=0.5; %0.500038854875226 %quadl('(tanh(x./(2*b)).^2).*normpdf(x,0,1)',-5,5,1.e-15) ; denumenator of the scale estimator = constant +for i=1:p + X=x(:,i); + j=1; + s_0=feval(options.sca,X); + t_0=feval(options.loc,X); + step=s_0; + if s_0==0 + out(i)=0; + else + while j<=options.k + u=(X-t_0)/step; + uu=tanh(u/(2*b)).^2; + step=step*sqrt(sum(uu)/(n*beta)); + j=j+1; + end + out(i)=step; + u=[]; + end +end + +result = out; + + diff --git a/LIBRA/normqqplot.m b/LIBRA/normqqplot.m new file mode 100644 index 0000000..aa41301 --- /dev/null +++ b/LIBRA/normqqplot.m @@ -0,0 +1,32 @@ +function normqqplot(y,class) + +%NORMQQPLOT produces a Quantile-Quantile plot in which the vector y is plotted against +% the quantiles of a standard normal distribution. +% +% Required input arguments: +% y : row or column vector +% +% Optional input arguments: +% class : a string used for the y-label and the title(default: ' ') +% +% This function is part of LIBRA: the Matlab library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +%Written by Nele Smets +% Last update: 12/03/2004 + +set(gcf,'Name', 'Normal QQ-plot', 'NumberTitle', 'off'); +if nargin==1 + class=' '; +end +n=length(y); +for i=1:n + normalquantile(i)=norminv((i-1/3)/(n+1/3),0,1); +end +y=sort(y); +plot(normalquantile,y,'bo') +xlabel('Quantiles of the standard normal distribution'); +ylabel(['Standardized ',class,' residual']); +title(class); +box on diff --git a/LIBRA/obj200.mat b/LIBRA/obj200.mat new file mode 100644 index 0000000000000000000000000000000000000000..d52100f9c987e54e8a6d8fcd3ca2c4145926c79d GIT binary patch literal 3392 zcmb7H2U``_7QG+{7}NyChQi!B_hN%Pg9XJ|*kbQ333-SHMMX#yG*P1xtcek8@L3R} z1~qo9iF&S}Ad(ntK~xaMh&07QQCiM9!}|emzHh#{%*;8n_g-tSwHX;WVMJhm!LOT- zFfwpL*BP_IL%RrL!b75FM9!P*FN_&5dFZGv!hm_9AyJ`U2$2!~!en|K79Am2dkelk z{=TxmWEHGFK7PXgeQ4~4j%=aPXwJE6G)?K%nV#&*4o7;jzpv5Lo}PB}WX~d}&88pI zXk6&;<#gToADfo98S;?)^V;iHWN@`Ibk}lI%Bnh|fADVU>)vgclZ(zN2n#461*pG;#UfYkGtue7p~~${P4a8SU9-_Q*G- z%7v)S?HndKV(Gf`b6nnKDb4{qChq?A6`6UraCYa)66HYJ@SbZg+oLJlu+-Jo{Hw!1 zJb2#3wq^CoPj6qyRu=8=GN5Bg9vSj@QR|~0SCXi~TPm9dmy^x@b|Y6bFHkZTT-x2C zdmZulU2OTmX?uLsqwmFWKQ*zfy_J{$!R9xF?-y1|uAJx=vDdDi6!hp|`%EZO&Q4pF z6Y!0lt>cmq>%`-ZP~)i}uWyZ<_TiKavMs_b)^^?17T>U8X^np+xt`kp$gI2?W#`ZO z^R=s=D(hL<#*`}F{+lwF4XGd#FCW-93b9JDgG@uHC$Fae@%iv;W#p0%D`0~%qUYMc zsiDp`7M*teev-fQqx}~H9iTpcPp;OVT>Sd@6H*n;p4egUCYE3!DQnK&*rt4Tf|Mro zts$e)DkSyxBjx_#_d#>7K2tV8k)xJC-c5v|Ma!jeWK!epA-%0)}XCRBU5^xy0)O7J>2$Ryxi90V7qQ* zh}9|-bc%ejLP_WnrfVQ7e@e(9md1x~lxEp`zYJ*RWLuRnfg(_@EcnW!d45+X+nmmo z@wu(qpwb)^mr_wo9*zDnKEf@T@W&gTD8WuQ+K;`zgZws>6)c}@NUZjKcC8=lo5wXExMlE)dabxS1id&)?gpaB71MWa+hWq zUhNDcZ-_rg-RidwvSTVpw%hB#zh>N3)crNKNxOeNaxXZa&~6FSG)TynU-lZ5EukL+ zY>6K@4=N>0IpFtapLz+rZ@m0alldYe)lJ6Po0>fPSrS-v%zb5Wybsa$PP^A+-mrrA z8{Ka(OTV3r@UJOi$uio{r;%u7aD0whu_)c*ziGG(9?lsFoOjD*EM~wp^8@FlMh36@ zR0$lnED8RMwu*#bnhiEa&y&G%3y_dZyI{;`5?GgSTW7_0Ptv#j-b2D{CL~WKaDL{> zU}|Kopm{U+tE^z%x@VBA#*--_enZPeJ$A?_nNJfMF5$pr8gYmfuig&(WYu%Jp3&M* zM%r9?`PnQP-KjcU*ec^Nt?GhE87FtWOiJ^Ralw%?{1*vf|Rz|^2NOj7G}Q^q5zJ_l21985ntNT5&K&5xJhzHV5t!xkBBb8b|Pzb%8Y z;N?OIjEx>6WN<#tlu=ki8}*`$>Xa`IO_hzg`T9hfVuPZp5uN`>CTgcl{Y-uh-*@OF4<0++`MPf#=* z-QEh&)m2942U%2{63y7nWGPLjzCYRk?jIpWe8Y(TJH5|{xXgk+%pO__urBPX^`iSQ zxMs!aaE>vmmjTrNG@3*~UBAZw%Es;^W*AkRYjmi38nWl_w|b0YhNv@u`y(;oN80_D zj+-!p9VR0ta>FyCfywt6t;(~HOo(Q-_rii3R5d1b@xawM--TU6^e7!mJ9v%&ZnNoT zQ1Ppb))-YjW$1BY%$)5DJ9Yvq)A%R>PngVX)Pu{YXD76OGNtX=n{AL!;gwRf;JV~4 zfMpqe#thNd`EbYIOh};g9lp?n09vkR-&&BeDrxfdO}a*W+|>AI=)m0EH{J-ksU|PQ z0PYtjL@-(=4K{$fqmOq+BYu6g2t71)!nh<+&CAsQ7NudbN&Ws$ZPk7;=@H2qGKPMi z33RSmty{SkDQ!*T9aBwez6W(G-)SAF>GnQl)*~zVlZx{l-0=sMss3dmxDPkeV=gnu zlY01!po;O27JgLjK6VotuR46e>^5BwuIt-%;PXq6R^_k3r0Sl|qV`+57M@AZhja_p zBXRBV$3>_05a{&d6k&quCuVS;?xx38ff2d`m`ny86V?7(+=xd%BYvT-d)#WmEM|-= z^`In}vwgBo&3l(lt;e&*bsl<^Z-D|FnZY*~8vW4%KHk`A!NO{IY;4?51o!t;EtpGI zx_803V@(RzIqTGV4WaxLX$E^d(C5n=Yi=cYg25vuc2@QOC$p;iiV-bFQ)jzofFm1E z0<_>h^RT)g;C?ps=hrj zsXQn(f{jr=v%T@IWzpGV1){1iu_87f{BG>fToHrrl~d(4gTZ|5Z!@^xdy1f}o%HP} zQLXn;le#`tgiOP#wim(Vt$M5oHf5}!I$&Uex>&>~jIaq7Rd))E&@qErC#w2!*Ni^Q z@aevMt~+A^9rmUi*lJR7rhQptqK!iL<38EL8xOv(3$5BGf{&N?L{+Enn8BvYTza3+ zzn9Hm?bJyWRo%QTg2{Te)dD`>JQh_Sm?Eluo@oLf50%bvVg~V0M8bb8nIAfN;}oU& z{b7x|o@WG~m&aSc=T=(ZN#7nlGtJ9_7&?t7RhiZ2$==}O!z?pc7)fhroJ^muh?vU^ zW1KgF>GtWrd1KE^3ZS1^y;5I%rcg)7lhBimzrK>HhuvjNrS{Oq-w$l;4l9>1@O@c{ z?VzOk;4ukw8u@zBL>a|YhQ12#q0S58jeb~T1+z0b#;VrgmR0S?O%nLO4=W^9r|EaN SPue8a&jw0h_TT-Xgnt2Vwzg;h literal 0 HcmV?d00001 diff --git a/LIBRA/ols.m b/LIBRA/ols.m new file mode 100644 index 0000000..0932428 --- /dev/null +++ b/LIBRA/ols.m @@ -0,0 +1,143 @@ +function result = ols(x,y,varargin) + +%OLS is the classical least squares estimator for multiple +% linear regression. It can handle both one or several predictor variables, +% and one response variable. +% If there are several response variables, the function mlr.m should be used. +% +% Required input arguments: +% x : Data matrix of the explanatory variables +% (n observations in rows, p variables in columns) +% Missing values (NaN's) and infinite values (Inf's) are allowed, since observations (rows) +% with missing or infinite values will automatically be excluded from the computations. +% y : Data vector with the response variable +% Missing values (NaN's) and infinite values (Inf's) are allowed, since observations (rows) +% with missing or infinite values will automatically be excluded from the computations. +% +% Optional input arguments: +% intercept : logical flag: if 1, a model with constant term will be +% fitted; if 0, no constant term will be included. (default: 1) +% plots : if 0, the plots are supressed (default:0) +% +% I/O: result=ols(x,y,'plots',0,'intercept',0) +% The user should only give the input arguments that have to change their default value. +% The name of the input arguments needs to be followed by their value. +% The order of the input arguments is of no importance. +% +% The output is a structure containing: +% +% result.slope : Slope estimate +% result.int : Intercept estimate (if no intercept is included, it equals zero) +% result.fitted : Fitted values +% result.res : Residuals +% result.scale : Scale estimate of the residuals +% result.rsquared : R-squared value +% result.md : Mahalanobis distances in x-space +% result.resd : Residual distances (which are equal to the standardized residuals) +% result.cutoff : Cutoff values for the score distances, and for the standardized residuals +% result.flag : The observations whose absolute standardized residual is larger than result.cutoff.resd +% receive a flag equal to zero. The other observations receive a flag 1. +% result.X : If x is univariate, data matrix without missing or infinite values. +% result.y : If x is univariate, response vector without missing or infinite values. +% result.class : 'LS' +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Nele Smets on 06/12/2003 +% Last update on 05/04/2004 + +if nargin<2 + error('there is a missing input argument') +end +default=struct('intercept',1,'plots',1); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +counter=1; +if nargin > 3 + % + % placing inputfields in array of strings + % + for j=1:nargin-2 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % + % Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) % in case of similarity + for j=1:nargin-3 % searching the index of the accompanying field + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end +end +intercept=options.intercept; +plots=options.plots; + + +[n,p]=size(x); +na.x=~isfinite(x*ones(p,1)); +na.y=~isfinite(y); +if size(na.x,1)~=size(na.y,1) + error('Number of observations in x and y are not equal.'); +end +ok=~(na.x|na.y); +x=x(ok,:); +y=y(ok,:); +n=length(y); +X=x; +if intercept + x=cat(2,x,ones(n,1)); + p=p+1; +end + +[coeff,bint,res] = regress(y,x); +fitted=x*coeff; +scale=sqrt(1/(n-p)*sum(res.^2)); +stdres=res/scale; +md=sqrt(mahalanobis(X,mean(X),'cov',cov(X)))'; +SSE=sum((y-fitted).^2); +if intercept + SST=sum((y-mean(y)).^2); + cutoff.md=sqrt(chi2inv(0.975,p-1)); +else + SST=sum(y.^2); + cutoff.md=sqrt(chi2inv(0.975,p)); +end +cutoff.resd=sqrt(chi2inv(0.975,1)); +rsquared=1-SSE/SST; +flags=(abs(stdres)<=cutoff.resd); + +result=struct('slope',{coeff(1:p)},'int',{0},'fitted',{fitted},'res',{res},'scale',{scale},'rsquared',{rsquared},... + 'md',md,'resd', {stdres},'cutoff',cutoff,'flag',{flags},'class',{'LS'},'X',{X},'y',{y}); + +if intercept + result=setfield(result,'slope',coeff(1:p-1)); + result=setfield(result,'int', coeff(p)); +end +if size(X,2)~=1 + result=rmfield(result,{'X','y'}); +end +try + if plots + makeplot(result) + end +catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu +end diff --git a/LIBRA/pam.m b/LIBRA/pam.m new file mode 100644 index 0000000..a74dcc8 --- /dev/null +++ b/LIBRA/pam.m @@ -0,0 +1,236 @@ +function result = pam(x,kclus,vtype,stdize,metric,plots) + +%PAM is the Partitioning Around Medoids clustering algorithm. +% It returns a list representing a clustering of the data into kclus +% clusters based on the search for kclus representative objects or medoids among the observations of +% the data set. +% +% The algorithm is fully described in: +% Kaufman, L. and Rousseeuw, P.J. (1990), +% "Finding groups in data: An introduction to cluster analysis", +% Wiley-Interscience: New York (Series in Applied Probability and +% Statistics), ISBN 0-471-87876-6. +% +% Required input arguments: +% x : Data matrix (rows = observations, columns = variables) +% or Dissimilarity vector (if number of columns equals 1). The +% dissimilarity vector should be obtained by reading row by row from the +% lower dissimilarity matrix. This can be the result from the +% function 'daisy'. +% kclus : The number of desired clusters +% vtype : Variable type vector (length equals number of variables) +% Possible values are 1 Asymmetric binary variable (0/1) +% 2 Nominal variable (includes symmetric binary) +% 3 Ordinal variable +% 4 Interval variable +% (if x is a dissimilarity matrix vtype is not required.) +% +% Optional input arguments: +% stdize : standardise the variables given by the x-matrix +% Possible values are 0 : no standardisation (default) +% 1 : standardisation by the mean +% 2 : standardisation by the median +% (if x is a dissimilarity matrix, stdize is ignored) +% metric : Metric to be used +% Possible values are 0 : Mixed (not all interval variables, default) +% 1 : Euclidean (all interval variables, default) +% 2 : Manhattan +% (if x is a dissimilarity matrix, metric is ignored) +% plots : draws figures +% Possible values are 0 : do not create any plot (default) +% 1 : create a silhouette plot and a clusplot +% +% I/O: +% result=pam(x,kclus,vtype,1,1,plots) +% +% Example: +% load ruspini.mat +% result=pam(ruspini,2,[4 4],0,1,1); +% or: +% dissim=daisy(ruspini,[4,4],1) +% result2=pam(dissim.dys,2); +% makeplot(result2); +% +% The output of PAM is a structure containing: +% result.dys : Dissimilarities (read column by column from the +% lower dissimilarity matrix) +% result.metric : Metric used +% result.number : Number of observations +% result.ttd : Average silhouette width per cluster +% result.ttsyl : Average silhouette width for dataset +% result.idmed : Id of medoid observations +% result.obj : Objective function at the first two iterations +% result.ncluv : Cluster membership for each observation +% result.clusinf : Matrix, each row gives numerical information for +% one cluster. These are the cardinality of the cluster +% (number of observations), the maximal and average +% dissimilarity between the observations in the cluster +% and the cluster's medoid, the diameter of the cluster +% (maximal dissimilarity between two observations of the +% cluster), and the separation of the cluster (minimal +% dissimilarity between an observation of the cluster +% and an observation of another cluster). +% result.sylinf : Matrix, with for each observation i the cluster to +% which i belongs, as well as the neighbor cluster of i +% (the cluster, not containing i, for which the average +% dissimilarity between its observations and i is minimal), +% and the silhouette width of i. The last column +% contains the original object number. +% result.nisol : Vector, with for each cluster specifying whether it is +% an isolated cluster (L- or L*-clusters) or not isolated. +% A cluster is an L*-cluster iff its diameter is smaller than +% its separation. A cluster is an L-cluster iff for each +% observation i the maximal dissimilarity between i and any +% other observation of the cluster is smaller than the minimal +% dissimilarity between i and any observation of another cluster. +% Clearly each L*-cluster is also an L-cluster. +% result.x : (Standardized) data or Dissimilarity vector (read +% row by row) +% result.class : 'PAM' +% +% And PAM will create the silhouette plot and the clusplot if plots equals 1 +% (an empty bar indicated by zero in the silhouette plot is a sparse between two clusters). +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Guy Brys (May 2006) +% Last updated: March 2009 by Sabine Verboven, Mia Hubert + +%Checking and filling in the inputs +res1=[]; +if (nargin<2) + error('Two input arguments required') +elseif ((nargin<3) && (size(x,2)~=1) && (size(x,1)~=1)) + error('Three input arguments required') +elseif (nargin<3) + if (size(x,2)==1) + x = x'; + end + res1.metric = 'unknown'; + res1.dys = x; + lookup=seekN(x); + res1.number = lookup.numb; %(1+sqrt(1+8*size(x,1)))/2; + stdize = 0; + plots = 0; +elseif (nargin<4) + stdize = 0; + plots = 0; + if (sum(vtype)~=4*size(x,2)) + metri=0; + metric = 'mixed'; + else + metri=1; + metric = 'euclidean'; + end +elseif (nargin<5) + plots = 0; + if (sum(vtype)~=4*size(x,2)) + metri=0; + metric = 'mixed'; + else + metri=1; + metric = 'euclidean'; + end +elseif (nargin<6) + plots = 0; +end + +if nargin>2 && length(vtype)~=size(x,2) + error('The variable type vector ''vtype'' has not the same length as the number of variables') +end + +% defining metric +if (nargin>4) + if (metric==1) + metric='euclidean'; + metri=1; + elseif (metric==2) + metric='manhattan'; + metri=2; + elseif (metric==0) + metric='mixed'; + metri=0; + else + error('metric must be 0,1 or 2') + end +end + +%Replacement of missing values +for i=1:size(x,1) + A=find(isnan(x(i,:))); + if (~(isempty(A))) + for j=A + valmisdat=0; + for c=1:size(x,2) + if (c~=j) + [a,b] = sort(x(:,c)); + if ~isempty(b(a==x(i,c))) + valmisdat=valmisdat+find(a==x(i,c)); + end + end + end + x(i,j)=prctile(x(isnan(x(:,j))==0,j),100*valmisdat/(size(x,1)*(size(x,2)-1))); + end + end +end + +%Standardization +if (stdize==1) & metri==1 + x = ((x - repmat(mean(x),size(x,1),1))./(repmat(std(x),size(x,1),1))); +elseif (stdize==2) & metri==1 + x = ((x - repmat(median(x),size(x,1),1))./(repmat(mad(x),size(x,1),1))); +end + +%Calculating the dissimilarities with daisy +if (isempty(res1)) + res1=daisy(x,vtype,metri); +end + +%Actual calculations (the second for latter use with CLUSPLOT) +[dys,ttd,ttsyl,idmed,obj,ncluv,clusinf,sylinf,nisol]=pamc(res1.number,kclus,[0 res1.dys]'); +dys=res1.dys(lowertouppertrinds(res1.number)); % lower dissimilarities matrix read column by column + +for c=1:kclus + avsylwclus(c)=mean(sylinf(sylinf(:,1)==c,3)); +end + +%Putting things together +result = struct('dys',dys,'metric',res1.metric,'number',res1.number,... + 'ttd',avsylwclus,'ttsyl',ttsyl,'idmed',idmed,'obj',obj,'ncluv',ncluv,... + 'clusinf',clusinf,'sylinf',sylinf,'nisol',nisol,'x',x,'class','PAM'); + +% Plots +try + if plots + makeplot(result,'classic',0) + end +catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu +end +%------------ +%SUBFUNCTIONS + +function dv = lowertouppertrinds(n) + +dv=[]; +for i=0:(n-2) + dv = [dv cumsum(i:(n-2))+repmat(1+sum(0:i),1,n-i-1)]; +end + +%--- +function outn = seekN(x) + +ok=0; +numb=0; +k=size(x,2); +sums=cumsum(1:k); +for i=1:k + if(sums(i)==k) + + numb=i+1; + ok=1; + end +end +outn=struct('numb',numb,'ok',ok); diff --git a/LIBRA/pamc.mexw32 b/LIBRA/pamc.mexw32 new file mode 100644 index 0000000000000000000000000000000000000000..7c3a29efb2278cb51740d512f2472f66126002f4 GIT binary patch literal 12288 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 Y!01yBY>8H18LV!o7s!L}|4$b9Pbo1 + cen=zeros(q,1)'; + result.resd=sqrt(mahalanobis(result.res,cen,'cov',inmodel.mcdreg.cov))'; + else + result.resd=result.res/inmodel.lts.scale; + end + %cutoffs + quan=chi2inv(0.99,inmodel.k); + result.cutoff.sd=quan^0.5; + if inmodel.k~=rank(X) + [m,s]=unimcd(inmodel.od.^(2/3),inmodel.h); + result.cutoff.od = sqrt(norminv(0.99,m,s).^3); + else + result.cutoff.od=0; + end + result.cutoff.resd=sqrt(chi2inv(0.99,q)); +elseif strcmp(inmodel.class,'RSIMPLS') + XRc=X-repmat(inmodel.robpca.M(1:p),n,1); + result.T=XRc*inmodel.weights.r; + result.sd=sqrt(mahalanobis(result.T,inmodel.Tcenter,'cov',inmodel.Tcov))'; + Xtilde=result.T*inmodel.weights.p'; + Rdiff=XRc-Xtilde; + for i=1:n + result.od(i,1)=norm(Rdiff(i,:)); + end + if q >1 + cen=zeros(q,1)'; + result.resd=sqrt(mahalanobis(result.res,cen,'cov',inmodel.cov))'; + else + result.resd=result.res/sqrt(inmodel.cov); + end + %cutoffs + quan=chi2inv(0.99,inmodel.k); + result.cutoff.sd=quan^0.5; + if inmodel.k~=rank(X) + [m,s]=unimcd(inmodel.od.^(2/3),inmodel.h); + result.cutoff.od = sqrt(norminv(0.99,m,s).^3); + else + result.cutoff.od=0; + end + result.cutoff.resd=sqrt(chi2inv(0.99,q)); +end + + +% Defining flags +result.flag.od=(result.od<=result.cutoff.od); +result.flag.sd=(result.sd<=result.cutoff.sd); +result.flag.resd=(abs(result.resd)<=result.cutoff.resd); +result.flag.all=result.flag.od & result.flag.resd; + +% Rmsep based on data with 'result.flag.resd = 1', and the trimmed RMSEPs +N=sum(result.flag.resd); +Nh=ceil(inmodel.h/size(inmodel.fitted,1)*n); +N50=ceil(0.5*n); +N75=ceil(0.75*n); +N95=ceil(0.95*n); +if q>1 + result.rmsep.outlfree=sqrt(1/(N*q)*sum(sum(result.res(result.flag.resd==1).^2,2))); + sortsqres=sort(sum(result.res.^2,2)); + trimh=sum(sortsqres(1:Nh,:)); + trim50=sum(sortsqres(1:N50,:)); + trim75=sum(sortsqres(1:N75,:)); + trim95=sum(sortsqres(1:N95,:)); + result.rmsep.trimh=sqrt(1/(Nh*q)*trimh); + result.rmsep.trim50=sqrt(1/(N50*q)*trim50); + result.rmsep.trim75=sqrt(1/(N75*q)*trim75); + result.rmsep.trim95=sqrt(1/(N95*q)*trim95); +else + result.rmsep.outlfree=sqrt(1/N*sum((result.res(result.flag.resd==1)).^2)); + sortsqres=sort(result.res.^2); + trimh=sum(sortsqres(1:Nh)); + trim50=sum(sortsqres(1:N50)); + trim75=sum(sortsqres(1:N75)); + trim95=sum(sortsqres(1:N95)); + result.rmsep.trimh=sqrt(1/Nh*trimh); + result.rmsep.trim50=sqrt(1/N50*trim50); + result.rmsep.trim75=sqrt(1/N75*trim75); + result.rmsep.trim95=sqrt(1/N95*trim95); +end +result.class=inmodel.class; + +%In case the classical output is also given +if isstruct(inmodel.classic) && strcmp(inmodel.class,'RPCR') + % fitted values, residuals, distances + result.classic.fitted=X*inmodel.classic.slope + repmat(inmodel.classic.int,n,1); + result.classic.res=Y-result.classic.fitted; + XRc=X-repmat(inmodel.classic.cpca.M,n,1); + result.classic.T=XRc*inmodel.classic.cpca.P; + result.classic.sd=sqrt(mahalanobis(result.classic.T,zeros(size(result.classic.T,2),1),'cov',inmodel.classic.cpca.L))'; + Xtilde=result.classic.T*inmodel.classic.cpca.P'; + Rdiff=XRc-Xtilde; + for i=1:n + result.classic.od(i,1)=norm(Rdiff(i,:)); + end + if q >1 + cen=zeros(q,1)'; + result.classic.resd=sqrt(mahalanobis(result.classic.res,cen,'cov',inmodel.classic.cov))'; + else + result.classic.resd=result.classic.res/sqrt(inmodel.classic.cov); + end + % cutoffs and flags + quan=chi2inv(0.99,inmodel.k); + result.classic.cutoff.sd=quan^0.5; + if inmodel.k~=rank(X) + [m,s]=unimcd(inmodel.classic.od.^(2/3),inmodel.h); + result.classic.cutoff.od = sqrt(norminv(0.99,m,s).^3); + else + result.classic.cutoff.od=0; + end + result.classic.cutoff.resd=sqrt(chi2inv(0.99,q)); + result.classic.flag.od=(result.classic.od<=result.classic.cutoff.od); + result.classic.flag.sd=(result.classic.sd<=result.classic.cutoff.sd); + result.classic.flag.resd=(abs(result.classic.resd)<=result.classic.cutoff.resd); + result.classic.flag.all=result.classic.flag.od & result.classic.flag.resd; + % classical rmsep of the good observations (from the robust analysis) + N=sum(result.flag.resd); + if q>1 + result.classic.rmsep.outlfree=sqrt(1/(N*q)*sum(sum(result.classic.res(result.flag.resd==1).^2,2))); + sortsqresc=sort(sum(result.classic.res.^2,2)); + trimh=sum(sortsqresc(1:Nh,:)); + trim50=sum(sortsqresc(1:N50,:)); + trim75=sum(sortsqresc(1:N75,:)); + trim95=sum(sortsqresc(1:N95,:)); + result.classic.rmsep.trimh=sqrt(1/(Nh*q)*trimh); + result.classic.rmsep.trim50=sqrt(1/(N50*q)*trim50); + result.classic.rmsep.trim75=sqrt(1/(N75*q)*trim75); + result.classic.rmsep.trim95=sqrt(1/(N95*q)*trim95); + else + result.classic.rmsep.outlfree=sqrt(1/N*sum((result.classic.res(result.flag.resd==1)).^2)); + sortsqresc=sort(result.classic.res.^2); + trimh=sum(sortsqresc(1:Nh)); + trim50=sum(sortsqresc(1:N50)); + trim75=sum(sortsqresc(1:N75)); + trim95=sum(sortsqresc(1:N95)); + result.classic.rmsep.trimh=sqrt(1/Nh*trimh); + result.classic.rmsep.trim50=sqrt(1/N50*trim50); + result.classic.rmsep.trim75=sqrt(1/N75*trim75); + result.classic.rmsep.trim95=sqrt(1/N95*trim95); + end + result.classic.class=inmodel.classic.class; +elseif isstruct(inmodel.classic) && strcmp(inmodel.class,'RSIMPLS') + % fitted values, residuals, distances + result.classic.fitted=X*inmodel.classic.slope + repmat(inmodel.classic.int,n,1); + result.classic.res=Y-result.classic.fitted; + XRc=X-repmat(inmodel.classic.M(1:p),size(X,1),1); + result.classic.T=XRc*inmodel.classic.weights.r; + result.classic.sd=sqrt(mahalanobis(result.classic.T,zeros(size(result.classic.T,2),1),'cov',inmodel.classic.Tcov))'; + Xtilde=result.classic.T*inmodel.classic.weights.p'; + Rdiff=XRc-Xtilde; + for i=1:n + result.classic.od(i,1)=norm(Rdiff(i,:)); + end + if q >1 + cen=zeros(q,1)'; + result.classic.resd=sqrt(mahalanobis(result.classic.res,cen,'cov',inmodel.classic.cov))'; + else + result.classic.resd=result.classic.res/sqrt(inmodel.classic.cov); + end + % cutoffs and flags + quan=chi2inv(0.99,inmodel.k); + result.classic.cutoff.sd=quan^0.5; + if inmodel.k~=rank(X) + [m,s]=unimcd(inmodel.classic.od.^(2/3),inmodel.h); + result.classic.cutoff.od = sqrt(norminv(0.99,m,s).^3); + else + result.classic.cutoff.od=0; + end + result.classic.cutoff.resd=sqrt(chi2inv(0.99,q)); + result.classic.flag.od=(result.classic.od<=result.classic.cutoff.od); + result.classic.flag.sd=(result.classic.sd<=result.classic.cutoff.sd); + result.classic.flag.resd=(abs(result.classic.resd)<=result.classic.cutoff.resd); + result.classic.flag.all=result.classic.flag.od & result.classic.flag.resd; + % classical rmsep of the good observations (from the robust analysis) + N=sum(result.flag.resd); + if q>1 + result.classic.rmsep.outlfree=sqrt(1/(N*q)*sum(sum(result.classic.res(result.flag.resd==1).^2,2))); + sortsqresc=sort(sum(result.classic.res.^2,2)); + trimh=sum(sortsqresc(1:Nh,:)); + trim50=sum(sortsqresc(1:N50,:)); + trim75=sum(sortsqresc(1:N75,:)); + trim95=sum(sortsqresc(1:N95,:)); + result.classic.rmsep.trimh=sqrt(1/(Nh*q)*trimh); + result.classic.rmsep.trim50=sqrt(1/(N50*q)*trim50); + result.classic.rmsep.trim75=sqrt(1/(N75*q)*trim75); + result.classic.rmsep.trim95=sqrt(1/(N95*q)*trim95); + else + result.classic.rmsep.outlfree=sqrt(1/N*sum((result.classic.res(result.flag.resd==1)).^2)); + sortsqresc=sort(result.classic.res.^2); + trimh=sum(sortsqresc(1:Nh)); + trim50=sum(sortsqresc(1:N50)); + trim75=sum(sortsqresc(1:N75)); + trim95=sum(sortsqresc(1:N95)); + result.classic.rmsep.trimh=sqrt(1/Nh*trimh); + result.classic.rmsep.trim50=sqrt(1/N50*trim50); + result.classic.rmsep.trim75=sqrt(1/N75*trim75); + result.classic.rmsep.trim95=sqrt(1/N95*trim95); + end + result.classic.class=inmodel.classic.class; +end + + diff --git a/LIBRA/putlabel.m b/LIBRA/putlabel.m new file mode 100644 index 0000000..6d8a4aa --- /dev/null +++ b/LIBRA/putlabel.m @@ -0,0 +1,56 @@ +function putlabel(x,y,names,z,znames) + +%PUTLABEL plots user-specified labels to the observations in a two- or +% a three-dimensional figure. +% If no labels are given, the indices are plotted. +% +% Required input arguments: +% x : x-coordinates of the data +% y : y-coordinates of the data +% +% Optional input arguments: +% names : labels to be added on the plot. They must be listed in a +% column vector. +% z : z-coordinates of the data +% znames : labels to be added on the 3D-plot. They must be listed in a +% columnvector. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% I/O: putlabel(x,y,names,z,znames) +% +% Written by S. Verboven on 01/10/2002 +% Last update on 18/02/2004 + +xrange=get(gca,'Xlim'); +range=xrange(2)-xrange(1); +if nargin<3 + for i=1:length(x) + text(x(i)+range/50,y(i),num2str(i)); + end +else + if nargin<4 + for i=1:length(x) + text(x(i)+range/50,y(i),names(i,:)); + end + else + if nargin<5 + for i=1:length(x) + text(x(i)+range/50,y(i),z(i),num2str(i)); + end + else + for i=1:length(x) + text(x(i)+range/50,y(i),z(i),znames(i,:)); + end + end + end +end + + + + + + + \ No newline at end of file diff --git a/LIBRA/qn.m b/LIBRA/qn.m new file mode 100644 index 0000000..d134dcf --- /dev/null +++ b/LIBRA/qn.m @@ -0,0 +1,31 @@ +function result=qn(x) + +%QN is a scale estimator which does not require an auxiliary location estimate. +% Essentially it is the first quartile of all pairwise distances between +% two data points. Its definition is given by +% qn(x)= c_n 2.2219{|x_i - x_j|; iH#5lAqly=cz26o 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 PRv!*&$hr6blL!6*rM{8c literal 0 HcmV?d00001 diff --git a/LIBRA/qnm.m b/LIBRA/qnm.m new file mode 100644 index 0000000..f3e5f8d --- /dev/null +++ b/LIBRA/qnm.m @@ -0,0 +1,74 @@ +function result=qnm(x) + +%QNM is a scale estimator which does not require an auxiliary location estimate. +% Essentially it is the first quartile of all pairwise distances between +% two data points. Its definition is given by +% qn(x)= c_n 2.2219{|x_i - x_j|; i0)); + Qn(j) = 2.2219*d(h*(h-1)/2); +end +if n<=9 + switch n + case 2 + dn=0.399; + case 3 + dn=0.994; + case 4 + dn=0.512; + case 5 + dn=0.844; + case 6 + dn=0.611; + case 7 + dn=0.857; + case 8 + dn=0.669; + case 9 + dn=0.872; + end +else + if mod(n,2)==1 + dn=n/(n+1.4); + end + if mod(n,2)==0 + dn=n/(n+3.8); + end +end +result=dn*Qn; \ No newline at end of file diff --git a/LIBRA/randomset.m b/LIBRA/randomset.m new file mode 100644 index 0000000..91b2ad9 --- /dev/null +++ b/LIBRA/randomset.m @@ -0,0 +1,39 @@ +function [ranset,seed]=randomset(tot,nel,seed) + +%RANDOMSET draws randomly a subsample of nel cases out of tot. +%(It is called if not all (p+1)-subsets out of n will be considered.) +% +% Required input arguments: +% tot : The total number of observations to consider +% nel : The number of observations that the subsample must contain +% Optional input arguments: +% seed : To define the state of the generator (default=0) +% (0 sets the generator to its default initial state) +% +% Output arguments: +% ranset : Random subset of nel cases out of tot. +% seed : The corresponding state. +% +% +% I/O: +% [ranset,seed]=randomset(n,n/2,0) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html + +if nargin==2 + seed=0; +end + +for j=1:nel + [random,seed]=uniran(seed); + num=floor(random*tot)+1; + if j > 1 + while any(ranset==num) + [random,seed]=uniran(seed); + num=floor(random*tot)+1; + end + end + ranset(j)=num; +end \ No newline at end of file diff --git a/LIBRA/rapca.m b/LIBRA/rapca.m new file mode 100644 index 0000000..5d51fea --- /dev/null +++ b/LIBRA/rapca.m @@ -0,0 +1,430 @@ +function result=rapca(data,varargin); + +%RAPCA is a 'Reflection-based Algorithm for Principal Components Analysis'. +% It is resistant to outliers in the data. The robust loadings are computed +% using projection-pursuit techniques and reflections. +% Therefore RAPCA can be applied to both low and high-dimensional data sets. +% In low dimensions (at most 15), we recommend to use the MCD method instead +% (see mcdcov.m). +% +% The RAPCA algorithm is described in +% Hubert, M., Rousseeuw, P.J., Verboven, S. (2002), +% "A fast method for robust principal components with applications to chemometrics", +% Chemometrics and Intelligent Laboratory Systems, 60, 101-111. +% +% Required input arguments: +% data : data matrix (observations in the rows, variables in the +% columns) +% +% Optional input arguments: +% k : number of principal components to compute +% plots 0/1 : if equal to 1 a screeplot and an outlier map are drawn (default = 1) +% else plots are suppressed +% labsd : the 'labsd' observations with largest score distance are +% labeled on the outlier map (default = 3) +% labod : the 'labod' observations with largest orthogonal distance are +% labeled on the outlier map (default = 3) +% center 0/1 : if equal to 1 the data are centered around the L1-median (default = 1) +% else the data are centered around the coordinatewise median +% (not orthogonally equivariant, but faster) +% classic : If equal to one, the classical PCA analysis will be performed +% (see also cpca.m). (default = 0) +% +% If k is missing, or k = 0, a screeplot is drawn which allows you to select +% the number of principal components. If k = 0 and plots = 0, the algorithm itself +% will determine the number of components. This is not recommended. +% +% I/O: result=rapca(x,'k',k,'plots',1,'labsd',3,'labod',3,'center',1,'classic',0); +% The user should only give the input arguments that have to change their default value. +% The name of the input arguments needs to be followed by their value. +% The order of the input arguments is of no importance. +% +% Examples: +% result=rapca(x,'k',3,'plots',0) +% result=rapca(x,'labsd',5,'center',0) +% +% The output of RAPCA is a structure containing +% +% result.P : Robust loadings (eigenvectors) +% result.L : Robust eigenvalues +% result.M : Robust center of the data +% result.T : Robust scores +% result.k : Number of (chosen) principal components +% result.sd : Robust score distances within the robust PCA subspace +% result.od : Orthogonal distances to the robust PCA subspace +% result.cutoff : Cutoff values for the robust score and orthogonal distances +% result.flag : The observations whose score distance is larger than result.cutoff.sd (==> result.flag.sd) +% or whose orthogonal distance is larger than result.cutoff.od (==> result.flag.od) +% can be considered as outliers and receive a flag equal to zero (result.flag.all). +% The regular observations receive a flag 1. +% result.class : 'RAPCA' +% result.classic : If the input argument 'classic' is equal to one, this structure +% contains results of the classical PCA analysis (see also cpca.m). +% +% Let n denote the number of observations, and p the number of original variables, +% then RAPCA finds a robust center (p x 1) of the data and a loading matrix P which +% is (p x k) dimensional. Its columns are orthogonal and define a new coordinate +% system. The scores (n x k) are the coordinates of the centered observations with +% respect to the loadings. The eigenvalues are the squared robust scales of the +% observations projected on each of the loadings. +% Note that RAPCA also yields a robust covariance matrix (often singular) which +% can be computed as +% cov=result.P*result.L*result.P' +% +% The screeplot shows the eigenvalues and is helpful to select the number of +% principal components. +% The outlier map visualizes the observations by plotting their orthogonal +% distance to the robust PCA subspace versus their robust distances +% within the PCA subspace. This allows to classify the data points into 4 types: +% regular observations, good leverage points, bad leverage points and +% orthogonal outliers. Remark that the RAPCA algorithm by construction passes +% through 'result.k' data points. The orthogonal distance of these data points is thus zero. +% +% The outlier map (or diagnostic plot) is described in +% Hubert, M., Rousseeuw, P.J., Vanden Branden K. (2005), +% "ROBPCA: a new approach to robust principal components analysis", +% Technometrics, 47, 64--79. +% +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Sabine Verboven and Mia Hubert +% +% Last Update: 23/12/2003 + + +[n,p]=size(data); +counter=1; +default=struct('k',0,'center',1,'plots',1,'labsd',3,'labod',3,'h',floor(0.75*n),'classic',0); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +% +if nargin>1 + % + %placing inputfields in array of strings + % + for j=1:nargin-1 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % + %Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) %in case of similarity + for j=1:nargin-2 %searching the index of the accompanying field + if rem(j,2)~=0 %fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end +end +k=options.k; +center=options.center; +plots=options.plots; + +if k<0 + warning(['The number of principal components should be positive!']); +end + +% First Step: classical SVD on the data +% This step reduces the data space to the affine subspace +% spanned by r=min(n-1,p) observations. +if n < p + [loads,scores,lambda,r,centerX,clm]=kernelEVD(data); +else + [loads,scores,lambda,r,centerX,clm]=classSVD(data); +end +X=scores; + +% Second Step: Rstep on X +% computes the robust eigenvectors and eigenvalues +[S,P,out.T,kmax,Rm]=rstep(X,k,center,r); +L=S'.^2; +out.P=loads*P; + +if center==1 + % Retransforming the robust location to the original space + out.M=clm+Rm*loads'; +else + out.M=median(data); + datacentr=data-repmat(out.M,size(data,1),1); + out.T=datacentr*out.P; +end + +% Making screeplot to decide on the number of principal components +if plots==1 & k==0 + screeplot(L,'RAPCA'); + k=input(['How many principal components would you like to retain?\n']); + k=max(min(k,kmax),1); +elseif plots==1 + screeplot(L,'RAPCA'); + k=min(k,kmax); +elseif k~=0 + k=min(k,kmax) +else + disp(['The number of principal components is defined by the algorithm.']); + disp(['It is set to ',num2str(kmax),'.']); + k=kmax; +end + +% shrinking to k-dimensional subspace +out.P=out.P(:,1:k); +out.T=out.T(:,1:k); +out.L=L(1:k); +disp(['The outlier map is based on ',num2str(k),' principal component(s).']) +out.k=k; +out.h=options.h; + +% Computing distances +% Robust score distances in robust PCA subspace +out.sd=sqrt(mahalanobis(out.T,zeros(size(out.T,2),1),'cov',out.L))'; +out.cutoff.sd=sqrt(chi2inv(0.975,out.k)); +% Orthogonal distances to robust PCA subspace +XRc=data-repmat(out.M,n,1); +Xtilde=out.T*out.P'; +Rdiff=XRc-Xtilde; +for i=1:n + out.od(i,1)=norm(Rdiff(i,:)); +end +% Robust cutoff-value for the orthogonal distance +if k~=r + [m,s]=unimcd(out.od.^(2/3),out.h); + out.cutoff.od = sqrt(norminv(0.975,m,s).^3); +else + out.cutoff.od=0; +end +% Classical analysis +if options.classic==1 + out.classic.P=loads(:,1:out.k); + out.classic.L=lambda(1:out.k); + out.classic.M=clm; + out.classic.T=scores(:,1:out.k); + out.classic.k=out.k; + % Mahalanobis distance in classical PCA subspace + Tclas=centerX*loads(:,1:out.k); + out.classic.sd=sqrt(mahalanobis(Tclas,zeros(size(Tclas,2),1),'cov',out.classic.L))'; + out.classic.cutoff.sd=sqrt(chi2inv(0.975,out.k)); + % Orthogonal distances to classical PCA subspace + Xtilde=Tclas*loads(:,1:out.k)'; + Cdiff=centerX-Xtilde; + for i=1:n + out.classic.od(i,1)=norm(Cdiff(i,:)); + end + % Classical cutoff-values + if k~=r + m=mean(out.classic.od.^(2/3)); + s=sqrt(var(out.classic.od.^(2/3))); + out.classic.cutoff.od = sqrt(norminv(0.975,m,s)^3); + else + out.classic.cutoff.od=0; + end + out.classic.cutoff.sd=sqrt(chi2inv(0.975,out.k)); + out.classic.flag.od=(out.classic.od<=out.classic.cutoff.od); + out.classic.flag.sd=(out.classic.sd<=out.classic.cutoff.sd); + out.classic.class='CPCA'; + out.classic.classic=1; +else + out.classic=0; +end + +if k~=r + out.flag.od=(out.od<=out.cutoff.od); + out.flag.sd=(out.sd<=out.cutoff.sd); + out.flag.all=(out.flag.od)&(out.flag.sd); + if options.classic==1 + out.classic.flag.all=(out.classic.flag.od)&(out.classic.flag.sd); + end +else + out.flag.od=(out.od<=out.cutoff.od); + out.flag.sd=(out.sd<=out.cutoff.sd); + out.flag.all=out.flag.sd; + if options.classic==1 + out.classic.flag.all=out.classic.flag.sd; + end +end + + +% The output +result=struct('P',{out.P},'L',{out.L},'M',{out.M},'T',{out.T},'k',{out.k},... + 'sd', {out.sd},'od',{out.od},'cutoff',{out.cutoff},'flag',out.flag',... + 'class',{'RAPCA'},'classic',{out.classic}); +% Making outlier map + +try + if plots & options.classic + makeplot(result,'classic',1) + elseif plots + makeplot(result) + %figure, scorediagplot(out.sd,out.od,out.k,out.cutoff.sd,out.cutoff.od,'RAPCA',options.labsd,options.labod) + end +catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu +end + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function [S,P,t,kmax,med]= rstep(X,k,center,r); + +%RSTEP: this is an auxiliary function for 'rapca.m'. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Created by Sabine Verboven and Mia Hubert +% Part of the code is based on S-PLUS code from C. Croux. +% +% Last Update: 01/22/2002 +% + +warning on; +if nargin<2 + k=0; +end +if nargin<3 + center=1; +end +if nargin<4 + r=rank(X); +end +[n,p]=size(X); +if k==0 + p1=min(floor(n/2),r); +else + p1=min([k,r,floor(n/2)]); +end + +if k==0 | k > p1 + disp(['The maximum number of principal components is ',num2str(p1),'.']) + disp(['This is the minimum of (number of data points/2) and the rank of the data matrix.']) +end +S=zeros(p1,1); +V=zeros(p,p1); +switch center +case 0 + med=median(X); + Xcentr=X-repmat(med,n,1); +case 1 + med=l1median(X); + Xcentr=X-repmat(med,n,1); +end +Xnewcentr=Xcentr; +kmax=0; +Transfo=eye(p); +for l=1:p1, + B=Xnewcentr; + Bnorm=zeros(n,1); + for i=1:n + Bnorm(i)=norm(B(i,:),2); + end + Bnormr=Bnorm(Bnorm > 1.e-12); + B=B(Bnorm > 1.e-12,:); + %Searching in directions A + A=diag(1./Bnormr)*B; + if size(Xnewcentr,2)==1 %case l=p1 + V(1:l-1,l)=0; + V(l:p,l)=1; + Vorigin(:,l)=Transfo*V(:,p1); + t=Xcentr*Vorigin(:,p1); %last step needs extraction of scale directly in p-dim space + if n>40 + S(p1)=A_scale(t); + else + S(p1)=qnm(t); + end + kmax=kmax+1; + break + else + Y=Xnewcentr*A'; %projected points in columns + end + if n>40 + s=A_scale(Y); + else + s=qnm(Y); + end + [c,vj]=sort(s); + j=vj(length(s)); + S(l)=s(j); + if (S(1)/S(l) > 10^3) &(kmax 1.e-12 + if V(l:p,l)'*Base(:,1) < 0 + V(l:p,l)=(-1)*V(l:p,l); + end + u=(1./norm(Base(:,1)-V(l:p,l)))*(Base(:,1)-V(l:p,l)); + U=Base-2*repmat(u'*Base,p-l+1,1).*repmat(u,1,p-l+1); + else + U=Base; + end + % Transforming eigenvectors to the original pxp dimensional space + if l==1 + Vorigin(:,l)=V(:,l); + Transfo=U; + else + Edge=eye(p); + Edge(l:p,l:p)=U; + Vorigin(:,l)=Transfo*V(:,l); + Transfo=Transfo*Edge; + end + Xnewcentr=Xnewcentr*U; %Reflection of data + Xnewcentr=removal(Xnewcentr,0,1); +end +[S,I]=greatsort(real(S(1:kmax))); +P=Vorigin(:,I); +t=Xcentr*P; + +%-------------------------------------------------------------------------- +function [A_est]=A_scale(Z) + +% A_SCALE calculates the A estimate of scale of the columns of Z +% +% I/O: [A_est]=A_scale(Z); +% + +Z=Z'; +U=(Z - repmat(median(Z,2),1,size(Z,2)))./(repmat(madc(Z')',1,size(Z,2))); +[n,p]=size(U); +for i=1:n + Ui=U(i,:); + if any(isnan(Ui)) + scale(i)=0; + else + Zi=Z(i,:); + med=median(Zi); + m=madc(Zi-med); + Zi=Zi(abs(Ui)<3.85); + Ui=Ui(abs(Ui)<3.85); + Ti=sqrt(sum((Ui.^2).*((3.85^2-Ui.^2).^4)))*sqrt(p)*0.9471*m; + Ni=abs(sum((3.85^2-Ui.^2).*(3.85^2-5*(Ui.^2)))); + scale(i)=Ti/Ni; + end +end +A_est=scale; diff --git a/LIBRA/rda.m b/LIBRA/rda.m new file mode 100644 index 0000000..edf718b --- /dev/null +++ b/LIBRA/rda.m @@ -0,0 +1,527 @@ +function result=rda(x,group,varargin) + +%RDA performs linear and quadratic robust discriminant analysis +% on the data matrix x with known group structure. It is based on the +% MCD estimator (see mcdcov.m), hence it has to be applied to +% low-dimensional data. +% +% The Robust Discriminant method is described in: +% Hubert, M., Van Driessen, K. (2004), +% "Fast and Robust Discriminant Analysis," +% Computational Statistics and Data Analysis, 45, 301-320. +% +% Required input arguments: +% x : training data set (matrix of size n by p). +% group : column vector containing the group numbers of the training +% set x. For the group numbers, any strict positive integer is +% allowed assuming that the first group is the one with the smallest group number. +% +% Optional input arguments: +% alpha : (1-alpha) measures the fraction of outliers the MCD-algorithm should +% resist. Any value between 0.5 and 1 may be specified. (default = 0.75) +% method : String which indicates whether a 'linear' (default) or 'quadratic' +% discriminant rule should be applied +% misclassif : String which indicates how to estimate the probability of +% misclassification. It can be based on the +% training data ('training'), a validation set ('valid'), +% or cross-validation ('cv'). Default is 'training'. +% membershipprob : Vector which contains the membership probability of each +% group (sorted by increasing group number). If no priors are given, they are estimated as the +% proportions of regular observations in the training set. +% valid : If misclassif was set to 'valid', this field should contain +% the validation set (a matrix of size m by p). +% groupvalid : If misclassif was set to 'valid', this field should contain the group numbers +% of the validation set (a column vector). +% predictset : Contains a new data set (a matrix of size mp by p) from which the +% class memberships are unknown and should be predicted. +% plots : If equal to 1, one figure is created with the training data and the +% MCD tolerance ellipses for each group. This plot is +% only available for bivariate data sets. For technical reasons, a maximum +% of 6 groups is allowed. Default is one. +% classic : If equal to one, classical linear or quadratic discriminant analysis will be performed +% (see also cda.m). (default = 0) +% compare : If equal to one, the classical CDA analysis will be performed +% with the same weights and the same priors as the robust analysis +% has been performed. This is especially useful to compare the robust +% and classical result on the same data with the same priors. (default = 0) +% +% I/O: result=rda('alpha',0.5,'plots',0,'misclassif','training','method','linear',... +% 'membershipprob',proportions,'valid',y,'groupvalid',groupy,'classic',0); +% The user should only give the input arguments that have to change their default value. +% The name of the input arguments needs to be followed by their value. +% The order of the input arguments is of no importance. +% +% Examples: out=rda(x,group,'method','linear') +% out=rda(x,group,'plots',0) +% out=rda(x,group,'valid',y,'groupvalid',groupy) +% +% The output is a structure containing the following fields: +% +% result.assignedgroup : If there is a validation set, this vector contains the assigned group numbers +% for the observations of the validation set. Otherwise it contains the +% assigned group numbers of the original observations based on the discriminant rules. +% result.scores : If there is a validation set, this columnvector of size m contains the maximal discriminant +% scores for each observation from the validation set. Otherwise it is a columnvector of size n +% containing the maximal discriminant scores of the training set. +% result.method : String containing the method used to obtain the discriminant rules (either 'linear' or 'quadratic'). +% This is the same as the input argument method. +% result.cov : If method equals 'linear', this is a matrix containing the estimated common covariance matrix. +% If method equals 'quadratic', it is a cell array containing the covariances per group. +% result.center : A vector in which the rows contain the estimated centers of the groups. +% result.rd : A vector of length n containing the robust distances of each observation from the training set +% to the center of its group. +% result.flagtrain : Observations from the training set whose robust distance exceeds a certain cut-off value +% can be considered as outliers and receive a flag equal to zero. +% The regular observations receive a flag 1. (See also mcdcov.m) +% result.flagvalid : Observations from the validation set whose robust distance (to the center of their group) +% exceeds a certain cut-off value can be considered as outliers and receive a +% flag equal to zero. The regular observations receive a flag 1. +% If there is no validation set, this field is equal to zero. +% result.grouppredict : If there is a prediction set, this vector contains the assigned group numbers +% for the observations of the prediction set. +% result.flagpredict : Observations from the new data set (predict) whose robust distance (to the center of their group) +% exceeds a certain cut-off value can be considered as overall outliers and receive a +% flag equal to zero. The regular observations receive a flag 1. +% If there is no prediction set, this field is equal to zero. +% result.membershipprob : A vector with the membership probabilities. +% result.misclassif : String containing the method used to estimate the misclassification probabilities +% (same as the input argument misclassif) +% result.groupmisclasprob : A vector containing the misclassification probabilities for each group. +% result.avemisclasprob : Overall probability of misclassification (weighted average of the misclassification +% probabilities over all groups). +% result.class : 'RDA' +% result.classic : If the input argument 'classic' is equal to one, this structure +% contains results of the classical discriminant analysis (see also cda.m). +% result.compare : If the input argument 'compare' is equal to one, this strucuture +% contains results for the classical discriminant analysis with the same weights +% and priors as in the robust analysis. +% result.x : The training data set (same as the input argument x). +% result.group : The group numbers of the training set (same as the input argument group). +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Nele Smets and Sabine Verboven on 01/03/2004 +% Last Update: 01/07/2005 +% + +if nargin<2 + error('There are too few input arguments.') +end + +% assigning default-values +[n,p]=size(x); +if size(group,1)~=1 + group=group'; +end +if n ~= length(group) + error('The number of observations is not the same as the length of the group vector!') +end +g=group; +countsorig=tabulate(g); %contingency table (outputmatrix with 3 colums): value - number - percentage +[lev,levi,levj]=unique(g); +%Redefining the group number +if any(lev~= (1:length(lev))) + lev=1:length(lev); + g=lev(levj); + counts=tabulate(g); +else + counts=countsorig; +end + +if ~all(counts(:,2)) %some groups have zero values, omit those groups + disp(['Warning: group(s) ', num2str(counts(counts(:,2)==0,1)'), 'are empty']); + empty=counts(counts(:,2)==0,:); + counts=counts(counts(:,2)~=0,:); +else + empty=[]; +end + +if any(counts(:,2)<5)%some groups have less than 5 observations + error(['Group(s) ', num2str(counts(counts(:,2)<5,1)'), ' have less than 5 observations.']); +end +proportions = zeros(size(counts,1),1); +y=0; %initial values of the validation data set and its groupsvector +groupy=0; +counter=1; +default=struct('alpha',0.75,'plots',1,'misclassif','training','method','linear','membershipprob',proportions,... + 'valid',y,'groupvalid',groupy,'classic',0,'compare',0,'predictset',[]); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +%reading the user's input +if nargin>2 + % + %placing inputfields in array of strings + % + for j=1:nargin-2 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % + %Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) %in case of similarity + for j=1:nargin-2 %searching the index of the accompanying field + if rem(j,2)~=0 %fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end +end + +%Checking prior (>0 ) +prior=options.membershipprob; +if size(prior,1)~=1 + prior=prior'; +end +epsilon=10^-4; +if sum(prior) ~= 0 + if (any(prior < 0) | (abs(sum(prior)-1)) > epsilon) + error('Invalid membership probabilities.') + end +end +ng=length(proportions); +if length(prior)~=ng + error('The number of membership probabilities is not the same as the number of groups.') +end + + +%%%%%%%%%%%%%%%%%%MAIN FUNCTION %%%%%%%%%%%%%%%%%%%%% +%Checking if a validation set is given +if strmatch(options.misclassif, 'valid','exact') + if options.valid==0 + error(['The misclassification error will be estimated through a validation set',... + 'but no validation set is given!']) + else + validx = options.valid; + validgrouping = options.groupvalid; + if size(validx,1)~=length(validgrouping) + error('The number of observations in the validation set is not the same as the length of its group vector!') + end + if size(validgrouping,1)~=1 + validgrouping = validgrouping'; + end + countsvalidorig=tabulate(validgrouping); + countsvalid=countsvalidorig(countsvalidorig(:,2)~=0,:); + if size(countsvalid,1)==1 + error('The validation set must contain observations from more than one group!') + elseif any(ismember(empty,countsvalid(:,1))) + error(['Group(s) ' ,num2str(empty(ismember(empty,countsvalid(:,1)))), 'was/were empty in the original dataset.']) + end + end +elseif options.valid~=0 + validx = options.valid; + validgrouping = options.groupvalid; + if size(validx,1) ~= length(validgrouping) + error('The number of observations in the validation set is not the same as the length of its group vector!') + end + if size(validgrouping,1)~=1 + validgrouping = validgrouping'; + end + options.misclassif='valid'; + countsvalidorig=tabulate(validgrouping); + countsvalid=countsvalidorig(countsvalidorig(:,2)~=0); + if size(countsvalid,1)==1 + error('The validation set must contain more than one group!') + elseif any(ismember(empty,countsvalid(:,1))) + error(['Group(s) ' , num2str(empty(ismember(empty,countsvalid(:,1)))), ' was/were empty in the original dataset.']) + end +end + +%Discriminant rule based on the training set x +result1 = rawrule(x, g, prior, options.alpha, options.method); + +%Apply discriminant rule on validation set +if strmatch(options.misclassif,'valid','exact') + result2 = rewrule(validx, result1); + finalgroup = result2.class; +else + result2 = rewrule(x, result1); + finalgroup = result2.class; +end + +%Estimating the misclassification error +switch options.misclassif + case 'valid' + [v,vi,vj]=unique(validgrouping); + %Redefining the group number + if any(v~= (1:length(v))) + v=1:length(v); + validgrouping=v(vj); + end + if any(countsvalidorig(:,2)==0) + empty=setdiff(countsvalidorig(find(countsvalidorig(:,2)==0),1), countsorig(find(countsorig(:,2)==0))); + disp(['Warning: the test group(s) ' , num2str(empty), ' are empty']); + else + empty=[]; + end + misclas=-ones(1,length(lev)); + for i=1:size(validx,1) + if strmatch(options.method,'quadratic','exact') + dist(i) = mahalanobis(validx(i,:), result1.center(vj(i),:),'invcov',result1.invcov{vj(i)}); + else + dist(i) = mahalanobis(validx(i, :), result1.center(vj(i),:),'invcov',result1.invcov); + end + end + weightsvalid=zeros(1,length(dist)); + weightsvalid(dist <= chi2inv(0.975, p))=1; + for i=1:length(v) + if ~isempty(intersect(i,v)) + misclas(i)=sum((validgrouping(weightsvalid==1)==finalgroup(weightsvalid==1)') & (validgrouping(weightsvalid==1)==repmat(lev(i),1,sum(weightsvalid)))); + ingroup(i) = sum((validgrouping(weightsvalid == 1) == repmat(lev(i),1, sum(weightsvalid)))); + misclas(i) = 1 - (misclas(i)./ingroup(i)); + end + end + if any(misclas==-1) + misclas(misclas==-1)=0; + end + misclasprobpergroup=misclas; + misclas=misclas.*result1.prior; + misclasprob=sum(misclas); + case 'training' + for i=1:ng + misclas(i) = sum((g(result1.weights==1)==finalgroup(result1.weights==1)')&(g(result1.weights==1)==repmat(lev(i),1,sum(result1.weights)))); + ingroup(i) = sum((g(result1.weights == 1) == repmat(lev(i),1,sum(result1.weights)))); + end + misclas = (1 - (misclas./ingroup)); + misclasprobpergroup = misclas; + misclas = misclas.*result1.prior; + misclasprob = sum(misclas); + weightsvalid=0;%only available with validation set + case 'cv' + finalgroup=[]; + for i=1:length(x) + if (result1.weights(i) == 1) + xnew=removal(x,i,0); + groupnew=removal(group,0,i); + functie1res = rawrule(xnew, groupnew,prior,options.alpha, options.method); + functie2res = rewrule(x(i, :), functie1res); + finalgroup = [finalgroup; functie2res.class(1)]; + end + end + for i=1:ng + misclas(i) = sum((g(result1.weights == 1) == finalgroup') & (g(result1.weights == 1) == repmat(lev(i),1,sum(result1.weights)))); + ingroup(i) = sum(g(result1.weights == 1) == repmat(lev(i),1,sum(result1.weights))); + end + misclas = (1 - (misclas./ingroup)); + misclasprobpergroup= misclas; + misclas = misclas.* result1.prior; + misclasprob = sum(misclas); + weightsvalid=0; %only available with validation set +end + +%classify the new observations (predict) +if ~isempty(options.predictset) + resultpredict = rewrule(options.predictset, result1); + finalgrouppredict = resultpredict.class; + for i=1:size(options.predictset,1) + for j = 1:ng + if strmatch(options.method,'quadratic','exact') + distpredict(i,j) = mahalanobis(options.predictset(i,:), result1.center(j,:),'invcov',result1.invcov{j}); + else + distpredict(i,j) = mahalanobis(options.predictset(i, :), result1.center(j,:),'invcov',result1.invcov); + end + end + end + weightspredict = zeros(1,size(distpredict,1)); + weightspredict(min(distpredict,[],2) <= chi2inv(0.975, p))=1; +else + finalgrouppredict = 0; + weightspredict = 0; +end + +if options.classic + classicout=cda(x,g,'method',result2.method,'misclassif',options.misclassif,'membershipprob',options.membershipprob,'valid',options.valid,... + 'groupvalid',options.groupvalid,'plots',0,'predictset',options.predictset); +else + classicout=0; +end + +if options.compare + compareout=cda(x,g,'method',result2.method,'misclassif',options.misclassif,'membershipprob',result1.prior,'valid',options.valid,... + 'groupvalid',options.groupvalid,'plots',0,'predictset',options.predictset,'weightstrain',result1.weights,'weightsvalid',weightsvalid); +else + compareout=0; +end + +%Output structure +result=struct('assignedgroup',{finalgroup'},'scores',{result2.scores'},'method',{result2.method},'cov',{result1.cov}, ... + 'center',{result1.center},'rd',{result1.dist'},'flagtrain',{result1.weights},... + 'flagvalid',weightsvalid,'grouppredict',finalgrouppredict,'flagpredict',weightspredict','membershipprob',{result1.prior},... + 'misclassif',{options.misclassif},'groupmisclasprob',{misclasprobpergroup},'avemisclasprob',{misclasprob},... + 'class',{'RDA'},'classic',{classicout},'compare',{compareout},'x',{x},'group',{group}); + +if size(x,2)~=2 + result=rmfield(result,{'x','group'}); +end + +%Plotting the output +try + if options.plots & options.classic + makeplot(result,'classic',1) + elseif options.plots + makeplot(result) + end +catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu +end +%-------------------------------------------------------------------------- +function result=rawrule(x, g,prior, alfa, method) + +%computes the discrimination rule based on the training set x. + +[n,p]=size(x); +epsilon=10^-4; +counts=tabulate(g); %contingency table (outputmatrix with 3 colums): value - number - percentage +[lev,levi,levj]=unique(g); +if ~all(counts(:,2)) %some groups have zero values, omit those groups + empty=counts(counts(:,2)==0,1); +else + empty=[]; +end + +ng=size(counts,1); +switch method +case 'linear' %equal covariances supposed + [gun,gi,gj]=unique(g); + for j=1:length(gun) + group.mcd{j}=mcdcov(x(g==gun(j),:),'alpha',alfa,'plots',0); %covariance of group j + group.center(j,:)=group.mcd{j}.center; %center of all groups, matrix of ng x p + end + for i=1:n + zgeg(i,:)=x(i,:)-group.center(gj(i),:); + end + zmcd = mcdcov(zgeg,'alpha',alfa,'plots',0); + zmcdcenter = zmcd.center; + zmcdcov = zmcd.cov; + zgeg = zgeg - repmat(zmcdcenter,length(zgeg),1); + group.center = group.center + repmat(zmcdcenter,size(group.center,1),1); + dist=zeros(n,1); + for j=1:length(gun) + dist(g==gun(j))=mahalanobis(x(g==gun(j),:),group.center(j,:),'invcov',inv(zmcd.cov)); + end + weights=zeros(n,1); + weights(dist <= chi2inv(0.975,p))=1; + result.cov=zmcd.cov; %over all group + result.invcov=inv(zmcd.cov); + result.center=group.center; %all groups + result.weights=weights'; + result.dist=dist; + result.method=method; +case 'quadratic' + [gun,gi,gj]=unique(g); + xmcdweights=zeros(n,1); + for j=1:length(gun) + [group.mcd{j} raw{j}]=mcdcov(x(g==gun(j),:),'alpha',alfa,'plots',0); + group.cov{j}=group.mcd{j}.cov; %covariance of group j + group.invcov{j}=inv(group.cov{j}); + group.center(j,:)=group.mcd{j}.center; %center of all groups + xmcdweights(g==gun(j))=raw{j}.wt; + end + for i=1:n + xdist(i)=mahalanobis(x(i,:), group.center(gj(i),:), 'invcov',group.invcov{gj(i)}); + end + weights=xmcdweights; + result.cov=group.cov; %per group + result.invcov=group.invcov; + result.center = group.center; %all groups + result.weights = xmcdweights'; + result.dist = xdist'; + result.method = method; +end + +%Define the prior +if sum(prior) ~= 0 + result.prior = prior; +else + ngood=sum(weights); + %regular points are kept + ggood = g(weights==1); + countsgood=tabulate(ggood); + if empty + for i=1:length(empty) + countsgood(countsgood(:,1)==empty(i),:)= []; + end + end + if ~any(countsgood(:,2)) + disp(['Warning: the group(s) ', num2str(countsgood(countsgood(:,2) == 0,1)'), 'contain only outliers']); + countsgood=countsgood(countsgood(:,2)~=0,:); + end + result.prior = (countsgood(:,3)/100)'; +end + +%-------------------------------------------------------------------------- +function result=rewrule(x, rawobject) + +epsilon=10^-4; +center=rawobject.center; +covar=rawobject.cov; +invcov=rawobject.invcov; +prior=rawobject.prior; +method=rawobject.method; + +if (length(prior) == 0 | length(prior) ~= size(center,1)) + error('invalid prior') +end +if sum(prior) ~= 0 + if (any(prior < 0) | (abs(sum(prior)-1)) > epsilon) + error('invalid prior') + end +end +ngroup=length(prior); +[n,p]=size(x); +switch method +case 'linear' + for j=1:ngroup + for i=1:n + scores(i,j) = linclassification(x(i,:)', center(j,:)', invcov, prior(j)); + end + end + [maxs,maxsI] = max(scores,[],2); + for i=1:n + maxscore(i,1) = scores(i,maxsI(i)); + end + result.scores = maxscore; + result.class = maxsI; + result.method = method; +case 'quadratic' + for j=1:ngroup + for i=1:n + scores(i, j) = classification(x(i,:)', center(j,:)', covar{j}, invcov{j}, prior(j)); + end + end + [maxs,maxsI] = max(scores,[],2); + for i=1:n + maxscore(i,1) = scores(i,maxsI(i)); + end + result.scores = maxscore; + result.class = maxsI; + result.method = method; +end + + +%--------------make sure the input variables are column vectors! + +function out=classification(x, center, covar,invcov, priorprob) + +out=-0.5*log(abs(det(covar)))-0.5*(x - center)' * invcov *(x - center)+log(priorprob); + +%------------------- +function out=linclassification(x, center, invcov, priorprob) + +out=center'*invcov*x - 0.5*center'*invcov*center+log(priorprob); + diff --git a/LIBRA/regresdiagplot.m b/LIBRA/regresdiagplot.m new file mode 100644 index 0000000..6081d5f --- /dev/null +++ b/LIBRA/regresdiagplot.m @@ -0,0 +1,148 @@ +function regresdiagplot(xdist,ydist,cutoffx,cutoffy,k,multi,attrib,labsd,labresd) + +%REGRESDIAGPLOT makes a regression outlier map. +% The score distances (SD)/mahalanobis distances (MD)/robust distances (RD) and +% residual distances (ResD) calculated in a PCR or PLS analysis, or in a +% univariate or multivariate regression are plotted on the x and y-axis, respectively. +% The cutoff values marked by a red line indicate which observations are +% outlying with respect to the majority of the data. The terminology used is +% as indicated in the tabular: +% small SD/MD/RD | large SD/MD/RD +% ---------------------------------------------------------- +% small ResD/StdRes | regular point | good leverage point +% large ResD/StdRes | vertical outlier | bad leverage point +% +% For more details see: +% Hubert, M., Verboven, S. (2003), +% "A robust PCR method for high-dimensional regressors," +% Journal of Chemometrics, 17, 438-452. +% +% Required input arguments: +% +% xdist : Score distances from a PCA analysis or +% Mahalanobis distances from LS or MLR analysis or +% Robust distances from LTS or MCDREG analysis, +% ydist : Residual distances of the PCR method or +% Standardized residuals of one of the regression methods, +% cutoffx : Cutoff value for the SD/MD/RD, +% cutoffy : Cutoff value for the residual-distance, +% k : Number of principal components used in the PCR/PLS method, or zero +% if not available. +% multi : 0 for univariate analysis, 1 for multivariate analysis +% attrib : String identifying the method used = 'LS', 'MLR', 'LTS', 'MCDREG', 'RPCR', +% 'CPCR', 'RSIMPLS', 'CSIMPLS' +% +% Optional inputs: +% labsd : number of displayed points with largest distance on x-axes (default = 3) +% labresd : number of displayed points with largest distance on y-axes (default = 3) +% +% +% I/O: regresdiagplot(out.sd,out.rd,out.cutoff.sd,out.cutoff.rd,k,multi,attrib,labsd,labresd) +% +% Example: regresdiagplot(out.sd,out.rd,out.cutoff.sd,out.cutoff.rd,out.k,1,'RPCR',5,5) +% regresdiagplot(out.md,out.stdres,out.cutoffmd,0,0,0,'LS',5,4) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Sabine Verboven +% Created 25 February 2002 +% Revised : 12/02/2004 +% + +if nargin==8 + labresd=3; +end +if nargin==7 + labsd=3; + labresd=3; +end +if nargin<7 + error('A required input variable is missing!') +end + +if ischar(xdist) | ischar(ydist) + mess=sprintf(['Warning: A singularity was detected in the analysis. ',... + '\n No regression outlier map available.']); + disp(mess) + return +end +%all LTS-analysis in RPCR are intercept included!!! +if multi==1 %multivariate analysis + x=xdist; %distances on x-axis: SD, MD or RD + y=ydist; %residual distances + quanty=cutoffy; + quantx=cutoffx; +else %univariate analysis + quantx=cutoffx; + quanty=sqrt(chi2inv(0.975,1)); + x=xdist; %distances on x-axis: SD, MD, RD + y=ydist; %standardized residuals!!! +end + +plot(x,y,'o'); +set(gcf,'Name', 'Regression outlier map', 'NumberTitle', 'off'); +hold on +xmin=0;%max(0,min(x));%-0.5; +xmax=max([quantx max(x)])+0.5; + +if multi==1 + ymin=0;%max(0,min(y));%-1; + if size(y,2)==1 + y=y'; + end + ymax=max([y,quanty])+1; + xlim([xmin,xmax]); + ylim([ymin,ymax]); + ylimi=get(gca,'Ylim'); + line([quantx,quantx],[ylimi(1),ylimi(2)],'Linestyle','-','Color','r'); + xlimi=get(gca,'Xlim'); + line([xlimi(1),xlimi(2)],[quanty,quanty],'Linestyle','-','Color','r'); + if labsd + plotnumbers(x',y',labsd,labresd,2)%sort by x and y + end + ylabel('Residual distance') +else + ymin=min([-4 min(y)])-0.5; + ymax=max([4 max(y)])+0.5; + xlim([xmin,xmax]); + ylim([ymin,ymax]); + ylimi=get(gca,'Ylim'); + ylimi=[min(ylimi(1),min([-4 min(y)]))-0.5,max(ylimi(2),max([4 max(y)]))+0.5]; + line([quantx,quantx],[ylimi(1),ylimi(2)],'Linestyle','-','Color','r'); + xlimi=get(gca,'Xlim'); + xlimi=[min(xlimi(1),min(x))-1,max(xlimi(2),max([quantx max(x)]))+1]; + line([xlimi(1),xlimi(2)],[quanty,quanty],'Linestyle','-','Color','r'); + line([xlimi(1),xlimi(2)],[-quanty,-quanty],'Linestyle','-','Color','r'); + if labsd + plotnumbers(x',y',labsd,labresd,3)%sort by x and abs(y) + end + ylabel('Standardized residual') +end + +box on + +switch attrib + case 'LS' + xlabel('Mahalanobis distance'); + ylabel('Standardized LS residual'); + case 'MLR' + xlabel('Mahalanobis distance'); + case 'LTS' + xlabel('Robust distance computed by MCD'); + ylabel('Standardized LTS residual'); + case 'MCDREG' + xlabel('Robust distance computed by MCD'); + case {'CPCR'} + xlabel(['Score distance (',num2str(k),' LV)']); + case {'RPCR'} + xlabel(['Score distance (',num2str(k),' LV)']); + case {'CSIMPLS'} + xlabel(['Score distance (',num2str(k),' LV)']); + case {'RSIMPLS'} + xlabel(['Score distance (',num2str(k),' LV)']); + +end +title(attrib) +hold off diff --git a/LIBRA/regresdiagplot3d.m b/LIBRA/regresdiagplot3d.m new file mode 100644 index 0000000..baf5ca0 --- /dev/null +++ b/LIBRA/regresdiagplot3d.m @@ -0,0 +1,153 @@ +function regresdiagplot3d(sdist,odist,rdist,cutoffsd,cutoffod,cutoffrd,k,class,multi,labsd,labod,labresd,labels) + +%REGRESDIAGPLOT3D is a 3D-outlier map which visualizes the orthogonal distance, +% the score distance and the residual distance calculated in a PCR or PLSR analysis. +% +% I/O: regresdiagplot3D(sdist,odist,rdist,cutoffsd,cutoffrd,k,class,labsd,labod,labresd,labels) +% +% Example: out=rpcr(X,Y); +% regresdiagplot3d(out.sd,out.od,out.resd,out.cutoff.sd,cutoff.od,... +% out.cutoff.resd,out.k,out.class,1,out.labsd,out.labod,out.labresd,0) +% +% Uses function: putlabel +% +% Created on 09/04/2004 by S.Verboven +% Last revision: 30/01/2005 +% + +%INITIALIZATION% +if nargin<13 + labels=0; +end +if nargin<10 + labsd=3; + labod=3; + labresd=3; + labels=0; +end +if nargin==10 + labsd=3 + labels=0; + labresd=3; + labod=3; +end +if nargin==11 + labels=0; + labresd=3; + labod=3; +end +if nargin==12 + labels=0; + labresd=3; +end +if nargin<9 + error('A required input variable is missing!') +end + +%all LTS-analysis in RPCR are intercept included!!! +% if ask==2 %multivariate analysis +% %residual distances +% +% else %univariate analysis +% %standardized residuals +% cutoffz=2.5; +% end +cutoffxx=cutoffsd; +cutoffxy=cutoffsd; +cutoffyy=cutoffod; +cutoffyx=cutoffod; +cutoffz=cutoffrd; +x=sdist; +y=odist; +z=rdist; + + +%%%%%%%MAIN FUNCTION%%%%%%% +set(gcf,'Name', '3D-Outlier map (regression)', 'NumberTitle', 'off')%,'Renderer','OpenGL'); + +%%%%%%%%Odist=0 not yet included in this standalone!!!!!!!!! included in +%%%%%%%%makeplot function!!!! +plot3(x,y,z,'ko','markerfacecolor',[0.75 0.75 0.75]) + +hold on +axhandle=gca; +ylen=get(axhandle, 'Ylim'); +xlen=get(axhandle,'Xlim'); +zlen=get(axhandle,'Zlim'); +xrange=xlen(2)-xlen(1); +upLimx=max(cutoffxx,xlen(2))+xrange*0.1; +lowLimx=xlen(1)-xrange*0.1; +yrange=ylen(2)-ylen(1); +upLimy=ylen(2)+yrange*0.1; +lowLimy=ylen(1)-yrange*0.1; +zrange=zlen(2)-zlen(1); +upLimz=max(cutoffz,zlen(2))+zrange*0.1; +if cutoffz==2.5 + lowLimz=min(-2.5,zlen(1))-zrange*0.1; +else + lowLimz=min(cutoffz,zlen(1))-zrange*0.1; +end + +%axis square; +set(gca, 'Xlim',[lowLimx upLimx],'Ylim',[lowLimy upLimy],'Zlim',[lowLimz,upLimz]) +hold on + +xlabel('Score distance') +ylabel('Orthogonal distance') +if cutoffz~=2.5 + zlabel('Residual distance') +else + zlabel('Standardized Residual') +end +grid on + + +%in XY-space +%red plane "vertical" on x axis +oppy=[upLimy:-0.01:lowLimy]; +n=length(oppy); +h=(upLimz-lowLimz)/n; +oppz=[lowLimz:h:upLimz]; + +X1=cutoffxx*ones(n); +Y1=repmat(oppy,n,1); +Z1=repmat([oppz(1:n-1)'; upLimz],1,n); +surf(X1,Y1,Z1,'edgecolor','none','facecolor','r') +alpha(.3) + + +%blue "horizontal" planes orthogonal on z-axis +%in ZX and ZY-space +oppx=[lowLimx:0.05:upLimx]; +n=length(oppx); +X2=repmat(oppx,n,1); +h=(upLimy-lowLimy)/n; +oppy=[lowLimy:h:upLimy]; +Y2=repmat(oppy(1:n)',1,n); +Z2=cutoffz*ones(n); +surf(X2,Y2,Z2,'edgecolor','none','facecolor','b') +alpha(0.3) + +% in XY-space +%green plane "vertical" on y axis +oppx=[upLimx:-0.01:lowLimx]; +n=length(oppx); +h=(upLimz-lowLimz)/n; +oppz=[lowLimz:h:upLimz]; +X1=repmat(oppx,n,1); +Y1=cutoffyy*ones(n); +Z1=repmat([oppz(1:n-1)'; upLimz],1,n); +surf(X1,Y1,Z1,'edgecolor','none','facecolor','g') +alpha(0.3) + +if multi==0 %univariate case + surf(X2,Y2,-Z2,'edgecolor','none','facecolor','b') + alpha(0.3) +end + +if labels~=0 + putlabel(x,y,labels,z,labels) +else + plotnumbers(x,y,labsd,labod,5,z,labresd) +end +hold off \ No newline at end of file diff --git a/LIBRA/removal.m b/LIBRA/removal.m new file mode 100644 index 0000000..d5ac28a --- /dev/null +++ b/LIBRA/removal.m @@ -0,0 +1,32 @@ +function restX=removal(X,r,k) + +%REMOVAL deletes rows(r) or columns(k) from X +% Type in zero if you do not want to delete any rows (or columns) +% r= rowvector of object numbers you want to remove +% k= rowvector of variable numbers you want to remove +% +%I/O: restX=removal(X,r,k) +% +%Created by S.Verboven (1999) +%Last edited 25/04/2000 + +if nargin <3 + error('All inputarguments must be given. Type in zero if you do not want to delete any rows(or columns)') +end +restX=X; +if size(r,1)~=1 + r=r'; +end +if size(k,1)~=1 + k=k'; +end +if k==0 & r~=0 + restX(r,:)=[]; +end +if r==0 & k~=0 + restX(:,k)=[]; +end +if r~=0 & k~=0 + restX(r,:)=[]; + restX(:,k)=[]; +end diff --git a/LIBRA/removeObsMcd.m b/LIBRA/removeObsMcd.m new file mode 100644 index 0000000..3390b07 --- /dev/null +++ b/LIBRA/removeObsMcd.m @@ -0,0 +1,91 @@ +function result = removeObsMcd(data,i,inputH0,inputFull,csteps); + +%REMOVEOBSMCD is an auxiliary function to perform cross-validation with MCD +% (see cvMcd.m). +% +% The input: +% data : the original data +% i : the index of the observation that has to be removed. +% inputH0 : a structure that contains the following fields: +% inputH0.H0 : the optimal H subset based on the original data +% inputH0.j : the index of the observation that is removed within H0. +% inputH0.same +% inputFull : a structure that contains the following fields: +% inputFull.T : the mean data(H0,:) +% inputFull.S : the cov data(H0,:) +% csteps : csteps.value = 1 (default) if csteps must performed on the updated cov and mu. else = 0. +% csteps.number = the maximal number of csteps to perform. default = 20. +% +% The output: +% out.P_min_i : the loadingvector after observation i is removed. +% out.L_min_i : the eigenvalues after observation i is removed. +% out.mu_min_i : the center of the data after observation i is removed. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by S. Engelen + +if nargin < 5 + csteps.value = 1; + csteps.number = 2; +end + +n = size(data,1); +p = size(data,2); +h = length(inputH0.H0); + +Tfull = inputFull.T; +Sfull = inputFull.S; + +H0 = inputH0.H0; +j = inputH0.j; + +data_min_i = removal(data,i,0); + +[upS,upT] = updatecov(data(H0,:),Sfull,Tfull,0,[j],0); +mahdist = mahalanobis(data_min_i,upT,'cov',upS); +sortmahdist = sort(mahdist); +factor=sortmahdist(h-1)/chi2inv((h-1)/(n-1),p/2); % adapted factor + +% performing c-steps on the updated mu and cov. +if csteps.value + oldobj = det(upS); + for noCsteps = 1:csteps.number + [mahsort, indsort] = sort(mahdist); + covcstep = cov(data_min_i(indsort(1:(h-1)),:)); + mucstep = mean(data_min_i(indsort(1:(h-1)),:)); + obj = det(covcstep); + if abs(oldobj - obj) > 1.e-12 + oldobj = obj; + else + break + end + end +end + +TMCD = upT; +SMCD = factor*upS; +mahdist = mahdist/factor; +weights = (mahdist <= chi2inv(0.975,p)); + +% Reweighting: +[mu_min_i,S_min_i] = weightmecov(data_min_i,weights); +[PS1,LS1] = eig(S_min_i); +[P_min_i,L_min_i] = sortPL(PS1,LS1); + +res.mu_min_i = mu_min_i; +res.L_min_i = L_min_i; +res.P_min_i = P_min_i; + +result = res; + +%--------------------------------------------------------------------------------------------- +function [sortedP,sortedL] = sortPL(P,L) + +% Sorts P and L for PCA, the columns are sorted by decreasing eigenvalues. + +[sortedEw,indexsortedEw] = greatsort(diag(L)); +sortedP = P(:,indexsortedEw); +sortedL = diag(sortedEw); \ No newline at end of file diff --git a/LIBRA/removeObsRobpca.m b/LIBRA/removeObsRobpca.m new file mode 100644 index 0000000..79a98d3 --- /dev/null +++ b/LIBRA/removeObsRobpca.m @@ -0,0 +1,168 @@ +function res = removeObsRobpca(data,i,k,Hsets_min_i,same,factor_ind,csteps) + +%REMOVEOBSROBPCA is an auxiliary function to perform cross-validation with ROBPCA, +% RPCR, RSIMPLS, RSIMCA (see cvRobpca.m, cvRpcr.m, cvRsimpls.m). +% +% Input: +% data : the data set +% i : the observation that is removed, index with respect to the whole data set. +% k : the number of principal components that has to be calculated. +% Hsets_min_i : contains H0_min_i, H1_min_i and Hfreq_min_i as first, second and third row respectively. +% The h-subsets are implemented by means of indices of the observations in data_min_i, which is +% is the original data set minus sample i. +% same : structure : +% same.value : indicates whether some part of this algorithm can be skipped (= 1) or not ( = 0). +% same.res : if same.value = 1, then some additional information is needed. +% factor : optional. default = 0, then the original consistency factor is used. +% Else factor = 1, the consistency factor is adapted to the kmax approach. +% csteps : optional structure: +% csteps.value : 1 (default, then csteps are performed within robpca), 0 (no csteps are performed within robpca) +% csteps.number : the number of csteps that need to be performed (default = 2). +% +% Output: +% res is the result structure. It contains: +% Pk_min_i : update of the loadingmatrix for a certain k when observation i is deleted. +% Lk_min_i : update of the eigenvalues for a certain k when observation i is deleted. +% muk_min_i : update of the center for a certain k when observation i is deleted. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by S. Engelen + +rot = []; +center = []; +P1 = []; + +n = size(data,1); +p = size(data,2); +h = size(Hsets_min_i,2) + 1; + +if nargin < 6 + factor_ind = 0; + csteps.number = 2; + csteps.value = 1; +elseif nargin == 6 + csteps.number = 2; + csteps.value = 1; +end + +data_min_i= removal(data,i,0); +mu0 = mean(data); +center = mu0; + +H0_min_j = Hsets_min_i(1,:); +H1_min_t = Hsets_min_i(2,:); + +if ~same.value + [PH0_min_i,TH0,LH0_min_i,rH0_min_i,centerX,mu1trafo]=kernelEVD(data_min_i(H0_min_j,:)); + LH0_min_i = diag(LH0_min_i); + + % calculation of the projection + % res.T2tilde = T2tilde; + center = mu1trafo; + rot = PH0_min_i; + T2tilde = (data_min_i - repmat(mu1trafo,n-1,1))*PH0_min_i; + T2tilde = T2tilde(:,1:k); + rot = rot(:,1:k); + + % defining the outputstructure res: + res.PH0_min_i = PH0_min_i; + res.LH0_min_i = LH0_min_i; + res.mu1trafo = mu1trafo; +else + % defining the input structure input + res = same.res; + PH0_min_i = res.PH0_min_i; + LH0_min_i = res.LH0_min_i; + mu1trafo = res.mu1trafo; + T2tilde = (data_min_i - repmat(mu1trafo,n-1,1))*PH0_min_i; + center = mu1trafo; + rot = PH0_min_i; + rot = rot(:,1:k); + T2tilde = T2tilde(:,1:k); +end + +mah = mahalanobis(T2tilde,zeros(1,k),'invcov',1./diag(LH0_min_i(1:k,1:k))); +oldobj = prod(diag(LH0_min_i(1:k,1:k)));P4 = eye(k); + +if csteps.value + oldobj = prod(diag(LH0_min_i(1:k,1:k))); + for j = 1:csteps.number + [mahsort, indsort] = sort(mah); + dataH1 = T2tilde(indsort(1:(h-1)),:); + [P,T,L,r3,Xm,clmX] = classSVD(dataH1); + obj = prod(L); + T2tilde = (T2tilde - repmat(clmX,n-1,1))*P; + center = center + clmX*rot'; + rot = rot*P; + mah = mahalanobis(T2tilde,zeros(1,size(T2tilde,2)),'invcov',1./L); + P4 = P4*P; + if abs(oldobj - obj) > 1.e-12 + oldobj = obj; + else + break + end + end +else + obj = oldobj; +end + +% extra reweighting: +if k~=r3 + XRc= data_min_i-repmat(center,n-1,1); + Xtilde = T2tilde*rot'; + Rdiff = XRc-Xtilde; + for i=1:n + odh(i,1)=norm(Rdiff(i,:)); + end + [m,s]=unimcd(odh.^(2/3),h); + cutoffodh = sqrt(norminv(0.975,m,s).^3); + indexset = find(odh<=cutoffodh)'; + [P,Threw,Lrew,rrew,Xmrew,clmX]=kernelEVD(data_min_i(indexset,:)); + center = clmX; + rot = P(:,1:k); +end +T2tilde = (data_min_i - repmat(center,n-1,1))*rot; + +% Perform mcdcov on some H-subsets: +[res_min_i,raw_min_i] = mcdcov(T2tilde,'Hsets',Hsets_min_i,'h',h-1,'ntrial',250,'plots',0,'factor',1); +% perform the last part of ROBPCA: +if raw_min_i.objective < obj + z = res_min_i; +else + sortmah = sort(mah); + if h==n + factor=1; + else + if factor_ind == 1 + factor = sortmah(h-1)/chi2inv((h-1)/size(T2tilde,1),size(T2tilde,2)/2); % adjusted factor. + else + factor = sortmah(h-1)/chi2inv((h-1)/size(T2tilde,1),size(T2tilde,2)); % non adjusted factor + end + end + mah = mah/factor; + weights = mah <= chi2inv(0.975,size(T2tilde,2)); + [center_noMCD,cov_noMCD] = weightmecov(T2tilde,weights); + mah = mahalanobis(T2tilde,center_noMCD,'cov',cov_noMCD); + z.flag = (mah <= chi2inv(0.975,size(T2tilde,2))); + z.center = center_noMCD; + z.cov = cov_noMCD; +end + +covf=z.cov; +centerf=z.center; + +% The final PC: +[P6tilde,L6]=eig(covf); +[L6,I]=greatsort(diag(L6)); +P6tilde=P6tilde(:,I); +T_min_i=(T2tilde-repmat(centerf,n-1,1))*P6tilde; + +Pk_min_i=rot(:,1:k)*P6tilde; +Lk_min_i = L6; + +res.Pk_min_i = Pk_min_i; +res.Lk_min_i = Lk_min_i; +res.muk_min_i = center + centerf*rot'; diff --git a/LIBRA/residualplot.m b/LIBRA/residualplot.m new file mode 100644 index 0000000..1958ab4 --- /dev/null +++ b/LIBRA/residualplot.m @@ -0,0 +1,52 @@ +function residualplot(x,residuals,attrib,labx,laby,nid) + +%RESIDUALPLOT plots the residuals from a regression analysis versus x +% +% Required input arguments: +% x : the vector to be plotted on de x-axis +% residuals : the residuals +% attrib : string identifying the used method = 'LS', 'MLR', 'LTS', 'MCDREG' +% +% Optional input arguments: +% labx : a label for the x-axis (default: ' ') +% laby : a label for the y-axis (default:' ') +% nid : number of points to be identified in plots (default: 3) +% +% I/O: residualplot(x,residuals,attrib,labx,laby,nid) +% +% Last update: 07/04/2004 +% + +set(gcf,'Name', 'Residual plot', 'NumberTitle', 'off'); +n=length(residuals); +if nargin<2 + error('A required input argument is missing.') +elseif nargin==3 + labx=''; + laby=''; + nid=3; +elseif nargin==4 + laby=''; + nid=3; +elseif nargin==5 + nid=3; +end + +plot(x,residuals,'bo') +xlabel(labx); +ylabel(laby); +ord=abs(residuals); +[ord,ind]=sort(ord); +ind=ind(n-nid+1:n); +text(x(ind),residuals(ind),int2str(ind)); +v=axis; +quant=sqrt(chi2inv(0.975,1)); +line([v(1),v(2)],[quant,quant],'color','r'); +line([v(1),v(2)],[0,0],'color','r'); +line([v(1),v(2)],[-quant,-quant],'color','r'); +xlim=([min(x), max(x)]); +ymin=min([-3 min(residuals)+0.05*min(residuals)]); +ymax= max([3 max(residuals)+0.05*max(residuals)]); +ylim([ymin,ymax]); +title(attrib) +box on \ No newline at end of file diff --git a/LIBRA/rmc.m b/LIBRA/rmc.m new file mode 100644 index 0000000..42be3b8 --- /dev/null +++ b/LIBRA/rmc.m @@ -0,0 +1,43 @@ +function [res] = rmc(x) + +%RMC calculates the right medcouple, a robust measure of +%right tail weight +% +% The right medcouple is described in: +% Brys, G., Hubert, M. and Struyf, A. (2006), +% "Robust Measures of Tail Weight", +% Computational Statistics and Data Analysis, +% 50 (No 3), 733-759. +% +% For the up-to-date reference, please consult the website: +% wis.kuleuven.be/stat/robust.html +% +% Required input arguments: +% x : Data matrix (rows=observations, columns=variables) +% +% I/O: +% result=rmc(x); +% +% Example: +% result = rmc([chi2rnd(5,1000,1) trnd(3,1000,1)]); +% +% The output of RMC is a vector containing the right medcouple +% for each column of the data matrix x +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Guy Brys +% Last Update: 17/03/2006 + +if (nargin<1) + error('No input arguments') +end +if (size(x,1)==1) + x = x'; +end +for (i=1:size(x,2)) + res(i) = mc(x(x(:,i)>=prctile(x(:,i),50),i)); +end + diff --git a/LIBRA/robpca.m b/LIBRA/robpca.m new file mode 100644 index 0000000..0c90029 --- /dev/null +++ b/LIBRA/robpca.m @@ -0,0 +1,866 @@ +function result=robpca(x,varargin) + +%ROBPCA is a 'ROBust method for Principal Components Analysis'. +% It is resistant to outliers in the data. The robust loadings are computed +% using projection-pursuit techniques and the MCD method. +% Therefore ROBPCA can be applied to both low and high-dimensional data sets. +% In low dimensions, the MCD method is applied (see mcdcov.m). +% +% The ROBPCA method is described in +% Hubert, M., Rousseeuw, P.J., Vanden Branden, K. (2005), ROBPCA: a +% new approach to robust principal components analysis, Technometrics, 47, 64-79. +% +% +% To select the number of principal components, a robust PRESS (predicted +% residual sum of squares) curve is drawn, based on a fast algorithm for +% cross-validation. This approach is described in: +% +% Hubert, M., Engelen, S. (2007), +% "Fast cross-validation of high-breakdown resampling algorithms for PCA", +% Computational Statistics and Data Analysis, 51, 5013-5024. +% +% ROBPCA is designed for normally distributed data. If the data are skewed, +% a modification of ROBPCA is available, based on the adjusted outlyingness +% (see adjustedoutlyingness.m). This method is described in: +% +% Hubert, M., Rousseeuw, P.J., Verdonck, T. (2009), +% "Robust PCA for skewed data and its outlier map", Computational Statistics +% and Data Analysis, 53, 2264-2274. +% +% Required input arguments: +% x : Data matrix (observations in the rows, variables in the +% columns) +% +% Optional input arguments: +% k : Number of principal components to compute. If k is missing, +% or k = 0, a scree plot and a press curve are drawn which allows you to select +% the number of principal components. +% kmax : Maximal number of principal components to compute (default = 10). +% If k is provided, kmax does not need to be specified, unless k is larger +% than 10. +% alpha : (1-alpha) measures the fraction of outliers the algorithm should +% resist. Any value between 0.5 and 1 may be specified (default = 0.75). +% h : (n-h+1) measures the number of outliers the algorithm should +% resist. Any value between n/2 and n may be specified. (default = 0.75*n) +% Alpha and h may not both be specified. +% mcd : If equal to one: when the number of variables is sufficiently small, +% the loadings are computed as the eigenvectors of the MCD covariance matrix, +% hence the function 'mcdcov.m' is automatically called. The number of +% principal components is then taken as k = rank(x). (default) +% If equal to zero, the robpca algorithm is always applied. +% plots : If equal to one, a scree plot, a press curve and a robust score outlier map are +% drawn (default). If the input argument 'classic' is equal to one, +% the classical plots are drawn as well. +% If 'plots' is equal to zero, all plots are suppressed (unless k is missing, +% then the scree plot and press curve are still drawn). +% See also makeplot.m +% labsd : The 'labsd' observations with largest score distance are +% labeled on the outlier map. (default = 3) +% labod : The 'labod' observations with largest orthogonal distance are +% labeled on the outlier map. default = 3) +% classic : If equal to one, the classical PCA analysis will be performed +% (see also cpca.m). (default = 0) +% scree : If equal to one, a scree plot is drawn. If k is given as input, the default value is 0, else the default value is one. +% press : If equal to one, a plot of robust press-values is drawn. +% If k is given as input, the default value is 0, else the default value is one. +% If the input argument 'skew' is equal to one, no plot is +% drawn. +% robpcamcd : If equal to one (default), the whole robpca procedure is run (computation of outlyingness and +% MCD). +% If equal to zero, the program stops after the computation of the outlyingness. The +% robust eigenvectors then correspond with the eigenvectors of the covariance matrix +% of the h observations with smallest outlyingness. This yields the same +% PCA subspace as the full robpca, but not the same eigenvectors and eigenvalues. +% skew : If equal to zero the regular robpca is run. If equal to +% one, the adjusted robpca algorithm for skewed data is run. +% +% I/O: result=robpca(x,'k',k,'kmax',10,'alpha',0.75,'h',h,'mcd',1,'plots',1,'labsd',3,'labod',3,'classic',0); +% The user should only give the input arguments that have to change their default value. +% The name of the input arguments needs to be followed by their value. +% The order of the input arguments is of no importance. +% +% Examples: +% result=robpca(x,'k',3,'alpha',0.65,'plots',0) +% result=robpca(x,'alpha',0.80,'kmax',15,'labsd',5) +% +% The output of ROBPCA is a structure containing +% +% result.P : Robust loadings (eigenvectors) +% result.L : Robust eigenvalues +% result.M : Robust center of the data +% result.T : Robust scores +% result.k : Number of (chosen) principal components +% result.kmax : Maximal number of principal components +% result.alpha : see interpretation in the list of input arguments +% result.h : The quantile h used throughout the algorithm +% result.Hsubsets : A structure that contains H0, H1 and Hfreq: +% H0 : The h-subset that contains the h points with the smallest outlyingness. +% H1 : The optimal h-subset of mcdcov. +% Hfreq : The subset of h points which are the most frequently selected during the mcdcov +% algorithm. +% result.sd : Robust score distances within the robust PCA subspace +% result.od : Orthogonal distances to the robust PCA subspace +% result.cutoff : Cutoff values for the robust score and orthogonal distances +% result.flag : The observations whose score distance is larger than result.cutoff.sd (==> result.flag.sd) +% or whose orthogonal distance is larger than result.cutoff.od (==> result.flag.od) +% can be considered as outliers and receive a flag equal to zero (result.flag.all). +% The regular observations receive a flag 1. +% result.class : 'ROBPCA' +% result.classic : If the input argument 'classic' is equal to one, this structure +% contains results of the classical PCA analysis (see also cpca.m). +% +% Short description of the method: +% +% Let n denote the number of observations, and p the number of original variables, +% then ROBPCA finds a robust center (p x 1) of the data M and a loading matrix P which +% is (p x k) dimensional. Its columns are orthogonal and define a new coordinate +% system. The scores (n x k) are the coordinates of the centered observations with +% respect to the loadings: T=(X-M)*P. +% Note that ROBPCA also yields a robust covariance matrix (often singular) which +% can be computed as +% cov=out.P*out.L*out.P' +% +% To select the number of principal components, it is useful to look at the scree plot which +% shows the eigenvalues, and the press curve which displays a weighted sum of the squared +% cross-validated orthogonal distances. +% The outlier map visualizes the observations by plotting their orthogonal +% distance to the robust PCA subspace versus their robust distances +% within the PCA subspace. This allows to classify the data points into 4 types: +% regular observations, good leverage points, bad leverage points and +% orthogonal outliers. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust +% +% Written by Mia Hubert, Sabine Verboven, Karlien Vanden Branden, Sanne Engelen, Tim Verdonck +% Last Update: 17/06/2003, 03/07/2006, 31/07/2007 +% Last Revision: 27/03/2008, 09/06/2008 + +% +% initialization with defaults +% +data=x; +[n,p]=size(data); + +% First Step: classical PCA on data +if n < p + [P1,T1,L1,r,Xc,clm]=kernelEVD(data); +else + [P1,T1,L1,r,Xc,clm]=classSVD(data); +end +% dim(P1): p x r + +if r==0 + error('All data points collapse!') +end + +niter=100; +counter=1; +kmax=min([10,floor(n/2),r]); +k=0; +alfa=0.75; +h=min(floor(2*floor((n+kmax+1)/2)-n+2*(n-floor((n+kmax+1)/2))*alfa),n); +labsd=3; +labod=3; +plots=1; +scree = 1; +press = 1; +mcd=1; % user wants the mcd approach (in case n>>p) +robpcamcd = 1; +cutoff = 0.975; +skew=0; +% default is a structure needed for input checking +default=struct('alpha',alfa,'h',h,'labsd',labsd,'labod',labod,... + 'k',k,'plots',plots,'kmax',kmax,'mcd',mcd,'classic',0,'scree',scree,'press',press,'robpcamcd',robpcamcd,'cutoff',cutoff,'skew',0); +list=fieldnames(default); +options=default; %input by user +IN=length(list); +i=1; +% +if nargin==2 + error('Incorrect number of input arguments!') +end +dummy = 0; %Assume we didn't get h or alpha, unless we find it below. +if nargin>2 + % + %placing inputfields in array of strings + % + for j=1:nargin-2 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + dummy=sum(strcmp(chklist,'h')+2*strcmp(chklist,'alpha')); %checking if h and alpha are provided both or not + switch dummy + case 0 % Take on default values + options.alpha=alfa; + if any(strcmp(chklist,'kmax')) + for j=1:nargin-2 % searching the index of the accompanying field + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp('kmax',varargin{j}) + I=j; + end + end + end + options=setfield(options,'kmax',varargin{I+1}); + kmax = max(min([floor(options.kmax),floor(n/2),r]),1); %acceptable kmax + options.h=min(floor(2*floor((n+kmax+1)/2)-n+2*(n-floor((n+kmax+1)/2))*alfa),n); %depends on kmax, so if kmax is given by user, get it first! + else %kmax is not given by user + options.h=h; + end + case 3 + error('Both inputarguments alpha and h are provided. Only one is required.') + end + + % + % Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) % in case of similarity + for j=1:nargin-2 % searching the index of the accompanying field + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end + options.h=floor(options.h); + options.kmax=floor(options.kmax); + options.k=floor(options.k); + kmax=max(min([options.kmax,floor(n/2),r]),1); + labod=max(0,min(floor(options.labod),n)); + labsd=max(0,min(floor(options.labsd),n)); + k=options.k; + + if k<0 + k=0; + elseif k > kmax + k=kmax; + mess=sprintf(['Attention (robpca.m): The number of principal components, k = ',num2str(options.k)... + ,'\n is larger than kmax= ',num2str(kmax),'; k is set to ',num2str(kmax)]); + disp(mess) + end + if dummy==1 % checking input variable h + options.alpha=options.h/n; + if k==0 + if options.h < floor((n+kmax+1)/2 ) + options.h=floor((n+kmax+1)/2); + options.alpha=options.h/n; + mess=sprintf(['Attention (robpca.m): h should be larger than (n+kmax+1)/2.\n',... + 'It is set to its minimum value ',num2str(options.h)]); + disp(mess) + end + else + if options.h < floor((n+k+1)/2) + options.h=floor((n+k+1)/2); + options.alpha=options.h/n; + mess=sprintf(['Attention (robpca.m): h should be larger than (n+k+1)/2.\n',... + 'It is set to its minimum value ',num2str(options.h)]); + disp(mess) + end + end + if options.h > n + options.alpha=0.75; + if k==0 + options.h=floor(2*floor((n+kmax+1)/2)-n+2*(n-floor((n+kmax+1)/2))*options.alpha); + else + options.h=floor(2*floor((n+k+1)/2)-n+2*(n-floor((n+k+1)/2))*options.alpha); + end + mess=sprintf(['Attention (robpca.m): h should be smaller than n. \n',... + 'It is set to its default value ',num2str(options.h)]); + disp(mess) + end + elseif dummy==2 %checking input variable alpha + if options.alpha < 0.5 + options.alpha=0.5; + mess=sprintf(['Attention (robpca.m): Alpha should be larger than 0.5.\n',... + 'It is set to 0.5.']); + disp(mess) + end + if options.alpha > 1 + options.alpha=0.75; + mess=sprintf(['Attention (robpca.m): Alpha should be smaller than 1. \n',... + 'It is set to 0.75.']); + disp(mess) + end + if k==0 + options.h=floor(2*floor((n+kmax+1)/2)-n+2*(n-floor((n+kmax+1)/2))*options.alpha); + else + options.h=floor(2*floor((n+k+1)/2)-n+2*(n-floor((n+k+1)/2))*options.alpha); + end + end + alfa=options.alpha; + dummyh = strcmp(chklist,'h'); + dummykmax = strcmp(chklist,'kmax'); + % if all(dummyh == 0) & any(dummykmax) & k==0 + % h = min(floor(2*floor((n+kmax+1)/2)-n+2*(n-floor((n+kmax+1)/2))*alfa),n); + % end + if all(dummyh == 0)&& any(dummykmax) %kmax was given by the user + if k==0 + options.h=floor(2*floor((n+kmax+1)/2)-n+2*(n-floor((n+kmax+1)/2))*options.alpha); + else + options.h=floor(2*floor((n+k+1)/2)-n+2*(n-floor((n+k+1)/2))*options.alpha); + end + elseif all(dummyh == 0) && ~any(dummykmax) %kmax is the default value + if k==0 + options.h=floor(2*floor((n+kmax+1)/2)-n+2*(n-floor((n+kmax+1)/2))*options.alpha); + else + options.h=floor(2*floor((n+k+1)/2)-n+2*(n-floor((n+k+1)/2))*options.alpha); + end + end + h=options.h; + dummyscree = strcmp(chklist,'scree'); + dummypress = strcmp(chklist,'press'); + if all(dummyscree == 0) + if k~=0 + options.scree = 0; + end + end + if all(dummypress == 0) + if k~=0 + options.press = 0; + end + end + scree = options.scree; + press = options.press; + labsd=floor(max(0,min(options.labsd,n))); + labod=floor(max(0,min(options.labod,n))); + plots=options.plots; + mcd=options.mcd; + robpcamcd = options.robpcamcd; + cutoff = options.cutoff; + skew = options.skew; + if skew==1 + press=0; %press curve not available for skewed data + end +end +% +% MAIN PART +% +X=T1; +center=clm; +rot=P1; +% Depending on n and p, perform MCD or ROBPCA: +% p << n => MCD +p1=size(X,2); +if p1<=min(floor(n/5),kmax) && mcd && (skew==0) + options.h=h; + [res,raw]=mcdcov(X,'h',h,'plots',0); + [U,S,P]=svd(res.cov,0); + L=diag(S); + if k~=0 + options.k=min(k,p1); + else + bdwidth=5; + topbdwidth=30; + set(0,'Units','pixels'); + scnsize=get(0,'ScreenSize'); + pos1=[bdwidth, 1/3*scnsize(4)+bdwidth, scnsize(3)/2-2*bdwidth, scnsize(4)/2-(topbdwidth+bdwidth)]; + pos2=[pos1(1)+scnsize(3)/2, pos1(2), pos1(3), pos1(4)]; + if press == 1 + outcvMcd = cvMcd(X,p1,res,h); + figure('Position',pos1) + set(gcf,'Name', 'PRESS curve','NumberTitle', 'off'); + plot(1:p1,outcvMcd.press,'o-') + title('MCD') + xlabel('number of LV') + ylabel('R-PRESS') + end + if scree == 1 + figure('Position',pos2) + screeplot(L,'MCD'); + end + if (scree == 1) || (press == 1) + cumperc = cumsum(L)./sum(L); + disp(['The cumulative percentage of variance explained by the first ',num2str(kmax),' components is:']); + disp(num2str(cumperc')); + disp(['How many principal components would you like to retain? Max = ',num2str(kmax),'. ']); + k=input(''); + end + % to close the figures. + if scree == 1 + close + end + if press == 1 + close + end + end + options.k = k; + T=(X-repmat(res.center,size(X,1),1))*U; + out.M=center+res.center*rot'; + out.L=L(1:options.k)'; + out.P=rot*U(:,1:options.k); + out.T=T(:,1:options.k); + out.h=h; + out.k=options.k; + out.alpha=alfa; + out.Hsubsets.H0 = res.Hsubsets.Hopt; + out.Hsubsets.H1 = []; + out.Hsubsets.Hfreq = res.Hsubsets.Hfreq; + out.skew=skew; +else + % p > n => ROBPCA + niter=100; + seed=0; + if h~=n + if skew==0 + B=twopoints(T1,250,seed); %n*ri + for i=1:size(B,1) + Bnorm(i)=norm(B(i,:),2); + end + Bnormr=Bnorm(Bnorm > 1.e-12); %ndirect*1 + B=B(Bnorm > 1.e-12,:); %ndirect*n + A=diag(1./Bnormr)*B; %ndirect*n + %projected points in columns + Y=T1*A';%n*ndirect + m=length(Bnormr); + Z=zeros(n,m); + for i=1:m + [tmcdi,smcdi,weights]=unimcd(Y(:,i),h); + if smcdi<1.e-12 + r2=rank(data(weights,:)); + if r2==1 + error(['At least ',num2str(sum(weights)),' obervations are identical.']); + end + else + Z(:,i)=abs(Y(:,i)-tmcdi)/smcdi; + end + end + d=max(Z,[],2); + else %adjusted robpca for skewed data + outAO=adjustedoutlyingness(T1,'ndir',min(250*p,2500)); + d=outAO.adjout; + end + [ds,is]=sort(d); + Xh=T1(is(1:h),:); % Xh contains h (good) points out of Xcentr + [P2,T2,L2,r2,Xm,clmX]=classSVD(Xh); + out.Hsubsets.H0 = is(1:h); + Tn=(T1-repmat(clmX,n,1))*P2; + else + P2=eye(r); + Tn=T1; + L2=L1; + r2=r; + out.Hsubsets.H0=1:n; + Xm=T1; + clmX=zeros(1,size(T1,2)); + end + + %dim(P2) = r x r2 + L=L2; + kmax=min(r2,kmax); + + % choice of k: + %------------- + bdwidth=5; + topbdwidth=30; + set(0,'Units','pixels'); + scnsize=get(0,'ScreenSize'); + pos1=[bdwidth, 1/3*scnsize(4)+bdwidth, scnsize(3)/2-2*bdwidth, scnsize(4)/2-(topbdwidth+bdwidth)]; + pos2=[pos1(1)+scnsize(3)/2, pos1(2), pos1(3), pos1(4)]; + + if press == 1 + disp('The robust press curve based on cross-validation is now computed.') + outprMCDkmax = projectMCD(Tn,L,kmax,h,niter,rot,P1,P2,center,cutoff); + if size(out.Hsubsets.H0,2)==1 + out.Hsubsets.H0=out.Hsubsets.H0'; + end + outprMCDkmax.Hsubsets.H0 = out.Hsubsets.H0; + outpress = cvRobpca(data,kmax,outprMCDkmax,0,h); + figure('Position',pos1) + set(gcf,'Name', 'PRESS curve','NumberTitle', 'off'); + plot(1:kmax,outpress.press,'o-') + title('ROBPCA') + xlabel('Number of LV') + ylabel('R-PRESS') + else + if size(out.Hsubsets.H0,2)==1 + out.Hsubsets.H0=out.Hsubsets.H0'; + end + end + + if scree == 1 + figure('Position',pos2) + screeplot(L(1:kmax),'ROBPCA') + end + + if (scree == 1)||(press == 1) + cumperc = (cumsum(L(1:kmax))./sum(L))'; + disp(['The cumulative percentage of variance explained by the first ',num2str(kmax),' components is:']); + disp(num2str(cumperc)); + disp(['How many principal components would you like to retain? Max = ',num2str(kmax),'.']); + k=input(''); + k=max(min(min(r2,k),kmax),1); + % we compute again the robpca results for a specific k value. alpha + % and h can change again, because until now they were based on the kmax + % value. + if dummy == 2 + options.h=floor(2*floor((n+k+1)/2)-n+2*(n-floor((n+k+1)/2))*alfa); + %if dummy == 1 no changes needed + elseif dummy~=1 + options.h=floor(2*floor((n+k+1)/2)-n+2*(n-floor((n+k+1)/2))*options.alpha); + end + h=options.h; + % to close the figures. + if scree == 1 + close + end + if press == 1 + close + end + else + k=min(min(r2,k),kmax); + end + + if k~=r % extra reweighting step + XRc=T1-repmat(clmX,n,1); + Xtilde=XRc*P2(:,1:k)*P2(:,1:k)'; + Rdiff = XRc-Xtilde; + for i=1:n + odh(i,1)=norm(Rdiff(i,:)); + end + if skew==0 + [m,s]=unimcd(odh.^(2/3),h); + cutoffodh = sqrt(norminv(cutoff,m,s).^3); + else %adjusted robpca for skewed data + mcodh=mc(odh); + if mcodh>0 + cutoffodh = prctile(odh,75)+1.5*exp(3*mcodh)*iqr(odh); + else + cutoffodh = prctile(odh,75)+1.5*iqr(odh); + end + ttup = sort(-odh(odhrh + k = rh; + end + end + center=center+clmX*rot'; + rot=rot*P2(:,1:k); + Tn=(T1-repmat(clmX,n,1))*P2; + + % if only the subspace is important, not the PC themselves: do not + % perform MCD anymore. + if ~robpcamcd + out.P = rot; %=P1*P2(:,1:k); + out.T = Tn(:,1:k); + out.M = center; + out.L = Lh; + out.k = k; + out.h = h; + out.alpha = alfa; + out.kmax=kmax; + out.skew=skew; + end + + % projection, mcd + %----------------- + if skew==0 + outpr = projectMCD(Tn,L,k,h,niter,rot,P1,P2,center,cutoff); + else %adjusted robpca for skewed data + outpr = projectAO(Tn,k,h,rot,center); + end + out.T = outpr.T; + out.P = outpr.P; + out.M = outpr.M; + out.L = outpr.L; + out.k = k; + out.kmax=kmax; + out.h = h; + out.alpha = alfa; + out.skew = skew; + if skew==0 + out.Hsubsets.H1 = outpr.Hsubsets.H1; + out.Hsubsets.Hfreq = outpr.Hsubsets.Hfreq; + else + out.AO=outpr.AO; + out.cutoff.AO=outpr.cutoff.AO; + end +end + +% Classical analysis +if options.classic==1 + out.classic.P=P1(:,1:out.k); + out.classic.L=L1(1:out.k)'; + out.classic.M=clm; + out.classic.T=T1(:,1:out.k); + out.classic.k=out.k; + out.classic.Xc=Xc; +end + +outpr = out; + +% Calculation of the distances, flags +%------------------------------------- + +if options.classic == 1 + outDist = CompDist(data,r,outpr,cutoff,robpcamcd,out.classic); +else + outDist = CompDist(data,r,outpr,cutoff,robpcamcd); +end + +out.sd = outDist.sd; +out.cutoff.sd = outDist.cutoff.sd; +out.od = outDist.od; +out.cutoff.od = outDist.cutoff.od; +out.flag = outDist.flag; +out.class = outDist.class; +out.classic = outDist.classic; +if options.classic == 1 + out.classic.sd = outDist.classic.sd; + out.classic.od = outDist.classic.od; + out.classic.cutoff.sd = outDist.classic.cutoff.sd; + out.classic.cutoff.od = outDist.classic.cutoff.od; + out.classic.class = outDist.classic.class; + out.classic.flag = outDist.classic.flag; +end + + +result=struct('P',{out.P},'L',{out.L},'M',{out.M},'T',{out.T},'k',{out.k},'kmax',{kmax},'alpha',{out.alpha},... + 'h',{out.h},'Hsubsets',{out.Hsubsets},'sd', {out.sd},'od',{out.od},'cutoff',{out.cutoff},'flag',out.flag',... + 'class',{out.class},'classic',{out.classic}); + +% Plots +try + if plots && options.classic + makeplot(result,'classic',1,'labsd',labsd,'labod',labod) + elseif plots + makeplot(result,'labsd',labsd,'labod',labod) + end +catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu +end + +%-------------------------------------------------------------------------- +function outprMCD = projectMCD(Tn,L,k,h,niter,rot,P1,P2,center,cutoff) + +% this function performs the last part of ROBPCA when k is determined. +% input : +% Tn : the projected data +% L : the matrix of the eigenvalues +% k : the number of components +% h : lower bound for regular observations +% niter : the number of iterations +% rot : the rotation matrix +% P1, P2: the different eigenvector matrices after each transformation +% center : the classical center of the data + +X2=Tn(:,1:k); +n = size(X2,1); +rot=rot(:,1:k); +% first apply c-step with h points from first step,i.e. those that +% determine the covariance matrix after the c-steps have converged. +mah=mahalanobis(X2,zeros(size(X2,2),1),'cov',L(1:k)); +oldobj=prod(L(1:k)); +P4=eye(k); +korig=k; +for j=1:niter + [mahs,is]=sort(mah); + Xh=X2(is(1:h),:); + [P,T,L,r3,Xm,clmX]=classSVD(Xh); + obj=prod(L); + X2=(X2-repmat(clmX,n,1))*P; + center=center+clmX*rot'; + rot=rot*P; + mah=mahalanobis(X2,zeros(size(X2,2),1),'cov',diag(L)); + P4=P4*P; + if ((r3==k) && (abs(oldobj-obj) < 1.e-12)) + break; + else + oldobj=obj; + j=j+1; + if r3 < k + j=1; + k=r3; + end + end +end +% dim(P4): k x k0 with k0 <= k but denoted as k +% dim X2: n x k0 +% perform mcdcov on X2 +[zres,zraw]= mcdcov(X2,'plots',0,'ntrial',250,'h',h); +out.resMCD = zres; +if zraw.objective < obj + z = zres; + out.Hsubsets.H1 = zres.Hsubsets.Hopt; +else + sortmah = sort(mah); + if h==n + factor=1; + else + factor = sortmah(h)/chi2inv(h/n,k); + end + mah = mah/factor; + weights = mah <= chi2inv(cutoff,k); + [center_noMCD,cov_noMCD] = weightmecov(X2,weights); + mah = mahalanobis(X2,center_noMCD,'cov',cov_noMCD); + z.flag = (mah <= chi2inv(cutoff,k)); + z.center = center_noMCD; + z.cov = cov_noMCD; + out.Hsubsets.H1 = is(1:h); +end +covf=z.cov; +centerf=z.center; +[P6,L]=eig(covf); +[L,I]=greatsort(diag(real(L))); +P6=P6(:,I); +out.T=(X2-repmat(centerf,n,1))*P6; +P=P1*P2; +out.P=P(:,1:korig)*P4*P6; +centerfp=center+centerf*rot'; +out.M=centerfp; +out.L=L'; +out.k=k; +out.h=h; + +% creation of Hfreq +out.Hsubsets.Hfreq = zres.Hsubsets.Hfreq(1:h); + +outprMCD = out; +%---------------------------------------------------------------------------- +function outprAO = projectAO(Tn,k,h,rot,center) + +% this function performs the last part of ROBPCA when k is determined. +% input : +% Tn : the projected data +% k : the number of components +% h : lower bound for regular observations +% rot : the rotation matrix +% center : the classical center of the data + +seed=0; +X2=Tn(:,1:k); +[n,p]=size(X2); +outAO=adjustedoutlyingness(X2,'ndir',min(250*p,2500)); +AO=outAO.adjout; +cutoffAO=outAO.cutoff; +indexset = find(AO<=cutoffAO)'; +%SVD +[P6,T6,L6,r6,Xm6,clmX6]=classSVD(X2(indexset,:)); +out.T = (X2-repmat(clmX6,n,1))*P6; +out.P = rot*P6; +out.M = center+clmX6*rot'; +out.L = L6; +out.k = k; +out.h = h; +out.AO=AO; +out.cutoff.AO=cutoffAO; +outprAO=out; + +%-------------------------------------------------------------------------------- +function outDist = CompDist(data,r,out,cutoff,robpcamcd,classic) + +% Calculates the distances. +% input: data : the original data +% r : the rank of the data +% out is a structure that contains the results of the PCA. +% classic: an optional structure: +% classic.P1 +% classic.T1 +% classic.L1 +% classic.clm +% classic.Xc + +if nargin < 6 + options.classic = 0; +else + options.classic = 1; +end + +n = size(data,1); +p = size(data,2); +k = out.k; +skew=out.skew; + +% Computing distances +% Robust score distances in robust PCA subspace +if robpcamcd + if skew==0 + out.sd=sqrt(mahalanobis(out.T,zeros(size(out.T,2),1),'cov',out.L))'; + out.cutoff.sd=sqrt(chi2inv(cutoff,out.k)); + else + out.sd=out.AO; + out.cutoff.sd=out.cutoff.AO; + end +else + out.sd=zeros(n,1); + out.cutoff.sd=0; +end +% Orthogonal distances to robust PCA subspace +XRc=data-repmat(out.M,n,1); +Xtilde=out.T*out.P'; +Rdiff=XRc-Xtilde; +for i=1:n + out.od(i,1)=norm(Rdiff(i,:)); +end +% Robust cutoff-value for the orthogonal distance +if k~=r + if skew==0 + [m,s]=unimcd(out.od.^(2/3),out.h); + out.cutoff.od = sqrt(norminv(cutoff,m,s).^3); + else + mcod=mc(out.od); + if mcod>0 + out.cutoff.od = prctile(out.od,75)+1.5*exp(3*mcod)*iqr(out.od); + else + out.cutoff.od = prctile(out.od,75)+1.5*iqr(out.od); + end + ttup = sort(-out.od(out.od1 + % + % placing inputfields in array of strings + % + for j=1:nargin-1 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % + % Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) % in case of similarity + for j=1:nargin-1 % searching the index of the accompanying field + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end +end +sca=options.sca; +loc=options.loc; +iterloc=options.iterloc; +itersca=options.itersca; +h=options.h; + +if ~ismember(sca,{'qnm','madc','adm','mscalelogist','unimcd'}) + error('No appropriate scale estimator was given.') +end +if ~ismember(loc,{'median','hl','mloclogist','mlochuber','unimcd'}) + error('No appropriate location estimator was given.') +end + +if strmatch(loc,'unimcd')&strmatch(sca,'unimcd') + [out.loc,out.sca]=unimcd(x,h); +else + switch loc + case {'hl','median'} + out.loc=feval(loc,x); + case 'unimcd' + out.loc=feval(loc,x,h); + case 'mlochuber' + out.loc=feval(loc,x,'k',iterloc); + case 'mloclogist' + out.loc=feval(loc,x,'k',iterloc); + end + switch sca + case {'adm', 'qnm','madc'} + out.sca=feval(sca,x); + case 'unimcd' + [l,out.sca]=feval(sca,x,h); + case 'mscalelogist' + out.sca=feval(sca,x,'k',itersca); + end +end +out.xstd=(x-repmat(out.loc,n,1))./repmat(out.sca,n,1); + +result = out; \ No newline at end of file diff --git a/LIBRA/rpcr.m b/LIBRA/rpcr.m new file mode 100644 index 0000000..17bf679 --- /dev/null +++ b/LIBRA/rpcr.m @@ -0,0 +1,464 @@ +function result=rpcr(x,y,varargin) + +%RPCR is a 'Robust Principal Components Regression' method based on ROBPCA. +% It can be applied to both low and high-dimensional predictor variables x, +% and to one or multiple response variables y. It is resistant to outliers +% in the data. First, a robust principal components method is applied to the +% predictor variables x (see robpca.m). Then a robust regression is performed. +% For univariate y, the LTS regression is used (see ltsregres.m). When there are +% several response variables, the MCD-regression method is applied (see mcdregres.m). +% +% The RPCR method is described in: +% Hubert, M., Verboven, S. (2003), +% "A robust PCR method for high-dimensional regressors", +% Journal of Chemometrics, 17, 438-452. +% +% To select the number of components in the regression model, a robust RMSECV (root mean squared +% error of cross validation) curve is drawn, based on a fast algorithm for +% cross-validation. This approach is described in: +% +% Engelen, S., Hubert, M. (2005), +% "Fast model selection for robust calibration methods", +% Analytica Chimica Acta, 544, 219-228. +% +% Required input arguments: +% x : Data matrix of the explanatory variables +% (n observations in rows, p variables in columns) +% y : Data matrix of the response variables +% (n observations in rows, q variables in columns) +% +% Optional input arguments: +% k : Number of principal components to be used in the PCA step +% (default = min(rank(x),kmax)). If k is not specified, +% it can be selected using the option 'rmsecv'. +% kmax : Maximal number of principal components to be used (default = 10). +% If k is provided, kmax does not need to be specified, unless k is larger +% than 10. +% alpha : (1-alpha) measures the fraction of outliers the algorithm should +% resist. Any value between 0.5 and 1 may be specified. (default = 0.75) +% h : (n-h+1) measures the number of observations the algorithm should +% resist. Any value between n/2 and n may be specified. (default = 0.75*n) +% Alpha and h may not both be specified. +% rmsecv : If equal to zero and k is not specified, a robust R-squared curve +% is plotted and an optimal k value can be chosen. (default) +% If equal to one and k is not specified, a robust component selection-curve is plotted and +% an optimal k value can be chosen. This curve computes a combination of +% the robust cross-validated mean squared error and the robust residual +% sum of squares for k=1 to kmax. Rmsecv and k may not both +% be specified. +% rmsep : If equal to one, the robust RMSEP-value (root mean squared error of +% prediction) for the model with k components. +% (default = 0). This value is automatically given if rmsecv = 1. +% intadjust : If equal to one, the intercept adjustment for the +% LTS-regression will be calculated (default = 0). See ltsregres.m for +% details. +% plots : If equal to one, a menu is shown which allows to draw several plots, +% such as a score outlier map and a regression outlier map. (default) +% If the input argument 'classic' is equal to one, the classical +% plots are drawn as well. +% If 'plots' is equal to zero, all plots are suppressed. +% See also makeplot.m +% labsd : The 'labsd' observations with largest score distance are +% labeled on the diagnostic plots. (default = 3) +% labod : The 'labod' observations with largest orthogonal distance are +% labeled on the score outlier map. (default = 3) +% labresd : The 'labresd' observations with largest residual distance are +% labeled on the regression outlier map. (default = 3) +% classic : If equal to one, the classical PCR analysis will be performed as well +% (see also cpcr.m). (default = 0) +% +% +% I/O: result=rpcr(x,y,'k',0,'kmax',10,'alpha',0.75,'h',h,'rmsecv',0,'rmsep',0,... +% 'plots',1,'labsd',3,'labod',3,'labresd',3,'classic',0); +% The user should only give the input arguments that have to change their default value. +% The name of the input arguments needs to be followed by their value. +% The order of the input arguments is of no importance. +% +% Example: result=rpcr(x,y,'alpha',0.65,'k',5,'plots',0,'classic',1); +% result=rpcr(x,y,'kmax',5,'labresd',7); +% +% The output of RPCR is a structure containing: +% +% result.slope : Robust slope +% result.int : Robust intercept +% result.fitted : Robust prediction vector +% result.res : Robust residuals +% result.cov : Estimated variance-covariance matrix of the residuals +% result.rsquared : Robust R-squared value for the optimal k. +% result.rcs : Robust Component Selection Criterion: +% This is a matrix with kmax columns. The first row contains the approximate +% R-squared values (for k=1,...,kmax) and the second row the square root of the weighted +% residuals sum of squares. This is equal to the RCS-value with +% gamma = 0 (see Engelen and Hubert (2005) for the definition). +% If the input argument rmsecv = 1, the third row contains the RCS-value sfor +% gamma = 0.5 and the fourth for gamma = 1. The last one is equal to the robust +% cross-validated RMSE values. +% Note that all the entries in this matrix depend on the choice of kmax. +% result.rmsep : Robust RMSEP value +% result.k : Number of principal components +% result.h : The quantile h used throughout the algorithm +% result.sd : Robust score distances within the robust PCA subspace +% result.od : Robust orthogonal distances to the robust PCA subspace +% result.resd : Residual distances (when there are several response variables). +% If univariate regression is performed, it contains the standardized residuals. +% result.cutoff : Cutoff values for the score, orthogonal and residual distances +% result.flag : The observations whose score distance is larger than +% 'result.cutoff.sd' receive a flag 'result.flag.sd' equal +% to zero (good leverage points). Otherwise 'result.flag.sd' +% is equal to one. +% The components 'result.flag.od' and 'result.flag.resd' are +% defined analogously, and determine the orthogonal outliers, +% resp. the bad leverage points/vertical outliers. +% The observations with 'result.flag.od' and 'result.flag.resd' +% equal to zero, can be considered as calibration outliers and receive +% 'result.flag.all' equal to zero. The regular observations and the good leverage +% points have 'result.flag.all' equal to one. +% result.class : 'RPCR' +% result.classic : If the input argument 'classic' is equal to one, this structure +% contains results of the classical PCR analysis (see also cpcr.m). +% result.robpca : Full output of the robust PCA analysis (see robpca.m) +% result.lts : If there is one response variable: full output of the LTS regression. +% (see ltsregres.m) +% result.mcdreg : If there are several response variables: full output of the MCD regression. +% (see mcdregres.m) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Sabine Verboven +% Created on: 31/10/2000 +% Last Update: 09/04/2004, 03/07/2006 +% Last Revision: 04/08/2006 + +% +% checking input +% +if rem(nargin,2)~=0 + error('Number of inputarguments must be even!'); +end +% +% initialization with defaults +% +counter=1; +[n,p]=size(x); +r=rank(x); +[n2,q]=size(y); +if n~=n2 + error('The response variables and the predictor variables have a different number of observations.') +end +kmax=min([10,floor(n/2),r]); +alfa=0.75; +k=0; +h=floor(2*floor((n+kmax+2)/2)-n+2*(n-floor((n+kmax+2)/2))*alfa); +default=struct('intadjust', 0, 'alpha',alfa,'h',h,'k',k,'kmax',kmax,'plots',1,... + 'rmsecv',0,'classic',0,'labsd',3,'labod',3,'labresd',3,'rmsep',0); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +% +if nargin>2 + % + % placing inputfields in array of strings + % + for j=1:nargin-2 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + dummy=sum(strcmp(chklist,'h')+2*strcmp(chklist,'alpha')); + switch dummy + case 0 % defaultvalues should be taken + options.alpha=alfa; + options.h=h; + case 3 + error('Both inputarguments alpha and h are provided. Only one is required.') + end + % + % Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) % in case of similarity + for j=1:nargin-2 % searching the index of the accompanying field + if rem(j,2)~=0 % fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end + options.h=floor(options.h); + options.kmax=floor(options.kmax); + options.k=floor(options.k); + options.labsd=max(0,min(floor(options.labsd),n)); + options.labod=max(0,min(floor(options.labod),n)); + options.labresd=max(0,min(floor(options.labresd),n)); + kmax=min([options.kmax,floor(n/2),r]); + dummyh = strcmp(chklist,'h'); + dummykmax = strcmp(chklist,'kmax'); + if all(dummyh == 0) && any(dummykmax) + h = floor(2*floor((n+kmax+1)/2)-n+2*(n-floor((n+kmax+1)/2))*alfa); + end + if options.k < 0 + options.k=0; + elseif options.k > kmax + mess=sprintf(['Attention (rpcr.m): The required number of principal components, k = ',num2str(options.k)... + ,'\n is larger than kmax= ',num2str(kmax),'; k is set to ',num2str(kmax)]); + disp(mess) + options.k=kmax; + end + if dummy==1% checking inputvariable h + if options.h-floor(options.h)~=0 + mess=sprintf(['Attention (rpcr.m): h must be an integer. \n']); + disp(mess) + end + if options.k==0 + if options.hn + options.alpha=0.75; + if options.k==0 + options.h=floor(2*floor((n+kmax+2)/2)-n+2*(n-floor((n+kmax+2)/2))*options.alpha); + else + options.h=floor(2*floor((n+options.k+1)/2)-n+2*(n-floor((n+options.k+1)/2))*options.alpha);%k+1 ipv k? + end + mess=sprintf(['Attention (rpcr.m): h should be smaller than n. \n',... + 'It is set to its default value ',num2str(options.h)]); + disp(mess) + end + elseif dummy==2 + if options.alpha < 0.5 + options.alpha=0.5; + mess=sprintf(['Attention (rpcr.m): Alpha should be larger than 0.5. \n',... + 'It is set to 0.5.']); + disp(mess) + end + if options.alpha > 1 + options.alpha=0.75; + mess=sprintf(['Attention (rpcr.m): Alpha should be smaller than 1.\n',... + 'It is set to 0.75.']); + disp(mess) + end + if options.k==0 + options.h=floor(2*floor((n+kmax+2)/2)-n+2*(n-floor((n+kmax+2)/2))*options.alpha); + else + options.h=floor(2*floor((n+options.k+1)/2)-n+2*(n-floor((n+options.k+1)/2))*options.alpha);%k+1 ipv k? + end + end +end +% +% +% MAIN PART +% +% If y is univariate, perform LTS regression, else MCD regression +if q==1 + options.method='lts'; +else + options.method='mcd'; +end +% Default value for the number of latent variables k +if n calculate R2 + [R2,final]=rsquared(x,y,kmax,'RPCR',options.h); + rss = final.rss; + options.k=final.k; + outrobpca=robpca(x,'h',options.h,'k',options.k,'kmax',kmax,'plots',0,'classic',options.classic); + options.k=outrobpca.k; + final=regression(x,y,options.k,outrobpca,options); + if options.rmsep==1 % the rmsep is asked, without prior knowledge + rmse=rrmse(x,y,options.h,kmax,'RPCR',0,options.k); + final.rmsep=rmse.rmsep; + end + final.robpca=outrobpca; + final.R2 = R2; + final.rss = rss; + else % RMSE with cross validation + crosscv=rrmse(x,y,options.h,kmax,'RPCR'); + options.k=crosscv.k; + rmsecv=crosscv.rmsecv; + pred=rrmse(x,y,options.h,kmax,'RPCR',0,options.k,crosscv.weight,crosscv.res); %calculate rmsep with rmsecv weights and residuals + outrobpca=robpca(x,'h',options.h,'k',options.k,'kmax',kmax,'plots',0,'classic',options.classic); + options.k=outrobpca.k; + final=regression(x,y,options.k,outrobpca,options); + final.robpca=outrobpca; + final.rmsecv=rmsecv; + final.rmsep=pred.rmsep; + final.R2 = crosscv.R2; + final.rss = crosscv.rss; + end +else % optimal number of latent variables is given by user + if options.rmsecv + error(['Both RMSECV and k were given.', ... + 'Please rerun your analysis with one of these inputs. (see help file)']) + end + kfixed = 1; + k=min(r,min(options.k,kmax)); + options.k=k; + outrobpca=robpca(x,'h',options.h,'k',options.k,'kmax',kmax,'plots',0,'classic',options.classic); + options.k=outrobpca.k; + final=regression(x,y,options.k,outrobpca,options); + if options.rmsep==1 % the rmsep is asked, without prior knowledge + rmse=rrmse(x,y,options.h,kmax,'RPCR',0,options.k); + final.rmsep=rmse.rmsep; + end + final.robpca=outrobpca; +end + +final.h=options.h; +final.k=options.k; +final.alpha=options.alpha; + +% scores-distances +final.sd=final.robpca.sd; +quan=chi2inv(0.975,final.k); +final.cutoff.sd=quan^0.5; + +% orthogonal distances +final.od=final.robpca.od; +final.cutoff.od=final.robpca.cutoff.od; +final.x=x; +final.y=y; + +if options.classic==1 + final.classic=cpcr(x,y,'k',final.k,'plots',0); +else + final.classic=0; +end + +if strmatch(options.method,'mcd','exact') + final.resd=final.mcdreg.resd; + final.cutoff.resd=final.mcdreg.cutoff.resd; +else + final.resd=final.lts.res/final.lts.scale; + final.cutoff.resd=sqrt(chi2inv(0.975,1)); +end +final.class='RPCR'; + +%Computing flags +final.flag.od=final.od<=final.cutoff.od; +final.flag.resd=abs(final.resd)<=final.cutoff.resd; +final.flag.all=(final.flag.od & final.flag.resd); + +% Assigning output +if kfixed == 0 + final.rcs = [final.R2;sqrt(final.rss)]; +else + final.rcs = 0; +end + +if options.rmsecv~=0 + gammahalf = 0.5*sqrt(final.rss) + 0.5*final.rmsecv; + final.rcs = [final.rcs;gammahalf;final.rmsecv]; + rmsecv_ind = 1; +else + rmsecv_ind = 0; +end +if options.rmsep~=1 && rmsecv_ind == 0 + final.rmsep=0; +end + +if isfield(final,'lts') + result=struct('slope',{final.slope}, 'int',{final.int}, 'fitted',{final.fitted},'res',{final.res},... + 'cov',{final.cov},'rsquared',{final.rsquared},'rcs',{final.rcs},'rmsep',... + {final.rmsep},'k',{final.k},'alpha',{final.alpha},'h',{final.h},'sd', {final.sd},'od',{final.od},'resd',{final.resd},... + 'cutoff',{final.cutoff},'flag',{final.flag},'class',{final.class},'classic',{final.classic},... + 'robpca',{final.robpca},'lts',{final.lts}); +else + result=struct('slope',{final.slope}, 'int',{final.int}, 'fitted',{final.fitted},'res',{final.res},... + 'cov',{final.cov},'rsquared',{final.rsquared},'rcs',{final.rcs},'rmsep',{final.rmsep},... + 'k',{final.k},'alpha',{final.alpha},'h',{final.h},'sd', {final.sd},'od',{final.od},'resd',{final.resd},... + 'cutoff',{final.cutoff},'flag',{final.flag},'class',{final.class},'classic',{final.classic},... + 'robpca',{final.robpca},'mcdreg',{final.mcdreg}); +end +if result.rcs == 0 + result = rmfield(result,'rcs'); +end +if result.rmsep==0 + result=rmfield(result,'rmsep'); +end + +% Plots +try + if options.plots && options.classic + makeplot(result,'classic',1,'labsd',options.labsd,'labod',options.labod,'labresd',options.labresd) + elseif options.plots + makeplot(result,'labsd',options.labsd,'labod',options.labod,'labresd',options.labresd) + end +catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu +end +%--------------------------------- +function out=regression(x,y,d,pre_out,options) + +switch options.method +case 'mcd' % MCD regression for multivariate responses + out.mcdreg=mcdregres(pre_out.T(:,1:d),y,'k',options.k,'h',options.h,'plots',0); + if ~isstruct(out.mcdreg) + return + end + %%coefficients in the original space + out.slope=pre_out.P(:,1:d)*out.mcdreg.slope; + out.int=out.mcdreg.int-pre_out.M*pre_out.P(:,1:d)*out.mcdreg.slope; + out.fitted=[pre_out.T(:,1:d) ones(size(pre_out.T(:,1:d),1),1)]*[out.mcdreg.slope; out.mcdreg.int]; + out.res=y-out.fitted; + out.cov=out.mcdreg.cov; % covariance matrix of residuals (capital sigma) + out.name='Multivariate robust MCD-regression'; + out.rsquared=out.mcdreg.rsquared; +case 'lts' + [out.lts,raw]=ltsregres(pre_out.T(:,1:d),y,'plots',0,'h',options.h,'intadjust',options.intadjust); + %%coefficients in the original space; + out.lts.weights=raw.wt; + out.slope=pre_out.P(:,1:d)*out.lts.slope; + out.int=out.lts.int-pre_out.M*pre_out.P(:,1:d)*out.lts.slope; + out.fitted=[pre_out.T(:,1:d) ones(size(pre_out.T(:,1:d),1),1)]*[out.lts.slope; out.lts.int]; + out.res=y-out.fitted; + out.cov=out.lts.scale.^2; + if out.cov==0 + mess=sprintf(['Attention (rpcr.m): No standardized residuals could be calculated \n',... + 'because their robust scale is zero.']); + disp(mess) + out.stdres=NaN; + else + out.stdres=out.res/out.lts.scale; + end + out.rsquared=out.lts.rsquared; + out.name='Robust LTS regression'; +end + +%----------------------------------------------------------------------------------------- +function quan=quanf(alfa,n,rk) + +quan=floor(2*floor((n+rk+1)/2)-n+2*(n-floor((n+rk+1)/2))*alfa); + + + + + diff --git a/LIBRA/rrmse.m b/LIBRA/rrmse.m new file mode 100644 index 0000000..79bf38b --- /dev/null +++ b/LIBRA/rrmse.m @@ -0,0 +1,140 @@ +function result=rrmse(x,y,h,kmax,attrib,plots,k,weight,res) + +%RRMSE calculates the robust RMSECV and/or the robust RMSEP-value +% for RPCR and RSIMPLS. +% +% The robust RMSECV is described in: +% +% Engelen, S., Hubert, M. (2005), +% "Fast model selection for robust calibration methods", +% Analytica Chimica Acta, 544, 219-228. +% +% Required input arguments: +% x : the regressors +% y : the response variables +% h : the quantile used in RPCR and RSIMPLS. +% kmax : the maximal number of components to be used. +% attrib : the name of the analysis 'RSIMPLS', or 'RPCR' +% +% Optional input arguments: +% plots : 0/1 = no plot / plot (default) of the RMSECV values +% k : the optimal number of components chosen by cross validation. +% if k is different from zero, the RMSEP will be calculated +% (default k=0) +% weight : only needed when calculating RMSEP value +% res : the residuals of each left-out observation for every k. +% +% I/O: result=rrmse(x,y,h,kmax,attrib,plots,k,weight,res); +% +% Example: h=0.75*size(x,1); +% result=rrmse(x,y,h,10,'RPCR'); +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Created by Karlien Vanden Branden on 05-07-2002 +% Revisions by Sabine Verboven, Sanne Engelen +% Last Update on 28-04-2004, 03/07/2006 +% Last Revision date 03-07-2006 + +%%%%%%%%% Initialisation %%%%%%%%%%%%%% +if nargin>=7 + in.pred=1; +else + in=struct(''); +end +if nargin<7 + k=0; +end +if nargin<6 + k=0; + plots=1; +end +if nargin<5 + error('Missing one or more input variables.') +end +if k<0 %preventing negative input of chosen number of PC's + k=0; +end + +[n,p]=size(x); +[n,q]=size(y); + +%%%%%Defining the maximum number of principal components to calculate +count=1; +if q>1 + while (count*q+q+(q*(q+1)/2))<=h + count=count+1; + end +else + while (count+2)<=h + count=count+1; + end +end +ktot=max(1,min(count-1,kmax)); +if ktot ~=kmax + disp(['Warning (rrmse.m): The maximal number of principal components is reduced to ',num2str(ktot)]) +end +%%%%%%%%%%%%%%%% MAIN PART %%%%%%%%%%%%%%%%%%%%%%%% +if (k~=0) && nargin>7 + disp(['The RMSEP value is based on ', num2str(k),' scores.']) + weight=weight(:,k); + res=res(:,(k-1)*q + 1:k*q).^2; + res=repmat(weight,1,q).*res; + vRMSECV2=sum(res,1)/sum(weight); %1xn -> 1x1 + out.rmsep=sqrt(sum(vRMSECV2)/q); +else + % Cross-validation + if k==0 + disp('Cross-validation is now performed.') + else + disp(['The RMSEP value is based on ', num2str(k),' components.']) + end + + resCV = crossvalid(attrib,x,y,ktot,h,k); + out.R2 = resCV.R2; + out.rss = resCV.rss; + + if plots && (k==0) + disp(['The robust RMSECV-values: ', num2str(resCV.rmsecv)]) + figure + set(gcf,'Name', 'Robust Component Selection plot', 'NumberTitle', 'off'); + plot(1:ktot,resCV.rmsecv,'o-'); + hold on + plot(1:ktot,sqrt(0.5*resCV.rmsecv.^2 + 0.5*resCV.rss),'r*--') + plot(1:ktot,sqrt(resCV.rss),'g>-') + xlabel('Number of components'); + set(gca,'XTick',1:1:ktot) + ylabel('RCS value'); + title(attrib) + legend('\gamma = 1 (CV)','\gamma = 0.5','\gamma = 0 (RSS)') + hold off + kout=input(['How many components would you like to retain? ']); + out.k=kout; + else + out.k=k; + end + if k==0 %output needed for the calculation of rmsep + out.weight=resCV.outWeights.weightsk; + out.res=resCV.residu; + out.rmsecv=resCV.rmsecv; + else + out.rmsep=resCV.rmsep; + out.k=k; + end +end + +result = out; +%%%%%%%%%%%%%%%%%%%%%%%%%subfunction%%%%%%%%%%%%%%%%%%%%%% + +function out=crossvalid(attrib,x,y,kmax,h,k) + +switch attrib +case 'RPCR' + out = cvRpcr(x,y,kmax,1,h,k); +case 'RSIMPLS' + out = cvRsimpls(x,y,kmax,1,h,k); +end + + diff --git a/LIBRA/rsimca.m b/LIBRA/rsimca.m new file mode 100644 index 0000000..72126a7 --- /dev/null +++ b/LIBRA/rsimca.m @@ -0,0 +1,610 @@ +function result = rsimca(x,group,varargin) + +%RSIMCA performs a robust version of the SIMCA method. This is a classification +% method on a data matrix x with a known group structure. On each group a +% robust PCA analysis (ROBPCA) is performed. Afterwards a classification +% rule is developped to determine the assignment of new observations. Since RSIMCA +% depends on the ROBPCA method (see robpca.m) it is able to deal with high-dimensional data. +% +% The RSIMCA method is described in: +% K. Vanden Branden and M. Hubert (2005), +% Robust classification in high dimensions based on the SIMCA method, +% Chemometrics and Intelligent Laboratory Systems, 79, 10-21. +% +% Required input arguments: +% x : training data set (matrix of size n by p). +% group : column vector containing the group numbers of the training +% set x. For the group numbers, any strict positive integer is +% allowed. +% +% Optional input arguments: +% alpha : (1-alpha) measures the fraction of outliers (in each group) the algorithm should +% be able to resist. Any value between 0.5 and 1 may be specified. (default = 0.75) +% If k groups are present in the data, per group a different value for alpha may be specified +% (default = alpha = [0.75, ... , 0.75]). +% k : Is a vector with size equal to the number of groups, or otherwise 0. It represents the number +% of components to be retained in each group. (default = 0). +% kmax : Maximal number of principal components to compute (default = 10). +% If k is provided, kmax does not need to be specified, unless k is larger +% than 10. +% scree : If equal to one, a scree plot is drawn for each group. (default = 0). +% press : If equal to one, a plot of robust press-values is drawn for each group. +% If k is given as input, the default value is 0, else the default value is one. +% method : Indicates which classification rule is wanted. `1' results in rule (R1) +% based on the scaled orthogonal and score distances. `2' corresponds with +% (R2) based on the squared scaled orthogonal and score distances. Default is 2. +% gamma : Represents the value(s) used in the classification rule: weight gamma is given to the od's, +% weight (1-gamma) to the sd's. (default = 0.5). +% misclassif : String which indicates how to estimate the probability of +% misclassification. It can be based on the training data ('training'), +% a validation set ('valid'), or cross-validation ('cv'). Default is 'training'. +% membershipprob : Vector which contains the membership probability of each +% group (sorted by increasing group number). These values are used to +% obtain the total misclassification percentage. If no priors are given, they are +% estimated as the proportions of regular observations in the training set. +% valid : If misclassif was set to 'valid', this field should contain +% the validation set (a matrix of size m by p). +% groupvalid : If misclassif was set to 'valid', this field should contain the group numbers +% of the validation set (a column vector). +% predictset : Contains a new data set (a matrix of size mp by p) from which the +% class memberships are unknown and should be predicted. +% plots : If equal to 1, one figure is created with the training data and the +% boundaries for each group. This plot is +% only available for trivariate (or smaller) data sets. For technical reasons, a maximum +% of 6 groups is allowed. Default is one. +% plotsrobpca : If equal to one, a robust score diagnostic plot is +% drawn (default). If the input argument 'classic' is equal to one, +% the classical plots are drawn as well. +% If 'plots' is equal to zero, this plot is suppressed. +% See also makeplot.m +% labsd : The 'labsd' observations with largest score distance are +% labeled on the diagnostic plot. (default = 3) +% labod : The 'labod' observations with largest orthogonal distance are +% labeled on the diagnostic plot. default = 3) +% mcd : If equal to one: when the number of variables is sufficiently small, +% the loadings are computed as the eigenvectors of the MCD covariance matrix, +% hence the function 'mcdcov.m' is automatically called. The number of +% principal components is then taken as k = rank(x). (default) +% If equal to zero, the robpca algorithm is always applied. +% classic : If equal to one, the classical SIMCA analysis will be performed +% (see also csimca.m). (default = 0) +% compare : If equal to one, the classical SIMCA analysis will be performed +% with the same weights and the same priors as the robust analysis +% has been performed. This is especially useful to compare the robust +% and classical result on the same data with the same priors. (default = 0) +% +% +% I/O: result=rsimca(x,group,'alpha',0.5,'method',1,'misclassif','training',... +% 'membershipprob',proportions,'valid',y,'groupvalid',groupy,'plots',0,'classic',0); +% +% The user should only give the input arguments that have to change their default value. +% The name of the input arguments needs to be followed by their value. +% The order of the input arguments is of no importance. +% +% Examples: out=rsimca(x,group,'method','1') +% out=rsimca(x,group,'plots',0) +% out=rsimca(x,group,'valid',y,'groupvalid',groupy) +% +% The output is a structure containing the following fields: +% result.assignedgroup : If there is a validation set, this vector contains the assigned group numbers +% for the observations of the validation set. Otherwise it contains the +% assigned group numbers of the original observations based on the discriminant rules. +% result.pca : A cell containing the results of the different ROBPCA analysis on the training sets. +% result.method : String containing the method used to obtain +% the discriminant rules (either 1 for 'R1' or 2 for 'R2'). This +% corresponds to the input argument method. +% result.flagtrain : Observations from the training set whose score distance and/or orthogonal distance +% exceeds a certain cut-off value can be considered as outliers and receive a flag equal +% to zero. The regular observations receive a flag 1. (See also robpca.m) +% result.flagvalid : Observations from the validation set whose score distance and/or orthogonal distance +% exceeds a certain cut-off value can be considered as outliers and receive a +% flag equal to zero. The regular observations receive a flag 1. +% If there is no validation set, this field is equal to zero. +% result.grouppredict : If there is a prediction set, this vector contains the assigned group numbers +% for the observations of the prediction set. +% result.flagpredict : Observations from the new data set (predict) whose robust distance (to the center of their group) +% exceeds a certain cut-off value can be considered as overall outliers and receive a +% flag equal to zero. The regular observations receive a flag 1. +% If there is no prediction set, this field is +% equal to zero. +% result.membershipprob : A vector with the membership probabilities. If no priors are given, they are estimated +% as the proportions of regular observations in the training set. +% result.misclassif : String containing the method used to estimate the misclassification probabilities +% (same as the input argument misclassif). +% result.groupmisclasprob : A vector containing the misclassification probabilities for each group. +% result.avemisclasprob : Overall probability of misclassification (weighted average of the misclassification +% probabilities over all groups). +% result.class : 'RSIMCA' +% result.classic : If the input argument 'classic' is equal to one, this structure +% contains results of the classical SIMCA analysis. +% result.compare : If the input argument 'compare' is equal to one, this strucuture +% contains results for the classical SIMCA analysis with the same weights +% and priors as in the robust analysis. +% result.x : The training data set (same as the input argument x) (only in output when p<=3). +% result.group : The group numbers of the training set (same as the input argument group) (only in output when p<=3). +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Karlien Vanden Branden +% Last Update: 05/07/2005 +% + +if nargin<2 + error('There are too few input arguments.') +end + +% assigning default-values +[n,p]=size(x); +if size(group,1)~=1 + group=group'; +end +if n ~= length(group) + error('The number of observations is not the same as the length of the group vector!') +end +g=group; +counts=tabulate(g); %contingency table (outputmatrix with 3 colums): value - number - percentage +[lev,levi,levj]=unique(g); +if ~all(counts(:,2)) %some groups have zero values, omit those groups + disp(['Warning: group(s) ', num2str(counts(counts(:,2)==0,1)'), 'are empty']); + empty=counts(counts(:,2)==0,:); + counts=counts(counts(:,2)~=0,:); +else + empty=[]; +end +ng=size(counts,1); +proportions = zeros(ng,1); +y=0; %initial values of the validation data set and its groupsvector +groupy=0; +labsd = 3; +labod = 3; +plotsrobpca = 0; +press = 1; +scree = 0; +counter=1; +gamma = 0.5; +k = zeros(ng,1); +for iClass = 1:ng + r(iClass,1) = rank(x(find(group == iClass),:)); + kmax(iClass,1) = min([10,floor(counts(iClass,2)/2),r(iClass)]); +end +default=struct('alpha',0.75,'k',k,'kmax',kmax,'scree',scree,'press',press,'method',2,... + 'gamma',0.5,'misclassif','training','membershipprob',proportions,'valid',y,... + 'groupvalid',groupy,'plots',1,'plotsrobpca',plotsrobpca,'labsd',labsd,'labod',labod,... + 'mcd',0,'classic',0,'compare',0,'predictset',[]); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +%reading the user's input +if nargin>2 + % + %placing inputfields in array of strings + % + for j=1:nargin-2 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + % + %Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact'); + if ~isempty(index) %in case of similarity + for j=1:nargin-2 %searching the index of the accompanying field + if rem(j,2)~=0 %fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end +end + +%Checking gamma +gamma = options.gamma; +if any(gamma>1) | any(gamma<0) + error('An inappropriate number for gamma is given. A correct value lies between 0 and 1.'); +end + +%Checking prior (>0 ) +prior=options.membershipprob; +if size(prior,1)~=1 + prior=prior'; +end +epsilon=10^-4; +if sum(prior) ~=0 & (any(prior < 0) | (abs(sum(prior)-1)) > epsilon) + error('Invalid membership probabilities.') +end +if length(prior)~=ng + error('The number of membership probabilities is not the same as the number of groups.') +end + +%%%%%%%%%%%%%%%%%%MAIN FUNCTION %%%%%%%%%%%%%%%%%%%%% +%Checking if a validation set is given +if strmatch(options.misclassif, 'valid','exact') + if options.valid==0 + error(['The misclassification error will be estimated through a validation set',... + 'but no validation set is given!']) + else + validx = options.valid; + validgroup = options.groupvalid; + if size(validx,1)~=length(validgroup) + error('The number of observations in the validation set is not the same as the length of its group vector!') + end + if size(validgroup,1)~=1 + validgroup = validgroup'; + end + countsvalid=tabulate(validgroup); + countsvalid=countsvalid(countsvalid(:,2)~=0,:); + if size(countsvalid,1)==1 + error('The validation set must contain observations from more than one group!') + elseif any(ismember(empty,countsvalid(:,1))) + error(['Group(s) ' ,num2str(empty(ismember(empty,countsvalid(:,1)))), 'was/were empty in the original dataset.']) + end + end +elseif options.valid~=0 + validx = options.valid; + validgroup = options.groupvalid; + if size(validx,1) ~= length(validgroup) + error('The number of observations in the validation set is not the same as the length of its group vector!') + end + if size(validgroup,1)~=1 + validgroup = validgroup'; + end + options.misclassif='valid'; + countsvalid=tabulate(validgroup); + countsvalid=countsvalid(countsvalid(:,2)~=0,:); + if size(countsvalid,1)==1 + error('The validation set must contain more than one group!') + elseif any(ismember(empty,countsvalid(:,1))) + error(['Group(s) ' , num2str(empty(ismember(empty,countsvalid(:,1)))), ' was/were empty in the original dataset.']) + end +end + +if length(options.alpha) == 1 + alfa = ones(ng,1)*options.alpha; +elseif length(options.alpha) ~= ng + error('The length of alpha does not correspond with the number of groups.'); +else + alfa = options.alpha; +end +model.counts = counts(:,2); +model.x = x; +model.group = group; + +%Checking input variable k & kmax: +if sum(options.kmax>0)~=ng + mess=sprintf(['Attention (rsimca.m): The value for kmax is incorrect, kmax = ',num2str(options.kmax'),... + '\n is smaller than 0. kmax is set to ',num2str(kmax')]); + disp(mess) +end +if sum(k<=options.kmax) ~= ng + error('The value for k is set too large.'); +end +if sum(options.k ~= 0) == ng + press = 0; + scree = 0; +else + press = options.press; + scree = options.scree; +end + +labsd = floor(max(0,min(options.labsd,n))); +labod = floor(max(0,min(options.labod,n))); + +%RSIMCA: +%PRINCIPAL COMPONENT ANALYSIS +%Perform ROBPCA on each group separately: +% a) if k is not given: decide on the optimal number of components using CV. +% b) if k is given: perform robpca with the optimal number of components. + +for iClass = 1:ng + indexgroup = find(g==iClass); + groupi = x(indexgroup,:); + if press == 1 & scree == 1 + disp(['A press curve and a scree plot are drawn for group ',num2str(iClass),'.']) + elseif press == 1 & scree == 0 + disp(['A press curve is drawn for group ',num2str(iClass),'.']) + elseif press == 0 & scree == 1 + disp(['A scree plot is drawn for group ',num2str(iClass),'.']) + end + model.result{iClass} = robpca(groupi,'k',options.k(iClass),'kmax',options.kmax(iClass),'plots',options.plotsrobpca,... + 'alpha',alfa(iClass),'scree',scree,'press',press,'labsd',labsd,'labod',labod,'mcd',options.mcd); + + model.flag(1,indexgroup) = model.result{iClass}.flag.all; + model.k(iClass) = model.result{iClass}.k; +end + +%CLASSIFICATION +%Discriminant rule based on the training set x +[odsc,sdsc] = testmodel(model,x,g); +finalgrouptrain = assigngroup(odsc,sdsc,options.method,ng,gamma); +result1.weights = model.flag; +if sum(prior) == 0 + for iClass=1:ng + result1.ingroup(iClass) = sum((group(result1.weights == 1) == repmat(lev(iClass),1,sum(result1.weights)))); + end + result1.prior = result1.ingroup./sum(result1.ingroup)'; +else + result1.prior = prior; +end + +%Compute scaled orthogonal and scaled score distances for the validation set +if strmatch(options.misclassif,'valid','exact') + [odsc,sdsc] = testmodel(model,validx); + finalgroup = assigngroup(odsc,sdsc,options.method,ng,gamma); +elseif strmatch(options.misclassif,'cv','exact') %use cv + [odsc,sdsc] = leave1out(model); + finalgroup = assigngroup(odsc,sdsc,options.method,ng,gamma); +end + +switch options.misclassif +case 'valid' + [v,vi,vj]=unique(validgroup); + odscgroup = []; + sdscgroup = []; + for iClass = 1:ng + indexgroup = find(validgroup == iClass); + odscgroup = [odscgroup;odsc(indexgroup,iClass)]; + sdscgroup = [sdscgroup;sdsc(indexgroup,iClass)]; + end + weightsvalid=zeros(1,length(odscgroup)); + weightsvalid(((odscgroup <= 1) & (sdscgroup <= 1)))=1; + for igamma = 1:length(gamma) + for iClass=1:ng + misclas(iClass)=sum((validgroup(weightsvalid==1)==finalgroup(weightsvalid==1,igamma)') & ... + (validgroup(weightsvalid==1)==repmat(v(iClass),1,sum(weightsvalid)))); + ingroup(iClass) = sum((validgroup(weightsvalid == 1) == repmat(v(iClass),1, sum(weightsvalid)))); + end + misclas = (1 - (misclas./ingroup)); + misclasprobpergroup(igamma,:)=misclas; + misclas=misclas.*result1.prior; + misclasprob(igamma)=sum(misclas); + end +case 'training' + for igamma = 1:length(gamma) + for iClass = 1:ng + result1.misclas(iClass) = sum((group(result1.weights==1)==finalgrouptrain(result1.weights==1,igamma)')& ... + (group(result1.weights==1)==repmat(lev(iClass),1,sum(result1.weights)))); + result1.ingroup(iClass) = sum((group(result1.weights == 1) == repmat(lev(iClass),1,sum(result1.weights)))); + end + misclas = (1 - (result1.misclas./result1.ingroup)); + misclasprobpergroup(igamma,:) = misclas; + misclas = misclas.*result1.prior; + misclasprob(igamma) = sum(misclas); + weightsvalid=0;%only available with validation set + end + finalgroup = finalgrouptrain; +case 'cv' + for igamma = 1:length(gamma) + for iClass=1:ng + misclas(iClass)=sum((group(result1.weights==1)==finalgroup(result1.weights==1,igamma)') & ... + (group(result1.weights==1)==repmat(lev(iClass),1,sum(result1.weights)))); + ingroup(iClass) = sum((group(result1.weights == 1) == repmat(lev(iClass),1,sum(result1.weights)))); + end + misclas = (1 - (misclas./ingroup)); + misclasprobpergroup(igamma,:)=misclas; + misclas=misclas.*result1.prior; + misclasprob(igamma)=sum(misclas); + end + weightsvalid=0; %only available with validation set +end + +if ~isempty(options.predictset) + [odscpredict,sdscpredict] = testmodel(model,options.predictset); + finalgrouppredict = assigngroup(odscpredict,sdscpredict,options.method,ng,gamma)'; + weightspredict = (max((odscpredict <= 1) & (sdscpredict <= 1),[],2))'; +else + finalgrouppredict = 0; + weightspredict = 0; +end + +if options.classic + classicout=csimca(x,g,'k',model.k,'scree',scree,'press',press,'method',options.method,'gamma',gamma,... + 'misclassif',options.misclassif,'membershipprob',prior,'valid',options.valid,... + 'groupvalid',options.groupvalid,'plots',0,'plotspca',0,'labsd',labsd,'labod',labod,... + 'predictset',options.predictset); +else + classicout=0; +end + +if options.compare + compareout=csimca(x,g,'k',model.k,'scree',scree,'press',press,'method',options.method,'gamma',gamma,... + 'misclassif',options.misclassif,'membershipprob',result1.prior,'valid',options.valid,... + 'groupvalid',options.groupvalid,'plots',0,'plotspca',0,'labsd',labsd,'labod',labod,... + 'weightstrain',result1.weights,'weightsvalid',weightsvalid,'predictset',options.predictset); +else + compareout = 0; +end + +%Output structure +result = struct('assignedgroup',{finalgroup'},'pca',{model.result},'method',options.method,... + 'flagtrain',{model.flag},'flagvalid',weightsvalid,'grouppredict',finalgrouppredict,'flagpredict',weightspredict,... + 'membershipprob',{result1.prior},'misclassif',{options.misclassif},'groupmisclasprob',{misclasprobpergroup},... + 'avemisclasprob',{misclasprob},'class',{'RSIMCA'},'classic',{classicout},'compare',{compareout},'x',x,'group',group); + +if size(x,2)>3 + result=rmfield(result,{'x','group'}); +end + +%Plots: +try + if options.plots + makeplot(result); + end +catch %output must be given even if plots are interrupted + %> delete(gcf) to get rid of the menu +end + +%--------------------------------------------- +%Leave-One-Out procedure + +function [odsc,sdsc] = leave1out(model) + +nClass = length(model.result); + +for iClass = 1:nClass + index = 1; + indexgroup = find(model.group == iClass); + teller_if_lus = 0; + groupi = model.x(indexgroup,:); + for i = 1:model.counts(iClass) + groupia = removal(groupi,i,0); + GRes = model.result; + H0 = GRes{iClass}.Hsubsets.H0; + H1 = GRes{iClass}.Hsubsets.H1; + Hfreq = GRes{iClass}.Hsubsets.Hfreq; + Hsets = [H0;H1;Hfreq]; + Hsetsmini = RemoveObsHsets(Hsets,i); + same.value = 0; + if isempty(find(H0 == i)) + if teller_if_lus >= 1 + same.value = 1; + end + teller_if_lus = teller_if_lus + 1; + end + interimRes = removeObsRobpca(groupi,i,GRes{iClass}.k,... + Hsetsmini,same); + if isempty(find(H0 == i)) + same.res = interimRes; + end + GRes{iClass}.M = interimRes.muk_min_i; + GRes{iClass}.P = interimRes.Pk_min_i; + GRes{iClass}.L = interimRes.Lk_min_i; + GRes{iClass}.T = (groupia - ones(model.counts(iClass)-1,1)*GRes{iClass}.M)*GRes{iClass}.P; + GRes{iClass}.h = GRes{iClass}.h - 1; + outDist = CompDist(groupia,GRes{iClass}); + GRes{iClass}.cutoff.od = outDist.cutoff.od; + GRes{iClass}.cutoff.sd = outDist.cutoff.sd; + + %Calculate for each class the sd and the od for the observation that was left out: + for jClass = 1:nClass + dataicentered = model.x(indexgroup(index),:)-GRes{jClass}.M; + scorei = dataicentered*GRes{jClass}.P; + dataitilde = scorei*GRes{jClass}.P'; + sd(indexgroup(index),jClass) = sqrt(scorei*(diag(1./GRes{jClass}.L))*scorei'); + od(indexgroup(index),jClass) = norm(dataicentered-dataitilde); + if GRes{jClass}.cutoff.od ~= 0 + odsc(indexgroup(index),jClass) = od(indexgroup(index),jClass)/GRes{jClass}.cutoff.od; + else + odsc(indexgroup(index),jClass) = 0; + end + sdsc(indexgroup(index),jClass) = sd(indexgroup(index),jClass)/GRes{jClass}.cutoff.sd; + end + index = index + 1; + end +end + +%-------------------------------- +function [odsc,sdsc] = testmodel(model,validx,validgroup) + +%Apply the given model on the test data to obtain different +%orthogonal distances and score distances. + +nClass = length(model.result); +n = size(validx,1); + +for jClass = 1:nClass + for index = 1:n + out{jClass} = model.result{jClass}; + dataicentered = validx(index,:)-out{jClass}.M; + scorei = dataicentered*out{jClass}.P; + dataitilde = scorei*out{jClass}.P'; + sd(index,jClass) = sqrt(scorei*(diag(1./out{jClass}.L))*scorei'); + od(index,jClass) = norm(dataicentered-dataitilde); + if out{jClass}.cutoff.od ~= 0 + odsc(index,jClass) = od(index,jClass)/out{jClass}.cutoff.od; + else + odsc(index,jClass) = 0; + end + sdsc(index,jClass) = sd(index,jClass)/out{jClass}.cutoff.sd; + end +end + + +%----------------------------------------------------------------------------------------------------------- +function Hsets_min_i = RemoveObsHsets(Hsets,i) + +% removes the right index from the $h$-subsets in Hsets to +% obtain (h - 1)-subsets. +% every h-set is put as a row in Hsets. +% i is the index of the observation that is removed from the whole data. + +for r = 1:size(Hsets,1) + if ~isempty(find(Hsets(r,:)== i)) + Hsets_min_i(r,:) = removal(Hsets(r,:),0,find(Hsets(r,:) == i)); + else + Hsets_min_i(r,:) = Hsets(r,1:(end-1)); + end + + for j = 1:length(Hsets_min_i(r,:)) + if Hsets_min_i(r,j) > i + Hsets_min_i(r,j) = Hsets_min_i(r,j) - 1; + end + end +end + +%------------------------- +function result = assigngroup(odsc,sdsc,method,nClass,gamma); + +%Obtain the group assignments for given od's and sd's. + +if method == 1 + sd = sdsc; + od = odsc; +elseif method == 2 + sd = sdsc.^2; + od = odsc.^2; +end + +for igamma = 1:length(gamma) + tdist = gamma(igamma).*od + (1-gamma(igamma)).*sd; + for i = 1:size(od,1) + result(i,igamma) = find(tdist(i,:) == min(tdist(i,:))); + end +end + +%---------------------------- + +function outDist = CompDist(data,out) + +% Calculates the score and orthogonal distances +% input: data : the original data +% out is a structure that contains the results of the PCA. + + +[n,p] = size(data); +r = rank(data); +k = out.k; + +% Computing distances +% Robust score distances +out.sd=sqrt(mahalanobis(out.T,zeros(size(out.T,2),1),'cov',out.L))'; +out.cutoff.sd=sqrt(chi2inv(0.975,out.k)); +% Orthogonal distances +XRc=data-repmat(out.M,n,1); +Xtilde=out.T*out.P'; +Rdiff=XRc-Xtilde; +out.od = []; +for i=1:n + out.od(i,1)=norm(Rdiff(i,:)); +end +% Robust cutoff-value for the orthogonal distance +if k~=r + [m,s]=unimcd(out.od.^(2/3),out.h); + out.cutoff.od = sqrt(norminv(0.975,m,s).^3); +else + out.cutoff.od=0; +end + +outDist = out; + diff --git a/LIBRA/rsimpls.m b/LIBRA/rsimpls.m new file mode 100644 index 0000000..74026cc --- /dev/null +++ b/LIBRA/rsimpls.m @@ -0,0 +1,519 @@ +function result=rsimpls(x,y,varargin) + +%RSIMPLS is a 'Robust method for Partial Least Squares Regression based on the +% SIMPLS algorithm'. It can be applied to both low and high-dimensional predictor variables x +% and to one or multiple response variables y. It is resistant to outliers in the data. +% The RSIMPLS algorithm is built on two main stages. First, a matrix of scores is derived +% based on a robust covariance criterion (see robpca.m), +% and secondly a robust regression is performed based on the results from ROBPCA. +% +% The RSIMPLS method is described in: +% Hubert, M., and Vanden Branden, K. (2003), +% "Robust Methods for Partial Least Squares Regression", +% Journal of Chemometrics, 17, 537-549. +% +% To select the number of components in the regression model, a robust RMSECV (root mean squared +% error of cross validation) curve is drawn, based on a fast algorithm for +% cross-validation. This approach is described in: +% +% Engelen, S., Hubert, M. (2005), +% "Fast model selection for robust calibration methods", +% Analytica Chimica Acta, 544, 219-228. +% +% Required input arguments: +% x : Data matrix of the explanatory variables +% (n observations in rows, p variables in columns) +% y : Data matrix of the response variables +% (n observations in rows, q variables in columns) +% +% Optional input arguments: +% k : Number of components to be used. +% (default = min(rank([x,y]),kmax)). If k is not specified, +% it can be selected using the option 'rmsecv'. +% kmax : Maximal number of components to be used (default = 9). +% If k is provided, kmax does not need to be specified, unless k is larger +% than 9. +% alpha : (1-alpha) measures the fraction of outliers the algorithm should +% resist. Any value between 0.5 and 1 may be specified. (default = 0.75) +% h : (n-h+1) measures the number of observations the algorithm should +% resist. Any value between n/2 and n may be specified. (default = 0.75*n) +% Alpha and h may not both be specified. +% rmsecv : If equal to zero and k is not specified, a robust R-squared curve +% is plotted and an optimal k value can be chosen. (default) +% If equal to one and k is not specified, a robust component selection-curve is plotted and +% an optimal k value can be chosen. This curve computes a combination of +% the cross-validated root mean squared error and the robust residual +% sum of squares for k=1 to kmax. Rmsecv and k may not both +% be specified. +% rmsep : If equal to one, the robust RMSEP-value (root mean squared error of +% prediction) for the model with k components (default = 0). This value +% is automatically given if rmsecv = 1. +% plots : If equal to one, a menu is shown which allows to draw several plots, +% such as a robust score outlier map and a regression +% outlier map are drawn. (default) +% If the input argument 'classic' is equal to one, the classical +% diagnostic plots are drawn as well. +% If 'plots' is equal to zero, all plots are suppressed. +% See also makeplot.m +% labsd : The 'labsd' observations with largest score distance are +% labeled on the outlier map (default = 3) +% labod : The 'labod' observations with largest orthogonal distance are +% labeled on the outlier map (default = 3) +% labresd : The 'labresd' observations with largest residual distance are +% labeled on the outlier map (default = 3) +% classic : If equal to one, the classical SIMPLS analysis will be performed as well +% (see also csimpls.m). (default = 0) +% +% Options for advanced users: +% kr : Total number of components used by the ROBPCA method. +% We advise to use kr=k+q. (default) +% kmaxr : Maximal number of components used by the ROBPCA method. +% default = min(kmax+q,rank([x,y])) +% plotsrobpca : If equal to one, a robust score outlier map from ROBPCA is drawn. +% If the input argument 'classic' is equal to one, the classical +% outlier map is drawn as well. +% If 'plotsrobpca' is equal to zero, all plots are suppressed. (default) +% st : Indicates the current stage of the algorithm for cross-validation (RMSECV) +% default = 0 -> performs all the stages of the algorithm and computes all the +% parameters for the full model. +% st=1, robpca is still performed, but when +% st=2, the algorithm proceeds based on the previous knownledge of the output from robpca. +% out : Is an empty structure, but is constructed while performing the cross-validation. +% (see rrmse.m) +% +% I/O: result=rsimpls(x,y,'k',k,'kmax',10,'alpha',0.75,'h',h,'rmsecv',0,'rmsep',0,... +% 'plots',1,'labsd',3,'labod',3,'labresd',3,'classic',1,'kr',kr,... +% 'kmaxr',kmaxr,'plotsrobpca',0,'st',0,'out',[]); +% The user should only give the input arguments that have to change their default value. +% The name of the input arguments needs to be followed by their value. +% The order of the input arguments is of no importance. +% +% Examples: +% rsimpls(x,y,'k',5,'plots',1); +% rsimpls(x,y,'classic',1,'rmsecv',1); +% +% The output of RSIMPLS is a structure containing: +% +% result.slope : Robust slope estimate +% result.int : Robust intercept estimate +% result.fitted : Robust fitted values +% result.res : Robust residuals +% result.cov : Estimated variance-covariance matrix of the residuals +% result.T : Robust scores +% result.weights.r : Robust simpls weights +% result.weights.p : Robust simpls weights +% result.Tcenter : Robust center of the scores +% result.Tcov : Robust covariance matrix of the scores +% result.rsquared : Robust R-squared value for the optimal k +% result.rcs : Robust Component Selection Criterion: +% This is a matrix with kmax columns. The first row contains the approximate +% R-squared values (for k=1,...,kmax) and the second row the square root of the weighted +% residuals sum of squares. This is equal to the RCS-value with +% gamma = 0 (see Engelen and Hubert (2005) for the definition). +% If the input argument rmsecv = 1, the third row contains the RCS-value for +% gamma = 0.5 and the fourth for gamma = 1. The last one is equal to the robust +% cross-validated RMSE values. +% Note that all the entries in this matrix depend on the choice of kmax. +% result.rmsep : Robust RMSEP value +% result.k : Number of components used in the regression +% result.h : The quantile h used throughout the algorithm +% result.sd : Robust score distances +% result.od : Robust orthogonal distances +% result.resd : Residual distances (when there are several response variables). +% If univariate regression is performed, it contains the standardized residuals. +% result.cutoff : Cutoff values for the score (result.cutoff.sd), orthogonal +% (result.cutoff.od) and residual distances (result.cutoff.resd). +% We use 0.975 quantiles of the chi-squared distribution. +% result.flag : The observations whose score distance is larger than +% 'result.cutoff.sd' receive a flag 'result.flag.sd' equal +% to zero (good leverage points). Otherwise 'result.flag.sd' +% is equal to one. +% The components 'result.flag.od' and 'result.flag.resd' are +% defined analogously, and determine the orthogonal outliers, +% resp. the bad leverage points/vertical outliers. +% The observations with 'result.flag.od' and 'result.flag.resd' +% equal to zero, can be considered as calibration outliers and receive +% 'result.flag.all' equal to zero. The regular observations and the good leverage +% points have 'result.flag.all' equal to one. +% result.class : 'RSIMPLS' +% result.classic : If the inputargument 'classic' is equal to 1, this structure +% contains results of the classical SIMPLS analysis. (see also csimpls.m) +% results.robpca : The results of robpca on [X,Y]. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Karlien Vanden Branden +% Version date: 07/04/2004 +% Last update: 04/08/2006 + +% +%initialization with defaults +% +if rem(nargin,2)~=0 + error('Number of input arguments must be even!'); +end +[n,p1]=size(x); +[n2,q1]=size(y); +z=[x,y]; +rx=rank(x); +rz=rank(z); +if n~=n2 + error('The response variables and the predictor variables have a different number of observations.') +end +niter=100;counter=1;mcd=0;alfa=0.75; +kmax=min([9,rx,floor(n/2),p1]); +kmaxr=min([kmax+q1,rz]); +h=floor(2*floor((n+kmaxr+1)/2)-n+2*(n-floor((n+kmaxr+1)/2))*alfa); +labsd=3;labod=3;labresd=3; +plotsrobpca=0;plots=1;k=0; +kr=k+q1; +st=0;rmsecv=0; +out=[];classic=0;rmsep=0;rmsep_value=nan;rmsecv_value = nan;rsquared_value = nan;rss_value = nan; +default=struct('alpha',alfa,'h',h,'labsd',labsd,'labod',labod,'labresd',labresd,'k',k,'kr',kr,... + 'plotsrobpca',plotsrobpca,'plots',plots,'kmax',kmax,'st',st,... + 'out',out,'rmsecv',rmsecv,'classic',classic,'rmsep',rmsep,... + 'kmaxr',kmaxr,'rmsep_value',rmsep_value,'rmsecv_value',rmsecv_value,'rsquared_value',... + rsquared_value,'rss_value',rss_value); +list=fieldnames(default); +options=default; +IN=length(list); +i=1; +% +if nargin>2 + % + %placing inputfields in array of strings + % + for j=1:nargin-3 + if rem(j,2)~=0 + chklist{i}=varargin{j}; + i=i+1; + end + end + dummy=sum(strcmp(chklist,'h')+2*strcmp(chklist,'alpha')); + switch dummy + case 0 %Take on default values + options.alpha=alfa;%0.75; + options.h=h; + case 3 + error('Both input arguments alpha and h are provided. Only one is required.') + end + % + %Checking which default parameters have to be changed + % and keep them in the structure 'options'. + % + while counter<=IN + index=strmatch(list(counter,:),chklist,'exact');%contains the users input one by one + if ~isempty(index) %in case of similarity + for j=1:nargin-3 %searching the index of the accompanying field + if rem(j,2)~=0 %fieldnames are placed on odd index + if strcmp(chklist{index},varargin{j}) + I=j; + end + end + end + options=setfield(options,chklist{index},varargin{I+1}); + index=[]; + end + counter=counter+1; + end + options.h=floor(options.h); + options.kmax=floor(options.kmax); + options.k=floor(options.k); + options.kmaxr=floor(options.kmaxr); + options.kr=floor(options.kr); + kmax=min([options.kmax,floor(n/2),rz,p1]); + kmaxr=max([min([options.kmaxr,rz]),kmax+q1]); + k=min(options.k,kmax); + while k<0 + k=input(['The number of components can not be negative.\n'... + 'How many principal components would you like to retain?\n']); + end + if any(strcmp(chklist,'kr')) + kr=floor(max([options.kr,k+q1])); + else + kr=k+q1; + end + if dummy==1 %checking inputvariable h + if options.h-floor(options.h)~=0 + mess=sprintf('Attention (rsimpls.m): h must be an integer. \n'); + disp(mess) + end + if kr==0 + if options.hn + options.alpha=0.75; + if kr==0 + options.h=floor(2*floor((n+kmaxr+1)/2)-n+2*(n-floor((n+kmaxr+1)/2))*options.alpha); + else + options.h=floor(2*floor((n+kr+1)/2)-n+2*(n-floor((n+kr+1)/2))*options.alpha); + end + mess=sprintf(['Attention (rsimpls.m): h should be smaller than n. \n',... + 'It is set to its default value ',num2str(options.h)]); + disp(mess) + end + elseif dummy==2 + if options.alpha < 0.5 + options.alpha=0.5; + mess=sprintf(['Attention (rsimpls.m): Alpha should be larger than 0.5. \n',... + 'It is set to 0.5.']); + disp(mess) + end + if options.alpha > 1 + options.alpha=0.75; + mess=sprintf(['Attention (rsimpls.m): Alpha should be smaller than 1.\n',... + 'It is set to 0.75.']); + disp(mess) + end + if kr==0 + options.h=floor(2*floor((n+kmaxr+1)/2)-n+2*(n-floor((n+kmaxr+1)/2))*options.alpha); + else + options.h=floor(2*floor((n+kr+1)/2)-n+2*(n-floor((n+kr+1)/2))*options.alpha); + end + end + h=options.h;alfa=options.alpha;labsd=max(0,min(floor(options.labsd),n)); + dummyh = strcmp(chklist,'h'); + dummykmax = strcmp(chklist,'kmax'); + if all(dummyh == 0) && any(dummykmax) + h = floor(2*floor((n+kmax+1)/2)-n+2*(n-floor((n+kmax+1)/2))*alfa); + end + labod=max(0,min(floor(options.labod),n));labresd=max(0,min(floor(options.labresd),n)); + plotsrobpca=options.plotsrobpca;plots=options.plots; + st=options.st; + out=options.out; + rmsecv=options.rmsecv; + classic=options.classic; + rmsep=options.rmsep; + rmsep_value = options.rmsep_value; + rmsecv_value = options.rmsecv_value; + rmsecv = options.rmsecv; + rsquared_value = options.rsquared_value; + rss_value = options.rss_value; +end + +if q1==1 && k>=(h-2) + mess=sprintf(['Attention (rsimpls.m): The number of components, k = ',num2str(k),... + '\n is larger than our recommended maximum value of k = ',num2str(h-2)-1,'.']); + disp(mess) +elseif q1>1 && k>=((h/q1)-(q1/2)-0.5) + mess=sprintf(['Attention (rsimpls.m): The number of components, k = ',num2str(k),... + '\n is larger than our recommended maximum value of k = ',num2str(floor((h/q1)-(q1/2)-1.5)),'.']); + disp(mess) +end + +% +%MAIN PART +% +% selection of number of components +if k == 0 + if rmsecv == 0 + [R2,final]=rsquared(x,y,kmax,'RSIMPLS',options.h); + rss = final.rss; + k = final.k; + result=rsimpls(x,y,'k',k,'kr',k+q1,'h',h,'rmsecv',0,'rmsep',options.rmsep,'plots',0,'classic',classic,'rsquared_value',R2,'rss_value',rss); + else + out=rrmse(x,y,h,kmax,'RSIMPLS',1); + R2 = out.R2; + rss = out.rss; + k=out.k; + rmsecv_value = out.rmsecv; + pred = rrmse(x,y,h,kmax,'RSIMPLS',0,k,out.weight,out.res); + result=rsimpls(x,y,'k',k,'kr',k+q1,'h',h,'rmsecv',0,'rmsep',0,'plots',0,'classic',classic,'rmsep_value',pred.rmsep,'rmsecv_value',rmsecv_value,'rsquared_value',R2,'rss_value',rss); + end +else + if rmsecv + error(['Both RMSECV and k were given.', ... + 'Please rerun your analysis with one of these inputs. (see help file)']) + end +%First stage: Obtain the scores T by first performing ROBPCA on z: + if st<=1 + out.robpca=robpca(z,'k',kr,'h',h,'plots',plotsrobpca,'kmax',kmaxr,'classic',classic,'mcd',mcd); + out.h=h; + out.centerz=out.robpca.M; + out.sigmaxy=out.robpca.P(1:p1,:)*diag(out.robpca.L)*out.robpca.P(p1+1:p1+q1,:)'; + out.sigmax=out.robpca.P(1:p1,:)*diag(out.robpca.L)*out.robpca.P(1:p1,:)'; + out.xcentr=x-repmat(out.centerz(1:p1),n,1); + out.ycentr=y-repmat(out.centerz(p1+1:p1+q1),n,1); + out.weights2=out.robpca.flag.all; + end + if st + i=k; + else + i=1; + end + while i<=k + out.sigmayx=out.sigmaxy'; + if q1>p1 + [RR,LL]=eig(out.sigmaxy*out.sigmayx); + [LL,I]=greatsort(diag(LL)); + rr=RR(:,I(1)); + qq=out.sigmayx*rr; + qq=qq/norm(qq); + else + [QQ,LL]=eig(out.sigmayx*out.sigmaxy); + [LL,I]=greatsort(diag(LL)); + qq=QQ(:,I(1)); + rr=out.sigmaxy*qq; + rr=rr/norm(rr); + end + tt=out.xcentr*rr; + uu=out.ycentr*qq; + pp=out.sigmax*rr/(rr'*out.sigmax*rr); + vv=pp; + if i>1 + vv=vv-out.v*(out.v'*pp); + end + if vv'*vv==0 + error('The number of components is too large') + end + vv=vv./norm(vv); + out.sigmaxy=out.sigmaxy-vv*(vv'*out.sigmaxy); + out.v(:,i)=vv; + out.q(:,i)=qq; + out.t(:,i)=tt; + out.u(:,i)=uu; + out.p(:,i)=pp; + out.r(:,i)=rr; + i=i+1; + end + + %Second Stage : Robust ROBPCA-regression + robpcareg=robpcaregres(out.t,y,out.weights2); + breg=robpcareg.coeffs(1:k,:); + Yhat=out.t*breg+repmat(robpcareg.coeffs(k+1,:),n,1); + b=out.r*breg; + int=robpcareg.coeffs(k+1,:)-out.centerz(1:p1)*out.r*breg; + + if rmsep==1 + rmse=rrmse(x,y,h,kmax,'RSIMPLS',0,k); + out.rmsep=rmse.rmsep; + end + + % testing several output parameters + if ~isnan(rmsep_value) + out.rmsep = rmsep_value; + options.rmsep = 1; + end + + if ~isnan(rsquared_value) + out.rcs = rsquared_value; + end + if ~isnan(rss_value) + out.rcs = [out.rcs;sqrt(rss_value)]; + end + if ~isnan(rmsecv_value) + gammahalf = 0.5*sqrt(rss_value) + 0.5*rmsecv_value; + out.rcs = [out.rcs;gammahalf;rmsecv_value]; + options.rmsecv = 1; + end + if any(isnan(rsquared_value)) && any(isnan(rss_value)) && any(isnan(rmsecv_value)) + out.rcs = 0; + end + + %The output: + out.T=out.t; + out.weights.p=out.p; + out.weights.r=out.r; + out.kr=kr; + out.h=h; + out.alpha=alfa; + out.slope=b; + out.int=int; + out.yhat=x*b+repmat(int,n,1); + out.x=x; + out.y=y; + out.res=y-out.yhat; + out.class='RSIMPLS'; + out.k=k; + out.cov=robpcareg.cov; + + if ~st + %calculation of robust distances + %Score distance + out.Tcov=robpcareg.sigma(1:k,1:k); + out.Tcenter=robpcareg.center(1:k); + out.sd=sqrt(mahalanobis(out.t,out.Tcenter,'cov',out.Tcov))'; + out.cutoff.sd=sqrt(chi2inv(0.975,k)); + %robust residual distance + if q1==1 + out.resd=out.res/sqrt(out.cov); + else + out.resd=sqrt(mahalanobis(out.res,zeros(1,q1),'cov',out.cov))'; + end + %robust orthogonal distances + xtilde=out.t*out.p'; + Cdiff=out.xcentr-xtilde; + for i=1:n + out.od(i,1)=norm(Cdiff(i,:)); + end + r=rank(x); + if k~=r + [m,s]=unimcd(out.od.^(2/3),out.h); + out.cutoff.od = sqrt(norminv(0.975,m,s)^3); + else + out.cutoff.od=0; + end + out.cutoff.resd=sqrt(chi2inv(0.975,q1)); + + %Computing flags + out.flag.od=out.od<=out.cutoff.od; + out.flag.resd=abs(out.resd)<=out.cutoff.resd; + out.flag.all=(out.flag.od & out.flag.resd); + + + %Multivariate Rsquared + Yw=y(out.flag.all==1,:); + cYw=mcenter(Yw); + res=out.res(out.flag.all==1,:); + out.rsquared=1-(det(res'*res)/det(cYw'*cYw)); + + %Assigning output + if options.rmsep~=1 && options.rmsecv~=1 + out.rmsep=0; + end + + if classic + resultclassic=csimpls(x,y,'k',k,'plots',0); + else + resultclassic=0; + end + result=struct('slope',{out.slope}, 'int',{out.int},'fitted',{out.yhat},'res',{out.res}, 'cov',{out.cov},... + 'T',{out.T}, 'weights', {out.weights},'Tcenter',{out.Tcenter},'Tcov',{out.Tcov},'rsquared',{out.rsquared},'rcs',{out.rcs},'rmsep',{out.rmsep},... + 'k',{out.k},'alpha',{out.alpha},'h',{out.h},'sd', {out.sd},'od',{out.od},... + 'resd',{out.resd},'cutoff',{out.cutoff},'flag',{out.flag},'class',{out.class},'classic',{resultclassic},'robpca',{out.robpca}); + if result.rcs==0 + result=rmfield(result,'rcs'); + end + if result.rmsep==0 + result=rmfield(result,'rmsep'); + end + else + result=out; + end +end + +% Plots +try + if plots && options.classic + makeplot(result,'classic',1,'labsd',labsd,'labod',labod,'labresd',labresd) + elseif plots + makeplot(result,'labsd',labsd,'labod',labod,'labresd',labresd) + end +catch %output must be given even if plots are interrupted +end diff --git a/LIBRA/rsquared.m b/LIBRA/rsquared.m new file mode 100644 index 0000000..b8bedd7 --- /dev/null +++ b/LIBRA/rsquared.m @@ -0,0 +1,106 @@ +function [R2,out]=rsquared(x,y,kmax,method,h) + +%RSQUARED calculates the R-squared value of the robust or classical PCR/PLS analysis. This function +% is used in rpcr.m and rsimpls.m. +% +% The Robust R-squared is described in: +% Hubert, M., Verboven, S. (2003), +% "A robust PCR method for high-dimensional regressors", +% Journal of Chemometrics, 17, 438-452. +% +% The required input arguments +% x : the explanatory variables +% y : the response variables +% kmax : the number of components to choose in the ROBPCA method +% method : the method for which the R2 has to be computed. It can be 'RSIMPLS' or 'RPCR'. +% +%Optional input arguments +% h : the quantile used in RPCR/RSIMPLS +% +% I/O: R2=rsquared(x,y,10) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by S.Verboven on 31-10-2002 +% Last updated on 18-02-2003 + + +if nargin<4 + error('Missing input arguments') +end +[n,p]=size(x); +[n,q]=size(y); +cutoffWeights = sqrt(chi2inv(0.975,q)); + +if nargin<5 + h=floor(0.75*n); +end + +count=1; +if q>1 + while count*q+q+(q*(q+1)/2) <= h + count=count+1; + end +else + while count+2 p1 + disp(['The maximum number of principal components is ',num2str(p1),'.']) + disp(['This is the minimum of (number of data points/2) and the rank of the data matrix.']) +end +S=zeros(p1,1); +V=zeros(p,p1); +switch center +case 0 + med=median(X); + Xcentr=X-repmat(med,n,1); +case 1 + med=l1median(X); + Xcentr=X-repmat(med,n,1); +end +Xnewcentr=Xcentr; +kmax=0; +Transfo=eye(p); +for l=1:p1, + B=Xnewcentr; + Bnorm=zeros(n,1); + for i=1:n + Bnorm(i)=norm(B(i,:),2); + end + Bnormr=Bnorm(Bnorm > 1.e-12); + B=B(Bnorm > 1.e-12,:); + %Searching in directions A + A=diag(1./Bnormr)*B; + if size(Xnewcentr,2)==1 %case l=p1 + V(1:l-1,l)=0; + V(l:p,l)=1; + Vorigin(:,l)=Transfo*V(:,p1); + t=Xcentr*Vorigin(:,p1); %last step needs extraction of scale directly in p-dim space + if n>40 + S(p1)=A_scale(t); + else + S(p1)=qnm(t); + end + kmax=kmax+1; + break + else + Y=Xnewcentr*A'; %projected points in columns + end + if n>40 + s=A_scale(Y); + else + s=qnm(Y); + end + [c,vj]=sort(s); + j=vj(length(s)); + S(l)=s(j); + if (S(1)/S(l) > 10^3) &(kmax 1.e-12 + if V(l:p,l)'*Base(:,1) < 0 + V(l:p,l)=(-1)*V(l:p,l); + end + u=(1./norm(Base(:,1)-V(l:p,l)))*(Base(:,1)-V(l:p,l)); + U=Base-2*repmat(u'*Base,p-l+1,1).*repmat(u,1,p-l+1); + else + U=Base; + end + % Transforming eigenvectors to the original pxp dimensional space + if l==1 + Vorigin(:,l)=V(:,l); + Transfo=U; + else + Edge=eye(p); + Edge(l:p,l:p)=U; + Vorigin(:,l)=Transfo*V(:,l); + Transfo=Transfo*Edge; + end + Xnewcentr=Xnewcentr*U; %Reflection of data + Xnewcentr=removal(Xnewcentr,0,1); +end +[S,I]=greatsort(real(S(1:kmax))); +P=Vorigin(:,I); +t=Xcentr*P; + +%-------------------------------------------------------------------------- +function [A_est]=A_scale(Z) + +% A_SCALE calculates the A estimate of scale of the columns of Z +% +% I/O: [A_est]=A_scale(Z); +% + +Z=Z'; +U=(Z - repmat(median(Z,2),1,size(Z,2)))./(repmat(madc(Z')',1,size(Z,2))); +[n,p]=size(U); +for i=1:n + Ui=U(i,:); + if any(isnan(Ui)) + scale(i)=0; + else + Zi=Z(i,:); + med=median(Zi); + m=madc(Zi-med); + Zi=Zi(abs(Ui)<3.85); + Ui=Ui(abs(Ui)<3.85); + Ti=sqrt(sum((Ui.^2).*((3.85^2-Ui.^2).^4)))*sqrt(p)*0.9471*m; + Ni=abs(sum((3.85^2-Ui.^2).*(3.85^2-5*(Ui.^2)))); + scale(i)=Ti/Ni; + end +end +A_est=scale; + diff --git a/LIBRA/ruspini.mat b/LIBRA/ruspini.mat new file mode 100644 index 0000000000000000000000000000000000000000..37015a5fc0e58f378c8373045aca42d4a4ef0807 GIT binary patch literal 335 zcmeZu4DoSvQZUssQ1EpO(M`+DN!3vZ$Vn_o%P-2cQV4Jk_w>_Ia4t$sEJ;mK$j`G< z2+1f_a4bz%Ffvy#G_*1_vNAGKFfuT(R3I5JFnap(GcYh52jYr3kCPJ;b}$^taX7=c z)UiSI)OWTdHH9>mq%s9PEnU46XHK(p`!Xkqh3Wal#mUtvCK_f+a%M_eR;gB+SWQN2MZS)OaI<*A;5n9ktIu>Jh?Jw&zwJxg3RwHM2Fj}YqN)|N6YiG z2UlmuJ74F>k9ODRm)9d4qicq2jvRZQY1Y!se28E7~!VubCm z0qM(K7ea0fyAV1Les0@Z5juTa;ky^ud3J~g^LjHpATH9E|5bCmXelPDW#|i+6 zd7Gx8_5i!=su~)_P#seHa=xk-Ni}5;sEQsN-;`$!i*#6PErH$;V~298&9?;AG0d%F_Y&M zSZ_}8wL=cjYLlN-6eT`s0_n*%c^4U&!AvrZNk=F!FoPBz*N^3%cBFFmVn6*#n|!r| z?Wq1O@^7G0;6aO1%_A;S2BA{EALl1h2-xods|b)51({udlWA3EAhqeAQ;1|nbwKLZ zwJz~^NL;q!fs*ib0Qcft6g83ug~1S!fD7%{COdHF9;-}q$RMG8Qv z8H+Y6{FZ*yA%7Tji zhh+Q>Jf1)#k;(j=Sn83O(#U112Nq>6RD8{op9_^FCO=))S*n6%oMQ4Y5t8?VabhCj zS;_4lY5E|Bn)wQa$tM;ZslVPk{nqza=aPp@iK(N>>IcdSNO(mO^GB0SKgQ|_aY+%D zpmZ0l5NBL0Ai)F+HXwmx_f5VMV)4dgb&61w7vai%L=p-K6WP(5Q-U$q*d~bu zrM?+YBAdWHlq}8^Oj3I>S-s;%EZ>yc3sa`MuoiCQAmNpr!1W*_kG9AS`22|0_>|#i zf2!z^d6*QoPJ)?77eSV6=Yuuh2C#Ww(w z#4H(IS)3#zt0Y{BFB>e+lq4YojA2z0z+EZrDZsQkVRoHxg$OZr3rji86z18Lc@~)2 z*XI^8BiYI)j5%rz_ROXkkb~AuDjJMcWa6dp&t5dxGaF~F7$A~^;uXti9X^Uo7LVh9 zHXIbQmQf3VGAd*|C}u39%Ww*Fm2=Bbq`BpJ&+J0x+9bmp%MjEJs)823Lu!+egc;DR zE=@664CkVfm;uMUKV5kmZO1x8o zVWmk0Ah(Ag-Es!Gk7_aRCAKhykTsNGf@J;G8xzmW*W6;ZU#|Haeom{%w{KmMeeWWh z%eT))3!2t~Uab&tRdCRi!%*^dp}#(TbE#ff3*IvN2`C6TSLD95o$QE<)(YOz*8!g7 zS0E!eO1T|z(QNp_*1jeTrc_W8tF%-6H5ExPc?=Gk~#~;7J zh$-sa_cg%BI&V|L{3 z<5(7c^RI(#9Q!#Tz_EV2-f`xq<&jH|Zt(w^3Y^zfz4l`W!6y@XT8AA13_gj_xDNXy zVDO2AF6*#9z~B?;-`3pVD+=^^ozF8m{ygwc0i|@9d)C1VaK-|&pMuSyx{nS z<2A?c9oHP&>HBG#eu@5jee}Td2Yz*6!1+bzmz-aBKIQzO^C!-ioUc0Ha4tLl=G@e< zyDr-$fadW1erpP@(Tv-DSKP}ykz z#R_Quz0`K8|ECxr7EBE=ZUS-v#eBgi=d;6YKe!(+*mv(ns0DD`IsyH-5cctbNLWQS Jp6{0m`~#UYWs(2@ literal 0 HcmV?d00001 diff --git a/LIBRA/scorediagplot.m b/LIBRA/scorediagplot.m new file mode 100644 index 0000000..788e052 --- /dev/null +++ b/LIBRA/scorediagplot.m @@ -0,0 +1,126 @@ + function scorediagplot(Xdist,Odist,k,cutoffx,cutoffo,attrib,labsd,labod) + +%SCOREDIAGPLOT plots the score outlier map. The score distances (SD) and +% orthogonal distances (OD) calculated in a PCA analysis are plotted +% on the x and y-axis, respectively. +% The cutoff values marked by a red line indicate which observations are +% outlying with respect to the majority of the data. The terminology used is +% as indicated in the tabular: +% small SD | large SD +% ---------------------------------------------------------- +% small OD | regular point | good PCA-leverage point +% large OD | orthogonal outlier | bad PCA-leverage point +% +% For more details see: +% Hubert, M., Verboven, S. (2003), +% "A robust PCR method for high-dimensional regressors", +% Journal of Chemometrics, 17, 438-452. +% +% Required input arguments: +% Xdist : Score distances out of a PCA analysis +% Odist : Orthogonal distances out of a PCA analysis +% k : Number of principal components used in a PCA/PCR analysis +% cutoffx : Cutoff value for the score distances +% (=97.5% quantile of a chisquare with k degrees of freedom) +% attrib : string identifying the used method = RPCR, ROBPCA, CPCR, +% CPCA, MCDREG... +% +% Optional inputs +% labsd : number of displayed points with largest score distance (default = 3) +% labod : number of displayed points with largest orthogonal distance (default=3) +% +% I/O: scorediagplot(out.sd,out.od,out.k,out.cutoff.sd,out.cutoff.od,'RPCR',3,5) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by S. Verboven +% Last update: 20/06/2003 + +%Checking input +if nargin==7 + labod=3; +end +if nargin==6 + labsd=3; + labod=3; +end +if nargin<6 + error('no class identifier is given.') +end + +%INITIALISATION% +%in case k=rank(T)=r then OD = zero +NihilO=(Odist <= 1.e-06); +Odist(NihilO)=0; +NihilX=(Xdist <= 1.e-06); +Xdist(NihilX)=0; +if ~any(Odist) %OD=0: x=index and y=Xdist + x=(1:length(Xdist))'; + y=Xdist; + difflabel=1; %change name of the axis-labels + if ~any(Xdist) + axes + box on + text(0.05,0.5,'The score outliermap is not drawn when all OD and SD are zero.','color','r') + return + end +elseif ~any(Xdist) % SD=0: x=index and y=Odist + x=(1:length(Xdist))'; + y=Odist; + difflabel=2; %change name of the axis-labels +else + x=Xdist; + y=Odist; + difflabel=0; +end +quantx=cutoffx; +set(gcf,'Name', 'Score outlier map', 'NumberTitle', 'off'); +hold on +xmin=0; +xmax=max([x; cutoffx]); +ymin=0; +ymax=max([y; cutoffo]); +xmarg=0.06*(xmax-xmin); +ymarg=0.06*(ymax-ymin); +xmin=xmin-xmarg; +xmax=xmax+xmarg; +ymin=ymin-ymarg; +ymax=ymax+ymarg; +n=length(x); +plot(x,y,'o'); +if difflabel==1 %in case OD = 0 + ymax=max(ymax,cutoffx+ymarg); + xlabel('Index') + ylabel(['Score distance (',num2str(k),' LV)']) + line('Xdata',[xmin xmax] ,'Ydata',[cutoffx cutoffx],'Linestyle','-','Color','r'); + xlim([xmin,xmax]); + ylim([ymin,ymax]); + box on + plotnumbers(x',y,0,labsd,2); + title(attrib) + hold off +elseif difflabel==2 %case SD = 0 + xmax=max(xmax,cutoffo+xmarg); + xlabel('Index') + ylabel(['Orthogonal distance (',num2str(k),' LV)']) + line('Xdata',[xmin xmax] ,'Ydata',[cutoffo cutoffo],'Linestyle','-','Color','r'); + xlim([xmin,xmax]); + ylim([ymin,ymax]); + box on + plotnumbers(x',y,0,labod,2); + title(attrib) + hold off +else + xlabel(['Score distance (',num2str(k),' LV)']); + ylabel('Orthogonal distance'); + line([cutoffx cutoffx],[ymin ymax] ,'Linestyle','-','Color','r'); + line('Xdata',[xmin xmax] ,'Ydata',[cutoffo cutoffo], 'Linestyle','-','Color','r'); + xlim([xmin,xmax]); + ylim([ymin,ymax]); + box on + plotnumbers(x',y,labsd,labod,2); + title(attrib) + hold off +end diff --git a/LIBRA/screeplot.m b/LIBRA/screeplot.m new file mode 100644 index 0000000..9ed807b --- /dev/null +++ b/LIBRA/screeplot.m @@ -0,0 +1,59 @@ +function screeplot(vec,attrib,lev) + +%SCREEPLOT draws the eigenvalues of the covariance matrix of the data in decreasing order. +% It can be used to decide how many latent variables to retain in a PCA analysis +% by looking at the kink in the curve. +% +% Reference: J.A. Jackson (1991), "A User's Guide to Principal Components", +% Wiley Series in Probability and Mathematical Statistics, New-York. +% +% I/O: screeplot(vec,attrib,lev) +% +% Required input arguments: +% vec : eigenvalues +% attrib : class identifier +% +% Optional input argument: +% lev : 0 (default) or 1 to include a plot of the logarithm of the eigenvalues +% +% Created 28 July 2000 by Sabine Verboven +% Last Update: 20/06/2003 + +if nargin < 3 + lev=0; +end +if nargin < 2 + error('no class identifier is given.') +end +%%%%%%%MAIN%%%%%%% +set(gcf,'Name', 'Scree plot','NumberTitle', 'off'); +cla +hold on +ymin=max([0,min(vec)]); +ymax=max(vec); +ymarg=0.06*(ymax-ymin); +ymin=ymin-ymarg; +ymax=ymax+ymarg; +plot(vec,'o-'); +xlabel('Index') +ylabel('Eigenvalue') +if length(vec)==1 + xlim([0,2]); + ymarg=0.5; + ymin=ymin-ymarg; + ymax=ymin+(2*ymarg); +else + xlim([1,length(vec)]); +end +ylim([ymin,ymax]); +if lev==1 + set(gcf,'Name', 'LEV plot','NumberTitle', 'off'); + hold on + plot(log(vec),'b.-'); + xlabel('Number of LV') + ylabel('Log(eigenvalue)') +end + +box on +title(attrib) +hold off \ No newline at end of file diff --git a/LIBRA/silhouetteplot.m b/LIBRA/silhouetteplot.m new file mode 100644 index 0000000..13e7b6c --- /dev/null +++ b/LIBRA/silhouetteplot.m @@ -0,0 +1,36 @@ +function silhouetteplot(sylinf,n,class) + +%SILHOUETTEPLOT creates a silhouetteplot for output of the cluster +% algorithms pam or fanny. +% +% I/O: silhouetteplot(sylinf,n) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Last update: 13/02/2009 + + +Y=sylinf(:,3); +% we calculate b= "Y but with a bar with length zero if the objects +% are from another cluster" +% and h= "objects but with a 0 between 2 clusters" = "g with a 0 if +% it is a sparse between 2 clusters" + +g=sylinf(:,4); %original objectnumbers +f=sylinf(:,1)-1; +for j=1:n + b(j+f(j))=Y(j); + h(j+f(j))=g(j); +end +b1=flipdim(b,2); +h1=flipdim(h,2); +% we use this b1 and h1 to plot the barh (instead of Y and g) +barh(b1,1); +title(['Silhouette Plot obtained with ',class,' clustering']) ; +xlabel('Silhouette width'); +YT=1:n+(sylinf(n,1)-1); +set(gca,'YTick',YT); +set(gca,'YTickLabel',h1); +axis([min([Y' 0]),max([Y' 0]),0.5,n+0.5+f(n)]); diff --git a/LIBRA/simcaplot.m b/LIBRA/simcaplot.m new file mode 100644 index 0000000..e6c457f --- /dev/null +++ b/LIBRA/simcaplot.m @@ -0,0 +1,169 @@ +function simcaplot(result); + +%SIMCAPLOT plots a scatter plot with the boundaries defined by the SIMCA method. +% It is based on the results from a simca analysis (see rsimca.m or csimca.m). +% +% For technical reasons, 6 different groups can be plotted (with different symbols). +% In case there are more groups, please adapt lines 15--17. +% +% I/O: simcaplot(result) +% +% Written by K. Vanden Branden on 01/08/04 +% Last update: 21/09/2004 + +p = size(result.x,2); +markings={'bx';'ro';'kdiamond';'y+';'g.';'m*'}; +linecolor={'b-','r-','k-','y-','g-','m-'}; +colorellipse={'b','r','k','y','g','m'}; +if p>3 + error('The dimension of the dataset is larger than 3.') +elseif length(result.avemisclasprob)>6 + error(['Only 6 groups can be drawn with different symbols. Please adapt the code',... + ' of simcaplot.m if you want to draw more groups.']) +else + legstr=[]; + nClass = length(result.groupmisclasprob); + for iClass = 1:nClass + out = result.pca{iClass}; + groupi = result.x(find(result.group == iClass),:); + %legstr=[legstr; sprintf(['Group',num2str(iClass)])]; + if out.k == 1 + [out.lbord,out.rbord]=confreg(out); + bord=[out.lbord,out.rbord]; + elseif out.k == 2 + bord=ellipse([0 0],diag(out.L(1:2))); + elseif out.k == 3 + bord=ellips3D(out.M, out.P*diag(out.L)*out.P'); + end + if p == 2 + xlabel('x');ylabel('y'); + if size(bord) == [1,4] + plot(groupi(:,1),groupi(:,2),markings{iClass});hold on; + plot([bord(1,1),bord(1,3)],[bord(1,2),bord(1,4)],linecolor{iClass}); + elseif size(bord)==[802,2] + plot(groupi(:,1),groupi(:,2),markings{iClass});hold on; + newbord=bord*out.P'+repmat(out.M,802,1); + line(newbord(:,1),newbord(:,2)); + + end + elseif p == 3 + xlabel('x');ylabel('y');zlabel('z') + if size(bord) == [1,6] + plot3(groupi(:,1),groupi(:,2),groupi(:,3),markings{iClass});hold on;grid on; + plot3([bord(1,1),bord(1,4)],[bord(1,2),bord(1,5)],[bord(1,3),bord(1,6)],linecolor{iClass}) + elseif size(bord)==[802,2] + plot3(groupi(:,1),groupi(:,2),groupi(:,3),markings{iClass});hold on;grid on; + newbord=bord*out.P'+repmat(out.M,802,1); + plot3(newbord(:,1),newbord(:,2),newbord(:,3),linecolor{iClass}); + else + plot3(groupi(:,1),groupi(:,2),groupi(:,3),markings{iClass});hold on;grid on; + mesh(bord.x,bord.y1,bord.z1,'Edgecolor',colorellipse{iClass});alpha(0.2); + end + end + end + title(result.class) + %legend(legstr,0) + hold off +end + + +%------------------ +function [lbord,rbord,outl]=confreg(result); + +n1=size(result.T,1);p=size(result.P,1); +rbord=result.M+sqrt(result.L)*result.cutoff.sd*result.P'; +lbord=result.M-sqrt(result.L)*result.cutoff.sd*result.P'; + +%---------------------- +function coord=ellipse(mean,covar) + +% Determines the coordinates of some points that lie on the 97.5 % tolerance ellipse. + +deter=covar(1,1)*covar(2,2)-covar(1,2)^2; +ylimit=sqrt(7.37776*covar(2,2)); +y=-ylimit:0.005*ylimit:ylimit; +sqtdi=sqrt(deter*(ylimit^2-y.^2))/covar(2,2); +sqtdi([1,end])=0; +b=mean(1)+covar(1,2)/covar(2,2)*y; +x1=b-sqtdi; +x2=b+sqtdi; +y=mean(2)+y; +coord=[x1,x2([end:-1:1]);y,y([end:-1:1])]'; + +%---------------- +function out = ellips3D(mu, sigma) + +%This function determines the 3D ellipsoid. + +dist = sqrt(chi2inv(0.975,3)); + +A = inv(sigma); +[ev,ew] = eig(A); +A = ev'*A*ev; + +c1 = 0.1; +zlimit = dist/sqrt(A(3,3)); +z = -zlimit:(c1*zlimit):zlimit; +c2 = (1/c1*2 + 1)*2; % is length(z)*2 +c3 = c2/2; % is length(z) +z1 = ones(1,c2)*z(1); +x1 = zeros(c3,c3); +x2 = zeros(c3,c3); +y = zeros(c3,c3); +y1 = zeros(c3,c2); + +for i = 2:(c3-1) + z1 = [z1 ones(1,c2)*z(i)]; + D = dist^2 - A(3,3)*z(i)^2; + ylimit = sqrt(D)/sqrt(A(2,2)); + y(i,:) = -ylimit:(c1*ylimit):ylimit; + y1(i,:) = [y(i,:) ylimit:(-c1*ylimit):-ylimit]; + + for j = 2:(c3-1) + x1(i,j) = sqrt((D - A(2,2)*y(i,j)^2)/A(1,1)); + x2(i,j) = -sqrt((D - A(2,2)*y(i,j)^2)/A(1,1)); + end +end + +x = [x1,x2]; +z1 = [z1 ones(1,c2)*z(c3)]; +z1 = vec2mat(z1,c2); +grotex = vec2mat([mat2vec(x) mat2vec(y1) mat2vec(z1)],c2*c3); +groottex = ev*grotex; + +groottex(1,:) = groottex(1,:) + mu(1); +groottex(2,:) = groottex(2,:) + mu(2); +groottex(3,:) = groottex(3,:) + mu(3); + +x = vec2mat(groottex(1,:),c2); +y1 = vec2mat(groottex(2,:),c2); +z1 = vec2mat(groottex(3,:),c2); +out.x=x; +out.y1=y1; +out.z1=z1; + + +%----------------- +function vec = mat2vec(mat) + +nkolom = size(mat,2); +nrij = size(mat,1); + +vec = []; + +for i=1:nkolom + hulpvec = wkeep(mat,[nrij,1],[1,1]); + vec = [vec hulpvec']; + mat = mat(:,2:(nkolom - (i-1))); +end + +%---------------------- + +function mat = vec2mat(vec,ncol) + +nrow = length(vec)/ncol; + +for i = 1:nrow + mat(i,:) = wkeep(vec,ncol,'l'); + vec = vec((ncol + 1):length(vec)); +end \ No newline at end of file diff --git a/LIBRA/spannc.mexw32 b/LIBRA/spannc.mexw32 new file mode 100644 index 0000000000000000000000000000000000000000..ed65876207612add2b595a6dd3562b5225dd0d29 GIT binary patch literal 7680 zcmeHM4{#LK8GoB>@Q}s33U#7$tp|n~+B#`&jV;NA_NXl23@I6t5VS1e$SvgJ{Rwxw zAxaLThn8g%q1EX));b^^bvjBIah&L&M3WlOYUx;8qPKuH+RfzJX5ea0xw-B4-tKZs z9h|myoZ89GynWyMzVChC@B7~O-tO$y-?kT(B7|sYx{lBw^!W0V`)A*JL43uvFI<71 zo&CmwL2C6I3!2(P2~TG{wj~~jc-jKdXiW5M5Ip?X%hxn|BC()QId|^m6{hJ1 z4x!c5EcA_Qcin7xJAIlfeXh) zCShGXf%Im#5uql`_9OHX*tz7@fY5`N6#nOeN>S(*Vfv9da06VVH*s55JqT^Aj0Xc^ zz{Cm+g4vA2rsuCT)X=k-S%r-LN=w7&{Rj$-n{A6nc+x69W?*poPcPdnTXq4ozN-?R4mk z+PMOr$8^lm)&NMbVBs|4NF^CcD$^F?_>v3)@@XJ-3BqC^vi-0#UHSoiziAs?I$ok;DAB<62i%AcVP})GIrzfFd~7BWXF<)%?elO{SC7j7G*4wf0xgWMG6X+J#Ok`=EE|Ma#?VMv;hZ1 zJS_85lM8z5zJ^&##ZsNi%BQ_UA76d^?7``={$&MubY;KWXDT4+Z6%pq*KXn%YQCy(f)E7sUDd-Vq>1Pn1f2P+&NIxK57ZZL0c|32d3p|%hCZ$mbF>B-(pTF zN!Qq4RW}I+2!8LFrt(8-*;9}$mESeW?vTa(Q7oEqgc z9Opy|Zq~G@F|EJ4pc)AT)a9i~P2uOK>GP_aBvmc@C0?40Lv>exKE^MVmr7VkrOz3B zO@o6cH3E=&6=|@)4(t)gX|C4b%MQ=FnK|?()^qTVBMD9?3(GAwsd&!;OXJ%mXI`Clz3X7>o<0KMaT6i)o z?Ni;~E5hq}y9nFa7a&kc7AKtwWJwx_)o>SV`V*E9RxNwPmbLPhmUrn2NT7BUPERvY z*aD;C4B1V*I?6qQ*L317IJ#AC2-Y0(0}M3ztx+ydh^v%j9v4_Cjy`KHbsln>t4Zc} zyk7*}^MK?$>OVe{ZvVlO}hmV#)qDm|_8Cm@~a`V+8$ zOm`D|!Ml+)=gwZBN4X5#(PWG;Gh&;P%oO1Ok~bB2T5&sND42WJW_V)0mUQ>OufN`E4szHrElf}?0TL`!PdSz`N*z$2s!&ad0i(D z9P%PjzP2TmbWx(S{Vq5+v?as^JvlFxbd>Ix+aAQs>kOunoNqne$_$RoX*T9bSSkn% zp5iv0uN3_bi&5l z4Lmpn|7%weZ1qCh4eddj-A8ToL7Lj$D~bRp*|qSL*1?4t%C1#r$M$-5bF^-V^BXt=p%z_h@%=55xTi} zygsp|34bmOBd$9nHpF9XLL!0wgkUBY?Fz+XQM{iT!EAI(0wVvAx@I`05np-qwQ%3wH*zo)wF4NC-5G#_NQ3-PEB zzJWEA7X!y?kwjNpTr~J~!2eg8wTd(S=gEM_oJ@Kb4Dk0`s7+J{ZRYy_&H$|UdWg_l z)mIhuKH+`V`@Hv6@9W-oz3+KH@?OaQEa1D{)H@KGk2we|d1KY%|W7o4` zb_aV8dmsBV_F?u>_A&Nx_E+pd_8Im$_5~KyX09(V1J=K{#T$+PX%zUzZ3&F)p!uOC pI^l0#TP2+3+it)MG;%dU%Yl!tR^YCXqHSU*7Bx6C_b*EZ{sE*5C>a0% literal 0 HcmV?d00001 diff --git a/LIBRA/tree.m b/LIBRA/tree.m new file mode 100644 index 0000000..6556708 --- /dev/null +++ b/LIBRA/tree.m @@ -0,0 +1,558 @@ +function tree(objectorder,heights) + +%TREE creates a tree in which the leaves represent +% objects. The vertical coordinate of the junction +% of two branches is the dissimilarity between the +% corresponding clusters (maximal 30 objects allowed). +% +% The algorithm is fully described in: +% Kaufman, L. and Rousseeuw, P.J. (1990), +% "Finding groups in data: An introduction to cluster analysis", +% Wiley-Interscience: New York (Series in Applied Probability and +% Statistics), ISBN 0-471-87876-6. +% +% Required input arguments: +% objectorder : order of objects +% heights : diameter of cluster before dividing it +% (=length of banner) +% +% I/O: +% tree(objectorder,heights) +% +% Example (subtracted from the referenced book) +% load agricul.mat +% result = diana(agricul,[4 4],0,0,1); +% tree(result.objectorder,result.heights) +% +% The output of TREE is a figure containing the +% agglomerative (agnes) or divise (diana) tree. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by Wai Yan Kong (May 2006) +% Last Revision: 04/06/2009 + + +if (nargin<2) + error('Two input arguments required') +elseif (nargin>2) + error('Too many input arguments') +end + +if(size(objectorder,2)~=size(heights,2)+1) + error('Missing values in objectorder or heights') +end + +Heights=H(heights); +number=size(objectorder,2); + +if (number>30) + error('Only 30 objects allowed') +end + +clf('reset') +middle(1)=0; +Maxi=0; +Mini=0; +PrevMID=middle(1); +high=[]; %height +indices=[]; +L=[]; + +[maxim,index]=max(Heights); +if(1<=index-1) + [Prevmax,Previndex]=max(Heights(1:(index-1))); +else + Prevmax=maxim; + Previndex=index; +end +if(index+1<=number-1) + [Postmax,Postindex]=max(Heights((index+1):(number-1))); + Postindex=Postindex+index+1-1; +else + Postmax=maxim; + Postindex=index; +end + +if(Postindex+1<=number-1) + [Post2max,Post2index]=max(Heights(Postindex+1:number-1)); + Post2index=Post2index+Postindex+1-1; +else + Post2max=Postmax; + Post2index=Postindex; +end + +if(Postindex+1<=Post2index-1) + [Betweenmax,Betweenindex]=max(Heights(index+1:Postindex-1)); + Betweenindex=Betweenindex+index+1-1; +else + Betweenmax=Postmax; + Betweenindex=Postindex; +end + +L=cat(2,L,maxim); + +high=cat(2,high,[Post2max,Postmax,Betweenmax,maxim,Prevmax]); +indices=cat(2,indices,[Post2index,Postindex,Betweenindex,index,Previndex]); + +M=0; +extra=number/2; +lengt(1)=Postindex-Previndex+extra+2; +PrevLEN=lengt(1); +rectangle('Position',[middle(1)-(lengt(1)/2),maxim,lengt(1),0.0001]); + +NbanFirst=1; +k=1; +branch=0; +over=0; +Element=0; +Special=0; +Spec=0; +Sp=0; +S=0; +ww=0; +if(maxim<20) + extrawaystick=0.8; + extrawaytext=1.5; +else + extrawaytext=0; + extrawaystick=0; +end + +right=0; +direct11=0; +direct111=0; +righttree=0; +if(index==1) + direct1=1; +else + direct1=0; +end + +Last=size(lengt,2); +LastM=size(middle,2); +LastL=size(lengt,2); + +while(k<=number) + w=0; + LastH=size(high,2); + if((high(LastH)~=high(LastH-1)) & Element==0) + while(branch==0) + S=0; + w=w+1; + ww=ww+1; + + if(over==0) + middle=cat(2,middle,middle(LastM)-(lengt(LastL)/2)); + end + + if(righttree==1) + middle=cat(2,middle,middle(LastM)-(lengt(LastL)/2)); + righttree=0; + end + + if(Sp==1) + LastM=size(middle,2); + LastL=size(lengt,2); + middle(LastM)=middle(LastM)+lengt(LastL)/2; + Sp=0; + S=1; + end + LastM=size(middle,2); + + Output=maxim-Prevmax; + rectangle('Position',[middle(LastM),Prevmax,0.0001,Output]); + + if(Betweenmax==Postmax | over==0) + Post2max=Postmax; + Post2index=Postindex; + end + + if((Betweenmax==Postmax | over==0) & (right~=1 | over==0)) + Postmax=maxim; + Postindex=index; + end + + maxim=Prevmax; + index=Previndex; + + if(NbanFirst<=(index-1)) + [Prevmax,Previndex]=max(Heights(NbanFirst:(index-1))); + Previndex=Previndex+NbanFirst-1; + else + Prevmax=maxim; + Previndex=index; + end + + if(index+1<=Postindex-1) + [Betweenmax,Betweenindex]=max(Heights(index+1:Postindex-1)); + Betweenindex=Betweenindex+index+1-1; + else + Betweenmax=Postmax; + Betweenindex=Postindex; + end + + if(right==1 & over~=0) + high=cat(2,high,[Prevmax Prevmax]); + indices=cat(2,indices,[Previndex Previndex]); + else + high=cat(2,high,Prevmax); + indices=cat(2,indices,Previndex); + end + + if(Postindex-Previndex<=0) + lengt=cat(2,lengt,1); + else + if(Postindex-Previndex-direct111>0) + if(ww==1) + lengt=cat(2,lengt,Postindex-Previndex-direct111+2); + else + lengt=cat(2,lengt,Postindex-Previndex-direct111); + end + else + if(ww==1) + lengt=cat(2,lengt,Postindex-Previndex+2); + else + lengt=cat(2,lengt,Postindex-Previndex); + end + end + end + L=cat(2,L,maxim); + + if(direct111~=0) + direct111=0; + end + + LastH=size(high,2); + LastL=size(lengt,2); + LastM=size(middle,2); + + + rectangle('Position',[middle(LastM)-(lengt(LastL)/2),maxim,lengt(LastL),0.0001]); + + if(over~=0) + over=0; + end + + if(right~=0) + right=0; + end + + if(NbanFirst==index) + branch=1; + end + end + + rectangle('Position',[middle(LastM)-(lengt(LastL)/2),maxim-1+extrawaystick,0.0001,1-extrawaystick]); + if(objectorder(k)>10) + + text(middle(LastM)-(lengt(LastL)/2)-0.25,maxim-2.0+extrawaytext,num2str(double(objectorder(k)))); + T=middle(LastM)-(lengt(LastL)/2-0.25); + else + + text(middle(LastM)-(lengt(LastL)/2)-0.1,maxim-2.0+extrawaytext,num2str(double(objectorder(k)))); + T=middle(LastM)-(lengt(LastL)/2-0.1); + end + + Maxi=max(Maxi,T); + Mini=min(Mini,T); + + if(Betweenmax~=Postmax) + high=high(1:LastH-2); + indices=indices(1:LastH-2); + + Prevmax=Betweenmax; + Previndex=Betweenindex; + + high=cat(2,high,Prevmax); + indices=cat(2,indices,Previndex); + LastH=size(high,2); + + LastL=size(lengt,2); + middle(LastM)=middle(LastM)+lengt(LastL)/2; + over=1; + end + direct1=0; + Special=0; + Spec=0; + else + if(Special==1) + Special=0; + Spec=1; + end + + if(Sp==1) + Sp=0; + end + + if(S==1) + S=2; + end + + direct111=0; + LastH=size(high,2); + LastM=size(middle,2); + LastL=size(lengt,2); + + if(direct1==0) + + rectangle('Position',[middle(LastM)+(lengt(LastL)/2),high(LastH)-1+extrawaystick,0.0001,1-extrawaystick]); + + text(middle(LastM)+lengt(LastL)/2-0.1,high(LastH)-2.0+extrawaytext,num2str(double(objectorder(k)))); + + T=middle(LastM)+lengt(LastL)/2-0.1; + Maxi=max(Maxi,T); + Mini=min(Mini,T); + else + + rectangle('Position',[middle(LastM)-(lengt(LastL)/2),high(LastH)-1+extrawaystick,0.0001,1-extrawaystick]); + + text(middle(LastM)-lengt(LastL)/2-0.1,high(LastH)-2.0+extrawaytext,num2str(double(objectorder(k)))); + + T=middle(LastM)-lengt(LastL)/2-0.1; + Maxi=max(Maxi,T); + Mini=min(Mini,T); + direct1=0; + direct11=1; + end + + if(high(LastH-1)==high(LastH)) + high=high(1:LastH-2); + indices=indices(1:LastH-2); + else + high=high(1:LastH-1); + indices=indices(1:LastH-1); + end + LastH=size(high,2); + + maxim=high(LastH); + index=indices(LastH); + + if(LastH-1>=1) + Postmax=high(LastH-1); + Postindex=indices(LastH-1); + end + + if(LastH-2>=1) + Post2max=high(LastH-2); + Post2index=indices(LastH-2); + end + + if(index+1<=Postindex-1) + [Betweenmax,Betweenindex]=max(Heights(index+1:Postindex-1)); + Betweenindex=Betweenindex+index+1-1; + else + Betweenmax=Postmax; + Betweenindex=Postindex; + end + + if(Betweenmax~=Postmax) + Prevmax=Betweenmax; + Previndex=Betweenindex; + else + Prevmax=maxim; + Previndex=index; + end + + if(high(LastH)>high(LastH-1)) + righttree=1; + end + + high(LastH)=Prevmax; + indices(LastH)=Previndex; + + LastM=size(middle,2); + LastL=size(lengt,2); + Last=size(L,2); + + if(LastM-1>=1) + middle=middle(1:LastM-1); + end + + if(LastL-1>=1) + lengt=lengt(1:LastL-1); + end + + if(Last-1>=1) + L=L(1:Last-1); + end + + LastM=size(middle,2); + LastL=size(lengt,2); + Last=size(L,2); + + if(Last>0) + while(maxim>L(Last)) + lengt=lengt(1:Last-1); + L=L(1:Last-1); + Last=size(L,2); + end + end + LastL=size(lengt,2); + + middle(LastM)=middle(LastM)+(lengt(LastL)/2); + + if(Betweenmax~=Postmax) + Element=0; + else + Element=1; + middle(LastM)=middle(LastM)-(lengt(LastL)/2); + end + + if(righttree==1 | direct11==1) + if(Spec==1) + Sp=1; + end + + PrevMAX=L(1); + [maxim,index]=max(Heights(NbanFirst+1:number-1)); + index=index+NbanFirst+1-1; + + if(NbanFirst+1<=index-1) + [Prevmax,Previndex]=max(Heights(NbanFirst+1:index-1)); + Previndex=Previndex+NbanFirst+1-1; + else + Prevmax=maxim; + Previndex=index; + end + + if(index+1<=number-1) + [Postmax,Postindex]=max(Heights(index+1:number-1)); + Postindex=Postindex+index+1-1; + else + Postmax=maxim; + Postindex=index; + end + + if(index+1<=Postindex-1) + [Betweenmax,Betweenindex]=max(Heights(index+1:Postindex-1)); + Betweenindex=Betweenindex+index+1-1; + else + Betweenmax=Postmax; + Betweenindex=Postindex; + end + + if(Postindex+1<=number-1) + [Post2max,Post2index]=max(Heights(Postindex+1:number-1)); + Post2index=Post2index+Postindex+1-1; + else + Post2max=Postmax; + Post2index=Postindex; + end + + + rectangle('Position',[PrevMID+(PrevLEN/2),maxim,0.0001,PrevMAX-maxim]); + + LastM=size(middle,2); + LastMID=PrevMID+(PrevLEN/2); + PrevMID=LastMID; + middle=[]; + middle(1)=LastMID; + LastM=size(middle,2); + + high=[]; + indices=[]; + high=cat(2,high,[Post2max,Postmax,Betweenmax,maxim,Prevmax]); + + indices=cat(2,indices,[Post2index,Postindex,Betweenindex,index,Previndex]); + LastH=size(high,2); + + L=[]; + L=cat(2,L,maxim); + Last=size(L,2); + lengt=[]; + if(Postindex>Previndex) + if(M10) + + text(LastMID-1/2-0.25,maxim-2.0+extrawaytext,num2str(double(objectorder(k+1)))); + Mini=min(Mini,LastMID-1/2-0.25); + else + + text(LastMID-1/2-0.1,maxim-2.0+extrawaytext,num2str(double(objectorder(k+1)))); + Mini=min(Mini,LastMID-1/2-0.1); + end + + rectangle('Position',[LastMID+1/2,maxim-1+extrawaystick,0.0001,1-extrawaystick]); + + text(LastMID+1/2-0.1,maxim-2.0+extrawaytext,num2str(double(objectorder(k+2)))); + Maxi=max(Maxi,LastMID+1/2-0.1); + + k=k+2; + end + PrevLEN=lengt(1); + LastL=size(lengt,2); + + if(Element==1) + Element=0; + end + + if(direct11==1) + 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 literal 0 HcmV?d00001 diff --git a/LIBRA/twopoints.m b/LIBRA/twopoints.m new file mode 100644 index 0000000..2219e3f --- /dev/null +++ b/LIBRA/twopoints.m @@ -0,0 +1,59 @@ +function result=twopoints(data,ndirect,seed) + +%TWOPOINTS calculates ndirect directions through two randomly chosen data points from data. +% If ndirect is larger than the number of all possible directions, then all +% these combinations are considered. +% +% Required input arguments: +% data : Data matrix +% ndirect : Number of directions through two random data points that +% needs to be constructed +% +% Optional input arguments: +% seed : To define the state of the generator (default=0) +% (0 sets the generator to its default initial state) +% +%I/O: +% result=twopoints(x,250,0); +% +% Output arguments: +% result : matrix containing the ndirect directions (each row is a +% direction) +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Last modified: 09/06/2008 + + if nargin==2 + seed=0; + end + +[n,p]=size(data); +nrich1=n*(n-1)/2; +ndirect=min(ndirect,nrich1); +true = (ndirect == nrich1); +B=zeros(ndirect,p); +if true + perm=[1 1]; +end +k=1; +for ndir=1:ndirect + if true + k1=2; + perm(k1)=perm(k1)+1; + while ~(k1==1 || perm(k1) <=(n-(k+1-k1))) + k1=k1-1; + perm(k1)=perm(k1)+1; + for j=(k1+1):k+1 + perm(j)=perm(j-1)+1; + end + end + index=perm; % index : contains trial subsample. + else + [index,seed]=randomset(n,2,seed); + end + B(ndir,:)=data(index(1),:)-data(index(2),:); +end +result=B; \ No newline at end of file diff --git a/LIBRA/unimcd.m b/LIBRA/unimcd.m new file mode 100644 index 0000000..ebb32de --- /dev/null +++ b/LIBRA/unimcd.m @@ -0,0 +1,71 @@ +function [tmcd,smcd,weights,initmean,initcov,res,raww,Hopt]=unimcd(y,h) + +%UNIMCD computes the MCD estimator of a univariate data set. This +% estimator is given by the subset of h observations with smallest variance. +% The MCD location estimate is then the mean of those h points, +% and the MCD scale estimate is their standard deviation. Based on these raw estimates, +% a reweigthing step is applied as in the FASTMCD algorithm (see mcdcov.m). +% We recommend to use the function mcdcov.m, which calls unimcd.m. +% +% The MCD method was introduced in: +% +% Rousseeuw, P.J. (1984), "Least Median of Squares Regression," +% Journal of the American Statistical Association, Vol. 79, pp. 871-881. +% +% The algorithm to compute the univariate MCD is described in +% +% Rousseeuw, P.J., Leroy, A., (1988), "Robust Regression and Outlier Detection," +% John Wiley, New York. +% +% This function is part of LIBRA: the Matlab Library for Robust Analysis, +% available at: +% http://wis.kuleuven.be/stat/robust.html +% +% Written by: Katrien Van Driessen +%Revision by S. Verboven +%Last update + +ncas=length(y); +len=ncas-h+1; +if len==1 + tmcd=mean(y); + smcd=sqrt(var(y)); + weights=ones(length(y),1); +else + [y,I]=sort(y); + ay(1)=sum(y(1:h)); + for samp=2:len + ay(samp)=ay(samp-1)-y(samp-1)+y(samp+h-1); + end + ay2=ay.^2/h; + sq(1)=sum(y(1:h).^2)-ay2(1); + for samp=2:len + sq(samp)=sq(samp-1)-y(samp-1)^2+y(samp+h-1)^2-ay2(samp)+ay2(samp-1); + end + sqmin=min(sq); + ii=find(sq==sqmin); + Hopt = I(ii:ii+h-1); + ndup=length(ii); + slutn(1:ndup)=ay(ii); + initmean=slutn(floor((ndup+1)/2))/h; %initial mean + initcov=sqmin/(h-1); %initial variance + % calculating consistency factor + res=(y-initmean).^2/initcov; + sortres=sort(res); + factor=sortres(h)/chi2inv(h/ncas,1); + initcov=factor*initcov; + res=(y-initmean).^2/initcov; %raw_robdist^2 + quantile=chi2inv(0.975,1); + weights=res 1; X = X'; end +if size(Y,1) == 1 && size(Y,2) > 1; Y = Y'; end + +if size(X,2) == 1 && size(Y,2) > 1 + X = repmat(X,1,size(Y,2)); +elseif size(Y,2) == 1 && size(X,2) > 1 + Y = repmat(Y,1,size(X,2)); +end + +if sum(size(X)~=size(Y)) ~= 0 + error('X and Y must have the same size') +end + +%% parameters +if nargin < 2 + error('two inputs requested'); +elseif nargin == 2 + fig_flag = 1; + level = 5/100; +elseif nargin == 3 + level = 5/100; +end + +[n p] = size(X); +if p==1 + [X Y]=pairwise_cleanup(X,Y); + [n p] = size(X); +end + +%% basic Pearson + +% compute r +r = nansum(demean(X).*demean(Y)) ./ ... + (nansum(demean(X).^2).*nansum(demean(Y).^2)).^(1/2); +t = r.*sqrt((n-2)./(1-r.^2)); +pval = 2*tcdf(-abs(t),n-2); + +%% bootstrap +if nargout > 3 + % adjust boot parameters + if p == 1 + nboot = 599; + % adjust percentiles following Wilcox + % even if NaN n is fine since data have + % been cleaned up + if n < 40 + low = 7 ; high = 593; + elseif n >= 40 && n < 80 + low = 8 ; high = 592; + elseif n >= 80 && n < 180 + low = 11 ; high = 588; + elseif n >= 180 && n < 250 + low = 14 ; high = 585; + elseif n >= 250 + low = 15 ; high = 584; + end + + else + nboot = 1000; + level = level / p; + % Bonferonni correction + low = round((level*nboot)/2); + if low == 0 + error('adjusted CI cannot be computed, too many tests for the number of observations') + else + high = nboot - low; + end + end + + % compute hboot and CI + table = randi(n,n,nboot); + for B=1:nboot + rb(B,:) = nansum(demean(X(table(:,B),:)).*demean(Y(table(:,B),:))) ./ ... + (nansum(demean(X(table(:,B),:)).^2).*nansum(demean(Y(table(:,B),:)).^2)).^(1/2); + if fig_flag ~= 0 % to make a nice figure get regression values + for c=1:size(X,2) + tmp = [X(table(:,B),c) Y(table(:,B),c)]; % take a pair + tmp(find(sum(isnan(tmp),2)),:) = []; % remove NaNs + b = pinv([tmp(:,1) ones(size(tmp,1),1)])*tmp(:,2); % solve + slope(B,c) = b(1); intercept(B,c) = b(2,:); + end + end + end + + rb = sort(rb,1); + [slope,index] = sort(slope,1); + % in theory we keep the slope/intercept pair, thus: + % intercept = intercept(index); % but doesn't work? + intercept = sort(intercept,1); + + % CI and h + adj_nboot = nboot - sum(isnan(rb)); + adj_low = round((level*adj_nboot)/2); + adj_high = adj_nboot - adj_low; + + for c=1:size(X,2) + CI(:,c) = [rb(adj_low(c),c) ; rb(adj_high(c),c)]; + hboot(c) = (rb(adj_low(c),c) > 0) + (rb(adj_high(c),c) < 0); + CIslope(:,c) = [slope(adj_low(c),c) ; slope(adj_high(c),c)]; + CIintercept(:,c) = [intercept(adj_low(c),c) ; intercept(adj_high(c),c)]; + end +end + +%% plots +if fig_flag ~= 0 + answer = []; + if p > 1 + answer = questdlg(['plots all ' num2str(p) ' correlations'],'Plotting option','yes','no','yes'); + else + if fig_flag == 1 + figure('Name','Pearson correlation'); + set(gcf,'Color','w'); + end + + if nargout>3 + subplot(1,2,1); + M = sprintf('Pearson corr r=%g \n %g%%CI [%g %g]',r,(1-level)*100,CI(1),CI(2)); + else + M = sprintf('Pearson corr r=%g \n p=%g',r,pval); + end + + scatter(X,Y,100,'filled'); grid on + xlabel('X','FontSize',14); ylabel('Y','FontSize',14); + title(M,'FontSize',16); + h=lsline; set(h,'Color','r','LineWidth',4); + box on;set(gca,'Fontsize',14) + + if nargout>3 % if bootstrap done plot CI + y1 = refline(CIslope(1),CIintercept(1)); set(y1,'Color','r'); + y2 = refline(CIslope(2),CIintercept(2)); set(y2,'Color','r'); + y1 = get(y1); y2 = get(y2); + xpoints=[[y1.XData(1):y1.XData(2)],[y2.XData(2):-1:y2.XData(1)]]; + step1 = y1.YData(2)-y1.YData(1); step1 = step1 / (y1.XData(2)-y1.XData(1)); + step2 = y2.YData(2)-y2.YData(1); step2 = step2 / (y2.XData(2)-y2.XData(1)); + filled=[[y1.YData(1):step1:y1.YData(2)],[y2.YData(2):-step2:y2.YData(1)]]; + hold on; fillhandle=fill(xpoints,filled,[1 0 0]); + set(fillhandle,'EdgeColor',[1 0 0],'FaceAlpha',0.2,'EdgeAlpha',0.8);%set edge color + + subplot(1,2,2); k = round(1 + log2(length(rb))); hist(rb,k); grid on; + title({'Bootstrapped correlations';['h=',num2str(hboot)]},'FontSize',16); hold on + xlabel('boot correlations','FontSize',14);ylabel('frequency','FontSize',14) + plot(repmat(CI(1),max(hist(rb,k)),1),[1:max(hist(rb,k))],'r','LineWidth',4); + plot(repmat(CI(2),max(hist(rb,k)),1),[1:max(hist(rb,k))],'r','LineWidth',4); + axis tight; colormap([.4 .4 1]) + box on;set(gca,'Fontsize',14) + end + end + + if strcmp(answer,'yes') + for f = 1:p + if fig_flag == 1 + figure('Name',[num2str(f) ' boostrapped Pearson correlation']) + set(gcf,'Color','w'); + end + + if nargout>3 + subplot(1,2,1); + M = sprintf('Pearson corr r=%g \n %g%%CI [%g %g]',r(f),(1-level)*100,CI(1,f),CI(2,f)); + else + M = sprintf('Pearson corr r=%g p=%g',r(f),pval(f)); + end + + scatter(X(:,f),Y(:,f),100,'filled'); grid on + xlabel('X','FontSize',14); ylabel('Y','FontSize',14); + title(M,'FontSize',16); + h=lsline; set(h,'Color','r','LineWidth',4); + box on;set(gca,'Fontsize',14) + + if nargout>3 + y1 = refline(CIslope(1,f),CIintercept(1,f)); set(y1,'Color','r'); + y2 = refline(CIslope(2,f),CIintercept(2,f)); set(y2,'Color','r'); + y1 = get(y1); y2 = get(y2); + xpoints=[[y1.XData(1):y1.XData(2)],[y2.XData(2):-1:y2.XData(1)]]; + step1 = y1.YData(2)-y1.YData(1); step1 = step1 / (y1.XData(2)-y1.XData(1)); + step2 = y2.YData(2)-y2.YData(1); step2 = step2 / (y2.XData(2)-y2.XData(1)); + filled=[[y1.YData(1):step1:y1.YData(2)],[y2.YData(2):-step2:y2.YData(1)]]; + hold on; fillhandle=fill(xpoints,filled,[1 0 0]); + set(fillhandle,'EdgeColor',[1 0 0],'FaceAlpha',0.2,'EdgeAlpha',0.8);%set edge color + + subplot(1,2,2); k = round(1 + log2(length(rb(:,f)))); hist(rb(:,f),k); grid on; + title({'Bootstrapped correlations';['h=',num2str(hboot(f))]},'FontSize',16); hold on + xlabel('boot correlations','FontSize',14);ylabel('frequency','FontSize',14) + plot(repmat(CI(1,f),max(hist(rb(:,f),k)),1),[1:max(hist(rb(:,f),k))],'r','LineWidth',4); + plot(repmat(CI(2,f),max(hist(rb(:,f),k)),1),[1:max(hist(rb(:,f),k))],'r','LineWidth',4); + axis tight; colormap([.4 .4 1]) + box on;set(gca,'Fontsize',14) + end + end + end +end + + + + + diff --git a/Spearman.m b/Spearman.m new file mode 100644 index 0000000..390d9a9 --- /dev/null +++ b/Spearman.m @@ -0,0 +1,222 @@ +function [r,t,pval,hboot,CI] = Spearman(X,Y,fig_flag,level) + +% Computes the Spearman correlation with its bootstrap CI. +% +% FORMAT: [r,t,p] = Spearman(X,Y) +% [r,t,p] = Spearman(X,Y,fig_flag,level) +% [r,t,p,hboot,CI] = Spearman(X,Y,fig_flag,level) +% +% INPUTS: X and Y are 2 vectors or matrices, in the latter case, +% correlations are computed column-wise +% fig_flag indicates to plot (1 - default) the data or not (0) +% level is the desired alpha level (5/100 is the default) +% +% OUTPUTS: r is the Spearman correlation +% t is the associated t value +% pval is the corresponding p value +% hboot 1/0 declares the test significant based on CI +% CI is the percentile bootstrap confidence interval +% +% If X and Y are matrices of size [n p], p correlations are computed +% and the CIs are adjusted at the alpha/p level (Bonferonni +% correction); hboot is based on these adjusted CIs but pval remains +% uncorrected - note also that if some values are NaN, the adjustement +% is based on the largest n +% +% This function requires the tiedrank.m and nansum.m functions +% from the matlab stat toolbox. +% +% Cyril Pernet v2 (10-01-2014 - deals with NaN) +% --------------------------------------------- +% Copyright (C) Corr_toolbox 2012 + +%% data check + +% if X a vector and Y a matrix, +% repmat X to perform multiple tests on Y (or the other around) +if size(X,1) == 1 && size(X,2) > 1; X = X'; end +if size(Y,1) == 1 && size(Y,2) > 1; Y = Y'; end + +if size(X,2) == 1 && size(Y,2) > 1 + X = repmat(X,1,size(Y,2)); +elseif size(Y,2) == 1 && size(X,2) > 1 + Y = repmat(Y,1,size(X,2)); +end + +if sum(size(X)~=size(Y)) ~= 0 + error('X and Y must have the same size') +end + +%% parameters +if nargin < 2 + error('two inputs requested'); +elseif nargin == 2 + fig_flag = 1; + level = 5/100; +elseif nargin == 3 + level = 5/100; +end + +[n p] = size(X); +if p==1 + [X Y]=pairwise_cleanup(X,Y); + [n p] = size(X); +end + +%% basic Spearman + +% The corr function in the stat toolbox uses +% permutations for n<10 and some other fancy +% things when n>10 and there are no ties among +% ranks - we just do the standard way. + +% compute r (default) +xrank = tiedrank(X,0); +yrank = tiedrank(Y,0); +r = nansum(demean(xrank).*demean(yrank)) ./ ... + (nansum(demean(xrank).^2).*nansum(demean(yrank).^2)).^(1/2); +t = r.*(sqrt(n-2)) ./ sqrt((1-r.^2)); +pval = 2*tcdf(-abs(t),n-2); + +%% bootstrap +if nargout > 3 + nboot = 1000; + if p > 1 + level = level / p; + end + low = round((level*nboot)/2); + if low == 0 + error('adjusted CI cannot be computed, too many tests for the number of observations') + else + high = nboot - low; + end + + % bootstrap + table = randi(n,n,nboot); + for B=1:nboot + xrank = tiedrank(X(table(:,B),:),0); + yrank = tiedrank(Y(table(:,B),:),0); + rb(B,:) = nansum(demean(xrank).*demean(yrank)) ./ ... + (nansum(demean(xrank).^2).*nansum(demean(yrank).^2)).^(1/2); + if fig_flag ~= 0 % to make a nice figure get regression values + for c=1:size(X,2) + tmp = [xrank(:,c) yrank(:,c)]; % take a pair + tmp(find(sum(isnan(tmp),2)),:) = []; % remove NaNs + b = pinv([tmp(:,1) ones(size(tmp,1),1)])*tmp(:,2); % solve + slope(B,c) = b(1); intercept(B,c) = b(2,:); + end + end + end + + rb = sort(rb,1); + slope = sort(slope,1); + intercept = sort(intercept,1); + + % CI and h + adj_nboot = nboot - sum(isnan(rb)); + adj_low = round((level*adj_nboot)/2); + adj_high = adj_nboot - adj_low; + + for c=1:size(X,2) + CI(:,c) = [rb(adj_low(c),c) ; rb(adj_high(c),c)]; + hboot(c) = (rb(adj_low(c),c) > 0) + (rb(adj_high(c),c) < 0); + CIslope(:,c) = [slope(adj_low(c),c) ; slope(adj_high(c),c)]; + CIintercept(:,c) = [intercept(adj_low(c),c) ; intercept(adj_high(c),c)]; + end +end + +%% plots +if fig_flag ~= 0 + answer = []; + if p > 1 + answer = questdlg(['plots all ' num2str(p) ' correlations'],'Plotting option','yes','no','yes'); + else + if fig_flag == 1 + figure('Name','Spearman correlation'); + set(gcf,'Color','w'); + end + + if nargout>3 + subplot(1,2,1); + M = sprintf('Spearman corr r=%g \n %g%%CI [%g %g]',r,(1-level)*100,CI(1),CI(2)); + else + M = sprintf('Spearman corr r=%g p=%g',r,pval); + end + + scatter(xrank,yrank,100,'filled'); grid on + xlabel('X Rank','FontSize',14); ylabel('Y Rank','FontSize',14); + title(M,'FontSize',16); + h=lsline; set(h,'Color','r','LineWidth',4); + box on;set(gca,'FontSize',14,'Layer','Top') + + if nargout >3 + y1 = refline(CIslope(1),CIintercept(1)); set(y1,'Color','r'); + y2 = refline(CIslope(2),CIintercept(2)); set(y2,'Color','r'); + y1 = get(y1); y2 = get(y2); + xpoints=[[y1.XData(1):y1.XData(2)],[y2.XData(2):-1:y2.XData(1)]]; + step1 = y1.YData(2)-y1.YData(1); step1 = step1 / (y1.XData(2)-y1.XData(1)); + step2 = y2.YData(2)-y2.YData(1); step2 = step2 / (y2.XData(2)-y2.XData(1)); + filled=[[y1.YData(1):step1:y1.YData(2)],[y2.YData(2):-step2:y2.YData(1)]]; + hold on; fillhandle=fill(xpoints,filled,[1 0 0]); + set(fillhandle,'EdgeColor',[1 0 0],'FaceAlpha',0.2,'EdgeAlpha',0.8);%set edge color + box on;set(gca,'FontSize',14) + + subplot(1,2,2); hold on + k = round(1 + log2(length(rb))); hist(rb,k); grid on; + title({'Bootstrapped correlations';['h=',num2str(hboot)]},'FontSize',16) + xlabel('boot correlations','FontSize',14);ylabel('frequency','FontSize',14) + plot(repmat(CI(1),max(hist(rb,k)),1),[1:max(hist(rb,k))],'r','LineWidth',4); + plot(repmat(CI(2),max(hist(rb,k)),1),[1:max(hist(rb,k))],'r','LineWidth',4); + axis tight; colormap([.4 .4 1]) + box on;set(gca,'FontSize',14,'Layer','Top') + end + end + + if strcmp(answer,'yes') + for f = 1:p + if fig_flag == 1 + figure('Color','w','Name',[num2str(f) ' Spearman correlation']) + end + + if nargout >3 + subplot(1,2,1); + M = sprintf('Spearman corr r=%g \n %g%%CI [%g %g]',r(f),(1-level)*100,CI(1,f),CI(2,f)); + else + M = sprintf('Spearman corr r=%g p=%g',r(f),pval(f)); + end + + scatter(xrank(:,f),yrank(:,f),100,'filled'); grid on + xlabel('X Rank','FontSize',14); ylabel('Y Rank','FontSize',14); + title(M,'FontSize',16); + h=lsline; set(h,'Color','r','LineWidth',4); + + if nargout >3 + y1 = refline(CIslope(1,f),CIintercept(1,f)); set(y1,'Color','r'); + y2 = refline(CIslope(2,f),CIintercept(2,f)); set(y2,'Color','r'); + y1 = get(y1); y2 = get(y2); + xpoints=[[y1.XData(1):y1.XData(2)],[y2.XData(2):-1:y2.XData(1)]]; + step1 = y1.YData(2)-y1.YData(1); step1 = step1 / (y1.XData(2)-y1.XData(1)); + step2 = y2.YData(2)-y2.YData(1); step2 = step2 / (y2.XData(2)-y2.XData(1)); + filled=[[y1.YData(1):step1:y1.YData(2)],[y2.YData(2):-step2:y2.YData(1)]]; + hold on; fillhandle=fill(xpoints,filled,[1 0 0]); + set(fillhandle,'EdgeColor',[1 0 0],'FaceAlpha',0.2,'EdgeAlpha',0.8);%set edge color + box on;set(gca,'FontSize',14) + + subplot(1,2,2); hold on + k = round(1 + log2(length(rb(:,f)))); hist(rb(:,f),k); grid on; + plot(repmat(CI(1,f),max(hist(rb(:,f),k)),1),[1:max(hist(rb(:,f),k))],'r','LineWidth',4); + plot(repmat(CI(2,f),max(hist(rb(:,f),k)),1),[1:max(hist(rb(:,f),k))],'r','LineWidth',4); + title({'Bootstrapped correlations';['h=',num2str(hboot(f))]},'FontSize',16) + xlabel('boot correlations','FontSize',14);ylabel('frequency','FontSize',14) + axis tight; colormap([.4 .4 1]) + box on;set(gca,'FontSize',14,'Layer','Top') + end + end + end +end + + + + + + diff --git a/bendcorr.m b/bendcorr.m new file mode 100644 index 0000000..f36ef59 --- /dev/null +++ b/bendcorr.m @@ -0,0 +1,308 @@ +function [r,t,p,hboot,CI,H,pH] = bendcorr(X,Y,fig_flag,beta) + +% Computes the percentage bend correlation along with the bootstrap CI +% +% FORMAT: [r,t,p] = bendcorr(X,Y) +% [r,t,p,hboot,CI,H,pH] = bendcorr(X,Y,fig_flag,beta) +% +% INPUTS: X and Y are 2 vectors or matrices. In the latter case, +% correlations are computed column-wise. +% fig_flag indicates to plot (1 - default) the data or not (0) +% beta represents the amount of trimming: 0 <= beta <= 0.5 +% (beta is also called the bending constant for omega - default = 0.2) +% +% OUTPUTS: r is the percentage bend correlation +% t is the associated t value +% pval is the corresponding p value +% hboot 1/0 declares the test significant based on CI +% CI is the percentile bootstrap confidence interval +% H is the measure of association between all pairs +% pH is the p value for an omnibus test of independence between all pairs +% +% The percentage bend correlation is a robust method that protects against +% outliers among the marginal distributions. If NaN values are present, the +% smallest available n is used across all pairs. +% +% Cyril Pernet and Guillaume Rousselet 26-01-2011 +% Reformatted for Corr_toolbox 02-7-2012 +% ---------------------------------------------- +% Copyright (C) Corr_toolbox 2012 + +%% data check + +if nargin<2 + error('two input vectors requested') +elseif nargin>4 + eror('too many inputs') +end + +% if X a vector and Y a matrix, +% repmat X to perform multiple tests on Y (or the other around) +if size(X,1) == 1 && size(X,2) > 1; X = X'; end +if size(Y,1) == 1 && size(Y,2) > 1; Y = Y'; end + +if size(X,2) == 1 && size(Y,2) > 1 + X = repmat(X,1,size(Y,2)); +elseif size(Y,2) == 1 && size(X,2) > 1 + Y = repmat(Y,1,size(X,2)); +end + +if sum(size(X)~=size(Y)) ~= 0 + error('X and Y must have the same size') +end + +%% parameters +level = 5/100; +if nargin < 2 + error('two inputs requested'); +elseif nargin == 2 + fig_flag = 1; + beta = 0.2; +elseif nargin == 3 + beta = 0.2; +end + +if beta>1 + beta = beta/100; +end + +if beta<0 || beta>.5 + error('beta must be between 0 and 50%') +end + +% remove NaNs +% ----------- +X = [X Y]; +X(find(sum(isnan(X),2)),:) = []; +n = length(X); + +%% compute +% -------- +if nargout > 5 + [r,t,p,XX,YY,H,pH] = bend_compute(X,beta); +else + [r,t,p,XX,YY] = bend_compute(X,beta); +end + +if nargout > 3 + % bootstrap + % ----------- + nboot = 1000; + level = level / (size(X,2)/2); + + low = round((level*nboot)/2); + if low == 0 + error('adjusted CI cannot be computed, too many tests for the number of observations') + else + high = nboot - low; + end + + table = randi(n,n,nboot); + for B=1:nboot + tmp = X(table(:,B),:); + rb(B,:) = bend_compute(tmp,beta); + for c=1:size(X,2)/2 + coef = pinv([tmp(:,c) ones(n,1)])*tmp(:,c+size(X,2)/2); + intercept(B,c) = coef(2); + slope(B,c) = rb(B,c) / (std(tmp(:,c))/std(tmp(:,c+size(X,2)/2))); + end + end + + rb = sort(rb); + slope = sort(slope,1); + intercept = sort(intercept,1); + + % CI and h + adj_nboot = nboot - sum(isnan(rb)); + adj_low = round((level*adj_nboot)/2); + adj_high = adj_nboot - adj_low; + + for c=1:size(X,2)/2 + CI(:,c) = [rb(adj_low(c),c) ; rb(adj_high(c),c)]; + hboot(c) = (rb(adj_low(c),c) > 0) + (rb(adj_high(c),c) < 0); + CIslope(:,c) = [slope(adj_low(c),c) ; slope(adj_high(c),c)]; + CIintercept(:,c) = [intercept(adj_low(c),c) ; intercept(adj_high(c),c)]; + end +end + +%% plot +% ----- +if fig_flag ~= 0 + answer = []; + if size(r,1) > 1 + answer = questdlg('plots all correlations','Plotting option','yes','no','yes'); + else + if fig_flag == 1 + figure('Name','Bend correlation'); + set(gcf,'Color','w'); + end + + if nargout >3 + subplot(1,2,1); + M = sprintf('Bend corr r=%g \n %g%%CI [%g %g]',r,(1-level)*100,rb(low),rb(high)); + else + M = sprintf('Bend corr r=%g \n p=%g',r,p); + end + + scatter(X(:,1),X(:,2),100,'filled'); + hold on; grid on; + % plot 'outliers' + scatter(X(XX{1},1),X(XX{1},2),110,'r','LineWidth',3); + scatter(X(YY{1},1),X(YY{1},2),110,'g','LineWidth',3); + scatter(X(intersect(XX{1},YY{1}),1),X(intersect(XX{1},YY{1}),2),110,'k','LineWidth',3); + xlabel('X bend','FontSize',12); ylabel('Y bend','FontSize',12); + title(M,'FontSize',16); + coef = pinv([X(:,1) ones(n,1)])*X(:,2); + s = r / (std(X(:,1))/std(X(:,2))); + h = refline(s,coef(2)); set(h,'Color','r','LineWidth',4); + box on;set(gca,'FontSize',14) + + if nargout>3 + if sum(slope == 0) == 0 + y1 = refline(CIslope(1),CIintercept(1)); set(y1,'Color','r'); + y2 = refline(CIslope(2),CIintercept(2)); set(y2,'Color','r'); + y1 = get(y1); y2 = get(y2); + xpoints=[[y1.XData(1):y1.XData(2)],[y2.XData(2):-1:y2.XData(1)]]; + step1 = y1.YData(2)-y1.YData(1); step1 = step1 / (y1.XData(2)-y1.XData(1)); + step2 = y2.YData(2)-y2.YData(1); step2 = step2 / (y2.XData(2)-y2.XData(1)); + filled=[[y1.YData(1):step1:y1.YData(2)],[y2.YData(2):-step2:y2.YData(1)]]; + hold on; fillhandle=fill(xpoints,filled,[1 0 0]); + set(fillhandle,'EdgeColor',[1 0 0],'FaceAlpha',0.2,'EdgeAlpha',0.8);%set edge color + box on;set(gca,'FontSize',14) + end + + subplot(1,2,2); k = round(1 + log2(length(rb))); hist(rb,k); grid on; + title({'Bootstrapped correlations';['h=' num2str(hboot)]},'FontSize',16); hold on + xlabel('boot correlations','FontSize',14);ylabel('frequency','FontSize',14) + plot(repmat(CI(1),max(hist(rb,k)),1),[1:max(hist(rb,k))],'r','LineWidth',4); + plot(repmat(CI(2),max(hist(rb,k)),1),[1:max(hist(rb,k))],'r','LineWidth',4); + axis tight; colormap([.4 .4 1]) + box on;set(gca,'FontSize',14,'Layer','Top') + end + end + + if strcmp(answer,'yes') + for f = 1:length(r) + if fig_flag == 1 + figure('Name',[num2str(f) ' boostrapped Bend correlation']); + set(gcf,'Color','w'); + end + + if nargout > 3 + subplot(1,2,1); + M = sprintf('Bend corr r=%g \n %g%%CI [%g %g]',r(f),(1-level)*100,CI(1,f),CI(2,f)); + else + M = sprintf('Bend corr r=%g p=%g',r(f),p(f)); + end + + scatter(X(:,f),X(:,f+size(X,2)/2),100,'b','filled'); + hold on; grid on; + % plot 'outliers' + scatter(X(XX{f},f),X(XX{f},f+size(X,2)/2),110,'r','LineWidth',3); + scatter(X(YY{f},f),X(YY{f},f+size(X,2)/2),110,'g','LineWidth',3); + scatter(X(intersect(XX{f},YY{f}),f),X(intersect(XX{f},YY{f}),f+size(X,2)/2),110,'k','LineWidth',3); + xlabel('X bend','FontSize',12); ylabel('Y bend','FontSize',12); + title(M,'FontSize',16); + coef = pinv([X(:,f) ones(n,1)])*X(:,f+size(X,2)/2); + s = r(f) / (std(X(:,f))/std(X(:,f+size(X,2)/2))); + h = refline(s,coef(2)); set(h,'Color','r','LineWidth',4); + box on;set(gca,'FontSize',14,'Layer','Top') + + if nargout >3 + if sum(slope(:,f) == 0) == 0 + y1 = refline(CIslope(1,f),CIintercept(1,f)); set(y1,'Color','r'); + y2 = refline(CIslope(2,f),CIintercept(2,f)); set(y2,'Color','r'); + y1 = get(y1); y2 = get(y2); + xpoints=[[y1.XData(1):y1.XData(2)],[y2.XData(2):-1:y2.XData(1)]]; + step1 = y1.YData(2)-y1.YData(1); step1 = step1 / (y1.XData(2)-y1.XData(1)); + step2 = y2.YData(2)-y2.YData(1); step2 = step2 / (y2.XData(2)-y2.XData(1)); + filled=[[y1.YData(1):step1:y1.YData(2)],[y2.YData(2):-step2:y2.YData(1)]]; + hold on; fillhandle=fill(xpoints,filled,[1 0 0]); + set(fillhandle,'EdgeColor',[1 0 0],'FaceAlpha',0.2,'EdgeAlpha',0.8);%set edge color + box on;set(gca,'FontSize',14,'Layer','Top') + end + + subplot(1,2,2); k = round(1 + log2(size(rb,1))); hist(rb(:,f),k); grid on; + title({'Bootstrapped correlations';['h=',num2str(hboot(f))]},'FontSize',16); hold on + xlabel('boot correlations','FontSize',14);ylabel('frequency','FontSize',14) + plot(repmat(CI(1,f),max(hist(rb(:,f),k)),1),[1:max(hist(rb(:,f),k))],'r','LineWidth',4); + plot(repmat(CI(2,f),max(hist(rb(:,f),k)),1),[1:max(hist(rb(:,f),k))],'r','LineWidth',4); + axis tight; colormap([.4 .4 1]) + box on;set(gca,'FontSize',14,'Layer','Top') + end + end + end +end + +end + +function [r,t,p,XX,YY,H,pH] = bend_compute(X,beta) + +H= []; pH = []; + +%% Medians and absolute deviation from the medians +% --------------------------------------------- +M = repmat(median(X),size(X,1),1); +W = sort(abs(X-M),1); + +% limits +% ------- +m = floor((1-beta)*size(X,1)); +omega = W(m,:); + +%% Compute the correlation +% ------------------------ +P = (X-M)./ repmat(omega,size(X,1),1); +P(isnan(P)) = 0; P(isinf(P)) = 0; % correct if omega = 0 +comb = [(1:size(X,2)/2)',((1:size(X,2)/2)+size(X,2)/2)']; % all pairs of columns +r = NaN(size(comb,1),1); +t = r; p = t; + +for j = 1:size(comb,1) + + % column 1 + psi = P(:,comb(j,1)); + i1 = length(psi(psi<-1)); + i2 = length(psi(psi>1)); + sx = X(:,comb(j,1)); + sx(psi<(-1)) = 0; + sx(psi>1) = 0; + pbos = (sum(sx)+ omega(comb(j,1))*(i2-i1)) / (size(X,1)-i1-i2); + a = (X(:,comb(j,1))-pbos)./repmat(omega(comb(j,1)),size(X,1),1); + + % column 2 + psi = P(:,comb(j,2)); + i1 = length(psi(psi<-1)); + i2 = length(psi(psi>1)); + sx = X(:,comb(j,2)); + sx(psi<(-1)) = 0; + sx(psi>1) = 0; + pbos = (sum(sx)+ omega(comb(j,2))*(i2-i1)) / (size(X,1)-i1-i2); + b = (X(:,comb(j,2))-pbos)./repmat(omega(comb(j,2)),size(X,1),1); + + % return values of a,b to plot + XX{j} = union(find(a <= -1),find(a >= 1)); + YY{j} = union(find(b <= -1),find(b >= 1)); + + % bend + a(a<=-1) = -1; a(a>=1) = 1; + b(b<=-1) = -1; b(b>=1) = 1; + + % get r, t and p + r(j) = sum(a.*b)/sqrt(sum(a.^2)*sum(b.^2)); + t(j) = r(j)*sqrt((size(X,1) - 2)/(1 - r(j).^2)); + p(j) = 2*(1 - tcdf(abs(t(j)),size(X,1)-2)); + +end + +if size(X,2) > 2 && nargout > 5 + bv = 48*(size(X,1)-2.5).^2; + for j=1:length(comb) + c(j) = sqrt((size(X,1)-2.5)*log(1+t(j)^2/(size(X,1)-2))); % S plus: cmat<-sqrt((nrow(m)-2.5)*log(1+tstat^2/(nrow(m)-2)))\ + z(j) = c(j) + (c(j)^3+3*c(j))/bv - ( (4*c(j).^7+33*c(j).^5+240*c(j)^3+855*c(j)) / (10*bv.^2+8*bv*c(j).^4+1000*bv) ); % S plus: cmat<-cmat+(cmat^3+3*cmat)/bv-(4*cmat^7+33*cmat^5+240^cmat^3+855*cmat)/(10*bv^2+8*bv*cmat^4+1000*bv)\ + end + H = sum(z.^2); + pH= 1- cdf('chi2',H,(size(X,2)*(size(X,2)-1))/2); +end + +end \ No newline at end of file diff --git a/compare_correlations.m b/compare_correlations.m new file mode 100644 index 0000000..c032c12 --- /dev/null +++ b/compare_correlations.m @@ -0,0 +1,142 @@ +function [D,CI] = compare_correlations(varargin) + +% Percentile bootstrap to compare correlation coef. +% +% FORMAT: [D,CI] = compare_correlations(data1,data2,method,type) +% +% INPUT: data1 data from gp 1 - matrix n1*2 +% data2 data from gp 2 - matrix n2*2 +% method type of correlation to use 'Pearson' 'Spearman' 'bendcorr' +% 'Skipped_P' or 'Skipped_S' (for skipped corr Pearson or Spearman) +% type=2 for independent case (default) or type=1 for apparied measures (then n1=n2) +% +% OUTPUT: D the observed difference +% CI the 95% of the observed difference +% +% Cyril Pernet v1 08 April 2013 + +%% input checks + +if nargin<3 + error('not enough argument'); +end + +names{1} = 'Pearson'; +names{2} = 'Spearman'; +names{3} = 'bendcorr'; +names{4} = 'Skipped_P'; +names{5} = 'Skipped_S'; + +if nargin == 3 + type = 2; +else + type = varargin{4}; +end + +if type == 1 + if sum((size(varargin{1})==size(varargin{2}))) ~=2 + error('for paired data, data1 and data2 must have the same size') + end +end +data1 = varargin{1}; +data2 = varargin{2}; + +method = varargin{3}; +if isempty(cell2mat(strfind(names,method))) + error('unknown method choose between ''Pearson'' ''Spearman'' ''bendcorr'' ''Skipped_P'' ''Skipped_S'''); +end + +%% bootstrap +nboot = 600; +low = round((5/100*nboot)/2); +high = nboot - low; + +switch type + case {1} % dependent groups + % boostrap data + table= randi(size(data,1),size(data1,1),599); + X1 = data1(:,1); X1 = X1(table); + Y1 = data1(:,2); Y1 = Y1(table); + X2 = data2(:,1); X2 = X2(table); + Y2 = data2(:,2); Y2 = Y2(table); + + for b=1:nboot + if strcmp(method,names{1}) + D = Pearson(data1(:,1),data1(:,2),0) - Pearson(data2(:,1),data2(:,2),0); + r1 = Pearson(X1,Y1,0); + r2 = Pearson(X2,Y2,0); + + elseif strcmp(method,names{2}) + D = Spearman(data1(:,1),data1(:,2),0) - Spearman(data2(:,1),data2(:,2),0); + r1 = Spearman(X1,Y1,0); + r2 = Spearman(X2,Y2,0); + elseif strcmp(method,names{3}) + D = bendcorr(data1(:,1),data1(:,2),0) - bendcorr(data2(:,1),data2(:,2),0); + r1 = bendcorr(X1,Y1,0); + r2 = bendcorr(X2,Y2,0); + else + if strcmp(method,names{4}), estimator = 'Pearson'; + else estimator = 'Spearman'; end + D = skipped_correlation(data1(:,1),data1(:,2),0,estimator) - skipped_correlation(data2(:,1),data2(:,2),0,estimator); + r1 = skipped_correlation(X1,Y1,0,estimator); + r2 = skipped_correlation(X2,Y2,0,estimator); + end + end + + d = sort(r1-r2); + CI = [d(low) d(high)]; + + case {2} % independent groups + % boostrap data + n1 = size(data1,1); table1= randi(n1,n1,599); + X1 = data1(:,1); X1 = X1(table1); + Y1 = data1(:,2); Y1 = Y1(table1); + + n2 = size(data2,1); table2= randi(n2,n2,599); + X2 = data2(:,1); X2 = X2(table2); + Y2 = data2(:,2); Y2 = Y2(table2); + + for b=1:nboot + if strcmp(method,names{1}) + D = Pearson(data1(:,1),data1(:,2),0) - Pearson(data2(:,1),data2(:,2),0); + r1 = Pearson(X1,Y1,0); + r2 = Pearson(X2,Y2,0); + + % adjust percentile following Wilcox 2012 + N = n1+n2; + if N<40 + low = 7; high = 593; + elseif N>=40 && N<80 + low = 8; high = 592; + elseif N>=80 && N<180 + low = 11; high = 588; + elseif N>=180 && N<250 + low = 14; high = 585; + elseif N>=250 + low = 15; high = 584; + end + + elseif strcmp(method,names{2}) + D = Spearman(data1(:,1),data1(:,2),0) - Spearman(data2(:,1),data2(:,2),0); + r1 = Spearman(X1,Y1,0); + r2 = Spearman(X2,Y2,0); + elseif strcmp(method,names{3}) + D = bendcorr(data1(:,1),data1(:,2),0) - bendcorr(data2(:,1),data2(:,2),0); + r1 = bendcorr(X1,Y1,0); + r2 = bendcorr(X2,Y2,0); + else + if strcmp(method,names{4}), estimator = 'Pearson'; + else estimator = 'Spearman'; end + D = skipped_correlation(data1(:,1),data1(:,2),0,estimator) - skipped_correlation(data2(:,1),data2(:,2),0,estimator); + r1 = skipped_correlation(X1,Y1,0,estimator); + r2 = skipped_correlation(X2,Y2,0,estimator); + end + end + + d = sort(r1-r2); + CI = [d(low) d(high)]; +end + + + + diff --git a/conditional.m b/conditional.m new file mode 100644 index 0000000..51e1aec --- /dev/null +++ b/conditional.m @@ -0,0 +1,30 @@ +function [values,variance]=conditional(X,Y) + +% Returns the conditional values and variances of X given Y and Y given X. +% The calculation is based on Pearson correlation values because +% if the X & Y are jointly normal and r = 0, then X & Y are independent. +% +% FORMAT: [values,variance]=conditional(X,Y) +% +% INPUTS: X and Y are two vectors of the same length +% +% OUTPUTS: values are the conditioned variables X and Y +% variances are the conditional variances +% +% +% Cyril Pernet v1 21/05/2012 +% --------------------------------- +% Copyright (C) Corr_toolbox 2012 + +if size(X)~=size(Y) + error('X and Y must have the same size') +end + +r = Pearson(X,Y,0); +Xhat = r*nanstd(X)*Y / nanstd(Y); +Yhat = r*nanstd(Y)*X / nanstd(X); +Cond_stdX = (1-r^2)*nanstd(X); +Cond_stdY = (1-r^2)*nanstd(Y); + +values = [Xhat Yhat]; +variance = [Cond_stdX^2 Cond_stdY^2]; diff --git a/corr_normplot.m b/corr_normplot.m new file mode 100644 index 0000000..6f5b527 --- /dev/null +++ b/corr_normplot.m @@ -0,0 +1,76 @@ +function corr_normplot(x,y) + +% Plots univariate histograms, scatterplot, and joint histogram +% for the bivariate data set defined by [x,y] +% +% FORMAT: corr_normplot(x,y) +% +% INPUTS: x and y are two vectors of the same length + +% Cyril Pernet v1 20/06/2012 +% ----------------------------- +% Copyright (C) Corr_toolbox 2012 + +if size(x)~=size(y) + error('X and Y must have the same size') +end + +[x,y]=pairwise_cleanup(x,y); + +[r c] = size(x); +if r == 1 && c > 1 + x = x'; + y = y'; +elseif r > 1 && c > 1 + error('X and Y must be 2 vectors, more than 1 column/row detected') +end + + +figure('Name','Histograms and scatter plot') +set(gcf,'Color','w'); + +% 1st univariate histogram +subplot(3,5,2:3); +[nu,x1,h1,xp,yp]=univar(x); +bar(x1,nu/(length(x)*h1),1,'FaceColor',[0.5 0.5 1]); +v = max(yp) + 0.02*max(yp); +grid on; axis([min(x)-1/10*min(x) max(x)+1/10*max(x) 0 v]); hold on +plot(xp,yp,'r','LineWidth',3); title('Density histogram for X','Fontsize',16); +ylabel('Freq.','FontSize',14); xlabel('X.','FontSize',14) +box on;set(gca,'FontSize',14) + +% 2nd univariate histogram +subplot(3,5,[6 11]); +[nu,x2,h2,xp,yp]=univar(y); +bar(x2,nu/(length(y)*h2),1,'FaceColor',[0.5 0.5 1]); +v = max(yp) + 0.02*max(yp); +grid on; axis([min(y)-1/10*min(y) max(y)+1/10*max(y) 0 v]); hold on +plot(xp,yp,'r','LineWidth',3); view(-90,90) +title('Density histogram for Y','Fontsize',16); +ylabel('Freq.','FontSize',14); xlabel('Y.','FontSize',14); +box on;set(gca,'FontSize',14) +drawnow + +% scatter plot +subplot(3,5,[7 8 12 13]); +scatter(x,y,100,'filled'); grid on +xlabel('x','Fontsize',14); ylabel('y','Fontsize',14); +axis([min(x)-1/10*min(x) max(x)+1/10*max(x) min(y)-1/10*min(y) max(y)+1/10*max(y)]) +title('Scatter plot','Fontsize',16); +box on;set(gca,'FontSize',14,'Layer','Top') +drawnow + +% joint histogram +subplot(3,5,[9 10 14 15]); +k = round(1 + log2(length(x))); +hist3([x y],[k k],'FaceAlpha',.65); +xlabel('X','FontSize',14); ylabel('Y','FontSize',14); title('Bivariate histogram','Fontsize',16) +set(gcf,'renderer','opengl'); +set(gca,'FontSize',14) +drawnow +try + set(get(gca,'child'),'FaceColor','interp','CDataMode','auto'); +end + + + diff --git a/demean.m b/demean.m new file mode 100644 index 0000000..9a3f223 --- /dev/null +++ b/demean.m @@ -0,0 +1,15 @@ +function y = demean(x) + +% simple routine to removes the mean value from the vector X +% or the mean value from each column, if X is a matrix. +% +% Cyril Pernet +% --------------------------------- +% Copyright (C) Corr_toolbox 2014 + +n = size(x,1); +if n == 1, + x = x(:); % If a single row, turn into column vector +end +N = size(x,1); +y = x - ones(N,1)*nanmean(x); \ No newline at end of file diff --git a/detect_outliers.m b/detect_outliers.m new file mode 100644 index 0000000..f26601d --- /dev/null +++ b/detect_outliers.m @@ -0,0 +1,263 @@ +function outliers = detect_outliers(X,Y,fig_flag,method) + +% Finds univariate and multivariate outliers +% 3 'methods' are available and they can return different +% results +% +% FORMAT outliers = detect_outliers(X,Y) +% outliers = detect_outliers(X,Y,fig_flag,'method') +% +% INPUTS: X and Y are two vectors of the same length. +% fig_flag 1/0 indicates to make a figure or not +% method 'boxplot' relies on the interquartile range +% 'MAD' relies on the median absolute deviation to the median +% 'S-outlier' relies on the median of absolute distances +% if empty or 'All', the 3 methods are computed +% +% OUTPUTS: outliers is a structure with: +% +% * univariate outliers flagged +% outliers.univariate = indices of univariate outliers in X +% outliers.univariate = indices of univariate outliers in Y +% +% * bivariate outliers flagged +% outliers.bivariate = indices of bivariate outliers +% +% The boxplot rule for univariate outliers uses Carling's modification of +% the boxplot rule - Carling, K. (2000). Resistant outlier rules and the +% non-Gaussian case. Statistics & Data analysis, 33, 249-258. +% +% The median absolute deviation standard deviations for univariate outliers +% uses a modification for finite sample size - William, J Stat Computation +% and Simulation, 81, 11, 2011 +% +% S-outliers relies on the median of absolute distances. Because it doesn't +% rely on an estimator of central tendency (like the MAD) it works well for +% non symmetric distributons - Rousseeuw, P.J. and Croux C. (1993). +% Alternatives to the the median absolute deviation. Journal of the American +% Statitical Association, 88 (424) p 1273-1263 +% +% Multivariate outliers are detected by computing all the distances to the +% center of the bivariate cloud (ie using a projection method) and then +% checking if the distances are above a given distance (Wilcox 2012). +% For 'boxplot' and 'MAD' it corresponds to the median distance + +% gval*value with gval = sqrt(chi2inv(0.975,2)) and value being the IQR or +% the normalized MAD. For 'S-outlier' it is like in the univariate case. +% +% See also MADMEDIANRULE, IQR_METHOD, MCDCOV +% +% Cyril Pernet, v1 23/07/2012 +% Cyril Pernet and Guillaume Rousselet, v2 08/10/2012 +% ------------------------------------------------- +% Copyright (C) Corr_toolbox 2012 + + +%% check inputs + +if nargin<2 + error('not enough input arguments'); +elseif nargin==2 + fig_flag = 1; + method = 'All'; +elseif nargin == 3 + method = 'All'; +elseif nargin>4 + error('too many input arguments'); +end + +% transpose if x or y are not in column +if size(X,1) == 1 && size(X,2) > 1; X = X'; end +if size(Y,1) == 1 && size(Y,2) > 1; Y = Y'; end + +if numel(size(X))>2 || numel(size(Y))>2 + error('only taking vectors as input') +end + +if numel(X) ~= numel(Y) + error('vector must be of the same length') +end + +[X,Y]=pairwise_cleanup(X,Y); +[n,p]=size(X); + +%% ------------------------------------------------------------------ +%% univariate outliers + + +if strcmp(method,'All') || strcmp(method,'boxplot') + % Carling's modification of the boxplot rule + Xoutliers_boxplot = iqr_method(X,2); + Youtliers_boxplot = iqr_method(Y,2); + outliers.univariate.boxplot = [Xoutliers_boxplot ,Youtliers_boxplot]; +end + +if strcmp(method,'All') || strcmp(method,'MAD') + % applies MAD with a correction for finite sample sizes + [Xoutliers_MAD,distance] = madmedianrule(X,2); + [Youtliers_MAD,distance] = madmedianrule(Y,2); + outliers.univariate.MAD = [Xoutliers_MAD Youtliers_MAD]; +end + +if strcmp(method,'All') || strcmp(method,'S-outlier') + [Xoutliers_Soutlier,distance] = madmedianrule(X,3); + [Youtliers_Soutlier,distance] = madmedianrule(Y,3); + outliers.univariate.Soutlier = [Xoutliers_Soutlier Youtliers_Soutlier]; +end + +if strcmp(method,'All') + outliers.univariate.intersection = zeros(n,2); + X_all = intersect(intersect(find(Xoutliers_boxplot),find(Xoutliers_MAD)),find(Xoutliers_Soutlier)); + Y_all = intersect(intersect(find(Youtliers_boxplot),find(Youtliers_MAD)),find(Youtliers_Soutlier)); + outliers.univariate.intersection(X_all,1) = 1; + outliers.univariate.intersection(Y_all,2) = 1; +end + +%% ------------------------------------------------------------------ +%% multivariate outliers + +tmpX=X; tmpY=Y; X = [X Y]; +% get the centre of the bivariate distribution +result=mcdcov(X,'cor',1,'plots',0,'h',floor((n+size(X,2)*2+1)/2)); +center = result.center; +flag = NaN(n,3); +gval = sqrt(chi2inv(0.975,2)); + +% orthogonal projection to the lines joining the center +% followed by univariate outlier detection + +for i=1:n % for each row + dis=NaN(n,1); + B = (X(i,:)-center)'; + BB = B.^2; + bot = sum(BB); + if bot~=0 + for j=1:n + A = (X(j,:)-center)'; + dis(j)= norm(A'*B/bot.*B); + end + + % IQR rule for skipped corr + [ql,qu]=idealf(dis); + record1{i} = (dis > median(dis)+gval.*(qu-ql)) ; + + % MAD rule for skipped corr + [out,value] = madmedianrule(dis,2); + record2{i} = dis > (median(dis)+gval.*value); + + % S-outlier + [record3{i},value] = madmedianrule(dis,3); + end +end + +flag(:,1) = sum(cell2mat(record1),2); % if any point is flagged +flag(:,2) = sum(cell2mat(record2),2); +flag(:,3) = sum(cell2mat(record3),2); +flag=(flag>=1); +vec=repmat([1:n]',1,8); +for m=1:3 + if sum(flag(:,m))==0 + outid{m}=[]; + else + outid{m} = vec(flag(:,m),m); + end +end + +vec = zeros(n,1); +if strcmp(method,'All') || strcmp(method,'boxplot') + outliers.bivariate.boxplot = vec; + outliers.bivariate.boxplot(outid{1}) = 1; +end + +if strcmp(method,'All') || strcmp(method,'MAD') + outliers.bivariate.MAD = vec; + outliers.bivariate.MAD(outid{2}) = 1; +end + +if strcmp(method,'All') || strcmp(method,'S-outlier') + outliers.bivariate.Soutlier = vec; + outliers.bivariate.Soutlier(outid{3}) = 1; +end + +if strcmp(method,'All') + outliers.bivariate.intersection = vec; + outliers.bivariate.intersection(intersect(intersect(outid{1},outid{2}),outid{3})) = 1; +end + + + +%% ------------------------------------------------------------------ +%% figure(s) + +if strcmp(method,'All') || strcmp(method,'boxplot') + figure('Color','w','Name','Outlier detection using the boxplot rule'); + make_figure(X(:,1),X(:,2),outliers.univariate.boxplot,outliers.bivariate.boxplot); +end + +if strcmp(method,'All') || strcmp(method,'MAD') + figure('Color','w','Name','Outlier detection using the MAD'); + make_figure(X(:,1),X(:,2),outliers.univariate.MAD,outliers.bivariate.MAD); +end + +if strcmp(method,'All') || strcmp(method,'S-outlier') + figure('Color','w','Name','Outlier detection using S-estimator'); + make_figure(X(:,1),X(:,2),outliers.univariate.Soutlier,outliers.bivariate.Soutlier); +end + +end + +function make_figure(X,Y,Univariate_out,Bivariate_out) +% routine to make the figure + +% univariate subplot +subplot(1,2,1); +scatter(X,Y,99,'o','filled'); grid on; hold on +xlabel('X','Fontsize',12); ylabel('Y','Fontsize',12); +title('Univariate outliers','Fontsize',16); +if sum(Univariate_out(:)) > 0 + Xoutliers = Univariate_out(:,1); + Youtliers = Univariate_out(:,2); + common = intersect(find(Xoutliers),find(Youtliers)); + if ~isempty(common) + scatter(X(common),Y(common),100,'k','filled') + try common ~= find(Xoutliers) + scatter(X(find(Xoutliers)),Y(find(Youtliers)),100,'r','filled') + scatter(X(find(Youtliers)),Y(find(Youtliers)),100,'g','filled') + legend('data','outliers in X','outliers in Y','outliers in X and in Y') + catch ME + legend('data','outliers in X and in Y') + end + + elseif ~isempty(find(Xoutliers)) && ~isempty(find(Youtliers)) + scatter(X(find(Xoutliers)),Y(find(Xoutliers)),100,'r','filled') + scatter(X(find(Youtliers)),Y(find(Youtliers)),100,'g','filled') + legend('data','outliers in X','outliers in Y'); + + elseif ~isempty(find(Xoutliers)) && isempty(find(Youtliers)) + scatter(X(find(Xoutliers)),Y(find(Xoutliers)),100,'r','filled') + legend('data','outliers in X') + + elseif ~isempty(find(Youtliers)) && isempty(find(Xoutliers)) + scatter(X(find(Youtliers)),Y(find(Youtliers)),100,'g','filled') + legend('data','outliers in Y') + end + disp(' '); + fprintf('%g outliers found in X \n',sum(Xoutliers)) + fprintf('%g outliers found in Y \n',sum(Youtliers)) +end +axis tight +box on;set(gca,'FontSize',14) + +% bivariate outliers +subplot(1,2,2); +a = X(find(Bivariate_out==0)); +b = Y(find(Bivariate_out==0)); +scatter(a,b,100,'b','fill'); +grid on; hold on; +hh = lsline; set(hh,'Color','r','LineWidth',4); +scatter(X(find(Bivariate_out)),Y(find(Bivariate_out)),100,'r','filled') +xlabel('X','Fontsize',12); ylabel('Y','Fontsize',12); +title('Bivariate outliers','Fontsize',16); +fprintf('%g bivartiate outliers found \n',sum(Bivariate_out)) +axis([min(X) max(X) min(Y) max(Y)]) +box on;set(gca,'FontSize',14) +end diff --git a/idealf.m b/idealf.m new file mode 100644 index 0000000..15ccfea --- /dev/null +++ b/idealf.m @@ -0,0 +1,18 @@ +function [ql,qu]=idealf(x) +% Compute the ideal fourths for data in x +% The estimate of the interquartile range is: +% IQR=qu-ql; +% Adapted from Rand Wilcox's idealf R function, described in +% Rand Wilcox, Introduction to Robust Estimation & Hypothesis Testing, 3rd +% edition, Academic Press, Elsevier, 2012 + +% Cyril Pernet & Guillaume Rousselet, v1 - September 2012 +% --------------------------------------------------- +% Copyright (C) Corr_toolbox 2012 + +j=floor(length(x)/4 + 5/12); +y=sort(x); +g=(length(x)/4)-j+(5/12); +ql=(1-g).*y(j)+g.*y(j+1); +k=length(x)-j+1; +qu=(1-g).*y(k)+g.*y(k-1); \ No newline at end of file diff --git a/iqr_method.m b/iqr_method.m new file mode 100644 index 0000000..d8c6d96 --- /dev/null +++ b/iqr_method.m @@ -0,0 +1,59 @@ +function [I,value] = iqr_method(a,type) + +% Returns a logical vector that flags outliers as 1s based +% on the IQR methods described in Wilcox 2012 p 96-97. +% +% FORMAT: I = iqr_method(a,type) +% [I,value] = iqr_method(a,type) +% +% INPUTS: +% a is a vector. +% +% type indicates the method to use +% +% type = 1 uses the standard boxplot approach, +% in which the quartiles are estimated by the ideal fourths, +% q1 and q2. An observation Xi is declared an outlier if: +% Xiq2+k(q2-q1), +% and k=1.5. +% +% type = 2 uses Carling's modification of the boxplot rule. +% An observation Xi is declared an outlier if: +% XiM+k(q2-q1), +% where M is the sample median, and +% k=(17.63n-23.64)/(7.74n-3.71), +% where n is the sample size. +% +% OUTPUTS: I = logical vector with 1s for outliers +% value = IQR, the inter-quartile range +% +% +% See also IDEALF. + +% Cyril Pernet / Guillaume Rousselet +% --------------------------------- +% Copyright (C) Corr_toolbox 2012 + +if nargin == 1 + type = 2; +end +a=a(:);n=length(a); +[q1,q2]=idealf(a); +value=q2-q1; + +switch type + + case 1 + % standard boxplot method + k=1.5; + I=a<(q1-k*value) | a>(q2+k*value); + + case 2 + % Carling's modification of the boxplot rule + M = median(a); + k=(17.63*n-23.64)/(7.74*n-3.71); + I=a<(M-k*value) | a>(M+k*value); +end + +I = I+isnan(a); + diff --git a/joint_density.m b/joint_density.m new file mode 100644 index 0000000..ee627ea --- /dev/null +++ b/joint_density.m @@ -0,0 +1,85 @@ +function density = joint_density(x,y,flag) + +% simple routine to compute and plot the joint density histogram of x and y +% +% FORMAT: joint_density(x,y) +% joint_density(x,y,flag) +% +% INPUTS: x and y are two vectors of the same length +% flag if 1 (default) plot both the mesh and isocontour else only plot isocontrour +% +% Ref: Martinez, W.L. & Martinez, A.R. 2008. +% Computational Statistics Handbook with Matlab. 2nd Ed. +% +% Cyril Pernet v2 23/07/2012 +% ----------------------------- +% Copyright (C) Corr_toolbox 2012 + +if nargin == 2 + flag = 1; +end + +if size(x)~=size(y) + error('X and Y must have the same size') +end + +[x,y]=pairwise_cleanup(x,y); + +[r c] = size(x); +if r == 1 && c > 1 + x = x'; + y = y'; +elseif r > 1 && c > 1 + error('X and Y must be 2 vectors, more than 1 column/row detected') +end + + +n = length(x); +% nb of bins +k = round(1 + log2(n)); +% bin sizes +[nu,p]=hist(x,k); h1 = p(2) - p(1); +[nu,p]=hist(y,k); h2 = p(2) - p(1); +% start binning at +bin0 = [floor(min(x)) floor(min(y))]; +% make a mesh +t1 = bin0(1):h1:(h1*k+bin0(1)); +t2 = bin0(2):h2:(h2*k+bin0(2)); +[X,Y]=meshgrid(t1,t2); +% frequency per bin in the mesh +[nr,nc]=size(X); +vu = zeros(nr-1,nc-1); +for i=1:size(vu,1) + for j=1:size(vu,2) + xv = [X(i,j) X(i,j+1) X(i+1,j+1) X(i+1,j)]; + yv = [Y(i,j) Y(i,j+1) Y(i+1,j+1) Y(i+1,j)]; % coordinates + in = inpolygon(x,y,xv,yv); % which data in the box + vu(i,j) = sum(in(:)); % count + end +end +pdf = vu./(n*h1*h2); % normalize + +% plot +if flag == 1 + figure('Name','Joint density pdf'); set(gcf,'Color','w'); + subplot(1,2,1); + surfl(pdf-0.01); axis tight ; xlabel('X','FontSize',14); + ylabel('Y','FontSize',14); zlabel('density','FontSize',14); + title('Joint density histogram','Fontsize',16); set(gcf,'renderer','opengl'); + set(get(gca,'child'),'FaceColor','interp','CDataMode','auto'); + set(gca,'FontSize',14,'Layer','Top') + axis square + + subplot(1,2,2); +else + figure('Name','Isocontour of the joint pdf'); set(gcf,'Color','w'); +end +contourf(pdf); axis tight ; xlabel('X','FontSize',14); +ylabel('Y','FontSize',14); +title('Isocontour of the joint density','Fontsize',16); +set(gca,'FontSize',14,'Layer','Top') +axis square + +if nargout > 0 + density = pdf; +end diff --git a/licenses.m b/licenses.m new file mode 100644 index 0000000..070047b --- /dev/null +++ b/licenses.m @@ -0,0 +1,58 @@ +% Licenses for the files included in the correlation toolbox + +%% Correlation toolbox - GNU General Public License +% Corr_toolbox is a matlab toolbox intended for the visualization and robust +% correlation analysis of any sets of data. This program is a collaborative +% work between Dr Cyril Pernet (University of Edinburgh), and Dr Guillaume +% Rousselet (University of Glasgow). +% Copyright (C) 2012 Cyril Pernet and Guillaume Rousselet. +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see . + + +%% HZmvntest.m - BSD license +% +% Trujillo-Ortiz, A., R. Hernandez-Walls, K. Barba-Rojo and L. Cupul-Magana. +% (2007). HZmvntest:Henze-Zirkler's Multivariate Normality Test. A MATLAB +% file. [WWW document]. URL http://www.mathworks.com/matlabcentral/ +% fileexchange/loadFile.do?objectId=17931 +% +% Copyright (c) 2009, Antonio Trujillo-Ortiz +% All rights reserved. +% +% Redistribution and use in source and binary forms, with or without +% modification, are permitted provided that the following conditions are +% met: +% +% * Redistributions of source code must retain the above copyright +% notice, this list of conditions and the following disclaimer. +% * Redistributions in binary form must reproduce the above copyright +% notice, this list of conditions and the following disclaimer in +% the documentation and/or other materials provided with the distribution +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% POSSIBILITY OF SUCH DAMAGE. + + + + diff --git a/madmedianrule.m b/madmedianrule.m new file mode 100644 index 0000000..743c57b --- /dev/null +++ b/madmedianrule.m @@ -0,0 +1,92 @@ +function [I,value] = madmedianrule(a,type) + +% Returns a logical vector that flags outliers as 1s based +% on the MAD-median rule described in Wilcox 2012 p 97. +% +% FORMAT: I = madmedianrule(a,type) +% +% INPUTS: +% a is a vector or matrix. In the latter case, +% the MAD median rule is applied column-wise. +% +% type indicates the method to use +% +% for type = 1, MADS = b_n*1.4826*median(abs(a - median(a)) +% b_n is the finite sample correction factor described in +% William, J Stat Computation and Simulation, 81, 11, 2011 +% 1.4826 is the consistancy factor (the std) for the Gaussian distribution +% +% for type = 2, MADN = median(abs(a - median(a)) ./ 0.6745 +% rescaled MAD by the .6745 to estimate the std of the Gaussian +% distribution - see Wilcox 2012 p 75. + +% Cyril Pernet / Guillaume Rousselet +% --------------------------------- +% Copyright (C) Corr_toolbox 2012 + +k = 2.2414; % = sqrt(chi2inv(0.975,1)) +[n,p]=size(a); +M = median(a); +MAD=median(abs(a - repmat(median(a),n,1))); + +switch type + + case 1 + % Median Absolute Deviation with finite sample correction factor + if n == 2 + bn=1.197; % 1.196; + elseif n == 3 + bn=1.49; % 1.495; + elseif n == 4 + bn=1.36; % 1.363; + elseif n == 5 + bn=1.217; % 1.206; + elseif n == 6 + bn=1.189; % 1.200; + elseif n == 7 + bn=1.138; % 1.140; + elseif n == 8 + bn=1.127; % 1.129; + elseif n == 9 + bn=1.101; % 1.107; + else + bn=n/(n-0.8); + end + + MADS=repmat((MAD.*1.4826.*bn),n,1); + I = a > (repmat(M,[n 1])+(k.*MADS)); + I = I+isnan(a); + value = MADS(1,:); + + case 2 + % Normalized Median Absolute Deviation + MADN = repmat((MAD./.6745),n,1); % same as MAD.*1.4826 :-) + I = (abs(a-repmat(M,n,1)) ./ MADN) > k; + I = I+isnan(a); + value = MADN(1,:); + + case 3 % S outliers + + value = NaN(n,p); + for p=1:size(a,2) + tmp = a(:,p); + points = find(~isnan(tmp)); + tmp(isnan(tmp)) = []; + + % compte all distances + n = length(tmp); + for i=1:n + j = points(i); + indices = [1:n]; indices(i) = []; + value(j,p) = median(abs(tmp(i) - tmp(indices))); + end + + % get the S estimator + % consistency factor c = 1.1926; + Sn = 1.1926*median(value(points,p)); + + % get the outliers in a normal distribution + I(:,p) = (value(:,p) ./ Sn) > k; % no scaling needed as S estimates already std(data) + I(:,p) = I(:,p)+isnan(a(:,p)); + end +end diff --git a/manual.pdf b/manual.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fef012212d46c97f78da46d16633121cce8e488f GIT binary patch literal 506832 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

`DjtOvCu*H;5Qd1OwT=Leb%jwvTsY?jJ{=wR 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

  • jzVa+XYHH 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⁡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 LtRu_Lge+3BNDz=H`lkG&krH4WxIX9k4>-#Ec? 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%0A*lHNFrR>1|GO#7P_w6(`&?d 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%0A=`$JtiYekB)$Z<$j_UH-qehV_kX;g~Z^8%Y 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 +% 29.10.2013 + +LX = length(X); +LY = length(Y); + +if ~isequal(LX,LY) + errdlg('Both vectors need to have the same length'); + exit; +end + +p = isnan(X)|isnan(Y); + +x = X(~any(p,2),:); +y = Y(~any(p,2),:); + +end diff --git a/randi.m.zip b/randi.m.zip new file mode 100644 index 0000000000000000000000000000000000000000..0e4e55fa6c6d633ea73ed09eba235264e41c90a4 GIT binary patch literal 376 zcmWIWW@Zs#W?gCo>*z0%1K%nKf%~6vM z>ymm#D;HPSLiKwLKOgySnzY&N+poHNa@LIsvx<{X@BV%E%r;M{RQ(U0AG;Ecd`dEy zetvRcXV~wniIJh@=BqfCXnM2=zPWz<^{YB@sefj zS;8TGCh`KCy+d`=(;F!Uik#c+c%CXd_h){Y@$L=;YL{XZKbZAJ=?hv+>|=C1D-ccP%+;Y4$tsyC0glwY_}Xkp%+BO}9y? zo!j|*S^HO!r*Ri&3;+H8DP1uC`T;(9kromEuVvx+^5tKPLw&RJJMJ#J5jCYXU+&qA s)6?? 1 + X = X'; + Y = Y'; +elseif r > 1 && c > 1 + error('X and Y must be 2 vectors, more than 1 column/row detected') +end +% [X,Y]=pairwise_cleanup(X,Y); +level = 5/100; + + +%% 1 - plot the data and generate univariate and bivariate histograms + +% figure 1 - histograms and scatter plot +corr_normplot(X,Y); + +% figure 2 - Joint density +joint_density(X,Y,1) + + +%% 2 - performs the Henze- Zirkler test for normality +[results.normality.test_value,results.normality.p_value] = HZmvntest(X,Y,level); + +%% 3 - get conditional data +[results.conditional.values,results.conditional.variances]=conditional(X,Y); + +%% 4 - test for heteroscedasticity based on conditional variances +[h,results.heteroscedasticity.CI] = variance_homogeneity(X,Y,1); +if h == 0 + results.heteroscedasticity.result = 'variance are equals'; +else + results.heteroscedasticity.result = 'variances are inequals'; +end + +disp(' ') +fprintf('heteroscedasticity testing indicates that %s\n',results.heteroscedasticity.result) +fprintf('conditional variances = %g %g \n', results.conditional.variances) + +%% 5 - check for outliers based on the MAD median rule & the IQR rule +results.outliers = detect_outliers(X,Y); + +%% 6 - Pearson correlation +[r,t,pval,hboot,CI] = Pearson(X,Y); +results.Pearson.r = r; +results.Pearson.t = t; +results.Pearson.p = pval; +results.Pearson.CI = CI; + +disp(' ') +if hboot == 1 + fprintf('Pearson correlation is significant \n') + fprintf('r=%g p=%g CI=[%g %g] \n',r,pval,CI) + results.Pearson.result = 'Pearson correlation is significant'; +else + fprintf('Pearson correlation is not significant \n') + fprintf('r=%g CI=[%g %g] \n',r,CI) + results.Pearson.result = 'Pearson correlation is not significant'; +end + +clear r t pval hboot CI +%% 6 - Bend correlation (remove effect of univariate outliers) + +[r_bend,t_bend,p_bend,h,CI] = bendcorr(X,Y); % use the default 20% triming +results.bend_correlation.r = r_bend; +results.bend_correlation.t = t_bend; +results.bend_correlation.p = p_bend; +results.bend_correlation.CI = CI; +results.bend_correlation.h = h; + +disp(' ') +if h == 1 + fprintf('Bend correlation is significant \n') + fprintf('r=%g p=%g CI=[%g %g] \n',r_bend,p_bend,CI) + results.bend_correlation.result = 'Bend correlation is significant '; +else + fprintf('Bend correlation is not significant \n') + fprintf('r=%g CI=[%g %g] \n',r_bend,CI) + results.bend_correlation.result = 'Bend correlation is not significant '; +end + +clear r_bend t_bend p_bend CI h +%% 7 - performs Spearman + +[r,t,pval,hboot,CI] = Spearman(X,Y); +results.Spearman.r = r; +results.Spearman.t = t; +results.Spearman.p = pval; +results.Spearman.CI = CI; + +disp(' ') +if hboot == 1 + fprintf('Spearman correlation is significant \n') + fprintf('r=%g p=%g CI=[%g %g] \n',r,pval,CI) + results.Spearman.result = 'Spearman correlation is significant '; +else + fprintf('Spearman correlation is not significant \n') + fprintf('r=%g CI=[%g %g] \n',r,CI) + results.Spearman.result = 'Spearman correlation is not significant '; +end +clear r t pval hboot CI + +%% 8 - skip correlation to remove effect of bivariate outliers + +[r,t,h,outliers,hboot,CI]=skipped_correlation(X,Y); +results.Skipped_correlation.Pearson.r = r.Pearson; +results.Skipped_correlation.Spearman.r = r.Spearman; +results.Skipped_correlation.Pearson.t = t.Pearson; +results.Skipped_correlation.Spearman.t = t.Spearman; +results.Skipped_correlation.Pearson.CI = CI.Pearson; +results.Skipped_correlation.Spearman.CI = CI.Spearman; +results.Skipped_correlation.Pearson.h = h.Pearson; +results.Skipped_correlation.Spearman.h = h.Spearman; + +disp(' ') +if hboot.Pearson == 1 + fprintf('Pearson Skipped correlation is significant \n') + fprintf('r=%g CI=[%g %g] \n',r.Pearson,CI.Pearson) + results.Skipped_correlation.Pearson.result = 'Skipped correlation is significant '; +else + fprintf('Skipped correlation is not significant \n') + fprintf('r=%g CI=[%g %g]\n',r.Pearson,CI.Pearson) + results.Skipped_correlation.Pearson.result = 'Skipped correlation is not significant '; +end + +if hboot.Spearman == 1 + fprintf('Spearman Skipped correlation is significant \n') + fprintf('r=%g CI=[%g %g] \n',r.Spearman,CI.Spearman) + results.Skipped_correlation.Spearman.result = 'Skipped correlation is significant '; +else + fprintf('Spearman Skipped correlation is not significant \n') + fprintf('r=%g CI=[%g %g] \n',r.Spearman,CI.Spearman) + results.Skipped_correlation.Spearman.result = 'Skipped correlation is not significant '; +end + +clear h r t outliers CI + diff --git a/skipped_correlation.m b/skipped_correlation.m new file mode 100644 index 0000000..c8cdce4 --- /dev/null +++ b/skipped_correlation.m @@ -0,0 +1,805 @@ +function [r,t,h,outid,hboot,CI]=skipped_correlation(x,y,fig_flag,estimator) + +% performs a robust correlation using pearson/spearman correlation on +% data cleaned up for bivariate outliers - that is after finding the +% central point in the distribution using the mid covariance determinant, +% orthogonal distances are computed to this point, and any data outside the +% bound defined by the idealf estimator of the interquartile range is removed. +% +% FORMAT: +% [r,t,h] = skipped_correlation(X,Y); +% [r,t,h] = skipped_correlation(X,Y,fig_flag); +% [r,t,h] = skipped_correlation(X,Y,fig_flag,'estimator'); +% [r,t,h,outid,hboot,CI] = skipped_correlation(X,Y,fig_flag); +% +% INPUTS: X and Y are 2 vectors or matrices, in the latter case, +% Spearman correlations are computed column-wise (only Spearman +% because there is no good control over type 1 error with Pearson) +% fig_flag (1/0) indicates to plot the data or not +% 'estimator' is either 'Pearson' or 'Spearman' +% +% OUTPUTS: +% r is the pearson/spearman correlation +% t is the T value associated to the skipped correlation +% h is the hypothesis of no association at alpha = 5% +% if NaN, h depends on the largest n across columns +% outid is the index of bivariate outliers +% +% optional: +% +% hboot 1/0 declares the test significant based on CI (h depends on t) +% CI is the robust confidence interval computed by bootstrapping the +% cleaned-up data set and taking the .95 centile values +% +% This code rely on the mid covariance determinant as implemented in LIBRA +% - Verboven, S., Hubert, M. (2005), LIBRA: a MATLAB Library for Robust Analysis, +% Chemometrics and Intelligent Laboratory Systems, 75, 127-136. +% - Rousseeuw, P.J. (1984), "Least Median of Squares Regression," +% Journal of the American Statistical Association, Vol. 79, pp. 871-881. +% +% The quantile of observations whose covariance is minimized is +% floor((n+size(X,2)*2+1)/2)), +% i.e. ((number of observations + number of variables*2)+1) / 2, +% thus for a correlation this is floor(n/2 + 5/2). +% +% See also MCDCOV, IDEALF. +% +% Cyril Pernet & Guillaume Rousselet, v1 - April 2012 +% Cyril Pernet v2 (10-01-2014 - deals with NaN) +% --------------------------------------------------- +% Copyright (C) Corr_toolbox 2012 + +%% data check + +if nargin <2 + error('not enough input arguments'); +elseif nargin == 2 + fig_flag = 1; + estimator = []; +elseif nargin > 4 + error('too many input arguments'); +end + +% transpose if x or y are not in column +if size(x,1) == 1 && size(x,2) > 1; x = x'; end +if size(y,1) == 1 && size(y,2) > 1; y = y'; end +if (size(x,2)+size(y,2)) == 2 % input is 2 vectors + [x,y] = pairwise_cleanup(x, y); % check for NaN here, otherwise deal with NaN column wise +end + +if nargin == 4 && (size(x,2)+size(y,2)) ~= 2 % input is not 2 vectors + if strcmp(estimator,'Pearson') == 1 + errordlg('Pearson cannot be used to control the type 1 error on multiple variables - switching to Spearman'); + estimator = []; + end +end + +% if X a vector and Y a matrix, +% repmat X to perform multiple tests on Y (or the other around) + +% the default hypothesis is to test that all pairs of correlations are 0 +hypothesis = 1; + +% now if x is a vector and we test multiple y (or the other way around) one +% has to adjust for this +if size(x,2) == 1 && size(y,2) > 1 + x = repmat(x,1,size(y,2)); + hypothesis = 2; +elseif size(y,2) == 1 && size(x,2) > 1 + y = repmat(y,1,size(x,2)); + hypothesis = 2; +end + +[n,p] = size(x); +if size(x) ~= size(y) + error('x and y are of different sizes') +elseif n < 10 + error('robust effects can''t be computed with less than 10 observations') +elseif n > 200 && p < 10 + warndlg('robust correlation and T value will be computed, but h is not validated for n>200') +elseif p > 10 + warndlg('the familly wise error correction for skipped correlation is not available for more than 10 correlations') +end + +gval = sqrt(chi2inv(0.975,2)); % in fact depends on size(X,2) but here always = 2 + +%% compute +for column = 1:p + if p>1 + fprintf('skipped correlation: processing pair %g \n',column); + end + + % get the centre of the bivariate distributions + [xx,yy] = pairwise_cleanup(x(:,column), y(:,column)); + X = [xx yy]; n = size(X,1); + if n < 10 + error('after NaN removal, there are less than 10 observations - robust correlation can''t be computed') + end + result=mcdcov(X,'cor',1,'plots',0,'h',floor((n+size(X,2)*2+1)/2)); + center = result.center; + + + % orthogonal projection to the lines joining the center + % followed by outlier detection using the boxplot rule + + vec=1:n; + for i=1:n % for each row + dis=NaN(n,1); + B = (X(i,:)-center)'; + BB = B.^2; + bot = sum(BB); + if bot~=0 + for j=1:n + A = (X(j,:)-center)'; + dis(j)= norm(A'*B/bot.*B); + end + + % MAD median rule + % [outliers,value] = madmedianrule(dis,2); + % record{i} = dis > (median(dis)+gval.*value); + + % IQR rule + [ql,qu]=idealf(dis); + record{i} = (dis > median(dis)+gval.*(qu-ql)) ; % + (dis < median(dis)-gval.*(qu-ql)); + end + end + + try + flag = nan(n,1); + flag = sum(cell2mat(record),2); % if any point is flagged + + catch ME % this can happen to have an empty cell so loop + flag = nan(n,size(record,2)); + index = 1; + for s=1:size(record,2) + if ~isempty(record{s}) + flag(:,index) = record{s}; + index = index+1; + end + end + flag(:,index:end) = []; + flag = sum(flag,2); + end + + if sum(flag)==0 + outid{column}=[]; + else + flag=(flag>=1); + outid{column}=vec(flag); + end + keep=vec(~flag); + + %% Pearson/Spearman correlation + + if p == 1 % in the special case of a single test Pearson is valid too + a{column} = xx(keep); + b{column} = yy(keep); + + rp = sum(demean(a{column}).*demean(b{column})) ./ ... + (sum(demean(a{column}).^2).*sum(demean(b{column}).^2)).^(1/2); + tp = rp*sqrt((n-2)/(1-rp.^2)); + r.Pearson = rp; t.Pearson = tp; + + xrank = tiedrank(a{column},0); yrank = tiedrank(b{column},0); + rs = sum(demean(xrank).*demean(yrank)) ./ ... + (sum(demean(xrank).^2).*sum(demean(yrank).^2)).^(1/2); + ts = rs*sqrt((n-2)/(1-rs.^2)); + r.Spearman = rs; t.Spearman = ts; + + % update outputs if estimator selected + if strcmp(estimator,'Pearson') + clear r t; r = rp; t = tp; + elseif strcmp(estimator,'Spearman') + clear r t; r = rs; t = ts; + end + + else % multiple tests, only use Spearman to control type 1 error + a{column} = xx(keep); xrank = tiedrank(a{column},0); + b{column} = yy(keep); yrank = tiedrank(b{column},0); + r(column) = sum(demean(xrank).*demean(yrank)) ./ ... + (sum(demean(xrank).^2).*sum(demean(yrank).^2)).^(1/2); + t(column) = r(column)*sqrt((n-2)/(1-r(column).^2)); + end + + clear X result centre dis record + +end + +%% get h + +% the default test of 0 correlation is for alpha = 5% +n = size(x,1); % readjust n to the largest available +c = 6.947 / n + 2.3197; % valid for n between 10 and 200 +if p == 1 + if strcmp(estimator,'Pearson') + h = abs(tp) >= c; + elseif strcmp(estimator,'Spearman') + h = abs(ts) >= c; + else + h.Pearson = abs(tp) >= c; + h.Spearman = abs(ts) >= c; + end +else + h= abs(t) >= c; +end + +%% adjustement for multiple testing using the .95 quantile of Tmax +if p>1 && p<=10 + switch hypothesis + + case 1 % Hypothesis of 0 correlation between all pairs + + if p == 2; q = 5.333*n^-1 + 2.374; end + if p == 3; q = 8.8*n^-1 + 2.78; end + if p == 4; q = 25.67*n^-1.2 + 3.03; end + if p == 5; q = 32.83*n^-1.2 + 3.208; end + if p == 6; q = 51.53*n^-1.3 + 3.372; end + if p == 7; q = 75.02*n^-1.4 + 3.502; end + if p == 8; q = 111.34*n^-1.5 + 3.722; end + if p == 9; q = 123.16*n^-1.5 + 3.825; end + if p == 10; q = 126.72*n^-1.5 + 3.943; end + + case 2 % Hypothesis of 0 correlation between x1 and all y + + if p == 2; q = 5.333*n^-1 + 2.374; end + if p == 3; q = 8.811*n^-1 + 2.54; end + if p == 4; q = 14.89*n^-1.2 + 2.666; end + if p == 5; q = 20.59*n^-1.2 + 2.920; end + if p == 6; q = 51.01*n^-1.5 + 2.999; end + if p == 7; q = 52.15*n^-1.5 + 3.097; end + if p == 8; q = 59.13*n^-1.5 + 3.258; end + if p == 9; q = 64.93*n^-1.5 + 3.286; end + if p == 10; q = 58.5*n^-1.5 + 3.414; end + end + + h = abs(t) >= q; +end + + +%% bootstrap +if nargout > 4 + + [n,p]=size(a); + nboot = 1000; + level = 5/100; + if p > 1 + level = level / p; + end + low = round((level*nboot)/2); + if low == 0 + error('adjusted CI cannot be computed, too many tests for the number of observations') + else + high = nboot - low; + end + + for column = 1:p + % here different resampling because length(a) changes + table = randi(length(a{column}),length(a{column}),nboot); + + for B=1:nboot + % do Spearman + tmp1 = a{column}; xrank = tiedrank(tmp1(table(:,B)),0); + tmp2 = b{column}; yrank = tiedrank(tmp2(table(:,B)),0); + rsb(B,column) = sum(demean(xrank).*demean(yrank)) ./ ... + (sum(demean(xrank).^2).*sum(demean(yrank).^2)).^(1/2); + % get regression lines for Spearman + coef = pinv([xrank ones(length(a{column}),1)])*yrank; + sslope(B,column) = coef(1); sintercept(B,column) = coef(2,:); + + if p == 1 % ie only 1 correlation thus Pearson is good too + rpb(B,column) = nansum(demean(tmp1(table(:,B))).*demean(tmp2(table(:,B)))) ./ ... + (nansum(demean(tmp1(table(:,B))).^2).*nansum(demean(tmp2(table(:,B))).^2)).^(1/2); + coef = pinv([tmp1(table(:,B)) ones(length(a{column}),1)])*tmp2(table(:,B)); + pslope(B,column) = coef(1); pintercept(B,column) = coef(2,:); + end + end + end + + % in all cases get CI for Spearman + rsb = sort(rsb,1); + sslope = sort(sslope,1); + sintercept = sort(sintercept,1); + + % CI and h + adj_nboot = nboot - sum(isnan(rsb)); + adj_low = round((level*adj_nboot)/2); + adj_high = adj_nboot - adj_low; + + for c=1:p + if adj_low(c) > 0 + CI(:,c) = [rsb(adj_low(c),c) ; rsb(adj_high(c),c)]; + hboot(c) = (rsb(adj_low(c),c) > 0) + (rsb(adj_high(c),c) < 0); + CIsslope(:,c) = [sslope(adj_low(c),c) ; sslope(adj_high(c),c)]; + CIsintercept(:,c) = [sintercept(adj_low(c),c) ; sintercept(adj_high(c),c)]; + else + CI(:,c) = [NaN NaN]'; + hboot(c) = NaN; + CIsslope(:,c) = NaN; + CIsintercept(:,c) = NaN; + end + end + + CIpslope = CIsslope; % used in plot - unless only one corr was computed + + % case only one correlation + if p == 1 + rpb = sort(rpb,1); + pslope = sort(pslope,1); + pintercept = sort(pintercept,1); + + % CI and h + adj_nboot = nboot - sum(isnan(rpb)); + adj_low = round((level*adj_nboot)/2); + adj_high = adj_nboot - adj_low; + + if adj_low>0 + CIp = [rpb(adj_low) ; rpb(adj_high)]; + hbootp(c) = (rpb(adj_low) > 0) + (rpb(adj_high) < 0); + CIpslope(:,c) = [pslope(adj_low) ; pslope(adj_high)]; + CIpintercept(:,c) = [pintercept(adj_low) ; pintercept(adj_high)]; + else + CIp = [NaN NaN]; + hbootp(c) = NaN; + CIpslope(:,c) = NaN; + CIpintercept(:,c) = NaN; + end + + % update outputs + tmp = hboot; clear hboot; + if strcmp(estimator,'Pearson') + hboot = hbootp; + elseif strcmp(estimator,'Spearman') + hboot = tmp; + else + hboot.Spearman = tmp; + hboot.Pearson = hbootp; + end + + tmp = CI; clear CI + if strcmp(estimator,'Pearson') + CI = CIp'; + elseif strcmp(estimator,'Spearman') + CI = tmp'; + else + CI.Spearman = tmp'; + CI.Pearson = CIp'; + end + end +end + +%% plot +if fig_flag ~= 0 + answer = []; + if p > 1 + answer = questdlg(['plots all ' num2str(p) ' correlations'],'Plotting option','yes','no','yes'); + else + if fig_flag == 1 + figure('Name','Skipped correlation'); + set(gcf,'Color','w'); + end + + + if strcmp(estimator,'Pearson') == 1 + if nargout>4 + if ~isnan(r); subplot(1,2,1); end + M = sprintf('Skipped correlation \n Pearson r=%g CI=[%g %g]',r,CI(1),CI(2)); + else + M = sprintf('Skipped correlation \n Pearson r=%g h=%g',r,h); + end + + scatter(a{1},b{1},100,'b','fill'); + grid on; hold on; + hh = lsline; set(hh,'Color','r','LineWidth',4); + try + [XEmin, YEmin] = ellipse(a{column},b{column}); + plot(real(XEmin), real(YEmin),'LineWidth',2); + MM = [min(XEmin) max(XEmin) min(YEmin) max(YEmin)]; + catch ME + text(min(x)+0.01*(min(x)),max(y),'no ellipse found','Fontsize',12) + MM = []; + end + xlabel('X','Fontsize',12); ylabel('Y','Fontsize',12); + title(M,'Fontsize',16); + + % add outliers and scale axis + scatter(x(outid{1}),y(outid{1}),100,'r','filled'); + MM2 = [min(x) max(x) min(y) max(y)]; + if isempty(MM); MM = MM2; end + A = floor(min([MM(:,1);MM2(:,1)]) - min([MM(:,1);MM2(:,1)])*0.01); + B = ceil(max([MM(:,2);MM2(:,2)]) + max([MM(:,2);MM2(:,2)])*0.01); + C = floor(min([MM(:,3);MM2(:,3)]) - min([MM(:,3);MM2(:,3)])*0.01); + D = ceil(max([MM(:,4);MM2(:,4)]) + max([MM(:,4);MM2(:,4)])*0.01); + axis([A B C D]); + box on;set(gca,'Fontsize',14) + + if nargout>4 && sum(~isnan(CIpslope))==2 + % add CI + y1 = refline(CIpslope(1),CIpintercept(1)); set(y1,'Color','r'); + y2 = refline(CIpslope(2),CIpintercept(2)); set(y2,'Color','r'); + y1 = get(y1); y2 = get(y2); + xpoints=[[y1.XData(1):y1.XData(2)],[y2.XData(2):-1:y2.XData(1)]]; + step1 = y1.YData(2)-y1.YData(1); step1 = step1 / (y1.XData(2)-y1.XData(1)); + step2 = y2.YData(2)-y2.YData(1); step2 = step2 / (y2.XData(2)-y2.XData(1)); + filled=[[y1.YData(1):step1:y1.YData(2)],[y2.YData(2):-step2:y2.YData(1)]]; + hold on; fillhandle=fill(xpoints,filled,[1 0 0]); + set(fillhandle,'EdgeColor',[1 0 0],'FaceAlpha',0.2,'EdgeAlpha',0.8);%set edge color + + % add histograms of bootstrap + subplot(1,2,2); k = round(1 + log2(length(rpb))); hist(rpb,k); grid on; + mytitle = sprintf('Bootstrapped \n Pearsons'' corr h=%g', hboot); + title(mytitle,'FontSize',16); hold on + xlabel('boot correlations','FontSize',14);ylabel('frequency','FontSize',14) + plot(repmat(CI(1),max(hist(rpb,k)),1),[1:max(hist(rpb,k))],'r','LineWidth',4); + plot(repmat(CI(2),max(hist(rpb,k)),1),[1:max(hist(rpb,k))],'r','LineWidth',4); + axis tight; colormap([.4 .4 1]) + box on;set(gca,'Fontsize',14) + end + + elseif strcmp(estimator,'Spearman') == 1 + + if nargout>4 + if ~isnan(r); subplot(1,2,1); end + M = sprintf('Skipped correlation \n Spearman r=%g CI=[%g %g]',r,CI(1),CI(2)); + else + M = sprintf('Skipped correlation \n Spearman r=%g h=%g',r,h); + end + + scatter(a{1},b{1},100,'b','fill'); + grid on; hold on; + hh = lsline; set(hh,'Color','r','LineWidth',4); + try + [XEmin, YEmin] = ellipse(a{column},b{column}); + plot(real(XEmin), real(YEmin),'LineWidth',2); + MM = [min(XEmin) max(XEmin) min(YEmin) max(YEmin)]; + catch ME + text(min(x)+0.01*(min(x)),max(y),'no ellipse found','Fontsize',12) + MM = []; + end + xlabel('X','Fontsize',12); ylabel('Y','Fontsize',12); + title(M,'Fontsize',16); + + % add outliers and scale axis + scatter(x(outid{1}),y(outid{1}),100,'r','filled'); + MM2 = [min(x) max(x) min(y) max(y)]; + if isempty(MM); MM = MM2; end + A = floor(min([MM(:,1);MM2(:,1)]) - min([MM(:,1);MM2(:,1)])*0.01); + B = ceil(max([MM(:,2);MM2(:,2)]) + max([MM(:,2);MM2(:,2)])*0.01); + C = floor(min([MM(:,3);MM2(:,3)]) - min([MM(:,3);MM2(:,3)])*0.01); + D = ceil(max([MM(:,4);MM2(:,4)]) + max([MM(:,4);MM2(:,4)])*0.01); + axis([A B C D]); + box on;set(gca,'Fontsize',14) + + if nargout>4 && sum(~isnan(CIpslope))==2 + % add CI + y1 = refline(CIsslope(1),CIsintercept(1)); set(y1,'Color','r'); + y2 = refline(CIsslope(2),CIsintercept(2)); set(y2,'Color','r'); + y1 = get(y1); y2 = get(y2); + xpoints=[[y1.XData(1):y1.XData(2)],[y2.XData(2):-1:y2.XData(1)]]; + step1 = y1.YData(2)-y1.YData(1); step1 = step1 / (y1.XData(2)-y1.XData(1)); + step2 = y2.YData(2)-y2.YData(1); step2 = step2 / (y2.XData(2)-y2.XData(1)); + filled=[[y1.YData(1):step1:y1.YData(2)],[y2.YData(2):-step2:y2.YData(1)]]; + hold on; fillhandle=fill(xpoints,filled,[1 0 0]); + set(fillhandle,'EdgeColor',[1 0 0],'FaceAlpha',0.2,'EdgeAlpha',0.8);%set edge color + + % add histograms of bootstrap + subplot(1,2,2); k = round(1 + log2(length(rsb))); hist(rsb,k); grid on; + mytitle = sprintf('Bootstrapped \n Spearmans'' corr h=%g', hboot); + title(mytitle,'FontSize',16); hold on + xlabel('boot correlations','FontSize',14);ylabel('frequency','FontSize',14) + plot(repmat(CI(1),max(hist(rsb,k)),1),[1:max(hist(rsb,k))],'r','LineWidth',4); + plot(repmat(CI(2),max(hist(rsb,k)),1),[1:max(hist(rsb,k))],'r','LineWidth',4); + axis tight; colormap([.4 .4 1]) + box on;set(gca,'Fontsize',14) + end + + else + if nargout>4 + if ~isnan(r.Pearson); subplot(1,3,1); end + M = sprintf('Skipped correlation \n Pearson r=%g CI=[%g %g] \n Spearman r=%g CI=[%g %g]',r.Pearson,CI.Pearson(1),CI.Pearson(2),r.Spearman,CI.Spearman(1),CI.Spearman(2)); + else + M = sprintf('Skipped correlation \n Pearson r=%g h=%g \n Spearman r=%g h=%g',r.Pearson,h.Pearson,r.Spearman,h.Spearman); + end + + scatter(a{1},b{1},100,'b','fill'); + grid on; hold on; + hh = lsline; set(hh,'Color','r','LineWidth',4); + try + [XEmin, YEmin] = ellipse(a{column},b{column}); + plot(real(XEmin), real(YEmin),'LineWidth',2); + MM = [min(XEmin) max(XEmin) min(YEmin) max(YEmin)]; + catch ME + text(min(x)+0.01*(min(x)),max(y),'no ellipse found','Fontsize',12) + MM = []; + end + xlabel('X','Fontsize',12); ylabel('Y','Fontsize',12); + title(M,'Fontsize',16); + + % add outliers and scale axis + scatter(x(outid{1}),y(outid{1}),100,'r','filled'); + MM2 = [min(x) max(x) min(y) max(y)]; + if isempty(MM); MM = MM2; end + A = floor(min([MM(:,1);MM2(:,1)]) - min([MM(:,1);MM2(:,1)])*0.01); + B = ceil(max([MM(:,2);MM2(:,2)]) + max([MM(:,2);MM2(:,2)])*0.01); + C = floor(min([MM(:,3);MM2(:,3)]) - min([MM(:,3);MM2(:,3)])*0.01); + D = ceil(max([MM(:,4);MM2(:,4)]) + max([MM(:,4);MM2(:,4)])*0.01); + axis([A B C D]); + box on;set(gca,'Fontsize',14) + + if nargout>4 && sum(~isnan(CIpslope))==2 + % add CI + y1 = refline(CIpslope(1),CIpintercept(1)); set(y1,'Color','r'); + y2 = refline(CIpslope(2),CIpintercept(2)); set(y2,'Color','r'); + y1 = get(y1); y2 = get(y2); + xpoints=[[y1.XData(1):y1.XData(2)],[y2.XData(2):-1:y2.XData(1)]]; + step1 = y1.YData(2)-y1.YData(1); step1 = step1 / (y1.XData(2)-y1.XData(1)); + step2 = y2.YData(2)-y2.YData(1); step2 = step2 / (y2.XData(2)-y2.XData(1)); + filled=[[y1.YData(1):step1:y1.YData(2)],[y2.YData(2):-step2:y2.YData(1)]]; + hold on; fillhandle=fill(xpoints,filled,[1 0 0]); + set(fillhandle,'EdgeColor',[1 0 0],'FaceAlpha',0.2,'EdgeAlpha',0.8);%set edge color + + % add histograms of bootstrap + subplot(1,3,2); k = round(1 + log2(length(rpb))); hist(rpb,k); grid on; + mytitle = sprintf('Bootstrapped \n Pearsons'' corr h=%g', hboot.Pearson); + title(mytitle,'FontSize',16); hold on + xlabel('boot correlations','FontSize',14);ylabel('frequency','FontSize',14) + plot(repmat(CI.Pearson(1),max(hist(rpb,k)),1),[1:max(hist(rpb,k))],'r','LineWidth',4); + plot(repmat(CI.Pearson(2),max(hist(rpb,k)),1),[1:max(hist(rpb,k))],'r','LineWidth',4); + axis tight; colormap([.4 .4 1]) + box on;set(gca,'Fontsize',14) + + subplot(1,3,3); k = round(1 + log2(length(rsb))); hist(rsb,k); grid on; + mytitle = sprintf('Bootstrapped \n Spearmans'' corr h=%g', hboot.Spearman); + title(mytitle,'FontSize',16); hold on + xlabel('boot correlations','FontSize',14);ylabel('frequency','FontSize',14) + plot(repmat(CI.Spearman(1),max(hist(rsb,k)),1),[1:max(hist(rsb,k))],'r','LineWidth',4); + plot(repmat(CI.Spearman(2),max(hist(rsb,k)),1),[1:max(hist(rsb,k))],'r','LineWidth',4); + axis tight; colormap([.4 .4 1]) + box on;set(gca,'Fontsize',14) + end + end + end + + if strcmp(answer,'yes') + for f = 1:p + if fig_flag == 1 + figure('Name',[num2str(f) ' Skipped correlation']) + set(gcf,'Color','w'); + end + + if nargout >4 + if ~isnan(r(f)); subplot(1,3,1); index = 3; else subplot(1,2,1); index = 2; end + M = sprintf('Spearman skipped correlation r=%g \n %g%%CI [%g %g]',r(f),(1-level)*100,CI(1,f),CI(2,f)); + else + subplot(1,2,1); index = 2; + M = sprintf('Spearman skipped correlation \n r=%g h=%g',r(f),h(f)); + end + + % plot data with outliers identified + scatter(a{f},b{f},100,'b','fill'); + grid on; hold on; + hh = lsline; set(hh,'Color','r','LineWidth',4); + try + [XEmin, YEmin] = ellipse(a{f},b{f}); + plot(XEmin, YEmin,'LineWidth',2); + MM = [min(XEmin) max(XEmin) min(YEmin) max(YEmin)]; + catch ME + text(min(a{f})+0.01*(min(a{f})),max(b{f}),'no ellipse found','Fontsize',12) + MM = []; + end + xlabel('X','Fontsize',12); ylabel('Y','Fontsize',12); + title('Outlier detection','Fontsize',16); + + % add outliers and scale axis + scatter(x(outid{f},f),y(outid{f},f),100,'r','filled'); + MM2 = [min(x(:,f)) max(x(:,f)) min(y(:,f)) max(y(:,f))]; + if isempty(MM); MM = MM2; end + A = floor(min([MM(:,1);MM2(:,1)]) - min([MM(:,1);MM2(:,1)])*0.01); + B = ceil(max([MM(:,2);MM2(:,2)]) + max([MM(:,2);MM2(:,2)])*0.01); + C = floor(min([MM(:,3);MM2(:,3)]) - min([MM(:,3);MM2(:,3)])*0.01); + D = ceil(max([MM(:,4);MM2(:,4)]) + max([MM(:,4);MM2(:,4)])*0.01); + axis([A B C D]); + box on;set(gca,'Fontsize',14) + + % plot the rank and Spearman + subplot(1,index,2); + xrank = tiedrank(a{f},0); + yrank = tiedrank(b{f},0); + scatter(xrank,yrank,100,'b','fill'); grid on; hold on + hh = lsline; set(hh,'Color','r','LineWidth',4); axis tight + xlabel('X rank','Fontsize',12); ylabel('Y rank','Fontsize',12); + title(M,'Fontsize',16); + box on;set(gca,'Fontsize',14) + + if nargout>4 && sum(isnan(CIpslope(:,f))) == 0 + % add CI + y1 = refline(CIsslope(1,f),CIsintercept(1,f)); set(y1,'Color','r'); + y2 = refline(CIsslope(2,f),CIsintercept(2,f)); set(y2,'Color','r'); + y1 = get(y1); y2 = get(y2); + xpoints=[[y1.XData(1):y1.XData(2)],[y2.XData(2):-1:y2.XData(1)]]; + step1 = y1.YData(2)-y1.YData(1); step1 = step1 / (y1.XData(2)-y1.XData(1)); + step2 = y2.YData(2)-y2.YData(1); step2 = step2 / (y2.XData(2)-y2.XData(1)); + filled=[[y1.YData(1):step1:y1.YData(2)],[y2.YData(2):-step2:y2.YData(1)]]; + hold on; fillhandle=fill(xpoints,filled,[1 0 0]); + set(fillhandle,'EdgeColor',[1 0 0],'FaceAlpha',0.2,'EdgeAlpha',0.8);%set edge color + + % add histograms of bootstrap + subplot(1,3,3); k = round(1 + log2(length(rsb(:,f)))); hist(rsb(:,f),k); grid on; + title(['Bootstrapped correlations h=' num2str(hboot(f))],'FontSize',16); hold on + xlabel('boot correlations','FontSize',14);ylabel('frequency','FontSize',14) + plot(repmat(CI(1,f),max(hist(rsb(:,f),k)),1),[1:max(hist(rsb(:,f),k))],'r','LineWidth',4); + plot(repmat(CI(2,f),max(hist(rsb(:,f),k)),1),[1:max(hist(rsb(:,f),k))],'r','LineWidth',4); + axis tight; colormap([.4 .4 1]) + box on;set(gca,'Fontsize',14) + end + end + end +end + + + +end + + +%% ploting with an ellipse around the good data points +function [XEmin, YEmin] = ellipse(X, Y) + +% Ellipse function - 15th September 2008 +% Returns X and Y values for an ellipse tightly surrounding all the data points +% Designed by Julien Rouger, Voice Neurocognition Laboratory +% Department of Psychology, University of Glasgow + +% Check data format +if size(X, 1) > size(X, 2), X = X'; end +if size(Y, 1) > size(Y, 2), Y = Y'; end + +% If the ellipse contains the convex hull, it will contain all data points +k = convhull(X, Y); k = k(1:end-1); + +th = 0:pi/1000:2*pi; +ct = cos(th); st = sin(th); +xo = X(k); yo = Y(k); +n = size(xo, 2); + +area = Inf; + +% ================================================================================================================================= +% Find best matching ellipse for any given four anchors in the convex hull +for t = 0:pi/16:2*pi + ct0 = cos(t); st0 = sin(t); + x = xo * ct0 + yo * st0; + y = -xo * st0 + yo * ct0; + + % Four nested loops to get only once all ordered groups of 4 points + for f = 1:n - 3 + for g = f + 1:n - 2 + for h = g + 1:n - 1 + for i = h + 1:n + coef1 = [x(f)^2 - x(g)^2; -2*(x(f) - x(g)); y(f)^2 - y(g)^2; -2*(y(f) - y(g))]; + coef2 = [x(f)^2 - x(h)^2; -2*(x(f) - x(h)); y(f)^2 - y(h)^2; -2*(y(f) - y(h))]; + coef3 = [x(f)^2 - x(i)^2; -2*(x(f) - x(i)); y(f)^2 - y(i)^2; -2*(y(f) - y(i))]; + + % Gaussian elimination + coef1 = coef1 * coef3(4) - coef3 * coef1(4); + coef2 = coef2 * coef3(4) - coef3 * coef2(4); + coef1 = coef1 * coef2(2) - coef2 * coef1(2); + + % k = b^2/a^2 + k = -coef1(3) / coef1(1); + + % k negative -> no solution for these 4 points + if k > 0 + coef2(3) = coef2(3) + coef2(1) * k; coef2(1) = 0; + coef3(3) = coef3(3) + coef3(1) * k; coef3(1) = 0; + + % Gaussian elimination + coef2(2) = coef2(2) * k; coef3(2) = coef3(2) * k; + xc = -coef2(3) / coef2(2); + yc = -(coef3(2) * xc + coef3(3)) / coef3(4); + a = sqrt((x(f) - xc)^2 + (y(f) - yc)^2 / k); + b = sqrt(k * a^2); + XE = xc + a * ct; + YE = yc + b * st; + + % Check if ellipse contains all points from the convex hull + ok = 1; + for j = 1:n + dx = x(j) - xc; dy = y(j) - yc; + rx = dx / a; + ry = dy / b; + if rx * rx + ry * ry > 1.0001 + ok = 0; + end + end + + % Update best fitting ellipse + if ok == 1 && a * b < area + area = a * b; + amin = a; + bmin = b; + xcmin = xc; + ycmin = yc; + tmin = t; + end + end + end + end + end + end +end + +if area < Inf + ct0 = cos(tmin); st0 = sin(tmin); + XE = xcmin + amin * ct; + YE = ycmin + bmin * st; + XEmin = XE * ct0 - YE * st0; + YEmin = XE * st0 + YE * ct0; +end; + + +% Previous part found the best matching ellipse for any group of 4 points +% That's fine, but may be there exist better matches using groups of 3 points + +% ================================================================================================================================= +% Find best matching ellipse for any given three anchors in the convex hull +x = xo; y = yo; + +% Three nested loops to get only once all ordered groups of 3 points +for f = 1:n - 2 + for g = f + 1:n - 1 + for h = g + 1:n + xc = (x(f) + x(g) + x(h)) / 3; + yc = (y(f) + y(g) + y(h)) / 3; + + % Centre of gravity of this triangle + a(1) = x(f) - xc; b(1) = y(f) - yc; + a(2) = x(g) - xc; b(2) = y(g) - yc; + a(3) = x(h) - xc; b(3) = y(h) - yc; + + % Newton iterative method + theta = pi; error = 1; + while abs(error) > 1e-4 + cth = cos(theta); sth = sin(theta); + a1 = a(1) * cth + b(1) * sth; b1 = -a(1) * sth + b(1) * cth; + a2 = a(2) * cth + b(2) * sth; b2 = -a(2) * sth + b(2) * cth; + a3 = a(3) * cth + b(3) * sth; b3 = -a(3) * sth + b(3) * cth; + da1 = a1 - a2; da2 = a2 - a3; da3 = a3 - a1; + db1 = b1 - b2; db2 = b2 - b3; db3 = b3 - b1; + fth = (da1.^2 - da2.^2).*(db1.^2 - db3.^2)-(da1.^2 - da3.^2).*(db1.^2 - db2.^2); + dfth = 2*(da1.*db1 - da2.*db2).*(db1.^2 - db3.^2 + da1.^2 - da3.^2)- 2*(db1.*da1 - db3.*da3).*(da1.^2 - da2.^2 + db1.^2 - db2.^2); + error = - fth / dfth; + theta = theta + error; + end + cth = cos(theta); sth = sin(theta); + a1 = a(1) * cth + b(1) * sth; b1 = -a(1) * sth + b(1) * cth; + a2 = a(2) * cth + b(2) * sth; b2 = -a(2) * sth + b(2) * cth; + a3 = a(3) * cth + b(3) * sth; b3 = -a(3) * sth + b(3) * cth; + da1 = a1 - a2; da2 = a2 - a3; + db1 = b1 - b2; db2 = b2 - b3; + k = sqrt(-(da1.^2 - da2.^2) / (db1.^2 - db2.^2)); + + R = sqrt(((a1 - a2)^2 + k^2 * (b1 - b2)^2) / 3); + + XE = xc + R * (ct * cth - st / k * sth); + YE = yc + R * (ct * sth + st / k * cth); + + % Check if ellipse contains all points from the convex hull + ok = 1; + for i = 1:n + dx = x(i) - xc; dy = y(i) - yc; + rx = dx * cth + dy * sth; + ry = k * (-dx * sth + dy * cth); + if rx * rx + ry * ry > 1.0001 * R^2 + ok = 0; + end + end + + % Update best fitting ellipse + if ok == 1 && R * R / k < area + area = R * R / k; + XEmin = XE; + YEmin = YE; + end + end + end +end +end diff --git a/univar.m b/univar.m new file mode 100644 index 0000000..02d1e84 --- /dev/null +++ b/univar.m @@ -0,0 +1,32 @@ +function [nu,x,h,xp,yp]=univar(X) + +% Computes the univariate pdf of data and the histogram values. +% Returns the frequency of data per bin (nu), the position of the bins (x) +% and their size (h). The pdf is returned in yp for the xp values +% +% Cyril Pernet v2 (10-01-2014 - deals with NaN) +% --------------------------------- +% Copyright (C) Corr_toolbox 2012 + +for c=1:size(X,2) + data = X(~isnan(X(:,c)),c); + mu = mean(data); + v = var(data); + + % get the normal pdf for this distribution + xp = linspace(min(data),max(data)); + if v <= 0 + error('Variance must be greater than zero') + return + end + arg = ((xp-mu).^2)/(2*v); + cons = sqrt(2*pi)*sqrt(v); + yp = (1/cons)*exp(-arg); + + % get histogram info using Surges' rule: + k = round(1 + log2(length(data))); + [nu{c},x{c}]=hist(data,k); + h{c} = x{c}(2) - x{c}(1); +end + +if c==1; nu = nu{1}; x = x{1}; h = h{1}; end diff --git a/variance_homogeneity.m b/variance_homogeneity.m new file mode 100644 index 0000000..29de698 --- /dev/null +++ b/variance_homogeneity.m @@ -0,0 +1,95 @@ +function [h,CI] = variance_homogeneity(x,y,condition) + +% Compares variances using a 95% percentile bootstrap CI. +% +% FORMAT: [h,CI] = variance_homogeneity(x,y) +% [h,CI] = variance_homogeneity(x,y,condition) +% +% INPUTS: x and y are two vectors of the same length +% condition = 0/1 (default=0) to condition x and y on each other +% +% OUTPUTS: h indicates if the data have the same variance (0) or not (1) +% CI is the 95% confidence interval of the difference between variances +% +% see also CONDITIONAL. + +% Cyril Pernet v1 +% --------------------------------- +% Copyright (C) Corr_toolbox 2012 + +if nargin == 2 + condition = 1; +end + +if size(x)~=size(y) + error('X and Y must have the same size') +end + +[x,y]=pairwise_cleanup(x,y); + +% computes +nboot = 600; +nm = length(x); +if nm < 40; l=6; u=593; +elseif nm >= 40 && nm < 80; l=7; u=592; +elseif nm >= 80 && nm < 180; l=10; u=589; +elseif nm >= 180 && nm < 250; l=13; u=586; +elseif nm >= 250; l=15; u=584; end + +% boostrap +table = randi(nm,nm,nboot); +for B=1:nboot + % resample + a = x(table(:,B)); + b = y(table(:,B)); + if condition == 1 + [values,variances]=conditional(a(:),b(:)); + Diff(B) = variances(1) - variances(2); + else + Diff(B) = var(a) - var(b); + end +end + +Diff = sort(Diff); +CI = [Diff(l+1) Diff(u)]; +if sum(isnan(Diff)) ~=0 + adj_nboot = nboot - sum(isnan(Diff)); + adj_l = round((5/100*adj_nboot)/2); + adj_u = adj_nboot - adj_l; + CI = [Diff(adj_l+1) Diff(adj_u)]; +end + + +% plot +figure('Name','Heteroscedasticity test'); +set(gcf,'Color','w'); +k = round(1 + log2(nboot)); +[n,p] = hist(Diff,k); +bar(p,n,1,'FaceColor',[0.5 0.5 1]); +grid on; axis tight; +ylabel('frequency','Fontsize',14); hold on +plot(repmat(CI(1),max(hist(Diff,k)),1),[1:max(hist(Diff,k))],'r','LineWidth',4); +plot(repmat(CI(2),max(hist(Diff,k)),1),[1:max(hist(Diff,k))],'r','LineWidth',4); +if CI(1) < 0 && CI(2) > 0 + h = 0; + if condition == 1 + mytitle = sprintf('Test on conditional variances: \n data are homoscedastic 95%% CI [%g %g]',CI(1),CI(2)); + xlabel('differences of conditional variances between X and Y','Fontsize',14); + else + mytitle = sprintf('Test on variances: \n data are homoscedastic 95%% CI [%g %g]',CI(1),CI(2)); + xlabel('differences of variances between X and Y','Fontsize',14); + end +else + h = 1; + if condition == 1 + mytitle = sprintf('Test on conditional variances: \n data are heteroscedastic 95%% CI [%g %g]',CI(1),CI(2)); + xlabel('differences of conditional variances between X and Y','Fontsize',14); + else + mytitle = sprintf('Test on variances: \n data are heteroscedastic 95%% CI [%g %g]',CI(1),CI(2)); + xlabel('differences of variances between X and Y','Fontsize',14); + end +end +title(mytitle,'Fontsize',14) +box on;set(gca,'Fontsize',14) + +