Skip to main content

Post Analysis Toolkit

post_analysis_toolkit - локальная библиотека для пост-анализа промо, rollout-ов и других квази-экспериментальных задач, где нужен воспроизводимый causal / quasi-causal пайплайн.

Пакет закрывает типовой цикл:

  1. подготовили панель или временной ряд;
  2. проверили предпосылки метода;
  3. посчитали эффект;
  4. проверили устойчивость результата;
  5. получили табличные результаты и готовые графики.

Документация в этом файле ориентирована на пользователя пакета.

Что умеет пакет

  • классический Difference-in-Differences
  • регрессионный DiD
  • DiD с cluster-robust SE по unit по умолчанию или HC1 при явном cluster_col=None
  • DiD с TWFE
  • DiD с лог-трансформацией outcome
  • DiD с относительной индексацией outcome
  • Synthetic Control
  • Synthetic DiD (SDiD) через diff-diff
  • Causal Impact
  • Interrupted Time Series
  • Triple Difference
  • Regression Discontinuity Design
  • Propensity Score Matching
  • Doubly Robust / AIPW
  • Event Study с эффектами по временным бинам
  • проверки предпосылок по каждому методу
  • базовые проверки устойчивости
  • встроенные matplotlib-графики

Стандартные ошибки и кластеризация

Для fit_did_regression, fit_event_study и fit_triple_difference по умолчанию используется односторонняя cluster-robust ошибка. При cluster_col="auto" (значение по умолчанию) пакет сначала использует unit_col, а затем ищет стандартную колонку unit, unit_id, pizzeria, city или cluster. Чтобы явно отключить кластеризацию и использовать HC1, передайте cluster_col=None. Если в данных нет подходящей колонки, auto безопасно откатывается к HC1; фактически выбранный режим смотрите в result.metadata["stderr_type"].

Односторонняя cluster covariance — CR1-style: statsmodels применяет finite-sample correction, а p-value по умолчанию рассчитывается с нормальной аппроксимацией (use_t=False). При небольшом числе кластеров можно выбрать se_type="cr2" или se_type="cr3": это leverage-adjusted варианты кластерной covariance без внешних зависимостей.

Для регрессионных методов можно явно выбрать se_type: "nonrobust" (обычная OLS SE без коррекции), "hc1", "cluster" (один уровень), "cr2", "cr3", "wild_cluster" или "two_way_cluster" (Cameron–Gelbach–Miller, unit × time). "auto" сохраняет прежнее поведение: кластеризация по cluster_col, если она задана/найдена, иначе HC1. В two-way режиме второй уровень по умолчанию берётся из time_col; его можно переопределить time_cluster_col. wild_cluster использует Rademacher wild cluster bootstrap для эмпирической covariance; число повторов задаётся wild_bootstrap_reps (по умолчанию 999), а seed — через random_state. Это bootstrap-оценка SE, а не отдельный null-imposed bootstrap-тест p-value.

fit_its использует HAC/Newey–West (hac_lags=7 по умолчанию), но также поддерживает se_type="hc1", "cluster", "cr2", "cr3", "wild_cluster" и "two_way_cluster" для панельных данных. fit_its/check_its_assumptions принимают cluster_col и time_cluster_col, а run_its_robustness по умолчанию перебирает hac_lags=(0, 3, 7, 14). Для RDD two-way режим требует явного time_cluster_col; density-регрессия в проверке предпосылок остаётся HC1.

Панельный ITS передаётся в long-формате с одной строкой на unit × date. Передайте se_type="cluster" и cluster_col с идентификатором юнита:

panel_its = fit_its(
    panel_df,
    outcome_col="orders_per_unit_day",
    time_col="sale_date",
    intervention_time="2026-06-14",
    se_type="cluster",
    cluster_col="pizzeria_id",
)

Для такой панели holdout и rolling-метрики считаются по календарным датам (среднее по юнитам), а график эффекта также показывает средний ряд по датам. PSM и Doubly Robust используют IID-оценку по score/differences. Synthetic Control не имеет модельной SE/p-value и использует placebo-диагностики, SDiD — variance_method="placebo", "bootstrap" или "jackknife" из diff-diff, а Causal Impact — байесовские posterior-интервалы. При небольшом числе кластеров, особенно при одном treated-кластере, кластерные p-value и FPR могут быть нестабильны; их нужно сопоставлять с placebo и sensitivity.

В ITS hac_lags — максимальный лаг автокорреляции для HAC/Newey–West. Для ежедневных данных 7 — разумная стартовая настройка при недельной сезонности, но это не универсальное значение. На практике сравнивайте несколько соседних значений (например, 0, 3, 7, 14) и проверяйте, меняется ли вывод об эффекте; run_its_robustness делает такой перебор автоматически.

Для ITS se_type="auto" означает: если передан или автоматически найден cluster_col, использовать one-way cluster; если кластер не задан — HAC. При этом значение по умолчанию у fit_its и check_its_assumptions — se_type="hac", поэтому для автоматического выбора нужно указать se_type="auto" явно.

MDE и чувствительность

Для всех MDE по умолчанию используются alpha=0.05, power=0.80 и двусторонний тест. Если метрика и дизайн допускают аналитическую оценку, базовый ориентир — t-test/формула через корректную стандартную ошибку.

MDE относится к конкретному estimand и спецификации. В рабочем результате нужно отдельно показывать абсолютный MDE в единицах метрики, относительный MDE относительно явно указанного baseline и cumulative MDE за весь post-period.

Режим mde_mode="auto" используется по умолчанию: если у метода есть корректная стандартная ошибка, выбирается аналитический расчет, иначе — параметрическая симуляция. mde_mode="parametric" принудительно включает быструю параметрическую симуляцию. mde_mode="resampling" включает полноценные симуляции с генерацией данных и повторным запуском оценивателя. Быстрый proxy доступен только явно как mde_mode="proxy". Для симуляций число повторов будет задаваться параметром n_simulations: 500 для отладки, 2000 для рабочего расчета и 5000+ для финального отчета. Вместе с ним нужно сохранять random_state и power curve.

Параметрическая симуляция: для каждого кандидатного эффекта пакет генерирует оценки по схеме delta + Normal(0, uncertainty_scale). Для регрессионных и observational-методов uncertainty_scale — SE. Для Event Study это SE конкретного коэффициента конкретного временного бина, поэтому MDE считается отдельно для каждого бина. Для Synthetic Control и Causal Impact используется pre-fit или validation RMSE, нормированный на размер post-period; для ITS при доступной covariance matrix используется delta-method SE среднего post-period gap, а validation RMSE остаётся fallback.

В режиме resampling пакет создает псевдоданные: берет fitted/counterfactual значения без эффекта, добавляет ресэмплированные центрированные остатки и кандидатный эффект, а затем повторно запускает весь поддержанный оцениватель. Для Synthetic Control заново подбираются веса, для PSM заново строится matching, для Causal Impact заново обучается state-space модель, для ITS заново оценивается регрессия, а для SDiD заново подбираются unit/time-веса. Такой режим заметно медленнее и требует явно задать разумное n_simulations и, при необходимости, mde_effect_grid.

Power curve можно посмотреть в result.details["mde_power_curve"].

После вызова любой fit_* MDE читается так:

result.summary[["mde_absolute", "mde_relative", "mde_cumulative", "mde_mode_resolved"]]
result.details["mde_power_curve"]

Пример полноценного resampling-MDE для поддержанного метода:

result = fit_synthetic_control(
    panel_df,
    outcome_col="outcome",
    unit_col="unit",
    time_col="sale_date",
    treated_unit="treated_city",
    intervention_time="2026-02-01",
    mde_mode="resampling",
    n_simulations=200,
    mde_effect_grid=[0, 2, 4, 6],
    random_state=42,
)
result.summary[["mde_mode_resolved", "mde_absolute"]]

В этой версии для каждого кандидата эффекта создаются псевдоданные и заново вызывается Synthetic Control. Поэтому resampling значительно медленнее параметрического режима. Для нестандартного оценивателя можно передать mde_simulator, который возвращает массив повторных оценок эффекта.

Параметры mde_mode, alpha, power, alternative, n_simulations, random_state, mde_effect_grid и mde_simulator добавлены в функции оценки эффекта. Вызов с mde_mode="proxy" дополнительно заполняет proxy_mde_*; при analytic-, parametric- или resampling-режиме эти legacy-поля остаются пустыми.

8. PSM

Используй, когда:

  • treatment бинарный
  • есть наблюдаемые ковариаты, через которые можно объяснять селекцию в treatment
  • хочется построить matched control на основе propensity score

В текущей версии пакет реализует ATT через nearest-neighbor matching по propensity score.

9. Doubly Robust

Используй, когда:

  • treatment бинарный
  • есть набор ковариат
  • хочется оценку, устойчивую к ошибке в одной из двух моделей: propensity model или outcome model

В текущей версии это AIPW / doubly robust ATE.

Что пакет не делает

  • не строит данные из сырых таблиц
  • не проверяет бизнес-смысл метрик за тебя
  • не знает, был ли contamination доноров
  • не умеет автоматически находить “правильный” метод
  • не снимает необходимость ручной интерпретации

То есть пакет отвечает за оценивание и diagnostics, а не за бизнес-контекст и data engineering.

Быстрый старт

Установка

В ноутбуке или Python-скрипте:

%pip install post-analysis-toolkit

Чтобы библиотека была доступна во всех ноутбуках персонального Databricks-кластера, один раз добавьте PyPI-библиотеку post-analysis-toolkit в разделе Libraries его настроек.

Базовый импорт

from post_analysis_toolkit import (
    check_did_assumptions,
    check_doubly_robust_assumptions,
    check_synthetic_did_assumptions,
    fit_did_regression,
    fit_event_study,
    fit_doubly_robust,
    fit_synthetic_control,
    fit_synthetic_did,
    fit_causal_impact,
    fit_its,
    fit_psm,
    fit_rdd,
    fit_triple_difference,
    run_synthetic_did_robustness,
)

Как начать, если видишь пакет впервые

Практически всегда работа с пакетом выглядит так:

  1. Подготовь данные в одном из поддерживаемых форматов: панель unit x time, один временной ряд или observational dataset.
  2. Выбери метод под свой дизайн.
  3. Сначала запусти check_*_assumptions(...).
  4. Потом запусти fit_* (...).
  5. Если метод выглядит применимым, запусти run_*_robustness(...).
  6. Посмотри summary, details и figures.

Минимальный шаблон:

diag = check_did_assumptions(...)
display(diag.checks)

result = fit_did_regression(...)
display(result.summary)

robust = run_did_robustness(...)
display(robust.summary)

Особенно важно:

  • подробное описание параметров теперь находится прямо внутри описания каждой функции, а не в отдельном общем словаре;
  • если видишь аргумент вида *_options, это почти всегда значит “перебор нескольких спецификаций”, а не “одна настройка модели”.

Как читать result-объекты

Почти все функции возвращают один из двух dataclass-объектов:

  • DiagnosticResult: результат проверки предпосылок
  • MethodResult: результат расчета эффекта или robustness-check

Обычно внутри есть:

  • summary: компактная итоговая таблица
  • details: более подробные таблицы, например коэффициенты, веса, placebo-результаты
  • figures: готовые matplotlib-графики
  • metadata: технические объекты, конфиги, fit-результаты библиотек

Базовые структуры данных

Panel data

Для DiD, Synthetic Control, Causal Impact ожидается длинная панель, обычно такого вида:

import pandas as pd

panel_df = pd.DataFrame(
    {
        "sale_date": [...],
        "unit": [...],          # город / пиццерия / страна / магазин
        "metric": [...],        # outcome
        "treated_flag": [...],  # 1 для treated, 0 для control
        "post_flag": [...],     # 1 после старта промо, 0 до
    }
)

Минимальные требования:

  • time_col должен парситься в дату
  • outcome_col должен быть числовым
  • treated_col и post_col должны кодироваться как 0/1
  • для Synthetic Control и Causal Impact должен существовать unit_col

Time series data

Для ITS нужен один временной ряд:

ts_df = pd.DataFrame(
    {
        "sale_date": [...],
        "metric": [...],
    }
)

Дополнительно можно передавать внешние фичи:

ts_df["weather_temp"] = ...
ts_df["holiday_flag"] = ...

Observational data

Для PSM и Doubly Robust нужен наблюдательный датасет с бинарным treatment:

obs_df = pd.DataFrame(
    {
        "user_id": [...],
        "treatment_flag": [...],   # 0/1
        "outcome": [...],
        "x1": [...],
        "x2": [...],
        "x3": [...],
    }
)

Минимальные требования:

  • treatment_col бинарный 0/1
  • outcome_col числовой
  • covariates должны быть числовыми или заранее закодированными

RDD data

Для RDD нужен срез наблюдений с running variable:

rdd_df = pd.DataFrame(
    {
        "running": [...],   # расстояние до порога или исходная шкала
        "outcome": [...],
        "x1": [...],        # optional covariates
    }
)

Минимальные требования:

  • running_col числовой
  • известен cutoff
  • по обе стороны cutoff есть наблюдения

Triple Difference data

Для Triple Difference нужна панель с третьим бинарным измерением:

ddd_df = pd.DataFrame(
    {
        "sale_date": [...],
        "unit": [...],
        "treated_flag": [...],
        "post_flag": [...],
        "subgroup_flag": [...],  # 1 = affected subgroup, 0 = internal control subgroup
        "outcome": [...],
    }
)

Полностью готовые минимальные примеры

Ниже четыре примера, которые можно буквально скопировать в ноутбук и запустить как первый smoke-test пакета.

1. End-to-end пример: DiD

import pandas as pd

from post_analysis_toolkit import (
    check_did_assumptions,
    fit_did_regression,
    run_did_robustness,
)

did_df = pd.DataFrame(
    {
        "sale_date": pd.date_range("2026-02-01", periods=12, freq="D").tolist() * 2,
        "unit": ["Batumi"] * 12 + ["Tbilisi"] * 12,
        "metric": [
            10, 11, 10, 12, 11, 10, 15, 16, 15, 17, 16, 15,
            9, 10, 9, 11, 10, 9, 10, 11, 10, 12, 11, 10,
        ],
        "treated_flag": [1] * 12 + [0] * 12,
    }
)
did_df["post_flag"] = (did_df["sale_date"] >= pd.Timestamp("2026-02-07")).astype(int)

did_diag = check_did_assumptions(
    did_df,
    outcome_col="metric",
    time_col="sale_date",
    treated_col="treated_flag",
    post_col="post_flag",
    intervention_time="2026-02-07",
    unit_col="unit",
    cluster_col="unit",
    twfe=True,
)
print(did_diag.checks)

# Можно сравнить несколько эмпирических FPR-процедур одновременно.
did_diag = check_did_assumptions(
    did_df,
    outcome_col="metric",
    time_col="sale_date",
    treated_col="treated_flag",
    post_col="post_flag",
    intervention_time="2026-02-07",
    unit_col="unit",
    cluster_col="unit",
    twfe=True,
    fpr_method=("time_placebo", "block_bootstrap", "placebo_units"),
    fpr_placebo_count=200,
    fpr_block_length=7,
)
print(did_diag.details["fpr_results"])

# Важно: FPR-процедуры должны получать ту же спецификацию, что и оценка
# эффекта (например, twfe=True, одинаковые covariates/log_transform/relative).

did_result = fit_did_regression(
    did_df,
    outcome_col="metric",
    time_col="sale_date",
    treated_col="treated_flag",
    post_col="post_flag",
    unit_col="unit",
    cluster_col="unit",
    twfe=True,
)
print(did_result.summary)

did_robust = run_did_robustness(
    did_df,
    outcome_col="metric",
    time_col="sale_date",
    treated_col="treated_flag",
    post_col="post_flag",
    intervention_time="2026-02-07",
    unit_col="unit",
    cluster_col="unit",
)
print(did_robust.summary)

2. End-to-end пример: Synthetic Control

import pandas as pd

from post_analysis_toolkit import (
    check_synthetic_control_assumptions,
    fit_synthetic_control,
    run_synthetic_control_robustness,
)

dates = pd.date_range("2026-02-01", periods=14, freq="D")

sc_df = pd.DataFrame(
    {
        "sale_date": list(dates) * 3,
        "unit": ["Batumi"] * 14 + ["Tbilisi"] * 14 + ["Kutaisi"] * 14,
        "metric": (
            [100, 101, 99, 100, 102, 101, 100, 110, 111, 109, 112, 111, 110, 109]
            + [98, 99, 97, 98, 100, 99, 98, 99, 100, 98, 101, 100, 99, 98]
            + [102, 103, 101, 102, 104, 103, 102, 103, 104, 102, 105, 104, 103, 102]
        ),
    }
)

sc_diag = check_synthetic_control_assumptions(
    sc_df,
    outcome_col="metric",
    unit_col="unit",
    time_col="sale_date",
    treated_unit="Batumi",
    intervention_time="2026-02-08",
    fpr_method=("time_placebo", "block_bootstrap", "placebo_units"),
    fpr_placebo_count=20,
    fpr_block_length=7,
)
print(sc_diag.checks)
print(sc_diag.details["fpr_results"])

sc_result = fit_synthetic_control(
    sc_df,
    outcome_col="metric",
    unit_col="unit",
    time_col="sale_date",
    treated_unit="Batumi",
    intervention_time="2026-02-08",
)
print(sc_result.summary)

sc_robust = run_synthetic_control_robustness(
    sc_df,
    outcome_col="metric",
    unit_col="unit",
    time_col="sale_date",
    treated_unit="Batumi",
    intervention_time="2026-02-08",
)
print(sc_robust.summary)

3. End-to-end пример: ITS

import pandas as pd

from post_analysis_toolkit import (
    check_its_assumptions,
    fit_its,
    run_its_robustness,
)

its_df = pd.DataFrame(
    {
        "sale_date": pd.date_range("2026-01-01", periods=20, freq="D"),
        "metric": [
            100, 102, 101, 103, 102, 101, 100, 102, 101, 103,
            108, 110, 109, 111, 110, 109, 108, 110, 109, 111,
        ],
    }
)

its_diag = check_its_assumptions(
    its_df,
    outcome_col="metric",
    time_col="sale_date",
    intervention_time="2026-01-11",
)
print(its_diag.checks)

# Для ITS доступен time placebo; placebo_units
# неприменим, потому что это single-series метод без units.
its_diag = check_its_assumptions(
    its_df,
    outcome_col="metric",
    time_col="sale_date",
    intervention_time="2026-01-11",
    fpr_method="time_placebo",
    fpr_placebo_count=200,
)
print(its_diag.details["fpr_results"])

its_result = fit_its(
    its_df,
    outcome_col="metric",
    time_col="sale_date",
    intervention_time="2026-01-11",
)
print(its_result.summary)

its_robust = run_its_robustness(
    its_df,
    outcome_col="metric",
    time_col="sale_date",
    intervention_time="2026-01-11",
)
print(its_robust.summary)

# Для panel ITS можно выбрать кластеризацию по unit и два уровня unit × time.
panel_its = fit_its(
    panel_df,
    outcome_col="outcome",
    time_col="sale_date",
    intervention_time="2026-02-01",
    se_type="cluster",
    cluster_col="unit",
)
panel_its_two_way = fit_its(
    panel_df,
    outcome_col="outcome",
    time_col="sale_date",
    intervention_time="2026-02-01",
    se_type="two_way_cluster",
    cluster_col="unit",
    time_cluster_col="sale_date",
)
# Для HAC sensitivity автоматически перебираются 0, 3, 7 и 14 лагов.

4. End-to-end пример: PSM

import pandas as pd

from post_analysis_toolkit import (
    check_psm_assumptions,
    fit_psm,
    run_psm_robustness,
)

psm_df = pd.DataFrame(
    {
        "user_id": list(range(1, 13)),
        "treatment_flag": [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
        "outcome": [12, 11, 13, 12, 14, 13, 9, 8, 10, 9, 11, 10],
        "x1": [30, 32, 31, 29, 33, 34, 30, 31, 29, 28, 32, 33],
        "x2": [5, 6, 5, 4, 6, 7, 5, 5, 4, 4, 6, 6],
        "x3": [100, 110, 105, 98, 115, 120, 99, 108, 101, 97, 112, 118],
    }
)

psm_diag = check_psm_assumptions(
    psm_df,
    outcome_col="outcome",
    treatment_col="treatment_flag",
    covariate_cols=["x1", "x2", "x3"],
    caliper=0.2,
)
print(psm_diag.checks)

psm_result = fit_psm(
    psm_df,
    outcome_col="outcome",
    treatment_col="treatment_flag",
    covariate_cols=["x1", "x2", "x3"],
    caliper=0.2,
)
print(psm_result.summary)

psm_robust = run_psm_robustness(
    psm_df,
    outcome_col="outcome",
    treatment_col="treatment_flag",
    covariate_cols=["x1", "x2", "x3"],
    caliper_options=(None, 0.1, 0.2),
    neighbor_options=(1, 2),
)
print(psm_robust.summary)

Что возвращают функции

У пакета два основных result-объекта:

  • MethodResult
  • DiagnosticResult

Оба описаны в contracts.py

MethodResult

Поля:

  • method - имя метода
  • summary - главный итоговый DataFrame
  • details - словарь с дополнительными таблицами
  • figures - словарь matplotlib-фигур
  • metadata - служебные объекты, fit-модели, выбранные доноры и т.д.

DiagnosticResult

Поля:

  • method - имя диагностики
  • checks - таблица проверок предпосылок
  • details - вспомогательные таблицы
  • figures - диагностические графики
  • metadata - служебная информация

Как обычно с этим работать

diag = check_did_assumptions(...)
display(diag.checks)

result = fit_did_regression(...)
display(result.summary)
display(result.details["coefficients"])

fig = result.figures["actual_vs_counterfactual"]
display(fig)

Рекомендуемый workflow

Для любого метода лучше идти одинаково:

  1. Проверить, что метрика и grain корректны.
  2. Проверить предпосылки через check_*_assumptions.
  3. Посчитать основной эффект через fit_*.
  4. Прогнать run_*_robustness.
  5. Уже после этого писать вывод.

Минимальный шаблон:

diag = check_did_assumptions(...)
display(diag.checks)

result = fit_did_regression(...)
display(result.summary)

robust = run_did_robustness(...)
display(robust.summary)

DiD: пример

Формат данных для функции

Пример для unit x day:

sale_date unit metric treated_flag post_flag
2026-02-10 Batumi 0.084 1 0
2026-02-10 Tbilisi 0.121 0 0
2026-02-11 Batumi 0.079 1 0
2026-02-25 Batumi 0.116 1 1
2026-02-25 Tbilisi 0.118 0 1

Обязательные колонки:

  • sale_date
  • unit
  • metric
  • treated_flag
  • post_flag

Опционально:

  • ковариаты
  • веса
  • колонка для кластеризации ошибок

1. Проверка предпосылок

from post_analysis_toolkit import check_did_assumptions

did_diag = check_did_assumptions(
    df=panel_df,
    outcome_col="metric",
    time_col="sale_date",
    treated_col="treated_flag",
    post_col="post_flag",
    intervention_time="2026-02-25",
    unit_col="unit",
    cluster_col="unit",
    event_bin_size=7,
)

Что смотреть:

  • строку Корреляция treated/control на предпериоде
  • строку Разница наклонов тренда на предпериоде
  • строку p-value разницы наклонов на предпериоде
  • строку p-value joint-теста лидов в event study
  • графики pretrend_triptych и event_study

2. Оценка эффекта

from post_analysis_toolkit import fit_did_regression

did_result = fit_did_regression(
    df=panel_df,
    outcome_col="metric",
    time_col="sale_date",
    treated_col="treated_flag",
    post_col="post_flag",
    unit_col="unit",
    cluster_col="unit",
    twfe=True,
    log_transform=False,
    relative=False,
)

Главные поля в summary:

  • effect_absolute, effect_relative, effect_cumulative
  • p_value
  • effect_ci
  • mde_absolute, mde_relative, mde_cumulative, mde_mode_resolved
  • details["mde_power_curve"]

Для DiD режим auto по умолчанию считает MDE аналитически через SE interaction- оценки. Параметрический режим можно включить через mde_mode="parametric", а полноценный повторный запуск регрессии — через mde_mode="resampling". При кластеризации ориентируйся также на stderr_type.

3. Robustness

from post_analysis_toolkit import run_did_robustness

did_robust = run_did_robustness(
    df=panel_df,
    outcome_col="metric",
    time_col="sale_date",
    treated_col="treated_flag",
    post_col="post_flag",
    unit_col="unit",
    cluster_col="unit",
)

Это прогоняет набор спецификаций:

  • с TWFE и без
  • с логами и без
  • с относительной индексацией и без

Для отдельной динамики эффекта используется Event Study. В его проверке предпосылок FPR считается по joint Wald-тесту всех post-бинов (а не по минимальному p-value отдельного бина):

from post_analysis_toolkit import check_event_study_assumptions, fit_event_study

event_diag = check_event_study_assumptions(
    panel_df,
    outcome_col="metric",
    time_col="sale_date",
    treated_col="treated_flag",
    intervention_time="2026-02-25",
    unit_col="unit",
    cluster_col="unit",
    fpr_method=("time_placebo", "block_bootstrap", "placebo_units"),
    fpr_placebo_count=200,
    fpr_block_length=7,
)
event_result = fit_event_study(
    panel_df,
    outcome_col="metric",
    time_col="sale_date",
    treated_col="treated_flag",
    intervention_time="2026-02-25",
    unit_col="unit",
    cluster_col="unit",
    event_bin_size=7,
)

В event_result.details["event_study_summary"] доступны joint_pre_p_value и joint_post_p_value; FPR-таблица находится в event_diag.details["fpr_results"]. MDE по каждому event-бину можно получить уже на этапе проверки предпосылок:

display(event_diag.details["event_study_mde"])

Для расчёта MDE на этом этапе используются те же параметры, что и в fit_event_study (mde_mode, power, alternative, n_simulations, random_state, mde_effect_grid, mde_simulator). По умолчанию mde_mode="auto" выбирает аналитический MDE по стандартной ошибке каждого event-бина.

Synthetic Control: пример

Формат данных для функции

Пример для unit x day с одним treated unit и несколькими донорами:

sale_date unit metric
2026-02-10 Batumi 0.084
2026-02-10 Tbilisi 0.121
2026-02-10 Kutaisi 0.097
2026-02-11 Batumi 0.079
2026-02-11 Tbilisi 0.120

Обязательные колонки:

  • sale_date
  • unit
  • metric

Что важно:

  • одна строка на один unit в одну дату
  • donor units не должны быть затронуты treatment
  • metric должна быть сопоставима между unit-ами
from post_analysis_toolkit import (
    check_synthetic_control_assumptions,
    fit_synthetic_control,
    run_synthetic_control_robustness,
)

synth_diag = check_synthetic_control_assumptions(
    df=panel_df,
    outcome_col="metric",
    unit_col="unit",
    time_col="sale_date",
    treated_unit="Batumi",
    intervention_time="2026-02-25",
    donor_units=["Tbilisi", "Kutaisi"],
    fpr_method=("time_placebo", "block_bootstrap", "placebo_units"),
    fpr_placebo_count=20,
    fpr_block_length=7,
)
print(synth_diag.details["fpr_results"])

Помимо in-space placebo_units, Synthetic Control поддерживает time_placebo (фиктивные даты внутри предпериода) и block_bootstrap (нулевые панели из общих временных блоков). Подробные результаты находятся в details["time_placebo_results"] и details["block_bootstrap_results"]; план доступных прогонов — в details["fpr_plan"].

synth_result = fit_synthetic_control(
    df=panel_df,
    outcome_col="metric",
    unit_col="unit",
    time_col="sale_date",
    treated_unit="Batumi",
    intervention_time="2026-02-25",
    donor_units=["Tbilisi", "Kutaisi"],
)

synth_robust = run_synthetic_control_robustness(
    df=panel_df,
    outcome_col="metric",
    unit_col="unit",
    time_col="sale_date",
    treated_unit="Batumi",
    intervention_time="2026-02-25",
    donor_units=["Tbilisi", "Kutaisi"],
)

Что смотреть:

  • donor_count
  • pre_rmse
  • pre_r2
  • pre_corr
  • avg_post_effect
  • avg_post_relative_effect
  • таблицу весов details["weights"]
  • details["weight_concentration"] с min/max весом, effective donors, числом доноров с положительным весом и долей веса крупнейшего донора
  • details["fpr_results"] с in-space FPR и permutation p-value

Проверки устойчивости:

  • placebo по каждому донору
  • leave-one-out по донорам
  • in-space placebo-калибровка FPR в check_synthetic_control_assumptions
  • time_placebo с фиктивными датами внутри предпериода
  • block_bootstrap с нулевыми панелями из общих временных блоков

Для Synthetic Control нет модельного p-value/SE. In-space FPR по очереди делает каждого донора псевдотритментом, сравнивает post/pre RMSPE-ratio с остальными донорами и возвращает permutation p-value исходной treated unit. time_placebo переносит дату вмешательства внутрь предпериода, а block_bootstrap создаёт нулевые панели, ресэмплируя общие временные блоки. В обоих случаях значимость всё равно определяется in-space permutation по RMSPE-ratio, а не обычной модельной SE. Число успешных placebo_units прогонов не может быть больше размера donor pool; для остальных методов ограничение задаётся числом допустимых дат или успешных bootstrap-прогонов. Для содержательной калибровки желательно иметь минимум три донора: при маленьком donor pool permutation p-value дискретен и FPR получается грубой диагностикой.

Концентрация donor weights не является автоматическим pass/fail-критерием. Если top1_weight_share близка к 100% или effective_donors близко к 1, нужно дополнительно проверить leave_one_out_results и устойчивость к расширению/очистке donor pool.

Synthetic DiD (SDiD)

Synthetic DiD сочетает DiD с unit- и time-весами synthetic control. В пакетe доступны три функции:

  • fit_synthetic_did(...) — расчёт эффекта и synthetic counterfactual;
  • check_synthetic_did_assumptions(...) — pre-fit с учётом level shift, proxy-MDE и placebo FPR;
  • run_synthetic_did_robustness(...) — чувствительность к pre-window и placebo датам.
from post_analysis_toolkit import fit_synthetic_did

sdid_result = fit_synthetic_did(
    df=panel_df,
    outcome_col="revenue",
    unit_col="pizzeria",
    time_col="sale_date",
    treated_col="treated_flag",
    intervention_time="2026-02-01",
    variance_method="placebo",
)
display(sdid_result.summary)
display(sdid_result.details["unit_weights"])
display(sdid_result.figures["actual_vs_counterfactual"])

treated_col должен быть бинарным и постоянным внутри unit. Основные поля summary: effect_absolute, effect_relative, effect_cumulative, effect_ci, effect_relative_ci, effect_cumulative_ci, std_error, p_value, mde_absolute, mde_relative, mde_cumulative, mde_mode_resolved.

Для SDiD доступны mde_mode="analytic", "parametric", "resampling" и "proxy". В resampling можно передать mde_simulator; если он не задан, пакет автоматически строит residual-bootstrap pipeline и повторно вызывает diff-diff для каждой симулированной панели.

В details["unit_weights"] и details["time_weights"] доступны полные списки весов. В details["weight_concentration"] пакет дополнительно показывает min/max, эффективное число доноров и pre-периодов, общее число pre-периодов, число периодов с положительным time weight, долю effective pre-периодов и долю веса трёх/пяти самых важных периодов. Это описательные метрики без автоматически заданных порогов; effective_pre_periods — это ESS по time weights, а не буквальное число использованных дат.

В details["series"] сохраняются сырые counterfactual/gap и level-shift-adjusted counterfactual_level_adjusted/gap_level_adjusted. effect_cumulative и графики SDiD используют adjusted gap, чтобы не включать допустимую постоянную разницу уровней в оценку инкрементального эффекта.

Для SDiD поддерживаются variance_method="placebo", "bootstrap" и "jackknife". Режим MDE auto при корректной SE от diff-diff выбирает аналитический plug-in расчёт; analytic можно задать явно. Это не закрытая SDiD-формула дизайн-мощности: unit/time weights оцениваются на исходных данных, а MDE использует итоговую inferential SE. parametric делает быстрые симуляции оценок с той же SE, proxy использует level-shift-adjusted pre-fit RMSE. resampling строит псевдопанели, добавляет эффект к treated-юнитам в post-периоде и заново оценивает SDiD вместе с unit/time-весами на каждом прогоне; это точнее, но существенно медленнее.

effect_ci — доверительный интервал взвешенного ATT от diff-diff. effect_relative_ci и effect_cumulative_ci — plug-in преобразования этого интервала для относительного (со знаком %) и cumulative estimand; дополнительная неопределённость baseline и level-shift alignment в них не моделируется.

В check_synthetic_did_assumptions(...) параметр fpr_placebo_count задаёт число placebo-прогонов. Если передать zeta_omega или zeta_lambda, эта же регуляризация будет использована и в основном fit, и во всех placebo-fit — это позволяет честно сравнивать эмпирическую FPR между вариантами zeta_lambda.

Параметр fpr_method принимает одну строку или список методов: time_placebo (fake intervention даты внутри pre-period), block_bootstrap (ресэмплинг общих временных блоков без эффекта) и placebo_units (формирование placebo- treated групп контрольных пиццерий того же размера, что и исходная treated- группа). Можно передать несколько методов сразу, например fpr_method=("time_placebo", "block_bootstrap", "placebo_units"). Для block bootstrap длина блока задаётся fpr_block_length (по умолчанию 7). Сводка по каждому методу находится в details["fpr_results"]; при выборе нескольких методов добавляются method-specific строки в checks и график figures["fpr_comparison"]. fpr_placebo_count используется как число попыток для time placebo/block bootstrap и как максимум контрольных units для placebo units; фактический знаменатель находится в колонке runs, а размер placebo-группы — в placebo_group_size.

Практический порядок работы: сначала запустите check_synthetic_did_assumptions(...) и посмотрите одновременно на FPR и details["weight_concentration"]. Если FPR заметно выше заданного alpha, а effective_pre_periods мало и top-3 time weights забирают большую долю веса, повторите основной fit и проверку с явно заданным большим zeta_lambda:

sdid_result = fit_synthetic_did(..., zeta_lambda=25_000.0)
sdid_checks = check_synthetic_did_assumptions(
    ..., zeta_lambda=25_000.0,
)

Это не универсальный порог: значение зависит от масштаба outcome и должно выбираться по sensitivity-таблице вместе с предметной интерпретацией. При изменении единиц outcome (например, переводе рублей в тысячи) zeta_lambda нужно масштабировать пропорционально. Если регуляризация зафиксирована, одно и то же значение следует передавать и в основной fit, и в FPR-проверку.

Не следует выбирать zeta_lambda только для того, чтобы получить FPR около 5%: регуляризация меняет time-weighted estimand. Сначала задайте сетку значений, сравните effective pre-periods, концентрацию top-3/top-5 весов, pre-fit и оценку эффекта, а затем зафиксируйте настройку до финального вывода.

В robustness можно опционально включить нативные проверки diff-diff:

robust = run_synthetic_did_robustness(
    panel_df,
    outcome_col="revenue",
    unit_col="pizzeria",
    time_col="sale_date",
    treated_col="treated_flag",
    intervention_time="2026-02-01",
    diff_diff_checks=("in_time_placebo", "zeta_omega_sensitivity"),
    diff_diff_placebo_periods=["2025-12-15"],
    zeta_lambda_multipliers=(0.1, 1.0, 10.0, 100.0, 1_000.0, 10_000.0, 100_000.0),
)

Доступные значения: in_time_placebo, leave_one_out (требует variance_method="jackknife") и zeta_omega_sensitivity. По умолчанию diff_diff_checks=(), поэтому дополнительные нативные прогоны не выполняются. Параметр zeta_lambda_multipliers запускает отдельные полные SDiD-фиты для множителей автоматически выбранного zeta_lambda; по умолчанию он пустой, чтобы не увеличивать время расчёта. Результаты находятся в details["zeta_lambda_sensitivity"], а графики — в figures["zeta_lambda_sensitivity"] и figures["zeta_lambda_weight_concentration"]. В таблице вместе с эффектом показываются ESS, доля effective pre-периодов, число положительных весов и накопленная доля top-3/top-5 time weights.

Causal Impact: пример

Формат данных для функции

Формат на вход такой же, как у Synthetic Control.

Пример:

sale_date unit metric
2026-02-10 Batumi 0.084
2026-02-10 Tbilisi 0.121
2026-02-10 Kutaisi 0.097
2026-02-11 Batumi 0.079
2026-02-11 Tbilisi 0.120

Обязательные колонки:

  • sale_date
  • unit
  • metric

Что важно:

  • treated unit один
  • pre-period должен быть достаточно длинным
  • после выравнивания treated и donor рядов не должно быть пропусков
from post_analysis_toolkit import (
    check_causal_impact_assumptions,
    fit_causal_impact,
    run_causal_impact_robustness,
)

ci_diag = check_causal_impact_assumptions(
    df=panel_df,
    outcome_col="metric",
    unit_col="unit",
    time_col="sale_date",
    treated_unit="Batumi",
    intervention_time="2026-02-25",
    donor_units=["Tbilisi", "Kutaisi"],
)

# Для Causal Impact доступны time placebo, block bootstrap и placebo units.
ci_diag = check_causal_impact_assumptions(
    df=panel_df,
    outcome_col="metric",
    unit_col="unit",
    time_col="sale_date",
    treated_unit="Batumi",
    intervention_time="2026-02-25",
    donor_units=["Tbilisi", "Kutaisi"],
    fpr_method=("time_placebo", "block_bootstrap", "placebo_units"),
    fpr_placebo_count=200,
    fpr_block_length=7,
)
print(ci_diag.details["fpr_results"])

ci_result = fit_causal_impact(
    df=panel_df,
    outcome_col="metric",
    unit_col="unit",
    time_col="sale_date",
    treated_unit="Batumi",
    intervention_time="2026-02-25",
    donor_units=["Tbilisi", "Kutaisi"],
)

ci_robust = run_causal_impact_robustness(
    df=panel_df,
    outcome_col="metric",
    unit_col="unit",
    time_col="sale_date",
    treated_unit="Batumi",
    intervention_time="2026-02-25",
    donor_units=["Tbilisi", "Kutaisi"],
)

Что смотреть:

  • строку R2 на валидации
  • строку RMSE на валидации
  • average_abs_effect
  • average_rel_effect
  • effect_absolute, effect_relative, effect_cumulative
  • effect_absolute_ci, effect_relative_ci
  • posterior_tail_area
  • mde_absolute, mde_relative, mde_cumulative, mde_mode_resolved
  • details["mde_power_curve"]

Для Causal Impact simulation-режим много раз получает случайную оценку с масштабом, равным RMSE на валидации. Это еще не повторный запуск state-space pipeline для каждого pseudo-dataset; такой callback остается отдельным расширением.

Проверки устойчивости:

  • placebo по более ранним датам
  • leave-one-out по донорам

ITS: пример

Формат данных для функции

Базовый пример для одного временного ряда:

sale_date metric
2026-01-01 125.0
2026-01-02 118.0
2026-01-03 133.0
2026-02-25 149.0
2026-02-26 152.0

Если есть дополнительные регрессоры:

sale_date metric weather_temp holiday_flag
2026-01-01 125.0 8.3 1
2026-01-02 118.0 7.9 0
2026-02-25 149.0 10.4 0

Обязательные колонки:

  • sale_date
  • metric

Что важно:

  • одна строка на одну дату
  • доп. фичи должны быть на том же дневном grain
  • ряд должен быть числовым и упорядочиваемым по времени
from post_analysis_toolkit import (
    build_standard_its_features,
    check_its_assumptions,
    fit_its,
    run_its_robustness,
)

its_diag = check_its_assumptions(
    df=ts_df,
    outcome_col="metric",
    time_col="sale_date",
    intervention_time="2026-02-25",
)

its_result = fit_its(
    df=ts_df,
    outcome_col="metric",
    time_col="sale_date",
    intervention_time="2026-02-25",
)

its_robust = run_its_robustness(
    df=ts_df,
    outcome_col="metric",
    time_col="sale_date",
    intervention_time="2026-02-25",
)

По умолчанию ITS использует:

  • time_index
  • post_flag
  • time_after_intervention
  • day-of-week dummies
  • month_start_flag
  • month_end_flag

Можно добавить дополнительные фичи:

its_result = fit_its(
    df=ts_df,
    outcome_col="metric",
    time_col="sale_date",
    intervention_time="2026-02-25",
    extra_feature_cols=["weather_temp", "holiday_flag"],
)

Что смотреть:

  • avg_abs_effect
  • avg_rel_effect
  • effect_absolute, effect_relative, effect_cumulative
  • строку R2 на валидации
  • строку RMSE на валидации
  • строку Средний rolling R2 на валидации
  • mde_absolute, mde_relative, mde_cumulative, mde_mode_resolved
  • details["mde_power_curve"]

Для ITS simulation-режим много раз получает случайную оценку с масштабом, равным RMSE на валидации. Он относится к выбранному агрегированному постпериодному estimand и не является отдельным MDE для level и slope.

Проверки устойчивости:

  • разные длины pre-window
  • placebo intervention dates
  • разные feature sets

Для проверки эмпирической FPR у ITS доступен fpr_method="time_placebo": пакет переоценивает модель на нескольких фиктивных датах вмешательства внутри предпериода. placebo_units для single-series ряда неприменим. Режим block_bootstrap для ITS не поддерживается; при его передаче будет ValueError. Число попыток задаётся fpr_placebo_count, а результаты сохраняются в details["fpr_results"].

Стандартные проверки предпосылок

Во всех checks-таблицах пакет теперь возвращает русские колонки:

  • проверка
  • значение
  • ориентир

Для DiD

  • визуальная параллельность трендов
  • pre-period correlation
  • slope gap на pre-period
  • joint pre-trend test через event study
  • отсутствие параллельных изменений

Для Synthetic Control

  • достаточно доноров
  • хороший pre-fit
  • отсутствие contamination доноров
  • reasonable donor weights
  • in-space placebo FPR и permutation p-value

Для Causal Impact

  • достаточно длинный pre-period
  • нормальное predictive quality на pre-period
  • отсутствие пропусков после выравнивания
  • отсутствие contamination контрольных рядов

Для ITS

  • достаточно длинный pre-period
  • нет сильных дырок в датах
  • модель более-менее объясняет pre-period
  • residual diagnostics не выглядят катастрофически
  • нет явных concurrent changes

Интерпретация результатов

Пакет возвращает числа, но не ставит автоматический ярлык “causal effect proven”.

Практически удобно мыслить так:

  • если assumptions слабые, а эффект красивый - не переоценивай вывод
  • если assumptions хорошие, но MDE огромный - отсутствие значимости мало что значит
  • если robustness разваливается - основной результат надо считать хрупким
  • если control-конструкция сомнительна - лучше честный descriptive / benchmark framing

Типичные ошибки

1. Неправильный grain

Например:

  • treated на уровне города
  • а outcome собран на уровне товара x день без аккуратной агрегации

Сначала выровняй unit анализа.

2. Плохой pre-period

Если pre-period слишком короткий, то:

  • ITS будет слабым
  • Causal Impact будет плохо предсказывать
  • Synthetic Control будет переобучаться

3. Перепутан post_flag

Проверь, что:

  • до даты intervention post_flag = 0
  • начиная с даты intervention post_flag = 1

4. Causal-язык без проверок

Нельзя говорить “акция вызвала рост”, если:

  • assumptions не прошли
  • donor pool слабый
  • robustness разваливается

Рекомендации по использованию в пост-анализах

  • Для продаж с нормальным donor pool начинай с Synthetic Control или Causal Impact
  • Для benchmark-сравнения treated vs control держи DiD
  • Если контроль надежно собрать не удалось, переходи к ITS
  • Если даже ITS слабый, оставляй descriptive analysis

Где смотреть готовые примеры

Публичный API

Пакет экспортирует:

  • compute_classic_did
  • fit_did_regression
  • fit_event_study
  • check_did_assumptions
  • run_did_robustness
  • fit_triple_difference
  • check_triple_difference_assumptions
  • run_triple_difference_robustness
  • fit_rdd
  • check_rdd_assumptions
  • run_rdd_robustness
  • fit_psm
  • check_psm_assumptions
  • run_psm_robustness
  • fit_doubly_robust
  • check_doubly_robust_assumptions
  • run_doubly_robust_robustness
  • fit_synthetic_control
  • check_synthetic_control_assumptions
  • run_synthetic_control_robustness
  • fit_synthetic_did
  • check_synthetic_did_assumptions
  • run_synthetic_did_robustness
  • fit_causal_impact
  • check_causal_impact_assumptions
  • run_causal_impact_robustness
  • build_standard_its_features
  • fit_its
  • check_its_assumptions
  • run_its_robustness

Ограничения текущей версии

  • Synthetic Control здесь реализован в облегченной форме, без сложного набора pre-treatment predictors
  • Causal Impact зависит от сторонней библиотеки и требует аккуратного окружения
  • MDE по умолчанию выбирает аналитический расчет при наличии SE, а для методов без аналитической формы использует параметрическую симуляцию; режим resampling переобучает поддержанный расчет на каждом повторе
  • contamination и concurrent changes проверяются вручную, а не автоматически

Планируемое развитие

Staggered Synthetic DiD пока не входит в публичный API. В roadmap предусмотрены два последовательных этапа:

  1. cohort-specific обёртка поверх fit_synthetic_did с отдельной оценкой по каждой дате начала воздействия и взвешенной агрегацией результатов;
  2. современный staggered DiD estimator с корректными aggregate SE, p-value и confidence interval, а также явной диагностикой неоднородности эффектов.

До отдельной реализации и валидации эти функции не следует считать доступными или поддержанными в текущей версии пакета.

Версия

Текущая версия пакета: 1.0.5

Metadata

Release files for post-analysis-toolkit 1.0.9

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for post-analysis-toolkit 1.0.9
File Size Uploaded
post_analysis_toolkit-1.0.9.tar.gz 238.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for post-analysis-toolkit 1.0.9
File Interpreter ABI Platform
post_analysis_toolkit-1.0.9-py3-none-any.whl Python 3 none any Details

Total release size: 430.3 kB

Release files / post_analysis_toolkit-1.0.9.tar.gz

Download URL post_analysis_toolkit-1.0.9.tar.gz
Size 238.7 kB
Tags Source
SHA-256 checksum
How to use checksums
6d7686d2aaddbeaaf633f5624f0db25584a7f4befa87b6e1092fcd8e09f3c070
BLAKE2b-256 checksum
How to use checksums
967d0635a566079c1951c66c5d130511d37fa379c1a01d097fa5e6d474ebc005
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Release files / post_analysis_toolkit-1.0.9-py3-none-any.whl

Download URL post_analysis_toolkit-1.0.9-py3-none-any.whl
Size 191.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
309d7b77e5fcd444972e3c05f42ca73d4c087d9ba4c34c8c0e262c9e3d270c0e
BLAKE2b-256 checksum
How to use checksums
37cd79fd87c8cd7fa452da1d46f6423209c4b226fef6b912413e34f03303227a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.9 This release

2 release files

1.0.8

2 release files

1.0.7

2 release files

1.0.5

2 release files

1.0.4

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page