From e6e1dc1ceaf115f84ec86411c3b7960ffe6c049c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Sat, 8 Aug 2026 14:56:01 +0200 Subject: [PATCH] Add cluster export and t-SNE caching Adds export support for clustering results (predicted cluster, soft memberships, and t-SNE coordinates) with new helper functions (.mlClusteringExportResultsToData, AddCluster/SoftMemberships/TSNE helpers and caching). Computes soft memberships for fuzzy-cmeans, model-based and RF (from proximity) and normalizes them. Moves t-SNE to reusable cached routine handling duplicate rows. Fixes randomForest predict calls to pass only predictor columns (classification/regression/DALEX explain). Updates QML UI to expose export options and updates .gitignore/.Rbuildignore for .positai/.claude. --- .Rbuildignore | 2 + .gitignore | 1 + R/commonMachineLearningClassification.R | 7 +- R/commonMachineLearningClustering.R | 148 +++++++++++++++++++----- R/mlClassificationRandomForest.R | 8 +- R/mlClusteringDensityBased.R | 5 +- R/mlClusteringFuzzyCMeans.R | 6 +- R/mlClusteringHierarchical.R | 5 +- R/mlClusteringKMeans.R | 5 +- R/mlClusteringModelBased.R | 6 +- R/mlClusteringRandomForest.R | 17 ++- R/mlRegressionRandomForest.R | 2 +- inst/qml/common/ui/ExportResults.qml | 109 +++++++++++------ inst/qml/mlClusteringDensityBased.qml | 1 + inst/qml/mlClusteringFuzzyCMeans.qml | 2 + inst/qml/mlClusteringHierarchical.qml | 1 + inst/qml/mlClusteringKMeans.qml | 1 + inst/qml/mlClusteringModelBased.qml | 2 + inst/qml/mlClusteringRandomForest.qml | 2 + 19 files changed, 247 insertions(+), 83 deletions(-) diff --git a/.Rbuildignore b/.Rbuildignore index e4e5d139..a7771289 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -3,3 +3,5 @@ ^.*\.Rproj$ ^\.Rproj\.user$ ^tests/upgrades$ +^\.positai$ +^\.claude$ diff --git a/.gitignore b/.gitignore index 6d92b821..9c7b6510 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ Rproj.user /.Rprofile /renv _processedLockFile.lock +.positai diff --git a/R/commonMachineLearningClassification.R b/R/commonMachineLearningClassification.R index 3e7a01e2..ee38c85d 100644 --- a/R/commonMachineLearningClassification.R +++ b/R/commonMachineLearningClassification.R @@ -1116,12 +1116,15 @@ } .calcAUCScore.randomForestClassification <- function(AUCformula, train, test, typeData, levelVar, options, noOfTrees, noOfPredictors, ...) { - typeData <- typeData[, -which(colnames(typeData) == "levelVar")] + predictors <- unlist(options[["predictors"]]) + predictors <- predictors[predictors != ""] + typeData <- typeData[, predictors, drop = FALSE] + testPred <- test[, predictors, drop = FALSE] fit <- randomForest::randomForest( x = typeData, y = factor(levelVar), ntree = noOfTrees, mtry = noOfPredictors, sampsize = ceiling(options[["baggingFraction"]] * nrow(train)), importance = TRUE, keep.forest = TRUE ) - score <- predict(fit, test, type = "prob")[, "TRUE"] + score <- predict(fit, testPred, type = "prob")[, "TRUE"] return(score) } diff --git a/R/commonMachineLearningClustering.R b/R/commonMachineLearningClustering.R index b646a2c3..dbfa336d 100644 --- a/R/commonMachineLearningClustering.R +++ b/R/commonMachineLearningClustering.R @@ -33,8 +33,9 @@ .mlClusteringReadData <- function(dataset, options) { predictors <- unlist(options[["predictors"]]) predictors <- predictors[predictors != ""] + dataset <- dataset[, predictors, drop = FALSE] dataset <- jaspBase::excludeNaListwise(dataset, predictors) - if (options[["scaleVariables"]] && length(unlist(options[["predictors"]])) > 0) { + if (options[["scaleVariables"]] && length(predictors) > 0) { dataset <- .scaleNumericData(dataset) } return(dataset) @@ -104,11 +105,13 @@ } .mlClusteringComputeResults <- function(dataset, options, jaspResults, ready, type) { - if (!is.null(jaspResults[["clusterResult"]])) { + clusterResult <- if (!is.null(jaspResults[["clusterResult"]])) jaspResults[["clusterResult"]]$object else NULL + needsSoftMemberships <- isTRUE(options[["addSoftMemberships"]]) && type %in% c("cmeans", "randomForest", "modelbased") + if (!is.null(clusterResult) && (!needsSoftMemberships || !is.null(clusterResult[["softMemberships"]]))) { return() } - .setSeedJASP(options) # Set the seed to make results reproducible if (ready) { + .setSeedJASP(options) # Set the seed to make results reproducible p <- try({ clusterResult <- switch(type, "kmeans" = .kMeansClustering(dataset, options, jaspResults), @@ -166,7 +169,6 @@ if (!ready) { return() } - .mlClusteringComputeResults(dataset, options, jaspResults, ready, type = type) clusterResult <- jaspResults[["clusterResult"]]$object if (options[["modelOptimization"]] != "manual") { criterion <- switch(options[["modelOptimizationMethod"]], @@ -338,30 +340,17 @@ return() } clusterResult <- jaspResults[["clusterResult"]]$object - .setSeedJASP(options) # Set the seed to make results reproducible - startProgressbar(2) - progressbarTick() - duplicates <- which(duplicated(dataset)) - if (length(duplicates) > 0) { - dataset <- dataset[-duplicates, ] - } - if (is.null(jaspResults[["tsneOutput"]])) { - tsne <- Rtsne::Rtsne(as.matrix(dataset), perplexity = nrow(dataset) / 4, check_duplicates = FALSE) - jaspResults[["tsneOutput"]] <- createJaspState(tsne) - jaspResults[["tsneOutput"]]$dependOn(options = c("predictors", "setSeed", "seed")) - } else { - tsne <- jaspResults[["tsneOutput"]]$object - } + tsneOutput <- .mlClusteringGetTsneOutput(dataset, options, jaspResults) + uniqueRows <- tsneOutput[["uniqueRows"]] predictions <- clusterResult[["pred.values"]] ncolors <- clusterResult[["clusters"]] if (type == "densitybased") { ncolors <- ncolors + 1 predictions[predictions == 0] <- gettext("Noisepoint") } - if (length(duplicates) > 0) { - predictions <- predictions[-duplicates] - } - plotData <- data.frame(x = tsne$Y[, 1], y = tsne$Y[, 2], cluster = predictions) + predictions <- predictions[uniqueRows] + coordinates <- tsneOutput[["coordinates"]][uniqueRows, , drop = FALSE] + plotData <- data.frame(x = coordinates[, 1], y = coordinates[, 2], cluster = predictions) plotData$cluster <- factor(plotData$cluster) xBreaks <- jaspGraphs::getPrettyAxisBreaks(plotData$x) yBreaks <- jaspGraphs::getPrettyAxisBreaks(plotData$y) @@ -376,12 +365,40 @@ axis.text.x = ggplot2::element_blank(), axis.text.y = ggplot2::element_blank()) if (options[["tsneClusterPlotLabels"]]) { - p <- p + ggrepel::geom_text_repel(ggplot2::aes(label = rownames(dataset), x = x, y = y), hjust = -1, vjust = 1, data = plotData, seed = 1) + p <- p + ggrepel::geom_text_repel(ggplot2::aes(label = rownames(dataset)[uniqueRows], x = x, y = y), hjust = -1, vjust = 1, data = plotData, seed = 1) } - progressbarTick() plot$plotObject <- p } +.mlClusteringGetTsneOutput <- function(dataset, options, jaspResults) { + if (!is.null(jaspResults[["tsneOutput"]])) { + tsneOutput <- jaspResults[["tsneOutput"]]$object + if (!is.null(tsneOutput[["coordinates"]]) && !is.null(tsneOutput[["uniqueRows"]])) { + return(tsneOutput) + } + } + .setSeedJASP(options) + rows <- asplit(dataset, 1L) + uniqueRows <- unname(which(!duplicated(rows))) + if (length(uniqueRows) < 5L) { + jaspBase:::.quitAnalysis(gettext("t-SNE requires at least 5 unique rows in the predictor data.")) + } + startProgressbar(1L) + coordinates <- Rtsne::Rtsne( + as.matrix(dataset[uniqueRows, , drop = FALSE]), + perplexity = length(uniqueRows) / 4, + check_duplicates = FALSE + )$Y + progressbarTick() + tsneOutput <- list( + coordinates = coordinates[match(rows, rows[uniqueRows]), , drop = FALSE], + uniqueRows = uniqueRows + ) + jaspResults[["tsneOutput"]] <- createJaspState(tsneOutput) + jaspResults[["tsneOutput"]]$dependOn(options = c("predictors", "setSeed", "seed", "scaleVariables")) + return(tsneOutput) +} + .mlClusteringPlotElbow <- function(dataset, options, jaspResults, ready, position) { if (!is.null(jaspResults[["optimPlot"]]) || !options[["elbowMethodPlot"]] || options[["modelOptimization"]] == "manual") { return() @@ -429,11 +446,23 @@ plot$plotObject <- p } -.mlClusteringAddPredictionsToData <- function(dataset, options, jaspResults, ready) { - if (!ready || !options[["addPredictions"]] || options[["predictionsColumn"]] == "") { +.mlClusteringExportResultsToData <- function(dataset, options, jaspResults, ready) { + if (!ready) { + return() + } + .mlClusteringAddClusterMembershipToData(dataset, options, jaspResults) + .mlClusteringAddSoftMembershipsToData(dataset, options, jaspResults) + .mlClusteringAddTsneCoordinatesToData(dataset, options, jaspResults) +} + +.mlClusteringAddClusterMembershipToData <- function(dataset, options, jaspResults) { + if (!isTRUE(options[["addPredictions"]]) || is.null(options[["predictionsColumn"]]) || options[["predictionsColumn"]] == "") { return() } clusterResult <- jaspResults[["clusterResult"]]$object + if (is.null(clusterResult) || is.null(clusterResult[["pred.values"]])) { + return() + } if (is.null(jaspResults[["predictionsColumn"]])) { predictions <- clusterResult[["pred.values"]] predictionsColumn <- rep(NA, max(as.numeric(rownames(dataset)))) @@ -444,6 +473,67 @@ } } +.mlClusteringAddSoftMembershipsToData <- function(dataset, options, jaspResults) { + if (!isTRUE(options[["addSoftMemberships"]]) || is.null(options[["softMembershipsColumn"]]) || options[["softMembershipsColumn"]] == "") { + return() + } + clusterResult <- jaspResults[["clusterResult"]]$object + .mlClusteringAddScaleColumnsToData( + dataset = dataset, + values = clusterResult[["softMemberships"]], + columnPrefix = options[["softMembershipsColumn"]], + resultPrefix = "softMembershipsColumn", + dependencies = c("addSoftMemberships", "softMembershipsColumn", .mlClusteringDependencies(options)), + jaspResults = jaspResults + ) +} + +.mlClusteringAddTsneCoordinatesToData <- function(dataset, options, jaspResults) { + if (!isTRUE(options[["addTsneCoordinates"]]) || is.null(options[["tsneCoordinatesColumn"]]) || options[["tsneCoordinatesColumn"]] == "") { + return() + } + if (!is.null(jaspResults[["tsneCoordinatesColumn1"]]) && !is.null(jaspResults[["tsneCoordinatesColumn2"]])) { + return() + } + tsneOutput <- .mlClusteringGetTsneOutput(dataset, options, jaspResults) + coordinates <- tsneOutput[["coordinates"]] + if (is.null(coordinates) || nrow(coordinates) != nrow(dataset)) { + return() + } + .mlClusteringAddScaleColumnsToData( + dataset = dataset, + values = coordinates, + columnPrefix = options[["tsneCoordinatesColumn"]], + resultPrefix = "tsneCoordinatesColumn", + dependencies = c("addTsneCoordinates", "tsneCoordinatesColumn", "setSeed", "seed", .mlClusteringDependencies(options)), + jaspResults = jaspResults + ) +} + +.mlClusteringAddScaleColumnsToData <- function(dataset, values, columnPrefix, resultPrefix, dependencies, jaspResults) { + if (is.null(values)) { + return() + } + values <- as.matrix(values) + if (nrow(values) != nrow(dataset) || ncol(values) == 0L) { + return() + } + rowIndices <- as.integer(rownames(dataset)) + exportedValues <- matrix(NA_real_, nrow = max(rowIndices), ncol = ncol(values)) + exportedValues[rowIndices, ] <- values + columnPrefix <- decodeColNames(columnPrefix) + for (i in seq_len(ncol(values))) { + resultName <- paste0(resultPrefix, i) + if (!is.null(jaspResults[[resultName]])) { + next + } + columnName <- paste0(columnPrefix, "_", i) + jaspResults[[resultName]] <- createJaspColumn(columnName = columnName) + jaspResults[[resultName]]$dependOn(options = dependencies) + jaspResults[[resultName]]$setScale(exportedValues[, i]) + } +} + .mlClusteringTableMeans <- function(dataset, options, jaspResults, ready, position) { if (!is.null(jaspResults[["clusterMeansTable"]]) || !options[["tableClusterMeans"]]) { return() @@ -468,11 +558,11 @@ clusterTitles <- gettextf("Cluster %s", clusterLevels) clusterMeans <- NULL for (i in clusterLevels) { - clusterSubset <- subset(dataset, clusters == i) + clusterSubset <- dataset[clusters == i, , drop = FALSE] clusterMeans <- rbind(clusterMeans, colMeans(clusterSubset)) } - clusterMeans <- cbind(cluster = clusterTitles, data.frame(clusterMeans[, options[["predictors"]], drop = FALSE])) - colnames(clusterMeans) <- c("cluster", as.character(options[["predictors"]])) + clusterMeans <- cbind(cluster = clusterTitles, data.frame(clusterMeans)) + colnames(clusterMeans) <- c("cluster", colnames(dataset)) table$setData(clusterMeans) } diff --git a/R/mlClassificationRandomForest.R b/R/mlClassificationRandomForest.R index a9f6848c..152d51cb 100644 --- a/R/mlClassificationRandomForest.R +++ b/R/mlClassificationRandomForest.R @@ -155,7 +155,7 @@ mlClassificationRandomForest <- function(jaspResults, dataset, options, ...) { result[["train"]] <- trainingSet result[["test"]] <- testSet result[["testIndicatorColumn"]] <- testIndicatorColumn - result[["classes"]] <- predict(testFit, newdata = dataset) + result[["classes"]] <- predict(testFit, newdata = dataset[, options[["predictors"]], drop = FALSE]) result[["oobAccuracy"]] <- 1 - testFit[["err.rate"]][length(testFit[["err.rate"]])] result[["varImp"]] <- plyr::arrange(data.frame( Variable = as.factor(names(testFit[["importance"]][, 1])), @@ -170,11 +170,11 @@ mlClassificationRandomForest <- function(jaspResults, dataset, options, ...) { result[["valid"]] <- validationSet result[["oobValidStore"]] <- oobAccuracy } - result[["explainer"]] <- DALEX::explain(result[["model"]], type = "multiclass", data = result[["train"]], y = result[["train"]][, options[["target"]]] , predict_function = function(model, data) predict(model, newdata = data, type = "prob")) + result[["explainer"]] <- DALEX::explain(result[["model"]], type = "multiclass", data = result[["train"]][, options[["predictors"]], drop = FALSE], y = result[["train"]][, options[["target"]]] , predict_function = function(model, data) predict(model, newdata = data, type = "prob")) if (nlevels(result[["testReal"]]) == 2) { - result[["explainer_fi"]] <- DALEX::explain(result[["model"]], type = "classification", data = result[["train"]], y = as.numeric(result[["train"]][, options[["target"]]]) - 1, predict_function = function(model, data) predict(model, newdata = data, type = "response")) + result[["explainer_fi"]] <- DALEX::explain(result[["model"]], type = "classification", data = result[["train"]][, options[["predictors"]], drop = FALSE], y = as.numeric(result[["train"]][, options[["target"]]]) - 1, predict_function = function(model, data) predict(model, newdata = data, type = "response")) } else { - result[["explainer_fi"]] <- DALEX::explain(result[["model"]], type = "multiclass", data = result[["train"]], y = result[["train"]][, options[["target"]]] , predict_function = function(model, data) predict(model, newdata = data, type = "prob")) + result[["explainer_fi"]] <- DALEX::explain(result[["model"]], type = "multiclass", data = result[["train"]][, options[["predictors"]], drop = FALSE], y = result[["train"]][, options[["target"]]] , predict_function = function(model, data) predict(model, newdata = data, type = "prob")) } return(result) } diff --git a/R/mlClusteringDensityBased.R b/R/mlClusteringDensityBased.R index aa665d4f..1da56127 100644 --- a/R/mlClusteringDensityBased.R +++ b/R/mlClusteringDensityBased.R @@ -25,10 +25,11 @@ mlClusteringDensityBased <- function(jaspResults, dataset, options, ...) { ready <- .mlClusteringReady(options) # Compute results and create the model summary table + .mlClusteringComputeResults(dataset, options, jaspResults, ready, type = "densitybased") .mlClusteringTableSummary(dataset, options, jaspResults, ready, position = 1, type = "densitybased") - # If the user wants to add the clusters to the data set - .mlClusteringAddPredictionsToData(dataset, options, jaspResults, ready) + # Export selected results to the data set + .mlClusteringExportResultsToData(dataset, options, jaspResults, ready) # Create the cluster information table .mlClusteringTableInformation(options, jaspResults, ready, position = 2, type = "densitybased") diff --git a/R/mlClusteringFuzzyCMeans.R b/R/mlClusteringFuzzyCMeans.R index 46c53483..963858e2 100644 --- a/R/mlClusteringFuzzyCMeans.R +++ b/R/mlClusteringFuzzyCMeans.R @@ -25,10 +25,11 @@ mlClusteringFuzzyCMeans <- function(jaspResults, dataset, options, ...) { ready <- .mlClusteringReady(options) # Compute results and create the model summary table + .mlClusteringComputeResults(dataset, options, jaspResults, ready, type = "cmeans") .mlClusteringTableSummary(dataset, options, jaspResults, ready, position = 1, type = "cmeans") - # If the user wants to add the clusters to the data set - .mlClusteringAddPredictionsToData(dataset, options, jaspResults, ready) + # Export selected results to the data set + .mlClusteringExportResultsToData(dataset, options, jaspResults, ready) # Create the cluster information table .mlClusteringTableInformation(options, jaspResults, ready, position = 2, type = "cmeans") @@ -112,6 +113,7 @@ mlClusteringFuzzyCMeans <- function(jaspResults, dataset, options, ...) { result[["BIC"]] <- sumSquares[["tot.within.ss"]] + log(length(fit[["cluster"]])) * ncol(fit[["centers"]]) * nrow(fit[["centers"]]) result[["Silh_score"]] <- silhouettes[["avg.width"]] result[["silh_scores"]] <- silhouettes[["clus.avg.widths"]] + result[["softMemberships"]] <- fit[["membership"]] if (options[["modelOptimization"]] != "manual") { result[["silhStore"]] <- avgSilh result[["aicStore"]] <- aicStore diff --git a/R/mlClusteringHierarchical.R b/R/mlClusteringHierarchical.R index ac8a0902..f9e27de9 100644 --- a/R/mlClusteringHierarchical.R +++ b/R/mlClusteringHierarchical.R @@ -25,10 +25,11 @@ mlClusteringHierarchical <- function(jaspResults, dataset, options, ...) { ready <- .mlClusteringReady(options) # Compute results and create the model summary table + .mlClusteringComputeResults(dataset, options, jaspResults, ready, type = "hierarchical") .mlClusteringTableSummary(dataset, options, jaspResults, ready, position = 1, type = "hierarchical") - # If the user wants to add the clusters to the data set - .mlClusteringAddPredictionsToData(dataset, options, jaspResults, ready) + # Export selected results to the data set + .mlClusteringExportResultsToData(dataset, options, jaspResults, ready) # Create the cluster information table .mlClusteringTableInformation(options, jaspResults, ready, position = 2, type = "hierarchical") diff --git a/R/mlClusteringKMeans.R b/R/mlClusteringKMeans.R index 40b7c23c..64d086a7 100644 --- a/R/mlClusteringKMeans.R +++ b/R/mlClusteringKMeans.R @@ -25,10 +25,11 @@ mlClusteringKMeans <- function(jaspResults, dataset, options, ...) { ready <- .mlClusteringReady(options) # Compute results and create the model summary table + .mlClusteringComputeResults(dataset, options, jaspResults, ready, type = "kmeans") .mlClusteringTableSummary(dataset, options, jaspResults, ready, position = 1, type = "kmeans") - # If the user wants to add the clusters to the data set - .mlClusteringAddPredictionsToData(dataset, options, jaspResults, ready) + # Export selected results to the data set + .mlClusteringExportResultsToData(dataset, options, jaspResults, ready) # Create the cluster information table .mlClusteringTableInformation(options, jaspResults, ready, position = 2, type = "kmeans") diff --git a/R/mlClusteringModelBased.R b/R/mlClusteringModelBased.R index 0947ff31..7f782a1d 100644 --- a/R/mlClusteringModelBased.R +++ b/R/mlClusteringModelBased.R @@ -25,10 +25,11 @@ mlClusteringModelBased <- function(jaspResults, dataset, options, ...) { ready <- .mlClusteringReady(options) # Compute results and create the model summary table + .mlClusteringComputeResults(dataset, options, jaspResults, ready, type = "modelbased") .mlClusteringTableSummary(dataset, options, jaspResults, ready, position = 1, type = "modelbased") - # If the user wants to add the clusters to the data set - .mlClusteringAddPredictionsToData(dataset, options, jaspResults, ready) + # Export selected results to the data set + .mlClusteringExportResultsToData(dataset, options, jaspResults, ready) # Create the cluster information table .mlClusteringTableInformation(options, jaspResults, ready, position = 2, type = "modelbased") @@ -128,6 +129,7 @@ mclustBIC <- mclust::mclustBIC } result[["parameters"]] <- fit[["parameters"]] result[["modelName"]] <- fit[["modelName"]] + result[["softMemberships"]] <- fit[["z"]] return(result) } diff --git a/R/mlClusteringRandomForest.R b/R/mlClusteringRandomForest.R index b0a0a14f..eb0e2495 100644 --- a/R/mlClusteringRandomForest.R +++ b/R/mlClusteringRandomForest.R @@ -25,10 +25,11 @@ mlClusteringRandomForest <- function(jaspResults, dataset, options, ...) { ready <- .mlClusteringReady(options) # Compute results and create the model summary table + .mlClusteringComputeResults(dataset, options, jaspResults, ready, type = "randomForest") .mlClusteringTableSummary(dataset, options, jaspResults, ready, position = 1, type = "randomForest") - # If the user wants to add the clusters to the data set - .mlClusteringAddPredictionsToData(dataset, options, jaspResults, ready) + # Export selected results to the data set + .mlClusteringExportResultsToData(dataset, options, jaspResults, ready) # Create the cluster information table .mlClusteringTableInformation(options, jaspResults, ready, position = 2, type = "randomForest") @@ -131,6 +132,18 @@ mlClusteringRandomForest <- function(jaspResults, dataset, options, ...) { result[["Silh_score"]] <- silhouettes[["avg.width"]] result[["silh_scores"]] <- silhouettes[["clus.avg.widths"]] result[["fit"]] <- fit + # Soft memberships from average proximity to members of each cluster + proximity <- fit[["proximity"]] + softMemberships <- matrix(0, nrow = nrow(proximity), ncol = clusters) + for (cluster in seq_len(clusters)) { + members <- which(predictions == cluster) + if (length(members) > 0) { + softMemberships[, cluster] <- rowMeans(proximity[, members, drop = FALSE]) + } + } + rowTotals <- rowSums(softMemberships) + rowTotals[rowTotals == 0] <- 1 + result[["softMemberships"]] <- softMemberships / rowTotals if (options[["modelOptimization"]] != "manual") { result[["silhStore"]] <- avgSilh result[["aicStore"]] <- aicStore diff --git a/R/mlRegressionRandomForest.R b/R/mlRegressionRandomForest.R index f599455d..ab9e9c40 100644 --- a/R/mlRegressionRandomForest.R +++ b/R/mlRegressionRandomForest.R @@ -127,7 +127,7 @@ mlRegressionRandomForest <- function(jaspResults, dataset, options, ...) { importance = TRUE, keep.forest = TRUE ) # Use the specified model to make predictions for dataset - dataPredictions <- predict(testFit, newdata = dataset) + dataPredictions <- predict(testFit, newdata = dataset[, options[["predictors"]], drop = FALSE]) # Create results object result <- list() result[["model"]] <- testFit diff --git a/inst/qml/common/ui/ExportResults.qml b/inst/qml/common/ui/ExportResults.qml index 9002096d..f66a243c 100644 --- a/inst/qml/common/ui/ExportResults.qml +++ b/inst/qml/common/ui/ExportResults.qml @@ -20,62 +20,101 @@ import QtQuick import QtQuick.Layouts import JASP.Controls -Group +Section { - property alias enabled: exportSection.enabled - property alias showSave: saveGroup.visible - property bool showProbs: false + property alias showSave: saveGroup.visible + property bool showProbs: false + property bool showSoftMemberships: false + property bool showTsne: false - id: exportSection - title: qsTr("Export Results") + title: qsTr("Export Results") + columns: 1 - CheckBox + CheckBox { - id: addPredictions - name: "addPredictions" - text: qsTr("Add predictions to data") - info: qsTr("Generates a new column in your dataset with the values of your regression result. This gives you the option to inspect, cluster, or predict the generated values.") + id: addPredictions + name: "addPredictions" + text: qsTr("Add predictions to data") + info: qsTr("Adds a column with the predicted cluster membership (clustering) or predicted values (regression/classification) to the dataset.") - ComputedColumnField + ComputedColumnField { - id: predictionsColumn - name: "predictionsColumn" - text: qsTr("Column name") - placeholderText: qsTr("e.g., predicted") - fieldWidth: 120 - enabled: addPredictions.checked - info: qsTr("The column name for the predicted values.") + id: predictionsColumn + name: "predictionsColumn" + text: qsTr("Column name") + placeholderText: qsTr("e.g., predicted") + fieldWidth: 120 + enabled: addPredictions.checked + info: qsTr("The column name for the predicted values.") } CheckBox { - id: probabilities - name: "addProbabilities" - text: qsTr("Add probabilities (classification only)") - visible: showProbs - info: qsTr("In classification analyses, append the predicted probabilities for each class to the data. For neural networks, this option provides the output of the final layer.") + id: probabilities + name: "addProbabilities" + text: qsTr("Add probabilities (classification only)") + visible: showProbs + info: qsTr("In classification analyses, append the predicted probabilities for each class to the data. For neural networks, this option provides the output of the final layer.") + } + } + + CheckBox + { + id: addSoftMemberships + name: "addSoftMemberships" + text: qsTr("Add soft memberships / posteriors") + visible: showSoftMemberships + info: qsTr("Adds one column per cluster with the soft membership or posterior probability of each observation. The column name is used as a prefix (e.g., membership_1, membership_2).") + + TextField + { + name: "softMembershipsColumn" + text: qsTr("Column name prefix") + placeholderText: qsTr("e.g., membership") + fieldWidth: 120 + enabled: addSoftMemberships.checked + info: qsTr("Prefix for the soft membership columns. One column is created per cluster.") + } + } + + CheckBox + { + id: addTsneCoordinates + name: "addTsneCoordinates" + text: qsTr("Add t-SNE coordinates") + visible: showTsne + info: qsTr("Adds the two-dimensional t-SNE embedding (dimension 1 and 2) for each observation. These are the same coordinates used in the t-SNE cluster plot. The column name is used as a prefix (e.g., tsne_1, tsne_2).") + + TextField + { + name: "tsneCoordinatesColumn" + text: qsTr("Column name prefix") + placeholderText: qsTr("e.g., tsne") + fieldWidth: 120 + enabled: addTsneCoordinates.checked + info: qsTr("Prefix for the t-SNE coordinate columns (suffixes _1 and _2 are added).") } } Group { - id: saveGroup + id: saveGroup CheckBox { - name: "saveModel" - text: qsTr("Save trained model") - info: qsTr("When clicked, the model is exported to the specified file path.") + name: "saveModel" + text: qsTr("Save trained model") + info: qsTr("When clicked, the model is exported to the specified file path.") FileSelector { - name: "savePath" - label: qsTr("Save as") - placeholderText: qsTr("e.g., location/model.jaspML") - filter: "*.jaspML" - save: true - fieldWidth: 180 * preferencesModel.uiScale - info: qsTr("The file path for the saved model.") + name: "savePath" + label: qsTr("Save as") + placeholderText: qsTr("e.g., location/model.jaspML") + filter: "*.jaspML" + save: true + fieldWidth: 180 * preferencesModel.uiScale + info: qsTr("The file path for the saved model.") } } } diff --git a/inst/qml/mlClusteringDensityBased.qml b/inst/qml/mlClusteringDensityBased.qml index a2fbe3ec..d061eb14 100644 --- a/inst/qml/mlClusteringDensityBased.qml +++ b/inst/qml/mlClusteringDensityBased.qml @@ -59,6 +59,7 @@ Form { enabled: vars.predictorCount > 1 showSave: false + showTsne: true } Section diff --git a/inst/qml/mlClusteringFuzzyCMeans.qml b/inst/qml/mlClusteringFuzzyCMeans.qml index b3b837ac..a51ff3bb 100644 --- a/inst/qml/mlClusteringFuzzyCMeans.qml +++ b/inst/qml/mlClusteringFuzzyCMeans.qml @@ -54,6 +54,8 @@ Form { enabled: vars.predictorCount > 1 showSave: false + showTsne: true + showSoftMemberships: true } Section diff --git a/inst/qml/mlClusteringHierarchical.qml b/inst/qml/mlClusteringHierarchical.qml index 853756b2..47f6635c 100644 --- a/inst/qml/mlClusteringHierarchical.qml +++ b/inst/qml/mlClusteringHierarchical.qml @@ -60,6 +60,7 @@ Form { enabled: vars.predictorCount > 1 showSave: false + showTsne: true } Section diff --git a/inst/qml/mlClusteringKMeans.qml b/inst/qml/mlClusteringKMeans.qml index 8b28b1a8..ed68c32a 100644 --- a/inst/qml/mlClusteringKMeans.qml +++ b/inst/qml/mlClusteringKMeans.qml @@ -54,6 +54,7 @@ Form { enabled: vars.predictorCount > 1 showSave: false + showTsne: true } Section diff --git a/inst/qml/mlClusteringModelBased.qml b/inst/qml/mlClusteringModelBased.qml index b4e67419..f849d7af 100644 --- a/inst/qml/mlClusteringModelBased.qml +++ b/inst/qml/mlClusteringModelBased.qml @@ -60,6 +60,8 @@ Form { enabled: vars.predictorCount > 1 showSave: false + showTsne: true + showSoftMemberships: true } Section diff --git a/inst/qml/mlClusteringRandomForest.qml b/inst/qml/mlClusteringRandomForest.qml index 438b3710..5812d73a 100644 --- a/inst/qml/mlClusteringRandomForest.qml +++ b/inst/qml/mlClusteringRandomForest.qml @@ -55,6 +55,8 @@ Form { enabled: vars.predictorCount > 1 showSave: false + showTsne: true + showSoftMemberships: true } Section