# Reproduce (build-ignored; not run on render). Requires kinship2 installed
# locally (install.packages("kinship2")) and Chrome (chromote, an indirect
# Suggests dependency via shinytest2, for the nprcgenekeepr diagram
# screenshots):
# Rscript data-raw/kinship2FidelityValidation.R
# Writes PNGs to vignettes/articles/kinship2-fidelity-validation-img/ and
# prints the numeric and structural-comparison summary below to the
# console.Why this article exists
The kinship2 R package is the field’s standard reference implementation for pedigree kinship computation and pedigree drawing (Sinnwell, Therneau & Schaid 2014). Its own supplementary material (Sinnwell, Therneau & Schaid, “The kinship2 R Package for Pedigree Data: Supplementary Material”) works a small, fully-specified 10-subject example pedigree through kinship2’s own kinship matrix, X-chromosome kinship matrix, and pedigree-trimming (“shrink”) functions.
The ratified kinship2 supplement full-reproduction plan closed 3 tracks against that supplement:
-
Track A –
kinship()gainedchrtype = c("autosome", "x")andsexarguments, reproducing the supplement’s X-chromosome kinship matrix (Table S2). -
Track B – new
shrinkPedigree(), akinship2::pedigree.shrink()equivalent over this package’s ownid/sire/damdata-frame pedigree representation. -
Track C –
makePedigreeMatingLayout()’s consanguineous-mating visual marker (a distinct color/width on a blood-related couple’s mate-line edges) now propagates correctly ontoedgeStyle = "rectilinear"’s dogleg-rerouted projection edges, not just the direct-style case.
Each track’s own implementing session (Track C: S563, Track A: S564, Track B: S565) verified its own fixtures against a live, installed kinship2 1.9.6.2 and recorded the results as hardcoded expected values in tests/testthat/test_kinship.R, test_shrinkPedigree.R, and test_makePedigreeMatingLayout.R. This article is the recorded, side-by-side evidence for a reader who was not in those sessions: for each track, the exact same fixture is run through both packages, live, and the numeric and graphic output is shown together, not just asserted equal in a test file. It is the same “validate before expose” discipline as fg-se-validation.qmd, applied to a reference package instead of a reference paper’s worked numbers.
kinship2 is not a dependency of nprcgenekeepr. Every comparison below was generated once, offline, by data-raw/kinship2FidelityValidation.R (kinship2 installed locally, used interactively – matching the same evidence standard and the same “no new Suggests dependency” choice the 3 tracks’ own implementing sessions already made) and the resulting numbers/images are embedded below, exactly as fg-se-validation.qmd embeds its own offline validation study’s results rather than recomputing them at render time.
Track A – X-chromosome kinship (Table S2)
Fixture
The kinship2 supplement’s own Figure S1 subset (reconstructed from Table S1’s printed kinship values, per KINSHIP2_SUPPLEMENT_REPRODUCIBILITY_AUDIT_2026-08-13.md): a 10-subject pedigree in which subjects 8 and 9 are full siblings declared as a monozygotic (MZ) twin pair, and subject 10 is a child of twin 8 – the load-bearing case for confirming a twin correction propagates to a non-twin descendant, not just the declared pair.
Numeric fidelity
Both packages compute the full 10x10 autosomal matrix and the full 10x10 X-linked matrix on the identical fixture (nprcgenekeepr’s own MZ-twin correction supplies the same relation/twinRelations declaration to each package in its own idiom):
| Comparison | max|nprcgenekeepr − kinship2| | Identical? |
|---|---|---|
| Autosomal kinship matrix (100 cells) | 0 | Yes |
| X-linked kinship matrix (100 cells) | 0 | Yes |
A few named cells, reproducing the supplement’s own Table S2 (self-kinship differs by sex on the X chromosome – a male’s self-kinship is 1.0, not 0.5, since he carries a single X copy):
| Pair | Relationship | nprcgenekeepr | kinship2 |
|---|---|---|---|
| (1, 1) | male self | 1.0000 | 1.0000 |
| (2, 2) | female self | 0.5000 | 0.5000 |
| (1, 3) | father-son | 0.0000 | 0.0000 |
| (1, 4) | father-daughter | 0.5000 | 0.5000 |
| (8, 9) | declared MZ twins, X-linked | 1.0000 | 1.0000 |
| (9, 10) | twin correction propagated to a child | 0.5625 | 0.5625 |
Graphic fidelity

Track B – shrinkPedigree() vs. pedigree.shrink()
Fixture
The composite 16-subject fixture from test_shrinkPedigree.R, constructed to exercise every removal phase of kinship2’s own algorithm in one pedigree: an unavailable (ungenotyped) terminal leaf, an unavailable founder couple with a single child, a childless “stray marry-in” founder, a genotyped but non-informative individual, and a priority-ordered affected-status reduction down to maxBits = 1.
Numeric fidelity
| Comparison | nprcgenekeepr | kinship2 | Match? |
|---|---|---|---|
| Surviving subject set (8 of 16) | C4, C4a, G3, L3, M1, P1, P2, P6 |
C4, C4a, G3, L3, M1, P1, P2, P6 |
Yes |
bitSize trajectory |
11 → 7 → 5 → 3 → 1 |
11 → 7 → 5 → 3 → 1 |
Yes |
Graphic fidelity
The same fixture, before and after shrinking, rendered by both packages. Both kinship2’s own plot.pedigree() and (as of Phase 1 of the P5-suppression fix, S644, 2026-08-27) nprcgenekeepr’s own makePedigreeMatingLayout() omit subject P5 (an isolated, mate-less, child-less founder) from the full-fixture diagram below – expected behavior for a disconnected singleton, not an error. See Structural verification below.

plot.pedigree() on the full 16-subject fixture. P5, a fully isolated founder, is declared but not placed on the plot grid.
makePedigreeMatingLayout() + visNetwork on the same full 16-subject fixture. As of Phase 1 of the P5-suppression fix (S644, 2026-08-27), this rendering now also omits P5, matching kinship2’s own convention of not placing a fully isolated individual on the diagram.
pedigree.shrink()$pedObj – shrunk to 8 subjects. Unavailable (‘?’) individuals still shown, per kinship2’s own convention.
shrinkPedigree()’s own surviving pedigree, same 8 subjects, rendered the same way. As of the disconnected-component separation fix (S667, 2026-09-02), the two unrelated surviving families are drawn as separate side-by-side blocks with the same one-unit gap kinship2 uses – every individual’s horizontal position matches kinship2’s own align.pedigree() exactly.What “matches” above means, precisely – and what it does not. The claim is scoped to which individuals appear at all (P5 now omitted by both) and to the structural parent-child/mate-pair relationships among the individuals that do appear (Structural verification below) – not to the two packages’ overall visual layout. Looking at the full-fixture pair directly: kinship2 draws each mated pair spread apart, with the descent line to their children dropping from the midpoint between the two symbols; makePedigreeMatingLayout() draws each pair close together, with the mating-unit marker (the small dot) positioned at the sire’s own symbol rather than centered between sire and dam. This is a real, visible difference in mate-line layout, not a rendering error – it is the established, already-decided rendering convention this package uses everywhere (the marker’s own visibility was weighed and kept as-is in issue #161), and it predates this article: it is unrelated to, and unchanged by, the P5-suppression fix, which only changes which individuals are placed on the diagram, never how a placed pair’s own mate line is drawn.
The two shrunk diagrams show the same 8 surviving subjects in the same 2 family groups – {P1, P2, M1, G3, L3} and {C4, P6, C4a} – confirming the numeric surviving-set match above is also structurally faithful, not just a matching id list.
Track C – consanguineous-marker propagation
Fixture
The 9-subject fixture from test_makePedigreeMatingLayout.R (S563): A and Y are full siblings (children of P1 x P2) who mate with each other – the consanguineous union under test – while A also mates with founder X in an unrelated union, and Y also mates with founder W in an unrelated union. Originally (S563), A anchored the A-X union despite the two individuals sitting at different generations, forcing exactly one edgeStyle = "rectilinear" “dogleg” reroute on A’s side – this was the fixture’s whole point, and the name it was given (“the 9-subject dogleg fixture”) in earlier project history. That dogleg no longer occurs, as of Track 4’s gen-aware anchor selection (S573, 2026-08-14, the day after this article was first published): under the gen-first tie-break, X (generation 3) now anchors the A-X union instead of A (generation 1), so A never anchors a union whose generation differs from his own – the consanguineous A-Y union is the only union A anchors, and it already matches his own generation. Track 4’s own structural invariant (genOf[[anchor]] == unitGen, unconditionally) makes this specific dogleg permanently unreachable, not just for this fixture – see tests/testthat/test_makePedigreeMatingLayout.R’s own “Track 4” comment and test (:1297-1368), which documents and asserts exactly this.
Graphic fidelity
kinship2 draws a consanguineous mating as a doubled connecting line between the two mates (visible directly between A and the left-hand Y) and – independently of anything to do with consanguinity – duplicates any individual who appears in more than one union (Y appears twice, joined by a dashed connector). makePedigreeMatingLayout() uses a distinct color and width on the consanguineous union’s own edges and also independently duplicates a multi-union individual with a dashed connector – the two packages converge on the same duplicate-node convention without having copied it from one another.

plot.pedigree(). The doubled line directly between A and the near Y node is kinship2’s consanguinity marker; the dashed arc connects Y’s two appearances (one per union she anchors).
makePedigreeMatingLayout(edgeStyle = "direct") – the A-Y union’s 2 mate edges render in vermillion (#D55E00) at width 4.
makePedigreeMatingLayout(edgeStyle = "rectilinear") – as of Track 4 (S573), this fixture no longer produces a dogleg on either side, so the marker propagates onto the same 2 direct edges as the direct style above.| Edge style | Marked (vermillion) edges | Expected |
|---|---|---|
direct |
2 | 2 (one edge per mate, no dogleg) |
rectilinear |
2 | 2 (identical to direct – Track 4, S573, eliminated the dogleg this fixture originally forced) |
Both packages flag the same union as consanguineous, using their own independent visual conventions – a thickened doubled line in kinship2, a distinct color and width in nprcgenekeepr – and nprcgenekeepr’s marker renders identically under both edge styles for this fixture, since Track 4 (S573, 2026-08-14) eliminated the dogleg reroute this fixture originally exercised (see the corrected Fixture description above).
Structural verification
Tracks B and C’s diagram images above are, on their own, still just two independently-rendered pictures placed side by side – a reader has to trust that they show the same family structure. The kinship2 structural/ topological comparison plan closes that gap: .extractKinship2Structure() and .extractNprcStructure() (both @noRd, zero kinship2 dependency) pull the real-individual-level parent-child edge set, mate-pair set, and rendered-individual set out of kinship2’s own pedigree object and out of makePedigreeMatingLayout()’s output respectively – at the level of who is related to whom, and who is shown at all, not which duplicate copy of a multi-union individual each package’s layout happens to draw (the two packages duplicate different individuals for the same union above: Y in kinship2’s rendering, A in nprcgenekeepr’s). .comparePedigreeStructures() then diffs all three sets directly – “structurally identical” in this article means identical on these three sets, never identical mate-line layout or node placement; see the caveat in Graphic fidelity above and Caveats carried forward below for the specific, visible layout convention the two packages do not share.
A real gap in this comparator was found and fixed 2026-08-26. The original version diffed only the parent-child edge set and mate-pair set – never the set of individuals actually displayed. An individual with zero edges on either side (a fully isolated founder, like P5 in Track B’s full fixture, described above) is invisible to that diff: it contributed no rows to either table regardless of whether either package rendered it, so the comparator reported identical = TRUE on a pair of images that visibly differ (16 rendered nodes vs. 15). The comparator now also diffs the rendered-individual set on each side (kinship2’s side computed from align.pedigree()’s own plot-grid placement, not merely its pedigree() object’s declared id list) – see tests/testthat/test_comparePedigreeStructure.R’s “individuals” test block for the full regression coverage, including this exact Track B fixture.
Run against this article’s own Track B and Track C fixtures, live, with the fixed comparator:
| Fixture | Structurally identical to kinship2? |
|---|---|
| Track B, full (16 subjects) |
Yes – nprcgenekeepr now suppresses P5, matching which individuals kinship2 places on the diagram (see Graphic fidelity above for what this does, and does not, say about the two packages’ mate-line layout) |
| Track B, shrunk (8 subjects) | Yes |
| Track C (9 subjects, consanguineous dogleg) | Yes |
All three fixtures are now a clean, no-discrepancy match. P5 does not survive shrinkPedigree()’s trim (an uninformative, unavailable founder with no descendants), so Track B shrunk never exercised this case; Track C’s fixture has no isolated individuals at all. Track B’s full fixture previously showed a genuine discrepancy here: makePedigreeMatingLayout() rendered P5 while kinship2’s own plot omitted it. That was an inconsistency in nprcgenekeepr’s own rendering convention, not a preferable colony-management default – kinship2’s own align.pedigree() never places a fully isolated individual (no recorded parents, mates, or offspring) on the plot grid, and nprcgenekeepr now matches that convention (makePedigreeMatingLayout()’s new isolation pre-filter, Phase 1 of the P5-suppression plan, S644, 2026-08-27, closing issue #164).
Caveats carried forward
-
The two packages’ mate-line layout is not, and was never claimed to be, visually identical. kinship2’s
align.pedigree()spreads a mated pair apart and drops the descent line to their children from the midpoint between the two symbols;makePedigreeMatingLayout()draws a mated pair close together, with the mating-unit marker (the small dot) positioned at the sire’s own symbol rather than centered between sire and dam – visible directly in Track B’s full-fixture image pair above. This is nprcgenekeepr’s own established rendering convention, unrelated to and unchanged by any track in this article (the marker’s own visibility was separately weighed and kept as-is in issue #161). Every “match”/ “identical” claim in this article is scoped to numeric values, surviving-subject sets, or the structural parent-child/mate-pair/rendered-individual sets (Structural verification) – never to mate-line spacing or node placement. -
kinship2 is not a package dependency. Nothing above runs at
quarto rendertime or inR CMD check– the numbers and images are the frozen output of one offline, interactively-run script, matching this package’s established precedent of never callingkinship2::live from committed test or documentation code. -
The full 17-subject
fam1pedigree from the kinship2 supplement’s main worked example is not reconstructable from this repository’s materials (its source figure lives in the kinship2 application note, not the supplement PDF this repository ships). Track A’s fixture is the fully-specified 10-subject Figure S1 subset, per the audit’s own scope caveat – not a limitation introduced by this article. -
Tracks B and C have no PDF-printed worked example at all – the supplement names only which subjects a shrink trims, never their relationships, and says nothing about visual conventions. Both tracks’ ground truth is a live, installed
kinship2::pedigree.shrink()/plot.pedigree()run on a fixture purpose-built to exercise the relevant algorithm, not a supplement-sourced value. -
kinship2::pedigree()’s sex-role validation is stricter than nprcgenekeepr’s own sire/dam columns. Track C’s own committed test fixture lists one individual (Y) as asirein one row despite her declared sex being female – valid input tomakePedigreeMatingLayout(), which does not enforce sex/column-role consistency, but rejected bykinship2::pedigree(), which does. The validation script swaps that one row’s 2 column values (same 2 parents, same family structure) only for the kinship2-side object; every nprcgenekeepr call in this article uses the fixture exactly as committed.
Verdict
PASS. Track A’s autosomal and X-linked kinship matrices are bit-for-bit identical to kinship2’s own output across every one of 200 compared cells (100 autosomal + 100 X-linked), including the MZ-twin correction and its propagation to a descendant. Track B’s shrinkPedigree() reproduces kinship2’s exact surviving subject set and exact bitSize trajectory, and the shrunk pedigrees are the same 2 family groups – confirmed by a real edge-set-and-individual-set diff, not just a visual read (Structural verification). Track B’s full 16-subject fixture previously showed a genuine discrepancy here: nprcgenekeepr rendered the isolated founder P5 while kinship2’s own plot.pedigree() silently omitted it – caught only after the structural comparator itself was fixed to diff the rendered-individual set, not just edges (the gap that let a false identical = TRUE pass unnoticed until an owner-directed image review caught it live, 2026-08-26). That discrepancy is now resolved: makePedigreeMatingLayout() suppresses a fully isolated individual (no recorded parents, mates, or offspring) from the rendered diagram, matching kinship2’s own convention (Phase 1 of the P5-suppression plan, S644, 2026-08-27, closing issue #164). Track C’s consanguineous-mating marker flags the same union kinship2 flags, under both edge styles, using an independently-converged duplicate-node convention for the same underlying multi-union case kinship2 also duplicates – structurally confirmed identical. All three tracks – Track A, both of Track B’s fixtures, and Track C – are now cleared as faithful, structurally identical reproductions of the kinship2 supplement’s own results. “Structurally identical” is precise, not a stand-in for “visually identical”: the two packages’ own mate-line layout conventions remain, and have always been, different (see Caveats carried forward).
References
Sinnwell, J.P., Therneau, T.M., Schaid, D.J. (2014) “The kinship2 R package for pedigree data.” Human Heredity 78(2):91-93.
Sinnwell, J.P., Therneau, T.M., Schaid, D.J. “The kinship2 R Package for Pedigree Data: Supplementary Material.” Mayo Clinic (PMC manuscript NIHMS593658); shipped in this repository at inst/extdata/reference/NIHMS593658-supplement-supplement_1.pdf.
See also kinship(), shrinkPedigree(), makePedigreeMatingLayout(), and kinship2-supplement-full-reproduction-plan.md.
