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