[Week 8] Starting Classification: Building a Logistic Regression Model ๐ŸŽฏ

English translation

Welcome to Week 8!
Last time, we addressed a regression problem that predicts continuous numerical values
using a Linear Regression model.

Today, we explore another major Machine Learning task: Classification.
The goal is to assign given data to one of the specified categories.

Implement and evaluate Logistic Regression, a basic but important classification model,
using scikit-learn.

๐Ÿค” Module 1: Why Is Linear Regression Insufficient? (Classification)

What is classification? - Assigning data to one of a predefined set of categories.

Limitations of Linear Regression:

Classification therefore requires a new model that restricts outputs to a particular range (e.g., between 0 and 1).

# Import libraries
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression

# Generate synthetic classification data (pass/fail based on study hours)
# Study hours (feature X)
study_hours = np.array(
    [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]]
    )
# Pass status (ground truth y, 0: fail, 1: pass)
pass_fail = np.array([0, 0, 0, 0, 1, 1, 1, 1, 1, 1])

# Try training a Linear Regression model
linear_model = LinearRegression()
linear_model.fit(study_hours, pass_fail)

# Visualize the results
plt.scatter(study_hours, pass_fail,
            c=pass_fail, cmap='coolwarm', s=100,
            label='Actual Data (0:Fail, 1:Pass)')
# Prediction line from the Linear Regression model
plt.plot(study_hours, linear_model.predict(study_hours),
         color='black', label='Linear Regression Prediction')

plt.title("Classification by regression")
plt.xlabel("Study time")
plt.ylabel("Pass or not (0 or 1)")
plt.yticks([0, 1]) # Fix the y-axis ticks at 0 and 1
plt.legend()
plt.grid(True)
plt.show()

print("๊ฒฐ๊ณผ ํ•ด์„:")
print("- ์˜ˆ์ธก ์ง์„ ์ด y=0๊ณผ y=1 ๋ฒ”์œ„๋ฅผ ํฌ๊ฒŒ ๋ฒ—์–ด๋‚ฉ๋‹ˆ๋‹ค.")
print("- ์˜ˆ๋ฅผ ๋“ค์–ด ๊ณต๋ถ€ ์‹œ๊ฐ„์ด 1์‹œ๊ฐ„์ผ ๋•Œ ์˜ˆ์ธก๊ฐ’์€ 0๋ณด๋‹ค ์ž‘๊ณ ,")
print("  10์‹œ๊ฐ„์ผ ๋•Œ๋Š” 1๋ณด๋‹ค ํฐ ๊ฐ’์ด ๋‚˜์˜ต๋‹ˆ๋‹ค.")
print("=> ์ฆ‰, ๋ถ„๋ฅ˜ ๋ฌธ์ œ์— ์ง์ ‘ ์ ์šฉํ•˜๊ธฐ ๋ถ€์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.")
<Figure size 640x480 with 1 Axes>
์‹คํ–‰ ๊ฒฐ๊ณผ
๊ฒฐ๊ณผ ํ•ด์„:
- ์˜ˆ์ธก ์ง์„ ์ด y=0๊ณผ y=1 ๋ฒ”์œ„๋ฅผ ํฌ๊ฒŒ ๋ฒ—์–ด๋‚ฉ๋‹ˆ๋‹ค.
- ์˜ˆ๋ฅผ ๋“ค์–ด ๊ณต๋ถ€ ์‹œ๊ฐ„์ด 1์‹œ๊ฐ„์ผ ๋•Œ ์˜ˆ์ธก๊ฐ’์€ 0๋ณด๋‹ค ์ž‘๊ณ ,
  10์‹œ๊ฐ„์ผ ๋•Œ๋Š” 1๋ณด๋‹ค ํฐ ๊ฐ’์ด ๋‚˜์˜ต๋‹ˆ๋‹ค.
=> ์ฆ‰, ๋ถ„๋ฅ˜ ๋ฌธ์ œ์— ์ง์ ‘ ์ ์šฉํ•˜๊ธฐ ๋ถ€์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

Module 2: Logistic Regression and the Sigmoid (ฯƒ\sigma) Function

Logistic Regression (Logistic Regression):

Sigmoid Function:

# Define the sigmoid function
def sigmoid(z):
  return 1 / (1 + np.exp(-z))

# Generate z values over the range (-10 ~ 10)
z = np.linspace(-10, 10, 100)
# Calculate the sigmoid function for each z value
sigma_z = sigmoid(z)

# Plot the sigmoid function
plt.plot(z, sigma_z)
plt.title("Sigmoid Function")
plt.xlabel("z (Linear Regression Output: Wx + b)")
plt.ylabel("Sigmoid Output (Probability)")
plt.grid(True)
# Mark the point where probability is 0.5 (z=0) with lines
plt.axhline(y=0.5, color='red', linestyle='--',
            label='Decision Boundary (Prob=0.5)')
plt.axvline(x=0, color='gray', linestyle=':')
plt.legend()
plt.show()

print("๊ฒฐ๊ณผ ํ•ด์„:")
print("- ์„ ํ˜• ํšŒ๊ท€์˜ ๊ฒฐ๊ณผ(z)๊ฐ€ ์•„๋ฌด๋ฆฌ ์ปค์ ธ๋„ ํ™•๋ฅ ์€ 1์— ์ˆ˜๋ ดํ•ฉ๋‹ˆ๋‹ค.")
print("- ๊ฒฐ๊ณผ(z)๊ฐ€ ์•„๋ฌด๋ฆฌ ์ž‘์•„์ ธ๋„ ํ™•๋ฅ ์€ 0์— ์ˆ˜๋ ดํ•ฉ๋‹ˆ๋‹ค.")
print("- ๊ฒฐ๊ณผ(z)๊ฐ€ 0์ผ ๋•Œ, ํ™•๋ฅ ์€ ์ •ํ™•ํžˆ 0.5๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.")
<Figure size 640x480 with 1 Axes>
์‹คํ–‰ ๊ฒฐ๊ณผ
๊ฒฐ๊ณผ ํ•ด์„:
- ์„ ํ˜• ํšŒ๊ท€์˜ ๊ฒฐ๊ณผ(z)๊ฐ€ ์•„๋ฌด๋ฆฌ ์ปค์ ธ๋„ ํ™•๋ฅ ์€ 1์— ์ˆ˜๋ ดํ•ฉ๋‹ˆ๋‹ค.
- ๊ฒฐ๊ณผ(z)๊ฐ€ ์•„๋ฌด๋ฆฌ ์ž‘์•„์ ธ๋„ ํ™•๋ฅ ์€ 0์— ์ˆ˜๋ ดํ•ฉ๋‹ˆ๋‹ค.
- ๊ฒฐ๊ณผ(z)๊ฐ€ 0์ผ ๋•Œ, ํ™•๋ฅ ์€ ์ •ํ™•ํžˆ 0.5๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

Decision Boundary:

Cost Function:

๐Ÿฉบ Module 3: Logistic Regression Modeling Step by Step (Breast Cancer Diagnosis)

Now solve a real classification problem using scikit-learn.
Using the breast cancer diagnosis dataset, build a model that classifies tumors as malignant or benign from their features.

Follow the 5-step Machine Learning workflow again.

[Supplement] NumPy Indexing [:, 0:1] Explained

Before feeding data into the scikit-learn model,
let's take another detailed look at the X = cancer.data[:, 0:1] code we use.

1. Shape of cancer.data - The breast cancer dataset contains multiple samples (tumors),
each with multiple features (radius, texture, etc.).

2. [:, 0] vs [:, 0:1]

Conclusion: For scikit-learn models, input X must always be a 2-dimensional array with shape (sample count, feature count).
Even when selecting one feature, use slicing such as [:, 0:1] to preserve dimensions.

# Import additional required libraries
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression # Change the model!
from sklearn.model_selection import train_test_split
# The metrics module contains performance metrics.
from sklearn.metrics import (accuracy_score, precision_score,
                             recall_score, f1_score,
                             confusion_matrix, classification_report)

# 1. Prepare data (breast cancer dataset)
cancer = load_breast_cancer()

# Use only 'mean radius' as a 2D array from the available features
X = cancer.data[:, 0:1]
y = cancer.target       # 0: Malignant, 1: Benign

# Explore the data (omitted here, but important in actual analysis)
print(f"๋ฐ์ดํ„ฐ์…‹ ํฌ๊ธฐ (์ƒ˜ํ”Œ ์ˆ˜, ํŠน์ง• ์ˆ˜): {X.shape}")
print(f"ํƒ€๊ฒŸ ์ข…๋ฅ˜ ๋ฐ ๊ฐœ์ˆ˜: {np.unique(y, return_counts=True)}")
๋ฐ์ดํ„ฐ์…‹ ํฌ๊ธฐ (์ƒ˜ํ”Œ ์ˆ˜, ํŠน์ง• ์ˆ˜): (569, 1)
ํƒ€๊ฒŸ ์ข…๋ฅ˜ ๋ฐ ๊ฐœ์ˆ˜: (array([0, 1]), array([212, 357]))
# 2. Split data into 'practice problems' and a 'real exam' at a 7:3 ratio
# stratify=y : Preserve the original class proportions (the ratio of 0 and 1) when splitting
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

print(f"Train ๋ฐ์ดํ„ฐ ํฌ๊ธฐ: {X_train.shape}")
print(f"Test ๋ฐ์ดํ„ฐ ํฌ๊ธฐ: {X_test.shape}")
Train ๋ฐ์ดํ„ฐ ํฌ๊ธฐ: (398, 1)
Test ๋ฐ์ดํ„ฐ ํฌ๊ธฐ: (171, 1)
# 3. Create a Logistic Regression model object
model = LogisticRegression()

# Train the model on 'practice problems' (fit)
# Internally, it calculates probabilities using the sigmoid function,
# then compares them with the ground truth (0 or 1) to find the optimal W, b.
model.fit(X_train, y_train)

print("๋กœ์ง€์Šคํ‹ฑ ํšŒ๊ท€ ๋ชจ๋ธ ํ•™์Šต ์™„๋ฃŒ!")
๋กœ์ง€์Šคํ‹ฑ ํšŒ๊ท€ ๋ชจ๋ธ ํ•™์Šต ์™„๋ฃŒ!

โœ… Module 4: Evaluating Classification Models with Confusion Matrices

Step 4: Evaluate Performance

To understand classification performance, go beyond 'how many were correct (Accuracy)'
and examine 'how' predictions were right or wrong.

Confusion Matrix (Confusion Matrix):

Let's understand this through the example of a doctor's cancer diagnosis.

  1. TN (True Negative): A doctor correctly diagnoses a healthy (Negative) person as healthy (Negative)

  2. FP (False Positive): The doctor incorrectly diagnoses someone who is actually healthy (Negative) as having cancer (Positive)
    (Telling a healthy person they have cancer - Type I Error)

  3. FN (False Negative): For someone who actually has cancer (Positive), the doctor incorrectly diagnoses them as healthy (Negative)
    (Missing a cancer patient - Type II Error)

  4. TP (True Positive): A doctor correctly diagnoses an actual cancer patient (Positive) as having cancer (Positive)

Key Performance Metrics:

  1. Precision (Precision): Among people diagnosed by the doctor as having cancer (TP + FP), the fraction who actually have cancer (TP)\
  1. Precision=TPTP+FPPrecision = \frac{TP}{TP + FP}\
  2. How precise was the doctor's diagnosis? (FP must be reduced.)
  1. Recall (Sensitivity): Among actual cancer patients (TP + FN), the proportion correctly diagnosed with cancer (TP)\
  1. Recall=TPTP+FNRecall = \frac{TP}{TP + FN}\
  2. How completely did the doctor identify cancer patients? (Reduce FN)
  1. Accuracy: The fraction of all predictions that are correct (TP + TN)\
  1. Accuracy=TP+TNTP+TN+FP+FNAccuracy = \frac{TP + TN}{TP + TN + FP + FN}\
  2. The most intuitive metric, but it can distort performance on imbalanced data (e.g., 99% normal, 1% defective).
  1. F1 Score: The harmonic mean of Precision and Recall. Used to consider both metrics in balance.\
  1. F1=2ร—Precisionร—RecallPrecision+RecallF_1 = 2 \times \frac{Precision \times Recall}{Precision + Recall}\
  2. F1 Score is high when both Precision and Recall are high.
# Make predictions on the 'real exam' data
y_pred = model.predict(X_test)

# 1. Calculate and display the Confusion Matrix
# TN  FP
# FN  TP
conf_matrix = confusion_matrix(y_test, y_pred)
print("--- Confusion Matrix ---")
print(conf_matrix)
tn, fp, fn, tp = conf_matrix.ravel() # Assign each value to a separate variable
--- Confusion Matrix ---
[[51 13]
 [11 96]]
# Visualize a heatmap using seaborn
import seaborn as sns

plt.figure(figsize=(6, 5))
sns.heatmap(conf_matrix, annot=True, fmt='d', cmap='Blues',
            xticklabels=['Predicted Malignant(0)', 'Predicted Benign(1)'],
            yticklabels=['Actual Malignant(0)', 'Actual Benign(1)'])
plt.xlabel('Predicted Label')
plt.ylabel('True Label')
plt.title('Confusion Matrix Visualization')
plt.show()
<Figure size 600x500 with 2 Axes>
์‹คํ–‰ ๊ฒฐ๊ณผ
# 2. Calculate the main performance metrics
accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)

print("\n--- ์ฃผ์š” ์„ฑ๋Šฅ ์ง€ํ‘œ ---")
print(f"์ •ํ™•๋„ (Accuracy): {accuracy:.4f}")   # (TP + TN) / Total
print(f"์ •๋ฐ€๋„ (Precision): {precision:.4f}")  # TP / (TP + FP)
print(f"์žฌํ˜„์œจ (Recall): {recall:.4f}")  # TP / (TP + FN)
print(f"F1 ์ ์ˆ˜ (F1 Score): {f1:.4f}")  # Harmonic mean of Precision and Recall
--- ์ฃผ์š” ์„ฑ๋Šฅ ์ง€ํ‘œ ---
์ •ํ™•๋„ (Accuracy): 0.8596
์ •๋ฐ€๋„ (Precision): 0.8807
์žฌํ˜„์œจ (Recall): 0.8972
F1 ์ ์ˆ˜ (F1 Score): 0.8889
# 3. Use classification_report() (summarize everything at once)
# support: Actual sample count for each class
print("\n--- Classification Report ---")
print(classification_report(y_test, y_pred,
                            target_names=['Malignant(0)', 'Benign(1)']))

print("\nํ•ด์„:")
print("- Malignant(0) Precision = 0.82: ์•”(์•…์„ฑ) ์ง„๋‹จํ•œ ๊ฒฝ์šฐ, 82%๋Š” ์‹ค์ œ ์•”์ด์—ˆ์Šต๋‹ˆ๋‹ค.")
print("- Malignant(0) Recall = 0.80: ์‹ค์ œ ์•”(์•…์„ฑ) ํ™˜์ž ์ค‘ 80%๋ฅผ ์ฐพ์•„๋ƒˆ์Šต๋‹ˆ๋‹ค. (20%๋Š” ๋†“์นจ)")
print("- Benign(1) Precision = 0.88: ์ •์ƒ(์–‘์„ฑ) ์ง„๋‹จํ•œ ๊ฒฝ์šฐ, 88%๋Š” ์‹ค์ œ ์ •์ƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.")
print("- Benign(1) Recall = 0.90: ์‹ค์ œ ์ •์ƒ(์–‘์„ฑ) ํ™˜์ž ์ค‘ 90%๋ฅผ ์ฐพ์•„๋ƒˆ์Šต๋‹ˆ๋‹ค. (10%๋Š” ๋†“์นจ)")
--- Classification Report ---
              precision    recall  f1-score   support

Malignant(0)       0.82      0.80      0.81        64
   Benign(1)       0.88      0.90      0.89       107

    accuracy                           0.86       171
   macro avg       0.85      0.85      0.85       171
weighted avg       0.86      0.86      0.86       171


ํ•ด์„:
- Malignant(0) Precision = 0.82: ์•”(์•…์„ฑ) ์ง„๋‹จํ•œ ๊ฒฝ์šฐ, 82%๋Š” ์‹ค์ œ ์•”์ด์—ˆ์Šต๋‹ˆ๋‹ค.
- Malignant(0) Recall = 0.80: ์‹ค์ œ ์•”(์•…์„ฑ) ํ™˜์ž ์ค‘ 80%๋ฅผ ์ฐพ์•„๋ƒˆ์Šต๋‹ˆ๋‹ค. (20%๋Š” ๋†“์นจ)
- Benign(1) Precision = 0.88: ์ •์ƒ(์–‘์„ฑ) ์ง„๋‹จํ•œ ๊ฒฝ์šฐ, 88%๋Š” ์‹ค์ œ ์ •์ƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.
- Benign(1) Recall = 0.90: ์‹ค์ œ ์ •์ƒ(์–‘์„ฑ) ํ™˜์ž ์ค‘ 90%๋ฅผ ์ฐพ์•„๋ƒˆ์Šต๋‹ˆ๋‹ค. (10%๋Š” ๋†“์นจ)
# Visualize results (including the decision boundary)
# Scatter plot of Test data (colored by ground truth)
plt.scatter(X_test, y_test, c=y_test, cmap='coolwarm',
            s=50, marker='o', alpha=0.1, label='Actual Class')
# Add small points colored by model predictions to identify incorrect predictions
plt.scatter(X_test, y_pred, c=y_pred, cmap='coolwarm',
            s=10, marker='x', label='Predicted Class')

# Calculate and plot the decision boundary (probability=0.5)
# Find where z = Wx + b = 0 -> x = -b / W
decision_boundary = -model.intercept_ / model.coef_[0]
plt.axvline(x=decision_boundary, color='black', linestyle='--',
            label='Decision Boundary')

plt.title("Test Data")
plt.xlabel("Mean Radius")
plt.ylabel("Class (0:Malignant, 1:Benign)")
plt.yticks([0, 1])
plt.legend()
plt.grid(True)
plt.show()

print(f"\n๊ฒฐ์ • ๊ฒฝ๊ณ„: ํ‰๊ท  ๋ฐ˜์ง€๋ฆ„ ์•ฝ {decision_boundary[0]:.2f}")
print("=> ์ด ๋ชจ๋ธ์€ ํ‰๊ท  ๋ฐ˜์ง€๋ฆ„์ด ์ด ๊ฐ’๋ณด๋‹ค ํฌ๋ฉด 0(์•…์„ฑ), ์ž‘์œผ๋ฉด 1(์–‘์„ฑ)์œผ๋กœ ์˜ˆ์ธกํ•ฉ๋‹ˆ๋‹ค.")
<Figure size 640x480 with 1 Axes>
์‹คํ–‰ ๊ฒฐ๊ณผ
๊ฒฐ์ • ๊ฒฝ๊ณ„: ํ‰๊ท  ๋ฐ˜์ง€๋ฆ„ ์•ฝ 14.71
=> ์ด ๋ชจ๋ธ์€ ํ‰๊ท  ๋ฐ˜์ง€๋ฆ„์ด ์ด ๊ฐ’๋ณด๋‹ค ํฌ๋ฉด 0(์•…์„ฑ), ์ž‘์œผ๋ฉด 1(์–‘์„ฑ)์œผ๋กœ ์˜ˆ์ธกํ•ฉ๋‹ˆ๋‹ค.

Step 5: Inspect Predicted Probabilities and Make Predictions

Logistic Regression calculates probabilities internally.
predict() returns the final classification (0 or 1),
whereas predict_proba() lets you inspect the probability of belonging to each class (0 and 1).

Use this to perform Step 5: prediction.

# Inspect predicted probabilities for the first 5 Test samples
# Results have the form [probability of 0, probability of 1].
probabilities = model.predict_proba(X_test[:5])
print("--- ์ฒซ 5๊ฐœ Test ๋ฐ์ดํ„ฐ์˜ ์˜ˆ์ธก ํ™•๋ฅ  ---")
print("[ 0(์•…์„ฑ)์ผ ํ™•๋ฅ , 1(์–‘์„ฑ)์ผ ํ™•๋ฅ  ]")
print(np.round(probabilities, 3)) # Round to 3 decimal places

# Inspect final predictions for the first 5 Test samples
predictions = model.predict(X_test[:5])
print("\n--- ์ฒซ 5๊ฐœ Test ๋ฐ์ดํ„ฐ์˜ ์ตœ์ข… ์˜ˆ์ธก ---")
print(predictions)

# Predict on new tumor data
# A tumor with mean radius 10
new_tumor_small = np.array([[10]])
pred_small_prob = model.predict_proba(new_tumor_small)
pred_small = model.predict(new_tumor_small)
print(f"\n--- ํ‰๊ท  ๋ฐ˜์ง€๋ฆ„ 10์ธ ์ข…์–‘ ---")
print(f" ์˜ˆ์ธก ํ™•๋ฅ  [์•…์„ฑ, ์–‘์„ฑ]: {np.round(pred_small_prob[0], 3)}")
print(f" ์ตœ์ข… ์˜ˆ์ธก: {'์–‘์„ฑ(Benign)' if pred_small[0] == 1 else '์•…์„ฑ(Malignant)'}")

# A tumor with mean radius 20
new_tumor_large = np.array([[20]])
pred_large_prob = model.predict_proba(new_tumor_large)
pred_large = model.predict(new_tumor_large)
print(f"\n--- ํ‰๊ท  ๋ฐ˜์ง€๋ฆ„ 20์ธ ์ข…์–‘ ---")
print(f" ์˜ˆ์ธก ํ™•๋ฅ  [์•…์„ฑ, ์–‘์„ฑ]: {np.round(pred_large_prob[0], 3)}")
print(f" ์ตœ์ข… ์˜ˆ์ธก: {'์–‘์„ฑ(Benign)' if pred_large[0] == 1 else '์•…์„ฑ(Malignant)'}")
--- ์ฒซ 5๊ฐœ Test ๋ฐ์ดํ„ฐ์˜ ์˜ˆ์ธก ํ™•๋ฅ  ---
[ 0(์•…์„ฑ)์ผ ํ™•๋ฅ , 1(์–‘์„ฑ)์ผ ํ™•๋ฅ  ]
[[0.267 0.733]
 [0.147 0.853]
 [0.052 0.948]
 [0.989 0.011]
 [0.44  0.56 ]]

--- ์ฒซ 5๊ฐœ Test ๋ฐ์ดํ„ฐ์˜ ์ตœ์ข… ์˜ˆ์ธก ---
[1 1 1 0 1]

--- ํ‰๊ท  ๋ฐ˜์ง€๋ฆ„ 10์ธ ์ข…์–‘ ---
 ์˜ˆ์ธก ํ™•๋ฅ  [์•…์„ฑ, ์–‘์„ฑ]: [0.008 0.992]
 ์ตœ์ข… ์˜ˆ์ธก: ์–‘์„ฑ(Benign)

--- ํ‰๊ท  ๋ฐ˜์ง€๋ฆ„ 20์ธ ์ข…์–‘ ---
 ์˜ˆ์ธก ํ™•๋ฅ  [์•…์„ฑ, ์–‘์„ฑ]: [0.996 0.004]
 ์ตœ์ข… ์˜ˆ์ธก: ์•…์„ฑ(Malignant)