Skip to content

Commit ae3dac0

Browse files
committed
Polish report compatibility and replay guidance wording
1 parent e28981d commit ae3dac0

3 files changed

Lines changed: 77 additions & 41 deletions

File tree

docs/ope_methods_math_guide_ru.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ V̂_replay = (1/|I|) · Σ_{i∈I} r_i
5050

5151
- Плюс: «честная» фильтрация без моделирования контрфактов.
5252
- Минус: может иметь большую дисперсию, если совпадений мало.
53+
- Важное ограничение: корректность Replay зависит от support/coverage в логах `π_A` и от того, насколько logging-процесс согласован с целевым сценарием оценки.
54+
- Практически: при низком overlap Replay полезен как диагностический baseline, но не как универсально несмещённая оценка для произвольных contextual-логов.
5355

5456
Источник: Li et al. (unbiased offline evaluation) — https://arxiv.org/abs/1003.5956
5557

src/policyscope/report.py

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -48,24 +48,41 @@ def decision_summary(res: Dict, metric_name: str, business_threshold: float = 0.
4848
V_A = res["V_A"]
4949
V_B = res["V_B"]
5050
D = res["Delta"]
51-
alpha = float(res.get("alpha", 0.05))
51+
alpha = float(res.get("alpha", res.get("inference_alpha", 0.05)))
5252
ci_level = int(round((1.0 - alpha) * 100))
53-
A_lo, A_hi = res["V_A_CI"]
54-
B_lo, B_hi = res["V_B_CI"]
55-
D_lo, D_hi = res["Delta_CI"]
53+
a_ci = res.get("V_A_CI")
54+
b_ci = res.get("V_B_CI")
55+
d_ci = res.get("Delta_CI")
5656

5757
lines = []
5858
lines.append(f"Метрика: {metric_name}")
59-
lines.append(f"V(A) = {V_A:.6f} ({ci_level}% CI: {A_lo:.6f} .. {A_hi:.6f})")
60-
lines.append(f"V(B) = {V_B:.6f} ({ci_level}% CI: {B_lo:.6f} .. {B_hi:.6f})")
61-
lines.append(f"Delta (B−A) = {D:.6f} ({ci_level}% CI: {D_lo:.6f} .. {D_hi:.6f})")
62-
63-
if D_lo > business_threshold:
64-
lines.append(f"Решение: модель B лучше A, поскольку нижняя граница CI превышает порог {business_threshold}.")
65-
elif D_hi < -business_threshold:
66-
lines.append(f"Решение: модель A лучше B, поскольку верхняя граница CI ниже -{business_threshold}.")
59+
if a_ci is not None:
60+
A_lo, A_hi = a_ci
61+
lines.append(f"V(A) = {V_A:.6f} ({ci_level}% CI: {A_lo:.6f} .. {A_hi:.6f})")
6762
else:
68-
lines.append("Решение: статистически значимого отличия не обнаружено или эффект слишком мал.")
63+
lines.append(f"V(A) = {V_A:.6f} (CI недоступен)")
64+
if b_ci is not None:
65+
B_lo, B_hi = b_ci
66+
lines.append(f"V(B) = {V_B:.6f} ({ci_level}% CI: {B_lo:.6f} .. {B_hi:.6f})")
67+
else:
68+
lines.append(f"V(B) = {V_B:.6f} (CI недоступен)")
69+
if d_ci is not None:
70+
D_lo, D_hi = d_ci
71+
lines.append(f"Delta (B−A) = {D:.6f} ({ci_level}% CI: {D_lo:.6f} .. {D_hi:.6f})")
72+
else:
73+
lines.append(f"Delta (B−A) = {D:.6f} (CI недоступен)")
74+
75+
if d_ci is not None:
76+
if D_lo > business_threshold:
77+
lines.append(f"Решение: модель B лучше A, поскольку нижняя граница CI превышает порог {business_threshold}.")
78+
elif D_hi < -business_threshold:
79+
lines.append(f"Решение: модель A лучше B, поскольку верхняя граница CI ниже -{business_threshold}.")
80+
else:
81+
lines.append("Решение: статистически значимого отличия не обнаружено или эффект слишком мал.")
82+
elif res.get("is_significant") is True:
83+
lines.append("Решение: обнаружено статистически значимое отличие, но без CI интерпретация менее устойчива.")
84+
else:
85+
lines.append("Решение: CI не передан; итог следует трактовать как предварительный.")
6986
recommendation = res.get("recommendation")
7087
trust_level = res.get("trust_level")
7188
if trust_level is not None:
@@ -94,7 +111,10 @@ def analyze_logs(
94111
) -> str:
95112
"""Проверяет наличие ключевых столбцов в логах и формирует краткий отчёт."""
96113

97-
lines = ["Проверка входных данных для off-policy оценки:"]
114+
lines = [
115+
"Проверка входных данных для off-policy оценки:",
116+
"- Рекомендуемый high-level путь: compare_policies(...) или OPEEvaluator(...).evaluate_summary(...).",
117+
]
98118

99119
# базовые колонки
100120
missing_basic = [c for c in (user_id_col, action_a_col) if c not in df.columns]
@@ -114,28 +134,27 @@ def analyze_logs(
114134
try:
115135
a_B = policyB.action_argmax(df)
116136
share = float(np.mean(a_B == df.get(action_a_col, -1)))
117-
lines.append(
118-
f"- Replay: политика B совпадает с A в {share * 100:.1f}% случаев."
119-
)
137+
lines.append(f"- Replay overlap A/B: {share * 100:.1f}%.")
120138
if share < 0.1:
121-
lines[-1] += " Требуется больше пересечений для надёжной оценки."
139+
lines.append("- Replay: низкий overlap, оценка может быть шумной и зависимой от support логирующей политики.")
140+
else:
141+
lines.append("- Replay: интерпретируйте как диагностический baseline, а не как универсально несмещённую оценку.")
122142
except Exception:
123-
lines.append(
124-
"- Replay: не удалось вычислить пересечение действий A и B."
125-
)
143+
lines.append("- Replay: не удалось вычислить пересечение действий A и B.")
126144
else:
127-
lines.append("- Replay: политика B не передана.")
145+
lines.append("- Replay: политика B не передана; overlap-диагностика недоступна.")
128146

129-
# IPS / SNIPS
147+
# Propensity source modes
130148
if propensity_col in df.columns:
131-
lines.append(f"- IPS/SNIPS: колонка {propensity_col} найдена.")
149+
lines.append(f"- Propensity: колонка {propensity_col} найдена; режим auto сможет использовать logged propensity path.")
150+
lines.append("- Propensity: estimated path также доступен через propensity_source='estimated'.")
132151
else:
133152
lines.append(
134-
"- IPS/SNIPS: propensities не найдены."
135-
" Необходимо добавить колонку с π_A(a|x) или обучить модель пропенсити."
153+
f"- Propensity: колонка {propensity_col} не найдена; режим auto перейдёт в estimated propensity path."
136154
)
155+
lines.append("- Propensity: strict logged path требует валидную propensity_col.")
137156

138-
# DM
157+
# Feature availability for DM/DR family
139158
if feature_cols is None:
140159
feature_candidates = ["age", "income", "risk", "loyal"]
141160
feats = [c for c in feature_candidates if c in df.columns]
@@ -144,19 +163,11 @@ def analyze_logs(
144163
if feats:
145164
lines.append(f"- DM: доступны признаки {feats}, ок.")
146165
else:
147-
lines.append("- DM: признаки не найдены.")
148-
149-
# DR
150-
if propensity_col not in df.columns:
151-
if feats:
152-
lines.append(
153-
"- DR: пропенсити отсутствуют, но можно применить DM;"
154-
" метод DR будет смещён, если модель неточна."
155-
)
156-
else:
157-
lines.append("- DR: нет пропенсити и признаков, метод неприменим.")
158-
else:
159-
lines.append("- DR: можно применить, пропенсити присутствуют.")
166+
lines.append("- DM/DR-family: признаки не найдены; модели nuisance могут быть нестабильны.")
167+
168+
if feats:
169+
lines.append("- DR/SNDR/Switch-DR: применимы через официальный comparison API; проверяйте CI/p-value и diagnostics вместе.")
170+
lines.append("- Cross-fit: опционально рекомендуется для дополнительного bias-hardening в DR-family режимах.")
160171

161172
for line in lines:
162173
logging.info(line)

tests/test_bootstrap_report.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from policyscope.bootstrap import cluster_bootstrap_ci, paired_bootstrap_ci
55
from policyscope.inference import infer_policy_comparison_bootstrap
6-
from policyscope.report import decision_summary
6+
from policyscope.report import analyze_logs, decision_summary
77

88

99
def test_cluster_bootstrap_ci_basic():
@@ -177,3 +177,26 @@ def test_decision_summary_uses_alpha_from_result():
177177
txt = decision_summary(res, "metric", business_threshold=0.0)
178178
assert "90% CI" in txt
179179
assert "Уровень доверия к оценке: caution." in txt
180+
181+
182+
def test_decision_summary_legacy_output_without_ci_is_supported():
183+
res = {"V_A": 0.2, "V_B": 0.25, "Delta": 0.05, "is_significant": False}
184+
txt = decision_summary(res, "metric", business_threshold=0.0)
185+
assert "CI недоступен" in txt
186+
assert "итог следует трактовать как предварительный" in txt
187+
188+
189+
def test_analyze_logs_mentions_official_workflow_and_propensity_modes():
190+
df = pd.DataFrame(
191+
{
192+
"user_id": [1, 2, 3],
193+
"a_A": [0, 1, 0],
194+
"accept": [1.0, 0.0, 1.0],
195+
"age": [20, 30, 40],
196+
}
197+
)
198+
txt = analyze_logs(df, policyB=None, propensity_col="propensity_A")
199+
assert "compare_policies" in txt
200+
assert "режим auto перейдёт в estimated propensity path" in txt
201+
assert "strict logged path требует валидную propensity_col" in txt
202+
assert "DR/SNDR/Switch-DR" in txt

0 commit comments

Comments
 (0)