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,423 @@
1
+ """C1 (adres) ayrıştırma + ülke / üst-kurum standartlaştırma yardımcıları.
2
+
3
+ WoS C1 formatı tipik olarak özelden genele sıralıdır:
4
+ "[Yazarlar] Departman, Alt-birim, Üniversite, Şehir, Eyalet Posta, Ülke"
5
+
6
+ Strateji (Harmonization / deterministik-önce):
7
+ - canonical_country: ülke adı varyantını ISO kanonik İngilizce ada eşler (USA -> United States)
8
+ - parse_c1_address: tek bir adres bileşeninden üst kurum + ülke çıkarır
9
+ - split_c1_addresses: çok-yazarlı C1'i ';' ile böler, "[yazar]" parantezini soyar
10
+ - extract_country_from_c1 / rollup_c1_to_parent: kayıt bazlı toplu yardımcılar
11
+
12
+ Çözülemeyen / belirsiz durumlar LLM katmanına (Tier 2) bırakılır.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+
19
+ # ── Ülke varyant -> kanonik (anahtarlar küçük harf). WoS (USA, PEOPLES R CHINA,
20
+ # ENGLAND...) + Scopus (United States, China...) varyantlarını kapsar. ──
21
+ COUNTRY_VARIANTS: dict[str, str] = {
22
+ "usa": "United States", "u.s.a.": "United States", "u.s.a": "United States",
23
+ "us": "United States", "u.s.": "United States", "u s a": "United States",
24
+ "united states of america": "United States", "america": "United States",
25
+ "united states": "United States",
26
+ "uk": "United Kingdom", "u.k.": "United Kingdom", "great britain": "United Kingdom",
27
+ "england": "United Kingdom", "scotland": "United Kingdom", "wales": "United Kingdom",
28
+ "northern ireland": "United Kingdom", "north ireland": "United Kingdom",
29
+ "united kingdom": "United Kingdom",
30
+ "peoples r china": "China", "p r china": "China", "prc": "China",
31
+ "peoples republic of china": "China", "china": "China", "mainland china": "China",
32
+ "south korea": "South Korea", "korea": "South Korea", "republic of korea": "South Korea",
33
+ "korea rep": "South Korea", "korea (south)": "South Korea",
34
+ "north korea": "North Korea", "dprk": "North Korea",
35
+ "russia": "Russia", "russian federation": "Russia", "ussr": "Russia",
36
+ "turkiye": "Turkey", "türkiye": "Turkey", "turkey": "Turkey",
37
+ "iran": "Iran", "islamic republic of iran": "Iran",
38
+ "czechia": "Czech Republic", "czech republic": "Czech Republic",
39
+ "netherlands": "Netherlands", "the netherlands": "Netherlands", "holland": "Netherlands",
40
+ "uae": "United Arab Emirates", "u arab emirates": "United Arab Emirates",
41
+ "united arab emirates": "United Arab Emirates",
42
+ "germany": "Germany", "deutschland": "Germany", "fed rep ger": "Germany",
43
+ "viet nam": "Vietnam", "vietnam": "Vietnam",
44
+ "taiwan": "Taiwan", "rep of china": "Taiwan",
45
+ "hong kong": "Hong Kong", "macau": "Macau", "macao": "Macau",
46
+ "saudi arabia": "Saudi Arabia", "ksa": "Saudi Arabia",
47
+ "brasil": "Brazil", "brazil": "Brazil",
48
+ "espana": "Spain", "españa": "Spain", "spain": "Spain",
49
+ "cote d'ivoire": "Ivory Coast", "ivory coast": "Ivory Coast",
50
+ }
51
+
52
+ # Halihazırda kanonik kabul edilen ülke adları (değişiklik gerekmez) — varyant
53
+ # değerleri + yaygın tek-formlu ülkeler.
54
+ CANONICAL_COUNTRIES: set[str] = set(COUNTRY_VARIANTS.values()) | {
55
+ "France", "Italy", "Japan", "Canada", "Australia", "India", "Mexico",
56
+ "Poland", "Sweden", "Norway", "Denmark", "Finland", "Belgium", "Austria",
57
+ "Switzerland", "Portugal", "Greece", "Ireland", "Israel", "Egypt", "Pakistan",
58
+ "Indonesia", "Malaysia", "Thailand", "Singapore", "Philippines", "Argentina",
59
+ "Chile", "Colombia", "Peru", "South Africa", "Nigeria", "Kenya", "Morocco",
60
+ "Romania", "Hungary", "Ukraine", "Bulgaria", "Croatia", "Serbia", "Slovenia",
61
+ "Slovakia", "Lithuania", "Latvia", "Estonia", "Qatar", "Kuwait", "Jordan",
62
+ "Lebanon", "Iraq", "New Zealand", "Bangladesh", "Sri Lanka", "Nepal",
63
+ }
64
+ _CANON_LOWER = {c.lower(): c for c in CANONICAL_COUNTRIES}
65
+
66
+ # Üst kurum anahtar kelimeleri (varlığı "kurum" işareti)
67
+ # Üst kurum anahtar kelimeleri — WoS kısaltmaları dahil (Inst, Hosp, Ctr, Coll...)
68
+ _ORG_KW = (
69
+ "univ", "institut", "inst", "college", "coll", "school", "hospital", "hosp",
70
+ "clinic", "center", "centre", "ctr", "laborator", " lab", "academy", "acad",
71
+ "polytechnic", "polytech", "hochschule", "universidad", "universita",
72
+ "université", "ministry", "council", "foundation", "fdn", "national", "natl",
73
+ "research", "tech", "sch", "klinik", "spital",
74
+ )
75
+ # Alt-birim işaretleri (kurum DEĞİL — tercih edilmez)
76
+ _SUBUNIT_KW = (
77
+ "depart", "dept", "division", " div ", "faculty", " fac ", "school of",
78
+ "section", "chair", "unit", "program", "laboratory of",
79
+ )
80
+ _POSTAL_RE = re.compile(r"\d{4,6}")
81
+ _US_STATES = {
82
+ "AL","AK","AZ","AR","CA","CO","CT","DE","FL","GA","HI","ID","IL","IN","IA",
83
+ "KS","KY","LA","ME","MD","MA","MI","MN","MS","MO","MT","NE","NV","NH","NJ",
84
+ "NM","NY","NC","ND","OH","OK","OR","PA","RI","SC","SD","TN","TX","UT","VT",
85
+ "VA","WA","WV","WI","WY","DC",
86
+ }
87
+
88
+
89
+ def _norm_country_token(token: str) -> str:
90
+ t = str(token or "").strip().lower().rstrip(".")
91
+ t = re.sub(r"\s+", " ", t)
92
+ return t
93
+
94
+
95
+ def canonical_country(token: str) -> str | None:
96
+ """Ülke varyantını kanonik İngilizce ada eşle. Bilinmiyorsa None (Tier 2)."""
97
+ t = _norm_country_token(token)
98
+ if not t:
99
+ return None
100
+ if t in COUNTRY_VARIANTS:
101
+ return COUNTRY_VARIANTS[t]
102
+ if t in _CANON_LOWER:
103
+ return _CANON_LOWER[t]
104
+ return None
105
+
106
+
107
+ # ── ISO 3166-1 alpha-2 KODU -> kanonik İngilizce ad. OpenAlex `country_code`
108
+ # (US, GB, CN, IN...) -> canonical_country ile aynı adlar (tutarlılık için). ──
109
+ COUNTRY_CODE_TO_NAME: dict[str, str] = {
110
+ "US": "United States", "GB": "United Kingdom", "CN": "China", "KR": "South Korea",
111
+ "KP": "North Korea", "RU": "Russia", "TR": "Turkey", "IR": "Iran",
112
+ "CZ": "Czech Republic", "NL": "Netherlands", "AE": "United Arab Emirates",
113
+ "DE": "Germany", "VN": "Vietnam", "TW": "Taiwan", "HK": "Hong Kong", "MO": "Macau",
114
+ "SA": "Saudi Arabia", "BR": "Brazil", "ES": "Spain", "CI": "Ivory Coast",
115
+ "FR": "France", "IT": "Italy", "JP": "Japan", "CA": "Canada", "AU": "Australia",
116
+ "IN": "India", "MX": "Mexico", "PL": "Poland", "SE": "Sweden", "NO": "Norway",
117
+ "DK": "Denmark", "FI": "Finland", "BE": "Belgium", "AT": "Austria",
118
+ "CH": "Switzerland", "PT": "Portugal", "GR": "Greece", "IE": "Ireland",
119
+ "IL": "Israel", "EG": "Egypt", "PK": "Pakistan", "ID": "Indonesia",
120
+ "MY": "Malaysia", "TH": "Thailand", "SG": "Singapore", "PH": "Philippines",
121
+ "AR": "Argentina", "CL": "Chile", "CO": "Colombia", "PE": "Peru",
122
+ "ZA": "South Africa", "NG": "Nigeria", "KE": "Kenya", "MA": "Morocco",
123
+ "RO": "Romania", "HU": "Hungary", "UA": "Ukraine", "BG": "Bulgaria",
124
+ "HR": "Croatia", "RS": "Serbia", "SI": "Slovenia", "SK": "Slovakia",
125
+ "LT": "Lithuania", "LV": "Latvia", "EE": "Estonia", "QA": "Qatar",
126
+ "KW": "Kuwait", "JO": "Jordan", "LB": "Lebanon", "IQ": "Iraq",
127
+ "NZ": "New Zealand", "BD": "Bangladesh", "LK": "Sri Lanka", "NP": "Nepal",
128
+ "LU": "Luxembourg", "IS": "Iceland", "CY": "Cyprus", "MT": "Malta",
129
+ "EC": "Ecuador", "UY": "Uruguay", "VE": "Venezuela", "CU": "Cuba",
130
+ "CR": "Costa Rica", "PA": "Panama", "BO": "Bolivia", "PY": "Paraguay",
131
+ "TN": "Tunisia", "DZ": "Algeria", "ET": "Ethiopia", "GH": "Ghana",
132
+ "TZ": "Tanzania", "UG": "Uganda", "CM": "Cameroon", "SN": "Senegal",
133
+ "OM": "Oman", "BH": "Bahrain", "YE": "Yemen", "SY": "Syria",
134
+ "AF": "Afghanistan", "KZ": "Kazakhstan", "UZ": "Uzbekistan", "AZ": "Azerbaijan",
135
+ "GE": "Georgia", "AM": "Armenia", "BY": "Belarus", "MD": "Moldova",
136
+ "MK": "North Macedonia", "AL": "Albania", "BA": "Bosnia and Herzegovina",
137
+ "MM": "Myanmar", "KH": "Cambodia", "LA": "Laos", "MN": "Mongolia",
138
+ "BN": "Brunei", "FJ": "Fiji", "PG": "Papua New Guinea", "ZW": "Zimbabwe",
139
+ "ZM": "Zambia", "BW": "Botswana",
140
+ }
141
+
142
+
143
+ def country_from_code(cc: str) -> str | None:
144
+ """ISO 3166-1 alpha-2 ülke kodunu (US, GB, CN...) kanonik İngilizce ada eşle."""
145
+ return COUNTRY_CODE_TO_NAME.get(str(cc or "").strip().upper())
146
+
147
+
148
+ def _looks_geographic(part: str) -> bool:
149
+ p = part.strip()
150
+ if not p:
151
+ return True
152
+ if _POSTAL_RE.search(p):
153
+ return True
154
+ toks = p.replace(",", " ").split()
155
+ if any(tk.upper() in _US_STATES for tk in toks):
156
+ return True
157
+ return False
158
+
159
+
160
+ # Net kurum kelimeleri: bir token bunlarla BAŞLIYORSA kurum say (universit*, institut*,
161
+ # hospital*, college*, ...). Uzun ve ayırt edici oldukları için ön-ek eşleşmesi güvenli.
162
+ _ORG_KW_PREFIX = (
163
+ "univ", "institut", "college", "hospital", "clinic", "center", "centre",
164
+ "laborator", "academ", "polytech", "hochschule", "universidad", "universita",
165
+ "ministr", "council", "foundation", "klinik",
166
+ )
167
+ # Kısaltmalar / kısa riskli kelimeler: yalnız TAM token eşleşmeli (yer adlarını
168
+ # yanlışlıkla kurum saymamak için: 'tech'≠'Techny', 'research'≠'Research Triangle').
169
+ # NOT: 'research'/'national'/'tech' tek başına kurum işareti DEĞİL (yer adı olabilir:
170
+ # 'Research Triangle Park', 'National City'); listeye alınmadı. 'Research Inst' yine
171
+ # 'inst' ile, 'Natl Univ' 'univ' ile yakalanır.
172
+ _ORG_KW_EXACT = (
173
+ "inst", "coll", "hosp", "ctr", "lab", "acad", "natl", "fdn", "sch", "dept",
174
+ "polytechnic", "school", "laboratory", "spital",
175
+ )
176
+
177
+
178
+ def _is_org(part: str) -> bool:
179
+ """Bileşen bir üst kurum mu? Net kelimeler ön-ekle, kısa/riskli kelimeler yalnız
180
+ TAM token ile eşleşir. Böylece 'Research Triangle Park' (şehir), 'Techny' (yer)
181
+ gibi adresler yanlışlıkla kurum sayılmaz."""
182
+ toks = re.findall(r"[a-zçğıöşü]+", part.lower())
183
+ if not toks:
184
+ return False
185
+ tokset = set(toks)
186
+ if tokset & set(_ORG_KW_EXACT):
187
+ return True
188
+ return any(tok.startswith(pfx) for tok in toks for pfx in _ORG_KW_PREFIX)
189
+
190
+
191
+ def _is_subunit(part: str) -> bool:
192
+ low = part.lower()
193
+ return any(kw in low for kw in _SUBUNIT_KW)
194
+
195
+
196
+ def split_c1_addresses(value: str) -> list[str]:
197
+ """Çok-yazarlı C1'i adres bileşenlerine böl; '[yazarlar]' parantezini soy.
198
+
199
+ ';' ile böler AMA köşeli parantez [ ... ] içindeki ';' korunur (yazar listesi
200
+ "[Smith, J; Doe, A]" bölünmemeli).
201
+ """
202
+ raw = str(value or "")
203
+ if not raw.strip() or raw.strip().upper() == "NAN":
204
+ return []
205
+ chunks: list[str] = []
206
+ buf: list[str] = []
207
+ depth = 0
208
+ for ch in raw:
209
+ if ch == "[":
210
+ depth += 1
211
+ buf.append(ch)
212
+ elif ch == "]":
213
+ depth = max(0, depth - 1)
214
+ buf.append(ch)
215
+ elif ch == ";" and depth == 0:
216
+ chunks.append("".join(buf))
217
+ buf = []
218
+ else:
219
+ buf.append(ch)
220
+ if buf:
221
+ chunks.append("".join(buf))
222
+ out: list[str] = []
223
+ for chunk in chunks:
224
+ c = re.sub(r"^\s*\[[^\]]*\]\s*", "", chunk).strip() # [Author; Author] kaldır
225
+ if c:
226
+ out.append(c)
227
+ return out
228
+
229
+
230
+ def parse_c1_address(addr: str) -> dict:
231
+ """Tek adres bileşeninden üst kurum + ülke çıkar.
232
+
233
+ Dönüş: {raw, institution, country, stripped}. institution/country None olabilir.
234
+ """
235
+ raw = str(addr or "").strip()
236
+ parts = [p.strip() for p in raw.split(",") if p.strip()]
237
+ country = None
238
+ if parts:
239
+ c = canonical_country(parts[-1])
240
+ if c:
241
+ country = c
242
+ parts = parts[:-1]
243
+ # sondaki şehir/eyalet/posta bileşenlerini soy
244
+ while parts and _looks_geographic(parts[-1]):
245
+ parts.pop()
246
+
247
+ institution = None
248
+ # 1) kurum anahtarı olan + alt-birim olmayan en sağdaki parça
249
+ for p in reversed(parts):
250
+ if _is_org(p) and not _is_subunit(p):
251
+ institution = p
252
+ break
253
+ # 2) kurum anahtarı olan herhangi bir parça
254
+ if institution is None:
255
+ for p in reversed(parts):
256
+ if _is_org(p):
257
+ institution = p
258
+ break
259
+ # 3) Kurum anahtarı hiç yoksa TAHMİN ETME (şehir seçme riski) — None bırak;
260
+ # bu adres Tier 2 (LLM) / elle düzeltmeye kalır, yanlış üst-kurum yazılmaz.
261
+
262
+ return {"raw": raw, "institution": institution, "country": country, "stripped": parts}
263
+
264
+
265
+ def extract_country_from_c1(value: str) -> list[str]:
266
+ """Bir C1 hücresindeki tüm ham ülke tokenlerini döndür (kanonikleştirmeden)."""
267
+ out: list[str] = []
268
+ for addr in split_c1_addresses(value):
269
+ parts = [p.strip() for p in addr.split(",") if p.strip()]
270
+ if parts:
271
+ out.append(parts[-1])
272
+ return out
273
+
274
+
275
+ def rollup_c1_to_parent(value: str) -> str:
276
+ """C1'in her adres bileşenini üst kuruma indir; çok-yazar yapısını koru."""
277
+ rolled: list[str] = []
278
+ for addr in split_c1_addresses(value):
279
+ info = parse_c1_address(addr)
280
+ inst = info["institution"]
281
+ country = info["country"]
282
+ if inst and country:
283
+ rolled.append(f"{inst}, {country}")
284
+ elif inst:
285
+ rolled.append(inst)
286
+ else:
287
+ rolled.append(addr)
288
+ # benzersizleştir (sırayı koru)
289
+ seen: set[str] = set()
290
+ uniq = [x for x in rolled if not (x in seen or seen.add(x))]
291
+ return "; ".join(uniq)
292
+
293
+
294
+ def _split_keep(value: str) -> list[str]:
295
+ """';' ile böl (parantez-farkındalıklı) AMA '[yazar]' parantezini KORU.
296
+
297
+ replace_* fonksiyonları için: orijinal yapı (yazar parantezi dahil) bozulmamalı.
298
+ """
299
+ chunks: list[str] = []
300
+ buf: list[str] = []
301
+ depth = 0
302
+ for ch in str(value or ""):
303
+ if ch == "[":
304
+ depth += 1
305
+ buf.append(ch)
306
+ elif ch == "]":
307
+ depth = max(0, depth - 1)
308
+ buf.append(ch)
309
+ elif ch == ";" and depth == 0:
310
+ chunks.append("".join(buf))
311
+ buf = []
312
+ else:
313
+ buf.append(ch)
314
+ if buf:
315
+ chunks.append("".join(buf))
316
+ return chunks
317
+
318
+
319
+ def replace_country_in_c1(value: str, mapping: dict) -> str:
320
+ """C1'deki her adresin ÜLKE bileşenini (en sağdaki virgül parçası) kanonikle değiştir.
321
+
322
+ mapping: {küçük-harf-varyant: kanonik}. Yazar parantezi ve diğer bileşenler korunur.
323
+ """
324
+ if not str(value or "").strip():
325
+ return value
326
+ out: list[str] = []
327
+ for chunk in _split_keep(value):
328
+ parts = chunk.split(",")
329
+ if parts:
330
+ last = parts[-1].strip()
331
+ canon = mapping.get(last.lower())
332
+ if canon and last and last.lower() != canon.lower():
333
+ parts[-1] = " " + canon
334
+ out.append(",".join(parts))
335
+ return ";".join(out)
336
+
337
+
338
+ def append_country_to_c1(value: str, append_map: dict) -> str:
339
+ """C1'deki ülke İÇERMEYEN adreslerin sonuna ülkeyi EKLE (replace değil).
340
+
341
+ append_map: {son-bileşen-küçük-harf: kanonik-ülke}. Bir adresin son virgül
342
+ bileşeni append_map'te VE zaten bir ülke DEĞİLSE (canonical_country None ise),
343
+ adresin sonuna ", <ülke>" eklenir. Kurum/şehir korunur, yalnız ülke eklenir.
344
+
345
+ Kullanım: LLM bir tokenin ülke OLMADIĞINI ama ülkesini bildiğini söylediğinde
346
+ (is_country=0). Böylece kurum adı yok edilmez; eksik ülke doldurulur.
347
+ """
348
+ if not str(value or "").strip():
349
+ return value
350
+ out: list[str] = []
351
+ for chunk in _split_keep(value):
352
+ # '[yazar]' parantezini eşleştirme için geçici soy (çıktıda korunur);
353
+ # extract_country_from_c1 da parantezi soyduğu için anahtarlar böyle eşleşir.
354
+ m = re.match(r"^(\s*\[[^\]]*\]\s*)?(.*)$", chunk, re.S)
355
+ addr = (m.group(2) or "").strip()
356
+ parts = addr.split(",")
357
+ last = parts[-1].strip() if parts else ""
358
+ country = append_map.get(last.lower())
359
+ # Zaten ülkeyse dokunma (replace yolu halleder); değilse sona ekle.
360
+ if country and last and canonical_country(last) is None:
361
+ chunk = chunk.rstrip() + ", " + country
362
+ out.append(chunk)
363
+ return ";".join(out)
364
+
365
+
366
+ def replace_affiliation_in_c1(value: str, variant_to_canonical: dict) -> str:
367
+ """C1'deki affiliation bileşenlerini kanonik forma indir (affiliation apply).
368
+
369
+ variant_to_canonical: {varyant_affiliation: kanonik}. Bileşen-TAM eşleşme yapar
370
+ (substring değil); '[yazar]' paranteziyle ve diğer ';'/',' bileşenleriyle çakışmaz.
371
+ Eşleşme önce tam ';'-bileşeni, sonra ','-alt-bileşeni üzerinde denenir; ikisi de
372
+ boşluk/büyük-küçük harf duyarsız karşılaştırılır.
373
+ """
374
+ if not str(value or "").strip():
375
+ return value
376
+ # Normalize edilmiş varyant -> kanonik (boşluk-collapse + lower)
377
+ norm_map = {re.sub(r"\s+", " ", str(k).strip()).lower(): v for k, v in variant_to_canonical.items()}
378
+
379
+ def _canon(token: str):
380
+ return norm_map.get(re.sub(r"\s+", " ", str(token).strip()).lower())
381
+
382
+ out: list[str] = []
383
+ for chunk in _split_keep(value):
384
+ m = re.match(r"^(\s*\[[^\]]*\]\s*)?(.*)$", chunk, re.S)
385
+ prefix = m.group(1) or ""
386
+ addr = (m.group(2) or "").strip()
387
+ # 1) Tüm adres tam eşleşiyor mu?
388
+ canon = _canon(addr)
389
+ if canon:
390
+ out.append(prefix + canon)
391
+ continue
392
+ # 2) Adresin virgülle ayrılmış bileşenlerinden biri tam eşleşiyor mu?
393
+ parts = addr.split(",")
394
+ replaced = False
395
+ for i, part in enumerate(parts):
396
+ c = _canon(part)
397
+ if c:
398
+ parts[i] = (" " if i > 0 and part.startswith(" ") else "") + c
399
+ replaced = True
400
+ out.append(prefix + ",".join(parts) if replaced else chunk)
401
+ return ";".join(out)
402
+
403
+
404
+ def replace_org_in_c1(value: str, variant_to_canonical: dict) -> str:
405
+ """C1'deki adresleri üst kurumla değiştir (org rollup apply).
406
+
407
+ variant_to_canonical: {tam_adres_varyantı: kanonik_üst_kurum} ('[yazar]' soyulmuş hâli).
408
+ Eşleşen adres bileşeni kanonikle değiştirilir; '[yazar]' parantezi korunur.
409
+ """
410
+ if not str(value or "").strip():
411
+ return value
412
+ # Boşluk/büyük-küçük harf toleranslı eşleme: LLM, adres metnini hafifçe
413
+ # değiştirebilir (fazladan boşluk, harf büyüklüğü). replace_affiliation_in_c1
414
+ # ile aynı normalizasyon deseni.
415
+ norm_map = {re.sub(r"\s+", " ", str(k).strip()).lower(): v for k, v in variant_to_canonical.items()}
416
+ out: list[str] = []
417
+ for chunk in _split_keep(value):
418
+ m = re.match(r"^(\s*\[[^\]]*\]\s*)?(.*)$", chunk, re.S)
419
+ prefix = m.group(1) or ""
420
+ addr = (m.group(2) or "").strip()
421
+ canon = norm_map.get(re.sub(r"\s+", " ", addr).lower())
422
+ out.append(prefix + canon if canon else chunk)
423
+ return ";".join(out)
@@ -0,0 +1,71 @@
1
+ import os
2
+ import glob
3
+ from typing import List
4
+ import pandas as pd
5
+ from datetime import datetime
6
+
7
+ def ensure_dir(directory: str):
8
+ """Ensure directory exists, create if not"""
9
+ if not os.path.exists(directory):
10
+ os.makedirs(directory)
11
+ print(f"Created directory: {directory}")
12
+
13
+ def find_files(directory: str, extension: str) -> List[str]:
14
+ """Find files with given extension in specified directory"""
15
+ pattern = os.path.join(directory, f"*.{extension}")
16
+ return glob.glob(pattern)
17
+
18
+ def find_data_folders() -> List[str]:
19
+ """Find folders with Data_ prefix"""
20
+ return [d for d in os.listdir() if os.path.isdir(d) and d.startswith('Data_')]
21
+
22
+ def save_statistics(stats: dict, output_file: str):
23
+ """Save statistics to file"""
24
+ with open(output_file, 'w', encoding='utf-8') as f:
25
+ f.write(f"Data Processing Statistics - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
26
+ f.write("="*50 + "\n\n")
27
+
28
+ for key, value in stats.items():
29
+ f.write(f"{key}:\n")
30
+ if isinstance(value, dict):
31
+ for k, v in value.items():
32
+ f.write(f" - {k}: {v}\n")
33
+ else:
34
+ f.write(f" {value}\n")
35
+ f.write("\n")
36
+
37
+ def save_comprehensive_statistics(stats: dict, wos_df: pd.DataFrame, scopus_df: pd.DataFrame,
38
+ merged_df: pd.DataFrame, output_file: str, simple_df: pd.DataFrame = None):
39
+ """Save all statistics and analyses to Excel file"""
40
+ from .stats_utils import generate_metadata_statistics, generate_metadata_comparison
41
+
42
+ with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
43
+ # Save basic statistics
44
+ stats_rows = []
45
+ for category, data in stats.items():
46
+ if isinstance(data, dict):
47
+ for key, value in data.items():
48
+ if isinstance(value, dict):
49
+ for subkey, subvalue in value.items():
50
+ stats_rows.append([category, f"{key} - {subkey}", subvalue])
51
+ else:
52
+ stats_rows.append([category, key, value])
53
+ else:
54
+ stats_rows.append([category, "", data])
55
+
56
+ stats_df = pd.DataFrame(stats_rows, columns=['Category', 'Metric', 'Value'])
57
+ stats_df.to_excel(writer, sheet_name='General', index=False)
58
+
59
+ # Save metadata statistics
60
+ wos_metadata_stats = generate_metadata_statistics(wos_df)
61
+ scopus_metadata_stats = generate_metadata_statistics(scopus_df)
62
+ merged_metadata_stats = generate_metadata_statistics(merged_df)
63
+
64
+ wos_metadata_stats.to_excel(writer, sheet_name='WoS Metadata', index=False)
65
+ scopus_metadata_stats.to_excel(writer, sheet_name='Scopus Metadata', index=False)
66
+ merged_metadata_stats.to_excel(writer, sheet_name='Merged Metadata', index=False)
67
+
68
+ # Save metadata comparison statistics
69
+ if simple_df is not None:
70
+ metadata_comparison = generate_metadata_comparison(simple_df, merged_df)
71
+ metadata_comparison.to_excel(writer, sheet_name='Metadata Comparison', index=False)