dapla-toolbelt-whodat
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.
Aktør | FNR-leting |
---|---|
Kildomaten | ✅ |
data-admins (interaktivt) | ✅ |
developers (interaktivt) | ✅ |
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.
NB: Alle variabler må være strengverdier i den faktiske DataFramen. Tall-typer støttes ikke.
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.
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:
- Bruke Validator()-funksjonalitet for å finne ugyldige FNR (Dokumentasjon)
- Hent ut alle rader fra den originale DataFramen med ugyldig FNR funnet i steg 1.
- Transformer alle hjelpevariablene til formatet beskrevet i Tabell 2, og endre navn på kolonnene.
- Utfør et FNR-søk basert på hjelpevariablene.
- Erstatt de ugyldige FNRene med gyldig FNR
- Pseudonymiser FNRene
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
= pd.DataFrame(
df
{"navn": ["Donald Duck", "Dolly Duck", "Onkel Skrue"]
"adressenavn": ["Lundlia", "Smøyatunvegen", "Simmenesvegen"]
}
)
= (
result
Whodat.from_pandas(df)
.search_fnr()"navn"])
.with_search_strategy([
.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ørewith_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)
- man kan gjøre fonetisk navnesøk på hver rad ved å angi
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()"navn"])
.with_search_strategy([
.run()
)
= result.to_list() found_personal_ids
to_list()
returnerer en liste av FNRer som er funnet.
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
= result.to_list()
found_personal_ids
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()"navn"])
.with_search_strategy(["navn", "adressenavn"])
.with_search_strategy([
.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:
= result.to_list()
found_personal_ids
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()"navn"])
.with_search_strategy(["navn", "adressenavn"])
.with_search_strategy(["navn", "adressenavn"], inkluder_doede=True)
.with_search_strategy([
.run()
)
= result.to_list()
found_personal_ids
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()"navn", "adressenavn", "husnummer", "postnummer", "foedselsdato"])
.with_search_strategy([
.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()"navn", "adressenavn", "husnummer", "postnummer", "foedselsdato"])
.with_search_strategy(["navn", "adressenavn", "husnummer", "postnummer", "foedselsaarFraOgMed", "foedselsaarTilOgMed"])
.with_search_strategy(["navn", "adressenavn", "husnummer", "postnummer", "foedselsaarFraOgMed", "foedselsaarTilOgMed"],
.with_search_strategy([=True)
inkluder_doede"navn", "adressenavn", "husnummer", "postnummer", "foedselsaarFraOgMed", "foedselsaarTilOgMed"],
.with_search_strategy([=True, opplysningsgrunnlag=True, inkluder_oppholdsadresse=True)
inkluder_doede"navn", "adressenavn", "husnummer", "postnummer", "foedselsaarFraOgMed", "foedselsaarTilOgMed"],
.with_search_strategy([=True, opplysningsgrunnlag=True, inkluder_oppholdsadresse=True, soek_fonetisk=True)
inkluder_doede
.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
= pd.DataFrame(
df
{"navn": ["Donald Duck", "Dolly Duck", "Onkel Skrue"]
"adressenavn": ["Lundlia", "Smøyatunvegen", "Simmenesvegen"]
}
)
= (
result
Whodat.from_pandas(df)
.search_fnr()"navn"])
.with_search_strategy(["navn", "adressenavn"])
.with_search_strategy([
.run()
)
= result.to_list()
found_personal_ids
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:
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
= result.details
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 heterfnr
df_helper_variables
: Et utsnitt avdf_original
, som kun inneholder radene med ugyldig FNR, og hjelpevariablene nødvendig for søket. Indeksen er lik som idf_original
Vi har to måter å få ut data fra FNR-søket:
1) Bruk av to_list()
= result.to_list()
found_personal_ids
print(found_personal_ids)
> ["11111123456", "22222209876", None]
Vi kan da erstatte rader i df_original
med følgende kode:
= (
df_valid_personal_ids "fnr": found_personal_ids},
pd.DataFrame({=df_helper_variables.index, copy=False)
index
)
= (
df_valid_personal_ids
df_valid_personal_ids
.reindex(df.index) # Pad dataset with NaN for rows with valid FNR
)
"fnr"] = (
df_original["fnr"]
df_valid_personal_ids["fnr"].notna(),
.where(df_valid_personal_ids["fnr"]) # Merge valid FNRs into 'df_original'
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.
= result.to_dict_from_original_indices()
found_personal_ids
print(found_personal_ids)
> {0: "11111123456", 1: "22222209876"}
FNR-erstatning:
"fnr"] = (
df_original[
pd.Series(found_personal_ids)"fnr"])
.combine_first(df_original[ )
Fotnoter
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.↩︎
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.↩︎