pathfindR is an R package for pathway/gene-set enrichment analysis that does something most enrichment tools don’t: instead of testing your gene list directly, it first maps your genes onto a protein–protein interaction network (PIN), finds active subnetworks (connected groups of interacting genes that are enriched for significantly altered genes), and then runs enrichment on those subnetworks. The payoff is that genes which don’t pass your significance cutoff on their own (but sit in the middle of a cluster of genes that do) still get a chance to contribute.
This post walks through a typical workflow end to end: run the analysis, cluster the results, visualize, and score terms per sample.
What changed in 3.0.0: no more Java
If you used older versions, you’ll remember the asterisk next to “easy install”:
the active subnetwork search ran on a bundled Java .jar, so you needed a JRE on
your PATH and a fair amount of “is Java configured correctly?” troubleshooting.
As of pathfindR 3.0.0, that’s gone. The greedy (GR), simulated annealing (SA), and genetic (GA) search algorithms have been re-implemented in R and C++ (via Rcpp), and the Java dependency was removed entirely. For GR and SA, results are numerically identical to the legacy JAR given the same seed and inputs so this is a drop-in upgrade, not a behaviour change. Installation is now just:
install.packages("pathfindR")
That’s the whole story. No JAVA_HOME, no PATH surgery.
The input
The input is a simple data frame with three columns: a gene symbol, an
(optional) change value such as log-fold-change, and a p-value. pathfindR ships
with an example derived from a rheumatoid arthritis vs. healthy
differential-expression dataset (GEO GSE15573):
library(pathfindR)
#> Loading required package: pathfindR.data
head(example_pathfindR_input)
#> Gene.symbol logFC adj.P.Val
#> 1 FAM110A -0.69394 3.4087e-06
#> 2 RNASE2 1.35350 1.0085e-05
#> 3 S100A8 1.54483 3.4664e-05
#> 4 S100A9 1.02809 2.2626e-04
#> 5 TEX261 -0.32360 2.2626e-04
#> 6 ARHGAP17 -0.69193 2.7081e-04
Running the analysis
The wrapper run_pathfindR() takes you from input to enriched terms in one call:
it processes the input, maps it onto the PIN, searches for active subnetworks,
and runs enrichment over a chosen number of iterations.
output_df <- run_pathfindR(example_pathfindR_input)
A few arguments worth knowing (all optional):
# pick the gene sets (default = "KEGG")
output_df <- run_pathfindR(example_pathfindR_input, gene_sets = "GO-MF")
# pick the PIN for the subnetwork search (default = "Biogrid")
output_df <- run_pathfindR(example_pathfindR_input, pin_name_path = "STRING")
# write the HTML report somewhere specific
output_df <- run_pathfindR(example_pathfindR_input, output_dir = "RA_results")
Available PINs include "Biogrid", "STRING", "GeneMania", "IntAct",
"KEGG" and "mmu_STRING"; available gene sets include "KEGG", "Reactome",
"BioCarta", the GO families ("GO-All", "GO-BP", "GO-CC", "GO-MF") and
"mmu_KEGG". You can also supply a custom PIN or custom gene sets.
The result is a data frame, one row per enriched term, with the term ID and description, enrichment p-values (lowest/highest across iterations), fold enrichment, and the affected up-/down-regulated input genes. The package also bundles a pre-computed copy of this output so we can keep going without waiting on the search:
head(example_pathfindR_output, 4)
#> ID Term_Description Fold_Enrichment occurrence
#> 1 hsa05415 Diabetic cardiomyopathy 3.7450 10
#> 2 hsa03082 ATP-dependent chromatin remodeling 4.2131 10
#> 3 hsa04130 SNARE interactions in vesicular transport 4.9366 10
#> 4 hsa00190 Oxidative phosphorylation 3.5587 10
#> support lowest_p highest_p
#> 1 0.087103 4.1728e-15 1.1843e-08
#> 2 0.024579 1.8507e-12 3.1203e-04
#> 3 0.033857 2.8509e-09 8.7854e-09
#> 4 0.044888 4.1980e-09 4.3382e-06
#> Up_regulated
#> 1 NCF4, NDUFA1, NDUFB3, UQCRQ, COX6A1, COX7A2, COX7C, MMP9, GAPDH
#> 2
#> 3 STX6
#> 4 NDUFB3, NDUFA1, COX7C, COX7A2, UQCRQ, COX6A1, ATP6V1D, ATP6V0E1
#> Down_regulated
#> 1 SLC25A5, VDAC1, ATP2A2, MTOR, PDHA1, PDHB, PARP1
#> 2 ARID1A, SMARCA4, ACTB, ACTG1, BRD7, BRD9, HDAC1, EP400, TRRAP
#> 3 BET1L, SNAP23, STX2
#> 4 ATP6V0E2
A quick way to eyeball the top hits is the enrichment chart, a bubble plot of the most significant terms:
enrichment_chart(example_pathfindR_output, top_terms = 15)
#> Plotting the enrichment bubble chart
Clustering the enriched terms
Enrichment output is often redundant, several terms describe overlapping
biology. cluster_enriched_terms() computes pairwise kappa statistics between
terms, clusters them (hierarchical by default), picks the cluster count that
maximizes the average silhouette width, and tags one representative term per
cluster:
clustered_df <- cluster_enriched_terms(example_pathfindR_output)
#> The maximum average silhouette width was 0.24 for k = 60


# the representative term from each cluster
clustered_df[clustered_df$Status == "Representative", c("Term_Description", "Cluster")]
#> Term_Description Cluster
#> 1 Diabetic cardiomyopathy 1
#> 2 ATP-dependent chromatin remodeling 2
#> 3 SNARE interactions in vesicular transport 3
#> 7 Nucleocytoplasmic transport 4
#> 11 NF-kappa B signaling pathway 5
#> 12 Polycomb repressive complex 6
#> 14 Fatty acid degradation 7
#> 15 Spliceosome 8
#> 18 Mismatch repair 9
#> 19 Shigellosis 10
#> 20 Th17 cell differentiation 11
#> 21 Human T-cell leukemia virus 1 infection 12
#> 22 Nucleotide excision repair 13
#> 23 Protein processing in endoplasmic reticulum 14
#> 24 TGF-beta signaling pathway 15
#> 25 Carbon metabolism 16
#> 26 Apoptosis 17
#> 27 Salmonella infection 18
#> 28 Oxytocin signaling pathway 19
#> 29 JAK-STAT signaling pathway 20
#> 32 IgSF CAM signaling 21
#> 33 Collecting duct acid secretion 22
#> 37 Adipocytokine signaling pathway 23
#> 38 Ubiquitin mediated proteolysis 24
#> 40 Rap1 signaling pathway 25
#> 42 Chronic myeloid leukemia 26
#> 44 Viral carcinogenesis 27
#> 45 Cellular senescence 28
#> 46 Proteasome 29
#> 47 Autophagy - animal 30
#> 51 Toxoplasmosis 31
#> 52 Neurotrophin signaling pathway 32
#> 55 mTOR signaling pathway 33
#> 56 Cell cycle 34
#> 57 Spinocerebellar ataxia 35
#> 61 Cysteine and methionine metabolism 36
#> 64 T cell receptor signaling pathway 37
#> 65 Vasopressin-regulated water reabsorption 38
#> 66 Neutrophil extracellular trap formation 39
#> 68 Fluid shear stress and atherosclerosis 40
#> 70 Transcriptional misregulation in cancer 41
#> 73 Phagocytosis 42
#> 77 Pertussis 43
#> 79 Influenza A 44
#> 81 Herpes simplex virus 1 infection 45
#> 86 IL-17 signaling pathway 46
#> 92 Integrated stress response (ISR) signaling pathway 47
#> 97 Epstein-Barr virus infection 48
#> 99 Lysosome biogenesis 49
#> 111 mRNA surveillance pathway 50
#> 114 Arginine and proline metabolism 51
#> 118 Lipid and atherosclerosis 52
#> 123 Osteoclast differentiation 53
#> 124 Ribosome 54
#> 126 FoxO signaling pathway 55
#> 131 Retrograde endocannabinoid signaling 56
#> 132 TNF signaling pathway 57
#> 134 Galactose metabolism 58
#> 136 Fc gamma R-mediated phagosome formation 59
#> 137 Antifolate resistance 60
enrichment_chart(example_pathfindR_output, plot_by_cluster = TRUE)
#> Plotting the enrichment bubble chart
#> For plotting by cluster, there must a column named `Cluster` in the input data frame!
Filtering to representatives gives you a compact, non-redundant shortlist to
report. There’s also a method = "fuzzy" option if you’d rather let terms belong
to more than one cluster.
Visualizing results
Beyond the enrichment chart, two visuals are especially handy.
A term–gene heatmap shows which input genes drive which terms. If you pass the original input, tiles are colored by change value:
term_gene_heatmap(example_pathfindR_output, example_pathfindR_input, num_terms = 10)
#> ## Processing input. Converting gene symbols,
#> if necessary (and if human gene symbols provided)
#> Number of genes provided in input: 572
#> Number of genes in input after p-value filtering: 572
#>
#> Could not find any interactions for 1 (0.17%) genes in the PIN
#> Final number of genes in input: 571
A term–gene graph shows the same relationships as a network, making shared genes between terms obvious. As of 3.x this is split into a builder and a plotter:
g <- create_term_gene_graph(example_pathfindR_output, num_terms = 10)
create_term_gene_plot(g)
Scoring terms per sample
If you have a per-sample expression matrix, score_terms() computes an
agglomerated z-score for each enriched term in each sample — useful for seeing
whether a pathway is collectively activated or repressed in particular samples or
groups:
case_sample_ids <- c(
"GSM389703", "GSM389704", "GSM389705", "GSM389706", "GSM389707", "GSM389708"
)
score_matrix <- score_terms(
enrichment_table = clustered_df[clustered_df$Status == "Representative", ],
exp_mat = example_experiment_matrix, # genes x samples
cases = case_sample_ids, # optional
plot_hmap = TRUE
)
Putting it together
The whole everyday workflow is really just:
library(pathfindR)
output_df <- run_pathfindR(example_pathfindR_input) # enrichment
clustered_df <- cluster_enriched_terms(output_df) # de-duplicate
enrichment_chart(clustered_df, plot_by_cluster=TRUE) # visualize
term_gene_heatmap(output_df, example_pathfindR_input) # who drives what
Four functions, and no Java to install. If you’re comparing two
conditions, take a look at combine_pathfindR_results() too.
pathfindR is described in Ulgen E, Ozisik O, Sezerman OU. 2019. pathfindR: An R Package for Comprehensive Identification of Enriched Pathways in Omics Data Through Active Subnetworks. Front. Genet. https://doi.org/10.3389/fgene.2019.00858. Docs: https://egeulgen.github.io/pathfindR/.