From pedigree records to genetic diversity analysis in R with visPedigree

A pedigree-based genetic diversity workflow in R: pedigree completeness, generation intervals, founder contributions, effective population size, and diversity half-life with visPedigree.
R
Genetics
Pedigree
Breeding
Conservation
r-bloggers
Author

Sheng Luan

Published

August 6, 2026

A pedigree is a family tree at population scale. Every breeding program, studbook, and conservation herd keeps one — usually as a spreadsheet with three columns: Ind, Sire, Dam, repeated row after row. Those columns tell you who is related to whom, but they say nothing about the questions that actually matter:

  1. How complete and deep is my pedigree? — Can I trust diversity estimates computed from it?
  2. How long is a generation? — What is the rate of genetic turnover?
  3. Who carries the gene pool? — Are a few founders or ancestors dominating the population?
  4. How much diversity remains? — What share of the founder genome is still present?
  5. How fast is it disappearing? — When will the founder genome halve?
  6. How large is the effective population size? — Is the population smaller than it appears?

In my previous tutorial, I showed how visPedigree turns raw pedigree tables into publication-ready graphs. This article covers the analysis side: how the same package computes the indicators used in breeding and conservation genetics — pedigree completeness, generation intervals, founder and ancestor contributions, effective population size, and a headline metric called the diversity half-life.

Getting started

Install visPedigree from CRAN and load it:

install.packages("visPedigree")
install.packages("optiSel")   # only needed for the ExamplePed dataset used below
library(visPedigree)

One tidy step, every analysis after

Every analysis in this article starts from the same object: a pedigree prepared by tidyped(). The function validates the parent–offspring records, orders individuals, infers generations, and flags pedigree loops.

We will use two datasets:

  • deep_ped (built in) — 4,396 individuals across deep generations, ideal for completeness, diversity, and half-life analysis.
  • ExamplePed from the optiSel package — the pedigree of a Hinterwald cattle population: 1,179 animals born between 1947 and 1995, ideal for generation-interval analysis because generations overlap.
data(deep_ped, package = "visPedigree")
data(ExamplePed, package = "optiSel")

tp_deep <- tidyped(deep_ped)
summary(tp_deep)
Pedigree Summary
================

Total Individuals:  4399 
  - Males:    1839 (41.8%) 
  - Females:  2039 (46.4%) 
  - Unknown:  521 (11.8%) 

Pedigree Structure:
  - Founders (no parents):   138 
  - Both parents known:      4242 
  - Dam only known:          19 

Generation:
  - Maximum:  13 
  - Distribution: 13 generations (use str() for details)

Reproduction:
  - Individuals with offspring:  1037 
  - Sires:  483  (Mean=8.8, Max=63 offspring)
  - Dams:   554  (Mean=7.7, Max=34 offspring)

Full-sibling Families:
  - Number of families:      540 
  - Mean family size:        7.86
  - Maximum family size:     34 
  - Top families by size:
      K010263AxK010702YZ: 34
      K010278ExK010139A: 33
      K010767AxK010130YZ: 33
      K010148KxK010566YZ: 32
      K010538DxK010507YZ: 32

================

Everything downstream — statistics, contributions, diversity, effective size — consumes this same tidied object.

Question 1: How complete and deep is your pedigree?

Every diversity indicator inherits the quality of the pedigree it is computed from. A shallow or incomplete pedigree can bias estimates of inbreeding and genetic diversity, because unknown ancestry reduces the information available for reconstructing relationships. pedstats() gives a compact structural summary in one call:

stats_deep <- pedstats(tp_deep)
stats_deep$summary
       N NSire  NDam NFounder MaxGen
   <int> <int> <int>    <int>  <int>
1:  4399   483   554      138     13

The $ecg component reports ancestral depth for every individual, summarized by three complementary metrics:

  • ECG — equivalent complete generations: a depth measure that accounts for both how far and how completely ancestry is known. It is an important pedigree-depth measure when interpreting inbreeding- and coancestry-based analyses.
  • FullGen — the number of fully traced ancestral generations.
  • MaxGen — the deepest known ancestral path, even if incomplete.
tail(stats_deep$ecg)
        Ind      ECG FullGen MaxGen
     <char>    <num>   <num>  <num>
1: K110997Q 7.616211       5     12
2: K110997Z 6.722656       5     12
3: K110998Q 4.606445       2     12
4: K110998Z 6.417969       5     12
5: K110999Q 7.345215       5     12
6: K110999Z 6.875977       5     12

pedstats() also prints and plots these summaries — for example, the distribution of ECG across the population:

plot(stats_deep, type = "ecg", metric = "ECG")
Figure 1: Distribution of equivalent complete generations (ECG) in deep_ped. The distribution is skewed toward deep ancestry — the median is 6.4 ECGs, and most individuals carry more than six ECGs.

ECG is such a useful quantity that it has its own function, pedecg(), returning the per-individual table you can sort, filter, or export.

Question 2: How long is a generation?

The generation interval — the average age of parents when their offspring are born — determines how quickly genetic material cycles through the population. Longer generation intervals generally slow the rate at which genetic improvement accumulates per unit time.

We use the Hinterwald cattle pedigree bundled with optiSel — generations overlap there, which is exactly what makes generation intervals interesting:

ped_cattle <- tidyped(ExamplePed)   # column names are detected automatically
stats_time <- pedstats(ped_cattle, timevar = "Born")
stats_time$gen_intervals
   Pathway     N     Mean       SD
    <char> <int>    <num>    <num>
1: Average  1564 5.429011 3.306798
2:      DD   436 7.128390 3.355804
3:      DO   758 7.097622 3.311750
4:      DS   322 7.055960 3.255885
5:      SD   466 3.763863 2.355588
6:      SO   806 3.859772 2.414623
7:      SS   340 3.991223 2.490764

The output reports generation intervals by parental pathway, including sire–son (SS), sire–daughter (SD), dam–son (DS), and dam–daughter (DD), plus an overall average. Here the contrast is striking: sires are about four years old on average at the birth of their calves, dams about seven. This difference reflects the distinct reproductive and selection strategies for males and females in cattle breeding, and directly affects the rate of genetic gain per year.

plot(stats_time, type = "genint")
Figure 2: Generation intervals by gametic pathway in the Hinterwald cattle pedigree (optiSel::ExamplePed). Sires are about 4 years old on average at the birth of their calves (SS, SD); dams about 7 years (DS, DD).

You can also call pedgenint() directly for the full pathway table, or pass cycle to compare the realized interval with a target breeding cycle (here, a 4-year cycle):

pedgenint(ped_cattle, timevar = "Born", unit = "year", cycle = 4)

Question 3: Who carries the gene pool?

The most informative way to describe a population’s genetic architecture is to ask where its genes come from. pedcontrib() traces expected genetic contributions from founders and ancestors into a reference population — the cohort you care about (recent generations, breeding candidates, genotyped animals).

Here the reference population is the most recent two generations of deep_ped:

ref_pop <- tp_deep[Gen >= max(Gen) - 1, Ind]
length(ref_pop)
[1] 3471
contrib_res <- pedcontrib(tp_deep, reference = ref_pop, mode = "both")

# Classical (q = 2) and Shannon-entropy (q = 1) effective numbers
contrib_res$summary[c("n_founder", "f_e_H", "f_e", "n_ancestor", "f_a_H", "f_a")]
$n_founder
[1] 157

$f_e_H
[1] 92.0943

$f_e
[1] 64.73344

$n_ancestor
[1] 94

$f_a_H
[1] 60.1444

$f_a
[1] 44.12033

Two numbers carry most of the interpretation:

  • Effective number of founders, fe — how many founders the population behaves like, if their contributions were equal. A small fe relative to the actual founder count means founders were used very unequally.
  • Effective number of ancestors, fa — how many ancestors explain the gene pool after removing contributions already explained by more influential ones. When fa is markedly smaller than fe, a small set of key ancestors has contributed disproportionately to the gene pool, indicating a potential genetic bottleneck.
top_anc <- contrib_res$ancestors[1:10]
barplot(top_anc$Contrib, names.arg = top_anc$Ind,
        las = 2, ylab = "Contribution to reference population",
        col = "steelblue", border = NA)
Figure 3: Top 10 ancestor contributions to the reference population of deep_ped. The ten most influential of the 94 ancestors account for about 38% of the gene pool.

Per-founder contributions are in contrib_res$founders. The Shannon-entropy variants feH and faH weight founder and ancestor contributions more evenly; a large ratio feH / fe indicates that genetic contributions from less influential founders are retained but underrepresented by classical metrics.

Question 4: How much diversity remains?

pediv() is the integrated diversity summary. From the reference population it computes founder genome equivalents (fg), mean coancestry, and the retained genetic diversity index GeneDiv:

div_res <- pediv(tp_deep, reference = ref_pop, top = 10, seed = 42L)
div_res$summary[, .(NFounder, fe, fa, fg, MeanCoan, GeneDiv)]
   NFounder       fe       fa       fg  MeanCoan   GeneDiv
      <int>    <num>    <num>    <num>     <num>     <num>
1:      157 64.73344 44.12033 19.17965 0.0260693 0.9739307

The headline index is GeneDiv — the pedigree-based retained genetic diversity, equal to one minus the mean coancestry of the reference population. An unrelated, non-inbred population has GeneDiv = 1; under the standard pedigree coancestry definition, a population of full sibs from two unrelated founders has GeneDiv ≈ 0.75. Because it is a dimensionless proportion, it is the easiest metric to communicate to managers and stakeholders.

Founder genome equivalents (fg) is the more conservative indicator: it captures drift as well as unequal founder use and bottlenecks, so it is often the smallest of the three.

Question 5: How fast is diversity disappearing? — the diversity half-life

pediv() is a snapshot. pedhalflife() turns it into a film: for every time point in the pedigree (here, every generation), it recomputes fe, fa, and fg, then fits a log-linear decay model to quantify the rate of diversity loss.

The total loss rate is decomposed into three additive components, each with a distinct biological meaning:

Component Symbol Source of loss
Foundation λe Unequal founder contributions
Bottleneck λb Overuse of a few key ancestors
Drift λd Random genetic drift in a finite population

The diversity half-life is the number of time units for fg to halve: T₁/₂ = ln 2 / λtotal.

hl <- pedhalflife(tp_deep, timevar = "Gen")
print(hl)
Information-Theoretic Diversity Half-Life
-----------------------------------------
Total Loss Rate (lambda_total):   0.128778
  Foundation  (lambda_e)      :   0.034626
  Bottleneck  (lambda_b)      :   0.025217
  Drift       (lambda_d)      :   0.068935
-----------------------------------------
Diversity Half-life (T_1/2)   :       5.38 (Gen)

Timeseries: 13 time points
hl$decay
      LambdaE    LambdaB    LambdaD LambdaTotal    THalf
        <num>      <num>      <num>       <num>    <num>
1: 0.03462645 0.02521662 0.06893453   0.1287776 5.382513

For deep_ped, the drift component (λd) dominates the total loss rate — most diversity loss here is random drift rather than fixable selection decisions. The half-life of about 5 generations means that, at the current rate, founder genome equivalents will halve after approximately five generations. A shorter diversity half-life indicates faster loss of founder genome representation and can serve as an indicator for diversity management.

The full object also exposes the per-generation time series, and can be plotted on the log scale (the fitted linear decays) or the raw scale:

plot(hl, type = "log")
Figure 4: Log-linear decay of effective founders (ln fe), effective ancestors (ln fa), and founder genome equivalents (ln fg) across generations in deep_ped. Slopes of these lines are λe, λb, and λd.
plot(hl, type = "raw")
Figure 5: The same diversity metrics on the original scale: fe, fa, and fg by generation.

To focus the half-life on a specific window — for example, the last four generations — pass them to the at argument:

hl_recent <- pedhalflife(tp_deep, timevar = "Gen",
                         at = tail(sort(unique(tp_deep$Gen)), 4))
print(hl_recent)

The λ decomposition turns “we are losing diversity” — which everyone already knew — into “we are losing it for this reason”, which tells you what to do: broaden founder use for λe, balance the use of influential ancestors for λb, and enlarge the breeding population (or cryopreserve) for λd.

Question 6: How large is the effective population size?

pedne() computes pedigree-based estimates of effective population size from the same tidied object, using demographic parent counts, the realized rate of inbreeding, or the rate of coancestry:

# Coancestry-based Ne — the strictest and most sensitive warning signal
ne_coan <- pedne(tp_deep, method = "coancestry", by = "Gen", seed = 42L)
ne_coan
    Cohort     N NSampled      DeltaC        Ne
     <int> <int>    <int>       <num>     <num>
 1:      1   138      138          NA        NA
 2:      2    96       96 0.003707511 134.86137
 3:      3    74       74 0.004556238 109.73967
 4:      4    91       91 0.011999547  41.66824
 5:      5    95       95 0.007279728  68.68388
 6:      6   119      119 0.016808626  29.74663
 7:      7    37       37 0.007397103  67.59403
 8:      8    41       41 0.007575599  66.00139
 9:      9    51       51 0.006888524  72.58449
10:     10    78       78 0.005429686  92.08636
11:     11   108      108 0.005479583  91.24782
12:     12   209      209 0.005239967  95.42044
13:     13  3262     1000 0.003887379 128.62137

The coancestry-based estimate detects the accumulation of relatedness before it is fully expressed as inbreeding, making it the earliest warning signal in managed populations. Individual inbreeding coefficients can also be computed with inbreed() (or tidyped(..., inbreed = TRUE)) and displayed directly on the pedigree graph with visped(showf = TRUE), as shown in the visualization tutorial; pedfclass() classifies individuals by inbreeding severity.

Where to go from here

This article covered the pedigree-based diversity pipeline: completeness (pedstats(), pedecg()), generation intervals (pedgenint()), gene-origin analysis (pedcontrib()), diversity snapshots (pediv()), diversity dynamics (pedhalflife()), effective size (pedne()), and inbreeding monitoring (inbreed(), pedfclass()).

The same tidied pedigree object also powers relationship matrices and large-scale pedigree-based computations. That is the topic of a future article.

The same workflow can also be applied to large-scale breeding populations, including aquaculture species with complex family structures.

For published research using visPedigree, see the Applications page and the visualization tutorial.

If visPedigree contributes to your published work, please cite it:

Luan S, Kong J, Xia Z, Kang Z, Qiang G, Luo K, Sui J. 2026. visPedigree: a comprehensive R package for tidying, analyzing, and visualizing breeding pedigrees. Bioinformatics Advances. DOI: 10.1093/bioadv/vbag210.

(citation("visPedigree") always returns the citation for your installed version.)

Install from CRAN:

install.packages("visPedigree")

Full documentation at luansheng.github.io/visPedigree.