Skip to content

fix: keep empty crosstab rows and columns through the reshape - #9

Merged
martin1english merged 1 commit into
masterfrom
fix-empty-crosstab-cells
Sep 3, 2026
Merged

fix: keep empty crosstab rows and columns through the reshape#9
martin1english merged 1 commit into
masterfrom
fix-empty-crosstab-cells

Conversation

@martin1english

Copy link
Copy Markdown
Contributor

Reported by PamGene (bionavigator) after upgrading to 1.1.1:

Failed to run operator, exit code = 1
Error in setnames(x, value) : Can't assign 5 names to a 4-column data.table
Calls: colnames<- ... names<-.data.table -> setnames -> stopf

Cause

dcast only emits a row/column for each .ri/.ci value that actually occurs in the data. A crosstab row or column with no observations at all therefore drops out of the wide table, while the headers are still built from the full rselect() / cselect(). The two then disagree:

df_wide  <- dcast(df_long, .ri ~ .ci, value.var = ".y")   # narrower than cselect()
raw_data <- df_wide[order(.ri)][, !".ri"]
...
colnames(df_out) <- c(row_block$names, new_col_names)     # boom

The error is the good outcome

When the number of surviving rows divides evenly into the expected count, cbind() recycles instead of erroring and the export silently contains the wrong values, with only a warning:

4 crosstab rows, rows 2 and 4 empty:
      Row     0     1     2
1:     r1     1     2     3
2:     r2     4     5     6
3:     r3     1     2     3   <- should be empty; carries r1's data
4:     r4     4     5     6   <- should be empty; carries r2's data

The default crosstab-view path (collapse_cols = FALSE) is affected the same way. So an unknown number of previously "successful" exports from tables with empty rows/columns may contain misaligned data.

Fix

Pin the .ri/.ci factor levels to the full index range and reshape with drop = FALSE, so the data block always lines up with the headers and genuinely empty cells come through as NA.

Verification

Ran the real main.R against a mock context, comparing every cell value against the expected grid.

Before (as released in 1.1.1):

== collapse_cols = TRUE ==
  no holes (regression)    -> PASS
  one empty column         -> ERROR: Can't assign 5 names to a 4-column data.table
  one empty row            -> ERROR: SILENT RECYCLING
  empty row + column       -> ERROR: SILENT RECYCLING
  2 of 4 rows empty        -> FAIL values misaligned/recycled
== collapse_cols = FALSE ==
  no holes (regression)    -> PASS
  one empty column         -> FAIL shape 3x3 want 3x4
  one empty row            -> ERROR: SILENT RECYCLING
  empty row + column       -> ERROR: SILENT RECYCLING
  2 of 4 rows empty        -> FAIL values misaligned/recycled

After: all 10 PASS.

Regression checks:

  • shipped tests/test_1 fixture reproduces its expected output byte-identical
  • CSV, TSV and XLSX all write valid output in both collapse_cols modes, and with collapse_rows = TRUE

🤖 Generated with Claude Code

https://claude.ai/code/session_01EeoFKnewHFjCdPK45kWjDG

dcast only emits a row/column for each .ri/.ci value actually present in
the data, so a crosstab row or column with no observations at all fell
out of the wide table while the headers were still built from the full
rselect()/cselect(). Reported by PamGene as:

  Error in setnames(x, value) :
    Can't assign 5 names to a 4-column data.table

That is the benign failure. When the surviving row count divides evenly
into the expected one, cbind() recycles instead of erroring and the
export silently contains the wrong values - rows 3 and 4 carrying the
data of rows 1 and 2, with only a warning. The default crosstab-view
path (collapse_cols = FALSE) is affected the same way.

Pin the .ri/.ci factor levels to the full index range and dcast with
drop = FALSE, so the data block always lines up with the headers and
empty cells come through as NA.

Verified against the shipped test_1 fixture (output byte-identical) and
across empty-row/empty-column shapes for both collapse_cols modes, CSV,
TSV and XLSX.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EeoFKnewHFjCdPK45kWjDG
@martin1english
martin1english merged commit e5fcaaa into master Sep 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant