Primero que nada, la demostración simple del problema:
> sprintf("%.60f",2.3 - 1.8)
[1] "0.499999999999999777955395074968691915273666381835937500000000"
> sprintf("%.60f",sqrt(2)^2)
[1] "2.000000000000000444089209850062616169452667236328125000000000"
Vemos que en el primer caso 2.3 - 1.8
no es exactamente 0.5
y también que sqrt(2)^2
tampoco es exactamente 2
. Lo que ocurre es que, para mayor claridad, en R no se muestra una gran cantidad de decimales por defecto:
> 2.3 - 1.8
[1] 0.5
Este problema no es de R, sino que es inherente a la propia arquitectura computacional (Ver: ¿Por qué mis programas no pueden hacer cálculos aritméticos correctamente?), la imposibilidad de representar ciertos números mediante coma flotante. Uno podría pensar que el problema es representar números como 1/3
o Pi
, pero no son solo estos, por el algebra binaria, incluso números decimales "más simples" no pueden ser representados de forma exacta, sino que lo que se maneja siempre es una aproximación:
> sprintf("%.60f", 0.5)
[1] "0.500000000000000000000000000000000000000000000000000000000000"
> sprintf("%.60f", .1) # 0.1 se maneja como una aproximación
[1] "0.100000000000000005551115123125782702118158340454101562500000"
Obviamente solo se ven los decimales más significativos, podemos ver hasta cuantos dígitos significativos se tomarán con getOption("digits")
ahora si modificamos esto:
> options(digits=22)
> 2.3 - 1.8
[1] 0.49999999999999978
Notamos claramente dónde tenemos el problema. ¿Como resolvemos esta inconsistencia?
Comparaciones (igualdad)
Uno de los principales problemas, como ya vimos, es el de comparar dos números dónde al menos uno sea producto de una operación con valores de coma flotante. La forma natural de resolverlo es usando la función all.equal()
y combinada con isTRUE()
> isTRUE(all.equal(2.3 - 1.8,0.5))
[1] TRUE
Esto sirve para tratar escalares, o siendo consistentes con el lenguaje, vectores de un solo elemento, sin embargo para vectores con varios elementos, hay que "vectorizar" la anterior solución, por ejemplo:
> a <- c(0.1+0.05, 1-0.1-0.1-0.1, 0.3/0.1, 0.1+0.1)
> b <- c(0.15, 0.7, 3, 0.15)
# Ya sabemos que la igualdad habitual no funcionará
> a==b
[1] FALSE FALSE FALSE FALSE
# Pero tampoco nos sirve all.equal()
> isTRUE(all.equal(a,b))
[1] FALSE
# La alternativa es vectorizar all.equal() usando mapply()
> mapply(function(x, y) {isTRUE(all.equal(x, y))}, a, b)
[1] TRUE TRUE TRUE FALSE
> # O mucho mejor creando una función ad-hoc
> elementwise.all.equal <- Vectorize(function(x, y) {isTRUE(all.equal(x, y))})
> elementwise.all.equal(a, b)
[1] TRUE TRUE TRUE FALSE
Nota: En caso de usar dplyr
contamos con near()
> library(tidyverse)
> near(a,b)
[1] TRUE TRUE TRUE FALSE
Otras comparaciones
Para el resto de las comparaciones, debemos atacar el problema de otra forma. Este sería el problema:
> v <- c(2.3 - 1.8, 0.1 + 0.4, sqrt(0.5)^2)
> v
[1] 0.5 0.5 0.5
> unique(v)
[1] 0.5 0.5 0.5
> v > 0.5
[1] FALSE FALSE TRUE
Aquí lo que podemos hacer es usar zapsmall()
que no es más que una "wrapper" alrededor de un round()
para quitar dígitos innecesarios:
> unique(zapsmall(v, digits=2))
[1] 0.5
> zapsmall(v, digits=2) > 0.5
[1] FALSE FALSE FALSE
Fuentes: