Compare commits

...

52 Commits
2.2.2 ... main

Author SHA1 Message Date
mkallipo eff8bd6d1e dix_name_id added - new filtering rule added for multiple results based on univ country if no other country is preset - ror061026 2026-10-15 11:41:24 +02:00
mkallipo 5466dad5fd bugs, new ids 2026-09-10 15:08:52 +02:00
mkallipo 1b5af9bb69 specific/acronym bug fixed 2026-07-10 11:07:54 +03:00
mkallipo 6d4b3d40ef bugs, new ids 2026-06-12 23:26:20 +02:00
mkallipo 587c4d06f9 minor fixes, tranlsations 2026-05-13 18:30:10 +02:00
mkallipo a8bb74f80c minor 2026-05-08 12:03:38 +02:00
mkallipo 3c41e97f47 minor changes 2026-05-08 09:56:28 +02:00
mkallipo b958fce5ab minor changes 2026-05-08 09:54:08 +02:00
mkallipo f6894aa186 documentation 2026-04-15 16:05:06 +02:00
mkallipo 783ded3137 documentation 2026-04-15 16:03:03 +02:00
mkallipo 97532099a5 documentation 2026-04-15 15:39:53 +02:00
mkallipo 8ff7607b51 documentation 2026-04-15 15:39:04 +02:00
mkallipo 293bb9b428 documentation 2026-04-15 15:38:35 +02:00
mkallipo a2db2e62d2 documentation 2026-04-15 15:37:22 +02:00
mkallipo 5ae7570618 documentation 2026-04-15 15:33:19 +02:00
mkallipo 70daca81b6 documentation 2026-04-14 10:23:05 +02:00
mkallipo c32e271e1f added confidence to affro_direct 2026-04-13 16:50:56 +02:00
mkallipo 876cdae6c6 added confidence to affro_direct 2026-04-13 16:49:01 +02:00
mkallipo aeffddff44 documentation 2026-04-10 12:44:39 +02:00
mkallipo 8305e128ea documentation 2026-04-10 12:43:41 +02:00
mkallipo c1b430e84e documentation 2026-04-10 12:40:19 +02:00
mkallipo e88a5b7652 documentation 2026-04-10 12:39:51 +02:00
mkallipo 93c279e4e6 documentation 2026-04-10 12:38:34 +02:00
mkallipo d14a106a6a documentation 2026-04-10 12:34:10 +02:00
mkallipo b1b9f19ddf documentation 2026-04-10 12:32:05 +02:00
mkallipo 50ac26cd7e documentation 2026-04-10 12:29:23 +02:00
mkallipo edc645b172 documentation 2026-04-10 12:27:53 +02:00
mkallipo f1715594d6 new ids, direct mapping 2026-04-09 10:04:17 +02:00
mkallipo 2220345e13 bugs, new ids 2026-03-30 19:41:23 +02:00
mkallipo 42e5f01885 bugs 2026-03-13 18:09:48 +01:00
mkallipo 4f5f6f7b78 bugs 2026-03-13 18:07:45 +01:00
mkallipo 3eadb67c3b bugs, new ids, introduced low_conf_ids, low_prob_countries 2026-03-11 16:33:02 +01:00
mkallipo bade62a864 bugs, new ids 2026-03-06 16:24:41 +01:00
mkallipo f12d145a8e bugs, ror Feb26 2026-02-27 10:04:20 +01:00
mkallipo 2d11a98ada bugs, new ids 2026-02-27 10:02:13 +01:00
mkallipo b3c9939bf6 minor improvements, new openorgs IDs 2026-02-17 16:53:46 +01:00
mkallipo bf6180dcd4 minor improvements, new openorgs IDs 2026-02-17 16:51:50 +01:00
mkallipo f0ff21c960 bugs, new ror version 2026-02-15 21:21:07 +01:00
mkallipo 2da715578e bugs, new ror version 2026-02-15 20:53:00 +01:00
mkallipo fe6c88586b bugs, new ROR ids 2026-02-05 15:30:28 +01:00
mkallipo 37253c4284 bugs regarding multidigit 2026-02-03 11:18:35 +01:00
mkallipo ebcc80093b compress jsons 2026-01-23 18:36:17 +01:00
mkallipo 20021aef7f keyword filtering for comparisons 2026-01-23 16:52:22 +01:00
mkallipo 5f429d96d2 minor changes 2026-01-22 19:52:19 +01:00
mkallipo d8ff4947e9 minor changes 2026-01-22 13:56:34 +01:00
mkallipo 30e88e87f0 clean_string_lucky 2025-12-15 11:49:48 +01:00
mkallipo abd11209b9 bugs 2025-12-12 17:52:37 +01:00
mkallipo b274291443 bugs, new ids 2025-11-19 11:23:53 +01:00
mkallipo baceddf91f just a bug 2025-11-18 19:52:58 +01:00
mkallipo 1ecee1372f just a bug 2025-11-18 18:45:57 +01:00
mkallipo 2da4348884 Fixed bugs, added new OpenOrgs IDs, updated ROR IDs, updated file structure, added is_first label to org names, improved organization's category handling 2025-11-18 13:16:17 +01:00
mkallipo c9914a47d0 new structure for the dictionaries, new openorgs ids, ror version oct 2025-10-17 17:25:01 +02:00
75 changed files with 583455 additions and 3137447 deletions

View File

@ -1,2 +1,2 @@
recursive-include affro/jsons *.json
recursive-include affro/jsons *.json.gz
recursive-include affro/txts *.txt

325
README.md
View File

@ -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
```

View File

@ -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
```

View File

@ -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

View File

@ -1 +1,5 @@
__version__ = "2.2.2"
__version__ = "3.4.0"
from .core import run_affro, matchings_affro
__all__ = ["__version__", "run_affro", "matchings_affro"]

View File

@ -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 ()

View File

@ -0,0 +1 @@
__version__ = "3.4.0"

View File

@ -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]

View File

@ -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]

View File

@ -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 []

306
affro/helpers/find_id.py Normal file
View File

@ -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)

120
affro/helpers/find_name.py Normal file
View File

@ -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

View File

@ -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*/?> |
&#0*13; | &#0*10; |
&#x0*0d; | &#x0*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))

View File

@ -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.

View File

@ -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.

File diff suppressed because it is too large Load Diff

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.

BIN
affro/jsons/dix_id.json.gz Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -8,4 +8,18 @@ universitatsbibliothek
universitatspital
universitetsjukhuset
universitatsaugenklinik
univesitatsfrauenklinik
universitatsfrauenklinik
universitetscentralsjukhus
universitatsverlag
universitaetsklinikum
universitatsalianz
universalmuseum
universitatszahnklinik
universitetsforlaget
universal
universum
hospitalfield
universitatsklinik
universitaetsklinik
universitetskliniken
universiteitsmuseum

View File

@ -1 +1,5 @@
__version__ = "2.2.2"
__version__ = "3.4.0"
from .core import run_affro, matchings_affro
__all__ = ["__version__", "run_affro", "matchings_affro"]

View File

@ -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 ()

View File

@ -0,0 +1 @@
__version__ = "3.4.0"

View File

@ -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]

View File

@ -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]

View File

@ -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 []

View File

@ -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)

View File

@ -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

View File

@ -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*/?> |
&#0*13; | &#0*10; |
&#x0*0d; | &#x0*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