# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """Family F: which items get served, and how often the same one comes back. Reads ``item_id``, which holds public identifiers only -- a Wikidata QID, a Commons page id, a Wolne Lektury slug, a content-addressed filename. Never a title, a caption or a name. That constraint is what makes this family possible at all: "the pope pool feels repetitive" is an impression, and "106 draws produced 61 distinct popes" is a number. The repeat rate is the headline. For a pool of size N drawn uniformly, the expected number of distinct items after k draws is N(1 - (1 - 1/N)^k), so a distinct count well below that means the draw is not uniform -- which is exactly the bug that put one identical photograph into a live group chat when iNaturalist silently ignored ``order_by=random``. """ from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Final if TYPE_CHECKING: import sqlite3 from signal_bot.analytics.scope import Scope ITEMS_SQL: Final = """ SELECT command, COUNT(*) AS draws, COUNT(DISTINCT item_id) AS distinct_items FROM command_events WHERE (:group IS NULL OR group_id = :group) AND item_id IS NOT NULL AND outcome = 'ok' GROUP BY command ORDER BY draws DESC, command """ TOP_ITEMS_SQL: Final = """ SELECT command, item_id, COUNT(*) AS draws FROM command_events WHERE (:group IS NULL OR group_id = :group) AND item_id IS NOT NULL AND outcome = 'ok' GROUP BY command, item_id HAVING draws > 1 ORDER BY draws DESC, command, item_id """ @dataclass(frozen=True, slots=True) class Pool: """One command's draws, and how varied they turned out to be.""" command: str draws: int distinct: int @property def repeat_rate(self) -> float: """Share of draws that produced something already seen. Zero means every draw was new. Approaching one means the pool is exhausted -- or was never as large as it looked. """ if self.draws <= 0: return 0.0 return 1.0 - (self.distinct / self.draws) def pools(conn: sqlite3.Connection, scope: Scope) -> list[Pool]: """Every command that names what it served, most drawn first.""" return [ Pool( command=str(row["command"]), draws=int(row["draws"]), distinct=int(row["distinct_items"]), ) for row in conn.execute(ITEMS_SQL, scope.params()) ] def repeats(conn: sqlite3.Connection, scope: Scope) -> list[tuple[str, str, int]]: """(command, item, times) for every item served more than once. The identifiers are safe to show -- that is the whole reason the column may exist -- but they are only meaningful next to the command, so they are never returned on their own. """ return [ (str(row["command"]), str(row["item_id"]), int(row["draws"])) for row in conn.execute(TOP_ITEMS_SQL, scope.params()) ] def expected_distinct(pool_size: int, draws: int) -> float: """How many distinct items a *uniform* draw would have produced. The comparison that turns a repeat rate into a verdict. A pool of 266 traps drawn 19 times should yield about 18.4 distinct ones; yielding 9 would mean the randomness is not what it claims, which has happened in this repository before and reached a live group chat. """ if pool_size <= 0 or draws <= 0: return 0.0 return pool_size * (1.0 - ((pool_size - 1) / pool_size) ** draws)