.1 == .3 / 3[1] FALSE
0.1 + 0.2 == 0.3[1] FALSE
Nikita M.
March 13, 2025
Floating-point numbers in R are subject to precision limitations due to IEEE 754 representation. This section explores how seemingly simple operations can lead to unexpected results.
Why does this return FALSE?
Due to how floating-point arithmetic works in computers, numbers like 0.1, 0.3, and 0.3 / 3 are stored in binary.
Some decimal fractions cannot be represented exactly in binary, leading to tiny rounding errors. E.g.:
Explanation: - 0x indicates a hexadecimal number. - 1.999999999999a represents the normalised significand (mantissa) in base-16 (hexadecimal).
p-4 is the exponent: p represents the power of 2 (exponent in base-2); 4 means the value is multiplied by 2⁻⁴.
Even though mathematically .1 and .3 / 3 should be the same, their internal representations are not exactly equal.
Explanation:
The first output shows what R normally prints, which is rounded for display.
The second output uses digits=16 to show more of the actual stored precision.
This reveals that 0.3 / 3 is actually stored as 0.09999999999999999, not exactly 0.1.
all.equal() Instead of ==Why does this return TRUE?
all.equal() does not check for exact equality but allows a small tolerance to account for floating-point errors.
It is the preferred way to compare floating-point numbers in R.
Do not use all.equal directly in if expressions—either use isTRUE(all.equal(...)) or identical if appropriate. If x and y are not equal, it returns a character string like: “Mean relative difference: 5.551115e-17”. The if statement expects TRUE or FALSE, but it receives a string, causing an error.
[1] 2.220446e-16
[1] "Mean relative difference: 1.387779e-16"
Explanation:
.Machine$double.eps is the smallest possible difference that R can distinguish between two floating-point numbers.
The default tolerance of all.equal() is sqrt(.Machine$double.eps), but we can adjust it manually if needed.
round()Explanation:
round(..., digits = 16) forces R to round the computed value to 16 decimal places, reducing precision issues.0.1 == round(0.3 / 3, digits = 16) now returns TRUE📖 More details: The R Inferno
Memory management in R determines how objects are stored and how much space they occupy. Understanding how R allocates memory can help optimize performance, especially with large datasets.
Why are both x and y 56 bytes?
Even though y is an integer (4 bytes) and x is a double (8 bytes), R adds metadata overhead.
Every object in R carries around 48 bytes of metadata, including type and reference count.
Memory is aligned in multiples of 8 bytes, so the total allocation rounds up to 56 bytes.
8.00 MB
4.00 MB
Why does the integer vector use half the memory?
Each integer (5L) takes 4 bytes per element.
Each double (5) takes 8 bytes per element.
For large datasets, using integers instead of doubles can save significant memory.
8.00 MB
64.00 MB
[[1]]
[1] 5
[[2]]
[1] 5.5
[[3]]
[1] 5
64.00 MB
Explanation:
Assigning 5.0 to y_vec forces R to convert the entire vector to double, increasing memory usage.
lapply(y_vec, as.integer) converts the values back to integers, but not reducing its size.
Assigning 5.5 back forces conversion to double again but only for the selected element because new_vec is now a list.
📖 More details: Mastering Software Development in R
R allows partial matching of list elements, which can lead to unintended behaviour and difficult-to-debug errors.
test_function <- function(alpha = NULL, power = NULL, p = NULL, p0 = NULL) {
user_defined <- list(alpha=alpha, power=power, p=p, p0=p0)
user_defined <- Filter(Negate(is.null), user_defined)
return(user_defined)
}
results = test_function(alpha=0.05, power=0.8)
print(results$p) # Partial matching[1] 0.8
$alpha
[1] 0.05
$power
[1] 0.8
test_function - the function takes four optional arguments, all defaulting to NULL, making them optional.
A named list is created to store all input arguments. If an argument is not provided, it remains NULL.
Filter(Negate(is.null), user_defined) removes all NULL elements, ensuring only explicitly provided parameters are retained.
Why is results$p returning 0.8?
p was not provided in test_function() call, but R still allows partial matching.[1] 10
Warning in x$p: partial match of 'p' to 'power'
[1] 10
NULL
Best Practices:
Use options(warnPartialMatchDollar = TRUE) to warn about unintended partial matches.
Always use x[["p"]] instead of x$p to ensure explicit matching.
📖 More details: Advanced R - Subsetting
[[ OperatorNow that we’ve seen how to extract specific elements from function outputs, we can extend this idea to other cases where structured outputs require efficient subsetting.
Extracting values from structured objects often requires a combination of sapply(), lapply(), and [[ to efficiently access named list elements.
t.test() and Extracting P-valuesWhen running multiple tests, we can store results in a list and apply subsetting with [[.
set.seed(123)
data_list <- list(
test1 = t.test(rnorm(10), rnorm(10)),
test2 = t.test(rnorm(15), rnorm(15)),
test3 = t.test(rnorm(20), rnorm(20))
)
p_values <- sapply(data_list, `[[`, "p.value")
print(p_values) test1 test2 test3
0.7672027 0.7040755 0.4752422
sapply(data_list, "[[", "p.value") extracts p.value from each result.[[ can extract “conf.int”, “statistic”, or “parameter”, making it a powerful way to subset structured outputs.📖 More details: Advanced R - Subsetting