| country | continent | year | life | population | gdp |
|---|---|---|---|---|---|
| Afghanistan | Asia | 2007 | 43.828 | 31889923 | 974.5803 |
| Albania | Europe | 2007 | 76.423 | 3600523 | 5937.0295 |
| Algeria | Africa | 2007 | 72.301 | 33333216 | 6223.3675 |
| Angola | Africa | 2007 | 42.731 | 12420476 | 4797.2313 |
| Argentina | Americas | 2007 | 75.320 | 40301927 | 12779.3796 |
41 Two variables
You have two columns. What can they tell you together? The two kinds of column, continuous and categorical, decide which question you can ask. Two measurements ask about relationship: when one is high, is the other high too? When one measurement is a time, the question can be quantity: how large was the value in each year? A category and a measurement ask about comparison: is the measurement larger in some groups than in others?
41.2 What is the trend through the points?
The first scatter in this chapter draws one dot for each of 142 countries. The dots show that life expectancy rises with GDP per person. Below $5,000, though, they spread from 40 to 74 years, so the typical value is hard to see. You want the trend, and you still want every country on the page. Layering a derived line over the points gives you both. + puts both marks in one panel, and * smooth fits one trend curve through all the points:
data(gapminder_2007) + x(gdp) + y(life) +
point + color(continent) +
line * smooth(data(gapminder_2007) + x(col.gdp) + y(col.life) +
point + color(col.continent) +
line * smooth)data(gapminder_2007) + x(:gdp) + y(:life) + point + color(:continent) +
line * smoothplot(data(gapminder_2007), x(col.gdp), y(col.life), point,
color(col.continent), layer(line, smooth))“Given gapminder 2007: x is gdp, y is life, points colored by continent, and also a line derived by smooth.”
The curve climbs steeply on the left and much more slowly on the right. Most of its rise happens below $10,000 of GDP per person. At the far right the curve runs above the dots of the five richest countries. A smooth curve is least reliable at the edge of the data, where there are few points.
The positions are written before any mark, so both layers share them. color is written after point, so only the points are split into one group per continent. The curve is fitted to all 142 countries, not to any one continent. Owning chapters: Transforms for smooth, Encoding scope for that rule.
A curve shows the shape of a trend, but it does not give the trend as one number. On a log scale of GDP the dots lie close to a straight line, as the first recipe in this chapter showed. So a straight line is a good summary of them. * straight fits the least-squares line, and ribbon * straight_band adds the band that shows how precisely the line is known:
data(gapminder_2007) + x(gdp, scale = "log") + y(life) +
point + color(continent) +
ribbon * straight_band + style(opacity = 0.3) +
line * straight(data(gapminder_2007) + x(col.gdp, scale = "log") + y(col.life) +
point + color(col.continent) +
ribbon * straight_band + style(opacity = 0.3) +
line * straight)data(gapminder_2007) + x(:gdp, scale = "log") + y(:life) + point +
color(:continent) + ribbon * straight_band + style(opacity = 0.3) +
line * straightplot(data(gapminder_2007), x(col.gdp, { scale: "log" }), y(col.life),
point, color(col.continent), layer(ribbon, straight_band),
style({ opacity: 0.3 }), layer(line, straight))“Given gapminder 2007: x is gdp on a log scale, y is life, points colored by continent, and also a ribbon derived by straight_band, and also a line derived by straight.”
The slope of the line gives the trend as one number: about 16.6 more years of life expectancy for each tenfold rise in GDP per person, such as from $1,000 to $10,000. The band is only a few years wide, because 142 countries are enough to know the line precisely. Even so, most countries lie outside it. The band describes the line, not the countries. Owning chapters: Transforms for straight and straight_band, Scales for why the line is fitted against the logarithm.
41.3 How much was there at each time?
The second question in the opening was quantity, which pairs a time with an amount. The actuals table holds one sales figure per year. Sometimes the question is not whether sales rose but how large they were in each year. A line says where the value was; an area says how much there was. The fill is the quantity, so it starts from zero:
| year | sales |
|---|---|
| 2019 | 120 |
| 2020 | 135 |
| 2021 | 128 |
| 2022 | 152 |
| 2023 | 168 |
data(actuals) + area + x(year) + y(sales)data(actuals) + area + x(col.year) + y(col.sales)data(actuals) + area + x(:year) + y(:sales)plot(data(actuals), area, x(col.year), y(col.sales))“Given the actuals: an area, x is year, y is sales.”
Sales stay between 120 and 168 in every year. Measured from zero, the fall in 2021, from 135 to 128, is small.
One atom away. Some quantities have no true zero, where zero means none at all, such as a temperature in degrees Celsius. For those, write line instead of area. Owning chapters: Line, Area.
41.4 What is the typical value per group?
The last question in the opening was comparison, which pairs a category with a measurement. Each continent holds many countries, and each country has its own life expectancy. To compare the continents you need one number for each, so you summarize the measurement within each category. The transforms that summarize this way are the aggregation family: sum, mean, median, max, min and quantile. Each one groups the rows by the category and reduces the measurement to one value per group. Only quantile needs a number of its own, the probability: quantile(0.9) gives the 90th percentile of each group. Here is mean, the average life expectancy in each continent:
data(gapminder_2007) + bar * mean + x(continent) + y(life)data(gapminder_2007) + bar * mean + x(col.continent) + y(col.life)data(gapminder_2007) + bar * mean + x(:continent) + y(:life)plot(data(gapminder_2007), layer(bar, mean), x(col.continent),
y(col.life))“Given gapminder 2007: bars derived by mean, x is continent, y is life.”
Oceania’s countries average 81 years and Europe’s 78, while Africa’s average 55.
One atom away. A few very high or very low values can change a mean noticeably. They change a median much less. When that matters, swap mean for median:
data(gapminder_2007) + bar * median + x(continent) + y(life)data(gapminder_2007) + bar * median + x(col.continent) + y(col.life)data(gapminder_2007) + bar * median + x(:continent) + y(:life)plot(data(gapminder_2007), layer(bar, median), x(col.continent),
y(col.life))“Given gapminder 2007: bars derived by median, x is continent, y is life.”
Africa’s bar falls by about two years and Asia’s rises by about the same. The other three change by less than a year. Where the two bars differ, a few very high or very low values have moved the mean. In Africa, seven countries live 71 to 76 years, well above the other 45. Owning chapters: Bar, Transforms.
41.5 What does every value per group look like?
A bar * mean gives each continent one number. That number hides how much life expectancy differs among the continent’s countries. Sometimes you want to see every country, not the mean. The plot that shows every row is the strip plot. Its sentence differs from the bar * mean sentence in two places: * mean is gone, and point is written where bar was. Each row is one dot, at its own height in its category’s slot:
data(gapminder_2007) + point + x(continent) + y(life)data(gapminder_2007) + point + x(col.continent) + y(col.life)data(gapminder_2007) + point + x(:continent) + y(:life)plot(data(gapminder_2007), point, x(col.continent), y(col.life))“Given gapminder 2007: points, x is continent, y is life.”
On each vertical line, dots with nearly the same value cover each other, and * jitter spreads them sideways. It moves them along the category axis only, so every dot keeps its exact height. style() sets a value instead of mapping a column. Here it sets opacity to 0.6, below the default, so you can see where the dots overlap:
data(gapminder_2007) + point * jitter + x(continent) + y(life) + style(opacity = 0.6)data(gapminder_2007) + point * jitter + x(col.continent) + y(col.life) + style(opacity = 0.6)data(gapminder_2007) + point * jitter + x(:continent) + y(:life) +
style(opacity = 0.6)plot(data(gapminder_2007), layer(point, jitter), x(col.continent),
y(col.life), style({ opacity: 0.6 }))“Given gapminder 2007: points derived by jitter, x is continent, y is life.”
One atom away. Swap the positions and the strips run horizontally, which helps when the category names are long. Owning chapters: Point, Transforms, Setting vs mapping.
41.6 What if the table already holds the count?
The mean and median recipes derived a number that was not in the table. The medals table holds one row per country, and the question is how many bronze medals each one won. That count is already a column, bronze, so no transform is needed. bar draws the numbers exactly as the table gives them:
| country | gold | silver | bronze |
|---|---|---|---|
| USA | 46 | 37 | 38 |
| China | 38 | 31 | 22 |
| Great Britain | 29 | 17 | 19 |
| Russia | 19 | 18 | 9 |
| Germany | 17 | 10 | 15 |
data(medals) + bar + x(country) + y(bronze)data(medals) + bar + x(col.country) + y(col.bronze)data(medals) + bar + x(:country) + y(:bronze)plot(data(medals), bar, x(col.country), y(col.bronze))“Given the medals: bars, x is country, y is bronze.”
One atom away. order() sorts the categories by a column you name, and desc = TRUE puts the largest first. The table already lists the countries by gold medals, and their bronze counts are in nearly the same order. Only Germany and Russia change places:
data(medals) + bar + x(country) + y(bronze) + order(bronze, desc = TRUE)data(medals) + bar + x(col.country) + y(col.bronze) + order(col.bronze, desc = True)data(medals) + bar + x(:country) + y(:bronze) +
order(:bronze, desc = true)plot(data(medals), bar, x(col.country), y(col.bronze),
order(col.bronze, { desc: true }))“Given the medals: bars, x is country, y is bronze, ordered by bronze, largest first.”
The engine counts nothing on its own. If you want a computation, you write its name. That is why How many of each? writes * count and this recipe does not. Owning chapters: Bar, Data.
41.7 What this section refuses
Every recipe in this chapter used at least one continuous column. Suppose both of your columns are categorical, a country and its continent. You write bar, with one column on each axis. A bar needs at least one continuous axis, because the value on that axis is what sets the length of the bar. Two categories give the bar nothing to measure. The one number you can still make from them is a count of rows, so the refusal names count:
data(gapminder_2007) + bar + x(country) + y(continent)data(gapminder_2007) + bar + x(col.country) + y(col.continent)data(gapminder_2007) + bar + x(:country) + y(:continent)plot(data(gapminder_2007), bar, x(col.country), y(col.continent))Error:
! gog: `bar` has categorical columns on both axes — `x(country)` and `y(continent)` — so there is nothing for it to measure. One axis must be a number: that is the length of the bar. To count the rows instead, count one column, `bar * count + x(country)` or `bar * count + x(continent)`, and `color()` splits each bar by the other. To keep both, `bar * count + x(country) + y(continent) + space()` stands one bar on each pair in the cube.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
Adding * count to this sentence still refuses. bar * count draws its count along the axis that has no column, so one axis must stay empty. To count the countries in each continent, remove country and write bar * count + x(continent), the sentence of How many of each?.