Skip to contents

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 psych package 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), see vignette("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   11

Stage 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.9566320

The 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.9556156

Reserves 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: median

Stage 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.36

Stage 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.5

Stage 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.95750

The 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: yes

It 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").