A Northstar-adatszerződés pénzösszegeket EUR-centben, egész minor
unitban tárol: basket_minor,
shipping_fee_minor és az összeg
basket_minor + shipping_fee_minor is egész
int64. A gyakorlatban mégis gyakran euróra váltják az
értékeket float64 osztással, mert a megjelenítés vagy egy
modell inputja ezt kéri. Ebből két kérdés lesz:
float64?A konkrét bemenet most öt képzeletbeli rendelés:
[1499, 2300, 8750, 0, 100] cent kosárérték és
[299, 0, 599, 0, 499] cent szállítási díj. A megfigyelési
egység egy rendelés; a cél a rendelésenkénti total_minor
pontos előállítása és annak ellenőrzése, hogy a lebegőpontos
euró-kerülőút nem rontja-e el a centet. Előzetes becslés: egyetlen
osztás-szorzás körüli hiba jellemzően 10^-16 relatív
nagyságrendű, így öt soron nem várunk centhibát, de a pontos
== ellenőrzés attól még hibás lehet.
A np.float64 az IEEE 754 double precision formátum: 1
előjelbit, 11 kitevőbit és 52 tárolt mantisszabit; a normalizált
értékeknél egy implicit vezető bit miatt a pontosság 53 bit. A
gép-epsilon
azt jelenti, hogy egy kerekítés relatív hibája legfeljebb
,
ha nem lépünk át nagyságrendi határt. A tizedes 0.1
binárisan végtelen tört, ezért a tárolt érték csak közelíti a tizedes
0.1-et. Ennek következménye a jól ismert példa:
0.1 + 0.2 0.30000000000000004 lesz, nem
pontosan 0.3.
A hibát két mennyiséggel mérjük. Ha a számított közelítés és a referencia, akkor az abszolút hiba:
a relatív hiba pedig esetén:
A 0.1 + 0.2 példában
,
.
Ez messze kisebb egy centnél
(),
tehát a hiba nagyságrendje más, mint a pénzügyi kerekítés hibája. Ezért
hasznos külön beszélni a numerikus toleranciáról és az üzleti
kerekítésről.
A Python int tetszőleges pontosságú, de a
np.int64 rögzített 64 bites, előjeles szám, tartománya:
Túlcsorduláskor a NumPy nem dob hibát alapértelmezésben, hanem modulo
visszatekeredik. A Northstar értékei bőven e tartomány alatt vannak,
ezért az int64 centbiztos. Ha viszont például egy
azonosítót véletlenül numerikus oszlopként olvasnánk be és szoroznánk
össze, a túlcsordulás csendben hamis eredményt adhat.
A np.nan (not a number) a hiányzó vagy érvénytelen
lebegőpontos érték jelölője. Fontos tulajdonságai:
nan nem közönséges szám, ezért
np.nan == np.nan hamis;nan szerepel,
nan-t ad;np.isnan() vizsgálja a hiányzó értéket;np.isfinite() a véges értékeket válogatja ki:
inf és nan kiesik.Egy int64 tömbben nem tárolhatunk nan-t; a
Northstar nyers adatában a hiányzó distance_km például
betöltés után float64 tömbben lehet nan.
A bitazonos == lebegőpontos számítás után általában
rossz ellenőrzés. A
np.allclose(a, b, rtol=1e-05, atol=1e-08, equal_nan=False)
elemenként ezt a feltételt nézi:
Itt
és
a két azonos shape-ű tömb megfelelő eleme. Az atol abszolút
tűrés, az rtol relatív tűrés: az rtol * |b_i|
tag miatt a feltétel nagy értékeknél is megengedőbb, az
atol miatt nulla körül sem válik lehetetlenné. A függvény
egyetlen logikai értéket ad, ha minden elem megfelel. Az
equal_nan=True használatával explicit módon elfogadhatjuk,
hogy két nan „egyenlő”, de ezt csak tudatos döntésként
szabad bekapcsolni.
A lebegőpontos összeadás nem asszociatív: minden részösszeg
kerekítődik, így egy nagy érték mellé adott kis érték elveszhet. A
np.sum páros összegzést használ, ami csökkenti, de nem
szünteti meg a hibát; a Python math.fsum kompenzált
összegzése sokkal pontosabb. A pénz centben tartásának egyik előnye,
hogy az egész összeadásnak nincs ilyen kerekítési hibája.
A következő kód a Northstar-szerű adatot egész centben és lebegőpontos euró-kerülőúton is feldolgozza. A kimenet szemléltető, elméletileg várt érték; ebben a generálásban nem futott Python-kód.
import math
import numpy as np
# Bemenet: 5 rendelés, Northstar-szerű egész cent
basket_minor = np.array([1499, 2300, 8750, 0, 100], dtype=np.int64)
shipping_fee_minor = np.array([299, 0, 599, 0, 499], dtype=np.int64)
# 1) Egész út: a szerződés szerinti total_minor
total_minor_int = basket_minor + shipping_fee_minor
print("total_minor_int:", total_minor_int, total_minor_int.dtype)
# 2) Lebegőpontos euró út
total_eur = (basket_minor.astype(np.float64) / 100.0
+ shipping_fee_minor.astype(np.float64) / 100.0)
total_minor_back = np.rint(total_eur * 100.0).astype(np.int64)
print("vissza centbe:", total_minor_back)
# 3) Bitazonos és toleráns ellenőrzés
print("bitazonos:", np.array_equal(total_minor_int, total_minor_back))
print("allclose tűrés nélkül:",
np.allclose(total_minor_int, total_minor_back, rtol=0.0, atol=0.0))
print("allclose centes euró-ellenőrzés:",
np.allclose(total_eur,
total_minor_int.astype(np.float64) / 100.0,
rtol=1e-12, atol=1e-12))
# 4) A híres egyenlőség-ellenpélda
x = 0.1 + 0.2
print(repr(x), "==", x == 0.3, "isclose:", np.isclose(x, 0.3))
# 5) Integer overflow, NaN, összegzési hiba
print("int64 max + 1:", np.int64(2**63 - 1) + np.int64(1))
v = np.array([1.0, np.nan, np.inf, -np.inf])
print("v == v:", v == v)
print("isnan:", np.isnan(v))
print("isfinite:", np.isfinite(v))
a = np.array([1e16, 1.0, -1e16], dtype=np.float64)
print("np.sum:", a.sum())
print("math.fsum:", math.fsum(a))A várt, szemléltető kimenet:
total_minor_int: [1798 2300 9349 0 599] int64
vissza centbe: [1798 2300 9349 0 599]
bitazonos: True
allclose tűrés nélkül: True
allclose centes euró-ellenőrzés: True
0.30000000000000004 == False isclose: True
int64 max + 1: -9223372036854775808
v == v: [ True False True True]
isnan: [False True False False]
isfinite: [ True False False False]
np.sum: 0.0
math.fsum: 1.0
A total_minor_int shape-je (5,), dtype-ja
int64, és az összeadás kerekítés nélkül pontos, mert egész
centeket ad össze. A lebegőpontos úton az euróra váltás bevezet ugyan
egy
nagyságrendű hibát, de a np.rint(total_eur * 100.0) centes
kerekítés itt éppen visszaadja az egész értéket, ezért a bitazonos
ellenőrzés is igaz. Ez nem általános garancia: a centes kerekítés
eltünteti a kis hibát, de ha a kód a kerekítés előtt hasonlít össze
==-gyel, a 0.1 + 0.2 == 0.3 példa hamis lesz.
Az allclose itt rtol=1e-12, atol=1e-12 mellett
is igaz, mert az osztás-szorzás hibája jóval ez alatt van. A nagy
int64 érték túlcsordulása csendben negatív számot ad; a
NaN összehasonlítása önmagával hamis; a
[1e16, 1, -1e16] naiv np.sum összege
0.0, miközben a kompenzált math.fsum
1.0-t ad.
Hiányzó adat és nem véges szám elkülönítése. A np.nan
lebegőpontos hiányjelző, a np.inf/-np.inf nem
véges numerikus érték, a pd.NA pedig a pandas
kiterjesztett, háromértékű logikájában szereplő hiányjelzés; ezek nem
felcserélhetők. Az np.isnan, np.isfinite és a
pandas isna műveletei eltérő típusfeltételekkel
használhatók.
Numerikus ellenpróba: számold ki ugyanazt a kifejezést matematikailag ekvivalens, de numerikusan eltérő alakban (például közeli számok kivonásával), és vizsgáld az abszolút, illetve relatív hibát. Az eredményhez mindig add meg a lebegőpontos típust, a bemeneti skálát és a toleranciát.
Adott a = 0.1, b = 0.2, referencia
r = 0.3 tizedesként. Számítsd ki az a + b
float64 közelítés és r abszolút, valamint
relatív hibáját! Hány nagyságrenddel kisebb ez, mint 0.01
abszolút hibája? Fogalmazd meg, miért hibás így ellenőrizni egy fizetési
egyenleget: if (0.1 + 0.2) == 0.3: ...
Írj függvényt check_roundtrip(minor_cents) néven, amely
int64 centértékeket float64 euróra vált, majd
np.rint(eur * 100).astype(np.int64) módon visszaállít
centbe, és visszaadja azon indexeket, ahol az eredeti és a
visszaállított érték nem egyezik. Generálj
rng = np.random.default_rng(20261020) seeddel
10_000 véletlen centértéket a [0, 10_000_000)
intervallumból, futtasd az ellenőrzést, és add meg a hibás sorok
arányát! Miért nem jelenti az eredmény azt, hogy a lebegőpontos út
mindig biztonságos?
Az alábbi kód egy hiányos értékeket tartalmazó sort szeretne megtisztítani, de rossz eredményt ad. Mi a hiba?
import numpy as np
values = np.array([100.0, np.nan, 50.0])
clean = values[values == np.nan] # a készítő szándéka: nan kihagyása
print(clean)
total = np.sum(values)
print(total)Javítsd np.isnan segítségével, majd mutasd meg az
np.nansum alternatívát is! Milyen shape-ű tömböt kapsz a
javítás után?
Tegyük fel, hogy ugyanezt a rendszert USD pennyben tároló
szolgáltatásra viszed át, és egy riport minden sort float32
euróra vált, mielőtt 10 millió sort összegez. A float32
gép-epsilonja
,
a kerekítés relatív hibája legfeljebb
.
Egy 100 dolláros sor abszolút hibájának felső becslése mennyi? Ha a
hibák azonos előjelűek maradnának, mekkora lehet a 10 millió soros
összeg abszolút hibája? Milyen tárolási döntést hoznál, és miért?
A float64 0.1 + 0.2 eredménye
0.30000000000000004. A különbség:
A relatív hiba:
Ez 0.01-hez képest
szorzóval, tehát körülbelül 15 nagyságrenddel kisebb. A ==
ellenőrzés azért hibás, mert a tizedes 0.3-nak nincs pontos
bináris reprezentációja; a kiszámított érték a legközelebbi
float64, ezért a bitminta különbözik attól a
float64-től, amely a 0.3 literált tárolja.
Pénzügyi ellenőrzésnél centben egész számokkal hasonlítsunk, vagy centes
kerekítés után np.array_equal-t, illetve megfelelő
atol/rtol mellett np.allclose-t
használjunk.
import numpy as np
def check_roundtrip(minor_cents: np.ndarray) -> np.ndarray:
eur = minor_cents.astype(np.float64) / 100.0
back = np.rint(eur * 100.0).astype(np.int64)
return np.flatnonzero(back != minor_cents)
rng = np.random.default_rng(20261020)
cents = rng.integers(0, 10_000_000, size=10_000, dtype=np.int64)
bad = check_roundtrip(cents)
print("hibás indexek száma:", bad.size)
print("hibás arány:", bad.size / cents.size)A várt eredmény a levezetés szerint 0 hibás sor, mert a
centes kerekítés elnyeli a float64 reprezentációs hibát. Ez
mégsem jelenti azt, hogy a lebegőpontos út általánosan biztonságos: a
hiba a kerekítés előtti összehasonlításokban, kivonásokban, nagy
értékeknél vagy float32 használatakor jelenhet meg. Az
egész centes út garantáltan pontos, ezért ahol a szerződés centet ír
elő, ott maradjunk int64-nél.
A values == np.nan minden elemre hamis, mert a
nan önmagával sem egyenlő, így clean üres
(0,) shape-ű tömb, az összeg pedig nan lesz.
Javítás:
import numpy as np
values = np.array([100.0, np.nan, 50.0])
clean = values[~np.isnan(values)]
print(clean) # [100. 50.]
print(clean.sum()) # 150.0
print(np.nansum(values)) # 150.0A javított clean shape-je (2,), dtype-ja
float64, és az összeg 150.0. Ellenőrző
invariant: clean csak véges értékeket tartalmaz, azaz
np.isfinite(clean).all() igaz.
Egy 100 dolláros sor abszolút hibája legfeljebb:
Ha 10 millió sor hibája azonos előjelű lenne, az abszolút hiba felső becslése:
Ez elfogadhatatlan. A pontos tárolás int64 pennyben
maradjon; ha lebegőpontos összegzésre van szükség, használjunk
float64-et, akkor soronként
USD, 10 millió sorra pedig legfeljebb
USD, ami elhanyagolható a centhez képest. A valós, véletlenszerű előjelű
hibák részben kiejthetik egymást, de a legrosszabb esetre tervezett
becslés megbízhatóbb, mint a szerencsére hagyatkozás.
A leggyakoribb hiba, hogy a nan-t közönséges számként
hasonlítják, vagy hogy lebegőpontos eredményt bitazonosan ellenőriznek.
Ellenpélda:
import numpy as np
missing = np.nan
print(missing == missing) # False
a = 0.1 + 0.2
print(a == 0.3) # FalseAz első esetben a False nem jelenti, hogy két különböző
értékünk van; a nan definíció szerint soha nem egyenlő
semmivel, még önmagával sem. A második esetben a False
ellenére a számított érték egy cent pontossággal helyes. A helyes szűrés
np.isnan, a helyes numerikus összehasonlítás
np.allclose vagy np.isclose. A
s018-ból ismert shape-ellenőrzés itt is hasznos: mielőtt
toleranciát választunk, győződjünk meg róla, hogy a és
b ugyanazt a (n,) vagy (n,1)
alakot jelenti, különben a broadcasting csendben más összehasonlítást
végezhet. A s015 metrika-moduljának mintájára a centösszeg
definícióját is érdemes egyetlen függvényben tartani, hogy a notebook és
a script ne két különböző kerekítést használjon. Az s003
munkanapló pedig akkor lesz becsületes, ha a futtatott verzió mellett
azt is rögzíti, hogy int64 centút vagy float64
euróút készítette a jelentést.
Önellenőrzés: meg tudom-e mondani, hogy a float64
relatív hibája milyen nagyságrendű? Meg tudom-e indokolni, miért nem
==-gyel ellenőrzök számított lebegőpontos összeget? Ki
tudom-e választani, mikor kell np.isnan a nan
detektálásához? Felismerem-e a csendes int64 túlcsordulást
egy nagy értékű szorzásnál?
A float64 15–17 tizedesjegy pontosságú közelítés, ezért
a tizedes törtek egy része nem ábrázolható pontosan; a
0.1 + 0.2 == 0.3 ezért hamis. Az abszolút hiba
|x^* - x|, a relatív hiba |x^* - x| / |x|, és
a float64 relatív hibája 10^-16 nagyságrendű.
A np.allclose az atol + rtol * |b| képlettel
ad toleráns feltételt. A nan nem közönséges szám, ezért
np.isnan és np.isfinite kell; az
int64 csendesen túlcsordulhat. A Northstar pénzadatot ezért
érdemes egész centben tartani, és csak a megjelenítéskor euróra váltani;
ha lebegőpontos út kell, a hibát nagyságrenddel és toleranciákkal
ellenőrizzük. A következő alfejezetben a vektorizálást és a mért
teljesítményt vizsgáljuk ezekkel az adattípusokkal.