You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
scikit-learn's LocalOutlierFactor requires n_neighbors < n_samples. For len(claims) == 2, n_neighbors is 2 and fit raises ValueError (Expected n_neighbors <= n_samples_fit, but n_neighbors = 2, n_samples_fit = 2). Only len(claims) == 1 is special-cased earlier, so a two-claim batch — a realistic batch for a small campaign — crashes the endpoint.
Separately, the feature matrix mixes incomparable scales:
returnnp.column_stack([
ip_enc.transform(ips), # label-encoded integer category codeshash_enc.transform(hashes),
loc_enc.transform(locs),
amounts, # raw float token amounts (up to 1e38)
]).astype(float)
LabelEncoder assigns arbitrary integers to distinct IPs/hashes/locations, so Euclidean distance in the LOF neighbourhood is dominated by whichever column has the largest code range and by raw amount magnitude, not by any meaningful similarity. Finally, _OUTLIER_THRESHOLD = -1.5 compares the raw negative_outlier_factor_ to a constant that has no calibration against this data, so is_flagged can flip with the batch composition.
Consequence: the fraud-risk signal that downstream flows use to flag claims either throws a 500 (two-claim batch) or produces scores driven by arbitrary category codes and magnitude rather than by anomalous behaviour. Flagging can be all-or-nothing depending on how many claims happen to be in the batch, which is a correctness problem for a disbursement-gating signal.
Root cause
The scorer was built around LOF defaults without a per-batch neighbour guard, without feature normalization/encoding design, and with a hardcoded raw-score threshold.
Why this is architecturally hard
This is a scoring-pipeline design, not a parameter tweak. The fix must choose a feature representation (one-hot / target encoding / distance-appropriate encoding for IP-hash-location, and normalization or a per-feature distance metric for amount), not just clamp n_neighbors.
Small-batch behaviour must be defined. For 2–3 claims, LOF is either undefined or degenerate; the pipeline must specify a fallback (e.g. a rule-based or threshold-on-features path) and return a defined score instead of crashing.
The threshold must be calibrated and versioned. A defensible is_flagged needs a documented calibration (percentile, contamination, or an explicit rule) and a model_version so that score meaning does not drift silently; closed issue Add a model_version field to all AI responses #272 already requires a model_version field on AI responses, which this must populate.
It is a trust boundary with no regression oracle. There is no golden set of known-fraudulent claims, so tests must at minimum pin: batch sizes 1, 2, 3 do not crash, and a constructed outlier is flagged while a homogeneous batch is not.
Proposed design
Guard n_neighbors to < n_samples (and define a fallback for tiny batches), encode categoricals with a distance-meaningful scheme and normalize/normalize-or-scale amount, and replace the raw -1.5 threshold with a calibrated, documented decision rule. Surface the chosen model_version in the response.
Acceptance criteria
Service
detect_fraud returns a result (never raises) for batch sizes 1, 2, and 3.
A constructed outlier in a batch is flagged, and a homogeneous batch is not, independent of the magnitude of the amount column.
The response includes a model_version consistent with the scoring pipeline version.
Tests
Tests pin the no-crash behaviour for sizes 1/2/3 and the outlier-flagging behaviour.
Out of scope
Proof-of-life liveness policy and OCR degradation handling are separate issues.
Problem
detect_fraudis numerically unsound and crashes on a small, legitimate batch size. Inapp/ai-service/services/fraud_detection.py:scikit-learn's
LocalOutlierFactorrequiresn_neighbors < n_samples. Forlen(claims) == 2,n_neighborsis2andfitraisesValueError(Expected n_neighbors <= n_samples_fit, but n_neighbors = 2, n_samples_fit = 2). Onlylen(claims) == 1is special-cased earlier, so a two-claim batch — a realistic batch for a small campaign — crashes the endpoint.Separately, the feature matrix mixes incomparable scales:
LabelEncoderassigns arbitrary integers to distinct IPs/hashes/locations, so Euclidean distance in the LOF neighbourhood is dominated by whichever column has the largest code range and by rawamountmagnitude, not by any meaningful similarity. Finally,_OUTLIER_THRESHOLD = -1.5compares the rawnegative_outlier_factor_to a constant that has no calibration against this data, sois_flaggedcan flip with the batch composition.Consequence: the fraud-risk signal that downstream flows use to flag claims either throws a 500 (two-claim batch) or produces scores driven by arbitrary category codes and magnitude rather than by anomalous behaviour. Flagging can be all-or-nothing depending on how many claims happen to be in the batch, which is a correctness problem for a disbursement-gating signal.
Root cause
The scorer was built around LOF defaults without a per-batch neighbour guard, without feature normalization/encoding design, and with a hardcoded raw-score threshold.
Why this is architecturally hard
amount), not just clampn_neighbors.is_flaggedneeds a documented calibration (percentile, contamination, or an explicit rule) and amodel_versionso that score meaning does not drift silently; closed issue Add amodel_versionfield to all AI responses #272 already requires amodel_versionfield on AI responses, which this must populate.Proposed design
Guard
n_neighborsto< n_samples(and define a fallback for tiny batches), encode categoricals with a distance-meaningful scheme and normalize/normalize-or-scaleamount, and replace the raw-1.5threshold with a calibrated, documented decision rule. Surface the chosenmodel_versionin the response.Acceptance criteria
Service
detect_fraudreturns a result (never raises) for batch sizes 1, 2, and 3.amountcolumn.model_versionconsistent with the scoring pipeline version.Tests
Out of scope
Proof-of-life liveness policy and OCR degradation handling are separate issues.
Getting started
Files:
app/ai-service/services/fraud_detection.py,app/ai-service/schemas/fraud.py,app/ai-service/tests/.cd app/ai-service pytestGood first files to read:
services/fraud_detection.py(_vectorize/detect_fraud) andschemas/fraud.pyfor the response shape.