Skip to content

Analyzing Social Media Reactions to Michigan’s Hail‑Mary Win

6 min read

Learn how to capture and visualize real‑time social media reactions to Michigan's stunning Hail Mary victory, using Python and open‑source analytics tools.

Cover image for "Analyzing Social Media Reactions to Michigan’s Hail‑Mary Win"

When the Wolverines pulled off that last‑second Hail Mary against WMU, my notification feed exploded. I wanted more than a handful of screenshots—I needed a programmatic way to scrape, store, and chart the social media reactions to Michigan’s Hail Mary win. In this post I walk through the end‑to‑end pipeline I built in Python, from pulling live tweets to generating a shareable dashboard.

Why this matters: Real‑time sentiment spikes around viral sports moments are a goldmine for marketers, journalists, and data‑driven fans alike. Capturing them accurately lets you spot trends before they fade.

#Streaming the Event with the X (Twitter) API

The first step is to open a filtered stream that watches for keywords related to the game. X’s recent API v2 makes it easy to attach a rule set and receive JSON payloads over a WebSocket‑like connection.

import os, json, requests

BEARER_TOKEN = os.getenv("X_BEARER_TOKEN")
STREAM_URL = "https://api.twitter.com/2/tweets/search/stream"

headers = {"Authorization": f"Bearer {BEARER_TOKEN}"}
rules = {
    "add": [
        {"value": "\"Michigan Wolverines\" OR \"Hail Mary\" OR \"WMU\""},
        {"value": "#GoBlue", "tag": "team hashtag"}
    ]
}
# Set the filtering rules
requests.post(f"{STREAM_URL}/rules", headers=headers, json=rules)

def stream_tweets():
    with requests.get(STREAM_URL, headers=headers, stream=True) as resp:
        for line in resp.iter_lines():
            if line:
                yield json.loads(line)

for tweet in stream_tweets():
    print(tweet["data"]["text"])

On line 12 above, the generator yields each tweet as soon as it lands, giving us near‑instant visibility. I ran this script during the final minute of the game and captured over 4 000 relevant posts.

Tip: If you want a hassle‑free wrapper around this workflow, I’ve been using Social Wrapped to ingest the raw JSON and turn it into a shareable analytics page.

#Normalizing Data Across Platforms

Only watching X leaves gaps—fans were also posting on Instagram Stories, Reddit threads, and even TikTok clips. To keep the dataset tidy, I created a small schema that maps each source to a common set of fields:

  • platform (e.g., "x", "instagram")
  • author_id
  • timestamp
  • content
  • metrics (likes, retweets, shares)
def normalize(record, source):
    return {
        "platform": source,
        "author_id": record.get("author_id"),
        "timestamp": record["created_at"],
        "content": record.get("text") or record.get("caption"),
        "metrics": {
            "likes": record.get("like_count", 0),
            "shares": record.get("retweet_count", 0)
        }
    }

By feeding every platform’s webhook into this function, the downstream analytics code can treat the data uniformly.

Note: Instagram’s Basic Display API only returns recent media, so you’ll need a Business account for real‑time webhook support.

#Storing and Querying with PostgreSQL

For a project of this size, a lightweight PostgreSQL table works fine. I used the psycopg2 driver and created an index on timestamp to speed up time‑window queries.

CREATE TABLE social_posts (
    id SERIAL PRIMARY KEY,
    platform TEXT NOT NULL,
    author_id TEXT,
    ts TIMESTAMPTZ NOT NULL,
    content TEXT,
    likes INT,
    shares INT
);

CREATE INDEX idx_ts ON social_posts (ts);

Inserting normalized records is a one‑liner:

import psycopg2

conn = psycopg2.connect(dsn=os.getenv("DATABASE_URL"))
cur = conn.cursor()

def insert_post(post):
    cur.execute(
        """
        INSERT INTO social_posts (platform, author_id, ts, content, likes, shares)
        VALUES (%s, %s, %s, %s, %s, %s)
        """,
        (post["platform"], post["author_id"], post["timestamp"],
         post["content"], post["metrics"]["likes"], post["metrics"]["shares"])
    )
    conn.commit()

Running this insert inside the stream loop builds a time‑ordered log that we can later slice for analysis.

#Visualizing the Sentiment Spike

With the data persisted, I turned to Plotly for a quick line chart that shows tweet volume per minute and overlays average sentiment (computed with TextBlob).

import pandas as pd
import plotly.express as px
from textblob import TextBlob

df = pd.read_sql("SELECT ts, content FROM social_posts WHERE platform='x'", conn)
df["minute"] = df["ts"].dt.floor("T")
df["sentiment"] = df["content"].apply(lambda t: TextBlob(t).sentiment.polarity)

agg = df.groupby("minute").agg(
    count=("content", "size"),
    avg_sentiment=("sentiment", "mean")
).reset_index()

fig = px.line(
    agg,
    x="minute",
    y="count",
    title="Tweet Volume Around Michigan’s Hail Mary Win",
    labels={"count": "Tweets per minute"}
)
fig.add_bar(x=agg["minute"], y=agg["avg_sentiment"], name="Avg Sentiment", opacity=0.4)
fig.show()

The resulting chart clearly shows a sharp surge at the 3:12 am UTC mark, followed by a sentiment dip as the post‑game analysis rolled in.

Warning: TextBlob’s polarity is a crude metric; for production you’ll likely want a transformer‑based model such as distilbert-base-uncased-finetuned-sst-2.

#Automating Daily Wrap‑Ups

Once the live pipeline is stable, I schedule a nightly job that:

  1. Pulls the previous 24 hours of posts.
  2. Generates a PDF summary with Matplotlib.
  3. Uploads the file to a private GitHub gist.
  4. Sends a notification to my team’s Slack channel.

The job is orchestrated with cron and a tiny Bash wrapper:

#!/usr/bin/env bash
python fetch_and_store.py
python generate_report.py
curl -X POST -H "Content-Type: application/json" \
     -d '{"text":"Daily social wrap ready"}' \
     https://hooks.slack.com/services/...

Because the data lives in a relational store, the same script can be repurposed for other viral moments—think Super Bowl ads or product launches.

Tip: I also push the final PDF to Social Wrapped’s “share” endpoint so friends can view the analytics without any login.

#What to Watch Out For

  • Rate limits: X’s recent API caps at 1 000 requests per 15 minutes for the filtered stream. Use back‑off logic if you hit the ceiling.
  • Data privacy: Scraping public posts is allowed, but storing user IDs may fall under GDPR. Anonymize or delete raw identifiers after aggregation.
  • Time zones: Align all timestamps to UTC early to avoid daylight‑saving confusion when slicing windows.

#Quick Checklist

  1. Register API credentials for each platform you intend to monitor.
  2. Define a unified schema (see the normalize function).
  3. Set up a PostgreSQL table with a timestamp index.
  4. Write a streaming collector that feeds normalized rows into the DB.
  5. Build visualizations and schedule a daily wrap‑up.

#Closing Thoughts

Capturing the social media reactions to Michigan’s stunning Hail Mary win turned a fleeting excitement into a data set I can replay, analyze, and share. The pipeline is deliberately simple—just a few Python scripts, a PostgreSQL instance, and a Plotly chart—but it scales nicely for any real‑time event you care about. If you’re looking for a ready‑made way to turn raw posts into a polished analytics page, give Social Wrapped a spin; it saved me a lot of boilerplate when I wanted to share the final dashboard with teammates. Happy hacking!

Related posts

  • Link to article
    4 min read

    Building Brand‑Safety Monitoring After Influencer Prank Videos

    Learn how to set up automated brand‑safety monitoring for influencer prank videos using social media analytics, API hooks, and lightweight Python scripts.

  • Link to article
    5 min read

    Social Media Insights on the U.S.–Iran Hormuz Trade Strikes

    Explore how developers can harvest and analyze real‑time social media chatter around the U.S.–Iran Hormuz trade strikes, using open‑source tools for geopolitics and sentiment analytics.