Plotting03/14
Starting sandbox…
Beginner·4 graded tests

Common Plot Types

Common Plot Types

Core Principle: Selecting appropriate visual marks depends on the statistical nature of the variables, distinguishing continuous distributions, bivariate relationships, paired comparisons, and categorical summaries.

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

Exercise

Write bin_counts(values, nbins). The first line of input is the bin count and every remaining line holds whitespace-separated values. Draw the histogram with ax.hist, choosing the spacing from the data: if the values are strictly positive and span at least two orders of magnitude: hi / lo >= 100: use nbins + 1 log-spaced edges from np.logspace and set the x scale to log; otherwise pass nbins and let matplotlib place linear edges. np.logspace reproduces its endpoints only to floating-point precision, so overwrite the first and last edge with the exact minimum and maximum, or the extreme observations fall outside the outermost bins and go uncounted.

Return three lines: counts= followed by the bar heights, read back as integers from the Rectangle patches ax.hist returned rather than from the counts array; edges= followed by every bin edge rounded to three decimals; and scale=<linear|log> total=<sum of counts> n=<number of values>, where total and n must agree. When all values are identical the range is degenerate and matplotlib widens it by half a unit on each side: report whatever bins that produces. Return the single line none for empty input.

exerciseEvery submission runs independently in a fresh namespace.

Test cases · 4

#viainputexpected stdout
1stdin4 1204 1150 2310 2402 455 470 441 462 counts=4 2 0 2 edges=441.0 931.25 1421.5 1911.75 2402.0 scale=linear total=8 n=8
2stdin5 1.0 12.0 150.0 900.0 15200.0 3.0 88.0 counts=2 1 2 1 1 edges=1.0 6.861 47.069 322.928 2215.516 15200.0 scale=log total=7 n=7
3stdin3 0.51 0.49 0.32 0.21 0.44 0.38 counts=1 2 3 edges=0.21 0.31 0.41 0.51 scale=linear total=6 n=6
4stdin2 7.5 7.5 7.5 7.5 counts=0 4 edges=7.0 7.5 8.0 scale=linear total=4 n=4

Hints · 3

01Hint

`ax.hist` returns `(counts, edges, patches)`; the patches are Rectangles with `get_height()`.

02Hint

`np.logspace` takes log10 of the bounds, not the bounds themselves.

03Hint

Compare `sum(heights)` with `len(values)` while testing — a mismatch means an edge is off by a float epsilon.