Source code for gain.genomic_resources.statistics.percentages

"""How a share of a whole is written, for every table that writes one.

One rule in one place, so every info-page table that renders a count
as a percentage of a total says the same thing the same way: the
genomic-score page's Coverage table (denominator: a chromosome's
length) and Alleles tables (denominator: an allele total), and the
reference-genome page's nucleotide and di-nucleotide tables
(denominator: the bases, or the pairs, counted).

:func:`percentage_of` is the rule itself, and it is deliberately
SCALAR -- one (count, total) pair to one string.  What varies between
the callers is the contract around a MISSING denominator, and that is
theirs to keep.  Coverage resolves a denominator per row and degrades
only that row, as do the Alleles table's per-class share columns,
whose denominator is each chromosome's own allele count.
:func:`percentages_over` is the whole-map answer -- no total, no
percentages at all -- which the substitution matrix, the complex table
and the reference-genome tables ask.

Every caller hands over the INTEGERS.  The boundaries are decided on
them, so a share stored already multiplied out cannot be written by
this rule; the reference-genome page derives its shares from the
stored counts for that reason.
"""
from __future__ import annotations

from collections.abc import Mapping


[docs] def percentage_of(count: int, total: int) -> str: """``count`` as a percentage of ``total``, floored and capped. The result is text for an HTML page and can carry markup-significant characters -- the floor and the ceiling begin with ``<`` and ``>`` -- so a template rendering it must escape, which the ``.jinja`` HTML templates do and the Markdown ones deliberately do not. ``total`` must be positive; a share of nothing is not a percentage, and what to show instead is the caller's question rather than this one's (see the module docstring). Two answers a bare ``"%.2f%%"`` gets wrong: * A nonzero count too small to survive two decimals renders ``<0.01%``, never ``0.00%``. On a real score ``complex`` is 881 alleles out of 727,413,443 while ``other`` is genuinely empty -- and telling those two apart is the whole reason a percentage is shown at all. * A count that falls SHORT of the total but rounds up to it renders ``>99.99%``, never ``100.00%`` -- the floor reflected (gain#990). On that same score the substitutions are all but 881 of the alleles, and a column reading ``substitution 100.00%`` beside ``complex <0.01%`` says the resource is entirely one class in the act of showing that it is not. Both boundaries are decided on the INTEGERS, never on the rendered float: a count that IS the total renders ``100.00%`` and a count of zero renders ``0.00%``, because only a share that is not the whole is written as short of it, and only a share that exists at all is written as too small to see. """ rendered = f"{100.0 * count / total:.2f}%" if count and rendered == "0.00%": return "<0.01%" if count < total and rendered == "100.00%": return ">99.99%" return rendered
[docs] def percentages_over[K]( counts: Mapping[K, int], total: int, ) -> dict[K, str] | None: """Each count as a percentage of ``total``, ``None`` without one. Every cell is written by :func:`percentage_of`. What this adds is the MAP contract around a missing denominator: a zero total has no percentage, and the answer is ``None`` for the WHOLE map rather than per cell, because the denominator is a property of the table. The caller decides what a table without one shows -- the Alleles section drops the column, the reference-genome page leaves the cells empty. """ if total <= 0: return None return { key: percentage_of(count, total) for key, count in counts.items() }