User Engagement Score

Composite metric measuring how actively and meaningfully users interact with your product

Overview

User Engagement Score is a composite metric that quantifies how actively and meaningfully users interact with your product. It combines multiple behavioral indicators including activity frequency, feature usage depth, session duration, and recency to provide a holistic view of user engagement levels.

This metric is essential for product-led growth strategies, helping teams identify highly engaged users for expansion opportunities, detect at-risk users for retention efforts, and optimize product experiences to drive deeper engagement across the user base.

Formula:
Engagement Score = Weighted Average of (Activity Score + Frequency Score + Depth Score + Recency Score)

Scoring Approach

Weight each component based on your product type and business model. B2B SaaS might emphasize depth and frequency, while consumer apps might prioritize activity volume and recency.

Why It Matters

  • Product Development: Guides feature prioritization and product roadmap decisions
  • User Segmentation: Enables targeted campaigns and personalized experiences
  • Retention Prediction: Identifies users at risk of churning early
  • Expansion Opportunities: Finds high-engagement users ready for upselling
  • Product-Market Fit: Indicates how well the product resonates with users
  • Customer Success: Enables proactive outreach and support

How to Measure It

Calculate user engagement scores by analyzing behavioral data across multiple dimensions, creating weighted scores for each component, and combining them into a comprehensive engagement metric.

Basic Engagement Score Calculation

-- Calculate comprehensive user engagement scores
WITH user_activity AS (
  SELECT 
    user_id,
    DATE_TRUNC('month', event_timestamp) as activity_month,
    COUNT(*) as total_events,
    COUNT(DISTINCT DATE(event_timestamp)) as active_days,
    COUNT(DISTINCT session_id) as total_sessions,
    COUNT(DISTINCT event_type) as unique_event_types,
    COUNT(DISTINCT feature_name) as features_used,
    AVG(session_duration_minutes) as avg_session_duration,
    MAX(event_timestamp) as last_activity_date,
    MIN(event_timestamp) as first_activity_date,
    COUNT(CASE WHEN event_type = 'core_action' THEN 1 END) as core_actions,
    COUNT(CASE WHEN event_type = 'advanced_feature' THEN 1 END) as advanced_actions
  FROM user_events
  WHERE event_timestamp >= CURRENT_DATE - INTERVAL '30 days'
    AND user_id IS NOT NULL
  GROUP BY user_id, DATE_TRUNC('month', event_timestamp)
),
user_profiles AS (
  SELECT 
    u.user_id,
    u.signup_date,
    u.user_tier,
    u.industry,
    EXTRACT(days FROM AGE(CURRENT_DATE, u.signup_date)) as account_age_days
  FROM users u
  WHERE u.signup_date <= CURRENT_DATE - INTERVAL '7 days'  -- Exclude very new users
),
engagement_scores AS (
  SELECT 
    ua.user_id,
    ua.activity_month,
    ua.total_events,
    ua.active_days,
    ua.total_sessions,
    ua.features_used,
    ua.avg_session_duration,
    ua.last_activity_date,
    EXTRACT(days FROM AGE(CURRENT_DATE, ua.last_activity_date)) as days_since_last_activity,
    
    -- Activity Score (0-25): Based on total events normalized
    CASE 
      WHEN ua.total_events >= 100 THEN 25
      WHEN ua.total_events >= 50 THEN 20
      WHEN ua.total_events >= 20 THEN 15
      WHEN ua.total_events >= 10 THEN 10
      WHEN ua.total_events >= 5 THEN 5
      ELSE 0
    END as activity_score,
    
    -- Frequency Score (0-25): Based on active days in month
    CASE 
      WHEN ua.active_days >= 20 THEN 25
      WHEN ua.active_days >= 15 THEN 20
      WHEN ua.active_days >= 10 THEN 15
      WHEN ua.active_days >= 5 THEN 10
      WHEN ua.active_days >= 2 THEN 5
      ELSE 0
    END as frequency_score,
    
    -- Depth Score (0-25): Based on feature usage breadth
    CASE 
      WHEN ua.features_used >= 10 THEN 25
      WHEN ua.features_used >= 8 THEN 20
      WHEN ua.features_used >= 6 THEN 15
      WHEN ua.features_used >= 4 THEN 10
      WHEN ua.features_used >= 2 THEN 5
      ELSE 0
    END as depth_score,
    
    -- Recency Score (0-25): Based on days since last activity
    CASE 
      WHEN EXTRACT(days FROM AGE(CURRENT_DATE, ua.last_activity_date)) <= 1 THEN 25
      WHEN EXTRACT(days FROM AGE(CURRENT_DATE, ua.last_activity_date)) <= 3 THEN 20
      WHEN EXTRACT(days FROM AGE(CURRENT_DATE, ua.last_activity_date)) <= 7 THEN 15
      WHEN EXTRACT(days FROM AGE(CURRENT_DATE, ua.last_activity_date)) <= 14 THEN 10
      WHEN EXTRACT(days FROM AGE(CURRENT_DATE, ua.last_activity_date)) <= 30 THEN 5
      ELSE 0
    END as recency_score,
    
    up.user_tier,
    up.account_age_days
  FROM user_activity ua
  JOIN user_profiles up ON ua.user_id = up.user_id
),
final_scores AS (
  SELECT 
    *,
    activity_score + frequency_score + depth_score + recency_score as total_engagement_score,
    CASE 
      WHEN activity_score + frequency_score + depth_score + recency_score >= 80 THEN 'High'
      WHEN activity_score + frequency_score + depth_score + recency_score >= 60 THEN 'Medium'
      WHEN activity_score + frequency_score + depth_score + recency_score >= 20 THEN 'Low'
      ELSE 'At Risk'
    END as engagement_tier,
    NTILE(10) OVER (ORDER BY activity_score + frequency_score + depth_score + recency_score DESC) as engagement_decile
  FROM engagement_scores
)
SELECT 
  user_id,
  activity_month,
  total_events,
  active_days,
  features_used,
  days_since_last_activity,
  activity_score,
  frequency_score,
  depth_score,
  recency_score,
  total_engagement_score,
  engagement_tier,
  engagement_decile,
  user_tier,
  account_age_days,
  RANK() OVER (PARTITION BY engagement_tier ORDER BY total_engagement_score DESC) as tier_rank
FROM final_scores
ORDER BY total_engagement_score DESC;

Engagement Trend Analysis

-- Analyze engagement score trends and patterns over time
WITH monthly_scores AS (
  SELECT 
    user_id,
    activity_month,
    total_engagement_score,
    engagement_tier,
    LAG(total_engagement_score) OVER (PARTITION BY user_id ORDER BY activity_month) as prev_month_score,
    LAG(engagement_tier) OVER (PARTITION BY user_id ORDER BY activity_month) as prev_month_tier,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY activity_month) as month_sequence
  FROM final_scores
  WHERE activity_month >= CURRENT_DATE - INTERVAL '6 months'
),
engagement_trends AS (
  SELECT 
    user_id,
    COUNT(*) as months_tracked,
    MIN(total_engagement_score) as min_score,
    MAX(total_engagement_score) as max_score,
    ROUND(AVG(total_engagement_score), 1) as avg_score,
    ROUND(STDDEV(total_engagement_score), 1) as score_volatility,
    MAX(total_engagement_score) - MIN(total_engagement_score) as score_range,
    ARRAY_AGG(engagement_tier ORDER BY activity_month) as tier_journey,
    COUNT(CASE WHEN total_engagement_score > prev_month_score THEN 1 END) as improving_months,
    COUNT(CASE WHEN total_engagement_score < prev_month_score THEN 1 END) as declining_months,
    CASE 
      WHEN COUNT(CASE WHEN total_engagement_score > prev_month_score THEN 1 END) > 
           COUNT(CASE WHEN total_engagement_score < prev_month_score THEN 1 END) THEN 'Improving'
      WHEN COUNT(CASE WHEN total_engagement_score > prev_month_score THEN 1 END) < 
           COUNT(CASE WHEN total_engagement_score < prev_month_score THEN 1 END) THEN 'Declining'
      ELSE 'Stable'
    END as trend_direction
  FROM monthly_scores
  WHERE prev_month_score IS NOT NULL
  GROUP BY user_id
  HAVING COUNT(*) >= 3  -- Users with at least 3 months of data
),
cohort_engagement AS (
  SELECT 
    DATE_TRUNC('month', up.signup_date) as signup_cohort,
    EXTRACT(months FROM AGE(fs.activity_month, up.signup_date)) as months_since_signup,
    COUNT(*) as users_in_cohort,
    ROUND(AVG(fs.total_engagement_score), 1) as avg_engagement_score,
    COUNT(CASE WHEN fs.engagement_tier = 'High' THEN 1 END) as high_engagement_users,
    COUNT(CASE WHEN fs.engagement_tier = 'At Risk' THEN 1 END) as at_risk_users,
    ROUND(100.0 * COUNT(CASE WHEN fs.engagement_tier = 'High' THEN 1 END) / COUNT(*), 1) as high_engagement_rate,
    ROUND(100.0 * COUNT(CASE WHEN fs.engagement_tier = 'At Risk' THEN 1 END) / COUNT(*), 1) as at_risk_rate
  FROM final_scores fs
  JOIN user_profiles up ON fs.user_id = up.user_id
  WHERE months_since_signup <= 12  -- First year analysis
  GROUP BY 1, 2
  HAVING COUNT(*) >= 20  -- Minimum cohort size
)
SELECT 
  signup_cohort,
  months_since_signup,
  users_in_cohort,
  avg_engagement_score,
  high_engagement_users,
  at_risk_users,
  high_engagement_rate,
  at_risk_rate,
  LAG(avg_engagement_score) OVER (PARTITION BY signup_cohort ORDER BY months_since_signup) as prev_month_avg,
  ROUND(avg_engagement_score - LAG(avg_engagement_score) OVER (PARTITION BY signup_cohort ORDER BY months_since_signup), 1) as score_change
FROM cohort_engagement
ORDER BY signup_cohort DESC, months_since_signup ASC;

Feature-Level Engagement Analysis

-- Analyze engagement patterns by feature usage
WITH feature_engagement AS (
  SELECT 
    fs.user_id,
    fs.total_engagement_score,
    fs.engagement_tier,
    ue.feature_name,
    COUNT(*) as feature_usage_count,
    COUNT(DISTINCT DATE(ue.event_timestamp)) as days_used_feature,
    MAX(ue.event_timestamp) as last_feature_use,
    MIN(ue.event_timestamp) as first_feature_use,
    AVG(ue.session_duration_minutes) as avg_session_duration_with_feature
  FROM final_scores fs
  JOIN user_events ue ON fs.user_id = ue.user_id 
    AND DATE_TRUNC('month', ue.event_timestamp) = fs.activity_month
  WHERE ue.feature_name IS NOT NULL
  GROUP BY 1, 2, 3, 4
),
feature_engagement_correlation AS (
  SELECT 
    feature_name,
    COUNT(DISTINCT user_id) as total_users,
    COUNT(CASE WHEN engagement_tier = 'High' THEN user_id END) as high_engagement_users,
    COUNT(CASE WHEN engagement_tier = 'At Risk' THEN user_id END) as at_risk_users,
    ROUND(AVG(total_engagement_score), 1) as avg_user_engagement_score,
    ROUND(AVG(feature_usage_count), 1) as avg_feature_usage,
    ROUND(100.0 * COUNT(CASE WHEN engagement_tier = 'High' THEN user_id END) / COUNT(DISTINCT user_id), 1) as high_engagement_rate,
    CORR(feature_usage_count, total_engagement_score) as usage_score_correlation,
    ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY feature_usage_count), 1) as median_usage_count
  FROM feature_engagement
  GROUP BY feature_name
  HAVING COUNT(DISTINCT user_id) >= 50  -- Minimum user threshold
),
engagement_by_feature_adoption AS (
  SELECT 
    user_id,
    total_engagement_score,
    engagement_tier,
    COUNT(DISTINCT feature_name) as total_features_used,
    ARRAY_AGG(DISTINCT feature_name ORDER BY feature_usage_count DESC) as top_features,
    SUM(feature_usage_count) as total_feature_interactions,
    STRING_AGG(
      feature_name || ':' || feature_usage_count::text, 
      ', ' ORDER BY feature_usage_count DESC
    ) as feature_usage_breakdown
  FROM feature_engagement
  GROUP BY 1, 2, 3
),
feature_adoption_patterns AS (
  SELECT 
    total_features_used,
    COUNT(*) as users_count,
    ROUND(AVG(total_engagement_score), 1) as avg_engagement_score,
    COUNT(CASE WHEN engagement_tier = 'High' THEN 1 END) as high_engagement_users,
    ROUND(100.0 * COUNT(CASE WHEN engagement_tier = 'High' THEN 1 END) / COUNT(*), 1) as high_engagement_rate
  FROM engagement_by_feature_adoption
  GROUP BY total_features_used
  ORDER BY total_features_used DESC
)
SELECT 
  fec.feature_name,
  fec.total_users,
  fec.high_engagement_users,
  fec.high_engagement_rate,
  fec.avg_user_engagement_score,
  fec.avg_feature_usage,
  ROUND(fec.usage_score_correlation, 3) as correlation_with_engagement,
  fec.median_usage_count,
  RANK() OVER (ORDER BY fec.high_engagement_rate DESC) as feature_engagement_rank,
  CASE 
    WHEN fec.usage_score_correlation >= 0.3 THEN 'Strong Positive'
    WHEN fec.usage_score_correlation >= 0.1 THEN 'Moderate Positive'
    WHEN fec.usage_score_correlation >= -0.1 THEN 'Neutral'
    ELSE 'Negative'
  END as engagement_correlation_strength
FROM feature_engagement_correlation fec
ORDER BY fec.high_engagement_rate DESC, fec.usage_score_correlation DESC;

Measurement Best Practices

Customize scoring weights based on your product and business model, track scores over time to identify trends, and segment users by engagement levels for targeted interventions and product improvements.

Best Practices

1. Score Definition

  • Define meaningful actions that indicate true engagement
  • Weight components based on business importance
  • Account for different user types and use cases
  • Normalize scores for fair comparison across segments

2. Data Collection

  • Track granular user actions and feature interactions
  • Capture session context and user intent
  • Ensure consistent event tracking across platforms
  • Include qualitative feedback to validate scores

3. Segmentation Strategy

  • Power Users: High engagement for expansion opportunities
  • Regular Users: Medium engagement for optimization
  • Casual Users: Low engagement for activation campaigns
  • At-Risk Users: Very low engagement for retention efforts

4. Optimization Tactics

  • Improve onboarding to drive initial engagement
  • Create engagement loops and habit-forming features
  • Personalize experiences based on usage patterns
  • Remove friction from high-value actions

5. Intervention Programs

  • Automated campaigns for different engagement tiers
  • Proactive customer success outreach
  • Feature education and training programs
  • Gamification and incentive programs