Python
FacetGrid
Heatmap
Use Seaborn to create multivariate visualizations: scatter plots, line pots, heatmaps, joint plots, facet grids, etc.
Author

Pinn Xu

Published

October 27, 2024

20 min read · 3970 EN words · 0 中文字
Environment and Versions
python==3.12
pandas==2.2.2
matplotlib==3.9.x
seaborn==0.13.2

1 Multivariate Data

“Tidy/flat” tabular data: Each row is one observation (also called instance or record), and each column is one variable (also called attribute, feature, or parameter).

Typically, multivariate refers to data with more than two variables — a multivariate plot generally uses three or more columns from the original DataFrame.

When creating a bar chart, in addition to the x and y variables, you can also set the hue parameter to include a third variable. For example, Figure 1 includes three variables: 'island', 'body_mass_g', and 'sex'.

# import libraries
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# load example dataset from Seaborn
df = sns.load_dataset('penguins')
sns.barplot(df, x='island', y='body_mass_g', hue='sex')
plt.show()
Figure 1: Bar chart with three variables

However, some chart types do not support setting x, y, and hue all at once. For example, a histogram only supports x and hue (see Figure 2): the x-axis shows distribution intervals of flipper length, and the y-axis shows the number of penguins in each interval, involving only two variables.

sns.histplot(data=df, x='flipper_length_mm', hue='island')
plt.show()
Figure 2: Histogram with two variables

2 Facet Grid

A facet grid (FacetGrid) is a common method for displaying multivariate data. Several small charts (facets) are arranged in a grid to form a facet grid. If you want to display a histogram while also showing more than two variables, you can use a facet grid.

The method for creating a facet grid with Seaborn is as follows:

# create an empty facet grid
g = sns.FacetGrid(
    data=df,
    col='sex',    # creates one column per unique value of 'sex'
    row='island', # 'island' has three values, creating three rows
)
plt.show()
Figure 3: Empty 3×2 facet grid

As shown in Figure 3, we created a 3×2 facet grid, but it is currently empty. Use the .map_dataframe() method to specify the content to fill in.

# fill the complete facet grid

# Step 1: create facet grid
g = sns.FacetGrid(
    data=df,
    col='sex',
    row='island',
    hue='species',
)

# Step 2: fill with histograms
g.map_dataframe(
    sns.histplot,
    x='flipper_length_mm',
    stat='density',    # sum of bar areas equals 1 for each group
    common_norm=False, # each color normalized independently within each facet
)

# Step 3: add and adjust legend
g.add_legend()
sns.move_legend(
    g,
    loc='lower center',
    bbox_to_anchor=(0.5, 1),
    ncol=3,  # number of legend columns
)
plt.show()
Figure 4: Filled facet grid

stat and common_norm stat='density' uses frequency density for plotting, so the total area of all bars in the histogram sums to 1. common_norm=False is used alongside stat, meaning each color’s bars within the same facet are normalized independently, rather than all colors being normalized together.

loc and bbox_to_anchor Setting both parameters together positions the lower-center edge of the legend at coordinates (0.5, 1) of the entire grid figure. If only loc='lower center' is set, the legend will be placed at the lower center of the grid figure directly.

As shown in Figure 4, the facet grid separates rows by island, columns by sex, and colors by species, simultaneously displaying four variables (flipper length, island, sex, species). Splitting by rows and columns allows two additional discrete variables; color differentiation can add one more discrete variable.

Note: Because of how rows and columns work, facet grids can only add discrete variables. To facet by a continuous variable, first bin it into intervals and save as a new discrete variable column, then use the facet grid for row or column splitting.

Consistent Axis Ranges When displaying multiple subplots together, each subplot should have the same axis range, because readers tend to assume that axis ranges are consistent across panels — inconsistency can lead to misinterpretation (Wilke, 2019, pp. 257-258).

In Figure 5, each subplot has a different y-axis range, which is easy to misread. Changing to Figure 6 with consistent axis ranges across all subplots reduces reading difficulty.

Bad multi-panel axis ranges (Wilke, 2019, p. 258)
Figure 5: Counter-example: inconsistent axis ranges (Wilke, 2019, p. 258)
Improved multi-panel axis ranges (Wilke, 2019, p. 259)
Figure 6: Improved version: consistent axis ranges (Wilke, 2019, p. 259)

3 Scatter Plot

3.1 Distinguishing Scatter Plots from Point Plots

Before creating scatter plots, it is important to distinguish between scatter plots and point plots.

Note: Point plots are also called dot plots (Wilke, 2019, p.43). The following distinction between scatter plots and point plots is based on the Seaborn library and may differ in other contexts.

A point plot can function similarly to a bar chart: the vertical line at each point is equivalent to a bar in a bar chart, and the point corresponds to the midpoint of the top edge of each bar. Like a bar chart, a point plot can be used for category-amount charts (see Figure 7 and Figure 8):

sns.pointplot(
    data=df, x='species', y='bill_length_mm',
    linestyles='none',  # no lines connecting points
)
plt.show()
Figure 7: Point plot: penguin species vs. bill length (black lines are error bars)
sns.barplot(data=df, x='species', y='bill_length_mm')
plt.show()
Figure 8: Bar chart: penguin species vs. bill length (black lines are error bars)

As shown in Figure 7 and Figure 8, both point plots and bar charts can represent category-amount relationships. One key difference is that the amount axis in a bar chart must start at 0, while a point plot does not. Both point plots and bar charts only show the mean of the y-axis values (or other statistics such as the median), with each x-axis value corresponding to only one y-axis value.

A scatter plot displays every individual data point (see Figure 9):

sns.scatterplot(data=df, x='species', y='bill_length_mm')
plt.show()
Figure 9: Scatter plot: category-amount (shows all data points, not averages)

In a scatter plot, each x-axis value can correspond to multiple y-axis values, while in a point plot each x-axis value corresponds to only one y-axis value.

Scatter plots are commonly used to explore the relationship between two (or more) variables and to examine whether correlations exist, primarily between two continuous numeric variables. The x-axis of a scatter plot is therefore usually ratio data, with tick marks automatically displayed as a continuous numeric range (see Figure 10):

sns.scatterplot(
    data=df, x='bill_length_mm', y='bill_depth_mm',
    alpha=0.7,  # semi-transparent to reveal point clustering
)
plt.show()
Figure 10: Scatter plot: penguin bill length vs. bill depth (ratio-ratio)

Note: Using transparency helps reveal point clustering, but if a gradient color is already used to differentiate a variable (as in Figure 14), transparency should not be applied.

When a point plot uses ratio data as the x-axis, each point maps to a specific tick mark, as shown in Figure 11:

sns.pointplot(
    data=df, x='bill_length_mm', y='bill_depth_mm',
    linestyle='none', errorbar=None, alpha=0.7,
)
plt.show()
Figure 11: Point plot: ratio variable on x-axis

Note: Although the point distribution in Figure 10 and Figure 11 looks similar, each x-axis value in a point plot has exactly one corresponding point, while a scatter plot can have multiple. Also, sns.pointplot() does not support adjusting point size, while sns.scatterplot() does.

Therefore, point plots are not suitable when the x-axis is continuous data with too many tick marks; they work best when the x-axis is discrete data with a limited number of categories. Scatter plots are the opposite — better suited for ratio-ratio data.

Another feature of point plots is that points can be connected with lines, which is appropriate when the x-axis uses ordinal or interval data. For example, in Figure 12, year is interval data and the connecting lines show passenger trends over time; in Figure 7, the x-axis represents penguin species (nominal data) where order is meaningless, so lines are not drawn.

This type of point plot becomes very similar to a line plot, which we will continue to discuss in Line Plot.

flights = sns.load_dataset('flights')
sns.pointplot(data=flights, x='year', y='passengers')
plt.show()
Figure 12: Point plot: year vs. passenger count (interval-ratio)

3.2 Multivariate Scatter Plot

In addition to the two continuous variables on the x and y axes, other variables can be introduced by setting parameters such as hue (point color), size (point size), and style (point shape). hue and size can be applied to either continuous or discrete variables; style can only be applied to discrete variables, because point shapes cannot vary continuously (in Seaborn).

For example, Figure 13 introduces three discrete variables simultaneously:

ax = sns.scatterplot(
    data=df, x='bill_length_mm', y='bill_depth_mm',
    hue='sex',
    size='island',
    style='species',
)
sns.move_legend(ax, loc='upper left', bbox_to_anchor=(1, 1))
plt.show()
Figure 13: Adding three discrete variables: hue & size & style

In Figure 13, in addition to the two continuous variables on the axes (bill length and bill depth), three discrete variables are encoded (sex, island, and species). Similarly, Figure 14 introduces two continuous variables:

ax = sns.scatterplot(
    data=df, x='bill_length_mm', y='bill_depth_mm',
    hue='flipper_length_mm',
    size='body_mass_g',
)
sns.move_legend(ax, loc='upper left', bbox_to_anchor=(1, 1))
plt.show()
Figure 14: Adding two continuous variables: hue & size (gradient color used, avoid transparency)

Note: Although hue and size can be applied to continuous variables, their legends are not strictly continuous.

See Table 1 for a comparison of multivariate scatter plots and facet grids:

Table 1: Comparison of multivariate scatter plots and facet grids
Dimension Multivariate Scatter Plot Facet Grid
View Global, all points in one coordinate system Split into panels for easy subplot comparison
Space Compact, only one chart needed Larger, requires multiple subplots
Continuous variables Can be added directly with auto-generated legend Requires manual binning into a new discrete variable
Visual complexity Information concentrated, may feel crowded Each subplot is independent, lower reading burden
Legend reliance Requires repeatedly consulting the legend Subplot titles explain directly

To make scatter plots easier to read, it is recommended to use both color and shape to distinguish the same variable (Wilke, 2019, pp.243-247), as in Figure 15:

ax = sns.scatterplot(
    data=df, x='bill_length_mm', y='bill_depth_mm',
    hue='island',
    style='island',
)
sns.move_legend(ax, loc='upper left', bbox_to_anchor=(1, 1))
plt.show()
Figure 15: hue+style both control the same variable (island)

Furthermore, avoid using both style and size to control two different variables in the same chart, because when shapes differ, size differences become harder to perceive (see Figure 16):

ax = sns.scatterplot(
    data=df, x='bill_length_mm', y='bill_depth_mm',
    hue='island',
    style='island',
    size='flipper_length_mm',
)
sns.move_legend(ax, loc='upper left', bbox_to_anchor=(1, 1))
plt.show()
Figure 16: hue+style control island, size controls flipper length separately

Other notes:

  • When using color, try to accommodate color-vision-impaired readers by choosing colors with clearly distinct differences in both hue and lightness.
  • When there are too many data points, it is not recommended to display markers in a line plot.

These details will be further discussed in Section 4.

3.3 Logarithmic Scale

A logarithmic scale is a nonlinear measurement scale used for data with a wide range of values, such as the Richter scale, acoustic volume, optical intensity, and solution pH. When plotting such data, the axis can be set to a logarithmic scale (the default is a linear scale — for example, the x-axis tick marks 35, 40, 45, 50 in Figure 10 increase linearly), making otherwise crowded data points more evenly distributed and easier to observe (Wikipedia - Logarithmic scale).

Wilke (2019, p.17) uses \((1, 3.16, 10, 31.6, 100)\) as an example to illustrate four number lines (Figure 17), where \(10^{0.5} = \sqrt{10} \approx 3.16\) and \(10^{1.5} \approx 31.6\).

Four number lines (Wilke, 2019, p.17)
Figure 17: Four number lines (Wilke, 2019, p.17)

As shown in Figure 17, the four number lines are interpreted as follows:

  • The first number line uses a linear scale with tick marks at \((0, 25, 50, 75, 100)\), displaying the original data \(x\). The data points are unevenly distributed.
  • The second number line also uses a linear scale, but applies a logarithm to the data, displaying \(\log_{10}(x)\). Data points are evenly distributed, with axis labels showing \(\log_{10}(x)\).
  • The third number line uses the original data \(x\) but switches to a logarithmic scale, with tick marks at \((1, 3.16, 10, 31.6, 100)\) (i.e., \((10^{0.0}, 10^{0.5}, 10^{1.0}, 10^{1.5}, 10^{2.0})\)). Data points are evenly distributed.
  • The fourth number line is the same as the third, but the axis label incorrectly reads \(\log_{10}(x)\).

Comparing the first and third number lines: for the same data, points that are unevenly distributed on a linear scale become evenly distributed on a logarithmic scale. This adjustment helps observe data that is unevenly distributed, especially data following an exponential growth trend.

Note that switching to a logarithmic scale only changes how the axis tick marks are displayed — it does not change the values of the data points. Therefore, for a chart that only changes the scale without modifying the original data, the axis title must not be written as \(\log_{10}(x)\) (see the fourth number line).

When to use a logarithmic scale and its trade-offs:

When to use

  • Data spans an extremely wide range (multiple orders of magnitude), such as population, income, distance, or mass
  • Relative change (multiplicative relationships) matters more than absolute differences
  • Data follows an exponential growth or decay trend (which appears more linear on a log scale)

Advantages

  • Compresses data range: allows very large and very small values to be shown together without large values “flattening” the overall structure
  • Improves visual distribution: reduces clustering in one interval, making the distribution more uniform
  • Highlights multiplicative comparisons: equal axis distances represent equal multiplicative change, better suited for certain analyses
  • Helps identify exponential relationships: exponential growth or decay is easier to observe on a log scale

Disadvantages

  • Not applicable to zero or negative values: the logarithm function is only defined for positive numbers
  • Lower readability: not intuitive for readers unfamiliar with logarithmic scales
  • May obscure absolute differences: compresses the high-value range, weakening the visual representation of raw differences

For example, Figure 18 shows data with a large range that is too concentrated on the left — switching to a logarithmic scale (Figure 19) improves the distribution.

planets = sns.load_dataset('planets')
g = sns.scatterplot(data=planets, x='distance', y='mass')
plt.title('Relationship between planetary distance and mass')
plt.show()
Figure 18: Scatter plot: planetary distance vs. mass, linear scale
planets = sns.load_dataset('planets')
g = sns.scatterplot(data=planets, x='distance', y='mass')
g.set_xscale('log')  # default base is 10; use the base parameter to change it
g.set_yscale('log')
plt.show()
Figure 19: Scatter plot: planetary distance vs. mass, logarithmic scale

Note: In Figure 19, the tick marks \((10^0, 10^1, 10^2, ...)\) are evenly spaced on a logarithmic scale. The minor tick marks between each pair of major tick marks represent linearly spaced actual values — for example, between \(10^0\) and \(10^1\) they are \(2, 3, ..., 9\), and between \(10^1\) and \(10^2\) they are \(20, 30, ..., 90\), and so on.

Because a logarithmic scale has no zero (with 1, i.e., the base raised to the power of 0, as the origin), a logarithmic scale is well suited for displaying ratio data. For example, Wilke (2019, pp.18-20) divides each county’s population in Texas by the median population as the y-axis, using a logarithmic scale (Figure 20) to show how each county’s population is distributed around the median (a ratio of 1 means the county’s population equals exactly the median).

Texas county population distribution — logarithmic scale
Figure 20: Texas county population distribution — logarithmic scale

If the data in Figure 20 were displayed with a linear scale, the origin would be 0, making it impossible to clearly show the distribution of values around 1 (Figure 21).

Texas county population distribution — linear scale
Figure 21: Texas county population distribution — linear scale

3.4 Regression Plot

A regression plot fits the scatter plot to a line, showing the relationship between two variables. The most common is linear fitting, which fits a linear regression line to the scatter plot. The confidence interval shows the uncertainty of the regression line (as an estimate): at a 95% confidence level, the true value has a 95% chance of falling within that interval. In Figure 22, the line is the linear regression line and the shaded area is the confidence interval.

sns.lmplot(data=df, x='bill_length_mm', y='bill_depth_mm', hue='sex')
plt.show()
Figure 22: Linear regression plot

sns.lmplot() (linear model plot) uses a 95% confidence interval by default to measure the uncertainty of the regression line.

Note: The hue parameter only works with discrete data.

4 Line Plot

In a scatter plot, data points exist independently and are typically used to explore the correlation or distribution between two variables, with no inherent ordering among data points. In a line plot, data points are connected by line segments to emphasize continuity or trend, typically used for time-series data to show how one variable changes over time, where data points have an inherent ordering.

For example, Figure 23 shows how monthly passenger counts for an airline change over the years:

sns.lineplot(
    data=flights, x='year', y='passengers',
    hue='month',
)
# note: year and passengers are not strictly continuous, but the data is sufficient to show trends
plt.show()
Figure 23: Line plot with colors distinguishing months

You can also display markers by setting markers=True and dashes=False (Figure 24):

sns.lineplot(
    data=flights, x='year', y='passengers',
    hue='month', style='month',
    markers=True,  # must be used together with style
    dashes=False,
)
plt.show()
Figure 24: Line plot with colors and marker shapes distinguishing months

When there are many data points, it is not recommended to display markers in a line plot — too many markers impede readability; clean lines better convey the trend (Wilke, 2019, pp. 135-137).

A similar effect can be achieved with a point plot (see Figure 25), but sns.pointplot() cannot use different marker shapes to distinguish variables and cannot remove markers, so sns.lineplot() is the better choice for observing trends over time.

sns.pointplot(
    data=flights, x='year', y='passengers', hue='month',
)
plt.show()
Figure 25: Point plot: passenger count trend over time

When the y-axis variable has multiple values at each time point and no color distinction is applied, sns.lineplot() automatically generates a mean line and error band (default 95% confidence level), see Figure 26:

sns.lineplot(data=flights, x='year', y='passengers')
plt.show()
Figure 26: Without hue, a mean line is generated (upper/lower edges indicate peak and off-peak seasons)

When using color to distinguish lines in a line plot, note:

  • The order of colors in the legend should match the order of the lines.
  • Choose colors that are accessible to color-vision-impaired readers.

In Figure 27, the legend and line color orders are inconsistent, which increases reading difficulty. Wilke (2019, p.248) provides a counter-example:

Counter-example: legend and line color order do not match
Figure 27: Counter-example: legend and line color order do not match

In contrast, Figure 28 has matching legend and line color orders. Wilke (2019, p.249) provides the improved chart:

Improved version: legend and line color order match (and colors are accessible to color-vision-impaired readers)
Figure 28: Improved version: legend and line color order match (and colors are accessible to color-vision-impaired readers)

Wilke (2019, p.250) also shows how Figure 28 appears under simulations of different color vision deficiencies and after desaturation (Figure 29):

Chart under color vision deficiency simulations and desaturation
Figure 29: Color vision deficiency simulations & desaturation (deuteranopia, protanopia, tritanopia, and desaturated)

From Figure 29, the color scheme in Figure 28 is distinguishable for all three types of color vision deficiencies, and remains distinguishable by lightness after desaturation. This is why matching legend and line color order is especially important.

Additionally, Wilke (2019, p.251) recommends removing the legend and labeling each line directly (Figure 30), which saves readers the effort of consulting a legend and makes the chart easier to read. However, this is difficult to implement in Seaborn, so in this course it is sufficient to use a legend with colors in the correct order as in Figure 28.

Remove legend and label each line directly
Figure 30: Remove legend and label lines directly

5 Heatmap

5.1 Pivot Table

Before creating a heatmap, the original data must first be reshaped into a pivot table. A pivot table presents two variables from the original table in matrix form: column names represent one variable and row names represent the other.

The pivot operation converts all unique values in a column of the original table into new column labels or row indices. For example, Table 3 converts all unique values of the 'year' column in Table 2 (1949–1960) into new column names, and the unique values of the 'month' column into row names.

The pivot and melt operations are opposites: pivot converts a “long” table into a “wide” table, while melt converts a “wide” table into a “long” table (melt turns column names into data values, increasing the number of rows).

flights.head()
Table 2: Original flights dataset (first five rows)
year month passengers
0 1949 Jan 112
1 1949 Feb 118
2 1949 Mar 132
3 1949 Apr 129
4 1949 May 121
# create pivot table (can also be named matrix)
flights_pivot = pd.pivot_table(
    flights,
    index='month',   # unique values of 'month' become row index
    columns='year',  # unique values of 'year' become column names
    values='passengers',
    observed=False,  # show all possible combinations of categorical variables
)

flights_pivot
Table 3: Pivot table: year & month vs. passenger count
year 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960
month
Jan 112.0 115.0 145.0 171.0 196.0 204.0 242.0 284.0 315.0 340.0 360.0 417.0
Feb 118.0 126.0 150.0 180.0 196.0 188.0 233.0 277.0 301.0 318.0 342.0 391.0
Mar 132.0 141.0 178.0 193.0 236.0 235.0 267.0 317.0 356.0 362.0 406.0 419.0
Apr 129.0 135.0 163.0 181.0 235.0 227.0 269.0 313.0 348.0 348.0 396.0 461.0
May 121.0 125.0 172.0 183.0 229.0 234.0 270.0 318.0 355.0 363.0 420.0 472.0
Jun 135.0 149.0 178.0 218.0 243.0 264.0 315.0 374.0 422.0 435.0 472.0 535.0
Jul 148.0 170.0 199.0 230.0 264.0 302.0 364.0 413.0 465.0 491.0 548.0 622.0
Aug 148.0 170.0 199.0 242.0 272.0 293.0 347.0 405.0 467.0 505.0 559.0 606.0
Sep 136.0 158.0 184.0 209.0 237.0 259.0 312.0 355.0 404.0 404.0 463.0 508.0
Oct 119.0 133.0 162.0 191.0 211.0 229.0 274.0 306.0 347.0 359.0 407.0 461.0
Nov 104.0 114.0 146.0 172.0 180.0 203.0 237.0 271.0 305.0 310.0 362.0 390.0
Dec 118.0 140.0 166.0 194.0 201.0 229.0 278.0 306.0 336.0 337.0 405.0 432.0

About the aggfunc parameter. The aggfunc (aggregation function) in pd.pivot_table() defaults to 'mean', meaning when a single cell corresponds to multiple rows in the original data, it takes the mean by default.

In Table 2, each combination of year and month has only one passenger value, so no averaging is needed. However, in the penguin dataset shown in Table 4, there is more than one penguin per species-island combination, and each combination maps to a single cell in the pivot table (Table 5), so the default mean is applied.

df.head()
Table 4
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 Male
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 Female
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 Female
3 Adelie Torgersen NaN NaN NaN NaN NaN
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 Female
# create pivot table using species and island columns from the penguin dataset
df_pivot = pd.pivot_table(
    df,
    index='species',
    columns='island',
    values='bill_length_mm',
)

df_pivot
Table 5: Pivot table: penguin species & island vs. bill length
island Biscoe Dream Torgersen
species
Adelie 38.975000 38.501786 38.95098
Chinstrap NaN 48.833824 NaN
Gentoo 47.504878 NaN NaN

5.2 Creating a Heatmap

Using Table 3 to create a heatmap, see Figure 31:

sns.heatmap(
    data=flights_pivot,
    cbar_kws={'label': 'passengers'},  # colorbar label
)
plt.show()
Figure 31: Basic heatmap: flights

.heatmap plots directly from a matrix-shaped table, rather than specifying particular columns through x/y parameters. cbar_kws (colorbar keyword arguments) sets the label for the colorbar.

Additional parameters such as cmap and annot can be used to further customize the heatmap (Figure 32):

sns.heatmap(
    data=flights_pivot,
    cbar_kws={'label': 'passengers'},
    cmap='viridis',  # Matplotlib built-in colormap
    annot=True,      # show values in each cell
    fmt='.0f',       # format: integer ('.nf' keeps n decimal places)
)
plt.show()
Figure 32: Heatmap with additional parameters

cmap accepts the name of a predefined color palette or colormap from Seaborn or Matplotlib, such as 'crest', 'flare', 'viridis', or 'pink'.

Similarly, using Table 5 to create a heatmap (Figure 33):

sns.heatmap(
    data=df_pivot,
    cmap='crest',
    cbar_kws={'label': 'bill_length_mm'},
    annot=True,
)
plt.show()
Figure 33: Penguin data heatmap

6 2D Histogram

According to Wilke (2019, pp. 222-225), scatter plots with semi-transparent points can show the distribution of data across two variables, but when data points are too numerous and dense, a scatter plot struggles to convey the overall distribution (see Figure 34: high-density areas appear as uniform dark blotches while low-density areas are nearly invisible).

Scatter plot with too many dense data points (Wilke, 2019, p. 223)
Figure 34: Scatter plot with too many dense data points (Wilke, 2019, p. 223)

In such cases, a 2D histogram can be used instead: the x-y plane is divided into small rectangles (bins), the number of data points falling into each bin is counted, and each rectangle is colored according to that count (see Figure 35):

sns.histplot(
    data=df, x='bill_depth_mm', y='bill_length_mm',
    cbar=True,  # show colorbar
    cbar_kws={'label': 'count'},
)
plt.show()
Figure 35: 2D histogram

Table 6 shows the differences between heatmaps and 2D histograms:

Table 6: Differences between heatmaps and 2D histograms
Property Heatmap 2D Histogram
Input data Pre-processed matrix (usually from a pivot table) Raw data
Axes Usually represent two discrete variables Usually represent two continuous variables
Number of variables Three: axes and color Two: axes only
Color meaning Magnitude of the value in each cell Number of data points in each bin
Layout Fixed rows and columns, fixed color-value mapping Shape can vary depending on the number of bins
Main use Show numerical patterns in a matrix Show the joint distribution density of two variables

7 Joint Plot

sns.jointplot() can simultaneously display the bivariate distribution of two variables along with their individual univariate distributions.

As shown in Figure 36, you can display a 2D histogram (same as Figure 35) while also showing the 1D histogram of each variable:

sns.jointplot(
    data=df, x='bill_depth_mm', y='bill_length_mm',
    kind='hist',  # default is 'scatter'; changed here to histogram
)
plt.show()
Figure 36: Joint plot: histogram form

You can also set kind='hex' to use hexagonal bins in the center panel (Figure 37):

sns.jointplot(
    data=df, x='bill_depth_mm', y='bill_length_mm',
    kind='hex',
)
plt.show()
Figure 37: Joint plot: hexagonal bin form

Color can also be used to distinguish variables (Figure 38):

sns.jointplot(
    data=df, x="bill_depth_mm", y="bill_length_mm",
    hue="species",
)
plt.show()
Figure 38: Joint plot: using hue (marginal plots automatically become density plots)

You can also use kind='kde' (kernel density estimation) to generate smoothed distribution curves (see Figure 39). However, smoothed curves are not actual data and can mislead readers, especially when there are few data points, so they are not recommended in this course.

sns.jointplot(
    data=df, x="bill_depth_mm", y="bill_length_mm",
    hue="species", kind='kde',
)
plt.show()
Figure 39: Joint plot: KDE smoothing (not recommended)

8 Pair Plot

sns.pairplot() makes it easy to plot pairwise distributions for multiple variables (i.e., multiple columns) in the original dataset, forming a grid of plots (Figure 40):

sns.pairplot(data=df, hue='species', height=1.7)
plt.show()
Figure 40: Pair plot (height adjusts the figure height)

In Figure 40, the four ratio variables in the original DataFrame are automatically detected and plotted: the diagonal plots are the univariate distributions of each variable, while the remaining subplots are bivariate distributions based on each pair of variables.

You can also modify the format of the diagonal and off-diagonal plots using diag_kind and kind (Figure 41):

sns.pairplot(
    data=df,
    hue='species',
    diag_kind='hist',  # format of diagonal plots
    kind='hist',       # format of off-diagonal bivariate plots
    height=1.7,
)
plt.show()
Figure 41: Pair plot: histogram form
Back to top