supercontest.core.stats
Handles the building of the plots on the statistics page, one batch at a time.
Queries the raw data and reformats it into bar charts – rows ranked, a value
per row for each series (supercontest.util.charts) – which the SPA draws.
Remember that the statistical analysis works by COUNTING contestants (both teams of every game)
for each combination of values. Eg vikings were the favorite, eagles were the home team, and
vikings covered. Coverage would be the value, the count is +1, and the dimension for “by” or “per”
may be prediction or location. You can slice however you’d like. The counting happens in the
database, as a GROUP BY over the contestants fact table – see
supercontest.dbsession.selects.get_stats_counts().
A CHART is “MEASURE by ROWS, under a CONDITION”: the measure is what the bars show (coverage,
margin, …), the rows are the values of one grouping dimension, and the condition optionally
narrows the contestants counted to one value of another (Team = Vikings). A BATCH is what one
page shows: a measure and a condition, with a chart for every grouping dimension that says
something under them – at most five – each drawn as the batch’s Aggregation (a
share of each row, a count, a mean, …). The page asks for one batch at a time
(build_batch()), so its cost is the batch’s, not the cost of every chart there is.
Measures (
MEASURES) are only ever measured: coverage and margin are the results, and grouping or conditioning by a result re-expresses the coverage charts backwards. Team is not a measure: “Team by X” is the 32-series transpose of “X by Team”, which is a batch already.Grouping dimensions (
GROUPERS) are the rows and the conditions. A chart never uses one dimension twice.
WHAT IS HIDDEN, AND WHY. Every game is two rows, one per team, and the two mirror each other: one
covers and the other does not, their margins are +m and -m, one is favored and one is home. A
dimension’s StatsDim.symmetry says how its two rows relate. The day of the week is the
game’s, the same on both rows. So a mirrored measure grouped by the day, with nothing breaking
the mirror, is decided by the arithmetic rather than the data: margin by day is 0.0 in every row,
prediction by day is 50/50. A condition that holds for only one row of a game (Favorite, Home, a
team, a line range) breaks the mirror, and the same chart becomes a real question – “do
favorites cover on Thursday”. forced() names each chart whose values are fixed before
anything is counted, and no batch includes one.
FETCH, THEN RENDER. fetch_counts() asks for every count a batch needs in one query, and
render_plots() builds the figures from those counts alone, on a worker thread: building
figures is CPU-bound, and inline it would stall every other request on the worker.
Module Attributes
The single "per" value of a chart with no condition. |
|
How a dimension's two rows in one game relate. |
|
A contestant is one side of one game, so the mean of its picks is the picks per game. |
|
Every aggregation, by name. |
|
Every dimension, by name. |
|
What each scope can measure, the default first. |
|
The dimensions a chart's rows, or its condition, can be, in page order. |
|
The database's counts, as |
|
The fact column that tells one game from another, for counting a condition's games. |
Functions
The charts one page shows: a chart per grouping dimension with something to say. |
|
One batch's charts, and how many games they count. |
|
Each grouping dimension's values in a season, as a condition names them. |
|
The conditions a measure can be put under: each value that leaves a chart to show. |
|
Counts everything a set of plot groups needs, in one query. |
|
Whether a chart's values are fixed before anything is counted. |
|
Arranges the counts for one group by key, and builds its plots. |
Classes
How a chart reduces what it counted in each row to the bars it draws. |
|
One "main by X" analysis: a plot per |
|
Our database has dimensions we care to analyze for stats. |
- supercontest.core.stats.AGGREGATIONS = {'count': Aggregation(name='count', label='Count', agg=None, percent=False, spread=False), 'mean': Aggregation(name='mean', label='Mean', agg=<function _mean>, percent=False, spread=False), 'median': Aggregation(name='median', label='Median', agg=<function _median>, percent=False, spread=False), 'percent': Aggregation(name='percent', label='%', agg=None, percent=True, spread=False), 'pergame': Aggregation(name='pergame', label='Per game', agg=<function _mean>, percent=False, spread=False), 'stdev': Aggregation(name='stdev', label='Std dev', agg=<function _stdev>, percent=False, spread=True), 'total': Aggregation(name='total', label='Total', agg=<function _total>, percent=False, spread=False)}
Every aggregation, by name.
- class supercontest.core.stats.Aggregation(name: str, label: str, agg: Callable[[Iterable[int | float | str]], int | float] | None = None, percent: bool = False, spread: bool = False)
Bases:
objectHow a chart reduces what it counted in each row to the bars it draws.
With no
agg, the bars are the measure’s values stacked: as a share of the row (percent) or as raw counts. With one, they are a single series, theaggof the row’s values.- agg: Callable[[Iterable[int | float | str]], int | float] | None = None
- label: str
What the chart calls it: its title’s prefix, and with
aggits series’ name.
- name: str
What the API calls it.
- percent: bool = False
- supercontest.core.stats.Counts
The database’s counts, as
supercontest.dbsession.queries.get_stats_counts()returns them.alias of
dict[tuple[str, …],list[tuple[tuple[Any, …],int]]]
- supercontest.core.stats.DIMENSIONS = {'coverage': <supercontest.core.stats.StatsDim object>, 'dayofweek': <supercontest.core.stats.StatsDim object>, 'line': <supercontest.core.stats.StatsDim object>, 'location': <supercontest.core.stats.StatsDim object>, 'margin': <supercontest.core.stats.StatsDim object>, 'picks': <supercontest.core.stats.StatsDim object>, 'prediction': <supercontest.core.stats.StatsDim object>, 'team': <supercontest.core.stats.StatsDim object>}
Every dimension, by name.
- supercontest.core.stats.GAME_COLUMN = 'game'
The fact column that tells one game from another, for counting a condition’s games.
- supercontest.core.stats.GROUPERS = [<supercontest.core.stats.StatsDim object>, <supercontest.core.stats.StatsDim object>, <supercontest.core.stats.StatsDim object>, <supercontest.core.stats.StatsDim object>, <supercontest.core.stats.StatsDim object>]
The dimensions a chart’s rows, or its condition, can be, in page order.
- supercontest.core.stats.MEASURES: dict[str, list[StatsDim]] = {'nfl': [<supercontest.core.stats.StatsDim object>, <supercontest.core.stats.StatsDim object>, <supercontest.core.stats.StatsDim object>, <supercontest.core.stats.StatsDim object>, <supercontest.core.stats.StatsDim object>, <supercontest.core.stats.StatsDim object>], 'sbsc': [<supercontest.core.stats.StatsDim object>]}
What each scope can measure, the default first. NFL counts every game against the line; SBSC counts the contest’s picks.
- supercontest.core.stats.NULL_PER_VAL = 'null'
The single “per” value of a chart with no condition.
- supercontest.core.stats.PER_GAME = Aggregation(name='pergame', label='Per game', agg=<function _mean>, percent=False, spread=False)
A contestant is one side of one game, so the mean of its picks is the picks per game.
- class supercontest.core.stats.PlotGroup(main_dim: StatsDim, by_dims: list[StatsDim], per_dim: StatsDim | None = None, per_value: int | float | str | None = None, aggregation: Aggregation | None = None)
Bases:
objectOne “main by X” analysis: a plot per
bydimension, under an optional condition.The “main” dimension sets the cols on the hbar, the xaxis vals. The “by” list creates a plot for each item, main dimension against this as rows, the yaxis vals. The “per” is the condition: only the contestants whose
per_dimisper_valueare counted (eg coverage by team when favorite).- aggregation: Aggregation | None = None
How the measure is charted; its default when None.
- property groupings: dict[str, tuple[str, ...]]
The fact columns counted for each
bydimension, per value first and main last.- Returns:
Keyed by the
bydimension’s name.
- per_value: int | float | str | None = None
- class supercontest.core.stats.StatsDim(name: str, symmetry: ~typing.Literal['mirrored', 'sign', 'game', 'none'], val_sort_key: ~collections.abc.Callable[[int | float | str], int | float | str], column: str | None = None, val_getter: ~collections.abc.Callable[[int | float | str], int | float | str] = <function _identity>, aggregations: tuple[~supercontest.core.stats.Aggregation, ...] = (Aggregation(name='percent', label='%', agg=None, percent=True, spread=False), Aggregation(name='count', label='Count', agg=None, percent=False, spread=False)), main_plot_sort_reverse: bool = True, by_val_getter: ~collections.abc.Callable[[int | float | str], int | float | str] | None = None, by_sort_key: ~collections.abc.Callable[[int | float | str], int | float | str] | None = None, shared: frozenset[str] = frozenset({}), determined_by: str | None = None, pins: ~collections.abc.Mapping[str, str] | None = None)
Bases:
objectOur database has dimensions we care to analyze for stats. These are their properties.
- group_val(raw: int | float | str) int | float | str
This dimension’s value for a row it groups, from the column’s value.
- Parameters:
raw – The value from the database.
- Returns:
The row, or the condition value, it falls under.
Whether both rows of one game can hold this value.
- Parameters:
value – A grouped value of this dimension.
- Returns:
True for every value of a
gamedimension, and for the listed others.
- pins: Mapping[str, str]
- symmetry: Literal['mirrored', 'sign', 'game', 'none']
- supercontest.core.stats.Symmetry
How a dimension’s two rows in one game relate.
mirrored: the two are opposites (a cover and a noncover, lines of -7 and +7).sign: mirrored, and the value is ONLY which side – nothing like a line’s size is left once the side is known.game: the same on both rows, a property of the game.none: neither (a team and its opponent are just two teams).alias of
Literal[‘mirrored’, ‘sign’, ‘game’, ‘none’]
- supercontest.core.stats.batch(main_dim: StatsDim, per_dim: StatsDim | None, per_value: int | float | str | None, aggregation: Aggregation | None = None) PlotGroup
The charts one page shows: a chart per grouping dimension with something to say.
- Parameters:
main_dim – What is measured.
per_dim – The condition’s dimension, or None for every game.
per_value – The condition’s value.
aggregation – How the measure is charted; its default when None.
- Returns:
The group; its
by_dimsare empty for a condition nothing can be said under, and for a measure conditioned on itself.
- async supercontest.core.stats.build_batch(session: AsyncSession, season: int | None, main: str, per: str | None = None, value: str | None = None, agg: str | None = None) tuple[list[BarChart], int]
One batch’s charts, and how many games they count.
- Parameters:
session – The session to read through.
season – The season to restrict the scope to, or None for all seasons.
main – The measure’s name.
per – The condition’s dimension, or None for every game.
value – The condition’s value.
agg – How the measure is charted, one of its
aggregations; its default when None.
- Returns:
The charts, and the number of games in scope: under a condition, the games with a side that meets it.
- async supercontest.core.stats.condition_values(session: AsyncSession, season: int | None) dict[str, list[str]]
Each grouping dimension’s values in a season, as a condition names them.
- Parameters:
session – The session to read through.
season – The season to restrict the scope to, or None for all seasons.
- Returns:
Keyed by dimension name, each dimension’s values in its own order.
- supercontest.core.stats.conditions(main_dim: StatsDim, values: Mapping[str, list[str]], aggregation: Aggregation | None = None) dict[str, list[str]]
The conditions a measure can be put under: each value that leaves a chart to show.
- Parameters:
main_dim – What is measured.
values – Each grouping dimension’s values, from
condition_values().aggregation – How the measure is charted; its default when None.
- Returns:
The dimensions with any such value, in page order, and those values.
- async supercontest.core.stats.fetch_counts(session: AsyncSession, season: int | None, groups: list[PlotGroup], extra: Iterable[tuple[str, ...]] = ()) dict[tuple[str, ...], list[tuple[tuple[Any, ...], int]]]
Counts everything a set of plot groups needs, in one query.
A count depends only on which columns are grouped, so the groups’ groupings are all asked for at once.
- Parameters:
session – The session to read through.
season – The season to restrict the scope to, or None for all seasons.
groups – The analyses to count for.
extra – More groupings to count in the same query.
- Returns:
The counts, for
render_plots().
- supercontest.core.stats.forced(main_dim: StatsDim, by_dim: StatsDim, per_dim: StatsDim | None, per_value: int | float | str | None, aggregation: Aggregation | None = None) bool
Whether a chart’s values are fixed before anything is counted.
A mirrored measure by a
gamedimension, under a condition that keeps the mirror whole: each row holds both sides of its games, so the measure is balanced in every row – 50/50, even counts, or a mean and median of 0. Not its spread: a balanced row can be tight or wide.A
gamemeasure by asigndimension, under the same: each game puts one row on each side, so the two sides’ rows are the same row twice.A measure grouped or conditioned by the dimension that decides it: 100% in every row.
Rows the condition decides: one row, which compares nothing.
Anything measured or grouped by a dimension the condition pins to one value.
- Parameters:
main_dim – What is measured.
by_dim – The rows.
per_dim – The condition’s dimension, or None for every game.
per_value – The condition’s value.
aggregation – How the measure is charted; its default when None.
- Returns:
True for a chart no batch should include.
- supercontest.core.stats.render_plots(group: PlotGroup, counts: dict[tuple[str, ...], list[tuple[tuple[Any, ...], int]]]) list[BarChart]
Arranges the counts for one group by key, and builds its plots.
The main dimension is bounded, and we need to show all data, even if zero (else we might have 1 bar instead of stacked bars). The “by” dimensions are not; we show whatever values the database counted. With a condition, only its value’s counts are plotted.
The plotter will default to calculate percentage-of-total from those counts, or
aggcan flatten into an average, etc.- Parameters:
group – The analysis.
counts – What
fetch_counts()returned for it (or for more groups).
- Returns:
The charts.