Hier geven we een voorbeeld hoe je makkelijk met een Python script data van OpenData (StatLine) kan halen en een plot kan maken in de CBS huisstijl.
We maken gebruik van de StatLineTable klasse uit de cbs_utils.readers module. Deze klasse is een toevoeging op de cbsodata module (het maakt voor het inlezen gebruik van cbsodata). Met StatLineTable kan je de data in een gestructureerde pandas dataframe stoppen, zodat het bewerken en plotten een stuk eenvoudiger gaat.
We delen dit voorbeeld op in vier secties:
We beginnen met het inladen van de benodigde modules en het initialiseren van de logger
%matplotlib inline
import matplotlib.pyplot as plt
import logging
import sys
from cbs_utils.misc import (create_logger, merge_loggers)
from cbs_utils.plotting import (CBSPlotSettings, add_axis_label_background)
from cbs_utils.readers import StatLineTable
logging.basicConfig(format='%(levelname)s : %(message)s', level=logging.INFO, stream=sys.stdout)
logger = logging.getLogger()
Het inladen van een tabel uit opendata is eenvoudig. We moeten eerst de ID van een tabel vinden door naar statline te gaan en je tabel te openen. Bijvoorbeeld, het ICT gebruik van bedrijven in 2018 naar bedrijfsgrootte staat in deze tabel
https://opendata.cbs.nl/#/CBS/nl/dataset/84410NED/table?ts=1568706226304
Dit betekent dat de tabel ID '84410NED' is. Deze gaan we nu inlezen
%%time
table_id = "84410NED"
logger.info(f"Start met het lezen van tabel {table_id}")
statline = StatLineTable(table_id=table_id)
Je ziet dat er 'under the hood' een hoop gebeurt:
De eerste keer dat we de script draaide duurde het zo'n 7 seconden, de tweede keer een fractie van een seconde. Dit tijdsvoordeel is vooral prettig als je de script vaak herdraait om een plaatje te 'tunen'
Als volgende stap gaan we bekijken hoe de data structuur van de vragenlijst eruit ziet. Zoals gezegd, dit kan je ook zien door de inhoud van QuestionTable.txt in the image directory te bekijken. Maar we kunnen het ook direct opvragen:
statline.show_module_table()
We zien hier een lijst van modules, submodules, en vragen. Een vraag kan weer uit meerder opties (aanvink mogelijkheden) bestaan. Een module heeft in de structuur het hoogste niveau binnen een tabel: heeft is een blok vragen die allen bij één onderwerp horen. Modules in deze lijst kan je herkennen omdat ze geen parentID hebben: ze zijn zelf het hoogst in de hiërarchie. De modules in deze tabel zijn dus:
Een module heeft als volgende niveau of een vraag of een submodule. Dit kunnen we zien door de structuur van de hele vragenlijst te printen. Dit doen we als volgt:
statline.show_question_table(max_width=23)
De eerste kolom geeft de ID van de vragen en de positie in de hierchie van de tabel. We zien in de eerste tabel dat de module 46 (ICT-veiligheid) als eerste een vraag heeft, namelijk 47 (gebruikte ICT maatregelen), en dat deze vraag 47 weer twaalf onderdelen heeft, namelijk optie 48 tot en met 59, die allemaal als ParentID 47 hebben. Dit is een voorbeeld van een vraag op niveau 1 (als we het module niveau als niveau 0 zien), met de opties op niveau 2.
Uit de eerste tabel zien we dat E-commerce (ID 97) als eerste een submodule bevat, 98 (gebruikt voor verkoop). Dit zien we omdat in de tweede tabel, 98 nooit aan een variabele toegekent wordt. Het is 99 (Verkoop via e-commerce) dat de eerste stanalone vraag is die een parent 98 heeft. Daarom dat deze vraag, die in een submodule zit, op niveau 2 komt. De Volgende vraag 100 is weer een vraag met optie. De vraag 100 is in de eerste tabel terug te vinden (Verdeling omzet: eigen site of extern), maar omdat deze vraag weer optie heeft, zitten deze optie weer op niveau 3: 101 en 102.
Deze structuur zit wel in de json files beschreven, maar StatLineTable maakt er een multi-index pandas data frame van, zodat je makkelijk de vragen en modules die bijelkaar horen kan processen (dit in tegenstelling tot de pandas dataframe die door cbsodata geretourneerd wordt. Dit is gewoon een platte lijst van variables, waardoor het lastig wordt om de vragen die bijelkaar horen te groeperen.
Naast het aanbrengen van structuur, zorgt StatLineTable er ook voor dat de volledige beschrijving van een variable en de eenheden in de pandas DataFrame terug te vinden zijn (in respectievelijk de 2de en 3de kolom).
Het belangrijkste wat je hier moet weten is hoe je kan opzoeken welke ID bij welke module wordt, zodat je deze nummers later kan gebruiken om een selectie van modules te maken.
Bovenstaande klinkt misschien complex, maar het doel van StatLineTable is juist om het eenvoudiger te maken een plaatje van de vragen te maken. Het maken van plaatjes kan dus intern door StatLineTable gedaan worden. Deze plaatjes zijn met default settings opgemaakt, dus het is niet geschikt voor publicatie, maar vooral voor een snelle blik op al je data in de tabel. Het maken van plaatjes van al je vragen gaat als volgt:
statline = StatLineTable(table_id=table_id, make_the_plots=True, modules_to_plot=[1, 46])
Nadat we klaar zijn hebben we een plaatje van 11 vragen geplot. Dit is niet alles, omdat we een lijst van modules die we willen plotten hebben meegegeven: 1 (Personeel en ICT) en 46 ICT-veiligheid. De nummers die je aan de lijst meegeeft hebben we in de eerste tabel boven terug kunnen vinden door het runnen van de show_module_table method (waar de modules de items zijn die geen ParentID hebben). Als de het argument modules_to_plot niet meegegeven hadden, dan hadden we een dump alle vragen gemaakt. Dat doen we hier maar even niet omwille van het beperken van de uitvoer.
Als je de plaatjes als png wilt saven dan moet je de optie save_plot=True meegeven. Dan kan je de plaatjes terug vinden in de directory images/84410NED, welke automatisch aangemaakt wordt mocht deze nog niet bestaan.
Het dumpen van de vragen als plots is handig om een overzicht van je data te krijgen, maar echt mooi zijn de plots niet. Dit komt omdat we de layout niet getuned hebben, en bovendien omdat voor iedere vraag de waardes voor alle grootteklasses geplot is. We gaan nu laten zien hoe we de data van één vraag kunnen plotten voor een selectie van grootteklases.
Eerst willen we kijken welke grootteklasses we allemaal tot ons beschikking hebben. We doen dit als volgt:
statline.show_selection()
De waardes van deze grootteklasses worden aan de selection_option attribute toegevoegd. Stel dat we voor de plot alleen de grootteklasses 2, 20 tot 50, en 500 of meer werkzame personen willen laten zien dan moeten we dus respectievelijk het 3de (index=2), 7de (index=6) en laatste (index=-1) item hebben. Laten we deze waardes er eens uit plukken:
selection = [statline.selection_options[2],
statline.selection_options[6],
statline.selection_options[-1]]
logger.info(f"We hebben de volgende grootteklasses geselecteerd:\n{selection}")
We hadden bij het initialiseren van de StatLineTable klasse deze selectie als argument mee kunnen geven als selection=selection, met daarnaast ook nog de apply_selection=True flag meegegeven. Maar in Python kan je de waardes ook nog achteraf toekennen. Laten we dat nu doen:
statline.selection = selection
statline.apply_selection = True
Doordat we de selectie aan de selection attribute hebben toegekend, zullen we dadelijk bij het reorganiseren van de data alleen nog de waardes van deze grootteklasses krijgen.
Voor het opvragen van een dataframe voor één vraag kunnen we de get_question_df method gebruiken. Bijvoorbeeld, stel we willen vraag met ID 47 plotten. Aan de eerste tabel die we met show_module_table lieten zien vinden we terug dat deze vraag hoort bij de vraag Gebruikte ICT-veiligheidsmaatregelen. Verder liet de 2de tabel die we met show_question_table maakten zien dat deze vraag 12 keuze mogelijkheden had. We trekken nu als volgt de vraag uit de tabel:
question_df = statline.get_question_df(47)
question_df.head(12) # dit laat de eerste twaalf rijen uit de data frame zien
De eerste twaalf regels van de dataframe laten zien dat we nog alle grootteklasse in de Dataframe terugvinden (we beginnen immers met de 2 of meer werknemers items). In totaal moeten we dus 12 * 11 = 132 in de dataframe hebben. Dit kunnen we met info zien
question_df.info()
Inderdaad hebben we 132 entries. Voordat we de dataframe klaar maken om te plotten, slaan we eerst even de eenheden van de data op. Deze moet als het goed is voor alle entries hetzelfde zijn omdat ze allemaal bij dezelfde vraag horen. Dus we doen
units = question_df[statline.units_key].values[0]
logger.info(f"De eenheden zijn: {units}")
Inderdaad, de vraag voor gebruikte ICT veiligheids maatregelen heeft % van bedrijven.
Als laatste stap voordat we gaan plotten, gaan we de data frame reorgiseren:
question_df = statline.prepare_data_frame(question_df)
question_df.head(12)
Door het runnen van prepare_data_frame wordt de selectie die we eerder aan de selection attribute toegekend hebben uitgevoerd, en bovendien worden de grootteklasses nu als kolommen weergeven. Deze data frame is geschikt om te plotten.
Allereerst kunnen we de plot settings voor een standaard CBS plot makkelijk instellen met de klasse CBSPlotSettings. Deze klasse stelt de figuurgrootte in en kiest het juiste kleurenschema. Dit doen we nu:
Door het laden van de CBSPlotSettings klasse wordt het koele kleuren schema gezet. Je kan het warme palet kiezen door de optie color_palette="warm" mee te geven, maar je niks meegeeft dan is de default palet koel.
plot_settings = CBSPlotSettings()
We hebben de data die we willen plotten dus in de question_df dataframe staan. Dit is een standaard pandas data frame, dus met het aanroepen van de plot method kunnen we al een plaatje maken. Echter, we willen de plot ook gaan tunen. Daarom is het handiger om buiten pandas om de figuur en assen te maken en deze aan de plot method mee te geven, zodat we de waardes daarna makkelijk kunnen aanpassen. We beginnen dus met het initialiseren van de figure en axis:n
fig, axis = plt.subplots(dpi=72) # dpi is noodzakelijk om het wegschrijven naar pdf goed te krijgen
fig.subplots_adjust(left=0.5, bottom=0.25, top=0.98)
Dit is nog niet zo spannend. Enige belangrijke is dat we de marges aangepast hebben. We gaan namelijk een horizontale bar plot maken, zodat we aan de linker kant ruimte nodig hebben voor de labels van de staven. Daarom dat we left=0.5 zetten (0 is vanaf links geen marge, 1 is vanaf links alleen maar marge tot de rechter as, dus 0.5 is een marge die de helft van de plot breedte inneemt. Ook aan de onderkant vergroten we de marge wat omdat we de legend later kwijt moeten. Aan de bovenkant verkleinen we de marge juist. Rechts laten we op de default staan.
We hebben door het gebruik van de subplots functie uit de matplotlib.pyplot module gelijk toegang tot de figuur en assen. Dus laten we nu de plot method van pandas gebruiken om een horizontale staafdiagram te maken in de assen die we zojuist gemaakt hebben
question_df.plot(kind="barh", ax=axis)
fig
We zie dat we inderdaad een liggend bar plot hebben met voldoen ruimte voor de labels links. Ook het kleurenschema is al wat we nodig hebben. Echter, we moeten nog wel wat tunen. We halen eerste de y-as label weg (die is default 'Title') en stellen de x-as label gelijk aan de units die we net opgeslagen hebben in units
axis.set_ylabel("")
axis.set_xlabel(units)
axis.xaxis.set_label_coords(0.98, -0.1)
Als default wordt de x-as label in het midden geplaatst. Met de set_label_coords method kunnen we deze wat meer naar rechts zetten (de coordinaten zien in fraxies van de assen, links onder is 0,0, rechtboven is 1,1)
Nu gaan we de randen van de assen aanpassen. Deze worden default alle vier getekend, maar we willen alleen de linker rand overhouden. Voor de gridlijnen nemen we alleen de lijnen vanaf de x-grid
for side in ["top", "bottom", "right"]:
axis.spines[side].set_visible(False)
axis.spines['left'].set_position('zero')
axis.tick_params(which="both", bottom=False, left=False)
axis.xaxis.grid(True)
axis.yaxis.grid(False)
We zien verder dat de x-range iets verder dan 100 doorloopt. Voor percentage willen we dat gewoon op 100 laten eindigen. Dus stel ook de x-range in, maar laat hem tot 101 doorlopen. Dit om te voorkomen dat de laatste gridlijn op 100 wegvalt
axis.set_xlim([0, 101])
Een andere eigenaardigheid is dat de items van de bar plot precies andersom geplot worden als dat we in de question_df dataframe hadden. Daar hadden we 'Antivirussoftware' op de eerste regel, en 'Andere maatregelen' als laatste, maar dit wordt precies omgekeerd geplot. We draaien dat om met invert_yaxis
axis.invert_yaxis()
Ten slotte gaan we de legenda wat aanpassen. Deze is boven op de bar plot gezet. We willen deze aan de onderkant hebben. Daarom doen we:
axis.legend(bbox_to_anchor=(0.01, 0.00), ncol=2, bbox_transform=fig.transFigure, loc="lower left", frameon=False)
Hier volgt een uitleg van de argumenten van de legend method:
Eens kijken hoe het plaatje er nu uitziet:
fig
We zijn er bijna. Om er een echt CBS plaatje van te maken willen we de linkerzijde onder de labels nog een grijsvlak met ronde hoekjes plaatsen. Bovendien moet er nog een CBS logo toegevoegd worden. Om dit te doen kan je gebruik maken van de add_axis_label_background functie uit de cbs_utils.plotting module
add_axis_label_background(fig=fig, axes=axis)
fig
Hiermee zijn we klaar met de plot. Als je de plot als PDF naar file weg schrijft kan je hem met de beste kwaliteit (namelijk als vectorformaat) in latex inlezen
fig.savefig("maatregelen.pdf", bbox_inches = 'tight')
De optie bbox_inches = 'tight' is alleen in Jupyter notebook noodzakelijk om de marges van de grafiek in de pdf goed te krijgen. Voor een normale script moet je tight juist weg laten omdat het de posities van je labels kan aanpassen.