Skip to article frontmatterSkip to article content
Sensorjournalistikk

Visualisering i notebooks 🗺️

Medieklyngen

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:

  1. 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.

  2. 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.

  3. Visualiseringer: Grafer, kart og diagrammer kan vises direkte i notatboken.

  4. 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:

  1. Gå til Google Colab.

  2. 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.

Slik går du frem:

Legg til en tekstcelle

  1. Klikk på Tekst-knappen i menylinjen.

Figur 2: Legg til ny tekstlinje.

  1. Skriv følgende:

# Installer nødevendige biblioteker

Legg til en kodecelle

  1. Klikk på Kode-knappen, og skriv inn:

!pip install duckdb plotly.express

Dette installerer DuckDB og Plotly i notatboken.

  1. 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

  1. Lag en ny tekstcelle med følgende innhold:

# Last inn DuckDB og nødvendige tilleggsmoduler
  1. Deretter 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.

  1. 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

  1. Klikk på mappeikonet i venstremenyen.

  2. Trykk på opplastingsikonet (pil opp) og velg Parquet-filene fra datamaskinen din.

Figur 4: Last opp filer fra maskinen din.

  1. Opprett en ny tekstcelle:

# Les inn dataene
  1. Opprett en ny kodecelle og les inn dataene:

con.sql('CREATE TABLE haisdata AS SELECT * FROM "*.parquet";')
  1. Opprett en ny tekstcelle:

# Test at alt fungerer
  1. Opprett en ny kodecelle og test at alt fungerer:

con.sql('DESCRIBE haisdata;').pl()
column_namecolumn_typenullkeydefaultextra
date_time_utcTIMESTAMPYESnullnullnull
mmsiINTEGERYESnullnullnull
longitudeDOUBLEYESnullnullnull
latitudeDOUBLEYESnullnullnull
statusTINYINTYESnullnullnull
..................
data_sourceVARCHARYESnullnullnull
ais_classVARCHARYESnullnullnull
hex_7BIGINTYESnullnullnull
hex_14BIGINTYESnullnullnull
geometryGEOMETRYYESnullnullnull

Nå er datene klare til analyse, visualisering og videre utforskning.

Tell antall datapunkter

Legg til en ny tekstcelle:

# Tell antall datapunkter

Og 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 datastrukturen

Og 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øy

Deretter 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 tabell

Kodecelle:

import polars as pl

pl.Config(set_tbl_rows=100)
fartoy = con.sql(sql).pl()
fartoy
mmsicallsignship_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:

  1. Vi importerer Polars.

  2. Vi setter opp konfigurasjonen slik at tabellen kan vise opptil 100 rader.

  3. Vi bruker .pl() til å hente resultatet fra DuckDB-spørringen som en Polars-tabell, og viser den ved å referere til variabelen fartø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 CSV

Og 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 type

Kodecelle:

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øylediagram

Kodecelle:

import plotly.express as px

stolpe = px.bar(skip_etter_type, x='type', y='fartoy', title="Fartøy etter type")
stolpe

Så enkelt – og så fint! Diagrammet er interaktivt, og du kan blant annet:

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 HTML

Kodecelle:

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 kart

Kodecelle:

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)
juanita

Kodecelle:

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)
kart

Oppgaver

Nå har du fått en grunnleggende innføring i bruk av notebooks, DuckDB, Polars og Plotly. Tid for litt praktisk trening: