The influence of the early 2020s crises on the .cz domain
ADAM report 2/2025
To better understand how the .cz domain fared in face of the challenges that emerged during the first half of the 2020s, I investigated whether the spread of COVID-19, inflation rate, and the war in Ukraine associated with the count of second-level domains under .cz, the count of Czech holders of .cz domains, and traffic under .cz. The analysis shows that all of these were diversely related to the changes in the transmission of COVID-19 and the rise and fall of the inflation rate in the Czech Republic. Domain and holder counts decreased during the first COVID-19 wave but increased during the second wave. Domain counts also decreased when the inflation rate was high. Traffic increased during both COVID-19 waves.
1 Introduction
The COVID-19 pandemic, increased inflation rate, and the war in Ukraine have influenced many aspects of life in the early 2020s. The goal of this report was to investigate whether any of these crises also related to trends within the .cz domain. Can we observe some associations between monthly counts of domains, counts of holders, and average traffic on one side and the spread of COVID-19, the rise and fall of inflation, and the Russo-Ukrainian war on the other?
COVID-19. One popular notion claims that the COVID-19 pandemic lock-downs forced much of social life into the online space (Zanella et al. 2024). People increasingly turned to the internet for the purposes of work, education, social life, and entertainment (Feldmann et al. 2021; Priyadarshini et al. 2022). Then, if businesses and individuals opted to move their operations and interests online, such increased demand for online activities and presence might have influenced the count of domains in the .cz TLD. Already in April 2020, an ADAM report (Andziński et al. 2020) observed an increase in domain registrations connected to the pandemic, although it focused on second-level domains directly related to the COVID-19 pandemic. Here, I focus on the count of all second level domains as the pandemic probably did not prompt an increase in creation of domains exclusively related to the COVID-19 pandemic (and because more time has passed since, allowing for such examination). After all, many actors might have been outright forced by the COVID-19 to go online with their interests. Supporting this assertion, we can observe that after a period of saturation and stagnation (2017–2019), the count of second-level domains in the .cz TLD started growing once again when the COVID-19 pandemic raged most prominently (in the period between 2020 and 2022). After 2022, the domain count seems to stagnate once again.
Beside the domain count, the count of Czech holders of .cz domains shows a larger increase in 2020 than the year-on-year increases observed between 2017 and 2019.1 Additionally, traffic kept on rising throughout the years (see Figure 3), providing us with yet another perspective for the inquiry on whether the COVID-19 pandemic influenced trends in the .cz domain. Taken together, we may hypothesize that the spread of COVID-19 influenced the count of domains, the count of Czech domain holders, and traffic; namely:
H1: Transmission of COVID-19 positively associates with the domain count.
H2: Transmission of COVID-19 positively associates with the count of Czech holders.
H3: Transmission of COVID-19 positively associates with QPS.
Inflation. Global supply chains problems, increases and volatility in energy and commodity prices, the Russo-Ukrainian war, increased demand, fiscal policies reacting to the COVID-19 pandemic, and companies passing the emergent price shocks to their customers all resulted in increased inflation (Komárek et al. 2024), which affected the Czech Republic rather harshly when compared to other European states (Czech National Bank 2022). When looking into the monthly year-on-year inflation rates, we can observe an increasing trend since summer 2021, peaking in summer 2022, and then decreasing throughout the whole year 2023, until reaching pre-2019 rates in 2024 (see Figure 5). Such increased consumer prices surely created a pressure to reevaluate resource allocation among individuals and businesses. In turn, this pressure could have lead to a diminished interest in creating new domains or renewing already registered domains, hypothetically reducing the counts of both domains and holders (As for the traffic, it is less clear whether the inflation should be envisioned to have influenced its values; however, it is worth to explore this possibility too, without stating the direction of the proposed association). This is, of course, an assertion that tries to paint a narrative for how inflation might have influenced the said variables. The price of a single .cz domain is not that high to necessarily burden most budgets. Nevertheless, it is not easy to discern whether inflation itself might be the culprit or if it “merely” coincides with other trends and processes which could have been “actually” responsible for such proposed associations. Even so, it is worth investigating the inflation rate because it can provide us with a useful proxy, describing a period of socio-economic hardship and its influence on the .cz domain. Then, with such a grain of salt, it seems worthwhile exploring hypotheses which propose the following.
H4: Inflation negatively associates with the domain count.
H5: Inflation negatively associates with the count of Czech domain holders.
H6: Inflation associates with QPS.
Russo-Ukrainian war. Lastly, although already mentioned in the previous paragraph as one of the causes of increased inflation, it might prove interesting to consider whether the Russo-Ukrainian war2 has manifested some influence on the .cz domain. In comparison to the already resolved inflation crisis, the conflict is still ongoing and may exert some influence of its own on the .cz domain. Beside causing geopolitical instability and energy crises, the Russian aggression also brought a large wave of Ukrainian war-refugees seeking asylum in the Czech Republic and an information war largely pronounced in the online space. Consequently, new domains dedicated either to help the Ukrainian war refugees or to the dissemination of information supporting either of the sides and their coalitionary partners could have emerged, along with new holders and increased QPS. Another ADAM report (Quiros Segovia, Andziński, and Helebrant 2022) found that a small increase in conflict-related domains was observed around the time when the invasion began. However, whether a similar increase can be estimated over a longer period and on the total domain count remains an open question. Therefore, we can hypothesize that:
H7: The war in Ukraine positively associates with the domain count.
H8: The war in Ukraine positively associates with the count of Czech domain holders.
H9: The war in Ukraine positively associates with QPS.
Of course, the temporal dimension is important for such an analysis because the count of domains, holders, and queries naturally change over time and are determined by their own preceding historical values. Therefore, I also focused on overall temporal trends and seasonal patterns which are known to associate with domain counts (see Quiros Segovia and Řezníček 2025).
Note that most charts (or at least the focal ones) in this report are interactive—they can be zoomed, filtered (by clicking or double clicking items in the legend), and hovering the mouse cursor over datapoints reveals further information.
Sections marked by “▶ Code” can be expanded to reveal the actual R code (R Core Team 2024) used for producing the statistics, graphs, and tables.
To keep the text lighter, additional information is often collapsed within callout blocks (such as this one) which can be expanded (or collapsed) by clicking on their headers.
Code
library(dplyr)
library(readr)
library(ggplot2)
library(plotly)
library(lubridate)
library(tidyr)
library(knitr)
library(kableExtra)
library(formattable)
library(jsonlite)
library(mgcv)
library(gratia)
library(forecast)
library(forcats)
library(colorspace)
library(countrycode)
library(ggthemes)
library(scales)
library(performance)
library(purrr)
library(GGally)
library(marginaleffects)
theme_set(theme_minimal())
Code
<- "https://stats.nic.cz"
root_endpoint
<- function(endpoint, ...) {
api_endpoint <- list(...)
pars if (length(pars) == 0) {
return(paste0(root_endpoint, endpoint))
}<- mapply(function(x, y) paste(x, URLencode(toString(y)), sep = "="),
pstr names(pars), pars)
paste0(root_endpoint, endpoint, "?", paste(pstr, collapse = "&"))
}
<- function(url, token=Sys.getenv("API_TOKEN")) {
fromJSONwithToken <- curl::new_handle()
curl_handle if (token != "") {
::handle_setheaders(curl_handle,
curl"Authorization" = paste("Bearer", token)
)
}<- curl::curl_fetch_memory(url, handle = curl_handle)
request return(jsonlite::fromJSON(rawToChar(request$content)))
}
2 Data
All data used in this report are available as .csv files in this report’s repository and also shown entirely in a callout block in Section 2.7.
2.1 Domains
Data on the domain count were obtained using the /fred_domains endpoint from the ADAM project’s Postgres database and operationalized as domain counts at the end of each month.
Figure 1 illustrates domain count’s overall trend, also discerning some seasonal patterns within respective years—beside the saturation of domain counts (left), we can observe the typical double-peaked seasonal pattern (right).
Code
#data imported and prepared
<- fromJSON(
df_domains api_endpoint("/fred_domains")
|>
) filter(zone == "cz") |>
mutate(
year = year(ts),
month = month(ts),
day = day(ts),
ts = as.Date(ts)
|>
) group_by(year, month) |>
filter(day == max(day)) |>
select(ts, year, month, domains) |>
ungroup() |>
filter(year >= 2020) |>
filter(ts < "2024-10-01")
#and saved as a csv
write_csv(df_domains, file = "domains.csv")
Code
<- read_csv("domains.csv", show_col_types = FALSE) df_domains
Code
plot_ly(
data = df_domains,
type = "scatter",
mode = "lines",
x = ~ts,
y = ~domains
|>
) layout(
xaxis = list(title = "Date"),
yaxis = list(title = "Domain count")
)plot_ly(
data = df_domains,
type = "scatter",
mode = "lines",
x = ~month,
y = ~domains,
color = ~as.factor(year)
|>
) layout(
xaxis = list(title = "Month"),
yaxis = list(title = "Domain count")
)
2.2 Czech holders
Data on the domain holders were obtained using the /fred_holders_by_cc endpoint. Again, the values were operationalized as counts at the end of each month and only Czech holders were kept in the analysis. Figure 2 illustrates the overall trend in holder count, also discerning the seasonal patterns within respective years. While the count of holders does not seem to saturate as the count of domains does, a similar seasonal pattern is present, although little less pronounced than in the domain count.
Code
#data imported and prepared
<- fromJSON(
df_holders api_endpoint("/fred_holders_by_cc")
|>
) filter(zone == "cz" & cc == "CZ") |>
mutate(
year = year(ts),
month = month(ts),
day = day(ts),
ts = as.Date(ts)
|>
) group_by(year, month) |>
filter(day == max(day)) |>
select(ts, year, month, holders) |>
ungroup() |>
filter(year >= 2020) |>
filter(ts < "2024-10-01")
#and saved as a csv
write_csv(df_holders, file = "holders.csv")
Code
<- read_csv("holders.csv", show_col_types = FALSE) df_holders
Code
plot_ly(
data = df_holders,
type = "scatter",
mode = "lines",
x = ~ts,
y = ~holders
|>
) layout(
xaxis = list(title = "Date"),
yaxis = list(title = "CZ holder count")
)plot_ly(
data = df_holders,
type = "scatter",
mode = "lines",
x = ~month,
y = ~holders,
color = ~as.factor(year)
|>
) layout(
xaxis = list(title = "Month"),
yaxis = list(title = "CZ holder count")
)
2.3 Traffic
As for the last response variable modeled in this report, traffic data were obtained using the /cz_qps_total_1h endpoint and summarized into monthly mean QPS values. The data begin in September 2020, getting rid of some non-reliable observations. Figure 3 illustrates the overall monthly trend in traffic, also discerning the seasonal patterns within respective years. The trend shows slightly accelerating increase over time. The seasonal pattern is not as clear as for the domain and holder counts but seems to exhibit some sort of summer depression (although not in 2024).
Code
#data imported and prepared
<- fromJSON(
df_qps api_endpoint("/cz_qps_total_1h")
|>
) mutate(
year = year(ts),
month = month(ts),
ts = as.Date(ts)
|>
) group_by(year, month) |>
reframe(
ts = max(ts),
year = year,
month = month,
qps = mean(qps)
|>
) ungroup() |>
unique() |>
filter(ts >= "2020-09-01") |>
filter(ts < "2024-10-01")
#and saved as a csv
write_csv(df_qps, file = "qps.csv")
Code
<- read_csv("qps.csv", show_col_types = FALSE) df_qps
Code
plot_ly(
data = df_qps,
type = "scatter",
mode = "lines",
x = ~ts,
y = ~qps
|>
) layout(
xaxis = list(title = "Date"),
yaxis = list(title = "QPS")
)plot_ly(
data = df_qps,
type = "scatter",
mode = "lines",
x = ~month,
y = ~qps,
color = ~as.factor(year)
|>
) layout(
xaxis = list(title = "Month"),
yaxis = list(title = "QPS")
)
2.4 COVID-19
For the COVID-19, data by Our World in Data were used. I considered the count of new cases, count of new deaths, and the stringency index as predictor variables, opting for the new cases as they exhibited most fluent seasonality patterns of the three (see Figure 4). Sub-figure a clearly shows two massive spikes of new COVID-19 cases in the winter of 2021 and 2022, and sub-figure b illustrates how this trend behaved seasonally, peaking in January and February (in 2021 and 2022) and gaining on strength since September (in 2020 and 2021) after a seasonal decline during summer.
Code
<-
df_covid read_csv("cz_covid.csv") |>
select(date,
stringency_index,
new_cases,
new_deaths,|>
) mutate(
year = year(date),
month = month(date),
ts = as.Date(date)
|>
) group_by(year, month) |>
reframe(
ts = max(ts),
max_stringency = max(stringency_index),
new_cases = sum(new_cases),
new_deaths = sum(new_deaths)
|>
) ungroup() |>
unique()
Code
plot_ly(
data = df_covid,
type = "scatter",
mode = "lines",
x = ~ts,
y = ~new_cases
|>
) layout(
xaxis = list(title = "Date"),
yaxis = list(title = "New cases per month")
)plot_ly(
data = df_covid,
type = "scatter",
mode = "lines",
x = ~month,
y = ~new_cases,
color = ~as.factor(year)
|>
) layout(
xaxis = list(title = "Month"),
yaxis = list(title = "New cases per month")
)
2.5 Inflation
For the inflation rate, data by the Czech Statistical Office were used. I opted for the increase in CPI compared with the corresponding month of a preceding year metric as it indicates a percentage change in the price level between the reference month of a given year and the corresponding month of a preceding year. Figure 5 shows inflation’s trend (and the irrelevance of seasonality).
Code
<-
df_inflation read_csv("inflation.csv") |>
pivot_longer(
!year, names_to = "month", values_to = "inflation"
|>
) filter(year > 2018) |>
mutate(
ts = as.Date(paste0(year, "-", month, "-15")),
year = as.integer(year),
month = as.integer(month)
|>
) arrange(year, month)
Code
plot_ly(
data = df_inflation,
type = "scatter",
mode = "lines",
x = ~ts,
y = ~inflation
|>
) layout(
xaxis = list(title = "Date"),
yaxis = list(title = "Inflation rate")
)plot_ly(
data = df_inflation,
type = "scatter",
mode = "lines",
x = ~month,
y = ~inflation,
color = ~as.factor(year)
|>
) layout(
xaxis = list(title = "Month"),
yaxis = list(title = "Inflation rate")
)
2.6 Russo-Ukrainian war
Lastly, the Russo-Ukrainian war was specified as a factor variable with values “Peace” (Until February 23th 2022) and “War” (since February 24th 2022).
Code
<- df_qps |>
df_crises left_join(df_domains, by = c("year", "month")) |>
left_join(df_holders, by = c("year", "month")) |>
left_join(df_covid, by = c("year", "month")) |>
left_join(df_inflation, by = c("year", "month")) |>
select(!c(ts.x, ts.y, ts.x.x, ts.y.y)) #drop duplicates
Code
<- df_crises |>
df_crises mutate(
ukraine =
case_when(ts < "2022-02-24" ~ "Peace",
> "2022-02-24" ~ "War"),
ts ukraine = as.factor(ukraine),
year_f = as.factor(year),
max_stringency =
if_else(is.na(max_stringency), 0, max_stringency),
new_cases =
if_else(is.na(new_cases), 0, new_cases),
new_deaths =
if_else(is.na(new_deaths), 0, new_deaths),
time = seq(from = 1,
to = nrow(df_crises),
by = 1),
new_cases = new_cases/1000 #turn into thousands
|>
) ungroup()
2.7 The dataset
The entire dataset is available for inspection in the callout block below. Note that new cases of COVID-19 were recalculated into thousands.
Code
|>
df_crises mutate(date = paste0(year, "-", month)) |>
select(date, time,
domains,
holders,
qps,
new_cases,
inflation,|>
ukraine) kable(
"html",
digits = 2,
col.names = c(
"Date", "Time",
"Domains",
"CZ holders",
"QPS",
"New COVID cases",
"Inflation",
"War in Ukraine"
) )
Date | Time | Domains | CZ holders | QPS | New COVID cases | Inflation | War in Ukraine |
---|---|---|---|---|---|---|---|
2020-9 | 1 | 1368781 | 654791 | 14894.78 | 39.25 | 3.2 | Peace |
2020-10 | 2 | 1364093 | 654611 | 13928.01 | 187.94 | 2.9 | Peace |
2020-11 | 3 | 1369032 | 656968 | 14671.87 | 269.55 | 2.7 | Peace |
2020-12 | 4 | 1370804 | 658071 | 15398.89 | 154.10 | 2.3 | Peace |
2021-1 | 5 | 1378195 | 661960 | 16585.94 | 318.62 | 2.2 | Peace |
2021-2 | 6 | 1387674 | 666320 | 16859.40 | 254.11 | 2.1 | Peace |
2021-3 | 7 | 1395295 | 669351 | 16143.66 | 284.34 | 2.3 | Peace |
2021-4 | 8 | 1399309 | 670642 | 14679.03 | 106.44 | 3.1 | Peace |
2021-5 | 9 | 1401016 | 670961 | 15334.59 | 42.92 | 2.9 | Peace |
2021-6 | 10 | 1401323 | 670946 | 15342.71 | 5.71 | 2.8 | Peace |
2021-7 | 11 | 1400490 | 670716 | 14853.23 | 5.57 | 3.4 | Peace |
2021-8 | 12 | 1403622 | 671094 | 15348.25 | 6.54 | 4.1 | Peace |
2021-9 | 13 | 1408476 | 672685 | 15507.51 | 10.86 | 4.9 | Peace |
2021-10 | 14 | 1413755 | 674090 | 15915.06 | 75.39 | 5.8 | Peace |
2021-11 | 15 | 1420857 | 675185 | 15788.99 | 369.83 | 6.0 | Peace |
2021-12 | 16 | 1424131 | 675342 | 15405.15 | 331.06 | 6.6 | Peace |
2022-1 | 17 | 1432454 | 676575 | 16097.25 | 617.10 | 9.9 | Peace |
2022-2 | 18 | 1441347 | 679048 | 15724.20 | 686.04 | 11.1 | Peace |
2022-3 | 19 | 1444213 | 679567 | 15897.77 | 253.42 | 12.7 | War |
2022-4 | 20 | 1442541 | 675817 | 15838.66 | 127.79 | 14.2 | War |
2022-5 | 21 | 1439607 | 674849 | 16318.36 | 28.96 | 16.0 | War |
2022-6 | 22 | 1438268 | 674863 | 16551.80 | 11.57 | 17.2 | War |
2022-7 | 23 | 1437182 | 674511 | 15555.57 | 75.01 | 17.5 | War |
2022-8 | 24 | 1440667 | 675363 | 17635.59 | 72.11 | 17.2 | War |
2022-9 | 25 | 1445559 | 677033 | 17168.33 | 81.06 | 18.0 | War |
2022-10 | 26 | 1452935 | 678306 | 17176.03 | 94.90 | 15.1 | War |
2022-11 | 27 | 1462843 | 679512 | 17342.93 | 21.35 | 16.2 | War |
2022-12 | 28 | 1463116 | 679025 | 17265.51 | 21.78 | 15.8 | War |
2023-1 | 29 | 1463084 | 680624 | 17330.86 | 11.38 | 17.5 | War |
2023-2 | 30 | 1470108 | 683378 | 17950.18 | 19.24 | 16.7 | War |
2023-3 | 31 | 1473706 | 684943 | 18565.98 | 22.70 | 15.0 | War |
2023-4 | 32 | 1468610 | 684686 | 19183.52 | 9.57 | 12.7 | War |
2023-5 | 33 | 1466240 | 684678 | 18901.38 | 1.64 | 11.1 | War |
2023-6 | 34 | 1467104 | 684544 | 18791.62 | 0.68 | 9.7 | War |
2023-7 | 35 | 1466822 | 683683 | 17519.86 | 0.34 | 8.8 | War |
2023-8 | 36 | 1468764 | 684997 | 18867.83 | 0.98 | 8.5 | War |
2023-9 | 37 | 1471167 | 686788 | 18599.48 | 5.38 | 6.9 | War |
2023-10 | 38 | 1472772 | 688821 | 19236.23 | 16.18 | 8.5 | War |
2023-11 | 39 | 1474194 | 690459 | 20969.48 | 24.73 | 7.3 | War |
2023-12 | 40 | 1468788 | 690326 | 19866.98 | 56.28 | 6.9 | War |
2024-1 | 41 | 1467715 | 692502 | 20806.68 | 9.49 | 2.3 | War |
2024-2 | 42 | 1471419 | 695908 | 20948.74 | 2.18 | 2.0 | War |
2024-3 | 43 | 1472117 | 696938 | 20194.06 | 0.66 | 2.0 | War |
2024-4 | 44 | 1469049 | 697046 | 19044.95 | 0.27 | 2.9 | War |
2024-5 | 45 | 1467388 | 697560 | 20573.34 | 0.27 | 2.6 | War |
2024-6 | 46 | 1465440 | 697782 | 21473.08 | 0.00 | 2.0 | War |
2024-7 | 47 | 1464980 | 698143 | 21833.11 | 0.00 | 2.2 | War |
2024-8 | 48 | 1465976 | 699412 | 21685.01 | 0.00 | 2.2 | War |
2024-9 | 49 | 1468911 | 701506 | 21624.75 | 0.00 | 2.6 | War |
3 Models
In this section, models estimating associations with domain counts are presented first (Section 3.1), followed by models focused on holder counts (Section 3.2), and then models focused on the traffic (Section 3.3). For all, the same modelling approach was used, although some models are hidden in collapsed callout blocks (available for the keen readers). The domain counts were modeled using the negative-binomial distribution, the domain holders with the Poisson distribution, and the traffic with the normal distribution. All models were fit using the mgcv
package’s gamm
function which provides tools for fitting generalized additive mixed models (“Mgcv: Mixed GAM Computation Vehicle with Automatic Smoothness Estimation” 2000, ver 1.9-1; Wood 2017; Simpson 2018a, 2018b; Pedersen et al. 2019) (see callout block below).
Generalized additive models (GAMs) are often portrayed to be situated in a middle ground between interpretable but often inflexible linear models and flexible but black-boxish machine learning models. GAMs can be used to model nonlinear relationships (overcoming limits of linear models) while still providing inferential statistics and explanatory insights (avoiding the black-box nature of predictions made by machine learning models).
To capture these non-linear relationships, GAMs use smooth functions which are functions that are composed of smaller basis functions. While the smaller basis functions capture smaller fractions of the relationships, they add up into the bigger smooth function, which is in turn able to describe nonlinear relationships between the variables. In effect, the associations estimated by GAMs wiggle as the size of the relationship between variables need not be linear.
Additionally, generalized additive mixed models (GAMMs) offer further modeling possibilities. In this report, it is the specification of correlation structures which helps account for residual autocorrelations in the data that has not been accounted for by the smooths.
For an introduction on GAMs, an interactive course by Noam Ross or an introductory text by Michael Clark are recommended. Furthermore, introductory lectures by Noam Ross and Gavin Simpson are also freely available.
3.1 Domains
To set some initial model specification, gamm_domains_1
estimates the domain count by a seasonal pattern (months within a year), an overall monthly trend, (thousands of) new COVID-19 cases, the rate of inflation, and whether the Russo-Ukrainian war was ongoing. I also specified a varying intercept for the respective years and a correlation structure accounting for the autocorrelation of observations. However, this initial model does not yet interact the focal predictors with the temporal variables (providing a simpler perspective which sets ground for the following models), therefore, all the results are hidden within the collapsed callout blocks.
Code
<- gamm(
gamm_domains_1 ~
domains s(month, k = 12, bs = "cc") +
s(time) +
s(new_cases) +
s(inflation) +
ukraine,random = list(year_f = ~ 1),
correlation =
corARMA(form = ~ time,
p = 2,
q = 2),
data = df_crises,
family = nb,
method = "REML"
)saveRDS(gamm_domains_1, file = "gamm_domains_1.rds")
Code
<- readRDS(file = "gamm_domains_1.rds") gamm_domains_1