Números Pseudoaleatorios · Tema 39

Precisión de punto flotante

Cómo guarda decimales tu máquina: binario finito, 53 bits y por qué 0,1 + 0,2 ≠ 0,3.

01 · Punto de partida

0,1 + 0,2 = 0,30000000000000004

Probalo en Python, en JS, en tu calculadora de programador: da 0,30000000000000004. No es un bug de tu código ni del lenguaje: es que 0,1 en binario es periódico (como 1/3 en decimal) y el double de 64 bits lo trunca a 53 bits. Toda tu simulación vive sobre esa grilla finita: conviene conocer sus reglas antes de que te muerdan (Temas 40–42).

Este tema abre el bloque numérico final (Temas 39–43): del bit al algoritmo estable.

  • ¿Cómo se arma un double (signo, exponente, mantisa)?
  • ¿Qué es el épsilon de máquina y cuánto vale?
  • ¿Por qué 0,1 no existe en binario finito?
  • ¿Cómo comparo decimales sin ==?

02 · Definición

Signo, exponente y 53 bits

◈

IEEE 754

1 bit de signo + 11 de exponente + 52 de mantisa (+1 implícito = 53). Rango ≈ ±1e308, paso relativo 2⁻⁵².

⇄

Épsilon ε

1 + ε/2 == 1 en doubles: ε = 2⁻⁵² ≈ 2,22e−16. Es la “moneda mínima” del sistema cerca del 1.

▣

Binario periódico

0,1₁₀ = 0,00011001100…₂ infinito: se trunca. 0,5 y 0,25 sí existen (potencias de 1/2); 0,1, 0,2, 0,3 no.

Qué decimales existen y cuáles no.
DecimalEn binarioExacto en double
0,5 · 0,25 · 0,75Finito (1/2, 1/4…)Sí
0,1 · 0,2 · 0,3Periódico infinitoNo (error ≈ 1e−17)
1e16 + 1Sin bits para el +1No: da 1e16 (absorción)

03 · Fenómenos básicos

Representación, absorción y == roto

Tres caras del mismo bit finito: el valor guardado no es el escrito (representación), sumar poco a mucho no suma nada (absorción) y comparar con == falla por 1 ulp (unidad de último lugar).

Error de representación
0.1 se guarda como 0,1000000000000000055511…: error ≈ 5,5e−18 relativo. Invisible solo, visible al acumular (Tema 42).
Absorción
Si |y| < |x|·ε/2, entonces x + y == x: 1e16 + 1 == 1e16 da True. El sumando chico desaparece.
Comparación con tolerancia
Nunca a == b con floats: math.isclose(a, b, rel_tol=1e-9). El == exige el mismo bit, no el mismo valor matemático.

Bien comparado

isclose con tolerancia

isclose(0,1+0,2, 0,3) → True: compara con margen relativo + absoluto. Tests verdes y honestos.

Mal comparado

== exacto

0,1+0,2 == 0,3 → False: un ulp de diferencia voltea el if y el test falla “sin razón”.

04 · Ejemplos

Tres clásicos de entrevista

Decimal escrito→Binario más cercano→Operación con redondeo
  1. 1
    0,1 + 0,2.

    Dos truncamientos + redondeo de la suma = …004. Error ≈ 4e−17: irrelevante solo, mortal acumulado un millón de veces.

  2. 2
    Bucle x += 0,1 10 veces.

    Termina en 0,9999999999999999, no en 1,0: el while x != 1,0 itera de más o de menos.

  3. 3
    1e16 + 1 == 1e16.

    True: el paso de la grilla cerca de 1e16 es 2,0. Contadores grandes + incrementos chicos = estancamiento.

05 · Demostración en Python

Ver el bit de cerca

Tres líneas que todo programador debe ejecutar una vez: la suma famosa, el épsilon y la absorción.

Python en tu navegador. Probá 0,1+0,2 == 0,3 y luego con isclose: el veredicto cambia.

import math
import sys

print(repr(0.1 + 0.2))
print("== 0.3:", (0.1 + 0.2) == 0.3)
print("isclose:", math.isclose(0.1 + 0.2, 0.3))
print("epsilon:", sys.float_info.epsilon)

…004, False, True, 2,22e−16: el resumen del tema en 4 líneas.

Absorción en vivo

grande = 1e16
print(grande + 1 == grande)
print(grande + 100 == grande)
x = 0.0
for _ in range(10):
    x += 0.1
print(repr(x), x == 1.0)

06 · Exploración

Laboratorio: el error que crece sumando

Sumá un paso chico N veces (x += paso) y mirá el error |acumulado − N·paso|: con paso 0,1 el error oscila y crece; con paso 0,5 (exacto) es siempre 0. El panel lo mide en ulps y en valor absoluto.

EXPERIMENTO 39

Sumar 0,1 mil veces

error = |Σ − N·paso|

Los resultados numéricos aparecen debajo.
Acumulado—
Exacto N·paso—
Error abs.—
En ulps—

Con paso 0,1 el error camina; con 0,5 es 0.

La curva es el error absoluto en función de N. Con paso exacto es una línea en 0; con inexacto, deriva.

Preguntas para explorar

  1. Con paso 0,1 y N = 5000: ¿el error supera 1 ulp? ¿Cuántos?
  2. Con paso 0,5: ¿hay algún N con error ≠ 0? ¿Por qué?
  3. Si este acumulado fuera tu reloj simulado, ¿qué creés que pasa con los eventos programados?
Ver respuestas sugeridas
  1. Sí, decenas de ulps (≈ 1e−12 absoluto): cada suma redondea y los redondeos se acumulan (Tema 42).
  2. No: 0,5 = 1/2 es exacto en binario y N·0,5 también (hasta 2⁵³). Sin truncamiento no hay deriva.
  3. Los eventos caen corridos y las comparaciones == fallan: reloj con enteros o tolerancia (Tema 40).

07 · Comprensión

Confusiones frecuentes

«Es un bug de Python»

Pasa en C, JS, Java y tu calculadora: es IEEE 754, el estándar del hardware. Cambiar de lenguaje no lo arregla.

«Imprimo 2 decimales y listo»

Formatear esconde el error a tus ojos, no al if: f"{x:.2f}" muestra 0,30 pero x == 0,3 sigue False.

«Uso Decimal y me olvido»

Decimal fija lo decimal (plata) pero es ~100× más lento y no sirve para física/simulación masiva: ahí manda float + tolerancia.

«El error es aleatorio, se cancela»

A veces sí, a veces no: sumas siempre positivas acumulan sesgo sistemático (Tema 42). “Se cancela” hay que demostrarlo, no suponerlo.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: el bucle que nunca llega

Mostrá que x += 0,1 10 veces no da 1,0 exacto y arreglalo iterando con enteros.

x = 0.0
for _ in range(10):
    x += 0.1
print(repr(x), x == 1.0)
print([i / 10 for i in range(11)][-1] == 1.0)
Ver solución razonada

0,9999999999999999 False True: acumular redondea 10 veces; dividir el entero 10 una vez es exacto. Relojes y grillas van con enteros.

Ejercicio 2: épsilon en acción

Verificá que 1 + ε/2 == 1 y que 1 + ε == 1 es False.

import sys

eps = sys.float_info.epsilon
print(eps, 1 + eps / 2 == 1, 1 + eps == 1)
Ver solución

2,22e−16 True False: el redondeo al par decide el borde. Esa es la “moneda mínima” cerca del 1.

Ejercicio 3: comparar bien

Escribí cerca(a, b) con tolerancia y probala con 0,1+0,2 frente a 0,3 y con 1e16+1 frente a 1e16.

Ver una posible respuesta
import math

def cerca(a, b, tol=1e-9):
    return math.isclose(a, b, rel_tol=tol, abs_tol=tol)


print(cerca(0.1 + 0.2, 0.3))
print(cerca(1e16 + 1, 1e16))

True True: el segundo True es correcto (difieren menos que la grilla local). Tolerancia = contexto, no pereza.

09 · Síntesis

Ideas para recordar

  • float64: 53 bits, ε ≈ 2,2e−16, ~15–16 dígitos.
  • 0,1 es periódico en binario: se trunca y el error acumula.
  • Absorción: x + y == x si y es chico frente a x.
  • Comparar con isclose, iterar con enteros.
  • Más dígitos impresos no son más precisión.

En el próximo tema, el primo del formato: el error de redondeo en cada operación.