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,425 @@
|
|
|
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 numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import scipy
|
|
9
|
+
from bertopic import BERTopic
|
|
10
|
+
from loguru import logger
|
|
11
|
+
from pandas import Timestamp
|
|
12
|
+
|
|
13
|
+
from bertrend.llm_utils.openai_client import OpenAI_Client
|
|
14
|
+
from bertrend import LLM_CONFIG
|
|
15
|
+
from bertrend.trend_analysis.data_structure import TopicSummaryList, SignalAnalysis
|
|
16
|
+
from bertrend.trend_analysis.prompts import get_prompt, fill_html_template
|
|
17
|
+
|
|
18
|
+
MAXIMUM_ANALYZED_PERIODS = 3
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def detect_weak_signals_zeroshot(
|
|
22
|
+
topic_models: dict[Timestamp, BERTopic],
|
|
23
|
+
zeroshot_topic_list: list[str],
|
|
24
|
+
granularity: int,
|
|
25
|
+
decay_factor: float = 0.01,
|
|
26
|
+
decay_power: float = 2,
|
|
27
|
+
) -> dict[str, dict[Timestamp, dict[str, any]]]:
|
|
28
|
+
"""
|
|
29
|
+
Detect weak signals based on the zero-shot list of topics to monitor.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
topic_models (Dict[Timestamp, BERTopic]): Dictionary of BERTopic models for each timestamp.
|
|
33
|
+
zeroshot_topic_list (List[str]): List of topics to monitor for weak signals.
|
|
34
|
+
granularity (int): The granularity of the timestamps in days.
|
|
35
|
+
decay_factor (float): The decay factor for exponential decay.
|
|
36
|
+
decay_power (float): The decay power for exponential decay.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Dict[str, Dict[Timestamp, Dict[str, any]]]: Dictionary of weak signal trends for each monitored topic.
|
|
40
|
+
"""
|
|
41
|
+
weak_signal_trends = {}
|
|
42
|
+
|
|
43
|
+
min_timestamp = min(topic_models.keys())
|
|
44
|
+
max_timestamp = max(topic_models.keys())
|
|
45
|
+
timestamps = pd.date_range(
|
|
46
|
+
start=min_timestamp, end=max_timestamp, freq=pd.Timedelta(days=granularity)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
for topic in zeroshot_topic_list:
|
|
50
|
+
weak_signal_trends[topic] = {}
|
|
51
|
+
topic_last_popularity = {}
|
|
52
|
+
topic_last_update = {}
|
|
53
|
+
|
|
54
|
+
for timestamp in timestamps:
|
|
55
|
+
# Check if the current timestamp has a corresponding topic model that was trained
|
|
56
|
+
# This is useful in scenarios where we have time skips in the data we were dealing with
|
|
57
|
+
# For example : Our data spans from jan 2024 to dec 2024 with a big gap in summer, and we used
|
|
58
|
+
# a monthly granularity, this means that timestamps of june, july, aug... won't have a corresponding
|
|
59
|
+
# topic model because no model was trained on that period due to non-existant data
|
|
60
|
+
|
|
61
|
+
if timestamp in topic_models:
|
|
62
|
+
topic_info = topic_models[timestamp].topic_info_df
|
|
63
|
+
topic_data = topic_info[topic_info["Name"] == topic]
|
|
64
|
+
|
|
65
|
+
if not topic_data.empty:
|
|
66
|
+
# zeroshot topic found in the model's output corresponding to current timestamp
|
|
67
|
+
representation = topic_data["Representation"].values[0]
|
|
68
|
+
representative_docs = topic_data["Representative_Docs"].values[0]
|
|
69
|
+
count = topic_data["Count"].values[0]
|
|
70
|
+
document_count = topic_data["Document_Count"].values[0]
|
|
71
|
+
|
|
72
|
+
if topic not in topic_last_popularity:
|
|
73
|
+
# If the First occurrence of the topic
|
|
74
|
+
topic_last_popularity[topic] = document_count
|
|
75
|
+
topic_last_update[topic] = timestamp
|
|
76
|
+
else:
|
|
77
|
+
# if not first appearance but receives an update in current timestamp, increase popularity
|
|
78
|
+
document_count += topic_last_popularity[topic]
|
|
79
|
+
topic_last_popularity[topic] = document_count
|
|
80
|
+
topic_last_update[topic] = timestamp
|
|
81
|
+
|
|
82
|
+
weak_signal_trends[topic][timestamp] = {
|
|
83
|
+
"Representation": representation,
|
|
84
|
+
"Representative_Docs": representative_docs,
|
|
85
|
+
"Count": count,
|
|
86
|
+
"Document_Count": document_count,
|
|
87
|
+
}
|
|
88
|
+
else:
|
|
89
|
+
# Topic not found in the current timestamp, apply decay to previously seen topics
|
|
90
|
+
weak_signal_trends[topic][timestamp] = _apply_decay(
|
|
91
|
+
topic,
|
|
92
|
+
timestamp,
|
|
93
|
+
topic_last_popularity,
|
|
94
|
+
topic_last_update,
|
|
95
|
+
granularity,
|
|
96
|
+
decay_factor,
|
|
97
|
+
decay_power,
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
# Timestamp not in topic_models, apply decay to previously seen topics
|
|
101
|
+
weak_signal_trends[topic][timestamp] = _apply_decay(
|
|
102
|
+
topic,
|
|
103
|
+
timestamp,
|
|
104
|
+
topic_last_popularity,
|
|
105
|
+
topic_last_update,
|
|
106
|
+
granularity,
|
|
107
|
+
decay_factor,
|
|
108
|
+
decay_power,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return weak_signal_trends
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _apply_decay(
|
|
115
|
+
topic,
|
|
116
|
+
timestamp,
|
|
117
|
+
topic_last_popularity,
|
|
118
|
+
topic_last_update,
|
|
119
|
+
granularity,
|
|
120
|
+
decay_factor,
|
|
121
|
+
decay_power,
|
|
122
|
+
):
|
|
123
|
+
"""Helper function to apply decay to topic popularity."""
|
|
124
|
+
last_popularity = topic_last_popularity.get(topic, 0)
|
|
125
|
+
last_update = topic_last_update.get(topic, timestamp)
|
|
126
|
+
|
|
127
|
+
time_diff = timestamp - last_update
|
|
128
|
+
periods_since_last_update = time_diff // pd.Timedelta(days=granularity)
|
|
129
|
+
|
|
130
|
+
decayed_popularity = last_popularity * np.exp(
|
|
131
|
+
-decay_factor * (periods_since_last_update**decay_power)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
topic_last_popularity[topic] = decayed_popularity
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
"Representation": None,
|
|
138
|
+
"Representative_Docs": None,
|
|
139
|
+
"Count": 0,
|
|
140
|
+
"Document_Count": decayed_popularity,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _filter_data(data, window_end, keep_documents):
|
|
145
|
+
"""Helper function to filter data based on window_end."""
|
|
146
|
+
return {
|
|
147
|
+
"Timestamps": [ts for ts in data["Timestamps"] if ts <= window_end],
|
|
148
|
+
"Popularity": [
|
|
149
|
+
pop
|
|
150
|
+
for ts, pop in zip(data["Timestamps"], data["Popularity"])
|
|
151
|
+
if ts <= window_end
|
|
152
|
+
],
|
|
153
|
+
"Representation": [
|
|
154
|
+
rep
|
|
155
|
+
for ts, rep in zip(data["Timestamps"], data["Representations"])
|
|
156
|
+
if ts <= window_end
|
|
157
|
+
],
|
|
158
|
+
"Documents": (
|
|
159
|
+
[doc for ts, docs in data["Documents"] if ts <= window_end for doc in docs]
|
|
160
|
+
if keep_documents
|
|
161
|
+
else []
|
|
162
|
+
),
|
|
163
|
+
"Sources": [sources for ts, sources in data["Sources"] if ts <= window_end],
|
|
164
|
+
"Docs_Count": [
|
|
165
|
+
count
|
|
166
|
+
for ts, count in zip(data["Timestamps"], data["Docs_Count"])
|
|
167
|
+
if ts <= window_end
|
|
168
|
+
],
|
|
169
|
+
"Paragraphs_Count": [
|
|
170
|
+
count
|
|
171
|
+
for ts, count in zip(data["Timestamps"], data["Paragraphs_Count"])
|
|
172
|
+
if ts <= window_end
|
|
173
|
+
],
|
|
174
|
+
"Source_Diversity": [
|
|
175
|
+
div
|
|
176
|
+
for ts, div in zip(data["Timestamps"], data["Source_Diversity"])
|
|
177
|
+
if ts <= window_end
|
|
178
|
+
],
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _is_rising_popularity(filtered_data, latest_timestamp):
|
|
183
|
+
"""Helper function to check if popularity is rising."""
|
|
184
|
+
retrospective_start = latest_timestamp - pd.Timedelta(days=14)
|
|
185
|
+
retrospective_data = [
|
|
186
|
+
(timestamp, popularity)
|
|
187
|
+
for timestamp, popularity in zip(
|
|
188
|
+
filtered_data["Timestamps"], filtered_data["Popularity"]
|
|
189
|
+
)
|
|
190
|
+
if retrospective_start <= timestamp <= latest_timestamp
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
if len(retrospective_data) >= 2:
|
|
194
|
+
x = range(len(retrospective_data))
|
|
195
|
+
y = [popularity for _, popularity in retrospective_data]
|
|
196
|
+
slope, _, _, _, _ = scipy.stats.linregress(x, y)
|
|
197
|
+
return slope > 0
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _create_df(topics, keep_documents):
|
|
202
|
+
df = pd.DataFrame(
|
|
203
|
+
[
|
|
204
|
+
{
|
|
205
|
+
"Topic": topic,
|
|
206
|
+
"Representation": filtered_data["Representation"][-1],
|
|
207
|
+
"Latest_Popularity": latest_popularity,
|
|
208
|
+
"Docs_Count": docs_count,
|
|
209
|
+
"Paragraphs_Count": paragraphs_count,
|
|
210
|
+
"Latest_Timestamp": latest_timestamp,
|
|
211
|
+
"Documents": filtered_data["Documents"] if keep_documents else [],
|
|
212
|
+
"Sources": {
|
|
213
|
+
source for sources in filtered_data["Sources"] for source in sources
|
|
214
|
+
},
|
|
215
|
+
"Source_Diversity": source_diversity,
|
|
216
|
+
}
|
|
217
|
+
for topic, latest_popularity, latest_timestamp, docs_count, paragraphs_count, source_diversity, filtered_data in topics
|
|
218
|
+
]
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# if not df.empty: df = df[df['Latest_Popularity'] >= 0.01] # Remove signals that faded away by filtering on latest popularity
|
|
222
|
+
return df
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _create_dataframes(
|
|
226
|
+
noise_topics, weak_signal_topics, strong_signal_topics, keep_documents
|
|
227
|
+
):
|
|
228
|
+
"""Helper function to create DataFrames for each category."""
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
_create_df(noise_topics, keep_documents),
|
|
232
|
+
_create_df(weak_signal_topics, keep_documents),
|
|
233
|
+
_create_df(strong_signal_topics, keep_documents),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _initialize_new_topic(
|
|
238
|
+
topic_sizes, topic_last_popularity, topic_last_update, topic, timestamp, row
|
|
239
|
+
):
|
|
240
|
+
"""Initialize a new topic with its first data point."""
|
|
241
|
+
topic_sizes[topic]["Timestamps"] = [timestamp]
|
|
242
|
+
topic_sizes[topic]["Popularity"] = [row["Document_Count1"]]
|
|
243
|
+
topic_sizes[topic]["Representation"] = "_".join(row["Representation1"])
|
|
244
|
+
topic_sizes[topic]["Documents"] = [(timestamp, row["Documents1"])]
|
|
245
|
+
topic_sizes[topic]["Sources"] = [(timestamp, row["Source1"])]
|
|
246
|
+
topic_sizes[topic]["Docs_Count"] = [row["Document_Count1"]]
|
|
247
|
+
topic_sizes[topic]["Paragraphs_Count"] = [row["Count1"]]
|
|
248
|
+
topic_sizes[topic]["Source_Diversity"] = [len(set(row["Source1"]))]
|
|
249
|
+
topic_sizes[topic]["Representations"] = [topic_sizes[topic]["Representation"]]
|
|
250
|
+
|
|
251
|
+
topic_last_popularity[topic] = row["Document_Count1"]
|
|
252
|
+
topic_last_update[topic] = timestamp
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def update_existing_topic(
|
|
256
|
+
topic_sizes,
|
|
257
|
+
topic_last_popularity,
|
|
258
|
+
topic_last_update,
|
|
259
|
+
topic,
|
|
260
|
+
timestamp,
|
|
261
|
+
granularity,
|
|
262
|
+
row,
|
|
263
|
+
):
|
|
264
|
+
"""Update an existing topic with new data."""
|
|
265
|
+
next_timestamp = timestamp + granularity
|
|
266
|
+
|
|
267
|
+
topic_sizes[topic]["Timestamps"].append(next_timestamp)
|
|
268
|
+
topic_sizes[topic]["Popularity"].append(
|
|
269
|
+
topic_last_popularity[topic] + row["Document_Count2"]
|
|
270
|
+
)
|
|
271
|
+
topic_sizes[topic]["Representation"] = "_".join(row["Representation2"])
|
|
272
|
+
topic_sizes[topic]["Documents"].append((next_timestamp, row["Documents2"]))
|
|
273
|
+
topic_sizes[topic]["Sources"].append((next_timestamp, row["Source2"]))
|
|
274
|
+
topic_sizes[topic]["Docs_Count"].append(
|
|
275
|
+
topic_sizes[topic]["Docs_Count"][-1] + row["Document_Count2"]
|
|
276
|
+
)
|
|
277
|
+
topic_sizes[topic]["Paragraphs_Count"].append(
|
|
278
|
+
topic_sizes[topic]["Paragraphs_Count"][-1] + row["Count2"]
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
all_sources = [
|
|
282
|
+
source for _, sources in topic_sizes[topic]["Sources"] for source in sources
|
|
283
|
+
]
|
|
284
|
+
all_sources.extend(row["Source2"])
|
|
285
|
+
topic_sizes[topic]["Source_Diversity"].append(len(set(all_sources)))
|
|
286
|
+
topic_sizes[topic]["Representations"].append(topic_sizes[topic]["Representation"])
|
|
287
|
+
|
|
288
|
+
topic_last_popularity[topic] = topic_last_popularity[topic] + row["Document_Count2"]
|
|
289
|
+
topic_last_update[topic] = next_timestamp # Update to next_timestamp
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _apply_decay_to_inactive_topics(
|
|
293
|
+
topic_sizes,
|
|
294
|
+
topic_last_popularity,
|
|
295
|
+
topic_last_update,
|
|
296
|
+
updated_topics,
|
|
297
|
+
topics_updated_next,
|
|
298
|
+
current_timestamp,
|
|
299
|
+
granularity,
|
|
300
|
+
decay_factor,
|
|
301
|
+
decay_power,
|
|
302
|
+
):
|
|
303
|
+
"""Apply decay to topics that were not updated in the current timestamp or the next."""
|
|
304
|
+
all_topics = set(topic_last_update.keys())
|
|
305
|
+
inactive_topics = all_topics - updated_topics - topics_updated_next
|
|
306
|
+
|
|
307
|
+
for topic in inactive_topics:
|
|
308
|
+
last_popularity = topic_last_popularity[topic]
|
|
309
|
+
last_update = topic_last_update[topic]
|
|
310
|
+
|
|
311
|
+
time_diff = current_timestamp - last_update
|
|
312
|
+
periods_since_last_update = time_diff // granularity
|
|
313
|
+
|
|
314
|
+
if periods_since_last_update > 0:
|
|
315
|
+
decayed_popularity = last_popularity * np.exp(
|
|
316
|
+
-decay_factor * (periods_since_last_update**decay_power)
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
topic_sizes[topic]["Timestamps"].append(current_timestamp)
|
|
320
|
+
topic_sizes[topic]["Popularity"].append(decayed_popularity)
|
|
321
|
+
topic_sizes[topic]["Docs_Count"].append(
|
|
322
|
+
topic_sizes[topic]["Docs_Count"][-1]
|
|
323
|
+
)
|
|
324
|
+
topic_sizes[topic]["Paragraphs_Count"].append(
|
|
325
|
+
topic_sizes[topic]["Paragraphs_Count"][-1]
|
|
326
|
+
)
|
|
327
|
+
topic_sizes[topic]["Source_Diversity"].append(
|
|
328
|
+
topic_sizes[topic]["Source_Diversity"][-1]
|
|
329
|
+
)
|
|
330
|
+
topic_sizes[topic]["Representations"].append(
|
|
331
|
+
topic_sizes[topic]["Representation"]
|
|
332
|
+
)
|
|
333
|
+
topic_last_popularity[topic] = decayed_popularity
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def analyze_signal(
|
|
337
|
+
bertrend,
|
|
338
|
+
topic_number: int,
|
|
339
|
+
current_date: Timestamp,
|
|
340
|
+
maximum_analysed_periods: int = MAXIMUM_ANALYZED_PERIODS,
|
|
341
|
+
) -> tuple[TopicSummaryList, SignalAnalysis]:
|
|
342
|
+
topic_merge_rows = bertrend.all_merge_histories_df[
|
|
343
|
+
bertrend.all_merge_histories_df["Topic1"] == topic_number
|
|
344
|
+
].sort_values("Timestamp")
|
|
345
|
+
topic_merge_rows_filtered = topic_merge_rows[
|
|
346
|
+
topic_merge_rows["Timestamp"] <= current_date
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
# In order to avoid to have too big contexts, keep only data that correspond to the N last models built
|
|
350
|
+
# before the current date
|
|
351
|
+
considered_periods = sorted(
|
|
352
|
+
[ts for ts in bertrend.get_periods() if ts < current_date]
|
|
353
|
+
)[-maximum_analysed_periods:]
|
|
354
|
+
if considered_periods:
|
|
355
|
+
topic_merge_rows_filtered = topic_merge_rows_filtered[
|
|
356
|
+
topic_merge_rows["Timestamp"] >= considered_periods[0]
|
|
357
|
+
]
|
|
358
|
+
|
|
359
|
+
if not topic_merge_rows_filtered.empty:
|
|
360
|
+
content_summary = "\n".join(
|
|
361
|
+
[
|
|
362
|
+
f"Timestamp: {row.Timestamp.strftime('%Y-%m-%d')}\n"
|
|
363
|
+
f"Topic representation: {row.Representation1}\n"
|
|
364
|
+
f"{' '.join(f'- {doc}' for doc in row.Documents1 if isinstance(doc, str))}\n"
|
|
365
|
+
f"Timestamp: {(row.Timestamp + pd.Timedelta(days=bertrend.config['granularity'])).strftime('%Y-%m-%d')}\n"
|
|
366
|
+
f"Topic representation: {row.Representation2}\n"
|
|
367
|
+
f"{' '.join(f'- {doc}' for doc in row.Documents2 if isinstance(doc, str))}\n"
|
|
368
|
+
for row in topic_merge_rows_filtered.itertuples()
|
|
369
|
+
]
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
language = bertrend.topic_model.config["global"]["language"]
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
openai_client = OpenAI_Client(
|
|
376
|
+
api_key=LLM_CONFIG["api_key"],
|
|
377
|
+
endpoint=LLM_CONFIG["endpoint"],
|
|
378
|
+
model=LLM_CONFIG["model"],
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# First prompt: Generate summary
|
|
382
|
+
logger.debug("First prompt - generate summary")
|
|
383
|
+
summary_prompt = get_prompt(
|
|
384
|
+
language,
|
|
385
|
+
"topic_summary",
|
|
386
|
+
topic_number=topic_number,
|
|
387
|
+
content_summary=content_summary,
|
|
388
|
+
)
|
|
389
|
+
summaries = openai_client.parse(
|
|
390
|
+
system_prompt=LLM_CONFIG["system_prompt"],
|
|
391
|
+
user_prompt=summary_prompt,
|
|
392
|
+
temperature=LLM_CONFIG["temperature"],
|
|
393
|
+
response_format=TopicSummaryList,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
if not summaries:
|
|
397
|
+
raise ValueError(
|
|
398
|
+
"An anomaly occured during topic summary generation. Context probably too long. Check other logs for details"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# Second prompt: Analyze weak signal
|
|
402
|
+
logger.debug("Second prompt - analyze weak signal")
|
|
403
|
+
weak_signal_prompt = get_prompt(
|
|
404
|
+
language,
|
|
405
|
+
prompt_type="weak_signal",
|
|
406
|
+
summary_from_first_prompt=summaries.model_dump_json(),
|
|
407
|
+
)
|
|
408
|
+
weak_signal_analysis = openai_client.parse(
|
|
409
|
+
system_prompt=LLM_CONFIG["system_prompt"],
|
|
410
|
+
user_prompt=weak_signal_prompt,
|
|
411
|
+
temperature=LLM_CONFIG["temperature"],
|
|
412
|
+
response_format=SignalAnalysis,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
return summaries, weak_signal_analysis
|
|
416
|
+
|
|
417
|
+
except Exception as e:
|
|
418
|
+
error_msg = f"An error occurred while generating the analysis: {str(e)}"
|
|
419
|
+
logger.error(error_msg)
|
|
420
|
+
return None, None
|
|
421
|
+
|
|
422
|
+
else:
|
|
423
|
+
error_msg = f"No data available for topic {topic_number} within the specified date range. Please enter a valid topic number."
|
|
424
|
+
logger.error(error_msg)
|
|
425
|
+
return None, None
|
|
@@ -0,0 +1,31 @@
|
|
|
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 os
|
|
7
|
+
import ssl
|
|
8
|
+
import nltk
|
|
9
|
+
from nltk.corpus import stopwords
|
|
10
|
+
|
|
11
|
+
# Ensures files are written with +rw permissions for both user and groups
|
|
12
|
+
os.umask(0o002)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ensure_stopwords():
|
|
16
|
+
"""Check if NLTK stopwords are available locally before downloading."""
|
|
17
|
+
try:
|
|
18
|
+
# Try to access stopwords to check if they're already downloaded
|
|
19
|
+
stopwords.words("english")
|
|
20
|
+
except LookupError:
|
|
21
|
+
nltk.download("stopwords")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Workaround for downloading nltk data in some environments
|
|
25
|
+
try:
|
|
26
|
+
_create_unverified_https_context = ssl._create_unverified_context
|
|
27
|
+
except AttributeError:
|
|
28
|
+
pass
|
|
29
|
+
else:
|
|
30
|
+
ssl._create_default_https_context = _create_unverified_https_context
|
|
31
|
+
ensure_stopwords()
|
|
@@ -0,0 +1,31 @@
|
|
|
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 hashlib
|
|
7
|
+
import pickle
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
# Ensures to write with +rw for both user and groups
|
|
14
|
+
os.umask(0o002)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_embeddings(cache_path: Path):
|
|
18
|
+
"""Loads embeddings as pickle"""
|
|
19
|
+
with open(cache_path, "rb") as f_in:
|
|
20
|
+
return pickle.load(f_in)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def save_embeddings(embeddings: list, cache_path: Path):
|
|
24
|
+
"""Save embeddings as pickle"""
|
|
25
|
+
with open(cache_path, "wb") as f_out:
|
|
26
|
+
pickle.dump(embeddings, f_out)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_hash(data: Any) -> str:
|
|
30
|
+
"""Returns a *stable* hash(persistent between different Python session) for any object. NB. The default hash() function does not guarantee this."""
|
|
31
|
+
return hashlib.md5(repr(data).encode("utf-8")).hexdigest()
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
import os
|
|
6
|
+
import tomllib
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _resolve_env_variables(config_dict: dict) -> dict:
|
|
11
|
+
"""
|
|
12
|
+
Recursively resolve environment variables in a dictionary.
|
|
13
|
+
Environment variables must be defined as a string like: $VAR_NAME.
|
|
14
|
+
"""
|
|
15
|
+
for key, value in config_dict.items():
|
|
16
|
+
# If the value is a string, check for environment variable
|
|
17
|
+
if isinstance(value, str):
|
|
18
|
+
config_dict[key] = os.path.expandvars(value)
|
|
19
|
+
# If it's a nested dictionary, recurse
|
|
20
|
+
elif isinstance(value, dict):
|
|
21
|
+
config_dict[key] = _resolve_env_variables(value)
|
|
22
|
+
return config_dict
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_toml_config(config_file: str | Path) -> dict:
|
|
26
|
+
"""
|
|
27
|
+
Load a TOML configuration file and resolve environment variables.
|
|
28
|
+
TOML `config_file` can be:
|
|
29
|
+
- a `str` representing the TOML file
|
|
30
|
+
- a `Path` to a TOML file
|
|
31
|
+
"""
|
|
32
|
+
if isinstance(config_file, str):
|
|
33
|
+
config_dict = tomllib.loads(config_file)
|
|
34
|
+
elif isinstance(config_file, Path):
|
|
35
|
+
with open(config_file, "rb") as f:
|
|
36
|
+
config_dict = tomllib.load(f)
|
|
37
|
+
else:
|
|
38
|
+
raise TypeError(
|
|
39
|
+
f"`config_file` must be `str` or `Path`, found {type(config_file)}"
|
|
40
|
+
)
|
|
41
|
+
return _resolve_env_variables(config_dict)
|