Source code for trickle_down.hierarchy

"""Classes for representing hypothesis hierarchies."""

from typing import TYPE_CHECKING

from trickle_down.hypothesis import Hypothesis

if TYPE_CHECKING:
    from matplotlib.figure import Figure

# matplotlib, networkx and EoN are imported inside visualize_hierarchy
# rather than here. They are needed only for drawing, and EoN in
# particular is not available in every environment this runs in; a
# top-level import would make the whole package unimportable there.


[docs] class Hierarchy: """Statistical hypothesis hierarchy.""" def __init__(self, fwer: float, root: Hypothesis) -> None: if not 0.0 < fwer < 1.0: raise ValueError(f"fwer must lie in (0, 1); got {fwer}") self.fwer = fwer self.root = root
[docs] def test_hypotheses(self) -> None: """Test every hypothesis at the level the procedure assigns it. Safe to call again, for instance after changing ``fwer``: every node is re-tested, so verdicts always agree with the levels recorded alongside them. P-values are cached on each hypothesis, so a re-run costs only the comparisons. """ queue = [self.root] self.root.set_available_level(self.fwer) self.root._normalize_children_weights_recursively() while queue: hypothesis = queue.pop(0) # Re-test even a node marked tested. Skipping it would keep a # verdict reached at an earlier level while the level below # is overwritten, so the two would disagree. hypothesis.test_hypothesis() # Every node reaches the queue only after its parent has set # its level, and the root's was set above. available = hypothesis.available_level if available is None: raise ValueError(f"{hypothesis!r} was queued with no available level.") # A rejection passes the whole available level down; a # retention passes only the unwagered part. passed_down = 1.0 if hypothesis.rejected else 1.0 - hypothesis.stake for child in hypothesis.children: child.set_available_level(passed_down * available * child.weight) queue.append(child)
[docs] def hypotheses(self) -> list[Hypothesis]: """Return every hypothesis in the tree, parents before children.""" ordered: list[Hypothesis] = [] queue = [self.root] while queue: hypothesis = queue.pop(0) ordered.append(hypothesis) queue.extend(hypothesis.children) return ordered
[docs] def visualize_hierarchy(self) -> "Figure": """Draw the tree, coloring rejected hypotheses blue.""" import matplotlib.pyplot as plt import networkx as nx from EoN.auxiliary import hierarchy_pos G: nx.DiGraph[Hypothesis] = nx.DiGraph() labels: dict[Hypothesis, str] = {} colors: dict[Hypothesis, str] = {} for hypothesis in self.hypotheses(): G.add_node(hypothesis) labels[hypothesis] = repr(hypothesis) colors[hypothesis] = "skyblue" if hypothesis.rejected else "orange" for child in hypothesis.children: G.add_edge(hypothesis, child) pos = hierarchy_pos(G) fig = plt.figure(figsize=(16, 8)) ax = plt.gca() nx.draw( G, pos, ax=ax, with_labels=False, node_size=4_000, node_color=[colors[node] for node in G.nodes()], node_shape="o", alpha=0.8, ) nx.draw_networkx_labels(G, pos, labels, font_size=12, ax=ax) plt.axis("off") return fig