Mathew K Analytics

Lesson 6 · Social Media Content Analytics

Python Basics for Social Media Data Analysis

In this lesson, we will learn how to analyze social media data using Python to discover what makes content successful. These analytics skills matter because…

⬇ 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

Python Basics for Social Media Data Analysis#

  • In this lesson, we will learn how to analyze social media data using Python to discover what makes content successful.
  • These analytics skills matter because brands, influencers, and creatives need to know what works and what does not for audience growth.
  • By the end, you will be able to load real datasets, measure engagement metrics, and find actionable content insights.
  • Everything is hands-on and designed for beginners with some Python experience.
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')

Key Concepts for Social Media Analytics#

  • Social media datasets represent content such as videos, posts, or comments and each row is one item.
  • Metrics like views, likes, comments, click-through rate (CTR), and watch time are main signals of performance.
  • Engagement rates help compare items of different scales.
  • Beginner mistakes include confusing total counts with averages, or mixing up ratios like CTR and engagement rate.
# Example 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
# Example 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
# Example 3: Find the most popular post by total views
top_post = df.loc[df['views'].idxmax()]
print('Top post by views:')
print(top_post[['post_id','platform','date','views','likes','comments','shares','engagement_rate']])
Top post by views:
post_id                            393
platform                        TikTok
date               2023-04-09 00:00:00
views                            99813
likes                            12361
comments                          4424
shares                            2837
engagement_rate              19.658762
Name: 392, dtype: object
# Example 4: Calculate average engagement rate by platform
avg_engage_platform = df.groupby('platform')['engagement_rate'].mean().sort_values(ascending=False)
print('Average engagement rate by platform (descending):')
print(avg_engage_platform)
Average engagement rate by platform (descending):
platform
Instagram    13.044316
TikTok       12.690745
YouTube      12.163010
Name: engagement_rate, dtype: float64
# Example 5: Time trend of content posting and engagement
df_sorted = df.sort_values('date')
df_sorted.set_index('date', inplace=True)
rolling_engage = df_sorted['engagement_rate'].rolling(window=20).mean()
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 4))
plt.plot(rolling_engage, label='Rolling Engagement Rate (%)')
plt.xlabel('Date')
plt.ylabel('Engagement Rate (%)')
plt.title('Rolling Engagement Rate Over Time')
plt.legend()
plt.tight_layout()
plt.show()
No description has been provided for this image
# Example 6: Identify posts with viral-like engagement
viral = df[df['engagement_rate'] > df['engagement_rate'].quantile(0.99)]
print(f'Number of viral-like posts: {len(viral)}')
print(viral[['post_id','platform','views','likes','comments','shares','engagement_rate']].head())
Number of viral-like posts: 5
     post_id   platform  views  likes  comments  shares  engagement_rate
10        11  Instagram  16123   2202       791     426        21.205731
87        88     TikTok  82898  11426      3823    1910        20.698931
351      352  Instagram  74643  10653      3569    2036        21.781011
383      384     TikTok  36731   4965      1769     952        20.925104
423      424     TikTok  53021   7572      2619     881        20.882292
# Example 7: Import and preview a YouTube trending dataset (synthetic fallback)
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))
Falling back to synthetic: <HttpError 403 when requesting https://youtube.googleapis.com/youtube/v3/videos?part=snippet%2Cstatistics&chart=mostPopular&regionCode=US&maxResults=50&key=YOUR_GOOGLE_API_KEY&alt=json returned "The request cannot be completed because you have exceeded your <a href="/youtube/v3/getting-started#quota">quota</a>.". Details: "[{'message': 'The request cannot be completed because you have exceeded your <a href="/youtube/v3/getting-started#quota">quota</a>.', 'domain': 'youtube.quota', 'reason': 'quotaExceeded'}]">
Synthetic fallback: (1000, 8)
  video_id trending_date          title channel_title  category_id    views  \
0     vid0    2023-01-01  Video Title 0      ChannelC           10  3958242   
1     vid1    2023-01-02  Video Title 1      ChannelA           10  1227060   
2     vid2    2023-01-03  Video Title 2      ChannelC           24  1041519   

    likes  comment_count  
0  144402          18611  
1  160181           4977  
2   33966          18763  
# Example 8: Calculate average views and likes by YouTube category
category_stats = yt_df.groupby('category_id')[['views','likes']].mean().sort_values('views', ascending=False)
print('Mean views and likes by category (descending views):')
print(category_stats.head())
Mean views and likes by category (descending views):
                    views          likes
category_id                             
1            2.675815e+06  102460.076471
28           2.661432e+06  101337.288235
22           2.597320e+06  103143.993464
24           2.524280e+06   99102.467066
2            2.498725e+06   91232.910180
# Example 9: Top trending channels by total engagement (likes + comment_count)
yt_df['total_engagement'] = yt_df['likes'] + yt_df['comment_count']
channel_stats = yt_df.groupby('channel_title')['total_engagement'].sum().sort_values(ascending=False)
print('Top 5 channels by total engagement:')
print(channel_stats.head())
Top 5 channels by total engagement:
channel_title
ChannelA    45421601
ChannelC    40292853
ChannelB    40006702
Name: total_engagement, dtype: int32
# Example 10: Identify possible viral trending videos
viral_trending = yt_df[yt_df['views'] > yt_df['views'].quantile(0.995)]
print(f'Top trending videos with viral-like view counts (N={len(viral_trending)}):')
print(viral_trending[['video_id','title','channel_title','views','likes','comment_count']].head())
Top trending videos with viral-like view counts (N=5):
    video_id            title channel_title    views   likes  comment_count
60     vid60   Video Title 60      ChannelB  4966289  196849          13006
307   vid307  Video Title 307      ChannelC  4975452   94575          10818
404   vid404  Video Title 404      ChannelB  4962075   59600          27501
795   vid795  Video Title 795      ChannelA  4973401   54231          21072
826   vid826  Video Title 826      ChannelC  4959982   41092          45460
# Example 11: Which day of the week trends the most?
yt_df['weekday'] = pd.to_datetime(yt_df['trending_date']).dt.day_name()
weekday_stats = yt_df.groupby('weekday')['views'].mean().sort_values(ascending=False)
print('Average trending views by weekday:')
print(weekday_stats)
Average trending views by weekday:
weekday
Thursday     2.739779e+06
Saturday     2.700885e+06
Sunday       2.607461e+06
Wednesday    2.562656e+06
Monday       2.505524e+06
Tuesday      2.448374e+06
Friday       2.375622e+06
Name: views, dtype: float64
# Example 12: Breakdown of engagement rate by channel
yt_df['engagement_rate'] = ((yt_df['likes'] + yt_df['comment_count']) / yt_df['views']) * 100
chan_rate = yt_df.groupby('channel_title')['engagement_rate'].mean().sort_values(ascending=False)
print('Channel engagement rates (mean, %):')
print(chan_rate.head())
Channel engagement rates (mean, %):
channel_title
ChannelB    12.093386
ChannelC    11.950758
ChannelA    11.706858
Name: engagement_rate, dtype: float64
# Example 13: Error Handling - What if some videos are missing likes?
yt_df_missing = yt_df.copy()
yt_df_missing.loc[yt_df_missing.sample(frac=0.02, random_state=42).index, 'likes'] = np.nan
yt_df_missing['likes_filled'] = yt_df_missing['likes'].fillna(0)
yt_df_missing['engagement_rate_recalculated'] = ((yt_df_missing['likes_filled'] + yt_df_missing['comment_count']) / yt_df_missing['views']) * 100
print('Null likes count:', yt_df_missing["likes"].isnull().sum())
print(yt_df_missing[['likes','likes_filled','engagement_rate_recalculated']].head(5))
Null likes count: 20
      likes  likes_filled  engagement_rate_recalculated
0  144402.0      144402.0                      4.118318
1  160181.0      160181.0                     13.459652
2   33966.0       33966.0                      5.062702
3  105500.0      105500.0                     15.704220
4   84346.0       84346.0                      2.726099
# Example 14: Debugging - Incorrect metric aggregation
wrong_total_engage = yt_df_missing['likes'].sum() + yt_df_missing['comment_count'].sum()
correct_total_engage = yt_df_missing[['likes_filled','comment_count']].sum().sum()
print(f'Total engagement (ignoring fills): {wrong_total_engage:,.0f}')
print(f'Total engagement (handling missing likes): {correct_total_engage:,.0f}')
Total engagement (ignoring fills): 124,019,455
Total engagement (handling missing likes): 124,019,455
# Example 15: Debugging - Misinterpreting ratios (CTR, engagement rate)
fake_ctr = yt_df['likes'] / (yt_df['views'] * 2)
true_engage_rate = ((yt_df['likes'] + yt_df['comment_count']) / yt_df['views']) * 100
print('Fake CTR (misinterpreted):', fake_ctr.head(3).to_list())
print('True Engagement Rate (%):', np.round(true_engage_rate.head(3),2).to_list())
Fake CTR (misinterpreted): [0.01824067351111933, 0.0652702394340945, 0.016305991537360336]
True Engagement Rate (%): [4.12, 13.46, 5.06]
# Example 16: Debugging - Grouping mistakes: wrong platform aggregation
df_wrong = df.copy()
df_wrong['platform_low'] = df_wrong['platform'].str.lower()
df_wrong.loc[df_wrong.sample(frac=0.01, random_state=1).index,'platform_low'] = 'unknown'
counts = df_wrong.groupby('platform_low')['post_id'].count()
print('Counts by possibly-wrong platform labels:')
print(counts)
Counts by possibly-wrong platform labels:
platform_low
instagram    161
tiktok       153
unknown        5
youtube      181
Name: post_id, dtype: int64

Best Practices and Patterns in Social Media Analytics#

  • Always define metrics like engagement rate and CTR clearly and use them consistently.
  • Compare content performance by normalizing for audience size and post timing.
  • Benchmark against the same category or platform, not across unrelated ones.
  • Segment audience or content by key variables for more actionable insights.
  • Track growth and content trends using rolling or period-based metrics.
  • Optimize by learning from your top-performing or viral posts.
# Example 17: Content performance benchmarking vs. platform average
platform_avg = df.groupby('platform')['engagement_rate'].mean()
df['benchmark_vs_platform'] = df.apply(lambda row: row['engagement_rate'] - platform_avg.loc[row['platform']], axis=1)
print(df[['platform','engagement_rate','benchmark_vs_platform']].head(5))
    platform  engagement_rate  benchmark_vs_platform
0  Instagram        17.143756               4.099440
1  Instagram        14.166667               1.122351
2  Instagram        10.366615              -2.677701
3    YouTube         7.187284              -4.975726
4  Instagram        10.636292              -2.408024
# Example 18: Segment content by posting hour
df['hour'] = pd.to_datetime(df['date']).dt.hour
hourly = df.groupby('hour')['engagement_rate'].mean().sort_values(ascending=False)
print('Average engagement rate by hour posted:')
print(hourly)
Average engagement rate by hour posted:
hour
12    13.243221
0     12.631587
18    12.589461
6     11.975891
Name: engagement_rate, dtype: float64
# Example 19: Trend and growth analysis for engagement rates
window = 30
df_trend = df.sort_values('date').set_index('date').copy()
df_trend['trend30'] = df_trend['engagement_rate'].rolling(window=window, min_periods=1).mean()
import matplotlib.pyplot as plt
plt.figure(figsize=(8,4))
plt.plot(df_trend['trend30'], label='30-Post Rolling Mean Engagement Rate')
plt.title('Content Engagement Rate 30-Post Trend')
plt.ylabel('Engage Rate (%)')
plt.xlabel('Date')
plt.legend()
plt.tight_layout()
plt.show()
No description has been provided for this image
# Example 20: Content optimization - Find keywords in viral titles
viral_titles = yt_df[yt_df['views'] > yt_df['views'].quantile(0.995)]['title'].str.lower().str.cat(sep=' ')
from collections import Counter
keyword_counts = Counter([w for w in viral_titles.split() if len(w) > 3])
print('Most common long keywords in viral trending titles:')
print(keyword_counts.most_common(10))
Most common long keywords in viral trending titles:
[('video', 5), ('title', 5)]
# End-to-End Example: Recommend a content strategy from analytics
top_platform = df.groupby('platform')['engagement_rate'].mean().idxmax()
best_time = df.groupby('hour')['engagement_rate'].mean().idxmax()
viral_keywords = [k for k, _ in keyword_counts.most_common(5)]
print(f'Recommend posting most often on {top_platform}.')
print(f'Peak audience engagement hours: {best_time} local time.')
print(f'Include these keywords in titles or descriptions: {viral_keywords}')
Recommend posting most often on Instagram.
Peak audience engagement hours: 12 local time.
Include these keywords in titles or descriptions: ['video', 'title']

YouTube: Continue Your Learning#

  • Subscribe for more hands-on Python and analytics tutorials.
  • Like and share if you found these practical examples helpful.

Found this useful?

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