cdotsanghvi's picture
add multi-head demo as 4th-6th tabs; restore Why Liquid + Integration
083b138
Raw
History Blame Contribute Delete
7.79 kB
"""Decode token IDs back to human-readable transaction descriptions.
Handles the reserved-token offset (0=MASK, 1=OOV, 2=NULL, 3+=values)
and maps each feature's raw value to readable text.
"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
from src.data.generator import AMOUNT_RANGE_LABELS
from src.data.schema import SchemaConfig, VALUES_START
from src.demo.merchant_catalog import DemoMerchantCatalog, MCC_NAMES
DOW_NAMES: list[str] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
ENTRY_MODE_DISPLAY: dict[str, str] = {
"card_present": "Card Present",
"card_not_present": "Online",
"contactless": "Contactless",
"chip": "Chip",
"manual_key": "Manual",
}
COUNTRY_NAMES: list[str] = [
"US", "UK", "CA", "DE", "FR", "JP", "AU", "BR", "IN", "MX",
"IT", "ES", "NL", "CH", "SE", "NO", "DK", "FI", "KR", "SG",
"HK", "TW", "NZ", "IE", "BE", "AT", "PT", "PL", "CZ", "GR",
"IL", "AE", "SA", "TH", "MY", "PH", "ID", "VN", "CL", "CO",
"AR", "ZA", "NG", "EG", "KE", "TR", "RU", "UA", "RO", "HU",
]
AVS_DISPLAY: dict[str, str] = {
"full_match": "AVS Match",
"zip_match": "ZIP Match",
"address_match": "Addr Match",
"no_match": "AVS No Match",
"not_checked": "AVS N/A",
}
CVV_DISPLAY: dict[str, str] = {
"match": "CVV Match",
"no_match": "CVV No Match",
"not_provided": "No CVV",
}
@dataclass
class DecodedTransaction:
"""A single transaction with all features decoded to display strings."""
index: int
hour: str
dow: str
days_since_last: str
is_recurring: str
mcc: str
merchant_name: str
merchant_category: str
customer_merchant_count: str
entry_mode: str
amount_range: str
card_product: str
country: str
avs: str
cvv: str
device_hash: str
customer_tenure: str
class TransactionDecoder:
"""Converts raw token_ids (T, F) into human-readable transactions."""
def __init__(self, schema: SchemaConfig, merchant_catalog: DemoMerchantCatalog) -> None:
self.schema = schema
self.merchants = merchant_catalog
self._feature_names = schema.feature_names()
def _decode_value(self, feature_idx: int, token_id: int) -> str:
"""Decode a single token_id for a given feature index."""
feat = self.schema.features[feature_idx]
if token_id == 0:
return "[MASK]"
if token_id == 1:
return "[OOV]"
if token_id == 2:
return "[NULL]"
value = token_id - VALUES_START
if feat.name == "hour":
if 0 <= value <= 23:
h = value % 12 or 12
ampm = "AM" if value < 12 else "PM"
return f"{h} {ampm}"
return f"H{value}"
if feat.name == "dow":
return DOW_NAMES[value] if 0 <= value < 7 else f"D{value}"
if feat.name == "days_since_last":
if value == 0:
return "Same day"
if value <= 5:
return f"{value}d ago"
bucket_size = 365 / 30
approx = int(value * bucket_size)
return f"~{approx}d ago"
if feat.name == "is_recurring":
return "Recurring" if value == 1 else "One-time"
if feat.name == "mcc":
return MCC_NAMES.get(value, f"MCC-{value}")
if feat.name == "merchant_id":
info = self.merchants.get(value)
return info.name
if feat.name == "customer_merchant_count":
if value == 0:
return "1st visit"
if value < 5:
return f"{value + 1} visits"
bucket_size = 500 / 20
approx = int(value * bucket_size)
return f"~{approx} visits"
if feat.name == "entry_mode":
if feat.values and value in feat.values:
raw = feat.values[value]
return ENTRY_MODE_DISPLAY.get(raw, raw)
return f"Entry-{value}"
if feat.name == "amount":
range_idx = value // 16
return AMOUNT_RANGE_LABELS.get(range_idx, f"${value}")
if feat.name == "card_product":
if feat.values and value in feat.values:
raw = feat.values[value]
return raw.replace("_", " ").title()
return f"Card-{value}"
if feat.name == "country":
return COUNTRY_NAMES[value] if 0 <= value < len(COUNTRY_NAMES) else f"Country-{value}"
if feat.name == "avs":
if feat.values and value in feat.values:
raw = feat.values[value]
return AVS_DISPLAY.get(raw, raw)
return f"AVS-{value}"
if feat.name == "cvv":
if feat.values and value in feat.values:
raw = feat.values[value]
return CVV_DISPLAY.get(raw, raw)
return f"CVV-{value}"
if feat.name == "device_hash":
return f"Device-{value:04d}"
if feat.name == "customer_tenure":
months = value * 12
if months < 12:
return f"<1 year"
return f"~{months // 12}yr"
return str(value)
def decode_sequence(self, token_ids: np.ndarray) -> list[DecodedTransaction]:
"""Decode a full (T, F) sequence into readable transactions.
Returns transactions in reverse chronological order (most recent first).
"""
num_tx = token_ids.shape[0]
txns: list[DecodedTransaction] = []
for t in range(num_tx - 1, -1, -1):
row = token_ids[t]
merchant_val = int(row[5]) - VALUES_START
merchant_info = self.merchants.get(max(0, merchant_val))
txn = DecodedTransaction(
index=t,
hour=self._decode_value(0, int(row[0])),
dow=self._decode_value(1, int(row[1])),
days_since_last=self._decode_value(2, int(row[2])),
is_recurring=self._decode_value(3, int(row[3])),
mcc=self._decode_value(4, int(row[4])),
merchant_name=self._decode_value(5, int(row[5])),
merchant_category=merchant_info.category,
customer_merchant_count=self._decode_value(6, int(row[6])),
entry_mode=self._decode_value(7, int(row[7])),
amount_range=self._decode_value(8, int(row[8])),
card_product=self._decode_value(9, int(row[9])),
country=self._decode_value(10, int(row[10])),
avs=self._decode_value(11, int(row[11])),
cvv=self._decode_value(12, int(row[12])),
device_hash=self._decode_value(13, int(row[13])),
customer_tenure=self._decode_value(14, int(row[14])),
)
txns.append(txn)
return txns
def summarize_customer(
self, token_ids: np.ndarray, is_fraud: bool,
) -> str:
"""One-line behavioral summary derived from actual token data."""
num_tx = token_ids.shape[0]
merchant_ids = set()
mcc_counts: dict[str, int] = {}
for t in range(num_tx):
mid = int(token_ids[t, 5]) - VALUES_START
merchant_ids.add(mid)
mcc_val = int(token_ids[t, 4]) - VALUES_START
cat = MCC_NAMES.get(mcc_val, f"Cat-{mcc_val}")
mcc_counts[cat] = mcc_counts.get(cat, 0) + 1
top_cats = sorted(mcc_counts.items(), key=lambda x: -x[1])[:3]
cat_str = ", ".join(c[0] for c in top_cats)
label = "FRAUD" if is_fraud else "Legitimate"
return (
f"{num_tx} transactions | {len(merchant_ids)} unique merchants | "
f"Top categories: {cat_str} | Label: {label}"
)