Når vi nå har brukt litt tid på å hente ut og analysere data med DuckDB, er neste steg å jobbe videre med materialet gjennom visualisering. Det kan vi gjøre 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 kode-editor, hvor du kan kombinere forklarende tekst, bilder og kode – alt i én og samme arbeidsflate. Det er et populært verktøy for datavitenskap, maskinlæring, undervisning – og ikke minst, datajournalistikk.
De viktigste egenskapene:
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 og grafverktøyet Plotly. Det er flere nye verktøy, 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å det 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.
Slik går du frem:
Legg til en tekstcelle¶
Klikk på Tekst-knappen i menylinjen.
Figur 2: Legg til ny tekstlinje.
Skriv følgende:
# Installer nødevendige bibliotekerLegg til en kodecelle¶
Klikk på Kode-knappen, og skriv inn:
!pip install duckdb plotly.expressDette installerer DuckDB og Plotly i notatboken.
Trykk på play-ikonet til venstre for cellen for å kjøre den. Du ser nå at Colab laster ned og installerer verktøyene.
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 spatial;')
con.sql('INSTALL httpfs;')
con.sql('INSTALL h3 FROM community;')
con.sql('LOAD spatial;')
con.sql('LOAD httpfs;')
con.sql('LOAD h3;')Du vil kanskje kjenne igjen noen av kommandoene, selv om syntaksen nå er litt annerledes. Kort fortalt er de skrevet i Python og sendes videre til DuckDB-biblioteket for å kjøres.
Kjør cellen med play-knappen. Når alt er ferdig, vises et grønt avhukingssymbol som bekrefter at alt fungerte som det skulle.
Last opp og importer data¶
Klikk på mappeikonet i venstremenyen.
Trykk på opplastingsikonet (pil opp) og velg Parquet-filene fra datamaskinen din.
Figur 4: Last opp filer fra maskinen din.
Opprett en ny tekstcelle:
# Les inn dataeneOpprett en ny kodecelle og les inn dataene:
con.sql('CREATE TABLE haisdata AS SELECT * FROM "*.parquet";')Opprett en ny tekstcelle:
# Test at alt fungererOpprett en ny kodecelle og test at alt fungerer:
con.sql('DESCRIBE haisdata;').pl()| column_name | column_type | null | key | default | extra |
|---|---|---|---|---|---|
| date_time_utc | TIMESTAMP | YES | null | null | null |
| mmsi | INTEGER | YES | null | null | null |
| longitude | DOUBLE | YES | null | null | null |
| latitude | DOUBLE | YES | null | null | null |
| status | TINYINT | YES | null | null | null |
| ... | ... | ... | ... | ... | ... |
| data_source | VARCHAR | YES | null | null | null |
| ais_class | VARCHAR | YES | null | null | null |
| hex_7 | BIGINT | YES | null | null | null |
| hex_14 | BIGINT | YES | null | null | null |
| geometry | GEOMETRY | YES | null | null | null |
Nå er datene klare til analyse, visualisering og videre utforskning.
Tell antall datapunkter¶
Legg til en ny tekstcelle:
# Tell antall datapunkterOg deretter en kodecelle:
con.sql('SELECT count(*) FROM haisdata')Du får et svar som dette:
┌──────────────┐
│ count_star() │
│ int64 │
├──────────────┤
│ 236934 │
└──────────────┘Over 239 tusen rader – et solid utgangspunkt!
Se på datastrukturen¶
Legg inn en ny tekstcelle:
# Beskriv datastrukturenOg legg deretter inn denne kodecellen:
con.sql('DESCRIBE haisdata').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 │
├────────────────────┼───────────────┼─────────┼─────────┼─────────┼─────────┤
│ date_time_utc │ TIMESTAMP │ YES │ NULL │ NULL │ NULL │
│ mmsi │ INTEGER │ YES │ NULL │ NULL │ NULL │
│ longitude │ DOUBLE │ YES │ NULL │ NULL │ NULL │
│ latitude │ DOUBLE │ YES │ NULL │ NULL │ NULL │
│ status │ TINYINT │ YES │ NULL │ NULL │ NULL │
│ course_over_ground │ DOUBLE │ YES │ NULL │ NULL │ NULL │
│ true_heading │ SMALLINT │ YES │ NULL │ NULL │ NULL │
│ speed_over_ground │ DOUBLE │ YES │ NULL │ NULL │ NULL │
│ rate_of_turn │ SMALLINT │ YES │ NULL │ NULL │ NULL │
│ maneuvre │ SMALLINT │ YES │ NULL │ NULL │ NULL │
│ imo │ INTEGER │ YES │ NULL │ NULL │ NULL │
│ callsign │ VARCHAR │ YES │ NULL │ NULL │ NULL │
│ ship_name │ VARCHAR │ YES │ NULL │ NULL │ NULL │
│ ship_type │ TINYINT │ YES │ NULL │ NULL │ NULL │
│ length │ SMALLINT │ YES │ NULL │ NULL │ NULL │
│ draught │ DECIMAL(10,2) │ YES │ NULL │ NULL │ NULL │
│ data_source │ VARCHAR │ YES │ NULL │ NULL │ NULL │
│ ais_class │ VARCHAR │ YES │ NULL │ NULL │ NULL │
│ hex_7 │ BIGINT │ YES │ NULL │ NULL │ NULL │
│ hex_14 │ BIGINT │ YES │ NULL │ NULL │ NULL │
│ geometry │ GEOMETRY │ YES │ NULL │ NULL │ NULL │
├────────────────────┴───────────────┴─────────┴─────────┴─────────┴─────────┤
│ 21 rows 6 columns │
└────────────────────────────────────────────────────────────────────────────┘Unike fartøy i datasettet¶
La oss gjøre en øvelse vi også har gjort tidligere i DuckDB: Vi finner unike fartøy i datasettet.
Start med å legge inn følgende tekstcelle:
# Filtrer på unike fartøyDeretter legger vi inn denne kodecellen:
sql = """
SELECT DISTINCT(mmsi), callsign, ship_name FROM haisdata ORDER BY ship_name;
"""
con.sql(sql).show(max_rows=30)
Denne spørringen gir oss en oversikt over hvor mange unike fartøy som finnes i datasettet. Utvalget vises med maksimum 30 rader:
┌───────────┬──────────┬──────────────────────┐
│ mmsi │ callsign │ ship_name │
│ int32 │ varchar │ varchar │
├───────────┼──────────┼──────────────────────┤
│ 235076245 │ 2CVX9 │ EDZARD SCHULTE │
│ 247302900 │ ICPE │ AIDASOL │
│ 244120000 │ PCYH │ ANTEOS │
│ 244120000 │ P C Y H │ ANTEOS │
│ 636020363 │ D5YW3 │ AQUASMERALDA │
│ 636023947 │ 5LQF3 │ ASPEN │
│ 246598000 │ PBTQ │ BEATRIX │
│ 209356000 │ 5BBK5 │ BORIS DAVYDOV │
│ 255806224 │ CQAN6 │ BOTHNIA │
│ 257619000 │ LFGV │ CAPRICE │
│ 255805753 │ CQCC │ CHRISTIAN ESSBERGER │
│ 215349000 │ 9HA2707 │ CSL TRIMNES │
│ 311023800 │ C6XW3 │ DEEP ARCTIC │
│ 310767000 │ ZCEC8 │ DEEPSEA STAVANGER │
│ 257089140 │ LAIX8 │ EAGLE BLANE │
│ · │ · │ · │
│ · │ · │ · │
│ · │ · │ · │
│ 257286000 │ LDZH │ SIEM PEARL │
│ 209190000 │ 5BXG4 │ SIF W │
│ 253309000 │ LXUB │ SIMON STEVIN │
│ 259888000 │ LAHA7 │ STAVFJORD │
│ 538010467 │ V7A6081 │ STI MYSTERY │
│ 538006343 │ V7MB8 │ STI SPIGA │
│ 246695000 │ PCKX │ STORNES │
│ 258527000 │ JWRE │ STRIL MAR │
│ 257129000 │ LGLA │ STRIL MERKUR │
│ 257600000 │ LAGH7 │ SYDSTRAUM │
│ 241456000 │ SVCL8 │ THOMAS ZAFIRAS │
│ 258390000 │ LLVY │ VIKING ENERGY │
│ 257271000 │ LACZ8 │ VOLANTIS │
│ 257970000 │ LAUR5 │ XANTHIA │
│ 311000634 │ C6DC2 │ YAKOV GAKKEL │
├───────────┴──────────┴──────────────────────┤
│ 75 rows (30 shown) 3 columns │
└─────────────────────────────────────────────┘Vi kjenner igjen resultatet fra tidligere, men vi kjører spørringen på en litt ny måte. Vi starter med sql = """, og legger inn SQL-spørringen som en tekstblokk. På den måten oppretter vi en variabel som inneholder spørringen – noe som gjør det enkelt å gjenbruke den senere, om vi ønsker. Her kjører vi den bare én gang, ved å sende variabelen inn i DuckDB-forbindelsen vår med con.sql(sql). Vi ber samtidig om å vise maksimalt 30 rader med show(max_rows=30).
Kanskje du, som meg, tenker at det minner litt vel mye om det vi gjorde direkte i DuckDB. Helt enig! Nå er det på tide å ta visualiseringen et hakk videre.
Visualiser 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.
Tekstcelle:
# Visualiser fartøyene i en penere tabellKodecelle:
import polars as pl
pl.Config(set_tbl_rows=100)
fartoy = con.sql(sql).pl()
fartoy| mmsi | callsign | ship_name |
|---|---|---|
| 247302900 | “ICPE” | “AIDASOL” |
| 244120000 | “PCYH” | “ANTEOS” |
| 244120000 | “P C Y H” | “ANTEOS” |
| 636020363 | “D5YW3” | “AQUASMERALDA” |
| 636023947 | “5LQF3” | “ASPEN” |
| 246598000 | “PBTQ” | “BEATRIX” |
| 209356000 | “5BBK5” | “BORIS DAVYDOV” |
| 255806224 | “CQAN6” | “BOTHNIA” |
| 257619000 | “LFGV” | “CAPRICE” |
| 255805753 | “CQCC” | “CHRISTIAN ESSBERGER” |
| 215349000 | “9HA2707” | “CSL TRIMNES” |
| 311023800 | “C6XW3” | “DEEP ARCTIC” |
| 310767000 | “ZCEC8” | “DEEPSEA STAVANGER” |
| 257089140 | “LAIX8” | “EAGLE BLANE” |
| 538006249 | “V7LA9” | “ECO ROYALTY” |
| 259665000 | “JWMZ3” | “EDDA FAUNA” |
| 231700000 | “OZ2077” | “ELDBORG” |
| 305530000 | “V2ER8” | “FITNES” |
| 220151000 | “OWQD” | “FRIDA AMALIE” |
| 245806000 | “PCNZ” | “FULDABORG” |
| 266235000 | “SJLF” | “FURE WEST” |
| 257105000 | “LMEL” | “G.O.SARS” |
| 211210150 | “DRAA” | “GERMAN WARSHIP F219” |
| 235526000 | “ZIUB9” | “GRAMPIAN DEFENDER” |
| 636092960 | “D5XR2” | “GW ELENI” |
| 258277000 | “LAVS7” | “HANNE KNUTSEN” |
| 231099000 | “OZ2088” | “HAV NES” |
| 259074000 | “LMZT” | “HAVILA CLIPPER” |
| 257438000 | “LCCC” | “HAVILA HEROY” |
| 259073000 | “LGSY3” | “HAVILA SUBSEA” |
| 219022165 | “OWBS” | “HM228 PONDUS” |
| 230687000 | “OJTE” | “JAARLI” |
| 230688000 | “OJTF” | “JATULI” |
| 205771000 | “ONJY” | “JOAN” |
| 258906000 | “LDLA” | “JUANITA” |
| 538008981 | “V7A4152” | “KANGTING” |
| 355600000 | “3EYL9” | “KARAVAS” |
| 236111791 | “ZDKS2” | “KEY BREEZE” |
| 248221000 | “9HA2267” | “KEY SOUTH” |
| 219597000 | “OZOI” | “L56 ANNA LISE” |
| 258293000 | “LNWA” | “M/V EIDHOLM” |
| 219668000 | “OUKU 2” | “MALIK ARCTICA” |
| 538010289 | “V7A5850” | “MIRAI” |
| 219023236 | “OXAB” | “MYGGENES” |
| 311027500 | “C6YA9” | “NANSEN SPIRIT” |
| 245974000 | “PARE” | “NATO WARSHIP A900” |
| 244534000 | “PFIB” | “NEW AMSTERDAM” |
| 636019498 | “D5UU5” | “NING MAY” |
| 258152000 | “LAWE7” | “NORDSTRAUM” |
| 259827000 | “LGPE” | “NORMAND SIGMA” |
| 257184000 | “LDGE” | “NORTH CRUYS” |
| 258895000 | “LDAD” | “NORTH POMOR” |
| 311027600 | “C6YM3” | “PEARY SPIRIT” |
| 259014300 | “JXQO” | “REBEKKA L” |
| 257297000 | “LLUO” | “REM STAR” |
| 310805000 | “ZCEZ8” | “RENAISSANCE” |
| 311000627 | “C6DB3” | “RUDOLF SAMOYLOVICH” |
| 231850000 | “OZ2076” | “SAEBORG” |
| 232419000 | “MZHR7” | “SEVEN NAVICA” |
| 257286000 | “LDZH” | “SIEM PEARL” |
| 209190000 | “5BXG4” | “SIF W” |
| 253309000 | “LXUB” | “SIMON STEVIN” |
| 259888000 | “LAHA7” | “STAVFJORD” |
| 538010467 | “V7A6081” | “STI MYSTERY” |
| 538006343 | “V7MB8” | “STI SPIGA” |
| 246695000 | “PCKX” | “STORNES” |
| 258527000 | “JWRE” | “STRIL MAR” |
| 257129000 | “LGLA” | “STRIL MERKUR” |
| 257600000 | “LAGH7” | “SYDSTRAUM” |
| 241456000 | “SVCL8” | “THOMAS ZAFIRAS” |
| 258390000 | “LLVY” | “VIKING ENERGY” |
| 257271000 | “LACZ8” | “VOLANTIS” |
| 257970000 | “LAUR5” | “XANTHIA” |
| 311000634 | “C6DC2” | “YAKOV GAKKEL” |
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 variabelenfartøy.
Resultatet er en langt mer leservennlig tabell enn den rå tekstutskriften fra DuckDB. Du får oversikt over alle land, sortert etter fartøynavn – og du kan enkelt bruke dataene videre i grafer, kart og annen visualisering.
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 enkelt å lagre det som en CSV-fil:
Legg til en tekstcelle:
# Lagre til CSVOg deretter denne kodecellen:
fartoy.write_csv("fartoy.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 5: Filoversikten i Google Colab – klikk på mappen nederst, og last inn på nytt om nødvendig.
Søylediagram¶
Nok tall og tabeller – nå er det på tide med litt visuell magi! Og da passer det perfekt å introdusere et nytt verktøy på scenen: Plotly. Dette visualiseringsbiblioteket lar deg lage stilrene og interaktive grafer med minimale mengder kode.
La oss generere en oversikt over skip etter type:
Tekstcelle:
# Skip etter typeKodecelle:
sql = """
SELECT ship_type as type,
COUNT(DISTINCT mmsi) AS fartoy
FROM haisdata
GROUP BY 1
ORDER BY 1 DESC
"""
skip_etter_type = con.sql(sql).pl()
skip_etter_type Tekstcelle:
# Visualiser som søylediagramKodecelle:
import plotly.express as px
stolpe = px.bar(skip_etter_type, x='type', y='fartoy', title="Fartøy etter type")
stolpeSå 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.
Trykke på kamerasymbolet for å laste ned grafen som PNG.
Vil du lagre hele grafen med all interaktiviteten intakt? Plotly har tenkt på det også. Du kan enkelt lagre visualiseringen som en HTML-fil:
Tekstcelle:
# Lagre til HTMLKodecelle:
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.
Visualisering på kart¶
Grafer gir oss innsikt, men i hendelsesjournalistikk er det ofte kartet som virkelig skaper forståelse. Heldigvis kan vi enkelt plassere fartøyets rute direkte på et interaktivt kart med Plotly:
Tekstcelle:
# Juanita på et kartKodecelle:
sql = """
SELECT date_time_utc, mmsi, ship_name, longitude, latitude
FROM haisdata
WHERE mmsi = '258906000'
ORDER BY date_time_utc
"""
juanita = con.sql(sql).pl()
pl.Config(set_tbl_rows=10)
juanitaKodecelle:
kart = px.line_map(juanita, lat='latitude', lon='longitude', hover_data=['date_time_utc', 'mmsi', 'ship_name'])
kart.update_layout(height=540, width=960, map_zoom=6, map_center_lat = 59.5, map_center_lon = 6)
kartOppgaver¶
Nå har du fått en grunnleggende innføring i bruk av notebooks, DuckDB, Polars og Plotly. Tid for litt praktisk trening: