Beyond Matplotlib and Seaborn: Python Data Visualization Tools That Work
This article is an extension of my conference talk of the same name, and contains content from https://github.com/skirmer/new-py-dataviz. To see all the code and other example visualizations, please visit the repo. If you’d rather see this content presented on video/live, check me out at the Outlier conference or ODSC East 2021!
This piece reflects my opinions and not those of any employer.
I have a confession to make: I am a Python dataviz complainer. Coming to Python from the R ecosystem, I was spoiled for years by the beautiful, easy visualization tools, starting from the foundation of ggplot2, and including all the spectacular libraries by Thomas Lin Pedersen and others.
Switching into Python for machine learning was always a struggle for me as a result. How do I visualize my data, assess my model performance, and present my output when all the visualization options in Python are a hot mess? Like lots of people, I am often a visual learner, and seeing one plot can help something click that I’d never fully understand if it was just a table or a paragraph.
After making this complaint many times, and hearing it from others as well, I began to wonder whether my initial impressions of Python dataviz were still true. I finally decided that if I was going to keep saying that Python dataviz was a dumpster fire, I needed to take a hard look at the visualization tools in Python and actually test them.
Spoiler: I was wrong, there are some good dataviz tools in Python. However, R’s toolkit is still better.
Most people starting out from zero with dataviz in Python will be pointed to Matplotlib. If they’re lucky, someone will say “Oh, try Seaborn, the results look nicer.” These are both among the least useful and user-friendly visualization tools available in Python, however. The terrible reputation of Python dataviz is largely due to the lousy experiences of people who start here.
Let’s look at the better choices. I’ll show a few plots from Matplotlib and Seaborn in the interests of fairness, but trust me: unless these libraries are the ONLY way that your desired plot can be created (and they probably aren’t), skip them.
I realize that these opinions may open me up to the “Well Actually” crowd, so let me set some ground rules. When I am looking for a good data visualization library, I have some criteria. These may not be what matters to you as regards visualization tools, and that’s ok! You do you.
A power user with extensive experience on a library can often get a lot done with not great tooling. This is fine, but it’s not what we should expect or demand from new users. Forcing people to learn to use a difficult tool (when there’s a better alternative) is a great way to frustrate users and get them to move on to other tools.
Once a user learns the basics, I expect that they should be able to improvise on that using common sense. If
set_title works for a scatterplot, that ought to be the same argument or parameter for a bar plot. Why wouldn’t it be? (Looking at you, Seaborn). When you can’t start to build on the basic knowledge, then the learning process feels like a slog of memorization. It’s the reason everyone finds English verbs hard to learn - irregularity is not a good thing in a language.
So the library can get you up and running pretty quick, this is great - but you’re going to need something special, customized, or altered pretty quickly. If, when the user goes out to find out “How do I change X”, the answer comes back that you can’t change X, or you have to build an entirely new toolkit to do it, that’s a big problem. If the tool is easy to use, but doesn’t have full features, it’s not going to be good enough.
We sometimes fall into extremes, either spending endless hours making plots attractive, tweaking each tiny detail, or we give up and decide that the aesthetics don’t matter (think of all the plain white slides with black Calibri font you see — lots of people just give up on decent design). I want a good dataviz library to help us find a middle ground. There’s no reason that a default plot can’t be reasonably attractive without a ton of effort on the user’s part.
- Matplotlib: 2003, https://matplotlib.org/
- Seaborn: 2012, https://seaborn.pydata.org/
- Bokeh: 2012, https://bokeh.org/
- Altair: 2016, https://altair-viz.github.io/
- Plotnine: 2017, https://plotnine.readthedocs.io/
- Plotly: 2013, https://plotly.com/python/
In order to make my evaluation as fair as possible, I chose five types of plot and rendered them in each of the six libraries, trying to make them as similar and as appealing as possible without a lot of alteration.
I chose these plots to get a sense of how the libraries handle different data types and groupings. There are of course myriad plot types that we might commonly use, but often these are based on core foundations of a few types of visualization. We’ll get a look at points, lines, and bars, using a couple of different grouping methods, and we’ll also get a look at numeric, categorical, and datetime data types.
- Faceted Scatterplot
- Grouped Bar
- Time Series Line
- Bonus: 3D Scatterplot
In Matplotlib, the approach is to create an initial plot object, and then overlay data series on top of that — this is a normal pattern that will be familiar as we go through the libraries. However, the initial plot object is a complicated thing and poses an immediate challenge to new users.
fig, ax = plt.subplots() is nearly always the first line of a plot in Matplotlib, and it indicates that the figure (fig) is a distinct object from the axes (ax). This already calls for a somewhat challenging cognitive understanding for users, who may wonder what a figure really is in this framework. A developer or data scientist whose expertise is not in visual design may be particularly frustrated here.
Matplotlib developers do, to their credit, have a ton of documentation and tutorial material to help a new user — but is a tool that requires so much explaining really the best option for most users? Is that not the definition of “unintuitive”?
As far as visual design goes, Matplotlib produces very distinctive plots, and not in a good way. The renderings are difficult to aesthetically customize, resolution is not sharp, and applying things like themes or orderly color schemes take work.
It’s also worth noting that Matplotlib is designed to work with NumPy arrays, and makes no promises for how it will work with other datatypes, such as pandas objects.
Seaborn is an adaptation on top of Matplotlib, meant to improve some functionality and result quality, but still hindered by the challenges of Matplotlib. It’s definitely easier to use than Matplotlib, but has to sacrifice some grammatical consistency since it is meant to be compatible with Matplotlib.
You don’t need to start with Matplotlib’s confusing elements such as
plt.subplots() necessarily, but you can quickly get into situations where you have to use Matplotlib layer elements to get the plot you want. As the official docs will tell you, “While you can be productive using only seaborn functions, full customization of your graphics will require some knowledge of matplotlib’s concepts and API.” Docs
Given what we’ve learned, let’s talk about four entirely different alternative libraries. These libraries have their own grammars and frameworks, mostly if not entirely separate from Matplotlib.
By and large, the grammar of Bokeh makes a lot of sense and lends itself really well to improvisation for even new users. In Bokeh, you begin with designating your output type (notebook or file) and then add a “figure” object which contains many of the overall aesthetic parameters. After that, you overlay data series on top, and may add some design adjustments at the end.
While it’s not a really fancy or complex task, I was surprised to see how much Bokeh struggles specifically and uniquely with rendering a histogram. It requires the user to generate histogram bounds numerically in NumPy, and then the user creates an overlay of rectangles on the figure to present the image of a histogram. If Matplotlib’s cognitive overhead is high, I think this particular Bokeh example is a little absurd in the mental gymnastics required.
I don’t want to sound like I’m down on Bokeh, however — for almost all other types of plot it can make, it’s among the top libraries available. It has gorgeous, sharp plots, and interactivity (such as zooming) on plots is really handy. Perhaps in part because of the visual impressiveness, Bokeh plots are slow to load in Jupyter and can sometimes just not appear until you hit Run twice on a chunk.
There are occasionally points where the pre-plotting data manipulation is confusing — sometimes you must do all data manipulation in advance, and sometimes Bokeh will do it for you — but I think this is a natural challenge for libraries and I wouldn’t be too harsh about that.
Like Bokeh, Altair brings a clear, comprehensible grammar and scheme to designing plots. In Altair, users start with a “Chart” object, then build on that with adding the chart type (such as “bar”), and then setting encodings to indicate the data that belongs on each axis. Design elements are usually added last, in a “properties” call. These different steps can all be chained together if you like (although this can make readability of the code a little hard.)
Altair has a big gap, however, that can’t be ignored. It strongly discourages users from using a dataset longer than 5,000 rows. Granted, large volumes of data can make any visualization render slowly, especially if there are interactive features or JSON backend. However, unlike all other libraries here, instead of having some performance loss that a user could expect, Altair produces a failing error at >5000 rows, which you can override at your peril. As a result, for the tests I did, I used downsampling, which makes the plots less informative in many cases.
If you do override that error, Altair’s performance degrades severely. I can see why the developers set this error message, given the performance degradation, but it seems like finding some solution to the performance degradation is the better plan. It may not be possible, but as it currently stands, despite how much I love the grammar and the results, I can’t recommend Altair for much unless you have small datasets or can preaggregate your data.
But don’t mistake — Altair’s output is beautiful and the syntax is very easy and consistent!
Plotnine is simply a port of the ggplot2 library into Python, and attempts to replicate the ggplot2 API as much as possible in Python. This is a really impressive feat, because R and Python are entirely different languages, and ggplot2’s grammar is decidedly non-pythonic, but this library pulls it off. A ggplot2 user will find this entirely familiar and easy to use.
The visual output from Plotnine, unfortunately, resembles that of Matplotlib because Plotnine is built on a foundation of Matplotlib. Thankfully, the grammar of Plotnine bears no resemblance to Matplotlib, but the output can’t be as sharp or beautiful as Altair or Bokeh because of this.
I’m biased, to be perfectly clear: I like ggplot2 and I think it’s a good tool, so I think that porting the API to Python is a good thing to do. Some folks will find that plotnine is extremely non-pythonic (they’re right) and that will be an insurmountable barrier for them. That’s ok, and valid critique. However, for R users for whom the data visualization failings of Python are the last major barrier to learning and productively using it, this is a tremendously useful tool. It’s not going to be the be-all end-all for any data visualizations, in all likelihood, but it allows an R user to be fully productive in Python without having to learn a new visualization grammar first.
Plotly in Python has two different sorts of grammar — Plotly Express, and Plotly Graph Objects. Plotly Express is described as “the easy-to-use, high-level interface to Plotly” which is recommended to new users getting started. Graph Objects, on the other hand, is both the framework underneath Plotly Express and also a standalone module that gives more flexibility and options. My experience is that Plotly Express is a reasonable starting point, but that many if not most users will hit its limits and want to transition to Graph Objects later — this transition will be difficult, however, because of grammar inconsistencies.
Despite this complaining on my part, I strongly believe that Plotly is the best tool for anything 3D or interactive in Python — Bokeh offers some interactivity, but Plotly does that piece better, with less user intervention, and plots load faster. I like to show off my 3D scatterplot when explaining how well Plotly does these things, because it’s a remarkably impressive result, but it’s not easy to build.
Plotly Express kind of lacks a niche, partly because Plotly output is not attractive by default. For the very simple plots that might be easily built in Plotly Express, the grammar is easy but the results are better looking (and equally as easy) in other libraries like Bokeh. For the complicated stuff that Plotly might be uniquely capable of, you need to work in Graph Objects.
So, you’ve read all my opinions after I tested all these plots, and now you’re trying to choose the correct library for your needs. I can’t promise that there’s one silver bullet that will work for everything, but here are a few tips:
- If you need really complex interactive and/or 3D graphs, use Plotly.
- If you’re an R user getting into the Python world, use plotnine.
- If you have small data sizes, consider Altair.
- If you need an all-purpose tool that you won’t need to switch later, use Bokeh. It’s a reasonably strong performer in many different categories.
And, honestly, if you have the freedom to choose any ecosystem, and you care deeply about the visualizations? Use R. ¯\_(ツ)_/¯