Ik woon ongeveer naast de plas waar de laatste tijd wat ophef over is. Het gaat over de uiterwaarden van de Maas in de gemeente West Maas en Waal, het project ‘Over de Maas’. Daar vinden stortingen plaatst van materiaal om de plas, nadat er waardevol zand is gewonnen , weer ‘ondiep’ te maken. Onderzoeksprogramma Zembla heeft een uitzending aan gewijd omdat zijn daar een luchtje aan vinden zitten. Daar vindt Rijkswaterstaat dan weer wat van.
Vorige week trof mij een tweet van GroenLinks kamerlid Suzanne Kroger waarin ze aangeeft 2GB aan WOB documenten te hebben gekregen en het nu zelf moet uitzoeken. Handmatig 1430 documenten lezen is een beetje veel werk inderdaad, dus laten we kijken of we met wat datascience en machinelearning een handje kunnen helpen.
Kunnen we de beschikbaar gestelde documenten automatisch lezen en rubriceren in onderwerpen zodat mensen snel en efficiënt de belangrijkste documenten over een bepaald onderwerp kunnen vinden en lezen.
Staatssecretaris Van Veldhoven informeert de Tweede Kamer over de (deel)besluiten op enkele verzoeken op grond van de Wet Openbaarheid van Bestuur (Wob) over granuliet. Hierin een link naar de data. Dit blijkt een gezipte folder met al dan niet deels “afgelakte” pdf documenten. Bij nader inzien is het grootste deel ingescand en dus niet doorzoekbaar op tekst.
Dus aan de slag!
Ik werk al tientallen jaren op een linux computer en daar heb ik met dit soort zaken veel profijt van. De unix/linux filosofie bestaat er uit dat er heel veel kleine gereedschapjes zijn die een bepaald klusje doen en dat goed doen. Dus laten we eens kijken wat we nodig hebben… Spaties in filenamen zijn altijd lastig dus laten we die vervangen door een underscore.
find ./data/ -depth -name "* *" -execdir rename 's/ /_/g' "{}" \;
De regel hierboven kijkt in de subdirectory ‘./data’ en vervangt alle spaties in alle file en directory namen door underscores.
Ok dan eens naar de kwaliteit van de scans kijken, blijkt nog wel wat issues op te leveren.
Als je hierin uitzoomt zie je welke uitdaging de Optical Character Recognition(OCR) tools hebben om hier leesbare tekst van te maken. Maar laten we het proberen, in linux hebben we de beschikking over python script “ocrmypdf” een handig tool dat pdf scans leest en er txt van maakt. Dit kun je zo gebruiken:
ocrmypdf --force-ocr test2-mail-zembla.pdf output.pdf -l nld
Met als resultaat
Hierin komen al direct interessante zaken naar voren, bijvoorbeeld dat de OCR techniek moeite heeft met het lettertype dat hier is gebruikt… Het woordje “granuliet” wordt alleen in deze email al 3x anders gelezen (“gramiet”, “gramet”,”gramin”)…
Ok dus nu hebben we de 1433 documenten bewerkt en txt documenten terug gekregen die niet 100% netjes zijn “vertaald” door de OCR. Kunnen we toch kijken of een textmining algoritme er iets nuttigs mee kan doen?
Een veel gestelde vraag in dit kader is vaak : “Waar gaat dit over?”
Met een topic model kun je onderzoeken welke woorden relevant zijn in een document en kijken welke woorden overde verschillende documenten samen bij elkaar horen en een “onderwerp” (topic) vormen, zie wikipedia voor meer achtergrond.
Voor deze oefening gaan we een corpus aanleggen van alle documenten in de directory die we van de staatssecretaris hebben gekregen. Deze corpus gaan we opschonen en bewerken zodat we relevante woorden overhouden van ieder document(we verwijderen de meeste ‘stopwoorden’ zoals “de”, “het”,,”een”, getallen en leestekens.
Daarna maken we een zogenaamde document term matrix , das handig omdat we daarmee allerlei slimme berekeningen kunnen maken.
Laten we kijken hoe we dat in mijn favoriete datascience tool R kunnen programmeren :
# LDA textmining op de granuliet weob dataset
library(topicmodels)
library(tm)
# read only txt
path = "~/Downloads/11-Documenten-toe-te-voegen-aan-besluit"
source = DirSource(path,recursive = TRUE, pattern='.txt$')
docs <- Corpus(source)
# use Dutch stopwords
stop_words = stopwords("nl")
docs <- tm_map(docs, stripWhitespace)
docs <- tm_map(docs, removePunctuation)
docs <- tm_map(docs, removeNumbers)
docs <- tm_map(docs, content_transformer(tolower))
docs <- tm_map(docs, removeWords, stop_words)
# inspect corpus
inspect(docs[[1]])
dtm <- DocumentTermMatrix(docs)
rowTotals <- apply(dtm , 1, sum) #Find the sum of words in each Document
dtm <- dtm[rowTotals > 0, ] #remove all docs without words
Het resultaat is een matrix waarmee we kunnen rekenen.
Een van de algoritmes die veel gebruikt wordt voor topic models is Latent Dirichlet Allocation kortweg LDA .
Vertaling door onze grote broer:
“De latente Dirichlet-toewijzing (LDA) is een generatief statistisch model waarmee verzamelingen van waarnemingen kunnen worden verklaard door niet-geobserveerde groepen die verklaren waarom sommige delen van de gegevens vergelijkbaar zijn. Als waarnemingen bijvoorbeeld woorden zijn die in documenten zijn verzameld, betekent dit dat elk document een combinatie is van een klein aantal onderwerpen en dat de aanwezigheid van elk woord kan worden toegeschreven aan een van de onderwerpen van het document. LDA is een voorbeeld van een onderwerpmodel en behoort tot de machine learning toolbox en in bredere zin tot de kunstmatige intelligentie toolbox.”
Laten we kijken wat dat aan “onderwerpen” oplevert, tijd voor wat code
# package lda
library(lda)
lldaformat <-dtm2ldaformat(dtm , omit_empty = TRUE)
documents <- lldaformat$documents
vocab <- lldaformat$vocab
# Compute some statistics related to the data set:
D <- length(documents) # number of documents (2,000)
W <- length(vocab) # number of terms in the vocab (14,568)
doc.length <- sapply(documents, function(x) sum(x[2, ])) # number of tokens per document [312, 288, 170, 436, 291, ...]
N <- sum(doc.length) # total number of tokens in the data (546,827)
# compute the table of terms:
term.table <- table(unlist(documents))
term.table <- sort(term.table, decreasing = TRUE)
term.frequency <- as.integer(term.table) # frequencies of terms in the corpus
vocab2 <- names(term.table)
# MCMC and model tuning parameters:
K <- 20
G <- 5000
alpha <- 0.02
eta <- 0.02
# Fit the model:
library(lda)
set.seed(357)
t1 <- Sys.time()
fit <- lda.collapsed.gibbs.sampler(documents = documents, K = K, vocab = vocab,
num.iterations = G, alpha = alpha,
eta = eta, initial = NULL, burnin = 0,
compute.log.likelihood = TRUE)
t2 <- Sys.time()
t2 - t1 # about 24 minutes on laptop| 6 min on pcloud
Ok, dus nu hebben we een topic model…
Voor het interactief visualiseren van de onderwerpen kunnen we gebruik maken van een mooi pakket dat het resultaat van de lda model visualiseert.
# interactief visualizeren lda topics
library(LDAvis)
# create the JSON object to feed the visualization:
json <- createJSON(phi = WOBgranuliet$phi,
theta = WOBgranuliet$theta,
doc.length = WOBgranuliet$doc.length,
vocab = WOBgranuliet$vocab,
term.frequency = WOBgranuliet$term.frequency)
# make gist
serVis(json, out.dir = 'vis', open.browser = TRUE, as.gist = TRUE)
En het mooie is dat we deze visualisatie via een gist online beschikbaar kunnen maken zodat de gebruiker zelf de onderwerpen kan onderzoeken
Dus mocht je een interessant onderwerp zien er de bijbehorende documenten willen lezen, hier een lijst met de top vijf documenten per onderwerp:
Topic 1
[1] “95._20191010_107a_doc_07_Schreurs_Milieuconsult_Aanv_informatie_toepassing_beveiligd_01.pdf.txt”
[2] “385._20200122_524_ZN_HH_Doc_Bijlage_1_document_Scheurs_bev.pdf.txt”
[3] “139._20191025_826_ZN_HH_Doc_Bijlage_Milieuhyg_toets_Granuliet_v2_20191009_beveiligd.pdf.txt”
[4] “260._20191118_695_ZN_HH_Doc_Bijlage_Milieuhygienische_toetsing_Granuliet_beveiligd.pdf.txt”
[5] “20191010_30.1_WVL_bestand.pdf.txt”
Topic 2
[6] “463._20200210_478_ZN_HH_Doc_Bijlage_1_20-100_bev.pdf.txt”
[7] “459._20200210_478_ZN_HH_Doc_Bijlage_3_19-100_bev.pdf.txt”
[8] “433._20200205_499_ZN_HH_Doc_Bijlage_2_12-100_bev.pdf.txt”
[9] “435._20200205_499_ZN_HH_Doc_Bijlage_1_1-100_(002)_1bev.pdf.txt”
[10] “455._20200210_478_ZN_HH_Doc_Bijlage_2_21-100_bev.pdf.txt”
Topic 3
[11] “7._20130213_05_MN_Memo_anoniem_vertroebeling_granuliet_beveiligd_1.pdf.txt”
[12] “227._20191109_742_ZN_HH_Doc_Bijlage_2_STOWA_2016-14_01_beveiligd.pdf.txt”
[13] “83._20190930_854_ZN_HH_Doc_Bijlage_2.2_tijdelijk-handelingskader_beveiligd_01.pdf.txt”
[14] “20._D_WNZ_Bijlage_2_bev.pdf.txt”
[15] “3._AB-ZN_Wmvergunning_2008_bev.pdf.txt”
Topic 4
[16] “164._20191029_809_ZN_HH_Foto_Bijlage_8_IMG_4775.jpg.txt”
[17] “195._20191030_1203.1_ZNHH_foto1.pdf.txt”
[18] “10._2018_04_04_0000_107843924___Bestand2_RWSD2018-00039954_referenties_opgegeven_door_Dekker_Futerra-LAK.pdf.txt”
[19] “2._2018_03_25_BSB_Granuliet_IZG-039-2_Bijlage_bij_Nieuwe_melding_voorgenomen_toepassing__445595_0_RWSD2018-00-LAK.pdf.txt”
[20] “195._20191030_1203.1_ZNHH_foto1_1_beveiligd.pdf.txt”
Topic 5
[21] “149._20191029_809_ZN_HH_Foto_Bijlage_6_IMG_4767.jpg.txt”
[22] “354._20200114_556_ZN_HH_Doc_Bijlage_Aanvoer_Granuliet_2019-2020_bev.pdf.txt”
[23] “368._20200116_537_ZN_HH_Doc_Bijlage_5_13175535_cresolen_bev.pdf.txt”
[24] “358._20200116_537_ZN_HH_Doc_Bijlage_4_13175535_BBK_vl_bev.pdf.txt”
[25] “223._20191107_752_ZN_HH_Doc_Bijlage_13136695_fenolen_bbk_beveiligd.pdf.txt”
Topic 6
[26] “228._20191109_742_ZN_HH_Doc_Bijlage_1_STOWA_1995-17_01_beveiligd.pdf.txt”
[27] “185._20191030_781_ZN_HH_Doc_Bijlage_STOWA_1995-17beveiligd.pdf.txt”
[28] “227._20191109_742_ZN_HH_Doc_Bijlage_2_STOWA_2016-14_01_beveiligd.pdf.txt”
[29] “20200205_265_BS_email_RE_Bijlage_bij_brief_DGWB_dd_10102019_beveiligd.pdf.txt”
[30] “140._20191025_825_ZN_HH_Doc_Bijlage_RIVM__711701075_beveiligd.pdf.txt”
Topic 7
[31] “3._AB-ZN_107250285___watervergunning_RWSD2016-00048805_bev.pdf.txt”
[32] “3._AB-ZN_watervergunning_bev.pdf.txt”
[33] “10._D_ZN-Over_de_Maas_RWSD2016-00048805__bev.pdf.txt”
[34] “Thumbs.db.txt”
[35] “10._D_ZN-_Over_de_maas_2016-28109_bev.pdf.txt”
Topic 8
[36] “1._Overzicht_zandwinplassen_ON_tbv_SVNL.pdf.txt”
[37] “1.1_AB-ZN_107000935___RWS-20154102_DMS2015-00000010618_bev.pdf.txt”
[38] “1._AB-ZN_bijlage_3_hydraulica_KRW3_Maasbommel_bev.pdf.txt”
[39] “1.1_AB-ZN_107000934___RWS-20154102_DMS2015-00000000000930_bev.pdf.txt”
[40] “1.1_AB-ZN_107000930___RWS-20154102_DMS2015-00000000000926_bev.pdf.txt”
Topic 9
[41] “2._AB-ZN_108192703___4213155_1550165647019_RWSD2019-00019805_bev.pdf.txt”
[42] “2._AB-ZN_108247235___Tekening_-_revisie_vergunning.pdf_bev.pdf.txt”
[43] “2._AB-ZN_108273507___4213155_1550165647019_Tekening_-_revisie_vergunnin.pdf_bev.pdf.txt”
[44] “2._AB-ZN_108273509___4213155_1550165647019_Tekeningrevisie_vergunnin.pdf_bev.pdf.txt”
[45] “2._AB-ZN_108286852___Bijlage_bij_2019057429,_tekening_18410040_d.d._11_februari_2019.pdf_bev.pdf.txt”
Topic 10
[46] “20191029_198_BS_email_RE_Dossier_granuliet__beveiligd.pdf.txt”
[47] “20191029_226_BS_email_FW_Dossier_granuliet_beveiligd.pdf.txt”
[48] “20191030_167_WVL_email.pdf.txt”
[49] “234._20191109_88_ZN_mail_FW_Over_granuliet_beveiligd.pdf.txt”
[50] “241._20191112_730_ZN_HH_Mail_RE_Over_granuliet_bev.pdf.txt”
Topic 11
[51] “20190619_111.2_bestand.pdf.txt”
[52] “20190619_1.2_WVL_bestand.pdf.txt”
[53] “55._20190617_889_ZN_HH_Doc_Bijlage_memo__Granuliet_versie_13_juni_2019_beveiligd_1.pdf.txt”
[54] “20190702_114.1_WVL-bestand.pdf.txt”
[55] “2019_06_28_28_WVL_bestand.pdf.txt”
Topic 12
[56] “20200109_50.1_WVL_bestand.pdf.txt”
[57] “2015_09_02_46.1_WVL_Bestand.pdf.txt”
[58] “154._20191029_176a_ZN_VV_doc_certificaat_granuliet_beveiligd.pdf.txt”
[59] “179._20191029_90a_ZN_VV_doc_certificaat_granuliet_beveiligd.pdf.txt”
[60] “273._20191120_672_ZN_HH_Doc_Bijlage_3_BSB-certificaat_Granuliet_IZG-039-2_beveiligd.pdf.txt”
Topic 13
[61] “140._20191025_825_ZN_HH_Doc_Bijlage_RIVM__711701075_beveiligd.pdf.txt”
[62] “27092019_218_WVL_SMS_Splitthoff.pdf.txt”
[63] “1._AB-ZN_bijlage_2_ontwerp_maasbommel_bev.pdf.txt”
[64] “20190809_217_BS_email_Granuliet_memo2_beveiligd.pdf.txt”
[65] “278._20191121_76_mail_FW_Granuliet_in_‘Over_de_Maas’_bev.pdf.txt”
Topic 14
[66] “018._2019_01_07_Bezwaarschrift_advocaat_namens_GIB.pdf.txt”
[67] “269._20191119_692_ZN_HH_Mail_RE_resultaten_onderzoek_granuliet_beveiligd.pdf.txt”
[68] “224._20191107_752_ZN_HH_Mail_RE_eerste_resultaten_monstername_beveiligd.pdf.txt”
[69] “373._20200120_529_ZN_HH_Mail_RE_eerste_analyseresultaten_bev.pdf.txt”
[70] “211._20191104_765_ZN_HH_Mail_FW_Polyacrylamide_bev.pdf.txt”
Topic 15
[71] “5._D_ZN-Grensmaas_Nh-Gb-Kw-Vi_2018_49803_bev.pdf.txt”
[72] “5._D_ZN-_Grensmaas_Nh-Gb-Kw-Vi_2018_37550_bev.pdf.txt”
[73] “5._D_ZN-Grensmaas_Nh-Gb-Kw-Vi_2018_21830_bev.pdf.txt”
[74] “5._D_ZN-Grensmaas_Nh-Gb-Kw-Vi_2019_17489_bev.pdf.txt”
[75] “5._D_ZN-Grensmaas_Nh-Gb-Kw-_Vi_2017_38393_bev.pdf.txt”
Topic 16
[76] “4._20120702_01_WNN_certificaat_beveiligd_1.pdf.txt”
[77] “5._20120813_04_MN_Bijlage_meldingsformulier_analyseresultaten_beveiligd_1.pdf.txt”
[78] “219._20191106_755_ZN_HH_Doc_Bijlage_3_Certificaat_13136652_beveiligd.pdf.txt”
[79] “364._20200116_537_ZN_HH_Doc_Bijlage_1_Certificaat_13175485_bev.pdf.txt”
[80] “207._20191104_291a_ZN_doc_synlab_13136695_fenolen_beveiligd.pdf.txt”
Topic 17
[81] “20._D_WNZ_Bijlage_3_bev.pdf.txt”
[82] “20._D_WNZ-_Bijlage_1_bev.pdf.txt”
[83] “19._D_WNZ_Aanvullen_afdekken_Oude_Maas_Spijkenisserbrug_bev.pdf.txt”
[84] “20._D__WNZ_Projectplan_bestorting_Spijkenisserbrug_bev.pdf.txt”
[85] “20._D_WNZ_Bijlage_2_bev.pdf.txt”
Topic 18
[86] “451._20200207_11b_ZN__doc_brief_West_Maas_en_Waal_aan_TK_der_Staten_Generaal_bev.pdf.txt”
[87] “448._20200207_11a_ZN__doc_brief_West_Maas_en_Waal_aan_minister_bev.pdf.txt”
[88] “20200207_102_WVL_bestand.pdf.txt”
[89] “20200207_102.1_WVL_bestand.pdf.txt”
[90] “20200206_027_BS_mail_Pre-RTM_Zembla_over_’afvaldump_door_Rijkswaterstaat’_beveiligd.pdf.txt”
Topic 19
[91] “125._20191018_258_ZN_VV_mail_FW_Beoordeeld_Gewijzigde_melding_5043911_beveiligd.pdf.txt”
[92] “20161003_Melding_besluit_bodemkwaliteit._Meldingsnummer__360166.1.pdf.txt”
[93] “444._20200207_15b_ZN_vv_doc_Melding_360166.0__bev.pdf.txt”
[94] “20160329_Melding_besluit_bodemkwaliteit._Meldingsnummer__360166.0.pdf.txt”
[95] “15._20130314_09_MN__Inboekingsmail_gewijzigde_melding_beveiligd_1.pdf.txt”
Topic 20
[96] “31012019_222_WVL_SMS_Splitthoff.pdf.txt”
[97] “3._AB-ZN_107765971___3589533_1522935857692_OVEM01-6-5001A_Beheerzonering_A3.pdf_bev.pdf.txt”
[98] “264._20191119_684_ZN_HH_Doc_Bijlage_1_aanpassing_BBK_ivm_PFAS_(002)_beveiligd.pdf.txt”
[99] “268._20191119_684_ZN_HH_Mail_Aanvullende_vragen_Over_de_Maas__PFAS_beveiligd.pdf.txt”
[100] “283._20191125_646_ZN_HH_Mail_RE_toepassing_Over_de_Maas_bev.pdf.txt”
Een vraag die ik recent ontving was of en hoe we de tijd mee kunnen nemen in deze visualisatie. Wat blijkt is dat de datum in de filenamen zit voor 1200 van de 1400 documenten. Als we Term Document matrix sorteren op de datum van de documenten kunnen we de termen per topic gebruiken voor een visualisatie.
Ik weet niks van dit onderwerp (granuliet storting en dergelijke), we hebben wat documentjes gekregen en er een algoritme op los gelaten. Zeker er is nog heel veel ruimte voor verbetering en de onderwerpen die hier gepresenteerd zijn hebben verder geen enkele betekenis. Maar het is toch leuk om te laten zien dat er met een paar regels datascience code een in mijn ogen best bruikbare data visualisatie is te maken waarmee we ons voordeel kunnen doen.
Mocht je meer willen weten, neem dan contact op met Hugo Koopmans.