This system detects fraudulent and suspicious copay card claims in GSK pharmaceutical transaction data using a 4-level hierarchical analytical framework:
Level
What It Detects
Key Features
Transaction
Per-claim anomalies
Gap between fills, quantity, days supply, benefit amount, OOP cost, NDC switch
Patient
Behavioral patterns
One-and-done patients, active duration, avg gap between fills, short/long gap %
HCP
Prescriber-driven fraud
Suspicious specialty, one-and-done %, patient concentration, avg benefit per patient
35 hard-coded business rules (23 original + 12 v4 group-aware rules)
Isolation Forest unsupervised anomaly detection trained on rule-clean data
SHAP explainability for every flagged claim
Hierarchical summary exports for investigative lens views
Exploratory Data Analysis (EDA) module for pre-modeling data profiling
The pipeline supports any vendor format — ELAAD, APLD, IQVIA, CMS DMR, generic CSV. It auto-discovers column names via synonym mapping, handles missing columns gracefully, and produces a schema report showing what it found and what it missed.
1# Generate test data2python generate_elaad_test_data.py
3# Creates data/elaad_test_trelegy.csv with embedded fraud patterns45# Run pipeline on synthetic data (high contamination needed because ~40% fraud)6python run_all_v3.py \7 --data-path data/elaad_test_trelegy.csv \8 --file-type csv \9 --contamination 0.40
EDA Module (eda.py)
The EDA module runs before the fraud detection pipeline to provide data quality assessment, trend analysis, and distribution profiling. It is critical for:
Identifying data quality issues before modeling
Understanding seasonal/temporal fraud patterns
Validating financial distributions against expectations
Vendor files rarely match the idealised spec. A column named IQVIA_PATIENT_ID in the spec might appear as:
PATIENT_ID (ELAAD format)
MEMBER_ID (APLD format)
PATIENTID (no underscore)
PAT ID (space instead of underscore)
Patient Id (mixed case)
PATIENT_KEY (different suffix)
The Solution: Schema Discovery
The pipeline uses COLUMN_SYNONYMS in config.py — a dictionary where each internal column name maps to a list of possible raw names (20+ synonyms per column). When a file is loaded:
Scan header → collect all raw column names
Normalize → uppercase, strip whitespace, replace underscores/spaces with single space
Match → for each internal column, try synonyms in order of preference
Report → log what was mapped and what was missed
Continue → pipeline runs with whatever columns are available
See config.py::COLUMN_SYNONYMS for the full list of 100+ synonyms.
35 Business Rules
Original 23 Rules (v3)
#
Rule
Condition
Fraud Signal
1
Early Refill
days_between_fills < 23
Early Refill Abuse
2
Impossible Qty
quantity != 1
Data Error / Fraud
3
Wrong Days Supply
days_supply != 30
Data Error / Fraud
4
Govt Insurance
insurance_type == Government
Program Violation
5
Underage
patient_age < 18
Program Violation
6
Duplicate
Same patient + date + pharmacy
Duplicate Billing
7
NDC Switch
patient_ndc_count > 1
Strength Switching
8
Suspicious Specialty
Prescriber not in valid list
Prescriber Collusion
9
Multi-Program
unique_programs_per_patient > 1
Card Stacking
10
Excessive Fills (90d)
patient_fill_count_90d > 4
Stockpiling
11
High-Risk Reject
reject_code in {76, 88, 79}
Maximizer / DUR
12
Maximizer Cap
maximizer_reject == 1
Benefit Exhaustion
13
Paper Submission
paper_submission == 1
Submission Fraud
14
Plan Switch
plan_switch_flag == 1
Plan Switching
15
Linked Claim
has_linked_claim == 1
Reversal / Adjustment
16
HCP High Benefit
hcp_avg_benefit_per_patient > 500
Prescriber-driven extraction
17
HCP One-Done Concentration
hcp_one_and_done_pct > 0.6
HCP with hit-and-run patients
18
Pharmacy Fraud Risk
pharmacy_fraud_risk_score > 0.6
Composite pharmacy ring score
19
Pharmacy HCP Concentration
pharmacy_hcp_concentration > 0.5
Single HCP dominates pharmacy
20
Pharmacy One-Done
pharmacy_one_and_done_pct > 0.5
Pharmacy with churn-and-burn
21
Short Active Burst
patient_active_duration <= 14 AND total_fills > 1
Quick-fire multi-fill scheme
22
Cross-State
patient_state != pharmacy_state
Out-of-state fraud
23
New Patient Burst
days_since_first <= 7 AND total_fills > 1
Same-week multiple fills
v4 Group-Aware Rules (NEW)
#
Rule
Condition
Fraud Signal
24
Scenario Not Covered
scenario_not_covered_flag == 1
Cash/Rejected under Group 8200
25
Benefit Cap Exceeded
excess_payment_amount > 0
Benefit > allowed cap for group
26
Invalid Period Benefit
invalid_period_benefit_flag == 1
$500 cap used outside Jan-Mar 2024
27
Annual Fill Limit
annual_fill_count > 12
Exceeds 12 fills/year
28
Annual DS Limit
annual_days_supply_count > 360
Exceeds 360 days supply/year
29
Non-Covered NDC
non_covered_ndc_flag == 1
NDC not in covered list
30
Govt With Benefit
govt_claim_with_benefit_flag == 1
Govt plan receiving benefit
31
Quantity Out of Range
quantity < 1 or quantity > 3
Impossible quantity
32
Days Supply Out of Range
days_supply < 1 or days_supply > 90
Invalid supply duration
33
Max Benefit Repeat
Patient+pharmacy hits cap ≥3 times
Organized cap-maximization ring
34
High Cap Utilization
cap_utilization_ratio > 1.0
Paid more than 100% of cap
35
Group Benefit Mismatch
group_benefit_mismatch_flag == 1
Any group+scenario+period mismatch
Rules whose source columns are missing are silently skipped (count = 0).
Model: Isolation Forest
python
1IsolationForest(2 n_estimators=200,3 contamination=0.03,# Adjustable via CLI (use 0.40 for synthetic test data)4 max_samples="auto",5 max_features=1.0,6 bootstrap=False,7 random_state=42,8 n_jobs=-1,9)
Training strategy: Train ONLY on claims where rule_flag == 0 (rule-clean). Score ALL claims.
Degraded mode: If zero rule-clean claims exist (e.g., synthetic data with 40% fraud), the model trains on ALL claims with contamination=min(orig×3, 0.5). This is a safety fallback — real production data will always have rule-clean claims.