dapla-toolbelt-whodat

Sist endret

October 17, 2025

dapla-toolbelt-whodat er en Python-pakke som gir statistikere mulighet for å søke etter fødselsnummer (FNR) basert på hjelpeopplysninger - for eksempel navn, adresse eller kommunenummer. Funksjonaliteten kan hjelpe statistikere å beholde rader som ellers hadde blitt forkastet, fordi man har manglende eller feil identifikator i datasettet. dapla-toolbelt-whodat bygger på Skatteetatens folkeregister-søk og er utviklet av team statistikktjenester.

Siden tilgang til direkte identifiserende opplysninger er underlagt strenge regler krever bruken av dapla-toolbelt-whodat at man forholder seg til vedtatte standarder som datatilstander og systemer som Kildomaten. I tillegg er det en streng tilgangsstyring til hvor man kan kalle funksjonaliteten fra.

Forberedelser

Før man tar i bruk funksjonaliteten er det viktig at man kjenner godt til tilgangstyring i Dapla-team og Kildomaten, og har diskutert med seksjonen hvordan man skal behandle direkte identifiserende opplysninger i de aktuelle dataene.

For at et Dapla-team skal kunne bruke dapla-toolbelt-whodat må Kildomaten være skrudd på for miljøet1 man ønsker å jobbe fra. Som standard får alle statistikkteam skrudd på Kildomaten i prod-miljøet og ikke i test-miljøet. Ønsker du å aktivere Kildomaten i test-miljøet kan dette gjøres selvbetjent som en feature.

Tilgangsstyring

Tilgang til funksjonalitet i dapla-toolbelt-whodat er i seg selv sensitivt. Tjenesten tilgangsstyres derfor strengt. I prod-miljøet kan man kun ta i bruk funksjonaliteten ved å prosessere dataene i Kildomaten. Det er bare tilgangsgruppen data-admins som har tilgang til å godkjenne slike automatiske prosesseringer. I test-miljøet, derimot, kan alle på teamet benytte seg av all funksjonalitet siden det aldri skal forekomme ekte data.

Tabell 1: Tilgangsstyring til dapla-toolbelt-whodat
(a) Test-miljø
Aktør FNR-leting
Kildomaten
data-admins (interaktivt)
developers (interaktivt)
(b) Prod-miljø
Aktør FNR-leting
Kildomaten
data-admins (interaktivt) 🚫
developers (interaktivt) 🚫

Vi ser fra Tabell 1 (a) at man i test-miljøet har full tilgang til funksjonaliteten i dapla-toolbelt-whodat, både fra Kildomaten og når man jobber interaktivt2 i Jupyterlab. Tabell 1 (b) viser at det kun er tilgang til FNR-leting fra Kildomaten i prod-miljøet, og man kan ikke kan bruke biblioteket interaktivt, da det potensielt kan avsløre samtlige FNR. Av den grunn er det alltid anbefalt å teste ut koden sin i test-miljøet før den produksjonssettes i prod-miljøet med Kildomaten.

Installering

dapla-toolbelt-whodat er ferdig installert i Kildomaten. Ønsker du å bruke den i test-miljøet til teamet kan du installere det i et ssb-project fra PyPI med denne kommandoen:

Terminal
poetry add dapla-toolbelt-whodat

Spesifikasjon

Dataformater

dapla-toolbelt-pseudo støtter innlesing av følgende dataformater:

Eksemplene under viser hovedsaklig innlesing av dataframes fra minnet.

Kolonner og opsjoner

I dapla-toolbelt-whodat konstruerer man en DataFrame med hjelpevariabler man skal bruke for søket (eksempler under). Da er det veldig viktig at kolonnenenavnene er riktige, og at formatet er helt likt som i tabellen under.

Warning

NB: Alle variabler må være strengverdier i den faktiske DataFramen. Tall-typer støttes ikke.

Tabell 2: Variabler
Kolonnenavn Format Kommentar
navn Et eller flere hele ord fra personnavnet, skilt med mellomrom. Tegnsetting: “Åse J. Ås” og “Günther” er gyldig, men ikke “مُحَمَّد”
kjoenn ‘mann’ eller ‘kvinne’
foedselsdato “YYYYMMDD”
foedselsaarFraOgMed “YYYY” Laveste fødselsår
foedselsaarTilOgMed “YYYY” Høyeste fødselsår
adressenavn Minst 3 tegn fra begynnelsen av gatenavn
husnummer Husnummer, med eller uten bokstav
postnummer Fire siffer
kommunenummer Fire siffer
fylkesnummer To siffer


Opsjoner er verdier som endrer søket, og utføres for hver rad.

Tabell 3: Opsjoner
Opsjon Format Default-verdi Kommentar
inkluder_oppholdsadresse true eller false false Treffer oppholdsasdresse i tillegg til bostedsadresse
soek_fonetisk true eller false false Søk på fonetisk lignende navn
inkluder_doede true eller false false Søket treffer også døde personer
opplysningsgrunnlag “gjeldende” eller “historisk” “gjeldende”. Styrer håndtering av historikk. Påvirker kun navn og adresse – for andre opplysninger søkes det alltid kun på gjeldende

Arbeidsflyt

En vanlig arbeidsflyt i Kildomaten for å bruke dette biblioteket vil være følgende:

  1. Bruke Validator()-funksjonalitet for å finne ugyldige FNR (Dokumentasjon)
  2. Hent ut alle rader fra den originale DataFramen med ugyldig FNR funnet i steg 1.
  3. Transformer alle hjelpevariablene til formatet beskrevet i Tabell 2, og endre navn på kolonnene.
  4. Utfør et FNR-søk basert på hjelpevariablene.
  5. Erstatt de ugyldige FNRene med gyldig FNR
  6. Pseudonymiser FNRene
Warning

Fra steg 2-5 er det veldig viktig at man tar vare på indeksen fra den originale DataFramen! Fordi man gjør et utdrag av DataFramen i steg 2, må man også kunne finne tilbake til de originale radene når man skal erstatte ugyldige FNR i steg 5. Ikke bruk Pandas’ df.reset_index() eller lignende i mellomtiden.

Polars har ikke en innebygd indeks, og man burde opprette en indeks-kolonne før man gjør utdrag av DataFramen i steg 2 med df.with_row_index()

I repoet whodat-examples finner man eksempler. Repoet inneholder en notebook man kan kjøre i Dapla Lab Test, med rollen dapla-felles-developers. Repoet inneholder også en notebook med et Kildomaten-eksempel

Eksempler

Enkle eksempler

FNR-leting følger et såkalt builder-pattern der man spesifiserer hva og i hvilken rekkefølge operasjonene skal gjøres. Anta at det finnes en Pandas dataframe i minnet som heter df hvor man skal gjøre et enkelt søk på “navn”-variabelen. Da vil koden se slik ut:

Enkelt søk
from dapla_whodat import Whodat
import pandas as pd

# Vi 
df = pd.DataFrame(
  {
    "navn": ["Donald Duck", "Dolly Duck", "Onkel Skrue"]
    "adressenavn": ["Lundlia", "Smøyatunvegen", "Simmenesvegen"]
  }
)

result = (
    Whodat.from_pandas(df)
    .search_fnr()
    .with_search_strategy(["navn"])
    .run()
)

Etter importering av pakker begynner vi med å lage en Pandas DataFrame med variablene navn og adresse.

Vi bruker følgende metoder fra pakken vår Whodat:

  • from_pandas(df): angir at hjelpevariablene vi ønsker å bruke for søket ligger i en Pandas DataFrame (df).

  • search_fnr(): angir hva vi skal gjøre

  • with_search_strategy(): forteller hvordan vi skal søke. I eksempelet søker vi på ["navn"].

    • man kan gjøre fonetisk navnesøk på hver rad ved å angi soek_fonetisk = True slik: .with_search_strategy(["navn"], soek_fonetisk = True)
  • run(): metoden som faktisk utfører søket.

Avanserte eksempler

Hente ut FNR
from dapla_whodat import Whodat

result = (
    Whodat.from_pandas(df)
    .search_fnr()
    .with_search_strategy(["navn"])
    .run()
)

found_personal_ids = result.to_list()

to_list() returnerer en liste av FNRer som er funnet.

Warning

Det blir kun returnert et FNR når søket gir et unikt treff. Søk som returnerer flere FNR, eller ingen FNR, vil bli erstattet med None.

La oss se på et eksempel:

Eksempel Vi har en DataFrame med 3 rader, og vi kjører koden over. Anta at:

  • Søk på den første raden med navn gir ett enkelt FNR.
  • Søk på den andre raden gir flere treff.
  • Søk på den tredje raden gir ingen treff.

Resultatet hadde da sett slik ut:

Eksempel ouput
found_personal_ids = result.to_list()

print(found_personal_ids)
> ["11111123456", None, None]

Her vil da radnummer være tilsvarende til plasseringen i listen, slik at første rad gir et gyldig FNR på det første elementet i listen mens andre og tredje rad returnerer None.

Flere søk

Vi kan også legge til flere søk for hver enkelt rad i en DataFrame. Dette ser slik ut:

from dapla_whodat import Whodat

result = (
    Whodat.from_pandas(df)
    .search_fnr()
    .with_search_strategy(["navn"])
    .with_search_strategy(["navn", "adressenavn"])
    .run()
)

I koden over har vi lagt til et søk som også søker på adressenavn. Dette endrer søkemetoden slik:

For hver rad, hvis vi får et unikt treff på det øverste søket (kun navn), returner det FNRet. Hvis ikke, gå videre til neste søk (navn og adressenavn). Repeter denne prosessen for hvert kall med with_search_strategy() til man enten har funnet et unikt FNR, eller alle søk har blitt forsøkt. Hvis man da ikke har funnet et unikt FNR, returner None.

Vi fortsetter med utgangspunkt i scenariet fra Eksempel over, men nå med koden over. Vi har nå fått returnert dataene:

found_personal_ids = result.to_list()

print(found_personal_ids)
> ["11111123456", "22222209876", None]

Når vi kjørte koden tidligere fikk vi None på det andre elementet i listen fordi vi fikk mer enn ett FNR. Siden vi nå har snevret inn søket ved å legge til en adressevariabel inner vi nå et unikt treff.

Likedan fikk vi, med den tidligere koden, returnert None for det tredje elementet, fordi vi ikke fikk treff på noen FNR. La oss se om vi kan finne det:

from dapla_whodat import Whodat

result = (
    Whodat.from_pandas(df)
    .search_fnr()
    .with_search_strategy(["navn"])
    .with_search_strategy(["navn", "adressenavn"])
    .with_search_strategy(["navn", "adressenavn"], inkluder_doede=True)
    .run()
)

found_personal_ids = result.to_list()

print(found_personal_ids)
> ["11111123456", "22222209876", "33333376543"]

Ved å legge til et tredje søk, som inkluderer døde, har vi funnet personnummeret i det tredje elementet. Merk at ved å legge til døde, har vi utvidet søket.

Søkestrategi

Det finnes ingen universell beste søkestrategi da søkemetodikken vil variere basert på datagrunnlaget og behov. Derfor har vi lagt inn mye funksjonalitet i dette biblioteket.

Her er noen aspekter man bør ta stilling til:

  • FNR-søk kan gi falskt positivt funn
    • Eksempel: Mån søker kun på etternavn, adresse, og fødselsdato - kan gi FNRet til en tvilling
  • FNR-søk kan gi falskt negativt funn
    • Eksempel: Man søker på navn og adresse, men vedkommende har flyttet - da får man ingen treff
  • Flere variabler (Tabell 2) gir et snevrere søk. Opsjoner (Tabell 3) utvider søket.

Case 1: Snevert søk

Noen seksjoner vil kanskje vurdere at man ønsker så langt som mulig å unngå falskt positive funn. Med andre ord tar man heller kostnaden av å ekskludere potensielt gyldige FNR til fordel for å være helt sikre på at de FNR-ene man finner er gyldige. En strategi kunne da ha vært å kjøre ett søk med alle variablene man har tilgjengelig - et snevert søk. Dette kunne ha sett slik ut:

from dapla_whodat import Whodat

result = (
    Whodat.from_pandas(df)
    .search_fnr()
    .with_search_strategy(["navn", "adressenavn", "husnummer", "postnummer", "foedselsdato"])
    .run()
)

Case 2: Bredt søk

Andre seksjoner rydder muligens opp i dataene sine senere i produksjonsløpet ved å koble mot annen data, og ønsker et veldig bredt søk for å få med alle potensielle FNRer. Vi kan da se for oss et iterativt søk: vi starter snevert og utvider søket etter hvert. Dette kunne ha sett slik ut:

from dapla_whodat import Whodat

result = (
    Whodat.from_pandas(df)
    .search_fnr()
    .with_search_strategy(["navn", "adressenavn", "husnummer", "postnummer", "foedselsdato"])
    .with_search_strategy(["navn", "adressenavn", "husnummer", "postnummer", "foedselsaarFraOgMed", "foedselsaarTilOgMed"])
    .with_search_strategy(["navn", "adressenavn", "husnummer", "postnummer", "foedselsaarFraOgMed", "foedselsaarTilOgMed"],
      inkluder_doede=True)
    .with_search_strategy(["navn", "adressenavn", "husnummer", "postnummer", "foedselsaarFraOgMed", "foedselsaarTilOgMed"],
      inkluder_doede=True, opplysningsgrunnlag=True, inkluder_oppholdsadresse=True)
    .with_search_strategy(["navn", "adressenavn", "husnummer", "postnummer", "foedselsaarFraOgMed", "foedselsaarTilOgMed"],
      inkluder_doede=True, opplysningsgrunnlag=True, inkluder_oppholdsadresse=True, soek_fonetisk=True)
    .run()
)

Vi anbefaler statistikere å teste ut hvilke variabler som gir best resultater med datagrunnlaget man har tilgjengelig.

Detaljer om FNR-søket

Man kan få metadata fra søket man har utført. Dette gir detaljert informasjon om på hvilken søkestrategi som ga treff, for hver rad.

Detaljer
from dapla_whodat import Whodat
import pandas as pd

df = pd.DataFrame(
  {
    "navn": ["Donald Duck", "Dolly Duck", "Onkel Skrue"]
    "adressenavn": ["Lundlia", "Smøyatunvegen", "Simmenesvegen"]
  }
)

result = (
    Whodat.from_pandas(df)
    .search_fnr()
    .with_search_strategy(["navn"])
    .with_search_strategy(["navn", "adressenavn"])
    .run()
)

found_personal_ids = result.to_list()

print(found_personal_ids)
> ["11111123456", "22222209876", None]

print(result.details)
> [
    {
        "index_fnr_search_df": 0,
        "index_original_df": 0,
        "number_of_found_ids": 1,
        "unique_response_step_number": 1,
    },
    {
        "index_fnr_search_df": 1,
        "index_original_df": 1,
        "number_of_found_ids": 1,
        "unique_response_step_number": 2,
    },
    {
        "index_fnr_search_df": 2,
        "index_original_df": 2,
        "number_of_found_ids": 0,
        "unique_response_step_number": None,
    },
  ]

Hver rad i df tilsvarer ett element i listen i result.details. Hvert element har følgende data:

Tabell 4: Beskrivelse av metadata-felter
Felt Beskrivelse
index_fnr_search_df Radnummeret i DataFramen - uavhengig av indeks
index_original_df Radnummeret i forhold til indeksen i DataFramen (følger df.index). Brukes når man erstatter ugyldige fødselsnummer
number_of_found_ids Hvor mange FNRer som ble funnet på siste søk
unique_response_step_number Hvilken with_search_strategy() som fant et unikt FNR. Hvis man ikke finner et unikt FNR, er feltet None

I eksempelet over fant vi to FNRer. De ligger i listen found_personal_ids.

Første fødselsnummer: unique_response_step_number = 1 forteller oss at vi fant et unikt FNR med kun navn.

Andre fødselsnummer: unique_response_step_number = 2 forteller oss at navn og adresse var nødvendig for å få et unikt FNR.

Lagre metadata
from gcsfs import GCSFileSystem
import json

details = result.details

with GCSFileSystem().open("gs://ssb-someteam-data-produkt-prod/somefolder/details_fnrsearch.json", "w") as f:
  json.dump(details, f)

Metadatane er ikke sensitive og kan derfor skrives til produktbøtta for videre utforskning fra Kildomaten.

Erstatte ugyldige med gyldige FNR

Vi antar at vi har fulgt fremgangsmåten i Arbeidsflyt. Vi har da to DataFrames når vi utfører FNR-søket:

  • df_original: Den originale DataFramen vi ønsker å utføre FNR-søk på. DataFramen sin FNR-kolonne heter fnr
  • df_helper_variables: Et utsnitt av df_original, som kun inneholder radene med ugyldig FNR, og hjelpevariablene nødvendig for søket. Indeksen er lik som i df_original

Vi har to måter å få ut data fra FNR-søket:

1) Bruk av to_list()

found_personal_ids = result.to_list()

print(found_personal_ids)
> ["11111123456", "22222209876", None]

Vi kan da erstatte rader i df_original med følgende kode:

df_valid_personal_ids = (
                        pd.DataFrame({"fnr": found_personal_ids},
                        index=df_helper_variables.index, copy=False)
                        )

df_valid_personal_ids = (
                        df_valid_personal_ids
                        .reindex(df.index) 
                        # Pad dataset with NaN for rows with valid FNR
                        )
  
df_original["fnr"] = (
                    df_valid_personal_ids["fnr"]
                    .where(df_valid_personal_ids["fnr"].notna(),
                    df_original["fnr"]) # Merge valid FNRs into 'df_original'
                    )

2) Bruk av to_dict_from_original_indices()

Vi kan også få ut data som et Python dictionary, hvor den originale indeksen er nøkkelen og FNR-et er verdien. Hvis det ikke ble funnet et unikt FNR, er ikke dataen i dictionaryen.

found_personal_ids = result.to_dict_from_original_indices()

print(found_personal_ids)
> {0: "11111123456", 1: "22222209876"}

FNR-erstatning:

df_original["fnr"] = (
                      pd.Series(found_personal_ids)
                      .combine_first(df_original["fnr"])
                    )

Fotnoter

  1. Et Dapla-team har både et test- og et prod-miljø. Kildomaten må være skrudd på i det miljøet du ønkser å benytte dapla-toolbelt-whodat fra.↩︎

  2. Med interaktiv jobbing menes at man skriver og kode og får tilbake output i samme verktøy. F.eks. er Jupyterlab et eksempel på et verktøy som lar deg jobbe interaktivt med data.↩︎