Deep-dive · The ABCD-Shaped Dataset

The dataset used in Modules 2, 3 and 4

Modules 2, 3 and 4 all run on one file: data/data.RDS. It is bigger and messier than the Module 1 dataset, and deliberately so — it is built to behave like a real ABCD release, down to the variable names, the coded factor levels, and the shape of the missing data.

This page is the reference for it. Come back here whenever you need to look up what a column means.

ImportantThe data is not real

Every row is simulated. No real participant contributes a row, a value or a record. The shape was fitted from a de-identified ABCD extract — column names, variable types, level sets, means, SDs and missingness rates — and the extract was then set aside. Only those summary numbers live in the generator, R/generate-abcd-shaped-data.R.

Get the files

Three files. Put all three in the data/ folder of your RStudio Project — the same place abcd-synthetic.csv went in Module 0.

data.RDS data_dictionary.csv data_dictionary_levels.csv

File What it holds
data.RDS The data itself — one row per participant per visit
data_dictionary.csv One row per variable: what it is, who reported it, which instrument it came from
data_dictionary_levels.csv One row per level of every coded variable: 1 means Male, 2 means Female, and so on

A real ABCD release arrives exactly like this: data in one place, meaning in another. Learning to work with the pair is part of the point.

Load it

library(readr)
library(dplyr)

Attaching package: 'dplyr'
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
data <- readRDS("data/data.RDS")

dict <- read_csv("data/data_dictionary.csv")
Rows: 51 Columns: 40
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (32): study, domain, sub_domain, source, metric, atlas, table_name, tabl...
dbl  (1): order_sort
lgl  (7): instruction, label_es, instruction_es, header_es, note_es, url_tab...

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
levels_dict <- read_csv("data/data_dictionary_levels.csv")
Rows: 84 Columns: 5
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (2): name, label
dbl (2): value, order_level
lgl (1): label_es

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

.RDS is R’s own file format, so readRDS() needs no package and takes no arguments beyond the path. It also preserves everything: factors stay factors, and the visit timestamp stays a date-time. A CSV would lose both, and you would have to rebuild them by hand every time you opened the file.

dim(data)
[1] 2295   53

2,295 rows and 53 columns — 500 participants, each with up to five visits.

The shape

One row is one participant at one visit. Two columns identify that row:

data |>
  select(participant_id, session_id, ab_g_dyn__visit_age) |>
  head(5)
  participant_id session_id ab_g_dyn__visit_age
1       sub-0001    ses-00A                10.2
2       sub-0001    ses-01A                10.7
3       sub-0001    ses-02A                12.4
4       sub-0001    ses-03A                12.9
5       sub-0001    ses-04A                14.0

Neither column identifies a row on its own — you need the pair. That is what makes this a longitudinal dataset, and it is the reason nearly everything in Modules 3 and 4 has to think about the fact that the five rows belonging to sub-0001 are not five independent observations.

The five sessions are annual visits:

session_id Visit
ses-00A Baseline
ses-01A 1-year follow-up
ses-02A 2-year follow-up
ses-03A 3-year follow-up
ses-04A 4-year follow-up
count(data, session_id)
  session_id   n
1    ses-00A 500
2    ses-01A 484
3    ses-02A 458
4    ses-03A 439
5    ses-04A 414

The counts fall because participants drop out — 500 at baseline, 414 still present four years later. Nobody who leaves comes back.

How ABCD names its variables

The column names look forbidding until you know they are structured. Read them left to right, splitting on the double underscore:

nt_y_stq__screen__wkdy_sum
│  │ │    │       │
│  │ │    │       └── the statistic: weekday sum
│  │ │    └────────── the scale: screen time
│  │ └─────────────── the instrument: Screen Time Questionnaire
│  └───────────────── who reported it: y = youth, p = parent, g = general
└──────────────────── the domain: nt = Novel Technologies

Eight domain prefixes appear in this dataset:

Prefix Domain
ab ABCD (General) — IDs, site, visit and demographic variables
fc Friends, Family, & Community
mh Mental Health
mr Imaging (structural MRI)
nc Neurocognition
nt Novel Technologies (screen use)
ph Physical Health
su Substance Use

Once that clicks, mh_p_cbcl__synd__int_sum reads as Mental Health · parent-reported · Child Behavior Checklist · syndrome scale · internalizing · sum without looking anything up. It also means select() helpers do real work — starts_with("nc_") gives you every cognition variable at once.

Browse the variables

All 53 columns, grouped by domain. Search, filter, sort, expand a domain, or download the whole table as a CSV.

source is who answered: Youth is the child’s own report, Parent the caregiver’s, General an administrative field. Parent and youth reports of the same construct disagree — this dataset has both a parent and a youth screen-time measure, and comparing them is a Module 3 exercise waiting to happen.

The fifteen instruments behind those variables:

instrument what it is source domain
ab_g_dyn ABCD Dynamic Variables [General] General ABCD (General)
ab_g_stc ABCD Static Variables [General] General ABCD (General)
fc_y_fes Family Environment Scale [Youth] Youth Friends, Family, & Community
fc_y_nsc Neighborhood Safety & Crime [Youth] Youth Friends, Family, & Community
fc_y_pm Parental Monitoring [Youth] Youth Friends, Family, & Community
mr_y_smri__thk__dsk sMRI - Cortical Thickness (Desikan) [Youth] Youth Imaging
mr_y_smri__vol__dsk sMRI - Volume (Desikan) [Youth] Youth Imaging
mh_p_cbcl Child Behavior Checklist [Parent] Parent Mental Health
mh_y_bisbas The Behavioral Inhibition System/Behavioral Activation System Scales [Youth] Youth Mental Health
mh_y_upps Urgency, Premeditation, Perseverance, Sensation Seeking, Positive Urgency, Impulsive Behavior Scale (Short Version) [Youth] Youth Mental Health
nc_y_nihtb NIH Toolbox [Youth] Youth Neurocognition
nt_p_yst Screen Time Questionnaire [Parent] Parent Novel Technologies
nt_y_stq Screen Time Questionnaire [Youth] Youth Novel Technologies
ph_y_anthr Anthropometrics [Youth] Youth Physical Health
su_y_itu PATH Intention to Use [Youth] Youth Substance Use

Coded values, and their labels

Here is the thing that surprises everyone the first time. Look at sex:

count(data, ab_g_stc__cohort_sex)
  ab_g_stc__cohort_sex    n
1                    1 1076
2                    2 1219

The values are 1 and 2. Not "Male" and "Female" — the meaning is not in the data file. It is in data_dictionary_levels.csv, which is why that file matters:

levels_dict |>
  filter(name == "ab_g_stc__cohort_sex")
# A tibble: 2 × 5
  name                 value order_level label  label_es
  <chr>                <dbl>       <dbl> <chr>  <lgl>   
1 ab_g_stc__cohort_sex     1           1 Male   NA      
2 ab_g_stc__cohort_sex     2           2 Female NA      

This is not sloppiness. Real ABCD releases ship coded values and a separate dictionary on purpose: codes survive translation, they do not break when a label is reworded, and the same file can serve English and Spanish labels (hence the label_es column). The cost is that you must apply the labels yourself.

Labelling one variable

Pull that variable’s levels out of the dictionary, then hand them to factor():

sex_levels <- levels_dict |>
  filter(name == "ab_g_stc__cohort_sex") |>
  arrange(order_level)

data <- data |>
  mutate(
    ab_g_stc__cohort_sex = factor(
      ab_g_stc__cohort_sex,
      levels = sex_levels$value,   # the codes stored in the data
      labels = sex_levels$label    # what each code means
    )
  )

count(data, ab_g_stc__cohort_sex)
  ab_g_stc__cohort_sex    n
1                 Male 1076
2               Female 1219

Three things worth noticing:

  • levels and labels must line up, position by position. arrange(order_level) is what guarantees that — the dictionary already knows the order these levels should appear in, which matters for ordinal variables like income bands, where alphabetical order would put > 100k before < 50k.
  • The variable is still a factor afterwards, not text. It plots and tabulates in the dictionary’s order rather than alphabetically.
  • Any code present in levels but absent from the data simply produces a zero count. That is usually what you want — Decline to answer is a real category even in a wave where nobody chose it.

Repeat that block for each variable you actually use. Doing all thirteen coded variables in one stroke is a Module 2 topic — it needs tools this page has not introduced yet, and typing it out longhand a few times first is not wasted effort.

Every coded variable

The thirteen variables that carry codes, and what each code means. Expand a variable to see its levels.

Two codes recur across variables and are not ordinary responses:

Code Meaning
777 Decline to answer
999 Don’t know

They are stored as ordinary levels, so a mean or an ordinal model that treats them as the top of the scale will be badly wrong. Deciding what to do with them — drop them, or keep them as their own category — is an analysis choice you have to make explicitly. Module 3 makes it.

What is missing, and why

About 27% of all cells are empty, and that is realistic rather than broken. What matters is that the missingness is structured: it is never a uniform random sprinkle, so dropping incomplete rows is never free.

colMeans(is.na(data)) |>
  sort(decreasing = TRUE) |>
  head(8) |>
  round(2)
                  fc_y_fes__cohes_mean    nc_y_nihtb__comp__tot__agecor_score 
                                  0.82                                   0.79 
nc_y_nihtb__comp__fluid__agecorr_score       nc_y_nihtb__crdst__agecorr_score 
                                  0.79                                   0.79 
       nc_y_nihtb__lswmt__agecor_score nc_y_nihtb__comp__cryst__agecorr_score 
                                  0.61                                   0.59 
              mr_y_smri__thk__dsk_mean            mr_y_smri__vol__dsk__lh_sum 
                                  0.51                                   0.51 

Four different mechanisms produce those gaps:

  • Instrument schedules. Several measures are only collected at baseline, the 2-year and the 4-year visit — so they are ~40% missing by design, not by accident. A few NIH Toolbox subtests were retired after baseline; the Family Environment cohesion scale was only added at the 4-year visit.
  • Visit type. Anthropometrics are ~80% missing at Remote visits and ~6% missing elsewhere. Nobody can measure a waist circumference over video.
  • Data quality. MRI failure rises with age, because older children move more in the scanner.
  • Attrition. 86 participants never reach the 4-year visit — and they are not a random 86. They had higher baseline externalizing scores and were more often in the lowest income band.

That last one is the important one. It means a complete-case analysis quietly changes who you are studying, and that is a point Module 3 has to make with this dataset rather than in the abstract.

Is there anything in here to find?

Yes. The dataset is not noise — real associations were built into it, sized so that a straightforward analysis at n = 500 will actually recover them. Finding them is the work of Modules 3 and 4.

Open this if you are teaching from the dataset, or working through it alone and want to check your answer. If you are attending Modules 3 and 4, you will get more out of them by leaving it shut.

  • E1 Age → youth-reported screen time. About +1.0 h/day per year on weekdays, +1.8 h/day per year at weekends.
  • E2 Screen time → CBCL internalizing, holding age and sex constant. About +0.27 points per weekday hour.
  • E3 Household income → NIH Toolbox crystallized, picture vocabulary and oral reading. About 0.6 SD between the lowest and the highest income group at baseline.
  • E4 Sex. Males about +1.5 externalizing, females about +1.1 internalizing, males about 7% larger total cortical volume.
  • E5 Site. Random intercepts on cognition (SD 2.5 standard-score points) and on screen time (SD 0.4 h) — the reason site belongs in a model rather than being ignored.
  • E6 Family conflict (FES) → CBCL externalizing, about +2.9 points across the range of the conflict mean.
  • E7 Parental monitoring → less substance-use curiosity; age → more curiosity.
  • E8 Age → cortical thinning, about −0.030 mm/year, and a slow decline in total cortical volume after age 12.
  • E9 Household income → perceived neighbourhood safety. 53% of the highest income group strongly agree their neighbourhood is safe, against 30% of the lowest.
  • E10 Missingness is structured, as described above — instrument schedules, remote visits, age-related MRI failure, and MNAR attrition.

Numbers are what a straightforward model recovers from the file as it stands, not the coefficients fed to the simulation.

Where this is used

  • Module 2 — exploring and visualising it
  • Module 3 — testing hypotheses on it
  • Module 4 — running an analysis end to end on it

Module 1 does not use this file. It uses the much smaller data/abcd-synthetic.csv, which has plain English column names and three visits — deliberately, so that a first hour in R is spent learning R rather than decoding variable names.

Real ABCD data

Everything on this page mirrors the real thing, but the real thing needs authorisation. Access runs through the NIMH Data Archive, and the official variable documentation lives at data.abcdstudy.org — the url_table column of data_dictionary.csv points at the relevant page for every instrument here.