If a graph is for seeing a pattern, a table is for reading an exact value. Ask which one your reader needs. If they need to feel the shape of a trend, draw it. If they need to look up “what was the bust rate for the Jets, exactly?” or compare a handful of precise numbers side by side, give them a table. A table is not a failed graph; it is a different instrument for a different job, and, like a graph, it can be honest or it can bury the point under too many digits.
We continue with the same real data from The Sports Page we used for graphics, so you can feel the difference between the two instruments on identical numbers.
27.1 Learning Objectives
Decide when a table serves the reader better than a graph.
Round to meaningful precision - kill false precision.
Order rows to carry information, and align numbers so they can be compared.
Summarize to the question, rather than dumping the raw data.
27.2 When a Table Beats a Graph
A table wins when the numbers are few, the exact values matter, or the reader will look things up rather than scan for a shape. The strikeout trend was a job for a line. But “what did each decade actually look like, in numbers I can quote?” is a job for a small table. Summarize the 42 years down to five decade averages - few rows, exact values, quotable:
NoteWorking in SPSS, Julia, or Python?
This chapter uses data/mlb_league_by_year.csv and data/nfl_draft_by_team.csv directly - no setup needed. See Getting the Book’s Data if you want the setup files for your language.
library(tidyverse) # dplyr, ggplot2, purrr, tibble, readr, stringr, forcatssource("_common.R") # book-wide helpers: round2(), fmt_p(), tidy2()mlb <-read_csv("data/mlb_league_by_year.csv", show_col_types =FALSE) |>mutate(decade =paste0(floor(year /10) *10, "s"))dec <- mlb |>summarise(across(c(K_9, BB_9, HR_9, AVG), mean), .by = decade) |>arrange(decade)# This chapter is about choosing digits on purpose, so it sets them by hand# rather than using the book's usual round2(): batting average is quoted to# three decimals, the way baseball has always quoted it.knitr::kable(dec, digits =c(0, 2, 2, 2, 3),col.names =c("Decade", "K/9", "BB/9", "HR/9", "AVG"),caption ="MLB by the decade: the strikeout climb, in exact numbers.")
MLB by the decade: the strikeout climb, in exact numbers.
Decade
K/9
BB/9
HR/9
AVG
1980s
5.70
3.30
0.87
0.257
1990s
6.18
3.45
0.96
0.264
2000s
6.62
3.38
1.08
0.265
2010s
7.87
3.12
1.08
0.254
2020s
8.69
3.31
1.19
0.244
* Summarise 42 years down to five decade averages.
GET DATA /TYPE=TXT /FILE='data/mlb_league_by_year.csv'
/DELIMITERS=',' /QUALIFIER='"' /FIRSTCASE=2
/VARIABLES=year F16.6 OPS F16.6 K_PA F16.6 BB_PA F16.6 HR_PA F16.6 AVG F16.6 ERA F16.6 K_9 F16.6 BB_9 F16.6 HR_9 F16.6.
COMPUTE decade = TRUNC(year / 10) * 10.
FORMATS decade (F4.0).
EXECUTE.
MEANS TABLES=K_9 BB_9 HR_9 AVG BY decade
/CELLS=MEAN.
Five rows, and you can quote any cell. The graph made you feel the strikeout climb; the table lets you say “K/9 went from about 5.7 in the 1980s to about 8.5 in the 2020s” without guessing off an axis. Same data, different instrument, different job.
27.3 Kill False Precision
The fastest way to make a table mislead, or simply to tire your reader, is to print every digit the computer produced. A bust rate stored as 0.5455 does not mean you know it to four decimals; with a few dozen draft picks, you barely know the second. Rounding is not sloppiness; it is honesty about how much you actually know. Watch the difference:
usingCSV, DataFrames, Plotsnfl = CSV.read("data/nfl_draft_by_team.csv", DataFrame)worst =sort(select(nfl, :team, :n_picks, :rate_bust_a, :rate_clean_hit),:rate_bust_a, rev =true)first(worst, 4) # too many digits for what the data can support
import pandas as pdimport matplotlib.pyplot as pltnfl = pd.read_csv("data/nfl_draft_by_team.csv")worst = (nfl.sort_values("rate_bust_a", ascending=False) [["team", "n_picks", "rate_bust_a", "rate_clean_hit"]])worst.head(4) # too many digits for what the data can support
The second table is easier to read and more truthful. Show the digits you can defend, and not one more.
27.4 Order Rows to Carry Information
A table’s row order is free information - spend it. An alphabetical list of teams makes the reader hunt; a list sorted by the thing you care about answers the question before they ask it. We already sorted by bust rate above (worst first), so the ranking is the message. Now show the two ends that a reader actually wants - the best and worst drafters - instead of dumping all 32:
ends <-bind_rows( worst |>slice_head(n =5) |>mutate(group ="Worst drafters", .before =1), worst |>slice_tail(n =5) |>mutate(group ="Best drafters", .before =1)) |>select(group, team, n_picks, rate_bust_a)knitr::kable(ends, row.names =FALSE,col.names =c("", "Team", "Picks", "Bust rate (%)"),caption ="The two ends of the ranking — the comparison the reader wanted.")
The two ends of the ranking — the comparison the reader wanted.
Team
Picks
Bust rate (%)
Worst drafters
LV
44
73
Worst drafters
CIN
52
72
Worst drafters
GB
49
69
Worst drafters
NYG
49
68
Worst drafters
LAR
51
68
Best drafters
BUF
46
48
Best drafters
MIN
48
48
Best drafters
KC
42
47
Best drafters
BAL
33
40
Best drafters
WAS
35
38
GET DATA /TYPE=TXT /FILE='data/nfl_draft_by_team.csv'
/DELIMITERS=',' /QUALIFIER='"' /FIRSTCASE=2
/VARIABLES=team A24 n_picks F16.6 n_judged F16.6 n_hof F16.6 n_bust_a F16.6 n_bust_b F16.6 n_bust_c F16.6 n_triple_bust F16.6 n_clean_hit F16.6 rate_bust_a F16.6 rate_bust_b F16.6 rate_bust_c F16.6 rate_triple_bust F16.6 rate_clean_hit F16.6 hof_per_pick F16.6.
* Show the two ends of the ranking, not all 32 rows.
* Flag the top and bottom five, then list only those.
COMPUTE rank_bust = $CASENUM.
COMPUTE ends = 0.
IF (rank_bust <= 5) ends = 1.
IF (rank_bust > 27) ends = 2.
VALUE LABELS ends 1 'Worst drafters' 2 'Best drafters'.
EXECUTE.
TEMPORARY.
SELECT IF (ends > 0).
LIST VARIABLES=ends team n_picks rate_bust_a.
group team n_picks rate_bust_a
18 Worst drafters LV 44 73.0
6 Worst drafters CIN 52 72.0
11 Worst drafters GB 49 69.0
23 Worst drafters NYG 49 68.0
17 Worst drafters LAR 51 68.0
3 Best drafters BUF 46 48.0
20 Best drafters MIN 48 48.0
15 Best drafters KC 42 47.0
2 Best drafters BAL 33 40.0
31 Best drafters WAS 35 38.0
Ten rows, sorted, grouped into the contrast that matters. A 32-row alphabetical dump would contain the same facts but communicate far less of them. Order and trim the table to the question; do not make the reader do your sorting.
ImportantThe honest-table checklist
Right instrument - do they need exact values or a lookup? If they need a shape, draw it instead.
Precision - round to what the data support; four decimals on a noisy rate is a lie of false confidence.
Order - sort rows by the quantity of interest; the ranking is information.
Trim - summarize to the comparison (decades, top/bottom); don’t dump raw rows.
Labels - human column names and units, so no cell needs a footnote to be understood.
27.5 Tables and Graphics Are Partners
The two instruments are not rivals; a good report uses both. Show the graph for the pattern - “strikeouts have soared” - and put the table beside it for the reader who needs the exact decade numbers to quote in their own argument. The graph earns the reader’s belief; the table arms them with the specifics. Reach for whichever answers the question in front of you, and often, reach for both.
27.6 Challenge
TipDo One Yourself
Rebuild the decade table but add a column for how much each stat changed from the previous decade. Which display - this table or the small-multiple graph from the last chapter - makes the change clearer, and for which reader?
Take the full 32-team draft data and design the single table you would actually publish. What do you sort by, how many rows do you keep, and to how many digits do you round? Defend each choice in one sentence.
Find a table in a real paper or report with obvious false precision (six decimals on a correlation, say). Round it honestly and note whether any conclusion actually depended on the digits you deleted.
27.7 Where We Go Next
You can now show data honestly - as a picture and as a table - and you have the full toolkit of this book behind you: describe, measure, model, and display. What remains is the hardest thing to teach and the most valuable to learn: judgment, which tool, when, and why. For that, we stop explaining and start showing, walking through real published analyses with every decision laid bare, beginning with the way The Sports Page thinks. Welcome to the apprenticeship.