HOWTO · Matplotlib

How to Plot Two Histograms Together in Matplotlib

Plot two histograms together in Matplotlib using grouped bars, transparent overlays, or separate subplots. Compare datasets with shared bins, labels, and styles.

On this page

To plot two histograms together in Matplotlib, pass both datasets to plt.hist([a, b], bins=...) for grouped bars, or call plt.hist() twice on the same axes for an overlay. Use separate subplots when you want each distribution to have its own panel. In every approach, use common bin edges so corresponding bars represent the same intervals.

The examples below require NumPy and Matplotlib. They are self-contained. Displaying a plot with plt.show() requires an interactive plotting environment. In a noninteractive environment, save the figure as described in the final section.

Plot two histograms without overlapping bars

Pass a list containing the two datasets to one plt.hist() call. With histtype="bar" and stacked=False, Matplotlib places their bars next to each other within each bin. This is useful for comparing counts interval by interval on a single pair of axes.

Matplotlib may report font-cache initialization on its first run. The logging setup keeps font-manager diagnostics visible on standard output rather than standard error; it does not suppress them.

This example uses generated samples and includes a legend:

import logging
import sys

# Keep first-run font-cache diagnostics visible on standard output.
font_logger = logging.getLogger("matplotlib.font_manager")
font_logger.addHandler(logging.StreamHandler(sys.stdout))
font_logger.propagate = False

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)
a = rng.normal(0, 3, 3000)
b = rng.normal(2, 4, 2000)
bins = np.linspace(min(a.min(), b.min()), max(a.max(), b.max()), 21)

plt.figure(figsize=(8, 4))
plt.hist([a, b], bins=bins, histtype="bar", stacked=False, label=["a", "b"])
plt.xlabel("Value")
plt.ylabel("Count")
plt.title("Two histograms with grouped bars")
plt.legend(loc="upper left")
plt.tight_layout()
plt.show()

Two histograms with grouped bars: the blue bars of dataset a and the orange bars of dataset b stand side by side in every bin, so their counts can be compared bin by bin.

Here, np.linspace() supplies 21 edges, producing 20 bins across the combined data range. Increase the number of edges to show finer detail; reduce it to emphasize broader patterns. A fixed range such as np.linspace(-10, 10, 21) is also possible, but observations outside that range are excluded from the histogram.

A histogram does not require normally distributed samples. Replace a and b with one-dimensional numeric arrays of your own measurements, using consistent units. Convert lists with np.asarray() before using the .min() and .max() calls shown above. Remove or handle missing and nonfinite values before deriving bin edges from the data. Both arrays must contain observations for the minimum and maximum calculations shown here. If all measurements have the same value, supply distinct, increasing edges that span that value instead of deriving identical edges.

Plot two histograms with overlapping bars

Call plt.hist() twice on the same axes to overlay the bars. Pass the same edge array to both calls. Using only bins=20 separately would let each call calculate edges from its own data range, so their intervals could differ despite having the same bin count.

import logging
import sys

# Keep first-run font-cache diagnostics visible on standard output.
font_logger = logging.getLogger("matplotlib.font_manager")
font_logger.addHandler(logging.StreamHandler(sys.stdout))
font_logger.propagate = False

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)
a = rng.normal(0, 3, 1000)
b = rng.normal(2, 4, 900)
bins = np.linspace(min(a.min(), b.min()), max(a.max(), b.max()), 51)

plt.figure(figsize=(8, 4))
plt.hist(a, bins=bins, alpha=0.5, label="a", color="tab:blue")
plt.hist(b, bins=bins, alpha=0.5, label="b", color="tab:orange")
plt.xlabel("Value")
plt.ylabel("Count")
plt.title("Two histograms with overlapping bars")
plt.legend(loc="upper left")
plt.tight_layout()
plt.show()

Two overlapping histograms with transparent bars: where the blue bars of dataset a and the orange bars of dataset b overlap, the colors blend, so both distributions stay visible.

The alpha parameter controls opacity: 0.0 is fully transparent, 1.0 is fully opaque, and 0.5 allows both colors to contribute where bars overlap. Omitting alpha normally produces opaque bars; it does not make them transparent. With opaque bars at the same zorder, the histogram drawn second can cover the first wherever their rectangles overlap, regardless of which count is larger.

The same two samples without alpha show why transparency matters. The orange histogram is drawn second and covers the blue one up to its own height wherever they overlap; where an orange bar is taller, the blue bar behind it disappears completely, even when it represents more observations.

The same two histograms without transparency: the opaque orange bars drawn second hide the blue bars behind them wherever the orange bar is at least as tall.

Counts also reflect sample size. When groups contain different numbers of observations and you want to compare distribution shapes, set density=True in both calls and label the vertical axis "Probability density". Each histogram then has total area one over its included observations; the bar heights themselves need not sum to one.

Compare histograms in side-by-side subplots

Separate panels avoid occlusion and make each distribution easier to inspect. Keep the same bins and axis scales so differences in panel scaling do not distort the comparison.

import logging
import sys

# Keep first-run font-cache diagnostics visible on standard output.
font_logger = logging.getLogger("matplotlib.font_manager")
font_logger.addHandler(logging.StreamHandler(sys.stdout))
font_logger.propagate = False

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)
data1 = rng.normal(0, 1, 1000)
data2 = rng.normal(1, 1, 1000)
bins = np.linspace(min(data1.min(), data2.min()), max(data1.max(), data2.max()), 31)

plt.figure(figsize=(9, 4))
left = plt.subplot(1, 2, 1)
plt.hist(data1, bins=bins, color="tab:blue")
plt.title("Dataset 1")
plt.xlabel("Value")
plt.ylabel("Count")

plt.subplot(1, 2, 2, sharex=left, sharey=left)
plt.hist(data2, bins=bins, color="tab:orange")
plt.title("Dataset 2")
plt.xlabel("Value")
plt.ylabel("Count")

plt.tight_layout()
plt.show()

Two histograms in separate side-by-side panels with shared axes: dataset 1 on the left and dataset 2 on the right use the same bins and scales.

In plt.subplot(1, 2, 1), the first two arguments define one row and two columns; the third selects the left panel. Position 2 selects the right panel. Sharing both axes keeps their scales aligned, while plt.tight_layout() adjusts spacing around the titles and labels. Unlike grouped bars, this arrangement uses two axes within one figure.

Derive bin edges safely when a dataset can be empty

The examples above derive the shared bin edges from the minimum and maximum of both datasets. That derivation fails when a dataset is empty, because an empty array has no minimum:

import numpy as np
import matplotlib.pyplot as plt

a = np.array([])
b = np.array([1.0, 2.0, 3.0])
bins = np.linspace(min(a.min(), b.min()), max(a.max(), b.max()), 21)
plt.hist([a, b], bins=bins)

Output:

Traceback (most recent call last):
  ...
ValueError: zero-size array to reduction operation minimum which has no identity

Filter out empty datasets before deriving the edges. Remove nonfinite values at the same step: a single NaN makes the minimum and maximum NaN, so every derived edge is NaN and the histogram has no usable bins.

import numpy as np

a = np.array([])
b = np.array([1.0, 2.0, 3.0])
datasets = [d for d in (a, b) if d.size > 0]
bins = np.linspace(min(d.min() for d in datasets), max(d.max() for d in datasets), 21)
print("datasets kept:", len(datasets))
print("first and last bin edges:", bins[0], bins[-1])

Output:

datasets kept: 1
first and last bin edges: 1.0 3.0

With the empty dataset filtered out, the remaining dataset determines the edges and can be plotted with the grouped or overlay calls from the earlier sections. If datasets is empty after filtering, there is nothing to plot; handle that case in your own code instead of deriving edges.

Customize and label the histograms

Give each dataset a descriptive legend label and a distinct color. Use edgecolor to outline bars, and linestyle to distinguish their outlines further. Light horizontal gridlines help readers estimate heights without dominating the comparison.

import logging
import sys

# Keep first-run font-cache diagnostics visible on standard output.
font_logger = logging.getLogger("matplotlib.font_manager")
font_logger.addHandler(logging.StreamHandler(sys.stdout))
font_logger.propagate = False

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)
data1 = rng.normal(0, 1, 1000)
data2 = rng.normal(1, 1, 1000)
bins = np.linspace(min(data1.min(), data2.min()), max(data1.max(), data2.max()), 31)

fig, ax = plt.subplots(figsize=(8, 4))
ax.hist(data1, bins=bins, color="tab:blue", edgecolor="black",
        linestyle="--", alpha=0.5, label="Dataset 1")
ax.hist(data2, bins=bins, color="tab:orange", edgecolor="black",
        linestyle=":", alpha=0.5, label="Dataset 2")
ax.set(title="Styled histogram comparison", xlabel="Value", ylabel="Count")
ax.set_axisbelow(True)
ax.grid(axis="y", alpha=0.3)
ax.legend()
fig.tight_layout()
plt.show()

Two styled overlapping histograms: dataset 1 has dashed bar outlines and dataset 2 dotted outlines, with light horizontal gridlines behind the bars.

To emphasize shapes without filled bars, use histtype="step" and a suitable linewidth in each histogram call. Add measurement units to the horizontal label when adapting these examples.

To save a plot, insert fig.savefig("histograms.png", dpi=150, bbox_inches="tight") before plt.show() in the final example. For the pyplot examples, use plt.savefig() before showing the figure. Saving before showing avoids relying on whether the plotting backend retains the figure afterward; saving alone also works without a display window.

For more than two datasets, extend the list passed to a single plt.hist() call and supply a label for each dataset, or add calls using the same edges. As the number of groups grows, outlines or separate panels often remain easier to read than many transparent fills.