Step 09 - Resolve roles
Source:vignettes/step-09-conditional-logic.Rmd
step-09-conditional-logic.RmdThe crosswalk maps each title to one role, but some titles mean different things in different organizations:
- A PRESIDENT is the paid chief executive in one organization and a volunteer board chair in the next.
- A VICE PRESIDENT may sit on the board or on staff.
- A DIRECTOR may be a board member or a department head.
conditional_logic() (a wrapper for resolve_roles()) settles these with what the filer reported about each person: the Part VII checkboxes (trustee, officer, key employee, highly compensated), pay and hours. It also names the chief executive when no title does, and decides that a filing has none when the board runs the organization.
Before and after
Four example filings, built by hand. “Before” is the title-only answer from steps 07-08. “After” is step 09’s answer for each person.
Boxes: T trustee, O officer, K key employee, H highly compensated.
| title | pay | hours | boxes | before: title only | after: step 09 | method |
|---|---|---|---|---|---|---|
| President & CEO | 410,000 | 55 | O | CEO | CEO (designated) | rules |
| Chair of the Board | 0 | 5 | TO | BOARD CHAIR | BOARD CHAIR | rules |
| Treasurer | 0 | 3 | TO | BOARD TREASURER | BOARD TREASURER | rules |
| Director | 0 | 2 | T | BOARD MEMBER | BOARD MEMBER | rules |
| Chief Financial Officer | 260,000 | 50 | O | OFFICER | OFFICER | rules |
| VP of Development | 190,000 | 45 | K | OFFICER | OFFICER | rules |
| Director of Nursing | 175,000 | 40 | H | MANAGER | MANAGER | rules |
| Staff Physician | 320,000 | 40 | H | PROFESSIONAL | PROFESSIONAL | rules |
The checkboxes settle most of filing A. The CEO is designated by a CEO-level title in an officer position. The board titles with the trustee box are board seats. The paid staff keep the level their title implies.
| title | pay | hours | boxes | before: title only | after: step 09 | method |
|---|---|---|---|---|---|---|
| President | 85,000 | 40 | TO | BOARD CHAIR | CEO (imputed) | rules |
| Vice President | 0 | 2 | T | BOARD VICE CHAIR | BOARD VICE CHAIR | rules |
| Secretary | 0 | 2 | TO | BOARD SECRETARY | BOARD SECRETARY | rules |
| Program Director | 42,000 | 40 | MANAGER | MANAGER | rules |
No title in filing B names a CEO, but the president is paid $85,000 for 40 hours a week and ticks the officer box: the president is the imputed CEO. The unpaid vice president is a board seat (VICE CHAIR), not a staff officer.
| title | pay | hours | boxes | before: title only | after: step 09 | method |
|---|---|---|---|---|---|---|
| President | 6,000 | 5 | TO | BOARD CHAIR | BOARD CHAIR | rules |
| Secretary | 0 | 2 | TO | BOARD SECRETARY | BOARD SECRETARY | rules |
| Treasurer | 0 | 2 | TO | BOARD TREASURER | BOARD TREASURER | rules |
| Bookkeeper | 18,000 | 10 | STAFF | STAFF | rules |
Filing C pays its president a $6,000 stipend for 5 hours a week. A board title held under 10 hours a week for under $25,000 is a board seat, even with the officer box. No paid person qualifies as a leader (the bookkeeper is staff), so the filing is board_governed: no CEO is named.
| title | pay | hours | boxes | before: title only | after: step 09 | method |
|---|---|---|---|---|---|---|
| President | 38,000 | 40 | none on a 990-EZ | BOARD CHAIR | CEO (model) | model (rules alone: CEO) |
| Administrator | 30,000 | 40 | none on a 990-EZ | MANAGER | OFFICER | model (rules alone: MANAGER) |
| Director of Operations | 0 | 10 | none on a 990-EZ | MANAGER | BOARD MEMBER | model (rules alone: MANAGER) |
| Secretary | 0 | 2 | none on a 990-EZ | BOARD SECRETARY | BOARD SECRETARY | model (rules alone: BOARD) |
| Treasurer | 0 | 2 | none on a 990-EZ | BOARD TREASURER | BOARD TREASURER | model (rules alone: BOARD) |
A 990-EZ has no checkboxes. When the xgboost package is installed, roles on a 990-EZ come from the no-checkbox model, and role.method says model. Without xgboost, or with resolve_roles(use_model = FALSE), the rules decide. Here they disagree on two people: - Director of Operations. The rules read the title as a department head (MANAGER). The model reads an unpaid person working 10 hours a week in a small filing as a volunteer board member. - Administrator. The model makes the second paid full-timer an officer rather than a manager.
Rules and models explains the difference.
What it does
conditional_logic() -> resolve_roles()
|
+- 1. position, per row trustee box = board; officer box = officer; both = board_officer;
| key-employee or high-comp box = staff; a paid CEO- or
| officer-level title counts as an officer without the box
+- 2. one role per person a person with split titles (PRESIDENT & TREASURER) gets
| the most senior position; role.primary marks one row
+- 3. leadership, per filing a CEO-level title in an officer position = designated CEO;
| else the highest-paid plausible leader = imputed CEO;
| else board_governed (paid people, no leader) or no_paid
+- 4. final role CEO, OFFICER, MANAGER, PROFESSIONAL, STAFF or BOARD,
| and for BOARD the board role
+- 5. 990-EZ the no-checkbox model replaces the rules' answer
Three thresholds keep token pay from making a leader:
- Imputed CEO. A leader with no CEO title must work at least 15 hours a week or earn at least $25,000, and have the officer box or a leadership title.
- Board chair pattern. A paid board chair with the trustee box who works under 25 hours a week is not imputed as CEO.
- Stipend rule. A board title held under 10 hours a week for under $25,000 is a board seat, with or without the officer box. On a 990-EZ the same holds for a board title paid a small fee.
Columns added
| Column | Values |
|---|---|
role.final |
CEO, OFFICER, MANAGER, PROFESSIONAL, STAFF, BOARD |
role.board |
for BOARD: CHAIR, VICE CHAIR, SECRETARY, TREASURER, MEMBER |
role.ceo |
for CEO: designated, co-ceo, interim, imputed, model |
role.position |
board, officer, board_officer, staff, unknown |
role.source |
what settled the position: checkbox, title+pay, title |
role.method |
rules, or model (990-EZ with xgboost installed) |
role.primary |
TRUE on one row per person (count people with it) |
org.leadership |
per filing: designated, imputed, model, board_governed, no_paid |
The crosswalk’s own emp.level and board.role are kept: role.final is the resolved answer, the crosswalk columns the title-only default. Taxonomy defines each value.
Assets
| Asset | Type | Used for | Load it with |
|---|---|---|---|
emp.level, board.role
|
columns of title.taxonomy (from step 08) |
the title-only role each rule starts from | get_title_taxonomy() |
| the role model | xgboost model file, inst/extdata/role-model/
|
roles on 990-EZ filings |
predict_roles(); features from role_model_features()
|
system.file("extdata", "role-model", package = "titleclassifier") |> list.files()
#> [1] "feature-names.txt" "role-model.ubj"
# the model's input: one row per person-title, no checkboxes
X <- role_model_features(ex[ex$object.id == "D small organization (990-EZ)", ])
dim(X)
#> [1] 5 100
head(colnames(X), 12)
#> [1] "comp" "hours" "paid" "former" "interim" "co"
#> [7] "founder" "exoff" "n_titles" "n_people" "n_paid" "pay_rank"
predict_roles(ex[ex$object.id == "D small organization (990-EZ)", ])
#> [1] "CEO" "OFFICER" "BOARD" "BOARD" "BOARD"Try it on the demo data
data(tinypartvii)
set.seed(8)
d <- dplyr::sample_n(tinypartvii, 1500)
df <- d |>
standardize_df() |> remove_dates() |> standardize_conj() |>
split_titles() |> standardize_spelling() |> gen_status_codes() |>
standardize_titles() |> categorize_titles() |> conditional_logic()
#> [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
people <- dplyr::filter(df, role.primary)
table(people$role.final)
#>
#> BOARD CEO MANAGER OFFICER PROFESSIONAL STAFF
#> 1333 120 2 24 1 20
table(people$formtype, people$role.method)
#>
#> model rules
#> 990 0 1082
#> 990EZ 418 0Titles the crosswalk files as board seats but that resolve otherwise, here a paid board president who runs the organization:
people |>
dplyr::filter(title.standard == "BOARD PRESIDENT", role.final != "BOARD") |>
dplyr::select(org.name, title.raw, tot.comp, tot.hours, role.final, role.ceo) |>
head(8)
#> org.name
#> 1 Noah Webster Educational Foundation
#> 2 LABORERS' INTERNATIONAL UNION LOCAL 1092
#> 3 LOCAL NO 1 ELEVATOR CONSTRUCTORS UNIONOF NEW YORK CITY AND VICINITY
#> 4 JUNIOR ACHIEVEMENT OF THE OCOEE REGION
#> 5 AMERICAN FEDERATION OF LABOROHIO AFL-CIO
#> 6 THE KEYSTONE PROJECT INC
#> 7 METROPORT CHAMBER OF COMMERCE
#> 8 Nonprofit Holistic Community Care Center
#> title.raw tot.comp tot.hours role.final role.ceo
#> 1 President 55015 30 CEO imputed
#> 2 PRESIDENT 255118 40 CEO imputed
#> 3 PRESIDENT/BUSINESS MGR 423136 55 CEO imputed
#> 4 PRESIDENT 62704 40 CEO imputed
#> 5 PRESIDENT 302557 40 CEO imputed
#> 6 PRESIDENT 106291 40 CEO imputed
#> 7 PRESIDENT, C 202089 50 CEO imputed
#> 8 President 14481 40 CEO modelHow each filing is led:
df |>
dplyr::distinct(object.id, formtype, org.leadership) |>
dplyr::count(formtype, org.leadership)
#> formtype org.leadership n
#> 1 990 board_governed 53
#> 2 990 designated 32
#> 3 990 imputed 66
#> 4 990 no_paid 775
#> 5 990EZ board_governed 20
#> 6 990EZ designated 12
#> 7 990EZ model 10
#> 8 990EZ no_paid 352Input: the output of categorize_titles(). Output: the same rows with the role columns added. See Performance for how often the roles agree with hand labels.