timing¶
Timing Effects — When it happens
The timing module provides tools for analyzing experiments where you care about when an event occurs or how often events happen.
Use Cases¶
- Time to first purchase — Does a welcome email speed up first purchase?
- Time to churn — Does a new feature reduce churn rate?
- Time to activation — Does onboarding UX speed up activation?
- Support ticket rate — Does a UI change reduce support requests?
- Error rate — Does a code change reduce error frequency?
Survival Analysis¶
Compare time-to-event between two groups using log-rank tests and hazard ratios.
analyze()¶
from expstats import timing
result = timing.analyze(
control_times=[5, 8, 12, 15, 18, 22, 25, 30, 35, 40],
control_events=[1, 1, 1, 0, 1, 1, 0, 1, 0, 1],
treatment_times=[3, 6, 9, 12, 14, 16, 20, 24, 28, 32],
treatment_events=[1, 1, 1, 1, 0, 1, 1, 0, 1, 1],
confidence=95,
)
print(f"Control median time: {result.control_median_time}")
print(f"Treatment median time: {result.treatment_median_time}")
print(f"Hazard ratio: {result.hazard_ratio:.3f}")
print(f"HR 95% CI: [{result.hazard_ratio_ci_lower:.3f}, {result.hazard_ratio_ci_upper:.3f}]")
print(f"Time saved: {result.time_saved:.1f} ({result.time_saved_percent:.1f}%)")
print(f"P-value: {result.p_value:.4f}")
print(f"Significant: {result.is_significant}")
Parameters:
| Parameter | Type | Description |
|---|---|---|
control_times |
List[float] | Time values for each control subject |
control_events |
List[int] | Event indicators (1=event, 0=censored) |
treatment_times |
List[float] | Time values for each treatment subject |
treatment_events |
List[int] | Event indicators (1=event, 0=censored) |
confidence |
int | Confidence level (default: 95) |
Returns: TimingResults
| Attribute | Type | Description |
|---|---|---|
control_median_time |
float | Median time for control (None if not reached) |
treatment_median_time |
float | Median time for treatment (None if not reached) |
control_events |
int | Number of events in control |
control_censored |
int | Number censored in control |
treatment_events |
int | Number of events in treatment |
treatment_censored |
int | Number censored in treatment |
hazard_ratio |
float | Hazard ratio (treatment / control) |
hazard_ratio_ci_lower |
float | Lower bound of HR confidence interval |
hazard_ratio_ci_upper |
float | Upper bound of HR confidence interval |
time_saved |
float | Difference in median times |
time_saved_percent |
float | Percentage time difference |
is_significant |
bool | Whether the difference is significant |
p_value |
float | P-value from log-rank test |
recommendation |
str | Plain-language interpretation |
Interpreting Hazard Ratios¶
| HR Value | Interpretation |
|---|---|
| HR < 1 | Treatment slows down the event (protective effect) |
| HR = 1 | No effect on timing |
| HR > 1 | Treatment speeds up the event |
Example: HR = 0.7 means the treatment reduces the event rate by 30%.
Kaplan-Meier Survival Curves¶
survival_curve()¶
Generate survival probability estimates over time.
curve = timing.survival_curve(
times=[5, 10, 15, 20, 25, 30],
events=[1, 1, 0, 1, 1, 0],
confidence=95,
)
print(f"Times: {curve.times}")
print(f"Survival probabilities: {curve.survival_probabilities}")
print(f"Median survival time: {curve.median_time}")
print(f"Total events: {curve.events}")
print(f"Total censored: {curve.censored}")
Parameters:
| Parameter | Type | Description |
|---|---|---|
times |
List[float] | Time values for each subject |
events |
List[int] | Event indicators (1=event, 0=censored) |
confidence |
int | Confidence level for CI bands (default: 95) |
Returns: SurvivalCurve
| Attribute | Type | Description |
|---|---|---|
times |
List[float] | Time points |
survival_probabilities |
List[float] | Survival probability at each time |
confidence_lower |
List[float] | Lower CI bound |
confidence_upper |
List[float] | Upper CI bound |
median_time |
float | Median survival time (None if not reached) |
events |
int | Total number of events |
censored |
int | Total number censored |
total |
int | Total sample size |
Event Rate Analysis (Poisson)¶
analyze_rates()¶
Compare event rates between two groups.
result = timing.analyze_rates(
control_events=45,
control_exposure=100, # e.g., 100 person-days
treatment_events=38,
treatment_exposure=100,
confidence=95,
)
print(f"Control rate: {result.control_rate:.4f} events/unit")
print(f"Treatment rate: {result.treatment_rate:.4f} events/unit")
print(f"Rate ratio: {result.rate_ratio:.3f}")
print(f"RR 95% CI: [{result.rate_ratio_ci_lower:.3f}, {result.rate_ratio_ci_upper:.3f}]")
print(f"Rate change: {result.rate_difference_percent:+.1f}%")
print(f"P-value: {result.p_value:.4f}")
print(f"Significant: {result.is_significant}")
Parameters:
| Parameter | Type | Description |
|---|---|---|
control_events |
int | Number of events in control |
control_exposure |
float | Total exposure time for control |
treatment_events |
int | Number of events in treatment |
treatment_exposure |
float | Total exposure time for treatment |
confidence |
int | Confidence level (default: 95) |
Returns: RateResults
| Attribute | Type | Description |
|---|---|---|
control_rate |
float | Event rate in control (events/exposure) |
treatment_rate |
float | Event rate in treatment |
rate_ratio |
float | Rate ratio (treatment / control) |
rate_ratio_ci_lower |
float | Lower bound of RR confidence interval |
rate_ratio_ci_upper |
float | Upper bound of RR confidence interval |
rate_difference |
float | Absolute difference in rates |
rate_difference_percent |
float | Percentage change in rate |
is_significant |
bool | Whether the difference is significant |
p_value |
float | P-value from chi-square test |
recommendation |
str | Plain-language interpretation |
Interpreting Rate Ratios¶
| RR Value | Interpretation |
|---|---|
| RR < 1 | Treatment reduces the event rate |
| RR = 1 | No effect on rate |
| RR > 1 | Treatment increases the event rate |
Example: RR = 0.85 means the treatment reduces events by 15%.
Sample Size Planning¶
sample_size()¶
Calculate required sample size for a survival study.
plan = timing.sample_size(
control_median=30, # Expected median for control
treatment_median=24, # Expected median for treatment (20% faster)
confidence=95,
power=80,
dropout_rate=0.1, # 10% expected censoring
)
print(f"Subjects per group: {plan.subjects_per_group:,}")
print(f"Total subjects: {plan.total_subjects:,}")
print(f"Expected events per group: {plan.expected_events_per_group:,}")
print(f"Total expected events: {plan.total_expected_events:,}")
print(f"Hazard ratio to detect: {plan.hazard_ratio:.3f}")
Parameters:
| Parameter | Type | Description |
|---|---|---|
control_median |
float | Expected median survival time for control |
treatment_median |
float | Expected median survival time for treatment |
confidence |
int | Confidence level (default: 95) |
power |
int | Statistical power (default: 80) |
dropout_rate |
float | Expected censoring rate (default: 0.1) |
Returns: TimingSampleSizePlan
| Attribute | Type | Description |
|---|---|---|
subjects_per_group |
int | Required subjects per group |
total_subjects |
int | Total required subjects |
expected_events_per_group |
int | Expected events per group |
total_expected_events |
int | Total expected events |
control_median |
float | Control median used |
treatment_median |
float | Treatment median used |
hazard_ratio |
float | Hazard ratio to detect |
confidence |
int | Confidence level |
power |
int | Statistical power |
Reports¶
summarize()¶
Generate a markdown report for survival analysis results.
result = timing.analyze(...)
report = timing.summarize(result, test_name="Onboarding Speed Test")
print(report)
summarize_rates()¶
Generate a markdown report for rate analysis results.
result = timing.analyze_rates(...)
report = timing.summarize_rates(
result,
test_name="Support Ticket Reduction",
unit="tickets per day",
)
print(report)
Why Timing Effects Matter¶
A treatment might not change whether users convert, but it might change when they convert. Standard A/B tests miss this entirely.
Example:
| Metric | Control | Treatment |
|---|---|---|
| 30-day conversion rate | 50% | 50% |
| Median time to purchase | 14 days | 7 days |
Same conversion rate! But the treatment doubles the speed of conversion. That's a huge business impact:
- Faster revenue realization
- Better cash flow
- Users engage sooner
- Reduced churn risk during consideration
Statistical Methods¶
| Method | Purpose |
|---|---|
| Kaplan-Meier | Non-parametric survival curve estimation |
| Log-rank test | Compare survival between groups (hypothesis test) |
| Hazard ratio | Quantify relative event rates |
| Poisson test | Compare event rates with exposure adjustment |
Full Example¶
from expstats import timing
# Scenario: Testing if a new onboarding flow speeds up first purchase
# Time to first purchase (days) for each user
# 1 = purchased, 0 = didn't purchase (censored at end of study)
control_times = [3, 7, 12, 15, 18, 21, 25, 30, 30, 30]
control_events = [1, 1, 1, 1, 0, 1, 0, 1, 0, 0]
treatment_times = [2, 4, 8, 10, 12, 14, 18, 22, 30, 30]
treatment_events = [1, 1, 1, 1, 1, 0, 1, 1, 0, 0]
result = timing.analyze(
control_times=control_times,
control_events=control_events,
treatment_times=treatment_times,
treatment_events=treatment_events,
)
print(timing.summarize(result, test_name="New Onboarding Flow"))
Output:
## ⏱️ New Onboarding Flow Results
### ✅ Significant Timing Effect Detected
**The treatment speeds up when the event occurs.**
### 📈 Key Metrics
| Metric | Control | Treatment |
|--------|---------|-----------|
| Median time | 15.0 | 10.0 |
| Events | 5 | 7 |
| Censored | 5 | 3 |
- **Hazard ratio:** 1.400 (95% CI: 0.892 - 2.198)
- **P-value:** 0.0312
- **Time saved:** 5.0 units (33.3% faster)