Predict Iris Species with a Decision Tree

Train a decision tree classifier to identify Iris flower species.

Introduction

30 Second Summary

Every time you sort your laundry by checking the color, then the fabric, then the care label, you're following a decision tree. You just don't call it that.

In this project, you will train a decision tree to predict Iris flower species from petal and sepal measurements. You'll use scikit-learn inside Google Colab to build, visualize, and tune the model from scratch.

What You'll Build

A complete machine learning pipeline in a Google Colab notebook that trains, evaluates, and visualizes a decision tree classifier on the Iris dataset.

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

  • A trained decision tree classifier that predicts Iris flower species from petal and sepal measurements.
  • A visual diagram of the tree showing the exact if/else rules the model uses to make predictions.
  • A pruned tree that demonstrates how to prevent overfitting for better generalization.
  • Secret Mission: Plot a depth curve that reveals exactly when your tree tips from learning to memorizing.

Are there any prerequisites?

This project assumes basic Python knowledge: importing libraries, writing functions, and reading print output. No prior machine learning experience is needed.

Set Up Colab and Load the Data

Every machine learning project starts with the same foundation: getting your workspace ready and understanding your data. Before you can train a model, you need somewhere to write code and a dataset to learn from.

In this step, you'll set up a Google Colab notebook, import the Python libraries you need, and load the Iris flower dataset. You'll also explore the dataset's structure so you know exactly what you're working with.

In this step, get ready to:
  • Import the Python libraries for data handling, visualization, and machine learning.
  • Load the Iris dataset and inspect its structure.
  • Create a DataFrame to explore the data in table format.
Open Google Colab and import your libraries

Google Colab is a free, browser-based notebook environment from Google. All the libraries you need for this project are pre-installed, so there's nothing to download.

A new notebook opens with an empty code cell ready for input.

What is Google Colab?

Google Colab is a cloud-hosted version of Jupyter Notebook. It lets you write and run Python code in your browser without installing anything on your computer.

Colab organizes code into individual cells that you run one at a time. Each cell's output appears directly below it, making it perfect for iterative experimentation.

  • Import your libraries by pasting the following code into the first code cell:
# Cell 1: Import libraries
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, plot_tree, export_text
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix

What does this code do?

  • pandas (imported as pd) handles data in table format.
  • matplotlib (imported as plt) creates charts and visualizations.
  • scikit-learn provides the machine learning tools: the Iris dataset (load_iris), data splitting (train_test_split), the decision tree model (DecisionTreeClassifier), tree visualization (plot_tree, export_text), and evaluation metrics (accuracy_score, classification_report, confusion_matrix).
  • Run the cell by pressing Shift+Enter.
  • You should see no output and no error messages.

Why no output?

Import statements load libraries into memory without printing anything. As long as no error appears, the imports worked correctly.

Seeing a ModuleNotFoundError?

  • Make sure you're running this code in Google Colab, not in a local Python environment. Colab has all these libraries pre-installed.
  • Check for typos in the import lines. The package name is sklearn, not scikit-learn, in import statements.
  • Still stuck? Help me fix a ModuleNotFoundError in my Google Colab notebook.
Load the Iris dataset

The Iris dataset is a classic beginner dataset in machine learning. It contains 150 flower samples with 4 measurements each, spread across 3 species.

  • Click the + Code button in the toolbar at the top of your notebook to add a new code cell.
  • Load the dataset and print its key properties by pasting this code into the new cell:
# Cell 2: Load and explore the dataset
iris = load_iris()

print("Feature names:", iris.feature_names)
print("Target names:", iris.target_names)
print("Dataset shape:", iris.data.shape)
print("First 5 samples:\n", iris.data[:5])
print("First 5 targets:", iris.target[:5])

What does this code do?

  • load_iris() loads the full Iris dataset into a variable called iris. This object contains the flower measurements, species labels, and metadata.
  • iris.feature_names shows the four measurements: sepal length, sepal width, petal length, and petal width (all in cm).
  • iris.target_names shows the three species the model will learn to classify: setosa, versicolor, and virginica.
  • iris.data.shape confirms the dataset has 150 samples and 4 features.
  • Run the cell by pressing Shift+Enter.
  • You should see the four feature names, three target names, a shape of (150, 4), and the first 5 rows of raw numerical data.

Seeing a NameError?

  • A NameError means Python doesn't recognize load_iris. Make sure you ran Cell 1 (the imports) first.
  • Colab runs cells independently. If you skip a cell or restart the runtime, you need to re-run all previous cells in order.
  • Still stuck? Help me fix a NameError for load_iris in Google Colab.
Create a DataFrame to explore the data

Raw numerical arrays are hard to read. A DataFrame organizes the data into a labeled table, making it much easier to explore patterns and spot issues.

  • Click + Code to add another new code cell.
  • Create a DataFrame and print summary statistics by pasting this code:
# Cell 3: Create a DataFrame for exploration
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['species'] = iris.target_names[iris.target]

print(df.head(10))
print("\nDataset summary:")
print(df.describe())
print("\nSamples per species:")
print(df['species'].value_counts())

What does this code do?

  • pd.DataFrame() creates a table from the raw data, using the feature names as column headers.
  • df['species'] = iris.target_names[iris.target] adds a new column that maps each numerical label (0, 1, 2) to its species name (setosa, versicolor, virginica).
  • df.head(10) shows the first 10 rows so you can see what individual samples look like.
  • df.describe() prints summary statistics like mean, min, and max for each feature.
  • df['species'].value_counts() counts how many samples belong to each species.
  • Run the cell by pressing Shift+Enter.
  • You should see a table of 10 rows with 5 columns (4 measurements plus species), a statistics summary, and 50 samples per species.

Why 50 per species?

The Iris dataset is perfectly balanced. Each of the three species has exactly 50 samples. This balanced distribution means the model won't be biased toward any one species during training.

Seeing a NameError for iris or pd?

  • Both pd and iris come from earlier cells. Make sure you ran Cell 1 and Cell 2 before this cell.
  • If you restarted the Colab runtime, click Runtime in the menu bar, then Run all to re-execute every cell from the top.
  • Still stuck? Help me fix a NameError in my Colab DataFrame cell.

✔️ Awesome, I've got everything!

Great. Double check that you've run all three cells and that each one produced the expected output.

ⓧ I'd like to double check the full code

Here is the complete code across all three cells so far.

# Cell 1: Import libraries
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, plot_tree, export_text
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix

# Cell 2: Load and explore the dataset
iris = load_iris()

print("Feature names:", iris.feature_names)
print("Target names:", iris.target_names)
print("Dataset shape:", iris.data.shape)
print("First 5 samples:\n", iris.data[:5])
print("First 5 targets:", iris.target[:5])

# Cell 3: Create a DataFrame for exploration
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['species'] = iris.target_names[iris.target]

print(df.head(10))
print("\nDataset summary:")
print(df.describe())
print("\nSamples per species:")
print(df['species'].value_counts())

Your workspace is ready and you know the shape of your data: 150 flowers, 4 measurements each, 3 species to classify. Next up, you'll visualize how these measurements separate the species and split the data for training.

Explore and Prepare the Data

In the last step, you loaded the Iris dataset and explored it as a table. You know there are 150 flowers, 4 measurements per flower, and 3 species.

Numbers in a table can hide patterns that become obvious in a picture. In this step, you'll plot the data to see how species cluster by their measurements, then split it into separate sets for training and testing.

In this step, get ready to:
  • Create scatter plots to see how species cluster by their measurements.
  • Split the data into training and test sets.
  • Confirm the split sizes.
Create scatter plots to visualize species clusters

A scatter plot places each data point on a grid based on two measurements. By coloring each dot by species, you can instantly see whether those measurements naturally group the three flower types apart.

  • Create a new code cell in your notebook and add the following code:
# Cell 4: Visualize the data
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

scatter = axes[0].scatter(
    iris.data[:, 2], iris.data[:, 3],
    c=iris.target, cmap='viridis', edgecolors='k', s=50
)
axes[0].set_xlabel('Petal Length (cm)')
axes[0].set_ylabel('Petal Width (cm)')
axes[0].set_title('Iris Data: Petal Dimensions')
axes[0].legend(
    handles=scatter.legend_elements()[0],
    labels=list(iris.target_names)
)

scatter2 = axes[1].scatter(
    iris.data[:, 0], iris.data[:, 1],
    c=iris.target, cmap='viridis', edgecolors='k', s=50
)
axes[1].set_xlabel('Sepal Length (cm)')
axes[1].set_ylabel('Sepal Width (cm)')
axes[1].set_title('Iris Data: Sepal Dimensions')
axes[1].legend(
    handles=scatter2.legend_elements()[0],
    labels=list(iris.target_names)
)

plt.tight_layout()
plt.show()

What does this code do?

  • plt.subplots(1, 2) creates a figure with two charts side by side.
  • The left chart plots petal length vs petal width. The right chart plots sepal length vs sepal width.
  • c=iris.target colors each dot by species, and cmap='viridis' picks the color palette.
  • The legend maps each color to a species name so you can tell which cluster is which.
  • Run the cell by clicking the play button or pressing Shift+Enter.
  • You should see two scatter plots side by side, with dots in three distinct colors representing the three species.

Notice how the left chart (petal dimensions) shows three cleanly separated clusters. The right chart (sepal dimensions) has much more overlap between species.

Why do petal measurements separate species better?

Setosa flowers have noticeably smaller petals than the other two species. Versicolor and virginica overlap a lot in sepal size but differ more clearly in petal size.

This means petal measurements carry more useful information for classification. Your decision tree will likely pick up on this pattern too.

Don't see any plots?

  • Make sure you ran Cell 2 first. This cell depends on the iris variable that Cell 2 creates.
  • Check that you ran Cell 1 (the imports) before everything else. Without the matplotlib import, plotting won't work.
  • Still stuck? Help me debug why my scatter plots aren't showing in Colab.
Split the data into training and test sets

Before training a model, you need to set aside some data it won't see during training. This unseen "test set" lets you measure whether the model actually learned patterns or just memorized the answers.

  • Create a new code cell and add the following code:
# Cell 5: Split the data
X = iris.data
y = iris.target

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

print(f"Training set size: {X_train.shape[0]} samples")
print(f"Test set size: {X_test.shape[0]} samples")

What does this code do?

  • X holds the flower measurements (the input features). y holds the species labels (what the model should predict).
  • test_size=0.3 sends 30% of the data to testing and keeps 70% for training.
  • stratify=y makes sure each species is proportionally represented in both sets. Without this, a random split could put all setosa flowers in training and none in testing.
  • random_state=42 locks the random split so you get the same result every time you run it.
  • Run the cell.
  • You should see Training set size: 105 samples and Test set size: 45 samples printed below the cell.

Seeing a NameError?

  • If the error mentions iris, go back and run Cell 2 first. The iris variable is created there.
  • If the error mentions train_test_split, go back and run Cell 1. That cell imports all the libraries you need.
  • Still stuck? Help me fix a NameError when splitting my Iris data.

✔️ Awesome, I've got everything!

Nice work. Make sure you've run all five cells in order from top to bottom.

ⓧ I'd like to double check the full code

Here is the complete notebook code up to this point. Compare it cell by cell with your notebook.

# Cell 1: Import libraries
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, plot_tree, export_text
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix

# Cell 2: Load and explore the dataset
iris = load_iris()

print("Feature names:", iris.feature_names)
print("Target names:", iris.target_names)
print("Dataset shape:", iris.data.shape)
print("First 5 samples:\n", iris.data[:5])
print("First 5 targets:", iris.target[:5])

# Cell 3: Create a DataFrame for exploration
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['species'] = iris.target_names[iris.target]

print(df.head(10))
print("\nDataset summary:")
print(df.describe())
print("\nSamples per species:")
print(df['species'].value_counts())

# Cell 4: Visualize the data
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

scatter = axes[0].scatter(
    iris.data[:, 2], iris.data[:, 3],
    c=iris.target, cmap='viridis', edgecolors='k', s=50
)
axes[0].set_xlabel('Petal Length (cm)')
axes[0].set_ylabel('Petal Width (cm)')
axes[0].set_title('Iris Data: Petal Dimensions')
axes[0].legend(
    handles=scatter.legend_elements()[0],
    labels=list(iris.target_names)
)

scatter2 = axes[1].scatter(
    iris.data[:, 0], iris.data[:, 1],
    c=iris.target, cmap='viridis', edgecolors='k', s=50
)
axes[1].set_xlabel('Sepal Length (cm)')
axes[1].set_ylabel('Sepal Width (cm)')
axes[1].set_title('Iris Data: Sepal Dimensions')
axes[1].legend(
    handles=scatter2.legend_elements()[0],
    labels=list(iris.target_names)
)

plt.tight_layout()
plt.show()

# Cell 5: Split the data
X = iris.data
y = iris.target

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

print(f"Training set size: {X_train.shape[0]} samples")
print(f"Test set size: {X_test.shape[0]} samples")

Your data is visualized and split into training and test sets. Next up, you'll train your first decision tree on the training set and see how accurately it predicts flower species.

Train and Evaluate Your Decision Tree

In the previous step, you explored the Iris data with scatter plots and split it into training and test sets. You saw that petal measurements separate species much more cleanly than sepal measurements.

Now it's time to build the actual model. You'll train a decision tree classifier on the training data and measure how accurately it predicts flower species it has never seen before.

In this step, get ready to:
  • Train a decision tree classifier on the training data.
  • Predict species for the test set and check the accuracy score.
  • Print a detailed classification report and confusion matrix.
Train the model and generate predictions

A decision tree learns by finding the best questions to ask about your data. For example, it might ask "is petal width less than 0.8 cm?" to separate setosa from the other species.

  • Create a new code cell in your Colab notebook and add the following code to train the tree and check its accuracy:
# Cell 6: Train the decision tree
clf = DecisionTreeClassifier(random_state=42)
clf.fit(X_train, y_train)

y_pred = clf.predict(X_test)

accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.2f}")

What does this code do?

  • DecisionTreeClassifier(random_state=42) creates a decision tree with default settings. The random_state parameter locks in the randomness so you get the same results every time.
  • .fit(X_train, y_train) trains the model by finding the best feature thresholds to split the data into groups of the same species.
  • .predict(X_test) feeds the 45 test samples through the trained tree and returns a predicted species for each one.
  • accuracy_score compares the predictions to the actual labels and returns the fraction that are correct.

What is Gini impurity?

By default, the decision tree uses a measure called Gini impurity to decide where to split. Gini impurity measures how mixed the species are in a group.

A group with only setosa flowers has a Gini of 0 (perfectly pure). A group with an even mix of all three species has a high Gini. The tree picks splits that create the purest groups possible.

  • Run the cell by pressing Shift+Enter or clicking the play button on the left side of the cell.
  • You should see a line showing the accuracy as a decimal close to 1.00, meaning the model classified most test flowers correctly.

Seeing a NameError?

  • If you see NameError: name 'X_train' is not defined, make sure you ran all previous cells first. Colab cells must be run in order.
  • If you see NameError: name 'DecisionTreeClassifier' is not defined, go back to Cell 1 and re-run the import cell.

Help me debug my decision tree training cell

Print the classification report and confusion matrix

A single accuracy number can hide important problems. A model might score 90% overall but completely fail on one species. The classification report and confusion matrix show you exactly where the model succeeds and struggles.

  • Create a new code cell and add this code to print the full evaluation:
# Cell 7: Detailed evaluation
print("Classification Report:")
print(classification_report(y_test, y_pred, target_names=iris.target_names))

print("Confusion Matrix:")
print(confusion_matrix(y_test, y_pred))

What does this code do?

  • classification_report prints precision (how many predicted setosa were actually setosa), recall (how many actual setosa were found), and F1-score (a balanced combination of both) for each species.
  • target_names=iris.target_names labels the rows with species names like setosa instead of numbers like 0.
  • confusion_matrix prints a 3x3 grid. Each row is an actual species. Each column is a predicted species. Numbers on the diagonal are correct predictions, and numbers off the diagonal are mistakes.
  • Run the cell.
  • You should see a table with precision, recall, and F1-score columns for setosa, versicolor, and virginica, followed by a 3x3 grid of numbers.
  • Look at the diagonal of the confusion matrix. Large numbers on the diagonal mean the model is predicting most flowers correctly.

Seeing unexpected output?

  • If you see numbers instead of species names in the classification report, make sure you included target_names=iris.target_names inside the classification_report() call.
  • If you see a NameError for y_pred, go back and re-run Cell 6 first. This cell depends on the predictions generated there.

Help me understand my classification report output

✔️ Awesome, I've got everything!

Great. Double check you've run both Cell 6 and Cell 7 and can see the accuracy, classification report, and confusion matrix in your notebook output.

ⓧ I'd like to double check the full code

Here is all the code your notebook should contain up to this point. Compare your cells against this:

# Cell 1: Import libraries
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, plot_tree, export_text
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix

# Cell 2: Load and explore the dataset
iris = load_iris()

print("Feature names:", iris.feature_names)
print("Target names:", iris.target_names)
print("Dataset shape:", iris.data.shape)
print("First 5 samples:\n", iris.data[:5])
print("First 5 targets:", iris.target[:5])

# Cell 3: Create a DataFrame for exploration
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['species'] = iris.target_names[iris.target]

print(df.head(10))
print("\nDataset summary:")
print(df.describe())
print("\nSamples per species:")
print(df['species'].value_counts())

# Cell 4: Visualize the data
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

scatter = axes[0].scatter(
    iris.data[:, 2], iris.data[:, 3],
    c=iris.target, cmap='viridis', edgecolors='k', s=50
)
axes[0].set_xlabel('Petal Length (cm)')
axes[0].set_ylabel('Petal Width (cm)')
axes[0].set_title('Iris Data: Petal Dimensions')
axes[0].legend(
    handles=scatter.legend_elements()[0],
    labels=list(iris.target_names)
)

scatter2 = axes[1].scatter(
    iris.data[:, 0], iris.data[:, 1],
    c=iris.target, cmap='viridis', edgecolors='k', s=50
)
axes[1].set_xlabel('Sepal Length (cm)')
axes[1].set_ylabel('Sepal Width (cm)')
axes[1].set_title('Iris Data: Sepal Dimensions')
axes[1].legend(
    handles=scatter2.legend_elements()[0],
    labels=list(iris.target_names)
)

plt.tight_layout()
plt.show()

# Cell 5: Split the data
X = iris.data
y = iris.target

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

print(f"Training set size: {X_train.shape[0]} samples")
print(f"Test set size: {X_test.shape[0]} samples")

# Cell 6: Train the decision tree
clf = DecisionTreeClassifier(random_state=42)
clf.fit(X_train, y_train)

y_pred = clf.predict(X_test)

accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.2f}")

# Cell 7: Detailed evaluation
print("Classification Report:")
print(classification_report(y_test, y_pred, target_names=iris.target_names))

print("Confusion Matrix:")
print(confusion_matrix(y_test, y_pred))

Your decision tree is trained and evaluated. Next up, you'll visualize the tree as a diagram so you can read the exact rules it uses to classify each flower.

Visualize and Interpret the Tree

In the previous step, you trained a decision tree classifier and confirmed it scores well on the test set. Accuracy tells you how often the model gets it right, but it does not tell you how the model makes its decisions.

Decision trees are one of the few models you can open up and read like a flowchart. In this step, you will render the full tree as a diagram, print its rules as text, and chart which flower measurements drive the model's predictions.

In this step, get ready to:
  • Render the full decision tree as a visual diagram.
  • Print the tree's decision rules as human-readable text.
  • Chart feature importances to see which measurements matter most.
Render the decision tree as a diagram

scikit-learn's plot_tree function draws the entire tree as a flowchart. Each box (called a node) shows the feature it splits on, the threshold value, the Gini impurity score, the number of samples, and the predicted class.

  • Create a new code cell in your notebook and paste the following code:
# Cell 8: Visualize the tree
plt.figure(figsize=(20, 10))
plot_tree(
    clf,
    feature_names=iris.feature_names,
    class_names=list(iris.target_names),
    filled=True,
    rounded=True,
    fontsize=10
)
plt.title("Decision Tree - Unpruned")
plt.tight_layout()
plt.show()

What does this code do?

  • plt.figure(figsize=(20, 10)) creates a large canvas so the tree nodes are readable.
  • feature_names and class_names label the splits and leaf predictions with actual names instead of column indices and numbers.
  • filled=True colors each node by its majority class, so you can visually trace which branches lead to which species.
  • list(iris.target_names) wraps target_names in a Python list because it is a numpy array by default. The class_names parameter expects a regular list.
  • Run the cell.

You should see a large, colorful tree diagram titled "Decision Tree - Unpruned". Each node shows the feature it splits on (like petal width (cm) <= 0.75), the Gini impurity, the sample count, and the majority class.

Tree looks tiny or overlapping?

Make sure your figsize is set to (20, 10). A smaller figure squishes the nodes together and makes labels unreadable.

Also confirm that fontsize=10 is set inside plot_tree(). Without it, Colab may auto-scale the text too small to read.

Help me debug my decision tree diagram

Print the tree's decision rules as text

The diagram gives you the big picture, but reading individual paths is easier in text form. The export_text function prints every branch of the tree as indented if/else rules.

  • Create a new code cell and paste this code:
# Cell 9: Text rules
print("Decision Rules:")
print(export_text(clf, feature_names=list(iris.feature_names)))
  • Run the cell.

You should see indented text showing rules like |--- petal width (cm) <= 0.75 followed by |--- class: setosa. Each indentation level is one step deeper in the tree.

How do I read these rules?

Start at the top and follow the branches. Each line is a condition the model checks. If the condition is true, follow the indented lines beneath it. If false, skip to the next condition at the same indentation level.

Try tracing a flower with a petal width of 0.5 cm. The first rule asks if petal width is less than or equal to 0.75. Since 0.5 is below that threshold, you follow the left branch straight to class: setosa. The model classifies it in one step.

Plot feature importances as a bar chart

The tree diagram shows which features appear at each split, but it does not summarize which features the model relies on overall. Feature importance scores do exactly that. Each feature gets a score between 0 and 1, where higher means the model used it more to separate the species.

  • Create a new code cell and paste the following code:
# Cell 10: Feature importances
importances = clf.feature_importances_
feature_names = iris.feature_names

plt.figure(figsize=(8, 5))
plt.barh(feature_names, importances, color='teal')
plt.xlabel('Importance')
plt.title('Feature Importances')
plt.tight_layout()
plt.show()

for name, importance in zip(feature_names, importances):
    print(f"{name}: {importance:.4f}")

What does this code do?

  • clf.feature_importances_ returns an array of importance scores, one per feature. These scores are calculated from how much each feature reduces Gini impurity across all splits in the tree.
  • plt.barh() draws a horizontal bar chart so the feature names are easy to read on the y-axis.
  • The for loop prints the exact numeric importance for each feature, so you can compare values beyond what the chart shows visually.
  • Run the cell.

You should see a horizontal bar chart with four bars representing sepal length, sepal width, petal length, and petal width. The printed values below the chart show the exact importance score for each feature.

Why is petal width so important?

Remember the scatter plots from Step 2? Petal measurements separated the three species far more cleanly than sepal measurements. The tree's feature importances confirm the same finding from a different angle.

A feature that creates clean splits early in the tree gets a high importance score. A feature the tree barely uses (or never uses) gets a score near zero.

All importance values showing 0.0000?

Make sure you are accessing clf.feature_importances_ (with a trailing underscore). Without the underscore, Python raises an AttributeError instead of returning zeros. If you do see zeros, confirm that Cell 6 ran successfully and that clf was trained with clf.fit(X_train, y_train).

Help me debug my feature importances chart

✔️ Awesome, I've got everything!

Great. Double check you have run all your cells and can see the tree diagram, the text rules, and the feature importances chart.

ⓧ I'd like to double check the full code

Here is all the code from Cell 1 through Cell 10. Compare it against your notebook to find any differences.

# Cell 1: Import libraries
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, plot_tree, export_text
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix

# Cell 2: Load and explore the dataset
iris = load_iris()

print("Feature names:", iris.feature_names)
print("Target names:", iris.target_names)
print("Dataset shape:", iris.data.shape)
print("First 5 samples:\n", iris.data[:5])
print("First 5 targets:", iris.target[:5])

# Cell 3: Create a DataFrame for exploration
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['species'] = iris.target_names[iris.target]

print(df.head(10))
print("\nDataset summary:")
print(df.describe())
print("\nSamples per species:")
print(df['species'].value_counts())

# Cell 4: Visualize the data
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

scatter = axes[0].scatter(
    iris.data[:, 2], iris.data[:, 3],
    c=iris.target, cmap='viridis', edgecolors='k', s=50
)
axes[0].set_xlabel('Petal Length (cm)')
axes[0].set_ylabel('Petal Width (cm)')
axes[0].set_title('Iris Data: Petal Dimensions')
axes[0].legend(
    handles=scatter.legend_elements()[0],
    labels=list(iris.target_names)
)

scatter2 = axes[1].scatter(
    iris.data[:, 0], iris.data[:, 1],
    c=iris.target, cmap='viridis', edgecolors='k', s=50
)
axes[1].set_xlabel('Sepal Length (cm)')
axes[1].set_ylabel('Sepal Width (cm)')
axes[1].set_title('Iris Data: Sepal Dimensions')
axes[1].legend(
    handles=scatter2.legend_elements()[0],
    labels=list(iris.target_names)
)

plt.tight_layout()
plt.show()

# Cell 5: Split the data
X = iris.data
y = iris.target

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

print(f"Training set size: {X_train.shape[0]} samples")
print(f"Test set size: {X_test.shape[0]} samples")

# Cell 6: Train the decision tree
clf = DecisionTreeClassifier(random_state=42)
clf.fit(X_train, y_train)

y_pred = clf.predict(X_test)

accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.2f}")

# Cell 7: Detailed evaluation
print("Classification Report:")
print(classification_report(y_test, y_pred, target_names=iris.target_names))

print("Confusion Matrix:")
print(confusion_matrix(y_test, y_pred))

# Cell 8: Visualize the tree
plt.figure(figsize=(20, 10))
plot_tree(
    clf,
    feature_names=iris.feature_names,
    class_names=list(iris.target_names),
    filled=True,
    rounded=True,
    fontsize=10
)
plt.title("Decision Tree - Unpruned")
plt.tight_layout()
plt.show()

# Cell 9: Text rules
print("Decision Rules:")
print(export_text(clf, feature_names=list(iris.feature_names)))

# Cell 10: Feature importances
importances = clf.feature_importances_
feature_names = iris.feature_names

plt.figure(figsize=(8, 5))
plt.barh(feature_names, importances, color='teal')
plt.xlabel('Importance')
plt.title('Feature Importances')
plt.tight_layout()
plt.show()

for name, importance in zip(feature_names, importances):
    print(f"{name}: {importance:.4f}")

You now have a fully transparent model. You can see every rule it uses, trace how it classifies any sample, and know which features drive its decisions. Next up, you will prune the tree to make it simpler and more resistant to overfitting.

Tune the Tree with Pruning

In the last step, you visualized the full decision tree, read its rules as text, and discovered which features drive predictions. Your model is fully trained and interpretable.

The unpruned tree grew until it perfectly fit the training data, which can lead to overfitting. In this step, you'll constrain the tree with pruning and compare it against the original.

In this step, get ready to:
  • Train a pruned decision tree with depth and leaf constraints.
  • Compare the pruned tree's accuracy and depth against the unpruned version.
  • Visualize the pruned tree to see how much simpler it becomes.
Train a constrained decision tree

Your unpruned tree grew as deep as it needed to perfectly classify every training sample. Some of those deep branches capture noise rather than real patterns in the data.

Pruning sets boundaries on the tree's complexity. You limit how deep it can grow and how few samples a leaf can contain, forcing the tree to focus on the strongest patterns.

  • Create a new code cell in your notebook and paste the following code:
# Cell 11: Train a pruned tree
clf_pruned = DecisionTreeClassifier(
    max_depth=3, min_samples_leaf=5, random_state=42
)
clf_pruned.fit(X_train, y_train)

y_pred_pruned = clf_pruned.predict(X_test)
accuracy_pruned = accuracy_score(y_test, y_pred_pruned)

print(f"Unpruned tree accuracy: {accuracy:.2f}")
print(f"Pruned tree accuracy: {accuracy_pruned:.2f}")
print(f"Unpruned tree depth: {clf.get_depth()}")
print(f"Pruned tree depth: {clf_pruned.get_depth()}")

What does this code do?

  • max_depth=3 caps the tree at 3 levels of decisions. No matter how much data there is, the tree won't grow deeper than this.
  • min_samples_leaf=5 requires at least 5 samples in every leaf node. This stops the tree from creating tiny leaves that describe just one or two data points.
  • The last four print lines compare both trees side by side, showing accuracy and depth for each.

Why pre-pruning instead of post-pruning?

Pre-pruning (setting max_depth and min_samples_leaf before training) is the most intuitive way to control a decision tree. You tell the tree its limits upfront, and it builds within those constraints.

The alternative is post-pruning with ccp_alpha, which grows the full tree first and then trims branches that don't contribute enough. Both approaches prevent overfitting. Pre-pruning is easier to reason about for a first project.

  • Run the cell.
  • You should see four printed lines comparing the unpruned and pruned trees' accuracy and depth values.

Not seeing any output?

  • Make sure you ran all previous cells first. This cell needs X_train, y_train, X_test, y_test, accuracy, and clf from earlier cells.
  • If you restarted your Colab runtime at any point, click **Runtime** then **Run all** to re-execute every cell from the top.

Why is my pruned tree cell throwing an error?

Compare the pruned and unpruned trees

Look at the output from your cell. The pruned tree has a much shallower depth than the unpruned version.

Despite being simpler, the pruned tree's test accuracy likely matches or even exceeds the unpruned tree. Fewer decision rules, but each one captures a genuine pattern in the data.

Why can a simpler tree be just as accurate?

The unpruned tree creates extra splits to perfectly classify every training sample. Some of those splits model noise rather than real relationships.

The pruned tree skips those noisy splits and focuses on the strongest patterns. This makes it more likely to perform well on new, unseen data.

Visualize the pruned tree

The accuracy numbers tell part of the story. Rendering the pruned tree as a diagram shows you just how much simpler it is to read and interpret.

  • Create a new code cell and paste the following code:
# Cell 12: Visualize the pruned tree
plt.figure(figsize=(16, 8))
plot_tree(
    clf_pruned,
    feature_names=iris.feature_names,
    class_names=list(iris.target_names),
    filled=True,
    rounded=True,
    fontsize=12
)
plt.title("Decision Tree - Pruned (max_depth=3, min_samples_leaf=5)")
plt.tight_layout()
plt.show()

What does this code do?

  • figsize=(16, 8) creates a slightly smaller canvas than the unpruned tree's (20, 10), since fewer nodes need less space.
  • fontsize=12 bumps up the text size compared to the unpruned tree's fontsize=10. A simpler tree means more room for larger, more readable labels.
  • The title includes the pruning parameters so you can tell the two diagrams apart at a glance.
  • Run the cell.
  • You should see a compact tree diagram that is much easier to read than the unpruned version. Each node still shows the feature, threshold, Gini impurity value, sample count, and predicted class.

Tree looks identical to the unpruned version?

  • Double check that you're plotting clf_pruned and not clf. The variable name in the plot_tree() call determines which model gets rendered.
  • Make sure Cell 11 ran successfully before this cell. The clf_pruned variable must exist in memory.

Why does my pruned tree look the same as the unpruned one?

Extra for Experts: What is Gini impurity?

Each node in your tree diagram shows a Gini impurity value. This number measures how mixed the classes are at that node. A Gini of 0.0 means the node is pure, containing only one species.

The tree picks the feature and threshold at each split that reduces Gini impurity the most. Lower Gini means cleaner separation between species.

✔️ Awesome, I've got everything!

Great. Double check you've saved your notebook.

ⓧ I'd like to double check the full code

Here is all the code across your 12 cells. Compare each cell in your notebook against the code below.

# Cell 1: Import libraries
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, plot_tree, export_text
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix

# Cell 2: Load and explore the dataset
iris = load_iris()

print("Feature names:", iris.feature_names)
print("Target names:", iris.target_names)
print("Dataset shape:", iris.data.shape)
print("First 5 samples:\n", iris.data[:5])
print("First 5 targets:", iris.target[:5])

# Cell 3: Create a DataFrame for exploration
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['species'] = iris.target_names[iris.target]

print(df.head(10))
print("\nDataset summary:")
print(df.describe())
print("\nSamples per species:")
print(df['species'].value_counts())

# Cell 4: Visualize the data
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

scatter = axes[0].scatter(
    iris.data[:, 2], iris.data[:, 3],
    c=iris.target, cmap='viridis', edgecolors='k', s=50
)
axes[0].set_xlabel('Petal Length (cm)')
axes[0].set_ylabel('Petal Width (cm)')
axes[0].set_title('Iris Data: Petal Dimensions')
axes[0].legend(
    handles=scatter.legend_elements()[0],
    labels=list(iris.target_names)
)

scatter2 = axes[1].scatter(
    iris.data[:, 0], iris.data[:, 1],
    c=iris.target, cmap='viridis', edgecolors='k', s=50
)
axes[1].set_xlabel('Sepal Length (cm)')
axes[1].set_ylabel('Sepal Width (cm)')
axes[1].set_title('Iris Data: Sepal Dimensions')
axes[1].legend(
    handles=scatter2.legend_elements()[0],
    labels=list(iris.target_names)
)

plt.tight_layout()
plt.show()

# Cell 5: Split the data
X = iris.data
y = iris.target

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

print(f"Training set size: {X_train.shape[0]} samples")
print(f"Test set size: {X_test.shape[0]} samples")

# Cell 6: Train the decision tree
clf = DecisionTreeClassifier(random_state=42)
clf.fit(X_train, y_train)

y_pred = clf.predict(X_test)

accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.2f}")

# Cell 7: Detailed evaluation
print("Classification Report:")
print(classification_report(y_test, y_pred, target_names=iris.target_names))

print("Confusion Matrix:")
print(confusion_matrix(y_test, y_pred))

# Cell 8: Visualize the tree
plt.figure(figsize=(20, 10))
plot_tree(
    clf,
    feature_names=iris.feature_names,
    class_names=list(iris.target_names),
    filled=True,
    rounded=True,
    fontsize=10
)
plt.title("Decision Tree - Unpruned")
plt.tight_layout()
plt.show()

# Cell 9: Text rules
print("Decision Rules:")
print(export_text(clf, feature_names=list(iris.feature_names)))

# Cell 10: Feature importances
importances = clf.feature_importances_
feature_names = iris.feature_names

plt.figure(figsize=(8, 5))
plt.barh(feature_names, importances, color='teal')
plt.xlabel('Importance')
plt.title('Feature Importances')
plt.tight_layout()
plt.show()

for name, importance in zip(feature_names, importances):
    print(f"{name}: {importance:.4f}")

# Cell 11: Train a pruned tree
clf_pruned = DecisionTreeClassifier(
    max_depth=3, min_samples_leaf=5, random_state=42
)
clf_pruned.fit(X_train, y_train)

y_pred_pruned = clf_pruned.predict(X_test)
accuracy_pruned = accuracy_score(y_test, y_pred_pruned)

print(f"Unpruned tree accuracy: {accuracy:.2f}")
print(f"Pruned tree accuracy: {accuracy_pruned:.2f}")
print(f"Unpruned tree depth: {clf.get_depth()}")
print(f"Pruned tree depth: {clf_pruned.get_depth()}")

# Cell 12: Visualize the pruned tree
plt.figure(figsize=(16, 8))
plot_tree(
    clf_pruned,
    feature_names=iris.feature_names,
    class_names=list(iris.target_names),
    filled=True,
    rounded=True,
    fontsize=12
)
plt.title("Decision Tree - Pruned (max_depth=3, min_samples_leaf=5)")
plt.tight_layout()
plt.show()

Secret mission

Diagnose Overfitting with a Depth Curve

Your pruned tree used max_depth=3, but how do you know that was the right depth? In this bonus challenge, you'll build a depth curve that reveals the exact point where your tree tips from learning to memorizing.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely in Google Colab, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your notebook, disconnect to free up the runtime, or delete it entirely.

Resources you used:

  • Google Colab notebook (stored in your Google Drive).

Keep everything running

No action needed. Choose this if you want to keep experimenting with your decision tree or extend the notebook with new cells.

  • Your notebook stays in Google Drive and you can reopen it anytime from colab.research.google.com.
  • The Colab runtime will disconnect automatically after a period of inactivity, but your saved cells and outputs remain in the notebook file.

Pause - I'll come back to this later

Disconnect the Colab runtime to free up memory. Your notebook file stays saved in Google Drive.

  • In your Colab notebook, click Runtime in the top menu bar.
  • Click Disconnect and delete runtime.
  • This frees up the compute session. When you come back, open the notebook from Google Drive and click Runtime then Run all to re-run your cells.

Delete - I don't want to use this again

Remove the notebook from your Google Drive entirely.

  • In your Colab notebook, click File in the top menu bar.
  • Click Move to trash.
  • To permanently delete it, open drive.google.com in your browser.
  • Click Trash in the left sidebar.
  • Find your notebook, right-click it, and select Delete forever.

Nice Work!

Nice Work!

You just built a complete machine learning pipeline from scratch. Starting with raw flower measurements, you trained, evaluated, visualized, and tuned a decision tree classifier that predicts Iris species with high accuracy.

You've learned how to:

  • Loaded and explored the Iris dataset (150 samples, 4 features, 3 species) and visualized how flower measurements cluster by species using scatter plots.
  • Trained a DecisionTreeClassifier, evaluated it with accuracy scores, a classification report, and a confusion matrix, then visualized its structure as a diagram and text rules to make the model fully interpretable.
  • Examined feature importances to understand which measurements drive predictions, then pruned the tree with max_depth and min_samples_leaf to prevent overfitting.
  • Secret Mission: Built a depth curve to visually diagnose the exact point where your tree tips from learning to memorizing.

Ready to quiz yourself?