This is a test version of Biostars. For the public version, visit https://www.biostars.org.
How upstream is a gene in any of it's pathway ?

Hello,

I have a list of differentially expressed gene in a given experiment.

I would like to get an idea of 'How upstream is a gene in any of it's pathway ?'
I would ideally get an 'upstream score' ranging from 0 to 1 for each gene, 0 meaning the gene is always at the downstream of it's pathways or 1 meaning the gene is always upstream of it's pathways (master regulator).

For instance, in the MAPK signalling Pathway TGFB would have a score of 1, intermediary genes would have a score between 0 and 1, and most downstream genes such as MAX would have scores of 0.

I was thinking about using KEGG pathways and R.

Thanks!

rna-seq r gene pathway

Hello Alex,

I used KEGGgraph as suggested to get Directed Graphs from KEGG pathway database for each gene in my gene list. Here is a solution, the code is probably suboptimal but I hope it is understandable.

The first fonction GetKEGGigraph takes in a KEGG pathway id (e.g. "04010" for MAPK signalling pathway). It first download the pathway as Directed Graph using KEGGgraph, then transform it into an "igraph graph" object. Each gene is a node and each relation geneA -> geneB is a directed edge. We first remove isolated nodes & self loops from the graphs. Then, as some pathways are Cyclic, we need to make them acyclic using Minimum Spanning Tree algorithm. This is because we cannot make a proper gene hierarchy in cyclic graphs. The GetKEGGigraph function returns the Minimum Spanning Tree.

The second function upstream_score_KEGG takes as input a gene list as gene names (e.g. c("TGFB1","MAX")). It first retrieve for each gene all the pathways containing the gene. Then, for each gene in each pathway, it calls GetKEGGigraph to get the Minimum Spanning Tree. The tree is sorted using 'Topological Sorting' algorithm (From wikipedia :"In computer science, a topological sort or topological ordering of a directed graph is a linear ordering of its vertices such that for every directed edge uv from vertex u to vertex v, u comes before v in the ordering"). This way, most upstream genes will be placed before more downstream genes. The function upstream_score_KEGG outputs a data.frame with the upstream score (1 - relative position) of the gene in each of it's pathway, or NA if no pathway is found.

I have very little experience with graph theory so if I would gladly take advices on this part, particularly : it this the best way to get an 'order' of genes in a graph ? I think that with topological sorting, two 'unrelated nodes' will be placed in a kind of arbitrary way.

Here is the code:

# Get "upstream scores" for a list of gene
library(igraph)
library(doParallel)
library(tidyverse)
library(org.Hs.eg.db)
library(KEGGREST)
library(KEGGgraph)

library(foreach)
# Choose number of cores to run parallely (takes time)
registerDoParallel(cores=6)

gene_list <- c("TGFB1", "MAX")
scores <- upstream_score_KEGG(gene_list)
print(scores)

        upstream_score KEGG_id KEGG_pathway  Gene                                      Pathway
X7040_04010      0.7312925    7040        04010 TGFB1                       MAPK signaling pathway
X7040_04060      0.9438596    7040        04060 TGFB1       Cytokine-cytokine receptor interaction
X7040_04110      0.7096774    7040        04110 TGFB1                                   Cell cycle
X7040_04144             NA    7040        04144 TGFB1                                  Endocytosis
X7040_04350      0.3152174    7040        04350 TGFB1                   TGF-beta signaling pathway
                                                                        ......
X4149_04010      0.1870748    4149        04010   MAX                       MAPK signaling pathway
X4149_05200      0.8090129    4149        05200   MAX                           Pathways in cancer
X4149_05222      0.9534884    4149        05222   MAX                       Small cell lung cancer

So the results is, for MAPK signalling pathway, TGFB1 has score of 0.731 & MAX has score of 0.187. This gives you an idea of how upstream the gene is in a pathway. In the 'TGF-beta signaling pathway' for instance the score for TGFB1 is only 0.31 because it is downstream of 5 genes in this pathway (TGF-beta signaling pathway).

The trouble comes when you average over all the pathways, because as you said pathways are complex and the end of any pathways usually comes before and after any pathway.... So the same gene might be the most downstream in one pathway and the most upstream in another pathway...

I hope the approach is not too complex for nothing and I would gladly hear any comments on better way to achieve the same results.

I must put the functions separately as the limit is 5,000 characers. Here is the first function

GetKEGGigraph <- function(KEGG_pathway_id, plot = FALSE, plot_file=NULL){
stopifnot(is.character(KEGG_pathway_id))

# retrieve pathway thanks to KEGGgraph
tmp <- tempfile()
res = KEGGgraph::retrieveKGML(KEGG_pathway_id, organism="hsa", destfile=tmp, method="wget", quiet=TRUE)
mapkG <- KEGGgraph::parseKGML2Graph(res,expandGenes=TRUE, genesOnly = TRUE)

outs <- sapply(KEGGgraph::edges(mapkG), length) > 0
ins <- sapply(KEGGgraph::inEdges(mapkG), length) > 0
ios <- outs | ins
## translate the KEGG IDs into Gene Symbol
if(require(org.Hs.eg.db)) {
    ioGeneID <- KEGGgraph::translateKEGGID2GeneID(names(ios))
    nodesNames <- sapply(mget(ioGeneID, org.Hs.egSYMBOL, ifnotfound=NA), "[[",1)
} else {
    nodesNames <- names(ios)
}
names(nodesNames) <- names(ios)

mapkG_igraph = igraph::igraph.from.graphNEL(mapkG, name = TRUE, weight = TRUE,
                                    unlist.attrs = TRUE)
mapkG_igraph = igraph::simplify(mapkG_igraph, remove.multiple = TRUE, remove.loops = TRUE,
                        edge.attr.comb = igraph::igraph_opt("edge.attr.comb"))
Isolated = which(igraph::degree(mapkG_igraph)==0)
mapkG_igraph = igraph::delete.vertices(mapkG_igraph, Isolated)

# Minimum spanning tree graph from pathway
mstree = igraph::mst(mapkG_igraph)
V(mstree)$id <- seq_len(vcount(mstree))-1
roots <- sapply(igraph::decompose(mstree), function(x) {
    V(x)$id[ igraph::topo_sort(x)[1]+1 ] })

if(plot & !is.null(plot_file)){
    # Change names to gene name for graph
    mapkG@nodes = nodesNames
    names(mapkG@edgeL) = nodesNames

    mapkG_igraph_gene = igraph::igraph.from.graphNEL(mapkG, name = TRUE, weight = TRUE,
                                             unlist.attrs = TRUE)
    mapkG_igraph_gene = igraph::simplify(mapkG_igraph_gene, remove.multiple = TRUE, remove.loops = TRUE,
                            edge.attr.comb = igraph::igraph_opt("edge.attr.comb"))
    Isolated = which(igraph::degree(mapkG_igraph_gene)==0)
    mapkG_igraph_gene = igraph::delete.vertices(mapkG_igraph_gene, Isolated)

    # Minimum spanning tree graph from pathway
    mstree_gene = igraph::mst(mapkG_igraph_gene)
    pdf(file.path(plot_file))
    plot(mstree_gene, layout = igraph::layout_nicely(mstree_gene),
         vertex.color= ifelse(,"red","grey"), vertex.size = 3.75,
         vertex.label.cex=0.25, edge.arrow.width=0.25, edge.arrow.size=0.25, edge.width=0.5)
    dev.off()

}

return(mstree)
}

Here is the second function:

upstream_score_KEGG <- function(gene_list){
##Get the Entrez gene IDs associated with those symbols
EG_IDs = mget(gene_list, revmap(org.Hs.egSYMBOL),ifnotfound=NA)

##Then get the KEGG IDs associated with those entrez genes.
KEGG_IDs = mget(as.character(EG_IDs), org.Hs.egPATH,ifnotfound=NA)

results <- foreach::foreach(KEGG_id = names(KEGG_IDs), .combine=rbind,
                            .packages=c('KEGGREST',"KEGGgraph",'igraph')) %dopar% 
    {
    # results=data.frame("upstream_score"=0,"KEGG_id"="")
     # for(KEGG_id in names(KEGG_IDs)) {
        KEGG_pathway_id = KEGG_IDs[[KEGG_id]]
        print(KEGG_id)
        ifis.na(KEGG_pathway_id[1])) {
            ret = data.frame("upstream_score"=NA, "KEGG_id"=KEGG_id)
            return(ret)
            # results = rbind(results,ret)
        } else {
            list_topo_sorted <- lapply(KEGG_pathway_id, function(id){
                mstree_sorted <- data.frame("upstream_score"=0,"KEGG_id"="")
                 try({
                    mstree <- GetKEGGigraph(id, plot=FALSE)
                    mstree_sorted <- igraph::as_ids(igraph::topo_sort(mstree))
                    }, TRUE)

                return(mstree_sorted)
            })
            names(list_topo_sorted) <- paste0(KEGG_id,"_",KEGG_pathway_id)
            list_topo_sorted = lapply(list_topo_sorted, function(x){
                n = which(x==paste0("hsa:",KEGG_id))/length(x)
                if(length(n)==0) return(NA) else return(n[1])
            } )
            df_topo_sorted = as.data.frame(t(as.data.frame(list_topo_sorted,drop=F)))
            df_topo_sorted$KEGG_id = rep(KEGG_id,nrow(df_topo_sorted))
            colnames(df_topo_sorted)[1] = "upstream_score"
            if(!is.null(df_topo_sorted)) return(df_topo_sorted)
            # if(!is.null(df_topo_sorted)) results=rbind(results,df_topo_sorted)
        }

    }
results$KEGG_pathway = rownames(results)
results$KEGG_pathway[grep("_",results$KEGG_pathway,invert = T)] = NA
results$KEGG_pathway = gsub(".*_","",results$KEGG_pathway)

results$Gene = sapply(mget(results$KEGG_id, org.Hs.egSYMBOL, ifnotfound=NA), "[[",1)
results$Pathway = ""

l = sapply(paste0("path:hsa",results$KEGG_pathway[which(!is.na(results$KEGG_pathway))]),function(x){
    print(x)
    ret = ""
    try({
        query = keggGet(x)
        ret = query[[1]]$PATHWAY_MAP
    }, TRUE)
    return(ret)
    })
results$Pathway[which(!is.na(results$KEGG_pathway))] = l
return(results)
}

You can put all this code in a GitHub gist. Make it public and then paste that link into your comment above. Biostars code will then render it all in one stretch.

Please use ADD COMMENT/ADD REPLY when responding to existing posts to keep threads logically organized. SUBMIT ANSWER is for new answers to original question.

Okay I will do that, thank you

If you can delete these duplicate comments that would be great. You can find "delete post" option under the moderate button for your own posts.

1 answer

I have absolutely no idea whether the metric you're asking for is really meaningful, but it's easy to get on a pathway module level.

Solution for R with the use of tidyverse and KEGGREST package.

kegg_id_names variable at the beginning of the code should be the vector of kegg id's for your genes. kegg_positions_by_module is a tibble containing relative position of a gene in each module that the gene is listed in (in kegg). Such a position for a given pathway is calculated from the list of genes in module description (like here in reaction section: https://www.genome.jp/kegg-bin/show_module?M00001 ). Genes on the same "level" share the same score.

library(tidyverse)
library(KEGGREST)

kegg_id_names = c('R01786', 'R01063')

path_ids = map(kegg_id_names, ~keggLink('module', .)) %>% 
  unlist() %>%
  unique()

path_data = keggGet(path_ids)

get_kegg_position = function(path_record) {

  gene_names = names(path_record$REACTION)

  if (is.null(gene_names)) {
    res = tibble(gene = NA,
                 position = NA)
  }else{
    gene_posns = seq(1, 0, length.out = length(gene_names))

    res = tibble(gene = gene_names,
                 position = gene_posns) %>% 
      separate_rows('gene', sep = ',')
  }

  res %>% mutate(path_id = path_record$ENTRY,
                 path_name = path_record$NAME)

}

kegg_positions_by_module = map_df(path_data,
                                  get_kegg_position) %>% 
  filter(!is.na(position))

You can get position in whole pathway replacing kegglink part by "~keggLink('pathway', .)", but I'think it can be wrong since whole pathway is more "intertwined".

Thank you for your answer, I managed to adapt it to Pathways instead of Modules :

library(tidyverse)
library(KEGGREST)

kegg_id_names = c('hsa:7040')

path_ids = map(kegg_id_names, ~keggLink('pathway', .)) %>% 
    unlist() %>%
    unique()

path_data = keggGet(path_ids[1:3])

get_kegg_position = function(path_record) {

    gene_ids = path_record$GENE[seq(1,length(path_record$GENE),2)]
    gene_names = gsub(";.*","",path_record$GENE[seq(2,length(path_record$GENE),2)])

    if (is.null(gene_ids)) {
        res = tibble(gene = NA,
                     position = NA)
    }else{
        gene_posns = seq(1, 0, length.out = length(gene_ids))

        res = tibble(gene_id = gene_ids,
                     gene_name = gene_names,
                     position = gene_posns) %>% 
            separate_rows('gene_id', sep = ',')
    }

    res %>% mutate(path_id = path_record$ENTRY,
                   path_name = path_record$NAME)

}

kegg_positions_by_module = map_df(path_data,
                                  get_kegg_position) %>% 
    filter(!is.na(position))

# TGFB1 score in MAPK pathway (should be close to 1)
kegg_positions_by_module %>%
    filter(path_id=="hsa04010") %>% filter(gene_name == "TGFB1")
# SRF score in MAPK pathway (should be lower than TGFB1 as it is only upstream of 1 gene (FOS)) 
    kegg_positions_by_module %>%
    filter(path_id=="hsa04010") %>% filter(gene_name == "SRF")
# MAX score in MAPK pathway (should be close to 0)
kegg_positions_by_module %>%
    filter(path_id=="hsa04010") %>% filter(gene_name == "MAX")

Here I obtain a value of only 0.433 for TGFB1 whereas I should get value closer to 1. For SRF gene, I obtain value of 0.481 which is higher than TGFB1 while SRF is upstream of only 1 gene and TGFB1 is upstream of many more genes. (See MAPK pathway diagram)

I think your approach is based on the hypothesis that KEGG ranks genes from more upstream to more downstream. While this might be true for reactions, I think it is not for Pathways. I think the order of genes in KEGG pathway is from Left to Right and Top to Bottom and in the KEGG diagrams.

I am sure this score would never be accurately quantitative but I would hope it gives an idea about how often a gene is upstream of pathways.

I think that this complexity on a pathway level may be managed using some sort of graphs, so perhaps KEGGgraph package may provide some solution. But I haven't work with it. If you'll find a solution, please share it.

Hello Alex,

I used KEGGgraph as suggested to get Directed Graphs from KEGG pathway database for each gene in my gene list. Here is a solution, the code is probably suboptimal but I hope it is understandable.

The first fonction GetKEGGigraph takes in a KEGG pathway id (e.g. "04010" for MAPK signalling pathway). It first download the pathway as Directed Graph using KEGGgraph, then transform it into an "igraph graph" object. Each gene is a node and each relation geneA -> geneB is a directed edge. We first remove isolated nodes & self loops from the graphs. Then, as some pathways are Cyclic, we need to make them acyclic using Minimum Spanning Tree algorithm. This is because we cannot make a proper gene hierarchy in cyclic graphs. The GetKEGGigraph function returns the Minimum Spanning Tree.

The second function upstream_score_KEGG takes as input a gene list as gene names (e.g. c("TGFB1","MAX")). It first retrieve for each gene all the pathways containing the gene. Then, for each gene in each pathway, it calls GetKEGGigraph to get the Minimum Spanning Tree. The tree is sorted using 'Topological Sorting' algorithm (From wikipedia :"In computer science, a topological sort or topological ordering of a directed graph is a linear ordering of its vertices such that for every directed edge uv from vertex u to vertex v, u comes before v in the ordering"). This way, most upstream genes will be placed before more downstream genes. The function upstream_score_KEGG outputs a data.frame with the upstream score (1 - relative position) of the gene in each of it's pathway, or NA if no pathway is found.

I have very little experience with graph theory so if I would gladly take advices on this part, particularly : it this the best way to get an 'order' of genes in a graph ? I think that with topological sorting, two 'unrelated nodes' will be placed in a kind of arbitrary way.

Here is the code:

So the results is, for MAPK signalling pathway, TGFB1 has score of 0.731 & MAX has score of 0.187. This gives you an idea of how upstream the gene is in a pathway. In the 'TGF-beta signaling pathway' for instance the score for TGFB1 is only 0.31 because it is downstream of 5 genes in this pathway (TGF-beta signaling pathway).

The trouble comes when you average over all the pathways, because as you said pathways are complex and the end of any pathways usually comes before and after any pathway.... So the same gene might be the most downstream in one pathway and the most upstream in another pathway...

I hope the approach is not too complex for nothing and I would gladly hear any comments on better way to achieve the same results.

Thanks for sharing the solution! Unfortunately, I can not provide you any further guidance(

Log in to answer this question.