Генерация транзакций для дропов¶

  • ноутбуки лучше просматривать на Github pages, т.к. при просмотре прямо в репозитории могут быть проблемы с отображением, например, обрезка вывода с широкими датафреймами. Если в адресной строке браузера есть iaroslav-dzh.github.io, то вы уже на Github pages.
    Ссылки:
    • Ссылка на этот ноутбук
    • Ссылка на страницу генератора где есть все ноутбуки

Информация о ноутбуке

  • в этом ноутбуке все что относится к генерации одной транзакции:
    • генерация части данных транзакций
    • сборка базовых классов дропов в один объект. Имеются в виду классы:
      • управления счетами
      • генерации сумм транзакций
      • генерации времени транзакций
      • управления поведением дропа
      • генерации части данных транзакций
    • полная генерация одной транзакции:
      • для переводов, снятий
      • для покупок
In [1]:
import pandas as pd
import numpy as np
import os
import pyarrow
import yaml
from typing import Union
In [2]:
from data_generator.general_time import *
from data_generator.utils import create_txns_df, load_configs, build_transaction
from data_generator.configs import DropDistributorCfg, DropPurchaserCfg
from data_generator.fraud.drops.build.config import DropConfigBuilder
In [3]:
np.set_printoptions(suppress=True)
pd.set_option('display.max_columns', None)
In [4]:
os.chdir("..")
os.getcwd()
Out[4]:
'C:\\Users\\iaros\\My_documents\\Education\\projects\\fraud_detection_01'
In [5]:
# Базовые конфиги
base_cfg = load_configs("./config/base.yaml")
# Настройки легальных транзакций
legit_cfg = load_configs("./config/legit.yaml")
# Общие настройки фрода
fraud_cfg = load_configs("./config/fraud.yaml")
# Настройки для дроп фрода
drops_cfg = load_configs("./config/drops.yaml")
# Настройки времени
time_cfg = load_configs("./config/time.yaml") 

# Пути к файлам
data_paths = base_cfg["data_paths"]

Создание объектов конфиг классов¶

In [6]:
# директорию текущего запуска генератора возьмем из предыдущего ноутбука. Т.к. нужны данные того, что сгенерировано до дроп фрода
# предварительно удалены папки дропов dist_drops и purch_drops т.к. при создании объектов конфиг классов они будут созданы заново 

run_dir = './data/generated/history/generation_run_2025-07-25_121029'
In [7]:
drop_cfg_build = DropConfigBuilder(base_cfg=base_cfg, legit_cfg=legit_cfg, fraud_cfg=fraud_cfg, drop_cfg=drops_cfg, \
                                   time_cfg=time_cfg, run_dir=run_dir)

Объект конфиг класса DropDistributorCfg конфиги для дропов распределителей

In [ ]:
dist_configs = drop_cfg_build.build_dist_cfg()

Объект конфиг класса DropPurchaserCfg конфиги для дропов покупателей

In [8]:
purch_configs = drop_cfg_build.build_purch_cfg()



1. Класс DropTxnPartData - генерация части данных транзакции дропов¶

  • модуль data_generator.fraud.txndata. Ссылка на исходный код в Github

Основной функционал класса

  • генерация части данных транзакции: мерчант id, координат, IP-адреса, города, ID устройства, канала транзакции, типа транзакции
  • берет данные действительно относящиеся к клиенту в плане: координат, IP-адреса, города, ID устройства
In [27]:
from data_generator.fraud.drops.txndata import DropTxnPartData

# пример будет на конфигах дропа распределителя. Для DropTxnPartData это не будет иметь разницы
# т.к. данные, которые он берет из конфигов, по смыслу одинаковы для обоих типов дропов
part_data = DropTxnPartData(configs=dist_configs)

# DropTxnPartData нуждается в данных клиента в виде namedtuple
part_data.client_info  = list(drop_clients.itertuples(name='Row', index=False))[0]
part_data.client_info
Out[27]:
Row(client_id=12092, birth_date=Timestamp('2002-05-09 00:00:00'), sex='male', region='Самарская', city='Самара', lat=53.1951657, lon=50.1067691, city_id=68, home_ip='2.60.20.113')

Демонстрация¶

Реальные данные текущего клиента (дропа)

In [28]:
client_id = part_data.client_info.client_id

drop_data = dist_configs.clients.query("client_id == @client_id")
drop_data
Out[28]:
client_id birth_date sex region city lat lon city_id home_ip
0 12092 2002-05-09 male Самарская Самара 53.195166 50.106769 68 2.60.20.113

1. Метод original_purchase¶

  • один из двух основных методов класса

Пример №1

  • дроп распределитель
  • предыдущей транзакции нет
  • метод check_previous() решает нужно ли взять частичные данные предыдущей транзакции
  • частичные данные предыдущей транзакции берутся если
    • это дроп распределитель
    • есть предыдущая транзакция и это была покупка криптовалюты
  • частичные данные предыдущей транзакции берутся, чтобы был одинаковый merchant_id при покупке криптовалюты т.е. при переводе денег на криптобиржу
  • check_previous() из переданной предыдущей транзакции узнает какой был канал транз-ции
In [36]:
get_cached = part_data.check_previous(dist=True, last_full=None)

# возвращает merchant_id, trans_lat, trans_lon, trans_ip, trans_city, device_id, channel, txn_type
merchant_id, trans_lat, trans_lon, trans_ip, trans_city, device_id, channel, txn_type = \
                            part_data.original_purchase(online=True, get_cached=get_cached)

# Соберем полученные данные в датафрейм для удобства демонстрации
txn_data_1 = pd.DataFrame([build_transaction(client_id=0, txn_time=0, txn_unix=0, amount=0, \
                          txn_type=txn_type, channel=channel, category_name=None, online=True, \
                          merchant_id=merchant_id, trans_city=trans_city, trans_lat=trans_lat, \
                          trans_lon=trans_lon, trans_ip=trans_ip, device_id=device_id, account=None, \
                          is_fraud=True, is_suspicious=False, status=None, rule=None)]) \
                          .drop(columns=['client_id', 'txn_time', 'unix_time', 'amount', 'category', \
                                        'account', 'is_suspicious', 'status', 'rule'])
txn_data_1
Out[36]:
type channel online merchant_id trans_city trans_lat trans_lon trans_ip device_id is_fraud
0 purchase None True 6908 Самара 53.195166 50.106769 2.60.20.113 9420 True

Пример №2

  • дроп распределитель
  • есть предыдущая транзакция с каналом "crypto_exchange"
  • в примере мы берем как-будто бы данные о последней транзакции, но достаточно передать только канал
  • т.к. это дроп распределитель и канал это криптобиржа, то вернутся кэшированные данные предыдущей транзакции
  • см. значение вывода выше и сравните с выводом ниже merchant_id повторяется т.к. вероятнее, что биржа будет та же самая
In [37]:
last_full = {"channel":"crypto_exchange"}
get_cached = part_data.check_previous(dist=True, last_full=last_full)
part_data.original_purchase(online=True, get_cached=get_cached)

merchant_id, trans_lat, trans_lon, trans_ip, trans_city, device_id, channel, txn_type = \
                            part_data.original_purchase(online=True, get_cached=get_cached)

# Соберем полученные данные в датафрейм для удобства демонстрации
txn_data_2 = pd.DataFrame([build_transaction(client_id=0, txn_time=0, txn_unix=0, amount=0, \
                          txn_type=txn_type, channel=channel, category_name=None, online=True, \
                          merchant_id=merchant_id, trans_city=trans_city, trans_lat=trans_lat, \
                          trans_lon=trans_lon, trans_ip=trans_ip, device_id=device_id, account=None, \
                          is_fraud=True, is_suspicious=False, status=None, rule=None)]) \
                          .drop(columns=['client_id', 'txn_time', 'unix_time', 'amount', 'category', \
                                        'account', 'is_suspicious', 'status', 'rule'])

# соединим две записи в один датафрейм и увидим одинаковые данные
pd.concat([txn_data_1,txn_data_2], ignore_index=True)
Out[37]:
type channel online merchant_id trans_city trans_lat trans_lon trans_ip device_id is_fraud
0 purchase None True 6908 Самара 53.195166 50.106769 2.60.20.113 9420 True
1 purchase None True 6908 Самара 53.195166 50.106769 2.60.20.113 9420 True

2. Метод original_data¶

  • второй из двух основных методов класса
  • генерирует частичные данные транз-ции для вх./исх. переводов и для снятий

Пример №1

  • входящая транзакция. Флаги online=True, receive=True
In [44]:
online = True

merchant_id, trans_lat, trans_lon, trans_ip, trans_city, device_id, channel, txn_type = \
                                            part_data.original_data(online=online, receive=True)

# Соберем полученные данные в датафрейм для удобства демонстрации
txn_data = pd.DataFrame([build_transaction(client_id=0, txn_time=0, txn_unix=0, amount=0, \
                          txn_type=txn_type, channel=channel, category_name=None, online=online, \
                          merchant_id=merchant_id, trans_city=trans_city, trans_lat=trans_lat, \
                          trans_lon=trans_lon, trans_ip=trans_ip, device_id=device_id, account=None, \
                          is_fraud=True, is_suspicious=False, status=None, rule=None)]) \
                          .drop(columns=['client_id', 'txn_time', 'unix_time', 'amount', 'category', \
                                        'account', 'is_suspicious', 'status', 'rule'])
txn_data
Out[44]:
type channel online merchant_id trans_city trans_lat trans_lon trans_ip device_id is_fraud
0 inbound transfer True NaN not applicable NaN NaN not applicable NaN True

Пример №2

  • исходящая транзакция. Флаги online=True, receive=False
In [43]:
online = True

merchant_id, trans_lat, trans_lon, trans_ip, trans_city, device_id, channel, txn_type = \
                                            part_data.original_data(online=online, receive=False)

# Соберем полученные данные в датафрейм для удобства демонстрации
txn_data = pd.DataFrame([build_transaction(client_id=0, txn_time=0, txn_unix=0, amount=0, \
                          txn_type=txn_type, channel=channel, category_name=None, online=online, \
                          merchant_id=merchant_id, trans_city=trans_city, trans_lat=trans_lat, \
                          trans_lon=trans_lon, trans_ip=trans_ip, device_id=device_id, account=None, \
                          is_fraud=True, is_suspicious=False, status=None, rule=None)]) \
                          .drop(columns=['client_id', 'txn_time', 'unix_time', 'amount', 'category', \
                                        'account', 'is_suspicious', 'status', 'rule'])
txn_data
Out[43]:
type channel online merchant_id trans_city trans_lat trans_lon trans_ip device_id is_fraud
0 outbound transfer True NaN Самара 53.195166 50.106769 2.60.20.113 9420 True

Пример №3

  • снятие. Флаги online=False, receive=False
In [42]:
online = False

merchant_id, trans_lat, trans_lon, trans_ip, trans_city, device_id, channel, txn_type = \
                                            part_data.original_data(online=online, receive=False)

# Соберем полученные данные в датафрейм для удобства демонстрации
txn_data = pd.DataFrame([build_transaction(client_id=0, txn_time=0, txn_unix=0, amount=0, \
                          txn_type=txn_type, channel=channel, category_name=None, online=online, \
                          merchant_id=merchant_id, trans_city=trans_city, trans_lat=trans_lat, \
                          trans_lon=trans_lon, trans_ip=trans_ip, device_id=device_id, account=None, \
                          is_fraud=True, is_suspicious=False, status=None, rule=None)]) \
                          .drop(columns=['client_id', 'txn_time', 'unix_time', 'amount', 'category', \
                                        'account', 'is_suspicious', 'status', 'rule'])
txn_data
Out[42]:
type channel online merchant_id trans_city trans_lat trans_lon trans_ip device_id is_fraud
0 withdrawal ATM False NaN Самара 53.195166 50.106769 not applicable NaN True



2. Класс DropBaseClasses - создание и агрегация основных классов для дропов¶

  • модуль data_generator.fraud.drops.build.builder. Ссылка на исходный код в Github

Основной функционал

  • Создает объекты классов и держит их в своих атрибутах:
    • DropAccountHandler - управление счетами транзакций
    • DropAmountHandler - управление суммами транзакций
    • DropTimeHandler - управление временем транзакций и его генерация
    • DistBehaviorHandler или PurchBehaviorHandler - управление поведением дропа
    • DropTxnPartData - генерация частичных данных транзакции
  • объект DropBaseClasses предназначен для удобной передачи основных дроп классов при создании объектов других более "высокоуровневых" классов, например:
    • CreateDropTxn - полная генерация одной транзакции
    • DropLifecycleManager - управление созданием активности одного дропа от начала до конца

Основная логика

  • принимает на вход конфиги дропов определенного типа и информацию о типе дропов в виде строки - distributor или purchaser
  • через метод build_all() создает объекты указанных выше классов и записывает их в свои атрибуты
  • также есть отдельные методы на создание объекта каждого класса по отдельности, но используется только build_all()

Демонстрация¶

In [9]:
from data_generator.fraud.drops.build.builder import DropBaseClasses

# Создадим объекты под каждый тип дропа
dist_base = DropBaseClasses(configs=dist_configs, drop_type="distributor")
purch_base = DropBaseClasses(configs=purch_configs, drop_type="purchaser")

Пример №1

  • создать объекты под дропов распределителей distributor
  • разница будет в объекте класса, который управляет поведением, для распределителей это DistBehaviorHandler, а для покупателей это PurchBehaviorHandler
In [46]:
# создаем все объекты
dist_base.build_all()
In [47]:
# смотрим типы
type(dist_base.behav_hand)
Out[47]:
data_generator.fraud.drops.behavior.DistBehaviorHandler
In [48]:
type(dist_base.amt_hand)
Out[48]:
data_generator.fraud.drops.base.DropAmountHandler

Пример №2

  • создать объекты под дропов покупателей purchaser
In [49]:
purch_base.build_all()
In [50]:
type(purch_base.behav_hand)
Out[50]:
data_generator.fraud.drops.behavior.PurchBehaviorHandler



3. Класс CreateDropTxn¶

  • модуль data_generator.fraud.drops.txns. Ссылка на исходный код в Github

Основной функционал

  • создание всех типов транзакций для дропов обоих типов
    • вх./исх переводы
    • снятия
    • покупки
  • определение достигнут ли абсолютный лимит по вх./исх. транзакциям для дропа

Основная логика

  • при создании объекта принимает объект конфиг класса DropDistributorCfg или DropPurchaserCfg и основные классы в виде объекта DropBaseClasses
  • имеет два главных метода для полной генерации транзакций - эти методы принимают некоторые аргументы при их вызове
    • trf_or_atm() - вх./исх. переводы либо снятие
    • purchase - покупки товаров либо криптовалюты
In [51]:
class CreateDropTxn:
    """
    Создание транзакций дропа под разное поведение.
    -----------------
    drop_type: str. 'distributor' или 'purchaser'
    configs: DropDistributorCfg | DropPurchaserCfg.
             Конфиги и данные для создания дроп транзакций.
    txn_part_data: DropTxnPartData. Генератор части данных транзакции - мерчант,
                    гео, ip, девайс и т.п.
    amt_hand: DropAmountHandler. Генератор активности дропов: суммы, счета, баланс.
    acc_hand: DropAccountHandler. Генератор номеров счетов входящих/исходящих транзакций.
                Учет использованных счетов.
    time_hand: DropTimeHandler. Управление временем транзакций дропа
    behav_hand: DistBehaviorHandler. Управление поведением дропа.
    categories: pd.DataFrame. Категории товаров с весами. Для дропов покупателей.
    in_txns: int. Количество входящих транзакций.
    out_txns: int. Количество исходящих транзакций.
    in_lim: int. Лимит входящих транзакций. Транзакции клиента совершенные после 
            достижения этого лимита отклоняются.
    out_lim: int. Лимит исходящих транзакций. Транзакции клиента совершенные 
                после достижения этого лимита отклоняются.
    last_txn: dict. Полные данные последней транзакции. По умолчанию None
    """
    def __init__(self, configs: Union[DropDistributorCfg, DropPurchaserCfg], base: DropBaseClasses):
        """
        configs: DropDistributorCfg | DropPurchaserCfg.
                 Конфиги и данные для создания дроп транзакций.
        base: DropBaseClasses. Объекты основных классов для дропов.
        """
        self.drop_type = base.drop_type
        self.configs = configs
        self.txn_part_data = base.part_data
        self.amt_hand = base.amt_hand
        self.acc_hand = base.acc_hand
        self.time_hand = base.time_hand
        self.behav_hand = base.behav_hand
        self.in_txns = 0
        self.out_txns = 0
        self.in_lim = configs.in_lim
        self.out_lim = configs.out_lim
        if isinstance(self.configs, DropPurchaserCfg):
            self.categories = configs.categories
        self.last_txn = None


    def category_and_channel(self):
        """
        Генерация категории и канала транзакции
        ---------------
        """
        drop_type = self.drop_type
        
        # Перевод на криптобиржу
        if drop_type == "distributor":
            channel = "crypto_exchange"
            category_name = "balance_top_up"
            return channel, category_name
        
        assert drop_type == "purchaser", \
            f"""'ecom' channel and categories sampling work only for self.drop_type as 'purchaser'.
            But {self.drop_type} was passed"""
        
        # Покупка в интернете
        channel = "ecom"
        category_name = self.categories.category \
                            .sample(1, weights=self.categories.weight).iat[0]
        return channel, category_name


    def status_and_rule(self, declined):
        """
        Статус транзакции, флаг is_fraud и правило.
        Зависит от типа дропа self.drop_type
        -----------------
        declined: bool. Будет ли текущая транзакция отклонена.
        """
        drop_type = self.drop_type

        if declined and drop_type == "distributor":
            status = "declined"
            is_fraud = True
            rule = "drop_flow_cashout"
            return status, is_fraud, rule
        
        if not declined and drop_type == "distributor":
            status = "approved"
            is_fraud = False
            rule = "not applicable"
            return status, is_fraud, rule
        
        if declined and drop_type == "purchaser":
            status = "declined"
            is_fraud = True
            rule = "drop_purchaser"
            return status, is_fraud, rule
        
        if not declined and drop_type == "purchaser":
            status = "approved"
            is_fraud = False
            rule = "not applicable"
            return status, is_fraud, rule
        

    def trf_or_atm(self, declined, to_drop, receive=False):
        """
        Один входящий/исходящий перевод либо одно снятие в банкомате.
        ---------------------
        dist: bool. Тип дропа. True - distributor. False - purchaser.
        declined: bool. Будет ли текущая транзакция отклонена.
        receive: bool. Входящий перевод или нет.
        """
        client_id = self.txn_part_data.client_info.client_id # берем из namedtuple
        
        # Время транзакции. Оно должно быть создано до увеличения счетчика self.in_txns
        txn_time, txn_unix = self.time_hand.get_txn_time(receive=receive, in_txns=self.in_txns)

        online = self.behav_hand.online
        in_chunks = self.behav_hand.in_chunks

        # перевод дропу
        if receive:
            self.in_txns += 1
            amount = self.amt_hand.receive(declined=declined)
            account = self.acc_hand.account
            online = True # Тут отдельно прописываем т.к. это вне сценариев поведения самого дропа
        # перевод от дропа    
        elif not receive and online:
            self.out_txns += 1
            account = self.acc_hand.get_account(to_drop=to_drop)
        # снятие дропом
        elif not receive and not online:
            account = self.acc_hand.account
            self.out_txns += 1
        
        # Генерация части данных транзакции. Здесь прописываются аргументы online и receive
        merchant_id, trans_lat, trans_lon, trans_ip, trans_city, device_id, channel, txn_type = \
                                                self.txn_part_data.original_data(online=online, receive=receive)
        
        # Генерация суммы если исходящая транзакция
        # т.к. этот метод и для входящих транзакций
        # а у входящих транзакций своя генерация суммы
        if not receive:
            amount = self.amt_hand.one_operation(online=online, declined=declined, in_chunks=in_chunks)

        status, is_fraud, rule = self.status_and_rule(declined=declined)

        # Статичные характеристики
        is_suspicious = False
        category_name="not applicable"

        # Сборка всех данных в транзакцию и запись как последней транзакции
        self.last_txn = build_transaction(client_id=client_id, txn_time=txn_time, txn_unix=txn_unix, amount=amount, \
                                          txn_type=txn_type, channel=channel, category_name=category_name, online=online, \
                                          merchant_id=merchant_id, trans_city=trans_city, trans_lat=trans_lat, \
                                          trans_lon=trans_lon, trans_ip=trans_ip, device_id=device_id, account=account, \
                                          is_fraud=is_fraud, is_suspicious=is_suspicious, status=status, rule=rule)
        return self.last_txn


    def purchase(self, declined):
        """
        Покупка дропом. На данный момент для крипты.
        --------------
        declined: bool. Будет ли текущая транзакция отклонена.
        dist: bool. Это дроп распределитель или покупатель. 
              У распределителя будет перевод на криптобиржу.
              У покупателя - покупка в интернете.
        """
        client_id = self.txn_part_data.client_info.client_id # берем из namedtuple
        receive = False

        # Время транзакции
        txn_time, txn_unix = self.time_hand.get_txn_time(receive=receive, in_txns=self.in_txns)
        online = self.behav_hand.online
        # self.behav_hand.in_chunks_val() вызывается вовне. До захода в цикл while balance > 0
        in_chunks = self.behav_hand.in_chunks
        self.out_txns += 1

        # Брать ли данные последней транзакции. Для случаев когда это дроп распределитель
        dist = self.drop_type == "distributor"
        get_cached = self.txn_part_data.check_previous(dist=dist, last_full=self.last_txn)

        # Генерация части данных транзакции. Здесь прописывается аргумент online
        # Вместо channel нижнее подчеркивание т.к. этот метод вернет None для channel
        merchant_id, trans_lat, trans_lon, trans_ip, trans_city, device_id, _, txn_type = \
                                            self.txn_part_data.original_purchase(online=online, get_cached=get_cached)
        
        amount = self.amt_hand.one_operation(online=online, declined=declined, in_chunks=in_chunks)

        channel, category_name = self.category_and_channel()
        status, is_fraud, rule = self.status_and_rule(declined=declined)

        # Статичные характеристики
        is_suspicious = False
        account = np.nan

        # Сборка всех данных в транзакцию и запись как послдней транзакции
        self.last_txn = build_transaction(client_id=client_id, txn_time=txn_time, txn_unix=txn_unix, amount=amount, \
                                          txn_type=txn_type, channel=channel, category_name=category_name, online=online, \
                                          merchant_id=merchant_id, trans_city=trans_city, trans_lat=trans_lat, \
                                          trans_lon=trans_lon, trans_ip=trans_ip, device_id=device_id, account=account, \
                                          is_fraud=is_fraud, is_suspicious=is_suspicious, status=status, rule=rule)
        return self.last_txn
        

    def limit_reached(self):
        """
        Проверка достижения лимитов входящих и исходящих транзакций
        Сверка с self.in_lim и self.out_lim
        ------------------------
        Вернет True если какой либо лимит достигнут
        """
        if self.in_lim <= self.in_txns:
            return True
        if self.out_lim <= self.out_txns:
            return True
        return False


    def reset_cache(self, only_counters=False):
        """
        Сброос кэшированных данных
        -------------
        only_counters: bool. Если True будут сброшены: self.in_txns, self.out_txns.
                       Если False то также сбросится информация 
                       о последней транзакции self.last_txn
        """
        self.in_txns = 0
        self.out_txns = 0
        if only_counters:
            return
        
        self.last_txn = None

Демонстрация¶

In [10]:
from data_generator.fraud.drops.txns import CreateDropTxn

Функция чисто для демонстрации

  • Нужно сбрасывать кэши в объектах классов при каждой демонстрации, чтобы не было старых данных от предыдущих примеров
In [11]:
def reset_caches(cr_drop_txn, behav_hand, amt_hand, time_hand, part_data):
    cr_drop_txn.reset_cache()
    behav_hand.reset_cache(all=False)
    amt_hand.reset_cache(all=True) # batch_txns здесь
    time_hand.reset_cache()
    part_data.reset_cache()

1. Дропы распределители¶

  • разделим демонстрацию на две части т.к. нужно создавать отдельные объекты класса DropBaseClasses
In [55]:
# Создаем объекты основных классов
dist_base = DropBaseClasses(configs=dist_configs, drop_type="distributor")
dist_base.build_all()

# Вынесем объекты созданных классов в отдельные переменные т.к. иногда нужно будет обращаться к ним
# для полноценной демонстрации
acc_hand = dist_base.acc_hand
amt_hand = dist_base.amt_hand
part_data = dist_base.part_data
time_hand = dist_base.time_hand
behav_hand = dist_base.behav_hand

# объект класса создания транзакций, который мы демонстрируем
create_txn = CreateDropTxn(configs=dist_configs, base=dist_base)

# возьмем одного клиента т.к. его данные нужно передать в объекты некоторых классов
# он и будет дропом
dist_client = list(dist_configs.clients.itertuples(name="Client", index=False))[0]

part_data.client_info = dist_client
acc_hand.client_id = dist_client.client_id

# запишем номер счета текущего клиента в атрибут acc_hand.account т.к. в некоторых транзакциях указывается счет
acc_hand.get_account(own=True)

# проверка данных
part_data.client_info
Out[55]:
Client(client_id=12092, birth_date=Timestamp('2002-05-09 00:00:00'), sex='male', region='Самарская', city='Самара', lat=53.1951657, lon=50.1067691, city_id=68, home_ip='2.60.20.113')

Метод trf_or_atm()¶

  • вх./исх. переводы, снятия

Пример №1

  • входящий перевод. Тут логика общая и для распределителей и для покупателей
In [62]:
reset_caches(create_txn, behav_hand, amt_hand, time_hand, part_data)

# создание транзакци
receive_txn = create_txn.trf_or_atm(declined=False, to_drop=False, receive=True)

pd.DataFrame([receive_txn])
Out[62]:
client_id txn_time unix_time amount type channel category online merchant_id trans_city trans_lat trans_lon trans_ip device_id account is_fraud is_suspicious status rule
0 12092 2025-01-03 14:55:00 1735916100 17700.0 inbound transfer not applicable True NaN not applicable NaN NaN not applicable NaN 15232 False False approved not applicable

Баланс дропа повысился на сумму перевода

In [63]:
amt_hand.balance
Out[63]:
np.float64(17700.0)

Пример №2

  • НЕ отклоненный исходящий перевод целиком
  • пояснение по проставленным аргументам:
    • receive=False - это не вх. транз.
    • to_drop=False - не пытаемся перевести другому дропу
    • declined=False - транзакция не будет отклонена
In [65]:
# это онлайн транзакция. Для исходящих транзакций/снятий этот флаг берется из атрибута behav_hand.online
behav_hand.online = True
# это транзакция на сумму всего баланса
behav_hand.in_chunks = False

# создаем транзакцию и объединяем записи о входящей созданной ранее транз. и о текущей
whole_out = create_txn.trf_or_atm(receive=False, to_drop=False, declined=False)
pd.concat([pd.DataFrame([receive_txn]), pd.DataFrame([whole_out])], ignore_index=True)
Out[65]:
client_id txn_time unix_time amount type channel category online merchant_id trans_city trans_lat trans_lon trans_ip device_id account is_fraud is_suspicious status rule
0 12092 2025-01-03 14:55:00 1735916100 17700.0 inbound transfer not applicable True NaN not applicable NaN NaN not applicable NaN 15232 False False approved not applicable
1 12092 2025-01-03 17:45:00 1735926300 17700.0 outbound transfer not applicable True NaN Самара 53.195166 50.106769 2.60.20.113 9420.0 23028 False False approved not applicable

Баланс дропа обнулен

In [66]:
amt_hand.balance
Out[66]:
np.float64(0.0)

Пример №3

  • НЕ отклоненное снятие целиком
In [67]:
reset_caches(create_txn, behav_hand, amt_hand, time_hand, part_data)

# это оффлайн транзакция
behav_hand.online = False
behav_hand.in_chunks = False

receive_txn = create_txn.trf_or_atm(declined=False, to_drop=False, receive=True)
whole_atm = create_txn.trf_or_atm(declined=False, to_drop=False, receive=False)

pd.concat([pd.DataFrame([receive_txn]), pd.DataFrame([whole_atm])], ignore_index=True)
Out[67]:
client_id txn_time unix_time amount type channel category online merchant_id trans_city trans_lat trans_lon trans_ip device_id account is_fraud is_suspicious status rule
0 12092 2025-01-03 12:57:00 1735909020 16000.0 inbound transfer not applicable True NaN not applicable NaN NaN not applicable NaN 15232 False False approved not applicable
1 12092 2025-01-03 14:49:00 1735915740 16000.0 withdrawal ATM not applicable False NaN Самара 53.195166 50.106769 not applicable NaN 15232 False False approved not applicable

Пример №4

  • НЕ отклоненный исходящий перевод частями

Пояснение

  • Тут уже нужно задать сценарий - split_transfer и применить метод behav_hand.guide_scenario(), который контролирует behav_hand.online атрибут при генерации каждой транзакции в зависимости от выпавшего сценария. guide_scenario() внедрялся из-за сценария atm+transfer, где behav_hand.online меняется после первой транзакции т.к. первая это снятие, а следущие это онлайн переводы
In [69]:
reset_caches(create_txn, behav_hand, amt_hand, time_hand, part_data)

# определяем сценарий
behav_hand.scen = "split_transfer"
# перевод частями
behav_hand.in_chunks = True
# входящая транз.
receive_txn = create_txn.trf_or_atm(declined=False, to_drop=False, receive=True)

all_txns = [receive_txn]

# дроп будет делать транзакции пока баланс не будет обнулен 
while amt_hand.balance > 0:
    behav_hand.guide_scenario()
    part_out = create_txn.trf_or_atm(declined=False, to_drop=False, receive=False)
    all_txns.append(part_out)
pd.DataFrame(all_txns)
Out[69]:
client_id txn_time unix_time amount type channel category online merchant_id trans_city trans_lat trans_lon trans_ip device_id account is_fraud is_suspicious status rule
0 12092 2025-01-04 14:57:00 1736002620 32700.0 inbound transfer not applicable True NaN not applicable NaN NaN not applicable NaN 15232 False False approved not applicable
1 12092 2025-01-04 17:11:00 1736010660 22000.0 outbound transfer not applicable True NaN Самара 53.195166 50.106769 2.60.20.113 9420.0 17653 False False approved not applicable
2 12092 2025-01-04 18:30:00 1736015400 9000.0 outbound transfer not applicable True NaN Самара 53.195166 50.106769 2.60.20.113 9420.0 19017 False False approved not applicable
3 12092 2025-01-04 19:38:00 1736019480 1700.0 outbound transfer not applicable True NaN Самара 53.195166 50.106769 2.60.20.113 9419.0 17224 False False approved not applicable

Баланс обнулен

In [70]:
amt_hand.balance
Out[70]:
np.float64(0.0)

Пример №5

  • отклоненный перевод
  • в данном случае сделаем пример на исходящих переводах
  • declined=True
  • в итоговом датафрейме смотрите на status и rule. drop_flow_cashout значит что это детект по правилу, что клиент это дроп через которого идут потоки денег, это и есть дроп распределитель
  • баланс дропа не изменится. На нем будет переведенная ему сумма
In [71]:
reset_caches(create_txn, behav_hand, amt_hand, time_hand, part_data)

behav_hand.scen = "transfer"
behav_hand.in_chunks = False

receive_txn = create_txn.trf_or_atm(declined=False, to_drop=False, receive=True)
all_txns = [receive_txn]
i = 0

while amt_hand.balance > 0 and i < 4:
    behav_hand.guide_scenario()
    txn_out = create_txn.trf_or_atm(declined=True, to_drop=False, receive=False)
    all_txns.append(txn_out)
    i += 1
pd.DataFrame(all_txns)
Out[71]:
client_id txn_time unix_time amount type channel category online merchant_id trans_city trans_lat trans_lon trans_ip device_id account is_fraud is_suspicious status rule
0 12092 2025-01-07 08:41:00 1736239260 28300.0 inbound transfer not applicable True NaN not applicable NaN NaN not applicable NaN 15232 False False approved not applicable
1 12092 2025-01-07 11:02:00 1736247720 28300.0 outbound transfer not applicable True NaN Самара 53.195166 50.106769 2.60.20.113 9420.0 23535 True False declined drop_flow_cashout
2 12092 2025-01-07 13:46:00 1736257560 21300.0 outbound transfer not applicable True NaN Самара 53.195166 50.106769 2.60.20.113 9419.0 24245 True False declined drop_flow_cashout
3 12092 2025-01-07 15:35:00 1736264100 14300.0 outbound transfer not applicable True NaN Самара 53.195166 50.106769 2.60.20.113 9419.0 16982 True False declined drop_flow_cashout
4 12092 2025-01-07 16:53:00 1736268780 7300.0 outbound transfer not applicable True NaN Самара 53.195166 50.106769 2.60.20.113 9419.0 20795 True False declined drop_flow_cashout

Баланс дропа не изменился. Ему не дали перевести полученные деньги

In [72]:
amt_hand.balance
Out[72]:
np.float64(28300.0)

Метод CreateTxn.purchase()¶

  • напомню, что пока это демонстрация, как это работает у дропов распределителей

Пример

  • НЕ отклоненная покупка крипты на все деньги
In [74]:
reset_caches(create_txn, behav_hand, amt_hand, time_hand, part_data)
acc_hand.reset_cache()

# покупка крипты проходит под сценарием transfer или split_transfer
behav_hand.scen = "transfer"
# покупка разом на всю сумму
behav_hand.in_chunks = False

# получение денег
receive_txn = create_txn.trf_or_atm(declined=False, to_drop=False, receive=True)
all_txns = [receive_txn]

behav_hand.guide_scenario()
whole_out = create_txn.purchase(declined=False)
all_txns.append(whole_out)
all_df = pd.DataFrame(all_txns)
all_df
Out[74]:
client_id txn_time unix_time amount type channel category online merchant_id trans_city trans_lat trans_lon trans_ip device_id account is_fraud is_suspicious status rule
0 12092 2025-01-09 13:45:00 1736430300 39700.0 inbound transfer not applicable True NaN not applicable NaN NaN not applicable NaN 15232.0 False False approved not applicable
1 12092 2025-01-09 15:17:00 1736435820 39700.0 purchase crypto_exchange balance_top_up True 6846.0 Самара 53.195166 50.106769 2.60.20.113 9419.0 NaN False False approved not applicable

Баланс обнулен

In [75]:
amt_hand.balance
Out[75]:
np.float64(0.0)

1. Дроп покупатель¶

  • у дропа покупателя будет демонстрация только метода purchase() т.к. метод trf_or_atm() применяется только для входящих транзакций, это было продемонстрировано на примере дропов распределителей
  • создадим объекты нужных классов заново, но уже под дропов покупателей
In [12]:
# Создаем объекты основных классов. Теперь drop_type="purchaser"
# и purch_configs вместо dist_configs
purch_base = DropBaseClasses(configs=purch_configs, drop_type="purchaser")
purch_base.build_all()

# Вынесем объекты созданных классов в отдельные переменные т.к. иногда нужно будет обращаться к ним
# для полноценной демонстрации
acc_hand = purch_base.acc_hand
amt_hand = purch_base.amt_hand
part_data = purch_base.part_data
time_hand = purch_base.time_hand
behav_hand = purch_base.behav_hand

# объект класса создания транзакций, который мы демонстрируем
create_txn = CreateDropTxn(configs=purch_configs, base=purch_base)

# возьмем одного клиента т.к. его данные нужно передать в объекты некоторых классов
# он и будет дропом
purch_client = list(dist_configs.clients.itertuples(name="Client", index=False))[3]

part_data.client_info = purch_client
acc_hand.client_id = purch_client.client_id

# запишем номер счета текущего клиента в атрибут acc_hand.account т.к. в некоторых транзакциях указывается счет
acc_hand.get_account(own=True)

# проверка данных
part_data.client_info
Out[12]:
Client(client_id=662, birth_date=Timestamp('1970-08-01 00:00:00'), sex='male', region='Ростовская', city='Ростов-на-Дону', lat=47.2224364, lon=39.7187866, city_id=5, home_ip='2.60.2.119')

Метод purchase()¶

Пример №1

  • НЕ отклоненная покупка целиком
In [13]:
reset_caches(create_txn, behav_hand, amt_hand, time_hand, part_data)

all_txns = []

receive_txn = create_txn.trf_or_atm(declined=False, to_drop=False, receive=True)
all_txns.append(receive_txn)

behav_hand.in_chunks = False

while amt_hand.balance > 0:
    txn_out = create_txn.purchase(declined=False)
    all_txns.append(txn_out)
    
all_df = pd.DataFrame(all_txns)
all_df
Out[13]:
client_id txn_time unix_time amount type channel category online merchant_id trans_city trans_lat trans_lon trans_ip device_id account is_fraud is_suspicious status rule
0 662 2025-01-15 15:08:00 1736953680 25700.0 inbound transfer not applicable True NaN not applicable NaN NaN not applicable NaN 10630.0 False False approved not applicable
1 662 2025-01-15 15:52:00 1736956320 25700.0 purchase ecom shopping_net True 6949.0 Ростов-на-Дону 47.222436 39.718787 2.60.2.119 1109.0 NaN False False approved not applicable

Пример №2

  • НЕ отклоненная покупка частями
In [14]:
reset_caches(create_txn, behav_hand, amt_hand, time_hand, part_data)

all_txns = []

receive_txn = create_txn.trf_or_atm(declined=False, to_drop=False, receive=True)
all_txns.append(receive_txn)

behav_hand.in_chunks = True

while amt_hand.balance > 0:
    txn_out = create_txn.purchase(declined=False)
    all_txns.append(txn_out)
    
all_df = pd.DataFrame(all_txns)
all_df
Out[14]:
client_id txn_time unix_time amount type channel category online merchant_id trans_city trans_lat trans_lon trans_ip device_id account is_fraud is_suspicious status rule
0 662 2025-01-02 03:25:00 1735788300 50400.0 inbound transfer not applicable True NaN not applicable NaN NaN not applicable NaN 10630.0 False False approved not applicable
1 662 2025-01-02 04:57:00 1735793820 25000.0 purchase ecom shopping_net True 6875.0 Ростов-на-Дону 47.222436 39.718787 2.60.2.119 1109.0 NaN False False approved not applicable
2 662 2025-01-02 07:38:00 1735803480 16000.0 purchase ecom shopping_net True 6906.0 Ростов-на-Дону 47.222436 39.718787 2.60.2.119 1109.0 NaN False False approved not applicable
3 662 2025-01-02 10:06:00 1735812360 9400.0 purchase ecom shopping_net True 6814.0 Ростов-на-Дону 47.222436 39.718787 2.60.2.119 1109.0 NaN False False approved not applicable

Пример №3

  • отклоненная покупка
  • обратите внимание на rule. drop_purchaser, тут говорит само за себя, это дроп покупатель.
In [17]:
reset_caches(create_txn, behav_hand, amt_hand, time_hand, part_data)

all_txns = []

receive_txn = create_txn.trf_or_atm(declined=False, to_drop=False, receive=True)
all_txns.append(receive_txn)

behav_hand.online = True
behav_hand.in_chunks = False

i = 0
while amt_hand.balance > 0 and i < 4:
    txn_out = create_txn.purchase(declined=True)
    all_txns.append(txn_out)
    i += 1
    
all_df = pd.DataFrame(all_txns)
all_df
Out[17]:
client_id txn_time unix_time amount type channel category online merchant_id trans_city trans_lat trans_lon trans_ip device_id account is_fraud is_suspicious status rule
0 662 2025-01-08 07:26:00 1736321160 54200.0 inbound transfer not applicable True NaN not applicable NaN NaN not applicable NaN 10630.0 False False approved not applicable
1 662 2025-01-08 08:24:00 1736324640 54200.0 purchase ecom shopping_net True 6930.0 Ростов-на-Дону 47.222436 39.718787 2.60.2.119 1109.0 NaN True False declined drop_purchaser
2 662 2025-01-08 10:24:00 1736331840 40700.0 purchase ecom shopping_net True 6879.0 Ростов-на-Дону 47.222436 39.718787 2.60.2.119 1109.0 NaN True False declined drop_purchaser
3 662 2025-01-08 12:11:00 1736338260 27200.0 purchase ecom shopping_net True 6935.0 Ростов-на-Дону 47.222436 39.718787 2.60.2.119 1109.0 NaN True False declined drop_purchaser
4 662 2025-01-08 13:37:00 1736343420 13700.0 purchase ecom misc_net True 6824.0 Ростов-на-Дону 47.222436 39.718787 2.60.2.119 1109.0 NaN True False declined drop_purchaser