Перейти к содержанию

Требования к данным: обнаружение мошенничества

~4 минуты чтения

Предварительно: Определение задачи, Feature Engineering

Fraud detection -- одна из самых data-intensive ML-систем. Для скоринга одной транзакции за < 100 мс нужно собрать 50-200 фичей из 5+ источников: транзакционная история (velocity за 1ч/24ч/7д), профиль пользователя (account age, avg amount), device fingerprint (30+ параметров браузера), IP reputation (VPN/proxy/Tor), граф связей (users sharing device/IP). При этом лейблы приходят с задержкой 30-90 дней (chargebacks), а 40% фрода вообще никогда не репортится. Data pipeline должен обрабатывать 50K+ событий/сек в реальном времени (Kafka + Flink) и параллельно строить batch-фичи в Spark для переобучения модели.

Источники данных

1. Transaction Data (Данные транзакций)

CREATE TABLE transactions (
    transaction_id UUID PRIMARY KEY,
    user_id BIGINT NOT NULL,
    merchant_id BIGINT NOT NULL,
    card_id BIGINT,
    amount DECIMAL(15,2),
    currency VARCHAR(3),
    timestamp TIMESTAMP,
    transaction_type VARCHAR(20),  -- purchase, refund, transfer
    channel VARCHAR(20),           -- online, pos, atm, mobile
    status VARCHAR(20),            -- approved, declined, pending
    decline_reason VARCHAR(50),
    mcc_code VARCHAR(4),           -- Merchant Category Code
    country VARCHAR(2),
    city VARCHAR(100),
    ip_address INET,
    device_id VARCHAR(100),
    session_id VARCHAR(100)
);

-- Labels (приходят с задержкой)
CREATE TABLE fraud_labels (
    transaction_id UUID PRIMARY KEY,
    is_fraud BOOLEAN,
    fraud_type VARCHAR(50),
    reported_at TIMESTAMP,
    source VARCHAR(20),  -- chargeback, customer_report, internal
    confirmed_by VARCHAR(50)
);

2. User/Account Data

CREATE TABLE users (
    user_id BIGINT PRIMARY KEY,
    created_at TIMESTAMP,
    kyc_status VARCHAR(20),
    kyc_verified_at TIMESTAMP,
    email_verified BOOLEAN,
    phone_verified BOOLEAN,
    address_verified BOOLEAN,
    country VARCHAR(2),
    risk_score FLOAT,
    account_type VARCHAR(20),
    lifetime_value DECIMAL(15,2)
);

CREATE TABLE user_devices (
    user_id BIGINT,
    device_id VARCHAR(100),
    device_type VARCHAR(20),
    os VARCHAR(20),
    first_seen TIMESTAMP,
    last_seen TIMESTAMP,
    is_trusted BOOLEAN,
    PRIMARY KEY (user_id, device_id)
);

3. Merchant Data

CREATE TABLE merchants (
    merchant_id BIGINT PRIMARY KEY,
    merchant_name VARCHAR(200),
    mcc_code VARCHAR(4),
    category VARCHAR(50),
    country VARCHAR(2),
    risk_category VARCHAR(20),
    onboarded_at TIMESTAMP,
    avg_transaction_amount DECIMAL(15,2),
    chargeback_rate FLOAT,
    fraud_rate FLOAT
);

4. Device & Session Data

device_fingerprint = {
    "device_id": "d123",
    "user_agent": "Mozilla/5.0...",
    "screen_resolution": "1920x1080",
    "timezone": "Europe/Moscow",
    "language": "ru-RU",
    "plugins": ["pdf", "flash"],
    "canvas_hash": "abc123",
    "webgl_hash": "def456",
    "audio_hash": "ghi789",
    "fonts": ["Arial", "Helvetica"],
    "touch_support": False,
    "cookies_enabled": True,
}

session_data = {
    "session_id": "s123",
    "ip_address": "1.2.3.4",
    "ip_geo": {"country": "RU", "city": "Moscow", "isp": "MTS"},
    "is_vpn": False,
    "is_proxy": False,
    "is_tor": False,
    "referrer": "https://google.com",
    "pages_viewed": 5,
    "time_on_site_sec": 120,
    "mouse_movements": 1500,
    "keystrokes": 50,
}

Feature Engineering

1. Transaction Features

transaction_features = {
    # Basic
    "amount": 150.00,
    "amount_log": 2.18,
    "currency": "USD",
    "hour_of_day": 14,
    "day_of_week": 3,
    "is_weekend": False,
    "is_night": False,  # 22:00-06:00

    # Merchant
    "mcc_code": "5411",  # Grocery stores
    "merchant_risk_score": 0.1,
    "is_high_risk_mcc": False,

    # Velocity (user-level)
    "txn_count_1h": 2,
    "txn_count_24h": 5,
    "txn_count_7d": 15,
    "txn_amount_1h": 250.00,
    "txn_amount_24h": 500.00,
    "txn_amount_7d": 1500.00,
    "unique_merchants_24h": 3,
    "unique_countries_24h": 1,

    # Deviation from pattern
    "amount_zscore": 1.2,  # vs user's average
    "is_new_merchant": False,
    "is_new_country": False,
    "is_new_device": False,
    "time_since_last_txn_min": 30,
}

2. User Behavior Features

user_behavior_features = {
    # Historical patterns
    "avg_txn_amount_30d": 75.00,
    "std_txn_amount_30d": 25.00,
    "max_txn_amount_30d": 200.00,
    "typical_hour_range": [9, 22],
    "typical_mcc_codes": ["5411", "5812", "5912"],

    # Account age & activity
    "account_age_days": 365,
    "days_since_first_txn": 350,
    "total_txn_count": 500,
    "total_txn_amount": 25000.00,

    # Risk indicators
    "failed_txn_count_7d": 1,
    "declined_txn_count_7d": 0,
    "password_changes_30d": 0,
    "email_changes_30d": 0,
    "device_changes_30d": 1,

    # Fraud history
    "prev_fraud_count": 0,
    "prev_chargeback_count": 1,
    "days_since_last_fraud": None,
}

3. Network/Graph Features

graph_features = {
    # Device graph
    "users_sharing_device": 1,
    "devices_per_user": 2,
    "is_device_linked_to_fraud": False,

    # IP graph
    "users_sharing_ip": 3,
    "ips_per_user_24h": 1,
    "is_ip_linked_to_fraud": False,

    # Card graph
    "cards_per_user": 2,
    "users_per_card": 1,

    # Email/phone graph
    "accounts_with_same_email_domain": 10,
    "accounts_with_same_phone_prefix": 5,

    # Community detection
    "user_community_id": 123,
    "community_fraud_rate": 0.02,
}

4. Real-time Session Features

session_features = {
    # Behavioral biometrics
    "typing_speed_wpm": 45,
    "mouse_movement_pattern": "human",  # vs bot
    "scroll_pattern": "normal",
    "time_to_complete_form_sec": 30,

    # Navigation
    "pages_before_checkout": 5,
    "time_on_checkout_page_sec": 60,
    "cart_changes_count": 2,

    # Anomalies
    "is_unusual_time": False,
    "is_unusual_location": False,
    "is_unusual_amount": False,
    "is_unusual_merchant": False,
}

Data Pipeline

graph TD
    subgraph RT["REAL-TIME PIPELINE"]
        TXN_RT["Transaction Event<br/>(JSON)"] --> KAFKA["Kafka<br/>Topic"]
        KAFKA --> FLINK["Flink<br/>Stream Processing"]
        FLINK --> REDIS["Redis<br/>Features"]
        FLINK --> SCORING["Fraud Scoring<br/>Service"]
    end

    subgraph BATCH["BATCH PIPELINE"]
        TXN_BATCH["Transaction Logs<br/>(Daily)"] --> S3["S3<br/>Raw Data"]
        S3 --> SPARK["Spark<br/>ETL"]
        SPARK --> FS["Feature<br/>Store"]
        SPARK --> TRAIN["Training<br/>Dataset"]
    end

    style TXN_RT fill:#e8eaf6,stroke:#3f51b5
    style KAFKA fill:#fff3e0,stroke:#ef6c00
    style FLINK fill:#f3e5f5,stroke:#9c27b0
    style REDIS fill:#e8f5e9,stroke:#4caf50
    style SCORING fill:#e8f5e9,stroke:#4caf50
    style TXN_BATCH fill:#e8eaf6,stroke:#3f51b5
    style S3 fill:#fff3e0,stroke:#ef6c00
    style SPARK fill:#f3e5f5,stroke:#9c27b0
    style FS fill:#e8f5e9,stroke:#4caf50
    style TRAIN fill:#e8f5e9,stroke:#4caf50

Label Management

Label Sources

label_sources = {
    "chargeback": {
        "delay": "30-90 days",
        "reliability": "high",
        "coverage": "60%"  # не все фроды оспариваются
    },
    "customer_report": {
        "delay": "1-7 days",
        "reliability": "medium",
        "coverage": "30%"
    },
    "internal_investigation": {
        "delay": "1-30 days",
        "reliability": "high",
        "coverage": "10%"
    },
    "rule_based_detection": {
        "delay": "real-time",
        "reliability": "low",
        "coverage": "varies"
    }
}

Handling Label Delay

# Вариант 1: Ждать mature labels
def get_training_data(cutoff_days=90):
    """Use only transactions older than 90 days with confirmed labels"""
    return df[df['transaction_date'] < today - timedelta(days=cutoff_days)]

# Вариант 2: Proxy labels
def create_proxy_labels(df):
    """Use early signals as proxy for fraud"""
    df['proxy_fraud'] = (
        (df['chargeback_initiated']) |
        (df['customer_dispute']) |
        (df['failed_delivery_with_claim']) |
        (df['multiple_failed_auth_attempts'])
    )
    return df

# Вариант 3: Semi-supervised learning
def pseudo_labeling(model, unlabeled_data, threshold=0.95):
    """Use high-confidence predictions as labels"""
    predictions = model.predict_proba(unlabeled_data)
    high_conf_fraud = predictions[:, 1] > threshold
    high_conf_legit = predictions[:, 1] < (1 - threshold)
    # Use these as additional training data

Data Quality Checks

fraud_data_quality_checks = {
    "transactions": [
        "transaction_id is unique",
        "amount > 0",
        "timestamp is not null and not in future",
        "user_id exists in users table",
        "no duplicate transactions within 1 second",
    ],
    "features": [
        "no null values in required features",
        "velocity features are non-negative",
        "amounts are in expected currency range",
        "device_id format is valid",
    ],
    "labels": [
        "is_fraud is boolean",
        "reported_at >= transaction timestamp",
        "no contradicting labels for same transaction",
    ],
    "monitoring": [
        "feature distribution drift < threshold",
        "label rate within expected range",
        "data freshness < 1 hour",
    ]
}

Privacy & Compliance

PCI DSS Requirements

  • Card numbers must be tokenized
  • Sensitive data encrypted at rest and in transit
  • Access logging and auditing
  • Data retention policies

GDPR Considerations

  • Right to explanation (why blocked?)
  • Right to erasure (delete user data)
  • Data minimization (only needed features)
  • Consent for profiling
# Data masking example
def mask_sensitive_data(transaction):
    return {
        **transaction,
        "card_number": hash(transaction["card_number"]),
        "ip_address": anonymize_ip(transaction["ip_address"]),
        "email": hash(transaction["email"]),
    }

Заблуждение: можно обучать модель на всех доступных данных

Нельзя использовать транзакции моложе 90 дней для обучения -- лейблы ещё не mature. Из 1M транзакций за последние 30 дней, 40% фродовых ещё не отмечены (chargeback не пришёл). Обучение на таких данных создаёт label noise: модель учится, что эти транзакции легитимны, хотя они фродовые. Решение -- mature window (90 дней) + proxy labels для свежих данных.

Заблуждение: device fingerprint однозначно идентифицирует устройство

Canvas hash, WebGL hash, audio hash меняются при обновлении браузера/ОС (примерно раз в 2-4 недели). VPN/инкогнито-режим скрывает IP и cookies. Фродстеры используют antidetect-браузеры (Multilogin, GoLogin), которые генерируют уникальные fingerprint за $100/мес. Device fingerprint -- один из сигналов, но не единственный. Надёжная идентификация требует комбинации 5+ факторов.

Заблуждение: velocity-фичи достаточно считать по user_id

Фродстеры создают новые аккаунты. Velocity нужно считать по 4 сущностям: user_id, device_id, ip_address, card_token. Один device_id с 10 разными user_id за час -- сильнейший сигнал. Один IP с 50 транзакциями от разных пользователей -- datacenter/botnet. Пропуск кросс-сущностных velocity -- одна из главных причин пропуска fraud rings.

Секция для интервью

Вопрос: "Как обрабатывать label delay в 30-90 дней?"

❌ Слабый ответ: "Обучаем модель на тех лейблах, которые есть."

✅ Сильный ответ: "Три стратегии: (1) Mature window -- обучаем только на транзакциях старше 90 дней, где chargeback уже пришёл или окно закрылось. Минус: модель отстаёт от новых паттернов на 3 месяца. (2) Proxy labels -- используем ранние сигналы (жалобы клиентов через 1-7 дней, failed delivery, suspicious login) как приближение к true labels. Coverage ниже, но задержка 1-7 дней вместо 90. (3) Semi-supervised -- обучаем модель на mature данных, затем используем high-confidence предсказания (score > 0.95) на свежих данных как pseudo-labels для дообучения. Комбинация трёх подходов даёт лучший результат."

Вопрос: "Какие фичи самые важные для fraud detection?"

❌ Слабый ответ: "Сумма транзакции и время."

✅ Сильный ответ: "По feature importance (SHAP) в production-системах: (1) Velocity-фичи -- txn_count_1h, txn_amount_24h -- отвечают за 15-20% predictive power; (2) Deviation from pattern -- amount_zscore (текущая сумма vs среднее пользователя), is_new_device, is_new_country -- ещё 15%; (3) Graph-фичи -- users_sharing_device, hops_to_known_fraudster -- 10-15%; (4) Device/session -- is_vpn, is_datacenter_ip, typing_speed. Важно: сырые фичи (amount, hour_of_day) менее информативны, чем производные (amount_zscore, time_since_last_txn). Фичи нужно считать в реальном времени (Flink -> Redis) для velocity и batch (Spark) для исторических профилей."