What’s better than a lazy sunday afternoon at my parents’ place, where to hack away a few hours?
Lately joyplots have been all the rage on the nerd part of twitter, thanks to the awesome ggjoy package for R. Joyplots are essentially just a number of stacked overlapping density plots, that look like a mountain ridge, if done right.
I find them usually nice.. and I like the name, that, in case you’re wondering, comes from the art cover of a post-punk masterpiece of the late 70’s, “Unkown Pleasure” by Joy Division.
So I wrote some code to have decent-looking joyplot in python, especially from dataframes. The result is here. I had fun.
Here are some examples.
import joypy import pandas as pd from matplotlib import pyplot as plt from matplotlib import cm
Obligatory iris stuff
Though not a great fit for this kind of visualization, we can generate some joyplots with the
iris = pd.read_csv("data/iris.csv")
joypy.joyplot() will draw joyplot with a density subplot for each numeric column in the dataframe.
The density is obtained with the
gaussian_kde function of
%matplotlib inline fig, axes = joypy.joyplot(iris)
If you pass a grouped dataframe, or if you pass a column name to the
by argument, you get a density plot
for each value in the grouped column.
%matplotlib inline fig, axes = joypy.joyplot(iris, by="Name")
In the previous plot, one subplot had a much larger
y extensions than the others.
Since, by default, the subplots share the
y-limits, the outlier causes all the other subplots to be quite
We can change this behavior letting each subplot take up the whole
y space with
ylim='own', as follows.
%matplotlib inline fig, axes = joypy.joyplot(iris, by="Name", ylim='own')
In this case, we achieved more overlap, but the subplots are no longer directly comparable.
Yet another option is to keep the default ylim behavior (i.e.,
and simply increase the overlap factor:
%matplotlib inline fig, axes = joypy.joyplot(iris, by="Name", overlap=3)
It’s also possible to draw histograms with
hist=True, though they don’t look nice when overlapping,
so it’s better to set
grid='both' you also get grid lines on both axis.
%matplotlib inline fig, axes = joypy.joyplot(iris, by="Name", column="SepalWidth", hist="True", bins=20, overlap=0, grid=True, legend=False)
Global daily temperatures
Something that is probably a better fit for joyplots than
iris: let’s visualize the distribution of
global daily temperatures from 1880 to 2014.
(The original file can be found here)
%matplotlib inline temp = pd.read_csv("data/daily_temp.csv",comment="%") temp.head()
|Date Number||Year||Month||Day||Day of Year||Anomaly|
Anomaly contains the global daily temperature (in °C) computed as the difference between the
daily value and the 1950-1980 global average.
We can draw the distribution of the temperatures in time, grouping by
Year, to see
how the daily temperature distribution shifted across time.
y label would get pretty crammed if we were to show all the year labels, we first prepare
a list where we leave only the multiples of 10.
To reduce the clutter, the option
x range of each individual density plot to the range where the density is non-zero
(+ an “aestethic” tolerance to avoid cutting the tails too early/abruptly),
rather than spanning the whole
colormap=cm.autumn_r provides a colormap to use along the plot.
(Grouping the dataframe and computing the density plots can take a few seconds here.)
%matplotlib inline labels=[y if y%10==0 else None for y in list(temp.Year.unique())] fig, axes = joypy.joyplot(temp, by="Year", column="Anomaly", labels=labels, range_style='own', grid="y", linewidth=1, legend=False, figsize=(6,5), title="Global daily temperature 1880-2014 \n(°C above 1950-80 average)", colormap=cm.autumn_r)
If you want, you can also plot the raw counts, rather than the estimated density. This makes for noisier plots, but it might be preferable in some cases.
fade=True, the subplots get a progressively larger alpha value.
%matplotlib inline labels=[y if y%10==0 else None for y in list(temp.Year.unique())] fig, axes = joypy.joyplot(temp, by="Year", column="Anomaly", labels=labels, range_style='own', grid="y", linewidth=1, legend=False, fade=True, figsize=(6,5), title="Global daily temperature 1880-2014 \n(°C above 1950-80 average)", kind="counts", bins=30)
Just for fun, let’s plot the same data as it were on the cover of Unknown Pleasures, the Joy Division’s album where the nickname to this kind of visualization comes from.
No labels/grids, no filling, black background, white lines, and a couple of adjustments just to make it look a bit more like the album cover.
%matplotlib inline fig, axes = joypy.joyplot(temp,by="Year", column="Anomaly", ylabels=False, xlabels=False, grid=False, fill=False, background='k', linecolor="w", linewidth=1, legend=False, overlap=0.5, figsize=(6,5),kind="counts", bins=80) plt.subplots_adjust(left=0, right=1, top=1, bottom=0) for a in axes[:-1]: a.set_xlim([-8,8])
NBA players - regular season stats
The files can be obtained from Kaggle datasets.
players = pd.read_csv("data/Players.csv",index_col=0) players.head()
|0||Curly Armstrong||180.0||77.0||Indiana University||1918.0||NaN||NaN|
|1||Cliff Barker||188.0||83.0||University of Kentucky||1921.0||Yorktown||Indiana|
|2||Leo Barnhorst||193.0||86.0||University of Notre Dame||1924.0||NaN||NaN|
|3||Ed Bartels||196.0||88.0||North Carolina State University||1925.0||NaN||NaN|
|4||Ralph Beard||178.0||79.0||University of Kentucky||1927.0||Hardinsburg||Kentucky|
seasons = pd.read_csv("data/Seasons_Stats.csv", index_col=0) seasons.head()
5 rows × 52 columns
Join the dataframes and filter:
- years starting from the 3 point line introduction (1979-80)
- player seasons with at least 10 field goal attempts.
joined = seasons.merge(players, on="Player") threepoints = joined[(joined.Year >= 1979) & (joined["FGA"] > 10)].sort_values("Year") threepoints["3Pfract"] = threepoints["3PA"]/threepoints.FGA
The fraction of 3 pointers attempted by each player in a season has clearly shifted a lot.
In today’s NBA there’s a good number of players who take 40% or more of their shots from behind the line.
%matplotlib inline decades = [int(y) if y%10==0 or y == 2017 else None for y in threepoints.Year.unique()] fig, axes = joypy.joyplot(threepoints, by="Year", column="3Pfract", kind="kde", range_style='own', tails=0.2, overlap=3, linewidth=1, colormap=cm.autumn_r, labels=decades, grid='y', figsize=(7,7), title="Fraction of 3 pointers \n over all field goal attempts")#, x_range=[-0.05,1])
In this last plot, the distributions of the 3P percentages across the players are drawn as raw binned counts.
kind=normalized_counts, the values are normalized
over the occurrences in each year: this is probably needed here, because that the number of teams and players
in the NBA has grown during the years.
The median NBA player has become a much better 3P shooter: no big surprise there!
%matplotlib inline threepoint_shooters = threepoints[threepoints["3PA"] >= 20] decades = [int(y) if y%10==0 or y == 2017 else None for y in threepoint_shooters.Year.unique()] fig, axes = joypy.joyplot(threepoint_shooters, by="Year", column="3P%", kind="normalized_counts", bins=30, range_style='all', x_range=[-0.05,0.65], overlap=2, linewidth=1, colormap=cm.autumn_r, labels=decades, grid='both', figsize=(7,7), title="3 Points % \n(at least 20 3P attempts)")