Data Analysis with Python (pandas, polars, DuckDB) Module 10 – Visualising data with matplotlib
Distributions, relationships and correlation charts
Compare distributions with histograms, box plots and ECDFs, show relationships with scatter plots and trend lines, and read correlation heatmaps carefully.
What you will learn
- Compare distributions with histograms, box plots and ECDFs
- Show relationships with scatter plots, transparency and trend lines
- Draw and read a correlation heatmap carefully
- Use seaborn for statistical plots when it saves effort, knowing its maintenance status
Before you start
What you’ll learn · Before you start
What you will learn
- Compare distributions with histograms, box plots and ECDFs
- Show relationships with scatter plots, transparency and trend lines
- Draw and read a correlation heatmap carefully
- Use seaborn for statistical plots when it saves effort, knowing its maintenance status
Before you start
On this page
Three training groups have forty invented scores each, and two hundred invented flats have paired areas and prices. One average cannot show a score distribution, and two separate averages cannot show which area belongs to which price. A histogram: A chart that groups numeric observations into intervals and represents their counts or density with adjoining bars. groups scores into bins; a box plot: A distribution summary that displays quartiles, a median and whiskers under a stated rule, with any separately drawn observations beyond them. compares medians, quartiles and whiskers; an empirical cumulative distribution function: For each value, the fraction of the observed sample at or below that value. gives the share at or below a value without binning.
For relationships, a scatter plot keeps each pair together. A fitted line summarises linear change, while Pearson correlation: A dimensionless measure of linear association between paired varying measurements, on a scale from minus one to one. measures standardised linear association. Neither proves cause. The examples draw the same score data three ways and the same flats as a scatter plot, so each summary can be compared with the observations it summarises.
Three charts answer different questions about the same scores
The data are invented, from fixed seeds, so that every run gets the same numbers: 40 students in each of three sections, and 200 flats with their area, price, age and distance to a metro station.
"""The invented data of this lesson, made from fixed seeds so that every run gets the same numbers."""
import numpy as np
import pandas as pd
def exam_scores() -> pd.DataFrame:
"""Scores out of 100 for three sections of 40 students each."""
rng = np.random.default_rng(2026)
a = rng.normal(64, 10, 40) # one broad group
b = np.concatenate([rng.normal(46, 6, 20), rng.normal(79, 6, 20)]) # two groups of students
c = 90 - rng.gamma(2.0, 8.0, 40) # most score high, a long tail below
scores = np.clip(np.concatenate([a, b, c]), 0, 100).round()
return pd.DataFrame({"section": np.repeat(["A", "B", "C"], 40), "score": scores})
def flats() -> pd.DataFrame:
"""200 flats: floor area in square feet, price in millions, age in years, km to the nearest metro station."""
rng = np.random.default_rng(7)
area = rng.uniform(450, 1800, 200).round()
age = rng.integers(0, 30, 200)
metro = rng.uniform(0.2, 6.0, 200).round(1)
price = (0.8 + 0.0058 * area - 0.04 * age - 0.25 * metro + rng.normal(0, 0.6, 200)).round(2)
return pd.DataFrame({"area": area, "price": price, "age": age, "metro_km": metro}) import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import MaxNLocator
from lesson_data import exam_scores
plt.style.use("house.mplstyle")
scores = exam_scores()
groups = {name: g["score"].to_numpy() for name, g in scores.groupby("section")}
styles = {"A": ("#6b7280", "-"), "B": ("#1d5fcb", "--"), "C": ("#1c2027", ":")}
fig, (hist, box, ecdf) = plt.subplots(1, 3, figsize=(8.4, 3.2), layout="constrained")
for name, values in groups.items():
colour, dash = styles[name]
hist.hist(values, bins=range(20, 101, 8), histtype="step", color=colour, linestyle=dash, linewidth=2, label=name)
ecdf.ecdf(values, color=colour, linestyle=dash, label=f"Section {name}")
box.boxplot(list(groups.values()), tick_labels=list(groups), orientation="vertical", widths=0.5)
hist.set(title="Histogram", xlabel="Score", ylabel="Students")
hist.yaxis.set_major_locator(MaxNLocator(integer=True))
box.set(title="Box plot", ylabel="Score")
ecdf.set(title="ECDF", xlabel="Score", ylabel="Share at or below")
ecdf.legend(loc="upper left", fontsize=10)
fig.savefig("three-views.svg", metadata={"Date": None})
print("section median Q1 Q3 IQR lowest below 50")
for name, values in groups.items():
q1, median, q3 = np.percentile(values, [25, 50, 75])
print(f"{name:>7} {median:7.1f} {q1:5.1f} {q3:5.1f} {q3 - q1:5.1f} {values.min():7.0f} {np.mean(values < 50):8.0%}") Output
section median Q1 Q3 IQR lowest below 50
A 63.0 59.0 70.2 11.2 45 2%
B 67.0 49.5 75.2 25.8 41 25%
C 76.5 67.5 83.0 15.5 53 0%
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 three_views.py
The figure three_views.py draws
Text description of the diagram
Three charts of the same 120 exam scores, 40 per section. Section A is drawn as a solid grey line, section B as a dashed blue line and section C as a dotted dark line.
- Histogram, bins 8 points wide from 20 to 100: A has one hump around 60 to 68; B has two humps, one in the 40s and one in the high 70s, with few students between them; C rises steadily towards the 80s.
- Box plot: A's box runs from 59 to 70.2 with the median at 63; B's box is the widest, from 49.5 to 75.2 with the median at 67, and shows nothing of B's two groups; C's box runs from 67.5 to 83 with the median at 76.5.
- ECDF, the share of students at or below each score: B's curve climbs steeply in the 40s, flattens through the 50s and 60s, then climbs again in the 70s, which is the shape of two groups; C's curve stays lowest until the 70s because most of C scored high.
Distributions show the spread of observations, here forty scores per section. Compare these distributions with three complementary views of the same data. The histogram shows the shape: section A has one hump in the 60s, and section B has two, one in the 40s and one in the high 70s. The box plot compares the sections in one glance: C’s median of 76.5 is the highest, and B’s box, from 49.5 to 75.2, is the widest, but the box says nothing about B’s two groups. The ECDF shows B’s two steep stretches and the gap between them; 25 % of section B is below 50. Reading its lower inverse at 0.5 gives the twentieth ordered observation. For C that is 76, while the usual interpolated sample median averages 76 and 77 to give 76.5. State the percentile convention rather than calling those different calculations identical.
| Criterion | Histogram | Box plot | ECDF |
|---|---|---|---|
| Shows | The shape: humps, gaps, skew | Median, quartiles, range, outliers | Every value, as the share at or below it |
| Hides | Detail inside each bin | The shape between the quartiles | Nothing, but shape is harder to see |
| A choice you make | The bin width | The whisker rule | None |
| Groups at once | Two or three | Many, side by side | Two to four |
| When to choose | The shape of one distribution | Comparing many groups | Percentiles, and comparing a few groups exactly |
Follow section C’s observations into their different summaries.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Keep forty scores. All three views read the same group's observations.
- Read each summary. Histogram counts depend on bins; quartiles summarise a box; the ECDF reports cumulative shares.
- State the convention. C's lower inverse-CDF p50 is 76; its interpolated median is 76.5.
The calculation’s name and convention are part of the result, especially when two middle observations differ.
Key idea
Checkpoint
Check: Three distribution views
A scatter plot preserves the pair that a fitted line summarises
For relationships between two numbers of each item, the scatter plot is the chart. Here each of the 200 flats is a dot placed by its area and its price, with a straight trend line fitted by least squares:
import matplotlib.pyplot as plt
import numpy as np
from lesson_data import flats
plt.style.use("house.mplstyle")
homes = flats()
slope, intercept = np.polyfit(homes["area"], homes["price"], deg=1) # highest power first
r = homes["area"].corr(homes["price"])
fig, ax = plt.subplots()
ax.scatter(homes["area"], homes["price"], s=16, alpha=0.6, color="#1d5fcb")
xs = np.array([450, 1800])
ax.plot(xs, intercept + slope * xs, color="#1c2027", linewidth=2)
ax.set(xlabel="Floor area (square feet)", ylabel="Price (millions)")
ax.set_title(f"About {slope * 100:.2f} million more per 100 square feet")
fig.savefig("scatter-trend.svg", metadata={"Date": None})
print(f"trend line: price = {intercept:.2f} + {slope:.5f} x area")
print(f"Pearson correlation r = {r:.2f}, so r squared = {r * r:.2f}")
print(f"the line's price for 1,000 square feet: {intercept + slope * 1000:.2f} million") Output
trend line: price = -0.72 + 0.00586 x area Pearson correlation r = 0.95, so r squared = 0.91 the line's price for 1,000 square feet: 5.15 million
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 scatter_trend.py
The chart scatter_trend.py draws
Text description of the diagram
A scatter plot of 200 invented flats: floor area from 455 to 1,794 square feet on the x-axis, price in millions on the y-axis, each flat a semi-transparent blue dot.
The dots rise from about 1 to 3 million for the smallest flats to about 9 to 10 million for the largest, in a band roughly 1.5 million wide. A dark trend line, fitted with np.polyfit, runs through the middle of the band: price = −0.72 + 0.00586 × area, about 0.59 million more for every 100 square feet. The correlation is r = 0.95.
np.polyfit(x, y, deg=1) returns the coefficients highest power first, so the slope comes before the intercept: each
extra square foot adds 0.00586 million on average, about 0.59 million per 100 square feet. The correlation, r = 0.95,
says how strongly the two variables move together linearly after their scales are standardised, from −1 to +1.
It does not measure prediction error in millions: changing the currency changes those distances while leaving r
unchanged. Three tools help when scatter plots get
harder. Transparency (alpha) draws points partly see-through, so places where many overlap look darker. hexbin
counts the points in hexagonal cells and shades each cell by its count. Polynomial.fit from numpy.polynomial fits
the same line, and NumPy’s documentation recommends it for new code because it is numerically more stable.
Follow the flat observations before interpreting the fitted line’s number.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Place two hundred pairs. Each flat is located by its paired measurements.
- Fit a summary. Least squares fits one straight line in the stated measurement units.
- Keep distinct meanings. Correlation standardises scales; a residual retains price units and assesses prediction error.
A high r is not a price-per-square-foot estimate or a measure of error in millions.
Key idea
Checkpoint
Check: Paired points and linear fit
A correlation matrix is a map to inspect valid pairs
Correlation charts compare columns in pairs to show which move together. These correlation charts summarise association rather than cause. Their numbers come from DataFrame.corr():
from lesson_data import flats
homes = flats()
print(homes.corr().round(2))
print()
print("strongest with price:", homes.corr()["price"].drop("price").abs().idxmax()) Output
area price age metro_km area 1.00 0.95 0.02 0.01 price 0.95 1.00 -0.13 -0.14 age 0.02 -0.13 1.00 0.04 metro_km 0.01 -0.14 0.04 1.00 strongest with price: area
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 corr_matrix.py
A heatmap colours each cell of this matrix, so a table of 30 columns, 435 pairs, can be scanned in seconds. The diagonal is 1 for nonconstant columns with enough paired observations. A constant or all-missing column instead has undefined correlations, which pandas represents as NaN. The matrix is symmetric, so half of it repeats the other half. Read correlation charts with care: Pearson’s r summarises standardised linear association, so a strong curve can score near 0, one far point can create a large r on its own, and none of it says which column causes which. Here area is the only strong partner of price, while age and the distance to the metro pull the price down only slightly.
seaborn builds on matplotlib and draws each of these statistical charts in one call, with grouping by a column and a legend built in. Its most recent release is 0.13.2, and it still works with pandas 3, so it is worth using when it saves code; the charts in this lesson use matplotlib directly, which does not depend on seaborn’s release pace.
Trace a Pearson cell from row pairs to its meaning.
The drawing shows the whole scene; its 3 steps are listed below.
Every step as text
- Select a pair. Use the same row's values only when both are observed.
- Check variation. A constant or all-missing Pearson column has undefined correlations.
- Inspect the plot. A matrix cell points to a scatter plot and an investigation rather than a causal conclusion.
The valid pair count and the pattern behind the cell matter as much as its displayed colour.
Key idea
Checkpoint
Check: Pairwise correlation
What each distribution chart can hide, reproduced Pro: opens with a Pro, Premium or Ultimate pass
Every distribution chart makes a choice for you: the bin width, the whisker rule, the definition of a percentile. This part reproduces what each choice can hide, works one box plot out by hand, and reads exact percentiles off an ECDF.
In this part:
- Changing bins can merge or split the same observed groups
- Identical box summaries can hide very different score locations
- Whiskers stop at observed scores within their fences
- Percentile methods can return different valid sample summaries
6 diagrams · 4 runnable examples (Python) · about 9 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Scatter plots, trend lines and correlation heatmaps without the traps Pro: opens with a Pro, Premium or Ultimate pass
Relationship charts fail in quieter ways: points hide each other, lines run past the data, and a correlation misses the shape. This part measures each failure, shows the chart or the number that catches it, and then draws the whole panel in seaborn.
In this part:
- Opacity saturates before a dense scatter plot reveals its counts
- A fitted line needs its observed range and residual scale
- Ranks and linear correlation summarise different patterns
- A fixed minus-one to one scale preserves correlation colour meaning
- A plotting wrapper still depends on its underlying API versions
- Your turn: a 2 × 2 exploration panel, checked by its objects
5 diagrams · 7 runnable examples (Python) · about 11 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practice
Read the score summaries and paired flat measurements before answering. Distinguish a plotted percentile from an interpolated median and a correlation from a causal claim.
Practice that adapts to you
Practise this lesson
6 questions about 12 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
-
Fresher · Warm-up
When would you show a distribution as a box plot rather than a histogram, and what can a box plot hide?
Use a box plot when you compare many groups at once. One narrow column per group shows the median, the middle half of the values and the extremes, so ten groups fit side by side on one scale, where ten histograms would need ten panels or would overlap into a tangle. A histogram is better for one or two groups when the shape matters: whether the values form one hump or two, or whether there is a long tail of large values. That shape is what a box plot can hide. Two groups of values with a gap between them give a wide box, and nothing in the box shows the gap. When a box is unusually wide, draw the histogram or the ECDF beside it.
-
Fresher to mid
Two analysts plot the same 400 delivery times. One histogram shows one peak, the other shows two. Who is right, and how do you decide what to show the operations team?
-
Mid to senior
A correlation heatmap of 30 product metrics shows r = 0.02 between discount depth and units sold, and a manager concludes that discounts do not affect sales. What do you check before agreeing, and what do you show instead?
-
Mid to senior
A scatter plot of 2 million orders, order value against delivery time, is one solid blob. How do you show the relationship, and how do you check whether a trend line through it means anything?
Key takeaways
- A histogram shows a distribution’s shape, a box plot compares many groups, and an ECDF shows every value and its percentiles without bins.
- A scatter plot shows a relationship;
np.polyfitreturns the slope first, and r measures standardised linear association rather than prediction error in the measured unit. - A Pearson correlation chart summarises linear association for each valid pair, so inspect curves, missing pairs and extreme observations before interpreting it.
- seaborn draws these charts in fewer lines on top of matplotlib.
Terms in this lesson
- box plot
- A distribution summary that displays quartiles, a median and whiskers under a stated rule, with any separately drawn observations beyond them.
- empirical cumulative distribution function
- For each value, the fraction of the observed sample at or below that value.
- histogram
- A chart that groups numeric observations into intervals and represents their counts or density with adjoining bars.
- Pearson correlation
- A dimensionless measure of linear association between paired varying measurements, on a scale from minus one to one.
Corrections
- Added scenes, checkpoints and adaptive practice; clarified correlation and percentile limits while retaining the score and flat data, chart programs and Figure worksheet.
References
- matplotlib.axes.Axes.hist (Matplotlib development team)
- matplotlib.axes.Axes.boxplot (Matplotlib development team)
- matplotlib.axes.Axes.ecdf (Matplotlib development team)
- matplotlib.axes.Axes.hexbin (Matplotlib development team)
- numpy.histogram_bin_edges (NumPy developers)
- numpy.polyfit (NumPy developers)
- numpy.quantile (NumPy developers)
- pandas.DataFrame.corr (pandas development team (NumFOCUS))
- seaborn: statistical data visualization (seaborn developers)
- seaborn on the Python Package Index (Python Software Foundation (PyPI))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress