I was curious if there’s a positive correlation between the total number of users on a server and how many followers I have from that server.

Include some packages…

library(rtoot)
library(stringr)
library(tidyverse)

Get {rtoot} authorised to talk to my server:

auth_setup(instance = "tech.lgbt", type ="user")

I’ll need my ID: who am I?

acc <- search_accounts("@Andi@tech.lgbt")
acc |>
  select(id, acct, display_name) |>
  head(1)

It me!

whoami <- "109273348690338129"

There’s a handy function in {rtoot} for getting all followers; however, it doesn’t currently support auto-pagination. After reading the friendly manual, here’s a workaround:

really_get_all_followers <- function(id, sure = "No!") {
  stopifnot(sure == "Yes, I know what I am doing")
  
  followers <- c()
  still_working <- TRUE
  max_id <- NULL
  
  while (still_working) {
    next_lot <- get_account_followers(id,
                                      max_id = max_id)
    followers <- bind_rows(followers, next_lot)
  
    attrs <- attr(next_lot, "headers")
    if ("max_id" %in% names(attrs))
      max_id <- attrs$max_id
    else
      still_working <- FALSE
  }
  
  followers
}

Get my followers:

my_followers <- really_get_all_followers(
  whoami,
  sure = "Yes, I know what I am doing"
)

This number is correct: it worked!

nrow(my_followers)
[1] 312

What servers are they from?

get_servers <- function(followers) {
  servers <- followers$acct |> str_split_fixed("@", 2)
  servers[,2]
}
followers_servers <- my_followers |>
  mutate(server = get_servers(my_followers)) |>
  mutate(server = ifelse(server == "", "tech.lgbt", server))

Here are the counts:

server_count <- followers_servers |>
  group_by(server) |>
  summarise(n = n()) |>
  arrange(desc(n))

server_count

Check everything adds up:

server_count$n |> sum()
[1] 312

So far so good.

Next up, how many users are there on each of those servers? Note the exception handling…

get_user_count <- Vectorize(function(server) {
  res <- NA
  
  # This will catch problems like missing servers
  tryCatch(
    res <- get_instance_general(server)$stats$user_count,
    error = function(e) {
        cat("***")
        cat(server)
        cat("***")
        cat("\n")
        print(e)
        cat("\n")
      }
  )
  res
})
server_count$server_user_n <- get_user_count(server_count$server)
***fediphilosophy.org***
<simpleError in make_get_request(token = token, path = "/api/v1/instance", instance = instance,     params = list(), anonymous = anonymous): something went wrong. Status code: 500>

***toot.theresnotime.io***
<simpleError in curl::curl_fetch_memory(url, handle = handle): schannel: SNI or certificate check failed: SEC_E_WRONG_PRINCIPAL (0x80090322) - The target principal name is incorrect.>
server_count

A couple of histograms:

server_count |>
  select(n, server_user_n) |>
  na.omit()  |>
  pivot_longer(cols = everything(),
               names_to = "key",
               values_to = "value") |>
  mutate(nice_name = case_when(key == "n" ~ "Followers",
                               key == "server_user_n" ~ "Users on server")) |>
  ggplot(aes(value)) +
    facet_wrap(~ nice_name, scales = "free") +
    geom_histogram(bins = 40) +
    labs(x = "Users", y = "Freq")

A scatterplot:

server_count |>
  na.omit() |>
  mutate(home = ifelse(server == "tech.lgbt",
                                 "My home server",
                                 "Elsewhere"),
         home = factor(home,
                       c("My home server", "Elsewhere"))) |>
  ggplot(aes(y = log(n, 10),
             x = log(server_user_n, 10),
             colour = home)) +
  geom_point() +
  scale_colour_manual(values = c("magenta", "black")) +
  #theme_bw() +
  labs(y = expression(log[10]~(followers)),
       x = expression(log[10]~(total~server~users)),
       title = "Follower count by server",
       colour = "")

There is indeed a correlation:

cor.test(~ n + server_user_n, data = server_count, method = "kendall")

    Kendall's rank correlation tau

data:  n and server_user_n
z = 5.984, p-value = 2.177e-09
alternative hypothesis: true tau is not equal to 0
sample estimates:
      tau 
0.4802056 

Last run (or at least knitted) Tue Nov 15 21:25:31 2022.

LS0tDQp0aXRsZTogIlBsYXlpbmcgd2l0aCB7cnRvb3R9Ig0KYXV0aG9yOiAiQGFuZGlAdGVjaC5sZ2J0Ig0KZGF0ZTogMTMgTm92IDIwMjINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6IA0KICAgIGNvZGVfZm9sZGluZzogbm9uZQ0KLS0tDQoNCkkgd2FzIGN1cmlvdXMgaWYgdGhlcmUncyBhIHBvc2l0aXZlIGNvcnJlbGF0aW9uIGJldHdlZW4gdGhlIHRvdGFsIG51bWJlciBvZiB1c2VycyBvbiBhIHNlcnZlciBhbmQgaG93IG1hbnkgZm9sbG93ZXJzIEkgaGF2ZSBmcm9tIHRoYXQgc2VydmVyLg0KDQpJbmNsdWRlIHNvbWUgcGFja2FnZXMuLi4NCg0KYGBge3J9DQpsaWJyYXJ5KHJ0b290KQ0KbGlicmFyeShzdHJpbmdyKQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpgYGANCg0KDQpHZXQge3J0b290fSBhdXRob3Jpc2VkIHRvIHRhbGsgdG8gbXkgc2VydmVyOg0KDQpgYGB7ciBldmFsPUZBTFNFfQ0KYXV0aF9zZXR1cChpbnN0YW5jZSA9ICJ0ZWNoLmxnYnQiLCB0eXBlID0idXNlciIpDQpgYGANCg0KSSdsbCBuZWVkIG15IElEOiB3aG8gYW0gST8NCg0KYGBge3J9DQphY2MgPC0gc2VhcmNoX2FjY291bnRzKCJAQW5kaUB0ZWNoLmxnYnQiKQ0KYWNjIHw+DQogIHNlbGVjdChpZCwgYWNjdCwgZGlzcGxheV9uYW1lKSB8Pg0KICBoZWFkKDEpDQpgYGANCg0KSXQgbWUhDQoNCmBgYHtyfQ0Kd2hvYW1pIDwtICIxMDkyNzMzNDg2OTAzMzgxMjkiDQpgYGANCg0KDQpUaGVyZSdzIGEgaGFuZHkgZnVuY3Rpb24gaW4ge3J0b290fSBmb3IgZ2V0dGluZyBhbGwgZm9sbG93ZXJzOyBob3dldmVyLCBpdCBkb2Vzbid0IGN1cnJlbnRseSBzdXBwb3J0IGF1dG8tcGFnaW5hdGlvbi4gQWZ0ZXIgW3JlYWRpbmcgdGhlIGZyaWVuZGx5IG1hbnVhbF0oaHR0cHM6Ly9naXRodWIuY29tL3NjaG9jaGFzdGljcy9ydG9vdC93aWtpL1BhZ2luYXRpb24pLCBoZXJlJ3MgYSB3b3JrYXJvdW5kOg0KDQpgYGB7cn0NCnJlYWxseV9nZXRfYWxsX2ZvbGxvd2VycyA8LSBmdW5jdGlvbihpZCwgc3VyZSA9ICJObyEiKSB7DQogIHN0b3BpZm5vdChzdXJlID09ICJZZXMsIEkga25vdyB3aGF0IEkgYW0gZG9pbmciKQ0KICANCiAgZm9sbG93ZXJzIDwtIGMoKQ0KICBzdGlsbF93b3JraW5nIDwtIFRSVUUNCiAgbWF4X2lkIDwtIE5VTEwNCiAgDQogIHdoaWxlIChzdGlsbF93b3JraW5nKSB7DQogICAgbmV4dF9sb3QgPC0gZ2V0X2FjY291bnRfZm9sbG93ZXJzKGlkLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXhfaWQgPSBtYXhfaWQpDQogICAgZm9sbG93ZXJzIDwtIGJpbmRfcm93cyhmb2xsb3dlcnMsIG5leHRfbG90KQ0KICANCiAgICBhdHRycyA8LSBhdHRyKG5leHRfbG90LCAiaGVhZGVycyIpDQogICAgaWYgKCJtYXhfaWQiICVpbiUgbmFtZXMoYXR0cnMpKQ0KICAgICAgbWF4X2lkIDwtIGF0dHJzJG1heF9pZA0KICAgIGVsc2UNCiAgICAgIHN0aWxsX3dvcmtpbmcgPC0gRkFMU0UNCiAgfQ0KICANCiAgZm9sbG93ZXJzDQp9DQpgYGANCg0KDQpHZXQgbXkgZm9sbG93ZXJzOg0KDQpgYGB7cn0NCm15X2ZvbGxvd2VycyA8LSByZWFsbHlfZ2V0X2FsbF9mb2xsb3dlcnMoDQogIHdob2FtaSwNCiAgc3VyZSA9ICJZZXMsIEkga25vdyB3aGF0IEkgYW0gZG9pbmciDQopDQpgYGANCg0KDQpUaGlzIG51bWJlciBpcyBjb3JyZWN0OiBpdCB3b3JrZWQhDQoNCmBgYHtyfQ0KbnJvdyhteV9mb2xsb3dlcnMpDQpgYGANCg0KDQpXaGF0IHNlcnZlcnMgYXJlIHRoZXkgZnJvbT8NCg0KYGBge3J9DQpnZXRfc2VydmVycyA8LSBmdW5jdGlvbihmb2xsb3dlcnMpIHsNCiAgc2VydmVycyA8LSBmb2xsb3dlcnMkYWNjdCB8PiBzdHJfc3BsaXRfZml4ZWQoIkAiLCAyKQ0KICBzZXJ2ZXJzWywyXQ0KfQ0KYGBgDQoNCg0KYGBge3J9DQpmb2xsb3dlcnNfc2VydmVycyA8LSBteV9mb2xsb3dlcnMgfD4NCiAgbXV0YXRlKHNlcnZlciA9IGdldF9zZXJ2ZXJzKG15X2ZvbGxvd2VycykpIHw+DQogIG11dGF0ZShzZXJ2ZXIgPSBpZmVsc2Uoc2VydmVyID09ICIiLCAidGVjaC5sZ2J0Iiwgc2VydmVyKSkNCmBgYA0KDQpIZXJlIGFyZSB0aGUgY291bnRzOg0KDQpgYGB7cn0NCnNlcnZlcl9jb3VudCA8LSBmb2xsb3dlcnNfc2VydmVycyB8Pg0KICBncm91cF9ieShzZXJ2ZXIpIHw+DQogIHN1bW1hcmlzZShuID0gbigpKSB8Pg0KICBhcnJhbmdlKGRlc2MobikpDQoNCnNlcnZlcl9jb3VudA0KYGBgDQoNCkNoZWNrIGV2ZXJ5dGhpbmcgYWRkcyB1cDoNCg0KYGBge3J9DQpzZXJ2ZXJfY291bnQkbiB8PiBzdW0oKQ0KYGBgDQoNClNvIGZhciBzbyBnb29kLg0KDQpOZXh0IHVwLCBob3cgbWFueSB1c2VycyBhcmUgdGhlcmUgb24gZWFjaCBvZiB0aG9zZSBzZXJ2ZXJzPyBOb3RlIHRoZSBleGNlcHRpb24gaGFuZGxpbmcuLi4NCg0KYGBge3J9DQpnZXRfdXNlcl9jb3VudCA8LSBWZWN0b3JpemUoZnVuY3Rpb24oc2VydmVyKSB7DQogIHJlcyA8LSBOQQ0KICANCiAgIyBUaGlzIHdpbGwgY2F0Y2ggcHJvYmxlbXMgbGlrZSBtaXNzaW5nIHNlcnZlcnMNCiAgdHJ5Q2F0Y2goDQogICAgcmVzIDwtIGdldF9pbnN0YW5jZV9nZW5lcmFsKHNlcnZlcikkc3RhdHMkdXNlcl9jb3VudCwNCiAgICBlcnJvciA9IGZ1bmN0aW9uKGUpIHsNCiAgICAgICAgY2F0KCIqKioiKQ0KICAgICAgICBjYXQoc2VydmVyKQ0KICAgICAgICBjYXQoIioqKiIpDQogICAgICAgIGNhdCgiXG4iKQ0KICAgICAgICBwcmludChlKQ0KICAgICAgICBjYXQoIlxuIikNCiAgICAgIH0NCiAgKQ0KICByZXMNCn0pDQpgYGANCg0KYGBge3J9DQpzZXJ2ZXJfY291bnQkc2VydmVyX3VzZXJfbiA8LSBnZXRfdXNlcl9jb3VudChzZXJ2ZXJfY291bnQkc2VydmVyKQ0Kc2VydmVyX2NvdW50DQpgYGANCg0KQSBjb3VwbGUgb2YgaGlzdG9ncmFtczoNCg0KYGBge3J9DQpzZXJ2ZXJfY291bnQgfD4NCiAgc2VsZWN0KG4sIHNlcnZlcl91c2VyX24pIHw+DQogIG5hLm9taXQoKSAgfD4NCiAgcGl2b3RfbG9uZ2VyKGNvbHMgPSBldmVyeXRoaW5nKCksDQogICAgICAgICAgICAgICBuYW1lc190byA9ICJrZXkiLA0KICAgICAgICAgICAgICAgdmFsdWVzX3RvID0gInZhbHVlIikgfD4NCiAgbXV0YXRlKG5pY2VfbmFtZSA9IGNhc2Vfd2hlbihrZXkgPT0gIm4iIH4gIkZvbGxvd2VycyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAga2V5ID09ICJzZXJ2ZXJfdXNlcl9uIiB+ICJVc2VycyBvbiBzZXJ2ZXIiKSkgfD4NCiAgZ2dwbG90KGFlcyh2YWx1ZSkpICsNCiAgICBmYWNldF93cmFwKH4gbmljZV9uYW1lLCBzY2FsZXMgPSAiZnJlZSIpICsNCiAgICBnZW9tX2hpc3RvZ3JhbShiaW5zID0gNDApICsNCiAgICBsYWJzKHggPSAiVXNlcnMiLCB5ID0gIkZyZXEiKQ0KYGBgDQoNCg0KDQpBIHNjYXR0ZXJwbG90Og0KDQpgYGB7cn0NCnNlcnZlcl9jb3VudCB8Pg0KICBuYS5vbWl0KCkgfD4NCiAgbXV0YXRlKGhvbWUgPSBpZmVsc2Uoc2VydmVyID09ICJ0ZWNoLmxnYnQiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIk15IGhvbWUgc2VydmVyIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJFbHNld2hlcmUiKSwNCiAgICAgICAgIGhvbWUgPSBmYWN0b3IoaG9tZSwNCiAgICAgICAgICAgICAgICAgICAgICAgYygiTXkgaG9tZSBzZXJ2ZXIiLCAiRWxzZXdoZXJlIikpKSB8Pg0KICBnZ3Bsb3QoYWVzKHkgPSBsb2cobiwgMTApLA0KICAgICAgICAgICAgIHggPSBsb2coc2VydmVyX3VzZXJfbiwgMTApLA0KICAgICAgICAgICAgIGNvbG91ciA9IGhvbWUpKSArDQogIGdlb21fcG9pbnQoKSArDQogIHNjYWxlX2NvbG91cl9tYW51YWwodmFsdWVzID0gYygibWFnZW50YSIsICJibGFjayIpKSArDQogICN0aGVtZV9idygpICsNCiAgbGFicyh5ID0gZXhwcmVzc2lvbihsb2dbMTBdfihmb2xsb3dlcnMpKSwNCiAgICAgICB4ID0gZXhwcmVzc2lvbihsb2dbMTBdfih0b3RhbH5zZXJ2ZXJ+dXNlcnMpKSwNCiAgICAgICB0aXRsZSA9ICJGb2xsb3dlciBjb3VudCBieSBzZXJ2ZXIiLA0KICAgICAgIGNvbG91ciA9ICIiKQ0KYGBgDQoNClRoZXJlIGlzIGluZGVlZCBhIGNvcnJlbGF0aW9uOg0KDQpgYGB7cn0NCmNvci50ZXN0KH4gbiArIHNlcnZlcl91c2VyX24sIGRhdGEgPSBzZXJ2ZXJfY291bnQsIG1ldGhvZCA9ICJrZW5kYWxsIikNCmBgYA0KDQpMYXN0IHJ1biAob3IgYXQgbGVhc3Qga25pdHRlZCkgYHIgZGF0ZSgpYC4NCg0KDQo=