Генерация легальных транзакций¶

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

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

  • В этом ноутбуке демонстрация основных функций и классов относящихся к генерации легальных транзакций:
    • генерация времени, генерация одной транзакции, многих транзакций, запись транзакций в файл
In [14]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import os
import geopandas as gpd
from scipy.stats import truncnorm, norm
import pyarrow
import yaml
from data_generator.utils import load_configs, create_txns_df
from data_generator.general_time import *
from data_generator.legit.time.utils import log_check_min_time
In [2]:
np.set_printoptions(suppress=True)
pd.set_option('display.max_columns', None)
In [3]:
os.chdir("..")
os.getcwd()
Out[3]:
'C:\\Users\\iaros\\My_documents\\Education\\projects\\fraud_detection_01'
In [73]:
# Базовые конфиги
base_cfg = load_configs("./config/base.yaml")
# Настройки легальных транзакций
legit_cfg = load_configs("./config/legit.yaml")
# Настройки времени
time_cfg = load_configs("./config/time.yaml") 

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

Создание конфиг класса с конфигами и данными для генерации¶



1. Конструктор конфиг класса LegitConfigBuilder¶

  • модуль data_generator.legit.build.config . Ссылка на исходный код в Github
  • Принимает на вход словари с данными из конфиг файлов для создания объекта
  • Метод build_cfg() создает объект конфиг класса легальных транзакций с данными и конфигами для генерации легальных транзакций, например:
    • датафреймами с данными - timestamp-ы для семплирования времени, данные выбранных клиентов, мерчанты; путями к директориям для записи файлов

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

In [15]:
from data_generator.legit.build.config import LegitConfigBuilder
from data_generator.runner.utils import make_dir_for_run
In [190]:
# Нужно создать директорию в которой хранятся файлы целого запуска генератора всех транзакций
# Т.к. билдеру необходимо знать этот путь
run_dir = make_dir_for_run(base_cfg) 
builder = LegitConfigBuilder(base_cfg, legit_cfg, time_cfg, run_dir) # передаем конфиги и путь
configs = builder.build_cfg() # Создаем объект конфиг класса

Примеры атрибутов созданного конфиг класса

In [18]:
# Семпл клиентов под генерацию легальных транзакций
configs.clients.head(3)
Out[18]:
client_id birth_date sex region city lat lon city_id home_ip
0 2269 1988-11-03 female Кемеровская Кемерово 55.390972 86.046786 48 2.60.8.101
1 2529 1964-06-15 male Новосибирская Новосибирск 55.028102 82.921058 70 2.60.9.97
2 4768 1985-03-28 female Москва Москва 55.753879 37.620373 1 2.60.17.157
In [19]:
# Диапазон timestamp-ов под генерацию случайного времени
configs.timestamps.head(3)
Out[19]:
timestamp hour unix_time
0 2025-01-01 00:00:00 0 1735689600
1 2025-01-01 00:01:00 0 1735689660
2 2025-01-01 00:02:00 0 1735689720




Функции генерации времени легальных транзакций¶



1. Функция check_min_interval_from_near_txn¶

  • модуль data_generator.legit.time.time. Ссылка на исходный код в Github
  • проверка что сгенерированное время создаваемой транзакции не ближе по времени к другим транзакциям чем выставлено в минимальных интервалах в конфиг файле legit.yaml
  • подразумевается вызов функции когда есть предыдущие транзакции

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

  1. получает на вход семплированное ранее время для создаваемой транзакции - и другие аргументы
  2. проверяет есть ли среди уже созданных транзакций такие, которые по времени ближе допутимого - мин. допустимые интервалы зависят от того онлайн или оффлайн создаваемая транзакция и от онлайн/оффлайн статуса уже созданных транзакций. Для разных отношений онлайн статусов, разные мин. интервалы.
  3. Если есть любые транзакции, которые ближе допустимого по времени, то проверяем онлайн статус последней по времени транзакции и в зависимости от отношения онлайн статусов генерируемой транзакции и последней создаем случайную дельту времени в соответствии с установленными мин. и макс. лимитами. Например: отношение онлайн-онлайн, создать дельту от 6 до 30 минут; оффлайн-онлайн создать дельту от 30 до 60 минут и т.п.
  4. Затем эту дельту прибавляем ко времени последней транзакции. Это и будет время текущей транзакции.
  5. Если нет транзакций близких по времени меньше допустимого, то просто возвращаем семплированное время

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

In [27]:
from data_generator.legit.time.time import check_min_interval_from_near_txn
from data_generator.legit.build.config import LegitConfigBuilder

# Создадим пустой датафрейм под транзакции с ограниченными колонками
trans_time_test = create_txns_df(base_cfg["txns_df"]).loc[:, ['client_id', 'txn_time', 'unix_time','online', 'is_fraud',]]
print("ready for tests")
ready for tests

Кейс¶

  • Текущая транзакция онлайн
  • Ближайшая к ней транзакция - онлайн. Она ближе допустимой online_time_diff
  • Последняя транзакция - оффлайн.

Ожидается:

  • детект недопустимого минимального интервала с ближайшей транзакцией
  • создание нового времени через создание дельты времени с учетом того что текущая транзакция онлайн, а последняя оффлайн и прибавление этой дельты ко времени последней транзакции

Какие мин. интервалы выставлены в конфигах для легальных транзакций
Возьмем только несколько примеров:

  • online_time_diff мин. разница между онлайн транз
  • online_ceil макс. разница от последней онлайн транз. если текущая онлайн.
  • general_diff мин. разница между онлайн и оффлайн транз.
  • general_ceil макс. разница от последней транз. если online статусы разные (оффлайн-онлайн, онлайн-оффлайн)
In [22]:
min_intervals = legit_cfg["time"]["min_intervals"]
online_time_diff = min_intervals["online_time_diff"]
online_ceil = min_intervals["online_ceil"]
general_diff = min_intervals["general_diff"]
general_ceil = min_intervals["general_ceil"]
print(f"""Время в минутах
online_time_diff: {online_time_diff} 
online_ceil: {online_ceil}
general_diff: {general_diff}
general_ceil: {general_ceil}""")
Время в минутах
online_time_diff: 6 
online_ceil: 60
general_diff: 30
general_ceil: 90
In [23]:
# Условно семплированное время создаваемой транзакции, которое нуждается в проверке
timstamp_check_min = pd.to_datetime("2025-01-31 08:19:00", format="%Y-%m-%d %H:%M:%S")
timstamp_check_min_unix = pd_timestamp_to_unix(timstamp_check_min)
timestamp_sample_check_min = pd.DataFrame([{"timestamp":timstamp_check_min, "unix_time":timstamp_check_min_unix}])

# Время ближайшей транзакции. 5 минут разницы. Допустимая разница 6 минут и более
nearest_time = pd.to_datetime("2025-01-31 08:14:00", format="%Y-%m-%d %H:%M:%S")
nearest_unix = pd_timestamp_to_unix(nearest_time)
print(f"nearest: {nearest_time}, {nearest_unix}")

# Время последней транзакции
last_time = pd.to_datetime("2025-01-31 09::00", format="%Y-%m-%d %H:%M:%S")
last_unix = pd_timestamp_to_unix(last_time)
print(f"last: {last_time}, {last_unix}")

timestamp_sample_check_min
nearest: 2025-01-31 08:14:00, 1738311240
last: 2025-01-31 09:28:00, 1738315680
Out[23]:
timestamp unix_time
0 2025-01-31 08:19:00 1738311540

Заполняем датафрейм

  • с условно уже сущействующими ближайшей и последней транзакциями
  • указываем client_id, время и онлайн статус и какая это транзакция в контексте демонстрации
  • Выставить online флаги для ближайшей и последней в соответствии с кейсом: True и False
In [28]:
trans_time_test.loc[1, ["client_id", "txn_time","unix_time", "online", "txn_type"]] = 28, nearest_time, nearest_unix, True, "closest"
trans_time_test.loc[2, ["client_id", "txn_time","unix_time", "online", "txn_type"]] = 28, last_time, last_unix, False, "last"
trans_time_test
Out[28]:
client_id txn_time unix_time online is_fraud txn_type
1 28.0 2025-01-31 08:14:00 1.738311e+09 True NaN closest
2 28.0 2025-01-31 09:28:00 1.738316e+09 False NaN last

Запуск функции

In [29]:
# Выставить в функции аргумент online в соответсвии с тест-кейсом
# True - создаваемая транзакция - онлайн. False - оффлайн
txn_time, txn_unix = check_min_interval_from_near_txn(client_txns=trans_time_test, timestamp_sample=timestamp_sample_check_min, \
                                                      online=True, round_clock=True, configs=configs)

# Запись сгенерированной транзакции в датафрейм: время, online флаг
trans_time_test.loc[3, ["client_id", "txn_time","unix_time", "online", "txn_type"]] = 28, txn_time, txn_unix, True, "current"
trans_time_test = trans_time_test.sort_values("txn_time")

# Расчет времени между транзакциями в минутах
trans_time_test["abs_time_proximity"] = trans_time_test.unix_time.sub(trans_time_test.unix_time.shift(1)).div(60)
trans_time_test
Out[29]:
client_id txn_time unix_time online is_fraud txn_type abs_time_proximity
1 28.0 2025-01-31 08:14:00 1.738311e+09 True NaN closest NaN
2 28.0 2025-01-31 09:28:00 1.738316e+09 False NaN last 74.000000
3 28.0 2025-01-31 10:22:04 1.738319e+09 True NaN current 54.066667

Как видно выше, текущая транзакция получила новое время на основе времени последней транзакции. Ко времени последней был прибавлено 54 минуты что в рамках границ для случайной разницы во времени между онлайн и оффлайн транзакциями - 30-90 минут.



2. Функция get_legit_txn_time¶

  • модуль data_generator.legit.time.time. Ссылка на исходный код в Github
  • генерация времени легальной транзакции

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

  1. проверяет есть ли созданные ранее транзакции
  2. если нет ни одной то просто семплирует время датафрейма с timestamp-ами и возвращает его
  3. если есть хотя бы одна транзакция, то семплирует время и проверяет его на нарушение минимальных интервалов времени между ним и уже имеющимися транзакциями через функцию check_min_interval_from_near_txn. Если нарушены интервалы, то создается другое время на основании времени последней транзакции, тоже через check_min_interval_from_near_txn и возвращается как результат.
In [64]:
def get_legit_txn_time(trans_df, time_weights, configs, round_clock, online=None):
    """
    Генерация времени для легальной транзакции
    ------------------------------------------
    trans_df: pd.DataFrame. Транзакции текущего клиента. Откуда брать информацию по предыдущим транзакциям клиента
    time_weights: pd.DataFrame. Веса часов в периоде времени
    configs: LegitCfg. Конфиги и данные для генерации легальных транзакций. 
    round_clock: bool. Круглосуточная или дневная категория.
    online: bool. Онлайн или оффлайн покупка. True or False
    -------------------------------------------
    Возвращает время для генерируемой транзакции в виде pd.Timestamp и в виде unix времени
    """
    timestamps = configs.timestamps
    timestamps_1st = configs.timestamps_1st

    # Время последней транзакции клиента. pd.Timestamp и unix в секундах
    last_txn_time = trans_df.txn_time.max()
    
    # Если нет никакой предыдущей транзакции т.е. нет последнего времени совсем
    if last_txn_time is pd.NaT:
        # время транзакции в виде timestamp и unix time.
        return sample_time_for_trans(timestamps=timestamps_1st, time_weights=time_weights)

    # Если есть предыдущая транзакция

    # берем случайный час передав веса часов для соответсвующейго временного паттерна
    txn_hour = time_weights.hours.sample(n=1, weights=time_weights.proportion, replace=True).iloc[0]
    
    # фильтруем по этому часу timestamp-ы и семплируем timestamp уже с равной вероятностью
    # Дальше будем обрабатывать этот timestamp в некоторых случаях
    timestamps_subset = timestamps.loc[timestamps.hour == txn_hour]
    timestamp_sample = timestamps_subset.sample(n=1, replace=True)

    # check_min_interval_from_near_txn проверит ближайшие к timestamp_sample по времени транзакции
    # в соответствии с установленными интервалами и если время до ближайшей транзакции меньше 
    # допустимогшо, то создаст другой timestamp. сли интервал допустимый, то вернет исходный timestamp
    txn_time, txn_unix = check_min_interval_from_near_txn(client_txns=trans_df, timestamp_sample=timestamp_sample, \
                                                          online=online, round_clock=round_clock, configs=configs)
    return txn_time, txn_unix




Функции генерации остальных данных транзакции и самих транзакций¶

  • Вспомогательные и самостоятельные


1. Функция генератор локации и мерчанта транзакции get_txn_location_and_merchant¶

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

Возвращает:

  • id мерчанта, координаты транзакции, ip адрес транзакции если это применимо и город транзакции

Основная логика
В зависимости от online флага текущей транзакции генерирует данные немного по-разному

  1. онлайн покупка: семплирование id онлайн мерчанта, координаты просто координаты города клиента, ip просто ip клиента, город - город клиента
  2. оффлайн покупка семплирует данные оффлайн мерчанта и оттуда берет: id мерчанта, его координаты как координаты транзакции

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

In [189]:
# Импорт самой функции
from data_generator.legit.txndata import get_txn_location_and_merchant
In [37]:
# Возьмем нужные данные из конфиг класса созданного ранее

offline_merchants = configs.offline_merchants
clients = configs.clients
In [32]:
# namedtuple с информацией об одном клиенте для примера
for row in clients.iloc[[0]].itertuples():
    one_client_info = row
one_client_info
Out[32]:
Pandas(Index=0, client_id=2269, birth_date=Timestamp('1988-11-03 00:00:00'), sex='female', region='Кемеровская', city='Кемерово', lat=55.3909721, lon=86.0467864, city_id=48, home_ip='2.60.8.101')
In [34]:
# Фильтруем оффлайн мерчантов по городу клиента т.к. на вход подается отфильтрованный датафрейм
offline_merchants_test_one_txn = offline_merchants[offline_merchants["city"] == one_client_info.city]
offline_merchants_test_one_txn.head(2)
Out[34]:
city city_id category merchant_id merchant_lat merchant_lon
319 Кемерово 48 gas_transport 320.0 55.288288 86.073445
320 Кемерово 48 grocery_pos 321.0 55.305180 86.101067
In [185]:
# Вызов get_txn_location_and_merchant - оффлайн покупка

get_txn_location_and_merchant(online=False, merchants_df=offline_merchants_test_one_txn, category_name="gas_transport", \
                              client_info=one_client_info, configs=configs)
# Получаем offline merchant id, его широта и долгота, значение для колонки ip, город транзакции
Out[185]:
(np.float64(4555.0),
 np.float64(55.388418458753),
 np.float64(86.168305358768),
 'not applicable',
 'Кемерово')
In [42]:
# Вызов get_txn_location_and_merchant - онлайн покупка

get_txn_location_and_merchant(online=True, merchants_df=offline_merchants_test_one_txn, category_name="gas_transport", \
                              client_info=one_client_info, configs=configs)
# Получаем online merchant id, широта и долгота города клиента(координаты транзакции), ip клиента, город клиента(город транзакции)
Out[42]:
(np.int64(6858), 55.3909721, 86.0467864, '2.60.8.101', 'Кемерово')


2. Функция генератор одной легальной транзакции generate_one_legit_txn¶

  • модуль data_generator.legit.txns. Ссылка на исходный код в Github
  • Полное создание одной легальной транзакции. Возвращает словарь с данными для каждого поля в датафрейме транзакций

Основные действия функции

  1. Создать случайную сумму транзакции по нормальному распределению в соот-вии с характеристиками категории покупки
  2. получить данные мерчанта, локации транзакции и ip от get_txn_location_and_merchant
  3. Определить типа распределения времени к которому относится категория: круглосуточная оффлайн, онлайн, оффлайн дневная и получить время от get_legit_txn_time
  4. Если это онлайн то семплировать id девайса клиента
  5. Задать канал транзакции в зависимости от онлайн статуса
  6. Задать значения для статичных полей, которые неизменны для легальных транзакций: is_fraud, txn_type (всегда purchase), rule и др.
  7. Собрать все значения в словарь через функцию build_transaction и вернуть их
In [43]:
def generate_one_legit_txn(client_info, client_trans_df, client_device_ids, category, \
                           merchants_df, configs):
    """
    Генерация одной легальной транзакции покупки для клиента.
    ------------------------------------------------
    client_info: namedtuple, полученная в результате итерации с помощью
                 .itertuples() через датафрейм с информацией о клиентах.
    client_trans_df: pd.DataFrame. Транзакции клиента.
    client_device_ids: pd.Series. id девайсов клиента.
    category: pd.DataFrame. Одна запись с категорией и её характеристиками.
    merchants_df: pd.DataFrame. Оффлайн мерчанты заранее отфильтрованные по
                  городу клиента т.к. это легальные транзакции.
    configs: LegitCfg. Конфиги и данные для генерации легальных транзакций.
    """
    all_time_weights = configs.all_time_weights

    client_id = client_info.client_id
    
    category_name = category["category"].iloc[0]
    round_clock = category["round_clock"].iloc[0]
    online = category["online"].iloc[0]
    # средняя сумма для этой категории
    amt_mean = category["avg_amt"].iloc[0]
    # стандартное отклонение сумм для этой категории
    amt_std = category["amt_std"].iloc[0]
    
    # случайно сгенерированная сумма транзакции, но не менее 1
    amount = max(1, round(np.random.normal(amt_mean, amt_std), 2))
    amount = amt_rounding(amount=amount, rate=0.6) # Случайное целочисленное округление

    # 1. Offline_24h_Legit - круглосуточные оффлайн покупки
    if not online and round_clock:
        weights_key = "Offline_24h_Legit"
        channel = "POS"
        device_id = np.nan
        
    # 2. Online_Legit - Онлайн покупки
    elif online:
        weights_key = "Online_Legit"
        # локация клиента по IP. Т.к. это не фрод. Просто записываем координаты города клиента
        channel = "ecom"
        device_id = client_device_ids.sample(n=1).iloc[0]
        
    # 3. Offline_Day_Legit - Оффлайн покупки. Дневные категории.
    elif not online and not round_clock:
        weights_key = "Offline_Day_Legit"
        channel = "POS"
        device_id = np.nan
        
    # Генерация мерчанта, координат транзакции. И если это онлайн, то IP адреса с которого сделана транзакция
    merchant_id, trans_lat, trans_lon, trans_ip, trans_city = \
                                get_txn_location_and_merchant(online=online, merchants_df=merchants_df, \
                                                              category_name=category_name, client_info=client_info, \
                                                              configs=configs)
    
    time_weights = all_time_weights[weights_key]["weights"]
    
    # Генерация времени транзакции
    txn_time, txn_unix = get_legit_txn_time(trans_df=client_trans_df, time_weights=time_weights, \
                                            configs=configs, round_clock=round_clock, online=online)
    # Статичные значения для данной функции.
    status = "approved"
    txn_type = "purchase"
    is_fraud = False
    is_suspicious = False
    account = np.nan
    rule = "not applicable"
    
    # Возвращаем словарь со всеми данными сгенерированной транзакции
    return 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)

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

In [61]:
# Импорт функций которые используются в generate_one_legit_txn
from data_generator.utils import build_transaction, amt_rounding
from data_generator.legit.time.time import get_legit_txn_time
from data_generator.legit.txndata import get_txn_location_and_merchant
In [50]:
client_devices = configs.client_devices
In [49]:
for row in clients.iloc[[0]].itertuples():
    one_client_info = row
one_client_info
Out[49]:
Pandas(Index=0, client_id=2269, birth_date=Timestamp('1988-11-03 00:00:00'), sex='female', region='Кемеровская', city='Кемерово', lat=55.3909721, lon=86.0467864, city_id=48, home_ip='2.60.8.101')
In [51]:
# id устройств которые принадлежат клиенту
device_id_demo = client_devices.loc[client_devices.client_id == one_client_info.client_id, "device_id"]
device_id_demo
Out[51]:
2148    3848
7068    3849
Name: device_id, dtype: int64
In [52]:
# Датафрейм под запись транзакций
client_trans_df = create_txns_df(base_cfg["txns_df"])
client_trans_df
Out[52]:
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

Онлайн пример

In [56]:
# выберем одну категорию и возьмем запись с ее характеристиками
category_demo_on_txn = cat_stats_full.query("category == 'shopping_net'")
category_demo_on_txn
Out[56]:
category avg_amt amt_std cat_count online share fraud_count fraud_share round_clock
5 shopping_net 1252.224798 3558.296372 41779 True 0.07518 506 0.012111 False
In [64]:
one_txn_demo = generate_one_legit_txn(client_info=one_client_info, client_trans_df=client_trans_df, \
                                      client_device_ids=device_id_demo, category=category_demo_on_txn, \
                                      merchants_df=offline_merchants_test_one_txn, configs=configs)
pd.DataFrame([one_txn_demo])
Out[64]:
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 2269 2025-01-06 14:54:00 1736175240 3293.61 purchase ecom shopping_net True 6923 Кемерово 55.390972 86.046786 2.60.8.101 3849 NaN False False approved not applicable

Оффлайн пример

In [65]:
# выберем одну категорию и возьмем запись с ее характеристиками
category_demo_on_txn2 = cat_stats_full.query("category == 'grocery_pos'")
category_demo_on_txn2
Out[65]:
category avg_amt amt_std cat_count online share fraud_count fraud_share round_clock
1 grocery_pos 1738.279905 773.284951 52553 False 0.094568 485 0.009229 False
In [66]:
one_txn_demo2 = generate_one_legit_txn(client_info=one_client_info, client_trans_df=client_trans_df, \
                                      client_device_ids=device_id_demo, category=category_demo_on_txn2, \
                                      merchants_df=offline_merchants_test_one_txn, configs=configs)
pd.DataFrame([one_txn_demo2])
Out[66]:
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 2269 2025-01-01 18:18:00 1735755480 921.0 purchase POS grocery_pos False 1168.0 Кемерово 55.410074 86.060446 not applicable NaN NaN False False approved not applicable


3. Класс LegitTxnsRecorder¶

  • модуль data_generator.legit.recorder. Ссылка на исходный код в Github
  • Пишет легальные транзакции в файл

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

  • демонстрации работы не будет, просто опишу его функционал
  1. Создает поддиректорию chunks в директории текущей генерации легальных транзакций
  2. Пишет транзакции в файлы чанками в эту поддиректорию. Размер чанков определяется в конфигах legit.yaml
  3. Читает все созданные чанки и собирает в единый датафрейм
  4. Записывает этот датафрейм в две директории: текущего запуска генератора транзакций и последнего запуска генератора

Подразумевается встраивание recorder-а в функцию генерации множества легальных транзакций. Сам по себе он не определяет логику когда писать/читать транзакции.



4. Функция генерации нескольких легальных транзакций gen_multiple_legit_txns¶

  • модуль data_generator.legit.txns. Ссылка на исходный код в Github
  • Основная функция для генерации легальных транзакций

Основная логика работы

  1. Итерирование через семпл клиентов хранящихся в configs.clients
  2. Генерация случайного числа транзакций для каждого клиента. Число транзакций берется из обрезанного норм. распределения. Параметры распределения указаны в legit.yaml
  3. Запись создаваемых транзакций чанками
  4. В конце чтение чанков и сборка единого датафрейма, запись этого датафрейма в файл
In [168]:
def gen_multiple_legit_txns(configs, txn_recorder, ignore_index=True):
    """
    Генерирует несколько транзакций для каждого клиента ориентируясь 
    на существующие транзакции если они есть.
    Количество на клиента берется по нормальному распределению с 
    указанными средним и стандартным отклонением.
    Ограничение забито в функцию gen_trans_number_norm: от 1 до 120 транзакций.
    ---------------------------------------------------
    configs: LegitCfg. Конфиги и данные для генерации легальных транзакций.
    txn_recorder: LegitTxnsRecorder. 
    ignore_index: bool. Сбросить ли индекс при конкатенации датафреймов
                  в финальный датафрейм с транзакциями всех клиентов
    """
    clients_df = configs.clients
    trans_df = configs.transactions
    client_devices = configs.client_devices
    offline_merchants = configs.offline_merchants
    categories = configs.categories
    avg_txn_num = configs.txn_num["avg_txn_num"]
    txn_num_std = configs.txn_num["txn_num_std"]
    low_bound = configs.txn_num["low_bound"]
    up_bound = configs.txn_num["up_bound"]
    
    # Сюда будем собирать сгенрированные транзакции клиента в виде словарей.
    client_txns = txn_recorder.client_txns
    
    for client_info in clients_df.itertuples():
        txn_recorder.clients_counter += 1

        # случайное кол-во транзакций на клиента взятое из нормального распределения с мин. и макс. лимитами
        txns_num = gen_trans_number_norm(avg_num=avg_txn_num, num_std=txn_num_std, low_bound=low_bound, \
                                             up_bound=up_bound)
        merchants_from_city = offline_merchants[offline_merchants["city"] == client_info.city]
        client_transactions = trans_df.loc[trans_df.client_id == client_info.client_id]
        
        # id девайсов клиента для онлайн транзакций
        client_device_ids = client_devices.loc[client_devices.client_id == client_info.client_id, "device_id"]
        
        for _ in range(txns_num):
            # семплирование категории для транзакции
            category = categories.sample(1, replace=True, weights=categories.share)

            # генерация одной транзакции
            one_txn = generate_one_legit_txn(client_info=client_info, client_trans_df=client_transactions, \
                                             category=category, client_device_ids=client_device_ids, \
                                             merchants_df=merchants_from_city, configs=configs)
            # Запись транз-ции в список транз-ций текущего клиента.
            client_txns.append(one_txn)
            txn_recorder.txns_counter += 1 # счетчик всех транз-ций

            # Управление записью транзакций чанками в файлы.
            txn_recorder.record_chunk(txn=one_txn, txns_num=txns_num)
            
            # Добавляем созданную транзакцию к транзакциям клиента, т.к. иногда 
            # при генерации других транзакций нужно знать уже созданные транзакции
            one_txn_df = pd.DataFrame([one_txn])
            client_transactions = pd.concat([client_transactions, one_txn_df], ignore_index=ignore_index)
        
        client_txns.clear() # Конец генерации на клиента. Чистим список для текущего кл-та

    # Сборка цельного датафрейма из чанков записанных в файлы. Датафрейм сохраняется 
    # в txn_recorder.all_txns.
    txn_recorder.build_from_chunks()

    # Запись собранного датафрейма в два файла в разные директории: data/generated/lastest/
    # И data/generated/history/<своя_папка_с_датой_временем>
    txn_recorder.write_built_data()

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

In [77]:
# Импорт зависимостей
from data_generator.legit.recorder import LegitTxnsRecorder
from data_generator.utils import gen_trans_number_norm
In [172]:
# Нужно создать объект LegitTxnsRecorder для передачи в gen_multiple_legit_txns
# Это надо делать заново если повторяем генерацию в этом ноутбуке т.к. нужно очистить некоторые атрибуты
# txn_recorder, в которых остаются некоторые данные после предыдущего запуска

txn_recorder = LegitTxnsRecorder(configs=configs)
In [173]:
# Путь к директории которую мы создали в самом начале
# В этой директории в папке legit будут храниться созданные легальные транзакции
configs.run_dir
Out[173]:
WindowsPath('data/generated/history/generation_run_2025-07-25_121029')

Если запуск повторный с теми же конфигами то в директории указанной в configs.run_dir открыть папку legit и удалить ее содержимое, т.к. это содержимое предыдущего запуска. Но это только если мы используем те же конфиги где указан тот же путь до директории прошлого запуска configs.run_dir. В целом варианте подразумевается что конфиг создается каждый запуск генератора и каждый раз это другая директория, но в тестовых запусках нужно это учесть если повторяем запуск с тем же configs

In [174]:
# Непосредственно генерация транзакций. 

gen_multiple_legit_txns(configs=configs, txn_recorder=txn_recorder)
In [175]:
# gen_multiple_legit_txns не возвращает результат. Прочитаем его из файла с транзакциями
# Также датафрейм со всеми созданными транзакциями можно взять из txn_recorder.all_txns

# Соберем полный путь к файлу
data_storage = legit_cfg["data_storage"] # конфиги названий поддиректорий и файлов
leg_dir = data_storage["folder_name"] # название поддиректории легальных транз.
leg_file = data_storage["files"]["txns"] # название файла с легальными транз.
path_to_leg_txns = os.path.join(run_dir, leg_dir, leg_file)

# Чтение датафрейма
multi_leg_txn_demo = pd.read_parquet(path_to_leg_txns)
In [181]:
# Итоговый датафрейм с легальными транзакциями. Уже отсортирован по времени

multi_leg_txn_demo.head(5)
Out[181]:
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 13556 2025-01-01 01:16:00 1735694160 615.46 purchase ecom shopping_net True 6828.0 Курск 51.730339 36.192645 2.60.20.218 9613.0 NaN False False approved not applicable
1 3733 2025-01-01 01:50:00 1735696200 955.55 purchase ecom grocery_net True 6787.0 Москва 55.753879 37.620373 2.60.13.206 6350.0 NaN False False approved not applicable
2 3289 2025-01-01 02:49:00 1735699740 1746.00 purchase ecom shopping_net True 6782.0 Калининград 54.707322 20.507246 2.60.12.44 5588.0 NaN False False approved not applicable
3 3769 2025-01-01 04:02:00 1735704120 204.63 purchase ecom grocery_net True 6820.0 Екатеринбург 56.838633 60.605489 2.60.13.241 6419.0 NaN False False approved not applicable
4 1245 2025-01-01 04:23:00 1735705380 1.00 purchase ecom shopping_net True 6862.0 Нижний Новгород 56.324209 44.005395 2.60.4.161 2105.0 NaN False False approved not applicable
In [177]:
# Сколько транзакций сгенерировалось
multi_leg_txn_demo.shape
Out[177]:
(5045, 19)




Оркестрация генерации легальных транзакций¶

1. Класс LegitRunner¶

  • модуль data_generator.runner.legit. Ссылка на исходный код в Github
  • Оркестрация генерации легальных транзакций
  • Это финальный уровень для легальных транзакций. Метод run() этого класса вызывается в файле запуска генератора всех транзакций run_generator.py

Функции класса

  1. Собирает все что нужно для генерации легальных транзакций воедино
  2. Запускает полную генерацию легальных транзакций

При создании объекта класса нужно просто передать загруженные конфиги из yaml файлов и путь к созданной директории текущего запуска генератора (создается функцией make_dir_for_run из модуля data_generator.runner.utils)

In [ ]:
class LegitRunner:
    """
    Запуск генератора легальных транзакций.
    ----------
    Атрибуты:
    ----------
    cfg_builder: LegitConfigBuilder.
    configs: LegitCfg. Конфиги и данные для генерации легальных транзакций.
    txn_recorder: LegitTxnsRecorder. Запись легальных транзакций в файл.
    text: str. Текст для вставки в спиннер.
    """
    def __init__(self, base_cfg, legit_cfg, time_cfg, run_dir):
        """
        base_cfg: dict. Конфиги из base.yaml
        legit_cfg: dict. Конфиги из legit.yaml
        time_cfg: dict. Конфиги из time.yaml
        run_dir: str. Название директории для хранения сгенерированных
                 данных текущей генерации.
        """
        self.cfg_builder = LegitConfigBuilder(base_cfg=base_cfg, legit_cfg=legit_cfg, \
                                              time_cfg=time_cfg, run_dir=run_dir)
        self.configs = self.cfg_builder.build_cfg()
        self.txn_recorder = LegitTxnsRecorder(configs=self.configs)
        self.text = "Legit txns generation"


    @spinner_decorator
    def run(self):
        """
        Запуск генератора.
        """
        configs = self.configs
        txn_recorder = self.txn_recorder

        gen_multiple_legit_txns(configs=configs, txn_recorder=txn_recorder)