Skip to content

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 abverdict 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 abverdict 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)