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,280 @@
|
|
|
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
|
+
import datamapplot
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import plotly.graph_objects as go
|
|
10
|
+
import seaborn as sns
|
|
11
|
+
import streamlit as st
|
|
12
|
+
import streamlit.components.v1 as components
|
|
13
|
+
from loguru import logger
|
|
14
|
+
from umap import UMAP
|
|
15
|
+
|
|
16
|
+
from bertrend import OUTPUT_PATH
|
|
17
|
+
from bertrend.demos.demos_utils.icons import ERROR_ICON, WARNING_ICON
|
|
18
|
+
from bertrend.demos.demos_utils.state_utils import restore_widget_state
|
|
19
|
+
from bertrend.demos.topic_analysis.messages import TRAIN_MODEL_FIRST_ERROR
|
|
20
|
+
from bertrend.demos.weak_signals.visualizations_utils import PLOTLY_BUTTON_SAVE_CONFIG
|
|
21
|
+
from bertrend.utils.data_loading import TEXT_COLUMN
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@st.cache_data
|
|
25
|
+
def plot_topics_hierarchy(_topic_model, width=700):
|
|
26
|
+
return _topic_model.visualize_hierarchy(width=width)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@st.cache_data
|
|
30
|
+
def plot_2d_topics(_topic_model, width=700):
|
|
31
|
+
return _topic_model.visualize_topics(width=width)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@st.cache_data
|
|
35
|
+
def overall_results():
|
|
36
|
+
"""Display overall results visualization."""
|
|
37
|
+
try:
|
|
38
|
+
return plot_2d_topics(st.session_state["topic_model"])
|
|
39
|
+
except TypeError as te:
|
|
40
|
+
logger.error(f"Error occurred: {te}")
|
|
41
|
+
return None
|
|
42
|
+
except ValueError as ve:
|
|
43
|
+
logger.error(f"Error occurred: {ve}")
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@st.cache_data
|
|
48
|
+
def create_topic_info_dataframe():
|
|
49
|
+
"""Create a DataFrame containing topics, number of documents per topic, and list of documents for each topic."""
|
|
50
|
+
docs = st.session_state["time_filtered_df"][TEXT_COLUMN].tolist()
|
|
51
|
+
topic_assignments = st.session_state["topics"]
|
|
52
|
+
|
|
53
|
+
topic_info = pd.DataFrame({"Document": docs, "Topic": topic_assignments})
|
|
54
|
+
topic_info_agg = (
|
|
55
|
+
topic_info.groupby("Topic").agg({"Document": ["count", list]}).reset_index()
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
topic_info_agg.columns = ["topic", "number_of_documents", "list_of_documents"]
|
|
59
|
+
topic_info_agg["topic"] = topic_info_agg["topic"].apply(
|
|
60
|
+
lambda x: (
|
|
61
|
+
", ".join(
|
|
62
|
+
[word for word, _ in st.session_state["topic_model"].get_topic(x)]
|
|
63
|
+
)
|
|
64
|
+
if x != -1
|
|
65
|
+
else "Outlier"
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return topic_info_agg[topic_info_agg["topic"] != "Outlier"]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@st.cache_data
|
|
73
|
+
def create_treemap():
|
|
74
|
+
"""Create a treemap visualization of topics and their corresponding documents."""
|
|
75
|
+
topic_info_df = create_topic_info_dataframe()
|
|
76
|
+
|
|
77
|
+
labels, parents, values = [], [], []
|
|
78
|
+
for _, row in topic_info_df.iterrows():
|
|
79
|
+
topic_label = f"{row['topic']} ({row['number_of_documents']})"
|
|
80
|
+
labels.append(topic_label)
|
|
81
|
+
parents.append("")
|
|
82
|
+
values.append(row["number_of_documents"])
|
|
83
|
+
|
|
84
|
+
for doc in row["list_of_documents"]:
|
|
85
|
+
labels.append(doc[:50] + "...")
|
|
86
|
+
parents.append(topic_label)
|
|
87
|
+
values.append(1)
|
|
88
|
+
|
|
89
|
+
fig = go.Figure(
|
|
90
|
+
go.Treemap(
|
|
91
|
+
labels=labels,
|
|
92
|
+
parents=parents,
|
|
93
|
+
values=values,
|
|
94
|
+
textinfo="label+value",
|
|
95
|
+
marker=dict(colors=[], line=dict(width=0), pad=dict(t=0)),
|
|
96
|
+
textfont=dict(size=34),
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
fig.update_layout(margin=dict(t=50, l=25, r=25, b=25))
|
|
100
|
+
fig.update_traces(marker=dict(cornerradius=10))
|
|
101
|
+
return fig
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_binary_file_downloader_html(bin_file, file_label="File"):
|
|
105
|
+
with open(bin_file, "rb") as f:
|
|
106
|
+
data = f.read()
|
|
107
|
+
return data
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def calculate_document_lengths(documents):
|
|
111
|
+
"""Calculate the length of each document in terms of word count."""
|
|
112
|
+
return np.array([len(doc.split()) for doc in documents])
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@st.cache_data
|
|
116
|
+
def create_datamap(include_outliers):
|
|
117
|
+
"""Create an interactive data map visualization."""
|
|
118
|
+
reduced_embeddings = UMAP(
|
|
119
|
+
n_neighbors=5, n_components=2, min_dist=0.2, metric="cosine"
|
|
120
|
+
).fit_transform(st.session_state["embeddings"])
|
|
121
|
+
|
|
122
|
+
topic_nums = list(set(st.session_state["topics"]))
|
|
123
|
+
topic_info = st.session_state["topic_model"].get_topic_info()
|
|
124
|
+
topic_representations = {
|
|
125
|
+
row["Topic"]: row["Name"]
|
|
126
|
+
for _, row in topic_info.iterrows()
|
|
127
|
+
if row["Topic"] in topic_nums
|
|
128
|
+
}
|
|
129
|
+
docs = st.session_state["time_filtered_df"][TEXT_COLUMN].tolist()
|
|
130
|
+
|
|
131
|
+
df = pd.DataFrame(
|
|
132
|
+
{
|
|
133
|
+
"document": docs,
|
|
134
|
+
"embedding": list(reduced_embeddings),
|
|
135
|
+
"topic_num": st.session_state["topics"],
|
|
136
|
+
"topic_representation": [
|
|
137
|
+
topic_representations.get(topic, f"-1_{topic}")
|
|
138
|
+
for topic in st.session_state["topics"]
|
|
139
|
+
],
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
df["is_noise"] = df["topic_representation"].str.contains("-1")
|
|
144
|
+
df["topic_representation"] = df.apply(
|
|
145
|
+
lambda row: (
|
|
146
|
+
"" if row["is_noise"] else topic_representations.get(row["topic_num"], "")
|
|
147
|
+
),
|
|
148
|
+
axis=1,
|
|
149
|
+
)
|
|
150
|
+
df["topic_color"] = df.apply(
|
|
151
|
+
lambda row: "#999999" if row["is_noise"] else None, axis=1
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if not include_outliers:
|
|
155
|
+
df = df[~df["is_noise"]]
|
|
156
|
+
|
|
157
|
+
if df.empty:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
embeddings_array = np.array(df["embedding"].tolist())
|
|
161
|
+
topic_representations_array = df["topic_representation"].values
|
|
162
|
+
|
|
163
|
+
unique_topics = df["topic_representation"].unique()
|
|
164
|
+
unique_topics = unique_topics[unique_topics != ""]
|
|
165
|
+
color_palette = sns.color_palette("tab20", len(unique_topics)).as_hex()
|
|
166
|
+
color_mapping = dict(zip(unique_topics, color_palette))
|
|
167
|
+
df["topic_color"] = df.apply(
|
|
168
|
+
lambda row: color_mapping.get(row["topic_representation"], row["topic_color"]),
|
|
169
|
+
axis=1,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
hover_data = df["document"].tolist()
|
|
174
|
+
document_lengths = calculate_document_lengths(hover_data)
|
|
175
|
+
normalized_sizes = 5 + 45 * (document_lengths - document_lengths.min()) / (
|
|
176
|
+
document_lengths.max() - document_lengths.min()
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
plot = datamapplot.create_interactive_plot(
|
|
180
|
+
embeddings_array,
|
|
181
|
+
topic_representations_array,
|
|
182
|
+
hover_text=hover_data,
|
|
183
|
+
marker_size_array=normalized_sizes,
|
|
184
|
+
inline_data=True,
|
|
185
|
+
noise_label="",
|
|
186
|
+
noise_color="#999999",
|
|
187
|
+
color_label_text=True,
|
|
188
|
+
label_wrap_width=16,
|
|
189
|
+
label_color_map=color_mapping,
|
|
190
|
+
width="100%",
|
|
191
|
+
height="100%",
|
|
192
|
+
darkmode=False,
|
|
193
|
+
marker_color_array=df["topic_color"].values,
|
|
194
|
+
use_medoids=True,
|
|
195
|
+
cluster_boundary_polygons=False,
|
|
196
|
+
enable_search=True,
|
|
197
|
+
search_field="hover_text",
|
|
198
|
+
point_line_width=0,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
save_path = OUTPUT_PATH / "datamapplot.html"
|
|
202
|
+
with open(save_path, "wb") as f:
|
|
203
|
+
f.write(plot._html_str.encode(encoding="UTF-8", errors="replace"))
|
|
204
|
+
|
|
205
|
+
with open(save_path, "r", encoding="utf-8") as HtmlFile:
|
|
206
|
+
source_code = HtmlFile.read()
|
|
207
|
+
return source_code
|
|
208
|
+
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.error(f"Error in creating datamap: {str(e)}")
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def main():
|
|
215
|
+
# Check if a model is trained
|
|
216
|
+
if "topic_model" not in st.session_state:
|
|
217
|
+
st.error(TRAIN_MODEL_FIRST_ERROR, icon=ERROR_ICON)
|
|
218
|
+
st.stop()
|
|
219
|
+
|
|
220
|
+
# Main execution
|
|
221
|
+
st.title("Visualizations")
|
|
222
|
+
|
|
223
|
+
# Sidebar
|
|
224
|
+
with st.sidebar:
|
|
225
|
+
include_outliers = st.checkbox("Include outliers (Topic = -1)", value=True)
|
|
226
|
+
|
|
227
|
+
# Overall Results
|
|
228
|
+
with st.expander("Overall Results", expanded=False):
|
|
229
|
+
overall_results_plot = overall_results()
|
|
230
|
+
if overall_results_plot is not None:
|
|
231
|
+
st.plotly_chart(
|
|
232
|
+
overall_results_plot,
|
|
233
|
+
config=PLOTLY_BUTTON_SAVE_CONFIG,
|
|
234
|
+
use_container_width=True,
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
st.error("Cannot display overall results", icon=ERROR_ICON)
|
|
238
|
+
st.warning("Try to change the UMAP parameters", icon=WARNING_ICON)
|
|
239
|
+
|
|
240
|
+
# Topics Treemap
|
|
241
|
+
with st.expander("Topics Treemap", expanded=False):
|
|
242
|
+
with st.spinner("Computing topics treemap..."):
|
|
243
|
+
treemap_plot = create_treemap()
|
|
244
|
+
st.plotly_chart(
|
|
245
|
+
treemap_plot, config=PLOTLY_BUTTON_SAVE_CONFIG, use_container_width=True
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Data Map
|
|
249
|
+
with st.expander("Data Map", expanded=True):
|
|
250
|
+
with st.spinner("Loading Data-map plot..."):
|
|
251
|
+
datamap_html = create_datamap(include_outliers)
|
|
252
|
+
if datamap_html is not None:
|
|
253
|
+
# Using st.html does fix the width and height issue by making it adaptive, but the html never loads (it loops indefinitely)
|
|
254
|
+
# This is the best solution so far, with a button to save the html and view it in fullscreen later.
|
|
255
|
+
components.html(datamap_html, width=1200, height=1000, scrolling=True)
|
|
256
|
+
|
|
257
|
+
# Add the fullscreen button
|
|
258
|
+
save_path = OUTPUT_PATH / "datamapplot.html"
|
|
259
|
+
|
|
260
|
+
# Create a download button
|
|
261
|
+
st.download_button(
|
|
262
|
+
label="View in fullscreen",
|
|
263
|
+
data=get_binary_file_downloader_html(save_path),
|
|
264
|
+
file_name="datamapplot.html",
|
|
265
|
+
mime="text/html",
|
|
266
|
+
use_container_width=True,
|
|
267
|
+
type="secondary",
|
|
268
|
+
)
|
|
269
|
+
else:
|
|
270
|
+
st.warning(
|
|
271
|
+
"No valid topics to visualize. All documents might be classified as outliers.",
|
|
272
|
+
icon=WARNING_ICON,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# Restore widget state
|
|
277
|
+
restore_widget_state()
|
|
278
|
+
main()
|
|
279
|
+
# FIXME: cluster_boundary_polygons=True causes a "pop from an empty set" error in the data map plot's generation process.
|
|
280
|
+
# It's not urgent, but should be looked into to see what's causing the problem and potentially get a better visualization where clusters are delimitted with contours.
|
|
@@ -0,0 +1,219 @@
|
|
|
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
|
+
import datetime
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import streamlit as st
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
from bertrend import OUTPUT_PATH
|
|
13
|
+
from bertrend.demos.demos_utils.data_loading_component import (
|
|
14
|
+
display_data_loading_component,
|
|
15
|
+
)
|
|
16
|
+
from bertrend.demos.demos_utils.embed_documents_component import (
|
|
17
|
+
display_embed_documents_component,
|
|
18
|
+
)
|
|
19
|
+
from bertrend.demos.demos_utils.icons import (
|
|
20
|
+
WARNING_ICON,
|
|
21
|
+
SUCCESS_ICON,
|
|
22
|
+
ERROR_ICON,
|
|
23
|
+
SETTINGS_ICON,
|
|
24
|
+
INFO_ICON,
|
|
25
|
+
EMBEDDING_ICON,
|
|
26
|
+
)
|
|
27
|
+
from bertrend.demos.demos_utils.messages import (
|
|
28
|
+
NO_EMBEDDINGS_WARNING_MESSAGE,
|
|
29
|
+
MODEL_TRAINING_COMPLETE_MESSAGE,
|
|
30
|
+
)
|
|
31
|
+
from bertrend.demos.demos_utils.state_utils import (
|
|
32
|
+
restore_widget_state,
|
|
33
|
+
SessionStateManager,
|
|
34
|
+
)
|
|
35
|
+
from bertrend.demos.topic_analysis.messages import (
|
|
36
|
+
EMBEDDINGS_CACHE_INFO,
|
|
37
|
+
SAVE_MODEL_REMINDER,
|
|
38
|
+
NO_MODEL_AVAILABLE_ERROR,
|
|
39
|
+
)
|
|
40
|
+
from bertrend.demos.demos_utils.parameters_component import (
|
|
41
|
+
display_bertopic_hyperparameters,
|
|
42
|
+
display_embedding_hyperparameters,
|
|
43
|
+
)
|
|
44
|
+
from bertrend.demos.weak_signals.visualizations_utils import PLOTLY_BUTTON_SAVE_CONFIG
|
|
45
|
+
from bertrend.metrics.topic_metrics import compute_cluster_metrics
|
|
46
|
+
from bertrend.config.parameters import BERTOPIC_SERIALIZATION
|
|
47
|
+
from bertrend.topic_analysis.visualizations import plot_docs_repartition_over_time
|
|
48
|
+
from bertrend.BERTopicModel import BERTopicModel
|
|
49
|
+
from bertrend.utils.data_loading import (
|
|
50
|
+
TEXT_COLUMN,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def generate_model_name(base_name="topic_model"):
|
|
55
|
+
"""
|
|
56
|
+
Generates a dynamic model name with the current date and time.
|
|
57
|
+
If a base name is provided, it uses that instead of the default.
|
|
58
|
+
"""
|
|
59
|
+
current_datetime = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
60
|
+
model_name = f"{base_name}_{current_datetime}"
|
|
61
|
+
return model_name
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def data_distribution(df: pd.DataFrame):
|
|
65
|
+
"""Display the distribution of data over time."""
|
|
66
|
+
with st.expander(
|
|
67
|
+
label="Data distribution",
|
|
68
|
+
expanded=False,
|
|
69
|
+
):
|
|
70
|
+
freq = st.select_slider(
|
|
71
|
+
"Time aggregation",
|
|
72
|
+
options=(
|
|
73
|
+
"1D",
|
|
74
|
+
"2D",
|
|
75
|
+
"1W",
|
|
76
|
+
"2W",
|
|
77
|
+
"1M",
|
|
78
|
+
"2M",
|
|
79
|
+
"1Y",
|
|
80
|
+
"2Y",
|
|
81
|
+
),
|
|
82
|
+
value="1M",
|
|
83
|
+
)
|
|
84
|
+
fig = plot_docs_repartition_over_time(df, freq)
|
|
85
|
+
st.plotly_chart(fig, config=PLOTLY_BUTTON_SAVE_CONFIG, use_container_width=True)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def save_model_interface():
|
|
89
|
+
"""Save the generated topic model to disk."""
|
|
90
|
+
st.write("## Save Model")
|
|
91
|
+
|
|
92
|
+
# Optional text box for custom model name
|
|
93
|
+
base_model_name = st.text_input(
|
|
94
|
+
"Enter a name for the model (optional):", key="base_model_name_input"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Button to save the model
|
|
98
|
+
if st.button("Save Model", key="save_model_button"):
|
|
99
|
+
if "topic_model" in st.session_state:
|
|
100
|
+
dynamic_model_name = generate_model_name(base_model_name or "topic_model")
|
|
101
|
+
model_save_path = OUTPUT_PATH / "saved_models" / dynamic_model_name
|
|
102
|
+
logger.debug(
|
|
103
|
+
f"Saving the model in the following directory: {model_save_path}"
|
|
104
|
+
)
|
|
105
|
+
try:
|
|
106
|
+
st.session_state["topic_model"].save(
|
|
107
|
+
model_save_path,
|
|
108
|
+
serialization=BERTOPIC_SERIALIZATION,
|
|
109
|
+
save_ctfidf=True,
|
|
110
|
+
save_embedding_model=True,
|
|
111
|
+
)
|
|
112
|
+
st.success(
|
|
113
|
+
f"Model saved successfully as {model_save_path}", icon=SUCCESS_ICON
|
|
114
|
+
)
|
|
115
|
+
st.session_state["model_saved"] = True
|
|
116
|
+
logger.success(f"Model saved successfully!", icon=SUCCESS_ICON)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
st.error(f"Failed to save the model: {e}", icon=ERROR_ICON)
|
|
119
|
+
logger.error(f"Failed to save the model: {e}")
|
|
120
|
+
else:
|
|
121
|
+
st.error(
|
|
122
|
+
NO_MODEL_AVAILABLE_ERROR,
|
|
123
|
+
icon=ERROR_ICON,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def train_model():
|
|
128
|
+
"""Train a BERTopic model based on provided data."""
|
|
129
|
+
with st.spinner("Training model..."):
|
|
130
|
+
dataset = st.session_state["time_filtered_df"][TEXT_COLUMN]
|
|
131
|
+
# indices = full_dataset.index.tolist()
|
|
132
|
+
|
|
133
|
+
# Initialize topic model
|
|
134
|
+
topic_model = BERTopicModel(st.session_state["bertopic_config"])
|
|
135
|
+
embeddings = st.session_state["embeddings"]
|
|
136
|
+
topic_model_output = topic_model.fit(
|
|
137
|
+
docs=dataset,
|
|
138
|
+
embeddings=embeddings,
|
|
139
|
+
)
|
|
140
|
+
bertopic = topic_model_output.topic_model
|
|
141
|
+
|
|
142
|
+
# Set session_state
|
|
143
|
+
st.session_state["topic_model"] = bertopic
|
|
144
|
+
st.session_state["topics"] = topic_model_output.topics
|
|
145
|
+
|
|
146
|
+
st.success(MODEL_TRAINING_COMPLETE_MESSAGE, icon=SUCCESS_ICON)
|
|
147
|
+
st.info(
|
|
148
|
+
EMBEDDINGS_CACHE_INFO,
|
|
149
|
+
icon=INFO_ICON,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
topic_info = bertopic.get_topic_info()
|
|
153
|
+
st.session_state["topics_info"] = topic_info[
|
|
154
|
+
topic_info["Topic"] != -1
|
|
155
|
+
] # exclude -1 topic from topic list
|
|
156
|
+
|
|
157
|
+
# compute cluster metrics (optional)
|
|
158
|
+
compute_cluster_metrics(bertopic, st.session_state["topics"], dataset)
|
|
159
|
+
|
|
160
|
+
# update state
|
|
161
|
+
st.session_state["model_trained"] = True
|
|
162
|
+
if not st.session_state["model_saved"]:
|
|
163
|
+
st.warning(SAVE_MODEL_REMINDER, icon=WARNING_ICON)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def main():
|
|
167
|
+
st.title(":part_alternation_mark: Topic analysis demo")
|
|
168
|
+
|
|
169
|
+
if "model_trained" not in st.session_state:
|
|
170
|
+
st.session_state["model_trained"] = False
|
|
171
|
+
if "model_saved" not in st.session_state:
|
|
172
|
+
st.session_state["model_saved"] = False
|
|
173
|
+
|
|
174
|
+
# In the sidebar form
|
|
175
|
+
with st.sidebar:
|
|
176
|
+
st.header(SETTINGS_ICON + " Settings")
|
|
177
|
+
st.subheader(EMBEDDING_ICON + " Embedding Hyperparameters")
|
|
178
|
+
display_embedding_hyperparameters()
|
|
179
|
+
display_bertopic_hyperparameters()
|
|
180
|
+
|
|
181
|
+
# Load data
|
|
182
|
+
display_data_loading_component()
|
|
183
|
+
|
|
184
|
+
# Data overview
|
|
185
|
+
if "time_filtered_df" not in st.session_state:
|
|
186
|
+
st.stop()
|
|
187
|
+
data_distribution(st.session_state["time_filtered_df"])
|
|
188
|
+
SessionStateManager.set("split_type", st.session_state["split_by_paragraph"])
|
|
189
|
+
|
|
190
|
+
# Embed documents
|
|
191
|
+
try:
|
|
192
|
+
display_embed_documents_component()
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error(f"An error occurred while embedding documents: {e}")
|
|
195
|
+
st.error(f"An error occurred while embedding documents: {e}", icon=ERROR_ICON)
|
|
196
|
+
|
|
197
|
+
if not SessionStateManager.get("data_embedded", False):
|
|
198
|
+
st.warning(NO_EMBEDDINGS_WARNING_MESSAGE, icon=WARNING_ICON)
|
|
199
|
+
st.stop()
|
|
200
|
+
|
|
201
|
+
# Train the model
|
|
202
|
+
if st.button(
|
|
203
|
+
"Train Model",
|
|
204
|
+
type="primary",
|
|
205
|
+
key="train_model_button",
|
|
206
|
+
help="Make sure to review the settings before clicking on this button.",
|
|
207
|
+
):
|
|
208
|
+
train_model()
|
|
209
|
+
|
|
210
|
+
# Save the model
|
|
211
|
+
save_model_interface()
|
|
212
|
+
|
|
213
|
+
# TODO: Investigate the potentially deprecated save_model_interface() I implemented a while ago
|
|
214
|
+
# to save a BERTopic model to either load it up later or load it up somewhere else
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# Restore widget state
|
|
218
|
+
restore_widget_state()
|
|
219
|
+
main()
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
EMBEDDINGS_CACHE_INFO = "Embeddings aren't saved in cache and thus aren't loaded. Please make sure to train the model without using cached embeddings if you want correct and functional temporal visualizations."
|
|
7
|
+
SAVE_MODEL_REMINDER = "Don't forget to save your model!"
|
|
8
|
+
NO_MODEL_AVAILABLE_ERROR = "No model available to save. Please train a model first."
|
|
9
|
+
NO_DOCUMENT_FOR_TOPIC = "No documents found for the selected topic."
|
|
10
|
+
TRAIN_MODEL_FIRST_ERROR = "Please train a model first."
|
|
11
|
+
REMOTE_EMBEDDING_SEVICE_TYPE_NOT_SUPPORTED_ERROR = (
|
|
12
|
+
"These visualizations are only available if a local embedding service is used."
|
|
13
|
+
)
|