Сообщений 4    Оценка 220        Оценить  
Система Orphus

Автоматическая привязка текстовой адресной информации к классификатору КЛАДР

Автор: Корниенко С. А.
Источник: RSDN Magazine #4-2007
Опубликовано: 15.03.2008
Исправлено: 10.12.2016
Версия текста: 1.0
Алгоритм 1
Алгоритм 2

Рассмотрим условие задачи: нужно разработать алгоритм привязки адресной информации, набранной пользователями вручную как попало, к фиксированному классификатору адресов КЛАДР средствами Oracle 8i+.

Чтобы вы лучше представили себе качество исходных данных, приведу пример из строчек, которым я нашел соответствие вручную:

Текстовые данные Соответствующая запись КЛАДР
г Елец УЛ МАРКСА 34 399774 Липецкая обл, Елецкий р-н, г Елец, ул К.Маркса
Г ЛEБEДЯHЬ ЕЛЕЦКАЯ Здание магазина 399610 Липецкая обл, Лебедянский р-н, г Лебедянь, ул Елецкая
Добринский район Талицкий Чамлык - ул. Советская д 46 399450 Липецкая обл, Добринский р-н, с Талицкий Чамлык
ЛИПЕЦКАЯ ОБЛАСТЬ ИЗМАЛКОВСКИЙ Р-Н СЕЛО ПРЕОБРАЖЕНИЕ 399012 Липецкая обл, Измалковский р-н, с Преображенье
Липецк г. район Цемзавода 398000 Липецкая обл, г Липецк
ДОБРОВСКИЙ Р-Н.С Б ХОМУТЕЦ 399148 Липецкая обл, Добровский р-н, с Большой Хомутец (не путать с Малым Хомутцом)
ЛИПЕЦК Г ,ПОБЕДЫ ПР,21 398024 Липецкая обл, г Липецк, пр-кт Победы (не путать с площадью Победы)

Итак, каким же образом столь корявой, хаотичной, иногда избыточной, иногда недостаточной информации можно поставить в соответствие единственную правильную строчку из справочника? Вот такой алгоритм напрашивается изначально:

Алгоритм 1

0. Приводим все строчки к одинаковому регистру букв (строчному и заглавному).

1. Разбираемся со знаками препинания. Как показывает практика, запятые и точки в адресах ставятся как попало, зачастую они заменяют и друг дружку, и пробел. Например "Г.ЛИПЕЦК,УЛ. НЕДЕЛИНА". То же самое с некоторой натяжкой можно сказать и про дефис. Однако если мы просто удалим точки и запятые из строк, слова сольются и у нас получится "ГЛИПЕЦКУЛ НЕДЕЛИНА" – поэтому, вместо удаления, заменяем их пробелами: "Г ЛИПЕЦК УЛ__НЕДЕЛИНА". А чтобы после такой замены не было разночтений, заменяем в строке все сдвоенные пробелы на одинарные: "Г ЛИПЕЦК УЛ НЕДЕЛИНА".

2. Теперь надо очистить строки от мусора – то есть от тех элементов, которые будут мешать сравнению. В первую очередь это адресные идентификаторы "с", "село", "г", "ул" – и другие. Они мешают, потому что мы не сможем заставить "ул" всегда стоять перед названием, а не после него. Тот же самый "район" можно написать как "район", "р-он" или "р-н". Каким же образом можно выделить из строк "мешающиеся" элементы?

Вариант 2.1 – завести перечень адресных сокращений и удалять совпадающие с ними части строк. Правда, как этот перечень составить? Например, официальный классификатор, поставляющийся вместе с КЛАДР, содержит единственный вариант написания слова "район": "р-н".

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

Замечу, что просто так, "вслепую", удалять сокращения из строк нельзя – надо производить удаление только целых слов, в том числе расположенных в начале или в конце строки – иначе вы рискуете лишиться всех букв "г" и "с", встречавшихся в адресе.

Вариант 2.2 – удалить все слова короче определенной длины. Этот вариант хотя и проще (не нужно возиться со справочниками), но при этом теряется слишком много полезной информации и остаётся слишком много мусора. Даже если удалять только слова короче пяти символов, мы напрочь потеряем "ул. 9 мая", зато оставим слова "район" и "поселок".

3. В каждой строке КЛАДР присутствует индекс (например "398001"), а в исходной адресной информации индекс отсутствует. Значит, перед сравнением строк индекс нужно удалить. Зато в исходной информации присутствуют номера домов (например "55" или "32А"), которых, естественно, нет в КЛАДР. Значит, номера домов тоже нужно удалить. Напрашивается решение – удалить все слова, начинающиеся с цифры.

Правда, это может сделать для нас одинаковыми улицы "ул. 1-я садовая" и "ул. 2-я садовая". Или улицы "30 лет октября" и "40 лет октября" – да, да, в одном городе! :) Будем считать это погрешностью алгоритма.

4. Но и это еще не всё. К сожалению, то, что принято считать номером дома, не обязательно начинается с цифры. В примерах мы видим, что в качестве номеров домов могут выступать надписи "Здание магазина" или "Район цемзавода". Почему они считаются номерами домов? Потому что про них ничего нет в КЛАДР. Текст, который идёт до них, задает общеизвестный населенный пункт, территорию или улицу, а правая часть надписи лишь расшифровывает, уточняет адрес. Иными словами, текст адреса может содержать справа сколь угодно большой объем дополнительной информации, не привязанной к КЛАДР.

К сожалению, про КЛАДР можно сказать то же самое: он может содержать дополнительную информацию слева. И дело даже не в индексе и названии области – "399774 Липецкая обл" – их всегда можно искусственно удалить. Дело в названии района. Очень часто, при вводе данных руками, районные центры указываются без указания района, к которому они относятся – дескать, что тут непонятного?! Вот и получается: у нас "г. Елец", а в КЛАДР – "Елецкий р-н, г. Елец". В то же время, информацию о районе удалять из адресной строки нельзя. Например, "деревня Ивановка" у нас есть практически в каждом районе области.

5. Таким образом, мы приходим к выводу, что соответствие нужно устанавливать между левой частью строки адреса и правой частью записи КЛАДР. Например, для адреса "г. Лебедянь ул. Елецкая – Здание магазина", это будет соответствие между строкой "лебедянь елецкая здание магазина" и записью – "липецкая лебедянский лебедянь елецкая". Как сформулировать условие такого соответствия на языке SQL? Оператор LIKE тут не поможет, регулярных выражений в Oracle 8i еще нет...

Заметим, что в предыдущем примере запись КЛАДРа "липецкая лебедянский лебедянь", отождествляющая целиком город Лебедянь, тоже удовлетворяет нашему правилу сравнения. Таким образом, нужно вводить некоторую количественную меру совпадения строк – чтобы определить, какая из частично совпавших записей наиболее подходит к анализируемой строке.

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

Если таких записей несколько – выберем наиболее короткую. Такое может случиться, если строка задает несуществующую улицу. Например, если в г. Лебедянь нет ул. Ситцевой, то следующие записи КЛАДР будут иметь одинаковое количество совпавших символов со строкой "лебедянь ситцевая": "липецкая лебедянский лебедянь", "липецкая лебедянский лебедянь елецкая". Выбрать в этом случае нужно запись, отождествляющую г.Лебедянь целиком.

Результаты: разработав и запустив этот алгоритм на 2000 адресных строк, и проведя визуальный контроль результатов, мы получили около 500 ошибок – то есть 25%. Рассмотрев подробнее характерные примеры, приведенные в начале статьи, видим:

Текстовые данные Соответствующая запись КЛАДРа Запись КЛАДРа, найденная алгоритмом  
елец маркса липецкая елецкий елец к маркса липецкая елецкий елец марта
лебедянь елецкая здание магазина липецкая лебедянский лебедянь елецкая липецкая лебедянский лебедянь елецкая +
добринский талицкий чамлык советская липецкая добринский талицкий чамлык липецкая добринский талицкий чамлык +
липецкая измалковский преображение липецкая измалковский преображенье липецкая измалковский преображенье +
липецк цемзавода липецкая липецк липецкая липецк целинная
добровский б хомутец липецкая добровский большой хомутец (не путать с Малым Хомутцом) добровский богородицкое
липецк победы(сокращение пр опущено) липецкая липецк победы(сокращение пр-кт опущено) липецкая липецк победы(сокращение пл опущено)

Как видно, основной недостаток алгоритма – то, что каждое следующее слово для него менее значимо, чем предыдущее. Кроме того, алгоритм игнорирует различия между служебными словами КЛАДР: "проспект Победы" и "площадь Победы" для него – одно и то же. Постараемся учесть все эти недостатки во втором варианте алгоритма.

Алгоритм 2

0-1. Эти начальные шаги обработки строк аналогичны алгоритму 1.

2. На этот раз мы не будем удалять служебные слова КЛАДР из строк – напротив, они помогут нам отличать площадь от проспекта. Мы вообще не будем никаким специфическим образом "чистить" информацию. Алгоритм будет довольно универсальным, и может быть применен для поиска соответствия любых строк.

Если к недостаткам прошлого алгоритма можно было отнести то, что ему требовался список служебных слов КЛАДР для исключения их из строк, то этому алгоритму такой список не нужен. Зато нам понадобятся две временные таблички, в которые мы будем разбирать на слова анализируемые строки адреса и записи КЛАДР. Итак:

create table TMP_WORDS_SRC( OBJ integer, WORD varchar2(30), SOUND varchar2(4), CNT integer, LEN integer);alter table TMP_WORDS_SRCadd constraint PK_TMP_WORDS_SRC primary key (OBJ, WORD); create table TMP_WORDS_KLADR(OBJ integer,WORD varchar2(30),SOUND varchar2(4),CNT integer,LEN integer);alter table TMP_WORDS_KLADRadd constraint PK_TMP_WORDS_KLADR primary key (OBJ, WORD);

3. Разбираем анализируемую строку и каждую запись КЛАДР на слова. Каждое слово заносим в соответствующую таблицу. При этом в поле OBJ заносим числовой код, позволяющий однозначно идентифицировать запись КЛАДР. Как видно из первичного ключа, если в строке два раза встречается одно и то же слово, оно должно быть занесено в таблицу лишь однажды. Само слово (приведенное к одному регистру) заносится в колонку WORD.

В колонку LEN заносится длина слова (предполагается, что взять длину из таблицы быстрее, чем вычислять её по строке). В колонку CNT заносим номер символа, означающий позицию в разбираемой строке, на которой заканчивается текущее слово. Эти данные пригодятся позже – когда нужно будет выделить правую часть строки, для которой не было найдено соответствия в КЛАДР – в качестве поля "Номер дома".

4. Самое "загадочное" поле – SOUND. Это некий четырехсимвольный хэш слова, построенный по алгоритму Soundex, впервые предложенному Робертом Расселом и Маргарет Обелл в 1918(!) году (подробнее – здесь). Ключевая особенность алгоритма – для созвучных слов он возвращает одинаковые хэши.

До сих пор большие дискуссии вызывает применимость алгоритма Soundex для русских слов. Сам алгоритм понимает только буквы латинского алфавита, и рассчитан на английскую фонетику – поэтому, если пытаться применить его к русским словам, в первую очередь необходимо получить их английскую транскрипцию. И чем правильнее это будет сделано, тем правильнее будет обрабатывать эти слова Soundex.

Самым примитивным образом это можно сделать так:

TRANSLATE(UPPER(xCurWord), 'ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮЁ', 'ICUKENGQQZHWFWVAPROLDJEAQSMITWBUE'); 

Однако для более качественной транскрипции рекомендую разработать (и опубликовать ;-) собственную функцию.

Для слов, начинающихся с цифр, Soundex возвращает null. Такие результаты записывать в таблицу не будем. Такие образом, мы снова игнорируем слова, начинающиеся с цифр – как в пункте 3 предыдущего алгоритма.

Для тестирования созвучности слов с точки зрения алгоритма soundex проведем следующее испытание:

Слово1 soundex(Слово1) Слово2 soundex(Слово2) Слово3 soundex(Слово3)  
raion R500 r-n R500 r-on R500 +
pl P400 pr P600 pr-kt P623
preobrajenie P616 preobrajenwe P616 +
dobrovskii D161 dobrinskii D165 +
moskovskaq M212 maqkovskogo M212

Как видим, алгоритм прекрасно понял, что "район", "р-н", "р-он" – одно и тоже, но зато "пл", "пр" и "пр-кт" с его точки зрения это три разных слова. Вообще-то он прав, так как "пр" с точки зрения словаря сокращений КЛАДР – это "проезд", а вовсе не "проспект". Только вот в большинстве наших адресов, и "проезд" и "проспект" обозначаются одинаково "пр". Я еще не встречал, чтобы в городе были проспект и проезд с одинаковыми названиями. А вот одноименные проспект и площадь встречаются регулярно.

Самым простым решением в этом случае будет замена слова "пр" на "пр-кт" на этапе 2 (перед помещением во временные таблицы) – как в исходных адресах, так и в записях КЛАДРа. Это избавит программу от путаницы между адресами вида "г. Липецк пл. Победы", "г. Липецк пр. Победы" и "г. Липецк пр-кт. Победы".

Soundex прекрасно справился с орфографической ошибкой в слове "Преображение", не спутал "Добринский" и "Добровский" районы. Зато, по его мнению, улицы "Московская" и "Маяковского" – абсолютно созвучные. Это говорит нам о том, что исключительно на Soundex полагаться нельзя. Из двух созвучных слов предпочтение нужно отдать слову со схожим написанием.

5. Теперь перейдём непосредственно к алгоритму сравнения строк. Сравнивая две строки адресной информации, преобразованные в пунктах 0-4 данного алгоритма к наборам слов, мы можем получить следующие числовые характеристики похожести/непохожести строк:

5.1 "Количество созвучных слов в строке А и строке Б". Поскольку в одной строчке может быть несколько слов с одинаковым звучанием, чтобы избежать повторения перестановок, определим этот показатель более узким образом: "Количество уникальных созвучий, имеющихся одновременно в строке А и в строке Б".

5.2 Наряду с этой характеристикой, можно также рассмотреть показатель "Суммарная длина слов, уникальные созвучия которых имеются одновременно в строке А и в строке Б". Длины созвучных слов стоит складывать из поля LEN таблицы TMP_WORDS_KLADR, так как в этой таблице слова более соответствуют эталонным.

5.3 "Количество полностью совпадающих слов между строками А и Б". Тут перестановок быть не может, так как поле "Слово" входит в первичные ключи таблиц, наряду с числовым идентификатором разбираемой строки.

5.4 "Суммарная длина слов, полностью совпавших в строках А и Б". Этот показатель по простоте напоминает числовую характеристику, использовавшуюся в прошлом алгоритме.

5.5 и 5.6 "Общее количество слов в данной записи КЛАДР" и "Общая длина строки данной записи КЛАДР" – эти показатели помогут выбрать соответствующую строку, если значения всех остальных их характеристик совпадают.

6. Утверждается, что используя данные 6 показателей, можно построить достаточно эффективный алгоритм сопоставления строк. Я предлагаю применять их в следующем порядке:

6.1 Сначала выбираем записи с максимальной длиной полностью совпавших слов.

6.2 Среди найденных строк, выбираем те, у которых наибольшая длина созвучных слов

6.3 Если в результате опять получилось несколько строк – выбираем строки с наименьшей общей длиной.

6.4 Честно говоря, даже в этом случае у вас может получиться несколько строк – в КЛАДР встречаются записи с одинаковой улицей и городом, но разными индексами. В этом случае берем первую попавшуюся строку.

7. Попробуем обосновать выбранный алгоритм. В первую очередь сомнение вызывает вопрос, какой критерий использовать первым: наибольшую длину совпавших или созвучных слов? У меня получались примерно одинаковые результаты в обоих вариантах работы.

Если слова созвучны – это еще не значит, что они одинаковые (см. пример ул. Московская/Маяковского). А вот если слова совпадают – значит, они совпадают. Правда, если слова не совпадают, это ещё не значит что они разные. ;) Будем исходить из принципа гарантированного результата, и ориентироваться в первую очередь именно на длину совпавших слов, и лишь среди них выбирать наибольшее число созвучных.

Замечу, что вместо "количества слов" я везде использую "длину". Сейчас я приведу пример, почему это предпочтительнее: рассмотрим строку "Добринский район с. Алексеевка ул. Ленина". Но в КЛАДР в селе Алексеевка нет улиц, есть только запись соответствующая всему селу: "Добринский район с. Алексеевка". Зато улица Ленина есть в селе Добринка: "Добринский район с. Добринка ул. Ленина":

Исходная строка Запись КЛАДР Количество совпавших слов Общая длина совпавших слов  
Добринский район с Алексеевка ул Ленина Добринский район с Алексеевка 4 26 +
Добринский район с Алексеевка ул Ленина Добринский район с Добринка ул Ленина 5 24

Как видно из таблицы, критерий суммарной длины совпавших слов в этом случае более достоверен, чем их количество. В этом случае мы не считаем все слова "равноценными", а на числовую характеристику совпадения влияет "вес" каждого совпавшего слова.

Финальный отбор по наименьшей общей длине строки нужен для отбрасывания излишней детализации. Например, если в г. Лебедянь нет ул. Ситцевой (и нет созвучной ей улицы), то из двух записей "Лебедянский район г. Лебедянь" и "Лебедянский район г. Лебедянь ул. Елецкая" будет выбрана именно первая:

Исходная строка Запись КЛАДР Общая длина совпавших слов Общая длина созвучных слов Общая длина записи КЛАДР  
Лебедянский район г Лебедянь ул Ситцевая Лебедянский район г Лебедянь 25 25 25 +
Лебедянский район г Лебедянь ул Ситцевая Лебедянский район г Лебедянь ул Елецкая 25 25 34

8. В общем виде для работы с табличками TMP_WORDS_SRC и TMP_WORDS_KLADR, SQL-запрос, сопоставляющий строки, выглядит следующим образом. В качестве результатов он выдает z_eq – код разбираемой строки, k_eq – код соответствующей ей строки КЛАДР, len_sovp_eq – суммарную длину полностью совпавших слов, len_sovp_ne – суммарную длину созвучных слов, len_total – общую длину выбранной записи КЛАДРа.

Выбор наиболее подходящей записи осуществляется с помощью аналитических функций ранжирования, доступных в Oracle 8i. Без них осуществить выборку совпавших строк в одном sql-запросе было бы существенно тяжелее.

      select * from
(
  select z_eq, k_eq, len_sovp_eq, len_sovp_ne, len_total,
  RANK() OVER(PARTITION BY z_eq ORDERBY len_sovp_eq DESC, len_sovp_ne DESC, len_total ASC, k_eq ASC) rank from
  ( 
 select words_od_zd_eq.z_eq, words_od_zd_eq.k_eq, 
  words_od_zd_eq.count_sovp_eq, words_od_zd_eq.len_sovp_eq, 
  words_od_zd_ne.count_sovp_ne, words_od_zd_ne.len_sovp_ne, 
  total_words_kladr.count_total, total_words_kladr.len_total
 from-- вычисляем общие характеристики каждой записи КЛАДРа
 ( select k.obj, count(*) count_total, sum(len) len_total from tmp_words_kladr k groupby k.obj ) total_words_kladr,
 -- проверяем все пары слов на созвучность
 ( select z_ne, k_ne, count(*) count_sovp_ne, sum(len) len_sovp_ne
from 
( select z.obj z_ne, k.obj k_ne, max(k.len) len
  from tmp_words_src z, tmp_words_kladr k 
  where z.sound=k.sound
  groupby z.obj, k.obj, k.sound )
groupby z_ne, k_ne ) words_od_zd_ne,
 -- проверяем все пары слов на равенство
 ( select z.obj z_eq, k.obj k_eq, count(*) count_sovp_eq, sum(k.len) len_sovp_eq
from tmp_words_src z, tmp_words_kladr k 
where z.word=k.word
groupby z.obj, k.obj ) words_od_zd_eq
 where
 total_words_kladr.obj = words_od_zd_ne.k_ne and
 words_od_zd_eq.k_eq = words_od_zd_ne.k_ne and
 words_od_zd_eq.z_eq = words_od_zd_ne.z_ne
  )
)
where rank=1

Применение данного алгоритма на практике показало довольно низкий процент ошибок: из 2000 записей, ручной корректировке подверглись не более 60 штук, что составляет всего лишь 3% от всей выборки.


Эта статья опубликована в журнале RSDN Magazine #4-2007. Информацию о журнале можно найти здесь
    Сообщений 4    Оценка 220        Оценить