E1 — Corpus Descriptives (Study 1)

Author

EduStories / habilitace-2

Published

September 10, 2026

Study 1 (PLAN.md §5 E1): descriptive profile of the full corpus snapshot S6 (May 2026), corpus growth across named snapshots, publication drift (S6b), and the human-coded K1 descriptives (behaviours / solutions / outcomes) on the reproducible K1_rez slice — with the pre-specified DUAL counts (cases with category vs total assignments). Text-length statistics conservatively exclude all 52 cases ever affected by extraction truncation (8 of them were later repaired but stay excluded; D4). Writes vystupy/tabulky/kap5_korpus_cisla.csv.

suppressPackageStartupMessages({
  library(dplyr); library(tidyr); library(readr); library(stringr); library(digest)
})
SEED <- 20260714; set.seed(SEED)
H2 <- normalizePath(file.path(getwd(), "../.."))
S6  <- file.path(H2, "data/raw/korpus/export-2026-05/Edustories_export_may26_svabik_v2.csv")
S6B <- file.path(H2, "data/raw/korpus/export-2026-05/Edustories_export_may26_svabik.xlsx")
REZ <- file.path(H2, "data/processed/k1_rez_ids.csv")
TRUNC <- file.path(H2, "data/processed/truncated_uids.csv")
sha256 <- function(p) {
  if (!file.exists(p)) return("public-derived")   # veřejný běh: raw není, otisk se neuvádí
  digest::digest(file = p, algo = "sha256")
}
S6_SHA <- sha256(S6); REZ_SHA <- sha256(REZ)
manifest <- list()
put <- function(metric, value, sha = S6_SHA) manifest[[length(manifest) + 1]] <<-
  data.frame(metric = metric, value = round(as.numeric(value), 4), data_sha = sha)

Full corpus S6 (May 2026)

# Studie 1 potřebuje z plného exportu jen ODVOZENÉ hodnoty (počty slov, věk,
# publikační status), NE texty kazuistik. Je-li plný export přítomen (stroj
# autora), spočítáme je a zároveň zapíšeme veřejnou tabulku BEZ TEXTŮ; když
# chybí (Binder / veřejný běh), načteme tuto tabulku. Čísla jsou identická
# (táž definice počtu slov wc()). Kategorie a ko-výskyt níže stojí na veřejném
# k1_rez_ids.csv, takže celá Studie 1 je reprodukovatelná bez textů korpusu.
PER <- file.path(H2, "data/processed/kap5_perkazuistika.csv")
wc <- function(x) str_count(x, "\\S+")
if (file.exists(S6)) {
  s6 <- read_csv(S6, col_types = cols(.default = col_character()))
  ai_cols <- grep("^ai_solution_.*_cs$", names(s6), value = TRUE)
  per <- tibble(
    uid = paste0("exp", s6$id),
    is_published = s6$is_published,
    age = suppressWarnings(as.numeric(s6$age)),
    wc_description = wc(s6$description_cs),
    wc_anamnesis   = wc(s6$anamnesis_cs),
    wc_solution    = wc(s6$solution_cs),
    wc_outcome     = wc(s6$outcome_cs),
    ai_complete    = rowSums(!is.na(s6[ai_cols]) & s6[ai_cols] != "") == 6L)
  write.csv(per, PER, row.names = FALSE)   # veřejná odvozená tabulka (bez textů)
  PER_SHA <- S6_SHA
} else {
  per <- read_csv(PER, col_types = cols(.default = col_character()))
  per$age <- suppressWarnings(as.numeric(per$age))
  for (c in grep("^wc_", names(per), value = TRUE)) per[[c]] <- as.integer(per[[c]])
  per$ai_complete <- per$ai_complete %in% c("TRUE", "true", "1")
  PER_SHA <- sha256(PER)
  cat("Plný export S6 není přítomen — Studie 1 běží z veřejné odvozené tabulky",
      "kap5_perkazuistika.csv (počty slov, věk, publikační status; bez textů korpusu).\n")
}
Plný export S6 není přítomen — Studie 1 běží z veřejné odvozené tabulky kap5_perkazuistika.csv (počty slov, věk, publikační status; bez textů korpusu).
put("n_korpus_s6", nrow(per))
put("n_zverejneno_s6", sum(per$is_published == "true", na.rm = TRUE))
trunc <- read_csv(TRUNC, show_col_types = FALSE)
per_len <- per |> filter(!(uid %in% trunc$case_uid))
put("n_vyrazeno_delky_trunc", nrow(per) - nrow(per_len))
len_rows <- lapply(c("description", "anamnesis", "solution", "outcome"), function(part) {
  w <- per_len[[paste0("wc_", part)]]
  put(paste0(part, "_slov_median"), median(w, na.rm = TRUE))
  put(paste0(part, "_slov_q1"), quantile(w, .25, na.rm = TRUE))
  put(paste0(part, "_slov_q3"), quantile(w, .75, na.rm = TRUE))
  data.frame(cast = part, median = median(w, na.rm = TRUE),
             q1 = quantile(w, .25, na.rm = TRUE), q3 = quantile(w, .75, na.rm = TRUE))
})
knitr::kable(bind_rows(len_rows), digits = 0, row.names = FALSE,
             caption = "Word counts (Czech text; all 52 truncation-affected cases excluded)")
Word counts (Czech text; all 52 truncation-affected cases excluded)
cast median q1 q3
description 102 76 148
anamnesis 79 66 109
solution 102 74 154
outcome 68 43 102
age <- per$age
# hygiena: věk 0 = nevyplněno (kód formuláře), ne skutečný věk → NA;
# horní mez neořezáváme (žák SŠ může být 20+), počet implauzibilních dokládáme
put("vek_n_nula_vyrazeno", sum(age == 0, na.rm = TRUE))
age[age == 0] <- NA
put("vek_median", median(age, na.rm = TRUE)); put("vek_n", sum(!is.na(age)))
put("n_ai_reseni_kompletni", sum(per$ai_complete, na.rm = TRUE))

Corpus growth and publication drift

Named snapshots (documented in DATA_README §1); S6b is the older export cut — its cases missing from S6 were unpublished in the meantime.

snap <- data.frame(
  rez = c("S1 (2023)", "S3 (04/2024)", "S4 (08/2024)", "S5 HF (release)", "S6 (05/2026)"),
  n = c(757, 1507, 1695, 1492, nrow(per)))
knitr::kable(snap, row.names = FALSE)
rez n
S1 (2023) 757
S3 (04/2024) 1507
S4 (08/2024) 1695
S5 HF (release) 1492
S6 (05/2026) 3202
for (i in seq_len(nrow(snap))) put(paste0("rust_", gsub("[^A-Za-z0-9]", "", snap$rez[i])), snap$n[i])
# Drift potřebuje jen SEZNAM ID staršího i aktuálního exportu (žádné texty).
# Při běhu autora je materializujeme jako veřejné tabulky, jinak je načteme.
S6B_IDS <- file.path(H2, "data/processed/kap5_s6b_ids.csv")
S6_IDS  <- file.path(H2, "data/processed/kap5_s6_ids.csv")
if (file.exists(S6B)) {
  s6b_ids <- as.integer(na.omit(suppressMessages(readxl::read_excel(S6B, sheet = "out"))$id))
  s6_ids  <- as.integer(sub("^exp", "", per$uid))
  write.csv(data.frame(id = s6b_ids), S6B_IDS, row.names = FALSE)
  write.csv(data.frame(id = s6_ids),  S6_IDS,  row.names = FALSE)
  S6B_SHA <- sha256(S6B)
} else {
  s6b_ids <- read_csv(S6B_IDS, show_col_types = FALSE)$id
  s6_ids  <- read_csv(S6_IDS,  show_col_types = FALSE)$id
  S6B_SHA <- sha256(S6B_IDS)
}
drift <- sum(!(s6b_ids %in% s6_ids))
put("n_s6b", length(s6b_ids), S6B_SHA)
put("n_odpublikovano_s6b_vs_s6", drift, S6B_SHA)
cat("S6b:", length(s6b_ids), "případů; z nich", drift, "už není v S6 (odpublikováno).\n")
S6b: 1984 případů; z nich 395 už není v S6 (odpublikováno).

K1 human coding on the K1_rez slice — DUAL counts

Multi-label: a case may carry several categories. We therefore report BOTH „cases with category” and „assignments” (PLAN §5 ⊕); their ratio explains why assignment totals exceed the number of cases.

rez <- read_csv(REZ, show_col_types = FALSE)
put("n_k1_rez", nrow(rez), REZ_SHA)
# normalizace štítků: sjednotit velikost písmen a známé překlepy na kanonickou formu
canon_label <- function(p) {
  p <- gsub("Částěčný", "Částečný", p)
  p <- gsub("odbonríky", "odborníky", p)           # překlep v datech
  substr(p, 1, 1) <- toupper(substr(p, 1, 1))       # „rozhovor" → „Rozhovor"
  p
}
# vyřazené štítky: placeholdery + mimo-kodbook u řešení. „Proaktivní řešení"
# (kategorie z V1, ve V2 zrušená) a „Humor" zaznamenala v lidském K1 jediná
# anotátorka kvůli chybně sestavenému výběrovému seznamu (viz příloha A / kap. 5);
# do lidské korpusové deskriptivy nepatří. V LLM srovnání (kap. 6) zůstávají.
#
# „Diagnóza" je JINÝ případ (rozhodnutí 27. 7. 2026): NEJDE o artefakt výběrového
# seznamu — kategorie č. 14 stromu V2 (kód DIA) byla v nabídce u všech pěti
# anotátorek. Vyřazena je z věcného důvodu: její definice („Žák/žákyně má
# diagnostikovanou některou z diagnóz SVP.") popisuje STAV ŽÁKA, ne chování,
# takže do dimenze problémového chování nepatří. Data to potvrzují: 94,4 %
# výskytů je co-kód k jinému chování, užití mezi anotátorkami kolísá 3,7-21,7 %,
# shoda na kategorii je 40,9 % a její vyřazení zvedne α dimenze z 0,474 na 0,509.
# Diagnóza se v knize reportuje zvlášť jako kontextová proměnná z pole
# diagnoses_unified (metriky `dia_*` níže). Doklad: příloha A + kap. 5.
EXCLUDE_LABELS <- c("-", "?", "(zvolit)", "Proaktivní řešení", "Humor", "Diagnóza")
split_cats <- function(x) {
  x <- x[!is.na(x)]
  unlist(lapply(x, function(v) {
    p <- trimws(unlist(strsplit(v, "[;,]")))
    p <- canon_label(p)
    p[p != "" & !(p %in% EXCLUDE_LABELS)]
  }))
}
dual <- lapply(c(chovani = "problems_annotated", reseni = "solutions_annotated",
                 dopad = "implications_annotated"), function(col) NULL)
for (nm in names(c(chovani = 1, reseni = 1, dopad = 1))) {
  col <- c(chovani = "problems_annotated", reseni = "solutions_annotated",
           dopad = "implications_annotated")[[nm]]
  cats_per_case <- lapply(rez[[col]], function(v) unique(split_cats(v)))
  assignments <- unlist(cats_per_case)
  n_cases_with <- table(assignments)                 # případů s kategorií
  n_assign <- length(assignments)
  put(paste0("k1_", nm, "_prirazeni_celkem"), n_assign, REZ_SHA)
  put(paste0("k1_", nm, "_pripadu_s_udajem"), sum(lengths(cats_per_case) > 0), REZ_SHA)
  put(paste0("k1_", nm, "_pomer_prirazeni_na_pripad"),
      n_assign / sum(lengths(cats_per_case) > 0), REZ_SHA)
  top <- sort(n_cases_with, decreasing = TRUE)[1:min(8, length(n_cases_with))]
  for (k in names(top))
    put(paste0("k1_", nm, "_pripadu:", substr(k, 1, 38)), top[[k]], REZ_SHA)
  # (dříve zde byla explicitní metrika „Proaktivní řešení" pro tvrzení o řídkosti
  #  v kap. 5; kategorie je artefaktem jednoho výběrového seznamu, proto vyřazena
  #  z lidské deskriptivy — viz EXCLUDE_LABELS výše a příloha A.)
  print(knitr::kable(data.frame(kategorie = names(top), pripadu = as.integer(top)),
                     caption = paste0(nm, " — top kategorie (počet PŘÍPADŮ)")))
}


Table: chovani — top kategorie (počet PŘÍPADŮ)

|kategorie                                            | pripadu|
|:----------------------------------------------------|-------:|
|Verbální agrese                                      |     330|
|Verbální narušování výuky                            |     328|
|Fyzická agrese                                       |     299|
|Nevěnování se výuce/Nepozornost při výuce            |     293|
|Porušování třídních/školních pravidel                |     255|
|Neplnění školních povinností/nepřipravenost na výuku |     201|
|Neverbální narušování výuky                          |     174|
|Emoční výbuchy                                       |     143|


Table: reseni — top kategorie (počet PŘÍPADŮ)

|kategorie                 | pripadu|
|:-------------------------|-------:|
|Rozhovor                  |     922|
|Spolupráce s odborníky    |     461|
|Podpora                   |     342|
|Důsledky                  |     245|
|Upozornění                |     229|
|Přemístění žáka           |     205|
|Práce s kolektivem        |     184|
|Nerespektující komunikace |     154|


Table: dopad — top kategorie (počet PŘÍPADŮ)

|kategorie         | pripadu|
|:-----------------|-------:|
|Dlouhodobý úspěch |     598|
|Neúspěch          |     483|
|Částečný úspěch   |     158|
|Krátkodobý úspěch |      84|

Diagnosis as a CONTEXT variable (not a behaviour category)

Since the Diagnóza label was removed from the behaviour dimension (see EXCLUDE_LABELS), the substantive question „do cases involving a pupil with a recorded diagnosis differ?” is answered from the curated corpus field diagnoses_unified (column ma_diagnozu in K1_rez), which is a property of the case, not an annotator judgement about behaviour.

dia <- rez$ma_diagnozu == 1
put("dia_pripadu", sum(dia), REZ_SHA)
put("dia_podil", mean(dia), REZ_SHA)
cat("K1_rez s vyplněnou diagnózou:", sum(dia), sprintf("(%.1f %%)\n", 100 * mean(dia)))
K1_rez s vyplněnou diagnózou: 530 (39.0 %)

Distributions for book figures (word lengths, age)

Case-level aggregates for fig_05_*.R (no case texts — safe to publish).

TABDIR <- file.path(H2, "vystupy/tabulky")
delky <- bind_rows(lapply(c("description", "anamnesis", "solution", "outcome"),
  function(part) data.frame(cast = part, slova = per_len[[paste0("wc_", part)]])))
delky <- delky[!is.na(delky$slova) & delky$slova > 0, ]
write.csv(delky, file.path(TABDIR, "kap5_delky_rozdeleni.csv"), row.names = FALSE)
put("delka_p90_description",
    quantile(delky$slova[delky$cast == "description"], .9, na.rm = TRUE))
vek_tab <- as.data.frame(table(vek = round(age)), responseName = "n")
write.csv(vek_tab, file.path(TABDIR, "kap5_vek_rozdeleni.csv"), row.names = FALSE)
put("vek_q1", quantile(age, .25, na.rm = TRUE))
put("vek_q3", quantile(age, .75, na.rm = TRUE))
cat("rozdělení: délky", nrow(delky), "řádků; věk", nrow(vek_tab), "hodnot\n")
rozdělení: délky 12481 řádků; věk 21 hodnot

K1 full category tables, co-occurrence and outcome-by-solution

All categories per dimension (figures), conditional co-occurrence P(solution | behaviour) on K1_rez cases, and share of long-term success by solution type. Unit = CASE (multi-label: a case counts once per category it carries). Descriptive only — no causal claims.

cats_of <- function(col) lapply(rez[[col]], function(v) unique(split_cats(v)))
CH <- cats_of("problems_annotated")
RE <- cats_of("solutions_annotated")
DO <- cats_of("implications_annotated")

# (a) úplné tabulky kategorií pro figury
plne <- bind_rows(
  data.frame(dimenze = "chovani", kategorie = unlist(CH)),
  data.frame(dimenze = "reseni",  kategorie = unlist(RE)),
  data.frame(dimenze = "dopad",   kategorie = unlist(DO))) |>
  count(dimenze, kategorie, name = "pripadu") |>
  arrange(dimenze, desc(pripadu))
write.csv(plne, file.path(TABDIR, "kap5_k1_kategorie.csv"), row.names = FALSE)

# (b) ko-výskyt: P(řešení | chování), jen buňky s n_chování >= 50
beh_n <- table(unlist(lapply(CH, unique)))
kov <- do.call(rbind, lapply(names(beh_n)[beh_n >= 50], function(b) {
  idx <- vapply(CH, function(s) b %in% s, logical(1))
  res_tab <- table(unlist(RE[idx]))
  data.frame(chovani = b, reseni = names(res_tab),
             n_spolu = as.integer(res_tab),
             n_chovani = sum(idx),
             podil = as.numeric(res_tab) / sum(idx))
}))
write.csv(kov, file.path(TABDIR, "kap5_kovyskyt.csv"), row.names = FALSE)
for (i in seq_len(nrow(kov)))
  put(paste0("kov_podil:", substr(kov$chovani[i], 1, 24), "|",
             substr(kov$reseni[i], 1, 24)), kov$podil[i], REZ_SHA)

# (b2) totéž pro KONTEXTOVOU proměnnou diagnózy: P(řešení | žák má diagnózu).
# Nahrazuje dřívější řádek `kov_podil:Diagnóza|*`, který stál na anotační
# kategorii vyřazené z dimenze chování; opora je nyní v poli diagnoses_unified.
# POZOR na jmenovatele: srovnávací podíl `*_ref` počítáme na CELÉM řezu
# (n = nrow(rez)), tedy na téže definici jako podíl u kazuistik s diagnózou
# (n = sum(dia)). Jinak by se srovnávaly dvě různé základny.
dia_res <- table(unlist(RE[dia]))
ref_res <- table(unlist(RE))
for (r in names(dia_res)) {
  put(paste0("dia_podil_reseni:", substr(r, 1, 24)),
      as.numeric(dia_res[[r]]) / sum(dia), REZ_SHA)
  put(paste0("dia_podil_reseni_ref:", substr(r, 1, 24)),
      as.numeric(ref_res[[r]]) / nrow(rez), REZ_SHA)
}
write.csv(data.frame(reseni = names(dia_res),
                     n_s_diagnozou = as.integer(dia_res), n_diagnoza = sum(dia),
                     podil_s_diagnozou = as.numeric(dia_res) / sum(dia),
                     n_cely_rez = as.integer(ref_res[names(dia_res)]),
                     n_rez = nrow(rez),
                     podil_cely_rez = as.numeric(ref_res[names(dia_res)]) / nrow(rez)),
          file.path(TABDIR, "kap5_diagnoza_reseni.csv"), row.names = FALSE)

# (c) dopad podle typu řešení: podíl dlouhodobého úspěchu (jen n >= 30)
dop <- do.call(rbind, lapply(names(table(unlist(RE))), function(r) {
  idx <- vapply(RE, function(s) r %in% s, logical(1)) &
         vapply(DO, length, integer(1)) > 0
  if (sum(idx) < 30) return(NULL)
  du  <- mean(vapply(DO[idx], function(s) "Dlouhodobý úspěch" %in% s, logical(1)))
  neu <- mean(vapply(DO[idx], function(s) "Neúspěch" %in% s, logical(1)))
  data.frame(reseni = r, n = sum(idx), podil_DU = du, podil_NEU = neu)
}))
dop <- dop[order(-dop$podil_DU), ]
write.csv(dop, file.path(TABDIR, "kap5_dopad_podle_reseni.csv"), row.names = FALSE)
for (i in seq_len(nrow(dop))) {
  put(paste0("dopadDU:", substr(dop$reseni[i], 1, 30)), dop$podil_DU[i], REZ_SHA)
  put(paste0("dopadN:",  substr(dop$reseni[i], 1, 30)), dop$n[i], REZ_SHA)
}
knitr::kable(dop, digits = 3,
             caption = "Podíl dlouhodobého úspěchu podle typu řešení (deskriptivně)")
Podíl dlouhodobého úspěchu podle typu řešení (deskriptivně)
reseni n podil_DU podil_NEU
7 Práce s kolektivem 178 0.669 0.213
2 Dohoda 129 0.628 0.233
6 Podpora 334 0.503 0.287
9 Rozhovor 894 0.493 0.343
10 Spolupráce s odborníky 449 0.443 0.376
1 (Kázeňské) Tresty 102 0.392 0.431
3 Důsledky 240 0.392 0.442
11 Upozornění 220 0.391 0.391
8 Přemístění žáka 201 0.368 0.438
4 Fyzické zakročení 41 0.268 0.439
5 Nerespektující komunikace 144 0.215 0.590

Freeze: write manifest

out <- bind_rows(manifest) |>
  mutate(skript = "analyzy/notebooks/10_korpus.qmd", seed = SEED,
         datum = format(Sys.Date())) |>
  select(metric, value, skript, data_sha, seed, datum)
write.csv(out, file.path(H2, "vystupy/tabulky/kap5_korpus_cisla.csv"), row.names = FALSE)
cat("Manifest:", nrow(out), "metrik → vystupy/tabulky/kap5_korpus_cisla.csv\n")
Manifest: 236 metrik → vystupy/tabulky/kap5_korpus_cisla.csv