Første eksperiment med kort

Forfattet af

Klart som blæk

Senest opdateret

31.1.2024

Vores søgeinterface er ordnet kronologisk. Det er på mange måder en konventionel fremstilling, der givetvis vil svare til de fleste brugeres forventning. Imidlertid kan man forestille sig andre indgange til artiklerne. En oplagt mulighed er at forsøge at præsentere materialet på kort. Denne artikel demonstrerer et eksperiment i at spatialisere vores data og skabe en indgang, der tillader en interaktiv indgang til disse data.

Skabelse af et brugbart datasæt over danske stedkoordinater

Der findes ikke noget udtømmende datasæt over stednavne og koordinater på lokaliteter fra starten af 1800-tallet. Til gengæld findes der flere ganske udførlige datasæt med moderne stednavne. I denne guide bruger vi et datasæt over danske stednavne med tilhørende koordinater hentet fra geonames.org. Datasættet har den fordel, at det også indeholder synonymer og stavevariationer for hvert stednavn. Imidlertid er der også mange dubletter og homonymer.

Det følgende kode er et forsøg på at transformere data fra geonames til et format, der kan bruges til vores formål: altså et format, hvor der for hver variation af et stednavn findes et enkelt sæt af koordinater.

library(tidyverse)

df <- read_delim("data/DK.txt", col_names = NULL) %>% 
  select(X2, X3, X4, X5, X6, X15)

df2 <- df %>% 
  mutate(Stednavn = paste(X2, X3, X4, sep = ",")) %>%
  select(Stednavn, X5, X6, X15) %>% 
  mutate(sted = str_split(Stednavn, ",")) %>% 
  unnest(sted) %>% 
  rename(længdegrad = X6) %>% 
  rename(breddegrad = X5) %>% 
  select(sted, længdegrad, breddegrad, X15)

df2_duplicates <- df2 %>% 
  filter(duplicated(sted))

df3 <- df2 %>% 
  anti_join(df2_duplicates)

df_duplicates_unique <- df2_duplicates %>% 
  filter(X15 > 0) %>% 
  unique()

df_4 <- df3 %>% 
  rbind(df_duplicates_unique)

write_csv(df_4, "data/Stedkoordinater.csv")

Så langt så godt. Imidlertid ved vi også, at datasættet langt fra er komplet. Især på mindre landsbyer er det upræcist af den simple grund, at der findes mange landsbyer med samme navn i Danmark; Astrup, Søby, Borup osv. Som princip valgte vi i tilfælde af homonymi at beholde den lokalitet datasættet har knyttet til flest indbyggere. Dette sker dog først senere i koden.

Identifikation af stednavne

At identificere stednavne med et enkelt sæt af koordinater er dermed ikke så ligetil, som man skulle tro. Et større problem er imidlertid at identificere teksternes stednavne. Det er en kendt udfordring, at de fleste værktøjer til automatiseret identifikation af enkelte tekstelementer bygger på moderne tekstkorpora.1 Det samme gør sig gældende på dansk, hvor der findes udmærkede modeller til såkaldt “named entity recognition” til moderne dansk. Disse kan identificere steder og personer med imponerende præcision. Men præcisionen falder dramatisk, når samme modeller benyttes på ældre versioner af samme sprog.

Alligevel gjorde vi forsøget. Vi brugte den meget udbredte Spacy-pakke, der egentlig er en python-pakke, men også kan benyttes via en såkaldt R-wrapper, der tillader interaktion med python via R. For at lette forvirringen ved 1800-tallets brug af store bogstaver (der leder til en fejlagtig identifikation af en lang række navneord som egennavne), gjorde vi alt tekst småt. Koden ses her:

library(reticulate)
library(spacyr)

# Initialiser en spacy-session. Hvis der endnu ikke er installeret en sprogmodel gøres det som følger: spacy_install(spacy_install(lang_models = "da_core_news_md", python_version = "3.9"))
spacy_initialize(model = "da_core_news_md")

# Importer og tilpas datasæt
df <- read_csv2("data/df_housekept.csv") %>% 
  rownames_to_column(var = "doc_id")

df_prepped <- df %>% 
  rename(text = tekst) %>% 
  mutate(text = str_to_lower(text)) %>% 
  select(doc_id, text)

# Identificer entiteter og behold alle lokaliteter
entities <- spacy_extract_entity(df_prepped) %>%
  filter(ent_type == "LOC")
  
entities_grouped <- entities %>% 
  group_by(doc_id) %>% 
  mutate(steder = paste0(text, collapse = "; ")) %>% 
  select(doc_id, steder) %>% 
  unique() %>% 
  mutate(doc_id = str_remove(doc_id, "text"))

# Skab en ny datastruktur med en artikel pr. stednavn
df_steder <- df %>% 
  left_join(entities_grouped)

# Og gem eet
df_steder %>% 
  write_csv("df_spacy_steder.csv")

#Luk SpaCy
spacy_finalize()

Resultatet var en version af vores datasæt, hvor hver artikel, der indeholder et identificeret stednavn optræder - potentielt endda flere gange, fordi der findes en observation pr. stednavn. I vores endelige præsentation vil en enkelt artikel altså kunne optræde flere steder, afhængigt af, hvilke stednavne den indeholder.

Join af data

Næste skridt var at flette de to datasæt sammen. Det skete ved at bruge stednavnet som en nøgle.

I samme proces lavede vi en række manuelle tilpasninger og slettede en række stednavne, der enten var systematisk fejlgenkendt eller blev knyttet til forkerte placeringer, fordi de er homonymer. Fejlidentifikationer kunne f.eks. være personnavne, der er identiske med steder.

stednavne <- read_csv("Stedkoordinater.csv") %>% 
  mutate(sted = str_to_lower(sted)) %>% 
  na.omit() %>% 
  group_by(sted) %>% 
  slice_max(X15, n = 1)

df_spacy <- read_csv("df_spacy_steder.csv") %>% 
  na.omit() %>% 
  mutate(sted = str_split(steder, "; ")) %>% 
  unnest(sted) %>% 
  mutate(sted = str_replace(sted, "lindholm", "nørresundby")) %>% 
  left_join(stednavne) %>%
  na.omit() %>% 
  filter(sted != "danmark") %>% 
  filter(sted != "paris") %>% 
  filter(sted != "norge") %>% 
  filter(sted != "holmen") %>%
  filter(sted != "tofte") %>% 
  filter(sted != "tommerup") %>% 
  filter(sted != "mollerup") %>% 
  filter(sted != "petersborg") %>% 
  filter(sted != "lund") %>% 
#  filter(!str_detect(sted, "strup")) %>% 
  select(dato, tekst, indeks, seneste_overskrift, længdegrad, breddegrad, sted)

df_spacy %>% write_csv("data/df_spacy_koordinater.csv")

Hele processen medfører et stort tab af data. Hvor stort er svært at sige præcist, men det er stort. Der er naturligvis det åbenlyse tab: Vi har kun stedkoordinater vedr. danske lokaliteter - men avisen har i høj grad et internationalt udsyn. Mindre systematisk er den store mængde af steder, algoritmen ikke kunne identificere i teksterne. Den uindviede risikerer at overse dette tab, fordi den store mængde data betyder, at algoritmen alligevel er lykkedes med at identificere mange - ofte korrekte - lokaliteter. Det fremstår derved som om, at eksperimentet er lykkedes. Men tabet er der som en tavshed. F.eks. ved vi i denne sammenhæng, at en del artikler handler om stormfloden ved Limfjordstangen i 1825. Men algoritmen finder sjældent de relevante steder - Agger og Thyborøn. Derimod findes enkelte af disse artikler knyttet til stednavnet Thy, hvilket betyder, at artiklerne ikke placeres på selve det konkrete sted, de omtaler. Omtaler af landsbyen Toft, der efter stormfloden blev opgivet og slugt af havet, må ydermere sies fra, grundet ordets homonymi. På denne måde illustrerer resultatet både potentialer og farer ved brugen af værktøjer, der ikke er skræddersyet historiske data.

Skabelsen af et kort-app

For at vise mulighederne ved en spatial præsentation, skabte vi et simpelt app-interface til de kortlagte data. Dette tillader, at man klikker på et stednavn og læser den artikel, der omtaler det. Koden til dette interface ses nedenfor.

Det var ikke muligt at præsentere alle artikler på en gang. Der var for mange. Derfor laver vi med koden en filtrering. Vi valgte at vise artikler, der ved hjælp af vores emne-identifikation var identificeret som knyttet til tematikkerne “naturkatastrofer” og “uvejr”. Vi valgte også at filtrere navnestof fra, da dette indeholder en høj grad af fejlidentifikation. Endelig introducerede vi en smule støj til alle koordinater, sådan at artikler med samme koordinater blev spredt ud i en lille sky. Denne offer af visuel præcision blev udført, for at gøre det muligt at klikke på individuelle artikler.

library(tidyverse)
library(shiny)
library(leaflet)

df_map <- read_csv("df_spacy_koordinater.csv") %>% 
  filter(!str_detect(indeks, "(navn)")) %>%
  filter(str_detect(indeks, "(natur|uvejr)")) %>% 
  mutate(tekst = paste0("<b>Dato:</b> ", 
                        dato, "<br><b>Sted:</b> ", 
                        sted, "<br><b>Tekst:</b> ", 
                        tekst)) %>% 
  mutate(længdegrad = jitter(længdegrad, amount = 0.04),
         breddegrad = jitter(breddegrad, amount = 0.02))

ui <- fluidPage(
  leafletOutput("mymap", height = "95vh"),
  p()
)

server <- function(input, output, session) {
  output$mymap <- renderLeaflet({
    leaflet() %>%
      addProviderTiles("Esri.WorldGrayCanvas") %>%
      addCircleMarkers(df_map,
        lng = df_map$længdegrad,
        lat = df_map$breddegrad,
        popup = df_map$tekst,
        radius = 3,
        weight = 0.5,
        opacity = 1,
        color = "red",
        fill = TRUE,
        fillOpacity = 0.05,
        popupOptions = popupOptions(maxHeight = 300))
  })
}

shinyApp(ui, server)

Du kan udforske denne app ved at klikke på dette link: https://hislabaau.shinyapps.io/Kort/

Fodnoter

  1. Devon Mordell, “Critical Questions for Archives as (Big) Data”, Archivaria 87, 2019: s. 140-161.↩︎