Создание дополнительных данных для генерации транзакций¶

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

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

  • Полные данные для категорий покупок
  • Правила для compromised client фрода
  • Распределение сумм для категорий, для compromised client фрода
  • Категории и их данные для purchase дроп фрода
  • Генерация счетов клиентов и внешних счетов
In [1]:
import pandas as pd
import numpy as np
import os
import pyarrow
import yaml
from data_generator.utils import load_configs
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 [4]:
# Базовые конфиги
base_cfg = load_configs("./config/base.yaml")

# Фрод конфиги
fraud_cfg = load_configs("./config/fraud.yaml")

# Пути к файлам
data_paths = base_cfg["data_paths"]
In [5]:
category_stats = pd.read_csv(data_paths["base"]["category_stats"])
fraud_kaggle = pd.read_csv("data/raw/fraudTest.csv.zip", compression="zip")
clients = pd.read_parquet(data_paths["clients"]["clients"])

Создание признаков для категорий покупок¶

  • нам понадобятся данные о категориях покупок, также нужны веса категорий (как часто в них покупают) и их информация: онлайн/оффлайн, круглосуточная/некруглосуточная, вероятность фрода. Далее мы создадим небольшой датафрейм с нужными данными для категорий
In [8]:
# добавим колонку онлайн или не онлайн категория со значениями True и False соответсвенно

category_stats.loc[category_stats.category.str.contains("net"), "online"] = True
category_stats.loc[~category_stats.category.str.contains("net"), "online"] = False
category_stats.head(6)
Out[8]:
category avg_amt amt_std cat_count online
0 gas_transport 63.577001 15.828399 56370 False
1 grocery_pos 115.885327 51.552330 52553 False
2 home 57.995413 48.085281 52345 False
3 shopping_pos 76.862457 232.484678 49791 False
4 kids_pets 57.506913 48.748482 48692 False
5 shopping_net 83.481653 237.219758 41779 True
In [9]:
# добавим долю категории, ее "вес" среди категорий - для определения распростаненности категории

category_stats["share"] = category_stats.cat_count.div(category_stats.cat_count.sum())
category_stats.head()
Out[9]:
category avg_amt amt_std cat_count online share
0 gas_transport 63.577001 15.828399 56370 False 0.101436
1 grocery_pos 115.885327 51.552330 52553 False 0.094568
2 home 57.995413 48.085281 52345 False 0.094193
3 shopping_pos 76.862457 232.484678 49791 False 0.089597
4 kids_pets 57.506913 48.748482 48692 False 0.087620
In [10]:
category_stats_final = category_stats.copy()
In [11]:
# умножим средние суммы транзакций по категориям и стандартное отклонение сумм на 15, для приближенности к ценам в рублях

category_stats_final[["avg_amt","amt_std"]] = category_stats_final[["avg_amt","amt_std"]] * 15
category_stats_final
Out[11]:
category avg_amt amt_std cat_count online share
0 gas_transport 953.655019 237.425981 56370 False 0.101436
1 grocery_pos 1738.279905 773.284951 52553 False 0.094568
2 home 869.931194 721.279215 52345 False 0.094193
3 shopping_pos 1152.936859 3487.270165 49791 False 0.089597
4 kids_pets 862.603690 731.227232 48692 False 0.087620
5 shopping_net 1252.224798 3558.296372 41779 True 0.075180
6 entertainment 959.772599 963.449020 40104 False 0.072166
7 personal_care 723.495309 741.164119 39327 False 0.070768
8 food_dining 761.669074 726.735802 39268 False 0.070662
9 health_fitness 808.011475 719.478766 36674 False 0.065994
10 misc_pos 932.733689 2009.808415 34574 False 0.062215
11 misc_net 1179.003552 2454.586333 27367 True 0.049246
12 grocery_net 805.975010 343.626548 19426 True 0.034957
13 travel 1685.845238 8939.194422 17449 False 0.031399
In [12]:
# Найдем количество фрода по категориям из датасета kaggle. Для определения вероятности фрода при генерации транзакций

fraud_trans_count_by_cat = fraud_kaggle.query("is_fraud == 1") \
                                        .groupby("category", as_index=False).agg({"trans_num":"count"}) \
                                        .rename(columns={"trans_num":"fraud_count"})
fraud_trans_count_by_cat.head()
Out[12]:
category fraud_count
0 entertainment 59
1 food_dining 54
2 gas_transport 154
3 grocery_net 41
4 grocery_pos 485
In [13]:
cat_stats_full = category_stats_final.merge(fraud_trans_count_by_cat, on="category")
cat_stats_full.head(3)
Out[13]:
category avg_amt amt_std cat_count online share fraud_count
0 gas_transport 953.655019 237.425981 56370 False 0.101436 154
1 grocery_pos 1738.279905 773.284951 52553 False 0.094568 485
2 home 869.931194 721.279215 52345 False 0.094193 67
In [14]:
# доля фрода в категории

cat_stats_full["fraud_share"] = cat_stats_full.fraud_count.div(cat_stats_full.cat_count)
cat_stats_full.head()
Out[14]:
category avg_amt amt_std cat_count online share fraud_count fraud_share
0 gas_transport 953.655019 237.425981 56370 False 0.101436 154 0.002732
1 grocery_pos 1738.279905 773.284951 52553 False 0.094568 485 0.009229
2 home 869.931194 721.279215 52345 False 0.094193 67 0.001280
3 shopping_pos 1152.936859 3487.270165 49791 False 0.089597 213 0.004278
4 kids_pets 862.603690 731.227232 48692 False 0.087620 65 0.001335
In [15]:
# Добавим критерий возможности круглосуточной покупки в категории

cat_stats_full["round_clock"] = False
In [16]:
round_clock = ['gas_transport', 'grocery_pos','shopping_net', 'food_dining', 'misc_pos', 'misc_net', 'grocery_net']
In [17]:
for category in round_clock:
    cat_stats_full.loc[cat_stats_full.category == category, "round_clock"] = True
cat_stats_full
Out[17]:
category avg_amt amt_std cat_count online share fraud_count fraud_share round_clock
0 gas_transport 953.655019 237.425981 56370 False 0.101436 154 0.002732 True
1 grocery_pos 1738.279905 773.284951 52553 False 0.094568 485 0.009229 True
2 home 869.931194 721.279215 52345 False 0.094193 67 0.001280 False
3 shopping_pos 1152.936859 3487.270165 49791 False 0.089597 213 0.004278 False
4 kids_pets 862.603690 731.227232 48692 False 0.087620 65 0.001335 False
5 shopping_net 1252.224798 3558.296372 41779 True 0.075180 506 0.012111 True
6 entertainment 959.772599 963.449020 40104 False 0.072166 59 0.001471 False
7 personal_care 723.495309 741.164119 39327 False 0.070768 70 0.001780 False
8 food_dining 761.669074 726.735802 39268 False 0.070662 54 0.001375 True
9 health_fitness 808.011475 719.478766 36674 False 0.065994 52 0.001418 False
10 misc_pos 932.733689 2009.808415 34574 False 0.062215 72 0.002082 True
11 misc_net 1179.003552 2454.586333 27367 True 0.049246 267 0.009756 True
12 grocery_net 805.975010 343.626548 19426 True 0.034957 41 0.002111 True
13 travel 1685.845238 8939.194422 17449 False 0.031399 40 0.002292 False

Сохранение cat_stats_full в csv¶

In [17]:
cat_stats_full.to_csv(data_paths["base"]["cat_stats_full"], index=False)



Правила для compromised client фрода¶


Список правил:

  1. быстрая смена гео при оффлайн покупке fast_geo_change
  2. другой ip и быстрая смена гео при онлайн покупке fast_geo_change_online
  3. другой ip(другого города) + другой девайс + высокая сумма - new_ip_and_device_high_amount
  4. другой ip(но тот же город), другой девайс + высокая сумма,а - new_device_and_high_amount
  5. увеличение кол-ва транзакций в единицу времени. trans_freq_increase
In [18]:
# вручную зададим веса правил детекта фрода - по каким правилам чаще будут генерироваться фрод/подозрительные транзакции
# воспользуюсь шкалой от 1 до 10

rule_names_and_weights = {"fast_geo_change":4, "fast_geo_change_online":7, \
                          "new_ip_and_device_high_amount":8, \
                          "new_device_and_high_amount":6, \
                          "trans_freq_increase":7}
In [19]:
# создадим пустой датафрейм для правил
rules_df = pd.DataFrame({"rule":pd.Series(dtype="str"),
             "weight":pd.Series(dtype="int")})
rules_df
Out[19]:
rule weight
In [20]:
# данные словаря запишем в датафрейм

for index, key in enumerate(rule_names_and_weights.keys()):
    rules_df.loc[index, "rule"] = key
    rules_df.loc[index, "weight"] = rule_names_and_weights[key]
In [21]:
# нормализуем веса - сделаем их долями от суммы всех весов

rules_df["weight"] = rules_df.weight.div(rules_df.weight.sum())
rules_df
Out[21]:
rule weight
0 fast_geo_change 0.12500
1 fast_geo_change_online 0.21875
2 new_ip_and_device_high_amount 0.25000
3 new_device_and_high_amount 0.18750
4 trans_freq_increase 0.21875
In [22]:
# Флаг онлайн/оффлайн для правил
# Пока что только одно правило относится к оффлайну - fast_geo_change

rules_df["online"] = True
rules_df.loc[rules_df.rule == "fast_geo_change", "online"] = False
rules_df
Out[22]:
rule weight online
0 fast_geo_change 0.12500 False
1 fast_geo_change_online 0.21875 True
2 new_ip_and_device_high_amount 0.25000 True
3 new_device_and_high_amount 0.18750 True
4 trans_freq_increase 0.21875 True

Выгрузка правил в csv¶

In [23]:
rules_df.to_csv(data_paths["base_fraud"]["rules"], index=False)



Распределение сумм для категорий, для compromised client фрода¶

  • У compromised client фрода свои параметры сумм транзакций
  • Зададим значения для генерации сумм в рамках каждой категории покупок.

Датафрейм с параметрами распределения сумм для категорий

In [26]:
cat_fraud_amts = cat_stats_full[['category', 'avg_amt', 'amt_std']].copy()
In [27]:
# конфиги для сумм compromised client фрода
compr_f_amt_cfg = fraud_cfg["purchase"]["amount"]
In [28]:
# Категории где более вероятна очень высокая сумма при мошенничестве
extreme_amt = compr_f_amt_cfg["extreme"]["categories"]

cat_fraud_amts["extreme"] = False
cat_fraud_amts.loc[cat_fraud_amts.category.isin(extreme_amt), "extreme"] = True
cat_fraud_amts.head()
Out[28]:
category avg_amt amt_std extreme
0 gas_transport 953.655019 237.425981 False
1 grocery_pos 1738.279905 773.284951 False
2 home 869.931194 721.279215 False
3 shopping_pos 1152.936859 3487.270165 True
4 kids_pets 862.603690 731.227232 False
In [29]:
# Назначение минимальной, максимальной, средней и отклонения суммы для фрода по критерию extreme
# extreme - категории где возможны очень высокие суммы
# other - остальные категории 

cat_fraud_amts["fraud_low"] = compr_f_amt_cfg["other"]["low"]
cat_fraud_amts["fraud_high"] = compr_f_amt_cfg["other"]["high"]
cat_fraud_amts["fraud_mean"] = compr_f_amt_cfg["other"]["mean"]
cat_fraud_amts["fraud_std"] = compr_f_amt_cfg["other"]["std"]

cat_fraud_amts.loc[cat_fraud_amts.extreme == True, "fraud_low"] = compr_f_amt_cfg["extreme"]["low"]
cat_fraud_amts.loc[cat_fraud_amts.extreme == True, "fraud_high"] = compr_f_amt_cfg["extreme"]["high"]
cat_fraud_amts.loc[cat_fraud_amts.extreme == True, "fraud_mean"] = compr_f_amt_cfg["extreme"]["mean"]
cat_fraud_amts.loc[cat_fraud_amts.extreme == True, "fraud_std"] = compr_f_amt_cfg["extreme"]["std"]
cat_fraud_amts.drop(columns=["avg_amt", "amt_std"], inplace=True)
cat_fraud_amts.head()
Out[29]:
category extreme fraud_low fraud_high fraud_mean fraud_std
0 gas_transport False 500 8000 4000 1000
1 grocery_pos False 500 8000 4000 1000
2 home False 500 8000 4000 1000
3 shopping_pos True 1000 60000 20000 20000
4 kids_pets False 500 8000 4000 1000
In [30]:
cat_fraud_amts.to_csv(data_paths["base_fraud"]["cat_fraud_amts"], index=False)



Категории и их данные для purchase дроп фрода¶

  • purchase дроп фрод это дропы покупатели: получают деньги и покупают на них товары для отмывания
In [8]:
cat_stats_full = pd.read_csv(data_paths["base"]["cat_stats_full"])
In [11]:
drop_purch_cats = cat_stats_full.query("online == True and category != 'grocery_net'")[["category"]] \
                                .reset_index(drop=True).copy()
In [12]:
drop_purch_cats["weight"] = [0.75, 0.25]
assert drop_purch_cats.weight.sum() == 1
drop_purch_cats
Out[12]:
category weight
0 shopping_net 0.75
1 misc_net 0.25
In [13]:
drop_purch_cats.to_csv(data_paths["base_fraud"]["drop_purch_cats"], index=False)



Генерация счетов клиентов и внешних счетов¶

  • внешние счета это счета вне нашего банка

1. Счета клиентов¶

Пускай начинаются с 10000

In [36]:
accounts = clients[["client_id"]].copy()
In [37]:
accounts["account_id"] = 1
In [38]:
accounts.loc[0, "account_id"] = 10000
In [39]:
accounts.head()
Out[39]:
client_id account_id
0 1 10000
1 2 1
2 3 1
3 4 1
4 5 1
In [40]:
# Кумулятивно складываем числа в серии. Получается в каждой записи будет результат сложения текущего и всех предыдущих чисел
# Т.е. 10000, 10000 + 1, 10001 + 1 и т.д. Так будут счета с номерами от 10000 до 10000 + n-1 клиентов

accounts["account_id"] = accounts["account_id"].cumsum()
accounts.head()
Out[40]:
client_id account_id
0 1 10000
1 2 10001
2 3 10002
3 4 10003
4 5 10004
In [41]:
accounts.agg({"account_id":["min","max"]})
Out[41]:
account_id
min 10000
max 15368
In [42]:
assert accounts.shape[0] == accounts.account_id.nunique(), "Values in account_id are not unique!"
In [43]:
accounts.shape[0]
Out[43]:
5369
In [44]:
# Колонка is_drop. Дроп клиент или нет. Пока нет дропов.
# Они будут обозначаться непосредственно во время генерации активности дропов

accounts["is_drop"] = False
accounts.head()
Out[44]:
client_id account_id is_drop
0 1 10000 False
1 2 10001 False
2 3 10002 False
3 4 10003 False
4 5 10004 False

2. Внешние счета¶

  • счета начинающиеся с максимального номера счета нашего клиента + 1
In [45]:
# Пусть будет 10000 счетов

start_id = accounts.account_id.max() + 1
outer_accounts = pd.Series(data=np.arange(start_id, start_id + 10000, step=1), name="account_id", dtype="int")
In [46]:
outer_accounts.iloc[np.r_[0:3,-3:0]]
Out[46]:
0       15369
1       15370
2       15371
9997    25366
9998    25367
9999    25368
Name: account_id, dtype: int64
In [47]:
# Не должно быть пересечений по account_id
assert accounts.merge(outer_accounts, on="account_id").empty, "Clients account ids are in the outer account ids"

Сохранение счетов в файлы¶

In [48]:
accounts.to_csv(data_paths["base"]["accounts_default"], index=False)
In [49]:
outer_accounts.to_csv(data_paths["base"]["outer_accounts"], index=False)