Class: ActiveSanction::Scorer::Result
- Inherits:
-
Object
- Object
- ActiveSanction::Scorer::Result
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/scorer/result.rb
Overview
One entity scored against one subject, and the account of how.
result = ActiveSanction::Scorer.call(subject, entity)
result.score # => 87.4
result.entity.id # => "ofac_sdn:2674"
result.name.value # => "AERO-CARIBBEAN"
result.explanation.map(&:to_s)
# => ["+91.2 name: matched alias 'AERO-CARIBBEAN' (aka)",
# "+4.0 dob: date of birth 1948 overlaps listed 1948-12-10",
# "-8.0 nationality: query RU vs listed EG"]
The score is the explanation
score is not stored beside the reasons, it is the sum of them, rounded
once. There is no arithmetic anywhere in this library that can move one
without the other, which is the point: a compliance officer has to
answer "why did this score 87?" to an examiner, and a number that merely
travels alongside a list of reasons is a number that can come apart from
them in a later release and be wrong quietly for a year.
So the explanation is never empty -- the blended name similarity is always the first reason, even when nothing else was known -- and it always adds up.
name is the specific spelling that produced the score
An entity's score is the best of its names, and this is the one that won. It matters more than it looks: OFAC ships more aliases than primary names, so the answer to "what did we match?" is usually an alias, and a report that quoted the primary name instead would be describing a comparison that never happened.
form is that name folded, which is the string the scorers actually
compared. Both are here for the reason Normalizer::Form carries both:
the published spelling is what a person reads and the folded one is what
a person checks.
What this is not
It is not MatchResult (#33). This carries what the scorer knows -- a
score, a name, an entity, an explanation -- and nothing about the
screening run that produced it. The snapshot checksum, the matcher
version, the thresholds and the backend all belong to the public API
above this one, which is where an audit record is assembled.
Instances are frozen on construction and compare by value.
Instance Attribute Summary collapse
- #entity ⇒ Entity readonly
-
#explanation ⇒ Array<Reason>
readonly
Never empty.
-
#form ⇒ Normalizer::Form
readonly
That name folded -- the string the comparison ran on.
-
#name ⇒ Name
readonly
The name that scored highest, as its publisher wrote it.
-
#score ⇒ Float
readonly
0..100, one decimal place, and equal to the sum of the explanation.
Instance Method Summary collapse
- #==(other) ⇒ Boolean (also: #eql?)
- #hash ⇒ Integer
- #initialize(entity:, name:, form:, explanation:) ⇒ void constructor
- #inspect ⇒ String
-
#penalties ⇒ Array<Reason>
The reasons that lowered the score, which is the half of an explanation a reviewer clearing an alert reads first.
-
#source ⇒ Symbol
The list this entity came from, which every hit has to name.
- #to_h ⇒ Hash{Symbol => T.untyped}
Constructor Details
#initialize(entity:, name:, form:, explanation:) ⇒ void
83 84 85 86 87 88 89 90 91 92 |
# File 'lib/active_sanction/scorer/result.rb', line 83 def initialize(entity:, name:, form:, explanation:) raise InvalidArgument, "a result needs at least one reason" if explanation.empty? @entity = entity @name = name @form = form @explanation = T.let(explanation.dup.freeze, T::Array[Reason]) @score = T.let(explanation.sum(&:contribution).round(Reason::PRECISION).to_f, Float) freeze end |
Instance Attribute Details
#entity ⇒ Entity (readonly)
61 62 63 |
# File 'lib/active_sanction/scorer/result.rb', line 61 def entity @entity end |
#explanation ⇒ Array<Reason> (readonly)
Never empty. See the class comment.
77 78 79 |
# File 'lib/active_sanction/scorer/result.rb', line 77 def explanation @explanation end |
#form ⇒ Normalizer::Form (readonly)
That name folded -- the string the comparison ran on.
69 70 71 |
# File 'lib/active_sanction/scorer/result.rb', line 69 def form @form end |
#name ⇒ Name (readonly)
The name that scored highest, as its publisher wrote it.
65 66 67 |
# File 'lib/active_sanction/scorer/result.rb', line 65 def name @name end |
#score ⇒ Float (readonly)
0..100, one decimal place, and equal to the sum of the explanation.
73 74 75 |
# File 'lib/active_sanction/scorer/result.rb', line 73 def score @score end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
115 116 117 118 119 |
# File 'lib/active_sanction/scorer/result.rb', line 115 def ==(other) return false unless other.instance_of?(self.class) entity == other.entity && name == other.name && explanation == other.explanation end |
#hash ⇒ Integer
123 |
# File 'lib/active_sanction/scorer/result.rb', line 123 def hash = [self.class, entity, name, explanation].hash |
#inspect ⇒ String
126 |
# File 'lib/active_sanction/scorer/result.rb', line 126 def inspect = "#<#{self.class} #{score} #{name.value.inspect} (#{entity.id})>" |
#penalties ⇒ Array<Reason>
The reasons that lowered the score, which is the half of an explanation a reviewer clearing an alert reads first.
101 |
# File 'lib/active_sanction/scorer/result.rb', line 101 def penalties = explanation.select(&:penalty?) |
#source ⇒ Symbol
The list this entity came from, which every hit has to name.
96 |
# File 'lib/active_sanction/scorer/result.rb', line 96 def source = entity.source |
#to_h ⇒ Hash{Symbol => T.untyped}
104 105 106 107 108 109 110 111 112 |
# File 'lib/active_sanction/scorer/result.rb', line 104 def to_h { score: score, entity_id: entity.id, source: source, name: name.to_h, explanation: explanation.map(&:to_h) } end |