punKEGGer: parsing and visualization of KEGG pathways for network science

Introduction

punKEGGer was created to make KEGG pathway data accessible and useful for network science workflows.

Traditional approaches to KEGG data within Bioconductor often reduce pathways to simple gene sets (e.g., for enrichment analysis), custom-saved graphNEL objects, or static visualizations with hardcoded layouts. While those are valid for many use cases, they often lose the rich topological and visual information encoded in KEGG’s own pathway representations.

punKEGGer provides a different approach: it parses the original .kgml files and converts them into clean, annotated igraph objects, with optional layouts and metadata preserved. From there, you can analyze topological features, integrate identifiers, combine multiple pathways, or run dynamic simulations — whatever your network science questions demand. It is, after all, a Pretty Useful Networks-from-KEGG Exploration Resource.

Example 1: Parse a single KEGG pathway

This first example shows the basic parsing workflow. We start from a KGML file, which is KEGG’s XML representation of a pathway, and convert it into a tidygraph/igraph object. The resulting graph preserves pathway relations and can be used directly with standard network analysis tools.

# usually you'll read from the official database like so
# kgml <- read_xml("https://rest.kegg.jp/get/hsa04210/kgml")
# or use the function fetch_kegg_kgml
# kgml <- fetch_kegg_kgml("hsa04210")
# but for reproducibility purposes you can use 
kgml_file <- system.file("extdata", "hsa04210.xml", package = "punKEGGer")
kgml <- xml2::read_xml(kgml_file)

g <- combine_kegg_network(kgml)
g
#> # A tbl_graph: 108 nodes and 141 edges
#> #
#> # A directed acyclic simple graph with 5 components
#> #
#> # Node Data: 108 × 2 (active)
#>    name  meta_id
#>    <chr> <chr>  
#>  1 53    53     
#>  2 47    47     
#>  3 54    54     
#>  4 48    48     
#>  5 43    43     
#>  6 152   152    
#>  7 51    51     
#>  8 46    46     
#>  9 38    295    
#> 10 156   156    
#> # ℹ 98 more rows
#> #
#> # Edge Data: 141 × 4
#>    from    to type  subtype   
#>   <int> <int> <chr> <chr>     
#> 1     1     2 PPrel activation
#> 2     2     5 PPrel activation
#> 3     3     4 PPrel activation
#> # ℹ 138 more rows

The printed object summarizes the parsed pathway as an tidygraph object. At this stage, nodes still are annotated using KEGG entry identifiers; annotation is added in the next step.

Example 2: Add gene annotations

KEGG entries often group several molecules (genes, etc) identifiers into a single pathway node. For molecule-level analyses, punKEGGer can expand these metanodes and then attach external identifiers, such as HGNC symbols, using a user-provided dictionary; such dictionaries can be obtained from resources such as BioMart.

node_info <- extract_kegg_nodes(kgml)
dict_path <- system.file("extdata", "example_dict.hsa04210.csv", package = "punKEGGer")
kegg_dict <- read.csv(dict_path)
# creates the dictionary for the metanodes
meta_dict <- create_meta_dict(node_info, kegg_dict)
# expands metanodes and returns a new graph
g_exp <- expand_metagraph(g, node_info, node_types = "gene")
#annotates expanded graph 
g_annot <- annotate_kegg_graph(g_exp, meta_dict, identifiers = "hgnc_symbol")

#show the expanded graph with annotated nodes
tidygraph::as_tibble(g_annot, active = "nodes") |>
  dplyr::select(name, meta_id, type, hgnc_symbol) |>
  head()
#> # A tibble: 6 × 4
#>   name     meta_id type  hgnc_symbol
#>   <chr>    <chr>   <chr> <chr>      
#> 1 hsa:598  10      gene  BCL2L1     
#> 2 hsa:1676 11      gene  DFFA       
#> 3 hsa:472  12      gene  ATM        
#> 4 hsa:7157 13      gene  TP53       
#> 5 hsa:2021 14      gene  ENDOG      
#> 6 hsa:9131 15      gene  AIFM1

Here, we expanded metanodes into single nodes. Then, we add identifier columns to the node table. These columns can then be used for plotting, filtering, or downstream analyses.

Example 3: Visualize your pathway

KEGG pathways include manually curated coordinates that reproduce the familiar KEGG pathway layout. layout_kegg() extracts those coordinates, and plot_kegg_graph() uses them to create a KEGG-like network plot.

layout_tbl <- layout_kegg(kgml)
g_layout <- add_kegg_layout(g_annot, layout_tbl)
plot_kegg_graph(g_layout, id = hgnc_symbol)

Alternative layout using ggraph:

In some analyses, the curated KEGG layout is less important than the graph topology itself. Since the output is a standard graph object, it can also be visualized with any other ggraph-compatible layouts.

library(ggraph)
#> Loading required package: ggplot2
library(tidygraph)
#> 
#> Attaching package: 'tidygraph'
#> The following object is masked from 'package:stats':
#> 
#>     filter

# let's calculate some simple centrality measures
g_annot <- 
  g_annot |> 
  mutate(degree = centrality_degree())

# now we visualize the network using a force-directed layout, and show node size and label size proportional to degree

ggraph(g_annot, layout = "fr") +
  geom_edge_link() +
  geom_node_label(aes(size = degree, 
                     label = hgnc_symbol),
                  fill = "#9D4EDD",
                  color = "white",
                  label.r = unit(0.15,"lines"),
                  label.padding = unit(0.25,"lines"),
                  label.size = 0.2,
                  repel = TRUE
                 ) + 
  theme_void()

Example 4: Combine pathways and connect to your network science pipeline



#let's identify modules in the network using the Infomap algorithm

g_annot <- 
  as_tbl_graph(g_annot)

g_annot <- 
  g_annot |> 
  mutate(infomap = group_infomap())



#let's calculate degree centrality 
g_annot <- 
  g_annot |> 
  mutate(degree = centrality_degree())

# now, let's identify the gene(s) with highest centrality per module 

df <- 
  g_annot |> 
  as_tibble() |> 
  group_by(infomap) |> 
  filter(degree == max(degree)) |> 
  arrange(infomap)

print(df)
#> # A tibble: 14 × 6
#> # Groups:   infomap [8]
#>    name      meta_id type  hgnc_symbol degree infomap
#>    <chr>     <chr>   <chr> <chr>        <dbl>   <int>
#>  1 hsa:10000 33      gene  AKT3             7       1
#>  2 hsa:207   33      gene  AKT1             7       1
#>  3 hsa:208   33      gene  AKT2             7       1
#>  4 hsa:4790  23      gene  NFKB1            5       2
#>  5 hsa:5970  23      gene  RELA             5       2
#>  6 hsa:823   32      gene  CAPN1            3       3
#>  7 hsa:9020  34      gene  MAP3K14          3       4
#>  8 hsa:8795  47      gene  TNFRSF10B        1       5
#>  9 hsa:8797  47      gene  TNFRSF10A        1       5
#> 10 hsa:355   48      gene  FAS              1       5
#> 11 hsa:3562  49      gene  IL3              1       5
#> 12 hsa:842   19      gene  CASP9            3       6
#> 13 hsa:9131  15      gene  AIFM1            1       7
#> 14 hsa:8743  53      gene  TNFSF10          1       8

In this example, we take our previous KEGG graph, find modules using a community detection method, and identify the most connected gene in each module. This is just an example; From here, you can compute centralities, detect communities, or simulate diffusion – whatever your network science needs demand.

#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04.1 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
#>  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
#>  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
#>  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
#> 
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] tidygraph_1.3.1   ggraph_2.2.2      ggplot2_4.0.3     punKEGGer_0.99.65
#> [5] BiocStyle_2.41.0 
#> 
#> loaded via a namespace (and not attached):
#>  [1] viridis_0.6.5       sass_0.4.10         utf8_1.2.6         
#>  [4] generics_0.1.4      tidyr_1.3.2         xml2_1.6.0         
#>  [7] stringi_1.8.9       digest_0.6.39       magrittr_2.0.5     
#> [10] evaluate_1.0.5      grid_4.6.1          RColorBrewer_1.1-3 
#> [13] fastmap_1.2.0       jsonlite_2.0.0      ggrepel_0.9.8      
#> [16] gridExtra_2.3.1     BiocManager_1.30.27 purrr_1.2.2        
#> [19] viridisLite_0.4.3   scales_1.4.0        tweenr_2.0.3       
#> [22] jquerylib_0.1.4     cli_3.6.6           graphlayouts_1.2.5 
#> [25] rlang_1.3.0         polyclip_1.10-7     withr_3.0.3        
#> [28] cachem_1.1.0        yaml_2.3.12         otel_0.2.0         
#> [31] tools_4.6.1         memoise_2.0.1       dplyr_1.2.1        
#> [34] buildtools_1.0.0    vctrs_0.7.3         R6_2.6.1           
#> [37] lifecycle_1.0.5     stringr_1.6.0       MASS_7.3-66        
#> [40] pkgconfig_2.0.3     pillar_1.11.1       bslib_0.12.0       
#> [43] gtable_0.3.6        Rcpp_1.1.2          glue_1.8.1         
#> [46] ggforce_0.5.0       xfun_0.61           tibble_3.3.1       
#> [49] tidyselect_1.2.1    sys_3.4.3           knitr_1.52         
#> [52] farver_2.1.2        htmltools_0.5.9     igraph_2.3.4       
#> [55] labeling_0.4.3      rmarkdown_2.32      maketools_1.3.2    
#> [58] compiler_4.6.1      S7_0.2.2