From 857a36cce7feeb5e5b15ec1f62cc5d4644fdeb68 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Tue, 1 Sep 2026 15:41:59 -0400 Subject: [PATCH] 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") + ) +})