diff --git a/DESCRIPTION b/DESCRIPTION index 9d473684..4f3643c9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -19,6 +19,8 @@ RoxygenNote: 7.3.2 Imports: primer.data, tutorial.helpers, + dplyr, + glue, katex, gt, tibble diff --git a/R/make_p_tables.R b/R/make_p_tables.R index 164ce6ae..455aaa23 100644 --- a/R/make_p_tables.R +++ b/R/make_p_tables.R @@ -1,18 +1,18 @@ #' Insert Preceptor and Population Table Templates in Quarto -#' +#' #' Inserts a Quarto-ready template consisting of multiple code chunks for creating #' **Preceptor Tables** and **Population Tables**. These tables support both causal #' and predictive workflows. #' #' The output includes: -#' - Editable footnotes for documentation #' - Empty `tibble`s for the Preceptor Table and Population Table (the latter includes #' the Preceptor rows) +#' - Editable footnotes for documentation #' - `gt` code chunks to render each table with labeled spanners and columns #' sized roughly proportional to label length #' - The Preceptor and Population tables include a final "More" column and #' a last empty row added during rendering for easier editing -#' +#' #' @name make_p_tables #' @title Insert Preceptor and Population Table Templates #' @@ -50,16 +50,17 @@ #' #' @examples #' \dontrun{ -#' # Insert causal tables for a study of senators' voting behavior over years +#' # Insert causal tables for a study of senators' voting behavior +#' # Outcomes reflect support conditional on the treatment #' make_p_tables( #' type = "causal", #' unit_label = c("Senator", "Session Year"), -#' outcome_label = c("Support Bill", "Oppose Bill"), +#' outcome_label = c("Support if Contact", "Support if No Contact"), #' treatment_label = "Lobbying Contact", #' covariate_label = "Senator Age" #' ) #' -#' # Insert predictive tables for a clinical trial measuring patient recovery over time +#' # Insert predictive tables for a clinical trial measuring patient recovery #' make_p_tables( #' type = "predictive", #' unit_label = c("Patient ID", "Visit Number"), @@ -69,6 +70,8 @@ #' ) #' } + + make_p_tables <- function( type, unit_label, @@ -88,113 +91,218 @@ make_p_tables <- function( stop("`type` must be either 'causal' or 'predictive'.") } -# DK: Add Source at end. Not now. - + # Both p_tibble and d_tibble use the same columns (no Source column yet) all_cols <- c(unit_label, outcome_label, treatment_label, covariate_label) - pop_cols <- if (source_col) c("Source", all_cols) else all_cols - - p_col_headers <- paste(make_labels(all_cols), collapse = ", ") - d_col_headers <- paste(make_labels(pop_cols), collapse = ", ") - -# Don't need duplicate code. - - p_rows <- paste( - paste(rep('"..."', length(all_cols)), collapse = ", "), - paste(rep('"..."', length(all_cols)), collapse = ", "), - paste(rep('"..."', length(all_cols)), collapse = ", "), - sep = ",\n " - ) - d_rows <- paste( - paste(rep('"..."', length(pop_cols)), collapse = ", "), - paste(rep('"..."', length(pop_cols)), collapse = ", "), - paste(rep('"..."', length(pop_cols)), collapse = ", "), - sep = ",\n " - ) - - # No need to rename these variables. + + # Source column only added during population table rendering + pop_unit_cols <- if (source_col) c("Source", unit_label) else unit_label - unit_spanner_cols <- unit_label - outcome_spanner_cols <- outcome_label - treatment_spanner_cols <- treatment_label - covariate_spanner_cols <- covariate_label - pop_unit_cols <- if (source_col) c("Source", unit_spanner_cols) else unit_spanner_cols + # Generate tribble code using helper function + p_tribble_code <- write_input_tribble(all_cols) + d_tribble_code <- write_input_tribble(all_cols) widths <- c( - nchar(unit_label[1]) + 2, - nchar(unit_label[2]) + 2, - rep(nchar(outcome_label[1]) + 2, length(outcome_label)), - nchar(treatment_label) + 2, - nchar(covariate_label) + 2, - 5 + if (source_col) 80 else NULL, # Source column width + max(nchar(unit_label[1]) * 8, 100), # Minimum 100px for first unit column + max(nchar(unit_label[2]) * 8, 120), # Minimum 120px for second unit column + rep(max(max(nchar(outcome_label)) * 8, 120), length(outcome_label)), # Minimum 120px per outcome + max(nchar(treatment_label) * 8, 120), # Minimum 120px for treatment + max(nchar(covariate_label) * 8, 120), # Minimum 120px for covariate + 60 # More column ) glue_cols <- function(cols) paste0("`", cols, "`", collapse = ", ") code_footnotes <- glue::glue( "```{{r}} -pre_title_footnote <- \"Preceptor Table Title\" -pre_units_footnote <- \"Units and time information\" -pre_outcome_footnote <- \"Outcome or potential outcomes description\" -pre_treatment_footnote <- \"Treatment or intervention description\" -pre_covariates_footnote <- \"Covariates and their units\" - -pop_title_footnote <- \"Population Table Title\" -pop_units_footnote <- \"Units and time information\" -pop_outcome_footnote <- \"Outcome or potential outcomes description\" -pop_treatment_footnote <- \"Treatment or intervention description\" -pop_covariates_footnote <- \"Covariates and their units\" - -p_tibble <- tibble::tribble( - {p_col_headers}, - {p_rows} -) +# Edit the following tibbles and footnotes, look at the vignette for more details + +p_tibble <- {p_tribble_code} -d_tibble <- tibble::tribble( - {d_col_headers}, - {d_rows} -) +d_tibble <- {d_tribble_code} + +pre_title_footnote <- \"...\" +pre_units_footnote <- \"...\" +pre_outcome_footnote <- \"...\" +pre_treatment_footnote <- \"...\" +pre_covariates_footnote <- \"...\" + +pop_title_footnote <- \"...\" +pop_units_footnote <- \"...\" +pop_outcome_footnote <- \"...\" +pop_treatment_footnote <- \"...\" +pop_covariates_footnote <- \"...\" ```" ) code_p_table <- glue::glue( "```{{r}} -p_tibble_full <- p_tibble |> - dplyr::add_row(!!!as.list(rep(NA, ncol(p_tibble)))) |> - dplyr::mutate(More = c(rep(NA, nrow(.) - 1), \"...\")) +# This code chunk will generate the Preceptor Table + +p_tibble_full <- expand_input_tibble(list(p_tibble), \"preceptor\") gt::gt(p_tibble_full) |> gt::tab_header(title = \"Preceptor Table\") |> - gt::tab_spanner(label = \"Unit\", id = \"unit_span\", columns = c({glue_cols(unit_spanner_cols)})) |> - gt::tab_spanner(label = \"Potential Outcomes\", id = \"outcome_span\", columns = c({glue_cols(outcome_spanner_cols)})) |> - gt::tab_spanner(label = \"Treatment\", id = \"treatment_span\", columns = c({glue_cols(treatment_spanner_cols)})) |> - gt::tab_spanner(label = \"Covariates\", id = \"covariates_span\", columns = c({glue_cols(covariate_spanner_cols)})) |> + gt::tab_spanner(label = \"Unit/Time\", id = \"unit_span\", columns = c({glue_cols(unit_label)})) |> + gt::tab_spanner(label = \"Potential Outcomes\", id = \"outcome_span\", columns = c({glue_cols(outcome_label)})) |> + gt::tab_spanner(label = \"Treatment\", id = \"treatment_span\", columns = c({glue_cols(treatment_label)})) |> + gt::tab_spanner(label = \"Covariates\", id = \"covariates_span\", columns = c({glue_cols(covariate_label)}, \"More\")) |> gt::cols_align(align = \"center\", columns = gt::everything()) |> gt::cols_align(align = \"left\", columns = c(`{unit_label[1]}`)) |> - gt::cols_width(columns = c({glue_cols(c(unit_spanner_cols, outcome_spanner_cols, treatment_spanner_cols, covariate_spanner_cols, \"More\"))}), - widths = gt::px(c({paste(widths, collapse = \", \")}))) |> - gt::fmt_markdown(columns = gt::everything()) + gt::cols_width({ + all_cols_with_more <- c(unit_label, outcome_label, treatment_label, covariate_label, \"More\") + width_assignments <- paste0('\"', all_cols_with_more, '\" ~ gt::px(', widths[!is.null(widths)], ')', collapse = \", \") + width_assignments + }) |> + gt::tab_style( + style = gt::cell_text(size = gt::px(14)), + locations = gt::cells_body() + ) |> + gt::tab_style( + style = list( + gt::cell_text(size = gt::px(14), weight = \"bold\"), + gt::cell_borders(sides = \"bottom\", weight = gt::px(2)) + ), + locations = gt::cells_column_labels() + ) |> + gt::tab_options( + table.font.size = gt::px(14), + data_row.padding = gt::px(12), + column_labels.padding = gt::px(12), + row_group.padding = gt::px(12), + table.width = gt::pct(100), + table.margin.left = gt::px(0), + table.margin.right = gt::px(0) + ) |> + gt::fmt_markdown(columns = gt::everything()) |> + gt::tab_footnote(footnote = pre_title_footnote, locations = gt::cells_title()) |> + gt::tab_footnote(footnote = pre_units_footnote, locations = gt::cells_column_spanners(spanners = \"unit_span\")) |> + gt::tab_footnote(footnote = pre_outcome_footnote, locations = gt::cells_column_spanners(spanners = \"outcome_span\")) |> + gt::tab_footnote(footnote = pre_treatment_footnote, locations = gt::cells_column_spanners(spanners = \"treatment_span\")) |> + gt::tab_footnote(footnote = pre_covariates_footnote, locations = gt::cells_column_spanners(spanners = \"covariates_span\")) ```" ) - code_pop_table <- glue::glue( - "```{{r}} -d_tibble_full <- d_tibble |> - dplyr::add_row(!!!as.list(rep(NA, ncol(d_tibble)))) |> - dplyr::mutate(More = c(rep(NA, nrow(.) - 1), \"...\")) + # Population table code - fixed to show all 4 data rows and proper structure + if (source_col) { + code_pop_table <- glue::glue( + "```{{r}} +# This code chunk will generate the Population Table + +data_tibble <- dplyr::bind_rows( + d_tibble[1:2, , drop = FALSE], + d_tibble[1, , drop = FALSE] |> dplyr::mutate(dplyr::across(dplyr::everything(), ~ \"...\")), + d_tibble[3, , drop = FALSE] +) |> + dplyr::mutate(Source = \"Data\", .before = 1) + +preceptor_tibble <- p_tibble_full |> + dplyr::select(-More) |> + dplyr::mutate(Source = \"Preceptor\", .before = 1) -gt::gt(d_tibble_full) |> +empty_row <- data_tibble[1, , drop = FALSE] +empty_row[,] <- \"...\" + +population_tibble <- dplyr::bind_rows( + empty_row, # Row 1: blank + data_tibble, # Rows 2-5: 4 data rows (3rd is blank) + empty_row, # Row 6: blank + preceptor_tibble, # Rows 7-10: 4 preceptor rows (3rd is blank) + empty_row # Row 11: blank +) + +population_tibble$More <- \"...\" + +gt::gt(population_tibble) |> gt::tab_header(title = \"Population Table\") |> gt::tab_spanner(label = \"Unit/Time\", id = \"unit_span\", columns = c({glue_cols(pop_unit_cols)})) |> - gt::tab_spanner(label = \"Potential Outcomes\", id = \"outcome_span\", columns = c({glue_cols(outcome_spanner_cols)})) |> - gt::tab_spanner(label = \"Treatment\", id = \"treatment_span\", columns = c({glue_cols(treatment_spanner_cols)})) |> - gt::tab_spanner(label = \"Covariates\", id = \"covariates_span\", columns = c({glue_cols(covariate_spanner_cols)})) |> + gt::tab_spanner(label = \"Potential Outcomes\", id = \"outcome_span\", columns = c({glue_cols(outcome_label)})) |> + gt::tab_spanner(label = \"Treatment\", id = \"treatment_span\", columns = c({glue_cols(treatment_label)})) |> + gt::tab_spanner(label = \"Covariates\", id = \"covariates_span\", columns = c({glue_cols(covariate_label)}, \"More\")) |> gt::cols_align(align = \"center\", columns = gt::everything()) |> gt::cols_align(align = \"left\", columns = c(`{unit_label[1]}`)) |> - gt::cols_width(columns = c({glue_cols(c(pop_unit_cols, outcome_spanner_cols, treatment_spanner_cols, covariate_spanner_cols, \"More\"))}), - widths = gt::px(c({paste(widths, collapse = \", \")}))) |> - gt::fmt_markdown(columns = gt::everything()) + gt::cols_width({ + all_cols_with_more <- c(pop_unit_cols, outcome_label, treatment_label, covariate_label, \"More\") + width_assignments <- paste0('\"', all_cols_with_more, '\" ~ gt::px(', widths[!is.null(widths)], ')', collapse = \", \") + width_assignments + }) |> + gt::tab_style( + style = gt::cell_text(size = gt::px(14)), + locations = gt::cells_body() + ) |> + gt::tab_style( + style = list( + gt::cell_text(size = gt::px(14), weight = \"bold\"), + gt::cell_borders(sides = \"bottom\", weight = gt::px(2)) + ), + locations = gt::cells_column_labels() + ) |> + gt::tab_options( + table.font.size = gt::px(14), + data_row.padding = gt::px(12), + column_labels.padding = gt::px(12), + row_group.padding = gt::px(12), + table.width = gt::pct(100), + table.margin.left = gt::px(0), + table.margin.right = gt::px(0) + ) |> + gt::fmt_markdown(columns = gt::everything()) |> + gt::tab_footnote(footnote = pop_title_footnote, locations = gt::cells_title()) |> + gt::tab_footnote(footnote = pop_units_footnote, locations = gt::cells_column_spanners(spanners = \"unit_span\")) |> + gt::tab_footnote(footnote = pop_outcome_footnote, locations = gt::cells_column_spanners(spanners = \"outcome_span\")) |> + gt::tab_footnote(footnote = pop_treatment_footnote, locations = gt::cells_column_spanners(spanners = \"treatment_span\")) |> + gt::tab_footnote(footnote = pop_covariates_footnote, locations = gt::cells_column_spanners(spanners = \"covariates_span\")) ```" - ) + ) + } else { + code_pop_table <- glue::glue( + "```{{r}} +# This code chunk will generate the Population Table + +data_tibble <- dplyr::bind_rows( + d_tibble[1:2, , drop = FALSE], + d_tibble[1, , drop = FALSE] |> dplyr::mutate(dplyr::across(dplyr::everything(), ~ \"...\")), + d_tibble[3, , drop = FALSE] +) + +preceptor_tibble <- p_tibble_full |> + dplyr::select(-More) + +empty_row <- data_tibble[1, , drop = FALSE] +empty_row[,] <- \"...\" + +population_tibble <- dplyr::bind_rows( + empty_row, # Row 1: blank + data_tibble, # Rows 2-5: 4 data rows (3rd is blank) + empty_row, # Row 6: blank + preceptor_tibble, # Rows 7-10: 4 preceptor rows (3rd is blank) + empty_row # Row 11: blank +) + +population_tibble$More <- \"...\" + +gt::gt(population_tibble) |> + gt::tab_header(title = \"Population Table\") |> + gt::tab_spanner(label = \"Unit/Time\", id = \"unit_span\", columns = c({glue_cols(pop_unit_cols)})) |> + gt::tab_spanner(label = \"Potential Outcomes\", id = \"outcome_span\", columns = c({glue_cols(outcome_label)})) |> + gt::tab_spanner(label = \"Treatment\", id = \"treatment_span\", columns = c({glue_cols(treatment_label)})) |> + gt::tab_spanner(label = \"Covariates\", id = \"covariates_span\", columns = c({glue_cols(covariate_label)}, \"More\")) |> + gt::cols_align(align = \"center\", columns = gt::everything()) |> + gt::cols_align(align = \"left\", columns = c(`{unit_label[1]}`)) |> + gt::cols_width({ + all_cols_with_more <- c(pop_unit_cols, outcome_label, treatment_label, covariate_label, \"More\") + width_assignments <- paste0('\"', all_cols_with_more, '\" ~ gt::px(', widths[!is.null(widths)], ')', collapse = \", \") + width_assignments + }) |> + gt::fmt_markdown(columns = gt::everything()) |> + gt::tab_footnote(footnote = pop_title_footnote, locations = gt::cells_title()) |> + gt::tab_footnote(footnote = pop_units_footnote, locations = gt::cells_column_spanners(spanners = \"unit_span\")) |> + gt::tab_footnote(footnote = pop_outcome_footnote, locations = gt::cells_column_spanners(spanners = \"outcome_span\")) |> + gt::tab_footnote(footnote = pop_treatment_footnote, locations = gt::cells_column_spanners(spanners = \"treatment_span\")) |> + gt::tab_footnote(footnote = pop_covariates_footnote, locations = gt::cells_column_spanners(spanners = \"covariates_span\")) +```" + ) + } full_code <- paste( code_footnotes, @@ -210,8 +318,3 @@ gt::gt(d_tibble_full) |> invisible(NULL) } - -make_labels <- function(x) { - paste0("~`", x, "`") -} - diff --git a/R/p_table_helpers.R b/R/p_table_helpers.R index b2c619d3..869cbc61 100644 --- a/R/p_table_helpers.R +++ b/R/p_table_helpers.R @@ -2,12 +2,12 @@ #' #' Generates a character string representing an R tibble::tribble() #' with the specified column names and 3 rows filled with placeholder -#' text `"..."`. Useful for quickly creating editable table templates. +#' text `"..."`. Column values are aligned under their headers for easy editing. #' #' @param names Character vector of column names. #' #' @return Character string containing the R code for an input tribble with placeholders `"..."`, -#' formatted for manual editing. +#' formatted for manual editing with aligned columns. #' #' @examples #' write_input_tribble(c("Unit", "Year", "Outcome", "Treatment")) @@ -15,16 +15,38 @@ #' @export write_input_tribble <- function(names) { n <- length(names) - # Create 3 rows of placeholders - rows <- replicate(3, paste(rep('"..."', n), collapse = ", "), simplify = FALSE) - # Prepare header row with backtick quoted column names - header <- paste0("~`", names, "`", collapse = ", ") - # Construct tribble text with line breaks for readability + # Calculate column widths based on header names (with backticks and ~) + header_widths <- nchar(paste0("~`", names, "`")) + # Ensure minimum width of 5 for "..." placeholder + col_widths <- pmax(header_widths, 5) + + # Create properly spaced header row + headers_spaced <- character(n) + for (i in 1:n) { + header_text <- paste0("~`", names[i], "`") + padding <- col_widths[i] - nchar(header_text) + headers_spaced[i] <- paste0(header_text, paste(rep(" ", padding), collapse = "")) + } + header <- paste(headers_spaced, collapse = ", ") + + # Create properly spaced data rows + placeholder_rows <- character(3) + for (row in 1:3) { + row_values <- character(n) + for (i in 1:n) { + value_text <- '"..."' + padding <- col_widths[i] - nchar(value_text) + row_values[i] <- paste0(value_text, paste(rep(" ", padding), collapse = "")) + } + placeholder_rows[row] <- paste(row_values, collapse = ", ") + } + + # Construct tribble text with aligned columns tribble_text <- paste0( "tibble::tribble(\n", " ", header, ",\n ", - paste(rows, collapse = ",\n "), + paste(placeholder_rows, collapse = ",\n "), "\n)" ) return(tribble_text) @@ -43,26 +65,29 @@ write_input_tribble <- function(names) { #' @return A single tibble with added missing rows and 'More' column, suitable for piping to gt. #' #' @details -#' For "preceptor": adds missing rows to ensure at least 3 rows, adds a "More" column -#' with NA except last row contains "...". +#' For "preceptor": adds a third row between the last 2 rows filled with "...", +#' adds a "More" column with "..." in all positions. #' #' For "population": expands each tibble similarly, then combines them with -#' empty rows before, between, and after, then adds "More" column. +#' empty rows before, between, and after, then adds "More" column with "...". #' #' @examples #' # Preceptor example #' pre_tib <- tibble::tribble(~Unit, ~Year, ~Outcome, #' "A", "2020", "5", -#' "B", "2021", "6") +#' "B", "2021", "6", +#' "C", "2022", "7") #' expand_input_tibble(list(pre_tib), "preceptor") #' #' # Population example #' pop1 <- tibble::tribble(~Source, ~Unit, ~Year, #' "S1", "A", "2020", -#' "S2", "B", "2021") +#' "S2", "B", "2021", +#' "S3", "C", "2022") #' pop2 <- tibble::tribble(~Source, ~Unit, ~Year, -#' "S1", "C", "2022", -#' "S2", "D", "2023") +#' "S1", "D", "2023", +#' "S2", "E", "2024", +#' "S3", "F", "2025") #' expand_input_tibble(list(pop1, pop2), "population", source = TRUE) #' #' @export @@ -73,52 +98,62 @@ expand_input_tibble <- function(x, type, source = FALSE) { if (length(x) != 1) stop("For 'preceptor', x must be a list of length 1.") tib <- x[[1]] - # Add missing rows to reach at least 3 rows - n_missing <- max(0, 3 - nrow(tib)) - if (n_missing > 0) { - missing_rows <- tib[rep(nrow(tib), n_missing), , drop = FALSE] - missing_rows[,] <- NA_character_ - tib <- dplyr::bind_rows(tib, missing_rows) + if (nrow(tib) >= 3) { + new_row <- tib[1, , drop = FALSE] + new_row[,] <- "..." + + tib_expanded <- dplyr::bind_rows( + tib[1:(nrow(tib)-1), ], + new_row, + tib[nrow(tib), ] + ) + } else { + tib_expanded <- tib } - # Add 'More' column: NA except last row is "..." - tib$More <- rep(NA_character_, nrow(tib)) - tib$More[nrow(tib)] <- "..." + tib_expanded$More <- "..." - return(tib) + return(tib_expanded) } else if (type == "population") { if (length(x) != 2) stop("For 'population', x must be a list of length 2.") - expand_one <- function(tib) { - n_missing <- max(0, 3 - nrow(tib)) - if (n_missing > 0) { - missing_rows <- tib[rep(nrow(tib), n_missing), , drop = FALSE] - missing_rows[,] <- NA_character_ - tib <- dplyr::bind_rows(tib, missing_rows) - } - tib - } + data_tibble <- x[[1]] # Should be 2 rows from d_tibble + preceptor_tibble <- x[[2]] # Should be 4 rows from p_tibble_full (without More column) + + # Create empty row template + empty_row <- data_tibble[1, , drop = FALSE] + empty_row[,] <- "..." - tib1 <- expand_one(x[[1]]) - tib2 <- expand_one(x[[2]]) + # Build the 11-row structure: + # Row 1: blank (all "...") + # Rows 2-3: data rows (2 rows) + # Row 4: blank ("..." in all columns) + # Rows 5-8: preceptor rows (4 rows, already has "..." in 3rd position) + # Row 9: blank (all "...") + + # For data section: 2 data rows + 1 blank + 1 more blank = 4 rows total + data_section <- dplyr::bind_rows( + data_tibble[1, ], # First data row + data_tibble[2, ], # Second data row + empty_row, # Blank row + empty_row # Another blank to match preceptor structure + ) - # Create empty row with same columns filled NA - empty_row <- tib1[1, , drop = FALSE] - empty_row[,] <- NA_character_ + # Preceptor section is already 4 rows from p_tibble_full + preceptor_section <- preceptor_tibble - # Combine with empty rows before, between, and after + # Combine: blank + data(4) + blank + preceptor(4) + blank = 11 rows combined <- dplyr::bind_rows( - empty_row, - tib1, - empty_row, - tib2, - empty_row + empty_row, # Row 1: blank + data_section, # Rows 2-5: data section (2 data + 2 blank) + empty_row, # Row 6: blank + preceptor_section, # Rows 7-10: preceptor section (4 rows) + empty_row # Row 11: blank ) - # Add 'More' column with NA except last row is "..." - combined$More <- rep(NA_character_, nrow(combined)) - combined$More[nrow(combined)] <- "..." + # Add More column + combined$More <- "..." return(combined) } diff --git a/TODO.txt b/TODO.txt index 1d7dfb7b..7dadb12c 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,103 +1,39 @@ -Change DESCRIPTION to use the current released version of primer.data, so it stops updating each time. - -```{r} -pre_title_footnote <- "Preceptor Table Title" -pre_units_footnote <- "Units and time information" -pre_outcome_footnote <- "Outcome or potential outcomes description" -pre_treatment_footnote <- "Treatment or intervention description" -pre_covariates_footnote <- "Covariates and their units" - -pop_title_footnote <- "Population Table Title" -pop_units_footnote <- "Units and time information" -pop_outcome_footnote <- "Outcome or potential outcomes description" -pop_treatment_footnote <- "Treatment or intervention description" -pop_covariates_footnote <- "Covariates and their units" - -p_tibble <- tibble::tribble( - ~`Senator`, ~`Session Year`, ~`Support Bill`, ~`Oppose Bill`, ~`Lobbying Contact`, ~`Senator Age`, - "...", "...", "...", "...", "...", "...", - "...", "...", "...", "...", "...", "...", - "...", "...", "...", "...", "...", "..." -) - -d_tibble <- tibble::tribble( - ~`Source`, ~`Senator`, ~`Session Year`, ~`Support Bill`, ~`Oppose Bill`, ~`Lobbying Contact`, ~`Senator Age`, - "...", "...", "...", "...", "...", "...", "...", - "...", "...", "...", "...", "...", "...", "...", - "...", "...", "...", "...", "...", "...", "..." -) -``` - -```{r} -p_tibble_full <- p_tibble |> - dplyr::add_row(!!!as.list(rep(NA, ncol(p_tibble)))) |> - dplyr::mutate(More = c(rep(NA, nrow(.) - 1), "...")) - -gt::gt(p_tibble_full) |> - gt::tab_header(title = "Preceptor Table") |> - gt::tab_spanner(label = "Unit", id = "unit_span", columns = c(`Senator`, `Session Year`)) |> - gt::tab_spanner(label = "Potential Outcomes", id = "outcome_span", columns = c(`Support Bill`, `Oppose Bill`)) |> - gt::tab_spanner(label = "Treatment", id = "treatment_span", columns = c(`Lobbying Contact`)) |> - gt::tab_spanner(label = "Covariates", id = "covariates_span", columns = c(`Senator Age`)) |> - gt::cols_align(align = "center", columns = gt::everything()) |> - gt::cols_align(align = "left", columns = c(`Senator`)) |> - gt::cols_width(columns = c(`Senator`, `Session Year`, `Support Bill`, `Oppose Bill`, `Lobbying Contact`, `Senator Age`, `More`), - widths = gt::px(c(9, 14, 14, 14, 18, 13, 5))) |> - gt::fmt_markdown(columns = gt::everything()) -``` - -```{r} -d_tibble_full <- d_tibble |> - dplyr::add_row(!!!as.list(rep(NA, ncol(d_tibble)))) |> - dplyr::mutate(More = c(rep(NA, nrow(.) - 1), "...")) - -gt::gt(d_tibble_full) |> - gt::tab_header(title = "Population Table") |> - gt::tab_spanner(label = "Unit/Time", id = "unit_span", columns = c(`Source`, `Senator`, `Session Year`)) |> - gt::tab_spanner(label = "Potential Outcomes", id = "outcome_span", columns = c(`Support Bill`, `Oppose Bill`)) |> - gt::tab_spanner(label = "Treatment", id = "treatment_span", columns = c(`Lobbying Contact`)) |> - gt::tab_spanner(label = "Covariates", id = "covariates_span", columns = c(`Senator Age`)) |> - gt::cols_align(align = "center", columns = gt::everything()) |> - gt::cols_align(align = "left", columns = c(`Senator`)) |> - gt::cols_width(columns = c(`Source`, `Senator`, `Session Year`, `Support Bill`, `Oppose Bill`, `Lobbying Contact`, `Senator Age`, `More`), - widths = gt::px(c(9, 14, 14, 14, 18, 13, 5))) |> - gt::fmt_markdown(columns = gt::everything()) -``` +Change DESCRIPTION to use the current released version of primer.data, so it stops updating each time. AP: how do i do this? ### make_p_table() -* "pre_title_footnote <- "Preceptor Table Title"" should be "pre_title_footnote <- "" +* "pre_title_footnote <- "Preceptor Table Title"" should be "pre_title_footnote <- "" AP: done -* center the ... in p/d_tibble creation +* center the ... in p/d_tibble creation AP: done, left align -* Example case does not work. +* Example case does not work. AP: fixed -* Example outcome labels need **if** and a reference to the treatment variable. Support in Contract and Support if No Contract. Include first sentence in vignette. +* Example outcome labels need **if** and a reference to the treatment variable. Support in Contract and Support if No Contract. Include first sentence in vignette. AP: fixed, need to include in vignette -* d_tibble should not include Source. You add that at the end, just before the gt code. +* d_tibble should not include Source. You add that at the end, just before the gt code. AP: DONE -* expand_input_tibble is not working. Ought to fail if not at least three rows. +* expand_input_tibble is not working. Ought to fail if not at least three rows. AP: fixed function -* Tell the AI to reorganize the code. Make sure that, at the top, it calculates the sizes of various things (and their names) before it needs to use those things. AP: had it calculate length and number of characters - +* Tell the AI to reorganize the code. Make sure that, at the top, it calculates the sizes of various things (and their names) before it needs to use those things. AP: had it calculate length and number of characters AP: all this is done +AP: all separate function stuff is done * Make this a couple of separate functions. All included on this one page. All with roxygen stuff which documents them. + Key function is write_input_tribble(names) which takes a character vector of names and then writes out the input tribble text, centering as appropriate. + Another function exapnd_input_tibble(x, type, source = FALSE) which takes a list of tibble x, a type which is preceptor or population and source which is TRUE or FALSE. If type is preceptor, then source must be false. If type is preceptor, then length(x) == 1. If type is population, then length(x) == 2. It first, takes any tibble in x and adds a more column and a missing third row. It preceptor, then done. If population, then combine the two tibbles and adding missing rows at start, between and end. Returns a single. - Feed the result from expand_input_tibble() directly to the gt pipeline for making the table. + Feed the result from expand_input_tibble() directly to the gt pipeline for making the table. AP: done -* Not this: "# Leave the third row and last column as-is to signal more rows exist" User only gets three rows. You add the third row of ... magically in your own code. +* Not this: "# Leave the third row and last column as-is to signal more rows exist" User only gets three rows. You add the third row of ... magically in your own code. AP: magically done * Use the number of characters in each label to space out the "..." entries so that they are beneath (centered) (even if only roughly) the relevant label. There is no Source column AP: still working on * You can change the `More` column header to ... in the gt() code using a command like col_label(). (Might not be exact name.) -* The Population Table code chunk does NOT get to assume that the Preceptor Table code was run. It begins with "raw" p_tibble and d_tibble, objects which only contain the data enterted by the user. That means that there will be some replication in the code in the two chunks: they both add a missing third row to p_tibble and a More column, for example. But that is OK. +* The Population Table code chunk does NOT get to assume that the Preceptor Table code was run. It begins with "raw" p_tibble and d_tibble, objects which only contain the data enterted by the user. That means that there will be some replication in the code in the two chunks: they both add a missing third row to p_tibble and a More column, for example. But that is OK. * Moreover, that suggest that we might want a transform_raw_tibble(x, which.rows = c(1, 3), more = TRUE) function which takes a tibble (either raw p_tibble or d_tibble) and then adds missing rows, which you specify as an argument. This function goes in its own .R file and is documented and exported. diff --git a/man/expand_input_tibble.Rd b/man/expand_input_tibble.Rd index 09eaea33..789743f6 100644 --- a/man/expand_input_tibble.Rd +++ b/man/expand_input_tibble.Rd @@ -22,26 +22,29 @@ Processes one or two input tibbles to add missing rows (to at least 3 rows), a final "More" column with "..." in the last row, and combines them if type is 'population'. } \details{ -For "preceptor": adds missing rows to ensure at least 3 rows, adds a "More" column -with NA except last row contains "...". +For "preceptor": adds a third row between the last 2 rows filled with "...", +adds a "More" column with "..." in all positions. For "population": expands each tibble similarly, then combines them with -empty rows before, between, and after, then adds "More" column. +empty rows before, between, and after, then adds "More" column with "...". } \examples{ # Preceptor example pre_tib <- tibble::tribble(~Unit, ~Year, ~Outcome, "A", "2020", "5", - "B", "2021", "6") + "B", "2021", "6", + "C", "2022", "7") expand_input_tibble(list(pre_tib), "preceptor") # Population example pop1 <- tibble::tribble(~Source, ~Unit, ~Year, "S1", "A", "2020", - "S2", "B", "2021") + "S2", "B", "2021", + "S3", "C", "2022") pop2 <- tibble::tribble(~Source, ~Unit, ~Year, - "S1", "C", "2022", - "S2", "D", "2023") + "S1", "D", "2023", + "S2", "E", "2024", + "S3", "F", "2025") expand_input_tibble(list(pop1, pop2), "population", source = TRUE) } diff --git a/man/make_p_tables.Rd b/man/make_p_tables.Rd index e1f99d88..1bd3a0b2 100644 --- a/man/make_p_tables.Rd +++ b/man/make_p_tables.Rd @@ -40,9 +40,9 @@ and predictive workflows. The output includes: \itemize{ -\item Editable footnotes for documentation \item Empty \code{tibble}s for the Preceptor Table and Population Table (the latter includes the Preceptor rows) +\item Editable footnotes for documentation \item \code{gt} code chunks to render each table with labeled spanners and columns sized roughly proportional to label length \item The Preceptor and Population tables include a final "More" column and @@ -65,16 +65,17 @@ helping maintain readable, centered columns. } \examples{ \dontrun{ -# Insert causal tables for a study of senators' voting behavior over years +# Insert causal tables for a study of senators' voting behavior +# Outcomes reflect support conditional on the treatment make_p_tables( type = "causal", unit_label = c("Senator", "Session Year"), - outcome_label = c("Support Bill", "Oppose Bill"), + outcome_label = c("Support if Contact", "Support if No Contact"), treatment_label = "Lobbying Contact", covariate_label = "Senator Age" ) -# Insert predictive tables for a clinical trial measuring patient recovery over time +# Insert predictive tables for a clinical trial measuring patient recovery make_p_tables( type = "predictive", unit_label = c("Patient ID", "Visit Number"), diff --git a/man/write_input_tribble.Rd b/man/write_input_tribble.Rd index 798fb518..42862748 100644 --- a/man/write_input_tribble.Rd +++ b/man/write_input_tribble.Rd @@ -11,12 +11,12 @@ write_input_tribble(names) } \value{ Character string containing the R code for an input tribble with placeholders \code{"..."}, -formatted for manual editing. +formatted for manual editing with aligned columns. } \description{ Generates a character string representing an R tibble::tribble() with the specified column names and 3 rows filled with placeholder -text \code{"..."}. Useful for quickly creating editable table templates. +text \code{"..."}. Column values are aligned under their headers for easy editing. } \examples{ write_input_tribble(c("Unit", "Year", "Outcome", "Treatment")) diff --git a/vignettes/tables.qmd b/vignettes/tables.qmd index 42762b7a..54edc776 100644 --- a/vignettes/tables.qmd +++ b/vignettes/tables.qmd @@ -1,3 +1,32 @@ +## The "More" Column + +One of the key features of both the Preceptor and Population tables is the automatically-added **"More" column**. This column serves several important purposes: + +### Purpose and Functionality + +* **Placeholder for expansion**: The "More" column acknowledges that your initial analysis might not include all relevant covariates. As you develop your model, you might discover that additional variables like "Previous Voting History", "Campaign Spending", or "District Demographics" are important. + +* **Visual reminder**: By including this column in your initial table structure, you're reminded to think critically about what other variables might matter for your analysis. + +* **Easy modification**: Rather than having to restructure your entire table when you want to add a new covariate, you can simply replace the "More" column header and add additional "More" columns as needed. + +### How It Works + +* The "More" column is automatically added during table rendering by the `expand_input_tibble()` function +* It's not part of your initial `p_tibble` or `d_tibble` - those contain only the variables you explicitly specify +* The column is filled with `"..."` placeholders in all rows +* It's grouped under the "Covariates" spanner along with your main covariate column + +### Practical Usage + +When you're ready to expand your analysis: + +1. **Replace the header**: Change `"More"` to your new variable name (e.g., `"Previous Experience"`) +2. **Add content**: Replace the `"..."` placeholders with actual values +3. **Add another "More"**: If you need space for yet another variable, you can manually add another "More" column + +This design encourages iterative analysis development while maintaining a clean, organized table structure from the start. + --- title: "Preceptor and Population Tables" author: "David Kane and Aashna Patel" @@ -13,7 +42,7 @@ vignette: > ## Overview -This vignette introduces the `make_p_tables()` function in the `primer.tutorials` package, which inserts a five-chunk Quarto-ready template into your open document for creating **Preceptor Tables** and **Population Tables**. +This vignette introduces the `make_p_tables()` function in the `primer.tutorials` package, which inserts a three-chunk Quarto-ready template into your open document for creating **Preceptor Tables** and **Population Tables**. These tables are designed to support both **causal** and **predictive** modeling workflows by clearly labeling variables with spanners and encouraging detailed documentation via footnotes. @@ -25,30 +54,31 @@ This format draws inspiration from the [Cardinal Virtues](https://ppbds.github.i ### Preceptor Table -A **Preceptor Table** contains hypothetical or expected outcomes for units (such as students or senators). It often includes unknowns (denoted by `"?"`) where real data is not yet available, and reflects researcher or instructor expectations. +A **Preceptor Table** contains hypothetical or expected outcomes for units (such as students or senators). It often includes unknowns (denoted by `"..."`) where real data is not yet available, and reflects researcher or instructor expectations. The table automatically includes a blank third row and a "More" column for additional covariates that you might want to include in your analysis. ### Population Table -A **Population Table** contains a merged view of observed data (from the population) alongside preceptor-defined expectations. It includes an additional column `Source` that distinguishes between actual data (`"Data"`) and expectations (`"Preceptor Table"`). +A **Population Table** contains a merged view of observed data (from the population) alongside preceptor-defined expectations. It includes an additional column `Source` that distinguishes between actual data (`"Data"`) and expectations (`"Preceptor"`). The table follows an 11-row structure with proper spacing between data and preceptor sections. ## Key Features The output of `make_p_tables()` includes: -* An empty `tibble` for the **Preceptor Table** -* An empty `tibble` for the **Population Table** (which includes rows from the Preceptor Table) +* Editable footnotes for documentation +* An empty `tibble` for the **Preceptor Table** (`p_tibble`) +* An empty `tibble` for data input (`d_tibble`) * `gt` code to render both tables with grouped column headers ("spanners") -* Editable footnotes for each section of the table -* Cleanup code to remove temporary objects +* Automatic addition of missing rows and "More" column during rendering +* Column alignment in the tribble code for easier editing ## Spanner Structure Each table includes spanners for: -* **Unit** (or **Unit/Time** in the Population Table) -* **Outcome** (for predictive models) or **Potential Outcomes** (for causal models) +* **Unit/Time** (for the unit columns) +* **Potential Outcomes** (for causal models) or **Outcome** (for predictive models) * **Treatment** (included only in causal models) -* **Covariates** (typically 3 columns, customizable) +* **Covariates** (includes the main covariate column and the "More" column for additional variables you might identify during analysis) > **Note:** All table entries must be surrounded by **double quotes**, even for numeric values (e.g., `"42"`). @@ -66,75 +96,69 @@ library(primer.tutorials) ## Running `make_p_tables()` -Preceptor and Population Tables are inserted together. The Population Table includes a `"Source"` column as its first column, which takes values `"Data"` or `"Preceptor Table"` depending on origin. This structure encourages comparison between expected and observed values. - -Behind the scenes, these tables are generated using `tibble::tribble()` for easier manual editing by row. This helps authors align values vertically and encourages clear visual structure in the Quarto document - -When you run `primer.tutorials::make_p_tables()` without any argument values, the following error appears in the console. +Preceptor and Population Tables are inserted together. The Population Table includes a `"Source"` column as its first column (controlled by the `source_col` argument), which takes values `"Data"` or `"Preceptor"` depending on origin. This structure encourages comparison between expected and observed values. -```` -> make_p_tables() -Error: -! Failed to evaluate glue component {covariate_1_label} -Caused by error: -! argument "covariate_1_label" is missing, with no default -Show Traceback -> -```` +Behind the scenes, these tables are generated using `tibble::tribble()` for easier manual editing by row. The tribble code is formatted with aligned columns to help authors maintain visual structure while editing. -This is because the function requires labels to create a template of the tables. For the best results, it is encouraged to fill in each argument when calling the function. +When you run `primer.tutorials::make_p_tables()` without any argument values, you'll get an error because the function requires specific labels to create the template. ## Understanding the Function Arguments The `make_p_tables()` function takes a set of user-defined labels and options that control how the **Preceptor** and **Population** tables are built and displayed. Each argument serves a clear conceptual role and is used to populate column headers, spanner labels, and the default content in `tibble::tribble()` calls. Here is a detailed breakdown: -* We use the term "label" rather than "vars" to indicate that these are the labels in the table rather than the variable names from the data. As such, they are often human-readable phrases with spaces, like "Math Score if in Small Class". Will long labels be wrapped automagically? We probably want that. Of course, these descriptions can not be too long and so must be shortened in some way. In a pinch, the variable names may be used, but this is not preferred. +* We use the term "label" rather than "vars" to indicate that these are the labels in the table rather than the variable names from the data. As such, they are often human-readable phrases with spaces, like "Math Score if in Small Class". These descriptions should be concise but meaningful. -### `is_causal` *(Logical)* +### `type` *(Character)* -* Set to `TRUE` to generate a causal table structure, which includes: - - * Two columns for **Potential Outcomes** (under treatment and control). +* Set to `"causal"` to generate a causal table structure, which includes: + * Multiple columns for **Potential Outcomes** (specified in `outcome_label`). * A **Treatment** column representing an intervention or assigned condition. -* Set to `FALSE` for a predictive model: - - * Includes a single **Outcome** column. - * Omits the Treatment column entirely. -* This flag determines not only what variables appear in the tables, but also how they are **spanned** and **labeled** in the rendered `gt` tables. +* Set to `"predictive"` for a predictive model: + * Includes outcome columns as specified in `outcome_label`. + * Treatment column is still included but represents the predictor variable. +* This determines not only what variables appear in the tables, but also how they are **spanned** and **labeled** in the rendered `gt` tables. --- -### `unit_label` *(Character)* +### `unit_label` *(Character vector of length 2)* -* Human-readable name for the **unit of analysis** — e.g., `"Senator"`, `"Student"`, or `"School"`. -* This will appear both as a column name in the tibble and as part of the `"Unit"` or `"Unit/Time"` spanner in the `gt` table. -* The label should be capitalized and concise. +* Human-readable names for the **unit of analysis** — e.g., `c("Senator", "Session Year")` or `c("Student", "Grade Level")`. +* These will appear as the first two columns and are grouped under the `"Unit/Time"` spanner. +* The labels should be capitalized and concise. --- -### `outcome_label` *(Character)* +### `outcome_label` *(Character vector)* -* Describes the **key outcome** being predicted or causally modeled. -* In predictive mode, this creates one column: `Outcome`. -* In causal mode, this generates two: `{outcome_label} 1` and `{outcome_label} 2`, reflecting potential outcomes under treatment and control, respectively. -* Should be an interpretable phrase like `"Test Score"` or `"Re-election Status"`. +* Describes the **key outcomes** being predicted or causally modeled. +* For causal models, typically includes multiple potential outcomes like `c("Support Bill", "Oppose Bill")`. +* For predictive models, might be a single outcome like `c("Test Score")`. +* Should be interpretable phrases that clearly describe the outcomes. --- ### `treatment_label` *(Character)* -* Only required if `is_causal = TRUE`. -* Appears as the label of the treatment column, such as `"Phone Call"` or `"Received Tutoring"`. +* Label for the treatment/predictor column, such as `"Phone Call"` or `"Tutoring Program"`. * Used to title the corresponding `gt::tab_spanner()`. +* Required for both causal and predictive models. --- -### `covariate_1_label` and `covariate_2_label`*(Characters)* +### `covariate_label` *(Character)* -* Labels for two covariates relevant to the analysis. -* These are grouped under a `"Covariates"` spanner. -* These labels will be used directly as column headers (e.g., `"Sex"`, `"Age"`, `"School Type"`). -* All should be simple phrases, and together they describe the pre-treatment information on which comparisons or predictions may be based. +* Label for the main covariate column relevant to the analysis. +* This is grouped under the `"Covariates"` spanner along with the automatically-added "More" column. +* Should be a simple phrase like `"Age"` or `"School Type"`. +* The "More" column is added during rendering and provides space for additional covariates that you might discover are important during your analysis. + +--- + +### `source_col` *(Logical, default TRUE)* + +* Controls whether the Population Table includes a `"Source"` column. +* When `TRUE`, adds a column distinguishing between `"Data"` and `"Preceptor"` rows. +* When `FALSE`, the Population Table omits the source column. Each of these labels should be understood as **descriptive display names**, not as variable names from an existing dataset. The goal is clarity and interpretability for readers of the resulting Quarto document. @@ -142,7 +166,7 @@ Each of these labels should be understood as **descriptive display names**, not ## Understanding the Footnotes -Footnotes in these tables are not decorative — they are an essential part of **documenting analytical intent**. When you use `make_p_tables()`, it generates editable placeholders for ten footnotes, five for each table. These can be filled in or deleted (by setting to `NULL`), and they appear in the rendered table using `gt::tab_footnote()`. +Footnotes in these tables are not decorative — they are an essential part of **documenting analytical intent**. When you use `make_p_tables()`, it generates editable placeholders for ten footnotes, five for each table. These can be filled in or deleted, and they appear in the rendered table using `gt::tab_footnote()`. Below is a guide to what each footnote is for: @@ -152,13 +176,13 @@ Below is a guide to what each footnote is for: * **`pre_title_footnote`**: Explains the background or motivation for the expectations shown. Useful for describing who the preceptors are, when the expectations were made, or why these units were selected. -* **`pre_units_footnote`**: Clarifies the definition of a “unit” in this context — e.g., “Each row represents a senator during the 2022 election.” Also helpful to include time span or location info if applicable. +* **`pre_units_footnote`**: Clarifies the definition of a "unit" in this context — e.g., "Each row represents a senator during the 2022 election." Also helpful to include time span or location info if applicable. -* **`pre_outcome_footnote`**: Documents why these potential outcomes are meaningful. May include a note on how they were estimated or what they signify (e.g., "Re-election status under treatment vs. control"). +* **`pre_outcome_footnote`**: Documents why these potential outcomes are meaningful. May include a note on how they were estimated or what they signify. * **`pre_treatment_footnote`**: Defines what the treatment actually entails and how it is operationalized. For example, a phone call campaign, assignment to tutoring, or access to a program. -* **`pre_covariates_footnote`**: Explains why the selected covariates were chosen and their role in forming expectations. Should also clarify whether these differ between tables. +* **`pre_covariates_footnote`**: Explains why the selected covariates were chosen and their role in forming expectations. Should also clarify the purpose of the "More" column - typically used to indicate that additional covariates could be relevant to the analysis but are not yet specified. --- @@ -166,13 +190,13 @@ Below is a guide to what each footnote is for: * **`pop_title_footnote`**: Describes the purpose of the population table — usually to compare expected vs. observed outcomes, merged with preceptor rows. -* **`pop_units_footnote`**: Defines what each unit represents in the population — e.g., “Each row represents an observed student or a preceptor-generated scenario.” +* **`pop_units_footnote`**: Defines what each unit represents in the population — e.g., "Each row represents an observed student or a preceptor-generated scenario." -* **`pop_outcome_footnote`**: Documents the source of outcome data. For example: “Outcomes observed from the 2022 voter file” or “Final grades from school records.” +* **`pop_outcome_footnote`**: Documents the source of outcome data. For example: "Outcomes observed from the 2022 voter file" or "Final grades from school records." * **`pop_treatment_footnote`**: Explains how actual treatment status was observed or inferred. May differ from the assumptions made in the Preceptor Table. -* **`pop_covariates_footnote`**: Describes where the covariate data comes from in the population table and whether it’s measured identically to the preceptor rows. +* **`pop_covariates_footnote`**: Describes where the covariate data comes from in the population table and whether it's measured identically to the preceptor rows. Should also explain what additional variables might be included in the "More" column. --- @@ -180,116 +204,203 @@ These footnotes ensure that **both the data structure and the logic of your mode --- +## Examples -### Examples When you run: ```r make_p_tables( - is_causal = TRUE, - unit_label = "Senator", - outcome_label = "Potential Outcomes", - treatment_label = "Phone Call", - covariate_1_label = "Sex", - covariate_2_label = "Age" + type = "causal", + unit_label = c("Senator", "Session Year"), + outcome_label = c("Support Bill", "Oppose Bill"), + treatment_label = "Lobbying Contact", + covariate_label = "Senator Age" ) ``` -The following chunks are inserted (after manually filling in the rest): +The following chunks are inserted: -### 1. Installation + Footnotes +### 1. Footnotes and Data Setup ```{r} -pre_title_footnote <- "Expected outcomes for 2022 Senate elections based on expert judgment and historical data." -pre_units_footnote <- "Each row represents a single U.S. senator during the 2022 election cycle." -pre_outcome_footnote <- "Potential outcomes reflect election results with and without treatment (phone calls)." -pre_treatment_footnote <- "Treatment indicates whether targeted phone call campaigns were conducted." -pre_covariates_footnote <- "Covariates include demographic factors (Sex, Age) and incumbency status." - -pop_title_footnote <- "Observed 2022 Senate election data merged with preceptor expectations." -pop_units_footnote <- "Each row represents an observed senator or a preceptor-defined expected case." -pop_outcome_footnote <- "Observed election results recorded after the 2022 election." -pop_treatment_footnote <- "Observed phone call campaign status from actual campaign data." -pop_covariates_footnote <- "Covariates drawn from voter file demographics and campaign records." -``` +pre_title_footnote <- "..." +pre_units_footnote <- "..." +pre_outcome_footnote <- "..." +pre_treatment_footnote <- "..." +pre_covariates_footnote <- "..." + +pop_title_footnote <- "..." +pop_units_footnote <- "..." +pop_outcome_footnote <- "..." +pop_treatment_footnote <- "..." +pop_covariates_footnote <- "..." -### 2. Preceptor Table - -```{r} -# Use "?" for unknowns in Preceptor Table rows p_tibble <- tibble::tribble( - ~`Senator`, ~`Time/Year`, ~`Potential Outcomes 1`, ~`Potential Outcomes 2`, ~`Phone Call`, ~`Sex`, ~`Age`, ~`Other`, - "John Smith", "2025", "?", "Not Re-elected", "Yes", "Male", "58", "...", - "Jane Doe", "2025", "?", "Not Re-elected", "No", "Female", "49", "...", - "...", "...", "...", "...", "...", "...", "...", "...", - "Chris Lee", "2025", "Re-elected", "?", "No", "Male", "55", "...", - "...", "...", "...", "...", "...", "...", "...", "...", + ~`Senator` , ~`Session Year` , ~`Support Bill` , ~`Oppose Bill` , ~`Lobbying Contact` , ~`Senator Age` , + "..." , "..." , "..." , "..." , "..." , "..." , + "..." , "..." , "..." , "..." , "..." , "..." , + "..." , "..." , "..." , "..." , "..." , "..." ) -``` - -### 3. Population Table (Includes Preceptor Rows) -```{r} d_tibble <- tibble::tribble( - ~`Source`, ~`Senator`, ~`Time/Year`, ~`Potential Outcomes 1`, ~`Potential Outcomes 2`, ~`Phone Call`, ~`Sex`, ~`Age`, ~`Other`, - "...", "...", "...", "...", "...", "...", "...", "...", "...", - "Data", "Mary Jackson", "2021", "Re-elected", "---", "Yes", "Female", "51", "...", - "Data", "Alex Johnson", "2022", "---", "Not Re-elected", "Yes", "Male", "46", "...", - "...", "...", "...", "...", "...", "...", "...", "...", "...", - "Data", "Bobby Norris", "2022", "Re-elected", "---", "No", "Male", "63", "...", - "...", "...", "...", "...", "...", "...", "...", "...", "...", - # Add Preceptor rows - "Preceptor Table", "John Smith", "2025", "?", "Not Re-elected", "Yes", "Male", "58", "...", - "Preceptor Table", "Jane Doe", "2025", "?", "Not Re-elected", "No", "Female", "49", "...", - "...", "...", "...", "...", "...", "...", "...", "...", "...", - "Preceptor Table", "Chris Lee", "2025", "Re-elected", "?", "No", "Male", "55", "...", - "...", "...", "...", "...", "...", "...", "...", "...", "..." + ~`Senator` , ~`Session Year` , ~`Support Bill` , ~`Oppose Bill` , ~`Lobbying Contact` , ~`Senator Age` , + "..." , "..." , "..." , "..." , "..." , "..." , + "..." , "..." , "..." , "..." , "..." , "..." , + "..." , "..." , "..." , "..." , "..." , "..." ) ``` -### 4. Rendering Tables +### 2. Preceptor Table ```{r} -# Preceptor Table -gt::gt(data = p_tibble) |> +p_tibble_full <- expand_input_tibble(list(p_tibble), "preceptor") + +gt::gt(p_tibble_full) |> gt::tab_header(title = "Preceptor Table") |> - gt::tab_spanner(label = "Unit/Time", id = "unit_span", columns = c(`Senator`, `Year`)) |> - gt::tab_spanner(label = "Potential Outcomes", id = "outcome_span", columns = c(`Potential Outcomes 1`, `Potential Outcomes 2`)) |> - gt::tab_spanner(label = "Treatment", id = "treatment_span", columns = c(`Phone Call`)) |> - gt::tab_spanner(label = "Covariates", id = "covariates_span", columns = c(`Sex`, `Age`)) |> - gt::cols_align("center", columns = gt::everything()) |> - gt::cols_align("left", columns = `Senator`) |> - gt::fmt_markdown(columns = gt::everything()) |> - gt::tab_footnote(pre_title_footnote, locations = gt::cells_title("title")) |> - gt::tab_footnote(pre_units_footnote, locations = gt::cells_column_spanners("unit_span")) |> - gt::tab_footnote(pre_outcome_footnote, locations = gt::cells_column_spanners("outcome_span")) |> - gt::tab_footnote(pre_treatment_footnote, locations = gt::cells_column_spanners("treatment_span")) |> - gt::tab_footnote(pre_covariates_footnote, locations = gt::cells_column_spanners("covariates_span")) + gt::tab_spanner(label = "Unit/Time", id = "unit_span", columns = c(`Senator`, `Session Year`)) |> + gt::tab_spanner(label = "Potential Outcomes", id = "outcome_span", columns = c(`Support Bill`, `Oppose Bill`)) |> + gt::tab_spanner(label = "Treatment", id = "treatment_span", columns = c(`Lobbying Contact`)) |> + gt::tab_spanner(label = "Covariates", id = "covariates_span", columns = c(`Senator Age`, "More")) |> + gt::cols_align(align = "center", columns = gt::everything()) |> + gt::cols_align(align = "left", columns = c(`Senator`)) |> + gt::cols_width( + "Senator" ~ gt::px(100), + "Session Year" ~ gt::px(120), + "Support Bill" ~ gt::px(120), + "Oppose Bill" ~ gt::px(120), + "Lobbying Contact" ~ gt::px(120), + "Senator Age" ~ gt::px(120), + "More" ~ gt::px(60) + ) |> + gt::tab_style( + style = gt::cell_text(size = gt::px(14)), + locations = gt::cells_body() + ) |> + gt::tab_style( + style = list( + gt::cell_text(size = gt::px(14), weight = "bold"), + gt::cell_borders(sides = "bottom", weight = gt::px(2)) + ), + locations = gt::cells_column_labels() + ) |> + gt::tab_options( + table.font.size = gt::px(14), + data_row.padding = gt::px(12), + column_labels.padding = gt::px(12), + row_group.padding = gt::px(12), + table.width = gt::pct(100), + table.margin.left = gt::px(0), + table.margin.right = gt::px(0) + ) |> + gt::fmt_markdown(columns = gt::everything()) ``` +### 3. Population Table + ```{r} -# Population Table -gt::gt(data = d_tibble) |> +# Create full data tibble with 4 rows (3 content, 1 blank in 3rd position) +data_tibble <- dplyr::bind_rows( + d_tibble[1:2, , drop = FALSE], # First 2 data rows + d_tibble[1, , drop = FALSE] |> dplyr::mutate(dplyr::across(dplyr::everything(), ~ "...")), # Blank row + d_tibble[3, , drop = FALSE] # Last data row +) |> + dplyr::mutate(Source = "Data", .before = 1) + +# Create preceptor tibble from p_tibble_full (remove More column, add Source) +preceptor_tibble <- p_tibble_full |> + dplyr::select(-More) |> + dplyr::mutate(Source = "Preceptor", .before = 1) + +# Create the 11-row population table structure manually +# Row structure: blank, 4 data (3rd blank), blank, 4 preceptor (3rd blank), blank + +# Create empty row template +empty_row <- data_tibble[1, , drop = FALSE] +empty_row[,] <- "..." + +# Build the 11-row structure +population_tibble <- dplyr::bind_rows( + empty_row, # Row 1: blank + data_tibble, # Rows 2-5: 4 data rows (3rd is blank) + empty_row, # Row 6: blank + preceptor_tibble, # Rows 7-10: 4 preceptor rows (3rd is blank) + empty_row # Row 11: blank +) + +# Add More column +population_tibble$More <- "..." + +gt::gt(population_tibble) |> gt::tab_header(title = "Population Table") |> - gt::tab_spanner(label = "Unit/Time", id = "unit_span", columns = c(`Senator`, `Time/Year`)) |> - gt::tab_spanner(label = "Potential Outcomes", id = "outcome_span", columns = c(`Potential Outcomes 1`, `Potential Outcomes 2`)) |> - gt::tab_spanner(label = "Phone Call", id = "treatment_span", columns = c(`Phone Call`)) |> - gt::tab_spanner(label = "Covariates", id = "covariates_span", columns = c(`Sex`, `Age`)) |> - gt::cols_align("center", columns = gt::everything()) |> - gt::cols_align("left", columns = `Senator`) |> - gt::fmt_markdown(columns = gt::everything()) |> - gt::tab_footnote(pop_title_footnote, locations = gt::cells_title("title")) |> - gt::tab_footnote(pop_units_footnote, locations = gt::cells_column_spanners("unit_span")) |> - gt::tab_footnote(pop_outcome_footnote, locations = gt::cells_column_spanners("outcome_span")) |> - gt::tab_footnote(pop_treatment_footnote, locations = gt::cells_column_spanners("treatment_span")) |> - gt::tab_footnote(pop_covariates_footnote, locations = gt::cells_column_spanners("covariates_span")) + gt::tab_spanner(label = "Unit/Time", id = "unit_span", columns = c(`Source`, `Senator`, `Session Year`)) |> + gt::tab_spanner(label = "Potential Outcomes", id = "outcome_span", columns = c(`Support Bill`, `Oppose Bill`)) |> + gt::tab_spanner(label = "Treatment", id = "treatment_span", columns = c(`Lobbying Contact`)) |> + gt::tab_spanner(label = "Covariates", id = "covariates_span", columns = c(`Senator Age`, "More")) |> + gt::cols_align(align = "center", columns = gt::everything()) |> + gt::cols_align(align = "left", columns = c(`Senator`)) |> + gt::cols_width( + "Source" ~ gt::px(80), + "Senator" ~ gt::px(100), + "Session Year" ~ gt::px(120), + "Support Bill" ~ gt::px(120), + "Oppose Bill" ~ gt::px(120), + "Lobbying Contact" ~ gt::px(120), + "Senator Age" ~ gt::px(120), + "More" ~ gt::px(60) + ) |> + gt::tab_style( + style = gt::cell_text(size = gt::px(14)), + locations = gt::cells_body() + ) |> + gt::tab_style( + style = list( + gt::cell_text(size = gt::px(14), weight = "bold"), + gt::cell_borders(sides = "bottom", weight = gt::px(2)) + ), + locations = gt::cells_column_labels() + ) |> + gt::tab_options( + table.font.size = gt::px(14), + data_row.padding = gt::px(12), + column_labels.padding = gt::px(12), + row_group.padding = gt::px(12), + table.width = gt::pct(100), + table.margin.left = gt::px(0), + table.margin.right = gt::px(0) + ) |> + gt::fmt_markdown(columns = gt::everything()) ``` -### 5. Cleanup +After filling in the tibbles and footnotes with actual data, you would see properly formatted tables with: -```{r} -rm(p_tibble, d_tibble) -``` +- **Preceptor Table**: 4 rows (3 content + 1 blank) with a "More" column +- **Population Table**: 11 rows with proper separation between data and preceptor sections +- **Column alignment**: Easy-to-edit tribble format with aligned columns +- **Source labeling**: Clear distinction between "Data" and "Preceptor" rows + +--- + +## Table Structure Details + +### Preceptor Table +- Uses `p_tibble` as input (3 rows of placeholders) +- Automatically adds a blank third row and "More" column via `expand_input_tibble()` +- The "More" column provides space for additional covariates you might identify later +- Results in 4 total rows for the final table + +### Population Table +- Uses `d_tibble` for data input (3 rows of placeholders) +- Creates 4 data rows (3 content + 1 blank in 3rd position) +- Uses the expanded preceptor table (4 rows) +- The "More" column is carried over from the preceptor table and added to the population structure +- Combines into 11-row structure: blank + 4 data + blank + 4 preceptor + blank +- All rows are properly labeled in the Source column + +### Column Alignment +The tribble code is formatted with aligned columns to make editing easier: +- Headers and values are padded to align vertically +- Minimum column width accommodates the `"..."` placeholder +- Makes it easy to see which column you're editing --- @@ -298,14 +409,13 @@ rm(p_tibble, d_tibble) The `make_p_tables()` function simplifies the creation of interpretable, spanner-labeled tables for modeling workflows. It promotes clarity, transparency, and rigor by encouraging authors to: * Replace placeholders with meaningful values -* Use `"?"` or `"---"` for unknowns in pre/post data +* Use proper formatting with double quotes around all entries * Fill in footnotes with useful context -* Ensure every table entry is surrounded by double quotes +* Take advantage of the aligned column structure for easy editing +* Understand the automatic row and column additions during rendering This workflow supports better modeling documentation and instructional design. For more on how and why to use these tables, see: * [The Cardinal Virtues](https://ppbds.github.io/primer.tutorials/articles/cardinal_virtues.html#preceptor-table) article from *primer.tutorials* - -