| title: “An analysis of the Danish broard harvests of 2013” |
| author: “Per Møldrup-Dalum” |
| output: rmarkdown::tufte_handout |
| date: “17. dec. 2014” |
We were given the task of extracting information from the Danish net archive to be used as grounds for a scientifically based decision on the number of yearly broard havests of the Danish Internet. The work and results from that task is presented in these notes.
TODO: a lot of preamble and introduction
Every broard harvest performed in Netarkivet is described on the Tværsnithøstningsstatistik page. On that page the performed harvests are defined with an unique identification string, its name, and some metadata for each harvest regarding size etc.
In this analysis we are interested in the year 2013, more specifically the harvests catagorized as /step-2/ harvests in that year. A /step-2/ harvest is defined as harvests that use a global upper limit of 10GB. During 2013, four such harvest were completed with the unique names: 2013-1-10GB, 2013-2-10GB, 2013-3-10GB, and 2013-4-10GB.
Data about these harvest is persisted in the production job database, a database which we do not have access to. Therefore we rolled a dump of the production database into our development enviromnent.
In the database thereby created we can get the ids of all the jobs performed in those four harvests. This is most easily done using a few simple SQL expressions.
select harvest_id,name
from harvestdefinitions
where name like '%2013-%-10GB%'
order by harvest_id;"
To execute a SQL expression on the job database, we can use the following expression
read.csv(text=system(paste("echo \"",sqlexp,"\" | ssh test@kb-test-adm-001.kb.dk psql -F , --no-align -U test -d prod_dump_20141111_harvestdb | sed '$d'"), intern=T),header = T)
## harvest_id name
## 1 181 2013-1-10GB
## 2 186 2013-2-10GB
## 3 191 2013-3-10GB
## 4 199 2013-4-10GB
The sed '$d' hack is just to remove the last line of the psql output containing the " (4 rows) " message.
As we’re going to use the above comman a few times a small function can be written
execSQL <- function(sqlString) {
return(
read.csv(
text=system(
paste("echo \"",
sqlString,
"\"|ssh test@kb-test-adm-001.kb.dk psql -F , --no-align -U test -d prod_dump_20141111_harvestdb | sed '$d'"),
intern=T),
header=T))
}
All this function requires is key based ssh access to the server running the job database. A R data frame can be pretty printed using knitr::kable() and we get commands like
h_df <- execSQL("select harvest_id,name from harvestdefinitions where name like '2013-%-10GB' order by harvest_id;")
knitr::kable(h_df)
| harvest_id | name |
|---|---|
| 181 | 2013-1-10GB |
| 186 | 2013-2-10GB |
| 191 | 2013-3-10GB |
| 199 | 2013-4-10GB |
To get the complete list of job ids from these four harvests we need this SQL expression (that really limits the output to 10 rows)
execSQL("select job_id,harvest_id,status from jobs where harvest_id = 181 or harvest_id = 186 or harvest_id = 191 or harvest_id = 199 limit 10;")
## job_id harvest_id status
## 1 174837 181 3
## 2 174891 181 3
## 3 174842 181 3
## 4 174829 181 3
## 5 174843 181 3
## 6 175180 181 3
## 7 174852 181 3
## 8 174851 181 3
## 9 174849 181 3
## 10 174848 181 3
This looks fine so we can modify it to get the full set of rows
jobIds <- execSQL("select job_id,harvest_id,status
from jobs
where harvest_id = 181
or harvest_id = 186
or harvest_id = 191
or harvest_id = 199;")
knitr::kable(head(jobIds))
| job_id | harvest_id | status |
|---|---|---|
| 174959 | 181 | 3 |
| 174958 | 181 | 3 |
| 175246 | 181 | 3 |
| 189597 | 191 | 3 |
| 189598 | 191 | 3 |
| 181414 | 186 | 3 |
nrow(jobIds)
## [1] 2723
The four broard harvests in 2013 consisted of 2723 jobs. The above SQL expression also returns the status of each job. This status enumeration has the following meaning
| number | meaning |
|---|---|
| 0 | NEW |
| 1 | SUBMITTED |
| 2 | STARTED |
| 3 | DONE |
| 4 | FAILED |
| 5 | RESUBMITTED |
How are these job status values distributed for the jobs in 2013? This can be explored using the following SQL
dist_status <- execSQL("select harvest_id,status,count(job_id)
from jobs
where harvest_id = 181
or harvest_id = 186
or harvest_id = 191
or harvest_id = 199
group by harvest_id, status
order by harvest_id,status;")
knitr::kable(dist_status)
| harvest_id | status | count |
|---|---|---|
| 181 | 3 | 596 |
| 181 | 4 | 21 |
| 186 | 3 | 473 |
| 186 | 4 | 13 |
| 191 | 3 | 775 |
| 191 | 4 | 5 |
| 191 | 5 | 101 |
| 199 | 3 | 692 |
| 199 | 4 | 16 |
| 199 | 5 | 31 |
To plot this table we need to cast the harvest_id and status variables as strings. We also add a new column that contains string representation of the status values for better visuals
status.names <- c(
"new",
"submitted",
"started",
"done",
"failed",
"resubmitted")
dist_status$named.status <- status.names[dist_status$status+1]
dist_status$harvest_id <- as.character(dist_status$harvest_id)
dist_status$status <- as.character(dist_status$status)
knitr::kable(dist_status)
| harvest_id | status | count | named.status |
|---|---|---|---|
| 181 | 3 | 596 | done |
| 181 | 4 | 21 | failed |
| 186 | 3 | 473 | done |
| 186 | 4 | 13 | failed |
| 191 | 3 | 775 | done |
| 191 | 4 | 5 | failed |
| 191 | 5 | 101 | resubmitted |
| 199 | 3 | 692 | done |
| 199 | 4 | 16 | failed |
| 199 | 5 | 31 | resubmitted |
The dist_status data can now be plottet as a stacked bar chart
ggplot(dist_status, aes(harvest_id, count, fill=named.status)) + geom_bar(stat="identity") + xlab("Harvest") + ylab("Number of jobs")
TODO: Need lots of explanations or conjectures on the data
We could now filter the jobs and look deeper into the DONE jobs.
done.jobs <- execSQL("
select job_id,harvest_id
from jobs
where (
harvest_id = 181
or harvest_id = 186
or harvest_id = 191
or harvest_id = 199)
and status = 3;")
nrow(done.jobs)
## [1] 2536
but instead we’ll look at the 2013 broard harvest from another perspective.
First we’ll create a list of all domains that are recorded from succesfull jobs. This can be done by this SQL expression
select domains.name,
historyinfo.harvest_id,
historyinfo.OBJECTCOUNT,
historyinfo.BYTECOUNT,
historyinfo.stopreason
from historyinfo, configurations, domains
where
job_id IN (
select job_id
from jobs
where (harvest_id=181
or harvest_id=186
or harvest_id=191
or harvest_id=199)
and status=3
)
and historyinfo.config_id=configurations.config_id
and configurations.domain_id = domains.domain_id;
As we know this data set will contain more than 500,000 entries, we’ll dump the data into a local file for easy access later. If the above SQL expression is stored in file domains.sql then the following Bash script will dump a file with the desired data
cat domains.sql | ssh test@kb-test-adm-001.kb.dk psql -F , --no-align -U test -d prod_dump_20141111_harvestdb | sed '$d'>domains.csv
#wc -l domains.csv
Let’s read this data into R
domains <- tbl_df(read.csv("domains.csv", header = T))
What did we just read?
nrow(domains)
## [1] 623568
head(domains)
## Source: local data frame [6 x 5]
##
## name harvest_id objectcount bytecount stopreason
## 1 kreativbyg.dk 181 840 240932590 0
## 2 lindegaardsskolen.dk 181 2775 111238617 0
## 3 kirkeskibet.dk 181 678 14242570 0
## 4 komarna.com 181 958 17661187 0
## 5 learnchinese.dk 181 47043 944998860 0
## 6 kyllings.dk 181 771 15614013 0
What can now be inferred from this data? What values can be extracted and conjectures created? Let’s just say that that question in it self can occupy a fair amount of time.
First we’ll look at stopreason. stopreason is an enumeration with the following string representations
| number | meaning |
|---|---|
| 0 | DOWNLOAD_COMPLETE |
| 1 | OBJECT_LIMIT |
| 2 | SIZE_LIMIT |
| 3 | CONFIG_SIZE_LIMIT |
| 4 | DOWNLOAD_UNFINISHED |
| 5 | CONFIG_OBJECT_LIMIT |
| 6 | TIME_LIMIT |
What values of stopreason do we observe in our data set?
First create a nice string representation of the stopreason based on the above table
stopreason.names <- c("DONWLOAD_COMPLETE",
"OBJECT_LIMIT",
"SIZE_LIMIT",
"CONFIG_SIZE_LIMIT",
"DONWLOAD_UNFINISHED",
"CONFIG_OBJECT_LIMIT",
"TIME_LIMIT")
domains$named.stopreason <- stopreason.names[domains$stopreason+1]
and then knead the data
domains.stopreasons <- domains %>%
group_by(named.stopreason) %>%
summarise(counts=n()) %>%
arrange(desc(counts))
knitr::kable(domains.stopreasons)
| named.stopreason | counts |
|---|---|
| DONWLOAD_COMPLETE | 559481 |
| CONFIG_SIZE_LIMIT | 35026 |
| DONWLOAD_UNFINISHED | 29048 |
| SIZE_LIMIT | 13 |
How does the data look if we spread the numbers out over the four broard harvests?
domains.stopreasons.harvest <- domains %>%
group_by(harvest_id,named.stopreason) %>%
summarise(counts=n()) %>%
arrange(desc(counts))
knitr::kable(
dcast(domains.stopreasons.harvest, harvest_id ~ named.stopreason))
## Using counts as value column: use value.var to override.
| harvest_id | CONFIG_SIZE_LIMIT | DONWLOAD_COMPLETE | DONWLOAD_UNFINISHED | SIZE_LIMIT |
|---|---|---|---|---|
| 181 | 8480 | 143591 | 420 | 1 |
| 186 | 8556 | 142841 | 87 | 4 |
| 191 | 9956 | 142747 | 10721 | 3 |
| 199 | 8034 | 130302 | 17820 | 5 |
or in a more visual way
Next let’s look at how the individual domains survives over the year. In other words how many domains apprears in the second broard harvest and has disappeared before the last borard harvest? Unfortunately this question is not easily answered as there are many reasons as to why a domain might not exist in a subsequent harvest. Especially if we only look at DOWNLOAD_COMPLETED domains. Then it could have disappeared from the internet, but it could just as easily have grown to a size beyond the CONFIG_SIZE_LIMIT or SIZE_LIMIT ending with a stopreason other than DONWLOAD_COMPLETE.
First let’s look at the survival rate of domains as we move through the year.
h1 <- domains %>%
filter(stopreason == 0, harvest_id == 181) %>%
select(name)
h2 <- domains %>%
filter(stopreason == 0, harvest_id == 186) %>%
select(name)
h3 <- domains %>%
filter(stopreason == 0, harvest_id == 191) %>%
select(name)
h4 <- domains %>%
filter(stopreason == 0, harvest_id == 199) %>%
select(name)
names <- c("1-2", "1-2", "2-3", "2-3", "3-4", "3-4")
growth <- c("increase", "decrease","increase", "decrease","increase", "decrease")
diffs <- c(nrow(setdiff(h2,h1)),-nrow(setdiff(h1,h2)), nrow(setdiff(h3,h2)),-nrow(setdiff(h2,h3)), nrow(setdiff(h4,h3)), -nrow(setdiff(h3,h4)) )
survival.rate <- data.frame(names,growth,diffs)
ggplot(data=survival.rate, aes(x=names, y=diffs, fill=growth)) + geom_bar(stat="identity", position=position_dodge()) + xlab("Intervals") + ylab("Gained or lost domains")
Next question could be to examine where the lost domains went. So, to do that, we check whether the lost but completed domains in interval 1-2 still exists in harvest 2 albeit with a different stopreason.
lost <- setdiff(h2,h1)
h2.notcompleted <- domains %>%
filter(stopreason != 0, harvest_id == 186) %>%
select(name)
nrow(setdiff(lost,h2.notcompleted)) - nrow(lost)
## [1] 0
If we take all domains completed in the first harvest but not completed in the second (that’s 20437 domains) and compares that set of domains with non-completed domains in the second harvest, we get a set of 0 domains. I.e. every domain missing from the second harvest is in reality gone. Or?
If we compare that to the distribution of stopreasons in the second harvest
| named.stopreason | counts |
|---|---|
| DONWLOAD_COMPLETE | 142841 |
| CONFIG_SIZE_LIMIT | 8556 |
| DONWLOAD_UNFINISHED | 87 |
| SIZE_LIMIT | 4 |
less than 9,000 domains reached a limit while more than 20,000 is missing.
Another way to look at this would be the following: How many domains are in number 2 but not in 1, 3, nor 4? This set of domains could be appearing and disapperaing between two yearly harvests and thereby being under our radar if we only harvested twice a year.
To make an estimate on that number we could do
only.two <- setdiff(setdiff(setdiff(h2,h1), h3), h4)
So why do 3313 domains only appear in the second harvest of 2013? If we take number 10 in that list of missing domains and look for it in the complete list of harvested domains, we see this:
only.two[10,]
## Source: local data frame [1 x 1]
##
## name
## 1 beerguide.dk
filter(domains,name=="beerguide.dk")
## Source: local data frame [1 x 6]
##
## name harvest_id objectcount bytecount stopreason
## 1 beerguide.dk 186 1245 39228627 0
## Variables not shown: named.stopreason (chr)
Today, 19 December, this site still exists. So, again, why did we only see it once in the middle of 2013?
If we want to shift our focus from the domains to the URLs, we need a well defined set of domains. We’ll define this URL corpus as the URLs from every completely domain harvested in 2013.
every.harvest <- intersect(intersect(intersect(h1,h2),h3),h4)
every.harvest
## Source: local data frame [79,072 x 1]
##
## name
## 1 fiskehuset.com
## 2 foleboks.dk
## 3 flsk.dk
## 4 gedved-if.dk
## 5 gojf.dk
## 6 fmauto.dk
## 7 forbina.dk
## 8 friis-gaardbutik.dk
## 9 galleri-nord.dk
## 10 fritzes-fotoalbum.dk
## .. ...
Do we have duplicated domains in the 4 harvests?
uniq.p <- c(
nrow(unique(h1)) == nrow(h1),
nrow(unique(h2)) == nrow(h2),
nrow(unique(h3)) == nrow(h3),
nrow(unique(h4)) == nrow(h4))
uniq.p
## [1] TRUE TRUE TRUE TRUE
Every domain only appears once in each broard harvest. Therefore another view would be to count the number of appearences of each domain in the large data set.
domains %>%
filter(stopreason == 0) %>%
group_by(name) %>%
summarise(objectcountSum = sum(objectcount),bytecountSum = sum(bytecount),cnt = n()) %>%
filter(cnt==4) %>%
arrange(desc(bytecountSum))
## Source: local data frame [79,072 x 4]
##
## name objectcountSum bytecountSum cnt
## 1 gentofte.dk 56995 24274844270 4
## 2 baptistkirke.dk 5121 22237396816 4
## 3 tekno.dk 52118 21066645516 4
## 4 morsoe.dk 54919 19180330721 4
## 5 safts.dk 260 18674915984 4
## 6 koncern.dk 9760 17573199044 4
## 7 adl.dk 1513840 15359441176 4
## 8 renaessancesprog.dk 76657 14127425644 4
## 9 rapspot.dk 212190 13858152580 4
## 10 ddfr.dk 347268 13025693228 4
## .. ... ... ... ...
How is the distribution of bytecount
domains.completed <- domains %>%
filter(stopreason == 0)
hist(domains.completed$bytecount, breaks=100)
If we focus on domains from which less than 1kB were havested
domains.completed <- domains %>%
filter(stopreason == 0, bytecount<1000)
hist(domains.completed$bytecount, breaks=100)
domains.completed <- domains %>%
filter(stopreason == 0, bytecount > 40 , bytecount<80)
hist(domains.completed$bytecount, breaks=100)
What if we just look at the tail?
domains.completed <- domains %>%
filter(stopreason == 0, bytecount > 200 , bytecount<10000)
hist(domains.completed$bytecount, breaks=100)
What if we split that data up into the 4 individual harvests?
## Loading required package: grid
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$inv: partial match of 'inv' to 'inverse'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$inv: partial match of 'inv' to 'inverse'
## Warning in scale$trans$inv: partial match of 'inv' to 'inverse'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$inv: partial match of 'inv' to 'inverse'
## Warning in plot$layer: partial match of 'layer' to 'layers'
## Warning in plot$coord: partial match of 'coord' to 'coordinates'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$inv: partial match of 'inv' to 'inverse'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$inv: partial match of 'inv' to 'inverse'
## Warning in scale$trans$inv: partial match of 'inv' to 'inverse'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$inv: partial match of 'inv' to 'inverse'
## Warning in plot$layer: partial match of 'layer' to 'layers'
## Warning in plot$coord: partial match of 'coord' to 'coordinates'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$inv: partial match of 'inv' to 'inverse'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$inv: partial match of 'inv' to 'inverse'
## Warning in scale$trans$inv: partial match of 'inv' to 'inverse'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$inv: partial match of 'inv' to 'inverse'
## Warning in plot$layer: partial match of 'layer' to 'layers'
## Warning in plot$coord: partial match of 'coord' to 'coordinates'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$inv: partial match of 'inv' to 'inverse'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$inv: partial match of 'inv' to 'inverse'
## Warning in scale$trans$inv: partial match of 'inv' to 'inverse'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$trans: partial match of 'trans' to 'transform'
## Warning in scale$trans$inv: partial match of 'inv' to 'inverse'
## Warning in plot$layer: partial match of 'layer' to 'layers'
## Warning in plot$coord: partial match of 'coord' to 'coordinates'
Again we see some interesting artefacts that could be investigated further.
One of the next steps would be to look at the individul URLs comprising the 4 harvests but that will have to waint for another time.