Monitoring Your MVP: Building a Simple but Powerful Analytics Dashboard with Python


In the fast-paced world of startups and product development, launching a Minimum Viable Product (MVP) is just the beginning. What separates successful products from those that fizzle out is often how well teams monitor, understand, and react to user behavior. In this post, we'll walk through building a simple yet powerful analytics dashboard using Python that will help you track the metrics that matter most for your MVP.
Why Build Your Own Dashboard?
While there are plenty of off-the-shelf analytics solutions available, creating a custom dashboard offers several advantages:
Complete control over your data - You own it all, no third-party dependencies
Customized to your specific KPIs - Track exactly what matters to your product
Cost-effective - Save money during your early startup stages
A learning opportunity - Gain valuable data analysis skills
What We'll Build
Our analytics dashboard will:
Track key user engagement metrics
Visualize conversion funnels
Monitor system performance
Alert you to unusual patterns
Present everything in an easy-to-understand web interface
Prerequisites
Basic Python knowledge
Familiarity with SQL for data queries
Your MVP should be collecting basic usage data
A development environment with Python 3.8+
Step 1: Setting Up Your Data Pipeline
Before we can visualize anything, we need to collect and structure our data. For an MVP, I recommend a simple setup:
import pandas as pd
import sqlite3
from datetime import datetime, timedelta
# Connect to your database
# This could be SQLite for simplicity or PostgreSQL/MySQL for production
conn = sqlite3.connect('mvp_analytics.db')
def collect_daily_metrics():
"""Gather key metrics from your database into a DataFrame"""
query = """
SELECT
date(created_at) as day,
COUNT(DISTINCT user_id) as daily_active_users,
COUNT(*) as total_actions,
SUM(CASE WHEN action_type = 'signup' THEN 1 ELSE 0 END) as signups,
SUM(CASE WHEN action_type = 'purchase' THEN 1 ELSE 0 END) as purchases
FROM user_actions
WHERE created_at >= date('now', '-30 days')
GROUP BY date(created_at)
ORDER BY day
"""
return pd.read_sql_query(query, conn)
# Example usage
daily_metrics = collect_daily_metrics()
print(daily_metrics.head())
This function pulls the last 30 days of user activity from our database, aggregating by day. Customize the SQL query to match your database schema and the specific metrics you want to track.
Step 2: Building Core Metric Functions
Next, let's create functions for calculating the key metrics most MVPs should track:
def calculate_retention(days_back=30, cohort_days=7):
"""Calculate user retention by cohort"""
query = """
WITH first_seen AS (
SELECT
user_id,
date(MIN(created_at)) as first_day
FROM user_actions
GROUP BY user_id
),
daily_activity AS (
SELECT
user_id,
date(created_at) as activity_day
FROM user_actions
GROUP BY user_id, date(created_at)
)
SELECT
first_day as cohort,
COUNT(DISTINCT fs.user_id) as cohort_size,
SUM(CASE WHEN julianday(da.activity_day) - julianday(fs.first_day) BETWEEN 0 AND 6 THEN 1 ELSE 0 END) as week_1,
SUM(CASE WHEN julianday(da.activity_day) - julianday(fs.first_day) BETWEEN 7 AND 13 THEN 1 ELSE 0 END) as week_2,
SUM(CASE WHEN julianday(da.activity_day) - julianday(fs.first_day) BETWEEN 14 AND 20 THEN 1 ELSE 0 END) as week_3,
SUM(CASE WHEN julianday(da.activity_day) - julianday(fs.first_day) BETWEEN 21 AND 27 THEN 1 ELSE 0 END) as week_4
FROM first_seen fs
LEFT JOIN daily_activity da ON fs.user_id = da.user_id
WHERE fs.first_day >= date('now', ? || ' days')
GROUP BY fs.first_day
ORDER BY fs.first_day
"""
retention_data = pd.read_sql_query(query, conn, params=[f'-{days_back}'])
# Convert to percentages
for week in ['week_1', 'week_2', 'week_3', 'week_4']:
retention_data[week] = (retention_data[week] / retention_data['cohort_size'] * 100).round(1)
return retention_data
def calculate_conversion_funnel():
"""Calculate conversion between key user journey steps"""
query = """
WITH user_journey AS (
SELECT
user_id,
MAX(CASE WHEN action_type = 'visit' THEN 1 ELSE 0 END) as reached_visit,
MAX(CASE WHEN action_type = 'signup' THEN 1 ELSE 0 END) as reached_signup,
MAX(CASE WHEN action_type = 'activation' THEN 1 ELSE 0 END) as reached_activation,
MAX(CASE WHEN action_type = 'purchase' THEN 1 ELSE 0 END) as reached_purchase
FROM user_actions
WHERE created_at >= date('now', '-30 days')
GROUP BY user_id
)
SELECT
SUM(reached_visit) as visits,
SUM(reached_signup) as signups,
SUM(reached_activation) as activations,
SUM(reached_purchase) as purchases
FROM user_journey
"""
funnel_data = pd.read_sql_query(query, conn)
# Reshape for easier visualization
funnel_steps = pd.DataFrame({
'step': ['Visit', 'Signup', 'Activation', 'Purchase'],
'count': [funnel_data['visits'][0], funnel_data['signups'][0],
funnel_data['activations'][0], funnel_data['purchases'][0]]
})
# Calculate conversion rates
funnel_steps['conversion_rate'] = (funnel_steps['count'] / funnel_steps['count'].shift(1) * 100).round(1)
funnel_steps.loc[0, 'conversion_rate'] = 100.0 # First step is always 100%
return funnel_steps
These functions handle two critical metrics:
User retention - How many users come back over time
Conversion funnel - How users progress through your key workflows
Step 3: Creating Data Visualizations
Now let's visualize our metrics using matplotlib and seaborn:
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
def plot_daily_metrics(metrics_df):
"""Create a time series plot of daily metrics"""
plt.figure(figsize=(12, 6))
# Plot daily active users
ax1 = plt.subplot(111)
ax1.plot(metrics_df['day'], metrics_df['daily_active_users'], 'b-', label='Daily Active Users')
ax1.set_xlabel('Date')
ax1.set_ylabel('Users', color='b')
ax1.tick_params('y', colors='b')
# Plot actions on a secondary y-axis
ax2 = ax1.twinx()
ax2.plot(metrics_df['day'], metrics_df['total_actions'], 'r-', label='Total Actions')
ax2.set_ylabel('Actions', color='r')
ax2.tick_params('y', colors='r')
# Add a legend
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')
plt.title('Daily User Activity')
plt.tight_layout()
plt.savefig('static/daily_metrics.png')
plt.close()
def plot_retention_heatmap(retention_df):
"""Create a heatmap of cohort retention"""
# Prepare data for heatmap
retention_matrix = retention_df.set_index('cohort')
retention_matrix = retention_matrix.drop('cohort_size', axis=1)
plt.figure(figsize=(10, 8))
sns.heatmap(retention_matrix, annot=True, cmap='YlGnBu', fmt='.1f',
cbar_kws={'label': 'Retention %'})
plt.title('User Retention by Cohort')
plt.tight_layout()
plt.savefig('static/retention_heatmap.png')
plt.close()
def plot_conversion_funnel(funnel_df):
"""Create a conversion funnel visualization"""
plt.figure(figsize=(10, 6))
# Plot the funnel
sns.barplot(x='step', y='count', data=funnel_df, color='skyblue')
# Add conversion percentages
for i, row in funnel_df.iterrows():
plt.text(i, row['count'] / 2, f"{row['conversion_rate']}%",
ha='center', va='center', color='white', fontweight='bold')
if i > 0:
plt.text(i - 0.5, (row['count'] + funnel_df.loc[i-1, 'count']) / 2,
f"→ {row['count'] / funnel_df.loc[i-1, 'count'] * 100:.1f}%",
ha='center', va='center', rotation=90)
plt.title('User Conversion Funnel')
plt.ylabel('Number of Users')
plt.tight_layout()
plt.savefig('static/conversion_funnel.png')
plt.close()
These functions create three key visualizations:
A time series of daily active users and actions
A cohort retention heatmap
A conversion funnel with conversion rates between steps
Step 4: Building a Web Dashboard with Flask
Let's put everything together in a simple web application using Flask:
from flask import Flask, render_template
import os
app = Flask(__name__)
@app.route('/')
def dashboard():
# Make sure our static directory exists
os.makedirs('static', exist_ok=True)
# Generate our analytics visualizations
daily_data = collect_daily_metrics()
plot_daily_metrics(daily_data)
retention_data = calculate_retention()
plot_retention_heatmap(retention_data)
funnel_data = calculate_conversion_funnel()
plot_conversion_funnel(funnel_data)
# Calculate summary metrics for the dashboard
current_dau = daily_data.iloc[-1]['daily_active_users']
previous_dau = daily_data.iloc[-2]['daily_active_users']
dau_growth = ((current_dau - previous_dau) / previous_dau * 100).round(1)
last_7_days = daily_data.iloc[-7:].sum()
weekly_active_users = last_7_days['daily_active_users']
conversion_rate = (funnel_data.iloc[-1]['count'] / funnel_data.iloc[0]['count'] * 100).round(1)
summary_stats = {
'daily_active_users': current_dau,
'dau_growth': dau_growth,
'weekly_active_users': weekly_active_users,
'conversion_rate': conversion_rate
}
return render_template('dashboard.html', stats=summary_stats)
if __name__ == '__main__':
app.run(debug=True)
Step 5: Creating the Dashboard Template
Create a templates
folder with a dashboard.html
file:
<!DOCTYPE html>
<html>
<head>
<title>MVP Analytics Dashboard</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
<style>
.metric-card {
padding: 20px;
border-radius: 5px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.metric-value {
font-size: 2em;
font-weight: bold;
}
.growth-positive {
color: #28a745;
}
.growth-negative {
color: #dc3545;
}
.chart-card {
padding: 20px;
border-radius: 5px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container mt-4">
<h1 class="mb-4">MVP Analytics Dashboard</h1>
<div class="row">
<div class="col-md-3">
<div class="metric-card bg-light">
<h5>Daily Active Users</h5>
<div class="metric-value">{{ stats.daily_active_users }}</div>
<div class="growth
{% if stats.dau_growth > 0 %}growth-positive{% else %}growth-negative{% endif %}">
{% if stats.dau_growth > 0 %}+{% endif %}{{ stats.dau_growth }}% from yesterday
</div>
</div>
</div>
<div class="col-md-3">
<div class="metric-card bg-light">
<h5>Weekly Active Users</h5>
<div class="metric-value">{{ stats.weekly_active_users }}</div>
</div>
</div>
<div class="col-md-3">
<div class="metric-card bg-light">
<h5>Visit-to-Purchase</h5>
<div class="metric-value">{{ stats.conversion_rate }}%</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="chart-card">
<h4>Daily User Activity</h4>
<img src="/static/daily_metrics.png" class="img-fluid">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="chart-card">
<h4>User Retention</h4>
<img src="/static/retention_heatmap.png" class="img-fluid">
</div>
</div>
<div class="col-md-6">
<div class="chart-card">
<h4>Conversion Funnel</h4>
<img src="/static/conversion_funnel.png" class="img-fluid">
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
Step 6: Setting Up Automated Reports
Let's add a scheduler to run daily reports and send them via email:
import schedule
import time
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
def send_daily_report():
"""Generate and email a daily analytics report"""
# Generate fresh data and charts
daily_data = collect_daily_metrics()
plot_daily_metrics(daily_data)
funnel_data = calculate_conversion_funnel()
plot_conversion_funnel(funnel_data)
# Calculate key metrics
current_dau = daily_data.iloc[-1]['daily_active_users']
previous_dau = daily_data.iloc[-2]['daily_active_users']
dau_growth = ((current_dau - previous_dau) / previous_dau * 100).round(1)
# Prepare email
msg = MIMEMultipart()
msg['Subject'] = f'Daily MVP Analytics Report - {datetime.now().strftime("%Y-%m-%d")}'
msg['From'] = 'your-email@example.com'
msg['To'] = 'team@yourstartup.com'
# Email body
email_body = f"""
<html>
<body>
<h2>Daily MVP Analytics Report</h2>
<p>Here are your key metrics for today:</p>
<ul>
<li>Daily Active Users: {current_dau} ({dau_growth:+.1f}% from yesterday)</li>
<li>New Signups: {daily_data.iloc[-1]['signups']}</li>
<li>Purchases: {daily_data.iloc[-1]['purchases']}</li>
</ul>
<h3>Daily Activity Chart</h3>
<img src="cid:daily_metrics">
<h3>Conversion Funnel</h3>
<img src="cid:conversion_funnel">
</body>
</html>
"""
msg.attach(MIMEText(email_body, 'html'))
# Attach images
with open('static/daily_metrics.png', 'rb') as f:
img = MIMEImage(f.read())
img.add_header('Content-ID', '<daily_metrics>')
msg.attach(img)
with open('static/conversion_funnel.png', 'rb') as f:
img = MIMEImage(f.read())
img.add_header('Content-ID', '<conversion_funnel>')
msg.attach(img)
# Send email (configure with your SMTP settings)
with smtplib.SMTP('smtp.example.com', 587) as server:
server.starttls()
server.login('your-email@example.com', 'password')
server.send_message(msg)
# Schedule the report to run every day at 6am
schedule.every().day.at("06:00").do(send_daily_report)
def run_scheduler():
"""Run scheduled tasks in a separate thread"""
while True:
schedule.run_pending()
time.sleep(60)
# You can start this in a separate thread
# import threading
# scheduler_thread = threading.Thread(target=run_scheduler)
# scheduler_thread.daemon = True
# scheduler_thread.start()
Step 7: Setting Up Simple Anomaly Detection
Let's add a basic anomaly detection system to alert you of unusual patterns:
def detect_anomalies():
"""Detect unusual patterns in your metrics"""
daily_data = collect_daily_metrics()
# Calculate 7-day moving average and standard deviation
daily_data['rolling_avg'] = daily_data['daily_active_users'].rolling(window=7).mean()
daily_data['rolling_std'] = daily_data['daily_active_users'].rolling(window=7).std()
# Flag days that are more than 2 standard deviations from the moving average
daily_data['is_anomaly'] = abs(daily_data['daily_active_users'] - daily_data['rolling_avg']) > (2 * daily_data['rolling_std'])
# Get today's data
today = daily_data.iloc[-1]
if today['is_anomaly']:
deviation = (today['daily_active_users'] - today['rolling_avg']) / today['rolling_avg'] * 100
direction = "higher" if deviation > 0 else "lower"
# Send an alert (this could be an email, SMS, Slack message, etc.)
message = f"ALERT: Today's user activity is {abs(deviation):.1f}% {direction} than expected!"
print(message) # Replace with your preferred notification method
return True, message
return False, None
Conclusion
Building your own analytics dashboard gives you deeper insights into your MVP's performance without breaking the bank. As your product evolves, you can extend this framework to track more sophisticated metrics, implement A/B testing, or integrate with additional data sources.
The most important aspects to monitor for most MVPs are:
User Engagement - Are people using your product? How often?
Retention - Do users come back after their first visit?
Conversion - Are users completing key workflows?
Feature Usage - Which features get used most/least?
Performance - Is your product stable and responsive?
Remember, the goal isn't just to collect data, but to act on it. Schedule regular meetings with your team to review the dashboard, identify trends, and make data-driven decisions to improve your product.
Next Steps
Once you've mastered the basics, consider:
Adding user segmentation to understand different user behaviors
Implementing event tracking to monitor specific feature usage
Setting up automated A/B testing
Integrating with external data sources like marketing campaigns
Happy monitoring!
Subscribe to my newsletter
Read articles from Johnny Ngare directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
