Back to Projects
Computer Vision Project

Digit Recognizer

Download public Digit Recognizer train/test CSVs, visualize flattened digit images, train a classifier, validate accuracy, and create submission.csv.

Final deliverable: A complete Digit Recognizer notebook with validation metrics and ImageId/Label submission.csv.

Dataset: Digit Recognizer public CSV mirror

The notebook downloads public train.csv and test.csv mirrors for the Kaggle Digit Recognizer task. Rows contain labels plus 784 pixel columns.

  • pixel0 through pixel783: grayscale pixel intensity values
  • label: digit label from 0 to 9 in train.csv
  • pixel0 through pixel783: grayscale pixel intensity values
Step 1

Download Digit Recognizer Data

Download public train.csv and test.csv files without Kaggle credentials.

Your task

Run the public download cell and confirm train.csv and test.csv exist.

Try writing this cell
# Run this cell in Colab, Kaggle, or a local notebook.
# It downloads public dataset files directly, so you do not need Kaggle credentials.

from pathlib import Path
from urllib.request import urlretrieve
import zipfile

DATA_DIR = Path("data/digit-recognizer")
DATA_DIR.mkdir(parents=True, exist_ok=True)

def find_file(name):
    possible_roots = [DATA_DIR, Path("/kaggle/input")]
    for root in possible_roots:
        if root.exists():
            matches = list(root.rglob(name))
            if matches:
                return matches[0]
    raise FileNotFoundError(f"Could not find {name}. Run the dataset download cell first.")

files = {
    "train.csv": "https://raw.githubusercontent.com/wehrley/Kaggle-Digit-Recognizer/master/train.csv",
    "test.csv": "https://raw.githubusercontent.com/wehrley/Kaggle-Digit-Recognizer/master/test.csv"
}

for filename, url in files.items():
    target = DATA_DIR / filename
    if target.exists() and target.stat().st_size > 0:
        print(f"already exists: {target}")
        continue
    print(f"downloading {filename} ...")
    urlretrieve(url, target)
    print(f"saved: {target}")

for zip_path in DATA_DIR.glob("*.zip"):
    extract_dir = DATA_DIR / zip_path.stem
    extract_dir.mkdir(exist_ok=True)
    if any(extract_dir.iterdir()):
        print(f"already extracted: {extract_dir}")
        continue
    with zipfile.ZipFile(zip_path, "r") as zf:
        zf.extractall(extract_dir)
    print(f"extracted: {zip_path} -> {extract_dir}")

print("Files:")
all_files = [path for path in sorted(DATA_DIR.rglob("*")) if path.is_file()]
for path in all_files[:40]:
    print(path)
if len(all_files) > 40:
    print(f"... {len(all_files) - 40} more files not shown")
Expected output
A file list containing train.csv and test.csv.
Show reference solution
# Run this cell in Colab, Kaggle, or a local notebook.
# It downloads public dataset files directly, so you do not need Kaggle credentials.

from pathlib import Path
from urllib.request import urlretrieve
import zipfile

DATA_DIR = Path("data/digit-recognizer")
DATA_DIR.mkdir(parents=True, exist_ok=True)

def find_file(name):
    possible_roots = [DATA_DIR, Path("/kaggle/input")]
    for root in possible_roots:
        if root.exists():
            matches = list(root.rglob(name))
            if matches:
                return matches[0]
    raise FileNotFoundError(f"Could not find {name}. Run the dataset download cell first.")

files = {
    "train.csv": "https://raw.githubusercontent.com/wehrley/Kaggle-Digit-Recognizer/master/train.csv",
    "test.csv": "https://raw.githubusercontent.com/wehrley/Kaggle-Digit-Recognizer/master/test.csv"
}

for filename, url in files.items():
    target = DATA_DIR / filename
    if target.exists() and target.stat().st_size > 0:
        print(f"already exists: {target}")
        continue
    print(f"downloading {filename} ...")
    urlretrieve(url, target)
    print(f"saved: {target}")

for zip_path in DATA_DIR.glob("*.zip"):
    extract_dir = DATA_DIR / zip_path.stem
    extract_dir.mkdir(exist_ok=True)
    if any(extract_dir.iterdir()):
        print(f"already extracted: {extract_dir}")
        continue
    with zipfile.ZipFile(zip_path, "r") as zf:
        zf.extractall(extract_dir)
    print(f"extracted: {zip_path} -> {extract_dir}")

print("Files:")
all_files = [path for path in sorted(DATA_DIR.rglob("*")) if path.is_file()]
for path in all_files[:40]:
    print(path)
if len(all_files) > 40:
    print(f"... {len(all_files) - 40} more files not shown")
Why this step matters

This keeps the notebook credential-free while matching the Kaggle submission shape.

Common mistake
Treating pixel columns as ordinary tabular features without reshaping a few examples to verify the image layout.
Step 2

Load and Inspect Pixels

Read train.csv and separate labels from pixel features.

Your task

Load train.csv, print shape, and inspect label distribution.

Try writing this cell
from pathlib import Path
import pandas as pd

possible_roots = [
    Path("data/digit-recognizer"),
    Path("/kaggle/input"),
]

def find_file(name):
    for root in possible_roots:
        if root.exists():
            matches = list(root.rglob(name))
            if matches:
                return matches[0]
    raise FileNotFoundError(f"Could not find {name}. Run the dataset download/load cell first.")

train_path = find_file("train.csv")
df = pd.read_csv(train_path)

print(train_path)
print(df.shape)
df.head()

# Separate y and X from the training DataFrame

# Check label counts
Expected output
X should have 784 pixel columns and y should contain digit labels.
Show reference solution
from pathlib import Path
import pandas as pd

possible_roots = [
    Path("data/digit-recognizer"),
    Path("/kaggle/input"),
]

def find_file(name):
    for root in possible_roots:
        if root.exists():
            matches = list(root.rglob(name))
            if matches:
                return matches[0]
    raise FileNotFoundError(f"Could not find {name}. Run the dataset download/load cell first.")

train_path = find_file("train.csv")
df = pd.read_csv(train_path)

print(train_path)
print(df.shape)
df.head()

import numpy as np
import matplotlib.pyplot as plt

y = df["label"].astype(int)
X = df.drop(columns=["label"])

print(X.shape, y.shape)
display(y.value_counts().sort_index())
Why this step matters

Each row is a flattened 28x28 image. Separating X and y makes the supervised learning setup explicit.

Common mistake
Leaving label inside X, which leaks the answer.
Step 3

Visualize Handwritten Digits

Reshape flat pixel rows into 28x28 images and plot examples.

Your task

Plot a small grid of digit images with their labels.

Try writing this cell
# Pick a few rows

# Reshape each row to 28x28

# Plot images
Expected output
A grid of handwritten digit images with labels.
Show reference solution
fig, axes = plt.subplots(2, 5, figsize=(8, 4))
for ax, idx in zip(axes.ravel(), range(10)):
    image = X.iloc[idx].values.reshape(28, 28)
    ax.imshow(image, cmap="gray")
    ax.set_title(f"label={y.iloc[idx]}")
    ax.axis("off")
plt.tight_layout()
plt.show()
Why this step matters

Visualizing images catches data-loading mistakes immediately. If the digits look scrambled, reshaping or column order is wrong.

Common mistake
Training a model without ever checking whether the pixel arrays display correctly.
Step 4

Train a Baseline Classifier

Train a practical baseline model on normalized pixel values.

Your task

Create a validation split, scale pixels to 0-1, train a RandomForestClassifier, and measure accuracy.

Try writing this cell
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report

# Split data

# Normalize pixel values

# Train model and evaluate
Expected output
Validation accuracy and per-class precision/recall/F1.
Show reference solution
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report

X_train, X_val, y_train, y_val = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

X_train_scaled = X_train / 255.0
X_val_scaled = X_val / 255.0

model = RandomForestClassifier(n_estimators=150, random_state=42, n_jobs=-1)
model.fit(X_train_scaled, y_train)
val_preds = model.predict(X_val_scaled)

print("accuracy:", accuracy_score(y_val, val_preds))
print(classification_report(y_val, val_preds))
Why this step matters

Accuracy is intuitive for balanced digit classification, but the class report helps catch digits the model confuses more often.

Common mistake
Scaling train data but forgetting to scale validation/test data the same way.
Step 5

Inspect Mistakes

Use a confusion matrix and visual examples to see which digits are confused.

Your task

Plot a confusion matrix and show a few incorrect predictions.

Try writing this cell
from sklearn.metrics import ConfusionMatrixDisplay

# Plot confusion matrix

# Display a few mistakes
Expected output
A confusion matrix and examples where true label differs from predicted label.
Show reference solution
from sklearn.metrics import ConfusionMatrixDisplay

ConfusionMatrixDisplay.from_predictions(y_val, val_preds, cmap="Blues")
plt.title("Digit Confusion Matrix")
plt.show()

mistakes = X_val[val_preds != y_val].head(10)
mistake_true = y_val[val_preds != y_val].head(10)
mistake_pred = val_preds[val_preds != y_val][:10]

fig, axes = plt.subplots(2, 5, figsize=(8, 4))
for ax, (_, row), true_label, pred_label in zip(axes.ravel(), mistakes.iterrows(), mistake_true, mistake_pred):
    ax.imshow(row.values.reshape(28, 28), cmap="gray")
    ax.set_title(f"T:{true_label} P:{pred_label}")
    ax.axis("off")
plt.tight_layout()
plt.show()
Why this step matters

Mistake inspection makes model evaluation concrete. It shows whether errors are random or tied to visually similar digits.

Common mistake
Only reporting leaderboard-style accuracy and never inspecting failure cases.
Step 6

Create Digit Recognizer Submission

Train on all labeled rows, predict test images, and save ImageId/Label submission.csv.

Your task

Load test.csv, scale pixels the same way, predict labels, and save submission.csv.

Try writing this cell
# Fit model on all labeled images

# Load and scale test.csv

# Predict labels and save submission.csv
Expected output
submission.csv with ImageId and Label columns.
Show reference solution
model.fit(X / 255.0, y)

test_df = pd.read_csv(find_file("test.csv"))
test_preds = model.predict(test_df / 255.0).astype(int)

submission = pd.DataFrame({
    "ImageId": np.arange(1, len(test_preds) + 1),
    "Label": test_preds,
})
submission.to_csv("submission.csv", index=False)
display(submission.head())
print("saved submission.csv", submission.shape)
Why this step matters

Digit Recognizer submissions use one-based ImageId values in the same order as test.csv.

Common mistake
Starting ImageId at 0 instead of 1.