Lesson 58 · Social Media Content Analytics
Data Storytelling for Content Creators
In this lesson, we will solve real content analytics problems for creators and marketers. We will learn how to use social media data to uncover stories…
- CourseSocial Media Content Analytics
- Lesson58 of 41
- Video31 min
- FormatJupyter notebook · 25 code cells
What you'll learn
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbData Storytelling for Content Creators#
- In this lesson, we will solve real content analytics problems for creators and marketers.
- We will learn how to use social media data to uncover stories behind what makes content successful.
- You will explore engagement metrics, trends, and patterns that help you make better content decisions.
- By the end, you will analyze real datasets to produce insights and recommendations for your strategy.
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')
Understanding Social Media Datasets and Engagement Metrics#
- Social media datasets often represent posts, videos, and their associated engagement (likes, views, comments, shares, etc).
- Metrics such as views, likes, comments, click-through rate (CTR), and watch time help us evaluate performance and audience interest.
- Beginners sometimes misinterpret metrics by only looking at big numbers rather than actual rates or impact.
- Comparing raw engagement counts without considering context (such as audience size or post reach) may be misleading.
# Beginner 1: Load a synthetic Social Media Content Dataset
np.random.seed(42)
n_posts = 500
views = np.random.randint(100, 100000, n_posts)
df = pd.DataFrame({
'post_id': range(1, n_posts+1),
'platform': np.random.choice(['YouTube','Instagram','TikTok'], n_posts),
'date': pd.date_range('2023-01-01', periods=n_posts, freq='6h'),
'views': views,
'likes': (views * np.random.uniform(0.02, 0.15, n_posts)).astype(int),
'comments': (views * np.random.uniform(0.001, 0.05, n_posts)).astype(int),
'shares': (views * np.random.uniform(0.001, 0.03, n_posts)).astype(int)
})
print(df.shape)
print(df.head(3))
# Beginner 2: Calculate engagement rate for each post
df['engagement_rate'] = ((df['likes'] + df['comments'] + df['shares']) / df['views']) * 100
print(df[['post_id', 'platform', 'views', 'likes', 'comments', 'shares', 'engagement_rate']].head(3))
# Beginner 3: Find the average engagement rate by platform
platform_engagement = df.groupby('platform')['engagement_rate'].mean().sort_values(ascending=False)
print(platform_engagement)
# Beginner 4: Identify the post with the highest engagement rate
top_post = df.loc[df['engagement_rate'].idxmax()]
print(top_post[['post_id', 'platform', 'views', 'likes', 'comments', 'shares', 'engagement_rate']])
# Beginner 5: Plot engagement rate distribution with matplotlib
import matplotlib.pyplot as plt
plt.figure(figsize=(8,4))
plt.hist(df['engagement_rate'], bins=30, color='skyblue', edgecolor='black')
plt.title('Distribution of Engagement Rates')
plt.xlabel('Engagement Rate (%)')
plt.ylabel('Number of Posts')
plt.grid(True, axis='y', alpha=0.5)
plt.tight_layout()
plt.savefig('engagement_rate_hist.png')
plt.show()
# Beginner 6: Compare engagement by posting hour
df['hour'] = df['date'].dt.hour
hourly_engagement = df.groupby('hour')['engagement_rate'].mean()
plt.figure(figsize=(8,4))
plt.plot(hourly_engagement.index, hourly_engagement.values, marker='o', linestyle='--', color='green')
plt.title('Average Engagement Rate by Posting Hour')
plt.xlabel('Hour of Day')
plt.ylabel('Average Engagement Rate (%)')
plt.grid(True, axis='y', alpha=0.5)
plt.tight_layout()
plt.savefig('hourly_engagement_rate.png')
plt.show()
# Intermediate 1: Load YouTube Trending Videos (synthetic or real) and preview
import os, pickle
from pathlib import Path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
SCOPES = ['https://www.googleapis.com/auth/youtube.readonly']
def get_yt_service():
api_key = os.environ.get('YOUTUBE_API_KEY')
if api_key:
return build('youtube', 'v3', developerKey=api_key)
if Path('client_secret.json').exists():
creds = None
if Path('token_ro.pickle').exists():
with open('token_ro.pickle', 'rb') as f:
creds = pickle.load(f)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file('client_secret.json', SCOPES)
creds = flow.run_local_server(port=0)
with open('token_ro.pickle', 'wb') as f:
pickle.dump(creds, f)
return build('youtube', 'v3', credentials=creds)
raise EnvironmentError('Set YOUTUBE_API_KEY or provide client_secret.json')
def fetch_yt_trending(max_results=200, region='US'):
youtube = get_yt_service()
records, token = [], None
while len(records) < max_results:
resp = youtube.videos().list(
part='snippet,statistics',
chart='mostPopular',
regionCode=region,
maxResults=min(50, max_results - len(records)),
pageToken=token
).execute()
for item in resp.get('items', []):
s = item['snippet']; st = item.get('statistics', {})
records.append({
'video_id': item['id'],
'trending_date': pd.Timestamp.today().date(),
'title': s.get('title', ''),
'channel_title': s.get('channelTitle', ''),
'category_id': s.get('categoryId', ''),
'views': int(st.get('viewCount', 0)),
'likes': int(st.get('likeCount', 0)),
'comment_count': int(st.get('commentCount', 0)),
})
token = resp.get('nextPageToken')
if not token: break
return pd.DataFrame(records)
try:
yt_df = fetch_yt_trending()
print('Live trending data:', yt_df.shape)
except Exception as e:
print(f'Falling back to synthetic: {e}')
np.random.seed(42)
n = 1000
yt_df = pd.DataFrame({
'video_id': [f'vid{i}' for i in range(n)],
'trending_date': pd.date_range('2023-01-01', periods=n, freq='D'),
'title': [f'Video Title {i}' for i in range(n)],
'channel_title': np.random.choice(['ChannelA','ChannelB','ChannelC'], n),
'category_id': np.random.choice([1,2,10,22,24,28], n),
'views': np.random.randint(10000, 5000000, n),
'likes': np.random.randint(100, 200000, n),
'comment_count': np.random.randint(10, 50000, n),
})
print('Synthetic fallback:', yt_df.shape)
print(yt_df.head(3))
# Intermediate 2: Calculate like-to-view and comment-to-view ratios for trending videos
yt_df['like_ratio'] = yt_df['likes'] / yt_df['views'] * 100
yt_df['comment_ratio'] = yt_df['comment_count'] / yt_df['views'] * 100
print(yt_df[['title', 'views', 'likes', 'like_ratio', 'comment_count', 'comment_ratio']].head(3))
# Intermediate 3: Top 5 trending videos with highest like ratios
top_like_videos = yt_df.sort_values('like_ratio', ascending=False).head(5)
print(top_like_videos[['title', 'channel_title', 'views', 'likes', 'like_ratio']])
# Intermediate 4: Top 5 trending videos with most comments
top_comment_videos = yt_df.sort_values('comment_count', ascending=False).head(5)
print(top_comment_videos[['title', 'channel_title', 'views', 'comment_count']])
# Intermediate 5: Analyze engagement by content category
cat_engagement = yt_df.groupby('category_id')['like_ratio'].mean().sort_values(ascending=False)
print(cat_engagement)
# Intermediate 6: Visualize top 5 categories by average like ratio
top_cat = cat_engagement.head(5)
plt.figure(figsize=(7,4))
plt.bar(top_cat.index.astype(str), top_cat.values, color='tomato', edgecolor='black')
plt.xlabel('Category ID')
plt.ylabel('Average Like Ratio (%)')
plt.title('Top 5 Video Categories by Like Ratio')
plt.tight_layout()
plt.savefig('top5_category_like_ratio.png')
plt.show()
# Advanced 1: Identify potential viral videos by quantile
threshold = yt_df['views'].quantile(0.99)
viral_videos = yt_df[yt_df['views'] >= threshold]
print(f'Number of potential viral videos (top 1% by views): {viral_videos.shape[0]}')
print(viral_videos[['title', 'channel_title', 'views', 'likes', 'comment_count']].head(3))
# Advanced 2: Analyze watch time vs view count on YouTube Analytics data
np.random.seed(42)
n_videos = 300
views = np.random.randint(100, 500000, n_videos)
yt_analysis = pd.DataFrame({
'video_id': range(1, n_videos+1),
'publish_date': pd.date_range('2022-01-01', periods=n_videos, freq='D'),
'views': views,
'watch_time': np.random.randint(1000, 500000, n_videos),
'likes': (views * np.random.uniform(0.01, 0.08, n_videos)).astype(int),
'comments': (views * np.random.uniform(0.001, 0.02, n_videos)).astype(int),
'ctr': np.round(np.random.uniform(2, 10, n_videos), 2)
})
plt.figure(figsize=(7,4))
plt.scatter(yt_analysis['views'], yt_analysis['watch_time'], alpha=0.5, color='purple')
plt.xlabel('Views')
plt.ylabel('Watch Time (minutes)')
plt.title('Views vs Watch Time for YouTube Videos')
plt.tight_layout()
plt.savefig('views_vs_watchtime.png')
plt.show()
# Advanced 3: Compute CTR quartiles and describe the outliers
ctr_quartiles = yt_analysis['ctr'].quantile([0.25, 0.5, 0.75])
outlier_ctr = yt_analysis[yt_analysis['ctr'] > ctr_quartiles[0.75]]
print('CTR Quartiles:', ctr_quartiles)
print('Outlier CTR videos (top 25%):')
print(outlier_ctr[['video_id', 'views', 'ctr']].head(5))
# Error Handling 1: Handling missing engagement values
test_df = df.copy()
test_df.loc[5:10, ['likes', 'comments']] = np.nan
na_rows = test_df[test_df[['likes', 'comments']].isnull().any(axis=1)]
print('Rows with missing engagement:')
print(na_rows[['post_id', 'likes', 'comments']])
test_df[['likes', 'comments']] = test_df[['likes', 'comments']].fillna(0)
print('After imputation of missing values:')
print(test_df.loc[5:10, ['post_id', 'likes', 'comments']])
# Error Handling 2: Incorrect aggregation can distort result
wrong_avg = df[['likes', 'comments', 'shares']].mean().mean()
right_avg = ((df['likes'] + df['comments'] + df['shares']) / 3).mean()
print(f'Incorrect simple mean of columns: {wrong_avg:.2f}')
print(f'Correct post-wise mean then average: {right_avg:.2f}')
# Error Handling 3: Misinterpreting ratios like CTR or engagement rate
wrong_ratio = df['likes'] / (df['likes'] + df['comments'] + 1)
correct_ratio = df['likes'] / df['views']
print('First 3 incorrect ratios:', wrong_ratio.head(3).round(3).tolist())
print('First 3 correct like-to-view ratios:', correct_ratio.head(3).round(3).tolist())
# Error Handling 4: Wrong grouping logic for content categories
try:
wrong_group = yt_df.groupby('title')['like_ratio'].mean()
print('Grouped by unique title (not recommended):')
print(wrong_group.head(3))
correct_group = yt_df.groupby('category_id')['like_ratio'].mean()
print('\nGrouped by category_id (best practice):')
print(correct_group.head(3))
except Exception as e:
print('Error grouping:', e)
Best Practices and Analytics Patterns for Effective Data Storytelling#
- Always define your metrics clearly (for example, what counts as a view or an engaged user).
- Benchmark performance by comparing to similar content from yourself or others.
- Segment your audience and content (such as by platform, category, or time) for deeper insights.
- Track trends and growth rates, not just totals.
- Optimize your content based on actual engagement and audience signals, not assumptions.
# Content Benchmarking Example: How does your average post compare to the best?
top10 = df.sort_values('engagement_rate', ascending=False).head(10)
benchmark = top10['engagement_rate'].mean()
overall = df['engagement_rate'].mean()
print(f'Benchmark (top 10 posts): {benchmark:.2f}%')
print(f'Your overall average: {overall:.2f}%')
print(f'Improvement potential: {benchmark - overall:.2f} percentage points')
# Audience segmentation: Analyze engagement by platform
platform_stats = df.groupby('platform')[['engagement_rate', 'views']].mean()
print(platform_stats.round(2))
# Trend and growth analysis: Rolling engagement rate average over time
df_sorted = df.sort_values('date')
df_sorted['rolling_engagement'] = df_sorted['engagement_rate'].rolling(window=20, min_periods=5).mean()
plt.figure(figsize=(9,4))
plt.plot(df_sorted['date'], df_sorted['rolling_engagement'], color='orange')
plt.title('Rolling Engagement Rate Trend (20 posts)')
plt.xlabel('Date')
plt.ylabel('Engagement Rate (rolling avg)')
plt.tight_layout()
plt.savefig('rolling_engagement_trend.png')
plt.show()
# Tiny End-to-End Problem: Find your optimal content strategy
combined = df.copy()
hour_perf = combined.groupby('hour')['engagement_rate'].mean()
best_hour = hour_perf.idxmax()
best_platform = combined.groupby('platform')['engagement_rate'].mean().idxmax()
your_recommendation = f'Try posting on {best_platform} at {best_hour}:00 for maximum engagement.'
print('Content Strategy Recommendation:')
print(your_recommendation)
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



