titleclassifier turns the free-text officer and key-employee titles reported on IRS Form 990 Part VII into a structured, analysis-ready taxonomy. The work is done by a nine-step pipeline. Each step is a single function you can run on its own, and each has its own article on this site, with before-and-after examples and the crosswalks it uses.
| Step | Function | What it does | Assets |
|---|---|---|---|
| 01 | standardize_df() |
Clean the raw table: titles, numeric pay and hours, 0/1 checkboxes. | Part VII schema |
| 02 | remove_dates() |
Strip and flag dates. | date.words |
| 03 | standardize_conj() |
One separator (&) between titles. |
likely.titles |
| 04 | split_titles() |
One row per title. | |
| 05 | standardize_spelling() |
Expand abbreviations, fix spelling. |
fix_* rules, possible.titles, likely.subjects, hunspell |
| 06 | gen_status_codes() |
Flag and remove FORMER, INTERIM, EX OFFICIO, … |
status.codes, number.words
|
| 07 | standardize_titles() |
Map to a standard title. |
title.xwalk, title.patterns
|
| 08 | categorize_titles() |
Attach the taxonomy; add per-filing features. | title.taxonomy |
| 09 | conditional_logic() |
Resolve each person’s role from checkboxes, pay, hours and title. | decision rules; the role model for 990-EZ |
Rules and models explains how the rule-based steps and the 990-EZ role model differ. Taxonomy defines every output category, and Performance reports coverage and accuracy.
Getting the data
Part VII compensation tables (F9-P07-T01-COMPENSATION) are published in the public NCCS efile store, one file per tax year. The panel990 package downloads and reads them, caching each file on disk:
library(panel990)
dl <- download_tables(years = 2022, tables = "F9-P07-T01-COMPENSATION")
pvii <- read_tables(dl)$tables[[1]] # 5.07 million rows, 37 columnstitleclassifier has its own small helpers for the same files:
library(titleclassifier)
# fetch + read the 2023 Part VII table in one call
d <- get_partvii(2023)
# or split the two steps (download once, re-read from cache)
paths <- fetch_partvii(2023, dest = "efdata")
d <- read_partvii(paths)For examples the package ships a demo slice, tinypartvii, already in the expected schema.
Running the pipeline
The steps compose with the base pipe. The title-standardization, status-code, and taxonomy crosswalks are package data, so a run needs no network access.
df <- d |>
standardize_df() |> # 01
remove_dates() |> # 02
standardize_conj() |> # 03
split_titles() |> # 04
standardize_spelling() |> # 05
gen_status_codes() |> # 06
standardize_titles() |> # 07
categorize_titles() |> # 08
conditional_logic() # 09
#> [OK] standardize df step complete
#> [OK] remove dates step complete
#> [OK] standardize conjunctions step complete
#> [OK] split titles step complete
#> [OK] standardize spelling step complete
#> [OK] generate status codes step complete
#> [OK] standardize titles step complete
#> [OK] categorize titles step complete
#>
#> [OK] conditional logic step complete
dim(df)
#> [1] 315 112The result
Each row is one standardized title for one person, carrying the raw text, the successive cleaned versions (title.v2 … title.v7), the canonical title.standard, taxonomy fields, status flags, organization-level features (pay/hours ranks, leadership counts), and the person’s resolved role.
df |>
select(title.raw, title.standard, domain.label, tot.comp, tot.hours, role.final, role.ceo) |>
arrange(desc(tot.comp)) |>
head(10)
#> title.raw title.standard domain.label
#> 1 PRESIDENT BOARD PRESIDENT board
#> 2 SECRETARY BOARD SECRETARY board
#> 3 CONTROLLER COMPTROLLER finance
#> 4 DIRECTOR BOARD MEMBER board
#> 5 STAFF PHARMACIST PHARMACIST medical
#> 6 SR ADMINISTRATOR ADMINISTRATOR administration
#> 7 DIRECTOR OF EVENTS DIRECTOR administration
#> 8 EMPLOYEE EMPLOYEE administration
#> 9 INTERIM EXECUTIVE DIRECTOR EXECUTIVE DIRECTOR general management
#> 10 CHIEF FINANCIAL OFFICER CHIEF FINANCIAL OFFICER finance
#> tot.comp tot.hours role.final role.ceo
#> 1 2389606 40.0 CEO imputed
#> 2 444028 53.0 CEO imputed
#> 3 235319 37.5 CEO imputed
#> 4 217880 55.0 CEO imputed
#> 5 203361 42.0 PROFESSIONAL <NA>
#> 6 190745 40.0 CEO imputed
#> 7 188401 40.0 CEO imputed
#> 8 132955 40.0 STAFF <NA>
#> 9 115500 40.0 CEO interim
#> 10 108251 40.0 CEO imputedrole.primary marks one row per person, so people (not titles) are counted with it:
people <- filter(df, role.primary)
count(people, formtype, role.final)
#> formtype role.final n
#> 1 990 BOARD 192
#> 2 990 CEO 20
#> 3 990 OFFICER 2
#> 4 990 PROFESSIONAL 1
#> 5 990 STAFF 2
#> 6 990EZ BOARD 79
#> 7 990EZ CEO 2
#> 8 990EZ OFFICER 1
#> 9 990EZ STAFF 1The successive title versions make the cleaning transparent — you can see exactly what each step changed:
df |>
select(title.raw, title.v3, title.v5, title.v6, title.standard) |>
distinct() |>
head(12)
#> title.raw title.v3 title.v5
#> 1 H&W TRUSTEE H AND W TRUSTEE H AND W TRUSTEE
#> 2 MEMBER MEMBER MEMBER
#> 3 DIRECTOR DIRECTOR DIRECTOR
#> 4 2ND VICE PRESIDENT SECOND VICE PRESIDENT SECOND VICE PRESIDENT
#> 5 Officer OFFICER OFFICER
#> 6 CEO CEO CEO
#> 7 President PRESIDENT PRESIDENT
#> 8 Secretary SECRETARY SECRETARY
#> 9 treasurer TREASURER TREASURER
#> 10 Treasurer TREASURER TREASURER
#> 11 Director DIRECTOR DIRECTOR
#> 12 Vice President VICE PRESIDENT VICE PRESIDENT
#> title.v6 title.standard
#> 1 H AND W TRUSTEE BOARD MEMBER
#> 2 MEMBER BOARD MEMBER
#> 3 DIRECTOR BOARD MEMBER
#> 4 VICE PRESIDENT BOARD VICE PRESIDENT
#> 5 OFFICER BOARD MEMBER
#> 6 CEO CEO
#> 7 PRESIDENT BOARD PRESIDENT
#> 8 SECRETARY BOARD SECRETARY
#> 9 TREASURER BOARD TREASURER
#> 10 TREASURER BOARD TREASURER
#> 11 DIRECTOR BOARD MEMBER
#> 12 VICE PRESIDENT BOARD VICE PRESIDENTEditing the crosswalks
The crosswalks are package data built from the CSV tables in data-raw/crosswalks/. To change one, edit its table in a source checkout and rebuild:
# from the package root
source("data-raw/crosswalks/build-crosswalks.R")
xwalk <- get_title_xwalk()See the Dictionaries and crosswalks article for what each crosswalk contains.