3  Select columns

Suppose we want to compare countries’ life expectancy and water coverage. The source has six variables, but we can make a more convenient table containing the three columns we need. Selecting columns keeps the same observations; moving columns changes their reading order.

By the end of this chapter, you will be able to select columns by name, position, or name pattern; move them with relocate(); and save a result while preserving its source. We will use lifeexp, a country dataset included with tidystata. Load the packages in your current session:

library(tidyverse)
library(tidystata)

Typing the dataset name prints the table, just as in Chapter 2:

lifeexp
# 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          1 Armenia                     -1.2    70   570        83
 3          1 Austria                      0.1    78 27810        97
 4          1 Azerbaijan                   0.9    64   510        49
 5          1 Belarus                     -0.5    68  1540        82
 6          1 Belgium                      0.2    77 26530       100
 7          1 Bosnia and Herzegovina       1.8    74  1230        87
 8          1 Bulgaria                    -0.7    71  1440        90
 9          1 Croatia                     -0.1    72    NA        85
10          1 Czechia                     -0.1    75  6140        95
# ℹ 58 more rows

The header reports 68 countries and six variables. Each row describes one country, and each column records one attribute. country identifies countries; region contains labelled region codes; popgrowth measures annual population growth in percent; lexp is life expectancy at birth in years; gnppc is income per person in current US dollars; and safewater is safe-water access in percent. NA marks an unavailable measurement. Most indicators refer to 1998; safe water refers to 2000. The dataset notes document the sources and units.

We can begin by choosing columns from this table. Later, Chapter 7 examines its individual records and variable descriptions more closely.

3.1 Keep the columns you need

select() takes the data first, followed by the columns to retain. In this function, write column names directly, without quotation marks. Save the result as country_view, then type its name to inspect it:

country_view <- select(lifeexp, country, lexp, safewater)
country_view
# A tibble: 68 × 3
   country                 lexp safewater
   <chr>                  <dbl>     <dbl>
 1 Albania                   74        49
 2 Armenia                   70        83
 3 Austria                   78        97
 4 Azerbaijan                64        49
 5 Belarus                   68        82
 6 Belgium                   77       100
 7 Bosnia and Herzegovina    74        87
 8 Bulgaria                  71        90
 9 Croatia                   72        85
10 Czechia                   75        95
# ℹ 58 more rows

country_view contains all 68 countries and three columns, in the order requested. Values, missing measurements, and row order are unchanged. <- stores this result under a new name. lifeexp still has all six columns. Selection does not erase columns from the original object.

The first argument is a table name; the remaining arguments identify its columns. country identifies observations, lexp measures life expectancy in years, and safewater measures water coverage in percent. Choosing fewer variables is different from choosing fewer countries.

We can check the number of observations directly. nrow() takes a table and returns its row count:

nrow(country_view)
[1] 68

The result is 68: all countries remain in the selected table. To count its columns, use ncol():

ncol(country_view)
[1] 3

The result is 3, matching the three names supplied to select(). These counting functions leave the table unchanged.

We can request a different reading order simply by changing the order of the selected names:

select(lifeexp, country, safewater, lexp)
# A tibble: 68 × 3
   country                safewater  lexp
   <chr>                      <dbl> <dbl>
 1 Albania                       49    74
 2 Armenia                       83    70
 3 Austria                       97    78
 4 Azerbaijan                    49    64
 5 Belarus                       82    68
 6 Belgium                      100    77
 7 Bosnia and Herzegovina        87    74
 8 Bulgaria                      90    71
 9 Croatia                       85    72
10 Czechia                       95    75
# ℹ 58 more rows

Water coverage now precedes life expectancy. This command prints a result but does not replace country_view or lifeexp. Assign a result when you want to use it again.

3.2 Check column names

Sometimes we want to check the column names without reading the country records. Tidystata’s ds() takes a table and returns its column names as text, in their current order:

ds(lifeexp)
[1] "region"    "country"   "popgrowth" "lexp"      "gnppc"     "safewater"

The six names follow the order in the original table. Quotation marks show that the result contains text. select() returns a table containing data; ds() returns the names of its columns. Neither call changes the source unless we assign its result back to that name. We will reuse ds() to check column order below.

3.3 Tidyselect: ways to choose columns

Tidyselect is the shared selection language used inside select(), relocate(), and other functions that choose columns. It combines column names, operators, and helper functions. You do not need another package-loading command: the tidyverse setup above makes these helpers available.

We will first use : for a consecutive range and - to exclude columns. Then we will work through three helpers for matching text in column names, one at a time. Each method identifies columns; it does not select observations.

3.4 Select ranges and exclude columns

Whole-number positions identify columns in their current order. A range made with : includes both endpoints and every column between them:

select(lifeexp, 1:3)
# A tibble: 68 × 3
       region country                popgrowth
   <hvn_lbll> <chr>                      <dbl>
 1          1 Albania                     -0.6
 2          1 Armenia                     -1.2
 3          1 Austria                      0.1
 4          1 Azerbaijan                   0.9
 5          1 Belarus                     -0.5
 6          1 Belgium                      0.2
 7          1 Bosnia and Herzegovina       1.8
 8          1 Bulgaria                    -0.7
 9          1 Croatia                     -0.1
10          1 Czechia                     -0.1
# ℹ 58 more rows
select(lifeexp, region:popgrowth)
# A tibble: 68 × 3
       region country                popgrowth
   <hvn_lbll> <chr>                      <dbl>
 1          1 Albania                     -0.6
 2          1 Armenia                     -1.2
 3          1 Austria                      0.1
 4          1 Azerbaijan                   0.9
 5          1 Belarus                     -0.5
 6          1 Belgium                      0.2
 7          1 Bosnia and Herzegovina       1.8
 8          1 Bulgaria                    -0.7
 9          1 Croatia                     -0.1
10          1 Czechia                     -0.1
# ℹ 58 more rows

Both results contain region, country, and popgrowth. A name range follows the table’s current order, not alphabetical order. Names are usually easier to check against a codebook than positions.

Separate arguments combine selections. Here we keep the country identifier and the range from life expectancy through water coverage:

select(lifeexp, country, lexp:safewater)
# 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

The result has country, lexp, gnppc, and safewater, with all 68 rows. A minus sign before a selection excludes those columns; it does not make their values negative:

select(lifeexp, -region)
# A tibble: 68 × 5
   country                popgrowth  lexp gnppc safewater
   <chr>                      <dbl> <dbl> <dbl>     <dbl>
 1 Albania                     -0.6    74   870        49
 2 Armenia                     -1.2    70   570        83
 3 Austria                      0.1    78 27810        97
 4 Azerbaijan                   0.9    64   510        49
 5 Belarus                     -0.5    68  1540        82
 6 Belgium                      0.2    77 26530       100
 7 Bosnia and Herzegovina       1.8    74  1230        87
 8 Bulgaria                    -0.7    71  1440        90
 9 Croatia                     -0.1    72    NA        85
10 Czechia                     -0.1    75  6140        95
# ℹ 58 more rows
select(lifeexp, -region, -popgrowth)
# 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

The first result omits the regional code; the second also omits population growth. All countries and the missing values in retained columns remain. Give each excluded column its own minus sign as shown.

To exclude a whole consecutive range, group it in parentheses and put the minus sign before the group:

select(lifeexp, -(region:popgrowth))
# A tibble: 68 × 3
    lexp gnppc safewater
   <dbl> <dbl>     <dbl>
 1    74   870        49
 2    70   570        83
 3    78 27810        97
 4    64   510        49
 5    68  1540        82
 6    77 26530       100
 7    74  1230        87
 8    71  1440        90
 9    72    NA        85
10    75  6140        95
# ℹ 58 more rows

The parentheses make region:popgrowth one selection to exclude. The result keeps lexp, gnppc, and safewater, still with 68 countries. The range includes country, so this particular demonstration also removes the identifier.

3.5 Select by name patterns

When column names share some text, a helper can spare us from typing each name. starts_with() matches text at the beginning of a column name. Supply that text in quotation marks inside the helper:

select(lifeexp, starts_with("pop"))
# A tibble: 68 × 1
   popgrowth
       <dbl>
 1      -0.6
 2      -1.2
 3       0.1
 4       0.9
 5      -0.5
 6       0.2
 7       1.8
 8      -0.7
 9      -0.1
10      -0.1
# ℹ 58 more rows

This selects popgrowth, whose name begins with pop. The result still contains all 68 countries.

To match the end of a name, use ends_with():

select(lifeexp, ends_with("water"))
# A tibble: 68 × 1
   safewater
       <dbl>
 1        49
 2        83
 3        97
 4        49
 5        82
 6       100
 7        87
 8        90
 9        85
10        95
# ℹ 58 more rows

This selects safewater, whose name ends in water. Again, only the set of columns changes.

To match text anywhere in a name, use contains():

select(lifeexp, contains("p"))
# A tibble: 68 × 3
   popgrowth  lexp gnppc
       <dbl> <dbl> <dbl>
 1      -0.6    74   870
 2      -1.2    70   570
 3       0.1    78 27810
 4       0.9    64   510
 5      -0.5    68  1540
 6       0.2    77 26530
 7       1.8    74  1230
 8      -0.7    71  1440
 9      -0.1    72    NA
10      -0.1    75  6140
# ℹ 58 more rows

This selects popgrowth, lexp, and gnppc, because each name contains the letter p. All 68 countries remain. The three helpers ignore letter case by default. They match spelling, not meaning: the three names containing p measure different things.

We can combine a named identifier with a helper, or exclude the helper’s selection:

select(lifeexp, country, starts_with("pop"))
# A tibble: 68 × 2
   country                popgrowth
   <chr>                      <dbl>
 1 Albania                     -0.6
 2 Armenia                     -1.2
 3 Austria                      0.1
 4 Azerbaijan                   0.9
 5 Belarus                     -0.5
 6 Belgium                      0.2
 7 Bosnia and Herzegovina       1.8
 8 Bulgaria                    -0.7
 9 Croatia                     -0.1
10 Czechia                     -0.1
# ℹ 58 more rows
select(lifeexp, -contains("p"))
# A tibble: 68 × 3
       region country                safewater
   <hvn_lbll> <chr>                      <dbl>
 1          1 Albania                       49
 2          1 Armenia                       83
 3          1 Austria                       97
 4          1 Azerbaijan                    49
 5          1 Belarus                       82
 6          1 Belgium                      100
 7          1 Bosnia and Herzegovina        87
 8          1 Bulgaria                      90
 9          1 Croatia                       85
10          1 Czechia                       95
# ℹ 58 more rows

The first table pairs country names with population growth. The second keeps region, country, and safewater. The minus sign applies to all columns identified by the helper. Neither call changes lifeexp.

An exact name is a request for a particular variable. If we write select(lifeexp, income), R reports that income does not exist; the income column here is gnppc. A pattern can legitimately match no names. For example:

select(lifeexp, starts_with("income"))
# A tibble: 68 × 0

This returns a table with 68 rows and zero columns, without an error. No names begin with income; the helper does not know that we meant income per person. A successful call can therefore still be unsuitable for our question. Check the header and the selected names, then correct the spelling or selection rule.

A shared spelling need not identify a shared measurement. Check the selected names before interpreting the result; choosing columns is a research decision as well as a coding operation.

3.6 Move a column without dropping others

relocate() also takes the table first, but moves named columns while keeping the rest. Its default places the selected column at the front. Use the familiar ds() check to compare the result with the original:

country_front <- relocate(lifeexp, safewater)
ds(country_front)
[1] "safewater" "region"    "country"   "popgrowth" "lexp"      "gnppc"    
ds(lifeexp)
[1] "region"    "country"   "popgrowth" "lexp"      "gnppc"     "safewater"

All six columns remain. Water coverage is first in country_front; the original names show that lifeexp has not changed. No country has moved to another row.

Use .after or .before to choose a more precise location. These are argument names, including their leading dots:

country_reading <- relocate(lifeexp, safewater, .after = country)
ds(country_reading)
[1] "region"    "country"   "safewater" "popgrowth" "lexp"      "gnppc"    
relocate(country_view, safewater, .before = lexp)
# A tibble: 68 × 3
   country                safewater  lexp
   <chr>                      <dbl> <dbl>
 1 Albania                       49    74
 2 Armenia                       83    70
 3 Austria                       97    78
 4 Azerbaijan                    49    64
 5 Belarus                       82    68
 6 Belgium                      100    77
 7 Bosnia and Herzegovina        87    74
 8 Bulgaria                      90    71
 9 Croatia                       85    72
10 Czechia                       95    75
# ℹ 58 more rows

The first result puts water coverage immediately after the country name. The second puts it immediately before life expectancy in the smaller table. = supplies a named argument here; <- stores a returned result.

Multiple names in relocate() move together: relocate(lifeexp, country, lexp) puts those two columns first and keeps the others in their relative order. Ranges depend on column order:

country_reordered <- relocate(lifeexp, country, .after = safewater)
select(country_reordered, region:popgrowth)
# A tibble: 68 × 2
       region popgrowth
   <hvn_lbll>     <dbl>
 1          1      -0.6
 2          1      -1.2
 3          1       0.1
 4          1       0.9
 5          1      -0.5
 6          1       0.2
 7          1       1.8
 8          1      -0.7
 9          1      -0.1
10          1      -0.1
# ℹ 58 more rows

Now only region and popgrowth lie in that range. The country names still exist in country_reordered, but at the end. Check the column order before relying on a range.

3.7 Review

  • select() keeps columns by name, range, or position; exclusions remove columns without removing observations.
  • starts_with(), ends_with(), and contains() match literal parts of column names.
  • Name patterns match spelling rather than meaning; check that the selected columns answer the research question.
  • relocate() moves columns while retaining the others; .before and .after specify their destination.
  • ds(), nrow(), and ncol() check the result; use <- to save it without replacing the source.

Next, Chapter 4 changes the order of whole rows.

3.8 Exercises

  1. Save a table named income_view containing country, gnppc, and lexp, in that order. Check its row and column counts.

  2. Move lexp immediately after country in the full dataset, saving the result as life_first. Predict its column order before checking it. Explain why this differs from selecting only these two columns. Then predict region:popgrowth after moving country to the end, as in Section 3.6.

  3. Select region:popgrowth, then select everything except that range. Predict the columns and row counts in both results.

  4. Predict the names selected by contains("p") and ends_with("water"), then run both calls. Diagnose the difference between select(lifeexp, income) and select(lifeexp, starts_with("income")). Which error or header would alert you that neither request supplies the recorded income variable? Correct the selection to keep country and income per person.

Worked answers

Run the chapter package setup first.

income_view <- select(lifeexp, country, gnppc, lexp)
income_view
# A tibble: 68 × 3
   country                gnppc  lexp
   <chr>                  <dbl> <dbl>
 1 Albania                  870    74
 2 Armenia                  570    70
 3 Austria                27810    78
 4 Azerbaijan               510    64
 5 Belarus                 1540    68
 6 Belgium                26530    77
 7 Bosnia and Herzegovina  1230    74
 8 Bulgaria                1440    71
 9 Croatia                   NA    72
10 Czechia                 6140    75
# ℹ 58 more rows
nrow(income_view)
[1] 68
ncol(income_view)
[1] 3

The result has 68 rows and three columns. The missing income measurement remains missing; selecting columns does not select only complete observations.

life_first <- relocate(lifeexp, lexp, .after = country)
ds(life_first)
[1] "region"    "country"   "lexp"      "popgrowth" "gnppc"     "safewater"
ncol(life_first)
[1] 6

The order is region, country, lexp, popgrowth, gnppc, safewater. All six columns remain. select(lifeexp, country, lexp) would instead return only two. Both retain all 68 rows and leave lifeexp unchanged. After moving country to the end of the original table, the range region:popgrowth contains only region and popgrowth; ranges follow the current order.

select(lifeexp, region:popgrowth)
# A tibble: 68 × 3
       region country                popgrowth
   <hvn_lbll> <chr>                      <dbl>
 1          1 Albania                     -0.6
 2          1 Armenia                     -1.2
 3          1 Austria                      0.1
 4          1 Azerbaijan                   0.9
 5          1 Belarus                     -0.5
 6          1 Belgium                      0.2
 7          1 Bosnia and Herzegovina       1.8
 8          1 Bulgaria                    -0.7
 9          1 Croatia                     -0.1
10          1 Czechia                     -0.1
# ℹ 58 more rows
select(lifeexp, -(region:popgrowth))
# A tibble: 68 × 3
    lexp gnppc safewater
   <dbl> <dbl>     <dbl>
 1    74   870        49
 2    70   570        83
 3    78 27810        97
 4    64   510        49
 5    68  1540        82
 6    77 26530       100
 7    74  1230        87
 8    71  1440        90
 9    72    NA        85
10    75  6140        95
# ℹ 58 more rows

The first table contains region, country, and popgrowth; the second contains lexp, gnppc, and safewater. Both retain all 68 countries. The minus sign excludes columns, not observations or negative measurements.

select(lifeexp, contains("p"))
# A tibble: 68 × 3
   popgrowth  lexp gnppc
       <dbl> <dbl> <dbl>
 1      -0.6    74   870
 2      -1.2    70   570
 3       0.1    78 27810
 4       0.9    64   510
 5      -0.5    68  1540
 6       0.2    77 26530
 7       1.8    74  1230
 8      -0.7    71  1440
 9      -0.1    72    NA
10      -0.1    75  6140
# ℹ 58 more rows
select(lifeexp, ends_with("water"))
# A tibble: 68 × 1
   safewater
       <dbl>
 1        49
 2        83
 3        97
 4        49
 5        82
 6       100
 7        87
 8        90
 9        85
10        95
# ℹ 58 more rows

The first result contains popgrowth, lexp, and gnppc; the second contains safewater. These rules inspect column names, not individual country values. All 68 countries remain in each result, including those with missing measurements. The exact request for income fails because that name is absent. The pattern request succeeds but its header reports zero columns: no name begins with income. Correct the selection using the recorded name:

select(lifeexp, country, gnppc)
# A tibble: 68 × 2
   country                gnppc
   <chr>                  <dbl>
 1 Albania                  870
 2 Armenia                  570
 3 Austria                27810
 4 Azerbaijan               510
 5 Belarus                 1540
 6 Belgium                26530
 7 Bosnia and Herzegovina  1230
 8 Bulgaria                1440
 9 Croatia                   NA
10 Czechia                 6140
# ℹ 58 more rows

This keeps all 68 countries and the two requested columns, in that order.