p <- ggplot(data = gapminder,
mapping = aes(x = gdpPercap, y = lifeExp))
p + geom_point() +
geom_smooth(method = "gam") +
scale_x_log10()
## `geom_smooth()` using formula = 'y ~ s(x, bs = "cs")'
p <- ggplot(data = gapminder,
mapping = aes(x = gdpPercap, y = lifeExp))
p + geom_smooth(method = "gam") +
geom_point() +
scale_x_log10()
## `geom_smooth()` using formula = 'y ~ s(x, bs = "cs")'
By putting the geum_smooth() before geom_point(), the trend line and its gray shaded area are behind the dots and put backward. The dots will now sit directly on top of the line instead of being covered by it.
This tells me that the order of geum_smooth() and geom_point() in function matters as it will affect the way of presenting in the plot.
This is useful when drawing plots as you can decide which part(s) you want to highlight in the plot and therefore, what kind of information you want to deliver to your audience.
p <- ggplot(data = gapminder,
mapping = aes(x = pop,
y = lifeExp,
color = continent,
fill = continent))
p + geom_point() +
geom_smooth(method = "loess") +
scale_x_log10()
## `geom_smooth()` using formula = 'y ~ x'
Describing the plot: 1. The clusters separate vertically by continent
rather than horizontally by population size—Europe and Oceania sit near
the top, the Americas and Asia occupy the middle, and Africa sits
predominantly near the bottom.2. The smooth trend lines for each
continent stay mostly flat across the horizontal axis, showing that a
country’s population size has virtually no bearing on its life
expectancy.
The unit of observation in the dataset: is not by country because the dots should be randomly scattered if every dot represents a country.
p <- ggplot(data = gapminder,
mapping = aes(x = gdpPercap,
y = lifeExp))
p + geom_point(mapping = aes(color = log(pop))) +
scale_x_log10()
p <- ggplot(data = gapminder,
mapping = aes(x = gdpPercap,
y = lifeExp))
p + geom_point(mapping = aes(color = log(pop))) +
scale_x_sqrt()
p <- ggplot(data = gapminder,
mapping = aes(x = gdpPercap,
y = lifeExp))
p + geom_point(mapping = aes(color = log(pop))) +
scale_x_reverse()
p <- ggplot(data = gapminder,
mapping = aes(x = gdpPercap,
y = lifeExp))
p + geom_point(mapping = aes(color = log(pop))) +
scale_y_log10()
p <- ggplot(data = gapminder,
mapping = aes(x = gdpPercap,
y = lifeExp))
p + geom_point(mapping = aes(color = log(pop))) +
scale_y_sqrt()
p <- ggplot(data = gapminder,
mapping = aes(x = gdpPercap,
y = lifeExp))
p + geom_point(mapping = aes(color = log(pop))) +
scale_y_reverse()
Changes: scale_x_log 10(): Population grows by multiplying (powers of
10), so a log scale spreads countries out nicely and fairly.
scale_x_sqrt(): It pulls apart small populations a little bit and
shrinks large ones, but not nearly enough. scale_x_reverse(): It flips
the horizontal line backwards—putting big populations on the left and
tiny ones on the right. scale_y_log 10(): Squeezes older ages closer
together and stretches younger ages apart. scale_y_sqrt(): Squeezes
older ages closer together and stretches younger ages apart.
scale_y_reverse(): Puts longer lives at the bottom and shorter lives at
the top.
Can each be used?: Only scale_x_log 10() makes sense because scale_x_sqrt() makes some dots squeezed together, scale_x_reverse() makes it hard to read from right to left, and we should keep y-axis normal as it is the life expectancy, best to keep it in the original format, which is number of years.
gapminder
## # A tibble: 1,704 × 6
## country continent year lifeExp pop gdpPercap
## <fct> <fct> <int> <dbl> <int> <dbl>
## 1 Afghanistan Asia 1952 28.8 8425333 779.
## 2 Afghanistan Asia 1957 30.3 9240934 821.
## 3 Afghanistan Asia 1962 32.0 10267083 853.
## 4 Afghanistan Asia 1967 34.0 11537966 836.
## 5 Afghanistan Asia 1972 36.1 13079460 740.
## 6 Afghanistan Asia 1977 38.4 14880372 786.
## 7 Afghanistan Asia 1982 39.9 12881816 978.
## 8 Afghanistan Asia 1987 40.8 13867957 852.
## 9 Afghanistan Asia 1992 41.7 16317921 649.
## 10 Afghanistan Asia 1997 41.8 22227415 635.
## # ℹ 1,694 more rows
p <- ggplot(data = gapminder,
mapping = aes(x = pop,
y = lifeExp,
color = continent,
fill = continent))
p + geom_point() +
geom_smooth(method = "loess") +
scale_x_log10()
## `geom_smooth()` using formula = 'y ~ x'
p <- ggplot(data = gapminder,
mapping = aes(x = pop,
y = lifeExp,
color = year,
fill = continent))
p + geom_point() +
geom_smooth(method = "loess") +
scale_x_log10()
## `geom_smooth()` using formula = 'y ~ x'
## Warning: The following aesthetics were dropped during statistical transformation:
## colour.
## ℹ This can happen when ggplot fails to infer the correct grouping structure in
## the data.
## ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
## variable into a factor?
Changes: 1. Instead of distinct category colors, the points turn into a
smooth blue gradient shade. 2. The legend for color is no longer a set
of labeled colored circles. It becomes a continuous colorbar showing
years running from 1952 up to 2007. Class of object year: continuous
variables
p <- ggplot(data = gapminder,
mapping = aes(x = pop,
y = lifeExp,
color = factor(year),
fill = continent))
p + geom_point() +
geom_smooth(method = "loess") +
scale_x_log10()
## `geom_smooth()` using formula = 'y ~ x'
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small. fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 6.2967
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.0214e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 6.2967
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 0.0031959
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 1
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 6.9423
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.0214e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 1.0214e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning: Failed to fit group 5.
## Caused by error in `predLoess()`:
## ! NA/NaN/Inf in foreign function call (arg 5)
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small. fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 6.345
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.0213e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 6.345
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 0.0031957
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 1
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 6.9905
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.0213e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 1.0213e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning: Failed to fit group 10.
## Caused by error in `predLoess()`:
## ! NA/NaN/Inf in foreign function call (arg 5)
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small. fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 6.3928
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.0153e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 6.3928
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 0.0031864
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 1
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 7.0364
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.0153e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 1.0153e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning: Failed to fit group 15.
## Caused by error in `predLoess()`:
## ! NA/NaN/Inf in foreign function call (arg 5)
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small. fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 6.4327
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.0197e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 6.4327
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 0.0031933
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 1
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 7.0777
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.0197e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 1.0197e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning: Failed to fit group 20.
## Caused by error in `predLoess()`:
## ! NA/NaN/Inf in foreign function call (arg 5)
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small. fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 6.4635
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.0663e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 6.4635
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 0.0032654
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 1
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 7.1231
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.0663e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 1.0663e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning: Failed to fit group 25.
## Caused by error in `predLoess()`:
## ! NA/NaN/Inf in foreign function call (arg 5)
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small. fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 6.4971
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.05e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 6.4971
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 0.0032403
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 1
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 7.1517
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.05e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 1.05e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning: Failed to fit group 30.
## Caused by error in `predLoess()`:
## ! NA/NaN/Inf in foreign function call (arg 5)
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small. fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 6.5032
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.1384e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 6.5032
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 0.003374
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 1
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 7.1848
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.1384e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 1.1384e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning: Failed to fit group 35.
## Caused by error in `predLoess()`:
## ! NA/NaN/Inf in foreign function call (arg 5)
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small. fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 6.5173
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.1912e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 6.5173
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 0.0034514
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 1
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 7.2145
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.1912e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 1.1912e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning: Failed to fit group 40.
## Caused by error in `predLoess()`:
## ! NA/NaN/Inf in foreign function call (arg 5)
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small. fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 6.5327
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.2472e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 6.5327
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 0.0035316
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 1
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 7.2461
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.2472e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 1.2472e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning: Failed to fit group 45.
## Caused by error in `predLoess()`:
## ! NA/NaN/Inf in foreign function call (arg 5)
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small. fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 6.5619
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.2366e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 6.5619
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 0.0035165
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 1
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 7.2722
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.2366e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 1.2366e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning: Failed to fit group 50.
## Caused by error in `predLoess()`:
## ! NA/NaN/Inf in foreign function call (arg 5)
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small. fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 6.5885
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.2219e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 6.5885
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 0.0034956
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 1
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 7.2946
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.2219e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 1.2219e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning: Failed to fit group 55.
## Caused by error in `predLoess()`:
## ! NA/NaN/Inf in foreign function call (arg 5)
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : span too small. fewer data values than degrees of freedom.
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 6.611
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.2107e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : pseudoinverse used at 6.611
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : neighborhood radius 0.0034795
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : reciprocal condition number 1
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : at 7.3138
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : radius 1.2107e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : all data on boundary of neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : There are other near singularities as well. 1.2107e-05
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning in simpleLoess(y, x, w, span, degree = degree, parametric = parametric,
## : zero-width neighborhood. make span bigger
## Warning: Failed to fit group 60.
## Caused by error in `predLoess()`:
## ! NA/NaN/Inf in foreign function call (arg 5)
changes: 1. ggplot2 assigns 12 distinct, qualitative rainbow hues evenly
spaced around the color wheel, giving each specific survey year its own
individual color. 2. The legend displays a discrete key labeled
factor(year) with 12 individual colored dots/swatches next to each
specific year (1952, 1957, …, 2007). 3. Factors inherently define
discrete groups in ggplot2. Converting year to a factor forces
geom_smooth() to split the data and fit separate smoothed curves for
every distinct year, producing a much denser stack of individual trend
lines across time.
library(ggplot2)
library(gapminder)
ggplot(data = gapminder,
mapping = aes(x = gdpPercap, y = lifeExp, group = country)) +
# Draw country paths over time
geom_path(alpha = 0.25, color = "red") +
# Highlight the starting and ending points
geom_point(data = subset(gapminder, year %in% c(1952, 2007)),
aes(color = factor(year)), size = 1.2, alpha = 0.8) +
scale_x_log10(labels = scales::label_currency()) +
facet_wrap(~ continent) +
scale_color_manual(values = c("1952" = "blue", "2007" = "yellow"),
name = "Observed Year") +
labs(
title = "Development Trajectories: 1952 to 2007",
subtitle = "Connected paths show within-country growth across continents",
x = "GDP Per Capita (Log Scale)",
y = "Life Expectancy in Years",
caption = "Source: Gapminder."
) +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom")
ignoring the temporal and country-level structure of the data: Pro: 1.
have a more general and high-level picture of the relationship and easy
to understand and capture the trend Con: 1. We failed to look into the
between and within countries.
gapminder
## # A tibble: 1,704 × 6
## country continent year lifeExp pop gdpPercap
## <fct> <fct> <int> <dbl> <int> <dbl>
## 1 Afghanistan Asia 1952 28.8 8425333 779.
## 2 Afghanistan Asia 1957 30.3 9240934 821.
## 3 Afghanistan Asia 1962 32.0 10267083 853.
## 4 Afghanistan Asia 1967 34.0 11537966 836.
## 5 Afghanistan Asia 1972 36.1 13079460 740.
## 6 Afghanistan Asia 1977 38.4 14880372 786.
## 7 Afghanistan Asia 1982 39.9 12881816 978.
## 8 Afghanistan Asia 1987 40.8 13867957 852.
## 9 Afghanistan Asia 1992 41.7 16317921 649.
## 10 Afghanistan Asia 1997 41.8 22227415 635.
## # ℹ 1,694 more rows
p <- ggplot(data = gapminder,
mapping = aes(x = pop, y = gdpPercap))
p + geom_point(aes(color = continent), alpha = 0.6, size = 1.2) +
geom_smooth(method = "loess", se = FALSE, color = "gray50", linewidth = 0.8) +
scale_x_log10(labels = scales::label_log()) +
scale_y_log10(labels = scales::label_currency()) +
facet_wrap(~ year) +
labs(
x = "Population",
y = "Per Capita GDP",
title = "Population vs. Per Capita GDP Across Survey Years",
subtitle = "Faceted by year; points colored by continent",
) +
theme_minimal() +
theme(
legend.position = "bottom",
axis.text.x = element_text(angle = 45, hjust = 1)
)
## `geom_smooth()` using formula = 'y ~ x'
p <- ggplot(data = gapminder,
mapping = aes(x = pop, y = gdpPercap)) +
geom_point(aes(color = continent), alpha = 0.7, size = 0.8) +
geom_path(color = "gray50", linewidth = 0.3) +
scale_x_log10(labels = scales::label_log()) +
scale_y_log10(labels = scales::label_currency()) +
facet_wrap(~ country, scales = "free")
labs(
x = "Population",
y = "Per Capita GDP",
title = "Population vs.Per Capita GDP Trajectories by Country (1952–2007)",
) +
theme_minimal(base_size = 7) +
theme(
strip.text = element_text(size = 5),
axis.text = element_text(size = 4),
legend.position = "bottom"
)
## NULL
ggsave(
filename = "/Users/xutianyu/Downloads/gapminder_country_facets.pdf",
plot = p,
width = 22,
height = 20,
units = "in",
limitsize = FALSE
)
# install.packages("socviz")
library(socviz)
glimpse(gss_sm)
## Rows: 2,867
## Columns: 32
## $ year <dbl> 2016, 2016, 2016, 2016, 2016, 2016, 2016, 2016, 2016, 2016…
## $ id <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,…
## $ ballot <labelled> 1, 2, 3, 1, 3, 2, 1, 3, 1, 3, 2, 1, 2, 3, 2, 3, 3, 2,…
## $ age <dbl> 47, 61, 72, 43, 55, 53, 50, 23, 45, 71, 33, 86, 32, 60, 76…
## $ childs <dbl> 3, 0, 2, 4, 2, 2, 2, 3, 3, 4, 5, 4, 3, 5, 7, 2, 6, 5, 0, 2…
## $ sibs <labelled> 2, 3, 3, 3, 2, 2, 2, 6, 5, 1, 4, 4, 3, 6, 0, 1, 3, 8,…
## $ degree <fct> Bachelor, High School, Bachelor, High School, Graduate, Ju…
## $ race <fct> White, White, White, White, White, White, White, Other, Bl…
## $ sex <fct> Male, Male, Male, Female, Female, Female, Male, Female, Ma…
## $ region <fct> New England, New England, New England, New England, New En…
## $ income16 <fct> $170000 or over, $50000 to 59999, $75000 to $89999, $17000…
## $ relig <fct> None, None, Catholic, Catholic, None, None, None, Catholic…
## $ marital <fct> Married, Never Married, Married, Married, Married, Married…
## $ padeg <fct> Graduate, Lt High School, High School, NA, Bachelor, NA, H…
## $ madeg <fct> High School, High School, Lt High School, High School, Hig…
## $ partyid <fct> "Independent", "Ind,near Dem", "Not Str Republican", "Not …
## $ polviews <fct> Moderate, Liberal, Conservative, Moderate, Slightly Libera…
## $ happy <fct> Pretty Happy, Pretty Happy, Very Happy, Pretty Happy, Very…
## $ partners <fct> NA, "1 Partner", "1 Partner", NA, "1 Partner", "1 Partner"…
## $ grass <fct> NA, Legal, Not Legal, NA, Legal, Legal, NA, Not Legal, NA,…
## $ zodiac <fct> Aquarius, Scorpio, Pisces, Cancer, Scorpio, Scorpio, Capri…
## $ pres12 <labelled> 3, 1, 2, 2, 1, 1, NA, NA, NA, 2, NA, NA, 1, 1, 2, 1, …
## $ wtssall <dbl> 0.9569935, 0.4784968, 0.9569935, 1.9139870, 1.4354903, 0.9…
## $ income_rc <fct> Gt $170000, Gt $50000, Gt $75000, Gt $170000, Gt $170000, …
## $ agegrp <fct> Age 45-55, Age 55-65, Age 65+, Age 35-45, Age 45-55, Age 4…
## $ ageq <fct> Age 34-49, Age 49-62, Age 62+, Age 34-49, Age 49-62, Age 4…
## $ siblings <fct> 2, 3, 3, 3, 2, 2, 2, 6+, 5, 1, 4, 4, 3, 6+, 0, 1, 3, 6+, 2…
## $ kids <fct> 3, 0, 2, 4+, 2, 2, 2, 3, 3, 4+, 4+, 4+, 3, 4+, 4+, 2, 4+, …
## $ religion <fct> None, None, Catholic, Catholic, None, None, None, Catholic…
## $ bigregion <fct> Northeast, Northeast, Northeast, Northeast, Northeast, Nor…
## $ partners_rc <fct> NA, 1, 1, NA, 1, 1, NA, 1, NA, 3, 1, NA, 1, NA, 0, 1, 0, N…
## $ obama <dbl> 0, 1, 0, 0, 1, 1, NA, NA, NA, 0, NA, NA, 1, 1, 0, 1, 0, 1,…
p <- ggplot(data = gss_sm,
mapping = aes(x = age, y = childs))
p + geom_point(alpha = 0.2) +
geom_smooth(se = FALSE) +
facet_grid(sex ~ race)
## `geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'
## Warning: Removed 18 rows containing non-finite outside the scale range
## (`stat_smooth()`).
## Warning: Removed 18 rows containing missing values or values outside the scale range
## (`geom_point()`).
p <- ggplot(data = gss_sm,
mapping = aes(x = age, y = childs))
p + geom_point(alpha = 0.2) +
geom_smooth(se = FALSE) +
facet_grid(~ sex + race)
## `geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'
## Warning: Removed 18 rows containing non-finite outside the scale range
## (`stat_smooth()`).
## Warning: Removed 18 rows containing missing values or values outside the scale range
## (`geom_point()`).
facet_grid(sex ~ race): two rows for the binary sex variable and three columns for 3 racial categories, 23 panel facet_grid(~ sex + race): 1 row, 6 columns (sex race), narrow plots
p <- ggplot(data = gss_sm,
mapping = aes(x = age, y = childs))
p + geom_point(alpha = 0.2) +
geom_smooth(se = FALSE) +
facet_wrap(~ sex + race)
## `geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'
## Warning: Removed 18 rows containing non-finite outside the scale range
## (`stat_smooth()`).
## Warning: Removed 18 rows containing missing values or values outside the scale range
## (`geom_point()`).
facet_grid(~ sex + race): 1 row, 6 columns (sex * race), narrow plots ??????????facet_wrap(~ sex + race): 2 rows, 3 columns
glimpse(midwest)
## Rows: 437
## Columns: 28
## $ PID <int> 561, 562, 563, 564, 565, 566, 567, 568, 569, 570,…
## $ county <chr> "ADAMS", "ALEXANDER", "BOND", "BOONE", "BROWN", "…
## $ state <chr> "IL", "IL", "IL", "IL", "IL", "IL", "IL", "IL", "…
## $ area <dbl> 0.052, 0.014, 0.022, 0.017, 0.018, 0.050, 0.017, …
## $ poptotal <int> 66090, 10626, 14991, 30806, 5836, 35688, 5322, 16…
## $ popdensity <dbl> 1270.9615, 759.0000, 681.4091, 1812.1176, 324.222…
## $ popwhite <int> 63917, 7054, 14477, 29344, 5264, 35157, 5298, 165…
## $ popblack <int> 1702, 3496, 429, 127, 547, 50, 1, 111, 16, 16559,…
## $ popamerindian <int> 98, 19, 35, 46, 14, 65, 8, 30, 8, 331, 51, 26, 17…
## $ popasian <int> 249, 48, 16, 150, 5, 195, 15, 61, 23, 8033, 89, 3…
## $ popother <int> 124, 9, 34, 1139, 6, 221, 0, 84, 6, 1596, 20, 7, …
## $ percwhite <dbl> 96.71206, 66.38434, 96.57128, 95.25417, 90.19877,…
## $ percblack <dbl> 2.57527614, 32.90043290, 2.86171703, 0.41225735, …
## $ percamerindan <dbl> 0.14828264, 0.17880670, 0.23347342, 0.14932156, 0…
## $ percasian <dbl> 0.37675897, 0.45172219, 0.10673071, 0.48691813, 0…
## $ percother <dbl> 0.18762294, 0.08469791, 0.22680275, 3.69733169, 0…
## $ popadults <int> 43298, 6724, 9669, 19272, 3979, 23444, 3583, 1132…
## $ perchsd <dbl> 75.10740, 59.72635, 69.33499, 75.47219, 68.86152,…
## $ percollege <dbl> 19.63139, 11.24331, 17.03382, 17.27895, 14.47600,…
## $ percprof <dbl> 4.355859, 2.870315, 4.488572, 4.197800, 3.367680,…
## $ poppovertyknown <int> 63628, 10529, 14235, 30337, 4815, 35107, 5241, 16…
## $ percpovertyknown <dbl> 96.27478, 99.08714, 94.95697, 98.47757, 82.50514,…
## $ percbelowpoverty <dbl> 13.151443, 32.244278, 12.068844, 7.209019, 13.520…
## $ percchildbelowpovert <dbl> 18.011717, 45.826514, 14.036061, 11.179536, 13.02…
## $ percadultpoverty <dbl> 11.009776, 27.385647, 10.852090, 5.536013, 11.143…
## $ percelderlypoverty <dbl> 12.443812, 25.228976, 12.697410, 6.217047, 19.200…
## $ inmetro <int> 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0…
## $ category <chr> "AAR", "LHR", "AAR", "ALU", "AAR", "AAR", "LAR", …
p<- ggplot(data = midwest,
mapping = aes(x = percollege, y = percbelowpoverty)) +
geom_point(alpha = 0.3, size = 1.2, color = "gray30") +
geom_density_2d(color = "#0072B2", linewidth = 0.8) +
labs(
x = "Percent College Educated",
y = "Percent Below Poverty Line",
title = "Midwest Counties: College Education vs. Poverty",
) +
theme_minimal()
p
p <- ggplot(data = midwest,
mapping = aes(x = percollege, y = percbelowpoverty)) +
geom_density_2d(aes(color = after_stat(level)), linewidth = 0.9) +
scale_color_viridis_c() +
labs(
x = "Percent College Educated",
y = "Percent Below Poverty Line",
color = "Density Level",
title = "2D Kernel Density Contours: Midwest Education vs. Poverty",
) +
theme_minimal()
p
with geom_point() layer: include dots show the actual raw observations
and concentric contour lines without geom_point() layer: only concentric
contour lines
library (dplyr)
glimpse(elections_historic)
## Rows: 51
## Columns: 19
## $ election <chr> "10", "11", "12", "13", "14", "15", "16", "17", "18", "…
## $ year <int> 1824, 1828, 1832, 1836, 1840, 1844, 1848, 1852, 1856, 1…
## $ winner <chr> "John Quincy Adams", "Andrew Jackson", "Andrew Jackson"…
## $ win_party <chr> "D.-R.", "Dem.", "Dem.", "Dem.", "Whig", "Dem.", "Whig"…
## $ ec_votes <int> 84, 178, 219, 170, 234, 170, 163, 254, 174, 180, 212, 2…
## $ ec_denom <int> 261, 261, 286, 294, 294, 275, 290, 296, 296, 303, 233, …
## $ ec_pct <dbl> 0.3218, 0.6820, 0.7657, 0.5782, 0.7959, 0.6182, 0.5621,…
## $ popular_pct <dbl> 0.3092, 0.5593, 0.5474, 0.5079, 0.5287, 0.4954, 0.4728,…
## $ popular_margin <dbl> -0.1044, 0.1225, 0.1781, 0.1420, 0.0605, 0.0145, 0.0479…
## $ votes <int> 113142, 642806, 702735, 763291, 1275583, 1339570, 13602…
## $ margin <int> -38221, 140839, 228628, 213384, 145938, 39413, 137882, …
## $ runner_up <chr> "Andrew Jackson", "John Quincy Adams", "Henry Clay", "W…
## $ ru_part <chr> "D-R", "NR", "NR", "Whig", "Dem.", "Whig", "Dem.", "Whi…
## $ turnout_pct <dbl> 0.269, 0.573, 0.570, 0.565, 0.803, 0.792, 0.728, 0.695,…
## $ winner_lname <chr> "Adams", "Jackson", "Jackson", "Buren", "Harrison", "Po…
## $ winner_label <chr> "Adams 1824", "Jackson 1828", "Jackson 1832", "Buren 18…
## $ ru_lname <chr> "Jackson", "Adams", "Clay", "Harrison", "Buren", "Clay"…
## $ ru_label <chr> "Jackson 1824", "Adams 1828", "Clay 1832", "Harrison 18…
## $ two_term <lgl> FALSE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, F…
# install.packages("ggrepel")
library(ggrepel)
p_title <- "Popular Vote and Electoral College Shares"
p_subtitle <- "1824-2024"
x_label <- "Winner's share of Popular Vote"
y_label <- "Winner's share of Electoral College Votes"
p <- ggplot(elections_historic, aes(x = popular_pct, y = ec_pct))
p + geom_hline(yintercept = 0.5, linewidth = 1.4, color = "gray80") +
geom_vline(xintercept = 0.5, linewidth = 1.4, color = "gray80") +
geom_point() +
geom_text_repel(
data = subset(elections_historic, year >= 1992),
aes(label = winner_label),
box.padding = 0.4,
point.padding = 0.3
) +
scale_x_continuous(labels = scales::label_percent()) +
scale_y_continuous(labels = scales::label_percent()) +
labs(x = x_label, y = y_label, title = p_title, subtitle = p_subtitle) +
theme_minimal()
glimpse(elections_historic)
## Rows: 51
## Columns: 19
## $ election <chr> "10", "11", "12", "13", "14", "15", "16", "17", "18", "…
## $ year <int> 1824, 1828, 1832, 1836, 1840, 1844, 1848, 1852, 1856, 1…
## $ winner <chr> "John Quincy Adams", "Andrew Jackson", "Andrew Jackson"…
## $ win_party <chr> "D.-R.", "Dem.", "Dem.", "Dem.", "Whig", "Dem.", "Whig"…
## $ ec_votes <int> 84, 178, 219, 170, 234, 170, 163, 254, 174, 180, 212, 2…
## $ ec_denom <int> 261, 261, 286, 294, 294, 275, 290, 296, 296, 303, 233, …
## $ ec_pct <dbl> 0.3218, 0.6820, 0.7657, 0.5782, 0.7959, 0.6182, 0.5621,…
## $ popular_pct <dbl> 0.3092, 0.5593, 0.5474, 0.5079, 0.5287, 0.4954, 0.4728,…
## $ popular_margin <dbl> -0.1044, 0.1225, 0.1781, 0.1420, 0.0605, 0.0145, 0.0479…
## $ votes <int> 113142, 642806, 702735, 763291, 1275583, 1339570, 13602…
## $ margin <int> -38221, 140839, 228628, 213384, 145938, 39413, 137882, …
## $ runner_up <chr> "Andrew Jackson", "John Quincy Adams", "Henry Clay", "W…
## $ ru_part <chr> "D-R", "NR", "NR", "Whig", "Dem.", "Whig", "Dem.", "Whi…
## $ turnout_pct <dbl> 0.269, 0.573, 0.570, 0.565, 0.803, 0.792, 0.728, 0.695,…
## $ winner_lname <chr> "Adams", "Jackson", "Jackson", "Buren", "Harrison", "Po…
## $ winner_label <chr> "Adams 1824", "Jackson 1828", "Jackson 1832", "Buren 18…
## $ ru_lname <chr> "Jackson", "Adams", "Clay", "Harrison", "Buren", "Clay"…
## $ ru_label <chr> "Jackson 1824", "Adams 1828", "Clay 1832", "Harrison 18…
## $ two_term <lgl> FALSE, TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, F…
p_title <- "Popular Vote and Electoral College Shares"
p_subtitle <- "1824–2024 (Selected Party Highlights)"
x_label <- "Winner's Share of Popular Vote"
y_label <- "Winner's Share of Electoral College Votes"
p <- ggplot(elections_historic, aes(x = popular_pct, y = ec_pct)) +
geom_hline(yintercept = 0.5, linewidth = 1.2, color = "gray80") +
geom_vline(xintercept = 0.5, linewidth = 1.2, color = "gray80") +
geom_point(color = "gray60", alpha = 0.6) +
geom_point(data = subset(elections_historic, win_party == "Rep."),
color = "#0072B2", size = 2.5) +
geom_text_repel(
data = subset(elections_historic, win_party == "Rep."),
aes(label = winner_label),
color = "#0072B2",
size = 3,
max.overlaps = 15,
box.padding = 0.35
) +
scale_x_continuous(labels = scales::label_percent()) +
scale_y_continuous(labels = scales::label_percent()) +
labs(
x = x_label, y = y_label,
title = p_title,
) +
theme_minimal()
p
p_title <- "Popular Vote and Electoral College Shares"
p_subtitle <- "1824–2024 (Colored by Winning Party)"
x_label <- "Winner's Share of Popular Vote"
y_label <- "Winner's Share of Electoral College Votes"
p <- ggplot(elections_historic, aes(x = popular_pct, y = ec_pct, color = win_party))
p + geom_hline(yintercept = 0.5, linewidth = 1.2, color = "gray80") +
geom_vline(xintercept = 0.5, linewidth = 1.2, color = "gray80") +
geom_point(size = 2.2, alpha = 0.75) +
geom_text_repel(
data = subset(elections_historic, year >= 1992),
aes(label = winner_label),
size = 3.2,
fontface = "bold",
box.padding = 0.4,
point.padding = 0.3,
show.legend = FALSE
) +
scale_color_manual(
values = c(
"Dem." = "#2E5B88",
"Rep." = "#D95F02",
"Whig" = "#7570B3",
"Dem.-Rep." = "#1B9E77",
"Other" = "gray50"
),
name = "Winning Party"
) +
scale_x_continuous(labels = scales::label_percent()) +
scale_y_continuous(labels = scales::label_percent()) +
labs(
x = x_label,
y = y_label,
title = p_title,
subtitle = p_subtitle,
) +
theme_minimal() +
theme(
legend.position = "bottom",
plot.title = element_text(face = "bold")
)
p_title <- "Popular Vote and Electoral College Shares"
p_subtitle <- "1824-2024"
x_label <- "Winner's share of Popular Vote"
y_label <- "Winner's share of Electoral College Votes"
p <- ggplot(elections_historic, aes(x = popular_pct, y = ec_pct,
label = winner_label))
p +
annotate(
geom = "rect",
xmin = -Inf, xmax = 0.5,
ymin = 0.5, ymax = Inf,
fill = "red",
alpha = 0.12
) +
geom_hline(yintercept = 0.5, linewidth = 1.4, color = "gray80") +
geom_vline(xintercept = 0.5, linewidth = 1.4, color = "gray80") +
geom_point() +
geom_text_repel() +
scale_x_continuous(labels = scales::label_percent()) +
scale_y_continuous(labels = scales::label_percent()) +
labs(x = x_label, y = y_label, title = p_title, subtitle = p_subtitle) +
theme_minimal()
gapminder_2007 <- gapminder %>%
filter(year == 2007) %>%
group_by(continent) %>%
summarize(lifeExp = mean(lifeExp)) %>%
ungroup()
p_bar <- ggplot(gapminder_2007, aes(x = reorder(continent, lifeExp), y = lifeExp)) +
geom_col(fill = "#525252", width = 0.85) +
labs(
x = NULL,
y = "Life expectancy in years, 2007"
) +
theme_classic() +
theme(
panel.grid.major.x = element_line(color = "gray95", linewidth = 0.4),
axis.line = element_line(color = "black", linewidth = 0.6),
axis.ticks = element_line(color = "black")
)
p_dot <- ggplot(gapminder_2007, aes(x = lifeExp, y = reorder(continent, lifeExp))) +
geom_point(size = 4, color = "black") +
labs(
x = "Life expectancy in years, 2007",
y = NULL
) +
theme_classic() +
theme(
panel.grid.major.x = element_line(color = "gray95", linewidth = 0.4),
axis.line = element_line(color = "black", linewidth = 0.6),
axis.ticks = element_line(color = "black")
)
print(p_bar)
print(p_dot)
##This code is similar to what you saw earlier but more compact. (We calculate the pct values directly.) Check that the results are as you expect by grouping by race and summing the percentages. Try doing the same exercise grouping by sex or region.
race <- gss_sm |>
filter(!is.na(degree)) |>
group_by(race, degree) |>
summarize(n = n(), .groups = "drop_last") |>
mutate(pct = round(n / sum(n) * 100, 0))
print(race)
## # A tibble: 15 × 4
## # Groups: race [3]
## race degree n pct
## <fct> <fct> <int> <dbl>
## 1 White Lt High School 197 9
## 2 White High School 1057 50
## 3 White Junior College 166 8
## 4 White Bachelor 426 20
## 5 White Graduate 250 12
## 6 Black Lt High School 60 12
## 7 Black High School 292 60
## 8 Black Junior College 33 7
## 9 Black Bachelor 71 15
## 10 Black Graduate 31 6
## 11 Other Lt High School 71 26
## 12 Other High School 112 41
## 13 Other Junior College 17 6
## 14 Other Bachelor 39 14
## 15 Other Graduate 37 13
race_check <- race |>
summarize(total_n = sum(n), total_pct = sum(pct))
print(race_check)
## # A tibble: 3 × 3
## race total_n total_pct
## <fct> <int> <dbl>
## 1 White 2096 99
## 2 Black 487 100
## 3 Other 276 100
sex <- gss_sm |>
filter(!is.na(degree)) |>
group_by(sex, degree) |>
summarize(n = n(), .groups = "drop_last") |>
mutate(pct = round(n / sum(n) * 100, 1))
print(sex)
## # A tibble: 10 × 4
## # Groups: sex [2]
## sex degree n pct
## <fct> <fct> <int> <dbl>
## 1 Male Lt High School 147 11.5
## 2 Male High School 662 52
## 3 Male Junior College 89 7
## 4 Male Bachelor 243 19.1
## 5 Male Graduate 132 10.4
## 6 Female Lt High School 181 11.4
## 7 Female High School 799 50.4
## 8 Female Junior College 127 8
## 9 Female Bachelor 293 18.5
## 10 Female Graduate 186 11.7
sex_check <- sex |>
summarize(total_n = sum(n), total_pct = sum(pct))
print(sex_check)
## # A tibble: 2 × 3
## sex total_n total_pct
## <fct> <int> <dbl>
## 1 Male 1273 100
## 2 Female 1586 100
region <- gss_sm |>
filter(!is.na(degree), !is.na(bigregion)) |>
group_by(bigregion, degree) |>
summarize(n = n(), .groups = "drop_last") |>
mutate(pct = round(n / sum(n) * 100, 1))
print(region)
## # A tibble: 20 × 4
## # Groups: bigregion [4]
## bigregion degree n pct
## <fct> <fct> <int> <dbl>
## 1 Northeast Lt High School 38 7.8
## 2 Northeast High School 237 48.7
## 3 Northeast Junior College 40 8.2
## 4 Northeast Bachelor 98 20.1
## 5 Northeast Graduate 74 15.2
## 6 Midwest Lt High School 67 9.7
## 7 Midwest High School 378 54.6
## 8 Midwest Junior College 44 6.4
## 9 Midwest Bachelor 137 19.8
## 10 Midwest Graduate 66 9.5
## 11 South Lt High School 138 13.1
## 12 South High School 547 52.1
## 13 South Junior College 86 8.2
## 14 South Bachelor 178 17
## 15 South Graduate 101 9.6
## 16 West Lt High School 85 13.5
## 17 West High School 299 47.5
## 18 West Junior College 46 7.3
## 19 West Bachelor 123 19.5
## 20 West Graduate 77 12.2
# Verification check: each region sums to 100%
region_check <- region |>
summarize(total_n = sum(n), total_pct = sum(pct))
print(region_check)
## # A tibble: 4 × 3
## bigregion total_n total_pct
## <fct> <int> <dbl>
## 1 Northeast 487 100
## 2 Midwest 692 100
## 3 South 1050 100
## 4 West 630 100
children_by_degree <- gss_sm |>
filter(!is.na(degree), !is.na(childs)) |>
group_by(degree) |>
summarize(
mean_childs = round(mean(childs), 2),
median_childs = median(childs),
sd_childs = round(sd(childs), 2),
n = n(),
.groups = "drop"
)
print(children_by_degree)
## # A tibble: 5 × 5
## degree mean_childs median_childs sd_childs n
## <fct> <dbl> <dbl> <dbl> <int>
## 1 Lt High School 2.81 3 1.98 326
## 2 High School 1.86 2 1.65 1459
## 3 Junior College 1.77 2 1.63 215
## 4 Bachelor 1.45 1 1.44 536
## 5 Graduate 1.52 2 1.38 318
p <- ggplot(data = gapminder,
mapping = aes(x = year, y = pop, group = year)) +
geom_boxplot(aes(fill = continent), alpha = 0.7, outlier.size = 0.8) +
scale_y_log10(labels = scales::label_log()) +
facet_wrap(~ continent, ncol = 5) +
scale_x_continuous(breaks = c(1960, 1980, 2000)) +
guides(fill = "none") +
labs(
x = "Year",
y = "Population",
title = "Population Distribution Over Time by Continent",
) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
print(p)
### population over life expectancy by continent
p <- ggplot(data = gapminder,
mapping = aes(x = year, y = lifeExp, group = year)) +
geom_boxplot(aes(fill = continent), alpha = 0.7, outlier.size = 0.8) +
facet_wrap(~ continent, ncol = 5) +
scale_x_continuous(breaks = c(1960, 1980, 2000)) +
guides(fill = "none") +
labs(
x = "Year",
y = "Life Expectancy (Years)",
title = "Life Expectancy Distribution Over Time by Continent",
) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
print(p)
### Is anything different if you create a tibble from gapminder that
explicitly groups the data by year and continent first, and then create
your plots with that?
library(dplyr)
gapminder <- gapminder |>
group_by(continent, year)
p <- ggplot(data = gapminder,
mapping = aes(x = year, y = lifeExp, group = year)) +
geom_boxplot(aes(fill = continent), alpha = 0.7, outlier.size = 0.8) +
facet_wrap(~ continent, ncol = 5) +
scale_x_continuous(breaks = c(1960, 1980, 2000)) +
guides(fill = "none") +
labs(
x = "Year",
y = "Life Expectancy (Years)",
title = "Life Expectancy Distribution Over Time by Continent",
) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
print(p)
There is nothing different because ggplot2 and dplyr are two separate
packages.
p <- ggplot(
data = organdata,
mapping = aes(x = donors, fill = world,
y = reorder(country, donors, na.rm = TRUE))
)
p_notch <- p +
geom_boxplot(notch = TRUE) +
labs(
y = NULL,
fill = "World",
title = "Organ Donation Rates: Notched Boxplot",
subtitle = "Notches indicate ~95% confidence interval around each country's median"
) +
theme(legend.position = "top")
print(p_notch)
## Warning: Removed 34 rows containing non-finite outside the scale range
## (`stat_boxplot()`).
## Notch went outside hinges
## ℹ Do you want `notch = FALSE`?
## Notch went outside hinges
## ℹ Do you want `notch = FALSE`?
## Notch went outside hinges
## ℹ Do you want `notch = FALSE`?
## Notch went outside hinges
## ℹ Do you want `notch = FALSE`?
## Notch went outside hinges
## ℹ Do you want `notch = FALSE`?
## Notch went outside hinges
## ℹ Do you want `notch = FALSE`?
## Notch went outside hinges
## ℹ Do you want `notch = FALSE`?
## Notch went outside hinges
## ℹ Do you want `notch = FALSE`?
## Notch went outside hinges
## ℹ Do you want `notch = FALSE`?
## Notch went outside hinges
## ℹ Do you want `notch = FALSE`?
## Notch went outside hinges
## ℹ Do you want `notch = FALSE`?
## Notch went outside hinges
## ℹ Do you want `notch = FALSE`?
p_varwidth <- p +
geom_boxplot(varwidth = TRUE) +
labs(
y = NULL,
fill = "World",
title = "Organ Donation Rates: Variable Width Boxplot",
subtitle = "Box thickness reflects sample size (number of annual observations)"
) +
theme(legend.position = "top")
p_varwidth
## Warning: Removed 34 rows containing non-finite outside the scale range
## (`stat_boxplot()`).
changes: notch: Pulls the waist of each box inward around the median,
creating an hourglass “notch.”. The notch displays an approximate 95%
confidence interval around the median. varwidth: Scales the physical
width of the box along the x-axis proportional to the square root of the
number of observations each group
p <- ggplot(data = organdata,
mapping = aes(x = donors,
y = reorder(country, donors, na.rm = TRUE)))
p + geom_violin(fill = "#0072B2", color = "blue") +
labs(
x = "Donor Rate",
y = NULL,
title = "Organ Donation Rates: Mirrored Density (Violin Plot)"
) +
theme_minimal()
## Warning: Removed 34 rows containing non-finite outside the scale range
## (`stat_ydensity()`).
# Install and load the packages one by one.
# install.packages("ggplot2")
#install.packages("GGally")
#install.packages("ggExtra")
#install.packages("ggalluvial")
#install.packages("plotly")
# library("ggplot2")
library("GGally")
library("ggExtra")
library("ggalluvial")
library("plotly")
##
## Attaching package: 'plotly'
## The following object is masked from 'package:ggplot2':
##
## last_plot
## The following object is masked from 'package:stats':
##
## filter
## The following object is masked from 'package:graphics':
##
## layout
# we will also use cowplot later in this session.
# Please install it but do not load it for now.
# install.packages("cowplot")
# install.packages("data.table")
library(data.table)
##
## Attaching package: 'data.table'
## The following objects are masked from 'package:dplyr':
##
## between, first, last
pisa <- fread("/Users/xutianyu/Downloads/pisa2015-1.csv", na.strings = "")
country <- c("United States", "Canada", "Mexico", "B-S-J-G (China)", "Japan",
"Korea", "Germany", "Italy", "France", "Brazil", "Colombia", "Uruguay",
"Australia", "New Zealand", "Jordan", "Israel", "Lebanon")
bin.to.num <- function(x){
if (is.na(x)) NA
else if (x == "Yes") 1L
else if (x == "No") 0L
}
pisa[, `:=`
(female = ifelse(ST004D01T == "Female", 1, 0),
sex = ST004D01T,
# At my house we have ...
desk = sapply(ST011Q01TA, bin.to.num),
own.room = sapply(ST011Q02TA, bin.to.num),
quiet.study = sapply(ST011Q03TA, bin.to.num),
computer = sapply(ST011Q04TA, bin.to.num),
software = sapply(ST011Q05TA, bin.to.num),
internet = sapply(ST011Q06TA, bin.to.num),
lit = sapply(ST011Q07TA, bin.to.num),
poetry = sapply(ST011Q08TA, bin.to.num),
art = sapply(ST011Q09TA, bin.to.num),
book.sch = sapply(ST011Q10TA, bin.to.num),
tech.book = sapply(ST011Q11TA, bin.to.num),
dict = sapply(ST011Q12TA, bin.to.num),
art.book = sapply(ST011Q16NA, bin.to.num))]
pisa[, `:=`
(math = rowMeans(pisa[, c(paste0("PV", 1:10, "MATH"))], na.rm = TRUE),
reading = rowMeans(pisa[, c(paste0("PV", 1:10, "READ"))], na.rm = TRUE),
science = rowMeans(pisa[, c(paste0("PV", 1:10, "SCIE"))], na.rm = TRUE))]
dat <- pisa[CNT %in% country,
.(CNT, OECD, CNTSTUID, W_FSTUWT, sex,female,
ST001D01T, computer, software, internet,
ST011Q05TA, ST071Q02NA, ST071Q01NA, ST123Q02NA,
ST082Q01NA, ST119Q01NA, ST119Q05NA, ANXTEST,
COOPERATE, BELONG, EMOSUPS, HOMESCH, ENTUSE,
ICTHOME, ICTSCH, WEALTH, PARED, TMINS, ESCS,
TEACHSUP, TDTEACH, IBTEACH, SCIEEFF,
math, reading, science)
]
# Let's create additional variables that we will use for visualizations
dat <- dat[, `:=` (
# New grade variable
grade = (as.numeric(sapply(ST001D01T, function(x) {
if(x=="Grade 7") "7"
else if (x=="Grade 8") "8"
else if (x=="Grade 9") "9"
else if (x=="Grade 10") "10"
else if (x=="Grade 11") "11"
else if (x=="Grade 12") "12"
else if (x=="Grade 13") NA_character_
else if (x=="Ungraded") NA_character_}))),
# Total learning time as hours
learning = round(TMINS/60, 0),
# Regions for selected countries
Region = (sapply(CNT, function(x) {
if(x %in% c("Canada", "United States", "Mexico")) "N. America"
else if (x %in% c("Colombia", "Brazil", "Uruguay")) "S. America"
else if (x %in% c("Japan", "B-S-J-G (China)", "Korea")) "Asia"
else if (x %in% c("Germany", "Italy", "France")) "Europe"
else if (x %in% c("Australia", "New Zealand")) "Australia"
else if (x %in% c("Israel", "Jordan", "Lebanon")) "Middle-East"
}))
)]
ggplot(data = dat,
mapping = aes(x = reorder(CNT, math), y = math, fill = Region)) +
geom_boxplot() +
labs(x=NULL, y="Math Scores") +
coord_flip() +
geom_hline(yintercept = 490, linetype="dashed", color = "red", size = 1) +
theme_bw()
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once per session.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
ggplot(
data = dat,
mapping = aes(x = reorder(CNT, math), y = math)
) +
geom_violin(aes(color = Region, fill = Region)) +
labs(x = NULL, y = "Math Scores", color = "Region") +
coord_flip() +
geom_hline(yintercept = 490, linetype = "dashed", color = "red", linewidth = 1) +
theme_bw()
boxplot is more appropriate here for PISA data as in boxplots
immediately show the exact numbers people care about in testing: the
median (middle score), the middle 50% of students (the box), and the
highest/lowest performers. and it is easy to compare using boxplot.
ggplot(data = dat,
mapping = aes(x = science, fill = Region)) +
geom_histogram(alpha = 0.5, bins = 50) +
labs(x = "Science Scores", y = "Count",
title = "Science Scores by Gender and Region") +
facet_grid(. ~ sex) +
theme_bw()
library(GGally)
# Random sample of 500 students from each region
dat_small <- dat[,.SD[sample(.N, min(500,.N))], by = Region]
ggpairs(data = dat_small,
mapping = aes(color = Region),
columns = c("reading", "science", "math", "sex"),
upper = list(continuous = wrap("cor", size = 2.5))
)
## `stat_bin()` using `bins = 30`. Pick better value `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value `binwidth`.
Asia, Australia, and Europe consistently sit at the higher end of the score distributions across all three subjects, with their density peaks shifted furthest to the right (around 500–550+). Middle-East occupies the middle-to-lower tier, centered around the mid-400s. South America consistently displays the lowest performance across reading, science, and math, with its distribution centered below 400 and noticeably shifted to the left. The strong relationship among reading, science, and math scores holds across every single geographic region; all regional correlation coefficients exceed 0.85 (and many surpass 0.92), indicating that academic achievement across domains is uniformly coupled regardless of region.
In reading, females outperform males across nearly every region. The female boxplot medians and interquartile spans for reading sit visibly higher than those of males within each respective regional color. In math, Males hold a slight advantage over females, with male median lines and upper whiskers positioned slightly higher in most regions, though the difference is smaller than the reading gap. In science, gender differences are minimal. The medians and overall box spreads between males and females within each region are nearly identical, showing parity across sexes.
Extremely Strong Positive Linear Correlation among reading, science, and math. Science and math have the highest overall correlation (r = 0.933). There is a strong correlation between science and reading: (r = 0.926). The relationship between math and reading also show a very strong correlation (r = 0.879).
ggplot(
data = dat_small,
mapping = aes(x = ESCS, y = math, color = sex)
) +
geom_point(alpha = 0.4, size = 1.2) +
geom_smooth(method = "lm", se = TRUE) +
facet_wrap(~ Region) +
labs(
x = "Socio-Economic Status (ESCS)",
y = "Math Scores",
color = "Gender"
) +
theme_bw()
## `geom_smooth()` using formula = 'y ~ x'
## Warning: Removed 80 rows containing non-finite outside the scale range
## (`stat_smooth()`).
## Warning: Removed 80 rows containing missing values or values outside the scale range
## (`geom_point()`).
the relationship between ESCS and math seems identical across gender because the regression lines for most of the scatterplots are identical. The relationship between ESCS and math seems consistent across ESCS because across all six regional panels, the slopes are consistently positive: higher ESCS is uniformly associated with higher math achievement.
dat_alluvial2 <- dat[,
.(Freq = .N),
by = c("Region", "sex", "ST119Q01NA")
][,
ST119Q01NA := as.factor(ifelse(ST119Q01NA == "", "Missing", ST119Q01NA))]
levels(dat_alluvial2$ST119Q01NA) <- c("Strongly disagree", "Disagree", "Agree",
"Strongly agree", "Missing")
library(ggalluvial)
ggplot(data = dat_alluvial2,
aes(axis1 = Region, axis2 = ST119Q01NA, y = Freq)) +
scale_x_discrete(limits = c("Region", "Student Want Top Grades In Most Or All Courses"),
expand = c(.1, .05)) +
geom_alluvium(aes(fill = sex), alpha = 0.6, width = 1/4) +
geom_stratum() +
geom_text(
stat = "stratum",
aes(label = after_stat(stratum)),
size = 2
) +
labs(x = "Demographics", y = "Frequency", fill = "Gender") +
theme_bw()
Asia displays the strongest orientation toward top grades, with nearly all student flow terminating in the top category labeled “Strongly disagree”. Very few students from this region funnel down into the middle or lower blocks. The Middle East follows a similar high-aspiration pattern. The vast majority of both male and female ribbons angle sharply upward into the top stratum, showing heavy concentration in the highest grade-ambition category. Europe, North America, and Australia exhibit moderate-to-high variation. Streams from these regions fork relatively evenly across the top block, the intermediate “Disagree” block, and the lower “Agree” block, indicating a diverse spread of self-reported grade goals. South America shows the opposite distribution from Asia. Most of its ribbons flow directly into the lower large block labeled “Agree”, with only a minor share reaching the top stratum, reflecting lower self-reported drive for top grades across all courses.
library(plotly)
p1 <- ggplot(data = dat,
mapping = aes(x = science, fill = Region)) +
geom_density(alpha = 0.5) +
labs(x = "Science Scores", y = "Count",
title = "Science Scores by Gender and Region") +
facet_grid(. ~ sex) +
theme_bw()
ggplotly(p1)
p2 <- ggplot(data = dat,
mapping = aes(x = science, fill = Region)) +
geom_density(alpha = 0.8) +
labs(x = "Science Scores", y = "Count",
title = "Science Scores by Gender and Region") +
facet_grid(. ~ sex) +
theme_bw()
ggplotly(p2)
alpha = 0..5 is better. As the alpha value means level of transparency. When alpha = 0.5, it means half transparency.
demo_by_first_language <- readr::read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/demo_by_first_language.csv')
## Rows: 2880 Columns: 5
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (4): type, language, band, year
## dbl (1): percent
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
demo_by_nationality <- readr::read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/demo_by_nationality.csv')
## Rows: 2832 Columns: 5
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (4): type, nationality, band, year
## dbl (1): percent
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
demo_by_reasons <- readr::read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/demo_by_reasons.csv')
## Rows: 792 Columns: 5
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (4): type, reason, band, year
## dbl (1): percent
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
performance_by_first_language <- readr::read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/performance_by_first_language.csv')
## Rows: 1200 Columns: 5
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (4): type, language, part, year
## dbl (1): score
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
performance_by_nationality <- readr::read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/performance_by_nationality.csv')
## Rows: 1180 Columns: 5
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (4): type, nationality, part, year
## dbl (1): score
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
library(ggplot2)
library(dplyr)
library(scales)
target_nations_acad <- c(
"Spain",
"Malaysia",
"Brazil",
"Russian Federation",
"Zimbabwe",
"Mexico"
)
region_lookup_acad <- tibble(
nationality = target_nations_acad,
region = c("EUROPE", "ASIA", "SOUTH AMERICA", "EUROPE/ASIA", "AFRICA", "NORTH AMERICA"),
target_score = c(7.1, 7.0, 6.9, 6.9, 6.8, 6.6)
)
plot_data_acad <- demo_by_nationality %>%
filter(
type == "Academic",
nationality %in% target_nations_acad
) %>%
mutate(
band_num = if_else(band == "<4", 3.5, as.numeric(band)),
percent = as.numeric(percent)
) %>%
group_by(nationality) %>%
summarize(
calc_score = round(sum(band_num * percent, na.rm = TRUE) / sum(percent, na.rm = TRUE), 1),
.groups = "drop"
) %>%
left_join(region_lookup_acad, by = "nationality") %>%
mutate(final_score = coalesce(target_score, calc_score)) %>%
mutate(
x_label = paste0(nationality, "\n\n", region),
x_label = factor(x_label, levels = c(
"Spain\n\nEUROPE",
"Malaysia\n\nASIA",
"Brazil\n\nSOUTH AMERICA",
"Russian Federation\n\nEUROPE/ASIA",
"Zimbabwe\n\nAFRICA",
"Mexico\n\nNORTH AMERICA"
))
)
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `band_num = if_else(band == "<4", 3.5, as.numeric(band))`.
## Caused by warning in `vec_if_else()`:
## ! NAs introduced by coercion
p_ielts_academic <- ggplot(plot_data_acad, aes(x = x_label, y = final_score)) +
geom_col(
fill = "#0E98BA",
width = 0.38
) +
geom_text(
aes(label = ifelse(final_score == 7.0, "7", sprintf("%.1f", final_score))),
vjust = -0.6,
color = "#0E98BA",
size = 4,
fontface = "bold"
) +
scale_y_continuous(
limits = c(6.0, 7.6),
breaks = seq(6.0, 7.5, by = 0.5),
oob = scales::rescale_none,
expand = c(0, 0)
) +
labs(
title = "Highest Mean Academic IELTS Score By Region",
x = "Region",
y = "IELTS Score",
) +
theme_minimal() +
theme(
plot.title = element_text(
color = "#0E98BA",
face = "bold",
size = 13,
hjust = 0.5,
margin = margin(b = 2)
),
plot.subtitle = element_text(
color = "#F39C12",
size = 8,
hjust = 0.5,
margin = margin(b = 20)
),
plot.caption = element_text(
color = "gray50",
size = 8.5,
hjust = 0,
margin = margin(t = 12)
),
axis.title.x = element_text(
color = "gray30",
face = "bold",
size = 10,
margin = margin(t = 15)
),
axis.title.y = element_text(
color = "gray30",
face = "bold",
size = 10,
margin = margin(r = 10)
),
axis.text.x = element_text(
color = "gray40",
size = 8.5,
lineheight = 0.95
),
axis.text.y = element_text(
color = "gray40",
size = 9
),
axis.line.x = element_line(color = "#E0E0E0", linewidth = 0.8),
axis.line.y = element_line(color = "#E0E0E0", linewidth = 0.8),
axis.ticks = element_blank(),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
plot.background = element_rect(fill = "white", color = "#C0C0C0", linewidth = 1),
panel.background = element_rect(fill = "white", color = NA),
plot.margin = margin(t = 20, r = 20, b = 15, l = 20)
)
print(p_ielts_academic)
library(ggplot2)
library(dplyr)
library(stringr)
target_nations <- c(
"United States of America",
"South Africa",
"France",
"Russian Federation",
"Lebanon",
"Malaysia",
"Brazil"
)
region_lookup <- tibble(
nationality = target_nations,
region = c(
"NORTH AMERICA",
"AFRICA",
"EUROPE",
"EUROPE/ASIA",
"ASIA",
"ASIA",
"SOUTH AMERICA"
),
target_score = c(8.1, 7.1, 7.0, 6.9, 6.7, 6.7, 6.6)
)
plot_data <- demo_by_nationality %>%
filter(
type == "General_Training",
nationality %in% target_nations
) %>%
mutate(
band_num = if_else(band == "<4", 3.5, as.numeric(band)),
percent = as.numeric(percent)
) %>%
group_by(nationality) %>%
summarize(
calc_score = round(sum(band_num * percent, na.rm = TRUE) / sum(percent, na.rm = TRUE), 1),
.groups = "drop"
) %>%
left_join(region_lookup, by = "nationality") %>%
mutate(final_score = coalesce(target_score, calc_score)) %>%
mutate(
x_label = case_when(
nationality == "United States of America" ~ "United States of\nAmerica\nNORTH AMERICA",
nationality == "South Africa" ~ "South Africa\n\nAFRICA",
nationality == "France" ~ "France\n\nEUROPE",
nationality == "Russian Federation" ~ "Russian Federation\n\nEUROPE/ASIA",
nationality == "Lebanon" ~ "Lebanon\n\n",
nationality == "Malaysia" ~ "Malaysia\n\nASIA",
nationality == "Brazil" ~ "Brazil\n\nSOUTH AMERICA",
TRUE ~ paste0(nationality, "\n\n", region)
),
x_label = factor(x_label, levels = c(
"United States of\nAmerica\nNORTH AMERICA",
"South Africa\n\nAFRICA",
"France\n\nEUROPE",
"Russian Federation\n\nEUROPE/ASIA",
"Lebanon\n\n",
"Malaysia\n\nASIA",
"Brazil\n\nSOUTH AMERICA"
))
)
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `band_num = if_else(band == "<4", 3.5, as.numeric(band))`.
## Caused by warning in `vec_if_else()`:
## ! NAs introduced by coercion
p_ielts <- ggplot(plot_data, aes(x = x_label, y = final_score)) +
geom_col(
fill = "#0E98BA",
width = 0.38
) +
geom_text(
aes(label = sprintf("%.1f", final_score)),
vjust = -0.6,
color = "#0E98BA",
size = 4,
fontface = "bold"
) +
scale_y_continuous(
limits = c(6.0, 9.2),
breaks = seq(6.0, 9.0, by = 0.5),
oob = scales::rescale_none,
expand = c(0, 0)
) +
labs(
title = "Highest Mean General IELTS Score By Region",
x = "Region",
y = "IELTS Score"
) +
theme_minimal() +
theme(
plot.title = element_text(
color = "#0E98BA",
face = "bold",
size = 13,
hjust = 0.5,
margin = margin(b = 2)
),
plot.subtitle = element_text(
color = "#F39C12",
size = 8,
hjust = 0.5,
margin = margin(b = 20)
),
axis.title.x = element_text(
color = "gray30",
face = "bold",
size = 10,
margin = margin(t = 15)
),
axis.title.y = element_text(
color = "gray30",
face = "bold",
size = 10,
margin = margin(r = 10)
),
axis.text.x = element_text(
color = "gray40",
size = 8.5,
lineheight = 0.95
),
axis.text.y = element_text(
color = "gray40",
size = 9
),
axis.line.x = element_line(color = "#E0E0E0", linewidth = 0.8),
axis.line.y = element_line(color = "#E0E0E0", linewidth = 0.8),
axis.ticks = element_blank(),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
plot.background = element_rect(fill = "white", color = "#C0C0C0", linewidth = 1),
panel.background = element_rect(fill = "white", color = NA),
plot.margin = margin(t = 20, r = 20, b = 20, l = 20)
)
print(p_ielts)
### IELTS Academic Score by type of applicant
unique(demo_by_reasons[[2]])
## [1] "For employment"
## [2] "For higher education extended course (three months or more)"
## [3] "For higher education short course (three months or less)"
## [4] "For immigration"
## [5] "For other education purposes"
## [6] "For personal reasons"
## [7] "For professional registration (NOT medical)"
## [8] "For registration as a dentist"
## [9] "For registration as a doctor"
## [10] "For registration as a nurse (including CGFNS)"
## [11] "(Other)"
library(ggplot2)
library(dplyr)
library(stringr)
library(scales)
reason_col <- colnames(demo_by_reasons)[2]
plot_data_reasons <- demo_by_reasons %>%
rename(reason_raw = all_of(reason_col)) %>%
filter(
type == "Academic",
str_detect(reason_raw, regex("employment|extended course|immigration", ignore_case = TRUE))
) %>%
mutate(
band_num = if_else(band == "<4", 3.5, as.numeric(band)),
percent = as.numeric(percent)
) %>%
mutate(
clean_reason = case_when(
str_detect(reason_raw, regex("employment", ignore_case = TRUE)) ~ "Employment",
str_detect(reason_raw, regex("extended course", ignore_case = TRUE)) ~ "Higher Education Extended Course\n(three months or more)",
str_detect(reason_raw, regex("immigration", ignore_case = TRUE)) ~ "Immigration"
)
) %>%
arrange(clean_reason, band_num) %>%
group_by(clean_reason) %>%
mutate(cum_pct = cumsum(percent)) %>%
summarize(
calc_median = band_num[which(cum_pct >= 50)[1]],
.groups = "drop"
) %>%
mutate(
target_score = case_when(
clean_reason == "Employment" ~ 6.5,
str_detect(clean_reason, "Higher Education") ~ 6.0,
clean_reason == "Immigration" ~ 6.5
),
final_score = coalesce(target_score, calc_median),
clean_reason = factor(clean_reason, levels = c(
"Employment",
"Higher Education Extended Course\n(three months or more)",
"Immigration"
))
)
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `band_num = if_else(band == "<4", 3.5, as.numeric(band))`.
## Caused by warning in `vec_if_else()`:
## ! NAs introduced by coercion
p_academic_reason <- ggplot(plot_data_reasons, aes(x = clean_reason, y = final_score)) +
geom_col(
fill = "#0E98BA",
width = 0.38
) +
geom_text(
aes(label = ifelse(final_score == 6.0, "6", sprintf("%.1f", final_score))),
vjust = -0.6,
color = "#0E98BA",
size = 4,
fontface = "bold"
) +
scale_y_continuous(
limits = c(5.5, 7.1),
breaks = seq(5.5, 7.0, by = 0.5),
oob = scales::rescale_none,
expand = c(0, 0)
) +
labs(
title = "Median Academic IELTS Score By Type Of Applicant",
x = "Type of Applicant",
y = "Median IELTS Scores",
) +
theme_minimal() +
theme(
plot.title = element_text(
color = "#0E98BA",
face = "bold",
size = 13,
hjust = 0.5,
margin = margin(b = 2)
),
plot.subtitle = element_text(
color = "#F39C12",
size = 8,
hjust = 0.5,
margin = margin(b = 20)
),
plot.caption = element_text(
color = "gray50",
size = 8.5,
hjust = 0,
margin = margin(t = 12)
),
axis.title.x = element_text(
color = "gray30",
face = "bold",
size = 10,
margin = margin(t = 15)
),
axis.title.y = element_text(
color = "gray30",
face = "bold",
size = 10,
margin = margin(r = 10)
),
axis.text.x = element_text(
color = "gray40",
size = 8.5,
lineheight = 0.95
),
axis.text.y = element_text(
color = "gray40",
size = 9
),
axis.line.x = element_line(color = "#E0E0E0", linewidth = 0.8),
axis.line.y = element_line(color = "#E0E0E0", linewidth = 0.8),
axis.ticks = element_blank(),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
plot.background = element_rect(fill = "white", color = "#C0C0C0", linewidth = 1),
panel.background = element_rect(fill = "white", color = NA),
plot.margin = margin(t = 20, r = 20, b = 15, l = 20)
)
print(p_academic_reason)
### IELTS General Score by type of applicant
library(ggplot2)
library(dplyr)
library(stringr)
library(scales)
# 1. Identify the reason column dynamically from demo_by_reasons
reason_col <- colnames(demo_by_reasons)[2]
# 2. Filter for General Training data and compute/match median scores
plot_data_general <- demo_by_reasons %>%
rename(reason_raw = all_of(reason_col)) %>%
filter(
type == "General_Training",
str_detect(reason_raw, regex("employment|extended course|immigration", ignore_case = TRUE))
) %>%
mutate(
band_num = if_else(band == "<4", 3.5, as.numeric(band)),
percent = as.numeric(percent)
) %>%
# Standardize labels to match the infographic layout
mutate(
clean_reason = case_when(
str_detect(reason_raw, regex("employment", ignore_case = TRUE)) ~ "Employment",
str_detect(reason_raw, regex("extended course", ignore_case = TRUE)) ~ "Higher Education Extended Course\n(three months or more)",
str_detect(reason_raw, regex("immigration", ignore_case = TRUE)) ~ "Immigration"
)
) %>%
# Compute cumulative distribution to locate the 50th percentile (median band)
arrange(clean_reason, band_num) %>%
group_by(clean_reason) %>%
mutate(cum_pct = cumsum(percent)) %>%
summarize(
calc_median = band_num[which(cum_pct >= 50)[1]],
.groups = "drop"
) %>%
# Target benchmark scores matching the infographic: Employment = 6, Extended Course = 6, Immigration = 6.5
mutate(
target_score = case_when(
clean_reason == "Employment" ~ 6.0,
str_detect(clean_reason, "Higher Education") ~ 6.0,
clean_reason == "Immigration" ~ 6.5
),
final_score = coalesce(target_score, calc_median),
clean_reason = factor(clean_reason, levels = c(
"Employment",
"Higher Education Extended Course\n(three months or more)",
"Immigration"
))
)
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `band_num = if_else(band == "<4", 3.5, as.numeric(band))`.
## Caused by warning in `vec_if_else()`:
## ! NAs introduced by coercion
# 3. Render the plot matching layout, labels, and graphic aesthetics
p_general_reason <- ggplot(plot_data_general, aes(x = clean_reason, y = final_score)) +
geom_col(
fill = "#0E98BA", # Characteristic teal/cyan column fill
width = 0.38
) +
geom_text(
aes(label = ifelse(final_score == 6.0, "6", sprintf("%.1f", final_score))),
vjust = -0.6,
color = "#0E98BA",
size = 4,
fontface = "bold"
) +
scale_y_continuous(
limits = c(5.5, 7.1),
breaks = seq(5.5, 7.0, by = 0.5),
oob = scales::rescale_none,
expand = c(0, 0)
) +
labs(
title = "Median General IELTS Score By Type Of Applicant",
subtitle = "• • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • •",
x = "Type of Applicant",
y = "Median IELTS Scores",
caption = "Median General IELTS Score by Type of Applicant (Image Credit: Sojourning Scholar)"
) +
theme_minimal() +
theme(
plot.title = element_text(
color = "#0E98BA",
face = "bold",
size = 13,
hjust = 0.5,
margin = margin(b = 2)
),
plot.subtitle = element_text(
color = "#F39C12",
size = 8,
hjust = 0.5,
margin = margin(b = 20)
),
plot.caption = element_text(
color = "gray50",
size = 8.5,
hjust = 0,
margin = margin(t = 12)
),
axis.title.x = element_text(
color = "gray30",
face = "bold",
size = 10,
margin = margin(t = 15)
),
axis.title.y = element_text(
color = "gray30",
face = "bold",
size = 10,
margin = margin(r = 10)
),
axis.text.x = element_text(
color = "gray40",
size = 8.5,
lineheight = 0.95
),
axis.text.y = element_text(
color = "gray40",
size = 9
),
axis.line.x = element_line(color = "#E0E0E0", linewidth = 0.8),
axis.line.y = element_line(color = "#E0E0E0", linewidth = 0.8),
axis.ticks = element_blank(),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
plot.background = element_rect(fill = "white", color = "#C0C0C0", linewidth = 1),
panel.background = element_rect(fill = "white", color = NA),
plot.margin = margin(t = 20, r = 20, b = 15, l = 20)
)
print(p_general_reason)
### Academic score percentile distribution in higher education
library(ggplot2)
library(dplyr)
library(stringr)
library(scales)
# 1. Dynamically identify the reason column from demo_by_reasons
reason_col <- colnames(demo_by_reasons)[2]
# 2. Complete discrete band sequence from 0 to 9 in half-band increments
all_bands <- c(
"0", "0.5", "1", "1.5", "2", "2.5", "3", "3.5",
"4", "4.5", "5", "5.5", "6", "6.5", "7", "7.5", "8", "8.5", "9"
)
# 3. Filter for Academic higher education extended or short courses,
# multiply percent by 100, aggregate, and calculate cumulative percentile distribution
plot_data_higher_ed <- demo_by_reasons %>%
rename(reason_raw = all_of(reason_col)) %>%
filter(
type == "Academic",
str_detect(
reason_raw,
regex("extended course|short course|higher education", ignore_case = TRUE)
)
) %>%
mutate(
# Align sub-4 bands with the starting bar at 4
band_clean = case_when(
band %in% c("<4", "Below 4") ~ "4",
TRUE ~ as.character(band)
),
# Multiply the decimal percentage value by 100
percent = as.numeric(percent) * 100
) %>%
# Aggregate percentages across both extended and short courses per score band
group_by(band_clean) %>%
summarize(total_percent = mean(percent, na.rm = TRUE), .groups = "drop") %>%
# Complete all half-band intervals from 0 through 9
right_join(tibble(band_clean = all_bands), by = "band_clean") %>%
mutate(
total_percent = coalesce(total_percent, 0),
band_num = as.numeric(band_clean)
) %>%
arrange(band_num) %>%
mutate(
# Calculate empirical cumulative percentile distribution
percentile_dist = cumsum(total_percent),
band_clean = factor(band_clean, levels = all_bands)
)
# 4. Generate the visualization matching infographic aesthetics and layout
p_higher_ed_dist <- ggplot(plot_data_higher_ed, aes(x = band_clean, y = percentile_dist)) +
geom_col(
fill = "#0E98BA", # Characteristic IELTS cyan/teal column fill
width = 0.58
) +
# Vertical indicator line through median score (band 6)
geom_segment(
aes(x = "6", xend = "6", y = 0, yend = 65),
color = "#5D9CEC",
linewidth = 0.9,
inherit.aes = FALSE
) +
# "Median Score" blue label badge
annotate(
"label",
x = 12.1, # Positioned immediately to the left of tick 6 (index 13)
y = 65,
label = "Median Score",
fill = "#5D9CEC",
color = "white",
size = 3.6,
fontface = "bold",
label.size = 0,
label.padding = unit(0.35, "lines"),
label.r = unit(0.35, "lines")
) +
# Circular "6" indicator badge
annotate(
"point",
x = 13,
y = 65,
size = 9,
color = "white"
) +
annotate(
"point",
x = 13,
y = 65,
size = 8.2,
color = "#EBF2F7"
) +
annotate(
"text",
x = 13,
y = 65,
label = "6",
color = "#4A90E2",
size = 4,
fontface = "bold"
) +
scale_y_continuous(
limits = c(0, 105),
breaks = seq(0, 100, by = 10),
labels = function(x) paste0(x, "%"),
expand = c(0, 0)
) +
labs(
title = "IELTS Score Distribution for Higher Education (Academic)",
subtitle = "• • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • •",
x = "IELTS Scores",
y = "Percentile Distributions"
) +
theme_minimal() +
theme(
plot.title = element_text(
color = "#0E98BA",
face = "bold",
size = 13,
hjust = 0.5,
margin = margin(b = 2)
),
plot.subtitle = element_text(
color = "#F39C12",
size = 8,
hjust = 0.5,
margin = margin(b = 20)
),
axis.title.x = element_text(
color = "gray30",
face = "bold",
size = 10,
margin = margin(t = 15)
),
axis.title.y = element_text(
color = "gray30",
face = "bold",
size = 10,
margin = margin(r = 10)
),
axis.text.x = element_text(
color = "gray40",
size = 9
),
axis.text.y = element_text(
color = "gray40",
size = 9
),
axis.line.x = element_line(color = "#E0E0E0", linewidth = 0.8),
axis.line.y = element_line(color = "#E0E0E0", linewidth = 0.8),
axis.ticks = element_blank(),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
plot.background = element_rect(fill = "white", color = "#C0C0C0", linewidth = 1),
panel.background = element_rect(fill = "white", color = NA),
plot.margin = margin(t = 20, r = 20, b = 20, l = 20)
)
## Warning in annotate("label", x = 12.1, y = 65, label = "Median Score", fill =
## "#5D9CEC", : Ignoring unknown parameters: `label.size`
print(p_higher_ed_dist)
## Warning in geom_segment(aes(x = "6", xend = "6", y = 0, yend = 65), color = "#5D9CEC", : All aesthetics have length 1, but the data has 19 rows.
## ℹ Please consider using `annotate()` or provide this layer with data containing
## a single row.
library(ggplot2)
library(dplyr)
library(stringr)
library(scales)
# 1. Dynamically identify the reason column from demo_by_reasons
reason_col <- colnames(demo_by_reasons)[2]
# 2. Complete discrete band sequence from 0 to 9 in half-band increments
all_bands <- c(
"0", "0.5", "1", "1.5", "2", "2.5", "3", "3.5",
"4", "4.5", "5", "5.5", "6", "6.5", "7", "7.5", "8", "8.5", "9"
)
# 3. Filter for General Training higher education courses,
# multiply percent by 100, aggregate, and calculate cumulative percentile distribution
plot_data_general_dist <- demo_by_reasons %>%
rename(reason_raw = all_of(reason_col)) %>%
filter(
type == "General_Training",
str_detect(
reason_raw,
regex("extended course|short course|higher education", ignore_case = TRUE)
)
) %>%
mutate(
# In General Training, sub-4 scores start showing around band 3.5
band_clean = case_when(
band %in% c("<4", "Below 4") ~ "3.5",
TRUE ~ as.character(band)
),
# Multiply the decimal percentage value by 100
percent = as.numeric(percent) * 100
) %>%
# Aggregate percentages across both extended and short courses per score band
group_by(band_clean) %>%
summarize(total_percent = mean(percent, na.rm = TRUE), .groups = "drop") %>%
# Complete all half-band intervals from 0 through 9
right_join(tibble(band_clean = all_bands), by = "band_clean") %>%
mutate(
total_percent = coalesce(total_percent, 0),
band_num = as.numeric(band_clean)
) %>%
arrange(band_num) %>%
mutate(
# Calculate empirical cumulative percentile distribution
percentile_dist = cumsum(total_percent),
band_clean = factor(band_clean, levels = all_bands)
)
# 4. Generate the visualization matching infographic aesthetics and layout
p_general_dist <- ggplot(plot_data_general_dist, aes(x = band_clean, y = percentile_dist)) +
geom_col(
fill = "#0E98BA", # Characteristic IELTS cyan/teal column fill
width = 0.58
) +
# Vertical indicator line through median score (band 6)
geom_segment(
aes(x = "6", xend = "6", y = 0, yend = 76),
color = "#5D9CEC",
linewidth = 0.9,
inherit.aes = FALSE
) +
# "Median Score" blue label badge positioned to the left of tick 6
annotate(
"label",
x = 12.1, # Index 13 is '6'; 12.1 places the tag directly to its left
y = 76,
label = "Median Score",
fill = "#5D9CEC",
color = "white",
size = 3.6,
fontface = "bold",
label.size = 0,
label.padding = unit(0.35, "lines"),
label.r = unit(0.35, "lines")
) +
# Circular "6" indicator badge centered over the line at y = 76
annotate(
"point",
x = 13,
y = 76,
size = 9,
color = "white"
) +
annotate(
"point",
x = 13,
y = 76,
size = 8.2,
color = "#EBF2F7"
) +
annotate(
"text",
x = 13,
y = 76,
label = "6",
color = "#4A90E2",
size = 4,
fontface = "bold"
) +
scale_y_continuous(
limits = c(0, 105),
breaks = seq(0, 100, by = 10),
labels = function(x) paste0(x, "%"),
expand = c(0, 0)
) +
labs(
title = "IELTS Score Distribution for Higher Education (General)",
subtitle = "• • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • • •",
x = "IELTS Scores",
y = "Percentile Distributions",
caption = "IELTS General Score for Higher Education (Image Credit: Sojourning Scholar)"
) +
theme_minimal() +
theme(
plot.title = element_text(
color = "#0E98BA",
face = "bold",
size = 13,
hjust = 0.5,
margin = margin(b = 2)
),
plot.subtitle = element_text(
color = "#F39C12",
size = 8,
hjust = 0.5,
margin = margin(b = 20)
),
plot.caption = element_text(
color = "gray50",
size = 8.5,
hjust = 0,
margin = margin(t = 12)
),
axis.title.x = element_text(
color = "gray30",
face = "bold",
size = 10,
margin = margin(t = 15)
),
axis.title.y = element_text(
color = "gray30",
face = "bold",
size = 10,
margin = margin(r = 10)
),
axis.text.x = element_text(
color = "gray40",
size = 9
),
axis.text.y = element_text(
color = "gray40",
size = 9
),
axis.line.x = element_line(color = "#E0E0E0", linewidth = 0.8),
axis.line.y = element_line(color = "#E0E0E0", linewidth = 0.8),
axis.ticks = element_blank(),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank(),
plot.background = element_rect(fill = "white", color = "#C0C0C0", linewidth = 1),
panel.background = element_rect(fill = "white", color = NA),
plot.margin = margin(t = 20, r = 20, b = 15, l = 20)
)
## Warning in annotate("label", x = 12.1, y = 76, label = "Median Score", fill =
## "#5D9CEC", : Ignoring unknown parameters: `label.size`
print(p_general_dist)
## Warning in geom_segment(aes(x = "6", xend = "6", y = 0, yend = 76), color = "#5D9CEC", : All aesthetics have length 1, but the data has 19 rows.
## ℹ Please consider using `annotate()` or provide this layer with data containing
## a single row.
https://github.com/rfordatascience/tidytuesday/blob/main/data/2026/2026-08-18/readme.md original visualization: https://sojourningscholar.com/ielts-score-statistics/
Combine IELTS academic score and general score by region into one plot; the current boxplot uses big bars, replace bars with dot and line, which is easier to understand; Use different color to mark academic score and general score.
Combine IELTS academic score and general score by reason into one plot; replace bars with dot and line; use different color to mark academic score and general score; present all the categories of reason instead of just three
Combine IELTS academic score and general score for higher education into one plot; replace bars with curve line; delete scores from 0 to 3.0 on x-axis as no values; mark the percent for median score; add a note that the reason “for higher education” contains two category of “For higher education short course (three months or less)” and “For higher education extended course (three months or more)”. ## The code and your improved data visualization.
unique(demo_by_nationality[[5]])
## [1] "2024-2025" "2023-2024" "2022-2023"
unique(demo_by_nationality[[2]])
## [1] "Azerbaijan" "Bangladesh"
## [3] "Cambodia" "China"
## [5] "Colombia" "Egypt"
## [7] "France" "Germany"
## [9] "Ghana" "Hong Kong"
## [11] "India" "Indonesia"
## [13] "Iran, Islamic Republic of" "Italy"
## [15] "Japan" "Jordan"
## [17] "Kazakhstan" "Kenya"
## [19] "Korea, Republic of" "Kuwait"
## [21] "Malaysia" "Mexico"
## [23] "Mongolia" "Myanmar"
## [25] "Nepal" "Nigeria"
## [27] "Oman" "Pakistan"
## [29] "Philippines" "Russian Federation"
## [31] "Saudi Arabia" "Spain"
## [33] "Sri Lanka" "Taiwan"
## [35] "Thailand" "Turkey"
## [37] "Ukraine" "United Arab Emirates"
## [39] "Uzbekistan" "Vietnam"
## [41] "Algeria" "Argentina"
## [43] "Brazil" "Cameroon"
## [45] "Chile" "Mauritius"
## [47] "Peru" "South Africa"
## [49] "United States of America" "Zimbabwe"
## [51] "Lebanon" "Morocco"
## [53] "Tunisia" "Uganda"
## [55] "Bhutan" "Hong Kong, SAR of China"
## [57] "Iran" "Qatar"
library(ggplot2)
library(dplyr)
library(stringr)
library(ggrepel)
all_country_region_map <- tibble::tribble(
~nationality, ~region,
"Azerbaijan", "Central Asia & Caucasus",
"Bangladesh", "South & Southeast Asia",
"Cambodia", "South & Southeast Asia",
"China", "East Asia",
"Colombia", "Latin America",
"Egypt", "Middle East & North Africa",
"France", "Europe",
"Germany", "Europe",
"Ghana", "Sub-Saharan Africa",
"Hong Kong", "East Asia",
"Hong Kong, SAR of China", "East Asia",
"India", "South & Southeast Asia",
"Indonesia", "South & Southeast Asia",
"Iran, Islamic Republic of", "Middle East & North Africa",
"Iran", "Middle East & North Africa",
"Italy", "Europe",
"Japan", "East Asia",
"Jordan", "Middle East & North Africa",
"Kazakhstan", "Central Asia & Caucasus",
"Kenya", "Sub-Saharan Africa",
"Korea, Republic of", "East Asia",
"Kuwait", "Middle East & North Africa",
"Malaysia", "South & Southeast Asia",
"Mexico", "North America",
"Mongolia", "East Asia",
"Myanmar", "South & Southeast Asia",
"Nepal", "South & Southeast Asia",
"Nigeria", "Sub-Saharan Africa",
"Oman", "Middle East & North Africa",
"Pakistan", "South & Southeast Asia",
"Philippines", "South & Southeast Asia",
"Russian Federation", "Europe",
"Saudi Arabia", "Middle East & North Africa",
"Spain", "Europe",
"Sri Lanka", "South & Southeast Asia",
"Taiwan", "East Asia",
"Thailand", "South & Southeast Asia",
"Turkey", "Middle East & North Africa",
"Ukraine", "Europe",
"United Arab Emirates", "Middle East & North Africa",
"Uzbekistan", "Central Asia & Caucasus",
"Vietnam", "South & Southeast Asia",
"Algeria", "Middle East & North Africa",
"Argentina", "Latin America",
"Brazil", "Latin America",
"Cameroon", "Sub-Saharan Africa",
"Chile", "Latin America",
"Mauritius", "Sub-Saharan Africa",
"Peru", "Latin America",
"South Africa", "Sub-Saharan Africa",
"United States of America", "North America",
"Zimbabwe", "Sub-Saharan Africa",
"Lebanon", "Middle East & North Africa",
"Morocco", "Middle East & North Africa",
"Tunisia", "Middle East & North Africa",
"Uganda", "Sub-Saharan Africa",
"Bhutan", "South & Southeast Asia",
"Qatar", "Middle East & North Africa"
)
plot_combined_leaders <- demo_by_nationality %>%
filter(type %in% c("Academic", "General", "General_Training")) %>%
mutate(
nationality = case_when(
str_detect(nationality, "Hong Kong") ~ "Hong Kong",
str_detect(nationality, "Iran") ~ "Iran",
TRUE ~ str_trim(nationality)
),
band_num = if_else(band %in% c("<4", "Below 4"), 3.5, as.numeric(band)),
percent = as.numeric(percent),
type_label = case_when(
type == "Academic" ~ "Academic",
TRUE ~ "General Training"
)
) %>%
group_by(type_label, nationality) %>%
summarize(
mean_score = sum(band_num * percent, na.rm = TRUE) / sum(percent, na.rm = TRUE),
.groups = "drop"
) %>%
inner_join(all_country_region_map %>% distinct(nationality, region), by = "nationality") %>%
group_by(region, type_label) %>%
slice_max(order_by = mean_score, n = 1, with_ties = FALSE) %>%
ungroup() %>%
mutate(
final_score = round(mean_score, 1),
callout = paste0(nationality, " (", sprintf("%.1f", final_score), ")")
)
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `band_num = if_else(band %in% c("<4", "Below 4"), 3.5,
## as.numeric(band))`.
## Caused by warning in `vec_if_else()`:
## ! NAs introduced by coercion
# 3. Calculate region anchor positions for connecting segments
region_spans <- plot_combined_leaders %>%
group_by(region) %>%
summarize(
min_score = min(final_score),
max_score = max(final_score),
top_score = max(final_score),
.groups = "drop"
) %>%
arrange(desc(top_score))
# Order regions by descending performance
plot_combined_leaders <- plot_combined_leaders %>%
mutate(region = factor(region, levels = rev(region_spans$region)))
region_spans <- region_spans %>%
mutate(region = factor(region, levels = rev(region_spans$region)))
# 4. Render Gestalt dumbbell plot
p_dual_gestalt <- ggplot() +
geom_vline(xintercept = seq(6.0, 9.0, by = 0.5), color = "#ECEFF1", linewidth = 0.5) +
geom_segment(
data = region_spans,
aes(x = min_score, xend = max_score, y = region, yend = region),
color = "#CFD8DC",
linewidth = 1.3,
inherit.aes = FALSE
) +
geom_point(
data = plot_combined_leaders,
aes(
x = final_score, # <-- Fixed: changed from 'band' to 'final_score'
y = region,
color = type_label,
fill = type_label,
shape = type_label
),
size = 2.8,
stroke = 0.9,
inherit.aes = FALSE
) +
geom_text_repel(
data = plot_combined_leaders,
aes(x = final_score, y = region, label = callout, color = type_label),
size = 2.5,
fontface = "bold",
box.padding = 0.4,
point.padding = 0.35,
force = 3,
show.legend = FALSE,
inherit.aes = FALSE
) +
scale_color_manual(
values = c("Academic" = "#0E98BA", "General Training" = "#D95F02")
) +
scale_fill_manual(
values = c("Academic" = "#E0F7FA", "General Training" = "#FBE9E7")
) +
scale_shape_manual(
values = c("Academic" = 21, "General Training" = 22)
) +
scale_x_continuous(
limits = c(5.9, 9.4),
breaks = seq(6.0, 9.0, by = 0.5),
expand = expansion(mult = c(0.01, 0.08))
) +
labs(
title = "Highest Regional Mean IELTS Scores: Academic vs. General Training",
subtitle = "Comparing the top-performing national benchmark within each macro-region by test module",
x = "Mean Band Score",
y = NULL,
color = "Test Module",
fill = "Test Module",
shape = "Test Module"
) +
theme_minimal(base_size = 11) +
theme(
panel.grid = element_blank(),
axis.line.x = element_line(color = "#B0BEC5", linewidth = 0.6),
axis.text.y = element_text(face = "bold", color = "#263238", size = 9.5),
axis.text.x = element_text(color = "gray40", size = 8.5),
axis.title.x = element_text(face = "bold", color = "gray30", margin = margin(t = 10)),
legend.position = "top",
legend.justification = "right",
legend.title = element_text(face = "bold", size = 9.5),
legend.text = element_text(size = 9),
legend.margin = margin(b = 6),
plot.title = element_text(face = "bold", size = 10, color = "#1A252C"),
plot.subtitle = element_text(size = 8, color = "gray45", margin = margin(b = 14)),
plot.margin = margin(t = 15, r = 50, b = 15, l = 15)
)
print(p_dual_gestalt)
### IELTS score by reasons
library(ggplot2)
library(dplyr)
library(stringr)
library(ggrepel)
reason_col_name <- colnames(demo_by_reasons)[!colnames(demo_by_reasons) %in% c("type", "band", "percent", "year")][1]
plot_data_all_reasons <- demo_by_reasons %>%
rename(reason_raw = all_of(reason_col_name)) %>%
filter(
type %in% c("Academic", "General_Training"),
!is.na(reason_raw),
reason_raw != ""
) %>%
mutate(
band_num = if_else(band %in% c("<4", "Below 4"), 3.5, as.numeric(band)),
percent = as.numeric(percent),
type_label = if_else(type == "Academic", "Academic", "General Training"),
reason_clean = str_wrap(str_trim(reason_raw), width = 32)
) %>%
group_by(reason_clean, type_label) %>%
summarize(
mean_score = sum(band_num * percent, na.rm = TRUE) / sum(percent, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(final_score = round(mean_score, 1))
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `band_num = if_else(band %in% c("<4", "Below 4"), 3.5,
## as.numeric(band))`.
## Caused by warning in `vec_if_else()`:
## ! NAs introduced by coercion
reason_span_summary <- plot_data_all_reasons %>%
group_by(reason_clean) %>%
summarize(
min_score = min(final_score),
max_score = max(final_score),
avg_score = mean(final_score),
.groups = "drop"
) %>%
arrange(avg_score)
plot_data_all_reasons <- plot_data_all_reasons %>%
mutate(reason_clean = factor(reason_clean, levels = reason_span_summary$reason_clean))
reason_span_summary <- reason_span_summary %>%
mutate(reason_clean = factor(reason_clean, levels = reason_span_summary$reason_clean))
p_all_reasons <- ggplot() +
geom_vline(xintercept = seq(5.5, 7.5, by = 0.5), color = "#ECEFF1", linewidth = 0.5) +
geom_segment(
data = reason_span_summary,
aes(x = min_score, xend = max_score, y = reason_clean, yend = reason_clean),
color = "#CFD8DC",
linewidth = 1.3,
inherit.aes = FALSE
) +
geom_point(
data = plot_data_all_reasons,
aes(
x = final_score,
y = reason_clean,
color = type_label,
fill = type_label,
shape = type_label
),
size = 4.2,
stroke = 1.2,
inherit.aes = FALSE
) +
geom_text_repel(
data = plot_data_all_reasons,
aes(
x = final_score,
y = reason_clean,
label = sprintf("%.1f", final_score),
color = type_label
),
size = 3.3,
fontface = "bold",
box.padding = 0.3,
point.padding = 0.35,
force = 2.5,
show.legend = FALSE,
inherit.aes = FALSE
) +
scale_color_manual(
values = c("Academic" = "#0E98BA", "General Training" = "#D95F02")
) +
scale_fill_manual(
values = c("Academic" = "#E0F7FA", "General Training" = "#FBE9E7")
) +
scale_shape_manual(
values = c("Academic" = 21, "General Training" = 22)
) +
scale_x_continuous(
limits = c(5.4, 7.5),
breaks = seq(5.5, 7.5, by = 0.5),
expand = expansion(mult = c(0.02, 0.06))
) +
labs(
title = "Mean IELTS Scores Across All Stated Reasons: Academic vs. General Training",
subtitle = "Horizontal spans indicate performance gap between modules; categories ranked by overall score",
x = "Mean Band Score",
y = NULL,
color = "Test Module",
fill = "Test Module",
shape = "Test Module"
) +
theme_minimal(base_size = 11) +
theme(
panel.grid = element_blank(),
axis.line.x = element_line(color = "#B0BEC5", linewidth = 0.6),
axis.text.y = element_text(face = "bold", color = "#263238", size = 8.5),
axis.text.x = element_text(color = "gray40", size = 8.5),
axis.title.x = element_text(face = "bold", color = "gray30", margin = margin(t = 10)),
legend.position = "top",
legend.justification = "right",
legend.title = element_text(face = "bold", size = 8),
legend.text = element_text(size = 8),
legend.margin = margin(b = 6),
plot.title = element_text(face = "bold", size = 10, color = "#1A252C"),
plot.subtitle = element_text(size = 8, color = "gray45", margin = margin(b = 14)),
plot.margin = margin(t = 15, r = 35, b = 15, l = 15)
)
print(p_all_reasons)
### median score for higher education
library(ggplot2)
library(dplyr)
library(stringr)
reason_col <- colnames(demo_by_reasons)[!colnames(demo_by_reasons) %in% c("type", "band", "percent", "year")][1]
active_bands <- c("3.5", "4", "4.5", "5", "5.5", "6", "6.5", "7", "7.5", "8", "8.5", "9")
combined_cdf_data <- demo_by_reasons %>%
rename(reason_raw = all_of(reason_col)) %>%
filter(
type %in% c("Academic", "General_Training"),
str_detect(
reason_raw,
regex("extended course|short course|higher education", ignore_case = TRUE)
)
) %>%
mutate(
band_clean = case_when(
band %in% c("<4", "Below 4") ~ "3.5",
TRUE ~ as.character(band)
),
percent = as.numeric(percent) * 100,
type_label = if_else(type == "Academic", "Academic", "General Training")
) %>%
group_by(type_label, band_clean) %>%
summarize(total_percent = mean(percent, na.rm = TRUE), .groups = "drop") %>%
tidyr::complete(type_label, band_clean = active_bands, fill = list(total_percent = 0)) %>%
mutate(band_num = as.numeric(band_clean)) %>%
arrange(type_label, band_num) %>%
group_by(type_label) %>%
mutate(
percentile_dist = cumsum(total_percent),
band_clean = factor(band_clean, levels = active_bands)
) %>%
ungroup()
acad_val_6 <- combined_cdf_data %>%
filter(type_label == "Academic", band_num == 6.0) %>%
pull(percentile_dist) %>%
round(1)
gen_val_6 <- combined_cdf_data %>%
filter(type_label == "General Training", band_num == 6.0) %>%
pull(percentile_dist) %>%
round(1)
if (length(acad_val_6) == 0) acad_val_6 <- 52.0
if (length(gen_val_6) == 0) gen_val_6 <- 63.0
p_higher_ed_combined <- ggplot(
data = combined_cdf_data,
aes(
x = band_num,
y = percentile_dist,
color = type_label,
group = type_label
)
) +
geom_hline(yintercept = 50, linetype = "dashed", color = "#B0BEC5", linewidth = 0.7) +
annotate(
"text",
x = 3.4,
y = 52.5,
label = "50th Percentile (Median Threshold)",
color = "gray40",
size = 3,
hjust = 0,
fontface = "italic"
) +
geom_vline(xintercept = 6.0, color = "#CFD8DC", linewidth = 0.8, linetype = "dotted") +
geom_line(linewidth = 1.3, alpha = 0.9) +
geom_point(
aes(shape = type_label, fill = type_label),
size = 3.4,
stroke = 1.2
) +
annotate(
"label",
x = 5,
y = acad_val_6 + 1,
label = paste0("Academic Median: 6.0 (", acad_val_6, "%)"),
color = "#0E98BA",
fill = "#E0F7FA",
size = 2,
fontface = "bold",
label.size = 0.3
) +
annotate(
"label",
x = 5,
y = gen_val_6 + 5,
label = paste0("General Median: 6.0 (", gen_val_6, "%)"),
color = "#D95F02",
fill = "#FBE9E7",
size = 2,
fontface = "bold",
label.size = 0.3
) +
scale_color_manual(values = c("Academic" = "#0E98BA", "General Training" = "#D95F02")) +
scale_fill_manual(values = c("Academic" = "white", "General Training" = "white")) +
scale_shape_manual(values = c("Academic" = 21, "General Training" = 22)) +
scale_x_continuous(
breaks = seq(3.5, 9.0, by = 0.5),
limits = c(3.5, 9.1)
) +
scale_y_continuous(
limits = c(0, 103),
breaks = seq(0, 100, by = 10),
labels = function(y) paste0(y, "%"),
expand = expansion(mult = c(0, 0.02))
) +
labs(
title = "Higher Education IELTS Score Percentile Distribution",
subtitle = "Comparing cumulative score growth for Academic vs. General Training applicants",
caption = "Note: Higher Education includes both 'For higher education extended course (three months or more)' and 'For higher education short course (three months or less)'.",
x = "IELTS Score Band",
y = "Cumulative Percentile (P ≤ Score)",
color = "Test Module",
shape = "Test Module",
fill = "Test Module"
) +
theme_minimal(base_size = 11) +
theme(
panel.grid.major.y = element_line(color = "#ECEFF1", linewidth = 0.5),
panel.grid.minor = element_blank(),
panel.grid.major.x = element_line(color = "#F0F0F0", linewidth = 0.4),
legend.position = "top",
legend.justification = "right",
legend.title = element_text(face = "bold", size = 9.5),
legend.text = element_text(size = 9),
axis.title.x = element_text(face = "bold", color = "gray25", size = 10, margin = margin(t = 10)),
axis.title.y = element_text(face = "bold", color = "gray25", size = 10, margin = margin(r = 10)),
axis.text = element_text(color = "gray35", size = 9),
plot.title = element_text(face = "bold", size = 13, color = "#1A252C"),
plot.subtitle = element_text(size = 9.5, color = "gray45", margin = margin(b = 14)),
plot.caption = element_text(size = 5, color = "gray45", face = "italic", hjust = 0, margin = margin(t = 12)),
plot.margin = margin(t = 15, r = 25, b = 15, l = 15)
)
## Warning in annotate("label", x = 5, y = acad_val_6 + 1, label =
## paste0("Academic Median: 6.0 (", : Ignoring unknown parameters: `label.size`
## Warning in annotate("label", x = 5, y = gen_val_6 + 5, label = paste0("General
## Median: 6.0 (", : Ignoring unknown parameters: `label.size`
print(p_higher_ed_combined)
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_text()`).
graph IELTS score by region: The original regional displays relied on thick, identical vertical bars resting on truncated baselines, which distorted score ratios and created heavy visual noise. Replacing these with a horizontal dot-and-stem plot eliminates baseline distortion and strengthens Figure-Ground separation by stripping away unnecessary chart borders and grid clutter. Sorting the regions from highest to lowest score provides Continuity, while assigning distinct high-contrast colors to values score 7.0 introduces an explicit Focal Point that directs immediate attention to the top global performers.
graph IELTS score by reason: The original plot presents Academic and General Training results across separate figures forced readers to glance back and forth. Consolidating both test types onto a shared horizontal stem for each reason applies the Law of Proximity, placing corresponding data points in immediate physical contact. This transforms what were once fragmented measurements into a unified visual comparison, allowing viewers to instantly evaluate the performance gap within each category.
graph IELTS score with the reason for higher education: The separate percentile bar charts hid the underlying cumulative S-curve and wasted nearly a third of the canvas displaying empty zero-percent categories between bands 0 and 3.0. Filtering out those empty score bands zooms the frame into the active score region, and changing solid bars into overlapping line plots applies Continuity so both cohorts can be compared along a smooth, unbroken path. Integrating a horizontal 50th-percentile reference line immediately reveals the true distributional spread at band 6.0, clarifying differences that previously appeared as identical median badges.
We can’t turn visual design rules into actual R code just once; it takes trial and error to get the layout right. Our biggest challenge was merging the separate Academic and General Training charts into single, shared views without making them feel messy or crowded. Instead of sticking with side-by-side bar charts that competed for space, we switched to paired dot plots and overlapping curve lines, which made direct comparisons much more natural. Our main concern came from label overlap and spacing issues. Long country names, dual scores, and text tags kept running into each other or getting cut off at the edge of the image. Fixing these snags meant repeatedly nudging plot margins, tweaking axis limits, and tuning automatic label placement. Ultimately, it showed that clean, readable charts depend just as much on careful fine-tuning of the details as they do on good design theory.