StringDistances.jl/src/normalize.jl

265 lines
8.8 KiB
Julia
Raw Permalink Normal View History

struct Normalize{S <: SemiMetric} <: SemiMetric
dist::S
end
"""
normalize(dist::SemiMetric)
Normalize a metric, so that `evaluate` always return a Float64 between 0 and 1
"""
normalize(dist::SemiMetric) = Normalize(dist)
normalize(dist::Normalize) = dist
# A normalized distance is between 0 and 1, and accept a third argument, max_dist.
function (dist::Normalize{<: Union{Levenshtein, DamerauLevenshtein}})(s1, s2, max_dist = 1.0)
2020-02-13 15:44:27 +01:00
((s1 === missing) | (s2 === missing)) && return missing
2019-08-20 19:21:31 +02:00
s1, s2 = reorder(s1, s2)
len1, len2 = length(s1), length(s2)
len2 == 0 && return 1.0
2020-02-12 15:41:46 +01:00
d = dist.dist(s1, s2, ceil(Int, len2 * max_dist))
out = d / len2
out > max_dist ? 1.0 : out
2017-08-05 20:45:19 +02:00
end
2018-05-16 00:39:50 +02:00
function (dist::Normalize{<: QGramDistance})(s1, s2, max_dist = 1.0)
2020-02-13 15:44:27 +01:00
((s1 === missing) | (s2 === missing)) && return missing
2019-12-12 20:48:52 +01:00
# When string length < q for qgram distance, returns s1 == s2
s1, s2 = reorder(s1, s2)
len1, len2 = length(s1), length(s2)
2020-02-09 19:42:29 +01:00
len1 <= dist.dist.q - 1 && return convert(Float64, s1 != s2)
2020-07-19 21:38:54 +02:00
if dist.dist isa QGram
2020-07-19 21:37:49 +02:00
out = dist.dist(s1, s2) / (len1 + len2 - 2 * dist.dist.q + 2)
2019-12-12 20:48:52 +01:00
else
2020-07-19 21:37:49 +02:00
out = dist.dist(s1, s2)
2019-12-12 20:48:52 +01:00
end
2020-07-19 21:37:49 +02:00
out > max_dist ? 1.0 : out
2019-12-12 19:21:36 +01:00
end
2019-08-17 18:26:24 +02:00
function (dist::Normalize)(s1, s2, max_dist = 1.0)
2020-07-19 21:37:49 +02:00
out = dist.dist(s1, s2)
out > max_dist ? 1.0 : out
2020-02-13 15:44:27 +01:00
end
"""
Partial(dist)
Creates the `Partial{dist}` distance.
`Partial{dist}` normalizes the string distance `dist` and modify it to return the
minimum distance between the shorter string and substrings of the longer string
### Examples
```julia-repl
julia> s1 = "New York Mets vs Atlanta Braves"
julia> s2 = "Atlanta Braves vs New York Mets"
julia> evaluate(Partial(RatcliffObershelp()), s1, s2)
0.5483870967741935
```
"""
struct Partial{S <: SemiMetric} <: SemiMetric
dist::S
Partial{S}(dist::S) where {S <: SemiMetric} = new(dist)
end
Partial(dist::SemiMetric) = Partial{typeof(normalize(dist))}(normalize(dist))
normalize(dist::Partial) = dist
function (dist::Partial)(s1, s2, max_dist = 1.0)
s1, s2 = reorder(s1, s2)
len1, len2 = length(s1), length(s2)
out = dist.dist(s1, s2, max_dist)
len1 == len2 && return out
len1 == 0 && return out
for x in qgrams(s2, len1)
curr = dist.dist(s1, x, max_dist)
out = min(out, curr)
max_dist = min(out, max_dist)
end
return out
end
2020-07-20 17:32:01 +02:00
function (dist::Partial{Normalize{RatcliffObershelp}})(s1, s2, max_dist = 1.0)
s1, s2 = reorder(s1, s2)
len1, len2 = length(s1), length(s2)
len1 == len2 && return dist.dist(s1, s2)
out = 1.0
for r in matching_blocks(s1, s2)
# Make sure the substring of s2 has length len1
s2_start = r[2] - r[1] + 1
s2_end = s2_start + len1 - 1
if s2_start < 1
s2_end += 1 - s2_start
s2_start += 1 - s2_start
elseif s2_end > len2
s2_start += len2 - s2_end
s2_end += len2 - s2_end
end
2020-07-20 17:32:01 +02:00
curr = dist.dist(s1, _slice(s2, s2_start - 1, s2_end))
out = min(out, curr)
end
return out
end
"""
TokenSort(dist)
Creates the `TokenSort{dist}` distance.
`TokenSort{dist}` normalizes the string distance `dist` and modify it to adjust for differences
in word orders by reording words alphabetically.
### Examples
```julia-repl
julia> s1 = "New York Mets vs Atlanta Braves"
julia> s1 = "New York Mets vs Atlanta Braves"
julia> s2 = "Atlanta Braves vs New York Mets"
julia> evaluate(TokenSort(RatcliffObershelp()), s1, s2)
0.0
```
"""
struct TokenSort{S <: SemiMetric} <: SemiMetric
dist::S
TokenSort{S}(dist::S) where {S <: SemiMetric} = new(dist)
end
TokenSort(dist::SemiMetric) = TokenSort{typeof(normalize(dist))}(normalize(dist))
normalize(dist::TokenSort) = dist
# http://chairnerd.seatgeek.com/fuzzywuzzy-fuzzy-string-matching-in-python/
function (dist::TokenSort)(s1::AbstractString, s2::AbstractString, max_dist = 1.0)
s1 = join(sort!(split(s1)), " ")
s2 = join(sort!(split(s2)), " ")
out = dist.dist(s1, s2, max_dist)
end
"""
TokenSet(dist)
Creates the `TokenSet{dist}` distance.
`TokenSet{dist}` normalizes the string distance `dist` and modify it to adjust for differences
in word orders and word numbers by comparing the intersection of two strings with each string.
### Examples
```julia-repl
julia> s1 = "New York Mets vs Atlanta"
julia> s2 = "Atlanta Braves vs New York Mets"
julia> evaluate(TokenSet(RatcliffObershelp()), s1, s2)
0.0
```
"""
struct TokenSet{S <: SemiMetric} <: SemiMetric
dist::S
TokenSet{S}(dist::S) where {S <: SemiMetric} = new(dist)
end
TokenSet(dist::SemiMetric) = TokenSet{typeof(normalize(dist))}(normalize(dist))
normalize(dist::TokenSet) = dist
# http://chairnerd.seatgeek.com/fuzzywuzzy-fuzzy-string-matching-in-python/
function (dist::TokenSet)(s1::AbstractString, s2::AbstractString, max_dist = 1.0)
v1 = unique!(sort!(split(s1)))
v2 = unique!(sort!(split(s2)))
v0 = intersect(v1, v2)
s0 = join(v0, " ")
s1 = join(v1, " ")
s2 = join(v2, " ")
isempty(s0) && return dist.dist(s1, s2, max_dist)
score_01 = dist.dist(s0, s1, max_dist)
max_dist = min(max_dist, score_01)
score_02 = dist.dist(s0, s2, max_dist)
max_dist = min(max_dist, score_02)
score_12 = dist.dist(s1, s2, max_dist)
min(score_01, score_02, score_12)
end
2019-08-18 01:45:31 +02:00
"""
2020-02-08 18:00:44 +01:00
TokenMax(dist)
2019-08-18 01:45:31 +02:00
2019-12-13 16:33:06 +01:00
Creates the `TokenMax{dist}` distance
2020-07-20 17:46:42 +02:00
`TokenMax{dist}` normalizes the distance `dist` and returns the minimum of the distance,
2019-12-13 16:33:06 +01:00
its [`Partial`](@ref) modifier, its [`TokenSort`](@ref) modifier, and its
[`TokenSet`](@ref) modifier, with penalty terms depending on string lengths.
### Examples
```julia-repl
julia> s1 = "New York Mets vs Atlanta"
julia> s2 = "Atlanta Braves vs New York Mets"
2020-02-09 19:41:47 +01:00
julia> evaluate(TokenMax(RatcliffObershelp()), s1, s2)
0.05
2019-12-13 16:33:06 +01:00
```
2019-08-18 01:45:31 +02:00
"""
struct TokenMax{S <: SemiMetric} <: SemiMetric
2019-12-13 16:33:06 +01:00
dist::S
TokenMax{S}(dist::S) where {S <: SemiMetric} = new(dist)
2018-05-17 17:38:55 +02:00
end
TokenMax(dist::SemiMetric) = TokenMax{typeof(normalize(dist))}(normalize(dist))
2020-07-13 20:40:51 +02:00
normalize(dist::TokenMax) = dist
2020-02-12 15:41:46 +01:00
function (dist::TokenMax)(s1::AbstractString, s2::AbstractString, max_dist = 1.0)
2019-08-19 19:54:38 +02:00
s1, s2 = reorder(s1, s2)
2019-08-19 19:33:33 +02:00
len1, len2 = length(s1), length(s2)
2020-02-12 15:41:46 +01:00
score = dist.dist(s1, s2, max_dist)
min_score = min(max_dist, score)
2018-05-17 17:38:55 +02:00
unbase_scale = 0.95
2019-08-18 18:52:37 +02:00
# if one string is much shorter than the other, use partial
2019-08-19 19:33:33 +02:00
if length(s2) >= 1.5 * length(s1)
2020-02-24 15:41:38 +01:00
partial_dist = Partial(dist.dist)
2019-08-19 19:33:33 +02:00
partial_scale = length(s2) > (8 * length(s1)) ? 0.6 : 0.9
2020-02-24 15:41:38 +01:00
score_partial = 1 - partial_scale * (1 - partial_dist(s1, s2, 1 - (1 - max_dist) / partial_scale))
min_score = min(max_dist, score_partial)
score_sort = 1 - unbase_scale * partial_scale *
2020-02-24 15:41:38 +01:00
(1 - TokenSort(partial_dist)(s1, s2, 1 - (1 - max_dist) / (unbase_scale * partial_scale)))
max_dist = min(max_dist, score_sort)
score_set = 1 - unbase_scale * partial_scale *
2020-02-24 15:41:38 +01:00
(1 - TokenSet(partial_dist)(s1, s2, 1 - (1 - max_dist) / (unbase_scale * partial_scale)))
2020-07-19 21:37:49 +02:00
out = min(score, score_partial, score_sort, score_set)
2018-05-17 17:38:55 +02:00
else
score_sort = 1 - unbase_scale *
2020-02-12 15:41:46 +01:00
(1 - TokenSort(dist.dist)(s1, s2, 1 - (1 - max_dist) / unbase_scale))
max_dist = min(max_dist, score_sort)
score_set = 1 - unbase_scale *
2020-02-12 15:41:46 +01:00
(1 - TokenSet(dist.dist)(s1, s2, 1 - (1 - max_dist) / unbase_scale))
2020-07-19 21:37:49 +02:00
out = min(score, score_sort, score_set)
2018-05-17 17:38:55 +02:00
end
2020-07-19 21:37:49 +02:00
out > max_dist ? 1.0 : out
2020-07-20 17:46:42 +02:00
end
"""
Winkler(dist; p::Real = 0.1, threshold::Real = 0.7, maxlength::Integer = 4)
Creates the `Winkler{dist, p, threshold, maxlength}` distance.
`Winkler{dist, p, threshold, length)` normalizes the string distance `dist` and modify it to decrease the
distance between two strings, when their original distance is below some `threshold`.
The boost is equal to `min(l, maxlength) * p * dist` where `l` denotes the
length of their common prefix and `dist` denotes the original distance
"""
struct Winkler{S <: SemiMetric} <: SemiMetric
dist::S
p::Float64 # scaling factor. Default to 0.1
threshold::Float64 # boost threshold. Default to 0.7
maxlength::Integer # max length of common prefix. Default to 4
Winkler{S}(dist::S, p, threshold, maxlength) where {S <: SemiMetric} = new(dist, p, threshold, maxlength)
end
function Winkler(dist::SemiMetric; p = 0.1, threshold = 0.7, maxlength = 4)
p * maxlength <= 1 || throw("scaling factor times maxlength of common prefix must be lower than one")
dist = normalize(dist)
Winkler{typeof(dist)}(dist, 0.1, 0.7, 4)
end
normalize(dist::Winkler) = dist
function (dist::Winkler)(s1, s2, max_dist = 1.0)
# cannot do max_dist because of boosting threshold
out = dist.dist(s1, s2)
if out <= 1 - dist.threshold
l = common_prefix(s1, s2)[1]
out -= min(l, dist.maxlength) * dist.p * out
end
out > max_dist ? 1.0 : out
2020-10-03 18:42:09 +02:00
end