# Base R only: nothing to load.Data types and operators
Numbers, text and TRUE/FALSE — and the surprises hidden in between
By the end of this lesson you will be able to:
- recognise the four basic data types and inspect them with
class(),typeof()andis.*(); - convert between types with
as.*()and predict what implicit coercion will do; - use arithmetic (including
%/%and%%), comparison and logical operators; - distinguish
NA,NULL,NaNandInf; - compare decimal numbers safely with
all.equal().
1. Why do data types matter?
Look at these two lines:
10 < 9#> [1] FALSE
"10" < "9"#> [1] TRUE
The first comparison is FALSE, as expected. The second is TRUE! With quotes, "10" and "9" are text, and text is compared alphabetically, character by character: "1" comes before "9", just as “apple” comes before “banana”.
This is not a curiosity. When you import a spreadsheet and one cell in a column of numbers contains "n/a", R may read the whole column as text. Your averages fail, your sorting is wrong, your plots look odd. Knowing what type each object has — and what R does when types are mixed — prevents a whole family of bugs.
2. The basic types
Every value in R has a type. Four are used every day:
| Type | Example | What it stores |
|---|---|---|
| numeric (double) | 4.5, -3, 1e6 |
Real numbers, with decimals. The default for numbers. |
| integer | 4L, 1:10 |
Whole numbers. The L suffix makes an integer. |
| character | "Adelie", 'R is fun' |
Text (strings), always in quotes, single or double. |
| logical | TRUE, FALSE |
Yes/no values (Booleans). Always in capitals, without quotes. |
A fifth type, complex (e.g. 2+3i), stores complex numbers; you are unlikely to need it in data analysis.
x <- 28
y <- "R is fantastic"
z <- TRUE
w <- 4L
class(x)#> [1] "numeric"
class(y)#> [1] "character"
class(z)#> [1] "logical"
class(w)#> [1] "integer"
class() vs typeof()
Two functions describe an object:
class()answers “what kind of object is this, for the user?” It is what decides how the object is printed, summarised and plotted.typeof()answers “how is it stored in memory?”
For simple values they mostly agree. The main difference you will notice is that ordinary numbers have class "numeric" but type "double" (short for double-precision floating point):
typeof(28)#> [1] "double"
typeof(28L)#> [1] "integer"
class(penguins$species) # a factor: see the lesson on factors#> [1] "factor"
typeof(penguins$species) # ...stored internally as integers#> [1] "integer"
You will meet the second pair again in Factors.
Testing and converting: is.*() and as.*()
Every type has a pair of functions: is.<type>() asks “is it?”, and as.<type>() says “make it so”.
is.numeric(28)#> [1] TRUE
is.character(28)#> [1] FALSE
as.numeric("3.14")#> [1] 3.14
as.character(42)#> [1] "42"
as.integer(3.9) # truncates towards zero, does not round!#> [1] 3
as.logical(c("TRUE", "false", "T", "yes"))#> [1] TRUE FALSE TRUE NA
Note two things: as.integer(3.9) gives 3, not 4; and "yes" cannot be turned into a logical, so R returns NA (missing). When a conversion fails, R does not stop — it returns NA and usually a warning:
as.numeric(c("12", "7.5", "n/a"))#> Warning: NA introdotti per coercizione
#> [1] 12.0 7.5 NA
This is exactly what happens to the spreadsheet column with an "n/a" cell from Section 1.
3. Implicit coercion: mixing types
A vector (a sequence of values, created with c() — more in Vectors) can hold only one type. If you mix types, R silently converts everything to the most “flexible” type. This is called implicit coercion, and it always follows the same hierarchy:
\[ \texttt{logical} \;\rightarrow\; \texttt{integer} \;\rightarrow\; \texttt{double} \;\rightarrow\; \texttt{character} \]
Anything can be written as text, but not all text can be a number; every logical can be a number (TRUE = 1, FALSE = 0), but not every number is TRUE/FALSE. So R always moves to the right.
c(1, TRUE, FALSE) # logical -> double#> [1] 1 1 0
c(1L, 2.5) # integer -> double#> [1] 1.0 2.5
c(1, "a", TRUE) # everything -> character#> [1] "1" "a" "TRUE"
The fact that TRUE becomes 1 and FALSE becomes 0 is very useful: the sum of a logical vector counts the TRUEs, and its mean is the proportion of TRUEs.
answers <- c(TRUE, FALSE, TRUE, TRUE)
sum(answers)#> [1] 3
mean(answers)#> [1] 0.75
Computers store real numbers in binary floating point. A double uses 64 bits: about 15–16 significant decimal digits, and numbers up to roughly \(1.8 \times 10^{308}\). An integer uses 32 bits and can only hold whole numbers up to 2,147,483,647 (i.e. \(2^{31} - 1\)). Because doubles cover a far wider range, R uses them by default; integers appear when you write 5L, use 1:10, or count things (nrow(), length()).
4. Arithmetic operators
| Operator | Meaning | Example | Result |
|---|---|---|---|
+ - |
addition, subtraction | 5 - 2 |
3 |
* / |
multiplication, division | 7 / 2 |
3.5 |
^ |
power | 2^5 |
32 |
%/% |
integer division (how many whole times?) | 17 %/% 5 |
3 |
%% |
modulo (remainder) | 17 %% 5 |
2 |
(5 + 5) / 2#> [1] 5
2^5#> [1] 32
17 %/% 5#> [1] 3
17 %% 5#> [1] 2
Read 17 %/% 5 and 17 %% 5 together: “17 is 5 three times, with remainder two”, because \(17 = 5 \times 3 + 2\). The modulo is handy in practice: x %% 2 == 0 tests whether x is even, and minutes %/% 60 and minutes %% 60 split a duration into hours and minutes.
R follows the usual order of operations: ^ first, then * and /, then + and -. When in doubt, add parentheses.
5. Comparison operators
Comparisons return a logical value.
| Operator | Meaning |
|---|---|
<, <= |
less than, less than or equal to |
>, >= |
greater than, greater than or equal to |
== |
equal to (two equals signs!) |
!= |
not equal to |
x <- 1:10
x > 5#> [1] FALSE FALSE FALSE FALSE FALSE TRUE TRUE TRUE TRUE TRUE
R compares each element of x with 5 and returns one TRUE/FALSE per element. Put that logical vector inside square brackets to keep only the elements where it is TRUE:
x[x > 5]#> [1] 6 7 8 9 10
This idea — select with a logical condition — is the basis of all data filtering in R. We will develop it in Vectors.
6. Logical operators
| Operator | Meaning |
|---|---|
!x |
NOT: flips TRUE and FALSE |
x & y |
AND, element by element: TRUE only if both are TRUE |
x \| y |
OR, element by element: TRUE if at least one is TRUE |
xor(x, y) |
exclusive OR: TRUE if exactly one is TRUE |
x && y, x \|\| y |
AND / OR for a single TRUE/FALSE value |
isTRUE(x) |
TRUE only if x is a single, non-missing TRUE |
x[(x > 4) & (x < 7)]#> [1] 5 6
x[x < 3 | x > 8]#> [1] 1 2 9 10
!c(TRUE, FALSE)#> [1] FALSE TRUE
& versus &&
&and|are vectorised: they work element by element on whole vectors. Use them to filter data.&&and||expect exactly one value on each side and return one value. Use them inif ()conditions (Conditionals). Since R 4.3, giving them a vector longer than one is an error:
c(TRUE, FALSE) & c(TRUE, TRUE) # vectorised: two answers#> [1] TRUE FALSE
c(TRUE, FALSE) && TRUE # error: && needs a single value#> Error in `c(TRUE, FALSE) && TRUE`:
#> ! 'length = 2' nella coercizione in 'logical(1)'
&& and || also short-circuit: if the left side already decides the answer (FALSE && …), the right side is not evaluated at all.
7. Special values: NA, NULL, NaN, Inf
| Value | Meaning | Example | length() |
|---|---|---|---|
NA |
Not Available: a value exists but is unknown (missing) | a penguin that was not weighed | 1 |
NULL |
nothing: the absence of an object | c() |
0 |
NaN |
Not a Number: an undefined calculation | 0 / 0 |
1 |
Inf, -Inf |
plus/minus infinity | 1 / 0 |
1 |
0 / 0#> [1] NaN
1 / 0#> [1] Inf
c(1, NA, 3) # NA keeps its place#> [1] 1 NA 3
c(1, NULL, 3) # NULL disappears#> [1] 1 3
NA is “contagious”: almost any calculation that involves an unknown value gives an unknown result.
NA + 1#> [1] NA
mean(c(4, NA, 6))#> [1] NA
mean(c(4, NA, 6), na.rm = TRUE)#> [1] 5
NA == NA#> [1] NA
NA == NA is NA, not TRUE: if two penguins have unknown weights, we do not know whether they weigh the same. For the same reason you cannot test for missing values with x == NA. Use is.na():
v <- c(4, NA, 6, NaN)
is.na(v) # TRUE for both NA and NaN#> [1] FALSE TRUE FALSE TRUE
is.nan(v) # TRUE only for NaN#> [1] FALSE FALSE FALSE TRUE
There are a few exceptions to the “contagious” rule, which follow from logic: NA & FALSE is FALSE (whatever the unknown value, AND with FALSE is FALSE) and NA | TRUE is TRUE.
8. The floating-point trap
Try this:
0.1 + 0.2 == 0.3#> [1] FALSE
FALSE! R is not broken: computers store numbers in binary, and 0.1 has no exact binary representation, just as 1/3 has no exact decimal one (0.333…). Printing more digits reveals the tiny error:
print(0.1 + 0.2, digits = 17)#> [1] 0.30000000000000004
The rule: never use == to compare decimal numbers that come from calculations. Use all.equal(), which allows a tiny tolerance (about \(1.5 \times 10^{-8}\) by default):
all.equal(0.1 + 0.2, 0.3)#> [1] TRUE
all.equal(1, 1.1)#> [1] "Mean relative difference: 0.1"
isTRUE(all.equal(1, 1.1))#> [1] FALSE
all.equal() returns TRUE when the numbers are equal up to the tolerance, but when they differ it returns a description of the difference ("Mean relative difference: 0.1"), not FALSE. That text would break an if () statement, so wrap it in isTRUE() whenever you need a strict TRUE/FALSE answer: if (isTRUE(all.equal(a, b))) ....
- Numbers stored as text.
"10" < "9"isTRUEandmean("5")fails. Check withclass()and convert withas.numeric()— and look for theNAs introduced by coercionwarning, which marks the values that could not be converted. - Using
=instead of==.x = 5assigns;x == 5compares. - Testing missingness with
x == NA. It always returnsNA. Useis.na(x). - Comparing decimals with
==. UseisTRUE(all.equal(a, b)). - Using
&&on vectors. It is an error in modern R. Use&to filter vectors and&&only for single values inif (). - Expecting
as.integer()to round. It truncates:as.integer(3.9)is 3. Useround()first if you want rounding.
9. Check your understanding
10. Exercises
Interactive (run in your browser)
★ Exercise 1 — predict the output. Before running the code, write down (in a comment) what type you expect each vector to have. Then run it and check with class().
Apply the hierarchy logical → integer → double → character. NA on its own is logical, so it adapts to whatever else is in the vector.
class(c(1, "a", TRUE)) # "character"
class(c(1, TRUE, FALSE)) # "numeric"
class(c(1L, 2L, 3.5)) # "numeric"
class(c(TRUE, NA)) # "logical"
class(c("1", NA)) # "character"NA never forces a conversion: it takes the type of the other values.
★★ Exercise 2 — integer division and modulo. A film lasts 135 minutes. Use %/% and %% to compute how many full hours and how many extra minutes that is. Then use %% to check which of the numbers 1 to 10 are even.
%/% gives the whole number of times; %% gives the remainder. An even number has remainder 0 when divided by 2.
minutes %/% 60 # 2
minutes %% 60 # 15
n[n %% 2 == 0] # 2 4 6 8 10135 minutes are 2 hours and 15 minutes.
★★ Exercise 3 — counting with logicals. Using penguins, compute (a) how many penguins have a flipper length of at least 210 mm, and (b) what proportion of the penguins with a known flipper length that is.
TRUE counts as 1 and FALSE as 0. Which function adds them up, and which one averages them?
sum(long, na.rm = TRUE)
mean(long, na.rm = TRUE)About 114 penguins, roughly one third (33%) of those with a recorded flipper length. Without na.rm = TRUE both results would be NA, because two birds have no flipper measurement.
★★★ Exercise 4 — debug the comparison. The code below should report that the computed value equals 0.3, but it prints FALSE. Fix the comparison so that it returns TRUE, and make sure the result is a single TRUE/FALSE even when the numbers differ.
Use all.equal(), and wrap it in isTRUE() so that a difference gives FALSE instead of a message.
total <- 0.1 + 0.2
isTRUE(all.equal(total, 0.3)) # TRUE
isTRUE(all.equal(1, 1.1)) # FALSEDecimal numbers produced by arithmetic carry tiny rounding errors; all.equal() ignores differences smaller than about \(1.5 \times 10^{-8}\).
Practice on your own computer
★★ Exercise 5 — cleaning a column read as text. A colleague sends you weights typed by hand: w <- c("3750", "3800", "n/a", "3250", "unknown", "3450"). (a) What is class(w)? (b) Convert it to numbers. (c) How many values could not be converted? (d) Compute the mean weight of the others.
w <- c("3750", "3800", "n/a", "3250", "unknown", "3450")
class(w) # "character"
w_num <- as.numeric(w) # warning: NAs introduced by coercion
w_num
sum(is.na(w_num)) # 2
mean(w_num, na.rm = TRUE) # 3562.5When you import a real file, you can tell readr::read_csv() which strings mean “missing” with its na argument (see Import).
Summary
| Function / operator | What it does |
|---|---|
class(), typeof() |
Kind of object / storage type |
is.numeric(), is.character(), … |
Test a type |
as.numeric(), as.character(), … |
Convert a type (failures become NA) |
+ - * / ^, %/%, %% |
Arithmetic, integer division, remainder |
== != < <= > >= |
Comparisons |
!, &, \|, xor() |
Vectorised logic |
&&, \|\| |
Logic on single values (for if ()) |
is.na(), is.nan() |
Detect missing / undefined values |
all.equal(), isTRUE() |
Safe comparison of decimals |
- Four everyday types: numeric (double), integer, character, logical.
- Mixing types coerces to the most flexible: logical → integer → double → character.
TRUEis 1 andFALSEis 0:sum()counts,mean()gives proportions.NAis contagious; test it withis.na(), never with== NA.- Never compare computed decimals with
==.