├── .gitignore ├── README.md ├── package.json ├── src ├── arrays │ └── index.js ├── decomp │ └── index.js ├── distributions │ └── index.js ├── misc │ └── index.js ├── models │ └── index.js ├── prep │ └── index.js ├── stat │ └── index.js └── tests │ └── index.js └── test ├── tests-arrays-heavy.js ├── tests-arrays.js ├── tests-decomp.js ├── tests-distributions.js ├── tests-misc.js ├── tests-models.js ├── tests-prep.js ├── tests-speed.js ├── tests-stat.js ├── tests-temp.js └── tests-tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/build/ 3 | .#package.json 4 | package-lock.json 5 | .DS_Store 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Javascript library for statistics and multivariate data analysis 2 | 3 | A simple library with implementation of most common methods for descriptive and inferential statistics as well as matrix operations, decompositions and projection based methods for multivariate data analysis. The library is currently under development, breaking changes may occur in the coming versions. 4 | 5 | ## What is new 6 | 7 | Version **1.x.x** introduces many breaking changes as the library was almost fully re-written from the scratch. If you used a pre-release version (0.6.1), do not upgrade. 8 | 9 | The documentation below has been also re-written to match the new version. 10 | 11 | ### v. 1.4.0 12 | released 24/09/2024 13 | 14 | * added methods for incomplete beta function, CDF anf quantile function for F-distribution, and corresponding helper methods. 15 | 16 | ### v. 1.3.x 17 | 18 | * contain small bug fixes. 19 | 20 | ### v. 1.3.0 21 | released 22/09/2023 22 | 23 | * improvements to chi-square distribution functions (`qchisq` and `pchisq`), to make better approximation, especially when DoF is large (above 30). 24 | 25 | ### v. 1.2.0 26 | released 15/09/2023 27 | 28 | * improvements to singular value decomposition methods (`svd` and `rsvd`). 29 | * refactoring and small improvements for PCA methods. 30 | 31 | 32 | ## Matrices, vectors and indices 33 | 34 | The values are represented using instances of the following classes: 35 | 36 | * `Vector` is a class for representing sequence of values. The values are stored inside the class instances as `Float64Array`. 37 | * `Matrix` is a class for representing numerical matrices (2D Arrays). The values are stored inside the class instances as `Float64Array`. 38 | * `Index` is a class for representing vectors of indices — integer numbers specifying positions of values in vectors and matrices. The values are stored inside the class instances as `Int32Array`. 39 | * `Factor` is a class for representing categorical variable, which is turned to array of labels for the categories (as strings) and array of indices (as `Uint8Array`). 40 | 41 | These classes and their methods can be imported from `'mdatools/arrays'` module. 42 | 43 | ### Vectors of indices 44 | 45 | The simplest way to create an instance of `Index` class is to use method `index(x)` where `x` is a conventional JavaScript array, e.g. `index([1, 2, 3])`. Alternatively index values can be generated as a sequence using static method `seq(start, end, by)`: 46 | 47 | ```javascript 48 | import { Index } from 'mdatools/arrays'; 49 | 50 | const ind1 = Index.seq(1, 5); // returns index([1, 2, 3, 4, 5]) 51 | const ind2 = Index.seq(2, 10, 2); // returns index([2, 4, 6, 8, 10]) 52 | ``` 53 | 54 | There are also two additional static methods for creating index vectors: 55 | 56 | * `Index.fill(v, n)` — creates an index vector of size `n` filled with integer value `v`. 57 | * `Index.ones(n)` — creates an index vector of size `n` filled with ones. 58 | 59 | Values of any index vector can be replicated using class methods (here `ind` is an instance of class `Index`): 60 | 61 | * `ind.rep(n)` — replicates the index vector `n` times. 62 | * `ind.repeach(n)` — replicates each value from the index vector `n` times. 63 | * `ind.sort([decreasing=false])` — sorts indices in `ind`. 64 | * `ind.shuffle()` — shuffles indices in `ind` using Fisher–Yates algorithm. 65 | 66 | All methods above return instance of `Index` as a result. It must be noted that when indices are used for subsetting values in vectors and matrices, it is expected that they start from 1, not 0. 67 | 68 | Two (or more) vectors with indices, e.g. `i1` and `i2` can be concatenated into one by using method `i = c(i1, i2)`. 69 | 70 | ### Class Vector 71 | 72 | The simplest way to create an instance of `Vector` class is to use method `vector(x)` where `x` is a conventional JavaScript array, e.g. `vector([1.1, 2.2, 3.3])`. The values can also be generated using the following static methods: 73 | 74 | * `Vector.zeros(n)` — creates a vector of size `n` filled with zeros. 75 | * `Vector.fill(v, n)` — creates a vector of size `n` filled with value `v`. 76 | * `Vector.ones(n)` — creates a vector of size `n` filled with ones. 77 | * `Vector.rand(n, [a=0], [b=1])` — creates a vector of size `n` filled with random values uniformly distributed between `a` and `b`. 78 | * `Vector.randn(n, [mu=0], [sigma=1])` — creates a vector of size `n` filled with normally distributed random values. 79 | * `Vector.seq(start, end, by)` — creates a sequence of values, similar to the `seq` method for `Index` class. 80 | * `Vector.c(a, b, ...)` — concatenates any amount of numbers, arrays or/and vectors into a vector. 81 | 82 | The `Vector` object object also has class methods `rep(n)` and `repeach(n)` which work similar to the methods for `Index` objects. 83 | 84 | Similar to indices, several vectors can be concatenated into a single vector by using method `c`: 85 | 86 | ```javascript 87 | import {vector, c} from 'mdatools/arrays'; 88 | 89 | const v1 = vector([1, 2, 3]); 90 | const v2 = vector([10, 11, 12]); 91 | const v3 = vector([20, 21, 22]); 92 | 93 | const x = c(v1, v2, v3); 94 | ``` 95 | 96 | Other class methods are listed below. 97 | 98 | #### Method for subsetting of vectors 99 | 100 | The indices used for subsetting of vectors and matrices, must start with one, not zero. All subsetting methods create a copy of values (not a reference to original values). 101 | 102 | * `x.subset(ind)` — creates a subset of vector `x` by taking values, whose position is specified by vector with indices `ind`. 103 | * `x.slice(start, end)` — creates a subset of vector `x` by taking a sequence of values between the two positions (both are included to the subset). 104 | * `x.copy()` — creates a deep copy of the vector. 105 | 106 | #### Methods for manipulation with vector values 107 | 108 | Following methods do something with vector values, e.g. arithmetic operations, transformations, etc. 109 | 110 | * `x.apply(fun)` — applies function `fun` to every value of `x` and creates a new vector with values returned by the function. 111 | * `x.add(y)` — adds values from another vector or a number. 112 | * `x.subtract(y)` — subtracts values from another vector or a number. 113 | * `x.mult(y)` — multiplies to values from another vector or to a number. 114 | * `x.divide(y)` — divides by values from another vector or by a number. 115 | * `x.dot(y)` — takes a dot (inner) product of two vectors (results in a number). 116 | * `x.sort([decreasing=false])` — sorts values in `x`. 117 | * `x.shuffle()` — shuffles values in `x` using Fisher–Yates algorithm. 118 | 119 | ### Class Factor 120 | 121 | Factors are vectors representing categorical variables. To create an instance of `Factor` use method `factor()` and provide it an array of categories as strings: 122 | 123 | ```javascript 124 | import {factor} from 'mdatools/arrays'; 125 | 126 | const f = factor(["red", "green", "red", "green", "red", "green", "blue]); 127 | ``` 128 | 129 | Factor has following class methods: 130 | 131 | * `f.which(label)` — returns indices (as instance of `Index` class) of all entries of given label inside the factor. 132 | 133 | 134 | 135 | ### Methods for class Matrix 136 | 137 | The simplest way to create an instance of `Matrix` class is to use method `matrix(x, nrows, ncols)` where `x` is a conventional JavaScript array, e.g. `matrix([1, 2, 3, 4], 2, 2)`. The values can also be generated using the following static methods: 138 | 139 | * `Matrix.zeros(nrows, ncols)` — creates a matrix filled with zeros. 140 | * `Matrix.fill(v, nrows, ncols)` — creates a matrix filled with value `v`. 141 | * `Matrix.ones(nrows, ncols)` — creates a matrix filled with ones. 142 | * `Matrix.rand(nrows, ncols, [a=0], [b=1])` — creates a matrix filled with random values uniformly distributed between `a` and `b`. 143 | * `Matrix.randn(nrows, ncols, [mu=0], [sigma=1])` — creates a matrix filled with normally distributed random values. 144 | * `Matrix.eye(nrows, ncols)` — creates an identity matrix (can be rectangular). 145 | * `Matrix.diagm(x)` — creates a squared matrix with main diagonal filled with values from vector `x`. 146 | * `Matrix.outer(x, y, fun)` — creates a matrix by applying function `fun` to all possible pairs of values from vectors `x` and `y`. 147 | 148 | One can also create a matrix by reshaping a vector as shown below. 149 | 150 | 151 | ```javascript 152 | import { vector, reshape } from 'mdatools/arrays'; 153 | 154 | const x = vector([1, 3, 5, 7, 9, 11, 13, 15, 17]); 155 | const Y = reshape(x, 3, 3); 156 | ``` 157 | Matrix can be created by merging/binding several vectors as rows (method `rbind()`) or as columns (method `cbind()`) of the matrix: 158 | 159 | ```javascript 160 | import {vector, cbind, rbind} from 'mdatools/arrays'; 161 | 162 | const v1 = vector([1, 2, 3]); 163 | const v2 = vector([10, 11, 12]); 164 | const v3 = vector([20, 21, 22]); 165 | 166 | const X1 = cbind(v1, v2, v3); 167 | const X2 = rbind(v1, v2, v3); 168 | ``` 169 | Both methods can also bind matrices as well as vectors and matrices. 170 | 171 | #### Method for subsetting of matrices 172 | 173 | The indices in the methods below can be single numbers, conventional Javascript arrays or instances of Index class. They must start with one, not zero. All subsetting methods create a copy of values (not reference to original values). 174 | 175 | * `X.subset(rind, cind)` — creates a subset of matrix `X` by taking values, whose position is specified by vector with row and column indices. If all rows or all columns must be taken just provide an empty array, `[]` as a value for index argument. Indices must start from 1. 176 | * `X.copy()` — creates a deep copy of the matrix object `X`. 177 | * `X.getcolumn(ic)` — returns values of `ic`-th column of `X` as a vector (index must start from 1). 178 | * `X.getrow(ir)` — returns values of `ir`-th row of `X` as a vector (index must start from 1). 179 | * `X.diag()` — returns values from main diagonal of the matrix as a vector. 180 | 181 | One can also replace some of the values in a matrix by providing another matrix with new values as well as row and column indices for the values to replace. In the example below we replace values for all odd rows and columns. 182 | 183 | ```javascript 184 | import { matrix } from 'mdatools/arrays'; 185 | 186 | const X = matrix([1, 3, 5, 7, 9, 11, 13, 15, 17], 3, 3); 187 | const Y = matrix([2, 4, 6, 8], 2, 2); 188 | 189 | X.replace(Y, [1, 3], [1, 3]); 190 | ``` 191 | 192 | #### Methods for manipulation with matrix values 193 | 194 | Following methods do something with matrix values, e.g. arithmetic operations, transformations, etc. 195 | 196 | * `X.apply(fun, [dims=2])` — applies function `fun` to every row (if `dims=1`), every column (if `dims=2`) or every single value (if `dims=0`) of matrix `X` and returns either a vector or a new matrix with values returned by the function. 197 | * `X.t()` — transposes matrix `X` (swaps columns and rows). 198 | * `X.inv()` — computes inverse of squared matrix `X` (if exists). 199 | 200 | The following methods implement element-wise arithmetic operations with matrices, a matrix and a vector and a matrix and a number. 201 | 202 | * `X.add(Y, [dims=2])` — adds values from another matrix, a vector or a number. 203 | * `X.subtract(Y, [dims=2])` — subtracts values from another matrix, a vector or a number. 204 | * `X.mult(Y, [dims=2])` — multiplies to values from another matrix, a vector or to a number. 205 | * `X.divide(Y, [dims=2])` — divides by values from another matrix, a vector or from by a number. 206 | * `X.dot(Y, [dims=2])` — takes a dot product of two matrices or a matrix and a vector. 207 | 208 | In case if the argument is a vector its length must match one of the matrix dimension (number of rows or columns) and you need to specify this dimension explicitly by providing second argument, `dims`. In the example below we subtract mean from every column of matrix `X` (the statistical functions are introduced later in this document). 209 | 210 | ```javascript 211 | import { matrix } from 'mdatools/arrays'; 212 | import { mean } from 'mdatools/stat'; 213 | 214 | // create matrix X 215 | const X = matrix([1, 3, 5, 7, 9, 11, 13, 15, 17], 3, 3); 216 | 217 | // compute mean of every column of X 218 | const m = X.apply(mean, 2); 219 | 220 | // mean center columns of X 221 | const Y = X.subtract(m, 2); 222 | ``` 223 | 224 | Following methods are implemented as a standalone methods (not class methods): 225 | 226 | * `crossprod(X, Y)` — computes a cross-product of two matrices as $\mathbf{X}^\textrm{T}\mathbf{Y}$. 227 | * `tcrossprod(X, Y)` — computes a cross-product of two matrices as $\mathbf{XY}^\textrm{T}$. 228 | 229 | #### Additional methods for matrices 230 | 231 | There is also a set of methods which check property of a matrix: 232 | 233 | * `X.islowertriangular()` — returns `true` if `X` is lower triangular matrix. 234 | * `X.isuppertriangular()` — returns `true` if `X` is upper triangular matrix. 235 | 236 | You can also convert a matrix to a string which looks nice if you want to show/check the 237 | matrix values by e.g. `console.log()`: 238 | 239 | * `X.toString(ndec)` — here `ndec` is number of decimals to show in the output. 240 | 241 | Finally there are two methods which can be helpful when it is necessary to read a matrix values from a CSV file or save the values as CSV. 242 | 243 | Class method `X.toCSV([sep=","], [dec="."], [header=[]], [labels=[]])` — generates a string which can be saved as CSV file. The parameters are: `sep`: symbol to use for separating values, `dec`: symbol to use for separating decimals, `header`: a conventional Javascript array with column names (variable names), `labels`: a conventional Javascript array with row names (observation labels). 244 | 245 | Static method `Matrix.parseCSV(str, sep, hasHeader, hasLabels)` — parses a string, which is a result of reading a CSV file, into a JSON. The parameters are: `str`: string with CSV file content with rows delimited by `\n` or `\r\n`, `sep`: symbol to use for separating values, `hasHeader`: logical, does the data have header or not, `hasLabels`: logical, does the data have labels or not. The method returns a JSON with matrix values, symbols for values and decimal separators, arrays with header and labels. 246 | 247 | ## Descriptive statistics 248 | 249 | The following methods can be used to compute most common statistics (or a vector with statistics). The methods can work with both conventional Javascript arrays and instances of class `Vector` although it is recommended to provide the latter in any case. 250 | 251 | Methods can be imported from `'mdatools/stat'` module. 252 | 253 | ### Methods for computing single statistics 254 | 255 | * `min(x)` — returns the smallest value in vector `x`. 256 | * `minind(x)` — returns index (position) of the smallest value in vector `x`. 257 | * `max(x)` — returns the largest value in vector `x`. 258 | * `maxind(x)` — returns index (position) of the largest value in vector `x`.  259 | * `prod(x)` — returns a product of all values from vector `x`. 260 | * `sum(x)` — returns a sum of all values from vector `x`. 261 | * `mean(x)` — returns mean (average) of values from vector `x`. 262 | * `variance(x, [biased=false])` — returns variance of values from vector `x`. 263 | * `sd(x, [biased=false])` — returns standard deviation of values from vector `x`. 264 | * `ssq(x)` — returns a sum of squared values from vector `x`. 265 | * `norm2(x)` — returns a Eucledian norm (length) of vector `x`. 266 | * `skewness(x)` — returns skewness of values from vector `x`. 267 | * `kurtosis(x)` — returns kurtosis of values from vector `x`. 268 | * `cor(x, y, method='pearson')` — returns a correlation ("pearson" or "spearman") of values from the two vectors. 269 | * `cov(x, y)` — returns a covariance ("pearson" or "spearman") of values from the two vectors. 270 | 271 | 272 | ### Methods for computing vectors of statistics 273 | 274 | * `range(x)` — returns an array with two values, smallest and largest. 275 | * `mrange(x, m)` — computes range with margin, `m` is how much range must be stretched (e.g. `0.1` for 10%). 276 | * `rank(x)` — returns rank (position) for every value from vector `x`. 277 | * `cumsum(x)` — returns a cumulative sum of the values from vector `x`. 278 | * `ppoints(n)` — computes `n` probabilities 279 | * `diff(x)` — returns difference between the adjacent values from vector `x`. 280 | * `mids(x)` — returns middle points for each pair of values from vector `x`. 281 | * `split(x, n)` — splits the range of values in `x` into equal intervals (bins). 282 | * `count(x, s)` — counts how many values from vector `x`, falls into each interval from `s`. 283 | * `quantile(x, p)` — returns `p`-th quantiles computed based on values from vector `x`. 284 | 285 | ## Distributions 286 | 287 | The following methods implement Probability Density Function (PDF), Cumulative Distribution Function (CDF) and Inverse Cumulative Distribution Function (ICDF) a.k.a. quantile function for several known distributions. For uniform and normal distributions there are also functions which generate random numbers. 288 | 289 | Methods can be imported from `'mdatools/distributions'` module. 290 | 291 | 292 | * `dnorm(x, mu, sigma)`, `pnorm(x, mu, sigma)`, `qnorm(p, mu, sigma)`, `rnorm(x, mu, sigma)` — PDF, CDF, ICDF, and random numbers generator for normal distribution. Default values for the paramaters are `mu=0`, `sigma=1`. 293 | * `dunif(x, a, b)`, `punif(x, a, b)`, `runif(x, a, b)` — PDF, CDF, and random numbers generator for uniform distribution. Default values for the paramaters are `a=0`, `b=1`. 294 | * `dt(t, dof)`, `pt(x, mu, sigma)`, `qt(p, mu, sigma)` — PDF, CDF, abd ICDF for Student's t-distribution. 295 | * `df(F, dof1, dof2)`, `pf(F, dof1, dof2)` — PDF and CDF for F-distribution. 296 | * `pchisq(x, dof)`, `qchisq(p, dof)` — CDF and ICDF function for chi-square distribution. 297 | 298 | There are also a set of helper functions used to compute the values for the distributions, which can be useful: 299 | 300 | * `erf(x)` — error function for normal distribution. 301 | * `beta(x, y)` — Beta function (approximation via numerical integration). 302 | * `gamma(z)` — Gamma function (approximation). 303 | * `ibeta(x, a, b)` — incomplete Beta function (approximation via numerical integration). 304 | 305 | Because the distribution values are computed using approximated functions they are not very accurate. The accuracy achieved in tests is about 10-5 comparing to implementation of the corresponding methods in e.g. R. 306 | 307 | ## Additional methods useful in statistics 308 | 309 | Methods can be imported from `'mdatools/misc'` module. 310 | 311 | * `closestind(x, a)` — finds index of a value from vector `x` which is the closest to value `a`. 312 | * `integrate(f, a, b)` — computes an integral of function `f` with limits `a`and `b`. 313 | * `getoutliers(x)` — returns values from `x`, lying beyond the 1.5IQR distance from the first and the third quartiles. 314 | * `expandgrid(...args)` — generates all possible combinations of values from two or more vectors provided as arguments to the function. Values must be unique. 315 | 316 | ## Hypothesis testing 317 | 318 | Methods can be imported from `'mdatools/tests'` module. 319 | 320 | ### Student's t-test for means 321 | 322 | The tests compute the observed effect, standard error, t- and p-values as well 323 | as confidence interval and return everything as JSON. The two-sample t-test assumes 324 | that population variances are equal. Confidence intervals always computed around 325 | the observed effect and for both tails. 326 | 327 | * `ttest(x, mu=0, alpha=0.05, tail='both')` — one sample t-test 328 | * `ttest2(x, y, alpha=0.05, tail='both')` — two sample t-test 329 | 330 | The `x` and `y` arguments must be instances of `Vector` class. 331 | 332 | 333 | ## Decompositions 334 | 335 | Functions can be imported from `'mdatools/decomp'` module. 336 | 337 | Methods for computing decomposition of matrices and related methods (e.g. inverse). 338 | 339 | * `qr(X)` — computes QR decomposition of `X` using Householder reflections. 340 | * `lu(X)` — computes LU decomposition of `X` using Givens rotations. 341 | * `svd(X)` — computes SVD decomposition of `X` using Golub-Reinsch bidiagonalization. 342 | * `rsvd(X)` — randomized version of SVD which is must faster than the original method. 343 | 344 | ## Preprocessing 345 | 346 | Functions can be imported from `'mdatools/prep'`. 347 | 348 | Methods for preprocessing of data values. Every method takes a matrix as a main argument and returns a matrix as a result (plus potentially other outcomes). 349 | 350 | * `scale(X, [center=true], [scale=false])` — center and scale each column of matrix `X`. If values for `center` and `scale` arguments are logical, the method will do standardization (using mean values for centring and standard deviation for scaling). Alternatively the values for these two arguments can be specified by user as a vector of values. Number of values must be equal to the number of columns in `X`. 351 | * `unscale(X, centerValues, scaleValues)` — does the inverse to `scale()` operation using vectors with values used to center and scale the data. 352 | 353 | More methods are coming in future versions. 354 | 355 | ## Modelling 356 | 357 | Functions can be imported from `'mdatools/models'`. 358 | 359 | Methods for fitting various models. Every method returns a JSON with fitted model parameters, their inference (where it can be done). The prediction methods contain the predicted values as well as performance statistics (when it is possible to compute). 360 | 361 | ### Simple and multiple linear regression 362 | * `lmfit(X, y)` — fits linear regression model (simple or multiple). 363 | * `lmpredict(m, X)` — computes predictions using model from `lmfit()` and vector or matrix with predictors. 364 | 365 | 366 | ### Polynomial regression 367 | * `polyfit(x, y, d)` — polynomial regression model (univariate). 368 | * `polypredict(m, x)` — computes predictions using polynomial model from `polyfit()`. 369 | 370 | ### Principal component anaylsis 371 | 372 | * `pcafit(X, ncomp)` — fits PCA model with given number of components. 373 | * `pcapredict(m, X)` — projects data from `X` to the PCA model and computes main outcomes (scores, distances, variance, etc.). 374 | 375 | ### DD-SIMCA classification 376 | 377 | DD-SIMCA classification is done based on PCA model with additional parameters. 378 | 379 | * `getsimcaparams(className, alpha, limType)` — creates an object with SIMCA parameters, necessary for classification. 380 | * `simcapredict(m, params, X, [cRef])` — classifies rows of data matrix `X` using PCA model `m` and DD-SIMCA parameters `params`. If factor with reference classes, `cRef` is provided, the method also computes classification statistics and adds it to he outcome. 381 | 382 | ### Principal components regression 383 | 384 | * `pcrfit(X, Y, ncomp)` — fits PCR model with given number of components. 385 | * `pcrpredict(m, X, [Y])` — projects data from `X` to the PCR model and computes main outcomes (predicted y-values for each component, scores, distances, variances and other performance statistics). 386 | * 387 | ### Partial least squares regression 388 | 389 | * `plsfit(X, Y, ncomp)` — fits PLS1 model with given number of components. 390 | * `plspredict(m, X, [Y])` — projects data from `X` to the PLS model and computes main outcomes (predicted y-values for each component, scores, distances, variances and other performance statistics). 391 | 392 | 393 | 394 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mdatools", 3 | "version": "1.4.0", 4 | "private": false, 5 | "type": "module", 6 | "description": "Tools and methods for statistics and multivariate data analysis.", 7 | "scripts": { 8 | "test": "mocha", 9 | "speedtest": "node ./test/tests-speed.js" 10 | }, 11 | "exports": { 12 | "./arrays": "./src/arrays/index.js", 13 | "./decomp": "./src/decomp/index.js", 14 | "./distributions": "./src/distributions/index.js", 15 | "./misc": "./src/misc/index.js", 16 | "./models": "./src/models/index.js", 17 | "./prep": "./src/prep/index.js", 18 | "./stat": "./src/stat/index.js", 19 | "./tests": "./src/tests/index.js" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/svkucheryavski/mdatools-js/" 24 | }, 25 | "keywords": [ 26 | "statistics" 27 | ], 28 | "author": "Sergey Kucheryavskiy (https://mda.tools)", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/svkucheryavski/mdatools-js/issues" 32 | }, 33 | "homepage": "https://github.com/svkucheryavski/mdatools-js/", 34 | "devDependencies": { 35 | "chai": "^4.x", 36 | "chai-almost": "^1.x", 37 | "mocha": "^10.x" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/decomp/index.js: -------------------------------------------------------------------------------- 1 | /************************************************************/ 2 | /* Methods for computing matrix decompositions */ 3 | /************************************************************/ 4 | 5 | import { ssq } from '../stat/index.js'; 6 | import { crossprod, Vector, Matrix, Index } from '../arrays/index.js'; 7 | 8 | 9 | /** 10 | * Randomized SVD decomposition. 11 | * 12 | * @param {Matrix} X - matrix to decompose. 13 | * @param {number} [ncomp] - number of components. 14 | * @param {number} [pa=1] - oversampling factor (ncomp * pa + pb). 15 | * @param {number} [pb=10] - oversampling increment (ncomp * pa + pb). 16 | * @param {number} [its=3] - number of iterations 17 | * 18 | * @returns {JSON} JSON with three fields, 's' - vector with singular values, 19 | * 'U', 'V' - matrices with left and right singular vectors. 20 | * 21 | */ 22 | export function rsvd(X, ncomp, pa, pb, its) { 23 | 24 | const m = X.nrows; 25 | const n = X.ncols; 26 | 27 | if (its === undefined) { 28 | its = 3; 29 | } 30 | 31 | if (pb === undefined) { 32 | pb = 5; 33 | } 34 | 35 | if (pa === undefined) { 36 | pa = 1; 37 | } 38 | 39 | if (its < 1 || its > 10) { 40 | throw new Error('rsvd: wrong value for parameter "its" (must be between 1 and 10).'); 41 | } 42 | 43 | if (pa < 1 || pa > 5) { 44 | throw new Error('rsvd: wrong value for parameter "pa" (must be between 1 and 5).'); 45 | } 46 | 47 | if (pb < 1 || pb > 100) { 48 | throw new Error('rsvd: wrong value for parameter "pb" (must be between 1 and 100).'); 49 | } 50 | 51 | 52 | if (ncomp === undefined) { 53 | ncomp = Math.round(Math.min(m - 1, n) / 1.5); 54 | } 55 | 56 | if (m < n) { 57 | const res = rsvd(X.t(), ncomp, pa, pb, its); 58 | return {s: res.s, V: res.U, U: res.V} 59 | } 60 | 61 | // the more the better but slower 62 | const l = Math.round(pa * ncomp + pb); 63 | let Q = X.dot(Matrix.rand(n, l, -1, 1)); 64 | 65 | for (let it = 1; it <= its; it++) { 66 | Q = lu(crossprod(X, Q)).L; 67 | Q = it < its ? lu(X.dot(Q)).L : qr(X.dot(Q)).Q; 68 | } 69 | 70 | const B = crossprod(Q, X); 71 | const res = svd(B, ncomp); 72 | return {s: res.s, V: res.V, U: Q.dot(res.U)}; 73 | } 74 | 75 | /** 76 | * QR decomposition 77 | * 78 | * @param {Matrix} X - matrix to decompose. 79 | * 80 | * @returns {JSON} JSON with two matrices, Q and R, so X = QR. 81 | * 82 | */ 83 | export function qr(X) { 84 | 85 | const m = X.nrows; 86 | const n = X.ncols; 87 | 88 | if (m < n) { 89 | const res = qr(X.subset([], Index.seq(1, m))); 90 | return {Q:res.Q, R:crossprod(res.Q, X)} 91 | } 92 | 93 | let Q = Matrix.eye(m); 94 | let Rt = X.t(); 95 | 96 | for (let j = 1; j <= n; j++) { 97 | for (let i = m; i >= (j + 1); i--) { 98 | 99 | const r = Rt.v.subarray((i - 2) * n, i * n); 100 | const q = Q.v.subarray((i - 2) * m, i * m); 101 | const rc = r.slice(); 102 | const qc = q.slice(); 103 | 104 | const [c, s, temp] = rot(rc[j - 1], rc[j - 1 + n]); 105 | 106 | // apply transformation to columns i and i - 1 of matrix Rt 107 | for (let k = 0; k < n; k++) { 108 | 109 | r[k] = rc[k] * c + rc[k + n] * s; 110 | r[k + n] = rc[k] * -s + rc[k + n] * c; 111 | 112 | q[k] = qc[k] * c + qc[k + m] * s; 113 | q[k + m] = qc[k] * -s + qc[k + m] * c; 114 | } 115 | 116 | for (let k = n; k < m; k++) { 117 | q[k] = qc[k] * c + qc[k + m] * s; 118 | q[k + m] = qc[k] * -s + qc[k + m] * c; 119 | } 120 | } 121 | } 122 | 123 | const ind = Index.seq(1, n); 124 | return { 125 | Q: m === n ? Q : Q.subset([], ind), 126 | R: m === n ? Rt.t() : Rt.subset([], ind).t() 127 | } 128 | } 129 | 130 | /** 131 | * LU decomposition. 132 | * 133 | * @param {Matrix} X - matrix to decompose. 134 | * 135 | * @returns {JSON} JSON with two matrices, L and U, so X = LU. 136 | * 137 | */ 138 | export function lu(X) { 139 | 140 | const nr = X.nrows; 141 | const nc = X.ncols 142 | const n = Math.min(nr, nc) 143 | 144 | const Lv = new Float64Array(nr * nc); 145 | const Uv = new Float64Array(nc * nc); 146 | const Xv = X.v.slice(); 147 | 148 | const s = Math.round(Math.pow(n, 0.5285425)); 149 | let z = 1; 150 | 151 | for (let c = 1; c <= n; c++) { 152 | 153 | if (c == z + s) { 154 | const nro = nr - c + 1; 155 | const nco = nc - c + 1; 156 | const ni = c - z; 157 | const Sv = new Float64Array(nro * nco); 158 | 159 | for (let i = 0; i < nro; i++) { 160 | for (let j = 0; j < nco; j++) { 161 | let s = 0; 162 | for (let k = 0; k < ni; k++) { 163 | s += Lv[(z - 1 + k) * nr + (c - 1) + i] * Uv[(c - 1 + j) * nc + (z - 1 + k)]; 164 | } 165 | Sv[j * nro + i] = s; 166 | } 167 | } 168 | 169 | for (let i = c; i <= nr; i++) { 170 | for (let j = c; j <= nc; j++) { 171 | Xv[(j - 1) * nr + (i - 1)] = Xv[(j - 1) * nr + (i - 1)] - Sv[(j - c) * nro + (i - c)]; 172 | } 173 | } 174 | 175 | z = c; 176 | } 177 | 178 | for (let i = c; i <= nr; i++) { 179 | let acc = Xv[(c - 1) * nr + (i - 1)]; 180 | for (let k = z; k <= c - 1; k++) { 181 | acc = acc - Lv[(k - 1) * nr + (i - 1)] * Uv[(c - 1) * nc + (k - 1)]; 182 | } 183 | Lv[(c - 1) * nr + (i - 1)] = acc; 184 | } 185 | 186 | for (let i = c; i <= nc; i++) { 187 | let acc = Xv[(i - 1) * nr + (c - 1)]; 188 | for (let k = z; k <= c - 1; k++) { 189 | acc = acc - Lv[(k - 1) * nr + (c - 1)] * Uv[(i - 1) * nc + (k - 1)]; 190 | } 191 | const l = Lv[(c - 1) * nr + (c - 1)]; 192 | Uv[(i - 1) * nc + (c - 1)] = (l == 0 ? acc : acc / l); 193 | } 194 | } 195 | 196 | return {L: new Matrix(Lv, nr, nc), U: new Matrix(Uv, nc, nc)} 197 | } 198 | 199 | 200 | /** 201 | * Singular value decomposition 202 | * 203 | * @param {Matrix} X - matrix to decompose. 204 | * @param {number} [ncomp] - number of components. 205 | * 206 | * @returns {JSON} JSON with three fields, 's' - vector with singular values, 207 | * 'U', 'V' - matrices with left and right singular vectors. 208 | * 209 | */ 210 | export function svd(X, ncomp) { 211 | 212 | let d, e, bV, bU, m, n, transposed; 213 | 214 | if (X.nrows < X.ncols) { 215 | [d, e, bV, bU] = bidiag(X.t()); 216 | m = X.ncols; 217 | n = X.nrows; 218 | transposed = true; 219 | } else { 220 | [d, e, bV, bU] = bidiag(X); 221 | m = X.nrows; 222 | n = X.ncols; 223 | transposed = false; 224 | } 225 | 226 | if (!ncomp) { 227 | ncomp = Math.min(m, n); 228 | } 229 | 230 | const maxit = 500 * n * n; 231 | const thresh = Math.pow(10, -64); 232 | let Gt = Matrix.eye(n); 233 | let P = Matrix.eye(n); 234 | 235 | for (let it = 0; it < maxit; it++) { 236 | 237 | // find first nonzero element in e from bottom 238 | let iU = 0; 239 | for (let i = n - 1; i >= 1; i--) { 240 | if (Math.abs(e.v[i - 1]) > thresh) { 241 | iU = i 242 | break; 243 | } 244 | } 245 | 246 | // find first nonzero element in e from top 247 | let iL = iU + 1; 248 | for (let i = 1; i <= n - 1; i++) { 249 | if (Math.abs(e.v[i - 1]) > thresh) { 250 | iL = i; 251 | break; 252 | } 253 | } 254 | 255 | // check the convergence and return result 256 | if ((iU == iL && Math.abs(e[iU - 1]) <= thresh) || (iU < iL)) { 257 | 258 | // compute final results: 259 | // V = bU * P 260 | // U = bV * Gt 261 | // and correct sign of singular values and columns of V 262 | 263 | const s = Vector.zeros(ncomp); 264 | const U = Matrix.zeros(X.nrows, ncomp); 265 | const V = Matrix.zeros(X.ncols, ncomp); 266 | 267 | for (let k = 1; k <= ncomp; k++) { 268 | const sign = Math.sign(d.v[k - 1]); 269 | s.v[k - 1] = Math.abs(d.v[k - 1]); 270 | 271 | let uk, vk; 272 | if (transposed) { 273 | uk = V.getcolref(k); 274 | vk = U.getcolref(k); 275 | } else { 276 | uk = U.getcolref(k); 277 | vk = V.getcolref(k); 278 | } 279 | 280 | const pk = P.getcolref(k); 281 | const gtk = Gt.getcolref(k); 282 | 283 | for (let i = 0; i < n; i++) { 284 | const pki = pk[i]; 285 | const gtki = gtk[i] * sign; 286 | 287 | const bur = bU.getcolref(i + 1); 288 | const vur = bV.getcolref(i + 1); 289 | 290 | for (let r = 0; r < n; r++) { 291 | uk[r] += bur[r] * pki; 292 | vk[r] += vur[r] * gtki; 293 | } 294 | for (let r = n; r < m; r++) { 295 | uk[r] += bur[r] * pki; 296 | } 297 | } 298 | } 299 | 300 | return {s: s, U: U, V: V}; 301 | } 302 | 303 | // re-sweep 304 | const cstart = iL - 1; 305 | const cend = iU + 1; 306 | const clen = iU - iL + 2; 307 | const [rd, re, rG, rPt] = vsweep(d.v.subarray(cstart, cend), e.v.subarray(cstart, cend - 1)); 308 | 309 | // replace elements of d and e 310 | d.v.set(rd, cstart); 311 | e.v.set(re, cstart); 312 | 313 | // now we need to compute: 314 | // G = rG' * G = (G' * rG)' -> this should be done only fo selected columns in G 315 | // P = P * rP' -> only for selected columns in P 316 | 317 | // get local copy of the "clen" columns from G' and P starting from column "cstart" 318 | const lgt = Gt.v.slice(cstart * n, cstart * n + clen * n); 319 | const lp = P.v.slice(cstart * n, cstart * n + clen * n); 320 | 321 | // compute dot(lG', rG) and save back to G' 322 | // compute dot(P, rP') and save back to P 323 | for (let c = 0; c < clen; c++) { 324 | 325 | // get pointers to column "c" (c from "cstart" to "cend") of Gt and P - to save the results 326 | const gtc = Gt.getcolref(c + cstart + 1); // nc x 1 327 | const pc = P.getcolref(c + cstart + 1); // nc x 1 328 | 329 | // get pointers to the columns of rG and rPt 330 | const rgc = rG.getcolref(c + 1); // clen x 1 331 | const rptc = rPt.getcolref(c + 1); // clen x 1 332 | 333 | // copmpute and save the dot products 334 | for (let r = 0; r < n; r++) { 335 | // row "r" of lGt (nc x clen) times column "c" of rG (clen x 1) 336 | // row of lP times column of rPt 337 | let gtcr = 0; 338 | let pcr = 0; 339 | for (let i = 0; i < clen; i++) { 340 | gtcr += lgt[i * n + r] * rgc[i]; 341 | pcr += lp[i * n + r] * rptc[i]; 342 | } 343 | gtc[r] = gtcr; 344 | pc[r] = pcr; 345 | } 346 | } 347 | 348 | 349 | // get local copy of selected columns from G' and P 350 | /* 351 | const lGt = Matrix.zeros(n, l); 352 | const lP = Matrix.zeros(n, l) 353 | for (let c = iL; c <= iU + 1; c++) { 354 | lGt.v.set(Gt.v.subarray((c - 1) * n, c * n), (c - iL) * n); 355 | lP.v.set(P.v.subarray((c - 1) * n, c * n), (c - iL) * n); 356 | } 357 | 358 | // compute dot(lG', rG) and save back to G 359 | // compute dot(P, rP') and save back to P 360 | for (let c = 1; c <= l; c++) { 361 | const newgc = lGt.dot(new Vector(rG.getcolref(c))); 362 | const newpc = lP.dot(new Vector(rPt.getcolref(c))); 363 | Gt.v.set(newgc.v, (c - 1 + iL - 1) * n); 364 | P.v.set(newpc.v, (c - 1 + iL - 1) * n); 365 | } 366 | */ 367 | } 368 | 369 | throw Error("svd: can not converge.") 370 | } 371 | 372 | 373 | /** 374 | * Applies givens rotations to diagonal elements of bidiagonalized matrix. 375 | * 376 | * @param {Float64Array} d - vector with main diagonal elements. 377 | * @param {Float64Array} e - vector with diagonal elements on top of main diagonal. 378 | * 379 | * @returns {Array} array with vectors 'd' and 'e' after rotation as well as right 380 | * transformation matrices G and P'. 381 | * 382 | */ 383 | export function vsweep(d, e) { 384 | 385 | const n = d.length; 386 | 387 | let cold = 1; 388 | let sold = 1; 389 | 390 | let c = 1; 391 | let s = 0; 392 | let r = 0; 393 | 394 | let G = Matrix.eye(n); 395 | let Pt = Matrix.eye(n); 396 | 397 | for (let k = 1; k <= (n - 1); k++) { 398 | 399 | [c, s, r] = rot(c * d[k - 1], e[k - 1]); 400 | 401 | // get two columns from G and rotate their values 402 | const gk = G.v.subarray((k - 1) * n, (k + 1) * n); 403 | const gkc = gk.slice(); 404 | 405 | for (let i = 0; i < n; i++) { 406 | gk[i] = gkc[i] * c + gkc[i + n] * s; 407 | gk[i + n] = -gkc[i] * s + gkc[i + n] * c; 408 | } 409 | 410 | if (k != 1) { 411 | e[k - 2] = r * sold; 412 | } 413 | 414 | [cold, sold, r] = rot(cold * r, d[k] * s); 415 | d[k - 1] = r; 416 | 417 | // get two columns from Pt and rotate their values 418 | const ptk = Pt.v.subarray((k - 1) * n, (k + 1) * n); 419 | const ptkc = ptk.slice(); 420 | 421 | for (let i = 0; i < n; i++) { 422 | ptk[i] = ptkc[i] * cold + ptkc[i + n] * sold; 423 | ptk[i + n] = -ptkc[i] * sold + ptkc[i + n] * cold; 424 | } 425 | 426 | } 427 | 428 | const h = c * d[n - 1]; 429 | e[n - 2] = h * sold; 430 | d[n - 1] = h * cold; 431 | 432 | return [d, e, G, Pt]; 433 | } 434 | 435 | 436 | /** 437 | * Generate c, s, r values for Givens rotations, so [c s; -s c][f; g] = [r; 0]. 438 | * 439 | * @param {number} f - first element of vector. 440 | * @param {number} g - second element of vector. 441 | * 442 | * @return array with values [c, s, r]. 443 | * 444 | */ 445 | export function rot(f, g) { 446 | 447 | if (f == 0) { 448 | return [0, 1, g]; 449 | } 450 | 451 | if (Math.abs(f) > Math.abs(g)) { 452 | const t = g / f; 453 | const t1 = Math.sqrt(1 + t * t); 454 | const c = 1 / t1 455 | return [c, t * c, f * t1]; 456 | } 457 | 458 | const t = f / g; 459 | const t1 = Math.sqrt(1 + t * t) 460 | const s = 1 / t1 461 | return [t * s, s, g * t1]; 462 | } 463 | 464 | 465 | /** 466 | * Golub-Reinsch bidiagonalization of matrix A 467 | * 468 | * @param {Matrix} A - matrix with values. 469 | * 470 | * @returns {Array} array with three matrices [B, V, U]. 471 | * 472 | */ 473 | export function bidiag(A) { 474 | 475 | const m = A.nrows; 476 | const n = A.ncols; 477 | 478 | if (m < n) { 479 | throw Error("bidiag: number of rows in A must not be smaller number of columns."); 480 | } 481 | 482 | let Ut = Matrix.eye(m); 483 | let V = Matrix.eye(n); 484 | let B = A.copy(); 485 | 486 | for (let k = 1; k <= (m > n ? n : n - 1); k++) { 487 | // for (let k = 1; k <= 1; k++) { 488 | const mk = m - k + 1; 489 | 490 | // compute: 491 | // B = H * B 492 | // U' = H' * U'; 493 | const H1 = householder(B.getcolref(k), k); 494 | 495 | for (let c = 1; c <= n; c++) { 496 | 497 | // get c-th column of B (starting from (k-1) row) and make a copy 498 | const bc = B.v.subarray((c - 1) * m + k - 1, c * m) 499 | const bcc = bc.slice(); 500 | 501 | // get c-th column of U' (starting from (k-1) row) and make a copy 502 | const utc = Ut.v.subarray((c - 1) * m + k - 1, c * m) 503 | const utcc = utc.slice(); 504 | 505 | for (let r = k; r <= m; r++) { 506 | // because H is symmetric we take column instead of row 507 | const hr = H1.getcolref(r - k + 1); 508 | let sb = 0; 509 | let sut = 0; 510 | for (let i = 0; i < mk; i++) { 511 | sb += hr[i] * bcc[i]; 512 | sut += hr[i] * utcc[i] 513 | } 514 | bc[r - k] = sb; 515 | utc[r - k] = sut; 516 | } 517 | } 518 | 519 | 520 | for (let c = n + 1; c <= m; c++) { 521 | 522 | // get c-th column of U' and make a copy 523 | const utc = Ut.v.subarray((c - 1) * m + k - 1, c * m) 524 | const utcc = utc.slice(); 525 | 526 | for (let r = k; r <= m; r++) { 527 | const hr = H1.getcolref(r - k + 1); 528 | let sut = 0; 529 | for (let i = 0; i < mk; i++) { 530 | sut += hr[i] * utcc[i]; 531 | } 532 | utc[r - k] = sut; 533 | } 534 | } 535 | 536 | if (k < n - 1) { 537 | 538 | // compute: 539 | // B = B * H' 540 | // V = V * H 541 | const H2 = householder(B.getrow(k).v, k + 1); 542 | 543 | // we need to process columns from k + 1, so make a subset from the start 544 | const Bvk = B.v.subarray(k * m); 545 | const Vvk = V.v.subarray(k * n); 546 | 547 | let br = new Float64Array(n - k) 548 | let vr = new Float64Array(n - k) 549 | 550 | // process first n rows for both B and V 551 | for (let r = 1; r <= n; r++) { 552 | 553 | // get elements from r-th row 554 | for (let i = 0; i < n - k; i++) { 555 | br[i] = Bvk[i * m + r - 1]; 556 | vr[i] = Vvk[i * n + r - 1]; 557 | } 558 | 559 | for (let c = k + 1; c <= n; c++) { 560 | const hc = H2.getcolref(c - k); 561 | let sb = 0; 562 | let sv = 0; 563 | for (let i = 0; i < H2.nrows; i++) { 564 | sb += br[i] * hc[i]; 565 | sv += vr[i] * hc[i]; 566 | } 567 | Bvk[(c - k - 1) * m + (r - 1)] = sb; 568 | Vvk[(c - k - 1) * n + (r - 1)] = sv 569 | } 570 | } 571 | 572 | // process rows from (n+1) to m for B only 573 | for (let r = n + 1; r <= m; r++) { 574 | 575 | // get elements from r-th row 576 | for (let i = 0; i < n - k; i++) { 577 | br[i] = Bvk[i * m + r - 1]; 578 | } 579 | 580 | for (let c = k + 1; c <= n; c++) { 581 | const hc = H2.getcolref(c - k); 582 | let sb = 0; 583 | for (let i = 0; i < H2.nrows; i++) { 584 | sb += br[i] * hc[i]; 585 | } 586 | Bvk[(c - k - 1) * m + (r - 1)] = sb; 587 | } 588 | } 589 | } 590 | } 591 | 592 | const d = Vector.zeros(n); 593 | const e = Vector.zeros(n - 1); 594 | for (let i = 0; i < n - 1; i++) { 595 | d.v[i] = B.v[i * m + i]; 596 | e.v[i] = B.v[(i + 1) * m + i]; 597 | } 598 | d.v[n - 1] = B.v[(n - 1) * m + (n - 1)]; 599 | 600 | return [d, e, V, Ut.t()]; 601 | } 602 | 603 | 604 | /** 605 | * Compute elements of Householder transformation as a matrix. 606 | * 607 | * @param {Float64Array} b - array with diagonal values. 608 | * @param {number} k - position to start with. 609 | * 610 | * @returns {Matrix} matrix with transformation values. 611 | * 612 | */ 613 | export function householder(b, k) { 614 | 615 | // get vector with values 616 | const h = householderv(b, k); 617 | const n = h.length; 618 | // compute matrix as outer product of the vector 619 | const H = Matrix.zeros(n); 620 | for (let c = 0; c < n; c++) { 621 | const hc = H.getcolref(c + 1); 622 | for (let r = 0; r < n; r++) { 623 | hc[r] = 1 * (r == c) - 2 * h[r] * h[c]; 624 | } 625 | } 626 | 627 | return H; 628 | } 629 | 630 | 631 | /** 632 | * Compute elements of Householder transformation as a vector. 633 | * 634 | * @param {Float64Array} b - array with diagonal values. 635 | * @param {number} k - position to start with. 636 | * 637 | * @returns {Float64Array} vector with transformation values. 638 | * 639 | */ 640 | export function householderv(b, k) { 641 | 642 | const n = b.length; 643 | 644 | if (k >= n) { 645 | throw Error("householder: parameter 'k' must be smaller than length of vector 'b'.") 646 | } 647 | 648 | const hlen = n - k + 1; 649 | const h = b.slice(k - 1); 650 | // to avoid computing norm2 twice we will 651 | // compute it based on the first norm 652 | const hssq = ssq(h); 653 | const hn2 = Math.sqrt(hssq); 654 | const s = Math.sign(h[0]); 655 | const a = h[0]; 656 | 657 | // change first value in the vector 658 | h[0] = a - s * hn2; 659 | 660 | // recompute the norm 661 | const hn2a = Math.sqrt(2 * hssq - 2 * s * a * hn2); 662 | 663 | // if norm is zero return vector as is 664 | if (hn2a < Number.EPSILON) return h 665 | 666 | // normalize vector and return 667 | for (let i = 0; i < hlen; i++) { 668 | h[i] /= hn2a; 669 | } 670 | return h; 671 | } 672 | 673 | 674 | 675 | 676 | 677 | -------------------------------------------------------------------------------- /src/misc/index.js: -------------------------------------------------------------------------------- 1 | /*************************************************/ 2 | /* Misc extra methods for statistics */ 3 | /*************************************************/ 4 | 5 | import { isvector, vector, Index, Vector } from "../arrays/index.js"; 6 | import { prod, quantile } from "../stat/index.js"; 7 | 8 | 9 | /** 10 | * Finds index of value in x which is closest to the value a. 11 | * 12 | * @param {Array|Vector} x - a vector with values. 13 | * @param {number} a - a value 14 | * 15 | * @returns {number} the index value (starts from 1). 16 | * 17 | */ 18 | export function closestind(x, a) { 19 | 20 | if (isvector(x)) { 21 | return closestind(x.v, a); 22 | } 23 | 24 | const c = x.reduce((prev, curr) => Math.abs(curr - a) < Math.abs(prev - a) ? curr : prev); 25 | return x.indexOf(c) + 1; 26 | } 27 | 28 | /** 29 | * Finds index of value in x which is closest to the value a from the left. 30 | * 31 | * @param {Array|Vector} x - a vector with values. 32 | * @param {number} a - a value 33 | * 34 | * @returns {number} the index value (starts from 1). 35 | * 36 | */ 37 | export function closestindleft(x, a) { 38 | 39 | if (isvector(x)) { 40 | return closestindleft(x.v, a); 41 | } 42 | 43 | if (x[0] > a) return 1; 44 | for (let i = 1; i < x.length; i++) { 45 | if (x[i] > a) return i; 46 | } 47 | return x.length; 48 | } 49 | 50 | /** 51 | * Finds index of value in x which is closest to the value a from the right. 52 | * 53 | * @param {Array|Vector} x - a vector with values. 54 | * @param {number} a - a value 55 | * 56 | * @returns {number} the index value (starts from 1). 57 | * 58 | */ 59 | export function closestindright(x, a) { 60 | 61 | if (isvector(x)) { 62 | return closestindright(x.v, a); 63 | } 64 | 65 | if (x[x.length - 1] < a) return x.length; 66 | for (let i = (x.length - 1); i >= 0; i--) { 67 | if (x[i] < a) return i + 2; 68 | } 69 | return 1; 70 | } 71 | 72 | 73 | /** 74 | * Computes numeric integral for function "f" with limits (a, b). 75 | * 76 | * @param {function} f - a reference to a function. 77 | * @param {number} a - lower limit for integration. 78 | * @param {number} b - upper limit for integration. 79 | * @param {number} [acc=0.000001] - absolute accuracy. 80 | * @param {number} [eps=0.00001] - relative accuracy. 81 | * @param {number[]} oldfs - vector of values needed for recursion. 82 | * 83 | * @returns {number} result of integration. 84 | * 85 | */ 86 | export function integrate(f, a, b, acc, eps, oldfs) { 87 | 88 | if (acc === undefined) { 89 | acc = 0.000001; 90 | } 91 | 92 | if (eps === undefined) { 93 | eps = 0.00001 94 | } 95 | 96 | if (typeof(a) !== "number" || typeof(b) !== "number") { 97 | throw Error("Parameters 'a' and 'b' must be numbers."); 98 | } 99 | 100 | if (b < a) { 101 | throw Error("Parameter 'b' must be larger 'a'."); 102 | } 103 | 104 | // special case when left limit is minus infinity 105 | if (a === -Infinity && b !== Infinity) { 106 | return integrate((t) => f(b - (1 - t) / t) / (t ** 2), 0, 1); 107 | } 108 | 109 | // special case when right limit is plus infinity 110 | if (a !== -Infinity && b === Infinity) { 111 | return integrate((t) => f(a + (1 - t) / t) / (t ** 2), 0, 1); 112 | } 113 | 114 | // special case when both limits are infinite 115 | if (a === -Infinity && b === Infinity) { 116 | return integrate((t) => (f((1 - t) / t) + f((t - 1) / t)) / t ** 2, 0, 1); 117 | } 118 | 119 | // constants for splitting the integration interval 120 | const x = [1/6, 2/6, 4/6, 5/6]; 121 | const w = [2/6, 1/6, 1/6, 2/6]; 122 | const v = [1/4, 1/4, 1/4, 1/4]; 123 | const p = [1, 0, 0, 1]; 124 | 125 | let n = x.length, h = b - a; 126 | let fs; 127 | 128 | if (oldfs === undefined) { 129 | fs = x.map(v => f(a + v * h)); 130 | } else { 131 | fs = new Array(n); 132 | for (let k = 0, i = 0; i < n; i++) { 133 | fs[i] = p[i] === 1 ? f(a + x[i] * h) : oldfs[k++]; 134 | } 135 | } 136 | 137 | let q4 = 0, q2 = 0; 138 | for (let i = 0; i < n; i++) { 139 | q4 += w[i] * fs[i] * h; 140 | q2 += v[i] * fs[i] * h; 141 | } 142 | 143 | if (isNaN(q2) || isNaN(q4)) { 144 | throw Error("Numerical integration ended up with NaN number.") 145 | } 146 | 147 | let tol = acc + eps * Math.abs(q4); 148 | let err = Math.abs((q4 - q2)/3); 149 | 150 | if (err < tol) return q4; 151 | 152 | acc = acc / Math.sqrt(2.); 153 | const mid = (a + b) / 2; 154 | const left = fs.filter((v, i) => i < n/2); 155 | const right = fs.filter((v, i) => i >= n/2); 156 | 157 | const ql = integrate(f, a, mid, eps, acc, left); 158 | const qr = integrate(f, mid, b, eps, acc, right); 159 | 160 | return ql + qr; 161 | } 162 | 163 | 164 | /** 165 | * Finds outliers in a vector based on inter-quartile range distance. 166 | * 167 | * @param {Array|Vector} x - vector with values. 168 | * @param {number} q1 - first quartile (optional parameter). 169 | * @param {number} q3 - third quartile (optional parameter). 170 | * 171 | * @returns {Vector} vector with outliers or empty vector if none were found. 172 | * 173 | */ 174 | export function getoutliers(x, q1 = undefined, q3 = undefined) { 175 | 176 | if (q1 === undefined) q1 = quantile(x, 0.25); 177 | if (q3 === undefined) q3 = quantile(x, 0.75); 178 | 179 | const iqr15 = (q3 - q1) * 1.5; 180 | const bl = q1 - iqr15 181 | const bu = q3 + iqr15 182 | 183 | return new Vector(x.v.filter(v => v < bl || v > bu)); 184 | } 185 | 186 | 187 | /** 188 | * Rounds number (or vector of numbers) to given amount of decimals. 189 | * 190 | * @param {numbr|Array|Vector} x - a number or a vector with values. 191 | * 192 | * @return {number|Array|Vector} a number or a vector with rounded values. 193 | * 194 | */ 195 | export function round(x, n = 0) { 196 | 197 | if (isvector(x)) { 198 | return vector(x.v.map(v => round(v, n))); 199 | } 200 | 201 | if (Array.isArray(x)) { 202 | return x.map(v => round(v, n)); 203 | } 204 | 205 | return Number.parseFloat(x.toFixed(n)); 206 | } 207 | 208 | 209 | /** 210 | * Generate combination of all levels of vectors. 211 | * 212 | * @param {...} args - a sequence of vectors. 213 | * 214 | * @returns {Array} array of arrays with values for each vector. 215 | * 216 | */ 217 | export function expandgrid(...args) { 218 | 219 | const nargs = args.length; 220 | const d = args.map(v => v.length); 221 | let orep = prod(d); 222 | 223 | let grid = Array(nargs); 224 | let repFac = 1; 225 | 226 | for (let i = 0; i < nargs; i++) { 227 | const x = isvector(args[i]) ? args[i] : vector(args[i]); 228 | const nx = x.length; 229 | orep = orep / nx; 230 | grid[i] = x.subset(Index.seq(1, nx).repeach(repFac).rep(orep)); 231 | repFac = repFac * nx; 232 | } 233 | 234 | return grid; 235 | } 236 | -------------------------------------------------------------------------------- /src/models/index.js: -------------------------------------------------------------------------------- 1 | import { rsvd } from '../decomp/index.js'; 2 | import { pf, pt, qt, qchisq } from '../distributions/index.js'; 3 | import { norm2, variance, median, iqr, mean, sd, ssq } from '../stat/index.js'; 4 | import { scale as prep_scale, unscale as prep_unscale } from '../prep/index.js'; 5 | import { _dot, isfactor, factor, cbind, tcrossprod, crossprod, reshape, ismatrix, Index, 6 | Matrix, vector, isvector, Vector } from '../arrays/index.js'; 7 | 8 | /** 9 | * Check if an object has a proper class. 10 | * 11 | * @param {JSON} obj - JSON wiht object (e.g. model or results). 12 | * @param {string} className - expected name of the class. 13 | * 14 | * @return {boolean} true or false. 15 | * 16 | */ 17 | export function isa(obj, className) { 18 | return obj && obj.class && Array.isArray(obj.class) && obj.class.includes(className); 19 | } 20 | 21 | 22 | 23 | export function getclassres(cPred, className, cRef) { 24 | 25 | // check inputs 26 | if (!cPred || !Array.isArray(cPred) || cPred.length < 1 || !cPred.every(isfactor)) { 27 | throw new Error('getclassres: parameter "cPred" must be array of factors.'); 28 | } 29 | 30 | if (!className || typeof(className) !== 'string' || className == '' || className == 'none') { 31 | throw new Error('getclassres: wrong value for "className" parameter (must me a string).'); 32 | } 33 | 34 | if (cRef && !isfactor(cRef)) { 35 | throw new Error('getclassres: parameter "cRef" must be a factor (or undefined).'); 36 | } 37 | 38 | // if reference values are not provided return results without statistics 39 | if (!cRef) { 40 | return { 41 | class: ['classres'], 42 | className: className, 43 | cPred: cPred, 44 | cRef: cRef 45 | } 46 | } 47 | 48 | // find index of class in factor with reference class names 49 | const refInd = cRef.labels.findIndex(v => v === className); 50 | const ncomp = cPred.length; 51 | 52 | // prepare variables for statistics 53 | const TP = Vector.zeros(ncomp); 54 | const FP = Vector.zeros(ncomp); 55 | const TN = Vector.zeros(ncomp); 56 | const FN = Vector.zeros(ncomp); 57 | const sensitivity = Vector.zeros(ncomp); 58 | const specificity = Vector.zeros(ncomp); 59 | const accuracy = Vector.zeros(ncomp); 60 | 61 | // loop over components 62 | for (let a = 1; a <= ncomp; a++) { 63 | 64 | const predInd = cPred[a - 1].labels.findIndex(v => v === className); 65 | let TPa = 0, TNa = 0, FPa = 0, FNa = 0; 66 | for (let i = 0; i < cRef.length; i++) { 67 | if (cPred[a - 1].v[i] === predInd && cRef.v[i] === refInd) TPa += 1; 68 | if (cPred[a - 1].v[i] !== predInd && cRef.v[i] !== refInd) TNa += 1; 69 | if (cPred[a - 1].v[i] === predInd && cRef.v[i] !== refInd) FPa += 1; 70 | if (cPred[a - 1].v[i] !== predInd && cRef.v[i] === refInd) FNa += 1; 71 | } 72 | 73 | sensitivity.v[a - 1] = TPa / (TPa + FNa); 74 | specificity.v[a - 1] = TNa / (TNa + FPa); 75 | accuracy.v[a - 1] = (TPa + TNa) / (TNa + TPa + FNa + FPa); 76 | TP.v[a - 1] = TPa; 77 | TN.v[a - 1] = TNa; 78 | FP.v[a - 1] = FPa; 79 | FN.v[a - 1] = FNa; 80 | 81 | } 82 | 83 | return { 84 | class: ['classres'], 85 | className: className, 86 | cPred: cPred, 87 | cRef: cRef, 88 | TP: TP, 89 | FN: FN, 90 | TN: TN, 91 | FP: FP, 92 | sensitivity: sensitivity, 93 | specificity: specificity, 94 | accuracy: accuracy 95 | } 96 | } 97 | 98 | 99 | export function getsimcaparams(className, alpha, limType) { 100 | 101 | const validLimTypes = ['classic', 'robust']; 102 | 103 | if (!alpha) alpha = 0.05; 104 | if (!limType) limType = 'classic'; 105 | 106 | if (!className || typeof(className) !== 'string' || className == '' || className == 'none') { 107 | throw new Error('simcamodel: wrong value for "className" parameter (must me a string).'); 108 | } 109 | 110 | if (isNaN(alpha) || alpha < 0 || alpha > 1) { 111 | throw new Error('simcamodel: wrong value for "alpha" parameter.'); 112 | } 113 | 114 | if (typeof(limType) !== 'string' || !validLimTypes.includes(limType)) { 115 | throw new Error('simcamodel: wrong value for "limType" parameter (must be either "classic" or "robust").'); 116 | } 117 | 118 | return { 119 | class: ['simcaparams'], 120 | className: className, 121 | alpha: alpha, 122 | limType: limType 123 | } 124 | } 125 | 126 | export function simcapredict(m, params, X, cRef) { 127 | 128 | // check inputs 129 | if (!isa(m, 'pcamodel')) { 130 | throw new Error('simcapredict: parameter "m" must be an object with PCA model.'); 131 | } 132 | 133 | if (!isa(params, 'simcaparams')) { 134 | throw new Error('simcapredict: parameter "params" must be an object from "simcaparams" method.'); 135 | } 136 | 137 | if (!ismatrix(X)) { 138 | throw new Error('simcapredict: parameter "X" must be instance of Matrix class.'); 139 | } 140 | 141 | if (cRef && !factor(cRef)) { 142 | throw new Error('simcapredict: parameter "cRef" must be instance of Factor class.'); 143 | } 144 | 145 | if (cRef && cRef.length !== X.nrows) { 146 | throw new Error('simcapredict: parameter "cRef" must have the same number of values as rows in "X".'); 147 | } 148 | 149 | 150 | // project data to PCA model 151 | const pcares = pcapredict(m, X); 152 | 153 | // prepare array for predictions 154 | const cPred = new Array(m.ncomp); 155 | 156 | // get parameters for the distances 157 | const Nq = m.qParams[params.limType][1].v; 158 | const q0 = m.qParams[params.limType][0].v; 159 | const h0 = m.hParams[params.limType][0].v; 160 | const Nh = m.hParams[params.limType][1].v; 161 | 162 | // loop over components 163 | for (let a = 1; a <= m.ncomp; a++) { 164 | 165 | const h0a = h0[a - 1]; 166 | const q0a = q0[a - 1]; 167 | const Nha = Nh[a - 1]; 168 | const Nqa = Nq[a - 1]; 169 | 170 | const ha = pcares.H.getcolref(a); 171 | const qa = pcares.Q.getcolref(a); 172 | const cpa = new Array(X.nrows); 173 | 174 | // compute critical value for FD 175 | const fCrit = qchisq(1 - params.alpha, Nqa + Nha); 176 | 177 | // loop over rows to make predictions 178 | for (let i = 0; i < X.nrows; i++) { 179 | const fa = ha[i] / h0a * Nha + qa[i] / q0a * Nqa; 180 | cpa[i] = fa <= fCrit ? params.className : "none"; 181 | } 182 | 183 | cPred[a - 1] = factor(cpa); 184 | } 185 | 186 | return { 187 | class: ['simcares'], 188 | pcares: pcares, 189 | classres: getclassres(cPred, params.className, cRef) 190 | } 191 | } 192 | 193 | /** 194 | * Fit Partial Least Squares Regression model. 195 | * 196 | * @param {Matrix} X - matrix with predictors. 197 | * @param {Matrix} Y - matirx with responses (must have one column). 198 | * @param {number} ncomp - number of components. 199 | * @param {boolean} [center=true] - logical, mean center X and Y or not. 200 | * @param {boolean} [scale=false] - logical, standardize X and Y or not. 201 | * 202 | * @returns {JSON} object with model parameters. 203 | * 204 | */ 205 | export function plsfit(X, Y, ncomp, center, scale) { 206 | 207 | if (!ismatrix(X) || X.ncols < 2 || X.nrows < 2) { 208 | throw Error('plsfit: parameter "X" must be a matrix with at least two rows and two columns.'); 209 | } 210 | 211 | if (!ismatrix(Y) || Y.ncols !== 1) { 212 | throw Error('plsfit: parameter "Y" must be a matrix with one column.'); 213 | } 214 | 215 | if (Y.nrows !== X.nrows) { 216 | throw Error('plsfit: number of rows in "X" and "Y" do not match.'); 217 | } 218 | 219 | if (center === undefined) { 220 | center = true; 221 | } 222 | 223 | if (scale === undefined) { 224 | scale = false; 225 | } 226 | 227 | if (!ncomp) { 228 | ncomp = Math.min(ncols, nrows - 1); 229 | } 230 | 231 | // center and scale the training set 232 | const [Xp, mX, sX] = prep_scale(X, center, scale, true); 233 | const [Yp, mY, sY] = prep_scale(Y, center, scale, true); 234 | 235 | // compute loadings and eigenvalues for X using randomized SVD 236 | const m = simpls(Xp, Yp, ncomp); 237 | 238 | // compute distances and variance 239 | const pcares = pcagetmainres(Xp, m.T, m.P, m.xeigenvals); 240 | 241 | // compute parameters for distances 242 | const hParams = getDistParams(pcares.H); 243 | const qParams = getDistParams(pcares.Q); 244 | 245 | // compute Y-loadings 246 | 247 | // return the model object 248 | return { 249 | class: ['plsmodel', 'regmodel', 'pcamodel'], 250 | ncomp: ncomp, 251 | center: center, 252 | scale: scale, 253 | 254 | // X part 255 | mX: mX, 256 | sX: sX, 257 | P: m.P, 258 | R: m.R, 259 | xeigenvals: m.xeigenvals, 260 | 261 | // Y part 262 | mY: mY, 263 | sY: sY, 264 | C: m.C, 265 | yeigenvals: m.yeigenvals, 266 | 267 | // distances 268 | qParams: qParams, 269 | hParams: hParams, 270 | }; 271 | } 272 | 273 | 274 | /** 275 | * Make predictions for PCR model and new dataset. 276 | * 277 | * @param {JSON} m - PCR model created by 'pcrfit()'. 278 | * @param {Matrix} X - matrix with predictors. 279 | * @param {Matrix} Y - matirx with responses (must have one column, use empty array [], if no response available). 280 | * @param {string} name - text label for the results with objects. 281 | * 282 | * @returns {JSON} object with main results. 283 | * 284 | */ 285 | export function plspredict(m, X, Y, name) { 286 | 287 | const Xp = prep_scale(X, m.mX, m.sX); 288 | 289 | // compute scores 290 | const T = Xp.dot(m.R); 291 | 292 | // compute distances and variance 293 | const pcares = pcagetmainres(Xp, T, m.P, m.xeigenvals); 294 | 295 | // compute Y-scores if reference values provided 296 | let U = null; 297 | if (Y !== undefined && Y !== null) { 298 | 299 | if (!ismatrix(Y)) { 300 | throw Error('plcapredict: parameter "Y" must be a matrix with one column or null/undefined.'); 301 | } 302 | 303 | const Yp = prep_scale(Y, m.mY, m.sY); 304 | U = Yp.dot(m.C); 305 | 306 | // oprthogonalize y-scores 307 | for (let a = 2; a <= m.ncomp; a++) { 308 | const ua = U.subset([], a); 309 | const Ta = T.subset([], Index.seq(1, a - 1)); 310 | const uao = ua.subtract(Ta.dot(crossprod(Ta, ua))); 311 | U.v.set(uao.v, (a - 1) * U.nrows); 312 | } 313 | } 314 | 315 | const regres = reggetmainres(T, m.C, m.mY, m.sY, Y); 316 | 317 | return { 318 | class: ['pcares', 'regres', 'plsres'], 319 | name: name, 320 | T: T, 321 | U: U, 322 | ...pcares, 323 | ...regres 324 | }; 325 | } 326 | 327 | 328 | /** 329 | * Fit Principal Component Regression model. 330 | * 331 | * @param {Matrix} X - matrix with predictors. 332 | * @param {Matrix} Y - matirx with responses (must have one column). 333 | * @param {number} ncomp - number of components. 334 | * @param {boolean} [center=true] - logical, mean center X and Y or not. 335 | * @param {boolean} [scale=false] - logical, standardize X and Y or not. 336 | * 337 | * @returns {JSON} object with model parameters. 338 | */ 339 | export function pcrfit(X, Y, ncomp, center, scale) { 340 | 341 | if (!ismatrix(X) || X.ncols < 2 || X.nrows < 2) { 342 | throw Error('pcrfit: parameter X must be a matrix with at least two rows and two columns.'); 343 | } 344 | 345 | if (!ismatrix(Y) || Y.ncols !== 1) { 346 | throw Error('pcrfit: parameter Y must be a matrix with one column.'); 347 | } 348 | 349 | if (Y.nrows !== X.nrows) { 350 | throw Error('pcrfit: number of rows in X and Y do not match.'); 351 | } 352 | 353 | 354 | if (center === undefined) { 355 | center = true; 356 | } 357 | 358 | if (scale === undefined) { 359 | scale = false; 360 | } 361 | 362 | if (!ncomp) { 363 | ncomp = Math.min(ncols, nrows - 1); 364 | } 365 | 366 | // center and scale the training set 367 | const [Xp, mX, sX] = prep_scale(X, center, scale, true); 368 | const [Yp, mY, sY] = prep_scale(Y, center, scale, true); 369 | 370 | // compute loadings and eigenvalues for X using randomized SVD 371 | const m = rsvd(Xp, ncomp); 372 | const eigenvals = m.s.apply(x => x * x / (X.nrows - 1)); 373 | 374 | // compute main PCA results for calibration set 375 | const calres = pcagetmainres(Xp, m.U.dot(Matrix.diagm(m.s)), m.V, eigenvals); 376 | 377 | // compute parameters for distances 378 | const hParams = getDistParams(calres.H); 379 | const qParams = getDistParams(calres.Q); 380 | 381 | // compute Y-loadings 382 | const C = Matrix.zeros(1, ncomp); 383 | for (let a = 1; a <= ncomp; a++) { 384 | const sigmaa = m.s.v[a - 1]; 385 | const ta = calres.T.getcolref(a); 386 | C.v[a - 1] = _dot(Yp.v, ta, 1, X.nrows, X.nrows, 1) / (sigmaa * sigmaa); 387 | } 388 | 389 | // return the model object 390 | return { 391 | class: ['pcrmodel', 'regmodel', 'pcamodel'], 392 | 393 | // PCA part 394 | P: m.V, 395 | eigenvals: eigenvals, 396 | center: center, 397 | scale: scale, 398 | mX: mX, 399 | sX: sX, 400 | qParams: qParams, 401 | hParams: hParams, 402 | 403 | // PCR part 404 | ncomp: ncomp, 405 | C: C, 406 | mY: mY, 407 | sY: sY, 408 | } 409 | } 410 | 411 | 412 | /** 413 | * Make predictions for PCR model and new dataset. 414 | * 415 | * @param {JSON} m - PCR model created by 'pcrfit()'. 416 | * @param {Matrix} X - matrix with predictors. 417 | * @param {Matrix} Y - matirx with responses (must have one column, use empty array [], if no response available). 418 | * @param {string} name - text label for the results with objects. 419 | * 420 | * @returns {JSON} object with main results. 421 | */ 422 | export function pcrpredict(m, X, Y, name) { 423 | 424 | 425 | const Xp = prep_scale(X, m.mX, m.sX); 426 | 427 | // compute PCA results 428 | const T = Xp.dot(m.P); 429 | const pcares = pcagetmainres(Xp, T, m.P, m.eigenvals); 430 | 431 | // compute regression results 432 | const regres = reggetmainres(T, m.C, m.mY, m.sY, Y); 433 | 434 | return { 435 | class: ['pcares', 'regres', 'pcrres'], 436 | name: name, 437 | ...pcares, 438 | ...regres 439 | }; 440 | } 441 | 442 | 443 | /** 444 | * Fit a Principal Component Analysis model. 445 | * 446 | * @param {Matrix} X - matrix or dataset with data values. 447 | * @param {number} ncomp - number of components to compute. 448 | * @param {boolean|Vector} [center=true] - logical (mean center or not) or vector with values for centering. 449 | * @param {boolean|Vector} [scale=false] - logical (standardize or not) or vector with values for scaling. 450 | * 451 | * @returns {Object} JSON with model parameters. 452 | * 453 | */ 454 | export function pcafit(X, ncomp, center, scale) { 455 | 456 | if (center === undefined) { 457 | center = true; 458 | } 459 | 460 | if (scale === undefined) { 461 | scale = false; 462 | } 463 | 464 | if (!ismatrix(X) || X.ncols < 2 || X.nrows < 2) { 465 | throw Error('plsfit: parameter "X" must be a matrix with at least two rows and two columns.'); 466 | } 467 | 468 | if (!ncomp) { 469 | ncomp = Math.min(X.ncols, X.nrows - 1); 470 | } 471 | 472 | if (ncomp < 0 || ncomp > X.ncols || ncomp > X.nrows - 1) { 473 | throw new Error('pcafit: wrong value for "ncomp" parameter.'); 474 | } 475 | 476 | // center and scale the training set 477 | const [Xp, mX, sX] = prep_scale(X, center, scale, true); 478 | 479 | // compute loadings and eigenvalues 480 | const m = rsvd(Xp, ncomp); 481 | const eigenvals = m.s.apply(v => v * v / (X.nrows - 1)); 482 | 483 | // compute main PCA results for calibration set 484 | const calres = pcagetmainres(Xp, m.U.dot(Matrix.diagm(m.s)), m.V, eigenvals); 485 | 486 | // compute mean values for distances 487 | const hParams = getDistParams(calres.H); 488 | const qParams = getDistParams(calres.Q); 489 | 490 | // return the model object 491 | return { 492 | class: ['pcamodel'], 493 | P: m.V, 494 | eigenvals: eigenvals, 495 | center: center, 496 | scale: scale, 497 | mX: mX, 498 | sX: sX, 499 | qParams: qParams, 500 | hParams: hParams, 501 | ncomp: ncomp, 502 | nCalObj: Xp.nrows, 503 | results: {'cal': calres} 504 | } 505 | } 506 | 507 | 508 | /** 509 | * Compute parameters of distance distributions from distance matrix U. 510 | * 511 | * @param {Matrix} U - matrix with distances. 512 | * 513 | * @returns {Array} array with two vectors, u0 (scalars) and Nu (number of degrees of freedom). 514 | * 515 | */ 516 | export function getDistParams(U) { 517 | 518 | const ncomp = U.ncols; 519 | const u0m = Vector.zeros(ncomp) 520 | const u0r = Vector.zeros(ncomp) 521 | const Num = Vector.zeros(ncomp) 522 | const Nur = Vector.zeros(ncomp) 523 | 524 | const Nu = Vector.zeros(ncomp); 525 | for (let i = 0; i < ncomp; i++) { 526 | const u = U.getcolref(i + 1); 527 | 528 | // moments 529 | u0m.v[i] = mean(u); 530 | const fm = u0m.v[i] / sd(u); 531 | const Nm = Math.round(2 * fm * fm); 532 | Num.v[i] = Nm < 1 ? 1 : Nm > 250 ? 250 : Nm; 533 | 534 | // robust 535 | const M = median(u); 536 | const S = iqr(u); 537 | const RM = S / M; 538 | const Nr = RM > 2.685592117 ? 1 : 539 | (RM < 0.194565995 ? 100 : 540 | Math.round(Math.exp(Math.pow(1.380948 * Math.log(2.68631 / RM), 1.185785)))); 541 | u0r.v[i] = 0.5 * Nr * (M / qchisq(0.5, Nr) + S / (qchisq(0.75, Nr) - qchisq(0.25, Nr))) 542 | Nur.v[i] = Nr; 543 | } 544 | 545 | return {'classic': [u0m, Num], 'robust': [u0r, Nur]}; 546 | } 547 | 548 | 549 | /** 550 | * Compute main PCA results for already preprocessed dataset. 551 | * 552 | * @param {Matrix} Xp - matrix with preprocessed (e.g. centered) data values. 553 | * @param {Matrix} T - matrix with scores. 554 | * @param {Matrix} P - matrix with loadings. 555 | * @param {Vector} eigenvals - vector with eigenvalues. 556 | * 557 | * @returns {Object} JSON with main outcomes (scores, distances, variances). 558 | * 559 | */ 560 | export function pcagetmainres(Xp, T, P, eigenvals) { 561 | 562 | const ncomp = P.ncols; 563 | const nrows = Xp.nrows; 564 | const nvars = Xp.ncols; 565 | 566 | // prepare objects for results 567 | const H = Matrix.zeros(nrows, ncomp); 568 | const Q = Matrix.zeros(nrows, ncomp); 569 | const expvar = Vector.zeros(ncomp); 570 | const cumexpvar = Vector.zeros(ncomp); 571 | 572 | // compute total sum of squares 573 | const ha = new Vector.valuesConstructor(nrows); 574 | const E = Xp.copy(); 575 | const totssq = ssq(Xp.v); 576 | 577 | // loop for computing variances and distances 578 | for (let a = 1; a <= ncomp; a++) { 579 | const ta = T.getcolref(a); 580 | const pa = P.getcolref(a); 581 | const ts = 1 / eigenvals.v[a - 1]; 582 | 583 | const qa = new Vector.valuesConstructor(nrows); 584 | let qs = 0; 585 | for (let c = 0; c < nvars; c++) { 586 | const e = E.getcolref(c + 1); 587 | for (let r = 0; r < nrows; r++) { 588 | e[r] = e[r] - ta[r] * pa[c] 589 | const essq = e[r] * e[r]; 590 | qa[r] += essq; 591 | qs += essq; 592 | } 593 | } 594 | 595 | for (let r = 0; r < nrows; r++) { 596 | ha[r] += ta[r] * ta[r] * ts; 597 | } 598 | 599 | H.v.set(ha, (a - 1) * nrows); 600 | Q.v.set(qa, (a - 1) * nrows); 601 | 602 | cumexpvar.v[a - 1] = 100 * (1 - qs / totssq); 603 | expvar.v[a - 1] = a === 1 ? cumexpvar.v[a - 1] : cumexpvar.v[a - 1] - cumexpvar.v[a - 2]; 604 | } 605 | 606 | return { 607 | class: ['pcares', 'ldecomp'], 608 | T: T, 609 | H: H, 610 | Q: Q, 611 | expvar: expvar, 612 | cumexpvar: cumexpvar 613 | } 614 | } 615 | 616 | 617 | export function getfulldistance(h, q, h0, q0, Nh, Nq) { 618 | const fh = Nh / h0; 619 | const fq = Nq / q0; 620 | 621 | const f = Vector.zeros(h.length); 622 | for (let r = 0; r <= f.length; r++) { 623 | f.v[r] = h.v[r] * fh + q.v[r] * fq; 624 | } 625 | 626 | return f; 627 | } 628 | 629 | 630 | /** 631 | * Project new data to a PCA model and create object with main outcomes. 632 | * 633 | * @param {Object} m - JSON with PCA model created by 'pcafit()'. 634 | * @param {Matrix} X - matrix with data values. 635 | * @param {string} name - name/label for the result object (e.g. "cal", "val", etc.). 636 | * 637 | * @returns {Object} - JSON wiht main outcomes (scores, distances, variance, etc.). 638 | * 639 | */ 640 | export function pcapredict(m, X, name) { 641 | 642 | if (!ismatrix(X) || X.nrows < 1) { 643 | throw Error('pcapredict: parameter "X" must be a matrix.'); 644 | } 645 | 646 | if (X.ncols != m.P.nrows) { 647 | throw Error('pcapredict: parameter "X" has wrong number of columns.'); 648 | } 649 | 650 | const Xp = prep_scale(X, m.mX, m.sX); 651 | const res = pcagetmainres(Xp, Xp.dot(m.P), m.P, m.eigenvals); 652 | res.name = name; 653 | 654 | return res; 655 | } 656 | 657 | 658 | /** 659 | * For given vector 'x' and number 'd' returns matrix with power of x values from 1 to d as columns. 660 | * 661 | * @param {Vector} x - vector with values. 662 | * @param {number} d - polynomial degree. 663 | * 664 | * @returns a matrix with 'd' columns. 665 | * 666 | */ 667 | export function polymat(x, d) { 668 | 669 | if (!isvector(x)) { 670 | throw Error('polymat: argument "x" must be a vector.'); 671 | } 672 | 673 | const X = Matrix.zeros(x.length, d); 674 | for (let i = 1; i <= d; i++) { 675 | X.v.set(x.apply(v => v ** i).v, x.length * (i - 1)); 676 | } 677 | 678 | return X; 679 | } 680 | 681 | 682 | /** 683 | * Fitting a univariate polynomial model. 684 | * 685 | * @param {Vector} x - vector with predictors. 686 | * @param {Vector} y - vector with responses. 687 | * @param {number} d - polynomial degree. 688 | * 689 | * @return JSON with model parameters and performance statistics. 690 | * 691 | */ 692 | export function polyfit(x, y, d) { 693 | 694 | if (d < 1 || d >= x.length) { 695 | throw Error('polyfit: polynomial degree "d" must a positive value smaller than number of measurements.'); 696 | } 697 | 698 | let model = lmfit(polymat(x, d), y); 699 | model.pdegree = d; 700 | model.class = "pm"; 701 | 702 | return model; 703 | } 704 | 705 | 706 | /** 707 | * Predicts response values based on the fitted model and predictors. 708 | * 709 | * @param {JSON} m - regression model (object returned by 'polyfit()'). 710 | * @param {vector} x - vector with predictors. 711 | * 712 | * @return vector with predicted response values. 713 | * 714 | */ 715 | export function polypredict(m, x) { 716 | 717 | if (!isvector(x)) { 718 | throw Error('polypredict: Argument "x" must be a vector.'); 719 | } 720 | 721 | if (m.class !== "pm") { 722 | throw Error('polypredict: argument "m" must be object with "pm" model.'); 723 | } 724 | 725 | return lmpredict(m, polymat(x, m.pdegree)); 726 | } 727 | 728 | 729 | /** 730 | * Fit a linear model (SLR or MLR). 731 | * 732 | * @param {Vector|Matrix} X - vector or matrix with predictors. 733 | * @param {Vector} y - vector with responses. 734 | * 735 | * @return JSON with model parameters and performance statistics. 736 | * 737 | */ 738 | export function lmfit(X, y) { 739 | 740 | if (isvector(X)) { 741 | X = reshape(X, X.length, 1); 742 | } 743 | 744 | if (!ismatrix(X)) { 745 | throw Error('lmfit: argument "X" must be a matrix or a vector.'); 746 | } 747 | 748 | if (!isvector(y)) { 749 | throw Error('lmfit: argument "y" must be a vector.'); 750 | } 751 | 752 | const n = X.nrows; 753 | if (y.length !== n) { 754 | throw Error('lmfit: arguments "X" and "y" must have the same number of objects.'); 755 | } 756 | 757 | if (n <= X.ncols) { 758 | throw Error('lmfit: number of objects must be larger than number of predictors.'); 759 | } 760 | 761 | // add column of ones for estimation of intercept 762 | const Xr = cbind(Vector.ones(n), X); 763 | 764 | // compute inverse of variance-covariance matrix 765 | const R = crossprod(Xr).inv(); 766 | 767 | // estimate regression coefficients 768 | const estimate = reshape(R.dot(Xr.t()).dot(y), X.ncols + 1); 769 | 770 | // compute predicted y-values and performance statistics 771 | const fitted = reshape(Xr.dot(estimate), n); 772 | const stat = regstat(y, fitted, X.ncols); 773 | 774 | // compute standard error and t-values for regression coefficients, H0: beta = 0 775 | const coeffse = R.mult(stat.se * stat.se).diag().apply(Math.sqrt); 776 | const tstat = estimate.divide(coeffse); 777 | 778 | // compute critical t-value for confidence intervals 779 | const tCrit = qt(0.975, stat.DoF); 780 | 781 | // return JSON with all results 782 | const errMargin = coeffse.mult(tCrit); 783 | return { 784 | class: "lm", 785 | data: {X: X, y: y}, 786 | coeffs: { 787 | estimate: estimate, 788 | se: coeffse, 789 | tstat: tstat, 790 | p: tstat.apply(t => t > 0 ? 2 * pt(-t, stat.DoF) : 2 * pt(t, stat.DoF)), 791 | lower: estimate.subtract(errMargin), 792 | upper: estimate.add(errMargin) 793 | }, 794 | fitted: fitted, 795 | stat: stat 796 | } 797 | } 798 | 799 | 800 | /** 801 | * Predicts response values based on the fitted model and predictors 802 | * @param {JSON} m - regression model (object returned by 'limfit()') 803 | * @param {number[]} X - vector or matrix with predictors 804 | * @return vector with predicted response values 805 | */ 806 | export function lmpredict(m, X) { 807 | 808 | if (isvector(X)) { 809 | X = reshape(X, X.length, 1); 810 | } 811 | 812 | if (!ismatrix(X)) { 813 | throw Error('lmpredict: argument "X" must be a matrix or a vector.'); 814 | } 815 | 816 | if (!(m.class === 'lm' || m.class === 'pm') || !m.coeffs.estimate || m.coeffs.estimate.length < 1) { 817 | throw Error('lmpredict: argument "m" must be object with "lm" model.'); 818 | } 819 | 820 | if (X.ncols !== (m.coeffs.estimate.length - 1)) { 821 | throw Error('lmpredict: number of columns in "X" does not match number of coefficients in model.'); 822 | } 823 | 824 | 825 | // add column of ones for estimation of intercept 826 | const n = X.nrows; 827 | const Xr = cbind(Vector.ones(n), X); 828 | return reshape(Xr.dot(m.coeffs.estimate), n); 829 | } 830 | 831 | 832 | /** 833 | * Computes performance statistics for predicted and reference response values 834 | * @param {number[]} y — vector with reference response values 835 | * @param {number[]} yp — vector with predicted response values 836 | * @return JSON with statistics (adjusted R2, R2, s(e), F-value, p-value) 837 | */ 838 | export function regstat(y, yp, p) { 839 | 840 | const n = y.length; 841 | if (!p) p = 1; 842 | const e = y.subtract(yp); 843 | const SSe = ssq(e); 844 | const SSy = variance(y) * (n - 1); 845 | const R2 = (1 - SSe / SSy); 846 | const DoF = n - p - 1 847 | const F = ((SSy - SSe)/p) / (SSe/DoF); 848 | 849 | return { 850 | R2: R2, 851 | R2adj: 1 - (1 - R2) * (n - 1) / DoF, 852 | Fstat: F, 853 | p: 1 - pf(F, p, DoF), 854 | DoF: DoF, 855 | se: Math.sqrt(SSe/DoF) 856 | }; 857 | } 858 | 859 | 860 | /** 861 | * Split matrix with data values into matrix of predictors (X) and and matrix with responses (Y). 862 | * 863 | * @param {Matrix} data - matrix with data values. 864 | * 865 | * @returns {Array} array with X and Y. 866 | */ 867 | export function splitregdata(data) { 868 | 869 | return [ 870 | new Matrix(data.v.subarray(data.nrows), data.nrows, data.ncols - 1), 871 | new Matrix(data.getcolref(1), data.nrows, 1) 872 | ]; 873 | } 874 | 875 | 876 | /** 877 | * Make predictions based on x-scores and y-loadings (for PCR or PLS1) and compute 878 | * performance statistics if reference y-values are provided. 879 | * 880 | * @param {Matrix} T - matrix with X-scores. 881 | * @param {Matrix} C - matrix with y-loadings. 882 | * @param {number} mY - number to use for centering y-values (from the model). 883 | * @param {number} sY - number to use for scaling y-values (from the model). 884 | * @param {Vector} [y] - vector with reference response values. 885 | */ 886 | export function reggetmainres(T, C, mY, sY, Yref) { 887 | 888 | const nrows = T.nrows; 889 | const ncomp = T.ncols; 890 | 891 | // prepare variables for predictions and statistics 892 | const Ypred = Matrix.zeros(nrows, ncomp); 893 | 894 | // if no reference values, compute predictions and return 895 | if (Yref === undefined || Yref === null || Yref.length === 0) { 896 | for (let a = 1; a <= ncomp; a++) { 897 | // compute predictions 898 | const cind = Index.seq(1, a); 899 | const Ypreda = prep_unscale(tcrossprod(T.subset([], cind), C.subset([], cind)), mY, sY); 900 | Ypred.v.set(Ypreda.v, (a - 1) * nrows); 901 | } 902 | 903 | return {'Ypred': Ypred}; 904 | } 905 | 906 | // otherwise compute statistics as well 907 | const rmse = Vector.zeros(ncomp); 908 | const r2 = Vector.zeros(ncomp); 909 | const bias = Vector.zeros(ncomp); 910 | 911 | // total y-variance 912 | const ssy = variance(Yref.v) * (Yref.nrows - 1); 913 | for (let a = 1; a <= ncomp; a++) { 914 | 915 | // compute predictions 916 | const cind = Index.seq(1, a); 917 | const Ypreda = prep_unscale(tcrossprod(T.subset([], cind), C.subset([], cind)), mY, sY); 918 | Ypred.v.set(Ypreda.v, (a - 1) * nrows); 919 | 920 | // compute performance statistics 921 | const e = Yref.subtract(Ypreda); 922 | const sse = ssq(e.v); 923 | bias.v[a - 1] = mean(e.v); 924 | rmse.v[a - 1] = Math.sqrt(sse / nrows); 925 | r2.v[a - 1] = 1 - sse / ssy; 926 | } 927 | 928 | return { 929 | "Yref": Yref, 930 | "Ypred": Ypred, 931 | "rmse": rmse, 932 | "r2": r2, 933 | "bias": bias 934 | }; 935 | } 936 | 937 | 938 | /** 939 | * Implementation of SIMPLS algorithm. 940 | * 941 | * @param {Matrix} X - matrix with predictors. 942 | * @param {Matrix} Y - matrix with responses. 943 | * @param {number} ncomp - number of components. 944 | * 945 | * @return {JSON} JSON with decomposition results. 946 | * 947 | */ 948 | export function simpls(X, Y, ncomp) { 949 | 950 | const nobj = X.nrows; 951 | const npred = X.ncols; 952 | const nresp = Y.ncols; 953 | 954 | // initial estimation 955 | let S = crossprod(X, Y); 956 | let M = crossprod(X); 957 | 958 | // prepare space for results 959 | const C = Matrix.zeros(nresp, ncomp) 960 | const R = Matrix.zeros(npred, ncomp) 961 | const V = Matrix.zeros(npred, ncomp) 962 | const P = Matrix.zeros(npred, ncomp) 963 | const T = Matrix.zeros(nobj, ncomp) 964 | const U = Matrix.zeros(nobj, ncomp) 965 | 966 | const xeigenvals = Vector.zeros(ncomp); 967 | const yeigenvals = Vector.zeros(ncomp); 968 | 969 | // loop for each components 970 | for (let a = 1; a <= ncomp; a++) { 971 | 972 | let r = rsvd(S, 1).U 973 | let t = X.dot(r); 974 | 975 | const tnorm = norm2(t.v); 976 | t = t.apply(v => v / tnorm, 0); 977 | r = r.apply(v => v / tnorm, 0); 978 | 979 | const p = crossprod(X, t); 980 | const c = crossprod(Y, t); 981 | let u = Y.dot(c); 982 | let v = p.copy() 983 | 984 | if (a > 1) { 985 | v = v.subtract(V.dot(crossprod(V, p))); 986 | u = u.subtract(T.dot(crossprod(T, u))); 987 | } 988 | 989 | const vnorm = norm2(v.v); 990 | v = v.apply(x => x / vnorm, 0); 991 | 992 | R.v.set(r.v, (a - 1) * npred); 993 | V.v.set(v.v, (a - 1) * npred); 994 | P.v.set(p.v, (a - 1) * npred); 995 | T.v.set(t.v, (a - 1) * nobj); 996 | U.v.set(u.v, (a - 1) * nobj); 997 | C.v.set(c.v, (a - 1) * nresp); 998 | 999 | 1000 | xeigenvals.v[a - 1] = ssq(t.v) / (nobj - 1); 1001 | yeigenvals.v[a - 1] = ssq(u.v) / (nobj - 1); 1002 | 1003 | M = M.subtract(tcrossprod(p)) 1004 | S = S.subtract(v.dot(crossprod(v, S))); 1005 | } 1006 | 1007 | return { 1008 | R: R, P: P, T: T, C: C, U: U, 1009 | xeigenvals: xeigenvals, 1010 | yeigenvals: yeigenvals 1011 | }; 1012 | } 1013 | -------------------------------------------------------------------------------- /src/prep/index.js: -------------------------------------------------------------------------------- 1 | /********************************************************************/ 2 | /* Methods for Preprocessing (work with vectors and matrices) */ 3 | /********************************************************************/ 4 | 5 | import { isvector, Vector, Matrix, vector } from '../arrays/index.js'; 6 | import { mean, sd } from '../stat/index.js'; 7 | 8 | /** 9 | * Uncenter and unscale values in every column of a matrix. 10 | * 11 | * @param {Matrix} X - matrix with values. 12 | * @param {Vector} center - vector with numbers to use for uncentering. 13 | * @param {Vector} scale - vector with numbers to use for unscaling. 14 | * 15 | * @returns {Matrix} a matrix with uncentered and unscaled column values. 16 | * 17 | */ 18 | export function unscale(X, centerValues, scaleValues) { 19 | 20 | const Xp = Matrix.zeros(X.nrows, X.ncols); 21 | for (let c = 1; c <= X.ncols; c++) { 22 | const xc = X.getcolref(c); 23 | const xpc = Xp.getcolref(c); 24 | 25 | const cv = centerValues.v[c - 1]; 26 | const sv = scaleValues.v[c - 1]; 27 | 28 | for (let r = 0; r < X.nrows; r++) { 29 | xpc[r] = xc[r] * sv + cv; 30 | } 31 | } 32 | 33 | return Xp; 34 | } 35 | 36 | 37 | /** 38 | * Center and scale values in every column of a matrix. 39 | * 40 | * @param {Matrix} X - matrix with values. 41 | * @param {boolean|Vector} [center=true] - logical value or a vector with numbers to use for centering. 42 | * @param {boolean|Vector} [scale=true] - logical value or a vector with numbers to use for scaling. 43 | * @param {boolean} [full=true] - logical, influences the return values. 44 | * 45 | * @returns {Array|Matrix} either a matrix with centered and scaled column values (if 'full = false') 46 | * or an array with three elements: the matrix, vector with values for centring and vector with values 47 | * for scaling. 48 | * 49 | */ 50 | export function scale(X, center, scale, full) { 51 | 52 | if (center === undefined) { 53 | center = true; 54 | } 55 | 56 | if (scale === undefined) { 57 | scale = false; 58 | } 59 | 60 | if (full === undefined) { 61 | full = false; 62 | } 63 | 64 | function getStat(X, param, fun, alt) { 65 | 66 | if (typeof(param) === "boolean") { 67 | return param ? X.apply(fun, 2) : vector([alt]).rep(X.ncols); 68 | } 69 | 70 | if (!isvector(param)) { 71 | throw Error("scale: parameters 'center' and 'scale' must be boolean or vectors with numeric values."); 72 | } 73 | 74 | if (param.length !== X.ncols ) { 75 | throw Error("scale: number of values for centring and scaling must be the same as number of columns in the matrix."); 76 | } 77 | 78 | return param; 79 | } 80 | 81 | // prepare values for centering and scaling 82 | const centerValues = getStat(X, center, mean, 0); 83 | const scaleValues = getStat(X, scale, sd, 1); 84 | 85 | const Xp = Matrix.zeros(X.nrows, X.ncols); 86 | for (let c = 1; c <= X.ncols; c++) { 87 | const xc = X.getcolref(c); 88 | const xpc = Xp.getcolref(c); 89 | 90 | const cv = centerValues.v[c - 1]; 91 | const sv = 1 / scaleValues.v[c - 1]; 92 | 93 | for (let r = 0; r < X.nrows; r++) { 94 | xpc[r] = (xc[r] - cv) * sv; 95 | } 96 | } 97 | 98 | return full ? [Xp, centerValues, scaleValues] : Xp; 99 | } 100 | 101 | -------------------------------------------------------------------------------- /src/stat/index.js: -------------------------------------------------------------------------------- 1 | /*************************************************/ 2 | /* Methods for computing statistics */ 3 | /*************************************************/ 4 | 5 | import { isnumber, isindex, isvector, vector, Vector, _sort } from '../arrays/index.js'; 6 | 7 | 8 | /** 9 | * Compute median of vector values. 10 | * 11 | * @param {Array|Vector} x - vector with values. 12 | * 13 | * @returns {number} median of x. 14 | * 15 | */ 16 | export function median(x) { 17 | return quantile(x, 0.5) 18 | } 19 | 20 | /** 21 | * Compute inter-quartile range for vector of values. 22 | * 23 | * @param {Array|Vector} x - vector with values. 24 | * 25 | * @returns {number} IQR of x (Q3 - Q1). 26 | * 27 | */ 28 | export function iqr(x) { 29 | return quantile(x, 0.75) - quantile(x, 0.25); 30 | } 31 | 32 | 33 | /** 34 | * Computes a p-th quantile/quantiles for a numeric vector. 35 | * 36 | * @param {Vector} x - vector with values. 37 | * @param {number|Array|Vector} p - probability (one value or a vector). 38 | * 39 | * @returns {number|Vector} quantile value or a vector with quantiles. 40 | */ 41 | export function quantile(x, p) { 42 | 43 | if (isvector(x)) { 44 | return quantile(x.v, p); 45 | } 46 | 47 | if (isvector(p)) { 48 | return quantile(x, p.v); 49 | } 50 | 51 | const n = x.length; 52 | 53 | if (!Array.isArray(p)) p = [p]; 54 | 55 | if (!isnumber(p[0]) || min(p) < 0 || max(p) > 1) { 56 | throw new Error("Parameter 'p' must be between 0 and 1 (both included)."); 57 | } 58 | 59 | function q(x, p) { 60 | const h = (n - 1) * p + 1; 61 | const n1 = Math.floor(h); 62 | const n2 = Math.ceil(h); 63 | return x[n1 - 1] + (x[n2 - 1] - x[n1 - 1]) * (h - Math.floor(h)); 64 | } 65 | 66 | const xs = _sort(x); 67 | const out = p.map(v => q(xs, v)); 68 | return p.length == 1 ? out[0] : vector(out); 69 | } 70 | 71 | 72 | /** 73 | * Counts how many values from a vector falls into provided intervals (bins). 74 | * 75 | * @param {Array|Vector} x - vector with values. 76 | * @param {Array|Vector} bins - vector with bins boundaries. 77 | * 78 | * @returns {Vector} vector with counts for each bean. 79 | * 80 | */ 81 | export function count(x, bins) { 82 | 83 | if (isvector(x)) { 84 | return count(x.v, isvector(bins) ? bins.v : bins); 85 | } 86 | 87 | const n = bins.length; 88 | 89 | // add a bit extra to right side of the last bin 90 | bins[n - 1] = bins[n - 1] * 1.0001 91 | 92 | // count 93 | let counts = new Vector.valuesConstructor(n - 1); 94 | for (let i = 0; i < x.length; i++) { 95 | for (let j = 0; j < n - 1; j++) { 96 | if (x[i] >= bins[j] && x[i] < bins[j + 1]) counts[j] += 1; 97 | } 98 | } 99 | 100 | return new Vector(counts); 101 | } 102 | 103 | 104 | /** 105 | * Computes middle points between values of a vector. 106 | * 107 | * @param {Array|Vector} x - vector with values. 108 | * 109 | * @returns {Vector} vector with middle points. 110 | * 111 | */ 112 | export function mids(x) { 113 | 114 | if (isvector(x)) { 115 | return mids(x.v); 116 | } 117 | 118 | const out = new Vector.valuesConstructor(x.length - 1); 119 | for (let i = 0; i < out.length; i++) { 120 | out[i] = 0.5 * (x[i] + x[i + 1]); 121 | } 122 | 123 | return new Vector(out); 124 | } 125 | 126 | 127 | /** 128 | * Splits range of vector values into equal intervals. 129 | * 130 | * @param {Array|Vector} x - vector with values. 131 | * @param {number} n - number of intervals. 132 | * 133 | * @returns {Vector} vector with boundaries of the intervals. 134 | * 135 | */ 136 | export function split(x, n) { 137 | 138 | if (isvector(x)) { 139 | split(x.v, n); 140 | } 141 | 142 | const rn = range(x); 143 | 144 | if (rn[0] === rn[1]) { 145 | throw new Error('split: values in a vector "x" should vary.'); 146 | } 147 | 148 | const by = (rn[1] - rn[0]) / n; 149 | return Vector.seq(rn[0], rn[1], by); 150 | } 151 | 152 | 153 | /** 154 | * Computes difference between all adjacent values in a vector. 155 | * 156 | * @param {Array|Vector} x - vector with values. 157 | * 158 | * @returns {Vector} vector with the differences. 159 | * 160 | */ 161 | export function diff(x) { 162 | 163 | if (isvector(x)) { 164 | return diff(x.v); 165 | } 166 | 167 | const out = new Vector.valuesConstructor(x.length - 1); 168 | for (let i = 0; i < x.length - 1; i++) { 169 | out[i] = x[i + 1] - x[i]; 170 | } 171 | 172 | return new Vector(out); 173 | } 174 | 175 | 176 | /** 177 | * Generate probability points for QQ plot. 178 | * 179 | * @param {number} n - number of points. 180 | * 181 | * @returns {Vector} a sequence of probabilities between 0 and 1. 182 | * 183 | */ 184 | export function ppoints(n) { 185 | 186 | const a = n < 10 ? 3.0/8.0 : 0.5; 187 | const out = new Vector.valuesConstructor(n); 188 | 189 | for (let i = 0; i < n; i++) { 190 | out[i] = (i + 1 - a) / (n + (1 - a) - a); 191 | } 192 | 193 | return new Vector(out); 194 | } 195 | 196 | 197 | /** 198 | * Computes cumulative sums for the vector values. 199 | * 200 | * @param {Array|Vector} x - vector with values. 201 | * 202 | * @returns {Vectors} vector with cumulative sums. 203 | * 204 | */ 205 | export function cumsum(x) { 206 | 207 | if (isvector(x)) { 208 | return cumsum(x.v); 209 | } 210 | 211 | let s = 0; 212 | let out = new Vector.valuesConstructor(x.length); 213 | for (let i = 0; i < x.length; i++) { 214 | s += x[i]; 215 | out[i] = s 216 | } 217 | 218 | return new Vector(out); 219 | } 220 | 221 | 222 | /** 223 | * Computes kurtosis of values. 224 | * 225 | * @param {Array|Vector} x - vector with values. 226 | * 227 | * @returns {number} kurtosis of x. 228 | * 229 | */ 230 | export function kurtosis(x) { 231 | 232 | if (isvector(x)) { 233 | return kurtosis(x.v); 234 | } 235 | 236 | let n = x.length; 237 | let m = mean(x); 238 | 239 | let m4 = 0.0; 240 | let m2 = 0.0; 241 | for (let i = 0; i < n; i++) { 242 | m2 = m2 + Math.pow((x[i] - m), 2); 243 | m4 = m4 + Math.pow((x[i] - m), 4); 244 | } 245 | 246 | return (m4/n) / Math.pow((m2/n), 2); 247 | } 248 | 249 | 250 | /** 251 | * Computes skewness of values. 252 | * 253 | * @param {Array|Vector} x - vector with values. 254 | * 255 | * @returns {number} skewness of x. 256 | * 257 | */ 258 | export function skewness(x) { 259 | 260 | if (isvector(x)) { 261 | return skewness(x.v); 262 | } 263 | 264 | let n = x.length; 265 | let m = mean(x); 266 | 267 | let m3 = 0.0; 268 | let m2 = 0.0; 269 | for (let i = 0; i < n; i++) { 270 | m2 = m2 + Math.pow((x[i] - m), 2); 271 | m3 = m3 + Math.pow((x[i] - m), 3); 272 | } 273 | 274 | return (m3/n) / Math.pow((m2/n), 1.5); 275 | } 276 | 277 | 278 | /** 279 | * Computes correlation between two vectors. 280 | * 281 | * @param {Array|Vector} x - vector with values. 282 | * @param {Array|Vector} y - vector with values. 283 | * @param {string} method - which method to use ("pearson" or "spearman"). 284 | * 285 | * @returns {number} correlation between x and y. 286 | * 287 | */ 288 | export function cor(x, y, method = "pearson") { 289 | 290 | if (isvector(x)) { 291 | return cor(x.v, y.v, method); 292 | } 293 | 294 | if (method === "spearman") { 295 | return cor(rank(x), rank(y)); 296 | } 297 | 298 | return cov(x, y) / (sd(x) * sd(y)); 299 | } 300 | 301 | 302 | /** 303 | * Computes covariance between two vectors. 304 | * 305 | * @param {Array|Vector} x - vector with values. 306 | * @param {Array|Vector} y - vector with values. 307 | * @param {boolean} [biased=false] - compute a biased version with n degrees of freedom or not (with n - 1). 308 | * @param {number} [mx=undefined] - mean of x values (if already known). 309 | * @param {number} [my=undefined] - mean of y values (if already known). 310 | * 311 | * @returns {number} covariance between x and y. 312 | * 313 | */ 314 | export function cov(x, y, biased = false, mx = undefined, my = undefined) { 315 | 316 | if (isvector(x)) { 317 | return cov(x.v, y.v, biased, mx, my); 318 | } 319 | 320 | const n = x.length; 321 | 322 | if (y.length !== n) { 323 | throw Error("Vectors 'x' and 'y' must have the same length."); 324 | } 325 | 326 | if (n < 2) { 327 | throw Error("Vectors 'x' and 'y' must have at least two values."); 328 | } 329 | 330 | if (mx === undefined) mx = mean(x); 331 | if (my === undefined) my = mean(y); 332 | 333 | let s = 0; 334 | for (let i = 0; i < n; i++) { 335 | s = s + (x[i] - mx) * (y[i] - my); 336 | } 337 | 338 | return s / (biased ? n : n - 1); 339 | } 340 | 341 | 342 | /** 343 | * Returns ranks of values in a vector (ranks start from 1, not 0). 344 | * 345 | * @param {Array|Vector} x - vector with values. 346 | * 347 | * @returns {Vector} vector with ranks. 348 | * 349 | */ 350 | export function rank(x) { 351 | 352 | if (isvector(x)) { 353 | return rank(x.v); 354 | } 355 | 356 | const y = [...x].sort((a, b) => a - b); 357 | 358 | return new Vector(x.map(v => y.indexOf(v) + 1)); 359 | } 360 | 361 | 362 | /** 363 | * Compute marginal range of values as [min, max] 364 | * 365 | * @param {Array|Vector} x - vector with values. 366 | * @param {number} m - margin as per cent of range (value between 0 and 1). 367 | * 368 | * @return {Array} array with two values, min and max. 369 | * 370 | */ 371 | export function mrange(x, m) { 372 | 373 | if (isvector(x)) { 374 | return mrange(x.v, m); 375 | } 376 | 377 | if (m === undefined) m = 0.10; 378 | const r = range(x); 379 | const d = (r[1] - r[0]) * m; 380 | 381 | return [r[0] - d, r[1] + d]; 382 | } 383 | 384 | 385 | /** 386 | * Compute range of values as [min, max]. 387 | * 388 | * @param {Array|Vector} x - vector with values. 389 | * 390 | * @return {Array} array with two values, min and max. 391 | * 392 | */ 393 | export function range(x) { 394 | 395 | if (isvector(x)) { 396 | return range(x.v); 397 | } 398 | 399 | let min = x[0]; 400 | let max = x[0]; 401 | 402 | for (let i = 1; i < x.length; i++) { 403 | if (x[i] < min) min = x[i]; 404 | if (x[i] > max) max = x[i]; 405 | } 406 | 407 | return [min, max]; 408 | } 409 | 410 | 411 | /** 412 | * Compute norm2 of a vector (Euclidean distance). 413 | * 414 | * @param {Array|Vector} x - vector with values. 415 | * 416 | * @returns {number} norm value. 417 | * 418 | */ 419 | export function norm2(x) { 420 | 421 | if (isvector(x)) { 422 | return norm2(x.v); 423 | } 424 | 425 | return Math.sqrt(ssq(x)); 426 | } 427 | 428 | 429 | /** 430 | * Compute sum of squared vector values 431 | * 432 | * @param {Array|Vector} x - vector with values. 433 | * 434 | * @returns {number} sum of squared values of x. 435 | * 436 | */ 437 | export function ssq(x) { 438 | 439 | if (isvector(x)) { 440 | return ssq(x.v); 441 | } 442 | 443 | let ssqv = 0; 444 | const n = x.length; 445 | for (let i = 0; i < n; i++) { 446 | const v = x[i] 447 | ssqv += v * v; 448 | } 449 | 450 | return ssqv; 451 | } 452 | 453 | 454 | /** 455 | * Compute standard deviation of vector values. 456 | * 457 | * @param {Array|Vector} x - vector with values. 458 | * @param {boolean} [biased=false] - compute a biased value (n degrees of freedom) or unbiased (n - 1 degrees of freedom) 459 | * 460 | * @returns {number} standard deviation of x. 461 | * 462 | */ 463 | export function sd(x, biased) { 464 | 465 | if (isvector(x)) { 466 | return sd(x.v, biased); 467 | } 468 | 469 | return Math.sqrt(variance(x, biased)); 470 | } 471 | 472 | 473 | /** 474 | * Compute variance of vector values. 475 | * 476 | * @param {Array|Vector} x - vector with values. 477 | * @param {boolean} [biased=false] - compute a biased variance (n degrees of freedom) or unbiased (n - 1 degrees of freedom) 478 | * 479 | * @returns {number} variance of x. 480 | * 481 | */ 482 | export function variance(x, biased) { 483 | 484 | if (biased === undefined) { 485 | biased = false; 486 | } 487 | 488 | if (isvector(x)) { 489 | return variance(x.v); 490 | } 491 | 492 | const m = mean(x); 493 | let s = 0; 494 | for (let i = 0; i < x.length; i++) { 495 | const d = (x[i] - m); 496 | s = s + d * d ; 497 | } 498 | 499 | return s / (biased ? x.length : (x.length - 1)); 500 | } 501 | 502 | 503 | /** 504 | * Compute average of vector values. 505 | * 506 | * @param {Array|Vector} x - vector with values. 507 | * 508 | * @returns {number} mean of x. 509 | * 510 | */ 511 | export function mean(x) { 512 | 513 | if (isvector(x)) { 514 | return mean(x.v); 515 | } 516 | 517 | return sum(x) / x.length; 518 | } 519 | 520 | 521 | /** 522 | * Compute sum of all values in a vector. 523 | * 524 | * @param {Array|Vector} x - vector with values. 525 | * 526 | * @returns {number} sum of x. 527 | * 528 | */ 529 | export function sum(x) { 530 | 531 | if (isvector(x)) { 532 | return sum(x.v); 533 | } 534 | 535 | let s = 0; 536 | for (let i = 0; i < x.length; i++) { 537 | s = s + x[i]; 538 | } 539 | 540 | return s; 541 | } 542 | 543 | 544 | /** 545 | * Compute product of all values in a vector. 546 | * 547 | * @param {Array|Vector} x - vector with values. 548 | * 549 | * @returns {number} product of x. 550 | * 551 | */ 552 | export function prod(x) { 553 | 554 | if (isvector(x)) { 555 | return prod(x.v); 556 | } 557 | 558 | let p = 1; 559 | for (let i = 0; i < x.length; i++) { 560 | p = p * x[i]; 561 | } 562 | 563 | return p; 564 | } 565 | 566 | 567 | /** 568 | * Find the smallest element in a vector. 569 | * 570 | * @param {Array|Vector|Index} x - vector or index with values. 571 | * 572 | * @returns {number} the smallest value. 573 | * 574 | */ 575 | export function min(x) { 576 | 577 | if (isvector(x) || isindex(x)) { 578 | return x.v[minind(x.v) - 1]; 579 | } 580 | 581 | return x[minind(x) - 1]; 582 | } 583 | 584 | 585 | /** 586 | * Find index of the smallest element in a vector. 587 | * 588 | * @param {Array|Vector|Index} x - vector or index with values. 589 | * 590 | * @returns {number} index of the smallest value (starting from 1). 591 | * 592 | */ 593 | export function minind(x) { 594 | 595 | if (isvector(x) || isindex(x)) { 596 | return minind(x.v); 597 | } 598 | 599 | let outind = 1; 600 | let out = x[0] 601 | for (let i = 2; i <= x.length; i++) { 602 | if (x[i - 1] < out) { 603 | outind = i; 604 | out = x[i - 1]; 605 | } 606 | } 607 | 608 | return outind; 609 | } 610 | 611 | 612 | /** 613 | * Find the largest element in a vector. 614 | * 615 | * @param {Array|Vector|Index} x - vector or index with values. 616 | * 617 | * @returns {number} the largest value. 618 | * 619 | */ 620 | export function max(x) { 621 | 622 | if (isvector(x) || isindex(x)) { 623 | return x.v[maxind(x.v) - 1]; 624 | } 625 | 626 | return x[maxind(x) - 1]; 627 | } 628 | 629 | 630 | /** 631 | * Find index of the largest element in a vector. 632 | * 633 | * @param {Array|Vector|Index} x - vector or index with values. 634 | * 635 | * @returns {number} index of the largest value (starting from 1). 636 | * 637 | */ 638 | export function maxind(x) { 639 | 640 | if (isvector(x) || isindex(x)) { 641 | return maxind(x.v); 642 | } 643 | 644 | let outind = 1; 645 | let out = x[0] 646 | for (let i = 2; i <= x.length; i++) { 647 | if (x[i - 1] > out) { 648 | outind = i; 649 | out = x[i - 1]; 650 | } 651 | } 652 | 653 | return outind; 654 | } 655 | 656 | 657 | -------------------------------------------------------------------------------- /src/tests/index.js: -------------------------------------------------------------------------------- 1 | /*************************************************/ 2 | /* Methods for hypothesis testing */ 3 | /*************************************************/ 4 | 5 | import { min, mean, variance, sd } from '../stat/index.js'; 6 | import { pt, qt } from '../distributions/index.js'; 7 | 8 | /** 9 | * Returns a p-value for any test. 10 | * 11 | * @param {function} pfun - reference to a CDF function (e.g. pnorm). 12 | * @param {number} crit - critical value for the test (e.g. z-score or t-score). 13 | * @param {string} tail - which tail to use ("left", "right", or "both"). 14 | * @param {Array} params - additional parameters to CDF function. 15 | * 16 | * @returns {number} a p-value for the test. 17 | * 18 | */ 19 | export function getpvalue(pfun, crit, tail, params = []) { 20 | 21 | if (tail === "left") { 22 | return(pfun(crit, ...params)); 23 | } 24 | 25 | if (tail === "right") { 26 | return(1 - pfun(crit, ...params)); 27 | } 28 | 29 | if (tail === "both") { 30 | let p = pfun(crit, ...params); 31 | return min([p, 1 - p]) * 2; 32 | } 33 | } 34 | 35 | 36 | /** 37 | * Makes two-sample t-test for a difference of means assuming population variances equal. 38 | * 39 | * @param {Vector} x - vector with sample 1 values. 40 | * @param {Vector} y - vector with sample 2 values. 41 | * @param {number} alpha - significance level (used to compute confidence interval). 42 | * @param {string} tail - which tail to use ("left", "right", or "both"). 43 | * 44 | * @returns {Object} a JSON with test results. 45 | * 46 | */ 47 | export function ttest2(x, y, alpha = 0.05, tail = "both") { 48 | 49 | const nx = x.length; 50 | const mx = mean(x); 51 | const my = mean(y); 52 | const ny = y.length; 53 | 54 | const effectExpected = 0; 55 | const effectObserved = mx - my; 56 | const se = Math.sqrt( (variance(x) / nx) + (variance(y) / ny)); 57 | const tValue = (effectObserved - effectExpected) / se; 58 | const DoF = (nx - 1) + (ny - 1); 59 | const errMargin = qt(1 - alpha/2, DoF) * se; 60 | 61 | return { 62 | test: "Two sample t-test", 63 | effectExpected: effectExpected, 64 | effectObserved: effectObserved, 65 | se: se, 66 | tValue: tValue, 67 | alpha: alpha, 68 | tail: tail, 69 | DoF: DoF, 70 | pValue: getpvalue(pt, tValue, tail, [DoF]), 71 | ci: [effectObserved - errMargin, effectObserved + errMargin] 72 | }; 73 | } 74 | 75 | 76 | /** 77 | * Makes one-sample t-test for a mean. 78 | * 79 | * @param {Vector} x - vector with sample values. 80 | * @param {number} mu - assumed mean value for population (H0). 81 | * @param {number} alpha - significance level (used to compute confidence interval). 82 | * @param {string} tail - which tail to use ("left", "right", or "both"). 83 | * 84 | * @returns {Object} a JSON with test results. 85 | * 86 | */ 87 | export function ttest(x, mu = 0, alpha = 0.05, tail = "both") { 88 | 89 | if (typeof(mu) !== "number") { 90 | throw Error("Parameter 'mu' should be a number."); 91 | } 92 | 93 | const nx = x.length; 94 | 95 | const effectExpected = mu; 96 | const effectObserved = mean(x); 97 | const se = sd(x) / Math.sqrt(nx); 98 | const tValue = (effectObserved - effectExpected) / se; 99 | const DoF = nx - 1 100 | const errMargin = qt(1 - alpha/2, DoF) * se; 101 | 102 | return { 103 | test: "One sample t-test", 104 | effectExpected: mu, 105 | effectObserved: effectObserved, 106 | se: se, 107 | tValue: tValue, 108 | alpha: alpha, 109 | tail: tail, 110 | DoF: DoF, 111 | pValue: getpvalue(pt, tValue, tail, [DoF]), 112 | ci: [effectObserved - errMargin, effectObserved + errMargin] 113 | }; 114 | } 115 | 116 | -------------------------------------------------------------------------------- /test/tests-arrays-heavy.js: -------------------------------------------------------------------------------- 1 | /****************************************************************/ 2 | /* Tests for array methods (Index/Vector/Matrix classes) */ 3 | /****************************************************************/ 4 | 5 | import {default as chai} from 'chai'; 6 | import {default as chaiAlmost} from 'chai-almost'; 7 | 8 | // import classes and related methods 9 | import { isindex, Index, ismatrix, Matrix, isvector, Vector } from '../src/arrays/index.js'; 10 | 11 | // import non-class methods 12 | import { tcrossprod, crossprod, rbind, cbind } from '../src/arrays/index.js'; 13 | 14 | // set up test settings 15 | const expect = chai.expect; 16 | chai.use(chaiAlmost(0.00001)); 17 | 18 | 19 | // function to test a matrix structure 20 | function testMatrixStructure(X, nr, nc, values) { 21 | expect(X.constructor).equal(Matrix); 22 | expect(X.v.constructor).equal(Float64Array); 23 | expect(X.v.length).equal(nr * nc); 24 | expect(X.nrows).equal(nr); 25 | expect(X.ncols).equal(nc); 26 | expect(ismatrix(X)).to.be.true; 27 | expect(isvector(X)).to.be.false; 28 | 29 | if (values) { 30 | expect(X.v).to.deep.equal(new Float64Array(values)); 31 | } 32 | } 33 | 34 | // function to test a vector structure 35 | function testVectorStructure(x, length, values) { 36 | expect(x.constructor).equal(Vector); 37 | expect(x.v.constructor).equal(Vector.valuesConstructor); 38 | expect(x.v.length).equal(length); 39 | expect(ismatrix(x)).to.be.false; 40 | expect(isvector(x)).to.be.true; 41 | 42 | if (values) { 43 | expect(x.v).to.deep.almost.equal(new Vector.valuesConstructor(values)); 44 | } 45 | } 46 | 47 | // function to test an index structure 48 | function testIndexStructure(x, length, values) { 49 | expect(x.constructor).equal(Index); 50 | expect(x.v.constructor).equal(Index.valuesConstructor); 51 | expect(x.v.length).equal(length); 52 | expect(ismatrix(x)).to.be.false; 53 | expect(isvector(x)).to.be.false; 54 | expect(isindex(x)).to.be.true; 55 | 56 | if (values) { 57 | expect(x.v).to.deep.equal(new Index.valuesConstructor(values)); 58 | } 59 | } 60 | 61 | describe('Tests of methods for matrices and vectors which are heavy in computation.', function() { 62 | 63 | it('tests for method "tcrossprod"', function() { 64 | 65 | // large matrices - nrows = ncols 66 | const X4 = Matrix.rand(1000, 1000); 67 | const Y4 = Matrix.rand(1000, 1000); 68 | const Z4 = tcrossprod(X4, Y4); 69 | testMatrixStructure(Z4, 1000, 1000); 70 | 71 | // large matrices - nrows > ncols 72 | const X5 = Matrix.rand(2000, 500); 73 | const Y5 = Matrix.rand(2000, 500); 74 | const Z5 = tcrossprod(X5, Y5); 75 | testMatrixStructure(Z5, 2000, 2000); 76 | 77 | // large matrices - ncols < nrows 78 | const X6 = Matrix.rand(500, 2000); 79 | const Y6 = Matrix.rand(500, 2000); 80 | const Z6 = tcrossprod(X6, Y6); 81 | testMatrixStructure(Z6, 500, 500); 82 | 83 | // large matrices - same matrix 84 | const Z6a = tcrossprod(X6); 85 | testMatrixStructure(Z6a, 500, 500); 86 | 87 | }).timeout(10000); 88 | 89 | it('tests for method "crossprod"', function() { 90 | 91 | // large matrices - nrows = ncols 92 | const X4 = Matrix.rand(1000, 1000); 93 | const Y4 = Matrix.rand(1000, 1000); 94 | const Z4 = crossprod(X4, Y4); 95 | testMatrixStructure(Z4, 1000, 1000); 96 | 97 | // large matrices - nrows > ncols 98 | const X5 = Matrix.rand(2000, 500); 99 | const Y5 = Matrix.rand(2000, 500); 100 | const Z5 = crossprod(X5, Y5); 101 | testMatrixStructure(Z5, 500, 500); 102 | 103 | // large matrices - ncols < nrows 104 | const X6 = Matrix.rand(500, 2000); 105 | const Y6 = Matrix.rand(500, 2000); 106 | const Z6 = crossprod(X6, Y6); 107 | testMatrixStructure(Z6, 2000, 2000); 108 | 109 | // large matrices - same matrix 110 | const Z6a = crossprod(X6); 111 | testMatrixStructure(Z6a, 2000, 2000); 112 | 113 | }).timeout(10000); 114 | 115 | it ('tests for method "rbind"', function() { 116 | 117 | const X4 = Matrix.rand(5000, 5000); 118 | const X5 = Matrix.rand(5000, 5000); 119 | const X6 = Matrix.rand(5000, 5000); 120 | 121 | const R3 = rbind(X4, X5, X6); 122 | testMatrixStructure(R3, 15000, 5000); 123 | 124 | }).timeout(10000); 125 | 126 | it ('tests for method "cbind"', function() { 127 | 128 | const X4 = Matrix.rand(5000, 5000); 129 | const X5 = Matrix.rand(5000, 5000); 130 | const X6 = Matrix.rand(5000, 5000); 131 | 132 | const R3 = cbind(X4, X5, X6); 133 | testMatrixStructure(R3, 5000, 15000); 134 | 135 | }).timeout(10000); 136 | 137 | it ('tests for method "dot"', function() { 138 | 139 | // large matrices - squared 140 | const X3 = Matrix.rand(2000, 2000); 141 | const Y3 = Matrix.rand(2000, 2000); 142 | const Z3 = X3.dot(Y3); 143 | testMatrixStructure(Z3, 2000, 2000); 144 | 145 | // large matrices - nrows > ncols 146 | const X4 = Matrix.rand(2000, 500); 147 | const Y4 = Matrix.rand(500, 2000); 148 | const Z4 = X4.dot(Y4); 149 | testMatrixStructure(Z4, 2000, 2000); 150 | 151 | // large matrices - ncols < nrows 152 | const X5 = Matrix.rand(500, 2000); 153 | const Y5 = Matrix.rand(2000, 500); 154 | const Z5 = X5.dot(Y5); 155 | testMatrixStructure(Z5, 500, 500); 156 | 157 | }).timeout(20000); 158 | 159 | it ('tests for method "mult"', function() { 160 | 161 | // difference two large matrices 162 | const L1 = Matrix.rand(10000, 10000); 163 | const L2 = Matrix.rand(10000, 10000); 164 | const L3 = L1.mult(L2); 165 | expect(L3.v[100]).to.be.almost.equal(L1.v[100] * L2.v[100]); 166 | }).timeout(10000); 167 | 168 | it ('tests for method "divide"', function() { 169 | 170 | // difference two large matrices 171 | const L1 = Matrix.rand(10000, 10000); 172 | const L2 = Matrix.rand(10000, 10000); 173 | const L3 = L1.divide(L2); 174 | expect(L3.v[100]).to.be.almost.equal(L1.v[100] / L2.v[100]); 175 | }).timeout(10000); 176 | 177 | it ('tests for method "subtract"', function() { 178 | 179 | // difference two large matrices 180 | const L1 = Matrix.rand(10000, 10000); 181 | const L2 = Matrix.rand(10000, 10000); 182 | const L3 = L1.subtract(L2); 183 | expect(L3.v[100]).to.be.almost.equal(L1.v[100] - L2.v[100]); 184 | 185 | }).timeout(10000); 186 | 187 | it ('tests for method "add"', function() { 188 | 189 | // sum of two large matrices 190 | const L1 = Matrix.rand(10000, 10000); 191 | const L2 = Matrix.rand(10000, 10000); 192 | const L3 = L1.add(L2); 193 | expect(L3.v[100]).to.be.almost.equal(L1.v[100] + L2.v[100]); 194 | }).timeout(10000); 195 | 196 | it ('tests for method "t"', function() { 197 | 198 | // large matrix - squared 199 | const X1 = Matrix.rand(10000, 10000); 200 | 201 | const X1t1 = X1.t(); 202 | expect(X1t1.v).to.not.deep.equal(X1.v); 203 | expect(X1t1.nrows).to.be.equal(10000); 204 | expect(X1t1.ncols).to.be.equal(10000); 205 | 206 | const X1t2 = X1t1.t(); 207 | expect(X1t2.v).to.deep.equal(X1.v); 208 | expect(X1t2.nrows).to.be.equal(X1.nrows); 209 | expect(X1t2.ncols).to.be.equal(X1.ncols); 210 | 211 | // large matrix - nrows > ncols 212 | const X2 = Matrix.rand(10000, 5000); 213 | 214 | const X2t1 = X2.t(); 215 | expect(X2t1.v).to.not.deep.equal(X2.v); 216 | expect(X2t1.nrows).to.be.equal(5000) 217 | expect(X2t1.ncols).to.be.equal(10000); 218 | 219 | const X2t2 = X2t1.t(); 220 | expect(X2t2.v).to.deep.equal(X2.v); 221 | expect(X2t2.nrows).to.be.equal(X2.nrows); 222 | expect(X2t2.ncols).to.be.equal(X2.ncols); 223 | 224 | // large matrix - ncols > nrows 225 | const X3 = Matrix.rand(5000, 10000); 226 | 227 | const X3t1 = X3.t(); 228 | expect(X3t1.v).to.not.deep.equal(X3.v); 229 | expect(X3t1.nrows).to.be.equal(10000); 230 | expect(X3t1.ncols).to.be.equal(5000); 231 | 232 | const X3t2 = X3t1.t(); 233 | expect(X3t2.v).to.deep.equal(X3.v); 234 | expect(X3t2.nrows).to.be.equal(X3.nrows); 235 | expect(X3t2.ncols).to.be.equal(X3.ncols); 236 | 237 | }).timeout(10000); 238 | 239 | }); 240 | 241 | -------------------------------------------------------------------------------- /test/tests-decomp.js: -------------------------------------------------------------------------------- 1 | /***************************************************************** 2 | * Tests for methods for decompositions of a matrix * 3 | *****************************************************************/ 4 | 5 | // Tests of randomized SVD decomposition. 6 | // ✓ rsvd() works correctly for m > n. (7789ms) 7 | 8 | // Tests of LU and QR decompositions. 9 | // ✓ qr() works correctly. (4100ms) 10 | 11 | // Tests of SVD decomposition and related methods. 12 | // ✓ svd() works correctly. (16686ms) 13 | // ✓ rot() works correctly. 14 | // ✓ tests for method "bidiag" (1645ms) 15 | // ✓ tests for method "householder" (49ms) 16 | 17 | 18 | // import dependencies 19 | import {default as chai} from 'chai'; 20 | import {default as chaiAlmost} from 'chai-almost'; 21 | import { crossprod, tcrossprod, reshape, isvector, vector, Vector, 22 | ismatrix, matrix, Matrix, Index } from '../src/arrays/index.js'; 23 | 24 | // import methods to test 25 | import { rsvd, qr, lu, svd, rot, bidiag, householder } from '../src/decomp/index.js'; 26 | 27 | // set up test settings 28 | const expect = chai.expect; 29 | chai.use(chaiAlmost(0.0001)); 30 | 31 | const ZERO = Math.pow(10.0, -6); 32 | 33 | // function to test a matrix structure 34 | function testMatrixStructure(X, nr, nc, values) { 35 | expect(X.constructor).equal(Matrix); 36 | expect(X.v.constructor).equal(Float64Array); 37 | expect(X.v.length).equal(nr * nc); 38 | expect(X.nrows).equal(nr); 39 | expect(X.ncols).equal(nc); 40 | expect(ismatrix(X)).to.be.true; 41 | expect(isvector(X)).to.be.false; 42 | 43 | if (values) { 44 | expect(X.v).to.deep.almost.equal(new Float64Array(values)); 45 | } 46 | } 47 | 48 | // computes sum of squared differences between two matrices 49 | function ssqdiff(A, B) { 50 | return A.subtract(B).v.map(v => v ** 2).reduce((p, s) => p + s); 51 | } 52 | 53 | // test SVD results 54 | function svdtests(X, r, ncomp) { 55 | // checks that: 56 | // - V and Y and s have proper dimensions 57 | // - V and U has orthogonal columns 58 | // - (T = XV) == (US) 59 | // - X ≈ TV' 60 | 61 | const m = X.nrows; 62 | const n = X.ncols; 63 | 64 | if (ncomp === undefined) { 65 | ncomp = Math.min(m, n); 66 | } 67 | 68 | // check size of U and V 69 | testMatrixStructure(r.U, m, ncomp); 70 | testMatrixStructure(r.V, n, ncomp); 71 | 72 | // check orthogonality of V and U 73 | const A = r.s.v.filter(v => v > Math.pow(10,-12)).length; 74 | const ind = Index.seq(1, A); 75 | expect(crossprod(r.V.subset([], ind)).apply(Math.abs, 0)).to.be.deep.almost.equal(Matrix.eye(A)); 76 | expect(crossprod(r.U.subset([], ind)).apply(Math.abs, 0)).to.be.deep.almost.equal(Matrix.eye(A)); 77 | 78 | // check that T = US 79 | const T = X.dot(r.V); 80 | const US = r.U.dot(Matrix.diagm(r.s)); 81 | expect(T).to.be.deep.almost.equal(US); 82 | 83 | // check that X ≈ USV 84 | const R = tcrossprod(r.U.dot(Matrix.diagm(r.s)), r.V); 85 | expect(X.v).to.be.deep.almost.equal(R.v); 86 | } 87 | 88 | // create simulated dataset as a linear combination of 6 sinus curves 89 | function simdata(m, n) { 90 | let x = new Float64Array(n); 91 | x = x.map((v, i) => i + 1.); 92 | const s1 = x.map(v => 9. * Math.sin(v)); 93 | const s2 = x.map(v => 8. * Math.sin(v / 2.)); 94 | const s3 = x.map(v => 7. * Math.sin(v / 3.)); 95 | const s4 = x.map(v => 6. * Math.sin(v / 4.)); 96 | const s5 = x.map(v => 5. * Math.sin(v / 5.)); 97 | const s6 = x.map(v => 4. * Math.sin(v / 6.)); 98 | const S = new Matrix(new Float64Array([...s1, ...s2, ...s3, ...s4, ...s5, ...s6]), n, 6); 99 | 100 | const C = Matrix.rand(m, 6); 101 | return tcrossprod(C, S); 102 | } 103 | 104 | 105 | describe('Tests for matrix decompositions.', function () { 106 | 107 | it('tests for method "rsvd".', function() { 108 | 109 | // simple example to compare with R results 110 | const A0a = matrix([1, 3, 17, 19, 10, 14, 7, 13, 9, 11, 2, 15, 8, 6, 4, 12, 22, 18], 6, 3); 111 | const r0a = rsvd(A0a, 3); 112 | svdtests(A0a, r0a) 113 | 114 | expect(r0a.s).to.be.deep.almost.equal(vector([ 115 | 47.82344, 15.49189, 12.07974 116 | ])); 117 | 118 | expect(r0a.V.t().apply(x => Math.abs(x), 0).v).to.be.deep.almost.equal(new Float64Array([ 119 | 0.6074766, 0.4602873, 0.64738, 120 | 0.4757956, 0.4417727, 0.76056, 121 | 0.6360745, 0.7700471, 0.04936 122 | ])); 123 | 124 | expect(r0a.U.t().apply(x => Math.abs(x), 0).v).to.be.deep.almost.equal(new Float64Array([ 125 | 0.1887493, 0.16832558, 0.4198321, 126 | 0.2472474, 0.16160874, 0.6822448, 127 | 0.3586852, 0.56291730, 0.3280726, 128 | 0.5103920, 0.28172128, 0.2766423, 129 | 0.4395333, 0.73939460, 0.3200999, 130 | 0.5664784, 0.05100958, 0.2676873 131 | ])); 132 | 133 | // simple example to compare with R results 134 | const A0b = matrix([1, 3, 17, 19, 10, 14, 7, 13, 9, 11, 2, 15, 8, 6, 4, 12, 22, 18, 11, 12], 5, 4); 135 | const r0b = rsvd(A0b, 4); 136 | svdtests(A0b, r0b) 137 | 138 | expect(r0b.s).to.be.deep.almost.equal(vector([ 139 | 50.4985905, 16.4651258, 10.7821742, 0.7326014 140 | ])); 141 | 142 | // simple example to compare with R results 143 | const A0c = reshape(Vector.seq(1, 20), 5, 4); 144 | const r0c = rsvd(A0c, 4); 145 | svdtests(A0c, r0c) 146 | 147 | expect(r0c.s).to.be.deep.almost.equal(vector([ 148 | 53.49077, 2.955910, 0, 0 149 | ])); 150 | 151 | // simulated data m > n 152 | const A1 = simdata(1000, 100) 153 | const r1a = rsvd(A1, 70) 154 | svdtests(A1, r1a, 70) 155 | const r1b = rsvd(A1, 10) 156 | svdtests(A1, r1b, 10) 157 | 158 | // simulated data m < n 159 | const A2 = simdata(100, 1000) 160 | const r2a = rsvd(A2, 70) 161 | svdtests(A2, r2a, 70) 162 | const r2b = rsvd(A2, 10) 163 | svdtests(A2, r2b, 10) 164 | 165 | // simulated data m = n 166 | const A3 = simdata(100, 100) 167 | const r3a = rsvd(A3, 70) 168 | svdtests(A3, r3a, 70) 169 | const r3b = rsvd(A3, 10) 170 | svdtests(A3, r3b, 10) 171 | 172 | }).timeout(200000); 173 | 174 | it('tests for method "qr.', function() { 175 | 176 | // test function, checks that: 177 | // - Q and R has proper dimensions 178 | // - Q has orthogonal columns 179 | // - R is upper triangular 180 | // - X ≈ QR 181 | function qrtests(X, r) { 182 | 183 | const m = X.nrows; 184 | const n = X.ncols; 185 | 186 | if (m > n) { 187 | testMatrixStructure(r.Q, m, n) 188 | testMatrixStructure(r.R, n, n) 189 | expect(crossprod(r.Q).v).to.be.deep.almost.equal(Matrix.eye(n).v); 190 | expect(r.R.isuppertriangular()).to.be.true; 191 | } else { 192 | testMatrixStructure(r.Q, m, m) 193 | testMatrixStructure(r.R, m, n) 194 | expect(crossprod(r.Q).v).to.be.deep.almost.equal(Matrix.eye(m).v); 195 | } 196 | expect(ssqdiff(X, r.Q.dot(r.R)) < ZERO).to.be.true; 197 | } 198 | 199 | // small squared matrix 200 | const X1 = matrix([4, 6, 3, 3], 2, 2); 201 | const r1 = qr(X1); 202 | qrtests(X1, r1); 203 | 204 | // small matrix m > n 205 | const X2a = matrix([4, 5, 6, 3, 3, 3], 3, 2); 206 | const r2a = qr(X2a); 207 | qrtests(X2a, r2a); 208 | 209 | // small matrix n > m 210 | const X2b = matrix([4, 5, 6, 3, 3, 3], 2, 3); 211 | const r2b = qr(X2b); 212 | qrtests(X2b, r2b); 213 | 214 | // small matrix from rsvd example 215 | const X2c = matrix([ 216 | 20.248701174781164, 22.01952684298161, 217 | 23.790352511182057, 25.5611781793825, 218 | 27.332003847582946, -1.9975236505272767, 219 | -1.0679127357664608, -0.13830182100564326, 220 | 0.7913090937551726, 1.7209200085159875, 221 | 18.675243127797636, 20.372992503051968, 222 | 22.0707418783063, 23.768491253560633, 223 | 25.466240628814965, 7.761140001162658, 224 | 7.8419852095081035, 7.922830417853547, 225 | 8.00367562619899, 8.084520834544435 226 | ], 5, 4); 227 | 228 | const r2c = qr(X2c); 229 | qrtests(X2c, r2c); 230 | 231 | 232 | // small matrix from paper describing algoritm 233 | // https://www.math.usm.edu/lambers/mat610/sum10/lecture9.pdf 234 | const X2d = matrix([ 235 | 0.8147, 0.9058, 0.1270, 0.9134, 0.6324, 236 | 0.0975, 0.2785, 0.5469, 0.9575, 0.9649, 237 | 0.1576, 0.9706, 0.9572, 0.4854, 0.8003 238 | ], 5, 3) 239 | 240 | const r2d = qr(X2d); 241 | qrtests(X2a, r2a); 242 | 243 | expect(r2d.Q.v).to.be.deep.almost.eql(new Float64Array([ 244 | 0.4927, 0.5478, 0.0768, 0.5524, 0.3824, 245 | -0.4807, -0.3583, 0.4754, 0.3391, 0.5473, 246 | 0.1780, -0.5777, -0.6343, 0.4808, 0.0311 247 | // -0.7033, 0.4825, -0.4317, 0.2769, -0.0983, 248 | // 0.0000, 0.0706, -0.4235, -0.5216, 0.7373 249 | ])); 250 | 251 | expect(r2d.R.v).to.be.deep.almost(new Float64Array([ 252 | 1.6536, 0.0000, 0.0000, 253 | 1.1405, 0.9661, 0.0000, 254 | 1.2569, 0.6341, -0.8816 255 | ])); 256 | 257 | 258 | // middle size random matrix m = n 259 | const n3 = 150 260 | const m3 = 150 261 | const X3 = Matrix.rand(m3, n3); 262 | const r3 = qr(X3); 263 | qrtests(X3, r3) 264 | 265 | // large size random matrix m > n 266 | const n4 = 500 267 | const m4 = 1000 268 | const X4 = Matrix.rand(m4, n4); 269 | const r4 = qr(X4); 270 | qrtests(X4, r4) 271 | 272 | // large size random matrix m < n 273 | const n5 = 1000 274 | const m5 = 500 275 | const X5 = Matrix.rand(m5, n5); 276 | const r5 = qr(X5); 277 | qrtests(X5, r5) 278 | 279 | }).timeout(20000); 280 | 281 | it('tests for method "lu".', function() { 282 | const X1 = matrix([4, 6, 3, 3], 2, 2); 283 | const r1 = lu(X1); 284 | // check that U is upper triangular 285 | expect(r1.U.isuppertriangular()).to.be.true; 286 | // check that L is lower triangular 287 | expect(r1.L.islowertriangular()).to.be.true; 288 | // check that X ≈ LU 289 | expect(ssqdiff(X1, r1.L.dot(r1.U)) < ZERO).to.be.true; 290 | 291 | const X2 = matrix([8, 4, 6, 2, 9, 7, 9, 4, 9], 3, 3); 292 | const r2 = lu(X2); 293 | // check that U is upper triangular 294 | expect(r2.U.isuppertriangular()).to.be.true; 295 | // check that L is lower triangular 296 | expect(r2.L.islowertriangular()).to.be.true; 297 | // check that X ≈ LU 298 | expect(ssqdiff(X2, r2.L.dot(r2.U)) < ZERO).to.be.true; 299 | 300 | const X3 = Matrix.rand(1000, 1000); 301 | const r3 = lu(X3); 302 | // check that U is upper triangular 303 | expect(r3.U.isuppertriangular()).to.be.true; 304 | // check that L is lower triangular 305 | expect(r3.L.islowertriangular()).to.be.true; 306 | // check that X ≈ LU 307 | expect(ssqdiff(X3, r3.L.dot(r3.U)) < ZERO).to.be.true; 308 | 309 | const X4 = Matrix.rand(1000, 500); 310 | const r4 = lu(X4); 311 | // check that X ≈ LU 312 | expect(ssqdiff(X4, r4.L.dot(r4.U)) < ZERO).to.be.true; 313 | 314 | const X5 = Matrix.rand(500, 1000); 315 | const r5 = lu(X5); 316 | // check that X ≈ LU 317 | expect(ssqdiff(X5, r5.L.dot(r5.U)) < ZERO).to.be.true; 318 | 319 | }).timeout(20000); 320 | 321 | it('tests for method "svd".', function() { 322 | 323 | // simple example to compare with R results 324 | const A0a = matrix([1, 3, 17, 19, 10, 14, 7, 13, 9, 11, 2, 15, 8, 6, 4, 12, 22, 18], 6, 3); 325 | const r0a = svd(A0a); 326 | svdtests(A0a, r0a) 327 | 328 | expect(r0a.s).to.be.deep.almost.equal(vector([ 329 | 47.82344, 15.49189, 12.07974 330 | ])); 331 | 332 | expect(r0a.V.t().apply(x => Math.abs(x), 0).v).to.be.deep.almost.equal(new Float64Array([ 333 | 0.6074766, 0.4602873, 0.64738, 334 | 0.4757956, 0.4417727, 0.76056, 335 | 0.6360745, 0.7700471, 0.04936 336 | ])); 337 | 338 | expect(r0a.U.t().apply(x => Math.abs(x), 0).v).to.be.deep.almost.equal(new Float64Array([ 339 | 0.1887493, 0.16832558, 0.4198321, 340 | 0.2472474, 0.16160874, 0.6822448, 341 | 0.3586852, 0.56291730, 0.3280726, 342 | 0.5103920, 0.28172128, 0.2766423, 343 | 0.4395333, 0.73939460, 0.3200999, 344 | 0.5664784, 0.05100958, 0.2676873 345 | ])); 346 | 347 | 348 | // simple example to compare with R results 349 | const A0b = matrix([1, 3, 17, 19, 10, 14, 7, 13, 9, 11, 2, 15, 8, 6, 4, 12, 22, 18, 11, 12], 5, 4); 350 | const r0b = svd(A0b); 351 | svdtests(A0b, r0b) 352 | 353 | expect(r0b.s).to.be.deep.almost.equal(vector([ 354 | 50.4985905, 16.4651258, 10.7821742, 0.7326014 355 | ])); 356 | 357 | // simple example to compare with R results 358 | const A0c = reshape(Vector.seq(1, 20), 5, 4); 359 | const r0c = svd(A0c); 360 | svdtests(A0c, r0c) 361 | 362 | expect(r0c.s).to.be.deep.almost.equal(vector([ 363 | 53.49077, 2.955910, 0, 0 364 | ])); 365 | 366 | // simulated data m > n 367 | const A1 = simdata(20, 10) 368 | const r1a = svd(A1) 369 | svdtests(A1, r1a) 370 | const r1b = svd(A1, 10) 371 | svdtests(A1, r1b, 10) 372 | 373 | // simulated data m < n 374 | const A2 = simdata(10, 20) 375 | const r2a = svd(A2) 376 | svdtests(A2, r2a) 377 | const r2b = svd(A2, 10) 378 | svdtests(A2, r2b, 10) 379 | 380 | // simulated data m = n 381 | const A3 = simdata(20, 20) 382 | const r3a = svd(A3) 383 | svdtests(A3, r3a) 384 | const r3b = svd(A3, 20) 385 | svdtests(A3, r3b, 20) 386 | 387 | }).timeout(10000000); 388 | 389 | it('tests for method "rot".', function () { 390 | expect(rot(1, 0)).to.be.deep.almost([1, 0, 1]); 391 | 392 | const r1 = rot(1, 2); 393 | const r12 = matrix([r1[0], -r1[1], r1[1], r1[0]], 2, 2).dot(matrix([1, 2], 2, 1)); 394 | expect(r12.v).to.be.deep.almost(new Float64Array([r1[2], 0])); 395 | 396 | const r2 = rot(0, 1); 397 | const r22 = matrix([r2[0], -r2[1], r2[1], r2[0]], 2, 2).dot(matrix([0, 1], 2, 1)); 398 | expect(r22.v).to.be.deep.almost(new Float64Array([r2[2], 0])); 399 | }); 400 | 401 | it ('tests for method "bidiag"', function () { 402 | function getB(d, e, m, n) { 403 | const B = Matrix.zeros(m, n); 404 | for (let i = 0; i < n - 1; i++) { 405 | B.v[i * m + i] = d.v[i]; 406 | B.v[(i + 1) * m + i] = e.v[i]; 407 | } 408 | B.v[(n - 1) * m + (n - 1)] = d.v[(n - 1)]; 409 | return B; 410 | } 411 | 412 | const A1 = reshape(Vector.seq(1, 12), 4, 3); 413 | const [d1, e1, V1, U1] = bidiag(A1); 414 | const B1 = getB(d1, e1, 4, 3); 415 | const R1 = U1.dot(tcrossprod(B1, V1)); 416 | expect(R1.v).to.be.deep.almost.equal(A1.v); 417 | 418 | // best time so far 1.42 for 201 x 200 419 | const A2 = Matrix.rand(201, 200); 420 | const [d2, e2, V2, U2] = bidiag(A2); 421 | const B2 = getB(d2, e2, 201, 200); 422 | const R2 = U2.dot(tcrossprod(B2, V2)); 423 | expect(R2.v).to.be.deep.almost.equal(A2.v); 424 | 425 | }).timeout(20000); 426 | 427 | it ('tests for method "householder"', function() { 428 | const H1 = householder([1, 2, 3], 2); 429 | testMatrixStructure(H1, 2, 2, [ 430 | 0.5547002, 0.8320503, 431 | 0.8320503, -0.5547002 432 | ]); 433 | 434 | const H2 = householder([1, 2, 3], 1); 435 | testMatrixStructure(H2, 3, 3, [ 436 | 0.2672612, 0.5345225, 0.8017837, 437 | 0.5345225, 0.6100735, -0.5848898, 438 | 0.8017837, -0.5848898, 0.1226653 439 | ]); 440 | 441 | const H3 = householder(Vector.seq(1, 10000).v, 5000); 442 | testMatrixStructure(H3, 5001, 5001); 443 | }); 444 | 445 | }); -------------------------------------------------------------------------------- /test/tests-distributions.js: -------------------------------------------------------------------------------- 1 | // import dependencies 2 | import {default as chai} from 'chai'; 3 | import {sum, sd, mean, min, max} from '../src/stat/index.js'; 4 | import { isvector, vector, Vector } from '../src/arrays/index.js'; 5 | 6 | // import of functions to test 7 | import {igamma, gamma, beta, runif, dunif, punif, rnorm, dnorm, pnorm, qnorm, 8 | dt, pt, qt, df, pf, qf, pchisq, qchisq} from '../src/distributions/index.js'; 9 | 10 | const should = chai.should(); 11 | const expect = chai.expect; 12 | 13 | describe('Tests for helper functions.', function () { 14 | 15 | it ('tests for "igamma" function.', function () { 16 | igamma(1, 1).should.be.closeTo(0.632120, 0.00001) 17 | igamma(1, 2).should.be.closeTo(0.264241, 0.00001) 18 | igamma(2, 1).should.be.closeTo(0.864665, 0.00001) 19 | igamma(2, 2).should.be.closeTo(0.593994, 0.00001) 20 | 21 | igamma( 5, 5).should.be.closeTo(13.42816, 0.00001) 22 | igamma( 5, 10).should.be.closeTo(11549.765, 0.001) 23 | igamma(10, 5).should.be.closeTo(23.29793, 0.00001) 24 | igamma(10, 10).should.be.closeTo(196706.465212, 0.00001) 25 | }); 26 | 27 | it ('tests for "gamma" function.', function () { 28 | expect(() => gamma(-1)).to.throw(Error, "gamma: the function only works with arguments > 0."); 29 | expect(() => gamma(0)).to.throw(Error, "gamma: the function only works with arguments > 0."); 30 | gamma(1).should.be.closeTo(1, 0.000001); 31 | gamma(4).should.be.closeTo(6, 0.000001); 32 | gamma(0.5).should.be.closeTo(1.772454, 0.000001); 33 | gamma(1.5).should.be.closeTo(0.8862269, 0.000001); 34 | 35 | expect(() => gamma(vector([1, 2, -1, 3]))).to.throw(Error, "gamma: the function only works with arguments > 0."); 36 | expect(() => gamma(vector([1, 2, 0, 3]))).to.throw(Error, "gamma: the function only works with arguments > 0."); 37 | const g = gamma(vector([1, 4, 0.5, 1.5])); 38 | g.v[0].should.be.closeTo(1, 0.000001); 39 | g.v[1].should.be.closeTo(6, 0.000001); 40 | g.v[2].should.be.closeTo(1.772454, 0.000001); 41 | g.v[3].should.be.closeTo(0.8862269, 0.000001); 42 | }); 43 | 44 | it ('tests for "beta".', function () { 45 | beta(1, 1).should.be.closeTo(1, 0.000001); 46 | beta(0.5, 0.5).should.be.closeTo(3.141593, 0.000001); 47 | beta(0.5, 1.5).should.be.closeTo(1.570796, 0.000001); 48 | }); 49 | 50 | }); 51 | 52 | describe('Tests for theoretical distribution functions.', function () { 53 | 54 | it ('tests for method "dunif".', function () { 55 | 56 | const n = 1000000; 57 | 58 | // standardized distribution for a = 0, b = 1 59 | const x1 = Vector.seq(0, 1, 1/(n - 1)); 60 | const d1 = dunif(x1); 61 | 62 | expect(d1.v).to.have.lengthOf(n); 63 | d1.v[0].should.be.closeTo(1, 0.0000000001); 64 | d1.v[n-1].should.be.closeTo(1, 0.0000000001); 65 | d1.v[n/2].should.be.closeTo(1, 0.0000000001); 66 | 67 | // distribution with mu = 10 and sigma = 10, for ± 3 sigma 68 | const a = 10; 69 | const b = 100; 70 | const x2 = Vector.seq(a, b, (b - a) / (n - 1)); 71 | const d2 = dunif(x2, a, b); 72 | 73 | expect(d2.v).to.have.lengthOf(n); 74 | d2.v[0].should.be.closeTo(1 / (b - a), 0.00000001); 75 | d2.v[n-1].should.be.closeTo(1 / (b - a), 0.00000001); 76 | d2.v[n/2].should.be.closeTo(1 / (b - a), 0.00000001); 77 | (sum(d2) * (b - a)/n).should.be.closeTo(1.0, 0.000001); 78 | 79 | dunif(a - 0.0000001, a, b).should.be.closeTo(0.0, 0.0000001); 80 | dunif(b + 0.0000001, a, b).should.be.closeTo(0.0, 0.0000001); 81 | }); 82 | 83 | it('tests for method "punif".', function () { 84 | 85 | const n = 1000000; 86 | 87 | // standardized distribution for a = 0, b = 1 88 | const x1 = Vector.seq(0, 1, 1 / (n - 1)); 89 | const p1 = punif(x1); 90 | 91 | expect(p1.v).to.have.lengthOf(n); 92 | p1.v[0].should.be.closeTo(0, 0.00001); 93 | p1.v[n-1].should.be.closeTo(1, 0.00001); 94 | p1.v[n/2].should.be.closeTo(0.5, 0.00001); 95 | 96 | // outside the range 97 | punif(-1).should.be.closeTo(0, 0.00001); 98 | punif( 1).should.be.closeTo(1, 0.00001); 99 | 100 | // distribution with mu = 10 and sigma = 10, for ± 3 sigma 101 | const a = 10; 102 | const b = 100; 103 | const x2 = Vector.seq(a, b, (b - a) / (n - 1)); 104 | const p2 = punif(x2, a, b); 105 | 106 | expect(p2.v).to.have.lengthOf(n); 107 | punif(a - 1, a, b).should.be.closeTo(0.0, 0.00001); 108 | punif(b + 1, a, b).should.be.closeTo(1.0, 0.00001); 109 | }); 110 | 111 | it('tests for method "runif".', function () { 112 | 113 | const n = 1000000; 114 | const r1 = runif(n); 115 | 116 | expect(r1.v).to.have.lengthOf(n) 117 | expect(min(r1)).to.be.above(0); 118 | expect(max(r1)).to.be.below(1); 119 | 120 | const r2 = runif(n, 10, 20); 121 | expect(r2.v).to.have.lengthOf(n) 122 | expect(min(r2)).to.be.above(10); 123 | expect(max(r2)).to.be.below(20); 124 | }); 125 | 126 | it ('tests for method "dnorm".', function () { 127 | 128 | const n = 1000000; 129 | 130 | // standardized distribution for ± 3 sigma 131 | const x1 = Vector.seq(-3, 3, 6 / (n - 1)); 132 | const d1 = dnorm(x1); 133 | expect(d1.v).to.have.lengthOf(n); 134 | d1.v[0].should.be.closeTo(0.004431848, 0.00000001); 135 | d1.v[n-1].should.be.closeTo(0.004431848, 0.00000001); 136 | d1.v[n/2].should.be.closeTo(0.3989423, 0.0000001); 137 | 138 | // distribution with mu = 10 and sigma = 10, for ± 3 sigma 139 | const mu = 10; 140 | const sigma = 10 141 | const x2 = Vector.seq(mu - 3 * sigma, mu + 3 * sigma, 6 * sigma / (n - 1)); 142 | const d2 = dnorm(x2, mu, sigma); 143 | expect(d2.v).to.have.lengthOf(n); 144 | d2.v[0].should.be.closeTo(0.0004431848, 0.00000001); 145 | d2.v[n-1].should.be.closeTo(0.0004431848, 0.00000001); 146 | d2.v[n/2].should.be.closeTo(0.03989423, 0.0000001); 147 | 148 | // distribution with mu = 10 and sigma = 10, for ± 6 sigma should have area of one 149 | const x3 = Vector.seq(mu - 6 * sigma, mu + 6 * sigma, 12 * sigma / (n - 1)); 150 | const d3 = dnorm(x3, mu, sigma); 151 | expect(d3.v).to.have.lengthOf(n); 152 | (sum(d3) * 12 * sigma / n).should.be.closeTo(1.0, 0.00001); 153 | 154 | // if values are far from mean density is 0 155 | dnorm(mu - 6 * sigma, mu, sigma).should.be.closeTo(0.0, 0.0000001); 156 | dnorm(mu + 6 * sigma, mu, sigma).should.be.closeTo(0.0, 0.0000001); 157 | }); 158 | 159 | it ('tests for method "pnorm".', function () { 160 | 161 | const n = 1000000; 162 | 163 | // standardized distribution for ± 3 sigma 164 | const x1 = Vector.seq(-3, 3, 6 / (n - 1)); 165 | const p1 = pnorm(x1); 166 | 167 | expect(p1.v).to.have.lengthOf(n); 168 | p1.v[ 0].should.be.closeTo(0.00134996, 0.00001); 169 | p1.v[n-1].should.be.closeTo(0.998650, 0.00001); 170 | p1.v[n/2].should.be.closeTo(0.5, 0.00001); 171 | 172 | // distribution with mu = 10 and sigma = 10, for ± 3 sigma 173 | const mu = 10; 174 | const sigma = 10 175 | const x2 = Vector.seq(mu - 3 * sigma, mu + 3 * sigma, 6 * sigma / (n - 1)); 176 | const p2 = pnorm(x2, mu, sigma); 177 | expect(p2.v).to.have.lengthOf(n); 178 | p2.v[ 0].should.be.closeTo(0.001350, 0.000001); 179 | p2.v[n-1].should.be.closeTo(0.998650, 0.000001); 180 | p2.v[n/2].should.be.closeTo(0.5, 0.00001); 181 | 182 | }); 183 | 184 | it ('tests for method "qnorm".', function () { 185 | 186 | const n = 1000000; 187 | 188 | // border cases 189 | qnorm(0).should.be.equal(-Infinity); 190 | qnorm(1).should.be.equal(Infinity); 191 | qnorm(vector([0, 0, 1, 1])).should.be.eql(vector([-Infinity, -Infinity, Infinity, Infinity])); 192 | 193 | // middle point and border cases 194 | qnorm(0.5).should.be.equal(0); 195 | qnorm(vector([0.5, 0.5])).should.be.eql(vector([0, 0])); 196 | qnorm(vector([0, 0.5, 1])).should.be.eql(vector([-Infinity, 0, Infinity])); 197 | 198 | // other cases 199 | qnorm(0.9999).should.be.closeTo( 3.719016, 0.00001); 200 | qnorm(0.0001).should.be.closeTo(-3.719016, 0.00001); 201 | 202 | qnorm(0.975).should.be.closeTo( 1.959964, 0.00001); 203 | qnorm(0.025).should.be.closeTo(-1.959964, 0.00001); 204 | 205 | qnorm(0.840).should.be.closeTo( 0.9944579, 0.00001); 206 | qnorm(0.160).should.be.closeTo(-0.9944579, 0.00001); 207 | 208 | qnorm(0.750).should.be.closeTo( 0.6744898, 0.00001); 209 | qnorm(0.250).should.be.closeTo(-0.6744898, 0.00001); 210 | 211 | // cases with non standard distribution 212 | qnorm(0.975, 10, 2).should.be.closeTo(13.91993, 0.00001); 213 | qnorm(0.025, 10, 2).should.be.closeTo( 6.080072, 0.00001); 214 | 215 | // errors 216 | expect(() => qnorm(-0.0001)).to.throw(Error, 'Parameter "p" must be between 0 and 1.'); 217 | expect(() => qnorm( 1.0001)).to.throw(Error, 'Parameter "p" must be between 0 and 1.'); 218 | 219 | // long vectors 220 | const p = Vector.seq(0.0001, 0.9999, 0.9998 / (n - 1)); 221 | const q = qnorm(p); 222 | expect(q.v).to.have.lengthOf(n); 223 | q.v[0].should.be.equal(qnorm(0.0001)); 224 | q.v[n-1].should.be.closeTo(qnorm(0.9999), 0.000000001); 225 | }); 226 | 227 | it('tests for method "rnorm".', function () { 228 | const n = 10000000; 229 | 230 | const r1 = rnorm(n); 231 | expect(r1.v).to.have.lengthOf(n) 232 | sd(r1).should.be.closeTo(1, 0.01); 233 | mean(r1).should.be.closeTo(0, 0.01); 234 | min(r1).should.be.above(-6); 235 | max(r1).should.be.below(6); 236 | 237 | const r2 = rnorm(n, 10, 5); 238 | expect(r2.v).to.have.lengthOf(n) 239 | sd(r2).should.be.closeTo(5, 0.01); 240 | mean(r2).should.be.closeTo(10, 0.01); 241 | min(r2).should.be.above(-20); 242 | max(r2).should.be.below(40); 243 | }); 244 | 245 | it ('tests for method "dt".', function () { 246 | 247 | const n = 100000; 248 | 249 | // distribution for DoF = 1 250 | const x1 = Vector.seq(-5, 5, 10/(n - 1)); 251 | const d1 = dt(x1, 1); 252 | expect(d1.v).to.have.lengthOf(n); 253 | d1.v[0].should.be.closeTo(0.01224269, 0.00000001); 254 | d1.v[n-1].should.be.closeTo(0.01224269, 0.00000001); 255 | d1.v[n/2].should.be.closeTo(0.31830989, 0.0000001); 256 | 257 | // distribution for DoF = 3 258 | const x2 = Vector.seq(-5, 5, 10/(n - 1)); 259 | const d2 = dt(x2, 3); 260 | expect(d2.v).to.have.lengthOf(n); 261 | d2.v[0].should.be.closeTo(0.004219354, 0.00000001); 262 | d2.v[n-1].should.be.closeTo(0.004219354, 0.00000001); 263 | d2.v[n/2].should.be.closeTo(0.3675526, 0.0000001); 264 | 265 | // distribution for DoF = 30 266 | const x3 = Vector.seq(-3, 3, 6/(n - 1)); 267 | const d3 = dt(x3, 30); 268 | expect(d3.v).to.have.lengthOf(n); 269 | d3.v[0].should.be.closeTo(0.006779063, 0.00000001); 270 | d3.v[n-1].should.be.closeTo(0.006779063, 0.00000001); 271 | d3.v[n/2].should.be.closeTo(0.3956322, 0.0000001); 272 | 273 | }); 274 | 275 | it ('tests for mehtod "pt".', function () { 276 | 277 | const n = 10000; 278 | 279 | // distribution for DoF = 1 280 | const t1 = Vector.seq(-5, 5, 10 / (n - 1)); 281 | const p1 = pt(t1, 1); 282 | expect(p1.v).to.have.lengthOf(n); 283 | p1.v[0].should.be.closeTo(0.06283296, 0.001); 284 | p1.v[n-1].should.be.closeTo(0.937167, 0.001); 285 | p1.v[n/2].should.be.closeTo(0.5, 0.001); 286 | 287 | // distribution for DoF = 3 288 | const t2 = Vector.seq(-5, 5, 10 / (n - 1)); 289 | const p2 = pt(t2, 3); 290 | expect(p2.v).to.have.lengthOf(n); 291 | p2.v[0].should.be.closeTo(0.007696219, 0.001); 292 | p2.v[n-1].should.be.closeTo(0.9923038, 0.001); 293 | p2.v[n/2].should.be.closeTo(0.5, 0.001); 294 | 295 | // distribution for DoF = 30 296 | const t3 = Vector.seq(-5, 5, 10 / (n - 1)); 297 | const p3 = pt(t3, 30); 298 | expect(p3.v).to.have.lengthOf(n); 299 | p3.v[0].should.be.closeTo(0.00001164834, 0.001); 300 | p3.v[n-1].should.be.closeTo(0.9999884, 0.001); 301 | p3.v[n/2].should.be.closeTo(0.5, 0.001); 302 | 303 | // special cases 304 | const p4 = pt(2.9703, 200) 305 | p4.should.be.closeTo(0.9983, 0.0001); 306 | 307 | const p5 = pt(1.97, 200) 308 | p5.should.be.closeTo(0.9748907, 0.0001); 309 | 310 | const p6 = pt(3, 500) 311 | p6.should.be.closeTo(0.9985828, 0.0001); 312 | 313 | }); 314 | 315 | it ('tests for method "qt".', function () { 316 | 317 | const n = 1000000; 318 | 319 | // border cases 320 | qt(0, 1).should.be.equal(-Infinity); 321 | qt(1, 1).should.be.equal(Infinity); 322 | qt(vector([0, 0, 1, 1]), 1).should.be.eql(vector([-Infinity, -Infinity, Infinity, Infinity])); 323 | 324 | // middle point and border cases 325 | qt(0.5, 1).should.be.equal(0); 326 | qt(vector([0.5, 0.5]), 1).should.be.eql(vector([0, 0])); 327 | qt(vector([0, 0.5, 1]), 1).should.be.eql(vector([-Infinity, 0, Infinity])); 328 | 329 | // other fixed cases 330 | const dof = [1, 2, 3, 4, 10, 30, 100]; 331 | let p, expected; 332 | 333 | p = 0.9999 334 | expected = [3183.098757, 70.700071, 22.203742, 13.033672, 5.693820, 4.233986, 3.861600]; 335 | for (let i = 0; i < dof.length; i++) { 336 | qt(p, dof[i]).should.be.closeTo(expected[i], 0.00001); 337 | qt(1 - p, dof[i]).should.be.closeTo(-expected[i], 0.00001); 338 | } 339 | 340 | p = 0.99 341 | expected = [31.820516, 6.964557, 4.540703, 3.746947, 2.763769, 2.457262, 2.364217]; 342 | for (let i = 0; i < dof.length; i++) { 343 | qt(p, dof[i]).should.be.closeTo(expected[i], 0.0001); 344 | qt(1 - p, dof[i]).should.be.closeTo(-expected[i], 0.0001); 345 | } 346 | 347 | p = 0.95 348 | expected = [6.313752, 2.919986, 2.353363, 2.131847, 1.812461, 1.697261, 1.660234]; 349 | for (let i = 0; i < dof.length; i++) { 350 | qt(p, dof[i]).should.be.closeTo(expected[i], 0.0001); 351 | qt(1 - p, dof[i]).should.be.closeTo(-expected[i], 0.0001); 352 | } 353 | 354 | p = 0.85 355 | expected = [1.962611, 1.386207, 1.249778, 1.189567, 1.093058, 1.054662, 1.041836]; 356 | for (let i = 0; i < dof.length; i++) { 357 | qt(p, dof[i]).should.be.closeTo(expected[i], 0.0001); 358 | qt(1 - p, dof[i]).should.be.closeTo(-expected[i], 0.0001); 359 | } 360 | 361 | p = 0.75 362 | expected = [1.0000000, 0.8164966, 0.7648923, 0.7406971, 0.6998121, 0.6827557, 0.6769510]; 363 | for (let i = 0; i < dof.length; i++) { 364 | qt(p, dof[i]).should.be.closeTo(expected[i], 0.0001); 365 | qt(1 - p, dof[i]).should.be.closeTo(-expected[i], 0.0001); 366 | } 367 | 368 | // errors 369 | expect(() => qt(-0.0001, 1)).to.throw(Error, 'Parameter "p" must be between 0 and 1.'); 370 | expect(() => qt( 1.0001, 1)).to.throw(Error, 'Parameter "p" must be between 0 and 1.'); 371 | expect(() => qt(0.2)).to.throw(Error, 'Parameter "df" (degrees of freedom) must be an integer number >= 1.'); 372 | expect(() => qt(0.2, -1)).to.throw(Error, 'Parameter "df" (degrees of freedom) must be an integer number >= 1.'); 373 | expect(() => qt(0.2, 0.5)).to.throw(Error, 'Parameter "df" (degrees of freedom) must be an integer number >= 1.'); 374 | 375 | // long vectors 376 | p = Vector.seq(0.0001, 0.9999, 0.9998/(n - 1)); 377 | const q = qt(p, 10); 378 | expect(q.v).to.have.lengthOf(n); 379 | q.v[0].should.be.equal(qt(0.0001, 10)); 380 | q.v[n - 1].should.be.equal(qt(0.9998999999999999, 10)); 381 | 382 | // special case 383 | const t4 = qt(0.9983, 200); 384 | t4.should.be.closeTo(2.97, 0.01); 385 | }); 386 | 387 | it ('tests for method "df".', function () { 388 | 389 | const n = 10000; 390 | 391 | // distribution for DoF = 1, 2 392 | const F1 = Vector.seq(0.001, 10, 10/n); 393 | const d1 = df(F1, 1, 2); 394 | expect(d1.v).to.have.lengthOf(n); 395 | d1.v[0].should.be.closeTo(11.17196, 0.001); 396 | d1.v[n-1].should.be.closeTo(0.007607258, 0.001); 397 | d1.v[n/2].should.be.closeTo(0.02414726, 0.001); 398 | 399 | // distribution for DoF = 3, 10 400 | const F2 = Vector.seq(0.001, 10, 10/n); 401 | const d2 = df(F2, 3, 10); 402 | expect(d2.v).to.have.lengthOf(n); 403 | d2.v[0].should.be.closeTo(0.07019374, 0.001); 404 | d2.v[n-1].should.be.closeTo(0.0008585295, 0.001); 405 | d2.v[n/2].should.be.closeTo(0.01288309, 0.001); 406 | 407 | }); 408 | 409 | it ('tests for method "pf".', function () { 410 | 411 | const n = 10000; 412 | 413 | // distribution for DoF = 1, 2 414 | const F1 = Vector.seq(0, 10, 10/n); 415 | const p1 = pf(F1, 1, 2); 416 | expect(p1.v).to.have.lengthOf(n + 1); 417 | p1.v[0].should.be.closeTo(0, 0.001); 418 | p1.v[n-1].should.be.closeTo(0.9128709, 0.001); 419 | p1.v[n/2].should.be.closeTo(0.8451543, 0.001); 420 | 421 | // distribution for DoF = 3, 10 422 | const F2 = Vector.seq(0, 10, 10/n); 423 | const p2 = pf(F2, 3, 10); 424 | expect(p2.v).to.have.lengthOf(n + 1); 425 | p2.v[0].should.be.closeTo(0, 0.001); 426 | p2.v[n].should.be.closeTo(0.9976484, 0.001); 427 | p2.v[n/2].should.be.closeTo(0.9773861, 0.001); 428 | 429 | // special cases 430 | const p31 = pf(2.97, 1, 200); 431 | p31.should.be.closeTo(0.9136327, 0.0001); 432 | 433 | }); 434 | 435 | it ('tests for method "pchisq".', function () { 436 | // all outcomes are compared with similar outcomes from R 437 | 438 | // single value 439 | let x = 0.5 440 | pchisq(x, 0).should.be.equal(1); 441 | pchisq(x, 1).should.be.closeTo(0.5205000, 0.0001); 442 | pchisq(x, 2).should.be.closeTo(0.2211990, 0.0001); 443 | pchisq(x, 5).should.be.closeTo(0.0078767, 0.0001); 444 | pchisq(x, 10).should.be.closeTo(0.0000000, 0.0001); 445 | pchisq(x, 20).should.be.closeTo(0.0000000, 0.0001); 446 | pchisq(x, 100).should.be.closeTo(0.0000000, 0.0001); 447 | pchisq(x, 250).should.be.closeTo(0.0000000, 0.0001); 448 | 449 | x = 5 450 | pchisq(x, 0).should.be.equal(1); 451 | pchisq(x, 1).should.be.closeTo(0.9746527, 0.0001); 452 | pchisq(x, 2).should.be.closeTo(0.9179150, 0.0001); 453 | pchisq(x, 5).should.be.closeTo(0.5841198, 0.0001); 454 | pchisq(x, 10).should.be.closeTo(0.1088220, 0.0001); 455 | pchisq(x, 20).should.be.closeTo(0.0002774, 0.0001); 456 | pchisq(x, 100).should.be.closeTo(0.0000000, 0.0001); 457 | pchisq(x, 250).should.be.closeTo(0.0000000, 0.0001); 458 | 459 | x = 10 460 | pchisq(x, 0).should.be.equal(1); 461 | pchisq(x, 1).should.be.closeTo(0.9984346, 0.0001); 462 | pchisq(x, 2).should.be.closeTo(0.9932621, 0.0001); 463 | pchisq(x, 5).should.be.closeTo(0.9247648, 0.0001); 464 | pchisq(x, 10).should.be.closeTo(0.5595067, 0.0001); 465 | pchisq(x, 20).should.be.closeTo(0.0318281, 0.0001); 466 | pchisq(x, 100).should.be.closeTo(0.0000000, 0.0001); 467 | pchisq(x, 250).should.be.closeTo(0.0000000, 0.0001); 468 | 469 | x = 50 470 | pchisq(x, 0).should.be.equal(1); 471 | pchisq(x, 1).should.be.closeTo(1.0000000, 0.0001); 472 | pchisq(x, 2).should.be.closeTo(1.0000000, 0.0001); 473 | pchisq(x, 5).should.be.closeTo(1.0000000, 0.0001); 474 | pchisq(x, 10).should.be.closeTo(0.9999997, 0.0001); 475 | pchisq(x, 20).should.be.closeTo(0.9997785, 0.0001); 476 | pchisq(x, 100).should.be.closeTo(0.0000000, 0.0001); 477 | pchisq(x, 250).should.be.closeTo(0.0000000, 0.0001); 478 | 479 | x = 150 480 | pchisq(x, 0).should.be.equal(1); 481 | pchisq(x, 1).should.be.closeTo(1.0000000, 0.0001); 482 | pchisq(x, 2).should.be.closeTo(1.0000000, 0.0001); 483 | pchisq(x, 5).should.be.closeTo(1.0000000, 0.0001); 484 | pchisq(x, 10).should.be.closeTo(1.0000000, 0.0001); 485 | pchisq(x, 20).should.be.closeTo(1.0000000, 0.0001); 486 | pchisq(x, 100).should.be.closeTo(0.9990961, 0.0001); 487 | pchisq(x, 150).should.be.closeTo(0.5153564, 0.0001); 488 | pchisq(x, 250).should.be.closeTo(0.0000000, 0.0001); 489 | 490 | // vector with values 491 | const x4 = vector([10, 0, 3.16]); 492 | const p4 = pchisq(x4, 5) 493 | 494 | expect(isvector(p4)).to.be.true; 495 | p4.length.should.be.equal(3) 496 | p4.v[0].should.be.closeTo(0.9247648, 0.0001); 497 | p4.v[1].should.be.closeTo(0.0000000, 0.0001); 498 | p4.v[2].should.be.closeTo(0.3246659, 0.0001); 499 | }); 500 | 501 | it ('tests for method "qchisq".', function () { 502 | // all outcomes are compared with similar outcomes from R 503 | 504 | // single value 505 | let p = 0.001 506 | qchisq(p, 0).should.be.equal(0); 507 | qchisq(p, 1).should.be.closeTo(0.000000, 0.0001); 508 | qchisq(p, 2).should.be.closeTo(0.002001, 0.001); 509 | qchisq(p, 5).should.be.closeTo(0.210213, 0.001); 510 | qchisq(p, 10).should.be.closeTo(1.478740, 0.001); 511 | qchisq(p, 20).should.be.closeTo(5.921040, 0.001); 512 | qchisq(p, 100).should.be.closeTo(61.9179, 0.01); 513 | qchisq(p, 250).should.be.closeTo(186.554, 0.01); 514 | 515 | p = 0.01 516 | qchisq(p, 0).should.be.equal(0); 517 | qchisq(p, 1).should.be.closeTo(0.000157, 0.0001); 518 | qchisq(p, 2).should.be.closeTo(0.020101, 0.001); 519 | qchisq(p, 5).should.be.closeTo(0.554298, 0.001); 520 | qchisq(p, 10).should.be.closeTo(2.558210, 0.001); 521 | qchisq(p, 20).should.be.closeTo(8.260400, 0.001); 522 | qchisq(p, 100).should.be.closeTo(70.0649, 0.01); 523 | qchisq(p, 250).should.be.closeTo(200.939, 0.01); 524 | 525 | p = 0.05 526 | qchisq(p, 0).should.be.equal(0); 527 | qchisq(p, 1).should.be.closeTo(0.003932, 0.0001); 528 | qchisq(p, 2).should.be.closeTo(0.102587, 0.001); 529 | qchisq(p, 5).should.be.closeTo(1.145476, 0.001); 530 | qchisq(p, 10).should.be.closeTo(3.940299, 0.001); 531 | qchisq(p, 20).should.be.closeTo(10.85081, 0.001); 532 | qchisq(p, 100).should.be.closeTo(77.92947, 0.01); 533 | qchisq(p, 250).should.be.closeTo(214.3916, 0.01); 534 | 535 | p = 0.10 536 | qchisq(p, 0).should.be.equal(0); 537 | qchisq(p, 1).should.be.closeTo(0.0157908, 0.0001); 538 | qchisq(p, 2).should.be.closeTo(0.210721, 0.001); 539 | qchisq(p, 5).should.be.closeTo(1.61031, 0.001); 540 | qchisq(p, 10).should.be.closeTo(4.86518, 0.001); 541 | qchisq(p, 20).should.be.closeTo(12.4426, 0.001); 542 | qchisq(p, 100).should.be.closeTo(82.3581, 0.01); 543 | qchisq(p, 250).should.be.closeTo(221.806, 0.01); 544 | 545 | p = 0.90 546 | qchisq(p, 0).should.be.equal(0); 547 | qchisq(p, 1).should.be.closeTo(2.70554, 0.0001); 548 | qchisq(p, 2).should.be.closeTo(4.60517, 0.001); 549 | qchisq(p, 5).should.be.closeTo(9.23636, 0.001); 550 | qchisq(p, 10).should.be.closeTo(15.9872, 0.001); 551 | qchisq(p, 20).should.be.closeTo(28.4120, 0.001); 552 | qchisq(p, 100).should.be.closeTo(118.498, 0.01); 553 | qchisq(p, 250).should.be.closeTo(279.050, 0.01); 554 | 555 | p = 0.95 556 | qchisq(p, 0).should.be.equal(0); 557 | qchisq(p, 1).should.be.closeTo(3.84146, 0.001); 558 | qchisq(p, 2).should.be.closeTo(5.99146, 0.001); 559 | qchisq(p, 5).should.be.closeTo(11.0705, 0.001); 560 | qchisq(p, 10).should.be.closeTo(18.3070, 0.001); 561 | qchisq(p, 20).should.be.closeTo(31.4104, 0.001); 562 | qchisq(p, 100).should.be.closeTo(124.342, 0.01); 563 | qchisq(p, 250).should.be.closeTo(287.882, 0.01); 564 | 565 | p = 0.99 566 | qchisq(p, 0).should.be.equal(0); 567 | qchisq(p, 1).should.be.closeTo(6.63490, 0.001); 568 | qchisq(p, 2).should.be.closeTo(9.21034, 0.001); 569 | qchisq(p, 5).should.be.closeTo(15.0863, 0.001); 570 | qchisq(p, 10).should.be.closeTo(23.2093, 0.001); 571 | qchisq(p, 20).should.be.closeTo(37.5662, 0.001); 572 | qchisq(p, 100).should.be.closeTo(135.807, 0.01); 573 | qchisq(p, 250).should.be.closeTo(304.94, 0.01); 574 | 575 | p = 0.999 576 | qchisq(p, 0).should.be.equal(0); 577 | qchisq(p, 1).should.be.closeTo(10.8276, 0.001); 578 | qchisq(p, 2).should.be.closeTo(13.8155, 0.001); 579 | qchisq(p, 5).should.be.closeTo(20.5150, 0.001); 580 | qchisq(p, 10).should.be.closeTo(29.5883, 0.001); 581 | qchisq(p, 20).should.be.closeTo(45.3147, 0.001); 582 | qchisq(p, 100).should.be.closeTo(149.449, 0.01); 583 | qchisq(p, 250).should.be.closeTo(324.832, 0.01); 584 | 585 | // vector with values 586 | const p3 = vector([0.01, 0.05]); 587 | const q3 = qchisq(p3, 5) 588 | q3.v[0].should.be.closeTo(0.55, 0.02); 589 | q3.v[1].should.be.closeTo(1.15, 0.02); 590 | 591 | // array with values 592 | const p4 = [0.01, 0.05]; 593 | const q4 = qchisq(p3, 5) 594 | q4.v[0].should.be.closeTo(0.55, 0.02); 595 | q4.v[1].should.be.closeTo(1.15, 0.02); 596 | }); 597 | 598 | it ('tests for method "qf".', function () { 599 | // all outcomes are compared with similar outcomes from R 600 | 601 | // single value 602 | let p ; 603 | 604 | p = 0.001 605 | qf(p, 0, 1).should.be.NaN; 606 | qf(p, 1, 0).should.be.NaN; 607 | qf(p, 1, 1).should.be.closeTo(0.000003, 0.000001); 608 | qf(p, 2, 3).should.be.closeTo(0.001, 0.0001); 609 | qf(p, 5, 10).should.be.closeTo(0.0371, 0.0001); 610 | qf(p, 10, 20).should.be.closeTo(0.128, 0.001); 611 | qf(p, 20, 30).should.be.closeTo(0.250, 0.01); 612 | qf(p, 100, 200).should.be.closeTo(0.572, 0.01); 613 | qf(p, 1, 200).should.be.closeTo(0.0000016, 0.0000001); 614 | qf(p, 250, 500).should.be.closeTo(0.7065, 0.0001); 615 | 616 | p = 0.01 617 | qf(p, 0, 1).should.be.NaN; 618 | qf(p, 1, 0).should.be.NaN; 619 | qf(p, 1, 1).should.be.closeTo(0.000247, 0.000001); 620 | qf(p, 2, 3).should.be.closeTo(0.010084, 0.000001); 621 | qf(p, 5, 10).should.be.closeTo(0.099492, 0.000001); 622 | qf(p, 10, 20).should.be.closeTo(0.2269944, 0.000001); 623 | qf(p, 20, 30).should.be.closeTo(0.3599084, 0.000001); 624 | qf(p, 100, 200).should.be.closeTo(0.6585758, 0.000001); 625 | qf(p, 1, 200).should.be.closeTo(0.000157, 0.000001); 626 | qf(p, 250, 500).should.be.closeTo(0.7707644, 0.000001); 627 | 628 | p = 0.05 629 | qf(p, 1, 1).should.be.closeTo(0.006194, 0.000001); 630 | qf(p, 2, 3).should.be.closeTo(0.052180, 0.000001); 631 | qf(p, 5, 10).should.be.closeTo(0.211190, 0.000001); 632 | qf(p, 10, 20).should.be.closeTo(0.3604881, 0.000001); 633 | qf(p, 20, 30).should.be.closeTo(0.4904158, 0.000001); 634 | qf(p, 100, 200).should.be.closeTo(0.745352, 0.000001); 635 | qf(p, 1, 200).should.be.closeTo(0.003942, 0.000001); 636 | qf(p, 250, 500).should.be.closeTo(0.8322605, 0.000001); 637 | 638 | p = 0.10 639 | qf(p, 1, 1).should.be.closeTo(0.02508563, 0.000001); 640 | qf(p, 2, 3).should.be.closeTo(0.109149, 0.000001); 641 | qf(p, 5, 10).should.be.closeTo(0.3032691, 0.000001); 642 | qf(p, 10, 20).should.be.closeTo(0.4543918, 0.000001); 643 | qf(p, 20, 30).should.be.closeTo(0.5753002, 0.000001); 644 | qf(p, 100, 200).should.be.closeTo(0.7954999, 0.000001); 645 | qf(p, 1, 200).should.be.closeTo(0.01583093, 0.000001); 646 | qf(p, 250, 500).should.be.closeTo(0.8667542, 0.000001); 647 | 648 | p = 0.90 649 | qf(p, 1, 1).should.be.closeTo(39.86346, 0.0001); 650 | qf(p, 2, 3).should.be.closeTo(5.462383, 0.00001); 651 | qf(p, 5, 10).should.be.closeTo(2.521641, 0.00001); 652 | qf(p, 10, 20).should.be.closeTo(1.936738, 0.00001); 653 | qf(p, 20, 30).should.be.closeTo(1.667309, 0.00001); 654 | qf(p, 100, 200).should.be.closeTo(1.241822, 0.00001); 655 | qf(p, 1, 200).should.be.closeTo(2.730783, 0.00001); 656 | qf(p, 250, 500).should.be.closeTo(1.148128, 0.00001); 657 | 658 | 659 | p = 0.95 660 | qf(p, 1, 1).should.be.closeTo(161.4476, 0.0001); 661 | qf(p, 2, 3).should.be.closeTo(9.552094, 0.00001); 662 | qf(p, 5, 10).should.be.closeTo(3.325835, 0.00001); 663 | qf(p, 10, 20).should.be.closeTo(2.347878, 0.00001); 664 | qf(p, 20, 30).should.be.closeTo(1.931653, 0.00001); 665 | qf(p, 100, 200).should.be.closeTo(1.320637, 0.00001); 666 | qf(p, 1, 200).should.be.closeTo(3.888375, 0.00001); 667 | qf(p, 250, 500).should.be.closeTo(1.194013, 0.00001); 668 | 669 | p = 0.99 670 | qf(p, 1, 1).should.be.closeTo(4052.181, 0.01); 671 | qf(p, 2, 3).should.be.closeTo(30.81652, 0.0001); 672 | qf(p, 5, 10).should.be.closeTo(5.636326, 0.00001); 673 | qf(p, 10, 20).should.be.closeTo(3.368186, 0.00001); 674 | qf(p, 20, 30).should.be.closeTo(2.548659, 0.00001); 675 | qf(p, 100, 200).should.be.closeTo(1.481056, 0.00001); 676 | qf(p, 1, 200).should.be.closeTo(6.763299, 0.00001); 677 | qf(p, 250, 500).should.be.closeTo(1.284611, 0.00001); 678 | 679 | p = 0.999 680 | qf(p, 1, 1).should.be.closeTo(405284.1, 20); 681 | qf(p, 2, 3).should.be.closeTo(148.5, 0.1); 682 | qf(p, 5, 10).should.be.closeTo(10.48072, 0.01); 683 | qf(p, 10, 20).should.be.closeTo(5.075246, 0.00001); 684 | qf(p, 20, 30).should.be.closeTo(3.492784, 0.00001); 685 | qf(p, 100, 200).should.be.closeTo(1.68243, 0.00001); 686 | qf(p, 1, 200).should.be.closeTo(11.1545, 0.00001); 687 | qf(p, 250, 500).should.be.closeTo(1.393628, 0.00001); 688 | 689 | // vector with values 690 | const p3 = vector([0.01, 0.99]); 691 | const q3 = qf(p3, 5, 10) 692 | q3.v[0].should.be.closeTo(0.09949242, 0.000001); 693 | q3.v[1].should.be.closeTo(5.636326, 0.00001); 694 | 695 | // array with values 696 | const p4 = [0.01, 0.99]; 697 | const q4 = qf(p4, 5, 10) 698 | q4.v[0].should.be.closeTo(0.09949242, 0.000001); 699 | q4.v[1].should.be.closeTo(5.636326, 0.00001); 700 | }); 701 | 702 | 703 | }); 704 | 705 | -------------------------------------------------------------------------------- /test/tests-misc.js: -------------------------------------------------------------------------------- 1 | /****************************************************************/ 2 | /* Tests for methods computing various statistics */ 3 | /****************************************************************/ 4 | 5 | // import dependencies 6 | import {default as chai} from 'chai'; 7 | import {default as chaiAlmost} from 'chai-almost'; 8 | import { vector } from '../src/arrays/index.js'; 9 | import { quantile } from '../src/stat/index.js'; 10 | 11 | // import methods to test 12 | import { closestind, closestindleft, closestindright, getoutliers, expandgrid, integrate, round } from '../src/misc/index.js'; 13 | 14 | // set up test settings 15 | const should = chai.should(); 16 | const expect = chai.expect; 17 | chai.use(chaiAlmost(0.00001)); 18 | 19 | describe('Tests for misc extra functions.', function () { 20 | 21 | it('tests for method "getoutliers"".', function () { 22 | 23 | const x4 = vector([-100, -2, -1, 0, 1, 2, 3, 50, 100]); 24 | const o1 = getoutliers(x4); 25 | expect(o1).to.eql(vector([-100, 50, 100])); 26 | 27 | const o2 = getoutliers(x4, quantile(x4, 0.25)); 28 | expect(o2).to.eql(vector([-100, 50, 100])); 29 | 30 | const o3 = getoutliers(x4, quantile(x4, 0.25), quantile(x4, 0.75)); 31 | expect(o3).to.eql(vector([-100, 50, 100])); 32 | 33 | }); 34 | 35 | it('tests for method "integrate".', function() { 36 | 37 | integrate(x => x**2, -1, 1).should.be.closeTo(0.6666667, 0.00001); 38 | integrate(x => x**2, -1, 0).should.be.closeTo(0.3333333, 0.00001); 39 | integrate(x => x**2, 0, 1).should.be.closeTo(0.3333333, 0.00001); 40 | 41 | integrate(Math.sin, 0, Math.PI).should.be.closeTo(2, 0.00001); 42 | integrate(Math.sin, 0, 2 * Math.PI).should.be.closeTo(0, 0.00001); 43 | integrate((x) => Math.exp(-x), 0, Infinity).should.be.closeTo(1, 0.00001); 44 | 45 | [0.1, 0.5, 1, 5, 10].map(a => 46 | integrate((x) => 1/Math.sqrt(a**2 - x**2), 0, a).should.be.closeTo(Math.PI/2, 0.00001) 47 | ); 48 | 49 | integrate((x) => Math.exp(-(x**2)), -Infinity, Infinity).should.be.closeTo(Math.sqrt(Math.PI), 0.0001); 50 | }); 51 | 52 | it('tests for method "expandgrid".', function () { 53 | 54 | const z1 = expandgrid(vector([100, 200, 300]), vector([10, 20])); 55 | z1.should.be.a('Array'); 56 | expect(z1).to.have.lengthOf(2); 57 | expect(z1[0]).to.eql(vector([100, 200, 300, 100, 200, 300])); 58 | expect(z1[1]).to.eql(vector([10, 10, 10, 20, 20, 20])); 59 | 60 | const z2 = expandgrid(vector([100, 200, 300]), vector([10, 20]), vector([-20, +20])); 61 | z2.should.be.a('Array'); 62 | expect(z2).to.have.lengthOf(3); 63 | expect(z2[0]).to.eql(vector([100, 200, 300, 100, 200, 300, 100, 200, 300, 100, 200, 300])); 64 | expect(z2[1]).to.eql(vector([10, 10, 10, 20, 20, 20, 10, 10, 10, 20, 20, 20])); 65 | expect(z2[2]).to.eql(vector([-20, -20, -20, -20, -20, -20, 20, 20, 20, 20, 20, 20])); 66 | 67 | }); 68 | 69 | it ('tests for method "closestindleft".', function() { 70 | const x1 = vector([-200, -100, -50, -0.5, 0.1, 1.5, 20, 500, 1000]); 71 | 72 | closestindleft(x1, -120).should.equal(1); 73 | closestindleft(x1, -101).should.equal(1); 74 | closestindleft(x1, -99).should.equal(2); 75 | closestindleft(x1, -51).should.equal(2); 76 | closestindleft(x1, -50).should.equal(3); 77 | closestindleft(x1, -49).should.equal(3); 78 | closestindleft(x1, 0).should.equal(4); 79 | closestindleft(x1, 0.099999).should.equal(4); 80 | closestindleft(x1, 0.1).should.equal(5); 81 | closestindleft(x1, 0.11).should.equal(5); 82 | closestindleft(x1, 499).should.equal(7); 83 | closestindleft(x1, 501).should.equal(8); 84 | closestindleft(x1, 2000).should.equal(9); 85 | }); 86 | 87 | it ('tests for method "closestindright".', function() { 88 | const x1 = vector([-200, -100, -50, -0.5, 0.1, 1.5, 20, 500, 1000]); 89 | 90 | closestindright(x1, -1200).should.equal(1); 91 | closestindright(x1, -120).should.equal(2); 92 | closestindright(x1, -101).should.equal(2); 93 | closestindright(x1, -99).should.equal(3); 94 | closestindright(x1, -51).should.equal(3); 95 | closestindright(x1, -50).should.equal(3); 96 | closestindright(x1, -49).should.equal(4); 97 | closestindright(x1, 0).should.equal(5); 98 | closestindright(x1, 0.099999).should.equal(5); 99 | closestindright(x1, 0.1).should.equal(5); 100 | closestindright(x1, 0.11).should.equal(6); 101 | closestindright(x1, 499).should.equal(8); 102 | closestindright(x1, 501).should.equal(9); 103 | closestindright(x1, 2000).should.equal(9); 104 | }); 105 | 106 | 107 | it ('tests for method "closestind".', function() { 108 | const x1 = vector([-200, -0.5, -0.45, 1.2, -5.1, 4.2, -10.0]); 109 | 110 | closestind(x1, -120).should.equal(1); 111 | closestind(x1, -20).should.equal(7); 112 | closestind(x1, -7).should.equal(5); 113 | closestind(x1, 0).should.equal(3); 114 | closestind(x1, 3).should.equal(6); 115 | closestind(x1, 100000000).should.equal(6); 116 | closestind(x1, -100000000).should.equal(1); 117 | }); 118 | 119 | it('tests for method "round".', function () { 120 | expect(round([1.23456789, 2.3456789])).be.eql([1, 2]) 121 | expect(round([1.23456789, 2.3456789], 2)).be.eql([1.23, 2.35]) 122 | }); 123 | 124 | }); -------------------------------------------------------------------------------- /test/tests-models.js: -------------------------------------------------------------------------------- 1 | /****************************************************************/ 2 | /* Tests for modelling methods */ 3 | /****************************************************************/ 4 | 5 | // import dependencies 6 | import {default as chai} from 'chai'; 7 | import {default as chaiAlmost} from 'chai-almost'; 8 | import { factor, cbind, vector, matrix, tcrossprod, Vector, Matrix } from '../src/arrays/index.js'; 9 | import { variance, median, mean, sd, sum } from '../src/stat/index.js'; 10 | import { svd } from '../src/decomp/index.js'; 11 | import { scale as prep_scale } from '../src/prep/index.js'; 12 | 13 | // import of functions to test 14 | import {simcapredict, getclassres, simpls, plsfit, plspredict, splitregdata, pcrfit, pcrpredict, pcafit, pcapredict, lmfit, 15 | lmpredict, polyfit, polypredict, getsimcaparams} from '../src/models/index.js'; 16 | 17 | // set up test settings 18 | const expect = chai.expect; 19 | const should = chai.should(); 20 | chai.use(chaiAlmost(0.001)); 21 | 22 | function testClassResObject(x, cPred, cRef, className, stat) { 23 | 24 | expect(x.class.includes('classres')); 25 | expect(x.cPred).to.be.deep.equal(cPred); 26 | expect(x.className).to.be.equal(className); 27 | 28 | if (cRef) { 29 | expect(x.cRef).to.be.deep.equal(cRef); 30 | expect(x.TP).to.be.deep.equal(vector(stat[0])); 31 | expect(x.FP).to.be.deep.equal(vector(stat[1])); 32 | expect(x.TN).to.be.deep.equal(vector(stat[2])); 33 | expect(x.FN).to.be.deep.equal(vector(stat[3])); 34 | expect(x.sensitivity).to.be.deep.equal(vector(stat[4])); 35 | expect(x.specificity).to.be.deep.equal(vector(stat[5])); 36 | expect(x.accuracy).to.be.deep.equal(vector(stat[6])); 37 | } else { 38 | expect(x.cRef === undefined).to.be.true; 39 | expect(x.TP === undefined).to.be.true; 40 | expect(x.FP === undefined).to.be.true; 41 | expect(x.TN === undefined).to.be.true; 42 | expect(x.FP === undefined).to.be.true; 43 | expect(x.sensitivity === undefined).to.be.true; 44 | expect(x.specificity === undefined).to.be.true; 45 | expect(x.accuracy === undefined).to.be.true; 46 | } 47 | } 48 | 49 | describe('Tests for classification methods.', function () { 50 | 51 | it ('tests for method "getclassres" - one predicted item.', function () { 52 | 53 | // perfect case 54 | const cp1 = [factor(['none', 'red', 'none', 'red', 'none', 'red', 'none', 'red'])]; 55 | const cr1 = factor(['blue', 'red', 'blue', 'red', 'blue', 'red', 'green', 'red']); 56 | const r1a = getclassres(cp1, 'red', cr1); 57 | const r1b = getclassres(cp1, 'red'); 58 | testClassResObject(r1a, cp1, cr1, 'red', [[4], [0], [4], [0], [1], [1], [1]]) 59 | testClassResObject(r1b, cp1, null, 'red') 60 | 61 | // one false negative 62 | const cp2 =[factor(['none', 'red', 'none', 'none', 'none', 'red', 'none', 'red'])]; 63 | const cr2 = factor(['blue', 'red', 'green', 'red', 'blue', 'red', 'green', 'red']); 64 | const r2a = getclassres(cp2, 'red', cr2); 65 | const r2b = getclassres(cp2, 'red'); 66 | testClassResObject(r2a, cp2, cr2, 'red', [[3], [0], [4], [1], [0.75], [1], [0.875]]) 67 | testClassResObject(r2b, cp2, null, 'red') 68 | 69 | // one false positive 70 | const cp3 = [factor(['red', 'red', 'none', 'red', 'none', 'red', 'none', 'red'])]; 71 | const cr3 = factor(['blue', 'red', 'blue', 'red', 'blue', 'red', 'green', 'red']); 72 | const r3a = getclassres(cp3, 'red', cr3); 73 | const r3b = getclassres(cp3, 'red'); 74 | testClassResObject(r3a, cp3, cr3, 'red', [[4], [1], [3], [0], [1], [0.75], [0.875]]) 75 | testClassResObject(r3b, cp3, null, 'red') 76 | 77 | // everything is wrong 78 | const cp4 = [factor(['red', 'none', 'red', 'none', 'red', 'none', 'red', 'none'])]; 79 | const cr4 = factor(['blue', 'red', 'blue', 'red', 'blue', 'red', 'green', 'red']); 80 | const r4a = getclassres(cp4, 'red', cr4); 81 | const r4b = getclassres(cp4, 'red'); 82 | testClassResObject(r4a, cp4, cr4, 'red', [[0], [4], [0], [4], [0], [0], [0]]) 83 | testClassResObject(r4b, cp4, null, 'red') 84 | 85 | // no class items in predictions (all none) 86 | const cp5 = [factor(['none', 'none', 'none', 'none', 'none', 'none', 'none', 'none'])]; 87 | const cr5 = factor(['blue', 'red', 'blue', 'red', 'blue', 'red', 'green', 'red']); 88 | const r5a = getclassres(cp5, 'red', cr5); 89 | const r5b = getclassres(cp5, 'red'); 90 | testClassResObject(r5a, cp5, cr5, 'red', [[0], [0], [4], [4], [0], [1], [0.5]]) 91 | testClassResObject(r5b, cp5, null, 'red') 92 | 93 | // no class items in references 94 | const cp6 = [factor(['red', 'none', 'red', 'none', 'red', 'none', 'red', 'none'])]; 95 | const cr6 = factor(['blue', 'green', 'blue', 'green', 'blue', 'green', 'green', 'blue']); 96 | const r6a = getclassres(cp6, 'red', cr6); 97 | const r6b = getclassres(cp6, 'red'); 98 | testClassResObject(r6a, cp6, cr6, 'red', [[0], [4], [4], [0], [NaN], [0.5], [0.5]]) 99 | testClassResObject(r6b, cp6, null, 'red') 100 | 101 | // only class items in references 102 | const cp7 = [factor(['red', 'none', 'red', 'none', 'red', 'none', 'red', 'none'])]; 103 | const cr7 = factor(['red', 'red', 'red', 'red', 'red', 'red', 'red', 'red']); 104 | const r7a = getclassres(cp7, 'red', cr7); 105 | const r7b = getclassres(cp7, 'red'); 106 | testClassResObject(r7a, cp7, cr7, 'red', [[4], [0], [0], [4], [0.5], [NaN], [0.5]]) 107 | testClassResObject(r7b, cp7, null, 'red') 108 | 109 | }); 110 | 111 | it ('tests for method "getclassres" - several predicted items.', function () { 112 | 113 | // perfect case 114 | const cp1 = [ 115 | factor(['none', 'red', 'none', 'red', 'none', 'red', 'none', 'red']), 116 | factor(['none', 'red', 'none', 'none', 'none', 'red', 'none', 'red']), 117 | factor(['red', 'none', 'red', 'none', 'red', 'none', 'red', 'none']), 118 | factor(['none', 'none', 'none', 'none', 'none', 'none', 'none', 'none']) 119 | ]; 120 | const cr1 = factor(['blue', 'red', 'blue', 'red', 'blue', 'red', 'green', 'red']); 121 | const r1a = getclassres(cp1, 'red', cr1); 122 | const r1b = getclassres(cp1, 'red'); 123 | testClassResObject(r1a, cp1, cr1, 'red', [ 124 | [4, 3, 0, 0], 125 | [0, 0, 4, 0], 126 | [4, 4, 0, 4], 127 | [0, 1, 4, 4], 128 | [1, 0.75, 0, 0], 129 | [1, 1, 0, 1], 130 | [1, 0.875, 0, 0.5] 131 | ]) 132 | testClassResObject(r1b, cp1, null, 'red') 133 | }); 134 | }); 135 | 136 | describe('Tests for SIMCA methods.', function () { 137 | 138 | it ('tests for method "simcapredict".', function () { 139 | const X1 = Matrix.rand(50, 10); 140 | const X2 = Matrix.rand(20, 10); 141 | const mpca = pcafit(X1, 10); 142 | const params = getsimcaparams('red', 0.05, 'classic'); 143 | const c = simcapredict(mpca, params, X2) 144 | }); 145 | 146 | }); 147 | 148 | describe('Tests for PLS methods.', function () { 149 | 150 | it ('tests for method "pldpredict"', function () { 151 | // common dataset for all tests 152 | const data = matrix([ 153 | 32, 150, 41, 28000, 119, 154 | 35, 160, 48, 31000, 129, 155 | 36, 166, 47, 28000, 112, 156 | 37, 166, 49, 14000, 123, 157 | 42, 175, 67, 38000, 105, 158 | 43, 180, 80, 30000, 129, 159 | 43, 181, 75, 31000, 105, 160 | 44, 180, 81, 42000, 113 161 | ], 5, 8).t(); 162 | 163 | const [Xc, Yc] = splitregdata(data); 164 | 165 | // make PCA and PCR for centered and scaled data and check all results in full 166 | const m1 = plsfit(Xc, Yc, 3, true, true); 167 | const r1 = plspredict(m1, Xc, Yc); 168 | const r2 = plspredict(m1, Xc); 169 | 170 | const T1 = matrix([ 171 | 0.4945617, 0.36754034, 0.1250141, 172 | 0.3220733, 0.02656872, 0.5115363, 173 | 0.1747736, 0.20483128, 0.2758381, 174 | 0.3630906, 0.53394896, 0.4310806, 175 | 0.2781758, 0.39131972, 0.1470429, 176 | 0.2420359, 0.59936775, 0.3803569, 177 | 0.3655882, 0.01375190, 0.4564504, 178 | 0.4686992, 0.15680855, 0.2935047, 179 | ], 3, 8).t(); 180 | 181 | const H1 = matrix([ 182 | 1.7121391, 2.6577404, 2.767140, 183 | 0.7261183, 0.7310596, 2.562745, 184 | 0.2138206, 0.5075116, 1.040118, 185 | 0.9228437, 2.9185541, 4.219368, 186 | 0.5416725, 1.6135903, 1.764942, 187 | 0.4100696, 2.9247616, 3.937461, 188 | 0.9355833, 0.9369071, 2.395336, 189 | 1.5377529, 1.7098753, 2.312890, 190 | ], 3, 8).t(); 191 | 192 | const Q1 = matrix([ 193 | 0.8783125, 0.15842635, 9.720942e-02, 194 | 1.1479065, 1.16544180, 3.649739e-02, 195 | 0.7186410, 0.38216058, 6.778351e-02, 196 | 2.0996044, 0.79275237, 8.243227e-05, 197 | 1.0857301, 0.09480393, 3.913798e-03, 198 | 3.0896422, 0.61662640, 2.667597e-03, 199 | 0.8971517, 0.90605443, 1.124068e-02, 200 | 0.4379797, 0.36701856, 1.254689e-03, 201 | ], 3, 8).t(); 202 | 203 | const U1 = matrix([ 204 | 3.950369, 0.16641101, 0.0064092242, 205 | 2.257354, 0.03450792, 0.0046740695, 206 | 1.693015, 0.12839530, 0.0102507432, 207 | 1.128677, 0.29291384, 0.0188942539, 208 | 1.693015, 0.03033479, 0.0206833688, 209 | 2.257354, 0.15737118, 0.0109039174, 210 | 2.257354, 0.03229076, 0.0069754088, 211 | 2.821692, 0.05834525, 0.0003642595, 212 | ], 3, 8).t(); 213 | 214 | const Yp1 = matrix([ 215 | 33.25851, 32.25875, 32.19505, 216 | 35.26097, 35.18870, 34.92805, 217 | 36.97101, 36.41384, 36.55440, 218 | 34.78479, 36.23720, 36.45686, 219 | 42.22941, 41.16497, 41.23990, 220 | 41.80985, 43.44021, 43.24640, 221 | 43.24420, 43.28161, 43.51419, 222 | 44.44125, 44.01471, 43.86515, 223 | ], 3, 8).t(); 224 | 225 | // check main outcomes 226 | expect(r1.T.apply(Math.abs, 0)).to.be.deep.almost.equal(T1); 227 | expect(r1.H).to.be.deep.almost.equal(H1); 228 | expect(r1.Q).to.be.deep.almost.equal(Q1); 229 | expect(r1.U.apply(Math.abs, 0)).to.be.deep.almost.equal(U1); 230 | expect(r1.Ypred).to.be.deep.almost.equal(Yp1); 231 | 232 | expect(r2.T.apply(Math.abs, 0)).to.be.deep.almost.equal(T1); 233 | expect(r2.H).to.be.deep.almost.equal(H1); 234 | expect(r2.Q).to.be.deep.almost.equal(Q1); 235 | expect(r2.Ypred).to.be.deep.almost.equal(Yp1); 236 | 237 | }); 238 | 239 | it ('tests for method "plsfit"', function () { 240 | 241 | // common dataset for all tests 242 | const data = matrix([ 243 | 32, 150, 41, 28000, 119, 244 | 35, 160, 48, 31000, 129, 245 | 36, 166, 47, 28000, 112, 246 | 37, 166, 49, 14000, 123, 247 | 42, 175, 67, 38000, 105, 248 | 43, 180, 80, 30000, 129, 249 | 43, 181, 75, 31000, 105, 250 | 44, 180, 81, 42000, 113 251 | ], 5, 8).t(); 252 | 253 | const [Xc, Yc] = splitregdata(data); 254 | 255 | // make PCA and PCR for centered and scaled data and check all results in full 256 | const ncomp = 3; 257 | const m1 = plsfit(Xc, Yc, ncomp, true, true); 258 | 259 | const R1 = matrix([ 260 | 0.15492668, 0.1881235, 0.12558132, 261 | 0.15317320, 0.1316393, 0.06823508, 262 | 0.08218702, 0.2824183, 0.32337181, 263 | 0.05900391, 0.2131759, 0.34074718 264 | ], 3, 4).t(); 265 | 266 | const P1 = matrix([ 267 | 2.481456, 0.7967174, 0.3398910, 268 | 2.526248, 0.6219986, 0.3463264, 269 | 1.828851, 1.5105447, 1.1672083, 270 | 1.326946, 1.6025908, 1.6324190 271 | ], 3, 4).t(); 272 | 273 | const C1 = matrix([ 274 | 2.559598, 0.5997323, 0.1123446 275 | ], 3, 1).t(); 276 | 277 | const yeigenvals1 = vector([ 278 | 6.5515441996, 0.0230428629, 0.0001600688 279 | ]); 280 | 281 | const xeigenvals1 = vector([ 282 | 0.1428571, 0.1428571, 0.1428571 283 | ]); 284 | 285 | const Nq1 = vector([ 286 | 4, 4, 1 287 | ]); 288 | 289 | const Nh1 = vector([ 290 | 6, 6, 13 291 | ]); 292 | 293 | const q01 = vector([ 294 | 1.29437100, 0.56041055, 0.02758119 295 | ]); 296 | 297 | const h01 = vector([ 298 | 0.875, 1.750, 2.625 299 | ]); 300 | 301 | // check that the model outcomes are correct 302 | expect(m1.P.apply(Math.abs, 0)).to.be.deep.almost.equal(P1); 303 | expect(m1.R.apply(Math.abs, 0)).to.be.deep.almost.equal(R1); 304 | expect(m1.C.apply(Math.abs, 0)).to.be.deep.almost.equal(C1); 305 | expect(m1.yeigenvals).to.be.deep.almost.equal(yeigenvals1); 306 | expect(m1.xeigenvals).to.be.deep.almost.equal(xeigenvals1); 307 | expect(m1.qParams['classic'][1]).to.be.deep.almost.equal(Nq1); 308 | expect(m1.hParams['classic'][1]).to.be.deep.almost.equal(Nh1); 309 | expect(m1.qParams['classic'][0]).to.be.deep.almost.equal(q01); 310 | expect(m1.hParams['classic'][0]).to.be.deep.almost.equal(h01); 311 | }); 312 | 313 | it ('tests for method "simpls"', function () { 314 | 315 | // common dataset for all tests 316 | const data = matrix([ 317 | 32, 150, 41, 28000, 119, 318 | 35, 160, 48, 31000, 129, 319 | 36, 166, 47, 28000, 112, 320 | 37, 166, 49, 14000, 123, 321 | 42, 175, 67, 38000, 105, 322 | 43, 180, 80, 30000, 129, 323 | 43, 181, 75, 31000, 105, 324 | 44, 180, 81, 42000, 113 325 | ], 5, 8).t(); 326 | 327 | const [Xc, Yc] = splitregdata(data); 328 | 329 | const Xp = prep_scale(Xc, true, true); 330 | const Yp = prep_scale(Yc, true, true); 331 | 332 | const R1 = matrix([ 333 | 0.15492668, 0.1881235, 0.12558132, 334 | 0.15317320, 0.1316393, 0.06823508, 335 | 0.08218702, 0.2824183, 0.32337181, 336 | 0.05900391, 0.2131759, 0.34074718 337 | ], 3, 4).t(); 338 | 339 | const P1 = matrix([ 340 | 2.481456, 0.7967174, 0.3398910, 341 | 2.526248, 0.6219986, 0.3463264, 342 | 1.828851, 1.5105447, 1.1672083, 343 | 1.326946, 1.6025908, 1.6324190 344 | ], 3, 4).t(); 345 | 346 | const T1 = matrix([ 347 | 0.4945617, 0.36754034, 0.1250141, 348 | 0.3220733, 0.02656872, 0.5115363, 349 | 0.1747736, 0.20483128, 0.2758381, 350 | 0.3630906, 0.53394896, 0.4310806, 351 | 0.2781758, 0.39131972, 0.1470429, 352 | 0.2420359, 0.59936775, 0.3803569, 353 | 0.3655882, 0.01375190, 0.4564504, 354 | 0.4686992, 0.15680855, 0.2935047 355 | ], 3, 8).t(); 356 | 357 | const C1 = matrix([ 358 | 2.559598, 0.5997323, 0.1123446 359 | ], 3, 1).t(); 360 | 361 | const U1 = matrix([ 362 | 3.950369, 0.16641101, 0.0064092242, 363 | 2.257354, 0.03450792, 0.0046740695, 364 | 1.693015, 0.12839530, 0.0102507432, 365 | 1.128677, 0.29291384, 0.0188942539, 366 | 1.693015, 0.03033479, 0.0206833688, 367 | 2.257354, 0.15737118, 0.0109039174, 368 | 2.257354, 0.03229076, 0.0069754088, 369 | 2.821692, 0.05834525, 0.0003642595 370 | ], 3, 8).t(); 371 | 372 | // partial decomposition - compare with results from R 373 | const m1 = simpls(Xp, Yp, 3); 374 | 375 | // decomposition of X 376 | expect(m1.R.apply(Math.abs, 0)).to.be.deep.almost.equal(R1); 377 | expect(m1.P.apply(Math.abs, 0)).to.be.deep.almost.equal(P1); 378 | expect(m1.T.apply(Math.abs, 0)).to.be.deep.almost.equal(T1); 379 | 380 | // decomposition of Y 381 | expect(m1.C.apply(Math.abs, 0)).to.be.deep.almost.equal(C1); 382 | expect(m1.U.apply(Math.abs, 0)).to.be.deep.almost.equal(U1); 383 | 384 | // full decomposition - check that X = TP' and Y = UC' 385 | const m2 = simpls(Xp, Yp, 4); 386 | // residual variance for X is almost 0 387 | expect(tcrossprod(m2.T, m2.P)).to.be.deep.almost.equal(Xp); 388 | // residual variance for Y is below 5% 389 | expect(sum(Yp.subtract(tcrossprod(m2.T, m2.C)).apply(v => v * v, 0).v) / sum(Yp.apply(v => v * v, 0).v) < 0.05).to.been.true; 390 | 391 | }); 392 | }); 393 | 394 | describe('Tests for PCR methods.', function () { 395 | 396 | it ('tests for method "pcrfit"', function () { 397 | 398 | // common dataset for all PCR tests 399 | const data = matrix([ 400 | 36, 166, 47, 28000, 112, 401 | 43, 180, 80, 30000, 129, 402 | 44, 180, 81, 42000, 113, 403 | 36, 166, 49, 14000, 123, 404 | 43, 181, 75, 31000, 105, 405 | 42, 175, 67, 38000, 105, 406 | 35, 160, 48, 31000, 129 407 | ], 5, 7).t(); 408 | 409 | const [Xc, Yc] = splitregdata(data); 410 | 411 | // make PCA and PCR for centered and scaled data and check all results in full 412 | const ncomp = 3; 413 | const m1 = pcrfit(Xc, Yc, ncomp, true, true); 414 | const m2 = pcafit(Xc, ncomp, true, true); 415 | const C1 = matrix([0.5953372, -0.2370964, 0.1571044], 1, ncomp); 416 | 417 | // check that PCA part is correct 418 | expect(m1.P.apply(Math.abs, 0)).to.be.deep.almost.equal(m2.P.apply(Math.abs, 0)); 419 | expect(m1.eigenvals).to.be.deep.almost.equal(m2.eigenvals); 420 | expect(m1.mX).to.be.deep.almost.equal(m2.mX); 421 | expect(m1.sX).to.be.deep.almost.equal(m2.sX); 422 | expect(m1.center).to.be.deep.almost.equal(m2.center); 423 | expect(m1.scale).to.be.deep.almost.equal(m2.scale); 424 | expect(m1.h0).to.be.deep.almost.equal(m2.h0); 425 | expect(m1.q0).to.be.deep.almost.equal(m2.q0); 426 | expect(m1.Nq).to.be.deep.almost.equal(m2.Nq); 427 | expect(m1.Nh).to.be.deep.almost.equal(m2.Nh); 428 | 429 | // check that PCR part is correct 430 | expect(m1.C.v.map(v => Math.abs(v))).to.be.deep.almost.equal(C1.v.map(v => Math.abs(v))); 431 | expect(m1.mY).to.be.deep.equal(Yc.apply(mean, 2)); 432 | expect(m1.sY).to.be.deep.equal(Yc.apply(sd, 2)); 433 | 434 | 435 | // make PCR for centered and non-scaled data and check only results PCR results 436 | const m3 = pcrfit(Xc, Yc, ncomp, true, false); 437 | const C3 = matrix([0.0002877219, -0.2121022329, 0.0296248313], 1, ncomp); 438 | expect(m3.C.v.map(v => Math.abs(v))).to.be.deep.almost.equal(C3.v.map(v => Math.abs(v))); 439 | expect(m3.mY).to.be.deep.equal(Yc.apply(mean)); 440 | expect(m3.sY).to.be.deep.equal(vector([1])); 441 | }); 442 | 443 | it ('tests for method "pcrpredict"', function () { 444 | 445 | // common dataset for all PCR tests 446 | const data = matrix([ 447 | 36, 166, 47, 28000, 112, 448 | 43, 180, 80, 30000, 129, 449 | 44, 180, 81, 42000, 113, 450 | 36, 166, 49, 14000, 123, 451 | 43, 181, 75, 31000, 105, 452 | 42, 175, 67, 38000, 105, 453 | 35, 160, 48, 31000, 129 454 | ], 5, 7).t(); 455 | 456 | const [Xc, Yc] = splitregdata(data); 457 | 458 | // make PCA and PCR for centered and scaled data and check all results in full 459 | const m1 = pcrfit(Xc, Yc, 3, true, true); 460 | const m2 = pcafit(Xc, 3, true, true); 461 | 462 | const r1 = pcrpredict(m1, Xc, Yc); 463 | const r2 = pcapredict(m2, Xc); 464 | 465 | // check that PCA part is correct 466 | expect(r2.T.apply(Math.abs, 0)).to.be.deep.almost.equal(r1.T.apply(Math.abs, 0)); 467 | expect(r2.H).to.be.deep.almost.equal(r1.H); 468 | expect(r2.Q).to.be.deep.almost.equal(r1.Q); 469 | expect(r2.expvar).to.be.deep.almost.equal(r1.expvar); 470 | expect(r2.cumexpvar).to.be.deep.almost.equal(r1.cumexpvar); 471 | 472 | }); 473 | 474 | }); 475 | 476 | describe('Tests for PCA methods.', function () { 477 | 478 | it ('tests for method "pcafit"', function () { 479 | 480 | const X1 = Matrix.rand(50, 10); 481 | const mX1 = X1.apply(mean, 2); 482 | const m11 = pcafit(X1); 483 | const m12 = svd(X1.subtract(mX1)); 484 | const T1 = m12.U.dot(Matrix.diagm(m12.s)); 485 | 486 | expect(m11.P.apply(Math.abs, 0)).to.be.deep.almost.equal(m12.V.apply(Math.abs, 0)); 487 | expect(m11.mX).to.be.deep.almost.equal(mX1); 488 | expect(m11.sX).to.be.deep.almost.equal(Vector.ones(X1.ncols)); 489 | expect(m11.eigenvals).to.be.deep.almost.equal(T1.apply(variance, 2)); 490 | expect(m11.center).to.be.true; 491 | expect(m11.scale).to.be.false; 492 | expect(m11.ncomp).to.be.equal(10); 493 | 494 | const X2 = Matrix.rand(10, 50); 495 | const mX2 = X2.apply(mean, 2); 496 | const sX2 = X2.apply(sd, 2); 497 | const m21 = pcafit(X2, 5, true, true); 498 | const m22 = svd(X2.subtract(mX2).divide(sX2), 5); 499 | const T2 = m22.U.dot(Matrix.diagm(m22.s)); 500 | 501 | expect(m21.P.apply(Math.abs, 0)).to.be.deep.almost.equal(m22.V.apply(Math.abs, 0)); 502 | expect(m21.mX).to.be.deep.almost.equal(mX2); 503 | expect(m21.sX).to.be.deep.almost.equal(sX2); 504 | expect(m21.eigenvals).to.be.deep.almost.equal(T2.apply(variance, 2)); 505 | expect(m21.center).to.be.true; 506 | expect(m21.scale).to.be.true; 507 | expect(m21.ncomp).to.be.equal(5); 508 | 509 | // compare with results from R 510 | const X3 = matrix([ 511 | 166, 180, 157, 180, 166, 181, 175, 160, 512 | 47, 80, 47, 81, 49, 75, 67, 48, 513 | 28000, 30000, 32000, 42000, 14000, 31000, 38000, 31000, 514 | 112, 129, 127, 113, 123, 105, 105, 129 515 | ], 8, 4); 516 | 517 | const P3 = matrix([ 518 | 0.5732978, -0.3559463, -0.2788432, 0.6832850, 519 | 0.5707159, -0.4525553, 0.0850746, -0.6798818, 520 | 0.4182815, 0.4024368, 0.7935761, 0.1825437, 521 | -0.4131024, -0.7117166, 0.5340840, 0.1938042 522 | ], 4, 4).t(); 523 | 524 | const eigenvals3 = vector([2.594793132, 0.714955083, 0.682758553, 0.007493231]); 525 | 526 | // moments based 527 | const h03m = vector([0.875000, 1.750000, 2.625000, 3.500000]); 528 | const q03m = vector([1.229556, 0.6039703, 0.006556578, 0.0]); 529 | const Nh3m = vector([6.0, 6.0, 12.0, 16.0]); 530 | const Nq3m = vector([4.0, 3.0, 1.0, 4.0]); 531 | 532 | // robust based 533 | const h03r = vector([1.041937, 1.484588, 2.282291, 3.934952]); 534 | const q03r = vector([0.977954, 0.671042, 0.005573, 0.0]); 535 | const Nh3r = vector([13.0, 19.0, 20.0, 11.0]); 536 | const Nq3r = vector([11.0, 5.0, 2.0, 7.0]); 537 | 538 | const m3 = pcafit(X3, 4, true, true); 539 | 540 | expect(m3.P.apply(Math.abs, 0)).to.be.deep.almost.equal(P3.apply(Math.abs, 0)); 541 | expect(m3.eigenvals).to.be.deep.almost.equal(eigenvals3); 542 | 543 | expect(m3.qParams['classic'][1].slice(1, 3)).to.be.deep.almost.equal(Nq3m.slice(1, 3)); 544 | expect(m3.hParams['classic'][1]).to.be.deep.almost.equal(Nh3m); 545 | expect(m3.hParams['classic'][0]).to.be.deep.almost.equal(h03m); 546 | expect(m3.qParams['classic'][0]).to.be.deep.almost.equal(q03m); 547 | 548 | chai.use(chaiAlmost(0.01)); 549 | expect(m3.qParams['robust'][1].slice(1, 3)).to.be.deep.almost.equal(Nq3r.slice(1, 3)); 550 | expect(m3.hParams['robust'][1]).to.be.deep.almost.equal(Nh3r); 551 | expect(m3.hParams['robust'][0]).to.be.deep.almost.equal(h03r); 552 | expect(m3.qParams['robust'][0]).to.be.deep.almost.equal(q03r); 553 | chai.use(chaiAlmost(0.001)); 554 | 555 | }); 556 | 557 | it ('tests for method "pcapredict"', function () { 558 | 559 | const X1 = Matrix.rand(50, 10); 560 | const m11 = pcafit(X1); 561 | const mX1 = X1.apply(mean, 2); 562 | const m12 = svd(X1.subtract(mX1)); 563 | const T1 = m12.U.dot(Matrix.diagm(m12.s)); 564 | 565 | const r1 = pcapredict(m11, X1); 566 | const q12 = X1.subtract(mX1) 567 | .subtract(tcrossprod(r1.T.subset([], [1, 2]), m11.P.subset([], [1, 2]))) 568 | .apply(v => v * v, 0) 569 | .apply(sum, 1); 570 | const h12 = m12.U.subset([], [1, 2]) 571 | .apply(v => v * v, 0) 572 | .apply(sum, 1) 573 | .apply(v => v * (X1.nrows -1)); 574 | 575 | expect(r1.T.apply(Math.abs, 0)).to.be.deep.almost.equal(T1.apply(Math.abs, 0)); 576 | expect(r1.Q.getcolumn(2)).to.be.deep.almost.equal(q12); 577 | expect(r1.H.getcolumn(2)).to.be.deep.almost.equal(h12); 578 | 579 | const X2 = Matrix.rand(10, 50); 580 | const mX2 = X2.apply(mean, 2); 581 | const sX2 = X2.apply(sd, 2); 582 | const m21 = pcafit(X2, 5, true, true); 583 | const m22 = svd(X2.subtract(mX2).divide(sX2), 5); 584 | const T2 = m22.U.dot(Matrix.diagm(m22.s)); 585 | 586 | const r2 = pcapredict(m21, X2); 587 | const q22 = X2.subtract(mX2).divide(sX2). 588 | subtract(tcrossprod(r2.T.subset([], [1, 2]), m21.P.subset([], [1, 2]))).apply(v => v * v, 0).apply(sum, 1); 589 | const h22 = m22.U.subset([], [1, 2]).apply(v => v * v, 0).apply(sum, 1).apply(v => v * (X2.nrows -1)); 590 | 591 | expect(r2.T.apply(Math.abs, 0)).to.be.deep.almost.equal(T2.apply(Math.abs, 0)); 592 | expect(r2.Q.getcolumn(2)).to.be.deep.almost.equal(q22); 593 | expect(r2.H.getcolumn(2)).to.be.deep.almost.equal(h22); 594 | 595 | 596 | // compare with results from R 597 | const X3 = matrix([ 598 | 166, 180, 157, 180, 166, 181, 175, 160, 599 | 47, 80, 47, 81, 49, 75, 67, 48, 600 | 28000, 30000, 32000, 42000, 14000, 31000, 38000, 31000, 601 | 112, 129, 127, 113, 123, 105, 105, 129 602 | ], 8, 4); 603 | 604 | const T3 = matrix([ 605 | -0.7231736, 0.87047760, -0.51644930, 0.143706174, 606 | 0.7468125, -1.68232514, 0.32979598, 0.062205787, 607 | -1.6567570, 0.36752606, 0.90839404, -0.126899054, 608 | 2.0362791, -0.01931637, 0.67077094, -0.014230821, 609 | -1.8047465, -0.63358644, -1.29333991, -0.049380702, 610 | 1.6340299, 0.12882816, -0.87065320, -0.076003459, 611 | 1.3393512, 0.92824983, -0.06167086, 0.002243734, 612 | -1.5717957, 0.04014630, 0.83315232, 0.058358340 613 | ], 4, 8).t(); 614 | 615 | const H3 = matrix([ 616 | 0.2015498, 1.2613804, 1.652031, 4.408047, 617 | 0.2149415, 4.1735369, 4.332840, 4.849247, 618 | 1.0578276, 1.2467561, 2.455353, 4.604408, 619 | 1.5979819, 1.5985038, 2.257498, 2.284524, 620 | 1.2552483, 1.8167267, 4.266682, 4.592103, 621 | 1.0290045, 1.0522181, 2.162474, 2.933374, 622 | 0.6913312, 1.8965087, 1.902079, 1.902751, 623 | 0.9521151, 0.9543694, 1.971043, 2.425546 624 | ], 4, 8).t(); 625 | 626 | const Q3 = matrix([ 627 | 1.0451026, 0.28737135, 2.065146e-02, 0.0, 628 | 2.9428528, 0.11263495, 3.869560e-03, 0.0, 629 | 0.9763585, 0.84128309, 1.610337e-02, 0.0, 630 | 0.4505093, 0.45013617, 2.025163e-04, 0.0, 631 | 2.0765984, 1.67516659, 2.438454e-03, 0.0, 632 | 0.7804102, 0.76381352, 5.776526e-03, 0.0, 633 | 0.8654561, 0.00380833, 5.034343e-06, 0.0, 634 | 0.6991602, 0.69754849, 3.405696e-03, 0.0 635 | ], 4, 8).t(); 636 | 637 | const cumexpvar3 = vector([64.86983, 82.74371, 99.81267, 100.00000]); 638 | const expvar3 = vector([64.8698283, 17.8738771, 17.0689638, 0.1873308]); 639 | const m3 = pcafit(X3, 4, true, true); 640 | const r3 = pcapredict(m3, X3) 641 | 642 | expect(r3.T.apply(Math.abs, 0)).to.be.deep.almost.equal(T3.apply(Math.abs, 0)); 643 | expect(r3.Q).to.be.deep.almost.equal(Q3); 644 | expect(r3.H).to.be.deep.almost.equal(H3); 645 | expect(r3.expvar).to.be.deep.almost.equal(expvar3); 646 | expect(r3.cumexpvar).to.be.deep.almost.equal(cumexpvar3) 647 | 648 | // same model new prediction set 649 | const X4 = matrix([ 650 | 168, 52, 23500, 100, 651 | 166, 49, 14000, 123 652 | ], 4, 2).t(); 653 | 654 | const T4 = matrix([ 655 | -0.1700773, 1.2564700, -1.603759, -0.2578108, 656 | -1.8047465, -0.6335864, -1.293340, -0.0493807 657 | ], 4, 2).t(); 658 | 659 | const H4 = matrix([ 660 | 0.01114782, 2.219282, 5.986415, 14.856606, 661 | 1.25524835, 1.816727, 4.266682, 4.592103 662 | ], 4, 2).t(); 663 | 664 | const Q4 = matrix([ 665 | 4.217225, 2.638508, 0.066466391, 1.195617e-30, 666 | 2.076598, 1.675167, 0.002438454, 1.103173e-30 667 | ], 4, 2).t(); 668 | 669 | const cumexpvar4 = vector([34.30150, 54.97142, 99.28073, 100.00000]); 670 | const expvar4 = vector([34.3015044, 20.6699134, 44.3093144, 0.7192678]); 671 | const r4 = pcapredict(m3, X4); 672 | 673 | expect(r4.T.apply(Math.abs, 0)).to.be.deep.almost.equal(T4.apply(Math.abs, 0)); 674 | expect(r4.Q).to.be.deep.almost.equal(Q4); 675 | expect(r4.H).to.be.deep.almost.equal(H4); 676 | expect(r4.expvar).to.be.deep.almost.equal(expvar4); 677 | expect(r4.cumexpvar).to.be.deep.almost.equal(cumexpvar4); 678 | 679 | }); 680 | 681 | }); 682 | 683 | describe('Tests for polynomial regression methods.', function () { 684 | 685 | it ('tests for method "polyfit".', function () { 686 | 687 | const x = vector([1, 2, 3, 4, 5]); 688 | const y = vector([11, 14, 19, 26, 35]); 689 | 690 | expect(() => polyfit(x, y, 5)).to.throw(Error, 'polyfit: polynomial degree "d" must a positive value smaller than number of measurements.'); 691 | expect(() => polyfit(cbind(x, x), y, 1)).to.throw(Error, 'polymat: argument "x" must be a vector.'); 692 | 693 | const m1 = polyfit(x, y, 3); 694 | 695 | // check class and polynomial degree 696 | m1.class.should.be.equal("pm"); 697 | m1.pdegree.should.be.equal(3); 698 | 699 | // check coefficients and related statistics 700 | m1.coeffs.estimate.length.should.be.equal(4); 701 | expect(m1.coeffs.estimate).to.be.deep.almost(vector([10, 0, 1, 0])); 702 | expect(m1.coeffs.p.apply(v => v < 0.10)).to.be.deep.almost(vector([1, 0, 1, 0])); 703 | }); 704 | 705 | it ('tests for method "polypredict".', function () { 706 | const x = vector([1, 2, 3, 4, 5]); 707 | const y = vector([11, 14, 19, 26, 35]); 708 | const m = polyfit(x, y, 3); 709 | 710 | const yp = polypredict(m, x); 711 | expect(yp).to.be.deep.almost(y); 712 | }); 713 | 714 | }); 715 | 716 | describe('Tests for lm (MLR) methods.', function () { 717 | 718 | it ('tests for method "lmfit".', function () { 719 | 720 | // errors 721 | expect(() => lmfit(1, 2)).to.throw(Error, 722 | 'lmfit: argument "X" must be a matrix or a vector.'); 723 | expect(() => lmfit(vector([1]), 2)).to.throw(Error, 724 | 'lmfit: argument "y" must be a vector.'); 725 | expect(() => lmfit(vector([1]), vector([2]))).to.throw(Error, 726 | 'lmfit: number of objects must be larger than number of predictors.'); 727 | expect(() => lmfit(vector([1, 2]), vector([2, 3, 4]))).to.throw(Error, 728 | 'lmfit: arguments "X" and "y" must have the same number of objects.'); 729 | 730 | // vector - vector 731 | const X = vector([1, 2, 3, 4, 5]); 732 | const y = vector([10, 19, 31, 39, 55]); 733 | const m = lmfit(X, y); 734 | 735 | m.class.should.be.equal('lm'); 736 | 737 | // check coefficients and related statistics 738 | m.coeffs.estimate.length.should.be.equal(2); 739 | expect(m.coeffs.estimate).to.be.deep.almost(vector([-2.2, 11.0])); 740 | expect(m.coeffs.se).to.be.deep.almost(vector([2.3295, 0.7024])); 741 | expect(m.coeffs.tstat).to.be.deep.almost(vector([-0.944, 15.661])); 742 | expect(m.coeffs.p).to.be.deep.almost(vector([0.4146, 0.000566])); 743 | 744 | // check predictions 745 | m.fitted.length.should.be.equal(y.length); 746 | expect(m.fitted).to.be.deep.almost(vector([8.8, 19.8, 30.8, 41.8, 52.8])); 747 | 748 | // check performance statistics 749 | m.stat.DoF.should.be.equal(3); 750 | expect(m.stat.se).to.be.almost(2.221); 751 | expect(m.stat.R2).to.be.almost(0.9879); 752 | expect(m.stat.R2adj).to.be.almost(0.9839); 753 | expect(m.stat.Fstat).to.be.almost(245.270); 754 | expect(m.stat.p).to.be.almost(0.0005658); 755 | 756 | // matrix - vector 757 | const X2 = matrix([1, 2, 3, 4, 5, 10, 20, 10, 20, 10], 5, 2); 758 | const y2 = vector([10, 19, 31, 39, 55]); 759 | const m2 = lmfit(X2, y2); 760 | 761 | m.class.should.be.equal("lm"); 762 | 763 | // check coefficients and related statistics 764 | m2.coeffs.estimate.length.should.be.equal(3); 765 | expect(m2.coeffs.estimate).to.be.deep.almost(vector([2.0, 11.0, -0.3])); 766 | expect(m2.coeffs.se).to.be.deep.almost(vector([2.3381, 0.4472, 0.1291])); 767 | expect(m2.coeffs.tstat).to.be.deep.almost(vector([0.855, 24.597, -2.324])); 768 | expect(m2.coeffs.p).to.be.deep.almost(vector([0.48245, 0.00165, 0.14576])); 769 | 770 | // check predictions 771 | m2.fitted.length.should.be.equal(y.length); 772 | expect(m2.fitted).to.be.deep.almost(vector([10, 18, 32, 40, 54])); 773 | 774 | // check performance statistics 775 | m2.stat.DoF.should.be.equal(2); 776 | expect(m2.stat.se).to.be.almost(1.414); 777 | expect(m2.stat.R2).to.be.almost(0.9967); 778 | expect(m2.stat.R2adj).to.be.almost(0.9935); 779 | expect(m2.stat.Fstat).to.be.almost(305.2); 780 | expect(m2.stat.p).to.be.almost(0.003266); 781 | 782 | }); 783 | 784 | it('tests for method "lmpredict".', function () { 785 | 786 | // vector - vector 787 | const X1 = vector([1, 2, 3, 4, 5]); 788 | const y1 = vector([10, 19, 31, 39, 55]); 789 | const m1 = lmfit(X1, y1); 790 | 791 | // errors 792 | expect(() => lmpredict({}, 2)).to.throw(Error, 'lmpredict: argument "X" must be a matrix or a vector.'); 793 | expect(() => lmpredict({}, vector([2, 3]))).to.throw(Error, 'lmpredict: argument "m" must be object with "lm" model.'); 794 | expect(() => lmpredict(m1, matrix([2, 3], 1, 2))).to.throw(Error, 'lmpredict: number of columns in "X" does not match number of coefficients in model.'); 795 | 796 | const yp1 = lmpredict(m1, X1) 797 | yp1.should.be.eql(m1.fitted); 798 | 799 | // matrix - vector 800 | const X2 = matrix([1, 2, 3, 4, 5, 10, 20, 10, 20, 10], 5, 2); 801 | const y2 = vector([10, 19, 31, 39, 55]); 802 | const m2 = lmfit(X2, y2); 803 | 804 | const yp2 = lmpredict(m2, X2) 805 | yp2.should.be.eql(m2.fitted); 806 | }); 807 | 808 | }); 809 | -------------------------------------------------------------------------------- /test/tests-prep.js: -------------------------------------------------------------------------------- 1 | /***************************************************************** 2 | * Tests for preprocessing methods * 3 | *****************************************************************/ 4 | 5 | // import dependencies 6 | import { default as chai} from 'chai'; 7 | import { default as chaiAlmost} from 'chai-almost'; 8 | import { vector, ismatrix, Vector, Matrix } from '../src/arrays/index.js'; 9 | import { sd, mean } from '../src/stat/index.js'; 10 | 11 | // import methods to test 12 | import { scale, unscale } from '../src/prep/index.js'; 13 | 14 | 15 | // set up test settings 16 | const expect = chai.expect; 17 | chai.use(chaiAlmost(0.0001)); 18 | 19 | describe('Tests of preprocessing methods.', function () { 20 | 21 | it ('tests for method "scale" for matrices', function () { 22 | 23 | const X1 = Matrix.rand(500, 10, 0, 2); 24 | const mX1 = X1.apply(mean, 2); 25 | const sX1 = X1.apply(sd, 2); 26 | 27 | // short version 28 | const X1p11 = scale(X1); 29 | const X1p12 = scale(X1, true, false); 30 | const X1p13 = scale(X1, true, true); 31 | const X1p14 = scale(X1, false, true); 32 | 33 | expect(X1p11.v).to.be.deep.almost.equal(X1p12.v); 34 | expect(X1p12.apply(mean, 2)).to.be.deep.almost.equal(Vector.zeros(10)); 35 | expect(X1p12.apply(sd, 2)).to.be.deep.almost.equal(sX1); 36 | expect(X1p13.apply(mean, 2)).to.be.deep.almost.equal(Vector.zeros(10)); 37 | expect(X1p13.apply(sd, 2)).to.be.deep.almost.equal(Vector.ones(10)); 38 | expect(X1p14.apply(mean, 2)).to.be.deep.almost.equal(mX1.divide(sX1)); 39 | expect(X1p14.apply(sd, 2)).to.be.deep.almost.equal(Vector.ones(10)); 40 | 41 | // full version 42 | const [X1p22, mp22, sp22] = scale(X1, true, false, true); 43 | const [X1p23, mp23, sp23] = scale(X1, true, true, true); 44 | const [X1p24, mp24, sp24] = scale(X1, false, true, true); 45 | 46 | expect(ismatrix(X1p22)) 47 | expect(mp22).to.be.deep.almost.equal(mX1); 48 | expect(sp22).to.be.deep.almost.equal(Vector.ones(10)); 49 | expect(mp23).to.be.deep.almost.equal(mX1); 50 | expect(sp23).to.be.deep.almost.equal(sX1); 51 | expect(mp24).to.be.deep.almost.equal(Vector.zeros(10)); 52 | expect(sp24).to.be.deep.almost.equal(sX1); 53 | }); 54 | 55 | it ('tests for method "unscale" for matrices', function () { 56 | 57 | const X1 = Matrix.rand(500, 5, 0, 2); 58 | const [X1s, mX1, sX1] = scale(X1, true, true, true); 59 | const X1u = unscale(X1s, mX1, sX1); 60 | expect(X1u.v).to.be.deep.almost.equal(X1.v); 61 | expect(X1s.v).to.be.deep.almost.not.equal(X1.v); 62 | 63 | const mX2 = vector([1, 2, 3, 4, 5]); 64 | const sX2 = vector([0.5, 0.4, 0.3, 0.2, 0.1]); 65 | const X2 = Matrix.rand(500, 5, 0, 2); 66 | const X2s = scale(X2, mX2, sX2); 67 | const X2u = unscale(X2s, mX2, sX2); 68 | expect(X2u.v).to.be.deep.almost.equal(X2.v); 69 | expect(X2s.v).to.be.deep.almost.not.equal(X2.v); 70 | 71 | }); 72 | }); 73 | 74 | -------------------------------------------------------------------------------- /test/tests-speed.js: -------------------------------------------------------------------------------- 1 | /****************************************************************/ 2 | /* Tests for array methods (Index/Vector/Matrix classes) */ 3 | /****************************************************************/ 4 | 5 | 6 | // Speed tests for operations with two 10.000 x 10.000 matrices: 7 | // --------------------------------------------------------------- 8 | // sum - done in: 0.152 s. 9 | // difference - done in: 0.964 s. 10 | // division - done in: 0.981 s. 11 | // multiplication - done in: 0.951 s. 12 | // transposition - done in: 0.331 s. 13 | 14 | // Speed tests for operations with two 2.000 x 2.000 matrices: 15 | // ------------------------------------------------------------- 16 | // tcrossprod - done in: 7.808 s. 17 | // crossprod - done in: 7.336 s. 18 | // dot - done in: 6.76 s. 19 | 20 | // Speed tests for decomposition of 2000 x 2000 matrix: 21 | // ---------------------------------------------------- 22 | // qr - done in: 30.673 s. 23 | // lu - done in: 4.524 s. 24 | 25 | // Speed tests for decomposition of 200 x 200 matrix: 26 | // ---------------------------------------------------- 27 | // bidiag - done in: 1.535 s. 28 | // svd - done in: 290.863 s. 29 | // rsvd - done in: 0.979 s. 30 | 31 | // Speed tests for modelling with 500 x 1000 matrix: 32 | // --------------------------------------------------- 33 | // pcafit - done in: 4.111 s. 34 | // pcapredict - done in: 0.03 s. 35 | // pcrfit - done in: 4.219 s. 36 | // pcrpredict - done in: 0.029 s. 37 | // plsfit - done in: 1.373 s. 38 | // plspredict - done in: 0.026 s. 39 | 40 | 41 | // import methods 42 | import { Matrix, tcrossprod, crossprod } from '../src/arrays/index.js'; 43 | import { bidiag, rsvd, svd, qr, lu } from '../src/decomp/index.js'; 44 | import { pcafit, pcapredict, plsfit, plspredict, pcrfit, pcrpredict } from '../src/models/index.js'; 45 | 46 | function measure(f, msg = '') { 47 | const start = Date.now(); 48 | const out = f() 49 | console.log(msg + ' - done in: ' + (Date.now() - start) / 1000 + ' s.') 50 | return out; 51 | } 52 | 53 | const X1 = Matrix.rand(10000, 10000); 54 | const Y1 = Matrix.rand(10000, 10000); 55 | const X2 = Matrix.rand(2000, 2000); 56 | const Y2 = Matrix.rand(2000, 2000); 57 | const X3 = Matrix.rand(200, 200); 58 | const X4 = Matrix.rand(500, 1000); 59 | const Y4 = Matrix.rand(500, 1); 60 | 61 | console.log('\nSpeed tests for operations with two 10.000 x 10.000 matrices:'); 62 | console.log('---------------------------------------------------------------'); 63 | measure( () => X1.add(Y1), 'sum'); 64 | measure( () => X1.subtract(Y1), 'difference'); 65 | measure( () => X1.divide(Y1), 'division'); 66 | measure( () => X1.mult(Y1), 'multiplication'); 67 | measure( () => X1.t(), 'transposition'); 68 | 69 | 70 | console.log('\nSpeed tests for operations with two 2.000 x 2.000 matrices:'); 71 | console.log('-------------------------------------------------------------'); 72 | measure( () => tcrossprod(X2, Y2), 'tcrossprod'); 73 | measure( () => crossprod(X2, Y2), 'crossprod'); 74 | measure( () => X2.dot(Y2), 'dot'); 75 | 76 | console.log('\nSpeed tests for decomposition of 2000 x 2000 matrix:'); 77 | console.log('----------------------------------------------------'); 78 | measure( () => qr(X2), 'qr'); 79 | measure( () => lu(X2), 'lu'); 80 | 81 | console.log('\nSpeed tests for decomposition of 200 x 200 matrix:'); 82 | console.log('----------------------------------------------------'); 83 | measure( () => bidiag(X3), 'bidiag'); 84 | measure( () => svd(X3, 20), 'svd'); 85 | measure( () => rsvd(X3, 20), 'rsvd'); 86 | 87 | 88 | console.log('\nSpeed tests for modelling with 500 x 1000 matrix:'); 89 | console.log('---------------------------------------------------'); 90 | 91 | const mpca = measure( () => pcafit(X4, 20), 'pcafit'); 92 | measure( () => pcapredict(mpca, X4), 'pcapredict'); 93 | const mpcr = measure( () => pcrfit(X4, Y4, 20), 'pcrfit'); 94 | measure( () => pcrpredict(mpcr, X4), 'pcrpredict'); 95 | const mpls = measure( () => plsfit(X4, Y4, 20), 'plsfit'); 96 | measure( () => plspredict(mpls, X4, Y4), 'plspredict'); 97 | 98 | console.log(''); 99 | -------------------------------------------------------------------------------- /test/tests-stat.js: -------------------------------------------------------------------------------- 1 | /****************************************************************/ 2 | /* Tests for methods computing various statistics */ 3 | /****************************************************************/ 4 | 5 | // import dependencies 6 | import {default as chai} from 'chai'; 7 | import {default as chaiAlmost} from 'chai-almost'; 8 | import { isvector, vector, Vector } from '../src/arrays/index.js'; 9 | 10 | // import methods to test 11 | import { 12 | norm2, quantile, count, mids, split, diff, ppoints, cumsum, 13 | cor, cov, skewness, kurtosis, rank, mean, sd, sum, prod, min, minind, max, maxind, 14 | median, iqr 15 | } from '../src/stat/index.js'; 16 | 17 | // set up test settings 18 | const should = chai.should(); 19 | const expect = chai.expect; 20 | chai.use(chaiAlmost(0.00001)); 21 | 22 | 23 | 24 | describe('Tests of stat methods.', function () { 25 | 26 | // mix of negative and positive numbers - main dataset for tests 27 | const mn1 = -2.008; // min 28 | const mx1 = 2.001; // max 29 | const sm1 = -0.007; // sum 30 | const av1 = -0.001; // average/mean 31 | const sd1 = 1.733785; // std (/n-1) 32 | const sd1b = 1.605173; // std biased (/n) 33 | 34 | 35 | const x1 = vector([mx1, 2, 1, 0, -1, -2, mn1]); 36 | 37 | // only negative numbers 38 | const mx2 = -0.001; 39 | const mn2 = -2.003; 40 | const sm2 = -5.004; 41 | const av2 = -1.251; 42 | const x2 = vector([mx2, -1, -2, mn2]); 43 | 44 | // only positive numbers 45 | const mn3 = 0.001; 46 | const mx3 = 2.003; 47 | const sm3 = 5.004; 48 | const av3 = 1.251; 49 | const x3 = vector([mn3, 1, 2, mx3]); 50 | 51 | // vectors for percentiles, etc. 52 | const xp = vector([2.001, 2, 1, 0, -1, -2, -2.008]); 53 | const Q1 = -1.5; // 25% percentile for x1 54 | const Q2 = 0; // 50% percentile for x1 55 | const Q3 = 1.5; // 75% percentile for x1 56 | const P10 = -2.0032; // 10% percentile for x1 57 | const P90 = 2.0004; // 90% percentile for x1 58 | 59 | const xp1 = vector([-10, -2, 0, 2, 10, 20, 50, 100, 150]); 60 | const xp2 = vector([150, -2, 100, 2, 50, 0, 10, 20, -10]); 61 | const xp3 = vector([150, 100, 50, 20, 10, 2, 0, -2, -10]); 62 | 63 | // large random vector 64 | const yp = Vector.rand(1000000, 10, 20); 65 | 66 | it('tests for method "iqr".', function () { 67 | iqr(xp).should.be.a('number'); 68 | iqr(xp).should.equal(quantile(xp, 0.75) - quantile(xp, 0.25)); 69 | }); 70 | 71 | it('tests for method "median".', function () { 72 | median(xp).should.be.a('number'); 73 | median(xp).should.equal(quantile(xp, 0.5)); 74 | median(yp).should.equal(quantile(yp, 0.5)); 75 | }); 76 | 77 | it('tests for method "norm2".', function () { 78 | const x1 = vector([1, 2, 3, 4]); 79 | const x2 = vector([1, -2, -3, 4]); 80 | const x3 = vector([0, 0, 0, 0]); 81 | const x4 = vector([1, 1]); 82 | 83 | expect(norm2(x1)).to.equal(Math.sqrt(30)); 84 | expect(norm2(x2)).to.equal(Math.sqrt(30)); 85 | expect(norm2(x3)).to.equal(0); 86 | expect(norm2(x4)).to.equal(Math.sqrt(2)); 87 | }); 88 | 89 | it('tests for method "quantile".', function () { 90 | 91 | expect(() => quantile(xp, -0.01)).to.throw(Error, "Parameter 'p' must be between 0 and 1 (both included)."); 92 | expect(() => quantile(xp, 1.00001)).to.throw(Error, "Parameter 'p' must be between 0 and 1 (both included)."); 93 | 94 | quantile(xp, 0.01).should.be.a('number'); 95 | quantile(xp, 0).should.equal(min(xp)); 96 | quantile(xp, 1).should.equal(max(xp)); 97 | 98 | quantile(xp, 0.25).should.be.closeTo(Q1, 0.000001); 99 | quantile(xp, 0.50).should.be.closeTo(Q2, 0.000001); 100 | quantile(xp, 0.75).should.be.closeTo(Q3, 0.000001); 101 | quantile(xp, 0.10).should.be.closeTo(P10, 0.000001); 102 | quantile(xp, 0.90).should.be.closeTo(P90, 0.000001); 103 | 104 | expect(() => quantile(xp, [0.2, 0.4, -0.01])).to.throw(Error, "Parameter 'p' must be between 0 and 1 (both included)."); 105 | expect(() => quantile(xp, [0.2, 1.00001, 0.4])).to.throw(Error, "Parameter 'p' must be between 0 and 1 (both included)."); 106 | 107 | const p1 = quantile(xp, [0.25, 0.50, 0.75, 0.10, 0.90]); 108 | expect(p1).to.deep.equal(vector([Q1, Q2, Q3, P10, P90])); 109 | 110 | const p2 = quantile(yp, [0.25, 0.50, 0.75]); 111 | p2.v[0].should.be.closeTo(12.5, 0.1); 112 | p2.v[1].should.be.closeTo(15.0, 0.1); 113 | p2.v[2].should.be.closeTo(17.5, 0.1); 114 | 115 | }); 116 | 117 | it('tests for method "count".', function () { 118 | 119 | const bins1 = [-20, 0, 200]; 120 | const c1 = count(xp1, bins1); 121 | isvector(c1).should.be.true; 122 | c1.v.should.have.lengthOf(2); 123 | c1.v[0].should.equal(2); 124 | c1.v[1].should.equal(7); 125 | 126 | const bins2 = [-20, 0, 50, 200]; 127 | const c2 = count(xp1, bins2); 128 | isvector(c2).should.be.true; 129 | c2.v.should.have.lengthOf(3); 130 | c2.v[0].should.equal(2); 131 | c2.v[1].should.equal(4); 132 | c2.v[2].should.equal(3); 133 | 134 | const bins = [10, 12, 14, 16, 18, 20]; 135 | const c3 = count(yp, bins); 136 | isvector(c3).should.be.true; 137 | c3.v.should.have.lengthOf(5); 138 | c3.v[0].should.be.closeTo(200000, 2000); 139 | c3.v[1].should.be.closeTo(200000, 2000); 140 | c3.v[2].should.be.closeTo(200000, 2000); 141 | c3.v[3].should.be.closeTo(200000, 2000); 142 | c3.v[4].should.be.closeTo(200000, 2000); 143 | 144 | }); 145 | 146 | it ('tests for method "mids".', function () { 147 | const m1 = mids(xp1); 148 | isvector(m1).should.be.true; 149 | m1.should.have.lengthOf(xp1.length - 1); 150 | m1.v[0].should.equal(0.5 * xp1.v[0] + 0.5 * xp1.v[1]); 151 | m1.v[1].should.equal(0.5 * xp1.v[1] + 0.5 * xp1.v[2]); 152 | m1.v[7].should.equal(0.5 * xp1.v[7] + 0.5 * xp1.v[8]); 153 | }); 154 | 155 | it ('tests for method "split".', function () { 156 | const s1 = split(xp1, 2); 157 | isvector(s1).should.be.true; 158 | s1.v.should.have.lengthOf(3); 159 | s1.v[0].should.equal(min(xp1)); 160 | s1.v[1].should.equal(min(xp1) + (max(xp1) - min(xp1)) / 2); 161 | s1.v[2].should.equal(max(xp1)); 162 | 163 | const s2 = split(xp1, 4); 164 | isvector(s2).should.be.true; 165 | s2.v.should.have.lengthOf(5); 166 | s2.v[0].should.equal(min(xp1)); 167 | s2.v[1].should.equal(min(xp1) + (max(xp1) - min(xp1)) * 0.25); 168 | s2.v[2].should.equal(min(xp1) + (max(xp1) - min(xp1)) * 0.50); 169 | s2.v[3].should.equal(min(xp1) + (max(xp1) - min(xp1)) * 0.75); 170 | s2.v[4].should.equal(max(xp1)); 171 | 172 | const s3 = split(yp, 2); 173 | isvector(s1).should.be.true; 174 | s3.v.should.have.lengthOf(3); 175 | s3.v[0].should.equal(min(yp)); 176 | s3.v[1].should.equal(min(yp) + (max(yp) - min(yp)) / 2); 177 | s3.v[2].should.equal(max(yp)); 178 | 179 | const s4 = split(yp, 100); 180 | isvector(s4).should.be.true; 181 | s4.v.should.have.lengthOf(101); 182 | 183 | }); 184 | 185 | it ('tests for method "diff".', function () { 186 | const d1 = diff(x1); 187 | 188 | isvector(d1).should.be.true; 189 | d1.v.should.have.lengthOf(x1.length - 1); 190 | d1.v[0].should.equal(x1.v[1] - x1.v[0]); 191 | d1.v[1].should.equal(x1.v[2] - x1.v[1]); 192 | d1.v[5].should.equal(x1.v[6] - x1.v[5]); 193 | }); 194 | 195 | it ('tests for method "ppoints".', function () { 196 | 197 | const p1 = ppoints(1); 198 | 199 | expect(p1.v).to.have.lengthOf(1); 200 | p1.v[0].should.be.closeTo(0.5, 0.0000001); 201 | 202 | const p2 = ppoints(3); 203 | expect(p2.v).to.have.lengthOf(3); 204 | p2.v[0].should.be.closeTo(0.1923077, 0.0000001); 205 | p2.v[1].should.be.closeTo(0.5000000, 0.0000001); 206 | p2.v[2].should.be.closeTo(0.8076923, 0.0000001); 207 | 208 | const p3 = ppoints(5); 209 | expect(p3.v).to.have.lengthOf(5); 210 | p3.v[0].should.be.closeTo(0.1190476, 0.0000001); 211 | p3.v[1].should.be.closeTo(0.3095238, 0.0000001); 212 | p3.v[2].should.be.closeTo(0.5000000, 0.0000001); 213 | p3.v[3].should.be.closeTo(0.6904762, 0.0000001); 214 | p3.v[4].should.be.closeTo(0.8809524, 0.0000001); 215 | 216 | const p4 = ppoints(11); 217 | expect(p4.v).to.have.lengthOf(11); 218 | p4.v[ 0].should.be.closeTo(0.04545455, 0.0000001); 219 | p4.v[ 3].should.be.closeTo(0.31818182, 0.0000001); 220 | p4.v[ 5].should.be.closeTo(0.50000000, 0.0000001); 221 | p4.v[ 7].should.be.closeTo(0.68181818, 0.0000001); 222 | p4.v[10].should.be.closeTo(0.95454545, 0.0000001); 223 | }); 224 | 225 | it ('tests for method "cumsum".', function () { 226 | const x1 = vector([1, 10, 20, 100, 300]); 227 | const x2 = vector([1, -10, 20, -100, 300]); 228 | 229 | expect(cumsum(x1)).to.be.deep.equal(vector([1, 11, 31, 131, 431])); 230 | expect(cumsum(x2)).to.be.deep.equal(vector([1, -9, 11, -89, 211])); 231 | }); 232 | 233 | it ('tests for method "rank".', function () { 234 | // i: 1 2 3 4 5 6 7 8 235 | // sorted: -3 0 1 2 3 4 8 9 236 | // rank: 5 1 2 6 3 4 8 7 237 | const x1 = vector([3, -3, 0, 4, 1, 2, 9, 8]); 238 | expect(rank(x1)).to.eql(vector([5, 1, 2, 6, 3, 4, 8, 7])); 239 | 240 | // i: 1 2 3 4 5 6 7 241 | // sorted: -2 -2 -2 1 3 10 11 242 | // rank: 1 5 1 4 6 7 1 243 | const x2 = vector([-2, 3, -2, 1, 10, 11, -2]); 244 | expect(rank(x2)).to.eql(vector([1, 5, 1, 4, 6, 7, 1])); 245 | }); 246 | 247 | it ('tests for methods "minind" and "min"', function () { 248 | 249 | // normal cases 250 | const x1l = vector([10, 1, 2, 3, 100, 0, 5, 500, 200]); 251 | const x2l = vector([-30, -10, -2, -1000, -100, -200]); 252 | const x3l = vector([0, 1, 2, 3, 100, 10, 5, 500, 200]); 253 | const x4l = vector([-1000, -10, -20, -100, -200]); 254 | const x5l = vector([10, 11, 2, 3, 100, 10, 5, 500, 1]); 255 | const x6l = vector([-10, -15, -20, -100, -200, -1000]); 256 | 257 | expect(minind(x1l)).equal(6) 258 | expect(minind(x2l)).equal(4) 259 | expect(minind(x3l)).equal(1) 260 | expect(minind(x4l)).equal(1) 261 | expect(minind(x5l)).equal(9) 262 | expect(minind(x6l)).equal(6) 263 | 264 | expect(min(x1l)).equal(0) 265 | expect(min(x2l)).equal(-1000) 266 | expect(min(x3l)).equal(0) 267 | expect(min(x4l)).equal(-1000) 268 | expect(min(x5l)).equal(1) 269 | expect(min(x6l)).equal(-1000) 270 | 271 | // large vector case 272 | const x7l = Vector.rand(1000000); 273 | x7l.v[99999] = -0.01; 274 | expect(minind(x7l)).equal(100000); 275 | expect(min(x7l)).almost.equal(-0.01); 276 | 277 | // repeated min values - return first index 278 | const x8l = [-1, 3, -5, 10, -11, 3, -2, -11, 3, 2, -11, -10, 1]; 279 | expect(minind(x8l)).equal(5); 280 | expect(min(x8l)).almost.equal(-11); 281 | 282 | }); 283 | 284 | it ('tests for methods "maxind" and "max"', function () { 285 | 286 | // normal cases 287 | const x1 = vector([10, 1, 2, 3, 100, 0, 5, 500, 200]); 288 | const x2 = vector([-30, -10, -2, -1, -100, -200]); 289 | const x3 = vector([1000, 1, 2, 3, 100, 10, 5, 500, 200]); 290 | const x4 = vector([-1, -10, -20, -100, -200]); 291 | const x5 = vector([0, 1, 2, 3, 100, 10, -5, 500, 2000]); 292 | const x6 = vector([-10, -15, -20, -100, -200, -0.00001]); 293 | 294 | expect(maxind(x1)).equal(8) 295 | expect(maxind(x2)).equal(4) 296 | expect(maxind(x3)).equal(1) 297 | expect(maxind(x4)).equal(1) 298 | expect(maxind(x5)).equal(9) 299 | expect(maxind(x6)).equal(6) 300 | 301 | expect(max(x1)).equal(500) 302 | expect(max(x2)).equal(-1) 303 | expect(max(x3)).equal(1000) 304 | expect(max(x4)).equal(-1) 305 | expect(max(x5)).equal(2000) 306 | expect(max(x6)).equal(-0.00001) 307 | 308 | 309 | // large vector case 310 | const x7 = Vector.rand(1000000); 311 | x7.v[99999] = 1.0; 312 | expect(maxind(x7.v)).equal(100000); 313 | expect(max(x7.v)).equal(1.0); 314 | 315 | // repeated min values - return first index 316 | const x8 = [-1, 3, -5, 10, 11, 3, -2, 11, 3, 2, 11, -10, 1]; 317 | expect(maxind(x8)).equal(5); 318 | expect(max(x8)).almost.equal(11); 319 | 320 | }); 321 | 322 | it ('tests for methods "min" and "max" (additional).', function () { 323 | min(x1).should.be.a('number'); 324 | min(x1).should.equal(mn1); 325 | min(x2).should.equal(mn2); 326 | min(x3).should.equal(mn3); 327 | 328 | max(x1).should.be.a('number'); 329 | max(x1).should.equal(mx1); 330 | max(x2).should.equal(mx2); 331 | max(x3).should.equal(mx3); 332 | }); 333 | 334 | it ('tests for methods "sum" and "prod".', function () { 335 | sum(x1).should.be.a('number'); 336 | sum(x1).should.be.closeTo(sm1, 0.0000001); 337 | sum(x2).should.be.closeTo(sm2, 0.0000001); 338 | sum(x3).should.be.closeTo(sm3, 0.0000001); 339 | 340 | prod(vector([1, 2, 3, 4, 5])).should.be.a('number'); 341 | prod(vector([1, 2, 3, 4, 5])).should.be.closeTo(120, 0.0000000001); 342 | prod(vector([10])).should.be.a('number'); 343 | prod(vector([10])).should.be.closeTo(10, 0.0000000001); 344 | }); 345 | 346 | it ('tests for method "mean".', function () { 347 | mean(x1).should.be.a('number'); 348 | mean(x1).should.be.closeTo(av1, 0.0000001); 349 | mean(x2).should.be.closeTo(av2, 0.0000001); 350 | mean(x3).should.be.closeTo(av3, 0.0000001); 351 | }); 352 | 353 | it ('tests for method "sd".', function () { 354 | sd(x1).should.be.a('number'); 355 | sd(x1).should.be.closeTo(sd1, 0.000001); 356 | sd(x1, true).should.be.closeTo(sd1b, 0.000001); 357 | sd(x1, false, mean(x1)).should.be.closeTo(sd1, 0.000001); 358 | }); 359 | 360 | it ('tests for method "skewness".', function() { 361 | skewness([0, 1, 2]).should.be.closeTo(0, 0.0000001); 362 | skewness([-10, 1, 2]).should.be.closeTo(-0.6892055, 0.0000001); 363 | skewness([10, 1, 2]).should.be.closeTo(0.6745555, 0.0000001); 364 | }); 365 | 366 | it ('tests for method "kurtosis".', function() { 367 | kurtosis([1, 2, 3]).should.be.closeTo(1.5, 0.0000001); 368 | }); 369 | 370 | it ('tests for method "cov".', function() { 371 | expect(() => cov(vector([1, 2]), vector([1, 2, 3])).to.throw(Error, "Vectors 'x' and 'y' must have the same length.")); 372 | expect(() => cov(vector([1]), vector([1])).to.throw(Error, "Vectors 'x' and 'y' must have at least two values.")); 373 | 374 | cov(vector([1, 2, 3]), vector([1, 2, 3])).should.equal(1); 375 | cov(vector([1, 2, 3]), vector([3, 2, 1])).should.equal(-1); 376 | cov(vector([1, 2, 3]), vector([10, 20, 30])).should.equal(10); 377 | cov(vector([1, 2, 3]), vector([30, 20, 10])).should.equal(-10); 378 | cov(vector([1, 2, 1, 2]), vector([10, 10, 20, 20])).should.equal(0); 379 | }); 380 | 381 | it ('tests for method "cor".', function() { 382 | cor(vector([1, 2, 3]), vector([1, 2, 3])).should.equal(1); 383 | cor(vector([1, 2, 3]), vector([3, 2, 1])).should.equal(-1); 384 | cor(vector([1, 2, 3]), vector([10, 20, 30])).should.equal(1); 385 | cor(vector([1, 2, 3]), vector([30, 20, 10])).should.equal(-1); 386 | cor(vector([1, 2, 1, 2]), vector([10, 10, 20, 20])).should.equal(0); 387 | }); 388 | 389 | }); 390 | 391 | -------------------------------------------------------------------------------- /test/tests-temp.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // import dependencies 4 | import {default as chai} from 'chai'; 5 | import {default as chaiAlmost} from 'chai-almost'; 6 | import { quantile } from 'mdatools/stat'; 7 | import { crossprod, tcrossprod, reshape, isvector, vector, Vector, 8 | ismatrix, matrix, Matrix, Index } from '../src/arrays/index.js'; 9 | 10 | // import methods to test 11 | import { rsvd, qr, lu, svd, rot, bidiag, householder } from '../src/decomp/index.js'; 12 | import { pchisq, qchisq, gamma, igamma } from '../src/distributions/index.js'; 13 | 14 | // set up test settings 15 | const expect = chai.expect; 16 | chai.use(chaiAlmost(0.0001)); 17 | 18 | const ZERO = Math.pow(10.0, -6); 19 | 20 | describe('Temporary tests.', function () { 21 | 22 | it ('tests for method "bidiag"', function () { 23 | }).timeout(20000); 24 | 25 | 26 | 27 | }); -------------------------------------------------------------------------------- /test/tests-tests.js: -------------------------------------------------------------------------------- 1 | // /****************************************************************** 2 | // * Tests for functions for hypothesis testing * 3 | // ******************************************************************/ 4 | 5 | // import dependencies 6 | import {default as chai} from 'chai'; 7 | import {pnorm} from '../src/distributions/index.js'; 8 | import {vector} from '../src/arrays/index.js'; 9 | 10 | // import of functions to test 11 | import {getpvalue, ttest, ttest2} from '../src/tests/index.js'; 12 | 13 | const should = chai.should(); 14 | const expect = chai.expect; 15 | 16 | describe('Tests of methods for hypothesis testing.', function () { 17 | 18 | it('tests for method "getpvalue".', function () { 19 | 20 | // left tail 21 | getpvalue(pnorm, 0, "left").should.be.closeTo(0.5, 0.00001); 22 | getpvalue(pnorm, 10, "left", [10, 1]).should.be.closeTo(0.5, 0.00001); 23 | getpvalue(pnorm, 8.04, "left", [10, 1]).should.be.closeTo(0.025, 0.00001); 24 | getpvalue(pnorm, 11.96, "left", [10, 1]).should.be.closeTo(0.975, 0.00001); 25 | 26 | // right tail 27 | getpvalue(pnorm, 0, "right").should.be.closeTo(0.5, 0.00001); 28 | getpvalue(pnorm, 10, "right", [10, 1]).should.be.closeTo(0.5, 0.00001); 29 | getpvalue(pnorm, 8.04, "right", [10, 1]).should.be.closeTo(0.975, 0.00001); 30 | getpvalue(pnorm, 11.96, "right", [10, 1]).should.be.closeTo(0.025, 0.00001); 31 | 32 | // both tails 33 | getpvalue(pnorm, 0, "both").should.be.closeTo(1.0, 0.00001); 34 | getpvalue(pnorm, 10, "both", [10, 1]).should.be.closeTo(1.0, 0.00001); 35 | getpvalue(pnorm, 8.04, "both", [10, 1]).should.be.closeTo(0.05, 0.00001); 36 | getpvalue(pnorm, 11.96, "both", [10, 1]).should.be.closeTo(0.05, 0.00001); 37 | }); 38 | 39 | it('tests for method "ttest".', function () { 40 | 41 | let res = ttest(vector([-2, -1, 0, 1, 2])); 42 | 43 | // exact numbers 44 | res.effectObserved.should.be.equal(0); 45 | res.alpha.should.be.equal(0.05); 46 | res.tail.should.be.equal("both"); 47 | res.DoF.should.be.equal(4); 48 | 49 | // estimated numbers 50 | res.se.should.be.closeTo(0.7071068, 0.00001); 51 | res.tValue.should.be.closeTo(0, 0.00001); 52 | res.pValue.should.be.closeTo(1, 0.00001); 53 | res.ci[0].should.be.closeTo(-1.963243, 0.00001); 54 | res.ci[1].should.be.closeTo(1.963243, 0.00001); 55 | 56 | // same but for left tail 57 | res = ttest(vector([-2, -1, 0, 1, 2]), 0, 0.05, "left"); 58 | 59 | // exact numbers 60 | res.effectObserved.should.be.equal(0); 61 | res.alpha.should.be.equal(0.05); 62 | res.tail.should.be.equal("left"); 63 | res.DoF.should.be.equal(4); 64 | 65 | // estimated numbers 66 | res.se.should.be.closeTo(0.7071068, 0.00001); 67 | res.tValue.should.be.closeTo(0, 0.00001); 68 | res.pValue.should.be.closeTo(0.5, 0.00001); 69 | res.ci[0].should.be.closeTo(-1.963243, 0.00001); 70 | res.ci[1].should.be.closeTo(1.963243, 0.00001); 71 | 72 | // same but for right tail 73 | res = ttest(vector([-2, -1, 0, 1, 2]), 0, 0.05, "right"); 74 | 75 | // exact numbers 76 | res.effectObserved.should.be.equal(0); 77 | res.alpha.should.be.equal(0.05); 78 | res.tail.should.be.equal("right"); 79 | res.DoF.should.be.equal(4); 80 | 81 | // estimated numbers 82 | res.se.should.be.closeTo(0.7071068, 0.00001); 83 | res.tValue.should.be.closeTo(0, 0.00001); 84 | res.pValue.should.be.closeTo(0.5, 0.00001); 85 | res.ci[0].should.be.closeTo(-1.963243, 0.00001); 86 | res.ci[1].should.be.closeTo(1.963243, 0.00001); 87 | 88 | // changed mu and alpha - both tails 89 | res = ttest(vector([-3, -2, -1, 0, 1, 2]), 2, 0.01, "both"); 90 | 91 | // exact numbers 92 | res.effectExpected.should.be.equal(2.0); 93 | res.effectObserved.should.be.equal(-0.5); 94 | res.alpha.should.be.equal(0.01); 95 | res.tail.should.be.equal("both"); 96 | res.DoF.should.be.equal(5); 97 | 98 | // estimated numbers 99 | res.se.should.be.closeTo(0.7637626, 0.0001); 100 | res.tValue.should.be.closeTo(-3.2733, 0.0001); 101 | res.pValue.should.be.closeTo( 0.02212, 0.0001); 102 | res.ci[0].should.be.closeTo(-3.5796, 0.0001); 103 | res.ci[1].should.be.closeTo( 2.5796, 0.0001); 104 | 105 | 106 | // changed mu and alpha - left tail 107 | res = ttest(vector([-3, -2, -1, 0, 1, 2]), 2, 0.01, "left"); 108 | 109 | // exact numbers 110 | res.effectExpected.should.be.equal(2.0); 111 | res.effectObserved.should.be.equal(-0.5); 112 | res.alpha.should.be.equal(0.01); 113 | res.tail.should.be.equal("left"); 114 | res.DoF.should.be.equal(5); 115 | 116 | // estimated numbers 117 | res.se.should.be.closeTo(0.7637626, 0.0001); 118 | res.tValue.should.be.closeTo(-3.2733, 0.0001); 119 | res.pValue.should.be.closeTo( 0.01106, 0.0001); 120 | res.ci[0].should.be.closeTo(-3.5796, 0.0001); 121 | res.ci[1].should.be.closeTo( 2.5796, 0.0001); 122 | 123 | // changed mu and alpha - right tail 124 | // changed mu and alpha - left tail 125 | res = ttest(vector([-3, -2, -1, 0, 1, 2]), 2, 0.01, "right"); 126 | 127 | // exact numbers 128 | res.effectExpected.should.be.equal(2.0); 129 | res.effectObserved.should.be.equal(-0.5); 130 | res.alpha.should.be.equal(0.01); 131 | res.tail.should.be.equal("right"); 132 | res.DoF.should.be.equal(5); 133 | 134 | // estimated numbers 135 | res.se.should.be.closeTo(0.7637626, 0.0001); 136 | res.tValue.should.be.closeTo(-3.2733, 0.0001); 137 | res.pValue.should.be.closeTo( 0.9889, 0.0001); 138 | res.ci[0].should.be.closeTo(-3.5796, 0.0001); 139 | res.ci[1].should.be.closeTo( 2.5796, 0.0001); 140 | }); 141 | 142 | it('tests for method "ttest2".', function () { 143 | let res = ttest2(vector([-2, -1, 0, 1, 2]), vector([-3, -2, -1, 0, 1])); 144 | 145 | // exact numbers 146 | res.effectObserved.should.be.equal(1); 147 | res.alpha.should.be.equal(0.05); 148 | res.tail.should.be.equal("both"); 149 | res.DoF.should.be.equal(8); 150 | 151 | // estimated numbers 152 | res.se.should.be.closeTo(1, 0.00001); 153 | res.tValue.should.be.closeTo(1, 0.00001); 154 | res.pValue.should.be.closeTo(0.3466, 0.0001); 155 | res.ci[0].should.be.closeTo(-1.306004, 0.00001); 156 | res.ci[1].should.be.closeTo(3.306004, 0.00001); 157 | 158 | res = ttest2(vector([-2, -1, 0, 1, 2]), vector([-3, -2, -1, 0, 1]), 0.05, "left"); 159 | 160 | // exact numbers 161 | res.effectObserved.should.be.equal(1); 162 | res.alpha.should.be.equal(0.05); 163 | res.tail.should.be.equal("left"); 164 | res.DoF.should.be.equal(8); 165 | 166 | // estimated numbers 167 | res.se.should.be.closeTo(1, 0.00001); 168 | res.tValue.should.be.closeTo(1, 0.00001); 169 | res.pValue.should.be.closeTo(0.8267, 0.0001); 170 | res.ci[0].should.be.closeTo(-1.306004, 0.00001); 171 | res.ci[1].should.be.closeTo(3.306004, 0.00001); 172 | 173 | res = ttest2(vector([-2, -1, 0, 1, 2]), vector([-3, -2, -1, 0, 1]), 0.05, "right"); 174 | 175 | // exact numbers 176 | res.effectObserved.should.be.equal(1); 177 | res.alpha.should.be.equal(0.05); 178 | res.tail.should.be.equal("right"); 179 | res.DoF.should.be.equal(8); 180 | 181 | // estimated numbers 182 | res.se.should.be.closeTo(1, 0.00001); 183 | res.tValue.should.be.closeTo(1, 0.00001); 184 | res.pValue.should.be.closeTo(0.1733, 0.0001); 185 | res.ci[0].should.be.closeTo(-1.306004, 0.00001); 186 | res.ci[1].should.be.closeTo(3.306004, 0.00001); 187 | 188 | res = ttest2(vector([-2, -1, 0, 1, 2]), vector([-3, -2, -1, 0, 1]), 0.01); 189 | 190 | // exact numbers 191 | res.effectObserved.should.be.equal(1); 192 | res.alpha.should.be.equal(0.01); 193 | res.tail.should.be.equal("both"); 194 | res.DoF.should.be.equal(8); 195 | 196 | // estimated numbers 197 | res.se.should.be.closeTo(1, 0.00001); 198 | res.tValue.should.be.closeTo(1, 0.00001); 199 | res.pValue.should.be.closeTo(0.3466, 0.0001); 200 | res.ci[0].should.be.closeTo(-2.355387, 0.00001); 201 | res.ci[1].should.be.closeTo(4.355387, 0.00001); 202 | }); 203 | 204 | }); 205 | 206 | --------------------------------------------------------------------------------