Ikke-kategoriseret

How we observed an unexpected high peak in the amount of hits in 2013 for ‘kierkegaard’

Background

In the initial phase of the DeiC National Pilot Project N.F.S. Grundtvig i danske medier, we have been discussing the design of the project, and thereby which material from The Royal Danish Library to use for the quantitative analyses. One of the cultural heritage collections will be The Danish Netarchive.

The project aims to explore the effect of Grundtvig in the Danish Culture by looking at how his name appears, in terms of but not limited to frequency, semantics, and graph networks.

So, in order to getting to understand the data, we decided to use Kierkegaard for a comparison to Grundtvig.

This text describes the discovery of an anomaly in the frequency of Kierkegaard-hits. I.e. we observed far too many documents containing the term ‘kierkegaard’ compared to what our intuition would expect. We also try to explain this anomaly, how it might affect studies using the Net Archive, and some paths going forward.

Exploring the data

We have counted all text documents in the Net Archive, that contain either ´kierkegaard’ or ‘grundtvig’. To normalise the results, we also counted every text document. All the counts have been grouped by the month the documents were harvested.

In order to remove the fluctuation of the harvest process, we use the relative amount of hits compared to the total number of harvested documents in a given month, i.e. the percentage of documents containing the search term.

If we visualise the relative counts as a function of the month they were harvested, we observe the mentioned anomaly.

Is it possible, that 2% of all documents in august 2013 mentioned Kierkagaard? Well, it was 200 years since his birth, his birthday being May 5th 1813, which is probably part of the explanation, but it would be extreme if that explanation accounted for the complete observation.

Exploring the data by searching and digging down in this anomaly, we discovered that very few domains accounted for most of the hits. One of these domains was a danish newspaper, that in all of 2013 had a topic around Søren Kierkegaard. They implemented this topic by having a drop-down menu on all web pages, containing a link to the topic, visualized by the Kierkegaard name (‘Kierkegaard – 200 år’), therefore every document harvested from that newspaper that year appears to be mentioning Kierkegaard. On top of that, said newspaper was and still is, harvested with a very high frequency, thereby boosting the effect. We confirmed the use of Kierkegaard in the menus by visual inspection, as can be seen in this screenshot. An interactive example can be enjoyed at The Internet Archive: JP 25. august 2013.

Example of Kierkegaard being part of a web page on a unrelated topic. Kilde: Jyllands-Posten.

So, to answer the question from above: yes, 2% of the documents from August 2013 in the archive did indeed contain the word Kierkegaard. Just not in a very semantic valuable form.

At the moment, we have no know methods implemented to discern between ‘kierkegaard’ appearing in menus and as part of the actual content of the webpage.

Going forward

This is an example on how the technical design of a web page can completely overshadow the actual content, that one tries to analyse. We have always had a suspicion on this, but this is to our knowledge one of the first examples of that actually skewing the results. We were lucky, that the skewness was huge and easily observable.

Even though we have no methods implemented to remedy this, we do have a few ideas:

  • In the specific newspaper, we could remove all instances of the text “Kierkegaard – 200 år” from the results, as that specific wording is used in the menu at least one newspaper. Still, that would only handle the skewing for that specific newspaper.
  • We could identify all websites having an unreasonably high counts of ‘kierkegaard, and eliminate those complete websites from the result. This would, of course, introduce other skewness.
  • We could look only at the board harvests, as done by Probing a Nation’s Web Domain project. Like above, this introduces other forms of skewness.
  • We could use an advanced re-rendering of the source HTML, and try to identify how to discern between design and content elements. As there is no standard way of building web pages, this would also have to be implemented per domain/media house/web publisher.
  • Instead of re-rendering the complete page, we could use existing tools for just extracting the visible parts of a webpage from the HTML code.
  • We could come up with a heuristic identifying when Kierkegaard is used in a sentence and not in design elements. This method could be a more general solution, as it is based on linguistics and not web design or technicalities.

It is important to realize that without some sort of processing, this text data cannot be used for topic modelling in any form.

Source code

The code producing the above graph and the analysis is for the time being published as a gist: why-so-many-hits.Rmd

Posted by Per Møldrup-Dalum in Ikke-kategoriseret, 0 comments

Publicering: Big data experiments with the archived Web: Methodological reflections on studying the development of a nation’s Web

Niels Brüggers forskning om udviklingen af det danske internetdomæne (.dk) mellem 2006-2015 er mundet ud i en forskningsartikel publiceret på tidsskriftet First Monday, som dækker forskning om internettet.

Artiklen undersøger bl.a. udviklingen af størrelsen på hjemmesider, links til andre domæner (både nationale og internationale) og hyperlinks til sociale medier. Dette skal alt sammen være med til at give indikationer om, hvordan det danske domæne er karakteriseret og har udviklet sig. Derudover er artiklen et eksempel på, hvordan forskningen kan rykke sig og åbne op for nye forskningsområder om internettet, gennem brug af computationelle metoder med High Performance Computing.

Forskningen bygger på pilotprojekter kørt på Kulturarvsclusteret, og artiklen kommer ind på, hvordan samarbejdet har muliggjort og understøttet forskningen.

Find artiklen skrevet af Niels Brügger, Janne Nielsen og Ditte Laursen her:

https://journals.uic.edu/ojs/index.php/fm/article/view/10384

God læselyst!





Posted by Mads Bossen Hansen in Ikke-kategoriseret, 0 comments

Aarhus Byraads forhandlingsprotokoller 1930-1940 term frequency(tf) – inverse document frequency(idf)

Denne rapport dokumenterer databehandlingen af data fra Aarhus Stadsarkivs Github-repository1. I dette tilfælde er der taget udgangspunkt i den del af datasættet der omhandler årene fra 1930 til 1940.

Datasættet er struktureret som følger:

DESCRIPTION
The datasets consist of the transcribed and proof-read text from the annually printed minutes. Text from one specific agenda item on one specific page produces one row. If the same agenda item runs across several pages, it just produces several rows of text.

Each row has the following columns:

date_of_meeting
The date of the meeting (yyyy-mm-dd)

publication_page
The original pagenumber from the printed minutes

page_url
Link to a scanned copy of the printed page

record_ids
One or more record_ids that the current agenda item references. The ids are assigned by the City Council

text
The transcribed text”[2]

Datasættet er behandlet i statistik-programmet R, der giver mange muligheder for statistisk arbejde og efterfølgende grafisk fremstilling af resultaterne. I R arbejder man med pakker, som tilføjer forskellige funktionaliteter til grundstammen af R-funktioner. I dette tilfælde er de relevante pakker:

library(tidyverse)
library(tidytext)
library(lubridate)
library(ggplot2)

Dokumentation for de enkelte pakker:
*https://www.tidyverse.org/packages/
*https://cran.r-project.org/web/packages/tidytext/vignettes/tidytext.html
*https://lubridate.tidyverse.org/
*https://ggplot2.tidyverse.org/

For mere information om R generelt:
https://www.r-project.org/

Indlæsning af data

Først indlæses datasættet i R. Dette gøre med et link til datasættet på Aarhus Stadsarkivs github:

meetings_1930_1940 <- read_csv("https://raw.githubusercontent.com/aarhusstadsarkiv/datasets/master/minutes/city-council/city-council-minutes-1930-1940.csv")

 

Oprensning af data

Data behandlingen vil tage udgangspunkt i Tidy Data-princippet som den er implementeret i tidytext-pakken. Tankegangen er her at tage en tekst og splitte den op i enkelte ord. På denne måde optræder der kun ét ord per række i datasættet. Dette er dog et problem i forhold til propier på formen “M. O. Pedersen”. Ved brug af tidytext princippet vil dette proprium blive til “M”, “O”, og “Pedersen” på hver sin række. Alt tegnsætning fjernes af tidytext-formattet, hvorfor det kun er “M” og “O”, der fremgår. Herved opstår der altså et meningstab i og med at “M” og “O” for sig selv ikke gør os klogere. Dette meningstab er vi interesseret i at undgå og dette gøres ved hjælp af regulære udtryk som:

“([A-Z]). ([A-Z]). ([A-z-]+)”, “\1\2\3”

Dette dette udtryk får R til at lede efter alle tilfælde hvor et stort bogstav efterfølges af et punktum, et mellemrum, et stort bogstav, et punktum, et mellemrum og et stort bogstav efterfulgt af et eller flere små bogstaver. Herefter erstattes punktummerne og mellemrummene med tegnet “_”, således at:

“M. O. Pedersen” ændres til “M_O_Pedersen”

Ved et kig på mødereferaterne kan man se, at propriet “Christian” forkortes “Chr.” efterfulgt af et efternavn. Det og lignende tilfælde er også søgt løst med regulære udtryk som vist herunder:

meetings_1930_1940 %>% 
  mutate(text = 
           str_replace_all(
                          text, 
                          pattern = 
                            "([A-Z])\\. ([A-Z])\\. ([A-z-]+)", "\\1_\\2_\\3")) %>%
  mutate(text = 
           str_replace_all(
                          text, 
                          pattern = 
                            "([A-Z])\\. ([A-Z])\\. ([A-Z])\\. ([A-z-]+)", 
                            "\\1_\\2_\\3_\\4")) %>% 
  mutate(text = 
           str_replace_all(
                          text, 
                          pattern = 
                            "([A-Z])\\. ([A-Z][a-z]+)", "\\1_\\2")) %>% 
  mutate(text = 
           str_replace_all(
                          text, 
                          pattern = 
                            "Chr\\. ([A-z-]+)", "Chr_\\1" )) %>% 
  mutate(text = 
           str_replace_all(
                          text, 
                          pattern = 
                            "Vald\\. ([A-z]+)", "Vald_\\1")) -> meetings_1930_1940

Dette kan muligvis vise sig at være utilstrækkeligt, da andre navne kan forkortes på lignende måder lige så vel som der kan være flere mellemnavne end de 3, der bliver kodet efter her.

I denne undersøgelse er ønsket at finde de vigtigste ord pr. år i Aarhus Byråds forhandlingsprotokoller. Problemet er imidlertidig, at tidsformatet i Stadsarkivets data er en dato på formen ÅÅÅÅ-MM-DD. Da vi her kun er interesseret i året, kan vi takket være pakken lubridate med funktionen ‘year’ udtrække året og sætte den over i sin egen kolonne:

meetings_1930_1940 %>% 
  mutate(aar = year(date_of_meeting)) %>% 
  select(aar, text, record_ids)
## # A tibble: 14,508 x 3
##      aar text                                                   record_ids
##    <dbl> <chr>                                                  <chr>     
##  1  1930 Mødet den 3. April 1930. (For lukkede Døre). Fraværen… <NA>      
##  2  1930 Indstilling fra Skolekommission og Skoleudvalg angaae… 54-1930   
##  3  1930 IV. 1. J_Jensen, do. 2. E_Nordvig-Petersen, do. 3. K_… 54-1930   
##  4  1930 Ting talte for, at man ikke skulde forbigaa de gifte … 54-1930   
##  5  1930 "Andragende fra Restauratør Carl Hansen, \"Pavillonen… 47-1930   
##  6  1930 Andragende fra Drejer H_C_Salbro om Eftergivelse af F… 17_31-1929
##  7  1930 Andragende fra Arbejdsmand Aage Nielsen om Eftergivel… 17_39-1929
##  8  1930 Andragende fra Enke Amalie Rodenberg om Eftergivelse … 17_38-1929
##  9  1930 Mødet den 3. April 1930. Fraværende: Chr_Nielsen og V… <NA>      
## 10  1930 Indenrigsministeriets Samtykke til Køb af Ejendommen … 716-1929  
## # ... with 14,498 more rows

Det næste der sker er, at vi omdanner data til det førnævnte tidytextformat, hvor hvert ord kommer til at stå på en række for sig selv:, hvilket gøres med unnest_tokens-funktionen:

meetings_1930_1940 %>% 
  mutate(aar = year(date_of_meeting)) %>% 
  select(aar, text, record_ids) %>% 
  unnest_tokens(word, text)
## # A tibble: 1,157,594 x 3
##      aar record_ids word       
##    <dbl> <chr>      <chr>      
##  1  1930 <NA>       mødet      
##  2  1930 <NA>       den        
##  3  1930 <NA>       3          
##  4  1930 <NA>       april      
##  5  1930 <NA>       1930       
##  6  1930 <NA>       for        
##  7  1930 <NA>       lukkede    
##  8  1930 <NA>       døre       
##  9  1930 <NA>       fraværende 
## 10  1930 <NA>       chr_nielsen
## # ... with 1,157,584 more rows

Analyse

Vi er nu interesserede i at finde de ord, der hyppigst forekommer pr. år i årene 1930-1940, som vores datasæt spænder over.

meetings_1930_1940 %>% 
  mutate(aar = year(date_of_meeting)) %>% 
  select(aar, text, record_ids) %>% 
  unnest_tokens(word, text) %>% 
  count(aar, word, sort = TRUE)
## # A tibble: 125,715 x 3
##      aar word      n
##    <dbl> <chr> <int>
##  1  1935 at     4979
##  2  1938 at     4010
##  3  1936 at     3949
##  4  1931 at     3922
##  5  1934 at     3863
##  6  1939 at     3818
##  7  1937 at     3732
##  8  1935 til    3499
##  9  1932 at     3374
## 10  1933 at     3238
## # ... with 125,705 more rows

Ikke overraskende er det småord, som optræder flest gange pr. år. Dette er ikke videre interessant i denne undersøgelse, så vi er nu interesseret i at finde et mål, der gør at vi kan sammenligne ords hyppighed på tværs af årene. Dette kan vi gøre ved at udregne ordets, termets, frekvens:

\[f = \frac{n_{term}}{N_{aar}}\]

Før vi kan tage dette skridt skal vi dog have R til at tælle, hvor mange ord, der er i de enkelte år. Dette gøres med funktionen group_by efterfulgt af summarise:

meetings_1930_1940 %>% 
  mutate(aar = year(date_of_meeting)) %>% 
  select(aar, text, record_ids) %>% 
  unnest_tokens(word, text) %>% 
  count(aar, word, sort = TRUE) %>% 

  group_by(aar) %>% 
  summarise(total = sum(n)) -> total_words


total_words
## # A tibble: 11 x 2
##      aar  total
##    <dbl>  <int>
##  1  1930  71014
##  2  1931 115062
##  3  1932 102449
##  4  1933 102346
##  5  1934 117568
##  6  1935 140307
##  7  1936 117619
##  8  1937 115134
##  9  1938 120169
## 10  1939 120286
## 11  1940  35640

Herefter skal vi have tilføjet det totale antal ord til vores dataframe, hvilket gøres med left_join:

meetings_1930_1940 %>% 
  mutate(aar = year(date_of_meeting)) %>% 
  select(aar, text, record_ids) %>% 
  unnest_tokens(word, text) %>% 
  count(aar, word, sort = TRUE) %>% 

  left_join(total_words, by = "aar") -> meetings_1930_1940
meetings_1930_1940
## # A tibble: 125,715 x 4
##      aar word      n  total
##    <dbl> <chr> <int>  <int>
##  1  1935 at     4979 140307
##  2  1938 at     4010 120169
##  3  1936 at     3949 117619
##  4  1931 at     3922 115062
##  5  1934 at     3863 117568
##  6  1939 at     3818 120286
##  7  1937 at     3732 115134
##  8  1935 til    3499 140307
##  9  1932 at     3374 102449
## 10  1933 at     3238 102346
## # ... with 125,705 more rows

Nu har vi de tal vi skal bruge for at udregne ordenes frekvenser. Her udregner vi for “at” i 1935.

\[f(\textrm{at})=\frac{4979}{140307}=0,0354864690\]

Ved at udregne frekvensen for termer kan vi sammenligne dem på tværs af år. Det er dog ikke videre interessant at sammenligne brugen af ordet “at” årene i mellem. Vi mangler derfor en måde at “straffe” ord som optræder hyppigt i alle årene. Til dette kan vi bruge inversed document frequency(idf):
\[\textrm{idf}(term)=\ln(\frac{n}{N})\]
Hvor n er det totale antal dokumenter(i vores tilfælde år) og N er antallet af år, hvor ordet fremgår.

\[\textrm{idf}(at)=\ln(\frac{10}{10})=0\]
Herved får vi altså straffet ord som optræder med stor hyppighed i alle årene eller mange af årene. Ord der forekommer i alle årene kan altså altså ikke fortælle os noget særlig om et givent år. Disse ord vil have en idf på 0 hvorfor deres tf_idf også bliver 0, da denne er defineret ved tf gange med idf.

Heldigvis kan R udregne tf og tf_idf for alle ordene for os i et snuptag med bind_tf_idf-funktionen:

meetings_1930_1940 <- meetings_1930_1940 %>% 
  bind_tf_idf(word, aar, n)
meetings_1930_1940
## # A tibble: 125,715 x 7
##      aar word      n  total     tf   idf tf_idf
##    <dbl> <chr> <int>  <int>  <dbl> <dbl>  <dbl>
##  1  1935 at     4979 140307 0.0355     0      0
##  2  1938 at     4010 120169 0.0334     0      0
##  3  1936 at     3949 117619 0.0336     0      0
##  4  1931 at     3922 115062 0.0341     0      0
##  5  1934 at     3863 117568 0.0329     0      0
##  6  1939 at     3818 120286 0.0317     0      0
##  7  1937 at     3732 115134 0.0324     0      0
##  8  1935 til    3499 140307 0.0249     0      0
##  9  1932 at     3374 102449 0.0329     0      0
## 10  1933 at     3238 102346 0.0316     0      0
## # ... with 125,705 more rows

Ikke desto mindre ser vi ikke nogen interessant ord. Dette skyldes at R lister ordene op i et stigende hierarki – altså lavest til højst.
Vi beder det om at gøre det faldende i stedet – højest tf_idf

meetings_1930_1940 %>% 
  select(-total) %>% 
  arrange(desc(tf_idf))
## # A tibble: 125,715 x 6
##      aar word              n       tf   idf   tf_idf
##    <dbl> <chr>         <int>    <dbl> <dbl>    <dbl>
##  1  1938 1938            452 0.00376  0.452 0.00170 
##  2  1940 flyvepladsen     18 0.000505 1.70  0.000861
##  3  1933 j_chr_møller    103 0.00101  0.788 0.000793
##  4  1940 eksercerplads    16 0.000449 1.70  0.000765
##  5  1932 j_chr_møller     92 0.000898 0.788 0.000708
##  6  1937 raadhus          99 0.000860 0.788 0.000678
##  7  1940 stadsdyrlægen    10 0.000281 2.40  0.000673
##  8  1936 1936            245 0.00208  0.318 0.000663
##  9  1939 1939            392 0.00326  0.201 0.000654
## 10  1930 j_chr_møller     56 0.000789 0.788 0.000622
## # ... with 125,705 more rows

Vi ser her at 1938, 1936 og 1939 kommer ret højt på listen. Dette skyldes formentlig at journalnumrene dannes udfra det pågældende år. Inden vi laver en grafisk visualisering, fjerner vi derfor alle årstal i teksten.

stopord <- data_frame(word = c("1930", "1931", "1932", "1933", "1934", "1935", 
                                   "1936", "1937", "1938", "1939", "1940"))
meetings_1930_1940 <- anti_join(meetings_1930_1940, stopord, by = "word")

Visualisering

Herefter kan vi gå over til en grafisk visualisering.

meetings_1930_1940 %>%
  arrange(desc(tf_idf)) %>%
  mutate(word = factor(word, levels = rev(unique(word)))) %>% 
  group_by(aar) %>% 
  top_n(15) %>% 
  ungroup %>%
  ggplot(aes(word, tf_idf)) +
  geom_col(show.legend = FALSE, fill = "skyblue2") +
  labs(x = NULL, y = "tf-idf") +
  facet_wrap(~aar, ncol = 3, scales = "free") +
  scale_y_continuous(labels = scales::comma_format(accuracy = 0.0001)) +
  coord_flip()
## Selecting by tf_idf

Klik for større billede

[1]: https://github.com/aarhusstadsarkiv/datasets/tree/master/minutes/city-council
[2]: Indsat fra https://github.com/aarhusstadsarkiv/datasets/tree/master/minutes/city-council

Posted by Max Odsbjerg Pedersen in Ikke-kategoriseret, 0 comments