Compare commits
52 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
eff8bd6d1e | |
|
|
5466dad5fd | |
|
|
1b5af9bb69 | |
|
|
6d4b3d40ef | |
|
|
587c4d06f9 | |
|
|
a8bb74f80c | |
|
|
3c41e97f47 | |
|
|
b958fce5ab | |
|
|
f6894aa186 | |
|
|
783ded3137 | |
|
|
97532099a5 | |
|
|
8ff7607b51 | |
|
|
293bb9b428 | |
|
|
a2db2e62d2 | |
|
|
5ae7570618 | |
|
|
70daca81b6 | |
|
|
c32e271e1f | |
|
|
876cdae6c6 | |
|
|
aeffddff44 | |
|
|
8305e128ea | |
|
|
c1b430e84e | |
|
|
e88a5b7652 | |
|
|
93c279e4e6 | |
|
|
d14a106a6a | |
|
|
b1b9f19ddf | |
|
|
50ac26cd7e | |
|
|
edc645b172 | |
|
|
f1715594d6 | |
|
|
2220345e13 | |
|
|
42e5f01885 | |
|
|
4f5f6f7b78 | |
|
|
3eadb67c3b | |
|
|
bade62a864 | |
|
|
f12d145a8e | |
|
|
2d11a98ada | |
|
|
b3c9939bf6 | |
|
|
bf6180dcd4 | |
|
|
f0ff21c960 | |
|
|
2da715578e | |
|
|
fe6c88586b | |
|
|
37253c4284 | |
|
|
ebcc80093b | |
|
|
20021aef7f | |
|
|
5f429d96d2 | |
|
|
d8ff4947e9 | |
|
|
30e88e87f0 | |
|
|
abd11209b9 | |
|
|
b274291443 | |
|
|
baceddf91f | |
|
|
1ecee1372f | |
|
|
2da4348884 | |
|
|
c9914a47d0 |
|
|
@ -1,2 +1,2 @@
|
|||
recursive-include affro/jsons *.json
|
||||
recursive-include affro/jsons *.json.gz
|
||||
recursive-include affro/txts *.txt
|
||||
|
|
|
|||
325
README.md
325
README.md
|
|
@ -1,41 +1,318 @@
|
|||
# Affiliation-Matching Repository \[aka AffRo\]
|
||||
# `run_affro` — Affiliation Matching Pipeline Documentation
|
||||
|
||||
This repository contains code for matching affiliation strings to ROR or/and OpenOrg IDs.
|
||||
**Version:** 3.3.0
|
||||
**Module:** `core.py`
|
||||
**Entry point:** `run_affro(aff: str) → list[dict]`
|
||||
|
||||
🚀 As it is still a work in progress, *the repository may not always be up-to-date*.
|
||||
However, I will incorporate improvements and bug fixes regularly.
|
||||
---
|
||||
|
||||
## Main files
|
||||
## Overview
|
||||
|
||||
`run_affro` is the main entry point of the **AffRo** affiliation-matching pipeline. It takes a raw affiliation string (as it appears in a publication metadata record) and returns a list of matched organizations with their identifiers (ROR and/or OpenOrgs), confidence scores, status and location information.
|
||||
|
||||
```python
|
||||
from core import run_affro
|
||||
|
||||
results = run_affro("Department of Physics, University of Milan, Italy")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## High-Level Pipeline
|
||||
|
||||
```
|
||||
raw affiliation string
|
||||
│
|
||||
▼
|
||||
direct_mapping() ← fast rule-based lookup for known institute families
|
||||
│
|
||||
├─ match found ──► run_affro_(shortened_aff) + direct results
|
||||
│
|
||||
└─ no match ─────► run_affro_(raw_aff)
|
||||
│
|
||||
▼
|
||||
normalise & stem
|
||||
│
|
||||
dix_name lookup ← exact key lookup
|
||||
│
|
||||
┌─────────────────┼────────────────────┐
|
||||
│ │
|
||||
single match multiple matches
|
||||
│ │
|
||||
▼ ▼
|
||||
build_result_list() ◀---- filter by 'first'/'top_level'/'parent'
|
||||
(algorithm path)
|
||||
│
|
||||
still ambiguous?
|
||||
│
|
||||
▼
|
||||
(algorithm fallback)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stage 1 — `direct_mapping(aff)`
|
||||
|
||||
**File:** `helpers/direct_mapping.py`
|
||||
|
||||
A fast, rule-based pre-processor that recognises affiliation strings belonging to specific **institute families** (Fraunhofer, CNR, Max Planck, Helmholtz, Leibniz, FORTH, Demokritos, IRCCS, …).
|
||||
|
||||
### What it does
|
||||
|
||||
1. Produce a normalized, stemmed version of the raw string.
|
||||
2. Checks for the presence of family-specific keywords (e.g. `fraunhofer`, `cnr`, `max planck`).
|
||||
3. For each recognized family, iterates over pre-built sorted key lists (longest keys first for specificity) and checks whether the key appears close enough to the trigger word using a character-distance heuristic:
|
||||
```
|
||||
distance(aff, trigger_word, key) < len(key) + len(trigger_word) + threshold
|
||||
```
|
||||
4. When a key matches, appends its ROR/OpenOrgs ID to `assigned` and removes the matched substring from the affiliation string (producing `shorten_aff`).
|
||||
|
||||
### Returns
|
||||
|
||||
```python
|
||||
[list_of_direct_results, shortened_aff_string]
|
||||
# list_of_direct_results: [] if nothing was matched, otherwise list of result dicts
|
||||
# shortened_aff_string: original affiliation with matched parts stripped out
|
||||
```
|
||||
|
||||
The result dicts from direct mapping use `provenance = "affro_direct"`.
|
||||
|
||||
|
||||
- `helpers/create_input.py`, `helpers/matching.py`, contain the main algorithm (preprocessing and matching phase, respectively).
|
||||
## Stage 2 — `run_affro_(raw_aff_string)`
|
||||
|
||||
**File:** `core.py`
|
||||
|
||||
The core matching logic, applied after (or instead of) `direct_mapping`.
|
||||
|
||||
### Step 2.1 — normalization of `raw_aff_string`
|
||||
|
||||
**File:** `helpers/functions.py`
|
||||
|
||||
A lightweight, fast normalization pass that produces a single flat string key used for `dix_name` lookup. It does **not** segment the affiliation — that is left for the algorithm path.
|
||||
|
||||
**Main transformations applied (in order):**
|
||||
|
||||
| Step | Transformation |
|
||||
|---|---|
|
||||
| 1 | `unidecode` (remove accents / transliterate) |
|
||||
| 2 | `process_parentheses` (keep parens with univ/hospital; drop others) |
|
||||
| 3 | `replace_comma_spaces`, `replace_double_consonants`, `replace_underscore` |
|
||||
| 4 | Lowercase, `replace_roman_numerals`, `remove_stop_words` |
|
||||
| 5 | Remove non-alphanumeric except `,;/:.−` |
|
||||
| 6 | `remove_multi_digit_numbers` |
|
||||
| 7 | Replace `:`, `;`, `/`, `—` → `,` |
|
||||
| 8 | `normalize_organization_names` (stem `university` → `univer`, `institution` → `instit`, etc.) |
|
||||
|
||||
**Returns:** a single normalised string, e.g.:
|
||||
`"univer milan, italy"` for input `"University of Milan, Italy"`
|
||||
|
||||
### Step 2.2 — `dix_name` lookup
|
||||
|
||||
`dix_name` is a dictionary loaded from `jsons/dix_name.json.gz`.
|
||||
|
||||
**Structure:**
|
||||
```json
|
||||
{
|
||||
"instit information science techn": [
|
||||
{
|
||||
"id": "https://ror.org/05kacka20",
|
||||
"city": ["pisa"],
|
||||
"country": ["italy"],
|
||||
"label": "cnr",
|
||||
"first": "y"
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
- test.ipynb
|
||||
## Algorithm Path — `produce_result(input, simU, simG, limit)`
|
||||
|
||||
## Description of the algorithm
|
||||
Used when the fast path fails. Called with `simU=0.42`, `simG=0.82`, `limit=500`.
|
||||
|
||||
### Goal: Identify organizations inside a raw affiliation string and match the corresponding ROR ids.
|
||||
### `create_df_algorithm(raw_aff_string, radius_u)` — `helpers/create_input.py`
|
||||
|
||||
### Steps:
|
||||
Segments and enriches the affiliation string into a structured input representation.
|
||||
|
||||
- **Preprocessing phase:**
|
||||
1. Cleaning and stemming
|
||||
2. Keyword labeling and partitioning
|
||||
3. Partition pruning and string shortening
|
||||
- **Matching phase:**
|
||||
1. Candidate identification and refinement
|
||||
2. Results disambiguation
|
||||
**Steps:**
|
||||
|
||||
### Basic parameters:
|
||||
1. Threshold for university related organizations (default 0.42).
|
||||
2. Threshold for other organizations (default 0.82).
|
||||
3. Context window for trimming text around the term "university" (default 3)
|
||||
1. `clean_string()` — full normalisation (includes `insert_space_between_lower_and_upper`, `replace_newlines_with_space`, `replace_double_consonants`, etc.)
|
||||
2. `remove_outer_parentheses`, `remove_leading_numbers`
|
||||
3. `description(clean_aff)` → detects countries present in the string
|
||||
4. `substrings_dict(reduce(clean_aff))` — segments the affiliation on `,;/:|` and `-` and applies `normalize_organization_names` to each segment
|
||||
5. `replace_abbr_univ` — expands abbreviations like `"u Milan"` → `"univer Milan"`
|
||||
6. Merges protected terms (e.g. `"univer california"`) with adjacent city/country tokens
|
||||
7. Removes city-only or remove-list tokens
|
||||
8. `shorten_keywords([x], radius_u)` — further reduces keywords
|
||||
9. `valueToCategory(keyword)` — classifies each keyword (Academia, Hospitals, Specific, …)
|
||||
|
||||
**Returns:**
|
||||
```python
|
||||
[clean_aff, light_aff, aff_list, countries_list, keys_list]
|
||||
# clean_aff: normalised full string
|
||||
# light_aff: comma-joined list of segments
|
||||
# aff_list: list of {index, keywords, category} dicts
|
||||
# countries_list: detected country names
|
||||
# keys_list: special category keys found
|
||||
```
|
||||
|
||||
### `find_name(input, dix_name, simU, simG, limit)` — `helpers/find_name.py`
|
||||
|
||||
Matches each keyword segment against `dix_name` candidates, using similarity scoring.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. `get_candidates(countries_list, keys_list)` → restricts the search space by country and special category keys (intersection of `dix_country_legalnames` and `dix_key_legalnames`).
|
||||
2. For each keyword `s`:
|
||||
- If `s` is directly in `candidates` → exact "lucky" match (score = 1).
|
||||
- Otherwise → `find_candidate(s, ...)` applies cosine similarity / edit-distance scoring against candidates, bounded by `simU` (universities) or `simG` (others).
|
||||
3. `index_multiple_matchings(pairs)` detects keywords matched by >1 candidate.
|
||||
4. `best_sim_score(...)` resolves multi-matched keywords using the full clean/light affiliation string.
|
||||
5. `unique_subset(best0, best1)` de-duplicates.
|
||||
|
||||
**Returns:** `[[name, score], ...]`
|
||||
|
||||
### `find_id(aff_input, best_names, dix_name, simG)` — `helpers/find_id.py`
|
||||
|
||||
Resolves each matched name to a specific organization ID, disambiguating when a name maps to multiple organizations in different countries/cities.
|
||||
|
||||
**Disambiguation cascade (in order):**
|
||||
|
||||
| Step | Strategy |
|
||||
|---|---|
|
||||
| 1 | City **and** Country match |
|
||||
| 2 | Country direct match |
|
||||
| 3 | Special country synonyms (US states, UK variants,…) |
|
||||
| 4 | City match (city not embedded in org name) |
|
||||
| 5 | Country appears in affiliation |
|
||||
| 6 | Country appears in both affiliation and org name |
|
||||
| 7 | Specific/Acronym category → prefer `top_level`, then `parent` |
|
||||
| 8 | Fallback: `first == 'y'` for non-department, non-lab, non-low-prob-country orgs |
|
||||
|
||||
**Returns:** `[[name, score, id], ...]` (deduplicated, highest score per ID kept)
|
||||
|
||||
### `disamb(input, id_list_, dix_id)` — `helpers/disambiguation.py`
|
||||
|
||||
Final post-processing to resolve cases where multiple organizations were matched.
|
||||
|
||||
**Logic:**
|
||||
|
||||
| Condition | Action |
|
||||
|---|---|
|
||||
| Single result | Return as-is |
|
||||
| No country detected in affiliation | Keep same-country results |
|
||||
| More active results than detected countries | Filter by country (with special handling for country names like US, UK,...) |
|
||||
| Otherwise | Return all results |
|
||||
|
||||
**Returns:** Full result list (see Output Schema below).
|
||||
|
||||
---
|
||||
|
||||
## Output Schema
|
||||
|
||||
Each item in the returned list is a dictionary:
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `provenance` | `str` | `"affro"` (algorithm path) or `"affro_direct"` (direct mapping) |
|
||||
| `version` | `str` | Pipeline version (ex. `"3.3.0"`) |
|
||||
| `pid` | `str` | `"ror"` or `"openorgs"` |
|
||||
| `value` | `str` | The organization identifier (ROR ID or OpenOrgs ID) |
|
||||
| `name` | `str` | Official organization name |
|
||||
| `confidence` | `float` | Match confidence score (0–1) |
|
||||
| `status` | `str` | `"active"`, `"inactive"`, `"withdrawn"`, or `"merged"` |
|
||||
| `country` | `list[str]` | Country or countries associated with the organization |
|
||||
|
||||
**Example output:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"provenance": "affro",
|
||||
"version": "3.3.0",
|
||||
"pid": "ror",
|
||||
"value": "https://ror.org/019kf3481",
|
||||
"name": "OpenAIRE Non-Profit Civil Partnership",
|
||||
"confidence": 1,
|
||||
"status": "active",
|
||||
"country": ["greece"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
> [NOTE]
|
||||
> When an organization is inactive/withdrawn, affro also appends the active successor(s) from `dix_id[id]['status'][1]` as separate entries in the list.
|
||||
|
||||
---
|
||||
|
||||
## Data Dictionaries
|
||||
|
||||
### `dix_name` — `jsons/dix_name.json.gz`
|
||||
|
||||
Maps **normalised name keys** to a list of candidate organizations. Each candidate has:
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `id` | `str` | ROR URI or OpenOrgs ID |
|
||||
| `first` | `str` | `"y"` if this is the canonical/primary org for this key |
|
||||
| `label` | `str` \| `null` | Family label (e.g. `"fraunhofer"`, `"cnr"`) used by direct mapping |
|
||||
| `country` | `list[str]` | Country names |
|
||||
| `city` | `list[str]` | City names |
|
||||
|
||||
### `dix_id` — `jsons/dix_id.json.gz`
|
||||
|
||||
Maps **organization IDs** to metadata:
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `name` | `str` | Official name |
|
||||
| `country` | `list[str]` | Country |
|
||||
| `status` | `list` | `[primary_status, [successor_ids]]` |
|
||||
| `top_level` | `str` | `"y"` if the org has no parent |
|
||||
| `parent` | `str` | `"y"` if the org is a parent to others |
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Command-line
|
||||
|
||||
Run a quick test directly from the terminal (no script needed):
|
||||
|
||||
```bash
|
||||
python -c "from affro.core import run_affro; import json; print(json.dumps(run_affro('Department of Chemistry, ETH Zurich, Switzerland'), indent=2))"
|
||||
```
|
||||
|
||||
|
||||
## Contact
|
||||
### Expected behaviour by case
|
||||
|
||||
If you have any questions, comments, or issues, please feel free to contact me. You can reach me via email at [myrto.kallipoliti@gmail.com]. Feedback and contributions are also welcome.
|
||||
| Input | Fast path taken | Reason |
|
||||
|---|---|---|
|
||||
| `"University of Cambridge"` | `dix_name` exact match | `"univer cambridge"` found in `dix_name` |
|
||||
| `"Fraunhofer, Institute for Industrial Engineering, Stuttgart"` | Direct mapping | `"fraunhofer"` + `"instit industrial engineering"` triggers `direct_mapping` |
|
||||
| `"Dept. of Physics, Univ. of Auckland, NZ"` | Algorithm path | Lucky key not in `dix_name` |
|
||||
| Inactive ROR org | Fast path + successor | Status list contains successor ID → appended to result |
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Any exception inside `run_affro_` is caught, logged to stdout with the input string, and an empty list `[]` is returned.
|
||||
- An empty result list `[]` indicates no match was found or an error occurred.
|
||||
|
||||
---
|
||||
|
||||
## Module Dependencies
|
||||
|
||||
```
|
||||
core.py
|
||||
├── helpers/functions.py # string cleaning, dix_name/dix_id loading, regex, utils
|
||||
├── helpers/create_input.py # create_df_algorithm, valueToCategory, substrings_dict
|
||||
├── helpers/matching.py # find_candidate, get_candidates, best_sim_score, cosine similarity
|
||||
├── helpers/find_name.py # find_name
|
||||
├── helpers/find_id.py # find_id, disambiguation helpers
|
||||
├── helpers/disambiguation.py # disamb, convert_to_result
|
||||
└── helpers/direct_mapping.py # direct_mapping, _build_label_keys
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Metadata-Version: 2.4
|
||||
Name: affro
|
||||
Version: 2.2.2
|
||||
Version: 3.4.0
|
||||
Summary: A tool to resolve organization names to ROR or OpenOrgs IDs
|
||||
Home-page: https://code-repo.d4science.org/mkallipo/affRo
|
||||
Author: Myrto Kallipoliti
|
||||
|
|
@ -16,44 +16,321 @@ Dynamic: home-page
|
|||
Dynamic: license
|
||||
Dynamic: summary
|
||||
|
||||
# Affiliation-Matching Repository \[aka AffRo\]
|
||||
# `run_affro` — Affiliation Matching Pipeline Documentation
|
||||
|
||||
This repository contains code for matching affiliation strings to ROR or/and OpenOrg IDs.
|
||||
**Version:** 3.3.0
|
||||
**Module:** `core.py`
|
||||
**Entry point:** `run_affro(aff: str) → list[dict]`
|
||||
|
||||
🚀 As it is still a work in progress, *the repository may not always be up-to-date*.
|
||||
However, I will incorporate improvements and bug fixes regularly.
|
||||
---
|
||||
|
||||
## Main files
|
||||
## Overview
|
||||
|
||||
`run_affro` is the main entry point of the **AffRo** affiliation-matching pipeline. It takes a raw affiliation string (as it appears in a publication metadata record) and returns a list of matched organizations with their identifiers (ROR and/or OpenOrgs), confidence scores, status and location information.
|
||||
|
||||
```python
|
||||
from core import run_affro
|
||||
|
||||
results = run_affro("Department of Physics, University of Milan, Italy")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## High-Level Pipeline
|
||||
|
||||
```
|
||||
raw affiliation string
|
||||
│
|
||||
▼
|
||||
direct_mapping() ← fast rule-based lookup for known institute families
|
||||
│
|
||||
├─ match found ──► run_affro_(shortened_aff) + direct results
|
||||
│
|
||||
└─ no match ─────► run_affro_(raw_aff)
|
||||
│
|
||||
▼
|
||||
normalise & stem
|
||||
│
|
||||
dix_name lookup ← exact key lookup
|
||||
│
|
||||
┌─────────────────┼────────────────────┐
|
||||
│ │
|
||||
single match multiple matches
|
||||
│ │
|
||||
▼ ▼
|
||||
build_result_list() ◀---- filter by 'first'/'top_level'/'parent'
|
||||
(algorithm path)
|
||||
│
|
||||
still ambiguous?
|
||||
│
|
||||
▼
|
||||
(algorithm fallback)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stage 1 — `direct_mapping(aff)`
|
||||
|
||||
**File:** `helpers/direct_mapping.py`
|
||||
|
||||
A fast, rule-based pre-processor that recognises affiliation strings belonging to specific **institute families** (Fraunhofer, CNR, Max Planck, Helmholtz, Leibniz, FORTH, Demokritos, IRCCS, …).
|
||||
|
||||
### What it does
|
||||
|
||||
1. Produce a normalized, stemmed version of the raw string.
|
||||
2. Checks for the presence of family-specific keywords (e.g. `fraunhofer`, `cnr`, `max planck`).
|
||||
3. For each recognized family, iterates over pre-built sorted key lists (longest keys first for specificity) and checks whether the key appears close enough to the trigger word using a character-distance heuristic:
|
||||
```
|
||||
distance(aff, trigger_word, key) < len(key) + len(trigger_word) + threshold
|
||||
```
|
||||
4. When a key matches, appends its ROR/OpenOrgs ID to `assigned` and removes the matched substring from the affiliation string (producing `shorten_aff`).
|
||||
|
||||
### Returns
|
||||
|
||||
```python
|
||||
[list_of_direct_results, shortened_aff_string]
|
||||
# list_of_direct_results: [] if nothing was matched, otherwise list of result dicts
|
||||
# shortened_aff_string: original affiliation with matched parts stripped out
|
||||
```
|
||||
|
||||
The result dicts from direct mapping use `provenance = "affro_direct"`.
|
||||
|
||||
|
||||
- `helpers/create_input.py`, `helpers/matching.py`, contain the main algorithm (preprocessing and matching phase, respectively).
|
||||
## Stage 2 — `run_affro_(raw_aff_string)`
|
||||
|
||||
**File:** `core.py`
|
||||
|
||||
The core matching logic, applied after (or instead of) `direct_mapping`.
|
||||
|
||||
### Step 2.1 — normalization of `raw_aff_string`
|
||||
|
||||
**File:** `helpers/functions.py`
|
||||
|
||||
A lightweight, fast normalization pass that produces a single flat string key used for `dix_name` lookup. It does **not** segment the affiliation — that is left for the algorithm path.
|
||||
|
||||
**Main transformations applied (in order):**
|
||||
|
||||
| Step | Transformation |
|
||||
|---|---|
|
||||
| 1 | `unidecode` (remove accents / transliterate) |
|
||||
| 2 | `process_parentheses` (keep parens with univ/hospital; drop others) |
|
||||
| 3 | `replace_comma_spaces`, `replace_double_consonants`, `replace_underscore` |
|
||||
| 4 | Lowercase, `replace_roman_numerals`, `remove_stop_words` |
|
||||
| 5 | Remove non-alphanumeric except `,;/:.−` |
|
||||
| 6 | `remove_multi_digit_numbers` |
|
||||
| 7 | Replace `:`, `;`, `/`, `—` → `,` |
|
||||
| 8 | `normalize_organization_names` (stem `university` → `univer`, `institution` → `instit`, etc.) |
|
||||
|
||||
**Returns:** a single normalised string, e.g.:
|
||||
`"univer milan, italy"` for input `"University of Milan, Italy"`
|
||||
|
||||
### Step 2.2 — `dix_name` lookup
|
||||
|
||||
`dix_name` is a dictionary loaded from `jsons/dix_name.json.gz`.
|
||||
|
||||
**Structure:**
|
||||
```json
|
||||
{
|
||||
"instit information science techn": [
|
||||
{
|
||||
"id": "https://ror.org/05kacka20",
|
||||
"city": ["pisa"],
|
||||
"country": ["italy"],
|
||||
"label": "cnr",
|
||||
"first": "y"
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
- test.ipynb
|
||||
## Algorithm Path — `produce_result(input, simU, simG, limit)`
|
||||
|
||||
## Description of the algorithm
|
||||
Used when the fast path fails. Called with `simU=0.42`, `simG=0.82`, `limit=500`.
|
||||
|
||||
### Goal: Identify organizations inside a raw affiliation string and match the corresponding ROR ids.
|
||||
### `create_df_algorithm(raw_aff_string, radius_u)` — `helpers/create_input.py`
|
||||
|
||||
### Steps:
|
||||
Segments and enriches the affiliation string into a structured input representation.
|
||||
|
||||
- **Preprocessing phase:**
|
||||
1. Cleaning and stemming
|
||||
2. Keyword labeling and partitioning
|
||||
3. Partition pruning and string shortening
|
||||
- **Matching phase:**
|
||||
1. Candidate identification and refinement
|
||||
2. Results disambiguation
|
||||
**Steps:**
|
||||
|
||||
### Basic parameters:
|
||||
1. Threshold for university related organizations (default 0.42).
|
||||
2. Threshold for other organizations (default 0.82).
|
||||
3. Context window for trimming text around the term "university" (default 3)
|
||||
1. `clean_string()` — full normalisation (includes `insert_space_between_lower_and_upper`, `replace_newlines_with_space`, `replace_double_consonants`, etc.)
|
||||
2. `remove_outer_parentheses`, `remove_leading_numbers`
|
||||
3. `description(clean_aff)` → detects countries present in the string
|
||||
4. `substrings_dict(reduce(clean_aff))` — segments the affiliation on `,;/:|` and `-` and applies `normalize_organization_names` to each segment
|
||||
5. `replace_abbr_univ` — expands abbreviations like `"u Milan"` → `"univer Milan"`
|
||||
6. Merges protected terms (e.g. `"univer california"`) with adjacent city/country tokens
|
||||
7. Removes city-only or remove-list tokens
|
||||
8. `shorten_keywords([x], radius_u)` — further reduces keywords
|
||||
9. `valueToCategory(keyword)` — classifies each keyword (Academia, Hospitals, Specific, …)
|
||||
|
||||
**Returns:**
|
||||
```python
|
||||
[clean_aff, light_aff, aff_list, countries_list, keys_list]
|
||||
# clean_aff: normalised full string
|
||||
# light_aff: comma-joined list of segments
|
||||
# aff_list: list of {index, keywords, category} dicts
|
||||
# countries_list: detected country names
|
||||
# keys_list: special category keys found
|
||||
```
|
||||
|
||||
### `find_name(input, dix_name, simU, simG, limit)` — `helpers/find_name.py`
|
||||
|
||||
Matches each keyword segment against `dix_name` candidates, using similarity scoring.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. `get_candidates(countries_list, keys_list)` → restricts the search space by country and special category keys (intersection of `dix_country_legalnames` and `dix_key_legalnames`).
|
||||
2. For each keyword `s`:
|
||||
- If `s` is directly in `candidates` → exact "lucky" match (score = 1).
|
||||
- Otherwise → `find_candidate(s, ...)` applies cosine similarity / edit-distance scoring against candidates, bounded by `simU` (universities) or `simG` (others).
|
||||
3. `index_multiple_matchings(pairs)` detects keywords matched by >1 candidate.
|
||||
4. `best_sim_score(...)` resolves multi-matched keywords using the full clean/light affiliation string.
|
||||
5. `unique_subset(best0, best1)` de-duplicates.
|
||||
|
||||
**Returns:** `[[name, score], ...]`
|
||||
|
||||
### `find_id(aff_input, best_names, dix_name, simG)` — `helpers/find_id.py`
|
||||
|
||||
Resolves each matched name to a specific organization ID, disambiguating when a name maps to multiple organizations in different countries/cities.
|
||||
|
||||
**Disambiguation cascade (in order):**
|
||||
|
||||
| Step | Strategy |
|
||||
|---|---|
|
||||
| 1 | City **and** Country match |
|
||||
| 2 | Country direct match |
|
||||
| 3 | Special country synonyms (US states, UK variants,…) |
|
||||
| 4 | City match (city not embedded in org name) |
|
||||
| 5 | Country appears in affiliation |
|
||||
| 6 | Country appears in both affiliation and org name |
|
||||
| 7 | Specific/Acronym category → prefer `top_level`, then `parent` |
|
||||
| 8 | Fallback: `first == 'y'` for non-department, non-lab, non-low-prob-country orgs |
|
||||
|
||||
**Returns:** `[[name, score, id], ...]` (deduplicated, highest score per ID kept)
|
||||
|
||||
### `disamb(input, id_list_, dix_id)` — `helpers/disambiguation.py`
|
||||
|
||||
Final post-processing to resolve cases where multiple organizations were matched.
|
||||
|
||||
**Logic:**
|
||||
|
||||
| Condition | Action |
|
||||
|---|---|
|
||||
| Single result | Return as-is |
|
||||
| No country detected in affiliation | Keep same-country results |
|
||||
| More active results than detected countries | Filter by country (with special handling for country names like US, UK,...) |
|
||||
| Otherwise | Return all results |
|
||||
|
||||
**Returns:** Full result list (see Output Schema below).
|
||||
|
||||
---
|
||||
|
||||
## Output Schema
|
||||
|
||||
Each item in the returned list is a dictionary:
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `provenance` | `str` | `"affro"` (algorithm path) or `"affro_direct"` (direct mapping) |
|
||||
| `version` | `str` | Pipeline version (ex. `"3.3.0"`) |
|
||||
| `pid` | `str` | `"ror"` or `"openorgs"` |
|
||||
| `value` | `str` | The organization identifier (ROR ID or OpenOrgs ID) |
|
||||
| `name` | `str` | Official organization name |
|
||||
| `confidence` | `float` | Match confidence score (0–1) |
|
||||
| `status` | `str` | `"active"`, `"inactive"`, `"withdrawn"`, or `"merged"` |
|
||||
| `country` | `list[str]` | Country or countries associated with the organization |
|
||||
|
||||
**Example output:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"provenance": "affro",
|
||||
"version": "3.3.0",
|
||||
"pid": "ror",
|
||||
"value": "https://ror.org/019kf3481",
|
||||
"name": "OpenAIRE Non-Profit Civil Partnership",
|
||||
"confidence": 1,
|
||||
"status": "active",
|
||||
"country": ["greece"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
> [NOTE]
|
||||
> When an organization is inactive/withdrawn, affro also appends the active successor(s) from `dix_id[id]['status'][1]` as separate entries in the list.
|
||||
|
||||
---
|
||||
|
||||
## Data Dictionaries
|
||||
|
||||
### `dix_name` — `jsons/dix_name.json.gz`
|
||||
|
||||
Maps **normalised name keys** to a list of candidate organizations. Each candidate has:
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `id` | `str` | ROR URI or OpenOrgs ID |
|
||||
| `first` | `str` | `"y"` if this is the canonical/primary org for this key |
|
||||
| `label` | `str` \| `null` | Family label (e.g. `"fraunhofer"`, `"cnr"`) used by direct mapping |
|
||||
| `country` | `list[str]` | Country names |
|
||||
| `city` | `list[str]` | City names |
|
||||
|
||||
### `dix_id` — `jsons/dix_id.json.gz`
|
||||
|
||||
Maps **organization IDs** to metadata:
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `name` | `str` | Official name |
|
||||
| `country` | `list[str]` | Country |
|
||||
| `status` | `list` | `[primary_status, [successor_ids]]` |
|
||||
| `top_level` | `str` | `"y"` if the org has no parent |
|
||||
| `parent` | `str` | `"y"` if the org is a parent to others |
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Command-line
|
||||
|
||||
Run a quick test directly from the terminal (no script needed):
|
||||
|
||||
```bash
|
||||
python -c "from affro.core import run_affro; import json; print(json.dumps(run_affro('Department of Chemistry, ETH Zurich, Switzerland'), indent=2))"
|
||||
```
|
||||
|
||||
|
||||
## Contact
|
||||
### Expected behaviour by case
|
||||
|
||||
If you have any questions, comments, or issues, please feel free to contact me. You can reach me via email at [myrto.kallipoliti@gmail.com]. Feedback and contributions are also welcome.
|
||||
| Input | Fast path taken | Reason |
|
||||
|---|---|---|
|
||||
| `"University of Cambridge"` | `dix_name` exact match | `"univer cambridge"` found in `dix_name` |
|
||||
| `"Fraunhofer, Institute for Industrial Engineering, Stuttgart"` | Direct mapping | `"fraunhofer"` + `"instit industrial engineering"` triggers `direct_mapping` |
|
||||
| `"Dept. of Physics, Univ. of Auckland, NZ"` | Algorithm path | Lucky key not in `dix_name` |
|
||||
| Inactive ROR org | Fast path + successor | Status list contains successor ID → appended to result |
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Any exception inside `run_affro_` is caught, logged to stdout with the input string, and an empty list `[]` is returned.
|
||||
- An empty result list `[]` indicates no match was found or an error occurred.
|
||||
|
||||
---
|
||||
|
||||
## Module Dependencies
|
||||
|
||||
```
|
||||
core.py
|
||||
├── helpers/functions.py # string cleaning, dix_name/dix_id loading, regex, utils
|
||||
├── helpers/create_input.py # create_df_algorithm, valueToCategory, substrings_dict
|
||||
├── helpers/matching.py # find_candidate, get_candidates, best_sim_score, cosine similarity
|
||||
├── helpers/find_name.py # find_name
|
||||
├── helpers/find_id.py # find_id, disambiguation helpers
|
||||
├── helpers/disambiguation.py # disamb, convert_to_result
|
||||
└── helpers/direct_mapping.py # direct_mapping, _build_label_keys
|
||||
```
|
||||
|
|
|
|||
|
|
@ -10,18 +10,20 @@ affro.egg-info/dependency_links.txt
|
|||
affro.egg-info/top_level.txt
|
||||
affro/helpers/__init__.py
|
||||
affro/helpers/create_input.py
|
||||
affro/helpers/direct_mapping.py
|
||||
affro/helpers/disambiguation.py
|
||||
affro/helpers/find_id.py
|
||||
affro/helpers/find_name.py
|
||||
affro/helpers/functions.py
|
||||
affro/helpers/matching.py
|
||||
affro/jsons/dix_categ.json
|
||||
affro/jsons/dix_city.json
|
||||
affro/jsons/dix_country.json
|
||||
affro/jsons/dix_country_legalnames.json
|
||||
affro/jsons/dix_id_country.json
|
||||
affro/jsons/dix_id_name.json
|
||||
affro/jsons/dix_mult.json
|
||||
affro/jsons/dix_org.json
|
||||
affro/jsons/dix_status.json
|
||||
affro/jsons/replacements.json
|
||||
affro/jsons/dashed_names.json.gz
|
||||
affro/jsons/dix_categ.json.gz
|
||||
affro/jsons/dix_country_legalnames.json.gz
|
||||
affro/jsons/dix_id.json.gz
|
||||
affro/jsons/dix_keys_names.json.gz
|
||||
affro/jsons/dix_name.json.gz
|
||||
affro/jsons/dix_name_id.json.gz
|
||||
affro/jsons/replacements.json.gz
|
||||
affro/txts/city_names.txt
|
||||
affro/txts/country_names.txt
|
||||
affro/txts/remove_list.txt
|
||||
|
|
|
|||
|
|
@ -1 +1,5 @@
|
|||
__version__ = "2.2.2"
|
||||
__version__ = "3.4.0"
|
||||
|
||||
from .core import run_affro, matchings_affro
|
||||
|
||||
__all__ = ["__version__", "run_affro", "matchings_affro"]
|
||||
|
|
|
|||
462
affro/core.py
462
affro/core.py
|
|
@ -1,164 +1,350 @@
|
|||
import sys
|
||||
from functools import lru_cache
|
||||
##import functions
|
||||
from affro.helpers.functions import *
|
||||
from affro.helpers.matching import *
|
||||
from affro.helpers.create_input import *
|
||||
import json
|
||||
from affro.helpers.find_name import *
|
||||
from affro.helpers.find_id import *
|
||||
from affro.helpers.disambiguation import *
|
||||
from affro.helpers.direct_mapping import *
|
||||
|
||||
from . import __version__
|
||||
|
||||
VERSION = __version__
|
||||
|
||||
dix_org = load_json('jsons/dix_org.json')
|
||||
dix_mult = load_json('jsons/dix_mult.json')
|
||||
dix_city = load_json('jsons/dix_city.json')
|
||||
dix_country = load_json('jsons/dix_country.json')
|
||||
dix_status = load_json('jsons/dix_status.json')
|
||||
dix_id_name = load_json('jsons/dix_id_name.json')
|
||||
dix_id_country = load_json('jsons/dix_id_country.json')
|
||||
dix_id_name = load_json('jsons/dix_id_name.json')
|
||||
|
||||
dix_id = load_json('jsons/dix_id.json.gz')
|
||||
# dix_name is already loaded in functions.py and imported via `from affro.helpers.functions import *`
|
||||
|
||||
|
||||
dix_status_new = {k :[dix_status[k][0], dix_status[k][1].split(', ')] for k in dix_status}
|
||||
us_states = [
|
||||
"alabama", "alaska", "arizona", "arkansas", "california",
|
||||
"colorado", "conecticut", "delaware", "florida", "georgia",
|
||||
"hawaii", "idaho", "ilinois", "indiana", "iowa",
|
||||
"kansas", "kentucky", "louisiana", "maine", "maryland",
|
||||
"masachusets", "michigan", "minesota", "misisipi", "misouri",
|
||||
"montana", "nebraska", "nevada", "new hampshire", "new jersey",
|
||||
"new mexico", "new york", "north carolina", "north dakota", "ohio",
|
||||
"oklahoma", "oregon", "pensylvania", "rhode island", "south carolina",
|
||||
"south dakota", "tenesee", "texas", "utah", "vermont",
|
||||
"virginia", "washington", "west virginia", "wisconsin", "wyoming"
|
||||
]
|
||||
def _country_set(result):
|
||||
countries = result.get('country')
|
||||
if not countries and result.get('value') in dix_id:
|
||||
countries = dix_id[result['value']].get('country')
|
||||
if not isinstance(countries, (list, tuple, set)):
|
||||
if isinstance(countries, str) and countries.strip():
|
||||
return {countries.strip().casefold()}
|
||||
return set()
|
||||
return {
|
||||
country.strip().casefold()
|
||||
for country in countries
|
||||
if isinstance(country, str) and country.strip()
|
||||
}
|
||||
|
||||
|
||||
def _is_country_in_string(country, aff_string):
|
||||
if not aff_string or not country:
|
||||
return False
|
||||
c_lower = country.strip().casefold()
|
||||
aff_lower = aff_string.casefold()
|
||||
clean_aff = clean_string(aff_string).casefold()
|
||||
|
||||
def contains_us_state(text):
|
||||
text = text.lower()
|
||||
return any(state in text for state in us_states)
|
||||
detected_countries = set(description(clean_aff)[1])
|
||||
terms = {c_lower}
|
||||
if c_lower in country_synonyms:
|
||||
terms.update(s.casefold() for s in country_synonyms[c_lower])
|
||||
if c_lower in country_to_group:
|
||||
terms.update(s.casefold() for s in country_to_group[c_lower])
|
||||
if c_lower in {'rusia', 'russia'}:
|
||||
terms.update({'rusia', 'russia'})
|
||||
|
||||
def find_ror(input, simU, simG, limit):
|
||||
light_aff = input[0]
|
||||
result = Aff_Ids(input, dix_org, dix_mult, dix_city, dix_country, simU, simG, limit)
|
||||
results_upd = []
|
||||
|
||||
for r in result:
|
||||
|
||||
if "openorgs" in r[2]:
|
||||
results_upd.append([r[1], 'openorgs', r[2], 'active', dix_id_country[r[2]]])
|
||||
|
||||
if not terms.isdisjoint(detected_countries):
|
||||
return True
|
||||
|
||||
for term in terms:
|
||||
if len(term) <= 3:
|
||||
pattern = r'\b' + re.escape(term) + r'\b'
|
||||
if re.search(pattern, aff_lower) or re.search(pattern, clean_aff):
|
||||
return True
|
||||
else:
|
||||
if dix_status_new[r[2]][0] == 'active':
|
||||
results_upd.append([r[1], 'ror', r[2], 'active', dix_id_country[r[2]]])
|
||||
else:
|
||||
if dix_status_new[r[2]][1][0] == '':
|
||||
results_upd.append([r[1], 'ror', r[2], dix_status_new[r[2]][0], dix_id_country[r[2]]])
|
||||
|
||||
|
||||
else:
|
||||
results_upd.append([r[1], 'ror', r[2], dix_status_new[r[2]][0],dix_id_country[r[2]]])
|
||||
for link in (dix_status_new[r[2]][1]):
|
||||
results_upd.append([r[1], 'ror', link, 'active',dix_id_country[r[2]],dix_id_country[link]])
|
||||
|
||||
if len(results_upd) > len(set(description(light_aff)[1])):
|
||||
if term in aff_lower or term in clean_aff:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def discard_country_mismatched_institutes(results, aff=None):
|
||||
"""
|
||||
Filter out non-direct institute and department matches that are in a different country
|
||||
from any university matched in the same affiliation string, unless the candidate's
|
||||
country is present in the affiliation string.
|
||||
"""
|
||||
if not results:
|
||||
return results
|
||||
|
||||
universities = [
|
||||
result
|
||||
for result in results
|
||||
if 'univ' in str(result.get('name', '')).casefold()
|
||||
]
|
||||
if not universities:
|
||||
return results
|
||||
|
||||
# Known country sets for matched universities
|
||||
univ_countries_list = [
|
||||
_country_set(u)
|
||||
for u in universities
|
||||
]
|
||||
known_univ_countries = [c for c in univ_countries_list if c]
|
||||
if not known_univ_countries:
|
||||
return results
|
||||
|
||||
filtered = []
|
||||
for result in results:
|
||||
# Never filter out the university itself or direct matches
|
||||
if result in universities or result.get('provenance') in ('affro_direct', 'dictionaries'):
|
||||
filtered.append(result)
|
||||
continue
|
||||
|
||||
# name = str(result.get('name', '')).casefold()
|
||||
# if not any(k in name for k in ('inst', 'depart', 'dipart', 'center')):
|
||||
# filtered.append(result)
|
||||
# continue
|
||||
|
||||
candidate_countries = _country_set(result)
|
||||
# Keep if country is unknown or shares at least one country with a university
|
||||
if not candidate_countries or any(
|
||||
not candidate_countries.isdisjoint(uc) for uc in known_univ_countries
|
||||
):
|
||||
filtered.append(result)
|
||||
continue
|
||||
|
||||
# Candidate country does not match university:
|
||||
# Discard only if the country is not present in the string
|
||||
if aff and any(_is_country_in_string(c, aff) for c in candidate_countries):
|
||||
filtered.append(result)
|
||||
continue
|
||||
|
||||
# Mismatched and country not present in string -> discarded
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
_discard_country_mismatched_institutes = discard_country_mismatched_institutes
|
||||
discard_city_mismatched_institutes = discard_country_mismatched_institutes
|
||||
_discard_city_mismatched_institutes = discard_country_mismatched_institutes
|
||||
|
||||
|
||||
|
||||
def produce_result(input, simU, simG, limit):
|
||||
best_name = find_name(input, dix_name, simU, simG, limit)
|
||||
id_result = find_id(input, best_name, dix_name, simG)
|
||||
result = disamb(input, id_result, dix_id)
|
||||
|
||||
return result
|
||||
|
||||
def _make_entry(pid, value, name, confidence, status, country):
|
||||
"""Build a single result dictionary."""
|
||||
return {
|
||||
'provenance': 'affro',
|
||||
'version': VERSION,
|
||||
'pid': pid,
|
||||
'value': value,
|
||||
'name': name,
|
||||
'confidence': confidence,
|
||||
'status': status,
|
||||
'country': country,
|
||||
}
|
||||
|
||||
|
||||
def build_result_list(id_, name_, country_, status_):
|
||||
"""
|
||||
Helper function to build the result list, reducing code duplication.
|
||||
"""
|
||||
pid = 'openorgs' if 'openorgs' in id_ else 'ror'
|
||||
|
||||
# For openorgs or active ROR entries, return a single active entry
|
||||
if 'openorgs' in id_ or status_[0] == 'active':
|
||||
return [_make_entry(pid, id_, name_, 1, 'active', country_)]
|
||||
|
||||
# Build primary entry with its current status
|
||||
res = [_make_entry(pid, id_, name_, 1, status_[0], country_)]
|
||||
|
||||
# Append active successors (if any)
|
||||
if len(status_) > 1:
|
||||
for successor in status_[1]:
|
||||
if successor != '':
|
||||
res.append(_make_entry(
|
||||
'ror', successor,
|
||||
dix_id[successor]['name'], 1,
|
||||
'active', dix_id[successor]['country'],
|
||||
))
|
||||
return res
|
||||
|
||||
|
||||
def _apply_secondary_conditions(ids):
|
||||
"""
|
||||
Apply progressive filtering conditions to find best candidate.
|
||||
Optimized version: Pre-fetches dictionary entries to avoid repeated O(N) access.
|
||||
"""
|
||||
# Separate openorgs immediately (they bypass all ror logic)
|
||||
openorgs = [k for k in ids if 'openorgs' in k]
|
||||
|
||||
# Pre-fetch active rors once to avoid heavy dictionary spam
|
||||
active_rors = [k for k in ids if 'ror' in k and dix_id[k]['status'][0] == 'active']
|
||||
|
||||
if openorgs:
|
||||
active_top = [k for k in active_rors if dix_id[k]['top_level'][0] == 'y']
|
||||
return openorgs + active_top
|
||||
|
||||
if not active_rors:
|
||||
return []
|
||||
|
||||
# Condition 1: Check Top level
|
||||
top_level = [k for k in active_rors if dix_id[k]['top_level'][0] == 'y']
|
||||
if top_level:
|
||||
return top_level
|
||||
|
||||
# Condition 2: Check Parents
|
||||
parents = [k for k in active_rors if dix_id[k]['parent'][0] == 'y']
|
||||
if parents:
|
||||
return parents
|
||||
|
||||
# Condition 3: Return any active rors
|
||||
return active_rors
|
||||
|
||||
|
||||
|
||||
final_matching = []
|
||||
light_aff_tokens = [clean_string_ror(x) for x in set(light_aff.split())]
|
||||
for id_ in results_upd:
|
||||
country = dix_id_country[id_[2]]
|
||||
if country == 'united states':
|
||||
if 'united states' in light_aff or 'usa' in light_aff_tokens or contains_us_state(light_aff):
|
||||
final_matching.append(id_)
|
||||
|
||||
elif country == 'united kingdom':
|
||||
if 'united kingdom' in light_aff or 'uk' in light_aff_tokens:
|
||||
final_matching.append(id_)
|
||||
|
||||
elif 'korea' in country:
|
||||
|
||||
if 'korea' in light_aff_tokens:
|
||||
final_matching.append(id_)
|
||||
|
||||
elif country in light_aff:
|
||||
final_matching.append(id_)
|
||||
def run_affro_(raw_aff_string):
|
||||
"""
|
||||
Optimized version of run_affro with better performance.
|
||||
|
||||
|
||||
if len(final_matching)>0:
|
||||
result_dict = [{'provenance': 'affro', 'version': VERSION, 'pid':'openorgs', 'value':x[2], 'name': dix_id_name[x[2]], 'confidence':x[0], 'status':x[3], 'country':dix_id_country[x[2]]} if "openorgs" in x[2] else {'provenance': 'affro', 'version': VERSION,'pid':'ror', 'value':x[2], 'name': dix_id_name[x[2]], 'confidence':x[0], 'status':x[3], 'country':dix_id_country[x[2]]} for x in final_matching]
|
||||
return result_dict
|
||||
else:
|
||||
|
||||
return [{'provenance': 'affro', 'version': VERSION, 'pid':'openorgs', 'value':x[2], 'name': dix_id_name[x[2]],'confidence':x[0], 'status':x[3], 'country':dix_id_country[x[2]]} if "openorgs" in x[2] else {'provenance': 'affro', 'version': VERSION, 'pid':'ror', 'value':x[2], 'name': dix_id_name[x[2]], 'confidence':x[0], 'status':x[3], 'country':dix_id_country[x[2]]} for x in results_upd]
|
||||
|
||||
elif len(results_upd)>0:
|
||||
return [{'provenance': 'affro', 'version': VERSION, 'pid':'openorgs', 'value':x[2], 'name': dix_id_name[x[2]], 'confidence':x[0], 'status':x[3], 'country':dix_id_country[x[2]]} if "openorgs" in x[2] else {'provenance': 'affro', 'version': VERSION, 'pid':'ror', 'value':x[2], 'name': dix_id_name[x[2]], 'confidence':x[0], 'status':x[3], 'country':dix_id_country[x[2]]} for x in results_upd]
|
||||
else:
|
||||
result_dict = []
|
||||
|
||||
return result_dict
|
||||
|
||||
Optimizations:
|
||||
- Cache lucky_guess and dix_name lookups to avoid redundant access
|
||||
- Reuse candidate lists instead of recalculating
|
||||
- Streamline conditional logic
|
||||
- Improve final loop efficiency
|
||||
"""
|
||||
|
||||
lucky_guess = clean_string_lucky(raw_aff_string)
|
||||
|
||||
def run_affro(raw_aff_string):
|
||||
lucky_guess = clean_string_ror(raw_aff_string)
|
||||
try:
|
||||
if lucky_guess in dix_org:
|
||||
if dix_mult[lucky_guess] == "unique":
|
||||
if 'openorgs' in dix_org[lucky_guess]:
|
||||
|
||||
return[{'provenance': 'affro', 'version': VERSION, 'pid': 'openorgs', 'value': dix_org[lucky_guess], 'name': dix_id_name[ dix_org[lucky_guess]], 'confidence': 1, 'status': 'active', 'country':dix_id_country[dix_org[lucky_guess]]}]
|
||||
else:
|
||||
if dix_status_new[dix_org[lucky_guess]][0] == 'active':
|
||||
return [{'provenance': 'affro', 'version': VERSION, 'pid': 'ror', 'value': dix_org[lucky_guess], 'name': dix_id_name[ dix_org[lucky_guess]], 'confidence': 1, 'status': 'active', 'country':dix_id_country[dix_org[lucky_guess]]}]
|
||||
elif dix_status_new[dix_org[lucky_guess]][1][0]== '':
|
||||
return [{'provenance': 'affro', 'version': VERSION, 'pid': 'ror', 'value': dix_org[lucky_guess], 'name': dix_id_name[ dix_org[lucky_guess]], 'confidence': 1, 'status': dix_status_new[dix_org[lucky_guess]][0], 'country':dix_id_country[dix_org[lucky_guess]]}]
|
||||
else:
|
||||
res = [{'provenance': 'affro', 'version': VERSION, 'pid' : 'ror', 'value': dix_org[lucky_guess], 'name': dix_id_name[ dix_org[lucky_guess]], 'confidence': 1, 'status': dix_status_new[dix_org[lucky_guess]][0], 'country':dix_id_country[dix_org[lucky_guess]]}]
|
||||
for successor in dix_status_new[dix_org[lucky_guess]][1]:
|
||||
res.append({'provenance': 'affro', 'version': VERSION, 'pid' : 'ror', 'value': successor, 'name': dix_id_name[ dix_org[lucky_guess]], 'confidence': 1, 'status': 'active', 'country':dix_id_country[dix_org[lucky_guess]]})
|
||||
return res
|
||||
else:
|
||||
cand_ids = [
|
||||
key
|
||||
for _, key in dix_city[lucky_guess]
|
||||
if ("ror" in key and dix_status_new[key][0] == "active") or ("openorgs" in key)
|
||||
]
|
||||
num_countries = len(
|
||||
set(
|
||||
dix_id_country[x[1]]
|
||||
for x in dix_city[lucky_guess]
|
||||
if ("ror" in x[1] and dix_status_new[x[1]][0] == "active") or ("openorgs" in x[1])
|
||||
)
|
||||
)
|
||||
|
||||
if len(cand_ids) == 1 or num_countries == 1:
|
||||
if 'openorgs' in dix_org[lucky_guess]:
|
||||
return [{'provenance': 'affro', 'version': VERSION, 'pid': 'openorgs', 'value': dix_org[lucky_guess], 'name': dix_id_name[ dix_org[lucky_guess]], 'confidence': 1, 'status': 'active', 'country':dix_id_country[dix_org[lucky_guess]]}]
|
||||
else:
|
||||
return [{'provenance': 'affro', 'version': VERSION, 'pid': 'ror', 'value': dix_org[lucky_guess], 'name': dix_id_name[ dix_org[lucky_guess]], 'confidence': 1, 'status': 'active', 'country':dix_id_country[dix_org[lucky_guess]]}]
|
||||
|
||||
else:
|
||||
return []
|
||||
else:
|
||||
# print('No lucky guess, running algorithm...')
|
||||
result = find_ror(create_df_algorithm(raw_aff_string, 3), 0.42, 0.82, 500)
|
||||
|
||||
# Check if lucky_guess exists in dix_name
|
||||
if lucky_guess not in dix_name:
|
||||
# Lucky guess miss: use algorithm fallback
|
||||
result = produce_result(create_df_algorithm(raw_aff_string, 3), 0.42, 0.82, 500)
|
||||
return result
|
||||
|
||||
# Cache the dix_name entry to avoid repeated lookups
|
||||
# print('lucky')
|
||||
lucky_match = dix_name[lucky_guess]
|
||||
|
||||
# Single exact match found
|
||||
if len(lucky_match) == 1:
|
||||
triplet = lucky_match[0]
|
||||
id_ = triplet['id']
|
||||
name_ = dix_id[id_]['name']
|
||||
country_ = dix_id[id_]['country']
|
||||
status_ = dix_id[id_]['status']
|
||||
return build_result_list(id_, name_, country_, status_)
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Multiple candidates: filter natively
|
||||
# -------------------------------------------------------------
|
||||
ids = [x['id'] for x in lucky_match]
|
||||
|
||||
# Local helper to avoid repeating the dictionary lookups and return logic
|
||||
def get_result(best_id):
|
||||
return build_result_list(
|
||||
best_id,
|
||||
dix_id[best_id]['name'],
|
||||
dix_id[best_id]['country'],
|
||||
dix_id[best_id]['status']
|
||||
)
|
||||
|
||||
# 1. Country Prioritization: If country is mentioned in lucky_guess, prioritize those candidates
|
||||
country_match_ids = [
|
||||
i for i in ids
|
||||
if any(c and str(c) in lucky_guess for c in dix_id[i].get('country', []))
|
||||
]
|
||||
|
||||
if len(country_match_ids) == 1:
|
||||
return get_result(country_match_ids[0])
|
||||
|
||||
# If there are multiple country matches, or NO country matches, restrict further checks
|
||||
working_ids = country_match_ids if country_match_ids else ids
|
||||
|
||||
# 1.5 Exact Name Prioritization: If raw_aff_string matches the institution name exactly (case-insensitive)
|
||||
name_match_ids = [
|
||||
i for i in working_ids
|
||||
if raw_aff_string.strip().lower() == dix_id[i].get('name', '').strip().lower()
|
||||
]
|
||||
|
||||
if len(name_match_ids) == 1:
|
||||
return get_result(name_match_ids[0])
|
||||
|
||||
working_ids = name_match_ids if name_match_ids else working_ids
|
||||
|
||||
# 2. Filter for 'first' == 'y' among the working_ids
|
||||
first_y_ids = [x['id'] for x in lucky_match if x.get('first') == 'y' and x['id'] in working_ids]
|
||||
|
||||
if len(first_y_ids) == 1:
|
||||
return get_result(first_y_ids[0])
|
||||
|
||||
# 3. Apply secondary conditions
|
||||
candidates_to_filter = first_y_ids if len(first_y_ids) > 1 else working_ids
|
||||
cand_ids = _apply_secondary_conditions(candidates_to_filter)
|
||||
|
||||
# 4. If no candidates remain, fall back to algorithm
|
||||
if not cand_ids:
|
||||
return produce_result(create_df_algorithm(raw_aff_string, 10), 0.42, 0.82, 500)
|
||||
|
||||
# 5. If EXACTLY one remains after secondary conditions, return it
|
||||
if len(cand_ids) == 1:
|
||||
return get_result(cand_ids[0])
|
||||
|
||||
# 6. Multiple candidates STILL remain after secondary conditions.
|
||||
# If the affiliation string is a University, we are allowed to pick a best-effort result
|
||||
if 'university' in lucky_guess:
|
||||
for c_id in cand_ids:
|
||||
if c_id in first_y_ids:
|
||||
return get_result(c_id)
|
||||
return get_result(cand_ids[0])
|
||||
|
||||
# To avoid making a blind guess between identical ties for other institutions, return no result.
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
# Return some indication of an error, or log the row
|
||||
print(f"Error: {str(e)}")
|
||||
print(raw_aff_string)
|
||||
pass
|
||||
print(f"Error in run_affro: {str(e)}")
|
||||
print(f"Input: {raw_aff_string}")
|
||||
return []
|
||||
|
||||
@lru_cache(maxsize=32768)
|
||||
def _run_affro_cached(aff, check_country=True):
|
||||
#print(f"Running run_affro for affiliation: {aff}")
|
||||
# lucky_guess = clean_string_lucky(aff)
|
||||
direct = direct_mapping(aff)
|
||||
if not direct[0]:
|
||||
res = run_affro_(aff)
|
||||
else:
|
||||
combined = direct[0] + run_affro_(direct[1])
|
||||
# Deduplicate by 'value' (ROR ID), keeping first occurrence
|
||||
seen = set()
|
||||
deduped = []
|
||||
for entry in combined:
|
||||
if entry['value'] not in seen:
|
||||
seen.add(entry['value'])
|
||||
deduped.append(entry)
|
||||
res = deduped
|
||||
|
||||
if check_country:
|
||||
res = discard_country_mismatched_institutes(res, aff=aff)
|
||||
return tuple(res)
|
||||
|
||||
|
||||
def run_affro(aff, check_country=True, **kwargs):
|
||||
if 'check_city' in kwargs and not kwargs['check_city']:
|
||||
check_country = False
|
||||
return list(_run_affro_cached(aff, check_country=check_country))
|
||||
|
||||
run_affro.cache_info = _run_affro_cached.cache_info
|
||||
run_affro.cache_clear = _run_affro_cached.cache_clear
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def matchings_affro(aff_string):
|
||||
def matchings_affro(aff_string, check_country=True, **kwargs):
|
||||
# global operation_counter
|
||||
try:
|
||||
matchings = run_affro(aff_string)
|
||||
matchings = run_affro(aff_string, check_country=check_country, **kwargs)
|
||||
# operation_counter += 1
|
||||
|
||||
# Ensure matchings is a list, even if affro returns a single dict
|
||||
|
|
@ -168,7 +354,7 @@ def matchings_affro(aff_string):
|
|||
# Create the result as a tuple that matches matchings_schema
|
||||
result = []
|
||||
for matching in matchings:
|
||||
# Assuming 'matching' is a dictionary that contains 'provenance', 'version', 'pid', 'value', 'name', 'confidence', 'status', 'country'
|
||||
# Assuming 'matching' is a dictionary that contains 'provenance', 'affro', 'value', 'confidence', 'status'
|
||||
result.append((
|
||||
matching.get("provenance", None),
|
||||
matching.get("version", None),
|
||||
|
|
@ -178,7 +364,6 @@ def matchings_affro(aff_string):
|
|||
float(matching.get("confidence", None)),
|
||||
matching.get("status", None),
|
||||
matching.get("country", None)
|
||||
|
||||
))
|
||||
if len(result)>0:
|
||||
return result
|
||||
|
|
@ -189,5 +374,4 @@ def matchings_affro(aff_string):
|
|||
return ()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
__version__ = "3.4.0"
|
||||
|
|
@ -1,91 +1,162 @@
|
|||
from affro.helpers.functions import *
|
||||
|
||||
def valueToCategory(value):
|
||||
flag = 0
|
||||
|
||||
for k in categ_dicts:
|
||||
if k in value and categ_dicts[k] in categ_string.split('|'):
|
||||
flag = 1
|
||||
return flag
|
||||
|
||||
keys = []
|
||||
values = []
|
||||
for k in categ_dicts:
|
||||
if k in value and categ_dicts[k] in _CATEG_SET:
|
||||
if k not in keys:
|
||||
keys.append(k)
|
||||
values.append(categ_dicts[k])
|
||||
|
||||
return [keys, values[0]] if len(values) > 0 else [[], '']
|
||||
|
||||
|
||||
# Pre-compute category set once at module level (avoids rebuilding on every call)
|
||||
_CATEG_SET = frozenset(categ_string.split('|'))
|
||||
|
||||
# Pre-compute protect set for O(1) lookup
|
||||
protect = {
|
||||
'national univer ireland',
|
||||
'univer',
|
||||
'univer california',
|
||||
'univer colege hospital',
|
||||
'univer colege',
|
||||
'univer hospital',
|
||||
'imperial colege',
|
||||
'city univer',
|
||||
'univer medical school',
|
||||
'california state univer',
|
||||
'national techn univer',
|
||||
'techn univer',
|
||||
'islamic azad univer',
|
||||
'univer nevada',
|
||||
'univer maryland',
|
||||
'state univer',
|
||||
'rijksuniver',
|
||||
'rijks univer',
|
||||
'univer medical center',
|
||||
'royal colege surgeons',
|
||||
'st patricks colege',
|
||||
'instit techn',
|
||||
'trinity colege',
|
||||
'st johns colege',
|
||||
'wiliam beaumont hospital',
|
||||
'american colege',
|
||||
'univer studi'
|
||||
}
|
||||
|
||||
keep = {'univer','trinity colege','imperial colege' ,'colege surgeons'}
|
||||
|
||||
# Pre-compute city_names and countries as sets for O(1) lookup
|
||||
city_names_set = None
|
||||
countries_set = None
|
||||
remove_list_set = None
|
||||
_city_or_remove_set = None
|
||||
_city_or_country_set = None
|
||||
|
||||
def _init_sets():
|
||||
"""Initialize sets on first call for O(1) lookups."""
|
||||
global city_names_set, countries_set, remove_list_set, _city_or_remove_set, _city_or_country_set
|
||||
if city_names_set is None:
|
||||
city_names_set = set(city_names)
|
||||
countries_set = set(countries)
|
||||
remove_list_set = set(remove_list)
|
||||
_city_or_remove_set = frozenset(city_names_set | remove_list_set)
|
||||
_city_or_country_set = frozenset(city_names_set | countries_set)
|
||||
|
||||
def _check_author_words(text):
|
||||
"""Pre-compile exclusion word checks."""
|
||||
author_words = {'asistant', 'researcher', 'phd', 'student', 'section', 'prof', 'director'}
|
||||
# required_words = {'school', 'univ', 'inst', 'lab', 'fac','hospital'}
|
||||
|
||||
return any(word in text for word in author_words)#, any(word in text for word in required_words)
|
||||
|
||||
# tokenization
|
||||
protect = ['national univer ireland',
|
||||
'univer',
|
||||
'univer california',
|
||||
'univer colege hospital',
|
||||
'univer colege',
|
||||
'univer hospital',
|
||||
'imperial colege',
|
||||
'city univer',
|
||||
'univer medical school',
|
||||
'california state univer',
|
||||
'national techn univer',
|
||||
'techn univer',
|
||||
'islamic azad univer',
|
||||
'univer nevada',
|
||||
'univer maryland',
|
||||
'state univer',
|
||||
'rijksuniver',
|
||||
'rijks univer',
|
||||
'univer medical center'
|
||||
]
|
||||
|
||||
def create_df_algorithm(raw_aff_string, radius_u):
|
||||
"""
|
||||
OPTIMIZED VERSION:
|
||||
- Pre-compute sets for O(1) lookups instead of repeated 'in' checks
|
||||
- Avoid redundant valueToCategory() calls by caching results
|
||||
- Combine string operations where possible
|
||||
- Use set operations for filtering
|
||||
"""
|
||||
_init_sets()
|
||||
# print("raw_aff_string:", raw_aff_string)
|
||||
clean_aff = clean_string(remove_outer_parentheses(remove_leading_numbers(raw_aff_string)))
|
||||
#print(0, clean_aff)
|
||||
# print("clean_aff:", clean_aff)
|
||||
countries_list = description(clean_aff)[1]
|
||||
aff_no_symbols_d = substrings_dict(reduce(clean_aff))
|
||||
#print(0.5, aff_no_symbols_d)
|
||||
aff_no_symbols_d = substrings_dict(reduce(clean_aff))
|
||||
# print("aff_no_symbols_d:", aff_no_symbols_d)
|
||||
substring_list = [replace_abbr_univ(x) for x in list(aff_no_symbols_d.values())]
|
||||
#print(1, substring_list)
|
||||
# print("substring_list after replace_abbr_univ:", substring_list)
|
||||
i = 0
|
||||
|
||||
while i < len(substring_list) - 1:
|
||||
if substring_list[i] in protect and any(name in substring_list[i+1] for name in city_names): #substring_list[i+1] in city_names:
|
||||
substring_list[i] = substring_list[i] + ' ' + substring_list[i+1]
|
||||
i = i+2
|
||||
current = substring_list[i]
|
||||
next_item = substring_list[i + 1]
|
||||
|
||||
# OPTIMIZATION: Use set membership for O(1) checks
|
||||
if current in protect and any(
|
||||
name in next_item#.split()
|
||||
for name in _city_or_country_set
|
||||
# if name != "univer"
|
||||
):
|
||||
# print('Merging protected:', current, 'with next:', next_item)
|
||||
substring_list[i] = current + ' ' + next_item
|
||||
i += 2
|
||||
continue
|
||||
|
||||
elif ('assistant' in substring_list[i] or 'researcher' in substring_list[i] or 'phd' in substring_list[i] or 'student' in substring_list[i] or 'section' in substring_list[i] or 'prof' in substring_list[i] or 'director' in substring_list[i]) and (not 'school' in substring_list[i+1] or 'univ', substring_list[i+1] or 'inst' in substring_list[i+1] or 'lab' in substring_list[i+1] or 'fac' in substring_list[i+1]):
|
||||
if not 'univ' in substring_list[i]:
|
||||
|
||||
# OPTIMIZATION: Pre-compute exclusion checks
|
||||
has_exclusion = _check_author_words(current)
|
||||
#has_exclusion, has_required = _check_exclusion_words(current)
|
||||
|
||||
# print(has_exclusion, has_required)
|
||||
if (has_exclusion and (not 'school' in next_item or any(w in next_item for w in {'univ', 'inst', 'lab', 'fac','colege'}))) or ('colege' in current and 'univ' in next_item): #if not 'univ' in current and 'trinity colege' not in current and 'colege surgeons' not in current:
|
||||
if not any(sub in current for sub in keep):
|
||||
|
||||
substring_list.pop(i)
|
||||
else:
|
||||
i = i+1
|
||||
i += 1
|
||||
|
||||
elif ('engineer' in substring_list[i] or 'progr'in substring_list[i] or 'unit' in substring_list[i] or 'dep' in substring_list[i] or 'school' in substring_list[i] #or 'lab' in substring_list[i] # or 'inst' in substring_list[i] #or is_contained('hosp', substring_list[i])
|
||||
or 'fac' in substring_list[i]) and 'univ' in substring_list[i+1]:
|
||||
if not 'univ' in substring_list[i]:
|
||||
elif any(word in current for word in {'engineer', 'progr', 'unit', 'dep', 'school', 'fac'}) and 'univ' in next_item:
|
||||
if not 'univ' in current and not 'hospital' in current:
|
||||
substring_list.pop(i)
|
||||
else:
|
||||
i = i+1
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# elif 'lab' in substring_list[i] and ('colege' in substring_list[i+1] or 'dep' in substring_list[i+1] or 'school' in substring_list[i+1]):
|
||||
# if not 'univ' in substring_list[i]: #'inst' in substring_list[i+1] or
|
||||
# substring_list.pop(i)
|
||||
# else:
|
||||
# i = i+1
|
||||
# continue
|
||||
|
||||
else:
|
||||
i += 1
|
||||
# print(1.4, substring_list)
|
||||
|
||||
light_aff = (', '.join((substring_list)))
|
||||
# print(1.5, light_aff)
|
||||
|
||||
substring_list = [x for x in substring_list if x.replace(' gmbh','') not in city_names+remove_list]
|
||||
# print(1.7,substring_list)
|
||||
|
||||
substring_list0 = [shorten_keywords([x], radius_u) for x in substring_list if len(shorten_keywords([x],radius_u))>0]
|
||||
# print(2,substring_list0 )
|
||||
|
||||
substring_list1 = [inner for outer in substring_list0 for inner in outer]
|
||||
# print(3,substring_list1 )
|
||||
|
||||
aff_list = [{"index": i, "keywords": substring_list1[i], "category": valueToCategory(substring_list1[i])} for i in range(len(substring_list1))]
|
||||
|
||||
filtered_list = [entry for entry in aff_list if entry.get("category") == 1]
|
||||
|
||||
return [clean_aff, light_aff, filtered_list, countries_list]
|
||||
# print("substring_list before merging:", substring_list)
|
||||
light_aff = ', '.join(substring_list)
|
||||
# print("light_aff:", light_aff)
|
||||
# OPTIMIZATION: Use set for filtering instead of list comprehension
|
||||
substring_list = [x for x in substring_list if x.replace(' gmbh', '') not in _city_or_remove_set]
|
||||
|
||||
# OPTIMIZATION: Cache valueToCategory results to avoid recomputation
|
||||
# OPTIMIZATION: compute shorten_keywords once per element (was called twice: once for guard, once for value)
|
||||
substring_list1 = []
|
||||
for x in substring_list:
|
||||
shortened = shorten_keywords([x], radius_u)
|
||||
if shortened:
|
||||
substring_list1.extend(shortened)
|
||||
|
||||
# OPTIMIZATION: Single pass through valueToCategory instead of multiple passes
|
||||
aff_list = []
|
||||
keys_list_raw = []
|
||||
|
||||
for i, keyword in enumerate(substring_list1):
|
||||
category_result = valueToCategory(keyword)
|
||||
if category_result[1] != '':
|
||||
aff_list.append({
|
||||
"index": i,
|
||||
"keywords": keyword,
|
||||
"category": category_result[1]
|
||||
})
|
||||
if len(category_result[0]) > 0:
|
||||
keys_list_raw.extend(category_result[0])
|
||||
|
||||
# OPTIMIZATION: Use set to remove duplicates O(n) instead of nested list comprehension
|
||||
keys_list = list(set(keys_list_raw))
|
||||
|
||||
return [clean_aff, light_aff, aff_list, countries_list, keys_list]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,245 @@
|
|||
from affro.helpers.functions import *
|
||||
from . import __version__
|
||||
|
||||
VERSION = __version__
|
||||
|
||||
def distance(s, substr1, substr2):
|
||||
pos1 = s.find(substr1) # start index of substr1
|
||||
pos2 = s.find(substr2) # start index of substr2
|
||||
|
||||
# One of the substrings not found
|
||||
|
||||
distance = abs(pos1 - pos2)
|
||||
return distance
|
||||
|
||||
# Sort by descending key length so the most specific keys are checked first,
|
||||
# giving earlier matches and fewer iterations on average.
|
||||
_LABELS = ('forth', 'demokritos', 'ircs', 'fraunhofer', 'cnr', 'max_planck', 'helmholtz', 'leibniz', 'infn')
|
||||
|
||||
def _build_all_label_keys():
|
||||
"""Return dict of {label: (sorted_keys, key_to_id)} in a single pass over dix_name."""
|
||||
keys_by_label = {lbl: [] for lbl in _LABELS}
|
||||
id_by_label = {lbl: {} for lbl in _LABELS}
|
||||
|
||||
for k, v in dix_name.items():
|
||||
for d in v:
|
||||
lbl = d.get('label')
|
||||
if lbl in keys_by_label:
|
||||
if lbl == 'cnr' and k in {'isti', 'cnr isti'}:
|
||||
continue
|
||||
keys_by_label[lbl].append(k)
|
||||
id_by_label[lbl][k] = d['id']
|
||||
break
|
||||
|
||||
result = {}
|
||||
for lbl in _LABELS:
|
||||
sorted_keys = sorted(keys_by_label[lbl], key=len, reverse=True)
|
||||
result[lbl] = (sorted_keys, id_by_label[lbl])
|
||||
return result
|
||||
|
||||
_ALL_LABEL_KEYS = _build_all_label_keys()
|
||||
|
||||
forth_keys, forth_id = _ALL_LABEL_KEYS["forth"]
|
||||
demokritos_keys, demokritos_id = _ALL_LABEL_KEYS["demokritos"]
|
||||
ircs_keys, ircs_id = _ALL_LABEL_KEYS["ircs"]
|
||||
fraunhofer_keys, fraunhofer_id = _ALL_LABEL_KEYS["fraunhofer"]
|
||||
cnr_keys, cnr_id = _ALL_LABEL_KEYS["cnr"]
|
||||
max_planck_keys, max_planck_id = _ALL_LABEL_KEYS["max_planck"]
|
||||
helmholtz_keys, helmholtz_id = _ALL_LABEL_KEYS["helmholtz"]
|
||||
leibniz_keys, leibniz_id = _ALL_LABEL_KEYS["leibniz"]
|
||||
infn_keys, infn_id = _ALL_LABEL_KEYS["infn"]
|
||||
|
||||
dashed_names = load_json('./jsons/dashed_names.json.gz')
|
||||
|
||||
names_dict = load_json('./jsons/dix_name_id.json.gz')
|
||||
|
||||
def _make_dict_entry(pid, id, info):
|
||||
return {
|
||||
'provenance': 'dictionaries',
|
||||
'version': VERSION,
|
||||
'pid': pid,
|
||||
'value': id,
|
||||
'name': info['name'],
|
||||
'confidence': 1,
|
||||
'status': info['status'][0],
|
||||
'country': info['country'],
|
||||
}
|
||||
|
||||
def _make_entry(pid, id, info):
|
||||
return {
|
||||
'provenance': 'affro_direct',
|
||||
'version': VERSION,
|
||||
'pid': pid,
|
||||
'value': id,
|
||||
'name': info['name'],
|
||||
'confidence': 1,
|
||||
'status': info['status'][0],
|
||||
'country': info['country'],
|
||||
|
||||
}
|
||||
|
||||
|
||||
def direct_mapping_schema(list_ids):
|
||||
res = []
|
||||
for id in set(list_ids):
|
||||
info = dix_id[id] # single lookup per id
|
||||
if 'ror' in id:
|
||||
res.append(_make_entry('ror', id, info))
|
||||
if info['status'][0] != 'active':
|
||||
for successor in info['status'][1]:
|
||||
res.append(_make_entry('ror', successor, dix_id[successor]))
|
||||
else:
|
||||
res.append(_make_entry('openorgs', id, info))
|
||||
return res
|
||||
|
||||
def direct_mapping(aff):
|
||||
clean_aff_str = str(aff).strip().lower()
|
||||
if clean_aff_str in names_dict:
|
||||
match_id = names_dict[clean_aff_str]
|
||||
info = dix_id[match_id]
|
||||
pid = 'ror' if 'ror' in match_id else 'openorgs'
|
||||
res = [_make_dict_entry(pid, match_id, info)]
|
||||
if pid == 'ror' and info['status'][0] != 'active' and len(info['status']) > 1:
|
||||
for successor in info['status'][1]:
|
||||
if successor and successor in dix_id:
|
||||
res.append(_make_dict_entry('ror', successor, dix_id[successor]))
|
||||
return [res, '']
|
||||
|
||||
assigned = []
|
||||
|
||||
# stem univ, inst
|
||||
aff_cleaned = normalize_organization_names(clean_string(aff), university_terms).replace(" and ", " ").replace("-", " ")
|
||||
shorten_aff = re.sub(r' +', ' ', aff_cleaned)
|
||||
|
||||
for name in dashed_names:
|
||||
if name in shorten_aff and any(country in shorten_aff for country in dix_id[dashed_names[name]]['country']):
|
||||
assigned.append(dashed_names[name])
|
||||
|
||||
shorten_aff = shorten_aff.replace(name, " ")
|
||||
|
||||
if 'instit aplied physics photonic materials' in shorten_aff and 'dresden' in shorten_aff:
|
||||
assigned.append('https://ror.org/031whys72')
|
||||
shorten_aff = shorten_aff.replace('instit aplied physics photonic materials', '')
|
||||
|
||||
if 'europ univer' in shorten_aff and 'cyprus' in shorten_aff:
|
||||
assigned.append('https://ror.org/04xp48827')
|
||||
shorten_aff = shorten_aff.replace('europ univer', '')
|
||||
|
||||
if 'univer north carolina' in shorten_aff and 'chapel hil' in shorten_aff:
|
||||
assigned.append('https://ror.org/0130frc33')
|
||||
shorten_aff = shorten_aff.replace('univer north carolina', '')
|
||||
|
||||
if 'frederick univer' in shorten_aff:
|
||||
assigned.append('https://ror.org/05d8tf882')
|
||||
shorten_aff = shorten_aff.replace('frederick univer', '')
|
||||
|
||||
if 'trinity colege dublin' in shorten_aff or 'dublin trinity colege' in shorten_aff:
|
||||
assigned.append('https://ror.org/02tyrky19')
|
||||
shorten_aff = shorten_aff.replace('trinity colege dublin', '').replace('dublin trinity colege', '')
|
||||
|
||||
if 'colege' in shorten_aff:
|
||||
if 'intercolege' in shorten_aff and ('cyprus' in shorten_aff or 'nicosia' in shorten_aff):
|
||||
assigned.append('https://ror.org/012gfrj24')
|
||||
shorten_aff = shorten_aff.replace('intercolege', '')
|
||||
if 'mary immaculate colege' in shorten_aff and ('limerick' in shorten_aff or 'ireland' in shorten_aff):
|
||||
assigned.append('https://ror.org/009q3yg92')
|
||||
shorten_aff = shorten_aff.replace('mary immaculate colege', ',')
|
||||
|
||||
if 'foundation research techn' in shorten_aff and ('helas' in shorten_aff or 'greece' in shorten_aff):
|
||||
# print('h')
|
||||
assigned.append('https://ror.org/052rphn09')
|
||||
shorten_aff = shorten_aff.replace('foundation research techn', '')
|
||||
|
||||
if 'helmholtz zentrum' in shorten_aff or 'helmholtz center' in shorten_aff:
|
||||
assigned.append('https://ror.org/00cfam450')
|
||||
|
||||
if 'instit' in shorten_aff or "istituto" in shorten_aff:
|
||||
|
||||
if 'friedrich loefler' in shorten_aff:
|
||||
assigned.append('https://ror.org/025fw7a54')
|
||||
shorten_aff = shorten_aff.replace('friedrich loefler instit', '').replace('bundesforschungsinstit tiergesundheit', '').replace('federal research instit animal health', '')
|
||||
|
||||
if 'julius kuhn' in shorten_aff or 'julius kuehn' in shorten_aff:
|
||||
assigned.append('https://ror.org/022d5qt08')
|
||||
shorten_aff = shorten_aff.replace('julius kuhn instit', '').replace('julius kuehn', '').replace('bundesforschungsinstit kulturpflanzen', '').replace('federal research center cultivated plants', '')
|
||||
|
||||
if "forth" in shorten_aff or 'foundation research techn' in shorten_aff:
|
||||
for key in forth_keys:
|
||||
if key in shorten_aff and (distance(shorten_aff, 'forth', key) < len(key)+len('forth')+5 or 'foundation research techn' in shorten_aff):
|
||||
ror_id = forth_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
shorten_aff = shorten_aff.replace(key, ',')
|
||||
|
||||
if "demokritos" in shorten_aff or "ncsr" in shorten_aff:
|
||||
for key in demokritos_keys:
|
||||
if key in shorten_aff and distance(shorten_aff, 'demokritos', key) < len(key)+len('demokritos')+5:
|
||||
ror_id = demokritos_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
shorten_aff = shorten_aff.replace(key, ',')
|
||||
|
||||
if 'tu clausthal' in shorten_aff and 'instit organische chemie' in shorten_aff and distance(shorten_aff, 'tu clausthal', 'instit organische chemie') < len('tu clausthal')+len('instit organische chemie')+5:
|
||||
assigned.append('openorgs____::0000103105')
|
||||
shorten_aff = shorten_aff.replace('instit organische chemie', ',')
|
||||
|
||||
if 'ircs' in shorten_aff or 'istituti ricovero e cura caratere scien' in shorten_aff or 'milan' in shorten_aff:
|
||||
for key in ircs_keys:
|
||||
if key in shorten_aff and distance(shorten_aff, 'ircs', key) < len(key)+len('istituti ricovero e cura caratere scien')+5:
|
||||
ror_id = ircs_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
shorten_aff = shorten_aff.replace(key, ',')
|
||||
if 'infn' in shorten_aff or 'istituto nazionale fisica nucleare' in shorten_aff:
|
||||
for key in infn_keys:
|
||||
if key in shorten_aff:
|
||||
ror_id = infn_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
shorten_aff = shorten_aff.replace(key, ',')
|
||||
|
||||
if ('instit comunication computer systems' in shorten_aff or 'ics' in shorten_aff or 'instit computer comunication systems') and ('national techn univer athens' in aff_cleaned or 'ntua' in aff_cleaned):
|
||||
assigned.append('https://ror.org/0483fn738')
|
||||
shorten_aff = shorten_aff.replace('instit comunication computer systems', ',')
|
||||
|
||||
if 'fraunhofer' in shorten_aff:
|
||||
for key in fraunhofer_keys:
|
||||
if key in shorten_aff and distance(shorten_aff, 'fraunhofer', key) < len(key)+len('fraunhofer')+5:
|
||||
ror_id = fraunhofer_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
shorten_aff = shorten_aff.replace(key, ',').replace('fraunhofer', ',')
|
||||
|
||||
if 'cnr' in shorten_aff or ('national research council' in shorten_aff and 'italy' in shorten_aff):
|
||||
for key in cnr_keys:
|
||||
if key in shorten_aff and distance(shorten_aff, 'cnr', key) < len(key)+len('national research council')+5:
|
||||
ror_id = cnr_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
shorten_aff = shorten_aff.replace(key, ',')
|
||||
|
||||
if 'max planck' in shorten_aff:
|
||||
for key in max_planck_keys:
|
||||
if key in shorten_aff and distance(shorten_aff, 'max planck', key) < len(key)+len('max planck')+15:
|
||||
ror_id = max_planck_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
shorten_aff = shorten_aff.replace(key, ',')
|
||||
|
||||
if 'helmholtz' in shorten_aff:
|
||||
for key in helmholtz_keys:
|
||||
if key in shorten_aff and distance(shorten_aff, 'helmholtz', key) < len(key)+len('helmholtz')+15:
|
||||
ror_id = helmholtz_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
|
||||
if 'leibniz' in shorten_aff:
|
||||
for key in leibniz_keys:
|
||||
if key in shorten_aff and distance(shorten_aff, 'leibniz', key) < len(key)+len('leibniz')+15:
|
||||
ror_id = leibniz_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
shorten_aff = shorten_aff.replace(key, ',')
|
||||
|
||||
|
||||
return [direct_mapping_schema(assigned), shorten_aff]
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
from affro.helpers.functions import *
|
||||
from affro.helpers.create_input import *
|
||||
from .. import __version__
|
||||
|
||||
VERSION = __version__
|
||||
|
||||
|
||||
|
||||
pattern = re.compile(r'\b(' + '|'.join(us_states) + r')\b')
|
||||
|
||||
def contains_us_state(text):
|
||||
return bool(pattern.search(text.lower()))
|
||||
|
||||
# def contains_gr_cities(text):
|
||||
# return bool(pattern.search(text.lower()))
|
||||
|
||||
# def contains_ie_cities(text):
|
||||
# return bool(pattern.search(text.lower()))
|
||||
|
||||
# weak_keywords = ['department']
|
||||
|
||||
def convert_to_result(id_list_, dix_id):
|
||||
result = []
|
||||
seen = set()
|
||||
|
||||
for r in id_list_:
|
||||
score = r[1]
|
||||
if score > 1.0:
|
||||
score = 1.0
|
||||
|
||||
value = r[2]
|
||||
rec = dix_id.get(value)
|
||||
if not rec:
|
||||
continue
|
||||
|
||||
name = rec.get('name')
|
||||
country = rec.get('country')
|
||||
|
||||
status = rec.get('status') or []
|
||||
primary = status[0] if status else None
|
||||
secondary = status[1] if len(status) > 1 else []
|
||||
|
||||
# openorgs
|
||||
if "openorgs" in value:
|
||||
key = ('openorgs', value)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
result.append({
|
||||
'provenance': 'affro',
|
||||
'version': VERSION,
|
||||
'pid': 'openorgs',
|
||||
'value': value,
|
||||
'name': name,
|
||||
'confidence': float(score),
|
||||
'status': 'active',
|
||||
'country': country
|
||||
})
|
||||
continue
|
||||
|
||||
# ROR active
|
||||
if primary == 'active':
|
||||
key = ('ror', value)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
result.append({
|
||||
'provenance': 'affro',
|
||||
'version': VERSION,
|
||||
'pid': 'ror',
|
||||
'value': value,
|
||||
'name': name,
|
||||
'confidence': float(score),
|
||||
'status': 'active',
|
||||
'country': country
|
||||
})
|
||||
continue
|
||||
|
||||
# ROR inactive
|
||||
key = ('ror', value)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
result.append({
|
||||
'provenance': 'affro',
|
||||
'version': VERSION,
|
||||
'pid': 'ror',
|
||||
'value': value,
|
||||
'name': name,
|
||||
'confidence': float(score),
|
||||
'status': primary,
|
||||
'country': country
|
||||
})
|
||||
|
||||
# linked active records
|
||||
if secondary and secondary[0] != '':
|
||||
for link in secondary:
|
||||
if not link:
|
||||
continue
|
||||
|
||||
link_key = ('ror', link)
|
||||
if link_key in seen:
|
||||
continue
|
||||
|
||||
link_rec = dix_id.get(link)
|
||||
if not link_rec:
|
||||
continue
|
||||
|
||||
seen.add(link_key)
|
||||
result.append({
|
||||
'provenance': 'affro',
|
||||
'version': VERSION,
|
||||
'pid': 'ror',
|
||||
'value': link,
|
||||
'name': link_rec.get('name'),
|
||||
'confidence': float(score),
|
||||
'status': 'active',
|
||||
'country': link_rec.get('country')
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
def count_active(items):
|
||||
return sum(1 for x in items if x.get("status") == "active")
|
||||
|
||||
def disamb(input, id_list_,dix_id):
|
||||
# print('disamb id_list_', id_list_)
|
||||
if id_list_ == []:
|
||||
return []
|
||||
clean_aff = input[0]
|
||||
# print('result_dict',result_dict)
|
||||
if 'https://ror.org/00nnh8h94' in str(id_list_) and ('greece' not in clean_aff and 'helas' not in clean_aff and 'athens' not in clean_aff):
|
||||
id_list_ = [id for id in id_list_ if id[2] != 'https://ror.org/00nnh8h94']
|
||||
result_dict = convert_to_result(id_list_, dix_id)
|
||||
|
||||
num_actives = count_active(result_dict)
|
||||
num_countries = len(description(clean_aff)[1])
|
||||
|
||||
# print('num_actives', num_actives)
|
||||
if len(id_list_) ==1 :
|
||||
return result_dict
|
||||
elif num_countries == 0:
|
||||
# print('no country in affiliation')
|
||||
# polytechnic?
|
||||
# countries_uni = {
|
||||
# country
|
||||
# for res in result_dict
|
||||
# if 'Uni' in res['name'] or 'Trinity' in res['name']
|
||||
# for country in res['country'] }-low_prob_countries
|
||||
|
||||
countries_high = {
|
||||
country
|
||||
for res in result_dict
|
||||
for country in res['country']
|
||||
if country not in low_prob_countries
|
||||
}
|
||||
# print(countries_high)
|
||||
# print('countries_uni',countries_uni)
|
||||
|
||||
#[res['country'] for res in result_dict if 'Uni' in res['name']]
|
||||
if len(countries_high) >0:
|
||||
#If we suspect the affiliation refers to a university, restrict matches to the same country as detected university candidates
|
||||
final_matching = [res for res in result_dict if any(c in countries_high for c in res['country'])]
|
||||
if len(final_matching)==1 or len(countries_high)==1:
|
||||
return final_matching
|
||||
else:
|
||||
final_matching2 = [res for res in final_matching if 'Inst' not in res['name'] and 'Lab' not in res['name']]
|
||||
return final_matching2
|
||||
|
||||
else:
|
||||
# print('no universities')
|
||||
return result_dict
|
||||
|
||||
elif num_actives > num_countries:
|
||||
# print('more results than countries')
|
||||
final_matching = []
|
||||
light_aff_tokens = [clean_string_ror(x) for x in set(clean_aff.split())]
|
||||
for res in result_dict:
|
||||
# print('res',res)
|
||||
|
||||
country = res['country']
|
||||
country_str = str(country)
|
||||
if 'united states' in country:
|
||||
if 'united states' in clean_aff or 'usa' in light_aff_tokens or contains_us_state(clean_aff):
|
||||
final_matching.append(res)
|
||||
|
||||
elif 'united kingdom' in country:
|
||||
if 'united kingdom' in clean_aff or 'uk' in light_aff_tokens:
|
||||
final_matching.append(res)
|
||||
|
||||
elif 'korea' in country_str:
|
||||
|
||||
if 'korea' in light_aff_tokens:
|
||||
final_matching.append(res)
|
||||
# elif 'greece' in country:
|
||||
# if 'greece' in clean_aff or 'helas' in clean_aff or contains_gr_cities(clean_aff):
|
||||
# inal_matching.append(res)
|
||||
|
||||
|
||||
elif any(c in clean_aff for c in country): #country in clean_aff:
|
||||
final_matching.append(res)
|
||||
|
||||
if final_matching:
|
||||
return final_matching
|
||||
|
||||
else:
|
||||
return result_dict
|
||||
|
||||
elif len(result_dict)>0:
|
||||
return result_dict
|
||||
else:
|
||||
# print('leider nichts')
|
||||
return []
|
||||
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
from affro.helpers.functions import *
|
||||
from affro.helpers.create_input import *
|
||||
|
||||
low_confidence_ids = {'https://ror.org/003agka18', 'https://ror.org/00wra1b14','https://ror.org/05fe6f045',
|
||||
'https://ror.org/05pz81c75', 'https://ror.org/05q882j59', 'https://ror.org/00tpdkh38',
|
||||
'https://ror.org/04fnhr972','https://ror.org/04155gf20', 'https://ror.org/0243vac68', 'https://ror.org/00p61c264',
|
||||
'https://ror.org/01p2ej961','https://ror.org/04vwgk321','https://ror.org/01pc4rp54','https://ror.org/034ymz395',
|
||||
'https://ror.org/018xp1p31', 'https://ror.org/042tmw509', 'https://ror.org/035qa0k76','https://ror.org/009ytmf35',
|
||||
'https://ror.org/048gmay44', 'https://ror.org/04wvz8b06', 'https://ror.org/01rk3xf02', 'https://ror.org/03gbw6p94', 'https://ror.org/02jjdwm75',
|
||||
'https://ror.org/05byh2e17', 'https://ror.org/02eaafc18'}
|
||||
|
||||
|
||||
specific = set(k for k in categ_dicts if categ_dicts[k] in {'Specific', 'Acronyms'})
|
||||
|
||||
special_countries = {k for k, v in country_synonyms.items() if len(v) > 1}
|
||||
|
||||
excluded = {'univ', 'inst', 'national', 'nacional', 'colege', 'center', 'organization', 'hospital'}
|
||||
|
||||
|
||||
|
||||
def keep_highest_score(data):
|
||||
"""
|
||||
Keeps only one entry per unique ID.
|
||||
Keeps the one with highest confidence score.
|
||||
"""
|
||||
best = {}
|
||||
|
||||
for org, conf, id_ in data:
|
||||
if id_ not in best or conf > best[id_][1]:
|
||||
best[id_] = [org, conf, id_]
|
||||
|
||||
return list(best.values())
|
||||
|
||||
|
||||
def _match_special_country(country_list, text, tokens):
|
||||
"""
|
||||
Check if any special-country synonym appears in the affiliation text.
|
||||
Safer token-based matching.
|
||||
"""
|
||||
country_syns = {
|
||||
synonym
|
||||
for country in country_list
|
||||
for synonym in country_synonyms.get(country, [country])
|
||||
}
|
||||
|
||||
# Strict token match first
|
||||
if country_syns & tokens:
|
||||
return True
|
||||
|
||||
# Controlled substring match (avoid very short tokens like 'us')
|
||||
for syn in country_syns:
|
||||
if len(syn) > 3 and re.search(r'\b' + re.escape(syn) + r'\b', text):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def disamb_country(org, light_aff):
|
||||
ids = []
|
||||
|
||||
for rec in dix_name[org]:
|
||||
country_list = rec['country']
|
||||
if any(c in light_aff for c in country_list) and \
|
||||
not any(c in org for c in country_list):
|
||||
ids.append(rec['id'])
|
||||
|
||||
return ids
|
||||
|
||||
|
||||
def disamb_city(org, light_aff):
|
||||
ids = []
|
||||
|
||||
for rec in dix_name[org]:
|
||||
city_list = rec['city']
|
||||
if any(c in light_aff for c in city_list) and \
|
||||
not any(c in org for c in city_list):
|
||||
ids.append(rec['id'])
|
||||
|
||||
return ids
|
||||
|
||||
|
||||
def disamb_city_country(org, light_aff):
|
||||
city_ids = set(disamb_city(org, light_aff))
|
||||
country_ids = set(disamb_country(org, light_aff))
|
||||
return list(city_ids & country_ids)
|
||||
|
||||
|
||||
def find_id(aff_input, best_names, dix_name, simG):
|
||||
clean_aff = aff_input[0]
|
||||
# print('find_id')
|
||||
# print(clean_aff)
|
||||
light_aff = aff_input[1]
|
||||
# print(best_names)
|
||||
id_list = []
|
||||
|
||||
clean_aff_tokens = {x.replace(',', '') for x in clean_aff.split()}
|
||||
light_aff_tokens = {x.strip(',;.:()') for x in light_aff.split()}
|
||||
for org_list in best_names:
|
||||
|
||||
|
||||
org = org_list[0]
|
||||
conf = org_list[1]
|
||||
|
||||
records = dix_name[org]
|
||||
|
||||
# -------------------------------------------------
|
||||
# SINGLE MATCH CASE
|
||||
# -------------------------------------------------
|
||||
if len(records) == 1:
|
||||
# print('Single match for:', org)
|
||||
rec = records[0]
|
||||
id_ = rec['id']
|
||||
city_list = rec['city']
|
||||
country_list = rec['country']
|
||||
if id_ in low_confidence_ids and conf < simG:
|
||||
# print('problem')
|
||||
continue
|
||||
|
||||
country_set = {
|
||||
synonym
|
||||
for country in country_list
|
||||
for synonym in country_synonyms.get(country, [country])
|
||||
}
|
||||
|
||||
# print('light_aff',light_aff)
|
||||
if org in light_aff or org in light_aff.split(','):
|
||||
# print('Exact match for:', org)
|
||||
id_list.append([org, conf, id_])
|
||||
|
||||
elif (
|
||||
not any(city in light_aff for city in city_list)
|
||||
and not country_set & light_aff_tokens
|
||||
and not any(word in org for word in excluded)
|
||||
and valueToCategory(org)[1] not in {'Company', 'Acronyms', 'Specific'}
|
||||
):
|
||||
# print('No city/country match but no exclusion words for:', org)
|
||||
pass
|
||||
|
||||
else:
|
||||
id_list.append([org, conf, id_])
|
||||
|
||||
# -------------------------------------------------
|
||||
# MULTIPLE MATCH CASE
|
||||
# -------------------------------------------------
|
||||
else:
|
||||
# print('Multiple matches for:', org)
|
||||
match_found = False
|
||||
|
||||
# Precompute country universe once
|
||||
countries_ids = {
|
||||
country
|
||||
for rec in records
|
||||
for country in rec['country']
|
||||
}
|
||||
|
||||
# -------------------------------
|
||||
# STEP 1: City + Country
|
||||
# -------------------------------
|
||||
disamb = disamb_city_country(org, light_aff)
|
||||
|
||||
if len(disamb) == 1:
|
||||
# print('STEP 1: City + Country disambiguation')
|
||||
id_list.append([org, conf, disamb[0]])
|
||||
continue
|
||||
|
||||
# -------------------------------
|
||||
# STEP 2: Country direct match
|
||||
# -------------------------------
|
||||
for rec in records:
|
||||
country_list = rec['country']
|
||||
id_ = rec['id']
|
||||
|
||||
if any(c in light_aff for c in country_list) and \
|
||||
not any(c in org for c in country_list):
|
||||
|
||||
id_list.append([org, conf, id_])
|
||||
match_found = True
|
||||
# print('STEP 2: Country direct match')
|
||||
break
|
||||
|
||||
# -------------------------------
|
||||
# STEP 3: Special country synonyms
|
||||
# -------------------------------
|
||||
if not match_found and countries_ids & special_countries:
|
||||
|
||||
for rec in records:
|
||||
if _match_special_country(
|
||||
rec['country'],
|
||||
clean_aff,
|
||||
clean_aff_tokens
|
||||
):
|
||||
id_list.append([org, conf, rec['id']])
|
||||
match_found = True
|
||||
# print('STEP 3: Special country synonym match')
|
||||
break
|
||||
|
||||
# -------------------------------
|
||||
# STEP 4: City match
|
||||
# -------------------------------
|
||||
if not match_found:
|
||||
# print('STEP 4: City match')
|
||||
for rec in records:
|
||||
city_list = rec['city']
|
||||
id_ = rec['id']
|
||||
|
||||
if any(c in clean_aff for c in city_list):
|
||||
# print('city')
|
||||
if not any(city in org for city in city_list):
|
||||
id_list.append([org, conf, id_])
|
||||
match_found = True
|
||||
# print('STEP 4: City match without city in org')
|
||||
break
|
||||
|
||||
elif any(clean_aff.count(city) > 1 for city in city_list):
|
||||
id_list.append([org, conf, id_])
|
||||
match_found = True
|
||||
# print('STEP 4: City match with multiple city mentions')
|
||||
break
|
||||
|
||||
# -------------------------------
|
||||
# STEP 5: First word of country
|
||||
# -------------------------------
|
||||
if not match_found:
|
||||
# print('STEP 5: First word of country')
|
||||
for rec in records:
|
||||
for country in rec['country']:
|
||||
if country.split()[0] in clean_aff and \
|
||||
not any(c in org for c in rec['country']):
|
||||
|
||||
id_list.append([org, conf, rec['id']])
|
||||
match_found = True
|
||||
# print('STEP 5: First word of country match')
|
||||
break
|
||||
if match_found:
|
||||
break
|
||||
|
||||
# -------------------------------
|
||||
# STEP 6: Country appears in both
|
||||
# -------------------------------
|
||||
if not match_found:
|
||||
|
||||
for rec in records:
|
||||
if any(c in clean_aff.split() for c in rec['country']) and \
|
||||
any(c in org for c in rec['country']):
|
||||
|
||||
id_list.append([org, conf, rec['id']])
|
||||
match_found = True
|
||||
# print('STEP 6: Country appears in both affiliation and org')
|
||||
break
|
||||
|
||||
# -------------------------------
|
||||
# STEP 7: Specific/Acronym preference
|
||||
# -------------------------------
|
||||
if not match_found:
|
||||
|
||||
for sp in specific:
|
||||
if sp in org:
|
||||
|
||||
for rec in records:
|
||||
if dix_id[rec['id']]['top_level'] == 'y':
|
||||
id_list.append([org, conf, rec['id']])
|
||||
match_found = True
|
||||
# print('STEP 7: Specific/Acronym preference with top-level match')
|
||||
break
|
||||
|
||||
if not match_found:
|
||||
for rec in records:
|
||||
if dix_id[rec['id']]['parent'] == 'y':
|
||||
id_list.append([org, conf, rec['id']])
|
||||
match_found = True
|
||||
# print('STEP 7: Specific/Acronym preference with parent match')
|
||||
break
|
||||
|
||||
break
|
||||
|
||||
# -------------------------------
|
||||
# STEP 8: Fallback first='y'
|
||||
# -------------------------------
|
||||
if not match_found:
|
||||
# print(org)
|
||||
# print('last chance')
|
||||
if org != 'national research council':
|
||||
if any(c in clean_aff for c in city_list):
|
||||
id_list.append([org, conf, id_])
|
||||
match_found = True
|
||||
break
|
||||
else:
|
||||
for rec in records:
|
||||
# print('rec', rec)
|
||||
id_ = rec['id']
|
||||
|
||||
if 'department' not in org and \
|
||||
'labora' not in org and \
|
||||
'instit' not in org and \
|
||||
not any( x in low_prob_countries for x in dix_id[id_]['country']) and \
|
||||
rec['first'] == 'y':
|
||||
id_list.append([org, conf, rec['id']])
|
||||
match_found = True
|
||||
# print('STEP 8: Fallback first="y" match')
|
||||
break
|
||||
|
||||
|
||||
# if not match_found:
|
||||
# print('No match found for:', org)
|
||||
# print(id_list)
|
||||
return keep_highest_score(id_list)
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
from affro.helpers.functions import *
|
||||
from affro.helpers.create_input import *
|
||||
from affro.helpers.matching import *
|
||||
|
||||
|
||||
def find_name(input, dix_name, simU, simG, limit):
|
||||
# print('start find_name')
|
||||
# print('input',input)
|
||||
"""
|
||||
Matches affiliations in DataFrame 'DF' with names from dictionary 'dix_org' and their ROR_ids based on similarity scores.
|
||||
|
||||
Args:
|
||||
m (int): The number of DOIs to check.
|
||||
DF (DataFrame): The input DataFrame containing affiliation data.
|
||||
dix_org (dict): A dictionary of names of organizations and their ROR_ids.
|
||||
simU (float): Similarity threshold for universities.
|
||||
simG (float): Similarity threshold for non-universities.
|
||||
|
||||
Returns:
|
||||
DataFrame: The final DataFrame with matched affiliations and their corresponding similarity scores.
|
||||
"""
|
||||
clean_aff = input[0]
|
||||
light_aff = input[1].replace(' gmbh', ' ').strip()
|
||||
df_list = input[2]
|
||||
|
||||
countries_list = input[3]
|
||||
keys_list = input[4]
|
||||
|
||||
dix = {} # will store indeces and legalnames of organizations of the DOI { i : [legalname1, legalname2,...]}
|
||||
result = {}
|
||||
pairs = []
|
||||
|
||||
keywords = [entry["keywords"].replace(' gmbh', ' ').strip() for entry in df_list]
|
||||
|
||||
candidates = get_candidates(countries_list, keys_list)
|
||||
|
||||
# print('candidates found for keywords:', len(candidates))
|
||||
if len(keywords) > 1 or len(keywords) == 1 and len(keywords[0])>1:
|
||||
for k,s in enumerate(keywords):
|
||||
if len(s) >1 and s not in countries:
|
||||
pairs_k = []
|
||||
# print(s)
|
||||
#--end september 2025
|
||||
if s in candidates:
|
||||
# print('lucky', s)
|
||||
pairs_k.append((s,s,1, dix_name[s][0]['id'],dix_name[s][0]['country']))
|
||||
if k not in dix:
|
||||
dix[k] = [s]
|
||||
else:
|
||||
dix[k].append(s)
|
||||
|
||||
else:
|
||||
# else:
|
||||
# print('not lucky')
|
||||
|
||||
try:
|
||||
pairs_k = find_candidate(s, k , dix, simU, simG, candidates, limit)
|
||||
except:
|
||||
pairs_k = []
|
||||
result[k] = pairs_k
|
||||
if len(pairs_k)>0:
|
||||
pairs.append(pairs_k)
|
||||
# else:
|
||||
# other_candidates = candidates_-candidates
|
||||
# if s in other_candidates:
|
||||
# # print('lucky', s)
|
||||
# pairs_k.append((s,s,1, dix_name[s][0]['id'],dix_name[s][0]['country']))
|
||||
# if k not in dix:
|
||||
# dix[k] = [s]
|
||||
# else:
|
||||
# dix[k].append(s)
|
||||
|
||||
# else:
|
||||
# # else:
|
||||
# # print('not lucky')
|
||||
|
||||
# try:
|
||||
# pairs_k = find_candidate(s, k , dix, simU, simG, other_candidates, limit)
|
||||
# except:
|
||||
# pairs_k = []
|
||||
# result[k] = pairs_k
|
||||
# if len(pairs_k)>0:
|
||||
# pairs.append(pairs_k)
|
||||
|
||||
# print('pairs', pairs)
|
||||
multi = index_multiple_matchings(pairs)
|
||||
# print('multi', multi)
|
||||
need_check_keys = []
|
||||
ready_keys = []
|
||||
ready_best = []
|
||||
for keyword in multi:
|
||||
try:
|
||||
if multi[keyword]>1:
|
||||
need_check_keys.append(keyword)
|
||||
else:
|
||||
for p in pairs:
|
||||
if keyword in p[0]:
|
||||
if p[0][1] not in ready_keys:
|
||||
ready_keys.append(p[0][1])
|
||||
|
||||
ready_best.append([p[0][1], p[0][2]])
|
||||
except Exception as e:
|
||||
print('ERROR, find_name', e)
|
||||
pass
|
||||
|
||||
pairs_check = [ pair for pair in pairs if pair[0][0] in need_check_keys ]
|
||||
# print('pairs_check',pairs_check)
|
||||
if len(need_check_keys)>0:
|
||||
# print(' len(need_check_keys)', len(need_check_keys))
|
||||
best0 = best_sim_score(clean_aff, light_aff, len(keywords), pairs_check, multi, simU, simG)
|
||||
# print('best0', best0)
|
||||
best1 = {x[0]:dix_name[x[0]][0]['id'] for x in best0 }
|
||||
# print('best1', best1)
|
||||
|
||||
best01 = unique_subset(best0, best1)
|
||||
best = best01 + ready_best
|
||||
else:
|
||||
best = ready_best
|
||||
# print('end find_name', best)
|
||||
return best
|
||||
|
|
@ -7,18 +7,55 @@ import json
|
|||
from sklearn.feature_extraction.text import CountVectorizer
|
||||
from sklearn.metrics.pairwise import cosine_similarity
|
||||
from importlib.resources import files
|
||||
import gzip
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pre-compiled regexes used by normalize_organization_names (called many
|
||||
# times per run_affro invocation — compiling once avoids repeated parsing).
|
||||
# ---------------------------------------------------------------------------
|
||||
_RE_UNIVER = re.compile(r'univer\w*', re.IGNORECASE)
|
||||
_RE_EUROP = re.compile(r'europ\w*', re.IGNORECASE)
|
||||
_RE_INSTIT = re.compile(r'instit\w*', re.IGNORECASE)
|
||||
_RE_HOPITAL = re.compile(r'hopital\b', re.IGNORECASE)
|
||||
_RE_HOSPITAL = re.compile(r'hospital(?!s)\w*', re.IGNORECASE)
|
||||
_RE_GEN_HOSPITAL = re.compile(r'\bgeneral hospital\b', re.IGNORECASE)
|
||||
_RE_GEN_UNI_HOSP = re.compile(r'\bgeneral univer hospital\b', re.IGNORECASE)
|
||||
_RE_SCIENCES = re.compile(r'sciences\b', re.IGNORECASE)
|
||||
_RE_SCIENZE = re.compile(r'scienze\b', re.IGNORECASE)
|
||||
_RE_SCIENZA = re.compile(r'scienza\b', re.IGNORECASE)
|
||||
_RE_LABORA = re.compile(r'labora\w*', re.IGNORECASE)
|
||||
_RE_CENTRE = re.compile(r'centre\b', re.IGNORECASE)
|
||||
_RE_CENTRUM = re.compile(r'centrum\b', re.IGNORECASE)
|
||||
_RE_MEDISCH = re.compile(r'medisch\b', re.IGNORECASE)
|
||||
_RE_SAINT = re.compile(r'\bsaint\b', re.IGNORECASE)
|
||||
_RE_TRINITY = re.compile(r'\btrinity col\b', re.IGNORECASE)
|
||||
_RE_TECHNISCHE = re.compile(r'\btechnische\b', re.IGNORECASE)
|
||||
_RE_TEKNOLOGI = re.compile(r'\bteknologi\b', re.IGNORECASE)
|
||||
_RE_POLITEHNICA = re.compile(r'\bpolitehnica\b', re.IGNORECASE)
|
||||
_RE_POLITE = re.compile(r'\bpolite\w*', re.IGNORECASE)
|
||||
_RE_POLYTE = re.compile(r'\bpolyte\w*', re.IGNORECASE)
|
||||
_RE_PANEPIST = re.compile(r'panepist\w*', re.IGNORECASE)
|
||||
_RE_ARISTOT = re.compile(r'\baristot\w*', re.IGNORECASE)
|
||||
_RE_TECHN = re.compile(r'\btechn\w*', re.IGNORECASE)
|
||||
_RE_CZECHOSLOVAK = re.compile(r'\bczechoslovak\b', re.IGNORECASE)
|
||||
_RE_MACAU = re.compile(r'\bmacau\b', re.IGNORECASE)
|
||||
_RE_MUNCHEN = re.compile(r'\bmunchen\b', re.IGNORECASE)
|
||||
_RE_ORGANISATION = re.compile(r'organisation\b', re.IGNORECASE)
|
||||
_RE_XI_AN = re.compile(r'\bxi an\b', re.IGNORECASE)
|
||||
_RE_MULTI_SPACE = re.compile(r'\s+')
|
||||
|
||||
|
||||
def load_json(relative_path, package="affro"):
|
||||
"""
|
||||
Safely load a JSON file from the package.
|
||||
relative_path: path relative to the package root, e.g. 'jsons/dix_acad.json'
|
||||
Safely load a gzipped JSON file from the package.
|
||||
relative_path: path relative to the package root, e.g. 'jsons/dix_acad.json.gz'
|
||||
package: name of the top-level package
|
||||
"""
|
||||
full_path = files(package).joinpath(relative_path)
|
||||
with full_path.open("r", encoding="utf-8") as f:
|
||||
with gzip.open(full_path, "rt", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_txt(relative_path, package="affro"):
|
||||
"""
|
||||
Safely load a text file from the package.
|
||||
|
|
@ -28,29 +65,38 @@ def load_txt(relative_path, package="affro"):
|
|||
with full_path.open("r", encoding="utf-8") as file:
|
||||
return [line.strip() for line in file]
|
||||
|
||||
_replacements_cache = None
|
||||
|
||||
def get_replacements():
|
||||
global _replacements_cache
|
||||
if _replacements_cache is None:
|
||||
_replacements_cache = load_json('jsons/replacements.json.gz')
|
||||
return _replacements_cache
|
||||
|
||||
#categ_string = 'Laboratory|Univ/Inst|Hospital|Foundation|Museum|Government|Company'
|
||||
categ_string = 'Academia|Hospitals|Foundations|Specific|Government|Company|Acronyms'
|
||||
|
||||
dix_org = load_json('jsons/dix_org.json')
|
||||
dix_city = load_json('jsons/dix_city.json')
|
||||
dix_country = load_json('jsons/dix_country.json')
|
||||
dix_mult = load_json('jsons/dix_mult.json')
|
||||
|
||||
dix_country_legalnames = load_json('jsons/dix_country_legalnames.json')
|
||||
|
||||
us_states = [
|
||||
us_states = {
|
||||
"alabama", "alaska", "arizona", "arkansas", "california",
|
||||
"colorado", "conecticut", "delaware", "florida", "georgia",
|
||||
"hawaii", "idaho", "ilinois", "indiana", "iowa",
|
||||
"kansas", "kentucky", "louisiana", "maine", "maryland",
|
||||
"masachusets", "michigan", "minesota", "misisipi", "misouri",
|
||||
"montana", "nebraska", "nevada", "new hampshire", "new jersey",
|
||||
"new mexico", "new york", "north carolina", "north dakota", "ohio",
|
||||
"oklahoma", "oregon", "pensylvania", "rhode island", "south carolina",
|
||||
"south dakota", "tennesee", "texas", "utah", "vermont",
|
||||
"virginia", "washington", "west virginia", "wisconsin", "wyoming"
|
||||
]
|
||||
"new mexico", "new york", "carolina", "dakota", "ohio",
|
||||
"oklahoma", "oregon", "pensylvania", "rhode island",
|
||||
"dakota", "tenesee", "texas", "utah", "vermont",
|
||||
"virginia", "washington", "wisconsin", "wyoming","beth","boston","chicago","ucla"
|
||||
}
|
||||
|
||||
low_prob_countries = {'st kits nevis', 'kenya','malaysia','philipines','burundi',
|
||||
'cambodia','bangladesh','bolivia','azerbaijan',
|
||||
'bosnia herzegovina', 'iran','islamabad',
|
||||
'zimbabwe', 'oman', 'iraq', 'yemen', 'somalia', 'sri lanka','costa rica','indonesia'}
|
||||
|
||||
dix_name = load_json('./jsons/dix_name.json.gz')
|
||||
|
||||
dix_country_legalnames = {k: set(v) for k, v in load_json('./jsons/dix_country_legalnames.json.gz').items()}
|
||||
dix_key_legalnames = {k: set(v) for k, v in load_json('./jsons/dix_keys_names.json.gz').items()}
|
||||
|
||||
def replace_double_consonants(text):
|
||||
# This regex pattern matches any double consonant
|
||||
|
|
@ -62,18 +108,34 @@ def replace_double_consonants(text):
|
|||
|
||||
#stop_words = ['from', 'the', 'of', 'at', 'de','for','et','für','des', 'in','as','a','and','fur','for','und','di']
|
||||
|
||||
|
||||
def remove_stop_words(text):
|
||||
text = text.replace('present adres', '')
|
||||
words = text.split()
|
||||
filtered_words = [word for word in words if word not in stop_words]
|
||||
return ' '.join(filtered_words)
|
||||
filtered_words = []
|
||||
|
||||
for word in words:
|
||||
if word.endswith(","):
|
||||
core = word[:-1] # remove the comma
|
||||
if core not in stop_words:
|
||||
filtered_words.append(core + ",")
|
||||
else:
|
||||
filtered_words.append(",") # keep only the comma
|
||||
else:
|
||||
if word not in stop_words:
|
||||
filtered_words.append(word)
|
||||
|
||||
result = " ".join(filtered_words)
|
||||
# remove spaces before commas
|
||||
result = result.replace(" ,", ",")
|
||||
return result
|
||||
|
||||
|
||||
stop_words = load_txt('txts/stop_words.txt')
|
||||
|
||||
dix_id_country = load_json('jsons/dix_id_country.json')
|
||||
dix_id = load_json('jsons/dix_id.json.gz')
|
||||
|
||||
categ_dicts = load_json('jsons/dix_categ.json')
|
||||
replacements = load_json('jsons/replacements.json')
|
||||
categ_dicts = load_json('jsons/dix_categ.json.gz')
|
||||
#replacements = load_json('jsons/replacements.json.gz')
|
||||
key_words = list(categ_dicts.keys()) + ['univer', 'labora']
|
||||
countries = load_txt('txts/country_names.txt')
|
||||
remove_list = [replace_double_consonants(x) for x in load_txt('txts/remove_list.txt')]
|
||||
|
|
@ -82,14 +144,80 @@ stop_words.remove('at')
|
|||
university_terms = [replace_double_consonants(x) for x in load_txt('txts/university_terms.txt')]
|
||||
city_names = [replace_double_consonants(x) for x in load_txt('txts/city_names.txt')]
|
||||
|
||||
# Pre-compile abbreviation cache for faster lookup (thousands of times faster)
|
||||
_abbr_univ_cache = {}
|
||||
for _city in city_names:
|
||||
_abbr_univ_cache[_city + " u"] = _city + " univer"
|
||||
_abbr_univ_cache["u " + _city] = "univer " + _city
|
||||
_abbr_univ_cache["tu " + _city] = "techn univer " + _city
|
||||
|
||||
def is_first(id, name):
|
||||
for quadruple in dix_name[name]:
|
||||
if quadruple['id'] == id:
|
||||
return quadruple['first']
|
||||
|
||||
|
||||
def get_candidates(country_list):
|
||||
if len(country_list) >0:
|
||||
cand = [dix_country_legalnames[country] for country in country_list if country in dix_country_legalnames]
|
||||
return list(set([item for sublist in cand for item in sublist]))
|
||||
else:
|
||||
return list(dix_org.keys())
|
||||
country_synonyms = {x: [x] for x in countries}
|
||||
country_synonyms["united states"] = ["united states", "u.s.a.", "usa", "usa.", "states"] + [x for x in us_states]
|
||||
country_synonyms["germany"] = ["germany", "deutschland"]
|
||||
country_synonyms["united kingdom"] = ["united kingdom", "u.k.", "uk", "uk.", "kingdom", "england"]
|
||||
country_synonyms["turkey"] = ["turkey", "turkiye", "cyprus"]
|
||||
country_synonyms["china"] = ["china", "prc", "chinese"]
|
||||
country_synonyms["ireland"] = ["eire", "ireland"]
|
||||
country_synonyms["south korea"] = ["south korea", "korea"]
|
||||
country_synonyms["new zealand"] = ["new zealand", "zealand"]
|
||||
country_synonyms["italy"] = ["italy", "milan", "rome", "italia", "pisa"]
|
||||
|
||||
# 1️⃣ Define equivalence classes (each set is a group of mutually equivalent countries)
|
||||
country_equivalence_groups = [
|
||||
{"united states", "u.s.a.", "usa", "usa.", "states","canada"},
|
||||
{"germany", "deutschland"},
|
||||
{"united kingdom", "u.k.", "uk", "uk.", "kingdom", "england","ireland", "eire"},
|
||||
{"turkey", "turkiye", "cyprus","greece", "macedonia"},
|
||||
{"china", "prc", "chinese"},
|
||||
{"south korea", "korea"},
|
||||
{"new zealand", "zealand"},
|
||||
]
|
||||
|
||||
|
||||
# 2️⃣ Build a symmetric lookup for all countries
|
||||
country_to_group = {}
|
||||
for group in country_equivalence_groups:
|
||||
for country in group:
|
||||
country_to_group[country] = group
|
||||
|
||||
|
||||
# 3️⃣ Define the function
|
||||
def get_candidates(country_list, key_list):
|
||||
# Candidate names from keys
|
||||
cand_sets = [dix_key_legalnames[k] for k in key_list if k in dix_key_legalnames]
|
||||
if not cand_sets:
|
||||
return set()
|
||||
cand_names = set.union(*cand_sets) if len(cand_sets) > 1 else cand_sets[0].copy()
|
||||
|
||||
# Remove empty/falsy country entries
|
||||
country_list = [c for c in country_list if c]
|
||||
|
||||
if country_list:
|
||||
# Expand countries using equivalence classes
|
||||
expanded_countries = {
|
||||
synonym
|
||||
for c in country_list
|
||||
for synonym in country_to_group.get(c, {c})
|
||||
}
|
||||
|
||||
# Collect legal names from expanded countries
|
||||
c_sets = [dix_country_legalnames[c] for c in expanded_countries if c in dix_country_legalnames]
|
||||
if not c_sets:
|
||||
return set()
|
||||
country_names = set.union(*c_sets) if len(c_sets) > 1 else c_sets[0]
|
||||
|
||||
return cand_names & country_names
|
||||
|
||||
return cand_names
|
||||
|
||||
|
||||
sp_all = [k for k in categ_dicts if categ_dicts[k] == 'Acronyms' or categ_dicts[k] == 'Specific']
|
||||
|
||||
|
||||
def is_contained(s, w):
|
||||
|
|
@ -110,6 +238,14 @@ def is_contained(s, w):
|
|||
return True # If all words from 's' are found in 'w', return True
|
||||
|
||||
|
||||
|
||||
def split_sub(s: str) -> str:
|
||||
# Add comma after certain word pairs
|
||||
pattern = r'\b((?:univer))\s+(department|faculty)\b'
|
||||
return re.sub(pattern, r'\1, \2', s, flags=re.IGNORECASE)
|
||||
|
||||
|
||||
|
||||
def starts_with_any(string, prefixes):
|
||||
"""
|
||||
Checks if the given string starts with any prefix from a list.
|
||||
|
|
@ -140,7 +276,10 @@ def remove_leading_numbers(s):
|
|||
return re.sub(r'^\d+', '', s)
|
||||
|
||||
def remove_multi_digit_numbers(text):
|
||||
return re.sub(r'\b\d{2,}\b', '', text).strip()
|
||||
if 'inserm' in text or 'cnrs' in text:
|
||||
return re.sub(r'\b\d{5,}\b', '', text).strip()
|
||||
else:
|
||||
return re.sub(r'\b\d{4,}\b', '', text).strip()
|
||||
|
||||
def remove_outer_parentheses(string):
|
||||
"""Remove outer parentheses from the string if they enclose the entire string."""
|
||||
|
|
@ -158,39 +297,42 @@ def replace_roman_numerals(text):
|
|||
|
||||
def insert_space_between_lower_and_upper(s):
|
||||
"""
|
||||
Inserts a space between a lowercase letter followed by an uppercase letter in a string.
|
||||
|
||||
Parameters:
|
||||
s (str): The input string.
|
||||
|
||||
Returns:
|
||||
str: The modified string with spaces inserted.
|
||||
Insert a space between a lowercase letter and a following uppercase letter,
|
||||
while protecting listed substrings (case-sensitive) and restoring them in lowercase.
|
||||
"""
|
||||
# Temporarily replace 'AstraZeneca' to prevent modification
|
||||
s = s.replace('gGmbH','gmbh')
|
||||
s = s.replace('AstraZeneca', 'ASTRAZENECA_TEMP')
|
||||
s = s.replace('BioNTech', 'BIONTECH_TEMP')
|
||||
s = s.replace('GlaxoSmithKline', 'GLAXO_TEMP')
|
||||
s = s.replace('GmbH', 'GMBH_TEMP')
|
||||
s = s.replace('gmbH', 'GMBH_TEMP')
|
||||
s = s.replace('gGmbH', 'GMBH_TEMP')
|
||||
protected = ['DePaul',
|
||||
'AstraZeneca',
|
||||
'BioNTech',
|
||||
'GlaxoSmithKline',
|
||||
'LifeWatch',
|
||||
'SoBigData',
|
||||
'GmbH',
|
||||
'gGmbH',
|
||||
'gmbH',
|
||||
'OpenAIRE',
|
||||
'LaserVision',
|
||||
'PhD',
|
||||
'HafenCity',
|
||||
'NamesforLife'
|
||||
]
|
||||
|
||||
# Replace protected words with placeholders mapping to their lowercase versions
|
||||
placeholders = {}
|
||||
for i, word in enumerate(protected):
|
||||
key = f"__PROT_{i}__"
|
||||
s = s.replace(word, key)
|
||||
placeholders[key] = word.lower()
|
||||
|
||||
|
||||
# Exclude cases where 'Mc' is followed by a capital letter
|
||||
modified_string = re.sub(r'(?<!Mc)([a-z])([A-Z])', r'\1 \2', s)
|
||||
|
||||
# Ensure no spaces are inserted within 'Mc' sequences
|
||||
modified_string = re.sub(r'(Mc) ([A-Z])', r'\1\2', modified_string)
|
||||
|
||||
# Restore 'AstraZeneca'
|
||||
modified_string = modified_string.replace('ASTRAZENECA_TEMP', 'AstraZeneca')
|
||||
modified_string = modified_string.replace('BIONTECH_TEMP', 'BioNTech')
|
||||
modified_string = modified_string.replace('GLAXO_TEMP', 'GlaxoSmithKline')
|
||||
modified_string = modified_string.replace('GMBH_TEMP', 'gmbh')
|
||||
# Add space between lowercase and uppercase (except after 'Mc')
|
||||
s = re.sub(r'(?<!Mc)([a-z])([A-Z])', r'\1 \2', s)
|
||||
s = re.sub(r'(Mc) ([A-Z])', r'\1\2', s)
|
||||
|
||||
# Restore placeholders to lowercase
|
||||
for key, lower_word in placeholders.items():
|
||||
s = s.replace(key, lower_word)
|
||||
|
||||
return s
|
||||
|
||||
|
||||
return modified_string
|
||||
|
||||
|
||||
|
||||
|
|
@ -210,27 +352,25 @@ def replace_acronyms(text):
|
|||
|
||||
|
||||
def replace_abbr_univ(token):
|
||||
for city in city_names:
|
||||
if token == city + " u":
|
||||
return city + " univer"
|
||||
elif token == "u " + city:
|
||||
return "univer " + city
|
||||
elif token == "tu " + city:
|
||||
return "technical univer " + city
|
||||
else:
|
||||
return token
|
||||
"""Optimized lookup using pre-compiled cache instead of linear iteration."""
|
||||
return _abbr_univ_cache.get(token, token)
|
||||
|
||||
|
||||
def remove_parentheses(text):
|
||||
return re.sub(r'\([^()]*\)', '', text)
|
||||
|
||||
L = ['univ', 'hospital', 'clinic', 'klinik', 'Univ', 'Hospital', 'Clinic', 'Klinik']
|
||||
acronyms = [x for x, cat in categ_dicts.items() if cat == 'Acronyms']
|
||||
L = ['univ', 'hospital', 'clinic', 'klinik', 'Univ', 'Hospital', 'Clinic', 'Klinik'] + [s.title() for s in countries] + countries + acronyms
|
||||
|
||||
word_pattern = "|".join(map(re.escape, L))
|
||||
|
||||
_RE_PAREN_REMOVE = re.compile(r'\((?![^)]*(' + word_pattern + r'))[^)]*\)')
|
||||
_RE_PAREN_REPLACE = re.compile(r'\(([^)]*(' + word_pattern + r')[^)]*)\)')
|
||||
|
||||
def process_parentheses(text):
|
||||
"""
|
||||
Processes parentheses in a given text by:
|
||||
1. Removing parentheses that do not contain any word from a the list L = ['univ', 'hospital', 'clinic', 'klinik', 'Univ', 'Hospital', 'Clinic', 'Klinik'].
|
||||
1. Removing parentheses that do not contain any word from the list L.
|
||||
2. Replacing parentheses with commas if they contain a word from the list.
|
||||
|
||||
Parameters:
|
||||
|
|
@ -239,20 +379,16 @@ def process_parentheses(text):
|
|||
Returns:
|
||||
str: The modified string after processing parentheses.
|
||||
"""
|
||||
text_lower = replace_double_consonants(text.lower())
|
||||
text_lower = _RE_PAREN_REMOVE.sub('', text_lower)
|
||||
text_lower = _RE_PAREN_REPLACE.sub(r', \1,', text_lower)
|
||||
|
||||
text = re.sub(r'\((?![^)]*(' + word_pattern + r'))[^)]*\)', '', text)
|
||||
|
||||
# Replace `(` with `,` and `)` with `,` if a word from L is inside
|
||||
text = re.sub(r'\(([^)]*(' + word_pattern + r')[^)]*)\)', r', \1,', text)
|
||||
|
||||
return text
|
||||
return text_lower
|
||||
|
||||
|
||||
|
||||
|
||||
def replace_comma_spaces(text):
|
||||
return text.replace(' ', ' ').replace(' , ', ', ')
|
||||
|
||||
def replace_underscore(text):
|
||||
if 'University-' in text and 'University-Hospital' not in text:
|
||||
return text.replace('-',',')
|
||||
|
|
@ -267,7 +403,45 @@ def fully_unescape(text):
|
|||
return new_text
|
||||
text = new_text
|
||||
|
||||
|
||||
|
||||
def replace_newlines_with_space(text: str, repl: str = " ") -> str:
|
||||
"""
|
||||
Replace many variants of newline/paragraph placeholders in `text`
|
||||
with `repl` (default: space), except:
|
||||
- '\r' (and '\\r') are replaced with ','
|
||||
|
||||
Then collapse repeated whitespace to a single space and trim ends.
|
||||
"""
|
||||
|
||||
if text is None:
|
||||
return text
|
||||
|
||||
text = html.unescape(text)
|
||||
|
||||
# Replace carriage returns with comma
|
||||
text = re.sub(r"(\\r|\r)", ",", text)
|
||||
|
||||
pattern = re.compile(
|
||||
r"""(
|
||||
\#r\#\#n\# | \#r\# | \#n\# |
|
||||
\^p | ¶ |
|
||||
<br\s*/?> |
|
||||
�*13; | �*10; |
|
||||
�*0d; | �*0a; |
|
||||
\\r\\n | \\n | \\u000a | \\u000d | \\x0a | \\x0d |
|
||||
\r\n | \n
|
||||
)""",
|
||||
flags=re.IGNORECASE | re.VERBOSE,
|
||||
)
|
||||
|
||||
cleaned = pattern.sub(repl, text)
|
||||
|
||||
cleaned = re.sub(r"\s+", " ", cleaned).strip()
|
||||
|
||||
return cleaned
|
||||
|
||||
def replace_newlines_with_space1(text: str, repl: str = " ") -> str:
|
||||
"""
|
||||
Replace many variants of newline/paragraph placeholders in `text`
|
||||
with `repl` (default: a single space), then collapse repeated
|
||||
|
|
@ -313,6 +487,72 @@ def replace_newlines_with_space(text: str, repl: str = " ") -> str:
|
|||
|
||||
return cleaned
|
||||
|
||||
def normalize_organization_names(value, university_terms):
|
||||
"""
|
||||
Normalize organization names by applying a series of regex substitutions
|
||||
and standardizations.
|
||||
|
||||
Args:
|
||||
value (str): Organization name substring.
|
||||
university_terms (list of str): List of terms indicating a university.
|
||||
|
||||
Returns:
|
||||
str: Normalized organization name.
|
||||
"""
|
||||
|
||||
# Replace dots with spaces and collapse multiple spaces
|
||||
modified_value = value.replace('.', ' ')
|
||||
modified_value = _RE_MULTI_SPACE.sub(' ', modified_value).strip()
|
||||
|
||||
# Protect compound university terms (e.g. universitatszahnklinik) with placeholders
|
||||
# so that plain "universitat" etc. still get shortened to "univer" in the same string.
|
||||
val_lower = modified_value.lower()
|
||||
placeholders = {}
|
||||
for i, term in enumerate(university_terms):
|
||||
t_low = term.lower()
|
||||
if (t_low.startswith('univer') or t_low.startswith('hospital')) and t_low in val_lower:
|
||||
key = f'__UNIV_{i}__'
|
||||
modified_value = re.sub(re.escape(term), key, modified_value, flags=re.IGNORECASE)
|
||||
placeholders[key] = term
|
||||
|
||||
modified_value = _RE_UNIVER.sub('univer', modified_value)
|
||||
|
||||
# General substitutions (all using pre-compiled patterns)
|
||||
modified_value = _RE_EUROP.sub('europ', modified_value)
|
||||
modified_value = _RE_INSTIT.sub('instit', modified_value)
|
||||
modified_value = _RE_HOPITAL.sub('hospital', modified_value)
|
||||
modified_value = _RE_HOSPITAL.sub('hospital', modified_value)
|
||||
modified_value = _RE_GEN_HOSPITAL.sub('hospital', modified_value)
|
||||
modified_value = _RE_GEN_UNI_HOSP.sub('univer hospital', modified_value)
|
||||
modified_value = _RE_SCIENCES.sub('science', modified_value)
|
||||
modified_value = _RE_SCIENZE.sub('science', modified_value)
|
||||
modified_value = _RE_SCIENZA.sub('science', modified_value)
|
||||
modified_value = _RE_LABORA.sub('labora', modified_value)
|
||||
modified_value = _RE_CENTRE.sub('center', modified_value)
|
||||
modified_value = _RE_CENTRUM.sub('center', modified_value)
|
||||
modified_value = _RE_MEDISCH.sub('medical', modified_value)
|
||||
modified_value = _RE_SAINT.sub('st', modified_value)
|
||||
modified_value = _RE_TRINITY.sub('trinity colege', modified_value)
|
||||
modified_value = _RE_TECHNISCHE.sub('technological', modified_value)
|
||||
modified_value = _RE_TEKNOLOGI.sub('technology', modified_value)
|
||||
modified_value = _RE_POLITEHNICA.sub('polytechnic', modified_value)
|
||||
modified_value = _RE_POLITE.sub('polytechnic', modified_value)
|
||||
modified_value = _RE_POLYTE.sub('polytechnic', modified_value)
|
||||
modified_value = _RE_PANEPIST.sub('univer', modified_value)
|
||||
modified_value = _RE_ARISTOT.sub('aristot', modified_value)
|
||||
modified_value = _RE_TECHN.sub('techn', modified_value)
|
||||
modified_value = _RE_CZECHOSLOVAK.sub('czech', modified_value)
|
||||
modified_value = _RE_MACAU.sub('macao', modified_value)
|
||||
modified_value = _RE_MUNCHEN.sub('munich', modified_value)
|
||||
modified_value = _RE_ORGANISATION.sub('organization', modified_value)
|
||||
modified_value = _RE_XI_AN.sub('xian', modified_value)
|
||||
|
||||
# Restore all protected compound terms (universit- and hospital- prefixed)
|
||||
for key, original in placeholders.items():
|
||||
modified_value = modified_value.replace(key, original)
|
||||
|
||||
return modified_value
|
||||
|
||||
def substrings_dict(string):
|
||||
"""
|
||||
Processes a given string by performing the following transformations:
|
||||
|
|
@ -330,10 +570,10 @@ def substrings_dict(string):
|
|||
dict: A dictionary where each key is an index, and the value is a processed substring.
|
||||
"""
|
||||
# print(0, string)
|
||||
for old, new in replacements.items():
|
||||
for old, new in get_replacements().items():
|
||||
string = string.replace(old, new)
|
||||
# split_strings = [s.strip() for s in re.split(r'[,;/]| – ', string) if s.strip()]
|
||||
split_strings = [replace_acronyms(s).strip() for s in re.split(r' - | – |[,;/:]', string) if s.strip()]
|
||||
|
||||
split_strings = [replace_acronyms(s).strip() for s in re.split(r' - | – |[,;/:|]', string) if s.strip()]
|
||||
|
||||
# Define a set of university-related terms for later use
|
||||
|
||||
|
|
@ -341,33 +581,9 @@ def substrings_dict(string):
|
|||
dict_string = {}
|
||||
index = 0
|
||||
|
||||
# print('split_strings',split_strings)
|
||||
|
||||
for value in split_strings:
|
||||
modified_value = value.replace('.', ' ')
|
||||
modified_value = re.sub(r'\s+', ' ', modified_value)
|
||||
|
||||
# Check if the substring contains any university-related terms
|
||||
if not any(term in modified_value.lower() for term in university_terms):
|
||||
# Apply regex substitutions for common patterns
|
||||
|
||||
modified_value = re.sub(r'univer\w*', 'univer', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'institu\w*', 'institu', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'hospital(?!s)\w*', 'hospital', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'labora\w*', 'labora', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'centre\b', 'center', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'centrum\b', 'center', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'\bsaint\b', 'st', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'\btrinity col\b', 'trinity colege', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'\btechnische\b', 'technological', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'\bteknologi\b', 'technology', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'\bpolitehnica\b', 'polytechnic', modified_value, flags=re.IGNORECASE)
|
||||
|
||||
modified_value = re.sub(r'\btechn\w*', 'techn', modified_value, flags=re.IGNORECASE)
|
||||
#modified_value = re.sub(r'techno\w*', 'techno', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'scien\w*', 'scien', modified_value, flags=re.IGNORECASE)
|
||||
|
||||
|
||||
modified_value = normalize_organization_names(value, university_terms)
|
||||
|
||||
# Add the modified substring to the dictionary
|
||||
|
||||
|
|
@ -375,20 +591,36 @@ def substrings_dict(string):
|
|||
index += 1
|
||||
|
||||
# Add the original substring to the dictionary
|
||||
# else:
|
||||
# dict_string[index] = value.lower().strip()
|
||||
# index += 1
|
||||
|
||||
|
||||
return dict_string
|
||||
|
||||
countries_set = set(countries)
|
||||
key_words_set = set(key_words)
|
||||
|
||||
def clean_string_ror(input_string):
|
||||
|
||||
input_string = replace_underscore(replace_comma_spaces(replace_double_consonants(unidecode(process_parentheses(fully_unescape(input_string.replace("’","'").replace(" ́e","e").replace("'s", "s").replace("'", " "))))))).strip()
|
||||
def split_country(text):
|
||||
parts = text.split(' ')
|
||||
|
||||
if len(parts) < 2:
|
||||
return text
|
||||
|
||||
if parts[-1] in countries_set and not parts[-2].startswith('univ'):
|
||||
return " ".join(parts[:-1]) + ", " + parts[-1]
|
||||
|
||||
return text
|
||||
|
||||
|
||||
|
||||
def clean_string_lucky(input_string):
|
||||
|
||||
input_string = replace_underscore(replace_comma_spaces(replace_double_consonants(unidecode(process_parentheses(fully_unescape(input_string.replace("’","'").replace(" ́e","e").replace("'s", "s").replace("'", "").replace('"', ' '))))))).strip()
|
||||
|
||||
result = remove_stop_words(replace_roman_numerals(input_string.lower()))
|
||||
result = result.replace(' and ',' ')
|
||||
result = result.replace('general hospital', 'hospital')
|
||||
|
||||
for old, new in get_replacements().items():
|
||||
result = result.replace(old, new)
|
||||
|
||||
# Remove characters that are not from the Latin alphabet, or allowed punctuation
|
||||
result = remove_multi_digit_numbers(replace_comma_spaces(re.sub(r'[^a-zA-Z0-9\s,;/:.\-\—]', '', result).strip()))
|
||||
|
|
@ -398,56 +630,52 @@ def clean_string_ror(input_string):
|
|||
result = result.replace(':',' ').replace(';',' ').replace('-',' ').replace('—',' ').replace(',',' ')
|
||||
# Replace consecutive whitespace with a single space
|
||||
|
||||
|
||||
result = replace_acronyms(result).replace('.', ' ')
|
||||
result = normalize_organization_names(result, university_terms)
|
||||
|
||||
return result.strip()
|
||||
|
||||
|
||||
def clean_string_ror(input_string):
|
||||
|
||||
input_string = replace_underscore(replace_comma_spaces(replace_double_consonants(unidecode(remove_parentheses(fully_unescape(input_string.replace("’","'").replace(" ́e","e").replace("'s", "s").replace("'", "").replace('"', ' '))))))).strip()
|
||||
|
||||
result = remove_stop_words(replace_roman_numerals(input_string.lower()))
|
||||
result = result.replace(' and ',' ')
|
||||
result = result.replace('general hospital', 'hospital')
|
||||
|
||||
for old, new in get_replacements().items():
|
||||
result = result.replace(old, new)
|
||||
|
||||
# Remove characters that are not from the Latin alphabet, or allowed punctuation
|
||||
result = remove_multi_digit_numbers(replace_comma_spaces(re.sub(r'[^a-zA-Z0-9\s,;/:.\-\—]', '', result).strip()))
|
||||
|
||||
# Restore the " - " sequence from the placeholder
|
||||
#result = result.replace(placeholder, " – ")
|
||||
result = result.replace(':',' ').replace(';',' ').replace('-',' ').replace('—',' ').replace(',',' ')
|
||||
# Replace consecutive whitespace with a single space
|
||||
|
||||
university_terms = {'universitatsklinikum', 'universitatskinderklinik',
|
||||
'universitatspital', 'universitatskliniken', 'universitetshospital',
|
||||
'universitatsmedizin', 'universitatsbibliothek','universitatszahnklinik'
|
||||
}
|
||||
|
||||
result = replace_acronyms(result).replace('.', ' ')
|
||||
result = re.sub(r'\s+', ' ', result)
|
||||
result = normalize_organization_names(result, university_terms)
|
||||
|
||||
# Replace consecutive whitespace with a single space
|
||||
if not any(term in result.lower() for term in university_terms):
|
||||
|
||||
result = re.sub(r'universi\w*', 'univer', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'\bsaint\b', 'st', result,flags=re.IGNORECASE)
|
||||
result = re.sub(r'institu\w*', 'institu', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'labora\w*', 'labora', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'centre\b', 'center', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'centrum\b', 'center', result, flags=re.IGNORECASE)
|
||||
|
||||
result = re.sub(r'hopital\b', 'hospital', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'hospital(?!s)\w*', 'hospital', result, flags=re.IGNORECASE)
|
||||
|
||||
#result = re.sub(r'centro\b', 'center', result, flags=re.IGNORECASE)
|
||||
|
||||
result = re.sub(r'\btechnische\b', 'technological', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'\bteknologi\b', 'technological', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'\bpolitehnica\b', 'polytechnic', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'czechoslovak\b', 'czech', result, flags=re.IGNORECASE)
|
||||
|
||||
result = re.sub(r'\btechn\w*', 'techn', result, flags=re.IGNORECASE)
|
||||
# result = re.sub(r'techno\w*', 'techno', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'scien\w*', 'scien', result, flags=re.IGNORECASE)
|
||||
# result = re.sub(r'\bsaint\b', 'st', result, flags=re.IGNORECASE)
|
||||
|
||||
return result.strip()
|
||||
|
||||
def clean_string(input_string):
|
||||
input_string = replace_underscore(replace_comma_spaces(unidecode(process_parentheses(fully_unescape(replace_newlines_with_space(input_string).replace("P.O. Box","").replace("’","'").replace(" ́e","e").replace("'s", "s").replace("'", " ")))))).strip()
|
||||
input_string = replace_underscore(replace_comma_spaces(unidecode(process_parentheses(insert_space_between_lower_and_upper(fully_unescape(replace_newlines_with_space(input_string).replace("P.O. Box","").replace("’","'").replace(" ́e","e").replace("'s", "s").replace('"', ' '))))))).strip()
|
||||
|
||||
# result = re.sub(r'(?<! )[–—-](?! )', ' ', input_string)
|
||||
|
||||
# print('h',input_string)
|
||||
|
||||
result = remove_stop_words(replace_double_consonants(replace_roman_numerals(insert_space_between_lower_and_upper(input_string).lower())))
|
||||
result = remove_stop_words(replace_double_consonants(replace_roman_numerals((input_string).lower().replace("'", ""))))
|
||||
|
||||
|
||||
# Remove characters that are not from the Latin alphabet, or allowed punctuation
|
||||
result = remove_multi_digit_numbers(replace_comma_spaces(re.sub(r'[^a-zA-Z0-9\s,;/:.\-\—]', '', result).strip()))
|
||||
|
||||
result = remove_multi_digit_numbers(replace_comma_spaces(re.sub(r'[^a-zA-Z0-9\s,;/:|.\-\—]', '', result).strip()))
|
||||
#result = normalize_organization_names(result, university_terms)
|
||||
|
||||
|
||||
# Restore the " - " sequence from the placeholder
|
||||
#result = result.replace(placeholder, " – ")
|
||||
|
|
@ -457,43 +685,46 @@ def clean_string(input_string):
|
|||
|
||||
#result = replace_roman_numerals(remove_stop_words(insert_space_between_lower_and_upper(result).lower()))
|
||||
|
||||
return result.strip() # Strip leading/trailing spaces
|
||||
|
||||
return result.strip() # Strip leading/trailing spaces
|
||||
return split_country(result.strip()) # Strip leading/trailing spaces
|
||||
|
||||
def description(aff_string):
|
||||
aff_string = aff_string.replace('turkiye', 'turkey')
|
||||
aff_string = aff_string.replace('turkiye', 'turkey').lower()
|
||||
aff_string = aff_string.replace('kirgizistan', 'kyrgyzstan')
|
||||
|
||||
aff_string = aff_string.replace('u.s.a.', 'usa').replace('u.k.', 'uk').replace('macau','macao')
|
||||
descr = []
|
||||
countries_ = []
|
||||
words = re.split(r'[ ,;:/]+', aff_string)
|
||||
# words = [word.strip() for word in re.split(r'[,;:]+', aff_string) if word.strip()]
|
||||
words = re.split(r'[ ,;:/.-]', aff_string)
|
||||
|
||||
has_multiword_state = any(
|
||||
state in aff_string
|
||||
for state in ('new york', 'new hampshire', 'new jersey', 'new mexico')
|
||||
)
|
||||
|
||||
for w in words:
|
||||
# if w in city_names:
|
||||
# descr.append('city')
|
||||
w = re.sub(r'[^A-Za-z\s]', '', w)
|
||||
|
||||
if replace_acronyms(w) in countries:
|
||||
if replace_acronyms(w) in countries_set:
|
||||
descr.append('country')
|
||||
countries_.append(w)
|
||||
|
||||
if replace_acronyms(w) in us_states:
|
||||
|
||||
if (
|
||||
replace_acronyms(w) in us_states
|
||||
or has_multiword_state
|
||||
):
|
||||
descr.append('country')
|
||||
countries_.append('usa')
|
||||
|
||||
elif w in ['univer', 'institu', 'hospital', 'labora']:
|
||||
|
||||
countries_.append('mexico')
|
||||
|
||||
elif w in ['univer', 'instit', 'hospital', 'labora', 'colege', 'foundation'] or w in sp_all:
|
||||
descr.append('basic_key')
|
||||
elif w == 'and':
|
||||
descr.append('and')
|
||||
|
||||
elif w in key_words and categ_dicts[w] == 'Specific':
|
||||
elif w in key_words_set and categ_dicts[w] == 'Specific':
|
||||
descr.append('basic_key')
|
||||
# elif w in key_words:
|
||||
# descr.append('key')
|
||||
else:
|
||||
descr.append('other') # Optional: label words that don’t fit any category
|
||||
descr.append('other')
|
||||
|
||||
return [descr, countries_]
|
||||
|
||||
|
|
@ -531,12 +762,12 @@ def split_and(string):
|
|||
tok_no_sl1 = ' '.join(token.replace('-', ' ').split())
|
||||
tok_no_sl2 = ' '.join(token.replace('—', ' ').split())
|
||||
tok_no = ' '.join(token.replace(' and ', ' ').replace(' at ', ' ').replace(' an ', ' ').replace('-', ' ').replace('—', ' ').split())
|
||||
if tok_no in dix_org:
|
||||
if tok_no in dix_name:
|
||||
token = tok_no
|
||||
|
||||
|
||||
else:
|
||||
if tok_no_and not in dix_org:
|
||||
if tok_no_and not in dix_name:
|
||||
# Store once instead of calling multiple times
|
||||
|
||||
if is_subsequence(replace_sequence, token_description):# and token.split(' and ', ' ') not in dix_org:
|
||||
|
|
@ -547,20 +778,27 @@ def split_and(string):
|
|||
else:
|
||||
token = tok_no_and
|
||||
|
||||
if tok_no_at not in dix_org:
|
||||
if tok_no_at not in dix_name:
|
||||
token = ' '.join(token.replace(' at ', ', ').split())
|
||||
else:
|
||||
token = tok_no_at
|
||||
|
||||
if tok_no_an not in dix_org:
|
||||
if tok_no_an not in dix_name:
|
||||
token = ' '.join(token.replace(' an ', ', ').split())
|
||||
else:
|
||||
token = tok_no_an
|
||||
if tok_no_sl1 not in dix_org:
|
||||
token = ' '.join(token.replace('-', ',').split())
|
||||
if tok_no_sl1 not in dix_name:
|
||||
parts = re.split(r'\s*-\s*', token)
|
||||
if len(parts) >= 2: # there is a dash in the current token
|
||||
if ('basic_key' in description(parts[0])[0]
|
||||
and 'basic_key' in description(parts[-1])[0]):
|
||||
token = re.sub(r'\s*-\s*', ', ', token)
|
||||
else:
|
||||
token = tok_no_sl1
|
||||
# else: no dash in current token, leave it unchanged
|
||||
else:
|
||||
token = tok_no_sl1
|
||||
if tok_no_sl2 not in dix_org:
|
||||
if tok_no_sl2 not in dix_name:
|
||||
token = ' '.join(token.replace('—', ',').split())
|
||||
else:
|
||||
token = tok_no_sl2
|
||||
|
|
@ -575,12 +813,13 @@ def split_and(string):
|
|||
def reduce(light_aff):
|
||||
|
||||
aff_no_symbols_d = substrings_dict(light_aff)
|
||||
# print('aff_no_symbols_d',aff_no_symbols_d)
|
||||
substring_list = list(aff_no_symbols_d.values())
|
||||
#light_aff_final = ', '.join((substring_list))
|
||||
# print('h', substring_list)
|
||||
light_aff_final = ', '.join((substring_list))
|
||||
# print('h', substring_list)
|
||||
light_aff_final = split_and(', '.join((substring_list)))
|
||||
# print('th', light_aff_final)
|
||||
return light_aff_final
|
||||
# print('th', light_aff_final)
|
||||
return split_sub(light_aff_final)
|
||||
|
||||
|
||||
def unique_subset(L, D):
|
||||
|
|
@ -598,55 +837,88 @@ def unique_subset(L, D):
|
|||
|
||||
def str_radius_u(string, radius_u):
|
||||
str_list = string.split()
|
||||
indices = []
|
||||
n = len(str_list)
|
||||
result = []
|
||||
|
||||
for i, x in enumerate(str_list):
|
||||
if 'univer' in x:
|
||||
indices.append(i)
|
||||
|
||||
for r0 in indices:
|
||||
lmin =max(0,r0-radius_u)
|
||||
lmax =min(r0+radius_u, len(str_list))
|
||||
s = str_list[lmin:lmax+1]
|
||||
|
||||
result.append(' '.join(s))
|
||||
if 'univer' in x or 'colege' in x:
|
||||
s = str_list[max(0, i - radius_u): min(i + radius_u + 1, n)]
|
||||
result.append(' '.join(s))
|
||||
|
||||
return result
|
||||
return result
|
||||
|
||||
|
||||
sp_specific = [k for k in categ_dicts if categ_dicts[k] == 'Specific' and ' ' in k]
|
||||
|
||||
# only_specific = [k for k in categ_dicts if categ_dicts[k] == 'Specific']
|
||||
# #
|
||||
# def str_radius_spec(string):
|
||||
# spec = False
|
||||
# for x in only_specific:
|
||||
# if x in string:# or categ_dicts[x] == 'Acronyms':
|
||||
# spec = True
|
||||
# return x
|
||||
# if spec ==False:
|
||||
# return string
|
||||
|
||||
def str_radius_spec(string):
|
||||
spec = False
|
||||
for x in string.split():
|
||||
try:
|
||||
if categ_dicts[x] == 'Specific':# or categ_dicts[x] == 'Acronyms':
|
||||
spec = True
|
||||
return x
|
||||
except:
|
||||
pass
|
||||
if spec == False:
|
||||
return string
|
||||
|
||||
for x in sp_specific:
|
||||
if x in string:# or categ_dicts[x] == 'Acronyms':
|
||||
spec = True
|
||||
return x
|
||||
if spec ==False:
|
||||
return string
|
||||
|
||||
|
||||
def shorten_keywords0(affiliations_simple, radius_u):
|
||||
affiliations_simple_n = []
|
||||
|
||||
for aff in affiliations_simple:
|
||||
# print('check aff', aff)
|
||||
if aff in dix_name:
|
||||
affiliations_simple_n.append(aff)
|
||||
|
||||
elif 'univer' in aff:
|
||||
affiliations_simple_n.extend(str_radius_u(aff, radius_u))
|
||||
elif 'colege' in aff:
|
||||
|
||||
affiliations_simple_n.extend(str_radius_u(aff, radius_u))
|
||||
|
||||
elif 'research' in aff:
|
||||
affiliations_simple_n.append(aff)
|
||||
|
||||
affiliations_simple_n.append(str_radius_spec(aff))
|
||||
|
||||
return list(set(affiliations_simple_n))
|
||||
|
||||
def shorten_keywords(affiliations_simple, radius_u):
|
||||
affiliations_simple_n = []
|
||||
|
||||
for aff in affiliations_simple:
|
||||
# print('check aff', aff)
|
||||
if aff in dix_org:
|
||||
# print('check aff', aff)
|
||||
|
||||
if aff in dix_name:
|
||||
affiliations_simple_n.append(aff)
|
||||
|
||||
|
||||
|
||||
|
||||
elif 'univer' in aff:
|
||||
affiliations_simple_n.extend(str_radius_u(aff, radius_u))
|
||||
|
||||
elif 'colege' in aff:
|
||||
|
||||
affiliations_simple_n.extend(str_radius_u(aff, radius_u))
|
||||
|
||||
elif 'research' in aff:
|
||||
affiliations_simple_n.append(aff)
|
||||
|
||||
else:
|
||||
affiliations_simple_n.append(str_radius_spec(aff))
|
||||
|
||||
|
||||
return affiliations_simple_n
|
||||
|
||||
for x in aff.split():
|
||||
if categ_dicts.get(x) in {'Acronyms', 'Specific'}:
|
||||
affiliations_simple_n.append(x)
|
||||
|
||||
affiliations_simple_n.append(str_radius_spec(aff))
|
||||
|
||||
return list(set(affiliations_simple_n))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,460 +1,378 @@
|
|||
import math
|
||||
import re
|
||||
from collections import Counter
|
||||
import numpy as np
|
||||
import Levenshtein
|
||||
from affro.helpers.functions import *
|
||||
from affro.helpers.create_input import *
|
||||
|
||||
_TOK_REGEX = re.compile(r'(?u)\b\w\w+\b')
|
||||
|
||||
|
||||
specific = [k for k in categ_dicts if categ_dicts[k] == 'Specific' or categ_dicts[k] == 'Acronyms']
|
||||
|
||||
|
||||
# print('HERE', len(dix_org))
|
||||
# print('HERE_city', len(dix_city))
|
||||
# print('HERE_country', len(dix_country))
|
||||
|
||||
INSTIT_PHRASES = {
|
||||
'instit techn', 'pasteur instit', 'instit pasteur','educational instit',
|
||||
'instit scien', 'machinery instit', 'national instit', 'research instit',
|
||||
'federal instit', 'instit biomedical', 'techn instit', 'instit aplied', 'fraunhofer instit', 'max planck instit',
|
||||
'leibniz instit', 'herie instit', 'german instit', 'forth instit'
|
||||
}
|
||||
|
||||
def index_multiple_matchings(pairs):
|
||||
"""
|
||||
Index multiple matchings by creating a dictionary mapping the first element
|
||||
of each pair group to its length.
|
||||
"""
|
||||
d = {}
|
||||
for p in pairs:
|
||||
d[p[0][0]] = len(p)
|
||||
|
||||
return d
|
||||
|
||||
def keep_highest_url(lst):
|
||||
best = {}
|
||||
|
||||
for item in lst:
|
||||
name, score, url = item
|
||||
if name not in best or url > best[name][2]: # Keep the highest URL
|
||||
best[name] = item # Store the full entry
|
||||
|
||||
return list(best.values()) # Convert dictionary values back to list
|
||||
|
||||
def find_candidate(keyword, k, dix, simU, simG, candidates_, limit):
|
||||
|
||||
vectorizer = CountVectorizer()
|
||||
|
||||
def find_candidate(keyword, k, dix, simU, simG, candidates_, limit):
|
||||
"""
|
||||
OPTIMIZED VERSION:
|
||||
1. Pre-splits keyword and pre-computes keyword word checks outside candidate loop.
|
||||
2. Uses longest-token fast rejection before running full containment check.
|
||||
3. Defers candidate attribute checks ('univ', 'instit', etc.) until after containment match.
|
||||
4. Pure-Python/NumPy term-frequency cosine similarity (replaces repeated CountVectorizer allocations).
|
||||
5. Preserves 100% numerical and logical parity with original scikit-learn implementation.
|
||||
"""
|
||||
similar_k = []
|
||||
pairs_k = []
|
||||
total_pairs = 0
|
||||
# if keyword in dix_org:
|
||||
# print('lucky')
|
||||
# pairs_k.append((keyword,keyword,1,dix_org[keyword], dix_id_country[dix_org[keyword]]))
|
||||
|
||||
# Pre-compute keyword checks
|
||||
keyword_has_univ = 'univ' in keyword
|
||||
keyword_has_school = 'school' in keyword or 'hochschule' in keyword
|
||||
keyword_has_instit = any(phrase in keyword for phrase in INSTIT_PHRASES) or ('state' in keyword and 'instit' in keyword)
|
||||
keyword_has_hosp = 'hospital' in keyword
|
||||
|
||||
kw_words = keyword.split()
|
||||
if not kw_words:
|
||||
return []
|
||||
|
||||
# Sort keyword words by descending length for fast rejection on Case 1
|
||||
kw_words_by_len = sorted(kw_words, key=len, reverse=True)
|
||||
longest_kw_word = kw_words_by_len[0]
|
||||
remaining_kw_words = kw_words_by_len[1:]
|
||||
|
||||
# Pre-tokenize keyword for exact cosine similarity
|
||||
kw_tokens = _TOK_REGEX.findall(keyword.lower())
|
||||
kw_counts = Counter(kw_tokens)
|
||||
kw_norm_sq = float(sum(c * c for c in kw_counts.values()))
|
||||
kw_inv_norm = np.float64(1.0) / np.sqrt(np.float64(kw_norm_sq)) if kw_norm_sq > 0 else np.float64(0.0)
|
||||
|
||||
min_sim = min(simU, simG)
|
||||
|
||||
for x in candidates_:
|
||||
# print('keyword', keyword)
|
||||
# Early exit
|
||||
if total_pairs >= limit:
|
||||
break
|
||||
|
||||
if is_contained(keyword, x):
|
||||
# print(0,x,total_pairs)
|
||||
# ------------------------------------
|
||||
# Fast containment check
|
||||
# ------------------------------------
|
||||
matched_case = 0
|
||||
|
||||
x_vector = vectorizer.fit_transform([x]).toarray()
|
||||
keyword_vector = vectorizer.transform([keyword]).toarray()
|
||||
# Case 1: keyword contained in x (all words of keyword must be substrings in x)
|
||||
if longest_kw_word in x:
|
||||
all_in_x = True
|
||||
for w in remaining_kw_words:
|
||||
if w not in x:
|
||||
all_in_x = False
|
||||
break
|
||||
if all_in_x:
|
||||
matched_case = 1
|
||||
|
||||
# Compute similarity between the vectors
|
||||
similarity = cosine_similarity(x_vector, keyword_vector)[0][0]
|
||||
if similarity > min(simU, simG):
|
||||
if ('univ' in keyword and 'univ' in x) and similarity > simU:
|
||||
# Case 2: x contained in keyword (all words of x must be substrings in keyword)
|
||||
if matched_case == 0:
|
||||
x_words = x.split()
|
||||
if x_words:
|
||||
all_in_kw = True
|
||||
for w in x_words:
|
||||
if w not in keyword:
|
||||
all_in_kw = False
|
||||
break
|
||||
if all_in_kw:
|
||||
matched_case = 2
|
||||
|
||||
if matched_case == 0:
|
||||
continue
|
||||
|
||||
# Candidate attribute checks (only needed when a containment match occurred)
|
||||
x_has_univ = 'univ' in x
|
||||
x_has_school = 'school' in x or 'hochschule' in x
|
||||
x_has_instit = 'instit' in x
|
||||
x_has_hosp = 'hospital' in x
|
||||
|
||||
if matched_case == 1:
|
||||
# ------------------------------------
|
||||
# Case 1: keyword contained in x
|
||||
# In original: CountVectorizer fits on [x], transforms [keyword].
|
||||
# ------------------------------------
|
||||
x_tokens = _TOK_REGEX.findall(x.lower())
|
||||
x_counts = Counter(x_tokens)
|
||||
dot = 0
|
||||
sub_kw_norm_sq = 0
|
||||
for w, c_kw in kw_counts.items():
|
||||
if w in x_counts:
|
||||
dot += c_kw * x_counts[w]
|
||||
sub_kw_norm_sq += c_kw * c_kw
|
||||
|
||||
if dot == 0 or sub_kw_norm_sq == 0 or kw_inv_norm == 0:
|
||||
similarity = np.float64(0.0)
|
||||
else:
|
||||
x_norm_sq = float(sum(c * c for c in x_counts.values()))
|
||||
similarity = (
|
||||
np.float64(dot)
|
||||
* (np.float64(1.0) / np.sqrt(np.float64(x_norm_sq)))
|
||||
* (np.float64(1.0) / np.sqrt(np.float64(sub_kw_norm_sq)))
|
||||
)
|
||||
|
||||
if similarity > min_sim:
|
||||
should_add = False
|
||||
if x_has_univ and not keyword_has_univ:
|
||||
pass
|
||||
elif ((keyword_has_univ and x_has_univ) or (keyword_has_school and x_has_school) or (keyword_has_hosp and x_has_hosp)) and similarity > simU:
|
||||
should_add = True
|
||||
elif (keyword_has_instit and x_has_instit) > simU:
|
||||
should_add = True
|
||||
elif (not keyword_has_univ and not x_has_univ) and (not keyword_has_school and not x_has_school) and (not keyword_has_instit and not x_has_instit) and (not keyword_has_hosp and not x_has_hosp) and similarity > simG:
|
||||
should_add = True
|
||||
|
||||
if should_add:
|
||||
similar_k.append(similarity)
|
||||
pairs_k.append((keyword,x,similarity,dix_org[x], dix_id_country[dix_org[x]]))
|
||||
total_pairs += 1 # Track total number of pairs
|
||||
|
||||
pairs_k.append((keyword, x, similarity))
|
||||
total_pairs += 1
|
||||
|
||||
if k not in dix:
|
||||
dix[k] = [x]
|
||||
else:
|
||||
dix[k].append(x)
|
||||
|
||||
|
||||
elif (not 'univ'in keyword and not 'univ' in x) and similarity > simG:
|
||||
# print('pass', keyword, x, similarity)
|
||||
similar_k.append(similarity)
|
||||
pairs_k.append((keyword,x,similarity,dix_org[x], dix_id_country[dix_org[x]]))
|
||||
total_pairs += 1 # Track total number of pairs
|
||||
|
||||
elif matched_case == 2:
|
||||
# ------------------------------------
|
||||
# Case 2: x contained in keyword
|
||||
# In original: CountVectorizer fits on [keyword], transforms [x].
|
||||
# ------------------------------------
|
||||
if (
|
||||
(keyword_has_univ and x_has_univ)
|
||||
or (not keyword_has_univ and not x_has_univ)
|
||||
or (keyword_has_school and x_has_school)
|
||||
or (not keyword_has_school and not x_has_school)
|
||||
or (keyword_has_instit and x_has_instit)
|
||||
or (not keyword_has_instit and not x_has_instit)
|
||||
or (keyword_has_hosp and x_has_hosp)
|
||||
or (not keyword_has_hosp and not x_has_hosp)
|
||||
):
|
||||
x_tokens = _TOK_REGEX.findall(x.lower())
|
||||
x_counts = Counter(x_tokens)
|
||||
dot = 0
|
||||
sub_x_norm_sq = 0
|
||||
for w, c_x in x_counts.items():
|
||||
if w in kw_counts:
|
||||
dot += c_x * kw_counts[w]
|
||||
sub_x_norm_sq += c_x * c_x
|
||||
|
||||
if dot == 0 or sub_x_norm_sq == 0 or kw_inv_norm == 0:
|
||||
similarity = np.float64(0.0)
|
||||
else:
|
||||
similarity = np.float64(dot) * kw_inv_norm * (np.float64(1.0) / np.sqrt(np.float64(sub_x_norm_sq)))
|
||||
|
||||
if (
|
||||
(keyword_has_univ and x_has_univ)
|
||||
or (keyword_has_school and x_has_school)
|
||||
or (keyword_has_hosp and x_has_hosp)
|
||||
):
|
||||
threshold = simU
|
||||
elif keyword_has_instit and x_has_instit:
|
||||
threshold = simU
|
||||
else:
|
||||
threshold = simG
|
||||
|
||||
if similarity > threshold:
|
||||
similar_k.append(similarity)
|
||||
pairs_k.append((keyword, x, similarity))
|
||||
total_pairs += 1
|
||||
|
||||
if k not in dix:
|
||||
dix[k] = [x]
|
||||
else:
|
||||
dix[k].append(x)
|
||||
|
||||
|
||||
|
||||
elif is_contained(x, keyword):
|
||||
# print(0.5,x,total_pairs)
|
||||
if ('univ'in keyword and 'univ' in x):
|
||||
# print(1,x,total_pairs)
|
||||
|
||||
|
||||
keyword_vector = vectorizer.fit_transform([keyword]).toarray()
|
||||
x_vector = vectorizer.transform([x]).toarray()
|
||||
|
||||
# Compute similarity between the vectors
|
||||
similarity = cosine_similarity(keyword_vector, x_vector)[0][0]
|
||||
if similarity > simU: #max(0.82,sim):
|
||||
similar_k.append(similarity)
|
||||
pairs_k.append((keyword,x,similarity,dix_org[x], dix_id_country[dix_org[x]]))
|
||||
total_pairs += 1 # Track total number of pairs
|
||||
|
||||
if k not in dix:
|
||||
dix[k] = [x]
|
||||
else:
|
||||
dix[k].append(x)
|
||||
|
||||
|
||||
|
||||
elif not 'univ' in keyword and not 'univ' in x:
|
||||
|
||||
keyword_vector = vectorizer.fit_transform([keyword]).toarray()
|
||||
x_vector = vectorizer.transform([x]).toarray()
|
||||
|
||||
# Compute similarity between the vectors
|
||||
similarity = cosine_similarity(keyword_vector, x_vector)[0][0]
|
||||
if similarity > simG: #max(0.82,sim):
|
||||
similar_k.append(similarity)
|
||||
pairs_k.append((keyword,x,similarity,dix_org[x], dix_id_country[dix_org[x]]))
|
||||
total_pairs += 1 # Track total number of pairs
|
||||
|
||||
if k not in dix:
|
||||
dix[k] = [x]
|
||||
else:
|
||||
dix[k].append(x)
|
||||
# total_pairs += len(pairs_k) # Track total number of pairs
|
||||
if total_pairs >= limit: # Stop if we reach
|
||||
return []
|
||||
|
||||
return pairs_k
|
||||
return pairs_k
|
||||
|
||||
|
||||
|
||||
def best_sim_score(clean_aff, light_raw, candidate_num, pairs_list, multi, simU, simG):
|
||||
"""
|
||||
Finds the best match between a keyword (clean_aff) and legal names from the PID database.
|
||||
"""
|
||||
OPTIMIZED VERSION with following improvements:
|
||||
1. Single-pass unique max confidence detection
|
||||
2. Set-based duplicate tracking instead of list membership
|
||||
3. Single CountVectorizer instance (reused throughout)
|
||||
4. Pre-computed light_raw_vector (computed once, reused)
|
||||
5. Early detection of unique highest confidence match
|
||||
6. FIX: University limiter moved outside pair_group loop to prevent
|
||||
non-university groups from zeroing out valid university matches
|
||||
|
||||
Expected speedup: 5-15x depending on data size
|
||||
"""
|
||||
# print('best_sim')
|
||||
max_conf = -1
|
||||
max_pair = None
|
||||
|
||||
# First, check priority pairs
|
||||
priority_pairs = []
|
||||
for group in pairs_list:
|
||||
# print(group)
|
||||
for _, org, conf in group:
|
||||
# print(_)
|
||||
if conf > 0.98:
|
||||
# print('org good',org)
|
||||
priority_pairs.append((org, conf))
|
||||
elif org in _ or _ in org:
|
||||
# print('org', org)
|
||||
priority_pairs.append((org, conf))
|
||||
|
||||
# Check priority case
|
||||
if priority_pairs:
|
||||
# print(priority_pairs)
|
||||
max_conf = max(conf for _, conf in priority_pairs)
|
||||
winners = [(org, conf) for org, conf in priority_pairs if conf == max_conf]
|
||||
# print(winners)
|
||||
if len(winners) == 1:
|
||||
return [list(winners[0])]
|
||||
|
||||
# Fallback: find unique max across all pairs
|
||||
max_conf = float("-inf")
|
||||
# print('max_conf', max_conf)
|
||||
max_pair = None
|
||||
max_count = 0
|
||||
# print('pairs_list', pairs_list)
|
||||
for group in pairs_list:
|
||||
# print(group)
|
||||
for _, org, conf in group:
|
||||
# print('org', org, 'conf', conf, 'max_conf', max_conf)
|
||||
if conf > max_conf:
|
||||
|
||||
max_conf = conf
|
||||
max_pair = [org, conf]
|
||||
max_count = 1
|
||||
elif conf == max_conf:
|
||||
max_count += 1
|
||||
|
||||
if max_pair and max_count == 1:
|
||||
return [max_pair]
|
||||
|
||||
# ============================================================
|
||||
# Create vectorizer once, reuse for all computations
|
||||
# ============================================================
|
||||
|
||||
# print('No unique max confidence found, proceeding with similarity checks...')
|
||||
vectorizer = CountVectorizer()
|
||||
univ_num = light_raw.lower().count('univ')
|
||||
|
||||
# Pre-compute light_raw_vector once
|
||||
light_raw_vector = None
|
||||
|
||||
result = []
|
||||
best = []
|
||||
|
||||
|
||||
# Use set for duplicate tracking (O(1) instead of O(n))
|
||||
seen_orgs = set()
|
||||
|
||||
for pair_group in pairs_list:
|
||||
|
||||
best_j = []
|
||||
affil = pair_group[0][0]
|
||||
num_uni_p = affil.count('univ')
|
||||
|
||||
# print('AFFIL', affil)
|
||||
num_uni_p = affil.count('univ')
|
||||
|
||||
for p in pair_group:
|
||||
# print('Processing pair:', p)
|
||||
organization, confidence = p[1], p[2]
|
||||
|
||||
# Skip duplicates
|
||||
if [organization, confidence] in result:
|
||||
continue
|
||||
org_lower = organization.lower()
|
||||
org_has_univ = 'univ' in org_lower
|
||||
|
||||
# Set-based duplicate check (O(1) lookup)
|
||||
org_key = (organization, confidence)
|
||||
if org_key in seen_orgs:
|
||||
continue
|
||||
seen_orgs.add(org_key)
|
||||
# print('multi[p[0]]',multi[p[0]])
|
||||
# Check similarity conditions
|
||||
if multi[p[0]] == 1:
|
||||
if 'univ' in organization.lower() and confidence > simU:
|
||||
# print('multi 1:', organization, confidence)
|
||||
if org_has_univ and confidence > simU:
|
||||
result.append([organization, confidence])
|
||||
elif confidence > simG:
|
||||
result.append([organization, confidence])
|
||||
|
||||
|
||||
elif confidence >= 0.98:
|
||||
result.append([organization, 1])
|
||||
|
||||
else:
|
||||
if "univ" not in organization:
|
||||
if not org_has_univ:# and not org_has_hosp:
|
||||
continue # Skip if 'univ' is missing
|
||||
|
||||
|
||||
try:
|
||||
s_vector = vectorizer.fit_transform([light_raw]).toarray()
|
||||
# Lazy initialization and reuse of light_raw_vector
|
||||
if light_raw_vector is None:
|
||||
light_raw_vector = vectorizer.fit_transform([light_raw]).toarray()
|
||||
|
||||
# Reuse vectorizer
|
||||
x_vector = vectorizer.transform([organization]).toarray()
|
||||
similarity = cosine_similarity(x_vector, s_vector)[0][0]
|
||||
|
||||
if similarity > 0.1: #use Levenshtein to better handle misspellings
|
||||
similarity_l = 1 - Levenshtein.distance(organization, affil) / max(len(organization), len(affil))
|
||||
similarity = cosine_similarity(x_vector, light_raw_vector)[0][0]
|
||||
|
||||
if similarity > 0.1:
|
||||
# Use Levenshtein to better handle misspellings
|
||||
if organization in affil:
|
||||
similarity_l = 1
|
||||
else:
|
||||
similarity_l = (
|
||||
1
|
||||
- Levenshtein.distance(organization, affil)
|
||||
/ max(len(organization), len(affil))
|
||||
)
|
||||
best_j.append([organization, similarity, similarity_l])
|
||||
|
||||
except Exception as ex:
|
||||
print("Error:", ex)
|
||||
|
||||
|
||||
# Step 2: Keep only the best similarity per organization
|
||||
max_numbers = defaultdict(float)
|
||||
for org, sim, sim_l in best_j:
|
||||
max_numbers[org] = max(max_numbers[org], sim)
|
||||
|
||||
reduced_best = [[org, sim, sim_l] for org, sim, sim_l in best_j if sim == max_numbers[org]]
|
||||
|
||||
|
||||
reduced_best = [
|
||||
[org, sim, sim_l]
|
||||
for org, sim, sim_l in best_j
|
||||
if sim == max_numbers[org]
|
||||
]
|
||||
|
||||
# Sort by similarity score (descending) and then lexicographically
|
||||
reduced_best.sort(key=lambda x: (x[1], x[2]), reverse=True)
|
||||
# print('REDUCED BEST: ', reduced_best)
|
||||
|
||||
|
||||
# Apply group-level university limiting
|
||||
group_univ = [r for r in reduced_best if 'univ' in r[0]]
|
||||
group_other = [r for r in reduced_best if 'univ' not in r[0]]
|
||||
if len(group_univ) > num_uni_p:
|
||||
reduced_best = group_univ[:num_uni_p] + group_other
|
||||
|
||||
result.extend(reduced_best)
|
||||
# print('RESULT EXT: ', result)
|
||||
|
||||
# Step 3: Limit university-related matches
|
||||
univ_list = [r for r in result if 'univ' in r[0]]
|
||||
other_list = [r for r in result if 'univ' not in r[0]]
|
||||
|
||||
limit = min(num_uni_p, candidate_num)
|
||||
if len(univ_list) > limit:
|
||||
result = univ_list[:limit] + other_list
|
||||
|
||||
|
||||
best.append(best_j)
|
||||
|
||||
# Step 4: Construct final dictionary **with highest confidence values**
|
||||
|
||||
# Step 3: Construct final dictionary with highest confidence values
|
||||
pairs_dict = {p[1]: p[2] for group in pairs_list for p in group}
|
||||
|
||||
# Select the best confidence score for each organization
|
||||
result_dict = {}
|
||||
for res in result:
|
||||
org = res[0]
|
||||
similarity_score = res[1]
|
||||
if org in pairs_dict:
|
||||
best_confidence = pairs_dict[org] # Original confidence score from pairs_list
|
||||
best_confidence = pairs_dict[org]
|
||||
if org not in result_dict or similarity_score > result_dict[org][1]:
|
||||
result_dict[org] = [best_confidence, similarity_score]
|
||||
|
||||
|
||||
# Convert to list format
|
||||
final_result = [[key, value[0]] for key, value in sorted(result_dict.items(), key=lambda x: x[1][1], reverse=True)]
|
||||
|
||||
# print("RESULT TO USE: ", final_result)
|
||||
final_result = [
|
||||
[key, value[0]]
|
||||
for key, value in sorted(result_dict.items(), key=lambda x: x[1][1], reverse=True)
|
||||
]
|
||||
|
||||
return final_result
|
||||
|
||||
|
||||
|
||||
def Aff_Ids(input, dix_org, dix_mult, dix_city, dix_country, simU, simG, limit):
|
||||
|
||||
"""
|
||||
Matches affiliations in DataFrame 'DF' with names from dictionary 'dix_org' and their ROR_ids based on similarity scores.
|
||||
|
||||
Args:
|
||||
m (int): The number of DOIs to check.
|
||||
DF (DataFrame): The input DataFrame containing affiliation data.
|
||||
dix_org (dict): A dictionary of names of organizations and their ROR_ids.
|
||||
simU (float): Similarity threshold for universities.
|
||||
simG (float): Similarity threshold for non-universities.
|
||||
|
||||
Returns:
|
||||
DataFrame: The final DataFrame with matched affiliations and their corresponding similarity scores.
|
||||
"""
|
||||
clean_aff = input[0]
|
||||
# print('CLEAN_AFF (LVL1): ', clean_aff)
|
||||
light_aff = input[1].replace(' gmbh', ' ').strip()
|
||||
# print('LIGHT_AFF (LVL2): ', light_aff)
|
||||
|
||||
df_list = input[2]
|
||||
|
||||
countries_list = input[3]
|
||||
# print('COUNTRIES_LIST: ', countries_list)
|
||||
vectorizer = CountVectorizer()
|
||||
|
||||
dix = {} # will store indeces and legalnames of organizations of the DOI { i : [legalname1, legalname2,...]}
|
||||
result = {}
|
||||
pairs = []
|
||||
|
||||
keywords = [entry["keywords"].replace(' gmbh', ' ').strip() for entry in df_list]
|
||||
|
||||
candidates = get_candidates(countries_list)
|
||||
|
||||
# print('KEYWORDS: ', keywords)
|
||||
if len(keywords) > 1 or len(keywords) == 1 and len(keywords[0])>1:
|
||||
|
||||
for k,s in enumerate(keywords):
|
||||
pairs_k = []
|
||||
# print('try', s)
|
||||
try:
|
||||
pairs_k.append((s,s,1,dix_org[s],dix_id_country[dix_org[s]]))
|
||||
# print('LUCKY')
|
||||
|
||||
# pairs.append((s,s,similarity,dix_org[s], dix_id_country[dix_org[s]]))
|
||||
|
||||
if k not in dix:
|
||||
dix[k] = [s]
|
||||
else:
|
||||
dix[k].append(s)
|
||||
|
||||
except:
|
||||
# print('NOT LUCKY')
|
||||
pairs_k = find_candidate(s, k , dix, simU, simG, candidates, limit)
|
||||
# print('PAIRS K: ', pairs_k)
|
||||
|
||||
result[k] = pairs_k
|
||||
if len(pairs_k)>0:
|
||||
# print('PAIRS K>0: ', pairs_k)
|
||||
|
||||
pairs.append(pairs_k)
|
||||
|
||||
# print('PAIRS: ', pairs)
|
||||
multi = index_multiple_matchings(pairs)
|
||||
# print('MULTIL ',multi)
|
||||
|
||||
need_check_keys = []
|
||||
ready_keys = []
|
||||
ready_best = []
|
||||
for keyword in multi:
|
||||
try:
|
||||
if multi[keyword]>1:
|
||||
need_check_keys.append(keyword)
|
||||
else:
|
||||
for p in pairs:
|
||||
if keyword in p[0]:
|
||||
if p[0][1] not in ready_keys:
|
||||
ready_keys.append(p[0][1])
|
||||
|
||||
ready_best.append([p[0][1], p[0][2]])
|
||||
except:
|
||||
pass
|
||||
# print('READY KEYWORD: ', ready_keys)
|
||||
# print('READY BEST: ', ready_best)
|
||||
|
||||
# print('NEED CHECK KEYWORD: ', need_check_keys)
|
||||
|
||||
pairs_check = [ pair for pair in pairs if pair[0][0] in need_check_keys ]
|
||||
# print('NEED CHECK PAIRS: ', pairs_check)
|
||||
|
||||
|
||||
if len(need_check_keys)>0:
|
||||
best0 = best_sim_score(clean_aff, light_aff, len(keywords), pairs_check, multi, simU, simG)
|
||||
# print('OUTPUT BEST: ', best0)
|
||||
best1 = {x[0]:dix_org[x[0]] for x in best0 }
|
||||
best01 = unique_subset(best0, best1)
|
||||
matched_org = list(set([x[0] for x in best01])) + ready_keys
|
||||
best = best01 + ready_best
|
||||
|
||||
|
||||
|
||||
# print('NEW BEST',best01)
|
||||
else:
|
||||
best = ready_best
|
||||
matched_org = ready_keys
|
||||
|
||||
|
||||
# print('FINAL BEST: ', best)
|
||||
## print('MATCHED: ', matched_org)
|
||||
|
||||
id_list = []
|
||||
|
||||
for org_list in best:
|
||||
org = org_list[0]
|
||||
conf = org_list[1]
|
||||
if dix_mult[org] == 'unique':
|
||||
# print('unique:', org)
|
||||
if 'institu' in org and 'univ' in org:
|
||||
#print('both inst and univ', clean_aff)
|
||||
if dix_city[org][0] not in clean_aff and dix_country[org][0] not in clean_aff:
|
||||
#print('pass')
|
||||
pass
|
||||
else:
|
||||
#print('correct')
|
||||
id_list.append([org, conf, dix_org[org]])
|
||||
else:
|
||||
id_list.append([org, conf, dix_org[org]])
|
||||
|
||||
|
||||
else:
|
||||
# print('not unique:', org)
|
||||
if org in dix_city:
|
||||
match_found = False
|
||||
|
||||
for city in dix_city[org]:
|
||||
if city[0] in clean_aff:
|
||||
if city[0] not in org:
|
||||
# print('city', city[0], org)
|
||||
id_list.append([org, conf, city[1]])
|
||||
match_found = True
|
||||
break
|
||||
else:
|
||||
if clean_aff.count(city[0]) >1:
|
||||
id_list.append([org, conf, city[1]])
|
||||
match_found = True
|
||||
break
|
||||
|
||||
if not match_found:
|
||||
for city in dix_city[org]:
|
||||
if city[0] in clean_aff and city[0] not in org:
|
||||
id_list.append([org, conf, city[1]])
|
||||
break
|
||||
|
||||
if not match_found:
|
||||
match_found2 = False
|
||||
match_found3 = False
|
||||
|
||||
all_countries = list(set([c[0] for c in dix_country[org]]))
|
||||
if len(all_countries) > 1:
|
||||
|
||||
for country in dix_country[org]:
|
||||
# print('country', country[0], org)
|
||||
|
||||
tokens = set(clean_aff.lower().split())
|
||||
text = clean_aff.lower()
|
||||
|
||||
if country[0] == 'united states' and (
|
||||
'united states' in text
|
||||
or {'usa', 'usa.'} & tokens
|
||||
or 'u.s.a.' in text
|
||||
):
|
||||
id_list.append([org, conf, country[1]])
|
||||
match_found2 = True
|
||||
match_found3 = True
|
||||
break
|
||||
|
||||
|
||||
if country[0] == 'united kingdom' and (
|
||||
'united kingdom' in text
|
||||
or {'uk', 'uk.'} & tokens
|
||||
or 'u.k.' in text
|
||||
):
|
||||
id_list.append([org, conf, country[1]])
|
||||
match_found2 = True
|
||||
match_found3 = True
|
||||
break
|
||||
# print('check country', clean_aff)
|
||||
# if country[0] == 'united states' and (country[0] in clean_aff or 'usa' in clean_aff.split() or 'usa.' in clean_aff.split() or 'u.s.a.' in clean_aff):
|
||||
# id_list.append([org, conf, country[1]])
|
||||
# match_found2 = True
|
||||
# match_found3 = True
|
||||
# break
|
||||
|
||||
# if country[0] == 'united kingdom' and (country[0] in clean_aff or 'uk' in clean_aff.split() or 'u.k.' in clean_aff):
|
||||
# id_list.append([org, conf, country[1]])
|
||||
# match_found2 = True
|
||||
# match_found3 = True
|
||||
# break
|
||||
if country[0] == 'turkey' and (
|
||||
'turkiye' in text
|
||||
#or 'u.k.' in text
|
||||
):
|
||||
# print('here turkey')
|
||||
|
||||
id_list.append([org, conf, country[1]])
|
||||
match_found2 = True
|
||||
match_found3 = True
|
||||
break
|
||||
elif country[0].split()[0] in clean_aff:
|
||||
|
||||
if country[0] not in org:
|
||||
id_list.append([org, conf, country[1]])
|
||||
match_found2 = True
|
||||
match_found3 = True
|
||||
break
|
||||
else:
|
||||
single_country = all_countries[0]
|
||||
if single_country in clean_aff:
|
||||
id_list.append([org, conf, dix_org[org]])
|
||||
match_found2 = True
|
||||
match_found3 = True
|
||||
break
|
||||
|
||||
if not match_found3:
|
||||
for country in dix_country[org]:
|
||||
if country[0] in clean_aff and country[0] in org:
|
||||
id_list.append([org, conf, country[1]])
|
||||
match_found2 = True
|
||||
break
|
||||
if not match_found2:
|
||||
for sp in specific:
|
||||
if sp in org:
|
||||
id_list.append([org, conf, dix_org[org]])
|
||||
|
||||
|
||||
# print("RESULT: ", id_list)
|
||||
id_list_final = keep_highest_url(id_list)
|
||||
|
||||
return id_list_final
|
||||
Binary file not shown.
|
|
@ -1,191 +0,0 @@
|
|||
{
|
||||
"research": "Academia",
|
||||
"centro": "Academia",
|
||||
"recherche": "Academia",
|
||||
"uniwersytet": "Academia",
|
||||
"investigacions": "Academia",
|
||||
"institu": "Academia",
|
||||
"istitut": "Academia",
|
||||
"univ": "Academia",
|
||||
"col": "Academia",
|
||||
"center": "Academia",
|
||||
"polytechnic": "Academia",
|
||||
"tech": "Academia",
|
||||
"politecnico": "Academia",
|
||||
"polutekhneio": "Academia",
|
||||
"prifysgol": "Academia",
|
||||
"centre": "Academia",
|
||||
"kentro": "Academia",
|
||||
"politechnika": "Academia",
|
||||
"szkola": "Academia",
|
||||
"faculty": "Academia",
|
||||
"school": "Academia",
|
||||
"academ": "Academia",
|
||||
"akadem": "Academia",
|
||||
"schule": "Academia",
|
||||
"ecole": "Academia",
|
||||
"observ": "Academia",
|
||||
"escuela": "Academia",
|
||||
"escola": "Academia",
|
||||
"unibertsi": "Academia",
|
||||
"yliopisto": "Academia",
|
||||
"instytut": "Academia",
|
||||
"zentrum": "Academia",
|
||||
"panepist": "Academia",
|
||||
"lab": "Academia",
|
||||
"scien": "Academia",
|
||||
"cientific": "Academia",
|
||||
"engingeering": "Academia",
|
||||
"egyetem": "Academia",
|
||||
"sterewacht": "Academia",
|
||||
"tecnol": "Academia",
|
||||
"data": "Academia",
|
||||
"univer": "Academia",
|
||||
"labora": "Academia",
|
||||
"hospital": "Hospitals",
|
||||
"clinic": "Hospitals",
|
||||
"hopital": "Hospitals",
|
||||
"klinik": "Hospitals",
|
||||
"oncol": "Hospitals",
|
||||
"medical": "Hospitals",
|
||||
"health": "Hospitals",
|
||||
"medicin": "Hospitals",
|
||||
"nosokomei": "Hospitals",
|
||||
"krankenhaus": "Hospitals",
|
||||
"ziekenhuis": "Hospitals",
|
||||
"spital": "Hospitals",
|
||||
"cancer": "Hospitals",
|
||||
"pharma": "Hospitals",
|
||||
"therapeutics": "Hospitals",
|
||||
"ospedale": "Hospitals",
|
||||
"dermato": "Hospitals",
|
||||
"gmbh": "Company",
|
||||
"company": "Company",
|
||||
"industr": "Company",
|
||||
"etaireia": "Company",
|
||||
"corporation": "Company",
|
||||
"inc": "Company",
|
||||
"ltd": "Company",
|
||||
"foundation": "Foundations",
|
||||
"asociation": "Foundations",
|
||||
"organization": "Foundations",
|
||||
"society": "Foundations",
|
||||
"group": "Foundations",
|
||||
"royal": "Foundations",
|
||||
"ofice": "Foundations",
|
||||
"trust": "Foundations",
|
||||
"survey": "Foundations",
|
||||
"museum": "Foundations",
|
||||
"museen": "Foundations",
|
||||
"library": "Foundations",
|
||||
"bank": "Foundations",
|
||||
"comision": "Foundations",
|
||||
"komision": "Foundations",
|
||||
"district": "Government",
|
||||
"federation": "Government",
|
||||
"government": "Government",
|
||||
"municipal": "Government",
|
||||
"county": "Government",
|
||||
"council": "Government",
|
||||
"agency": "Government",
|
||||
"unknown": "Unknown",
|
||||
"squib": "Specific",
|
||||
"intel": "Specific",
|
||||
"motorola": "Specific",
|
||||
"samsung": "Specific",
|
||||
"hitachi": "Specific",
|
||||
"roche": "Specific",
|
||||
"google": "Specific",
|
||||
"yahoo": "Specific",
|
||||
"ebay": "Specific",
|
||||
"novartis": "Specific",
|
||||
"microsoft": "Specific",
|
||||
"ibm": "Specific",
|
||||
"alergan": "Specific",
|
||||
"medtronic": "Specific",
|
||||
"xilinx": "Specific",
|
||||
"pfizer": "Specific",
|
||||
"glaxosmithkline": "Specific",
|
||||
"astrazeneca": "Specific",
|
||||
"boehringer ingelheim": "Specific",
|
||||
"demokritos": "Specific",
|
||||
"siemens": "Specific",
|
||||
"forth": "Specific",
|
||||
"eli lily": "Specific",
|
||||
"boeing": "Specific",
|
||||
"alphabet": "Specific",
|
||||
"johnson johnson": "Specific",
|
||||
"moderna": "Specific",
|
||||
"bayer": "Specific",
|
||||
"huawei": "Specific",
|
||||
"amazon": "Specific",
|
||||
"sinopec": "Specific",
|
||||
"novo nordisk": "Specific",
|
||||
"sanofi": "Specific",
|
||||
"amgen": "Specific",
|
||||
"abvie": "Specific",
|
||||
"basf": "Specific",
|
||||
"toyota": "Specific",
|
||||
"gilead": "Specific",
|
||||
"teagasc": "Specific",
|
||||
"apc microbiome": "Specific",
|
||||
"nestle": "Specific",
|
||||
"perkinelmer": "Specific",
|
||||
"shel": "Specific",
|
||||
"visteon": "Specific",
|
||||
"hewlet packard": "Specific",
|
||||
"fujitsu": "Specific",
|
||||
"toshiba": "Specific",
|
||||
"eth zurich": "Acronyms",
|
||||
"riken": "Acronyms",
|
||||
"ucd": "Acronyms",
|
||||
"tcd": "Acronyms",
|
||||
"nasa": "Acronyms",
|
||||
"bgi": "Acronyms",
|
||||
"ntt": "Acronyms",
|
||||
"dow": "Acronyms",
|
||||
"ustc": "Acronyms",
|
||||
"zju": "Acronyms",
|
||||
"pku": "Acronyms",
|
||||
"ucas": "Acronyms",
|
||||
"nju": "Acronyms",
|
||||
"scu": "Acronyms",
|
||||
"mit": "Acronyms",
|
||||
"hust": "Acronyms",
|
||||
"ucla": "Acronyms",
|
||||
"uc san diego": "Acronyms",
|
||||
"uc berkeley": "Acronyms",
|
||||
"jhu": "Acronyms",
|
||||
"uchicago": "Acronyms",
|
||||
"caltech": "Acronyms",
|
||||
"usp": "Acronyms",
|
||||
"conicet": "Acronyms",
|
||||
"unicamp": "Acronyms",
|
||||
"cnrs": "Acronyms",
|
||||
"ucl": "Acronyms",
|
||||
"csic": "Acronyms",
|
||||
"epfl": "Acronyms",
|
||||
"ucph": "Acronyms",
|
||||
"tum": "Acronyms",
|
||||
"lmu": "Acronyms",
|
||||
"ku leuven": "Acronyms",
|
||||
"ircs": "Acronyms",
|
||||
"nhs": "Acronyms",
|
||||
"discovery programe": "Acronyms",
|
||||
"ncsr demokritos": "Acronyms",
|
||||
"nui galway": "Acronyms",
|
||||
"nui maynooth": "Acronyms",
|
||||
"tu wien": "Acronyms",
|
||||
"tu dublin": "Acronyms",
|
||||
"athena": "Acronyms",
|
||||
"openaire": "Acronyms",
|
||||
"erasmus": "Acronyms",
|
||||
"ist austria": "Acronyms",
|
||||
"nit": "Acronyms",
|
||||
"cern": "Acronyms",
|
||||
"lg": "Acronyms",
|
||||
"chu": "Acronyms",
|
||||
"jst": "Acronyms",
|
||||
"lumc": "Acronyms",
|
||||
"niab": "Acronyms"
|
||||
}
|
||||
Binary file not shown.
536192
affro/jsons/dix_city.json
536192
affro/jsons/dix_city.json
File diff suppressed because it is too large
Load Diff
536184
affro/jsons/dix_country.json
536184
affro/jsons/dix_country.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
119124
affro/jsons/dix_id_name.json
119124
affro/jsons/dix_id_name.json
File diff suppressed because it is too large
Load Diff
Binary file not shown.
128381
affro/jsons/dix_mult.json
128381
affro/jsons/dix_mult.json
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
128381
affro/jsons/dix_org.json
128381
affro/jsons/dix_org.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
|
@ -1,80 +0,0 @@
|
|||
{"kirgizistan" : "kyrgyzstan",
|
||||
"hopital": "hospital",
|
||||
"hosp.": "hospital",
|
||||
"med. ctr.": "medical center",
|
||||
"lab.": "labora",
|
||||
"czechoslovak": "czech",
|
||||
"saint": "st",
|
||||
"aghia": "agia",
|
||||
"kyprou": "kuprou",
|
||||
"technologiko": "tekhnologiko",
|
||||
"panepistimio": "panepistemio",
|
||||
"universitatsfrauenklinik": "univer hospital",
|
||||
"Universitätsaugenklinik": "univer eye hospital",
|
||||
"universitatsklinikum": "univer hospital",
|
||||
"universitetshospital": "univer hospital",
|
||||
"universitatskinderklinik": "univer childrens hospital",
|
||||
"universitatskliniken": "univer hospital",
|
||||
"universitätsklinik": "univer hospital",
|
||||
"universitatsmedizin": "univer medicine",
|
||||
"universitatsbibliothek": "univer library",
|
||||
"nat.": "national",
|
||||
"uniaersity": "univer",
|
||||
"univesity": "univer",
|
||||
"unversity": "univer",
|
||||
"uni-versity": "univer",
|
||||
"un iversity": "univer",
|
||||
"uniuersity": "univer",
|
||||
"unviersity": "univer",
|
||||
"uni versity": "univer",
|
||||
"unive rsity": "univer",
|
||||
"univ ersity": "univer",
|
||||
"ilniversity": "univer",
|
||||
"univcrsity": "univer",
|
||||
"unirersity": "univer",
|
||||
"univsity": "univer",
|
||||
"inst ": "institu ",
|
||||
"adv ": "advanced ",
|
||||
"univ ": "univer ",
|
||||
"stud ": "studies ",
|
||||
"inst.": "institu",
|
||||
"sci.": "scien",
|
||||
"acad.": "academy",
|
||||
"adv.": "advanced",
|
||||
"univ.": "univer",
|
||||
"stud.": "studies",
|
||||
"techniche": "technological",
|
||||
"univ col": "univer colege",
|
||||
"univ. col.": "univer colege",
|
||||
"col.": "colege",
|
||||
"medical school university": "univer",
|
||||
"hipokration": "hipocration",
|
||||
"belfield, dublin": "dublin",
|
||||
"balsbridge, dublin": "dublin",
|
||||
"earlsfort terace, dublin": "dublin",
|
||||
"bon secours hospital, cork": "bon secours hospital cork",
|
||||
"bon secours hospital, dublin": "bon secours hospital dublin",
|
||||
"bon secours hospital, galway": "bon secours hospital galway",
|
||||
"bon secours hospital, tralee": "bon secours hospital tralee",
|
||||
"bon secours health system": "bon secours hospital dublin",
|
||||
"bon secours hospital, glasnevin": "bon secours hospital dublin",
|
||||
"imperial colege science, technology medicine": "imperial colege science technology medicine",
|
||||
"ucl queen square institute neurology": "ucl, london",
|
||||
"ucl institute neurology": "ucl, london",
|
||||
"royal holoway, university london": "royal holoway univer london",
|
||||
"city, university london": "city univer london",
|
||||
"city university, london": "city univer london",
|
||||
"aeginition": "eginition",
|
||||
"national technical university, athens": "national technical university athens",
|
||||
"leiden institu physics": "univer leiden",
|
||||
"leyden": "leiden",
|
||||
"leiden law school": "univer leiden",
|
||||
"leiden umc": "leiden univer medical center",
|
||||
"rijksuniver": "univer",
|
||||
"leids": "leiden",
|
||||
"lumc leiden": "lumc,leiden",
|
||||
"medisch": "medical",
|
||||
"hahn-meitner-institut berlin" : "helmholtz-zentrum berlin",
|
||||
"fachhochschule gelsenkirchen" : "westfalische hochschule",
|
||||
"turkiye" : "turkey"
|
||||
}
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,29 @@
|
|||
helas
|
||||
ostereich
|
||||
belgie
|
||||
belgique
|
||||
belgien
|
||||
hrvatska
|
||||
kypros
|
||||
cesko
|
||||
czech
|
||||
elada
|
||||
italia
|
||||
nederland
|
||||
espana
|
||||
sverige
|
||||
schweiz
|
||||
suise
|
||||
svizera
|
||||
macedonia
|
||||
herzegovina
|
||||
zealand
|
||||
eire
|
||||
turkiye
|
||||
trinidad
|
||||
united
|
||||
kingdom
|
||||
states
|
||||
emirates
|
||||
hong
|
||||
niederland
|
||||
holand
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
colege street
|
||||
research institu
|
||||
research instit
|
||||
labora
|
||||
gmbh
|
||||
inc
|
||||
|
|
@ -7,17 +7,17 @@ ltd
|
|||
research center
|
||||
foundation
|
||||
faculty
|
||||
national institu
|
||||
national instit
|
||||
school medicine
|
||||
univer school
|
||||
graduate school
|
||||
graduate school engineering
|
||||
institu tropical medicine
|
||||
institu virology
|
||||
instit tropical medicine
|
||||
instit virology
|
||||
faculty medicine
|
||||
labora
|
||||
univer park
|
||||
institu science
|
||||
instit science
|
||||
polytechnic univer
|
||||
univer 1
|
||||
ciudad univer
|
||||
|
|
@ -29,9 +29,12 @@ univer str
|
|||
clinic
|
||||
techn
|
||||
univer
|
||||
scien
|
||||
institu st
|
||||
po box
|
||||
rijksuniver
|
||||
institu
|
||||
instit
|
||||
hochschule
|
||||
united states america
|
||||
united states america
|
||||
univer way
|
||||
univer rd
|
||||
|
|
@ -20,4 +20,18 @@ di
|
|||
l
|
||||
street
|
||||
post-box
|
||||
e.v.
|
||||
e.v.
|
||||
do
|
||||
ng
|
||||
zu
|
||||
degli
|
||||
1tq
|
||||
cb2
|
||||
candidate
|
||||
panepistimiou
|
||||
panepistimioupoli
|
||||
dele
|
||||
dela
|
||||
ercity
|
||||
1/2
|
||||
oeaw
|
||||
|
|
@ -8,4 +8,18 @@ universitatsbibliothek
|
|||
universitatspital
|
||||
universitetsjukhuset
|
||||
universitatsaugenklinik
|
||||
univesitatsfrauenklinik
|
||||
universitatsfrauenklinik
|
||||
universitetscentralsjukhus
|
||||
universitatsverlag
|
||||
universitaetsklinikum
|
||||
universitatsalianz
|
||||
universalmuseum
|
||||
universitatszahnklinik
|
||||
universitetsforlaget
|
||||
universal
|
||||
universum
|
||||
hospitalfield
|
||||
universitatsklinik
|
||||
universitaetsklinik
|
||||
universitetskliniken
|
||||
universiteitsmuseum
|
||||
|
|
@ -1 +1,5 @@
|
|||
__version__ = "2.2.2"
|
||||
__version__ = "3.4.0"
|
||||
|
||||
from .core import run_affro, matchings_affro
|
||||
|
||||
__all__ = ["__version__", "run_affro", "matchings_affro"]
|
||||
|
|
|
|||
|
|
@ -1,164 +1,350 @@
|
|||
import sys
|
||||
from functools import lru_cache
|
||||
##import functions
|
||||
from affro.helpers.functions import *
|
||||
from affro.helpers.matching import *
|
||||
from affro.helpers.create_input import *
|
||||
import json
|
||||
from affro.helpers.find_name import *
|
||||
from affro.helpers.find_id import *
|
||||
from affro.helpers.disambiguation import *
|
||||
from affro.helpers.direct_mapping import *
|
||||
|
||||
from . import __version__
|
||||
|
||||
VERSION = __version__
|
||||
|
||||
dix_org = load_json('jsons/dix_org.json')
|
||||
dix_mult = load_json('jsons/dix_mult.json')
|
||||
dix_city = load_json('jsons/dix_city.json')
|
||||
dix_country = load_json('jsons/dix_country.json')
|
||||
dix_status = load_json('jsons/dix_status.json')
|
||||
dix_id_name = load_json('jsons/dix_id_name.json')
|
||||
dix_id_country = load_json('jsons/dix_id_country.json')
|
||||
dix_id_name = load_json('jsons/dix_id_name.json')
|
||||
|
||||
dix_id = load_json('jsons/dix_id.json.gz')
|
||||
# dix_name is already loaded in functions.py and imported via `from affro.helpers.functions import *`
|
||||
|
||||
|
||||
dix_status_new = {k :[dix_status[k][0], dix_status[k][1].split(', ')] for k in dix_status}
|
||||
us_states = [
|
||||
"alabama", "alaska", "arizona", "arkansas", "california",
|
||||
"colorado", "conecticut", "delaware", "florida", "georgia",
|
||||
"hawaii", "idaho", "ilinois", "indiana", "iowa",
|
||||
"kansas", "kentucky", "louisiana", "maine", "maryland",
|
||||
"masachusets", "michigan", "minesota", "misisipi", "misouri",
|
||||
"montana", "nebraska", "nevada", "new hampshire", "new jersey",
|
||||
"new mexico", "new york", "north carolina", "north dakota", "ohio",
|
||||
"oklahoma", "oregon", "pensylvania", "rhode island", "south carolina",
|
||||
"south dakota", "tenesee", "texas", "utah", "vermont",
|
||||
"virginia", "washington", "west virginia", "wisconsin", "wyoming"
|
||||
]
|
||||
def _country_set(result):
|
||||
countries = result.get('country')
|
||||
if not countries and result.get('value') in dix_id:
|
||||
countries = dix_id[result['value']].get('country')
|
||||
if not isinstance(countries, (list, tuple, set)):
|
||||
if isinstance(countries, str) and countries.strip():
|
||||
return {countries.strip().casefold()}
|
||||
return set()
|
||||
return {
|
||||
country.strip().casefold()
|
||||
for country in countries
|
||||
if isinstance(country, str) and country.strip()
|
||||
}
|
||||
|
||||
|
||||
def _is_country_in_string(country, aff_string):
|
||||
if not aff_string or not country:
|
||||
return False
|
||||
c_lower = country.strip().casefold()
|
||||
aff_lower = aff_string.casefold()
|
||||
clean_aff = clean_string(aff_string).casefold()
|
||||
|
||||
def contains_us_state(text):
|
||||
text = text.lower()
|
||||
return any(state in text for state in us_states)
|
||||
detected_countries = set(description(clean_aff)[1])
|
||||
terms = {c_lower}
|
||||
if c_lower in country_synonyms:
|
||||
terms.update(s.casefold() for s in country_synonyms[c_lower])
|
||||
if c_lower in country_to_group:
|
||||
terms.update(s.casefold() for s in country_to_group[c_lower])
|
||||
if c_lower in {'rusia', 'russia'}:
|
||||
terms.update({'rusia', 'russia'})
|
||||
|
||||
def find_ror(input, simU, simG, limit):
|
||||
light_aff = input[0]
|
||||
result = Aff_Ids(input, dix_org, dix_mult, dix_city, dix_country, simU, simG, limit)
|
||||
results_upd = []
|
||||
|
||||
for r in result:
|
||||
|
||||
if "openorgs" in r[2]:
|
||||
results_upd.append([r[1], 'openorgs', r[2], 'active', dix_id_country[r[2]]])
|
||||
|
||||
if not terms.isdisjoint(detected_countries):
|
||||
return True
|
||||
|
||||
for term in terms:
|
||||
if len(term) <= 3:
|
||||
pattern = r'\b' + re.escape(term) + r'\b'
|
||||
if re.search(pattern, aff_lower) or re.search(pattern, clean_aff):
|
||||
return True
|
||||
else:
|
||||
if dix_status_new[r[2]][0] == 'active':
|
||||
results_upd.append([r[1], 'ror', r[2], 'active', dix_id_country[r[2]]])
|
||||
else:
|
||||
if dix_status_new[r[2]][1][0] == '':
|
||||
results_upd.append([r[1], 'ror', r[2], dix_status_new[r[2]][0], dix_id_country[r[2]]])
|
||||
|
||||
|
||||
else:
|
||||
results_upd.append([r[1], 'ror', r[2], dix_status_new[r[2]][0],dix_id_country[r[2]]])
|
||||
for link in (dix_status_new[r[2]][1]):
|
||||
results_upd.append([r[1], 'ror', link, 'active',dix_id_country[r[2]],dix_id_country[link]])
|
||||
|
||||
if len(results_upd) > len(set(description(light_aff)[1])):
|
||||
if term in aff_lower or term in clean_aff:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def discard_country_mismatched_institutes(results, aff=None):
|
||||
"""
|
||||
Filter out non-direct institute and department matches that are in a different country
|
||||
from any university matched in the same affiliation string, unless the candidate's
|
||||
country is present in the affiliation string.
|
||||
"""
|
||||
if not results:
|
||||
return results
|
||||
|
||||
universities = [
|
||||
result
|
||||
for result in results
|
||||
if 'univ' in str(result.get('name', '')).casefold()
|
||||
]
|
||||
if not universities:
|
||||
return results
|
||||
|
||||
# Known country sets for matched universities
|
||||
univ_countries_list = [
|
||||
_country_set(u)
|
||||
for u in universities
|
||||
]
|
||||
known_univ_countries = [c for c in univ_countries_list if c]
|
||||
if not known_univ_countries:
|
||||
return results
|
||||
|
||||
filtered = []
|
||||
for result in results:
|
||||
# Never filter out the university itself or direct matches
|
||||
if result in universities or result.get('provenance') in ('affro_direct', 'dictionaries'):
|
||||
filtered.append(result)
|
||||
continue
|
||||
|
||||
# name = str(result.get('name', '')).casefold()
|
||||
# if not any(k in name for k in ('inst', 'depart', 'dipart', 'center')):
|
||||
# filtered.append(result)
|
||||
# continue
|
||||
|
||||
candidate_countries = _country_set(result)
|
||||
# Keep if country is unknown or shares at least one country with a university
|
||||
if not candidate_countries or any(
|
||||
not candidate_countries.isdisjoint(uc) for uc in known_univ_countries
|
||||
):
|
||||
filtered.append(result)
|
||||
continue
|
||||
|
||||
# Candidate country does not match university:
|
||||
# Discard only if the country is not present in the string
|
||||
if aff and any(_is_country_in_string(c, aff) for c in candidate_countries):
|
||||
filtered.append(result)
|
||||
continue
|
||||
|
||||
# Mismatched and country not present in string -> discarded
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
_discard_country_mismatched_institutes = discard_country_mismatched_institutes
|
||||
discard_city_mismatched_institutes = discard_country_mismatched_institutes
|
||||
_discard_city_mismatched_institutes = discard_country_mismatched_institutes
|
||||
|
||||
|
||||
|
||||
def produce_result(input, simU, simG, limit):
|
||||
best_name = find_name(input, dix_name, simU, simG, limit)
|
||||
id_result = find_id(input, best_name, dix_name, simG)
|
||||
result = disamb(input, id_result, dix_id)
|
||||
|
||||
return result
|
||||
|
||||
def _make_entry(pid, value, name, confidence, status, country):
|
||||
"""Build a single result dictionary."""
|
||||
return {
|
||||
'provenance': 'affro',
|
||||
'version': VERSION,
|
||||
'pid': pid,
|
||||
'value': value,
|
||||
'name': name,
|
||||
'confidence': confidence,
|
||||
'status': status,
|
||||
'country': country,
|
||||
}
|
||||
|
||||
|
||||
def build_result_list(id_, name_, country_, status_):
|
||||
"""
|
||||
Helper function to build the result list, reducing code duplication.
|
||||
"""
|
||||
pid = 'openorgs' if 'openorgs' in id_ else 'ror'
|
||||
|
||||
# For openorgs or active ROR entries, return a single active entry
|
||||
if 'openorgs' in id_ or status_[0] == 'active':
|
||||
return [_make_entry(pid, id_, name_, 1, 'active', country_)]
|
||||
|
||||
# Build primary entry with its current status
|
||||
res = [_make_entry(pid, id_, name_, 1, status_[0], country_)]
|
||||
|
||||
# Append active successors (if any)
|
||||
if len(status_) > 1:
|
||||
for successor in status_[1]:
|
||||
if successor != '':
|
||||
res.append(_make_entry(
|
||||
'ror', successor,
|
||||
dix_id[successor]['name'], 1,
|
||||
'active', dix_id[successor]['country'],
|
||||
))
|
||||
return res
|
||||
|
||||
|
||||
def _apply_secondary_conditions(ids):
|
||||
"""
|
||||
Apply progressive filtering conditions to find best candidate.
|
||||
Optimized version: Pre-fetches dictionary entries to avoid repeated O(N) access.
|
||||
"""
|
||||
# Separate openorgs immediately (they bypass all ror logic)
|
||||
openorgs = [k for k in ids if 'openorgs' in k]
|
||||
|
||||
# Pre-fetch active rors once to avoid heavy dictionary spam
|
||||
active_rors = [k for k in ids if 'ror' in k and dix_id[k]['status'][0] == 'active']
|
||||
|
||||
if openorgs:
|
||||
active_top = [k for k in active_rors if dix_id[k]['top_level'][0] == 'y']
|
||||
return openorgs + active_top
|
||||
|
||||
if not active_rors:
|
||||
return []
|
||||
|
||||
# Condition 1: Check Top level
|
||||
top_level = [k for k in active_rors if dix_id[k]['top_level'][0] == 'y']
|
||||
if top_level:
|
||||
return top_level
|
||||
|
||||
# Condition 2: Check Parents
|
||||
parents = [k for k in active_rors if dix_id[k]['parent'][0] == 'y']
|
||||
if parents:
|
||||
return parents
|
||||
|
||||
# Condition 3: Return any active rors
|
||||
return active_rors
|
||||
|
||||
|
||||
|
||||
final_matching = []
|
||||
light_aff_tokens = [clean_string_ror(x) for x in set(light_aff.split())]
|
||||
for id_ in results_upd:
|
||||
country = dix_id_country[id_[2]]
|
||||
if country == 'united states':
|
||||
if 'united states' in light_aff or 'usa' in light_aff_tokens or contains_us_state(light_aff):
|
||||
final_matching.append(id_)
|
||||
|
||||
elif country == 'united kingdom':
|
||||
if 'united kingdom' in light_aff or 'uk' in light_aff_tokens:
|
||||
final_matching.append(id_)
|
||||
|
||||
elif 'korea' in country:
|
||||
|
||||
if 'korea' in light_aff_tokens:
|
||||
final_matching.append(id_)
|
||||
|
||||
elif country in light_aff:
|
||||
final_matching.append(id_)
|
||||
def run_affro_(raw_aff_string):
|
||||
"""
|
||||
Optimized version of run_affro with better performance.
|
||||
|
||||
|
||||
if len(final_matching)>0:
|
||||
result_dict = [{'provenance': 'affro', 'version': VERSION, 'pid':'openorgs', 'value':x[2], 'name': dix_id_name[x[2]], 'confidence':x[0], 'status':x[3], 'country':dix_id_country[x[2]]} if "openorgs" in x[2] else {'provenance': 'affro', 'version': VERSION,'pid':'ror', 'value':x[2], 'name': dix_id_name[x[2]], 'confidence':x[0], 'status':x[3], 'country':dix_id_country[x[2]]} for x in final_matching]
|
||||
return result_dict
|
||||
else:
|
||||
|
||||
return [{'provenance': 'affro', 'version': VERSION, 'pid':'openorgs', 'value':x[2], 'name': dix_id_name[x[2]],'confidence':x[0], 'status':x[3], 'country':dix_id_country[x[2]]} if "openorgs" in x[2] else {'provenance': 'affro', 'version': VERSION, 'pid':'ror', 'value':x[2], 'name': dix_id_name[x[2]], 'confidence':x[0], 'status':x[3], 'country':dix_id_country[x[2]]} for x in results_upd]
|
||||
|
||||
elif len(results_upd)>0:
|
||||
return [{'provenance': 'affro', 'version': VERSION, 'pid':'openorgs', 'value':x[2], 'name': dix_id_name[x[2]], 'confidence':x[0], 'status':x[3], 'country':dix_id_country[x[2]]} if "openorgs" in x[2] else {'provenance': 'affro', 'version': VERSION, 'pid':'ror', 'value':x[2], 'name': dix_id_name[x[2]], 'confidence':x[0], 'status':x[3], 'country':dix_id_country[x[2]]} for x in results_upd]
|
||||
else:
|
||||
result_dict = []
|
||||
|
||||
return result_dict
|
||||
|
||||
Optimizations:
|
||||
- Cache lucky_guess and dix_name lookups to avoid redundant access
|
||||
- Reuse candidate lists instead of recalculating
|
||||
- Streamline conditional logic
|
||||
- Improve final loop efficiency
|
||||
"""
|
||||
|
||||
lucky_guess = clean_string_lucky(raw_aff_string)
|
||||
|
||||
def run_affro(raw_aff_string):
|
||||
lucky_guess = clean_string_ror(raw_aff_string)
|
||||
try:
|
||||
if lucky_guess in dix_org:
|
||||
if dix_mult[lucky_guess] == "unique":
|
||||
if 'openorgs' in dix_org[lucky_guess]:
|
||||
|
||||
return[{'provenance': 'affro', 'version': VERSION, 'pid': 'openorgs', 'value': dix_org[lucky_guess], 'name': dix_id_name[ dix_org[lucky_guess]], 'confidence': 1, 'status': 'active', 'country':dix_id_country[dix_org[lucky_guess]]}]
|
||||
else:
|
||||
if dix_status_new[dix_org[lucky_guess]][0] == 'active':
|
||||
return [{'provenance': 'affro', 'version': VERSION, 'pid': 'ror', 'value': dix_org[lucky_guess], 'name': dix_id_name[ dix_org[lucky_guess]], 'confidence': 1, 'status': 'active', 'country':dix_id_country[dix_org[lucky_guess]]}]
|
||||
elif dix_status_new[dix_org[lucky_guess]][1][0]== '':
|
||||
return [{'provenance': 'affro', 'version': VERSION, 'pid': 'ror', 'value': dix_org[lucky_guess], 'name': dix_id_name[ dix_org[lucky_guess]], 'confidence': 1, 'status': dix_status_new[dix_org[lucky_guess]][0], 'country':dix_id_country[dix_org[lucky_guess]]}]
|
||||
else:
|
||||
res = [{'provenance': 'affro', 'version': VERSION, 'pid' : 'ror', 'value': dix_org[lucky_guess], 'name': dix_id_name[ dix_org[lucky_guess]], 'confidence': 1, 'status': dix_status_new[dix_org[lucky_guess]][0], 'country':dix_id_country[dix_org[lucky_guess]]}]
|
||||
for successor in dix_status_new[dix_org[lucky_guess]][1]:
|
||||
res.append({'provenance': 'affro', 'version': VERSION, 'pid' : 'ror', 'value': successor, 'name': dix_id_name[ dix_org[lucky_guess]], 'confidence': 1, 'status': 'active', 'country':dix_id_country[dix_org[lucky_guess]]})
|
||||
return res
|
||||
else:
|
||||
cand_ids = [
|
||||
key
|
||||
for _, key in dix_city[lucky_guess]
|
||||
if ("ror" in key and dix_status_new[key][0] == "active") or ("openorgs" in key)
|
||||
]
|
||||
num_countries = len(
|
||||
set(
|
||||
dix_id_country[x[1]]
|
||||
for x in dix_city[lucky_guess]
|
||||
if ("ror" in x[1] and dix_status_new[x[1]][0] == "active") or ("openorgs" in x[1])
|
||||
)
|
||||
)
|
||||
|
||||
if len(cand_ids) == 1 or num_countries == 1:
|
||||
if 'openorgs' in dix_org[lucky_guess]:
|
||||
return [{'provenance': 'affro', 'version': VERSION, 'pid': 'openorgs', 'value': dix_org[lucky_guess], 'name': dix_id_name[ dix_org[lucky_guess]], 'confidence': 1, 'status': 'active', 'country':dix_id_country[dix_org[lucky_guess]]}]
|
||||
else:
|
||||
return [{'provenance': 'affro', 'version': VERSION, 'pid': 'ror', 'value': dix_org[lucky_guess], 'name': dix_id_name[ dix_org[lucky_guess]], 'confidence': 1, 'status': 'active', 'country':dix_id_country[dix_org[lucky_guess]]}]
|
||||
|
||||
else:
|
||||
return []
|
||||
else:
|
||||
# print('No lucky guess, running algorithm...')
|
||||
result = find_ror(create_df_algorithm(raw_aff_string, 3), 0.42, 0.82, 500)
|
||||
|
||||
# Check if lucky_guess exists in dix_name
|
||||
if lucky_guess not in dix_name:
|
||||
# Lucky guess miss: use algorithm fallback
|
||||
result = produce_result(create_df_algorithm(raw_aff_string, 3), 0.42, 0.82, 500)
|
||||
return result
|
||||
|
||||
# Cache the dix_name entry to avoid repeated lookups
|
||||
# print('lucky')
|
||||
lucky_match = dix_name[lucky_guess]
|
||||
|
||||
# Single exact match found
|
||||
if len(lucky_match) == 1:
|
||||
triplet = lucky_match[0]
|
||||
id_ = triplet['id']
|
||||
name_ = dix_id[id_]['name']
|
||||
country_ = dix_id[id_]['country']
|
||||
status_ = dix_id[id_]['status']
|
||||
return build_result_list(id_, name_, country_, status_)
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Multiple candidates: filter natively
|
||||
# -------------------------------------------------------------
|
||||
ids = [x['id'] for x in lucky_match]
|
||||
|
||||
# Local helper to avoid repeating the dictionary lookups and return logic
|
||||
def get_result(best_id):
|
||||
return build_result_list(
|
||||
best_id,
|
||||
dix_id[best_id]['name'],
|
||||
dix_id[best_id]['country'],
|
||||
dix_id[best_id]['status']
|
||||
)
|
||||
|
||||
# 1. Country Prioritization: If country is mentioned in lucky_guess, prioritize those candidates
|
||||
country_match_ids = [
|
||||
i for i in ids
|
||||
if any(c and str(c) in lucky_guess for c in dix_id[i].get('country', []))
|
||||
]
|
||||
|
||||
if len(country_match_ids) == 1:
|
||||
return get_result(country_match_ids[0])
|
||||
|
||||
# If there are multiple country matches, or NO country matches, restrict further checks
|
||||
working_ids = country_match_ids if country_match_ids else ids
|
||||
|
||||
# 1.5 Exact Name Prioritization: If raw_aff_string matches the institution name exactly (case-insensitive)
|
||||
name_match_ids = [
|
||||
i for i in working_ids
|
||||
if raw_aff_string.strip().lower() == dix_id[i].get('name', '').strip().lower()
|
||||
]
|
||||
|
||||
if len(name_match_ids) == 1:
|
||||
return get_result(name_match_ids[0])
|
||||
|
||||
working_ids = name_match_ids if name_match_ids else working_ids
|
||||
|
||||
# 2. Filter for 'first' == 'y' among the working_ids
|
||||
first_y_ids = [x['id'] for x in lucky_match if x.get('first') == 'y' and x['id'] in working_ids]
|
||||
|
||||
if len(first_y_ids) == 1:
|
||||
return get_result(first_y_ids[0])
|
||||
|
||||
# 3. Apply secondary conditions
|
||||
candidates_to_filter = first_y_ids if len(first_y_ids) > 1 else working_ids
|
||||
cand_ids = _apply_secondary_conditions(candidates_to_filter)
|
||||
|
||||
# 4. If no candidates remain, fall back to algorithm
|
||||
if not cand_ids:
|
||||
return produce_result(create_df_algorithm(raw_aff_string, 10), 0.42, 0.82, 500)
|
||||
|
||||
# 5. If EXACTLY one remains after secondary conditions, return it
|
||||
if len(cand_ids) == 1:
|
||||
return get_result(cand_ids[0])
|
||||
|
||||
# 6. Multiple candidates STILL remain after secondary conditions.
|
||||
# If the affiliation string is a University, we are allowed to pick a best-effort result
|
||||
if 'university' in lucky_guess:
|
||||
for c_id in cand_ids:
|
||||
if c_id in first_y_ids:
|
||||
return get_result(c_id)
|
||||
return get_result(cand_ids[0])
|
||||
|
||||
# To avoid making a blind guess between identical ties for other institutions, return no result.
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
# Return some indication of an error, or log the row
|
||||
print(f"Error: {str(e)}")
|
||||
print(raw_aff_string)
|
||||
pass
|
||||
print(f"Error in run_affro: {str(e)}")
|
||||
print(f"Input: {raw_aff_string}")
|
||||
return []
|
||||
|
||||
@lru_cache(maxsize=32768)
|
||||
def _run_affro_cached(aff, check_country=True):
|
||||
#print(f"Running run_affro for affiliation: {aff}")
|
||||
# lucky_guess = clean_string_lucky(aff)
|
||||
direct = direct_mapping(aff)
|
||||
if not direct[0]:
|
||||
res = run_affro_(aff)
|
||||
else:
|
||||
combined = direct[0] + run_affro_(direct[1])
|
||||
# Deduplicate by 'value' (ROR ID), keeping first occurrence
|
||||
seen = set()
|
||||
deduped = []
|
||||
for entry in combined:
|
||||
if entry['value'] not in seen:
|
||||
seen.add(entry['value'])
|
||||
deduped.append(entry)
|
||||
res = deduped
|
||||
|
||||
if check_country:
|
||||
res = discard_country_mismatched_institutes(res, aff=aff)
|
||||
return tuple(res)
|
||||
|
||||
|
||||
def run_affro(aff, check_country=True, **kwargs):
|
||||
if 'check_city' in kwargs and not kwargs['check_city']:
|
||||
check_country = False
|
||||
return list(_run_affro_cached(aff, check_country=check_country))
|
||||
|
||||
run_affro.cache_info = _run_affro_cached.cache_info
|
||||
run_affro.cache_clear = _run_affro_cached.cache_clear
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def matchings_affro(aff_string):
|
||||
def matchings_affro(aff_string, check_country=True, **kwargs):
|
||||
# global operation_counter
|
||||
try:
|
||||
matchings = run_affro(aff_string)
|
||||
matchings = run_affro(aff_string, check_country=check_country, **kwargs)
|
||||
# operation_counter += 1
|
||||
|
||||
# Ensure matchings is a list, even if affro returns a single dict
|
||||
|
|
@ -168,7 +354,7 @@ def matchings_affro(aff_string):
|
|||
# Create the result as a tuple that matches matchings_schema
|
||||
result = []
|
||||
for matching in matchings:
|
||||
# Assuming 'matching' is a dictionary that contains 'provenance', 'version', 'pid', 'value', 'name', 'confidence', 'status', 'country'
|
||||
# Assuming 'matching' is a dictionary that contains 'provenance', 'affro', 'value', 'confidence', 'status'
|
||||
result.append((
|
||||
matching.get("provenance", None),
|
||||
matching.get("version", None),
|
||||
|
|
@ -178,7 +364,6 @@ def matchings_affro(aff_string):
|
|||
float(matching.get("confidence", None)),
|
||||
matching.get("status", None),
|
||||
matching.get("country", None)
|
||||
|
||||
))
|
||||
if len(result)>0:
|
||||
return result
|
||||
|
|
@ -189,5 +374,4 @@ def matchings_affro(aff_string):
|
|||
return ()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
__version__ = "3.4.0"
|
||||
|
|
@ -1,91 +1,162 @@
|
|||
from affro.helpers.functions import *
|
||||
|
||||
def valueToCategory(value):
|
||||
flag = 0
|
||||
|
||||
for k in categ_dicts:
|
||||
if k in value and categ_dicts[k] in categ_string.split('|'):
|
||||
flag = 1
|
||||
return flag
|
||||
|
||||
keys = []
|
||||
values = []
|
||||
for k in categ_dicts:
|
||||
if k in value and categ_dicts[k] in _CATEG_SET:
|
||||
if k not in keys:
|
||||
keys.append(k)
|
||||
values.append(categ_dicts[k])
|
||||
|
||||
return [keys, values[0]] if len(values) > 0 else [[], '']
|
||||
|
||||
|
||||
# Pre-compute category set once at module level (avoids rebuilding on every call)
|
||||
_CATEG_SET = frozenset(categ_string.split('|'))
|
||||
|
||||
# Pre-compute protect set for O(1) lookup
|
||||
protect = {
|
||||
'national univer ireland',
|
||||
'univer',
|
||||
'univer california',
|
||||
'univer colege hospital',
|
||||
'univer colege',
|
||||
'univer hospital',
|
||||
'imperial colege',
|
||||
'city univer',
|
||||
'univer medical school',
|
||||
'california state univer',
|
||||
'national techn univer',
|
||||
'techn univer',
|
||||
'islamic azad univer',
|
||||
'univer nevada',
|
||||
'univer maryland',
|
||||
'state univer',
|
||||
'rijksuniver',
|
||||
'rijks univer',
|
||||
'univer medical center',
|
||||
'royal colege surgeons',
|
||||
'st patricks colege',
|
||||
'instit techn',
|
||||
'trinity colege',
|
||||
'st johns colege',
|
||||
'wiliam beaumont hospital',
|
||||
'american colege',
|
||||
'univer studi'
|
||||
}
|
||||
|
||||
keep = {'univer','trinity colege','imperial colege' ,'colege surgeons'}
|
||||
|
||||
# Pre-compute city_names and countries as sets for O(1) lookup
|
||||
city_names_set = None
|
||||
countries_set = None
|
||||
remove_list_set = None
|
||||
_city_or_remove_set = None
|
||||
_city_or_country_set = None
|
||||
|
||||
def _init_sets():
|
||||
"""Initialize sets on first call for O(1) lookups."""
|
||||
global city_names_set, countries_set, remove_list_set, _city_or_remove_set, _city_or_country_set
|
||||
if city_names_set is None:
|
||||
city_names_set = set(city_names)
|
||||
countries_set = set(countries)
|
||||
remove_list_set = set(remove_list)
|
||||
_city_or_remove_set = frozenset(city_names_set | remove_list_set)
|
||||
_city_or_country_set = frozenset(city_names_set | countries_set)
|
||||
|
||||
def _check_author_words(text):
|
||||
"""Pre-compile exclusion word checks."""
|
||||
author_words = {'asistant', 'researcher', 'phd', 'student', 'section', 'prof', 'director'}
|
||||
# required_words = {'school', 'univ', 'inst', 'lab', 'fac','hospital'}
|
||||
|
||||
return any(word in text for word in author_words)#, any(word in text for word in required_words)
|
||||
|
||||
# tokenization
|
||||
protect = ['national univer ireland',
|
||||
'univer',
|
||||
'univer california',
|
||||
'univer colege hospital',
|
||||
'univer colege',
|
||||
'univer hospital',
|
||||
'imperial colege',
|
||||
'city univer',
|
||||
'univer medical school',
|
||||
'california state univer',
|
||||
'national techn univer',
|
||||
'techn univer',
|
||||
'islamic azad univer',
|
||||
'univer nevada',
|
||||
'univer maryland',
|
||||
'state univer',
|
||||
'rijksuniver',
|
||||
'rijks univer',
|
||||
'univer medical center'
|
||||
]
|
||||
|
||||
def create_df_algorithm(raw_aff_string, radius_u):
|
||||
"""
|
||||
OPTIMIZED VERSION:
|
||||
- Pre-compute sets for O(1) lookups instead of repeated 'in' checks
|
||||
- Avoid redundant valueToCategory() calls by caching results
|
||||
- Combine string operations where possible
|
||||
- Use set operations for filtering
|
||||
"""
|
||||
_init_sets()
|
||||
# print("raw_aff_string:", raw_aff_string)
|
||||
clean_aff = clean_string(remove_outer_parentheses(remove_leading_numbers(raw_aff_string)))
|
||||
#print(0, clean_aff)
|
||||
# print("clean_aff:", clean_aff)
|
||||
countries_list = description(clean_aff)[1]
|
||||
aff_no_symbols_d = substrings_dict(reduce(clean_aff))
|
||||
#print(0.5, aff_no_symbols_d)
|
||||
aff_no_symbols_d = substrings_dict(reduce(clean_aff))
|
||||
# print("aff_no_symbols_d:", aff_no_symbols_d)
|
||||
substring_list = [replace_abbr_univ(x) for x in list(aff_no_symbols_d.values())]
|
||||
#print(1, substring_list)
|
||||
# print("substring_list after replace_abbr_univ:", substring_list)
|
||||
i = 0
|
||||
|
||||
while i < len(substring_list) - 1:
|
||||
if substring_list[i] in protect and any(name in substring_list[i+1] for name in city_names): #substring_list[i+1] in city_names:
|
||||
substring_list[i] = substring_list[i] + ' ' + substring_list[i+1]
|
||||
i = i+2
|
||||
current = substring_list[i]
|
||||
next_item = substring_list[i + 1]
|
||||
|
||||
# OPTIMIZATION: Use set membership for O(1) checks
|
||||
if current in protect and any(
|
||||
name in next_item#.split()
|
||||
for name in _city_or_country_set
|
||||
# if name != "univer"
|
||||
):
|
||||
# print('Merging protected:', current, 'with next:', next_item)
|
||||
substring_list[i] = current + ' ' + next_item
|
||||
i += 2
|
||||
continue
|
||||
|
||||
elif ('assistant' in substring_list[i] or 'researcher' in substring_list[i] or 'phd' in substring_list[i] or 'student' in substring_list[i] or 'section' in substring_list[i] or 'prof' in substring_list[i] or 'director' in substring_list[i]) and (not 'school' in substring_list[i+1] or 'univ', substring_list[i+1] or 'inst' in substring_list[i+1] or 'lab' in substring_list[i+1] or 'fac' in substring_list[i+1]):
|
||||
if not 'univ' in substring_list[i]:
|
||||
|
||||
# OPTIMIZATION: Pre-compute exclusion checks
|
||||
has_exclusion = _check_author_words(current)
|
||||
#has_exclusion, has_required = _check_exclusion_words(current)
|
||||
|
||||
# print(has_exclusion, has_required)
|
||||
if (has_exclusion and (not 'school' in next_item or any(w in next_item for w in {'univ', 'inst', 'lab', 'fac','colege'}))) or ('colege' in current and 'univ' in next_item): #if not 'univ' in current and 'trinity colege' not in current and 'colege surgeons' not in current:
|
||||
if not any(sub in current for sub in keep):
|
||||
|
||||
substring_list.pop(i)
|
||||
else:
|
||||
i = i+1
|
||||
i += 1
|
||||
|
||||
elif ('engineer' in substring_list[i] or 'progr'in substring_list[i] or 'unit' in substring_list[i] or 'dep' in substring_list[i] or 'school' in substring_list[i] #or 'lab' in substring_list[i] # or 'inst' in substring_list[i] #or is_contained('hosp', substring_list[i])
|
||||
or 'fac' in substring_list[i]) and 'univ' in substring_list[i+1]:
|
||||
if not 'univ' in substring_list[i]:
|
||||
elif any(word in current for word in {'engineer', 'progr', 'unit', 'dep', 'school', 'fac'}) and 'univ' in next_item:
|
||||
if not 'univ' in current and not 'hospital' in current:
|
||||
substring_list.pop(i)
|
||||
else:
|
||||
i = i+1
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# elif 'lab' in substring_list[i] and ('colege' in substring_list[i+1] or 'dep' in substring_list[i+1] or 'school' in substring_list[i+1]):
|
||||
# if not 'univ' in substring_list[i]: #'inst' in substring_list[i+1] or
|
||||
# substring_list.pop(i)
|
||||
# else:
|
||||
# i = i+1
|
||||
# continue
|
||||
|
||||
else:
|
||||
i += 1
|
||||
# print(1.4, substring_list)
|
||||
|
||||
light_aff = (', '.join((substring_list)))
|
||||
# print(1.5, light_aff)
|
||||
|
||||
substring_list = [x for x in substring_list if x.replace(' gmbh','') not in city_names+remove_list]
|
||||
# print(1.7,substring_list)
|
||||
|
||||
substring_list0 = [shorten_keywords([x], radius_u) for x in substring_list if len(shorten_keywords([x],radius_u))>0]
|
||||
# print(2,substring_list0 )
|
||||
|
||||
substring_list1 = [inner for outer in substring_list0 for inner in outer]
|
||||
# print(3,substring_list1 )
|
||||
|
||||
aff_list = [{"index": i, "keywords": substring_list1[i], "category": valueToCategory(substring_list1[i])} for i in range(len(substring_list1))]
|
||||
|
||||
filtered_list = [entry for entry in aff_list if entry.get("category") == 1]
|
||||
|
||||
return [clean_aff, light_aff, filtered_list, countries_list]
|
||||
# print("substring_list before merging:", substring_list)
|
||||
light_aff = ', '.join(substring_list)
|
||||
# print("light_aff:", light_aff)
|
||||
# OPTIMIZATION: Use set for filtering instead of list comprehension
|
||||
substring_list = [x for x in substring_list if x.replace(' gmbh', '') not in _city_or_remove_set]
|
||||
|
||||
# OPTIMIZATION: Cache valueToCategory results to avoid recomputation
|
||||
# OPTIMIZATION: compute shorten_keywords once per element (was called twice: once for guard, once for value)
|
||||
substring_list1 = []
|
||||
for x in substring_list:
|
||||
shortened = shorten_keywords([x], radius_u)
|
||||
if shortened:
|
||||
substring_list1.extend(shortened)
|
||||
|
||||
# OPTIMIZATION: Single pass through valueToCategory instead of multiple passes
|
||||
aff_list = []
|
||||
keys_list_raw = []
|
||||
|
||||
for i, keyword in enumerate(substring_list1):
|
||||
category_result = valueToCategory(keyword)
|
||||
if category_result[1] != '':
|
||||
aff_list.append({
|
||||
"index": i,
|
||||
"keywords": keyword,
|
||||
"category": category_result[1]
|
||||
})
|
||||
if len(category_result[0]) > 0:
|
||||
keys_list_raw.extend(category_result[0])
|
||||
|
||||
# OPTIMIZATION: Use set to remove duplicates O(n) instead of nested list comprehension
|
||||
keys_list = list(set(keys_list_raw))
|
||||
|
||||
return [clean_aff, light_aff, aff_list, countries_list, keys_list]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,245 @@
|
|||
from affro.helpers.functions import *
|
||||
from . import __version__
|
||||
|
||||
VERSION = __version__
|
||||
|
||||
def distance(s, substr1, substr2):
|
||||
pos1 = s.find(substr1) # start index of substr1
|
||||
pos2 = s.find(substr2) # start index of substr2
|
||||
|
||||
# One of the substrings not found
|
||||
|
||||
distance = abs(pos1 - pos2)
|
||||
return distance
|
||||
|
||||
# Sort by descending key length so the most specific keys are checked first,
|
||||
# giving earlier matches and fewer iterations on average.
|
||||
_LABELS = ('forth', 'demokritos', 'ircs', 'fraunhofer', 'cnr', 'max_planck', 'helmholtz', 'leibniz', 'infn')
|
||||
|
||||
def _build_all_label_keys():
|
||||
"""Return dict of {label: (sorted_keys, key_to_id)} in a single pass over dix_name."""
|
||||
keys_by_label = {lbl: [] for lbl in _LABELS}
|
||||
id_by_label = {lbl: {} for lbl in _LABELS}
|
||||
|
||||
for k, v in dix_name.items():
|
||||
for d in v:
|
||||
lbl = d.get('label')
|
||||
if lbl in keys_by_label:
|
||||
if lbl == 'cnr' and k in {'isti', 'cnr isti'}:
|
||||
continue
|
||||
keys_by_label[lbl].append(k)
|
||||
id_by_label[lbl][k] = d['id']
|
||||
break
|
||||
|
||||
result = {}
|
||||
for lbl in _LABELS:
|
||||
sorted_keys = sorted(keys_by_label[lbl], key=len, reverse=True)
|
||||
result[lbl] = (sorted_keys, id_by_label[lbl])
|
||||
return result
|
||||
|
||||
_ALL_LABEL_KEYS = _build_all_label_keys()
|
||||
|
||||
forth_keys, forth_id = _ALL_LABEL_KEYS["forth"]
|
||||
demokritos_keys, demokritos_id = _ALL_LABEL_KEYS["demokritos"]
|
||||
ircs_keys, ircs_id = _ALL_LABEL_KEYS["ircs"]
|
||||
fraunhofer_keys, fraunhofer_id = _ALL_LABEL_KEYS["fraunhofer"]
|
||||
cnr_keys, cnr_id = _ALL_LABEL_KEYS["cnr"]
|
||||
max_planck_keys, max_planck_id = _ALL_LABEL_KEYS["max_planck"]
|
||||
helmholtz_keys, helmholtz_id = _ALL_LABEL_KEYS["helmholtz"]
|
||||
leibniz_keys, leibniz_id = _ALL_LABEL_KEYS["leibniz"]
|
||||
infn_keys, infn_id = _ALL_LABEL_KEYS["infn"]
|
||||
|
||||
dashed_names = load_json('./jsons/dashed_names.json.gz')
|
||||
|
||||
names_dict = load_json('./jsons/dix_name_id.json.gz')
|
||||
|
||||
def _make_dict_entry(pid, id, info):
|
||||
return {
|
||||
'provenance': 'dictionaries',
|
||||
'version': VERSION,
|
||||
'pid': pid,
|
||||
'value': id,
|
||||
'name': info['name'],
|
||||
'confidence': 1,
|
||||
'status': info['status'][0],
|
||||
'country': info['country'],
|
||||
}
|
||||
|
||||
def _make_entry(pid, id, info):
|
||||
return {
|
||||
'provenance': 'affro_direct',
|
||||
'version': VERSION,
|
||||
'pid': pid,
|
||||
'value': id,
|
||||
'name': info['name'],
|
||||
'confidence': 1,
|
||||
'status': info['status'][0],
|
||||
'country': info['country'],
|
||||
|
||||
}
|
||||
|
||||
|
||||
def direct_mapping_schema(list_ids):
|
||||
res = []
|
||||
for id in set(list_ids):
|
||||
info = dix_id[id] # single lookup per id
|
||||
if 'ror' in id:
|
||||
res.append(_make_entry('ror', id, info))
|
||||
if info['status'][0] != 'active':
|
||||
for successor in info['status'][1]:
|
||||
res.append(_make_entry('ror', successor, dix_id[successor]))
|
||||
else:
|
||||
res.append(_make_entry('openorgs', id, info))
|
||||
return res
|
||||
|
||||
def direct_mapping(aff):
|
||||
clean_aff_str = str(aff).strip().lower()
|
||||
if clean_aff_str in names_dict:
|
||||
match_id = names_dict[clean_aff_str]
|
||||
info = dix_id[match_id]
|
||||
pid = 'ror' if 'ror' in match_id else 'openorgs'
|
||||
res = [_make_dict_entry(pid, match_id, info)]
|
||||
if pid == 'ror' and info['status'][0] != 'active' and len(info['status']) > 1:
|
||||
for successor in info['status'][1]:
|
||||
if successor and successor in dix_id:
|
||||
res.append(_make_dict_entry('ror', successor, dix_id[successor]))
|
||||
return [res, '']
|
||||
|
||||
assigned = []
|
||||
|
||||
# stem univ, inst
|
||||
aff_cleaned = normalize_organization_names(clean_string(aff), university_terms).replace(" and ", " ").replace("-", " ")
|
||||
shorten_aff = re.sub(r' +', ' ', aff_cleaned)
|
||||
|
||||
for name in dashed_names:
|
||||
if name in shorten_aff and any(country in shorten_aff for country in dix_id[dashed_names[name]]['country']):
|
||||
assigned.append(dashed_names[name])
|
||||
|
||||
shorten_aff = shorten_aff.replace(name, " ")
|
||||
|
||||
if 'instit aplied physics photonic materials' in shorten_aff and 'dresden' in shorten_aff:
|
||||
assigned.append('https://ror.org/031whys72')
|
||||
shorten_aff = shorten_aff.replace('instit aplied physics photonic materials', '')
|
||||
|
||||
if 'europ univer' in shorten_aff and 'cyprus' in shorten_aff:
|
||||
assigned.append('https://ror.org/04xp48827')
|
||||
shorten_aff = shorten_aff.replace('europ univer', '')
|
||||
|
||||
if 'univer north carolina' in shorten_aff and 'chapel hil' in shorten_aff:
|
||||
assigned.append('https://ror.org/0130frc33')
|
||||
shorten_aff = shorten_aff.replace('univer north carolina', '')
|
||||
|
||||
if 'frederick univer' in shorten_aff:
|
||||
assigned.append('https://ror.org/05d8tf882')
|
||||
shorten_aff = shorten_aff.replace('frederick univer', '')
|
||||
|
||||
if 'trinity colege dublin' in shorten_aff or 'dublin trinity colege' in shorten_aff:
|
||||
assigned.append('https://ror.org/02tyrky19')
|
||||
shorten_aff = shorten_aff.replace('trinity colege dublin', '').replace('dublin trinity colege', '')
|
||||
|
||||
if 'colege' in shorten_aff:
|
||||
if 'intercolege' in shorten_aff and ('cyprus' in shorten_aff or 'nicosia' in shorten_aff):
|
||||
assigned.append('https://ror.org/012gfrj24')
|
||||
shorten_aff = shorten_aff.replace('intercolege', '')
|
||||
if 'mary immaculate colege' in shorten_aff and ('limerick' in shorten_aff or 'ireland' in shorten_aff):
|
||||
assigned.append('https://ror.org/009q3yg92')
|
||||
shorten_aff = shorten_aff.replace('mary immaculate colege', ',')
|
||||
|
||||
if 'foundation research techn' in shorten_aff and ('helas' in shorten_aff or 'greece' in shorten_aff):
|
||||
# print('h')
|
||||
assigned.append('https://ror.org/052rphn09')
|
||||
shorten_aff = shorten_aff.replace('foundation research techn', '')
|
||||
|
||||
if 'helmholtz zentrum' in shorten_aff or 'helmholtz center' in shorten_aff:
|
||||
assigned.append('https://ror.org/00cfam450')
|
||||
|
||||
if 'instit' in shorten_aff or "istituto" in shorten_aff:
|
||||
|
||||
if 'friedrich loefler' in shorten_aff:
|
||||
assigned.append('https://ror.org/025fw7a54')
|
||||
shorten_aff = shorten_aff.replace('friedrich loefler instit', '').replace('bundesforschungsinstit tiergesundheit', '').replace('federal research instit animal health', '')
|
||||
|
||||
if 'julius kuhn' in shorten_aff or 'julius kuehn' in shorten_aff:
|
||||
assigned.append('https://ror.org/022d5qt08')
|
||||
shorten_aff = shorten_aff.replace('julius kuhn instit', '').replace('julius kuehn', '').replace('bundesforschungsinstit kulturpflanzen', '').replace('federal research center cultivated plants', '')
|
||||
|
||||
if "forth" in shorten_aff or 'foundation research techn' in shorten_aff:
|
||||
for key in forth_keys:
|
||||
if key in shorten_aff and (distance(shorten_aff, 'forth', key) < len(key)+len('forth')+5 or 'foundation research techn' in shorten_aff):
|
||||
ror_id = forth_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
shorten_aff = shorten_aff.replace(key, ',')
|
||||
|
||||
if "demokritos" in shorten_aff or "ncsr" in shorten_aff:
|
||||
for key in demokritos_keys:
|
||||
if key in shorten_aff and distance(shorten_aff, 'demokritos', key) < len(key)+len('demokritos')+5:
|
||||
ror_id = demokritos_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
shorten_aff = shorten_aff.replace(key, ',')
|
||||
|
||||
if 'tu clausthal' in shorten_aff and 'instit organische chemie' in shorten_aff and distance(shorten_aff, 'tu clausthal', 'instit organische chemie') < len('tu clausthal')+len('instit organische chemie')+5:
|
||||
assigned.append('openorgs____::0000103105')
|
||||
shorten_aff = shorten_aff.replace('instit organische chemie', ',')
|
||||
|
||||
if 'ircs' in shorten_aff or 'istituti ricovero e cura caratere scien' in shorten_aff or 'milan' in shorten_aff:
|
||||
for key in ircs_keys:
|
||||
if key in shorten_aff and distance(shorten_aff, 'ircs', key) < len(key)+len('istituti ricovero e cura caratere scien')+5:
|
||||
ror_id = ircs_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
shorten_aff = shorten_aff.replace(key, ',')
|
||||
if 'infn' in shorten_aff or 'istituto nazionale fisica nucleare' in shorten_aff:
|
||||
for key in infn_keys:
|
||||
if key in shorten_aff:
|
||||
ror_id = infn_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
shorten_aff = shorten_aff.replace(key, ',')
|
||||
|
||||
if ('instit comunication computer systems' in shorten_aff or 'ics' in shorten_aff or 'instit computer comunication systems') and ('national techn univer athens' in aff_cleaned or 'ntua' in aff_cleaned):
|
||||
assigned.append('https://ror.org/0483fn738')
|
||||
shorten_aff = shorten_aff.replace('instit comunication computer systems', ',')
|
||||
|
||||
if 'fraunhofer' in shorten_aff:
|
||||
for key in fraunhofer_keys:
|
||||
if key in shorten_aff and distance(shorten_aff, 'fraunhofer', key) < len(key)+len('fraunhofer')+5:
|
||||
ror_id = fraunhofer_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
shorten_aff = shorten_aff.replace(key, ',').replace('fraunhofer', ',')
|
||||
|
||||
if 'cnr' in shorten_aff or ('national research council' in shorten_aff and 'italy' in shorten_aff):
|
||||
for key in cnr_keys:
|
||||
if key in shorten_aff and distance(shorten_aff, 'cnr', key) < len(key)+len('national research council')+5:
|
||||
ror_id = cnr_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
shorten_aff = shorten_aff.replace(key, ',')
|
||||
|
||||
if 'max planck' in shorten_aff:
|
||||
for key in max_planck_keys:
|
||||
if key in shorten_aff and distance(shorten_aff, 'max planck', key) < len(key)+len('max planck')+15:
|
||||
ror_id = max_planck_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
shorten_aff = shorten_aff.replace(key, ',')
|
||||
|
||||
if 'helmholtz' in shorten_aff:
|
||||
for key in helmholtz_keys:
|
||||
if key in shorten_aff and distance(shorten_aff, 'helmholtz', key) < len(key)+len('helmholtz')+15:
|
||||
ror_id = helmholtz_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
|
||||
if 'leibniz' in shorten_aff:
|
||||
for key in leibniz_keys:
|
||||
if key in shorten_aff and distance(shorten_aff, 'leibniz', key) < len(key)+len('leibniz')+15:
|
||||
ror_id = leibniz_id.get(key)
|
||||
if ror_id is not None:
|
||||
assigned.append(ror_id)
|
||||
shorten_aff = shorten_aff.replace(key, ',')
|
||||
|
||||
|
||||
return [direct_mapping_schema(assigned), shorten_aff]
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
from affro.helpers.functions import *
|
||||
from affro.helpers.create_input import *
|
||||
from .. import __version__
|
||||
|
||||
VERSION = __version__
|
||||
|
||||
|
||||
|
||||
pattern = re.compile(r'\b(' + '|'.join(us_states) + r')\b')
|
||||
|
||||
def contains_us_state(text):
|
||||
return bool(pattern.search(text.lower()))
|
||||
|
||||
# def contains_gr_cities(text):
|
||||
# return bool(pattern.search(text.lower()))
|
||||
|
||||
# def contains_ie_cities(text):
|
||||
# return bool(pattern.search(text.lower()))
|
||||
|
||||
# weak_keywords = ['department']
|
||||
|
||||
def convert_to_result(id_list_, dix_id):
|
||||
result = []
|
||||
seen = set()
|
||||
|
||||
for r in id_list_:
|
||||
score = r[1]
|
||||
if score > 1.0:
|
||||
score = 1.0
|
||||
|
||||
value = r[2]
|
||||
rec = dix_id.get(value)
|
||||
if not rec:
|
||||
continue
|
||||
|
||||
name = rec.get('name')
|
||||
country = rec.get('country')
|
||||
|
||||
status = rec.get('status') or []
|
||||
primary = status[0] if status else None
|
||||
secondary = status[1] if len(status) > 1 else []
|
||||
|
||||
# openorgs
|
||||
if "openorgs" in value:
|
||||
key = ('openorgs', value)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
result.append({
|
||||
'provenance': 'affro',
|
||||
'version': VERSION,
|
||||
'pid': 'openorgs',
|
||||
'value': value,
|
||||
'name': name,
|
||||
'confidence': float(score),
|
||||
'status': 'active',
|
||||
'country': country
|
||||
})
|
||||
continue
|
||||
|
||||
# ROR active
|
||||
if primary == 'active':
|
||||
key = ('ror', value)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
result.append({
|
||||
'provenance': 'affro',
|
||||
'version': VERSION,
|
||||
'pid': 'ror',
|
||||
'value': value,
|
||||
'name': name,
|
||||
'confidence': float(score),
|
||||
'status': 'active',
|
||||
'country': country
|
||||
})
|
||||
continue
|
||||
|
||||
# ROR inactive
|
||||
key = ('ror', value)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
result.append({
|
||||
'provenance': 'affro',
|
||||
'version': VERSION,
|
||||
'pid': 'ror',
|
||||
'value': value,
|
||||
'name': name,
|
||||
'confidence': float(score),
|
||||
'status': primary,
|
||||
'country': country
|
||||
})
|
||||
|
||||
# linked active records
|
||||
if secondary and secondary[0] != '':
|
||||
for link in secondary:
|
||||
if not link:
|
||||
continue
|
||||
|
||||
link_key = ('ror', link)
|
||||
if link_key in seen:
|
||||
continue
|
||||
|
||||
link_rec = dix_id.get(link)
|
||||
if not link_rec:
|
||||
continue
|
||||
|
||||
seen.add(link_key)
|
||||
result.append({
|
||||
'provenance': 'affro',
|
||||
'version': VERSION,
|
||||
'pid': 'ror',
|
||||
'value': link,
|
||||
'name': link_rec.get('name'),
|
||||
'confidence': float(score),
|
||||
'status': 'active',
|
||||
'country': link_rec.get('country')
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
def count_active(items):
|
||||
return sum(1 for x in items if x.get("status") == "active")
|
||||
|
||||
def disamb(input, id_list_,dix_id):
|
||||
# print('disamb id_list_', id_list_)
|
||||
if id_list_ == []:
|
||||
return []
|
||||
clean_aff = input[0]
|
||||
# print('result_dict',result_dict)
|
||||
if 'https://ror.org/00nnh8h94' in str(id_list_) and ('greece' not in clean_aff and 'helas' not in clean_aff and 'athens' not in clean_aff):
|
||||
id_list_ = [id for id in id_list_ if id[2] != 'https://ror.org/00nnh8h94']
|
||||
result_dict = convert_to_result(id_list_, dix_id)
|
||||
|
||||
num_actives = count_active(result_dict)
|
||||
num_countries = len(description(clean_aff)[1])
|
||||
|
||||
# print('num_actives', num_actives)
|
||||
if len(id_list_) ==1 :
|
||||
return result_dict
|
||||
elif num_countries == 0:
|
||||
# print('no country in affiliation')
|
||||
# polytechnic?
|
||||
# countries_uni = {
|
||||
# country
|
||||
# for res in result_dict
|
||||
# if 'Uni' in res['name'] or 'Trinity' in res['name']
|
||||
# for country in res['country'] }-low_prob_countries
|
||||
|
||||
countries_high = {
|
||||
country
|
||||
for res in result_dict
|
||||
for country in res['country']
|
||||
if country not in low_prob_countries
|
||||
}
|
||||
# print(countries_high)
|
||||
# print('countries_uni',countries_uni)
|
||||
|
||||
#[res['country'] for res in result_dict if 'Uni' in res['name']]
|
||||
if len(countries_high) >0:
|
||||
#If we suspect the affiliation refers to a university, restrict matches to the same country as detected university candidates
|
||||
final_matching = [res for res in result_dict if any(c in countries_high for c in res['country'])]
|
||||
if len(final_matching)==1 or len(countries_high)==1:
|
||||
return final_matching
|
||||
else:
|
||||
final_matching2 = [res for res in final_matching if 'Inst' not in res['name'] and 'Lab' not in res['name']]
|
||||
return final_matching2
|
||||
|
||||
else:
|
||||
# print('no universities')
|
||||
return result_dict
|
||||
|
||||
elif num_actives > num_countries:
|
||||
# print('more results than countries')
|
||||
final_matching = []
|
||||
light_aff_tokens = [clean_string_ror(x) for x in set(clean_aff.split())]
|
||||
for res in result_dict:
|
||||
# print('res',res)
|
||||
|
||||
country = res['country']
|
||||
country_str = str(country)
|
||||
if 'united states' in country:
|
||||
if 'united states' in clean_aff or 'usa' in light_aff_tokens or contains_us_state(clean_aff):
|
||||
final_matching.append(res)
|
||||
|
||||
elif 'united kingdom' in country:
|
||||
if 'united kingdom' in clean_aff or 'uk' in light_aff_tokens:
|
||||
final_matching.append(res)
|
||||
|
||||
elif 'korea' in country_str:
|
||||
|
||||
if 'korea' in light_aff_tokens:
|
||||
final_matching.append(res)
|
||||
# elif 'greece' in country:
|
||||
# if 'greece' in clean_aff or 'helas' in clean_aff or contains_gr_cities(clean_aff):
|
||||
# inal_matching.append(res)
|
||||
|
||||
|
||||
elif any(c in clean_aff for c in country): #country in clean_aff:
|
||||
final_matching.append(res)
|
||||
|
||||
if final_matching:
|
||||
return final_matching
|
||||
|
||||
else:
|
||||
return result_dict
|
||||
|
||||
elif len(result_dict)>0:
|
||||
return result_dict
|
||||
else:
|
||||
# print('leider nichts')
|
||||
return []
|
||||
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
from affro.helpers.functions import *
|
||||
from affro.helpers.create_input import *
|
||||
|
||||
low_confidence_ids = {'https://ror.org/003agka18', 'https://ror.org/00wra1b14','https://ror.org/05fe6f045',
|
||||
'https://ror.org/05pz81c75', 'https://ror.org/05q882j59', 'https://ror.org/00tpdkh38',
|
||||
'https://ror.org/04fnhr972','https://ror.org/04155gf20', 'https://ror.org/0243vac68', 'https://ror.org/00p61c264',
|
||||
'https://ror.org/01p2ej961','https://ror.org/04vwgk321','https://ror.org/01pc4rp54','https://ror.org/034ymz395',
|
||||
'https://ror.org/018xp1p31', 'https://ror.org/042tmw509', 'https://ror.org/035qa0k76','https://ror.org/009ytmf35',
|
||||
'https://ror.org/048gmay44', 'https://ror.org/04wvz8b06', 'https://ror.org/01rk3xf02', 'https://ror.org/03gbw6p94', 'https://ror.org/02jjdwm75',
|
||||
'https://ror.org/05byh2e17', 'https://ror.org/02eaafc18'}
|
||||
|
||||
|
||||
specific = set(k for k in categ_dicts if categ_dicts[k] in {'Specific', 'Acronyms'})
|
||||
|
||||
special_countries = {k for k, v in country_synonyms.items() if len(v) > 1}
|
||||
|
||||
excluded = {'univ', 'inst', 'national', 'nacional', 'colege', 'center', 'organization', 'hospital'}
|
||||
|
||||
|
||||
|
||||
def keep_highest_score(data):
|
||||
"""
|
||||
Keeps only one entry per unique ID.
|
||||
Keeps the one with highest confidence score.
|
||||
"""
|
||||
best = {}
|
||||
|
||||
for org, conf, id_ in data:
|
||||
if id_ not in best or conf > best[id_][1]:
|
||||
best[id_] = [org, conf, id_]
|
||||
|
||||
return list(best.values())
|
||||
|
||||
|
||||
def _match_special_country(country_list, text, tokens):
|
||||
"""
|
||||
Check if any special-country synonym appears in the affiliation text.
|
||||
Safer token-based matching.
|
||||
"""
|
||||
country_syns = {
|
||||
synonym
|
||||
for country in country_list
|
||||
for synonym in country_synonyms.get(country, [country])
|
||||
}
|
||||
|
||||
# Strict token match first
|
||||
if country_syns & tokens:
|
||||
return True
|
||||
|
||||
# Controlled substring match (avoid very short tokens like 'us')
|
||||
for syn in country_syns:
|
||||
if len(syn) > 3 and re.search(r'\b' + re.escape(syn) + r'\b', text):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def disamb_country(org, light_aff):
|
||||
ids = []
|
||||
|
||||
for rec in dix_name[org]:
|
||||
country_list = rec['country']
|
||||
if any(c in light_aff for c in country_list) and \
|
||||
not any(c in org for c in country_list):
|
||||
ids.append(rec['id'])
|
||||
|
||||
return ids
|
||||
|
||||
|
||||
def disamb_city(org, light_aff):
|
||||
ids = []
|
||||
|
||||
for rec in dix_name[org]:
|
||||
city_list = rec['city']
|
||||
if any(c in light_aff for c in city_list) and \
|
||||
not any(c in org for c in city_list):
|
||||
ids.append(rec['id'])
|
||||
|
||||
return ids
|
||||
|
||||
|
||||
def disamb_city_country(org, light_aff):
|
||||
city_ids = set(disamb_city(org, light_aff))
|
||||
country_ids = set(disamb_country(org, light_aff))
|
||||
return list(city_ids & country_ids)
|
||||
|
||||
|
||||
def find_id(aff_input, best_names, dix_name, simG):
|
||||
clean_aff = aff_input[0]
|
||||
# print('find_id')
|
||||
# print(clean_aff)
|
||||
light_aff = aff_input[1]
|
||||
# print(best_names)
|
||||
id_list = []
|
||||
|
||||
clean_aff_tokens = {x.replace(',', '') for x in clean_aff.split()}
|
||||
light_aff_tokens = {x.strip(',;.:()') for x in light_aff.split()}
|
||||
for org_list in best_names:
|
||||
|
||||
|
||||
org = org_list[0]
|
||||
conf = org_list[1]
|
||||
|
||||
records = dix_name[org]
|
||||
|
||||
# -------------------------------------------------
|
||||
# SINGLE MATCH CASE
|
||||
# -------------------------------------------------
|
||||
if len(records) == 1:
|
||||
# print('Single match for:', org)
|
||||
rec = records[0]
|
||||
id_ = rec['id']
|
||||
city_list = rec['city']
|
||||
country_list = rec['country']
|
||||
if id_ in low_confidence_ids and conf < simG:
|
||||
# print('problem')
|
||||
continue
|
||||
|
||||
country_set = {
|
||||
synonym
|
||||
for country in country_list
|
||||
for synonym in country_synonyms.get(country, [country])
|
||||
}
|
||||
|
||||
# print('light_aff',light_aff)
|
||||
if org in light_aff or org in light_aff.split(','):
|
||||
# print('Exact match for:', org)
|
||||
id_list.append([org, conf, id_])
|
||||
|
||||
elif (
|
||||
not any(city in light_aff for city in city_list)
|
||||
and not country_set & light_aff_tokens
|
||||
and not any(word in org for word in excluded)
|
||||
and valueToCategory(org)[1] not in {'Company', 'Acronyms', 'Specific'}
|
||||
):
|
||||
# print('No city/country match but no exclusion words for:', org)
|
||||
pass
|
||||
|
||||
else:
|
||||
id_list.append([org, conf, id_])
|
||||
|
||||
# -------------------------------------------------
|
||||
# MULTIPLE MATCH CASE
|
||||
# -------------------------------------------------
|
||||
else:
|
||||
# print('Multiple matches for:', org)
|
||||
match_found = False
|
||||
|
||||
# Precompute country universe once
|
||||
countries_ids = {
|
||||
country
|
||||
for rec in records
|
||||
for country in rec['country']
|
||||
}
|
||||
|
||||
# -------------------------------
|
||||
# STEP 1: City + Country
|
||||
# -------------------------------
|
||||
disamb = disamb_city_country(org, light_aff)
|
||||
|
||||
if len(disamb) == 1:
|
||||
# print('STEP 1: City + Country disambiguation')
|
||||
id_list.append([org, conf, disamb[0]])
|
||||
continue
|
||||
|
||||
# -------------------------------
|
||||
# STEP 2: Country direct match
|
||||
# -------------------------------
|
||||
for rec in records:
|
||||
country_list = rec['country']
|
||||
id_ = rec['id']
|
||||
|
||||
if any(c in light_aff for c in country_list) and \
|
||||
not any(c in org for c in country_list):
|
||||
|
||||
id_list.append([org, conf, id_])
|
||||
match_found = True
|
||||
# print('STEP 2: Country direct match')
|
||||
break
|
||||
|
||||
# -------------------------------
|
||||
# STEP 3: Special country synonyms
|
||||
# -------------------------------
|
||||
if not match_found and countries_ids & special_countries:
|
||||
|
||||
for rec in records:
|
||||
if _match_special_country(
|
||||
rec['country'],
|
||||
clean_aff,
|
||||
clean_aff_tokens
|
||||
):
|
||||
id_list.append([org, conf, rec['id']])
|
||||
match_found = True
|
||||
# print('STEP 3: Special country synonym match')
|
||||
break
|
||||
|
||||
# -------------------------------
|
||||
# STEP 4: City match
|
||||
# -------------------------------
|
||||
if not match_found:
|
||||
# print('STEP 4: City match')
|
||||
for rec in records:
|
||||
city_list = rec['city']
|
||||
id_ = rec['id']
|
||||
|
||||
if any(c in clean_aff for c in city_list):
|
||||
# print('city')
|
||||
if not any(city in org for city in city_list):
|
||||
id_list.append([org, conf, id_])
|
||||
match_found = True
|
||||
# print('STEP 4: City match without city in org')
|
||||
break
|
||||
|
||||
elif any(clean_aff.count(city) > 1 for city in city_list):
|
||||
id_list.append([org, conf, id_])
|
||||
match_found = True
|
||||
# print('STEP 4: City match with multiple city mentions')
|
||||
break
|
||||
|
||||
# -------------------------------
|
||||
# STEP 5: First word of country
|
||||
# -------------------------------
|
||||
if not match_found:
|
||||
# print('STEP 5: First word of country')
|
||||
for rec in records:
|
||||
for country in rec['country']:
|
||||
if country.split()[0] in clean_aff and \
|
||||
not any(c in org for c in rec['country']):
|
||||
|
||||
id_list.append([org, conf, rec['id']])
|
||||
match_found = True
|
||||
# print('STEP 5: First word of country match')
|
||||
break
|
||||
if match_found:
|
||||
break
|
||||
|
||||
# -------------------------------
|
||||
# STEP 6: Country appears in both
|
||||
# -------------------------------
|
||||
if not match_found:
|
||||
|
||||
for rec in records:
|
||||
if any(c in clean_aff.split() for c in rec['country']) and \
|
||||
any(c in org for c in rec['country']):
|
||||
|
||||
id_list.append([org, conf, rec['id']])
|
||||
match_found = True
|
||||
# print('STEP 6: Country appears in both affiliation and org')
|
||||
break
|
||||
|
||||
# -------------------------------
|
||||
# STEP 7: Specific/Acronym preference
|
||||
# -------------------------------
|
||||
if not match_found:
|
||||
|
||||
for sp in specific:
|
||||
if sp in org:
|
||||
|
||||
for rec in records:
|
||||
if dix_id[rec['id']]['top_level'] == 'y':
|
||||
id_list.append([org, conf, rec['id']])
|
||||
match_found = True
|
||||
# print('STEP 7: Specific/Acronym preference with top-level match')
|
||||
break
|
||||
|
||||
if not match_found:
|
||||
for rec in records:
|
||||
if dix_id[rec['id']]['parent'] == 'y':
|
||||
id_list.append([org, conf, rec['id']])
|
||||
match_found = True
|
||||
# print('STEP 7: Specific/Acronym preference with parent match')
|
||||
break
|
||||
|
||||
break
|
||||
|
||||
# -------------------------------
|
||||
# STEP 8: Fallback first='y'
|
||||
# -------------------------------
|
||||
if not match_found:
|
||||
# print(org)
|
||||
# print('last chance')
|
||||
if org != 'national research council':
|
||||
if any(c in clean_aff for c in city_list):
|
||||
id_list.append([org, conf, id_])
|
||||
match_found = True
|
||||
break
|
||||
else:
|
||||
for rec in records:
|
||||
# print('rec', rec)
|
||||
id_ = rec['id']
|
||||
|
||||
if 'department' not in org and \
|
||||
'labora' not in org and \
|
||||
'instit' not in org and \
|
||||
not any( x in low_prob_countries for x in dix_id[id_]['country']) and \
|
||||
rec['first'] == 'y':
|
||||
id_list.append([org, conf, rec['id']])
|
||||
match_found = True
|
||||
# print('STEP 8: Fallback first="y" match')
|
||||
break
|
||||
|
||||
|
||||
# if not match_found:
|
||||
# print('No match found for:', org)
|
||||
# print(id_list)
|
||||
return keep_highest_score(id_list)
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
from affro.helpers.functions import *
|
||||
from affro.helpers.create_input import *
|
||||
from affro.helpers.matching import *
|
||||
|
||||
|
||||
def find_name(input, dix_name, simU, simG, limit):
|
||||
# print('start find_name')
|
||||
# print('input',input)
|
||||
"""
|
||||
Matches affiliations in DataFrame 'DF' with names from dictionary 'dix_org' and their ROR_ids based on similarity scores.
|
||||
|
||||
Args:
|
||||
m (int): The number of DOIs to check.
|
||||
DF (DataFrame): The input DataFrame containing affiliation data.
|
||||
dix_org (dict): A dictionary of names of organizations and their ROR_ids.
|
||||
simU (float): Similarity threshold for universities.
|
||||
simG (float): Similarity threshold for non-universities.
|
||||
|
||||
Returns:
|
||||
DataFrame: The final DataFrame with matched affiliations and their corresponding similarity scores.
|
||||
"""
|
||||
clean_aff = input[0]
|
||||
light_aff = input[1].replace(' gmbh', ' ').strip()
|
||||
df_list = input[2]
|
||||
|
||||
countries_list = input[3]
|
||||
keys_list = input[4]
|
||||
|
||||
dix = {} # will store indeces and legalnames of organizations of the DOI { i : [legalname1, legalname2,...]}
|
||||
result = {}
|
||||
pairs = []
|
||||
|
||||
keywords = [entry["keywords"].replace(' gmbh', ' ').strip() for entry in df_list]
|
||||
|
||||
candidates = get_candidates(countries_list, keys_list)
|
||||
|
||||
# print('candidates found for keywords:', len(candidates))
|
||||
if len(keywords) > 1 or len(keywords) == 1 and len(keywords[0])>1:
|
||||
for k,s in enumerate(keywords):
|
||||
if len(s) >1 and s not in countries:
|
||||
pairs_k = []
|
||||
# print(s)
|
||||
#--end september 2025
|
||||
if s in candidates:
|
||||
# print('lucky', s)
|
||||
pairs_k.append((s,s,1, dix_name[s][0]['id'],dix_name[s][0]['country']))
|
||||
if k not in dix:
|
||||
dix[k] = [s]
|
||||
else:
|
||||
dix[k].append(s)
|
||||
|
||||
else:
|
||||
# else:
|
||||
# print('not lucky')
|
||||
|
||||
try:
|
||||
pairs_k = find_candidate(s, k , dix, simU, simG, candidates, limit)
|
||||
except:
|
||||
pairs_k = []
|
||||
result[k] = pairs_k
|
||||
if len(pairs_k)>0:
|
||||
pairs.append(pairs_k)
|
||||
# else:
|
||||
# other_candidates = candidates_-candidates
|
||||
# if s in other_candidates:
|
||||
# # print('lucky', s)
|
||||
# pairs_k.append((s,s,1, dix_name[s][0]['id'],dix_name[s][0]['country']))
|
||||
# if k not in dix:
|
||||
# dix[k] = [s]
|
||||
# else:
|
||||
# dix[k].append(s)
|
||||
|
||||
# else:
|
||||
# # else:
|
||||
# # print('not lucky')
|
||||
|
||||
# try:
|
||||
# pairs_k = find_candidate(s, k , dix, simU, simG, other_candidates, limit)
|
||||
# except:
|
||||
# pairs_k = []
|
||||
# result[k] = pairs_k
|
||||
# if len(pairs_k)>0:
|
||||
# pairs.append(pairs_k)
|
||||
|
||||
# print('pairs', pairs)
|
||||
multi = index_multiple_matchings(pairs)
|
||||
# print('multi', multi)
|
||||
need_check_keys = []
|
||||
ready_keys = []
|
||||
ready_best = []
|
||||
for keyword in multi:
|
||||
try:
|
||||
if multi[keyword]>1:
|
||||
need_check_keys.append(keyword)
|
||||
else:
|
||||
for p in pairs:
|
||||
if keyword in p[0]:
|
||||
if p[0][1] not in ready_keys:
|
||||
ready_keys.append(p[0][1])
|
||||
|
||||
ready_best.append([p[0][1], p[0][2]])
|
||||
except Exception as e:
|
||||
print('ERROR, find_name', e)
|
||||
pass
|
||||
|
||||
pairs_check = [ pair for pair in pairs if pair[0][0] in need_check_keys ]
|
||||
# print('pairs_check',pairs_check)
|
||||
if len(need_check_keys)>0:
|
||||
# print(' len(need_check_keys)', len(need_check_keys))
|
||||
best0 = best_sim_score(clean_aff, light_aff, len(keywords), pairs_check, multi, simU, simG)
|
||||
# print('best0', best0)
|
||||
best1 = {x[0]:dix_name[x[0]][0]['id'] for x in best0 }
|
||||
# print('best1', best1)
|
||||
|
||||
best01 = unique_subset(best0, best1)
|
||||
best = best01 + ready_best
|
||||
else:
|
||||
best = ready_best
|
||||
# print('end find_name', best)
|
||||
return best
|
||||
|
|
@ -7,18 +7,55 @@ import json
|
|||
from sklearn.feature_extraction.text import CountVectorizer
|
||||
from sklearn.metrics.pairwise import cosine_similarity
|
||||
from importlib.resources import files
|
||||
import gzip
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pre-compiled regexes used by normalize_organization_names (called many
|
||||
# times per run_affro invocation — compiling once avoids repeated parsing).
|
||||
# ---------------------------------------------------------------------------
|
||||
_RE_UNIVER = re.compile(r'univer\w*', re.IGNORECASE)
|
||||
_RE_EUROP = re.compile(r'europ\w*', re.IGNORECASE)
|
||||
_RE_INSTIT = re.compile(r'instit\w*', re.IGNORECASE)
|
||||
_RE_HOPITAL = re.compile(r'hopital\b', re.IGNORECASE)
|
||||
_RE_HOSPITAL = re.compile(r'hospital(?!s)\w*', re.IGNORECASE)
|
||||
_RE_GEN_HOSPITAL = re.compile(r'\bgeneral hospital\b', re.IGNORECASE)
|
||||
_RE_GEN_UNI_HOSP = re.compile(r'\bgeneral univer hospital\b', re.IGNORECASE)
|
||||
_RE_SCIENCES = re.compile(r'sciences\b', re.IGNORECASE)
|
||||
_RE_SCIENZE = re.compile(r'scienze\b', re.IGNORECASE)
|
||||
_RE_SCIENZA = re.compile(r'scienza\b', re.IGNORECASE)
|
||||
_RE_LABORA = re.compile(r'labora\w*', re.IGNORECASE)
|
||||
_RE_CENTRE = re.compile(r'centre\b', re.IGNORECASE)
|
||||
_RE_CENTRUM = re.compile(r'centrum\b', re.IGNORECASE)
|
||||
_RE_MEDISCH = re.compile(r'medisch\b', re.IGNORECASE)
|
||||
_RE_SAINT = re.compile(r'\bsaint\b', re.IGNORECASE)
|
||||
_RE_TRINITY = re.compile(r'\btrinity col\b', re.IGNORECASE)
|
||||
_RE_TECHNISCHE = re.compile(r'\btechnische\b', re.IGNORECASE)
|
||||
_RE_TEKNOLOGI = re.compile(r'\bteknologi\b', re.IGNORECASE)
|
||||
_RE_POLITEHNICA = re.compile(r'\bpolitehnica\b', re.IGNORECASE)
|
||||
_RE_POLITE = re.compile(r'\bpolite\w*', re.IGNORECASE)
|
||||
_RE_POLYTE = re.compile(r'\bpolyte\w*', re.IGNORECASE)
|
||||
_RE_PANEPIST = re.compile(r'panepist\w*', re.IGNORECASE)
|
||||
_RE_ARISTOT = re.compile(r'\baristot\w*', re.IGNORECASE)
|
||||
_RE_TECHN = re.compile(r'\btechn\w*', re.IGNORECASE)
|
||||
_RE_CZECHOSLOVAK = re.compile(r'\bczechoslovak\b', re.IGNORECASE)
|
||||
_RE_MACAU = re.compile(r'\bmacau\b', re.IGNORECASE)
|
||||
_RE_MUNCHEN = re.compile(r'\bmunchen\b', re.IGNORECASE)
|
||||
_RE_ORGANISATION = re.compile(r'organisation\b', re.IGNORECASE)
|
||||
_RE_XI_AN = re.compile(r'\bxi an\b', re.IGNORECASE)
|
||||
_RE_MULTI_SPACE = re.compile(r'\s+')
|
||||
|
||||
|
||||
def load_json(relative_path, package="affro"):
|
||||
"""
|
||||
Safely load a JSON file from the package.
|
||||
relative_path: path relative to the package root, e.g. 'jsons/dix_acad.json'
|
||||
Safely load a gzipped JSON file from the package.
|
||||
relative_path: path relative to the package root, e.g. 'jsons/dix_acad.json.gz'
|
||||
package: name of the top-level package
|
||||
"""
|
||||
full_path = files(package).joinpath(relative_path)
|
||||
with full_path.open("r", encoding="utf-8") as f:
|
||||
with gzip.open(full_path, "rt", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_txt(relative_path, package="affro"):
|
||||
"""
|
||||
Safely load a text file from the package.
|
||||
|
|
@ -28,29 +65,38 @@ def load_txt(relative_path, package="affro"):
|
|||
with full_path.open("r", encoding="utf-8") as file:
|
||||
return [line.strip() for line in file]
|
||||
|
||||
_replacements_cache = None
|
||||
|
||||
def get_replacements():
|
||||
global _replacements_cache
|
||||
if _replacements_cache is None:
|
||||
_replacements_cache = load_json('jsons/replacements.json.gz')
|
||||
return _replacements_cache
|
||||
|
||||
#categ_string = 'Laboratory|Univ/Inst|Hospital|Foundation|Museum|Government|Company'
|
||||
categ_string = 'Academia|Hospitals|Foundations|Specific|Government|Company|Acronyms'
|
||||
|
||||
dix_org = load_json('jsons/dix_org.json')
|
||||
dix_city = load_json('jsons/dix_city.json')
|
||||
dix_country = load_json('jsons/dix_country.json')
|
||||
dix_mult = load_json('jsons/dix_mult.json')
|
||||
|
||||
dix_country_legalnames = load_json('jsons/dix_country_legalnames.json')
|
||||
|
||||
us_states = [
|
||||
us_states = {
|
||||
"alabama", "alaska", "arizona", "arkansas", "california",
|
||||
"colorado", "conecticut", "delaware", "florida", "georgia",
|
||||
"hawaii", "idaho", "ilinois", "indiana", "iowa",
|
||||
"kansas", "kentucky", "louisiana", "maine", "maryland",
|
||||
"masachusets", "michigan", "minesota", "misisipi", "misouri",
|
||||
"montana", "nebraska", "nevada", "new hampshire", "new jersey",
|
||||
"new mexico", "new york", "north carolina", "north dakota", "ohio",
|
||||
"oklahoma", "oregon", "pensylvania", "rhode island", "south carolina",
|
||||
"south dakota", "tennesee", "texas", "utah", "vermont",
|
||||
"virginia", "washington", "west virginia", "wisconsin", "wyoming"
|
||||
]
|
||||
"new mexico", "new york", "carolina", "dakota", "ohio",
|
||||
"oklahoma", "oregon", "pensylvania", "rhode island",
|
||||
"dakota", "tenesee", "texas", "utah", "vermont",
|
||||
"virginia", "washington", "wisconsin", "wyoming","beth","boston","chicago","ucla"
|
||||
}
|
||||
|
||||
low_prob_countries = {'st kits nevis', 'kenya','malaysia','philipines','burundi',
|
||||
'cambodia','bangladesh','bolivia','azerbaijan',
|
||||
'bosnia herzegovina', 'iran','islamabad',
|
||||
'zimbabwe', 'oman', 'iraq', 'yemen', 'somalia', 'sri lanka','costa rica','indonesia'}
|
||||
|
||||
dix_name = load_json('./jsons/dix_name.json.gz')
|
||||
|
||||
dix_country_legalnames = {k: set(v) for k, v in load_json('./jsons/dix_country_legalnames.json.gz').items()}
|
||||
dix_key_legalnames = {k: set(v) for k, v in load_json('./jsons/dix_keys_names.json.gz').items()}
|
||||
|
||||
def replace_double_consonants(text):
|
||||
# This regex pattern matches any double consonant
|
||||
|
|
@ -62,18 +108,34 @@ def replace_double_consonants(text):
|
|||
|
||||
#stop_words = ['from', 'the', 'of', 'at', 'de','for','et','für','des', 'in','as','a','and','fur','for','und','di']
|
||||
|
||||
|
||||
def remove_stop_words(text):
|
||||
text = text.replace('present adres', '')
|
||||
words = text.split()
|
||||
filtered_words = [word for word in words if word not in stop_words]
|
||||
return ' '.join(filtered_words)
|
||||
filtered_words = []
|
||||
|
||||
for word in words:
|
||||
if word.endswith(","):
|
||||
core = word[:-1] # remove the comma
|
||||
if core not in stop_words:
|
||||
filtered_words.append(core + ",")
|
||||
else:
|
||||
filtered_words.append(",") # keep only the comma
|
||||
else:
|
||||
if word not in stop_words:
|
||||
filtered_words.append(word)
|
||||
|
||||
result = " ".join(filtered_words)
|
||||
# remove spaces before commas
|
||||
result = result.replace(" ,", ",")
|
||||
return result
|
||||
|
||||
|
||||
stop_words = load_txt('txts/stop_words.txt')
|
||||
|
||||
dix_id_country = load_json('jsons/dix_id_country.json')
|
||||
dix_id = load_json('jsons/dix_id.json.gz')
|
||||
|
||||
categ_dicts = load_json('jsons/dix_categ.json')
|
||||
replacements = load_json('jsons/replacements.json')
|
||||
categ_dicts = load_json('jsons/dix_categ.json.gz')
|
||||
#replacements = load_json('jsons/replacements.json.gz')
|
||||
key_words = list(categ_dicts.keys()) + ['univer', 'labora']
|
||||
countries = load_txt('txts/country_names.txt')
|
||||
remove_list = [replace_double_consonants(x) for x in load_txt('txts/remove_list.txt')]
|
||||
|
|
@ -82,14 +144,80 @@ stop_words.remove('at')
|
|||
university_terms = [replace_double_consonants(x) for x in load_txt('txts/university_terms.txt')]
|
||||
city_names = [replace_double_consonants(x) for x in load_txt('txts/city_names.txt')]
|
||||
|
||||
# Pre-compile abbreviation cache for faster lookup (thousands of times faster)
|
||||
_abbr_univ_cache = {}
|
||||
for _city in city_names:
|
||||
_abbr_univ_cache[_city + " u"] = _city + " univer"
|
||||
_abbr_univ_cache["u " + _city] = "univer " + _city
|
||||
_abbr_univ_cache["tu " + _city] = "techn univer " + _city
|
||||
|
||||
def is_first(id, name):
|
||||
for quadruple in dix_name[name]:
|
||||
if quadruple['id'] == id:
|
||||
return quadruple['first']
|
||||
|
||||
|
||||
def get_candidates(country_list):
|
||||
if len(country_list) >0:
|
||||
cand = [dix_country_legalnames[country] for country in country_list if country in dix_country_legalnames]
|
||||
return list(set([item for sublist in cand for item in sublist]))
|
||||
else:
|
||||
return list(dix_org.keys())
|
||||
country_synonyms = {x: [x] for x in countries}
|
||||
country_synonyms["united states"] = ["united states", "u.s.a.", "usa", "usa.", "states"] + [x for x in us_states]
|
||||
country_synonyms["germany"] = ["germany", "deutschland"]
|
||||
country_synonyms["united kingdom"] = ["united kingdom", "u.k.", "uk", "uk.", "kingdom", "england"]
|
||||
country_synonyms["turkey"] = ["turkey", "turkiye", "cyprus"]
|
||||
country_synonyms["china"] = ["china", "prc", "chinese"]
|
||||
country_synonyms["ireland"] = ["eire", "ireland"]
|
||||
country_synonyms["south korea"] = ["south korea", "korea"]
|
||||
country_synonyms["new zealand"] = ["new zealand", "zealand"]
|
||||
country_synonyms["italy"] = ["italy", "milan", "rome", "italia", "pisa"]
|
||||
|
||||
# 1️⃣ Define equivalence classes (each set is a group of mutually equivalent countries)
|
||||
country_equivalence_groups = [
|
||||
{"united states", "u.s.a.", "usa", "usa.", "states","canada"},
|
||||
{"germany", "deutschland"},
|
||||
{"united kingdom", "u.k.", "uk", "uk.", "kingdom", "england","ireland", "eire"},
|
||||
{"turkey", "turkiye", "cyprus","greece", "macedonia"},
|
||||
{"china", "prc", "chinese"},
|
||||
{"south korea", "korea"},
|
||||
{"new zealand", "zealand"},
|
||||
]
|
||||
|
||||
|
||||
# 2️⃣ Build a symmetric lookup for all countries
|
||||
country_to_group = {}
|
||||
for group in country_equivalence_groups:
|
||||
for country in group:
|
||||
country_to_group[country] = group
|
||||
|
||||
|
||||
# 3️⃣ Define the function
|
||||
def get_candidates(country_list, key_list):
|
||||
# Candidate names from keys
|
||||
cand_sets = [dix_key_legalnames[k] for k in key_list if k in dix_key_legalnames]
|
||||
if not cand_sets:
|
||||
return set()
|
||||
cand_names = set.union(*cand_sets) if len(cand_sets) > 1 else cand_sets[0].copy()
|
||||
|
||||
# Remove empty/falsy country entries
|
||||
country_list = [c for c in country_list if c]
|
||||
|
||||
if country_list:
|
||||
# Expand countries using equivalence classes
|
||||
expanded_countries = {
|
||||
synonym
|
||||
for c in country_list
|
||||
for synonym in country_to_group.get(c, {c})
|
||||
}
|
||||
|
||||
# Collect legal names from expanded countries
|
||||
c_sets = [dix_country_legalnames[c] for c in expanded_countries if c in dix_country_legalnames]
|
||||
if not c_sets:
|
||||
return set()
|
||||
country_names = set.union(*c_sets) if len(c_sets) > 1 else c_sets[0]
|
||||
|
||||
return cand_names & country_names
|
||||
|
||||
return cand_names
|
||||
|
||||
|
||||
sp_all = [k for k in categ_dicts if categ_dicts[k] == 'Acronyms' or categ_dicts[k] == 'Specific']
|
||||
|
||||
|
||||
def is_contained(s, w):
|
||||
|
|
@ -110,6 +238,14 @@ def is_contained(s, w):
|
|||
return True # If all words from 's' are found in 'w', return True
|
||||
|
||||
|
||||
|
||||
def split_sub(s: str) -> str:
|
||||
# Add comma after certain word pairs
|
||||
pattern = r'\b((?:univer))\s+(department|faculty)\b'
|
||||
return re.sub(pattern, r'\1, \2', s, flags=re.IGNORECASE)
|
||||
|
||||
|
||||
|
||||
def starts_with_any(string, prefixes):
|
||||
"""
|
||||
Checks if the given string starts with any prefix from a list.
|
||||
|
|
@ -140,7 +276,10 @@ def remove_leading_numbers(s):
|
|||
return re.sub(r'^\d+', '', s)
|
||||
|
||||
def remove_multi_digit_numbers(text):
|
||||
return re.sub(r'\b\d{2,}\b', '', text).strip()
|
||||
if 'inserm' in text or 'cnrs' in text:
|
||||
return re.sub(r'\b\d{5,}\b', '', text).strip()
|
||||
else:
|
||||
return re.sub(r'\b\d{4,}\b', '', text).strip()
|
||||
|
||||
def remove_outer_parentheses(string):
|
||||
"""Remove outer parentheses from the string if they enclose the entire string."""
|
||||
|
|
@ -158,39 +297,42 @@ def replace_roman_numerals(text):
|
|||
|
||||
def insert_space_between_lower_and_upper(s):
|
||||
"""
|
||||
Inserts a space between a lowercase letter followed by an uppercase letter in a string.
|
||||
|
||||
Parameters:
|
||||
s (str): The input string.
|
||||
|
||||
Returns:
|
||||
str: The modified string with spaces inserted.
|
||||
Insert a space between a lowercase letter and a following uppercase letter,
|
||||
while protecting listed substrings (case-sensitive) and restoring them in lowercase.
|
||||
"""
|
||||
# Temporarily replace 'AstraZeneca' to prevent modification
|
||||
s = s.replace('gGmbH','gmbh')
|
||||
s = s.replace('AstraZeneca', 'ASTRAZENECA_TEMP')
|
||||
s = s.replace('BioNTech', 'BIONTECH_TEMP')
|
||||
s = s.replace('GlaxoSmithKline', 'GLAXO_TEMP')
|
||||
s = s.replace('GmbH', 'GMBH_TEMP')
|
||||
s = s.replace('gmbH', 'GMBH_TEMP')
|
||||
s = s.replace('gGmbH', 'GMBH_TEMP')
|
||||
protected = ['DePaul',
|
||||
'AstraZeneca',
|
||||
'BioNTech',
|
||||
'GlaxoSmithKline',
|
||||
'LifeWatch',
|
||||
'SoBigData',
|
||||
'GmbH',
|
||||
'gGmbH',
|
||||
'gmbH',
|
||||
'OpenAIRE',
|
||||
'LaserVision',
|
||||
'PhD',
|
||||
'HafenCity',
|
||||
'NamesforLife'
|
||||
]
|
||||
|
||||
# Replace protected words with placeholders mapping to their lowercase versions
|
||||
placeholders = {}
|
||||
for i, word in enumerate(protected):
|
||||
key = f"__PROT_{i}__"
|
||||
s = s.replace(word, key)
|
||||
placeholders[key] = word.lower()
|
||||
|
||||
|
||||
# Exclude cases where 'Mc' is followed by a capital letter
|
||||
modified_string = re.sub(r'(?<!Mc)([a-z])([A-Z])', r'\1 \2', s)
|
||||
|
||||
# Ensure no spaces are inserted within 'Mc' sequences
|
||||
modified_string = re.sub(r'(Mc) ([A-Z])', r'\1\2', modified_string)
|
||||
|
||||
# Restore 'AstraZeneca'
|
||||
modified_string = modified_string.replace('ASTRAZENECA_TEMP', 'AstraZeneca')
|
||||
modified_string = modified_string.replace('BIONTECH_TEMP', 'BioNTech')
|
||||
modified_string = modified_string.replace('GLAXO_TEMP', 'GlaxoSmithKline')
|
||||
modified_string = modified_string.replace('GMBH_TEMP', 'gmbh')
|
||||
# Add space between lowercase and uppercase (except after 'Mc')
|
||||
s = re.sub(r'(?<!Mc)([a-z])([A-Z])', r'\1 \2', s)
|
||||
s = re.sub(r'(Mc) ([A-Z])', r'\1\2', s)
|
||||
|
||||
# Restore placeholders to lowercase
|
||||
for key, lower_word in placeholders.items():
|
||||
s = s.replace(key, lower_word)
|
||||
|
||||
return s
|
||||
|
||||
|
||||
return modified_string
|
||||
|
||||
|
||||
|
||||
|
|
@ -210,27 +352,25 @@ def replace_acronyms(text):
|
|||
|
||||
|
||||
def replace_abbr_univ(token):
|
||||
for city in city_names:
|
||||
if token == city + " u":
|
||||
return city + " univer"
|
||||
elif token == "u " + city:
|
||||
return "univer " + city
|
||||
elif token == "tu " + city:
|
||||
return "technical univer " + city
|
||||
else:
|
||||
return token
|
||||
"""Optimized lookup using pre-compiled cache instead of linear iteration."""
|
||||
return _abbr_univ_cache.get(token, token)
|
||||
|
||||
|
||||
def remove_parentheses(text):
|
||||
return re.sub(r'\([^()]*\)', '', text)
|
||||
|
||||
L = ['univ', 'hospital', 'clinic', 'klinik', 'Univ', 'Hospital', 'Clinic', 'Klinik']
|
||||
acronyms = [x for x, cat in categ_dicts.items() if cat == 'Acronyms']
|
||||
L = ['univ', 'hospital', 'clinic', 'klinik', 'Univ', 'Hospital', 'Clinic', 'Klinik'] + [s.title() for s in countries] + countries + acronyms
|
||||
|
||||
word_pattern = "|".join(map(re.escape, L))
|
||||
|
||||
_RE_PAREN_REMOVE = re.compile(r'\((?![^)]*(' + word_pattern + r'))[^)]*\)')
|
||||
_RE_PAREN_REPLACE = re.compile(r'\(([^)]*(' + word_pattern + r')[^)]*)\)')
|
||||
|
||||
def process_parentheses(text):
|
||||
"""
|
||||
Processes parentheses in a given text by:
|
||||
1. Removing parentheses that do not contain any word from a the list L = ['univ', 'hospital', 'clinic', 'klinik', 'Univ', 'Hospital', 'Clinic', 'Klinik'].
|
||||
1. Removing parentheses that do not contain any word from the list L.
|
||||
2. Replacing parentheses with commas if they contain a word from the list.
|
||||
|
||||
Parameters:
|
||||
|
|
@ -239,20 +379,16 @@ def process_parentheses(text):
|
|||
Returns:
|
||||
str: The modified string after processing parentheses.
|
||||
"""
|
||||
text_lower = replace_double_consonants(text.lower())
|
||||
text_lower = _RE_PAREN_REMOVE.sub('', text_lower)
|
||||
text_lower = _RE_PAREN_REPLACE.sub(r', \1,', text_lower)
|
||||
|
||||
text = re.sub(r'\((?![^)]*(' + word_pattern + r'))[^)]*\)', '', text)
|
||||
|
||||
# Replace `(` with `,` and `)` with `,` if a word from L is inside
|
||||
text = re.sub(r'\(([^)]*(' + word_pattern + r')[^)]*)\)', r', \1,', text)
|
||||
|
||||
return text
|
||||
return text_lower
|
||||
|
||||
|
||||
|
||||
|
||||
def replace_comma_spaces(text):
|
||||
return text.replace(' ', ' ').replace(' , ', ', ')
|
||||
|
||||
def replace_underscore(text):
|
||||
if 'University-' in text and 'University-Hospital' not in text:
|
||||
return text.replace('-',',')
|
||||
|
|
@ -267,7 +403,45 @@ def fully_unescape(text):
|
|||
return new_text
|
||||
text = new_text
|
||||
|
||||
|
||||
|
||||
def replace_newlines_with_space(text: str, repl: str = " ") -> str:
|
||||
"""
|
||||
Replace many variants of newline/paragraph placeholders in `text`
|
||||
with `repl` (default: space), except:
|
||||
- '\r' (and '\\r') are replaced with ','
|
||||
|
||||
Then collapse repeated whitespace to a single space and trim ends.
|
||||
"""
|
||||
|
||||
if text is None:
|
||||
return text
|
||||
|
||||
text = html.unescape(text)
|
||||
|
||||
# Replace carriage returns with comma
|
||||
text = re.sub(r"(\\r|\r)", ",", text)
|
||||
|
||||
pattern = re.compile(
|
||||
r"""(
|
||||
\#r\#\#n\# | \#r\# | \#n\# |
|
||||
\^p | ¶ |
|
||||
<br\s*/?> |
|
||||
�*13; | �*10; |
|
||||
�*0d; | �*0a; |
|
||||
\\r\\n | \\n | \\u000a | \\u000d | \\x0a | \\x0d |
|
||||
\r\n | \n
|
||||
)""",
|
||||
flags=re.IGNORECASE | re.VERBOSE,
|
||||
)
|
||||
|
||||
cleaned = pattern.sub(repl, text)
|
||||
|
||||
cleaned = re.sub(r"\s+", " ", cleaned).strip()
|
||||
|
||||
return cleaned
|
||||
|
||||
def replace_newlines_with_space1(text: str, repl: str = " ") -> str:
|
||||
"""
|
||||
Replace many variants of newline/paragraph placeholders in `text`
|
||||
with `repl` (default: a single space), then collapse repeated
|
||||
|
|
@ -313,6 +487,72 @@ def replace_newlines_with_space(text: str, repl: str = " ") -> str:
|
|||
|
||||
return cleaned
|
||||
|
||||
def normalize_organization_names(value, university_terms):
|
||||
"""
|
||||
Normalize organization names by applying a series of regex substitutions
|
||||
and standardizations.
|
||||
|
||||
Args:
|
||||
value (str): Organization name substring.
|
||||
university_terms (list of str): List of terms indicating a university.
|
||||
|
||||
Returns:
|
||||
str: Normalized organization name.
|
||||
"""
|
||||
|
||||
# Replace dots with spaces and collapse multiple spaces
|
||||
modified_value = value.replace('.', ' ')
|
||||
modified_value = _RE_MULTI_SPACE.sub(' ', modified_value).strip()
|
||||
|
||||
# Protect compound university terms (e.g. universitatszahnklinik) with placeholders
|
||||
# so that plain "universitat" etc. still get shortened to "univer" in the same string.
|
||||
val_lower = modified_value.lower()
|
||||
placeholders = {}
|
||||
for i, term in enumerate(university_terms):
|
||||
t_low = term.lower()
|
||||
if (t_low.startswith('univer') or t_low.startswith('hospital')) and t_low in val_lower:
|
||||
key = f'__UNIV_{i}__'
|
||||
modified_value = re.sub(re.escape(term), key, modified_value, flags=re.IGNORECASE)
|
||||
placeholders[key] = term
|
||||
|
||||
modified_value = _RE_UNIVER.sub('univer', modified_value)
|
||||
|
||||
# General substitutions (all using pre-compiled patterns)
|
||||
modified_value = _RE_EUROP.sub('europ', modified_value)
|
||||
modified_value = _RE_INSTIT.sub('instit', modified_value)
|
||||
modified_value = _RE_HOPITAL.sub('hospital', modified_value)
|
||||
modified_value = _RE_HOSPITAL.sub('hospital', modified_value)
|
||||
modified_value = _RE_GEN_HOSPITAL.sub('hospital', modified_value)
|
||||
modified_value = _RE_GEN_UNI_HOSP.sub('univer hospital', modified_value)
|
||||
modified_value = _RE_SCIENCES.sub('science', modified_value)
|
||||
modified_value = _RE_SCIENZE.sub('science', modified_value)
|
||||
modified_value = _RE_SCIENZA.sub('science', modified_value)
|
||||
modified_value = _RE_LABORA.sub('labora', modified_value)
|
||||
modified_value = _RE_CENTRE.sub('center', modified_value)
|
||||
modified_value = _RE_CENTRUM.sub('center', modified_value)
|
||||
modified_value = _RE_MEDISCH.sub('medical', modified_value)
|
||||
modified_value = _RE_SAINT.sub('st', modified_value)
|
||||
modified_value = _RE_TRINITY.sub('trinity colege', modified_value)
|
||||
modified_value = _RE_TECHNISCHE.sub('technological', modified_value)
|
||||
modified_value = _RE_TEKNOLOGI.sub('technology', modified_value)
|
||||
modified_value = _RE_POLITEHNICA.sub('polytechnic', modified_value)
|
||||
modified_value = _RE_POLITE.sub('polytechnic', modified_value)
|
||||
modified_value = _RE_POLYTE.sub('polytechnic', modified_value)
|
||||
modified_value = _RE_PANEPIST.sub('univer', modified_value)
|
||||
modified_value = _RE_ARISTOT.sub('aristot', modified_value)
|
||||
modified_value = _RE_TECHN.sub('techn', modified_value)
|
||||
modified_value = _RE_CZECHOSLOVAK.sub('czech', modified_value)
|
||||
modified_value = _RE_MACAU.sub('macao', modified_value)
|
||||
modified_value = _RE_MUNCHEN.sub('munich', modified_value)
|
||||
modified_value = _RE_ORGANISATION.sub('organization', modified_value)
|
||||
modified_value = _RE_XI_AN.sub('xian', modified_value)
|
||||
|
||||
# Restore all protected compound terms (universit- and hospital- prefixed)
|
||||
for key, original in placeholders.items():
|
||||
modified_value = modified_value.replace(key, original)
|
||||
|
||||
return modified_value
|
||||
|
||||
def substrings_dict(string):
|
||||
"""
|
||||
Processes a given string by performing the following transformations:
|
||||
|
|
@ -330,10 +570,10 @@ def substrings_dict(string):
|
|||
dict: A dictionary where each key is an index, and the value is a processed substring.
|
||||
"""
|
||||
# print(0, string)
|
||||
for old, new in replacements.items():
|
||||
for old, new in get_replacements().items():
|
||||
string = string.replace(old, new)
|
||||
# split_strings = [s.strip() for s in re.split(r'[,;/]| – ', string) if s.strip()]
|
||||
split_strings = [replace_acronyms(s).strip() for s in re.split(r' - | – |[,;/:]', string) if s.strip()]
|
||||
|
||||
split_strings = [replace_acronyms(s).strip() for s in re.split(r' - | – |[,;/:|]', string) if s.strip()]
|
||||
|
||||
# Define a set of university-related terms for later use
|
||||
|
||||
|
|
@ -341,33 +581,9 @@ def substrings_dict(string):
|
|||
dict_string = {}
|
||||
index = 0
|
||||
|
||||
# print('split_strings',split_strings)
|
||||
|
||||
for value in split_strings:
|
||||
modified_value = value.replace('.', ' ')
|
||||
modified_value = re.sub(r'\s+', ' ', modified_value)
|
||||
|
||||
# Check if the substring contains any university-related terms
|
||||
if not any(term in modified_value.lower() for term in university_terms):
|
||||
# Apply regex substitutions for common patterns
|
||||
|
||||
modified_value = re.sub(r'univer\w*', 'univer', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'institu\w*', 'institu', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'hospital(?!s)\w*', 'hospital', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'labora\w*', 'labora', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'centre\b', 'center', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'centrum\b', 'center', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'\bsaint\b', 'st', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'\btrinity col\b', 'trinity colege', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'\btechnische\b', 'technological', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'\bteknologi\b', 'technology', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'\bpolitehnica\b', 'polytechnic', modified_value, flags=re.IGNORECASE)
|
||||
|
||||
modified_value = re.sub(r'\btechn\w*', 'techn', modified_value, flags=re.IGNORECASE)
|
||||
#modified_value = re.sub(r'techno\w*', 'techno', modified_value, flags=re.IGNORECASE)
|
||||
modified_value = re.sub(r'scien\w*', 'scien', modified_value, flags=re.IGNORECASE)
|
||||
|
||||
|
||||
modified_value = normalize_organization_names(value, university_terms)
|
||||
|
||||
# Add the modified substring to the dictionary
|
||||
|
||||
|
|
@ -375,20 +591,36 @@ def substrings_dict(string):
|
|||
index += 1
|
||||
|
||||
# Add the original substring to the dictionary
|
||||
# else:
|
||||
# dict_string[index] = value.lower().strip()
|
||||
# index += 1
|
||||
|
||||
|
||||
return dict_string
|
||||
|
||||
countries_set = set(countries)
|
||||
key_words_set = set(key_words)
|
||||
|
||||
def clean_string_ror(input_string):
|
||||
|
||||
input_string = replace_underscore(replace_comma_spaces(replace_double_consonants(unidecode(process_parentheses(fully_unescape(input_string.replace("’","'").replace(" ́e","e").replace("'s", "s").replace("'", " "))))))).strip()
|
||||
def split_country(text):
|
||||
parts = text.split(' ')
|
||||
|
||||
if len(parts) < 2:
|
||||
return text
|
||||
|
||||
if parts[-1] in countries_set and not parts[-2].startswith('univ'):
|
||||
return " ".join(parts[:-1]) + ", " + parts[-1]
|
||||
|
||||
return text
|
||||
|
||||
|
||||
|
||||
def clean_string_lucky(input_string):
|
||||
|
||||
input_string = replace_underscore(replace_comma_spaces(replace_double_consonants(unidecode(process_parentheses(fully_unescape(input_string.replace("’","'").replace(" ́e","e").replace("'s", "s").replace("'", "").replace('"', ' '))))))).strip()
|
||||
|
||||
result = remove_stop_words(replace_roman_numerals(input_string.lower()))
|
||||
result = result.replace(' and ',' ')
|
||||
result = result.replace('general hospital', 'hospital')
|
||||
|
||||
for old, new in get_replacements().items():
|
||||
result = result.replace(old, new)
|
||||
|
||||
# Remove characters that are not from the Latin alphabet, or allowed punctuation
|
||||
result = remove_multi_digit_numbers(replace_comma_spaces(re.sub(r'[^a-zA-Z0-9\s,;/:.\-\—]', '', result).strip()))
|
||||
|
|
@ -398,56 +630,52 @@ def clean_string_ror(input_string):
|
|||
result = result.replace(':',' ').replace(';',' ').replace('-',' ').replace('—',' ').replace(',',' ')
|
||||
# Replace consecutive whitespace with a single space
|
||||
|
||||
|
||||
result = replace_acronyms(result).replace('.', ' ')
|
||||
result = normalize_organization_names(result, university_terms)
|
||||
|
||||
return result.strip()
|
||||
|
||||
|
||||
def clean_string_ror(input_string):
|
||||
|
||||
input_string = replace_underscore(replace_comma_spaces(replace_double_consonants(unidecode(remove_parentheses(fully_unescape(input_string.replace("’","'").replace(" ́e","e").replace("'s", "s").replace("'", "").replace('"', ' '))))))).strip()
|
||||
|
||||
result = remove_stop_words(replace_roman_numerals(input_string.lower()))
|
||||
result = result.replace(' and ',' ')
|
||||
result = result.replace('general hospital', 'hospital')
|
||||
|
||||
for old, new in get_replacements().items():
|
||||
result = result.replace(old, new)
|
||||
|
||||
# Remove characters that are not from the Latin alphabet, or allowed punctuation
|
||||
result = remove_multi_digit_numbers(replace_comma_spaces(re.sub(r'[^a-zA-Z0-9\s,;/:.\-\—]', '', result).strip()))
|
||||
|
||||
# Restore the " - " sequence from the placeholder
|
||||
#result = result.replace(placeholder, " – ")
|
||||
result = result.replace(':',' ').replace(';',' ').replace('-',' ').replace('—',' ').replace(',',' ')
|
||||
# Replace consecutive whitespace with a single space
|
||||
|
||||
university_terms = {'universitatsklinikum', 'universitatskinderklinik',
|
||||
'universitatspital', 'universitatskliniken', 'universitetshospital',
|
||||
'universitatsmedizin', 'universitatsbibliothek','universitatszahnklinik'
|
||||
}
|
||||
|
||||
result = replace_acronyms(result).replace('.', ' ')
|
||||
result = re.sub(r'\s+', ' ', result)
|
||||
result = normalize_organization_names(result, university_terms)
|
||||
|
||||
# Replace consecutive whitespace with a single space
|
||||
if not any(term in result.lower() for term in university_terms):
|
||||
|
||||
result = re.sub(r'universi\w*', 'univer', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'\bsaint\b', 'st', result,flags=re.IGNORECASE)
|
||||
result = re.sub(r'institu\w*', 'institu', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'labora\w*', 'labora', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'centre\b', 'center', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'centrum\b', 'center', result, flags=re.IGNORECASE)
|
||||
|
||||
result = re.sub(r'hopital\b', 'hospital', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'hospital(?!s)\w*', 'hospital', result, flags=re.IGNORECASE)
|
||||
|
||||
#result = re.sub(r'centro\b', 'center', result, flags=re.IGNORECASE)
|
||||
|
||||
result = re.sub(r'\btechnische\b', 'technological', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'\bteknologi\b', 'technological', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'\bpolitehnica\b', 'polytechnic', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'czechoslovak\b', 'czech', result, flags=re.IGNORECASE)
|
||||
|
||||
result = re.sub(r'\btechn\w*', 'techn', result, flags=re.IGNORECASE)
|
||||
# result = re.sub(r'techno\w*', 'techno', result, flags=re.IGNORECASE)
|
||||
result = re.sub(r'scien\w*', 'scien', result, flags=re.IGNORECASE)
|
||||
# result = re.sub(r'\bsaint\b', 'st', result, flags=re.IGNORECASE)
|
||||
|
||||
return result.strip()
|
||||
|
||||
def clean_string(input_string):
|
||||
input_string = replace_underscore(replace_comma_spaces(unidecode(process_parentheses(fully_unescape(replace_newlines_with_space(input_string).replace("P.O. Box","").replace("’","'").replace(" ́e","e").replace("'s", "s").replace("'", " ")))))).strip()
|
||||
input_string = replace_underscore(replace_comma_spaces(unidecode(process_parentheses(insert_space_between_lower_and_upper(fully_unescape(replace_newlines_with_space(input_string).replace("P.O. Box","").replace("’","'").replace(" ́e","e").replace("'s", "s").replace('"', ' '))))))).strip()
|
||||
|
||||
# result = re.sub(r'(?<! )[–—-](?! )', ' ', input_string)
|
||||
|
||||
# print('h',input_string)
|
||||
|
||||
result = remove_stop_words(replace_double_consonants(replace_roman_numerals(insert_space_between_lower_and_upper(input_string).lower())))
|
||||
result = remove_stop_words(replace_double_consonants(replace_roman_numerals((input_string).lower().replace("'", ""))))
|
||||
|
||||
|
||||
# Remove characters that are not from the Latin alphabet, or allowed punctuation
|
||||
result = remove_multi_digit_numbers(replace_comma_spaces(re.sub(r'[^a-zA-Z0-9\s,;/:.\-\—]', '', result).strip()))
|
||||
|
||||
result = remove_multi_digit_numbers(replace_comma_spaces(re.sub(r'[^a-zA-Z0-9\s,;/:|.\-\—]', '', result).strip()))
|
||||
#result = normalize_organization_names(result, university_terms)
|
||||
|
||||
|
||||
# Restore the " - " sequence from the placeholder
|
||||
#result = result.replace(placeholder, " – ")
|
||||
|
|
@ -457,43 +685,46 @@ def clean_string(input_string):
|
|||
|
||||
#result = replace_roman_numerals(remove_stop_words(insert_space_between_lower_and_upper(result).lower()))
|
||||
|
||||
return result.strip() # Strip leading/trailing spaces
|
||||
|
||||
return result.strip() # Strip leading/trailing spaces
|
||||
return split_country(result.strip()) # Strip leading/trailing spaces
|
||||
|
||||
def description(aff_string):
|
||||
aff_string = aff_string.replace('turkiye', 'turkey')
|
||||
aff_string = aff_string.replace('turkiye', 'turkey').lower()
|
||||
aff_string = aff_string.replace('kirgizistan', 'kyrgyzstan')
|
||||
|
||||
aff_string = aff_string.replace('u.s.a.', 'usa').replace('u.k.', 'uk').replace('macau','macao')
|
||||
descr = []
|
||||
countries_ = []
|
||||
words = re.split(r'[ ,;:/]+', aff_string)
|
||||
# words = [word.strip() for word in re.split(r'[,;:]+', aff_string) if word.strip()]
|
||||
words = re.split(r'[ ,;:/.-]', aff_string)
|
||||
|
||||
has_multiword_state = any(
|
||||
state in aff_string
|
||||
for state in ('new york', 'new hampshire', 'new jersey', 'new mexico')
|
||||
)
|
||||
|
||||
for w in words:
|
||||
# if w in city_names:
|
||||
# descr.append('city')
|
||||
w = re.sub(r'[^A-Za-z\s]', '', w)
|
||||
|
||||
if replace_acronyms(w) in countries:
|
||||
if replace_acronyms(w) in countries_set:
|
||||
descr.append('country')
|
||||
countries_.append(w)
|
||||
|
||||
if replace_acronyms(w) in us_states:
|
||||
|
||||
if (
|
||||
replace_acronyms(w) in us_states
|
||||
or has_multiword_state
|
||||
):
|
||||
descr.append('country')
|
||||
countries_.append('usa')
|
||||
|
||||
elif w in ['univer', 'institu', 'hospital', 'labora']:
|
||||
|
||||
countries_.append('mexico')
|
||||
|
||||
elif w in ['univer', 'instit', 'hospital', 'labora', 'colege', 'foundation'] or w in sp_all:
|
||||
descr.append('basic_key')
|
||||
elif w == 'and':
|
||||
descr.append('and')
|
||||
|
||||
elif w in key_words and categ_dicts[w] == 'Specific':
|
||||
elif w in key_words_set and categ_dicts[w] == 'Specific':
|
||||
descr.append('basic_key')
|
||||
# elif w in key_words:
|
||||
# descr.append('key')
|
||||
else:
|
||||
descr.append('other') # Optional: label words that don’t fit any category
|
||||
descr.append('other')
|
||||
|
||||
return [descr, countries_]
|
||||
|
||||
|
|
@ -531,12 +762,12 @@ def split_and(string):
|
|||
tok_no_sl1 = ' '.join(token.replace('-', ' ').split())
|
||||
tok_no_sl2 = ' '.join(token.replace('—', ' ').split())
|
||||
tok_no = ' '.join(token.replace(' and ', ' ').replace(' at ', ' ').replace(' an ', ' ').replace('-', ' ').replace('—', ' ').split())
|
||||
if tok_no in dix_org:
|
||||
if tok_no in dix_name:
|
||||
token = tok_no
|
||||
|
||||
|
||||
else:
|
||||
if tok_no_and not in dix_org:
|
||||
if tok_no_and not in dix_name:
|
||||
# Store once instead of calling multiple times
|
||||
|
||||
if is_subsequence(replace_sequence, token_description):# and token.split(' and ', ' ') not in dix_org:
|
||||
|
|
@ -547,20 +778,27 @@ def split_and(string):
|
|||
else:
|
||||
token = tok_no_and
|
||||
|
||||
if tok_no_at not in dix_org:
|
||||
if tok_no_at not in dix_name:
|
||||
token = ' '.join(token.replace(' at ', ', ').split())
|
||||
else:
|
||||
token = tok_no_at
|
||||
|
||||
if tok_no_an not in dix_org:
|
||||
if tok_no_an not in dix_name:
|
||||
token = ' '.join(token.replace(' an ', ', ').split())
|
||||
else:
|
||||
token = tok_no_an
|
||||
if tok_no_sl1 not in dix_org:
|
||||
token = ' '.join(token.replace('-', ',').split())
|
||||
if tok_no_sl1 not in dix_name:
|
||||
parts = re.split(r'\s*-\s*', token)
|
||||
if len(parts) >= 2: # there is a dash in the current token
|
||||
if ('basic_key' in description(parts[0])[0]
|
||||
and 'basic_key' in description(parts[-1])[0]):
|
||||
token = re.sub(r'\s*-\s*', ', ', token)
|
||||
else:
|
||||
token = tok_no_sl1
|
||||
# else: no dash in current token, leave it unchanged
|
||||
else:
|
||||
token = tok_no_sl1
|
||||
if tok_no_sl2 not in dix_org:
|
||||
if tok_no_sl2 not in dix_name:
|
||||
token = ' '.join(token.replace('—', ',').split())
|
||||
else:
|
||||
token = tok_no_sl2
|
||||
|
|
@ -575,12 +813,13 @@ def split_and(string):
|
|||
def reduce(light_aff):
|
||||
|
||||
aff_no_symbols_d = substrings_dict(light_aff)
|
||||
# print('aff_no_symbols_d',aff_no_symbols_d)
|
||||
substring_list = list(aff_no_symbols_d.values())
|
||||
#light_aff_final = ', '.join((substring_list))
|
||||
# print('h', substring_list)
|
||||
light_aff_final = ', '.joi | ||||