Crypto Twitter Scraper
Scrape cryptocurrency-related discussions from Twitter and X using the official API and unofficial scraping techniques, extracting post content, engagement metrics, author influence signals, and temporal patterns for comprehensive crypto sentiment analysis.
Scrape Crypto Twitter/X Posts
This notebook outlines a structured approach to programmatically interact with Twitter/X data, specifically focusing on cryptocurrency-related posts. Due to recent changes in Twitter/X's API access policies, direct scraping without authenticated API access or specialized tools can be challenging. This notebook provides a conceptual framework and demonstrates how one would typically structure such an endeavor, including data acquisition (conceptually using snscrape), processing, analysis, and visualization.
Concepts Covered:
| Concept | Description |
|---|---|
| Twitter/X Scraping | Programmatic retrieval of posts from the Twitter/X platform. |
| Data Preprocessing | Cleaning and transforming raw tweet data for analysis. |
| Sentiment Analysis | Determining the emotional tone (positive, negative, neutral) of tweets. |
| Time Series Analysis | Analyzing tweet volume and sentiment trends over time. |
| API Interaction | General principles for interacting with external web services (Twitter/X API conceptually). |
| Error Handling | Strategies for robust script execution, including retries. |
Dependency Installation
This section installs all necessary Python libraries. We'll use snscrape (though its functionality for X is now very limited, it serves as a conceptual placeholder), pandas for data manipulation, matplotlib and seaborn for visualization, nltk for natural language processing (sentiment), and tqdm for progress bars. A requests library is also included for general web interaction concepts.
import sys
# Install core libraries
!{sys.executable} -m pip install pandas matplotlib seaborn tqdm nltk snscrape requests
# Download NLTK data for sentiment analysis (moved to import cell for consolidation)
Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2) Requirement already satisfied: matplotlib in /usr/local/lib/python3.12/dist-packages (3.10.0) Requirement already satisfied: seaborn in /usr/local/lib/python3.12/dist-packages (0.13.2) Requirement already satisfied: tqdm in /usr/local/lib/python3.12/dist-packages (4.67.3) Requirement already satisfied: nltk in /usr/local/lib/python3.12/dist-packages (3.9.1) Requirement already satisfied: snscrape in /usr/local/lib/python3.12/dist-packages (0.7.0.20230622) Requirement already satisfied: requests in /usr/local/lib/python3.12/dist-packages (2.32.4) Requirement already satisfied: numpy>=1.26.0 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.0.2) Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0) Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2) Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.2) Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.3.3) Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (0.12.1) Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (4.63.0) Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.5.0) Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (26.2) Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (11.3.0) Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (3.3.2) Requirement already satisfied: click in /usr/local/lib/python3.12/dist-packages (from nltk) (8.4.1) Requirement already satisfied: joblib in /usr/local/lib/python3.12/dist-packages (from nltk) (1.5.3) Requirement already satisfied: regex>=2021.8.3 in /usr/local/lib/python3.12/dist-packages (from nltk) (2025.11.3) Requirement already satisfied: lxml in /usr/local/lib/python3.12/dist-packages (from snscrape) (6.1.1) Requirement already satisfied: beautifulsoup4 in /usr/local/lib/python3.12/dist-packages (from snscrape) (4.13.5) Requirement already satisfied: filelock in /usr/local/lib/python3.12/dist-packages (from snscrape) (3.29.1) Requirement already satisfied: charset_normalizer<4,>=2 in /usr/local/lib/python3.12/dist-packages (from requests) (3.4.7) Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.12/dist-packages (from requests) (3.18) Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.12/dist-packages (from requests) (2.5.0) Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.12/dist-packages (from requests) (2026.5.20) Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0) Requirement already satisfied: soupsieve>1.2 in /usr/local/lib/python3.12/dist-packages (from beautifulsoup4->snscrape) (2.8.4) Requirement already satisfied: typing-extensions>=4.0.0 in /usr/local/lib/python3.12/dist-packages (from beautifulsoup4->snscrape) (4.15.0) Requirement already satisfied: PySocks!=1.5.7,>=1.5.6 in /usr/local/lib/python3.12/dist-packages (from requests[socks]->snscrape) (1.7.1)
Library Imports
This block imports all required libraries. Standard libraries are imported first, followed by third-party libraries. logging is included for robust feedback.
import sys
import datetime
import time
import random
import logging
from collections import deque
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# import snscrape.modules.twitter as sntwitter # Conceptual for scraping, currently unreliable
from nltk.sentiment.vader import SentimentIntensityAnalyzer
from tqdm.notebook import tqdm
import requests # For general web interaction and exponential backoff concept
import nltk
# Download NLTK data for sentiment analysis
try:
nltk.data.find('sentiment/vader_lexicon.zip')
except LookupError:
nltk.download('vader_lexicon')
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)Core Functions
Function Name: create_scraper_state
This function initializes the state dictionary for the Twitter/X scraping process. It sets up initial parameters like the search query, date range, maximum number of tweets to scrape, and a placeholder for scraped data.
Algorithm:
- Define default values for scraping parameters.
- Create a dictionary containing these parameters.
- Return the initialized state dictionary.
Parameters:
query(str): The search query to use for scraping (e.g., '#bitcoin').since(str): The start date for scraping in 'YYYY-MM-DD' format.until(str): The end date for scraping in 'YYYY-MM-DD' format.max_tweets(int): The maximum number of tweets to retrieve. Defaults to 1000.
Returns:
dict: An initialized state dictionary containing scraping parameters and an empty list forscraped_data.
def create_scraper_state(query: str, since: str, until: str, max_tweets: int = 1000) -> dict:
"""
Initializes the state dictionary for the Twitter/X scraping process.
Parameters
----------
query : str
The search query to use for scraping (e.g., '#bitcoin').
since : str
The start date for scraping in 'YYYY-MM-DD' format.
until : str
The end date for scraping in 'YYYY-MM-DD' format.
max_tweets : int, optional
The maximum number of tweets to retrieve. Defaults to 1000.
Returns
-------
dict
An initialized state dictionary containing scraping parameters and an empty list for `scraped_data`.
Examples
--------
>>> state = create_scraper_state(query='#ethereum', since='2023-01-01', until='2023-01-07', max_tweets=500)
>>> assert 'query' in state
>>> assert state['max_tweets'] == 500
"""
logger.info(f"Initializing scraper state for query: '{query}' from {since} to {until}")
state = {
'query': query,
'since': since,
'until': until,
'max_tweets': max_tweets,
'scraped_data': []
}
logger.debug("Scraper state initialized successfully.")
return stateFunction Name: exponential_backoff_retry
This helper function implements an exponential backoff strategy for retrying an operation that might fail (e.g., API calls, network requests). It introduces random jitter to prevent thundering herd problems.
Algorithm:
- Define a maximum number of retries and initial delay.
- Loop for the specified number of retries.
- Attempt to execute the provided
func. - If
funcsucceeds, return its result. - If
funcfails with a specified exception, log a warning, calculate the delay with exponential backoff and random jitter, and wait. - If all retries fail, re-raise the last exception.
Parameters:
func(callable): The function to execute.exceptions(tuple): A tuple of exception types to catch and retry on.max_retries(int): The maximum number of retry attempts. Defaults to 5.initial_delay(float): The initial delay in seconds before the first retry. Defaults to 1.0.
Returns:
Any: The result of thefuncif successful.
Raises:
Exception: Re-raises the last exception if all retries fail.
def exponential_backoff_retry(func: callable, exceptions: tuple, max_retries: int = 5, initial_delay: float = 1.0):
"""
Implements an exponential backoff strategy for retrying a function.
Parameters
----------
func : callable
The function to execute.
exceptions : tuple
A tuple of exception types to catch and retry on.
max_retries : int, optional
The maximum number of retry attempts. Defaults to 5.
initial_delay : float, optional
The initial delay in seconds before the first retry. Defaults to 1.0.
Returns
-------
Any
The result of the `func` if successful.
Raises
------
Exception
Re-raises the last exception if all retries fail.
Examples
--------
>>> def flaky_api_call():
... if random.random() < 0.7: # Simulate 70% failure rate
... raise requests.exceptions.RequestException("API error")
... return "Data"
>>> # result = exponential_backoff_retry(flaky_api_call, (requests.exceptions.RequestException,)) # This will now work as intended as logger is defined.
>>> # assert result == "Data" # This might still fail sometimes, for demonstration.
"""
delay = initial_delay
for i in range(max_retries):
try:
return func()
except exceptions as e:
logger.warning(f"Attempt {i + 1}/{max_retries} failed with {type(e).__name__}: {e}")
if i == max_retries - 1:
logger.error(f"Max retries reached. Failing permanently.")
raise
jitter = random.uniform(0.5 * delay, 1.5 * delay)
sleep_time = min(delay + jitter, 60.0) # Cap sleep time for practicality
logger.info(f"Retrying in {sleep_time:.2f} seconds...")
time.sleep(sleep_time)
delay *= 2Function Name: scrape_tweets
This function simulates scraping tweets using snscrape. Note: As of late 2022/early 2023, snscrape's ability to reliably scrape Twitter/X without an authenticated API or advanced techniques has been severely limited or broken due to platform changes. This function serves as a conceptual example of how scraping would be integrated, assuming a functional scraping library or API. For real-world applications, consider official Twitter/X APIs or specialized commercial tools if direct scraping is no longer viable.
Algorithm:
- Construct the search string based on the state's query, since, and until dates.
- Initialize
snscrape's Twitter search module (conceptually). - Iterate through the results, collecting relevant tweet attributes.
- Store collected tweet data in the
state['scraped_data']list. - Use
tqdmfor a progress bar.
Parameters:
state(dict): The current scraper state dictionary.
Returns:
dict: The updated state dictionary withscraped_datapopulated.
def scrape_tweets(state: dict) -> dict:
"""
Simulates scraping tweets based on the provided state.
**Note: Due to recent Twitter/X API changes, direct scraping via snscrape may be unreliable.**
This function demonstrates the structure, assuming a functional scraping method.
Parameters
----------
state : dict
The current scraper state dictionary containing 'query', 'since', 'until', and 'max_tweets'.
Returns
-------
dict
The updated state dictionary with `scraped_data` populated.
Examples
--------
>>> # state = create_scraper_state(query='#dogecoin', since='2023-01-01', until='2023-01-02', max_tweets=10)
>>> # updated_state = scrape_tweets(state)
>>> # assert len(updated_state['scraped_data']) <= 10
"""
query = state['query']
since = state['since']
until = state['until']
max_tweets = state['max_tweets']
scraped_tweets_list = []
search_string = f"{query} since:{since} until:{until}"
logger.info(f"Starting conceptual tweet scraping for: '{search_string}' (max_tweets={max_tweets})")
# Simulate scraping due to snscrape limitations for actual X data
# In a real scenario, this would iterate through sntwitter.TwitterSearchScraper(search_string).get_items()
# and perform error handling with exponential_backoff_retry for each API call.
# Generate dummy data for demonstration purposes
for i in tqdm(range(min(max_tweets, 500)), desc="Simulating Tweet Scraping"):
tweet_date = datetime.datetime.strptime(since, '%Y-%m-%d') + datetime.timedelta(days=random.randint(0, (datetime.datetime.strptime(until, '%Y-%m-%d') - datetime.datetime.strptime(since, '%Y-%m-%d')).days))
tweet_time = tweet_date + datetime.timedelta(hours=random.randint(0,23), minutes=random.randint(0,59))
scraped_tweets_list.append({
'date': tweet_time.isoformat(),
'id': f'{random.randint(1000000000000000000, 9999999999999999999)}',
'content': f"This is a sample tweet about {query}. Sentiment: {'positive' if random.random() > 0.5 else 'negative' if random.random() < 0.2 else 'neutral'}. crypto trading analysis!",
'username': f"user_{random.randint(1, 100)}",
'reply_count': random.randint(0, 100),
'retweet_count': random.randint(0, 500),
'like_count': random.randint(0, 2000)
})
# Actual snscrape usage (commented out due to likely breaking changes on Twitter/X)
# try:
# for i, tweet in enumerate(sntwitter.TwitterSearchScraper(search_string).get_items()):
# if i >= max_tweets: break
# scraped_tweets_list.append({
# 'date': tweet.date.isoformat(),
# 'id': tweet.id,
# 'content': tweet.rawContent,
# 'username': tweet.user.username,
# 'reply_count': tweet.replyCount,
# 'retweet_count': tweet.retweetCount,
# 'like_count': tweet.likeCount
# })
# except Exception as e:
# logger.error(f"Error during snscrape operation: {e}. Falling back to dummy data.")
# # Placeholder for dummy data generation if snscrape fails
state['scraped_data'] = scraped_tweets_list
logger.info(f"Finished conceptual tweet scraping. Collected {len(state['scraped_data'])} tweets.")
return stateFunction Name: process_tweets
This function takes the raw scraped tweet data, converts it into a pandas DataFrame, and performs basic preprocessing. This includes converting the 'date' column to datetime objects and ensuring other numerical columns are correctly typed.
Algorithm:
- Convert the list of dictionaries (
scraped_data) into a pandas DataFrame. - Convert the 'date' column to datetime objects.
- Ensure numerical columns ('reply_count', 'retweet_count', 'like_count') are of integer type.
- Handle any potential missing values if necessary (e.g., fill with 0 or drop).
Parameters:
state(dict): The current scraper state dictionary containingscraped_data.
Returns:
dict: The updated state dictionary with a new keyprocessed_dfcontaining the pandas DataFrame.
def process_tweets(state: dict) -> dict:
"""
Processes raw scraped tweet data into a pandas DataFrame and performs basic cleaning.
Parameters
----------
state : dict
The current scraper state dictionary containing 'scraped_data'.
Returns
-------
dict
The updated state dictionary with a new key 'processed_df' containing the pandas DataFrame.
Examples
--------
>>> state_with_data = {'scraped_data': [{'date': '2023-01-01T12:00:00', 'content': 'Test', 'reply_count': 1}]}
>>> updated_state = process_tweets(state_with_data)
>>> assert 'processed_df' in updated_state
>>> assert isinstance(updated_state['processed_df'], pd.DataFrame)
"""
logger.info("Starting tweet data processing.")
if not state['scraped_data']:
logger.warning("No scraped data found. Returning empty DataFrame.")
state['processed_df'] = pd.DataFrame()
return state
df = pd.DataFrame(state['scraped_data'])
# Convert 'date' column to datetime objects
df['date'] = pd.to_datetime(df['date'])
# Ensure numerical columns are correctly typed, filling NaNs with 0 before conversion
numerical_cols = ['reply_count', 'retweet_count', 'like_count']
for col in numerical_cols:
if col in df.columns:
df[col] = df[col].fillna(0).astype(int)
state['processed_df'] = df
logger.info(f"Tweet data processed. DataFrame shape: {df.shape}")
logger.debug(f"Processed DataFrame columns: {df.columns.tolist()}")
return stateFunction Name: analyze_sentiment
This function performs basic sentiment analysis on the tweet content using NLTK's VADER (Valence Aware Dictionary and sEntiment Reasoner) lexicon. It assigns a compound sentiment score to each tweet.
Algorithm:
- Initialize the VADER sentiment intensity analyzer.
- Define a helper function to get the compound sentiment score from a text.
- Apply this helper function to the 'content' column of the
processed_df. - Store the sentiment scores in a new 'sentiment_score' column.
Parameters:
state(dict): The current scraper state dictionary containingprocessed_df.
Returns:
dict: The updated state dictionary withprocessed_dfnow including a 'sentiment_score' column.
def analyze_sentiment(state: dict) -> dict:
"""
Performs sentiment analysis on tweet content using NLTK VADER.
Parameters
----------
state : dict
The current scraper state dictionary containing 'processed_df'.
Returns
-------
dict
The updated state dictionary with 'processed_df' including a 'sentiment_score' column.
Examples
--------
>>> df_example = pd.DataFrame({'content': ['This is great!', 'I hate this.', 'It\'s okay.'], 'date': pd.to_datetime(['2023-01-01', '2023-01-01', '2023-01-01'])})
>>> state_with_df = {'processed_df': df_example}
>>> updated_state = analyze_sentiment(state_with_df)
>>> assert 'sentiment_score' in updated_state['processed_df'].columns
"""
logger.info("Starting sentiment analysis on tweets.")
if state['processed_df'].empty:
logger.warning("Processed DataFrame is empty. Skipping sentiment analysis.")
return state
sid = SentimentIntensityAnalyzer()
def get_vader_sentiment(text: str) -> float:
return sid.polarity_scores(text)['compound']
state['processed_df']['sentiment_score'] = state['processed_df']['content'].apply(get_vader_sentiment)
logger.info("Sentiment analysis completed. 'sentiment_score' column added.")
logger.debug(f"Sentiment scores head:\n{state['processed_df']['sentiment_score'].head()}")
return stateFunction Name: aggregate_daily_metrics
This function aggregates tweet data on a daily basis, calculating daily tweet counts, average sentiment, and total engagement metrics (likes, retweets, replies).
Algorithm:
- Extract the date component from the 'date' column.
- Group the DataFrame by this daily date.
- Calculate the count of tweets, mean sentiment, and sum of engagement metrics for each day.
- Reset the index to make the date a regular column.
Parameters:
state(dict): The current scraper state dictionary containingprocessed_df.
Returns:
dict: The updated state dictionary with a new keydaily_metrics_dfcontaining the aggregated daily DataFrame.
def aggregate_daily_metrics(state: dict) -> dict:
"""
Aggregates tweet data to daily metrics, including tweet count, average sentiment,
and total engagement (likes, retweets, replies).
Parameters
----------
state : dict
The current scraper state dictionary containing 'processed_df'.
Returns
-------
dict
The updated state dictionary with a new key 'daily_metrics_df' containing the aggregated daily DataFrame.
Examples
--------
>>> df_example = pd.DataFrame({
... 'date': pd.to_datetime(['2023-01-01 10:00', '2023-01-01 14:00', '2023-01-02 09:00']),
... 'sentiment_score': [0.5, -0.1, 0.8],
... 'like_count': [10, 5, 20],
... 'retweet_count': [2, 1, 5],
... 'reply_count': [1, 0, 2]
... })
>>> state_with_df = {'processed_df': df_example}
>>> updated_state = aggregate_daily_metrics(state_with_df)
>>> assert 'daily_metrics_df' in updated_state
>>> assert updated_state['daily_metrics_df'].shape[0] == 2 # Two unique dates
"""
logger.info("Aggregating tweet data to daily metrics.")
if state['processed_df'].empty:
logger.warning("Processed DataFrame is empty. Cannot aggregate daily metrics.")
state['daily_metrics_df'] = pd.DataFrame()
return state
df = state['processed_df'].copy()
df['day'] = df['date'].dt.date
daily_metrics_df = df.groupby('day').agg(
tweet_count=('id', 'count'),
avg_sentiment=('sentiment_score', 'mean'),
total_likes=('like_count', 'sum'),
total_retweets=('retweet_count', 'sum'),
total_replies=('reply_count', 'sum')
).reset_index()
daily_metrics_df['day'] = pd.to_datetime(daily_metrics_df['day'])
state['daily_metrics_df'] = daily_metrics_df
logger.info(f"Daily metrics aggregated. DataFrame shape: {daily_metrics_df.shape}")
logger.debug(f"Daily metrics head:\n{daily_metrics_df.head()}")
return stateFunction Name: save_data
This function saves the processed tweet data (DataFrame) to a CSV file. It's a simple utility for persistence.
Algorithm:
- Check if the
processed_dfexists in the state. - If it exists, save the DataFrame to the specified
output_filepathwithout the index.
Parameters:
state(dict): The current scraper state dictionary containingprocessed_df.output_filepath(str): The path to the CSV file where the data will be saved.
Returns:
dict: The unchanged state dictionary.
def save_data(state: dict, output_filepath: str) -> dict:
"""
Saves the processed tweet data (DataFrame) to a CSV file.
Parameters
----------
state : dict
The current scraper state dictionary containing 'processed_df'.
output_filepath : str
The path to the CSV file where the data will be saved.
Returns
-------
dict
The unchanged state dictionary.
Examples
--------
>>> df_to_save = pd.DataFrame({'col1': [1,2], 'col2': ['A','B']})
>>> state_to_save = {'processed_df': df_to_save}
>>> # save_data(state_to_save, 'test_output.csv') # This would create a file.
"""
logger.info(f"Attempting to save data to '{output_filepath}'.")
if 'processed_df' in state and not state['processed_df'].empty:
try:
state['processed_df'].to_csv(output_filepath, index=False)
logger.info(f"Data successfully saved to '{output_filepath}'.")
except Exception as e:
logger.error(f"Error saving data to '{output_filepath}': {e}")
else:
logger.warning("No processed DataFrame found or it is empty. No data saved.")
return stateDemonstration and Visualization
This section demonstrates the full workflow from initializing the state, (conceptually) scraping tweets, processing them, performing sentiment analysis, aggregating daily metrics, and finally visualizing the results. This simulation provides a clear example of how the developed functions are used in practice.
Step 1: Initialize Scraper State
We start by setting up the parameters for our scraping task. Here, we'll search for '#bitcoin' tweets within a specific date range.
# Define scraping parameters
CRYPTO_QUERY = '#bitcoin'
START_DATE = '2023-01-01'
END_DATE = '2023-01-08' # A week of data
MAX_TWEETS_TO_SCRAPE = 1000
# Create the initial state
scraper_state = create_scraper_state(
query=CRYPTO_QUERY,
since=START_DATE,
until=END_DATE,
max_tweets=MAX_TWEETS_TO_SCRAPE
)
print(f"Initial scraper state:\n{scraper_state}")Initial scraper state:
{'query': '#bitcoin', 'since': '2023-01-01', 'until': '2023-01-08', 'max_tweets': 1000, 'scraped_data': []}
Step 2: (Conceptual) Scrape Tweets
This step calls the scrape_tweets function. As noted, this will simulate scraping due to snscrape limitations, providing dummy data that matches the expected structure. In a live scenario, this would attempt to fetch real tweets.
# Perform conceptual scraping
scraper_state = scrape_tweets(scraper_state)
print(f"\nNumber of (simulated) tweets scraped: {len(scraper_state['scraped_data'])}")
if scraper_state['scraped_data']:
print(f"First 3 scraped tweets:\n{scraper_state['scraped_data'][:3]}")Simulating Tweet Scraping: 0%| | 0/500 [00:00<?, ?it/s]
Number of (simulated) tweets scraped: 500
First 3 scraped tweets:
[{'date': '2023-01-03T01:45:00', 'id': '6026456791994122430', 'content': 'This is a sample tweet about #bitcoin. Sentiment: positive. crypto trading analysis!', 'username': 'user_72', 'reply_count': 24, 'retweet_count': 30, 'like_count': 1187}, {'date': '2023-01-02T15:28:00', 'id': '3126245068595037685', 'content': 'This is a sample tweet about #bitcoin. Sentiment: neutral. crypto trading analysis!', 'username': 'user_33', 'reply_count': 6, 'retweet_count': 227, 'like_count': 1353}, {'date': '2023-01-02T12:19:00', 'id': '7002520806860157911', 'content': 'This is a sample tweet about #bitcoin. Sentiment: neutral. crypto trading analysis!', 'username': 'user_33', 'reply_count': 6, 'retweet_count': 450, 'like_count': 366}]
Step 3: Process Scraped Data
Convert the raw list of dictionaries into a clean pandas DataFrame, handling data types.
# Process the scraped data into a DataFrame
scraper_state = process_tweets(scraper_state)
print(f"\nProcessed DataFrame info:")
scraper_state['processed_df'].info()
print(f"\nFirst 5 rows of processed DataFrame:\n{scraper_state['processed_df'].head()}")
Processed DataFrame info:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 500 entries, 0 to 499
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 date 500 non-null datetime64[ns]
1 id 500 non-null object
2 content 500 non-null object
3 username 500 non-null object
4 reply_count 500 non-null int64
5 retweet_count 500 non-null int64
6 like_count 500 non-null int64
dtypes: datetime64[ns](1), int64(3), object(3)
memory usage: 27.5+ KB
First 5 rows of processed DataFrame:
date id \
0 2023-01-03 01:45:00 6026456791994122430
1 2023-01-02 15:28:00 3126245068595037685
2 2023-01-02 12:19:00 7002520806860157911
3 2023-01-08 19:26:00 6163403503156824111
4 2023-01-02 09:38:00 9848387242011883661
content username reply_count \
0 This is a sample tweet about #bitcoin. Sentime... user_72 24
1 This is a sample tweet about #bitcoin. Sentime... user_33 6
2 This is a sample tweet about #bitcoin. Sentime... user_33 6
3 This is a sample tweet about #bitcoin. Sentime... user_89 18
4 This is a sample tweet about #bitcoin. Sentime... user_38 17
retweet_count like_count
0 30 1187
1 227 1353
2 450 366
3 190 1859
4 364 1188
Step 4: Analyze Sentiment
Apply VADER sentiment analysis to the content of each tweet.
# Perform sentiment analysis
scraper_state = analyze_sentiment(scraper_state)
print(f"\nDataFrame head with sentiment scores:\n{scraper_state['processed_df'][['content', 'sentiment_score']].head()}")
print(f"\nSentiment score descriptive statistics:\n{scraper_state['processed_df']['sentiment_score'].describe()}")
DataFrame head with sentiment scores:
content sentiment_score
0 This is a sample tweet about #bitcoin. Sentime... 0.5983
1 This is a sample tweet about #bitcoin. Sentime... 0.0000
2 This is a sample tweet about #bitcoin. Sentime... 0.0000
3 This is a sample tweet about #bitcoin. Sentime... 0.0000
4 This is a sample tweet about #bitcoin. Sentime... -0.6114
Sentiment score descriptive statistics:
count 500.000000
mean 0.222376
std 0.402889
min -0.611400
25% 0.000000
50% 0.000000
75% 0.598300
max 0.598300
Name: sentiment_score, dtype: float64
Step 5: Aggregate Daily Metrics
Aggregate the processed data to get daily summaries of tweet count, average sentiment, and engagement.
# Aggregate daily metrics
scraper_state = aggregate_daily_metrics(scraper_state)
print(f"\nDaily metrics DataFrame head:\n{scraper_state['daily_metrics_df'].head()}")
Daily metrics DataFrame head:
day tweet_count avg_sentiment total_likes total_retweets \
0 2023-01-01 64 0.128627 59919 16525
1 2023-01-02 62 0.220260 59109 16106
2 2023-01-03 69 0.224877 68813 16340
3 2023-01-04 60 0.217412 59927 14758
4 2023-01-05 60 0.238010 58890 14459
total_replies
0 2848
1 2771
2 3357
3 2926
4 3050
Step 6: Visualize Daily Tweet Volume and Sentiment
These plots illustrate the tweet volume and average sentiment over the scraped period. This helps identify trends and potential correlations with market movements.
if not scraper_state['daily_metrics_df'].empty:
plt.figure(figsize=(14, 6))
# Plot 1: Daily Tweet Count
plt.subplot(1, 2, 1)
sns.lineplot(x='day', y='tweet_count', data=scraper_state['daily_metrics_df'], marker='o')
plt.title(f'Daily Tweet Volume for {CRYPTO_QUERY}')
plt.xlabel('Date')
plt.ylabel('Number of Tweets')
plt.grid(True)
plt.xticks(rotation=45)
# Plot 2: Daily Average Sentiment
plt.subplot(1, 2, 2)
sns.lineplot(x='day', y='avg_sentiment', data=scraper_state['daily_metrics_df'], marker='o', color='green')
plt.axhline(y=0, color='r', linestyle='--', label='Neutral Sentiment')
plt.title(f'Daily Average Sentiment for {CRYPTO_QUERY}')
plt.xlabel('Date')
plt.ylabel('Average Sentiment Score')
plt.grid(True)
plt.xticks(rotation=45)
plt.legend()
plt.tight_layout()
plt.show()
else:
logger.warning("Daily metrics DataFrame is empty, skipping daily volume and sentiment plots.")Step 7: Visualize Sentiment Distribution
This histogram shows the overall distribution of sentiment scores across all scraped tweets, helping to understand the general mood around the cryptocurrency.
if 'processed_df' in scraper_state and not scraper_state['processed_df'].empty and 'sentiment_score' in scraper_state['processed_df'].columns:
plt.figure(figsize=(8, 6))
sns.histplot(scraper_state['processed_df']['sentiment_score'], bins=30, kde=True)
plt.title(f'Distribution of Sentiment Scores for {CRYPTO_QUERY} Tweets')
plt.xlabel('Sentiment Score (VADER Compound)')
plt.ylabel('Frequency')
plt.grid(True)
plt.show()
else:
logger.warning("Processed DataFrame or sentiment_score column missing/empty, skipping sentiment distribution plot.")Step 8: Save Processed Data
Finally, save the full processed DataFrame to a CSV file for future use or further analysis.
OUTPUT_FILENAME = 'crypto_tweets_processed.csv'
scraper_state = save_data(scraper_state, OUTPUT_FILENAME)
# Display first few rows of the saved (or hypothetical saved) data
if 'processed_df' in scraper_state and not scraper_state['processed_df'].empty:
print(f"\nSample of data that would be saved to '{OUTPUT_FILENAME}':\n")
display(scraper_state['processed_df'].head())Sample of data that would be saved to 'crypto_tweets_processed.csv':
| date | id | content | username | reply_count | retweet_count | like_count | sentiment_score | |
|---|---|---|---|---|---|---|---|---|
| 0 | 2023-01-03 01:45:00 | 6026456791994122430 | This is a sample tweet about #bitcoin. Sentime... | user_72 | 24 | 30 | 1187 | 0.5983 |
| 1 | 2023-01-02 15:28:00 | 3126245068595037685 | This is a sample tweet about #bitcoin. Sentime... | user_33 | 6 | 227 | 1353 | 0.0000 |
| 2 | 2023-01-02 12:19:00 | 7002520806860157911 | This is a sample tweet about #bitcoin. Sentime... | user_33 | 6 | 450 | 366 | 0.0000 |
| 3 | 2023-01-08 19:26:00 | 6163403503156824111 | This is a sample tweet about #bitcoin. Sentime... | user_89 | 18 | 190 | 1859 | 0.0000 |
| 4 | 2023-01-02 09:38:00 | 9848387242011883661 | This is a sample tweet about #bitcoin. Sentime... | user_38 | 17 | 364 | 1188 | -0.6114 |
Production Considerations
When deploying a Twitter/X scraping and analysis system, several factors need to be considered for robustness, scalability, and compliance.
| Consideration | Best Practice |
|---|---|
| API Keys & Limits | Use official Twitter/X API with proper authentication. Respect rate limits and implement robust exponential backoff and retry mechanisms. Store API keys securely (e.g., Colab secrets, environment variables). |
| Terms of Service | Always review and comply with Twitter/X's Developer Agreement and Policies. Unauthorized scraping can lead to account suspension. |
| Data Storage | For large datasets, consider efficient databases (e.g., PostgreSQL, MongoDB) instead of CSV files. Implement incremental data loading to avoid reprocessing existing data. |
| Scalability | For high-volume scraping, consider distributed architectures (e.g., Apache Kafka, Celery with RabbitMQ) or cloud-based scraping services. |
| Error Handling | Implement comprehensive try-except blocks with logging for network issues, API errors, and unexpected data formats. Use exponential_backoff_retry for transient failures. |
| Data Privacy | Anonymize or aggregate user data if publishing insights. Be mindful of GDPR and other data privacy regulations, especially when handling personal data. |
| Sentiment Model | For more accurate sentiment, fine-tune models on domain-specific (crypto) text. VADER is general-purpose; transformer-based models (e.g., BERT, RoBERTa) might offer better performance. |
| Performance | Optimize data processing with vectorized operations in pandas. Consider parallel processing for time-consuming tasks. |
| Monitoring | Set up monitoring for script execution, API usage, error rates, and data volume to quickly detect and respond to issues. |
Conclusion
This notebook provided a structured framework for (conceptually) scraping and analyzing cryptocurrency-related posts from Twitter/X. We covered the following key components:
- State Management: Initialized and updated a dictionary to manage scraping parameters and data throughout the workflow.
- Conceptual Scraping: Demonstrated how a
scrape_tweetsfunction would integrate, using simulated data due to current API restrictions. - Data Processing: Transformed raw scraped data into a clean pandas DataFrame, handling data types.
- Sentiment Analysis: Applied NLTK's VADER to derive sentiment scores from tweet content.
- Data Aggregation: Summarized tweet activity and sentiment on a daily basis.
- Visualization: Illustrated daily tweet volume, average sentiment, and overall sentiment distribution using
matplotlibandseaborn. - Data Persistence: Included a utility function to save the processed data to a CSV file.
- Production Considerations: Outlined best practices for robust and scalable deployment, emphasizing API compliance and error handling.
While direct access to Twitter/X data without API keys remains challenging, this notebook provides a solid architectural foundation for building social media intelligence systems, focusing on robust function design, data handling, and meaningful visualizations.