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

NULL_PER_VAL

The single "per" value of a chart with no condition.

Symmetry

How a dimension's two rows in one game relate.

PER_GAME

A contestant is one side of one game, so the mean of its picks is the picks per game.

AGGREGATIONS

Every aggregation, by name.

DIMENSIONS

Every dimension, by name.

MEASURES

What each scope can measure, the default first.

GROUPERS

The dimensions a chart's rows, or its condition, can be, in page order.

Counts

The database's counts, as supercontest.dbsession.queries.get_stats_counts() returns them.

GAME_COLUMN

The fact column that tells one game from another, for counting a condition's games.

Functions

batch

The charts one page shows: a chart per grouping dimension with something to say.

build_batch

One batch's charts, and how many games they count.

condition_values

Each grouping dimension's values in a season, as a condition names them.

conditions

The conditions a measure can be put under: each value that leaves a chart to show.

fetch_counts

Counts everything a set of plot groups needs, in one query.

forced

Whether a chart's values are fixed before anything is counted.

render_plots

Arranges the counts for one group by key, and builds its plots.

Classes

Aggregation

How a chart reduces what it counted in each row to the bars it draws.

PlotGroup

One "main by X" analysis: a plot per by dimension, under an optional condition.

StatsDim

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: object

How 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, the agg of 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 agg its series’ name.

name: str

What the API calls it.

percent: bool = False
spread: bool = False

It measures spread, which a mirror leaves free: a mirrored row’s mean is 0, but its values can be tight or wide. See forced().

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: object

One “main by X” analysis: a plot per by dimension, 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_dim is per_value are counted (eg coverage by team when favorite).

aggregation: Aggregation | None = None

How the measure is charted; its default when None.

by_dims: list[StatsDim]
property groupings: dict[str, tuple[str, ...]]

The fact columns counted for each by dimension, per value first and main last.

Returns:

Keyed by the by dimension’s name.

main_dim: StatsDim
per_dim: StatsDim | None = None
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: object

Our 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.

is_shared(value: int | float | str) → bool

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 game dimension, 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_dims are 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 game dimension, 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 game measure by a sign dimension, 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 agg can flatten into an average, etc.

Parameters:
  • group – The analysis.

  • counts – What fetch_counts() returned for it (or for more groups).

Returns:

The charts.