Multi-Touch Revenue Attribution for Web Roguelikes: A Practical Guide

A practical guide to measuring true contribution of each monetization touchpoint and designing experiments without disrupting live operations in web roguelike games.

---

1. Why Single-Touch Attribution Fails in Roguelikes

Most small-scale roguelike developers treat conversions as single events. "User pressed the button, purchase happened" -- but this simple attribution misses the cumulative effect of dozens of touchpoints.

Purchase patterns in roguelikes are not single-session events but probabilistic accumulation across multiple sessions.

Typical conversion journey (web roguelike - avg 4.2 sessions to purchase):
--------------------------------------------------------------------------------
Session 1: Organic acquisition -> Tutorial -> 2-3 runs -> No ad exposure
Session 2: Watched rewarded ad -> 1 run -> Received 10 premium currency
Session 3: Clicked push notification -> Joined event -> Earned item (free)
Session 4: Received email -> Returned -> Watched ads x2 -> [CONVERSION] $4.99 skin purchase

Where was the conversion actually decided?
- Last-Touch says: Email (Session 4)
- First-Touch says: Organic (Session 1)
- Reality: Rewarded ad + Push + Email all contributed

---

2. Attribution Model Spectrum

Last-Touch (Default)

Position-Based (U-Shape) 40/20/20/20

Time Decay

Markov Chain (Multi-Touch)

---

3. Practical Implementation: Uplift Testing (Indie Recommended)

# uplift_test.py - Minimum Viable Uplift Framework
import pandas as pd
import numpy as np
from scipy import stats

class RoguelikeUpliftTest:
    def __init__(self, exposed_rate=0.3):
        self.exposed_rate = exposed_rate  # Treatment group ratio
    
    def assign_group(self, user_id: str, bucket: str = 'default') -> str:
        """Deterministic hashing for consistent assignment"""
        import hashlib
        h = hashlib.sha256(f"{user_id}-{bucket}".encode()).hexdigest()
        bucket_value = int(h[:8], 16) % 10000 / 10000
        if bucket_value < self.exposed_rate:
            return 'treatment'
        return 'control'
    
    def calculate_uplift(self, df: pd.DataFrame) -> dict:
        """Calculate treatment effect with confidence interval"""
        control = df[df.group == 'control'].converted
        treatment = df[df.group == 'treatment'].converted
        
        # T-test
        t_stat, p_val = stats.ttest_ind(treatment, control)
        
        effect_size = treatment.mean() - control.mean()
        relative_uplift = effect_size / control.mean() * 100
        
        return {
            'control_conversion_rate': control.mean(),
            'treatment_conversion_rate': treatment.mean(),
            'absolute_uplift': effect_size,
            'relative_uplift_pct': relative_uplift,
            'p_value': p_val,
            'statistically_significant': p_val < 0.05,
            'required_sample_per_group': self._min_sample_size(
                baseline_rate=control.mean(),
                mde=0.01  # Minimum Detectable Effect: 1%
            )
        }
    
    def _min_sample_size(self, baseline_rate: float, mde: float,
                         alpha: float = 0.05, power: float = 0.80) -> int:
        """Two-proportion z-test sample size calculation"""
        from statsmodels.stats.power import NormalIndPower
        analysis = NormalIndPower()
        effect_size = mde / (baseline_rate * (1 - baseline_rate)) ** 0.5
        sample = analysis.solve_power(
            effect_size=effect_size,
            alpha=alpha,
            power=power,
            alternative='two-sided'
        )
        return int(np.ceil(sample))

---

4. Policy and Privacy Compliance

AdSense & AdMob Policy

GDPR & Privacy (EEA Users)

Child Protection (Global)

---

5. Implementation Roadmap

PhaseScopeTime RequiredCost
1GA4/Firebase + basic event logging1-2 weeks$0 (free tiers)
2U-Shape + Time Decay comparison1 month$50-$300/month
3Markov Chain Attribution2-3 months$2,000+/month

---