Skip to content

Revision of qcscore_model_spe branch - #64

Merged
drighelli merged 2 commits into
qcscore_model_spefrom
qscore_model_spe
Jul 31, 2026
Merged

Revision of qcscore_model_spe branch#64
drighelli merged 2 commits into
qcscore_model_spefrom
qscore_model_spe

Conversation

@BenedettaBanzi

Copy link
Copy Markdown
Collaborator
  • Fixed some issues with QS computation with custom formula;
  • updated QC functions documentation;
  • added more customazable settings to plot functions;
  • function name standardization to Quality Score (QScore);
  • updated vignettes.

…lected metrics. Function name standardization to QScore. plotCellsFovs with distinct parameters for points and FOV number sizes. Updated documentation and vignettes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates SpaceTrooper’s Quality Score (QS) APIs and documentation, including renaming/standardizing “QCScore” to “QScore”, expanding plot customization controls, and refreshing vignettes/man pages for the new interface.

Changes:

  • Renames QS-related functions/outputs (e.g., computeQCScore()computeQScore(), QC_scoreQScore) and updates package docs/vignettes accordingly.
  • Adds/extends support for user-specified QS model formulas and updates QS model training/application documentation.
  • Expands plotting customization parameters (e.g., point size/alpha and label sizes) and updates associated docs/vignettes.

Reviewed changes

Copilot reviewed 14 out of 32 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
vignettes/SpaceTrooper_utilities.Rmd Updates utilities vignette for QScore naming and adds custom-formula example.
vignettes/RNA_tutorial_vignette.rmd Updates RNA tutorial to use renamed QS APIs and updated plotting args.
vignettes/Protein_tutorial_vignette.Rmd Updates protein tutorial to use renamed QS APIs and updated plotting args.
README.md Updates quickstart example to computeQScore()/computeQScoreFlags().
R/spatialQCPlots.R Adds plot customization parameters and refactors plotting function signatures.
R/readMerfish.R Updates warning message wording for Quality Score terminology.
R/QC.R Renames/updates QS computation pipeline, formula handling, model application helper, and extensive roxygen updates.
NEWS.md Updates release notes for 1.1.8 changes.
NAMESPACE Updates exports to new function names (adds .applyQScoreModel, swaps QCScore exports for QScore).
man/trainModel.Rd Updates documentation wording for ridge logistic regression model fitting.
man/spatialPerCellQC.Rd Updates parameter descriptions and expanded behavior documentation.
man/plotZoomFovsMap.Rd Documents new plotZoomFovsMap arguments and naming.
man/plotCellsFovs.Rd Documents new plotCellsFovs arguments and naming.
man/getModelFormula.Rd Updates usage to metricList (but needs consistency fixes).
man/dot-computeXenMerTrainSet.Rd Documentation wording update for internal helper.
man/dot-computeCosmxTrainSet.Rd Documentation wording update for internal helper.
man/dot-computeCosmxProteinTrainSet.Rd Documentation wording update for internal helper.
man/dot-computeBorderDistanceCosMx.Rd Documentation wording updates for FOV terminology.
man/dot-checkSkw.Rd Documentation wording update for internal helper.
man/dot-applyQScoreModel.Rd Adds documentation for .applyQScoreModel() (example currently inconsistent with actual API).
man/computeTrainDF.Rd Updates docs to QScore_train naming and other wording.
man/computeThresholdFlags.Rd Updates docs wording and clarifies outputs.
man/computeSpatialOutlier.Rd Updates docs wording and clarifies behavior.
man/computeQScoreFlags.Rd Adds documentation for computeQScoreFlags().
man/computeQScore.Rd Adds documentation for computeQScore().
man/computeQCScoreFlags.Rd Removes deprecated/renamed QCScoreFlags man page.
man/computeQCScore.Rd Removes deprecated/renamed computeQCScore man page.
man/computeOutliersQScore.Rd Renames and updates outlier-computation documentation for QScore.
man/computeLambda.Rd Updates usage to new signature (but needs argument/example consistency fixes).
man/checkOutliers.Rd Updates docs wording and naming to Quality Score.
man/applyQCScoreModel.Rd Removes deprecated/renamed applyQCScoreModel man page.
DESCRIPTION Bumps version to 1.1.8.
Files not reviewed (18)
  • man/checkOutliers.Rd: Generated file
  • man/computeLambda.Rd: Generated file
  • man/computeQScore.Rd: Generated file
  • man/computeQScoreFlags.Rd: Generated file
  • man/computeSpatialOutlier.Rd: Generated file
  • man/computeThresholdFlags.Rd: Generated file
  • man/computeTrainDF.Rd: Generated file
  • man/dot-applyQScoreModel.Rd: Generated file
  • man/dot-checkSkw.Rd: Generated file
  • man/dot-computeBorderDistanceCosMx.Rd: Generated file
  • man/dot-computeCosmxProteinTrainSet.Rd: Generated file
  • man/dot-computeCosmxTrainSet.Rd: Generated file
  • man/dot-computeXenMerTrainSet.Rd: Generated file
  • man/getModelFormula.Rd: Generated file
  • man/plotCellsFovs.Rd: Generated file
  • man/plotZoomFovsMap.Rd: Generated file
  • man/spatialPerCellQC.Rd: Generated file
  • man/trainModel.Rd: Generated file
Comments suppressed due to low confidence (4)

man/computeLambda.Rd:18

  • computeLambda documentation is inconsistent with the new signature computeLambda(modelMatrix, trainDF): it still documents a modelFormula argument and the examples call computeLambda(df_train, modform). This will mislead users and fail during example checks.

This issue also appears on line 39 of the same file.
man/computeLambda.Rd:42

  • The example still uses the old API (getModelFormula(metadata(spe)$formula_variables) and computeLambda(df_train, modform)), but the function signatures were updated. This example will error if executed.
    vignettes/SpaceTrooper_utilities.Rmd:513
  • The vignette currently tells users the I(abs(log2AspectRatio) * as.numeric(dist_border < 50)) term "must have spaces around the * and < operators". Requiring exact whitespace makes the API brittle and is easy for users to get wrong; it’s better to document that whitespace is irrelevant (and ensure the implementation normalizes it).
    R/QC.R:369
  • computeLambda() docs still say it "constructs the design matrix via model.matrix()", but the function now takes modelMatrix as an argument and no longer builds it internally. This makes the description misleading.
#' Internally, the function:
#'   constructs the design matrix via \code{model.matrix()},
#'   runs k-fold cross-validation of ridge logistic regression using `cv.glmnet` with `alpha = 0`,
#'   extracts and returns `ridge_cv$lambda.min`.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread R/QC.R
df <- ctx$df; out_var <- ctx$out_var; tech <- ctx$tech

train_df <- computeTrainDF(df, out_var, tech, verbose)
model_formula <- getModelFormula(names(out_var))

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a wrong suggestion. model_formula should be built ALWAYS using the computed variables contained in out_var, not just when modelFormula is null. out_var is the vector of metrics resulting from the checks made by .prepQCContext. If the number of outliers is not enough, that metric is automatically discarded from the vector

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non è così, se fornisci un argomento (modelFormula), stai dando la possibilità di usarlo.
Altrimenti è inutile fornire tale argomento.

Quindi il funzionamento logico è:
se la formula in argomento è NULL, allora si usano le componenti che abbiamo impostato noi di default. Quindi questo è il comportamento standard suggerito dagli autori (noi).
In alternativa, se l'utente vuole sperimentare altre metriche, scrive una formula con queste metriche e la passa a modelFormula, che quindi andrà a sostituire la formula decisa da noi.

@BenedettaBanzi BenedettaBanzi Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ma l'argomento infatti viene utilizzato! Viene usato in questa parte qui, per costruire la prima versione della formula custom che poi però deve essere controllata:

if (!is.null(modelFormula)) { model_formula <- modelFormula metricList <- attr(terms(as.formula(modelFormula)), "term.labels") metricList <- metricList[!grepl(":", metricList, fixed=TRUE)] if("I(abs(log2AspectRatio) * as.numeric(dist_border < 50))" %in% metricList) { metricList <- gsub("I\\(abs\\((log2AspectRatio)\\) \\* as\\.numeric\\((dist_border) < 50\\)\\)", "log2AspectRatio", metricList) } }

L'utente può scegliere che metriche mettere nella formula, ma non può mettere quello che vuole. Devono essere tra le metriche che noi abbiamo approfondito, come abbiamo illustrato nella supplementary note. Altrimenti che senso ha aver inserito una tabella di metriche e tutta la spiegazione sul perché le abbiamo scelte? Se avessimo dovuto lasciare all'utente il compito di cercarsi quali metriche combinare assieme, non avrebbe avuto senso pubblicare proprio SpaceTrooper.

model_formula <- getModelFormula(names(out_var))

Questa riga di codice ci deve essere sempre perché viene dopo questo:

ctx <- .prepQCContext(spe, metricList, verbose) df <- ctx$df; out_var <- ctx$out_var; tech <- ctx$tech

Il che significa che stiamo veramente dando all'utente la possibilità di togliere o mettere delle metriche nella sua formula, ma ci stiamo anche assicurando che la formula che usa rispetti i criteri che valgono per la formula di default e che devono valere sempre. Ossia:

  1. log2SignalDensity deve esserci per forza perché è il requisito minimo per calcolare il Quality Score;
  2. per ciascuna metrica ci deve essere un numero minimo di outlier pari allo 0.01% della numerosità del dataset;
  3. per dataset di Xenium e MERFISH/MERSCOPE non si deve usare il border effect.

Se non rispettiamo il primo criterio, non abbiamo la più pallida idea di quello che si ritrova in mano l'utente per fare QC, e mettere una metrica per cui non c'è un numero sufficiente di outlier, o il border effect per tecnologie in cui non lo abbiamo osservato, vuol dire addestrare il modello su una variabile che non è informativa per quel dataset, col rischio di andare a filtrare delle cellule su un criterio che è sbagliato.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, capisco quello che dici.

A questo punto, dobbiamo però separare chiaramente libertà dell’utente e validazione del metodo.

Secondo me qui vanno distinti due problemi diversi.

Il primo è permettere davvero all’utente di usare una formula personalizzata. Se esiste un argomento chiamato modelFormula, la formula fornita deve essere quella effettivamente usata dal modello. Se l’utente sceglie una formula poco sensata, se ne assume la responsabilità; al massimo possiamo avvertirlo, ma non dovremmo sostituirla silenziosamente con una formula ricostruita da noi.

Con il codice attuale, invece, modelFormula viene usata soltanto per estrarre le metriche e poi viene sovrascritta da:

model_formula <- getModelFormula(names(out_var))

Quindi l’utente non sta realmente scegliendo la formula, ma soltanto un sottoinsieme di variabili e combinato nel modo stabilito da noi (ad es imponendo che ci siano sempre gli interaction term).

Il secondo problema è fare dei controlli di validità. Questo è indipendente dal primo e può essere gestito separatamente verificando, per esempio:

  • che le variabili indicate esistano in colData;
  • che log2SignalDensity sia presente, se la consideriamo un requisito strutturale del QScore;
  • che alcune metriche siano compatibili con la tecnologia;
  • che le metriche validate da SpaceTrooper abbiano un numero sufficiente di outlier.

Le metriche fuori dalla whitelist potrebbero comunque essere accettate, producendo un warning:

unsupported <- setdiff(
    all.vars(as.formula(modelFormula)),
    validated_metrics
)

if (length(unsupported)) {
    warning(
        "The following predictors have not been validated by SpaceTrooper ",
        "and are used at the user's own risk: ",
        paste(unsupported, collapse = ", ")
    )
}

La scelta della formula dovrebbe quindi restare distinta dai controlli:

model_formula <- if (is.null(modelFormula)) {
    getModelFormula(names(out_var))
} else {
    modelFormula
}

In sintesi: noi possiamo controllare che la formula sia applicabile e avvertire l’utente quando esce dal perimetro validato, ma non dovremmo modificarne automaticamente la struttura. Altrimenti modelFormula non è davvero una formula custom.

Comment thread R/QC.R
Comment on lines 757 to 760
#' @param formulaVars A named character vector mapping variable names
#' (e.g. `"log2SignalDensity"`, `"Area_um"`, etc.) to their corresponding
#' outlier label columns, typically from
#' `metadata(spe)$formula_variables`.
Comment thread R/QC.R
out_var <- metricList
if ("log2AspectRatio" %in% out_var) {
out_var[grep("log2AspectRatio", out_var)] <-
"I(abs(log2AspectRatio) * as.numeric(dist_border<50))"
Comment thread R/QC.R
Comment on lines +1261 to 1265
#' spe_test <- applyQScoreModel(
#' spe=spe_test,
#' qcModel=qc_model,
#' scoreName="QC_score_transferred"
#' scoreName="QScore_transferred"
#' )
Comment on lines +58 to +62
spe_test <- applyQScoreModel(
spe=spe_test,
qcModel=qc_model,
scoreName="QScore_transferred"
)
Comment thread man/getModelFormula.Rd
Comment on lines +7 to 13
getModelFormula(metricList)
}
\arguments{
\item{formulaVars}{A named character vector mapping variable names
(e.g. `"log2SignalDensity"`, `"Area_um"`, etc.) to their corresponding
outlier label columns, typically from
`metadata(spe)$formula_variables`.}
Comment thread R/QC.R
Comment on lines +433 to +437
#' `~(log2SignalDensity + Area_um + I(abs(log2AspectRatio) * as.numeric(dist_border < 50)) + log2Ctrl_total_ratio)^2`.
#' When user-provided, the formula must follow the same default syntax and
#' removed (or added) terms should be written exactly as in the default formula,
#' e.g. `I(abs(log2AspectRatio) * as.numeric(dist_border < 50))` must have spaces
#' around the `*` and `<` operators.
Comment thread R/QC.R
Comment on lines +466 to 469
computeQScore <- function(spe, bestLambda=NULL, modelFormula=NULL, verbose=FALSE) {
stopifnot(is(spe, "SpatialExperiment"))
if (dim(spe[,spe$total == 0])[2] != 0) {
warning(paste0(dim(spe[,spe$total == 0])[2],
Comment thread R/QC.R
Comment on lines 352 to 356
#' @param trainDF `data.frame`
#' A data frame for training that must include:
#' A data frame for QS model training that must include:
#' Predictor columns: All columns referenced in the formula returned by `getModelFormula()`.
#' `qscore_train` A binary (0/1) response vector to be modeled.
#' @param modelFormula `character`
Comment thread R/QC.R
Comment on lines 1216 to 1220
#' @description
#' Apply a previously trained QC score model to a new SpatialExperiment object.
#' Internal: applies a previously trained Quality Score model to a new SpatialExperiment object.
#' See details for important considerations when applying a model to a
#' different dataset.
#' @details
@drighelli

Copy link
Copy Markdown
Owner

@copilot based on your review, provide possible solutions to the problems in a different PR, and add the old computeQCScore function with the .Deprecated instruction based on the bioconductor guidelines present here:
https://contributions.bioconductor.org/deprecation.html

Copilot AI commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

@drighelli I've opened a new pull request, #65, to work on those changes. Once the pull request is ready, I'll request review from you.

Comment thread R/spatialQCPlots.R
Comment on lines 64 to +68
plotCellsFovs <- function(spe, sampleId=unique(spe$sample_id),
pointCol="firebrick", numbersCol="black",
alphaNumbers=0.8, fovDim=metadata(spe)$fov_dim,
size=0.05, alpha=0.8,
pointCol="firebrick", pointSize=0.05,
pointAlpha=0.8, numbersCol="black",
numberSize= 1, numbersAlpha=0.8,
fovDim=metadata(spe)$fov_dim,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread R/spatialQCPlots.R
Comment on lines 495 to +498
map <- plotCellsFovs(spefovs, pointCol=mapPointCol,
numbersCol=mapNumbersCol, alphaNumbers=mapAlphaNumbers,
sampleId=NULL, size=csize, alpha=calpha, scaleBar=scaleBarMap)
pointSize=mapPointSize, pointAlpha=mapPointAlpha,
numbersCol=fovNumbersCol, numberSize=fovNumberSize,
numbersAlpha=fovNumbersAlpha, sampleId=NULL, scaleBar=scaleBarMap)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread DESCRIPTION
Type: Package
Title: SpaceTrooper performs Quality Control analysis of Image-Based spatial
Version: 1.1.7
Version: 1.1.8

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BenedettaBanzi never do the version bump together with all the modifications.
It requires a different commit.

Comment thread R/QC.R
df <- ctx$df; out_var <- ctx$out_var; tech <- ctx$tech

train_df <- computeTrainDF(df, out_var, tech, verbose)
model_formula <- getModelFormula(names(out_var))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, capisco quello che dici.

A questo punto, dobbiamo però separare chiaramente libertà dell’utente e validazione del metodo.

Secondo me qui vanno distinti due problemi diversi.

Il primo è permettere davvero all’utente di usare una formula personalizzata. Se esiste un argomento chiamato modelFormula, la formula fornita deve essere quella effettivamente usata dal modello. Se l’utente sceglie una formula poco sensata, se ne assume la responsabilità; al massimo possiamo avvertirlo, ma non dovremmo sostituirla silenziosamente con una formula ricostruita da noi.

Con il codice attuale, invece, modelFormula viene usata soltanto per estrarre le metriche e poi viene sovrascritta da:

model_formula <- getModelFormula(names(out_var))

Quindi l’utente non sta realmente scegliendo la formula, ma soltanto un sottoinsieme di variabili e combinato nel modo stabilito da noi (ad es imponendo che ci siano sempre gli interaction term).

Il secondo problema è fare dei controlli di validità. Questo è indipendente dal primo e può essere gestito separatamente verificando, per esempio:

  • che le variabili indicate esistano in colData;
  • che log2SignalDensity sia presente, se la consideriamo un requisito strutturale del QScore;
  • che alcune metriche siano compatibili con la tecnologia;
  • che le metriche validate da SpaceTrooper abbiano un numero sufficiente di outlier.

Le metriche fuori dalla whitelist potrebbero comunque essere accettate, producendo un warning:

unsupported <- setdiff(
    all.vars(as.formula(modelFormula)),
    validated_metrics
)

if (length(unsupported)) {
    warning(
        "The following predictors have not been validated by SpaceTrooper ",
        "and are used at the user's own risk: ",
        paste(unsupported, collapse = ", ")
    )
}

La scelta della formula dovrebbe quindi restare distinta dai controlli:

model_formula <- if (is.null(modelFormula)) {
    getModelFormula(names(out_var))
} else {
    modelFormula
}

In sintesi: noi possiamo controllare che la formula sia applicabile e avvertire l’utente quando esce dal perimetro validato, ma non dovremmo modificarne automaticamente la struttura. Altrimenti modelFormula non è davvero una formula custom.

Comment thread R/QC.R
model_matrix <- model.matrix(as.formula(modelFormula), data=trainDF)
model_matrix <- .dropModelIntercept(model_matrix)
ridge_cv <- cv.glmnet(model_matrix, trainDF$qcscore_train,
computeLambda <- function(modelMatrix, trainDF) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BenedettaBanzi come mai hai cambiato qui?
ogni cambio di argomento diventa un "deprecated"

@drighelli
drighelli merged commit a608da4 into devel Jul 31, 2026
5 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants