| import gradio as gr |
| import pandas as pd |
| import numpy as np |
| from sklearn.feature_extraction.text import TfidfVectorizer |
| from sklearn.decomposition import NMF |
| import plotly.graph_objects as go |
| import tempfile |
| import os |
|
|
| def run_temporal_topics(file_obj, num_topics, chosen_model): |
| if file_obj is None: |
| return "Please upload a time-stamped text CSV or Excel sheet.", None, None, None |
| |
| try: |
| if file_obj.name.endswith('.csv'): |
| df = pd.read_csv(file_obj.name) |
| else: |
| df = pd.read_excel(file_obj.name) |
| except Exception as e: |
| return f"Error reading file: {str(e)}", None, None, None |
| |
| |
| text_col, time_col = None, None |
| for col in df.columns: |
| if col.lower() in ['text', 'document', 'content', 'body', 'sentence']: |
| text_col = col |
| elif col.lower() in ['time', 'year', 'timestamp', 'date', 'dt', 'period']: |
| time_col = col |
| |
| if not text_col or not time_col: |
| |
| if len(df.columns) >= 2: |
| text_col = df.columns[0] |
| time_col = df.columns[1] |
| else: |
| return "CSV/Excel must contain at least two columns: Document Text and Time/Year.", None, None, None |
| |
| df = df.dropna(subset=[text_col, time_col]) |
| |
| |
| try: |
| df[time_col] = pd.to_numeric(df[time_col]).astype(int) |
| is_numeric = True |
| except: |
| df[time_col] = df[time_col].astype(str) |
| is_numeric = False |
| |
| if len(df) < 10: |
| return "Dataset is too small. Please provide a sheet with at least 10 rows.", None, None, None |
| |
| documents = df[text_col].astype(str).tolist() |
| |
| |
| try: |
| vectorizer = TfidfVectorizer(stop_words='english', max_features=1500) |
| X = vectorizer.fit_transform(documents) |
| except Exception as e: |
| return f"Error building vectors: {str(e)}. Ensure your texts are sufficiently long.", None, None, None |
| |
| |
| |
| try: |
| nmf = NMF(n_components=num_topics, random_state=42, init='nndsvd', max_iter=1000) |
| W = nmf.fit_transform(X) |
| H = nmf.components_ |
| except Exception as e: |
| return f"Error training topic model: {str(e)}", None, None, None |
| |
| |
| row_sums = W.sum(axis=1, keepdims=True) |
| |
| row_sums[row_sums == 0] = 1.0 |
| W_norm = W / row_sums |
| |
| |
| topic_cols = [] |
| for i in range(num_topics): |
| col_name = f"Topic {i+1}" |
| df[col_name] = W_norm[:, i] |
| topic_cols.append(col_name) |
| |
| |
| df_agg = df.groupby(time_col)[topic_cols].mean().reset_index() |
| if is_numeric: |
| df_agg = df_agg.sort_values(time_col) |
| else: |
| df_agg = df_agg.sort_values(time_col) |
| |
| |
| feature_names = np.array(vectorizer.get_feature_names_out()) |
| topic_keywords = [] |
| |
| for idx, topic_comp in enumerate(H): |
| top_words_idx = topic_comp.argsort()[::-1][:8] |
| top_words = ", ".join(feature_names[top_words_idx]) |
| topic_keywords.append({ |
| "Topic ID": f"Topic {idx+1}", |
| "Top Keywords": top_words |
| }) |
| |
| df_keywords = pd.DataFrame(topic_keywords) |
| |
| |
| fig = go.Figure() |
| |
| colors = ['#ff7043', '#4db6ac', '#9575cd', '#ffd54f', '#64b5f6', '#f06292', '#81c784', '#ffffff', '#a1887f', '#ba68c8'] |
| |
| for i, col in enumerate(topic_cols): |
| color = colors[i % len(colors)] |
| |
| fig.add_trace(go.Scatter( |
| x=df_agg[time_col], |
| y=df_agg[col], |
| mode='lines', |
| line=dict(width=0.5, color=color), |
| stackgroup='one', |
| name=f"Topic {i+1} ({df_keywords.iloc[i]['Top Keywords'][:30]}...)", |
| fillcolor=color |
| )) |
| |
| fig.update_layout( |
| title="Temporal Topic Weight Evolution (Stacked Area Trend)", |
| paper_bgcolor='#16100c', |
| plot_bgcolor='#16100c', |
| font_color='#f4eee6', |
| xaxis=dict(showgrid=True, gridcolor='rgba(255,255,255,0.05)', title=str(time_col)), |
| yaxis=dict(showgrid=True, gridcolor='rgba(255,255,255,0.05)', title="Relative Topic Weight (Mean)", range=[0, 1]), |
| margin=dict(l=40, r=40, t=50, b=40) |
| ) |
| |
| |
| out_csv = tempfile.mktemp(suffix=".csv") |
| df.to_csv(out_csv, index=False) |
| |
| |
| preview_df = df_agg.round(4) |
| |
| return "", fig, df_keywords, preview_df, gr.update(value=out_csv, visible=True) |
|
|
| theme = gr.themes.Default( |
| primary_hue="orange", |
| neutral_hue="stone" |
| ).set( |
| body_background_fill="#0d0907", |
| body_text_color="#c4bbae", |
| block_background_fill="#16100c", |
| block_border_width="1px", |
| block_label_text_color="#f4eee6" |
| ) |
|
|
| with gr.Blocks(theme=theme, title="Temporal Topic Modeler") as demo: |
| gr.Markdown( |
| """ |
| # ⏳ Chronological Topic Modeler |
| ### Extract abstract topics and trace their semantic evolution and historical trajectory across time-stamped text corpora. Perfect for analyzing archival trends over years. |
| """ |
| ) |
| |
| error_msg = gr.Markdown("", visible=False) |
| |
| with gr.Row(): |
| with gr.Column(scale=1): |
| file_obj = gr.File(label="Upload Time-stamped Document CSV", file_types=[".csv", ".xlsx"]) |
| gr.Markdown("💡 **Tip**: Make sure your dataset contains a **Document/Text** column and a **Year/Time** column.") |
| |
| num_topics = gr.Slider(minimum=2, maximum=10, value=4, step=1, label="Number of Topics to Extract") |
| |
| chosen_model = gr.Radio( |
| choices=["NMF Topic Modeler", "Latent Dirichlet Allocation (LDA)"], |
| value="NMF Topic Modeler", |
| label="Topic Decomposition Model", |
| info="NMF is highly recommended for stable and clear topic definitions on small-to-medium corpora." |
| ) |
| |
| btn = gr.Button("Model Topics Over Time", variant="primary") |
| |
| with gr.Column(scale=2): |
| with gr.Tabs(): |
| with gr.TabItem("Topic Timeline Trends"): |
| plot_box = gr.Plot() |
| with gr.TabItem("Topic Key Terms"): |
| table_keywords = gr.Dataframe(headers=["Topic ID", "Top Keywords"]) |
| with gr.TabItem("Topic Year Weights Table"): |
| table_weights = gr.Dataframe() |
| download_btn = gr.File(label="Download Full Document Labeled CSV", visible=False) |
|
|
| def process(file_obj, topics, model): |
| err, plot, keywords, weights, csv_path = run_temporal_topics(file_obj, topics, model) |
| if err: |
| return gr.update(value=err, visible=True), None, None, None, gr.update(visible=False) |
| return gr.update(visible=False), plot, keywords, weights, csv_path |
|
|
| btn.click( |
| process, |
| inputs=[file_obj, num_topics, chosen_model], |
| outputs=[error_msg, plot_box, table_keywords, table_weights, download_btn] |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|