From 857a36cce7feeb5e5b15ec1f62cc5d4644fdeb68 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Tue, 1 Sep 2026 15:41:59 -0400 Subject: [PATCH 1/4] fix(proteinGroups): Adjust annotateProteinInfoFromIndra to handle protein groups --- R/annotateProteinInfoFromIndra.R | 254 +++++++++++++----- man/annotateProteinInfoFromIndra.Rd | 46 +++- man/dot-populateEntityInformationWithGilda.Rd | 8 +- ...populateEntityInformationWithIndraCogex.Rd | 11 +- man/dot-populateKinaseInfoInDataFrame.Rd | 4 +- man/dot-populatePhophataseInfoInDataFrame.Rd | 4 +- ...ulateTranscriptionFactorInfoInDataFrame.Rd | 4 +- man/dot-populateUniprotIdsInDataFrame.Rd | 4 +- .../test-annotateProteinInfoFromIndra.R | 217 +++++++++++++++ 9 files changed, 463 insertions(+), 89 deletions(-) diff --git a/R/annotateProteinInfoFromIndra.R b/R/annotateProteinInfoFromIndra.R index 512266a..f26f454 100644 --- a/R/annotateProteinInfoFromIndra.R +++ b/R/annotateProteinInfoFromIndra.R @@ -6,36 +6,61 @@ #' #' @param df output of \code{\link[MSstats]{groupComparison}} function's #' comparisonResult table. Must contain a \code{Protein} column whose -#' values are interpreted according to \code{proteinIdType}. +#' values are interpreted according to \code{proteinIdType}. A value +#' may name a protein group -- several identifiers for the same +#' quantified analyte joined by \code{";"}, e.g. +#' \code{"P13747;P23132"} -- in which case every member is grounded +#' independently and the results are pooled onto the row (see +#' Details). #' @param proteinIdType A character string specifying the type of analyte #' identifier in the \code{Protein} column. One of #' \code{"Uniprot"}, \code{"Uniprot_Mnemonic"}, \code{"Hgnc_Name"}, or #' \code{"Metabolite"}. The \code{"Metabolite"} value treats inputs as #' metabolite names and grounds them through Gilda, keeping whatever #' namespace Gilda returns (CHEBI / PUBCHEM / CHEMBL / ...). +#' @details +#' Protein group members are split on \code{";"}, each member is stripped +#' of its PTM site suffix and grounded on its own, and the groundings of +#' all members are concatenated -- in member order, deduplicated on +#' \code{(EntityNamespace, EntityId)} -- into the semicolon-joined +#' \code{Entity*} columns. This is the same representation used when a +#' single input grounds to several candidates, and +#' \code{\link{getSubnetworkFromIndra}} fans each pair out into its own +#' query node. +#' +#' Because \code{IsTranscriptionFactor} / \code{IsKinase} / +#' \code{IsPhosphatase} describe one gene, they are left \code{NA} +#' whenever a row carries more than one grounding. A group whose members +#' all resolve to the same gene collapses to a single grounding and does +#' get the flags. #' @return A data frame with the following columns: #' \describe{ #' \item{Protein}{Character. The original identifier from the input.} -#' \item{GlobalProtein}{Character. The input identifier without the PTM +#' \item{GlobalProtein}{Character. The input identifier with the PTM #' site suffix (typically \code{_}, e.g. -#' \code{_S148}) stripped, used as the grounding key.} -#' \item{UniprotId}{Character. The Uniprot ID of the protein, or +#' \code{_S148}) stripped from each protein group member, used as +#' the grounding key. \code{NA} when the input holds no usable +#' identifier.} +#' \item{UniprotId}{Character. The Uniprot ID of the protein, +#' semicolon-joined over the members of a protein group, or #' \code{NA} for \code{"Hgnc_Name"} and \code{"Metabolite"} inputs.} #' \item{EntityNamespace}{Character. The grounding namespace -#' (e.g. \code{"HGNC"}, \code{"CHEBI"}). When a single input grounds -#' to multiple candidates, namespaces are semicolon-joined and +#' (e.g. \code{"HGNC"}, \code{"CHEBI"}). When a row grounds to +#' multiple candidates -- whether from a protein group or from an +#' ambiguous single input -- namespaces are semicolon-joined and #' positionally aligned with \code{EntityId} and \code{EntityName}.} #' \item{EntityId}{Character. The bare grounding identifier within its #' namespace (e.g. \code{"1097"} for HGNC, \code{"28748"} for #' CHEBI). Semicolon-joined when multi-grounded.} #' \item{EntityName}{Character. The canonical display name from the -#' grounding source. Semicolon-joined when multi-grounded.} +#' grounding source. Semicolon-joined when multi-grounded, with +#' \code{"NA"} in the positions whose name lookup failed.} #' \item{IsTranscriptionFactor}{Logical. \code{NA} for -#' \code{proteinIdType == "Metabolite"}.} +#' \code{proteinIdType == "Metabolite"} and for multi-grounded rows.} #' \item{IsKinase}{Logical. \code{NA} for -#' \code{proteinIdType == "Metabolite"}.} +#' \code{proteinIdType == "Metabolite"} and for multi-grounded rows.} #' \item{IsPhosphatase}{Logical. \code{NA} for -#' \code{proteinIdType == "Metabolite"}.} +#' \code{proteinIdType == "Metabolite"} and for multi-grounded rows.} #' } #' @examples #' df <- data.frame(Protein = c("CLH1_HUMAN")) @@ -69,21 +94,74 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { } } +#' Split a protein group into its member identifiers +#' +#' A \code{Protein} value may name a protein group -- several identifiers +#' for the same quantified analyte joined by \code{";"}. Splits on +#' \code{";"}, trims surrounding whitespace and drops empty members, so a +#' plain single identifier comes back as a length-one vector. +#' +#' @param x A length-one character value, possibly \code{NA}. +#' @return A character vector of member identifiers, empty when the input +#' holds none. +#' @keywords internal +#' @noRd +.splitProteinGroup <- function(x) { + if (length(x) == 0 || is.na(x)) { + return(character(0)) + } + members <- trimws(unlist(strsplit(as.character(x), ";", fixed = TRUE), + use.names = FALSE)) + return(members[nzchar(members)]) +} + +#' Join protein group members back into a single value +#' +#' @param members A character vector of member identifiers. +#' @return The members joined by \code{";"}, or \code{NA} when empty. +#' @keywords internal +#' @noRd +.joinProteinGroup <- function(members) { + if (length(members) == 0) { + return(NA_character_) + } + return(paste(members, collapse = ";")) +} + +#' Strip the PTM site suffix from identifiers +#' +#' Removes a trailing \code{_} suffix (e.g. +#' \code{_S148}) from each element, leaving other identifiers untouched. +#' +#' @param x A character vector of identifiers. +#' @return The character vector with site suffixes removed. +#' @keywords internal +#' @noRd +.stripPtmSite <- function(x) { + return(ifelse(grepl("_[A-Z][0-9]", x), + gsub("_[A-Z][0-9].*", "", x, perl = TRUE), + x)) +} + #' Populate Uniprot IDs in Data Frame #' +#' Derives \code{GlobalProtein} by stripping the PTM site suffix from each +#' protein group member, then resolves UniProt IDs per member. For a +#' protein group the resolved IDs are semicolon-joined in member order. +#' #' @param df A data frame containing protein information. #' @param proteinIdType A character string specifying the type of protein ID. #' @return A data frame with populated Uniprot IDs. .populateUniprotIdsInDataFrame <- function(df, proteinIdType) { - if ("GlobalProtein" %in% colnames(df)) { - protein_ids = unique(as.character(df$GlobalProtein)) - } else { - df$Protein = as.character(df$Protein) - df$GlobalProtein = ifelse(grepl("_[A-Z][0-9]", df$Protein), - gsub("_[A-Z][0-9].*", "", df$Protein, perl = TRUE), - df$Protein) - protein_ids = unique(df$GlobalProtein) + if (!("GlobalProtein" %in% colnames(df))) { + df$Protein = as.character(df$Protein) + df$GlobalProtein = vapply(df$Protein, function(protein) { + .joinProteinGroup(.stripPtmSite(.splitProteinGroup(protein))) + }, character(1), USE.NAMES = FALSE) } + df$GlobalProtein = as.character(df$GlobalProtein) + groupMembers <- lapply(df$GlobalProtein, .splitProteinGroup) + protein_ids = unique(unlist(groupMembers, use.names = FALSE)) df$UniprotId <- NA if (proteinIdType == "Uniprot") { df$UniprotId <- as.character(df$GlobalProtein) @@ -93,11 +171,10 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { mnemonicProteins <- protein_ids if (length(mnemonicProteins) > 0) { uniprotMapping <- .callGetUniprotIdsFromUniprotMnemonicIdsApi(as.list(mnemonicProteins)) - for (mnemonicId in names(uniprotMapping)) { - if (!is.null(uniprotMapping[[mnemonicId]])) { - df$UniprotId[df$GlobalProtein == mnemonicId] <- uniprotMapping[[mnemonicId]] - } - } + df$UniprotId <- vapply(groupMembers, function(members) { + mapped <- unlist(uniprotMapping[members], use.names = FALSE) + .joinProteinGroup(unique(as.character(mapped))) + }, character(1), USE.NAMES = FALSE) } } @@ -117,9 +194,9 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { #' @param proteinIdType A character string specifying the type of protein ID. #' @return A data frame with populated entity grounding columns. .populateEntityInformationInDataFrame <- function(df, proteinIdType) { - df$EntityNamespace <- NA - df$EntityId <- NA - df$EntityName <- NA + df$EntityNamespace <- NA_character_ + df$EntityId <- NA_character_ + df$EntityName <- NA_character_ if (proteinIdType == "Uniprot" || proteinIdType == "Uniprot_Mnemonic") { df <- .populateEntityInformationWithIndraCogex(df) } else { @@ -130,33 +207,44 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { #' Populate entity grounding columns via INDRA cogex APIs #' -#' Converts \code{UniprotId} to an HGNC id via the INDRA cogex endpoint, -#' then looks up the canonical HGNC name. Sets \code{EntityNamespace = -#' "HGNC"} for any row whose UniProt resolved. +#' Converts each \code{UniprotId} member to an HGNC id via the INDRA cogex +#' endpoint, then looks up the canonical HGNC name. Sets +#' \code{EntityNamespace = "HGNC"} for any row whose UniProt resolved. A +#' protein group contributes one grounding per member that resolved, +#' deduplicated and semicolon-joined across the three Entity columns. #' -#' @param df A data frame with a populated \code{UniprotId} column. +#' @param df A data frame with a populated \code{UniprotId} column, whose +#' values may be semicolon-joined protein groups. #' @return The data frame with EntityNamespace, EntityId, EntityName set #' for resolved rows. .populateEntityInformationWithIndraCogex <- function(df) { - validMask <- !is.na(df$UniprotId) - validUniprots <- unique(df$UniprotId[validMask]) - if (length(validUniprots) > 0) { - hgncMapping <- .callGetHgncIdsFromUniprotIdsApi(as.list(validUniprots)) - for (uniprotId in names(hgncMapping)) { - if (!is.null(hgncMapping[[uniprotId]])) { - df$EntityNamespace[df$UniprotId == uniprotId] <- "HGNC" - df$EntityId[df$UniprotId == uniprotId] <- hgncMapping[[uniprotId]] - } - } + groupMembers <- lapply(df$UniprotId, .splitProteinGroup) + validUniprots <- unique(unlist(groupMembers, use.names = FALSE)) + if (length(validUniprots) == 0) { + return(df) } - validHgncMask <- !is.na(df$EntityId) - validHgncs <- unique(df$EntityId[validHgncMask]) + hgncMapping <- .callGetHgncIdsFromUniprotIdsApi(as.list(validUniprots)) + validHgncs <- unique(as.character(unlist(hgncMapping, use.names = FALSE))) + nameMapping <- list() if (length(validHgncs) > 0) { - nameMapping <- .callGetHgncNamesFromHgncIdsApi(as.list(validHgncs)) - for (entityId in names(nameMapping)) { - if (!is.null(nameMapping[[entityId]])) { - df$EntityName[df$EntityId == entityId] <- nameMapping[[entityId]] - } + nameResponse <- .callGetHgncNamesFromHgncIdsApi(as.list(validHgncs)) + if (!is.null(nameResponse)) { + nameMapping <- nameResponse + } + } + for (i in seq_along(groupMembers)) { + entityIds <- unique(as.character( + unlist(hgncMapping[groupMembers[[i]]], use.names = FALSE))) + if (length(entityIds) == 0) { + next + } + entityNames <- vapply(nameMapping[entityIds], function(entityName) { + if (is.null(entityName)) NA_character_ else as.character(entityName)[1] + }, character(1), USE.NAMES = FALSE) + df$EntityNamespace[i] <- .joinProteinGroup(rep("HGNC", length(entityIds))) + df$EntityId[i] <- .joinProteinGroup(entityIds) + if (!all(is.na(entityNames))) { + df$EntityName[i] <- .joinProteinGroup(entityNames) } } return(df) @@ -164,43 +252,65 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { #' Populate entity grounding columns via Gilda #' -#' Grounds each \code{GlobalProtein} text through Gilda. For +#' Grounds each \code{GlobalProtein} member text through Gilda. For #' \code{"Hgnc_Name"} the response is filtered to HGNC candidates #' (and restricted to human via the organism filter); for #' \code{"Metabolite"} every grounding namespace Gilda returns is kept. #' Multi-grounded inputs are semicolon-joined and positionally aligned -#' across all three Entity columns. +#' across all three Entity columns; a protein group pools the groundings +#' of all its members into that same representation. #' -#' @param df A data frame with a \code{GlobalProtein} column. +#' @param df A data frame with a \code{GlobalProtein} column, whose values +#' may be semicolon-joined protein groups. #' @param proteinIdType One of \code{"Hgnc_Name"} or \code{"Metabolite"}. #' @return The data frame with EntityNamespace, EntityId, EntityName set #' for resolved rows. .populateEntityInformationWithGilda <- function(df, proteinIdType) { keep_only <- if (proteinIdType == "Hgnc_Name") "HGNC" else NULL organisms <- if (proteinIdType == "Hgnc_Name") list("9606") else NULL - textInputs <- unique(df$GlobalProtein) - if (length(textInputs) > 0) { - grounding_map <- .callGroundEntitiesFromGildaApi( - as.list(textInputs), - keep_only = keep_only, - organisms = organisms) - if (!is.null(grounding_map)) { - for (input_text in names(grounding_map)) { - g <- grounding_map[[input_text]] - stopifnot(length(g$ns) == length(g$id), - length(g$ns) == length(g$name)) - row_mask <- df$GlobalProtein == input_text - df$EntityNamespace[row_mask] <- paste(g$ns, collapse = ";") - df$EntityId[row_mask] <- paste(g$id, collapse = ";") - df$EntityName[row_mask] <- paste(g$name, collapse = ";") + groupMembers <- lapply(df$GlobalProtein, .splitProteinGroup) + textInputs <- unique(unlist(groupMembers, use.names = FALSE)) + if (length(textInputs) == 0) { + return(df) + } + grounding_map <- .callGroundEntitiesFromGildaApi( + as.list(textInputs), + keep_only = keep_only, + organisms = organisms) + if (is.null(grounding_map)) { + return(df) + } + for (i in seq_along(groupMembers)) { + namespaces <- character(0) + entityIds <- character(0) + entityNames <- character(0) + for (g in grounding_map[groupMembers[[i]]]) { + if (is.null(g)) { + next } + stopifnot(length(g$ns) == length(g$id), + length(g$ns) == length(g$name)) + namespaces <- c(namespaces, as.character(g$ns)) + entityIds <- c(entityIds, as.character(g$id)) + entityNames <- c(entityNames, as.character(g$name)) } + keep <- !duplicated(paste(namespaces, entityIds, sep = ":")) + if (!any(keep)) { + next + } + df$EntityNamespace[i] <- .joinProteinGroup(namespaces[keep]) + df$EntityId[i] <- .joinProteinGroup(entityIds[keep]) + df$EntityName[i] <- .joinProteinGroup(entityNames[keep]) } return(df) } #' Populate Transcription Factor Info in Data Frame #' +#' Rows carrying more than one grounding -- a semicolon-joined +#' \code{EntityName}, from a protein group or an ambiguous input -- are +#' skipped, because the flag describes a single gene. +#' #' @param df A data frame containing protein information. #' @param proteinIdType The proteinIdType supplied by the caller. Gene-only #' flags are \code{NA} (no API call) when this is \code{"Metabolite"}. @@ -217,7 +327,7 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { charMapping <- .callIsTranscriptionFactorApi(validNamesList) for (entityName in names(charMapping)) { if (!is.null(charMapping[[entityName]])) { - df$IsTranscriptionFactor[df$EntityName == entityName] <- charMapping[[entityName]] + df$IsTranscriptionFactor[which(df$EntityName == entityName)] <- charMapping[[entityName]] } } } @@ -226,6 +336,10 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { #' Populate Kinase Info in Data Frame #' +#' Rows carrying more than one grounding -- a semicolon-joined +#' \code{EntityName}, from a protein group or an ambiguous input -- are +#' skipped, because the flag describes a single gene. +#' #' @param df A data frame containing protein information. #' @param proteinIdType The proteinIdType supplied by the caller. Gene-only #' flags are \code{NA} (no API call) when this is \code{"Metabolite"}. @@ -242,7 +356,7 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { charMapping <- .callIsKinaseApi(validNamesList) for (entityName in names(charMapping)) { if (!is.null(charMapping[[entityName]])) { - df$IsKinase[df$EntityName == entityName] <- charMapping[[entityName]] + df$IsKinase[which(df$EntityName == entityName)] <- charMapping[[entityName]] } } } @@ -251,6 +365,10 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { #' Populate Phosphatase Info in Data Frame #' +#' Rows carrying more than one grounding -- a semicolon-joined +#' \code{EntityName}, from a protein group or an ambiguous input -- are +#' skipped, because the flag describes a single gene. +#' #' @param df A data frame containing protein information. #' @param proteinIdType The proteinIdType supplied by the caller. Gene-only #' flags are \code{NA} (no API call) when this is \code{"Metabolite"}. @@ -267,7 +385,7 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { charMapping <- .callIsPhosphataseApi(validNamesList) for (entityName in names(charMapping)) { if (!is.null(charMapping[[entityName]])) { - df$IsPhosphatase[df$EntityName == entityName] <- charMapping[[entityName]] + df$IsPhosphatase[which(df$EntityName == entityName)] <- charMapping[[entityName]] } } } diff --git a/man/annotateProteinInfoFromIndra.Rd b/man/annotateProteinInfoFromIndra.Rd index 365419c..2b80ac0 100644 --- a/man/annotateProteinInfoFromIndra.Rd +++ b/man/annotateProteinInfoFromIndra.Rd @@ -9,7 +9,12 @@ annotateProteinInfoFromIndra(df, proteinIdType) \arguments{ \item{df}{output of \code{\link[MSstats]{groupComparison}} function's comparisonResult table. Must contain a \code{Protein} column whose -values are interpreted according to \code{proteinIdType}.} +values are interpreted according to \code{proteinIdType}. A value +may name a protein group -- several identifiers for the same +quantified analyte joined by \code{";"}, e.g. +\code{"P13747;P23132"} -- in which case every member is grounded +independently and the results are pooled onto the row (see +Details).} \item{proteinIdType}{A character string specifying the type of analyte identifier in the \code{Protein} column. One of @@ -22,26 +27,31 @@ namespace Gilda returns (CHEBI / PUBCHEM / CHEMBL / ...).} A data frame with the following columns: \describe{ \item{Protein}{Character. The original identifier from the input.} - \item{GlobalProtein}{Character. The input identifier without the PTM + \item{GlobalProtein}{Character. The input identifier with the PTM site suffix (typically \code{_}, e.g. - \code{_S148}) stripped, used as the grounding key.} - \item{UniprotId}{Character. The Uniprot ID of the protein, or + \code{_S148}) stripped from each protein group member, used as + the grounding key. \code{NA} when the input holds no usable + identifier.} + \item{UniprotId}{Character. The Uniprot ID of the protein, + semicolon-joined over the members of a protein group, or \code{NA} for \code{"Hgnc_Name"} and \code{"Metabolite"} inputs.} \item{EntityNamespace}{Character. The grounding namespace - (e.g. \code{"HGNC"}, \code{"CHEBI"}). When a single input grounds - to multiple candidates, namespaces are semicolon-joined and + (e.g. \code{"HGNC"}, \code{"CHEBI"}). When a row grounds to + multiple candidates -- whether from a protein group or from an + ambiguous single input -- namespaces are semicolon-joined and positionally aligned with \code{EntityId} and \code{EntityName}.} \item{EntityId}{Character. The bare grounding identifier within its namespace (e.g. \code{"1097"} for HGNC, \code{"28748"} for CHEBI). Semicolon-joined when multi-grounded.} \item{EntityName}{Character. The canonical display name from the - grounding source. Semicolon-joined when multi-grounded.} + grounding source. Semicolon-joined when multi-grounded, with + \code{"NA"} in the positions whose name lookup failed.} \item{IsTranscriptionFactor}{Logical. \code{NA} for - \code{proteinIdType == "Metabolite"}.} + \code{proteinIdType == "Metabolite"} and for multi-grounded rows.} \item{IsKinase}{Logical. \code{NA} for - \code{proteinIdType == "Metabolite"}.} + \code{proteinIdType == "Metabolite"} and for multi-grounded rows.} \item{IsPhosphatase}{Logical. \code{NA} for - \code{proteinIdType == "Metabolite"}.} + \code{proteinIdType == "Metabolite"} and for multi-grounded rows.} } } \description{ @@ -49,6 +59,22 @@ This function standardizes entity identifiers from protein, compound, or gene inputs to a unified namespace using ID conversion from INDRA cogex or Gilda grounding. } +\details{ +Protein group members are split on \code{";"}, each member is stripped +of its PTM site suffix and grounded on its own, and the groundings of +all members are concatenated -- in member order, deduplicated on +\code{(EntityNamespace, EntityId)} -- into the semicolon-joined +\code{Entity*} columns. This is the same representation used when a +single input grounds to several candidates, and +\code{\link{getSubnetworkFromIndra}} fans each pair out into its own +query node. + +Because \code{IsTranscriptionFactor} / \code{IsKinase} / +\code{IsPhosphatase} describe one gene, they are left \code{NA} +whenever a row carries more than one grounding. A group whose members +all resolve to the same gene collapses to a single grounding and does +get the flags. +} \examples{ df <- data.frame(Protein = c("CLH1_HUMAN")) annotated_df <- annotateProteinInfoFromIndra(df, "Uniprot_Mnemonic") diff --git a/man/dot-populateEntityInformationWithGilda.Rd b/man/dot-populateEntityInformationWithGilda.Rd index e34150b..06e5f5b 100644 --- a/man/dot-populateEntityInformationWithGilda.Rd +++ b/man/dot-populateEntityInformationWithGilda.Rd @@ -7,7 +7,8 @@ .populateEntityInformationWithGilda(df, proteinIdType) } \arguments{ -\item{df}{A data frame with a \code{GlobalProtein} column.} +\item{df}{A data frame with a \code{GlobalProtein} column, whose values +may be semicolon-joined protein groups.} \item{proteinIdType}{One of \code{"Hgnc_Name"} or \code{"Metabolite"}.} } @@ -16,10 +17,11 @@ The data frame with EntityNamespace, EntityId, EntityName set for resolved rows. } \description{ -Grounds each \code{GlobalProtein} text through Gilda. For +Grounds each \code{GlobalProtein} member text through Gilda. For \code{"Hgnc_Name"} the response is filtered to HGNC candidates (and restricted to human via the organism filter); for \code{"Metabolite"} every grounding namespace Gilda returns is kept. Multi-grounded inputs are semicolon-joined and positionally aligned -across all three Entity columns. +across all three Entity columns; a protein group pools the groundings +of all its members into that same representation. } diff --git a/man/dot-populateEntityInformationWithIndraCogex.Rd b/man/dot-populateEntityInformationWithIndraCogex.Rd index 41864b8..7d3b4f7 100644 --- a/man/dot-populateEntityInformationWithIndraCogex.Rd +++ b/man/dot-populateEntityInformationWithIndraCogex.Rd @@ -7,14 +7,17 @@ .populateEntityInformationWithIndraCogex(df) } \arguments{ -\item{df}{A data frame with a populated \code{UniprotId} column.} +\item{df}{A data frame with a populated \code{UniprotId} column, whose +values may be semicolon-joined protein groups.} } \value{ The data frame with EntityNamespace, EntityId, EntityName set for resolved rows. } \description{ -Converts \code{UniprotId} to an HGNC id via the INDRA cogex endpoint, -then looks up the canonical HGNC name. Sets \code{EntityNamespace = -"HGNC"} for any row whose UniProt resolved. +Converts each \code{UniprotId} member to an HGNC id via the INDRA cogex +endpoint, then looks up the canonical HGNC name. Sets +\code{EntityNamespace = "HGNC"} for any row whose UniProt resolved. A +protein group contributes one grounding per member that resolved, +deduplicated and semicolon-joined across the three Entity columns. } diff --git a/man/dot-populateKinaseInfoInDataFrame.Rd b/man/dot-populateKinaseInfoInDataFrame.Rd index bbd60e1..7d61905 100644 --- a/man/dot-populateKinaseInfoInDataFrame.Rd +++ b/man/dot-populateKinaseInfoInDataFrame.Rd @@ -16,5 +16,7 @@ flags are \code{NA} (no API call) when this is \code{"Metabolite"}.} A data frame with populated kinase information. } \description{ -Populate Kinase Info in Data Frame +Rows carrying more than one grounding -- a semicolon-joined +\code{EntityName}, from a protein group or an ambiguous input -- are +skipped, because the flag describes a single gene. } diff --git a/man/dot-populatePhophataseInfoInDataFrame.Rd b/man/dot-populatePhophataseInfoInDataFrame.Rd index 240ac37..b905673 100644 --- a/man/dot-populatePhophataseInfoInDataFrame.Rd +++ b/man/dot-populatePhophataseInfoInDataFrame.Rd @@ -16,5 +16,7 @@ flags are \code{NA} (no API call) when this is \code{"Metabolite"}.} A data frame with populated phosphatase information. } \description{ -Populate Phosphatase Info in Data Frame +Rows carrying more than one grounding -- a semicolon-joined +\code{EntityName}, from a protein group or an ambiguous input -- are +skipped, because the flag describes a single gene. } diff --git a/man/dot-populateTranscriptionFactorInfoInDataFrame.Rd b/man/dot-populateTranscriptionFactorInfoInDataFrame.Rd index ce69add..76c0149 100644 --- a/man/dot-populateTranscriptionFactorInfoInDataFrame.Rd +++ b/man/dot-populateTranscriptionFactorInfoInDataFrame.Rd @@ -16,5 +16,7 @@ flags are \code{NA} (no API call) when this is \code{"Metabolite"}.} A data frame with populated transcription factor information. } \description{ -Populate Transcription Factor Info in Data Frame +Rows carrying more than one grounding -- a semicolon-joined +\code{EntityName}, from a protein group or an ambiguous input -- are +skipped, because the flag describes a single gene. } diff --git a/man/dot-populateUniprotIdsInDataFrame.Rd b/man/dot-populateUniprotIdsInDataFrame.Rd index 4d26321..116d3a3 100644 --- a/man/dot-populateUniprotIdsInDataFrame.Rd +++ b/man/dot-populateUniprotIdsInDataFrame.Rd @@ -15,5 +15,7 @@ A data frame with populated Uniprot IDs. } \description{ -Populate Uniprot IDs in Data Frame +Derives \code{GlobalProtein} by stripping the PTM site suffix from each +protein group member, then resolves UniProt IDs per member. For a +protein group the resolved IDs are semicolon-joined in member order. } diff --git a/tests/testthat/test-annotateProteinInfoFromIndra.R b/tests/testthat/test-annotateProteinInfoFromIndra.R index bfce822..9f492f1 100644 --- a/tests/testthat/test-annotateProteinInfoFromIndra.R +++ b/tests/testthat/test-annotateProteinInfoFromIndra.R @@ -128,3 +128,220 @@ test_that("annotateProteinInfoFromIndra works correctly with HGNC name", { expect_type(annotated_df$IsPhosphatase, "logical") }) + +# ----- Protein groups: ";"-joined identifiers are grounded member by member, +# then pooled into the semicolon-joined Entity* columns that +# getSubnetworkFromIndra already fans out. Mocked, so no network access. ----- + +test_that(".splitProteinGroup splits, trims and drops empty members", { + expect_equal(MSstatsBioNet:::.splitProteinGroup("P13747;P23132"), + c("P13747", "P23132")) + expect_equal(MSstatsBioNet:::.splitProteinGroup("P13747"), "P13747") + expect_equal(MSstatsBioNet:::.splitProteinGroup(" P13747 ; P23132 "), + c("P13747", "P23132")) + expect_equal(MSstatsBioNet:::.splitProteinGroup(";P13747;;"), "P13747") + expect_equal(MSstatsBioNet:::.splitProteinGroup(NA), character(0)) + expect_equal(MSstatsBioNet:::.splitProteinGroup(""), character(0)) +}) + +test_that(".joinProteinGroup round-trips and returns NA when empty", { + expect_equal(MSstatsBioNet:::.joinProteinGroup(c("P13747", "P23132")), + "P13747;P23132") + expect_equal(MSstatsBioNet:::.joinProteinGroup("P13747"), "P13747") + expect_true(is.na(MSstatsBioNet:::.joinProteinGroup(character(0)))) +}) + +test_that(".stripPtmSite removes only the site suffix", { + expect_equal(MSstatsBioNet:::.stripPtmSite(c("P13747_S148", "P23132")), + c("P13747", "P23132")) + expect_equal(MSstatsBioNet:::.stripPtmSite("CLH1_HUMAN"), "CLH1_HUMAN") +}) + +test_that("PTM site suffixes are stripped from every protein group member", { + df <- data.frame(Protein = c("P13747_S148;P23132_T20", "P13747_S148"), + stringsAsFactors = FALSE) + out <- MSstatsBioNet:::.populateUniprotIdsInDataFrame(df, "Uniprot") + + expect_equal(out$GlobalProtein, c("P13747;P23132", "P13747")) + expect_equal(out$UniprotId, c("P13747;P23132", "P13747")) +}) + +test_that("annotateProteinInfoFromIndra grounds each Uniprot protein group member", { + df <- data.frame(Protein = c("P13747;P23132", "Q00610"), + stringsAsFactors = FALSE) + local_mocked_bindings( + .callGetHgncIdsFromUniprotIdsApi = function(uniprotIds) { + list(P13747 = "4931", P23132 = "10012", Q00610 = "2092") + }, + .callGetHgncNamesFromHgncIdsApi = function(hgncIds) { + list(`4931` = "HLA-E", `10012` = "RAD23A", `2092` = "CLTC") + }, + .callIsTranscriptionFactorApi = function(genes) list(CLTC = FALSE), + .callIsKinaseApi = function(genes) list(CLTC = FALSE), + .callIsPhosphataseApi = function(genes) list(CLTC = FALSE) + ) + annotated_df <- annotateProteinInfoFromIndra(df, "Uniprot") + + group_row <- annotated_df[annotated_df$Protein == "P13747;P23132", ] + expect_equal(group_row$UniprotId, "P13747;P23132") + expect_equal(group_row$EntityNamespace, "HGNC;HGNC") + expect_equal(group_row$EntityId, "4931;10012") + expect_equal(group_row$EntityName, "HLA-E;RAD23A") + # Gene-only flags stay NA while the row carries more than one grounding + expect_true(is.na(group_row$IsTranscriptionFactor)) + expect_true(is.na(group_row$IsKinase)) + expect_true(is.na(group_row$IsPhosphatase)) + + # A single-identifier row is unaffected by the split + single_row <- annotated_df[annotated_df$Protein == "Q00610", ] + expect_equal(single_row$EntityNamespace, "HGNC") + expect_equal(single_row$EntityId, "2092") + expect_equal(single_row$EntityName, "CLTC") + expect_equal(single_row$IsKinase, FALSE) +}) + +test_that("a protein group whose members share a gene collapses to one grounding", { + df <- data.frame(Protein = "P13747;P13747-2", stringsAsFactors = FALSE) + local_mocked_bindings( + .callGetHgncIdsFromUniprotIdsApi = function(uniprotIds) { + list(P13747 = "4931", `P13747-2` = "4931") + }, + .callGetHgncNamesFromHgncIdsApi = function(hgncIds) list(`4931` = "HLA-E"), + .callIsTranscriptionFactorApi = function(genes) list(`HLA-E` = FALSE), + .callIsKinaseApi = function(genes) list(`HLA-E` = FALSE), + .callIsPhosphataseApi = function(genes) list(`HLA-E` = TRUE) + ) + annotated_df <- annotateProteinInfoFromIndra(df, "Uniprot") + + expect_equal(annotated_df$EntityNamespace, "HGNC") + expect_equal(annotated_df$EntityId, "4931") + expect_equal(annotated_df$EntityName, "HLA-E") + # Collapsed to a single grounding, so the gene-only flags are populated + expect_equal(annotated_df$IsTranscriptionFactor, FALSE) + expect_equal(annotated_df$IsPhosphatase, TRUE) +}) + +test_that("unresolvable protein group members are dropped, not carried as NA", { + df <- data.frame(Protein = c("P13747;NOTANID", "NOTANID;ALSONOT"), + stringsAsFactors = FALSE) + local_mocked_bindings( + .callGetHgncIdsFromUniprotIdsApi = function(uniprotIds) list(P13747 = "4931"), + .callGetHgncNamesFromHgncIdsApi = function(hgncIds) list(`4931` = "HLA-E"), + .callIsTranscriptionFactorApi = function(genes) list(`HLA-E` = FALSE), + .callIsKinaseApi = function(genes) list(`HLA-E` = FALSE), + .callIsPhosphataseApi = function(genes) list(`HLA-E` = FALSE) + ) + annotated_df <- annotateProteinInfoFromIndra(df, "Uniprot") + + # Partially resolved group keeps only the member that grounded, and so is + # single-grounded and does get the gene-only flags + expect_equal(annotated_df$EntityId[1], "4931") + expect_equal(annotated_df$EntityName[1], "HLA-E") + expect_equal(annotated_df$IsKinase[1], FALSE) + + # Fully unresolved group stays NA across the Entity columns, and its + # presence alongside a resolved row must not break the flag lookups + expect_true(is.na(annotated_df$EntityNamespace[2])) + expect_true(is.na(annotated_df$EntityId[2])) + expect_true(is.na(annotated_df$EntityName[2])) + expect_true(is.na(annotated_df$IsKinase[2])) +}) + +test_that("Uniprot_Mnemonic groups map each member to its own UniProt id", { + df <- data.frame(Protein = c("CLH1_HUMAN;HLAE_HUMAN", "CLH1_HUMAN;NOPE_HUMAN"), + stringsAsFactors = FALSE) + local_mocked_bindings( + .callGetUniprotIdsFromUniprotMnemonicIdsApi = function(uniprotMnemonicIds) { + list(CLH1_HUMAN = "Q00610", HLAE_HUMAN = "P13747") + }, + .callGetHgncIdsFromUniprotIdsApi = function(uniprotIds) { + list(Q00610 = "2092", P13747 = "4931") + }, + .callGetHgncNamesFromHgncIdsApi = function(hgncIds) { + list(`2092` = "CLTC", `4931` = "HLA-E") + }, + .callIsTranscriptionFactorApi = function(genes) list(CLTC = FALSE), + .callIsKinaseApi = function(genes) list(CLTC = FALSE), + .callIsPhosphataseApi = function(genes) list(CLTC = FALSE) + ) + annotated_df <- annotateProteinInfoFromIndra(df, "Uniprot_Mnemonic") + + expect_equal(annotated_df$UniprotId[1], "Q00610;P13747") + expect_equal(annotated_df$EntityNamespace[1], "HGNC;HGNC") + expect_equal(annotated_df$EntityId[1], "2092;4931") + expect_equal(annotated_df$EntityName[1], "CLTC;HLA-E") + + # Only one member resolves, so the row degrades to that single grounding + expect_equal(annotated_df$UniprotId[2], "Q00610") + expect_equal(annotated_df$EntityName[2], "CLTC") + expect_equal(annotated_df$IsKinase[2], FALSE) +}) + +test_that("Hgnc_Name groups pool and deduplicate Gilda groundings", { + df <- data.frame(Protein = c("EGFR;ERBB2", "EGFR;EGFR"), + stringsAsFactors = FALSE) + local_mocked_bindings( + .callGroundEntitiesFromGildaApi = function(textInputs, keep_only = NULL, organisms = NULL) { + list(EGFR = list(ns = "HGNC", id = "3236", name = "EGFR"), + ERBB2 = list(ns = "HGNC", id = "3430", name = "ERBB2")) + }, + .callIsTranscriptionFactorApi = function(genes) list(EGFR = FALSE), + .callIsKinaseApi = function(genes) list(EGFR = TRUE), + .callIsPhosphataseApi = function(genes) list(EGFR = FALSE) + ) + annotated_df <- annotateProteinInfoFromIndra(df, "Hgnc_Name") + + expect_equal(annotated_df$EntityNamespace[1], "HGNC;HGNC") + expect_equal(annotated_df$EntityId[1], "3236;3430") + expect_equal(annotated_df$EntityName[1], "EGFR;ERBB2") + + # A repeated member contributes its grounding once + expect_equal(annotated_df$EntityId[2], "3236") + expect_equal(annotated_df$EntityName[2], "EGFR") + expect_equal(annotated_df$IsKinase[2], TRUE) +}) + +test_that("Metabolite groups keep every namespace Gilda returns per member", { + df <- data.frame(Protein = "glucose;citrate", stringsAsFactors = FALSE) + local_mocked_bindings( + .callGroundEntitiesFromGildaApi = function(textInputs, keep_only = NULL, organisms = NULL) { + list(glucose = list(ns = c("CHEBI", "MESH"), + id = c("17234", "D005947"), + name = c("glucose", "Glucose")), + citrate = list(ns = "CHEBI", id = "133748", name = "citrate")) + } + ) + annotated_df <- annotateProteinInfoFromIndra(df, "Metabolite") + + expect_equal(annotated_df$EntityNamespace, "CHEBI;MESH;CHEBI") + expect_equal(annotated_df$EntityId, "17234;D005947;133748") + expect_equal(annotated_df$EntityName, "glucose;Glucose;citrate") + expect_true(is.na(annotated_df$UniprotId)) + expect_true(is.na(annotated_df$IsKinase)) +}) + +test_that("a protein group's groundings fan out into separate query nodes", { + df <- data.frame(Protein = "P13747;P23132", stringsAsFactors = FALSE) + local_mocked_bindings( + .callGetHgncIdsFromUniprotIdsApi = function(uniprotIds) { + list(P13747 = "4931", P23132 = "10012") + }, + .callGetHgncNamesFromHgncIdsApi = function(hgncIds) { + list(`4931` = "HLA-E", `10012` = "RAD23A") + }, + .callIsTranscriptionFactorApi = function(genes) list(), + .callIsKinaseApi = function(genes) list(), + .callIsPhosphataseApi = function(genes) list() + ) + annotated_df <- annotateProteinInfoFromIndra(df, "Uniprot") + + pairs <- MSstatsBioNet:::.buildCogexGroundings( + namespaces = annotated_df$EntityNamespace, + ids = annotated_df$EntityId, + force_include_other = NULL + ) + expect_setequal( + vapply(pairs, function(p) paste(p[[1]], p[[2]], sep = ":"), character(1)), + c("HGNC:4931", "HGNC:10012") + ) +}) From 70f0e8ac17e95b5962c16466fec34d0070c616cf Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Tue, 22 Sep 2026 10:18:41 -0400 Subject: [PATCH 2/4] fix documentation, slight code adjustments --- R/annotateProteinInfoFromIndra.R | 43 +++++++------------ man/annotateProteinInfoFromIndra.Rd | 23 ++-------- man/dot-populateEntityInformationWithGilda.Rd | 27 ------------ ...populateEntityInformationWithIndraCogex.Rd | 23 ---------- man/dot-populateKinaseInfoInDataFrame.Rd | 22 ---------- man/dot-populatePhophataseInfoInDataFrame.Rd | 22 ---------- ...ulateTranscriptionFactorInfoInDataFrame.Rd | 22 ---------- man/dot-populateUniprotIdsInDataFrame.Rd | 21 --------- 8 files changed, 18 insertions(+), 185 deletions(-) delete mode 100644 man/dot-populateEntityInformationWithGilda.Rd delete mode 100644 man/dot-populateEntityInformationWithIndraCogex.Rd delete mode 100644 man/dot-populateKinaseInfoInDataFrame.Rd delete mode 100644 man/dot-populatePhophataseInfoInDataFrame.Rd delete mode 100644 man/dot-populateTranscriptionFactorInfoInDataFrame.Rd delete mode 100644 man/dot-populateUniprotIdsInDataFrame.Rd diff --git a/R/annotateProteinInfoFromIndra.R b/R/annotateProteinInfoFromIndra.R index f26f454..8bc44b8 100644 --- a/R/annotateProteinInfoFromIndra.R +++ b/R/annotateProteinInfoFromIndra.R @@ -10,39 +10,24 @@ #' may name a protein group -- several identifiers for the same #' quantified analyte joined by \code{";"}, e.g. #' \code{"P13747;P23132"} -- in which case every member is grounded -#' independently and the results are pooled onto the row (see -#' Details). +#' independently and the results are pooled onto the row. #' @param proteinIdType A character string specifying the type of analyte #' identifier in the \code{Protein} column. One of #' \code{"Uniprot"}, \code{"Uniprot_Mnemonic"}, \code{"Hgnc_Name"}, or #' \code{"Metabolite"}. The \code{"Metabolite"} value treats inputs as #' metabolite names and grounds them through Gilda, keeping whatever #' namespace Gilda returns (CHEBI / PUBCHEM / CHEMBL / ...). -#' @details -#' Protein group members are split on \code{";"}, each member is stripped -#' of its PTM site suffix and grounded on its own, and the groundings of -#' all members are concatenated -- in member order, deduplicated on -#' \code{(EntityNamespace, EntityId)} -- into the semicolon-joined -#' \code{Entity*} columns. This is the same representation used when a -#' single input grounds to several candidates, and -#' \code{\link{getSubnetworkFromIndra}} fans each pair out into its own -#' query node. -#' -#' Because \code{IsTranscriptionFactor} / \code{IsKinase} / -#' \code{IsPhosphatase} describe one gene, they are left \code{NA} -#' whenever a row carries more than one grounding. A group whose members -#' all resolve to the same gene collapses to a single grounding and does -#' get the flags. +#' #' @return A data frame with the following columns: #' \describe{ #' \item{Protein}{Character. The original identifier from the input.} -#' \item{GlobalProtein}{Character. The input identifier with the PTM +#' \item{GlobalProtein}{Character. The input identifier without the PTM #' site suffix (typically \code{_}, e.g. #' \code{_S148}) stripped from each protein group member, used as #' the grounding key. \code{NA} when the input holds no usable #' identifier.} #' \item{UniprotId}{Character. The Uniprot ID of the protein, -#' semicolon-joined over the members of a protein group, or +#' semicolon-joined in the case of multiple proteins, or #' \code{NA} for \code{"Hgnc_Name"} and \code{"Metabolite"} inputs.} #' \item{EntityNamespace}{Character. The grounding namespace #' (e.g. \code{"HGNC"}, \code{"CHEBI"}). When a row grounds to @@ -130,8 +115,9 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { #' Strip the PTM site suffix from identifiers #' -#' Removes a trailing \code{_} suffix (e.g. -#' \code{_S148}) from each element, leaving other identifiers untouched. +#' Removes 1+ trailing \code{_} suffixes (e.g. +#' \code{_S148}, \code{_S148_T150}) from each element, +#' leaving other identifiers untouched. #' #' @param x A character vector of identifiers. #' @return The character vector with site suffixes removed. @@ -145,18 +131,16 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { #' Populate Uniprot IDs in Data Frame #' -#' Derives \code{GlobalProtein} by stripping the PTM site suffix from each -#' protein group member, then resolves UniProt IDs per member. For a -#' protein group the resolved IDs are semicolon-joined in member order. #' #' @param df A data frame containing protein information. #' @param proteinIdType A character string specifying the type of protein ID. #' @return A data frame with populated Uniprot IDs. +#' @noRd .populateUniprotIdsInDataFrame <- function(df, proteinIdType) { if (!("GlobalProtein" %in% colnames(df))) { df$Protein = as.character(df$Protein) df$GlobalProtein = vapply(df$Protein, function(protein) { - .joinProteinGroup(.stripPtmSite(.splitProteinGroup(protein))) + .stripPtmSite(protein) }, character(1), USE.NAMES = FALSE) } df$GlobalProtein = as.character(df$GlobalProtein) @@ -209,14 +193,13 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { #' #' Converts each \code{UniprotId} member to an HGNC id via the INDRA cogex #' endpoint, then looks up the canonical HGNC name. Sets -#' \code{EntityNamespace = "HGNC"} for any row whose UniProt resolved. A -#' protein group contributes one grounding per member that resolved, -#' deduplicated and semicolon-joined across the three Entity columns. +#' \code{EntityNamespace = "HGNC"} for any row whose UniProt resolved. #' #' @param df A data frame with a populated \code{UniprotId} column, whose #' values may be semicolon-joined protein groups. #' @return The data frame with EntityNamespace, EntityId, EntityName set #' for resolved rows. +#' @noRd .populateEntityInformationWithIndraCogex <- function(df) { groupMembers <- lapply(df$UniprotId, .splitProteinGroup) validUniprots <- unique(unlist(groupMembers, use.names = FALSE)) @@ -265,6 +248,7 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { #' @param proteinIdType One of \code{"Hgnc_Name"} or \code{"Metabolite"}. #' @return The data frame with EntityNamespace, EntityId, EntityName set #' for resolved rows. +#' @noRd .populateEntityInformationWithGilda <- function(df, proteinIdType) { keep_only <- if (proteinIdType == "Hgnc_Name") "HGNC" else NULL organisms <- if (proteinIdType == "Hgnc_Name") list("9606") else NULL @@ -315,6 +299,7 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { #' @param proteinIdType The proteinIdType supplied by the caller. Gene-only #' flags are \code{NA} (no API call) when this is \code{"Metabolite"}. #' @return A data frame with populated transcription factor information. +#' @noRd .populateTranscriptionFactorInfoInDataFrame <- function(df, proteinIdType) { df$IsTranscriptionFactor <- NA if (proteinIdType == "Metabolite") { @@ -344,6 +329,7 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { #' @param proteinIdType The proteinIdType supplied by the caller. Gene-only #' flags are \code{NA} (no API call) when this is \code{"Metabolite"}. #' @return A data frame with populated kinase information. +#' @noRd .populateKinaseInfoInDataFrame <- function(df, proteinIdType) { df$IsKinase <- NA if (proteinIdType == "Metabolite") { @@ -373,6 +359,7 @@ annotateProteinInfoFromIndra <- function(df, proteinIdType) { #' @param proteinIdType The proteinIdType supplied by the caller. Gene-only #' flags are \code{NA} (no API call) when this is \code{"Metabolite"}. #' @return A data frame with populated phosphatase information. +#' @noRd .populatePhophataseInfoInDataFrame <- function(df, proteinIdType) { df$IsPhosphatase <- NA if (proteinIdType == "Metabolite") { diff --git a/man/annotateProteinInfoFromIndra.Rd b/man/annotateProteinInfoFromIndra.Rd index 2b80ac0..0f23525 100644 --- a/man/annotateProteinInfoFromIndra.Rd +++ b/man/annotateProteinInfoFromIndra.Rd @@ -13,8 +13,7 @@ values are interpreted according to \code{proteinIdType}. A value may name a protein group -- several identifiers for the same quantified analyte joined by \code{";"}, e.g. \code{"P13747;P23132"} -- in which case every member is grounded -independently and the results are pooled onto the row (see -Details).} +independently and the results are pooled onto the row.} \item{proteinIdType}{A character string specifying the type of analyte identifier in the \code{Protein} column. One of @@ -27,13 +26,13 @@ namespace Gilda returns (CHEBI / PUBCHEM / CHEMBL / ...).} A data frame with the following columns: \describe{ \item{Protein}{Character. The original identifier from the input.} - \item{GlobalProtein}{Character. The input identifier with the PTM + \item{GlobalProtein}{Character. The input identifier without the PTM site suffix (typically \code{_}, e.g. \code{_S148}) stripped from each protein group member, used as the grounding key. \code{NA} when the input holds no usable identifier.} \item{UniprotId}{Character. The Uniprot ID of the protein, - semicolon-joined over the members of a protein group, or + semicolon-joined in the case of multiple proteins, or \code{NA} for \code{"Hgnc_Name"} and \code{"Metabolite"} inputs.} \item{EntityNamespace}{Character. The grounding namespace (e.g. \code{"HGNC"}, \code{"CHEBI"}). When a row grounds to @@ -59,22 +58,6 @@ This function standardizes entity identifiers from protein, compound, or gene inputs to a unified namespace using ID conversion from INDRA cogex or Gilda grounding. } -\details{ -Protein group members are split on \code{";"}, each member is stripped -of its PTM site suffix and grounded on its own, and the groundings of -all members are concatenated -- in member order, deduplicated on -\code{(EntityNamespace, EntityId)} -- into the semicolon-joined -\code{Entity*} columns. This is the same representation used when a -single input grounds to several candidates, and -\code{\link{getSubnetworkFromIndra}} fans each pair out into its own -query node. - -Because \code{IsTranscriptionFactor} / \code{IsKinase} / -\code{IsPhosphatase} describe one gene, they are left \code{NA} -whenever a row carries more than one grounding. A group whose members -all resolve to the same gene collapses to a single grounding and does -get the flags. -} \examples{ df <- data.frame(Protein = c("CLH1_HUMAN")) annotated_df <- annotateProteinInfoFromIndra(df, "Uniprot_Mnemonic") diff --git a/man/dot-populateEntityInformationWithGilda.Rd b/man/dot-populateEntityInformationWithGilda.Rd deleted file mode 100644 index 06e5f5b..0000000 --- a/man/dot-populateEntityInformationWithGilda.Rd +++ /dev/null @@ -1,27 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/annotateProteinInfoFromIndra.R -\name{.populateEntityInformationWithGilda} -\alias{.populateEntityInformationWithGilda} -\title{Populate entity grounding columns via Gilda} -\usage{ -.populateEntityInformationWithGilda(df, proteinIdType) -} -\arguments{ -\item{df}{A data frame with a \code{GlobalProtein} column, whose values -may be semicolon-joined protein groups.} - -\item{proteinIdType}{One of \code{"Hgnc_Name"} or \code{"Metabolite"}.} -} -\value{ -The data frame with EntityNamespace, EntityId, EntityName set - for resolved rows. -} -\description{ -Grounds each \code{GlobalProtein} member text through Gilda. For -\code{"Hgnc_Name"} the response is filtered to HGNC candidates -(and restricted to human via the organism filter); for -\code{"Metabolite"} every grounding namespace Gilda returns is kept. -Multi-grounded inputs are semicolon-joined and positionally aligned -across all three Entity columns; a protein group pools the groundings -of all its members into that same representation. -} diff --git a/man/dot-populateEntityInformationWithIndraCogex.Rd b/man/dot-populateEntityInformationWithIndraCogex.Rd deleted file mode 100644 index 7d3b4f7..0000000 --- a/man/dot-populateEntityInformationWithIndraCogex.Rd +++ /dev/null @@ -1,23 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/annotateProteinInfoFromIndra.R -\name{.populateEntityInformationWithIndraCogex} -\alias{.populateEntityInformationWithIndraCogex} -\title{Populate entity grounding columns via INDRA cogex APIs} -\usage{ -.populateEntityInformationWithIndraCogex(df) -} -\arguments{ -\item{df}{A data frame with a populated \code{UniprotId} column, whose -values may be semicolon-joined protein groups.} -} -\value{ -The data frame with EntityNamespace, EntityId, EntityName set - for resolved rows. -} -\description{ -Converts each \code{UniprotId} member to an HGNC id via the INDRA cogex -endpoint, then looks up the canonical HGNC name. Sets -\code{EntityNamespace = "HGNC"} for any row whose UniProt resolved. A -protein group contributes one grounding per member that resolved, -deduplicated and semicolon-joined across the three Entity columns. -} diff --git a/man/dot-populateKinaseInfoInDataFrame.Rd b/man/dot-populateKinaseInfoInDataFrame.Rd deleted file mode 100644 index 7d61905..0000000 --- a/man/dot-populateKinaseInfoInDataFrame.Rd +++ /dev/null @@ -1,22 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/annotateProteinInfoFromIndra.R -\name{.populateKinaseInfoInDataFrame} -\alias{.populateKinaseInfoInDataFrame} -\title{Populate Kinase Info in Data Frame} -\usage{ -.populateKinaseInfoInDataFrame(df, proteinIdType) -} -\arguments{ -\item{df}{A data frame containing protein information.} - -\item{proteinIdType}{The proteinIdType supplied by the caller. Gene-only -flags are \code{NA} (no API call) when this is \code{"Metabolite"}.} -} -\value{ -A data frame with populated kinase information. -} -\description{ -Rows carrying more than one grounding -- a semicolon-joined -\code{EntityName}, from a protein group or an ambiguous input -- are -skipped, because the flag describes a single gene. -} diff --git a/man/dot-populatePhophataseInfoInDataFrame.Rd b/man/dot-populatePhophataseInfoInDataFrame.Rd deleted file mode 100644 index b905673..0000000 --- a/man/dot-populatePhophataseInfoInDataFrame.Rd +++ /dev/null @@ -1,22 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/annotateProteinInfoFromIndra.R -\name{.populatePhophataseInfoInDataFrame} -\alias{.populatePhophataseInfoInDataFrame} -\title{Populate Phosphatase Info in Data Frame} -\usage{ -.populatePhophataseInfoInDataFrame(df, proteinIdType) -} -\arguments{ -\item{df}{A data frame containing protein information.} - -\item{proteinIdType}{The proteinIdType supplied by the caller. Gene-only -flags are \code{NA} (no API call) when this is \code{"Metabolite"}.} -} -\value{ -A data frame with populated phosphatase information. -} -\description{ -Rows carrying more than one grounding -- a semicolon-joined -\code{EntityName}, from a protein group or an ambiguous input -- are -skipped, because the flag describes a single gene. -} diff --git a/man/dot-populateTranscriptionFactorInfoInDataFrame.Rd b/man/dot-populateTranscriptionFactorInfoInDataFrame.Rd deleted file mode 100644 index 76c0149..0000000 --- a/man/dot-populateTranscriptionFactorInfoInDataFrame.Rd +++ /dev/null @@ -1,22 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/annotateProteinInfoFromIndra.R -\name{.populateTranscriptionFactorInfoInDataFrame} -\alias{.populateTranscriptionFactorInfoInDataFrame} -\title{Populate Transcription Factor Info in Data Frame} -\usage{ -.populateTranscriptionFactorInfoInDataFrame(df, proteinIdType) -} -\arguments{ -\item{df}{A data frame containing protein information.} - -\item{proteinIdType}{The proteinIdType supplied by the caller. Gene-only -flags are \code{NA} (no API call) when this is \code{"Metabolite"}.} -} -\value{ -A data frame with populated transcription factor information. -} -\description{ -Rows carrying more than one grounding -- a semicolon-joined -\code{EntityName}, from a protein group or an ambiguous input -- are -skipped, because the flag describes a single gene. -} diff --git a/man/dot-populateUniprotIdsInDataFrame.Rd b/man/dot-populateUniprotIdsInDataFrame.Rd deleted file mode 100644 index 116d3a3..0000000 --- a/man/dot-populateUniprotIdsInDataFrame.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/annotateProteinInfoFromIndra.R -\name{.populateUniprotIdsInDataFrame} -\alias{.populateUniprotIdsInDataFrame} -\title{Populate Uniprot IDs in Data Frame} -\usage{ -.populateUniprotIdsInDataFrame(df, proteinIdType) -} -\arguments{ -\item{df}{A data frame containing protein information.} - -\item{proteinIdType}{A character string specifying the type of protein ID.} -} -\value{ -A data frame with populated Uniprot IDs. -} -\description{ -Derives \code{GlobalProtein} by stripping the PTM site suffix from each -protein group member, then resolves UniProt IDs per member. For a -protein group the resolved IDs are semicolon-joined in member order. -} From 5440e731edf4b61a90ad719cfea689d77032bf92 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Tue, 22 Sep 2026 10:30:12 -0400 Subject: [PATCH 3/4] fix some unit tests based on feedback --- tests/testthat/test-annotateProteinInfoFromIndra.R | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/testthat/test-annotateProteinInfoFromIndra.R b/tests/testthat/test-annotateProteinInfoFromIndra.R index 9f492f1..8a36d9c 100644 --- a/tests/testthat/test-annotateProteinInfoFromIndra.R +++ b/tests/testthat/test-annotateProteinInfoFromIndra.R @@ -129,10 +129,6 @@ test_that("annotateProteinInfoFromIndra works correctly with HGNC name", { }) -# ----- Protein groups: ";"-joined identifiers are grounded member by member, -# then pooled into the semicolon-joined Entity* columns that -# getSubnetworkFromIndra already fans out. Mocked, so no network access. ----- - test_that(".splitProteinGroup splits, trims and drops empty members", { expect_equal(MSstatsBioNet:::.splitProteinGroup("P13747;P23132"), c("P13747", "P23132")) @@ -155,10 +151,12 @@ test_that(".stripPtmSite removes only the site suffix", { expect_equal(MSstatsBioNet:::.stripPtmSite(c("P13747_S148", "P23132")), c("P13747", "P23132")) expect_equal(MSstatsBioNet:::.stripPtmSite("CLH1_HUMAN"), "CLH1_HUMAN") + expect_equal(MSstatsBioNet:::.stripPtmSite(c("P13747_S148_T149")), + c("P13747")) }) test_that("PTM site suffixes are stripped from every protein group member", { - df <- data.frame(Protein = c("P13747_S148;P23132_T20", "P13747_S148"), + df <- data.frame(Protein = c("P13747;P23132_T20", "P13747_S148"), stringsAsFactors = FALSE) out <- MSstatsBioNet:::.populateUniprotIdsInDataFrame(df, "Uniprot") From 5c65ef67f8c37bb1d0ac77d5a61bb9ab43a7ee2f Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Tue, 22 Sep 2026 10:39:41 -0400 Subject: [PATCH 4/4] fix styling of remaining tests --- .../test-annotateProteinInfoFromIndra.R | 17 ++++------------- tests/testthat/test-multi-grounding.R | 2 +- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/tests/testthat/test-annotateProteinInfoFromIndra.R b/tests/testthat/test-annotateProteinInfoFromIndra.R index 8a36d9c..bfc122a 100644 --- a/tests/testthat/test-annotateProteinInfoFromIndra.R +++ b/tests/testthat/test-annotateProteinInfoFromIndra.R @@ -174,9 +174,9 @@ test_that("annotateProteinInfoFromIndra grounds each Uniprot protein group membe .callGetHgncNamesFromHgncIdsApi = function(hgncIds) { list(`4931` = "HLA-E", `10012` = "RAD23A", `2092` = "CLTC") }, - .callIsTranscriptionFactorApi = function(genes) list(CLTC = FALSE), - .callIsKinaseApi = function(genes) list(CLTC = FALSE), - .callIsPhosphataseApi = function(genes) list(CLTC = FALSE) + .callIsTranscriptionFactorApi = function(genes) list(RAD23A = FALSE, `HLA-E` = FALSE, CLTC = FALSE), + .callIsKinaseApi = function(genes) list(RAD23A = FALSE, `HLA-E` = FALSE, CLTC = FALSE), + .callIsPhosphataseApi = function(genes) list(RAD23A = FALSE, `HLA-E` = FALSE, CLTC = FALSE) ) annotated_df <- annotateProteinInfoFromIndra(df, "Uniprot") @@ -185,12 +185,10 @@ test_that("annotateProteinInfoFromIndra grounds each Uniprot protein group membe expect_equal(group_row$EntityNamespace, "HGNC;HGNC") expect_equal(group_row$EntityId, "4931;10012") expect_equal(group_row$EntityName, "HLA-E;RAD23A") - # Gene-only flags stay NA while the row carries more than one grounding expect_true(is.na(group_row$IsTranscriptionFactor)) expect_true(is.na(group_row$IsKinase)) expect_true(is.na(group_row$IsPhosphatase)) - # A single-identifier row is unaffected by the split single_row <- annotated_df[annotated_df$Protein == "Q00610", ] expect_equal(single_row$EntityNamespace, "HGNC") expect_equal(single_row$EntityId, "2092") @@ -214,7 +212,6 @@ test_that("a protein group whose members share a gene collapses to one grounding expect_equal(annotated_df$EntityNamespace, "HGNC") expect_equal(annotated_df$EntityId, "4931") expect_equal(annotated_df$EntityName, "HLA-E") - # Collapsed to a single grounding, so the gene-only flags are populated expect_equal(annotated_df$IsTranscriptionFactor, FALSE) expect_equal(annotated_df$IsPhosphatase, TRUE) }) @@ -231,14 +228,10 @@ test_that("unresolvable protein group members are dropped, not carried as NA", { ) annotated_df <- annotateProteinInfoFromIndra(df, "Uniprot") - # Partially resolved group keeps only the member that grounded, and so is - # single-grounded and does get the gene-only flags expect_equal(annotated_df$EntityId[1], "4931") expect_equal(annotated_df$EntityName[1], "HLA-E") expect_equal(annotated_df$IsKinase[1], FALSE) - # Fully unresolved group stays NA across the Entity columns, and its - # presence alongside a resolved row must not break the flag lookups expect_true(is.na(annotated_df$EntityNamespace[2])) expect_true(is.na(annotated_df$EntityId[2])) expect_true(is.na(annotated_df$EntityName[2])) @@ -268,8 +261,7 @@ test_that("Uniprot_Mnemonic groups map each member to its own UniProt id", { expect_equal(annotated_df$EntityNamespace[1], "HGNC;HGNC") expect_equal(annotated_df$EntityId[1], "2092;4931") expect_equal(annotated_df$EntityName[1], "CLTC;HLA-E") - - # Only one member resolves, so the row degrades to that single grounding + expect_equal(annotated_df$UniprotId[2], "Q00610") expect_equal(annotated_df$EntityName[2], "CLTC") expect_equal(annotated_df$IsKinase[2], FALSE) @@ -293,7 +285,6 @@ test_that("Hgnc_Name groups pool and deduplicate Gilda groundings", { expect_equal(annotated_df$EntityId[1], "3236;3430") expect_equal(annotated_df$EntityName[1], "EGFR;ERBB2") - # A repeated member contributes its grounding once expect_equal(annotated_df$EntityId[2], "3236") expect_equal(annotated_df$EntityName[2], "EGFR") expect_equal(annotated_df$IsKinase[2], TRUE) diff --git a/tests/testthat/test-multi-grounding.R b/tests/testthat/test-multi-grounding.R index 3597451..cf8fe78 100644 --- a/tests/testthat/test-multi-grounding.R +++ b/tests/testthat/test-multi-grounding.R @@ -197,7 +197,7 @@ test_that("annotateProteinInfoFromIndra with Metabolite mocks Gilda and skips ge # ----- Metabolite E2E test (mocked end-to-end; skipped if real fixture absent) ----- test_that("annotateProteinInfoFromIndra(Metabolite) -> getSubnetworkFromIndra E2E (mocked, real fixture)", { - fixture_path <- system.file("extdata/groupComparisonModel_metabolite.csv", + fixture_path <- system.file("extdata/groupComparisonModel_compound.csv", package = "MSstatsBioNet") skip_if_not(nzchar(fixture_path) && file.exists(fixture_path), "Metabolite fixture not yet provided (see TODO-MSBio-20260528).")