1. Como o computador guarda números¶
Todo método numérico erra, e de propósito. O método de Euler do capítulo 0 finge que a taxa fica parada durante o passo. Uma série de Taylor é cortada depois de alguns termos. Esse é o erro de truncamento, e ele é escolhido: o método troca precisão por trabalho.
Existe um segundo erro, que ninguém escolhe: o computador não guarda os números exatamente. Ele tem só cerca de 16 algarismos significativos para cada número, e alguns números que parecem simples, como \(0{,}1\), não cabem nesse espaço. Esse é o erro de arredondamento. Quase sempre ele é invisível. Às vezes ele derruba um foguete.
Este capítulo ensina a medir o erro, a decidir quando parar um método que se aproxima da resposta aos poucos, e a reconhecer as três armadilhas do arredondamento: o acúmulo, o cancelamento e o estouro.
Exato não existe¶
# O computador guarda numeros em binario, com 53 algarismos binarios
# (cerca de 16 decimais). 0.1 nao tem representacao exata em binario.
soma = 0.1 + 0.2
print("0.1 + 0.2 =", soma)
print("0.1 + 0.2 == 0.3 ?", soma == 0.3)
print(f"0.1 com 20 casas: {0.1:.20f}")
total = 0.0
for i in range(10):
total = total + 0.1
print("0.1 somado 10 vezes:", total)
# o jeito certo de comparar floats: com tolerancia
tol = 1e-9
print("perto o bastante?", abs(soma - 0.3) < tol)
0.1 + 0.2 = 0.30000000000000004
0.1 + 0.2 == 0.3 ? False
0.1 com 20 casas: 0.10000000000000000555
0.1 somado 10 vezes: 0.9999999999999999
perto o bastante? True
🌍 Contexto: por que 0,1 não cabe
O computador guarda números em binário, como uma soma de potências de 2:
metades, quartos, oitavos... Assim como \(1/3 = 0{,}333\ldots\) não tem fim em
decimal, \(1/10\) não tem fim em binário: \(0{,}000110011001100\ldots_2\). O
computador guarda os primeiros 53 algarismos binários (o padrão IEEE 754, em
precisão dupla, que é o float do Python) e descarta o resto. O \(0{,}1\)
guardado é \(0{,}1000000000000000055\ldots\): errado na 18.ª casa.
🧰 Comando novo: np.float32, a precisão simples
O float do Python tem cerca de 16 algarismos (precisão dupla, 64 bits).
Muitos microcontroladores, placas de vídeo e sensores usam a precisão
simples, com 32 bits e cerca de 7 algarismos. np.float32(x) guarda x
desse jeito, para ver o que esses aparelhos veem:
import numpy as np
print(f"0.1 em precisao dupla: {0.1:.20f}")
print(f"0.1 em precisao simples: {np.float32(0.1):.20f}")
Em precisão simples, o 0,1 já erra na nona casa. Conta feita com
np.float32 continua em float32.
A consequência prática é uma regra que vale para o curso inteiro: nunca compare
floats com ==. Compare com uma tolerância: abs(a - b) < tol. É assim que os
testes das listas funcionam.
Erro absoluto e erro relativo¶
Para medir o erro de uma aproximação, há duas contas:
# Dois erros de 1 cm: um e desprezivel, o outro e grave.
# O que diferencia os dois e o erro RELATIVO.
def erros(medido, exato):
absoluto = abs(medido - exato)
relativo = absoluto / abs(exato) * 100
return absoluto, relativo
absoluto, relativo = erros(9999, 10000)
print(f"vao de uma ponte (cm): erro absoluto = {absoluto:6.2f} erro relativo = {relativo:6.2f} %")
absoluto, relativo = erros(9, 10)
print(f"rebite (cm): erro absoluto = {absoluto:6.2f} erro relativo = {relativo:6.2f} %")
absoluto, relativo = erros(0.5, 0.4)
print(f"dose de remedio (mg): erro absoluto = {absoluto:6.2f} erro relativo = {relativo:6.2f} %")
vao de uma ponte (cm): erro absoluto = 1.00 erro relativo = 0.01 %
rebite (cm): erro absoluto = 1.00 erro relativo = 10.00 %
dose de remedio (mg): erro absoluto = 0.10 erro relativo = 25.00 %
O mesmo centímetro de erro é desprezível no vão de uma ponte (0,01 %) e inaceitável num rebite (10 %). Uma dose de remédio 0,1 mg acima parece pouco em valor absoluto, mas é 25 % a mais do que foi prescrito. O erro relativo é o que diz se o erro importa.
Mas quem sabe o valor exato?
Para calcular \(\varepsilon_t\), é preciso conhecer a resposta exata, e quem a conhece não precisa de método numérico. Na prática, o erro é estimado comparando uma aproximação com a anterior. É o assunto da próxima seção.
No quadro: Taylor e o critério de parada¶
Muitos métodos se aproximam da resposta aos poucos: cada iteração dá um valor melhor que o anterior. A pergunta é quando parar.
🧑🏫 No quadro — a série de Taylor e o erro aproximado
1. A série de Taylor de \(e^x\) em torno de \(0\) (todas as derivadas de \(e^x\) valem \(1\) em \(x = 0\)):
2. Cortar a série depois de alguns termos é o erro de truncamento. Cada termo novo é o anterior vezes \(x/n\). Não é preciso calcular potência nem fatorial:
3. Sem conhecer \(e^x\), dá para estimar o erro comparando a soma nova com a anterior. É o erro relativo aproximado:
4. Quando parar? Para garantir pelo menos \(n\) algarismos significativos corretos, pare quando \(\varepsilon_a\) ficar abaixo da tolerância de Scarborough:
Para 3 algarismos, \(\varepsilon_s = 0{,}05\,\%\).
# e^0.5 pela serie de Taylor, 1 + x + x²/2! + x³/3! + ...
# Paramos quando o erro APROXIMADO (que da para calcular sem saber a
# resposta) fica abaixo da tolerancia para 3 algarismos significativos.
import numpy as np
x = 0.5
exato = np.exp(x) # so para comparar
n_alg = 3
eps_s = 0.5 * 10**(2 - n_alg) # tolerancia de Scarborough, em %
soma = 1.0
termo = 1.0
n = 0
print("termos soma eps_t (%) eps_a (%)")
print(f"{n + 1:5d} {soma:.9f} {abs(exato - soma) / exato * 100:9.6f}")
while True:
n = n + 1
termo = termo * x / n # x^n/n! a partir do termo anterior
anterior = soma
soma = soma + termo
eps_t = abs(exato - soma) / exato * 100
eps_a = abs(soma - anterior) / soma * 100
print(f"{n + 1:5d} {soma:.9f} {eps_t:9.6f} {eps_a:9.6f}")
if eps_a < eps_s:
break
print(f"parou com {n + 1} termos: eps_a < eps_s = {eps_s} %")
termos soma eps_t (%) eps_a (%)
1 1.000000000 39.346934
2 1.500000000 9.020401 33.333333
3 1.625000000 1.438768 7.692308
4 1.645833333 0.175162 1.265823
5 1.648437500 0.017212 0.157978
6 1.648697917 0.001416 0.015795
parou com 6 termos: eps_a < eps_s = 0.05 %
Repare nas duas colunas de erro. \(\varepsilon_a\) (que o programa consegue calcular) é sempre maior que \(\varepsilon_t\) (que só conhecemos porque \(e^{0{,}5}\) tem resposta conhecida). Ele é uma estimativa pessimista, e é isso que o torna seguro como critério de parada. Esse mesmo critério reaparece em raízes de equações, em sistemas lineares iterativos e em integrais.
Arredondamento que se acumula¶
Um erro de \(10^{-7}\) é desprezível uma vez. Somado milhões de vezes, deixa de ser.
🌍 Contexto: o míssil Patriot (1991)
Na Guerra do Golfo, uma bateria de mísseis antimísseis Patriot em Dhahran, na Arábia Saudita, não interceptou um míssil Scud. 28 soldados morreram. O relatório do governo americano (GAO, 1992) achou a causa: o relógio do sistema contava décimos de segundo e os convertia para segundos multiplicando por \(0{,}1\), guardado num registrador de 24 bits. A bateria estava ligada havia 100 horas.
O programa abaixo refaz a conta do relógio. Para imitar o registrador de 23 bits, ele multiplica 0,1 por \(2^{23}\), corta a parte fracionária e divide de volta: o que sobrar de 0,1 é o que cabia no registrador.
🧰 Comando novo: np.floor
np.floor(x) arredonda para baixo: devolve o maior inteiro que não passa
de x. É o "cortar as casas" de um registrador ou de um sistema que guarda
centavos:
import numpy as np
print(np.floor(2.9)) # arredonda para baixo
print(np.floor(-2.1)) # "para baixo" nos negativos e para longe do zero
print(round(2.5), round(3.5)) # o round do Python arredonda o 5 para o PAR
Cuidado com os negativos: "para baixo" é na direção de \(-\infty\), então
\(-2{,}1\) vira \(-3\). E o round do Python arredonda o 5 para o número par
(\(2{,}5 \to 2\)), para os erros se compensarem numa soma longa.
# Dhahran, 25/02/1991: uma bateria de misseis Patriot nao interceptou um
# missil Scud. O relogio do sistema contava decimos de segundo e multiplicava
# por 0.1, guardado num registrador de 24 bits (23 depois da virgula binaria).
import numpy as np
decimo_guardado = np.floor(0.1 * 2**23) / 2**23 # 0.1 cortado em 23 bits
erro_por_tique = 0.1 - decimo_guardado
print(f"0.1 guardado: {decimo_guardado:.12f}")
print(f"erro por tique: {erro_por_tique:.3e} s")
horas_ligado = 100
tiques = horas_ligado * 3600 * 10
atraso = tiques * erro_por_tique
print(f"depois de {horas_ligado} h ligado: {tiques} tiques, relogio atrasado {atraso:.4f} s")
velocidade_scud = 1676 # m/s
print(f"nesse tempo o Scud percorre {atraso * velocidade_scud:.0f} m")
0.1 guardado: 0.099999904633
erro por tique: 9.537e-08 s
depois de 100 h ligado: 3600000 tiques, relogio atrasado 0.3433 s
nesse tempo o Scud percorre 575 m
Um erro de um décimo de milionésimo de segundo por tique virou um terço de segundo depois de 3,6 milhões de tiques. O radar procurou o Scud mais de meio quilômetro longe de onde ele estava. A correção do software (contar os tiques como inteiro e converter uma vez só, no fim) chegou a Dhahran no dia seguinte.
Cancelamento catastrófico¶
A pior operação para o arredondamento é subtrair dois números quase iguais. Os algarismos iguais se cancelam, e o que sobra são justamente as últimas casas, as que já estavam contaminadas. Você já viu isso acontecer: foi o h pequeno demais da derivada numérica. Aqui ele aparece na fórmula mais conhecida da escola:
# Bhaskara com b muito maior que a e c: x² + 1e8 x + 1 = 0.
# As raizes sao quase -1e8 e -1e-8.
import numpy as np
a = 1.0
b = 1e8
c = 1.0
delta = b**2 - 4 * a * c
x_grande = (-b - np.sqrt(delta)) / (2 * a)
x_pequena = (-b + np.sqrt(delta)) / (2 * a) # -b + quase b: cancelamento!
print(f"Bhaskara: x1 = {x_grande:.10e} x2 = {x_pequena:.10e}")
# x1 * x2 = c / a: a raiz pequena sai da grande, sem subtrair quase-iguais
x_pequena_estavel = c / (a * x_grande)
print(f"forma estavel: x2 = {x_pequena_estavel:.10e}")
print(f"conferindo: a*x2² + b*x2 + c = {a * x_pequena**2 + b * x_pequena + c:.2e} (Bhaskara)")
print(f" a*x2² + b*x2 + c = {a * x_pequena_estavel**2 + b * x_pequena_estavel + c:.2e} (estavel)")
Bhaskara: x1 = -1.0000000000e+08 x2 = -7.4505805969e-09
forma estavel: x2 = -1.0000000000e-08
conferindo: a*x2² + b*x2 + c = 2.55e-01 (Bhaskara)
a*x2² + b*x2 + c = 1.11e-16 (estavel)
Na raiz pequena, \(-b + \sqrt{\Delta}\) subtrai dois números que concordam em quase todas as casas (\(\sqrt{\Delta} \approx b\)), e a fórmula de Bhaskara erra em 25 %. Substituída na equação, ela nem chega perto de zero. A saída é reescrever a conta para não subtrair quase-iguais: como \(x_1 x_2 = c/a\), a raiz pequena sai da raiz grande por uma divisão. A matemática é a mesma; a conta no computador, não.
Números grandes demais¶
O float tem limite: o maior número que ele guarda é cerca de \(1{,}8 \times
10^{308}\). Passou disso, vira inf (infinito), sem mensagem de erro. Inteiros de
tamanho fixo, como os de um processador embarcado, são piores: passaram do limite,
dão a volta para os negativos, como o hodômetro de um carro que passa de
99999 e volta a 00000. O Python tem inteiros sem limite, então o exemplo imita o
chip com uma conta de resto (%).
🌍 Contexto: o voo 501 do Ariane 5 (1996)
37 segundos depois do lançamento, o foguete europeu Ariane 5 desviou da rota e se autodestruiu, com quatro satélites a bordo. O software de navegação, reaproveitado do Ariane 4, convertia a velocidade horizontal (um float de 64 bits) para um inteiro de 16 bits. No Ariane 4, mais lento, o valor sempre cabia. No Ariane 5, não coube. O programa parou, e o computador reserva, que rodava o mesmo código, parou do mesmo jeito.
# Limites do tamanho dos numeros.
# 1) O float vai ate cerca de 1.8e308. Depois disso, vira inf (infinito).
fatorial = 1.0
for n in range(1, 172):
fatorial = fatorial * n
if n >= 169:
print(f"{n}! = {fatorial}")
# 2) Ariane 5, 04/06/1996: a velocidade horizontal foi guardada num inteiro
# de 16 bits, que so vai de -32768 a 32767. Esta funcao faz o que o chip faz
# com um valor que nao cabe: ele "da a volta" pelos negativos.
def inteiro_16_bits(valor):
return (valor + 32768) % 65536 - 32768
for v in [20000, 32767, 32768, 40000]:
print(f"valor {v:6d} -> inteiro de 16 bits: {inteiro_16_bits(v):6d}")
169! = 4.2690680090047027e+304
170! = 7.257415615307994e+306
171! = inf
valor 20000 -> inteiro de 16 bits: 20000
valor 32767 -> inteiro de 16 bits: 32767
valor 32768 -> inteiro de 16 bits: -32768
valor 40000 -> inteiro de 16 bits: -25536
Dois cuidados práticos saem daqui. O primeiro: calcular \(x^n/n!\) fazendo a potência e o fatorial separados estoura bem antes do necessário (o fatorial passa de \(10^{308}\) em \(171!\)), enquanto o termo anterior vezes \(x/n\) do quadro nunca estoura. O segundo: todo valor que muda de tipo (float para inteiro, 64 para 16 bits) precisa de uma pergunta. E se não couber?
Mesmo método, outra área¶
O arredondamento também mora no dinheiro. Um banco guarda saldos em centavos, então todo rendimento tem de ser arredondado. Arredondar ou cortar os centavos parece um detalhe:
# Mesmo assunto, outra area: um banco com um milhao de contas de poupanca.
# Cada conta rende 0,5 % ao mes e o saldo e guardado em centavos. Quanto
# muda se o sistema ARREDONDA ou se CORTA os centavos todo mes?
import numpy as np
capital = 1234.56
taxa = 0.005
meses = 120
exato = capital
arredondado = capital
cortado = capital
for mes in range(meses):
exato = exato * (1 + taxa)
arredondado = np.round(arredondado * (1 + taxa), 2)
cortado = np.floor(cortado * (1 + taxa) * 100) / 100
print(f"sem arredondar: R$ {exato:.4f}")
print(f"arredondando todo mes: R$ {arredondado:.2f}")
print(f"cortando todo mes: R$ {cortado:.2f}")
print(f"diferenca por conta (corte): R$ {exato - cortado:.4f}")
perda_total = (exato - cortado) * 1e6
print(f"em um milhao de contas: R$ {perda_total:.2f}")
sem arredondar: R$ 2246.1544
arredondando todo mes: R$ 2246.09
cortando todo mes: R$ 2245.38
diferenca por conta (corte): R$ 0.7744
em um milhao de contas: R$ 774431.97
Setenta e sete centavos por conta em dez anos. Ninguém reclama de setenta e sete centavos. Em um milhão de contas, são mais de setecentos mil reais que mudaram de dono por causa de uma escolha de arredondamento. Arredondar erra para os dois lados e os erros tendem a se compensar. Cortar erra sempre para o mesmo lado, e erros de mesmo sinal se acumulam, exatamente como no relógio do Patriot.
Erros comuns deste capítulo¶
| Sintoma | Causa provável |
|---|---|
if x == 0.3: nunca é verdadeiro |
comparação de float com ==: use abs(x - 0.3) < tol |
o laço while x != 1.0: nunca termina |
o mesmo, num laço: x passa por 0.9999999999999999 e segue em frente |
erro relativo dá ZeroDivisionError ou inf |
o valor exato (ou o atual, no \(\varepsilon_a\)) é zero: use o erro absoluto |
| a série para cedo demais | comparou \(\varepsilon_a\) em fração (0,0005) com \(\varepsilon_s\) em porcentagem (0,05), ou o contrário |
resultado inf ou nan no meio das contas |
estouro: potência ou fatorial calculados separados; use o termo anterior |
| uma fórmula certa dá resultado errado | cancelamento: procure uma subtração de números quase iguais e reescreva a conta |
Onde isso é usado de verdade¶
- Todo software de engenharia declara a tolerância com que compara números.
Os testes das próprias bibliotecas
numpyescipycomparam resultados com tolerância, exatamente como oabs(a - b) < toldeste capítulo. - Sistemas financeiros guardam dinheiro como inteiro de centavos (ou em
tipos decimais), nunca como
float, e as regras de arredondamento são fixadas em norma. - Aviação, espaço e defesa exigem análise de faixa de valores em toda conversão de tipo. É uma lição direta do Ariane 5.
- GPS: um erro de um microssegundo no relógio vira 300 metros de erro na posição. Os receptores tratam o tempo com extremo cuidado numérico.
- Aprendizado de máquina treina redes em precisão reduzida (16 bits) para ganhar velocidade, e passa boa parte do esforço de engenharia evitando estouro e cancelamento.
Resumo¶
- O computador guarda cerca de 16 algarismos significativos; números como \(0{,}1\)
não cabem exatamente. Nunca compare floats com
==. - O erro relativo é o que diz se um erro importa.
- Sem a resposta exata, o erro é estimado pela diferença entre iterações, \(\varepsilon_a\). Para \(n\) algarismos significativos, pare quando \(\varepsilon_a < \varepsilon_s = 0{,}5 \times 10^{2-n}\,\%\).
- As três armadilhas do arredondamento: o acúmulo (erros de mesmo sinal somados muitas vezes), o cancelamento (subtrair quase-iguais) e o estouro (passar do maior número que o tipo guarda).
- A saída costuma ser reescrever a conta: contar tiques como inteiro, tirar a raiz pequena da grande, calcular o termo a partir do anterior.
Para praticar¶
A Lista 01 começa com erros e algarismos significativos à mão (✏️) e segue com a comparação com tolerância, a série de Taylor com critério de parada, a Bhaskara estável, a soma que se acumula e os centavos do banco.