--- title: "CorNetto Workflow" author: "Bradley Ward" date: "`r Sys.Date()`" output: BiocStyle::html_document: toc: true vignette: > %\VignetteIndexEntry{1. CorNetto Workflow} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(CorNetto) ``` # Introduction CorNetto builds knowledge-guided multi-omic correlation networks from normalized assay data stored in a `r BiocStyle::Biocpkg("MultiAssayExperiment")`. It is intended for workflows where transcriptomic, proteomic, metabolomic, or related assays are already normalized and the analysis question is whether groups differ in correlation structure, prior-supported connectivity, or node-level rewiring. The package uses standard Bioconductor containers for assay and sample metadata, and returns standardized edge tables that can be combined, converted to `r BiocStyle::CRANpkg("igraph")` objects, or exported as [Cytoscape](https://cytoscape.org)-ready node and edge tables. CorNetto is for downstream analysis after assay-level preprocessing. It uses Bioconductor containers, implements the Fisher z-difference test for differential correlation, and uses `r BiocStyle::CRANpkg("igraph")` for graph representation. The package adds the glue needed for prior-guided candidate-edge differential correlation, knowledge-network-aware integration, focused subnetworks, and rewiring summaries in a single reproducible workflow. # Relationship to existing Bioconductor software The Bioconductor package `r BiocStyle::Biocpkg("dcanr")` provides several methods and an evaluation framework for differential co-expression or association network inference. CorNetto has a narrower role. It works directly with `r BiocStyle::Biocpkg("MultiAssayExperiment")`, tests prespecified within- or cross-assay edges from heterogeneous prior networks, and carries those results through network integration and node-level rewiring summaries. It is therefore complementary to `r BiocStyle::Biocpkg("dcanr")` rather than a replacement for its method-comparison and benchmarking framework. # Workflow overview Green boxes are CorNetto functions, blue boxes are the objects they return, and orange boxes are inputs and the `r BiocStyle::Biocpkg("MultiAssayExperiment")` that carries results between steps. The diagram outlines how the functions relate to one another. Inputs (yellow) are read either by `readKnowledgeNetwork()`, for prior-knowledge databases, or by `createAnalysisData()`, for multi-omic abundance data and sample metadata. Each returns a package-native object: a prior network, or a CorNetto `MultiAssayExperiment`. From there the package builds correlation networks, differential correlation networks, rewiring scores and permuted rewiring scores. The order shown is illustrative, not prescriptive. You could equally run `createDifferentialCorrelationNetwork()`, narrow the result with `filterNetworkByNodes()`, then call `createNetworkGraph()` to obtain a focused differential-correlation graph. The bottom-right panel shows the two ways results are handled: a function can return its result directly, or store it inside the CorNetto `MultiAssayExperiment`, from which `getCorNettoResult()` retrieves it by name. Both work; which you use is a matter of preference. ```{r workflow-flowchart, echo = FALSE, out.width = "100%", fig.cap = "The CorNetto workflow, from input data and prior knowledge through to rewiring validation and export."} knitr::include_graphics("figures/flowchart.png") ``` # Version information ```{r version-information} c( R = R.version.string, Bioconductor = as.character(BiocManager::version()), CorNetto = as.character(utils::packageVersion("CorNetto")) ) ``` # Installation After acceptance to Bioconductor, install CorNetto with: ```{r install, eval = FALSE} if (!requireNamespace("BiocManager", quietly = TRUE)) { install.packages("BiocManager") } BiocManager::install("CorNetto") ``` # Example data ```{r load-data} analysisData <- exampleAnalysisData() knowledgeNetwork <- exampleKnowledgeNetwork() seedNodes <- exampleSeedNodes() analysisData summarizeAnalysisData(analysisData, groupColumn = "clinicalGroup", quiet = TRUE) head(knowledgeNetwork) seedNodes ``` The synthetic example contains three assays named `protein`, `transcript`, and `metabolite`. The example prior network also contains a drug-target edge to demonstrate that prior networks can include static knowledge beyond the measured assays. Prior-guided candidate-edge analyses must therefore be given only prior edges whose assay names are present in the analysis object. ```{r measured-priors} measuredAssays <- c("protein", "transcript", "metabolite") measuredKnowledgeNetwork <- knowledgeNetwork[ knowledgeNetwork$fromAssayName %in% measuredAssays & knowledgeNetwork$toAssayName %in% measuredAssays, , drop = FALSE ] ``` # Dense group-specific correlations `createCorrelationNetwork()` computes all pairwise within-omic correlations for one assay and one group. Dense all-pairs mode is intended for small-to-medium assays; for large assays, prefer prior-guided candidate-edge testing when suitable. ```{r dense-correlations} recoveredProteinNetwork <- createCorrelationNetwork( analysisData = analysisData, assayName = "protein", groupColumn = "clinicalGroup", groupLevel = "Recovered", correlationMethod = "pearson", minimumAbsoluteCorrelation = 0, adjustedPValueThreshold = 1, pAdjustMethod = "fdr", storeResult = FALSE ) head(recoveredProteinNetwork) ``` # Prior-guided sparse differential correlation `testDifferentialCorrelation()` can test only feature pairs supported by a prior network when `candidateEdgeTable` is supplied. This is the sparse mode for differential correlation: it avoids exhaustive cross-omic testing and keeps the dynamic network aligned to interpretable prior knowledge. ```{r prior-guided-differential} priorGuidedDifferentialResults <- testDifferentialCorrelation( analysisData = analysisData, candidateEdgeTable = measuredKnowledgeNetwork, groupColumn = "clinicalGroup", groupLevels = c("PASC", "Recovered"), minimumAbsoluteCorrelation = 0, adjustedPValueThreshold = 1, pAdjustMethod = "fdr", storeResult = FALSE ) priorGuidedDifferentialNetwork <- createDifferentialCorrelationNetwork( differentialCorrelationTable = priorGuidedDifferentialResults, minimumAbsoluteCorrelation = 0 ) c(nrow(priorGuidedDifferentialResults), nrow(priorGuidedDifferentialNetwork)) ``` # Permutation validation `permuteRewiringScores()` permutes group labels, recomputes the differential-correlation statistics over a fixed edge universe, and compares each node's observed rewiring score with the permutation distribution. When both `candidateEdgeTable` and `assayName` are omitted, that universe is built from dense within-omic pairs in every assay; cross-omic pairs are not generated. The fixed edge set is attempted in every permutation. An edge can still be unscored if a permuted group has insufficient pairwise-complete data or a constant feature, and `contributingPermutations` records the resulting loss. Supplying `candidateEdgeTable` with all observed-data filters disabled, as below, lets CorNetto fix the candidate universe before inspecting the group labels. The result is marked as a randomization p-value only when every edge and the selected node score are estimable under the permitted allocations. The user is still responsible for prespecifying the candidates and for the exchangeability assumption. ```{r permutation-validation} backend <- BiocParallel::SnowParam(workers = 2, type = "SOCK") rewiringValidation <- permuteRewiringScores( analysisData = analysisData, candidateEdgeTable = measuredKnowledgeNetwork, groupColumn = "clinicalGroup", groupLevels = c("PASC", "Recovered"), minimumAbsoluteCorrelation = 0, adjustedPValueThreshold = NULL, pAdjustMethod = "fdr", nPermutations = 99, seed = 1, # Remove the three lines below to run serially: verbose = TRUE, progressEvery = 50, BPPARAM = backend ) head(rewiringValidation$rewiringTable) rewiringValidation$inferenceStatus ``` For a fully scored node, the smallest tail probability is `1 / (nPermutations + 1)`, so 99 permutations bottom out at 0.01. `contributingPermutations` records how many permutations produced a score for each node. Treat the values as conditional rankings whenever `inferenceStatus` says so. The call above uses a `r BiocStyle::Biocpkg("BiocParallel")` socket backend, which is the portable choice on Windows. Keep examples and automated checks at two workers or fewer. `verbose` and `progressEvery` control progress reporting; `BPPARAM` selects the backend. A fixed `seed` gives the same label allocations whatever the worker count, so serial and parallel runs agree numerically. # Differential correlation and rewiring `testDifferentialCorrelation()` compares correlation coefficients between two groups. `createDifferentialCorrelationNetwork()` converts the test results to a weighted network, and `calculateRewiringScores()` summarizes node-level rewiring. When `assayName` is omitted, every assay is tested, but pairs are generated only within each assay: protein-protein and transcript-transcript pairs are included, for example, whereas protein-transcript pairs are not. ```{r differential-correlation} differentialResults <- testDifferentialCorrelation( analysisData = analysisData, groupColumn = "clinicalGroup", groupLevels = c("PASC", "Recovered"), correlationMethod = "pearson", minimumAbsoluteCorrelation = 0, adjustedPValueThreshold = 1, pAdjustMethod = "fdr", storeResult = FALSE ) differentialNetwork <- createDifferentialCorrelationNetwork( differentialCorrelationTable = differentialResults, minimumAbsoluteCorrelation = 0 ) rewiringScores <- calculateRewiringScores( differentialCorrelationNetwork = differentialNetwork, storeResult = FALSE ) head(rewiringScores) ``` The three scores are descriptive, not tests. `rawRewiringScore` is the L2 norm of a node's incident z-scores and grows with degree. `rootMeanSquareRewiringScore` divides that by `sqrt(degree)` and is the one to compare across nodes of different degree. `degreeMatchedZScore` standardizes within bins of `log2(degree + 1)`, so it is a within-bin ranking aid rather than a z-score against a null model; it is `NA` for bins holding fewer than five nodes, since a smaller bin cannot produce a meaningful spread. `permuteRewiringScores()` adds a permutation reference; its `inferenceStatus` states whether the result meets the computational conditions for randomization inference. Features are mutually tested only against features in the same assay. In this example, each protein and transcript node therefore has degree four, while each metabolite node has degree three. On a real network with a spread of degrees, the bins do the work they are named for. # Integrated and focused network `combineNetworks()` merges static prior knowledge, group-specific correlation layers, and differential-correlation layers. `createFocusedNetwork()` extracts a seed-centered neighborhood, which is useful for pathway-driven analyses. ```{r integrated-network} integratedNetwork <- combineNetworks( knowledgeNetwork = knowledgeNetwork, correlationNetwork = recoveredProteinNetwork, differentialCorrelationNetwork = list( priorGuidedDifferentialNetwork, differentialNetwork ), includeReverseEdges = TRUE ) focusedNetwork <- createFocusedNetwork( networkEdgeTable = integratedNetwork, seedNodes = seedNodes, neighborhoodOrder = 1 ) cytoscapeTables <- prepareCytoscapeTables( networkEdgeTable = focusedNetwork, rewiringTable = rewiringScores ) names(cytoscapeTables) nrow(cytoscapeTables$nodes) nrow(cytoscapeTables$edges) ``` # Graph creation `createNetworkGraph()` converts a standardized CorNetto edge table to an `r BiocStyle::CRANpkg("igraph")` object. Plotting can then use standard igraph methods. A node is one feature in one assay, drawn with an assay-specific colour and shape. An edge is a connection between two features. Node colour could equally encode a rewiring score, a permutation tail probability, or any other node-level column. ```{r graph-plot, fig.width = 8, fig.height = 6} displayGraph <- igraph::simplify( igraph::as_undirected(createNetworkGraph(focusedNetwork), mode = "collapse") ) assayStyle <- data.frame( assay = c("protein", "transcript", "metabolite"), colour = c("#2B8CBE", "#31A354", "#E6842A"), shape = c("circle", "square", "csquare") ) idx <- match(igraph::V(displayGraph)$assayName, assayStyle$assay) set.seed(1) plot( displayGraph, layout = igraph::layout_with_fr(displayGraph), vertex.color = assayStyle$colour[idx], vertex.shape = assayStyle$shape[idx], vertex.label = igraph::V(displayGraph)$nodeName, vertex.label.cex = 0.7, vertex.label.color = "black", vertex.label.family = "sans", vertex.frame.color = "white", vertex.size = 20, edge.color = "grey65", main = "Focused CorNetto network around the seed nodes" ) legend( "bottomleft", legend = assayStyle$assay, pt.bg = assayStyle$colour, pch = c(21, 22, 23), pt.cex = 1.6, bty = "n", title = "Assay" ) ``` ```{r graph-counts} c(nodes = igraph::gorder(displayGraph), connections = igraph::gsize(displayGraph)) ``` The `r nrow(cytoscapeTables$edges)` exported edge rows reduce to `r igraph::gsize(displayGraph)` drawn connections for two reasons, and no edge is filtered out on the way. First, `combineNetworks(includeReverseEdges = TRUE)` stores each undirected edge in both directions, which accounts for most of the difference. Reverse copies are added for undirected edges only. This is a workaround rather than a claim about biology: several Cytoscape algorithms require an explicit direction to traverse an edge, while the edges themselves are not inherently directed, as with a correlation or a protein-protein interaction. Storing both orientations lets those algorithms move either way across such an edge. Genuinely directed edges are left untouched. Second, one feature pair can carry several rows when more than one layer supports it, since prior knowledge, a group-specific correlation and a differential-correlation result are separate rows describing the same pair. The display graph shows one line per connected pair; the exported tables keep every row. # Export Use `writeNetworkTables()` when [Cytoscape](https://cytoscape.org)-ready files are needed. The example writes to a temporary directory. ```{r export} writtenFiles <- writeNetworkTables( networkTables = cytoscapeTables, directoryPath = tempdir(), prefix = "cornettoExample", fileFormat = "tsv" ) basename(writtenFiles) ``` # Citation ```{r citation} citation("CorNetto") ``` # Session information ```{r session-info} sessionInfo() ```