Números Pseudoaleatorios · Tema 41

Error de truncamiento

Cortar decimales a propósito: por qué truncar sesga, cuándo pierde hasta 1 unidad y cómo reemplazarlo.

01 · Punto de partida

Cortar no es redondear

En el Tema 40 viste el redondeo del hardware: el vecino más cercano, error ≤ ½ ulp y sin sesgo. Truncar es lo contrario: cortar los decimales que sobran y quedarse con lo que entra, siempre hacia cero. Lo hacés cada vez que convertís con int(), que armás un índice con int(u * n) o que mostrás un precio con 2 decimales “cortados”.

El corte parece inofensivo, pero tiene dos pecados: pierde hasta el doble que redondear (1 unidad en vez de ½) y sesga: con positivos siempre resta, con negativos siempre suma. En una cuenta no pasa nada; en un millón de cuentas o en un histograma, el sesgo se ve.

  • ¿Qué diferencia hay entre truncar, redondear y pisar (floor)?
  • ¿Por qué int(-2,7) da −2 y no −3?
  • ¿Cuánto se pierde al truncar a k decimales?
  • ¿Dónde muerde el sesgo en simulación: índices, bins, plata?

02 · Definición

Tres cortes, tres destinos

◈

trunc (hacia cero)

trunc(2,7) = 2, trunc(−2,7) = −2. Es lo que hace int() con floats. Corta el decimal y punto.

⇄

floor (hacia −∞)

floor(2,7) = 2, floor(−2,7) = −3. Es lo que hace // e int(u*n) bien usado para índices y bins.

▣

round (al cercano)

round(2,7) = 3, round(−2,7) = −3. Error ≤ mitad, sin sesgo sistemático. La opción por defecto para medir.

El mismo número, tres resultados.
xtruncfloorround
2,675223
−2,675−2−3−3
0,9999 (k = 2)0,990,991,00

03 · Anatomía

Dónde nace el sesgo

El redondeo del Tema 40 a veces suma y a veces resta: en promedio se compensa. El truncamiento con positivos siempre resta: cada corte deja una “basura” entre 0 y 10⁻ᵏ que nunca se recupera. Tres lugares donde aparece:

Truncamiento de visualización
Mostrar con int(x*100)/100 o cortar el string a 2 decimales: el ticket dice 2,67 aunque el valor real es 2,679. Para mostrar se usa round/formato, no corte.
Truncamiento de conversión
int(u * n) con u en [0,1): si u puede valer 1,0 por redondeo, el índice se sale de rango. Y con negativos, int no es floor: los bins se corren un lugar (Tema 35).
Truncamiento de modelo (aviso)
En métodos numéricos también se llama “truncamiento” a cortar una serie infinita (Taylor, Euler): otro corte con otro análisis. Acá nos ocupamos del corte de decimales; el de series reaparece en el Tema 43 como inestabilidad.

Bien cortado

floor para bins e índices

min(int(u*n), n-1) o math.floor: cada bin mide lo mismo y el borde 1,0 no rompe nada. Predecible con negativos.

Mal cortado

int() para plata o promedios

int(precio*100)/100 siempre regala centavos al mismo lado; promediado miles de veces, el faltante se vuelve plata real.

04 · Ejemplos

Tres cortes que duelen

Valor real→Corte a k decimales→Basura de hasta 10⁻ᵏ
  1. 1
    El centavo fantasma.

    Truncar 2,679 a 2 decimales da 2,67 (pierde 0,009); redondear da 2,68 (pierde 0,001). En 100 000 tickets, el corte regala ~450 $ del mismo bolsillo.

  2. 2
    El índice que se escapa.

    int(0,9999999 * 10) = 9 está bien, pero si el redondeo deja u = 1,0, int(1,0*10) = 10 explota la lista. El min(..., n-1) lo salva (Tema 22).

  3. 3
    El bin corrido.

    Con int(x) sobre negativos, el intervalo [−3,−2) cae en −2 en vez de −3: el histograma cojea justo donde cruza el cero. Con floor cada bin mide 1 parejo.

05 · Demostración en Python

Cortar a mano y medir la basura

Dos funciones de 2 líneas separan todo: truncar corta, redondear elige al cercano. Medí la basura de cada una sobre el mismo valor.

Python en tu navegador. Probá con 2,675, −2,675 y 0,9999: el corte siempre pierde más y siempre del mismo lado.

import math

def truncar(x, k=2):
    f = 10 ** k
    return math.trunc(x * f) / f

def redondear(x, k=2):
    return round(x, k)

for v in [2.675, -2.675, 0.9999]:
    print(v, truncar(v), redondear(v))

Verás 2,67 vs 2,68, −2,67 vs −2,67… y el 0,99 vs 1,0: el corte pierde hasta 0,01 entero.

El sesgo en cámara lenta

import math
import random

random.seed(41)
vals = [random.random() * 10 for _ in range(20000)]
basura_trunc = sum(v - math.trunc(v * 100) / 100 for v in vals)
basura_round = sum(v - round(v, 2) for v in vals)
print("trunc suma:", round(basura_trunc, 2))
print("round suma:", round(basura_round, 4))

06 · Exploración

Laboratorio: truncar frente a redondear

Elegí un valor y cuántos decimales conservar (k). El punto verde es el valor real, el naranja el truncado y el celeste el redondeado, sobre el intervalo entre los dos múltiplos de 10⁻ᵏ que lo encierran. La barra naranja es siempre más larga que la celeste… o igual en el peor empate.

EXPERIMENTO 41

Corte a k decimales

|trunc − x| < 10⁻ᵏ · |round − x| ≤ ½·10⁻ᵏ

Los resultados numéricos aparecen debajo.
Truncado—
Redondeado—
Error trunc—
Error round—

Con π y k = 2 el corte pierde más del doble que el redondeo.

La recta es el intervalo de largo 10⁻ᵏ entre los dos candidatos. El truncado cae siempre en el borde hacia cero; el redondeado, en el más cercano al valor real.

Preguntas para explorar

  1. Con π y k = 2: ¿cuánto pierde truncar (3,14) frente a redondear (3,14)? ¿Y con k = 0?
  2. Con 2,675 y k = 2: ¿por qué el redondeado da 2,67 y no 2,68? (Pista: Tema 40.)
  3. Con −2,675: ¿dónde cae el truncado respecto del redondeado? ¿Qué pasaría con floor?
Ver respuestas sugeridas
  1. Truncar pierde ≈ 0,00159 y redondear ≈ 0,00159 también acá (π ≈ 3,1416 está casi al medio bajo); con k = 0 truncar pierde 0,14 y redondear solo 0,14… no: trunc(π) = 3 pierde 0,1416 y round(π) = 3 pierde lo mismo: el empate se rompe con 1/3 (0,33 vs 0,33 iguales, pero con k = 0: 0 vs 0).
  2. Porque el literal 2,675 ya es 2,6749999… en binario (Tema 40): el vecino más cercano a 2 decimales es 2,67. El “debería dar 2,68” vive en el pizarrón, no en la grilla.
  3. El truncado (−2,67) queda a la derecha del real y el redondeado (−2,67 o −2,68 según el vecino binario) a la izquierda o igual: el corte tira hacia cero y floor hacia −∞ (−2,68). Con negativos se separan un paso entero.

07 · Comprensión

Confusiones frecuentes

«Truncar es redondear para abajo»

Solo con positivos. Con negativos truncar es “para arriba” (hacia cero): int(−2,7) = −2. “Para abajo” de verdad es floor. Esa diferencia de 1 rompe bins que cruzan el cero.

«Cortar a 2 decimales es exacto para plata»

Cortar siempre regala la basura al mismo lado: en miles de operaciones el faltante suma. Para plata se redondea (o se trabaja en centavos enteros), no se corta.

«int(u * n) siempre da un índice válido»

Si u llega a 1,0 por redondeo, int(n) = n se sale de la lista. El tope min(..., n-1) no es paranoia: es el borde del intervalo [0,1] (Tema 22).

«Truncamiento y redondeo pierden lo mismo»

Truncar pierde hasta 1 unidad (10⁻ᵏ) y redondear hasta media: el doble. Y el de truncamiento es sesgado. A igual k, redondear es siempre mejor para medir.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: los tres destinos de −2,7

Mostrá en una línea que int, // y round no son sinónimos con negativos.

import math

x = -2.7
print(int(x), math.floor(x), round(x))
print(x // 1, math.trunc(x))
Ver solución razonada

−2 −3 −3 y −3,0 −2: int/trunc van hacia cero y floor/// hacia −∞. Con positivos coinciden y el bug se esconde hasta que la grilla cruza el cero.

Ejercicio 2: el centavo que suma

Compará truncar frente a redondear 20 000 importes a 2 decimales: ¿cuánto regala cada uno en total?

import math
import random

random.seed(41)
vals = [random.random() * 100 for _ in range(20000)]
t = sum(v - math.trunc(v * 100) / 100 for v in vals)
r = sum(v - round(v, 2) for v in vals)
print("trunc regala:", round(t, 2))
print("round regala:", round(r, 4))
Ver solución

Truncar regala ≈ 100 $ (media 0,005 × 20 000) siempre del mismo lado; redondear oscila cerca de 0. Ese es el sesgo convertido en plata.

Ejercicio 3: bins sin corrimiento

Escribí bin_int (con int) y bin_floor (con floor) y probalas con valores a ambos lados del cero.

Ver una posible respuesta
import math

def bin_int(x, w=1.0):
    return int(x / w)

def bin_floor(x, w=1.0):
    return math.floor(x / w)


for v in [-2.7, -2.0, -0.2, 0.2, 2.7]:
    print(v, bin_int(v), bin_floor(v))

Con negativos difieren en 1: int junta [−3,−2) con [−2,−1) en el borde. floor deja todos los bins de ancho w parejo.

09 · Síntesis

Ideas para recordar

  • Truncar corta hacia cero; floor hacia −∞; round al cercano.
  • Truncar pierde hasta 10⁻ᵏ (el doble que redondear) y siempre del mismo lado.
  • Con negativos, int ≠ floor por 1 unidad: bins parejos van con floor.
  • Índices con min(int(u*n), n-1); plata con round o centavos enteros.
  • El corte es sesgo, no ruido: sumado miles de veces se vuelve plata o histograma torcido.

En el próximo tema veremos qué pasa cuando esos cortes y redondeos se suman miles de veces: la acumulación de errores numéricos.