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
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
Xand known outputsy. The model learns the mapping between them. -
Unsupervised learning: "Given this data, find structure I didn't already know about." You have
X, but noy. 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())
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)}")
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)
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))
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:
- Swap
LogisticRegressionforRandomForestClassifier(fromsklearn.ensemble) and compare the classification report. - Change
test_sizeto0.5and 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'])
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())
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()
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_}")
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
- Try
n_clusters=2andn_clusters=5on the same data. Does either produce a more "sensible" grouping than 3? - Swap
KMeansforDBSCAN(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})")
"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=-1on estimators that support it (likeRandomForestClassifier) to parallelize across CPU cores. -
For large datasets,
MiniBatchKMeanstrades a little accuracy for a lot of speed compared to standardKMeans. -
Don't fit your scaler on the full dataset before splitting. Fit on
X_trainonly, then.transform()onX_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_testso you're not recomputing it every run.
Best Practices Worth Internalizing
- 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.
- Start simple. Logistic Regression before Random Forest before a neural net. Get a baseline before reaching for complexity.
-
Visualize before you model. A quick
seaborn.pairplot()ordf.describe()catches more bugs than any algorithm will. - Don't trust a single metric. Accuracy alone hides class imbalance problems; inertia alone hides bad cluster counts. Look at multiple angles.
-
Version your data and your model, not just your code.
pip freeze > requirements.txtand 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)