The get_*() metrics answer “how does this organization
look on this ratio?” The fiscal-health index
answers a harder question: “how does this organization look
overall?” It combines many standardized indicators into a small
number of empirical dimensions and, optionally, a single composite
score.
Rather than hard-code a formula, fiscal builds a
reusable measurement model in explicit stages. Each
stage is a separate function that returns an inspectable object, so the
pipeline is auditable and the fitted model can be re-applied to later
panel years without re-estimating anything.
fiscal_diagnostics() -> fiscal_triage() -> fiscal_dimensions()
(screen indicators) (decide keep/drop) (discover structure)
|
fiscal_health_index() <- fiscal_weights() <- fiscal_health_score() <- fiscal_health_fit()
(composite score) (combine) (apply model) (freeze the model)
Two stages deliberately require human judgment —
fiscal_triage() recommends but lets you override, and
fiscal_dimensions() discovers but does not name
dimensions. The wrapper fiscal_health() chains the
mechanical stages once you have reviewed the structure.
This subsystem uses the
psychpackage for factor-analytic diagnostics; make sure it is installed. For the measurement reasoning behind each stage (what counts as an indicator, how many dimensions to keep, how to validate the result), seevignette("fiscal-health-index").
Prepare indicators
Indicators should be the normalized (_z) versions of the
metrics, so they share a common scale. Most inputs exist only on the
full Form 990, so start from full 990 filers with positive revenue,
expenses, and assets, then compute the metrics with
compute_all():
full990 <- dat10k[dat10k$RETURN_TYPE == "990" &
dat10k$F9_08_REV_TOT_TOT > 0 & !is.na(dat10k$F9_08_REV_TOT_TOT) &
dat10k$F9_09_EXP_TOT_TOT > 0 & !is.na(dat10k$F9_09_EXP_TOT_TOT) &
dat10k$F9_10_ASSET_TOT_EOY > 0 & !is.na(dat10k$F9_10_ASSET_TOT_EOY), ]
scored <- compute_all(full990, metrics = "z", verbose = FALSE)Select candidate indicators that are directional in
fiscal_metrics() and defined for nearly every filer. The
list deliberately includes a few problems for the pipeline to catch:
equity_z is an algebraic mirror of
debt_assets_z, and the program- and administrative-expense
shares describe spending patterns rather than financial condition.
indicators <- scored[, c(
"days_cash_ops_z", "op_reserve_z", "liquid_assets_months_z", # reserves
"debt_assets_z", "equity_z", # solvency
"surplus_margin_z", "profit_predepr_z", "return_assets_z", # performance
"prog_exp_z", "expenses_admin_z", "self_suff_z" # business model
)]
dim(indicators)
#> [1] 5892 11Stage 1 — diagnose
fiscal_diagnostics() computes variable-level diagnostics
that distinguish indicators participating in a shared multivariate
structure from those that are redundant, standalone, or noisy
(correlation summaries, KMO/MSA, communalities, and so on).
diagnostics <- fiscal_diagnostics(indicators)
diagnostics
#> Fiscal indicator diagnostics
#> Indicators: 11
#> Redundant pairs: 3
#> Near-zero variance: 0
diagnostics$redundancy_pairs
#> var1 var2 r
#> 1 op_reserve_z liquid_assets_months_z 0.9295005
#> 2 debt_assets_z equity_z -0.9403921
#> 3 surplus_margin_z profit_predepr_z 0.9566320The diagnostics find the three near-duplicate pairs:
equity_z mirrors debt_assets_z, and the two
reserve measures and the two margin measures differ only in small
adjustments. The program- and administrative-expense shares are related
to each other, but neither they nor the self-sufficiency ratio share
much variance with the financial-condition indicators.
Stage 2 — triage
fiscal_triage() turns those diagnostics into an
auditable recommendation: which indicators to retain
for dimension estimation, which to keep as standalone
measures, and which to drop as redundant or noise.
triage <- fiscal_triage(diagnostics)
triage$decisions[, c("variable", "decision", "reason")]
#> variable decision
#> 1 days_cash_ops_z retain
#> 2 op_reserve_z redundant
#> 3 liquid_assets_months_z retain
#> 4 debt_assets_z redundant
#> 5 equity_z retain
#> 6 surplus_margin_z retain
#> 7 profit_predepr_z redundant
#> 8 return_assets_z retain
#> 9 prog_exp_z retain
#> 10 expenses_admin_z retain
#> 11 self_suff_z standalone
#> reason
#> 1 Eligible for multivariate dimension modeling.
#> 2 Provisional drop from a highly correlated redundancy group.
#> 3 Eligible for multivariate dimension modeling.
#> 4 Provisional drop from a highly correlated redundancy group.
#> 5 Eligible for multivariate dimension modeling.
#> 6 Eligible for multivariate dimension modeling.
#> 7 Provisional drop from a highly correlated redundancy group.
#> 8 Eligible for multivariate dimension modeling.
#> 9 Eligible for multivariate dimension modeling.
#> 10 Eligible for multivariate dimension modeling.
#> 11 Low communality; retain as a possible standalone indicator rather than forcing it into a shared dimension.The triage keeps one indicator from each redundant pair. Its recommendations for the three business-model indicators follow only from their correlations: the two expense shares correlate with each other, so the rules can retain them for dimension estimation (where they would form a small “spending mix” dimension), and self-sufficiency, which correlates with nothing, is flagged as standalone or noise. A statistical rule cannot tell whether an indicator measures the construct at all. These three describe a spending pattern and a revenue model rather than financial condition, so we override the recommendations and exclude them:
triage <- fiscal_triage(
diagnostics,
overrides = c(prog_exp_z = "noise", expenses_admin_z = "noise",
self_suff_z = "noise")
)
triage$selected_for_dimensions
#> [1] "days_cash_ops_z" "liquid_assets_months_z" "equity_z"
#> [4] "surplus_margin_z" "return_assets_z"
triage$retained_standalone
#> character(0)Stage 3 — discover dimensions
fiscal_dimensions() uses PCA (or exploratory factor
analysis) to propose empirical dimensions from the retained indicators.
When nfactors is NULL, parallel analysis
chooses the number. It discovers and describes groupings; it does not
name them — that is your call.
dimensions <- fiscal_dimensions(
indicators,
triage = triage,
method = "pca",
seed = 42
)
#> Parallel analysis suggests that the number of factors = NA and the number of components = 2
dimensions
#> Fiscal dimension discovery
#> Method: PCA
#> Dimensions: 2 ( parallel )
#> Multi-indicator dimensions: 2
#> Cross-loading indicators: 0
#> Unassigned indicators: 0
dimensions$assignments[, c("variable", "primary_dimension", "primary_loading")]
#> variable primary_dimension primary_loading
#> 1 days_cash_ops_z Dimension1 0.6922338
#> 2 liquid_assets_months_z Dimension1 0.8898898
#> 3 equity_z Dimension1 0.7687656
#> 4 surplus_margin_z Dimension2 0.9192845
#> 5 return_assets_z Dimension2 0.9556156Reserves and solvency indicators form one dimension; the margin and return indicators form the other. Name them after their content:
a <- dimensions$assignments
perf <- a$primary_dimension[a$variable == "surplus_margin_z"]
dimension_names <- setNames(
ifelse(dimensions$multi_indicator_dimensions == perf, "performance", "resilience"),
dimensions$multi_indicator_dimensions
)
dimension_names
#> Dimension1 Dimension2
#> "resilience" "performance"Stage 4 — fit a reusable model
fiscal_health_fit() freezes the reviewed dimensions into
a scoring specification. The fitted object stores indicator orientation
(inferred from fiscal_metrics() directions), centers,
scales, missing-value treatment, and dimension coefficients — everything
needed to score new data identically later.
fit <- fiscal_health_fit(indicators, dimensions = dimensions,
dimension_names = dimension_names)
fit
#> Fitted fiscal-health measurement model
#> Dimensions: 2
#> Standalone indicators: 0
#> Reference observations: 5892
#> Missing-value rule: medianStage 5 — score
fiscal_health_score() applies the fitted model. No
parameters are re-estimated; it just transforms and combines indicators
into dimension scores (prefixed fh_). Because the model is
frozen, the same fit can score a different
panel year on the same scale.
scored_dims <- fiscal_health_score(indicators, fit)
grep("^fh_", names(scored_dims), value = TRUE)
#> [1] "fh_resilience" "fh_performance"
#> [3] "fh_resilience_coverage" "fh_performance_coverage"
round(cor(scored_dims$fh_resilience, scored_dims$fh_performance, use = "complete.obs"), 2)
#> [1] 0.36Stage 6 — weight
fiscal_weights() defines how dimension scores and
standalone indicators combine into the composite. The default is equal
weighting; min_coverage controls how much of an
observation’s weighted inputs must be present before it receives a
score.
weights <- fiscal_weights(fit, min_coverage = 0.5)
weights
#> Fiscal-health weights
#> Method: equal
#> Measures: 2
#> Missing-score rule: reweight
#> measure measure_type original_weight weight
#> resilience dimension 1 0.5
#> performance dimension 1 0.5Stage 7 — the composite index
fiscal_health_index() produces the single composite
score (and a coverage column recording how much information each score
is based on).
indexed <- fiscal_health_index(scored_dims, weights)
summary(indexed$fiscal_health)
#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> -2.50353 -0.59180 -0.02616 0.00000 0.54486 2.95750The shortcut: fiscal_health()
Once you have reviewed the triage and dimensions,
fiscal_health() runs the mechanical stages — fit, score,
weight, index — in one call:
result <- fiscal_health(indicators, dimensions = dimensions,
fit_args = list(dimension_names = dimension_names))
result
#> Fiscal-health result
#> Mode: fit_and_score
#> Observations: 5892
#> Dimensions: 2
#> Standalone indicators: 0
#> Composite index: yesIt deliberately does not automate diagnostics, triage, or dimension discovery, because those stages require substantive review.
Scoring new data with a fitted model
The payoff of separating fit from score is reuse.
Fit the model once on a reference sample (say, one panel year), then
score every other year on the same scale by passing the stored
fit:
fit <- fiscal_health_fit(indicators_2023, dimensions = reviewed_dims,
dimension_names = dimension_names)
scores_2024 <- fiscal_health_score(indicators_2024, fit)
scores_2025 <- fiscal_health_score(indicators_2025, fit)This keeps scores comparable across years — exactly what you need
when the index feeds a longitudinal analysis of the panel you built in
vignette("panel-workflow").