Bivariate Density Plot (Python)

Authors

[Editor] Shixiang Wang;

[Contributors] Claude Code โ€” AI-generated, independently AI-reviewed;

Modified

2026-10-19

๐Ÿค– AI Skill โ€” Download the Bizard unified skill for your AI assistant โฌ‡๏ธ Download Skill ZIP
๐Ÿ”Œ MCP Server โ€” Let AI agents (Claude Code, Cursor, โ€ฆ) query Bizard live: claude mcp add bizard -- npx @openbiox/bizard-mcp โ€” or the free hosted instance (no install, faster from mainland China): claude mcp add --transport http bizard http://biotree.top:38123/bizard-mcp/mcp ๐Ÿ“– MCP Docs
๐Ÿ“– Cite Bizard โ€” if Bizard helps your research, please cite: Li, K., Zheng, H., Huang, K., et al. (2026). Bizard: A Community-Driven Platform for Accelerating and Enhancing Biomedical Data Visualization. iMetaMed, e70038. ๐Ÿ”— DOI / BibTeX

When a scatter has thousands of points, overplotting destroys it; bivariate density displays replace the dots with estimated density (filled KDE or contours) or binned counts (hexbin). In biomedical research these are the everyday answer to dense flow-cytometry panels, embeddings, and QC metric clouds. seaborn.kdeplot and matplotlib.hexbin cover both families.

Example

Setup

  • System Requirements: Cross-platform (Linux/MacOS/Windows)
  • Programming Language: Python
  • Dependencies: matplotlib, seaborn, pandas, numpy
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np

Data Preparation

We simulate a 20,000-cell two-marker cytometry measurement with three lymphocyte populations whose separation is only visible in density form.

np.random.seed(111)

n = 20000
sub = np.random.choice(3, n, p=[0.5, 0.3, 0.2])
centers = [(-0.6, -0.4), (0.5, 0.7), (1.4, -0.9)]
covs = [np.array([[0.18, 0.05], [0.05, 0.15]]),
        np.array([[0.12, -0.03], [-0.03, 0.10]]),
        np.array([[0.15, 0.02], [0.02, 0.12]])]
xy = np.stack([rng for rng in
               [np.random.multivariate_normal(centers[s], covs[s]) for s in sub]])
cells = pd.DataFrame(xy, columns=['CD4', 'CD8'])

Visualization

Filled 2D KDE

A filled kernel density estimate shows population shapes smoothly; raising thresh removes the thin outermost tails that only add noise.

fig, ax = plt.subplots(figsize=(5.8, 5))
sns.kdeplot(data=cells, x='CD4', y='CD8', fill=True, thresh=0.02,
            levels=40, cmap='mako', cbar=True,
            cbar_kws=dict(label='Density'), ax=ax)
ax.set_xlabel('CD4 intensity (a.u.)')
ax.set_ylabel('CD8 intensity (a.u.)')
ax.spines[['top', 'right']].set_visible(False)
plt.tight_layout()
plt.show()
Figureย 1: Filled 2D density of a 20,000-cell cytometry sample

Hexbin Plot

Hexagonal bins count points directly โ€” no smoothing assumptions, and the colour bar keeps the mapping honest. The right gridsize balances resolution against empty bins.

fig, ax = plt.subplots(figsize=(5.8, 5))
hb = ax.hexbin(cells['CD4'], cells['CD8'], gridsize=60, cmap='viridis',
               mincnt=1, linewidths=0.1)
cb = fig.colorbar(hb, ax=ax, shrink=0.85)
cb.set_label('Cell count')
ax.set_xlabel('CD4 intensity (a.u.)')
ax.set_ylabel('CD8 intensity (a.u.)')
ax.spines[['top', 'right']].set_visible(False)
plt.tight_layout()
plt.show()
Figureย 2: Hexbin counts of the same cytometry sample

Contours over a Sparse Scatter

For gated subpopulations, draw filled contours per group and overlay a light scatter of a subsample โ€” the gates stay interpretable while the full density remains visible.

cells['Population'] = np.array(['P1', 'P2', 'P3'])[sub]
palette = {'P1': '#66c2a5', 'P2': '#8da0cb', 'P3': '#e63946'}

fig, ax = plt.subplots(figsize=(5.8, 5))
sns.kdeplot(data=cells, x='CD4', y='CD8', hue='Population', palette=palette,
            fill=False, levels=6, linewidths=1.6, thresh=0.03, ax=ax)
sub_sample = cells.sample(frac=0.02, random_state=0)
sns.scatterplot(data=sub_sample, x='CD4', y='CD8', hue='Population',
                palette=palette, s=8, alpha=0.5, legend=False, ax=ax)
ax.set_xlabel('CD4 intensity (a.u.)')
ax.set_ylabel('CD8 intensity (a.u.)')
ax.spines[['top', 'right']].set_visible(False)
plt.tight_layout()
plt.show()
Figureย 3: Per-population contours with a 2% scatter overlay

References

  1. Waskom, M. L. (2021). seaborn: statistical data visualization. Journal of Open Source Software, 6(60), 3021.
  2. Hunter, J. D. (2007). Matplotlib: A 2D graphics environment. Computing in Science & Engineering, 9(3), 90-95.