Sentiment & NLP·Social Media Scraping·Beginner

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.

data-fetchingsentiment-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:

ConceptDescription
Twitter/X ScrapingProgrammatic retrieval of posts from the Twitter/X platform.
Data PreprocessingCleaning and transforming raw tweet data for analysis.
Sentiment AnalysisDetermining the emotional tone (positive, negative, neutral) of tweets.
Time Series AnalysisAnalyzing tweet volume and sentiment trends over time.
API InteractionGeneral principles for interacting with external web services (Twitter/X API conceptually).
Error HandlingStrategies 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.

[38]
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.

[39]
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:

  1. Define default values for scraping parameters.
  2. Create a dictionary containing these parameters.
  3. 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 for scraped_data.
[28]
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 state

Function 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:

  1. Define a maximum number of retries and initial delay.
  2. Loop for the specified number of retries.
  3. Attempt to execute the provided func.
  4. If func succeeds, return its result.
  5. If func fails with a specified exception, log a warning, calculate the delay with exponential backoff and random jitter, and wait.
  6. 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 the func if successful.

Raises:

  • Exception: Re-raises the last exception if all retries fail.
[29]
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 *= 2

Function 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:

  1. Construct the search string based on the state's query, since, and until dates.
  2. Initialize snscrape's Twitter search module (conceptually).
  3. Iterate through the results, collecting relevant tweet attributes.
  4. Store collected tweet data in the state['scraped_data'] list.
  5. Use tqdm for a progress bar.

Parameters:

  • state (dict): The current scraper state dictionary.

Returns:

  • dict: The updated state dictionary with scraped_data populated.
[30]
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 state

Function 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:

  1. Convert the list of dictionaries (scraped_data) into a pandas DataFrame.
  2. Convert the 'date' column to datetime objects.
  3. Ensure numerical columns ('reply_count', 'retweet_count', 'like_count') are of integer type.
  4. Handle any potential missing values if necessary (e.g., fill with 0 or drop).

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.
[31]
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 state

Function 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:

  1. Initialize the VADER sentiment intensity analyzer.
  2. Define a helper function to get the compound sentiment score from a text.
  3. Apply this helper function to the 'content' column of the processed_df.
  4. Store the sentiment scores in a new 'sentiment_score' column.

Parameters:

  • state (dict): The current scraper state dictionary containing processed_df.

Returns:

  • dict: The updated state dictionary with processed_df now including a 'sentiment_score' column.
[32]
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 state

Function 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:

  1. Extract the date component from the 'date' column.
  2. Group the DataFrame by this daily date.
  3. Calculate the count of tweets, mean sentiment, and sum of engagement metrics for each day.
  4. Reset the index to make the date a regular column.

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.
[33]
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 state

Function Name: save_data

This function saves the processed tweet data (DataFrame) to a CSV file. It's a simple utility for persistence.

Algorithm:

  1. Check if the processed_df exists in the state.
  2. If it exists, save the DataFrame to the specified output_filepath without the index.

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.
[34]
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 state

Demonstration 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.

[20]
# 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.

[21]
# 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.

[22]
# 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.

[23]
# 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.

[24]
# 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.

[25]
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.")
cell output

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.

[26]
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.")
cell output

Step 8: Save Processed Data

Finally, save the full processed DataFrame to a CSV file for future use or further analysis.

[27]
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.

ConsiderationBest Practice
API Keys & LimitsUse 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 ServiceAlways review and comply with Twitter/X's Developer Agreement and Policies. Unauthorized scraping can lead to account suspension.
Data StorageFor large datasets, consider efficient databases (e.g., PostgreSQL, MongoDB) instead of CSV files. Implement incremental data loading to avoid reprocessing existing data.
ScalabilityFor high-volume scraping, consider distributed architectures (e.g., Apache Kafka, Celery with RabbitMQ) or cloud-based scraping services.
Error HandlingImplement comprehensive try-except blocks with logging for network issues, API errors, and unexpected data formats. Use exponential_backoff_retry for transient failures.
Data PrivacyAnonymize or aggregate user data if publishing insights. Be mindful of GDPR and other data privacy regulations, especially when handling personal data.
Sentiment ModelFor 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.
PerformanceOptimize data processing with vectorized operations in pandas. Consider parallel processing for time-consuming tasks.
MonitoringSet 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:

  1. State Management: Initialized and updated a dictionary to manage scraping parameters and data throughout the workflow.
  2. Conceptual Scraping: Demonstrated how a scrape_tweets function would integrate, using simulated data due to current API restrictions.
  3. Data Processing: Transformed raw scraped data into a clean pandas DataFrame, handling data types.
  4. Sentiment Analysis: Applied NLTK's VADER to derive sentiment scores from tweet content.
  5. Data Aggregation: Summarized tweet activity and sentiment on a daily basis.
  6. Visualization: Illustrated daily tweet volume, average sentiment, and overall sentiment distribution using matplotlib and seaborn.
  7. Data Persistence: Included a utility function to save the processed data to a CSV file.
  8. 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.