Filling gaps without breaking the books
consistent-gap-filling.RmdWhen you complete a panel’s gaps with [panel_complete()], do the filled rows still obey the accounting identities (a total equals the sum of its parts)? This tutorial shows when they do, when they don’t, and the one-step fix.
We use a small panel: organization A skips 2021, with a filler F keeping 2021 in the window. The fields are the four columns of one revenue line (total = related + unrelated + excluded).
ivars <- c("F9_08_REV_OTH_INVEST_INCOME_RLTD", "F9_08_REV_OTH_INVEST_INCOME_UBIZ",
"F9_08_REV_OTH_INVEST_INCOME_EXCL", "F9_08_REV_OTH_INVEST_INCOME_TOT")
panel <- data.frame(
EIN2 = c("A", "A", "F", "F", "F"),
TAX_YEAR = c(2020, 2022, 2020, 2021, 2022),
F9_08_REV_OTH_INVEST_INCOME_RLTD = c(10, 11, 1, 1, 1),
F9_08_REV_OTH_INVEST_INCOME_UBIZ = c( 5, 6, 1, 1, 1),
F9_08_REV_OTH_INVEST_INCOME_EXCL = c(35, 36, 1, 1, 1),
F9_08_REV_OTH_INVEST_INCOME_TOT = c(50, 53, 3, 3, 3) # 10+5+35=50, 11+6+36=53
)The good news: linear filling preserves identities
A’s bracketing years both balance. Filling 2021 by interpolation is a linear blend of two balanced rows, so the filled row balances too:
done <- panel_complete(panel, vars = ivars)
a2021 <- done[done$EIN2 == "A" & done$TAX_YEAR == 2021, ]
a2021[, ivars] # 10.5 + 5.5 + 35.5 = 51.5
#> F9_08_REV_OTH_INVEST_INCOME_RLTD F9_08_REV_OTH_INVEST_INCOME_UBIZ
#> 2 10.5 5.5
#> F9_08_REV_OTH_INVEST_INCOME_EXCL F9_08_REV_OTH_INVEST_INCOME_TOT
#> 2 35.5 51.5
accounting_check(a2021) # no violations
#> [1] EIN2 TAX_YEAR identity residual ok
#> <0 rows> (or 0-length row.names)So most of the time you get consistent rows for free – interpolation, mean, and carry-forward are all affine, and an affine blend of balanced vectors is balanced.
The catch: a bad bracket (or rounding) propagates
Now suppose A’s 2022 total is a filer error – 60 instead of 50 – while its components are unchanged:
bad <- panel
bad$F9_08_REV_OTH_INVEST_INCOME_TOT[bad$EIN2 == "A" & bad$TAX_YEAR == 2022] <- 60
done_bad <- panel_complete(bad, vars = ivars)
accounting_check(done_bad[done_bad$EIN2 == "A" & done_bad$TAX_YEAR == 2021, ])
#> EIN2 TAX_YEAR identity residual ok
#> 1 A 2021 rev_invest_columns 3.5 FALSEThe interpolated 2021 row inherits the imbalance: its total is now 5 more than its components. (Rounding via as_integers = TRUE can break identities the same way, since each cell is rounded independently.)
The fix: reconcile while completing
Pass reconcile = TRUE and panel_complete() snaps the newly filled rows back onto the identities with the least change – in one step:
fixed <- panel_complete(bad, vars = ivars, reconcile = TRUE)
a2021_fixed <- fixed[fixed$EIN2 == "A" & fixed$TAX_YEAR == 2021, ]
a2021_fixed[, ivars]
#> F9_08_REV_OTH_INVEST_INCOME_RLTD F9_08_REV_OTH_INVEST_INCOME_UBIZ
#> 2 11.375 6.375
#> F9_08_REV_OTH_INVEST_INCOME_EXCL F9_08_REV_OTH_INVEST_INCOME_TOT
#> 2 36.375 54.125
accounting_check(a2021_fixed) # balanced
#> [1] EIN2 TAX_YEAR identity residual ok
#> <0 rows> (or 0-length row.names)The imputed row is now consistent, and only imputed rows are touched – A’s reported (if erroneous) 2022 row is left exactly as filed:
fixed$F9_08_REV_OTH_INVEST_INCOME_TOT[fixed$EIN2 == "A" & fixed$TAX_YEAR == 2022]
#> [1] 60Reconciliation runs on every identity whose fields are present, so a real panel with revenue, expense, and balance-sheet columns is reconciled across all of them at once. Use reconcile_fixed = to hold particular columns and reconcile_section = to restrict which identities apply. See the Accounting consistency article for the identity registry and how reconcile() chooses the least-change adjustment. ```