Spaces:
Runtime error
Runtime error
File size: 10,794 Bytes
372c2d4 2817797 372c2d4 cf739bf 372c2d4 cf739bf 372c2d4 cf739bf 372c2d4 cf739bf 372c2d4 cf739bf 372c2d4 cf739bf 372c2d4 2817797 cf739bf 372c2d4 2817797 372c2d4 2817797 372c2d4 2817797 372c2d4 cf739bf 372c2d4 2817797 cf739bf 2817797 cf739bf 372c2d4 cf739bf 372c2d4 cf739bf 372c2d4 cf739bf 372c2d4 cf739bf 372c2d4 cf739bf 372c2d4 cf739bf 372c2d4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | """
================================================================================
FEATURE ENGINEERING MODULE - Tabular Feature Extraction for ML Models
================================================================================
PURPOSE:
Transforms raw transactions and graph topology into machine learning features.
Combines network centrality scores, temporal patterns, and behavioral signals
into a tabular matrix for XGBoost training and inference.
FEATURE CATEGORIES:
1. TRANSACTION VOLUME (Behavioral Baseline)
- tx_count_total: Total number of transactions sent
- amount_sent_total: Sum of outgoing amounts
- tx_count_7d: Transactions in last 7 days (recent activity)
- amount_sent_7d: Outgoing amount last 7 days
- amount_received_total: Sum of incoming amounts
- amount_received_7d: Incoming amount last 7 days
2. TEMPORAL DYNAMICS (Activity Patterns)
- first_tx: Date of first transaction (account age)
- last_tx: Date of most recent transaction
- days_since_last_tx: Inactive period indicator
- velocity_ratio_7d: Recent / historical activity ratio
3. TRANSACTION CHARACTERISTICS (Sophistication)
- avg_tx_amount: Mean transaction size
- amount_std: Variance in transaction amounts
- forward_ratio: Outgoing / incoming amount ratio
- in_out_ratio: Incoming / outgoing ratio
- channel_diversity: Count of unique payment types
- bank_diversity: Count of unique counterparty banks
- currency_diversity: Count of unique currencies used
4. NETWORK TOPOLOGY (Graph Importance)
- pagerank_score: Centrality by transaction volume flow
- betweenness_score: Intermediary role in network
- community_fraud_rate: Fraud rate in local Louvain community
5. SUSPICIOUS PATTERN DETECTION (Anomaly Signals)
- in_fraud_network: Binary: present in any is_laundering=1 edge
- cycle_length: Longest round-tripping cycle (0 if none)
- cycle_amount: Max amount in detected cycle
- mule_network_score: Proximity to mule accounts
FEATURE ENGINEERING PROCESS:
1. Pre-aggregate statistics (sent/received, total/7d windows)
2. Calculate diversity metrics (channels, banks, currencies)
3. Extract temporal features (first_tx, last_tx, velocity)
4. Compute network centrality (PageRank, betweenness, community detection)
5. Identify suspicious patterns (cycles, fraud networks)
6. Build DataFrame indexed by account, aligned for model input
DESIGN DECISIONS:
1. Dual-Window Aggregation (Total + 7-day)
- Captures both steady-state and recent behavior change
- Enables detection of sudden activity shifts (pattern break)
2. Variance in Transaction Amounts
- High variance suggests testing phase (micro-transactions)
- Low variance suggests routine operations
- Combined with std: standard deviation robust to outliers
3. Diversity Metrics
- Channel/bank diversity indicates sophistication
- Legitimate accounts reuse same channels
- Fraudsters use many channels to obscure patterns
4. Community Target-Encoding
- Louvain community fraud rate as node feature
- Accounts in high-fraud communities are higher risk
- More informative than raw community ID
5. Cycle Detection (vs Binary is_in_cycle)
- Cycle length: longer cycles = more coordinated schemes
- Cycle amount: higher amounts = more serious laundering
- Better than binary flag for risk scoring
6. Forward Ratio & In/Out Ratio
- Forward ratio >> 1: More sender than receiver (suspicious)
- Forward ratio << 1: More receiver (may be deposit)
- In/out ratio complements forward_ratio perspective
DEPENDENCIES:
- pandas: DataFrame operations and groupby aggregations
- numpy: Numerical calculations
- networkx (via graph_builder): Centrality scores
- python-louvain: Community detection
- src.config_loader: Configuration (window sizes, thresholds)
HYPERPARAMETERS:
- RECENT_WINDOW_DAYS = 7: Sliding window for recent activity
USAGE EXAMPLE:
features_df = engineer_features(
df=transactions,
G=transaction_graph,
pagerank_scores=pr_scores,
betweenness_scores=bc_scores,
louvain_partition=communities,
cycle_alerts=detected_cycles,
)
# Returns: DataFrame with shape (num_accounts, num_features)
# Index: account IDs
# Columns: feature names (ready for ML model input)
================================================================================
"""
import pandas as pd
import numpy as np
from datetime import datetime
from src.config_loader import get_config
RECENT_WINDOW_DAYS = 7
def engineer_features(
df: pd.DataFrame,
G,
pagerank_scores: dict,
betweenness_scores: dict,
louvain_partition: dict,
cycle_alerts: list,
) -> pd.DataFrame:
cfg = get_config()
df = df.copy()
df['timestamp'] = pd.to_datetime(df['timestamp'])
max_date = df['timestamp'].max()
cutoff_7d = max_date - pd.Timedelta(days=RECENT_WINDOW_DAYS)
now = pd.Timestamp(datetime.now())
# Collect all accounts that have been flagged in any is_laundering=1 edge
fraud_sources = (
set(df[df['is_laundering'] == 1]['source'].unique()) |
set(df[df['is_laundering'] == 1]['target'].unique())
)
all_accounts = set(df['source'].unique()) | set(df['target'].unique())
# Pre-aggregate transaction stats to avoid repeated groupby operations.
# Separating total vs 7-day windows lets us capture recent behavior changes.
sent_all = df.groupby('source').agg(
tx_count_total=('amount', 'count'),
amount_sent_total=('amount', 'sum'),
)
sent_7d = df[df['timestamp'] >= cutoff_7d].groupby('source').agg(
tx_count_7d=('amount', 'count'),
amount_sent_7d=('amount', 'sum'),
)
recv_all = df.groupby('target').agg(
amount_received_total=('amount', 'sum'),
)
recv_7d = df[df['timestamp'] >= cutoff_7d].groupby('target').agg(
amount_received_7d=('amount', 'sum'),
)
# Combine sent and received amounts to compute variance across all transactions.
# High variance in transaction amounts is suspicious (often indicates testing phase).
sent_amounts = df[['source', 'amount']].rename(columns={'source': 'account'})
recv_amounts = df[['target', 'amount']].rename(columns={'target': 'account'})
combined_amounts = pd.concat([sent_amounts, recv_amounts], ignore_index=True)
amount_std = combined_amounts.groupby('account')['amount'].std().rename('amount_std')
# Temporal features: account age and channel/bank diversity indicate sophistication
first_tx = df.groupby('source')['timestamp'].min().rename('first_tx')
last_tx = df.groupby('source')['timestamp'].max().rename('last_tx')
channel_div = df.groupby('source')['payment_type'].nunique().rename('channel_diversity')
bank_div = df.groupby('source')['target_bank'].nunique().rename('bank_diversity') \
if 'target_bank' in df.columns else pd.Series(dtype=float, name='bank_diversity')
currency_div = df.groupby('source')['Payment Currency'].nunique().rename('currency_diversity') \
if 'Payment Currency' in df.columns else pd.Series(dtype=float, name='currency_diversity')
# Extract cycle length and max involved amounts for accounts in detected round-tripping patterns.
# We keep the longest cycle per account to capture the most sophisticated laundering scheme.
cycle_length_map = {}
cycle_amount_map = {}
for alert in cycle_alerts:
if alert.get('typology') == 'RoundTripping':
members = alert.get('cycle_members', [alert['account']])
length = alert.get('tx_count', len(members))
amount = alert.get('amount_involved', 0)
for m in members:
if m not in cycle_length_map or length > cycle_length_map[m]:
cycle_length_map[m] = length
cycle_amount_map[m] = amount
# Target-encode community membership: compute fraud rate per Louvain community.
# Communities with high fraud rates are more suspicious and signal stronger risk.
community_fraud = {}
for node, cid in louvain_partition.items():
community_fraud.setdefault(cid, []).append(1 if node in fraud_sources else 0)
community_fraud_rate = {cid: np.mean(vals) for cid, vals in community_fraud.items()}
# Build feature DataFrame
features = pd.DataFrame(index=list(all_accounts))
features.index.name = 'account'
features = features.join(sent_all)
features = features.join(sent_7d)
features = features.join(recv_all)
features = features.join(recv_7d)
features['forward_ratio'] = features['amount_sent_total'] / (features['amount_received_total'] + 1)
features['avg_tx_amount'] = features['amount_sent_total'] / (features['tx_count_total'] + 1)
features = features.join(amount_std)
features['in_out_ratio'] = features['amount_received_total'] / (features['amount_sent_total'] + 1)
features['velocity_ratio_7d'] = features['tx_count_7d'] / (features['tx_count_total'] + 1)
features['pagerank_score'] = features.index.map(lambda a: pagerank_scores.get(a, 0))
features['betweenness_score'] = features.index.map(lambda a: betweenness_scores.get(a, 0))
features['in_degree'] = features.index.map(lambda a: G.in_degree(a) if a in G else 0)
features['out_degree'] = features.index.map(lambda a: G.out_degree(a) if a in G else 0)
features['fan_in_ratio'] = features['in_degree'] / (features['in_degree'] + features['out_degree'] + 1)
# FIX: target-encoded community (fraud rate per community)
features['community_encoded'] = features.index.map(
lambda a: community_fraud_rate.get(louvain_partition.get(a, -1), 0.0)
)
# FIX: cycle_length + cycle_max_amount
features['cycle_length'] = features.index.map(lambda a: cycle_length_map.get(a, 0))
features['cycle_max_amount'] = features.index.map(lambda a: cycle_amount_map.get(a, 0))
features = features.join(first_tx)
features = features.join(last_tx)
features['account_age_days'] = (features['last_tx'] - features['first_tx']).dt.days.fillna(0)
features['days_since_last_tx'] = (now - features['last_tx']).dt.days.fillna(9999)
features = features.drop(columns=['first_tx', 'last_tx'], errors='ignore')
features = features.join(currency_div)
features = features.join(channel_div)
features = features.join(bank_div)
features['fraud_flag'] = features.index.map(lambda a: 1 if a in fraud_sources else 0)
features = features.replace([np.inf, -np.inf], 0).fillna(0)
features = features.reset_index()
return features
|