Создание дополнительных данных для генерации транзакций¶
- ноутбуки лучше просматривать на 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 фрода¶
Список правил:
- быстрая смена гео при оффлайн покупке
fast_geo_change
- другой ip и быстрая смена гео при онлайн покупке
fast_geo_change_online
- другой ip(другого города) + другой девайс + высокая сумма -
new_ip_and_device_high_amount
- другой ip(но тот же город), другой девайс + высокая сумма,а -
new_device_and_high_amount
- увеличение кол-ва транзакций в единицу времени.
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)