Skip to contents

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:

  1. Every metric is a get_*() function that appends its result to your data frame. There are about fifty of them, catalogued in vignette("metrics-catalog").
  2. 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.

library(fiscal)

dim(dat10k)
#> [1] 10000    93
table(dat10k$RETURN_TYPE)
#> 
#>   990 990EZ 
#>  6284  3716

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            88

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

Set 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                        FALSE

The get_*() functions are also pipe-enabled, so metrics can be chained:

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