Logo
Afbeelding

Ouderdomsanalyse & Dubieuze Debiteuren: Analyse met Python

Geschreven door The Audit Analytics | 12 minuten

Iedere organisatie wil natuurlijk snel betaald krijgen voor de producten of diensten die zij levert, maar in de praktijk moet je vaak even wachten. Dan is het wél handig om te weten hoelang je al wacht en of die klant überhaupt nog gaat betalen.

In dit artikel duiken we in de ouderdomsanalyse van debiteuren en laten we stap voor stap zien hoe je met data-analyse (in Python) snel inzicht krijgt in wie op tijd betaalt en wie niet.

Ouderdomsanalyse en dubieuze debiteuren cover

Waarom is deze ouderdomsanalyse relevant in audits?

Voor we beginnen eerst even de vraag: Waarom wil je deze analyse uberhaupt uitvoeren als auditor?

Ten eerste moet een jaarrekening een getrouw beeld geven van de debiteurenpositie op de balansdatum. Daarom controleer je of openstaande posten niet te rooskleurig zijn voorgesteld. Een groot bedrag aan oude, onbetaalde facturen kan betekenen dat de debiteuren (en opbrengsten) mogelijk te hoog zijn weergegeven.

Vaak moet in zulke gevallen een voorziening voor dubieuze debiteuren worden getroffen: dit is een correctie op de waarde van de debiteuren om rekening te houden met verwachte oninbaarheden. Een gangbare praktijk is bijvoorbeeld om facturen die ouder zijn dan 90 dagen (drie maanden) als (gedeeltelijk) dubieus te beschouwen en hiervoor een bedrag af te boeken (bijv. 25% bij >2 maanden, 50% bij >3 maanden, oplopend tot 100% bij >9 maanden achterstallig).

Tijdens een audit wordt doorgaans gekeken naar de openstaande postenlijst op balansdatum en wordt beoordeeld welke van die posten inmiddels (na balansdatum) betaald zijn. Facturen die lang na balansdatum nog open staan, beschouwt men veelal als risico: zo’n post kan als oninbaar afgeboekt moeten worden als deze langer dan een jaar open blijft staan. Je zal hierover in gesprek moeten gaan met het bedrijf en controleren of er een adequate voorziening is getroffen voor deze dubieuze debiteuren.

Kortom, de aging-analyse helpt bij het identificeren van potentiële 'probleem' debiteuren en is een belangrijk instrument om zowel de waardering van debiteuren als het incassorisico in te schatten.

Dubieze debiteuren

Een belangrijk begrip hierbij is 'dubieuze debiteuren'. We spreken van een dubieuze debiteur wanneer het onzeker (dubieus) is of de klant de openstaande factuur nog zal betalen. Met andere woorden: een dubieuze debiteur is een klant waarvan de vordering vermoedelijk oninbaar is.

Dubieuze debiteuren betekenis

Zolang een factuur binnen de normale betalingstermijn valt of slechts kort erna, beschouw je deze als inbaar (incasseerbaar). Pas wanneer een factuur (veel) te lang openstaat of er concrete twijfels zijn over de betaling, boek je deze om van reguliere debiteuren naar dubieuze debiteuren. Mocht uiteindelijk blijken dat een factuur helemaal niet betaald wordt, dan is sprake van een oninbare vordering.

Het verschil tussen een inbare debiteur en een dubieuze debiteur ligt dus in de inschatting van de betaalkans: inbare posten verwacht je alsnog te innen, dubieuze posten acht je waarschijnlijk oninbaar.

Analyse

Dan is het nu tijd voor de analyse. We laten je stap voor stap zien hoe je met Python (pandas) een aging-analyse op openstaande posten uitvoert. We gebruiken daarvoor een (vereenvoudigde) dummy dataset, maar probeer het vooral zelf met je eigen data!

Stap 1: Data inladen

Voor dit voorbeeld creëren we een kleine dataset in Python. In de praktijk zou je deze gegevens uit een financieel systeem exporteren (bijvoorbeeld als CSV/Excel) en met pandas inlezen. Je verwacht wel een minimaal aantal velden die zijn opgenomen in je dataset:

  • Klant – de naam of code van de klant.

  • Factuurdatum – de datum waarop de factuur is uitgereikt.

  • Vervaldatum – de datum waarop de betaling uiterlijk binnen moet zijn (bijv. 30 dagen na factuurdatum). *

  • Bedrag – het openstaande bedrag van de factuur.

*Stel je hanteert als bedrijf altijd een vaste betalingstermijn, dan kun je deze ook zelf berekenen aan de hand van de factuurdatum bijvoorbeeld.

We importeren eerst de pandas-library en maken vervolgens handmatig een DataFrame met voorbeeldgegevens:

import pandas as pd

# Voorbeeld dataset met openstaande facturen
data = [
    {"Klant": "Klant A", "Factuurdatum": "2024-08-01", "Vervaldatum": "2024-09-01", "Bedrag": 4000},
    {"Klant": "Klant A", "Factuurdatum": "2024-10-01", "Vervaldatum": "2024-11-01", "Bedrag": 2500},
    {"Klant": "Klant A", "Factuurdatum": "2024-11-01", "Vervaldatum": "2024-12-01", "Bedrag": 3000},
    {"Klant": "Klant B", "Factuurdatum": "2024-07-01", "Vervaldatum": "2024-08-01", "Bedrag": 5000},
    {"Klant": "Klant B", "Factuurdatum": "2024-10-15", "Vervaldatum": "2024-11-15", "Bedrag": 1500},
    {"Klant": "Klant B", "Factuurdatum": "2024-12-15", "Vervaldatum": "2025-01-15", "Bedrag": 2000},
    # ... (meer klanten en facturen) ...
]

df = pd.DataFrame(data)
df.head()

In een echte situatie zou je wellicht ook kolommen hebben zoals Betaaldatum of een indicator of er al (deels) is betaald. Voor deze aging-analyse volstaan we met openstaande posten. Eventueel betaalgedrag van de klant zullen we afleiden uit hoe laat hun facturen gemiddeld betaald (of niet betaald) worden.

Stap 2: Leeftijd van openstaande posten berekenen

De volgende stap is bepalen hoelang elke factuur al openstaat. We definiëren eerst een peildatum voor de analyse, bijvoorbeeld het einde van het jaar 2024. Ten opzichte van deze peildatum berekenen we voor elke factuur het aantal dagen dat verstreken is sinds de vervaldatum. Dit geeft de ouderdom of achterstalligheid van de post aan in dagen.

In Python kunnen we dat als volgt doen:

# Stel de peildatum (analyse datum) in
peildatum = pd.to_datetime("2024-12-31")

# Converteer datumkolommen naar echte datumtypes
df["Factuurdatum"] = pd.to_datetime(df["Factuurdatum"])
df["Vervaldatum"] = pd.to_datetime(df["Vervaldatum"])

# Bereken het aantal dagen sinds de vervaldatum
df["Dagen achterstallig"] = (peildatum - df["Vervaldatum"]).dt.days

# Als de uitkomst negatief is (vervaldatum na peildatum), zet dit op 0
df["Dagen achterstallig"] = df["Dagen achterstallig"].apply(lambda x: x if x > 0 else 0)

df[["Klant", "Factuurdatum", "Vervaldatum", "Bedrag", "Dagen achterstallig"]].head(6)

We trekken hier de vervaldatum af van de peildatum. Krijgen we een positief getal, dan betekent dit dat de factuur al die hoeveelheid dagen over tijd is. Is het resultaat negatief, dan is de vervaldatum nog in de toekomst (dus de factuur is nog niet vervallen op de peildatum) – in dat geval zetten we het aantal dagen achterstallig op 0.

In de uitvoer zien we nu een nieuwe kolom 'Dagen achterstallig'. Ter illustratie enkele regels uit een dataset:

KlantFactuurdatumVervaldatumBedragDagen achterstallig
Klant A2024-08-012024-09-01€4000121
Klant A2024-10-012024-11-01€250060
Klant A2024-11-012024-12-01€300030
Klant B2024-07-012024-08-01€5000152
Klant B2024-10-152024-11-15€150046
Klant B2024-12-152025-01-15€20000

Laten we dit even duiden. Voor Klant A is de factuur van 1 augustus 2024 ter waarde van €4.000 vervallen op 1 september 2024. Op peildatum 31-12-2024 staat deze dus 121 dagen open (ruim 4 maanden te laat). De factuur van €2.500 (vervaldatum 1-11-2024) is 60 dagen over tijd op 31-12, en de factuur van €3.000 (vervaldatum 1-12-2024) is 30 dagen over tijd. Bij Klant B zien we bijvoorbeeld de factuur van €5.000 met vervaldatum 1-8-2024 152 dagen over tijd op 31-12-2024. Daarentegen heeft Klant B ook een factuur van €2.000 met vervaldatum 15-1-2025; op 31-12-2024 is deze nog niet vervallen, daarom noteren we 0 dagen achterstallig (de betalingstermijn is nog niet verstreken).

Deze kolom 'Dagen achterstallig' vormt de basis voor onze aging-categorieën.

Stap 3: Categoriseren in aging buckets

In de praktijk worden openstaande debiteurenbedragen vaak gegroepeerd in buckets op basis van hun ouderdom. Veelgebruikte buckets zijn bijvoorbeeld: 0-30 dagen, 31-60 dagen, 61-90 dagen, en >90 dagen over tijd. Dit maakt het makkelijker om in één oogopslag te zien hoeveel er relatief recent openstaat en hoeveel al erg lang.

We voegen nu een kolom 'Bucket' toe aan de dataset, gebaseerd op de waarde in 'Dagen achterstallig'. In Python kunnen we de functie pd.cut gebruiken om klassen in te delen:

# Definieer de grenzen (in dagen) en labels van de buckets
bins = [0, 30, 60, 90, float("inf")]
labels = ["0-30", "31-60", "61-90", ">90"]

# Deel de kolom 'Dagen achterstallig' in volgens deze buckets
df["Bucket"] = pd.cut(df["Dagen achterstallig"], bins=bins, labels=labels, right=True, include_lowest=True)

df[["Klant", "Bedrag", "Dagen achterstallig", "Bucket"]].head(6)

De resulterende Bucket-kolom geeft voor elke factuur de leeftijdscategorie. Enkele voorbeelden uit onze data:

KlantBedragDagen achterstalligBucket
Klant A€4000121>90
Klant A€25006031-60
Klant A€3000300-30
Klant B€5000152>90
Klant B€15004631-60
Klant B€200000-30

We zien dat dit klopt: een post die 121 dagen achterstallig is valt in de bucket >90, 60 dagen valt in 31-60, 30 dagen in 0-30, etc. Hiermee hebben we de openstaande posten klaarstaan voor analyse per leeftijdscategorie.

Stap 4: Inzicht in de verdeling (per klant en totaal)

Nu de data per factuur gecategoriseerd is, kunnen we de verdeling van openstaande bedragen over de buckets gaan analyseren. Dit kan op totaalniveau (alle klanten samen) en op individueel klantniveau.

Eerst kijken we naar het totaal. We groeperen de dataset op de Bucket-kolom en tellen of sommeren de bedragen:

# Totaal openstaand bedrag per bucket
totaal_per_bucket = df.groupby("Bucket")["Bedrag"].sum()
print(totaal_per_bucket)

Stel dat dit (fictieve) resultaat voor onze dataset als volgt is:

BucketSum Bedrag
0-30€19.500
31-60€4.000
61-90€6.000
>90€36.000

Deze verdeling laat zien dat in totaal het merendeel van de openstaande posten in de oudste categorie (>90 dagen) valt. Dat is een potentieel risico: €36k aan facturen staat al meer dan drie maanden uit. Een gezonde debiteurenportefeuille heeft idealiter het grootste deel van de openstaande posten in de jongere buckets (dus recent vervallen of nog niet vervallen). Veel in de >90-dagen categorie kan betekenen dat debiteurenbeheer verbeterd kan worden of dat bepaalde vorderingen problematisch zijn.

Vervolgens bekijken we de verdeling per klant. Met een zogeheten pivot_table of multi-index groupby kunnen we per klant en per bucket het openstaande bedrag optellen:

verdeling_klant = df.pivot_table(index="Klant", columns="Bucket", values="Bedrag", aggfunc="sum", fill_value=0)
print(verdeling_klant)

Uit onze dummy data komt bijvoorbeeld het volgende beeld (bedragen per klant per bucket):

Klant A: €3.000 in 0-30 dagen, €2.500 in 31-60, €0 in 61-90, €4.000 in >90.

Klant B: €2.000 in 0-30, €1.500 in 31-60, €0 in 61-90, €5.000 in >90.

Klant C: €0 in 0-30, €0 in 31-60, €0 in 61-90, €20.000 in >90.

Hier valt Klant C duidelijk op: deze klant heeft €20k openstaan en alles zit in de oudste categorie (>90 dagen). Dat duidt erop dat Klant C zeer laat of mogelijk helemaal niet betaalt.

Als auditor geeft zo'n uitsplitsing meteen richting: je kunt de aandacht vooral richten op de klanten en posten in de hoogste aging-categorieën. Bijvoorbeeld, van Klant C wil je weten wat er aan de hand is (zijn er disputen, is de klant in financiële problemen, etc.), en of die €20.000 nog inbaar is of dat hiervoor een voorziening getroffen moet worden.

Stap 5: Detectie van structureel slechte betalers

Tot slot kunnen we een stap verder gaan en kijken naar betaalgedrag per klant over de tijd. We analyseren welke klanten structureel laat betalen, in tegenstelling tot een keertje een late betaling. Met de aging-data kunnen we dit op meerdere manieren doen:

Een eenvoudige aanpak is het berekenen van de gemiddelde ouderdom van openstaande facturen per klant. Dit geeft één getal per klant: hoog betekent dat deze klant gemiddeld erg lang over zijn (onbetaalde) facturen doet.

gemiddelde_dagen = df.groupby("Klant")["Dagen achterstallig"].mean()
slechte_betalers = gemiddelde_dagen[gemiddelde_dagen > 90]
print(slechte_betalers)

Stel dat dit resulteert in:

KlantAvg. Dagen achterstallig
Klant C594.5
Klant E183.0

Hier zien we dat Klant C gemiddeld bijna 595 dagen achterstallig is en Klant E 183 dagen. Deze twee springen eruit boven de 90-dagen grens, wat betekent dat zij structureel erg laat betalen (Klant E heeft weliswaar maar één factuur openstaan in dit voorbeeld, maar die staat extreem lang open). Klant A en B hadden ook een oude factuur, maar doordat zij daarnaast recentere openstaande posten hebben, liggen hun gemiddelden rond ~60-70 dagen.

Een alternatieve benadering is te kijken naar het percentage van de facturen of bedrag per klant dat in de langste bucket valt. In ons voorbeeld had Klant C 100% van zijn facturen in de >90 dagen categorie (2 uit 2 facturen), Klant E 100% (maar 1 uit 1), terwijl Klant A/B/D rond 33% van hun facturen >90 dagen hadden en Klant F 0%. Ook deze metrieken bevestigen dat vooral Klant C – en in mindere mate E – structureel laat betaalt.

In de praktijk kun je betaalhistorie nog nauwkeuriger in kaart brengen door ook werkelijk betaalde facturen te analyseren. Zo kun je per klant de gemiddelde werkelijke betalingstermijn of de mediaan berekenen en klanten vergelijken. In ons voorbeeld zouden we bijvoorbeeld concluderen dat Klant C hoogstwaarschijnlijk een dubieuze debiteur is (wellicht moet hier een voorziening voor getroffen worden), terwijl bij andere klanten de kans op inning beter is.

Conclusie

Dus, we hebben gezien hoe je met Python stap voor stap inzicht kunt krijgen in welke facturen lang openstaan en welke klanten mogelijk een kredietrisico vormen. We begonnen met een eenvoudige lijst van openstaande posten en berekenden de ouderdom van elke post ten opzichte van een peildatum. Vervolgens deelden we de posten in buckets (0-30, 31-60, 61-90, >90 dagen) om de verdeling van de openstaande bedragen te analyseren. Dit bracht direct aan het licht waar de grootste risico’s zitten. Probeer het nu zelf ook eens uit met je eigen data!