Why Design a Pipeline as Stages?
A single-cell analysis may begin with a count matrix and end with tables, plots, and a written report. Between those endpoints are multiple operations: summarizing cells, transforming counts, selecting features, producing an embedding, and assembling results for readers. If these operations are tangled together, a small revision can be hard to make safely. A changed filtering rule, for example, may accidentally alter how a plot is labeled or which metadata rows are paired with the expression values.
A modular pipeline divides the workflow into stages that each perform a defined task. A stage receives specified inputs, returns specified outputs, and states the assumptions it makes about them. This makes it possible to examine or test one task without needing to rerun or understand every other task. The goal is not to split every line of code into a separate function. It is to create boundaries around meaningful transformations and make the handoffs between them unambiguous.
The introductory material on computational and statistical reproducibility emphasizes that a reproducible result depends on more than a successful run. Modular design addresses one part of that problem: it makes the path from data to results easier to inspect, test, and revise. It does not, by itself, establish which input file was used, preserve the software environment, or guarantee that a stochastic method returns the same result on every run.
Specify Inputs, Outputs, and Assumptions
For a simple workflow, write \(X\) for the raw count matrix, with \(n\) cells in rows and \(G\) genes in columns. Its entry \(X_{ij}\) is the observed count for cell \(i\) and gene \(j\). Write \(M\) for cell-level metadata, with one row per cell and at least a cell identifier. A pipeline stage may also receive named analysis parameters, but its required data and parameter inputs should be clear.
An interface is the contract between a stage and the rest of the workflow. At minimum, describe the input object, its structure, the output object, and any assumptions that must hold. For example, a stage that computes library sizes might require a nonnegative count matrix and return one total per cell, in the same cell order. A downstream stage should not have to guess whether the output is a vector, a table, or a matrix, or whether its rows correspond to the original cell order.
In a count-matrix workflow, useful interface assumptions include the orientation of the matrix, the meaning of its entries, whether row and column names identify cells and genes, and how metadata rows align to cells. If a stage receives normalized expression rather than observed counts, that distinction must also be explicit. A label such as “expression matrix” is often too vague to serve as an interface specification.
One possible sequence is shown below. The labels describe responsibilities, not a mandate to use a particular normalization or clustering method. The analytical choices belong to the relevant methods; here, the focus is on how to organize and test the handoffs.
| Stage | Inputs | Outputs | Interface question |
|---|---|---|---|
| Read and represent data | Count data and cell metadata | \(X\), \(M\) | Are rows cells, columns genes, and identifiers aligned? |
| Summarize cells | \(X\) | Per-cell summary table | Does each summary row identify the corresponding cell? |
| Transform counts | \(X\), stated transformation choices | Transformed matrix \(Z\) | Does \(Z\) retain the same cell and gene indexing? |
| Analyze transformed data | \(Z\), stated analysis choices | Features, coordinates, or group labels | Can each output row be linked to a cell or gene? |
| Assemble report | Summaries, analysis outputs, labels | Tables and figures | Do displayed labels match the data being shown? |
Worked Examples of Modular Design
Worked Example: Defining a Library-Size Stage
Suppose a hypothetical raw count matrix has three cells and two genes:
Rows are cells \(c_1,c_2,c_3\), and columns are genes \(g_1,g_2\). Define the library size \(L_i\) of cell \(i\) as the sum of its observed gene counts:
The stage receives \(X\) and returns a table with one row per cell and columns for cell identifier and library size. Its assumptions are that rows really are cells, columns really are genes, and matrix entries are nonnegative counts. The calculations are \(L_1=4+6=10\), \(L_2=0+5=5\), and \(L_3=3+1=4\). Thus the output library-size column, in cell order, is \((10,5,4)\) counts per cell.
A focused unit test can use this small matrix and check that the returned values are exactly \(10,5,4\), that there are three output rows, and that their cell identifiers remain \(c_1,c_2,c_3\) in order. These checks target this stage’s contract; they do not claim that any downstream biological interpretation is correct.
Worked Example: Testing a Normalization Stage
Use the same hypothetical matrix and define a simple counts-per-million transformation for a cell with positive library size:
The normalization stage receives \(X\), calculates or receives \(L_i\), and returns \(Z\) with the same cell and gene indexing. For cell \(c_1\), the library size is 10, so the two transformed entries are \((4/10)\times1{,}000{,}000=400{,}000\) and \((6/10)\times1{,}000{,}000=600{,}000\). For cell \(c_2\), they are \((0/5)\times1{,}000{,}000=0\) and \((5/5)\times1{,}000{,}000=1{,}000{,}000\). For cell \(c_3\), they are \((3/4)\times1{,}000{,}000=750{,}000\) and \((1/4)\times1{,}000{,}000=250{,}000\).
For each cell with positive library size, its transformed entries sum to \(1{,}000{,}000\). For example, cell \(c_3\) has \(750{,}000+250{,}000=1{,}000{,}000\). A unit test can check that property, the output dimensions, and the preserved labels. The contract must also address a zero-library cell: the displayed formula would divide by zero. This example’s matrix has no such cell, but a general implementation should state how that case is handled, such as returning a flagged missing result or reporting that the input does not meet the stage’s requirements. It should not silently produce an infinite value.
Worked Example: Revising a Report Without Rewriting Analysis
Suppose a later stage returns two cell coordinates and group labels for the same three cells: \(c_1\) has coordinates \((1,0)\) and label A, \(c_2\) has coordinates \((0,2)\) and label B, and \(c_3\) has coordinates \((2,1)\) and label A. A report stage receives this table and a plotting choice, such as whether to display a legend. Its responsibility is to produce a plot and caption from the supplied values and labels; it should not silently recalculate coordinates or reorder labels.
A small integration test can connect the normalization output and a deliberately simple downstream result table, then verify that the report-stage input contains three cells and pairs each coordinate with the intended label. If the legend wording is revised, only the report stage and its test need to change. If the cell-to-label pairing is wrong, the test reveals an interface or assembly problem before a full report is reviewed.
The test does not establish that the coordinates are scientifically adequate or that the labels are biologically meaningful. It checks a narrower claim: the report displays the outputs it was given with the intended cell-label correspondence.
Why the Boundaries Help: A Short Justification
A staged workflow can be viewed as a sequence of functions. Let \(f_1\) prepare the data, \(f_2\) summarize it, \(f_3\) transform it, and \(f_4\) assemble a report. The full computation is the composition \(f_4(f_3(f_2(f_1(X,M))))\), with any required parameters included as explicit inputs. If each function’s output satisfies the next function’s input contract, then the complete computation is well-defined: the output type and meaning required by \(f_{k+1}\) are supplied by \(f_k\).
This gives a practical reason to define interfaces before connecting stages. If a composition fails, the mismatch must occur at a handoff or inside a stage. A test of each stage on a small input can distinguish those possibilities more quickly than inspecting an opaque, end-to-end script. The argument is about localizing software errors; it does not prove the statistical validity of the methods implemented by the stages.
Testing, Diagnostics, and Alternatives
A useful test plan has two complementary levels. A unit test checks one stage against a small, known input and an expected output. An integration test checks that connected stages exchange compatible objects and preserve intended identities, such as cell and gene labels. Tiny fixtures make expected results easy to calculate by hand; they are not substitutes for biological validation or for testing performance on realistic data.
For a count-matrix workflow, stage tests can check such properties as dimensions, nonnegative counts at the input boundary, preservation of identifiers, expected totals, and the handling of a specified edge case. A transformation test can check a known cell’s numerical result. An integration test can confirm that a table of analysis coordinates and a table of cell metadata join to the same cells without duplication or loss. Tests should reflect the stage’s stated contract rather than assume an implementation detail that users do not depend on.
Common warning signs include a stage that reads files, transforms counts, performs an analysis, and writes final figures all at once; unnamed objects whose meaning changes during execution; and downstream code that relies on an undocumented row order. If an error appears only at the end, inspect the intermediate outputs and the interface assumptions at each handoff. A dimension mismatch, missing identifier, or unexpected data type often points to a boundary problem. A plausible-looking plot does not rule out a cell-label mismatch.
A monolithic script can be reasonable for a very small exploratory task, especially when the operations are simple and unlikely to be reused. But as analyses grow or are revised, its shared state and implicit assumptions make isolated testing difficult. At the other extreme, creating many tiny stages adds overhead and can obscure the scientific sequence. Prefer boundaries that correspond to meaningful responsibilities, reused transformations, or places where inputs and outputs need independent checks.
Modularity has limits. A perfectly tested interface can consistently pass the wrong biological quantity, and a correct transformation can still be applied under unsuitable scientific assumptions. A stage may also depend on choices not represented in its interface, making its behavior hard to understand. State the scientific meaning of its inputs and outputs alongside their computational structure, and keep claims from tests appropriately narrow. Modularity supports revision and diagnosis; it is not a replacement for methodological judgment.
Common Mistake
Check Your Understanding
Answer each question, then compare your reasoning with the solutions.
- A matrix has 80 cell rows and 12,000 gene columns. What should a library-size stage return, and what indexing property should it preserve?
- A transformation is tested only on matrices with positive row sums. What important case has not been addressed?
- A report plot shows the right number of points but some cells have the wrong group colors. Which kind of check would most directly help detect this?
- Why can unit tests of individual stages not, by themselves, establish that the complete workflow is correctly connected?
- A developer splits a one-line label-formatting operation into a separate stage, but it has no independent interface or reuse value. What design concern does this illustrate?
Solutions. (1) It should return one library-size value per cell, with an explicit cell identifier or a guaranteed matching cell order; it should preserve the correspondence to the 80 rows. (2) The zero-library case remains unspecified because the normalization denominator would be zero. The stage contract should define its handling. (3) An integration test should verify the pairing of each cell identifier with its group label before plotting, rather than checking only the number of points. (4) Individual tests can pass even if one stage’s output is connected to the wrong input, or if identifiers are misaligned at a handoff; integration tests address those connections. (5) This illustrates over-fragmentation: a boundary is useful when it clarifies a meaningful responsibility, supports independent testing or reuse, or makes an important handoff explicit, not merely because code can be separated.