fiscal turns IRS 990 financial data into a standard set of nonprofit fiscal-health metrics — liquidity, leverage, margins, efficiency, and reserves — each computed the same way every time so results are comparable across organizations and years.
Two ideas carry the whole package:
-
Every metric is a
get_*()function that appends its result to your data frame. There are about fifty of them, catalogued invignette("metrics-catalog"). - Every metric returns four versions of itself — the raw ratio plus winsorized, standardized, and percentile-rank variants — so outliers and scale never distort comparisons.
compute_all() runs the entire battery in one call.
Where fiscal fits
fiscal is the scoring layer. Its companion package, panel990, is the data layer: it retrieves and normalizes the underlying 990 e-file panels. The usual workflow is:
panel990 — assemble & clean a 990 panel → fiscal — score every organization-year
The panel-facing functions in fiscal (get_panel(), deduplicate(), panel_smooth(), …) are thin, convenience wrappers over panel990; see vignette("panel-workflow").
Install
fiscal depends on panel990; installing from GitHub pulls it in automatically through the package’s Remotes: field.
# install.packages("remotes")
remotes::install_github("nonprofit-open-data-collective/fiscal")Quick start
The package bundles dat10k, a 10,000-row sample of 990 financials (both full 990 and 990-EZ filers), so everything below runs offline with no download.
Score the entire battery of fiscal-health metrics and append it to your data:
scored <- compute_all(dat10k, verbose = FALSE)
# the metric columns that were appended
new_cols <- setdiff(names(scored), names(dat10k))
length(new_cols)
#> [1] 152
head(new_cols)
#> [1] "current" "current_w" "current_z" "current_p" "quick" "quick_w"Or compute a single metric. Each get_*() function defaults to the correct 990 e-file field names but accepts your own column names:
df <- get_debt_assets_ratio(dat10k)
df[1:5, c("debt_assets", "debt_assets_w", "debt_assets_z", "debt_assets_p")]
#> debt_assets debt_assets_w debt_assets_z debt_assets_p
#> 1 0.00000000 0.00000000 -0.7663079 1
#> 2 0.00000000 0.00000000 -0.7663079 1
#> 3 0.02459776 0.02459776 -0.5728525 59
#> 4 0.00000000 0.00000000 -0.7663079 1
#> 5 0.48026395 0.48026395 1.1317288 88The four versions of every metric
get_debt_assets_ratio() (like every metric) appends four columns:
| Column | Meaning |
|---|---|
debt_assets |
the raw financial ratio |
debt_assets_w |
winsorized (extreme tails clipped) |
debt_assets_z |
standardized as a z-score (from the winsorized values) |
debt_assets_p |
percentile rank, 1–100 |
The winsorized, standardized, and percentile versions exist so that outliers and differences in scale don’t dominate cross-organization comparisons or any downstream index. What each transformation actually does — and how the package chooses one automatically — is covered in vignette("normalization").
summary(df[, c("debt_assets", "debt_assets_w", "debt_assets_z", "debt_assets_p")])
#> debt_assets debt_assets_w debt_assets_z debt_assets_p
#> Min. : -5.2753 Min. :0.000000 Min. :-0.7663 Min. : 1.00
#> 1st Qu.: 0.0000 1st Qu.:0.000000 1st Qu.:-0.7663 1st Qu.: 25.00
#> Median : 0.0061 Median :0.006083 Median :-0.7181 Median : 50.00
#> Mean : 0.8679 Mean :0.189722 Mean :-0.1752 Mean : 50.39
#> 3rd Qu.: 0.1618 3rd Qu.:0.161757 3rd Qu.: 0.2605 3rd Qu.: 75.00
#> Max. :3355.8017 Max. :2.945267 Max. : 2.6978 Max. :100.00
#> NA's :268 NA's :268 NA's :268 NA's :268Set summarize = TRUE on any metric to print these summaries and plot the four density curves as you compute:
df <- get_debt_assets_ratio(dat10k, summarize = TRUE)Controlling the output
compute_all() (and each get_*()) takes a metrics argument that selects which of the four versions to return:
| Value | Returns |
|---|---|
"ratio" |
raw financial ratio |
"w" |
winsorized version |
"z" |
z-score (standardized) |
"p" |
percentile rank (1–100) |
# only the raw ratios and their percentile ranks
scored_rp <- compute_all(dat10k, metrics = c("ratio", "p"), verbose = FALSE)append_to_df controls the shape of the result:
| Value | Returns |
|---|---|
TRUE (default) |
your original data frame with metric columns appended |
FALSE |
e-file identifier columns + metric columns only |
metrics_only <- compute_all(dat10k, append_to_df = FALSE, verbose = FALSE)
metrics_only[1:3, 1:6]
#> EIN2 OBJECTID ORG_EIN
#> 1 EIN-56-2210510 OID-202223049349201027 562210510
#> 2 EIN-20-8447239 OID-202213089349303136 208447239
#> 3 EIN-30-0212534 OID-202311399349300406 300212534
#> ORG_NAME_L1 ORG_NAME_L2 RETURN_AMENDED_X
#> 1 Wintergreen Primary School PTA FALSE
#> 2 RICHARD ELLIS RADIO FALSE
#> 3 INTERFAITH AMERICA FALSEThe get_*() functions are also pipe-enabled, so metrics can be chained:
library(dplyr)
df <- dat10k |>
get_debt_assets_ratio() |>
get_current_ratio() |>
get_program_expenses_ratio()990 vs 990-EZ
dat10k mixes full 990 and 990-EZ filers. Because the two forms report different fields, some metrics are computable for both and some only for full-990 filers — each metric’s help entry (and the catalog) states the scope under Calculated For. fiscal handles the difference automatically: blank financials are interpreted correctly and, where a field has a 990-EZ equivalent, the two are coalesced. This is the subject of vignette("form-scope").
Where to go next
-
vignette("metrics-catalog")— every ratio, with formula, inputs, definitional range, and benchmarks. -
vignette("normalization")— what the_w/_z/_pcolumns are and how transformations are chosen. -
vignette("panel-workflow")— build, clean, and score a multi-year panel. -
vignette("panel-smoothing")— smoothing noisy longitudinal financials. -
vignette("fiscal-health-index")— combine metrics into a composite fiscal-health index. -
vignette("form-scope")— 990 vs 990-EZ field scope and sanitization.