This is the problem statement

## UPI Synthetic Data Hackathon Instruction Manual

## Overview

Welcome to the UPI Fraud Detection Hackathon! In this event, you will be
tasked with building a fraud detection model using machine learning algorithms on the synthetic data generated from the UPI data. The model will be tested on how accurately it can predict fraudulent transactions from the synthetic data.
<br/>

The synthetic data was generated using a Generative AI model, which was trained on a curated sample of UPI data.
UPI interface facilitates inter-bank peer-to-peer and person-to-merchant
transactions. It is used on Mobile devices to instantly transfer funds
between two bank accounts. UPI also lets you make payments by scanning
QR codes at your nearby stores. UPI is designed to promote financial
inclusion and provide access to banking services for individuals who may
not have access to traditional banking services.

## Participants in UPI Transaction:
1. Remitter bank
2. Beneficiary bank
3. Payer PSP
4. Payee PSP
5. NPCI
6. Payer/Payee

## UPI has specific functionalities such as below:
1. **One time mandate:**  The one-time mandate gives you the facility
to schedule a payment in the future date.
2. **Recurring mandate :** The autopay allows users to make recurring
transactions automatically through UPI apps.
3. **E-RUPI:** It's a digital voucher which is issued to the end user
by any sponsor with the help of the certified bank happening in UPI
platform. The e-RUPI would be shared with the users for a specific
use by organisations via SMS or QR code.
4. **PPI on UPI:** UPI transactions are real-time transactions between
two bank accounts, whereas PPI transactions involve prepaid payment
instruments like mobile wallets, gift cards, and other virtual
payment modes.
5. **123Pay (voice-based payments):** The UPI 123PAY
functionality allows you to make UPI payments over a phone call
through bank's IVR number 9188-123-123 i.e. no need to have
smartphone or internet connectivity.
6. **UPI Lite (Single FA payments):** With UPI Lite, you can make
payments of up to ₹200 INR without the use of a UPI PIN on the
TPAPs.
7. **UDIR (Unified Dispute and Issue Resolution) mechanism:** To
raise dispute regarding transactions if any.
8. **Credit card on UPI:** Credit cards on UPI gives our customers
access to the ease and convenience of transacting on UPI while they
continue to enjoy the benefits of a credit card. They do not even
have to carry the card with them., etc.


## UPI Fraud Model
The NPCI helps the banks and customers to carry out secure payment transactions by identifying and detecting fraud that can take place on the UPI network. At a high level, there are primarily two ways that UPI fraud occurs.

1. **Social Engineering Frauds**
2. **Technical Frauds**

The NPCI makes predictions about a transaction that will be fraudulent
or not and stops the transaction if it is fraudulent. This helps in
regulating such types of fraud and preventing frauds from taking place
through UPI. Scammers misuse the payment request option in UPI-enabled
apps to source the PIN or OTP necessary to authorize a transaction. They
initiate a payment request and accordingly contacts the individual to
ask for the OTP or PIN, which they say is needed on their end to
complete a transaction.

#### Social Engineering Frauds <hr/>
People can be tricked into disclosing their bank information, such as
card numbers and UPI pins, by engaging in social engineering fraud.
Typically, this is done by calling the target and persuading them to
disclose the information, or by installing screen sharing software on
the target's phone and trying to extract the information.


#### Technical Frauds <hr/>
Technical types of frauds are those frauds that can arise when there are
some vulnerabilities in the devices or apps are certain components
within the system, fraudsters try to leverage these loopholes to fraud
the victims.
<br/>

## Dataset

The dataset for this hackathon includes a set of anonymized synthetic transaction records, labelled as either fraudulent (Y=1) or genuine (Y=0). 

The dataset contains a mix of profiling-features and transaction level raw features. The features such as: txn_id, trndate, payee_bank, payee_tpap, txn_type, initiation_mode, payer_requested_amount, payer_tpap, payer_bank, payee_account_number, payer_address, etc. are the transaction level features. 
The profiling features are the features that were created from the historical data of the payee or the the payer for difference scenarios.  

The data is provided in a CSV format and includes the following variables. Their description are as follows:
<br/>

| **Features**              | **Description** |
| :---------------- | :------ | 
|txn_id| Serial Number|
|trndate|Completion Timestamp of transaction|
|payee_bank|The payee/s bank|
|payee_tpap|Third party app used by the payee|
|txn_type|Type of transaction (Pay or Collect)|
|initiation_mode|Additional condition to identify variants of UPI initmode = '6'  refers to UPI Tap and Pay|
|payer_requested_amount|Transaction amount|
|payer_tpap|Third party app used by the payer|
|payer_bank|The payer's bank|
|payee_account_number|The payee's account number|
|payer_address|The payers vpa address|
|fraud|Target column. If the fraud flag is 1, The particular transaction has been marked as fraud.|
|py_min_amt_payee|Minimum transaction amount of a payee account has as a payee in last 90 days|
|py_max_amt_payee|Maximum transaction amount of a payee account has as a payee in last 90 days|
|py_cnt_error_u16_payee|Count of No of U16 Error a payer account has as payee in last 90 days|
|py_cnt_error_u30_payee|Count of No of U30 error codes a payer account has as payee in last 90 days|
|nft_reqregmob_cnt|Separate NFT table based on mobile number (last 24 hours)|
|pr_min_amt_payee|Minimum transaction amount of a payee account has as a payer in last 90 days|
|pr_max_amt_payee|Maximum transaction amount of a payee account has as a payer in last 90 days|
|py_min_amt_payer|Minimum transaction amount a payer account has as a payee in last 90 days|
|py_max_amt_payer|Maximum transaction amount payer account has as a payee in last 90 days|
|py_avg_amt_payer|Average of transaction amount a payer account has as a payee in last 90 days|
|mas_deviceid_txncnt|Count of no. of transaction done with that device id in last 90 days|
|mas_deviceid_act_days|Count active days for that device id in last 90 days|
|mas_vpa_py_txncnt_payer|Count of transactions for that VPA of a payer account as payee in last 90 days|
|mas_bnkifsc_txncnt_prpy_pypr|Count of transactions between the payee and payer in last 90 days|
|mas_vpa_pr_cnt_days_payer|Count of active days for that VPA of a payer account as payee in last 90 days|
|mas_med_amt_mob_payer|Median of transaction amount of the payer account for the mobile number as a payer in last 90 days|
|mas_cnt_days_mob_payee|Count of distinct active days a payee account has as a payer for the payer mobile number in last 90 days|
|bad_trust_rank_payer|Bad score for remitter/payer generated from trust rank algorithm using graph|
|good_trust_rank_payer|Good score for remitter/payer generated from trust rank algorithm using graph|
|bad_trust_rank_payee|Bad score for beneficiary/payee generated from trust rank algorithm using graph|
|good_trust_rank_payee|Good score for beneficiary/payee generated from trust rank algorithm using graph|
|fail_by_sec_txn_ratio_payer|The ratio of total fail count to total success count as a payee by the payer|
|fail_by_sec_amt_ratio_payer|The ratio of total fail amount to total success amount as a payee by the payer|
|less5rs_by_act_ratio_payer|The ratio of count of less than 5rs transactions  received as a payee by payer|
|reg_txn_amts_payer|The regular transaction amount count (mode count of the transaction amount) as a payee by the payer|
|py_12_17_cnt_gain_rate_payer|The ratio of total count of successful transactions done in between 12pm to 17 pm  to the active days in last 90 days as payee by the payer|
|py_17_22_cnt_gain_rate_payer|The ratio of total count of successful transactions done in between 17pm to 22 pm  to the active days in last 90 days as payee by the payer|
|pr_max_amt_payer|Maximum of transaction amount a payer account has as a payer in the last 90 days|
|pr_cnt_zm_payee|Count of ZM declines as payer by the payee|
|p2p_p2m_inter_cnt_payee|Count of all the payer side interactions as payee|
|less5rs_by_act_ratio_payee|The ratio of count of less than 5rs transactions  received as a payee by payee|
|reg_txn_amts_payee|The regular transaction amount count (mode count of the transaction amount) as a payee by the payee|
|pr_med_amt_payer|Median of transaction amount for included pay and collect a payer account has as payer in last 90 days|
|pr_cnt_days_payer|Count of No of distinct Days a payer account has as payer in last 90 days|
|pr_cnt_paid_payer|Count of Distinct payee accounts a payer account has as a payer in the last 90 days|
|mas_vpa_py_txncnt_payee|Count of transactions for that VPA of a payee account as payer in last 90 days|
|mas_vpa_py_cnt_days_payee|Count of distinct days for that VPA of a payee account as payer in last 90 days|
|py_07_12_amt_gain_rate_payee|The ratio of total sum of successful transaction amounts done in between 12pm to 17 pm  to the active days in last 90 days as payee by the payee|
|py_12_17_cnt_gain_rate_payee|The ratio of total count of successful transactions done in between 12pm to 17 pm  to the active days in last 90 days as payee by the payee|
|py_17_22_amt_gain_rate_payee|The ratio of total sum of successful transaction amounts done in between 17pm to 2 pm  to the active days in last 90 days as payee by the payee|
|py_07_12_cnt_gain_rate_payee|The ratio of total count of successful transactions done in between 07am to 12 pm  to the active days in last 90 days as payee by the payee|
|py_cnt_days_payee|Count of no. of Active Days a payee has as a payee in last 90 days|
|pr_cnt_zm_payer|Count of ZM declines as payer by the payer|
|pr_07_12_amt_sent_rate_payee|The ratio of total sum of successful transaction amounts done in between 7am to 12 pm  to the active days in last 90 days as payer by the payee|
|pr_12_17_amt_sent_rate_payee|The ratio of total sum of successful transaction amounts done in between 12pm to 17 pm  to the active days in last 90 days as payer by the payee|
|pr_17_22_amt_sent_rate_payee|The ratio of total sum of successful transaction amounts done in between 17pm to 22 pm  to the active days in last 90 days as payer by the payee|
|pr_07_12_cnt_sent_rate_payee|The ratio of total count of successful transactions done in between 7am to 12 pm  to the active days in last 90 days as payer by the payee|
|pr_12_17_cnt_sent_rate_payee|The ratio of total count of successful transactions done in between 12pm to 17 pm  to the active days in last 90 days as payer by the payee|
|pr_17_22_cnt_sent_rate_payee|The ratio of total count of successful transactions done in between  17pm to 22 pm  to the active days in last 90 days as payer by the payee|
|mas_minasdt_mob_payer|Minimum asdt of a payer account for the mobile number and account number as payer in last 90 days|
|mas_minasdt_mob_payee|Minimum transaction date of a payee account for the payer mobile number as a payer in last 90 days|
|mas_vpa_pr_minasdt_payer|Minimum transaction date for that VPA of a payer account as payee in last 90 days|
|mas_vpa_pr_maxasdt_payer|Maximum transaction date for that VPA of a payer account as payee in last 90 days|
|mas_vpa_py_minasdt_payee|Minimum transaction date for that VPA of a payee account as payer in last 90 days|
|mas_vpa_py_maxasdt_payee|Maximum transaction date for that VPA of a payee account as payer in last 90 days|
|mas_maxasdt_mob_payer|Maximum asdt of a payer account for the mobile number and account number as payer in last 90 days|
|mas_maxasdt_mob_payee|Maximum transaction date of a payee account as a payer for the payer mobile number in last 90 days|
|upi_min_dt_payer|Date of Payers first appearance on UPI platform in the last 90 days|
|py_max_asdt_payer|Date of Latest Transaction by a payer as a payee in the last 90 days|
|upi_min_dt_payee|Date of First Transaction of a payee account has as a payer in the last 90 days|
|py_max_asdt_payee|Date of Latest Transaction of a payee account has as a payee in last 90 days|
|fraud|Target column. If the fraud flag is 1, The particular transaction has been marked as fraud.|

## Files to be submitted

* **model_testing.py** - Python file for testing the model performance.
* **artifacts.zip** - Zip file containing all artifacts/model files and features that were used or created.
* **requirements.txt** - A text file for required pacakages and versions.

## Rules
### To participate in this hackathon, you must agree to the following rules:
* You must use the provided synthetic dataset for your model training and evaluation.
* You must submit below files with the same file names:
    
    * **model_testing.py** -  You must mention all logic used while training like preprocessing and feature engineering. It must contain **class** and all the logic should be written inside the **predict** function only.
    * **artifacts.zip** -  All the artifacts like model and features used for training the model should saved as a **pickle file(.pkl)** and stored in the artifacts folder. This folder has to be zipped and provided.
    * **requirements.txt** -  All the dependencies used with specified versions of the packages must be provided.
* Your model must be able to predict whether a transaction is fraudulent or not and provide an output of 0 or 1 only.
* Your model's prediction time per transaction should not exceed **10 milliseconds**.
* Your model should **not exceed a maximum file size of 100MB**.
* You must adhere to the code of conduct outlined in this hackathon.

## Reference Codes
* Refer to the hyperlink for example code for model training and generation of artifacts. (No need to submit this file) <br/>
[model_training.py](https://git.npci.org.in/synthetic-data-competition/upi-synthetic-data-competition./blob/master/model_training.py)
* Refer to the hyperlink for example code for model_testing.py (You can modify and submit this file) <br/>
[model_testing.py](https://git.npci.org.in/synthetic-data-competition/upi-synthetic-data-competition./blob/master/model_testing.py)
* Refer to below hyperlink for understanding how your model will be evaluated. (No need to submit this) <br/>
[model_testing_test.py](https://git.npci.org.in/synthetic-data-competition/upi-synthetic-data-competition./blob/master/model_testing_test)


## Evaluation Criteria
Your model submissions will be judged based on the following criteria:

* **Accuracy:** The ability of your model to accurately predict fraudulent transactions.
* **Efficiency:** The speed and computational efficiency of your model.
* **Innovation:**  The uniqueness and creativity of your approach to fraud modelling.
* **Presentation:** The quality and clarity of your code and documentation.

The evaluation matrics are **(R * 100) - (10000 * FPR), Precision, recall and F1 score** as well.

Where, R = Recall, FPR = False Positive Rate.

## Code of Conduct
All participants in this hackathon must abide by the following code of conduct:
* Respectful behavior: Participants must treat each other with respect and professionalism, and refrain from any discriminatory or offensive language or behavior.
* Collaboration: Participants are encouraged to collaborate and share ideas but must not plagiarize or infringe on the work of others.
* Ethical considerations: Participants must consider the ethical implications of their work and ensure that their model does not perpetuate or exacerbate existing biases or discrimination.

## **Good Luck and Have Fun!!**

these are the relevant code files

# model_training.py

import pandas as pd
import numpy as np
import pickle
import os
import itertools
from tqdm import tqdm
from datetime import datetime

from sklearn.model_selection import train_test_split, StratifiedKFold
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.metrics import recall_score, precision_score, f1_score, roc_auc_score, confusion_matrix

import xgboost as xgb

def load_data(file_path):
    """
    Load the dataset from a CSV file.
    """
    date_columns = [
        'trndate', 'upi_min_dt_payer', 'upi_min_dt_payee',
        'py_max_asdt_payer', 'py_max_asdt_payee',
        'mas_minasdt_mob_payer', 'mas_minasdt_mob_payee',
        'mas_vpa_pr_minasdt_payer', 'mas_vpa_pr_maxasdt_payer',
        'mas_vpa_py_minasdt_payee', 'mas_vpa_py_maxasdt_payee',
        'mas_maxasdt_mob_payer', 'mas_maxasdt_mob_payee'
    ]
    df = pd.read_csv(file_path, parse_dates=date_columns, dayfirst=True, infer_datetime_format=True)
    return df

def preprocess_data(df, label_encoders=None, scaler=None, fit=True):
    """
    Preprocess the dataset:
    - Drop irrelevant columns
    - Handle missing values
    - Encode categorical variables
    - Feature engineering on date columns
    - Scale numerical features
    """
    # Drop identifier and irrelevant columns
    drop_cols = ['txn_id', 'trndate', 'payee_account_number', 'payer_address']
    df = df.drop(columns=drop_cols, errors='ignore')
    
    # Handle missing values
    df.fillna(-999, inplace=True)  # Using a placeholder for missing values
    
    # Encode categorical variables
    categorical_cols = ['payee_bank', 'payee_tpap', 'txn_type', 'initiation_mode',
                        'payer_tpap', 'payer_bank']
    
    if label_encoders is None:
        label_encoders = {}
        for col in categorical_cols:
            le = LabelEncoder()
            df[col] = le.fit_transform(df[col].astype(str))
            label_encoders[col] = le
    elif not fit:
        for col in categorical_cols:
            le = label_encoders[col]
            df[col] = df[col].astype(str).apply(lambda x: x if x in le.classes_ else 'Unknown')
            # Handle unseen labels by adding 'Unknown' if not present
            if 'Unknown' not in le.classes_:
                le.classes_ = np.append(le.classes_, 'Unknown')
            df[col] = le.transform(df[col])
    
    # Feature engineering: Extract year, month, day from date columns
    date_cols = [
        'upi_min_dt_payer', 'upi_min_dt_payee', 'py_max_asdt_payer', 'py_max_asdt_payee',
        'mas_minasdt_mob_payer', 'mas_minasdt_mob_payee',
        'mas_vpa_pr_minasdt_payer', 'mas_vpa_pr_maxasdt_payer',
        'mas_vpa_py_minasdt_payee', 'mas_vpa_py_maxasdt_payee',
        'mas_maxasdt_mob_payer', 'mas_maxasdt_mob_payee'
    ]
    
    for col in date_cols:
        if col in df.columns:
            df[col] = pd.to_datetime(df[col], errors='coerce')
            df[col + '_year'] = df[col].dt.year.fillna(0).astype(int)
            df[col + '_month'] = df[col].dt.month.fillna(0).astype(int)
            df[col + '_day'] = df[col].dt.day.fillna(0).astype(int)
            df = df.drop(columns=[col])
    
    # Scale numerical features
    if scaler is None and fit:
        scaler = StandardScaler()
        numeric_cols = df.select_dtypes(include=['int64', 'float64']).columns.tolist()
        numeric_cols.remove('fraud')  # Exclude target variable
        df[numeric_cols] = scaler.fit_transform(df[numeric_cols])
    elif scaler is not None and not fit:
        numeric_cols = df.select_dtypes(include=['int64', 'float64']).columns.tolist()
        if 'fraud' in numeric_cols:
            numeric_cols.remove('fraud')  # Exclude target if present
        df[numeric_cols] = scaler.transform(df[numeric_cols])
    
    return df, label_encoders, scaler

def train_evaluate_model(model, X, y, params, cv_splits=3):
    """
    Train and evaluate the model using cross-validation.
    
    Returns the average AUC across the folds.
    """
    skf = StratifiedKFold(n_splits=cv_splits, shuffle=True, random_state=42)
    aucs = []
    
    for train_index, test_index in skf.split(X, y):
        X_train_fold, X_test_fold = X.iloc[train_index], X.iloc[test_index]
        y_train_fold, y_test_fold = y.iloc[train_index], y.iloc[test_index]
        
        model.set_params(**params)
        model.fit(X_train_fold, y_train_fold)
        y_pred_prob = model.predict_proba(X_test_fold)[:,1]
        auc = roc_auc_score(y_test_fold, y_pred_prob)
        aucs.append(auc)
    
    return np.mean(aucs)

def manual_grid_search(X, y, param_grid, cv_splits=3):
    """
    Perform manual grid search with a progress bar.
    Returns the best parameters and best score.
    """
    all_params = list(itertools.product(*param_grid.values()))
    param_names = list(param_grid.keys())
    best_score = -np.inf
    best_params = None
    results = []
    
    model = xgb.XGBClassifier(
        objective='binary:logistic',
        eval_metric='auc',
        use_label_encoder=False,
        n_jobs=-1,
        tree_method='gpu_hist',  # Enable GPU acceleration
        random_state=42
    )
    
    with tqdm(total=len(all_params), desc="Grid Search Progress") as pbar:
        for param_tuple in all_params:
            params = dict(zip(param_names, param_tuple))
            try:
                score = train_evaluate_model(model, X, y, params, cv_splits)
                results.append((params, score))
                if score > best_score:
                    best_score = score
                    best_params = params
            except Exception as e:
                print(f"Error with parameters {params}: {e}")
            pbar.update(1)
    
    return best_params, best_score, results

def evaluate_model(model, X_test, y_test):
    """
    Evaluate the model using various metrics.
    """
    y_pred_prob = model.predict_proba(X_test)[:,1]
    y_pred = model.predict(X_test)
    
    recall = recall_score(y_test, y_pred)
    precision = precision_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred)
    roc_auc = roc_auc_score(y_test, y_pred_prob)
    
    cm = confusion_matrix(y_test, y_pred)
    if cm.shape == (2,2):
        tn, fp, fn, tp = cm.ravel()
        fpr = fp / (fp + tn) if (fp + tn) > 0 else 0
    else:
        fpr = 0  # Handle cases where one class is missing
    
    # Custom Metric: (R * 100) - (10000 * FPR)
    R = recall
    custom_metric = (R * 100) - (10000 * fpr)
    
    print(f"Recall: {recall:.4f}")
    print(f"Precision: {precision:.4f}")
    print(f"F1 Score: {f1:.4f}")
    print(f"AUC: {roc_auc:.4f}")
    print(f"False Positive Rate: {fpr:.6f}")
    print(f"Custom Metric (R * 100 - 10000 * FPR): {custom_metric:.4f}")
    
    metrics = {
        'recall': recall,
        'precision': precision,
        'f1_score': f1,
        'auc': roc_auc,
        'fpr': fpr,
        'custom_metric': custom_metric
    }
    
    return metrics

def save_artifacts(model, label_encoders, scaler, output_dir='artifacts'):
    """
    Save the trained model and preprocessing objects.
    """
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    
    with open(os.path.join(output_dir, 'model.pkl'), 'wb') as f:
        pickle.dump(model, f)
    
    with open(os.path.join(output_dir, 'label_encoder.pkl'), 'wb') as f:
        pickle.dump(label_encoders, f)
    
    with open(os.path.join(output_dir, 'scaler.pkl'), 'wb') as f:
        pickle.dump(scaler, f)
    
    print(f"Artifacts saved in '{output_dir}' directory.")

def main():
    # Load data
    df = load_data('upi_synthetic_dataset.csv')  # Replace with your actual CSV file path
    
    # Preprocess data
    df_processed, label_encoders, scaler = preprocess_data(df, fit=True)
    
    # Split into features and target
    X = df_processed.drop('fraud', axis=1)
    y = df_processed['fraud']
    
    # Split into training and testing sets
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42, stratify=y
    )
    
    # Define hyperparameter grid
    param_grid = {
        'n_estimators': [100, 200],
        'max_depth': [4, 6, 8],
        'learning_rate': [0.01, 0.1, 0.2],
        'subsample': [0.8, 1],
        'colsample_bytree': [0.8, 1]
    }
    
    # Perform manual grid search with progress bar
    best_params, best_score, results = manual_grid_search(X_train, y_train, param_grid, cv_splits=3)
    
    print(f"\nBest Parameters: {best_params}")
    print(f"Best AUC Score: {best_score:.4f}")
    
    # Train the best model on the entire training set
    best_model = xgb.XGBClassifier(
        objective='binary:logistic',
        eval_metric='auc',
        use_label_encoder=False,
        n_jobs=-1,
        tree_method='gpu_hist',  # Enable GPU acceleration
        random_state=42,
        **best_params
    )
    
    best_model.fit(X_train, y_train)
    
    # Evaluate the model on the test set
    metrics = evaluate_model(best_model, X_test, y_test)
    
    # Save artifacts
    save_artifacts(best_model, label_encoders, scaler)
    
    # Save metrics
    with open(os.path.join('artifacts', 'metrics.txt'), 'w') as f:
        for key, value in metrics.items():
            f.write(f"{key}: {value}\n")
    
    # Optionally, save the grid search results
    results_df = pd.DataFrame(results, columns=['params', 'auc_score'])
    results_df.to_csv(os.path.join('artifacts', 'grid_search_results.csv'), index=False)
    print("Grid search results saved.")
    
if __name__ == '__main__':
    main()


# model_testing.py

import pandas as pd
import numpy as np
import pickle
import os
from datetime import datetime

class Hackathon:
    def __init__(self):
        """
        Initialize the model by loading all necessary artifacts.
        """
        # Load artifacts
        artifacts_dir = 'artifacts'
        with open(os.path.join(artifacts_dir, 'model.pkl'), 'rb') as f:
            self.model = pickle.load(f)
        
        with open(os.path.join(artifacts_dir, 'label_encoder.pkl'), 'rb') as f:
            self.label_encoders = pickle.load(f)
            
        with open(os.path.join(artifacts_dir, 'scaler.pkl'), 'rb') as f:
            self.scaler = pickle.load(f)
            
        # Store feature names used during training
        self.feature_names = None
        if hasattr(self.model, 'feature_names_in_'):
            self.feature_names = self.model.feature_names_in_
    
    def _preprocess_dates(self, df):
        """
        Process date columns to extract year, month, day features.
        """
        date_cols = [
            'upi_min_dt_payer', 'upi_min_dt_payee', 'py_max_asdt_payer', 'py_max_asdt_payee',
            'mas_minasdt_mob_payer', 'mas_minasdt_mob_payee',
            'mas_vpa_pr_minasdt_payer', 'mas_vpa_pr_maxasdt_payer',
            'mas_vpa_py_minasdt_payee', 'mas_vpa_py_maxasdt_payee',
            'mas_maxasdt_mob_payer', 'mas_maxasdt_mob_payee'
        ]
        
        for col in date_cols:
            if col in df.columns:
                # Explicitly specify date format
                df[col] = pd.to_datetime(df[col], format='%d/%m/%Y', errors='coerce')
                df[col + '_year'] = df[col].dt.year.fillna(0).astype(int)
                df[col + '_month'] = df[col].dt.month.fillna(0).astype(int)
                df[col + '_day'] = df[col].dt.day.fillna(0).astype(int)
                df = df.drop(columns=[col])
        
        return df
    
    def _preprocess_input(self, transaction_dict):
        """
        Preprocess a single transaction dictionary.
        """
        try:
            # Convert dictionary to DataFrame
            df = pd.DataFrame([transaction_dict])
            
            # Drop identifier and irrelevant columns
            drop_cols = ['txn_id', 'trndate', 'payee_account_number', 'payer_address', 'fraud']
            df = df.drop(columns=[col for col in drop_cols if col in df.columns])
            
            # Handle missing values
            df.fillna(-999, inplace=True)
            
            # Encode categorical variables
            categorical_cols = ['payee_bank', 'payee_tpap', 'txn_type', 'initiation_mode',
                              'payer_tpap', 'payer_bank']
            
            for col in categorical_cols:
                if col in df.columns:
                    le = self.label_encoders[col]
                    df[col] = df[col].astype(str).apply(lambda x: x if x in le.classes_ else 'Unknown')
                    if 'Unknown' not in le.classes_:
                        le.classes_ = np.append(le.classes_, 'Unknown')
                    df[col] = le.transform(df[col])
            
            # Process date columns
            df = self._preprocess_dates(df)
            
            # Scale numerical features
            numeric_cols = df.select_dtypes(include=['int64', 'float64']).columns.tolist()
            df[numeric_cols] = self.scaler.transform(df[numeric_cols])
            
            # Ensure columns match training features
            if self.feature_names is not None:
                missing_cols = set(self.feature_names) - set(df.columns)
                extra_cols = set(df.columns) - set(self.feature_names)
                
                # Add missing columns with default values
                for col in missing_cols:
                    df[col] = -999
                
                # Remove extra columns
                df = df.drop(columns=list(extra_cols))
                
                # Reorder columns to match training features
                df = df[self.feature_names]
            
            return df
            
        except Exception as e:
            raise Exception(f"Error in preprocessing: {str(e)}")
    
    def predict(self, transaction_dict):
        """
        Predict whether a transaction is fraudulent.
        
        Args:
            transaction_dict (dict): Dictionary containing transaction details
            
        Returns:
            dict: Prediction result containing:
                - prediction: Binary prediction (0 or 1)
                - probability: Probability of fraud
                - status: Status of prediction
                - message: Additional information
        """
        try:
            # Preprocess input
            df_processed = self._preprocess_input(transaction_dict)
            
            # Make prediction
            fraud_probability = self.model.predict_proba(df_processed)[0][1]
            fraud_prediction = int(fraud_probability >= 0.5)
            
            # return {
            #     'prediction': fraud_prediction,
            #     'probability': float(fraud_probability),
            #     'status': 'success',
            #     'message': 'Prediction successful'
            # }
            return  fraud_prediction
                
            
        except Exception as e:
            return {
                'prediction': None,
                'probability': None,
                'status': 'error',
                'message': f'Error during prediction: {str(e)}'
            }


# model_testing_test.py

import pandas as pd
from datetime import datetime
import pickle
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
import numpy as np

from model_testing import Hackathon

df = pd.read_csv('sample_test.csv')

df = df.replace(np.nan,None)

input_dict = df.T.to_dict()
hackathon_obj = Hackathon()

total_execution_time=0
prediction_list = []
for i in input_dict:
    r = input_dict[i]
    for key in r:
        if r[key] is None:
            r[key]=0
    start_time = datetime.now().second
    prediction = hackathon_obj.predict(r)
    end_time = datetime.now().second
    execution_time = end_time - start_time
    total_execution_time += execution_time
    average_execution_time = (total_execution_time / (i+1)) * 1000   ## Average execution time should not exceed a set target value.
    pred = prediction
    prediction_list.append(pred)
    
df['prediction'] = prediction_list
cm = confusion_matrix(df['fraud'],df['prediction'],labels=[0, 1])
cm[np.isnan(cm)] = 0

tn, fp, fn, tp = cm.flatten()
tn, fp, fn, tp = tn.item(), fp.item(), fn.item(), tp.item()
recall = tp / (tp + fn)
precision = tp/(tp+fp)
f1_score = 2*(precision * recall / (precision + recall))
fpr = fp / (fp + tn)
score = (recall*100)-(10000*fpr)
print('Score:',score)

print('Recall:',recall)
print('Precision:',precision)
print('F1 Score:',f1_score)


these are my scores

(fraud_env) PS C:\Users\DarksouL\Desktop\upi-synthetic-data-competition-master> python .\model_testing_test.py
Score: -1079.0636643167434
Recall: 0.9946808510638298
Precision: 0.5203478260869565
F1 Score: 0.6832610184973739


what am i doing wrong??