Tweede-kamerleden

Afgelopen zaterdag was er in Nieuwsweekend een aardig gesprek met Kees Boonman, waarin onder andere gesproken werd over het gegeven dat slechts weinig tweede-kamerleden buiten de Randstad wonen. Op zich is dit niet zo vreemd, want Nederland heeft natuurlijk geen districtenstelsel zoals in de VS of het VK. Maar het wierp bij mij wel de vraag op: waar wonen die tweede-kamerleden zoal… Die vraag was de aanleiding voor een aangename middag code schrijven.

Geruimte tijd geleden heb ik een harverster geschreven voor Funda. Dat was een behoorlijk complex ding geworden, want dat was ook de uitdaging die ik me toen had gesteld. Nu wilde ik gewoon even snel die data hebben, dus ik besloot een eenvoudige scraper in Python te schrijven. Een site waar de betreffende gegevens staan was snel gevonden.

Om het lezen van die pagina door het script wat te versnellen, haalde ik hem eerst maar eens naar mijn locale machine. Dit zou er ook voor zorgen dat ik geen last zou krijgen van een eventuele IP-block, hoewel me dat bij een dergelijke site niet heel waarschijnlijk leek. Voor het binnenhalen van de html maakte ik gebruik van requests; het parsen deed ik met html/lxml.

Het overzicht van de kamerleden is vanzelfsprekend gegenereerde code, wat ook duidelijk bleek uit de html view van de pagina. Ik begon dus maar met één persoon binnen te halen, met het idee dat naderhand te veralgemeniseren. Een eerste stap was om eens te kijken wat voor XPath FireFox zelf van zo’n kaartje maakte:


Het resultaat hiervan was /html/body/div[1]/main/div[2]/div/section/div/div[2]/div[1]/div. Dat werkt natuurlijk wel, maar was wel erg specifiek. Het ging mij om de data die staat binnen de div met als klasse ‘card’. Dat was natuurlijk een eenvoudige opgave:

kamerleden = tree.xpath("/html/body//div[@class='card']")
print (len(kamerleden))

Het resultaat hiervan bleek 151 te zijn, waar we eigenlijk 150 zouden verwachten. Die ene afwijking zou ik later nog wel uitzoeken.

De data die ik van die site kon (en wilde) ophalen was de volledige naam, de partij, de woonplaats, de leeftijd, de anciënniteit en de url naar de foto. Niet alles was voor het eigenlijke vraagstuk even relevant, maar ik was er nou toch. Om te beginnen kon ik de onderstaande XPath gebruiken om de volledige naam te achterhalen. Een opvallend verschijnsel was dat het resultaat hiervan ['Aalst, R.R. van'] was, hoewel die naam op die manier geschreven in het hele html-document niet voorkomt.

test = kamerleden[0]
naam = test.xpath("div[1]/div[1]/div[1]/a[1]/text()")

Dit werkte wel, maar ik vond de XPath wel erg specifiek en besloot dit wat te veralgemeniseren. Omdat die test feitelijk een sub-tree is van de originele html, zou je verwachten dat je via een wildcard binnen deze subtree zou moeten kunnen zoeken. Maar wanneer ik test.xpath("//a[@class='member__name']") deed, kreeg ik weer alle 151 resultaten terug.

Nadat ik hier te lang mee had geëxperimenteerd, besloot ik dan maar eieren voor m’n geld te kiezen en de concrete XPath naar de verschillende data-elementen binnen de card-div uit te schrijven – dat was niet zo gek veel werk en ik wilde gewoon die data hebben.

Ik maakte dus een eenvoudige loop door de kamerleden die ik al eerder had gedefinieerd en haalde de specifieke data uit elke iteratie. Omdat die methode xpath() een lijst (van als het goed is één element) teruggeeft, haal ik telkens het eerste element hiervan op. Ik voegde ook even een tellertje bij, wat een goed idee bleek; hierdoor kwam ik er al snel achter dat de loop bij zevenenveertigste iteratie ophield.

ctr = 0;
for lid in kamerleden:
  ctr += 1
  url = lid.xpath("div[1]/div[1]/img[1]/@src")[0]
  naam = lid.xpath("div[1]/div[1]/div[1]/a[1]/text()")[0]
  partij = lid.xpath("div[1]/div[1]/div[1]/span[1]/text()")[0]
  woonplaats = lid.xpath("div[1]/table[1]//tr[1]/td[2]/text()")[0]
  leeftijd = lid.xpath("div[1]/table[1]//tr[2]/td[2]/text()")[0]
  ancien = lid.xpath("div[1]/table[1]//tr[3]/td[2]/text()")[0]

Na wat inspectie bleek dat de zevenenveertigste persoon in de lijst geen woonplaats had opgegeven. Verder onderzoek wees uit dat dit drie personen betrof. De meest eenvoudige oplossing hiervoor was natuurlijk de hele boel in een try-catch-block te zetten en bij een exceptie de woonplaats op ‘onbekend’ te zetten (wat ook past binnen het Python-adagium dat toestemming vragen lastiger is dan sorry zeggen).

for lid in kamerleden:
  ctr += 1
  try:
    # doe al die dingen zoals hierboven
  except:
    woonplaats = 'onbekend'

Er staan twee data in dat overzicht die berekend zijn: de leeftijd en de anciënniteit. De leeftijd wordt weergegeven in aantal jaren, dus het is lastig om hier een specifieke datum uit te destilleren (om dat voor elkaar te krijgen zouden we de pagina van de bewindpersoon in kwestie moeten bezoeken om de geboortedatum op te halen – leuk om te doen, maar nu even niet). De anciënniteit wordt weergegeven in aantal dagen, dus dat is eenvoudig terug te rekenen. Daarvoor moest ik natuurlijk wel even het woord ‘dagen’ uit die string halen. Het terugrekenen naar de datum waarop de persoon in kwestie is begonnen laat ik dan door mysql zelf doen (date_sub met interval).

Omdat ik toch bezig was die data wat op te schonen, haalde ik ook de GET-variabel uit de url van het plaatje, en ook het woordje ‘jaar’ uit de leeftijd (dan zou ik ook gelijk gemiddelde leeftijd en zo uit kunnen rekenen).

Omdat ik het te veel gedoe vond om de data via python zelf in mysql te stoppen, besloot ik gewoon insert-statements uit te printen. Die kon ik dan later in het proces wel pipen naar een bestand en die weer inlezen. Het totale plaatje werd dan als volgt:

for lid in kamerleden:
  ctr += 1
  try:
    # doe al die dingen zoals hierboven
  except:
    woonplaats = 'onbekend'

  str = 'insert into kamerleden values ("{}","{}","{}","{}",{},date_sub(date(now()), interval {} day));'.format(url,naam,partij,woonplaats,leeftijd,ancien)
  print (str)

print ("In totaal {} leden.".format(ctr))

Nu was het tijd om de data in mysql te laden. Het datamodel was natuurlijk bijzonder eenvoudig (zie hieronder), maar er bleek nog wel een probleem te zijn met de naam van VVD tweede-kamerlid Yeşilgöz-Zegerius. Hoewel alle verdere encoding goed leek te gaan, ging mysql over z’n nek bij de ş. Blijkbaar is utf-8 niet de juiste encoding voor dergelijke karakters, maar dankzij onze trouwe vriend stackoverflow kwam ik er snel genoeg achter dat je gebruik moet maken van utf8mb4. Ik moest nog wel even de hele tabel opnieuw definiëren omdat deze wijziging blijkbaar geen effect heeft op tabellen die al gemaakt zijn, maar daarna stond eindelijk alle data er goed in.

Resultaten
Omdat dit project ontstaan was door de vraag naar de woonplaatsen van de tweede-kamerleden besloot ik eerst hier maar eens naar te kijken. Het bleek dat de leden in 69 verschillende woonplaatsen wonen, waarbij de top tien niet heel veel verrassingen herbergt:

select count(distinct woonplaats) as wp from kamerleden;
+----+
| wp |
+----+
| 69 |
+----+
1 row in set (0,00 sec)

select woonplaats,count(woonplaats) as aantal from kamerleden group by woonplaats order by aantal desc limit 10;
+------------+--------+
| woonplaats | aantal |
+------------+--------+
| Amsterdam  |     23 |
| Den Haag   |     18 |
| Utrecht    |      9 |
| Groningen  |      6 |
| Rotterdam  |      5 |
| Breda      |      4 |
| Haarlem    |      4 |
| Middelburg |      3 |
| Leiden     |      3 |
| Leeuwarden |      3 |
+------------+--------+
10 rows in set (0,00 sec)

Het enige opvallende aan deze tabel is dat Groningen op de vierde plek staat, terwijl dat de achtste stad van het land is. Verder zien we toch best nog wel wat steden van buiten de Randstad in de top tien.

En omdat we toch die leeftijd hadden meegenomen, konden we daar ook even wat data uit ophalen:


select min(leeftijd), max(leeftijd),avg(leeftijd) from kamerleden;
+---------------+---------------+---------------+
| min(leeftijd) | max(leeftijd) | avg(leeftijd) |
+---------------+---------------+---------------+
|            27 |            76 |       45.9400 |
+---------------+---------------+---------------+

De gemiddelde leeftijd van de kamerleden valt gelukkig nog wel mee.

De database is hier te downloaden.

Leave a comment

Your email address will not be published. Required fields are marked *