Note
Go to the end to download the full example code.
Suspicious financial activity detection using quantum computer¶
In this example, we will illustrate the use of Riemannian geometry and quantum computing for the detection of suspicious activity on financial data [1].
The dataset contains synthetic data generated from a real dataset of CaixaBank’s express loans [2]. Each entry contains, for example, the date and amount of the loan request, the client identification number and the creation date of the account. A loan is tagged with either tentative or confirmation of fraud, when a fraudster has impersonates the client to claim the loan and steal the client funds.
Once the fraud is characterized, a complex task is to identify whether or not a collusion is taking place. One fraudster can for example corrupt a client having already a good history with the bank. The fraud can also involves a bank agent who is mandated by the client. The scam perdurates over time, sometime over month or years. Identifying these participants is essential to prevent similar scam to happen in the future.
In this example, we will use RG to identify whether or no a fraud is a probable collusion. Because this method works on a small number of components, it is also compatible with quantum computing.
# Authors: Gregoire Cattan, Filipe Barroso
# License: BSD (3-clause)
import os
import numpy as np
import pandas as pd
from imblearn.under_sampling import NearMiss
from matplotlib import pyplot as plt
from pyriemann.estimation import XdawnCovariances
from pyriemann.preprocessing import Whitening
from pyriemann.utils.viz import plot_waveforms
from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import balanced_accuracy_score
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import LabelEncoder
from sklearn.svm import SVC
from pyriemann_qiskit.classification import QuanticSVM
from pyriemann_qiskit.utils.hyper_params_factory import gen_zz_feature_map
from pyriemann_qiskit.utils.preprocessing import NdRobustScaler
# import warnings
print(__doc__)
# getting rid of the warnings about the future
# warnings.simplefilter(action="ignore", category=FutureWarning)
# warnings.simplefilter(action="ignore", category=RuntimeWarning)
# warnings.filterwarnings("ignore")
Some utils functions for plotting¶
def plot_ERP(X, title, n=10, ylim=None, add_digest=False):
epochs = ToEpochs(n=n).transform(X)
reduced_centered_epochs = NdRobustScaler().fit_transform(epochs)
fig = plot_waveforms(reduced_centered_epochs, "mean+/-std")
fig.axes[0].set_title(f"{title} ({len(X)})")
if ylim is None:
ylim = []
for i_channel in range(len(channels)):
ylim.append(fig.axes[i_channel].get_ylim())
for i_channel in range(len(channels)):
fig.axes[i_channel].set_ylim(ylim[i_channel])
if not add_digest:
return fig, ylim
for i_channel in range(len(channels)):
fig.axes[i_channel].set(ylabel=digest[i_channel])
return fig, ylim
def merge_2axes(fig1, fig2, file_name1="f1.png", file_name2="f2.png"):
fig1.savefig(file_name1)
fig2.savefig(file_name2)
plt.close(fig1)
plt.close(fig2)
# inherit figures' dimensions, partially
w1, w2 = [int(np.ceil(fig.get_figwidth())) for fig in (fig1, fig2)]
hmax = int(np.ceil(max([fig.get_figheight() for fig in (fig1, fig2)])))
fig, axes = plt.subplots(1, w1 + w2, figsize=(w1 + w2, hmax))
# make two axes of desired height proportion
gs = axes[0].get_gridspec()
for ax in axes.flat:
ax.remove()
ax1 = fig.add_subplot(gs[0, :w1])
ax2 = fig.add_subplot(gs[0, w1:])
ax1.imshow(plt.imread(file_name1))
ax2.imshow(plt.imread(file_name2))
for ax in (ax1, ax2):
for side in ("top", "left", "bottom", "right"):
ax.spines[side].set_visible(False)
ax.tick_params(
left=False, right=False, labelleft=False, labelbottom=False, bottom=False
)
return fig
def plot_ERPs(X, y, n=10, label0="Fraud", label1="Genuine"):
fig0, ylim = plot_ERP(X[y == 1], label0, n, add_digest=True)
fig1, _ = plot_ERP(X[y == 0], label1, n, ylim)
merge_2axes(fig0, fig1)
plt.show()
Data pre-processing¶
Pre-process financial data (loan transactions)
# Download data
url = "https://zenodo.org/record/7418458/files/INFINITECH_synthetic_inmediate_loans.csv"
dataset = pd.read_csv(url, sep=";")
# Transform into binary classification, regroup frauds and suspicions of fraud
dataset.loc[dataset.FRAUD == 2, "FRAUD"] = 1
# Select a few features for the example
channels = [
"IP_TERMINAL",
"FK_CONTRATO_PPAL_OPE",
"POSICION_GLOBAL_ANTES_PRESTAMO",
"SALDO_ANTES_PRESTAMO",
"FK_NUMPERSO",
"FECHA_ALTA_CLIENTE",
"FK_TIPREL",
]
digest = [
"IP",
"Contract",
"Account balance",
"Global balance",
"ID",
"Date",
"Ownership",
]
features = dataset[channels]
target = dataset.FRAUD
# let's display a screenshot of the pre-processed dataset
# We only have about 200 frauds epochs over 30K entries.
print(features.head())
print(f"number of fraudulent loans: {target[target == 1].size}")
print(f"number of genuine loans: {target[target == 0].size}")
# Simple treatment for NaN value
features.ffill(inplace=True)
# Convert date value to linux time
features["FECHA_ALTA_CLIENTE"] = pd.to_datetime(features["FECHA_ALTA_CLIENTE"])
features["FECHA_ALTA_CLIENTE"] = features["FECHA_ALTA_CLIENTE"].apply(
lambda x: 2023 - x.year
)
# Let's encode our categorical variables (LabelEncoding):
# features["IP_TERMINAL"] = features["IP_TERMINAL"].astype("category").cat.codes
le = LabelEncoder()
le.fit(features["IP_TERMINAL"].astype("category"))
features["IP_TERMINAL"] = le.transform(features["IP_TERMINAL"].astype("category"))
# ... and create an 'index' column in the dataset
# Note: this is done only for programming reasons, due to our implementation
# of the `ToEpochs` transformer (see below)
features["index"] = features.index
IP_TERMINAL ... FK_TIPREL
0 CIAPPLPh8,4C48OJ1mCGLu4D+3ziZlYq0q9CdA= ... 1.0
1 213.4.31.4 ... 1.0
2 83.50.136.195 ... 1.0
3 cIAPPLh12.82S7azzoEruAyT8uE2r5gb8RUHJpf ... 1.0
4 aAXIAORedmivtrnDZnbtJ9HSAhR6/b8EvZQJ98= ... 1.0
[5 rows x 7 columns]
number of fraudulent loans: 214
number of genuine loans: 35843
/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/checkouts/latest/examples/other_datasets/plot_financial_data.py:176: UserWarning: Parsing dates in %d/%m/%Y format when dayfirst=False (the default) was specified. Pass `dayfirst=True` or specify a format to silence this warning.
features["FECHA_ALTA_CLIENTE"] = pd.to_datetime(features["FECHA_ALTA_CLIENTE"])
Pipeline for binary classification¶
Let’s create the pipeline as suggested in the patent application [1]. Let’s start by creating the required transformers:
class ToEpochs(TransformerMixin, BaseEstimator):
def __init__(self, n):
self.n = n
def fit(self, X, y):
return self
def transform(self, X):
all_epochs = []
for x in X:
index = x[-1]
epoch = features[features.index > index - self.n - 1]
epoch = epoch[epoch.index < index]
epoch.drop(columns=["index"], inplace=True)
all_epochs.append(np.transpose(epoch))
all_epochs = np.array(all_epochs)
return all_epochs
def slim(x, keep_diagonal=True):
# Vectorize covariance matrices by removing redundant information.
length = len(x) // 2
first = range(0, length)
last = range(len(x) - length, len(x))
down_cadrans = x[np.ix_(last, last)]
if keep_diagonal:
down_cadrans = [down_cadrans[i, j] for i in first for j in first if i <= j]
else:
down_cadrans = [down_cadrans[i, j] for i in first for j in first if i < j]
first_cadrans = np.reshape(x[np.ix_(last, first)], (1, len(x)))
ret = np.append(first_cadrans, down_cadrans)
return ret
class SlimVector(TransformerMixin, BaseEstimator):
def __init__(self, keep_diagonal):
self.keep_diagonal = keep_diagonal
def fit(self, X, y):
return self
def transform(self, X):
return np.array([slim(x, self.keep_diagonal) for x in X])
class OptionalWhitening(TransformerMixin, BaseEstimator):
def __init__(self, process=True, n_components=4):
self.process = process
self.n_components = n_components
def fit(self, X, y):
return self
def transform(self, X):
if not self.process:
return X
return Whitening(dim_red={"n_components": self.n_components}).fit_transform(X)
# Create a RandomForest for baseline comparison of direct classification:
rf = RandomForestClassifier(random_state=42)
# Classical pipeline: puts together transformers,
# then adds at the end a classical SVM
pipe = make_pipeline(
ToEpochs(n=10),
NdRobustScaler(),
XdawnCovariances(nfilter=1),
OptionalWhitening(process=True, n_components=4),
SlimVector(keep_diagonal=True),
SVC(probability=True),
)
if os.getenv("CI") == "true":
print("Feeding a good estimator for CI (skipping grid search)")
param_grid: dict = {
"toepochs__n": [30],
"xdawncovariances__nfilter": [1],
"optionalwhitening__process": [True],
"optionalwhitening__n_components": [4],
"slimvector__keep_diagonal": [True],
"svc__C": [1],
"svc__gamma": ["auto"],
}
else:
param_grid: dict = {
"toepochs__n": [20, 30],
"xdawncovariances__nfilter": [1, 2],
"optionalwhitening__process": [True, False],
"optionalwhitening__n_components": [2, 4],
"slimvector__keep_diagonal": [True, False],
"svc__C": [0.1, 1],
"svc__gamma": ["auto", "scale"],
}
# Optimize the pipeline:
# let's save some time and run the optimization with the classical SVM
gs = GridSearchCV(
pipe,
param_grid=param_grid,
scoring="balanced_accuracy",
cv=4,
verbose=1,
)
Balance dataset¶
Balance the data and display the “ERP” [3].
# Let's balance the problem using NearMiss.
# There is no special need to do this with SVM,
# as we could also play with the `class_weight` parameter.
# parameter. However, reducing the data is practical for Ci/Cd.
# So `NearMiss` we choose the closest non-fraud epochs to the fraud-epochs.
# Here we will keep a ratio of 2 non-fraud epochs for 1 fraud epochs.
# Note: at this stage `features` also contains the `index` column.
# Drop rows where target is NaN and cast to int for imbalanced-learn
valid_mask = target.notna()
features = features[valid_mask]
target = target[valid_mask].astype(int)
# Possibly avoids tie-break situations
np.random.seed(42)
X, y = NearMiss(sampling_strategy=0.5, n_jobs=-1, n_neighbors=3).fit_resample(
features.to_numpy(), target.to_numpy()
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
labels, counts = np.unique(y_train, return_counts=True)
print(
f"Training set shape: {X_train.shape}, genuine: {counts[0]}, \
frauds: {counts[1]}"
)
labels, counts = np.unique(y_test, return_counts=True)
print(
f"Testing set shape: {X_test.shape}, genuine: {counts[0]}, \
frauds: {counts[1]}"
)
Training set shape: (513, 8), genuine: 342, frauds: 171
Testing set shape: (129, 8), genuine: 86, frauds: 43
Supervised classification¶
Run evaluation on classical vs quantum classifiers.
# Let's fit our GridSearchCV, to find the best hyper parameters
gs.fit(X_train, y_train)
# Print best parameters
print("Best parameters are:")
print(gs.best_params_)
best_C = gs.best_params_["svc__C"]
best_gamma = gs.best_params_["svc__gamma"]
best_n = gs.best_params_["toepochs__n"]
# This is the best score with the classical SVM.
# (with this train/test split at least)
train_pred_svm = gs.best_estimator_.predict(X_train)
train_score_svm = balanced_accuracy_score(y_train, train_pred_svm)
pred_svm = gs.best_estimator_.predict(X_test)
score_svm = balanced_accuracy_score(y_test, pred_svm)
# Quantum pipeline:
# let's take the same parameters but evaluate the pipeline with a quantum SVM.
# Note: From experience, quantum SVM tends to overfit quickly.
# So it is debatable if we want to keep the same penalties
# for the quantum SVM as for the classical one.
gs.best_estimator_.steps[-1] = (
"quanticsvm",
QuanticSVM(
quantum=True,
C=best_C,
gamma=best_gamma,
gen_feature_map=gen_zz_feature_map(),
seed=42,
n_jobs=1,
use_qiskit_symb=False,
),
)
train_pred_qsvm = gs.best_estimator_.fit(X_train, y_train).predict(X_train)
train_score_qsvm = balanced_accuracy_score(y_train, train_pred_qsvm)
pred_qsvm = gs.best_estimator_.predict(X_test)
score_qsvm = balanced_accuracy_score(y_test, pred_qsvm)
# Create a point of comparison with the RandomForest
train_pred_rf = rf.fit(X_train, y_train).predict(X_train)
train_score_rf = balanced_accuracy_score(y_train, train_pred_rf)
pred_rf = rf.predict(X_test)
score_rf = balanced_accuracy_score(y_test, pred_rf)
Fitting 4 folds for each of 128 candidates, totalling 512 fits
/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/model_selection/_validation.py:490: FitFailedWarning:
256 fits failed out of a total of 512.
The score on these train-test partitions for these parameters will be set to nan.
If these failures are not expected, you can try to debug them by setting error_score='raise'.
Below are more details about the failures:
--------------------------------------------------------------------------------
128 fits failed with the following error:
Traceback (most recent call last):
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/model_selection/_validation.py", line 833, in _fit_and_score
estimator.fit(X_train, y_train, **fit_params)
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/base.py", line 1336, in wrapper
return fit_method(estimator, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/pipeline.py", line 613, in fit
Xt = self._fit(X, y, routed_params, raw_params=params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/pipeline.py", line 547, in _fit
X, fitted_transformer = fit_transform_one_cached(
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/joblib/memory.py", line 326, in __call__
return self.func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/pipeline.py", line 1484, in _fit_transform_one
res = transformer.fit_transform(X, y, **params.get("fit_transform", {}))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/utils/_set_output.py", line 316, in wrapped
data_to_wrap = f(self, X, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/base.py", line 910, in fit_transform
return self.fit(X, y, **fit_params).transform(X)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/utils/_set_output.py", line 316, in wrapped
data_to_wrap = f(self, X, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/checkouts/latest/examples/other_datasets/plot_financial_data.py", line 243, in transform
return np.array([slim(x, self.keep_diagonal) for x in X])
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/checkouts/latest/examples/other_datasets/plot_financial_data.py", line 230, in slim
first_cadrans = np.reshape(x[np.ix_(last, first)], (1, len(x)))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/numpy/_core/fromnumeric.py", line 299, in reshape
return _wrapfunc(a, 'reshape', shape, order=order)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/numpy/_core/fromnumeric.py", line 54, in _wrapfunc
return bound(*args, **kwds)
^^^^^^^^^^^^^^^^^^^^
ValueError: cannot reshape array of size 1 into shape (1,2)
--------------------------------------------------------------------------------
128 fits failed with the following error:
Traceback (most recent call last):
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/model_selection/_validation.py", line 833, in _fit_and_score
estimator.fit(X_train, y_train, **fit_params)
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/base.py", line 1336, in wrapper
return fit_method(estimator, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/pipeline.py", line 613, in fit
Xt = self._fit(X, y, routed_params, raw_params=params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/pipeline.py", line 547, in _fit
X, fitted_transformer = fit_transform_one_cached(
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/joblib/memory.py", line 326, in __call__
return self.func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/pipeline.py", line 1484, in _fit_transform_one
res = transformer.fit_transform(X, y, **params.get("fit_transform", {}))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/utils/_set_output.py", line 316, in wrapped
data_to_wrap = f(self, X, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/base.py", line 910, in fit_transform
return self.fit(X, y, **fit_params).transform(X)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/utils/_set_output.py", line 316, in wrapped
data_to_wrap = f(self, X, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/checkouts/latest/examples/other_datasets/plot_financial_data.py", line 243, in transform
return np.array([slim(x, self.keep_diagonal) for x in X])
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/checkouts/latest/examples/other_datasets/plot_financial_data.py", line 230, in slim
first_cadrans = np.reshape(x[np.ix_(last, first)], (1, len(x)))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/numpy/_core/fromnumeric.py", line 299, in reshape
return _wrapfunc(a, 'reshape', shape, order=order)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/numpy/_core/fromnumeric.py", line 54, in _wrapfunc
return bound(*args, **kwds)
^^^^^^^^^^^^^^^^^^^^
ValueError: cannot reshape array of size 16 into shape (1,8)
warnings.warn(some_fits_failed_message, FitFailedWarning)
/home/docs/checkouts/readthedocs.org/user_builds/pyriemann-qiskit/envs/latest/lib/python3.12/site-packages/sklearn/model_selection/_search.py:1137: UserWarning: One or more of the test scores are non-finite: [ nan nan nan nan nan nan
nan nan nan nan nan nan
nan nan nan nan nan nan
nan nan nan nan nan nan
nan nan nan nan nan nan
nan nan 0.67813986 nan 0.67196274 nan
0.67813986 nan 0.68684369 nan 0.68556283 nan
0.6869129 nan 0.68409224 nan 0.68407514 nan
0.60172709 nan 0.59300616 nan 0.6516994 nan
0.62930917 nan 0.65197626 nan 0.63093528 nan
0.6622891 nan 0.66066299 nan 0.62453342 0.5
0.61878868 0.50725034 0.62306283 0.5246922 0.62169566 0.55277588
0.62159224 0.60053335 0.61731809 0.58637955 0.62593561 0.61532799
0.63475995 0.61411716 0.62302782 0.50725034 0.61858104 0.51901668
0.64047049 0.577087 0.63185297 0.60546544 0.62591769 0.60510146
0.6289631 0.59086134 0.65353479 0.65626832 0.65480099 0.62868787
0.67813986 nan 0.67196274 nan 0.67813986 nan
0.68684369 nan 0.68556283 nan 0.6869129 nan
0.68409224 nan 0.68407514 nan 0.60172709 nan
0.59300616 nan 0.6516994 nan 0.62930917 nan
0.65197626 nan 0.63093528 nan 0.6622891 nan
0.66066299 nan]
warnings.warn(
Best parameters are:
{'optionalwhitening__n_components': 2, 'optionalwhitening__process': False, 'slimvector__keep_diagonal': True, 'svc__C': 1, 'svc__gamma': 'auto', 'toepochs__n': 30, 'xdawncovariances__nfilter': 1}
[QClass] Initializing Quantum Classifier
[QClass] Quantum simulation will be performed
GPU optimization disabled. No device found.
[QClass] Fitting: (513, 7)
[QClass] Feature dimension = 7
[QClass] Quantum backend = AerSimulator('aer_simulator_statevector')
[QClass] seed = 42
[QClass] SVM initiating algorithm
[QClass] Training...
[QClass] Prediction finished.
[QClass] Prediction finished.
Print the results of direct classification of fraud records
SVM/QSVM pipeline use the loans preceding the actual fraud, without the fraud itself. RandomForest uses only the fraud record itself.
print("----Training score:----")
print(
f"Classical SVM: {train_score_svm:.3f}\
\nQuantum SVM: {train_score_qsvm:.3f}\
\nClassical RandomForest: {train_score_rf:.3f}" # noqa: E231
)
print("----Testing score:----")
print(
f"Classical SVM: {score_svm:.3f}\
\nQuantum SVM: {score_qsvm:.3f}\
\nClassical RandomForest: {score_rf:.3f}" # noqa: E231
)
----Training score:----
Classical SVM: 0.696
Quantum SVM: 0.743
Classical RandomForest: 1.000
----Testing score:----
Classical SVM: 0.727
Quantum SVM: 0.738
Classical RandomForest: 0.890
Unsupervised classification¶
We will now predict whether or not the fraud was a collusion or not. This is a two-stage process:
We have the no-aware ERP method (namely RandomForest) to predict whether or not the transaction is a fraud;
If the fraud is characterized, we use the QSVC pipeline to predict whether or not it is a collusion.
class ERP_CollusionClassifier(ClassifierMixin):
def __init__(self, erp_clf, row_clf, threshold=0.5):
self.row_clf = row_clf
self.erp_clf = erp_clf
self.threshold = threshold
def fit(self, X, y):
# Do not apply: Classifiers are already fitted
return self
def predict(self, X):
y_pred = self.row_clf.predict(X).astype(float)
collusion_prob = self.erp_clf.predict_proba(X)
y_pred[y_pred == 1] = collusion_prob[y_pred == 1, 1].transpose()
y_pred[y_pred >= self.threshold] = 1
y_pred[y_pred < self.threshold] = 0
return y_pred
# Plot the temporal response of collusion vs no-collusion.
y_pred = ERP_CollusionClassifier(gs.best_estimator_, rf).predict(X)
plot_ERPs(X, y_pred, best_n, "Collusion", "No-fraud & Fraud without collusion")
# Let's predict the type of fraud using our two-stage:
# The y_pred here contains 1 if the fraud is a possible collusion or 0 else,
# i.e: not a fraud or not a collusion fraud
y_pred = ERP_CollusionClassifier(gs.best_estimator_, rf).predict(X_test)
# We will get the epochs associated with these frauds
high_warning_loan = np.concatenate(ToEpochs(n=best_n).transform(X_test[y_pred == 1]))
# and from there the IPs of incriminated terminals
# and the IDs of the suspicious customers
# for further investigation:
high_warning_ip = le.inverse_transform(high_warning_loan[0, :].astype(int))
high_warning_id = high_warning_loan[3, :].astype(str)
print("IP involved in probable collusion: ", high_warning_ip)
print("ID involved in probable collusion: ", high_warning_id)

IP involved in probable collusion: ['212.106.242.162' 'cIAPPLh10.6N0wM4ARhiBJsBHQV+Gp9t1JSCWRS'
'AASAMSSMN98W+myopFV3xLfLXPpc671gBopqxs='
'CIAPPLPh8.4LMAAdTEx0HAt5Hn/rjpGoUzwq3c='
'AASAMSSMJ71PrvjQ1XngEy3vfvw46qzD9ACAPM='
'cIAPPLh10.54of6TUOv9J80i50osF59snVzRXE='
'AAHUAWDUBLX6ILAAHbRfgMer89SGAogpZMa754='
'cIAPPLh10.5XVm6U+g4RN+dr9Utws066iSWhm8=' '92.57.178.110'
'AAHUAWVOGL2y/wT7tZtu00ovOubi853unm3tZU='
'cIAPPLh12.1kOdYlcHr7FwBfjvAPoxLQ71PPu81'
'AASAMSSMN95L3uZrtx6tOEGJ//SD0Pm0XSeeSM=' '88.8.230.21'
'AAXIAOMI9__k1zRoWxyApdUUBwHQ9nPzISiKE4='
'AAXIAORedmi4tg0Q6ZQTVvQguGADYXx9nLbRZA='
'AAHUAWSNELXVBV7v7HrfBNzD0XmU9shc+8hV5U='
'aAGOOGsdkgpwRQCBVH3jlMgkwowvuoqaMww+0I='
'aAGOOGsdkgpwRQCBVH3jlMgkwowvuoqaMww+0I='
'aAGOOGsdkgpwRQCBVH3jlMgkwowvuoqaMww+0I='
'aAGOOGsdkgpwRQCBVH3jlMgkwowvuoqaMww+0I=' '94.198.88.169'
'AASAMSSMA40djuMytZawwvC7YwUOdDelEpILTs='
'AAHUAWFIGLXUGqbL13Kxt3EOyOwwaYbCLmxvQM=' '154.60.232.64' '154.60.232.64'
'AAXIAORedmibeKhJAMmLo+OaPDdVb17ZSzsqKk='
'cIAPPLPh9.3uXMjYi89igmNgVUpYj1wwCHFjJM='
'AASAMSSMG96w8b3jDrQmA6SduW6nrq0OTUBMF4=' '90.162.11.94'
'aAHUAWLDNL2vnN7JF8NcIw1+sPQrC59Oqq2Y38=']
ID involved in probable collusion: ['147.0' '29189.0' '4663.0' '123388.0' '225.0' '197885.0' '106398.0'
'58615.0' '131445.0' '115705.0' '153.0' '2381.0' '19903.0' '4302.0'
'109356.0' '-17845.0' '425289.0' '25281.0' '425288.0' '625676.0'
'16781.0' '6784.0' '115486.0' '79273.0' '48408.0' '23841.0' '84.0'
'-10689.0' '-23925.0' '291386.0']
References¶
Total running time of the script: (8 minutes 14.652 seconds)