File size: 19,039 Bytes
5f7bd44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aec0fd0
5f7bd44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aec0fd0
5f7bd44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aec0fd0
5f7bd44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aec0fd0
 
 
 
 
5f7bd44
 
 
 
aec0fd0
 
 
 
 
 
 
 
 
5f7bd44
aec0fd0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5f7bd44
 
 
 
 
 
 
 
 
 
 
aec0fd0
 
 
5f7bd44
 
 
 
 
 
 
 
aec0fd0
 
 
 
 
5f7bd44
 
 
 
 
 
 
 
 
 
aec0fd0
 
 
 
 
5f7bd44
 
 
 
 
aec0fd0
5f7bd44
aec0fd0
6d185a4
 
 
 
 
 
 
aec0fd0
6d185a4
 
 
 
aec0fd0
 
 
 
 
6d185a4
 
 
5f7bd44
aec0fd0
 
 
6d185a4
 
aec0fd0
 
 
 
 
 
 
 
 
 
 
5f7bd44
 
6d185a4
aec0fd0
 
5f7bd44
aec0fd0
 
 
5f7bd44
 
 
aec0fd0
5f7bd44
aec0fd0
6d185a4
 
5f7bd44
aec0fd0
 
 
6d185a4
 
 
 
 
 
 
 
aec0fd0
6d185a4
aec0fd0
 
 
6d185a4
aec0fd0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5f7bd44
 
aec0fd0
 
6d185a4
5f7bd44
aec0fd0
 
 
 
 
 
5f7bd44
aec0fd0
 
5f7bd44
 
 
 
 
 
 
 
6d185a4
aec0fd0
 
5f7bd44
 
aec0fd0
 
 
 
 
 
 
 
5f7bd44
 
aec0fd0
5f7bd44
aec0fd0
5f7bd44
 
 
 
 
 
 
 
 
 
 
 
aec0fd0
 
 
 
 
 
 
 
 
 
 
5f7bd44
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
import streamlit as st
import pandas as pd
import plotly.express as px
import os
import re
from datetime import datetime
from textblob import TextBlob
import networkx as nx
from pyvis.network import Network
import streamlit.components.v1 as components

# Transformers & Semantic Search
from transformers import pipeline
from sentence_transformers import SentenceTransformer, util
import wikipedia  # For offline events summary
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
from sklearn.manifold import TSNE

# --------------------------------------------------------------------------------
# ----------------------- Data Loading and Normalization -------------------------
# --------------------------------------------------------------------------------
@st.cache_data
def load_raw_data(filepath):
    """Load the newline-delimited JSON file into a Pandas DataFrame."""
    try:
        raw_df = pd.read_json(filepath, lines=True)
    except ValueError as e:
        st.error("Error reading the JSONL file. Please check the file format.")
        raise e
    return raw_df

DATA_PATH = "data.jsonl"
if not os.path.exists(DATA_PATH):
    st.error("data.jsonl file not found. Please ensure it is in the same directory as this app.")
    st.stop()
else:
    raw_df = load_raw_data(DATA_PATH)

st.sidebar.markdown("### Raw Dataset Columns")
st.sidebar.write(raw_df.columns.tolist())

# Normalize the nested "data" column if present
if 'data' in raw_df.columns:
    try:
        df = pd.json_normalize(raw_df['data'])
    except Exception as e:
        st.error("Error normalizing the 'data' column.")
        df = raw_df
else:
    df = raw_df

st.sidebar.markdown("### Normalized Data Columns")
st.sidebar.write(df.columns.tolist())

# --------------------------------------------------------------------------------
# ------------------------- Column Mapping (Reddit Data) ---------------------------
# --------------------------------------------------------------------------------
# Typical Reddit fields:
timestamp_col = "created_utc"  # Unix timestamp (in seconds)
user_col = "author"            # Author

# For text, prefer "selftext" if available; otherwise, use "title".
if "selftext" in df.columns and df["selftext"].notnull().sum() > 0:
    text_col = "selftext"
elif "title" in df.columns:
    text_col = "title"
else:
    text_col = None

# For hashtags: if not provided, extract them from text using regex.
if "hashtags" not in df.columns:
    def extract_hashtags(row):
        text = ""
        if "title" in row and pd.notnull(row["title"]):
            text += row["title"] + " "
        if "selftext" in row and pd.notnull(row["selftext"]):
            text += row["selftext"]
        return re.findall(r"#\w+", text)
    df["hashtags"] = df.apply(extract_hashtags, axis=1)
hashtags_col = "hashtags"

# Convert Unix timestamp to datetime if available
if timestamp_col in df.columns:
    try:
        df[timestamp_col] = pd.to_datetime(df[timestamp_col], unit='s')
    except Exception as e:
        st.error(f"Error converting timestamp. Check the format of '{timestamp_col}'.")

# --------------------------------------------------------------------------------
# --------------------------- Sidebar: Filters & Platform --------------------------
# --------------------------------------------------------------------------------
st.sidebar.header("Filters & Platform")

# Platform Selector (simulate multiple platforms)
platform = st.sidebar.selectbox("Select Platform", ["Reddit", "Twitter", "Facebook"])
if platform != "Reddit":
    st.sidebar.info(f"Data for {platform} is not available. Showing Reddit data.")

# Date Filter
if timestamp_col in df.columns:
    try:
        min_date = df[timestamp_col].min().date()
        max_date = df[timestamp_col].max().date()
        start_date = st.sidebar.date_input("Start date", min_date, min_value=min_date, max_value=max_date)
        end_date = st.sidebar.date_input("End date", max_date, min_value=min_date, max_value=max_date)
        if start_date > end_date:
            st.sidebar.error("Error: End date must fall after start date.")
        # Filter df between selected dates
        df = df[(df[timestamp_col].dt.date >= start_date) & (df[timestamp_col].dt.date <= end_date)]
    except Exception as e:
        st.sidebar.error("Error processing the timestamp column for filtering.")
else:
    st.sidebar.info(f"No '{timestamp_col}' column found for filtering by date.")

# Keyword/Hashtag Search
search_term = st.sidebar.text_input("Search for a keyword/hashtag:")
if search_term:
    if text_col in df.columns:
        df = df[df[text_col].str.contains(search_term, case=False, na=False)]
    st.sidebar.markdown(f"### Showing results for '{search_term}'")

# --------------------------------------------------------------------------------
# ------------------------- Main Dashboard: Basic Visualizations -----------------
# --------------------------------------------------------------------------------
st.title("Social Media Data Analysis Dashboard")
st.markdown("""
This dashboard visualizes Reddit data, showcasing trends over time, key contributors, topic embeddings, and more.
""")

# Summary Metrics
total_posts = len(df)
st.markdown("### Summary Metrics")
st.write("**Total Posts:**", total_posts)
if user_col in df.columns:
    unique_users = df[user_col].nunique()
    st.write("**Unique Users:**", unique_users)
else:
    st.write("**Unique Users:** Data not available")

# Time Series Plot with 7-day Moving Average
if timestamp_col in df.columns:
    st.markdown("### Posts Over Time with Moving Average")
    df["date"] = df[timestamp_col].dt.date
    time_series = df.groupby("date").size().reset_index(name="count")
    time_series["7-day Moving Avg"] = time_series["count"].rolling(window=7).mean()
    fig_time = px.line(
        time_series, x="date", y=["count", "7-day Moving Avg"],
        labels={"date": "Date", "value": "Number of Posts"},
        title="Posts Over Time with 7-day Moving Average"
    )
    st.plotly_chart(fig_time)
else:
    st.info("No timestamp data available for time series plot.")

# --------------------------------------------------------------------------------
# --------------------------- Network Diagram (Above Pie) -------------------------
# --------------------------------------------------------------------------------
"""
We'll create a user <-> community network from the top users and top subreddits. 
For simplicity, we only include each user/subreddit once to avoid extremely large networks.
"""
st.markdown("### Network Diagram")

community_col = "subreddit" if "subreddit" in df.columns else user_col

# Build a small network of user->community edges
if community_col in df.columns and user_col in df.columns:
    # Let's focus on top communities
    top_communities_df = df[community_col].value_counts().nlargest(5)  # top 5 subreddits or communities
    top_communities = set(top_communities_df.index)

    # For each row, if subreddit in top_communities, link author->subreddit
    # For performance, take a sample of the entire dataset or filter only relevant rows.
    sub_df = df[df[community_col].isin(top_communities)].copy()
    sub_df = sub_df.dropna(subset=[user_col, community_col])
    sub_df = sub_df.sample(min(500, len(sub_df)), random_state=42)  # sample to reduce network size

    net = Network(height="600px", width="100%", notebook=False, bgcolor="#ffffff", font_color="black")

    # We'll track which nodes we've added to avoid duplicates
    added_users = set()
    added_comms = set()

    for _, row in sub_df.iterrows():
        user = str(row[user_col])
        comm = str(row[community_col])

        if user not in added_users:
            net.add_node(user, label=user, color="#FFAAAA")  # user node
            added_users.add(user)

        if comm not in added_comms:
            net.add_node(comm, label=comm, color="#AAAACC")  # community node
            added_comms.add(comm)

        net.add_edge(user, comm)

    net.set_options("""
    var options = {
      "nodes": {
        "scaling": {
          "min": 10,
          "max": 30
        }
      },
      "edges": {
        "smooth": {
          "type": "continuous"
        }
      },
      "physics": {
        "barnesHut": {
          "gravitationalConstant": -8000,
          "springLength": 250
        }
      }
    }
    """)
    # Generate network HTML
    net.save_graph("network.html")
    html_file = open("network.html", "r", encoding="utf-8")
    components.html(html_file.read(), height=620)
    html_file.close()
else:
    st.info("Cannot build a network diagram without both user and community/subreddit columns.")

# --------------------------------------------------------------------------------
# --------------------------- Pie Chart of Top Contributors -----------------------
# --------------------------------------------------------------------------------
if community_col in df.columns:
    st.markdown("### Top Communities/Accounts Contributions")
    contributions = df[community_col].value_counts().reset_index()
    contributions.columns = [community_col, "count"]
    top_contributions = contributions.head(10)
    fig_pie = px.pie(top_contributions, values="count", names=community_col,
                     title="Top 10 Contributors")
    st.plotly_chart(fig_pie)
else:
    st.info("No community or account data available for contributor pie chart.")

# --------------------------------------------------------------------------------
# ---------------------- Top Hashtags & Sentiment Analysis -----------------------
# --------------------------------------------------------------------------------
# Top Hashtags Bar Chart
if hashtags_col in df.columns:
    st.markdown("### Top Hashtags")
    hashtags_exploded = df.explode(hashtags_col)
    hashtags_exploded = hashtags_exploded[hashtags_exploded[hashtags_col] != ""]
    top_hashtags = hashtags_exploded[hashtags_col].value_counts().reset_index()
    top_hashtags.columns = ['hashtag', 'count']
    if not top_hashtags.empty:
        fig_hashtags = px.bar(
            top_hashtags.head(10), x='hashtag', y='count',
            labels={'hashtag': 'Hashtag', 'count': 'Frequency'},
            title="Top 10 Hashtags"
        )
        st.plotly_chart(fig_hashtags)
    else:
        st.info("No hashtag data available.")
else:
    st.info("No 'hashtags' column found in the dataset.")

# Sentiment Analysis on Text Data
if text_col is not None and text_col in df.columns:
    st.markdown("### Sentiment Analysis")
    df['sentiment'] = df[text_col].apply(lambda x: TextBlob(x).sentiment.polarity if isinstance(x, str) else 0)
    fig_sentiment = px.histogram(
        df, x='sentiment', nbins=30,
        labels={'sentiment': 'Sentiment Polarity'},
        title="Sentiment Polarity Distribution"
    )
    st.plotly_chart(fig_sentiment)
else:
    st.info(f"No '{text_col}' column available for sentiment analysis.")

# --------------------------------------------------------------------------------
# ------------------------- Topic Embedding Visualization -------------------------
# --------------------------------------------------------------------------------
st.markdown("## Topic Embedding Visualization (LDA + TSNE)")
if text_col in df.columns:
    texts = df[text_col].dropna().sample(n=min(500, len(df)), random_state=42).tolist()
    vectorizer = CountVectorizer(stop_words='english', max_features=1000)
    X = vectorizer.fit_transform(texts)
    lda = LatentDirichletAllocation(n_components=5, random_state=42)
    topic_matrix = lda.fit_transform(X)
    dominant_topic = topic_matrix.argmax(axis=1)

    tsne_model = TSNE(n_components=2, random_state=42)
    tsne_values = tsne_model.fit_transform(topic_matrix)
    tsne_df = pd.DataFrame(tsne_values, columns=["x", "y"])
    tsne_df["Dominant Topic"] = dominant_topic.astype(str)

    fig_topics = px.scatter(
        tsne_df, x="x", y="y", color="Dominant Topic",
        title="TSNE Embedding of Topics"
    )
    st.plotly_chart(fig_topics)
else:
    st.info("No text data available for topic embedding.")

# --------------------------------------------------------------------------------
# ----------------------- GenAI Summary for Time Series Plot ---------------------
# --------------------------------------------------------------------------------
st.markdown("## GenAI Summary for Time Series")
if timestamp_col in df.columns:
    time_df = df.groupby(df[timestamp_col].dt.date).size().reset_index(name="count")
    if not time_df.empty:
        start = time_df[timestamp_col].min()
        end = time_df[timestamp_col].max()
        avg_posts = time_df["count"].mean()
        peak = time_df.loc[time_df["count"].idxmax()]
        description = (
            f"From {start} to {end}, the average number of posts per day was {avg_posts:.1f}. "
            f"The highest activity was on {peak[timestamp_col]} with {peak['count']} posts."
        )

        st.write("Time Series Description:")
        st.write(description)

        # Use a smaller, faster FLAN-T5 model
        ts_summarizer = pipeline("text2text-generation", model="google/flan-t5-base")
        try:
            # We'll prompt it in a summarization style for clarity
            prompt = f"Summarize this data description: {description}"
            ts_summary = ts_summarizer(prompt, max_length=80, do_sample=False)[0]['generated_text']
            st.markdown("**GenAI Summary:**")
            st.write(ts_summary)
        except Exception as e:
            st.error(f"Error generating time series summary: {e}")
    else:
        st.info("No data available for time series summarization.")
else:
    st.info("No timestamp column available for time series summary.")

# --------------------------------------------------------------------------------
# ----------------------- Offline Events from Wikipedia --------------------------
# --------------------------------------------------------------------------------
st.markdown("## Offline Events from Wikipedia")
wiki_topic = st.text_input("Enter a topic to fetch offline events (e.g., 'Russian invasion of Ukraine'):")
if wiki_topic:
    try:
        wiki_summary = wikipedia.summary(wiki_topic, sentences=5)
        st.markdown(f"**Wikipedia Summary for '{wiki_topic}':**")
        st.write(wiki_summary)
    except Exception as e:
        st.error(f"Error retrieving Wikipedia data: {e}")

# --------------------------------------------------------------------------------
# ----------------- Semantic Search on Posts using Sentence Transformers ---------
# --------------------------------------------------------------------------------
st.markdown("## Semantic Search on Posts")
if text_col and text_col in df.columns:
    search_query = st.text_input("Enter your semantic search query:")
    if search_query:
        @st.cache_data
        def get_post_embeddings(texts):
            # Use a smaller, faster model
            model = SentenceTransformer("sentence-transformers/all-distilroberta-v1")
            return model.encode(texts, convert_to_tensor=True)

        posts = df[text_col].dropna().tolist()

        if posts:
            embeddings = get_post_embeddings(posts)
            model = SentenceTransformer("sentence-transformers/all-distilroberta-v1")
            query_embedding = model.encode(search_query, convert_to_tensor=True)

            cos_scores = util.cos_sim(query_embedding, embeddings)[0]
            top_results = cos_scores.topk(5)

            st.markdown("**Top Matching Posts:**")
            for score, idx in zip(top_results.values, top_results.indices):
                st.write(f"Score: {score.item():.3f}")
                st.write(posts[idx])
                st.write("---")
        else:
            st.info("No text data available for semantic search.")
else:
    st.info("No text column available to perform semantic search.")

# --------------------------------------------------------------------------------
# ------------------------ AI-Generated Summary of Posts -------------------------
# --------------------------------------------------------------------------------
st.markdown("## AI-Generated Summary of Posts")
if text_col in df.columns:
    # Use the same FLAN-T5 base model or DistilBart for summarization
    summarizer = pipeline("text2text-generation", model="google/flan-t5-base")

    def generate_summary(text, summarizer, max_chunk_length=1000):
        """
        Break text into chunks of up to max_chunk_length,
        and pass them through the summarizer in sequence,
        then do a final summarization pass on the combined summary.
        """
        sentences = text.split('. ')
        chunks, current_chunk = [], ""

        for sentence in sentences:
            sentence = sentence.strip() + ". "
            if len(current_chunk) + len(sentence) <= max_chunk_length:
                current_chunk += sentence
            else:
                chunks.append(current_chunk.strip())
                current_chunk = sentence
        if current_chunk:
            chunks.append(current_chunk.strip())

        # Summarize each chunk
        interim_summaries = []
        for chunk in chunks:
            if len(chunk) > 50:
                prompt = f"Summarize this text: {chunk}"
                summary_chunk = summarizer(prompt, max_length=150, do_sample=False)[0]['generated_text']
                interim_summaries.append(summary_chunk)

        # Summarize the combined interim summary
        combined_summary = " ".join(interim_summaries)
        final_prompt = f"Summarize this overall text: {combined_summary}"
        final_summary = summarizer(final_prompt, max_length=150, do_sample=False)[0]['generated_text']
        return final_summary

    # Take a sample of up to 10 random posts
    sample_text = " ".join(df[text_col].dropna().sample(n=min(10, len(df)), random_state=42).tolist())
    if sample_text.strip():
        final_summary = generate_summary(sample_text, summarizer, max_chunk_length=1000)
        st.write(final_summary)
    else:
        st.info("Not enough text data available for summarization.")
else:
    st.info("No text data available for AI summarization.")

# --------------------------------------------------------------------------------
# ------------------------------- End of Dashboard -------------------------------
# --------------------------------------------------------------------------------
st.markdown("### End of Dashboard")
st.markdown("""
This dashboard is a prototype for analyzing Reddit social media data.  
It demonstrates:
- Trend analysis with a 7-day moving average
- A user-to-community network diagram
- Top contributors and hashtags
- Sentiment analysis
- Topic embeddings with LDA + t-SNE
- **GenAI time series summary** (FLAN-T5)
- **Offline Wikipedia events** integration
- **Semantic search** with Sentence Transformers
- **Full AI-generated summary** of posts
""")