The previous Seaborn article introduced the figure-level vs. axes-level distinction at a general level. This article goes deeper into exactly how that distinction plays out within three specific, closely related tools: seaborn heatmap for correlation matrices, seaborn pairplot distribution plots for multivariate overviews, and the axes-level histplot()/kdeplot() functions that power them underneath.
Three Tools for Seeing Data's Shape and Structure
A quick recap
As covered in the previous article: axes-level Seaborn functions draw onto a single Matplotlib Axes, while figure-level functions manage their own entire figure, often creating multiple subplots automatically. This article goes considerably deeper into exactly how that relationship plays out for three specific, closely related tools.
Why these three belong together
Distribution plots (understanding a single variable's shape), pair plots (surveying relationships across every variable at once), and heatmaps (visualizing matrix-like data such as correlations) work together to reveal a dataset's actual structure — patterns, anomalies, and variable interactions genuinely essential for effective feature engineering and model building, rather than just producing individually pretty charts.
What this article covers
histplot()/kdeplot() for distributions, pairplot() for multivariate overviews, heatmap() for correlation matrices — and, running throughout, exactly how the axes-level and figure-level versions of each relate to one another.
Distribution Plots: histplot(), kdeplot(), and Their Figure-Level Cousins
The structure of the distributions module
Seaborn's distributions module contains axes-level functions — histplot(), kdeplot(), ecdfplot(), and rugplot() — which are then grouped together within figure-level functions like displot(), jointplot(), and pairplot(). This is exactly the axes-level/figure-level relationship from the previous article, but concretely: the figure-level functions covered in this article aren't separate implementations — they're built directly on top of the same axes-level functions, orchestrating them across multiple subplots or panels.
histplot() vs. kdeplot()
import seaborn as sns
import pandas as pd
tips = sns.load_dataset("tips")
sns.histplot(data=tips, x="total_bill")
sns.kdeplot(data=tips, x="total_bill")histplot() draws a classic binned histogram, with sensible default bin counts chosen automatically. kdeplot() draws a smooth, continuous probability density curve instead — a genuinely different way of visualizing the exact same underlying distribution, trading the histogram's discrete, sometimes jagged bins for a smoothed estimate of the underlying probability density.
sns.histplot(data=tips, x="total_bill", hue="time")
sns.kdeplot(data=tips, x="total_bill", hue="time")Both support a hue= parameter for automatically comparing distributions across a categorical group — here, comparing the total_bill distribution for lunch versus dinner, in a single call, without manually filtering and plotting each group separately.
A practical historical note: distplot() is deprecated
Worth knowing directly, since it affects how you should read older tutorials and Stack Overflow answers: Seaborn deprecated the older distplot() function, because it combined multiple functionalities — histograms, KDE, rug plots — somewhat inconsistently into one function with confusing parameter overlap. histplot() and kdeplot() are the clean, current replacements, each with a clearly focused, single purpose. If you encounter a tutorial still centered on sns.distplot(), it's showing genuinely outdated syntax — worth treating as a signal that the rest of that tutorial's content may also be dated.
Bivariate Distributions with jointplot()
What jointplot() adds
sns.jointplot(data=tips, x="total_bill", y="tip")jointplot() augments a bivariate relational or distribution plot with the marginal distributions of both variables — showing the relationship between total_bill and tip in the center (by default, a scatter plot), with each variable's own individual distribution (by default, a histogram) shown along the top and right edges. This gives you both the joint relationship and each variable's individual shape, all in one combined figure.
Changing the kind parameter
sns.jointplot(data=tips, x="total_bill", y="tip", kind="kde")Changing kind="kde" switches both the joint plot and the marginal plots to use kdeplot() instead of the default scatter-and-histogram combination — the central panel becomes a 2D density contour plot, and the marginals become smooth curves rather than histograms. This directly demonstrates the axes-level/figure-level relationship from Section 2 concretely: jointplot() (figure-level) is genuinely just orchestrating the same underlying axes-level functions (scatterplot()/histplot(), or kdeplot() for both, depending on kind) into one combined, multi-panel presentation.
Using JointGrid directly for full manual control
g = sns.JointGrid(data=tips, x="total_bill", y="tip")
g.plot_joint(sns.histplot)
g.plot_marginals(sns.boxplot)When the convenience wrapper's defaults aren't flexible enough — here, pairing a histplot() joint plot with boxplot() marginals, a combination jointplot()'s built-in kind options don't directly offer — the underlying JointGrid class lets you assign the joint plot and marginal plots independently, mixing and matching axes-level functions however genuinely suits your specific data.
Pair Plots: A Small-Multiple Overview of Every Variable
The small-multiple approach
numeric_tips = tips[["total_bill", "tip", "size"]]
sns.pairplot(numeric_tips)Rather than focusing on a single relationship, pairplot() uses a "small-multiple" approach — visualizing the univariate distribution of every variable in a dataset, along with all of their pairwise relationships, at once. This produces a grid where each row and column corresponds to one variable: off-diagonal cells show a scatter plot of each variable pair, and diagonal cells show each individual variable's own distribution. It's an excellent way to get a quick, comprehensive overview before deciding which specific relationships deserve deeper, more focused investigation.
The default behavior and hue
The simplest invocation uses scatterplot() for each off-diagonal variable pairing, and histplot() for the diagonal marginal plots.
sns.pairplot(tips[["total_bill", "tip", "size", "time"]], hue="time")Assigning a hue= variable adds a semantic color mapping across every panel simultaneously, and changes the default diagonal plots from stacked histograms to a layered KDE instead — a genuinely sensible default switch, since overlapping, differently-colored KDE curves are considerably easier to visually compare than overlapping histogram bars, which tend to visually obscure each other once stacked or overlaid.
Using PairGrid directly for full manual control
g = sns.PairGrid(numeric_tips)
g.map_upper(sns.scatterplot)
g.map_lower(sns.kdeplot)
g.map_diag(sns.histplot)The underlying PairGrid class lets you map different plot functions to the upper triangle, lower triangle, and diagonal separately — here, scatter plots above the diagonal, KDE contours below it, and histograms directly on the diagonal. This produces a richer, genuinely non-redundant multi-panel view: since the upper and lower triangles of a pairwise grid are mirror images of each other (variable A vs. B is the same relationship as B vs. A, just transposed), using two different plot types for each half means you're not wasting visual space repeating the identical relationship twice in the exact same way.
Heatmaps: Visualizing Correlation Matrices
The most common application
correlati numeric_tips.corr()
sns.heatmap(correlation_matrix, annot=True, cmap="coolwarm")Heatmaps are excellent for visualizing matrix-like data where colors represent values — and the most common application in everyday data analysis is a correlation matrix of numerical features, computed directly via pandas' .corr() method (tying back to the earlier pandas and NumPy statistical functions articles) and passed straight into sns.heatmap().
Reading a correlation heatmap correctly
Values close to 1 indicate strong positive linear relationships — as one variable increases, the other tends to increase too. Values close to -1 indicate strong negative relationships — as one increases, the other tends to decrease. Values near 0 indicate relatively uncorrelated variables, with no strong linear relationship between them. annot=True overlays the actual numeric correlation value directly on top of each cell, giving you both the color's quick visual impression and the precise underlying number in one combined view.
Closing practical workflow: tying all three tools together
A genuinely effective real-world sequence, using everything covered in this article together: use a quick correlation heatmap first, to identify which variable pairs actually show the strongest relationships across a potentially large dataset. Then build a pairplot() focused on just those specific variables — rather than every single column, which gets visually overwhelming (and genuinely hard to read) past roughly 5–6 variables — for a detailed, digestible multivariate exploration of exactly the relationships the heatmap flagged as worth a closer look. This two-step workflow — broad correlation overview first, focused pairwise detail second — is considerably more practical for a dataset with more than a handful of columns than jumping straight to a full pairplot() across every variable at once, which would produce a genuinely overwhelming grid nobody can actually read carefully.