Case Study 2: ggplot2 and the Grammar of Graphics
Before seaborn, there was ggplot2. Hadley Wickham's 2005 implementation of Leland Wilkinson's "grammar of graphics" became the template for declarative statistical visualization. seaborn is, in many ways, Python's closest equivalent to ggplot2. Understanding ggplot2 illuminates what seaborn is trying to do and where it succeeds and falls short.
The Situation
In 2005, R was already the dominant statistical computing environment in academia. It had good matrix operations, good statistical modeling, and a built-in plotting system. The plotting system — known as base graphics — was functional but ugly and imperative. Producing a complex statistical chart required manual calls to plot, lines, points, legend, and many other functions. The code was tedious to write and hard to maintain.
Hadley Wickham was a PhD student at Iowa State University in statistics. He had been reading Leland Wilkinson's 1999 book The Grammar of Graphics, which proposed a systematic framework for describing any statistical chart as a combination of layered elements: data, aesthetic mappings, geometric objects, statistics, scales, facets, and coordinate systems. Wilkinson's framework was theoretical — he described the grammar but did not implement it in a widely-used tool. Wickham decided to implement it in R.
The first version, called ggplot, was released in 2005. It was a faithful but clumsy implementation of Wilkinson's grammar. Wickham refined it, and in 2007 he released ggplot2, a cleaner rewrite that became the standard R statistical visualization library within a few years. By 2010, ggplot2 was more widely used than base R graphics for any non-trivial chart. By 2015, it had spawned a whole ecosystem of extensions (ggrepel, ggthemes, gganimate, etc.) and was central to the R data science workflow.
ggplot2's influence extended beyond R. Wickham's grammar-of-graphics framework became the template for how many Python libraries organized their APIs. seaborn, plotnine (a literal Python port of ggplot2), Altair (which also uses grammar-of-graphics concepts), and Plotly Express all show clear ggplot2 influence. When you write sns.scatterplot(data=df, x="col", y="col", hue="category"), you are using a pattern that ggplot2 invented.
This case study examines ggplot2's design, its relationship to the grammar of graphics, and how seaborn reflects (and departs from) that design.
The Grammar of Graphics
Wilkinson's grammar decomposes any statistical chart into seven layered elements:
1. Data: the source observations, typically as a tidy DataFrame.
2. Aesthetic mappings: how variables in the data map to visual properties (x position, y position, color, shape, size, etc.). Aesthetic mappings are the core of the declarative style — you specify the mapping, and the library handles the rendering.
3. Geometric objects (geoms): the specific visual forms used to represent the data. A scatter plot uses geom_point, a line chart uses geom_line, a bar chart uses geom_bar, a histogram uses geom_histogram, and so on. Multiple geoms can be layered on the same plot (for example, a scatter plot with a regression line overlay).
4. Statistics: transformations applied to the data before plotting. A histogram's stat_bin bins the data; a regression line's stat_smooth fits a model; a summary plot's stat_summary computes an aggregate. Statistics are often invoked implicitly by the geom (geom_histogram uses stat_bin automatically) but can be customized explicitly.
5. Scales: how aesthetic mappings translate to actual visual properties. A scale_color_brewer maps categorical data to a ColorBrewer palette. A scale_y_log10 makes the y-axis logarithmic. Scales bridge the data space and the visual space.
6. Facets: the mechanism for creating small multiples. facet_wrap(~category) creates one panel per category. facet_grid(row ~ col) creates a 2D grid.
7. Coordinate system: Cartesian by default, but can be polar, map projection, or custom. The coordinate system transforms the raw x and y positions into pixel coordinates on the output.
A ggplot2 chart is constructed by layering these elements:
library(ggplot2)
ggplot(data = mtcars, aes(x = wt, y = mpg, color = factor(cyl))) +
geom_point(size = 3) +
geom_smooth(method = "lm") +
scale_color_brewer(palette = "Set2") +
facet_wrap(~am) +
labs(
title = "Fuel Economy vs. Weight",
x = "Weight (1000 lbs)",
y = "MPG",
color = "Cylinders"
) +
theme_minimal()
This single block declares: data is mtcars, aesthetic mappings are weight-to-x and mpg-to-y and cylinders-to-color, geoms are points and a linear regression smooth, scale is a ColorBrewer palette, facets are by transmission type, labels are specified, and the theme is minimal.
The result is a faceted scatter plot with colored groups, regression lines per group, and clean styling — produced by one coherent declarative expression. The same chart in base R graphics would take many more lines of imperative code.
The Influence on seaborn
seaborn is not a literal port of ggplot2 — Waskom chose a different API style — but the conceptual influence is unmistakable. Compare the same chart in seaborn:
import seaborn as sns
g = sns.lmplot(
data=mtcars,
x="wt",
y="mpg",
hue="cyl",
col="am",
palette="Set2",
height=4,
)
g.set_axis_labels("Weight (1000 lbs)", "MPG")
g.fig.suptitle("Fuel Economy vs. Weight")
The same declarative mappings (data=, x=, y=, hue=, col=), the same implicit layering (scatter points with a regression line), the same palette specification, the same faceting. The syntax differs — seaborn uses function arguments instead of ggplot2's + operator — but the underlying pattern is identical.
Specific ggplot2 concepts that map directly to seaborn:
| ggplot2 | seaborn |
|---|---|
aes(x, y, color, shape, size) |
x=, y=, hue=, style=, size= parameters |
geom_point() |
sns.scatterplot() |
geom_line() |
sns.lineplot() |
geom_bar() |
sns.barplot() or sns.countplot() |
geom_histogram() |
sns.histplot() |
geom_density() |
sns.kdeplot() |
geom_boxplot() |
sns.boxplot() |
geom_violin() |
sns.violinplot() |
geom_smooth() |
regression overlay in sns.regplot() or sns.lmplot() |
facet_wrap() |
col= and col_wrap= parameters |
facet_grid() |
col= and row= parameters |
scale_color_brewer(palette = "Set2") |
palette="Set2" parameter |
theme_minimal() |
sns.set_theme(style="ticks") |
The correspondences are not exact, but the conceptual mapping is clear. A practitioner who knows ggplot2 can learn seaborn quickly because the underlying ideas are the same.
Where seaborn Departs from ggplot2
seaborn is not a literal ggplot2 clone, and the differences are worth understanding.
1. seaborn uses function arguments; ggplot2 uses layered additions. In ggplot2, you build a plot by adding layers with +. In seaborn, you call a function that takes all the options as arguments. The ggplot2 approach is more composable — you can save a partial plot and add to it later — but the seaborn approach is simpler for single-call usage.
2. seaborn does not have a unified grammar across all functions. Each seaborn function has its own parameter set, and parameters that look similar can behave differently across functions. ggplot2 is more systematically consistent because every layer uses the same grammar.
3. seaborn's figure-level functions hide the layering. When you call sns.lmplot, seaborn builds a scatter plot with a regression overlay without you explicitly requesting the overlay. In ggplot2, you would write geom_point() + geom_smooth() explicitly. seaborn's approach is more convenient for the common case but less flexible for unusual combinations.
4. seaborn inherits from matplotlib; ggplot2 has its own rendering. seaborn produces matplotlib Figure and Axes objects that can be further customized with matplotlib. ggplot2 uses the grid graphics system in R, which is different from base R graphics and requires separate customization.
5. seaborn has no explicit grammar user-facing. A seaborn user can use the library effectively without knowing anything about the grammar of graphics. A ggplot2 user implicitly learns the grammar because the API is built around it. Whether this is better or worse depends on the user's goals.
The seaborn.objects Experimental API
In seaborn 0.12 (2022), Waskom introduced a new API called seaborn.objects that is explicitly modeled after the grammar of graphics. It looks much more like ggplot2:
import seaborn.objects as so
p = (
so.Plot(tips, x="total_bill", y="tip")
.add(so.Dot())
.add(so.Line(), so.PolyFit())
.facet("day")
.scale(color="deep")
)
p.show()
The layered .add() calls, the explicit statistical transformations (so.PolyFit), and the scales are all ggplot2-inspired. The objects API is experimental and not yet the main seaborn interface, but its existence shows that Waskom recognizes the value of a more grammar-faithful API.
For now, the classic seaborn API (using sns.scatterplot, sns.relplot, etc.) remains the primary interface. The objects API is worth knowing about for advanced users who want more composability, but it is not required for most use cases.
Lessons from the ggplot2 Experience
The ggplot2 trajectory offers several lessons for data visualization tool design.
1. A good abstraction can last for decades. Wilkinson's grammar of graphics was published in 1999 and still shapes visualization APIs today. Good abstractions outlast specific implementations.
2. Declarative is productive. The imperative-to-declarative shift that ggplot2 demonstrated was a large productivity win for statistical visualization. Declarative code is shorter, more maintainable, and easier to teach. Python's adoption of similar patterns (seaborn, Altair) validates the ggplot2 approach.
3. Tidy data is a precondition. Both ggplot2 and seaborn expect tidy data. This is not a coincidence — the declarative style requires a consistent data structure to map from. Wide-form data does not support the aesthetic-mapping pattern effectively.
4. Themes matter. Both ggplot2 and seaborn put effort into themes and default aesthetics. Users do not just want the right chart; they want it to look good by default. Libraries that ignore aesthetics lose adoption to libraries that do not.
5. Ecosystems build on good foundations. ggplot2 spawned ggrepel, ggthemes, gganimate, and many other extensions. A well-designed library becomes a platform for additional tools. seaborn's ecosystem is less elaborate, partly because matplotlib already fills the "lower layer" role that a grid-based library cannot.
6. Community-driven development beats commercial tools. ggplot2, seaborn, matplotlib, pandas, NumPy — all are open source, all are maintained by volunteer or semi-volunteer teams, and all have become standards despite competing with commercial alternatives (Tableau, Power BI, SPSS graphics). The community-driven pattern wins on adoption even when commercial tools have more resources.
Discussion Questions
-
On the grammar of graphics as abstraction. Wilkinson's grammar decomposes any chart into seven layered elements. Do you find this decomposition useful, or does it feel like theoretical overkill? Does seaborn's less-rigorous approach lose anything?
-
On declarative vs. imperative. ggplot2 is strictly declarative; seaborn is mostly declarative but allows imperative matplotlib customization. Is the hybrid approach better or worse than a pure declarative library?
-
On ggplot2 porting. The plotnine library is a direct port of ggplot2 to Python. Why do you think plotnine has fewer users than seaborn, given that ggplot2 is more popular in R than seaborn is in Python?
-
On the seaborn.objects API. Waskom's new objects API is closer to ggplot2. If seaborn.objects becomes the primary interface, will that make seaborn better or worse for current users who have learned the classic API?
-
On language communities. R users generally prefer ggplot2 over base graphics; Python users generally prefer seaborn over matplotlib for statistical work. What do these preferences say about the relationship between language communities and their tools?
-
On which to learn. If you had to learn one declarative statistical visualization library for a new role — ggplot2, seaborn, plotnine, or Altair — which would you choose? Why?
ggplot2 is the template that modern Python statistical visualization tools have been trying to match. seaborn is the closest match in the Python ecosystem, and understanding ggplot2 illuminates what seaborn is trying to do. Both libraries implement the same basic idea — declarative aesthetic mappings from DataFrame columns to visual channels — and both succeed at it. The differences are in specific API choices, not in fundamental vision. Learning seaborn after learning ggplot2 (or vice versa) is fast because the underlying concepts transfer directly.