Source code for oscar_colony.optimise.estimate_offspring
from dataclasses import dataclass, field
from oscar_colony.breeding_scheme import (
BreedingScheme,
Genotype,
generate_breeding_schemes,
)
from oscar_colony.historical_stats import LineStatistics
[docs]
@dataclass
class ExpectedOffspring:
"""Summary of expected average number of offspring from a single mating"""
total_n: float = 0 # litter size
n_per_genotype: dict[tuple[Genotype, ...], float] = field(
default_factory=dict
)
[docs]
def estimate_n_offspring_per_mating(
line_stats: LineStatistics,
default_litter_size: int,
min_n_matings: int = 3,
min_n_offspring: int = 10,
) -> dict[BreedingScheme, ExpectedOffspring]:
"""For all possible breeding schemes for the given line, estimate the
number of offspring produced per mating.
Calculates the total number across all genotypes == the litter size. As
well as the expected number per offspring genotype.
Parameters
----------
line_stats : LineStatistics
Statistics from historical data for the line
default_litter_size: float
The default value used for average litter size if there isn't enough
historical data for the line. This should usually be set to the average
litter size across all available data for all lines.
min_n_matings : int, optional
Minimum number of successful matings required to use the measured
litter size from line_stats. If there aren't enough matings for a
specific breeding scheme, the average of the whole line will be used
instead. If the whole line also doesn't have enough matings, then
default_litter_size is used.
min_n_offspring: int, optional
Minimum number of offspring required from a breeding scheme to use
the measured proportion of each genotype from line_stats. If not met,
the theoretical mendelian ratio will be used instead.
Returns
-------
dict[BreedingScheme, ExpectedOffspring]
Returns a dict mapping each breeding scheme to the expected number
of offspring it will produce per mating.
"""
# The breeding schemes listed in line_stats.stats_per_breeding_scheme are
# sparse i.e. only breeding schemes that appeared in the historical data
# are included.
# Here we need ALL possible schemes:
n_mutations = line_stats.n_mutations
breeding_schemes = generate_breeding_schemes(n_mutations)
expected_offspring_per_scheme = {}
for breeding_scheme in breeding_schemes:
expected_offspring = ExpectedOffspring()
litter_size = _expected_litter_size(
breeding_scheme, line_stats, min_n_matings, default_litter_size
)
expected_offspring.total_n = litter_size
proportion_per_genotype = _expected_proportion_per_genotype(
breeding_scheme, line_stats, min_n_offspring
)
for genotype, proportion in proportion_per_genotype.items():
expected_n = proportion * litter_size
if expected_n > 0:
expected_offspring.n_per_genotype[genotype] = expected_n
expected_offspring_per_scheme[breeding_scheme] = expected_offspring
return expected_offspring_per_scheme
def _expected_litter_size(
breeding_scheme: BreedingScheme,
line_stats: LineStatistics,
min_n_matings: int,
default_litter_size: float,
) -> float:
"""Create an estimated average litter size (total number of individuals
produced from one mating).
Parameters
----------
breeding_scheme : BreedingScheme
The breeding scheme to return litter size for
line_stats : LineStatistics
Summary statistics from historical data for the line
min_n_matings : int
Minimum number of successful matings required to use the measured
litter size from line_stats. If there aren't enough matings for a
specific breeding scheme, the average of the whole line will be used
instead. If the whole line also doesn't have enough matings, then
default_litter_size is used.
default_litter_size : float
Default litter size to fallback to if there's not enough historical
data for the line.
Returns
-------
float
Expected litter size
"""
scheme_stats = line_stats.stats_per_breeding_scheme.get(
breeding_scheme, None
)
if (scheme_stats is not None) and (
scheme_stats.n_successful_matings >= min_n_matings
):
return scheme_stats.average_litter_size
elif line_stats.total_n_successful_matings >= min_n_matings:
return line_stats.average_litter_size
else:
return default_litter_size
def _expected_proportion_per_genotype(
breeding_scheme: BreedingScheme,
line_stats: LineStatistics,
minimum_n_offspring: int,
) -> dict[tuple[Genotype, ...], float]:
"""Calculate the expected proportion of offspring of each genotype.
If enough historical data is available in line_stats, the measured
proportion will be used. Otherwise, the theoretical mendelian ratio
will be returned.
Parameters
----------
breeding_scheme: BreedingScheme
Breeding scheme to return proportions for
line_stats : LineStatistics
Summary line statistics from historical data
minimum_n_offspring: int
The minimum number of offspring required for this breeding scheme to
use the genotyping ratio (measured from historical data). Otherwise,
defaults to theoretical mendelian ratio.
Returns
-------
dict[tuple[Genotype, ...], float]
A dictionary mapping offspring genotypes to the expected proportion
of that type
"""
scheme_stats = line_stats.stats_per_breeding_scheme.get(
breeding_scheme, None
)
# If there's enough recorded offspring, use the observed proportion
# from historical data
if (scheme_stats is not None) and (
scheme_stats.total_n_offspring >= minimum_n_offspring
):
return scheme_stats.proportion_offspring_per_genotype
else:
return breeding_scheme.mendelian_ratio()