library(tidyverse)
library(tidystata)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:
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(), andcontains()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;.beforeand.afterspecify their destination.ds(),nrow(), andncol()check the result; use<-to save it without replacing the source.
Next, Chapter 4 changes the order of whole rows.
3.8 Exercises
Save a table named
income_viewcontainingcountry,gnppc, andlexp, in that order. Check its row and column counts.Move
lexpimmediately aftercountryin the full dataset, saving the result aslife_first. Predict its column order before checking it. Explain why this differs from selecting only these two columns. Then predictregion:popgrowthafter movingcountryto the end, as in Section 3.6.Select
region:popgrowth, then select everything except that range. Predict the columns and row counts in both results.Predict the names selected by
contains("p")andends_with("water"), then run both calls. Diagnose the difference betweenselect(lifeexp, income)andselect(lifeexp, starts_with("income")). Which error or header would alert you that neither request supplies the recorded income variable? Correct the selection to keepcountryand 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.