DEV Community

Supervised vs Unsupervised Learning: A Hands-On Guide for Developers

Got it — I'll add them naturally in a short footer note (the least spammy placement) and give you the complete article.


Supervised vs Unsupervised Learning: A Hands-On Guide for Developers

If you've spent any time around machine learning content, you've seen this distinction thrown around a hundred times: supervised learning uses labeled data, unsupervised learning doesn't. That sentence is true, and also almost useless on its own. It doesn't tell you when to reach for which one, what the code actually looks like, or where things go wrong in practice.

This tutorial fixes that. We're going to build a real classifier, build a real clustering model, break them on purpose, and fix them. By the end you'll have working code, a mental model you can actually use, and a list of mistakes to avoid because you'll have made a few of them yourself.

What You'll Need

  • Python 3.9+
  • A virtual environment (recommended, not optional)
  • Basic comfort with Python and reading pandas DataFrames
  • No prior ML experience required

Let's set up the environment first.

python -m venv ml-tutorial
source ml-tutorial/bin/activate  # Windows: ml-tutorial\Scripts\activate

pip install scikit-learn pandas numpy matplotlib seaborn jupyter
Enter fullscreen mode Exit fullscreen mode

If you want to follow along without setting anything up locally, consider putting the full code for this tutorial in your own GitHub repo as you work through it — or fork one of scikit-learn's official example notebooks, which cover both paradigms and are worth exploring on their own.

The Core Distinction, Concretely

Forget "labeled vs unlabeled" for a second. Think about it in terms of the question you're asking:

  • Supervised learning: "Given these examples where I already know the answer, predict the answer for a new example." You have inputs X and known outputs y. The model learns the mapping between them.
  • Unsupervised learning: "Given this data, find structure I didn't already know about." You have X, but no y. The model finds patterns, groupings, or reduced representations on its own.

A rough analogy that actually holds up: supervised learning is like studying for an exam with an answer key — you check your work against ground truth. Unsupervised learning is like being handed a box of mixed Lego bricks and sorting them into piles based on color and size, with no instruction sheet telling you the "correct" piles.

Let's write code for both.

Part 1: Supervised Learning — Building a Classifier

We'll use the classic Iris dataset (flower measurements → species) because it's small, clean, and lets you focus on the workflow instead of data wrangling.

Step 1: Load and Explore the Data

import pandas as pd
from sklearn.datasets import load_iris

iris = load_iris(as_frame=True)
df = iris.frame
df['species'] = df['target'].map(dict(enumerate(iris.target_names)))

print(df.head())
print(df['species'].value_counts())
Enter fullscreen mode Exit fullscreen mode

Notice we have a target column — that's our label, y. This is what makes it supervised. Without it, this would just be a bag of numbers with no ground truth to learn from.

Step 2: Split Your Data

This is the step people skip when they're excited, and it's the single most important habit to build.

from sklearn.model_selection import train_test_split

X = df.drop(columns=['target', 'species'])
y = df['target']

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

print(f"Train size: {len(X_train)}, Test size: {len(X_test)}")
Enter fullscreen mode Exit fullscreen mode

stratify=y keeps the class proportions consistent across the split — important when classes aren't perfectly balanced. random_state=42 just makes the split reproducible; the number itself is arbitrary.

Step 3: Train a Model

from sklearn.linear_model import LogisticRegression

clf = LogisticRegression(max_iter=200)
clf.fit(X_train, y_train)
Enter fullscreen mode Exit fullscreen mode

That's it. fit() is where the actual learning happens — the model adjusts internal weights to map X_train to y_train as accurately as it can.

Step 4: Evaluate

from sklearn.metrics import classification_report, confusion_matrix

y_pred = clf.predict(X_test)

print(classification_report(y_test, y_pred, target_names=iris.target_names))
print(confusion_matrix(y_test, y_pred))
Enter fullscreen mode Exit fullscreen mode

You should see precision, recall, and F1 scores in the high 0.9s for this dataset — it's an easy one. Real-world data won't be this forgiving, which is exactly why you evaluate on held-out data instead of trusting your gut.

A Quick Exercise

Before moving on, try this yourself:

  1. Swap LogisticRegression for RandomForestClassifier (from sklearn.ensemble) and compare the classification report.
  2. Change test_size to 0.5 and see how much the metrics shift. What does that tell you about how much data this model actually needs?

Part 2: Unsupervised Learning — Finding Structure

Now let's throw away the labels and pretend we don't know the species. This is a common real scenario — you get a dataset and genuinely don't have ground-truth categories yet.

Step 1: Prep the Data (No Labels This Time)

X_unsupervised = df.drop(columns=['target', 'species'])
Enter fullscreen mode Exit fullscreen mode

That's the whole "label" story — there isn't one. X_unsupervised is just four measurement columns.

Step 2: Cluster with K-Means

from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_unsupervised)

kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
clusters = kmeans.fit_predict(X_scaled)

df['cluster'] = clusters
print(df.groupby('cluster')['species'].value_counts())
Enter fullscreen mode Exit fullscreen mode

Run this and you'll usually see the clusters line up fairly well with the actual species — which is a nice sanity check, but remember: the algorithm never saw the species labels. It found groupings purely from the structure of the measurements.

Step 3: Figure Out How Many Clusters You Actually Need

In the real world you don't know n_clusters in advance. The "elbow method" is the standard starting point:

import matplotlib.pyplot as plt

inertias = []
k_range = range(1, 10)

for k in k_range:
    km = KMeans(n_clusters=k, random_state=42, n_init=10)
    km.fit(X_scaled)
    inertias.append(km.inertia_)

plt.plot(k_range, inertias, marker='o')
plt.xlabel('Number of clusters (k)')
plt.ylabel('Inertia')
plt.title('Elbow Method for Optimal k')
plt.savefig('elbow_plot.png')
plt.show()
Enter fullscreen mode Exit fullscreen mode

You're looking for the point where adding more clusters stops meaningfully reducing inertia — the "elbow" in the curve. It's a heuristic, not a precise answer, so pair it with domain knowledge whenever you can.

Step 4: Dimensionality Reduction (Another Flavor of Unsupervised Learning)

Clustering isn't the only unsupervised technique. PCA (Principal Component Analysis) compresses your features into fewer dimensions while preserving as much variance as possible — useful for visualization and for speeding up downstream models.

from sklearn.decomposition import PCA

pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)

plt.scatter(X_pca[:, 0], X_pca[:, 1], c=clusters, cmap='viridis')
plt.xlabel('PC1')
plt.ylabel('PC2')
plt.title('PCA Projection Colored by Cluster')
plt.savefig('pca_plot.png')
plt.show()

print(f"Explained variance ratio: {pca.explained_variance_ratio_}")
Enter fullscreen mode Exit fullscreen mode

If explained_variance_ratio_ sums to something like 0.95+, your 2D plot is a pretty faithful representation of the original 4D data.

A Quick Exercise

  1. Try n_clusters=2 and n_clusters=5 on the same data. Does either produce a more "sensible" grouping than 3?
  2. Swap KMeans for DBSCAN (from sklearn.cluster import DBSCAN) — no need to specify cluster count up front. Compare the results.

Side-by-Side Comparison

Supervised Unsupervised
Needs labeled data Yes No
Typical goal Predict a known output Discover structure
Example algorithms Logistic Regression, Random Forest, SVM K-Means, DBSCAN, PCA
Evaluation Accuracy, F1, RMSE against ground truth Inertia, silhouette score, domain judgment
Common use cases Spam detection, price prediction, image classification Customer segmentation, anomaly detection, topic modeling
Data cost Labeling is often expensive Often cheaper — you already have raw data

Common Errors and Troubleshooting

"My classifier gets 100% accuracy on training data but performs terribly on new data."
Classic overfitting. Your model memorized the training set instead of learning generalizable patterns. Fixes: reduce model complexity, add regularization (LogisticRegression(C=0.1) for stronger regularization), get more training data, or use cross-validation to catch this earlier:

from sklearn.model_selection import cross_val_score
scores = cross_val_score(clf, X, y, cv=5)
print(f"CV accuracy: {scores.mean():.3f} (+/- {scores.std():.3f})")
Enter fullscreen mode Exit fullscreen mode

"KMeans gives different results every time I run it."
KMeans initializes cluster centers somewhat randomly. Set random_state for reproducibility and use n_init=10 (or higher) so it runs multiple initializations and keeps the best one.

"My clusters don't make any sense."
Almost always a scaling issue. K-Means uses distance, and if one feature is measured in the thousands (like income) while another is 0–1 (like a ratio), the large-scale feature dominates the distance calculation regardless of actual importance. Always StandardScaler or MinMaxScaler your features before clustering.

"ValueError: Input contains NaN."
Both paradigms choke on missing values. Check with df.isnull().sum() before doing anything else, and decide deliberately: drop rows, impute with SimpleImputer, or investigate why the data is missing in the first place — sometimes missingness itself is informative.

"My model is 'accurate' but useless in production."
Check for class imbalance. A classifier predicting "not fraud" 100% of the time can hit 99% accuracy on a dataset where fraud is 1% of cases, while being completely useless. Look at precision/recall per class, not just overall accuracy, and consider class_weight='balanced' in your model.

Performance Tips

  • Scale before you cluster, always. This is the single most common unsupervised bug.
  • Use n_jobs=-1 on estimators that support it (like RandomForestClassifier) to parallelize across CPU cores.
  • For large datasets, MiniBatchKMeans trades a little accuracy for a lot of speed compared to standard KMeans.
  • Don't fit your scaler on the full dataset before splitting. Fit on X_train only, then .transform() on X_test. Fitting on everything leaks test-set information into training — a subtle form of cheating that inflates your metrics.
  • Cache expensive preprocessing. If you're iterating on model choice but not on feature engineering, pickle your processed X_train/X_test so you're not recomputing it every run.

Best Practices Worth Internalizing

  1. Always hold out a test set, even for quick experiments. It's tempting to skip this "just to check something," and that's exactly when bad habits form.
  2. Start simple. Logistic Regression before Random Forest before a neural net. Get a baseline before reaching for complexity.
  3. Visualize before you model. A quick seaborn.pairplot() or df.describe() catches more bugs than any algorithm will.
  4. Don't trust a single metric. Accuracy alone hides class imbalance problems; inertia alone hides bad cluster counts. Look at multiple angles.
  5. Version your data and your model, not just your code. pip freeze > requirements.txt and consider DVC or even just saving dataset hashes if reproducibility matters to your project.

Where to Go From Here

  • scikit-learn's official user guide — genuinely one of the best-written docs in the Python ecosystem, worth reading cover to cover once.
  • Google's Machine Learning Crash Course — free, hands-on, good for reinforcing the fundamentals covered here.
  • Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow by Aurélien Géron — the book most working ML engineers point beginners toward.
  • Kaggle Learn — short, free, code-first courses, plus real datasets to practice both supervised and unsupervised techniques on once you're past the toy examples.
  • The scikit-learn GitHub examples directory is worth bookmarking — it's a good habit to skim a real implementation before reaching for a tutorial.

Wrapping Up

The labeled/unlabeled distinction is the right starting point, but the real difference shows up in the workflow: supervised learning is a loop of predict → compare to ground truth → adjust. Unsupervised learning is a loop of group or compress → inspect → decide if it's meaningful, because there's no ground truth to check against.

Both of the code examples above are short enough to run start to finish in under ten minutes. If you haven't already, open a notebook and actually run them — swapping in your own dataset once you've got the Iris example working is the fastest way to make this stick.

If you hit an error not covered here, drop it in the comments — troubleshooting real stack traces is usually more useful than any tutorial section.


Side note: this write-up started as prep material for a session on some of these concepts — if you're weighing data science courses in Bangalore's BTM Layout, or data science training in Bangalore more generally, feel free to ask in the comments and I'm happy to share notes.

Top comments (0)