Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .firebaserc

This file was deleted.

2 changes: 0 additions & 2 deletions .gitattributes

This file was deleted.

119 changes: 32 additions & 87 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,102 +1,47 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log*
firebase-debug.*.log*

# Firebase cache
.firebase/

# Firebase config

# Uncomment this if you'd like others to create their own Firebase project.
# For a team working on the same Firebase project(s), it is recommended to leave
# it commented so all members can deploy to the same project(s) in .firebaserc.
# .firebaserc

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# dataconnect generated files
.dataconnect


# Python
__pycache__/
*.pyc
*.pyo
*.pyd
*.py[cod]
*.so
.Python
venv/
.venv/
env/

# Firebase
firebase-credentials.json
.firebase/

# Environment
.env

# Models
*.pkl
src/models/*.pkl
# IDE
.vscode/
.idea/
.DS_Store

# Jupyter
.ipynb_checkpoints/

# Raw datasets
data/raw/*
!data/raw/.gitkeep

# Generated datasets
data/processed/
*.parquet

# Model artifacts
artifacts/*
!artifacts/.gitkeep

# Generated figures
reports/figures/*
!reports/figures/.gitkeep

# Logs
logs/
*.log

# VS Code
.vscode/settings.json
# Testing
.pytest_cache/
.coverage
htmlcov/

# Data
data/
*.csv
*.json
# Large data
*.csv
3 changes: 0 additions & 3 deletions SAML-D.csv

This file was deleted.

Empty file added artifacts/.gitkeep
Empty file.
25 changes: 25 additions & 0 deletions data/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Dataset

This project uses the Synthetic Anti-Money Laundering Dataset (SAML-D).

The raw dataset is intentionally not committed to this repository because of
its size and third-party licensing terms.

## Dataset

SAML-D contains approximately 9.5 million synthetic financial transactions,
including rare suspicious transaction patterns and multiple laundering
typologies.

Expected raw file:

data/raw/SAML-D.csv

## Citation

B. Oztas, D. Cetinkaya, F. Adedoyin, M. Budka, H. Dogan and G. Aksu,
"Enhancing Anti-Money Laundering: Development of a Synthetic Transaction
Monitoring Dataset," 2023 IEEE International Conference on e-Business
Engineering (ICEBE), 2023.

Dataset license: CC BY-NC-SA 4.0.
Empty file added data/raw/.gitkeep
Empty file.
File renamed without changes.
File renamed without changes.
19 changes: 19 additions & 0 deletions notebooks/03_backtesting.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Out-of-Time Backtesting\n",
"\n",
"Use this notebook for walk-forward evaluation, alert-budget metrics, threshold optimisation, drift monitoring, and laundering-typology analysis. Populate results only from executed experiments."
]
}
],
"metadata": {
"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
"language_info": {"name": "python", "version": "3.12"}
},
"nbformat": 4,
"nbformat_minor": 5
}
Empty file added reports/.gitkeep
Empty file.
Empty file added reports/figures/.gitkeep
Empty file.
34 changes: 18 additions & 16 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
pandas>=1.3.0
numpy>=1.21.0
scikit-learn>=1.0.0
xgboost>=1.5.0
imbalanced-learn>=0.8.0
flask>=2.0.0
flask-cors>=3.0.10
python-dotenv>=0.19.0
firebase-admin>=6.0.0
google-cloud-firestore>=2.7.0
schedule>=1.1.0
matplotlib>=3.4.0
seaborn>=0.11.0
plotly>=5.0.0
pytest>=6.2.0
python-dateutil>=2.8.0
pandas>=2.2,<3.0
numpy>=1.26,<3.0
scikit-learn>=1.5,<2.0
xgboost>=2.1,<4.0

duckdb>=1.1,<2.0
pyarrow>=17,<22
joblib>=1.4,<2.0

matplotlib>=3.9,<4.0
shap>=0.46,<1.0

flask>=3.0,<4.0
flask-cors>=5.0,<7.0
python-dotenv>=1.0,<2.0
requests>=2.32,<3.0

pytest>=8.0,<9.0
40 changes: 40 additions & 0 deletions scripts/build_features.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import sys
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))

from src.data.loader import load_saml_data
from src.features.behavioral_features import add_behavioral_features
from src.features.transaction_features import add_transaction_features

RAW_PATH = PROJECT_ROOT / "data/raw/SAML-D.csv"
OUTPUT_PATH = PROJECT_ROOT / "data/processed/transactions_features.parquet"


def main():
print("Loading SAML-D...")
df = load_saml_data(RAW_PATH)

print(f"Loaded {len(df):,} transactions")

print("Building transaction features...")
df = add_transaction_features(df)

print("Building behavioral features...")
df = add_behavioral_features(df)

OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)

df.to_parquet(
OUTPUT_PATH,
index=False,
compression="snappy",
)

print(f"Feature dataset written to {OUTPUT_PATH}")
print(f"Shape: {df.shape}")


if __name__ == "__main__":
main()
93 changes: 93 additions & 0 deletions scripts/feature_ablation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import sys
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))

import pandas as pd

from src.evaluation.metrics import (
evaluate_model,
precision_recall_at_alert_rate,
)
from src.models.calibration import ProbabilityCalibrator
from src.models.train import (
ABLATION_FEATURE_SETS,
TARGET,
chronological_split,
fit_model,
)

FEATURE_PATH = PROJECT_ROOT / "data/processed/transactions_features.parquet"
ALERT_RATE = 0.005


def evaluate_feature_set(name, features, train, validation, test):
(
preprocessor,
model,
X_validation_processed,
y_validation,
) = fit_model(train, validation, features)

validation_probabilities = model.predict_proba(
X_validation_processed
)[:, 1]

calibrator = ProbabilityCalibrator().fit(
validation_probabilities,
y_validation,
)

test_probabilities = calibrator.predict(
model.predict_proba(
preprocessor.transform(test[features])
)[:, 1]
)

metrics = evaluate_model(
test[TARGET],
test_probabilities,
)
alert_metrics = precision_recall_at_alert_rate(
test[TARGET],
test_probabilities,
ALERT_RATE,
)

return {
"model": name,
"pr_auc": metrics["pr_auc"],
"recall_at_0.5%": alert_metrics["recall"],
}


def main():
df = pd.read_parquet(FEATURE_PATH)
train, validation, test = chronological_split(df)

results = [
evaluate_feature_set(
name,
features,
train,
validation,
test,
)
for name, features in ABLATION_FEATURE_SETS.items()
]

print("\nFEATURE ABLATION RESULTS")
print("=" * 55)
print(f"{'Model':<22} {'PR-AUC':>12} {'Recall@0.5%':>16}")
print("-" * 55)
for result in results:
print(
f"{result['model']:<22} "
f"{result['pr_auc']:>12.6f} "
f"{result['recall_at_0.5%']:>16.6f}"
)


if __name__ == "__main__":
main()
Loading