Mirror Chart

Organic vs Paid Traffic Sessions

Mirror histogram comparing user engagement with bounce rate annotations

Output
Organic vs Paid Traffic Sessions
Python
import numpy as np
import matplotlib.pyplot as plt

np.random.seed(111)
BG_COLOR = '#0a0a0f'

# Session duration (seconds)
organic = np.random.exponential(200, 1000) + 20
paid = np.random.exponential(90, 1000) + 15
organic = organic[organic < 800]
paid = paid[paid < 800]

fig, ax = plt.subplots(figsize=(12, 7), facecolor=BG_COLOR)
ax.set_facecolor(BG_COLOR)

bins = np.linspace(0, 600, 40)
bin_width = bins[1] - bins[0]
centers = (bins[:-1] + bins[1:]) / 2

# Organic (top) - green
h1, _ = np.histogram(organic, bins=bins, density=True)
ax.bar(centers, h1, width=bin_width*0.85, color='#6CF527', alpha=0.7,
       edgecolor='#6CF527', linewidth=1, label=f'Organic (μ={np.mean(organic):.0f}s)')

# Paid (bottom) - pink
h2, _ = np.histogram(paid, bins=bins, density=True)
ax.bar(centers, -h2, width=bin_width*0.85, color='#F5276C', alpha=0.7,
       edgecolor='#F5276C', linewidth=1, label=f'Paid (μ={np.mean(paid):.0f}s)')

ax.axhline(0, color='#555555', linewidth=2)

# Bounce rate threshold (< 30 seconds)
bounce_threshold = 30
ax.axvline(bounce_threshold, color='#ef4444', linestyle='--', linewidth=2, alpha=0.7)
ax.fill_betweenx([min(-h2), max(h1)], 0, bounce_threshold, alpha=0.1, color='#ef4444')

# Bounce rates
organic_bounce = (organic < bounce_threshold).mean() * 100
paid_bounce = (paid < bounce_threshold).mean() * 100

# Engagement score
organic_engaged = (organic > 120).mean() * 100
paid_engaged = (paid > 120).mean() * 100

# Stats annotations
ax.text(bounce_threshold + 10, max(h1)*0.9, f'Bounce: {organic_bounce:.0f}%', 
        color='#6CF527', fontsize=10, fontweight='bold')
ax.text(bounce_threshold + 10, min(-h2)*0.9, f'Bounce: {paid_bounce:.0f}%', 
        color='#F5276C', fontsize=10, fontweight='bold')

# Engagement quality
quality_diff = organic_engaged - paid_engaged
stats = f"Engaged Users (>2min): Organic {organic_engaged:.0f}% vs Paid {paid_engaged:.0f}% | Δ = +{quality_diff:.0f}%"
ax.text(0.5, 0.98, stats, transform=ax.transAxes, ha='center', va='top',
        fontsize=10, color='#6CF527', fontfamily='monospace',
        bbox=dict(boxstyle='round,pad=0.4', facecolor=BG_COLOR, edgecolor='#6CF527', lw=2))

ax.set_xlabel('Session Duration (seconds)', fontsize=12, color='white', fontweight='500')
ax.set_title('Traffic Quality: Organic vs Paid', fontsize=16, color='white', fontweight='bold', pad=25)

ax.tick_params(colors='#888888', labelsize=10)
ax.set_yticks([])
for spine in ['top', 'right']:
    ax.spines[spine].set_visible(False)
for spine in ['bottom', 'left']:
    ax.spines[spine].set_color('#333333')
ax.legend(loc='upper right', facecolor=BG_COLOR, edgecolor='#333333', labelcolor='white', fontsize=10)

plt.tight_layout()
plt.show()
Library

Matplotlib

Category

Statistical

Did this help you?

Support PyLucid to keep it free & growing

Support