Normalised data and fold change · Choose a method

Statistics on fold change and normalised data (% of control, ratio to control)

Analyse fold changes on the log scale, paired by experiment when each blot or plate has its own control, instead of setting controls to 1 and testing against them. Worked Western blot example in R and Python.

By Licklider · Published · Last reviewed · How this page was made · Markdown

On this page
  1. Decide in three questions
  2. What goes wrong when the controls are set to 1
  3. Why the log scale
  4. Worked example: a Western blot with four blots
  5. Showing % of control in a figure
  6. Several treatments against one control
  7. Before you normalise: the loading control and the ratio
  8. Check what an AI assistant ran
  9. Report it
  10. References

Quick answer. When each experiment (blot, plate or culture day) has its own control, compute one fold change per experiment, take its logarithm, and test the log fold changes against 0 — this is the same as a paired t-test on log values. Report the back-transformed result as a fold change with its 95% confidence interval. When treated and control samples are independent, use Welch’s t-test on log values. Both routes need every normalised value to be positive and the treatment to scale the signal; if background-subtracted values are zero or negative, fix the quantification first. Use the paired log test instead of setting every control to exactly 1 (or 100%) and running a t-test against those 1s, which treats the control as known without error, and in our simulation it gave 7–11% false positives with three to six experiments (9% with four), and more with more experiments, against about 5% for the paired log test.

Terms used below. Fold change is treated ÷ control; 1 means no change. Normalised means divided by a reference (a loading control, a housekeeping gene, or the control condition). The log fold change (here log₂) turns ratios into differences: a doubling is +1, a halving is −1. The geometric mean is the back-transformed mean of logs. A false positive is a significant result when there is no true effect. An experiment here is one independent run with its own control (one blot, one plate, one culture day). Paired means two values from the same experiment analysed as one difference. A 95% confidence interval (CI) is the range of fold changes compatible with the data; back-transformed means converted from the log scale to a fold change (2 to the power of the log₂ value). In t(3), 3 is the degrees of freedom (number of experiments − 1). Two-sided means the test looks for a change in either direction. Holm’s method adjusts p-values when you make several comparisons; a many-to-one comparison tests each treatment against one control.

Decide in three questions

  1. Does each experiment contain its own control? If every blot has a control lane and a treated lane, the two values from one blot belong together: analyse one log fold change per blot (paired). If treated and control samples are separate animals or cultures with no shared run, analyse the log values of the two groups with Welch’s t-test.
  2. What is one observation? One experiment or one animal, not one lane, well or technical replicate. Average technical replicates within an experiment first (what counts as n).
  3. One treatment or several? One treatment against control: the paired test on logs. Several treatments against the same control: one paired log test per treatment with Holm’s adjustment, decided before you see the data (which correction to use).

Do this: write down the pairing and the comparisons before quantifying the bands.

What goes wrong when the controls are set to 1

The habit: divide each blot’s treated value by its own control, set every control to 1 (or 100%), and compare the treated values with the column of 1s in a t-test. The 1s carry no variance, so the test treats the control as measured without error and counts it as extra data. In SciPy the Student t-test even warns about “precision loss … when the data are nearly identical”, because the control column is constant.

We simulated 20,000 experiments per row with no true treatment effect, a large blot-to-blot difference (SD 0.5 on the natural-log scale, about 0.72 on the log₂ scale) and lane noise as shown, so every p < 0.05 is a false positive. Code and output: simulation.py and simulation_output.json (seed 20261110).

Experiments (blots) Lane noise (log SD) Controls set to 1, Student t-test One-sample t-test of raw ratios vs 1 Paired t-test on logs Welch t-test on logs, pairing ignored
3 0.5 11.4% 5.6% 5.1% 1.5%
4 0.2 9.1% 5.2% 5.1% 0.2%
4 0.5 9.0% 5.3% 5.1% 1.4%
6 0.5 7.1% 4.3% 4.8% 1.2%
20 0.2 7.7% 6.8% 5.2% 0.0%
20 0.5 15.4% 13.2% 5.0% 0.8%
40 0.2 11.5% 10.8% 5.1% 0.0%
40 0.5 38.5% 36.8% 4.7% 0.6%

Three things follow:

  • Both ratio-scale tests drift further from 5% as experiments are added. Raw ratios are biased upward (next section), and more experiments only make the test more certain of that bias: with 40 experiments and the noisier lanes, the controls-set-to-1 test gave 38.5% false positives and the raw-ratio test 36.8%. With 3–6 experiments the controls-set-to-1 test is already at 7–11%.
  • The one-sample test of raw ratios against 1 stays near 5% only with a handful of experiments, and only because each ratio uses its own same-blot control; it averages ratios on a skewed scale, so its mean fold change and its interval are distorted. When fold changes are computed against a control-group mean instead of a same-run control, the raw-ratio test fails even with few samples (qPCR ΔΔCt statistics shows the case).
  • Ignoring the pairing is safe but wastes the experiment: with four blots and a true 1.5-fold increase (lane noise 0.2), the paired test on logs detected it in 50% of simulated studies and the unpaired Welch test in 7%, because blot-to-blot differences swamp the effect.

Do this: test the log fold changes, one per experiment, against 0 — the paired log test stayed at 4.7–5.2% in every row.

Why the log scale

Ratios are lopsided. A doubling is 2 and a halving is 0.5: the same size of effect sits 1 unit above 1 and 0.5 below it. Average a doubling and a halving and you get 1.25, an “increase” from two effects that cancel. On the log₂ scale they are +1 and −1 and average to 0. With no true effect and our noisier setting, the arithmetic mean of simulated ratios was 1.29 while their median was 1.00.

GraphPad’s documentation for its ratio t-test makes the same point: “ratios are intrinsically asymmetric”, so “it makes more sense to look at the logarithm of ratios”; the test “averages the logarithm of the ratio of treated/control” and reports “the 95% confidence interval of the geometric mean of the ratios” (GraphPad Prism 11 Statistics Guide). Keene (1995) argues that log-transformed analyses “should frequently be preferred” for positive measurements and that the choice belongs in the protocol, not after looking at the data.

Two practical conditions: every value must be positive (a log of 0 or a negative value does not exist), and the effect should be multiplicative — the treatment scales the signal rather than adding a fixed amount. Densitometry, expression and concentration data can behave this way — check that a doubling looks like a doubling on your scale; if some background-subtracted values are zero or negative, fix the quantification before choosing the test.

Do this: check that every value is positive and that the effect scales the signal before logging; use log₂ for fold changes so that +1 reads as “doubled”; state the base in Methods.

Worked example: a Western blot with four blots

Constructed data: four independent blots, each with one control and one treated lane. Each lane gives a target band and a loading-control band; the lane value is target ÷ loading control.

Blot Control ratio Treated ratio Fold change log₂ fold change
1 0.600 0.905 1.51 0.593
2 0.533 0.938 1.76 0.814
3 1.042 1.130 1.09 0.118
4 0.333 0.647 1.94 0.957

The control values differ threefold between blots, which is why the pairing matters.

Analysis Result
Paired t-test on log₂ values (recommended) t(3) = 3.38, p = 0.043; fold change 1.54, 95% CI 1.03 to 2.30
Controls set to 1, Student t-test t(6) = 3.09, p = 0.021 — overstated, as in the simulation
One-sample t-test of raw fold changes vs 1 t(3) = 3.09, p = 0.054; mean fold change 1.57
Welch t-test on log₂ values, blots ignored t(4.39) = 1.65, p = 0.17

In Python (SciPy 1.18.1):

import numpy as np
from scipy import stats

# Target / loading-control ratio per lane, one control and one treated lane per blot
ctrl = np.array([1200, 800, 2500, 600]) / np.array([2000, 1500, 2400, 1800])
trt  = np.array([1900, 1500, 2600, 1100]) / np.array([2100, 1600, 2300, 1700])

log_fc = np.log2(trt / ctrl)                # one log2 fold change per blot
res = stats.ttest_1samp(log_fc, 0.0)        # same p as stats.ttest_rel(np.log2(trt), np.log2(ctrl))
ci = res.confidence_interval(0.95)
print(f"t({res.df:.0f}) = {res.statistic:.2f}, p = {res.pvalue:.3f}")
print(f"fold change {2**log_fc.mean():.2f}, 95% CI {2**ci.low:.2f} to {2**ci.high:.2f}")

In R (4.3.3):

# Target / loading-control ratio per lane, one control and one treated lane per blot
ctrl <- c(1200, 800, 2500, 600) / c(2000, 1500, 2400, 1800)
trt  <- c(1900, 1500, 2600, 1100) / c(2100, 1600, 2300, 1700)

res <- t.test(log2(trt), log2(ctrl), paired = TRUE)   # = t.test(log2(trt / ctrl), mu = 0)
res
2^res$estimate   # geometric mean fold change
2^res$conf.int   # its 95% confidence interval

Both print t = 3.38 with 3 df, p = 0.043, and a fold change of 1.54 (95% CI 1.03 to 2.30).

When treated and control samples are independent animals or cultures with no shared blot, compare the log values with Welch’s t-test and back-transform the difference into a ratio of geometric means. Treating the four blots above as if they were independent:

import numpy as np
from scipy import stats

ctrl = np.array([0.600, 0.533, 1.042, 0.333])    # normalised values, control animals
trt = np.array([0.905, 0.938, 1.130, 0.647])     # normalised values, treated animals
res = stats.ttest_ind(np.log2(trt), np.log2(ctrl), equal_var=False)
ci = res.confidence_interval(0.95)
ratio = 2 ** (np.log2(trt).mean() - np.log2(ctrl).mean())
print(f"t({res.df:.2f}) = {res.statistic:.2f}, p = {res.pvalue:.2f}; ratio {ratio:.2f}, 95% CI {2**ci.low:.2f} to {2**ci.high:.2f}")
# t(4.39) = 1.65, p = 0.17; ratio 1.54, 95% CI 0.76 to 3.10
ctrl <- c(0.600, 0.533, 1.042, 0.333)   # normalised values, control animals
trt  <- c(0.905, 0.938, 1.130, 0.647)   # normalised values, treated animals
res <- t.test(log2(trt), log2(ctrl))    # Welch by default: t(4.39) = 1.65, p = 0.17
2^(mean(log2(trt)) - mean(log2(ctrl)))  # ratio of geometric means 1.54
2^res$conf.int                          # 95% CI 0.76 to 3.10

confidence_interval() is a method of the result object in recent SciPy releases (we used 1.18.1); state your own version. With only three experiments the test still runs, but it detects only large effects (t-test with n = 3).

Do this: report the fold change and its interval from the log analysis, not the arithmetic mean of the ratios.

Showing % of control in a figure

Plot one point per experiment for the treated condition as % of its own control (here 151%, 176%, 109% and 194%), draw the control as a reference line at 100%, and use a log axis so that 50% and 200% sit at equal distances from it. A control bar with no error bar at exactly 100% hides that the control was measured too; the reference line says what was done. Put the geometric mean and its 95% CI from the log analysis on the plot.

Do this: one dot per experiment, log axis, reference line at 100%, interval from the log analysis.

Several treatments against one control

With two or more treatments on every blot, compute one log fold change per treatment per blot and run one paired log test per treatment, then adjust the p-values with Holm’s method (R p.adjust(p, "holm"); Python statsmodels.stats.multitest.multipletests(p, method="holm")). If the treatment groups are independent animals rather than lanes on shared blots, use a many-to-one comparison on the log values: Dunnett’s test assumes equal spreads, so with clearly different spreads use Welch t-tests against control with Holm’s adjustment (which correction to use).

import numpy as np
from scipy import stats
from statsmodels.stats.multitest import multipletests

ctrl_blot = np.array([0.600, 0.533, 1.042, 0.333])          # control lane, blots 1-4
treatments = {"A": np.array([0.905, 0.938, 1.130, 0.647]),   # treated lanes on the same blots
              "B": np.array([0.700, 0.520, 1.300, 0.380])}
p = [stats.ttest_1samp(np.log2(t / ctrl_blot), 0.0).pvalue for t in treatments.values()]
reject, p_holm, _, _ = multipletests(p, method="holm")
for name, raw, adj in zip(treatments, p, p_holm):
    print(name, round(raw, 3), round(adj, 3))   # A 0.043 0.087, then B 0.103 0.103
ctrl_blot <- c(0.600, 0.533, 1.042, 0.333)
treatments <- list(A = c(0.905, 0.938, 1.130, 0.647), B = c(0.700, 0.520, 1.300, 0.380))
p <- sapply(treatments, function(t) t.test(log2(t / ctrl_blot), mu = 0)$p.value)
round(cbind(raw = p, holm = p.adjust(p, "holm")), 3)   # A 0.043 0.087; B 0.103 0.103

In this constructed example treatment A loses significance after adjustment (Holm p = 0.087). Report it that way, with its fold change and interval, rather than dropping treatment B to avoid the adjustment; for how to word it, see what a non-significant p-value means.

Do this: fix the list of treatment-versus-control comparisons before analysis, and adjust for that list.

Before you normalise: the loading control and the ratio

Dividing by a loading control assumes the target signal is proportional to the loading signal — Curran-Everett (2013) notes that a ratio is meaningful “only if the relationship between numerator and denominator is a straight line through the origin”. Check that both bands are in the linear range of detection (a dilution series on the same blot shows it), and follow a validated quantification workflow for the blot itself, such as the one set out by Pillai-Kastoori et al. (2020). For qPCR, where the log scale is built in (Ct values), see qPCR ΔΔCt statistics.

Do this: confirm linearity for target and loading control before you trust any ratio.

Check what an AI assistant ran

Normalised data are where generated analysis code goes quietly wrong. Check four lines:

  1. Is there a column of 1s or 100s? If the code builds a control group of identical values and runs ttest_ind or t.test(..., var.equal = TRUE) against it, replace it with the paired log test above.
  2. Is there a log? Look for np.log2/log2 before the test. A t-test on raw ratios reports an arithmetic mean that is biased upward.
  3. Is the pairing kept? One fold change per experiment, or paired = TRUE / ttest_rel on log values.
  4. Is n the number of experiments? Lanes or technical replicates pooled across blots inflate n.

Report it

For the constructed four-blot example above (not real data; the interval barely excludes 1):

Band intensities were normalised to the loading control within each lane. For each of four independent blots, the treated-to-control ratio was log₂-transformed and the log ratios were tested against 0 with a two-sided one-sample t-test (equivalent to a paired t-test on log₂ values). Treatment increased the normalised target signal 1.54-fold (geometric mean; 95% CI 1.03 to 2.30; t(3) = 3.38, p = 0.043; n = 4 blots). Analyses used R 4.3.3.

Replace the numbers with yours. For a decrease, the log₂ fold change is negative and the fold change is below 1: write “reduced the signal to 0.65 of control (95% CI …)”. If the interval includes 1 or the adjusted p is above 0.05, keep the estimate and interval and say the change was not statistically significant. If you ran several treatments, add “p-values were adjusted with Holm’s method across the [k] treatment-versus-control comparisons”.

References

  • Curran-Everett D (2013). Explorations in statistics: the analysis of ratios and normalized data. Advances in Physiology Education 37:213–219. doi:10.1152/advan.00053.2013
  • Keene ON (1995). The log transformation is special. Statistics in Medicine 14:811–819. doi:10.1002/sim.4780140810
  • Pillai-Kastoori L, Schutz-Geschwender AR, Harford JA (2020). A systematic approach to quantitative Western blot analysis. Analytical Biochemistry 593:113608. doi:10.1016/j.ab.2020.113608
  • GraphPad Prism 11 Statistics Guide. Paired or ratio t test? graphpad.com (accessed 10 November 2026).
  • SciPy 1.18.1 documentation: scipy.stats.ttest_1samp, scipy.stats.ttest_rel. R 4.3.3 documentation: t.test.

How this page was made

  • Written and published by Licklider. Recommendations cite peer-reviewed methods papers and official software documentation listed under References; cited passages were checked against the source text.
  • The Western blot example uses constructed data. Its numbers and our simulation were produced by stored code (Python with SciPy 1.18.1; R 4.3.3), and the two languages agree.
  • Separate review passes checked the statistics, the citations and readability before publication.