BibexPy 2.0.0__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 (171) hide show
  1. bibex_core/MergeDB.py +1189 -0
  2. bibex_core/__init__.py +13 -0
  3. bibex_core/modules/__init__.py +35 -0
  4. bibex_core/modules/api_utils.py +1172 -0
  5. bibex_core/modules/c1_utils.py +423 -0
  6. bibex_core/modules/file_utils.py +71 -0
  7. bibex_core/modules/ml_utils.py +332 -0
  8. bibex_core/modules/post_process.py +50 -0
  9. bibex_core/modules/stats_utils.py +216 -0
  10. bibex_core/scp2xlsx.py +235 -0
  11. bibex_core/wos2xlsx.py +159 -0
  12. bibex_core/xlsx2vos.py +185 -0
  13. bibexpy/__init__.py +9 -0
  14. bibexpy/__main__.py +6 -0
  15. bibexpy/_server/__init__.py +1 -0
  16. bibexpy/_server/config.py +92 -0
  17. bibexpy/_server/jobs/__init__.py +3 -0
  18. bibexpy/_server/jobs/runner.py +301 -0
  19. bibexpy/_server/main.py +211 -0
  20. bibexpy/_server/models/__init__.py +0 -0
  21. bibexpy/_server/models/project.py +25 -0
  22. bibexpy/_server/routers/__init__.py +0 -0
  23. bibexpy/_server/routers/audit.py +65 -0
  24. bibexpy/_server/routers/convert.py +101 -0
  25. bibexpy/_server/routers/disambiguate.py +177 -0
  26. bibexpy/_server/routers/downloads.py +26 -0
  27. bibexpy/_server/routers/enrich.py +42 -0
  28. bibexpy/_server/routers/export.py +116 -0
  29. bibexpy/_server/routers/export_folder.py +97 -0
  30. bibexpy/_server/routers/filter.py +131 -0
  31. bibexpy/_server/routers/jobs.py +72 -0
  32. bibexpy/_server/routers/merge.py +437 -0
  33. bibexpy/_server/routers/prepare.py +241 -0
  34. bibexpy/_server/routers/projects.py +49 -0
  35. bibexpy/_server/routers/quality.py +288 -0
  36. bibexpy/_server/routers/records.py +204 -0
  37. bibexpy/_server/routers/report.py +111 -0
  38. bibexpy/_server/routers/settings.py +383 -0
  39. bibexpy/_server/routers/system.py +209 -0
  40. bibexpy/_server/routers/tools.py +257 -0
  41. bibexpy/_server/routers/upload.py +110 -0
  42. bibexpy/_server/services/__init__.py +0 -0
  43. bibexpy/_server/services/analyses.py +364 -0
  44. bibexpy/_server/services/audit.py +340 -0
  45. bibexpy/_server/services/bibex_adapter.py +47 -0
  46. bibexpy/_server/services/bibtex_writer.py +104 -0
  47. bibexpy/_server/services/converter.py +205 -0
  48. bibexpy/_server/services/disambiguation/__init__.py +19 -0
  49. bibexpy/_server/services/disambiguation/blocking.py +581 -0
  50. bibexpy/_server/services/disambiguation/cache.py +47 -0
  51. bibexpy/_server/services/disambiguation/deepseek_client.py +203 -0
  52. bibexpy/_server/services/disambiguation/orcid.py +261 -0
  53. bibexpy/_server/services/disambiguation/pipeline.py +985 -0
  54. bibexpy/_server/services/disambiguation/similarity.py +91 -0
  55. bibexpy/_server/services/enricher.py +338 -0
  56. bibexpy/_server/services/exporter.py +112 -0
  57. bibexpy/_server/services/filter_engine.py +300 -0
  58. bibexpy/_server/services/merger.py +202 -0
  59. bibexpy/_server/services/methodology.py +102 -0
  60. bibexpy/_server/services/report_export.py +300 -0
  61. bibexpy/_server/services/ris_writer.py +62 -0
  62. bibexpy/_server/services/smart_merger.py +967 -0
  63. bibexpy/_server/services/storage.py +165 -0
  64. bibexpy/_web/404/index.html +1 -0
  65. bibexpy/_web/404.html +1 -0
  66. bibexpy/_web/_next/static/chunks/117-7ad5186126fa076d.js +2 -0
  67. bibexpy/_web/_next/static/chunks/156-75beffb8ae92a7f3.js +1 -0
  68. bibexpy/_web/_next/static/chunks/169-301c04fcec22686f.js +4 -0
  69. bibexpy/_web/_next/static/chunks/2-ecc1c36a5632ac61.js +1 -0
  70. bibexpy/_web/_next/static/chunks/244-be343e0f8aa00fb5.js +1 -0
  71. bibexpy/_web/_next/static/chunks/367-b7457c2b95f6932b.js +1 -0
  72. bibexpy/_web/_next/static/chunks/382-9cd163eed632e241.js +1 -0
  73. bibexpy/_web/_next/static/chunks/392-afa8bfa4c1a9893d.js +1 -0
  74. bibexpy/_web/_next/static/chunks/507-63607ded9f83c1aa.js +1 -0
  75. bibexpy/_web/_next/static/chunks/624-cae565632f47217d.js +1 -0
  76. bibexpy/_web/_next/static/chunks/739-a00c6b9c0f11813e.js +1 -0
  77. bibexpy/_web/_next/static/chunks/862-8e1bd5f93c082c02.js +1 -0
  78. bibexpy/_web/_next/static/chunks/app/_not-found/page-873ba59b5eb7c342.js +1 -0
  79. bibexpy/_web/_next/static/chunks/app/layout-0d5fc68ac8971a04.js +1 -0
  80. bibexpy/_web/_next/static/chunks/app/page-cf2bea5edc74459a.js +1 -0
  81. bibexpy/_web/_next/static/chunks/app/projects/[id]/convert/page-ab6dd645ae113b64.js +1 -0
  82. bibexpy/_web/_next/static/chunks/app/projects/[id]/disambiguate/page-dd98482cc47344e9.js +1 -0
  83. bibexpy/_web/_next/static/chunks/app/projects/[id]/enrich/page-4727e431468514e9.js +1 -0
  84. bibexpy/_web/_next/static/chunks/app/projects/[id]/export/page-f9005d164bb22cb0.js +1 -0
  85. bibexpy/_web/_next/static/chunks/app/projects/[id]/layout-07755fda55b3ae1f.js +1 -0
  86. bibexpy/_web/_next/static/chunks/app/projects/[id]/merge/page-45b95318ea7beccf.js +1 -0
  87. bibexpy/_web/_next/static/chunks/app/projects/[id]/records/page-bfad80457da9eb0b.js +1 -0
  88. bibexpy/_web/_next/static/chunks/app/projects/[id]/report/page-f05767e74158e995.js +1 -0
  89. bibexpy/_web/_next/static/chunks/app/projects/[id]/upload/page-8df78ca9cdecbcb8.js +1 -0
  90. bibexpy/_web/_next/static/chunks/app/projects/page-99960711ac6a13e9.js +1 -0
  91. bibexpy/_web/_next/static/chunks/app/settings/page-002df809d6c5a227.js +1 -0
  92. bibexpy/_web/_next/static/chunks/app/tools/page-27eee1c6a590859f.js +1 -0
  93. bibexpy/_web/_next/static/chunks/fd9d1056-f2718be92181f426.js +1 -0
  94. bibexpy/_web/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
  95. bibexpy/_web/_next/static/chunks/main-app-d138a6e2f8c847f6.js +1 -0
  96. bibexpy/_web/_next/static/chunks/main-c4b9ddbf514503ef.js +1 -0
  97. bibexpy/_web/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
  98. bibexpy/_web/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
  99. bibexpy/_web/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  100. bibexpy/_web/_next/static/chunks/webpack-c81f7fd28659d64f.js +1 -0
  101. bibexpy/_web/_next/static/css/1c4638f3e39cf127.css +3 -0
  102. bibexpy/_web/_next/static/ypx2IQAkZiSaFiKWnzZGB/_buildManifest.js +1 -0
  103. bibexpy/_web/_next/static/ypx2IQAkZiSaFiKWnzZGB/_ssgManifest.js +1 -0
  104. bibexpy/_web/images/PROMPTS.md +83 -0
  105. bibexpy/_web/images/about-illustration.png +0 -0
  106. bibexpy/_web/images/authors/alperen-sahin.jpg +0 -0
  107. bibexpy/_web/images/authors/burak-can-kara.jpg +0 -0
  108. bibexpy/_web/images/authors/taskin-dirsehan.jpg +0 -0
  109. bibexpy/_web/images/bibexpy-logo-full.png +0 -0
  110. bibexpy/_web/images/bibexpy-logo-header.png +0 -0
  111. bibexpy/_web/images/bibexpy-logo-in.png +0 -0
  112. bibexpy/_web/images/bibexpy-logo.webp +0 -0
  113. bibexpy/_web/images/hero-illustration.png +0 -0
  114. bibexpy/_web/images/loader.gif +0 -0
  115. bibexpy/_web/images/workflow-diagram.png +0 -0
  116. bibexpy/_web/index.html +1 -0
  117. bibexpy/_web/index.txt +11 -0
  118. bibexpy/_web/projects/_/convert/index.html +1 -0
  119. bibexpy/_web/projects/_/convert/index.txt +12 -0
  120. bibexpy/_web/projects/_/disambiguate/index.html +1 -0
  121. bibexpy/_web/projects/_/disambiguate/index.txt +12 -0
  122. bibexpy/_web/projects/_/enrich/index.html +1 -0
  123. bibexpy/_web/projects/_/enrich/index.txt +12 -0
  124. bibexpy/_web/projects/_/export/index.html +1 -0
  125. bibexpy/_web/projects/_/export/index.txt +12 -0
  126. bibexpy/_web/projects/_/merge/index.html +1 -0
  127. bibexpy/_web/projects/_/merge/index.txt +12 -0
  128. bibexpy/_web/projects/_/records/index.html +1 -0
  129. bibexpy/_web/projects/_/records/index.txt +12 -0
  130. bibexpy/_web/projects/_/report/index.html +1 -0
  131. bibexpy/_web/projects/_/report/index.txt +12 -0
  132. bibexpy/_web/projects/_/upload/index.html +1 -0
  133. bibexpy/_web/projects/_/upload/index.txt +12 -0
  134. bibexpy/_web/projects/index.html +1 -0
  135. bibexpy/_web/projects/index.txt +11 -0
  136. bibexpy/_web/settings/index.html +1 -0
  137. bibexpy/_web/settings/index.txt +11 -0
  138. bibexpy/_web/tools/README.md +65 -0
  139. bibexpy/_web/tools/bibexcel.svg +6 -0
  140. bibexpy/_web/tools/bibliometrix.png +0 -0
  141. bibexpy/_web/tools/biblioshiny.svg +12 -0
  142. bibexpy/_web/tools/citavi.svg +6 -0
  143. bibexpy/_web/tools/citespace.svg +7 -0
  144. bibexpy/_web/tools/citnetexplorer.svg +8 -0
  145. bibexpy/_web/tools/endnote.svg +6 -0
  146. bibexpy/_web/tools/excel.svg +1 -0
  147. bibexpy/_web/tools/gephi.svg +66 -0
  148. bibexpy/_web/tools/histcite.svg +7 -0
  149. bibexpy/_web/tools/index.html +1 -0
  150. bibexpy/_web/tools/index.txt +11 -0
  151. bibexpy/_web/tools/jabref.svg +122 -0
  152. bibexpy/_web/tools/latex.svg +1 -0
  153. bibexpy/_web/tools/mendeley.svg +1 -0
  154. bibexpy/_web/tools/openrefine.svg +15 -0
  155. bibexpy/_web/tools/overleaf.svg +1 -0
  156. bibexpy/_web/tools/papers.svg +7 -0
  157. bibexpy/_web/tools/powerbi.svg +1 -0
  158. bibexpy/_web/tools/python.svg +1 -0
  159. bibexpy/_web/tools/r.svg +1 -0
  160. bibexpy/_web/tools/refworks.svg +5 -0
  161. bibexpy/_web/tools/scite.svg +5 -0
  162. bibexpy/_web/tools/tableau.svg +1 -0
  163. bibexpy/_web/tools/vosviewer.png +0 -0
  164. bibexpy/_web/tools/zotero.svg +1 -0
  165. bibexpy/cli.py +242 -0
  166. bibexpy-2.0.0.dist-info/METADATA +105 -0
  167. bibexpy-2.0.0.dist-info/RECORD +171 -0
  168. bibexpy-2.0.0.dist-info/WHEEL +5 -0
  169. bibexpy-2.0.0.dist-info/entry_points.txt +2 -0
  170. bibexpy-2.0.0.dist-info/licenses/LICENSE +674 -0
  171. bibexpy-2.0.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,332 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ from sklearn.feature_extraction.text import TfidfVectorizer
4
+ from sklearn.multioutput import MultiOutputClassifier
5
+ from sklearn.ensemble import RandomForestClassifier
6
+ from sklearn.preprocessing import MultiLabelBinarizer
7
+ import re
8
+ from colorama import Fore, Style
9
+
10
+ # NLTK OPSIYONELDIR. Import veya veri (punkt/stopwords) yoksa builtin fallback'lere
11
+ # dusulur; bu, cevrimdisi makinede ve ilk-calistirmada cokmemeyi garanti eder.
12
+ # Veri indirme import-zamaninda DEGIL, yalniz ilk gercek kullanimda ve sessizce
13
+ # denenir (paket import edilince ag erisimi/indirme tetiklenmez).
14
+ try:
15
+ import nltk # type: ignore
16
+ except Exception: # pragma: no cover
17
+ nltk = None
18
+
19
+ # NLTK yoksa kullanilan builtin Ingilizce stopword listesi.
20
+ _FALLBACK_STOPWORDS = frozenset((
21
+ "a about above after again against all am an and any are aren't as at be because been "
22
+ "before being below between both but by can cannot could couldn't did didn't do does "
23
+ "doesn't doing don't down during each few for from further had hadn't has hasn't have "
24
+ "haven't having he her here hers herself him himself his how i if in into is isn't it "
25
+ "its itself just me more most my myself no nor not now of off on once only or other our "
26
+ "ours ourselves out over own same she should shouldn't so some such than that the their "
27
+ "theirs them themselves then there these they this those through to too under until up "
28
+ "very was wasn't we were weren't what when where which while who whom why will with won't "
29
+ "would wouldn't you your yours yourself yourselves"
30
+ ).split())
31
+
32
+ _nltk_ready = None # None=denenmedi; True/False=sonuc (bir kez denenir)
33
+
34
+
35
+ def _ensure_nltk() -> bool:
36
+ """punkt+stopwords mevcut mu? Ilk cagride sessizce indirmeyi dener.
37
+ Basarisizsa (cevrimdisi/izinsiz) False doner ve builtin fallback kullanilir."""
38
+ global _nltk_ready
39
+ if _nltk_ready is not None:
40
+ return _nltk_ready
41
+ if nltk is None:
42
+ _nltk_ready = False
43
+ return _nltk_ready
44
+ try:
45
+ try:
46
+ nltk.data.find('tokenizers/punkt')
47
+ nltk.data.find('corpora/stopwords')
48
+ except LookupError:
49
+ for pkg in ('punkt', 'punkt_tab', 'stopwords'):
50
+ try:
51
+ nltk.download(pkg, quiet=True)
52
+ except Exception:
53
+ pass
54
+ nltk.data.find('tokenizers/punkt')
55
+ nltk.data.find('corpora/stopwords')
56
+ _nltk_ready = True
57
+ except Exception:
58
+ _nltk_ready = False
59
+ return _nltk_ready
60
+
61
+
62
+ def _tokenize(text: str) -> list:
63
+ """NLTK varsa word_tokenize; yoksa basit regex tokenizer."""
64
+ if _ensure_nltk():
65
+ try:
66
+ from nltk.tokenize import word_tokenize
67
+ return word_tokenize(text)
68
+ except Exception:
69
+ pass
70
+ return re.findall(r"[a-z]+", text)
71
+
72
+
73
+ def _stopword_set() -> set:
74
+ """NLTK varsa Ingilizce stopword'leri; yoksa builtin fallback."""
75
+ if _ensure_nltk():
76
+ try:
77
+ from nltk.corpus import stopwords
78
+ return set(stopwords.words('english'))
79
+ except Exception:
80
+ pass
81
+ return set(_FALLBACK_STOPWORDS)
82
+
83
+
84
+ def preprocess_text(text):
85
+ """Preprocess text data for ML"""
86
+ if pd.isna(text):
87
+ return ""
88
+ text = str(text).lower()
89
+ text = re.sub(r'[^a-zA-Z\s]', ' ', text)
90
+ text = ' '.join(text.split())
91
+ tokens = _tokenize(text)
92
+ stop_words = _stopword_set()
93
+ tokens = [token for token in tokens if token not in stop_words]
94
+ return ' '.join(tokens)
95
+
96
+ def prepare_training_data(df, text_columns=['TI', 'AB']):
97
+ """Prepare text data for training"""
98
+ # Combine and preprocess text columns
99
+ combined_text = df[text_columns].fillna('').agg(' '.join, axis=1)
100
+ processed_text = combined_text.apply(preprocess_text)
101
+
102
+ return processed_text
103
+
104
+ def train_keyword_model(train_df, text_columns=['TI', 'AB']):
105
+ """Train model for keyword prediction"""
106
+ # Prepare text data
107
+ X = prepare_training_data(train_df, text_columns)
108
+
109
+ # Prepare keywords data
110
+ mlb = MultiLabelBinarizer()
111
+ keywords = train_df['DE'].fillna('').str.split(';')
112
+ keywords = [[kw.strip() for kw in kws if kw.strip()] for kws in keywords]
113
+ y = mlb.fit_transform(keywords)
114
+
115
+ # Create and train TF-IDF vectorizer
116
+ vectorizer = TfidfVectorizer(max_features=5000)
117
+ X_tfidf = vectorizer.fit_transform(X)
118
+
119
+ # Train model
120
+ model = MultiOutputClassifier(RandomForestClassifier(n_estimators=100))
121
+ model.fit(X_tfidf, y)
122
+
123
+ return vectorizer, model, mlb
124
+
125
+ def train_subject_model(train_df, text_columns=['TI', 'AB']):
126
+ """Train model for subject category prediction"""
127
+ # Prepare text data
128
+ X = prepare_training_data(train_df, text_columns)
129
+
130
+ # Prepare subject categories data
131
+ mlb = MultiLabelBinarizer()
132
+ subjects = train_df['SC'].fillna('').str.split(';')
133
+ subjects = [[subj.strip() for subj in subjs if subj.strip()] for subjs in subjects]
134
+ y = mlb.fit_transform(subjects)
135
+
136
+ # Create and train TF-IDF vectorizer
137
+ vectorizer = TfidfVectorizer(max_features=5000)
138
+ X_tfidf = vectorizer.fit_transform(X)
139
+
140
+ # Train model
141
+ model = MultiOutputClassifier(RandomForestClassifier(n_estimators=100))
142
+ model.fit(X_tfidf, y)
143
+
144
+ return vectorizer, model, mlb
145
+
146
+ def predict_keywords(text, vectorizer, model, mlb, threshold=0.3):
147
+ """Predict keywords for given text"""
148
+ # Preprocess text
149
+ processed_text = preprocess_text(text)
150
+
151
+ # Transform text using vectorizer
152
+ X_tfidf = vectorizer.transform([processed_text])
153
+
154
+ # Predict probabilities
155
+ y_pred_proba = model.predict_proba(X_tfidf)
156
+
157
+ # Get keywords above threshold
158
+ keywords = []
159
+ for i, proba in enumerate(zip(*[estimator.predict_proba(X_tfidf)[0] for estimator in model.estimators_])):
160
+ if max(proba) >= threshold:
161
+ keywords.append(mlb.classes_[i])
162
+
163
+ return '; '.join(keywords) if keywords else None
164
+
165
+ def predict_subjects(text, vectorizer, model, mlb, threshold=0.3):
166
+ """Predict subject categories for given text"""
167
+ # Preprocess text
168
+ processed_text = preprocess_text(text)
169
+
170
+ # Transform text using vectorizer
171
+ X_tfidf = vectorizer.transform([processed_text])
172
+
173
+ # Predict probabilities
174
+ y_pred_proba = model.predict_proba(X_tfidf)
175
+
176
+ # Get subjects above threshold
177
+ subjects = []
178
+ for i, proba in enumerate(zip(*[estimator.predict_proba(X_tfidf)[0] for estimator in model.estimators_])):
179
+ if max(proba) >= threshold:
180
+ subjects.append(mlb.classes_[i])
181
+
182
+ return '; '.join(subjects) if subjects else None
183
+
184
+ def train_keywords_plus_model(train_df, text_columns=['TI', 'AB']):
185
+ """Train model for Keywords Plus prediction"""
186
+ # Prepare text data
187
+ X = prepare_training_data(train_df, text_columns)
188
+
189
+ # Prepare Keywords Plus data
190
+ mlb = MultiLabelBinarizer()
191
+ keywords_plus = train_df['ID'].fillna('').str.split(';')
192
+ keywords_plus = [[kw.strip() for kw in kws if kw.strip()] for kws in keywords_plus]
193
+ y = mlb.fit_transform(keywords_plus)
194
+
195
+ # Create and train TF-IDF vectorizer
196
+ vectorizer = TfidfVectorizer(max_features=5000)
197
+ X_tfidf = vectorizer.fit_transform(X)
198
+
199
+ # Train model
200
+ model = MultiOutputClassifier(RandomForestClassifier(n_estimators=100))
201
+ model.fit(X_tfidf, y)
202
+
203
+ return vectorizer, model, mlb
204
+
205
+ def predict_keywords_plus(text, vectorizer, model, mlb, threshold=0.3):
206
+ """Predict Keywords Plus for given text"""
207
+ # Preprocess text
208
+ processed_text = preprocess_text(text)
209
+
210
+ # Transform text using vectorizer
211
+ X_tfidf = vectorizer.transform([processed_text])
212
+
213
+ # Predict probabilities
214
+ y_pred_proba = model.predict_proba(X_tfidf)
215
+
216
+ # Get keywords above threshold
217
+ keywords_plus = []
218
+ for i, proba in enumerate(zip(*[estimator.predict_proba(X_tfidf)[0] for estimator in model.estimators_])):
219
+ if max(proba) >= threshold:
220
+ keywords_plus.append(mlb.classes_[i])
221
+
222
+ return '; '.join(keywords_plus) if keywords_plus else None
223
+
224
+ def enrich_metadata_ml(df):
225
+ """Enrich metadata using ML models"""
226
+ print("\nStarting ML-based metadata enrichment...")
227
+
228
+ # Create copies of the dataframe for training
229
+ train_df_keywords = df[df['DE'].notna()].copy()
230
+ train_df_keywords_plus = df[df['ID'].notna()].copy() # New training set for Keywords Plus
231
+ train_df_subjects = df[df['SC'].notna()].copy()
232
+
233
+ # Create a copy of the input dataframe
234
+ enriched_df = df.copy()
235
+
236
+ # Initialize counters for ID field
237
+ original_empty_id = df['ID'].isna().sum()
238
+
239
+ # Train models if enough training data is available
240
+ if len(train_df_keywords) > 10:
241
+ print(f"\n{Fore.CYAN}Training keyword prediction model...{Style.RESET_ALL}")
242
+ print(f"Using {len(train_df_keywords)} records for training")
243
+ kw_vectorizer, kw_model, kw_mlb = train_keyword_model(train_df_keywords)
244
+
245
+ # Predict missing keywords
246
+ print(f"\n{Fore.CYAN}Predicting missing keywords...{Style.RESET_ALL}")
247
+ mask = enriched_df['DE'].isna()
248
+ total = mask.sum()
249
+ completed = 0
250
+
251
+ for idx in enriched_df[mask].index:
252
+ text = ' '.join([
253
+ str(enriched_df.loc[idx, 'TI']) if pd.notna(enriched_df.loc[idx, 'TI']) else '',
254
+ str(enriched_df.loc[idx, 'AB']) if pd.notna(enriched_df.loc[idx, 'AB']) else ''
255
+ ])
256
+ if text.strip():
257
+ predicted_keywords = predict_keywords(text, kw_vectorizer, kw_model, kw_mlb)
258
+ if predicted_keywords:
259
+ enriched_df.loc[idx, 'DE'] = predicted_keywords
260
+ completed += 1
261
+ if completed % 10 == 0:
262
+ print(f"Progress: {completed}/{total} records processed", end='\r')
263
+ print() # New line after progress
264
+
265
+ # Train Keywords Plus model if enough training data is available
266
+ if len(train_df_keywords_plus) > 10:
267
+ print(f"\n{Fore.CYAN}Training Keywords Plus prediction model...{Style.RESET_ALL}")
268
+ print(f"Using {len(train_df_keywords_plus)} records for training")
269
+ kw_plus_vectorizer, kw_plus_model, kw_plus_mlb = train_keywords_plus_model(train_df_keywords_plus)
270
+
271
+ # Predict missing Keywords Plus
272
+ print(f"\n{Fore.CYAN}Predicting missing Keywords Plus...{Style.RESET_ALL}")
273
+ mask = enriched_df['ID'].isna()
274
+ total = mask.sum()
275
+ completed = 0
276
+
277
+ for idx in enriched_df[mask].index:
278
+ text = ' '.join([
279
+ str(enriched_df.loc[idx, 'TI']) if pd.notna(enriched_df.loc[idx, 'TI']) else '',
280
+ str(enriched_df.loc[idx, 'AB']) if pd.notna(enriched_df.loc[idx, 'AB']) else ''
281
+ ])
282
+ if text.strip():
283
+ predicted_keywords_plus = predict_keywords_plus(text, kw_plus_vectorizer, kw_plus_model, kw_plus_mlb)
284
+ if predicted_keywords_plus:
285
+ enriched_df.loc[idx, 'ID'] = predicted_keywords_plus
286
+ completed += 1
287
+ if completed % 10 == 0:
288
+ print(f"Progress: {completed}/{total} records processed", end='\r')
289
+ print() # New line after progress
290
+
291
+ # Train models if enough training data is available
292
+ if len(train_df_subjects) > 10:
293
+ print(f"\n{Fore.CYAN}Training subject category prediction model...{Style.RESET_ALL}")
294
+ print(f"Using {len(train_df_subjects)} records for training")
295
+ subj_vectorizer, subj_model, subj_mlb = train_subject_model(train_df_subjects)
296
+
297
+ # Predict missing subject categories
298
+ print(f"\n{Fore.CYAN}Predicting missing subject categories...{Style.RESET_ALL}")
299
+ mask = enriched_df['SC'].isna()
300
+ total = mask.sum()
301
+ completed = 0
302
+
303
+ for idx in enriched_df[mask].index:
304
+ text = ' '.join([
305
+ str(enriched_df.loc[idx, 'TI']) if pd.notna(enriched_df.loc[idx, 'TI']) else '',
306
+ str(enriched_df.loc[idx, 'AB']) if pd.notna(enriched_df.loc[idx, 'AB']) else ''
307
+ ])
308
+ if text.strip():
309
+ predicted_subjects = predict_subjects(text, subj_vectorizer, subj_model, subj_mlb)
310
+ if predicted_subjects:
311
+ enriched_df.loc[idx, 'SC'] = predicted_subjects
312
+ enriched_df.loc[idx, 'WC'] = predicted_subjects # Copy to WC as well
313
+ completed += 1
314
+ if completed % 10 == 0:
315
+ print(f"Progress: {completed}/{total} records processed", end='\r')
316
+ print() # New line after progress
317
+
318
+ # Calculate enrichment statistics
319
+ stats = {
320
+ 'total_records': len(df),
321
+ 'original_empty_keywords': df['DE'].isna().sum(),
322
+ 'original_empty_subjects': df['SC'].isna().sum(),
323
+ 'original_empty_id': original_empty_id,
324
+ 'enriched_empty_keywords': enriched_df['DE'].isna().sum(),
325
+ 'enriched_empty_subjects': enriched_df['SC'].isna().sum(),
326
+ 'enriched_empty_id': enriched_df['ID'].isna().sum(),
327
+ 'keywords_filled': df['DE'].isna().sum() - enriched_df['DE'].isna().sum(),
328
+ 'subjects_filled': df['SC'].isna().sum() - enriched_df['SC'].isna().sum(),
329
+ 'id_filled': original_empty_id - enriched_df['ID'].isna().sum()
330
+ }
331
+
332
+ return enriched_df, stats
@@ -0,0 +1,50 @@
1
+ import pandas as pd
2
+ import os
3
+
4
+ def copy_cr_raw_to_cr(file_path: str) -> bool:
5
+ """
6
+ Copies CR_raw column to CR column
7
+
8
+ Args:
9
+ file_path (str): Path to Excel file
10
+
11
+ Returns:
12
+ bool: True if successful, False otherwise
13
+ """
14
+ try:
15
+ # Read Excel file
16
+ df = pd.read_excel(file_path)
17
+
18
+ # If CR_raw column exists and CR column is empty or doesn't exist
19
+ if 'CR_raw' in df.columns:
20
+ df['CR'] = df['CR_raw']
21
+
22
+ # Save changes
23
+ df.to_excel(file_path, index=False)
24
+ print(f"\nCR_raw column copied to CR: {os.path.basename(file_path)}")
25
+ return True
26
+
27
+ except Exception as e:
28
+ print(f"\nError: Failed to process file {file_path}: {str(e)}")
29
+ return False
30
+
31
+ def process_merged_files(result_dir: str) -> None:
32
+ """
33
+ Copies CR_raw column to CR column in Merged_Bib.xlsx and
34
+ Merged_API_Enriched_Bib.xlsx files in Cell_Files folder
35
+
36
+ Args:
37
+ result_dir (str): Path to result directory
38
+ """
39
+ cell_files_dir = os.path.join(result_dir, "Cell_Files")
40
+
41
+ if os.path.exists(cell_files_dir):
42
+ # Files to process
43
+ files_to_process = [
44
+ os.path.join(cell_files_dir, "Merged_Bib.xlsx"),
45
+ os.path.join(cell_files_dir, "Merged_API_Enriched_Bib.xlsx")
46
+ ]
47
+
48
+ for file_path in files_to_process:
49
+ if os.path.exists(file_path):
50
+ copy_cr_raw_to_cr(file_path)
@@ -0,0 +1,216 @@
1
+ import pandas as pd
2
+ from datetime import datetime
3
+
4
+ def generate_detailed_statistics(wos_df: pd.DataFrame, scopus_df: pd.DataFrame, merged_df: pd.DataFrame) -> dict:
5
+ """Generate detailed statistics"""
6
+ # Basic statistics
7
+ basic_stats = {
8
+ 'General Statistics': {
9
+ 'Total WoS Records': len(wos_df),
10
+ 'Total Scopus Records': len(scopus_df),
11
+ 'Merged Records': len(merged_df),
12
+ 'Merge Rate': f"{(len(merged_df) / (len(wos_df) + len(scopus_df)) * 100):.2f}%"
13
+ }
14
+ }
15
+
16
+ # Column statistics
17
+ column_stats = {
18
+ 'Column Statistics': {
19
+ 'WoS Column Count': len(wos_df.columns),
20
+ 'Scopus Column Count': len(scopus_df.columns),
21
+ 'Merged Column Count': len(merged_df.columns),
22
+ 'Common Column Count': len(set(wos_df.columns) & set(scopus_df.columns))
23
+ }
24
+ }
25
+
26
+ # Data quality statistics
27
+ quality_stats = {
28
+ 'Data Quality': {
29
+ 'WoS Empty Cell Rate': f"{(wos_df.isna().sum().sum() / (len(wos_df) * len(wos_df.columns)) * 100):.2f}%",
30
+ 'Scopus Empty Cell Rate': f"{(scopus_df.isna().sum().sum() / (len(scopus_df) * len(scopus_df.columns)) * 100):.2f}%",
31
+ 'Merged Empty Cell Rate': f"{(merged_df.isna().sum().sum() / (len(merged_df) * len(merged_df.columns)) * 100):.2f}%"
32
+ }
33
+ }
34
+
35
+ # Year-based statistics
36
+ if 'Year' in merged_df.columns:
37
+ year_stats = merged_df['Year'].value_counts().sort_index().to_dict()
38
+ year_distribution = {'Year Distribution': year_stats}
39
+ else:
40
+ year_distribution = {'Year Distribution': {'Year information not found': 0}}
41
+
42
+ return {**basic_stats, **column_stats, **quality_stats, **year_distribution}
43
+
44
+ def generate_metadata_statistics(df: pd.DataFrame) -> pd.DataFrame:
45
+ """Generate quality statistics for metadata fields"""
46
+ metadata_desc = {
47
+ 'AU': 'Author',
48
+ 'DT': 'Document Type',
49
+ 'PY': 'Publication Year',
50
+ 'TI': 'Title',
51
+ 'TC': 'Total Citation',
52
+ 'SO': 'Journal',
53
+ 'AB': 'Abstract',
54
+ 'C1': 'Affiliation',
55
+ 'DI': 'DOI',
56
+ 'RP': 'Corresponding Author',
57
+ 'ID': 'Keywords Plus',
58
+ 'WC': 'Science Categories',
59
+ 'DE': 'Keywords',
60
+ 'LA': 'Language',
61
+ 'CR': 'Cited References'
62
+ }
63
+
64
+ stats = []
65
+ total_docs = len(df)
66
+
67
+ for field, desc in metadata_desc.items():
68
+ if field in df.columns:
69
+ missing = df[field].isna().sum() + df[field].eq('').sum()
70
+ missing_pct = (missing / total_docs) * 100
71
+
72
+ # Determine status based on missing percentage
73
+ if missing_pct == 0:
74
+ status = 'Excellent'
75
+ elif missing_pct < 1:
76
+ status = 'Very Good'
77
+ elif missing_pct < 5:
78
+ status = 'Good'
79
+ elif missing_pct < 20:
80
+ status = 'Acceptable'
81
+ elif missing_pct < 50:
82
+ status = 'Poor'
83
+ elif missing_pct < 90:
84
+ status = 'Critical'
85
+ else:
86
+ status = 'Completely Missing'
87
+
88
+ stats.append({
89
+ 'Field': field,
90
+ 'Description': desc,
91
+ 'Missing Count': missing,
92
+ 'Missing %': round(missing_pct, 2),
93
+ 'Status': status
94
+ })
95
+
96
+ return pd.DataFrame(stats)
97
+
98
+ def generate_metadata_comparison(simple_df: pd.DataFrame, enhanced_df: pd.DataFrame) -> pd.DataFrame:
99
+ """Compare metadata statistics between two merge methods"""
100
+ metadata_desc = {
101
+ 'AU': 'Author',
102
+ 'DT': 'Document Type',
103
+ 'PY': 'Publication Year',
104
+ 'TI': 'Title',
105
+ 'TC': 'Total Citation',
106
+ 'SO': 'Journal',
107
+ 'AB': 'Abstract',
108
+ 'C1': 'Affiliation',
109
+ 'DI': 'DOI',
110
+ 'RP': 'Corresponding Author',
111
+ 'ID': 'Keywords Plus',
112
+ 'WC': 'Science Categories',
113
+ 'DE': 'Keywords',
114
+ 'LA': 'Language',
115
+ 'CR': 'Cited References'
116
+ }
117
+
118
+ comparison_stats = []
119
+
120
+ for field, desc in metadata_desc.items():
121
+ if field in simple_df.columns and field in enhanced_df.columns:
122
+ simple_missing = simple_df[field].isna().sum() + simple_df[field].eq('').sum()
123
+ enhanced_missing = enhanced_df[field].isna().sum() + enhanced_df[field].eq('').sum()
124
+
125
+ simple_missing_pct = (simple_missing / len(simple_df)) * 100
126
+ enhanced_missing_pct = (enhanced_missing / len(enhanced_df)) * 100
127
+
128
+ # Calculate improvement rate
129
+ improvement = simple_missing_pct - enhanced_missing_pct
130
+ improvement_pct = (improvement / simple_missing_pct * 100) if simple_missing_pct > 0 else 0
131
+
132
+ # Determine status based on enhanced method results
133
+ if enhanced_missing_pct == 0:
134
+ status = 'Excellent'
135
+ elif enhanced_missing_pct < 1:
136
+ status = 'Very Good'
137
+ elif enhanced_missing_pct < 5:
138
+ status = 'Good'
139
+ elif enhanced_missing_pct < 20:
140
+ status = 'Acceptable'
141
+ elif enhanced_missing_pct < 50:
142
+ status = 'Poor'
143
+ elif enhanced_missing_pct < 90:
144
+ status = 'Critical'
145
+ else:
146
+ status = 'Completely Missing'
147
+
148
+ comparison_stats.append({
149
+ 'Field': field,
150
+ 'Description': desc,
151
+ 'Simple Missing': simple_missing,
152
+ 'Simple Missing %': round(simple_missing_pct, 2),
153
+ 'Enhanced Missing': enhanced_missing,
154
+ 'Enhanced Missing %': round(enhanced_missing_pct, 2),
155
+ 'Improvement': round(improvement, 2),
156
+ 'Improvement %': round(improvement_pct, 2),
157
+ 'Status': status
158
+ })
159
+
160
+ return pd.DataFrame(comparison_stats)
161
+
162
+ def compare_merge_methods(simple_stats: dict, enhanced_stats: dict,
163
+ simple_df: pd.DataFrame, enhanced_df: pd.DataFrame) -> dict:
164
+ """Compare two merge methods and provide detailed analysis"""
165
+
166
+ # 1. Basic Comparison Metrics
167
+ basic_comparison = {
168
+ 'Record Counts and Rates': {
169
+ 'Simple Merge Records': simple_stats['Total Records'],
170
+ 'Enhanced Merge Records': enhanced_stats['Total Records'],
171
+ 'Record Difference': enhanced_stats['Total Records'] - simple_stats['Total Records'],
172
+ 'Improvement Rate': f"{((enhanced_stats['Total Records'] - simple_stats['Total Records']) / simple_stats['Total Records'] * 100):.2f}%"
173
+ }
174
+ }
175
+
176
+ # 2. Data Quality Metrics
177
+ simple_empty = simple_df.isna().sum().sum()
178
+ enhanced_empty = enhanced_df.isna().sum().sum()
179
+ simple_total = len(simple_df) * len(simple_df.columns)
180
+ enhanced_total = len(enhanced_df) * len(enhanced_df.columns)
181
+
182
+ quality_metrics = {
183
+ 'Data Quality Metrics': {
184
+ 'Simple Merge Completeness': f"{(1 - simple_empty / simple_total) * 100:.2f}%",
185
+ 'Enhanced Merge Completeness': f"{(1 - enhanced_empty / enhanced_total) * 100:.2f}%",
186
+ 'Empty Cell Improvement Rate': f"{((simple_empty/simple_total - enhanced_empty/enhanced_total) / (simple_empty/simple_total) * 100):.2f}%",
187
+ 'Total Improved Cells': simple_empty - enhanced_empty
188
+ }
189
+ }
190
+
191
+ # 3. Field-Based Detailed Analysis
192
+ field_improvements = {}
193
+ for col in enhanced_df.columns:
194
+ if col in simple_df.columns:
195
+ simple_empty = simple_df[col].isna().sum()
196
+ enhanced_empty = enhanced_df[col].isna().sum()
197
+ simple_unique = simple_df[col].nunique()
198
+ enhanced_unique = enhanced_df[col].nunique()
199
+
200
+ if simple_empty != enhanced_empty or simple_unique != enhanced_unique:
201
+ improvement = {
202
+ 'Simple Empty Cells': simple_empty,
203
+ 'Enhanced Empty Cells': enhanced_empty,
204
+ 'Empty Cell Improvement': simple_empty - enhanced_empty,
205
+ 'Improvement Rate': f"{((simple_empty - enhanced_empty) / simple_empty * 100):.2f}%" if simple_empty > 0 else "0%",
206
+ 'Simple Unique Values': simple_unique,
207
+ 'Enhanced Unique Values': enhanced_unique,
208
+ 'Enrichment': enhanced_unique - simple_unique
209
+ }
210
+ field_improvements[col] = improvement
211
+
212
+ return {
213
+ 'Basic Comparison': basic_comparison,
214
+ 'Data Quality': quality_metrics,
215
+ 'Field-Based Improvements': field_improvements
216
+ }