Chris Parmer

Chris Parmer

November 12, 2026

Regression Plots - A Complete Guide

TL;DR

  • A regression plot is a scatter plot with a fitted line or curve through it, usually an ordinary least squares (OLS) line, often with a shaded confidence band, so you can read how much y changes for each unit of x and how well that describes the points.
  • Regression plots are also known as scatter plots with a trendline, regplots (after seaborn's regplot, with lmplot for several panels), fitted line plots and lines of best fit. They are similar to scatter plots, marginal plots, residual plots and binned scatter plots.
  • Read the slope first (24% cheaper solar modules per doubling of installed capacity, 2.17 megawatts lost per degree of warmer air), then R², then the residuals, because four very different data sets can share one line (Anscombe's quartet).
  • You can make one with Plotly in one line of code with px.scatter(df, x=..., y=..., trendline="ols"), switch to "lowess", "rolling", "ewm" or "expanding", and read the fitted model back with px.get_trendline_results.

What is a regression plot?

Regression plots look like this:

Regression plot of solar photovoltaic module prices against cumulative installed capacity on log scales, 1975 to 2025: 51 yearly points fall along a straight orange least-squares line with a narrow blue 95% confidence band, from $132.38 per watt in 1975 to $0.12 in 2025, with 2008 at $4.75 above the line, and the fit stating that each doubling of capacity cut the price by 24% with R² = 0.97

Data: Our World in Data, solar photovoltaic module prices (constant 2025 US dollars) and cumulative installed capacity, from the International Renewable Energy Agency (IRENA), Nemet (2009) and Farmer and Lafond (2016).

How do you read a regression plot? In the example above, notice these things:

  • Each dot is one year from 1975 to 2025. Its x position is how many megawatts (MW) of solar panels the world had installed by then, and its y position is what a watt of module cost that year, both on log scales.
  • The orange line is the least-squares fit to the logs. Its slope, −0.402, means that each doubling of installed capacity cut the price by 24% (1 − 2^−0.402), a pattern called Swanson's law.
  • R² = 0.97 says the line accounts for 97% of the variation in the log price. The shaded band is the 95% confidence band for the line, and it's narrow because 51 points pin a straight line down well.
  • The points the line misses tell their own story: from 2005 to 2011 prices sat well above it (2008 at $4.75 a watt, 2.2 times the fitted value), and by 2025 they were below it at $0.12, under half of the line's $0.28.

It's often used for charts like these:

Regression plot of 934 adult children's heights against their mid-parent height from Galton's 1886 data: a cloud of jittered blue points with an orange least-squares line of slope 0.71 that is flatter than the dashed y = x line, and labels showing that children of mid-parents 72 inches or taller averaged 72.0 inches while their parents averaged 73.0

Heredity and regression toward the mean, the classic example. The least-squares line through Galton's 934 children has a slope of 0.71, flatter than the y = x line, so children of tall parents were tall but closer to average: mid-parents of 73.0 inches had children of 72.0. Data: Galton (1886), as transcribed in the R package HistData (GaltonFamilies).

Okun's law regression plot of US real GDP growth against the change in the unemployment rate, 1949 to 2024: 76 yearly points fall around a downward orange line with a blue confidence band, crossing zero at 3.1% growth, with 2020, 2009, 1982, 2021 and 1950 labeled

Economics and finance. Okun's law, the link between growth and jobs, is a regression line: each extra point of real gross domestic product (GDP) growth went with a 0.42-point fall in US unemployment from 1949 to 2024, and unemployment held steady at 3.1% growth. Data: Federal Reserve Economic Data (FRED), GDPCA from the Bureau of Economic Analysis and UNRATE from the Bureau of Labor Statistics.

Calibration curve of a load cell from NIST: 40 readings of deflection against load fall on a line, and a residual panel below shows the straight line's residuals arching from -4.3 up to +2.4 thousandths and back down while the quadratic's residuals stay within half a thousandth of zero

Calibration curves in labs and test benches. A straight line fits this load cell to R² = 0.99999, but its residuals arch, so the US National Institute of Standards and Technology (NIST) certifies a quadratic, whose residuals are 10 times smaller. Data: NIST Statistical Reference Datasets, Pontius load cell calibration.

How a regression line is fit

A regression plot answers one question: if x goes up by one unit, how much does y go up or down on average? The line is the answer, and the rest of the chart (the band, R², the residuals) tells you how far to trust it. These four ideas are all you need to read one, and each is shown on real data below.

Least squares: the line with the smallest squared misses

For any straight line, each point has a residual, its vertical distance from the line (observed y minus the line's y at that x). Ordinary least squares picks the one line that makes the sum of the squared residuals as small as possible. Squaring makes misses above and below the line count the same and punishes big misses more than small ones:

Anscombe's first data set of 11 points with the orange least-squares line y = 3.00 + 0.500x and each residual drawn as a red vertical segment, the point at x = 13 labeled observed 7.58 minus fitted 9.50 = -1.92, and the sum of squared residuals 13.76

Data: Anscombe (1973), via the R datasets package.

For a straight line the answer has a short formula: the slope is the covariance of x and y divided by the variance of x, and the line passes through the point (mean of x, mean of y). Here that gives y = 3.00 + 0.500x, and no other straight line has a smaller sum of squared residuals than 13.76. Because squares grow fast, one point far from the others can pull the line toward it, which is why you look at the plot before you trust the numbers.

R²: how much of the spread the line explains

R² (R-squared, the coefficient of determination) compares two totals. The first is the sum of squared distances from each point to the mean of y, the spread you would have with no line at all. The second is the sum of squared residuals the line leaves over:

Two panels of Anscombe's first data set: on the left, gray squares from each point to the dashed mean line, totaling 41.27; on the right, orange squares from each point to the least-squares line, totaling 13.76, giving R² = 1 - 13.76 / 41.27 = 0.67

Data: Anscombe (1973), via the R datasets package.

R² is 1 minus the second total divided by the first: 1 − 13.76 / 41.27 = 0.67, so the line removes two-thirds of the squared spread. R² runs from 0 (the line does no better than the mean) to 1 (every point is on the line). What counts as high depends on the field: 0.25 for Galton's heights is normal for people, where much of the variation is individual, while a calibration curve below 0.999 would be a problem. A high R² doesn't mean a straight line is the right shape, as the load cell and Anscombe's quartet below show.

Confidence band and prediction band

The shaded band around a regression line is usually a 95% confidence band: if you collected new data the same way many times, the true line would fall inside the band 95% of the time. A prediction band is wider, because it adds the scatter of single points around the line, and it answers a different question, where the next observation is likely to land:

Anscombe's first data set with the least-squares line, a blue 95% confidence band that is narrowest at x = 9 and widens to both ends, and a much wider orange 95% prediction band; at x = 9 the confidence band runs from 6.66 to 8.34 and the prediction band from 4.58 to 10.42

Data: Anscombe (1973), via the R datasets package.

At x = 9, the middle of the data, the line is pinned between 6.66 and 8.34, but a new point could land anywhere from 4.58 to 10.42. Both bands are narrowest at the mean of x and widen toward the ends, because a small change in the slope moves the line most far from the middle. With more points the confidence band shrinks toward the line, while the prediction band never gets narrower than the scatter itself. Use the confidence band to say how well you know the slope, and the prediction band to say how far a single season, batch or patient can stray, as in the baseball example below.

Residual plots: what the line missed

A residual plot draws the residuals against x (or against time, or against the fitted values) with a line at zero. If the model has the right shape, the residuals form a flat band with no pattern. A curve means the shape is wrong, and a funnel means the spread changes with x:

Two residual plots for the monthly Keeling curve: the residuals of a straight line form a U, averaging about +10 ppm in the late 1950s, -5 in the 1990s and +11 in 2025 and 2026, while the residuals of a quadratic fit form a flat band between about -4 and +4 ppm that wiggles with the seasons

Data: NOAA Global Monitoring Laboratory and Scripps Institution of Oceanography, monthly mean carbon dioxide (CO₂) at Mauna Loa.

A straight line through 68 years of CO₂ readings has an R² of 0.975, which sounds excellent, but its residuals form a U: the line is too high in the middle and too low at both ends, because CO₂ is rising faster each decade. A quadratic, a curve with one bend, removes the U and leaves mostly the yearly cycle of plants growing and decaying, with a residual standard deviation of 2.2 parts per million (ppm) instead of 5.2. To check whether the residuals also follow a normal distribution, the assumption behind the bands, draw them on a Q-Q plot.

A short history

Adrien-Marie Legendre published the method of least squares in 1805, fitting the shape of the Earth from survey measurements, and Carl Friedrich Gauss used it for planetary orbits in 1809 (Wikipedia: Least squares). The word regression comes from Francis Galton's 1886 paper Regression towards mediocrity in hereditary stature, the heights shown above, where the fitted line was flatter than y = x. William Cleveland's LOWESS paper (1979) gave the smooth curve most tools draw today. The best-known argument for drawing the plot instead of reporting only the numbers is Francis Anscombe's 1973 quartet, redrawn here from his published values:

Anscombe's quartet redrawn as four panels, each with 11 points and the same orange line y = 3.00 + 0.50x and R² = 0.67: set I a loose straight cloud, set II a smooth curve, set III a straight line with one outlier, and set IV ten points stacked at x = 8 with one point at x = 19

Data: Anscombe (1973), The American Statistician, via the R datasets package.

All four sets have the same means, the same least-squares line, y = 3.00 + 0.50x, and the same R² of 0.67, but only the first is described well by that line. The second needs a curve, the third has one outlier tilting the line, and in the fourth a single point decides the slope.

Alternatives to a regression plot

Here's the same 9,568 hours of a gas-fired power plant drawn six ways. Some are better for the question, some worse, some just different:

Six panels of the same 9,568 hourly power plant readings of output against temperature: a plain scatter plot, a scatter plot with a black OLS line, a scatter plot with an orange LOWESS curve that bends at about 25 degrees, a binned scatter of 25 bin means with error bars that flattens above 30 degrees, a 2D histogram with the OLS line, and a residual plot that sags below zero between about 18 and 27 degrees and climbs above 30
  • Scatter plot alone (worse for the slope, works too for a first look). It shows the shape and the outliers, but readers have to guess the slope by eye, and with 9,568 points the dense middle hides how many points are where.
  • LOWESS curve (better when the shape isn't a straight line). Locally weighted scatterplot smoothing (LOWESS) follows the data without assuming a shape. Here it shows output falling about 2.3 MW per degree up to 25 °C and only 1.1 above it, which the straight line misses. It gives no single slope to report.
  • Binned scatter plot (better for big data and for showing curvature). Averaging y in 25 bins of x, with error bars, turns thousands of points into a few means you can read exactly, and it makes the flattening above 30 °C obvious. You lose the individual points and the outliers.
  • 2D histogram or hexbin plot with the line (better for overplotting). Counting points per cell shows where most hours are, which a cloud of transparent dots only hints at.
  • Residual plot (different, and the one to draw next). It removes the trend so you can see what's left: here the residuals sag between about 18 and 27 °C and climb above 30 °C, so a curve would fit better than a line.
  • Marginal plot (works too). A histogram on each edge of a regression plot adds the distribution of each variable, like seaborn's jointplot(kind="reg").

How to make a regression plot

With AI

ChatGPT, Claude Code, GitHub Copilot, Cursor and other AI tools are excellent at writing Plotly code. Name Plotly in the prompt, and say which kind of fit you want, whether to transform the axes and which numbers from the fit to show:

Make a plotly regression plot from solar.csv (columns: year, price_usd_per_w, cumulative_mw).
- x = cumulative_mw and y = price_usd_per_w, both on log axes, one marker per year with the year in the hover text.
- Fit an ordinary least squares line to log10(price) on log10(capacity) with statsmodels and draw it with a
95% confidence band from get_prediction(...).summary_frame(alpha=0.05), filled with fill="toself".
- Write the slope, R² and the learning rate (1 - 2**slope, the price drop per doubling of capacity) on the chart.
- Axis titles with units, and a title that states the takeaway.
- Check: print the slope, R² and learning rate, and the year with the largest residual.

With Plotly Studio

Plotly Studio is a desktop app where you point an AI at a dataset and it builds charts and a Dash app around your questions. Ask for a regression plot the same way:

Show net electrical output against ambient temperature as a regression plot with an OLS trendline and its slope in MW per °C, a dropdown to switch the trendline between OLS, LOWESS and a rolling mean, and a residual plot underneath.

Make a regression plot in Python or JavaScript

This is the code AI tools write under the hood. In Python, Plotly Express adds a fitted line to a scatter plot with the trendline argument and fits it with statsmodels, so you need pip install plotly statsmodels. The trendline documentation covers every option:

import pandas as pd
import plotly.express as px
df = pd.read_csv("power_plant.csv") # temp_c, output_mw: 9,568 hourly readings
fig = px.scatter(df, x="temp_c", y="output_mw", trendline="ols", trendline_color_override="black",
opacity=0.3, render_mode="webgl",
labels={"temp_c": "Ambient temperature (°C)", "output_mw": "Net output (MW)"})
fig.show()
fit = px.get_trendline_results(fig).px_fit_results.iloc[0] # a statsmodels OLS result
print(fit.params) # intercept and slope: [497.03, -2.17]
print(fit.rsquared) # 0.899
print(fit.summary()) # standard errors, confidence intervals, p-values

px.get_trendline_results(fig) returns a table with one fitted model per trace, so with color= or facets you get one row per group. The same argument draws five other kinds of trendline, shown here on the monthly CO₂ record:

Six small panels of monthly CO2 at Mauna Loa from 1958 to 2026, each with an orange trendline: a straight OLS line that misses the curve at both ends, a smooth LOWESS curve, a 12-month rolling mean that follows the data, an exponentially weighted mean that lags slightly below, an expanding mean that lags far below and ends near 360 ppm, and an OLS fit with log_y that curves gently

Data: NOAA Global Monitoring Laboratory and Scripps Institution of Oceanography.

import pandas as pd
import plotly.express as px
co2 = pd.read_csv("co2.csv") # decimal_date, co2_ppm: monthly, 1958 onward
mammals = pd.read_csv("mammals.csv") # body_mass_g, metabolic_rate_w
penguins = pd.read_csv("penguins.csv") # species, bill_length_mm, bill_depth_mm
# A smooth local fit; frac is the share of the points each local fit uses
px.scatter(co2, x="decimal_date", y="co2_ppm", trendline="lowess", trendline_options=dict(frac=0.3))
# Moving averages use pandas: rolling, ewm and expanding take the same arguments as in pandas
px.scatter(co2, x="decimal_date", y="co2_ppm", trendline="rolling", trendline_options=dict(window=12))
px.scatter(co2, x="decimal_date", y="co2_ppm", trendline="ewm", trendline_options=dict(halflife=24))
px.scatter(co2, x="decimal_date", y="co2_ppm", trendline="expanding")
# A power law: fit OLS to log x and log y, and draw it on log axes so it's straight
px.scatter(mammals, x="body_mass_g", y="metabolic_rate_w", log_x=True, log_y=True,
trendline="ols", trendline_options=dict(log_x=True, log_y=True))
# One line per color by default; trendline_scope="overall" fits one line to all the points
px.scatter(penguins, x="bill_length_mm", y="bill_depth_mm", color="species", trendline="ols")
fig = px.scatter(penguins, x="bill_length_mm", y="bill_depth_mm", color="species", trendline="ols",
trendline_scope="overall", trendline_color_override="black")
fig.show()

"rolling" is a moving average over a window of points, "ewm" is an exponentially weighted moving average (EWM) that gives recent points more weight, and "expanding" is the running average of every point so far, which lags behind any trend and suits a stable process better. These three follow the order of the rows, so sort by x first.

Plotly Express doesn't shade a confidence band the way seaborn's regplot does, but statsmodels computes both bands and Plotly draws them as a filled trace, which also lets you choose the confidence level:

import numpy as np
import pandas as pd
import plotly.graph_objects as go
import statsmodels.api as sm
df = pd.read_csv("solar.csv") # year, price_usd_per_w, cumulative_mw
x, y = np.log10(df["cumulative_mw"]), np.log10(df["price_usd_per_w"])
model = sm.OLS(y, sm.add_constant(x)).fit()
grid = np.linspace(x.min(), x.max(), 100)
band = model.get_prediction(sm.add_constant(grid)).summary_frame(alpha=0.05)
# band has mean, mean_ci_lower/upper (confidence band) and obs_ci_lower/upper (prediction band)
fig = go.Figure([
go.Scatter(x=np.r_[grid, grid[::-1]], y=np.r_[band.mean_ci_upper, band.mean_ci_lower[::-1]],
fill="toself", fillcolor="rgba(42,120,214,0.2)", line_width=0, name="95% confidence band"),
go.Scatter(x=grid, y=band["mean"], mode="lines", line_color="#eb6834", name="Least-squares fit"),
go.Scatter(x=x, y=y, mode="markers", text=df["year"], name="Years"),
])
fig.update_layout(xaxis_title="log10 cumulative capacity (MW)", yaxis_title="log10 price ($/W)")
fig.show()
print(f"learning rate: {1 - 2 ** model.params.iloc[1]:.1%} per doubling") # 24.3%

For a residual plot, fit the model with statsmodels' formula interface, which also handles curves such as I(x**2), and plot model.resid:

import pandas as pd
import plotly.express as px
import statsmodels.formula.api as smf
co2 = pd.read_csv("co2.csv")
straight = smf.ols("co2_ppm ~ decimal_date", data=co2).fit()
curved = smf.ols("co2_ppm ~ decimal_date + I(decimal_date**2)", data=co2).fit()
co2["straight line"] = straight.resid
co2["quadratic"] = curved.resid
long = co2.melt(id_vars="decimal_date", value_vars=["straight line", "quadratic"],
var_name="fit", value_name="residual")
fig = px.scatter(long, x="decimal_date", y="residual", facet_row="fit")
fig.add_hline(y=0, line_dash="dash")
fig.show()

In JavaScript, plotly.js draws a regression plot from a markers trace and a lines trace once you've computed the least-squares line, which takes a few lines:

<div id="chart"></div>
<script src="https://cdn.plot.ly/plotly-4.1.1.min.js"></script>
<script>
// Anscombe's first data set (Anscombe 1973)
const x = [10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5];
const y = [8.04, 6.95, 7.58, 8.81, 8.33, 9.96, 7.24, 4.26, 10.84, 4.82, 5.68];
// Least squares: slope = covariance(x, y) / variance(x), and the line passes through the means
const mean = a => a.reduce((s, v) => s + v, 0) / a.length;
const mx = mean(x), my = mean(y);
const sxy = x.reduce((s, xi, i) => s + (xi - mx) * (y[i] - my), 0);
const sxx = x.reduce((s, xi) => s + (xi - mx) ** 2, 0);
const slope = sxy / sxx, intercept = my - slope * mx;
const sse = x.reduce((s, xi, i) => s + (y[i] - intercept - slope * xi) ** 2, 0);
const sst = y.reduce((s, yi) => s + (yi - my) ** 2, 0);
const r2 = 1 - sse / sst;
Plotly.newPlot("chart", [
{x: x, y: y, mode: "markers", marker: {size: 10}, name: "Data"},
{x: [4, 14], y: [intercept + slope * 4, intercept + slope * 14], mode: "lines", name: "Least squares"}
], {
width: 800, height: 500, showlegend: false,
title: {text: `y = ${intercept.toFixed(2)} + ${slope.toFixed(3)}x, R² = ${r2.toFixed(2)}`},
xaxis: {title: {text: "x"}}, yaxis: {title: {text: "y"}}
});
</script>

Share it

Every Plotly chart has a Share chart button (the cloud icon in its toolbar) that uploads it to Plotly Cloud and gives you a link. Only you can see it until you change its visibility.

For a live app, put the chart in a Dash app and publish it with the Plotly Cloud command-line interface (CLI). Coding agents can run these commands for you:

pip install "dash[cloud]"
plotly user login
plotly app publish

If your data has to stay in your own cloud or data center, publish to Dash Enterprise instead.

Dos and don'ts

Most regression plot mistakes are about the model rather than the styling: fitting a straight line to data that isn't straight, drawing it past the data and reporting more digits than the data support. Here is the solar price data both ways, in the same colors.

Don't fit a straight line to raw values that span six orders of magnitude, extend it past the data until it predicts negative prices, print an unrounded equation and an R² of 0.06306247, keep the raw column names as axis titles or use a default serif font:

The solar module price data on linear axes: all but a few points crushed against the left edge and the bottom, a straight orange line that crosses zero at about 1.4 million MW and keeps falling to -28 at 3.5 million, the equation y = -0.0000129008x + 17.50925618 with R² = 0.06306247, and the axis titles cumulative_capacity and cost in a serif font

Do transform the axes until the relationship is straight (here log-log, the scale learning curves live on), fit the line on that scale, keep it within the range of the data, add a confidence band, write axis titles with units and state the fit in words:

The same data on log-log axes with the points along an orange least-squares line, a narrow blue confidence band, dollar ticks, axis titles with units, and the note -24% per doubling of capacity, R² = 0.97, 95% confidence band

Checklist

Do

  • Plot the data before you trust the fit, and draw a residual plot when the shape matters.
  • Use log axes and fit on the logs (trendline_options=dict(log_x=True, log_y=True)) for data that grows by multiples, like prices, sizes and doses.
  • Say what kind of fit it is (OLS, LOWESS, a 12-month rolling mean) and what the band is (a 95% confidence or prediction band).
  • Report the slope in the units readers care about (MW per °C, runs per win, % per doubling), rounded.
  • Use trendline_scope="overall" or one line per group on purpose, and check whether the groups disagree.
  • Use WebGL (render_mode="webgl") and low marker opacity above about 10,000 points.

Don't

  • Extend the line far beyond the data; the fit says nothing about what happens there.
  • Read a high R² as proof that a straight line is the right shape.
  • Report R² without the slope, or a slope without its units.
  • Fit one line through groups that behave differently (see the penguins example).
  • Treat a regression line as cause and effect; it describes how y moves with x in this data.

Options to customize

A regression plot in Plotly is a scatter trace plus a fitted line from Plotly Express or from statsmodels, so these are the options that matter most. The full lists are in the trendline documentation, the px.scatter reference and the statsmodels regression documentation.

  1. trendline: "ols" for a least-squares line, "lowess" for a smooth local fit, "rolling", "ewm" or "expanding" for moving averages.
  2. trendline_options: frac for LOWESS (smaller follows the points more closely), window, halflife and other pandas arguments for the moving averages, log_x, log_y and add_constant for OLS.
  3. trendline_scope: "trace" (the default) fits one line per color, symbol or facet; "overall" fits one line to all the points.
  4. trendline_color_override: one color for every trendline, such as black over colored points.
  5. px.get_trendline_results(fig): a table of fitted statsmodels results, one per trace, with params, rsquared, conf_int() and summary().
  6. log_x, log_y: log axes, usually together with the matching trendline_options so the fit is straight on screen.
  7. render_mode="webgl" and opacity: fast drawing and readable density for tens of thousands of points or more.
  8. facet_col, facet_row: one panel per group with its own line, like seaborn's lmplot.
  9. marginal_x, marginal_y: histograms, box plots or rugs on the edges.
  10. model.get_prediction(X).summary_frame(alpha=0.05) (statsmodels): the confidence band (mean_ci_*) and prediction band (obs_ci_*) for any fitted model.
  11. fill="toself" or fill="tonexty": a shaded band from the lower and upper edges.
  12. smf.ols("y ~ x + np.square(x)", data=df) (statsmodels formula interface): curved and multi-variable fits, with model.resid for the residual plot.

More regression plot examples

Six more regression plots built from real public data, each with its source, the technique that makes it work and a prompt to rebuild it.

1. The Keeling curve with a curved fit

Monthly CO2 at Mauna Loa from 1958 to 2026 as blue dots rising from 315 to 432 ppm, a dashed gray straight line that is too high in the middle, and an orange quadratic curve that follows the data, labeled with growth rates of 0.8 ppm a year in 1960 and 2.6 ppm a year in 2025

A straight line through 823 monthly CO₂ readings gives 1.67 ppm a year, but a quadratic fits better (R² 0.996) and its slope shows the growth rate itself rising, from 0.8 ppm a year in 1960 to 2.6 in 2025. Data: NOAA Global Monitoring Laboratory and Scripps Institution of Oceanography, Mauna Loa monthly mean CO₂.

Tip: the slope of a quadratic at time t is b1 + 2·b2·t, so label the curve with its slope at a few dates instead of printing the coefficients.

Get the data from the NOAA Global Monitoring Laboratory monthly mean CO2 at Mauna Loa file co2_mm_mlo.csv
(columns: year, month, decimal_date, co2_ppm).
Build a plotly regression plot with small markers, a dashed straight OLS line and an orange quadratic fit from
statsmodels ols("co2_ppm ~ decimal_date + I(decimal_date**2)"), and label the quadratic's slope in ppm a year
at 1960 and 2025.

2. A power plant loses 2.17 MW for every degree of warmer air

Regression plot of 9,568 hourly readings of a combined cycle power plant: net output from 420 to 496 MW falls with ambient temperature from 2 to 37 °C along a black OLS line, with 486 MW at 5 °C and 432 MW at 30 °C labeled and the equation output = 497.0 - 2.17 × temperature, R² = 0.90

Warm air is less dense, so a gas turbine burns less of it, and across 9,568 hours this plant's output fell 2.17 MW for each degree, from 486 MW at 5 °C to 432 MW at 30 °C. Temperature alone explains 90% of the variation (R² 0.90). Data: UCI Machine Learning Repository, Combined Cycle Power Plant (creators Pınar Tüfekci and Heysem Kaya; described in Tüfekci, 2014).

Tip: with thousands of points use render_mode="webgl", opacity=0.25 and a black trendline_color_override, so the line stays visible over the cloud.

Get the data from the UCI Machine Learning Repository Combined Cycle Power Plant data set (Folds5x2_pp.xlsx,
first sheet; columns: AT ambient temperature °C, V exhaust vacuum, AP ambient pressure, RH relative humidity,
PE net output MW).
Build a plotly regression plot of PE against AT with px.scatter, trendline="ols", render_mode="webgl",
opacity 0.25 and a black trendline, read the slope and R² from px.get_trendline_results and write the
equation on the chart.

3. Penguins: one line for all species slopes the wrong way

Regression plot of bill depth against bill length for 342 penguins: blue Adelie, orange Chinstrap and green Gentoo points each with a rising least-squares line of +0.18, +0.22 and +0.20 mm depth per mm length, and a dashed black line for all penguins together sloping down at -0.09

Within each species, a penguin with a longer bill has a deeper one (+0.18 to +0.22 mm per mm), but one line through all 342 birds slopes down (−0.09), because Gentoo penguins have long, shallow bills. This reversal is called Simpson's paradox. Data: palmerpenguins R package, from Gorman, Williams and Fraser (2014).

Tip: draw the per-group lines with color= and add the trendline_scope="overall" line dashed, so readers see both answers at once.

Get the data from the palmerpenguins R package (penguins.csv; Palmer Station, Antarctica, 2007 to 2009;
columns: species, island, bill_length_mm, bill_depth_mm, body_mass_g, sex).
Build a plotly regression plot of bill depth against bill length colored by species with trendline="ols",
add the trendline_scope="overall" line as a dashed black trace, and label each line with its slope from
px.get_trendline_results.

4. Kleiber's law: metabolic rate against body mass

Log-log regression plot of resting metabolic rate against body mass for 422 mammal species, from a 2.4 g shrew to a 3,672 kg Asiatic elephant: points along an orange least-squares line with exponent 0.71, between dashed reference slopes of 0.67 and 0.75, with the house mouse, human and elephant labeled

Across 422 mammals, metabolic rate grows with body mass to the power 0.71 (95% confidence interval 0.69 to 0.72), so per kilogram a mouse burns 13 times what a person does. The same scaling is why drug doses don't carry over from animals to people by weight: the US Food and Drug Administration's (FDA) first-in-human dose guidance converts by body surface area, mass to the power 0.67. Data: AnAge, the Animal Ageing and Longevity Database (de Magalhães and Costa, 2009).

Tip: trendline_options=dict(log_x=True, log_y=True) fits a power law, and its slope is the exponent, so add the textbook exponents as dashed reference lines to compare.

Get the data from AnAge, the Animal Ageing and Longevity Database (genomics.senescence.info, dataset.zip,
anage_data.txt), mammals with both values (columns: species, common_name, order, body_mass_g, metabolic_rate_w).
Build a plotly regression plot with log_x and log_y, trendline="ols" with trendline_options=dict(log_x=True,
log_y=True), dashed reference lines with slopes 0.67 and 0.75, the exponent and its 95% confidence interval
from px.get_trendline_results, and the mouse, human and elephant labeled.

5. Blood pressure by age with LOWESS trendlines

Scatter plot of systolic blood pressure against age for 6,123 US adults in faint blue for men and orange for women, with a LOWESS curve for each: men rise from 116 at 20 to 128 at 80, women from 105 to 135, crossing at about 69

Women's systolic blood pressure starts about 11 points lower than men's at 20, rises twice as fast with age and passes it at about 69. LOWESS shows that bend without assuming a shape. Data: US Centers for Disease Control and Prevention (CDC), National Health and Nutrition Examination Survey (NHANES), August 2021 to August 2023, unweighted.

Tip: whole-number ages stack into columns, so add a small random jitter to x for display and say so in the subtitle.

Get the data from CDC NHANES August 2021-August 2023 files BPXO_L (BPXOSY1-3, systolic blood pressure in
millimeters of mercury) and DEMO_L (RIDAGEYR age, RIAGENDR sex), adults 18 and over, with the mean of
the three readings (columns: seqn, age, sex, systolic).
Build a plotly regression plot of systolic against age colored by sex with trendline="lowess",
trendline_options=dict(frac=0.6), opacity 0.18 and ages jittered by up to 0.4 years, and label each curve at
ages 20 and 80.

Another way to see it: a binned scatter plot, the mean of each 5-year age band with its 95% confidence interval, shows the same crossing with error bars, and shows women's average rising fastest between the early 40s and the late 40s:

Mean systolic blood pressure by 5-year age band for men and women with 95% confidence intervals: men rise from 117 to 130 mmHg, women from 106 to 137, the two lines meeting at the 70 to 74 band

6. Runs and wins in Major League Baseball, with a prediction band

Regression plot of wins against run differential for 810 Major League Baseball team seasons: points along an orange line with a narrow dark confidence band and a light prediction band about 8 wins either side, with the 2022 Dodgers (111 wins, +334 runs), 2025 Rockies (43 wins, -424), 2021 Mariners (90 wins, -51) and 2025 White Sox (60 wins, -95) labeled

Across 810 team seasons, every 10.2 runs of run differential (runs scored minus runs allowed) was worth one more win, with R² 0.89. The 95% prediction band is about ±8 wins wide: the 2021 Seattle Mariners won 90 games while being outscored by 51 runs, 14 more than the line predicts. Data: Lahman Baseball Database, via the R package Lahman, 1998 to 2025 without the shortened 2020 season.

Tip: shade the prediction band lighter than the confidence band, so readers can see both how well the slope is known and how far a single season strays.

Get the data from the Lahman Baseball Database Teams table (R package Lahman; columns: yearID, teamID, name,
G, W, L, R, RA), seasons 1998 to 2025 with at least 160 games, and compute run_diff = R - RA.
Build a plotly regression plot of W against run_diff with a statsmodels OLS line, a dark 95% confidence band
and a light 95% prediction band from get_prediction().summary_frame(), and label the two seasons furthest above
and below the line.

FAQ

bluesly
x
instagram
youtube
medium
fcaebook

Product

© 2026
Plotly. All rights reserved.
Cookie Preferences
aicpa/soc2