Skip to content

Commit

Permalink
Merge pull request #33 from statisticsnorway/carl-blogg-dat-to-parquet
Browse files Browse the repository at this point in the history
Carl blogg dat to parquet
  • Loading branch information
skars82 authored Jan 29, 2024
2 parents 2fc6fcf + b8d0c3c commit 978cf1a
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"hash": "eabade2bb21c54bc8795862a030b26b8",
"result": {
"markdown": "---\ntitle: Fra arkiv til parquet\nsubtitle: Konvertering av arkivfiler\nimage: ../../../images/parquet-logo.jpg\ncategories:\n - Python\n - Arkiv\n - Datadok\n - Parquet\nauthor:\n - name: Carl F. Corneil\n affiliation:\n - name: Seksjon for Kultur- og Utdanningsstatistikk (360)\n email: [email protected]\ndate: 01/19/2024\ndate-modified: 01/22/2024\ndraft: false\nfreeze: true\n---\n\nI arkivet til SSB ligger data lagret som posisjonerte flatfiler, også kalt fastbredde-fil eller *fixed width file* på engelsk. I Datadok ligger det spesifisert hvordan du leser inn disse filene fra arkivet til **sas7bdat**-formatet, men ikke hvordan man konverterer til Parquet-formatet. I denne artikkelen deler jeg hvordan jeg gikk frem for å konvertere arkivfiler til Parquet. \n\n## Hva er en fastbredde-fil?\n\nEn fastbredde-fil er en fil der hver rad har en fast lengde, og hver kolonne har en fast posisjon. Det er ingen komma eller andre tegn som skiller kolonnene, slik som i en CSV-fil. En fastbredde-fil er en ren tekstfil, dvs. at du kan åpne den opp i teksteditor og kikke på innholdet direkte.\n\nUnder er et eksempel hvor samme data er lagret både på CSV-formatet og som fastbredde-fil:\n\n:::: {.columns}\n\n::: {.column width=\"47.5%\"}\n```{.python filename=\"csv\"}\n012345;;Ola Nordmann;\n345678;Kvinne;Kari Nordmann;\n```\n:::\n\n::: {.column width=\"5%\"}\n<!-- empty column to create gap -->\n:::\n\n::: {.column width=\"47.5%\"}\n```{.python filename=\"fastbredde-fil\"}\n012345 Ola Nordmann\n345678KvinneKari Nordmann\n```\n:::\n\n::::\n\nI csv-filen over til venstre ser vi at hver kolonne er separert med et semikolon, og at hver rad er separert med et linjeskift. I fastbredde-filen til høyre ser vi at hver kolonne har en fast lengde, og at hver rad har en fast lengde. I tillegg ser vi at det er et ekstra mellomrom mellom `Ola Nordmann` og `Kari Nordmann` i den første raden. Dette er fordi `Ola Nordmann` er 12 tegn lang, mens `Kari Nordmann` er 13 tegn lang.\n\n\n\n## Lese med Pandas\n\nVi kan bruke pandas-funksjonen `read_fwf()` for å lese inn en fastbredde-fil. Denne funksjonen tar inn en filsti, og en liste med bredder for hver kolonne. I tillegg kan vi spesifisere navn på kolonnene, og hvilken datatype kolonnene skal ha og hvordan missing-verdier skal representeres. \n\nVi er helt avhengig av å vite bredden på hver kolonne for å kunne lese inn en fastbredde-fil. Dette kan vi finne ut ved å åpne filen i en teksteditor og telle antall tegn i hver kolonne. Alternativt kan vi bruke innlesingsskriptet for SAS som finnes i Datadok, siden breddene er spesifisert der. Under er et ekspempel på hvordan vi kan lese inn en fastbredde-fil fra forrige avsnitt^[`/n` i strengen `112345 Ola Nordmann \\n345678KvinneKari Nordmann\\n` betyr linjeskift.]:\n\n::: {.cell execution_count=1}\n``` {.python .cell-code}\nimport pandas as pd\nfrom io import StringIO # Nødvendig siden vi sender en streng, ikke en filsti til .read_fwf\ninstring = \"112345 Ola Nordmann \\n345678KvinneKari Nordmann\\n\"\ndf = pd.read_fwf(StringIO(instring),\n names=['pers_id', 'kjonn', 'navn'], # Navngi kolonner\n dtype='object', # Alle kolonnene settes til \"object\"\n na_values=['.', ' .'], # Hvilke karakterer bruker SAS for tom verdi?\n widths=[6, 6, 13]) # Tell/regn ut dissa sjøl\ndf\n```\n\n::: {.cell-output .cell-output-display execution_count=1}\n```{=html}\n<div>\n<style scoped>\n .dataframe tbody tr th:only-of-type {\n vertical-align: middle;\n }\n\n .dataframe tbody tr th {\n vertical-align: top;\n }\n\n .dataframe thead th {\n text-align: right;\n }\n</style>\n<table border=\"1\" class=\"dataframe\">\n <thead>\n <tr style=\"text-align: right;\">\n <th></th>\n <th>pers_id</th>\n <th>kjonn</th>\n <th>navn</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <th>0</th>\n <td>112345</td>\n <td>NaN</td>\n <td>Ola Nordmann</td>\n </tr>\n <tr>\n <th>1</th>\n <td>345678</td>\n <td>Kvinne</td>\n <td>Kari Nordmann</td>\n </tr>\n </tbody>\n</table>\n</div>\n```\n:::\n:::\n\n\nKoden over returnerer en Pandas Dataframe i minnet. Den kan vi lett lagre til Parquet-formatet. Men innlesingen måtte vi spesifisere en masse detaljer manuelt. Hvis vi skal lese inn mange filer med ulik struktur, så er ikke denne fremgangsmåten skalerbar. Dette er en fremgangsmåte for å lese inn noen få filer. \n\n## Datadok\n\nSom nevnt over så finnes det et innlesingsskript for SAS i Datadok. Dette skriptet kan vi bruke til å lese inn en fastbredde-fil i Python. Vi kan også bruke det til å finne breddene på hver kolonne. Et slik skript har denne formen:\n\n```{.bash filename=\"innlesingsskript.sas\"}\nDATA sas_data;\n INFILE '/ssb/bruker/felles/flatfileksempel_dapla_manual_blogg.dat' MISSOVER PAD LRECL=36;\n INPUT\n @1 pers_id 6.0\n @7 kjonn $CHAR6.0\n @13 navn $CHAR13.0\n ;\nRUN;\n```\nVi kunne brukt informasjonen her til å finne ut hvordan filen skal leses inn med `read_fwf()`. Men fortsatt innebærer dette potensielt en del manuelt arbeid. \n\n## Lese med saspy\n\nEn annen tilnærming enn å bruke Pandas er å bruke biblioteket `saspy`. Dette biblioteket lar oss kjøre SAS-kode fra Python. Vi kan bruke det til å kjøre sas-skript hentet fra Datadok, konvertere til dataframe, og til slutt skrive til Parquet. I det følgende antar vi at du jobber i Jupyterlab i prodsonen (sl-jupyter-p), og at du har lagret innlesingsskriptet i en variabel, slik som vist under: \n\n```{.python filename=\"python\"}\nscript = \"\"\"\nDATA sas_data;\n INFILE '/ssb/bruker/felles/flatfileksempel_dapla_manual_blogg.dat' MISSOVER PAD LRECL=36;\n INPUT\n @1 pers_id 6.0\n @7 kjonn $CHAR6.0\n @13 navn $CHAR13.0\n ;\nRUN;\n\"\"\"\n```\n\nLa oss deretter kjøre følgende kode fra Jupyterlab:\n\n```{.python filename=\"python\"}\nfrom fagfunksjoner import saspy_session@\n\n# Kobler til sas-serverne\nsas = saspy_session()\n\n# Vi bruker tilkoblingen til å sende inn Datadok-skriptet\nresult = sas.submit(script)\n\n# Lagere sas-loggen i en variabel\nlog = result[\"LOG\"]\n\n# Ber om å få dataframe tilbake\ndf_frasas = sas.sd2df(\"sas_data\", \"work\")\n\n# Lukker koblingen til sas-serverne\nsas._endsas()\n\n# Printer ut datasettet\ndf_frasas\n```\n\nI koden over har vi brukt en pakke som heter `ssb-fagfunksjoner` for å gjøre en del oppgaver. Pakken er et overbygg over saspy, og koden over forutsetter at du har lagret passorder ditt på en spesiell måte^[Hvis du ønsker kan du bruker `ssb-fagfunksjoner` til å lagre passordet ditt i kryptert form. Da kan du lagre passordet i en fil på din egen maskin, og slipper å skrive det inn hver gang du skal koble til SAS. Funksjonen heter `set_password()`.].\n\n### Datatyper\n\nVi har nå en pandas dataframe med datatyper, men disse er basert på den lave mengden datatyper i SAS. Ofte bør det ryddes i datatyper før man skriver til Parquet. Spesielt bør du tenke på følgende:\n\n- `Character` mappes gjerne til `object` i pandas, ikke den strengere varianten `string`. \n- `Numeric` mappes stort sett til `float64` i pandas, vi får som regel ikke heltall direkte `Int64`. \n\nDu kan la Pandas gjøre ett nytt forsøk på å gjette datatyper ved å kjøre følgende kode:\n\n```{.python filename=\"python\"}\ndf_pd_dtypes = df_frasas.convert_dtypes()\ndf_pd_dtypes.dtypes\n```\n\nOm du vil teste min selvskrevne funksjon for å gjette på datatyper så ligger den i fellesfunksjons-pakken:\n\n```{.python filename=\"python\"}\nfrom fagfunksjoner import auto_dtype\ndf_auto = auto_dtype(df_frasas)\ndf_auto.dtypes\n```\n\n\nSjekk gjerne ut parameteret `cardinality_threshold`, om du er interessert i å automatisk sette `categorical dtypes`.\n\n### Skalering\n\nHvis du har mange arkivfiler, med mange forskjellige innlesingsskript, så kan du lagre alle skriptene i en mappe, og så iterere over de. \n\n```{.python filename=\"python\"}\nsas_script_path = \"/ssb/bruker/felles/flatfileksempel_dapla_manual_blogg.sas\"\nwith open(sas_script_path, \"r\", encoding=\"latin1\") as sas_script:\n script = sas_script.read().strip()\n script = \"DATA \" + script.split(\"DATA \")[1] # Forkort ned scriptet til det vi trenger\nprint(script)\n```\nHer henter jeg inn et innlesingsskript fra Datadok som jeg har lagret som en tekstfil i en mappe. Deretter gjør jeg den om til et object i minnet som kan brukes i saspy-koden som er vist over. Dermed er det bare å finne en logikk som gjør at du vet hvilket innlesingskript som skal brukes til hvilke filer, og du kan jobbe veldig effektivt med konvertering. Når alt er konvertert kan du f.eks. kjøre et script som validerer datatypene i alle parquet-filer. \n\n## Lagre dataframen til parquet\n\nNå er det veldig rett å skrive filen til Parquet-formatet. \n\n```{.python filename=\"python\"}\ndf_auto.to_parquet(\n \"/ssb/bruker/felles/flatfileksempel_dapla_manual_blogg.parquet\"\n )\n```\n\n## NUDB\n\nI omleggingen av NUDB (Nasjonal utdanningsdatabase), måtte vi konvertere hele arkivet vårt på 750+ dat-filer.\n\nDet var ønskelig å slippe å lagre til sas7bdat i mellom, for å slippe mye dataduplikasjon og arbeidsprosesser.\n\nI stor grad kunne dette arbeidet automatiseres (bortsett fra å lagre ut innlastingsscript fra gamle datadok). Funksjonene jeg utviklet for dette ligger stort sett i denne filen: \n[github.com/utd-nudb/prodsone/konverter_arkiv/archive.py](https://github.com/statisticsnorway/utd-nudb/blob/main/prodsone/konverter_arkiv/archive.py)\n\n",
"supporting": [
"index_files"
],
"filters": [],
"includes": {
"include-in-header": [
"<script src=\"https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js\" integrity=\"sha512-c3Nl8+7g4LMSTdrm621y7kf9v3SDPnhxLNhcjFJbKECVnmZHTdo+IRO05sNLTH/D3vA6u1X32ehoLC7WFVdheg==\" crossorigin=\"anonymous\"></script>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js\" integrity=\"sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==\" crossorigin=\"anonymous\"></script>\n<script type=\"application/javascript\">define('jquery', [],function() {return window.jQuery;})</script>\n"
]
}
}
}
Loading

0 comments on commit 978cf1a

Please sign in to comment.