Number-line tests

1 Set up

Code
library(dplyr)
library(lubridate)
library(tidyr)
library(here)
library(ggplot2)
library(stringr)
library(purrr)
library(stringr)
library(readr)
library(moments)
library(patchwork)
library(scales)
library(kableExtra)
library(knitr)
library(glue)
library(openxlsx)

probe_dir <- here("02-Number_line")
data_dir   <- "Raw"
calib_file <- "Raw/00_number_line_coordinates.csv"
key_file   <- "Raw/00_number_line_answer_key.csv"
student_group_file <- "Raw/student_assessment_map.csv"

2 Question presentation

2.1 a) Bounded number line 0 - 100 (BNL0-100)

2.2 b) Bounded number line 0 - 20 (BNL0-20)

2.3 c) Unbounded number line chair (UBNL)

2.4 d) Unbounded number line no chair (UBNL)

3 Build conversion table from calibration points

Scale limit for each test type’s number line

  • n_left → Number on the left end

  • n_right → Number on the right end

  • Slope = units per pixel on number line (used to compute right edge for UBNL)

Code
coord_calibration_df <- read_csv(calib_file)

conv_tbl <- coord_calibration_df %>% 
  group_by(TestType) %>% # assumes two coordinate rows per test
  reframe(
    x_left = min(X_coord),  
    n_left = Number[which.min(X_coord)],
    x2 = max(X_coord), 
    n2 = Number[which.max(X_coord)],
    slope  = (n2 - n_left) / (x2 - x_left),    # units per pixel
    
    # Assume left end = x1, decide real right end for each test
    x_right = case_when(
      TestType == "UNL0-20" ~ 1000,             
      TRUE                   ~ x2
    ),
    n_right = n_left + slope * (x_right - x_left),   # infer right end number
  ) %>% 
  select(TestType, x_left, n_left, x_right, n_right, slope) %>% 
  distinct()

print(conv_tbl)
# A tibble: 3 × 6
  TestType x_left n_left x_right n_right  slope
  <chr>     <dbl>  <dbl>   <dbl>   <dbl>  <dbl>
1 BNL0-100  10         0    1089   100   0.0927
2 BNL0-20   11         0    1108    20   0.0182
3 UNL0-20    5.25      0    1000    34.4 0.0346

4 Read & tidy every student-response CSV

Code
nl_raw <- list.files(data_dir, pattern = "_responses\\.csv$", full.names = TRUE) %>%
  read_csv(show_col_types = FALSE, id = "source_file") %>%   # `id` keeps the file name
  select(-starts_with("..."))                  

# De-duplicate: keep only the latest per Identifier × Test Identifier × Question Identifier
tmp_nl <- nl_raw %>%
  mutate(.ts = dmy_hm(`Date Completed`))

nl_raw <- tmp_nl %>%
  group_by(Identifier, `Test Identifier`, `Question Identifier`) %>%
  slice_max(.ts, n = 1, with_ties = FALSE) %>%
  ungroup() %>%
  select(-.ts)

n_removed <- nrow(tmp_nl) - nrow(nl_raw)
message(n_removed, " duplicate rows removed; keeping only the most recent per Identifier/Test/Question")

# Pull out pixel coordinates
nl_raw <- nl_raw %>% 
  mutate(
    X_coord = str_match(`Answer Response`, '"X":\\s*"([0-9\\.]+)"')[, 2] %>% as.numeric(),
    Y_coord = str_match(`Answer Response`, '"Y":\\s*"([0-9\\.]+)"')[, 2] %>% as.numeric()
  )

# Number of students per file
# nl_raw %>%
#  count(source_file, name = "rows") 

# Normalise TestType
normalise_type <- function(id) {
  id %>%                                           
    sub("_\\d{4}.*$", "", .) %>%
    sub("-\\d{4}.*$", "", .) %>%
    sub("[Ff]$", "", .) %>%                        # drop prefix F and nc
    sub("-?nc", "", ., ignore.case = TRUE)
}

# Clean Question Identifier text string
clean_qid <- function(x) {
  x %>%
    str_replace(regex("^UNL[cn]*c?0-20", ignore_case = TRUE), "UBNL0-20") %>%
    str_remove("[-_]new[-_]?copy$") %>%     
    str_remove("[-_]copy$") %>% 
    str_remove("[-_]?new$")
}

# Replace exam group with the following:
student_df <- read_csv(student_group_file) %>% 
  select(-starts_with("..."))

student_clean <- student_df %>% 
  mutate(
    GradeGroup = case_when(
      str_detect(`Assessment Event Identifier`, "^year-1a-2025$")          ~ "Year 1A",
      str_detect(`Assessment Event Identifier`, "^year-1b-2025$")          ~ "Year 1B",
      str_detect(`Assessment Event Identifier`, "^foundation-a-2025$")     ~ "Foundation A",
      str_detect(`Assessment Event Identifier`, "^foundation-b-2025$")     ~ "Foundation B",
      TRUE                                                                 ~ NA_character_  
    )
  ) %>% 
  select(Identifier, GradeGroup) %>%          
  distinct()     

nl_tidy <- nl_raw %>%
  mutate(TestType = normalise_type(`Test Identifier`),
         `Question Identifier` = clean_qid(`Question Identifier`))


nl_tidy <- nl_tidy %>% 
  left_join(student_clean, by = "Identifier") %>% 
  select(-`Exam Group`)

# Fix grade groups
nl_tidy <- nl_tidy %>% 
  mutate(
    GradeGroup = case_when(
      `Test Identifier` == "BNL0-100_2025" & GradeGroup == "Foundation B" ~ "Year 1B",
      `Test Identifier` == "BNL0-20F_2025" & GradeGroup == "Year 1A" ~ "Foundation A",
      `Test Identifier` == "BNL0-20F_2025" & GradeGroup == "Year 1B" ~ "Foundation B",
      `Test Identifier` == "UNLC0-20F_2025" & GradeGroup == "Year 1B" ~ "Foundation B",
      `Test Identifier` == "UNLc0-20-2025" & GradeGroup == "Foundation B" ~ "Year 1B",
      `Test Identifier` == "UNLnc0-20F_2025" & GradeGroup == "Year 1A" ~ "Foundation A",
      TRUE ~ GradeGroup
    )
  )


# Group unbounded number line variants
nl_tidy <- nl_tidy %>% 
  mutate(
    TestType = case_when(
      TestType %in% c("UNLc0-20", "UNLC0-20") ~ "UNL0-20",   
      TRUE                                    ~ TestType     
    )
  ) %>% 
  select(-source_file)

5 Map number clicks and compute score

Code
# Map pixels to number
nl_clicks <- nl_tidy %>% 
  filter(!is.na(X_coord)) %>% 
  filter(!str_detect(`Question Identifier`, "TRANS_INTRO|_Prac|_Tch|-Prac|_prac")) %>% 
  left_join(conv_tbl, by = "TestType") %>% 
  # clamp clicks to both ends of line
  mutate(
    x_clamp = pmin(pmax(X_coord, x_left), x_right),
    Number_clicked = n_left + slope * (x_clamp - x_left)
  )

# Add answer key
answer_key_df <- read_csv(key_file, show_col_types = FALSE)

# Identify any Question Identifier missing from the key
missing_qids <- nl_clicks %>% 
  anti_join(answer_key_df, by = "Question Identifier") %>% 
  distinct(`Question Identifier`, TestType)  

# print(missing_qids)

# Compute PAE
nl_scored <- nl_clicks %>% 
  left_join(
    answer_key_df %>% select(`Question Identifier`, `Answer Key`),
    by = c(`Question Identifier` = "Question Identifier")
  ) %>% 
  mutate(
    abs_error = abs(Number_clicked - `Answer Key`),
    signed_error = Number_clicked - `Answer Key`,
    PAE = abs_error / (n_right - n_left),
    Score = 1 - PAE
  )

# Compute inverse efficiency
nl_scored <- nl_scored %>% 
  mutate(IE = `Answer Duration` / (1 - PAE))

6 Exploratory analysis

6.1 Student count per probe x grade group

See below for detailed probe summary statistics

Code
nl_scored %>% 
  group_by(GradeGroup, TestType) %>% 
  summarise(
    n_student = n_distinct(Identifier),
    mean_PAE = mean(PAE),
    mean_IE = mean(IE),
    .groups = "drop"
  ) %>% 
  arrange(GradeGroup)
# A tibble: 8 × 5
  GradeGroup   TestType n_student mean_PAE mean_IE
  <chr>        <chr>        <int>    <dbl>   <dbl>
1 Foundation A BNL0-20       1206    0.165   19.7 
2 Foundation A UNL0-20       1221    0.207   12.5 
3 Foundation B BNL0-20       1128    0.166   19.8 
4 Foundation B UNL0-20       1153    0.204   14.5 
5 Year 1A      BNL0-100      1260    0.140   25.4 
6 Year 1A      UNL0-20       1254    0.119    6.87
7 Year 1B      BNL0-100      1050    0.138   11.0 
8 Year 1B      UNL0-20       1063    0.126    7.41

Generate wide format

Code
# 1. Build one big wide table, keeping GradeGroup & TestType as keys
wide_by_group_test <- nl_scored %>%
  # extract the 3-digit item suffix
  mutate(item = str_extract(`Question Identifier`, "\\d{3}$")) %>%
  # keep only the columns we need
  select(Identifier, GradeGroup, `Test Identifier`, item,
         Number_clicked, `Answer Duration`, PAE) %>%
  # pivot to wide, with one block of columns per item
  pivot_wider(
    names_from  = item,
    values_from = c(Number_clicked, `Answer Duration`, PAE),
    names_glue  = "{.value}_item{item}"
  )

# 2. Split into a list by GradeGroup × Test Identifier
wide_list <- wide_by_group_test %>%
  group_by(GradeGroup, `Test Identifier`) %>%
  group_split(.keep = TRUE)

# capture matching sheet names
sheet_names <- wide_by_group_test %>%
  distinct(GradeGroup, `Test Identifier`) %>%
  mutate(sheet = glue("{GradeGroup}_{`Test Identifier`}")) %>%
  pull(sheet)

# 3. Create workbook and add each sub‐wide table as its own sheet
wb <- createWorkbook()
walk2(wide_list, sheet_names, function(df, sh) {
  addWorksheet(wb, sh)
  writeData(wb, sh, df)
})

# 4. Finally add the master answer‐key on its own sheet
addWorksheet(wb, "Answer Key")
writeData(wb, "Answer Key", answer_key_df)

# 5. Save the file
saveWorkbook(
  wb,
  file      = "numb_line_by_grade_test.xlsx",
  overwrite = TRUE
)

6.2 Distribution of response per item

Code
plot_response <- function(df, bins) {
  
  df <- df %>% 
    mutate(facet = paste0(`Question Identifier`, " (", `Answer Key`, ")"))
  
  ## -- build the plot *first* (we'll grab its stats in a moment)
  base_plot <- ggplot(
      df,
      aes(Number_clicked, group = facet)     # one histogram per item
    ) +
    geom_histogram(
      aes(y = after_stat(count / sum(count))),
      bins   = bins,
      boundary = 0,
      colour = "grey20", fill = "grey80"
    ) +
    geom_vline(aes(xintercept = `Answer Key`), colour = "red") +
    facet_wrap(~ facet, scales = "free_y", ncol = 4) +
    scale_y_continuous(labels = percent_format(accuracy = 0.1)) +
    labs(
      x = "Number clicked",
      y = "Percent of students",
      caption = "Red line = target number"
    ) +
    theme_bw()
  
  ## -- extract max proportion across all facets
  stats     <- ggplot_build(base_plot)$data[[1]]
  max_prop  <- max(stats$y, na.rm = TRUE)            # highest bar
  y_limit   <- max_prop * 1.05                       # +5 % head-room
  
  ## -- return the finished plot with a universal y-limit
  base_plot +
    coord_cartesian(ylim = c(0, y_limit), clip = "off")
}

bins_map <- c(
  "BNL0-100" = 25,
  "BNL0-20"  = 21,
  "UNL0-20"  = 34
)

unique(nl_scored$TestType) %>% 
  walk(function(tt) {
    
    df_probe <- nl_scored %>% filter(TestType == tt)
    bins     <- bins_map[tt]
    
    p <- plot_response(df_probe, bins)
    
    print(p)   
    
    ggsave(
      filename = file.path(probe_dir, glue("response_dist_{tt}.png")),
      plot     = p,
      width    = 12, height = 8, dpi = 300, units = "in"
    )
  })

6.3 Distribution of answer duration

Code
plot_duration <- function(df,
                               test_name,
                               bins      = 60,
                               trim_fast = 0,   
                               trim_slow = 60)  
{
  df <- df %>% 
    filter(`Answer Duration` >= trim_fast,
           `Answer Duration` <= trim_slow) %>%
    mutate(facet_label = paste0(`Question Identifier`, " (", `Answer Key`, ")"))

  ggplot(df, aes(x = `Answer Duration`)) +
    geom_histogram(
      aes(y = after_stat(count / sum(count))),
      bins   = bins,
      colour = "grey10",
      fill   = "steelblue"
    ) +
    facet_wrap(~ facet_label, scales = "free_y", ncol = 4) +
    scale_y_continuous(labels = scales::percent_format(accuracy = 0.01)) +
    labs(
      title   = paste("Answer duration distribution –", test_name),
      x       = "Seconds",
      y       = "Percent of students",
      caption = paste("Only", trim_fast, "–", trim_slow, "s shown")
    ) +
    theme_bw() +
    theme(panel.grid.y = element_line(colour = "grey90"),
          strip.text   = element_text(face = "bold"))
}
  
# Print and save plot
unique(nl_scored$TestType) %>%
  walk(function(tt) {
    df_test <- nl_scored %>% filter(TestType == tt)

    p <- plot_duration(df_test, tt)    

    print(p)

    ggsave(
      filename = file.path(probe_dir, glue("duration_dist_{tt}.png")),
      plot     = p,
      width    = 12, height = 8, dpi = 300, units = "in"
    )
  })

6.4 PAE and Inverse Efficiency per student by test type

Inverse Efficiency (IE) = Answer Duration / (1 - PAE). i.e. seconds per perfectly accurate response

Code
# Each student's mean score per probe
nl_student <- nl_scored %>%
  group_by(TestType, Identifier) %>%
  summarise(
    PAE = mean(PAE, na.rm = TRUE),
    IE  = mean(IE,  na.rm = TRUE),
    .groups = "drop"
  )

nl_long <- nl_student %>% 
  pivot_longer(c(PAE, IE), names_to = "Metric", values_to = "Value")

# Trim extreme outliers
k_trim <- 2

trim_iqr <- function(df, k_out = k_trim) {
  df %>% 
    group_by(TestType, Metric) %>% 
    mutate(
      Q1  = quantile(Value, 0.25, na.rm = TRUE),
      Q3  = quantile(Value, 0.75, na.rm = TRUE),
      hi  = Q3 + k_out * (Q3 - Q1),
      lo  = pmax(0, Q1 - k_out * (Q3 - Q1))
    ) %>% 
    ungroup() %>% 
    filter(Value >= lo, Value <= hi) %>% 
    select(-Q1, -Q3, -hi, -lo)
}

nl_trim <- trim_iqr(nl_long)

# Violin plot
vplot <- function(df, metric, y_lab, lab_fun) {
  ggplot(filter(df, Metric == metric),
         aes(TestType, Value, fill = TestType)) +
    geom_violin(trim = FALSE, alpha = .6, colour = NA) +
    geom_boxplot(width = .15, outlier.shape = NA, colour = "grey20") +
    scale_fill_brewer(palette = "Set2", guide = "none") +
    scale_y_continuous(labels = lab_fun,
                       expand = expansion(mult = c(0, .05))) +
    labs(title = metric, x = NULL, y = y_lab) +
    theme_bw() +
    theme(panel.grid.y = element_line(colour = "grey90"))
}

p_pae <- vplot(nl_trim, "PAE", "Percent of scale",
               lab_fun = percent_format(accuracy = 1))
p_ie  <- vplot(nl_trim, "IE",  "Inverse efficiency",
               lab_fun = comma_format(accuracy = 1))

p_probe_violin <- (p_pae | p_ie) +
  plot_annotation(
    title = paste0("PAE and IE by probe (student means, IQR-trim k = ", k_trim, ")"),
    theme = theme(plot.title = element_text(face = "bold"))
  )

p_probe_violin

Code
ggsave(
  filename = file.path(probe_dir, "probe_pae_ie_dist.png"),
  plot     = p_probe_violin,
  width    = 12, height = 6, dpi = 300, units = "in"
)

6.5 Plot clicks for questions

Code
plot_clicks_xy <- function(qid, data = nl_scored) {
  df <- data %>%
    filter(`Question Identifier` == qid) %>%
    arrange(Identifier) %>%
    mutate(student_idx = row_number())  

  answer <- unique(df$`Answer Key`)
  if (length(answer) != 1) {
    stop("Question ", qid, " has ", length(answer), " distinct answer keys!")
  }

  p <- ggplot(df, aes(x = Number_clicked, y = Y_coord)) +
    geom_point(alpha = 0.6, size = 1.8) +
    geom_vline(xintercept = answer,
               colour     = "red",
               linetype   = "dashed",
               size       = 1) +
    labs(
      title = paste("Click positions for", qid),
      x     = "Number clicked (converted from X coord)",
      y     = "Y coord clicked"
    ) +
    theme_minimal()

  print(p)
  invisible(p)
}

plot_clicks_xy("UBNL0-20_005")

7 Item summary statistics

7.1 Distribution of PAE and Inverse Efficiency per Item

Code
# Shorten item label
item_label <- function(qid, target) {
  item_no <- str_extract(qid, "(?<=_)(\\d{3})(?=$|[_-])")  # "001" part
  glue("Item {item_no} ({target})")
}

nl_item_long <- nl_scored %>% 
  mutate(
    ItemLab = item_label(`Question Identifier`, `Answer Key`)
  ) %>% 
  select(TestType, ItemLab, Target = `Answer Key`, PAE, IE)

# Boxplot
make_item_box <- function(df, metric, y_lab) {
  ggplot(df, aes(x = .data[[metric]],           
                 y = reorder(ItemLab, Target))) +
    geom_boxplot(fill = "steelblue", width = .6, outlier.size = .8) +
    labs(x = y_lab, y = NULL) +
    theme_bw() +
    theme(
      axis.text.y = element_text(size = 8),
      panel.grid.major.y = element_blank()
    )
}

# Trim outliers for inverse efficiency
k_trim_ie <- 2

trim_ie <- function(df, k = k_trim_ie) {
  df %>% 
    group_by(ItemLab) %>%                       
    mutate(
      Q1  = quantile(IE, .25, na.rm = TRUE),
      Q3  = quantile(IE, .75, na.rm = TRUE),
      hi  = Q3 + k * (Q3 - Q1),
      lo  = pmax(0, Q1 - k * (Q3 - Q1))
    ) %>% 
    ungroup() %>% 
    filter(IE >= lo, IE <= hi) %>% 
    select(-Q1, -Q3, -hi, -lo)
}

metrics <- c(PAE = "Percent absolute error", IE = "Inverse efficiency (s)")

nl_item_long %>% 
  split(.$TestType) %>% 
  iwalk(function(df_probe, probe_name) {

    walk2(names(metrics), metrics, function(metric, xlab) {
      
      df_use <- if (metric == "IE") trim_ie(df_probe) else df_probe
      
      p <- make_item_box(df_use, metric, xlab) +
           ggtitle(glue("{probe_name} – {metric}")) + 
           if (metric == "IE")
             labs(caption = paste0("Outliers beyond ± ", k_trim_ie, " × IQR per item removed"))

      print(p)    

      ggsave(
        file.path(probe_dir, glue("box_{probe_name}_{metric}.png")),
        plot   = p, width = 8, height = 6, units = "in"
      )
    })
  })

7.2 PAE

Code
nl_scored_item_pae_summary <- nl_scored %>% 
  group_by(`Question Identifier`) %>% 
  summarise(
    n      = n_distinct(Identifier),
    Target = first(`Answer Key`),
    pae_mean = mean(PAE),
    pae_sd   = sd(PAE),
    pae_min  = min(PAE),
    pae_max  = max(PAE),
    pae_skew = moments::skewness(PAE),
    pae_kurt = moments::kurtosis(PAE),
    .groups = "drop"
  )
  
nl_scored_item_pae_summary %>% 
  mutate(across(where(is.numeric), round, 3)) %>% 
  # plain kable
  kbl(
    caption   = "Item-level PAE summary",
    digits    = 3,
    align     = c("l", rep("r", 8)),  # left for ID, right for numbers
    col.names = c("Item", "n", "Target",
                  "Mean", "SD", "Min", "Max", "Skew", "Kurt")
  ) |>
  # kableExtra niceties
  kable_styling(full_width = FALSE, position = "left") %>% 
  scroll_box(width = "100%",  height = "500px")   
Item-level PAE summary
Item n Target Mean SD Min Max Skew Kurt
BNL0-100_001 2232 59 0.085 0.086 0.000 0.590 1.931 7.733
BNL0-100_002 2201 20 0.171 0.137 0.000 0.788 1.436 5.402
BNL0-100_003 2220 46 0.103 0.095 0.000 0.521 1.518 5.249
BNL0-100_004 2232 13 0.168 0.146 0.000 0.866 1.454 5.497
BNL0-100_005 2219 72 0.113 0.114 0.000 0.718 2.013 7.769
BNL0-100_006 2223 31 0.130 0.121 0.000 0.690 1.691 6.107
BNL0-100_007 2241 97 0.200 0.173 0.001 0.968 1.699 5.872
BNL0-100_008 2225 85 0.168 0.132 0.000 0.850 1.522 6.505
BNL0-100_009 2216 68 0.128 0.112 0.000 0.680 1.590 6.392
BNL0-100_010 2254 4 0.090 0.121 0.000 0.951 3.909 21.462
BNL0-100_011 2209 13 0.179 0.147 0.000 0.870 1.486 5.806
BNL0-100_012 2207 68 0.126 0.112 0.000 0.678 1.521 5.649
BNL0-100_013 2194 85 0.170 0.149 0.000 0.811 1.324 4.589
BNL0-100_014 2197 31 0.134 0.122 0.000 0.684 1.577 5.562
BNL0-100_015 2191 72 0.127 0.120 0.000 0.720 1.720 6.262
BNL0-100_016 2187 97 0.198 0.178 0.000 0.970 1.559 5.218
BNL0-100_017 2220 20 0.166 0.152 0.000 0.800 1.595 5.693
BNL0-100_018 2216 59 0.107 0.096 0.000 0.558 1.427 5.022
BNL0-100_019 2248 4 0.098 0.126 0.000 0.959 3.330 16.146
BNL0-100_020 2214 46 0.126 0.102 0.000 0.540 1.203 4.207
BNL0-20_001 2194 2 0.107 0.168 0.000 0.900 2.457 9.018
BNL0-20_002 2038 5 0.124 0.121 0.000 0.750 2.602 11.865
BNL0-20_003 2113 16 0.209 0.184 0.000 0.800 1.215 3.903
BNL0-20_004 2157 9 0.148 0.126 0.000 0.550 1.012 3.259
BNL0-20_005 2164 13 0.171 0.137 0.000 0.650 1.252 4.475
BNL0-20_006 2180 7 0.142 0.123 0.000 0.650 1.701 6.405
BNL0-20_007 2195 4 0.121 0.138 0.000 0.800 2.531 10.154
BNL0-20_008 2177 19 0.251 0.240 0.000 0.950 0.941 2.986
BNL0-20_009 2140 18 0.223 0.224 0.000 0.900 1.101 3.348
BNL0-20_010 2162 11 0.161 0.133 0.000 0.550 0.821 2.734
UBNL0-20_001 4567 5 0.091 0.138 0.000 0.855 3.452 16.362
UBNL0-20_002 4426 16 0.198 0.141 0.000 0.535 0.652 2.447
UBNL0-20_003 4470 9 0.165 0.166 0.000 0.739 1.741 5.660
UBNL0-20_004 4448 13 0.209 0.167 0.000 0.623 0.840 2.758
UBNL0-20_005 4472 2 0.070 0.169 0.000 0.942 3.765 16.950
UBNL0-20_006 4444 7 0.136 0.157 0.000 0.797 2.272 8.478
UBNL0-20_007 4441 4 0.096 0.150 0.000 0.884 3.256 14.471
UBNL0-20_008 4414 19 0.234 0.138 0.000 0.552 0.077 1.882
UBNL0-20_009 4386 18 0.227 0.135 0.001 0.523 0.158 1.937
UBNL0-20_010 4432 11 0.191 0.161 0.000 0.681 1.197 3.980
UBNL0-20_011 2210 7 0.103 0.119 0.000 0.793 2.909 13.843
UBNL0-20_012 2203 19 0.194 0.124 0.000 0.552 0.444 2.304
UBNL0-20_013 2217 4 0.085 0.134 0.000 0.883 3.425 16.191
UBNL0-20_014 2199 9 0.120 0.128 0.000 0.738 2.283 9.299
UBNL0-20_015 2201 13 0.138 0.123 0.000 0.621 1.594 5.651
UBNL0-20_016 2202 5 0.096 0.124 0.000 0.853 3.038 14.480
UBNL0-20_017 2203 2 0.058 0.135 0.000 0.933 4.099 21.005
UBNL0-20_018 2183 16 0.163 0.116 0.000 0.535 0.876 3.335
UBNL0-20_019 2187 11 0.132 0.122 0.000 0.680 1.924 7.624
UBNL0-20_020 2191 18 0.176 0.121 0.000 0.523 0.677 2.683

7.3 Inverse Efficiency

Code
nl_scored_item_ie_summary <- nl_scored %>% 
  group_by(`Question Identifier`) %>% 
  summarise(
    n      = n_distinct(Identifier),
    Target = first(`Answer Key`),
    IE_mean = mean(IE),
    IE_sd   = sd(IE),
    IE_min  = min(IE),
    IE_max  = max(IE),
    IE_skew = moments::skewness(IE),
    IE_kurt = moments::kurtosis(IE),
    .groups = "drop"
  )

nl_scored_item_ie_summary %>% 
  mutate(across(where(is.numeric), round, 3)) %>% 
  kbl(
    caption   = "Item-level IE summary",
    digits    = 3,
    align     = c("l", rep("r", 8)),  # left for ID, right for numbers
    col.names = c("Item", "n", "Target",
                  "Mean", "SD", "Min", "Max", "Skew", "Kurt")
  ) |>
  # kableExtra niceties
  kable_styling(full_width = FALSE, position = "left") %>% 
  scroll_box(width = "100%",  height = "500px")   
Item-level IE summary
Item n Target Mean SD Min Max Skew Kurt
BNL0-100_001 2232 59 48.326 414.360 3.041 18732.379 41.468 1853.533
BNL0-100_002 2201 20 22.888 24.941 2.189 326.970 4.607 39.765
BNL0-100_003 2220 46 15.837 16.878 1.083 352.943 6.000 85.190
BNL0-100_004 2232 13 13.561 13.424 1.084 174.244 3.336 22.820
BNL0-100_005 2219 72 14.368 111.880 1.161 5253.167 46.241 2165.514
BNL0-100_006 2223 31 10.880 10.275 1.016 120.056 3.414 21.618
BNL0-100_007 2241 97 14.098 20.274 1.272 394.544 9.468 135.477
BNL0-100_008 2225 85 12.254 84.984 1.037 3989.279 46.024 2153.591
BNL0-100_009 2216 68 50.856 1950.165 1.015 91873.442 47.073 2216.905
BNL0-100_010 2254 4 8.815 9.927 1.076 187.326 7.968 102.045
BNL0-100_011 2209 13 9.140 9.715 1.027 124.881 4.476 34.740
BNL0-100_012 2207 68 53.031 2102.634 1.006 98853.553 46.977 2207.918
BNL0-100_013 2194 85 8.355 7.021 1.004 113.288 3.974 36.167
BNL0-100_014 2197 31 48.912 1943.868 1.012 91203.042 46.882 2198.939
BNL0-100_015 2191 72 7.543 6.854 1.025 98.323 4.388 38.823
BNL0-100_016 2187 97 8.927 13.427 1.050 366.667 14.500 316.288
BNL0-100_017 2220 20 7.510 7.545 0.000 96.546 4.146 29.446
BNL0-100_018 2216 59 7.610 8.898 1.005 160.427 10.212 150.460
BNL0-100_019 2248 4 6.063 5.739 1.010 92.089 4.993 45.074
BNL0-100_020 2214 46 9.079 56.865 1.008 2664.628 45.979 2147.159
BNL0-20_001 2194 2 41.034 121.540 2.033 4725.990 27.959 1021.534
BNL0-20_002 2038 5 24.418 45.155 1.033 1712.420 26.620 961.107
BNL0-20_003 2113 16 27.786 31.000 1.059 642.862 6.504 91.113
BNL0-20_004 2157 9 17.680 16.280 1.007 204.633 4.085 33.965
BNL0-20_005 2164 13 17.898 19.112 1.003 289.794 4.695 42.001
BNL0-20_006 2180 7 13.020 12.584 1.028 148.525 3.848 26.346
BNL0-20_007 2195 4 10.565 9.912 1.011 131.992 4.118 32.665
BNL0-20_008 2177 19 19.489 31.290 1.019 545.170 8.575 110.453
BNL0-20_009 2140 18 14.198 18.171 1.017 281.831 5.614 53.869
BNL0-20_010 2162 11 11.493 12.581 1.001 352.253 11.027 259.080
UBNL0-20_001 4567 5 18.573 34.715 1.375 953.931 15.466 334.798
UBNL0-20_002 4426 16 16.160 47.399 1.007 2978.996 55.141 3438.742
UBNL0-20_003 4470 9 11.162 12.198 1.002 140.079 3.958 27.474
UBNL0-20_004 4448 13 11.668 13.349 0.000 228.133 4.786 47.500
UBNL0-20_005 4472 2 8.292 17.817 1.001 424.109 11.946 194.980
UBNL0-20_006 4444 7 8.809 11.543 1.002 192.405 6.896 79.157
UBNL0-20_007 4441 4 7.502 10.594 1.001 353.008 13.071 324.220
UBNL0-20_008 4414 19 10.791 14.004 1.007 454.201 11.299 277.666
UBNL0-20_009 4386 18 8.979 10.128 0.000 204.350 5.347 63.263
UBNL0-20_010 4432 11 8.518 11.227 0.000 317.308 11.446 263.430
UBNL0-20_011 2210 7 5.523 5.625 1.003 69.472 4.729 37.771
UBNL0-20_012 2203 19 6.623 7.531 0.000 179.450 7.409 134.453
UBNL0-20_013 2217 4 5.159 9.514 1.002 299.996 21.488 595.597
UBNL0-20_014 2199 9 5.361 5.259 1.002 71.540 4.612 40.179
UBNL0-20_015 2201 13 5.916 6.593 0.000 107.964 4.783 45.236
UBNL0-20_016 2202 5 4.958 5.673 1.000 96.417 7.983 101.623
UBNL0-20_017 2203 2 4.484 7.869 0.000 202.274 17.009 385.847
UBNL0-20_018 2183 16 6.345 7.159 0.000 87.223 4.268 30.188
UBNL0-20_019 2187 11 5.301 5.758 0.000 81.820 4.628 37.589
UBNL0-20_020 2191 18 6.088 6.436 1.001 68.049 3.635 21.744

7.4 Signed error

Code
nl_bias <- nl_scored %>%
  mutate(
    signed_error = Number_clicked - `Answer Key`
  )

ggplot(nl_bias, aes(x = `Answer Key`, y = signed_error)) +
  geom_point(alpha = 0.2, size = 0.8) +        # scatter of all clicks
  geom_smooth(method = "lm", colour = "red") + 
  facet_wrap(~ TestType, scales = "free") +
  labs(
    x = "Target number",
    y = "Signed error (clicked – target)",
    title = "Signed error vs target",
  ) +
  theme_bw()

7.5 Standard dev of abs error by target

Code
abs_error_sd <- nl_scored %>% 
  group_by(TestType, Target = `Answer Key`) %>% 
  summarise(
    sd_abs_error = sd(abs_error, na.rm = TRUE),
    .groups      = "drop"
  )

ggplot(abs_error_sd, aes(x = Target, y = sd_abs_error)) +
  geom_line(colour = "steelblue", size = 0.8) +
  geom_point(colour = "steelblue", size = 1.5) +
  facet_wrap(~ TestType, scales = "free_x", nrow = 1) +
  scale_x_continuous(breaks = pretty_breaks()) +
  labs(
    title =   "SD of Absolute Error by Target Number",
    x =       "Target number",
    y =       "SD of |clicked – target|",
    caption = "Each point = SD over all pupils for that target"
  ) +
  theme_bw() +
  theme(
    panel.grid.minor = element_blank(),
    strip.background = element_rect(fill = "grey90"),
    strip.text       = element_text(face = "bold")
  )

7.6 Click spread

Code
click_spread_df <- nl_scored %>%
  group_by(TestType, Identifier, Target = `Answer Key`) %>%
  summarise(
    n_attempts   = n(),                                
    click_spread = max(Number_clicked, na.rm = TRUE)
                 - min(Number_clicked, na.rm = TRUE),
    .groups = "drop"
  ) %>% 
  filter(TestType != "BNL0-20")

plot_spread <- function(df, probe_name) {
  df %>%
    mutate(Target = factor(Target, levels = sort(unique(Target)))) %>%
    ggplot(aes(x = click_spread,
               y = Target)) +
    geom_boxplot(fill = "steelblue", colour = "grey30", width = 0.6) +
    labs(
      title = paste0("Click spread by target number - ",probe_name),
      x     = "Click spread",
      y     = "Target number"
    ) +
    theme_bw() +
    theme(
      axis.text.y   = element_text(size = 8),
      panel.grid.minor = element_blank()
    )
}

click_spread_df %>%
  split(.$TestType) %>%
  iwalk(function(df_probe, probe_name) {
    p <- plot_spread(df_probe, probe_name)
    print(p)
    ggsave(
      filename = file.path(probe_dir, glue("click_spread_{probe_name}.png")),
      plot     = p,
      width    = 8, height = 6, units = "in", dpi = 300
    )
  })

7.7 Midpoint click position metrics

Code
nl_mid <- nl_scored %>%
  group_by(TestType, Identifier, `Answer Key`) %>%
  summarise(
    click_mean   = mean(Number_clicked, na.rm = TRUE),
    dur_mean     = mean(`Answer Duration`,   na.rm = TRUE),
    n_left       = first(n_left),
    n_right      = first(n_right),
    .groups      = "drop"
  ) %>%
  mutate(
    abs_error       = abs(click_mean - `Answer Key`),
    PAE_mid         = abs_error / (n_right - n_left),
    IE_mid          = dur_mean / (1 - PAE_mid)
  ) %>%
  select(TestType, Identifier, `Answer Key`,
         click_mean, dur_mean,
         abs_error, PAE_mid, IE_mid)

item_pae_mid_summary <- nl_mid %>%
  group_by(TestType, Target = `Answer Key`) %>%
  summarise(
    n        = n_distinct(Identifier),
    pae_mean = mean(PAE_mid, na.rm = TRUE),
    pae_sd   = sd(PAE_mid,   na.rm = TRUE),
    pae_min  = min(PAE_mid,  na.rm = TRUE),
    pae_max  = max(PAE_mid,  na.rm = TRUE),
    pae_skew = skewness(PAE_mid),
    pae_kurt = kurtosis(PAE_mid),
    .groups  = "drop"
  )

item_ie_mid_summary <- nl_mid %>%
  group_by(TestType, Target = `Answer Key`) %>%
  summarise(
    n       = n_distinct(Identifier),
    ie_mean = mean(IE_mid, na.rm = TRUE),
    ie_sd   = sd(IE_mid,   na.rm = TRUE),
    ie_min  = min(IE_mid,  na.rm = TRUE),
    ie_max  = max(IE_mid,  na.rm = TRUE),
    ie_skew = skewness(IE_mid),
    ie_kurt = kurtosis(IE_mid),
    .groups = "drop"
  )

item_pae_mid_summary %>% 
  mutate(across(where(is.numeric), ~ round(.x, 3))) %>% 
  kbl(
    caption   = "Item-level PAE (midpoint) summary",
    align     = c("l", rep("r", 7)),  
    col.names = c("Probe", "Target", "n",
                  "Mean_PAE", "SD_PAE", "Min_PAE", "Max_PAE", "Skew_PAE", "Kurt_PAE")
  ) %>% 
  kable_styling(full_width = FALSE, position = "left") %>% 
  scroll_box(width = "100%", height = "400px")
Item-level PAE (midpoint) summary
Probe Target n Mean_PAE SD_PAE Min_PAE Max_PAE Skew_PAE Kurt_PAE
BNL0-100 4 2295 0.094 0.111 0.000 0.955 3.231 16.564
BNL0-100 13 2288 0.172 0.137 0.000 0.870 1.266 4.957
BNL0-100 20 2301 0.163 0.134 0.000 0.794 1.328 5.030
BNL0-100 31 2293 0.122 0.110 0.000 0.684 1.569 5.818
BNL0-100 46 2299 0.101 0.084 0.000 0.540 1.537 5.826
BNL0-100 59 2308 0.079 0.073 0.000 0.590 1.855 7.912
BNL0-100 68 2284 0.115 0.099 0.000 0.673 1.514 6.044
BNL0-100 72 2293 0.109 0.103 0.000 0.719 1.865 7.567
BNL0-100 85 2286 0.166 0.127 0.000 0.850 1.229 4.953
BNL0-100 97 2292 0.200 0.162 0.001 0.965 1.484 5.117
BNL0-20 2 2194 0.108 0.168 0.000 0.900 2.448 8.971
BNL0-20 4 2195 0.121 0.138 0.000 0.800 2.524 10.105
BNL0-20 5 2038 0.124 0.121 0.000 0.750 2.597 11.820
BNL0-20 7 2180 0.142 0.123 0.000 0.650 1.700 6.392
BNL0-20 9 2157 0.148 0.126 0.000 0.550 1.015 3.266
BNL0-20 11 2162 0.161 0.133 0.000 0.550 0.820 2.734
BNL0-20 13 2164 0.171 0.136 0.000 0.650 1.258 4.509
BNL0-20 16 2113 0.208 0.183 0.000 0.800 1.218 3.923
BNL0-20 18 2140 0.223 0.224 0.000 0.900 1.102 3.350
BNL0-20 19 2177 0.250 0.240 0.000 0.950 0.943 2.992
UNL0-20 2 4529 0.074 0.168 0.000 0.942 3.588 15.902
UNL0-20 4 4507 0.100 0.148 0.000 0.884 3.056 13.293
UNL0-20 5 4604 0.096 0.137 0.000 0.855 3.231 15.068
UNL0-20 7 4507 0.136 0.152 0.000 0.797 2.195 8.248
UNL0-20 9 4542 0.162 0.162 0.000 0.739 1.697 5.580
UNL0-20 11 4491 0.187 0.159 0.000 0.681 1.214 4.028
UNL0-20 13 4515 0.202 0.164 0.000 0.623 0.883 2.866
UNL0-20 16 4501 0.192 0.137 0.000 0.535 0.708 2.597
UNL0-20 18 4462 0.216 0.136 0.000 0.523 0.270 1.981
UNL0-20 19 4467 0.226 0.138 0.000 0.552 0.157 1.911
Code
item_ie_mid_summary %>% 
  mutate(across(where(is.numeric), ~ round(.x, 3))) %>% 
  kbl(
    caption   = "Item-level IE (midpoint) summary",
    align     = c("l", rep("r", 7)),  
    col.names = c("Probe", "Target", "n",
                  "Mean_IE", "SD_IE", "Min_IE", "Max_IE", "Skew_IE", "Kurt_IE")
  ) %>% 
  kable_styling(full_width = FALSE, position = "left") %>% 
  scroll_box(width = "100%", height = "400px")
Item-level IE (midpoint) summary
Probe Target n Mean_IE SD_IE Min_IE Max_IE Skew_IE Kurt_IE
BNL0-100 4 2295 7.371 6.106 1.106 88.076 5.075 46.364
BNL0-100 13 2288 11.267 9.681 1.105 145.854 3.408 27.639
BNL0-100 20 2301 14.869 14.632 0.000 326.970 6.567 103.660
BNL0-100 31 2293 28.838 947.706 1.103 45388.901 47.850 2290.737
BNL0-100 46 2299 12.882 56.318 1.035 2664.628 45.469 2139.837
BNL0-100 59 2308 28.167 226.214 1.097 10456.514 42.812 1960.154
BNL0-100 68 2284 50.814 1421.811 1.011 49403.732 33.788 1143.579
BNL0-100 72 2293 10.650 46.432 1.284 2199.333 45.721 2154.944
BNL0-100 85 2286 10.106 35.844 1.242 1682.890 44.542 2076.931
BNL0-100 97 2292 11.087 11.453 1.467 304.066 10.228 209.937
BNL0-20 2 2194 41.104 121.838 2.033 4725.990 27.892 1016.591
BNL0-20 4 2195 10.567 9.934 1.011 131.992 4.112 32.541
BNL0-20 5 2038 24.409 45.263 1.033 1712.420 26.574 957.204
BNL0-20 7 2180 13.033 12.610 1.028 148.525 3.841 26.246
BNL0-20 9 2157 17.694 16.315 1.007 204.633 4.075 33.818
BNL0-20 11 2162 11.504 12.607 1.001 352.253 11.013 258.232
BNL0-20 13 2164 17.904 19.150 1.003 289.794 4.690 41.878
BNL0-20 16 2113 27.771 31.032 1.059 642.862 6.510 91.136
BNL0-20 18 2140 14.203 18.212 1.017 281.831 5.604 53.661
BNL0-20 19 2177 19.471 31.350 1.019 545.170 8.570 110.188
UNL0-20 2 4529 7.863 17.296 0.854 424.109 12.568 214.465
UNL0-20 4 4507 7.153 9.847 1.001 353.008 13.882 379.291
UNL0-20 5 4604 16.522 33.756 1.091 953.931 16.017 361.207
UNL0-20 7 4507 8.378 10.696 1.005 192.405 7.098 86.225
UNL0-20 9 4542 10.290 11.712 1.000 140.079 4.384 32.333
UNL0-20 11 4491 8.169 10.881 0.000 317.308 12.243 294.772
UNL0-20 13 4515 10.768 12.624 0.000 228.133 5.247 56.836
UNL0-20 16 4501 14.392 46.741 1.007 2978.996 56.841 3597.465
UNL0-20 18 4462 8.561 9.294 0.567 173.073 4.582 43.581
UNL0-20 19 4467 9.992 11.703 1.018 251.524 6.715 91.432

8 Probe summary statistics

Note: We first aggregate each student’s overall accuracy (PAE) and efficiency (IE) on each probe, then calculate the probe level statistics.

8.1 PAE

Code
nl_scored_probe_pae_summary <- nl_student %>% 
  group_by(TestType) %>% 
  summarise(
    n      = n_distinct(Identifier),
    pae_mean = mean(PAE),
    pae_sd   = sd(PAE),
    pae_min  = min(PAE),
    pae_max  = max(PAE),
    pae_skew = moments::skewness(PAE),
    pae_kurt = moments::kurtosis(PAE),
    .groups = "drop"
  )
  
nl_scored_probe_pae_summary %>% 
  mutate(across(where(is.numeric), round, 3)) %>% 
  kbl(
    caption   = "Probe-level PAE summary",
    digits    = 3,
    align     = c("l", rep("r", 7)),   # 1 left + 7 right = 8
    col.names = c("Probe", "n",
                  "Mean", "SD", "Min", "Max",
                  "Skew", "Kurt")
  ) %>% 
  kable_styling(full_width = FALSE, position = "left") %>% 
  scroll_box(width = "100%", height = "400px")
Probe-level PAE summary
Probe n Mean SD Min Max Skew Kurt
BNL0-100 2310 0.141 0.073 0.003 0.514 1.031 4.140
BNL0-20 2334 0.168 0.101 0.006 0.773 1.199 4.805
UNL0-20 4691 0.166 0.095 0.006 0.688 1.466 6.386

8.2 Inverse efficiency

Code
nl_scored_probe_ie_summary <- nl_student %>% 
  group_by(TestType) %>% 
  summarise(
    n      = n_distinct(Identifier),
    IE_mean = mean(IE),
    IE_sd   = sd(IE),
    IE_min  = min(IE),
    IE_max  = max(IE),
    IE_skew = moments::skewness(IE),
    IE_kurt = moments::kurtosis(IE),
    .groups = "drop"
  )
  
nl_scored_probe_ie_summary%>% 
  mutate(across(where(is.numeric), round, 3)) %>% 
  kbl(
    caption   = "Probe-level IE summary",
    digits    = 3,
    align     = c("l", rep("r", 7)),   # 1 left + 7 right = 8
    col.names = c("Probe", "n",
                  "Mean", "SD", "Min", "Max",
                  "Skew", "Kurt")
  ) %>% 
  kable_styling(full_width = FALSE, position = "left") %>% 
  scroll_box(width = "100%", height = "400px")
Probe-level IE summary
Probe n Mean SD Min Max Skew Kurt
BNL0-100 2310 19.430 177.640 2.333 5214.747 26.900 739.882
BNL0-20 2334 20.962 23.607 1.717 543.390 10.645 183.662
UNL0-20 4691 10.787 10.874 1.692 310.487 8.510 162.358

9 Export

# Prepare export tables
item_responses <- nl_scored %>% 
  select(Identifier, GradeGroup, `Test Identifier`, Test, 
         `Question Identifier`, `Answer Duration`, Number_clicked, 
         `Answer Key`, abs_error, PAE, IE, `Date Completed`)

item_summary <- nl_scored_item_pae_summary %>% 
  left_join(nl_scored_item_ie_summary %>% 
              select(-n, -Target),
            by = "Question Identifier")

probe_mean_summary <- nl_scored_probe_pae_summary %>% 
  left_join(nl_scored_probe_ie_summary %>% select(-n),
            by = "TestType")

mid_point_summary <- item_pae_mid_summary %>% 
  left_join(
    item_ie_mid_summary %>% select(-n), 
    by = c("TestType", "Target")
  )

# Bundle into list and export
csv_out <- list(
  Number_line_item_response      = item_responses,
  Number_line_item_summary       = item_summary,
  Number_line_probe_mean_summary = probe_mean_summary,
  Item_mid_point_summary = mid_point_summary,
  Click_spread = click_spread_df
)

imap(csv_out, ~ {
  out_path <- file.path(probe_dir, paste0(.y, ".csv"))
  write_csv(.x, out_path)
})
$Number_line_item_response
# A tibble: 132,736 × 12
   Identifier           GradeGroup `Test Identifier` Test  `Question Identifier`
   <chr>                <chr>      <chr>             <chr> <chr>                
 1 00107783-010a-f011-… Foundatio… BNL0-20F_2025     Boun… BNL0-20_001          
 2 00107783-010a-f011-… Foundatio… BNL0-20F_2025     Boun… BNL0-20_002          
 3 00107783-010a-f011-… Foundatio… BNL0-20F_2025     Boun… BNL0-20_003          
 4 00107783-010a-f011-… Foundatio… BNL0-20F_2025     Boun… BNL0-20_004          
 5 00107783-010a-f011-… Foundatio… BNL0-20F_2025     Boun… BNL0-20_005          
 6 00107783-010a-f011-… Foundatio… BNL0-20F_2025     Boun… BNL0-20_006          
 7 00107783-010a-f011-… Foundatio… BNL0-20F_2025     Boun… BNL0-20_007          
 8 00107783-010a-f011-… Foundatio… BNL0-20F_2025     Boun… BNL0-20_008          
 9 00107783-010a-f011-… Foundatio… BNL0-20F_2025     Boun… BNL0-20_009          
10 00107783-010a-f011-… Foundatio… BNL0-20F_2025     Boun… BNL0-20_010          
# ℹ 132,726 more rows
# ℹ 7 more variables: `Answer Duration` <dbl>, Number_clicked <dbl>,
#   `Answer Key` <dbl>, abs_error <dbl>, PAE <dbl>, IE <dbl>,
#   `Date Completed` <chr>

$Number_line_item_summary
# A tibble: 50 × 15
   `Question Identifier`     n Target pae_mean pae_sd   pae_min pae_max pae_skew
   <chr>                 <int>  <dbl>    <dbl>  <dbl>     <dbl>   <dbl>    <dbl>
 1 BNL0-100_001           2232     59   0.0854 0.0864 0.000120    0.59      1.93
 2 BNL0-100_002           2201     20   0.171  0.137  0.000185    0.788     1.44
 3 BNL0-100_003           2220     46   0.103  0.0952 0.0000880   0.521     1.52
 4 BNL0-100_004           2232     13   0.168  0.146  0.000250    0.866     1.45
 5 BNL0-100_005           2219     72   0.113  0.114  0.000111    0.718     2.01
 6 BNL0-100_006           2223     31   0.130  0.121  0.000232    0.69      1.69
 7 BNL0-100_007           2241     97   0.200  0.173  0.000584    0.968     1.70
 8 BNL0-100_008           2225     85   0.168  0.132  0.0000463   0.85      1.52
 9 BNL0-100_009           2216     68   0.128  0.112  0.0000927   0.68      1.59
10 BNL0-100_010           2254      4   0.0904 0.121  0.000130    0.951     3.91
# ℹ 40 more rows
# ℹ 7 more variables: pae_kurt <dbl>, IE_mean <dbl>, IE_sd <dbl>, IE_min <dbl>,
#   IE_max <dbl>, IE_skew <dbl>, IE_kurt <dbl>

$Number_line_probe_mean_summary
# A tibble: 3 × 14
  TestType     n pae_mean pae_sd pae_min pae_max pae_skew pae_kurt IE_mean IE_sd
  <chr>    <int>    <dbl>  <dbl>   <dbl>   <dbl>    <dbl>    <dbl>   <dbl> <dbl>
1 BNL0-100  2310    0.141 0.0733 0.00335   0.514     1.03     4.14    19.4 178. 
2 BNL0-20   2334    0.168 0.101  0.00574   0.773     1.20     4.81    21.0  23.6
3 UNL0-20   4691    0.166 0.0952 0.00637   0.688     1.47     6.39    10.8  10.9
# ℹ 4 more variables: IE_min <dbl>, IE_max <dbl>, IE_skew <dbl>, IE_kurt <dbl>

$Item_mid_point_summary
# A tibble: 30 × 15
   TestType Target     n pae_mean pae_sd    pae_min pae_max pae_skew pae_kurt
   <chr>     <dbl> <int>    <dbl>  <dbl>      <dbl>   <dbl>    <dbl>    <dbl>
 1 BNL0-100      4  2295   0.0938 0.111  0.000148     0.955     3.23    16.6 
 2 BNL0-100     13  2288   0.172  0.137  0.000213     0.87      1.27     4.96
 3 BNL0-100     20  2301   0.163  0.134  0.000185     0.794     1.33     5.03
 4 BNL0-100     31  2293   0.122  0.110  0.00000927   0.684     1.57     5.82
 5 BNL0-100     46  2299   0.101  0.0839 0.0000572    0.54      1.54     5.83
 6 BNL0-100     59  2308   0.0787 0.0729 0.000102     0.59      1.85     7.91
 7 BNL0-100     68  2284   0.115  0.0995 0.000204     0.673     1.51     6.04
 8 BNL0-100     72  2293   0.109  0.103  0.0000185    0.719     1.87     7.57
 9 BNL0-100     85  2286   0.166  0.127  0.000139     0.85      1.23     4.95
10 BNL0-100     97  2292   0.200  0.162  0.000584     0.965     1.48     5.12
# ℹ 20 more rows
# ℹ 6 more variables: ie_mean <dbl>, ie_sd <dbl>, ie_min <dbl>, ie_max <dbl>,
#   ie_skew <dbl>, ie_kurt <dbl>

$Click_spread
# A tibble: 68,064 × 5
   TestType Identifier                           Target n_attempts click_spread
   <chr>    <chr>                                 <dbl>      <int>        <dbl>
 1 BNL0-100 00137783-010a-f011-bcb9-8fb474f3e22e      4          2         2.04
 2 BNL0-100 00137783-010a-f011-bcb9-8fb474f3e22e     13          2         3.47
 3 BNL0-100 00137783-010a-f011-bcb9-8fb474f3e22e     20          2        11.1 
 4 BNL0-100 00137783-010a-f011-bcb9-8fb474f3e22e     31          2        45.4 
 5 BNL0-100 00137783-010a-f011-bcb9-8fb474f3e22e     46          2         8.79
 6 BNL0-100 00137783-010a-f011-bcb9-8fb474f3e22e     59          2        14.3 
 7 BNL0-100 00137783-010a-f011-bcb9-8fb474f3e22e     68          2        14.7 
 8 BNL0-100 00137783-010a-f011-bcb9-8fb474f3e22e     72          2         2.46
 9 BNL0-100 00137783-010a-f011-bcb9-8fb474f3e22e     85          2         6.47
10 BNL0-100 00137783-010a-f011-bcb9-8fb474f3e22e     97          2         1.01
# ℹ 68,054 more rows