The simple graph has brought more information to the data analyst's mind than any other device.
As a data scientist, I spend much of my time making simple plots to understand complex data sets (exploratory data analysis) and help others understand them (presentations).
In particular, I make a lot of bar charts (including histograms), line plots (including time series), scatter plots, and density plots from data in Pandas data frames. I often want to facet these on various categorical variables and layer them on a common grid.
Python has many plotting libraries. Matplotlib is the best known, and several others build on it.
"Matplotlib makes easy things easy and hard things possible." It hands you figures, axes, and drawing primitives. You assemble everything above that level yourself: faceting, stacking, density estimation, smoothing. That assembly is what sends analysts to Stack Overflow.
Put the Matplotlib and ggplot2 versions of the two-variable faceted scatter plot below side by side: eighteen lines of subplot bookkeeping against four lines of grammar. If Matplotlib annoys you and you haven't read Effectively Using Matplotlib by Chris Moffitt, go read it.
Pandas plotting provides "the basics ... to easily create decent looking plots" from data frames. That is about 70% of what I do day-to-day. It has no faceting, no categorical color mapping, and no smoothing, so five of the examples below have no pandas column.
Seaborn calls itself "statistical data visualization." Its classic interface is a set of named functions (histplot, scatterplot, countplot, lmplot, kdeplot) plus FacetGrid, which I use for faceting more than anything else in the library. It covers every plot below, once you know which function to reach for.
Seaborn 0.12 added seaborn.objects, a second interface built on the grammar of graphics. It composes a plot from marks and statistical transforms instead of dispatching to a named plotting function. The interface has no loess smoother and no regression confidence band, so those two examples are missing.
"plotnine is a data visualization package for Python based on the grammar of graphics." It tracks ggplot2 closely enough that most R code translates line for line, down to the + for layering. I reach for it when I want ggplot2 semantics without leaving Python.
These libraries draw in the browser. The examples here are static PNGs, so their tooltips, panning, and linked selection are gone.
"Vega-Altair is a declarative visualization library for Python," built on Vega-Lite. According to Jake Vanderplas, "Declarative visualization lets you think about data and relationships, rather than incidental details." You describe the encoding and Altair chooses the marks, scales, and legend.
"plotly's Python graphing library makes interactive, publication-quality graphs." The examples here use Plotly Express, which the project calls "the recommended starting point for creating most common figures." Express covers most of these plots in one call; the regression and smoothing examples fall back to graph_objects and statsmodels.
JetBrains writes Lets-Plot, which it calls "a faithful port of R's ggplot2 to Python and Kotlin." The claim holds up: most of the examples below are the ggplot2 column with lp. prefixes. Like Altair, it renders to HTML in the notebook.
"Bokeh is a Python library for creating interactive visualizations for modern web browsers." The Bokeh examples below go through hvPlot, which adds an .hvplot accessor to data frames. The accessor echoes the pandas .plot API, so most of these plots are one call plus a few keyword arguments. hvPlot has no regression line or loess smoother, so it is absent from those two examples.
Jake Vanderplas's PyCon 2017 talk The Python Visualization Landscape still explains how these libraries relate to one another, as does Dan Saber's A Dramatic Tour through Python's Data Visualization Landscape (including ggplot and Altair), though both predate several of the libraries here.
Open source developers do most Python plotting development, an (almost) thankless job. I am grateful for the hours they have spent helping me do mine. Please keep it up!
Before I started using Python, I did most of my data analysis work in R. Like many Pythonistas, I remain a fan of Hadley Wickham's ggplot2, a "grammar of graphics" implementation in R, for exploratory data analysis.
Like scikit-learn for machine learning in Python, ggplot2 has a consistent API and sane defaults. The consistent interface lets me iterate without stopping to think. The sane defaults make it easy to drop plots right into an email or presentation.
ggplot2 makes basic plots (bar, histogram, line, scatter, density, violin) from data frames with faceting and layering by discrete values.
Hadley Wickham and Garrett Grolemund's R for Data Science teaches ggplot2 well.
Below is a list of basic plots for exploratory data analysis, each made with as many libraries as time (and library) permit.
I hope it helps you work with what exists today and inspires what gets built next.
Contributing instructions are on GitHub. General feedback or other plot suggestions are welcome.
ggplot2 ships the datasets used below: the Prices of 50,000 round cut diamonds and Fuel economy data from 1999 and 2008 for 38 popular models of car.
The time series example is a random walk I generate with a quick Python script.
A few rows of each:
| date | value |
|---|---|
| 2000-01-01 | -1.129240 |
| 2000-01-02 | -0.713866 |
| 2000-01-03 | -0.967130 |
| 2000-01-04 | -0.443428 |
| 2000-01-05 | 1.366886 |
| manufacturer | model | displ | year | cyl | trans | drv | cty | hwy | fl | class |
|---|---|---|---|---|---|---|---|---|---|---|
| audi | a4 | 1.8 | 1999 | 4 | auto(l5) | f | 18 | 29 | p | compact |
| audi | a4 | 1.8 | 1999 | 4 | manual(m5) | f | 21 | 29 | p | compact |
| audi | a4 | 2.0 | 2008 | 4 | manual(m6) | f | 20 | 31 | p | compact |
| audi | a4 | 2.0 | 2008 | 4 | auto(av) | f | 21 | 30 | p | compact |
| audi | a4 | 2.8 | 1999 | 6 | auto(l5) | f | 16 | 26 | p | compact |
| carat | cut | color | clarity | depth | table | price | x | y | z |
|---|---|---|---|---|---|---|---|---|---|
| 0.23 | Ideal | E | SI2 | 61.5 | 55.0 | 326 | 3.95 | 3.98 | 2.43 |
| 0.21 | Premium | E | SI1 | 59.8 | 61.0 | 326 | 3.89 | 3.84 | 2.31 |
| 0.23 | Good | E | VS1 | 56.9 | 65.0 | 327 | 4.05 | 4.07 | 2.31 |
| 0.29 | Premium | I | VS2 | 62.4 | 58.0 | 334 | 4.20 | 4.23 | 2.63 |
| 0.31 | Good | J | SI2 | 63.3 | 58.0 | 335 | 4.34 | 4.35 | 2.75 |
(sns
.FacetGrid(mpg, hue='class', height=10)
.map(pyplot.scatter, 'displ', 'hwy')
.add_legend()
.set(
title='Engine Displacement in Liters vs Highway MPG',
xlabel='Engine Displacement in Liters',
ylabel='Highway MPG'
))
seaborn.FacetGrid overrides the rcParams['figure.figsize'] global parameter.
You have to set the size in the size withheight=inFacetGrid`
classes = sorted(mpg['class'].unique())
fig, axes = pyplot.subplots(
2, 4, sharex=True, sharey=True)
for ax, c in zip(axes.flat, classes):
d = mpg[mpg['class'] == c]
ax.scatter(d['displ'], d['hwy'], s=20)
ax.set_title(c, fontsize=16)
ax.tick_params(labelsize=12)
for ax in axes.flat[len(classes):]:
ax.remove()
Matplotlib has no faceting. subplots makes
the grid splitting the data, titling each panel
and hiding the leftover axes is manual.
drvs = sorted(mpg['drv'].unique())
cyls = sorted(mpg['cyl'].unique())
fig, axes = pyplot.subplots(
len(drvs), len(cyls),
sharex=True, sharey=True)
for i, drv in enumerate(drvs):
for j, cyl in enumerate(cyls):
d = mpg[(mpg['drv'] == drv)
& (mpg['cyl'] == cyl)]
ax = axes[i, j]
ax.scatter(d['displ'], d['hwy'], s=20)
ax.tick_params(labelsize=12)
for j, cyl in enumerate(cyls):
axes[0, j].set_title(cyl, fontsize=16)
for i, drv in enumerate(drvs):
ax = axes[i, -1]
ax.set_ylabel(drv, fontsize=16)
ax.yaxis.set_label_position('right')
The drv by cyl grid is indexed by hand.
The strip labels are axis titles on the top row
and the right column.
import statsmodels.api as sm
from statsmodels.stats.outliers_influence import summary_table
y=mpg.hwy
x=mpg.displ
X = sm.add_constant(x)
res = sm.OLS(y, X).fit()
st, data, ss2 = summary_table(res, alpha=0.05)
preds = pd.DataFrame.from_records(data, columns=[s.replace('\n', ' ') for s in ss2])
preds['displ'] = mpg.displ
preds = preds.sort_values(by='displ')
fig = graph_objects.Figure(layout={
'title' : 'Engine Displacement in Liters vs Highway MPG',
'xaxis' : {
'title' : 'Engine Displacement in Liters'
},
'yaxis' : {
'title' : 'Highway MPG'
}
})
p1 = graph_objects.Scatter(**{
'mode' : 'markers',
'x' : mpg.displ,
'y' : mpg.hwy,
'name' : 'Points'
})
p2 = graph_objects.Scatter({
'mode' : 'lines',
'x' : preds['displ'],
'y' : preds['Predicted Value'],
'name' : 'Regression',
})
#Add a lower bound for the confidence interval, white
p3 = graph_objects.Scatter({
'mode' : 'lines',
'x' : preds['displ'],
'y' : preds['Mean ci 95% low'],
'name' : 'Lower 95% CI',
'showlegend' : False,
'line' : {
'color' : 'white'
}
})
# Upper bound for the confidence band, transparent but with fill
p4 = graph_objects.Scatter( {
'type' : 'scatter',
'mode' : 'lines',
'x' : preds['displ'],
'y' : preds['Mean ci 95% upp'],
'name' : '95% CI',
'fill' : 'tonexty',
'line' : {
'color' : 'white'
},
'fillcolor' : 'rgba(255, 127, 14, 0.3)'
})
fig.add_trace(p1)
fig.add_trace(p2)
fig.add_trace(p3)
fig.add_trace(p4)
No built in method to calculate and display confidence intervals. Must calculate manually and utilise existing features to build the confidence band.
import statsmodels.api as sm
fig, ax = pyplot.subplots()
for c, d in mpg.groupby('class'):
ax.scatter(d['displ'], d['hwy'], label=c)
sub = mpg[mpg['class'] == 'subcompact']
fit = sm.nonparametric.lowess(sub['hwy'],
sub['displ'])
ax.plot(fit[:, 0], fit[:, 1], color='black')
ax.legend()
Matplotlib has no smoother, so the loess fit comes from statsmodels.
traces = []
for cls in mpg['class'].unique():
traces.append(graph_objects.Scatter({
'mode' : 'markers',
'x' : mpg.displ[mpg['class'] == cls],
'y' : mpg.hwy[mpg['class'] == cls],
'name' : cls
}))
subcompact = mpg[mpg['class'] == 'subcompact'].sort_values(by='displ')
traces.append(graph_objects.Scatter({
'mode' : 'lines',
'x' : subcompact.displ,
'y' : subcompact.hwy,
'name' : 'smoothing',
'line' : {
'shape' : 'spline',
'smoothing' : 1.3
}
}))
fig = graph_objects.Figure(**{
'data' : traces,
'layout' : {
'title' : 'Engine Displacement in Liters vs Highway MPG',
'xaxis' : {
'title' : 'Engine Displacement in Liters',
},
'yaxis' : {
'title' : 'Highway MPG'
}
}
})
Plotly's builtin smoothing function is very weak
scatter = (
alt.Chart(
mpg,
title='Engine Displacement in Liters vs Highway MPG',
)
.mark_circle()
.encode(
x=alt.X(
'displ',
axis=alt.Axis(
title='Engine Displacament in Liters'
),
),
y=alt.Y(
'hwy',
axis=alt.Axis(
title='Highway MPG'
),
),
color='class',
)
)
line = (
alt.Chart(
mpg[mpg['class'] == 'subcompact']
)
.transform_loess('displ', 'hwy')
.mark_line()
.encode(x=alt.X('displ'), y=alt.Y('hwy'))
)
scatter + line
counts = (diamonds
.groupby(['cut', 'clarity'])
.size()
.unstack())
fig, ax = pyplot.subplots()
bottom = np.zeros(len(counts))
for clarity in counts.columns:
ax.bar(counts.index, counts[clarity],
bottom=bottom, label=clarity)
bottom += counts[clarity].values
ax.legend()
Matplotlib stacks bars by carrying the running
total of each series in bottom.
counts = (diamonds
.groupby(['cut', 'clarity'])
.size()
.unstack())
x = np.arange(len(counts))
width = .8 / len(counts.columns)
fig, ax = pyplot.subplots()
for i, clarity in enumerate(counts.columns):
ax.bar(x + i * width, counts[clarity],
width=width, label=clarity)
ax.set_xticks(x + .4 - width / 2)
ax.set_xticklabels(counts.index, rotation=45)
ax.legend()
Dodging is manual: shift each series by its own offset and move the ticks back to the group centers.
fig, ax = pyplot.subplots()
ax.set_xlim(55, 70)
for cut in diamonds['cut'].unique():
s = diamonds[diamonds['cut'] == cut]['depth']
s.plot.kde(ax=ax, label=cut)
ax.legend()
I don't know whether Pandas can fill a KDE curve.
This requires using some Matplotlib to get them to stack and to have a legend.
from scipy.stats import gaussian_kde
grid = np.linspace(55, 70, 200)
fig, ax = pyplot.subplots()
for cut, d in diamonds.groupby('cut')['depth']:
density = gaussian_kde(d)(grid)
ax.fill_between(grid, density, alpha=.1)
ax.plot(grid, density, label=cut)
ax.set_xlim(55, 70)
ax.legend()
Matplotlib has no density estimator, so the
KDE comes from scipy. set_xlim clips the axis
ggplot2's xlim() drops rows first.
Note:
seaborn.objects has no
coord_flip, so the categorical variable is mapped toyinstead.