Building upon the insights obtained during the Exploratory Data Analysis (EDA) phase, this stage transforms the raw transaction and identity data into a clean, consistent, and model-ready dataset.
The data-wrangling, preprocessing, and feature-engineering workflow includes handling missing values, removing low-information features, encoding categorical variables, scaling numerical features where appropriate, engineering new predictive features, selecting relevant variables, and creating reproducible training, validation, and testing datasets.
The output of this notebook serves as the input for the subsequent ML Model Selection, Tuning & Evaluation phase of the Explainable Fraud Detection and Investigation Platform.
Deliverables
By the end of this notebook, the following artifacts will be produced:
A cleaned and preprocessed dataset suitable for machine learning.
Engineered features designed to improve predictive performance.
Encoded and transformed numerical and categorical variables.
Train, validation, and test datasets.
A reproducible preprocessing pipeline that can be reused for future model training and inference.
1.Import Libraries
Code
# ==========================================================# 1. Import Libraries# ==========================================================# Standard Librariesimport hashlibimport jsonimport randomfrom pathlib import Path# Data Manipulationimport numpy as npimport pandas as pd# Data Visualizationimport matplotlib.pyplot as pltimport seaborn as sns# Scikit-Learn - Preprocessingfrom sklearn.preprocessing import ( RobustScaler, OneHotEncoder)# Scikit-Learn - Imputationfrom sklearn.impute import SimpleImputer# Save preprocessing objectsimport joblib# ----------------------------------------------------------# Display Configuration# ----------------------------------------------------------pd.set_option("display.max_columns", None)pd.set_option("display.max_rows", 100)pd.set_option("display.width", 1200)pd.set_option("display.float_format", "{:.2f}".format)# ----------------------------------------------------------# Plot Configuration# ----------------------------------------------------------sns.set_theme(style="whitegrid")plt.rcParams["figure.figsize"] = (10, 6)# ----------------------------------------------------------# Reproducibility# ----------------------------------------------------------RANDOM_STATE =42np.random.seed(RANDOM_STATE)random.seed(RANDOM_STATE)print("Libraries imported successfully.")print(f"Random State: {RANDOM_STATE}")
# ==========================================================# 2. Load the Dataset# ==========================================================# Define project pathsCURRENT_PATH = Path.cwd().resolve()if CURRENT_PATH.name =="notebooks": PROJECT_ROOT = CURRENT_PATH.parentelif (CURRENT_PATH /"data").exists(): PROJECT_ROOT = CURRENT_PATHelse:raiseFileNotFoundError("Could not identify the project root. ""Run the notebook from the repository root or notebooks folder." )RAW_DATA_PATH = PROJECT_ROOT /"data"/"raw"PROCESSED_DATA_PATH = PROJECT_ROOT /"data"/"processed"PROCESSED_DATA_PATH.mkdir(parents=True, exist_ok=True)print(f"Project Root: {PROJECT_ROOT}")print(f"Raw Data Path: {RAW_DATA_PATH}")required_files = [ RAW_DATA_PATH /"train_transaction.csv", RAW_DATA_PATH /"train_identity.csv"]missing_files = [ path for path in required_filesifnot path.exists()]if missing_files:raiseFileNotFoundError("Missing required files:\n"+"\n".join(str(path) for path in missing_files) )
Project Root: /Users/hshazel/Projects/explainable-fraud-investigation-platform
Raw Data Path: /Users/hshazel/Projects/explainable-fraud-investigation-platform/data/raw
2.2 Load Raw Data
Code
# Load raw input tablestrain_transaction = pd.read_csv( RAW_DATA_PATH /"train_transaction.csv",#low_memory=False)train_identity = pd.read_csv( RAW_DATA_PATH /"train_identity.csv",#low_memory=False # For large datasets, this prevents dtype inference and speeds up loading)print("Datasets loaded successfully.")
Datasets loaded successfully.
2.3 Merge Datasets
Code
# Merge transaction and identity tablesassert train_transaction["TransactionID"].is_uniqueassert train_identity["TransactionID"].is_uniquerows_before_merge =len(train_transaction)df = train_transaction.merge( train_identity, on="TransactionID", how="left", validate="one_to_one")assertlen(df) == rows_before_mergeprint("Datasets merged successfully.")print(f"Rows preserved after merge: {len(df):,}")# Release memory by deleting the original DataFramesdel train_transactiondel train_identity# Release memory by invoking garbage collectionimport gcgc.collect()
Datasets merged successfully.
Rows preserved after merge: 590,540
16
Code
# ==========================================================# Create Working Copy# ==========================================================df_processed = df.copy()print("Working copy created successfully.")print(f"Shape: {df_processed.shape}")# Saving the original feature count for referencefeature_counts = {}feature_counts["Original merged dataset"] = df_processed.shape[1]
Working copy created successfully.
Shape: (590540, 434)
The transaction and identity tables from the IEEE-CIS Fraud Detection dataset were successfully loaded and merged using the TransactionID field.
Basic integrity checks confirmed that the target variable is present, the dataset contains no duplicate transaction identifiers, and the merged dataset is ready for preprocessing.
The resulting dataset will serve as the foundation for the data wrangling, preprocessing, feature engineering, and model preparation steps performed throughout this notebook.
3. Review Data Quality
Before applying preprocessing techniques, it is important to verify the overall quality and consistency of the merged dataset.
This review confirms that the dataset is structurally sound and identifies any issues that must be addressed during preprocessing, such as duplicate observations, missing values, inconsistent data types, low-information variables, and potential infinite values.
The findings from this section will guide the preprocessing strategy implemented throughout the remainder of this notebook.
============================================================
Near-Constant Features
============================================================
Near-Constant Features: 11
Feature
0
C3
1
V107
2
V111
3
V113
4
V117
5
V118
6
V119
7
V120
8
V121
9
V122
10
V305
Data Quality Findings
The dataset passed the initial quality review and is suitable for preprocessing.
The integrity checks confirmed that no duplicate transactions or duplicate observations are present, the target variable contains no missing values, and no infinite numerical values were detected.
A substantial number of variables contain missing values, which is expected for the IEEE-CIS Fraud Detection dataset and will be addressed during the missing value treatment stage. Several variables also exhibit little or no variability, making them potential candidates for removal during feature selection.
Overall, the dataset is structurally consistent and ready for data wrangling, preprocessing, and feature engineering.
4. Deterministic Feature Engineering
This section creates features using row-level transformations that do not learn parameters from the overall dataset. These transformations can therefore be performed before splitting without introducing data leakage.
Any transformation that learns population statistics—such as imputation values, frequency maps, category sets, scaling parameters, low-variance filters, or correlation thresholds—is postponed until after the train, validation, and test split.
Code
# ==========================================================# 4.1 Create Deterministic Features# ==========================================================# Conversion constants used to transform TransactionDT from seconds into# elapsed day and hour components.SECONDS_PER_DAY =24*60*60SECONDS_PER_HOUR =60*60# Create a separate feature frame with the same index as df_processed so# every engineered value remains aligned with its original transaction.engineered_df = pd.DataFrame(index=df_processed.index)# TransactionAmt_Log: apply log(1 + amount) to preserve zero-valued amounts# while reducing the right skew of the original transaction amount.engineered_df["TransactionAmt_Log"] = np.log1p( df_processed["TransactionAmt"])# TransactionAmt_Rounded: round the transaction amount to the nearest whole# monetary unit to capture coarse amount patterns and common price points.engineered_df["TransactionAmt_Rounded"] = ( df_processed["TransactionAmt"].round())# TransactionDay: use floor division to count complete 24-hour periods since# the dataset's unspecified time origin. This is an elapsed day, not a date.engineered_df["TransactionDay"] = ( df_processed["TransactionDT"] // SECONDS_PER_DAY).astype("int32")# TransactionHour: take the seconds remaining within each 24-hour period and# floor-divide by 3,600 to obtain an hour value from 0 through 23. Because the# origin and timezone are unknown, this is an elapsed-hour component only.engineered_df["TransactionHour"] = ( (df_processed["TransactionDT"] % SECONDS_PER_DAY) // SECONDS_PER_HOUR).astype("int8")# TransactionDT has an unspecified origin, so an actual weekday/weekend# cannot be inferred safely from the elapsed-day value.# EmailMatch: assign 1 only when both email domains are present and exactly# equal; otherwise assign 0.engineered_df["EmailMatch"] = ( df_processed["P_emaildomain"].notna()& df_processed["R_emaildomain"].notna()& (df_processed["P_emaildomain"] == df_processed["R_emaildomain"])).astype("int8")# P_EmailProvider: replace a missing purchaser email domain with 'unknown'# and keep the text before the first period (for example, gmail.com -> gmail).engineered_df["P_EmailProvider"] = ( df_processed["P_emaildomain"].fillna("unknown").str.split(".").str[0])# R_EmailProvider: apply the same provider extraction to the recipient email# domain, again using 'unknown' when the original value is missing.engineered_df["R_EmailProvider"] = ( df_processed["R_emaildomain"].fillna("unknown").str.split(".").str[0])# CardID: convert the anonymized card1 and card2 fields to strings, replace# missing components with 'unknown', and join them with an underscore. This# creates a composite anonymized card grouping, not a real card number.engineered_df["CardID"] = ( df_processed["card1"].astype("string").fillna("unknown")+"_"+ df_processed["card2"].astype("string").fillna("unknown"))# IsMobile: assign 1 when DeviceType is exactly 'mobile' and 0 otherwise.# Missing DeviceType values therefore receive 0 and are not assumed mobile.engineered_df["IsMobile"] = ( df_processed["DeviceType"] =="mobile").astype("int8")# UnknownDevice: assign 1 when the original DeviceInfo value is missing and# 0 when it is present. This preserves missingness before later imputation.engineered_df["UnknownDevice"] = ( df_processed["DeviceInfo"].isna()).astype("int8")# Append all ten deterministic features to the working dataset by index.df_processed = pd.concat([df_processed, engineered_df], axis=1)print("="*60)print("Deterministic Feature Engineering")print("="*60)print(f"New features created: {engineered_df.shape[1]}")print(f"Dataset shape: {df_processed.shape}")feature_counts["After deterministic feature engineering"] = df_processed.shape[1]
============================================================
Deterministic Feature Engineering
============================================================
New features created: 10
Dataset shape: (590540, 444)
Deterministic Feature Engineering Findings
Ten row-level features were created without using information from other observations. Missing-device status was captured before categorical imputation so that the original missingness signal was preserved. A weekend flag is intentionally not created because TransactionDT is elapsed time from an unspecified origin rather than a known calendar timestamp.
The dataset is now ready for chronological training, validation, and test partitioning before any data-dependent preprocessing is fitted.
5. Train / Validation / Test Split
Transactions are sorted by TransactionDT and split chronologically: the earliest 70% form training, the next 15% form validation, and the latest 15% form the final test set. This better represents deployment on future transactions and allows natural class-prevalence drift to remain visible.
All subsequent preprocessing parameters are learned exclusively from the training period and applied unchanged to later periods.
No TransactionID overlap and chronological boundaries verified.
Dataset
Start day
End day
0
Training
1.0
120.8
1
Validation
120.8
152.2
2
Test
152.2
183.0
Dataset Split Findings
The chronological split preserves transaction order, keeps every TransactionID in exactly one subset, and exposes changes in fraud prevalence across time. It does not force equal class proportions. From this point forward, only the earliest training period is used to learn preprocessing rules and transformation parameters.
6. Missing Value Treatment
Missing-value thresholds and imputation parameters are learned exclusively from the training dataset. The resulting feature-removal list and fitted imputers are then applied unchanged to the validation and test datasets.
============================================================
High-Missing Features
============================================================
Threshold: 95%
Features identified: 9
# ==========================================================# 6.8 Verify Missing Value Treatment# ==========================================================print("="*60)print("Missing Value Verification")print("="*60)print("Training missing values:",int(X_train.isna().to_numpy().sum()))print("Validation missing values:",int(X_validation.isna().to_numpy().sum()))print("Test missing values:",int(X_test.isna().to_numpy().sum()))
============================================================
Missing Value Verification
============================================================
Training missing values: 0
Validation missing values: 0
Test missing values: 0
Missing Value Treatment Findings
Features exceeding 95% missingness were identified using the training dataset only and removed consistently from all three subsets.
Numerical and categorical imputers were fitted exclusively on the training data. The learned medians and most-frequent categories were then applied unchanged to the validation and test data, preventing information from those subsets from influencing preprocessing.
7. Remove Low-Information Features
Constant and near-constant features are detected using the imputed training dataset only. The resulting removal list is then applied consistently to the validation and test datasets.
============================================================
Low-Information Features
============================================================
Constant features: 1
Near-constant features: 33
Unique features to remove: 33
Low-information features were selected exclusively from the training data using a 99.5% dominant-value threshold. Removing the same columns from every subset reduces unnecessary dimensionality while preserving identical feature spaces.
8. Encode Categorical Variables
Categorical preprocessing uses a hybrid strategy. Low-cardinality variables are one-hot encoded, while high-cardinality variables are frequency encoded.
Feature cardinality, frequency maps, and one-hot categories are all learned from the training dataset only. Validation and test observations are transformed using those training-derived artifacts.
Code
# ==========================================================# 8.1 Split Categorical Features by Training Cardinality# ==========================================================MAX_ONEHOT_CATEGORIES =20categorical_features = ( X_train .select_dtypes(include=["object", "string"]) .columns .tolist())low_cardinality_features = [ columnfor column in categorical_featuresif X_train[column].nunique(dropna=False)<= MAX_ONEHOT_CATEGORIES]high_cardinality_features = [ columnfor column in categorical_featuresif X_train[column].nunique(dropna=False)> MAX_ONEHOT_CATEGORIES]cardinality_summary = pd.DataFrame({"Feature": categorical_features,"Training Unique Categories": [ X_train[column].nunique(dropna=False)for column in categorical_features ],"Encoding": ["One-Hot"if column in low_cardinality_featureselse"Frequency"for column in categorical_features ]}).sort_values("Training Unique Categories", ascending=False)print(f"Low-cardinality features: {len(low_cardinality_features)}")print(f"High-cardinality features: {len(high_cardinality_features)}")display(cardinality_summary)
Categorical encoding is now leakage-free. Cardinality decisions, frequency mappings, and one-hot categories were learned from the training dataset only.
Unknown validation or test categories are handled safely: one-hot encoding ignores unseen categories, while unseen high-cardinality values receive a frequency of zero.
9. Scale Numerical Features
The numerical variables in the dataset operate on substantially different scales. For example, transaction amounts, elapsed-time variables, count-based features, and identity-related measurements have different numerical ranges.
Feature scaling is particularly important for algorithms such as Logistic Regression and neural networks, which are sensitive to differences in feature magnitude. Tree-based algorithms such as Decision Trees, Random Forest, and XGBoost are generally less sensitive to scaling.
RobustScaler is used because several numerical variables contain highly skewed distributions and extreme values. The scaler uses the median and interquartile range, making it less sensitive to outliers than standardization based on the mean and standard deviation.
To reduce data leakage, the scaler is fitted exclusively on the training dataset. The fitted transformation is then applied unchanged to the validation and test datasets.
9.1 Identify features to scale
One-hot encoded variables and binary indicators are intentionally excluded from scaling.
Code
# ==========================================================# 9.1 Identify Numerical Features to Scale# ==========================================================numerical_columns = ( X_train .select_dtypes(include=np.number) .columns .tolist())# One-hot columns created by the encoderonehot_columns = [ columnfor column in onehot_feature_namesif column in X_train.columns]# Binary features should remain coded as 0 and 1binary_columns = [ columnfor column in numerical_columnsif X_train[column].nunique(dropna=False) <=2]numerical_features_to_scale = [ columnfor column in numerical_columnsif column notin onehot_columnsand column notin binary_columns]print("="*60)print("Numerical Feature Selection")print("="*60)print(f"Total model features: {X_train.shape[1]}")print(f"Numerical features: {len(numerical_columns)}")print(f"One-hot features excluded: {len(onehot_columns)}")print(f"Binary features excluded: {len(binary_columns)}")print(f"Numerical features selected for scaling: "f"{len(numerical_features_to_scale)}")
============================================================
Numerical Feature Selection
============================================================
Total model features: 433
Numerical features: 433
One-hot features excluded: 55
Binary features excluded: 59
Numerical features selected for scaling: 374
This reduces memory usage and prevents dtype-assignment warnings.
Code
# ==========================================================# 9.2 Prepare Numerical Columns# ==========================================================for dataset in [X_train, X_validation, X_test]: dataset[numerical_features_to_scale] = ( dataset[numerical_features_to_scale] .astype(np.float32) )print("Numerical features converted to float32.")
Numerical features converted to float32.
9.3 Fit the scaler on training data only
Code
# ==========================================================# 9.3 Fit RobustScaler# ==========================================================robust_scaler = RobustScaler()robust_scaler.fit( X_train[numerical_features_to_scale] # We use X_train, not df_processed, to avoid data leakage)print("RobustScaler fitted on the training dataset.")
# ==========================================================# 9.7 Scaling Integrity Checks# ==========================================================print("="*60)print("Scaling Integrity Checks")print("="*60)print("Train/Validation columns match:",list(X_train.columns)==list(X_validation.columns))print("Train/Test columns match:",list(X_train.columns)==list(X_test.columns))print("Missing values in training:",int(X_train.isna().to_numpy().sum()))print("Missing values in validation:",int(X_validation.isna().to_numpy().sum()))print("Missing values in test:",int(X_test.isna().to_numpy().sum()))def count_infinite_values(dataframe):returnint( np.isinf( dataframe.to_numpy(dtype=np.float32) ).sum() )print("Infinite values in training:", count_infinite_values(X_train))print("Infinite values in validation:", count_infinite_values(X_validation))print("Infinite values in test:", count_infinite_values(X_test))print(f"Training shape: {X_train.shape}")print(f"Validation shape: {X_validation.shape}")print(f"Test shape: {X_test.shape}")
============================================================
Scaling Integrity Checks
============================================================
Train/Validation columns match: True
Train/Test columns match: True
Missing values in training: 0
Missing values in validation: 0
Missing values in test: 0
Infinite values in training: 0
Infinite values in validation: 0
Infinite values in test: 0
Training shape: (413378, 433)
Validation shape: (88581, 433)
Test shape: (88581, 433)
Numerical Feature Scaling Findings
A total of 374 numerical features were transformed using RobustScaler.
One-hot encoded variables and binary indicator variables were excluded from scaling to preserve their original binary representation. The remaining numerical variables were scaled using the median and interquartile range calculated from the training dataset, reducing the influence of outliers and differences in feature magnitude.
The scaler was fitted exclusively on the training data and then applied unchanged to the validation and test datasets, preventing data leakage.
Post-scaling verification confirmed that: - All datasets retained identical feature sets consisting of 433 features before correlation filtering. - No missing values or infinite values were introduced during scaling. - The median of most scaled numerical variables is approximately 0, while variables with sufficient variability have an interquartile range close to 1, indicating that the RobustScaler transformation was applied successfully. - Several variables retained an interquartile range of 0 because the majority of their observations share the same value after preprocessing. This behavior is expected for low-variance or highly concentrated features and does not indicate an error.
The scaled datasets are now prepared for machine learning models that are sensitive to feature magnitude, including Logistic Regression and neural networks.
10. Feature Selection
The objective of this section is to reduce unnecessary model complexity by removing features that contribute little predictive information while preserving variables that may improve fraud detection.
Feature selection is performed using data-driven criteria rather than manual inspection. Constant features are removed because they contain no information, and highly correlated numerical variables are eliminated to reduce redundancy.
Model-based feature importance will be applied later during model development to further evaluate feature relevance.
============================================================
Constant Features
============================================================
Constant features found: 0
# ==========================================================# 10.3 Correlation Analysis# ==========================================================continuous_features = [ columnfor column in numerical_features_to_scaleif column in X_train.columns]correlation_matrix = ( X_train[continuous_features] .corr() .abs())upper_triangle = correlation_matrix.where( np.triu( np.ones(correlation_matrix.shape), k=1 ).astype(bool))correlated_features = [ columnfor column in upper_triangle.columnsifany(upper_triangle[column] >0.95)]print("="*60)print("Highly Correlated Features")print("="*60)print(f"Features to remove: {len(correlated_features)}")
============================================================
Highly Correlated Features
============================================================
Features to remove: 74
Feature selection was performed to reduce redundancy and model complexity while retaining one representative feature from each highly correlated group.
First, constant features containing no variability were identified and removed because they cannot contribute to machine learning models. Next, highly correlated numerical features (absolute Pearson correlation greater than 0.95) were eliminated to reduce multicollinearity, simplify the feature space, and improve computational efficiency while retaining much of the shared linear information.
The same features were removed consistently from the training, validation, and test datasets, ensuring that all datasets maintained identical feature spaces throughout the machine learning pipeline.
After the earlier low-information filter, the final constant-feature check found 0 additional constant columns. Correlation filtering then removed 74 features, reducing the model matrix from 433 to 359 features and creating a more compact dataset for model development. Tree-based feature importance will be applied during model training and evaluation rather than during preprocessing.
11. Save Processed Data
The final step in the preprocessing pipeline is to persist the processed datasets and preprocessing artifacts for reuse in subsequent notebooks.
Saving the processed training, validation, and test datasets ensures that model development can begin immediately without repeating the computationally intensive preprocessing steps. This also improves reproducibility and guarantees that every model is trained using exactly the same feature set.
In addition to the processed datasets, all preprocessing objects—including imputers, encoders, scalers, and the final feature list—have been saved to support future inference and deployment.
Total model features: 359
All processed datasets successfully exported.
Processed Data Export Findings
The fully preprocessed datasets were successfully exported in Parquet format for use during the model development phase. The training, validation, and test datasets preserve identical feature spaces consisting of 359 processed model features, ensuring consistency throughout the machine learning pipeline.
The corresponding target variables and transaction identifiers were also saved separately to support model evaluation, fraud investigation, and explainability analyses.
By exporting the processed datasets and preprocessing artifacts, the project substantially improves reproducibility and eliminates the need to repeat computationally intensive preprocessing during subsequent experiments.
12. Data Wrangling, Preprocessing & Feature Engineering Summary and Next Steps
The data-wrangling, preprocessing, and feature-engineering workflow included data quality assessment, deterministic feature engineering, chronological dataset partitioning, removal of high-missing features, training-fitted imputation, removal of low-information features, categorical encoding, robust numerical scaling, correlation-based filtering, and export of the processed datasets and preprocessing artifacts.
Data Wrangling and Preprocessing Pipeline
✓ Data quality assessment
✓ Deterministic feature engineering without unsupported calendar assumptions
✓ Chronological train / validation / test split
✓ Identifier and temporal-boundary verification
✓ Removal of high-missing and low-information features
✓ Training-fitted numerical and categorical imputation
✓ Frequency and one-hot encoding fitted on training only
✓ Robust scaling fitted on training only
✓ Correlation-based feature selection
✓ Export of processed data, preprocessing objects, and feature-contract metadata
The next notebook will compare multiple classifiers under class imbalance, tune the selected boosting model with time-aware cross-validation, choose an operating threshold on validation data, and evaluate the frozen decision once on the later test period.