我有一个独立的 CSV 数据加载过程,我用 Java 编码,它必须使用一些模糊字符串匹配。这绝对不理想,但我没有太多选择。我使用名字和姓氏进行匹配,并在运行开始时缓存所有可能性。找到匹配项后,我需要该人在运行期间对象多个位置。我用的是 Guava 的 Objects.hashCode()
从名字和姓氏中创建一个哈希值。
缓存机制如下所示:
Map<Integer,PersonDO> personCache = Maps.newHashMap();
for(PersonDO p: dao.getPeople()) {
personCache.put(Objects.hashCode(p.getFirstName(),p.getLastName()), p);
}
大多数情况下,我会点击名字+姓氏,但是当它错过时,我会使用 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
等等。除了找出一种将主键添加到正在加载的文件中的方法之外,有没有人看到改进此过程的方法?任何其他匹配算法可以在这里提供帮助?
最佳答案
当我看到这个问题时,我注意到一些关键事实可以作为一些改进的基础:
事实和观察
要求
我已经将您的问题解释为需要以下两件事:
PersonDO
要通过基于名称的键查找的对象。听起来您想这样做是因为 您需要一个预先存在的 PersonDO
其中一个存在每个唯一名称 ,并且在您的循环/工作流程中可能会多次出现相同的名称。 PersonDO
(换句话说,一个人的唯一标识符是他们的名字,这在现实生活中显然不是这样,但在这里似乎对你有用)。 执行
接下来,让我们看看对您的代码进行一些改进:
1.清理:不必要的哈希码操作。
您不需要自己生成哈希码。这有点混淆了这个问题。
您只需为名字 + 姓氏的组合生成一个哈希码。这正是
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.清理:构建一个检索函数来执行查找。
由于我们更改了 map 中的键,因此我们需要更改查找功能。我们将像一个迷你 API 一样构建它。如果我们总是准确地知道 key (即唯一 ID),我们当然会使用
Map.get
.所以我们将从这个开始,但是因为我们知道我们需要添加模糊匹配,所以我们将在可能发生的情况下添加一个包装器:public PersonDO findPersonDO(String searchFirst, String searchLast) {
return personCache.get(getPersonKey(searchFirst, searchLast));
}
3. 使用评分自己构建模糊匹配算法。
请注意,由于您使用的是 Guava,因此我在这里使用了一些便利(
Ordering
、 ImmutableList
、 Doubles
等)。首先,我们希望保留我们所做的工作,以确定匹配的接近程度。使用 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);
}
};
}
接下来,我们创建一个对通用名称进行评分的方法。我们应该分别对名字和姓氏进行评分,因为它可以减少噪音。例如,我们不关心名字是否与姓氏的任何部分匹配——除非您的名字可能不小心出现在姓氏字段中,反之亦然,您应该有意而不是无意地考虑到这一点(我们稍后会解决这个问题) .
请注意,我们不再需要“最大编辑距离”。这是因为我们将它们标准化为长度,稍后我们将选择最接近的匹配项。 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));
}
如上所述,您可以插入第三方或其他单词匹配算法,并从所有这些算法的共享知识中获益。
现在,我们遍历整个列表并对每个名称进行评分。请注意,我为“调整”添加了一个位置。调整可能包括:
checkForReversal
这将对名称进行反向评分,如果该分数明显更高,则取该分数。 如果正好相反,你会给它 1.0 分 . 正如您所看到的,将其构建为一系列 API 为我们提供了逻辑位置,可以轻松地将其调整为我们的核心内容。
继续算法:
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);
}
.. 然后修改我们原来的 getter:
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
— 这样我们就可以修改程序,将模糊匹配与精确匹配区别对待。你可能想用这个做一些事情:
结论
正如您所看到的,自己编写代码并不多。令人怀疑的是,是否会出现一个可以预测名称以及您自己可以了解数据的库。
像我在上面的例子中所做的那样分块构建它会 允许您轻松迭代和调整 甚至插入第三方库来提高你的得分,而不是完全依赖它们——错误等等。
关于Java模糊字符串与名称匹配,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21057708/