Mathew K Analytics

Lesson 10 · Social Media Content Analytics

Loading Social Media Data from CSV and APIs

In this lesson, we will learn how to load real social media data using Python. We will explore both CSV datasets and loading data from APIs like YouTube.…

What you'll learn

Datasets used in this lesson

Save these next to the notebook. In Google Colab, upload them with the 📁 icon on the left first.

📓 Full notebook

Download .ipynb

Loading Social Media Data from CSV and APIs#

  • In this lesson, we will learn how to load real social media data using Python.
  • We will explore both CSV datasets and loading data from APIs like YouTube.
  • This is important because understanding content engagement starts with getting the right data.
  • For content creators and businesses, the ability to analyze your own or public data leads to better strategies.
  • By the end of this lesson, you will be able to load social media datasets, explore key engagement metrics, and spot top-performing content.
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
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

Understanding Core Social Media Analytics Concepts#

  • Social media datasets usually represent videos, posts, channels, or user interactions.
  • Engagement metrics like views, likes, comments, shares, and CTR show how audiences respond to content.
  • Metrics such as watch time or engagement rate can indicate content quality or popularity.
  • It is important not to confuse absolute counts with rates: ten thousand likes is huge for a small creator but low for a superstar.
  • A common mistake is aggregating metrics incorrectly, such as mixing different time periods or ignoring platform differences.
  • Always check if data is missing, mislabeled, or grouped the wrong way before running deeper analyses.
# Beginner Example 1: Load social media content data from a DataFrame
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 Example 2: Calculate engagement rate for each post
df['engagement_rate'] = ((df['likes'] + df['comments'] + df['shares']) / df['views'] * 100).round(2)
print(df[['platform', 'views', 'likes', 'comments', 'shares', 'engagement_rate']].head(3))
    platform  views  likes  comments  shares  engagement_rate
0  Instagram  15895   2356       116     253            17.14
1  Instagram    960     94        16      26            14.17
2  Instagram  76920   3910      2879    1185            10.37
# Beginner Example 3: Save the dataset to CSV and reload it
csv_path = 'social_media_posts.csv'
df.to_csv(csv_path, index=False)
df_loaded = pd.read_csv(csv_path)
print(df_loaded.head(3))
   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   

   engagement_rate  
0            17.14  
1            14.17  
2            10.37  
# Intermediate Example 1: Load YouTube trending data from the API, or use synthetic fallback
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}')
    n = 1000
    np.random.seed(42)
    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  
# Intermediate Example 2: List the top 5 most-viewed trending videos
top_videos = yt_df.sort_values('views', ascending=False).head(5)
print(top_videos[['title', 'channel_title', 'views', 'likes', 'comment_count']])
               title channel_title    views   likes  comment_count
307  Video Title 307      ChannelC  4975452   94575          10818
795  Video Title 795      ChannelA  4973401   54231          21072
60    Video Title 60      ChannelB  4966289  196849          13006
404  Video Title 404      ChannelB  4962075   59600          27501
826  Video Title 826      ChannelC  4959982   41092          45460
# Intermediate Example 3: Compute average view and like counts by channel
channel_summary = yt_df.groupby('channel_title')[['views', 'likes']].mean().sort_values('views', ascending=False).head(5)
print(channel_summary)
                      views          likes
channel_title                             
ChannelA       2.576728e+06  102190.645070
ChannelB       2.566553e+06   98037.337423
ChannelC       2.543346e+06  101778.203762
# Intermediate Example 4: Import a YouTube analytics-style dataset (simulated)
np.random.seed(42)
n_videos = 300
views = np.random.randint(100, 500000, n_videos)
analytics_df = 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)
})
print(analytics_df.head(3))
   video_id publish_date   views  watch_time  likes  comments   ctr
0         1   2022-01-01  122058      158381   1890      1975  4.14
1         2   2022-01-02  146967      481671   1730      1900  6.99
2         3   2022-01-03  132032      195806  10217       337  5.28
# Intermediate Example 5: Find videos with highest engagement rate
analytics_df['engagement_rate'] = ((analytics_df['likes'] + analytics_df['comments']) / analytics_df['views'] * 100).round(2)
best_engagement = analytics_df.sort_values('engagement_rate', ascending=False).head(5)
print(best_engagement[['video_id', 'views', 'likes', 'comments', 'engagement_rate']])
     video_id   views  likes  comments  engagement_rate
188       189  219069  17314      4058             9.76
160       161  351379  27369      6002             9.50
134       135  120251   9176      2175             9.44
158       159  375813  29097      6239             9.40
167       168  477967  37210      7016             9.25
# Intermediate Example 6: Analyze posting frequency and total views over time
posts_per_month = analytics_df.groupby(analytics_df['publish_date'].dt.to_period('M')).size()
views_per_month = analytics_df.groupby(analytics_df['publish_date'].dt.to_period('M'))['views'].sum()
summary = pd.DataFrame({'posts': posts_per_month, 'total_views': views_per_month})
print(summary)
              posts  total_views
publish_date                    
2022-01          31      6874384
2022-02          28      7131717
2022-03          31      8200910
2022-04          30      7162344
2022-05          31      7408411
2022-06          30      8504458
2022-07          31      7218389
2022-08          31      8455547
2022-09          30      7866087
2022-10          27      7208611
# Advanced Example 1: Detect top rising channels by trending appearances
trending_counts = yt_df['channel_title'].value_counts().head(5)
print(trending_counts)
channel_title
ChannelA    355
ChannelB    326
ChannelC    319
Name: count, dtype: int64
# Advanced Example 2: Find categories with the highest average engagement rates
if 'engagement_rate' not in yt_df.columns:
    yt_df['engagement_rate'] = ((yt_df['likes'] + yt_df['comment_count']) / yt_df['views'] * 100).round(2)
cat_summary = yt_df.groupby('category_id')['engagement_rate'].mean().sort_values(ascending=False).head(5)
print(cat_summary)
category_id
24    15.585389
10    13.590347
2     12.109281
1     10.455706
28    10.417647
Name: engagement_rate, dtype: float64
# Advanced Example 3: Import a time series dataset and analyze daily engagement
np.random.seed(42)
n_days = 365
dates = pd.date_range('2023-01-01', periods=n_days, freq='D')
views = np.random.randint(1000, 50000, n_days)
likes = (views * np.random.uniform(0.03, 0.12, n_days)).astype(int)
ts_df = pd.DataFrame({
    'date': dates,
    'views': views,
    'likes': likes,
    'engagement_rate': np.round(likes / views * 100, 2)
})
print(ts_df.head(3))
        date  views  likes  engagement_rate
0 2023-01-01  16795   1790            10.66
1 2023-01-02   1860    108             5.81
2 2023-01-03  39158   1772             4.53
# Advanced Example 4: Visualize daily views and engagement rate as simple trend lines
import matplotlib.pyplot as plt
plt.figure(figsize=(10,4))
plt.plot(ts_df['date'], ts_df['views'], label='Views')
plt.plot(ts_df['date'], ts_df['engagement_rate'], label='Engagement Rate (%)')
plt.xlabel('Date')
plt.legend()
plt.title('Daily Views and Engagement Rate Over Time')
plt.tight_layout()
plt.savefig('daily_trends.png')
plt.close()
# Error Handling 1: Dealing with missing engagement fields
df_missing = df.copy()
df_missing.loc[::50, 'likes'] = np.nan
print(f"Posts with missing likes: {df_missing['likes'].isna().sum()}")
df_missing['likes'] = df_missing['likes'].fillna(0)
print(f"After fill, missing likes: {df_missing['likes'].isna().sum()}")
Posts with missing likes: 10
After fill, missing likes: 0
# Error Handling 2: Preventing division by zero in engagement rate calculation
df_zero = df.copy()
df_zero['views'].iloc[0:3] = 0
df_zero['engagement_rate'] = np.where(df_zero['views'] == 0, 0, ((df_zero['likes'] + df_zero['comments'] + df_zero['shares']) / df_zero['views'] * 100).round(2))
print(df_zero[['views', 'likes', 'engagement_rate']].head(5))
   views  likes  engagement_rate
0      0   2356             0.00
1      0     94             0.00
2      0   3910             0.00
3  54986   1827             7.19
4   6365    253            10.64
# Error Handling 3: Aggregation mistake - sum vs average
agg_sum = df.groupby('platform')[['likes','comments']].sum()
agg_avg = df.groupby('platform')[['likes','comments']].mean()
print('Sum aggregation:')
print(agg_sum)
print('Average aggregation:')
print(agg_avg.head())
Sum aggregation:
            likes  comments
platform                   
Instagram  761858    216242
TikTok     687145    194197
YouTube    743813    246944
Average aggregation:
                 likes     comments
platform                           
Instagram  4702.827160  1334.827160
TikTok     4491.143791  1269.261438
YouTube    4020.610811  1334.832432
# Error Handling 4: Misinterpreting CTR - raw count vs percent
analytics_df['ctr_fake_raw'] = (analytics_df['ctr'] * analytics_df['views'] / 100).astype(int)
print(analytics_df[['views', 'ctr', 'ctr_fake_raw']].head(3))
    views   ctr  ctr_fake_raw
0  122058  4.14          5053
1  146967  6.99         10272
2  132032  5.28          6971
# Best Practices 1: Benchmarking content performance against category averages
cat_avg_engagement = yt_df.groupby('category_id')['engagement_rate'].mean()
yt_df['category_benchmarking'] = yt_df['engagement_rate'] / yt_df['category_id'].map(cat_avg_engagement)
print(yt_df[['title','category_id','engagement_rate','category_benchmarking']].head(5))
           title  category_id  engagement_rate  category_benchmarking
0  Video Title 0           10             4.12               0.303156
1  Video Title 1           10            13.46               0.990409
2  Video Title 2           24             5.06               0.324663
3  Video Title 3           22            15.70               1.732967
4  Video Title 4           28             2.73               0.262055
# Best Practices 2: Segmenting audience performance by platform
platform_stats = df.groupby('platform')[['views', 'likes', 'comments', 'shares', 'engagement_rate']].mean()
print(platform_stats)
                  views        likes     comments      shares  engagement_rate
platform                                                                      
Instagram  52754.950617  4702.827160  1334.827160  812.067901        13.044444
TikTok     50206.411765  4491.143791  1269.261438  784.267974        12.690850
YouTube    49390.124324  4020.610811  1334.832432  748.513514        12.163027
# Best Practices 3: Detecting time-based content growth
monthly_growth = analytics_df.set_index('publish_date').resample('M')['views'].sum()
growth_pct = monthly_growth.pct_change().fillna(0).round(2)
for month, growth in zip(monthly_growth.index.strftime('%Y-%m'), growth_pct):
    print(f'Month {month}: total views={monthly_growth[month]}, growth={growth*100:.1f}%')
Month 2022-01: total views=publish_date
2022-01-31    6874384
Freq: ME, Name: views, dtype: int32, growth=0.0%
Month 2022-02: total views=publish_date
2022-02-28    7131717
Freq: ME, Name: views, dtype: int32, growth=4.0%
Month 2022-03: total views=publish_date
2022-03-31    8200910
Freq: ME, Name: views, dtype: int32, growth=15.0%
Month 2022-04: total views=publish_date
2022-04-30    7162344
Freq: ME, Name: views, dtype: int32, growth=-13.0%
Month 2022-05: total views=publish_date
2022-05-31    7408411
Freq: ME, Name: views, dtype: int32, growth=3.0%
Month 2022-06: total views=publish_date
2022-06-30    8504458
Freq: ME, Name: views, dtype: int32, growth=15.0%
Month 2022-07: total views=publish_date
2022-07-31    7218389
Freq: ME, Name: views, dtype: int32, growth=-15.0%
Month 2022-08: total views=publish_date
2022-08-31    8455547
Freq: ME, Name: views, dtype: int32, growth=17.0%
Month 2022-09: total views=publish_date
2022-09-30    7866087
Freq: ME, Name: views, dtype: int32, growth=-7.0%
Month 2022-10: total views=publish_date
2022-10-31    7208611
Freq: ME, Name: views, dtype: int32, growth=-8.0%

SOCIAL MEDIA ANALYTICS: End-to-End Example#

  • Let us combine what we have learned and deliver an actual content strategy recommendation.
  • We will use real or simulated YouTube trending data.
  • Our task: Find the top 3 channels by total engagement (likes + comments), and recommend when to post for maximum reach.
  • By the end, you will have practiced loading, analyzing, and generating actionable insights from large social media datasets.
# End-to-End: Find top 3 channels by total engagement
yt_df['total_engagement'] = yt_df['likes'] + yt_df['comment_count']
top_channels = yt_df.groupby('channel_title')['total_engagement'].sum().sort_values(ascending=False).head(3)
print(top_channels)
channel_title
ChannelA    45421601
ChannelC    40292853
ChannelB    40006702
Name: total_engagement, dtype: int32
# End-to-End: Recommend best posting time by analyzing trending video dates
if 'trending_date' in yt_df.columns:
    yt_df['trending_date'] = pd.to_datetime(yt_df['trending_date'])
    yt_df['weekday'] = yt_df['trending_date'].dt.day_name()
    best_day = yt_df['weekday'].value_counts().idxmax()
    print(f'Based on our trending data, the best day to post is: {best_day}')
Based on our trending data, the best day to post is: Sunday

Best Practices Review#

  • Always check for missing or corrupted values in your data files.
  • Aggregate metrics thoughtfully: use averages to compare typical post performance, and totals for reach.
  • Be careful to define engagement metrics the same way every time.
  • Use visualizations and segmentation to spot underlying trends, not just surface spikes.
  • Check assumptions when comparing across platforms or time periods.
  • Remember: consistent, reproducible analysis is the foundation of successful social media strategies.

Want to take your social media analytics skills to the next level?#

  • Consider exploring advanced analytics like audience sentiment, share prediction, or automation using Python!
  • Subscribe to our YouTube channel for in-depth tutorials and hands-on data projects.

Found this useful?

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