# 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 example pedigree (the 17-subject fam1, of which this repository reproduces a 10-subject subset; see Caveats) 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()gained achrtypeargument ("autosome", the default, or"x") and asexargument, 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. That said, looking at the full-fixture pair directly, the two mate-line conventions now agree more closely than when this article was first published: both packages draw a mated pair side by side and drop the descent line to their children from the midpoint between the two symbols. The visible difference that remains is the mating-unit marker itself – the small dot makePedigreeMatingLayout() draws at that midpoint, which kinship2 does not draw at all (the marker’s own visibility was weighed and kept as-is in issue #161). Under the joint-solver positioning engine (S673–S675, 2026-09), a union dot’s placement comes from a soft centering objective under hard minimum-separation floors: the dot sits at the exact midpoint between its two rendered mates wherever spacing allows – on this fixture, all four union dots are exactly centered (live-measured, S698; re-verified at the 2026-09-18 census re-run) – while on a crowded colony-scale pedigree a small off-center residual can remain where the separation floors bind (6 of 237 unions on the bundled 375-individual fixture, per the project’s standing drawing-error census: the 2026-09-18 re-run, whose class-(b) predicate now skips solver-precision dust below an invisible 0.12-px floor, leaving the 6 genuinely floor-bound unions the structural-residual test also names). An earlier revision of this paragraph described the dot as positioned at the sire’s own symbol; that was accurate for the engine at the time (through S667) and is superseded by the joint-solver engine. All of this 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, which document and assert 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 do not duplicate the same individuals for the unions above). .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’ overall layout is not, and was never claimed to be, visually identical. Their mate-line conventions now largely agree – both draw a mated pair side by side with the descent line dropping from the midpoint (visible directly in Track B’s full-fixture image pair above) – but
makePedigreeMatingLayout()additionally marks the union with a small dot at that midpoint, which kinship2 does not draw (the marker’s own visibility was separately weighed and kept as-is in issue #161), and on a crowded colony-scale pedigree the dot’s exact centering is a soft objective that can leave a small off-center residual where the engine’s separation floors bind (see the rewritten mate-line paragraph in Track B above). 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 exact 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.
