library(tidyverse)
library(tidystata)4 Sort observations
Which countries have the highest recorded life expectancy? Sorting makes the order useful for inspection without separating a country’s values from its name. Moving columns in Chapter 3 changed the reading order of variables; here we change the reading order of observations.
By the end of this chapter, you will be able to prepare columns from stored names or numeric storage, sort rows, reverse a sorting direction, resolve ties, and locate missing sorting values. Start with the unchanged package data:
4.1 Prepare ranking columns
Before ranking countries, we can prepare a table containing the identifier and measurements we need. The preceding chapter chose columns directly. Sometimes we want to save their names for reuse, for example when producing several versions of a ranking. We will store quoted names, then tell select() how to interpret them. None of these preparation steps sorts rows or changes lifeexp.
c() combines its inputs into a vector, an ordered sequence of values. Here we use it only to store column names as text. Put each name in quotation marks and separate the names with commas:
ranking_names <- c("country", "lexp", "gnppc")
ranking_names[1] "country" "lexp" "gnppc"
The result contains three character values in the order supplied. ranking_names holds names, not the corresponding country measurements. Quoted "lexp" is text in this vector; the unquoted lexp in our earlier selection identified a column directly.
Inside the familiar select(), all_of() reads a character vector as required column names:
ranking_table <- select(lifeexp, all_of(ranking_names))
ranking_table# A tibble: 68 × 3
country lexp gnppc
<chr> <dbl> <dbl>
1 Albania 74 870
2 Armenia 70 570
3 Austria 78 27810
4 Azerbaijan 64 510
5 Belarus 68 1540
6 Belgium 77 26530
7 Bosnia and Herzegovina 74 1230
8 Bulgaria 71 1440
9 Croatia 72 NA
10 Czechia 75 6140
# ℹ 58 more rows
The table has 68 countries and three columns, following the vector’s order. all_of() requires every name to exist. If we replaced "gnppc" with "income", the call would fail because that name is absent. This check protects a ranking that depends on particular variables: a missing required column should stop the task so we can correct it.
For genuinely optional columns, any_of() keeps whichever requested names exist. Save the optional request separately, then use it alongside explicitly required columns:
optional_names <- c("safewater", "income")select(lifeexp, country, lexp, gnppc, any_of(optional_names))# A tibble: 68 × 4
country lexp gnppc safewater
<chr> <dbl> <dbl> <dbl>
1 Albania 74 870 49
2 Armenia 70 570 83
3 Austria 78 27810 97
4 Azerbaijan 64 510 49
5 Belarus 68 1540 82
6 Belgium 77 26530 100
7 Bosnia and Herzegovina 74 1230 87
8 Bulgaria 71 1440 90
9 Croatia 72 NA 85
10 Czechia 75 6140 95
# ℹ 58 more rows
This returns country, lexp, gnppc, and safewater. It silently omits income; it neither creates that variable nor substitutes gnppc for it. Country names and the two intended sorting variables remain explicitly required. Use optional selection only when the task can proceed without an absent variable; using it for a needed sorting key would hide the problem until a later step.
We can also choose columns by storage type. First try is.numeric() on plain inputs. It returns a logical answer, TRUE for numeric input and FALSE otherwise:
is.numeric(25)[1] TRUE
is.numeric("25")[1] FALSE
The first result is TRUE; the second is FALSE. Quotation marks make the second input text even though it contains digits. These calls return answers without changing their inputs.
Inside select(), where() keeps columns that pass a predicate, a function returning one TRUE or FALSE decision for each complete column. Supply is.numeric without parentheses: where() will call that function on each column in turn.
numeric_ranking <- select(lifeexp, country, where(is.numeric))
ds(numeric_ranking)[1] "country" "region" "popgrowth" "lexp" "gnppc" "safewater"
country remains explicitly selected because names identify the records we will rank. The other columns pass the numeric test, including the labelled region codes. Numeric storage does not establish quantitative meaning: a region code identifies a category, so its magnitude is not a measure of how much region a country has. Use the variable descriptions to choose meaningful sorting keys. The predicate selects whole columns, not individual cells, and leaves missing entries and all 68 countries in place. Next we will introduce row sorting and apply it to a prepared table.
4.2 Order whole records
arrange() takes a table and one or more sorting columns. Its default order is ascending, from smaller to larger. Use the ranking_table prepared above. Save the sorted result, then use the familiar head() to return the first six rows for inspection without replacing the full table:
life_low_first <- arrange(ranking_table, lexp)
head(life_low_first)# A tibble: 6 × 3
country lexp gnppc
<chr> <dbl> <dbl>
1 Haiti 57 410
2 Bolivia 61 960
3 Tajikistan 62 170
4 Honduras 62 960
5 Kazakhstan 63 1340
6 Azerbaijan 64 510
The lowest life expectancies appear first. All three selected columns move with each country. The result still contains 68 countries; head() returns the first six rows for inspection. lifeexp remains in its original order because we saved the sorted result separately.
Put a sorting column inside desc() to reverse its direction, from larger to smaller:
arrange(lifeexp, desc(lexp))# A tibble: 68 × 6
region country popgrowth lexp gnppc safewater
<hvn_lbll> <chr> <dbl> <dbl> <dbl> <dbl>
1 1 France 0.4 79 25740 97
2 1 Italy 0 79 22270 95
3 1 Spain 0.4 79 15410 99
4 1 Sweden 0.1 79 31100 99
5 1 Switzerland 0.3 79 46650 97
6 2 Canada 0.8 79 20900 99
7 1 Austria 0.1 78 27810 97
8 1 Greece 0.6 78 13540 99
9 1 Netherlands 0.6 78 27840 100
10 1 Norway 0.6 78 36120 100
# ℹ 58 more rows
This specifies an order; it does not replace life expectancy with a negative measurement. For numeric keys, you may also encounter arrange(lifeexp, -lexp), which sorts on temporary negative values but returns the original measurements. desc() states the direction directly and also works with text.
4.3 Resolve ties with additional keys
When countries share a life expectancy, sorting by that key alone keeps their previous relative order. Supply country as a second key to make the order within ties explicit:
life_high_first <- arrange(lifeexp, desc(lexp), country)
head(life_high_first)# A tibble: 6 × 6
region country popgrowth lexp gnppc safewater
<hvn_lbll> <chr> <dbl> <dbl> <dbl> <dbl>
1 2 Canada 0.8 79 20900 99
2 1 France 0.4 79 25740 97
3 1 Italy 0 79 22270 95
4 1 Spain 0.4 79 15410 99
5 1 Sweden 0.1 79 31100 99
6 1 Switzerland 0.3 79 46650 97
The highest recorded life expectancy is 79 years. Some countries share a recorded value. The second key, country, orders names within each tied life expectancy. The first key has priority: the entire dataset is not sorted alphabetically. For example, country name decides the order only after life expectancy is tied. Reversing the keys to arrange(lifeexp, country, desc(lexp)) would make country name the primary order, answering a different question. These historical measurements describe the countries and years in the source, not current rankings.
4.4 Interpret text order and missing values
A text key can also determine the entire ordering:
arrange(lifeexp, country)# A tibble: 68 × 6
region country popgrowth lexp gnppc safewater
<hvn_lbll> <chr> <dbl> <dbl> <dbl> <dbl>
1 1 Albania -0.6 74 870 49
2 3 Argentina 1.2 73 7960 NA
3 1 Armenia -1.2 70 570 83
4 1 Austria 0.1 78 27810 97
5 1 Azerbaijan 0.9 64 510 49
6 1 Belarus -0.5 68 1540 82
7 1 Belgium 0.2 77 26530 100
8 3 Bolivia 1.8 61 960 NA
9 1 Bosnia and Herzegovina 1.8 74 1230 87
10 3 Brazil 1.5 69 4980 76
# ℹ 58 more rows
Country names follow dplyr’s default C locale text order for data in memory. This is a reproducible character order, but not necessarily language-sensitive dictionary order for every writing system. The country key above follows the same rule within tied life expectancies.
Water coverage is unavailable for some countries. Missing sorting values appear after known ones, including when the known values are sorted in descending order. Tidystata’s print_headtail() previews rows from both ends, so we can inspect high known values and missing keys together. It returns the complete input table unchanged; only the display is shortened:
water_high_first <- arrange(lifeexp, desc(safewater), country)
print_headtail(water_high_first)# A tibble: 68 × 6
region country popgrowth lexp gnppc safewater
<hvn_lbll> <chr> <dbl> <dbl> <dbl> <dbl>
1 1 Belgium 0.2 77 26530 100
2 1 Germany 0 77 28070 100
3 1 Netherlands 0.6 78 27840 100
4 1 Norway 0.6 78 36120 100
5 1 United Kingdom 0.3 77 27040 100
⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮
64 2 Trinidad and Tobago 0.3 68 4450 NA
65 1 Turkiye 1.4 71 4150 NA
66 2 United States 1.2 77 32040 NA
67 3 Uruguay 0.5 74 7360 NA
68 3 Venezuela, RB 2 72 3380 NA
# ... 58 rows omitted in the middle; use print(x, n = Inf) to show all rows.
nrow(water_high_first)[1] 68
The first five displayed rows are at the high end of known water coverage. The last five have NA, not exceptionally low coverage. All 68 countries remain in the returned table. A preview from both ends is not a representative sample or a selection of just the highest values.
To inspect positions throughout the sorted table, use print_interval() from tidystata. It displays approximately evenly spaced rows, including the first and last:
print_interval(water_high_first)# A tibble: 68 × 6
region country popgrowth lexp gnppc safewater
<hvn_lbll> <chr> <dbl> <dbl> <dbl> <dbl>
1 1 Belgium 0.2 77 26530 100
8 1 Spain 0.4 79 15410 99
16 1 Finland 0.3 77 25810 94
23 1 Bulgaria -0.7 71 1440 90
31 3 Brazil 1.5 69 4980 76
38 3 Colombia 1.8 71 2600 68
46 1 Albania -0.6 74 870 49
53 1 Moldova 0 67 580 40
61 2 Nicaragua 1.5 64 890 NA
68 3 Venezuela, RB 2 72 3380 NA
# ... 58 rows omitted between displayed positions; use print(x, n = Inf) to
# show all rows.
Both helpers display ten data rows by default. print_headtail() divides them between the beginning and end; print_interval() spreads them through the table. Read the row numbers to see their positions. Here, the interval view lets us inspect water coverage between the high end and the missing values at the bottom. Evenly spaced positions need not have evenly spaced measurements.
Choose the view that fits the question: head/tail for boundaries, interval for coverage across the current order. Both helpers return the complete input table unchanged and invisibly, so R prints the preview without automatically printing the returned table again. They do not select a subset for later calculations. Section 7.7.3 shows why interval previews are also useful when nearby rows belong to the same place.
4.5 Review
c()stores quoted names;all_of()requires them andany_of()tolerates absent optional names.where()applies a column predicate such asis.numeric(); numeric storage alone does not justify a quantitative interpretation.arrange()sorts whole records while keeping each observation’s values together.desc()reverses a sorting key; missing keys still follow known values in these local tables.- Additional sorting keys resolve ties, with priority from left to right.
print_headtail()displays both ends;print_interval()displays positions throughout the ordered table. Both return the complete table.<-saves the sorted result; sorting alone neither replaces the source nor removes observations.
Next, Chapter 5 connects these operations in reading order.
4.6 Exercises
Store
"country","lexp", and"gnppc"asranking_names, then prepare a table usingall_of(). Sort it by ascending life expectancy, breaking ties by country name. Save the result and preview its first rows. Explain why all measurements must move with the country name.Keep
country,gnppc, andlexpin a new table. Sort that table by descending income, with country name as a second key. Use separate named steps. Predict what changes ifcountrybecomes the first sorting key. Compare head/tail and interval previews. Which helps locate missing income, and which shows more of the interior of the income ordering?A collaborator supplies
optional_names <- c("safewater", "income"). Prepare a table withcountry,lexp, andgnppcrequired, plus any available optional names. Predict its columns. Explain why putting every name insideany_of()is unsuitable when the ranking needsgnppcandcountry.Predict the difference between
is.numeric(25)andis.numeric("25"). Selectcountryand all numeric columns fromlifeexp, then check its names and row count. Does includingregionestablish that its codes are quantities? Does the selection remove missing water measurements? Describe where those missing keys appear after descending water sorting.
Worked answers
Run the chapter package setup first.
ranking_names <- c("country", "lexp", "gnppc")
ranking_table <- select(lifeexp, all_of(ranking_names))
life_order <- arrange(ranking_table, lexp, country)
head(life_order)# A tibble: 6 × 3
country lexp gnppc
<chr> <dbl> <dbl>
1 Haiti 57 410
2 Bolivia 61 960
3 Honduras 62 960
4 Tajikistan 62 170
5 Kazakhstan 63 1340
6 Azerbaijan 64 510
Life expectancy has first priority, and country names resolve ties. Previewing the first six rows does not change the stored 68-row table. Moving complete records keeps each country attached to its income and life expectancy; sorting one column independently would break those connections.
income_view <- select(lifeexp, country, gnppc, lexp)
income_order <- arrange(income_view, desc(gnppc), country)
print_headtail(income_order)# A tibble: 68 × 3
country gnppc lexp
<chr> <dbl> <dbl>
1 Switzerland 46650 79
2 Norway 36120 78
3 Denmark 33440 76
4 United States 32040 77
5 Sweden 31100 79
⋮ ⋮ ⋮ ⋮
64 Azerbaijan 510 64
65 Haiti 410 57
66 Kyrgyz Republic 360 67
67 Tajikistan 170 62
68 Croatia NA 72
# ... 58 rows omitted in the middle; use print(x, n = Inf) to show all rows.
print_interval(income_order)# A tibble: 68 × 3
country gnppc lexp
<chr> <dbl> <dbl>
1 Switzerland 46650 79
8 Austria 27810 78
16 Spain 15410 79
23 Czechia 6140 75
31 Turkiye 4150 71
38 Latvia 2790 69
46 Paraguay 1800 70
53 Bosnia and Herzegovina 1230 74
61 Moldova 580 67
68 Croatia NA 72
# ... 58 rows omitted between displayed positions; use print(x, n = Inf) to
# show all rows.
Making country the first key would order countries by name, rather than place the highest incomes first. The country with unavailable income appears last. The head/tail view makes the two ends easy to compare; the interval view also reaches positions throughout the income ordering. Its spacing refers to rows, not equal dollar differences. Each country’s life expectancy remains attached to it, and both previews retain the complete table. Neither selection nor sorting modifies lifeexp.
optional_names <- c("safewater", "income")
optional_view <- select(lifeexp, country, lexp, gnppc, any_of(optional_names))
ds(optional_view)[1] "country" "lexp" "gnppc" "safewater"
The order is country, lexp, gnppc, safewater. No column is called income, so that optional name is omitted. Explicitly required names fail immediately if absent. Putting a needed key inside any_of() would let selection succeed without it; the later sorting call could then fail.
is.numeric(25)[1] TRUE
is.numeric("25")[1] FALSE
numeric_view <- select(lifeexp, country, where(is.numeric))
ds(numeric_view)[1] "country" "region" "popgrowth" "lexp" "gnppc" "safewater"
nrow(numeric_view)[1] 68
water_order <- arrange(numeric_view, desc(safewater), country)
print_headtail(water_order)# A tibble: 68 × 6
country region popgrowth lexp gnppc safewater
<chr> <hvn_lbll> <dbl> <dbl> <dbl> <dbl>
1 Belgium 1 0.2 77 26530 100
2 Germany 1 0 77 28070 100
3 Netherlands 1 0.6 78 27840 100
4 Norway 1 0.6 78 36120 100
5 United Kingdom 1 0.3 77 27040 100
⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮
64 Trinidad and Tobago 2 0.3 68 4450 NA
65 Turkiye 1 1.4 71 4150 NA
66 United States 2 1.2 77 32040 NA
67 Uruguay 3 0.5 74 7360 NA
68 Venezuela, RB 3 2 72 3380 NA
# ... 58 rows omitted in the middle; use print(x, n = Inf) to show all rows.
The tests return TRUE and FALSE. All six original columns remain, with country moved first, and all 68 countries remain. region has numeric storage but records categories; its inclusion does not make a numerical ranking substantively meaningful. Missing water values are retained and appear after the known values even in descending order. The preview’s bottom rows therefore do not represent the countries with the lowest known coverage.