How to make beautiful data visualizations in Python with matplotlib

It’s been well over a year since I wrote my last tutorial, so I figure I’m overdue. This time, I’m going to focus on how you can make beautiful data visualizations in Python with matplotlib.

There are already tons of tutorials on how to make basic plots in matplotlib. There’s even a huge example plot gallery right on the matplotlib web site, so I’m not going to bother covering the basics here. However, one aspect that’s missing in all of these tutorials and examples is how to make a nice-looking plot.

Below, I’m going to outline the basics of effective graphic design and show you how it’s done in matplotlib. I’ll note that these tips aren’t limited to matplotlib; they apply just as much in R/ggplot2, matlab, Excel, and any other graphing tool you use.

Less is more

The most important tip to learn here is that when it comes to plotting, less is more. Novice graphical designers often make the mistake of thinking that adding a cute semi-related picture to the background of a data visualization will make it more visually appealing. (Yes, that graphic was an official release from the CDC.) Or perhaps they’ll fall prey to more subtle graphic design flaws, such as using an excess of chartjunk that their graphing tool includes by default.

At the end of the day, data looks better naked. Spend more time stripping your data down than dressing it up. Darkhorse Analytics made an excellent GIF to explain the point:

data-ink

(click on the GIF for a gfycat version that allows you to move through it at your own pace)

Antoine de Saint-Exupery put it best:

Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away.

You’ll see this in the spirit of all of my plots below.

Color matters

The default color scheme in matplotlib is pretty ugly. Die-hard matlab/matplotlib fans may stand by their color scheme to the end, but it’s undeniable that Tableau’s default color scheme is orders of magnitude better than matplotlib’s.

Use established default color schemes from software that is well-known for producing beautiful plots. Tableau has an excellent set of color schemes to use, ranging from grayscale to colored to color blind-friendly. Which brings me to my next point…

Many graphic designers completely forget about color blindness, which affects over 5% of the viewers of their graphics. For example, a plot using red and green to differentiate two categories of data is going to be completely incomprehensible for anyone with red-green color blindness. Whenever possible, stick to using color blind-friendly color schemes, such as Tableau’s “Color Blind 10.”

Required libraries

You’ll need the following Python libraries installed to run this code, and all of the code should be run inside an IPython Notebook:

  • IPython
  • matplotlib
  • pandas

The Anaconda Python distribution provides an easy double-click installer that includes all of the libraries you’ll need.

Blah, blah, blah… let’s get to the code

Now that we’ve covered the basics of graphic design, let’s dive into the code. I’ll explain the “what” and “why” of each line of code with inline comments.

Line plots

percent-bachelors-degrees-women-usa

%pylab inline
from pandas import read_csv

# Read the data into a pandas DataFrame.
gender_degree_data = read_csv("http://www.randalolson.com/wp-content/uploads/percent-bachelors-degrees-women-usa.csv")

# These are the "Tableau 20" colors as RGB.
tableau20 = [(31, 119, 180), (174, 199, 232), (255, 127, 14), (255, 187, 120),
             (44, 160, 44), (152, 223, 138), (214, 39, 40), (255, 152, 150),
             (148, 103, 189), (197, 176, 213), (140, 86, 75), (196, 156, 148),
             (227, 119, 194), (247, 182, 210), (127, 127, 127), (199, 199, 199),
             (188, 189, 34), (219, 219, 141), (23, 190, 207), (158, 218, 229)]

# Scale the RGB values to the [0, 1] range, which is the format matplotlib accepts.
for i in range(len(tableau20)):
    r, g, b = tableau20[i]
    tableau20[i] = (r / 255., g / 255., b / 255.)

# You typically want your plot to be ~1.33x wider than tall. This plot is a rare
# exception because of the number of lines being plotted on it.
# Common sizes: (10, 7.5) and (12, 9)
figure(figsize=(12, 14))

# Remove the plot frame lines. They are unnecessary chartjunk.
ax = subplot(111)
ax.spines["top"].set_visible(False)
ax.spines["bottom"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["left"].set_visible(False)

# Ensure that the axis ticks only show up on the bottom and left of the plot.
# Ticks on the right and top of the plot are generally unnecessary chartjunk.
ax.get_xaxis().tick_bottom()
ax.get_yaxis().tick_left()

# Limit the range of the plot to only where the data is.
# Avoid unnecessary whitespace.
ylim(0, 90)
xlim(1968, 2014)

# Make sure your axis ticks are large enough to be easily read.
# You don't want your viewers squinting to read your plot.
yticks(range(0, 91, 10), [str(x) + "%" for x in range(0, 91, 10)], fontsize=14)
xticks(fontsize=14)

# Provide tick lines across the plot to help your viewers trace along
# the axis ticks. Make sure that the lines are light and small so they
# don't obscure the primary data lines.
for y in range(10, 91, 10):
    plot(range(1968, 2012), [y] * len(range(1968, 2012)), "--", lw=0.5, color="black", alpha=0.3)

# Remove the tick marks; they are unnecessary with the tick lines we just plotted.
plt.tick_params(axis="both", which="both", bottom="off", top="off",
                labelbottom="on", left="off", right="off", labelleft="on")

# Now that the plot is prepared, it's time to actually plot the data!
# Note that I plotted the majors in order of the highest % in the final year.
majors = ['Health Professions', 'Public Administration', 'Education', 'Psychology',
          'Foreign Languages', 'English', 'Communications\nand Journalism',
          'Art and Performance', 'Biology', 'Agriculture',
          'Social Sciences and History', 'Business', 'Math and Statistics',
          'Architecture', 'Physical Sciences', 'Computer Science',
          'Engineering']

for rank, column in enumerate(majors):
    # Plot each line separately with its own color, using the Tableau 20
    # color set in order.
    plot(gender_degree_data.Year.values,
            gender_degree_data[column.replace("\n", " ")].values,
            lw=2.5, color=tableau20[rank])
    
    # Add a text label to the right end of every line. Most of the code below
    # is adding specific offsets y position because some labels overlapped.
    y_pos = gender_degree_data[column.replace("\n", " ")].values[-1] - 0.5
    if column == "Foreign Languages":
        y_pos += 0.5
    elif column == "English":
        y_pos -= 0.5
    elif column == "Communications\nand Journalism":
        y_pos += 0.75
    elif column == "Art and Performance":
        y_pos -= 0.25
    elif column == "Agriculture":
        y_pos += 1.25
    elif column == "Social Sciences and History":
        y_pos += 0.25
    elif column == "Business":
        y_pos -= 0.75
    elif column == "Math and Statistics":
        y_pos += 0.75
    elif column == "Architecture":
        y_pos -= 0.75
    elif column == "Computer Science":
        y_pos += 0.75
    elif column == "Engineering":
        y_pos -= 0.25
    
    # Again, make sure that all labels are large enough to be easily read
    # by the viewer.
    text(2011.5, y_pos, column, fontsize=14, color=tableau20[rank])
    
# matplotlib's title() call centers the title on the plot, but not the graph,
# so I used the text() call to customize where the title goes.

# Make the title big enough so it spans the entire plot, but don't make it
# so big that it requires two lines to show.

# Note that if the title is descriptive enough, it is unnecessary to include
# axis labels; they are self-evident, in this plot's case.
text(1995, 93, "Percentage of Bachelor's degrees conferred to women in the U.S.A."
       ", by major (1970-2012)", fontsize=17, ha="center")

# Always include your data source(s) and copyright notice! And for your
# data sources, tell your viewers exactly where the data came from,
# preferably with a direct link to the data. Just telling your viewers
# that you used data from the "U.S. Census Bureau" is completely useless:
# the U.S. Census Bureau provides all kinds of data, so how are your
# viewers supposed to know which data set you used?
text(1966, -8, "Data source: nces.ed.gov/programs/digest/2013menu_tables.asp"
       "\nAuthor: Randy Olson (randalolson.com / @randal_olson)"
       "\nNote: Some majors are missing because the historical data "
       "is not available for them", fontsize=10)

# Finally, save the figure as a PNG.
# You can also save it as a PDF, JPEG, etc.
# Just change the file extension in this call.
# bbox_inches="tight" removes all the extra whitespace on the edges of your plot.
savefig("percent-bachelors-degrees-women-usa.png", bbox_inches="tight");

Line plots with error bars

chess-number-ply-over-time

%pylab inline
from pandas import read_csv
from scipy.stats import sem

# This function takes an array of numbers and smoothes them out.
# Smoothing is useful for making plots a little easier to read.
def sliding_mean(data_array, window=5):
    data_array = array(data_array)
    new_list = []
    for i in range(len(data_array)):
        indices = range(max(i - window + 1, 0),
                        min(i + window + 1, len(data_array)))
        avg = 0
        for j in indices:
            avg += data_array[j]
        avg /= float(len(indices))
        new_list.append(avg)
        
    return array(new_list)

# Due to an agreement with the ChessGames.com admin, I cannot make the data
# for this plot publicly available. This function reads in and parses the
# chess data set into a tabulated pandas DataFrame.
chess_data = read_chess_data()

# These variables are where we put the years (x-axis), means (y-axis), and error bar values.
# We could just as easily replace the means with medians,
# and standard errors (SEMs) with standard deviations (STDs).
years = chess_data.groupby("Year").PlyCount.mean().keys()
mean_PlyCount = sliding_mean(chess_data.groupby("Year").PlyCount.mean().values,
                                window=10)
sem_PlyCount = sliding_mean(chess_data.groupby("Year").PlyCount.apply(sem).mul(1.96).values,
                                window=10)

# You typically want your plot to be ~1.33x wider than tall.
# Common sizes: (10, 7.5) and (12, 9)
figure(figsize=(12, 9))

# Remove the plot frame lines. They are unnecessary chartjunk.
ax = subplot(111)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)

# Ensure that the axis ticks only show up on the bottom and left of the plot.
# Ticks on the right and top of the plot are generally unnecessary chartjunk.
ax.get_xaxis().tick_bottom()
ax.get_yaxis().tick_left()

# Limit the range of the plot to only where the data is.
# Avoid unnecessary whitespace.
ylim(63, 85)

# Make sure your axis ticks are large enough to be easily read.
# You don't want your viewers squinting to read your plot.
xticks(range(1850, 2011, 20), fontsize=14)
yticks(range(65, 86, 5), fontsize=14)

# Along the same vein, make sure your axis labels are large
# enough to be easily read as well. Make them slightly larger
# than your axis tick labels so they stand out.
ylabel("Ply per Game", fontsize=16)

# Use matplotlib's fill_between() call to create error bars.
# Use the dark blue "#3F5D7D" as a nice fill color.
fill_between(years, mean_PlyCount - sem_PlyCount,
                mean_PlyCount + sem_PlyCount, color="#3F5D7D")

# Plot the means as a white line in between the error bars. 
# White stands out best against the dark blue.
plot(years, mean_PlyCount, color="white", lw=2)

# Make the title big enough so it spans the entire plot, but don't make it
# so big that it requires two lines to show.
title("Chess games are getting longer", fontsize=22)

# Always include your data source(s) and copyright notice! And for your
# data sources, tell your viewers exactly where the data came from,
# preferably with a direct link to the data. Just telling your viewers
# that you used data from the "U.S. Census Bureau" is completely useless:
# the U.S. Census Bureau provides all kinds of data, so how are your
# viewers supposed to know which data set you used?
xlabel("\nData source: www.ChessGames.com | "
        "Author: Randy Olson (randalolson.com / @randal_olson)", fontsize=10)

# Finally, save the figure as a PNG.
# You can also save it as a PDF, JPEG, etc.
# Just change the file extension in this call.
# bbox_inches="tight" removes all the extra whitespace on the edges of your plot.
savefig("evolution-of-chess/chess-number-ply-over-time.png", bbox_inches="tight");

Histograms

chess-elo-rating-distribution

%pylab inline
from pandas import read_csv

# Due to an agreement with the ChessGames.com admin, I cannot make the data
# for this plot publicly available. This function reads in and parses the
# chess data set into a tabulated pandas DataFrame.
chess_data = read_chess_data()

# You typically want your plot to be ~1.33x wider than tall.
# Common sizes: (10, 7.5) and (12, 9)
figure(figsize=(12, 9))

# Remove the plot frame lines. They are unnecessary chartjunk.
ax = subplot(111)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)

# Ensure that the axis ticks only show up on the bottom and left of the plot.
# Ticks on the right and top of the plot are generally unnecessary chartjunk.
ax.get_xaxis().tick_bottom()
ax.get_yaxis().tick_left()

# Make sure your axis ticks are large enough to be easily read.
# You don't want your viewers squinting to read your plot.
xticks(fontsize=14)
yticks(range(5000, 30001, 5000), fontsize=14)

# Along the same vein, make sure your axis labels are large
# enough to be easily read as well. Make them slightly larger
# than your axis tick labels so they stand out.
xlabel("Elo Rating", fontsize=16)
ylabel("Count", fontsize=16)

# Plot the histogram. Note that all I'm passing here is a list of numbers.
# matplotlib automatically counts and bins the frequencies for us.
# "#3F5D7D" is the nice dark blue color.
# Make sure the data is sorted into enough bins so you can see the distribution.
hist(list(chess_data.WhiteElo.values) + list(chess_data.BlackElo.values),
        color="#3F5D7D", bins=100)

# Always include your data source(s) and copyright notice! And for your
# data sources, tell your viewers exactly where the data came from,
# preferably with a direct link to the data. Just telling your viewers
# that you used data from the "U.S. Census Bureau" is completely useless:
# the U.S. Census Bureau provides all kinds of data, so how are your
# viewers supposed to know which data set you used?
text(1300, -5000, "Data source: www.ChessGames.com | "
       "Author: Randy Olson (randalolson.com / @randal_olson)", fontsize=10)

# Finally, save the figure as a PNG.
# You can also save it as a PDF, JPEG, etc.
# Just change the file extension in this call.
# bbox_inches="tight" removes all the extra whitespace on the edges of your plot.
savefig("chess-elo-rating-distribution.png", bbox_inches="tight");

Easy interactives

As an added bonus, thanks to plot.ly, it only takes one more line of code to turn your matplotlib plot into an interactive.


More Python plotting libraries

In this tutorial, I focused on making data visualizations with only Python’s basic matplotlib library. If you don’t feel like tweaking the plots yourself and want the library to produce better-looking plots on its own, check out the following libraries.


Recommended reading

Edward Tufte has been a pioneer of the “simple, effective plots” approach. Most of the graphic design of my visualizations has been inspired by reading his books.

The Visual Display of Quantitative Information is a classic book filled with plenty of graphical examples that everyone who wants to create beautiful data visualizations should read.

Envisioning Information is an excellent follow-up to the first book, again with a plethora of beautiful graphical examples.

There are plenty of other books out there about beautiful graphical design, but the two books above are the ones I found the most educational and inspiring.


Want to know how I made any of my other plots? Leave a comment and put in a request.

Average IQ of students by college major and gender ratio

After all the controversy that arose after I posted my breakdown of college majors by gender last week, I promised myself I’d stay away from controversial gender-related topics for a while. But when I ran across an ETS-curated data set of average student IQs by college major, I couldn’t avoid putting this visualization together. Below, I plotted several college major’s estimated average student IQ over the gender ratio of that major.

The result? A shockingly clear correlation: the more female-dominated a college major is, the lower the average IQ of the students studying in the major. A naive reader may look at this graph and conclude that men are smarter than women, but it is vital to note that, on average, men and women have about the same IQ.

iq-by-college-major-gender

By popular request, here’s an interactive version of the above chart: https://plot.ly/~etpinard/330/us-college-majors-average-iq-of-students-by-gender-ratio/

IQs are typically classified as follows:

  • 130+: Very superior intelligence
  • 120-129: Superior
  • 110-119: Above average
  • 90-109: Average

Considering that many of the female-dominated majors heavily involve interpersonal interactions, my initial thought was that this all made sense: Women are widely known to be more socially-inclined and nurturing than men, so we would expect to see them dominate fields that heavily involve people. But how does that explain the drastic IQ differences between male- and female-dominated fields, if the average man and woman have the same IQ?

The answer comes from the fact that the IQ score here is estimated from the students’ SAT score. This isn’t an altogether unreasonable approach: Several studies have shown a strong correlation between SAT scores and IQ scores. But if we break down the SAT score by Verbal and Quantitative, we see why this IQ estimation is potentially misleading.

verbal-by-college-major-gender

If we re-make the first plot against the Verbal SAT score, we see that it’s basically a wash: there’s no correlation between a major’s gender ratio and the average student’s Verbal SAT score.

quant-by-college-major-gender

When we plot the students’ Quantitative SAT score against the major’s gender ratio, we see the negative correlation appear again. This tells us that the original plot is actually showing preference for quantitative majors: The higher the estimated IQ, the more quantitative/analytical the major, and the fewer women enrolling in those majors.

This brings up an interesting question of how valuable the SAT is as a standardized test across all majors, if a higher SAT score is really only indicating that the student is better at solving quantitative/analytical problems. Not all majors require a high analytical aptitude, after all.

Technical bits

Some of my readers requested the R^2 for the above plots. Here they are:

The R^2 on the IQ vs major’s gender ratio graph is 0.601

The R^2 on the Verbal SAT vs. major’s gender ratio graph is 0.019

The R^2 on the Quantitative SAT vs. major’s gender ratio graph is 0.738

The R^2 between Quantitative SAT score and Verbal SAT score is 0.027

For those who want to know what R^2 means: http://en.wikipedia.org/wiki/Coefficient_of_determination

Notice about the IQ data

Since I posted this article, the veracity of the IQ data set has been brought into question. I think StatisticBrain is a fairly reliable data source, but it’s possible this data set has issues. I haven’t seen anything conclusive yet that would make me take this post down, but I write this here so readers can come to their own opinion about what this data shows, and how much to trust it.

  1. The data source says “Graduate Record Examination scores” then goes on to list SAT scores. Which is it? I wish I knew.
  2. A similar data set has shown up on another blog years ago that makes it look like GRE scores. But that’s some random blog, so who knows how reliable the information on there is.

Have any tips on the data source? Please email them over.

Why the Dutch are so tall

It’s fairly common knowledge that the Dutch are some of the tallest people in the world. Whereas the average American man measures in at about 5’9″ (176 cm), the average Dutch man stands at well over 6′ (185 cm) tall. What is it about this small, traditionally seafaring nation that breeds such extraordinarily tall people? Contrary to popular belief, it’s not to keep their heads above water.

To provide a historical perspective, I charted the median male height for various countries between 1820 and 2013 below. It was surprisingly difficult to find this kind of height data, but fortunately many of these country’s militaries meticulously recorded the median height of their new conscripts every year. These records provide a convenient (albeit somewhat biased) sample of the young generation of men during the time period.

The raw data for this chart is available on figshare here. You’ll notice that there’s several holes in the data set, which I simply extrapolated the trends over. I compiled this data set from half a dozen different sources, so if you plan to use this data set for any of your research, I strongly suggest double-checking the sources I list there.

historical-median-male-height

The most surprising revelation here is that the Dutch became the tallest Europeans only recently in the 1980s. Before then, they were one of the shortest people in Europe at only 5’5″ (165 cm) for first half of the 19th century. What changed after 1850 that led to this explosive Dutch growth? Prof. Drukker at the University of Groningen suggests that it has a lot to do with the distribution of wealth. As Cecily Layzell writes:

The Dutch growth spurt of the mid-19th century coincided with the establishment of the first liberal democracy. Before this time, [The Netherlands] had grown rich off its colonies but the wealth had stayed in the hands of the elite. After this time, the wealth began to trickle down to all levels of society, the average income went up and so did the height.

This explanation makes intuitive sense: It’s well-known that we’re much taller than our ancestors 100 years ago because of improved nutrition, especially in our adolescent years. If the average citizen has more money to buy healthy food, then we would expect their children to grow bigger, stronger, and taller. To add more evidence to the pile: GapMinder clearly shows that the Dutch income per capita stagnated until the mid-late 19th century, right when the Dutch median height started rising as well.

wealth-height-netherlands

So, there we have it. Make sure all of our citizens are wealthy enough to buy healthy food and their children will grow up to be bigger, stronger, and healthier. It’s not as fun an answer as we would’ve hoped for, but at least we can put this “head above water” theory to rest!


Edit (6/29/2014): Several of my readers have rightly pointed out that although this data explains why many Europeans have grown taller in the past 150 years, it doesn’t necessarily explain why the Dutch are so much taller than the rest of Europe. There are a couple possibilities that merit further investigation:

  • The Dutch diet: The average Dutch citizen eats a lot of breads, meats, cheese, and drinks a lot of milk — moreso than many of their European counterparts.
  • The Dutch genes: It’s fairly well-known that pre-civilization humans were much taller than their civilized counterparts. It’s possible that the Dutch ancestors from thousands of years ago were always taller, but Dutch diet and nutrition limited how large they grew. That still leaves open the question of why the Dutch ancestors were taller than the rest, however.

We can only forecast the weather a few days into the future

Another fascinating point from Nate Silver’s The Signal and the Noise is where he talks about how far into the future we can forecast weather. It’s one thing to forecast what tomorrow’s weather will be like, but what about next weekend’s weather? Or next month’s? Silver provided one chart, with data courtesy of Eric Floehr at ForecastWatch.com, that highlights just how hard it is to forecast weather. I’ve reproduced that chart below.

weather-forecast-errors

This chart compares three major weather forecasting methods:

  1. Persistence: This method assumes that tomorrow’s weather will be a lot like today’s weather. Tomorrow’s temperature will be today’s temperature ± a few degrees.
  2. Climatology: Since we have decades of historical weather data, we can average what happened in the past on each day to forecast what the weather will be like. This method assumes that the weather on July 4, 2014 in East Lansing, Michigan will be a lot like the weather in East Lansing on July 4 in all the previous years.
  3. Commercial Forecasting: Now that the National Weather Service provides so much data about the current weather, we can simulate the weather down to the molecule and create a model of what the weather is going to be like tomorrow.

As we’d expect, persistence forecasting performs pretty terribly. If you just take a look at your local weather for the past week, it’s rare for temperatures to follow a linear pattern of rising or falling temperatures for more than a day. Even averaging historical data is consistently off the mark by as much as 7°F. The real winners here are the weather models, which can forecast the correct temperature within 4°F up to 3 days out.

But even weather models have their limitations: Any forecasts more than a week out are going to be less accurate than climatological forecasts on average, which we’ve already established makes for a pretty poor baseline. By a week out, small inaccuracies in the weather models build up exponentially, to the point that the model is predicting temperatures far divorced from reality. This observation leads me to wonder why commercial weather forecasting sites like AccuWeather even bother providing forecasts up to two weeks out, considering we’d be better off just looking at the historical averages at that point.

So don’t bother looking past the 5-day forecasts on your favorite weather site. More likely than not, their forecasts are wrong.

Accuracy of three major weather forecasting services

For the past month, I’ve been slowly working my way through Nate Silver’s book, The Signal and the Noise. It’s really a great read, but if you’re a regular reader on this blog, I’d imagine you’ve already read it. This book is loaded with all kinds of great examples of where predictive analytics succeeds and fails, and I decided to highlight his weather forecasting example because of how surprising it was to me.

For those who aren’t in the know: Most of the weather forecasts out there for the U.S. are originally based on data from the U.S. National Weather Service, a government-run agency tasked with measuring and predicting everything related to weather across all of North America. Commercial companies like The Weather Channel then build off of those data and forecasts and try to produce a “better” forecast — a fairly lucky position to be in, if you consider that the NWS does a good portion of the heavy lifting for them.

We all rely on these weather forecasts to plan our day-to-day activities. For example, before planning a summer grill out over the weekend, we’ll check our favorite weather web site to see whether it’s going to rain. Of course, we’re always left to wonder: Just how accurate are these forecasts? Plotted below is the accuracy of three major weather forecasting services. Note that a perfect forecast means that, e.g., the service forecasted a 20% chance of rain for 40 days of the year, and exactly 8 (20%) of those days actually had rain.

weather-forecast-accuracy-flipped

There’s some pretty startling trends here. For one, The Weather Service is pretty accurate for the most part, and that’s because they consistently try to provide the most accurate forecasts possible. They pride themselves on the fact that if you go to Weather.gov and it says there’s a 60% chance of rain, there really is a 60% chance of rain that day.

With the advantage of having The Weather Service’s forecasts and data as a starting point, it’s perhaps unsurprising that The Weather Channel manages to be slightly more accurate in their forecasts. The only major inaccuracy they have, which is surprisingly consistent, is in the lower and higher probabilities of raining: Weather.com often forecasts that there’s a higher probability of raining than there really is.

This phenomenon is commonly known as a wet bias, where weather forecasters will err toward predicting more rain than there really is. After all, we all take notice when forecasters say there won’t be rain and it ends up raining (= ruined grill out!); but when they predict rain and it ends up not raining, we’ll shrug it off and count ourselves lucky.

The worst part of this graph is the performance of local TV meteorologists. These guys consistently over-predict rain so much that it’s difficult to place much confidence in their forecasts at all. As Silver notes:

TV weathermen they aren’t bothering to make accurate forecasts because they figure the public won’t believe them anyway. But the public shouldn’t believe them, because the forecasts aren’t accurate.

Even worse, some meteorologists have admitted that they purposely fudge their rain forecasts to improve ratings. What’s a better way to keep you tuning in every day than to make you think it’s raining all the time, and they’re the only ones saving you from soaking your favorite outfit?

For me, the big lesson learned from this chapter in Silver’s book is that I’ll be tuning in to Weather.gov for my weather forecasts from now on. Most notably because, as Silver puts it:

The further you get from the government’s original data, and the more consumer facing the forecasts, the worse this bias becomes. Forecasts “add value” by subtracting accuracy.