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.
Files changed (181) hide show
  1. bertrend/BERTopicModel.py +320 -0
  2. bertrend/BERTrend.py +1211 -0
  3. bertrend/__init__.py +59 -0
  4. bertrend/config/bertopic_default_config.toml +48 -0
  5. bertrend/config/bertrend_default_config.toml +16 -0
  6. bertrend/config/parameters.py +92 -0
  7. bertrend/config/services_default_config.toml +32 -0
  8. bertrend/demos/__init__.py +4 -0
  9. bertrend/demos/demos_utils/__init__.py +13 -0
  10. bertrend/demos/demos_utils/data_loading_component.py +267 -0
  11. bertrend/demos/demos_utils/embed_documents_component.py +52 -0
  12. bertrend/demos/demos_utils/icons.py +41 -0
  13. bertrend/demos/demos_utils/messages.py +11 -0
  14. bertrend/demos/demos_utils/parameters_component.py +162 -0
  15. bertrend/demos/demos_utils/state_utils.py +99 -0
  16. bertrend/demos/start_summarizer_app.sh +9 -0
  17. bertrend/demos/start_topic_analysis_app.sh +9 -0
  18. bertrend/demos/start_weak_signals_app.sh +9 -0
  19. bertrend/demos/summarization/__init__.py +4 -0
  20. bertrend/demos/summarization/summarizer_app.py +98 -0
  21. bertrend/demos/topic_analysis/__init__.py +4 -0
  22. bertrend/demos/topic_analysis/app.py +87 -0
  23. bertrend/demos/topic_analysis/app_utils.py +69 -0
  24. bertrend/demos/topic_analysis/demo_pages/__init__.py +4 -0
  25. bertrend/demos/topic_analysis/demo_pages/explore_topics.py +455 -0
  26. bertrend/demos/topic_analysis/demo_pages/newsletters_generation.py +178 -0
  27. bertrend/demos/topic_analysis/demo_pages/temporal_visualizations.py +573 -0
  28. bertrend/demos/topic_analysis/demo_pages/topic_visualizations.py +280 -0
  29. bertrend/demos/topic_analysis/demo_pages/training_page.py +219 -0
  30. bertrend/demos/topic_analysis/messages.py +13 -0
  31. bertrend/demos/weak_signals/__init__.py +4 -0
  32. bertrend/demos/weak_signals/app.py +510 -0
  33. bertrend/demos/weak_signals/messages.py +32 -0
  34. bertrend/demos/weak_signals/visualizations_utils.py +403 -0
  35. bertrend/llm_utils/__init__.py +4 -0
  36. bertrend/llm_utils/newsletter.css +131 -0
  37. bertrend/llm_utils/newsletter_features.py +272 -0
  38. bertrend/llm_utils/openai_client.py +235 -0
  39. bertrend/llm_utils/prompts.py +94 -0
  40. bertrend/metrics/__init__.py +4 -0
  41. bertrend/metrics/temporal_metrics_embedding.py +861 -0
  42. bertrend/metrics/topic_metrics.py +199 -0
  43. bertrend/resources/common_ngrams.json +23 -0
  44. bertrend/resources/stopwords-en.json +3 -0
  45. bertrend/resources/stopwords-fr.json +1 -0
  46. bertrend/resources/stopwords-rte.json +15 -0
  47. bertrend/services/__init__.py +4 -0
  48. bertrend/services/authentication.py +66 -0
  49. bertrend/services/embedding_client.py +154 -0
  50. bertrend/services/embedding_service.py +380 -0
  51. bertrend/services/summarizer.py +45 -0
  52. bertrend/services/summary/__init__.py +4 -0
  53. bertrend/services/summary/abstractive_summarizer.py +74 -0
  54. bertrend/services/summary/chatgpt_summarizer.py +67 -0
  55. bertrend/services/summary/extractive_summarizer.py +368 -0
  56. bertrend/services/summary/lexrank.py +126 -0
  57. bertrend/services/summary/prompts.py +43 -0
  58. bertrend/tests/__init__.py +4 -0
  59. bertrend/tests/test_abstractive_summarizer.py +140 -0
  60. bertrend/tests/test_bertrend.py +1091 -0
  61. bertrend/tests/test_cache_utils.py +111 -0
  62. bertrend/tests/test_chatgpt_summarizer.py +150 -0
  63. bertrend/tests/test_config_utils.py +54 -0
  64. bertrend/tests/test_data_loading.py +326 -0
  65. bertrend/tests/test_data_structure.py +53 -0
  66. bertrend/tests/test_embedding_client.py +546 -0
  67. bertrend/tests/test_extractive_summarizer.py +250 -0
  68. bertrend/tests/test_installation.py +183 -0
  69. bertrend/tests/test_newsletter_features.py +354 -0
  70. bertrend/tests/test_openai_client.py +210 -0
  71. bertrend/tests/test_representative_docs.py +125 -0
  72. bertrend/tests/test_summarizer.py +115 -0
  73. bertrend/tests/test_temporal_metrics_embedding.py +349 -0
  74. bertrend/tests/test_topic_description.py +159 -0
  75. bertrend/tests/test_topic_metrics.py +233 -0
  76. bertrend/tests/test_topic_model.py +285 -0
  77. bertrend/tests/test_trend_analysis_data_structure.py +174 -0
  78. bertrend/tests/test_trend_analysis_prompts.py +225 -0
  79. bertrend/tests/test_trend_analysis_visualizations.py +307 -0
  80. bertrend/tests/test_trend_analysis_weak_signals.py +598 -0
  81. bertrend/tests/test_utils_init.py +70 -0
  82. bertrend/tests/test_visualizations.py +306 -0
  83. bertrend/topic_analysis/__init__.py +4 -0
  84. bertrend/topic_analysis/data_structure.py +13 -0
  85. bertrend/topic_analysis/prompts.py +42 -0
  86. bertrend/topic_analysis/representative_docs.py +57 -0
  87. bertrend/topic_analysis/topic_description.py +66 -0
  88. bertrend/topic_analysis/visualizations.py +315 -0
  89. bertrend/trend_analysis/__init__.py +4 -0
  90. bertrend/trend_analysis/data_structure.py +78 -0
  91. bertrend/trend_analysis/prompts.py +229 -0
  92. bertrend/trend_analysis/signal_llm_template_en.html +181 -0
  93. bertrend/trend_analysis/signal_llm_template_fr.html +181 -0
  94. bertrend/trend_analysis/visualizations.py +605 -0
  95. bertrend/trend_analysis/weak_signals.py +425 -0
  96. bertrend/utils/__init__.py +31 -0
  97. bertrend/utils/cache_utils.py +31 -0
  98. bertrend/utils/config_utils.py +41 -0
  99. bertrend/utils/data_loading.py +329 -0
  100. bertrend-0.3.1.dist-info/AUTHORS.txt +15 -0
  101. bertrend-0.3.1.dist-info/LICENSE.md +373 -0
  102. bertrend-0.3.1.dist-info/METADATA +400 -0
  103. bertrend-0.3.1.dist-info/RECORD +181 -0
  104. bertrend-0.3.1.dist-info/WHEEL +4 -0
  105. bertrend_apps/__init__.py +4 -0
  106. bertrend_apps/common/__init__.py +4 -0
  107. bertrend_apps/common/crontab_utils.py +113 -0
  108. bertrend_apps/common/mail_utils.py +91 -0
  109. bertrend_apps/config/feeds/arxiv_feed.toml +15 -0
  110. bertrend_apps/config/feeds/decarbonation_feed.toml +15 -0
  111. bertrend_apps/config/feeds/llm_en_feed.toml +15 -0
  112. bertrend_apps/config/feeds/llm_feed.toml +16 -0
  113. bertrend_apps/config/feeds/nlp_feed.toml +15 -0
  114. bertrend_apps/config/feeds/rte_feed.toml +15 -0
  115. bertrend_apps/config/gmail_credentials.json +1 -0
  116. bertrend_apps/config/newsletters/arxiv_newsletter_from_last.toml +39 -0
  117. bertrend_apps/config/newsletters/arxiv_newsletter_from_scratch.toml +39 -0
  118. bertrend_apps/config/newsletters/decarbonation_newsletter_from_last.toml +38 -0
  119. bertrend_apps/config/newsletters/decarbonation_newsletter_from_scratch.toml +38 -0
  120. bertrend_apps/config/newsletters/llm_en_newsletter_from_last.toml +38 -0
  121. bertrend_apps/config/newsletters/llm_newsletter_from_last.toml +39 -0
  122. bertrend_apps/config/newsletters/nlp_newsletter_from_last.toml +38 -0
  123. bertrend_apps/config/newsletters/rte_newsletter_from_last.toml +38 -0
  124. bertrend_apps/config/newsletters/rte_newsletter_from_scratch.toml +38 -0
  125. bertrend_apps/data_provider/__init__.py +9 -0
  126. bertrend_apps/data_provider/__main__.py +237 -0
  127. bertrend_apps/data_provider/arxiv_provider.py +161 -0
  128. bertrend_apps/data_provider/bing_news_provider.py +82 -0
  129. bertrend_apps/data_provider/curebot_provider.py +105 -0
  130. bertrend_apps/data_provider/data_provider.py +194 -0
  131. bertrend_apps/data_provider/google_news_provider.py +94 -0
  132. bertrend_apps/data_provider/newscatcher_provider.py +74 -0
  133. bertrend_apps/data_provider/utils.py +165 -0
  134. bertrend_apps/exploration/__init__.py +4 -0
  135. bertrend_apps/exploration/curebot/__init__.py +4 -0
  136. bertrend_apps/exploration/curebot/app.py +45 -0
  137. bertrend_apps/exploration/curebot/app_utils.py +317 -0
  138. bertrend_apps/exploration/curebot/config.toml +33 -0
  139. bertrend_apps/exploration/curebot/import_extraction_article.ipynb +108 -0
  140. bertrend_apps/exploration/curebot/old_veille_analyse.py +298 -0
  141. bertrend_apps/exploration/curebot/prompts.py +24 -0
  142. bertrend_apps/exploration/curebot/start_newsletter_generator.sh +8 -0
  143. bertrend_apps/exploration/curebot/tabs/tab1.py +150 -0
  144. bertrend_apps/exploration/curebot/tabs/tab2.py +95 -0
  145. bertrend_apps/exploration/curebot/tabs/tab3.py +125 -0
  146. bertrend_apps/exploration/dataviz/Main_page.py +10 -0
  147. bertrend_apps/exploration/dataviz/__init__.py +4 -0
  148. bertrend_apps/exploration/dataviz/pages/1_Data_feeds.py +105 -0
  149. bertrend_apps/exploration/geolocalization/__init__.py +4 -0
  150. bertrend_apps/exploration/geolocalization/entity_utils.py +118 -0
  151. bertrend_apps/exploration/geolocalization/spacy/__init__.py +4 -0
  152. bertrend_apps/exploration/geolocalization/spacy/common_factory.py +142 -0
  153. bertrend_apps/exploration/geolocalization/spacy/dbpedia.py +206 -0
  154. bertrend_apps/exploration/geolocalization/spacy/nlp.config +37 -0
  155. bertrend_apps/exploration/geolocalization/spacy/utils.py +39 -0
  156. bertrend_apps/install_feeds_and_newsletters.py +29 -0
  157. bertrend_apps/newsletters/__init__.py +4 -0
  158. bertrend_apps/newsletters/__main__.py +277 -0
  159. bertrend_apps/prospective_demo/.streamlit/config.toml +3 -0
  160. bertrend_apps/prospective_demo/.streamlit/secrets.toml +9 -0
  161. bertrend_apps/prospective_demo/__init__.py +70 -0
  162. bertrend_apps/prospective_demo/app.py +111 -0
  163. bertrend_apps/prospective_demo/authentication.py +44 -0
  164. bertrend_apps/prospective_demo/dashboard_analysis.py +132 -0
  165. bertrend_apps/prospective_demo/dashboard_common.py +79 -0
  166. bertrend_apps/prospective_demo/dashboard_signals.py +226 -0
  167. bertrend_apps/prospective_demo/feeds_common.py +38 -0
  168. bertrend_apps/prospective_demo/feeds_config.py +261 -0
  169. bertrend_apps/prospective_demo/feeds_data.py +95 -0
  170. bertrend_apps/prospective_demo/i18n.py +136 -0
  171. bertrend_apps/prospective_demo/llm_utils.py +34 -0
  172. bertrend_apps/prospective_demo/models_info.py +361 -0
  173. bertrend_apps/prospective_demo/perf_utils.py +23 -0
  174. bertrend_apps/prospective_demo/process_new_data.py +348 -0
  175. bertrend_apps/prospective_demo/report_generation.py +214 -0
  176. bertrend_apps/prospective_demo/report_template.html +148 -0
  177. bertrend_apps/prospective_demo/report_utils.py +132 -0
  178. bertrend_apps/prospective_demo/start_demo.sh +9 -0
  179. bertrend_apps/prospective_demo/streamlit_utils.py +37 -0
  180. bertrend_apps/start_apps.sh +15 -0
  181. 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)