Er is een gecombineerde dataset gemaakt door de afgeronde meldingen en de doorgestuurde meldingen samen te voegen. Hieronder zijn enkele terugmeldingen te zien:
80% van de dataset is gebruikt om een Random Forest model mee te trainen. Dit model voorspelt op basis van de omschrijving van de melding op welke registratie de terugmelding wordt gedaan. 20% van de dataset is gebruikt om het model op te testen. Voor elke terugmelding in deze testset is bepaald wat de waarschijnlijkheid (probability) is dat de melding bij een registratie (BAG, BGT of BRT) hoort. De registratie met de hoogste waarschijnlijkheid is de voorspelde registratie van de terugmelding.
# Pre processing
stop_words = stopwords.words('dutch')
stop_words = [word for word in stop_words if word not in ['niet', 'geen']]
def clean_text(x):
x = re.sub(r'\S+@\S+', ' ', x)
x = re.sub(r'http\S+|www\.\S+', ' ', x)
custom_punct = string.punctuation + '—’–…“”²³'
x = re.sub(f"[{re.escape(custom_punct)}]", " ", x)
x = x.lower()
x = re.sub(r'x{2,}', ' ', x)
x = x.strip()
x = nltk.word_tokenize(x, language='dutch')
x = [token for token in x if not token in stop_words]
x = [token for token in x if len(token) > 1]
x = ' '.join(x)
return x
df_combined.loc[:, 'clean'] = df_combined['omschrijving'].apply(lambda x: clean_text(x))
df_combined = df_combined[df_combined['clean']!=""].reset_index(drop=True)
# train-test split
X_raw = df_combined['clean']
labels = df_combined['registratie']
le = preprocessing.LabelEncoder()
le.fit(labels)
y = le.transform(labels)
X_train_raw, X_test_raw, y_train, y_test = train_test_split(
X_raw, y,
test_size=0.2,
random_state=1
)
# Vectoriseren
vectorizer = TfidfVectorizer()
vectorizer.fit(X_train_raw)
X_train = vectorizer.transform(X_train_raw)
X_test = vectorizer.transform(X_test_raw)
target_names = [le.classes_[i] for i in np.unique(y_test)]
# Model laden
with open(r'../modellen/rf_text_classifier_combined_optuna.pkl', 'rb') as f:
model = pickle.load(f)
y_pred = model.predict(X_test)
# Voorspelde percentages per registratie per melding
probs = model.predict_proba(X_test)
class_labels = le.inverse_transform(model.classes_)
df_probs = pd.DataFrame(
probs,
columns=class_labels,
index=X_test_raw.index
)
df_probs['true_class'] = le.inverse_transform(y_test)
df_probs['pred_class'] = le.inverse_transform(y_pred)
# Voorspellingen toevoegen aan test df
df_combined.loc[X_test_raw.index, 'voorspelling'] = [le.classes_[i] for i in y_pred]
df_test = df_combined.loc[X_test_raw.index].copy()
df_test = df_test.join(df_probs[['BAG', 'BGT', 'BRT']])
# Alleen doorgestuurde meldingen
df_test_doorgestuurd = df_test[df_test['doorgestuurd_van_registratie'].notnull()].copy()
test_bag = len(df_test[df_test['registratie']=='BAG'])
test_bgt = len(df_test[df_test['registratie']=='BGT'])
test_brt = len(df_test[df_test['registratie']=='BRT'])
test_fout_bag = len(df_test_doorgestuurd[df_test_doorgestuurd['doorgestuurd_van_registratie']=='BAG'])
test_fout_bgt = len(df_test_doorgestuurd[df_test_doorgestuurd['doorgestuurd_van_registratie']=='BGT'])
test_fout_brt = len(df_test_doorgestuurd[df_test_doorgestuurd['doorgestuurd_van_registratie']=='BRT'])
human_error = {
"BAG": test_fout_bag / test_bag * 100,
"BGT": test_fout_bgt / test_bgt * 100,
"BRT": test_fout_brt / test_brt * 100,
}
thresholds = np.linspace(0.7, 1.0, 31)
results = []
for cls in class_labels:
df_cls = df_probs[df_probs["pred_class"] == cls]
total_cls = len(df_cls)
for t in thresholds:
df_t = df_cls[df_cls[cls] >= t]
total_predicted = len(df_t)
if total_predicted == 0:
results.append({
"registratie": cls,
"threshold": t,
"dekkingspercentage": 0,
"voorspeld": 0,
"fout": 0,
"foutpercentage": 0
})
continue
# Percentage boven threshold van totaal per registratie
coverage = total_predicted / total_cls * 100
# Correct = true_class == deze klasse
correct = (df_t["true_class"] == cls).sum()
# Fout = predicted != true or true != class
wrong = total_predicted - correct
percentage = wrong / total_predicted * 100
results.append({
"registratie": cls,
"threshold": t,
"dekkingspercentage": coverage,
"voorspeld": total_predicted,
"fout": wrong,
"foutpercentage": percentage
})
df_results = pd.DataFrame(results)
def plot_interactive(df_results, metric, ylabel, is_percentage=False, human_error=None, subtitle=None, template="plotly_white"):
fig = px.line(
df_results,
x="threshold",
y=metric,
color="registratie",
markers=True,
template=template,
color_discrete_sequence=px.colors.qualitative.Vivid,
hover_data={"threshold": False}
)
if subtitle:
fig.update_layout(title={"text": f"{ylabel} per threshold per registratie<br><sup>{subtitle}</sup>"})
else:
fig.update_layout(title=f"{ylabel} per threshold per registratie")
is_dark = template == "plotly_dark"
fig.update_layout(
xaxis_title="Threshold",
yaxis_title=ylabel,
legend_title="Registratie",
hovermode="x unified",
autosize=True,
height=500,
width=1000,
hoverlabel=dict(
bgcolor="#111111" if is_dark else "#ffffff",
font=dict(
color="#f8f9fa" if is_dark else "#111111"
),
bordercolor="#444444" if is_dark else "#cccccc"
)
)
fig.update_layout({
'plot_bgcolor': 'rgba(0, 0, 0, 0)',
'paper_bgcolor': 'rgba(0, 0, 0, 0)',
})
color_map = {
trace.name: trace.line.color
for trace in fig.data
if trace.mode == "lines+markers"
}
if is_percentage:
y_format = "%{y:.2f}%"
else:
y_format = "%{y:.0f}"
for trace in fig.data:
if trace.mode == "lines+markers":
trace.hovertemplate = (
"Registratie: %{fullData.name}<br>"
"Threshold: %{x}<br>"
f"{ylabel}: {y_format}<extra></extra>"
)
if human_error is not None:
x_min = df_results["threshold"].min()
x_max = df_results["threshold"].max()
for registratie, value in human_error.items():
fig.add_trace(
go.Scatter(
x=[x_min, x_max],
y=[value, value],
mode="lines",
name=registratie,
line=dict(
color=color_map.get(registratie),
dash="dot",
width=2
),
hovertemplate=(
f"Registratie: {registratie}<br>"
f"Menselijk foutpercentage: {value:.2f}%<extra></extra>"
)
)
)
display(fig)
In de grafiek “Aantal voorspeld ≥ threshold per threshold per registratie” is te zien dat de meeste terugmeldingen voorspeld worden op de BAG en de minste op de BRT. Dit komt overeen met de daadwerkelijke registraties waarop een terugmelding is gedaan. Van de 152.371 terugmeldingen zijn 88.790 terugmeldingen op de BAG (58,3%), 56.094 op de BGT (36,8%) en 7.487 op de BRT gedaan (4,9%).
Voorbeeld: “15.000 terugmeldingen worden op de BAG voorspeld bij een waarschijnlijkheidsdrempel van 75%.”
In de grafiek “Percentage voorspeld ≥ threshold t.o.v. totaal per threshold per registratie” is te zien hoeveel procent van de terugmeldingen op een registratie is voorspeld met een bepaalde waarschijnlijkheidsscore. De lijn van de BAG ligt het hoogste, gevolgd door de BGT en daarna de BRT. De voorspellingen die op de BAG zijn gedaan hebben dus over het algemeen een hogere waarschijnlijkheidsscore dan voorspellingen die op de BGT en BRT zijn gedaan. Een reden hiervoor zou kunnen zijn dat terugmeldingen op de BAG goed te herkennen zijn op basis van de omschrijving en bovendien relatief veel voorkomen. Het model kan hierdoor goed leren hoe een terugmelding op de BAG eruitziet. Voor de BRT zijn er weinig terugmeldingen, waardoor het model hier minder goed in is en het percentage voorspellingen met een hoge waarschijnlijkheid lager ligt.
Voorbeeld: “80% van de terugmeldingen die op de BAG worden voorspeld hebben een waarschijnlijkheidsdrempel van 76%.”
In de grafiek “Aantal foute voorspellingen per threshold per registratie” valt op dat de lijn van de BGT het hoogste ligt, gevolgd door de BAG en daarna de BRT. Hoewel de meeste terugmeldingen op de BAG worden voorspeld en daarna op de BGT, laat deze grafiek zien dat in verhouding tot het aantal voorspellingen het model meer fouten maakt bij de BGT dan bij de BAG.
Voorbeeld: “Van de 15.000 voorspellingen op de BAG worden 200 terugmeldingen (1.3%) verkeerd voorspeld bij een waarschijnlijkheidsdrempel van 75%.”
In de grafiek “Foutpercentage per threshold per registratie” is te zien dat het foutpercentage daalt naarmate de waarschijnlijkheidsdrempel hoger wordt. Dat is logisch, aangezien bij een hogere waarschijnlijkheidsdrempel alleen voorspellingen met hoge zekerheid worden meegenomen. Zoals ook uit de vorige grafiek bleek, ligt het foutpercentage bij de BGT hoger dan bij de BAG. Wat daar nog niet duidelijk zichtbaar was, is dat de BRT het hoogste foutpercentage heeft. Bij de BRT daalt het foutpercentage pas sterk na een waarschijnlijkheidsdrempel van 96%.
Voorbeeld: “4% van de voorspelde terugmeldingen op de BGT wordt verkeerd voorspeld bij een waarschijnlijkheidsdrempel van 78%.”
De stippellijnen in de grafiek geven het menselijke foutpercentage weer en kunnen als referentiewaarden worden beschouwd. Zo is te zien dat 0,69% van de terugmeldingen die op de BAG zijn gedaan is doorgestuurd naar een andere registratie, omdat deze bijvoorbeeld ten onrechte op de BAG zijn gemeld. Het foutpercentage bij de BGT ligt een stuk hoger, namelijk 5,81%. Uit een eerdere data-analyse van de terugmeldingen bleek dat van de 4.023 doorgestuurde terugmeldingen er 3.102 (77%) van de BGT afkomstig waren. Het kwam regelmatig voor dat een terugmelding van de BGT naar de BAG werd doorgestuurd, maar uiteindelijk op beide registraties werd afgerond. Je zou je dus kunnen afvragen of dit in alle gevallen daadwerkelijk als ‘fout’ moet worden beschouwd.
Wanneer de lijnen van het model de stippellijnen kruisen en daaronder komen te liggen, betekent dit dat het AI-model boven die probability threshold betere voorspellingen doet en dus minder fouten maakt dan het huidige systeem. Voor BGT ligt dit omslagpunt al vóór de probability threshold van 70%. Voor een hoge gebruikersacceptatie van AI-suggesties kan het Generieke Geo Services team ervoor kiezen om bij introductie van AI een hogere probability threshold in te stellen (bijvoorbeeld 95%). Hierdoor is minder dan 2% van alle AI-suggesties voor de BGT foutief. In zo’n geval zou voor 3.698 terugmeldingen een AI-voorstel voor BGT aangeboden worden. Voor BAG ligt het omslagpunt rond een probability threshold van 83%. Het foutpercentage is op dit punt slechts 0,68%. Voor 13.309 terugmeldingen zouden AI-suggesties voor de BAG worden gedaan. Voor BRT ligt het omslagpunt pas bij een probability threshold van ongeveer 96%. Onder deze drempel maakt het model relatief meer fouten dan het huidige systeem, maar bij een probability threshold van 97% is minder dan 1% van alle AI-suggesties voor de BRT foutief. In zo’n geval wordt voor 237 terugmeldingen een AI-voorstel voor BRT aangeboden.
Er zitten 833 doorgestuurde meldingen in de testset. 540 hiervan (64.8%) worden door het model wel goed voorspeld, op de registratie waarnaar de terugmelding is doorgestuurd. Hieronder zijn een aantal terugmeldingen te zien die zijn voorspeld op de registratie waar de terugmelding naar is doorgestuurd.
Van de 293 terugmeldingen die verkeerd worden voorspeld worden 271 terugmeldingen (92.5%) voorspeld op de oorspronkelijk gemelde registratie. Een verklaring hiervoor zou kunnen zijn dat een aanpassing nodig is geweest op zowel de oorspronkelijke registratie als op de registratie naar waar doorgestuurd is. Hieronder zijn een aantal terugmeldingen te zien die zijn voorspeld op de oorspronkelijk gemelde registratie.
De grafiek hieronder laat zien hoeveel doorgestuurde meldingen een AI-voorstel krijgen per registratie bij verschillende thresholds. Bij een threshold van 0,83 voor de BAG krijgen 142 meldingen een AI-voorstel. Dit is 37,9% van de voorspellingen op de BAG en 17% van alle doorgestuurde meldingen in de testset. Bij een threshold van 0,95 voor de BGT krijgen 26 meldingen een AI-voorstel. Dit is 7,2% van de voorspellingen op de BGT en 3,1% van alle doorgestuurde meldingen in de testset. Bij een threshold van 0,97 voor de BRT krijgen 15 meldingen een AI-voorstel. Dit is 15,2% van de voorspellingen op de BRT en 1,8% van alle doorgestuurde meldingen in de testset.
results_doorgestuurd = []
for cls in class_labels:
df_cls = df_test_doorgestuurd[df_test_doorgestuurd["voorspelling"] == cls]
total_cls = len(df_cls)
for t in thresholds:
df_t = df_cls[df_cls[cls] >= t]
total_predicted = len(df_t)
if total_predicted == 0:
results_doorgestuurd.append({
"registratie": cls,
"threshold": t,
"dekkingspercentage": 0,
"voorspeld": 0,
"goed": 0,
"fout": 0,
"foutpercentage": 0
})
continue
# Percentage boven threshold van totaal per registratie
coverage = total_predicted / total_cls * 100
# Correct = true_class == deze klasse
correct = (df_t["registratie"] == cls).sum()
# Fout = predicted != true or true != class
wrong = total_predicted - correct
percentage = wrong / total_predicted * 100
results_doorgestuurd.append({
"registratie": cls,
"threshold": t,
"dekkingspercentage": coverage,
"voorspeld": total_predicted,
"goed": correct,
"fout": wrong,
"foutpercentage": percentage
})
df_results_doorgestuurd = pd.DataFrame(results_doorgestuurd)
In de grafiek “Aantal foute AI-voorstellen per threshold per registratie” is te zien dat met name de BGT verkeerd wordt voorspeld. We zagen al eerder dat een deel van de doorgestuurde meldingen ook op de oorspronkelijke registratie zijn afgerond met aanpassing. Er is bijvoorbeeld een terugmelding gedaan op de BGT, maar het object waar het over gaat staat ook nog niet in de BAG, dus het wordt doorgestuurd naar de BAG, maar op beide registraties wordt een aanpassing gedaan.
In de grafiek “Aantal foute voorspellingen per threshold per registratie” onder het kopje “Threshold analyse op basis van voorspelmodel” is te zien hoeveel nieuwe fouten per registratie worden geïntroduceerd bij verschillende thresholds. Voor de gekozen thresholds van 0,83 voor de BAG, 0,95 voor de BGT en 0,97 voor de BRT zijn dat respectievelijk 91, 69 en 2 fouten, in totaal dus 162. In de grafiek “Aantal correcte AI-voorstellen per threshold per registratie” hieronder is te zien hoeveel doorgestuurde meldingen terecht een AI-voorstel hebben gekregen. Bij dezelfde threshold waardes zijn dat 128 (BAG), 18 (BGT) en 15 (BRT) opgeloste ‘fouten’, in totaal 161. Voor de BAG en BRT worden daarmee meer fouten opgelost dan nieuwe fouten geïntroduceerd. Voor de BGT is dat niet het geval, maar zoals hierboven geconstateerd, wordt 92,5% van de verkeerd voorspelde meldingen toegekend aan de oorspronkelijk gemelde registratie, waardoor slechts 7,5% van de verkeerd voorspelde meldingen daadwerkelijk aan een andere registratie wordt toegekend dan oorspronkelijk gemeld.