loan_default_data_load
Read a file C:\Users\user\Downloads\loan_default_100_rows.csv
Thinking
Requesting file clarification
I'm thinking about the need for clarification on a hidden file. The user has supplied a specific path, so I should load and inspect it. I'll use the exact pattern mandated for this. My plan is to parse the CSV. It seems like I might only need to include code, and I should ensure that any explanation is concise, making it easier for the user to understand. Let's get started on this!
</>Load loan data
import pandas as pd
file_path = r"C:\Users\user\Downloads\loan_default_100_rows.csv"
df = pd.read_csv(file_path)
print(f"Shape: {df.shape}")
columns = df.columns.tolist()
print(f"Columns ({len(columns)}):", columns if len(columns) <= 100 else columns[:100] + ["..."])
df.head()Shape: (100, 8) Columns (8): ['Applicant_ID', 'Age', 'Annual_Income', 'Credit_Score', 'Loan_Amount', 'Employment_Status', 'Existing_Debt', 'Loan_Default']
| Applicant_ID | Age | Annual_Income | Credit_Score | Loan_Amount | Employment_Status | Existing_Debt | Loan_Default | |
|---|---|---|---|---|---|---|---|---|
| 0 | A001 | 48 | 42009 | 624 | 43587 | Employed | 22944 | Yes |
| 1 | A002 | 50 | 118559 | 810 | 43029 | Self-Employed | 9371 | No |
| 2 | A003 | 43 | 72337 | 794 | 30474 | Employed | 16337 | No |
| 3 | A004 | 35 | 70485 | 745 | 21386 | Employed | 21857 | No |
| 4 | A005 | 50 | 35445 | 561 | 49209 | Employed | 5548 | No |
The file contains 100 loan applications with 8 fields. It includes applicant demographics and financial indicators—age, annual income, credit score, loan amount, employment status, and existing debt—plus Loan_Default as the outcome label (Yes/No). Applicant_ID is a record identifier.
Train a machine learning classification model to predict Loan_Default (Yes/No).
Use all relevant columns as input features except Applicant_ID.
Automatically preprocess numerical and categorical columns.
Try multiple classification algorithms and compare their performance.
Use appropriate evaluation metrics including accuracy, precision, recall, F1-score, ROC-AUC, and confusion matrix.
Select the best-performing model and provide the training results and model evaluation.
Thinking
Considering modeling approach
I’m thinking we need to take an iterative approach here. Should I start with modeling? It seems like training several models as requested could be helpful in a baseline way, but the policy suggests focusing on iterations. I should inspect the target distribution and check for any missing types before training. Maybe even create a default table or chart. Keeping the code concise is also essential!
First, check data types, missing values, and the class balance for Loan_Default so the evaluation split and preprocessing are appropriate.
</>Summarize columns and target counts
summary = pd.DataFrame({
"dtype": df.drop(columns="Applicant_ID").dtypes.astype(str),
"missing": df.drop(columns="Applicant_ID").isna().sum(),
"unique_values": df.drop(columns="Applicant_ID").nunique()
})
display(summary)
df["Loan_Default"].value_counts(dropna=False)| dtype | missing | unique_values | |
|---|---|---|---|
| Age | int64 | 0 | 35 |
| Annual_Income | int64 | 0 | 100 |
| Credit_Score | int64 | 0 | 91 |
| Loan_Amount | int64 | 0 | 100 |
| Employment_Status | str | 0 | 3 |
| Existing_Debt | int64 | 0 | 99 |
| Loan_Default | str | 0 | 2 |
Loan_Default No 62 Yes 38 Name: count, dtype: int64
There are no missing values, and the target has 38 Yes versus 62 No cases. Applicant_ID should be excluded as an identifier; the remaining five numeric columns and Employment_Status can be automatically preprocessed. The stratified split preserves the default-rate balance in both training and test data.
</>Evaluate model performance
from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, confusion_matrix, classification_report
import pandas as pd
X = df.drop(columns=["Applicant_ID", "Loan_Default"])
y = (df["Loan_Default"] == "Yes").astype(int)
numeric_features = X.select_dtypes(include="number").columns.tolist()
categorical_features = X.select_dtypes(exclude="number").columns.tolist()
preprocessor = ColumnTransformer([
("numeric", Pipeline([
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler())
]), numeric_features),
("categorical", Pipeline([
("imputer", SimpleImputer(strategy="most_frequent")),
("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False))
]), categorical_features)
])
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.20, stratify=y, random_state=42
)
models = {
"Logistic Regression": LogisticRegression(max_iter=1000, random_state=42),
"Random Forest": RandomForestClassifier(n_estimators=300, random_state=42, class_weight="balanced"),
"Gradient Boosting": GradientBoostingClassifier(random_state=42)
}
results = []
fitted_models = {}
for name, model in models.items():
pipeline = Pipeline([("preprocessor", preprocessor), ("model", model)])
pipeline.fit(X_train, y_train)
predictions = pipeline.predict(X_test)
probabilities = pipeline.predict_proba(X_test)[:, 1]
fitted_models[name] = pipeline
results.append({
"Model": name,
"Accuracy": accuracy_score(y_test, predictions),
"Precision": precision_score(y_test, predictions, zero_division=0),
"Recall": recall_score(y_test, predictions, zero_division=0),
"F1-score": f1_score(y_test, predictions, zero_division=0),
"ROC-AUC": roc_auc_score(y_test, probabilities)
})
results_df = pd.DataFrame(results).sort_values(
["ROC-AUC", "F1-score"], ascending=False
).reset_index(drop=True)
best_model_name = results_df.loc[0, "Model"]
best_model = fitted_models[best_model_name]
best_predictions = best_model.predict(X_test)
display(results_df.style.format({
"Accuracy": "{:.3f}", "Precision": "{:.3f}", "Recall": "{:.3f}",
"F1-score": "{:.3f}", "ROC-AUC": "{:.3f}"
}))
print(f"Best model: {best_model_name}")
print("\nConfusion matrix (rows: actual No/Yes; columns: predicted No/Yes):")
print(confusion_matrix(y_test, best_predictions))
print("\nClassification report:")
print(classification_report(y_test, best_predictions, target_names=["No", "Yes"], zero_division=0))| Model | Accuracy | Precision | Recall | F1-score | ROC-AUC | |
|---|---|---|---|---|---|---|
| 0 | Gradient Boosting | 0.950 | 1.000 | 0.875 | 0.933 | 0.927 |
| 1 | Random Forest | 0.950 | 1.000 | 0.875 | 0.933 | 0.896 |
| 2 | Logistic Regression | 0.900 | 1.000 | 0.750 | 0.857 | 0.865 |
Best model: Gradient Boosting
Confusion matrix (rows: actual No/Yes; columns: predicted No/Yes):
[[12 0]
[ 1 7]]
Classification report:
precision recall f1-score support
No 0.92 1.00 0.96 12
Yes 1.00 0.88 0.93 8
accuracy 0.95 20
macro avg 0.96 0.94 0.95 20
weighted avg 0.95 0.95 0.95 20Thinking
Evaluating model performance
I'm noticing that we don't have numerical values for model comparisons because Styler repr is being used. The original request mentioned comparing performance across all metrics, but I see we’re missing the best model’s ROC AUC. It might be helpful to print the results dataframe as well. A plot of the confusion matrix was requested, but it seems the textual matrix meets the requirements. I just need to ensure I have the exact code for this.
Gradient Boosting was selected as best and made only one error on the 20-row holdout set: one actual default was predicted as non-default. This gives 95% accuracy and strong default-class performance (precision 1.00, recall 0.88, F1-score 0.93). The per-model comparison values were not rendered in the notebook output, so display them directly.
</>Display results dataframe
display(results_df.round(3))| Model | Accuracy | Precision | Recall | F1-score | ROC-AUC | |
|---|---|---|---|---|---|---|
| 0 | Gradient Boosting | 0.95 | 1.0 | 0.875 | 0.933 | 0.927 |
| 1 | Random Forest | 0.95 | 1.0 | 0.875 | 0.933 | 0.896 |
| 2 | Logistic Regression | 0.90 | 1.0 | 0.750 | 0.857 | 0.865 |
Applicant_ID was excluded, while all other available columns were used: five numerical features plus Employment_Status. Numerical variables were median-imputed and standardized; the categorical feature was most-frequent-imputed and one-hot encoded. No missing values were observed, so imputation was precautionary.
The 100-row dataset contains 38 defaults (Yes) and 62 non-defaults (No). A stratified 80/20 split yielded a 20-case test set with 8 defaults and 12 non-defaults.
| Model | Accuracy | Precision (Yes) | Recall (Yes) | F1-score (Yes) | ROC-AUC |
|---|---|---|---|---|---|
| Gradient Boosting | 0.950 | 1.000 | 0.875 | 0.933 | 0.927 |
| Random Forest | 0.950 | 1.000 | 0.875 | 0.933 | 0.896 |
| Logistic Regression | 0.900 | 1.000 | 0.750 | 0.857 | 0.865 |
Gradient Boosting was selected because it had the highest ROC-AUC (0.927), while matching the best accuracy and F1-score.
Its confusion matrix, with rows representing actual classes (No, Yes) and columns predicted classes, was:
| Actual / Predicted | No | Yes |
|---|---|---|
| No | 12 | 0 |
| Yes | 1 | 7 |
The model correctly identified all 12 non-default cases and 7 of 8 default cases. Its one error was a default predicted as non-default. The default-class precision of 1.00 means every predicted default was correct, while recall of 0.875 means it detected 87.5% of actual defaults on this test set.