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.
- bibex_core/MergeDB.py +1189 -0
- bibex_core/__init__.py +13 -0
- bibex_core/modules/__init__.py +35 -0
- bibex_core/modules/api_utils.py +1172 -0
- bibex_core/modules/c1_utils.py +423 -0
- bibex_core/modules/file_utils.py +71 -0
- bibex_core/modules/ml_utils.py +332 -0
- bibex_core/modules/post_process.py +50 -0
- bibex_core/modules/stats_utils.py +216 -0
- bibex_core/scp2xlsx.py +235 -0
- bibex_core/wos2xlsx.py +159 -0
- bibex_core/xlsx2vos.py +185 -0
- bibexpy/__init__.py +9 -0
- bibexpy/__main__.py +6 -0
- bibexpy/_server/__init__.py +1 -0
- bibexpy/_server/config.py +92 -0
- bibexpy/_server/jobs/__init__.py +3 -0
- bibexpy/_server/jobs/runner.py +301 -0
- bibexpy/_server/main.py +211 -0
- bibexpy/_server/models/__init__.py +0 -0
- bibexpy/_server/models/project.py +25 -0
- bibexpy/_server/routers/__init__.py +0 -0
- bibexpy/_server/routers/audit.py +65 -0
- bibexpy/_server/routers/convert.py +101 -0
- bibexpy/_server/routers/disambiguate.py +177 -0
- bibexpy/_server/routers/downloads.py +26 -0
- bibexpy/_server/routers/enrich.py +42 -0
- bibexpy/_server/routers/export.py +116 -0
- bibexpy/_server/routers/export_folder.py +97 -0
- bibexpy/_server/routers/filter.py +131 -0
- bibexpy/_server/routers/jobs.py +72 -0
- bibexpy/_server/routers/merge.py +437 -0
- bibexpy/_server/routers/prepare.py +241 -0
- bibexpy/_server/routers/projects.py +49 -0
- bibexpy/_server/routers/quality.py +288 -0
- bibexpy/_server/routers/records.py +204 -0
- bibexpy/_server/routers/report.py +111 -0
- bibexpy/_server/routers/settings.py +383 -0
- bibexpy/_server/routers/system.py +209 -0
- bibexpy/_server/routers/tools.py +257 -0
- bibexpy/_server/routers/upload.py +110 -0
- bibexpy/_server/services/__init__.py +0 -0
- bibexpy/_server/services/analyses.py +364 -0
- bibexpy/_server/services/audit.py +340 -0
- bibexpy/_server/services/bibex_adapter.py +47 -0
- bibexpy/_server/services/bibtex_writer.py +104 -0
- bibexpy/_server/services/converter.py +205 -0
- bibexpy/_server/services/disambiguation/__init__.py +19 -0
- bibexpy/_server/services/disambiguation/blocking.py +581 -0
- bibexpy/_server/services/disambiguation/cache.py +47 -0
- bibexpy/_server/services/disambiguation/deepseek_client.py +203 -0
- bibexpy/_server/services/disambiguation/orcid.py +261 -0
- bibexpy/_server/services/disambiguation/pipeline.py +985 -0
- bibexpy/_server/services/disambiguation/similarity.py +91 -0
- bibexpy/_server/services/enricher.py +338 -0
- bibexpy/_server/services/exporter.py +112 -0
- bibexpy/_server/services/filter_engine.py +300 -0
- bibexpy/_server/services/merger.py +202 -0
- bibexpy/_server/services/methodology.py +102 -0
- bibexpy/_server/services/report_export.py +300 -0
- bibexpy/_server/services/ris_writer.py +62 -0
- bibexpy/_server/services/smart_merger.py +967 -0
- bibexpy/_server/services/storage.py +165 -0
- bibexpy/_web/404/index.html +1 -0
- bibexpy/_web/404.html +1 -0
- bibexpy/_web/_next/static/chunks/117-7ad5186126fa076d.js +2 -0
- bibexpy/_web/_next/static/chunks/156-75beffb8ae92a7f3.js +1 -0
- bibexpy/_web/_next/static/chunks/169-301c04fcec22686f.js +4 -0
- bibexpy/_web/_next/static/chunks/2-ecc1c36a5632ac61.js +1 -0
- bibexpy/_web/_next/static/chunks/244-be343e0f8aa00fb5.js +1 -0
- bibexpy/_web/_next/static/chunks/367-b7457c2b95f6932b.js +1 -0
- bibexpy/_web/_next/static/chunks/382-9cd163eed632e241.js +1 -0
- bibexpy/_web/_next/static/chunks/392-afa8bfa4c1a9893d.js +1 -0
- bibexpy/_web/_next/static/chunks/507-63607ded9f83c1aa.js +1 -0
- bibexpy/_web/_next/static/chunks/624-cae565632f47217d.js +1 -0
- bibexpy/_web/_next/static/chunks/739-a00c6b9c0f11813e.js +1 -0
- bibexpy/_web/_next/static/chunks/862-8e1bd5f93c082c02.js +1 -0
- bibexpy/_web/_next/static/chunks/app/_not-found/page-873ba59b5eb7c342.js +1 -0
- bibexpy/_web/_next/static/chunks/app/layout-0d5fc68ac8971a04.js +1 -0
- bibexpy/_web/_next/static/chunks/app/page-cf2bea5edc74459a.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/convert/page-ab6dd645ae113b64.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/disambiguate/page-dd98482cc47344e9.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/enrich/page-4727e431468514e9.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/export/page-f9005d164bb22cb0.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/layout-07755fda55b3ae1f.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/merge/page-45b95318ea7beccf.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/records/page-bfad80457da9eb0b.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/report/page-f05767e74158e995.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/upload/page-8df78ca9cdecbcb8.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/page-99960711ac6a13e9.js +1 -0
- bibexpy/_web/_next/static/chunks/app/settings/page-002df809d6c5a227.js +1 -0
- bibexpy/_web/_next/static/chunks/app/tools/page-27eee1c6a590859f.js +1 -0
- bibexpy/_web/_next/static/chunks/fd9d1056-f2718be92181f426.js +1 -0
- bibexpy/_web/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
- bibexpy/_web/_next/static/chunks/main-app-d138a6e2f8c847f6.js +1 -0
- bibexpy/_web/_next/static/chunks/main-c4b9ddbf514503ef.js +1 -0
- bibexpy/_web/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
- bibexpy/_web/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
- bibexpy/_web/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- bibexpy/_web/_next/static/chunks/webpack-c81f7fd28659d64f.js +1 -0
- bibexpy/_web/_next/static/css/1c4638f3e39cf127.css +3 -0
- bibexpy/_web/_next/static/ypx2IQAkZiSaFiKWnzZGB/_buildManifest.js +1 -0
- bibexpy/_web/_next/static/ypx2IQAkZiSaFiKWnzZGB/_ssgManifest.js +1 -0
- bibexpy/_web/images/PROMPTS.md +83 -0
- bibexpy/_web/images/about-illustration.png +0 -0
- bibexpy/_web/images/authors/alperen-sahin.jpg +0 -0
- bibexpy/_web/images/authors/burak-can-kara.jpg +0 -0
- bibexpy/_web/images/authors/taskin-dirsehan.jpg +0 -0
- bibexpy/_web/images/bibexpy-logo-full.png +0 -0
- bibexpy/_web/images/bibexpy-logo-header.png +0 -0
- bibexpy/_web/images/bibexpy-logo-in.png +0 -0
- bibexpy/_web/images/bibexpy-logo.webp +0 -0
- bibexpy/_web/images/hero-illustration.png +0 -0
- bibexpy/_web/images/loader.gif +0 -0
- bibexpy/_web/images/workflow-diagram.png +0 -0
- bibexpy/_web/index.html +1 -0
- bibexpy/_web/index.txt +11 -0
- bibexpy/_web/projects/_/convert/index.html +1 -0
- bibexpy/_web/projects/_/convert/index.txt +12 -0
- bibexpy/_web/projects/_/disambiguate/index.html +1 -0
- bibexpy/_web/projects/_/disambiguate/index.txt +12 -0
- bibexpy/_web/projects/_/enrich/index.html +1 -0
- bibexpy/_web/projects/_/enrich/index.txt +12 -0
- bibexpy/_web/projects/_/export/index.html +1 -0
- bibexpy/_web/projects/_/export/index.txt +12 -0
- bibexpy/_web/projects/_/merge/index.html +1 -0
- bibexpy/_web/projects/_/merge/index.txt +12 -0
- bibexpy/_web/projects/_/records/index.html +1 -0
- bibexpy/_web/projects/_/records/index.txt +12 -0
- bibexpy/_web/projects/_/report/index.html +1 -0
- bibexpy/_web/projects/_/report/index.txt +12 -0
- bibexpy/_web/projects/_/upload/index.html +1 -0
- bibexpy/_web/projects/_/upload/index.txt +12 -0
- bibexpy/_web/projects/index.html +1 -0
- bibexpy/_web/projects/index.txt +11 -0
- bibexpy/_web/settings/index.html +1 -0
- bibexpy/_web/settings/index.txt +11 -0
- bibexpy/_web/tools/README.md +65 -0
- bibexpy/_web/tools/bibexcel.svg +6 -0
- bibexpy/_web/tools/bibliometrix.png +0 -0
- bibexpy/_web/tools/biblioshiny.svg +12 -0
- bibexpy/_web/tools/citavi.svg +6 -0
- bibexpy/_web/tools/citespace.svg +7 -0
- bibexpy/_web/tools/citnetexplorer.svg +8 -0
- bibexpy/_web/tools/endnote.svg +6 -0
- bibexpy/_web/tools/excel.svg +1 -0
- bibexpy/_web/tools/gephi.svg +66 -0
- bibexpy/_web/tools/histcite.svg +7 -0
- bibexpy/_web/tools/index.html +1 -0
- bibexpy/_web/tools/index.txt +11 -0
- bibexpy/_web/tools/jabref.svg +122 -0
- bibexpy/_web/tools/latex.svg +1 -0
- bibexpy/_web/tools/mendeley.svg +1 -0
- bibexpy/_web/tools/openrefine.svg +15 -0
- bibexpy/_web/tools/overleaf.svg +1 -0
- bibexpy/_web/tools/papers.svg +7 -0
- bibexpy/_web/tools/powerbi.svg +1 -0
- bibexpy/_web/tools/python.svg +1 -0
- bibexpy/_web/tools/r.svg +1 -0
- bibexpy/_web/tools/refworks.svg +5 -0
- bibexpy/_web/tools/scite.svg +5 -0
- bibexpy/_web/tools/tableau.svg +1 -0
- bibexpy/_web/tools/vosviewer.png +0 -0
- bibexpy/_web/tools/zotero.svg +1 -0
- bibexpy/cli.py +242 -0
- bibexpy-2.0.0.dist-info/METADATA +105 -0
- bibexpy-2.0.0.dist-info/RECORD +171 -0
- bibexpy-2.0.0.dist-info/WHEEL +5 -0
- bibexpy-2.0.0.dist-info/entry_points.txt +2 -0
- bibexpy-2.0.0.dist-info/licenses/LICENSE +674 -0
- 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)
|