bertrend 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bertrend/BERTopicModel.py +320 -0
- bertrend/BERTrend.py +1211 -0
- bertrend/__init__.py +59 -0
- bertrend/config/bertopic_default_config.toml +48 -0
- bertrend/config/bertrend_default_config.toml +16 -0
- bertrend/config/parameters.py +92 -0
- bertrend/config/services_default_config.toml +32 -0
- bertrend/demos/__init__.py +4 -0
- bertrend/demos/demos_utils/__init__.py +13 -0
- bertrend/demos/demos_utils/data_loading_component.py +267 -0
- bertrend/demos/demos_utils/embed_documents_component.py +52 -0
- bertrend/demos/demos_utils/icons.py +41 -0
- bertrend/demos/demos_utils/messages.py +11 -0
- bertrend/demos/demos_utils/parameters_component.py +162 -0
- bertrend/demos/demos_utils/state_utils.py +99 -0
- bertrend/demos/start_summarizer_app.sh +9 -0
- bertrend/demos/start_topic_analysis_app.sh +9 -0
- bertrend/demos/start_weak_signals_app.sh +9 -0
- bertrend/demos/summarization/__init__.py +4 -0
- bertrend/demos/summarization/summarizer_app.py +98 -0
- bertrend/demos/topic_analysis/__init__.py +4 -0
- bertrend/demos/topic_analysis/app.py +87 -0
- bertrend/demos/topic_analysis/app_utils.py +69 -0
- bertrend/demos/topic_analysis/demo_pages/__init__.py +4 -0
- bertrend/demos/topic_analysis/demo_pages/explore_topics.py +455 -0
- bertrend/demos/topic_analysis/demo_pages/newsletters_generation.py +178 -0
- bertrend/demos/topic_analysis/demo_pages/temporal_visualizations.py +573 -0
- bertrend/demos/topic_analysis/demo_pages/topic_visualizations.py +280 -0
- bertrend/demos/topic_analysis/demo_pages/training_page.py +219 -0
- bertrend/demos/topic_analysis/messages.py +13 -0
- bertrend/demos/weak_signals/__init__.py +4 -0
- bertrend/demos/weak_signals/app.py +510 -0
- bertrend/demos/weak_signals/messages.py +32 -0
- bertrend/demos/weak_signals/visualizations_utils.py +403 -0
- bertrend/llm_utils/__init__.py +4 -0
- bertrend/llm_utils/newsletter.css +131 -0
- bertrend/llm_utils/newsletter_features.py +272 -0
- bertrend/llm_utils/openai_client.py +235 -0
- bertrend/llm_utils/prompts.py +94 -0
- bertrend/metrics/__init__.py +4 -0
- bertrend/metrics/temporal_metrics_embedding.py +861 -0
- bertrend/metrics/topic_metrics.py +199 -0
- bertrend/resources/common_ngrams.json +23 -0
- bertrend/resources/stopwords-en.json +3 -0
- bertrend/resources/stopwords-fr.json +1 -0
- bertrend/resources/stopwords-rte.json +15 -0
- bertrend/services/__init__.py +4 -0
- bertrend/services/authentication.py +66 -0
- bertrend/services/embedding_client.py +154 -0
- bertrend/services/embedding_service.py +380 -0
- bertrend/services/summarizer.py +45 -0
- bertrend/services/summary/__init__.py +4 -0
- bertrend/services/summary/abstractive_summarizer.py +74 -0
- bertrend/services/summary/chatgpt_summarizer.py +67 -0
- bertrend/services/summary/extractive_summarizer.py +368 -0
- bertrend/services/summary/lexrank.py +126 -0
- bertrend/services/summary/prompts.py +43 -0
- bertrend/tests/__init__.py +4 -0
- bertrend/tests/test_abstractive_summarizer.py +140 -0
- bertrend/tests/test_bertrend.py +1091 -0
- bertrend/tests/test_cache_utils.py +111 -0
- bertrend/tests/test_chatgpt_summarizer.py +150 -0
- bertrend/tests/test_config_utils.py +54 -0
- bertrend/tests/test_data_loading.py +326 -0
- bertrend/tests/test_data_structure.py +53 -0
- bertrend/tests/test_embedding_client.py +546 -0
- bertrend/tests/test_extractive_summarizer.py +250 -0
- bertrend/tests/test_installation.py +183 -0
- bertrend/tests/test_newsletter_features.py +354 -0
- bertrend/tests/test_openai_client.py +210 -0
- bertrend/tests/test_representative_docs.py +125 -0
- bertrend/tests/test_summarizer.py +115 -0
- bertrend/tests/test_temporal_metrics_embedding.py +349 -0
- bertrend/tests/test_topic_description.py +159 -0
- bertrend/tests/test_topic_metrics.py +233 -0
- bertrend/tests/test_topic_model.py +285 -0
- bertrend/tests/test_trend_analysis_data_structure.py +174 -0
- bertrend/tests/test_trend_analysis_prompts.py +225 -0
- bertrend/tests/test_trend_analysis_visualizations.py +307 -0
- bertrend/tests/test_trend_analysis_weak_signals.py +598 -0
- bertrend/tests/test_utils_init.py +70 -0
- bertrend/tests/test_visualizations.py +306 -0
- bertrend/topic_analysis/__init__.py +4 -0
- bertrend/topic_analysis/data_structure.py +13 -0
- bertrend/topic_analysis/prompts.py +42 -0
- bertrend/topic_analysis/representative_docs.py +57 -0
- bertrend/topic_analysis/topic_description.py +66 -0
- bertrend/topic_analysis/visualizations.py +315 -0
- bertrend/trend_analysis/__init__.py +4 -0
- bertrend/trend_analysis/data_structure.py +78 -0
- bertrend/trend_analysis/prompts.py +229 -0
- bertrend/trend_analysis/signal_llm_template_en.html +181 -0
- bertrend/trend_analysis/signal_llm_template_fr.html +181 -0
- bertrend/trend_analysis/visualizations.py +605 -0
- bertrend/trend_analysis/weak_signals.py +425 -0
- bertrend/utils/__init__.py +31 -0
- bertrend/utils/cache_utils.py +31 -0
- bertrend/utils/config_utils.py +41 -0
- bertrend/utils/data_loading.py +329 -0
- bertrend-0.3.1.dist-info/AUTHORS.txt +15 -0
- bertrend-0.3.1.dist-info/LICENSE.md +373 -0
- bertrend-0.3.1.dist-info/METADATA +400 -0
- bertrend-0.3.1.dist-info/RECORD +181 -0
- bertrend-0.3.1.dist-info/WHEEL +4 -0
- bertrend_apps/__init__.py +4 -0
- bertrend_apps/common/__init__.py +4 -0
- bertrend_apps/common/crontab_utils.py +113 -0
- bertrend_apps/common/mail_utils.py +91 -0
- bertrend_apps/config/feeds/arxiv_feed.toml +15 -0
- bertrend_apps/config/feeds/decarbonation_feed.toml +15 -0
- bertrend_apps/config/feeds/llm_en_feed.toml +15 -0
- bertrend_apps/config/feeds/llm_feed.toml +16 -0
- bertrend_apps/config/feeds/nlp_feed.toml +15 -0
- bertrend_apps/config/feeds/rte_feed.toml +15 -0
- bertrend_apps/config/gmail_credentials.json +1 -0
- bertrend_apps/config/newsletters/arxiv_newsletter_from_last.toml +39 -0
- bertrend_apps/config/newsletters/arxiv_newsletter_from_scratch.toml +39 -0
- bertrend_apps/config/newsletters/decarbonation_newsletter_from_last.toml +38 -0
- bertrend_apps/config/newsletters/decarbonation_newsletter_from_scratch.toml +38 -0
- bertrend_apps/config/newsletters/llm_en_newsletter_from_last.toml +38 -0
- bertrend_apps/config/newsletters/llm_newsletter_from_last.toml +39 -0
- bertrend_apps/config/newsletters/nlp_newsletter_from_last.toml +38 -0
- bertrend_apps/config/newsletters/rte_newsletter_from_last.toml +38 -0
- bertrend_apps/config/newsletters/rte_newsletter_from_scratch.toml +38 -0
- bertrend_apps/data_provider/__init__.py +9 -0
- bertrend_apps/data_provider/__main__.py +237 -0
- bertrend_apps/data_provider/arxiv_provider.py +161 -0
- bertrend_apps/data_provider/bing_news_provider.py +82 -0
- bertrend_apps/data_provider/curebot_provider.py +105 -0
- bertrend_apps/data_provider/data_provider.py +194 -0
- bertrend_apps/data_provider/google_news_provider.py +94 -0
- bertrend_apps/data_provider/newscatcher_provider.py +74 -0
- bertrend_apps/data_provider/utils.py +165 -0
- bertrend_apps/exploration/__init__.py +4 -0
- bertrend_apps/exploration/curebot/__init__.py +4 -0
- bertrend_apps/exploration/curebot/app.py +45 -0
- bertrend_apps/exploration/curebot/app_utils.py +317 -0
- bertrend_apps/exploration/curebot/config.toml +33 -0
- bertrend_apps/exploration/curebot/import_extraction_article.ipynb +108 -0
- bertrend_apps/exploration/curebot/old_veille_analyse.py +298 -0
- bertrend_apps/exploration/curebot/prompts.py +24 -0
- bertrend_apps/exploration/curebot/start_newsletter_generator.sh +8 -0
- bertrend_apps/exploration/curebot/tabs/tab1.py +150 -0
- bertrend_apps/exploration/curebot/tabs/tab2.py +95 -0
- bertrend_apps/exploration/curebot/tabs/tab3.py +125 -0
- bertrend_apps/exploration/dataviz/Main_page.py +10 -0
- bertrend_apps/exploration/dataviz/__init__.py +4 -0
- bertrend_apps/exploration/dataviz/pages/1_Data_feeds.py +105 -0
- bertrend_apps/exploration/geolocalization/__init__.py +4 -0
- bertrend_apps/exploration/geolocalization/entity_utils.py +118 -0
- bertrend_apps/exploration/geolocalization/spacy/__init__.py +4 -0
- bertrend_apps/exploration/geolocalization/spacy/common_factory.py +142 -0
- bertrend_apps/exploration/geolocalization/spacy/dbpedia.py +206 -0
- bertrend_apps/exploration/geolocalization/spacy/nlp.config +37 -0
- bertrend_apps/exploration/geolocalization/spacy/utils.py +39 -0
- bertrend_apps/install_feeds_and_newsletters.py +29 -0
- bertrend_apps/newsletters/__init__.py +4 -0
- bertrend_apps/newsletters/__main__.py +277 -0
- bertrend_apps/prospective_demo/.streamlit/config.toml +3 -0
- bertrend_apps/prospective_demo/.streamlit/secrets.toml +9 -0
- bertrend_apps/prospective_demo/__init__.py +70 -0
- bertrend_apps/prospective_demo/app.py +111 -0
- bertrend_apps/prospective_demo/authentication.py +44 -0
- bertrend_apps/prospective_demo/dashboard_analysis.py +132 -0
- bertrend_apps/prospective_demo/dashboard_common.py +79 -0
- bertrend_apps/prospective_demo/dashboard_signals.py +226 -0
- bertrend_apps/prospective_demo/feeds_common.py +38 -0
- bertrend_apps/prospective_demo/feeds_config.py +261 -0
- bertrend_apps/prospective_demo/feeds_data.py +95 -0
- bertrend_apps/prospective_demo/i18n.py +136 -0
- bertrend_apps/prospective_demo/llm_utils.py +34 -0
- bertrend_apps/prospective_demo/models_info.py +361 -0
- bertrend_apps/prospective_demo/perf_utils.py +23 -0
- bertrend_apps/prospective_demo/process_new_data.py +348 -0
- bertrend_apps/prospective_demo/report_generation.py +214 -0
- bertrend_apps/prospective_demo/report_template.html +148 -0
- bertrend_apps/prospective_demo/report_utils.py +132 -0
- bertrend_apps/prospective_demo/start_demo.sh +9 -0
- bertrend_apps/prospective_demo/streamlit_utils.py +37 -0
- bertrend_apps/start_apps.sh +15 -0
- bertrend_apps/stop_apps.sh +11 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
# Copyright (c) 2024, RTE (https://www.rte-france.com)
|
|
2
|
+
# See AUTHORS.txt
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This file is part of BERTrend.
|
|
5
|
+
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import streamlit as st
|
|
10
|
+
|
|
11
|
+
from bertrend.demos.demos_utils.icons import ERROR_ICON, INFO_ICON
|
|
12
|
+
from bertrend.demos.topic_analysis.messages import (
|
|
13
|
+
TRAIN_MODEL_FIRST_ERROR,
|
|
14
|
+
REMOTE_EMBEDDING_SEVICE_TYPE_NOT_SUPPORTED_ERROR,
|
|
15
|
+
)
|
|
16
|
+
from bertrend.metrics.temporal_metrics_embedding import TempTopic
|
|
17
|
+
from bertrend.demos.topic_analysis.app_utils import (
|
|
18
|
+
compute_topics_over_time,
|
|
19
|
+
)
|
|
20
|
+
from bertrend.topic_analysis.visualizations import (
|
|
21
|
+
plot_topics_over_time,
|
|
22
|
+
plot_topic_evolution,
|
|
23
|
+
plot_temporal_stability_metrics,
|
|
24
|
+
plot_overall_topic_stability,
|
|
25
|
+
)
|
|
26
|
+
from bertrend.demos.demos_utils.state_utils import (
|
|
27
|
+
register_widget,
|
|
28
|
+
save_widget_state,
|
|
29
|
+
restore_widget_state,
|
|
30
|
+
register_multiple_widget,
|
|
31
|
+
SessionStateManager,
|
|
32
|
+
)
|
|
33
|
+
from bertrend.demos.weak_signals.visualizations_utils import PLOTLY_BUTTON_SAVE_CONFIG
|
|
34
|
+
from bertrend.utils.data_loading import (
|
|
35
|
+
TIMESTAMP_COLUMN,
|
|
36
|
+
TEXT_COLUMN,
|
|
37
|
+
DOCUMENT_ID_COLUMN,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def check_model_trained():
|
|
42
|
+
"""Check if a model is trained and display an error if not."""
|
|
43
|
+
if "topic_model" not in st.session_state:
|
|
44
|
+
st.error(
|
|
45
|
+
TRAIN_MODEL_FIRST_ERROR,
|
|
46
|
+
icon=ERROR_ICON,
|
|
47
|
+
)
|
|
48
|
+
st.stop()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def check_embedding_type():
|
|
52
|
+
"""Check the embedding service type. Remote embedding service currently not support these visualizations."""
|
|
53
|
+
if SessionStateManager.get("embedding_service_type", "remote") != "local":
|
|
54
|
+
st.error(
|
|
55
|
+
REMOTE_EMBEDDING_SEVICE_TYPE_NOT_SUPPORTED_ERROR,
|
|
56
|
+
icon=ERROR_ICON,
|
|
57
|
+
)
|
|
58
|
+
st.stop()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parameters_changed():
|
|
62
|
+
"""Check if any of the parameters have changed."""
|
|
63
|
+
params_to_check = [
|
|
64
|
+
"window_size",
|
|
65
|
+
"k",
|
|
66
|
+
"alpha",
|
|
67
|
+
"double_agg",
|
|
68
|
+
"doc_agg",
|
|
69
|
+
"global_agg",
|
|
70
|
+
"evolution_tuning",
|
|
71
|
+
"global_tuning",
|
|
72
|
+
"granularity",
|
|
73
|
+
]
|
|
74
|
+
return any(
|
|
75
|
+
st.session_state.get(f"prev_{param}") != st.session_state.get(param)
|
|
76
|
+
for param in params_to_check
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def display_sidebar():
|
|
81
|
+
"""Display the sidebar with TEMPTopic parameters."""
|
|
82
|
+
with st.sidebar:
|
|
83
|
+
register_multiple_widget(
|
|
84
|
+
"window_size",
|
|
85
|
+
"k",
|
|
86
|
+
"alpha",
|
|
87
|
+
"double_agg",
|
|
88
|
+
"doc_agg",
|
|
89
|
+
"global_agg",
|
|
90
|
+
"evolution_tuning",
|
|
91
|
+
"global_tuning",
|
|
92
|
+
)
|
|
93
|
+
with st.expander("TEMPTopic Parameters", expanded=False):
|
|
94
|
+
st.number_input(
|
|
95
|
+
"Window Size",
|
|
96
|
+
min_value=2,
|
|
97
|
+
value=2,
|
|
98
|
+
step=1,
|
|
99
|
+
key="window_size",
|
|
100
|
+
on_change=save_widget_state,
|
|
101
|
+
)
|
|
102
|
+
st.number_input(
|
|
103
|
+
"Number of Nearest Embeddings (k)",
|
|
104
|
+
min_value=1,
|
|
105
|
+
value=1,
|
|
106
|
+
step=1,
|
|
107
|
+
key="k",
|
|
108
|
+
on_change=save_widget_state,
|
|
109
|
+
help="The k-th nearest neighbor used for Topic Representation Stability calculation.",
|
|
110
|
+
)
|
|
111
|
+
st.number_input(
|
|
112
|
+
"Alpha (Topic vs Representation Stability Weight)",
|
|
113
|
+
min_value=0.0,
|
|
114
|
+
max_value=1.0,
|
|
115
|
+
value=0.8,
|
|
116
|
+
step=0.01,
|
|
117
|
+
key="alpha",
|
|
118
|
+
help="Closer to 1 gives more weight given to Topic Embedding Stability, Closer to 0 gives more weight to topic representation stability.",
|
|
119
|
+
on_change=save_widget_state,
|
|
120
|
+
)
|
|
121
|
+
st.checkbox(
|
|
122
|
+
"Use Double Aggregation",
|
|
123
|
+
value=True,
|
|
124
|
+
key="double_agg",
|
|
125
|
+
on_change=save_widget_state,
|
|
126
|
+
help="If unchecked, only Document Aggregation Method will be globally used.",
|
|
127
|
+
)
|
|
128
|
+
st.selectbox(
|
|
129
|
+
"Document Aggregation Method",
|
|
130
|
+
["mean", "max"],
|
|
131
|
+
key="doc_agg",
|
|
132
|
+
on_change=save_widget_state,
|
|
133
|
+
)
|
|
134
|
+
st.selectbox(
|
|
135
|
+
"Global Aggregation Method",
|
|
136
|
+
["max", "mean"],
|
|
137
|
+
key="global_agg",
|
|
138
|
+
on_change=save_widget_state,
|
|
139
|
+
)
|
|
140
|
+
st.checkbox(
|
|
141
|
+
"Use Evolution Tuning",
|
|
142
|
+
value=True,
|
|
143
|
+
key="evolution_tuning",
|
|
144
|
+
on_change=save_widget_state,
|
|
145
|
+
)
|
|
146
|
+
st.checkbox(
|
|
147
|
+
"Use Global Tuning",
|
|
148
|
+
value=False,
|
|
149
|
+
key="global_tuning",
|
|
150
|
+
on_change=save_widget_state,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _format_timedelta(td: timedelta) -> str:
|
|
155
|
+
"""Format a timedelta object into a string with days, hours, minutes, and seconds."""
|
|
156
|
+
days = td.days
|
|
157
|
+
hours, remainder = divmod(td.seconds, 3600)
|
|
158
|
+
minutes, seconds = divmod(remainder, 60)
|
|
159
|
+
parts = []
|
|
160
|
+
if days > 0:
|
|
161
|
+
parts.append(f"{days} day{'s' if days > 1 else ''}")
|
|
162
|
+
if hours > 0:
|
|
163
|
+
parts.append(f"{hours} hour{'s' if hours > 1 else ''}")
|
|
164
|
+
if minutes > 0:
|
|
165
|
+
parts.append(f"{minutes} minute{'s' if minutes > 1 else ''}")
|
|
166
|
+
if (
|
|
167
|
+
seconds > 0 or not parts
|
|
168
|
+
): # Always show seconds if no larger units or if it's the only non-zero unit
|
|
169
|
+
parts.append(f"{seconds} second{'s' if seconds > 1 else ''}")
|
|
170
|
+
return " ".join(parts)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def select_time_granularity(max_granularity: timedelta) -> timedelta:
|
|
174
|
+
"""Allow user to select custom time granularity within limits."""
|
|
175
|
+
col0, col1, col2, col3, col4 = st.columns(5)
|
|
176
|
+
|
|
177
|
+
max_days = max_granularity.days
|
|
178
|
+
register_multiple_widget(
|
|
179
|
+
"granularity_days", "granularity_hours", "granularity_minutes"
|
|
180
|
+
)
|
|
181
|
+
with col0:
|
|
182
|
+
st.write("Select custom time granularity")
|
|
183
|
+
with col1:
|
|
184
|
+
days = st.slider(
|
|
185
|
+
"Days",
|
|
186
|
+
min_value=0,
|
|
187
|
+
max_value=max_days,
|
|
188
|
+
value=min(1, max_days),
|
|
189
|
+
key="granularity_days",
|
|
190
|
+
on_change=save_widget_state,
|
|
191
|
+
)
|
|
192
|
+
with col2:
|
|
193
|
+
hours = st.slider(
|
|
194
|
+
"Hours",
|
|
195
|
+
min_value=0,
|
|
196
|
+
max_value=23,
|
|
197
|
+
value=0,
|
|
198
|
+
key="granularity_hours",
|
|
199
|
+
on_change=save_widget_state,
|
|
200
|
+
)
|
|
201
|
+
with col3:
|
|
202
|
+
minutes = st.slider(
|
|
203
|
+
"Minutes",
|
|
204
|
+
min_value=0,
|
|
205
|
+
max_value=59,
|
|
206
|
+
value=0,
|
|
207
|
+
key="granularity_minutes",
|
|
208
|
+
on_change=save_widget_state,
|
|
209
|
+
)
|
|
210
|
+
with col4:
|
|
211
|
+
seconds = st.slider(
|
|
212
|
+
"Seconds",
|
|
213
|
+
min_value=0,
|
|
214
|
+
max_value=59,
|
|
215
|
+
value=0,
|
|
216
|
+
key="granularity_seconds",
|
|
217
|
+
on_change=save_widget_state,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
selected_granularity = timedelta(
|
|
221
|
+
days=days, hours=hours, minutes=minutes, seconds=seconds
|
|
222
|
+
)
|
|
223
|
+
formatted_max = _format_timedelta(max_granularity)
|
|
224
|
+
|
|
225
|
+
st.info(
|
|
226
|
+
f"Granularity must be greater than zero and less than or equal to {formatted_max}.",
|
|
227
|
+
icon=INFO_ICON,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if (
|
|
231
|
+
selected_granularity.total_seconds() == 0
|
|
232
|
+
or selected_granularity > max_granularity
|
|
233
|
+
):
|
|
234
|
+
st.stop()
|
|
235
|
+
|
|
236
|
+
return pd.Timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _calculate_max_granularity(df: pd.DataFrame) -> timedelta:
|
|
240
|
+
"""Calculate the maximum allowed granularity based on the timestamp range."""
|
|
241
|
+
max_timestamp, min_timestamp = (
|
|
242
|
+
df[TIMESTAMP_COLUMN].max(),
|
|
243
|
+
df[TIMESTAMP_COLUMN].min(),
|
|
244
|
+
)
|
|
245
|
+
time_range = max_timestamp - min_timestamp
|
|
246
|
+
max_granularity = time_range / 2
|
|
247
|
+
return max_granularity
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _group_timestamps(timestamps: pd.DataFrame, granularity: timedelta):
|
|
251
|
+
"""Group timestamps based on a custom granularity."""
|
|
252
|
+
# Find the minimum timestamp
|
|
253
|
+
min_timestamp = timestamps.min()
|
|
254
|
+
|
|
255
|
+
# Calculate the difference from the minimum timestamp
|
|
256
|
+
diff = timestamps - min_timestamp
|
|
257
|
+
|
|
258
|
+
# Integer divide the difference by the granularity
|
|
259
|
+
groups = diff // granularity
|
|
260
|
+
|
|
261
|
+
# Calculate the new timestamps
|
|
262
|
+
return min_timestamp + groups * granularity
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def process_data_and_fit_temptopic(time_granularity: timedelta):
|
|
266
|
+
"""Process data and fit TempTopic with custom time granularity."""
|
|
267
|
+
df = st.session_state["time_filtered_df"].copy()
|
|
268
|
+
df[TIMESTAMP_COLUMN] = pd.to_datetime(df[TIMESTAMP_COLUMN])
|
|
269
|
+
|
|
270
|
+
# Convert time_granularity to timedelta if it's not already
|
|
271
|
+
if not isinstance(time_granularity, timedelta):
|
|
272
|
+
time_granularity = pd.Timedelta(time_granularity)
|
|
273
|
+
|
|
274
|
+
# Group timestamps using the custom function
|
|
275
|
+
df["grouped_timestamp"] = _group_timestamps(df[TIMESTAMP_COLUMN], time_granularity)
|
|
276
|
+
|
|
277
|
+
aggregated_df = (
|
|
278
|
+
df.groupby("grouped_timestamp")
|
|
279
|
+
.agg({TEXT_COLUMN: list, DOCUMENT_ID_COLUMN: list})
|
|
280
|
+
.reset_index()
|
|
281
|
+
)
|
|
282
|
+
indices = st.session_state["time_filtered_df"][DOCUMENT_ID_COLUMN]
|
|
283
|
+
docs = [st.session_state["split_df"][TEXT_COLUMN][i] for i in indices]
|
|
284
|
+
|
|
285
|
+
index_to_timestamp = {
|
|
286
|
+
idx: timestamp
|
|
287
|
+
for timestamp, idx_sublist in zip(
|
|
288
|
+
aggregated_df["grouped_timestamp"], aggregated_df[DOCUMENT_ID_COLUMN]
|
|
289
|
+
)
|
|
290
|
+
for idx in idx_sublist
|
|
291
|
+
}
|
|
292
|
+
timestamps_repeated = [
|
|
293
|
+
index_to_timestamp[idx].strftime("%Y-%m-%d %H:%M:%S") for idx in indices
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
# Initialize and fit TempTopic
|
|
297
|
+
with st.spinner("Fitting TempTopic..."):
|
|
298
|
+
temptopic = TempTopic(
|
|
299
|
+
st.session_state["topic_model"],
|
|
300
|
+
docs,
|
|
301
|
+
st.session_state["embeddings"],
|
|
302
|
+
st.session_state["token_embeddings"],
|
|
303
|
+
st.session_state["token_strings"],
|
|
304
|
+
timestamps_repeated,
|
|
305
|
+
evolution_tuning=st.session_state.evolution_tuning,
|
|
306
|
+
global_tuning=st.session_state.global_tuning,
|
|
307
|
+
)
|
|
308
|
+
temptopic.fit(
|
|
309
|
+
window_size=st.session_state.window_size,
|
|
310
|
+
k=st.session_state.k,
|
|
311
|
+
double_agg=st.session_state.double_agg,
|
|
312
|
+
doc_agg=st.session_state.doc_agg,
|
|
313
|
+
global_agg=st.session_state.global_agg,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Store the fitted TempTopic object and current parameter values
|
|
317
|
+
st.session_state.temptopic = temptopic
|
|
318
|
+
st.session_state.aggregated_df = aggregated_df
|
|
319
|
+
for param in [
|
|
320
|
+
"window_size",
|
|
321
|
+
"k",
|
|
322
|
+
"alpha",
|
|
323
|
+
"double_agg",
|
|
324
|
+
"doc_agg",
|
|
325
|
+
"global_agg",
|
|
326
|
+
"evolution_tuning",
|
|
327
|
+
"global_tuning",
|
|
328
|
+
"granularity",
|
|
329
|
+
]:
|
|
330
|
+
st.session_state[f"prev_{param}"] = st.session_state.get(param)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def display_topic_evolution_dataframe():
|
|
334
|
+
"""Display the Topic Evolution Dataframe."""
|
|
335
|
+
with st.expander("Topic Evolution Dataframe"):
|
|
336
|
+
columns_to_display = ["Topic", "Words", "Frequency", "Timestamp"]
|
|
337
|
+
columns_present = [
|
|
338
|
+
col
|
|
339
|
+
for col in columns_to_display
|
|
340
|
+
if col in st.session_state.temptopic.final_df.columns
|
|
341
|
+
]
|
|
342
|
+
st.dataframe(
|
|
343
|
+
st.session_state.temptopic.final_df[columns_present].sort_values(
|
|
344
|
+
by=["Topic", "Timestamp"], ascending=[True, True]
|
|
345
|
+
),
|
|
346
|
+
use_container_width=True,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def display_topic_info_dataframe():
|
|
351
|
+
"""Display the Topic Info Dataframe."""
|
|
352
|
+
with st.expander("Topic Info Dataframe"):
|
|
353
|
+
st.dataframe(
|
|
354
|
+
st.session_state.temptopic.topic_model.get_topic_info(),
|
|
355
|
+
use_container_width=True,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def display_documents_per_date_dataframe():
|
|
360
|
+
"""Display the Documents per Date Dataframe."""
|
|
361
|
+
with st.expander("Documents per Date Dataframe"):
|
|
362
|
+
st.dataframe(st.session_state.aggregated_df, use_container_width=True)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def display_temptopic_visualizations():
|
|
366
|
+
"""Display TempTopic Visualizations."""
|
|
367
|
+
with st.expander("TempTopic Visualizations"):
|
|
368
|
+
# Create a list of topics with their representations
|
|
369
|
+
topic_options = sorted(
|
|
370
|
+
[
|
|
371
|
+
(
|
|
372
|
+
topic,
|
|
373
|
+
", ".join(
|
|
374
|
+
st.session_state.temptopic.final_df[
|
|
375
|
+
st.session_state.temptopic.final_df["Topic"] == topic
|
|
376
|
+
]["Words"]
|
|
377
|
+
.iloc[0]
|
|
378
|
+
.split(", ")[:3]
|
|
379
|
+
),
|
|
380
|
+
)
|
|
381
|
+
for topic in st.session_state.temptopic.final_df["Topic"].unique()
|
|
382
|
+
]
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Create a dictionary for streamlit multiselect
|
|
386
|
+
topic_dict = {f"Topic {topic}: {repr}": topic for topic, repr in topic_options}
|
|
387
|
+
|
|
388
|
+
selected_topics = st.multiselect(
|
|
389
|
+
"Topics to Show",
|
|
390
|
+
options=list(topic_dict.keys()),
|
|
391
|
+
format_func=lambda x: x,
|
|
392
|
+
default=None,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# Convert selected topics back to topic numbers
|
|
396
|
+
topics_to_show = [topic_dict[topic] for topic in selected_topics]
|
|
397
|
+
|
|
398
|
+
display_topic_evolution(topics_to_show)
|
|
399
|
+
display_overall_topic_stability(topics_to_show)
|
|
400
|
+
display_temporal_stability_metrics(topics_to_show)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def display_topic_evolution(topics_to_show):
|
|
404
|
+
"""Display Topic Evolution in Time and Semantic Space."""
|
|
405
|
+
st.header("Topic Evolution in Time and Semantic Space")
|
|
406
|
+
n_neighbors = st.slider(
|
|
407
|
+
"UMAP n_neighbors", min_value=2, max_value=100, value=15, step=1
|
|
408
|
+
)
|
|
409
|
+
min_dist = st.slider(
|
|
410
|
+
"UMAP min_dist", min_value=0.0, max_value=0.99, value=0.1, step=0.01
|
|
411
|
+
)
|
|
412
|
+
metric = st.selectbox("UMAP Metric", ["cosine", "euclidean", "manhattan"])
|
|
413
|
+
color_palette = st.selectbox("Color Palette", ["Plotly", "D3", "Alphabet"])
|
|
414
|
+
|
|
415
|
+
fig_topic_evolution = plot_topic_evolution(
|
|
416
|
+
st.session_state.temptopic,
|
|
417
|
+
granularity=st.session_state.granularity,
|
|
418
|
+
topics_to_show=topics_to_show,
|
|
419
|
+
n_neighbors=n_neighbors,
|
|
420
|
+
min_dist=min_dist,
|
|
421
|
+
metric=metric,
|
|
422
|
+
color_palette=color_palette,
|
|
423
|
+
)
|
|
424
|
+
st.plotly_chart(
|
|
425
|
+
fig_topic_evolution, config=PLOTLY_BUTTON_SAVE_CONFIG, use_container_width=True
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
st.divider()
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def display_overall_topic_stability(topics_to_show):
|
|
432
|
+
"""Display Overall Topic Stability."""
|
|
433
|
+
st.header("Overall Topic Stability")
|
|
434
|
+
normalize_overall_stability = st.checkbox("Normalize", value=False)
|
|
435
|
+
overall_stability_df = st.session_state.temptopic.calculate_overall_topic_stability(
|
|
436
|
+
window_size=st.session_state.window_size,
|
|
437
|
+
k=st.session_state.k,
|
|
438
|
+
alpha=st.session_state.alpha,
|
|
439
|
+
)
|
|
440
|
+
fig_overall_stability = plot_overall_topic_stability(
|
|
441
|
+
st.session_state.temptopic,
|
|
442
|
+
topics_to_show=topics_to_show,
|
|
443
|
+
normalize=normalize_overall_stability,
|
|
444
|
+
darkmode=True,
|
|
445
|
+
)
|
|
446
|
+
st.plotly_chart(
|
|
447
|
+
fig_overall_stability,
|
|
448
|
+
config=PLOTLY_BUTTON_SAVE_CONFIG,
|
|
449
|
+
use_container_width=True,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
st.divider()
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def display_temporal_stability_metrics(topics_to_show):
|
|
456
|
+
"""Display Temporal Stability Metrics."""
|
|
457
|
+
st.header("Temporal Stability Metrics")
|
|
458
|
+
|
|
459
|
+
fig_topic_stability = plot_temporal_stability_metrics(
|
|
460
|
+
st.session_state.temptopic,
|
|
461
|
+
metric="topic_stability",
|
|
462
|
+
topics_to_show=topics_to_show,
|
|
463
|
+
)
|
|
464
|
+
st.plotly_chart(
|
|
465
|
+
fig_topic_stability, config=PLOTLY_BUTTON_SAVE_CONFIG, use_container_width=True
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
fig_representation_stability = plot_temporal_stability_metrics(
|
|
469
|
+
st.session_state.temptopic,
|
|
470
|
+
metric="representation_stability",
|
|
471
|
+
topics_to_show=topics_to_show,
|
|
472
|
+
)
|
|
473
|
+
st.plotly_chart(
|
|
474
|
+
fig_representation_stability,
|
|
475
|
+
config=PLOTLY_BUTTON_SAVE_CONFIG,
|
|
476
|
+
use_container_width=True,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def display_topics_popularity():
|
|
481
|
+
"""Display the popularity of topics over time."""
|
|
482
|
+
with st.spinner("Computing topics over time..."):
|
|
483
|
+
with st.expander("Popularity of topics over time"):
|
|
484
|
+
if TIMESTAMP_COLUMN in st.session_state["time_filtered_df"]:
|
|
485
|
+
st.write("## Popularity of topics over time")
|
|
486
|
+
|
|
487
|
+
# Parameters
|
|
488
|
+
st.text_input(
|
|
489
|
+
"Topics list (format 1,12,52 or 1:20)",
|
|
490
|
+
key="dynamic_topics_list",
|
|
491
|
+
value="0:10",
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
st.number_input("nr_bins", min_value=1, value=10, key="nr_bins")
|
|
495
|
+
|
|
496
|
+
# Compute topics over time
|
|
497
|
+
st.session_state["topics_over_time"] = compute_topics_over_time(
|
|
498
|
+
st.session_state["topic_model"],
|
|
499
|
+
st.session_state["time_filtered_df"],
|
|
500
|
+
nr_bins=st.session_state["nr_bins"],
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# Visualize
|
|
504
|
+
st.plotly_chart(
|
|
505
|
+
plot_topics_over_time(
|
|
506
|
+
st.session_state["topics_over_time"],
|
|
507
|
+
st.session_state["dynamic_topics_list"],
|
|
508
|
+
st.session_state["topic_model"],
|
|
509
|
+
),
|
|
510
|
+
config=PLOTLY_BUTTON_SAVE_CONFIG,
|
|
511
|
+
use_container_width=True,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def main():
|
|
516
|
+
"""Main function to run the Streamlit topic_analysis."""
|
|
517
|
+
# Check if model is trained
|
|
518
|
+
check_model_trained()
|
|
519
|
+
check_embedding_type()
|
|
520
|
+
|
|
521
|
+
st.title("Temporal visualizations of topics")
|
|
522
|
+
|
|
523
|
+
# Display sidebar
|
|
524
|
+
display_sidebar()
|
|
525
|
+
|
|
526
|
+
# Calculate max granularity
|
|
527
|
+
max_granularity = _calculate_max_granularity(st.session_state["time_filtered_df"])
|
|
528
|
+
|
|
529
|
+
# Select time granularity
|
|
530
|
+
time_granularity = select_time_granularity(max_granularity)
|
|
531
|
+
|
|
532
|
+
col1, col2 = st.columns(2)
|
|
533
|
+
with col1:
|
|
534
|
+
# Add Apply button
|
|
535
|
+
apply_button = st.button("Apply Granularity and Parameters", type="primary")
|
|
536
|
+
with col2:
|
|
537
|
+
register_widget("temptopic_visualizations")
|
|
538
|
+
st.segmented_control(
|
|
539
|
+
"Show table results",
|
|
540
|
+
selection_mode="multi",
|
|
541
|
+
key="temptopic_visualizations",
|
|
542
|
+
on_change=save_widget_state,
|
|
543
|
+
options=["Topic evolution", "Topic info", "Documents per date"],
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
if apply_button:
|
|
547
|
+
if time_granularity is not None:
|
|
548
|
+
st.session_state.granularity = time_granularity
|
|
549
|
+
process_data_and_fit_temptopic(time_granularity)
|
|
550
|
+
else:
|
|
551
|
+
st.error("Please select a valid granularity before applying.")
|
|
552
|
+
|
|
553
|
+
# Display visualizations only if TempTopic has been fitted
|
|
554
|
+
if "temptopic" in st.session_state:
|
|
555
|
+
if "Topic evolution" in st.session_state["temptopic_visualizations"]:
|
|
556
|
+
display_topic_evolution_dataframe()
|
|
557
|
+
if "Topic info" in st.session_state["temptopic_visualizations"]:
|
|
558
|
+
display_topic_info_dataframe()
|
|
559
|
+
if "Documents per date" in st.session_state["temptopic_visualizations"]:
|
|
560
|
+
display_documents_per_date_dataframe()
|
|
561
|
+
display_temptopic_visualizations()
|
|
562
|
+
else:
|
|
563
|
+
st.info(
|
|
564
|
+
"Please apply granularity and parameters to view the temporal visualizations.",
|
|
565
|
+
icon=INFO_ICON,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# Restore widget state
|
|
570
|
+
restore_widget_state()
|
|
571
|
+
main()
|
|
572
|
+
|
|
573
|
+
# FIXME: Popularity of topics over time visualization is based on the number of paragraphs instead of original articles, since it's the default BERTopic method
|