Versioning & Changelog
The MMM Framework is at version 1.5.0, published on
PyPI
(pip install mmm-framework).
From 1.0.0 on, the project follows semantic
versioning strictly and the public contracts documented on the
API Contracts page are frozen: breaking changes
only arrive in a major version.
1.0: what "stable" covers
The stability promise covers the documented Python API of the core
mmm-framework package, the model spec keys, the persisted model
format, and the REST API of mmm-framework-server
(see REST API reference). Modules marked
Experimental below may still change in minor releases — they are
flagged in the stability grid and in their docstrings. Pinning
(pip install mmm-framework==1.5.0) remains good production hygiene.
API Stability by Module
Different parts of the framework have matured at different rates. Use this as a guide for what to depend on:
Frozen at 1.0
BayesianMMMMMMResults/PredictionResults- Builders (
ModelConfigBuilder, etc.) config(Pydantic dataclasses)- Adstock, saturation, seasonality, trend transforms
MFFLoader/ data loading + model spec keysestimands(declarative, named, serializable estimand registry)serialization(MMMSerializer; persisted model format)- REST API (
mmm-framework-server) and org/tenant auth (auth/)
Active surface
reporting(charts, extractors, generator; Augur client readout)analysis(counterfactual, marginal)- React web UI — the Augur frontend (Orrery, Auspices, Sextant, Chronicle, Almanac, Constellation, Oracle, Atelier, Codex, College, Curia, Sanctum)
- Agent workspace (LLM providers: Anthropic, OpenAI, Google Gemini, Vertex AI, LM Studio)
- Measurement-loop planning engine (EIG/EVOI priorities, experiment lifecycle, design studio, experiment economics, Pareto-front optimizer, calibration)
eda+ Data Studio (pre-fit data quality: validation, EDA, outlier detection and treatment, cleaning pipelines)diagnostics(convergence gate, prior→posterior learning, Simulation-Based Calibration)validation(rolling-origin backtests, SBC + LOO-PIT interval checks)
Subject to change
mmm_extensions(NestedMMM, MultivariateMMM, CombinedMMM)continuous_learning(model-free geo response-surface learning loop)garden/ Atelier (bespoke-model authoring, compatibility suite, Model Garden)- Non-MMM model families (CFA / latent-class garden examples)
dag_model_builder- Excel config workflow
Versioning Policy
From 1.0.0 the project follows SemVer strictly:
- Patch (
1.0.x) — bug fixes, doc updates, internal refactors. Always safe to upgrade. - Minor (
1.x.0) — backward-compatible features. Modules flagged Experimental in the stability grid may still change here, with release-note callouts. - Major (
x.0.0) — breaking changes to any documented contract: the core Python API, model spec keys, persisted model formats, or REST endpoints.
The frozen surface is documented on the API Contracts page and pinned by contract-gate tests in CI (tests/test_api_contracts.py, tests/test_lean_imports.py) — a change that breaks a contract fails the build rather than shipping silently.
Release Notes
Release notes are summarised on this page — it is the public record of the framework’s release history. The history below covers each version.
Contracts first, then capability. The release opens with a safety net rather than a feature: four contract gates that pin what the framework computes today — a graph-fingerprint matrix over 41 model configurations, a real serializer round-trip, an import-layer ratchet, and a snapshot of the private surface that 19 registered garden models actually call. None of it is user-visible; everything that follows is safer because it exists.
That safety is then spent. Cross-client benchmarks pool channel
performance across tenants without any client's results being recoverable by another
— k-anonymity alone does not achieve that, since n−1 colluding
contributors can recover a held-out brand's cell mean by arithmetic, so releases are
differentially private under an ε ledger and the insider attack is run as a test.
Asymmetric event shapes let a calendar event dip before a sale and spike
during it, which one regressor column and one coefficient never could.
utils/intervals.py collapses fourteen interval helpers into
one that is honestly named — the most-called one had been returning an
equal-tailed interval under the name HDI since the beginning. And
TransformOrder makes the adstock/saturation composition an
explicit choice instead of a buried assumption.
Behaviour changes to read before upgrading: the cross-project
/portfolio-benchmark payload is now admin-only — non-admin members of
an org receive their own projects compared against the privacy-gated published vintage,
with exact cross-brand distributions and percentile ranks removed. Everything else is
additive: every graph-fingerprint golden that existed before this release still holds,
including through the transform-order and event-shape work, and
compute_hdi_bounds keeps its exact signature and its numbers.
Finance-grade planning. The measurement loop now closes at the CFO's desk:
a KPI valuation that is never silently one dollar (kpi_to_dollars resolves a
declared valuation with provenance, and fund-to-breakeven refuses without one), a
forward forecast under a plan whose caveats lead the headline, an append-only hash-chained
plan of record that reproduces from provenance to 1e-9, per-channel
payback horizons with truncation and prior-domination disclosures,
promo-depth optimization with per-arm cost bases (flag promos and price
recommendations refuse), and a variance-to-plan bridge that sums to
actual − committed exactly and refuses the refit "effectiveness" split.
Also: confounding-sensitivity tipping points on the decision scale, a realized-KPI actuals
store, seven new technical-docs specs, a capability-reachability CI gate, and
make api-sync (whose auth check caught a real unguarded route).
Behaviour changes to read before upgrading: free-mode budget allocation
raises UnresolvedValueError without a valuation; forecasts refuse when a
model has controls and no future values were supplied (there is no defensible default,
so code that silently assumed one now gets an explicit error); decomposition shares are
computed against the signed total, not a sum of magnitudes (previously rendered "% of
total" figures meant something else); the CFO one-pager's baseline is now the model's
fitted non-marketing outcome with the residual named, reconciling as
base + marketing + unexplained = observed; multiplicative
models now refuse sample_channel_contributions() instead of returning
log-scale numbers as contributions; and the report and slide deck resolve one shared
break-even, changing tier recommendations for projects with a saved margin. Full detail on
Finance-Grade Planning.
A security release. Generated HTML reports did not escape </ in
the Plotly chart JSON embedded in inline <script> blocks, so a
string in a chart payload containing </script> closed the block
early and the browser parsed the rest as HTML. It affects the classic and augur
report shells; the interactive report already carried the same guard.
Channel names could not reach it — PyMC rejects / in
random-variable names, so a hostile channel name fails at fit time. But control
and geography names are coords, not variable names, and they flow into chart
traces, hover templates and axis titles, which makes the vector reachable from
any untrusted modeled dataset. The fix is tracked as GHSA-7q6v-xpwm-4937;
report security issues to m.reda94@gmail.com.
Cut from the 1.3.2 tag with only this fix, so 1.3.x users can take it without adopting in-progress 1.4 work. The premature tag also corrupted the surrounding chart payloads, so this repairs rendering too — charts drawn went from 26/28 to 28/28 in the reproduction. Found by pressure-testing the report generators against a nine-model matrix carrying deliberately hostile names.
Three names 1.3.1’s release notes described as public were not actually
exported. rebuild_like(), audit_forward_pass() and
audit_refit() were reachable only from
mmm_framework.validation.backtest, not from
mmm_framework.validation as documented. The claim is now true, and
mmm_framework.validation joins the frozen public surface, so
removing or renaming any of them is a declared breaking change.
Found by importing the published wheel and checking it against its own release notes rather than against the working tree — nothing pinned the export set, and the docs snippet gate reads code fences, so a prose claim about the API surface had nothing checking it.
A correctness release for the out-of-time forecast path, and one bug of the kind this project treats as most serious: a code path that reads as complete and silently does nothing for one configuration.
- The product level offset is now replayed, and every term the
forward pass genuinely cannot reproduce raises the new
ForecastUnsupportedError— naming the feature, at construction time, so a caller cannot hold a forecaster whose output it is not allowed to trust. This turns some previously “working” backtests into hard refusals; the numbers they produced were wrong. - Backtesting a custom or garden model no longer downcasts it to a plain additive MMM and reports that model’s accuracy under the custom model’s name.
- On a geo × product panel, a spline, GP or piecewise trend was
replayed at the wrong time index —
trend_componentis registered per-observation but was indexed by period, stretching the trend by the number of panel cells. Measured at up to 26% of KPI level. National panels were unaffected, which is why it went unseen. - Student-t fits drew Gaussian observation noise, so the interval coverage the backtest graded was not the model’s. The rolling-window cross-validation also forecast seasonality out of phase.
PosteriorForecaster.trend_extrapolationnow states how the trend is continued past the training window. Spline, GP and piecewise trends hold the last fitted level flat, so those forecast intervals do not widen with horizon — previously an unstated assumption.
A second estimation paradigm. With each channel's carryover and
saturation held fixed, this model is linear in everything else — so it can be
solved in closed form instead of sampled. frequentist_ridge and
frequentist_cvxpy, enum values since early in the project, now select
a real estimator: transforms chosen by rolling-origin out-of-sample search, a
penalized linear solve, and confidence intervals from a moving-block bootstrap.
frequentist_cvxpy adds hard linear constraints — a sign
restriction, an ordering, a contribution total that must match a booked number
— via the optional [frequentist] extra.
- Honest labelling throughout. A frequentist fit reports
convergedasNonerather thanTrue— R-hat and effective sample size describe an MCMC sampler and there is no chain — and every interval it produces is named a confidence interval. Posterior-predictive checks, Bayesian p-values, prior-predictive checks and prior-to-posterior contraction are gated off with a stated reason rather than computed anyway. - Block bootstrap, because residuals are autocorrelated. Block length is estimated from the residual autocorrelation rather than assumed. At ρ = 0.6, an iid bootstrap covers 79.6% of nominal 90% intervals and the block version 90.4%; at ρ = 0 the two agree, so nothing pays a width penalty for a dependence that is not there.
- The cheap interval is labelled, not silently shipped.
Conditioning every replicate on one transform search omits selection
uncertainty — measured at 2.1×–7.6× too narrow on one
dataset.
refit_search=Trueis the publishable interval. - New:
run_recovery_coverage(refit=...)grades any estimator, not only PyMC ones;mmm_framework.diagnostics.provenanceis the single source of estimation-paradigm vocabulary. Absence of the provenance field reads as Bayesian, so no existing fit changes. - Notebook:
nbs/demos/frequentist_vs_bayesian.ipynb— the paradigm axis, alongside the existing method and backend comparisons.
Knobs that read as configuration and were no-ops. Sampler selection worked in one
place and silently failed in four; fixing it required new public API — a third
NUTS backend that was already a declared dependency but that no code path could
reach — so this is a minor rather than a patch bump. If you set
target_accept, selected a frequentist inference method, or read a
multi-outcome cross-effect table, your results change.
frequentist_ridge and
frequentist_cvxpy now raise instead of silently
fitting a full Bayesian posterior. Neither has ever been implemented —
fit() dispatches on the fit method, not the inference method — so
selecting one asked for a fast frequentist point estimate and got MCMC with no
indication the request had been ignored. Use fit(method="map") for the
fast penalized estimate, or an explicit Bayesian method to keep exactly what you
were already getting. The enum values are retained (removing them would break the
frozen-enum contract); they become live, not removed, when the frequentist
estimation path lands. Also raises the nutpie floor to >=0.16.10 —
PyMC 6 raises ImportError below it, so the old pin shipped a sampler
that could not have started.
ModelConfig.target_acceptis now honored byfit()— the fallback went straight to a literal0.9, so.with_target_accept(0.95)was a silent no-op: the first knob the sampling-failure playbook tells you to reach for, doing nothing about divergences because it never reachedpm.sample. Precedence is now explicit argument → config →0.9; an untouched config still samples byte-identically to 1.1.0.fit(nuts_sampler=...)no longer raises on the core model — the keyword fell into**kwargsand collided with the argument already passed topm.sample, so the same line worked on an extension model and failed on a plain one with aTypeErrornamingpm.samplerather than anything the caller wrote.- Cross-effect summaries no longer report structurally-zero outcome pairs as estimated — the report helper probed for a method name neither multi-outcome model spells that way, so every report fell through to a manual branch that walks the whole off-diagonal matrix. Pairs you never declared appeared alongside the real ones. If you have a multi-outcome report with undeclared cross-effect rows, that is this bug; re-run for the declared set.
- A third NUTS backend:
InferenceMethod.BAYESIAN_NUTPIEplus.bayesian_nutpie()on both builders, alongside.bayesian_pymc()/.bayesian_numpyro(). The three sample the same graph — only the NUTS implementation differs — and a real MMM fit through nutpie agrees with the pymc backend.ModelConfig.nuts_sampleris the single resolver from inference method to the sampler string.
Extension models (Nested / MV / Combined / Structural) keep their
"pymc" default deliberately: their bespoke graphs are not all
JAX-traceable, so inheriting a numpyro config would break fits rather than speed
them up.
Two methodological fixes in validation. Both corrected numbers that read
as more trustworthy than they were, so both change output you may
have quoted. A minor rather than a patch bump: the fixes come with new public API,
and one changes a default so that a re-run returns a different number than 1.0.0 did.
- Spec-curve model averaging no longer weights a causal estimand by predictive skill. LOO-stacking maximizes expected predictive utility — "which mixture forecasts held-out
ybest?" — while a spec curve averages a causal estimand. The two objectives come apart exactly where MMM specs differ, because two specs can predict the KPI equally well while splitting that same fitted mean very differently between media and baseline; a spec that overfits the confounder block often predicts better while being less trustworthy for the causal contrast. The default is now equal weights over the pre-registered set — pre-registration already asserted every variant is defensible, so there is no post-hoc predictive ground to promote one.run_spec_curve(..., weighting="stacking")opts back in, with a warning; the stacking weights are still computed and reported as a diagnostic (SpecCurveResult.predictive_weights), because divergence from uniform is worth seeing and not worth acting on. - The unobserved-confounding robustness value is no longer inflated by tight priors. The robustness value is strictly increasing in
|t|, andt = posterior_mean / posterior_sd— so tightening a prior shrank the posterior sd and raised reported robustness with no new evidence. The most prior-dominated channel could report the most robust value, and values were not comparable across channels with differently tight priors. Prior contraction is now computed per channel; a channel below the threshold renders as "Not assessable (prior-driven)" rather than a green "Robust", and "could not check" is reported distinctly from "checked and passed".
If you have a stored spec-curve result or a report produced by 1.0.0, its
model-averaged ROI was stacking-weighted. Re-run for the equal-weight number, or
read predictive_weights to see how far the two diverge on your set.
The first stable release: the package was split into a lean modeling core and optional application layers, the public contracts were audited and frozen, and the project now follows strict semantic versioning.
pip install mmm-framework is now business logic only —
no FastAPI, no LLM stack. The LangGraph agent stack moved to the
mmm-framework[agents] extra. The FastAPI app moved out of the
package into the separate mmm-framework-server workspace package
(server/): the uvicorn target is now
mmm_framework_server.main:app, and the former
mmm_framework.api service modules (sessions store, run history,
pacing, scorecards, …) are now mmm_framework.platform.
Deployments must sync the server package explicitly
(uv sync --frozen --no-dev --package mmm-framework-server).
A pre-existing dev sessions DB at the old api/sessions.db location
keeps working via a legacy-path fallback.
- Lean core: a notebook user gets build/fit/analyze/report/validate from a plain
pip installwith no web or LLM dependencies; a lean-import gate test (tests/test_lean_imports.py) pins the invariant in CI.cryptographyis now a declared dependency (it was previously an accidental transitive). - Contract freeze: full REST API reference generated from the live OpenAPI schema (rest-api.html), a consolidated API Contracts page (Python surface, model spec keys, persisted formats, sessions-store schema), and contract-gate tests that fail CI on any breaking drift.
- Architecture documentation: package/dependency structure, model-building data flow, app request flow, and the measurement loop as flow diagrams (architecture.html), plus a Sphinx API reference covering every public subpackage.
- Hardened images: the sandboxed session-kernel image now ships the lean core closure only (no LLM/web stack), a repo-level
.dockerignorekeeps runtime state out of container builds, and the kernel-side plot capture moved to a dependency-light module (agents/figures.py).
Everything shipped since the 0.1.0 PyPI release (June 24), headlined by a core-stack upgrade. Full test suite green at release: 3,587 fast + 164 slow tests.
DataTrees; models saved under 0.1.0
(PyMC 5) reload cleanly. Pathfinder variational inference now works out of the box
(pymc-extras is a declared dependency), and the netcdf engines
(h5netcdf/h5py) trace persistence needs are declared explicitly.
If you extend the framework with raw arviz calls, route them through
mmm_framework.utils.arviz_compat — several arviz 1.x changes fail
silently (notably az.summary returning formatted strings).
- Continuous sequential learning (
continuous_learning, experimental): a model-free geo response-surface engine that learns spend→outcome directly from designed experiments — central-composite designs, Thompson sampling, funding-line and ENBS stopping rules, Laplace knowledge-gradient design scoring, information discounting and P-spline shrinkage for drifting media behaviour, and the Sextant UI to run the loop. - Approximate fits from the Oracle: MAP / ADVI / full-rank ADVI / Pathfinder selectable end-to-end (spec registry, agent tool, spec-editor UI), for seconds-fast model checking before paying for NUTS. Loading a saved model now restores the exact settings it was fit with.
- Impression-/click-measured media: per-channel measurement descriptors drive honest ROI vs efficiency-per-unit metrics across reports, estimands, and the UI.
- Simulation-Based Calibration in
diagnostics, plus SBC + LOO-PIT machine-verification of interval calibration invalidation; out-of-time backtests extended to spline/GP/piecewise trends and geo panels. - Augur client deliverables: the Media Performance Readout is the default client report, with posterior-predictive fit sections, AI insights, slide-deck deep-dives, and marginal-ROI break-even spend zones; the Almanac budget planner returned with an allocation section.
- Data Studio: upload → interactive EDA → replayable cleaning pipeline → commit as the working dataset, with no LLM round-trip; also exposed as agent tools.
- Experiments: structural-identification design panel, off-panel calibration (experiments from windows the model was not fit on), one-click calibration handoff, and a CombinedMMM starter template in the DAG planner.
- Estimands on the Performance page (grouped by estimand × KPI across fitted models), per-geo channel effectiveness (
vary_media_by_geo), call-time estimand selection, and full spec-path validation on every agent setting write. - Model validation surface: a first-class validation battery (PPC, residuals, channel diagnostics, causal refutation, cross-validation) as agent tools, a one-click Validation tab, and a persona review panel.
- Platform: real IV/2SLS and front-door estimators, S3 object store, content-addressed dataset lineage, run-comparison deltas, SaaS security and governance foundation, CI with lint + fast tests + coverage + dependency audit, and a React test harness.
- Docs: site-wide modern refresh (dark mode, Cmd-K search, quizzes, deep-dives), mobile-safe rendering, and analyst-focused continuous-learning guides with recorded animations.
- Research blog: a new Modern Measurement Research section — 15 essays on causal inference, geo experiments, Bayesian MMM, and experimental design, written from the primary literature.
The first published release of the MMM Framework. Includes:
ModelConfig.use_parametric_adstock now defaults to
True — new fits estimate a continuous in-graph adstock
kernel per channel (geometric by default; delayed/Weibull per
MediaChannelConfig.adstock) instead of the legacy fixed-alpha blend.
Motivation: the pressure-testing series measured the legacy blend at ~28% attribution
error vs ~7% parametric on carryover-sensitive worlds. To reproduce older fits, set
use_parametric_adstock=False (or
ModelConfigBuilder().with_legacy_blend_adstock()); models serialized
before the change keep their original behavior on load. Validator cross-validation
now routes out-of-sample prediction through the backtest forecaster, which supports
both adstock paths and fails loudly on panel models instead of silently
mispredicting. All notebooks and documentation were re-baked/updated under the new
default.
- Standalone PyMC-based Bayesian MMM core (
BayesianMMM) with hierarchical specs, configurable adstock/saturation, and reproducible model serialization (plus optional PyMC-Marketing interop for reporting) - Fluent builders for model, channel, and variable configuration
- MFF (Master Flat File) data loader with validation
- Counterfactual and marginal analysis utilities
- HTML report generator with Plotly charts and design-token theming
- Extension models (NestedMMM, MultivariateMMM, CombinedMMM) — experimental
- FastAPI service + ARQ worker for async job execution
- React (TypeScript) web UI as the modern frontend — the Augur application (Beta); Streamlit UI retained as a legacy surface
- AI agent workspace: an analyst-assistant with tools for data validation and EDA, model configuration and fitting, ROI and decomposition analysis, experiment planning, sandboxed Python execution, knowledge-base search, and branded client reports — with LLM providers Anthropic, OpenAI, Google (Gemini), Vertex AI (Anthropic & Gemini via ADC), and local LM Studio
- Measurement-loop planning engine: EIG/EVOI experiment priorities, an experiment lifecycle (draft → pre-registered → running → completed → calibrated), and calibration of experiment readouts into the next model fit
- EDA / data-quality module for pre-fit validation, exploratory checks, and outlier detection and treatment
- Sphinx-generated API reference + this static documentation site
Development highlights (within the 0.1.0 line)
The largest additions since the initial feature set, newest first:
- Continuous sequential learning (
continuous_learning, experimental) — a model-free geo response-surface learning loop that estimates channel response and synergies directly from designed geo experiments, with central-composite designs, Thompson-sampling allocation, a marginal-ROAS funding line, expected-net-benefit stopping, and Laplace knowledge-gradient design selection. Wired end to end: agent tools,/learning-programsAPI, and the Sextant page in the web UI. Supports NegBinomial likelihoods for count KPIs, an optional national time effect, and stratified geo assignment. See the analyst guide and the math companion. - Experiment-registry hardening — lifecycle state-machine enforcement (illegal transitions are rejected), a full audit trail on every experiment write, and additive calibration staging so applying one experiment no longer silently un-stages others.
- Sub-channel (breakout) calibration — a share-based likelihood for calibrating sub-channel splits inside a parent channel, plus a partial-pooled breakout-weighted MMM in the Model Garden.
- Impression- and click-measured media — per-channel measurement descriptors (spend / impressions / clicks with optional spend column, CPM, or CPC) so ROI, marginal ROAS, and report labels are computed on the right cost basis, or degrade honestly to efficiency-per-unit metrics.
- Data Studio — upload a raw file, explore it with interactive EDA (distributions, correlation, missingness, outliers), build a replayable cleaning pipeline, and commit the result as the session’s working dataset without a chat round-trip.
- Simulation-Based Calibration (SBC) — a Talts-style posterior-calibration check with rank-histogram and ECDF-difference diagnostics, available as an agent tool and a one-click validation job; joins SBC + LOO-PIT interval checks in the validation module.
- Almanac budget planner — persistent budget plans with optimal allocation, geo/DMA splits, a forward flighting calendar, CSV export, and an allocation section in client reports.
- Augur client report — the default client deliverable is now an editorial, evidence-coded “Media Performance Readout” with posterior-predictive fit sections and AI-drafted planner insights; the classic technical readout remains available. MMM reports also gained estimand credible-interval tables and posterior-predictive goodness-of-fit sections.
- Model validation surface — first-class validation/verification/plotting agent tools, a one-click Validation tab in the Oracle workspace, and a persona review panel (statistician / planner / CMO) for adversarial readouts.
- Per-geo channel effectiveness (
vary_media_by_geo) — opt-in partial-pooled per-geo channel betas, plus geo-panel and spline/GP-trend out-of-time backtests. - Estimands on the Performance page — fitted models grouped by estimand × KPI with fit-time persistence, alongside the existing trajectory, agreement, and model-health views.
- SaaS foundations — a dependency-free org/tenant auth layer (built-in JWT, IdP-ready), S3-capable object storage, content-addressed dataset lineage, CI (lint, fast tests, coverage, dependency audit, mypy baseline), and a convergence gate on core and extended fits.
- Model Garden & Atelier (experimental) — author bespoke Bayesian models in an in-app studio (editor, docs, Jupyter-like notebook, modeling copilot), prove them against a nine-tier compatibility suite on synthetic ground truth, and publish immutable versions any analyst can fit through the agent. The contract is family-aware: per-model config schemas, pluggable likelihoods, and non-MMM families (confirmatory factor analysis, latent-class analysis) ride the same fit → estimand → report rails.
- Declarative estimands — a named, serializable estimand registry (contribution ROI, counterfactual ROI, marginal ROAS, contribution, latent quantities) evaluated post-hoc or in-graph, unifying the framework’s previously scattered ROI notions while keeping every legacy number bit-stable.
- Experiment design engine — the design studio gained model-anchored economics (incremental-ROAS expectations, opportunity cost, powered/underpowered verdicts), A/A & A/B methodology simulation with an empirical false-positive-rate check, a Pareto-front optimizer over MDE × power × cost × duration, off-panel calibration for experiments run outside the training window, and multi-level flighting designs that identify saturation and adstock parameters.
- Parametric adstock by default — see the behavioral-change note above.
Feedback
Bug reports and feature requests are welcome by email at mattreda@mattreda.pro; report security issues to m.reda94@gmail.com.
For questions about the methodology, the FAQ is a good starting point; for the underlying math, the Bayesian Workflow and Causal Inference guides cover most of the foundations.