Skip to contents

link_panel() links a person to themselves across years within one organization. This article covers the complementary problem: linking the same person across different organizations — the basis of interlocking directorate studies, where one individual sits on several nonprofit boards.

The two are deliberately separate. Within-org linkage can lean on a tight block (same ein) and rich cross-year signal. Cross-org linkage cannot — it must search the whole population — so it uses a different machine: person profiles plus hash-lookup blocking.

The shape of the pipeline

link_panel()            -> EMP_ID (within-org, across years)
  |
build_person_profile()  -> one identity profile per EMP_ID, with blocking keys
  |
blocking_passes()       -> hash keys that bring candidate pairs together
  |
score_candidate_pairs() -> score cross-org pairs
  |
filter_edges_by_role()  -> keep the interlocks you care about (e.g. board-board)
  |
stamp_xorg()            -> a cross-org person id

link_cross_org() wraps the whole chain; the pieces are exported for custom pipelines.

Person profiles

A person may have many within-org records. build_person_profile() reduces each EMP_ID cluster to one canonical profile — the identity you actually match on across orgs — and precomputes the blocking keys used to find candidates:

linked   <- link_panel(panel)              # must already carry EMP_ID
profiles <- build_person_profile(linked)
# each profile carries last-name blocking keys for the hash join:
str(profiles$last_name_keys[[1]])

Blocking passes

Comparing every profile to every other is quadratic and hopeless at population scale. Instead, candidates are generated by blocking passes — each pass emits a hash key, and only profiles sharing a key are ever compared. The available passes are inspectable:

names(blocking_passes())
#> [1] "strict"   "geo"      "industry" "surname"

Multiple passes give multiple chances to co-locate a true pair (e.g. a last-name-based key and a phonetic key), so a single noisy field does not hide an interlock. person_blocking_keys() computes the keys for a profile; candidate_pairs() turns shared keys into the pairs to score.

Scoring and role filtering

Cross-org pairs are scored with the same Fellegi–Sunter machinery as within-org linkage (see How the matching works), using the population-level surname prior (population_surname_weight()) rather than a per-org one. You then keep the edges whose role composition matches your question — most interlock studies want board-to-board ties:

xo <- link_cross_org(linked, state = "state", ntee = "ntee", threshold = 8)

subset(xo$profiles, XORG_N_ORGS > 1)                 # people who interlock
board_only_edges(xo$edges)                           # board <-> board ties only
non_board_edges(xo$edges)                            # staff/officer ties

filter_edges_by_role() is the general form; board_only_edges() and non_board_edges() are convenience wrappers. Role is assigned by the taxonomy in Title roles.

Stamping the cross-org id

stamp_xorg() closes the accepted cross-org edges into components and stamps a cross-organization person identifier, alongside a count of the distinct organizations each person appears in (XORG_N_ORGS) — the raw material for an interlock network.