From 342e2ffdc52bec38c365fc5bb80a2a43fd673441 Mon Sep 17 00:00:00 2001 From: Sergey Kucheryavskiy Date: Mon, 29 Apr 2013 11:42:33 +0200 Subject: [PATCH] v. 0.2.0 - various improvements and documentation for PCA and related methods --- DESCRIPTION | 10 + NAMESPACE | 1 + R/.DS_Store | Bin 0 -> 6148 bytes R/classres.R | 32 ++ R/crossval.R | 22 ++ R/ldecomp.R | 318 ++++++++++++++++++ R/mdaplots.R | 354 ++++++++++++++++++++ R/pca.R | 571 +++++++++++++++++++++++++++++++++ R/pcacvres.R | 72 +++++ R/pcares.R | 24 ++ R/plotCumVariance.R | 4 + R/plotLoadings.R | 4 + R/plotPredictions.R | 4 + R/plotRMSE.R | 4 + R/plotRegcoeffs.R | 4 + R/plotResiduals.R | 4 + R/plotScores.R | 4 + R/plotVariance.R | 4 + R/plotXResiduals.R | 4 + R/plotXYLoadings.R | 4 + R/plotXYScores.R | 4 + R/plotYResiduals.R | 4 + R/pls.R | 513 +++++++++++++++++++++++++++++ R/plsregcoeffs.R | 81 +++++ R/plsresult.R | 286 +++++++++++++++++ R/prep.R | 68 ++++ R/regcoeffs.R | 73 +++++ R/selectCompNum.R | 4 + data/.DS_Store | Bin 0 -> 6148 bytes data/Jam.RData | Bin 0 -> 711 bytes data/People.RData | Bin 0 -> 1023 bytes data/Simdata.RData | Bin 0 -> 91508 bytes man/.DS_Store | Bin 0 -> 6148 bytes man/ldecomp.Rd | 65 ++++ man/ldecomp.getDistances.Rd | 47 +++ man/pca.Rd | 113 +++++++ man/pca.mvreplace.Rd | 61 ++++ man/pcacvres.Rd | 74 +++++ man/pcares.Rd | 78 +++++ man/plot.pca.Rd | 47 +++ man/plotCumVariance.ldecomp.Rd | 47 +++ man/plotCumVariance.pca.Rd | 31 ++ man/plotLoadings.pca.Rd | 45 +++ man/plotResiduals.ldecomp.Rd | 50 +++ man/plotResiduals.pca.Rd | 61 ++++ man/plotScores.ldecomp.Rd | 51 +++ man/plotScores.pca.Rd | 53 +++ man/plotVariance.ldecomp.Rd | 45 +++ man/plotVariance.pca.Rd | 44 +++ man/predict.pca.Rd | 61 ++++ man/regresult.Rd | 41 +++ man/selectCompNum.pca.Rd | 52 +++ mdatools.Rproj | 16 + test/.DS_Store | Bin 0 -> 6148 bytes test/matlab/People.csv | 1 + test/matlab/People.mat | Bin 0 -> 1152 bytes test/matlab/mand.m | 23 ++ test/matlab/pca_mvreplace.m | 18 ++ test/matlab/test_pca.m | 30 ++ test/test_mvreplace.R | 40 +++ test/test_pca.R | 124 +++++++ test/test_pls.R | 35 ++ test/test_prep.R | 5 + 63 files changed, 3835 insertions(+) create mode 100755 DESCRIPTION create mode 100644 NAMESPACE create mode 100755 R/.DS_Store create mode 100644 R/classres.R create mode 100644 R/crossval.R create mode 100755 R/ldecomp.R create mode 100755 R/mdaplots.R create mode 100755 R/pca.R create mode 100755 R/pcacvres.R create mode 100755 R/pcares.R create mode 100755 R/plotCumVariance.R create mode 100755 R/plotLoadings.R create mode 100755 R/plotPredictions.R create mode 100755 R/plotRMSE.R create mode 100755 R/plotRegcoeffs.R create mode 100755 R/plotResiduals.R create mode 100755 R/plotScores.R create mode 100755 R/plotVariance.R create mode 100755 R/plotXResiduals.R create mode 100755 R/plotXYLoadings.R create mode 100755 R/plotXYScores.R create mode 100755 R/plotYResiduals.R create mode 100644 R/pls.R create mode 100644 R/plsregcoeffs.R create mode 100644 R/plsresult.R create mode 100644 R/prep.R create mode 100644 R/regcoeffs.R create mode 100755 R/selectCompNum.R create mode 100755 data/.DS_Store create mode 100755 data/Jam.RData create mode 100755 data/People.RData create mode 100755 data/Simdata.RData create mode 100755 man/.DS_Store create mode 100644 man/ldecomp.Rd create mode 100644 man/ldecomp.getDistances.Rd create mode 100644 man/pca.Rd create mode 100644 man/pca.mvreplace.Rd create mode 100644 man/pcacvres.Rd create mode 100644 man/pcares.Rd create mode 100644 man/plot.pca.Rd create mode 100644 man/plotCumVariance.ldecomp.Rd create mode 100644 man/plotCumVariance.pca.Rd create mode 100644 man/plotLoadings.pca.Rd create mode 100644 man/plotResiduals.ldecomp.Rd create mode 100644 man/plotResiduals.pca.Rd create mode 100644 man/plotScores.ldecomp.Rd create mode 100644 man/plotScores.pca.Rd create mode 100644 man/plotVariance.ldecomp.Rd create mode 100644 man/plotVariance.pca.Rd create mode 100644 man/predict.pca.Rd create mode 100755 man/regresult.Rd create mode 100644 man/selectCompNum.pca.Rd create mode 100755 mdatools.Rproj create mode 100644 test/.DS_Store create mode 100644 test/matlab/People.csv create mode 100644 test/matlab/People.mat create mode 100644 test/matlab/mand.m create mode 100644 test/matlab/pca_mvreplace.m create mode 100644 test/matlab/test_pca.m create mode 100755 test/test_mvreplace.R create mode 100755 test/test_pca.R create mode 100644 test/test_pls.R create mode 100644 test/test_prep.R diff --git a/DESCRIPTION b/DESCRIPTION new file mode 100755 index 0000000..6cf1f1f --- /dev/null +++ b/DESCRIPTION @@ -0,0 +1,10 @@ +Package: mdatools +Title: Multivariate Data Analysis +Version: 0.2.0 +Date: 2013-04-29 +Author: Sergey Kucheryavskiy +Maintainer: Sergey Kucheryavskiy +Description: The package contains useful functions for preprocessing, exploring and analysis of multivariate data. +Suggests: +Depends: +License: GPL-2 diff --git a/NAMESPACE b/NAMESPACE new file mode 100644 index 0000000..9c9f9ac --- /dev/null +++ b/NAMESPACE @@ -0,0 +1 @@ +exportPattern("^[^\\.]") diff --git a/R/.DS_Store b/R/.DS_Store new file mode 100755 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0= 24 && nobj < 40) { nseg = 8} + else if (nobj > 40) { nseg = 4 } + } + else if (nseg == 1) + { + nseg = nobj + } + + seglen = ceiling(nobj / nseg) + fulllen = seglen * nseg + + idx = c(sample(1:nobj), rep(NA, fulllen - nobj)) + idx = matrix(idx, nrow = nseg, byrow = T) + + return (idx) +} \ No newline at end of file diff --git a/R/ldecomp.R b/R/ldecomp.R new file mode 100755 index 0000000..9d8e161 --- /dev/null +++ b/R/ldecomp.R @@ -0,0 +1,318 @@ +# class and methods for linear decomposition X = TP' + E # +ldecomp = function(scores, loadings, residuals, fullvar, ...) UseMethod("ldecomp") + +ldecomp.default = function(scores, loadings, residuals, fullvar, tnorm = NULL, ncomp.selected = NULL) +{ + # Creates an object of ldecomp class. + # + # Arguments: + # scores: matrix with score values (nobj x ncomp). + # loadings: matrix with loading values (nvar x ncomp). + # residuals: matrix with data residuals + # fullvar: full variance of original data, preprocessed and centered + # tnorm: singular values for score normalization + # ncomp.selected: number of selected components + # + # Returns: + # object (list) of class ldecomp with following fields: + # obj$scores: matrix with score values (nobj x ncomp). + # obj$residuals: matrix with residuals (nobj x nvar). + # obj$fullvar: full variance of original data + # obj$Q2: matrix with Q2 residuals (nobj x ncomp). + # obj$T2: matrix with T2 distances (nobj x ncomp) + # obj$ncomp.selected: selected number of components + # obj$expvar: explained variance for each component + # obj$cumexpvar: cumulative explained variance + + scores = as.matrix(scores) + loadings = as.matrix(loadings) + residuals = as.matrix(residuals) + + # check dimension + if (ncol(scores) != ncol(loadings) || + nrow(scores) != nrow(residuals) || nrow(loadings) != ncol(residuals)) + stop('Dimensions of scores, loadings and data do not correspond to each other!') + + # set names for scores and loadings + rownames(scores) = rownames(residuals) + colnames(scores) = paste('Comp', 1:ncol(scores)) + rownames(loadings) = colnames(residuals) + colnames(loadings) = paste('Comp', 1:ncol(loadings)) + + if (is.null(ncomp.selected)) + ncomp.selected = ncol(scores) + + # calculate residual distances and explained variance + obj = ldecomp.getDistances(scores, loadings, residuals, tnorm) + var = ldecomp.getVariances(obj$Q2, fullvar) + + obj$expvar = var$expvar + obj$cumexpvar = var$cumexpvar + obj$scores = scores + obj$residuals = residuals + obj$fullvar = fullvar + obj$ncomp.selected = ncomp.selected + obj$call = match.call() + + class(obj) = "ldecomp" + + return (obj) +} + +ldecomp.getDistances = function(scores, loadings, residuals, tnorm = NULL) +{ + # Computes residual distances (Q2 and T2) for a decomposition. + # The distances are calculated for every 1:n components, where n + # goes from 1 to ncomp (number of columns in scores and loadings) + # + # Arguments: + # scores: matrix with scores (nobj x ncomp). + # loadings: matrix with loadings (nvar x ncomp) + # residuals: matrix with data residuals + # + # Returns: + # res$Q2: matrix with Q2 residuals (nobj x ncomp). + # res$T2: matrix with T2 distances (nobj x ncomp) + + ncomp = ncol(scores) + nobj = nrow(scores) + T2 = matrix(0, nrow = nobj, ncol = ncomp) + Q2 = matrix(0, nrow = nobj, ncol = ncomp) + + # calculate normalized scores + if (is.null(tnorm)) + tnorm = sqrt(colSums(scores ^ 2)/(nrow(scores) - 1)); + scoresn = sweep(scores, 2L, tnorm, '/', check.margin = F); + + # calculate distances for each set of components + for (i in 1:ncomp) + { + if (i < ncomp) + res = residuals + scores[, (i + 1):ncomp, drop = F] %*% t(loadings[, (i + 1):ncomp, drop = F]) + else + res = residuals + + Q2[, i] = rowSums(res^2) + T2[, i] = rowSums(scoresn[, 1:i, drop = F]^2) + } + + # set dimnames and return results + colnames(Q2) = colnames(T2) = colnames(scores) + rownames(Q2) = rownames(T2) = rownames(scores) + + res = list( + Q2 = Q2, + T2 = T2, + tnorm = tnorm + ) +} + +ldecomp.getVariances = function(Q2, fullvar) +{ + # Computes explained variance and cumulative explained variance + # for every component of a decomposition. + # + # Arguments: + # scores: matrix with scores. + # loadings: matrix with loadings. + # residuals: matrix with residuals. + # Q2: matrix with Q2 values + # Returns: + # res$expvar: vector with explained variance for every component + # res$cumexpvar: vector with cumulative explained variance + + cumresvar = colSums(Q2) / fullvar * 100 + cumexpvar = 100 - cumresvar + expvar = c(cumexpvar[1], diff(cumexpvar)) + + res = list( + expvar = expvar, + cumexpvar = cumexpvar + ) +} + +ldecomp.getResLimits = function(eigenvals, nobj, ncomp, alpha = 0.05) +{ + # Computes statistical limits for Q2 residuals and T2 distances. + # + # Arguments: + # eigenvals: vector with eigenvalues for a model. + # nobj: number of objects in calibration data + # ncomp: number of selected components + # alpha: significance level for Q2 limits + # + # Returns: + # res$Q2lim: limit for Q2 residuals + # res$T2lim: limit for T2 distances + + # calculate T2 limit using Hotelling statistics + if (nobj == ncomp) + T2lim = 0 + else + T2lim = (ncomp * (nobj - 1) / (nobj - ncomp)) * qf(1 - alpha, ncomp, nobj - ncomp); + + # calculate Q2 limit using F statistics + conflim = 100 - alpha * 100; + + nvar = length(eigenvals) + + if (ncomp < nvar) + { + eigenvals = eigenvals[(ncomp + 1):nvar] + + cl = 2 * conflim - 100 + t1 = sum(eigenvals) + t2 = sum(eigenvals^2) + t3 = sum(eigenvals^3) + h0 = 1 - 2 * t1 * t3/3/(t2^2); + + if (h0 < 0.001) + h0 = 0.001 + + ca = sqrt(2) * erfinv(cl/100) + h1 = ca * sqrt(2 * t2 * h0^2)/t1 + h2 = t2 * h0 * (h0 - 1)/(t1^2) + Q2lim = t1 * (1 + h1 + h2)^(1/h0) + } + else + Q2lim = 0 + + res = list( + T2lim = T2lim, + Q2lim = Q2lim + ) +} + +plotCumVariance.ldecomp = function(obj, show.labels = F) +{ + # Shows cumulative explained variance plot. + # + # Arguments: + # obj: object of ldecomp class. + # show.labels: show or not labels for plot points + + data = cbind(1:length(obj$cumexpvar), obj$cumexpvar) + data = rbind(c(0, 0), data) + colnames(data) = c('Components', 'Explained variance, %') + rownames(data) = round(c(0, obj$cumexpvar), 1) + mdaplots.linescatter(data, main = 'Cumulative variance', show.labels = show.labels) +} + +plotVariance.ldecomp = function(obj, show.labels = F) +{ + # Shows explained variance plot. + # + # Arguments: + # obj: object of ldecomp class. + # show.labels: show or not labels for plot points + # + + data = cbind(1:length(obj$expvar), obj$expvar) + colnames(data) = c('Components', 'Explained variance, %') + rownames(data) = round(obj$expvar, 1) + mdaplots.linescatter(data, main = 'Variance', show.labels = show.labels) +} + +plotScores.ldecomp = function(obj, comp = c(1, 2), cgroup = NULL, + show.labels = F, show.colorbar = T, + show.axes = T) +{ + # Shows scores plot. + # + # Arguments: + # obj: object of ldecomp class. + # comp: which components to show on x and y axis. + # cgroup: variable for color grouping of plot points. + # show.labels: show or not labels for plot points. + # show.colorbar: show or not a colorbar legend if cgroup is provided. + # show.axes: show or not axes crossing (0, 0) point. + + if (length(comp) == 1) + { + # scores vs objects + data = cbind(1:nrow(obj$scores), obj$scores[, comp]) + colnames(data) = c('Objects', colnames(obj$scores)[comp]) + rownames(data) = rownames(obj$scores) + mdaplots.scatter(data, main = 'Scores', cgroup = cgroup, + show.labels = show.labels, + show.colorbar = show.colorbar + ) + } + else if (length(comp) == 2) + { + # scores vs scores + data = obj$scores[, c(comp[1], comp[2])] + + if (show.axes == T) + show.lines = c(0, 0) + else + show.lines = F + + mdaplots.scatter(data, main = 'Scores', cgroup = cgroup, + show.labels = show.labels, + show.colorbar = show.colorbar, + show.lines = show.lines) + } + else + { + stop('Wrong number of components!') + } +} + +plotResiduals.ldecomp = function(obj, ncomp = NULL, cgroup = NULL, + show.labels = F, show.colorbar = T) +{ + # Shows T2 vs Q2 residuals plot. + # + # Arguments: + # obj: object of ldecomp class. + # ncomp: number of components for the residuals + # cgroup: variable for color grouping of plot points. + # show.labels: show or not labels for plot points. + # show.colorbar: show or not a colorbar legend if cgroup is provided. + + if (is.null(ncomp)) + ncomp = obj$ncomp.selected + + data = cbind(obj$T2[, ncomp], obj$Q2[, ncomp]) + colnames(data) = c('T2', 'Q2') + mdaplots.scatter(data, main = sprintf('Residuals (ncomp = %d)', ncomp), + cgroup = cgroup, show.labels = show.labels, + show.colorbar = show.colorbar) +} + +print.ldecomp = function(obj, str = NULL) +{ + if (is.null(str)) + str ='Results of data decomposition (class ldecomp)' + + cat('\n') + cat(str, '\n') + cat('\nMajor fields:\n') + cat('$scores - matrix with score values (nobj x ncomp)\n') + cat('$T2 - matrix with T2 distances (nobj x ncomp)\n') + cat('$Q2 - matrix with Q2 residuals (nobj x ncomp)\n') + cat('$ncomp.selected - selected number of components\n') + cat('$expvar - explained variance for each component\n') + cat('$cumexpvar - cumulative explained variance\n\n') +} + +summary.ldecomp = function(obj, str = NULL) +{ + if (is.null(str)) + str ='Summary for data decomposition (class ldecomp)' + + cat('\n') + cat(str, '\n') + cat(sprintf('\nSelected components: %d\n\n', obj$ncomp.selected)) + + data = cbind(round(obj$expvar, 2), + round(obj$cumexpvar, 2)) + + colnames(data) = c('Exp. var', 'Cum. exp. var') + show(data) +} + +erfinv = function (x) qnorm((1 + x)/2)/sqrt(2) + + diff --git a/R/mdaplots.R b/R/mdaplots.R new file mode 100755 index 0000000..1ce5b5b --- /dev/null +++ b/R/mdaplots.R @@ -0,0 +1,354 @@ +# class and methods for plotting # +mdaplots = function(scores, loadings, data, ...) UseMethod("mdaplots") + +mdaplots.getAxesLim = function(data, show.colorbar = F, multi.y = F, show.legend = F, show.lines = F) +{ + scale = 0.05 # 5% + if (multi.y == F ) + { + if (is.list(data)) + { + xmax = max(data[[1]][, 1]) + xmin = min(data[[1]][, 1]) + ymax = max(data[[1]][, 2]) + ymin = min(data[[1]][, 2]) + + for (i in 1:length(data)) + { + xmax = max(xmax, data[[i]][, 1]) + xmin = min(xmin, data[[i]][, 1]) + ymax = max(ymax, data[[i]][, 2]) + ymin = min(ymin, data[[i]][, 2]) + } + } + else + { + xmax = max(data[, seq(1, ncol(data), 2)]) + xmin = min(data[, seq(1, ncol(data), 2)]) + ymax = max(data[, seq(2, ncol(data), 2)]) + ymin = min(data[, seq(2, ncol(data), 2)]) + } + } + else + { + if (is.list(data)) + { + } + else + { + xmax = max(data[, 1]) + xmin = min(data[, 1]) + ymax = max(data[, 2:ncol(data)]) + ymin = min(data[, 2:ncol(data)]) + } + } + + if (is.numeric(show.lines)) + { + if (!is.na(show.lines[1])) + { + xmax = max(xmax, show.lines[1]) + xmin = min(xmin, show.lines[1]) + } + + if (!is.na(show.lines[2])) + { + ymax = max(ymax, show.lines[2]) + ymax = max(ymax, show.lines[2]) + } + } + + dx = (xmax - xmin) * scale + dy = (ymax - ymin) * scale + + xlim = c(xmin - dx, xmax + dx) + ylim = c(ymin - dy, ymax + dy) + + if (show.colorbar == T) + ylim[2] = ylim[2] + dy * 2 + + if (show.legend == T) + { + xlim[2] = xlim[2] + dx + ylim[2] = ylim[2] + dy + } + + lim = list( + xlim = xlim, + ylim = ylim + ) +} + +mdaplots.showColorbar = function(col, cgroup) +{ + # TODO(svk): format data for colorbar legend + + col = mdaplots.getColors(length(unique(col))) + ncol = length(col) + cgroup = levels(cut(as.vector(cgroup), ncol)) + lvals = as.numeric( sub("\\((.+),.*", "\\1", cgroup) ) + rvals = as.numeric( sub("[^,]*,([^]]*)\\]", "\\1", cgroup) ) + + lim = par('usr') + dx = lim[2] - lim[1] + dy = lim[4] - lim[3] + + w = (dx * 0.8)/ncol + h = dy * 0.015 + + x = lim[1] + dx * 0.1 + y = lim[4] - (h + 0.1 * h); + + for (i in 1:length(col)) + { + rect(x + w * (i - 1), y, x + w * i, y - h, col = col[i], border = NA) + text(x + w * (i - 1), y - h, format(lvals[i], digits = 3), + cex = 0.6, pos = 1, col = 'gray') + } + text(x + w * i, y - h, rvals[i], cex = 0.6, pos = 1, col = 'gray') +} + +mdaplots.getColors = function(n = 1, cgroup = NULL) +{ + rgbcol = c(213, 62, 79, + 244, 109, 67, + 253, 174, 97, + 254, 224, 139, + 230, 245, 152, + 171, 221, 164, + 102, 194, 165, + 50, 136, 189) + rgbcol = matrix(rgbcol, ncol = 3, byrow = T) + ngroups = nrow(rgbcol) + col8 = rgb(rgbcol[, 1], rgbcol[, 2], rgbcol[, 3], maxColorValue = 255) + col8 = col8[seq(length(col8), 1, -1)] + + if (!is.null(cgroup)) + { + cgroup = cut(as.vector(cgroup), ngroups, labels = 1:ngroups) + col = col8[cgroup] + } + else + { + if (length(n) == 1) + { + if (n == 1) + col = col8[1] + else if (n == 2) + col = col8[c(1, 8)] + else if (n == 3) + col = col8[c(1, 5, 8)] + else + col = col8 + } + } + + return (col) +} + +mdaplots.showLegend = function(legend, col, pch = NULL, lty = NULL, pos = 'topright') +{ + legend(pos, legend, col = col, pch = pch, lty = lty, + cex = 0.8, inset = 0.01, bg = 'white') +} + +mdaplots.showLabels = function(data) +{ + if (is.null(rownames(data))) + rownames(data) = 1:nrow(data) + + text(data[, 1], data[, 2], rownames(data), cex = 0.6, pos = 3, col = 'gray') +} + +mdaplots.showLines = function(point) +{ + if (!is.na(point[2])) + abline(h = point[2], lty = 2, col = 'darkgray') + if (!is.na(point[1])) + abline(v = point[1], lty = 2, col = 'darkgray') +} + +mdaplots.scatterg = function(data, pch = 16, legend = NULL, xlab = NULL, + ylab = NULL, show.labels = F, show.lines = F, ...) +{ + lim = mdaplots.getAxesLim(data, show.lines = show.lines) + col = mdaplots.getColors(length(data)) + + if (is.null(xlab)) + xlab = colnames(data[[1]])[1] + if (is.null(ylab)) + ylab = colnames(data[[1]])[2] + + for (i in 1:length(data)) + { + if (i == 1) + { + plot(data[[i]][, 1], data[[i]][, 2], type = 'p', + col = col[i], pch = pch, + xlim = lim$xlim, + ylim = lim$ylim, + xlab = xlab, + ylab = ylab, + ...) + } + else + { + points(data[[i]][, 1], data[[i]][, 2], type = 'p', + col = col[i], pch = pch) + } + if (show.labels == T) + mdaplots.showLabels(data[[i]]) + + if (!is.null(legend) && length(legend) > 1) + mdaplots.showLegend(legend, col, pch = pch) + } + + grid() + + if (is.numeric(show.lines) && length(show.lines) == 2 ) + mdaplots.showLines(show.lines) + +} + +mdaplots.scatter = function(data, pch = 16, cgroup = NULL, show.labels = F, + show.colorbar = T, show.lines = F, ...) +{ + data = as.matrix(data) + + if (!is.null(cgroup)) + { + lim = mdaplots.getAxesLim(data, show.colorbar = show.colorbar, show.lines = show.lines) + col = mdaplots.getColors(cgroup = cgroup) + } + else + { + lim = mdaplots.getAxesLim(data, show.lines = show.lines) + col = mdaplots.getColors(1) + } + + plot(data[, 1], data[, 2], type = 'p', + col = col, pch = pch, + xlab = colnames(data)[1], + ylab = colnames(data)[2], + xlim = lim$xlim, + ylim = lim$ylim, + ...) + + if (show.labels == T) + mdaplots.showLabels(data) + + grid() + + if (is.numeric(show.lines) && length(show.lines) == 2 ) + mdaplots.showLines(show.lines) + + if (!is.null(cgroup) && show.colorbar == T) + mdaplots.showColorbar(col, cgroup) +} + +mdaplots.line = function(data, type = 'l', show.legend = F, pch = 16, + xlab = NULL, ylab = NULL, show.labels = F, ...) +{ + data = as.matrix(data) + ny = ncol(data) - 1 + col = mdaplots.getColors(ny) + lim = mdaplots.getAxesLim(data, multi.y = T) + + if (!(type == 'l' | type == 'b')) + type = 'l' + + if (is.null(xlab)) + xlab = colnames(data)[1] + + if (is.null(ylab)) + ylab = colnames(data)[2] + + for (i in 1:ny) + { + if (i == 1) + { + if (nrow(data) <= 10) + plot(data[, 1], data[, i + 1], col = col[i], type = type, + xlab = xlab, + ylab = ylab, + ylim = lim$ylim, + xaxt = 'n', + pch = pch, + ...) + else + plot(data[, 1], data[, i + 1], col = col[i], type = type, + xlab = xlab, + ylab = ylab, + ylim = lim$ylim, + pch = pch, + ...) + + } + else + { + lines(data[, 1], data[, i + 1], col = col[i], type = type, pch = pch) + } + } + + if (nrow(data) < 10) + axis(side = 1, at = seq(1, nrow(data)), rownames(data)) + + if (show.legend == T && ny > 1) + mdaplots.showLegend(colnames(data[, 2:ncol(data)]), col, lty = 1) + grid() +} + +mdaplots.lineg = function(data, pch = 16, type = 'l', show.labels = F, + legend = NULL, xlab = NULL, ylab = NULL, ...) +{ + lim = mdaplots.getAxesLim(data) + col = mdaplots.getColors(length(data)) + + if (is.null(xlab)) + xlab = colnames(data[[1]])[1] + if (is.null(ylab)) + ylab = colnames(data[[1]])[2] + + for (i in 1:length(data)) + { + if (i == 1) + { + plot(data[[i]][, 1], data[[i]][, i + 1], col = col[i], type = type, + xlab = xlab, + ylab = ylab, + ylim = lim$ylim, + pch = pch, + ...) + } + else + { + lines(data[[i]][, 1], data[[i]][, 2], col = col[i], type = type, pch = pch) + } + + if (show.labels == T) + mdaplots.showLabels(data[[i]]) + + } + + if (!is.null(legend) && length(legend) > 1) + mdaplots.showLegend(legend, col, pch = pch) + grid() +} + +mdaplots.linescatter = function(data, pch = 16, show.labels = F, ...) +{ + data = as.matrix(data) + col = mdaplots.getColors(1) + lim = mdaplots.getAxesLim(data) + + plot(data[, 1], data[, 2], type = 'b', + col = col, pch = pch, + xlab = colnames(data)[1], + ylab = colnames(data)[2], + xlim = lim$xlim, + ylim = lim$ylim, + ...) + if (show.labels == T) + mdaplots.showLabels(data) + grid() +} diff --git a/R/pca.R b/R/pca.R new file mode 100755 index 0000000..6d748c8 --- /dev/null +++ b/R/pca.R @@ -0,0 +1,571 @@ +# class and methods for Principal component analysis # +pca = function(data, ...) UseMethod("pca") + +pca.default = function(data, ncomp = 20, center = T, scale = F, cv = NULL, + test.data = NULL, alpha = 0.05, info = '', ...) +{ + data = as.matrix(data) + + # check if data has missing values + if (sum(is.na(data)) > 0) + { + warning('Data has missing values, will try to fix using pca.mvreplace.') + data = pca.mvreplace(data, center = center, scale = scale) + } + + # calibrate model and select number of components + ncomp = min(ncomp, ncol(data), nrow(data) - 1) + res = pca.cal(data, ncomp, center = center, scale = scale) + model = list( + loadings = res$loadings, + eigenvals = res$eigenvals, + tnorm = res$tnorm, + center = res$center, + scale = res$scale, + ncomp = ncol(res$loadings) + ) + + model$ncomp.selected = model$ncomp + model$info = info + model$alpha = alpha + + # apply model to calibration set + model$calres = predict.pca(model, data) + + # do cross-validation if needed + if (!is.null(cv)) + model$cvres = pca.crossval(model, data, cv) + + # apply model to test set if any + if (!is.null(test.data)) + model$testres = predict.pca(model, test.data) + + # calculate and assign limit values for T2 and Q2 residuals + lim = ldecomp.getResLimits(model$eigenvals, nrow(data), model$ncomp.selected, model$alpha) + model$T2lim = lim$T2lim + model$Q2lim = lim$Q2lim + + model$call = match.call() + class(model) = "pca" + + return (model) +} + +selectCompNum.pca = function(model, ncomp) +{ + if (is.null(model)) + stop('Object with model is not specified!') + + if (ncomp < 1 || ncomp > model$ncomp) + stop('Wrong number of selected components!') + + model$ncomp.selected = ncomp + lim = ldecomp.getResLimits(model$eigenvals, nrow(model$calres$scores), + model$ncomp.selected, model$alpha) + model$T2lim = lim$T2lim + model$Q2lim = lim$Q2lim + + model$calres$ncomp.selected = ncomp + + if (!is.null(model$testres)) + model$testres$ncomp.selected = ncomp + + if (!is.null(model$cvres)) + model$cvres$ncomp.selected = ncomp + + return (model) +} + +pca.mvreplace = function(data, center = T, scale = F, maxncomp = 7, + expvarlim = 0.95, covlim = 10^-6, maxiter = 100) +{ + # initial estimates with mean values + cdata = data + mvidx = is.na(cdata) + + for (i in 1:ncol(data)) + { + mv = is.na(data[, i]) + + if (sum(mv)/length(data[, i]) > 0.2) + stop(sprintf('To many missing values in column #%d', i)) + + cdata[mv, i] = mean(data[, i], na.rm = T) + } + + # autoscale + cdata = scale(cdata, center = center, scale = scale) + + if (scale == T) + gsd = attr(cdata, 'scaled:scale') + + if (center == T) + gmean = attr(cdata, 'scaled:center'); + + data = cdata + + # iterations + n = 1 + scoresp = 0 + scores = 1 + cond = 1 + while (cond > covlim && n < maxiter) + { + n = n + 1 + + cdata = scale(cdata, center = T, scale = F) + lmean = attr(cdata, 'scaled:center') + + res = pca.svd(cdata, maxncomp) + + expvar = cumsum(res$eigenvals/sum(res$eigenvals)) + ncomp = min(which(expvar >= expvarlim), maxncomp) + + if (ncomp == 0) + ncomp = 1 + if (ncomp == length(expvar)) + ncomp = ncomp - 1 + + scoresp = scores + loadings = res$loadings[, 1:ncomp] + scores = cdata %*% loadings + newdata = scores %*% t(loadings) + + newdata = sweep(newdata, 2L, lmean, '+', check.margin = F) + + cdata = data + cdata[mvidx] = newdata[mvidx] + + if (n > 2) + { + # calculate difference between scores + ncompcond = min(ncol(scores), ncol(scoresp)) + cond = sum((scores[, 1:ncompcond] - scoresp[, 1:ncompcond])^2) + } + } + + # rescale the data back and return + if (scale == T) + cdata = sweep(cdata, 2L, gsd, '*', check.margin = F) + + if (center == T) + cdata = sweep(cdata, 2L, gmean, '+', check.margin = F) + + return (cdata) +} + +pca.cal = function(data, ncomp, center = T, scale = F) +{ + data = prep.autoscale(data, center = center, scale = scale) + res = pca.svd(data, ncomp) + + res$tnorm = sqrt(colSums(res$scores ^ 2)/(nrow(res$scores) - 1)); + + rownames(res$loadings) = colnames(data) + colnames(res$loadings) = paste('Comp', 1:ncol(res$loadings)) + res$center = attr(data, 'prep:center') + res$scale = attr(data, 'prep:scale') + + return (res) +} + +pca.svd = function(data, ncomp) +{ + nobj = nrow(data) + nvar = ncol(data) + ncomp = min(ncomp, nobj - 1, nvar) + + s = svd(data) + loadings = s$v[, 1:ncomp] + + res = list( + loadings = loadings, + scores = data %*% loadings, + eigenvals = (s$d^2)/(nrow(data) - 1) + ) +} + +pca.nipals = function(data, ncomp) +{ + nobj = nrow(data) + nvar = ncol(data) + ncomp = min(ncomp, nobj - 1, nvar) + + scores = matrix(0, nrow = nobj, ncol = ncomp) + loadings = matrix(0, nrow = nvar, ncol = ncomp) + eigenvals = rep(0, ncomp); + + E = data + for (i in 1:ncomp) + { + ind = which.max(apply(E, 2, sd)) + t = E[, ind, drop = F] + tau = 99999 + th = 9999 + + while (th > 0.000001) + { + p = (t(E) %*% t) / as.vector((t(t) %*% t)) + p = p / as.vector(t(p) %*% p) ^ 0.5 + t = (E %*% p)/as.vector(t(p) %*% p) + th = abs(tau - as.vector(t(t) %*% t)) + tau = as.vector(t(t) %*% t) + } + + E = E - t %*% t(p) + scores[, i] = t + loadings[, i] = p + eigenvals[i] = tau / (nobj - 1) + } + + res = list( + loadings = loadings, + scores = scores, + eigenvals = eigenvals + ) +} + +pca.crossval = function(model, data, cv) +{ + scale = model$scale + center = model$center + ncomp = model$ncomp + + nobj = nrow(data) + nvar = ncol(data) + + # get matrix with indices for cv segments + idx = crossval(nobj, cv) + + seglen = ncol(idx); + + Q2 = matrix(0, ncol = ncomp, nrow = nobj) + T2 = matrix(0, ncol = ncomp, nrow = nobj) + + # loop over segments + for (i in 1:nrow(idx)) + { + ind = na.exclude(idx[i,]) + + if (length(ind) > 0) + { + datac = data[-ind, , drop = F] + datat = data[ind, , drop = F] + + m = pca.cal(datac, ncomp, model$center, model$scale) + res = predict.pca(m, datat, stripped = T) + Q2[ind, ] = res$Q2 + T2[ind, ] = res$T2 + } + } + + rownames(Q2) = rownames(T2) = rownames(data) + colnames(Q2) = colnames(T2) = colnames(model$scores) + + res = pcacvres(T2, Q2, model$calres$fullvar) +} + +predict.pca = function(model, data, stripped = F) +{ + data = prep.autoscale(data, model$center, model$scale) + scores = data %*% model$loadings + residuals = data - scores %*% t(model$loadings) + + if (stripped == F) + { + fullvar = sum(data^2) + res = pcares(scores, model$loadings, residuals, fullvar, model$tnorm, model$ncomp.selected) + } + else + { + res = ldecomp.getDistances(scores, model$loadings, residuals, model$tnorm) + } + + return (res) +} + + +plotCumVariance.pca = function(obj, show.labels = F, show.legend = T) +{ + legend = NULL + + cdata = cbind(0:length(obj$calres$cumexpvar), c(0, obj$calres$cumexpvar)) + colnames(cdata) = c('Components', 'Explained variance, %') + rownames(cdata) = c(0, round(obj$calres$cumexpvar, 1)) + data = list(cdata = cdata) + + if (show.legend == T) + legend = 'cal' + + if (!is.null(obj$cvres)) + { + cvdata = cbind(0:length(obj$cvres$cumexpvar), c(0, obj$cvres$cumexpvar)) + colnames(cvdata) = c('Components', 'Explained variance, %') + rownames(cvdata) = c(0, round(obj$cvres$cumexpvar, 1)) + + data$cvdata = cvdata + if (show.legend == T) + legend = c(legend, 'cv') + } + + if (!is.null(obj$testres)) + { + tdata = cbind(0:length(obj$testres$cumexpvar), c(0, obj$testres$cumexpvar)) + colnames(tdata) = c('Components', 'Explained variance, %') + rownames(tdata) = c(0, round(obj$testres$cumexpvar, 1)) + + data$tdata = tdata + if (show.legend == T) + legend = c(legend, 'test') + } + + mdaplots.lineg(data, main = 'Cumulative variance', ylab = 'Explained variance, %', + show.labels = show.labels, legend = legend, pch = 16, type = 'b') +} + +plotVariance.pca = function(obj, show.labels = F, show.legend = T) +{ + legend = NULL + + cdata = cbind(1:length(obj$calres$expvar), obj$calres$expvar) + colnames(cdata) = c('Components', 'Explained variance, %') + rownames(cdata) = round(obj$calres$expvar, 1) + data = list(cdata = cdata) + + if (show.legend == T) + legend = 'cal' + + if (!is.null(obj$cvres)) + { + cvdata = cbind(1:length(obj$cvres$expvar), obj$cvres$expvar) + colnames(cvdata) = c('Components', 'Explained variance, %') + rownames(cvdata) = round(obj$cvres$expvar, 1) + data$cvdata = cvdata + + if (show.legend == T) + legend = c(legend, 'cv') + } + + if (!is.null(obj$testres)) + { + tdata = cbind(1:length(obj$testres$expvar), obj$testres$expvar) + colnames(tdata) = c('Components', 'Explained variance, %') + rownames(tdata) = round(obj$testres$expvar, 1) + data$tdata = tdata + + if (show.legend == T) + legend = c(legend, 'test') + } + + mdaplots.lineg(data, main = 'Variance', ylab = 'Explained variance, %', + show.labels = show.labels, legend = legend, pch = 16, type = 'b') +} + +plotScores.pca = function(obj, comp = c(1, 2), + show.labels = F, show.legend = T, + show.axes = T) +{ + legend = NULL; + if (length(comp) == 1) + { + nobj.cal = nrow(obj$calres$scores) + + # scores vs objects + cdata = cbind(1:nobj.cal, obj$calres$scores[, comp]) + colnames(cdata) = c('Objects', colnames(obj$calres$scores)[comp]) + rownames(cdata) = rownames(obj$calres$scores) + + data = list(cdata = cdata) + + if (!is.null(obj$testres)) + { + nobj.test = nrow(obj$testres$scores) + tdata = cbind((nobj.cal + 1):(nobj.cal + nobj.test), obj$testres$scores[, comp]) + colnames(tdata) = c('Objects', colnames(obj$testres$scores)[comp]) + rownames(tdata) = rownames(obj$testres$scores) + data$tdata = tdata + if (show.legend == T) + legend = c('cal', 'test') + } + + mdaplots.scatterg(data, main = 'Scores', + show.labels = show.labels, + legend = legend + ) + } + else if (length(comp) == 2) + { + # scores vs scores + cdata = cbind(obj$calres$scores[, comp[1]], obj$calres$scores[, comp[2]]) + colnames(cdata) = colnames(obj$calres$scores)[comp] + rownames(cdata) = rownames(obj$calres$scores) + + data = list(cdata = cdata) + + if (!is.null(obj$testres)) + { + tdata = cbind(obj$testres$scores[, comp[1]], obj$testres$scores[, comp[2]]) + colnames(tdata) = colnames(obj$testres$scores)[comp] + rownames(tdata) = rownames(obj$testres$scores) + data$tdata = tdata + if (show.legend == T) + legend = c('cal', 'test') + } + + if (show.axes == T) + show.lines = c(0, 0) + else + show.lines = F + + mdaplots.scatterg(data, main = 'Scores', + show.labels = show.labels, + legend = legend, + show.lines = show.lines) + + } + else + { + stop('Wrong number of components!') + } +} + +plotResiduals.pca = function(obj, ncomp = NULL, show.labels = F, show.legend = T, show.limits = T) +{ + if (show.limits == T && (is.null(ncomp) || ncomp == obj$ncomp.selected)) + show.lines = c(obj$T2lim, obj$Q2lim) + else + show.lines = F + + if (is.null(ncomp)) + ncomp = obj$ncomp.selected + + cdata = cbind(obj$calres$T2[, ncomp], obj$calres$Q2[, ncomp]) + colnames(cdata) = c('T2', 'Q2') + rownames(cdata) = rownames(obj$calres$scores) + + data = list(cdata = cdata) + legend = NULL + + if (show.legend == T) + legend = 'cal' + + if (!is.null(obj$cvres)) + { + cvdata = cbind(obj$cvres$T2[, ncomp], obj$cvres$Q2[, ncomp]) + colnames(cvdata) = c('T2', 'Q2') + rownames(cvdata) = rownames(obj$cvres$T2) + + data$cvdata = cvdata + if (show.legend == T) + legend = c(legend, 'cv') + } + + if (!is.null(obj$testres)) + { + tdata = cbind(obj$testres$T2[, ncomp], obj$testres$Q2[, ncomp]) + colnames(tdata) = c('T2', 'Q2') + rownames(tdata) = rownames(obj$testres$scores) + + data$tdata = tdata + if (show.legend == T) + legend = c(legend, 'test') + } + + mdaplots.scatterg(data, main = sprintf('Residuals (ncomp = %d)', ncomp), + show.labels = show.labels, + legend = legend, + show.lines = show.lines) +} + +plotLoadings.pca = function(obj, comp = c(1, 2), show.labels = T, + show.legend = T, type = 'p') +{ + ncomp = length(comp) + + if (ncomp == 2 && type == 'p') + { + # scatter plot + data =obj$loadings[, c(comp[1], comp[2])] + mdaplots.scatter(data, show.labels = show.labels, main = 'Loadings', show.lines = c(0, 0)) + } + else if (ncomp < 1 | ncomp > 8 ) + { + stop ('Number of components must be between 1 and 8!') + } + else + { + if (type == 'p') + type = 'l' + + # line plot + data = cbind(1:nrow(obj$loadings), obj$loadings[, comp, drop = F]) + mdaplots.line(data, show.legend = show.legend, type = type, main = 'Loadings', + ylab = 'Loadings', xlab = 'Variables') + } +} + +plot.pca = function(obj, comp = c(1, 2), show.labels = F, show.legend = T) +{ + par(mfrow = c(2, 2)) + plotScores(obj, comp = comp, show.labels = show.labels, + show.legend = show.legend) + plotLoadings(obj, comp = comp, show.labels = show.labels, + show.legend = show.legend) + plotResiduals(obj, ncomp = obj$ncomp.selected, + show.labels = show.labels, show.legend = show.legend, show.limits = T) + plotCumVariance(obj, show.legend = show.legend) + par(mfrow = c(1, 1)) +} + +print.pca = function(model, ...) +{ + cat('\nPCA model (class pca)\n') + + if (length(model$info) > 0) + { + cat('\nInfo:\n') + cat(model$info) + } + + cat('\nCall:\n') + print(model$call) + + cat('\nMajor fields and methods:\n') + cat('$loadings - matrix with loadings\n') + cat('$eigenvals - eigenvalues for components\n') + cat('$ncomp - number of calculated components\n') + cat('$ncomp.selected - selected number of components\n') + cat('$center - values for centering data\n') + cat('$scale - values for scaling data\n') + cat('$cv - number of segments for cross-validation\n') + cat('$alpha - significance level for Q2 residuals\n\n') + cat('$calres - results (scores, etc) for calibration set\n') + + if (!is.null(model$cvres)) + { + cat('$cvres - results for cross-validation\n') + } + if (!is.null(model$testres)) + { + cat('$testres - results for test set\n') + } + cat('\nTry also: show(model$calres), summary(model) and plot(model)\n') + +} + +summary.pca = function(model) +{ + ncomp = model$ncomp.selected + cat('\nPCA model (class pca) summary\n') + + if (length(model$info) > 0) + cat(sprintf('\nInfo:\n%s\n\n', model$info)) + + data = cbind(round(model$eigenvals[1:model$ncomp], 3), + round(model$calres$expvar, 2), + round(model$calres$cumexpvar, 2)) + + colnames(data) = c('Eigvals', 'Exp. var', 'Cum. exp. var') + show(data) +} + diff --git a/R/pcacvres.R b/R/pcacvres.R new file mode 100755 index 0000000..5cbace3 --- /dev/null +++ b/R/pcacvres.R @@ -0,0 +1,72 @@ +# class and methods for PCA cross-validates results # +pcacvres = function(T2, Q2, fullvar, ...) UseMethod("pcacvres") + +pcacvres.default = function(T2, Q2, fullvar, ncomp.selected = NULL, ...) +{ + # Creates an object of pcacvres class. The returned object also inherits class + # ldecomp and some of its methods. + # + # Arguments: + # T2: matrix with T2 distances (nobj x ncomp) for cross-validation. + # Q2: matrix with Q2 residuals (nobj x ncomp) for cross-validation. + # fullvar: full variance of data + # ncomp.selected: number of selected components + # + # Returns: + # list with cross-validation results (object of class pcacvres) + # obj$Q2: matrix with Q2 residuals (nobj x ncomp). + # obj$T2: matrix with T2 distances (nobj x ncomp) + # obj$ncomp.selected: selected number of components + # obj$expvar: explained variance for each component + # obj$cumexpvar: cumulative explained variance + + if (is.null(ncomp.selected)) + ncomp.selected = ncol(T2) + + obj = list( + T2 = T2, + Q2 = Q2 + ) + + var = ldecomp.getVariances(obj$Q2, fullvar) + + obj$expvar = var$expvar + obj$cumexpvar = var$cumexpvar + obj$ncomp.selected = ncomp.selected + + obj$call = match.call() + class(obj) = c('pcacvres', 'ldecomp') + + return (obj) +} + +plotScores.pcacvres = function(obj, ...) +{ + # stub functon to show that scores plot is not available for cv results + + stop('Scores plot is not available for cross-validated results.') +} + +print.pcacvres = function(obj) +{ + cat('\nCross-validation results for PCA model (class pcacvres) \n') + + cat('\nMajor fields:\n') + cat('$T2 - matrix with T2 distances (nobj x ncomp)\n') + cat('$Q2 - matrix with Q2 residuals (nobj x ncomp)\n') + cat('$ncomp.selected - selected number of components\n') + cat('$expvar - explained variance for each component\n') + cat('$cumexpvar - cumulative explained variance\n\n') +} + +summary.pcacvres = function(obj) +{ + cat('\nSummary for cross-validation of PCA model\n') + cat(sprintf('Selected components: %d\n', obj$ncomp.selected)) + + data = cbind(round(obj$expvar, 2), + round(obj$cumexpvar, 2)) + + colnames(data) = c('Exp. var', 'Cum. exp. var') + show(data) +} \ No newline at end of file diff --git a/R/pcares.R b/R/pcares.R new file mode 100755 index 0000000..42424a1 --- /dev/null +++ b/R/pcares.R @@ -0,0 +1,24 @@ +# class and methods for PCA results # +pcares = function(scores, loadings, residuals, fullvar, ...) UseMethod("pcares") + +pcares.default = function(scores, loadings, residuals, fullvar, tnorm = NULL, ncomp.selected = NULL, ...) +{ + # Creates an object of pcares class. In fact the class is a wrapper for ldecomp and + # uses its methods and attributes. + + pcares = ldecomp(scores, loadings, residuals, fullvar, tnorm = tnorm, ncomp.selected = ncomp.selected, ...) + class(pcares) = c('pcares', 'ldecomp') + + return (pcares) +} + + +print.pcares = function(obj) +{ + print.ldecomp(obj, 'Results for PCA decomposition (class pcares)') +} + +summary.pcares = function(obj) +{ + summary.ldecomp(obj, 'Summary for PCA results') +} \ No newline at end of file diff --git a/R/plotCumVariance.R b/R/plotCumVariance.R new file mode 100755 index 0000000..e6896c2 --- /dev/null +++ b/R/plotCumVariance.R @@ -0,0 +1,4 @@ +plotCumVariance = function(object, ...) +{ + UseMethod("plotCumVariance") +} \ No newline at end of file diff --git a/R/plotLoadings.R b/R/plotLoadings.R new file mode 100755 index 0000000..a8396bc --- /dev/null +++ b/R/plotLoadings.R @@ -0,0 +1,4 @@ +plotLoadings = function(object, ...) +{ + UseMethod("plotLoadings") +} \ No newline at end of file diff --git a/R/plotPredictions.R b/R/plotPredictions.R new file mode 100755 index 0000000..11c58cb --- /dev/null +++ b/R/plotPredictions.R @@ -0,0 +1,4 @@ +plotPredictions = function(object, ...) +{ + UseMethod("plotPredictions") +} \ No newline at end of file diff --git a/R/plotRMSE.R b/R/plotRMSE.R new file mode 100755 index 0000000..ddee12b --- /dev/null +++ b/R/plotRMSE.R @@ -0,0 +1,4 @@ +plotRMSE = function(object, ...) +{ + UseMethod("plotRMSE") +} \ No newline at end of file diff --git a/R/plotRegcoeffs.R b/R/plotRegcoeffs.R new file mode 100755 index 0000000..1a14214 --- /dev/null +++ b/R/plotRegcoeffs.R @@ -0,0 +1,4 @@ +plotRegcoeffs = function(object, ...) +{ + UseMethod("plotRegcoeffs") +} \ No newline at end of file diff --git a/R/plotResiduals.R b/R/plotResiduals.R new file mode 100755 index 0000000..b18062e --- /dev/null +++ b/R/plotResiduals.R @@ -0,0 +1,4 @@ +plotResiduals = function(object, ...) +{ + UseMethod("plotResiduals") +} \ No newline at end of file diff --git a/R/plotScores.R b/R/plotScores.R new file mode 100755 index 0000000..243a387 --- /dev/null +++ b/R/plotScores.R @@ -0,0 +1,4 @@ +plotScores = function(object, ...) +{ + UseMethod("plotScores") +} \ No newline at end of file diff --git a/R/plotVariance.R b/R/plotVariance.R new file mode 100755 index 0000000..1bc018d --- /dev/null +++ b/R/plotVariance.R @@ -0,0 +1,4 @@ +plotVariance = function(object, ...) +{ + UseMethod("plotVariance") +} \ No newline at end of file diff --git a/R/plotXResiduals.R b/R/plotXResiduals.R new file mode 100755 index 0000000..7209744 --- /dev/null +++ b/R/plotXResiduals.R @@ -0,0 +1,4 @@ +plotXResiduals = function(object, ...) +{ + UseMethod("plotXResiduals") +} \ No newline at end of file diff --git a/R/plotXYLoadings.R b/R/plotXYLoadings.R new file mode 100755 index 0000000..1bc018d --- /dev/null +++ b/R/plotXYLoadings.R @@ -0,0 +1,4 @@ +plotVariance = function(object, ...) +{ + UseMethod("plotVariance") +} \ No newline at end of file diff --git a/R/plotXYScores.R b/R/plotXYScores.R new file mode 100755 index 0000000..1472b26 --- /dev/null +++ b/R/plotXYScores.R @@ -0,0 +1,4 @@ +plotXYScores = function(object, ...) +{ + UseMethod("plotXYScores") +} \ No newline at end of file diff --git a/R/plotYResiduals.R b/R/plotYResiduals.R new file mode 100755 index 0000000..f56ce91 --- /dev/null +++ b/R/plotYResiduals.R @@ -0,0 +1,4 @@ +plotYResiduals = function(object, ...) +{ + UseMethod("plotYResiduals") +} \ No newline at end of file diff --git a/R/pls.R b/R/pls.R new file mode 100644 index 0000000..6d237de --- /dev/null +++ b/R/pls.R @@ -0,0 +1,513 @@ +# class and methods for Partial Least Squares regression # +pls = function(X, y, ...) UseMethod("pls") + +pls.default = function(X, y, ncomp = 12, cv = 0, autoscale = 1, Xt = NULL, yt = NULL, ...) +{ + X = as.matrix(X) + y = as.matrix(y) + + ncomp = min(ncol(X), nrow(X) - 1, ncomp) + + # build a model and apply to calibration set + model = pls.cal(X, y, ncomp, autoscale) + model$calres = predict.pls(model, X, y) + + # do cross-validation if needed + if (cv > 0) + model$cvres = pls.crossval(model, X, y, cv) + + # do test set validation if provided + if (!is.null(Xt) && !is.null(yt)) + { + Xt = as.matrix(Xt) + yt = as.matrix(yt) + model$testres = predict.pls(model, Xt, yt) + } + + model$ncomp.selected = ncomp + model$call = match.call() + + class(model) = "pls" + + model +} + +pls.cal = function(X, y, ncomp, autoscale) +{ + X = as.matrix(X) + y = as.matrix(y) + + if (autoscale > 0) + { + # find mean values for X and y + mX = apply(X, 2, mean) + my = apply(y, 2, mean) + + if (autoscale == 2) + { + # calculate stadnard deviations for X variables + sdX = apply(X, 2, sd) + sdy = apply(y, 2, sd) + } + else + { + # use vector with ones if no standardization is needed + sdX = rep(1, ncol(X)) + sdy = rep(1, ncol(y)) + } + + # autoscale X and y + X = scale(X, center = mX, scale = sdX) + #y = scale(y, center = my, scale = sdy) + y = y - my + } + + + # do SIMPLS + model = pls.simpls(X, y, ncomp) + + model$autoscale = autoscale + + if (autoscale > 0) + { + model$mX = mX + model$sdX = sdX + model$my = my + model$sdy = sdy + } + + model$ncomp = ncomp + + return (model) +} + +## SIMPLS algorithm ### +pls.simpls = function(X, y, ncomp, stripped = FALSE) +{ + X = as.matrix(X) + y = as.matrix(y) + + objnames = rownames(X); + prednames = colnames(X); + respnames = colnames(y); + + nobj = dim(X)[1] + npred = dim(X)[2] + nresp = 1 + + V = R = matrix(0, nrow = npred, ncol = ncomp) + tQ = matrix(0, nrow = ncomp, ncol = nresp) + B = array(0, dim = c(npred, nresp, ncomp)) + + if (!stripped) { + P = R + U = TT = matrix(0, nrow = nobj, ncol = ncomp) + } + + S = crossprod(X, y) + for (a in 1:ncomp) { + q.a = 1 + r.a = S %*% q.a + t.a = X %*% r.a + t.a = t.a - mean(t.a) + tnorm = sqrt(c(crossprod(t.a))) + + t.a = t.a/tnorm + r.a = r.a/tnorm + p.a = crossprod(X, t.a) + q.a = crossprod(y, t.a) + v.a = p.a + if (a > 1) { + v.a = v.a - V %*% crossprod(V, p.a) + } + v.a = v.a/sqrt(c(crossprod(v.a))) + S = S - v.a %*% crossprod(v.a, S) + R[, a] = r.a + tQ[a, ] = q.a + V[, a] = v.a + B[, , a] = R[, 1:a, drop = FALSE] %*% tQ[1:a, , drop = FALSE] + if (!stripped) { + u.a = y %*% q.a + if (a > 1) + u.a = u.a - TT %*% crossprod(TT, u.a) + P[, a] = p.a + TT[, a] = t.a + U[, a] = u.a + } + } + + B = B[, 1, ] + + if (stripped) { + list(coeffs = B) + } + else { + + lvnames = paste("Comp", 1:ncomp) + ncompnames = paste(1:ncomp, " components") + rownames(B) = prednames + colnames(B) = ncompnames + rownames(TT) = rownames(U) = objnames + colnames(TT) = colnames(U) = lvnames + list( + coeffs = B, + xloadings = P, + yloadings = t(tQ), + weights = R + ) + } +} + +pls.crossval = function(model, X, y, cv) +{ + X = as.matrix(X) + y = as.matrix(y) + + autoscale = model$autoscale + ncomp = model$ncomp + nobj = nrow(X) + nvar = ncol(X) + + # get matrix with indices for cv segments + idx = crossval(nobj, cv) + + seglen = ncol(idx); + + yp = matrix(0, nrow = nobj, ncol = ncomp) + + # loop over segments + for (i in 1:nrow(idx)) + { + ind = na.exclude(idx[i,]) + + if (length(ind) > 0) + { + Xc = X[-ind, , drop = F] + yc = y[-ind, , drop = F] + Xt = X[ind, , drop = F] + yt = y[ind, , drop = F] + + m = pls.cal(Xc, yc, ncomp, autoscale) + res = predict.pls(m, Xt, stripped = T) + yp[ind, ] = res$yp + } + } + + res = plsresult(yp, y) +} + +## select optimal ncomp for the model ## +pls.selectncomp = function(model, ncomp) +{ + if (ncomp <= model$ncomp && ncomp > 0) + { + model$ncomp.selected = ncomp; + model$calres$ncomp.selected = ncomp + + if (!is.null(model$cvres)) + model$cvres$ncomp.selected = ncomp + + if (!is.null(model$testres)) + model$testres$ncomp.selected = ncomp + } + + return (model) +} + +predict.pls = function(model, X, y = NULL, stripped = FALSE) +{ + + if (model$autoscale > 0) + X = scale(X, center = model$mX, scale = model$sdX) + + yp = X %*% as.matrix(model$coeffs) + + if (model$autoscale > 0) + yp = yp + model$my + + if (stripped == FALSE) + { + xscores = X %*% (model$weights %*% solve(t(model$xloadings) %*% model$weights)) + + if (!is.null(y)) + { + yy = y - model$my + yscores = as.matrix(yy) %*% model$yloadings + } + + rownames(xscores) = rownames(yscores) = rownames(X) + colnames(xscores) = colnames(yscores) = paste("LV", 1:model$ncomp) + + res = plsresult(yp, y, X = X, + xscores = xscores, + yscores = yscores, + xloadings = model$xloadings, + yloadings = model$yloadings + ) + } + else + { + res = list(yp = yp) + } +} + + +plotRMSE.pls = function(model, + main = 'RMSE', xlab = 'ncomps', ylab = 'RMSE', + type = 'b', + show.legend = T) +{ + ncomp = model$ncomp + + legend = c('cal') + cdata = cbind(1:ncomp, model$calres$rmse) + data = list(cdata = cdata) + + if (!is.null(model$cvres)) + { + cvdata = cbind(1:ncomp, model$cvres$rmse) + data$cvdata = cvdata + legend = c(legend, 'cv') + } + + if (!is.null(model$testres)) + { + testdata = cbind(1:ncomp, model$testres$rmse) + data$testdata = testdata + legend = c(legend, 'test') + } + + if (show.legend == F) + legend = NULL + + mdaplots.lineg(data, legend = legend, type = type, + main = main, xlab = xlab, ylab = ylab) +} + +plotXYScores.pls = function(model, ncomp = 1, main = 'XY Scores', + show.labels = F, show.legend = T) +{ + main = sprintf('XY scores (ncomp = %d)', ncomp) + + cdata = cbind(model$calres$xscores[, ncomp], model$calres$yscores[, ncomp]) + colnames(cdata) = c('X scores', 'Y scores') + data = list(cdata = cdata) + legend = c('cal') + + if (!is.null(model$testres)) + { + tdata = cbind(model$testres$xscores[, ncomp], model$testres$yscores[, ncomp]) + data$tdata = tdata + legend = c(legend, 'test') + } + + if (show.legend == F) + legend = NULL + + mdaplots.scatterg(data, legend = legend, show.labels = show.labels, main = main) +} + +## plot with measured vs predicted y values ## +plotPredictions.pls = function(model, ncomp = 0, main = 'Predictions', + xlab = 'y, measured', + ylab = 'y, predicted', + show.labels = F, + show.legend = T) +{ + if (ncomp == 0) + ncomp = model$ncomp.selected + + if (ncomp > model$ncomp) + { + warning(sprintf('\nChosen ncomp is larger than model has. Use %d instead.', model$ncomp)) + ncomp = model$ncomp + } + + cdata = cbind(model$calres$y, model$calres$yp[, ncomp]) + colnames(cdata) = c('y, measured', 'y, predicted') + rownames(cdata) = rownames(model$calres$yp) + legend = c('cal') + data = list(cdata = cdata) + + if (!is.null(model$cvres)) + { + cvdata = cbind(model$cvres$y, model$cvres$yp[, ncomp]) + colnames(cvdata) = c('y, measured', 'y, predicted') + rownames(cvdata) = rownames(model$cvres$yp) + legend = c(legend, 'cv') + data$cvdata = cvdata + } + + if (!is.null(model$testres)) + { + testdata = cbind(model$testres$y, model$testres$yp[, ncomp]) + colnames(testdata) = c('y, measured', 'y, predicted') + rownames(testdata) = rownames(model$testres$yp) + legend = c(legend, 'test') + data$testdata = testdata + } + + if (show.legend == F) + legend = NULL + + mdaplots.scatterg(data, legend = legend, show.labels = show.labels, + main = main) +} + +plotYResiduals.pls = function(model, ncomp = 0, main = 'Y residuals', + xlab = 'y residuals', + ylab = 'y values', + show.labels = F, + show.legend = T) +{ + if (ncomp == 0) + ncomp = model$ncomp.selected + + if (ncomp > model$ncomp) + { + warning(sprintf('\nChosen ncomp is larger than model has. Use %d instead.', model$ncomp)) + ncomp = model$ncomp + } + + cdata = cbind(model$calres$y, model$calres$yp[, ncomp] - model$calres$y) + colnames(cdata) = c('y values', 'y residuals') + rownames(cdata) = rownames(model$calres$yp) + legend = c('cal') + data = list(cdata = cdata) + + if (!is.null(model$cvres)) + { + cvdata = cbind(model$cvres$y, model$cvres$yp[, ncomp] - model$cvres$y) + colnames(cvdata) = c('y values', 'y residuals') + rownames(cvdata) = rownames(model$cvres$yp) + legend = c(legend, 'cv') + data$cvdata = cvdata + } + + if (!is.null(model$testres)) + { + testdata = cbind(model$testres$y, model$testres$yp[, ncomp] - model$testres$y) + colnames(testdata) = c('y values', 'y residuals') + rownames(testdata) = rownames(model$testres$yp) + legend = c(legend, 'test') + data$testdata = testdata + } + + if (show.legend == F) + legend = NULL + + mdaplots.scatterg(data, legend = legend, show.labels = show.labels, + main = sprintf('Y residuals (ncomp = %d)', ncomp)) +} + +plotRegcoeffs.pls = function(model, main = 'Regression coefficients', + show.labels = F, ncomp = 0) +{ + if (ncomp == 0) + ncomp = model$ncomp.selected + + if (ncomp > model$ncomp) + { + warning(sprintf('\nChosen ncomp is larger than model has. Use %d instead.', model$ncomp)) + ncomp = model$ncomp + } + + data = cbind(1:nrow(model$coeffs), model$coeffs[, ncomp]) + colnames(data) = c('Variables', 'Coefficients') + rownames(data) = rownames(model$xloadings) + mdaplots.line(data, type = 'l', main = main, show.labels = show.labels) +} + + +plotXResiduals.pls = function(model, ncomp = NULL, show.labels = F, show.legend = T) +{ + if (is.null(ncomp)) + ncomp = model$ncomp.selected + cdata = cbind(model$calres$T2[, ncomp], model$calres$Q2[, ncomp]) + + colnames(cdata) = c('T2', 'Q2') + rownames(cdata) = rownames(model$calres$scores) + + data = list(cdata = cdata) + legend = NULL + + if (!is.null(model$testres)) + { + tdata = cbind(model$testres$T2[, ncomp], model$testres$Q2[, ncomp]) + colnames(tdata) = c('T2', 'Q2') + rownames(tdata) = rownames(model$testres$scores) + + data$tdata = tdata + if (show.legend == T) + legend = c('cal', 'test') + } + + mdaplots.scatterg(data, main = sprintf('X residuals (ncomp = %d)', ncomp), + show.labels = show.labels, + legend = legend) +} + +## makes a plot with regression results ## +plot.pls = function(model, show.legend = T, show.labels = F) +{ + par(mfrow = c(2, 2)) + plotXYScores(model, show.labels = show.labels, show.legend = show.legend) + plotRegcoeffs(model, show.labels = show.labels) + plotRMSE(model, show.legend = show.legend) + plotPredictions(model, show.labels = show.labels, show.legend = show.legend) + par(mfrow = c(1, 1)) +} + + +## show summary for a model ## +summary.pls = function(model) +{ + ncomp = model$ncomp.selected + cat('\nPLS model (class pls) summary\n') + cat('\nPerformance and validation:\n') + cat(sprintf('Selected LVs: %d\n\n', ncomp)) + cat(' ') + cat(sprintf('%6s\t', colnames(as.matrix(model$calres)))) + cat('\n') + cat('Cal: ') + cat(sprintf('%.4f\t', as.matrix(model$calres)[ncomp, , drop = F])) + cat('\n') + + if (!is.null(model$cvres)) + { + cat('CV: ') + cat(sprintf('%.4f\t', as.matrix(model$cvres)[ncomp, , drop = F])) + cat('\n') + } + + if (!is.null(model$testres)) + { + cat('Test: ') + cat(sprintf('%.4f\t', as.matrix(model$testres)[ncomp, , drop = F])) + cat('\n') + } + +} + +## print information about a model ## +print.pls = function(model, ...) +{ + cat('\nPLS model (class pls)\n') + cat('\nCall:\n') + print(model$call) + cat('\nMajor fields:\n') + cat('$coeffs - vector with regression coefficients\n') + cat('$xloadings - vector with X loadings\n') + cat('$yloadings - vector with Y loadings\n') + cat('$calres - results for calibration set\n') + if (!is.null(model$cvres)) + { + cat('$cvres - results for cross-validation\n') + } + if (!is.null(model$testres)) + { + cat('$testres - results for test set\n') + } + cat('\nTry summary(model) and plot(model) to see the results of validation\n') + +} diff --git a/R/plsregcoeffs.R b/R/plsregcoeffs.R new file mode 100644 index 0000000..4850675 --- /dev/null +++ b/R/plsregcoeffs.R @@ -0,0 +1,81 @@ +# class and methods for PLS regression coefficients # +plsregcoeffs = function(coeffs, ...) UseMethod("plsregcoeffs") + +## default method ## +plsregcoeffs.default = function(coeffs) +{ + plsregcoeffs = list(values = as.matrix(coeffs)) + plsregcoeffs$call = match.call() + + class(plsregcoeffs) = "plsregcoeffs" + + plsregcoeffs +} + +as.matrix.plsregcoeffs = function(plsregcoeffs, nlv = 0, ...) +{ + if (nlv == 0) + { + return (plsregcoeffs$values) + } + else + { + return (plsregcoeffs$values[, nlv]) + } +} + +print.plsregcoeffs = function(coeffs, nlv = 0, digits = 3, ...) +{ + show(coeffs) + cat('\nRegression coefficients (class plsregcoeffs)\n') + if (nlv == 0) { nlv = ncol(coeffs.values)} + print(round(coeffs$values[, nlv], digits)) +} + +plot.plsregcoeffs = function(plsregcoeffs, nlv = 0, main = 'Regression coefficients', + xlab = 'Variables', ylab = 'Coefficients', + pch = 16, col = 'blue', ...) +{ + + if (nlv == 0) { nlv = ncol(plsregcoeffs$values)} + + coeffs = plsregcoeffs$values[, nlv] + ncoeff = length(coeffs) + + # select limits for y axis + ylim = max(abs(coeffs)) + + # chose plot type depending on number of coefficients + if (ncoeff < 30) { type = 'b' }else{ type = 'l' } + + # show plot + plot(coeffs, type = type, col = col, pch = pch, + main = main, + xlab = xlab, + ylab = ylab, + ylim = c(-ylim, ylim), + axes = F + ) + abline(c(0, 0), c(0, ncoeff)) + + if (is.null(dim(coeffs))) { names = names(coeffs)} + else {names = rownames(coeffs)} + + # show axes and labels if needed + if (ncoeff > 20) + { + atx = seq(1, ncoeff, ncoeff/10) + } + else + { + atx = 1:ncoeff + } + axis(1, at = atx, labels = names[atx], cex.axis = 0.85) + axis(2, cex.axis = 0.85) + if (ncoeff < 30) + { + text(1:ncoeff, coeffs, names, cex = 0.6, pos = 3, col = 'gray') + } + grid() + box() +} \ No newline at end of file diff --git a/R/plsresult.R b/R/plsresult.R new file mode 100644 index 0000000..343f74c --- /dev/null +++ b/R/plsresult.R @@ -0,0 +1,286 @@ +plsresult = function(yp, y, ...) UseMethod("plsresult") + +plsresult.default = function(yp, y = NULL, X = NULL, nlv = 0, xscores = NULL, yscores = NULL, + xloadings = NULL, yloadings = NULL) +{ + yp = as.matrix(yp) + + if (!is.null(xloadings)) + { + xdist = ldecomp.getDistances(xscores, xloadings, X) + xvar = ldecomp.getVariances(xdist$T2, sum(X^2)) + Q2 = xdist$Q2 + T2 = xdist$T2 + xexpvar = xvar$expvar + } + else + { + xexpvar = NULL + Q2 = NULL + T2 = NULL + } + + if (!is.null(yloadings) && !is.null(y)) + { + ydist = ldecomp.getDistances(yscores, yloadings, y) + yvar = ldecomp.getVariances(ydist$T2, sum(y^2)) + yexpvar = yvar$expvar + } + else + { + yexpvar = NULL + } + + if (!is.null(y)) + { + y = as.numeric(y) + + plsresult = list( + yp = yp, + y = y, + xscores = xscores, + yscores = yscores, + T2 = T2, + Q2 = Q2, + xexpvar = xexpvar, + yexpvar = yexpvar, + rmse = plsresult.rmse(y, yp), + slope = plsresult.slope(y, yp), + r2 = cor(y, yp)^2, + bias = apply(y - yp, 2, mean) + ) + } + else + { + plsresult = list( + yp = yp, + y = y, + xscores = xscores, + yscores = yscores, + T2 = T2, + Q2 = Q2, + xexpvar = xexpvar, + yexpvar = yexpvar, + rmse = NULL, + slope = NULL, + r2 = NULL, + bias = NULL + ) + } + + if (nlv == 0) { nlv = ncol(yp) } + + plsresult$nlvselected = nlv + plsresult$call = match.call() + class(plsresult) = "plsresult" + + plsresult +} + +# calculates root mean squared error for prediction +plsresult.rmse = function(y, yp) +{ + return (sqrt(colSums((y - yp)^2)/length(y))) +} + +# calculates slope of a line fit for (y, yp) values +plsresult.slope = function(y, yp) +{ + nlv = dim(yp)[2] + slope = 1:nlv + for (a in 1:nlv) + { + m = lm(yp[, a] ~ y) + slope[a] = m$coefficients[2] + } + + return (slope) +} + +plsresult.plot_rmse = function(result, col = 'blue', type = 'b', main = 'RMSE', xlab = 'LVs', + ylab = 'RMSE', pch = 16, xlim = NULL, ylim = NULL) +{ + if (!is.null(result$rmse)) + { + nlv = length(result$rmse) + + if (is.null(xlim) || is.null(ylim)) + { + plot(1:nlv, result$rmse, type = type, col = col, main = main, pch = pch, + xlab = xlab, ylab = ylab) + } + else + { + plot(1:nlv, result$rmse, type = type, col = col, main = main, pch = pch, + xlab = xlab, ylab = ylab, xlim = xlim, ylim = ylim) + } + points(result$nlvselected, result$rmse[result$nlvselected], col = 'red') + grid() + } +} + +plsresult.points_rmse = function(result, col = 'blue', type = 'b', pch = 16) +{ + if (!is.null(result$rmse)) + { + nlv = length(result$rmse) + lines(1:nlv, result$rmse, type = type, col = col, pch = pch) + points(result$nlvselected, result$rmse[result$nlvselected], col = 'red') + grid() + } +} + +plsresult.plot_predictions = function(result, nlv = 0, col = 'blue', + labels = F, + main = 'Predictions', + xlab = NULL, ylab = 'y, predicted', + pch = 16, ...) +{ + if (nlv == 0) { nlv = result$nlvselected } + + if (!is.null(result$y)) + { + if (is.null(xlab)) { xlab = 'y, measured' } + + # show predicted versus measured values plot + plot(result$y, result$yp[, nlv], col = col, pch = pch, + main = main, xlab = xlab, ylab = ylab, cex.axis = 0.85, ...) + abline(lm(result$yp[, nlv] ~ result$y), col = col) + if (labels == T) + { + text(result$y, result$yp[,nlv], rownames(result$yp), cex = 0.6, pos = 3, col = 'gray') + } + } + else + { + if (is.null(xlab)) { xlab = 'Objects' } + + # show predicted y values for every object + plot(1:length(result$yp[, nlv]), result$yp[, nlv], + col = col, pch = pch, + main = main, + xlab = xlab, + ylab = ylab, + cex.axis = 0.85, + ... + ) + if (labels == T) + { + text(1:length(result$yp[, nlv]), result$yp[,nlv], rownames(result$yp), cex = 0.6, pos = 3, col = 'gray') + } + } + grid() +} + +plsresult.plot_xscores = function(result, nlv = c(1, 2), col = 'blue', main = 'X scores', + pch = 16, labels = F, ...) +{ + plot(result$xscores[, nlv[1]], result$xscores[, nlv[2]], col = col, pch = pch, + xlab = colnames(result$xscores)[nlv[1]], + ylab = colnames(result$xscores)[nlv[2]], + main = main, ...) + + if (labels == T) + { + text(result$xscores[, nlv[1]], result$xscores[, nlv[2]], rownames(result$xscores), + cex = 0.6, pos = 3, col = 'gray') + } +} + +plsresult.points_xscores = function(result, nlv = c(1, 2), col = 'blue', labels = F, pch = 16, ...) +{ + points(result$xscores[, nlv[1]], result$xscores[, nlv[2]], col = col, pch = pch, ...) + if (labels == T) + { + text(result$xscores[, nlv[1]], result$xscores[, nlv[2]], rownames(result$xscores), + cex = 0.6, pos = 3, col = 'gray') + } +} + +plsresult.plot_xyscores = function(result, nlv = 1) +{ + plot(result$xscores[, nlv], result$yscores[, nlv]) +} + +plsresult.plot_xresiduals = function() +{ + +} + +plsresult.plot_yresiduals = function() +{ + +} + +plsresult.points_predictions = function(result, nlv = 0, labels = F, col = 'blue', pch = 16, ...) +{ + if (nlv == 0) { nlv = result$nlvselected } + + if (!is.null(result$y)) + { + # show predicted versus measured values plot + points(result$y, result$yp[, nlv], col = col, pch = pch, ... ) + abline(lm(result$yp[, nlv] ~ result$y), col = col) + if (labels == T) + { + text(result$y, result$yp[,nlv], rownames(result$yp), cex = 0.6, pos = 3, col = 'gray') + } + } + else + { + # show predicted y values for every object + points(1:length(result$yp[, nlv]), result$yp[, nlv], col = col, pch = pch, ... ) + if (labels == T) + { + text(1:length(result$yp[, nlv]), result$yp[,nlv], rownames(result$yp), cex = 0.6, pos = 3, col = 'gray') + } + } +} + +plot.plsresult = function(result) +{ + plsresult.plot_predictions(result) +} + +as.matrix.plsresult = function(result) +{ + nlv = result$nlvselected + + if (!is.null(result$y)) + { + res = matrix(c(result$rmse, result$r2, result$slope, result$bias), ncol = 4) + colnames(res) = c('RMSE', 'R^2', 'Slope', 'Bias') + } + else + { + res = NULL + } + return (res) +} + +print.plsresult = function(result, ...) +{ + cat('\nPLS results (class plsresult)\n') + cat('\nPredictions:\n') +} + +summary.plsresult = function(result, ...) +{ + cat('\nPLS results (class plsresult) summary\n') + if (!is.null(result$y)) + { + nlv = result$nlvselected + res = as.matrix(result)[nlv, , drop = FALSE] + + cat('\nPerformance:') + cat('\n') + cat(sprintf('\n\t%s: %d', 'Number of LVs', nlv)) + cat(sprintf('\n\t%s: %.4f', colnames(res), res)) + } + else + { + cat('No reference data provided to calculate prediction performance.') + } +} + + diff --git a/R/prep.R b/R/prep.R new file mode 100644 index 0000000..0bea761 --- /dev/null +++ b/R/prep.R @@ -0,0 +1,68 @@ +prep = + setRefClass('prep', + fields = list(methods = 'list'), + methods = list( + add = function(name, ...) + { + p = as.list(match.call(expand.dots = TRUE)[-1]) + methods <<- c(methods, c(name, p)) + } + ) + ) + + +prep.autoscale = function(data, center = T, scale = F) +{ + if (is.logical(center) && center == T ) + center = apply(data, 2, mean) + else if (is.numeric(center)) + center = center + + if (is.logical(scale) && scale == T) + scale = apply(data, 2, sd) + else if(is.numeric(scale)) + scale = scale + + data = scale(data, center = center, scale = scale) + attr(data, 'scaled:center') = NULL + attr(data, 'scaled:scale') = NULL + attr(data, 'prep:center') = center + attr(data, 'prep:scale') = scale + + return (data) +} + +prep.snv = function(X) +{ + X = t(X) + X = scale(X, center = T, scale = T) + X = t(X) +} + +prep.savgol <- function(TT, fl, forder = 1, idorder = 0) +{ + nobj = nrow(TT) + TT2 = matrix(0, ncol = ncol(TT), nrow = nrow(TT)) + + for (i in 1:nobj) + { + T = TT[i,] + m <- length(T) + dorder = idorder + 1 + + fc <- (fl - 1)/2 + X <- outer(-fc:fc, 0:forder, FUN="^") + + Y <- pinv(X); + T2 <- convolve(T, rev(Y[dorder, ]), type="o") + T2 <- T2[(fc+1):(length(T2)-fc)] + TT2[i,] = T2 + } + TT2 +} + +pinv <- function(A) +{ + s <- svd(A) + s$v %*% diag(1/s$d) %*% t(s$u) +} \ No newline at end of file diff --git a/R/regcoeffs.R b/R/regcoeffs.R new file mode 100644 index 0000000..89cde75 --- /dev/null +++ b/R/regcoeffs.R @@ -0,0 +1,73 @@ +# class and methods for regression coefficients # +regcoeffs = function(coeffs, ...) UseMethod("regcoeffs") + +## default method ## +regcoeffs.default = function(coeffs) +{ + regcoeffs = list(values = coeffs) + regcoeffs$call = match.call() + + class(regcoeffs) = "regcoeffs" + + regcoeffs +} + +as.matrix.regcoeffs = function(regcoeffs, ...) +{ + return (regcoeffs$values) +} + +print.regcoeffs = function(regcoeffs, digits = 3, ...) +{ + cat('\nRegression coefficients (class regcoeffs)\n') + print(round(regcoeffs$values, digits)) +} + +plot.regcoeffs = function(regcoeffs, main = 'Regression coefficients', + xlab = 'Variables', ylab = 'Coefficients', + pch = 16, col = 'blue', ...) +{ + + + # remove Intercept + ncoeff = length(regcoeffs$values) + coeffs = regcoeffs$values[2:ncoeff] + ncoeff = ncoeff - 1 + + # select limits for y axis + ylim = max(abs(coeffs)) + + # chose plot type depending on number of coefficients + if (ncoeff < 30) { type = 'b' }else{ type = 'l' } + + # show plot + plot(coeffs, type = type, col = col, pch = pch, + main = main, + xlab = xlab, + ylab = ylab, + ylim = c(-ylim, ylim), + axes = F + ) + abline(c(0, 0), c(0, ncoeff)) + + if (is.null(dim(coeffs))) { names = names(coeffs)} + else {names = rownames(coeffs)} + + # show axes and labels if needed + if (ncoeff > 20) + { + atx = seq(1, ncoeff, ncoeff/10) + } + else + { + atx = 1:ncoeff + } + axis(1, at = atx, labels = names[atx], cex.axis = 0.85) + axis(2, cex.axis = 0.85) + if (ncoeff < 30) + { + text(1:ncoeff, coeffs, names, cex = 0.6, pos = 3, col = 'gray') + } + grid() + box() +} \ No newline at end of file diff --git a/R/selectCompNum.R b/R/selectCompNum.R new file mode 100755 index 0000000..70d0dc9 --- /dev/null +++ b/R/selectCompNum.R @@ -0,0 +1,4 @@ +selectCompNum = function(object, ...) +{ + UseMethod("selectCompNum") +} \ No newline at end of file diff --git a/data/.DS_Store b/data/.DS_Store new file mode 100755 index 0000000000000000000000000000000000000000..fd87c4b485ca39c9460a2380c094542e3f8e2486 GIT binary patch literal 6148 zcmeHKJ5B>Z41EqMf&eKc<&;!N++Y`-j2qme#UmQxmvu7#PfP!+4d&e4tz8fyWFV-B0p*9WuKl)x&m zb7X`fUP|;*i55e=oZ}_(s=&_C%OTNxNUZ$YyojyN`HQ7PD#x_RKr*n;fcCzWsowvW z{L2hB`IeF<8At~HD+4mF7xj!^7kBHY@6@}tP;aR!n%AjAp}+SC;14}VPA%y1qCWGg Wz|K);(Qzv$=0m^;Ns|ox0s}99turkE literal 0 HcmV?d00001 diff --git a/data/Jam.RData b/data/Jam.RData new file mode 100755 index 0000000000000000000000000000000000000000..c98c61e6b0da52670aead7141f65c5446c2d38e1 GIT binary patch literal 711 zcmV;&0yzC2iwFP!0000016@>0XcJKsohD6_$#?QkOQ^|KWS|)lp`g!1L`6wVEo4!w zA$>!}CMo@9?XI}!N`wTZiW})Be!yM*fFgn}-FDNR8&`shB2JQXAI}*`?z``ubMLwL zzM1OfTESoQD~jS$ToL={wwIGG`)@*-R8-r#@73G(Y1m#qW>Ldn`)BSmO&L8GPrePG z?@a9tpFYcv_gM}$0h@uHHO=Lh)(7T;pYHXZmLD zEWZAUovZv7_*{65n*}}#+%){m`Cr9if51}UO(R|!xbQ*m!fEe3iz06vI%Ck4053u3 z$eRFP@JKg!N#qG5UKDyLZXA~KYtTu&H1dYvQ~Z>p9?@_38tF&A5YEHvM@yHk?TEZ- z;6i)(=kMM=U=i3PcuC};ImDntrF=LiT~wdQgFdCuC(1){f~Z^6Pjd#3;zyxZ%%A20 zT#)(;ehm9$ebD2@JtZ9~@FDC6NEhO{j(7RPL0RwusM`&kA2_=ID&hvA+Y21^mFo1s zpK$b%<~W789{6-0lBl2Nl5)H+!s2|!xetDZc$k-lerohyQQu*yE~=mEqdsahce+pL zhlV_KAE^Eq^f=!;njiJSkMjWHg&lg3&-v+FvFuNu5*mFS6(u-c{|E8ekWRhLH|=#C zIBkR-pL%qhs6g3KvEV+fo6$_6+22JbXD zcDz--(&3w%aHmRZt+nk9cR8gTud3?{ZQg1Lwys}k)prEG%lVd!U$BEozQ1T~ tZE<)*0tjdg1lXvy%XbyjezkmK&5J1@m002;HZzuo& literal 0 HcmV?d00001 diff --git a/data/People.RData b/data/People.RData new file mode 100755 index 0000000000000000000000000000000000000000..df44f922d5674082a5e83c99a0b16476e69b3a2b GIT binary patch literal 1023 zcmV4-vNrPR# z(ewI$e%&*8Ytvwexb%Ttj3a&vT#f*p{KXCUKo|PiJYpUB^&soR`irbL;^fmmaQu-k zIS=ZX4_ye|H1HbmD(aO`=X1m_bKJo)bmtlOv-xGjC%~_EvpRCtcl9OKhrSA4?Pc}E zE7WrwJ`+wJ_`~1+9nU)WrsHmWxBR{Q_R4$P`s8+9#vLpI&k?%@$H2$BSzXZ?ck?;! z))_~<V9hpMF3e@sq$)PF~T8pKg{7Qee|@R_-Q{BO|Lx9H;qbX(B<976xK!Jk^$ z{B_jtY#MB1U0UD?)-8c<4e>Sb2-u>}R|cB_#-VQ^zWNDp!(fbl;s*wK-YoPC&ZD2D z5%{7Xo?8O`0sI=M%X4KOesl0q7@vdBEPQ6bYrsizj86gi9KM9{De!CHyq|fl1jxGy z&gcCU(4J@T;W?%^vh(M8WzRLtPawX4Jf4$0PXq7<>MTKz^?TRn@c;Gs|EB%f{pNmg zpLtIH?VJYZdClvD$bG%SxR>>t0_XL@y3@UX*3*Z2*5~gO>Z;vrpZtB|`f)zc@3kNA z8+Y69{|fyXVBj0P*)uqc_>C@fKmp`^#p{PY*Bzf3@Ei04pY^kR4*Foz;CwF|=W~G1 zE7oCO0kG!w(aq-bxrhFpw}Zh@HB&h3^)zgV$O!o~wvB z!T0@_Wn0F4^QelX*-5NSYDw8e*=}1>QrjeVUTT{$7OQ27D$$ON38_NG8C(!c)=9>p zV0J;RVa&*Zii~s=TjP@ItSF&(@Mu+N&FK#Hl? tjH29L&}n72VQ^BeF+BN2df~A%Syzz;8z}eRX#VVf{sLv31!Ub0004tgA|?O; literal 0 HcmV?d00001 diff --git a/data/Simdata.RData b/data/Simdata.RData new file mode 100755 index 0000000000000000000000000000000000000000..4bad91609e8f9b1efdce8210ce52553dfbc79722 GIT binary patch literal 91508 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

@Qk%CR)`q#cE3?MW&Uf@{`>JO6Eh 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 RW;p?a{lLQwu18=!`Cm_HmQDZw literal 0 HcmV?d00001 diff --git a/man/.DS_Store b/man/.DS_Store new file mode 100755 index 0000000000000000000000000000000000000000..78002ef920515f1ebd07731f110c1e910d8a16f9 GIT binary patch literal 6148 zcmeHKxlY4C5Pb_NB0-Uo(qBNtADGB=h$0c9gHO)unOQ&Bv3UVcxOU4)UPN^&89Id8*!>G)*Vt-BieWf>Xr9bZG5b5_dIRnmsGvEyDfdSsx zBEu6yx6Xhw;0$~+Am>9u6-*ZbQw6d?nv(cD}FM)kNj?l_nZM| z;GZ!N2H7l|a8vPa{kA>1YZK}{RYdYyX;9ei`~n!9XxDX5iMk4e$SF{4$e8{x~Ii!9Xx@Wen)7 zUA0SW%J0@++v8oE&|c9fj4L!Cut$#oba;+DR;T$BZNz2AV#rxIUekf`5U7NN3I_gx FfluuFDSQ9` literal 0 HcmV?d00001 diff --git a/test/matlab/People.csv b/test/matlab/People.csv new file mode 100644 index 0000000..32a96b9 --- /dev/null +++ b/test/matlab/People.csv @@ -0,0 +1 @@ +,Height,Weight,Hairleng,Shoesize,Age,Income,Beer,Wine,Sex,Swim,Region,IQ Lars,198,92,-1,48,48,4.50E+04,420,115,-1,98,-1,100 Peter,184,84,-1,44,33,3.30E+04,350,102,-1,92,-1,130 Rasmus,183,83,-1,44,37,3.40E+04,320,98,-1,91,-1,127 Lene,166,47,-1,36,32,2.80E+04,270,78,1,75,-1,112 Mette,170,60,1,38,23,2.00E+04,312,99,1,81,-1,110 Gitte,172,64,1,39,24,2.20E+04,308,91,1,82,-1,102 Jens,182,80,-1,42,35,3.00E+04,398,65,-1,85,-1,140 Erik,180,80,-1,43,36,3.00E+04,388,63,-1,84,-1,129 Lotte,169,51,1,36,24,2.30E+04,250,89,1,78,-1,98 Heidi,168,52,1,37,27,2.35E+04,260,86,1,78,-1,100 Kaj,183,81,-1,42,37,3.50E+04,345,45,-1,90,-1,105 Gerda,157,47,1,36,32,3.20E+04,235,92,1,70,-1,127 Anne,164,50,1,38,41,3.40E+04,255,134,1,76,-1,101 Britta,162,49,1,37,40,3.40E+04,265,124,1,75,-1,108 Magnus,180,82,-1,44,43,3.70E+04,355,82,-1,88,-1,109 Casper,180,81,-1,44,46,4.20E+04,362,90,-1,86,-1,113 Luka,185,82,-1,45,26,1.60E+04,295,180,-1,92,1,109 Federico,187,84,-1,46,27,1.65E+04,299,178,-1,95,1,119 Dona,168,50,1,37,49,3.40E+04,170,162,1,76,1,135 Fabrizia,166,49,1,36,21,1.40E+04,150,245,1,75,1,123 Lisa,158,46,1,34,30,1.80E+04,120,120,1,70,1,119 Benito,177,65,-1,41,26,1.80E+04,209,160,-1,86,1,120 Franko,180,72,-1,43,33,1.90E+04,236,175,-1,85,1,115 Alessandro,181,75,-1,43,42,3.10E+04,198,161,-1,83,1,105 Leonora,163,50,1,36,18,1.10E+04,143,136,1,75,1,102 Giuliana,162,50,1,36,20,1.15E+04,133,146,1,74,1,132 Giovanni,176,68,-1,42,50,3.60E+04,195,177,-1,82,1,96 Leonardo,175,67,1,42,55,3.80E+04,185,187,-1,80,1,105 Marta,165,51,1,36,36,2.60E+04,121,129,1,76,1,126 Rosetta,161,48,1,35,41,3.15E+04,116,196,1,75,1,120 Romeo,178,75,-1,42,30,2.40E+04,203,208,-1,81,1,118 Romina,160,48,1,35,40,3.10E+04,118,198,1,74,1,129 \ No newline at end of file diff --git a/test/matlab/People.mat b/test/matlab/People.mat new file mode 100644 index 0000000000000000000000000000000000000000..096fec61acac3000f52b53ab4c8609eb4e3d5f88 GIT binary patch literal 1152 zcmV-`1b_QYK~zjZLLfCRFd$7qR4ry{Y-KDUP;6mzW^ZzBIv`C!LrFF?EFeR2Wnpw> zWFT*DIv`YaWgtOtav(A@ATcyLGB-LfFd#B8F*6__ARr(hARr(hARr(hARr(hARr(h zARr(hARr(hARr(hARr(hARr(B00000000000ZB~{000180ssJboUPOCYmIRn$MNr} z5v3&&Q8GI-GdIIx55y5Ijn;0EnVaow)PyF-Omj0D_F%Cj&*)%;l+oPfi6SW;%rk$2 zvIm|hdBXd<-{;EJNfSApSJ!p@zQ3F6cm2NKd^0N>Z^qvySh)IcoSx@k5)zhYt<9Nc zZ}(1bxIDvC+{QiJ#}f?UF77CP6$7}Tn0t;F>f__D;h|!WaZ~*V@c_4Q9gmR34s1p} zmS7c9sFCF;N3G(^uoQJ_tJCf8_iUqu=OPd;B@NPmD1x zH#XMzgx+DHBry-gSb$Qs73n#O7sbXb(w>VHpNkTWjm9T6KANLY_k~)&K;u0}relU; zN!`zi#eMGin%mc%skndpX_&3H0qzji!R&n_IoeC-6QQ5$?KhiDGIp;v%cMfv;0ki4GN`EH-H(}l<0VG;P}x4S_RD7d zd#HmhvzI$Ve1JpPg%t8pfpT0x55BW(9@hqZ_H)BT?pv@A#VAdLy19Z*e87l>+Pa9d zcx9nhSkf|hhYlRZUZla+tp$zB;Wc3UwpHiswQ>%}E#&=n3w7qTt`+0n%9|s zwo4Xj((CjzUSnAA_op2GpZWj0`O?~#@7ZfIs%cVSw63eg_sX%_CiUB<@8V;3$J#5^ z?&Is#R;4*>V*P5>p1Y5~{l)kFFIsU1r_hdr%7;FkRa$-8)Q9x7{h(Crkgi8@9$mWc zR9>}dE=t3i^n6nB4$c1)y|F+94*&oFMF0Q*c%0*7U|_IdW?*0g(i}j{$N&MXNPHeB z%?ZS1iAC{wiMgr8P<4DD{R{~GY*_R&BALqx6^DQy^`rVn^$%tJ07v5+kq-a>07d}- z0C=3GmD@_gKoEwv)E@Mr_y|6NmtI;Ajc5we7m%qLWSVA1b`vjraxdK_2_+$rbqow) zmVEoqX*Vll%+JD@xqmOcO-%jF8vm+U3wG{J_J=oy0$uan%GjHN(!p%Bd?Z0A)w$UBq=RH37J8(tilyM`YHtsE!B}T?Q#3yv({~Gr*veR&nmU3;p zsr)bBC!~xb^PP71sd<5ezRqYfP}X_Paq}@Wmv`1T81!?!g%57$TEpLi`wRz5buOwL zVsw|Ls~h|cW@zbMG`bJMdR^FC`X*52OD;-xu+PX;ioQ=93!FFn?FfI-u;HM}o7NYo}`X^K!3H1F-NWOrXw0wvkY$oda9BkR%6Po(%#9=t_TK%0A SD3SVu|3N8;eFDGe>|CMboGPFI literal 0 HcmV?d00001 diff --git a/test/matlab/mand.m b/test/matlab/mand.m new file mode 100644 index 0000000..ac978a7 --- /dev/null +++ b/test/matlab/mand.m @@ -0,0 +1,23 @@ +rmin = 1; +rmax = 4; +rstep = 0.01; + +figure +hold on +for r = rmin:rstep:rmax + + xinit = 0.01; + + for j = 1:50 + xnext = r * xinit * (1 - xinit); + xinit = xnext; + end + + for j = 1:16 + xnext = r * xinit * (1 - xinit); + xinit = xnext; + scatter(r, xnext, '.b', 'SizeData', 2); + end + +end +hold off \ No newline at end of file diff --git a/test/matlab/pca_mvreplace.m b/test/matlab/pca_mvreplace.m new file mode 100644 index 0000000..82c73b7 --- /dev/null +++ b/test/matlab/pca_mvreplace.m @@ -0,0 +1,18 @@ +clear + +%% simple example +x = 1:10 +data = [x' 2*x' 3*x']; +data(5, 1) = NaN; +data(8, 3) = NaN; + +options = mdcheck('options'); +options.display = 'on'; + +[a, b, newdata] = mdcheck(data, options) + +newdata + +%% People example + +%load('People.mat') \ No newline at end of file diff --git a/test/matlab/test_pca.m b/test/matlab/test_pca.m new file mode 100644 index 0000000..2ed71e4 --- /dev/null +++ b/test/matlab/test_pca.m @@ -0,0 +1,30 @@ +clear +load 'People.mat'; + +options = pca('options'); +options.plots = 'none'; +options.preprocessing = {'autoscale'}; + +cvoptions = crossval('options'); +cvoptions.preprocessing = 2; +options.plots = 'none'; + +ncomp = 4; +model = pca(data, ncomp, options); +modelcv = crossval(data, [], 'pca', {'loo'}, ncomp, cvoptions); + +x1 = model.tsqs{1, 1}; +y1 = model.ssqresiduals{1, 1}; +x2 = modelcv.tsqs{1, 1}; +y2 = modelcv.ssqresiduals{1, 1}; +figure +hold on +scatter(x1, y1, '.b'); +scatter(x2, y2, '.r'); +hold off +text(x1, y1, obj_names); +text(x2, y2, obj_names); +line([model.detail.tsqlim{1} model.detail.tsqlim{1}], [min(y1) max(y1)]) +line([min(x1) max(x1)], [model.detail.reslim{1} model.detail.reslim{1}]) + +disp(model.detail.reslim) \ No newline at end of file diff --git a/test/test_mvreplace.R b/test/test_mvreplace.R new file mode 100755 index 0000000..5ad10b2 --- /dev/null +++ b/test/test_mvreplace.R @@ -0,0 +1,40 @@ +library(mdatools) +data(People) + +data = people +mvdata = data + +# add missing values +mvdata[c(1, 11, 17), 1] = NA +mvdata[3, 4] = NA +mvdata[c(5, 7, 9, 23), 8] = NA +mvdata[c(25, 27, 31, 32), 2] = NA +mvdata[c(4, 2), 12] = NA + +# make PCA models +mvdata2 = pca.mvreplace(mvdata, center = T, scale = T) +#res = prep.autoscale(data, scale = T) +#data = res$x + +show(cbind(data[, 1], mvdata[, 1], mvdata2[, 1])) + +model1 = pca(data, scale = T) +model2 = pca(mvdata2, scale = T) +par(mfrow = c(2, 2)) +plotScores(model1, show.labels = T) +plotScores(model2, show.labels = T) +plotLoadings(model1, show.labels = T) +plotLoadings(model2, show.labels = T) +par(mfrow = c(1, 1)) + +a = matrix(c(1:10, 2*(1:10), 3 * (1:10)), ncol = 3) +a[5, 1] = NA +a[8, 3] = NA + + +ap = pca.mvreplace(a, center = T, scale = F) + +show(cbind(a, round(ap, 2))) + + + diff --git a/test/test_pca.R b/test/test_pca.R new file mode 100755 index 0000000..fc21e40 --- /dev/null +++ b/test/test_pca.R @@ -0,0 +1,124 @@ +library(mdatools) + +do_people = T + +if (do_people == T) { + data(People) + data = people + data[4, 4] = NA + pcamodel = pca(data, ncomp = 8, scale = T, cv = 1, info = 'My first model') + pcamodel = selectCompNum(pcamodel, 5) +} else +{ + ncobj = 200 + ntobj = 100 + nvar = 2000 + ncomp = 10 + values = rnorm((ncobj + ntobj) * nvar, 2, 2) + data = matrix(values[1:(ncobj * nvar)], nrow = ncobj, ncol = nvar) + tdata = matrix(values[(ncobj * nvar + 1):length(values)], nrow = ntobj, ncol = nvar) + + t1 = system.time({ + pcamodel = pca(data, ncomp = ncomp, scale = T, cv = 1, test.data = tdata) + }) + t2 = system.time({ + pcamodel = selectCompNum(pcamodel, 8) + }) + show(t1) + show(t2) +} + +show(pcamodel) +summary(pcamodel) + +cat('\n1. Check variance plot for cv results\n') +par(mfrow = c(2, 2)) +plotVariance(pcamodel$cvres) +plotVariance(pcamodel$cvres, show.labels = T) +plotCumVariance(pcamodel$cvres) +plotCumVariance(pcamodel$cvres, show.labels = T) +readline('Press Enter to continue...') + +cat('\n2. Check residual plot for cv results\n') +par(mfrow = c(2, 2)) +plotResiduals(pcamodel$cvres) +plotResiduals(pcamodel$cvres, show.labels = T) +plotResiduals(pcamodel$cvres, ncomp = 2) +plotResiduals(pcamodel$cvres, ncomp = 2, show.labels = T) +readline('Press Enter to continue...') + +cat('\n3. print and summary for cv results\n') +print(pcamodel$cvres) +summary(pcamodel$cvres) +readline('Press Enter to continue...') + +cat('\n4. Check scores plots for cal results\n') +par(mfrow = c(2, 2)) +plotScores(pcamodel$calres) +plotScores(pcamodel$calres, comp = 1, cgroup = data[, 1]) +plotScores(pcamodel$calres, c(1, 3), show.labels = T) +plotScores(pcamodel$calres, c(1, 2), cgroup = data[, 1], + show.labels = T, show.colorbar = F, show.axes = F) +readline('Press Enter to continue...') + +cat('\n5. Check residuals plots for cal results\n') +par(mfrow = c(2, 2)) +plotResiduals(pcamodel$calres) +plotResiduals(pcamodel$calres, ncomp = 3, show.labels = T) +plotResiduals(pcamodel$calres, ncomp = 3, cgroup = data[, 1], show.colorbar = T) +plotResiduals(pcamodel$calres, ncomp = 3, cgroup = data[, 2], show.labels = T) +readline('Press Enter to continue...') + +cat('\n6. Check variance plot for cal results\n') +par(mfrow = c(2, 2)) +plotVariance(pcamodel$calres) +plotVariance(pcamodel$calres, show.labels = T) +plotCumVariance(pcamodel$calres) +plotCumVariance(pcamodel$calres, show.labels = T) +readline('Press Enter to continue...') + +cat('\n7. print and summary for cal results\n') +print(pcamodel$calres) +summary(pcamodel$calres) +readline('Press Enter to continue...') + +cat('\n8. Check scores plots for pca model\n') +par(mfrow = c(2, 2)) +plotScores(pcamodel, 1, show.labels = T) +plotScores(pcamodel) +plotScores(pcamodel, c(1, 3), show.labels = T, show.legend = F) +plotScores(pcamodel, c(1, 2), show.axes = F) +readline('Press Enter to continue...') + +cat('\n9. Check loadings plots for pls model\n') +par(mfrow = c(2, 2)) +plotLoadings(pcamodel) +plotLoadings(pcamodel, c(1, 3), show.labels = F) +plotLoadings(pcamodel, c(1, 3), type = 'b') +plotLoadings(pcamodel, 1:4, show.legend = T) +readline('Press Enter to continue...') + +cat('\n10. Check residuals plots for pca model\n') +par(mfrow = c(2, 2)) +plotResiduals(pcamodel) +plotResiduals(pcamodel, ncomp = 2) +plotResiduals(pcamodel, show.labels = T) +plotResiduals(pcamodel, ncomp = 1, show.legend = F) +readline('Press Enter to continue...') + +cat('\n11. Check variance plots for pca model\n') +par(mfrow = c(2, 2)) +plotVariance(pcamodel) +plotVariance(pcamodel, show.legend = F, show.labels = T) +plotCumVariance(pcamodel) +plotCumVariance(pcamodel, show.legend = F, show.labels = T) +readline('Press Enter to continue...') + +cat('\n12. Check summary plot for pca model\n') +par(mfrow = c(1, 1)) +plot(pcamodel, show.labels = F) +readline('Press Enter to continue...') + + + + diff --git a/test/test_pls.R b/test/test_pls.R new file mode 100644 index 0000000..a3f9596 --- /dev/null +++ b/test/test_pls.R @@ -0,0 +1,35 @@ +library('mdatools') + +data(Simdata) + +nobj = nrow(spectra) +nvar = ncol(spectra) + +varstep = 1 +yvar = 1 + +X = spectra[1:70, seq(1, nvar, varstep)] +y = conc[1:70, yvar] +# y[5] = y[5] * 2 +Xt = spectra[71:nobj, seq(1, nvar, varstep)] +yt = conc[71:nobj, yvar] + +plsmodel = pls(X, y, Xt = Xt, yt = yt, autoscale = 1, cv = 0) +plsmodel = pls.selectncomp(plsmodel, 2) + +summary(plsmodel) +plot(plsmodel) +#par(mfrow = c(1, 2)) +#plotYResiduals(plsmodel, show.labels = T) +#plotXResiduals(plsmodel, show.labels = T) +#par(mfrow = c(2, 2)) +#plotRMSE(plsmodel) +plotXYScores(plsmodel, 1, show.labels = T) +#plotRegcoeffs(plsmodel) +plotPredictions(plsmodel, show.labels = T) + +#par(mfrow = c(1, 1)) +##plotResiduals(plsmodel, ncomp = 1) +#plotXLoadings(plsmodel, 2) +#plotWeights(plsmodel) +#plotYVariance(plsmodel) diff --git a/test/test_prep.R b/test/test_prep.R new file mode 100644 index 0000000..37d81b5 --- /dev/null +++ b/test/test_prep.R @@ -0,0 +1,5 @@ +x = prep() +x$add('savgol', 1, 2, 3) +x$add('autoscale') + +x \ No newline at end of file