Last active
September 3, 2026 13:28
-
-
Save vidhav/b5fcffba0df946a78dcb393b82f8bcfa to your computer and use it in GitHub Desktop.
Hurtigkurs i Cloud Native Formater
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "cells": [ | |
| { | |
| "cell_type": "markdown", | |
| "id": "35cd7225", | |
| "metadata": {}, | |
| "source": [ | |
| "# Hurtigkurs i Cloud Native Formater" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "9e3909d4", | |
| "metadata": {}, | |
| "source": [ | |
| "# Installasjon og oppsett\n", | |
| "\n", | |
| "Før vi kan begynne må vi installere nødvendige Python-pakker. Vi bruker `%pip install` for å gjøre dette direkte i notebooken.\n", | |
| "\n", | |
| "### Hva installerer vi?\n", | |
| "\n", | |
| "| Pakke | Hva brukes det til? |\n", | |
| "|---|---|\n", | |
| "| `duckdb` | Kjøre SQL-spørringer direkte mot store datafiler |\n", | |
| "| `geopandas` | Lese, analysere og visualisere geografiske data |\n", | |
| "| `lonboard` | Rask, interaktiv kartvisualisering av store datasett |" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "id": "7f4e52dc", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "%%capture\n", | |
| "%pip install duckdb geopandas lonboard\n", | |
| "\n", | |
| "from datetime import date\n", | |
| "from pathlib import Path\n", | |
| "from tempfile import TemporaryDirectory\n", | |
| "from urllib.request import urlretrieve\n", | |
| "from urllib.parse import urlencode, urlparse\n", | |
| "from warnings import filterwarnings\n", | |
| "from zipfile import ZipFile\n", | |
| "\n", | |
| "import duckdb\n", | |
| "import geopandas as gpd\n", | |
| "from lonboard import Map, PolygonLayer, ScatterplotLayer\n", | |
| "import pandas as pd\n", | |
| "\n", | |
| "filterwarnings(\"ignore\")" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "f567f695", | |
| "metadata": {}, | |
| "source": [ | |
| "## Sette opp DuckDB\n", | |
| "\n", | |
| "Vi oppretter en DuckDB-oppkobling, som kun eksisterer i minnet og laster inn spatial-utvidelsen som gir oss til gang til romlige spørringer à la PostGIS." | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "id": "d40dcd3f", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "conn = duckdb.connect(\":memory:\")\n", | |
| "\n", | |
| "conn.execute(\"INSTALL spatial;\")\n", | |
| "\n", | |
| "conn.execute(\"LOAD spatial;\")" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "85584099", | |
| "metadata": {}, | |
| "source": [ | |
| "# Laste ned og klargjøre data\n", | |
| "\n", | |
| "Her laster vi ned datasett fra Geonorge, i FGDB-formatet, og transformerer disse til Parquet-filer. Da får vi med alle tabellene, både de med og de uten geografiske data. Hver tabell blir en egen fil, som vi igjen kan behandle som tabeller i en spørring." | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "id": "2fa414b7", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "urls = [\n", | |
| " \"https://nedlasting.geonorge.no/geonorge/Plan/Markagrensen/FGDB/Plan_0000_Norge_25833_Markagrensen_FGDB.zip\",\n", | |
| " \"https://nedlasting.geonorge.no/geonorge/Befolkning/KirkebyggEnkel/FGDB/Befolkning_0000_Norge_25833_KirkebyggEnkel_FGDB.zip\",\n", | |
| " \"https://nedlasting.geonorge.no/geonorge/Basisdata/Fylker/FGDB/Basisdata_0000_Norge_25833_Fylker_FGDB.zip\",\n", | |
| " \"https://nedlasting.geonorge.no/geonorge/Basisdata/Kommuner/FGDB/Basisdata_0000_Norge_25833_Kommuner_FGDB.zip\",\n", | |
| " \"https://nedlasting.geonorge.no/geonorge/Samfunnssikkerhet/TilfluktsromOffentlige/FGDB/Samfunnssikkerhet_0000_Norge_25833_TilfluktsromOffentlige_FGDB.zip\",\n", | |
| "]\n", | |
| "\n", | |
| "results = []\n", | |
| "\n", | |
| "for url in urls:\n", | |
| " # Hent ut path-delen av URL-en og bryt den opp på katalog-nivå for å få info\n", | |
| " # om kategori, datasett-navn og filformat.\n", | |
| " path = Path(urlparse(url).path[1:])\n", | |
| " category = path.parts[1].lower()\n", | |
| " dataset = path.parts[2].lower()\n", | |
| " format_ = path.parts[3].lower()\n", | |
| "\n", | |
| " # Vi ønsker kun FGDB-filer.\n", | |
| " if format_ != \"fgdb\":\n", | |
| " continue\n", | |
| "\n", | |
| " # Opprett en egen data-katalog for Parquet-filene.\n", | |
| " dst_dir = Path(\"data\", category, dataset)\n", | |
| " dst_dir.mkdir(exist_ok=True, parents=True)\n", | |
| "\n", | |
| " # Vi ønsker ikke å ta vare på filene etter at de er lastet ned, så vi bruker\n", | |
| " # en midlertidig katalog.\n", | |
| " with TemporaryDirectory() as tmp:\n", | |
| " tmp_dir = Path(tmp)\n", | |
| " zip_file = tmp_dir.joinpath(path.name)\n", | |
| "\n", | |
| " # Last ned og pakk ut ZIP-fil.\n", | |
| " urlretrieve(url, zip_file)\n", | |
| " with ZipFile(zip_file, \"r\") as zip_ref:\n", | |
| " zip_ref.extractall(tmp_dir)\n", | |
| "\n", | |
| " # Siden FGDB egentlig er en mappe, som later som om den er en fil, må vi\n", | |
| " # finne første det første som dukker opp med endelsen \".gdb\" i arkivet.\n", | |
| " src_file = next(tmp_dir.glob(\"*.gdb\"))\n", | |
| "\n", | |
| " # Gå gjennom alle lagene i datasettet.\n", | |
| " for _, layer in gpd.list_layers(src_file).iterrows():\n", | |
| " dst_file = dst_dir.joinpath(layer[\"name\"]).with_suffix(\".parquet\")\n", | |
| "\n", | |
| " df = gpd.read_file(\n", | |
| " filename=src_file,\n", | |
| " layer=layer[\"name\"],\n", | |
| " )\n", | |
| "\n", | |
| " # Dersom laget har geografisk informasjon, transformerer vi koordinatene\n", | |
| " # til EPSG:4326, slik at det blir enklere å visualisere.\n", | |
| " if pd.isna(layer[\"geometry_type\"]) == False:\n", | |
| " df.to_crs(4326, inplace=True)\n", | |
| "\n", | |
| " df.to_parquet(dst_file)\n", | |
| "\n", | |
| " print(f\"- {dst_file}\")" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "34d03015", | |
| "metadata": {}, | |
| "source": [ | |
| "## Teste data\n", | |
| "\n", | |
| "Vi kan teste med en enkel *-spørring som viser alle kolonnene. Legg merke til at strukturen på tabellen er slik man forventer fra Geonorge." | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "id": "f0d732bc", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "conn.sql(\"\"\"\n", | |
| "SELECT *\n", | |
| "FROM './data/befolkning/kirkebyggenkel/kirkebygg.parquet'\n", | |
| "ORDER BY bygningsnavn\n", | |
| "LIMIT 5\n", | |
| "\"\"\")" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "bc1aea66", | |
| "metadata": {}, | |
| "source": [ | |
| "# Analyser og spørringer\n", | |
| "\n", | |
| "Her følger noen eksempler på spørringer og analyser man kan gjøre mot diverse fil-kilder - helt uten servere." | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "1fa99b4f", | |
| "metadata": {}, | |
| "source": [ | |
| "## Hvor mange kirker ligger innenfor markagrensen?\n", | |
| "\n", | |
| "Datasettet [Markagrensen](kartkatalog.geonorge.no/metadata/45e9ba77-388c-40eb-87df-e34e9d9ab300) er en avgrensning av Oslomarka mot byggesonen i omkringliggende kommuner og inneholder både grensene som linjer og som flater.\n", | |
| "\n", | |
| "Datasettet [Kirkebygg - forenklet](https://kartkatalog.geonorge.no/metadata/eea87664-d936-478e-897a-c38ca7c478a0) er basert på Ordna eiendom, som inneholder registeret Kirkebyggdatabasen, og inneholder et representasjonspunkt per kirkebygg.\n", | |
| "\n", | |
| "For å finne ut hvilke kirker som ligger innenfor markagrensen, kjører vi en enkel geografisk spørring - direkte mot Parquet-filene - med SQL!" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "id": "f8332062", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "markagrensen = conn.sql(\"\"\"\n", | |
| " SELECT geometry::GEOMETRY AS geometry\n", | |
| " FROM './data/plan/markagrensen/markagrensen_lovvirkeomrade.parquet'\n", | |
| "\"\"\")\n", | |
| "\n", | |
| "kirker = conn.sql(\"\"\"\n", | |
| " SELECT k.bygningsnavn AS navn, k.geometry::GEOMETRY AS geometry\n", | |
| " FROM './data/befolkning/kirkebyggenkel/kirkebygg.parquet' AS k\n", | |
| " JOIN 'markagrensen' AS m\n", | |
| " ON ST_Contains(m.geometry, k.geometry)\n", | |
| "\"\"\")\n", | |
| "\n", | |
| "display(kirker)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "id": "eb5b53fe", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "Map([\n", | |
| " PolygonLayer.from_duckdb(\n", | |
| " markagrensen,\n", | |
| " get_fill_color=[150, 255, 150, 64],\n", | |
| " get_line_color=[7, 255, 7, 204],\n", | |
| " get_line_width=2,\n", | |
| " line_width_units=\"pixels\",\n", | |
| " ),\n", | |
| " ScatterplotLayer.from_duckdb(\n", | |
| " kirker,\n", | |
| " get_fill_color=[255, 0, 0, 255],\n", | |
| " radius_min_pixels=5,\n", | |
| " ),\n", | |
| "])" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "0326dae8", | |
| "metadata": {}, | |
| "source": [ | |
| "## Tilfluktsrom\n", | |
| "\n", | |
| "La oss kjøre en analyse på hvor god dekning det er på tilfluktsrom (offentlige) i forhold til folketall. Vi kombinerer da tilfluktsrom fra DSB, folketall fra SSB, fylker fra Kartverket.\n", | |
| "\n", | |
| "SSB har allerede Parquet som output-format ([se dokumentasjon](https://www.ssb.no/api/pxwebapiv2)).\n", | |
| "\n", | |
| "Eksempel på kommuner med folketall i HTML:\n", | |
| "\n", | |
| "```\n", | |
| "https://data.ssb.no/api/pxwebapi/v2/tables/11342/data?lang=no&outputFormat=html&valuecodes[Tid]=2026&valuecodes[Region]=*&codelist[Region]=agg_KommGjeldende&valuecodes[ContentsCode]=Folkemengde&stub=Region,Tid\n", | |
| "```" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "id": "ec024503", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "params = urlencode({\n", | |
| " \"lang\": \"no\",\n", | |
| " \"outputFormat\": \"parquet\",\n", | |
| " \"valuecodes[Tid]\": date.today().year,\n", | |
| " \"valuecodes[Region]\": \"*\",\n", | |
| " \"codelist[Region]\": \"agg_KommGjeldende\",\n", | |
| " \"valuecodes[ContentsCode]\": \"Folkemengde\",\n", | |
| "})\n", | |
| "\n", | |
| "folketall = conn.sql(f\"\"\"\n", | |
| " SELECT SUBSTRING(region, 1, 2) AS fylkesnummer, SUM(value)::INTEGER AS folkemengde\n", | |
| " FROM read_parquet('https://data.ssb.no/api/pxwebapi/v2/tables/11342/data?{params}')\n", | |
| " WHERE region != '9999'\n", | |
| " GROUP BY fylkesnummer\n", | |
| "\"\"\")\n", | |
| "\n", | |
| "display(folketall)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "id": "0c5972b1", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "fylkesnavn = conn.sql(\"\"\"\n", | |
| " SELECT f.fylkesnummer AS fylkesnummer, n.navn AS fylkesnavn\n", | |
| " FROM 'data/basisdata/fylker/fylke.parquet' AS f\n", | |
| " JOIN 'data/basisdata/fylker/administrativenhetnavn.parquet' AS n\n", | |
| " ON n.fylke_fk = f.lokalid AND n.sprak = 'nor'\n", | |
| "\"\"\")\n", | |
| "\n", | |
| "display(fylkesnavn)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "id": "b9e97bdc", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "tilfluktsrom = conn.sql(\"\"\"\n", | |
| " SELECT f.fylkesnummer\n", | |
| " , ANY_VALUE(n.fylkesnavn) AS fylkesnavn\n", | |
| " , SUM(t.plasser) AS kapasitet\n", | |
| " , ANY_VALUE(ft.folkemengde) AS folketall\n", | |
| " , ROUND((kapasitet / folketall) * 100, 2) AS dekning\n", | |
| " FROM 'data/basisdata/fylker/fylke.parquet' AS f\n", | |
| " JOIN 'data/samfunnssikkerhet/tilfluktsromoffentlige/tilfluktsromoffentlige_tilfluktsrom.parquet' AS t\n", | |
| " ON ST_Contains(f.geometry, t.geometry)\n", | |
| " JOIN 'fylkesnavn' AS n ON n.fylkesnummer = f.fylkesnummer\n", | |
| " JOIN 'folketall' AS ft ON ft.fylkesnummer = f.fylkesnummer\n", | |
| " GROUP BY f.fylkesnummer\n", | |
| " ORDER BY dekning DESC\n", | |
| "\"\"\")\n", | |
| "\n", | |
| "display(tilfluktsrom)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "21fbe31c", | |
| "metadata": {}, | |
| "source": [ | |
| "## Andre datakilder\n", | |
| "\n", | |
| "DuckDB kan lese andre datakilder, som f.eks. JSON og CSV, enten lokalt eller fra en URL. Dersom ikke filendelsen er eksplisitt har DuckDB egne funksjoner for innlesing." | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "id": "cff15d62", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "tettsteder_csv = conn.sql(\"\"\"\n", | |
| " SELECT *\n", | |
| " FROM 'https://raw.githubusercontent.com/geopython/pygeoapi/refs/heads/master/tests/data/norwegian_urban_areas.csv'\n", | |
| "\"\"\")\n", | |
| "\n", | |
| "display(tettsteder_csv)" | |
| ] | |
| }, | |
| { | |
| "cell_type": "markdown", | |
| "id": "73e2e49b", | |
| "metadata": {}, | |
| "source": [ | |
| "Dersom resultatet fra en DuckDB-spørring har en gyldig geometri-kolonne, kan spørringen brukes i GIS-analyser *og* som en kilde i QGIS sin DuckDB-plugin!" | |
| ] | |
| }, | |
| { | |
| "cell_type": "code", | |
| "execution_count": null, | |
| "id": "327f9ca0", | |
| "metadata": {}, | |
| "outputs": [], | |
| "source": [ | |
| "tettsteder_geo = conn.sql(\"\"\"\n", | |
| " SELECT name,\n", | |
| " population_2022 AS population,\n", | |
| " ST_Transform(ST_Point(easting, northing), 'EPSG:25833', 'EPSG:4326', true) AS geometry\n", | |
| " FROM read_csv('https://raw.githubusercontent.com/geopython/pygeoapi/refs/heads/master/tests/data/norwegian_urban_areas.csv')\n", | |
| " ORDER BY population ASC\n", | |
| "\"\"\")\n", | |
| "\n", | |
| "display(tettsteder_geo)" | |
| ] | |
| } | |
| ], | |
| "metadata": { | |
| "kernelspec": { | |
| "display_name": ".venv (3.12.3)", | |
| "language": "python", | |
| "name": "python3" | |
| }, | |
| "language_info": { | |
| "codemirror_mode": { | |
| "name": "ipython", | |
| "version": 3 | |
| }, | |
| "file_extension": ".py", | |
| "mimetype": "text/x-python", | |
| "name": "python", | |
| "nbconvert_exporter": "python", | |
| "pygments_lexer": "ipython3", | |
| "version": "3.12.3" | |
| } | |
| }, | |
| "nbformat": 4, | |
| "nbformat_minor": 5 | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment