Prepare DESeq2, limma or edgeR results for lisaR

Start with the complete result table from a differential-expression model that you have already fitted. This guide shows two routes:

The adapter is optional. It does not fit a model, choose a comparison or prove which software produced a table.

The worked tables below are small fictional examples. The gene names make the code easy to follow, but the numbers are not measurements. Use your complete tested table for a real study.

Choose the direct or adapted route

Your input needs one row per gene and these five values:

Value What to supply
Gene identifier A non-empty identifier that matches the TERM2GENE membership resource.
Effect The model’s effect for the chosen comparison, such as log2FoldChange or logFC.
Signed ranking statistic A statistic for the same comparison, ordered towards the declared positive direction.
P-value The unadjusted p-value from the fitted model.
Adjusted p-value The model’s adjusted p-value for the complete tested population.

If the exported table already contains these values, save it as TSV and point symbol_col, logfc_col, rank_col, pvalue_col and padj_col to the existing names in the study file. A DESeq2 Wald stat or a limma moderated t can be used directly. You do not need to create rank_value.

Use the adapter when standard names or its audit will make the hand-off clearer. It returns symbol, log2FoldChange, rank_value, pvalue and padj, ready to be named in a study file. The adapter works on an exported data frame without loading DESeq2, limma or edgeR.

State the comparison and positive direction

Before saving either form, write down the model comparison and what a positive effect means. The signed statistic must describe the same comparison. Keep the producing software, model specification and original export with the prepared file.

Input Suitable starting point What not to infer
DESeq2 Wald result Signed stat for the stated coefficient/contrast A DESeq2 likelihood-ratio stat need not be signed
limma one-contrast result Signed moderated t A multi-coefficient F statistic does not have one direction
edgeR one-contrast result An explicitly declared signed ranking derived for that contrast The adapter cannot infer direction from unsigned F/LR
Any result Complete tested table, with original adjusted p-values A significant-only table is not the GSEA universe

Use the complete tested table rather than filtering to padj < 0.05 before GSEA. Non-significant genes remain part of the ordered background against which a set is evaluated. Retain the adjusted p-values calculated for the original tested population.

Adapt a DESeq2-style Wald table

This fictional result has six tested genes. One has no statistic and another has a finite ranking but no adjusted p-value, illustrating why absence of an adjusted p-value should not silently remove a row from the saved table.

library(lisaR)

wald <- data.frame(
  gene = c("CXCL9", "GZMB", "MKI67", "TCF7", "IL7R", "CCR7"),
  log2FoldChange = c(1.4, 0.8, -0.7, 0.15, -0.2, NA_real_),
  stat = c(4.2, 2.1, -2.8, 0.4, -0.6, NA_real_),
  pvalue = c(0.00003, 0.036, 0.005, 0.69, 0.55, NA_real_)
)
wald$padj <- p.adjust(wald$pvalue, method = "BH")
wald$padj[5L] <- NA_real_  # Illustrative missing adjusted p-value.

prepared <- prepare_lisa_de_input(
  wald,
  method = "DESeq2",
  columns = list(symbol = "gene"),
  positive_direction = "treated relative to control",
  matrix_scale = "transformed"
)
prepared$data
##   symbol log2FoldChange rank_value  pvalue    padj
## 1  CXCL9           1.40        4.2 0.00003 0.00015
## 2   GZMB           0.80        2.1 0.03600 0.06000
## 3  MKI67          -0.70       -2.8 0.00500 0.01250
## 4   TCF7           0.15        0.4 0.69000 0.69000
## 5   IL7R          -0.20       -0.6 0.55000      NA
## 6   CCR7             NA         NA      NA      NA
stopifnot(
  nrow(prepared$data) == nrow(wald),
  identical(prepared$data$rank_value, wald$stat),
  is.na(prepared$data$padj[5L]),
  prepared$row_audit$rank_eligible[5L],
  !prepared$row_audit$rank_eligible[6L]
)

The output columns are symbol, log2FoldChange, rank_value, pvalue and padj. No threshold has been applied. The missing statistic remains missing; it is not replaced by zero. rank_eligible records a finite ranking value, not biological validity or every downstream eligibility condition.

Inspect what the adapter actually declared:

prepared$column_mapping
##   canonical         source       declaration
## 1    symbol           gene          explicit
## 2     logfc log2FoldChange method_convention
## 3      rank           stat method_convention
## 4    pvalue         pvalue method_convention
## 5      padj           padj method_convention
prepared$row_audit[c(
  "source_row", "source_id", "retained", "rank_eligible",
  "missing_rank", "missing_padj"
)]
##   source_row source_id retained rank_eligible missing_rank missing_padj
## 1          1     CXCL9     TRUE          TRUE        FALSE        FALSE
## 2          2      GZMB     TRUE          TRUE        FALSE        FALSE
## 3          3     MKI67     TRUE          TRUE        FALSE        FALSE
## 4          4      TCF7     TRUE          TRUE        FALSE        FALSE
## 5          5      IL7R     TRUE          TRUE        FALSE         TRUE
## 6          6      CCR7     TRUE         FALSE         TRUE         TRUE
prepared$receipt[c(
  "declared_method", "positive_direction", "rank_source",
  "significance_filter", "producing_software_verified"
)]
##   declared_method          positive_direction rank_source significance_filter
## 1          DESeq2 treated relative to control        stat                none
##   producing_software_verified
## 1                       FALSE

producing_software_verified is false because a data frame cannot establish which software fitted its model. Retain the original table and model records; the adapter’s receipt is an input declaration, not independent provenance.

For real DESeq2 results, export the complete result for the intended contrast, then convert it as above. Do not combine a statistic for one coefficient with an effect from another. For likelihood-ratio results, choose and justify a compatible signed statistic separately rather than relabelling an unsigned statistic.

Adapt a limma result with its moderated t statistic

For one specified limma coefficient or contrast, the conventional effect and p-value columns are recognised. The gene column is still declared explicitly:

limma_table <- data.frame(
  gene = c("CXCL9", "GZMB", "MKI67", "TCF7"),
  logFC = c(1.3, 0.7, -0.6, 0.1),
  t = c(4.8, 2.5, -3.2, 0.3),
  P.Value = c(0.0001, 0.018, 0.004, 0.77)
)
limma_table$adj.P.Val <- p.adjust(limma_table$P.Value, "BH")
limma_input <- prepare_lisa_de_input(
  limma_table, method = "limma",
  columns = list(symbol = "gene"),
  positive_direction = "treated relative to control"
)
stopifnot(
  identical(limma_input$data$rank_value, limma_table$t),
  nrow(limma_input$data) == nrow(limma_table)
)
limma_input$data
##   symbol log2FoldChange rank_value pvalue   padj
## 1  CXCL9            1.3        4.8 0.0001 0.0004
## 2   GZMB            0.7        2.5 0.0180 0.0240
## 3  MKI67           -0.6       -3.2 0.0040 0.0080
## 4   TCF7            0.1        0.3 0.7700 0.7700

When exporting a real topTable(), request all tested rows rather than its default small top table, and use the intended single contrast. If biological identifiers are in non-default row names, declare columns = list(symbol = ".rownames"). Default row numbers are not gene IDs.

Declare a signed edgeR ranking

An edgeR F or likelihood-ratio statistic is unsigned. For a single one-degree-of-freedom contrast, one possible ranking convention is sign(logFC) * sqrt(F), using the effect for that same contrast. This is a declared ranking transformation, not a new p-value or a claim that it is a limma moderated t statistic. The design and test metadata must establish the one-contrast restriction; the numbers in an exported table cannot prove it.

The following fictional example explicitly assumes that restriction:

edger_table <- data.frame(
  symbol = c("CXCL9", "GZMB", "MKI67", "TCF7"),
  logFC = c(1.2, 0.6, -0.5, 0.1),
  F = c(16, 6.25, 9, 0.09),
  PValue = c(0.0008, 0.02, 0.006, 0.77)
)
edger_table$FDR <- p.adjust(edger_table$PValue, "BH")
edger_table$signed_sqrt_F <-
  sign(edger_table$logFC) * sqrt(edger_table$F)

edger_input <- prepare_lisa_de_input(
  edger_table, method = "edgeR",
  columns = list(rank = "signed_sqrt_F"),
  positive_direction = "treated relative to control"
)
stopifnot(
  identical(edger_input$data$rank_value, edger_table$signed_sqrt_F),
  identical(edger_input$data$padj, edger_table$FDR)
)
edger_input$data
##   symbol log2FoldChange rank_value pvalue       padj
## 1  CXCL9            1.2        4.0 0.0008 0.00320000
## 2   GZMB            0.6        2.5 0.0200 0.02666667
## 3  MKI67           -0.5       -3.0 0.0060 0.01200000
## 4   TCF7            0.1        0.3 0.7700 0.77000000

For a multi-degree-of-freedom F or LR test, do not attach the sign of an arbitrarily chosen logFC. Return to the fitted model and define a meaningful one-dimensional contrast, or choose a method appropriate to the original multidimensional question. Renaming F to signed_rank does not make it signed.

The adapter requires the rank mapping for edgeR and rejects conventional unsigned F/LR columns and p-value/FDR columns as rankings. It also checks sign agreement where both the rank and effect are finite and nonzero. Those checks detect some mistakes; they cannot establish that a declared transformation was scientifically justified.

Resolve orientation, missing values and duplicates

Orientation

positive_direction describes the effect in words. With the default rank_direction = "same_as_effect", the ranking must follow it too. If the ranking really encodes the reverse comparison while the effect is correct, rank_direction = "opposite_to_effect" negates the rank once and records the choice. It does not reverse the effect, rename a contrast or repair mixed model coefficients.

Missing values

The default na_policy = "retain" saves missing statistics and records their status. na_policy = "error" instead rejects them. Missing/empty identifiers, infinite values and malformed numbers always fail. Keep model-specific reasons such as filtering, non-convergence or independent filtering beside the source table; the adapter does not reconstruct them.

Duplicate identifiers

The default is an error, including case-insensitive collisions. Investigate whether two rows are transcripts, aliases, repeated exports or distinct features mapped to the same gene. Do not average their logFC, statistics or p-values.

An explicit row-selection policy is available when it is justified. This toy example demonstrates its audit, not a recommended biological rule:

duplicated_source <- rbind(wald, wald[1L, , drop = FALSE])
selected <- prepare_lisa_de_input(
  duplicated_source, "DESeq2",
  columns = list(symbol = "gene"),
  positive_direction = "treated relative to control",
  duplicate_policy = list(
    type = "select", criterion = "max_abs_rank", tie_breaker = "row_number"
  )
)
stopifnot(
  nrow(selected$full_data) == 7L,
  nrow(selected$data) == 6L,
  sum(!selected$row_audit$retained) == 1L
)
selected$row_audit[c("source_row", "source_id", "retained")]
##   source_row source_id retained
## 1          1     CXCL9     TRUE
## 2          2      GZMB     TRUE
## 3          3     MKI67     TRUE
## 4          4      TCF7     TRUE
## 5          5      IL7R     TRUE
## 6          6      CCR7     TRUE
## 7          7     CXCL9    FALSE

The repeated row is preserved in full_data and its disposition is recorded. On real non-identical duplicates, selecting the largest absolute statistic can favour stronger effects. Choose the policy before interpretation and retain the mapping/selection evidence. See the input contract in Configuration reference for row-level mapping policies.

Expression matrices are a separate input

The adapter only records matrix_scale; it neither reads nor transforms a matrix, and that argument is not a YAML configuration field. Use matrix_scale = "transformed" in the adapter receipt for values already transformed, such as VST, rlog or documented log2 expression. Record "count_like" only for count-like values. This declaration does not itself configure downstream heatmaps. Their matrix-path convention is separate; see the configuration reference before attaching a matrix. Do not describe VST values as raw counts or apply a second log transform by accident.

Match matrix feature IDs to DE IDs, review sample order/annotation, and keep the transformation and filtering provenance. An effect/ranking table cannot reconstruct sample-level expression. The standard gene-evidence view can show DE values without an expression matrix; expression heatmaps need that separate input.

Save the adapted table and its audit

Write the prepared table and its audit into a new directory:

input_dir <- "prepared-de-inputs"
stopifnot(!file.exists(input_dir))
dir.create(input_dir)

for (part in c(
  "data", "full_data", "receipt", "column_mapping",
  "row_audit", "duplicate_resolution"
)) {
  write.table(
    prepared[[part]], file.path(input_dir, paste0(part, ".tsv")),
    sep = "\t", quote = FALSE, row.names = FALSE, na = "NA"
  )
}

Retain the original source table and model record too: extra columns such as model diagnostics are not copied into the canonical data.

The new folder contains the canonical TSV used by lisaR and the audit files that explain how it was made. Keep the original export and model record beside this folder.

Point the analysis configuration at data.tsv with this column mapping. This is a fragment to place in the complete study file created in the next guide:

single_de:
  - analysis_id: treated_vs_control
    label: Treated versus control
    comparison: Treated minus control
    positive_direction: Higher in treated than control
    de_path: prepared-de-inputs/data.tsv
    species: Homo sapiens
    symbol_col: symbol
    logfc_col: log2FoldChange
    rank_col: rank_value
    pvalue_col: pvalue
    padj_col: padj

For the adapted route, the rank column is now rank_value, not the original stat or t. For the direct route, keep the original column name in rank_col. Relative paths are interpreted from the folder containing the configuration file.

The six-row example is too small for a real enrichment analysis. Apply the chosen route to your complete tested results, then follow Getting started to validate resources, plan, run and verify the study.

For supported DESeq2/edgeR objects already in memory, the alternative local API is documented in Function reference. It is separate from this table-first workflow; no object file needs to be deserialised to follow this page.