Relatório final

Projeto final · CPAD

TDAH em contexto: onde escola, rotina e adversidades se encontram

Da heterogeneidade de registros brutos a uma leitura exploratória sobre saúde, escola, rotina e adversidades.

David Erick Araújo Ferraz

1. Contexto, roteiro e pacotes

O Transtorno de Déficit de Atenção/Hiperatividade (TDAH) raramente aparece sozinho no cotidiano de uma criança. A pergunta que guia esta análise não é apenas “quem relatou TDAH?”, mas em quais combinações de escola, saúde, experiências adversas e rotina esse relato aparece com mais frequência?

A análise utiliza os microdados públicos de 2023 da National Survey of Children’s Health (NSCH), pesquisa norte-americana voltada a crianças e adolescentes de 0 a 17 anos. A fonte oficial disponibiliza os arquivos e a documentação das variáveis em Census Bureau - 2023 NSCH Data Release e no Topical Variable List.

O problema prático é responder: quais perfis escolares, clínicos e sociais aparecem com maior frequência entre crianças com e sem diagnóstico informado de TDAH? Os potenciais usuários são profissionais de educação, saúde infantil e gestores interessados em planejamento de apoio, sempre sem usar esta análise para diagnóstico individual.

Roteiro de implementação

  1. Importar a base bruta e verificar a integridade de seu esquema.
  2. Estruturar textos, códigos, valores ausentes e tipos de dados com base na documentação técnica da pesquisa.
  3. Juntar uma tabela de referência de códigos FIPS para identificar os estados.
  4. Criar indicadores analíticos de diagnóstico, comorbidades, adversidades, rotina e desempenho escolar.
  5. Validar a qualidade da estrutura obtida e apresentar a base analítica de forma condensada.
  6. Produzir tabelas, gráficos estáticos e recursos interativos para comparar os grupos com e sem TDAH.
  7. Interpretar os resultados como associações descritivas e registrar limitações.

Pacotes requeridos

# Não instale pacotes automaticamente. Se necessário, execute uma única vez no RStudio:
# install.packages(c("readr", "dplyr", "tidyr", "stringr", "forcats", "ggplot2",
#                    "scales", "DT", "plotly", "knitr"))
# Opcional para reprodutibilidade do projeto: renv::init()

required_packages <- c(
  "readr", "dplyr", "tidyr", "stringr", "forcats", "ggplot2",
  "scales", "DT", "plotly", "knitr"
)

missing_packages <- required_packages[!vapply(required_packages, requireNamespace, logical(1), quietly = TRUE)]
if (length(missing_packages) > 0) {
  stop(
    "Pacotes ausentes: ", paste(missing_packages, collapse = ", "),
    ". Instale-os no RStudio com install.packages(c(\"",
    paste(missing_packages, collapse = "\", \""), "\"))."
  )
}

invisible(lapply(required_packages, library, character.only = TRUE))

package_purpose <- tibble::tibble(
  Pacote = required_packages,
  Finalidade = c(
    "Importação tipada de CSV",
    "Transformação e agregação de dados",
    "Reformatação de tabelas",
    "Padronização de texto e expressões regulares",
    "Ordenação explícita de fatores",
    "Gráficos estáticos",
    "Formatação de percentuais",
    "Tabelas HTML interativas",
    "Versões interativas de gráficos",
    "Formatação de tabelas no relatório"
  )
)

knitr::kable(package_purpose, caption = "Pacotes utilizados e sua finalidade.")
Pacotes utilizados e sua finalidade.
Pacote Finalidade
readr Importação tipada de CSV
dplyr Transformação e agregação de dados
tidyr Reformatação de tabelas
stringr Padronização de texto e expressões regulares
forcats Ordenação explícita de fatores
ggplot2 Gráficos estáticos
scales Formatação de percentuais
DT Tabelas HTML interativas
plotly Versões interativas de gráficos
knitr Formatação de tabelas no relatório

2. O conjunto de dados e suas variáveis

A fonte é a National Survey of Children’s Health (NSCH) 2023, uma pesquisa norte-americana com informações fornecidas por pais ou cuidadores sobre saúde, acesso a cuidados, ambiente familiar, experiências escolares e bem-estar de crianças e adolescentes de 0 a 17 anos. A edição de 2023 reúne 55.162 respostas no arquivo tópico de uso público.

O arquivo bruto recebido para este projeto contém 31 variáveis e mantém a unidade de análise da pesquisa: uma criança selecionada por domicílio. Ele reúne identificação, demografia, diagnóstico informado de TDAH, comorbidades, escola, experiências adversas, rotina e condição socioeconômica. Antes da análise, os valores precisam ser estruturados porque uma mesma variável pode conter códigos, palavras, capitalizações ou formatos numéricos diferentes. A documentação técnica usada como referência para definir os padrões de destino está no dicionário de variáveis da NSCH.

variable_dictionary <- tibble::tribble(
  ~Grupo, ~Variável, ~Representa,
  "Identificação", "HHID", "Identificador do domicílio respondente.",
  "Identificação", "FIPSST", "Código FIPS do estado de residência.",
  "Identificação", "YEAR", "Ano de referência da pesquisa.",
  "Demografia", "SC_AGE_YEARS", "Idade da criança selecionada em anos completos.",
  "Demografia", "SC_SEX", "Sexo da criança selecionada.",
  "Demografia", "SC_RACE_R", "Raça detalhada da criança selecionada.",
  "TDAH", "K2Q31A", "Algum profissional já informou diagnóstico de ADD/TDAH.",
  "TDAH", "K2Q31B", "A criança apresenta ADD/TDAH atualmente.",
  "TDAH", "K2Q31C", "Gravidade atual informada para ADD/TDAH.",
  "TDAH", "K2Q31D", "Uso atual de medicação para ADD/TDAH.",
  "TDAH", "ADDTREAT", "Recebeu tratamento comportamental para ADD/TDAH nos últimos 12 meses.",
  "Comorbidades", "K2Q30A", "Diagnóstico já informado de deficiência de aprendizagem.",
  "Comorbidades", "K2Q32A", "Diagnóstico já informado de depressão.",
  "Comorbidades", "K2Q33A", "Diagnóstico já informado de problemas de ansiedade.",
  "Escola", "GRADES", "Padrão de notas recebido no ano escolar de 2022-2023.",
  "Escola", "REPEATED", "Repetiu alguma série desde o início do jardim de infância.",
  "Escola", "K7Q04R_R", "Número de contatos da escola com o domicílio por problemas escolares.",
  "Escola", "K6Q15", "Já teve plano de educação especial ou intervenção precoce.",
  "ACEs", "ACE1", "Frequência de dificuldade para cobrir necessidades básicas como alimentação ou moradia.",
  "ACEs", "ACE3", "Pais ou responsáveis se divorciaram ou se separaram.",
  "ACEs", "ACE4", "Morte de pai, mãe ou responsável.",
  "ACEs", "ACE5", "Pai, mãe ou responsável cumpriu pena de prisão.",
  "ACEs", "ACE6", "Viu ou ouviu violência física entre adultos no domicílio.",
  "ACEs", "ACE7", "Foi vítima ou testemunha de violência na vizinhança.",
  "ACEs", "ACE8", "Viveu com pessoa com doença mental, ideação suicida ou depressão grave.",
  "ACEs", "ACE9", "Viveu com pessoa com problema de álcool ou drogas.",
  "ACEs", "ACE10", "Sofreu tratamento injusto por raça ou grupo étnico.",
  "ACEs", "ACE11", "Sofreu tratamento injusto por condição de saúde ou deficiência.",
  "Rotina", "HOURSLEEP", "Horas médias de sono nas noites de semana.",
  "Rotina", "SCREENTIME", "Tempo de tela em um dia típico de semana.",
  "Socioeconômico", "FPL_I1", "Primeira imputação da razão de pobreza familiar (Federal Poverty Level)."
)

DT::datatable(
  variable_dictionary,
  caption = "Dicionário das 31 variáveis usadas no projeto. Use a busca para localizar um campo.",
  rownames = FALSE,
  filter = "top",
  options = list(pageLength = 12, scrollX = TRUE)
)
Como ler a base: variáveis de diagnóstico e ACEs são respostas relatadas por cuidadores; elas não constituem avaliação clínica independente. Além disso, a ausência em algumas questões pode ser esperada quando o item não se aplicava à faixa etária ou ao questionário recebido.

3. Preparação dos dados

Diagnóstico do problema e estratégia de estruturação

O arquivo bruto reúne 55.162 registros e 31 colunas relacionadas a identificação, demografia, TDAH, escola, ACEs, rotina e condição socioeconômica. Sua principal característica é a heterogeneidade de representação: respostas equivalentes aparecem como códigos numéricos e rótulos textuais, há diferenças de capitalização e espaços, números foram registrados com unidades e alguns ausentes utilizam marcadores textuais.

A estruturação transforma cada campo para um domínio consistente e verificável, sem substituir valores ausentes por zero e sem descartar registros apenas porque exigem recodificação. A variável de peso amostral não está presente no arquivo bruto; portanto, os resultados desta etapa analítica são descritivos e não ponderados, e não devem ser interpretados como estimativas nacionais.

Importação

data_candidates <- list.files(
  "outputs",
  pattern = "\\.csv$",
  recursive = TRUE,
  full.names = TRUE
)

if (length(data_candidates) != 1) {
  stop(
    "Esperava-se exatamente um arquivo CSV de dados dentro de 'outputs'. ",
    "Abra o projeto pelo diretório raiz e confirme os arquivos disponíveis."
  )
}

data_path <- data_candidates[[1]]

# Todas as colunas entram inicialmente como texto para evitar conversões silenciosas.
raw_data <- readr::read_csv(
  data_path,
  col_types = readr::cols(.default = readr::col_character()),
  na = c("", "NA", "n/a", "."),
  name_repair = "minimal",
  show_col_types = FALSE
)

expected_columns <- c(
  "HHID", "FIPSST", "YEAR", "SC_AGE_YEARS", "SC_SEX", "SC_RACE_R",
  "K2Q31A", "K2Q31B", "K2Q31C", "K2Q31D", "ADDTREAT",
  "K2Q30A", "K2Q32A", "K2Q33A", "GRADES", "REPEATED", "K7Q04R_R", "K6Q15",
  "ACE1", "ACE3", "ACE4", "ACE5", "ACE6", "ACE7", "ACE8", "ACE9", "ACE10", "ACE11",
  "HOURSLEEP", "SCREENTIME", "FPL_I1"
)

stopifnot(identical(names(raw_data), expected_columns))
stopifnot(nrow(raw_data) == 55162)

raw_display_data <- raw_data |>
  dplyr::mutate(
    .populated_fields = rowSums(!is.na(dplyr::across(dplyr::everything())))
  ) |>
  dplyr::arrange(dplyr::desc(.populated_fields)) |>
  dplyr::select(-.populated_fields)

DT::datatable(
  raw_display_data |>
    dplyr::slice_head(n = 10),
  caption = "Tabela 1. Base bruta: todas as 31 colunas. A primeira página prioriza registros completos para que nenhum campo apareça vazio; essa ordenação é apenas visual e não altera a base. Use a barra horizontal para consultar todas as variáveis.",
  options = list(pageLength = 10, scrollX = TRUE, scrollY = "420px")
)

Técnicas e regras de estruturação

A estruturação é executada em etapas explícitas. Primeiro, a importação preserva todos os campos como texto, evitando que o R transforme silenciosamente códigos ou identificadores. Em seguida, espaços são comprimidos e letras são normalizadas para permitir comparações consistentes. Funções de recodificação convertem códigos e rótulos equivalentes para domínios únicos, como Yes/No, níveis de gravidade, faixas de sono e categorias de notas. parse_number() recupera números inseridos com unidades, por exemplo em idade e razão de pobreza familiar. Marcadores NA, n/a, . e células vazias passam a representar uma ausência real em R.

O destino de cada coluna segue uma regra verificável. HHID permanece apenas como identificador. FIPSST vira indicadores binários de estado e YEAR vira um indicador binário do ano observado. SC_AGE_YEARS e FPL_I1 são convertidas em escalas min-max de 0 a 1. SC_SEX, SC_RACE_R, K2Q31C, GRADES e K7Q04R_R passam por codificação one-hot, criando uma coluna binária por categoria. K2Q31A, K2Q31B, K2Q31D, ADDTREAT, K2Q30A, K2Q32A, K2Q33A, REPEATED, K6Q15 e ACE3 a ACE11 tornam-se indicadores binários de presença (1) ou ausência (0). ACE1, HOURSLEEP e SCREENTIME são variáveis ordinais e são convertidas em escores normalizados de 0 a 1, preservando a ordem das respostas.

Por fim, o código FIPS é unido a uma tabela de estados antes da codificação; fatores recebem níveis ordenados quando existe uma ordem substantiva; e indicadores derivados são criados somente depois de os campos de origem estarem padronizados. A auditoria posterior mede ausências e valores que não puderam ser associados a uma regra, mantendo qualquer incerteza visível.

normalize_text <- function(x) {
  x |>
    stringr::str_squish() |>
    stringr::str_to_upper(locale = "en")
}

clean_yes_no <- function(x) {
  value <- normalize_text(x)
  dplyr::case_when(
    is.na(value) ~ NA_character_,
    value %in% c("1", "1.0", "YES", "Y") ~ "Yes",
    value %in% c("2", "2.0", "NO", "N") ~ "No",
    stringr::str_detect(value, "NO BEHAVIORAL TREATMENT|BEHAVIORAL TREATMENT: NO|DEPRESSION\\s*-\\s*NO") ~ "No",
    stringr::str_detect(value, "NOT TAKING|NO MEDS|NOT TREATED|TREATMENT: NO|MEDICATION: NO") ~ "No",
    stringr::str_detect(value, "NO LEARNING|NO DEPRESSION|LEARNING DISABILITY: NO|DEPRESSION: NO|LD - NO") ~ "No",
    stringr::str_detect(value, "^BEHAVIORAL TREATMENT$|BEHAVIORAL TREATMENT: YES|DEPRESSION\\s*-\\s*YES") ~ "Yes",
    stringr::str_detect(value, "TAKING MEDICATION|ON MEDS|RECEIVED TREATMENT|TREATMENT: YES|HAS ") ~ "Yes",
    stringr::str_detect(value, "MEDICATION: YES|LEARNING DISABILITY: YES|DEPRESSION: YES|LD - YES") ~ "Yes",
    TRUE ~ NA_character_
  )
}

clean_sex <- function(x) {
  value <- normalize_text(x)
  dplyr::case_when(
    value %in% c("1", "1.0", "M", "MALE") ~ "Male",
    value %in% c("2", "2.0", "F", "FEMALE") ~ "Female",
    TRUE ~ NA_character_
  )
}

clean_race <- function(x) {
  value <- normalize_text(x)
  dplyr::case_when(
    value %in% c("1", "1.0", "WHITE", "WHITE ALONE") ~ "White alone",
    value %in% c("2", "2.0", "BLACK", "BLACK ALONE", "BLACK OR AFRICAN AMERICAN") ~ "Black or African American alone",
    value %in% c("3", "3.0", "AIAN", "AMERICAN INDIAN/ALASKA NATIVE", "AMERICAN INDIAN OR ALASKA NATIVE ALONE") ~ "American Indian or Alaska Native alone",
    value %in% c("4", "4.0", "ASIAN", "ASIAN ALONE") ~ "Asian alone",
    value %in% c("5", "5.0", "NHPI", "NATIVE HAWAIIAN OR PACIFIC ISLANDER", "NATIVE HAWAIIAN AND OTHER PACIFIC ISLANDER ALONE") ~ "Native Hawaiian or Other Pacific Islander alone",
    value %in% c("7", "7.0", "TWO OR MORE RACES", "MULTIPLE RACES", "TWO+ RACES") ~ "Two or more races",
    TRUE ~ NA_character_
  )
}

clean_severity <- function(x) {
  value <- normalize_text(x)
  dplyr::case_when(
    value %in% c("1", "1.0", "MILD", "LOW") ~ "Mild",
    value %in% c("2", "2.0", "MODERATE", "MEDIUM") ~ "Moderate",
    value %in% c("3", "3.0", "SEVERE", "HIGH") ~ "Severe",
    TRUE ~ NA_character_
  )
}

clean_grades <- function(x) {
  value <- normalize_text(x) |> stringr::str_replace_all("'", "")
  dplyr::case_when(
    value %in% c("1", "1.0", "MOSTLY AS", "A", "A GRADES") ~ "Mostly A's",
    value %in% c("2", "2.0", "MOSTLY AS AND BS", "A/B", "MOSTLY AS & BS") ~ "Mostly A's and B's",
    value %in% c("3", "3.0", "MOSTLY BS AND CS", "B/C", "MOSTLY BS & CS") ~ "Mostly B's and C's",
    value %in% c("4", "4.0", "MOSTLY CS AND DS", "C/D", "MOSTLY CS & DS") ~ "Mostly C's and D's",
    value %in% c("5", "5.0", "MOSTLY DS OR LOWER", "D OR BELOW", "DS OR LOWER") ~ "Mostly D's or lower",
    value %in% c("6", "6.0", "SCHOOL DOES NOT GIVE GRADES", "NO LETTER GRADES", "N/A - SCHOOL") ~ "No letter grades",
    TRUE ~ NA_character_
  )
}

clean_school_contact <- function(x) {
  value <- normalize_text(x)
  dplyr::case_when(
    value %in% c("1", "1.0", "NONE", "0 CONTACTS", "NO CONTACTS") ~ "None",
    value %in% c("2", "2.0", "1 TIME", "ONE CONTACT", "1X") ~ "1 time",
    value %in% c("3", "3.0", "2 OR MORE TIMES", "2+ CONTACTS", "MULTIPLE CONTACTS") ~ "2 or more times",
    TRUE ~ NA_character_
  )
}

clean_ace1 <- function(x) {
  value <- normalize_text(x)
  dplyr::case_when(
    value %in% c("1", "1.0", "NEVER", "0 - NEVER") ~ "Never",
    value %in% c("2", "2.0", "RARELY", "INFREQUENT") ~ "Rarely",
    value %in% c("3", "3.0", "SOMEWHAT OFTEN", "SOMETIMES OFTEN", "OCCASIONAL") ~ "Somewhat often",
    value %in% c("4", "4.0", "VERY OFTEN", "FREQUENT") ~ "Very often",
    TRUE ~ NA_character_
  )
}

clean_sleep <- function(x) {
  value <- normalize_text(x)
  dplyr::case_when(
    value %in% c("1", "1.0", "UNDER 6 HOURS", "<6H", "LESS THAN SIX HOURS") ~ "Less than 6 hours",
    value %in% c("2", "2.0", "6 H", "6 HOURS", "SIX HOURS") ~ "6 hours",
    value %in% c("3", "3.0", "7 H", "7 HOURS", "SEVEN HOURS") ~ "7 hours",
    value %in% c("4", "4.0", "8 H", "8 HOURS", "EIGHT HOURS") ~ "8 hours",
    value %in% c("5", "5.0", "9 H", "9 HOURS", "NINE HOURS") ~ "9 hours",
    value %in% c("6", "6.0", "10 H", "10 HOURS", "TEN HOURS") ~ "10 hours",
    value %in% c("7", "7.0", "11+ HOURS", "11 OR MORE HOURS", "11+ H") ~ "11 or more hours",
    TRUE ~ NA_character_
  )
}

clean_screen_time <- function(x) {
  value <- normalize_text(x)
  dplyr::case_when(
    value %in% c("1", "1.0", "UNDER 1 HOUR", "<1H", "LESS THAN ONE HOUR") ~ "Less than 1 hour",
    value %in% c("2", "2.0", "1 HOUR", "1 H", "ONE HOUR") ~ "1 hour",
    value %in% c("3", "3.0", "2 HOURS", "2 H", "TWO HOURS") ~ "2 hours",
    value %in% c("4", "4.0", "3 HOURS", "3 H", "THREE HOURS") ~ "3 hours",
    value %in% c("5", "5.0", "4+ HOURS", "4 OR MORE HOURS", "4H+") ~ "4 or more hours",
    TRUE ~ NA_character_
  )
}

Estruturação, junção e variáveis derivadas

# Tabela auxiliar para dar significado geográfico ao código FIPS.
fips_lookup <- tibble::tibble(
  FIPSST = c("01", "02", "04", "05", "06", "08", "09", "10", "11", "12", "13", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "44", "45", "46", "47", "48", "49", "50", "51", "53", "54", "55", "56"),
  state = c("Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "District of Columbia", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming")
)

clean_data <- raw_data |>
  dplyr::transmute(
    hhid = as.character(HHID),
    fipsst = stringr::str_pad(stringr::str_extract(FIPSST, "\\d+"), width = 2, pad = "0"),
    year = readr::parse_number(YEAR),
    age_years = readr::parse_number(SC_AGE_YEARS),
    sex = clean_sex(SC_SEX),
    race = clean_race(SC_RACE_R),
    adhd_ever = clean_yes_no(K2Q31A),
    adhd_current = clean_yes_no(K2Q31B),
    adhd_severity = clean_severity(K2Q31C),
    adhd_medication = clean_yes_no(K2Q31D),
    adhd_behavioral_treatment = clean_yes_no(ADDTREAT),
    learning_disability = clean_yes_no(K2Q30A),
    depression = clean_yes_no(K2Q32A),
    anxiety = clean_yes_no(K2Q33A),
    grades = clean_grades(GRADES),
    repeated_grade = clean_yes_no(REPEATED),
    school_contact = clean_school_contact(K7Q04R_R),
    special_education_plan = clean_yes_no(K6Q15),
    ace1_frequency = clean_ace1(ACE1),
    ace3_parent_separation = clean_yes_no(ACE3),
    ace4_parent_death = clean_yes_no(ACE4),
    ace5_parent_incarceration = clean_yes_no(ACE5),
    ace6_domestic_violence = clean_yes_no(ACE6),
    ace7_neighborhood_violence = clean_yes_no(ACE7),
    ace8_household_mental_illness = clean_yes_no(ACE8),
    ace9_household_substance_problem = clean_yes_no(ACE9),
    ace10_racial_unfair_treatment = clean_yes_no(ACE10),
    ace11_health_unfair_treatment = clean_yes_no(ACE11),
    sleep = clean_sleep(HOURSLEEP),
    screen_time = clean_screen_time(SCREENTIME),
    fpl_ratio = readr::parse_number(FPL_I1)
  ) |>
  dplyr::left_join(fips_lookup, by = c("fipsst" = "FIPSST")) |>
  dplyr::mutate(
    age_group = cut(age_years, breaks = c(-Inf, 5, 11, 17), labels = c("0-5", "6-11", "12-17")),
    sex = factor(sex, levels = c("Male", "Female")),
    race = forcats::fct_relevel(factor(race), "White alone", "Black or African American alone"),
    adhd_severity = factor(adhd_severity, levels = c("Mild", "Moderate", "Severe")),
    grades = factor(grades, levels = c("Mostly A's", "Mostly A's and B's", "Mostly B's and C's", "Mostly C's and D's", "Mostly D's or lower", "No letter grades")),
    school_contact = factor(school_contact, levels = c("None", "1 time", "2 or more times")),
    sleep = factor(sleep, levels = c("Less than 6 hours", "6 hours", "7 hours", "8 hours", "9 hours", "10 hours", "11 or more hours"), ordered = TRUE),
    screen_time = factor(screen_time, levels = c("Less than 1 hour", "1 hour", "2 hours", "3 hours", "4 or more hours"), ordered = TRUE),
    ace1_hardship = dplyr::case_when(
      ace1_frequency == "Never" ~ "No",
      !is.na(ace1_frequency) ~ "Yes",
      TRUE ~ NA_character_
    ),
    grade_risk = dplyr::case_when(
      grades %in% c("Mostly C's and D's", "Mostly D's or lower") ~ TRUE,
      grades %in% c("Mostly A's", "Mostly A's and B's", "Mostly B's and C's") ~ FALSE,
      TRUE ~ NA
    ),
    school_contact_2plus = dplyr::case_when(
      school_contact == "2 or more times" ~ TRUE,
      school_contact %in% c("None", "1 time") ~ FALSE,
      TRUE ~ NA
    )
  )

comorbidity_vars <- c("learning_disability", "depression", "anxiety")
school_risk_vars <- c("repeated_grade", "grade_risk", "school_contact_2plus")
ace_vars <- c(
  "ace1_hardship", "ace3_parent_separation", "ace4_parent_death",
  "ace5_parent_incarceration", "ace6_domestic_violence", "ace7_neighborhood_violence",
  "ace8_household_mental_illness", "ace9_household_substance_problem",
  "ace10_racial_unfair_treatment", "ace11_health_unfair_treatment"
)

clean_data <- clean_data |>
  dplyr::mutate(
    comorbidity_answered = rowSums(!is.na(dplyr::across(dplyr::all_of(comorbidity_vars)))),
    comorbidity_count = dplyr::if_else(
      comorbidity_answered > 0,
      rowSums(dplyr::across(dplyr::all_of(comorbidity_vars)) == "Yes", na.rm = TRUE),
      NA_integer_
    ),
    ace_answered = rowSums(!is.na(dplyr::across(dplyr::all_of(ace_vars)))),
    ace_exposure_count = dplyr::if_else(
      ace_answered > 0,
      rowSums(dplyr::across(dplyr::all_of(ace_vars)) == "Yes", na.rm = TRUE),
      NA_integer_
    ),
    ace_2plus = dplyr::case_when(
      ace_exposure_count >= 2 ~ TRUE,
      !is.na(ace_exposure_count) ~ FALSE,
      TRUE ~ NA
    ),
    ace_band = dplyr::case_when(
      ace_exposure_count == 0 ~ "0 ACEs",
      ace_exposure_count == 1 ~ "1 ACE",
      ace_exposure_count == 2 ~ "2 ACEs",
      ace_exposure_count >= 3 ~ "3+ ACEs",
      TRUE ~ NA_character_
    ),
    fpl_band = dplyr::case_when(
      fpl_ratio <= 100 ~ "Até 100% FPL",
      fpl_ratio <= 200 ~ "101-200% FPL",
      fpl_ratio <= 400 ~ "201-400% FPL",
      fpl_ratio > 400 ~ "Acima de 400% FPL",
      TRUE ~ NA_character_
    ),
    school_vulnerability_answered = rowSums(!is.na(dplyr::across(dplyr::all_of(school_risk_vars)))),
    school_vulnerability_count = dplyr::if_else(
      school_vulnerability_answered > 0,
      rowSums(dplyr::across(dplyr::all_of(school_risk_vars)) == TRUE, na.rm = TRUE),
      NA_integer_
    )
  ) |>
  dplyr::mutate(
    ace_band = factor(ace_band, levels = c("0 ACEs", "1 ACE", "2 ACEs", "3+ ACEs"), ordered = TRUE),
    fpl_band = factor(fpl_band, levels = c("Até 100% FPL", "101-200% FPL", "201-400% FPL", "Acima de 400% FPL"), ordered = TRUE)
  )

# Funções para a matriz estruturada: 0/1 para categorias e escala [0, 1] para medidas ordenadas.
min_max_01 <- function(x) {
  x <- as.numeric(x)
  limits <- range(x, na.rm = TRUE)
  if (!all(is.finite(limits))) return(rep(NA_real_, length(x)))
  if (limits[1] == limits[2]) return(ifelse(is.na(x), NA_real_, 1))
  (x - limits[1]) / (limits[2] - limits[1])
}

one_hot_encode <- function(x, prefix) {
  values <- as.character(x)
  levels <- sort(unique(values[!is.na(values)]))
  encoded <- lapply(levels, function(level) {
    ifelse(is.na(values), NA_real_, as.numeric(values == level))
  })
  names(encoded) <- paste0(prefix, "__", make.names(levels))
  tibble::as_tibble(encoded)
}

yes_no_to_binary <- function(x) {
  dplyr::case_when(
    x == "Yes" ~ 1,
    x == "No" ~ 0,
    TRUE ~ NA_real_
  )
}

structured_data <- tibble::tibble(
  hhid = clean_data$hhid,
  year_2023 = ifelse(is.na(clean_data$year), NA_real_, as.numeric(clean_data$year == 2023)),
  age_years_01 = min_max_01(clean_data$age_years),
  fpl_ratio_01 = min_max_01(clean_data$fpl_ratio),
  ace1_frequency_01 = min_max_01(match(clean_data$ace1_frequency, c("Never", "Rarely", "Somewhat often", "Very often"))),
  sleep_01 = min_max_01(as.integer(clean_data$sleep)),
  screen_time_01 = min_max_01(as.integer(clean_data$screen_time)),
  adhd_ever_01 = yes_no_to_binary(clean_data$adhd_ever),
  adhd_current_01 = yes_no_to_binary(clean_data$adhd_current),
  adhd_medication_01 = yes_no_to_binary(clean_data$adhd_medication),
  adhd_behavioral_treatment_01 = yes_no_to_binary(clean_data$adhd_behavioral_treatment),
  learning_disability_01 = yes_no_to_binary(clean_data$learning_disability),
  depression_01 = yes_no_to_binary(clean_data$depression),
  anxiety_01 = yes_no_to_binary(clean_data$anxiety),
  repeated_grade_01 = yes_no_to_binary(clean_data$repeated_grade),
  special_education_plan_01 = yes_no_to_binary(clean_data$special_education_plan),
  ace3_parent_separation_01 = yes_no_to_binary(clean_data$ace3_parent_separation),
  ace4_parent_death_01 = yes_no_to_binary(clean_data$ace4_parent_death),
  ace5_parent_incarceration_01 = yes_no_to_binary(clean_data$ace5_parent_incarceration),
  ace6_domestic_violence_01 = yes_no_to_binary(clean_data$ace6_domestic_violence),
  ace7_neighborhood_violence_01 = yes_no_to_binary(clean_data$ace7_neighborhood_violence),
  ace8_household_mental_illness_01 = yes_no_to_binary(clean_data$ace8_household_mental_illness),
  ace9_household_substance_problem_01 = yes_no_to_binary(clean_data$ace9_household_substance_problem),
  ace10_racial_unfair_treatment_01 = yes_no_to_binary(clean_data$ace10_racial_unfair_treatment),
  ace11_health_unfair_treatment_01 = yes_no_to_binary(clean_data$ace11_health_unfair_treatment)
)

structured_data <- dplyr::bind_cols(
  structured_data,
  one_hot_encode(clean_data$state, "state"),
  one_hot_encode(clean_data$sex, "sex"),
  one_hot_encode(clean_data$race, "race"),
  one_hot_encode(clean_data$adhd_severity, "adhd_severity"),
  one_hot_encode(clean_data$grades, "grades"),
  one_hot_encode(clean_data$school_contact, "school_contact")
)

non_identifier_columns <- setdiff(names(structured_data), "hhid")
stopifnot(all(vapply(structured_data[non_identifier_columns], function(x) all(is.na(x) | (x >= 0 & x <= 1)), logical(1))))

structured_display_data <- structured_data |>
  dplyr::mutate(
    .populated_features = rowSums(!is.na(dplyr::across(dplyr::all_of(non_identifier_columns))))
  ) |>
  dplyr::arrange(dplyr::desc(.populated_features)) |>
  dplyr::select(-.populated_features)

# Universo principal: crianças com idade e status de TDAH informados.
analysis_data <- clean_data |>
  dplyr::filter(!is.na(age_years), !is.na(adhd_ever))

stopifnot(nrow(analysis_data) > 0)

safe_mean <- function(x) {
  if (all(is.na(x))) NA_real_ else mean(x, na.rm = TRUE)
}

headline_n <- nrow(analysis_data)
headline_adhd_rate <- safe_mean(analysis_data$adhd_ever == "Yes")
headline_adhd_n <- sum(analysis_data$adhd_ever == "Yes", na.rm = TRUE)
headline_ace_2plus <- safe_mean(analysis_data$ace_2plus)
Universo analítico51,498
TDAH informado10.4%
Com 2+ ACEs20.7%
Primeiro sinal. Após excluir apenas registros sem idade ou status de TDAH, há 5,338 crianças com diagnóstico informado. Esse número abre a investigação, mas não é uma conclusão clínica: o objetivo é observar padrões de coexistência e não atribuir causa.

Validação da estrutura obtida

quality_map <- c(
  SC_AGE_YEARS = "age_years", SC_SEX = "sex", SC_RACE_R = "race",
  K2Q31A = "adhd_ever", K2Q31C = "adhd_severity", K2Q31D = "adhd_medication",
  ADDTREAT = "adhd_behavioral_treatment", K2Q30A = "learning_disability",
  K2Q32A = "depression", GRADES = "grades", K7Q04R_R = "school_contact",
  ACE1 = "ace1_frequency", ACE6 = "ace6_domestic_violence", HOURSLEEP = "sleep",
  SCREENTIME = "screen_time", FPL_I1 = "fpl_ratio"
)

quality_table <- tibble::tibble(
  source_variable = names(quality_map),
  cleaned_variable = unname(quality_map),
  missing_raw = vapply(names(quality_map), function(x) sum(is.na(raw_data[[x]])), integer(1)),
  missing_after_cleaning = vapply(unname(quality_map), function(x) sum(is.na(clean_data[[x]])), integer(1)),
  non_missing_not_standardized = vapply(seq_along(quality_map), function(i) {
    sum(!is.na(raw_data[[names(quality_map)[i]]]) & is.na(clean_data[[quality_map[[i]]]]))
  }, integer(1))
)

values_not_standardized <- sum(quality_table$non_missing_not_standardized)

join_audit <- clean_data |>
  dplyr::summarise(
    records = dplyr::n(),
    records_without_state_match = sum(is.na(state))
  )
Validação: a matriz estruturada preserva 55,162 registros. Todos os campos, exceto o identificador HHID, foram verificados para conter apenas 0, 1, valores normalizados entre 0 e 1 ou ausência real. A auditoria encontrou 0 valores não ausentes sem regra de padronização e 0 registros sem correspondência de estado.

Matriz estruturada para análise

DT::datatable(
  structured_display_data |>
    dplyr::slice_head(n = 10),
  caption = "Tabela 2. Matriz estruturada: todas as variáveis transformadas em indicadores binários ou escalas normalizadas. A primeira página prioriza registros completos; essa ordenação é apenas visual. Use a barra horizontal para visualizar todas as colunas.",
  options = list(pageLength = 10, scrollX = TRUE, scrollY = "420px")
)

Dicionário da matriz estruturada

Esta descrição se refere à nova matriz exibida acima, e não aos nomes originais da importação. Há três convenções de leitura: em indicadores binários, 1 significa que a característica ou categoria está presente e 0 significa que não está; nas escalas normalizadas, 0 representa o menor nível observado e 1 o maior; NA permanece como resposta ausente, não aplicável ou não informada — nunca como zero.

Identificação, geografia e demografia

  • hhid: identificador do registro. É preservado para rastreabilidade e não entra como medida analítica.
  • year_2023: indicador do ano de referência; 1 representa 2023.
  • state__<nome_do_estado>: família criada a partir de FIPSST. Cada estado possui uma coluna; por exemplo, state__Florida = 1 indica residência na Flórida e 0 indica outro estado.
  • age_years_01: idade em anos, normalizada por min–max para o intervalo de 0 a 1.
  • sex__Female e sex__Male: indicadores de sexo criados por one-hot encoding.
  • race__<categoria>: indicadores de raça detalhada criados por one-hot encoding. Apenas a categoria correspondente recebe 1 em cada registro com resposta válida.

Núcleo clínico do TDAH

  • adhd_ever_01: profissional já informou diagnóstico de TDAH/ADD (1 = sim).
  • adhd_current_01: TDAH/ADD informado como condição atual (1 = sim).
  • adhd_severity__Mild, adhd_severity__Moderate e adhd_severity__Severe: gravidade atual relatada, convertida em três indicadores mutuamente exclusivos.
  • adhd_medication_01: uso atual de medicamento para TDAH/ADD (1 = sim).
  • adhd_behavioral_treatment_01: tratamento comportamental para TDAH/ADD relatado (1 = sim).

Comorbidades e indicadores escolares

  • learning_disability_01, depression_01 e anxiety_01: diagnóstico informado de deficiência de aprendizagem, depressão e ansiedade, respectivamente (1 = sim).
  • grades__<categoria>: padrão de notas transformado em um indicador para cada categoria, como grades__Mostly.A.s ou grades__Mostly.C.s.and.D.s.
  • repeated_grade_01: repetiu alguma série desde o início do jardim de infância (1 = sim).
  • school_contact__None, school_contact__X1.time e school_contact__X2.or.more.times: quantidade de contatos da escola com o domicílio por problemas escolares, codificada em indicadores mutuamente exclusivos.
  • special_education_plan_01: já teve plano de educação especial ou intervenção precoce (1 = sim).

Experiências adversas na infância (ACEs)

  • ace1_frequency_01: frequência de dificuldade para cobrir necessidades básicas, da menor à maior frequência, normalizada entre 0 e 1.
  • ace3_parent_separation_01: separação ou divórcio de pais/responsáveis (1 = sim).
  • ace4_parent_death_01: falecimento de pai/mãe ou responsável (1 = sim).
  • ace5_parent_incarceration_01: encarceramento de pai/mãe ou responsável (1 = sim).
  • ace6_domestic_violence_01: convivência com violência doméstica (1 = sim).
  • ace7_neighborhood_violence_01: vivência de violência no bairro (1 = sim).
  • ace8_household_mental_illness_01: convivência com problema de saúde mental no domicílio (1 = sim).
  • ace9_household_substance_problem_01: convivência com problema de álcool ou outras substâncias no domicílio (1 = sim).
  • ace10_racial_unfair_treatment_01: tratamento injusto por raça/etnia relatado (1 = sim).
  • ace11_health_unfair_treatment_01: tratamento injusto em serviços de saúde relatado (1 = sim).

Rotina e condição socioeconômica

  • sleep_01: categoria de horas de sono em noites de semana, ordenada e normalizada de 0 a 1.
  • screen_time_01: categoria de tempo diário de tela, ordenada e normalizada de 0 a 1.
  • fpl_ratio_01: razão de pobreza familiar (Federal Poverty Level), convertida para escala min–max de 0 a 1. Valores maiores representam maior razão em relação ao limiar federal de pobreza.
Regra de interpretação: colunas da mesma família — como state__, race__, grades__, school_contact__ e adhd_severity__ — devem ser lidas conjuntamente. Elas representam uma única variável categórica distribuída em indicadores, e não várias características independentes.

4. Análise exploratória

Eixo 1 — Identificação, geografia e demografia

HHID garante a rastreabilidade de cada registro sem entrar como variável analítica. FIPSST localiza o estado e será usado para uma exploração geográfica mais adiante. Aqui, o ponto de partida é demográfico: idade, sexo e raça ajudam a evitar comparações entre grupos com composições muito diferentes. Se o diagnóstico informado estiver concentrado em certos perfis, essa distribuição precisa enquadrar todas as interpretações seguintes.

adhd_by_age_sex <- analysis_data |>
  dplyr::filter(!is.na(age_group), !is.na(sex)) |>
  dplyr::group_by(age_group, sex) |>
  dplyr::summarise(
    n = dplyr::n(),
    adhd_prevalence = safe_mean(adhd_ever == "Yes"),
    .groups = "drop"
  )

peak_adhd_group <- adhd_by_age_sex |>
  dplyr::slice_max(adhd_prevalence, n = 1, with_ties = FALSE)

adhd_by_race <- analysis_data |>
  dplyr::filter(!is.na(race)) |>
  dplyr::group_by(race) |>
  dplyr::summarise(
    n = dplyr::n(),
    adhd_prevalence = safe_mean(adhd_ever == "Yes"),
    .groups = "drop"
  ) |>
  dplyr::arrange(adhd_prevalence)

plot_adhd_demographics <- ggplot2::ggplot(
  adhd_by_age_sex,
  ggplot2::aes(x = age_group, y = adhd_prevalence, fill = sex)
) +
  ggplot2::geom_col(position = ggplot2::position_dodge(width = 0.75), width = 0.65) +
  ggplot2::scale_y_continuous(labels = scales::label_percent(accuracy = 1)) +
  ggplot2::scale_fill_manual(values = c("Male" = "#2878B5", "Female" = "#E07A9A")) +
  ggplot2::labs(
    title = "Diagnóstico informado de TDAH por faixa etária e sexo",
    subtitle = "Percentuais não ponderados entre registros com informação válida",
    x = "Faixa etária (anos)", y = "Percentual com TDAH informado", fill = "Sexo"
  ) +
  ggplot2::theme_minimal(base_size = 12) +
  ggplot2::theme(legend.position = "top")

plot_adhd_demographics

ggplot2::ggplot(adhd_by_race, ggplot2::aes(x = race, y = adhd_prevalence, color = adhd_prevalence, size = n)) +
  ggplot2::geom_point(alpha = 0.9) +
  ggplot2::coord_flip() +
  ggplot2::scale_color_gradient(low = "#E9BD27", high = "#990000", labels = scales::label_percent(accuracy = 1)) +
  ggplot2::scale_y_continuous(labels = scales::label_percent(accuracy = 1)) +
  ggplot2::labs(
    title = "Diagnóstico informado de TDAH por raça detalhada",
    subtitle = "O tamanho do ponto representa o número de registros no grupo",
    x = NULL, y = "Percentual com TDAH informado", color = "Percentual", size = "Registros"
  ) +
  ggplot2::theme_minimal(base_size = 12)

Leitura do eixo demográfico. O maior percentual observado está no grupo de Male entre 12-17 anos (21.5%). A leitura por raça mostra por que percentuais devem ser acompanhados do tamanho do grupo. São associações descritivas, não explicações causais, e não incorporam pesos complexos da pesquisa.

Eixo 2 — Núcleo clínico do TDAH

O núcleo clínico não se resume a “sim” ou “não”. K2Q31A informa o diagnóstico alguma vez relatado; K2Q31B indica situação atual; K2Q31C registra gravidade; e K2Q31D e ADDTREAT descrevem duas modalidades de cuidado. A sequência abaixo revela como essas respostas se distribuem sem presumir que uma modalidade seja superior à outra.

adhd_clinical <- analysis_data |>
  dplyr::filter(adhd_ever == "Yes")

clinical_summary <- adhd_clinical |>
  dplyr::summarise(
    n_diagnosed = dplyr::n(),
    current_adhd = safe_mean(adhd_current == "Yes"),
    medication = safe_mean(adhd_medication == "Yes"),
    behavioral_treatment = safe_mean(adhd_behavioral_treatment == "Yes")
  )

clinical_treatment <- adhd_clinical |>
  dplyr::filter(!is.na(adhd_severity)) |>
  dplyr::select(adhd_severity, adhd_medication, adhd_behavioral_treatment) |>
  tidyr::pivot_longer(-adhd_severity, names_to = "care", values_to = "received") |>
  dplyr::group_by(adhd_severity, care) |>
  dplyr::summarise(proportion = safe_mean(received == "Yes"), n = dplyr::n(), .groups = "drop") |>
  dplyr::mutate(care = dplyr::recode(care,
    adhd_medication = "Medicação atual",
    adhd_behavioral_treatment = "Tratamento comportamental"
  ))

knitr::kable(
  clinical_summary |>
    dplyr::mutate(dplyr::across(-n_diagnosed, scales::percent, accuracy = 0.1)),
  caption = "Cascata clínica entre registros com TDAH informado."
)
Cascata clínica entre registros com TDAH informado.
n_diagnosed current_adhd medication behavioral_treatment
5338 94.3% 54.5% 45.0%
ggplot2::ggplot(clinical_treatment, ggplot2::aes(x = adhd_severity, y = proportion, fill = care)) +
  ggplot2::geom_col(position = ggplot2::position_dodge(width = 0.75), width = 0.65) +
  ggplot2::scale_y_continuous(labels = scales::label_percent(accuracy = 1)) +
  ggplot2::scale_fill_manual(values = c("Medicação atual" = "#990000", "Tratamento comportamental" = "#E9BD27")) +
  ggplot2::labs(
    title = "Modalidades de cuidado por gravidade relatada de TDAH",
    subtitle = "Percentual entre casos com gravidade e modalidade de cuidado informadas",
    x = "Gravidade relatada", y = "Percentual", fill = NULL
  ) +
  ggplot2::theme_minimal(base_size = 12) +
  ggplot2::theme(legend.position = "top")

Leitura clínica. Entre os 5,338 registros com TDAH informado, 94.3% relatam a condição como atual. A comparação entre gravidade, medicação e tratamento comportamental descreve padrões de cuidado relatados; ela não mede efetividade, adesão nem qualidade da assistência.

Eixo 3 — Comorbidades: o diagnóstico não explica tudo

Deficiência de aprendizagem, depressão e ansiedade foram organizadas como três comorbidades distintas. Em vez de somá-las imediatamente, primeiro observamos a prevalência de cada uma por grupo de TDAH. Isso mostra onde se concentram os contrastes antes de usar a contagem agregada de comorbidades.

comorbidity_long <- analysis_data |>
  dplyr::select(adhd_ever, learning_disability, depression, anxiety) |>
  tidyr::pivot_longer(-adhd_ever, names_to = "condition", values_to = "reported") |>
  dplyr::group_by(adhd_ever, condition) |>
  dplyr::summarise(prevalence = safe_mean(reported == "Yes"), n = dplyr::n(), .groups = "drop") |>
  dplyr::mutate(condition = dplyr::recode(condition,
    learning_disability = "Deficiência de aprendizagem",
    depression = "Depressão",
    anxiety = "Ansiedade"
  ))

comorbidity_gap <- comorbidity_long |>
  dplyr::select(adhd_ever, condition, prevalence) |>
  tidyr::pivot_wider(names_from = adhd_ever, values_from = prevalence) |>
  dplyr::mutate(gap = Yes - No) |>
  dplyr::slice_max(gap, n = 1, with_ties = FALSE)

ggplot2::ggplot(comorbidity_long, ggplot2::aes(x = adhd_ever, y = condition, fill = prevalence)) +
  ggplot2::geom_tile(color = "white", linewidth = 1.1) +
  ggplot2::geom_text(ggplot2::aes(label = scales::percent(prevalence, accuracy = 0.1)), color = "#1c1a1a", fontface = "bold") +
  ggplot2::scale_fill_gradient(low = "#FFF4C8", high = "#990000", labels = scales::label_percent(accuracy = 1)) +
  ggplot2::labs(
    title = "Comorbidades relatadas por status de TDAH",
    x = "TDAH informado", y = NULL, fill = "Percentual"
  ) +
  ggplot2::theme_minimal(base_size = 12)

O contraste mais forte. A maior diferença de prevalência ocorre em Ansiedade, com distância de 39.2% entre os grupos. Esse resultado ajuda a explicar por que a análise escolar deve considerar necessidades concomitantes, mas não permite concluir que uma condição tenha causado a outra.

Eixo 4 — Desempenho escolar: onde as diferenças aparecem?

O passo seguinte testa uma interpretação plausível: se o TDAH informado aparece junto de mais necessidades educacionais e de saúde, os indicadores escolares devem revelar contrastes entre os grupos. A tabela evita reduzir a história a um único resultado; o gráfico destaca os quatro indicadores mais diretos de acompanhamento escolar.

outcomes_numeric <- analysis_data |>
  dplyr::group_by(adhd_ever) |>
  dplyr::summarise(
    n = dplyr::n(),
    mean_comorbidities = safe_mean(comorbidity_count),
    repeated_grade = safe_mean(repeated_grade == "Yes"),
    special_education_plan = safe_mean(special_education_plan == "Yes"),
    lower_grades = safe_mean(grade_risk),
    school_contact_2plus = safe_mean(school_contact_2plus),
    .groups = "drop"
  )

outcomes_by_adhd <- outcomes_numeric |>
  dplyr::mutate(dplyr::across(c(repeated_grade, special_education_plan, lower_grades, school_contact_2plus), scales::percent, accuracy = 0.1))

school_gap <- outcomes_numeric |>
  dplyr::select(adhd_ever, school_contact_2plus) |>
  tidyr::pivot_wider(names_from = adhd_ever, values_from = school_contact_2plus) |>
  dplyr::mutate(gap = Yes - No) |>
  dplyr::pull(gap)
school_gap_direction <- ifelse(school_gap >= 0, "maior", "menor")

DT::datatable(
  outcomes_by_adhd,
  caption = "Indicadores escolares e comorbidades por status de TDAH informado.",
  rownames = FALSE,
  options = list(dom = "tip", pageLength = 10)
)
outcomes_for_plot <- analysis_data |>
  dplyr::group_by(adhd_ever) |>
  dplyr::summarise(
    `Repetiu alguma série` = safe_mean(repeated_grade == "Yes"),
    `Plano de educação especial` = safe_mean(special_education_plan == "Yes"),
    `Notas C/D ou inferiores` = safe_mean(grade_risk),
    `2+ contatos da escola` = safe_mean(school_contact_2plus == TRUE),
    .groups = "drop"
  ) |>
  tidyr::pivot_longer(-adhd_ever, names_to = "indicator", values_to = "proportion")

plot_school_outcomes <- ggplot2::ggplot(
  outcomes_for_plot,
  ggplot2::aes(x = indicator, y = proportion, fill = adhd_ever,
               text = paste0("Indicador: ", indicator, "<br>Status TDAH: ", adhd_ever,
                             "<br>Percentual: ", scales::percent(proportion, accuracy = 0.1)))
) +
  ggplot2::geom_col(position = ggplot2::position_dodge(width = 0.75), width = 0.65) +
  ggplot2::coord_flip() +
  ggplot2::scale_y_continuous(labels = scales::label_percent(accuracy = 1)) +
  ggplot2::scale_fill_manual(values = c("No" = "#9ECAE1", "Yes" = "#E6550D")) +
  ggplot2::labs(
    title = "Indicadores escolares por status de TDAH informado",
    x = NULL, y = "Percentual não ponderado", fill = "TDAH informado"
  ) +
  ggplot2::theme_minimal(base_size = 12) +
  ggplot2::theme(legend.position = "top")

plot_school_outcomes

plotly::ggplotly(plot_school_outcomes, tooltip = "text")
school_flow <- analysis_data |>
  dplyr::filter(!is.na(adhd_ever), !is.na(school_contact_2plus)) |>
  dplyr::count(adhd_ever, school_contact_2plus, name = "value") |>
  dplyr::mutate(
    source = ifelse(adhd_ever == "No", 0L, 1L),
    target = ifelse(school_contact_2plus, 3L, 2L)
  )

plotly::plot_ly(
  type = "sankey",
  orientation = "h",
  node = list(
    label = c("TDAH não informado", "TDAH informado", "0-1 contato da escola", "2+ contatos da escola"),
    color = c("#9ECAE1", "#E6550D", "#C7DCEB", "#D9AA00"),
    pad = 18,
    thickness = 22
  ),
  link = list(
    source = school_flow$source,
    target = school_flow$target,
    value = school_flow$value,
    color = ifelse(school_flow$adhd_ever == "Yes", "rgba(230,85,13,0.42)", "rgba(40,120,181,0.30)")
  )
) |>
  plotly::layout(title = "Fluxo interativo: status de TDAH e contatos da escola", font = list(size = 13))
O que muda na escola? A diferença mais visível para acompanhamento é a proporção de crianças com dois ou mais contatos da escola: ela é 26.4% maior no grupo com TDAH informado. O diagrama de fluxo transforma esse contraste em caminhos visuais. O padrão deve ser entendido como necessidade potencial de apoio e investigação adicional, não como desempenho individual determinístico.

Eixo 5 — Traumas na infância (ACEs) e desempenho escolar

As ACEs registram experiências difíceis no ambiente familiar, comunitário ou social. Elas não medem toda a história de vida da criança, mas permitem investigar um ponto central do projeto: à medida que exposições adversas se acumulam, os indicadores escolares também se alteram? A resposta abaixo é associativa, não causal.

ace_by_adhd <- analysis_data |>
  dplyr::group_by(adhd_ever) |>
  dplyr::summarise(
    n = dplyr::n(),
    mean_ace_exposures = safe_mean(ace_exposure_count),
    ace_2plus = safe_mean(ace_2plus),
    .groups = "drop"
  )

ace_gap <- ace_by_adhd |>
  dplyr::select(adhd_ever, mean_ace_exposures) |>
  tidyr::pivot_wider(names_from = adhd_ever, values_from = mean_ace_exposures) |>
  dplyr::mutate(gap = Yes - No) |>
  dplyr::pull(gap)

knitr::kable(
  ace_by_adhd |>
    dplyr::mutate(ace_2plus = scales::percent(ace_2plus, accuracy = 0.1)),
  caption = "Número médio de ACEs e proporção com duas ou mais exposições."
)
Número médio de ACEs e proporção com duas ou mais exposições.
adhd_ever n mean_ace_exposures ace_2plus
No 46160 0.8237252 17.9%
Yes 5338 1.8337126 44.6%
Uma interpretação possível. A média de exposições adversas selecionadas difere em 1.01 entre os grupos. Esse contraste não permite afirmar que as ACEs expliquem o TDAH, mas reforça a utilidade de olhar para o contexto familiar e comunitário ao interpretar indicadores de saúde e escola.
ace_school_summary <- analysis_data |>
  dplyr::filter(!is.na(ace_band)) |>
  dplyr::group_by(ace_band, adhd_ever) |>
  dplyr::summarise(
    n = dplyr::n(),
    `2+ contatos da escola` = safe_mean(school_contact_2plus),
    `Notas C/D ou inferiores` = safe_mean(grade_risk),
    `Vulnerabilidades escolares médias` = safe_mean(school_vulnerability_count),
    .groups = "drop"
  )

knitr::kable(
  ace_school_summary |>
    dplyr::mutate(dplyr::across(c(`2+ contatos da escola`, `Notas C/D ou inferiores`), scales::percent, accuracy = 0.1)),
  caption = "Indicadores escolares por faixa de ACEs e status de TDAH informado."
)
Indicadores escolares por faixa de ACEs e status de TDAH informado.
ace_band adhd_ever n 2+ contatos da escola Notas C/D ou inferiores Vulnerabilidades escolares médias
0 ACEs No 23670 5.0% 1.2% 0.0579414
0 ACEs Yes 1481 23.8% 8.8% 0.3005780
1 ACE No 13569 8.7% 3.2% 0.1106307
1 ACE Yes 1442 32.0% 14.6% 0.4274981
2 ACEs No 4320 12.4% 5.6% 0.1686476
2 ACEs Yes 916 38.1% 19.0% 0.5283237
3+ ACEs No 3802 20.9% 10.0% 0.2889318
3+ ACEs Yes 1435 48.2% 25.8% 0.6835536
mode_value <- function(x) names(sort(table(x), decreasing = TRUE))[1]

school_model_data <- analysis_data |>
  dplyr::filter(
    !is.na(school_contact_2plus), !is.na(ace_band), !is.na(adhd_ever),
    !is.na(age_years), !is.na(sex), !is.na(race), !is.na(fpl_ratio)
  ) |>
  dplyr::mutate(adhd_ever = factor(adhd_ever, levels = c("No", "Yes")))

school_contact_model <- stats::glm(
  school_contact_2plus ~ ace_band * adhd_ever + age_years + sex + race + fpl_ratio,
  data = school_model_data,
  family = stats::binomial()
)

prediction_grid <- tidyr::crossing(
  ace_band = factor(levels(analysis_data$ace_band), levels = levels(analysis_data$ace_band), ordered = TRUE),
  adhd_ever = factor(c("No", "Yes"), levels = c("No", "Yes"))
) |>
  dplyr::mutate(
    age_years = median(school_model_data$age_years),
    sex = factor(mode_value(school_model_data$sex), levels = levels(school_model_data$sex)),
    race = factor(mode_value(school_model_data$race), levels = levels(school_model_data$race)),
    fpl_ratio = median(school_model_data$fpl_ratio)
  )

predicted_link <- stats::predict(school_contact_model, newdata = prediction_grid, type = "link", se.fit = TRUE)
prediction_grid <- prediction_grid |>
  dplyr::mutate(
    probability = stats::plogis(predicted_link$fit),
    lower = stats::plogis(predicted_link$fit - 1.96 * predicted_link$se.fit),
    upper = stats::plogis(predicted_link$fit + 1.96 * predicted_link$se.fit)
  )

adjusted_ace_gap <- prediction_grid |>
  dplyr::filter(ace_band == "3+ ACEs") |>
  dplyr::select(adhd_ever, probability) |>
  tidyr::pivot_wider(names_from = adhd_ever, values_from = probability) |>
  dplyr::mutate(gap = Yes - No) |>
  dplyr::pull(gap)

ggplot2::ggplot(prediction_grid, ggplot2::aes(x = ace_band, y = probability, color = adhd_ever, group = adhd_ever, fill = adhd_ever)) +
  ggplot2::geom_ribbon(ggplot2::aes(ymin = lower, ymax = upper), alpha = 0.16, color = NA) +
  ggplot2::geom_line(linewidth = 1.1) +
  ggplot2::geom_point(size = 3) +
  ggplot2::scale_y_continuous(labels = scales::label_percent(accuracy = 1)) +
  ggplot2::scale_color_manual(values = c("No" = "#2878B5", "Yes" = "#990000")) +
  ggplot2::scale_fill_manual(values = c("No" = "#2878B5", "Yes" = "#990000")) +
  ggplot2::labs(
    title = "ACEs e probabilidade ajustada de contatos escolares frequentes",
    subtitle = "Modelo logístico ajustado por idade, sexo, raça e razão de pobreza familiar",
    x = "Exposições adversas selecionadas", y = "Probabilidade prevista de 2+ contatos", color = "TDAH informado", fill = "TDAH informado"
  ) +
  ggplot2::theme_minimal(base_size = 12) +
  ggplot2::theme(legend.position = "top")

O que a associação ajustada acrescenta. Mesmo ao comparar perfis com a mesma idade, sexo, raça e FPL de referência, o modelo mostra como a probabilidade prevista de contatos escolares frequentes muda ao longo das faixas de ACEs. Na faixa de 3+ ACEs, a diferença prevista entre os grupos com e sem TDAH é 27.8%. Isso é compatível com a hipótese de que adversidade e escola se conectam, mas não prova que as ACEs tenham causado os desfechos.

Eixo 6 — Rotina e razão de pobreza familiar

Sono, tempo de tela e razão de pobreza familiar são dimensões de contexto, não marcadores de responsabilidade individual. Nesta etapa, verificamos como esses fatores se distribuem segundo TDAH e faixas de ACEs. A leitura conjunta evita interpretar rotina fora das condições materiais e das experiências relatadas.

routine_by_adhd <- analysis_data |>
  dplyr::filter(!is.na(sleep), !is.na(screen_time)) |>
  dplyr::count(adhd_ever, sleep, screen_time, name = "n") |>
  dplyr::group_by(adhd_ever) |>
  dplyr::mutate(proportion = n / sum(n)) |>
  dplyr::ungroup()

fpl_ace_profile <- analysis_data |>
  dplyr::filter(!is.na(fpl_band), !is.na(ace_band)) |>
  dplyr::count(fpl_band, ace_band, adhd_ever, name = "n") |>
  dplyr::group_by(fpl_band, adhd_ever) |>
  dplyr::mutate(proportion = n / sum(n)) |>
  dplyr::ungroup()

ggplot2::ggplot(routine_by_adhd, ggplot2::aes(x = sleep, y = proportion, fill = screen_time)) +
  ggplot2::geom_col(position = "stack") +
  ggplot2::facet_wrap(~ adhd_ever) +
  ggplot2::scale_y_continuous(labels = scales::label_percent(accuracy = 1)) +
  ggplot2::scale_fill_brewer(palette = "YlOrRd", name = "Tempo de tela") +
  ggplot2::labs(
    title = "Sono e tempo de tela por status de TDAH informado",
    x = "Horas de sono em noites de semana", y = "Proporção dentro do grupo"
  ) +
  ggplot2::theme_minimal(base_size = 12) +
  ggplot2::theme(axis.text.x = ggplot2::element_text(angle = 35, hjust = 1), legend.position = "bottom")

plotly::ggplotly(
  ggplot2::ggplot(fpl_ace_profile, ggplot2::aes(x = fpl_band, y = ace_band, fill = proportion,
      text = paste0("FPL: ", fpl_band, "<br>ACEs: ", ace_band, "<br>TDAH: ", adhd_ever,
                    "<br>Proporção: ", scales::percent(proportion, accuracy = 0.1)))) +
    ggplot2::geom_tile(color = "white") +
    ggplot2::facet_wrap(~ adhd_ever) +
    ggplot2::scale_fill_gradient(low = "#FFF4C8", high = "#7A0019", labels = scales::label_percent(accuracy = 1)) +
    ggplot2::labs(title = "ACEs e FPL: distribuição dentro de cada faixa socioeconômica", x = "Razão de pobreza familiar", y = "Faixa de ACEs", fill = "Proporção") +
    ggplot2::theme_minimal(base_size = 12),
  tooltip = "text"
)
Leitura contextual: FPL é uma razão de pobreza familiar e não renda individual. O gráfico interativo mostra como a distribuição de ACEs varia dentro das faixas socioeconômicas em cada grupo de TDAH. Ele é descritivo e não identifica mecanismos de causa.

Eixo 7 — Perfis integrados que emergem dos dados

Para fugir de comparações isoladas, esta etapa reúne idade, razão de pobreza familiar, comorbidades, adversidades, sono, tempo de tela e indicadores escolares. O algoritmo k-means identifica grupos de observações com perfis semelhantes; em seguida, uma PCA reduz essas dimensões para um plano visual. Os grupos são exploratórios e não equivalem a categorias clínicas ou diagnósticas.

cluster_features <- analysis_data |>
  dplyr::transmute(
    age_years,
    fpl_ratio,
    comorbidity_count,
    ace_exposure_count,
    sleep_score = as.integer(sleep),
    screen_time_score = as.integer(screen_time),
    grade_risk = as.integer(grade_risk),
    school_contact_2plus = as.integer(school_contact_2plus),
    adhd_ever
  ) |>
  dplyr::filter(dplyr::if_all(-adhd_ever, ~ !is.na(.)))

if (nrow(cluster_features) < 100) {
  stop("Não há registros completos suficientes para o agrupamento exploratório.")
}

feature_names <- setdiff(names(cluster_features), "adhd_ever")
feature_matrix <- scale(as.matrix(cluster_features[, feature_names]))

set.seed(2026)
cluster_fit <- stats::kmeans(feature_matrix, centers = 4, nstart = 30, iter.max = 100)
pca_fit <- stats::prcomp(feature_matrix, center = FALSE, scale. = FALSE)

cluster_data <- cluster_features |>
  dplyr::mutate(
    cluster = factor(cluster_fit$cluster, labels = paste("Perfil", seq_len(4))),
    pc1 = pca_fit$x[, 1],
    pc2 = pca_fit$x[, 2]
  )

cluster_profile <- cluster_data |>
  dplyr::group_by(cluster) |>
  dplyr::summarise(
    n = dplyr::n(),
    idade_média = mean(age_years),
    FPL_médio = mean(fpl_ratio),
    comorbidades_médias = mean(comorbidity_count),
    ACEs_médias = mean(ace_exposure_count),
    TDAH_informado = safe_mean(adhd_ever == "Yes"),
    .groups = "drop"
  ) |>
  dplyr::mutate(TDAH_informado = scales::percent(TDAH_informado, accuracy = 0.1))

cluster_sample_n <- min(4500, nrow(cluster_data))
set.seed(2026)
cluster_sample <- dplyr::slice_sample(cluster_data, n = cluster_sample_n)

plotly::plot_ly(
  cluster_sample,
  x = ~pc1,
  y = ~pc2,
  color = ~cluster,
  symbol = ~adhd_ever,
  type = "scattergl",
  mode = "markers",
  colors = c("#7A0019", "#D89B00", "#2878B5", "#5F7D3A"),
  marker = list(size = 7, opacity = 0.68),
  text = ~paste0(
    "Perfil: ", cluster,
    "<br>TDAH informado: ", adhd_ever,
    "<br>Idade: ", age_years,
    "<br>Comorbidades: ", comorbidity_count,
    "<br>ACEs: ", ace_exposure_count,
    "<br>FPL: ", round(fpl_ratio, 0)
  ),
  hoverinfo = "text"
) |>
  plotly::layout(
    title = "Mapa interativo de perfis: PCA + k-means",
    xaxis = list(title = "Componente principal 1"),
    yaxis = list(title = "Componente principal 2"),
    legend = list(orientation = "h", x = 0, y = -0.15),
    paper_bgcolor = "rgba(0,0,0,0)",
    plot_bgcolor = "rgba(255,255,255,0.85)"
  )
knitr::kable(cluster_profile, digits = 2, caption = "Perfil médio dos quatro agrupamentos exploratórios.")
Perfil médio dos quatro agrupamentos exploratórios.
cluster n idade_média FPL_médio comorbidades_médias ACEs_médias TDAH_informado
Perfil 1 10688 14.58 281.96 0.40 1.32 14.2%
Perfil 2 1248 13.80 225.28 1.11 2.47 47.8%
Perfil 3 8647 9.54 300.52 0.10 0.63 7.3%
Perfil 4 2105 11.82 266.91 0.83 1.97 38.7%
Como ler o mapa: pontos próximos compartilham combinações mais parecidas das variáveis incluídas. Passe o cursor para inspecionar casos; observe a proporção de TDAH na tabela de perfis; e evite interpretar as fronteiras entre cores como divisões naturais ou diagnósticos.

Eixo 8 — Geografia: diferenças que merecem investigação

O código FIPS permite deslocar a pergunta do indivíduo para o território. Como o arquivo não contém pesos, a exploração por estado não serve para produzir rankings populacionais. Ela serve para identificar onde a composição observada de TDAH, ACEs e contatos escolares parece diferente e onde análises ponderadas futuras podem ser prioritárias.

state_summary_numeric <- analysis_data |>
  dplyr::filter(!is.na(state)) |>
  dplyr::group_by(state) |>
  dplyr::summarise(
    n = dplyr::n(),
    adhd_prevalence = safe_mean(adhd_ever == "Yes"),
    mean_comorbidities = safe_mean(comorbidity_count),
    ace_2plus = safe_mean(ace_2plus),
    school_contact_2plus = safe_mean(school_contact_2plus),
    .groups = "drop"
  )

state_summary <- state_summary_numeric |>
  dplyr::arrange(dplyr::desc(adhd_prevalence)) |>
  dplyr::mutate(dplyr::across(c(adhd_prevalence, ace_2plus, school_contact_2plus), scales::percent, accuracy = 0.1))

DT::datatable(
  state_summary,
  caption = "Exploração descritiva não ponderada por estado.",
  rownames = FALSE,
  filter = "top",
  options = list(pageLength = 10, scrollX = TRUE)
)
plotly::plot_ly(
  state_summary_numeric,
  x = ~ace_2plus,
  y = ~school_contact_2plus,
  size = ~n,
  color = ~adhd_prevalence,
  colors = c("#FFF4C8", "#990000"),
  type = "scatter",
  mode = "markers",
  marker = list(opacity = 0.75),
  text = ~paste0(state, "<br>Registros: ", n,
                 "<br>TDAH: ", scales::percent(adhd_prevalence, accuracy = 0.1),
                 "<br>2+ ACEs: ", scales::percent(ace_2plus, accuracy = 0.1),
                 "<br>2+ contatos: ", scales::percent(school_contact_2plus, accuracy = 0.1)),
  hoverinfo = "text"
) |>
  plotly::layout(
    title = "Exploração estadual: ACEs, contatos escolares e TDAH",
    xaxis = list(title = "Proporção com 2+ ACEs", tickformat = ".0%"),
    yaxis = list(title = "Proporção com 2+ contatos escolares", tickformat = ".0%")
  )
Fechando a exploração. A tabela por estado serve para localizar heterogeneidade aparente e formular perguntas futuras. Como os valores são não ponderados e os tamanhos amostrais estaduais variam, ela não deve ser usada para ranquear estados ou produzir inferência populacional.

5. Conclusões

Da pergunta inicial à resposta possível

O relatório partiu de uma pergunta deliberadamente ampla: em quais combinações de saúde, escola, adversidades e rotina o TDAH informado aparece com mais frequência? A resposta não é um único número. Após estruturar os registros, o universo analítico reuniu 51,498 crianças com idade e status de TDAH válidos; 5,338 tiveram TDAH informado (10.4%). A partir daí, cada eixo acrescentou uma peça à mesma história.

TDAH informado10.4%
Diferença em 2+ contatos escolares26.4%
Diferença ajustada em 3+ ACEs27.8%
Síntese central. O padrão observado não descreve um único perfil de criança com TDAH. Ele sugere que o relato de TDAH, as comorbidades, as demandas escolares, as adversidades e as condições de vida precisam ser lidos em conjunto. Essa é uma associação descritiva — não um diagnóstico individual e nem uma demonstração de causa.

Perguntas levantadas na exploração — e o que os dados respondem

1. Onde o TDAH informado aparece com maior frequência?

O maior percentual descritivo foi observado entre Male de 12-17 anos (21.5%). A distribuição também varia entre os grupos de raça e entre estados. Contudo, essas diferenças mostram a composição observada do arquivo, não explicam por que alguém recebeu um diagnóstico e não podem ser convertidas em estimativas nacionais sem os pesos da pesquisa.

2. O relato de TDAH aparece isolado do restante da saúde?

Não é assim que ele se apresenta nesta base. Ao comparar deficiência de aprendizagem, depressão e ansiedade separadamente, o maior contraste ocorreu em Ansiedade, com diferença de 39.2% entre os grupos. Isso sustenta uma leitura de necessidades concomitantes: uma análise escolar responsável não deve resumir toda a situação da criança ao rótulo de TDAH.

No núcleo clínico, 94.3% dos registros com TDAH informado também o reportaram como atual. As modalidades de cuidado variaram segundo a gravidade relatada, mas o banco não mede qualidade, continuidade, adesão ou efetividade do tratamento. Portanto, o resultado descreve cuidado relatado; não permite eleger uma modalidade como melhor.

3. Há sinais de maior demanda escolar no grupo com TDAH informado?

Há um contraste claro nos indicadores explorados. A proporção de crianças com dois ou mais contatos da escola foi 26.4% maior no grupo com TDAH informado. Repetência, plano de educação especial, notas mais baixas e contatos escolares foram analisados lado a lado justamente para evitar que um único indicador seja tratado como sinônimo de desempenho.

Interpretação prática: contatos frequentes da escola podem sinalizar necessidade de conversa, suporte pedagógico ou acompanhamento compartilhado com a família e a saúde. Eles não definem capacidade, comportamento ou futuro de uma criança.

4. As adversidades na infância ajudam a contextualizar a questão escolar?

Sim, como contexto associado. A média de ACEs selecionadas diferiu em 1.01 entre os grupos com e sem TDAH informado. Além disso, no modelo que comparou crianças com a mesma idade, sexo, raça e FPL de referência, a faixa de 3+ ACEs manteve uma diferença prevista de 27.8% em contatos escolares frequentes entre os grupos de TDAH.

Esse é o ponto mais importante do roteiro: adversidade não é uma explicação automática para TDAH, e TDAH não é uma explicação automática para dificuldades na escola. Os resultados são compatíveis com a hipótese de que saúde, experiências adversas e escola se cruzam; o delineamento transversal não permite afirmar que uma variável causou a outra.

5. Rotina, condição socioeconômica e território contam a mesma história?

Não. Sono, tempo de tela e FPL ampliaram o cenário, mas os perfis exploratórios por PCA e k-means mostraram combinações diferentes dessas dimensões. Da mesma forma, a visualização estadual revelou heterogeneidade aparente, e não um ranking confiável de estados. A leitura útil é que os apoios precisam ser sensíveis ao contexto, em vez de aplicar uma única trajetória a todas as crianças.

O que fazer com estes achados

Para educação, saúde infantil e gestão, o uso responsável do relatório é transformar os padrões em perguntas de acompanhamento: há necessidade educacional não atendida? existem comorbidades que merecem triagem? há estressores familiares ou materiais que exigem encaminhamento? A conclusão não é classificar crianças, e sim defender uma visão coordenada entre escola, família e serviços de saúde.

Limites que mudam a interpretação

  1. O arquivo de trabalho não contém pesos amostrais; por isso, todos os percentuais são descritivos e não ponderados.
  2. As informações são relatadas por pais ou cuidadores e podem conter lembrança imperfeita ou diferenças de percepção.
  3. A pesquisa é transversal: observa coexistências em um momento, não sequências de causa e efeito.
  4. Algumas respostas dependem de idade, elegibilidade ou rota do questionário, gerando ausências que não devem ser tratadas como resposta negativa.
  5. A estruturação segue a documentação técnica e os valores observados; valores não mapeados foram preservados como ausentes, em vez de serem adivinhados.

Próximo capítulo da investigação

Uma continuação mais robusta deve recuperar a variável de peso e o desenho complexo da NSCH, usar o pacote survey para estimativas e intervalos de confiança, e testar se os padrões permanecem após incorporar outras variáveis disponíveis na fonte. Comparações entre anos também exigem conferir a compatibilidade do dicionário de variáveis antes de serem interpretadas. Assim, esta análise encerra a exploração com uma conclusão prudente: os dados apontam para a importância de olhar a criança inteira e o seu contexto, sem transformar associações em sentenças causais.

6. Referências