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:

library(tidyverse)
library(tidystata)

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 and any_of() tolerates absent optional names.
  • where() applies a column predicate such as is.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

  1. Store "country", "lexp", and "gnppc" as ranking_names, then prepare a table using all_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.

  2. Keep country, gnppc, and lexp in a new table. Sort that table by descending income, with country name as a second key. Use separate named steps. Predict what changes if country becomes 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?

  3. A collaborator supplies optional_names <- c("safewater", "income"). Prepare a table with country, lexp, and gnppc required, plus any available optional names. Predict its columns. Explain why putting every name inside any_of() is unsuitable when the ranking needs gnppc and country.

  4. Predict the difference between is.numeric(25) and is.numeric("25"). Select country and all numeric columns from lifeexp, then check its names and row count. Does including region establish 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.