Build a Bank Marketing Classifier

Build a leakage-safe model that ranks bank prospects for term-deposit calls.

Introduction

30 Second Summary

A bank can spend hours calling customers who have little interest in a term deposit. Better targeting helps the team focus its time on the people most likely to say yes.

In this project, you will build a bank marketing classifier in Google Colab that ranks customers by their predicted chance of subscribing. The notebook turns those rankings into a practical call recommendation while protecting the final test results.

What You'll Build

Run your completed notebook from top to bottom to see the strongest prospects rise to the top of a ranked call list.

By the end of this project, you'll have:

  • A visible class-balance audit that reveals how a strong accuracy score can hide zero subscriber recall.
  • A leakage-safe model comparison between logistic regression and a decision tree that leads to a tuned call threshold.
  • A submission-ready notebook that includes your ranked call list. A matching HTML export opens on Windows.
  • Secret Mission: Simulate a campaign that calls only the top 10% of prospects. Measure its precision, recall, and lift over random targeting.

Are there any prerequisites?

You should be comfortable with Python fundamentals and running notebook cells in Google Colab. You need the supplied competition template plus bank-additional.csv before Step 1.

Before We Start

A strong competition entry starts with a clear prediction goal and a clear business reason. You are building a pre-call term-deposit targeting model to improve conversions while conserving caller time.

Set Up and Verify the Colab Notebook

A competition result is hard to trust when the notebook hides its runtime details or depends on files that nobody can trace. Your first job is to make the starting state visible.

This step prepares a reproducible notebook in Google Colab. You will confirm the versions of Python, pandas, NumPy, scikit-learn, and Matplotlib before loading the bank marketing data.

In this step, get ready to:
  • Open the provided competition template under your competition filename.
  • Upload bank-additional.csv to the temporary /content runtime.
  • Verify the runtime versions plus the loaded dataset outputs.
Open and rename the template

The supplied template provides the structure expected in your submission. Keeping its headings intact gives your analysis a clear audit trail from setup through recommendations.

The notebook never asks you to paste Google credentials.

  • Open the Google Colab website in your browser.
  • Sign in with the Google account you use for the competition.
  • Click File in the top menu.
  • Click Upload notebook.
  • Select the provided competition notebook template in the Windows file picker.
  • Click Open.

The template is now open in Colab. Its required headings give each later result a defined place.

  • Click the notebook filename in the top-left corner.
  • Type Challenge1_BankMarketing_your name.ipynb as the new filename.
  • Press Enter to apply the new filename.
  • Confirm every required section heading from the supplied template remains in the notebook.

You should now see the renamed notebook in the browser tab. The original competition structure remains ready for your analysis.

Upload the data and build the setup cell

Colab places files uploaded to the active runtime under /content. The notebook uses a fixed path so every later cell reads the same dataset.

  • Click the folder icon in the left sidebar to show the runtime files.
  • Select the upload control at the top of the files pane.
  • Select bank-additional.csv in the Windows file picker.
  • Click Open to upload the file.
  • Confirm bank-additional.csv appears in the runtime file list.

The upload copies the dataset into the active runtime. Your original Windows file remains unchanged.

  • Click the first code cell beneath the template's setup area.
  • Replace its placeholder contents with the first import group below:
# Cell 1: Imports, versions, and reusable evaluation helper
import sys

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import sklearn
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
    accuracy_score,
    average_precision_score,
    classification_report,
    confusion_matrix,
    f1_score,
    precision_recall_curve,
    precision_score,
    recall_score,
    roc_auc_score,
    roc_curve,
)

What do these imports provide?

  • The core libraries handle tables, arrays, runtime details, and plots.
  • The metric imports measure classification quality from several angles.
  • The transformer plus logistic regression import prepare reusable model components for later steps.
  • Run the first code cell by clicking its play button.

The cell should finish without an error. This confirms that the runtime can load the core analysis libraries.

Import cell stopping early?

  • Compare every import line with the code block above.
  • Check that the closing parenthesis appears after the metric imports.

Ask for help with the failing import: Why is my first Colab import cell failing?

  • Place your cursor after the closing parenthesis in the same code cell.
  • Append the model-selection imports below:
from sklearn.model_selection import (
    GridSearchCV,
    StratifiedKFold,
    cross_val_predict,
    train_test_split,
)
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.tree import DecisionTreeClassifier

Why import these tools now?

  • The model-selection tools support splitting, cross-validation, prediction, and parameter tuning.
  • The preprocessing classes prepare categorical plus numeric features inside each model pipeline.
  • The tree classifier provides the second model family required by the competition workflow.
  • Run the updated code cell with its play button.

The cell should complete without an error again. All model-building classes are now available to the notebook.

Model import failing?

  • Confirm each import remains inside the first code cell.
  • Check the capitalization of every imported class against the reference block.

Ask for help with the model imports: Which scikit-learn import is breaking my setup cell?

  • Place your cursor below the final import in the same code cell.
  • Append the reproducibility settings plus version output below:
RANDOM_STATE = 42
DATA_PATH = "/content/bank-additional.csv"

print("Python:", sys.version.split()[0])
print("pandas:", pd.__version__)
print("NumPy:", np.__version__)
print("scikit-learn:", sklearn.__version__)
print("Matplotlib:", matplotlib.__version__)

What do these settings prove?

The fixed random state gives later data splits and models a reproducible seed. The data path points every loading cell at the uploaded CSV.

  • Python should report 3.13.15.
  • pandas should report 2.2.3.
  • NumPy should report 2.1.3.
  • scikit-learn should report 1.6.0.
  • Matplotlib should report 3.10.0.
  • Run the updated code cell with its play button.

You should see five labeled version lines beneath the cell. These values document the environment behind every later score.

Versions missing or different?

  • Confirm the version print lines sit below their matching imports.
  • Check that the runtime completed the cell without stopping on an earlier line.

Ask for help comparing the runtime: Why does my Colab runtime show different package versions?

  • Place your cursor beneath the version print lines in the same code cell.
  • Append the reusable metric helper below:
def metric_row(model_name, split_name, y_true, probabilities, threshold=0.5):
    predictions = (np.asarray(probabilities) >= threshold).astype(int)
    return {
        "Model": model_name,
        "Split": split_name,
        "Threshold": round(float(threshold), 3),
        "Accuracy": accuracy_score(y_true, predictions),
        "Precision": precision_score(y_true, predictions, zero_division=0),
        "Recall": recall_score(y_true, predictions, zero_division=0),
        "F1": f1_score(y_true, predictions, zero_division=0),
        "ROC-AUC": roc_auc_score(y_true, probabilities),
        "Average Precision": average_precision_score(y_true, probabilities),
    }

What does the metric helper do?

  • The threshold converts subscription probabilities into call or no-call predictions.
  • The helper calculates accuracy, precision, recall, and F1 from those predictions.
  • It also measures ranking quality with ROC-AUC plus average precision.
  • The returned dictionary gives every model the same comparison-table structure.
  • Run the completed setup cell with its play button.

Good work. The version lines should print again while metric_row() becomes available for every later evaluation.

Metric helper causing a syntax error?

  • Check that every dictionary entry ends with a comma.
  • Confirm the closing brace aligns inside the function.

Ask for help with the helper definition: Why will my metric_row function not run?

Load the dataset and verify the notebook

The loading cell uses pandas delimiter detection so the supplied file can be read through one reproducible path. An assertion checks for the target column before any analysis begins.

  • Insert a new code cell directly below the completed setup cell.
  • Paste the data-loading code below into the new cell:
# Cell 2: Load and validate the supplied file
bank = pd.read_csv(DATA_PATH, sep=None, engine="python")
assert "y" in bank.columns, "Expected a target column named 'y'."

print("Shape:", bank.shape)
print("Columns:", bank.columns.tolist())
print(bank.head())

What does the loading cell check?

  • pandas reads the file from the fixed runtime path and detects its delimiter.
  • The assertion confirms that y is available as the subscription target.
  • The printed shape, column list, and preview make the loaded data visible before analysis starts.

Before you run this cell, what outputs would convince you that the correct dataset has loaded?

  • Run the data-loading cell with its play button.

You should see a shape containing 4,119 rows. The column list should contain y.

You should also see the first five customer records. Together, these outputs prove that the notebook can read the supplied dataset.

Dataset cell not loading?

  • Confirm bank-additional.csv appears in the files pane.
  • Check that DATA_PATH points to /content/bank-additional.csv.
  • Confirm you selected the supplied bank marketing CSV during upload.

Ask for help with the loading failure: Why can my Colab notebook not load bank-additional.csv?

✔️ Awesome, I've got everything!

Your first two cells now contain the complete setup plus data-loading code.

ⓧ I'd like to double check the full code

  • Compare your first two notebook cells with the full reference below:
# Cell 1: Imports, versions, and reusable evaluation helper
import sys

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import sklearn
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
    accuracy_score,
    average_precision_score,
    classification_report,
    confusion_matrix,
    f1_score,
    precision_recall_curve,
    precision_score,
    recall_score,
    roc_auc_score,
    roc_curve,
)
from sklearn.model_selection import (
    GridSearchCV,
    StratifiedKFold,
    cross_val_predict,
    train_test_split,
)
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.tree import DecisionTreeClassifier

RANDOM_STATE = 42
DATA_PATH = "/content/bank-additional.csv"

print("Python:", sys.version.split()[0])
print("pandas:", pd.__version__)
print("NumPy:", np.__version__)
print("scikit-learn:", sklearn.__version__)
print("Matplotlib:", matplotlib.__version__)


def metric_row(model_name, split_name, y_true, probabilities, threshold=0.5):
    predictions = (np.asarray(probabilities) >= threshold).astype(int)
    return {
        "Model": model_name,
        "Split": split_name,
        "Threshold": round(float(threshold), 3),
        "Accuracy": accuracy_score(y_true, predictions),
        "Precision": precision_score(y_true, predictions, zero_division=0),
        "Recall": recall_score(y_true, predictions, zero_division=0),
        "F1": f1_score(y_true, predictions, zero_division=0),
        "ROC-AUC": roc_auc_score(y_true, probabilities),
        "Average Precision": average_precision_score(y_true, probabilities),
    }


# Cell 2: Load and validate the supplied file
bank = pd.read_csv(DATA_PATH, sep=None, engine="python")
assert "y" in bank.columns, "Expected a target column named 'y'."

print("Shape:", bank.shape)
print("Columns:", bank.columns.tolist())
print(bank.head())

Before the final check, which version details and dataset clues do you expect the notebook to display?

  • Click Runtime in the Colab menu.
  • Click Run all.

Both cells should finish without errors. The first cell should print five labeled version lines.

The second cell should print the dataset shape plus a column list containing y. A five-row preview should appear beneath those checks.

Your notebook now exposes its runtime and loads the competition data from a fixed path. Next, you will audit the dataset and reveal why a high accuracy score can still fail the campaign.

Expose the Accuracy Trap and Remove Leakage

Your notebook already loads bank-additional.csv inside Google Colab. It also confirms that y exists.

Class imbalance can make an all-no classifier look accurate while it misses every subscriber.

The duration field records the last contact duration. Using it for a pre-call decision creates data leakage that can inflate later results.

In this step, get ready to:
  • Audit the dataset without silently deleting any records.
  • Run an all-no baseline to expose the weakness of accuracy.
  • Create one leakage-safe stratified split for training and testing.
Audit the dataset and target balance

A data audit checks the evidence before modeling begins. This audit keeps every row visible while counting missing values plus duplicates.

  • Select the next empty code cell below the cell that loads bank.
  • Paste this first part of Cell 3:
# Cell 3: Audit the data and expose the majority-class accuracy trap
print("Missing values:", int(bank.isna().sum().sum()))
print("Duplicate rows:", int(bank.duplicated().sum()))
print("\nData types:\n", bank.dtypes)

raw_target = bank["y"].astype(str).str.lower()
target_counts = raw_target.value_counts()
target_rates = raw_target.value_counts(normalize=True).rename("rate")
print("\nTarget counts:\n", target_counts)
print("\nTarget rates:\n", target_rates)

fig, ax = plt.subplots(figsize=(6, 4))
target_counts.reindex(["no", "yes"]).plot.bar(ax=ax, color=["#4C78A8", "#F58518"])
ax.set_title("Term-deposit subscription class balance")
ax.set_xlabel("Subscribed")
ax.set_ylabel("Customers")
ax.tick_params(axis="x", rotation=0)
plt.show()

What does this audit do?

  • The missing-value calculation totals empty entries across the dataset.
  • The duplicate calculation counts repeated rows without removing them.
  • The raw_target values become lowercase strings so capitalization cannot split the same class into separate groups.
  • The bar chart makes the difference between the no class and the yes class visible.
  • Save Challenge1_BankMarketing_<YourName>.ipynb using the notebook's save control.
  • Run Cell 3.

You'll see totals for missing values plus duplicate rows. You'll also see the data types.

Below those details, you'll see the yes/no counts plus rates. The bar chart shows that the no class is much larger.

Missing the target chart?

  • Confirm that Cell 2 ran successfully so bank contains the uploaded dataset.
  • Check that the chart code ends with plt.show().
  • Verify that the target column is spelled y with lowercase text.

Help me fix the missing audit output.

Run the majority-class baseline

A majority-class baseline assigns every customer to the most common outcome. Comparing several metrics reveals whether its accuracy represents useful subscriber discovery.

  • Place your cursor on a blank line immediately after plt.show() in Cell 3.
  • Paste the baseline code below that line:
y = (raw_target == "yes").astype(int)
majority_probabilities = np.zeros(len(y), dtype=float)
majority_result = pd.DataFrame(
    [metric_row("Always predict no", "All data", y, majority_probabilities)]
)
print("\nMajority baseline:\n", majority_result.round(3).to_string(index=False))

What does this baseline do?

  • The y series encodes no as 0.
  • The same series encodes yes as 1.
  • The majority_probabilities array gives every customer a zero subscription probability.
  • The metric_row() helper calculates the full metric set for the baseline.

Before you run Cell 3, do you think a model that predicts no for everyone can still post a strong accuracy score?

  • Save Challenge1_BankMarketing_<YourName>.ipynb using the notebook's save control.
  • Run Cell 3 again.

You'll see one Always predict no row. Its accuracy looks strong because the no class dominates the dataset.

Subscriber recall is 0.000.

Precision is 0.000. F1 is 0.000.

The same row reports ROC-AUC plus Average Precision. You have exposed the designed shortfall behind the headline accuracy.

Baseline row missing?

  • Confirm that the imports cell ran so np plus pd are available.
  • Verify that metric_row() is defined in Cell 1.
  • Check that the baseline code remains inside Cell 3 below the chart code.

Help me restore the majority baseline output.

Create a leakage-safe split

The final model must make its decision before a call begins. Removing duration keeps that post-call information out of the predictors.

The test split acts as the final unseen check. Stratification keeps the subscriber rate similar across both splits.

  • Place your cursor on a blank line immediately after the baseline print statement in Cell 3.
  • Paste the split code below that line:
leakage_columns = [column for column in ["duration"] if column in bank.columns]
X = bank.drop(columns=["y", *leakage_columns])
assert "duration" not in X.columns
assert y.nunique() == 2, "Expected both yes and no target classes."

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.25,
    random_state=RANDOM_STATE,
    stratify=y,
)

print("\nRemoved leakage columns:", leakage_columns)
print("Train shape:", X_train.shape, "Test shape:", X_test.shape)
print("Train positive rate:", round(y_train.mean(), 4))
print("Test positive rate:", round(y_test.mean(), 4))

How does this protect the evaluation?

  • The leakage_columns list captures duration when that column is present.
  • The X matrix excludes the target plus the leakage column.
  • The test_size=0.25 setting reserves one quarter of the rows for the final test.
  • The stratify=y setting preserves similar class proportions across the split.
  • The RANDOM_STATE value makes the split reproducible.

Before you rerun Cell 3, do you expect its two positive rates to stay close?

  • Save Challenge1_BankMarketing_<YourName>.ipynb using the notebook's save control.
  • Run Cell 3 from the top.

You'll see duration named as the removed leakage column.

The test shape contains roughly one quarter of the rows. The training shape contains the remaining rows.

The training positive rate stays close to the test positive rate. The assertions also confirm that duration is absent plus y is binary.

Split cell not completing?

  • Confirm that the baseline section created y before the split code runs.
  • Check that train_test_split was imported in Cell 1.
  • Verify that RANDOM_STATE still equals 42.

Help me debug the leakage removal or stratified split.

✔️ Awesome, I've got everything!

Your notebook now contains Cells 1 to 3. Cell 3 runs from the audit through the train-test split.

  • Save Challenge1_BankMarketing_<YourName>.ipynb before continuing.

ⓧ I'd like to double check the full code

Compare your first three code cells with this cumulative reference. Keep the notebook's required section headings around these cells.

# Cell 1: Imports, versions, and reusable evaluation helper
import sys

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import sklearn
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
    accuracy_score,
    average_precision_score,
    classification_report,
    confusion_matrix,
    f1_score,
    precision_recall_curve,
    precision_score,
    recall_score,
    roc_auc_score,
    roc_curve,
)
from sklearn.model_selection import (
    GridSearchCV,
    StratifiedKFold,
    cross_val_predict,
    train_test_split,
)
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.tree import DecisionTreeClassifier

RANDOM_STATE = 42
DATA_PATH = "/content/bank-additional.csv"

print("Python:", sys.version.split()[0])
print("pandas:", pd.__version__)
print("NumPy:", np.__version__)
print("scikit-learn:", sklearn.__version__)
print("Matplotlib:", matplotlib.__version__)


def metric_row(model_name, split_name, y_true, probabilities, threshold=0.5):
    predictions = (np.asarray(probabilities) >= threshold).astype(int)
    return {
        "Model": model_name,
        "Split": split_name,
        "Threshold": round(float(threshold), 3),
        "Accuracy": accuracy_score(y_true, predictions),
        "Precision": precision_score(y_true, predictions, zero_division=0),
        "Recall": recall_score(y_true, predictions, zero_division=0),
        "F1": f1_score(y_true, predictions, zero_division=0),
        "ROC-AUC": roc_auc_score(y_true, probabilities),
        "Average Precision": average_precision_score(y_true, probabilities),
    }


# Cell 2: Load and validate the supplied file
bank = pd.read_csv(DATA_PATH, sep=None, engine="python")
assert "y" in bank.columns, "Expected a target column named 'y'."

print("Shape:", bank.shape)
print("Columns:", bank.columns.tolist())
print(bank.head())


# Cell 3: Audit the data and expose the majority-class accuracy trap
print("Missing values:", int(bank.isna().sum().sum()))
print("Duplicate rows:", int(bank.duplicated().sum()))
print("\nData types:\n", bank.dtypes)

raw_target = bank["y"].astype(str).str.lower()
target_counts = raw_target.value_counts()
target_rates = raw_target.value_counts(normalize=True).rename("rate")
print("\nTarget counts:\n", target_counts)
print("\nTarget rates:\n", target_rates)

fig, ax = plt.subplots(figsize=(6, 4))
target_counts.reindex(["no", "yes"]).plot.bar(ax=ax, color=["#4C78A8", "#F58518"])
ax.set_title("Term-deposit subscription class balance")
ax.set_xlabel("Subscribed")
ax.set_ylabel("Customers")
ax.tick_params(axis="x", rotation=0)
plt.show()

y = (raw_target == "yes").astype(int)
majority_probabilities = np.zeros(len(y), dtype=float)
majority_result = pd.DataFrame(
    [metric_row("Always predict no", "All data", y, majority_probabilities)]
)
print("\nMajority baseline:\n", majority_result.round(3).to_string(index=False))

leakage_columns = [column for column in ["duration"] if column in bank.columns]
X = bank.drop(columns=["y", *leakage_columns])
assert "duration" not in X.columns
assert y.nunique() == 2, "Expected both yes and no target classes."

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.25,
    random_state=RANDOM_STATE,
    stratify=y,
)

print("\nRemoved leakage columns:", leakage_columns)
print("Train shape:", X_train.shape, "Test shape:", X_test.shape)
print("Train positive rate:", round(y_train.mean(), 4))
print("Test positive rate:", round(y_test.mean(), 4))

You found the accuracy trap without letting duration contaminate the predictors. Next, you'll build two models that learn from the pre-call features while rewarding subscriber discovery.

Build Two Leakage-Safe Models

Your audit exposed the weakness in the all-no baseline. Its accuracy hides the fact that it finds zero subscribers.

This step replaces that baseline with logistic regression and a decision tree. Class weighting helps both models learn from the smaller subscriber class.

In this step, get ready to:
  • Detect categorical and numeric columns from the training data.
  • Configure separate preprocessing pipelines for logistic regression and a decision tree.
  • Fit both models and compare their training metrics without using the test split.
Detect columns and build preprocessing

A scikit-learn ColumnTransformer applies different preparation rules to different column types. Categorical values receive one-hot encoding while numeric values receive standardization.

The reusable Pipeline function keeps these transformations beside each model. Every fit can then learn its preprocessing from training data only.

  • Add a new code cell beneath Cell 3.
  • Build the column lists and reusable preprocessing function by pasting this code into the new cell:
# Cell 4: Build leakage-safe preprocessing and two first models
categorical_columns = X_train.select_dtypes(include=["object", "category"]).columns.tolist()
numeric_columns = X_train.select_dtypes(exclude=["object", "category"]).columns.tolist()

print("Categorical columns:", categorical_columns)
print("Numeric columns:", numeric_columns)


def build_pipeline(model):
    preprocessor = ColumnTransformer(
        transformers=[
            (
                "categorical",
                OneHotEncoder(handle_unknown="ignore"),
                categorical_columns,
            ),
            ("numeric", StandardScaler(), numeric_columns),
        ]
    )
    return Pipeline(
        steps=[
            ("preprocessor", preprocessor),
            ("model", model),
        ]
    )

What Does This Code Do?

  • The two column lists inspect X_train so the held-out test data does not influence preprocessing decisions.
  • The categorical transformer converts categories into numeric indicator columns. Unknown categories remain safe when later data passes through the pipeline.
  • The numeric transformer scales numeric columns using values learned during fitting.
  • The build_pipeline(model) function returns one preprocessing and model workflow.
  • Run Cell 4 using the play button on its left.
  • Confirm the output shows separate categorical and numeric column lists.

Column Lists Missing?

Confirm Cells 1 through 3 have run before Cell 4. The new code depends on X_train from the stratified split.

Check that the entire build_pipeline(model) function remains indented exactly as shown.

Help me troubleshoot my preprocessing cell.

Configure separate model pipelines

The first pipeline uses LogisticRegression as a linear probability benchmark. Its balanced class weights give the subscriber class more influence during fitting.

  • Place your cursor below the closing parenthesis of build_pipeline(model).
  • Configure the logistic regression pipeline by pasting this code:
logistic_model = build_pipeline(
    LogisticRegression(
        class_weight="balanced",
        max_iter=1000,
        random_state=RANDOM_STATE,
    )
)

How Is Logistic Regression Configured?

  • The build_pipeline wrapper places preprocessing before logistic regression.
  • The balanced class weights reduce the majority class's dominance during learning.
  • The iteration limit gives the optimizer room to converge after preprocessing.
  • The shared RANDOM_STATE keeps the notebook settings reproducible.
  • Run Cell 4 again using the play button.
  • Confirm the cell finishes without a traceback and still prints both column lists.

Logistic Pipeline Failing?

Check that logistic_model sits outside the build_pipeline(model) function. Its first character should align with the function definition.

Help me fix my logistic regression pipeline.

The second pipeline uses DecisionTreeClassifier to learn branching rules from the prepared features. Its depth and split settings provide a controlled starting point for the later tuning step.

  • Configure the base decision tree directly below logistic_model by pasting this code:
base_tree_model = build_pipeline(
    DecisionTreeClassifier(
        class_weight="balanced",
        max_depth=5,
        min_samples_split=20,
        random_state=RANDOM_STATE,
    )
)

How Is the Base Tree Configured?

  • The tree receives its own preprocessor through build_pipeline.
  • The balanced class weights make subscriber mistakes matter more during fitting.
  • The depth limit controls how many branching levels the base tree can create.
  • The minimum split size prevents a node from splitting when it has too few training examples.
  • Run Cell 4 again using the play button.
  • Confirm the cell finishes without a traceback and prints both column lists.

Decision Tree Pipeline Failing?

Confirm base_tree_model appears below the complete logistic_model definition. Check each closing parenthesis against the snippet.

Help me fix my decision tree pipeline.

Fit and compare the models

Fitting each pipeline on X_train teaches its preprocessor and estimator from the same training split. The metrics then measure how well each model recovers subscribers from that training data.

The comparison includes accuracy alongside precision, recall, F1, ROC-AUC, and average precision. These minority-sensitive metrics reveal whether the models improve on the zero-recall baseline.

  • Complete Cell 4 by pasting this code beneath base_tree_model.
logistic_model.fit(X_train, y_train)
base_tree_model.fit(X_train, y_train)

training_results = pd.DataFrame(
    [
        metric_row(
            "Logistic Regression",
            "Train",
            y_train,
            logistic_model.predict_proba(X_train)[:, 1],
        ),
        metric_row(
            "Base Decision Tree",
            "Train",
            y_train,
            base_tree_model.predict_proba(X_train)[:, 1],
        ),
    ]
)
print(training_results.round(3).to_string(index=False))

What Does This Comparison Measure?

  • Each fit call learns preprocessing rules and model patterns from X_train with y_train as the target.
  • Each predict_proba call produces a subscription probability for every training row.
  • The metric_row helper converts those probabilities into the same metric set used for the majority baseline.
  • The training_results table keeps both model families comparable without opening the held-out test set.

Before you run the completed cell, do you expect both learned models to recover some subscribers that the all-no baseline missed?

  • Run the completed Cell 4 using the play button.
  • Confirm the output contains rows for Logistic Regression and Base Decision Tree.

You should see nonzero subscriber recall for both model rows. The table should also contain Accuracy, Precision, F1, ROC-AUC, and Average Precision columns.

That is the key recovery from the accuracy trap. Both learned models can now identify subscribers from the training data while the held-out test split remains untouched.

Training Table Missing or Failing?

  • Confirm the preprocessing code and both model definitions are in the same completed Cell 4.
  • Check that Cells 1 through 3 have run so X_train and y_train exist.
  • Compare the model names and probability slices with the complete code below.

Help me debug my training comparison.

✔️ Awesome, I've got everything!

Your completed Cell 4 fits both leakage-safe pipelines and prints their training metrics. Keep the test split closed for the next step.

ⓧ I'd like to double check the full code

# Cell 1: Imports, versions, and reusable evaluation helper
import sys

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import sklearn
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
    accuracy_score,
    average_precision_score,
    classification_report,
    confusion_matrix,
    f1_score,
    precision_recall_curve,
    precision_score,
    recall_score,
    roc_auc_score,
    roc_curve,
)
from sklearn.model_selection import (
    GridSearchCV,
    StratifiedKFold,
    cross_val_predict,
    train_test_split,
)
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.tree import DecisionTreeClassifier

RANDOM_STATE = 42
DATA_PATH = "/content/bank-additional.csv"

print("Python:", sys.version.split()[0])
print("pandas:", pd.__version__)
print("NumPy:", np.__version__)
print("scikit-learn:", sklearn.__version__)
print("Matplotlib:", matplotlib.__version__)


def metric_row(model_name, split_name, y_true, probabilities, threshold=0.5):
    predictions = (np.asarray(probabilities) >= threshold).astype(int)
    return {
        "Model": model_name,
        "Split": split_name,
        "Threshold": round(float(threshold), 3),
        "Accuracy": accuracy_score(y_true, predictions),
        "Precision": precision_score(y_true, predictions, zero_division=0),
        "Recall": recall_score(y_true, predictions, zero_division=0),
        "F1": f1_score(y_true, predictions, zero_division=0),
        "ROC-AUC": roc_auc_score(y_true, probabilities),
        "Average Precision": average_precision_score(y_true, probabilities),
    }


# Cell 2: Load and validate the supplied file
bank = pd.read_csv(DATA_PATH, sep=None, engine="python")
assert "y" in bank.columns, "Expected a target column named 'y'."

print("Shape:", bank.shape)
print("Columns:", bank.columns.tolist())
print(bank.head())


# Cell 3: Audit the data and expose the majority-class accuracy trap
print("Missing values:", int(bank.isna().sum().sum()))
print("Duplicate rows:", int(bank.duplicated().sum()))
print("\nData types:\n", bank.dtypes)

raw_target = bank["y"].astype(str).str.lower()
target_counts = raw_target.value_counts()
target_rates = raw_target.value_counts(normalize=True).rename("rate")
print("\nTarget counts:\n", target_counts)
print("\nTarget rates:\n", target_rates)

fig, ax = plt.subplots(figsize=(6, 4))
target_counts.reindex(["no", "yes"]).plot.bar(ax=ax, color=["#4C78A8", "#F58518"])
ax.set_title("Term-deposit subscription class balance")
ax.set_xlabel("Subscribed")
ax.set_ylabel("Customers")
ax.tick_params(axis="x", rotation=0)
plt.show()

y = (raw_target == "yes").astype(int)
majority_probabilities = np.zeros(len(y), dtype=float)
majority_result = pd.DataFrame(
    [metric_row("Always predict no", "All data", y, majority_probabilities)]
)
print("\nMajority baseline:\n", majority_result.round(3).to_string(index=False))

leakage_columns = [column for column in ["duration"] if column in bank.columns]
X = bank.drop(columns=["y", *leakage_columns])
assert "duration" not in X.columns
assert y.nunique() == 2, "Expected both yes and no target classes."

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.25,
    random_state=RANDOM_STATE,
    stratify=y,
)

print("\nRemoved leakage columns:", leakage_columns)
print("Train shape:", X_train.shape, "Test shape:", X_test.shape)
print("Train positive rate:", round(y_train.mean(), 4))
print("Test positive rate:", round(y_test.mean(), 4))


# Cell 4: Build leakage-safe preprocessing and two first models
categorical_columns = X_train.select_dtypes(include=["object", "category"]).columns.tolist()
numeric_columns = X_train.select_dtypes(exclude=["object", "category"]).columns.tolist()

print("Categorical columns:", categorical_columns)
print("Numeric columns:", numeric_columns)


def build_pipeline(model):
    preprocessor = ColumnTransformer(
        transformers=[
            (
                "categorical",
                OneHotEncoder(handle_unknown="ignore"),
                categorical_columns,
            ),
            ("numeric", StandardScaler(), numeric_columns),
        ]
    )
    return Pipeline(
        steps=[
            ("preprocessor", preprocessor),
            ("model", model),
        ]
    )


logistic_model = build_pipeline(
    LogisticRegression(
        class_weight="balanced",
        max_iter=1000,
        random_state=RANDOM_STATE,
    )
)

base_tree_model = build_pipeline(
    DecisionTreeClassifier(
        class_weight="balanced",
        max_depth=5,
        min_samples_split=20,
        random_state=RANDOM_STATE,
    )
)

logistic_model.fit(X_train, y_train)
base_tree_model.fit(X_train, y_train)

training_results = pd.DataFrame(
    [
        metric_row(
            "Logistic Regression",
            "Train",
            y_train,
            logistic_model.predict_proba(X_train)[:, 1],
        ),
        metric_row(
            "Base Decision Tree",
            "Train",
            y_train,
            base_tree_model.predict_proba(X_train)[:, 1],
        ),
    ]
)
print(training_results.round(3).to_string(index=False))

Your two model families now learn through the same leakage-safe preprocessing pattern. Next, you will tune the decision tree and freeze a calling threshold using training data only.

Tune the Tree and Its Calling Threshold

Your first two pipelines now learn from the training split without exposing the held-out test data. Their training metrics give you a starting point for improving the tree.

A flexible decision tree can overfit its training examples. This step uses cross-validation to control model complexity before freezing an F1-optimized calling threshold.

In this step, get ready to:
  • Tune decision-tree complexity with stratified five-fold cross-validation.
  • Generate out-of-fold subscription probabilities from the training split.
  • Freeze the calling threshold with the strongest out-of-fold F1 score.
Tune tree complexity with cross-validation

Cross-validation tests each parameter combination across several training folds. Stratification keeps the subscriber proportion similar in every fold.

  • Add a new code cell beneath Cell 4 in Challenge1_BankMarketing_<YourName>.ipynb.
  • Paste the first part of Cell 5 into the new cell:
# Cell 5: Tune decision-tree complexity using only the training split
cross_validation = StratifiedKFold(
    n_splits=5,
    shuffle=True,
    random_state=RANDOM_STATE,
)

parameter_grid = {
    "model__max_depth": [3, 5, 8],
    "model__min_samples_split": [10, 30],
    "model__min_samples_leaf": [5, 15],
    "model__class_weight": [None, "balanced"],
}

scoring = {
    "f1": "f1",
    "roc_auc": "roc_auc",
    "average_precision": "average_precision",
}

How is the search configured?

  • The StratifiedKFold object creates five training folds with similar class proportions.
  • The parameter_grid dictionary controls tree depth plus the minimum samples required for branches or leaves.
  • The scoring dictionary keeps minority-sensitive metrics visible during the search.
  • Complete Cell 5 by pasting this code directly below the scoring dictionary:
tree_search = GridSearchCV(
    estimator=build_pipeline(
        DecisionTreeClassifier(random_state=RANDOM_STATE)
    ),
    param_grid=parameter_grid,
    scoring=scoring,
    refit="f1",
    cv=cross_validation,
    n_jobs=-1,
    return_train_score=True,
)

tree_search.fit(X_train, y_train)
best_tree_model = tree_search.best_estimator_

print("Best parameters:", tree_search.best_params_)
print("Best cross-validated F1:", round(tree_search.best_score_, 4))

What does the search do?

  • The GridSearchCV search fits every parameter combination across the five folds.
  • The refit="f1" setting rebuilds the strongest F1 configuration using the full training split.
  • The best_tree_model variable stores that fitted pipeline for threshold tuning.
  • The held-out X_test data never enters this search.

Before you run Cell 5, predict whether the best tree will use every available split or stop earlier.

  • Run the completed Cell 5 from its left edge.

You should see lines beginning with Best parameters: plus Best cross-validated F1:. That is the complexity search complete using training data only.

Tree search not completing?

  • Confirm Cells 1 through 4 have run in the current Colab runtime.
  • Check that the second code chunk sits inside the same Cell 5 as the cross-validation configuration.
  • Compare the parameter names with the model step created by build_pipeline(model).

Help me fix my decision-tree grid search.

Find the best calling threshold

The tuned tree outputs subscription probabilities. Out-of-fold predictions give every training row a probability from a model that did not train on that row.

  • Add a new code cell beneath Cell 5.
  • Paste the threshold-selection portion of Cell 6 into the new cell:
# Cell 6: Tune the business threshold using out-of-fold training probabilities
oof_probabilities = cross_val_predict(
    best_tree_model,
    X_train,
    y_train,
    cv=cross_validation,
    method="predict_proba",
    n_jobs=-1,
)[:, 1]

threshold_rows = []
for threshold in np.arange(0.10, 0.91, 0.01):
    predictions = (oof_probabilities >= threshold).astype(int)
    threshold_rows.append(
        {
            "Threshold": threshold,
            "Precision": precision_score(y_train, predictions, zero_division=0),
            "Recall": recall_score(y_train, predictions, zero_division=0),
            "F1": f1_score(y_train, predictions, zero_division=0),
        }
    )

threshold_table = pd.DataFrame(threshold_rows)
best_threshold_index = threshold_table["F1"].idxmax()
best_threshold = float(threshold_table.loc[best_threshold_index, "Threshold"])
best_threshold_row = threshold_table.loc[best_threshold_index]

print("Frozen threshold:", round(best_threshold, 3))
print(best_threshold_row.round(3).to_string())

How is the threshold chosen?

  • The cross_val_predict call produces out-of-fold probabilities from the training split.
  • The loop tests candidate thresholds from 0.10 through 0.90.
  • Each candidate becomes a row containing precision plus recall plus F1.
  • The best_threshold variable freezes the threshold with the highest out-of-fold F1.

Before you run Cell 6, predict where the frozen threshold will fall between zero and one.

  • Run the current Cell 6 from its left edge.

You should see a frozen threshold between 0 plus 1. The printed row should also show its precision plus recall plus F1 trade-off.

Threshold cell not producing results?

  • Confirm Cell 5 printed its best parameters before running Cell 6.
  • Check that best_tree_model exists in the current runtime.
  • Compare the threshold range with np.arange(0.10, 0.91, 0.01).

Help me debug my out-of-fold threshold search.

Plot the frozen threshold trade-off

A single F1 maximum does not show what the calling rule trades away. Plotting all three metrics makes the chosen operating point auditable.

  • Append this plotting code to the bottom of Cell 6:
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(threshold_table["Threshold"], threshold_table["Precision"], label="Precision")
ax.plot(threshold_table["Threshold"], threshold_table["Recall"], label="Recall")
ax.plot(threshold_table["Threshold"], threshold_table["F1"], label="F1")
ax.axvline(best_threshold, color="black", linestyle="--", label="Chosen threshold")
ax.set_title("Out-of-fold metric trade-off by call threshold")
ax.set_xlabel("Threshold")
ax.set_ylabel("Score")
ax.legend()
plt.show()

What does the plot reveal?

  • The precision curve shows how concentrated predicted subscribers become as the threshold changes.
  • The recall curve shows how much of the subscriber class remains above each threshold.
  • The F1 curve balances those two measurements at every candidate threshold.
  • The dashed vertical line marks the frozen operating threshold.

Before you rerun Cell 6, predict where the dashed chosen-threshold line will sit relative to the strongest F1 point.

  • Rerun the completed Cell 6 from its left edge.

You should see the precision plus recall plus F1 curves across the threshold range. A dashed vertical line should pass through the selected operating point.

Strong work. Your tree complexity plus calling rule are now frozen using training evidence while X_test plus y_test remain untouched.

Threshold plot missing or incomplete?

  • Confirm the plotting code sits below the threshold-selection code in Cell 6.
  • Check that Cell 6 creates threshold_table before the first plotting line.
  • Rerun Cell 6 after reconnecting the Colab runtime if earlier variables were cleared.

Help me restore my threshold trade-off plot.

✔️ Awesome, I've got everything!

Great. Double-check that Cells 5 plus 6 are saved in your Colab notebook before continuing.

ⓧ I'd like to double check the full code

# Cell 1: Imports, versions, and reusable evaluation helper
import sys

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import sklearn
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
    accuracy_score,
    average_precision_score,
    classification_report,
    confusion_matrix,
    f1_score,
    precision_recall_curve,
    precision_score,
    recall_score,
    roc_auc_score,
    roc_curve,
)
from sklearn.model_selection import (
    GridSearchCV,
    StratifiedKFold,
    cross_val_predict,
    train_test_split,
)
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.tree import DecisionTreeClassifier

RANDOM_STATE = 42
DATA_PATH = "/content/bank-additional.csv"

print("Python:", sys.version.split()[0])
print("pandas:", pd.__version__)
print("NumPy:", np.__version__)
print("scikit-learn:", sklearn.__version__)
print("Matplotlib:", matplotlib.__version__)


def metric_row(model_name, split_name, y_true, probabilities, threshold=0.5):
    predictions = (np.asarray(probabilities) >= threshold).astype(int)
    return {
        "Model": model_name,
        "Split": split_name,
        "Threshold": round(float(threshold), 3),
        "Accuracy": accuracy_score(y_true, predictions),
        "Precision": precision_score(y_true, predictions, zero_division=0),
        "Recall": recall_score(y_true, predictions, zero_division=0),
        "F1": f1_score(y_true, predictions, zero_division=0),
        "ROC-AUC": roc_auc_score(y_true, probabilities),
        "Average Precision": average_precision_score(y_true, probabilities),
    }


# Cell 2: Load and validate the supplied file
bank = pd.read_csv(DATA_PATH, sep=None, engine="python")
assert "y" in bank.columns, "Expected a target column named 'y'."

print("Shape:", bank.shape)
print("Columns:", bank.columns.tolist())
print(bank.head())


# Cell 3: Audit the data and expose the majority-class accuracy trap
print("Missing values:", int(bank.isna().sum().sum()))
print("Duplicate rows:", int(bank.duplicated().sum()))
print("\nData types:\n", bank.dtypes)

raw_target = bank["y"].astype(str).str.lower()
target_counts = raw_target.value_counts()
target_rates = raw_target.value_counts(normalize=True).rename("rate")
print("\nTarget counts:\n", target_counts)
print("\nTarget rates:\n", target_rates)

fig, ax = plt.subplots(figsize=(6, 4))
target_counts.reindex(["no", "yes"]).plot.bar(ax=ax, color=["#4C78A8", "#F58518"])
ax.set_title("Term-deposit subscription class balance")
ax.set_xlabel("Subscribed")
ax.set_ylabel("Customers")
ax.tick_params(axis="x", rotation=0)
plt.show()

y = (raw_target == "yes").astype(int)
majority_probabilities = np.zeros(len(y), dtype=float)
majority_result = pd.DataFrame(
    [metric_row("Always predict no", "All data", y, majority_probabilities)]
)
print("\nMajority baseline:\n", majority_result.round(3).to_string(index=False))

leakage_columns = [column for column in ["duration"] if column in bank.columns]
X = bank.drop(columns=["y", *leakage_columns])
assert "duration" not in X.columns
assert y.nunique() == 2, "Expected both yes and no target classes."

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.25,
    random_state=RANDOM_STATE,
    stratify=y,
)

print("\nRemoved leakage columns:", leakage_columns)
print("Train shape:", X_train.shape, "Test shape:", X_test.shape)
print("Train positive rate:", round(y_train.mean(), 4))
print("Test positive rate:", round(y_test.mean(), 4))


# Cell 4: Build leakage-safe preprocessing and two first models
categorical_columns = X_train.select_dtypes(include=["object", "category"]).columns.tolist()
numeric_columns = X_train.select_dtypes(exclude=["object", "category"]).columns.tolist()

print("Categorical columns:", categorical_columns)
print("Numeric columns:", numeric_columns)


def build_pipeline(model):
    preprocessor = ColumnTransformer(
        transformers=[
            (
                "categorical",
                OneHotEncoder(handle_unknown="ignore"),
                categorical_columns,
            ),
            ("numeric", StandardScaler(), numeric_columns),
        ]
    )
    return Pipeline(
        steps=[
            ("preprocessor", preprocessor),
            ("model", model),
        ]
    )


logistic_model = build_pipeline(
    LogisticRegression(
        class_weight="balanced",
        max_iter=1000,
        random_state=RANDOM_STATE,
    )
)

base_tree_model = build_pipeline(
    DecisionTreeClassifier(
        class_weight="balanced",
        max_depth=5,
        min_samples_split=20,
        random_state=RANDOM_STATE,
    )
)

logistic_model.fit(X_train, y_train)
base_tree_model.fit(X_train, y_train)

training_results = pd.DataFrame(
    [
        metric_row(
            "Logistic Regression",
            "Train",
            y_train,
            logistic_model.predict_proba(X_train)[:, 1],
        ),
        metric_row(
            "Base Decision Tree",
            "Train",
            y_train,
            base_tree_model.predict_proba(X_train)[:, 1],
        ),
    ]
)
print(training_results.round(3).to_string(index=False))


# Cell 5: Tune decision-tree complexity using only the training split
cross_validation = StratifiedKFold(
    n_splits=5,
    shuffle=True,
    random_state=RANDOM_STATE,
)

parameter_grid = {
    "model__max_depth": [3, 5, 8],
    "model__min_samples_split": [10, 30],
    "model__min_samples_leaf": [5, 15],
    "model__class_weight": [None, "balanced"],
}

scoring = {
    "f1": "f1",
    "roc_auc": "roc_auc",
    "average_precision": "average_precision",
}

tree_search = GridSearchCV(
    estimator=build_pipeline(
        DecisionTreeClassifier(random_state=RANDOM_STATE)
    ),
    param_grid=parameter_grid,
    scoring=scoring,
    refit="f1",
    cv=cross_validation,
    n_jobs=-1,
    return_train_score=True,
)

tree_search.fit(X_train, y_train)
best_tree_model = tree_search.best_estimator_

print("Best parameters:", tree_search.best_params_)
print("Best cross-validated F1:", round(tree_search.best_score_, 4))


# Cell 6: Tune the business threshold using out-of-fold training probabilities
oof_probabilities = cross_val_predict(
    best_tree_model,
    X_train,
    y_train,
    cv=cross_validation,
    method="predict_proba",
    n_jobs=-1,
)[:, 1]

threshold_rows = []
for threshold in np.arange(0.10, 0.91, 0.01):
    predictions = (oof_probabilities >= threshold).astype(int)
    threshold_rows.append(
        {
            "Threshold": threshold,
            "Precision": precision_score(y_train, predictions, zero_division=0),
            "Recall": recall_score(y_train, predictions, zero_division=0),
            "F1": f1_score(y_train, predictions, zero_division=0),
        }
    )

threshold_table = pd.DataFrame(threshold_rows)
best_threshold_index = threshold_table["F1"].idxmax()
best_threshold = float(threshold_table.loc[best_threshold_index, "Threshold"])
best_threshold_row = threshold_table.loc[best_threshold_index]

print("Frozen threshold:", round(best_threshold, 3))
print(best_threshold_row.round(3).to_string())

fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(threshold_table["Threshold"], threshold_table["Precision"], label="Precision")
ax.plot(threshold_table["Threshold"], threshold_table["Recall"], label="Recall")
ax.plot(threshold_table["Threshold"], threshold_table["F1"], label="F1")
ax.axvline(best_threshold, color="black", linestyle="--", label="Chosen threshold")
ax.set_title("Out-of-fold metric trade-off by call threshold")
ax.set_xlabel("Threshold")
ax.set_ylabel("Score")
ax.legend()
plt.show()

Your tuned tree plus calling threshold are ready for a fair final test. Next, you will open the held-out split once to measure performance plus build the campaign evidence.

Evaluate, Explain, and Export the Results

Your tree settings and calling threshold are frozen using training data. The untouched test set can now provide an honest final measurement of how the models handle new prospects.

This step turns those measurements into campaign evidence. You will compare the models once, inspect their mistakes, rank prospects, write recommendations, and export the completed submission.

In this step, get ready to:
  • Evaluate all three model settings on the training split and untouched test split.
  • Create diagnostic plots, feature evidence, segment evidence, and a ranked call list.
  • Write evidence-backed recommendations and export matching notebook and HTML files.
Evaluate the untouched test set

A final test evaluation estimates performance on data that played no role in model selection. Keeping this reveal until now protects the credibility of your comparison.

Before you run the evaluation, which model do you expect to achieve the strongest test F1 score?

  • Add Cell 7 below the threshold-tuning cell in your open notebook.
  • Compare the three frozen model settings by pasting this first chunk into Cell 7:
# Cell 7: Open the untouched test set once and compare models
model_settings = {
    "Logistic Regression": (logistic_model, 0.5),
    "Base Decision Tree": (base_tree_model, 0.5),
    "Tuned Decision Tree": (best_tree_model, best_threshold),
}

final_rows = []
for model_name, (model, threshold) in model_settings.items():
    for split_name, features, labels in [
        ("Train", X_train, y_train),
        ("Test", X_test, y_test),
    ]:
        probabilities = model.predict_proba(features)[:, 1]
        final_rows.append(
            metric_row(model_name, split_name, labels, probabilities, threshold)
        )

final_results = pd.DataFrame(final_rows)
print(final_results.round(3).to_string(index=False))

What does this code do?

  • The model_settings dictionary keeps each model beside its frozen decision threshold.
  • The nested loops calculate one training row and one test row for every model setting.
  • The shared metric_row() helper keeps every comparison on the same six metrics.
  • Run Cell 7.
  • Confirm that the table contains six rows covering the Train and Test splits for all three model settings.
  • Compare each model's test Precision, Recall, F1, ROC-AUC, and Average Precision values.

The untouched results are now visible. You have one consistent comparison instead of a score selected through repeated test-set checking.

Missing rows or test metrics?

  • Confirm that Cells 1 through 6 completed before Cell 7 ran.
  • Check that model_settings contains all three model names exactly as shown.
  • Rerun Cell 7 after correcting any indentation copied into the nested loops.

Ask for help with the comparison cell: Why is my final model comparison missing rows or metrics?

  • Append the tuned tree predictions and classification report to the bottom of Cell 7:
# Cell 7: Open the untouched test set once and compare models
test_probabilities = best_tree_model.predict_proba(X_test)[:, 1]
test_predictions = (test_probabilities >= best_threshold).astype(int)

print("\nTuned decision tree test classification report:\n")
print(
    classification_report(
        y_test,
        test_predictions,
        target_names=["No subscription", "Subscription"],
        zero_division=0,
    )
)

How is the final decision created?

  • The tuned tree converts every held-out prospect into a subscription probability.
  • The frozen best_threshold converts those probabilities into call recommendations.
  • The classification report separates performance for non-subscribers and subscribers.
  • Run Cell 7 again.
  • Confirm that the classification report contains rows for No subscription and Subscription.
  • Save the notebook using Colab's save control.

Classification report not displayed?

  • Check that the appended code remains inside Cell 7.
  • Confirm that test_probabilities uses X_test.
  • Confirm that test_predictions uses the frozen best_threshold.

Get help with the report: Why is my tuned tree classification report failing?

Build diagnostic and targeting evidence

A metric table gives the overall score. Diagnostic plots reveal which mistakes produced that score, while feature and segment tables connect the model to practical targeting decisions.

  • Add Cell 8 below Cell 7.
  • Build the tuned tree confusion matrix by pasting this first chunk into Cell 8:
# Cell 8: Plot final diagnostics
matrix = confusion_matrix(y_test, test_predictions)
fig, ax = plt.subplots(figsize=(5, 4))
image = ax.imshow(matrix, cmap="Blues")
for row in range(matrix.shape[0]):
    for column in range(matrix.shape[1]):
        ax.text(column, row, matrix[row, column], ha="center", va="center")
ax.set_xticks([0, 1], labels=["Predict no", "Predict yes"])
ax.set_yticks([0, 1], labels=["Actual no", "Actual yes"])
ax.set_title("Tuned decision tree confusion matrix")
fig.colorbar(image, ax=ax)
plt.show()

What does the matrix reveal?

  • The diagonal cells count correct decisions.
  • The off-diagonal cells count missed subscribers and unnecessary calls.
  • These counts make the campaign trade-off more concrete than accuracy alone.
  • Run Cell 8.
  • Confirm that you see a two-by-two matrix with Actual labels on the vertical axis.
  • Save the notebook.

Confusion matrix missing?

  • Confirm that Cell 7 created test_predictions before Cell 8 ran.
  • Check that both loops remain indented beneath their matching for statements.
  • Rerun Cell 8 after restoring any missing plotting line.

Ask for plotting help: Why is my confusion matrix missing or malformed?

  • Append the ROC and precision-recall plots to the bottom of Cell 8:
# Cell 8: Plot final diagnostics
false_positive_rate, true_positive_rate, _ = roc_curve(y_test, test_probabilities)
precision_curve, recall_curve, _ = precision_recall_curve(y_test, test_probabilities)

fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].plot(false_positive_rate, true_positive_rate)
axes[0].plot([0, 1], [0, 1], linestyle="--", color="gray")
axes[0].set_title("ROC curve")
axes[0].set_xlabel("False positive rate")
axes[0].set_ylabel("True positive rate")

axes[1].plot(recall_curve, precision_curve)
axes[1].axhline(y_test.mean(), linestyle="--", color="gray", label="Positive rate")
axes[1].set_title("Precision-recall curve")
axes[1].set_xlabel("Recall")
axes[1].set_ylabel("Precision")
axes[1].legend()
plt.show()

Why use both curves?

The ROC curve compares true-positive coverage with false-positive exposure across thresholds. The precision-recall curve focuses directly on the minority subscriber class.

The dashed positive-rate line gives the precision-recall chart a campaign baseline. A useful ranking keeps precision above that baseline across relevant recall levels.

  • Run Cell 8 again.
  • Confirm that the confusion matrix is followed by separate ROC and precision-recall plots.
  • Save the notebook.

Curves not appearing?

  • Confirm that test_probabilities contains probabilities instead of binary predictions.
  • Check that the final plt.show() remains at the bottom of Cell 8.
  • Rerun Cell 7 before Cell 8 if the probability variable is missing.

Get help with the curves: Why are my ROC or precision-recall curves failing?

  • Add Cell 9 below Cell 8.
  • Extract the tuned tree's strongest transformed features by pasting this first chunk into Cell 9:
# Cell 9: Produce feature evidence, customer segments, and a ranked call list
feature_names = best_tree_model.named_steps["preprocessor"].get_feature_names_out()
feature_importances = best_tree_model.named_steps["model"].feature_importances_

top_features = (
    pd.DataFrame(
        {
            "Feature": feature_names,
            "Importance": feature_importances,
        }
    )
    .sort_values("Importance", ascending=False)
    .head(15)
)
print("Top transformed features:\n", top_features.to_string(index=False))

What is transformed-feature evidence?

One-hot encoding expands categorical columns into model-ready indicators. The transformed names let you connect the tree's importance values back to recognizable customer attributes.

Importance shows which features influenced the tree's splits most. It does not prove that those features caused subscription behavior.

  • Run Cell 9.
  • Confirm that the output lists 15 transformed features in descending importance order.
  • Save the notebook.

Feature names or importances failing?

  • Confirm that best_tree_model is the fitted estimator from tree_search.
  • Check that the pipeline step names remain preprocessor and model.
  • Rerun Cell 5 if the fitted best estimator is missing.

Ask for feature-evidence help: Why can't my fitted pipeline return feature names or importances?

  • Append the ranked prospect table to the bottom of Cell 9:
# Cell 9: Produce feature evidence, customer segments, and a ranked call list
test_ranked = X_test.copy()
test_ranked["actual_subscribed"] = y_test
test_ranked["subscription_probability"] = test_probabilities
test_ranked["recommended_call"] = test_predictions
test_ranked = test_ranked.sort_values("subscription_probability", ascending=False)

How does ranking support targeting?

The table keeps each held-out prospect beside the model's probability and final call recommendation. Sorting by probability places the strongest candidates first.

This ranking supports campaigns with a fixed calling capacity. The Secret Mission uses it to test a limited call budget.

  • Run Cell 9 again.
  • Confirm that Cell 9 completes without an error after creating test_ranked.
  • Save the notebook.

Ranked table failing?

  • Confirm that X_test still has the same index as y_test.
  • Check that Cell 7 created one probability and one prediction for every test row.
  • Rerun Cell 7 before rebuilding the ranked table.

Get help with the ranking: Why can't I combine my test rows with probabilities and predictions?

  • Append the categorical segment analysis to Cell 9:
# Cell 9: Produce feature evidence, customer segments, and a ranked call list
segment_tables = []
for column in categorical_columns:
    segment = (
        test_ranked.groupby(column, dropna=False)
        .agg(
            customers=("actual_subscribed", "size"),
            actual_subscription_rate=("actual_subscribed", "mean"),
            mean_predicted_probability=("subscription_probability", "mean"),
            recommended_call_rate=("recommended_call", "mean"),
        )
        .reset_index()
        .rename(columns={column: "Segment"})
    )
    segment["Feature"] = column
    segment_tables.append(segment)

segment_evidence = pd.concat(segment_tables, ignore_index=True)
segment_evidence = segment_evidence[segment_evidence["customers"] >= 10]
segment_evidence = segment_evidence.sort_values(
    ["mean_predicted_probability", "customers"],
    ascending=[False, False],
).head(15)

Why filter the segment table?

Each categorical value becomes a segment with measured size, subscription rate, predicted probability, and call rate. The minimum of 10 customers removes the smallest groups from this summary.

The final sort surfaces larger high-probability segments that can support a targeting recommendation. These patterns remain descriptive evidence.

  • Run Cell 9 again.
  • Confirm that the cell completes after creating segment_evidence.
  • Save the notebook.

Segment analysis failing?

  • Confirm that categorical_columns was created from X_train in Cell 4.
  • Check the indentation of every line inside the segment loop.
  • Confirm that test_ranked contains all three added evidence columns.

Ask for segment-analysis help: Why is my categorical segment table failing?

  • Append the evidence outputs and recommendation prompt to the bottom of Cell 9:
# Cell 9: Produce feature evidence, customer segments, and a ranked call list
print("\nHigh-probability segment evidence:\n", segment_evidence.to_string(index=False))
print("\nTop-ranked call candidates:\n", test_ranked.head(20).to_string())

print(
    "\nWrite 2-3 recommendations in markdown. Cite the final metric table, "
    "confusion matrix, top features, and segment evidence. Avoid causal claims."
)

How should you use this evidence?

  • The segment table identifies groups with high predicted probabilities.
  • The ranked table identifies individual prospects for the front of a call queue.
  • The printed prompt keeps your recommendations tied to measured evidence.
  • Run Cell 9 again.
  • Confirm that you see the feature table, segment table, and top 20 ranked prospects.
  • Save the notebook.

Evidence tables missing?

  • Run the complete Cell 9 from its first line.
  • Confirm that each print statement remains below the code that creates its table.
  • Check that the final closing parenthesis remains beneath the recommendation text.

Get help with the evidence output: Why are my final evidence tables missing from Cell 9?

✔️ Awesome, I've got everything!

Great work. Cells 7 through 9 now turn the untouched test results into model diagnostics and campaign evidence.

  • Save Challenge1_BankMarketing_<YourName>.ipynb before moving to the recommendations.

ⓧ I'd like to double check the full code

Compare your nine code cells with this complete notebook code. Keep the competition template's required section headings around these cells.

# Cell 1: Imports, versions, and reusable evaluation helper
import sys

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import sklearn
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
    accuracy_score,
    average_precision_score,
    classification_report,
    confusion_matrix,
    f1_score,
    precision_recall_curve,
    precision_score,
    recall_score,
    roc_auc_score,
    roc_curve,
)
from sklearn.model_selection import (
    GridSearchCV,
    StratifiedKFold,
    cross_val_predict,
    train_test_split,
)
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.tree import DecisionTreeClassifier

RANDOM_STATE = 42
DATA_PATH = "/content/bank-additional.csv"

print("Python:", sys.version.split()[0])
print("pandas:", pd.__version__)
print("NumPy:", np.__version__)
print("scikit-learn:", sklearn.__version__)
print("Matplotlib:", matplotlib.__version__)


def metric_row(model_name, split_name, y_true, probabilities, threshold=0.5):
    predictions = (np.asarray(probabilities) >= threshold).astype(int)
    return {
        "Model": model_name,
        "Split": split_name,
        "Threshold": round(float(threshold), 3),
        "Accuracy": accuracy_score(y_true, predictions),
        "Precision": precision_score(y_true, predictions, zero_division=0),
        "Recall": recall_score(y_true, predictions, zero_division=0),
        "F1": f1_score(y_true, predictions, zero_division=0),
        "ROC-AUC": roc_auc_score(y_true, probabilities),
        "Average Precision": average_precision_score(y_true, probabilities),
    }


# Cell 2: Load and validate the supplied file
bank = pd.read_csv(DATA_PATH, sep=None, engine="python")
assert "y" in bank.columns, "Expected a target column named 'y'."

print("Shape:", bank.shape)
print("Columns:", bank.columns.tolist())
print(bank.head())


# Cell 3: Audit the data and expose the majority-class accuracy trap
print("Missing values:", int(bank.isna().sum().sum()))
print("Duplicate rows:", int(bank.duplicated().sum()))
print("\nData types:\n", bank.dtypes)

raw_target = bank["y"].astype(str).str.lower()
target_counts = raw_target.value_counts()
target_rates = raw_target.value_counts(normalize=True).rename("rate")
print("\nTarget counts:\n", target_counts)
print("\nTarget rates:\n", target_rates)

fig, ax = plt.subplots(figsize=(6, 4))
target_counts.reindex(["no", "yes"]).plot.bar(ax=ax, color=["#4C78A8", "#F58518"])
ax.set_title("Term-deposit subscription class balance")
ax.set_xlabel("Subscribed")
ax.set_ylabel("Customers")
ax.tick_params(axis="x", rotation=0)
plt.show()

y = (raw_target == "yes").astype(int)
majority_probabilities = np.zeros(len(y), dtype=float)
majority_result = pd.DataFrame(
    [metric_row("Always predict no", "All data", y, majority_probabilities)]
)
print("\nMajority baseline:\n", majority_result.round(3).to_string(index=False))

leakage_columns = [column for column in ["duration"] if column in bank.columns]
X = bank.drop(columns=["y", *leakage_columns])
assert "duration" not in X.columns
assert y.nunique() == 2, "Expected both yes and no target classes."

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.25,
    random_state=RANDOM_STATE,
    stratify=y,
)

print("\nRemoved leakage columns:", leakage_columns)
print("Train shape:", X_train.shape, "Test shape:", X_test.shape)
print("Train positive rate:", round(y_train.mean(), 4))
print("Test positive rate:", round(y_test.mean(), 4))


# Cell 4: Build leakage-safe preprocessing and two first models
categorical_columns = X_train.select_dtypes(include=["object", "category"]).columns.tolist()
numeric_columns = X_train.select_dtypes(exclude=["object", "category"]).columns.tolist()

print("Categorical columns:", categorical_columns)
print("Numeric columns:", numeric_columns)


def build_pipeline(model):
    preprocessor = ColumnTransformer(
        transformers=[
            (
                "categorical",
                OneHotEncoder(handle_unknown="ignore"),
                categorical_columns,
            ),
            ("numeric", StandardScaler(), numeric_columns),
        ]
    )
    return Pipeline(
        steps=[
            ("preprocessor", preprocessor),
            ("model", model),
        ]
    )


logistic_model = build_pipeline(
    LogisticRegression(
        class_weight="balanced",
        max_iter=1000,
        random_state=RANDOM_STATE,
    )
)

base_tree_model = build_pipeline(
    DecisionTreeClassifier(
        class_weight="balanced",
        max_depth=5,
        min_samples_split=20,
        random_state=RANDOM_STATE,
    )
)

logistic_model.fit(X_train, y_train)
base_tree_model.fit(X_train, y_train)

training_results = pd.DataFrame(
    [
        metric_row(
            "Logistic Regression",
            "Train",
            y_train,
            logistic_model.predict_proba(X_train)[:, 1],
        ),
        metric_row(
            "Base Decision Tree",
            "Train",
            y_train,
            base_tree_model.predict_proba(X_train)[:, 1],
        ),
    ]
)
print(training_results.round(3).to_string(index=False))


# Cell 5: Tune decision-tree complexity using only the training split
cross_validation = StratifiedKFold(
    n_splits=5,
    shuffle=True,
    random_state=RANDOM_STATE,
)

parameter_grid = {
    "model__max_depth": [3, 5, 8],
    "model__min_samples_split": [10, 30],
    "model__min_samples_leaf": [5, 15],
    "model__class_weight": [None, "balanced"],
}

scoring = {
    "f1": "f1",
    "roc_auc": "roc_auc",
    "average_precision": "average_precision",
}

tree_search = GridSearchCV(
    estimator=build_pipeline(
        DecisionTreeClassifier(random_state=RANDOM_STATE)
    ),
    param_grid=parameter_grid,
    scoring=scoring,
    refit="f1",
    cv=cross_validation,
    n_jobs=-1,
    return_train_score=True,
)

tree_search.fit(X_train, y_train)
best_tree_model = tree_search.best_estimator_

print("Best parameters:", tree_search.best_params_)
print("Best cross-validated F1:", round(tree_search.best_score_, 4))


# Cell 6: Tune the business threshold using out-of-fold training probabilities
oof_probabilities = cross_val_predict(
    best_tree_model,
    X_train,
    y_train,
    cv=cross_validation,
    method="predict_proba",
    n_jobs=-1,
)[:, 1]

threshold_rows = []
for threshold in np.arange(0.10, 0.91, 0.01):
    predictions = (oof_probabilities >= threshold).astype(int)
    threshold_rows.append(
        {
            "Threshold": threshold,
            "Precision": precision_score(y_train, predictions, zero_division=0),
            "Recall": recall_score(y_train, predictions, zero_division=0),
            "F1": f1_score(y_train, predictions, zero_division=0),
        }
    )

threshold_table = pd.DataFrame(threshold_rows)
best_threshold_index = threshold_table["F1"].idxmax()
best_threshold = float(threshold_table.loc[best_threshold_index, "Threshold"])
best_threshold_row = threshold_table.loc[best_threshold_index]

print("Frozen threshold:", round(best_threshold, 3))
print(best_threshold_row.round(3).to_string())

fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(threshold_table["Threshold"], threshold_table["Precision"], label="Precision")
ax.plot(threshold_table["Threshold"], threshold_table["Recall"], label="Recall")
ax.plot(threshold_table["Threshold"], threshold_table["F1"], label="F1")
ax.axvline(best_threshold, color="black", linestyle="--", label="Chosen threshold")
ax.set_title("Out-of-fold metric trade-off by call threshold")
ax.set_xlabel("Threshold")
ax.set_ylabel("Score")
ax.legend()
plt.show()


# Cell 7: Open the untouched test set once and compare models
model_settings = {
    "Logistic Regression": (logistic_model, 0.5),
    "Base Decision Tree": (base_tree_model, 0.5),
    "Tuned Decision Tree": (best_tree_model, best_threshold),
}

final_rows = []
for model_name, (model, threshold) in model_settings.items():
    for split_name, features, labels in [
        ("Train", X_train, y_train),
        ("Test", X_test, y_test),
    ]:
        probabilities = model.predict_proba(features)[:, 1]
        final_rows.append(
            metric_row(model_name, split_name, labels, probabilities, threshold)
        )

final_results = pd.DataFrame(final_rows)
print(final_results.round(3).to_string(index=False))

test_probabilities = best_tree_model.predict_proba(X_test)[:, 1]
test_predictions = (test_probabilities >= best_threshold).astype(int)

print("\nTuned decision tree test classification report:\n")
print(
    classification_report(
        y_test,
        test_predictions,
        target_names=["No subscription", "Subscription"],
        zero_division=0,
    )
)


# Cell 8: Plot final diagnostics
matrix = confusion_matrix(y_test, test_predictions)
fig, ax = plt.subplots(figsize=(5, 4))
image = ax.imshow(matrix, cmap="Blues")
for row in range(matrix.shape[0]):
    for column in range(matrix.shape[1]):
        ax.text(column, row, matrix[row, column], ha="center", va="center")
ax.set_xticks([0, 1], labels=["Predict no", "Predict yes"])
ax.set_yticks([0, 1], labels=["Actual no", "Actual yes"])
ax.set_title("Tuned decision tree confusion matrix")
fig.colorbar(image, ax=ax)
plt.show()

false_positive_rate, true_positive_rate, _ = roc_curve(y_test, test_probabilities)
precision_curve, recall_curve, _ = precision_recall_curve(y_test, test_probabilities)

fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].plot(false_positive_rate, true_positive_rate)
axes[0].plot([0, 1], [0, 1], linestyle="--", color="gray")
axes[0].set_title("ROC curve")
axes[0].set_xlabel("False positive rate")
axes[0].set_ylabel("True positive rate")

axes[1].plot(recall_curve, precision_curve)
axes[1].axhline(y_test.mean(), linestyle="--", color="gray", label="Positive rate")
axes[1].set_title("Precision-recall curve")
axes[1].set_xlabel("Recall")
axes[1].set_ylabel("Precision")
axes[1].legend()
plt.show()


# Cell 9: Produce feature evidence, customer segments, and a ranked call list
feature_names = best_tree_model.named_steps["preprocessor"].get_feature_names_out()
feature_importances = best_tree_model.named_steps["model"].feature_importances_

top_features = (
    pd.DataFrame(
        {
            "Feature": feature_names,
            "Importance": feature_importances,
        }
    )
    .sort_values("Importance", ascending=False)
    .head(15)
)
print("Top transformed features:\n", top_features.to_string(index=False))

test_ranked = X_test.copy()
test_ranked["actual_subscribed"] = y_test
test_ranked["subscription_probability"] = test_probabilities
test_ranked["recommended_call"] = test_predictions
test_ranked = test_ranked.sort_values("subscription_probability", ascending=False)

segment_tables = []
for column in categorical_columns:
    segment = (
        test_ranked.groupby(column, dropna=False)
        .agg(
            customers=("actual_subscribed", "size"),
            actual_subscription_rate=("actual_subscribed", "mean"),
            mean_predicted_probability=("subscription_probability", "mean"),
            recommended_call_rate=("recommended_call", "mean"),
        )
        .reset_index()
        .rename(columns={column: "Segment"})
    )
    segment["Feature"] = column
    segment_tables.append(segment)

segment_evidence = pd.concat(segment_tables, ignore_index=True)
segment_evidence = segment_evidence[segment_evidence["customers"] >= 10]
segment_evidence = segment_evidence.sort_values(
    ["mean_predicted_probability", "customers"],
    ascending=[False, False],
).head(15)

print("\nHigh-probability segment evidence:\n", segment_evidence.to_string(index=False))
print("\nTop-ranked call candidates:\n", test_ranked.head(20).to_string())

print(
    "\nWrite 2-3 recommendations in markdown. Cite the final metric table, "
    "confusion matrix, top features, and segment evidence. Avoid causal claims."
)
Write recommendations and export the submission

Your recommendations should translate measured behavior into a practical campaign choice. Each claim needs a visible result from the final metric table, confusion matrix, feature evidence, or segment evidence.

  • Add a Markdown cell below Cell 9.
  • Write two or three campaign recommendations.
  • Cite a measured model metric in at least one recommendation.
  • Cite a confusion-matrix count in at least one recommendation.
  • Support your targeting advice with a top feature or high-probability segment.
  • Use phrases such as associated with or ranked highly when discussing patterns.
  • Avoid stating that a feature causes a customer to subscribe.

Keep recommendations auditable

A reviewer should be able to trace each recommendation to a table or plot directly above it. Include the observed value and the campaign action it supports.

Acknowledge the main trade-off between reaching subscribers and spending caller time on false positives. That trade-off is the business meaning of your frozen threshold.

Before you rerun the notebook, do you expect every result to reproduce without changing the frozen threshold?

  • Use Colab's Runtime menu to run every notebook cell from top to bottom.
  • Confirm that Cells 1 through 9 finish without an error.
  • Confirm that the final output contains six train-and-test comparison rows.
  • Confirm that the confusion matrix, ROC curve, and precision-recall curve are visible.
  • Confirm that the feature table, segment table, and ranked call list are visible.
  • Confirm that your Markdown recommendations remain below Cell 9.
  • Save Challenge1_BankMarketing_<YourName>.ipynb in Colab.

That is the full analytical workflow reproduced. Your final evidence now comes from one untouched test evaluation.

Top-to-bottom run failing?

  • Check that bank-additional.csv still appears in the temporary Colab runtime.
  • Upload the same dataset again if the runtime was reset.
  • Rerun from Cell 1 after restoring the file.

Ask for reproducibility help: Why does my notebook fail when I run every cell from the top?

  • Use Colab's File menu to download the completed notebook in .ipynb format.
  • Confirm that Windows saved Challenge1_BankMarketing_<YourName>.ipynb in your Downloads folder.
  • Create a copy of the downloaded notebook in Windows File Explorer.
  • Rename the copy to mynotebook.ipynb.
  • Upload mynotebook.ipynb to the temporary Colab runtime using the Files panel.
  • Add a temporary code cell to check the installed Jupyter nbconvert version by running:
!jupyter nbconvert --version

✔️ I see version 7.17.2

Your Jupyter nbconvert version matches the project pin. You can continue to the HTML conversion.

ⓧ I see an older version

The runtime has nbconvert, but its version does not match this project. Upgrade the temporary runtime package before exporting.

  • Install the pinned version in the temporary Colab runtime by running:
!pip install nbconvert==7.17.2
  • Restart the runtime when Colab prompts you.
  • Run Cells 1 through 9 again after the restart.
  • Run the version check again and confirm that it prints 7.17.2.

ⓧ Command not found

The temporary runtime cannot find nbconvert. Install the pinned package before exporting.

  • Install nbconvert in the temporary Colab runtime by running:
!pip install nbconvert==7.17.2
  • Restart the runtime when Colab prompts you.
  • Run Cells 1 through 9 again after the restart.
  • Run the version check again and confirm that it prints 7.17.2.

nbconvert install failing?

  • Confirm that the Colab runtime has an active internet connection.
  • Wait for the installation cell to finish before restarting the runtime.
  • Rerun the version check after the restart.

Ask for installation help: Why can't my Colab runtime install or find nbconvert 7.17.2?

The HTML export freezes your code, outputs, plots, tables, and Markdown recommendations into one browser-readable file. The temporary copy keeps the documented export command independent of the learner-specific filename.

  • Convert the temporary notebook to HTML by running the documented command:
!jupyter nbconvert --to html mynotebook.ipynb

What does this command create?

Jupyter nbconvert reads mynotebook.ipynb and creates mynotebook.html in the same temporary runtime.

The HTML file preserves the rendered submission for reviewers who do not run the notebook.

  • Confirm that the command reports a completed HTML write.
  • Download mynotebook.html from the Colab Files panel.
  • Rename the downloaded HTML file so its basename matches Challenge1_BankMarketing_<YourName>.ipynb.
  • Confirm that the renamed file is Challenge1_BankMarketing_<YourName>.html.
  • Open the downloaded notebook on Windows.
  • Confirm that its code cells and Markdown recommendations are present.
  • Open the matching HTML file in your Windows browser.
  • Confirm that its plots, tables, outputs, and recommendations are rendered.

You now have both submission formats open successfully. The notebook remains reproducible, while the HTML file presents the completed analysis as a fixed report.

HTML missing outputs or plots?

  • Return to the completed Colab notebook.
  • Run every cell from top to bottom again.
  • Download a fresh notebook after all outputs are visible.
  • Repeat the HTML conversion with that updated notebook.

Ask for export help: Why is my exported HTML missing notebook outputs?

Secret mission

Secret Mission: Optimize a Fixed Call Budget

Your tuned model already ranks prospects by subscription probability. This challenge simulates a campaign that can call only the top 10% of prospects, then measures its lift over random targeting.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep the project, pause the runtime, or remove its files. Google Colab's free tier keeps this project at $0.

Resources you used:

  • One Google Colab notebook named Challenge1_BankMarketing_<YourName>.ipynb stored in Google Drive or downloaded to Windows.
  • One local HTML export named Challenge1_BankMarketing_<YourName>.html.
  • One uploaded CSV file named bank-additional.csv in temporary Colab storage.

Keep everything running

No action is needed. Choose this if you want to revisit the model results or continue improving the campaign analysis.

  • Retain Challenge1_BankMarketing_<YourName>.ipynb in Google Drive if it is saved there.
  • Retain the downloaded Challenge1_BankMarketing_<YourName>.ipynb file on Windows.
  • Retain Challenge1_BankMarketing_<YourName>.html on Windows.

Your stored notebook files remain available for future work. The uploaded CSV can disappear when Colab recycles the temporary runtime.

Pause - I'll come back to this later

Disconnect the current Colab runtime to release temporary compute while preserving your stored notebook files.

  • Confirm that the latest Challenge1_BankMarketing_<YourName>.ipynb copy is present in Google Drive or on Windows.
  • Use Colab's runtime status controls to disconnect the current runtime.

Your stored notebook files remain available after disconnection. Colab may remove bank-additional.csv from temporary runtime storage.

Delete - I don't want to use this again

Remove all project files if you no longer need the notebook or its exported results.

Deletion becomes permanent after you empty each trash location. Separate copies remain available until you remove each one.

Start with the temporary dataset copy in Colab:

  • Switch back to the open Colab notebook.
  • Show the runtime file list from Colab's left sidebar.
  • Locate bank-additional.csv under /content.
  • Use the file's context menu to delete bank-additional.csv.
  • Use Colab's runtime status controls to disconnect the current runtime.

The uploaded dataset is now removed from the current Colab session.

The Google Drive cleanup applies only when the notebook was stored there.

  • Go to Google Drive in your browser.
  • Search for Challenge1_BankMarketing_<YourName>.ipynb.
  • Open the notebook file's context menu.
  • Choose the option that moves the notebook to the trash area.
  • Open the trash area in Google Drive.
  • Permanently delete Challenge1_BankMarketing_<YourName>.ipynb from the trash area.

The Google Drive copy is now removed.

Your downloaded notebook and HTML export are separate local copies on Windows.

  • Press the Windows key to open system search.
  • Type File Explorer into system search.
  • Press Enter to open File Explorer.
  • Use File Explorer's search box to locate Challenge1_BankMarketing_<YourName>.ipynb.
  • Select the notebook in the search results.
  • Press Delete to move the selected notebook to the Recycle Bin.

The local notebook is now in the Recycle Bin.

  • Use File Explorer's search box to locate Challenge1_BankMarketing_<YourName>.html.
  • Select the HTML export in the search results.
  • Press Delete to move the selected HTML export to the Recycle Bin.
  • Empty the Recycle Bin to remove both local files permanently.

That's the cleanup complete. The temporary CSV, notebook copies, and HTML export are removed.

Nice Work!

Nice Work!

Brilliant work. Your competition-ready Google Colab notebook now ranks prospects before a call while excluding duration from its predictors.

What you learned:

  • Reveal the class imbalance trap with a majority-class baseline. Exclude post-call leakage before creating a stratified train-test split.
  • Train logistic regression alongside decision tree models inside leakage-safe preprocessing pipelines. Tune tree complexity with stratified cross-validation. Freeze an F1-optimal threshold from out-of-fold probabilities before opening the test set.
  • Evaluate the untouched test set exactly once. Interpret the final metrics through diagnostic plots. Use feature evidence to support customer-segment recommendations. Export a completed notebook alongside its matching HTML file.
  • Secret Mission: Simulate a campaign with a fixed 10% call budget. Measure precision at that budget. Measure recall at the same budget. Calculate lift against random targeting.

Ready to quiz yourself?