Når vi nå har brukt litt tid på å hente ut og analysere data med DuckDB, er neste steg å jobbe visuelt med materialet gjennom visualisering. En god og oversiktlig måte å gjøre dette på, er å jobbe i såkalte notebooks – digitale notatbøker som lar oss dokumentere hele arbeidsprosessen med både tekst og kode. Du kan kjøre notebooks lokalt, for eksempel med Jupyter Notebook, men for enkelhetens skyld bruker vi Google Colab – en nettbasert løsning som sikrer at alle har samme utgangspunkt.
Hva er notebooks?¶
En notebook er et interaktivt verktøy der du skriver og kjører kode direkte i nettleseren. Tenk på det som en blanding av en tekstbehandler og en kodeeditor, hvor du kan kombinere forklarende tekst, bilder og kode i én og samme arbeidsflate. Det er et populært verktøy innen datavitenskap, maskinlæring, undervisning – og ikke minst datajournalistikk.
De viktigste funksjonene¶
Kjøring av kode i små deler (celler)
Du kan kjøre koden stykkevis, noe som gjør det enklere å teste, feilsøke og jobbe iterativt.
Kombinasjon av tekst og kode
Du kan forklare hva du gjør underveis, noe som gjør prosessen mer forståelig – både for deg selv og andre.
Visualiseringer
Grafer, kart og diagrammer kan vises direkte i notatboken.
Dokumentasjon
Hele arbeidsprosessen lagres, slik at du enkelt kan reprodusere resultatene. Perfekt hvis du for eksempel skal sende inn en SKUP-rapport.
I de neste øvelsene skal vi bruke notebooks, DuckDB, litt enkel Python, grafverktøyet Plotly, dataverktøyet Polars og visualiseringsverktøyet Lonboard. Dette er flere nye ting på en gang, men vi holder det på et overkommelig nivå. Har du ikke programmert før, trenger du ikke bekymre deg – mange av kommandoene vil ligne på SQL-syntaksen du allerede har brukt i DuckDB.
Opprette en notebook¶
Vi starter med å opprette en tom notebook i Google Colab:
Gå til Google Colab.
Velg Fil → Ny notatbok. Du får opp en tom notatbok som ser slik ut:

Figur 1: Tom notebook.
Legg til tekst- og kodeceller¶
En notebook består av to hovedtyper celler: tekst og kode.
Tekstceller bruker vi til å dokumentere hva vi gjør og hvorfor.
Kodeceller bruker vi til å skrive og kjøre kommandoer.
Legg til en tekstcelle¶
Klikk på Tekst-knappen i menylinjen.

Figur 2: Legg til ny tekstlinje.
Dobbeltklikk i cellen (eller trykk på Enter) for å redigere
Skriv følgende:
# Installer nødvendige bibliotekerLegg til en kodecelle¶
Klikk på Kode-knappen, og skriv inn:
!pip install lonboard==v0.13.0 duckdb==v1.4.2 plotly.expressTrykk på play-ikonet til venstre for cellen for å kjøre den. Du ser nå at Colab laster ned og installerer verktøyene.
Dette installerer Lonboard, DuckDB og Plotly i notebooken.

Figur 3: Første kodecelle installerer avhengigheter.
Last inn DuckDB og nødvendige moduler¶
Lag en ny tekstcelle med følgende innhold:
# Last inn DuckDB og nødvendige tilleggsmodulerDeretter legger du inn følgende kode i en ny kodecelle:
import duckdb
con = duckdb.connect()
con.sql('INSTALL httpfs;')
con.sql('INSTALL postgres;')
con.sql('INSTALL ducklake;')
con.sql('INSTALL spatial;')
con.sql('INSTALL h3 FROM community;')
con.sql('LOAD httpfs;')
con.sql('LOAD postgres;')
con.sql('LOAD ducklake;')
con.sql('LOAD spatial;')
con.sql('LOAD h3;')Du vil kanskje kjenne igjen noen av kommandoene, selv om syntaksen nå er litt annerledes. Kort fortalt er dette kode skrevet i Python som sendes videre til DuckDB-biblioteket for å kjøres.
Kjør cellen med play-knappen. Når alt er ferdig, vises et grønt avhukingssymbol til venstre som bekrefter at alt fungerte som det skulle.
Logg på Medieklyngens datasjø¶
Vi er snart klare til å hente data fra Medieklyngens datasjø. Men først må vi logge på, på samme måte som tidligere.
Start med en ny tekstcelle:
# Legg inn nøkler og påloggingsinformasjon til DuckLakeOg så en ny kodecelle med dette innholdet:
con.sql("CREATE SECRET (TYPE R2, KEY_ID '9030e0f90a86af08b08b6e2a1222a778', SECRET '2fe64ae1c22869400f577bb9421602f0f81a83a2f658cea6bdd556f4fc65064b', ACCOUNT_ID 'bca3475a0f4afeb0640daafc17ec2b18');")
con.sql("ATTACH 'ducklake:postgres:dbname=mcn_ducklake host=ep-fragrant-unit-a20fur0h.eu-central-1.aws.neon.tech user=fra_signaler_til_skup_h2_2025 password=npg_TLH6IjXdAW9q sslmode=require' AS mcn (DATA_PATH 'r2://mcn-ducklake', READ_ONLY);")Trykk på play. Hvis alt gikk bra, vil du se en svart linje under kodevinduet.
Gratulerer! Du er nå logget på DuckLake og klar til å hente og visualisere data.
Last inn og undersøk data¶
Nå skal vi hente inn et datasett fra 11. juli 2025. Start med å legge til en ny tekstcelle med følgende tittel:
# Last inn data fra 11. juli 2025Deretter legger du inn denne kodecellen:
con.sql("CREATE OR REPLACE TABLE FLIGHTS_11_07_2025 AS SELECT timestamp, icao, registration, squawk, flight, latitude, longitude, altitude_barometric, on_ground, ground_speed, flydenity_nation, h3_15, is_military, is_interesting, is_pia, is_ladd FROM mcn.airtraces WHERE timestamp >= '2025-07-11 00:00:00' and timestamp < '2025-07-12 00:00:00';")Dette kan ta litt tid. Du ser fremdriften via den horisontale linjen under cellen. Når den grønne avkrysningsboksen vises, er operasjonen fullført og du kan gå videre.
Tell antall datapunkter¶
Legg til en ny tekstcelle:
# Tell antall datapunkterOg deretter en kodecelle:
con.sql("SELECT count(*) FROM FLIGHTS_11_07_2025")Du får et svar som dette:
┌──────────────┐
│ count_star() │
│ int64 │
├──────────────┤
│ 7132121 │
└──────────────┘Over 7,1 millioner rader – et solid utgangspunkt!
Se på datastrukturen¶
Legg inn en ny tekstcelle:
# Beskriv datastrukturenOg legg deretter inn denne kodecellen:
con.sql("DESCRIBE FLIGHTS_11_07_2025").show(max_width=100)Resultatet ser omtrent slik ut – og mye bør nå være kjent stoff:
┌─────────────────────┬──────────────────────────┬─────────┬─────────┬─────────┬─────────┐
│ column_name │ column_type │ null │ key │ default │ extra │
│ varchar │ varchar │ varchar │ varchar │ varchar │ varchar │
├─────────────────────┼──────────────────────────┼─────────┼─────────┼─────────┼─────────┤
│ timestamp │ TIMESTAMP WITH TIME ZONE │ YES │ NULL │ NULL │ NULL │
│ icao │ VARCHAR │ YES │ NULL │ NULL │ NULL │
│ registration │ VARCHAR │ YES │ NULL │ NULL │ NULL │
│ squawk │ VARCHAR │ YES │ NULL │ NULL │ NULL │
│ flight │ VARCHAR │ YES │ NULL │ NULL │ NULL │
│ latitude │ DECIMAL(9,6) │ YES │ NULL │ NULL │ NULL │
│ longitude │ DECIMAL(10,6) │ YES │ NULL │ NULL │ NULL │
│ altitude_barometric │ INTEGER │ YES │ NULL │ NULL │ NULL │
│ on_ground │ BOOLEAN │ YES │ NULL │ NULL │ NULL │
│ ground_speed │ DECIMAL(6,1) │ YES │ NULL │ NULL │ NULL │
│ flydenity_nation │ VARCHAR │ YES │ NULL │ NULL │ NULL │
│ h3_15 │ VARCHAR │ YES │ NULL │ NULL │ NULL │
│ is_military │ BOOLEAN │ YES │ NULL │ NULL │ NULL │
│ is_interesting │ BOOLEAN │ YES │ NULL │ NULL │ NULL │
│ is_pia │ BOOLEAN │ YES │ NULL │ NULL │ NULL │
│ is_ladd │ BOOLEAN │ YES │ NULL │ NULL │ NULL │
├─────────────────────┴──────────────────────────┴─────────┴─────────┴─────────┴─────────┤
│ 16 rows 6 columns │
└────────────────────────────────────────────────────────────────────────────────────────┘Inspiser dataene¶
Neste steg er å ta en nærmere titt på selve innholdet i tabellen. Legg først inn en tekstcelle:
# Inspiser dataeneDeretter denne kodecellen:
con.sql("SELECT * FROM FLIGHTS_11_07_2025").show(max_width=300)Du får nå en tabell med flydata som inneholder alle de 16 kolonnene vi valgte ut.
┌────────────────────────────┬─────────┬──────────────┬─────────┬─────────┬──────────────┬───────────────┬─────────────────────┬───────────┬──────────────┬──────────────────┬─────────────────┬─────────────┬────────────────┬─────────┬─────────┐
│ timestamp │ icao │ registration │ squawk │ flight │ latitude │ longitude │ altitude_barometric │ on_ground │ ground_speed │ flydenity_nation │ h3_15 │ is_military │ is_interesting │ is_pia │ is_ladd │
│ timestamp with time zone │ varchar │ varchar │ varchar │ varchar │ decimal(9,6) │ decimal(10,6) │ int32 │ boolean │ decimal(6,1) │ varchar │ varchar │ boolean │ boolean │ boolean │ boolean │
├────────────────────────────┼─────────┼──────────────┼─────────┼─────────┼──────────────┼───────────────┼─────────────────────┼───────────┼──────────────┼──────────────────┼─────────────────┼─────────────┼────────────────┼─────────┼─────────┤
│ 2025-07-11 10:34:43.931+00 │ 4ACA73 │ SE-RSS │ 6022 │ SAS4112 │ 64.378188 │ 12.515735 │ 35000 │ false │ 452.9 │ Sweden │ 8f08022d34b1665 │ false │ false │ false │ false │
│ 2025-07-11 10:34:43.931+00 │ 47839C │ LN-OOB │ 5672 │ DOC79 │ 64.031348 │ 10.984241 │ 4325 │ false │ 147.3 │ Norway │ 8f0802d69246693 │ false │ false │ false │ false │
│ 2025-07-11 10:34:43.931+00 │ 4ACA72 │ SE-RSR │ 6031 │ SAS71A │ 62.313664 │ 11.196864 │ 36000 │ false │ 457.0 │ Sweden │ 8f0816add48121b │ false │ false │ false │ false │
│ 2025-07-11 10:34:43.939+00 │ 4007DA │ G-SAJL │ 2201 │ LOG44L │ 58.416092 │ 4.262695 │ 21650 │ false │ 337.8 │ United Kingdom │ 8f09864302908c1 │ false │ false │ false │ false │
│ 2025-07-11 10:34:43.964+00 │ 4406DE │ OE-LPE │ 0603 │ AUA81 │ 61.292072 │ -0.466207 │ 34000 │ false │ 471.9 │ Austria │ 8f09a56621a6b75 │ false │ false │ false │ false │
│ 2025-07-11 10:34:43.967+00 │ 478ECD │ LN-WDO │ 0233 │ WIF8KT │ 63.346664 │ 10.466485 │ 9500 │ false │ 234.9 │ Norway │ 8f0815100d86442 │ false │ false │ false │ false │
│ 2025-07-11 10:34:43.979+00 │ 478D83 │ LN-WDR │ 2563 │ WIF569 │ 62.702592 │ 7.292074 │ 18675 │ false │ 232.4 │ Norway │ 8f08328a5a58961 │ false │ false │ false │ false │
│ 2025-07-11 10:34:43.979+00 │ 478DF2 │ LN-WEB │ 0402 │ WIF2GB │ 62.297600 │ 8.238267 │ 40000 │ false │ 431.6 │ Norway │ 8f083240b48d749 │ false │ false │ false │ false │
│ 2025-07-11 10:34:43.997+00 │ 485871 │ PH-EXV │ 0514 │ KLM94Q │ 56.403576 │ 4.843254 │ 38000 │ false │ 473.8 │ Netherlands │ 8f0996803d43c1c │ false │ false │ false │ false │
│ 2025-07-11 10:34:43.997+00 │ 46B8AB │ SX-NEK │ 6024 │ AEE757 │ 58.051248 │ 13.013580 │ 37000 │ false │ 457.9 │ Greece │ 8f08b6b0485d4d9 │ false │ false │ false │ false │
│ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │
│ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │
│ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │ · │
│ 2025-07-11 10:36:06.677+00 │ 4007DA │ G-SAJL │ 2201 │ LOG44L │ 58.461556 │ 4.488373 │ 18950 │ false │ 327.0 │ United Kingdom │ 8f09864e39148c5 │ false │ false │ false │ false │
│ 2025-07-11 10:36:06.747+00 │ 406544 │ G-EZUK │ 1402 │ EZY53ZV │ 59.333632 │ 8.241577 │ 34025 │ false │ 460.0 │ United Kingdom │ 8f099dcd294e6f1 │ false │ false │ false │ false │
│ 2025-07-11 10:36:06.747+00 │ 4787AA │ LN-WDL │ 1207 │ WIF4AH │ 59.810920 │ 8.250638 │ 24000 │ false │ 306.5 │ Norway │ 8f099db997160e2 │ false │ false │ false │ false │
│ 2025-07-11 10:36:06.749+00 │ 45AB4D │ OY-JZM │ 6637 │ JTD410 │ 53.032124 │ 13.081107 │ 38000 │ false │ 437.9 │ Denmark │ 8f1f1dda82dc6dd │ false │ false │ false │ false │
│ 2025-07-11 10:36:06.749+00 │ 461F9E │ OH-LZI │ 6440 │ FIN1DF │ 51.973800 │ 14.137268 │ 35000 │ false │ 422.2 │ Finland │ 8f1f195332c1b23 │ false │ false │ false │ false │
│ 2025-07-11 10:36:06.758+00 │ 484C52 │ PH-EZN │ 1000 │ KLM1921 │ 52.296660 │ 5.160904 │ 13450 │ false │ 380.9 │ Netherlands │ 8f1969ccd5638b2 │ false │ false │ false │ false │
│ 2025-07-11 10:36:06.763+00 │ 479E29 │ LN-OQT │ 4177 │ HKS125 │ 58.878752 │ 5.625092 │ -250 │ false │ 3.7 │ Norway │ 8f0982166752bb3 │ false │ false │ false │ false │
│ 2025-07-11 10:36:06.771+00 │ 4BA95A │ TC-JJZ │ 7673 │ THY76Y │ 59.895492 │ 6.365387 │ 33000 │ false │ 486.8 │ Turkey │ 8f0983a2266c925 │ false │ false │ false │ false │
│ 2025-07-11 10:36:06.771+00 │ 4D24AE │ 9H-WDQ │ 6516 │ WZZ47HA │ 60.044848 │ 6.288398 │ 10500 │ false │ 318.3 │ Malta │ 8f0983b6a655973 │ false │ false │ false │ false │
│ 2025-07-11 10:36:06.772+00 │ 478DF2 │ LN-WEB │ 0402 │ WIF2GB │ 62.164904 │ 8.031413 │ 40000 │ false │ 431.1 │ Norway │ 8f0832582303583 │ false │ false │ false │ false │
├────────────────────────────┴─────────┴──────────────┴─────────┴─────────┴──────────────┴───────────────┴─────────────────────┴───────────┴──────────────┴──────────────────┴─────────────────┴─────────────┴────────────────┴─────────┴─────────┤
│ ? rows (>9999 rows, 20 shown) 16 columns │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘Antall fly per land¶
La oss gjøre en øvelse vi også har gjort tidligere i DuckDB: Vi teller antall fly per land.
Start med å legge inn følgende tekstcelle:
# Tell antall fly per landDeretter denne kodecellen:
sql = """
SELECT flydenity_nation as land,
COUNT(DISTINCT icao) AS fly
FROM FLIGHTS_11_07_2025
WHERE LENGTH(flydenity_nation) > 0
GROUP BY 1
ORDER BY 2 DESC
"""
con.sql(sql).show(max_rows=30)Denne spørringen gir oss en oversikt over hvor mange ulike fly som er registrert i hvert land den dagen. Utvalget vises med maksimum 30 rader.
┌──────────────────────┬───────┐
│ land │ fly │
│ varchar │ int64 │
├──────────────────────┼───────┤
│ Germany │ 409 │
│ United Kingdom │ 390 │
│ Netherlands │ 289 │
│ Norway │ 206 │
│ Ireland │ 201 │
│ Turkey │ 200 │
│ Malta │ 190 │
│ United States │ 169 │
│ Sweden │ 132 │
│ Austria │ 107 │
│ Poland │ 92 │
│ China │ 84 │
│ Denmark │ 70 │
│ United Arab Emirates │ 64 │
│ Qatar │ 62 │
│ · │ · │
│ · │ · │
│ · │ · │
│ Malaysia │ 2 │
│ Bangladesh │ 1 │
│ Georgia │ 1 │
│ Aruba │ 1 │
│ Belarus │ 1 │
│ Sri Lanka │ 1 │
│ Kenya │ 1 │
│ Mauritius │ 1 │
│ Brunei │ 1 │
│ Oman │ 1 │
│ Slovenia │ 1 │
│ Guernsey │ 1 │
│ Indonesia │ 1 │
│ Algeria │ 1 │
│ Luxembourg │ 1 │
├──────────────────────┴───────┤
│ 76 rows (30 shown) 2 columns │
└──────────────────────────────┘Kanskje du tenker at dette minner litt vel mye om det vi gjorde direkte i DuckDB. Helt enig! Nå er det på tide å ta visualiseringen et hakk videre.
Visualiser fly per land i penere tabell¶
Vi starter enkelt, med å konvertere dataene til et format som er mer egnet for moderne nettleservisning. Dette gjør vi ved hjelp av Polars – et raskt og fleksibelt Python-verktøy for databehandling.
Legg til en tekstcelle:
# Visualiser fly per land i penere tabellDeretter denne kodecellen:
import polars as pl
pl.Config(set_tbl_rows=100)
flyperland = con.sql(sql).pl()
flyperlandResultatet er en langt mer leservennlig tabell enn den rå tekstutskriften fra DuckDB:
| land | fly |
|---|---|
| “Germany” | 409 |
| “United Kingdom” | 390 |
| “Netherlands” | 289 |
| “Norway” | 206 |
| “Ireland” | 201 |
| “Turkey” | 200 |
| “Malta” | 190 |
| “United States” | 169 |
| “Sweden” | 132 |
| “Austria” | 107 |
| “Poland” | 92 |
| “China” | 84 |
| “Denmark” | 70 |
| “United Arab Emirates” | 64 |
| “Qatar” | 62 |
| “Finland” | 61 |
| “Spain” | 58 |
| “France” | 55 |
| “Russia” | 52 |
| “Portugal” | 46 |
| “Belgium” | 39 |
| “Liechtenstein” | 38 |
| “Hungary” | 37 |
| “Latvia” | 33 |
| “Iceland” | 27 |
| “Canada” | 27 |
| “Bulgaria” | 21 |
| “Czech Republic” | 16 |
| “Greece” | 16 |
| “Lithuania” | 15 |
| “Israel” | 14 |
| “India” | 14 |
| “Romania” | 13 |
| “Singapore” | 13 |
| “Egypt” | 12 |
| “Jordan” | 8 |
| “Slovakia” | 8 |
| “Ethiopia” | 7 |
| “Estonia” | 7 |
| “Serbia” | 7 |
| “Saudi Arabia” | 6 |
| “Croatia” | 6 |
| “Japan” | 5 |
| “Uzbekistan” | 5 |
| “Bahrain” | 5 |
| “San Marino” | 5 |
| “Thailand” | 5 |
| “Morocco” | 4 |
| “Australia” | 4 |
| “Isle of Man” | 4 |
| “Italy” | 4 |
| “Taiwan” | 3 |
| “Ukraine” | 3 |
| “South Korea” | 3 |
| “Azerbaijan” | 3 |
| “Kuwait” | 3 |
| “Zimbabwe” | 2 |
| “Lebanon” | 2 |
| “Pakistan” | 2 |
| “Tunisia” | 2 |
| “Malaysia” | 2 |
| “Brazil” | 2 |
| “Slovenia” | 1 |
| “Guernsey” | 1 |
| “Mauritius” | 1 |
| “Oman” | 1 |
| “Kenya” | 1 |
| “Brunei” | 1 |
| “Algeria” | 1 |
| “Luxembourg” | 1 |
| “Indonesia” | 1 |
| “Belarus” | 1 |
| “Bangladesh” | 1 |
| “Aruba” | 1 |
| “Georgia” | 1 |
| “Sri Lanka” | 1 |
Her skjer det tre ting:
Vi importerer Polars
Vi setter opp konfigurasjonen slik at tabellen kan vise opptil 100 rader
Vi bruker
.pl()til å hente resultatet fra DuckDB-spørringen som en Polars-tabell, og viser den ved å referere til variabelenflyperland
Lagre tabellen til CSV¶
En av fordelene med Polars er hvor enkelt det er å eksportere data til ulike formater. Når du har hentet ut et datasett du vil ta vare på eller bruke videre, er det for eksempel veldig lett å lagre det som en CSV-fil.
Legg til en tekstcelle:
# Lagre til CSVOg deretter denne kodecellen:
flyperland.write_csv("flyperland.csv")Hvor finner jeg filen?¶
Et godt spørsmål! Til venstre i Google Colab-vinduet finner du en vertikal meny. Klikk på mappesymbolet nederst for å åpne filvisningen. Om du ikke ser filen med en gang, kan du klikke på last inn på nytt-knappen (de to sirklene som danner en pil) for å oppdatere visningen.

Figur 4: Filoversikten i Google Colab – klikk på mappen nederst, og last inn på nytt om nødvendig.
Stolpediagram¶
Nok tall og tabeller – nå er det på tide med litt visuell magi! Og da passer det perfekt å introdusere et nytt verktøy: Plotly. Dette visualiseringsbiblioteket lar deg lage stilrene og interaktive grafer med minimale mengder kode.
La oss starte med å lage et stolpediagram over fly per land.
Legg til en tekstcelle:
# Visualiser som stolpediagramDeretter denne kodecellen:
import plotly.express as px
stolpe = px.bar(flyperland, x='land', y='fly', title="Fly per land")
stolpe
Figur 5: Fly per land visualisert som stolpediagram.
Så enkelt – og så fint! Diagrammet er interaktivt, og du kan blant annet:
Hovre med musepekeren i øvre bildekant for å få opp en meny
Zoome inn og ut
Laste ned grafen som PNG ved å trykke på kamerasymbolet
Lagre som HTML¶
Vil du lagre hele grafen med all interaktiviteten intakt? Utviklerne bak Plotly har tenkt på det også. Du kan enkelt lagre visualiseringen som en HTML-fil.
Legg til en tekstcelle:
# Lagre til HTMLDeretter denne kodecellen:
stolpe.write_html("stolpe.html")Filen dukker opp i den samme filutforskeren som vi brukte da vi lagret CSV. Klikk på mappesymbolet til venstre, og husk å trykke last inn på nytt hvis du ikke ser filen med én gang.
Kakediagram¶
Siden flyobservasjonene våre utgjør en absolutt mengde, egner de seg også godt for visualisering som kakediagram. Det krever bare en liten endring fra stolpediagrammet, og vips – dataene blir til kake.
Legg til en tekstcelle:
# Visualiser som kakediagramDeretter denne kodecellen:
kake = px.pie(flyperland, names='land', values='fly', title="Fly per land")
kake
Figur 6: Fly per land visualisert som (et litt sprengt) kakediagram.
Tada! Kakediagram, rett ut av ovnen. Men – dette ser kanskje litt sprengt ut?
Når vi presser inn 75 land med ulikt antall fly i én og samme sirkel, blir resultatet både visuelt rotete og vanskelig å lese. Mange land har bare noen få fly, og det gir lite mening å gi dem hver sin tynne kakebit.
Begrens detaljnivået – grupper små land¶
Løsningen? Vi samler alle land med færre enn 100 observerte fly i en felles gruppe: Other countries. Det gjør vi ved å legge inn litt enkel logikk i SQL-spørringen.
Legg til en tekstcelle:
# Kakediagram – grupper små landDeretter denne kodecellen:
sql = """
WITH flight_counts AS (
SELECT
flydenity_nation as land,
COUNT(DISTINCT icao) AS fly
FROM
FLIGHTS_11_07_2025
WHERE
LENGTH(flydenity_nation) > 0
GROUP BY
flydenity_nation
)
SELECT
CASE
WHEN fly < 100 THEN 'Other countries'
ELSE land
END AS land,
CAST(SUM(fly) AS BIGINT) AS fly
FROM
flight_counts
GROUP BY
1
ORDER BY
2 DESC;
"""
flyperland = con.sql(sql).pl()
kake = px.pie(flyperland, names='land', values='fly', title="Fly per land")
kakeHer har vi bakt litt enkel programmeringslogikk inn i spørringen. Kort fortalt finner vi land som har under 100 observerte fly den aktuelle tidsperioden, og legger disse til i kategorien Other countries. Da ser vi umiddelbart at kakediagrammet både blir penere og mer lesbart.

Figur 7: Fly per land visualisert som (et penere) kakediagram.
Ved hjelp av CASE WHEN-logikken over slår vi sammen alle smånasjonene i én kategori. Resultatet? Et diagram som både ser bedre ut og er langt lettere å lese.
Widerøe-flyet som nødlandet i Bergen¶
La oss vende tilbake til et eksempel vi har sett på tidligere. Fredag 11. juli 2025 nødlandet et Widerøe-fly i Bergen. Nå skal vi jobbe oss gjennom hvordan vi kan hente ut, analysere og visualisere flydata knyttet til en slik hendelse ved hjelp av notebooks – på måter som kunne vært relevante i lokal nyhetsdekning.
Hent ut data for aktuell dag¶
Vi har «jukset litt» for å spare tid, og har allerede hentet ut de nødvendige dataene for den aktuelle dagen gjennom spørringen vi kjørte tidligere i kapittelet. For å repetere:
Legg til en tekstcelle:
# Last inn data fra 11. juli 2025Deretter denne kodecellen:
con.sql("CREATE OR REPLACE TABLE FLIGHTS_11_07_2025 AS SELECT timestamp, icao, registration, squawk, flight, latitude, longitude, altitude_barometric, on_ground, ground_speed, flydenity_nation, h3_15, is_military, is_interesting, is_pia, is_ladd FROM mcn.airtraces WHERE timestamp >= '2025-07-11 00:00:00' and timestamp < '2025-07-12 00:00:00';")Finn ICAO-kode¶
Vi vet at flyet som nødlandet hadde registreringsnummer LN-WDI. For å få flest mulig datapunkter hentet ut, trenger vi som tidligere sett å finne ICAO-koden til dette flyet. Det gjør vi med denne spørringen.
Legg til en tekstcelle:
# Finn ICAO-kode for LN-WDIDeretter denne kodecellen:
sql = """
SELECT timestamp, icao, registration
FROM FLIGHTS_11_07_2025
WHERE registration = 'LN-WDI'
LIMIT 1;
"""
con.sql(sql)Dette gir oss:
┌────────────────────────────┬─────────┬──────────────┐
│ timestamp │ icao │ registration │
│ timestamp with time zone │ varchar │ varchar │
├────────────────────────────┼─────────┼──────────────┤
│ 2025-07-11 05:48:55.648+00 │ 4785BF │ LN-WDI │
└────────────────────────────┴─────────┴──────────────┘Vi ser at flyet har ICAO-koden 4785BF.
Filtrer på aktuell hendelse¶
Deretter filtrerer vi datasettet på ICAO-koden 4785BF i aktuell tidsperiode.
Legg til en tekstcelle:
# Filtrer på ICAO-kode 4785BF i aktuell tidsperiodeDeretter denne kodecellen:
sql = """
SELECT
icao,
registration,
CAST(longitude AS DOUBLE) as longitude,
CAST(latitude AS DOUBLE) as latitude,
altitude_barometric,
CAST(ground_speed AS DOUBLE) as ground_speed,
timestamp
FROM FLIGHTS_11_07_2025
WHERE icao = '4785BF'
AND timestamp >= '2025-07-11 05:40:00'
AND timestamp < '2025-07-11 06:09:00'
AND latitude IS NOT NULL
AND longitude IS NOT NULL
AND altitude_barometric IS NOT NULL
ORDER BY timestamp ASC;
"""
nodlanding = con.sql(sql).pl()
pl.Config(set_tbl_rows=10)
nodlandingResultatet viser alle datapunktene vi har om denne flyvningen, filtrert på ICAO-kode og tidsintervall:
| icao | registration | longitude | latitude | altitude_barometric | ground_speed | timestamp |
|---|---|---|---|---|---|---|
| “4785BF” | “LN-WDI” | 5.220566 | 60.286568 | -75 | 153.8 | 2025-07-11 05:42:24.781 UTC |
| “4785BF” | “LN-WDI” | 5.220566 | 60.286568 | -75 | 153.8 | 2025-07-11 05:42:24.813 UTC |
| “4785BF” | “LN-WDI” | 5.214528 | 60.303408 | 300 | 154.9 | 2025-07-11 05:42:25.543 UTC |
| “4785BF” | “LN-WDI” | 5.214244 | 60.304136 | 350 | 155.5 | 2025-07-11 05:42:26.527 UTC |
| “4785BF” | “LN-WDI” | 5.214092 | 60.304516 | 375 | 155.5 | 2025-07-11 05:42:27.069 UTC |
| … | … | … | … | … | … | … |
| “4785BF” | “LN-WDI” | 5.211497 | 60.313064 | 150 | 141.9 | 2025-07-11 06:05:56.868 UTC |
| “4785BF” | “LN-WDI” | 5.211836 | 60.312104 | 125 | 141.9 | 2025-07-11 06:05:58.348 UTC |
| “4785BF” | “LN-WDI” | 5.212918 | 60.308760 | 75 | 140.9 | 2025-07-11 06:06:03.678 UTC |
| “4785BF” | “LN-WDI” | 5.213307 | 60.307448 | 50 | 140.9 | 2025-07-11 06:06:05.660 UTC |
| “4785BF” | “LN-WDI” | 5.213503 | 60.307120 | 50 | 140.9 | 2025-07-11 06:06:06.154 UTC |
Ved å bruke pl.Config(set_tbl_rows=10) får vi automatisk vist de første og siste 5 radene i tabellen – en praktisk måte å få et raskt inntrykk av datasettet på.
Høyde og fart over tid¶
La oss se nærmere på hvordan flyet beveget seg gjennom luftrommet, både i høyde og fart.
Visualiser høyde¶
Legg til en tekstcelle:
# Flyets høyde over tidDeretter denne kodecellen:
linje = px.line(nodlanding, x='timestamp', y='altitude_barometric', title="Flyets høyde over tid")
linje
Figur 8: Flyets høyde over tid.
Visualiser fart¶
Legg til en tekstcelle:
# Flyets fart over tidDeretter denne kodecellen:
linje = px.line(nodlanding, x='timestamp', y='ground_speed', title="Flyets fart over tid")
linje
Figur 9: Flyets fart over tid.
Enkelt, oversiktlig og nyttig. Her får vi raskt et bilde av flyets bevegelser og endringer underveis. Og som vi ser – til tross for nødlandingen ser dataene så langt ganske udramatiske.
Visualisering på kart¶
Grafene gir oss innsikt, men i hendelsesjournalistikk er det ofte kartet som virkelig skaper forståelse. Heldigvis kan vi enkelt plassere flyets rute direkte på et interaktivt kart med Plotly.
Legg til en tekstcelle:
# Plott Widerøe-flighten på et kartDeretter denne kodecellen:
kart = px.line_map(nodlanding, lat='latitude', lon='longitude', hover_data=['timestamp', 'altitude_barometric', 'ground_speed'])
kart.update_layout(height=540, width=960, map_zoom=8.4, map_center_lat=60.42, map_center_lon=5.2)
kart
Figur 10: Widerøe-flyets rute plottet på kart.
Og der har vi det! Flyets ferd fra Flesland, via Norhordland, og tilbake igjen til Flesland.
Selv om det krever litt mer kode enn de tidligere grafene, er det fortsatt overkommelig. Her bruker vi line_map for å tegne ruten basert på geografiske koordinater. Ved å legge til hover_data får vi i tillegg opp info om tidspunkt, høyde og fart når vi beveger musepekeren over ruten.
Når du er fornøyd med visualiseringen, kan du eksportere kartet som en PNG eller lagre det som HTML med full interaktivitet – perfekt for bruk i en nyhetsartikkel på nett.
Heatmaps med Lonboard¶
Vi var så vidt innom heatmaps da vi jobbet med sanntidsdata tidligere i kurset. Som du kanskje husker, er dette en utmerket måte å kartlegge flyaktivitet på: Hvor er det mest trafikk, og hvordan fordeler bevegelsene seg i rom og tid?
Visualisering av slike mønstre krever ofte store datamengder – og da kommer Lonboard til sin rett. Verktøyet er spesiallaget for å håndtere store geografiske datasett, og genererer heatmaps på en både rask og imponerende måte.
La oss bruke Lonboard til å lage et heatmap av all tilgjengelig flytrafikk 11. juli 2025.
Forbered dataene¶
Nok en gang bruker vi de samme dataene som tidligere. Vi har allerede hentet dem ut, men for å repetere:
Legg til en tekstcelle:
# Last inn data fra 11. juli 2025Deretter denne kodecellen:
con.sql("CREATE OR REPLACE TABLE FLIGHTS_11_07_2025 AS SELECT timestamp, icao, registration, squawk, flight, latitude, longitude, altitude_barometric, on_ground, ground_speed, flydenity_nation, h3_15, is_military, is_interesting, is_pia, is_ladd FROM mcn.airtraces WHERE timestamp >= '2025-07-11 00:00:00' and timestamp < '2025-07-12 00:00:00';")Gjør dataene klare for Lonboard¶
Vi lager en spørring som henter ut tidspunkt, posisjon og registreringsnummer, og gjør posisjonen om til et geometriobjekt (ST_POINT).
Legg til en tekstcelle:
# Forbered data for LonboardDeretter denne kodecellen:
sql = """
SELECT
timestamp as time,
ST_POINT(longitude, latitude) as geom,
registration as regnr
FROM FLIGHTS_11_07_2025
WHERE longitude IS NOT NULL
AND latitude IS NOT NULL
GROUP BY ALL;
"""
query = con.sql(sql)Generer heatmap¶
Nå er alt klart! Vi bruker Lonboards HeatmapLayer og tegner det på et interaktivt kart.
Legg til en tekstcelle:
# Generer heatmap med LonboardDeretter denne kodecellen:
from lonboard import Map, HeatmapLayer, PathLayer
layer = HeatmapLayer.from_duckdb(query, con)
m = Map(layer)
mOg voilà – du får opp et kart som viser hvor det var mest aktivitet i luftrommet denne dagen. De varme, røde områdene indikerer høy konsentrasjon av datapunkter, mens kjøligere områder har mindre trafikk.

Figur 11: Heatmap av flytrafikk 11. juli 2025, generert med Lonboard.
Politihelikopternes bevegelser¶
Det kan være nyttig å se all flytrafikk på ett kart – men det er enda mer spennende når vi snevrer det inn. La oss undersøke politihelikopternes bevegelser 11. juli 2025.
Finn alle observasjoner¶
Legg til en tekstcelle:
# Observasjoner av politihelikoptre 11. juli 2025Deretter denne kodecellen:
sql = """
SELECT
timestamp as time,
ST_POINT(longitude, latitude) as geom,
registration as regnr
FROM FLIGHTS_11_07_2025
WHERE longitude IS NOT NULL
AND latitude IS NOT NULL
AND (registration = 'LN-ORA' OR registration = 'LN-ORB' OR registration = 'LN-ORC')
GROUP BY ALL;
"""
query = con.sql(sql)Generer heatmap for politihelikoptre¶
Legg til en tekstcelle:
# Heatmap av politihelikoptreDeretter denne kodecellen:
from lonboard import Map, HeatmapLayer, PathLayer
layer = HeatmapLayer.from_duckdb(query, con)
m = Map(layer)
m
Figur 12: Heatmap av politihelikoptre 11. juli 2025, generert med Lonboard.
Se der, ja! Vi ser tydelig en konsentrasjon av aktivitet på Østlandet. Men hvilket helikopter var i luften – og når?
Oversikt over aktivitet per time¶
Legg til en tekstcelle:
# Se hvilket politihelikopter som var i luften nårDeretter denne kodecellen:
sql = """
SELECT
registration as regnr,
EXTRACT(HOUR FROM CAST(timestamp AS TIMESTAMP)) AS time,
COUNT(*) AS datapunkter
FROM FLIGHTS_11_07_2025
WHERE (registration = 'LN-ORA' OR registration = 'LN-ORB' OR registration = 'LN-ORC')
GROUP BY registration, time
ORDER BY time;
"""
query = con.sql(sql)
query.pl()Dette viser antall datapunkter per registreringsnummer og time (time på døgnet):
| regnr | time | datapunkter |
|---|---|---|
| “LN-ORA” | 0 | 1273 |
| “LN-ORB” | 0 | 4 |
| “LN-ORB” | 1 | 96 |
| “LN-ORB” | 18 | 533 |
| “LN-ORB” | 19 | 372 |
| “LN-ORC” | 19 | 162 |
| “LN-ORB” | 20 | 162 |
Her ser vi at alle helikopterne var aktive i løpet av dagen. LN-ORA og LN-ORB var begge i luften rundt midnatt UTC, mens LN-ORB og LN-ORC tok av igjen neste kveld. La oss se på LN-ORA, som har flest punkter på kortest tid.
Visualiser bevegelsene til LN-ORA¶
Legg til en tekstcelle:
# LN-ORA 11. juli 2025Deretter denne kodecellen:
sql = """
SELECT
timestamp as time,
ST_POINT(longitude, latitude) as geom,
registration as registration
FROM FLIGHTS_11_07_2025
WHERE longitude IS NOT NULL
AND latitude IS NOT NULL
AND registration = 'LN-ORA'
GROUP BY ALL;
"""
query = con.sql(sql)Deretter denne kodecellen:
from lonboard import viz
viz(query)
Figur 13: Bevegelser for politihelikopteret LN-ORA, visualisert med Lonboard.
Nå ser vi alle registrerte bevegelser for LN-ORA denne dagen. Klikker du med musepekeren på brødsmulestien på kartet, får du opp tidspunkter – og du kunne lett lagt til høyde, fart og andre datapunkter om ønskelig.
Visualisere et geografisk område¶
Tidligere i kurset har vi sett hvordan vi kan filtrere flydata basert på geografiske områder. Nå henter vi frem igjen H3-systemet, og bruker det til å lage en visualisering av flytrafikken i en gitt sektor.
Ved å velge en H3-sektor med oppløsning 2 midt i Sør-Norge, kan vi hente ut all flytrafikk i dette området for datoen vår, 11. juli 2025.
Legg til en tekstcelle:
# Visualisere et geografisk områdeDeretter denne kodecellen:
sql = """
SELECT
registration as regnr,
EXTRACT(HOUR FROM CAST(timestamp AS TIMESTAMP)) AS time,
COUNT(*) AS datapunkter,
ST_POINT(longitude, latitude) as geom,
timestamp
FROM FLIGHTS_11_07_2025
WHERE h3_cell_to_parent(h3_15, 2) = '82098ffffffffff'
GROUP BY ALL
ORDER BY time;
"""
query = con.sql(sql)Heatmap over all trafikk i en gitt sektor¶
Vi fortsetter med å lage en Lonboard-heatmap av sektoren vår.
Legg til en tekstcelle:
# Lage heatmap av geografisk områdeDeretter denne kodecellen:
layer = HeatmapLayer.from_duckdb(query, con)
m = Map(layer)
m
Figur 14: Heatmap av flyaktivitet i en H3-sektor.
Interessant! Vi kan nesten ane heksagonens konturer. Men vi gjør det enda tydeligere:
Viz-kart over all trafikk i en gitt sektor¶
Legg til en tekstcelle:
# Lage Viz-kart av geografisk områdeDeretter denne kodecellen:
viz(query)
Figur 15: Viz-kart over geografisk område.
Enkle linjer – og nå ser vi tydelig H3-heksagonens form. Og som alltid med Viz-kart: du kan klikke med musepekeren på brødsmulestiene for å se detaljer som fartøyenes registreringsnummer og tidspunkter.
Visualisere all militær trafikk¶
Nå gjør vi en liten justering – og bruker samme metode for å vise all militærtrafikk i datasettet. Alt vi trenger er et lite filter til.
Legg til en tekstcelle:
# Visualisere all militær trafikkDeretter denne kodecellen:
sql = """
SELECT
timestamp as time,
ST_POINT(longitude, latitude) as geom,
registration as regnr
FROM FLIGHTS_11_07_2025
WHERE longitude IS NOT NULL
AND latitude IS NOT NULL
AND is_military
GROUP BY ALL;
"""
query = con.sql(sql)Lage heatmap av all militær trafikk¶
Legg til en tekstcelle:
# Lage heatmap av all militær trafikkDeretter denne kodecellen:
layer = HeatmapLayer.from_duckdb(query, con)
m = Map(layer)
m
Figur 16: Heatmap av all militær trafikk 11. juli 2025.
Her var det litt å boltre seg i. La oss visualisere de samme dataene på et Viz-kart:
Viz-kart med detaljert info¶
Legg til en tekstcelle:
# Lage Viz-kart av all militær trafikkDeretter denne kodecellen:
from lonboard import viz
viz(query)
Figur 17: Viz-kart over militær trafikk.
Her får vi den fulle oversikten – og du kan enkelt undersøke hvilke fartøy som var i luften, hvor og når.
Oppsummering¶
I dette kapittelet har vi utforsket hvordan notebooks kan brukes til å analysere og visualisere flydata. La oss oppsummere de viktigste punktene:
Hva er notebooks?¶
Interaktive arbeidsbøker som kombinerer kode, tekst og visualiseringer
Kjører kode stykkevis i celler, lar oss jobbe iterativt
Dokumenterer hele arbeidsprosessen
Kan kjøres lokalt gjennom f.eks Jupyter Notebooks, eller i skyen gjennom f.eks Google Colab.
Verktøykassen¶
DuckDB
Samme SQL-spørrespråk som tidligere, integrert i Python
Kobles til Medieklyngens datasjø
Polars
Konverterer DuckDB-resultater til lesbare tabeller
Eksport til CSV:
flyperland.write_csv("flyperland.csv")
Plotly
Lager interaktive grafer og kart
Eksport til HTML:
stolpe.write_html("stolpe.html")
Lonboard
Håndterer store geografiske datasett
Vi så på
HeatmapLayerogviz
Praktiske eksempler¶
Vi analyserte over 7,1 millioner datapunkter fra 11. juli 2025, inkludert:
Widerøe-nødlanding (LN-WDI, ICAO 4785BF)
Politihelikopternes bevegelser
Militær flytrafikk
Trafikk i spesifikke H3-sektorer
Viktige læringspunkter¶
Notebooks kombinerer tekst og kode
Tekstceller dokumenterer, kodeceller kjører analyser, spørringer og visualiseringer
DuckDB, Polars, Plotly og Lonboard kjøres sømløst i notebooks
Visualiseringer vises direkte i notebooks
Resultater eksporteres til CSV, HTML eller andre formater
Store datasett håndteres effektivt med Lonboard
Med denne kunnskapen kan du nå kombinere SQL-analyse med kraftige visualiseringer i en dokumentert arbeidsflyt!
Oppgaver¶
Nå har du fått en grunnleggende innføring i bruk av notebooks, DuckDB, Polars, Plotly og Lonboard. Tid for litt praktisk trening: