Glittr stats
In this report you can find some general statistics about Glittr.org. The plots and statistics created are amongst others used in the manuscript. Since Glittr.org is an ongoing project these statistics are updated weekly.
Set up the environment
This is required if you run this notebook locally. Loading required packages.
To run locally, create a file named .env
and add your GitHub PAT (variable named PAT
) and google api key (named GOOGLE_API_KEY
) in there, e.g.:
# this is an example, store it as .env:
export PAT="ghp_aRSRESCTZII20Lklser3H"
export GOOGLE_API_KEY="AjKSLE5SklxuRsxwPP8s0"
Now use your UNIX terminal to source this file to get the keys as objects:
source .env
In R, get environment variables as objects:
pat <- Sys.getenv("PAT")
google_api_key <- Sys.getenv("GOOGLE_API_KEY")
matomo_api_key <- Sys.getenv("MATOMO_API_KEY")
Setting colors. These correspond to the category colours on glittr.org.
glittr_cols <- c(
"Scripting and languages" = "#3a86ff",
"Computational methods and pipelines" = "#fb5607",
"Omics analysis" = "#ff006e",
"Reproducibility and data management" = "#ffbe0b",
"Statistics and machine learning" = "#8338ec",
"Others" = "#000000")
Parse repository data
Using the glittr.org REST API to get repository metadata, among which the stargazers, recency, category, license and tags.
Code
# while loop to loop over all pages
page_list <- list()
page <- 1
has_more_pages <- TRUE
while (has_more_pages) {
# Create and send the request with pagination
response <- request("https://glittr.org/api/repositories") |>
req_url_query(`page[size]` = 100, `page[number]` = page) |>
req_perform() |>
resp_body_json()
# Append the data to the list
page_list <- append(page_list, response$data)
# Check if there are more pages (this logic depends on the API's response structure)
has_more_pages <- length(response$data) > 0 # Adjust this condition based on your API's pagination logic
page <- page + 1
}
# extract relevant items as dataframe
repo_info_list <- lapply(page_list, function(x) data.frame(
repo = x$name,
author_name = x$author$name,
stargazers = x$stargazers,
recency = x$days_since_last_push,
url = x$url,
license = ifelse(is.null(x$license), "none", x$license),
main_tag = x$tags[[1]]$name,
main_category = x$tags[[1]]$category,
website = x$website,
author_profile = x$author$profile,
author_website = x$author$website
))
repo_info <- do.call(rbind, repo_info_list)
# create a column with provider (either github or gitlab)
repo_info$provider <- ifelse(grepl("github", repo_info$url), "github", "gitlab")
# create a factor for categories for sorting
repo_info$main_category <- factor(repo_info$main_category,
levels = names(glittr_cols))
# category table to keep order the same in the plots
cat_table <- table(category = repo_info$main_category)
cat_table <- sort(cat_table)
Number of repositories: 670
Get contributors info
Using the GitHub REST API to get the number of contributors for each repository on glittr.org. This takes a few minutes, so if the contributors haven’t changed, it will use a cached version.
Code
# take long time to run, so try to use cache results if no repos have been
# added in the meantime
# check if data/n_contributors.rds exists
if(file.exists("data/n_contributors.rds")) {
n_contributors <- readRDS("data/n_contributors.rds")
} else {
n_contributors <- NULL
}
# get contributors info only from github repos
repo_info_gh <- repo_info[repo_info$provider == "github", ]
# get contributor info from github api if update is needed
if(!identical(sort(repo_info_gh$repo), sort(names(n_contributors)))) {
dir.create("data", showWarnings = FALSE)
n_contributors <- sapply(repo_info_gh$repo, function(x) {
# get repo contributors
resp <- request("https://api.github.com/repos/") |>
req_url_path_append(x) |>
req_url_path_append("contributors") |>
req_url_query(per_page = 1) |>
req_headers(
Accept = "application/vnd.github+json",
Authorization = paste("Bearer", pat),
`X-GitHub-Api-Version` = "2022-11-28",
) |>
req_perform()
link_url <- resp_link_url(resp, "last")
if(is.null(link_url)) {
return(1)
} else {
npages <- strsplit(link_url, "&page=")[[1]][2] |> as.numeric()
return(npages)
}
})
# overwrite rds file
saveRDS(n_contributors, "data/n_contributors.rds")
}
repo_info_gh$contributors <- n_contributors[repo_info_gh$repo]
Get country information
Here we get country information for all authors and organizations. It uses the free text specified at ‘location’. Since this can be anything, we use the google REST API to translate that into country.
Code
# check whether author info exists for caching
if(file.exists("data/author_info.rds")) {
author_info <- readRDS("data/author_info.rds")
author_info_authors <- unique(author_info$author) |> sort()
} else {
author_info_authors <- NULL
}
gh_authors <- repo_info$author_name[repo_info$provider == "github"] |>
unique() |>
sort()
# if the author info is out of date, update it
if(!identical(gh_authors, author_info_authors)) {
author_info_list <- list()
for(author in gh_authors) {
parsed <- request("https://api.github.com/users/") |>
req_url_path_append(author) |>
req_headers(
Accept = "application/vnd.github+json",
Authorization = paste("Bearer", pat),
`X-GitHub-Api-Version` = "2022-11-28",
) |>
req_perform() |>
resp_body_json()
author_info_list[[author]] <- data.frame(
author = parsed$login,
type = parsed$type,
name = ifelse(is.null(parsed$name), NA, parsed$name),
location = ifelse(is.null(parsed$location), NA, parsed$location)
)
}
author_info <- do.call(rbind, author_info_list)
author_info_loc <- author_info[!is.na(author_info$location), ]
author_loc <- author_info_loc$location
names(author_loc) <- author_info_loc$author
ggmap::register_google(key = google_api_key)
loc_info <- ggmap::geocode(author_loc,
output = 'all')
get_country <- function(loc_results) {
if("results" %in% names(loc_results)) {
for(results in loc_results$results) {
address_info <- results$address_components |>
lapply(unlist) |>
do.call(rbind, args = _) |>
as.data.frame()
country <- address_info$long_name[address_info$types1 == "country"]
if (length(country) == 0) next
}
if (length(country) == 0) return(NA)
return(country)
} else {
return(NA)
}
}
countries <- sapply(loc_info, get_country)
names(countries) <- names(author_loc)
author_info$country <- countries[author_info$author]
saveRDS(author_info, "data/author_info.rds")
}
repo_info <- merge(repo_info, author_info, by.x = "author_name",
by.y = "author")
repo_info$country[is.na(repo_info$country)] <- "undefined"
- Number of authors: 317
- Number of countries: 26
Parse tag data
Here, we create tag_df
that contains information for each tag by using the glittr.org API.
parsed <- request("https://glittr.org/api/tags") |>
req_perform() |>
resp_body_json()
tag_dfs <- list()
for(i in seq_along(parsed)) {
category <- parsed[[i]]$category
name <- sapply(parsed[[i]]$tags, function(x) x$name)
repositories <- sapply(parsed[[i]]$tags, function(x) x$repositories)
tag_dfs[[category]] <- data.frame(name, category, repositories)
}
tag_df <- do.call(rbind, tag_dfs) |> arrange(repositories)
Number of tags/topics: 59
Parse outlink data
Here, we use the matomo API to retrieve outlinks to repositories. In this way, we can have an idea which repositories are popular on glittr.org and how often people click on a link. It is summarized over a year (from today).
Code
url <- "https://matomo.sib.swiss/?module=API"
# Create and send the request
response <- request(url) |>
req_body_form(
method = "Actions.getOutlinks",
idSite = 217,
format = "json",
date = "today",
period = "year",
expanded = 1,
filter_limit = -1,
token_auth = matomo_api_key
) |>
req_perform() |>
resp_body_json()
## Get outlinks metadata in a dataframe
outlinks_list <- list()
for(domain in response) {
label <- domain$label
url_info <- lapply(domain$subtable, function(x) {
data.frame(
url = ifelse(is.null(x$url),NA , x$url),
nb_visits = ifelse(is.null(x$nb_visits),NA , x$nb_visits),
domain = label
)
})
outlinks_list[[domain$label]] <- do.call(rbind, url_info)
}
outlinks_df <- do.call(rbind, outlinks_list)
row.names(outlinks_df) <- NULL
# function to clean urls for matching
clean_url <- function(url) {
trimws(url) |> gsub("/$", "", x = _) |> tolower()
}
# function to match outlink data with repo and website url per entry
match_url <- function(outlinks_df, repo_info, column = "repo_url") {
url_clean <- clean_url(outlinks_df$url)
column_clean <- clean_url(repo_info[[column]])
outlinks_df[[paste0("is_", column)]] <- url_clean %in% column_clean
outlinks_df[[paste0("ass_repo_", column)]] <- repo_info$repo[match(url_clean,
column_clean)]
return(outlinks_df)
}
# apply the match url function on urls associated with repo entry
for(column in c("url", "website", "author_profile", "author_website")) {
outlinks_df <- match_url(outlinks_df, repo_info, column = column)
}
# filter for only repo url and website (ignore author info)
# check whether associations match
outlinks_df$associated_entry <- outlinks_df |>
select(ass_repo_url, ass_repo_website) |>
apply(1, function(x) {
x <- x[!is.na(x)] |> unique()
if(length(x) == 1) return(x[1])
if(length(x == 0) == 0) return(NA)
if(length(x == 2)) return("do not correspond")
})
# create a list of outlink visits by entry
visits_by_entry <- outlinks_df |>
select(url, nb_visits, associated_entry) |>
filter(!is.na(associated_entry)) |>
group_by(associated_entry) |>
summarise(total_visits = sum(nb_visits))
visits_by_entry <- merge(visits_by_entry, repo_info,
by.x = "associated_entry",
by.y = "repo") |>
arrange(desc(total_visits))
visits_by_namespace <- visits_by_entry |>
select(total_visits, author_name) |>
group_by(author_name) |>
summarise(total_visits = sum(total_visits)) |>
arrange(desc(total_visits))
# visits by tag
visits_by_main_tag <- visits_by_entry |>
select(total_visits, main_tag) |>
group_by(main_tag) |>
summarise(total_visits = sum(total_visits))
# add main category information for plotting
visits_by_main_tag <- merge(visits_by_main_tag, tag_df,
by.x = "main_tag",
by.y = "name") |>
mutate(visits_per_repo = total_visits/repositories) |>
arrange(desc(visits_per_repo))
- The number of outlink visits in the last year: 2444
- Number of repositories found using Glittr.org: 262
- Most popular repo was TheAlgorithms/Python with 206 outlinks.
- Most popular namespace was TheAlgorithms with 206 outlinks.
Number of repositories by category
This is figure 2A in the manuscript.
cat_count_plot <- table(category = repo_info$main_category) |>
as.data.frame() |>
ggplot(aes(x = reorder(category, Freq), y = Freq, fill = category)) +
geom_bar(stat = "identity") +
scale_fill_manual(values = glittr_cols) +
coord_flip() +
theme_classic() +
ggtitle("Categories") +
theme(legend.position = "none",
axis.title.y = element_blank()) +
ylab("Number of repositories")
print(cat_count_plot)
And a table with the actual numbers
category_count <- table(category = repo_info$main_category) |> as.data.frame()
knitr::kable(category_count)
category | Freq |
---|---|
Scripting and languages | 311 |
Computational methods and pipelines | 48 |
Omics analysis | 154 |
Reproducibility and data management | 43 |
Statistics and machine learning | 83 |
Others | 29 |
Number of contributors per repository separated by category
This is figure 2B in the manuscript.
repo_info_gh$main_category <- factor(repo_info_gh$main_category,
levels = names(cat_table))
contributors_plot <- repo_info_gh |>
ggplot(aes(x = main_category, y = contributors, fill = main_category)) +
geom_violin(scale = "width") +
geom_boxplot(width = 0.1, col = "darkgrey") +
coord_flip() +
ggtitle("Contributors") +
ylab("Number of contributors") +
scale_y_sqrt() +
scale_fill_manual(values = glittr_cols) +
theme_bw() +
theme(legend.position = "none",
axis.title.y = element_blank(),
plot.margin = margin(t = 5, r = 10, b = 5, l = 10))
print(contributors_plot)
And some statistics of contributors.
- More than 10 contributors: 25%
- More than 1 contributor: 80.1%
- Between 1 and 5 contributors: 60.2%
Number of repositories per tag
This is figure 2C in the manuscript.
tag_freq_plot <- tag_df |>
filter(repositories > 10) |>
ggplot(aes(x = reorder(name, repositories),
y = repositories, fill = category)) +
geom_bar(stat = "identity") +
coord_flip() +
scale_fill_manual(values = glittr_cols) +
ggtitle("Tags with > 10 repositories") +
ylab("Number of repositories") +
annotate(geom = "text", x = 2, y = 150,
label = paste("Total number of tags: ",
nrow(tag_df)),
color="black") +
theme_classic() +
theme(legend.position = "none",
axis.title.y = element_blank())
print(tag_freq_plot)
And a table with the actual numbers.
Number of repositories per license
This is figure 2E in the manuscript.
lic_freq_data <- table(license = repo_info$license,
main_category = repo_info$main_category) |>
as.data.frame()
lic_freq_data$main_category <- factor(lic_freq_data$main_category,
levels = names(cat_table))
lic_freq_plot <- lic_freq_data |>
ggplot(aes(x = reorder(license, Freq), y = Freq, fill = main_category)) +
geom_bar(stat = "identity") +
coord_flip() +
scale_fill_manual(values = glittr_cols) +
theme_classic() +
ggtitle("License type") +
ylab("Number of repositories") +
theme(legend.position = "none",
axis.title.y = element_blank())
print(lic_freq_plot)
And a table with the actual numbers.
repo_info$license |>
table() |>
as.data.frame() |>
mutate(perc = round(Freq/nrow(repo_info)*100, 1)) |>
arrange(desc(Freq)) |>
knitr::kable()
Var1 | Freq | perc |
---|---|---|
other | 226 | 33.8 |
none | 192 | 28.7 |
mit | 74 | 11.1 |
cc-by-sa-4.0 | 48 | 7.2 |
cc-by-4.0 | 43 | 6.4 |
gpl-3.0 | 30 | 4.5 |
cc0-1.0 | 25 | 3.7 |
apache-2.0 | 12 | 1.8 |
bsd-3-clause | 12 | 1.8 |
agpl-3.0 | 2 | 0.3 |
artistic-2.0 | 2 | 0.3 |
unlicense | 1 | 0.1 |
wtfpl | 1 | 0.1 |
Number of repositories per country
This is figure 2F in the mansucript.
country_freq <- table(country = repo_info$country,
main_category = repo_info$main_category) |>
as.data.frame()
country_freq$main_category <- factor(country_freq$main_category,
levels = names(cat_table))
country_freq_plot <- country_freq |>
filter(country != "undefined") |>
ggplot(aes(x = reorder(country, Freq), y = Freq, fill = main_category)) +
geom_bar(stat = "identity") +
coord_flip() +
ggtitle("Country") +
ylab("Number of repositories") +
scale_fill_manual(values = glittr_cols) +
annotate(geom = "text", x = 2, y = 70,
label = paste("Repos with undefined country: ",
sum(repo_info$country == "undefined")),
color="black") +
theme_classic() +
theme(legend.position = "none",
axis.title.y = element_blank())
print(country_freq_plot)
And a table with the actual numbers.
repo_info$country |>
table() |>
as.data.frame() |>
arrange(desc(Freq)) |>
knitr::kable()
Var1 | Freq |
---|---|
undefined | 268 |
United States | 180 |
Canada | 32 |
Switzerland | 31 |
Sweden | 23 |
United Kingdom | 23 |
Australia | 15 |
France | 14 |
Germany | 14 |
Netherlands | 12 |
Portugal | 11 |
Belgium | 10 |
Spain | 8 |
Denmark | 4 |
India | 4 |
Norway | 4 |
Ireland | 3 |
Italy | 3 |
Bulgaria | 2 |
Argentina | 1 |
China | 1 |
Finland | 1 |
Luxembourg | 1 |
Mexico | 1 |
Poland | 1 |
Ukraine | 1 |
Number of outlinks by repo
Here, we use the site data from matomo in order to investigate what visitors of Glittr.org have clicked on. This gives us an idea about which repositories and topics are popular among users. In Figure 7 we see that TheAlgorithms/Python was the most popular repository with 206 clicks.
visits_by_entry |>
slice(1:30) |>
ggplot(aes(x = reorder(associated_entry, total_visits),
y = total_visits, fill = main_category)) +
geom_bar(stat = "identity") +
coord_flip() +
scale_fill_manual(values = glittr_cols) +
ggtitle("Top 30 of most outlinked repos") +
ylab("Number of outlinks") +
theme_classic() +
theme(legend.position = "none",
axis.title.y = element_blank())
visits_by_entry |>
select(associated_entry, total_visits) |>
rename(repository = associated_entry) |>
slice(1:30) |>
knitr::kable()
repository | total_visits |
---|---|
TheAlgorithms/Python | 206 |
jakevdp/PythonDataScienceHandbook | 89 |
ISUgenomics/bioinformatics-workbook | 80 |
realpython/python-guide | 74 |
cxli233/FriendsDontLetFriends | 70 |
galaxyproject/training-material | 69 |
lexfridman/mit-deep-learning | 64 |
mlabonne/llm-course | 60 |
theislab/single-cell-tutorial | 59 |
zhiwehu/Python-programming-exercises | 52 |
hbctraining/Training-modules | 43 |
hadley/r4ds | 40 |
bobbyiliev/introduction-to-bash-scripting | 39 |
trekhleb/learn-python | 39 |
jeroenjanssens/data-science-at-the-command-line | 38 |
instillai/TensorFlow-Course | 37 |
DataTalksClub/data-engineering-zoomcamp | 36 |
sib-swiss/single-cell-training | 36 |
Yorko/mlcourse.ai | 34 |
google/comprehensive-rust | 31 |
clauswilke/dataviz | 29 |
rmcelreath/stat_rethinking_2022 | 29 |
harvardinformatics/learning-bioinformatics-at-home | 27 |
genome/bfx-workshop | 25 |
hbctraining/scRNA-seq_online | 25 |
Functional-Genomics-Lab/Applied-Genomics | 23 |
rstudio/bookdown | 22 |
rust-lang/rustlings | 21 |
hadley/ggplot2-book | 20 |
AllenDowney/ThinkStats2 | 19 |
It becomes more interesting when we aggregate the data per main tag (Figure 8). This gives us an idea of topic which people are looking for when visiting glittr.org. The most popular topic is Python with 540 outlinks.
visits_by_main_tag |>
slice(1:30) |>
ggplot(aes(x = reorder(main_tag, total_visits),
y = total_visits, fill = category)) +
geom_bar(stat = "identity") +
coord_flip() +
scale_fill_manual(values = glittr_cols) +
ggtitle("Top 30 of most outlinked main tags") +
ylab("Number of outlinks") +
theme_classic() +
theme(legend.position = "none",
axis.title.y = element_blank())
When we normalize the visits by number of repositories per tag (Figure 9), we can get an idea where training materials might be lacking. A high number here show repositories that are quite unique in what they teach, but are still popular. Here, we see that there are 2 repositires with the main tag Rust with an average of 26 outlinks.
visits_by_main_tag |>
arrange(desc(visits_per_repo)) |>
slice(1:30) |>
ggplot(aes(x = reorder(main_tag, visits_per_repo),
y = visits_per_repo, fill = category)) +
geom_bar(stat = "identity") +
coord_flip() +
scale_fill_manual(values = glittr_cols) +
ggtitle("Top 30 of most outlinked main tags normalized by repo number") +
ylab("Number of outlinks per tag/number of repositories having tag") +
theme_classic() +
theme(legend.position = "none",
axis.title.y = element_blank())
main_tag | total_visits | category | repositories | visits_per_repo |
---|---|---|---|---|
Rust | 52 | Scripting and languages | 2 | 26.0000000 |
Galaxy | 74 | Computational methods and pipelines | 3 | 24.6666667 |
Artificial intelligence | 79 | Statistics and machine learning | 6 | 13.1666667 |
General | 223 | Others | 34 | 6.5588235 |
Multiomics | 23 | Omics analysis | 4 | 5.7500000 |
Spatial transcriptomics | 50 | Omics analysis | 9 | 5.5555556 |
Python | 540 | Scripting and languages | 104 | 5.1923077 |
Machine learning | 200 | Statistics and machine learning | 51 | 3.9215686 |
Data visualization | 140 | Scripting and languages | 36 | 3.8888889 |
Workflows | 71 | Computational methods and pipelines | 21 | 3.3809524 |
Single-cell sequencing | 166 | Omics analysis | 50 | 3.3200000 |
Long read sequencing | 24 | Omics analysis | 9 | 2.6666667 |
Unix/Linux | 101 | Scripting and languages | 40 | 2.5250000 |
MATLAB/Octave | 2 | Scripting and languages | 1 | 2.0000000 |
Quarto | 21 | Scripting and languages | 11 | 1.9090909 |
Proteomics | 15 | Omics analysis | 8 | 1.8750000 |
Cloud computing | 17 | Computational methods and pipelines | 10 | 1.7000000 |
Genomics | 88 | Omics analysis | 53 | 1.6603774 |
Pathways and Networks | 8 | Omics analysis | 5 | 1.6000000 |
GNU Make | 3 | Scripting and languages | 2 | 1.5000000 |
Statistics | 83 | Statistics and machine learning | 61 | 1.3606557 |
Containerization | 23 | Computational methods and pipelines | 20 | 1.1500000 |
Epigenetics | 13 | Omics analysis | 12 | 1.0833333 |
C/C++ | 1 | Scripting and languages | 1 | 1.0000000 |
Data science | 50 | Statistics and machine learning | 55 | 0.9090909 |
Metagenomics | 13 | Omics analysis | 18 | 0.7222222 |
Next generation sequencing | 46 | Omics analysis | 65 | 0.7076923 |
R | 184 | Scripting and languages | 268 | 0.6865672 |
High performance computing | 6 | Computational methods and pipelines | 10 | 0.6000000 |
Transcriptomics | 52 | Omics analysis | 87 | 0.5977011 |
We can also check popularity by namespace, i.e. author (Table 8).
author_name | total_visits |
---|---|
TheAlgorithms | 206 |
sib-swiss | 192 |
jakevdp | 89 |
hbctraining | 87 |
hadley | 86 |
NBISweden | 84 |
ISUgenomics | 80 |
realpython | 74 |
theislab | 71 |
cxli233 | 70 |
galaxyproject | 69 |
lexfridman | 64 |
mlabonne | 60 |
carpentries-incubator | 54 |
zhiwehu | 52 |
bobbyiliev | 39 |
trekhleb | 39 |
jeroenjanssens | 38 |
instillai | 37 |
DataTalksClub | 36 |
rmcelreath | 35 |
Yorko | 34 |
31 | |
clauswilke | 29 |
harvardinformatics | 27 |
AllenDowney | 26 |
genome | 25 |
rstudio | 25 |
Functional-Genomics-Lab | 23 |
griffithlab | 23 |
Summary plot
Full figure 2 of the manuscript.