--- title: "Advanced usage of gipsDA" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Advanced usage of gipsDA} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 5 ) ``` ## Overview This vignette gives a more detailed overview of the main modeling options in `gipsDA`. It covers: - data preparation, - the differences between `gipslda()`, `gipsqda()`, and `gipsmultqda()`, - the `MAP`, `optimizer`, `max_iter`, `prior`, and `weighted_avg` arguments, - interpretation of permutation output, - prediction methods, - leave-one-out prediction, - model inspection and diagnostics. For a shorter first example, see the [Getting started](getting-started.html) vignette. ```{r} library(gipsDA) ``` ## Example data We use the built-in `iris` data set. ```{r} set.seed(42) train_id <- unlist( lapply(split(seq_len(nrow(iris)), iris$Species), sample, size = 35), use.names = FALSE ) train <- iris[train_id, ] test <- iris[-train_id, ] table(train$Species) table(test$Species) ``` ## Preparing data `gipsDA` models assume numeric predictors and a categorical grouping variable. Before fitting a model, it is usually useful to: - remove identifier columns, - remove or impute missing values, - remove constant or almost constant columns, - encode categorical predictors, - check whether predictors are on comparable scales. The last point is especially important for `gipsDA`. The method searches for permutation symmetries between variables. Such symmetries are most meaningful when variables are comparable, for example when they are measured in the same units or represent analogous sensor readings. ### Checking predictor types ```{r} str(train) ``` For formula-based usage, the response variable should be a factor or a categorical variable. ```{r} is.factor(train$Species) ``` The predictors in `iris` are already numeric. ```{r} vapply(train[, 1:4], is.numeric, logical(1)) ``` ### Scaling predictors Scaling may be useful when predictors are measured on very different scales. However, scaling should be done carefully: the center and scale should be estimated only on the training data and then applied to new data. ```{r} x_train <- train[, 1:4] x_test <- test[, 1:4] train_center <- vapply(x_train, mean, numeric(1)) train_scale <- vapply(x_train, sd, numeric(1)) train_scaled <- train test_scaled <- test train_scaled[, 1:4] <- scale( x_train, center = train_center, scale = train_scale ) test_scaled[, 1:4] <- scale( x_test, center = train_center, scale = train_scale ) ``` Then fit the model on the scaled data. ```{r} fit_scaled <- gipslda(Species ~ ., data = train_scaled) pred_scaled <- predict(fit_scaled, test_scaled) mean(pred_scaled$class == test_scaled$Species) ``` Scaling is not always necessary. It depends on whether the original variables are already comparable and whether scaling is meaningful for the application. ## Model choice The package provides three main classifiers. | Function | Covariance-matrix assumption | Typical use case | |---|---|---| | `gipslda()` | all classes share one projected covariance matrix | classes differ mainly in their means | | `gipsqda()` | each class has its own projected covariance matrix and its own permutation structure | classes may have different covariance patterns | | `gipsmultqda()` | each class has its own covariance matrix, but all classes share one permutation structure | classes may differ in scale, but share a dependency pattern | Fit all three models on the same data. ```{r} lda_fit <- gipslda(Species ~ ., data = train) qda_fit <- gipsqda(Species ~ ., data = train) joint_qda_fit <- gipsmultqda(Species ~ ., data = train) ``` Compare test-set accuracy. ```{r} lda_pred <- predict(lda_fit, test) qda_pred <- predict(qda_fit, test) joint_qda_pred <- predict(joint_qda_fit, test) c( gipslda = mean(lda_pred$class == test$Species), gipsqda = mean(qda_pred$class == test$Species), gipsmultqda = mean(joint_qda_pred$class == test$Species) ) ``` ## Model hierarchy The inclusion relations between the model classes are shown below. ```{r model-hierarchy, echo = FALSE, fig.cap = "The diagram illustrates the hierarchical relationships between the models.", out.width = "95%"} knitr::include_graphics("figures/models_hierarchy.png") ``` The figure shows that `gipsqda()` is contained in QDA, `gipslda()` is contained in LDA, and `gipsmultqda()` lies between `gipsqda()` and `gipslda()`. In practical terms, moving inward in the diagram means imposing stronger assumptions on the covariance structure. Stronger assumptions can reduce estimation variance, especially when the number of observations is small, but they may be too restrictive if the assumed structure is not present in the data. ## MAP: Maximum A Posteriori `MAP` stands for **Maximum A Posteriori**. The `MAP` argument controls how the covariance matrix is projected after the permutation search. When `MAP = TRUE`, the model selects the single most probable permutation structure and projects the covariance matrix onto the invariant space determined by that structure. ```{r} lda_map <- gipslda( Species ~ ., data = train, MAP = TRUE ) lda_map ``` When `MAP = FALSE`, the model uses posterior probabilities over retained permutation structures and computes a posterior-weighted projection. ```{r} lda_avg <- gipslda( Species ~ ., data = train, MAP = FALSE ) lda_avg ``` Conceptually, if \(S\) is an empirical covariance matrix and \(S_c\) is its projection under permutation structure \(c\), then the two approaches can be summarized as follows. For `MAP = TRUE`: \[ \hat{S} = S_{c^*}, \] where \[ c^* = \arg\max_c P(c \mid X). \] For `MAP = FALSE`: \[ \hat{S} = \sum_c P(c \mid X) S_c. \] The second option averages over several possible symmetry structures instead of using only one selected structure. By default, `gipsDA` stores posterior probabilities of retained permutations. For faster MAP-only fitting, set `store_probabilities = FALSE`. In that case, the selected MAP permutation is still stored and shown, but posterior probabilities are not stored in the fitted model object. With one predictor, `gipslda()` and `gipsqda()` require `MAP = TRUE`. The identity permutation `()` is the only possible permutation and has probability `1`, which is stored when `store_probabilities = TRUE`. `gipsmultqda()` requires at least two predictors. Both QDA fitters reject unused levels in the grouping factor with an error listing those levels. Use `droplevels(grouping)` before fitting to remove them. ## Interpreting permutation output Printed model output may contain permutations such as: ```text (1,2) (1,2)(3,4) (1,2,4,3) () ``` This notation is called cycle notation. For example, the cycle ```text (1,2,4,3) ``` means that the permutation maps: ```text 1 -> 2 2 -> 4 4 -> 3 3 -> 1 ``` For cycles of length greater than two, this should be understood as invariance under the cyclic permutation and its repeated applications. It does not necessarily mean full exchangeability under every possible pairwise swap of features in the cycle. A product of cycles such as: ```text (1,2)(3,4) ``` means that feature 1 is swapped with feature 2, and feature 3 is swapped with feature 4. The empty permutation: ```text () ``` is the identity permutation. It means that no non-trivial permutation symmetry was selected. In the context of `gipsDA`, a selected permutation structure describes invariance constraints imposed on the covariance estimator. For `gipslda()`, the permutation search is performed after centering observations by their class means and scaling the resulting within-class residuals to unit marginal variance. Therefore, the selected permutation describes symmetry in the standardized within-class covariance structure, not necessarily symmetry of the raw covariance matrix in the original units. For `gipsqda()` and `gipsmultqda()`, the class covariance matrices are projected on the original predictor scale. Because of this difference, selected permutations from LDA and QDA should not always be interpreted in exactly the same way when predictors are measured on different scales. ## Optimizer The `optimizer` argument controls how permutation structures are searched. | Value | Meaning | Typical use | |---|---|---| | `"BF"` | brute-force search | small number of dimensions, default for `p <= 10` | | `"MH"` | Metropolis-Hastings search | larger number of dimensions, default for `p > 10` | The brute-force optimizer searches the relevant permutation space exhaustively. It is deterministic, but its cost grows quickly with the number of features. ```{r} fit_bf <- gipsqda( Species ~ ., data = train, optimizer = "BF" ) fit_bf ``` For larger problems, use the Metropolis-Hastings optimizer. ```{r, eval = FALSE} fit_mh <- gipsqda( Species ~ ., data = train, optimizer = "MH", max_iter = 1000 ) ``` ## `max_iter` `max_iter` controls the number of Metropolis-Hastings iterations when `optimizer = "MH"`. Increasing `max_iter` gives the stochastic search more time to explore the permutation space, but also increases runtime. ```{r, eval = FALSE} fit_mh_100 <- gipsqda( Species ~ ., data = train, optimizer = "MH", max_iter = 100 ) fit_mh_1000 <- gipsqda( Species ~ ., data = train, optimizer = "MH", max_iter = 1000 ) ``` For `optimizer = "BF"`, `max_iter` is ignored. ## Class priors The `prior` argument specifies prior probabilities of classes. By default, priors are estimated from the training data. ```{r} lda_fit$prior ``` You can set them manually. ```{r} equal_prior <- rep(1 / length(levels(train$Species)), length(levels(train$Species))) names(equal_prior) <- levels(train$Species) lda_equal_prior <- gipslda( Species ~ ., data = train, prior = equal_prior ) lda_equal_prior$prior ``` The prior vector should contain one value per class and should sum to one. ```{r} sum(equal_prior) ``` Class priors affect posterior probabilities and may affect predicted classes, especially when classes overlap. ## `weighted_avg` in `gipslda()` The `weighted_avg` argument is specific to `gipslda()`. It controls how the pooled covariance matrix is constructed before the `gips` projection is applied. Let: - \(K\) be the number of classes, - \(n_k\) be the number of observations in class \(k\), - \(n = \sum_{k=1}^{K} n_k\) be the total number of observations, - \(S_k\) be the sample covariance matrix in class \(k\). With `weighted_avg = FALSE`, `gipslda()` uses the classic pooled covariance estimator: \[ S_{\mathrm{classic}} = \frac{1}{n - K} \sum_{k = 1}^{K} (n_k - 1) S_k. \] With `weighted_avg = TRUE`, `gipslda()` uses: \[ S_{\mathrm{weighted}} = \frac{1}{n} \sum_{k = 1}^{K} n_k S_k. \] Fit both variants. ```{r} lda_classic <- gipslda( Species ~ ., data = train, weighted_avg = FALSE ) lda_weighted <- gipslda( Species ~ ., data = train, weighted_avg = TRUE ) ``` Compare predictions. ```{r} pred_classic <- predict(lda_classic, test) pred_weighted <- predict(lda_weighted, test) c( classic = mean(pred_classic$class == test$Species), weighted = mean(pred_weighted$class == test$Species) ) ``` The two variants can behave differently when class sizes are imbalanced or when class-specific covariance estimates differ substantially. ## Formula interface The formula interface is usually the most convenient interface. ```{r} fit_formula <- gipslda( Species ~ Sepal.Length + Sepal.Width + Petal.Length + Petal.Width, data = train ) fit_formula ``` The shorthand `Species ~ .` uses all remaining columns as predictors. ```{r} fit_formula_short <- gipslda( Species ~ ., data = train ) fit_formula_short ``` Formula methods also support `subset`. ```{r} fit_subset <- gipslda( Species ~ ., data = iris, subset = Species != "setosa" ) fit_subset ``` ## Matrix interface The matrix interface separates predictors from class labels. ```{r} x <- as.matrix(iris[, 1:4]) grouping <- iris$Species fit_matrix_lda <- gipslda(x, grouping) fit_matrix_qda <- gipsqda(x, grouping) fit_matrix_joint <- gipsmultqda(x, grouping) ``` Prediction can then be performed on a matrix with the same columns. ```{r} predict(fit_matrix_lda, x[1:5, ])$class predict(fit_matrix_qda, x[1:5, ])$class predict(fit_matrix_joint, x[1:5, ])$class ``` ## Data-frame interface Predictors can also be passed as a data frame, with labels supplied separately. ```{r} x_df <- iris[, 1:4] y <- iris$Species fit_df_lda <- gipslda(x_df, y) fit_df_qda <- gipsqda(x_df, y) fit_df_joint <- gipsmultqda(x_df, y) ``` ```{r} predict(fit_df_lda, x_df[1:5, ])$class predict(fit_df_qda, x_df[1:5, ])$class predict(fit_df_joint, x_df[1:5, ])$class ``` ## Prediction output Prediction returns a list. ```{r} pred <- predict(lda_fit, test) names(pred) ``` The most important components are: | Component | Meaning | |---|---| | `class` | predicted class labels | | `posterior` | posterior class probabilities | | `x` | discriminant coordinates, when available | ```{r} head(pred$class) head(pred$posterior) ``` For `gipsqda()` and `gipsmultqda()`, the output has the same main structure. ```{r} names(qda_pred) names(joint_qda_pred) ``` ## Prediction methods for `gipslda()` For `gipslda()` objects, `predict()` supports the same main prediction-method names as `MASS::predict.lda()`: - `"plug-in"`, - `"predictive"`, - `"debiased"`. The default method is `"plug-in"`. ```{r} pred_plugin <- predict(lda_fit, test, method = "plug-in") pred_predictive <- predict(lda_fit, test, method = "predictive") pred_debiased <- predict(lda_fit, test, method = "debiased") ``` The `"plug-in"` method uses estimated parameters directly in the discriminant rule. The `"predictive"` and `"debiased"` methods are alternative LDA prediction rules following the `MASS::predict.lda()` interface. See `?MASS::predict.lda` for the original description of these prediction rules. For easy data sets, the predicted classes may be identical across methods. ```{r} c( plugin = mean(pred_plugin$class == test$Species), predictive = mean(pred_predictive$class == test$Species), debiased = mean(pred_debiased$class == test$Species) ) ``` Differences may be more visible in posterior probabilities. ```{r} head(pred_plugin$posterior) head(pred_predictive$posterior) head(pred_debiased$posterior) ``` ## Leave-one-out prediction for QDA models For `gipsqda()` and `gipsmultqda()` objects, leave-one-out cross-validation can be requested by omitting `newdata` and using `method = "looCV"`. In leave-one-out prediction, each training observation is classified as if it had not been used to fit the model. This provides an internal estimate of classification performance without creating a separate test set. ```{r} qda_loo <- predict(qda_fit, method = "looCV") joint_qda_loo <- predict(joint_qda_fit, method = "looCV") c( gipsqda_loo_accuracy = mean(qda_loo$class == train$Species), gipsmultqda_loo_accuracy = mean(joint_qda_loo$class == train$Species) ) ``` Use leave-one-out results as an internal diagnostic, not as a replacement for a proper independent test set when one is available. ## Inspecting fitted models The most readable way to inspect a fitted model is to print it. ```{r} print(lda_fit) ``` ```{r} print(qda_fit) ``` ```{r} print(joint_qda_fit) ``` The printed output shows the model call, prior probabilities, group means, and information about selected or averaged permutation structures. For a structural view of the object, use `summary()`. ```{r} summary(lda_fit) summary(qda_fit) summary(joint_qda_fit) ``` Fitted models are list-like S3 objects, so you can inspect their component names. ```{r} names(lda_fit) names(qda_fit) names(joint_qda_fit) ``` The exact set of components depends on the model family, but the most useful components are usually: | Component | Meaning | |---|---| | `prior` | prior probabilities of classes | | `counts` | number of observations in each class | | `means` | class-wise feature means | | `scaling` | scaling or decomposition information used for prediction | | `ldet` | log-determinant information for QDA-type models | | `lev` | class labels | | `call` | original function call | | `optimization_info` | information returned by the permutation optimization step | Inspect the optimization information directly. ```{r} lda_fit$optimization_info ``` ```{r} qda_fit$optimization_info ``` ```{r} joint_qda_fit$optimization_info ``` A compact helper can be useful when debugging fitted objects. ```{r} inspect_model <- function(object) { data.frame( component = names(object), class = vapply( object, function(x) paste(class(x), collapse = ", "), character(1) ), length = vapply(object, length, integer(1)), dim = vapply( object, function(x) { d <- dim(x) if (is.null(d)) "" else paste(d, collapse = " x ") }, character(1) ), row.names = NULL ) } ``` ```{r} inspect_model(lda_fit) ``` ```{r} inspect_model(qda_fit) ``` ```{r} inspect_model(joint_qda_fit) ``` ## LDA diagnostics For `gipslda()` objects, standard LDA-style diagnostics are available. ```{r} coef(lda_fit) ``` ```{r, fig.width = 6, fig.height = 5, fig.alt = "Plot of the fitted gipslda model in discriminant space."} plot(lda_fit) ``` ```{r, fig.width = 6, fig.height = 5, fig.alt = "Pairs plot of discriminant coordinates for the fitted gipslda model."} pairs(lda_fit, type = "std") ``` These methods are useful for inspecting the fitted discriminant directions and class separation. ## Practical workflow A typical workflow is: 1. prepare numeric predictors and a categorical response, 2. split data into training and test sets, 3. start with `gipslda()`, 4. compare with `gipsqda()` or `gipsmultqda()` if class-specific covariance structure may matter, 5. inspect `optimization_info`, 6. evaluate performance on held-out data. ```{r} fit_lda <- gipslda(Species ~ ., data = train) fit_qda <- gipsqda(Species ~ ., data = train) fit_joint <- gipsmultqda(Species ~ ., data = train) pred_lda <- predict(fit_lda, test) pred_qda <- predict(fit_qda, test) pred_joint <- predict(fit_joint, test) c( gipslda = mean(pred_lda$class == test$Species), gipsqda = mean(pred_qda$class == test$Species), gipsmultqda = mean(pred_joint$class == test$Species) ) ``` ## Troubleshooting ### The model is slow Use `optimizer = "MH"` for larger numbers of features. ```{r, eval = FALSE} fit <- gipsqda( Species ~ ., data = train, optimizer = "MH", max_iter = 1000 ) ``` Decrease `max_iter` for faster exploratory runs, then increase it for final runs. ### The output selects `()` The permutation `()` is the identity permutation. It means that the selected structure did not impose a non-trivial permutation symmetry. This can happen when: - the data do not contain strong exchangeability patterns, - the number of observations is too small, - predictors are not meaningfully comparable, - the optimizer did not find a more structured permutation. ### Predictions are identical across models This may happen on simple data sets. Compare posterior probabilities and inspect `optimization_info` for more detail. ```{r} head(pred_lda$posterior) head(pred_qda$posterior) head(pred_joint$posterior) ``` ### Training and test data have different preprocessing When preprocessing uses estimated quantities, such as means and standard deviations for scaling, estimate them on the training data only and apply the same transformation to test data. ## Summary The main advanced controls are: | Argument | Use | |---|---| | `MAP = TRUE` | use the single Maximum A Posteriori permutation | | `MAP = FALSE` | average covariance projections using posterior probabilities | | `optimizer = "BF"` | exhaustive search, typically for `p <= 10` | | `optimizer = "MH"` | stochastic search, typically for `p > 10` | | `max_iter` | number of Metropolis-Hastings iterations | | `prior` | manually set class prior probabilities | | `weighted_avg` | choose the pooled covariance estimator in `gipslda()` | | `store_probabilities` | whether to store posterior probabilities of retained permutations | The three main models are: | Function | Use when | |---|---| | `gipslda()` | classes can share one covariance structure | | `gipsqda()` | each class may have its own covariance structure | | `gipsmultqda()` | classes may have different covariance matrices but one shared permutation structure |