From bf10f2279137f1ad59bb1257b5e9610a20dd8004 Mon Sep 17 00:00:00 2001 From: Samarth Uday Date: Sat, 29 Aug 2026 01:34:39 +0530 Subject: [PATCH] fix: Remove dead Flask-era code, fix lint errors, add docs/assets fallback An external review of main flagged several potential integration issues. Verified against the actual codebase: temporal_split, fit_model(config=...), exact top-K alert ranking, currency-aware behavioral windows, and the FastAPI/model feature schema were already consistent (test_leakage.py already covers top-K-on-ties and currency-aware windows). The following were real issues and are fixed here: **Dead code removal:** - Deleted src/api/app.py (orphaned Flask app, unreachable, not imported anywhere), src/api/schemas.py (Marshmallow, Flask-only), src/api/openapi.yaml (hand-written spec superseded by FastAPI's auto-generated schema), src/api/templates/ (Flask Jinja template superseded by public/index.html), and src/models/inference.py (duplicate of src/api/inference.py, only consumer was the deleted app.py) - Removed dead threshold_for_alert_rate() from metrics.py (quantile-based, unused everywhere -- top_k_alert_mask is the only path actually used) **Lint (ruff check was failing with 23 errors -- CI would have failed):** - Fixed import ordering and removed unused imports across affected files - Removed f-strings without placeholders in train_model.py **Dashboard resilience:** - results_loader.py now falls back to the committed docs/assets/ snapshot when reports/*.csv or reports/figures/*.png don't exist locally, so a fresh clone shows real published results without retraining first (verified by simulating a clean clone with no local reports/ output) **Docs:** - Fixed stale "Flask" mention in the tech-stack line (README already documented FastAPI correctly everywhere else, and already embedded all 6 result charts) All 60 tests pass, ruff check is clean. Co-Authored-By: Claude Sonnet 5 --- README.md | 2 +- scripts/generate_report_figures.py | 4 +- scripts/train_model.py | 4 +- src/api/app.py | 113 -------- src/api/main.py | 2 +- src/api/models.py | 3 +- src/api/openapi.yaml | 431 ----------------------------- src/api/results_loader.py | 42 ++- src/api/schemas.py | 70 ----- src/api/templates/dashboard.html | 425 ---------------------------- src/config.py | 2 +- src/data/validation.py | 3 +- src/evaluation/metrics.py | 20 -- src/models/inference.py | 56 ---- src/monitoring/drift.py | 2 +- tests/test_api.py | 3 +- tests/test_config.py | 3 +- tests/test_data_validation.py | 4 +- tests/test_features.py | 5 +- tests/test_inference.py | 3 +- 20 files changed, 48 insertions(+), 1149 deletions(-) delete mode 100644 src/api/app.py delete mode 100644 src/api/openapi.yaml delete mode 100644 src/api/schemas.py delete mode 100644 src/api/templates/dashboard.html delete mode 100644 src/models/inference.py diff --git a/README.md b/README.md index 306b174..38846bc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ### A research-oriented framework for rare-event financial transaction surveillance using temporal, behavioral, and network signals. -**Python · XGBoost · scikit-learn · DuckDB · SHAP · Flask** +**Python · XGBoost · scikit-learn · DuckDB · SHAP · FastAPI** diff --git a/scripts/generate_report_figures.py b/scripts/generate_report_figures.py index e1108e3..da4c692 100644 --- a/scripts/generate_report_figures.py +++ b/scripts/generate_report_figures.py @@ -6,15 +6,13 @@ import joblib import matplotlib.pyplot as plt -import numpy as np import pandas as pd -from sklearn.metrics import roc_curve, precision_recall_curve, auc +from sklearn.metrics import auc, precision_recall_curve, roc_curve PROJECT_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(PROJECT_ROOT)) from src.logging_config import setup_logging -from src.models.calibration import ProbabilityCalibrator from src.models.train import MODEL_FEATURES, TARGET, temporal_split logger = setup_logging(__name__) diff --git a/scripts/train_model.py b/scripts/train_model.py index aad4d1a..43a93bd 100644 --- a/scripts/train_model.py +++ b/scripts/train_model.py @@ -307,9 +307,9 @@ def main(): logger.info(f"Saving model artifact to {args.artifact}...") joblib.dump(artifact, args.artifact) - logger.info(f"Model saved successfully") + logger.info("Model saved successfully") - logger.info(f"Writing metrics report...") + logger.info("Writing metrics report...") report_path = PROJECT_ROOT / "reports/model_metrics.json" report_path.parent.mkdir(parents=True, exist_ok=True) report = { diff --git a/src/api/app.py b/src/api/app.py deleted file mode 100644 index ef3e0fd..0000000 --- a/src/api/app.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Artifact-backed API for feature-store supplied transaction features. - -This API intentionally does not calculate behavioural history from a single -raw transaction. That responsibility belongs to an online feature store. -""" - -from __future__ import annotations - -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -import joblib -from flask import Flask, jsonify, request, render_template -from flask_cors import CORS - -from marshmallow import ValidationError - -from src.api.schemas import validate_feature_vector -from src.models.inference import ( - model_input_from_features, - predict_calibrated_probability, - probability_percentile, -) - -PROJECT_ROOT = Path(__file__).resolve().parents[2] -MODEL_PATH = PROJECT_ROOT / "artifacts/risk_model.joblib" -TEMPLATE_DIR = Path(__file__).resolve().parent / "templates" - - -def create_app(model_path: Path = MODEL_PATH) -> Flask: - app = Flask(__name__, template_folder=str(TEMPLATE_DIR)) - CORS(app) - - artifact: dict[str, Any] | None = None - load_error: str | None = None - try: - artifact = joblib.load(model_path) - except FileNotFoundError: - load_error = f"Model artifact not found at {model_path}. Train the offline model first." - except Exception as error: # pragma: no cover - defensive startup path - load_error = f"Unable to load model artifact: {error}" - - @app.get("/") - def dashboard(): - return render_template("dashboard.html") - - @app.get("/api/health") - def health(): - return jsonify( - { - "status": "healthy" if artifact else "model_unavailable", - "model_loaded": artifact is not None, - "timestamp": datetime.now(timezone.utc).isoformat(), - "detail": load_error, - } - ) - - @app.get("/api/model/info") - def model_info(): - if artifact is None: - return jsonify({"error": load_error}), 503 - return jsonify( - { - "model": "XGBoost", - "model_version": artifact["model_version"], - "alert_rate": artifact["alert_rate"], - "decision_threshold": artifact["decision_threshold"], - "test_metrics": artifact["test_metrics"], - } - ) - - @app.post("/api/predict") - def predict(): - if artifact is None: - return jsonify({"error": load_error}), 503 - - try: - payload = request.get_json(force=True) - except Exception as error: - return jsonify({"error": f"Invalid JSON: {str(error)}"}), 400 - - if not isinstance(payload, dict): - return jsonify({"error": "Request body must be a JSON object."}), 400 - - try: - validated_features = validate_feature_vector(payload) - except ValidationError as error: - return jsonify({"error": error.messages}), 400 - - try: - features = model_input_from_features(validated_features, artifact["features"]) - probability = predict_calibrated_probability(artifact, features) - except ValueError as error: - return jsonify({"error": str(error)}), 400 - - result: dict[str, Any] = { - "risk_probability": probability, - "requires_review": probability >= artifact["decision_threshold"], - } - percentile = probability_percentile(artifact, probability) - if percentile is not None: - result["risk_percentile"] = percentile - return jsonify(result) - - return app - - -app = create_app() - - -if __name__ == "__main__": - app.run(host="0.0.0.0", port=5000, debug=False) diff --git a/src/api/main.py b/src/api/main.py index 0b8b851..c27b5a3 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -22,8 +22,8 @@ ) from src.api.results_loader import ( get_ablation_results, - get_typology_results, get_all_figures, + get_typology_results, ) PROJECT_ROOT = Path(__file__).resolve().parents[2] diff --git a/src/api/models.py b/src/api/models.py index 266bba8..46d2fbb 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,6 +1,7 @@ """Pydantic models for API request/response validation.""" -from typing import Optional, Dict, Any +from typing import Any, Dict, Optional + from pydantic import BaseModel, Field diff --git a/src/api/openapi.yaml b/src/api/openapi.yaml deleted file mode 100644 index 4d5395a..0000000 --- a/src/api/openapi.yaml +++ /dev/null @@ -1,431 +0,0 @@ -openapi: 3.0.0 -info: - title: Transaction Risk Prediction API - description: | - Inference API for predicting transaction risk using XGBoost model. - - **Note:** This API accepts pre-computed feature vectors only. Raw account identifiers - and behavioral history computation are outside scope—that responsibility belongs to an - online feature store. - version: "2.1.0" - contact: - name: Project Repository - url: https://github.com/Samarthuday/Quantitative-Transaction-Risk-Modeling - -servers: - - url: http://localhost:5000 - description: Local development - - url: https://api.example.com - description: Production - -paths: - /api/health: - get: - summary: Service health check - description: | - Returns the current health status of the service and model availability. - operationId: getHealth - tags: - - Health - responses: - '200': - description: Service is healthy - content: - application/json: - schema: - $ref: '#/components/schemas/HealthResponse' - '503': - description: Service unavailable or model failed to load - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /api/model/info: - get: - summary: Model metadata and evaluation metrics - description: | - Returns information about the trained model including version, - decision threshold, and test set performance metrics. - operationId: getModelInfo - tags: - - Model - responses: - '200': - description: Model information retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/ModelInfoResponse' - '503': - description: Model not available - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /api/predict: - post: - summary: Predict transaction risk probability - description: | - Predicts the risk probability for a transaction given its pre-computed feature vector. - - **Important:** Input must be a complete feature vector with all required fields. - Missing or invalid fields will result in a 400 error. - operationId: predict - tags: - - Prediction - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/FeatureVector' - examples: - transaction: - summary: Example feature vector - value: - Amount: 1000.0 - log_amount: 6.908 - hour_sin: 0.5 - hour_cos: 0.866 - dow_sin: 0.0 - dow_cos: 1.0 - month_sin: 0.259 - month_cos: 0.966 - is_weekend: 0 - is_night: 0 - currency_mismatch: 0 - cross_border: 0 - is_round_amount: 1 - sender_txn_count_24h: 5 - sender_amount_sum_24h: 5000.0 - sender_amount_mean_30d: 1000.0 - sender_amount_std_30d: 500.0 - sender_amount_zscore: 0.0 - receiver_txn_count_24h: 3 - receiver_amount_sum_24h: 3000.0 - seconds_since_sender_txn: 3600 - sender_txn_count_lifetime: 100 - receiver_txn_count_lifetime: 50 - sender_out_degree: 25 - receiver_in_degree: 20 - pair_transaction_count: 5 - sender_counterparty_hhi: 0.2 - Payment_type: "Transfer" - Payment_currency: "USD" - Received_currency: "USD" - Sender_bank_location: "US" - Receiver_bank_location: "US" - responses: - '200': - description: Risk prediction successful - content: - application/json: - schema: - $ref: '#/components/schemas/PredictionResponse' - '400': - description: Invalid request (missing/invalid features) - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '503': - description: Model not available - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - -components: - schemas: - HealthResponse: - type: object - required: - - status - - model_loaded - - timestamp - properties: - status: - type: string - enum: - - healthy - - model_unavailable - description: Overall service health status - model_loaded: - type: boolean - description: Whether the model artifact is available - timestamp: - type: string - format: date-time - description: Current UTC timestamp - detail: - type: string - nullable: true - description: Error details if model failed to load - - ModelInfoResponse: - type: object - required: - - model - - model_version - - alert_rate - - decision_threshold - - test_metrics - properties: - model: - type: string - example: "XGBoost" - description: Model type identifier - model_version: - type: string - example: "2.1.0" - description: Semantic version of the model - alert_rate: - type: number - format: float - example: 0.005 - description: Alert rate used for threshold selection (0.5%) - decision_threshold: - type: number - format: float - example: 0.156 - description: Calibrated probability threshold for alerts - test_metrics: - type: object - description: Model performance metrics on held-out test set - properties: - pr_auc: - type: number - format: float - description: Precision-Recall AUC - roc_auc: - type: number - format: float - description: ROC AUC - brier_score: - type: number - format: float - description: Brier score (lower is better) - log_loss: - type: number - format: float - description: Logarithmic loss - alert_0.1%_precision: - type: number - format: float - alert_0.1%_recall: - type: number - format: float - alert_0.5%_precision: - type: number - format: float - alert_0.5%_recall: - type: number - format: float - alert_1.0%_precision: - type: number - format: float - alert_1.0%_recall: - type: number - format: float - - FeatureVector: - type: object - required: - - Amount - - log_amount - - hour_sin - - hour_cos - - dow_sin - - dow_cos - - month_sin - - month_cos - - is_weekend - - is_night - - currency_mismatch - - cross_border - - is_round_amount - - sender_txn_count_24h - - sender_amount_sum_24h - - sender_amount_mean_30d - - sender_amount_std_30d - - sender_amount_zscore - - receiver_txn_count_24h - - receiver_amount_sum_24h - - seconds_since_sender_txn - - sender_txn_count_lifetime - - receiver_txn_count_lifetime - - sender_out_degree - - receiver_in_degree - - pair_transaction_count - - sender_counterparty_hhi - - Payment_type - - Payment_currency - - Received_currency - - Sender_bank_location - - Receiver_bank_location - properties: - Amount: - type: number - format: float - minimum: 0 - description: Transaction amount in base currency - log_amount: - type: number - format: float - description: Log(1 + Amount) - hour_sin: - type: number - format: float - minimum: -1 - maximum: 1 - description: Cyclical encoding of hour - hour_cos: - type: number - format: float - minimum: -1 - maximum: 1 - dow_sin: - type: number - format: float - minimum: -1 - maximum: 1 - description: Day of week cyclical encoding - dow_cos: - type: number - format: float - minimum: -1 - maximum: 1 - month_sin: - type: number - format: float - minimum: -1 - maximum: 1 - description: Month cyclical encoding - month_cos: - type: number - format: float - minimum: -1 - maximum: 1 - is_weekend: - type: integer - enum: [0, 1] - is_night: - type: integer - enum: [0, 1] - currency_mismatch: - type: integer - enum: [0, 1] - description: 1 if payment_currency != received_currency - cross_border: - type: integer - enum: [0, 1] - description: 1 if sender_location != receiver_location - is_round_amount: - type: integer - enum: [0, 1] - description: 1 if amount is divisible by 1000 - sender_txn_count_24h: - type: integer - minimum: 0 - sender_amount_sum_24h: - type: number - format: float - minimum: 0 - sender_amount_mean_30d: - type: number - format: float - nullable: true - sender_amount_std_30d: - type: number - format: float - nullable: true - sender_amount_zscore: - type: number - format: float - receiver_txn_count_24h: - type: integer - minimum: 0 - receiver_amount_sum_24h: - type: number - format: float - minimum: 0 - seconds_since_sender_txn: - type: integer - description: Time since sender's last transaction (-1 if none) - sender_txn_count_lifetime: - type: integer - minimum: 0 - receiver_txn_count_lifetime: - type: integer - minimum: 0 - sender_out_degree: - type: integer - minimum: 0 - description: Unique counterparties sender has transacted with - receiver_in_degree: - type: integer - minimum: 0 - description: Unique senders who have transacted with receiver - pair_transaction_count: - type: integer - minimum: 0 - description: Number of prior transactions between sender-receiver pair - sender_counterparty_hhi: - type: number - format: float - minimum: 0 - maximum: 1 - description: HHI concentration measure for sender's counterparties - Payment_type: - type: string - example: "Transfer" - Payment_currency: - type: string - example: "USD" - Received_currency: - type: string - example: "USD" - Sender_bank_location: - type: string - example: "US" - Receiver_bank_location: - type: string - example: "US" - - PredictionResponse: - type: object - required: - - risk_probability - - requires_review - properties: - risk_probability: - type: number - format: float - minimum: 0 - maximum: 1 - description: Calibrated risk probability (0 = low risk, 1 = high risk) - requires_review: - type: boolean - description: Whether transaction exceeds alert threshold - risk_percentile: - type: number - format: float - minimum: 0 - maximum: 100 - nullable: true - description: Percentile rank against validation set (0-100) - - ErrorResponse: - type: object - required: - - error - properties: - error: - type: string - description: Error message describing what went wrong - detail: - type: object - nullable: true - description: Additional error details (e.g., validation errors per field) diff --git a/src/api/results_loader.py b/src/api/results_loader.py index 36f77db..a41ef28 100644 --- a/src/api/results_loader.py +++ b/src/api/results_loader.py @@ -6,12 +6,27 @@ from typing import Any, Optional PROJECT_ROOT = Path(__file__).resolve().parents[2] +REPORTS_DIR = PROJECT_ROOT / "reports" +ASSETS_DIR = PROJECT_ROOT / "docs/assets" + + +def _resolve_path(relative_name: str) -> Optional[Path]: + """Prefer a freshly generated report, fall back to the committed published copy.""" + local_path = REPORTS_DIR / relative_name + if local_path.exists(): + return local_path + + fallback_path = ASSETS_DIR / relative_name + if fallback_path.exists(): + return fallback_path + + return None def load_ablation_results() -> list[dict[str, Any]]: """Load feature ablation study results from CSV.""" - ablation_path = PROJECT_ROOT / "reports/ablation_results.csv" - if not ablation_path.exists(): + ablation_path = _resolve_path("ablation_results.csv") + if ablation_path is None: return [] results = [] @@ -28,8 +43,8 @@ def load_ablation_results() -> list[dict[str, Any]]: def load_typology_results() -> list[dict[str, Any]]: """Load behavioral typology analysis results from CSV.""" - typology_path = PROJECT_ROOT / "reports/typology_results.csv" - if not typology_path.exists(): + typology_path = _resolve_path("typology_results.csv") + if typology_path is None: return [] results = [] @@ -46,12 +61,7 @@ def load_typology_results() -> list[dict[str, Any]]: def encode_all_figures() -> dict[str, str]: - """Load all report figures from reports/figures/*.png and encode to base64.""" - figures_dir = PROJECT_ROOT / "reports/figures" - if not figures_dir.exists(): - return {} - - figures = {} + """Load report figures, preferring freshly generated ones over the committed fallback.""" figure_mapping = { "calibration_curve": "Calibration Curve", "roc_curve": "ROC Curve", @@ -61,8 +71,16 @@ def encode_all_figures() -> dict[str, str]: "typology_detection": "Typology Detection", } - for png_path in sorted(figures_dir.glob("*.png")): - stem = png_path.stem + # Fallback pass first so freshly generated figures (below) take precedence. + png_paths: dict[str, Path] = {} + for directory in (ASSETS_DIR, REPORTS_DIR / "figures"): + if not directory.exists(): + continue + for png_path in directory.glob("*.png"): + png_paths[png_path.stem] = png_path + + figures = {} + for stem, png_path in sorted(png_paths.items()): with png_path.open("rb") as f: image_bytes = f.read() base64_str = base64.b64encode(image_bytes).decode("utf-8") diff --git a/src/api/schemas.py b/src/api/schemas.py deleted file mode 100644 index a14c9ba..0000000 --- a/src/api/schemas.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Request validation schemas for the inference API.""" - -from marshmallow import Schema, fields, ValidationError, validate - - -class FeatureVectorSchema(Schema): - """Schema for validating feature vectors in prediction requests.""" - - Amount = fields.Float(required=True, validate=validate.Range(min=0)) - log_amount = fields.Float(required=True) - - hour_sin = fields.Float(required=True, validate=validate.Range(min=-1, max=1)) - hour_cos = fields.Float(required=True, validate=validate.Range(min=-1, max=1)) - - dow_sin = fields.Float(required=True, validate=validate.Range(min=-1, max=1)) - dow_cos = fields.Float(required=True, validate=validate.Range(min=-1, max=1)) - - month_sin = fields.Float(required=True, validate=validate.Range(min=-1, max=1)) - month_cos = fields.Float(required=True, validate=validate.Range(min=-1, max=1)) - - is_weekend = fields.Int(required=True, validate=validate.OneOf([0, 1])) - is_night = fields.Int(required=True, validate=validate.OneOf([0, 1])) - - currency_mismatch = fields.Int(required=True, validate=validate.OneOf([0, 1])) - cross_border = fields.Int(required=True, validate=validate.OneOf([0, 1])) - is_round_amount = fields.Int(required=True, validate=validate.OneOf([0, 1])) - - sender_txn_count_24h = fields.Int(required=True, validate=validate.Range(min=0)) - sender_amount_sum_24h = fields.Float(required=True, validate=validate.Range(min=0)) - sender_amount_mean_30d = fields.Float(required=True, allow_none=True) - sender_amount_std_30d = fields.Float(required=True, allow_none=True) - sender_amount_zscore = fields.Float(required=True) - - receiver_txn_count_24h = fields.Int(required=True, validate=validate.Range(min=0)) - receiver_amount_sum_24h = fields.Float(required=True, validate=validate.Range(min=0)) - - seconds_since_sender_txn = fields.Int(required=True) - - sender_txn_count_lifetime = fields.Int(required=True, validate=validate.Range(min=0)) - receiver_txn_count_lifetime = fields.Int(required=True, validate=validate.Range(min=0)) - sender_out_degree = fields.Int(required=True, validate=validate.Range(min=0)) - receiver_in_degree = fields.Int(required=True, validate=validate.Range(min=0)) - pair_transaction_count = fields.Int(required=True, validate=validate.Range(min=0)) - sender_counterparty_hhi = fields.Float(required=True, validate=validate.Range(min=0, max=1)) - - Payment_type = fields.Str(required=True) - Payment_currency = fields.Str(required=True) - Received_currency = fields.Str(required=True) - Sender_bank_location = fields.Str(required=True) - Receiver_bank_location = fields.Str(required=True) - - class Meta: - unknown = "raise" - - -def validate_feature_vector(payload): - """ - Validate a feature vector against the schema. - - Args: - payload: Dictionary with feature values - - Returns: - Tuple of (cleaned_data, errors) - - Raises: - ValidationError: If validation fails - """ - schema = FeatureVectorSchema() - return schema.load(payload) diff --git a/src/api/templates/dashboard.html b/src/api/templates/dashboard.html deleted file mode 100644 index acf7185..0000000 --- a/src/api/templates/dashboard.html +++ /dev/null @@ -1,425 +0,0 @@ - - - - - - Transaction Risk Model - Dashboard - - - -
-
-

🚀 Transaction Risk Model

-

Real-time monitoring dashboard for XGBoost risk prediction model

-
- -
- -
-
-

Model Status

-
-
Current State
-
-
- -
-

Test Metrics

-
-
PR-AUC Score
-
- -
-

ROC-AUC

-
-
ROC AUC Score
-
- -
-

Alert Threshold

-
-
Decision Threshold
-
- -
-

Alert Rate

-
-
Top-K Percentage
-
- -
-

Model Version

-
-
Training Version
-
-
- -
-

📊 Performance Metrics

-
-
- -
-

🎯 Feature Sets

-
-
- Transaction - Amount, time, cyclical -
-
- Behavioral - Activity, patterns -
-
- Network - Graph, concentration -
-
- Categorical - Type, currency, location -
-
-
- -
-

🔒 Quantitative Transaction Risk Modeling | Real-time Dashboard

-

Last updated:

-
-
- - - - diff --git a/src/config.py b/src/config.py index ecdec7a..2d9cbb1 100644 --- a/src/config.py +++ b/src/config.py @@ -1,6 +1,6 @@ """Configuration management for model training and experiments.""" -from dataclasses import dataclass, asdict +from dataclasses import asdict, dataclass @dataclass diff --git a/src/data/validation.py b/src/data/validation.py index 132ceca..cb34a98 100644 --- a/src/data/validation.py +++ b/src/data/validation.py @@ -1,8 +1,9 @@ """Data quality validation and checks.""" import logging -import pandas as pd + import numpy as np +import pandas as pd from src.models.train import MODEL_FEATURES, TARGET diff --git a/src/evaluation/metrics.py b/src/evaluation/metrics.py index 9b5c786..9ed1242 100644 --- a/src/evaluation/metrics.py +++ b/src/evaluation/metrics.py @@ -11,26 +11,6 @@ ) -def threshold_for_alert_rate( - probabilities: Union[np.ndarray, list], - alert_rate: float = 0.005, -) -> float: - """ - Alert only the top X% highest-risk transactions. - """ - - probabilities = np.asarray(probabilities, dtype=float) - if not 0 < alert_rate <= 1: - raise ValueError("alert_rate must be in (0, 1].") - - return float( - np.quantile( - probabilities, - 1 - alert_rate, - ) - ) - - def top_k_alert_mask( probabilities: Union[np.ndarray, list], alert_rate: float = 0.005, diff --git a/src/models/inference.py b/src/models/inference.py deleted file mode 100644 index 2742e41..0000000 --- a/src/models/inference.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Artifact-backed scoring helpers shared by offline and API inference.""" - -from __future__ import annotations - -from typing import Any, Mapping - -import numpy as np -import pandas as pd - - -def model_input_from_features( - transaction_features: Mapping[str, Any], - feature_names: list[str], -) -> pd.DataFrame: - """Validate and order an already-computed feature payload. - - Account identifiers are deliberately absent: a production feature store - must turn them into historical behavioural variables before this boundary. - """ - - forbidden = {"Sender_account", "Receiver_account", "Is_laundering", "Laundering_type"} - supplied = set(transaction_features) - forbidden_supplied = supplied & forbidden - if forbidden_supplied: - raise ValueError( - "Identifier or target fields are not model inputs: " - f"{sorted(forbidden_supplied)}" - ) - - missing = set(feature_names) - supplied - if missing: - raise ValueError( - "Missing model features. Historical behavioural features must be " - f"provided by the feature store: {sorted(missing)}" - ) - - return pd.DataFrame([{name: transaction_features[name] for name in feature_names}]) - - -def predict_calibrated_probability(artifact: Mapping[str, Any], features: pd.DataFrame) -> float: - """Score one feature row with the saved preprocessing, model, and calibrator.""" - - processed = artifact["preprocessor"].transform(features) - raw_probability = artifact["model"].predict_proba(processed)[:, 1] - return float(artifact["calibrator"].predict(raw_probability)[0]) - - -def probability_percentile(artifact: Mapping[str, Any], probability: float) -> float | None: - """Rank a probability against validation probabilities, when available.""" - - reference = artifact.get("validation_probability_quantiles") - if not reference: - return None - - values = np.asarray(reference, dtype=float) - return float(100 * np.searchsorted(values, probability, side="right") / len(values)) diff --git a/src/monitoring/drift.py b/src/monitoring/drift.py index ee1b165..322a66f 100644 --- a/src/monitoring/drift.py +++ b/src/monitoring/drift.py @@ -1,6 +1,6 @@ """Model drift and data distribution monitoring.""" -from typing import Dict, Optional, List +from typing import Dict, List import numpy as np import pandas as pd diff --git a/tests/test_api.py b/tests/test_api.py index 3dd6d44..57cf7bd 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,9 +1,8 @@ """Tests for FastAPI inference API.""" import json -import pytest -from pathlib import Path +import pytest from fastapi.testclient import TestClient from src.api.main import create_app diff --git a/tests/test_config.py b/tests/test_config.py index 95f9f01..3e2d5f3 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,11 +1,10 @@ """Tests for configuration management.""" -import pytest from src.config import ( - XGBoostConfig, PreprocessingConfig, TrainingConfig, + XGBoostConfig, get_config, get_fast_config, get_production_config, diff --git a/tests/test_data_validation.py b/tests/test_data_validation.py index 6b3ee33..4cb84ec 100644 --- a/tests/test_data_validation.py +++ b/tests/test_data_validation.py @@ -1,8 +1,8 @@ """Tests for data quality validation.""" -import pytest -import pandas as pd import numpy as np +import pandas as pd +import pytest from src.data.validation import DataQualityValidator, validate_features from src.models.train import MODEL_FEATURES, TARGET diff --git a/tests/test_features.py b/tests/test_features.py index 57b155c..4183129 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -1,9 +1,8 @@ -import pytest -import pandas as pd import numpy as np +import pandas as pd -from src.features.transaction_features import add_transaction_features from src.features.behavioral_features import add_behavioral_features +from src.features.transaction_features import add_transaction_features def test_transaction_features_do_not_encode_account_ids(): diff --git a/tests/test_inference.py b/tests/test_inference.py index c037f12..b499547 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -1,11 +1,10 @@ -import asyncio from pathlib import Path import pytest from fastapi.testclient import TestClient -from src.api.main import create_app from src.api.inference import model_input_from_features +from src.api.main import create_app @pytest.mark.asyncio