Mathew K Analytics

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…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

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 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))
(500, 7)
   post_id   platform                date  views  likes  comments  shares
0        1  Instagram 2023-01-01 00:00:00  15895   2356       116     253
1        2  Instagram 2023-01-01 06:00:00    960     94        16      26
2        3  Instagram 2023-01-01 12:00:00  76920   3910      2879    1185
# 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))
   post_id   platform  views  likes  comments  shares  engagement_rate
0        1  Instagram  15895   2356       116     253        17.143756
1        2  Instagram    960     94        16      26        14.166667
2        3  Instagram  76920   3910      2879    1185        10.366615
# Beginner 3: Find the average engagement rate by platform
platform_engagement = df.groupby('platform')['engagement_rate'].mean().sort_values(ascending=False)
print(platform_engagement)
platform
Instagram    13.044316
TikTok       12.690745
YouTube      12.163010
Name: engagement_rate, dtype: float64
# 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']])
post_id                  352
platform           Instagram
views                  74643
likes                  10653
comments                3569
shares                  2036
engagement_rate    21.781011
Name: 351, dtype: object
# 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()
No description has been provided for this image
# 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()
No description has been provided for this image
# 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))
Live trending data: (199, 8)
      video_id trending_date  \
0  82-jTNka3uc    2026-06-02   
1  3oB9AxspVow    2026-06-02   
2  l-cyT28MFyk    2026-06-02   

                                               title     channel_title  \
0  Ariana Grande - hate that i made you love me (...  ArianaGrandeVevo   
1           The End of Oak Street | Official Trailer      Warner Bros.   
2        PLAYING HARDCORE MINECRAFT UNTIL WE BEAT IT            Jynxzi   

  category_id    views   likes  comment_count  
0          10  3881440  511194          29488  
1           1  3868073   42776           3358  
2          24   791258   16959            418  
# 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))
                                               title    views   likes  \
0  Ariana Grande - hate that i made you love me (...  3881440  511194   
1           The End of Oak Street | Official Trailer  3868073   42776   
2        PLAYING HARDCORE MINECRAFT UNTIL WE BEAT IT   791258   16959   

   like_ratio  comment_count  comment_ratio  
0   13.170215          29488       0.759718  
1    1.105874           3358       0.086813  
2    2.143296            418       0.052827  
# 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']])
                                             title  channel_title  views  \
42     3Quency - Girls Talk (Official Music Video)        3Quency  15264   
33                          Vince Staples - Cotton  Vince Staples  38843   
39   Mastodon - Your Ghost Again (Official Visual)       Mastodon  30020   
53       bleood - ding dong (official music video)         bleood  27846   
198             we raised $1,000,000 for charity 🥺      lilsimsie  62092   

     likes  like_ratio  
42    3534   23.152516  
33    7803   20.088562  
39    5341   17.791472  
53    4884   17.539323  
198  10029   16.151839  
# 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']])
                                                 title     channel_title  \
3    IShowSpeed - World Cup (Champions) [Official M...        IShowSpeed   
0    Ariana Grande - hate that i made you love me (...  ArianaGrandeVevo   
14             1000 VS 1000 Player Minecraft Civil War        FlameFrags   
6                         MEOVV(미야오) - ‘DDI RO RI’ M/V     THEBLACKLABEL   
156                  The Most SATISFYING Roblox Game..            Foltyn   

       views  comment_count  
3    4130503          54461  
0    3881440          29488  
14   2484272          25358  
6    3336600          10115  
156  2439523           8121  
# Intermediate 5: Analyze engagement by content category
cat_engagement = yt_df.groupby('category_id')['like_ratio'].mean().sort_values(ascending=False)
print(cat_engagement)
category_id
22    8.816062
10    6.012986
17    5.537428
20    4.670353
24    4.619957
28    4.143389
1     2.708252
Name: like_ratio, dtype: float64
# 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()
No description has been provided for this image
# 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))
Number of potential viral videos (top 1% by views): 2
                                               title     channel_title  \
0  Ariana Grande - hate that i made you love me (...  ArianaGrandeVevo   
3  IShowSpeed - World Cup (Champions) [Official M...        IShowSpeed   

     views   likes  comment_count  
0  3881440  511194          29488  
3  4130503  648008          54461  
# 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()
No description has been provided for this image
# 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))
CTR Quartiles: 0.25    4.0800
0.50    6.3750
0.75    8.2525
Name: ctr, dtype: float64
Outlier CTR videos (top 25%):
    video_id   views   ctr
6          7  110368  9.59
9         10  137437  8.95
11        12  430510  9.16
12        13   87598  8.40
20        21   64920  8.68
# 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']])
Rows with missing engagement:
    post_id  likes  comments
5         6    NaN       NaN
6         7    NaN       NaN
7         8    NaN       NaN
8         9    NaN       NaN
9        10    NaN       NaN
10       11    NaN       NaN
After imputation of missing values:
    post_id  likes  comments
5         6    0.0       0.0
6         7    0.0       0.0
7         8    0.0       0.0
8         9    0.0       0.0
9        10    0.0       0.0
10       11    0.0       0.0
# 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}')
Incorrect simple mean of columns: 2160.15
Correct post-wise mean then average: 2160.15
# 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())
First 3 incorrect ratios: [0.953, 0.847, 0.576]
First 3 correct like-to-view ratios: [0.148, 0.098, 0.051]
# 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)
Grouped by unique title (not recommended):
title
'Oops All Commander' w/ Cosmonaut Marcus | Shuffle Up & Play 104 | Magic: The Gathering Gameplay    4.242255
007 FIRST LIGHT Walkthrough Gameplay Part 6 - KNIGHTFALL (FULL GAME)                                5.530100
1 Hour Of Top Tier Jason Builds                                                                     3.648750
Name: like_ratio, dtype: float64

Grouped by category_id (best practice):
category_id
1     2.708252
10    6.012986
17    5.537428
Name: like_ratio, dtype: float64

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')
Benchmark (top 10 posts): 20.61%
Your overall average: 12.61%
Improvement potential: 8.00 percentage points
# Audience segmentation: Analyze engagement by platform
platform_stats = df.groupby('platform')[['engagement_rate', 'views']].mean()
print(platform_stats.round(2))
           engagement_rate     views
platform                            
Instagram            13.04  52754.95
TikTok               12.69  50206.41
YouTube              12.16  49390.12
# 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()
No description has been provided for this image
# 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)
Content Strategy Recommendation:
Try posting on Instagram at 12:00 for maximum engagement.
 

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.