Show code (Load required R packages)
library(tidyverse)
library(glue)
library(lubridate)
library(ggdist)
library(patchwork)
library(cowplot)
library(tinytable)
library(effectsize)Multi-platform Trace Data Reveal Demographic Differences in Video Game Play, but Individuals Vary Far More
Most knowledge about “who plays (what) video games” comes from surveys subject to recall bias and social desirability effects. Using publicly available behavioral logs from Steam, Xbox, and Nintendo spanning 1.5 million hours from 3768 US and UK adults (18–40 years old), I present a visualization-driven descriptive analysis of how play patterns differ according to age, gender, ethnicity, and neurodiversity. Results uncover a variety of trends that have rarely been observed in public data: among others, that older players’ peak playtime occurs approximately 1 hour earlier in the day, that women tend to re-engage with the same game for a longer time period, and that sports games were more popular among Black, Asian, and neurotypical players than among other ethnic groups and neurodiverse players. Despite intriguing group-level trends, however, within-group variation is far larger: demographic characteristics account for at most 5.4% of gameplay behavioral variation. This work provides an empirically grounded complement to survey research and motivates future investigation into the structural and cultural factors shaping play behavior.
video games, demographics, behavioral data, descriptive, genres
library(tidyverse)
library(glue)
library(lubridate)
library(ggdist)
library(patchwork)
library(cowplot)
library(tinytable)
library(effectsize)set.seed(8675309)
options(scipen = 999, timeout = 600)
theme_set(theme_minimal())
theme_update(
plot.background = element_rect(fill = "white", color = NA),
panel.background = element_rect(fill = "white", color = NA),
strip.background = element_rect(fill = "black"),
strip.text = element_text(color = "white", size = 10, face = "bold"),
axis.text.y = element_text(color = "black", size = 10),
axis.text.x = element_text(color = "black", size = 10),
panel.grid.minor = element_blank(),
panel.border = element_rect(
colour = "black",
fill = NA,
linewidth = 1
)
)
# Color codebook for consistent theming across all figures
# Each demographic dimension uses a single-hue gradient for easy visual grouping
# Lightest colors are still dark enough to be visible on white backgrounds
# Age: Blues (light to dark with age)
colors_age <- c(
"18-24" = "#9ECAE1",
"25-30" = "#4292C6",
"31-35" = "#2171B5",
"36-40" = "#084594"
)
# Gender: Oranges/reds
colors_gender <- c(
"Man" = "#FD8D3C",
"Woman" = "#D94801",
"Non-binary" = "#7F2704"
)
# Ethnicity: Greens
colors_ethnicity <- c(
"Asian" = "#74C476",
"Black" = "#31A354",
"Multiple" = "#006D2C",
"Other" = "#00441B",
"White" = "#002910"
)
# Neurodiversity: Purples
colors_neuro <- c(
"Neurotypical" = "#9E9AC8",
"ADHD" = "#756BB1",
"ASD" = "#54278F"
)
# Combined list for easy access
demo_colors <- list(
Age = colors_age,
Gender = colors_gender,
Ethnicity = colors_ethnicity,
Neurodiversity = colors_neuro
)
# Helper function to get colors for a demographic dimension
get_demo_colors <- function(demographic) {
demo_colors[[demographic]]
}
# Load helper functions
source("R/helpers.R")# Download open-play v1.2.1 from Zenodo (cached locally after first download)
zenodo_url <- "https://zenodo.org/records/20119134/files/digital-wellbeing/open-play-v1.2.1.zip?download=1"
zip_path <- "data/open-play-v1.2.1.zip"
if (!file.exists(zip_path)) {
dir.create(dirname(zip_path), showWarnings = FALSE, recursive = TRUE)
options(timeout = max(600, getOption("timeout")))
message("Downloading 194MB file from Zenodo (may take a few minutes)...")
tryCatch(
{
download.file(zenodo_url, zip_path, mode = "wb", method = "libcurl")
message("Download complete!")
},
error = function(e) {
message("Download failed. Please download manually from:")
message(zenodo_url)
message(glue("And save to: {normalizePath(zip_path, mustWork = FALSE)}"))
stop(e)
}
)
}
# Extract zip if not already extracted
extract_dir <- "data/open-play-v1.2.1"
if (!dir.exists(extract_dir)) {
message("Extracting zip archive...")
unzip(zip_path, exdir = "data")
# Rename the extracted folder to a simpler name
extracted_folder <- list.dirs("data", recursive = FALSE, full.names = TRUE)
extracted_folder <- extracted_folder[grepl(
"digital-wellbeing-open-play",
extracted_folder
)]
if (length(extracted_folder) == 1 && extracted_folder != extract_dir) {
file.rename(extracted_folder, extract_dir)
}
}
# Read raw data files
intake <- read_csv(
file.path(extract_dir, "data/clean/survey_intake.csv.gz"),
guess_max = 10000
)
surveys <- read_csv(
file.path(extract_dir, "data/clean/survey_daily.csv.gz")
) |>
filter(pid %in% intake$pid)
xbox <- read_csv(file.path(extract_dir, "data/clean/xbox.csv.gz"))
nintendo <- read_csv(file.path(extract_dir, "data/clean/nintendo.csv.gz"))
steam <- read_csv(
file.path(extract_dir, "data/clean/steam.csv.gz"),
guess_max = 10000
)
games <- read_csv(
file.path(extract_dir, "data/clean/game_metadata.csv.gz"),
guess_max = 10000
)
# Load helper functions from the dataset (includes get_dst_offset for timezone handling)
source(file.path(extract_dir, "R/helpers.R"))# Cache directory for processed telemetry (speeds up re-renders)
cache_dir <- "data/cache"
cache_file <- file.path(cache_dir, "telemetry_cache.rds")
if (file.exists(cache_file)) {
# Load cached objects
message("Loading cached telemetry data...")
cache <- readRDS(cache_file)
sessions_telemetry <- cache$sessions_telemetry
hourly_telemetry <- cache$hourly_telemetry
daily_telemetry <- cache$daily_telemetry
weekly_telemetry <- cache$weekly_telemetry
telemetry_spans <- cache$telemetry_spans
rm(cache)
} else {
message("Processing telemetry data (this may take a few minutes)...")
# Map participants to their local timezone (for DST-aware conversion)
tz_map <- intake |>
mutate(
pid = as.character(pid),
country,
local_timezone,
.keep = "none"
) |>
distinct(pid, .keep_all = TRUE)
# ---------------------------------------------------------------------------
# SESSION-LEVEL (Nintendo + Xbox only; Steam doesn't provide session data)
# ---------------------------------------------------------------------------
sessions_telemetry <- bind_rows(
xbox |> mutate(platform = "Xbox"),
nintendo |> mutate(platform = "Nintendo")
) |>
left_join(tz_map, by = "pid") |>
filter(!is.na(local_timezone)) |>
mutate(
offset_start = get_dst_offset(session_start, country, local_timezone),
offset_end = get_dst_offset(session_end, country, local_timezone),
start_local = session_start + offset_start,
end_local = session_end + offset_end,
duration_min = as.numeric(difftime(
session_end,
session_start,
units = "mins"
))
) |>
filter(
!is.na(session_start),
!is.na(session_end),
session_end > session_start,
duration_min >= 1
)
# ---------------------------------------------------------------------------
# HOURLY (all platforms)
# ---------------------------------------------------------------------------
hourly_from_sessions <- sessions_telemetry |>
filter(!is.na(start_local), !is.na(end_local)) |>
mutate(
h0_local = floor_date(start_local, "hour"),
h1_local = floor_date(end_local - seconds(1), "hour"),
n_hours = as.integer(difftime(h1_local, h0_local, units = "hours")) + 1
) |>
filter(!is.na(n_hours), n_hours > 0) |>
tidyr::uncount(n_hours, .remove = FALSE, .id = "k") |>
mutate(
hour_start_local = h0_local + hours(k - 1),
minutes = pmax(
0,
as.numeric(difftime(
pmin(end_local, hour_start_local + hours(1)),
pmax(start_local, hour_start_local),
units = "mins"
))
),
hour_start_utc = with_tz(hour_start_local, tzone = "UTC")
) |>
select(
pid,
platform,
title_id,
hour_start_local,
hour_start_utc,
minutes
) |>
group_by(pid, platform, title_id, hour_start_local, hour_start_utc) |>
summarise(minutes = sum(minutes, na.rm = TRUE), .groups = "drop")
hourly_from_steam <- steam |>
select(pid, title_id, datetime_hour_start, minutes) |>
mutate(pid = as.character(pid)) |>
left_join(tz_map, by = "pid") |>
filter(!is.na(local_timezone)) |>
mutate(
platform = "Steam",
hour_start_utc = datetime_hour_start,
offset = get_dst_offset(datetime_hour_start, country, local_timezone),
hour_start_local = datetime_hour_start + offset
) |>
select(
pid,
platform,
title_id,
hour_start_local,
hour_start_utc,
minutes
) |>
group_by(pid, platform, title_id, hour_start_local, hour_start_utc) |>
summarise(minutes = sum(minutes, na.rm = TRUE), .groups = "drop")
hourly_telemetry <- bind_rows(hourly_from_sessions, hourly_from_steam)
# ---------------------------------------------------------------------------
# DAILY (aggregated from hourly)
# ---------------------------------------------------------------------------
daily_telemetry <- hourly_telemetry |>
mutate(day_local = as.Date(hour_start_local)) |>
group_by(pid, platform, day_local) |>
summarise(minutes = sum(minutes, na.rm = TRUE), .groups = "drop")
# ---------------------------------------------------------------------------
# WEEKLY (aggregated from daily)
# ---------------------------------------------------------------------------
weekly_telemetry <- daily_telemetry |>
mutate(week = floor_date(day_local, "week")) |>
group_by(pid, platform, week) |>
summarise(minutes = sum(minutes, na.rm = TRUE), .groups = "drop")
# ---------------------------------------------------------------------------
# TELEMETRY SPANS (date ranges per participant/platform)
# ---------------------------------------------------------------------------
telemetry_spans <- daily_telemetry |>
group_by(pid, platform) |>
summarise(
telemetry_start = min(day_local, na.rm = TRUE),
telemetry_end = max(day_local, na.rm = TRUE) + hours(1),
week = floor_date(telemetry_end, "week"),
n_weeks = as.integer(difftime(
telemetry_end,
telemetry_start,
units = "weeks"
)) +
1,
.groups = "drop"
)
# Save to cache
dir.create(cache_dir, showWarnings = FALSE, recursive = TRUE)
saveRDS(
list(
sessions_telemetry = sessions_telemetry,
hourly_telemetry = hourly_telemetry,
daily_telemetry = daily_telemetry,
weekly_telemetry = weekly_telemetry,
telemetry_spans = telemetry_spans
),
cache_file
)
message("Telemetry cache saved to ", cache_file)
}# Clean and collapse demographic variables
demographics <- intake |>
mutate(
pid = as.character(pid),
# Age bins
age_group = cut(
age,
breaks = c(17, 24, 30, 35, 40),
labels = c("18-24", "25-30", "31-35", "36-40")
),
# Gender: collapse to Man / Woman / Non-binary or other
gender_clean = case_when(
gender %in% c("Man", "Male") ~ "Man",
gender %in% c("Woman", "Female") ~ "Woman",
gender == "Prefer not to say" ~ NA_character_,
is.na(gender) ~ NA_character_,
TRUE ~ "Non-binary"
),
# Ethnicity: harmonize UK/US categories
ethnicity_clean = case_when(
ethnicity %in% c("White", "White alone") ~ "White",
ethnicity %in%
c(
"Black",
"Black or African American alone",
"Black, African, Caribbean or Black British"
) ~ "Black",
ethnicity %in%
c("Asian", "Asian alone", "Asian or Asian British") ~ "Asian",
ethnicity %in%
c(
"Mixed",
"Mixed or multiple ethnic groups",
"Two or More Races"
) ~ "Multiple",
ethnicity %in% c("Prefer not to say") ~ NA_character_,
is.na(ethnicity) ~ NA_character_,
TRUE ~ "Other"
),
# Neurodiversity: specific categories (identified or diagnosed)
is_neurotypical = neuro_identify == "No",
is_adhd = !is.na(neuro_iden_adhd) | !is.na(neuro_diag_adhd),
is_autism = !is.na(neuro_iden_asd) | !is.na(neuro_diag_asd)
) |>
select(
pid,
country,
age,
age_group,
gender = gender_clean,
ethnicity = ethnicity_clean,
is_neurotypical,
is_adhd,
is_autism
)
# Aggregate weekly playtime to person-level (mean weekly hours across all weeks)
person_playtime <- weekly_telemetry |>
group_by(pid) |>
summarise(
weekly_hours = mean(minutes, na.rm = TRUE) / 60,
total_hours = sum(minutes, na.rm = TRUE) / 60,
n_weeks_observed = n(),
.groups = "drop"
)
# Join demographics to playtime
person_data <- person_playtime |>
left_join(demographics, by = "pid") |>
filter(!is.na(age_group)) # Keep only participants with demographics
# Create long-form dataset for faceted plotting
# Each row = one person × one demographic dimension
# First, create long form for age/gender/ethnicity
demo_long_basic <- person_data |>
pivot_longer(
cols = c(age_group, gender, ethnicity),
names_to = "demographic",
values_to = "group"
) |>
filter(!is.na(group)) |>
select(pid, weekly_hours, total_hours, n_weeks_observed, demographic, group)
# Create long form for neurodiversity (people can appear in multiple categories)
demo_long_neuro <- person_data |>
pivot_longer(
cols = c(is_neurotypical, is_adhd, is_autism),
names_to = "neuro_type",
values_to = "has_condition"
) |>
filter(has_condition == TRUE) |>
mutate(
demographic = "neurodiversity",
group = case_when(
neuro_type == "is_neurotypical" ~ "Neurotypical",
neuro_type == "is_adhd" ~ "ADHD",
neuro_type == "is_autism" ~ "ASD"
)
) |>
select(pid, weekly_hours, total_hours, n_weeks_observed, demographic, group)
# Combine and set factor levels for display order
person_data_long <- bind_rows(demo_long_basic, demo_long_neuro) |>
mutate(
demographic = factor(
demographic,
levels = c("age_group", "gender", "ethnicity", "neurodiversity"),
labels = c("Age", "Gender", "Ethnicity", "Neurodiversity")
)
)
pids_with_telemetry <- daily_telemetry |> distinct(pid) |> pull(pid)
analytic_sample <- demographics |>
filter(pid %in% pids_with_telemetry, !is.na(age_group))“It is in playing and only in playing that the individual child or adult is able to be creative and to use the whole personality, and it is only in being creative that the individual discovers the self” (Winnicott, 1971, p. 54)
In few domains is Winnicott’s assessment more directly observable than in video games, played by billions worldwide across every possible combination of demographic groups such as age, gender, ethnicity, and neurodiversity. A substantial body of theory predicts that these demographic characteristics relate to play behavior. Motivational accounts, leisure-constraints research, role-incompatibility models, representation theory, and neurodiversity research each generate directional predictions about how volume, timing, genre, and social play should vary across demographic groups. In turn, these demographic variables are routinely used throughout video games research and practice: as moderators of outcomes ranging from technology acceptance to game preferences (González-González et al., 2022; Harnadi et al., 2025; Kordyaka et al., 2023; Zhang et al., 2021), as predictors of player types and problematic gaming (Liu, 2016; Lopez-Fernandez et al., 2019; Santos et al., 2025), and as guides for targeting and design decisions (Gerling et al., 2012; Kaufman et al., 2019).
Yet there is also broad agreement that demographic categories are relatively weak predictors of behavior (Norman, 2020; Yin et al., 2025). Variables such as player motivations, preferences, situational constraints, and prior behavior generally explain substantially more variation than age, gender, or ethnicity. In this sense, the field has largely moved beyond strong demographic determinism.
The problem is that this consensus rests on surprisingly limited evidence. A predictor can be weak on average while still carrying meaningful information in particular behavioral domains, for particular groups, or at particular levels of analysis. However, existing evidence provides little basis for determining where demographic differences are practically consequential and where they are negligible (Kirk, 1996; Vornhagen et al., 2020). Most available evidence relies on self-report data with well-understood limitations (Choi et al., 2023; Kahn et al., 2014; e.g., Parry et al., 2021), highly aggregated industry statistics (e.g., Entertainment Software Association, 2025), and/or homogenous samples (e.g., Larrieu et al., 2023). As a result, demographic characteristics are often treated simultaneously as important enough to include in theories, models, and design decisions, yet insufficiently informative to warrant close scrutiny.
One of the principal reasons for this ambiguity is the lack of fine-grained behavioral data from diverse populations. Opportunities to observe how people actually play—at scale, across platforms, and across the demographic breadth implicated by existing theories—have largely been restricted to private industry datasets. Consequently, the field has been unable to characterize the size, distribution, and practical significance of demographic differences with much precision. The key question is therefore not whether demographics matter more than psychographics or behavior, but rather what the demographic signal that does exist actually looks like when observed directly.
Digital trace data, defined as behavioral logs automatically collected by digital devices and online platforms, offers a complementary lens (Freelon, 2014; Stier et al., 2020). By observing play directly, trace data sidesteps the recall bias and social desirability effects inherent in self-report, while capturing dimensions of play (such as hour-by-hour temporal patterns or engagement span across titles) that surveys cannot feasibly measure.
This study does not relitigate whether demographic or psychographic segmentation better predicts play; that comparison is not in dispute. It asks instead what the demographic signal that does exist looks like in naturalistic, multi-platform behaviour: where it is concentrated, how large it is relative to within-group variation, and how far the resulting group-level differences license claims about individuals. By leveraging the Open Play dataset (Ballou et al., 2025), a public dataset of 3768 quasi-representative US and UK 18-40 year olds with digital trace data from five gaming platforms (Xbox, Nintendo Switch, Steam, iOS and Android), the current study:
Frequent reports, often industry-led, document the changing demographics of people who play games. In the US, players are now almost equally split by gender, are slightly more likely to be white compared to national averages, and 28% are over age 50 (Entertainment Software Association, 2025), illustrating the durability of gaming across the lifespan. Among teenagers, the demographics most likely to play are early adolescents (13–14), boys, Black youth, and those from lower-income households (Gottfried & Sidoti, 2024). Among US adolescents with mental health difficulties, gaming was relatively popular among Black females and Asian males (Carson et al., 2012).
These contemporary findings contrast with early work characterizing video game players as predominantly male, competitive, and academically high-performing (McClure, 1985; McClure & Mears, 1984), a portrait that reflected both the demographics of early gaming and the narrow samples used to study them. More recent work has demonstrated the value of large-scale open data for illuminating within- and between-group heterogeneity, as in the case of board game preferences (Cross et al., 2023).
By contrast, the focus of the current work is not who does or does not play games, which would require a different approach foregrounding nationally representative sampling, but rather how play patterns differ among those who do.
Equally interesting is not who plays games at all, but how, among video game players, subgroups differ. Complementing a large body of work on player types and motivations (Hughes & Cairns, 2020; Yee, 2006), a range of studies have examined how demographic characteristics relate to specific dimensions of play.
Age, gender, and education level have also been weakly linked to player type archetypes such as “achiever” and “disrupter,” though they appear unrelated to self-reported play frequency (Santos et al., 2025).
Survey-based motivational research established that men and women differ in why they seek out games — men more drawn to competition and achievement, women to social interaction and narrative — and that these motivational profiles shift with age and correlate with genre preferences (Lucas & Sherry, 2004; Phan et al., 2012; Yee, 2006).
role incompatibility accounts predict gaming volume and timing should shift as adult responsibilities accumulate with age (Ream et al., 2013)
Industry data similarly indicate that older players (Gen X and above) gravitate toward ‘Skill & Chance’ games and away from action titles, compared to younger cohorts (Entertainment Software Association, 2025).
representation research raises the possibility that genre preferences vary by ethnicity, given the persistent overrepresentation of White male protagonists and the concentration of Black and Latino characters in sports titles (Jones et al., 2025; Williams, Martins, et al., 2009).
Leisure constraints research suggests women’s play should be more temporally fragmented due to differential discretionary time (Winn & Heeter, 2009)
With regard to genre preferences, survey research has found that male gamers are more likely to play Strategy, Role Playing, Action, and Fighting genres, whereas female gamers are more likely to play Social, Puzzle/Card, Music/Dance, and Simulation games (Phan et al., 2012; Tondello & Nacke, 2019).
A smaller number of studies have used behavioral trace data rather than self-report, and their findings sometimes diverge from survey-based accounts. Early analyses of EverQuest II trace data found that playtime did not differ by ethnic background, religious affiliation, or income (Williams et al., 2008), but that women played approximately 15% more hours per week than men, were more motivated by social factors, and were more likely to under-report their playtime relative to logged data (Williams, Consalvo, et al., 2009). An analysis of League of Legends data showed that women had fewer matches played but accrued skill at the same rate, and were more likely to play support roles (Ratan et al., 2015).
Platform choice also varies: teenage boys are more likely to play on console, while smartphones and tablets make up a larger share of teenage girls’ play (Gottfried & Sidoti, 2024).
Beyond games, analyses of time-use diaries have shown that time spent playing video games is highly sensitive to total available leisure time for younger men, but not for younger women or older men (Aguiar et al., 2017).
For neurodiverse players, hyperfocus accounts predict longer, harder-to-interrupt sessions among those with ADHD, while special interest and sensory profiles suggest genre concentration among autistic players (Mazurek et al., 2015; Millington et al., 2022)
Studies of neurodiverse players have found a preference for RPGs among adults with autism (Mazurek et al., 2015), as well as higher motivations for escapism, completionism, and customisation (Millington et al., 2022). Adolescents with ADHD tend to have higher playtime and problematic gaming (Isorna Folgar et al., 2024).
Researchers have criticized the games HCI literature on neurodiversity for being prescriptive or focusing on “cures” rather than understanding the diversity of play experiences and preferences among neurodiverse players, and specifically call for more comparisons between neurotypical and neurodiverse behavioral patterns (Spiel & Gerling, 2021).
Taken together, the existing literature provides a partial but fragmented picture. Most findings derive from single platforms or single games, rely on self-report, or lack demographic diversity. The present study aims to complement this body of work by examining demographic variation in play across multiple platforms using observed behavioral data from a diverse sample, and by quantifying how much heterogeneity in play patterns is accounted for within vs between groups.
The present study thus attempts to improve the field’s understanding of demographic differences in play behavior by leveraging the Open Play dataset (Ballou et al., 2025). The Open Play dataset is a publicly available dataset of 3768 US and UK 18-40 year olds who contributed digital trace data from five gaming platforms (Xbox, Nintendo Switch, Steam, iOS and Android), who were screened using quasi-representative methods (with partial quotas for age, gender, and ethnicity). Analyses are guided by two research questions:
RQ1: How do the volume, composition, and temporal distribution of observed play vary across age, gender, ethnicity, and neurodiversity in this dataset?
RQ2: How much of the total variation in play behavior is attributable to between-group demographic differences versus within-group individual differences?
It is vital to be clear about what the study does not aim to do. Results here should be treated as pattern discovery and fodder for hypothesis generation, rather than population inference. I do not conduct statistical tests, and the present dataset is neither representative of the general population (who vary tremendously in their propensity to play games) nor of the gaming population (whose demographics remain only loosely understood, and who themselves vary in willingness to participate or share data). Nonetheless, the diversity present in the sample and the breadth of trace data allows for a more detailed look at naturalistic gaming behavior than available in many contemporary studies. Further, the data do not support claims of inherent preferences, or shed light on mechanisms—observed behavioral patterns may reflect structural, cultural, platform-level, or other factors.
# Calculate date range for each platform's telemetry
platform_ranges <- daily_telemetry |>
group_by(platform) |>
summarise(
min_date = min(day_local, na.rm = TRUE),
max_date = max(day_local, na.rm = TRUE),
.groups = "drop"
) |>
mutate(
range_text = glue(
"{format(min_date, '%b %Y')} to {format(max_date, '%b %Y')}"
)
)
# Format as a single inline-ready string
telemetry_range_sentence <- platform_ranges |>
mutate(platform_range = glue("{platform} ({range_text})")) |>
pull(platform_range) |>
paste(collapse = ", ")
# Calculate intake survey date range
intake_range <- intake |>
summarise(
min_date = min(date, na.rm = TRUE),
max_date = max(date, na.rm = TRUE)
)
intake_range_sentence <- glue(
"{format(intake_range$min_date, '%b %Y')} and {format(intake_range$max_date, '%b %Y')}"
)
games_per_player <- hourly_telemetry |>
group_by(pid) |>
summarise(
n_games = length(unique(title_id))
) |>
pull(n_games) |>
mean() |>
round(1)The data for this study comprise a subset of the data from the Open Play study (Ballou et al., 2025), version 1.2.1. In that study, participants provided access to automated records of their gaming history on one or more platforms (Xbox, Steam, Nintendo Switch, iOS, Android; Xbox is included for US participants only) and completed an intake survey followed by daily and biweekly surveys. The present study uses only the digital trace data from console and PC and demographic data from the intake survey, and does not include daily or biweekly survey data or mobile data (due to lower granularity compared to other trace data streams). Intake surveys were completed between Sep 2024 and Jan 2026. Digital trace data span the following periods: Nintendo (May 2022 to Oct 2025), Steam (Nov 2024 to Oct 2025), Xbox (Apr 2022 to Sep 2025).
Participants were recruited in collaboration with two panel providers, Prolific and PureProfile. Participants were eligible if they were aged 18 or older, resided in the United States or United Kingdom, self-reported playing video games for at least 1 hour per week with at least 50% of their play happening on eligible platforms (Nintendo, Xbox, and Steam), and successfully linked at least one gaming account on Xbox, Steam, and/or Nintendo Switch with validated recent digital trace data.
The procedure for linking gameplay data differed per platform: Steam was collected through a custom platform developed for research purposes, while Xbox and Nintendo data were collected via non-financial data-sharing agreements with the platform owners. An overview of the procedure from the participant perspective is shown in Appendix Table 3, while full details of the recruitment procedures and study methodology are available in (Ballou et al., 2025).
All data and analysis code are available on [repository link removed for anonymous review; anonymized supplementary materials uploaded to PCS].
A description of the sample is shown in Table 1.
Participants in the initial screening sample were quasi-representative; quotas ensured that those screened were approximately nationally representative according to age, gender, and ethnicity. However, the analytic sample is non-representative, as both prevalence of gaming (i.e., likelihood of qualifying for the study) and willingness to participate in the intensive study differed across demographic groups in the screening sample. Nonetheless, the analytic sample consists of a diverse sample across gender and ethnicity.
Particularly noteworthy is the neurodiversity of the sample: 23.8% of participants reported having an ADHD diagnosis, and 16.8% of participants reported having an autism spectrum disorder diagnosis (with 7.4% reporting both)—both far above national averages (see e.g., estimates that 6.0% of US adults have ADHD Staley et al., 2024; and 2.2% of US adults have autism Dietz et al., 2020). While such a high prevalence creates challenges for the generalizability of full-sample analyses, having this degree of diversity present in the sample allows for a more nuanced look at how play patterns vary across different neurotypes, rather than treating neurodivergent players as a monolithic group.
# Analytic sample: participants with trace data who have demographic info
# Sample sizes by country
n_total <- nrow(analytic_sample)
n_us <- sum(analytic_sample$country == "US")
n_uk <- sum(analytic_sample$country == "UK")
# Build the table
summary_table <- bind_rows(
# Sample size
make_table_row(
"**N**",
as.character(n_total),
as.character(n_us),
as.character(n_uk)
),
# Age
make_table_row(
"Age (years)",
format_mean_sd(analytic_sample$age),
format_mean_sd(analytic_sample$age[analytic_sample$country == "US"]),
format_mean_sd(analytic_sample$age[analytic_sample$country == "UK"])
),
# Gender
make_demo_rows(
analytic_sample,
"gender",
"Gender",
c("Man", "Woman", "Non-binary"),
n_total,
n_us,
n_uk
),
# Ethnicity
make_demo_rows(
analytic_sample,
"ethnicity",
"Ethnicity",
c("White", "Asian", "Black", "Multiple", "Other"),
n_total,
n_us,
n_uk
),
# Neurodiversity (non-exclusive, requires manual handling)
make_table_row("**Neurodiversity**", "", "", ""),
{
n_tot <- sum(analytic_sample$is_neurotypical, na.rm = TRUE)
n_us_val <- sum(
analytic_sample$is_neurotypical & analytic_sample$country == "US",
na.rm = TRUE
)
n_uk_val <- sum(
analytic_sample$is_neurotypical & analytic_sample$country == "UK",
na.rm = TRUE
)
make_table_row(
" Neurotypical",
format_n_pct(n_tot, n_total),
format_n_pct(n_us_val, n_us),
format_n_pct(n_uk_val, n_uk)
)
},
{
n_tot <- sum(analytic_sample$is_adhd, na.rm = TRUE)
n_us_val <- sum(
analytic_sample$is_adhd & analytic_sample$country == "US",
na.rm = TRUE
)
n_uk_val <- sum(
analytic_sample$is_adhd & analytic_sample$country == "UK",
na.rm = TRUE
)
make_table_row(
" ADHD",
format_n_pct(n_tot, n_total),
format_n_pct(n_us_val, n_us),
format_n_pct(n_uk_val, n_uk)
)
},
{
n_tot <- sum(analytic_sample$is_autism, na.rm = TRUE)
n_us_val <- sum(
analytic_sample$is_autism & analytic_sample$country == "US",
na.rm = TRUE
)
n_uk_val <- sum(
analytic_sample$is_autism & analytic_sample$country == "UK",
na.rm = TRUE
)
make_table_row(
" ASD",
format_n_pct(n_tot, n_total),
format_n_pct(n_us_val, n_us),
format_n_pct(n_uk_val, n_uk)
)
},
# Platform breakdown
make_table_row("**Platform**", "", "", ""),
{
platform_by_country <- daily_telemetry |>
distinct(pid, platform) |>
left_join(analytic_sample |> select(pid, country), by = "pid") |>
filter(!is.na(country))
map_dfr(c("Nintendo", "Steam", "Xbox"), function(plat) {
n_tot <- platform_by_country |> filter(platform == plat) |> nrow()
n_us_val <- platform_by_country |>
filter(platform == plat, country == "US") |>
nrow()
n_uk_val <- platform_by_country |>
filter(platform == plat, country == "UK") |>
nrow()
make_table_row(
glue(" {plat}"),
format_n_pct(n_tot, n_total),
format_n_pct(n_us_val, n_us),
format_n_pct(n_uk_val, n_uk)
)
})
}
)
# Identify rows for styling
header_rows <- which(str_detect(summary_table$Characteristic, "^\\*\\*"))
indent_rows <- which(str_detect(summary_table$Characteristic, "^ "))
# Create table
summary_table |>
tt(
notes = "Values are M (SD) or N (percent). Neurodiversity categories are non-exclusive."
) |>
format_tt(j = 1, markdown = TRUE) |>
style_tt(i = header_rows, bold = TRUE) |>
format_tt(i = header_rows, j = 1, fn = \(x) str_remove_all(x, "\\*\\*")) |>
style_tt(i = indent_rows, j = 1, indent = 1) |>
format_tt(i = indent_rows, j = 1, fn = \(x) str_trim(x)) |>
style_tt(fontsize = 0.8) |>
style_tt(i = 0, bold = TRUE)| Characteristic | Total | US | UK |
|---|---|---|---|
| Values are M (SD) or N (percent). Neurodiversity categories are non-exclusive. | |||
| N | 3768 | 2172 | 1596 |
| Age (years) | 27.1 (5.2) | 26.7 (5) | 27.5 (5.4) |
| Gender | |||
| Man | 2332 (61.9\%) | 1298 (59.8\%) | 1034 (64.8\%) |
| Woman | 1233 (32.7\%) | 740 (34.1\%) | 493 (30.9\%) |
| Non-binary | 198 (5.3\%) | 132 (6.1\%) | 66 (4.1\%) |
| Ethnicity | |||
| White | 2679 (71.1\%) | 1353 (62.3\%) | 1326 (83.1\%) |
| Asian | 323 (8.6\%) | 188 (8.7\%) | 135 (8.5\%) |
| Black | 250 (6.6\%) | 212 (9.8\%) | 38 (2.4\%) |
| Multiple | 377 (10\%) | 307 (14.1\%) | 70 (4.4\%) |
| Other | 129 (3.4\%) | 110 (5.1\%) | 19 (1.2\%) |
| Neurodiversity | |||
| Neurotypical | 2158 (57.3\%) | 1201 (55.3\%) | 957 (60\%) |
| ADHD | 898 (23.8\%) | 596 (27.4\%) | 302 (18.9\%) |
| ASD | 633 (16.8\%) | 345 (15.9\%) | 288 (18\%) |
| Platform | |||
| Nintendo | 1442 (38.3\%) | 789 (36.3\%) | 653 (40.9\%) |
| Steam | 2805 (74.4\%) | 1577 (72.6\%) | 1228 (76.9\%) |
| Xbox | 326 (8.7\%) | 326 (15\%) | 0 (0.0\%) |
The following demographic variables were measured in the intake survey.
Age: Participants entered their age as an integer. Because the multivariate visualizations used here (e.g., radar charts comparing genre profiles) require categorical groupings, continuous age is binned into four groups (18–24, 25–30, 31–35, 36–40) for comparability with the other demographic dimensions.
Gender: Participants selected from the following options: Woman, Man, Non-binary, Prefer to specify, and Prefer not to say. For readability, “non-binary” and “prefer to specify” were both coded as “Non-binary”.
Ethnicity: Response options were drawn from primary census categories in each respective country. US participants selected between White alone; Black or African American alone; American Indian and Alaska Native alone; Asian alone; Native Hawaiian and Other Pacific Islander alone; Some Other Race alone; and Two or More Races. UK participants selected among White; Mixed or multiple ethnic groups; Asian or Asian British; Black, African, Caribbean or Black British; Other ethnic group; Prefer not to say. For simplicity, I harmonized these categories into a smaller set of labels (e.g., “Black or African American alone” and “Black, African, Caribbean or Black British” were both recoded as “Black”).
Neurodiversity: Participants were asked if they had received a formal diagnosis of neurodiverse conditions from a qualified healthcare professional; if they selected yes, they were provided a list of 12 options. In this paper, I focus solely on players who reported having a diagnosis of either autism spectrum disorder or attention deficit hyperactive disorder, as prevalence of other categories (e.g., dyscalculia) was too small for meaningful analysis.
Gaming behavior on Xbox, Steam, and Nintendo Switch was measured via a mix of (a) session-level data provided by Nintendo of America, Nintendo of Europe, and Microsoft; and (b) hourly playtime collected using open source methods built on the Steam API. The exact data-sharing procedure and data collected varied by platform; details are available in Appendix Table 3, and are described in exhaustive detail in Ballou et al. (2025).
Because Xbox titles are replaced with a random identifier instead of the specific game, subsequent analyses in this paper do not focus on particular games, but rather on genres (as this metadata was provided alongside Xbox games). For reference, the most popular 5 non-Xbox games for each demographic group are shown in Table 4
From the hourly and session-level data, I calculated various summary variables including: total playtime (hours), session count, mean session duration, genre categories (how assigned to titles), title diversity, and hour of day and day of week distributions. These derived variables form the basis of the descriptions to come.
Nintendo and Steam data contain full game titles; to collect game metadata including genres, we used the Internet Games Database (IGDB) API. Xbox data was provided using random identifiers in place of game titles, but with genre labels as seen on the Xbox store. For simplicity, I therefore harmonized IGDB and Xbox genres into a smaller subset of categories (e.g., turn-based strategy, real-time strategy, tactical, MOBA were collapsed into a single “Strategy” category). Full details of the genre mapping are available in the supplementary materials. For the primary genre analysis, each game was assigned to its first listed genre, typically the primary genre; a supplementary multi-genre analysis (Appendix Figure 5) apportioned playtime equally across all genres a game was tagged with and largely replicated the findings.
This study received ethical approval from [redacted for anonymous peer review]. All participants provided informed consent at the start of the study, including consent to their data being shared openly for reanalysis.
Participants were paid at an average rate of £12/hour, equating to: £0.20 for a 1-minute screening, £2 for the 10-minute intake survey (plus £5 for linking at least one account with recent data), £0.80 for each 4-minute daily survey. Participants received a £10 bonus payment for completing at least 24 out of 30 daily surveys.
Results below first depict the behavioral patterns observed in play volume, engagement patterns and temporal organization, and genre composition, before quantifying total explanatory power.
# Calculate per-person session metrics from sessions_telemetry (Nintendo + Xbox only)
person_sessions <- sessions_telemetry |>
group_by(pid) |>
summarise(
n_sessions = n(),
median_duration = median(duration_min, na.rm = TRUE),
.groups = "drop"
) |>
left_join(demographics, by = "pid") |>
filter(!is.na(age_group))
# Combined color palette
all_demo_colors <- c(colors_age, colors_gender, colors_ethnicity, colors_neuro)
# Helper to create a single distribution panel
make_volume_panel <- function(
data,
x_var,
x_label,
x_scale = "identity",
x_lim = NULL
) {
p <- data |>
ggplot(aes(x = .data[[x_var]], y = group, fill = group)) +
stat_slabinterval(
slab_alpha = 0.7,
point_interval = median_qi,
interval_color = "black",
point_color = "black",
point_size = 1.5,
scale = 1.2,
.width = c(0.66, 0.95)
) +
scale_fill_manual(values = all_demo_colors) +
facet_wrap(~demographic, scales = "free_y", nrow = 1) +
labs(x = x_label, y = NULL) +
theme(
legend.position = "none",
strip.text = element_text(
size = 8,
color = "white",
margin = margin(t = 2, b = 2)
),
strip.background = element_rect(fill = "black"),
axis.text.y = element_text(size = 8),
axis.text.x = element_text(size = 8, angle = 45, hjust = 1),
panel.spacing.x = unit(8, "pt"),
panel.spacing.y = unit(2, "pt")
)
if (x_scale == "log10") {
p <- p + scale_x_log10(labels = scales::comma_format())
}
if (!is.null(x_lim)) {
p <- p + coord_cartesian(xlim = x_lim)
}
p
}
# Panel A: Weekly hours (from person_data_long)
p_weekly <- make_volume_panel(
person_data_long,
"weekly_hours",
"Mean weekly playtime (hours)",
x_lim = c(0, 40)
)
# Prepare sessions data in long form
sessions_long_basic <- person_sessions |>
pivot_longer(
cols = c(age_group, gender, ethnicity),
names_to = "demographic",
values_to = "group"
) |>
filter(!is.na(group)) |>
select(pid, n_sessions, median_duration, demographic, group)
sessions_long_neuro <- person_sessions |>
pivot_longer(
cols = c(is_neurotypical, is_adhd, is_autism),
names_to = "neuro_type",
values_to = "has_condition"
) |>
filter(has_condition == TRUE) |>
mutate(
demographic = "neurodiversity",
group = case_when(
neuro_type == "is_neurotypical" ~ "Neurotypical",
neuro_type == "is_adhd" ~ "ADHD",
neuro_type == "is_autism" ~ "ASD"
)
) |>
select(pid, n_sessions, median_duration, demographic, group)
sessions_long <- bind_rows(sessions_long_basic, sessions_long_neuro) |>
mutate(
demographic = factor(
demographic,
levels = c("age_group", "gender", "ethnicity", "neurodiversity"),
labels = c("Age", "Gender", "Ethnicity", "Neurodiversity")
)
)
# Panel B: Session count
p_sessions <- make_volume_panel(
sessions_long,
"n_sessions",
"Total sessions (log scale)",
x_scale = "log10",
x_lim = c(10, 5000)
)
# Panel C: Session duration
p_duration <- make_volume_panel(
sessions_long,
"median_duration",
"Median session duration (minutes)",
x_lim = c(0, 120)
)
# Combine vertically with A/B/C labels
(p_weekly / p_sessions / p_duration) +
plot_layout(heights = c(1, 1, 1)) +
plot_annotation(tag_levels = "A") &
theme(
plot.tag = element_text(face = "bold", size = 12),
plot.margin = margin(2, 2, 2, 2, "pt")
)
# Gender comparisons (weekly hours)
gender_hours <- person_data_long |>
filter(demographic == "Gender") |>
group_by(group) |>
summarise(median_hours = median(weekly_hours, na.rm = TRUE), .groups = "drop")
hours_women <- gender_hours |> filter(group == "Woman") |> pull(median_hours)
hours_men <- gender_hours |> filter(group == "Man") |> pull(median_hours)
hours_women_fmt <- sprintf("%.1f", hours_women)
hours_men_fmt <- sprintf("%.1f", hours_men)
# Neurodiversity comparisons (sessions and duration)
neuro_sessions <- sessions_long |>
filter(demographic == "Neurodiversity") |>
group_by(group) |>
summarise(
median_sessions = median(n_sessions, na.rm = TRUE),
median_duration = median(median_duration, na.rm = TRUE),
.groups = "drop"
)
sessions_adhd <- neuro_sessions |>
filter(group == "ADHD") |>
pull(median_sessions)
sessions_neurotypical <- neuro_sessions |>
filter(group == "Neurotypical") |>
pull(median_sessions)
duration_adhd <- neuro_sessions |>
filter(group == "ADHD") |>
pull(median_duration)
duration_neurotypical <- neuro_sessions |>
filter(group == "Neurotypical") |>
pull(median_duration)
sessions_adhd_fmt <- scales::comma(round(sessions_adhd))
sessions_neurotypical_fmt <- scales::comma(round(sessions_neurotypical))
duration_adhd_fmt <- sprintf("%.0f", duration_adhd)
duration_neurotypical_fmt <- sprintf("%.0f", duration_neurotypical)Figure 1 visualizes the typical weekly playtime (Panel A, top row), total recorded sessions (Panel B, middle row), and typical session duration (Panel C, bottom row) for each group. The heavily overlapping histograms across groups indicate that group differences account for very little variation in playtime: in the sample, most groups showed similar distributions of play volume.
A few trends nonetheless emerge: women played fewer weekly hours than men (5.2 vs 8.1 mean hours). There is a small observed difference in the total number of sessions played by participants with ADHD compared to neurotypical players (169 vs 128), but no difference in median session duration (31 vs 31 minutes). Asian players in the sample had slightly lower playtime and sessions than other ethnic groups.
# Compute hourly playtime proportions for all demographic groups
hourly_demo <- hourly_telemetry |>
mutate(hour = hour(hour_start_local)) |>
left_join(demographics, by = "pid") |>
filter(!is.na(age_group))
# Exclusive demographics
hourly_long <- hourly_demo |>
pivot_longer(
cols = c(age_group, gender, ethnicity),
names_to = "demographic",
values_to = "group"
) |>
filter(!is.na(group)) |>
select(pid, hour, minutes, demographic, group) |>
bind_rows(
hourly_demo |>
pivot_longer(
cols = c(is_neurotypical, is_adhd, is_autism),
names_to = "neuro_type",
values_to = "has_condition"
) |>
filter(has_condition == TRUE) |>
mutate(
demographic = "neurodiversity",
group = case_when(
neuro_type == "is_neurotypical" ~ "Neurotypical",
neuro_type == "is_adhd" ~ "ADHD",
neuro_type == "is_autism" ~ "ASD"
)
) |>
select(pid, hour, minutes, demographic, group)
) |>
mutate(
demographic = factor(
demographic,
levels = c("age_group", "gender", "ethnicity", "neurodiversity"),
labels = c("Age", "Gender", "Ethnicity", "Neurodiversity")
)
) |>
filter(!is.na(demographic))
# Proportion of each group's total playtime per hour
hourly_props <- hourly_long |>
group_by(demographic, group, hour) |>
summarise(total_minutes = sum(minutes, na.rm = TRUE), .groups = "drop") |>
group_by(demographic, group) |>
mutate(prop = total_minutes / sum(total_minutes)) |>
ungroup()
# Order legend entries by demographic group
all_demo_colors <- c(colors_age, colors_gender, colors_ethnicity, colors_neuro)
group_levels <- names(all_demo_colors)
hourly_props <- hourly_props |>
mutate(group = factor(group, levels = group_levels))
ggplot(hourly_props, aes(x = hour, y = prop, colour = group, group = group)) +
geom_line(linewidth = 0.7, alpha = 0.85) +
scale_x_continuous(
breaks = seq(0, 23, by = 3),
labels = c("12am", "3am", "6am", "9am", "12pm", "3pm", "6pm", "9pm")
) +
scale_y_continuous(labels = scales::percent_format()) +
scale_colour_manual(values = all_demo_colors, breaks = group_levels) +
facet_wrap(~demographic, nrow = 2) +
labs(x = "Hour of day", y = "Share of playtime", colour = NULL) +
theme_minimal(base_size = 10) +
theme(
legend.position = "bottom",
legend.text = element_text(size = 8),
strip.background = element_rect(fill = "black", colour = "black"),
strip.text = element_text(colour = "white", face = "bold", size = 9),
panel.border = element_rect(colour = "grey70", fill = NA, linewidth = 0.4),
panel.grid.minor = element_blank(),
panel.spacing = unit(10, "pt")
) +
guides(colour = guide_legend(ncol = 4))
Next, I assessed how the time of play differs across demographic groups in the sample. For each user, I calculated the proportion of total playtime taking place in each of 24 hourly bins, after converting session timestamps to local timezones. Figure 2 shows the resulting distributions for each demographic variable.
Results show that across all age groups, play peaks in the evening hours, with 9pm being the modal hour for 18–35 year olds. Playtime among 36–40 year-olds shifts slightly earlier, peaking at 8pm. Gender and ethnicity groups show broadly similar temporal distributions, with evening peaks across all groups. Among neurodiversity groups, the distributions are also similar, though ADHD and ASD groups show slightly more concentrated evening peaks.
# -----------------------------------------------------------------------------
# PRIMARY GENRE MAPPING (first-listed genre only)
# -----------------------------------------------------------------------------
games_genres_primary <- games |>
mutate(
genre_raw = str_extract(genres, "^[^,]+") |> str_trim(),
genre_clean = clean_genre(genre_raw)
) |>
filter(!is.na(genre_clean)) |>
distinct(original_name, genre_clean)
# -----------------------------------------------------------------------------
# MULTI-GENRE MAPPING (games assigned to all listed genres, time apportioned)
# -----------------------------------------------------------------------------
games_genres_multi <- games |>
filter(!is.na(genres)) |>
separate_rows(genres, sep = ",\\s*") |>
mutate(genre_clean = clean_genre(genres)) |>
filter(!is.na(genre_clean)) |>
distinct(original_name, genre_clean) |>
group_by(original_name) |>
mutate(genre_weight = 1 / n()) |>
ungroup()
# -----------------------------------------------------------------------------
# PRIMARY GENRE DATA (for main bar chart)
# -----------------------------------------------------------------------------
genre_playtime_primary <- hourly_telemetry |>
left_join(games_genres_primary, by = c("title_id" = "original_name")) |>
filter(!is.na(genre_clean)) |>
left_join(demographics, by = "pid") |>
filter(!is.na(age_group))
genre_by_demo_primary <- genre_playtime_primary |>
group_by(
pid,
genre_clean,
age_group,
gender,
ethnicity,
is_neurotypical,
is_adhd,
is_autism
) |>
summarise(minutes = sum(minutes, na.rm = TRUE), .groups = "drop")
# Abbreviate long genre names for display
abbreviate_genre <- function(x) {
case_when(
x == "Role-playing (RPG)" ~ "RPG",
x == "Simulator" ~ "Simulation",
TRUE ~ x
)
}
# Top genres by total playtime (used for both versions)
top_genres <- genre_by_demo_primary |>
group_by(genre_clean) |>
summarise(total = sum(minutes), .groups = "drop") |>
slice_max(total, n = 8) |>
mutate(genre_clean = abbreviate_genre(genre_clean)) |>
pull(genre_clean)
# Apply abbreviation to base data before computing proportions
genre_by_demo_primary <- genre_by_demo_primary |>
mutate(genre_clean = abbreviate_genre(genre_clean))
genre_props_primary <- build_genre_props(genre_by_demo_primary)
# Calculate deviation on ALL genres first, then filter for display
genre_props_dev_primary <- calc_genre_deviation(
genre_by_demo_primary,
genre_props_primary
)
genre_props_dev_plot_primary <- genre_props_dev_primary |>
filter(genre_clean %in% top_genres) |>
mutate(genre_clean = factor(genre_clean, levels = top_genres))
group_ns_primary <- calc_group_ns(genre_by_demo_primary)
# -----------------------------------------------------------------------------
# MULTI-GENRE DATA (for appendix bar chart)
# Playtime is apportioned equally across all genres a game belongs to
# -----------------------------------------------------------------------------
genre_playtime_multi <- hourly_telemetry |>
left_join(
games_genres_multi,
by = c("title_id" = "original_name"),
relationship = "many-to-many"
) |>
filter(!is.na(genre_clean)) |>
# Apply weight to apportion playtime across genres
mutate(minutes_weighted = minutes * genre_weight) |>
left_join(demographics, by = "pid") |>
filter(!is.na(age_group))
genre_by_demo_multi <- genre_playtime_multi |>
group_by(
pid,
genre_clean,
age_group,
gender,
ethnicity,
is_neurotypical,
is_adhd,
is_autism
) |>
summarise(minutes = sum(minutes_weighted, na.rm = TRUE), .groups = "drop") |>
# Apply abbreviation to base data before computing proportions
mutate(genre_clean = abbreviate_genre(genre_clean))
genre_props_multi <- build_genre_props(genre_by_demo_multi)
# Calculate deviation on ALL genres first, then filter for display
# (filtering before deviation calculation breaks leave-one-out math)
genre_props_dev_multi <- calc_genre_deviation(
genre_by_demo_multi,
genre_props_multi
)
genre_props_dev_plot_multi <- genre_props_dev_multi |>
filter(genre_clean %in% top_genres) |>
mutate(genre_clean = factor(genre_clean, levels = top_genres))
group_ns_multi <- calc_group_ns(genre_by_demo_multi)
# Bootstrap 95% CIs for deviation ratios (resamples both focal and reference groups)
genre_ci_primary <- bootstrap_genre_ci(genre_by_demo_primary, n_boot = 1000)
genre_ci_multi <- bootstrap_genre_ci(genre_by_demo_multi, n_boot = 1000)# Flatten per-group colors (unlist adds prefixes like "Age.18-24")
group_colors <- unlist(demo_colors)
names(group_colors) <- sub(".*\\.", "", names(group_colors))
build_genre_bar_grid(
genre_props_dev_plot_primary,
genre_ci_primary,
group_ns_primary,
group_colors
)
# Count unique raw genres before simplification
n_raw_genres <- games |>
filter(!is.na(genres)) |>
separate_rows(genres, sep = ",\\s*") |>
distinct(genres) |>
nrow()To visualize differences in genre preferences, I calculated the sum of each user’s playtime taking place in each of the 8 genres within the simplified genre taxonomy described in the measure section (raw play proportions for all 23 genres in the unsimplified data can be found in Appendix Table 5).
Results (Figure 3) visualizes these results. Each demographic group is a panel (demographic variables organized by row). Bars extending to the left from the center indicate that this genre is relatively unpopular among that demographic group compared to the other subgroups (e.g., popular among men compared to women and non-binary participants), whereas bars extending to the right indicate that the genre is relatively popular.
A wide variety of observed differences emerge. Among other differences, results in the present sample align with well-documented preferences among men for sports games, and among women for puzzle and simulation games. Asian players in the sample played relatively high amounts of racing, platform, and sports games, whereas White players played slightly more puzzle games than other ethnic groups. Neurodiverse players had slight preference for RPGs compared to neurotypical players, who played more racing games.
The preceding analyses reveal differences in central tendency across demographic groups, but these visualizations can obscure how much of the total variation in play behavior lies between groups versus within groups. If between-group variance is small relative to within-group variance, demographic categories explain little of the overall heterogeneity in how people play—even if group means differ noticeably.
To quantify how much of the variation in play behavior is attributable to demographic categories versus individual differences, I calculated the proportion of total variance in an outcome that lies between groups rather than within groups, computed as SS_between / SS_total. For the combined estimate, I fit a multiple linear regression predicting each outcome from all four demographic variables simultaneously (age group, gender, ethnicity, and neurodiversity) and extracted \(R^2\), which represents the total variance explained by the full demographic profile. Values near zero indicate that demographic categories explain little of the overall heterogeneity in play behavior, even when group means differ noticeably.
# Genre diversity (Shannon entropy) per person
genre_diversity <- genre_playtime_primary |>
group_by(pid, genre_clean) |>
summarise(minutes = sum(minutes, na.rm = TRUE), .groups = "drop") |>
group_by(pid) |>
mutate(prop = minutes / sum(minutes)) |>
summarise(
genre_entropy = -sum(prop * log(prop + 1e-10)),
n_genres = n_distinct(genre_clean),
.groups = "drop"
)
# Person-level dataset with all outcomes
variance_data <- person_data |>
left_join(
person_sessions |> select(pid, n_sessions, median_duration),
by = "pid"
) |>
left_join(
mode_playtime |> select(pid, pct_competitive, pct_cooperative),
by = "pid"
) |>
left_join(genre_diversity, by = "pid") |>
mutate(
neuro_simple = case_when(
is_neurotypical ~ "Neurotypical",
is_adhd | is_autism ~ "Neurodiverse",
TRUE ~ NA_character_
)
)
# Outcomes and demographics
outcomes <- tribble(
~var , ~label , ~category ,
"weekly_hours" , "Weekly hours" , "Volume" ,
"n_sessions" , "Session count" , "Volume" ,
"median_duration" , "Session duration" , "Volume" ,
"genre_entropy" , "Genre diversity" , "Composition" ,
"n_genres" , "Genres played" , "Composition" ,
"pct_competitive" , "Competitive multi. \\%" , "Modes" ,
"pct_cooperative" , "Cooperative \\%" , "Modes"
)
demo_vars <- c(
age_group = "Age",
gender = "Gender",
ethnicity = "Ethnicity",
neuro_simple = "Neuro"
)
# Eta-squared for each outcome × demographic via effectsize
eta_results <- expand_grid(
outcome_idx = seq_len(nrow(outcomes)),
demo_var = names(demo_vars)
) |>
pmap_dfr(function(outcome_idx, demo_var) {
d <- variance_data |>
select(y = all_of(outcomes$var[outcome_idx]), g = all_of(demo_var)) |>
filter(!is.na(y), !is.na(g))
if (nrow(d) < 10 || n_distinct(d$g) < 2) {
return(NULL)
}
eta <- eta_squared(aov(y ~ g, data = d), verbose = FALSE)
tibble(
outcome = outcomes$label[outcome_idx],
category = outcomes$category[outcome_idx],
demographic = demo_vars[demo_var],
eta_sq = eta$Eta2
)
})
# Combined R² (all demographics in one model)
combined_results <- map_dfr(seq_len(nrow(outcomes)), function(i) {
d <- variance_data |>
select(y = all_of(outcomes$var[i]), all_of(names(demo_vars))) |>
filter(if_all(everything(), ~ !is.na(.)))
if (nrow(d) < 10) {
return(NULL)
}
f <- as.formula(glue("y ~ {paste(names(demo_vars), collapse = ' + ')}"))
tibble(
outcome = outcomes$label[i],
category = outcomes$category[i],
demographic = "Combined",
eta_sq = summary(lm(f, data = d))$r.squared
)
})
# Steelman: splines on age + all two-way interactions
steelman_results <- map_dfr(seq_len(nrow(outcomes)), function(i) {
d <- variance_data |>
select(y = all_of(outcomes$var[i]), age, gender, ethnicity, neuro_simple) |>
filter(if_all(everything(), ~ !is.na(.)))
if (nrow(d) < 50) {
return(NULL)
}
fit <- lm(
y ~ splines::ns(age, 4) *
gender +
splines::ns(age, 4) * ethnicity +
splines::ns(age, 4) * neuro_simple +
gender * ethnicity +
gender * neuro_simple +
ethnicity * neuro_simple,
data = d
)
tibble(outcome = outcomes$label[i], steelman_r2 = summary(fit)$r.squared)
})
max_steelman_r2_pct <- round(100 * max(steelman_results$steelman_r2), 1)
# Format table
fmt_pct <- \(x) str_replace(scales::percent(x, accuracy = 0.1), "%", "\\\\%")
eta_wide <- bind_rows(eta_results, combined_results) |>
mutate(eta_pct = fmt_pct(eta_sq)) |>
select(category, outcome, demographic, eta_pct) |>
pivot_wider(names_from = demographic, values_from = eta_pct) |>
rename(`Combined (linear)` = Combined) |>
left_join(
steelman_results |>
mutate(`Combined (complex)` = fmt_pct(steelman_r2)) |>
select(outcome, `Combined (complex)`),
by = "outcome"
)
# Build table with category header rows programmatically
categories <- c("Volume", "Composition", "Modes")
col_names <- c(
"Outcome",
"Age",
"Gender",
"Ethnicity",
"Neuro",
"Combined (linear)",
"Combined (complex)"
)
empty_row <- \(label) {
set_names(
as.list(c(label, rep("", length(col_names) - 1))),
col_names
) |>
as_tibble()
}
eta_table <- map_dfr(categories, function(cat) {
rows <- eta_wide |>
filter(category == cat) |>
select(
Outcome = outcome,
Age,
Gender,
Ethnicity,
Neuro,
`Combined (linear)`,
`Combined (complex)`
)
bind_rows(empty_row(cat), rows)
})
header_rows <- which(eta_table$Age == "")
eta_table |>
tt(
notes = "Combined (linear) = R² from multiple regression with all demographic main effects. Combined (complex) = R² with natural splines on age and all two-way interactions. Neuro = Neurotypical vs. Neurodiverse."
) |>
style_tt(i = header_rows, bold = TRUE) |>
style_tt(fontsize = 0.85) |>
style_tt(i = 0, bold = TRUE)
# Inline variables
max_combined_r2_pct <- round(
100 * max(combined_results$eta_sq, na.rm = TRUE),
1
)
max_single_eta_pct <- round(
100 * max(eta_results$eta_sq, na.rm = TRUE),
1
)| Outcome | Age | Gender | Ethnicity | Neuro | Combined (linear) | Combined (complex) |
|---|---|---|---|---|---|---|
| Combined (linear) = R² from multiple regression with all demographic main effects. Combined (complex) = R² with natural splines on age and all two-way interactions. Neuro = Neurotypical vs. Neurodiverse. | ||||||
| Volume | ||||||
| Weekly hours | 0.1\% | 2.7\% | 0.3\% | 0.2\% | 3.5\% | 5.0\% |
| Session count | 0.6\% | 2.5\% | 0.4\% | 0.0\% | 3.2\% | 5.4\% |
| Session duration | 0.5\% | 0.0\% | 0.3\% | 0.0\% | 0.9\% | 4.6\% |
| Composition | ||||||
| Genre diversity | 0.2\% | 0.8\% | 0.5\% | 0.6\% | 2.2\% | 3.4\% |
| Genres played | 0.4\% | 1.2\% | 0.6\% | 1.1\% | 3.3\% | 4.8\% |
| Modes | ||||||
| Competitive multi. \% | 2.1\% | 0.2\% | 0.1\% | 0.1\% | 2.6\% | 3.8\% |
| Cooperative \% | 2.2\% | 0.9\% | 0.1\% | 0.0\% | 3.1\% | 4.1\% |
Table 2 shows that across all outcomes and demographic dimensions, the vast majority of variance in play behavior is within-group rather than between-group. Demographic categories explain less than 3% of total variance in every case, with most values below 1%. Even in a more parameterized specification with flexible age effects and all two-way interactions, the maximum variance explained was 5.4%.
In short, individuals within the same demographic group differ from one another far more than group averages differ from each other. This pattern holds across play volume, genre composition, and game mode outcomes, reinforcing that demographic labels capture only a small slice of the heterogeneity in how people play.
# Per-person shooter proportion
shooter_by_person <- genre_by_demo_primary |>
group_by(pid, gender) |>
summarise(
shooter_min = sum(minutes[genre_clean == "Shooter"], na.rm = TRUE),
total_min = sum(minutes, na.rm = TRUE),
.groups = "drop"
) |>
mutate(shooter_pct = 100 * shooter_min / total_min)
men_shooter_pct <- round(
mean(shooter_by_person$shooter_pct[shooter_by_person$gender == "Man"]),
1
)
women_shooter_pct <- round(
mean(shooter_by_person$shooter_pct[shooter_by_person$gender == "Woman"]),
1
)
women_above_result <- sum(
shooter_by_person$shooter_pct[shooter_by_person$gender == "Woman"] >
men_shooter_pct
)The results presented here show a variety of trends among demographic groups in a diverse sample of UK and US adult video game players. Many these have been documented in prior survey-based research, such as the prevalence of sports game play among men and simulation game play among women (Phan et al., 2012) or the preference for RPGs among adults with autism (Mazurek et al., 2015). Other patterns are intuitive and have been theorized, but rarely directly observed and quantified in naturalistic behavioral data, such as the earlier peak play hour for older players (Ream et al., 2013).
However, despite the presence of these trends, the overall picture is one of substantial heterogeneity within groups, and thus overlap between them. For every observed difference, there are many individuals in the “opposite” group who show the same behavior. For example, although there is a marked difference in the average proportion of time men and women in the sample spent playing games in the shooter genre (NA% vs NA% on average), there are still NA women who have a higher shooter proportion than the average man. Results showing that effectively all permutations of play volume, timing and genre allocation are present in all demographic groups serves as a form of counter-stereotypical examples, a method recommended for reducing and reversing implicit biases in games culture (Flanagan & Kaufman, 2017).
In other words: while trends emerge at a bird’s eye view, knowing an individual’s complete demographic profile tells us remarkably little about their actual play behavior. Even with all demographic variables combined, a researcher could explain at most 5.4% of the variance in any single play outcome (Table 2). These findings echo early trace data studies showing demographic categories explain minimal variance in play patterns (Williams et al., 2008), but extend them to contemporary multi-platform contexts and provide explicit variance decomposition.
It is vital to interpret these results with care, and to avoid overgeneralization or stereotyping. The observed differences do not necessarily reflect stable preferences or inherent traits of demographic groups, but rather patterns of behavior that emerge from a complex interplay of structural factors, cultural contexts, and individual circumstances. For example, differences in temporal play patterns may reflect differences in work schedules (e.g., Lee & Chen, 2023), caregiving responsibilities (e.g., Wang et al., 2018), accessibility concerns (Porter & Kientz, 2013), or availability of gaming platforms (e.g., Ha & Kim, 2024), rather than intrinsic preferences for when to play. Similarly, genre preferences may be shaped by factors such as marketing, social norms, or peer influence, rather than inherent tastes. For example, it is long-established that White adult male characters are over-represented among video game characters, while Black female characters and many other groups are under-represented (Jones et al., 2025; Williams, Martins, et al., 2009; cf. Gardner & Tanenbaum, 2018), which may contribute to the observed differences in genre allocation.
With those caveats in mind, this work still offers several contributions for the field: it offers a foundation for hypothesis generation, theory specification, and study design; highlights behavioral dimensions such as temporal patterns or genre diversity that may be more informative outcomes than total playtime; and points to the value of behavioral trace data in revealing how people actually play, rather than what they say about their play.
These findings complicate how demographic categories should function in games HCI theory. When within-group variance is at least 18x larger than between-group variance, demographics may be valuable as contextual background variables but are unlikely to be useful as primary predictors of behavioral outcomes. Such empirical backing reinforces known challenges for theory, quantitative methods, and design.
On the theory side, I argue for improved specification of the specific mechanisms through which demographics matter rather than treating them as proxies for preferences. Although there is widespread recognition of the pitfalls of demographic essentialism at the theory level, empirical studies often use and interpret demographics are key predictors of outcomes such as motivational styles or player types (Yee, 2006), esports participation (Kordyaka et al., 2023), or internet gaming disorder (Lopez-Fernandez et al., 2019). Such use of demographic variables may serve as a proxy for preferences or behaviors, masking the underlying mechanistic differences on which the field could actually intervene. Future work with representative samples and/or qualitative methods can help unpack which of these trends generalize to other metrics of real-world play, and why they occur (access? cultural exposure? time constraints?).
Eventually, such work may allow mechanism-related variables to supplement demographics themselves in quantitative studies. Results here show that the use of demographics variables in statistical models should be carefully considered. Researchers should report and be mindful of effect sizes, and ensure that statistical significance of a particular demographic variable is not conflated with the practical significance thereof (Kirk, 1996; Vornhagen et al., 2020).
With regard to design, these results suggest that demographic categories may be useful for coarse segmentation, but heterogeneity will often be too large to reliably base more granular decisions such as design for diverse groups such as women or older players (Gerling et al., 2012; Kaufman et al., 2019). Behavioral segmentation based on characteristics like genre allocation and temporal play patterns, a well-established industry practice (Norman, 2020; Yin et al., 2025), offers a valuable alternative for many design or intervention decisions.
To reiterate, low predictive strength does not mean demographics have no value for design or analytics. Observed differences in temporal organization and engagement span suggest that features such as session length expectations, save mechanics, or live-service timing may differentially fit players with distinct demographic constraints (Figure 2). Similarly, genre portfolio patterns can inform decisions about cross-promotion, onboarding pathways, and content diversification strategies.
As highlighted throughout this piece, these data do not provide strong evidence for generalizable differences between groups. I did not apply weighting or conduct hypothesis tests. Observed differences should not be interpreted as stable preferences or group traits, as these data cannot meaningfully distinguish access effects from preference effects.
The gameplay behavior observed here is wide but shallow - while the data captured individuals’ play across a wide variety of titles (mean: 19.6 distinct titles per player), it does not capture their behavior within particular games. To advance knowledge on various topics typically studied using self-reports or in lab settings, deep game-level is needed instead: for example, avatar selection data to study the Proteus Effect (Yee & Bailenson, 2007), performance data to study skill acquisition (Ratan et al., 2015), and communications and friends data to understand gender differences in social behavior (Wilhelm, 2018). Achieving such advances will require more widespread adoption of the full trace data collection toolkit, ranging from data donation (Es & Nguyen, 2025; which has already proven successful for behavioral research on social media, e.g., Yap et al., 2024), scraping-based methods (Ballou et al., 2024), and existing APIs (e.g., Vuorre et al., 2021; although see Davidson et al., 2023 for transparency issues associated with platform-provided APIs).
Further, while the quasi-representative sampling strategy underlying this data produced a sample more diverse than many comparable studies of video game trace data (e.g., Ballou et al., 2024; Larrieu et al., 2023), the sample here is not random or representative of either the general population or of all video game players. Such trade-offs are inherent: with the exception of extremely rare studies that have access to population data via platform owners (e.g., Zendle et al., 2023), collecting demographic data requires contacting individual participants who may elect not to join the study. Selection biases are thus present with regard to willingness to share data, sufficient gaming on included platforms, and participation in the survey platforms from which recruitment took place (Prolific, PureProfile).
I particularly draw attention to the exclusion of mobile gaming due to insufficient data granularity, as previous results have shown that the demographics of mobile players systematically differ from those of other gaming platforms (Entertainment Software Association, 2025; Gottfried & Sidoti, 2024). Similarly, the lack of Xbox data among UK participants means recorded behavior and game composition differs across countries ; to mitigate this, the analyses intentionally do not compare the US and UK, but group all participants. The combination of selection biases present in the study also likely contributed to some of the surprising characteristics of the sample, such as high self-reported rates of neurodiversity.
In short, more representative or targeted samples, deep game-level data, and qualitative follow-up are all needed to build on this work. Future research should aim to disentangle access effects from preference effects, assess the generalizability of these patterns, and further unpack the lived experiences underlying observed demographic differences.
This study used publicly available behavioral trace data from Steam, Xbox, and Nintendo Switch to describe how play patterns vary across age, gender, ethnicity, and neurodiversity in a diverse sample of 3768 US and UK adults. Results revealed a range of demographic differences in play volume, temporal organization, engagement span, and genre composition, many of which corroborate prior survey-based findings, while others (such as the earlier peak playtime among older players or the longer engagement spans among women and Black players) have rarely been directly observed in naturalistic data.
Yet the overarching finding is one of within-group heterogeneity: demographic characteristics collectively explained less than 6% of the variance in any dimension of play behavior examined. These results suggest that while demographic categories remain useful for coarse description and hypothesis generation, they are poor proxies for how any individual actually plays. Future work should aim to move beyond demographics as default predictors, instead investigating the structural, cultural, and individual-level mechanisms that give rise to the patterns observed here, and doing so with representative samples, deep game-level data, and methods that center players’ own accounts of their play.
[Redacted for anonymous peer review]
[Redacted for anonymous peer review]
A portion of the data in this study (Xbox and Nintendo Switch trace data) was collected via data-sharing agreements with Microsoft, Nintendo of America, and Nintendo of Europe. Industry partners did not contribute funding for the research or any of the researchers involved in conducting it, and had no role in the design, analysis, or publication of results.
The author declares no other potential financial, intellectual, or institutional conflicts of interests.
Claude Code (claude-opus-4-5-20251101) assisted with writing, editing, and documenting analysis code. All analyses were reviewed, amended where necessary and approved; the author takes full responsibility for the content of the analyses and paper.
[Redacted]
tibble(
Platform = c(
"Nintendo",
"Xbox (US only)",
"Steam",
"iOS",
"Android"
),
Source = c(
"Nintendo of America/Europe data-sharing",
"Microsoft data-sharing",
"Custom web app (Gameplay.Science)",
"iOS Screen Time screenshots",
"Digital Wellbeing screenshots"
),
`Account Linking` = c(
"Participants share QR code identifier from Nintendo web interface; Nintendo retrieves and shares gameplay data",
"Participants opt in via Xbox Insiders; Microsoft shares pseudonymized data",
"Participants authenticate via Steam API (OpenID); gameplay monitored for study duration",
"Screenshots of up to 3 weeks of gaming; data extracted via OCR",
"Screenshots of up to 3 weeks of gaming; data extracted via OCR"
),
`Data Type` = c(
"Session records (game, time, duration) for first-party Nintendo games only",
"Session records with anonymized titles; genre and age rating preserved",
"Hourly aggregates per game.",
"Daily aggregates",
"Daily aggregates"
)
) |>
tt(
notes = "Nintendo-published games accounted for 63 percent of Switch playtime in the sample.",
width = 1
) |>
style_tt(fontsize = 0.7) |>
style_tt(i = 0, bold = TRUE, align = "c")| Platform | Source | Account Linking | Data Type |
|---|---|---|---|
| Nintendo-published games accounted for 63 percent of Switch playtime in the sample. | |||
| Nintendo | Nintendo of America/Europe data-sharing | Participants share QR code identifier from Nintendo web interface; Nintendo retrieves and shares gameplay data | Session records (game, time, duration) for first-party Nintendo games only |
| Xbox (US only) | Microsoft data-sharing | Participants opt in via Xbox Insiders; Microsoft shares pseudonymized data | Session records with anonymized titles; genre and age rating preserved |
| Steam | Custom web app (Gameplay.Science) | Participants authenticate via Steam API (OpenID); gameplay monitored for study duration | Hourly aggregates per game. |
| iOS | iOS Screen Time screenshots | Screenshots of up to 3 weeks of gaming; data extracted via OCR | Daily aggregates |
| Android | Digital Wellbeing screenshots | Screenshots of up to 3 weeks of gaming; data extracted via OCR | Daily aggregates |
# Get top games per demographic group (excluding Xbox)
# First prepare data with neurodiversity as a single column
telemetry_with_demo <- hourly_telemetry |>
filter(platform != "Xbox") |>
left_join(demographics, by = "pid") |>
filter(!is.na(age_group))
# Standard demographics (age, gender, ethnicity)
top_games_standard <- telemetry_with_demo |>
pivot_longer(
cols = c(age_group, gender, ethnicity),
names_to = "demo_type",
values_to = "demo_group"
) |>
filter(!is.na(demo_group)) |>
group_by(demo_type, demo_group, title_id) |>
summarise(total_hours = sum(minutes, na.rm = TRUE) / 60, .groups = "drop")
# Neurodiversity (non-exclusive categories handled separately)
top_games_neuro <- bind_rows(
telemetry_with_demo |>
filter(is_neurotypical == TRUE) |>
group_by(title_id) |>
summarise(
total_hours = sum(minutes, na.rm = TRUE) / 60,
.groups = "drop"
) |>
mutate(demo_type = "neurodiversity", demo_group = "Neurotypical"),
telemetry_with_demo |>
filter(is_adhd == TRUE) |>
group_by(title_id) |>
summarise(
total_hours = sum(minutes, na.rm = TRUE) / 60,
.groups = "drop"
) |>
mutate(demo_type = "neurodiversity", demo_group = "ADHD"),
telemetry_with_demo |>
filter(is_autism == TRUE) |>
group_by(title_id) |>
summarise(
total_hours = sum(minutes, na.rm = TRUE) / 60,
.groups = "drop"
) |>
mutate(demo_type = "neurodiversity", demo_group = "ASD")
)
top_games_by_demo <- bind_rows(top_games_standard, top_games_neuro) |>
# Get top 5 per demographic group
group_by(demo_type, demo_group) |>
slice_max(total_hours, n = 5) |>
mutate(rank = row_number()) |>
ungroup() |>
# Clean up demo_type names
mutate(
demo_type = case_when(
demo_type == "age_group" ~ "Age",
demo_type == "gender" ~ "Gender",
demo_type == "ethnicity" ~ "Ethnicity",
demo_type == "neurodiversity" ~ "Neurodiversity"
)
)
# Pivot to wide format for display
top_games_wide <- top_games_by_demo |>
mutate(
game_label = glue("{title_id}\n({scales::comma(round(total_hours))}h)")
) |>
select(demo_type, demo_group, rank, game_label) |>
pivot_wider(
names_from = rank,
values_from = game_label,
names_prefix = "Rank "
) |>
arrange(demo_type, demo_group) |>
rename(Demographic = demo_type, Group = demo_group)
top_games_wide |>
tt(width = 1) |>
style_tt(fontsize = 0.6) |>
style_tt(i = 0, bold = TRUE)| Demographic | Group | Rank 1 | Rank 2 | Rank 3 | Rank 4 | Rank 5 |
|---|---|---|---|---|---|---|
| Age | 18-24 | Animal Crossing New Horizons (14,080h) | Marvel Rivals (10,507h) | The Legend of Zelda Tears of the Kingdom (8,281h) | Counter-Strike 2 (6,519h) | Splatoon 3 (6,194h) |
| Age | 25-30 | Animal Crossing New Horizons (16,075h) | Pokemon Scarlet / Violet (11,369h) | The Legend of Zelda Tears of the Kingdom (11,129h) | Marvel Rivals (8,187h) | Counter-Strike 2 (6,566h) |
| Age | 31-35 | The Legend of Zelda Tears of the Kingdom (5,047h) | Pokemon Scarlet / Violet (2,857h) | Animal Crossing New Horizons (2,814h) | FINAL FANTASY XIV Online (2,136h) | Football Manager 2024 (1,664h) |
| Age | 36-40 | The Legend of Zelda Tears of the Kingdom (3,128h) | Animal Crossing New Horizons (3,061h) | Pokemon Scarlet / Violet (2,870h) | Mario Kart 8 Deluxe (1,253h) | Football Manager 2024 (959h) |
| Ethnicity | Asian | Marvel Rivals (1,758h) | Umamusume: Pretty Derby (1,748h) | MapleStory (1,710h) | The Legend of Zelda Tears of the Kingdom (1,560h) | Pokemon Scarlet / Violet (1,508h) |
| Ethnicity | Black | Animal Crossing New Horizons (2,710h) | Marvel Rivals (2,047h) | Super Smash Bros Ultimate (1,697h) | Splatoon 3 (1,621h) | The Legend of Zelda Tears of the Kingdom (1,532h) |
| Ethnicity | Multiple | Marvel Rivals (3,876h) | Animal Crossing New Horizons (2,723h) | FINAL FANTASY XIV Online (2,277h) | Pokemon Scarlet / Violet (1,918h) | The Legend of Zelda Tears of the Kingdom (1,660h) |
| Ethnicity | Other | Pokemon UNITE (1,399h) | Super Smash Bros Ultimate (1,069h) | Marvel Rivals (711h) | Umamusume: Pretty Derby (698h) | The Legend of Zelda Tears of the Kingdom (584h) |
| Ethnicity | White | Animal Crossing New Horizons (28,694h) | The Legend of Zelda Tears of the Kingdom (22,238h) | Pokemon Scarlet / Violet (16,978h) | Marvel Rivals (11,847h) | Counter-Strike 2 (10,766h) |
| Gender | Man | Pokemon Scarlet / Violet (13,868h) | The Legend of Zelda Tears of the Kingdom (13,449h) | Counter-Strike 2 (12,824h) | Marvel Rivals (12,164h) | Animal Crossing New Horizons (7,842h) |
| Gender | Non-binary | Animal Crossing New Horizons (4,157h) | Baldur's Gate 3 (2,361h) | The Legend of Zelda Tears of the Kingdom (2,155h) | Warframe (1,973h) | Splatoon 3 (1,442h) |
| Gender | Woman | Animal Crossing New Horizons (23,987h) | The Legend of Zelda Tears of the Kingdom (11,980h) | Marvel Rivals (6,713h) | Pokemon Scarlet / Violet (6,417h) | Splatoon 3 (5,736h) |
| Neurodiversity | ADHD | Animal Crossing New Horizons (12,890h) | The Legend of Zelda Tears of the Kingdom (7,363h) | Marvel Rivals (6,823h) | Pokemon Scarlet / Violet (5,170h) | Baldur's Gate 3 (4,804h) |
| Neurodiversity | ASD | Animal Crossing New Horizons (8,301h) | The Legend of Zelda Tears of the Kingdom (6,134h) | Pokemon Scarlet / Violet (5,172h) | Splatoon 3 (4,536h) | Baldur's Gate 3 (4,077h) |
| Neurodiversity | Neurotypical | The Legend of Zelda Tears of the Kingdom (14,973h) | Animal Crossing New Horizons (14,348h) | Pokemon Scarlet / Violet (11,657h) | Marvel Rivals (10,916h) | Counter-Strike 2 (10,309h) |
build_genre_bar_grid(
genre_props_dev_plot_multi,
genre_ci_multi,
group_ns_multi,
group_colors
)
# Get all raw genres (before simplification)
games_all_genres <- games |>
filter(!is.na(genres)) |>
separate_rows(genres, sep = ",\\s*") |>
distinct(original_name, genres) |>
group_by(original_name) |>
mutate(genre_weight = 1 / n()) |>
ungroup()
# Calculate playtime by raw genre and demographic
genre_playtime_raw <- hourly_telemetry |>
left_join(
games_all_genres,
by = c("title_id" = "original_name"),
relationship = "many-to-many"
) |>
filter(!is.na(genres)) |>
mutate(minutes_weighted = minutes * genre_weight) |>
left_join(demographics, by = "pid") |>
filter(!is.na(age_group))
# Step 1: Calculate individual-level genre proportions
individual_genre_props <- genre_playtime_raw |>
group_by(pid, genres) |>
summarise(
genre_minutes = sum(minutes_weighted, na.rm = TRUE),
.groups = "drop"
) |>
group_by(pid) |>
mutate(individual_prop = genre_minutes / sum(genre_minutes) * 100) |>
ungroup()
# Join demographic info back
individual_with_demo <- individual_genre_props |>
left_join(
demographics |>
select(
pid,
age_group,
gender,
ethnicity,
is_neurotypical,
is_adhd,
is_autism
),
by = "pid"
)
# Helper to calculate median props for a grouping variable
calc_median_props <- function(data, group_var, demo_name) {
data |>
filter(!is.na(.data[[group_var]])) |>
group_by(.data[[group_var]], genres) |>
summarise(prop = median(individual_prop, na.rm = TRUE), .groups = "drop") |>
rename(group = all_of(group_var)) |>
mutate(demo = demo_name)
}
# Calculate for all demographics using median of individual allocations
all_genre_props <- bind_rows(
calc_median_props(individual_with_demo, "age_group", "Age"),
calc_median_props(individual_with_demo, "gender", "Gender"),
calc_median_props(individual_with_demo, "ethnicity", "Ethnicity"),
# Neurodiversity (non-exclusive categories)
bind_rows(
individual_with_demo |>
filter(is_neurotypical == TRUE) |>
group_by(genres) |>
summarise(
prop = median(individual_prop, na.rm = TRUE),
.groups = "drop"
) |>
mutate(group = "Neurotypical"),
individual_with_demo |>
filter(is_adhd == TRUE) |>
group_by(genres) |>
summarise(
prop = median(individual_prop, na.rm = TRUE),
.groups = "drop"
) |>
mutate(group = "ADHD"),
individual_with_demo |>
filter(is_autism == TRUE) |>
group_by(genres) |>
summarise(
prop = median(individual_prop, na.rm = TRUE),
.groups = "drop"
) |>
mutate(group = "Autism")
) |>
mutate(demo = "Neuro")
) |>
mutate(prop_fmt = sprintf("%.1f", prop))
# Create genre mapping - only show if it maps to one of the TOP 8 genres in radar
genre_mapping <- tibble(genres = unique(all_genre_props$genres)) |>
mutate(
cleaned = clean_genre(genres),
abbreviated = abbreviate_genre(cleaned),
# Only show mapping if it's in the top_genres used in radar
maps_to = if_else(abbreviated %in% top_genres, abbreviated, "—")
) |>
select(genres, maps_to)
# Calculate overall mean to sort by
genre_order <- all_genre_props |>
group_by(genres) |>
summarise(mean_prop = mean(prop), .groups = "drop") |>
arrange(desc(mean_prop)) |>
pull(genres)
# Pivot to wide format with readable column names (~5 chars)
genre_table <- all_genre_props |>
mutate(
group_abbr = case_when(
group == "18-24" ~ "18-24",
group == "25-30" ~ "25-30",
group == "31-35" ~ "31-35",
group == "36-40" ~ "36-40",
group == "Man" ~ "Man",
group == "Woman" ~ "Woman",
group == "Non-binary" ~ "NB/Ot",
group == "Asian" ~ "Asian",
group == "Black" ~ "Black",
group == "Mixed/Multiple" ~ "Multiple",
group == "Other" ~ "Other",
group == "White" ~ "White",
group == "Neurotypical" ~ "Neuro",
group == "ADHD" ~ "ADHD",
group == "Autism" ~ "Autis",
TRUE ~ group
)
) |>
select(genres, group_abbr, prop_fmt) |>
pivot_wider(
names_from = group_abbr,
values_from = prop_fmt,
values_fill = "0.0"
) |>
left_join(genre_mapping, by = "genres") |>
mutate(
genres = str_replace_all(genres, "&", "and"),
genres = factor(genres, levels = str_replace_all(genre_order, "&", "and"))
) |>
arrange(genres) |>
select(Genre = genres, Radar = maps_to, everything())
genre_table |>
tt(
notes = "Values are median percentages of individual players' genre allocations (matching radar chart methodology). 'Radar' shows the simplified category used in the main text radar plot (— if genre is not included in the 8 simplified categories). Neuro = Neurotypical, ASD = Autism spectrum.",
width = 1
) |>
style_tt(fontsize = 0.5) |>
style_tt(i = 0, bold = TRUE, fontsize = 0.45)| Genre | Radar | 18-24 | 25-30 | 31-35 | 36-40 | Man | NB/Ot | Woman | Asian | Black | Multiple | Other | White | Neuro | ADHD | Autis |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Values are median percentages of individual players' genre allocations (matching radar chart methodology). 'Radar' shows the simplified category used in the main text radar plot (— if genre is not included in the 8 simplified categories). Neuro = Neurotypical, ASD = Autism spectrum. | ||||||||||||||||
| Adventure | — | 14.8 | 17.3 | 20.0 | 20.3 | 16.5 | 15.7 | 18.1 | 17.4 | 16.4 | 15.1 | 14.3 | 17.5 | 17.5 | 15.2 | 16.4 |
| Role-playing (RPG) | RPG | 11.1 | 14.5 | 15.4 | 14.0 | 12.7 | 15.0 | 13.1 | 13.2 | 13.5 | 12.5 | 13.6 | 13.1 | 13.1 | 13.3 | 14.4 |
| Shooter | Shooter | 15.3 | 12.5 | 9.9 | 13.4 | 14.4 | 8.7 | 9.5 | 16.0 | 14.6 | 14.2 | 20.0 | 12.2 | 13.9 | 13.0 | 11.0 |
| Simulator | Simulation | 11.4 | 10.7 | 8.5 | 11.1 | 8.3 | 12.2 | 17.2 | 8.6 | 10.4 | 9.6 | 7.5 | 11.3 | 10.4 | 10.7 | 12.3 |
| Indie | — | 9.9 | 10.0 | 9.3 | 10.6 | 9.1 | 12.4 | 12.0 | 10.5 | 8.6 | 11.0 | 6.9 | 9.9 | 9.3 | 10.0 | 10.1 |
| Strategy | Strategy | 6.2 | 8.0 | 7.4 | 6.5 | 7.3 | 5.4 | 7.2 | 6.4 | 5.4 | 7.8 | 4.2 | 7.4 | 7.3 | 6.5 | 7.7 |
| Platform | Platform | 3.2 | 2.4 | 3.3 | 4.5 | 2.8 | 1.7 | 3.3 | 3.2 | 3.1 | 2.6 | 4.2 | 2.7 | 3.1 | 2.1 | 2.2 |
| Sport | Sport | 2.0 | 2.9 | 2.2 | 2.7 | 3.0 | 1.0 | 1.5 | 4.4 | 4.4 | 1.2 | 2.2 | 2.4 | 3.2 | 1.6 | 1.4 |
| Puzzle | Puzzle | 1.9 | 2.1 | 2.9 | 3.8 | 1.8 | 1.8 | 3.6 | 1.6 | 1.8 | 1.5 | 3.2 | 2.3 | 2.5 | 1.8 | 2.0 |
| Tactical | Strategy | 3.1 | 2.4 | 2.4 | 2.2 | 3.1 | 0.9 | 1.7 | 3.3 | 1.6 | 1.6 | 1.7 | 2.8 | 3.1 | 2.0 | 2.2 |
| Racing | Racing | 2.5 | 1.9 | 1.8 | 1.9 | 2.0 | 0.9 | 2.4 | 2.9 | 2.3 | 1.7 | 2.8 | 2.1 | 2.6 | 1.8 | 1.7 |
| Turn-based strategy (TBS) | Strategy | 1.7 | 2.0 | 2.2 | 3.2 | 1.9 | 1.7 | 2.2 | 2.9 | 2.1 | 1.2 | 1.9 | 2.0 | 2.2 | 1.7 | 1.9 |
| Real Time Strategy (RTS) | Strategy | 1.2 | 2.4 | 1.8 | 2.3 | 2.1 | 1.8 | 1.3 | 2.8 | 1.2 | 2.2 | 2.7 | 1.7 | 2.3 | 1.4 | 1.7 |
| Card and Board Game | Puzzle | 1.2 | 1.9 | 2.3 | 2.6 | 1.3 | 1.0 | 3.2 | 2.4 | 1.9 | 1.7 | 1.4 | 1.6 | 2.2 | 1.3 | 1.3 |
| Arcade | — | 2.0 | 1.3 | 1.4 | 1.6 | 1.4 | 0.9 | 2.1 | 2.3 | 2.5 | 1.1 | 3.3 | 1.5 | 1.7 | 1.3 | 1.4 |
| Hack and slash/Beat 'em up | — | 1.2 | 1.5 | 1.5 | 1.3 | 1.5 | 1.2 | 1.0 | 2.4 | 1.3 | 1.6 | 1.5 | 1.3 | 1.5 | 1.0 | 1.2 |
| Visual Novel | — | 1.0 | 0.9 | 1.4 | 1.3 | 0.8 | 1.5 | 1.2 | 1.8 | 2.7 | 1.2 | 1.4 | 0.7 | 1.1 | 0.9 | 0.8 |
| Fighting | — | 1.0 | 0.8 | 0.9 | 1.1 | 1.1 | 0.6 | 0.7 | 1.3 | 1.9 | 1.5 | 1.5 | 0.8 | 1.0 | 0.8 | 0.6 |
| MOBA | Strategy | 0.7 | 1.2 | 2.1 | 0.9 | 1.1 | 0.5 | 0.8 | 0.7 | 0.8 | 0.7 | 1.1 | 1.1 | 1.0 | 1.2 | 0.8 |
| Music | — | 0.7 | 0.5 | 1.0 | 1.0 | 0.5 | 1.0 | 0.7 | 1.4 | 0.6 | 0.6 | 0.3 | 0.5 | 0.5 | 0.7 | 0.8 |
| Point-and-click | — | 0.4 | 0.5 | 0.7 | 0.4 | 0.3 | 0.7 | 0.7 | 0.2 | 3.0 | 0.7 | 0.1 | 0.5 | 0.5 | 0.4 | 0.5 |
| Quiz/Trivia | Puzzle | 0.2 | 0.1 | 0.3 | 0.7 | 0.2 | 0.4 | 0.1 | 0.1 | 0.2 | 0.1 | 0.1 | 0.2 | 0.2 | 0.2 | 0.4 |
| Pinball | — | 0.2 | 0.1 | 0.4 | 0.0 | 0.2 | 0.0 | 0.0 | 0.0 | 0.9 | 0.0 | 0.2 | 0.1 | 0.2 | 0.0 | 0.1 |
4.3.1 Social play patterns
Show code (game mode proportions)
Show code (game modes dot plot)