Java нечеткая строка, соответствующая именам
у меня есть автономный процесс загрузки данных CSV, который я закодировал на Java, который должен использовать некоторое нечеткое соответствие строк. Это определенно не идеально, но у меня нет выбора. Я сопоставляю, используя имя и фамилию, и я кэширую все возможности в начале запуска. После поиска совпадения мне нужно, чтобы этот объект person был в нескольких местах во время запуска. Я использовал guava Objects.hashCode()
чтобы создать хэш из firstname и lastname.
механизм кэширования выглядит это:
Map<Integer,PersonDO> personCache = Maps.newHashMap();
for(PersonDO p: dao.getPeople()) {
personCache.put(Objects.hashCode(p.getFirstName(),p.getLastName()), p);
}
большую часть времени я получаю хиты на firstname + lastname, но когда он пропускает, я отступаю, используя Apache StringUtils.getLevenshteinDistance()
чтобы попытаться сопоставить его. Вот как идет соответствующий логический поток:
person = personCache.get(Objects.hashCode(firstNameFromCSV,lastNameFromCSV));
if(person == null) {//fallback to fuzzy matching
person = findClosetMatch(firstNameFromCSV+lastNameFromCSV);
}
это findClosetMatch()
способ:
private PersonDO findClosetMatch(String name) {
int min = 15;//initial value
int testVal=0;
PersonDO matchedPerson = null;
for(PersonDO person: personCache.values()) {
testVal = StringUtils.getLevenshteinDistance(name,person.getFirstName()+person.getLastName());
if( testVal < min ) {
min = testVal;
matchedPerson = person;
}
}
if(matchedPerson == null) {
throw new Exception("Unable to find person: " + name)
}
return matchedPerson;
}
это отлично работает с простыми орфографическими ошибками, опечатками и сокращенными именами(т. е. Mike->Michael), но когда я полностью пропускаю одно из входящих имен в кэше, я возвращаю ложноположительное совпадение. Чтобы этого не произошло, я установил минимальное значение в findClosetMatch()
до 15 (т. е. не более 15 символов); он работает большую часть времени, но у меня все еще было несколько неправильных совпадений:Mike Thompson
хиты Mike Thomas
etc.
вне выяснения способа получить первичный ключ в загружаемый файл, кто-нибудь видит способ улучшить этот процесс? Есть другие подходящие алгоритмы,которые могут помочь?
4 ответов
когда я смотрю на эту проблему, я замечаю пару ключевых фактов, чтобы основывать некоторые улучшения на:
факты и наблюдения
- максимальные итерации 1000.
- 15 для Левенштейна расстояние звуки действительно высокая для меня.
- вы знаете, наблюдая данные эмпирически, как должно выглядеть ваше нечеткое сопоставление (есть много случаев для нечеткого сопоставления, и каждый зависит от почему данные плохой.)
- построив этот API-как вы могли бы подключить многие алгоритмы, в том числе ваши собственные и другие, как Soundex, вместо того, чтобы зависеть только от одного.
требования
я интерпретировал вашу проблему как требующую следующих двух вещей:
- вы
PersonDO
объекты, которые вы хотите посмотреть с помощью ключа, основанного на имени. Похоже, вы хотите сделать это, потому что тебе нужна ранее существовавшиеPersonDO
из которых один существует на уникальное имя, и одно и то же имя может появиться несколько раз в вашем цикле/рабочем процессе. - вам нужно "нечеткое соответствие", потому что входящие данные не являются чистыми. Для целей этого алгоритма мы предположим, что если имя "соответствует", оно всегда должно использовать одно и то же
PersonDO
(другими словами, уникальный идентификатор человека - это его имя, что, очевидно, не так в реальной жизни, но, похоже, работает для вас здесь.)
реализация
Далее, давайте рассмотрим некоторые улучшения в вашем коде:
1. Очистка: ненужная манипуляция хэш-кодом.
вам не нужно создавать хэш-коды самостоятельно. Это немного запутывает проблему.
вы просто генерируете хэш-код для комбинации firstname + lastname. Это именно то, что HashMap
сделал бы, если бы вы дали ему объединенную строку как ключ. Итак, просто сделайте это (и добавьте пробел, на всякий случай, если мы хотим отменить разбор первого/последнего из ключа позже).
Map<String, PersonDO> personCache = Maps.newHashMap();
public String getPersonKey(String first, String last) {
return first + " " + last;
}
...
// Initialization code
for(PersonDO p: dao.getPeople()) {
personCache.put(getPersonKey(p.getFirstName(), p.getLastName()), p);
}
2. Cleanup: создайте функцию извлечения для выполнения поиска.
поскольку мы изменили ключ на карте, нам нужно изменить функцию поиска. Мы построим это как мини-API. Если бы мы всегда точно знали ключ (т. е. уникальные идентификаторы), мы бы, конечно, просто использовали Map.get
. Поэтому мы начнем с этого, но так как мы знаем, нужно будет добавить нечеткое соответствие мы добавим обертку, где это может произойти:
public PersonDO findPersonDO(String searchFirst, String searchLast) {
return personCache.get(getPersonKey(searchFirst, searchLast));
}
3. Создайте алгоритм нечеткого сопоставления самостоятельно, используя скоринг.
обратите внимание, что, поскольку вы используете гуаву, я использовал здесь несколько удобств (Ordering
, ImmutableList
, Doubles
, etc.).
во-первых, мы хотим сохранить работу, которую мы делаем, чтобы выяснить, насколько близко матч. Сделайте это с помощью POJO:
class Match {
private PersonDO candidate;
private double score; // 0 - definitely not, 1.0 - perfect match
// Add candidate/score constructor here
// Add getters for candidate/score here
public static final Ordering<Match> SCORE_ORDER =
new Ordering<Match>() {
@Override
public int compare(Match left, Match right) {
return Doubles.compare(left.score, right.score);
}
};
}
Далее мы создаем метод для оценки родовое название. Мы должны забивать имена и фамилии отдельно, потому что это уменьшает шум. Например, нам все равно, соответствует ли имя какой - либо части фамилии -если ваше имя не может случайно находиться в поле lastname или наоборот, что вы должны учитывать намеренно, а не случайно (мы рассмотрим это позже).
обратите внимание, что нам больше не нужно "расстояние Макса Левенштейна". Это потому, что мы нормализуем их по длине, а ближайший матч мы выберем позже. 15 символов добавляет / редактирует / удаляет кажется очень высоким, и поскольку мы минимизировали пустую проблему имени/фамилии, забив имена отдельно, мы могли бы, вероятно, теперь выбрать максимум 3-4, если вы хотите (забив что-нибудь еще как 0).
// Typos on first letter are much more rare. Max score 0.3
public static final double MAX_SCORE_FOR_NO_FIRST_LETTER_MATCH = 0.3;
public double scoreName(String searchName, String candidateName) {
if (searchName.equals(candidateName)) return 1.0
int editDistance = StringUtils.getLevenshteinDistance(
searchName, candidateName);
// Normalize for length:
double score =
(candidateName.length() - editDistance) / candidateName.length();
// Artificially reduce the score if the first letters don't match
if (searchName.charAt(0) != candidateName.charAt(0)) {
score = Math.min(score, MAX_SCORE_FOR_NO_FIRST_LETTER_MATCH);
}
// Try Soundex or other matching here. Remember that you don't want
// to go above 1.0, so you may want to create a second score and
// return the higher.
return Math.max(0.0, Math.min(score, 1.0));
}
как отмечалось выше, вы можете подключить сторонние или другие алгоритмы сопоставления слов и получить от общего знания всех из них.
теперь, мы идем через весь список и оценка каждое имя. Обратите внимание, что я добавил место для "твиков". Твики могут включать:
-
разворот: если Персондо "Бенджамин Франклин", но CSV-лист может содержать" Франклин, Бенджамин", то вы захотите исправить для обратных имен. В этом случае вы, вероятно, захотите добавить метод
checkForReversal
это забьет имя в обратном порядке и возьмет этот балл, если он значительно выше. если оно соответствовало в обратном точно, то вы дали бы ему 1.0 оценка. - сокращения: вы можете дать оценку бонус удар, Если либо имя / фамилия совпадает идентично, а другой полностью в кандидате (или наоборот). Это может означать сокращение. Вы можете дать пособие Левенштейна 1 для учета "Ник/Николас" или аналогичного.
- общие прозвища: вы можете добавить набор известных прозвищ ("Роберт - > Боб, Роб, Бобби, Robby"), а затем забить имя поиска против всех из них и взять высокий балл. если он соответствует любому из них, вы, вероятно, дадите ему оценку 1.0.
как вы можете видеть, построение этого как серии API дает нам логические местоположения, чтобы легко настроить это на содержание нашего сердца.
на С alogrithm:
public static final double MIN_SCORE = 0.3;
public List<Match> findMatches(String searchFirst, String searchLast) {
List<Match> results = new ArrayList<Match>();
// Keep in mind that this doesn't scale well.
// With only 1000 names that's not even a concern a little bit, but
// thinking ahead, here are two ideas if you need to:
// - Keep a map of firstnames. Each entry should be a map of last names.
// Then, only iterate through last names if the firstname score is high
// enough.
// - Score each unique first or last name only once and cache the score.
for(PersonDO person: personCache.values()) {
// Some of my own ideas follow, you can tweak based on your
// knowledge of the data)
// No reason to deal with the combined name, that just makes things
// more fuzzy (like your problem of too-high scores when one name
// is completely missing).
// So, score each name individually.
double scoreFirst = scoreName(searchFirst, person.getFirstName());
double scoreLast = scoreName(searchLast, person.getLastName());
double score = (scoreFirst + scoreLast)/2.0;
// Add tweaks or alternate scores here. If you do alternates, in most
// cases you'll probably want to take the highest, but you may want to
// average them if it makes more sense.
if (score > MIN_SCORE) {
results.add(new Match(person, score));
}
}
return ImmutableList.copyOf(results);
}
теперь мы модифицируем ваш findClosestMatch, чтобы получить только самый высокий из всех матчей (броски NoSuchElementException
если нет в списке).
возможные твики:
- вы можете проверить, если несколько имен забили очень близко, и либо сообщить о бегунах (см. ниже), или пропустить строку для ручного выбора позже.
- вы можете сообщить, сколько других матчей было (если у вас очень жесткий алгоритм подсчета очков).
код:
public Match findClosestMatch(String searchFirst, String searchLast) {
List<Match> matches = findMatch(searchFirst, searchLast);
// Tweak here
return Match.SCORE_ORDER.max(list);
}
.. а затем измените наш оригинальный геттер:
public PersonDO findPersonDO(String searchFirst, String searchLast) {
PersonDO person = personCache.get(getPersonKey(searchFirst, searchLast));
if (person == null) {
Match match = findClosestMatch(searchFirst, searchLast);
// Do something here, based on score.
person = match.getCandidate();
}
return person;
}
4. Доклад "нечеткость" по-другому.
наконец, вы заметите, что findClosestMatch
не просто возвращает человека, он возвращает Match
- это так, что мы можем изменить программу для обработки нечетких совпадений по-разному от точных совпадений.
некоторые вещи, которые вы, вероятно, хотите сделать с этим:
- отчет догадки: сохранить все имена, которые соответствуют на основе нечеткости в список, так что вы можете сообщить те, и они могут быть проверены позже.
- проверить сначала: вы можете добавить элемент управления для включения и выключения, действительно ли он использует нечеткие совпадения или просто сообщает о них, чтобы вы могли массировать данные, прежде чем он придет.
- defenesiveness данные: вы можете квалифицировать любые изменения, сделанные в нечетком матче, как "неопределенные". Например, вы можете запретить любые "основные изменения" в записи Person, если совпадение размытый.
вывод
как вы можете видеть, это не слишком много кода, чтобы сделать это самостоятельно. Сомнительно, что когда-либо будет библиотека, которая будет предсказывать имена, а также вы можете знать данные самостоятельно.
построение этого в кусках, как я сделал в примере выше, будет позволяют вам итерации и настройки легко и даже подключить сторонние библиотеки, чтобы улучшить ваш счет, а не в зависимости от них полностью-ошибки и все.
использовать вас db для выполнения поиска ? Использование регулярного выражения в вашем select или use
LIKE
операторпроанализируйте свою базу данных и попробуйте построить или дерево Хаффмана или несколько таблиц для выполнения поиска по ключу.
нет лучшего решения, во всяком случае, вам придется иметь дело с какой-то эвристикой. Но вы можете искать другую реализацию расстояния Левенштейна (или реализовать ее самостоятельно). Эта реализация должна давать разные оценки различным символьным операциям (вставка, удаление) для разных символов. Например, вы можете дать более низкие оценки для пар символов, которые близки на клавиатуре. Кроме того, вы можете динамически вычислять порог максимального расстояния на основе строки длина.
и у меня есть совет для вас. Каждый раз, когда вы вычисляете расстояние Левенштейна, выполняются N * m операций, где n и m - длины строк. Есть Левенштейна автомата который вы создаете один раз, а затем очень быстро оцениваете для каждой строки. Будьте осторожны, так как NFA очень дорого оценивать, вам нужно сначала преобразовать его в DFA.
может быть, вы должны взглянуть на введение. Я надеюсь на это. включает все возможности нечеткого поиска, которые вам нужны. Из вас даже можно использовать СУБД полнотекстового поиска, если он поддерживается. Например, PostgreSQL поддерживает полный текст.
это то, что я сделал с подобным случаем использовать:
- сопоставьте имя и фамилию отдельно, это сделает более точное совпадение и устранит некоторые из ложных срабатываний:
distance("a b", "a c") is 33% max(distance("a", "a"), distance("b", "c")) is 100%
- ваши базы
min
критерии расстояния по длине входных строк, т. е.0
для строк короче 2 символов,1
для строк короче 3 символов.
int length = Math.min(s1.length(), s2.length);
int min;
if(length <= 2) min = 0; else
if(length <= 4) min = 1; else
if(length <= 6) min = 2; else
...
эти два должны работать для вашего ввода.