Metadata-Version: 2.4
Name: reg_normalizer
Version: 1.3.0
Summary: Tool for normalizing and standardizing Russian region names
Author-email: Vitovt Kopytok <vitovt.kopytok@gmail.com>
License: CC BY-NC-SA 4.0
Project-URL: Homepage, https://github.com/tochno-st/reg_normalizer
Project-URL: Documentation, https://github.com/tochno-st/reg_normalizer#readme
Project-URL: Repository, https://github.com/tochno-st/reg_normalizer
Project-URL: Bug Tracker, https://github.com/tochno-st/reg_normalizer/issues
Keywords: regions,normalization,russian,fuzzy-matching,data-cleaning
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: Free for non-commercial use
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing :: Linguistic
Classifier: Natural Language :: Russian
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: pyyaml>=6.0
Requires-Dist: fuzzywuzzy>=0.18.0
Requires-Dist: python-levenshtein>=0.20.0
Requires-Dist: pandas>=1.5.0
Requires-Dist: nltk>=3.8.0
Provides-Extra: dev
Requires-Dist: ipykernel; extra == "dev"
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"

[![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC_BY--NC--SA_4.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc-sa/4.0/)
[![PyPI version](https://badge.fury.io/py/reg_normalizer.svg)](https://badge.fury.io/py/reg_normalizer)
[![Python Versions](https://img.shields.io/pypi/pyversions/reg_normalizer.svg)](https://pypi.org/project/reg_normalizer/)

# Region Normalizer

**Region Normalizer** — инструмент для нормализации и стандартизации наименований российских регионов, а также добавления нормализующих переменных. Он помогает распознавать регион даже в случаях, когда в названии встречаются опечатки, латинские буквы или другие особенности написания. Инструмент сопоставляет различные формы написания с [эталонным справочником](https://github.com/tochno-st/reg_normalizer/blob/main/data/interim/regions_etalon_v2.0.yaml) и позволяет извлекать дополнительные атрибуты, такие как коды ОКАТО, ISO, английские названия и многое другое.

Разные наименования одного и того же региона — частая проблема в реальных данных, например, на портале ЕМИСС встречается до 275 различных вариантов написания регионов. Особенно многообразны варианты у Тюменской и Архангельской областей — по 8 и 7 вариантов соответственно. В состав этих регионов входят автономные округа (ХМАО, ЯНАО, НАО), и часть ведомств отмечает, что данные приведены без автономных округов, но сокращения и формулировки используются самые разные.

## Возможности

- Поиск и нормализация региона по произвольному названию (с учетом опечаток, сокращений, аббревиатур, смешения латиницы и кириллицы)
- Пакетная обработка больших таблиц с названиями регионов
- Гибкая настройка весов алгоритмов сопоставления
- Добавление дополнительных полей из эталонного справочника (ОКАТО, ISO, английское название и др.)
- Добавление нормализующих переменных (численность населения, индекс потребительских цен)

## Установка

Установите пакет с помощью pip:

```bash
pip install reg_normalizer
```

### Установка для разработки

Если вы хотите внести изменения в код:

1. Клонируйте репозиторий:

```bash
git clone https://github.com/tochno-st/reg_normalizer.git
cd reg_normalizer
```

1. Создайте виртуальное окружение с помощью `uv`:

```bash
uv venv
```

1. Активируйте виртуальное окружение:

**На macOS/Linux:**

```bash
source .venv/bin/activate
```

**На Windows:**

```bash
.venv\Scripts\activate
```

1. Установите зависимости для разработки:

```bash
uv pip install -e ".[dev]"
```

Или используйте традиционный подход с `pip`:

```bash
pip install -r requirements.txt
pip install -e ".[dev]"
```

## Быстрый старт

### 1. Импорт и инициализация

```python
from reg_normalizer import RegionMatcher

matcher = RegionMatcher()
```

### 2. Нормализация одного региона

```python
region_name = "московск область"
match, score = matcher.find_best_match(region_name)
print(f"Input: {region_name}")
print(f"Match: {match}")
print(f"Score: {score:.2f}")
```

### 3. Использование с DataFrame

```python
import pandas as pd
sample_data = pd.DataFrame({
    'region_name': [
        'московск Обл',
        'свердловск',
        'петербург',
        'Mосковская област',
        'татарстан респ.',
        'Свердлов обл',
        'aлтайский к',
        'Республика     Алтай',
        'ХМао',
        'Юж федеральный округ',
        'спб',
        'рт',
        'город москва столица российской федерации город федерального значения',
        'тюменская область (кроме ханты мансийского автономного округа югры и ямало ненецкого автономного округа)'
    ]
})

result_df = matcher.match_dataframe(
    sample_data,
    'region_name',
    weights={'levenshtein': 0.4, 'token_set': 0.6},
    approach_weights={'original': 0.3, 'stemmed': 0.7},
    threshold=70
)
```

### 4. Добавление дополнительных полей

```python
# Одно поле: передайте список из одного элемента
result_df = matcher.attach_fields(result_df, 'region_name', ['name_eng'])

# Несколько полей
result_df = matcher.attach_fields(result_df, 'region_name',
                                  ['name_eng', 'okato', 'iso_code'])

print(result_df.head())
```

### 5. Просмотр журнала преобразований

После вызова `match_dataframe` или `attach_fields` на экземпляре класса становится доступен журнал всех произведенных преобразований — метод `get_match()`.

```python
matcher.match_dataframe(df, 'region_name')

log = matcher.get_match()
print(log)
```

Возвращает `pd.DataFrame`, где каждая строка — одно уникальное исходное значение. Результаты отсортированы по сложности: **сначала самые нетривиальные случаи**.

| Колонка | Описание |
|---|---|
| `original` | Исходное название из данных |
| `normalized` | Итоговое нормализованное название |
| `score` | Балл совпадения (0–100), `None` если совпадение не найдено |
| `event` | Тип события (см. ниже) |
| `note` | Дополнительное пояснение |

#### Типы событий (`event`) — по приоритету

| Событие | Описание |
|---|---|
| `parent_resolved` | Регион-родитель переопределен на вариант «без АО», т.к. в данных найдены автономные округа отдельно (напр. Архангельская область → без НАО) |
| `low_score` | Совпадение не найдено выше порога — нужна ручная проверка |
| `parent_kept` | Регион-родитель оставлен в варианте «с АО», т.к. автономные округа отдельно не встречаются |
| `match` | Обычное успешное совпадение |

Пример фильтрации проблемных случаев:

```python
log = matcher.get_match()

# Только то, что требует внимания
log[log['event'].isin(['parent_resolved', 'low_score'])]
```

### 6. Работа с показателями (индикаторами)

Пакет позволяет присоединять к данным региональные статистические показатели (население, стоимость потребительской корзины, индекс бюджетных расходов и др.) из встроенной таблицы `normalizers.csv`. Данные доступны в разбивке по годам (2000–2025).

#### Список доступных показателей


| Код                     | Описание                                                                   |
| ----------------------- | -------------------------------------------------------------------------- |
| `pop_total`             | Численность населения — всего                                              |
| `pop_men`               | Численность населения — мужчины                                            |
| `pop_women`             | Численность населения — женщины                                            |
| `pop_urban`             | Численность населения — городское население                                |
| `pop_rural`             | Численность населения — сельское население                                 |
| `pop_0_17`              | Численность населения — 0–17 лет                                           |
| `pop_18_plus`           | Численность населения — 18 лет и старше                                    |
| `pop_below_working`     | Численность населения — моложе трудоспособного                             |
| `pop_working`           | Численность населения — в трудоспособном возрасте (муж. 16–59, жен. 16–54) |
| `pop_above_working`     | Численность населения — старше трудоспособного                             |
| `pop_pension`           | Численность населения — пенсионного возраста (66 лет и старше)             |
| `pop_total_avg`         | Численность населения — всего (в среднем за год)                           |
| `pop_men_avg`           | Численность населения — мужчины (в среднем за год)                         |
| `pop_women_avg`         | Численность населения — женщины (в среднем за год)                         |
| `pop_urban_avg`         | Численность населения — городское население (в среднем за год)             |
| `pop_rural_avg`         | Численность населения — сельское население (в среднем за год)              |
| `pop_0_17_avg`          | Численность населения — 0–17 лет (в среднем за год)                        |
| `pop_18_plus_avg`       | Численность населения — 18 лет и старше (в среднем за год)                 |
| `pop_below_working_avg` | Численность населения — моложе трудоспособного (в среднем за год)          |
| `pop_working_avg`       | Численность населения — в трудоспособном возрасте (в среднем за год)       |
| `pop_above_working_avg` | Численность населения — старше трудоспособного (в среднем за год)          |
| `pop_pension_avg`       | Численность населения — пенсионного возраста (в среднем за год)            |
| `fixed_basket`          | Стоимость фиксированного набора потребительских товаров и услуг            |
| `ibr`                   | Индекс бюджетных расходов                                                  |


Получить этот список программно можно так:

```python
from reg_normalizer import RegionMatcher

matcher = RegionMatcher()
descriptions = matcher.get_indicator_descriptions()
for code, description in descriptions.items():
    print(f"{code}: {description}")
```

#### Присоединение показателей к данным

Метод `attach_indicators` поддерживает три сценария:

**Сценарий 1. В данных есть столбец с годом:**

```python
import pandas as pd
from reg_normalizer import RegionMatcher

matcher = RegionMatcher()

df = pd.DataFrame({
    'region': ['Московская область', 'Республика Татарстан'],
    'year': [2023, 2023]
})

# Нормализуем названия
df = matcher.match_dataframe(df, 'region')

# Присоединяем показатели — merge по региону и году
df = matcher.attach_indicators(
    df,
    indicators=['pop_total', 'ibr'],
    name_col='region',
    year_col='year'
)
```

**Сценарий 2. В данных нет столбца с годом — указываем год явно:**

```python
df = pd.DataFrame({
    'region': ['Московская область', 'Республика Татарстан']
})

df = matcher.match_dataframe(df, 'region')

# Присоединяем показатели за конкретный год
df = matcher.attach_indicators(
    df,
    indicators='pop_total',  # можно передать один код строкой
    name_col='region',
    year=2023
)
```

**Сценарий 3. Несколько показателей сразу:**

```python
df = matcher.attach_indicators(
    df,
    indicators=['pop_total', 'pop_urban', 'pop_rural', 'fixed_basket', 'ibr'],
    name_col='region',
    year=2020
)
```

#### Параметры `attach_indicators`


| Параметр     | Тип                   | Описание                                                        |
| ------------ | --------------------- | --------------------------------------------------------------- |
| `df`         | `DataFrame`           | Таблица с нормализованными названиями регионов                  |
| `indicators` | `str` или `list[str]` | Код показателя или список кодов (см. таблицу выше)              |
| `name_col`   | `str`                 | Имя столбца с названием региона. По умолчанию `'object_name'`   |
| `year_col`   | `str` (опц.)          | Столбец с годом в данных. Если указан — merge по региону и году |
| `year`       | `int` (опц.)          | Год для фильтрации. Используется, если `year_col` не задан      |
| `how`        | `str`                 | Тип соединения: `'left'` (по умолчанию) или `'outer'`           |


> Необходимо указать либо `year_col`, либо `year`. Если не указан ни один — будет вызвана ошибка `ValueError`.

#### Требования к формату таблицы

Метод `attach_indicators` ожидает, что входная таблица имеет **«длинный» (long) формат** — регионы и годы должны идти **в строках**, а не в столбцах:


| region               | year | ... |
| -------------------- | ---- | --- |
| Московская область   | 2020 | ... |
| Московская область   | 2021 | ... |
| Республика Татарстан | 2020 | ... |


Если в вашей таблице годы расположены по столбцам («широкий» формат), ее нужно предварительно привести к длинному формату, например с помощью `pd.melt`:

```python
# Было: столбцы — годы
#   region              | 2020 | 2021 | 2022
#   Московская область  | ...  | ...  | ...

df_long = df.melt(id_vars='region', var_name='year', value_name='value')
df_long['year'] = df_long['year'].astype(int)

# Теперь можно присоединять показатели
df_long = matcher.attach_indicators(df_long, indicators='pop_total',
                                    name_col='region', year_col='year')
```

## Как работает алгоритм

При вызове `find_best_match` или `match_dataframe` каждое название региона проходит через несколько последовательных шагов:

**1. Препроцессинг** — приведение строки к стандартному виду:
- замена визуально похожих латинских символов на кириллические (`o` → `о`, `p` → `р` и т.д.)
- дефисы и тире → пробелы, нормализация пробелов, lowercase
- расширение аббревиатур «Республика»: `Респ. X` и `X Респ.` → `республика X`
- удаление сносок (`1);2)`, `3);4)`) и единиц измерения (`, млн т`, `, млрд. руб.`)
- удаление служебных фраз (`в границах`, `без учета новых субъектов` и др.)

**2. Замена аббревиатур** — если preprocessed строка целиком совпадает с ключом в справочнике сокращений, она заменяется на полное название. Например: `спб` → `Санкт-Петербург`, `хмао` → `Ханты-Мансийский автономный округ — Югра`.

**3. Обнаружение составных строк** — если строка содержит разделители (`и`, `,`, `;`), каждая часть матчится отдельно. Если набор частей соответствует одному из правил (напр. `Тюменская область + ХМАО + ЯНАО`), возвращается канонический составной регион. Если правило не найдено — возвращается `None`.

**4. Нечеткое сопоставление** — для каждого эталонного региона вычисляется взвешенная оценка из четырех компонент:
- Levenshtein ratio по оригинальному тексту
- token_set_ratio по оригинальному тексту
- Levenshtein ratio по стеммированному тексту (Snowball, русский)
- token_set_ratio по стеммированному тексту

Итоговая оценка = взвешенная сумма по алгоритмам × взвешенная сумма по подходам. Если лучшая оценка ниже `threshold` — совпадение не засчитывается.

**5. Пост-анализ (только для `match_dataframe`)** — после матчинга всего датасета анализируется весь набор совпадений. Если в данных одновременно присутствует регион-родитель (напр. `Архангельская область`) и его автономный округ (`Ненецкий АО`), родитель автоматически переопределяется на вариант «без АО». Если автономный округ в данных не найден — родитель остается в варианте «с АО».

## Разработка и тестирование

### Запуск тестов

Проект использует `pytest` для тестирования. Все тесты находятся в директории `tests/`.

**Запустить все тесты:**

```bash
pytest tests/ -v
```

**Запустить тесты с подробным выводом:**

```bash
pytest tests/ -v -s
```

**Запустить конкретный тестовый файл:**

```bash
pytest tests/test_indicators.py -v
pytest tests/test_regions_validator.py -v
```

**Запустить конкретный тест:**

```bash
pytest tests/test_indicators.py::test_get_indicator_descriptions -v
```

**Запустить тесты с покрытием кода:**

```bash
pytest tests/ --cov=reg_normalizer --cov-report=html
```

После выполнения команды отчет о покрытии будет доступен в `htmlcov/index.html`.

### Структура тестов

- `tests/test_indicators.py` — тесты для функций работы с индикаторами (`get_indicator_descriptions`, `attach_indicators`)
- `tests/test_regions_validator.py` — тесты для `RegionMatcher` и вспомогательных функций (`preprocess_name`, `stem_region_name`, `find_best_match`, `match_dataframe`, `attach_fields`)

### Работа с виртуальным окружением `uv`

Если вы используете `uv` для управления зависимостями:

**Создание нового окружения:**

```bash
uv venv
```

**Установка зависимостей:**

```bash
uv pip install -e ".[dev]"
```

**Обновление зависимостей:**

```bash
uv pip install --upgrade -e ".[dev]"
```

**Деактивация окружения:**

```bash
deactivate
```

## Кастомизация

- **weights** — веса для алгоритмов сравнения ('levenshtein', 'token_set')
- **approach_weights** — веса для подходов ('original', 'stemmed')
- **threshold** — пороговое значение для принятия совпадения

Пример:

```python
custom_weights = {'levenshtein': 0.3, 'token_set': 0.7}
custom_approach_weights = {'original': 0.2, 'stemmed': 0.8}
match, score = matcher.find_best_match(
    "свердловск",
    weights=custom_weights,
    approach_weights=custom_approach_weights,
    threshold=60
)
```

## TODO

- Добавить обработку сокращений "Республика" (респ., р., Респ.)
- Решить проблему с фразами, в которых встречается "Российская Федерация", но это не регион
- Проверить, что хорошо обрабатываются вложенные автономные округа

## Лицензия  
  
Creative Commons License Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
