pythonforbio.
[WASM idle]
Plotting03/14

Common Plot Types

Starting sandbox…
Beginnerlesson

Common Plot Types

In one line: which mark to use for which question — and the matplotlib/seaborn call for each.

The choice, by question

Question Plot matplotlib seaborn
How is one variable distributed? histogram, KDE ax.hist sns.histplot, sns.kdeplot
How do two continuous variables relate? scatter ax.scatter sns.scatterplot
How does y change over an ordered x? line ax.plot sns.lineplot
How does a distribution differ across groups? box, violin, strip ax.boxplot sns.boxplot, sns.violinplot, sns.stripplot
How do counts compare across categories? bar ax.bar sns.countplot, sns.barplot
What is the structure of a matrix? heatmap ax.imshow sns.heatmap, sns.clustermap
Too many points to see? hexbin, 2-D density ax.hexbin sns.histplot(..., cbin)
Uncertainty around a line? line + band ax.fill_between sns.lineplot (automatic)
Composition of a whole? stacked bar ax.bar(bottom=)

Distributions

ax.hist(values, bins=50, alpha=0.7, edgecolor="white")
ax.hist(values, bins=np.logspace(0, 5, 50)); ax.set_xscale("log")

sns.histplot(data=df, x="af", bins=50, hue="group", element="step")
sns.histplot(data=df, x="expr", log_scale=(True, False))
sns.kdeplot(data=df, x="expr", hue="group", fill=True, common_norm=False)
sns.ecdfplot(data=df, x="expr", hue="group")

Bin count matters more than people admit. Too few hides structure (bimodality!), too many is noise. Try several. bins="auto" uses a reasonable heuristic.

sns.ecdfplot is underused. An empirical CDF has no binning parameter, so it cannot mislead through bin choice, and comparing two ECDFs is a much fairer visual comparison than two overlaid histograms.

KDE on bounded data lies. Allele frequencies live in [0,1]; a KDE will draw density below 0 and above 1. Use histplot, an ECDF, or set clip=(0, 1).

Scatter

ax.scatter(x, y, s=8, alpha=0.4, c=colors, cmap="viridis",
           edgecolors="none", rasterized=True)
sns.scatterplot(data=df, x="pc1", y="pc2", hue="batch", style="sex", s=40)

For overplotting — which is every genomics scatter — the options are, roughly in order:

ax.scatter(x, y, s=2, alpha=0.1, rasterized=True)     # transparency
ax.hexbin(x, y, gridsize=50, cmap="Blues", mincnt=1)   # bin it
sns.histplot(data=df, x="a", y="b", bins=60, cbar=True) # 2-D histogram
sns.kdeplot(data=df, x="a", y="b", levels=6)            # contours

Alpha alone stops working past ~50,000 points. Hexbin or a 2-D histogram is the honest answer for a million-point scatter.

Lines

ax.plot(x, y, lw=1.5, ls="--", marker="o", ms=4, label="treated")
ax.fill_between(x, lo, hi, alpha=0.2)
ax.errorbar(x, y, yerr=err, capsize=3, fmt="o-")

sns.lineplot(data=long, x="time", y="expr", hue="gene", errorbar=("ci", 95))

sns.lineplot aggregates repeated x values automatically and draws a bootstrap CI band. Convenient, and worth knowing it is happening — see Statistical Estimation in seaborn.

Lines imply continuity between points. Do not connect categorical x values with a line.

Categorical comparisons

sns.boxplot(data=long, x="condition", y="expr", hue="tissue")
sns.violinplot(data=long, x="condition", y="expr", inner="quartile", cut=0)
sns.stripplot(data=long, x="condition", y="expr", size=3, alpha=0.5)
sns.swarmplot(data=long, x="condition", y="expr", size=3)
sns.pointplot(data=long, x="dose", y="response", errorbar="se")
sns.barplot(data=long, x="condition", y="expr", errorbar="sd")
sns.countplot(data=variants, x="consequence")

Overlay the points on the box. A box plot of n=4 is four numbers pretending to be a distribution:

sns.boxplot(data=long, x="condition", y="expr", showfliers=False,
            boxprops=dict(alpha=0.5))
sns.stripplot(data=long, x="condition", y="expr",
              color="black", size=3, alpha=0.6)

showfliers=False avoids drawing the outliers twice.

cut=0 on a violin stops the KDE extending beyond your actual data range — otherwise a violin of read counts happily extends below zero.

Bar plots of means with error bars ("dynamite plots") hide the distribution and are actively discouraged in most journals now. Box + strip, or a violin, shows the same summary plus the data.

Heatmaps

im = ax.imshow(matrix, aspect="auto", cmap="viridis", interpolation="nearest")
fig.colorbar(im, ax=ax, label="log2 CPM")

sns.heatmap(corr, cmap="vlag", center=0, vmin=-1, vmax=1,
            square=True, annot=True, fmt=".2f")
sns.clustermap(mat, z_score=0, cmap="vlag", center=0,
               figsize=(8, 10), col_colors=condition_colors)

center=0 with a diverging colormap for anything signed (correlations, log fold changes, z-scores) — otherwise the colour scale's midpoint is arbitrary and the plot implies structure that is not there. See Colormaps and Color.

interpolation="nearest" prevents imshow from smoothing your cells into each other.

See Applied - Heatmaps and Clustermaps.

Multi-panel comparisons

sns.pairplot(df[["pc1", "pc2", "pc3"]], hue="batch", diag_kind="kde")
sns.jointplot(data=df, x="a", y="b", kind="hex")
sns.relplot(data=long, x="dose", y="expr", col="gene", col_wrap=4, kind="line")

See Faceting.

Bioinformatics-specific plots

None of these are built in; all are compositions of the above.

# volcano — scatter with thresholds
ax.scatter(log2fc, neglog10p, s=6, c=np.where(sig, "crimson", "lightgrey"))

# MA plot — log ratio vs mean expression
ax.scatter(np.log10(base_mean), log2fc, s=4, alpha=0.3)
ax.axhline(0, c="grey")

# Manhattan — position vs -log10 p, coloured by chromosome
for i, (chrom, sub) in enumerate(df.groupby("chrom", observed=True)):
    ax.scatter(sub["cum_pos"], sub["neglog10p"],
               s=3, c=["#4C72B0", "#DD8452"][i % 2], rasterized=True)

# QQ plot — observed vs expected p-value quantiles
expected = -np.log10(np.linspace(1/n, 1, n))
ax.scatter(expected, np.sort(neglog10p)[::-1], s=4)
ax.plot([0, expected.max()], [0, expected.max()], "r--")

# coverage track
ax.fill_between(positions, depth, step="mid", alpha=0.6)

# survival (Kaplan-Meier) — use the lifelines package

Common mistakes

  • Bar plots of means instead of showing the distribution.
  • Box plots with n < 10 and no points overlaid.
  • KDE on bounded data producing impossible values.
  • Overplotted scatters where alpha is not enough.
  • Truncated y-axes on bar charts. Bars encode length; a bar chart must start at zero. (Line and scatter plots may be truncated; bars may not.)
  • Diverging colormaps without center=.
  • Pie charts. Humans compare angles badly. A bar chart is better in essentially every case.
  • Not rasterizing big scatters in vector output.
  • Dual y-axes implying a correlation that is an artefact of two arbitrary scales.

See also

Figure and Axes · Colormaps and Color · Statistical Estimation in seaborn · Faceting · Applied - Differential Expression Volcano Plot · Applied - Heatmaps and Clustermaps · Applied - Quality Control Plots

scratch

No output yet — run the code to populate this drawer.