Class: GitFit::Dedup::Matcher

Inherits:
Object
  • Object
show all
Defined in:
lib/git_fit/dedup/matcher.rb

Constant Summary collapse

WEIGHTS =
{ time: 0.35, distance: 0.35, duration: 0.15, category: 0.15 }.freeze
TRAJECTORY_WEIGHTS =
{
  dtw_similarity: 0.30,
  overlap_score: 0.25,
  hausdorff_score: 0.25,
  distance_match: 0.20,
}.freeze

Class Method Summary collapse

Class Method Details

.distance_score(d1, d2) ⇒ Object



63
64
65
66
67
68
69
# File 'lib/git_fit/dedup/matcher.rb', line 63

def self.distance_score(d1, d2)
  max = [d1, d2].max
  return nil if max <= 0
  diff = (d1 - d2).abs
  return nil if diff > max * 0.15
  1.0 - diff / (max * 0.15)
end

.duration_score(d1, d2) ⇒ Object



71
72
73
74
75
76
77
# File 'lib/git_fit/dedup/matcher.rb', line 71

def self.duration_score(d1, d2)
  max = [d1, d2].max
  return 0.0 if max <= 0
  diff = (d1 - d2).abs
  return 0.0 if diff > max * 0.15
  1.0 - diff / (max * 0.15)
end

.effective_duration(act) ⇒ Object



16
17
18
# File 'lib/git_fit/dedup/matcher.rb', line 16

def self.effective_duration(act)
  [act[:moving_time].to_f, act[:elapsed_time].to_f].max
end

.external_id_match(a, b) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
# File 'lib/git_fit/dedup/matcher.rb', line 20

def self.external_id_match(a, b)
  a_eid = a[:external_id]
  b_eid = b[:external_id]
  return nil if a_eid.nil? || b_eid.nil?
  return nil if a_eid.empty? || b_eid.empty?

  a_normalized = a_eid.sub(%r{^strava://activities/}, '')
  b_normalized = b_eid.sub(%r{^strava://activities/}, '')

  { confidence: 1.0, flag: 'derived', source: 'external_id' } if a_normalized == b_normalized
end

.match(a, b) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/git_fit/dedup/matcher.rb', line 32

def self.match(a, b)
  exact = external_id_match(a, b)
  return exact[:confidence] if exact

  t_diff = time_diff_abs(a[:start_date], b[:start_date])
  max_diff = [300, (a[:elapsed_time] || 0).to_i, (b[:elapsed_time] || 0).to_i].max
  return 0.0 if t_diff > max_diff

  t_score = 1.0 - t_diff / max_diff.to_f
  d_score = distance_score(a[:distance].to_f, b[:distance].to_f)
  return 0.0 unless d_score

  dur_score = duration_score(effective_duration(a), effective_duration(b))
  cat_score = a[:sport_category] == b[:sport_category] ? 1.0 : -0.67

  result = t_score * WEIGHTS[:time] +
           d_score * WEIGHTS[:distance] +
           dur_score * WEIGHTS[:duration] +
           cat_score * WEIGHTS[:category]
  result.round(4)
end

.time_diff_abs(t1, t2) ⇒ Object



54
55
56
57
58
59
60
61
# File 'lib/git_fit/dedup/matcher.rb', line 54

def self.time_diff_abs(t1, t2)
  t1 = t1.to_s
  t2 = t2.to_s
  return 9999 if t1.empty? || t2.empty?
  (Time.parse(t1) - Time.parse(t2)).abs
rescue StandardError
  9999
end

.trajectory_score(scores) ⇒ Object



79
80
81
82
83
84
85
86
87
# File 'lib/git_fit/dedup/matcher.rb', line 79

def self.trajectory_score(scores)
  return 0.0 if scores.nil? || scores.empty?
  weighted = TRAJECTORY_WEIGHTS.sum do |metric, weight|
    score = scores[metric]
    next 0.0 unless score.is_a?(Numeric) && score >= 0.0 && score <= 1.0
    score * weight
  end
  weighted.round(4)
end