CBS open data interface in R gebruiken (1)

Shows how to use the CBS open data interface in R.

Note 22-01-2015: zie het commentaar over een probleem met jsonlite en de workaround.  De code, met updates, staat inmiddels permanent op mijn github: https://github.com/MrOoijer/R-work/tree/master/openCBS

CBS biedt sinds kort een open data interface naar “al” zijn data. Het protocol is gebaseerd op Versie 3 van odata.org.

CBS kent twee toegangen: een voor applicaties, en een voor bulk bevragingen. Het gekke is dat deze ingangen default een verschillend formaat gebruiken om het antwoord terug te geven. De applicaties-ingang geeft JSON terug, de bulk-ingang gebruikt op ATOM gebaseerd XML. Op het eerste gezicht heel erg helaas, want deze ATOM is (nagemeten!)  zo’n 700% inefficiënter qua ruimte. Voorts is het is ook vreselijk lastig te verwerken als je geen goede parser hebt. Die is er wel voor algemeen gebruik van XML binnen R, maar ik heb geen kant-en klare oplossing voor ATOM gevonden, en om die nu zelf te schrijven, nee….

De JSON-interface weigert dienst bij meer dan 10.000 records, de ATOM-interface geeft er maximaal 10.000 tegelijk, maar je kunt om meer vragen. Gelukkig echter kun je met een extra argument achter de url de default ATOM omzetten naar JSON en werkt het daarna precies hetzelfde. Het staat nergens op de CBS-site genoemd – het heeft me dus wel een paar dagen proberen gekost voordat ik de hier onder getoonde simpele oplossing had. Het R-pakket “jsonlite” doet al het vertaalwerk voor ons.

CBS-tabellen worden  normaal gesproken niet vaker dan eens per maand ververst, en dus gebruik ik normaal gesproken eens per maand een nieuwe kopie. De “gewone” call van

data_table= open_api_CBS(CBS_table_name)

haalt dus bij voorkeur de die maand al gecachte versie op. Een nieuwe versie wordt geforceerd opgehaald door new=TRUE toe te voegen.  In de code zelf wordt eerst een lijst met interface-URL’s opgehaald, dan (evt. herhaald) de TypedDataSet, en vervolgens worden van de gecodeerde kolommen versies toegevoegd waarin de codering is vervangen door de betekenis. De data types van de TypedDataSet sluiten goed aan bij de data.frames van R. Er is geen conversie nodig. Er zijn nog wel wat overblijvende eigenaardigheden weg te werken, maar die behandel ik een volgende keer.

Code:

library(jsonlite)
library(RCurl)
##
## call CBS feed or get cached table
##
##
open_feed_api_CBS  =  function(table_name, new=FALSE, progress=FALSE){
        #
        api_base = "http://opendata.cbs.nl/ODataFeed/odata/"
        my_cache="./CBSdata/"
        file_name=paste0(my_cache, table_name,sprintf("_%s.dat", format(Sys.time(), "%Y_%b")))
        force_json= "?$format=json"
        #
        if (new == TRUE || ! file.exists(file_name)){
                api_url = paste0(api_base,table_name, force_json)
                my_data  =  getURL(api_url)
                my_json  = fromJSON(my_data)
                api_table  = my_json$value
                table_url  =  api_table$url[api_table$name=="TypedDataSet"]
                url= paste0(table_url, force_json)
                first=TRUE; iter=1
                while (TRUE){
                        # read api interface
                        my_data  =  getURL(url)
                        my_json  = fromJSON(my_data)
                        if( length(my_json$value) == 0) break
                        if( first == FALSE) {data_table =  rbind(data_table, my_json$value)}
                        if( first == TRUE) {
                                data_table  =  my_json$value
                                first= FALSE
                        }
                        if (length(my_json) < 3) break
                        url=my_json[[3]]
                        if ( progress == TRUE){
                                cat(sprintf("\r..%d\r", iter)) ### shows progress during testing 
                                iter=iter+1
                        }
                }        
                # replace coded fields key with titles
                # ... but use new column name
                #
                for (i in 5:dim(api_table)[1]){
                        Naam= api_table$name[i]
                        Naam2= paste0(Naam, "_C")
                        url= paste0(api_table$url[i], force_json)
                        my_data  =  getURL(url)
                        my_json  = fromJSON(my_data)
                        key_data  =  my_json$value
                        for(k in 1:dim(key_data)[1]){
                                data_table[data_table[, Naam] == key_data$Key[k], Naam2] = key_data$Title[k]
                        }
                }
                write.table(data_table, file=file_name, row.names=FALSE)
                return(data_table)
        }
        return(read.table(file_name, header=TRUE, stringsAsFactors = FALSE))
}

4 Comment

  1. Erwin says: Reply

    Hallo,

    Ik heb je code geprobeerd om een tabel in te laden, maar krijg een foutmelding “Error in parseJSON(txt) : lexical error: invalid character inside string.”

    samentelling van categorieën: A Landbouw, bosbouw en visse
    (right here) ——^

    Enig idee? Het lijkt met encoding te maken hebben…

    Bedankt alvast!

    Erwin

    1. MrOoijer says: Reply

      Sorry dat ik zo laat reageer. De library jsonlite heeft een andere engine gekregen in de laatste versie. Kennelijk komt het daardoor. Nader onderzoek leert dat op de plaats van de fout een \r\n in de tekst van de description stond, en daar kan die nieuwe engine kennelijk niet tegen. Waarom weet ik niet, het lijkt een bug.

      Ik heb wat geexperimenteerd en er is voorlopig een workaround: vervang in bovenstaande code elke getURL(url) door readLines(url). Je krijgt dan wel wat warnings over onvolledige regels, maar het werkt wel weer.

      Succes er mee!

  2. Maarten says: Reply

    Normaal doe ik ‘t nooit, maar bij deze: ontzettend bedankt!

    De truc zit ‘m vooral in de “?$format=json”, de XML is te weinig generiek en daardoor echt gekkenwerk om te ontleden…

    Weet je trouwens of CBS IP bans uitdeelt na unfair use van de API? Andere nadelen ondervonden door de jaren heen?

    1. MrOoijer says: Reply

      Hoi Maarten – ik heb hem nauwelijks meer gebruikt de laatste tijd; maar toevallig gisteren op de tabel “71013ned”, en toen faalde het stukje for (i in 5:dim(api_table)[1])

      Bij deze tabel moet het vanaf 6. de eerste 5 zijn hier speciaal. Waarom moet ik nog uitzoeken maar daar heb ik nog geen tijd voor gehad.

      Van een ban heb ik nooit iets gemerkt.

      Groet, Jan

Leave a Reply to MrOoijer Cancel reply