Influencer Impact Analysis
Quantitatively measure the short-term market price and volume impact of influential cryptocurrency social media accounts by analyzing price movements and trading volume surges in the minutes and hours following high-engagement posts and viral crypto content.
Influencer Impact Analysis — Sentiment & NLP
Category: Sentiment & NLP | Subcategory: Signals
What This Notebook Does
In crypto markets, a single tweet from a high-follower account can move price by 5–15% within minutes. This notebook quantifies influencer market impact — measuring how specific accounts, post types, and sentiment directions affect short-term price movement.
This notebook:
- Loads scraped influencer post data (from Notebook 113/114) and OHLCV price data
- Categorizes influencers by tier (mega, macro, micro) based on follower count
- Measures price impact in the 1h, 4h, 24h, and 72h windows after each post
- Classifies post sentiment (bullish, bearish, neutral) using VADER
- Runs event-study analysis — average cumulative abnormal return (CAR) by influencer
- Builds an influencer impact score for use in live trading signal generation
- Visualizes impact distributions and per-account statistics
The Event Study Framework
Borrowed from academic finance, an event study measures the abnormal return around a specific event:
Abnormal Return(t) = Actual Return(t) - Expected Return(t)
Expected Return = average return over a 7-day pre-event window
CAR = cumulative sum of abnormal returns over [0, T] hours
By averaging CAR across many posts by the same influencer, we estimate their true average market impact, filtered from background noise.
Prerequisites
- Post data CSV from Notebook 113 (
reddit_crypto_scraper) or 114 (telegram_channel_monitor) - Hourly OHLCV CSV for BTC or relevant asset
- Or use the synthetic generator in Section 2
!pip install pandas numpy matplotlib seaborn scipy vaderSentiment --quiet[?25l [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m [32m0.0/126.0 kB[0m [31m?[0m eta [36m-:--:--[0m [2K [91m━━━━━━━━━━━━━━━━━━━━━━━━━━[0m[90m╺[0m[90m━━━━━━━━━━━━━[0m [32m81.9/126.0 kB[0m [31m2.3 MB/s[0m eta [36m0:00:01[0m [2K [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m [32m126.0/126.0 kB[0m [31m2.0 MB/s[0m eta [36m0:00:00[0m [?25h
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
from datetime import datetime, timedelta, timezone
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (13, 5)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
sns.set_palette('husl')
VADER = SentimentIntensityAnalyzer()
print('Imports ready.')Imports ready.
Section 2 — Configuration & Data Loading
# ── CONFIGURATION ─────────────────────────────────────────────────────────────
IMPACT_WINDOWS_HOURS = [1, 4, 24, 72] # hours after post to measure price impact
PRE_EVENT_WINDOW = 7 * 24 # hours before post for expected return baseline
MIN_POSTS_FOR_ANALYSIS = 5 # minimum posts per influencer to include
INFLUENCER_TIERS = {
'mega': 1_000_000, # followers >= 1M
'macro': 100_000, # followers >= 100K
'micro': 10_000, # followers >= 10K
'nano': 0, # all others
}
# ─────────────────────────────────────────────────────────────────────────────
def generate_synthetic_posts(n_influencers: int = 8, posts_per: int = 20, seed: int = 42) -> pd.DataFrame:
"""
Generate synthetic influencer post data for pipeline testing.
Parameters
----------
n_influencers : int
Number of unique influencer accounts to simulate.
posts_per : int
Average posts per influencer (Poisson-distributed).
seed : int
Random seed.
Returns
-------
pd.DataFrame
Columns: author, followers, timestamp, text, platform.
Timestamps span the last 90 days.
"""
np.random.seed(seed)
accounts = [
{'author': f'influencer_{i}',
'followers': int(np.random.choice([5_000_000, 800_000, 200_000, 50_000, 15_000], p=[0.1, 0.15, 0.25, 0.25, 0.25])),
'platform': np.random.choice(['twitter', 'telegram', 'reddit'])}
for i in range(n_influencers)
]
bullish_texts = [
'BTC breaking out — accumulate now',
'ETH to new ATH this cycle, no doubt',
'Massive buy signal just triggered',
'This is the dip to buy, trust the process',
]
bearish_texts = [
'Crypto market looking very weak, reduce exposure',
'BTC rejection at resistance — could see 30% drop',
'SEC crackdown coming, sell the news',
'Bearish divergence on daily — be careful',
]
neutral_texts = [
'Interesting on-chain data this week',
'Market consolidating — patience is key',
'Long thread on Layer 2 scaling solutions',
]
records = []
now = datetime.now(tz=timezone.utc)
for acc in accounts:
n_posts = max(MIN_POSTS_FOR_ANALYSIS, np.random.poisson(posts_per))
for _ in range(n_posts):
hours_ago = np.random.uniform(0, 90 * 24)
sentiment_choice = np.random.choice(['bullish', 'bearish', 'neutral'], p=[0.45, 0.35, 0.20])
text = np.random.choice(
bullish_texts if sentiment_choice == 'bullish'
else (bearish_texts if sentiment_choice == 'bearish' else neutral_texts)
)
records.append({
'author': acc['author'],
'followers': acc['followers'],
'platform': acc['platform'],
'timestamp': now - timedelta(hours=hours_ago),
'text': text,
})
df = pd.DataFrame(records).sort_values('timestamp').reset_index(drop=True)
print(f'Generated {len(df)} posts from {n_influencers} influencers.')
return df
def generate_synthetic_ohlcv_hourly(n_hours: int = 90 * 24, seed: int = 42) -> pd.DataFrame:
"""
Generate synthetic hourly BTC-like OHLCV data.
Parameters
----------
n_hours : int
Number of hourly candles to generate.
seed : int
Random seed.
Returns
-------
pd.DataFrame
Hourly OHLCV DataFrame with DatetimeIndex.
"""
np.random.seed(seed)
end = datetime.now(tz=timezone.utc).replace(minute=0, second=0, microsecond=0)
idx = pd.date_range(end=end, periods=n_hours, freq='1H')
log_ret = np.random.randn(n_hours) * 0.005
close = 40000 * np.exp(np.cumsum(log_ret))
return pd.DataFrame({
'open': close * (1 + np.random.randn(n_hours) * 0.001),
'high': close * (1 + np.abs(np.random.randn(n_hours) * 0.003)),
'low': close * (1 - np.abs(np.random.randn(n_hours) * 0.003)),
'close': close,
'volume': np.random.exponential(1e8, n_hours),
}, index=idx)
# ── Load or generate ──────────────────────────────────────────────────────────
USE_SYNTHETIC = True
if USE_SYNTHETIC:
posts_df = generate_synthetic_posts(n_influencers=8, posts_per=25)
ohlcv_df = generate_synthetic_ohlcv_hourly(n_hours=90 * 24)
else:
posts_df = pd.read_csv('influencer_posts.csv', parse_dates=['timestamp'])
ohlcv_df = pd.read_csv('ohlcv_hourly.csv', parse_dates=[0], index_col=0)
print(posts_df[['author', 'followers', 'platform', 'timestamp']].head())Generated 190 posts from 8 influencers.
author followers platform timestamp
0 influencer_5 50000 reddit 2026-03-15 03:47:50.985422+00:00
1 influencer_2 50000 telegram 2026-03-15 14:17:01.038455+00:00
2 influencer_2 50000 telegram 2026-03-15 19:05:40.626021+00:00
3 influencer_0 200000 twitter 2026-03-15 19:30:35.601585+00:00
4 influencer_3 800000 reddit 2026-03-16 01:46:20.574643+00:00
Section 3 — Post Sentiment Scoring & Influencer Tiering
def score_post_sentiment(posts_df: pd.DataFrame) -> pd.DataFrame:
"""
Score each post's sentiment using VADER and classify as bullish/bearish/neutral.
Parameters
----------
posts_df : pd.DataFrame
Post DataFrame with a 'text' column.
Returns
-------
pd.DataFrame
posts_df with added columns: vader_compound, post_sentiment.
post_sentiment: 'bullish' (compound > 0.05), 'bearish' (< -0.05), 'neutral'.
"""
df = posts_df.copy()
df['vader_compound'] = df['text'].apply(lambda t: VADER.polarity_scores(str(t))['compound'])
conditions = [df['vader_compound'] > 0.05, df['vader_compound'] < -0.05]
df['post_sentiment'] = np.select(conditions, ['bullish', 'bearish'], default='neutral')
dist = df['post_sentiment'].value_counts()
print(f'Post sentiment: {dict(dist)}')
return df
def assign_influencer_tiers(posts_df: pd.DataFrame, tier_thresholds: dict) -> pd.DataFrame:
"""
Assign each influencer to a tier based on follower count.
Parameters
----------
posts_df : pd.DataFrame
Post DataFrame with 'followers' column.
tier_thresholds : dict
{tier_name: minimum_followers} sorted descending by threshold.
Example: {'mega': 1_000_000, 'macro': 100_000, 'micro': 10_000, 'nano': 0}
Returns
-------
pd.DataFrame
posts_df with added 'tier' column.
"""
sorted_tiers = sorted(tier_thresholds.items(), key=lambda x: x[1], reverse=True)
def get_tier(followers):
for tier, threshold in sorted_tiers:
if followers >= threshold:
return tier
return 'nano'
df = posts_df.copy()
df['tier'] = df['followers'].apply(get_tier)
print(df.groupby('tier')['author'].nunique().rename('unique_accounts').to_string())
return df
posts_df = score_post_sentiment(posts_df)
posts_df = assign_influencer_tiers(posts_df, INFLUENCER_TIERS)Post sentiment: {'bullish': np.int64(93), 'neutral': np.int64(70), 'bearish': np.int64(27)}
tier
macro 4
mega 1
micro 3
Section 4 — Event Study: Price Impact Measurement
def compute_abnormal_return(
post_time: pd.Timestamp,
ohlcv_df: pd.DataFrame,
hours_forward: int,
pre_event_hours: int = 168
) -> float:
"""
Compute the abnormal return for a given number of hours after a post.
Parameters
----------
post_time : pd.Timestamp
Timestamp of the influencer post (timezone-aware).
ohlcv_df : pd.DataFrame
Hourly OHLCV DataFrame with a timezone-aware DatetimeIndex.
hours_forward : int
Hours ahead to measure price return.
pre_event_hours : int
Hours before the event to compute the expected (baseline) return.
Returns
-------
float
Abnormal return = actual_return - expected_return. NaN if price data unavailable.
Notes
-----
Expected return is the mean hourly return over the pre-event window,
scaled to `hours_forward` hours. This removes the baseline drift so
that only the event-attributable price move is measured.
"""
try:
post_time = pd.Timestamp(post_time).tz_localize('UTC') if post_time.tzinfo is None else post_time
target_time = post_time + timedelta(hours=hours_forward)
pre_start = post_time - timedelta(hours=pre_event_hours)
pre_prices = ohlcv_df.loc[pre_start:post_time, 'close']
if len(pre_prices) < 10:
return np.nan
pre_returns = pre_prices.pct_change().dropna()
expected_return = pre_returns.mean() * hours_forward
entry_price = ohlcv_df['close'].asof(post_time)
exit_price = ohlcv_df['close'].asof(target_time)
if pd.isna(entry_price) or pd.isna(exit_price) or entry_price == 0:
return np.nan
actual_return = (exit_price - entry_price) / entry_price
return actual_return - expected_return
except Exception:
return np.nan
def run_event_study(
posts_df: pd.DataFrame,
ohlcv_df: pd.DataFrame,
impact_windows: list
) -> pd.DataFrame:
"""
Compute abnormal returns for each post across all impact windows.
Parameters
----------
posts_df : pd.DataFrame
Post DataFrame with 'timestamp', 'author', 'tier', 'post_sentiment', 'followers'.
ohlcv_df : pd.DataFrame
Hourly OHLCV DataFrame.
impact_windows : list of int
Hours ahead to measure (e.g., [1, 4, 24, 72]).
Returns
-------
pd.DataFrame
posts_df with additional columns: abnormal_return_{h}h for each window.
Notes
-----
This can be slow for large post counts. For 500+ posts, batch compute
using vectorized OHLCV lookups rather than row-by-row.
"""
df = posts_df.copy()
print(f'Computing abnormal returns for {len(df)} posts across {len(impact_windows)} windows...')
for h in impact_windows:
col = f'abnormal_return_{h}h'
df[col] = df['timestamp'].apply(
lambda ts: compute_abnormal_return(ts, ohlcv_df, h)
)
valid = df[col].notna().sum()
print(f' {h}h window: {valid}/{len(df)} valid data points')
return df
# ── Run event study ───────────────────────────────────────────────────────────
results_df = run_event_study(posts_df, ohlcv_df, IMPACT_WINDOWS_HOURS)Computing abnormal returns for 190 posts across 4 windows... 1h window: 190/190 valid data points 4h window: 190/190 valid data points 24h window: 190/190 valid data points 72h window: 190/190 valid data points
Section 5 — Influencer Impact Scoring
def compute_influencer_impact_scores(
results_df: pd.DataFrame,
primary_window: int = 24,
min_posts: int = 5
) -> pd.DataFrame:
"""
Aggregate per-influencer impact statistics across all their posts.
Parameters
----------
results_df : pd.DataFrame
Output of run_event_study() with abnormal return columns.
primary_window : int
The window (in hours) used as the primary impact metric for scoring.
min_posts : int
Minimum number of valid posts required to include an influencer.
Returns
-------
pd.DataFrame
Per-influencer summary with columns: author, tier, followers, n_posts,
mean_impact, std_impact, t_stat, p_value, impact_score.
impact_score is a composite metric combining effect size and statistical significance.
"""
col = f'abnormal_return_{primary_window}h'
groups = results_df.groupby('author')
rows = []
for author, grp in groups:
valid = grp[col].dropna()
if len(valid) < min_posts:
continue
mean_impact = valid.mean()
std_impact = valid.std()
t_stat, p_val = stats.ttest_1samp(valid, 0)
# Impact score = effect magnitude weighted by statistical confidence
confidence = 1 - min(p_val, 1.0)
impact_score = abs(mean_impact) * confidence * np.sign(mean_impact)
rows.append({
'author': author,
'tier': grp['tier'].iloc[0],
'followers': grp['followers'].iloc[0],
'n_posts': len(valid),
'mean_impact': round(mean_impact * 100, 3),
'std_impact': round(std_impact * 100, 3),
't_stat': round(t_stat, 3),
'p_value': round(p_val, 4),
'impact_score': round(impact_score * 100, 4),
})
score_df = pd.DataFrame(rows).sort_values('impact_score', ascending=False).reset_index(drop=True)
print(f'Scored {len(score_df)} influencers (minimum {min_posts} posts each)')
print(score_df.to_string(index=False))
return score_df
def plot_impact_by_tier(
results_df: pd.DataFrame,
window_hours: int = 24
) -> None:
"""
Box plot showing price impact distribution by influencer tier and sentiment direction.
Parameters
----------
results_df : pd.DataFrame
Output of run_event_study().
window_hours : int
Impact window to visualize.
"""
col = f'abnormal_return_{window_hours}h'
plot_df = results_df.dropna(subset=[col]).copy()
plot_df[col] = plot_df[col] * 100 # convert to percent
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
sns.boxplot(data=plot_df, x='tier', y=col, order=['mega', 'macro', 'micro', 'nano'],
palette='husl', ax=axes[0])
axes[0].axhline(0, color='black', linewidth=0.8, linestyle='--')
axes[0].set_title(f'Abnormal Return ({window_hours}h) by Influencer Tier')
axes[0].set_ylabel('Abnormal Return (%)')
sns.boxplot(data=plot_df, x='post_sentiment', y=col,
order=['bullish', 'neutral', 'bearish'],
palette=['green', 'grey', 'red'], ax=axes[1])
axes[1].axhline(0, color='black', linewidth=0.8, linestyle='--')
axes[1].set_title(f'Abnormal Return ({window_hours}h) by Post Sentiment')
axes[1].set_ylabel('Abnormal Return (%)')
plt.tight_layout()
plt.show()
def plot_cumulative_impact(
results_df: pd.DataFrame,
windows: list,
filter_tier: str = None
) -> None:
"""
Plot average cumulative abnormal return over increasing time windows.
Parameters
----------
results_df : pd.DataFrame
Output of run_event_study().
windows : list of int
Impact windows to plot (x-axis).
filter_tier : str, optional
Restrict to a specific tier: 'mega', 'macro', 'micro', 'nano'. None = all.
"""
df = results_df if filter_tier is None else results_df[results_df['tier'] == filter_tier]
means = {}
for sent in ['bullish', 'bearish', 'neutral']:
sub = df[df['post_sentiment'] == sent]
means[sent] = [sub[f'abnormal_return_{h}h'].mean() * 100 for h in windows]
fig, ax = plt.subplots(figsize=(10, 5))
colors = {'bullish': 'green', 'bearish': 'red', 'neutral': 'grey'}
for sent, vals in means.items():
ax.plot(windows, vals, marker='o', linewidth=2, color=colors[sent], label=sent.capitalize())
ax.axhline(0, color='black', linewidth=0.8, linestyle='--')
title = f'Avg Cumulative Abnormal Return by Sentiment'
if filter_tier:
title += f' ({filter_tier.capitalize()} tier)'
ax.set_title(title)
ax.set_xlabel('Hours After Post')
ax.set_ylabel('Avg Abnormal Return (%)')
ax.legend()
plt.tight_layout()
plt.show()
# ── Score influencers ─────────────────────────────────────────────────────────
impact_scores = compute_influencer_impact_scores(results_df, primary_window=24, min_posts=MIN_POSTS_FOR_ANALYSIS)
plot_impact_by_tier(results_df, window_hours=24)
plot_cumulative_impact(results_df, IMPACT_WINDOWS_HOURS)Scored 8 influencers (minimum 5 posts each)
author tier followers n_posts mean_impact std_impact t_stat p_value impact_score
influencer_2 micro 50000 30 0.626 2.787 1.229 0.2288 0.4825
influencer_6 mega 5000000 21 0.384 2.095 0.841 0.4103 0.2267
influencer_7 micro 50000 26 0.169 2.398 0.360 0.7217 0.0471
influencer_0 macro 200000 20 0.024 2.488 0.043 0.9664 0.0008
influencer_5 micro 50000 27 -0.020 2.871 -0.036 0.9712 -0.0006
influencer_1 macro 800000 19 -0.224 2.322 -0.421 0.6789 -0.0720
influencer_4 macro 200000 23 -0.437 3.324 -0.630 0.5353 -0.2029
influencer_3 macro 800000 24 -0.428 2.680 -0.783 0.4417 -0.2391
Section 6 — Live Signal: Influencer-Triggered Alert
def build_influencer_watchlist(
impact_scores: pd.DataFrame,
min_impact_score: float = 0.01,
p_value_cutoff: float = 0.10
) -> pd.DataFrame:
"""
Build a curated watchlist of high-impact influencers for live signal generation.
Parameters
----------
impact_scores : pd.DataFrame
Output of compute_influencer_impact_scores().
min_impact_score : float
Minimum absolute impact_score to include (filters noise).
p_value_cutoff : float
Maximum p-value allowed (ensures statistical significance).
Returns
-------
pd.DataFrame
Filtered and ranked watchlist of high-impact accounts.
Notes
-----
In a live system, this watchlist is checked each time a new post
arrives. If the author is on the watchlist, a signal is generated
proportional to their impact score and the post's VADER sentiment.
"""
watchlist = impact_scores[
(impact_scores['p_value'] <= p_value_cutoff) &
(impact_scores['impact_score'].abs() >= min_impact_score)
].copy()
watchlist = watchlist.sort_values('impact_score', ascending=False)
print(f'Watchlist: {len(watchlist)} accounts qualify (p < {p_value_cutoff}, |impact| > {min_impact_score}%)')
return watchlist
def generate_live_influencer_signal(
new_post: dict,
watchlist: pd.DataFrame
) -> dict:
"""
Generate a trading signal when a new post arrives from a watchlisted influencer.
Parameters
----------
new_post : dict
Post record with keys: author, text, timestamp.
watchlist : pd.DataFrame
Output of build_influencer_watchlist().
Returns
-------
dict
Signal record with: author, signal_direction (+1/-1/0), signal_strength,
post_sentiment, impact_score_historical, timestamp.
Returns None if author not in watchlist.
"""
match = watchlist[watchlist['author'] == new_post['author']]
if match.empty:
return None
hist_impact = match.iloc[0]['impact_score']
vader_score = VADER.polarity_scores(str(new_post['text']))['compound']
if vader_score > 0.05:
direction, sentiment = 1, 'bullish'
elif vader_score < -0.05:
direction, sentiment = -1, 'bearish'
else:
direction, sentiment = 0, 'neutral'
return {
'author': new_post['author'],
'signal_direction': direction,
'signal_strength': round(abs(hist_impact) * abs(vader_score), 4),
'post_sentiment': sentiment,
'impact_score_historical': round(hist_impact, 4),
'timestamp': new_post['timestamp'],
}
# ── Build watchlist and demonstrate live signal ───────────────────────────────
watchlist = build_influencer_watchlist(impact_scores, min_impact_score=0.0, p_value_cutoff=0.5)
print(watchlist[['author', 'tier', 'followers', 'mean_impact', 'p_value', 'impact_score']].to_string(index=False))
if len(watchlist) > 0:
demo_post = {
'author': watchlist.iloc[0]['author'],
'text': 'BTC is going to absolutely moon — biggest opportunity of the decade',
'timestamp': datetime.now(tz=timezone.utc),
}
demo_signal = generate_live_influencer_signal(demo_post, watchlist)
print('\nLive signal generated:')
for k, v in demo_signal.items():
print(f' {k}: {v}')Watchlist: 3 accounts qualify (p < 0.5, |impact| > 0.0%)
author tier followers mean_impact p_value impact_score
influencer_2 micro 50000 0.626 0.2288 0.4825
influencer_6 mega 5000000 0.384 0.4103 0.2267
influencer_3 macro 800000 -0.428 0.4417 -0.2391
Live signal generated:
author: influencer_2
signal_direction: 1
signal_strength: 0.2034
post_sentiment: bullish
impact_score_historical: 0.4825
timestamp: 2026-06-12 07:17:20.825439+00:00
Section 7 — Export
def export_influencer_analysis(
results_df: pd.DataFrame,
impact_scores: pd.DataFrame,
watchlist: pd.DataFrame
) -> None:
"""
Export event study results, impact scores, and watchlist to CSV files.
Parameters
----------
results_df : pd.DataFrame
Full event study DataFrame with abnormal returns.
impact_scores : pd.DataFrame
Per-influencer impact score summary.
watchlist : pd.DataFrame
Curated high-impact influencer watchlist.
"""
results_df.to_csv('influencer_event_study.csv', index=False)
impact_scores.to_csv('influencer_impact_scores.csv', index=False)
watchlist.to_csv('influencer_watchlist.csv', index=False)
print('Exported: influencer_event_study.csv')
print('Exported: influencer_impact_scores.csv')
print('Exported: influencer_watchlist.csv')
export_influencer_analysis(results_df, impact_scores, watchlist)Exported: influencer_event_study.csv Exported: influencer_impact_scores.csv Exported: influencer_watchlist.csv
Summary & Next Steps
What We Built
| Step | Output |
|---|---|
| Post sentiment scoring | VADER labels per post |
| Influencer tiering | mega / macro / micro / nano |
| Event study | Abnormal returns at 1h, 4h, 24h, 72h |
| Impact scoring | Statistical significance + effect size |
| Watchlist | Curated high-impact accounts |
| Live signal | Real-time signal generation on new posts |