Kennisbank · data-rariteiten

Wanneer 0,1 geen 0,1 is

De decimale waarde 0,1 lijkt exact. In een binair floating-pointgetal is zij meestal een benadering. Dat is geen rekenfout, maar een eigenschap van de gekozen representatie.

Een verrassend eenvoudige som

Vraag een mens om 0,1 en 0,2 bij elkaar op te tellen en het antwoord is vanzelfsprekend 0,3. Laat een programmeertaal dezelfde berekening uitvoeren met binaire floating-pointgetallen en u kunt iets zien als:

0.1 + 0.2 = 0.30000000000000004

De processor heeft niet verkeerd opgeteld. De drie decimale waarden kunnen alleen niet allemaal exact worden weergegeven in een eindig binair floating-pointformaat. De berekening gebruikt daarom de dichtstbijzijnde beschikbare benaderingen.

Meestal is het verschil onschadelijk en zelfs onzichtbaar. Het wordt relevant zodra u waarden exact vergelijkt, afrondingsgrenzen raakt, grote aantallen optelt of geldbedragen verwerkt.

Waarom een tiende binair oneindig doorgaat

In het decimale stelsel is een derde niet eindig te schrijven: 0,3333… Een computer die binaire floating point gebruikt, werkt met machten van twee. Daardoor zijn breuken waarvan de noemer na vereenvoudiging uitsluitend uit factoren 2 bestaat exact eindig weer te geven. Zo zijn 0,5 en 0,25 geen probleem.

De breuk 1/10 bevat ook de factor 5. Haar binaire ontwikkeling gaat daarom oneindig door, net zoals 1/3 dat decimaal doet. Een eindig formaat moet de reeks ergens afbreken en afronden.

Exact binair weer te geven 0,5 = 1/2 0,25 = 1/4 0,125 = 1/8
Binair doorgaande breuk 0,1 = 1/10 0,2 = 1/5 0,01 = 1/100

Floating point is niet onnauwkeurig zonder reden

IEEE 754 beschrijft gestandaardiseerde formaten en bewerkingen voor floating-pointrekenen. Een getal wordt grofweg opgeslagen als een teken, een significand en een exponent. Met een vast aantal bits kan zo een enorm bereik aan zeer grote en zeer kleine waarden worden vertegenwoordigd.

Dat maakt floating point bijzonder geschikt voor wetenschappelijke berekeningen, metingen, grafische toepassingen en statistiek. De prijs voor dat bereik is dat veel waarden slechts bij benadering kunnen worden opgeslagen.

De juiste conclusie is dus niet dat floating point slecht is. Het is een uitstekend datatype voor het doel waarvoor het is ontworpen. Het wordt pas een probleem wanneer een systeem exacte decimale semantiek nodig heeft, maar zonder bewuste keuze een binair benaderingstype gebruikt.

Een klein PostgreSQL-experiment

PostgreSQL maakt het verschil tussen een binaire benadering en een exact decimaal getal direct zichtbaar:

SELECT
    0.1::double precision + 0.2::double precision
        AS floating_point,
    0.1::numeric + 0.2::numeric
        AS exact_decimaal,
    (0.1::double precision + 0.2::double precision)
        = 0.3::double precision
        AS floating_point_is_gelijk,
    (0.1::numeric + 0.2::numeric)
        = 0.3::numeric
        AS numeric_is_gelijk;

De floating-pointsom wordt 0.30000000000000004 en de exacte vergelijking levert false op. De numeric-som wordt 0.3 en de vergelijking levert true op.

Afronden verbergt het verschil, maar lost het model niet op

Een veelgebruikte reactie is om iedere uitkomst meteen af te ronden. Dat kan voor presentatie nodig zijn, maar tussentijds afronden kan een berekening inhoudelijk veranderen. Bij duizenden transacties kan het verschil tussen eerst per regel afronden en pas na de totaalsom afronden materieel worden.

Bovendien bestaan er verschillende afrondingsregels. PostgreSQL rondt bij numeric een waarde die precies halverwege ligt van nul af, terwijl double precision op de meeste systemen naar het dichtstbijzijnde even getal afrondt. De schaal alleen is dus niet de hele afspraak; ook het afrondingsmoment en de afrondingsmethode horen bij de definitie.

Welk datatype past bij welke gegevenssoort?

Gegevenssoort Logische keuze Aandachtspunt
Geldbedrag numeric(p,s) of een integer in de kleinste rekeneenheid Leg valuta, schaal en afrondingsregel expliciet vast.
Wisselkoers numeric met voldoende schaal Bewaar meer decimalen dan de uiteindelijke geldbedragen nodig hebben.
Aantal integer of bigint Gebruik geen floating point voor waarden die per definitie geheel zijn.
Wetenschappelijke meting double precision De meetonzekerheid is doorgaans belangrijker dan exacte decimale representatie.
Percentage of ratio Afhankelijk van het doel Kies numeric voor contractuele exactheid en floating point voor analyse of modellering.

Geld vraagt meer dan twee decimalen

Een kolom numeric(19,2) voorkomt binaire benaderingsfouten, maar vormt nog geen volledige gelddefinitie. Het bedrag 10,00 zegt zonder valuta niet genoeg. Belastingen, kortingen, wisselkoersen en rente kunnen bovendien tussentijds meer decimalen nodig hebben dan het eindbedrag.

PostgreSQL rondt een waarde bij opslag in numeric(p,s) af op de gedeclareerde schaal. Dat kan precies de gewenste regel zijn, maar ook een invoerfout verhullen. Bepaal daarom of te veel decimalen moeten worden afgerond of juist geweigerd.

CREATE DOMAIN bedrag_eur AS numeric
    CHECK (
        VALUE IS NULL
        OR (
            VALUE = round(VALUE, 2)
            AND abs(VALUE) < 100000000000000000
        )
    );

CREATE TABLE factuurregel
(
    factuurregel_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    netto_bedrag    bedrag_eur NOT NULL,
    btw_bedrag      bedrag_eur NOT NULL,
    bruto_bedrag    bedrag_eur NOT NULL
);

In tegenstelling tot numeric(19,2) rondt dit domein te precieze invoer niet stilzwijgend af, maar weigert het waarden met meer dan twee decimalen. Het centraliseert daarmee schaal en bereik. Valuta, afrondingsmoment en de relatie tussen netto, btw en bruto blijven afzonderlijke bedrijfsregels die eveneens expliciet moeten worden vastgelegd.

Vergelijk floating-pointwaarden niet blind op gelijkheid

Bij metingen en rekenmodellen is floating point vaak de juiste keuze. Een exacte gelijkheidsvergelijking is dan niet altijd betekenisvol. Vergelijk waarden binnen een tolerantie die past bij de gegevenssoort en het doel:

SELECT
    abs(gemeten_waarde - verwachte_waarde) <= 0.000001
        AS voldoende_dichtbij
FROM meting;

Een vaste tolerantie van 0,000001 is geen universele oplossing. Bij zeer grote of zeer kleine waarden kan een relatieve tolerantie passender zijn. Ook dit is een inhoudelijke afspraak, geen programmeertruc.

Wat goed datamanagement hier vraagt

  • Definieer de betekenis. Gaat het om geld, een telling, een meting, een percentage of een rekenresultaat?
  • Kies het datatype op semantiek. Laat de standaard van een programmeertaal of importtool niet stilzwijgend beslissen.
  • Leg schaal en bereik vast. Beschrijf hoeveel decimalen betekenisvol zijn en welke waarden geldig zijn.
  • Leg afronding vast. Benoem methode, moment en tussenresultaten.
  • Bewaar eenheden en valuta. Een getal zonder context is geen volledig gegeven.
  • Test de hele keten. Een database kan numeric gebruiken terwijl een API-client de waarde alsnog naar een binair floating-pointtype omzet.

Ook hier geldt het Single Point of Maintenance-principe: definieer de numerieke data class één keer en gebruik dezelfde regels bij invoer, opslag, berekening, uitwisseling en presentatie.

Bronnen en verdere verdieping

  1. IEEE – IEEE 754-2019, Standard for Floating-Point Arithmetic
  2. PostgreSQL – Numeric Types
  3. Python-documentatie – Floating-Point Arithmetic: Issues and Limitations
  4. ECMAScript Language Specification – The Number Type

Een getal is ook een gegevensafspraak

Wilt u weten waar impliciete datatypen, afrondingsregels of conversies de betrouwbaarheid van uw gegevens beïnvloeden? Met de datamanagement quickscan brengt u sterke punten, risico’s en concrete verbeterkansen in beeld.