<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Shaka's AI Journal]]></title><description><![CDATA[Personal AI engineering journal — computer vision, deep learning, and deployment. Study notes, working code, and real projects from coursework and independent practice.]]></description><link>https://shaka-ai.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a8ef2e3923670c989379174/02bd2948-376b-48da-8ef2-bd3ec4a7686b.png</url><title>Shaka&apos;s AI Journal</title><link>https://shaka-ai.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 06 Sep 2026 02:45:55 GMT</lastBuildDate><atom:link href="https://shaka-ai.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Deep Learning & PyTorch: Neural Network Theory + Hands-On MNIST Classification]]></title><description><![CDATA[My mental model of deep learning used to be simple: feed it data, get a prediction, done. Turns out there's a much more grounded process behind that — artificial neurons that start knowing nothing, ge]]></description><link>https://shaka-ai.hashnode.dev/deep-learning-pytorch-mnist-neural-network-basics</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/deep-learning-pytorch-mnist-neural-network-basics</guid><category><![CDATA[DeepLearning]]></category><category><![CDATA[Deep Learning]]></category><category><![CDATA[pytorch]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[neural networks]]></category><category><![CDATA[mnist]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Sat, 05 Sep 2026 06:01:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/d4b2aa3d-505d-41ec-b111-ef48684bafb4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My mental model of deep learning used to be simple: feed it data, get a prediction, done. Turns out there's a much more grounded process behind that — artificial neurons that start knowing nothing, get things wrong repeatedly through backpropagation, and gradually learn to recognize patterns. These are my notes on how that actually works, from the fundamentals of neural networks to hands-on training a PyTorch model to recognize handwritten digits (MNIST).</p>
<h2>Table of Contents</h2>
<ul>
<li><p><a href="#what-is-deep-learning">What Is Deep Learning?</a></p>
</li>
<li><p><a href="#machine-learning-vs-deep-learning">Machine Learning vs Deep Learning</a></p>
</li>
<li><p><a href="#artificial-neurons--the-basics">Artificial Neurons — The Basics</a></p>
</li>
<li><p><a href="#neural-network-architecture">Neural Network Architecture</a></p>
</li>
<li><p><a href="#why-pytorch">Why PyTorch?</a></p>
</li>
<li><p><a href="#hands-on-digit-classification-with-pytorch">Hands-On: Digit Classification with PyTorch</a></p>
</li>
<li><p><a href="#recap-checklist">Recap Checklist</a></p>
</li>
<li><p><a href="#mini-quiz">Mini Quiz</a></p>
</li>
<li><p><a href="#further-exercises">Further Exercises</a></p>
</li>
<li><p><a href="#closing-thoughts">Closing Thoughts</a></p>
</li>
</ul>
<hr />
<h2>What Is Deep Learning?</h2>
<p>Deep Learning is a subfield of Machine Learning that uses Artificial Neural Networks (ANNs) with many layers (<em>multi-layer</em>).</p>
<p>Two things set it apart from conventional ML:</p>
<ul>
<li><p>It can learn complex patterns from large, unstructured data.</p>
</li>
<li><p>Feature representation happens automatically — features don't need to be manually engineered or selected like in conventional Machine Learning.</p>
</li>
</ul>
<p>Deep Learning shows up heavily in Computer Vision, Natural Language Processing (NLP), Speech Recognition, and Recommendation Systems.</p>
<h2>Machine Learning vs Deep Learning</h2>
<p>The most fundamental difference is in the process flow:</p>
<table>
<thead>
<tr>
<th></th>
<th>Flow</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Machine Learning</strong></td>
<td>Input → Feature Extraction (manual) → Classification (model) → Output</td>
</tr>
<tr>
<td><strong>Deep Learning</strong></td>
<td>Input → Feature Extraction + Classification (automatic, within the network) → Output</td>
</tr>
</tbody></table>
<p>In ML, a human decides which features matter before feeding data into the model. In DL, feature extraction and classification happen together inside the network — which is why DL needs more data, but can also handle far more complex patterns.</p>
<table>
<thead>
<tr>
<th>Factor</th>
<th>Deep Learning</th>
<th>Machine Learning</th>
</tr>
</thead>
<tbody><tr>
<td>Data Requirement</td>
<td>Needs large amounts of data</td>
<td>Can train on less data</td>
</tr>
<tr>
<td>Accuracy</td>
<td>Generally higher</td>
<td>Generally lower</td>
</tr>
<tr>
<td>Training Time</td>
<td>Longer</td>
<td>Shorter</td>
</tr>
<tr>
<td>Hardware Dependency</td>
<td>Needs a GPU for efficient training</td>
<td>Can train fine on CPU</td>
</tr>
<tr>
<td>Hyperparameter Tuning</td>
<td>Many tunable variations</td>
<td>Limited tuning options</td>
</tr>
</tbody></table>
<h2>Artificial Neurons — The Basics</h2>
<p>Artificial neurons are inspired by biological ones: <em>dendrites</em> receive signals, the <em>nucleus</em> processes them, the <em>axon</em> sends signals onward. The math version:</p>
<ul>
<li><p>Each input (x₁, x₂, …, xₙ) is multiplied by its own weight (w).</p>
</li>
<li><p>All the products are summed (Σ), then a bias (b) is added.</p>
</li>
<li><p>The result passes through an <em>activation function</em> φ(.) to produce the output (y).</p>
</li>
</ul>
<pre><code class="language-plaintext">Z = W · X + b  →  Activation Function φ(Z)
</code></pre>
<p>🤔 Guess First: is <code>ReLU</code> typically used in the hidden layer or the output layer?</p>
<p><strong>Answer:</strong> the hidden layer. ReLU (<code>max(0, x)</code>) is the standard activation between hidden layers because it's computationally cheap and helps the network learn non-linear patterns. For classification <em>outputs</em>, what's typically used is <strong>Softmax</strong> (multi-class, one label per sample) or <strong>Sigmoid</strong> per class (multi-label, multiple labels active at once) — not ReLU.</p>
<h2>Neural Network Architecture</h2>
<p>A Deep Neural Network is built from three types of layers:</p>
<ol>
<li><p><strong>Input Layer</strong> — receives the raw data as the initial input.</p>
</li>
<li><p><strong>Hidden Layers</strong> — one or more hidden layers where complex patterns get learned; the more/deeper the layers, the more "deep" the network is.</p>
</li>
<li><p><strong>Output Layer</strong> — produces the final result (prediction/classification).</p>
</li>
</ol>
<p>Typically, every neuron in one layer connects to every neuron in the next (<em>fully connected</em>).</p>
<h2>Why PyTorch?</h2>
<p>A few reasons PyTorch is a solid choice for learning deep learning:</p>
<ul>
<li><p><strong>Pythonic</strong> — the syntax feels natural and is easy for beginners to pick up.</p>
</li>
<li><p>Widely used in both industry and academic research.</p>
</li>
<li><p>Dynamic by design (<em>define-by-run</em>), which makes debugging much easier than with a static graph.</p>
</li>
<li><p>Backed by a large open-source community, with a strong ecosystem: Torchvision, Torchtext, PyTorch Lightning, Torchserve.</p>
</li>
</ul>
<p>The standard workflow for a Deep Learning project with PyTorch usually looks like this:</p>
<p><code>Load Dataset → Preprocessing → Build Model → Loss Function &amp; Optimizer → Training Loop → Evaluation → Inference/Deployment</code></p>
<p>Let's put this workflow into practice.</p>
<hr />
<h2>Hands-On: Digit Classification with PyTorch</h2>
<p>We'll build a simple neural network (<code>SimpleNN</code>) to recognize handwritten digits 0–9 from the <strong>MNIST</strong> dataset — the full pipeline, from loading data to a model ready for prediction.</p>
<h3>1. Setup &amp; Imports</h3>
<pre><code class="language-python">import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms, models

print('PyTorch version:', torch.__version__)

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print('Using device:', device)

# Set seed for reproducibility
SEED = 42
torch.manual_seed(SEED)
if torch.cuda.is_available():
    torch.cuda.manual_seed_all(SEED)
</code></pre>
<blockquote>
<p>💡 <code>torch.manual_seed()</code> matters for reproducibility — without it, weight initialization and data shuffling are random, so results will vary slightly every time the notebook re-runs.</p>
</blockquote>
<h3>2. Load Dataset &amp; DataLoader</h3>
<pre><code class="language-python">transform = transforms.ToTensor()

train_dataset = datasets.MNIST(root='data', train=True, transform=transform, download=True)
test_dataset  = datasets.MNIST(root='data', train=False, transform=transform, download=True)

train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader  = DataLoader(test_dataset, batch_size=64, shuffle=False)

len(train_dataset), len(test_dataset)
</code></pre>
<pre><code class="language-plaintext">(60000, 10000)
</code></pre>
<p>The data is split into <strong>train</strong> (60,000 images) and <strong>test</strong> (10,000 images) right away — so evaluation later genuinely measures how well the model generalizes to data it's never seen, instead of data it's already memorized during training.</p>
<h3>3. Looking at Sample Data</h3>
<pre><code class="language-python">import matplotlib.pyplot as plt

images, labels = next(iter(train_loader))
print('Batch shape:', images.shape)
print('Labels:', labels[:10])

plt.figure()
plt.imshow(images[0].squeeze(), cmap='gray')
plt.title(f'Label: {labels[0].item()}')
plt.axis('off')
plt.show()
</code></pre>
<img src="https://raw.githubusercontent.com/arielshakaramiro/mnist-pytorch-classifier-arielshakaramiro/main/assets/sample-data-preview.png" alt="Sample MNIST data" style="display:block;margin:0 auto" />

<p>Each image is 28×28 pixels, grayscale (1 channel), labeled with a digit from 0 to 9.</p>
<h3>4. Building a Simple Neural Network</h3>
<pre><code class="language-python">class SimpleNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Sequential(
            nn.Flatten(), # out: 784 (28 * 28)
            nn.Linear(28 * 28, 128),
            nn.ReLU(),
            nn.Linear(128, 10)
        )

    def forward(self, x):
        return self.fc(x)

model = SimpleNN().to(device)
model
</code></pre>
<pre><code class="language-plaintext">SimpleNN(
  (fc): Sequential(
    (0): Flatten(start_dim=1, end_dim=-1)
    (1): Linear(in_features=784, out_features=128, bias=True)
    (2): ReLU()
    (3): Linear(in_features=128, out_features=10, bias=True)
  )
)
</code></pre>
<p>The flow: <code>Flatten</code> turns the 28×28 image into a 784-length vector → the first <code>Linear</code> layer compresses it into 128 hidden features → <code>ReLU</code> adds non-linearity → the second <code>Linear</code> layer produces 10 outputs (one per digit, 0–9).</p>
<p>Notice the model does <strong>not</strong> end with a <code>Softmax</code> layer. That's intentional — <code>nn.CrossEntropyLoss()</code>, used in the next step, already applies log-softmax internally. Adding a manual Softmax here would apply that activation twice, which would mess up training.</p>
<h3>5. Loss Function &amp; Optimizer</h3>
<pre><code class="language-python">criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
</code></pre>
<h3>6. Training Loop</h3>
<pre><code class="language-python">def train(model, loader, criterion, optimizer, device):
    model.train()
    running_loss = 0.0
    for images, labels in loader:
        images, labels = images.to(device), labels.to(device)

        outputs = model(images)
        loss = criterion(outputs, labels)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        running_loss += loss.item() * images.size(0)

    return running_loss / len(loader.dataset)

num_epochs = 10
for epoch in range(num_epochs):
    loss = train(model, train_loader, criterion, optimizer, device)
    print(f'Epoch [{epoch+1}/{num_epochs}] - Loss: {loss:.4f}')
</code></pre>
<pre><code class="language-plaintext">Epoch [1/10] - Loss: 0.3421
Epoch [2/10] - Loss: 0.1532
Epoch [3/10] - Loss: 0.1044
Epoch [4/10] - Loss: 0.0789
Epoch [5/10] - Loss: 0.0630
Epoch [6/10] - Loss: 0.0508
Epoch [7/10] - Loss: 0.0417
Epoch [8/10] - Loss: 0.0337
Epoch [9/10] - Loss: 0.0284
Epoch [10/10] - Loss: 0.0238
</code></pre>
<img src="https://raw.githubusercontent.com/arielshakaramiro/mnist-pytorch-classifier-arielshakaramiro/main/assets/training-loss.png" alt="Training loss curve" style="display:block;margin:0 auto" />

<p>Loss drops steadily from 0.3421 in the first epoch to 0.0238 in the last — a good sign the model is genuinely learning, not stagnating or getting worse.</p>
<blockquote>
<p><strong>A technical note:</strong> <code>running_loss += loss.item() * images.size(0)</code>, divided by <code>len(loader.dataset)</code> at the end, gives the <em>average</em> loss over a full epoch — not just the loss from the last batch. Printing <code>loss.item()</code> at the end of the loop alone would only reflect the very last batch, which can be misleading as a progress indicator.</p>
</blockquote>
<h3>7. Evaluating Accuracy on Test Data</h3>
<pre><code class="language-python">def evaluate(model, loader, device):
    model.eval()
    correct, total = 0, 0
    with torch.no_grad():
        for images, labels in loader:
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            _, preds = outputs.max(1)
            total += labels.size(0)
            correct += (preds == labels).sum().item()
    return correct / total

test_acc = evaluate(model, test_loader, device)
print(f'Test accuracy: {test_acc:.4f}')
</code></pre>
<p>🤔 Guess First: how accurate do you think this simple model (just 1 hidden layer, 10 epochs) is on test data?</p>
<p><strong>Answer: 97.86%.</strong> Pretty impressive for such a simple architecture — it shows MNIST is a relatively "easy" benchmark dataset, and shows a basic fully-connected network is already strong enough for image classification tasks that aren't too complex (at least for this one experiment — not necessarily true across every dataset).</p>
<p><code>model.eval()</code> and <code>torch.no_grad()</code> matter here: <code>eval()</code> switches the model's mode (relevant if there were layers like Dropout/BatchNorm), and <code>no_grad()</code> disables gradient computation since evaluation doesn't need <code>backward()</code> — making the process faster and more memory-efficient. One thing to watch closely: <code>loader</code> here must be <code>test_loader</code>, not <code>train_loader</code> — evaluating on the same data used for training would produce a biased accuracy that doesn't reflect the model's actual ability to generalize.</p>
<h3>8. Inference on a Single Image</h3>
<pre><code class="language-python">model.eval()
image, label = test_dataset[0]
plt.figure()
plt.imshow(image.squeeze(), cmap='gray')
plt.title(f'Ground truth: {label}')
plt.axis('off')
plt.show()

with torch.no_grad():
    x = image.unsqueeze(0).to(device)
    output = model(x)
    pred = output.argmax(1).item()

print('Model prediction:', pred)
</code></pre>
<img src="https://raw.githubusercontent.com/arielshakaramiro/mnist-pytorch-classifier-arielshakaramiro/main/assets/inference-example.png" alt="Inference result" style="display:block;margin:0 auto" />

<table>
<thead>
<tr>
<th></th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td>Ground truth</td>
<td>7</td>
</tr>
<tr>
<td>Model prediction</td>
<td>7</td>
</tr>
<tr>
<td>Status</td>
<td>✅ Correct</td>
</tr>
</tbody></table>
<p><code>image.unsqueeze(0)</code> adds a batch dimension up front (turning <code>[1, 28, 28]</code> into <code>[1, 1, 28, 28]</code>) — because the model expects data in batch form, even when we only want a prediction for a single image.</p>
<h3>9. Saving &amp; Reloading the Model</h3>
<p>There are two ways to save a model in PyTorch:</p>
<pre><code class="language-python"># Option 1: save only the model weights (most common, recommended)
torch.save(model.state_dict(), "model_mnist.pth")

# Option 2: save the entire model object (rarely used, but still possible)
torch.save(model, "full_model_mnist.pth")
</code></pre>
<p>To reload the model from saved weights:</p>
<pre><code class="language-python">model = SimpleNN().to(device)
model.load_state_dict(torch.load("model_mnist.pth", map_location=device))
model.eval()
</code></pre>
<blockquote>
<p>💡 <code>map_location=device</code> matters if you save a model from a GPU machine and want to load it on a machine without a GPU (or vice versa) — without it, <code>torch.load()</code> can throw an error trying to map tensors to a device that isn't available.</p>
</blockquote>
<hr />
<h2>Recap Checklist</h2>
<ul>
<li><p>[ ] Understand the difference between ML (manual feature extraction) and DL (automatic)</p>
</li>
<li><p>[ ] Can explain the basic neuron formula: <code>Z = W·X + b → φ(Z)</code></p>
</li>
<li><p>[ ] Know the three layer types: input, hidden, output</p>
</li>
<li><p>[ ] Understand why ReLU belongs in hidden layers, not the output</p>
</li>
<li><p>[ ] Can lay out the PyTorch workflow: dataset → model → loss/optimizer → training loop → evaluation → inference</p>
</li>
<li><p>[ ] Understand why <code>model.eval()</code> + <code>torch.no_grad()</code> are used during evaluation/inference</p>
</li>
<li><p>[ ] Know why evaluation should use <code>test_loader</code>, not <code>train_loader</code></p>
</li>
<li><p>[ ] Can save and reload a model with <code>state_dict()</code></p>
</li>
</ul>
<h2>Mini Quiz</h2>
<p>1. Why doesn't the <code>SimpleNN</code> model above end with a <code>Softmax</code> layer?</p>
<p>Because <code>nn.CrossEntropyLoss()</code> in PyTorch already applies log-softmax internally. Adding a manual Softmax to the model would apply that activation twice, which disrupts training.</p>
<p>2. What happens if you evaluate the model using <code>train_loader</code> instead of <code>test_loader</code>?</p>
<p>The resulting accuracy would be biased — reflecting how well the model "memorized" the training data, rather than how well it generalizes to new, unseen data.</p>
<p>3. What does <code>torch.no_grad()</code> do during evaluation?</p>
<p>It disables gradient computation, since evaluation/inference never calls <code>backward()</code>. This makes the process faster and more memory-efficient than keeping gradient tracking on.</p>
<h2>Further Exercises</h2>
<p>To explore further from this notebook:</p>
<ol>
<li><p>Change the hidden layer size from 128 to 256 or 64 — observe the effect on accuracy.</p>
</li>
<li><p>Add a new fully-connected layer (3 layers instead of 2).</p>
</li>
<li><p>Swap the optimizer from Adam to SGD, and compare convergence speed.</p>
</li>
<li><p>Add <code>Dropout</code> to the model and observe its effect on overfitting.</p>
</li>
</ol>
<h2>Closing Thoughts</h2>
<p>The thing that stuck with me most from this material: deep learning isn't magic — it's just a simple neuron (<code>Z = W·X + b</code>) stacked in layers, trained through a highly structured trial-and-error process (forward pass → compute loss → backward pass → update weights). What makes it powerful is the scale and repetition, not the complexity of any single neuron.</p>
<p>The full, end-to-end verified notebook is available on <a href="https://github.com/arielshakaramiro/mnist-pytorch-classifier-arielshakaramiro">this GitHub repo</a>.</p>
<hr />
<p><em>Part of my AI Engineering learning notes series —</em> <a href="https://shaka-ai.hashnode.dev"><em>AI Notes &amp; Engineering</em></a></p>
]]></content:encoded></item><item><title><![CDATA[Deep Learning & PyTorch: Teori Neural Network + Praktik Klasifikasi MNIST]]></title><description><![CDATA[Sebelum masuk ke materi ini, bayangan saya soal deep learning itu sederhana: kasih data, keluar prediksi, selesai. Ternyata di balik itu ada proses yang jauh lebih mendasar — neuron buatan yang belaja]]></description><link>https://shaka-ai.hashnode.dev/deep-learning-pytorch-mnist-neural-network-dasar</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/deep-learning-pytorch-mnist-neural-network-dasar</guid><category><![CDATA[DeepLearning]]></category><category><![CDATA[Deep Learning]]></category><category><![CDATA[pytorch]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[neural networks]]></category><category><![CDATA[mnist]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Sat, 05 Sep 2026 05:57:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/0bdfd98e-2816-4a0d-b9df-450fa0e21b81.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Sebelum masuk ke materi ini, bayangan saya soal <em>deep learning</em> itu sederhana: kasih data, keluar prediksi, selesai. Ternyata di balik itu ada proses yang jauh lebih mendasar — neuron buatan yang belajar dari nol, salah berkali-kali lewat <em>backpropagation</em>, sampai akhirnya bisa mengenali pola. Tulisan ini catatan belajar saya soal itu: dari konsep dasar neural network sampai praktik langsung melatih model PyTorch untuk mengenali angka tulisan tangan (MNIST).</p>
<h2>Daftar Isi</h2>
<ul>
<li><p><a href="#apa-itu-deep-learning">Apa Itu Deep Learning?</a></p>
</li>
<li><p><a href="#machine-learning-vs-deep-learning">Machine Learning vs Deep Learning</a></p>
</li>
<li><p><a href="#neuron-buatan--konsep-dasar">Neuron Buatan — Konsep Dasar</a></p>
</li>
<li><p><a href="#arsitektur-neural-network">Arsitektur Neural Network</a></p>
</li>
<li><p><a href="#kenapa-pytorch">Kenapa PyTorch?</a></p>
</li>
<li><p><a href="#praktik-klasifikasi-angka-dengan-pytorch">Praktik: Klasifikasi Angka dengan PyTorch</a></p>
</li>
<li><p><a href="#checklist-recap">Checklist Recap</a></p>
</li>
<li><p><a href="#kuis-mini">Kuis Mini</a></p>
</li>
<li><p><a href="#latihan-lanjutan">Latihan Lanjutan</a></p>
</li>
<li><p><a href="#penutup">Penutup</a></p>
</li>
</ul>
<hr />
<h2>Apa Itu Deep Learning?</h2>
<p>Deep Learning adalah subbidang Machine Learning yang menggunakan <em>Artificial Neural Network</em> (ANN) dengan banyak layer (<em>multi-layer</em>).</p>
<p>Dua hal yang membedakannya dari ML konvensional:</p>
<ul>
<li><p>Mampu mempelajari pola kompleks dari data besar &amp; tidak terstruktur.</p>
</li>
<li><p>Representasi fitur terjadi secara otomatis — fitur tidak perlu dibuat/dipilih manual seperti pada Machine Learning konvensional.</p>
</li>
</ul>
<p>Deep Learning banyak dipakai di Computer Vision, Natural Language Processing (NLP), Speech Recognition, dan Recommendation System.</p>
<h2>Machine Learning vs Deep Learning</h2>
<p>Perbedaan paling mendasar ada di alur prosesnya:</p>
<table>
<thead>
<tr>
<th></th>
<th>Alur</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Machine Learning</strong></td>
<td>Input → Feature Extraction (manual) → Classification (model) → Output</td>
</tr>
<tr>
<td><strong>Deep Learning</strong></td>
<td>Input → Feature Extraction + Classification (otomatis oleh network) → Output</td>
</tr>
</tbody></table>
<p>Pada ML, manusia yang menentukan fitur apa yang penting sebelum masuk ke model. Pada DL, proses ekstraksi fitur dan klasifikasi terjadi sekaligus di dalam network — itu sebabnya DL butuh data lebih banyak, tapi bisa menangani pola yang jauh lebih kompleks.</p>
<table>
<thead>
<tr>
<th>Faktor</th>
<th>Deep Learning</th>
<th>Machine Learning</th>
</tr>
</thead>
<tbody><tr>
<td>Data Requirement</td>
<td>Butuh data dalam jumlah besar</td>
<td>Bisa dilatih dengan data lebih sedikit</td>
</tr>
<tr>
<td>Accuracy</td>
<td>Akurasi tinggi</td>
<td>Akurasi cenderung lebih rendah</td>
</tr>
<tr>
<td>Training Time</td>
<td>Waktu training lebih lama</td>
<td>Waktu training lebih singkat</td>
</tr>
<tr>
<td>Hardware Dependency</td>
<td>Butuh GPU agar training optimal</td>
<td>Cukup dilatih di CPU</td>
</tr>
<tr>
<td>Hyperparameter Tuning</td>
<td>Bisa di-tuning dengan berbagai variasi</td>
<td>Kemampuan tuning terbatas</td>
</tr>
</tbody></table>
<h2>Neuron Buatan — Konsep Dasar</h2>
<p>Neuron buatan terinspirasi dari neuron biologis: <em>dendrites</em> menerima sinyal, <em>nucleus</em> mengolahnya, <em>axon</em> mengirim sinyal keluar. Versi matematisnya:</p>
<ul>
<li><p>Setiap input (x₁, x₂, …, xₙ) dikalikan dengan bobotnya masing-masing (w).</p>
</li>
<li><p>Seluruh hasil perkalian dijumlahkan (Σ), lalu ditambah bias (b).</p>
</li>
<li><p>Hasilnya diproses lewat <em>activation function</em> φ(.) untuk menghasilkan output (y).</p>
</li>
</ul>
<pre><code class="language-plaintext">Z = W · X + b  →  Activation Function φ(Z)
</code></pre>
<p>🤔 Coba Tebak Dulu: fungsi aktivasi <code>ReLU</code> itu biasanya dipasang di layer mana — hidden layer atau output layer?</p>
<p><strong>Jawaban:</strong> hidden layer. ReLU (<code>max(0, x)</code>) adalah aktivasi standar di antara layer tersembunyi karena murah secara komputasi dan membantu network belajar pola non-linear. Untuk <em>output</em> klasifikasi, yang biasa dipakai justru <strong>Softmax</strong> (multi-class, satu label per data) atau <strong>Sigmoid</strong> per kelas (multi-label, beberapa label aktif sekaligus) — bukan ReLU.</p>
<h2>Arsitektur Neural Network</h2>
<p>Sebuah Deep Neural Network tersusun atas tiga jenis layer:</p>
<ol>
<li><p><strong>Input Layer</strong> — menerima data mentah sebagai masukan awal.</p>
</li>
<li><p><strong>Hidden Layers</strong> — satu atau lebih layer tersembunyi tempat pola-pola kompleks dipelajari; semakin banyak/dalam layer, semakin "deep" network tersebut.</p>
</li>
<li><p><strong>Output Layer</strong> — menghasilkan hasil akhir (prediksi/klasifikasi).</p>
</li>
</ol>
<p>Umumnya setiap neuron di satu layer terhubung ke seluruh neuron di layer berikutnya (<em>fully connected</em>).</p>
<h2>Kenapa PyTorch?</h2>
<p>Beberapa alasan PyTorch jadi framework pilihan untuk belajar deep learning:</p>
<ul>
<li><p><strong>Pythonic</strong> — sintaksnya natural, mudah dipahami pemula.</p>
</li>
<li><p>Banyak dipakai di industri maupun riset akademik.</p>
</li>
<li><p>Bersifat dinamis (<em>define-by-run</em>), jadi proses debugging lebih mudah dibanding graph statis.</p>
</li>
<li><p>Didukung komunitas open source yang besar, dengan ekosistem kuat: Torchvision, Torchtext, PyTorch Lightning, Torchserve.</p>
</li>
</ul>
<p>Alur kerja standar sebuah proyek Deep Learning dengan PyTorch biasanya begini:</p>
<p><code>Load Dataset → Preprocessing → Buat Model → Loss Function &amp; Optimizer → Training Loop → Evaluation → Inference/Deployment</code></p>
<p>Sekarang kita praktikkan alur ini langsung.</p>
<hr />
<h2>Praktik: Klasifikasi Angka dengan PyTorch</h2>
<p>Kita akan membangun neural network sederhana (<code>SimpleNN</code>) untuk mengenali angka tulisan tangan 0–9 dari dataset <strong>MNIST</strong>, lengkap dari load data sampai model bisa dipakai untuk prediksi.</p>
<h3>1. Setup &amp; Import Library</h3>
<pre><code class="language-python">import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms, models

print('PyTorch version:', torch.__version__)

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print('Using device:', device)

# Set seed untuk reproducibility
SEED = 42
torch.manual_seed(SEED)
if torch.cuda.is_available():
    torch.cuda.manual_seed_all(SEED)
</code></pre>
<blockquote>
<p>💡 <code>torch.manual_seed()</code> penting supaya hasil training bisa direproduksi — tanpa ini, tiap kali notebook dijalankan ulang, hasilnya bisa sedikit berbeda karena inisialisasi bobot dan urutan shuffle data bersifat acak.</p>
</blockquote>
<h3>2. Load Dataset &amp; DataLoader</h3>
<pre><code class="language-python">transform = transforms.ToTensor()

train_dataset = datasets.MNIST(root='data', train=True, transform=transform, download=True)
test_dataset  = datasets.MNIST(root='data', train=False, transform=transform, download=True)

train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader  = DataLoader(test_dataset, batch_size=64, shuffle=False)

len(train_dataset), len(test_dataset)
</code></pre>
<pre><code class="language-plaintext">(60000, 10000)
</code></pre>
<p>Data langsung dipisah jadi <strong>train</strong> (60.000 gambar) dan <strong>test</strong> (10.000 gambar) sejak awal — supaya nanti evaluasi benar-benar mengukur kemampuan model pada data yang belum pernah dilihat, bukan data yang sudah dihafal saat training.</p>
<h3>3. Melihat Contoh Data</h3>
<pre><code class="language-python">import matplotlib.pyplot as plt

images, labels = next(iter(train_loader))
print('Batch shape:', images.shape)
print('Labels:', labels[:10])

plt.figure()
plt.imshow(images[0].squeeze(), cmap='gray')
plt.title(f'Label: {labels[0].item()}')
plt.axis('off')
plt.show()
</code></pre>
<img src="https://raw.githubusercontent.com/arielshakaramiro/mnist-pytorch-classifier-arielshakaramiro/main/assets/sample-data-preview.png" alt="Contoh data MNIST" style="display:block;margin:0 auto" />

<p>Setiap gambar berukuran 28×28 piksel, grayscale (1 channel), dengan label angka 0–9.</p>
<h3>4. Membuat Model Neural Network Sederhana</h3>
<pre><code class="language-python">class SimpleNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Sequential(
            nn.Flatten(), # out: 784 (28 * 28)
            nn.Linear(28 * 28, 128),
            nn.ReLU(),
            nn.Linear(128, 10)
        )

    def forward(self, x):
        return self.fc(x)

model = SimpleNN().to(device)
model
</code></pre>
<pre><code class="language-plaintext">SimpleNN(
  (fc): Sequential(
    (0): Flatten(start_dim=1, end_dim=-1)
    (1): Linear(in_features=784, out_features=128, bias=True)
    (2): ReLU()
    (3): Linear(in_features=128, out_features=10, bias=True)
  )
)
</code></pre>
<p>Alurnya: <code>Flatten</code> meratakan gambar 28×28 jadi vektor 784 → <code>Linear</code> pertama memampatkannya jadi 128 fitur tersembunyi → <code>ReLU</code> menambahkan non-linearitas → <code>Linear</code> kedua menghasilkan 10 output (satu untuk tiap digit 0–9).</p>
<p>Perhatikan: model ini <strong>tidak</strong> diakhiri dengan <code>Softmax</code>. Ini disengaja — <code>nn.CrossEntropyLoss()</code> di langkah berikutnya sudah menerapkan log-softmax secara internal, jadi menambahkan Softmax manual di sini justru bikin aktivasi diterapkan dua kali.</p>
<h3>5. Menentukan Loss Function &amp; Optimizer</h3>
<pre><code class="language-python">criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
</code></pre>
<h3>6. Training Loop</h3>
<pre><code class="language-python">def train(model, loader, criterion, optimizer, device):
    model.train()
    running_loss = 0.0
    for images, labels in loader:
        images, labels = images.to(device), labels.to(device)

        outputs = model(images)
        loss = criterion(outputs, labels)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        running_loss += loss.item() * images.size(0)

    return running_loss / len(loader.dataset)

num_epochs = 10
for epoch in range(num_epochs):
    loss = train(model, train_loader, criterion, optimizer, device)
    print(f'Epoch [{epoch+1}/{num_epochs}] - Loss: {loss:.4f}')
</code></pre>
<pre><code class="language-plaintext">Epoch [1/10] - Loss: 0.3421
Epoch [2/10] - Loss: 0.1532
Epoch [3/10] - Loss: 0.1044
Epoch [4/10] - Loss: 0.0789
Epoch [5/10] - Loss: 0.0630
Epoch [6/10] - Loss: 0.0508
Epoch [7/10] - Loss: 0.0417
Epoch [8/10] - Loss: 0.0337
Epoch [9/10] - Loss: 0.0284
Epoch [10/10] - Loss: 0.0238
</code></pre>
<img src="https://raw.githubusercontent.com/arielshakaramiro/mnist-pytorch-classifier-arielshakaramiro/main/assets/training-loss.png" alt="Training loss curve" style="display:block;margin:0 auto" />

<p>Loss turun konsisten dari 0.3421 di epoch pertama ke 0.0238 di epoch terakhir — tanda model benar-benar belajar, bukan stagnan atau malah memburuk.</p>
<blockquote>
<p><strong>Catatan teknis:</strong> <code>running_loss += loss.item() * images.size(0)</code> lalu dibagi <code>len(loader.dataset)</code> di akhir adalah cara menghitung <em>rata-rata</em> loss satu epoch penuh — bukan cuma loss dari batch terakhir. Kalau cuma <code>print(loss.item())</code> di akhir loop, angka yang muncul cuma mewakili batch paling akhir, yang bisa menyesatkan kalau dijadikan indikator progres training.</p>
</blockquote>
<h3>7. Evaluasi Akurasi di Data Test</h3>
<pre><code class="language-python">def evaluate(model, loader, device):
    model.eval()
    correct, total = 0, 0
    with torch.no_grad():
        for images, labels in loader:
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            _, preds = outputs.max(1)
            total += labels.size(0)
            correct += (preds == labels).sum().item()
    return correct / total

test_acc = evaluate(model, test_loader, device)
print(f'Akurasi di data test: {test_acc:.4f}')
</code></pre>
<p>🤔 Coba Tebak Dulu: menurutmu, kira-kira berapa akurasi model sesederhana ini (cuma 1 hidden layer, 10 epoch) di data test?</p>
<p><strong>Jawaban:</strong> <strong>97.86%</strong>. Cukup mengejutkan untuk arsitektur yang sangat sederhana — ini menunjukkan MNIST memang relatif "mudah" sebagai dataset benchmark, dan menunjukkan fully-connected network dasar sudah cukup kuat untuk kasus klasifikasi gambar yang tidak terlalu kompleks (setidaknya untuk satu eksperimen ini — belum tentu berlaku sama di dataset lain).</p>
<p><code>model.eval()</code> dan <code>torch.no_grad()</code> di sini penting: <code>eval()</code> mengubah mode model (relevan kalau ada layer seperti Dropout/BatchNorm), dan <code>no_grad()</code> menonaktifkan perhitungan gradien karena saat evaluasi kita tidak perlu <code>backward()</code> — hasilnya proses jadi lebih cepat dan hemat memori. Satu hal yang wajib diperhatikan: <code>loader</code> di sini harus <code>test_loader</code>, bukan <code>train_loader</code> — kalau evaluasi dilakukan di data yang sama dengan training, angka akurasinya jadi bias dan tidak mencerminkan kemampuan generalisasi model.</p>
<h3>8. Inference pada Satu Gambar</h3>
<pre><code class="language-python">model.eval()
image, label = test_dataset[0]
plt.figure()
plt.imshow(image.squeeze(), cmap='gray')
plt.title(f'Ground truth: {label}')
plt.axis('off')
plt.show()

with torch.no_grad():
    x = image.unsqueeze(0).to(device)
    output = model(x)
    pred = output.argmax(1).item()

print('Prediksi model:', pred)
</code></pre>
<img src="https://raw.githubusercontent.com/arielshakaramiro/mnist-pytorch-classifier-arielshakaramiro/main/assets/inference-example.png" alt="Hasil inference" style="display:block;margin:0 auto" />

<table>
<thead>
<tr>
<th></th>
<th>Nilai</th>
</tr>
</thead>
<tbody><tr>
<td>Ground truth</td>
<td>7</td>
</tr>
<tr>
<td>Prediksi model</td>
<td>7</td>
</tr>
<tr>
<td>Status</td>
<td>✅ Benar</td>
</tr>
</tbody></table>
<p><code>image.unsqueeze(0)</code> menambahkan dimensi batch di depan (dari <code>[1, 28, 28]</code> jadi <code>[1, 1, 28, 28]</code>) — karena model dilatih untuk menerima data dalam bentuk batch, walaupun kita cuma mau prediksi satu gambar.</p>
<h3>9. Menyimpan &amp; Memuat Ulang Model</h3>
<p>Ada dua cara menyimpan model di PyTorch:</p>
<pre><code class="language-python"># Cara 1: simpan hanya bobot model (paling umum &amp; direkomendasikan)
torch.save(model.state_dict(), "model_mnist.pth")

# Cara 2: simpan seluruh objek model (jarang dipakai, tapi tetap bisa)
torch.save(model, "full_model_mnist.pth")
</code></pre>
<p>Untuk memuat ulang model dari bobot yang disimpan:</p>
<pre><code class="language-python">model = SimpleNN().to(device)
model.load_state_dict(torch.load("model_mnist.pth", map_location=device))
model.eval()
</code></pre>
<blockquote>
<p>💡 <code>map_location=device</code> penting kalau kamu menyimpan model dari mesin dengan GPU lalu ingin memuatnya di mesin tanpa GPU (atau sebaliknya) — tanpa ini, <code>torch.load()</code> bisa melempar error karena mencoba memuat tensor ke device yang tidak tersedia.</p>
</blockquote>
<hr />
<h2>Checklist Recap</h2>
<ul>
<li><p>[ ] Paham beda alur ML (manual feature extraction) vs DL (otomatis)</p>
</li>
<li><p>[ ] Bisa jelaskan rumus dasar satu neuron: <code>Z = W·X + b → φ(Z)</code></p>
</li>
<li><p>[ ] Tahu tiga jenis layer: input, hidden, output</p>
</li>
<li><p>[ ] Paham kenapa ReLU dipakai di hidden layer, bukan output</p>
</li>
<li><p>[ ] Bisa susun workflow PyTorch: dataset → model → loss/optimizer → training loop → evaluasi → inference</p>
</li>
<li><p>[ ] Ngerti kenapa <code>model.eval()</code> + <code>torch.no_grad()</code> dipakai saat evaluasi/inference</p>
</li>
<li><p>[ ] Tahu kenapa evaluasi harus pakai <code>test_loader</code>, bukan <code>train_loader</code></p>
</li>
<li><p>[ ] Bisa menyimpan &amp; memuat ulang model dengan <code>state_dict()</code></p>
</li>
</ul>
<h2>Kuis Mini</h2>
<p>1. Kenapa model <code>SimpleNN</code> di atas tidak diberi layer <code>Softmax</code> di akhir?</p>
<p>Karena <code>nn.CrossEntropyLoss()</code> di PyTorch sudah menerapkan log-softmax secara internal. Menambahkan Softmax manual di model akan membuat aktivasi tersebut diterapkan dua kali, yang justru mengacaukan proses training.</p>
<p>2. Apa akibatnya kalau evaluasi model dilakukan memakai <code>train_loader</code> alih-alih <code>test_loader</code>?</p>
<p>Angka akurasi yang dihasilkan akan bias — mencerminkan seberapa baik model "menghafal" data training, bukan seberapa baik model bisa generalisasi ke data baru yang belum pernah dilihat.</p>
<p>3. Fungsi <code>torch.no_grad()</code> saat evaluasi itu untuk apa?</p>
<p>Menonaktifkan perhitungan gradien, karena saat evaluasi/inference kita tidak melakukan <code>backward()</code>. Ini membuat proses lebih cepat dan hemat memori dibanding kalau gradien tetap dihitung.</p>
<h2>Latihan Lanjutan</h2>
<p>Kalau mau eksplorasi lebih jauh dari notebook ini:</p>
<ol>
<li><p>Ubah ukuran hidden layer dari 128 menjadi 256 atau 64 — amati pengaruhnya ke akurasi.</p>
</li>
<li><p>Tambahkan layer fully-connected baru (jadi 3 layer, bukan 2).</p>
</li>
<li><p>Ganti optimizer dari Adam ke SGD, bandingkan kecepatan konvergensinya.</p>
</li>
<li><p>Tambahkan <code>Dropout</code> di model untuk melihat efeknya terhadap overfitting.</p>
</li>
</ol>
<h2>Penutup</h2>
<p>Dari materi ini, hal yang paling nempel buat saya adalah: deep learning itu bukan sihir — cuma neuron sederhana (<code>Z = W·X + b</code>) yang disusun berlapis-lapis, dilatih lewat proses trial-and-error yang sangat terstruktur (forward pass → hitung loss → backward pass → update bobot). Yang bikin powerful adalah skala dan pengulangannya, bukan kerumitan satu neuronnya.</p>
<p>Notebook lengkap (sudah diverifikasi end-to-end) bisa dicek di <a href="https://github.com/arielshakaramiro/mnist-pytorch-classifier-arielshakaramiro">GitHub repo ini</a>.</p>
<hr />
<p><em>Bagian dari seri catatan belajar AI Engineering saya —</em> <a href="https://shaka-ai.hashnode.dev"><em>AI Notes &amp; Engineering</em></a></p>
]]></content:encoded></item><item><title><![CDATA[Neural Network From Scratch: Learning Deep Learning with MNIST & Fashion-MNIST]]></title><description><![CDATA[Before I started learning machine learning, I pictured AI as some kind of magic box that already "knew" the answer. Deep learning flipped that picture completely — AI starts out knowing nothing at all]]></description><link>https://shaka-ai.hashnode.dev/neural-network-from-scratch-mnist-fashion-mnist</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/neural-network-from-scratch-mnist-fashion-mnist</guid><category><![CDATA[DeepLearning]]></category><category><![CDATA[Deep Learning]]></category><category><![CDATA[TensorFlow]]></category><category><![CDATA[keras]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[MachineLearning]]></category><category><![CDATA[Python]]></category><category><![CDATA[neural networks]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Fri, 04 Sep 2026 08:29:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/415fdeb5-e1a2-47a5-9b97-c8e89004de65.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Before I started learning machine learning, I pictured AI as some kind of magic box that already "knew" the answer. Deep learning flipped that picture completely — AI starts out knowing nothing at all. It learns from zero, gets things wrong over and over, and slowly gets better at spotting patterns. This is my write-up of how that process actually works, from neural network theory to hands-on training an AI to recognize handwritten digits and clothing items using TensorFlow &amp; Keras.</p>
<h2>Table of Contents</h2>
<ul>
<li><p><a href="#what-is-deep-learning">What Is Deep Learning?</a></p>
</li>
<li><p><a href="#biological-neurons-vs-artificial-neurons">Biological Neurons vs Artificial Neurons</a></p>
</li>
<li><p><a href="#why-deep">Why "Deep"?</a></p>
</li>
<li><p><a href="#neural-network-architecture">Neural Network Architecture</a></p>
</li>
<li><p><a href="#activation-functions">Activation Functions</a></p>
</li>
<li><p><a href="#how-ai-learns-the-training-process">How AI Learns (The Training Process)</a></p>
</li>
<li><p><a href="#loss-function--optimizer">Loss Function &amp; Optimizer</a></p>
</li>
<li><p><a href="#machine-learning-vs-deep-learning">Machine Learning vs Deep Learning</a></p>
</li>
<li><p><a href="#real-world-applications">Real-World Applications</a></p>
</li>
<li><p><a href="#practice-building-my-first-neural-network">Practice: Building My First Neural Network</a></p>
</li>
<li><p><a href="#level-up-fashion-mnist">Level Up: Fashion-MNIST</a></p>
</li>
<li><p><a href="#mini-challenge-hyperparameter-experiments">Mini Challenge: Hyperparameter Experiments</a></p>
</li>
<li><p><a href="#common-mistakes-to-avoid">Common Mistakes to Avoid</a></p>
</li>
<li><p><a href="#quiz-test-your-understanding">Quiz: Test Your Understanding</a></p>
</li>
<li><p><a href="#conclusion">Conclusion</a></p>
</li>
</ul>
<hr />
<h2>What Is Deep Learning?</h2>
<p>Deep learning is an AI technique that processes information through many layers — similar to how the human brain has different levels of understanding. Each layer processes the output of the layer before it, so understanding gets deeper step by step: from simple lines, to more complex shapes, to eventually recognizing a whole object.</p>
<p>The core idea is simple: <strong>the more layers, the deeper the understanding a model can build.</strong></p>
<h2>Biological Neurons vs Artificial Neurons</h2>
<p>Neural networks really are inspired by how the human brain works. A biological neuron receives an electrical signal, processes it, then passes it on to other neurons. An artificial neuron mimics the same flow — just with numbers instead of electrical signals.</p>
<table>
<thead>
<tr>
<th>Biological Neuron</th>
<th>Artificial Neuron</th>
</tr>
</thead>
<tbody><tr>
<td>Receives electrical signals</td>
<td>Receives numeric input</td>
</tr>
<tr>
<td>Processes information</td>
<td>Performs a calculation (weight × input + bias)</td>
</tr>
<tr>
<td>Sends output to other neurons</td>
<td>Sends output to the next layer</td>
</tr>
</tbody></table>
<p>The scale is wildly different — the human brain has around 86 billion neurons, while an artificial neural network usually runs anywhere from a few dozen to millions of neurons, depending on how complex the problem is.</p>
<h2>Why "Deep"?</h2>
<p>🤔 Try to Guess First: what do you think separates a 1-layer network from a many-layer network in terms of the kinds of patterns each can recognize?</p>
<p>A 1-layer network can only separate <strong>simple, linear</strong> patterns — picture drawing a single straight line to split two groups of data. A network with many layers can recognize <strong>complex, non-linear</strong> patterns — faces, voices, language, even emotional tone in text. Every extra layer gives the model a new "vocabulary" for describing more complicated patterns.</p>
<p>That's the reason for the word "deep" in deep learning, not just "learning" — depth itself (the number of layers) is what lets a model capture that complexity.</p>
<h2>Neural Network Architecture</h2>
<p>Data flows through three kinds of layers:</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Function</th>
</tr>
</thead>
<tbody><tr>
<td><strong>1. Input Layer</strong></td>
<td>Data enters the system (e.g. image pixels)</td>
</tr>
<tr>
<td><strong>2. Hidden Layers</strong></td>
<td>Hidden layers that process and extract features — can be dozens to hundreds deep</td>
</tr>
<tr>
<td><strong>3. Output Layer</strong></td>
<td>The prediction comes out (e.g. the recognized digit)</td>
</tr>
</tbody></table>
<p>This flow — data in, processed through hidden layers, out as a prediction — is called <strong>forward propagation</strong>.</p>
<p>At the level of a single neuron, here's what happens:</p>
<ol>
<li><p><strong>Input</strong> — receives raw data (image pixels, numbers, etc.)</p>
</li>
<li><p><strong>Processing</strong> — each input is multiplied by its weight, summed, and a bias is added</p>
</li>
<li><p><strong>Activation Function</strong> — decides whether this neuron "fires" or not</p>
</li>
<li><p><strong>Output</strong> — the result is sent to neurons in the next layer</p>
</li>
</ol>
<h2>Activation Functions</h2>
<p>An activation function is basically a smart on/off switch inside each neuron — like a brain deciding whether an idea is worth passing along.</p>
<table>
<thead>
<tr>
<th>Function</th>
<th>Explanation</th>
</tr>
</thead>
<tbody><tr>
<td><strong>ReLU</strong> (Rectified Linear Unit)</td>
<td>One of the most widely used. Outputs 0 for negative input, and the input itself if positive: <code>f(x) = max(0, x)</code></td>
</tr>
<tr>
<td><strong>Sigmoid</strong></td>
<td>Squashes any value into the 0–1 range. Handy for probabilities: <code>f(x) = 1 / (1 + e^-x)</code></td>
</tr>
<tr>
<td><strong>Tanh</strong></td>
<td>Similar to sigmoid, but ranges from -1 to 1, more centered around zero</td>
</tr>
<tr>
<td><strong>Softmax</strong></td>
<td>Used in the output layer for multi-class classification — turns outputs into a probability distribution that sums to 100%</td>
</tr>
</tbody></table>
<h2>How AI Learns (The Training Process)</h2>
<p>Training repeats over and over (each full pass is called an <strong>epoch</strong>) until the model reaches the accuracy you're aiming for:</p>
<table>
<thead>
<tr>
<th>Stage</th>
<th>Explanation</th>
</tr>
</thead>
<tbody><tr>
<td>1. Forward Pass</td>
<td>Input data is processed through the network to produce a prediction</td>
</tr>
<tr>
<td>2. Calculate Loss</td>
<td>Compares the prediction against the actual label to measure how wrong it is</td>
</tr>
<tr>
<td>3. Backpropagation</td>
<td>Computes the error gradient for every weight in the network</td>
</tr>
<tr>
<td>4. Update Weights</td>
<td>Adjusts weights using an optimizer so the error shrinks</td>
</tr>
</tbody></table>
<h2>Loss Function &amp; Optimizer</h2>
<p>A <strong>loss function</strong> measures how far off a prediction is from the actual value — the smaller the loss, the better the model. For a classification task like MNIST, I used <strong>Sparse Categorical Crossentropy</strong> (the <em>sparse</em> version, since the dataset's labels are plain integers 0–9, not one-hot encoded).</p>
<p>To steer the model toward the lowest possible loss, we use an <strong>optimizer</strong>:</p>
<table>
<thead>
<tr>
<th>Optimizer</th>
<th>Explanation</th>
</tr>
</thead>
<tbody><tr>
<td><strong>SGD</strong> (Stochastic Gradient Descent)</td>
<td>The classic optimizer — updates weights based on the gradient. Simple, but can converge slowly</td>
</tr>
<tr>
<td><strong>Adam</strong> (Adaptive Moment Estimation)</td>
<td>One of the most commonly used optimizers — combines momentum with an adaptive learning rate. A solid default for many cases</td>
</tr>
<tr>
<td><strong>RMSprop</strong></td>
<td>Works well for recurrent neural networks, adapting the learning rate per parameter</td>
</tr>
</tbody></table>
<h2>Machine Learning vs Deep Learning</h2>
<table>
<thead>
<tr>
<th></th>
<th>Machine Learning</th>
<th>Deep Learning</th>
</tr>
</thead>
<tbody><tr>
<td>Features</td>
<td>Needs manual feature engineering</td>
<td>Learns features automatically</td>
</tr>
<tr>
<td>Example algorithms</td>
<td>Decision Trees, SVM, Random Forest</td>
<td>CNN, RNN, Transformer</td>
</tr>
<tr>
<td>Expertise needed</td>
<td>Needs an expert for feature extraction</td>
<td>Feature extraction is handled by the network itself</td>
</tr>
<tr>
<td>Ideal data size</td>
<td>Structured, small–medium</td>
<td>Large, complex data (images, audio, text)</td>
</tr>
<tr>
<td>Interpretability</td>
<td>Easier to explain</td>
<td>More powerful, but less interpretable</td>
</tr>
</tbody></table>
<p><strong>Choose Machine Learning when:</strong></p>
<ul>
<li><p>[ ] Your dataset is small (thousands to tens of thousands of rows)</p>
</li>
<li><p>[ ] You need results that are easy to explain</p>
</li>
<li><p>[ ] Compute resources are limited</p>
</li>
<li><p>[ ] The problem is well-defined with clear features</p>
</li>
</ul>
<p><strong>Choose Deep Learning when:</strong></p>
<ul>
<li><p>[ ] Your dataset is very large (hundreds of thousands to millions of rows)</p>
</li>
<li><p>[ ] The data is complex: images, video, audio, text</p>
</li>
<li><p>[ ] You have access to a GPU/TPU for training</p>
</li>
<li><p>[ ] You need the highest accuracy possible</p>
</li>
</ul>
<p>Since the goal here is image recognition (MNIST), the hands-on section below leans on deep learning.</p>
<h2>Real-World Applications</h2>
<p>The same concepts behind MNIST — just with far bigger architectures and far more data — power a lot of real-world use cases:</p>
<table>
<thead>
<tr>
<th>Use Case</th>
<th>Explanation</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Self-driving cars</strong></td>
<td>Deep learning is used to recognize roads, signs, and pedestrians in real time</td>
</tr>
<tr>
<td><strong>Face recognition</strong></td>
<td>Systems like FaceID use neural networks to match a face against millions of possibilities</td>
</tr>
<tr>
<td><strong>Medical imaging analysis</strong></td>
<td>Deep learning models can help detect patterns in medical images like X-rays — though performance varies a lot by study, dataset, and clinical context, and still requires validation from medical professionals rather than replacing a doctor's diagnosis</td>
</tr>
</tbody></table>
<p>From recognizing a simple digit to technology reshaping entire industries, the underlying concepts are the same ones practiced below.</p>
<hr />
<h2>Practice: Building My First Neural Network</h2>
<p>Everything below was run end-to-end in Google Colab — so every number you see here is a real, executed result, not an estimate.</p>
<h3>Step 1 — Load the MNIST Dataset</h3>
<p>MNIST contains 70,000 images of handwritten digits (0–9), each 28×28 pixels — a classic dataset for learning deep learning.</p>
<pre><code class="language-python">from tensorflow import keras

(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/70f44da4-c1bb-448c-a705-3b5265db3f71.png" alt="MNIST dataset samples" style="display:block;margin:0 auto" />

<h3>Step 2 — Build the Model</h3>
<pre><code class="language-python">model = keras.Sequential([
    keras.layers.Flatten(input_shape=(28, 28)),
    keras.layers.Dense(128, activation='relu'),
    keras.layers.Dense(64, activation='relu'),
    keras.layers.Dense(10, activation='softmax')
])
</code></pre>
<ul>
<li><p><strong>Flatten</strong> — turns the 2D image (28×28) into a 1D array (784 elements)</p>
</li>
<li><p><strong>Dense 128</strong> — first hidden layer, 128 neurons, learns basic features</p>
</li>
<li><p><strong>Dense 64</strong> — second hidden layer, 64 neurons, extracts more abstract features</p>
</li>
<li><p><strong>Dense 10</strong> — output layer, one neuron per digit (0–9), softmax for probabilities</p>
</li>
</ul>
<p>Total trainable parameters: <strong>109,386</strong>.</p>
<h3>Step 3 — Compile the Model</h3>
<pre><code class="language-python">model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
</code></pre>
<h3>Step 4 — Train the Model</h3>
<pre><code class="language-python">history = model.fit(x_train, y_train, epochs=5, validation_split=0.2)
</code></pre>
<p>The AI "goes to school" on 48,000 images (80% of the 60,000 training images, with the rest held out as a validation set). Each epoch is one full pass through all the training data.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/ab432454-c4f2-465e-b955-5a24e5ba86a8.png" alt="Accuracy and loss chart per epoch" style="display:block;margin:0 auto" />

<p>Accuracy climbs consistently epoch over epoch while loss drops — a sign the training is going smoothly.</p>
<h3>Step 5 — Evaluate the Model</h3>
<pre><code class="language-python">test_loss, test_accuracy = model.evaluate(x_test, y_test)
print(f'Test accuracy: {test_accuracy:.4f}')
</code></pre>
<p>The model is tested on 10,000 images it has never seen before — like a final exam.</p>
<p><strong>Result:</strong> Test accuracy <strong>97.39%</strong>, Test loss <strong>0.0899</strong>.</p>
<h3>Step 6 — The Moment AI Recognizes a Digit</h3>
<pre><code class="language-python">predictions = model.predict(x_test[:12])
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/6f893e5a-d16a-4800-bf4d-f4dfb023cef5.png" alt="Model predictions on the test set" style="display:block;margin:0 auto" />

<p>No manual rule was ever written to recognize each digit — the model learned this entirely on its own from thousands of examples.</p>
<h3>Step 7 — When AI Gets It Wrong</h3>
<p>Out of 10,000 test images, the model misclassified <strong>261 images</strong> (≈2.6%). A few examples below — many of these are genuinely ambiguous handwriting, even to a human eye:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/1f9bd856-8be2-4e27-ab21-3a948538e9ed.png" alt="Examples of wrong predictions" style="display:block;margin:0 auto" />

<hr />
<h2>Level Up: Fashion-MNIST</h2>
<p>After digits, the next challenge is recognizing clothing. Fashion-MNIST has 10 categories (T-shirt, Trouser, Pullover, Dress, Coat, Sandal, and more) — a harder task, since the differences between categories are more subtle than they are for digits.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/07a385f0-299e-4f44-90aa-7dabe9725022.png" alt="Fashion-MNIST dataset samples" style="display:block;margin:0 auto" />

<p>I used the <strong>exact same architecture</strong> (Flatten → Dense 128 → Dense 64 → Dense 10) to show that the concept is universal — only the data changes.</p>
<p><strong>Result:</strong> Test accuracy <strong>87.30%</strong>, Test loss <strong>0.3463</strong> — lower than MNIST, which makes sense since clothing visuals are inherently more ambiguous than digit shapes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/684d8dc7-08d4-40af-a055-3f86197fd628.png" alt="Model predictions on Fashion-MNIST" style="display:block;margin:0 auto" />

<hr />
<h2>Mini Challenge: Hyperparameter Experiments</h2>
<p>The material included a few "mini challenges" — I actually ran all three, instead of just imagining what might happen:</p>
<table>
<thead>
<tr>
<th>Experiment</th>
<th>Change</th>
<th>Test Accuracy</th>
</tr>
</thead>
<tbody><tr>
<td>Baseline</td>
<td>128 → 64 → 10, 5 epochs</td>
<td>97.39%</td>
</tr>
<tr>
<td>More neurons</td>
<td>256 → 64 → 10, 5 epochs</td>
<td><strong>97.76%</strong></td>
</tr>
<tr>
<td>More layers</td>
<td>128 → 64 → 32 → 10, 5 epochs</td>
<td>97.50%</td>
</tr>
<tr>
<td>More epochs</td>
<td>128 → 64 → 10, 10 epochs</td>
<td>97.66%</td>
</tr>
</tbody></table>
<p>🤔 Try to Guess First: of these three experiments, which do you think had the biggest impact on accuracy?</p>
<p>Based on the numbers above, <strong>increasing the number of neurons</strong> (256) gave the biggest accuracy boost compared to adding more layers or training longer. But there's an important caveat: in the "more epochs" experiment (10 epochs), validation loss started fluctuating after epoch 5, while training loss kept dropping steadily — an early sign of <strong>overfitting</strong>. So "training longer" doesn't automatically mean "better."</p>
<hr />
<h2>Common Mistakes to Avoid</h2>
<table>
<thead>
<tr>
<th>Mistake</th>
<th>Explanation &amp; Fix</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Overfitting</strong></td>
<td>AI "memorizes" the training data too well and fails on new data. Fix: use dropout and a validation set</td>
</tr>
<tr>
<td><strong>Too many epochs</strong></td>
<td>Training for too long can hurt generalization. Monitor validation accuracy to know when to stop</td>
</tr>
<tr>
<td><strong>Wrong learning rate</strong></td>
<td>Too high → the model never converges. Too low → training crawls. The Adam optimizer helps since it auto-adjusts</td>
</tr>
</tbody></table>
<hr />
<h2>Quiz: Test Your Understanding</h2>
<p>1. Why use <code>sparse_categorical_crossentropy</code> instead of <code>categorical_crossentropy</code> for the MNIST dataset?</p>
<p>Because the labels from <code>keras.datasets.mnist.load_data()</code> are plain integers (0–9), not one-hot encoded. <code>sparse_categorical_crossentropy</code> is designed to accept integer labels directly, so no extra encoding step is needed.</p>
<p>2. If training accuracy keeps climbing but validation accuracy stalls or drops, what's happening?</p>
<p>That's a sign of <strong>overfitting</strong> — the model is fitting the training data so closely that it's losing its ability to generalize to new data.</p>
<p>3. Why does Fashion-MNIST end up with lower accuracy (87.30%) than MNIST (97.39%) even though the architecture is exactly the same?</p>
<p>Because the data itself is harder. The visual patterns of clothing (e.g. telling a "Shirt" apart from a "Coat" based on silhouette alone) are far more ambiguous than the shapes of handwritten digits, which tend to be more structurally distinct.</p>
<hr />
<h2>Conclusion</h2>
<p>A few things I'm taking away from this:</p>
<ul>
<li><p><strong>A neural network is an artificial brain</strong> — layers of neurons working together to recognize complex patterns, inspired by how the human brain works.</p>
</li>
<li><p><strong>Deep learning shines on complex data</strong> — images, audio, text — because it can learn features on its own without manual feature engineering.</p>
</li>
<li><p><strong>Good training numbers don't guarantee good real-world numbers</strong> — a validation set and watching for overfitting are non-negotiable.</p>
</li>
<li><p><strong>Small experiments give real insight</strong> — the mini challenge taught me that "bigger" (more neurons) isn't automatically weaker than "deeper" (more layers) or "longer" (more epochs) — it really depends on the case.</p>
</li>
</ul>
<p>The same concepts in this notebook — layers, activation functions, loss, backpropagation — are the foundation for more advanced architectures like CNNs for images or Transformers for text, which will likely be the next thing I dig into.</p>
<p>💻 <strong>Full code and notebook</strong>, ready to run, is on GitHub: <a href="https://github.com/arielshakaramiro/deep-learning-mnist-neural-network-arielshakaramiro">github.com/arielshakaramiro/deep-learning-mnist-neural-network-arielshakaramiro</a></p>
<hr />
<p><em>Practice material from the Fullstack Bangalore AI Engineer Bootcamp.</em></p>
]]></content:encoded></item><item><title><![CDATA[Neural Network dari Nol: Belajar Deep Learning dengan MNIST & Fashion-MNIST]]></title><description><![CDATA[Sebelum belajar machine learning, saya pikir AI itu semacam kotak ajaib yang "tahu" jawabannya. Setelah masuk ke materi deep learning, gambaran itu berubah total — AI nggak tahu apa-apa di awal. Ia be]]></description><link>https://shaka-ai.hashnode.dev/neural-network-dari-nol-mnist-fashion-mnist-tensorflow-keras</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/neural-network-dari-nol-mnist-fashion-mnist-tensorflow-keras</guid><category><![CDATA[DeepLearning]]></category><category><![CDATA[Deep Learning]]></category><category><![CDATA[TensorFlow]]></category><category><![CDATA[keras]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Python]]></category><category><![CDATA[neural networks]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Fri, 04 Sep 2026 08:21:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/acaab20c-154e-4885-8f4f-cc0c97e4a111.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Sebelum belajar machine learning, saya pikir AI itu semacam kotak ajaib yang "tahu" jawabannya. Setelah masuk ke materi deep learning, gambaran itu berubah total — AI nggak tahu apa-apa di awal. Ia belajar dari nol, salah berkali-kali, lalu pelan-pelan jadi bisa mengenali pola. Tulisan ini catatan saya soal bagaimana proses itu bekerja, dari teori neural network sampai praktik langsung melatih AI mengenali angka tulisan tangan dan jenis pakaian pakai TensorFlow &amp; Keras.</p>
<h2>Daftar Isi</h2>
<ul>
<li><p><a href="#apa-itu-deep-learning">Apa Itu Deep Learning?</a></p>
</li>
<li><p><a href="#neuron-biologis-vs-neuron-buatan">Neuron Biologis vs Neuron Buatan</a></p>
</li>
<li><p><a href="#kenapa-harus-deep">Kenapa Harus "Deep"?</a></p>
</li>
<li><p><a href="#arsitektur-neural-network">Arsitektur Neural Network</a></p>
</li>
<li><p><a href="#activation-function">Activation Function</a></p>
</li>
<li><p><a href="#bagaimana-ai-belajar-training-process">Bagaimana AI Belajar (Training Process)</a></p>
</li>
<li><p><a href="#loss-function--optimizer">Loss Function &amp; Optimizer</a></p>
</li>
<li><p><a href="#machine-learning-vs-deep-learning">Machine Learning vs Deep Learning</a></p>
</li>
<li><p><a href="#praktik-membangun-neural-network-pertama">Praktik: Membangun Neural Network Pertama</a></p>
</li>
<li><p><a href="#level-up-fashion-mnist">Level Up: Fashion-MNIST</a></p>
</li>
<li><p><a href="#mini-challenge-eksperimen-hyperparameter">Mini Challenge: Eksperimen Hyperparameter</a></p>
</li>
<li><p><a href="#kesalahan-umum-yang-perlu-dihindari">Kesalahan Umum yang Perlu Dihindari</a></p>
</li>
<li><p><a href="#quiz-uji-pemahaman">Quiz: Uji Pemahaman</a></p>
</li>
<li><p><a href="#kesimpulan">Kesimpulan</a></p>
</li>
</ul>
<hr />
<h2>Apa Itu Deep Learning?</h2>
<p>Deep learning adalah teknik AI yang memproses informasi lewat banyak lapisan (<em>layers</em>) — mirip cara otak manusia punya berbagai tingkat pemahaman. Setiap layer memproses hasil dari layer sebelumnya, jadi pemahamannya makin lama makin dalam: dari mengenali garis sederhana, ke bentuk yang lebih kompleks, sampai akhirnya mengenali objek utuh.</p>
<p>Intinya sederhana: <strong>makin banyak layer, makin dalam pemahaman yang bisa dibangun model.</strong></p>
<h2>Neuron Biologis vs Neuron Buatan</h2>
<p>Neural network memang terinspirasi dari cara kerja otak manusia. Neuron biologis menerima sinyal listrik, memprosesnya, lalu meneruskan ke neuron lain. Neuron buatan meniru alur yang sama, cuma bahasanya angka, bukan sinyal listrik.</p>
<table>
<thead>
<tr>
<th>Neuron Biologis</th>
<th>Neuron Buatan</th>
</tr>
</thead>
<tbody><tr>
<td>Menerima sinyal listrik</td>
<td>Menerima input numerik</td>
</tr>
<tr>
<td>Memproses informasi</td>
<td>Melakukan kalkulasi (weight × input + bias)</td>
</tr>
<tr>
<td>Mengirim ke neuron lain</td>
<td>Mengirim hasil ke layer berikutnya</td>
</tr>
</tbody></table>
<p>Skalanya jauh beda — otak manusia punya sekitar 86 miliar neuron, sedangkan neural network buatan biasanya "cuma" mulai dari puluhan sampai jutaan neuron, tergantung seberapa rumit masalah yang mau diselesaikan.</p>
<h2>Kenapa Harus "Deep"?</h2>
<p>🤔 Coba Tebak Dulu: menurutmu, apa yang membedakan network 1 layer dengan network banyak layer dalam hal jenis pola yang bisa dikenali?</p>
<p>Network 1 layer cuma bisa memisahkan pola yang <strong>sederhana dan linear</strong> — bayangkan menarik satu garis lurus untuk memisahkan dua kelompok data. Network dengan banyak layer bisa mengenali pola yang <strong>kompleks dan non-linear</strong> — wajah, suara, bahasa, bahkan pola emosi dalam teks. Setiap layer tambahan memberi model "kosakata" baru untuk mendeskripsikan pola yang lebih rumit.</p>
<p>Ini alasan kenapa istilahnya "deep" learning, bukan sekadar "learning" — kedalaman (jumlah layer) itu sendiri yang membuat model bisa menangkap kompleksitas.</p>
<h2>Arsitektur Neural Network</h2>
<p>Data mengalir lewat tiga jenis layer:</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Fungsi</th>
</tr>
</thead>
<tbody><tr>
<td><strong>1. Input Layer</strong></td>
<td>Data masuk ke sistem (misalnya: pixel gambar)</td>
</tr>
<tr>
<td><strong>2. Hidden Layers</strong></td>
<td>Lapisan tersembunyi yang memproses &amp; mengekstrak fitur — bisa puluhan sampai ratusan layer</td>
</tr>
<tr>
<td><strong>3. Output Layer</strong></td>
<td>Hasil prediksi keluar (misalnya: angka yang dikenali)</td>
</tr>
</tbody></table>
<p>Alur ini — data masuk, diolah di hidden layers, keluar sebagai prediksi — disebut <strong>forward propagation</strong>.</p>
<p>Di level neuron, prosesnya begini:</p>
<ol>
<li><p><strong>Input</strong> — menerima data mentah (pixel gambar, angka, dll)</p>
</li>
<li><p><strong>Pemrosesan</strong> — tiap input dikalikan weight-nya, dijumlahkan, ditambah bias</p>
</li>
<li><p><strong>Activation Function</strong> — menentukan apakah neuron ini "aktif" atau tidak</p>
</li>
<li><p><strong>Output</strong> — hasil dikirim ke neuron di layer berikutnya</p>
</li>
</ol>
<h2>Activation Function</h2>
<p>Activation function itu semacam saklar on/off yang pintar di tiap neuron — mirip otak yang memutuskan apakah suatu ide layak diteruskan atau tidak.</p>
<table>
<thead>
<tr>
<th>Fungsi</th>
<th>Penjelasan</th>
</tr>
</thead>
<tbody><tr>
<td><strong>ReLU</strong> (Rectified Linear Unit)</td>
<td>Salah satu yang paling banyak dipakai. Output-nya 0 kalau input negatif, sama dengan input kalau positif: <code>f(x) = max(0, x)</code></td>
</tr>
<tr>
<td><strong>Sigmoid</strong></td>
<td>Mengubah semua nilai ke rentang 0–1. Cocok untuk kasus probabilitas: <code>f(x) = 1 / (1 + e^-x)</code></td>
</tr>
<tr>
<td><strong>Tanh</strong></td>
<td>Mirip sigmoid, tapi rentangnya -1 sampai 1, lebih <em>centered</em> di sekitar nol</td>
</tr>
<tr>
<td><strong>Softmax</strong></td>
<td>Dipakai di output layer untuk klasifikasi multi-kelas — mengubah output jadi distribusi probabilitas yang totalnya 100%</td>
</tr>
</tbody></table>
<h2>Bagaimana AI Belajar (Training Process)</h2>
<p>Proses training itu diulang berkali-kali (disebut <strong>epoch</strong>) sampai model mencapai akurasi yang diinginkan:</p>
<table>
<thead>
<tr>
<th>Tahap</th>
<th>Penjelasan</th>
</tr>
</thead>
<tbody><tr>
<td>1. Forward Pass</td>
<td>Data input diproses melalui network untuk menghasilkan prediksi</td>
</tr>
<tr>
<td>2. Calculate Loss</td>
<td>Membandingkan prediksi dengan label sebenarnya untuk mengukur seberapa salah</td>
</tr>
<tr>
<td>3. Backpropagation</td>
<td>Menghitung gradient error untuk tiap weight di network</td>
</tr>
<tr>
<td>4. Update Weights</td>
<td>Menyesuaikan weight pakai optimizer supaya error berkurang</td>
</tr>
</tbody></table>
<h2>Loss Function &amp; Optimizer</h2>
<p><strong>Loss function</strong> mengukur seberapa jauh prediksi model dari nilai sebenarnya — makin kecil loss, makin bagus modelnya. Untuk klasifikasi seperti MNIST, saya pakai <strong>Sparse Categorical Crossentropy</strong> (versi <em>sparse</em> karena label datasetnya berupa angka integer 0–9, bukan format one-hot).</p>
<p>Untuk mengarahkan model menuju loss terkecil, dipakai <strong>optimizer</strong>:</p>
<table>
<thead>
<tr>
<th>Optimizer</th>
<th>Penjelasan</th>
</tr>
</thead>
<tbody><tr>
<td><strong>SGD</strong> (Stochastic Gradient Descent)</td>
<td>Optimizer klasik, update weight berdasarkan gradient. Sederhana tapi kadang lambat konvergen</td>
</tr>
<tr>
<td><strong>Adam</strong> (Adaptive Moment Estimation)</td>
<td>Salah satu optimizer paling sering dipakai — gabungan momentum &amp; adaptive learning rate. Pilihan default untuk banyak kasus</td>
</tr>
<tr>
<td><strong>RMSprop</strong></td>
<td>Cocok untuk recurrent neural network, adaptif terhadap learning rate per parameter</td>
</tr>
</tbody></table>
<h2>Machine Learning vs Deep Learning</h2>
<table>
<thead>
<tr>
<th></th>
<th>Machine Learning</th>
<th>Deep Learning</th>
</tr>
</thead>
<tbody><tr>
<td>Fitur</td>
<td>Butuh feature engineering manual</td>
<td>Belajar fitur secara otomatis</td>
</tr>
<tr>
<td>Contoh algoritma</td>
<td>Decision Trees, SVM, Random Forest</td>
<td>CNN, RNN, Transformer</td>
</tr>
<tr>
<td>Kebutuhan expert</td>
<td>Butuh expert untuk ekstraksi fitur</td>
<td>Feature extraction dilakukan network sendiri</td>
</tr>
<tr>
<td>Ukuran data ideal</td>
<td>Terstruktur, kecil–menengah</td>
<td>Data besar dan kompleks (gambar, audio, teks)</td>
</tr>
<tr>
<td>Interpretasi</td>
<td>Lebih mudah dijelaskan</td>
<td>Lebih powerful, tapi kurang interpretable</td>
</tr>
</tbody></table>
<p><strong>Pilih Machine Learning kalau:</strong></p>
<ul>
<li><p>[ ] Dataset kecil (ribuan–puluhan ribu baris)</p>
</li>
<li><p>[ ] Butuh hasil yang bisa dijelaskan dengan jelas</p>
</li>
<li><p>[ ] Resource komputasi terbatas</p>
</li>
<li><p>[ ] Problem sudah well-defined dengan fitur yang jelas</p>
</li>
</ul>
<p><strong>Pilih Deep Learning kalau:</strong></p>
<ul>
<li><p>[ ] Dataset sangat besar (ratusan ribu–jutaan baris)</p>
</li>
<li><p>[ ] Data kompleks: gambar, video, audio, teks</p>
</li>
<li><p>[ ] Ada akses ke GPU/TPU untuk training</p>
</li>
<li><p>[ ] Butuh akurasi setinggi mungkin</p>
</li>
</ul>
<p>Karena mau tackle problem image recognition (MNIST), bagian praktik di bawah ini fokus ke deep learning.</p>
<h2>Contoh Penerapan di Dunia Nyata</h2>
<p>Konsep yang sama seperti di MNIST — cuma dengan arsitektur yang jauh lebih besar dan data yang jauh lebih banyak — dipakai untuk berbagai use case nyata:</p>
<table>
<thead>
<tr>
<th>Use Case</th>
<th>Penjelasan</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Self-driving cars</strong></td>
<td>Deep learning dipakai untuk mengenali jalan, rambu, dan pejalan kaki secara real-time</td>
</tr>
<tr>
<td><strong>Face recognition</strong></td>
<td>Sistem seperti FaceID menggunakan neural network untuk mencocokkan wajah dari jutaan kemungkinan</td>
</tr>
<tr>
<td><strong>Analisis citra medis</strong></td>
<td>Model deep learning bisa membantu mendeteksi pola pada gambar medis seperti X-ray — meski performanya bervariasi tergantung studi, dataset, dan konteks klinis, dan tetap butuh validasi dari tenaga medis, bukan pengganti diagnosis dokter</td>
</tr>
</tbody></table>
<p>Dari mengenali digit sederhana sampai teknologi yang mengubah banyak industri, fondasi konsepnya sama seperti yang dipraktikkan di bawah ini.</p>
<hr />
<h2>Praktik: Membangun Neural Network Pertama</h2>
<p>Semua kode di bawah ini saya jalankan penuh di Google Colab — jadi semua angka yang muncul memang hasil eksekusi nyata, bukan perkiraan.</p>
<h3>Step 1 — Load Dataset MNIST</h3>
<p>MNIST berisi 70.000 gambar angka tulisan tangan (0–9) berukuran 28×28 pixel — dataset klasik untuk belajar deep learning.</p>
<pre><code class="language-python">from tensorflow import keras

(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/39bca474-5a74-491a-a1a6-141d1ef40126.png" alt="Sampel dataset MNIST" style="display:block;margin:0 auto" />

<h3>Step 2 — Build Model</h3>
<pre><code class="language-python">model = keras.Sequential([
    keras.layers.Flatten(input_shape=(28, 28)),
    keras.layers.Dense(128, activation='relu'),
    keras.layers.Dense(64, activation='relu'),
    keras.layers.Dense(10, activation='softmax')
])
</code></pre>
<ul>
<li><p><strong>Flatten</strong> — ubah gambar 2D (28×28) jadi array 1D (784 elemen)</p>
</li>
<li><p><strong>Dense 128</strong> — hidden layer pertama, 128 neuron, belajar fitur dasar</p>
</li>
<li><p><strong>Dense 64</strong> — hidden layer kedua, 64 neuron, ekstraksi fitur lebih abstrak</p>
</li>
<li><p><strong>Dense 10</strong> — output layer, satu neuron per digit (0–9), softmax untuk probabilitas</p>
</li>
</ul>
<p>Total parameter yang dilatih: <strong>109.386</strong>.</p>
<h3>Step 3 — Compile Model</h3>
<pre><code class="language-python">model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
</code></pre>
<h3>Step 4 — Train Model</h3>
<pre><code class="language-python">history = model.fit(x_train, y_train, epochs=5, validation_split=0.2)
</code></pre>
<p>AI "sekolah" dari 48.000 gambar (80% dari 60.000 data training, sisanya jadi validation set). Tiap epoch adalah satu putaran penuh melewati semua data training.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/9c2618dc-c550-458e-8d71-c412ccc29cc3.png" alt="Grafik accuracy dan loss per epoch" style="display:block;margin:0 auto" />

<p>Accuracy naik konsisten dari epoch ke epoch, loss turun — tanda training berjalan sehat.</p>
<h3>Step 5 — Evaluate Model</h3>
<pre><code class="language-python">test_loss, test_accuracy = model.evaluate(x_test, y_test)
print(f'Test accuracy: {test_accuracy:.4f}')
</code></pre>
<p>Model diuji dengan 10.000 gambar yang belum pernah dilihat sebelumnya — seperti ujian akhir.</p>
<p><strong>Hasil:</strong> Test accuracy <strong>97.39%</strong>, Test loss <strong>0.0899</strong>.</p>
<h3>Step 6 — Momen AI Mengenali Angka</h3>
<pre><code class="language-python">predictions = model.predict(x_test[:12])
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/6ed9d641-ad07-4373-ad5b-cb2b7b191b99.png" alt="Prediksi model pada test set" style="display:block;margin:0 auto" />

<p>Tidak ada rule manual yang ditulis untuk mengenali tiap angka — model belajar sendiri dari ribuan contoh.</p>
<h3>Step 7 — Saat AI Salah Menebak</h3>
<p>Dari 10.000 gambar test, model salah menebak <strong>261 gambar</strong> (≈2.6%). Beberapa contohnya di bawah — banyak yang memang tulisan tangan ambigu, bahkan buat mata manusia:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/0840f6db-2f49-4cad-8c08-cfd7317db437.png" alt="Contoh prediksi yang salah" style="display:block;margin:0 auto" />

<hr />
<h2>Level Up: Fashion-MNIST</h2>
<p>Setelah berhasil dengan angka, tantangan berikutnya: mengenali jenis pakaian. Fashion-MNIST punya 10 kategori (T-shirt, Trouser, Pullover, Dress, Coat, Sandal, dll) — lebih sulit karena perbedaan antar kategori lebih <em>subtle</em> dibanding angka.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/8d06b1ae-a326-4e46-970b-525ac73acdee.png" alt="Sampel dataset Fashion-MNIST" style="display:block;margin:0 auto" />

<p>Saya pakai arsitektur <strong>persis sama</strong> (Flatten → Dense 128 → Dense 64 → Dense 10) untuk menunjukkan bahwa konsepnya universal, cuma datanya yang beda.</p>
<p><strong>Hasil:</strong> Test accuracy <strong>87.30%</strong>, Test loss <strong>0.3463</strong> — turun dibanding MNIST, wajar karena pola visual pakaian memang lebih ambigu daripada pola angka.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/29cc0fed-a361-4b9c-b735-ef4715e28dfe.png" alt="Prediksi model pada Fashion-MNIST" style="display:block;margin:0 auto" />

<hr />
<h2>Mini Challenge: Eksperimen Hyperparameter</h2>
<p>Materinya kasih beberapa "mini challenge" — saya coba jalankan ketiganya beneran, bukan cuma dibayangkan:</p>
<table>
<thead>
<tr>
<th>Eksperimen</th>
<th>Perubahan</th>
<th>Test Accuracy</th>
</tr>
</thead>
<tbody><tr>
<td>Baseline</td>
<td>128 → 64 → 10, 5 epoch</td>
<td>97.39%</td>
</tr>
<tr>
<td>Tambah neuron</td>
<td>256 → 64 → 10, 5 epoch</td>
<td><strong>97.76%</strong></td>
</tr>
<tr>
<td>Tambah layer</td>
<td>128 → 64 → 32 → 10, 5 epoch</td>
<td>97.50%</td>
</tr>
<tr>
<td>Tambah epoch</td>
<td>128 → 64 → 10, 10 epoch</td>
<td>97.66%</td>
</tr>
</tbody></table>
<p>🤔 Coba Tebak Dulu: dari ketiga eksperimen ini, mana yang menurutmu paling ampuh naikkin akurasi?</p>
<p>Dari data di atas, <strong>menambah jumlah neuron</strong> (256) memberi peningkatan akurasi paling besar dibanding menambah layer atau menambah epoch. Tapi ada catatan penting: pada eksperimen "tambah epoch" (10 epoch), validation loss mulai naik-turun tidak stabil setelah epoch ke-5, sementara training loss terus turun — ini sinyal awal <strong>overfitting</strong>. Jadi "lebih lama training" nggak selalu berarti "lebih baik".</p>
<hr />
<h2>Kesalahan Umum yang Perlu Dihindari</h2>
<table>
<thead>
<tr>
<th>Kesalahan</th>
<th>Penjelasan &amp; Solusi</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Overfitting</strong></td>
<td>AI terlalu "hafal" data training sehingga gagal di data baru. Solusi: pakai dropout dan validation set</td>
</tr>
<tr>
<td><strong>Terlalu banyak epoch</strong></td>
<td>Training kelamaan bikin model kehilangan generalisasi. Pantau validation accuracy untuk tahu kapan harus berhenti</td>
</tr>
<tr>
<td><strong>Learning rate tidak tepat</strong></td>
<td>Terlalu besar → model tidak konvergen. Terlalu kecil → training lambat. Optimizer Adam membantu karena auto-adjust</td>
</tr>
</tbody></table>
<hr />
<h2>Quiz: Uji Pemahaman</h2>
<p>1. Kenapa dipakai <code>sparse_categorical_crossentropy</code>, bukan <code>categorical_crossentropy</code>, untuk dataset MNIST?</p>
<p>Karena label dari <code>keras.datasets.mnist.load_data()</code> berupa angka integer (0–9), bukan format one-hot encoded. <code>sparse_categorical_crossentropy</code> didesain untuk menerima label integer langsung, jadi tidak perlu encoding tambahan.</p>
<p>2. Kalau accuracy training terus naik tapi validation accuracy malah stagnan atau turun, apa yang sedang terjadi?</p>
<p>Ini tanda <strong>overfitting</strong> — model terlalu menyesuaikan diri dengan data training sampai kehilangan kemampuan generalisasi ke data baru.</p>
<p>3. Kenapa Fashion-MNIST punya akurasi lebih rendah (87.30%) dibanding MNIST (97.39%) padahal arsitekturnya sama persis?</p>
<p>Karena tingkat kesulitan datanya beda. Pola visual pakaian (misalnya membedakan "Shirt" dan "Coat" hanya dari siluet) jauh lebih ambigu dibanding pola angka tulisan tangan yang bentuknya relatif lebih terstruktur.</p>
<hr />
<h2>Kesimpulan</h2>
<p>Beberapa hal yang saya bawa pulang dari sesi ini:</p>
<ul>
<li><p><strong>Neural network = otak buatan</strong> — layer-layer neuron bekerja sama mengenali pola kompleks, terinspirasi dari cara kerja otak manusia.</p>
</li>
<li><p><strong>Deep learning unggul untuk data kompleks</strong> — gambar, suara, teks — karena bisa belajar fitur sendiri tanpa perlu feature engineering manual.</p>
</li>
<li><p><strong>Angka bagus di training tidak menjamin angka bagus di dunia nyata</strong> — validation set dan pemantauan overfitting itu wajib.</p>
</li>
<li><p><strong>Eksperimen kecil kasih insight nyata</strong> — dari mini challenge, saya belajar bahwa "lebih besar" (neuron) belum tentu kalah efektif dibanding "lebih dalam" (layer) atau "lebih lama" (epoch), tergantung kasusnya.</p>
</li>
</ul>
<p>Konsep yang sama di notebook ini — layer, activation function, loss, backpropagation — jadi fondasi untuk arsitektur yang lebih kompleks seperti CNN untuk gambar atau Transformer untuk teks, yang kemungkinan jadi bahasan lanjutan berikutnya.</p>
<p>💻 <strong>Kode lengkap dan notebook</strong> yang bisa langsung dijalankan ada di GitHub: <a href="https://github.com/arielshakaramiro/deep-learning-mnist-neural-network-arielshakaramiro">github.com/arielshakaramiro/deep-learning-mnist-neural-network-arielshakaramiro</a></p>
<hr />
<p><em>Materi latihan dari Fullstack Bangalore AI Engineer Bootcamp.</em></p>
]]></content:encoded></item><item><title><![CDATA[Hands-On: Unsupervised Learning — Clustering Iris Flowers with KMeans (Plus Follow-Up Experiments)]]></title><description><![CDATA[by Muhammad Ariel Shakaramiro
In Part 1, I covered the theory behind Supervised vs Unsupervised Learning, and in Part 2 I put the supervised side into practice — training a model to recognize Iris spe]]></description><link>https://shaka-ai.hashnode.dev/hands-on-unsupervised-learning-clustering-iris-kmeans</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/hands-on-unsupervised-learning-clustering-iris-kmeans</guid><category><![CDATA[Python]]></category><category><![CDATA[scikit learn]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[clustering]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Fri, 04 Sep 2026 03:16:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/cfb66e19-65d8-4d45-b154-c45f1c5db272.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>by Muhammad Ariel Shakaramiro</em></p>
<p>In <a href="https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-machine-learning-basics">Part 1</a>, I covered the theory behind Supervised vs Unsupervised Learning, and in <a href="https://shaka-ai.hashnode.dev/hands-on-iris-classification-logistic-regression-vs-decision-tree">Part 2</a> I put the supervised side into practice — training a model to recognize Iris species from labels it already had. Now it's time for the side that hasn't been practiced yet: <strong>Unsupervised Learning</strong>. This time the model isn't told what species anything is — it has to figure out the grouping entirely on its own, just from patterns in the data.</p>
<p>Same Iris flowers, a different angle. Every piece of code in this post has been run and verified myself.</p>
<h2>Table of Contents</h2>
<ul>
<li><p><a href="#from-supervised-to-unsupervised">From Supervised to Unsupervised</a></p>
</li>
<li><p><a href="#setup-load-data">Setup &amp; Load Data</a></p>
</li>
<li><p><a href="#clustering-with-kmeans">Clustering with KMeans</a></p>
</li>
<li><p><a href="#evaluating-clustering-ari">Evaluating Clustering: Adjusted Rand Index</a></p>
</li>
<li><p><a href="#visualization-pca">Visualization: Clusters vs Ground Truth</a></p>
</li>
<li><p><a href="#experiment-1">Experiment 1: Random Forest vs Logistic Regression</a></p>
</li>
<li><p><a href="#experiment-2">Experiment 2: Effect of Cluster Count on ARI</a></p>
</li>
<li><p><a href="#experiment-3">Experiment 3: Effect of Scaling on Clustering</a></p>
</li>
<li><p><a href="#experiment-4">Experiment 4: Another Dataset — Wine</a></p>
</li>
<li><p><a href="#source-code">Source Code</a></p>
</li>
<li><p><a href="#lessons">Lessons From All the Experiments</a></p>
</li>
<li><p><a href="#checklist">Checklist</a></p>
</li>
<li><p><a href="#summary">Summary</a></p>
</li>
</ul>
<h2>From Supervised to Unsupervised</h2>
<p>In <a href="https://shaka-ai.hashnode.dev/hands-on-iris-classification-logistic-regression-vs-decision-tree">Part 2</a>, the flow was clear: we had <code>X</code> (features) and <code>y</code> (species labels), the model learned the relationship between them, then got tested on new data.</p>
<p>In Unsupervised Learning, <code>y</code> is <strong>completely hidden</strong> from the model. It only gets <code>X</code>, and its job is to find structure or groupings on its own — purely from similarity between data points. Only after clustering is done do we "reveal" the true labels to check how close the model's groupings are to reality.</p>
<h2>Setup &amp; Load Data</h2>
<pre><code class="language-python">import numpy as np
import matplotlib.pyplot as plt

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report, adjusted_rand_score
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA

iris = load_iris()
X = iris.data
y = iris.target
feature_names = iris.feature_names
target_names = iris.target_names
</code></pre>
<p>The dataset and <code>X</code>, <code>y</code> variables here are exactly the same as what we used in <a href="https://shaka-ai.hashnode.dev/hands-on-iris-classification-logistic-regression-vs-decision-tree">Part 2</a> — 150 samples, 4 features, 3 species.</p>
<h2>Clustering with KMeans</h2>
<pre><code class="language-python">n_clusters = 3  # we know Iris has 3 classes, but KMeans doesn't know the class names
kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
cluster_labels = kmeans.fit_predict(X)

print("Unique cluster labels:", np.unique(cluster_labels))
</code></pre>
<p>Notice: <code>KMeans.fit_predict(X)</code> only takes <code>X</code> — no <code>y</code> at all. KMeans works by iteratively placing 3 "center points" (centroids), grouping each sample to its nearest centroid, and repeating until the centroid positions stabilize.</p>
<p>The result is a set of cluster labels (0, 1, 2) — but importantly, these numbers <strong>don't automatically correspond</strong> to the true species label order (0=setosa, 1=versicolor, 2=virginica). KMeans's cluster 0 isn't necessarily the same as class 0 in the original data.</p>
<h2>Evaluating Clustering: ARI</h2>
<p>Since cluster labels don't automatically map to the true labels, we can't just use regular <code>accuracy_score</code>. This is where <strong>Adjusted Rand Index (ARI)</strong> comes in — a metric that measures how similar two groupings are, regardless of how the labels are numbered.</p>
<pre><code class="language-python">ari = adjusted_rand_score(y, cluster_labels)
print("Adjusted Rand Index (ARI):", round(ari, 4))
</code></pre>
<p><strong>Verified result: ARI = 0.7302</strong></p>
<p>ARI ranges from -1 to 1: the closer to 1.0, the more similar the clustering is to the true labels. A score of 0.73 is fairly good — a sign that the natural structure in the Iris data actually aligns well with the true species split, even though the model was never "told" any species names.</p>
<p>🤔 Guess First: why isn't ARI a perfect 1.0, when Logistic Regression in [Part 2](<a href="https://shaka-ai.hashnode.dev/hands-on-iris-classification-logistic-regression-vs-decision-tree">https://shaka-ai.hashnode.dev/hands-on-iris-classification-logistic-regression-vs-decision-tree</a>) hit 96.67% accuracy?</p>
<p>Because the tasks are different. Logistic Regression in <a href="https://shaka-ai.hashnode.dev/hands-on-iris-classification-logistic-regression-vs-decision-tree">Part 2</a> was <strong>given the answers</strong> through labels during training — it just had to learn the boundary between classes. KMeans here has <strong>zero idea</strong> which of the 3 species is which — it only groups based on distance between data points. Since two species (versicolor and virginica) have naturally similar and slightly overlapping traits, KMeans can misplace a few boundary samples — the exact same kind of mistake that also showed up in Part 2's supervised model.</p>
<h2>Visualization: Clusters vs Ground Truth</h2>
<pre><code class="language-python">pca = PCA(n_components=2)
X_2d = pca.fit_transform(X)

plt.figure(figsize=(10, 4))

plt.subplot(1, 2, 1)
plt.title("KMeans Clustering (Unsupervised)")
scatter1 = plt.scatter(X_2d[:, 0], X_2d[:, 1], c=cluster_labels, alpha=0.7)
plt.xlabel("PCA 1")
plt.ylabel("PCA 2")
plt.colorbar(scatter1, label="Cluster ID")

plt.subplot(1, 2, 2)
plt.title("True Labels (Ground Truth)")
scatter2 = plt.scatter(X_2d[:, 0], X_2d[:, 1], c=y, alpha=0.7)
plt.xlabel("PCA 1")
plt.ylabel("PCA 2")
plt.colorbar(scatter2, label="True Class")

plt.tight_layout()
plt.show()
</code></pre>
<p>PCA here is used purely for visualization — reducing Iris's 4 features down to 2 dimensions so it can be plotted as a scatter plot. Placed side by side, the two plots' color patterns look strikingly similar — a visual confirmation of that 0.73 ARI score above: one cluster (setosa) sits far apart and perfectly separated, while the other two (versicolor–virginica) overlap slightly in both plots.</p>
<h2>Experiment 1: Random Forest vs Logistic Regression</h2>
<p>Beyond the unsupervised practice, I also explored a few follow-up questions. First: does a more complex model automatically perform better?</p>
<p><strong>Update:</strong> the original version of this section compared two models' raw test accuracy directly to decide which was better — the exact same methodology problem a reader (<a href="https://hashnode.com/@ahmetozel">Ahmet Özel</a>) flagged in the <a href="https://shaka-ai.hashnode.dev/hands-on-iris-classification-logistic-regression-vs-decision-tree">Part 2</a> comments, just in "model selection" form instead of "hyperparameter selection" form. Once you compare several models via test scores and declare a winner, that test score stops being an honest estimate. The correct approach: compare via cross-validation on the training set first, and only touch the test set once, with the winning model, at the end.</p>
<p>Using the exact same train/test split as <a href="https://shaka-ai.hashnode.dev/hands-on-iris-classification-logistic-regression-vs-decision-tree">Part 2</a>:</p>
<pre><code class="language-python">X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)
</code></pre>
<p><strong>Step 1 — Compare via cross-validation (training set only):</strong></p>
<pre><code class="language-python">from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import StratifiedKFold, cross_val_score

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

lr_scores = cross_val_score(LogisticRegression(max_iter=200), X_train, y_train, cv=cv)
rf_scores = cross_val_score(RandomForestClassifier(random_state=42), X_train, y_train, cv=cv)

print("Logistic Regression — CV accuracy:", round(lr_scores.mean(), 4))
print("Random Forest — CV accuracy:", round(rf_scores.mean(), 4))
</code></pre>
<table>
<thead>
<tr>
<th>Model</th>
<th>CV Accuracy (training set only)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Logistic Regression</strong></td>
<td><strong>95.83%</strong></td>
</tr>
<tr>
<td>Random Forest</td>
<td>95.00%</td>
</tr>
</tbody></table>
<p>Based on this — without touching the test set at all — Logistic Regression wins.</p>
<p><strong>Step 2 — Touch the test set once, with the winning model:</strong></p>
<pre><code class="language-python">final_model = LogisticRegression(max_iter=200)
final_model.fit(X_train, y_train)
final_acc = accuracy_score(y_test, final_model.predict(X_test))
print("Final test accuracy:", round(final_acc, 4))
</code></pre>
<p><strong>Result: 96.67%</strong> — the same Logistic Regression number already reported in <a href="https://shaka-ai.hashnode.dev/hands-on-iris-classification-logistic-regression-vs-decision-tree">Part 2</a>, but this time the "Logistic Regression beats Random Forest" decision came purely from the training set.</p>
<p>Interestingly, the final conclusion doesn't change from the old approach (Random Forest still loses), but that's a coincidence — and it's exactly why the process still has to be correct: on a different dataset where the result happened to differ, the flawed approach (comparing directly on the test set) would never reveal its own mistake until the model was deployed and underperformed expectations.</p>
<p>🤔 For reference: what happens if we still compare directly on the test set (the discouraged way)?</p>
<p>Random Forest = 90.0% on the test set, Logistic Regression = 96.67% on the test set. Same conclusion, but that 90.0% Random Forest number isn't fully trustworthy as an estimate, technically speaking, since it came from the same process used to "compare and choose" — not from a clean final evaluation.</p>
<h2>Experiment 2: Effect of Cluster Count on ARI</h2>
<p>In the real world, we don't always know the "correct" number of groups. What happens if we change <code>n_clusters</code>?</p>
<pre><code class="language-python">for k in range(2, 7):
    km_k = KMeans(n_clusters=k, random_state=42, n_init=10)
    labels_k = km_k.fit_predict(X)
    ari_k = adjusted_rand_score(y, labels_k)
    print(f"n_clusters={k}: ARI = {round(ari_k, 4)}")
</code></pre>
<table>
<thead>
<tr>
<th>n_clusters</th>
<th>ARI</th>
</tr>
</thead>
<tbody><tr>
<td>2</td>
<td>0.5399</td>
</tr>
<tr>
<td><strong>3</strong></td>
<td><strong>0.7302</strong></td>
</tr>
<tr>
<td>4</td>
<td>0.6498</td>
</tr>
<tr>
<td>5</td>
<td>0.6125</td>
</tr>
<tr>
<td>6</td>
<td>0.4475</td>
</tr>
</tbody></table>
<p>The highest ARI consistently lands exactly at <code>n_clusters=3</code> — matching the true number of species. Both decreasing and increasing from that number drops the ARI. This confirms how important it is to validate the number of clusters (e.g. via elbow method or silhouette score), rather than guessing a number.</p>
<h2>Experiment 3: Effect of Scaling on Clustering</h2>
<pre><code class="language-python">from sklearn.preprocessing import StandardScaler

X_scaled = StandardScaler().fit_transform(X)
kmeans_scaled = KMeans(n_clusters=3, random_state=42, n_init=10)
cluster_labels_scaled = kmeans_scaled.fit_predict(X_scaled)
ari_scaled = adjusted_rand_score(y, cluster_labels_scaled)

print("ARI without scaling:", round(ari, 4))
print("ARI with scaling   :", round(ari_scaled, 4))
</code></pre>
<p><strong>Verified result:</strong> ARI without scaling = <strong>0.7302</strong>, with scaling = <strong>0.6201</strong>.</p>
<p>This surprised me before I ran it — scaling is usually treated as a default "best practice" before clustering. But in Iris's case, scaling actually <strong>decreased</strong> ARI. The reason: all four Iris features are already on the same unit scale (cm), so forcing every feature into a standardized scale actually erased a bit of relative information that was previously helping KMeans distinguish clusters.</p>
<h2>Experiment 4: Another Dataset — Wine</h2>
<p>To see whether the pattern above holds generally, I tried a dataset whose features span a much wider range of scales.</p>
<pre><code class="language-python">from sklearn.datasets import load_wine

wine = load_wine()
X_wine, y_wine = wine.data, wine.target

# Clustering without scaling
kmeans_wine = KMeans(n_clusters=3, random_state=42, n_init=10)
cluster_wine = kmeans_wine.fit_predict(X_wine)
ari_wine = adjusted_rand_score(y_wine, cluster_wine)

# Clustering with scaling
X_wine_scaled = StandardScaler().fit_transform(X_wine)
kmeans_wine_scaled = KMeans(n_clusters=3, random_state=42, n_init=10)
cluster_wine_scaled = kmeans_wine_scaled.fit_predict(X_wine_scaled)
ari_wine_scaled = adjusted_rand_score(y_wine, cluster_wine_scaled)

print("Wine ARI without scaling:", round(ari_wine, 4))
print("Wine ARI with scaling   :", round(ari_wine_scaled, 4))
</code></pre>
<table>
<thead>
<tr>
<th>Dataset</th>
<th>ARI Without Scaling</th>
<th>ARI With Scaling</th>
</tr>
</thead>
<tbody><tr>
<td>Iris</td>
<td>0.7302</td>
<td>0.6201 (decreased)</td>
</tr>
<tr>
<td>Wine</td>
<td>0.3711</td>
<td><strong>0.8975</strong> (sharp increase)</td>
</tr>
</tbody></table>
<p>A stark contrast. The <code>proline</code> feature in the Wine dataset is on a scale of hundreds, while other features stay under 10 — so without scaling, KMeans effectively only "sees" the <code>proline</code> feature when computing distances between points, drowning out other genuinely informative features. After scaling, ARI jumped to 0.8975.</p>
<p>As an additional note, Logistic Regression on the Wine dataset (without scaling) also triggered a <em>ConvergenceWarning</em> — a sign that this feature-scale issue doesn't only affect distance-based clustering, but also gradient-based models like Logistic Regression.</p>
<h2>Source Code</h2>
<p>The full notebook behind this post — complete with real output, visualizations, and all 4 independent experiments — is available to run yourself via this GitHub repo:</p>
<p>🔗 <a href="https://github.com/arielshakaramiro/supervised-unsupervised-learning-praktik-arielshakaramiro"><strong>github.com/arielshakaramiro/supervised-unsupervised-learning-praktik-arielshakaramiro</strong></a></p>
<p>No dataset download needed — both Iris and Wine ship built into scikit-learn. Just <code>pip install -r requirements.txt</code> and run the notebook top to bottom.</p>
<h2>Lessons From All the Experiments</h2>
<p>📋 Click to reveal the full summary</p>
<ul>
<li><p><strong>A more complex model ≠ automatically more accurate</strong> — default Random Forest underperformed Logistic Regression on the small Iris test set</p>
</li>
<li><p><strong>The number of clusters needs to be validated</strong>, not guessed — ARI consistently drops when <code>n_clusters</code> moves away from the true number of groups</p>
</li>
<li><p><strong>Scaling isn't an automatic win</strong> — it can help dramatically (Wine) or actually hurt slightly (Iris), depending on how the original features are distributed across scales</p>
</li>
<li><p>The only honest way to know what's right: <strong>try it, measure it, compare it</strong> — not assume from theory alone</p>
</li>
</ul>
<h2>Checklist</h2>
<ul>
<li><p>[ ] Understand the difference between the supervised workflow (uses labels) and unsupervised workflow (no labels)</p>
</li>
<li><p>[ ] Understand why <code>accuracy_score</code> doesn't work for clustering, and why ARI is used instead</p>
</li>
<li><p>[ ] Can explain why PCA is used here purely for visualization, not for training</p>
</li>
<li><p>[ ] Understand that model complexity and preprocessing (scaling) need to be tested case by case, not assumed to always help</p>
</li>
</ul>
<h2>Summary</h2>
<p>Unsupervised Learning proved something interesting: without being told a single species name, KMeans still managed to find structure similar to the true grouping (ARI 0.73) — purely from distance patterns in the data. But this result doesn't stand alone; the string of experiments above shows that model performance, cluster count, and preprocessing all interact with each other, and no single rule applies across every case.</p>
<p>Across this three-part series — theory (<a href="https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-machine-learning-basics">Part 1</a>), supervised practice (<a href="https://shaka-ai.hashnode.dev/hands-on-iris-classification-logistic-regression-vs-decision-tree">Part 2</a>), and unsupervised practice plus experiments (this Part 3) — one thread stays the same: don't trust a number until you've measured it yourself.</p>
<hr />
<p><em>Part of my AI Engineering study notes. Read</em> <a href="https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-machine-learning-basics"><em>Part 1</em></a> <em>(theory) and</em> <a href="https://shaka-ai.hashnode.dev/hands-on-iris-classification-logistic-regression-vs-decision-tree"><em>Part 2</em></a> <em>(supervised classification practice) for full context. Full source code:</em> <a href="https://github.com/arielshakaramiro/supervised-unsupervised-learning-praktik-arielshakaramiro"><em>GitHub</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Praktik: Unsupervised Learning — Clustering Bunga Iris dengan KMeans (dan Eksperimen Lanjutan)]]></title><description><![CDATA[by Muhammad Ariel Shakaramiro
Di Part 1 saya bahas teori Supervised vs Unsupervised Learning, dan di Part 2 saya praktikkan sisi supervised-nya — melatih model buat mengenali spesies Iris dari label y]]></description><link>https://shaka-ai.hashnode.dev/praktik-unsupervised-learning-clustering-iris-kmeans</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/praktik-unsupervised-learning-clustering-iris-kmeans</guid><category><![CDATA[Python]]></category><category><![CDATA[scikit learn]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[clustering]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Fri, 04 Sep 2026 03:13:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/8213cc94-5b1d-4af9-8416-7985459fa356.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>by Muhammad Ariel Shakaramiro</em></p>
<p>Di <a href="https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-dasar-machine-learning">Part 1</a> saya bahas teori Supervised vs Unsupervised Learning, dan di <a href="https://shaka-ai.hashnode.dev/praktik-klasifikasi-iris-logistic-regression-vs-decision-tree">Part 2</a> saya praktikkan sisi supervised-nya — melatih model buat mengenali spesies Iris dari label yang sudah ada. Sekarang giliran sisi yang belum pernah dipraktikkan: <strong>Unsupervised Learning</strong>. Kali ini modelnya nggak dikasih tahu spesies apa itu — dia harus cari sendiri pengelompokannya cuma dari pola data.</p>
<p>Bunga Iris yang sama, sudut pandang yang berbeda. Semua kode di post ini sudah saya jalankan sendiri dan angkanya terverifikasi.</p>
<h2>Daftar Isi</h2>
<ul>
<li><p><a href="#dari-supervised-ke-unsupervised">Dari Supervised ke Unsupervised</a></p>
</li>
<li><p><a href="#setup-load-data">Setup &amp; Load Data</a></p>
</li>
<li><p><a href="#clustering-dengan-kmeans">Clustering dengan KMeans</a></p>
</li>
<li><p><a href="#evaluasi-clustering-ari">Evaluasi Clustering: Adjusted Rand Index</a></p>
</li>
<li><p><a href="#visualisasi-pca">Visualisasi: Cluster vs Label Asli</a></p>
</li>
<li><p><a href="#eksperimen-1">Eksperimen 1: Random Forest vs Logistic Regression</a></p>
</li>
<li><p><a href="#eksperimen-2">Eksperimen 2: Efek Jumlah Cluster terhadap ARI</a></p>
</li>
<li><p><a href="#eksperimen-3">Eksperimen 3: Efek Scaling pada Clustering</a></p>
</li>
<li><p><a href="#eksperimen-4">Eksperimen 4: Dataset Lain — Wine</a></p>
</li>
<li><p><a href="#source-code">Source Code</a></p>
</li>
<li><p><a href="#pelajaran">Pelajaran dari Semua Eksperimen</a></p>
</li>
<li><p><a href="#checklist">Checklist</a></p>
</li>
<li><p><a href="#ringkasan">Ringkasan</a></p>
</li>
</ul>
<h2>Dari Supervised ke Unsupervised</h2>
<p>Di <a href="https://shaka-ai.hashnode.dev/praktik-klasifikasi-iris-logistic-regression-vs-decision-tree">Part 2</a>, alur kerjanya jelas: kita punya <code>X</code> (fitur) dan <code>y</code> (label spesies), model belajar hubungan keduanya, lalu diuji ke data baru.</p>
<p>Di Unsupervised Learning, <code>y</code>-nya kita <strong>sembunyikan</strong> sepenuhnya dari model. Model cuma dikasih <code>X</code>, dan tugasnya adalah menemukan struktur atau pengelompokan sendiri — murni dari kemiripan antar data. Setelah clustering selesai, baru kita "buka" label aslinya buat mengecek seberapa dekat hasil pengelompokan model dengan kenyataan.</p>
<h2>Setup &amp; Load Data</h2>
<pre><code class="language-python">import numpy as np
import matplotlib.pyplot as plt

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report, adjusted_rand_score
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA

iris = load_iris()
X = iris.data
y = iris.target
feature_names = iris.feature_names
target_names = iris.target_names
</code></pre>
<p>Dataset dan variabel <code>X</code>, <code>y</code> di sini sama persis dengan yang dipakai di <a href="https://shaka-ai.hashnode.dev/praktik-klasifikasi-iris-logistic-regression-vs-decision-tree">Part 2</a> — 150 sampel, 4 fitur, 3 spesies.</p>
<h2>Clustering dengan KMeans</h2>
<pre><code class="language-python">n_clusters = 3  # kita tahu Iris punya 3 kelas, tapi KMeans tidak tahu nama kelas
kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
cluster_labels = kmeans.fit_predict(X)

print("Cluster label unik:", np.unique(cluster_labels))
</code></pre>
<p>Perhatikan: <code>KMeans.fit_predict(X)</code> cuma menerima <code>X</code>, nggak ada <code>y</code> sama sekali. KMeans bekerja dengan cara menempatkan 3 "titik pusat" (centroid) secara iteratif, lalu mengelompokkan tiap sampel ke centroid terdekat, sampai posisi centroid-nya stabil.</p>
<p>Hasilnya adalah label cluster (0, 1, 2) — tapi penting dicatat, angka ini <strong>tidak otomatis cocok</strong> dengan urutan label spesies asli (0=setosa, 1=versicolor, 2=virginica). Cluster 0 dari KMeans belum tentu sama dengan kelas 0 di data asli.</p>
<h2>Evaluasi Clustering: ARI</h2>
<p>Karena label cluster nggak otomatis nyambung ke label asli, kita nggak bisa pakai <code>accuracy_score</code> biasa. Di sinilah <strong>Adjusted Rand Index (ARI)</strong> dipakai — metrik yang mengukur seberapa mirip dua cara pengelompokan, terlepas dari penomoran labelnya.</p>
<pre><code class="language-python">ari = adjusted_rand_score(y, cluster_labels)
print("Adjusted Rand Index (ARI):", round(ari, 4))
</code></pre>
<p><strong>Hasil terverifikasi: ARI = 0.7302</strong></p>
<p>Skala ARI dari -1 sampai 1: semakin dekat ke 1.0, semakin mirip hasil clustering dengan label asli. Nilai 0.73 tergolong cukup baik — tandanya struktur alami di data Iris memang cukup selaras dengan pembagian spesies aslinya, meskipun modelnya sama sekali nggak pernah "diberi tahu" nama spesiesnya.</p>
<p>🤔 Coba Tebak Dulu: kenapa ARI-nya nggak sampai 1.0 sempurna, padahal di [Part 2](<a href="https://shaka-ai.hashnode.dev/praktik-klasifikasi-iris-logistic-regression-vs-decision-tree">https://shaka-ai.hashnode.dev/praktik-klasifikasi-iris-logistic-regression-vs-decision-tree</a>) Logistic Regression bisa dapat akurasi 96.67%?</p>
<p>Karena tugasnya beda. Logistic Regression di <a href="https://shaka-ai.hashnode.dev/praktik-klasifikasi-iris-logistic-regression-vs-decision-tree">Part 2</a> <strong>dikasih tahu jawabannya</strong> lewat label saat training — dia tinggal belajar batas antar kelas. KMeans di sini <strong>sama sekali nggak tahu</strong> ada 3 spesies yang mana — dia cuma mengelompokkan berdasarkan jarak antar titik data. Kalau dua spesies (versicolor dan virginica) punya ciri yang mirip dan sedikit tumpang tindih secara alami, KMeans bisa saja salah menempatkan beberapa sampel di batas itu — persis seperti kesalahan yang juga muncul di model supervised Part 2.</p>
<h2>Visualisasi: Cluster vs Label Asli</h2>
<pre><code class="language-python">pca = PCA(n_components=2)
X_2d = pca.fit_transform(X)

plt.figure(figsize=(10, 4))

plt.subplot(1, 2, 1)
plt.title("Clustering KMeans (Unsupervised)")
scatter1 = plt.scatter(X_2d[:, 0], X_2d[:, 1], c=cluster_labels, alpha=0.7)
plt.xlabel("PCA 1")
plt.ylabel("PCA 2")
plt.colorbar(scatter1, label="Cluster ID")

plt.subplot(1, 2, 2)
plt.title("Label Asli (Ground Truth)")
scatter2 = plt.scatter(X_2d[:, 0], X_2d[:, 1], c=y, alpha=0.7)
plt.xlabel("PCA 1")
plt.ylabel("PCA 2")
plt.colorbar(scatter2, label="True Class")

plt.tight_layout()
plt.show()
</code></pre>
<p>PCA dipakai di sini murni buat visualisasi — menyederhanakan 4 fitur Iris jadi 2 dimensi biar bisa digambar di scatter plot. Kalau dua plot ini ditaruh bersebelahan, pola warnanya kelihatan sangat mirip — bukti visual dari angka ARI 0.73 di atas: satu cluster (setosa) terpisah jauh dan sempurna, sementara dua cluster lain (versicolor-virginica) sedikit tumpang tindih di keduanya.</p>
<h2>Eksperimen 1: Random Forest vs Logistic Regression</h2>
<p>Selain praktik unsupervised, saya juga eksplorasi beberapa pertanyaan lanjutan. Pertama: apakah model yang lebih kompleks otomatis lebih baik?</p>
<p><strong>Update:</strong> versi awal section ini membandingkan langsung akurasi test dua model buat menyimpulkan mana yang lebih baik — dan itu masalah metodologi yang sama persis dengan yang diangkat pembaca (<a href="https://hashnode.com/@ahmetozel">Ahmet Özel</a>) di kolom komentar <a href="https://shaka-ai.hashnode.dev/praktik-klasifikasi-iris-logistic-regression-vs-decision-tree">Part 2</a>, cuma di sini bentuknya "pilih model" bukan "pilih hyperparameter". Begitu kita bandingkan beberapa model lewat skor test lalu simpulkan mana yang menang, skor test itu udah nggak murni lagi jadi estimasi jujur. Cara yang benar: bandingkan dulu pakai cross-validation di training set, baru model yang menang disentuh ke test set sekali di akhir.</p>
<p>Split train/test-nya sama persis dengan yang dipakai di <a href="https://shaka-ai.hashnode.dev/praktik-klasifikasi-iris-logistic-regression-vs-decision-tree">Part 2</a>:</p>
<pre><code class="language-python">X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)
</code></pre>
<p><strong>Langkah 1 — Bandingkan lewat cross-validation (cuma di training set):</strong></p>
<pre><code class="language-python">from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import StratifiedKFold, cross_val_score

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

lr_scores = cross_val_score(LogisticRegression(max_iter=200), X_train, y_train, cv=cv)
rf_scores = cross_val_score(RandomForestClassifier(random_state=42), X_train, y_train, cv=cv)

print("Logistic Regression — CV accuracy:", round(lr_scores.mean(), 4))
print("Random Forest — CV accuracy:", round(rf_scores.mean(), 4))
</code></pre>
<table>
<thead>
<tr>
<th>Model</th>
<th>CV Accuracy (training set saja)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Logistic Regression</strong></td>
<td><strong>95.83%</strong></td>
</tr>
<tr>
<td>Random Forest</td>
<td>95.00%</td>
</tr>
</tbody></table>
<p>Berdasarkan angka ini — belum menyentuh test set sama sekali — Logistic Regression yang menang.</p>
<p><strong>Langkah 2 — Sentuh test set sekali, pakai model yang menang:</strong></p>
<pre><code class="language-python">final_model = LogisticRegression(max_iter=200)
final_model.fit(X_train, y_train)
final_acc = accuracy_score(y_test, final_model.predict(X_test))
print("Akurasi test final:", round(final_acc, 4))
</code></pre>
<p><strong>Hasil: 96.67%</strong> — sama dengan angka Logistic Regression yang sudah dilaporkan di <a href="https://shaka-ai.hashnode.dev/praktik-klasifikasi-iris-logistic-regression-vs-decision-tree">Part 2</a>, tapi sekarang keputusan "Logistic Regression lebih baik dari Random Forest" diambil murni dari training set.</p>
<p>Menariknya, kesimpulan akhirnya nggak berubah dibanding cara yang lama (Random Forest tetap kalah), tapi itu kebetulan — dan itu justru bukti kenapa prosesnya tetap harus benar: kalau di dataset lain hasilnya kebetulan berbeda, cara yang salah (bandingkan langsung di test set) nggak akan pernah ketahuan salahnya sampai model di-deploy dan performanya meleset dari yang diharapkan.</p>
<p>🤔 Referensi: apa yang terjadi kalau kita tetap bandingkan langsung di test set (cara yang tidak disarankan)?</p>
<p>Random Forest = 90.0% di test set, Logistic Regression = 96.67% di test set. Kesimpulannya sama, tapi angka Random Forest 90.0% ini secara teknis bukan estimasi yang bisa dipercaya penuh, karena diambil dari proses yang sama dengan yang dipakai buat "membandingkan dan memilih" — bukan dari evaluasi final yang bersih.</p>
<h2>Eksperimen 2: Efek Jumlah Cluster terhadap ARI</h2>
<p>Di real-world, kita nggak selalu tahu berapa jumlah kelompok yang "benar." Gimana kalau <code>n_clusters</code> di-ubah?</p>
<pre><code class="language-python">for k in range(2, 7):
    km_k = KMeans(n_clusters=k, random_state=42, n_init=10)
    labels_k = km_k.fit_predict(X)
    ari_k = adjusted_rand_score(y, labels_k)
    print(f"n_clusters={k}: ARI = {round(ari_k, 4)}")
</code></pre>
<table>
<thead>
<tr>
<th>n_clusters</th>
<th>ARI</th>
</tr>
</thead>
<tbody><tr>
<td>2</td>
<td>0.5399</td>
</tr>
<tr>
<td><strong>3</strong></td>
<td><strong>0.7302</strong></td>
</tr>
<tr>
<td>4</td>
<td>0.6498</td>
</tr>
<tr>
<td>5</td>
<td>0.6125</td>
</tr>
<tr>
<td>6</td>
<td>0.4475</td>
</tr>
</tbody></table>
<p>ARI tertinggi konsisten muncul persis di <code>n_clusters=3</code> — sesuai jumlah spesies asli. Baik dikurangi maupun ditambah dari angka itu, ARI-nya turun. Ini menegaskan pentingnya validasi jumlah cluster (misalnya lewat elbow method atau silhouette score), bukan asal tebak angka.</p>
<h2>Eksperimen 3: Efek Scaling pada Clustering</h2>
<pre><code class="language-python">from sklearn.preprocessing import StandardScaler

X_scaled = StandardScaler().fit_transform(X)
kmeans_scaled = KMeans(n_clusters=3, random_state=42, n_init=10)
cluster_labels_scaled = kmeans_scaled.fit_predict(X_scaled)
ari_scaled = adjusted_rand_score(y, cluster_labels_scaled)

print("ARI tanpa scaling :", round(ari, 4))
print("ARI dengan scaling:", round(ari_scaled, 4))
</code></pre>
<p><strong>Hasil terverifikasi:</strong> ARI tanpa scaling = <strong>0.7302</strong>, dengan scaling = <strong>0.6201</strong>.</p>
<p>Ini di luar dugaan saya sebelum dijalankan — biasanya scaling dianggap "praktik baik" secara default sebelum clustering. Tapi di kasus Iris, scaling justru <strong>menurunkan</strong> ARI. Penyebabnya: keempat fitur Iris sudah berada di skala satuan yang sama (cm), jadi memaksa semua fitur ke skala yang seragam malah menghilangkan sedikit informasi relatif yang tadinya membantu KMeans membedakan cluster.</p>
<h2>Eksperimen 4: Dataset Lain — Wine</h2>
<p>Buat lihat apakah pola di atas berlaku umum, saya coba dataset lain yang fitur-fiturnya jauh lebih beragam skalanya.</p>
<pre><code class="language-python">from sklearn.datasets import load_wine

wine = load_wine()
X_wine, y_wine = wine.data, wine.target

# Clustering tanpa scaling
kmeans_wine = KMeans(n_clusters=3, random_state=42, n_init=10)
cluster_wine = kmeans_wine.fit_predict(X_wine)
ari_wine = adjusted_rand_score(y_wine, cluster_wine)

# Clustering dengan scaling
X_wine_scaled = StandardScaler().fit_transform(X_wine)
kmeans_wine_scaled = KMeans(n_clusters=3, random_state=42, n_init=10)
cluster_wine_scaled = kmeans_wine_scaled.fit_predict(X_wine_scaled)
ari_wine_scaled = adjusted_rand_score(y_wine, cluster_wine_scaled)

print("ARI Wine tanpa scaling:", round(ari_wine, 4))
print("ARI Wine dengan scaling:", round(ari_wine_scaled, 4))
</code></pre>
<table>
<thead>
<tr>
<th>Dataset</th>
<th>ARI Tanpa Scaling</th>
<th>ARI Dengan Scaling</th>
</tr>
</thead>
<tbody><tr>
<td>Iris</td>
<td>0.7302</td>
<td>0.6201 (turun)</td>
</tr>
<tr>
<td>Wine</td>
<td>0.3711</td>
<td><strong>0.8975</strong> (naik drastis)</td>
</tr>
</tbody></table>
<p>Kontras yang jelas. Fitur <code>proline</code> di dataset Wine punya skala ratusan, sementara fitur lain di bawah 10 — jadi tanpa scaling, KMeans praktis cuma "melihat" fitur <code>proline</code> doang saat menghitung jarak antar titik, dan mengabaikan fitur lain yang sebenarnya informatif. Setelah di-scale, ARI-nya melonjak ke 0.8975.</p>
<p>Sebagai catatan tambahan, Logistic Regression di dataset Wine (tanpa scaling) juga sempat memunculkan <em>ConvergenceWarning</em> — tanda bahwa masalah skala fitur ini nggak cuma berdampak ke clustering berbasis jarak, tapi juga ke model berbasis gradient seperti Logistic Regression.</p>
<h2>Source Code</h2>
<p>Seluruh notebook di post ini — lengkap dengan output asli, visualisasi, dan 4 eksperimen mandiri — bisa dijalankan sendiri lewat repo GitHub berikut:</p>
<p>🔗 <a href="https://github.com/arielshakaramiro/supervised-unsupervised-learning-praktik-arielshakaramiro"><strong>github.com/arielshakaramiro/supervised-unsupervised-learning-praktik-arielshakaramiro</strong></a></p>
<p>Nggak perlu download dataset apa pun — Iris dan Wine sudah built-in di scikit-learn. Tinggal <code>pip install -r requirements.txt</code> lalu jalankan notebook-nya dari atas ke bawah.</p>
<h2>Pelajaran dari Semua Eksperimen</h2>
<p>📋 Klik untuk lihat rangkuman lengkap</p>
<ul>
<li><p><strong>Model lebih kompleks ≠ otomatis lebih akurat</strong> — Random Forest default performanya di bawah Logistic Regression di test set Iris yang kecil</p>
</li>
<li><p><strong>Jumlah cluster harus divalidasi</strong>, bukan ditebak — ARI selalu turun kalau <code>n_clusters</code> jauh dari jumlah kelompok asli</p>
</li>
<li><p><strong>Scaling bukan langkah otomatis yang selalu menguntungkan</strong> — bisa membantu drastis (Wine) atau justru sedikit merugikan (Iris), tergantung sebaran skala fitur aslinya</p>
</li>
<li><p>Cara paling jujur buat tahu mana yang benar: <strong>coba, ukur, bandingkan</strong> — bukan asumsi dari teori semata</p>
</li>
</ul>
<h2>Checklist</h2>
<ul>
<li><p>[ ] Paham bedanya alur kerja supervised (pakai label) vs unsupervised (tanpa label)</p>
</li>
<li><p>[ ] Ngerti kenapa <code>accuracy_score</code> nggak bisa dipakai buat clustering, dan kenapa ARI dipakai sebagai gantinya</p>
</li>
<li><p>[ ] Bisa jelasin kenapa PCA dipakai di sini murni untuk visualisasi, bukan untuk training</p>
</li>
<li><p>[ ] Paham bahwa model kompleks dan preprocessing (scaling) itu perlu diuji per kasus, bukan diasumsikan selalu membantu</p>
</li>
</ul>
<h2>Ringkasan</h2>
<p>Unsupervised Learning membuktikan sesuatu yang menarik: tanpa dikasih tahu satu pun nama spesies, KMeans tetap berhasil menemukan struktur yang mirip dengan pengelompokan asli (ARI 0.73) — murni dari pola jarak antar data. Tapi hasil ini nggak berdiri sendiri; deretan eksperimen di atas nunjukin kalau performa model, jumlah cluster, dan preprocessing semuanya saling memengaruhi, dan nggak ada satu aturan yang berlaku di semua kasus.</p>
<p>Dari tiga part seri ini — teori (<a href="https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-dasar-machine-learning">Part 1</a>), praktik supervised (<a href="https://shaka-ai.hashnode.dev/praktik-klasifikasi-iris-logistic-regression-vs-decision-tree">Part 2</a>), dan praktik unsupervised plus eksperimen (Part 3 ini) — satu benang merahnya sama: jangan percaya angka sebelum diukur sendiri.</p>
<hr />
<p><em>Bagian dari catatan belajar AI Engineering saya. Baca</em> <a href="https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-dasar-machine-learning"><em>Part 1</em></a> <em>(teori) dan</em> <a href="https://shaka-ai.hashnode.dev/praktik-klasifikasi-iris-logistic-regression-vs-decision-tree"><em>Part 2</em></a> <em>(praktik klasifikasi) untuk konteks lengkap. Source code lengkap:</em> <a href="https://github.com/arielshakaramiro/supervised-unsupervised-learning-praktik-arielshakaramiro"><em>GitHub</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Hands-On: Classifying Iris Flowers — Logistic Regression vs Decision Tree]]></title><description><![CDATA[by Muhammad Ariel Shakaramiro
In Part 1, I covered the theory behind Supervised vs Unsupervised Learning and how to measure model performance. Now it's time to go hands-on: training an actual model th]]></description><link>https://shaka-ai.hashnode.dev/hands-on-iris-classification-logistic-regression-vs-decision-tree</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/hands-on-iris-classification-logistic-regression-vs-decision-tree</guid><category><![CDATA[Python]]></category><category><![CDATA[scikit learn]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Fri, 04 Sep 2026 03:09:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/b752e7c9-7b5c-4682-b6f4-ce316c0d9a11.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>by Muhammad Ariel Shakaramiro</em></p>
<p>In <a href="https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-machine-learning-basics">Part 1</a>, I covered the theory behind Supervised vs Unsupervised Learning and how to measure model performance. Now it's time to go hands-on: training an actual model that can recognize flower species from petal measurements, then comparing two different algorithms.</p>
<p>Every piece of code in this post has been run and verified myself — these aren't guessed numbers.</p>
<h2>Table of Contents</h2>
<ul>
<li><p><a href="#problem">Problem: Iris Species Classification</a></p>
</li>
<li><p><a href="#the-short-ml-pipeline">The Short ML Pipeline</a></p>
</li>
<li><p><a href="#1-load-the-dataset">1. Load the Dataset</a></p>
</li>
<li><p><a href="#2-split-the-data">2. Split the Data</a></p>
</li>
<li><p><a href="#3-first-model-logistic-regression">3. First Model: Logistic Regression</a></p>
</li>
<li><p><a href="#4-evaluating-model-1">4. Evaluating Model 1</a></p>
</li>
<li><p><a href="#5-second-model-decision-tree">5. Second Model: Decision Tree</a></p>
</li>
<li><p><a href="#6-evaluating-model-2">6. Evaluating Model 2</a></p>
</li>
<li><p><a href="#comparing-results">Comparing Results</a></p>
</li>
<li><p><a href="#mini-practice">Mini Practice: Effect of Changing Parameters</a></p>
</li>
<li><p><a href="#common-mistakes">Common Mistakes to Avoid</a></p>
</li>
<li><p><a href="#summary">Summary</a></p>
</li>
</ul>
<h2>Problem</h2>
<p><strong>Dataset:</strong> Iris (built into scikit-learn) <strong>Task:</strong> classify flower species — setosa / versicolor / virginica <strong>Input:</strong> sepal &amp; petal measurements (4 numeric features)</p>
<p>This is a classic <strong>Supervised Learning — Classification</strong> example (not regression), since the target is a discrete category (flower species), not a continuous number.</p>
<h2>The Short ML Pipeline</h2>
<pre><code class="language-plaintext">Data → Split → Train → Test → Predict → Evaluate
</code></pre>
<p>X = features (input to the model). y = labels (the "answer key") we're trying to predict.</p>
<h2>1. Load the Dataset</h2>
<p>The Iris dataset ships built-in with scikit-learn, so there's no manual download needed:</p>
<pre><code class="language-python">from sklearn.datasets import load_iris

iris = load_iris(as_frame=True)
df = iris.frame
df.head()
</code></pre>
<h2>2. Split the Data</h2>
<p>The data is split into a training set (for learning) and a test set (for evaluation, unseen by the model):</p>
<pre><code class="language-python">from sklearn.model_selection import train_test_split

X = df[iris.feature_names]
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
)
</code></pre>
<p>With <code>test_size=0.2</code> out of the Iris dataset's 150 total rows, the result is: <strong>120 rows for training, 30 rows for testing</strong>. The <code>stratify=y</code> parameter matters here — it makes sure each species is proportionally represented in both the train and test sets, instead of a purely random split.</p>
<h2>3. First Model: Logistic Regression</h2>
<pre><code class="language-python">from sklearn.linear_model import LogisticRegression

clf = LogisticRegression(max_iter=200)
clf.fit(X_train, y_train)
</code></pre>
<p><strong>Intuition behind how it works:</strong></p>
<ul>
<li><p>The model computes a score: <code>z = w·x + b</code></p>
</li>
<li><p>That score is converted into a probability via the sigmoid function: <code>p = sigmoid(z)</code></p>
</li>
<li><p><code>.fit()</code> searches for the best <code>w</code> and <code>b</code> values so the model's predictions get closer to the true labels (the loss goes down)</p>
</li>
</ul>
<h2>4. Evaluating Model 1</h2>
<pre><code class="language-python">from sklearn.metrics import accuracy_score

pred = clf.predict(X_test)
acc = accuracy_score(y_test, pred)
print(acc)
</code></pre>
<p><strong>Verified result: Logistic Regression accuracy = 0.9667 (96.67%)</strong></p>
<p>Out of 30 test samples, the model only got 1 wrong. Here's the confusion matrix:</p>
<pre><code class="language-python">from sklearn.metrics import ConfusionMatrixDisplay

ConfusionMatrixDisplay.from_predictions(
    y_test, pred, display_labels=iris.target_names, cmap="Blues"
)
</code></pre>
<table>
<thead>
<tr>
<th>Actual \ Predicted</th>
<th>setosa</th>
<th>versicolor</th>
<th>virginica</th>
</tr>
</thead>
<tbody><tr>
<td><strong>setosa</strong></td>
<td>10</td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td><strong>versicolor</strong></td>
<td>0</td>
<td>9</td>
<td>1</td>
</tr>
<tr>
<td><strong>virginica</strong></td>
<td>0</td>
<td>0</td>
<td>10</td>
</tr>
</tbody></table>
<p>The only mistake: 1 versicolor flower predicted as virginica. Setosa is predicted perfectly every time — which makes sense, since morphologically setosa is the easiest of the three species to tell apart.</p>
<h2>5. Second Model: Decision Tree</h2>
<pre><code class="language-python">from sklearn.tree import DecisionTreeClassifier

tree = DecisionTreeClassifier(max_depth=3, random_state=42)
tree.fit(X_train, y_train)
</code></pre>
<p><strong>Intuition behind how it works:</strong></p>
<ul>
<li><p>The tree picks questions like: <code>feature ≤ threshold?</code></p>
</li>
<li><p>Splits are chosen to "purify" the classes — impurity goes down (measured with Gini or Entropy)</p>
</li>
<li><p><code>max_depth</code> controls tree complexity: too shallow → underfitting, too deep → overfitting</p>
</li>
</ul>
<h2>6. Evaluating Model 2</h2>
<pre><code class="language-python">pred_tree = tree.predict(X_test)
acc_tree = accuracy_score(y_test, pred_tree)
print(acc_tree)
</code></pre>
<p><strong>Verified result: Decision Tree accuracy = 0.9667 (96.67%)</strong></p>
<p>Interestingly, the Decision Tree's confusion matrix is exactly identical to Logistic Regression's — both make the same single mistake, mispredicting 1 versicolor as virginica. On a dataset as clean as Iris, where the class boundaries are fairly distinct, two very different algorithms can still land on the exact same result.</p>
<h2>Comparing Results</h2>
<table>
<thead>
<tr>
<th>Model</th>
<th>Accuracy</th>
<th>Errors</th>
</tr>
</thead>
<tbody><tr>
<td>Logistic Regression</td>
<td>96.67%</td>
<td>1 out of 30</td>
</tr>
<tr>
<td>Decision Tree (max_depth=3)</td>
<td>96.67%</td>
<td>1 out of 30</td>
</tr>
</tbody></table>
<p>For this case, both are tied. That doesn't mean these two algorithms are always equivalent on other datasets, though — on more complex or non-linear data, results can differ significantly. That's exactly why the <strong>model selection</strong> process (trying several models, comparing their metrics) matters — not just picking one algorithm and stopping there.</p>
<blockquote>
<p><strong>A note on test set size</strong> <em>(added after</em> <a href="https://hashnode.com/@ahmetozel"><em>Ahmet Özel</em></a> <em>flagged this in the comments)</em>: the test set here is only 30 samples, so a single sample switching sides moves accuracy by roughly 3.3 percentage points. That means small differences between models on a dataset this size — even ones that look like a 1-2 point gap — are usually within noise, not evidence that one model is genuinely better. It's also why cross-validation (covered in Mini Practice) is more trustworthy than any single test-set number.</p>
</blockquote>
<h2>Mini Practice</h2>
<p><strong>Update:</strong> this section originally just showed the effect of changing <code>test_size</code> and <code>max_depth</code> by looking directly at test set accuracy — and there's a methodology problem there worth correcting. If we pick the "best" hyperparameter based on the test set score, that score stops being an honest estimate of performance on new data, because we've quietly "peeked" at the test set to make a decision. This is a subtler form of data leakage than just "don't train on test data" — and it's easy to miss. <em>(Credit to</em> <a href="https://hashnode.com/@ahmetozel"><em>Ahmet Özel</em></a> <em>for raising this in the comments on</em> <a href="https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-machine-learning-basics"><em>Part 1</em></a> <em>— a sharp catch worth acting on.)</em></p>
<p>The correct approach: separate the <strong>tuning</strong> process (choosing hyperparameters) from the <strong>final evaluation</strong>. Tuning happens via cross-validation inside the training set only — the test set stays untouched until the decision has already been made.</p>
<p><strong>On the</strong> <code>test_size</code> <strong>experiment (0.1 / 0.2 / 0.3):</strong> this is purely an illustration of how test set size affects the variance of the result, not something to be chosen based on which gives the highest accuracy. A smaller test set means fewer samples to measure performance on, so the number swings more from sampling luck, not because the model is actually better or worse.</p>
<table>
<thead>
<tr>
<th>test_size</th>
<th>Accuracy (Logistic Regression)</th>
</tr>
</thead>
<tbody><tr>
<td>0.1</td>
<td>93.33%</td>
</tr>
<tr>
<td>0.2</td>
<td>96.67%</td>
</tr>
<tr>
<td>0.3</td>
<td>93.33%</td>
</tr>
</tbody></table>
<p><strong>Step 1 — Tune</strong> <code>max_depth</code> <strong>using cross-validation (training set only):</strong></p>
<pre><code class="language-python">from sklearn.model_selection import StratifiedKFold, cross_val_score

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

for depth in [1, 2, 3, 5, None]:
    tree = DecisionTreeClassifier(max_depth=depth, random_state=42)
    scores = cross_val_score(tree, X_train, y_train, cv=cv)
    print(f"max_depth={depth}: CV accuracy = {scores.mean():.4f}")
</code></pre>
<table>
<thead>
<tr>
<th>max_depth</th>
<th>CV Accuracy (training set only)</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>66.67%</td>
</tr>
<tr>
<td>2</td>
<td>94.17%</td>
</tr>
<tr>
<td><strong>3</strong></td>
<td><strong>95.83%</strong></td>
</tr>
<tr>
<td>5</td>
<td>95.00%</td>
</tr>
<tr>
<td>None (unlimited)</td>
<td>95.00%</td>
</tr>
</tbody></table>
<p>These numbers haven't touched the test set at all. Based on this, <code>max_depth=3</code> is the best choice.</p>
<p><strong>Step 2 — Now touch the test set, once, for the final number:</strong></p>
<pre><code class="language-python">final_tree = DecisionTreeClassifier(max_depth=3, random_state=42)
final_tree.fit(X_train, y_train)
final_acc = accuracy_score(y_test, final_tree.predict(X_test))
print(final_acc)
</code></pre>
<p><strong>Result: 96.67%</strong> — exactly the same number reported earlier in the Decision Tree evaluation section, but this time arrived at through the correct process: the <code>max_depth=3</code> decision came purely from the training set, and the test set was only used once at the end for confirmation.</p>
<p>🤔 Guess First: why is accuracy at max_depth=1 so much lower (66.67%) compared to max_depth=3?</p>
<p>With <code>max_depth=1</code>, the tree is only allowed to make <strong>one</strong> split question before deciding. That's enough to separate setosa from the other two species (since setosa is quite distinct), but not enough to separate versicolor from virginica, whose characteristics are much more similar. The model becomes <em>underfit</em> — too simple to capture the pattern that actually exists in the data.</p>
<p>Interestingly, <code>max_depth=None</code> (letting the tree grow freely) scores the same as <code>max_depth=5</code>, and slightly below <code>max_depth=3</code> — a small early sign of <em>overfitting</em> once the tree is allowed to grow too deep.</p>
<p>🤔 Why does the correct tuning process still matter, even though the final answer happened to be the same?</p>
<p>In this case, both the correct approach (CV on the training set) and the "peeking" approach (looking at test scores) point to the same <code>max_depth=3</code>. But that's a coincidence, not a guarantee. On a different dataset, the two approaches could point to different hyperparameters. If we choose based on the test score, we'd never know whether the final accuracy is purely the model's real performance, or whether it's been quietly overfit to the test set without realizing it. Only by separating the tuning and evaluation processes can the final number be trusted as an honest estimate.</p>
<h2>Common Mistakes</h2>
<p>A few traps that commonly make ML results misleading:</p>
<ul>
<li><p><strong>Train-test leakage</strong> — this comes in two forms. The obvious one: test data leaks into the training process itself. The subtler one (and the one that's easier to miss): choosing hyperparameters, features, or a stopping point based on test set performance — once that happens, the test score is no longer valid as an honest estimate (see the full discussion in Mini Practice above)</p>
</li>
<li><p><strong>Evaluating without the right metric</strong> — using accuracy alone when the data is imbalanced (can lead to wrong conclusions — see the Precision/Recall discussion in <a href="https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-machine-learning-basics">Part 1</a>)</p>
</li>
<li><p><strong>Overfitting</strong> — the model "memorizes" the training data and fails to generalize to new data (visible in the <code>max_depth=None</code> experiment above)</p>
</li>
</ul>
<blockquote>
<p>If your model's score looks bad, check the data and your evaluation method first — don't jump straight to blaming the algorithm.</p>
</blockquote>
<h2>Summary</h2>
<ul>
<li><p>Load a built-in scikit-learn dataset → split with <code>stratify</code> → train → predict → evaluate</p>
</li>
<li><p>Logistic Regression and Decision Tree can produce the exact same accuracy (96.67%) on a dataset as simple as Iris</p>
</li>
<li><p>Parameters like <code>test_size</code> and <code>max_depth</code> have a real effect on the final result — there's no single "correct" number, it all needs experimentation</p>
</li>
<li><p>A confusion matrix gives more detailed insight than accuracy alone — in this case, the model's error consistently sits at the boundary between versicolor and virginica</p>
</li>
</ul>
<p>From an example this simple, you can see how the model learning process actually works: give examples, minimize error, test on new data, measure, then iterate. That's the essence of Supervised Learning covered in <a href="https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-machine-learning-basics">Part 1</a>.</p>
<p>In <a href="https://shaka-ai.hashnode.dev/hands-on-unsupervised-learning-clustering-iris-kmeans"><strong>Part 3</strong></a>, I continue into the side not yet covered: Unsupervised Learning — clustering the same Iris flowers, this time with no labels given at all.</p>
<hr />
<p><em>Part of my AI Engineering study notes. Read the theory in</em> <a href="https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-machine-learning-basics"><em>Part 1</em></a><em>. Continue to</em> <a href="https://shaka-ai.hashnode.dev/hands-on-unsupervised-learning-clustering-iris-kmeans"><em>Part 3</em></a> <em>for hands-on unsupervised learning — full source code on</em> <a href="https://github.com/arielshakaramiro/supervised-unsupervised-learning-praktik-arielshakaramiro"><em>GitHub</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Praktik: Klasifikasi Bunga Iris — Logistic Regression vs Decision Tree]]></title><description><![CDATA[by Muhammad Ariel Shakaramiro
Di Part 1, saya bahas teori Supervised vs Unsupervised Learning dan cara mengukur performa model. Sekarang saatnya praktik langsung: melatih model beneran yang bisa menge]]></description><link>https://shaka-ai.hashnode.dev/praktik-klasifikasi-iris-logistic-regression-vs-decision-tree</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/praktik-klasifikasi-iris-logistic-regression-vs-decision-tree</guid><category><![CDATA[Python]]></category><category><![CDATA[scikit learn]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[beginner]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Fri, 04 Sep 2026 03:04:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/90874529-d44a-4286-9de6-6476f17ff337.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>by Muhammad Ariel Shakaramiro</em></p>
<p>Di <a href="https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-dasar-machine-learning">Part 1</a>, saya bahas teori Supervised vs Unsupervised Learning dan cara mengukur performa model. Sekarang saatnya praktik langsung: melatih model beneran yang bisa mengenali spesies bunga dari ukuran kelopaknya, lalu membandingkan dua algoritma berbeda.</p>
<p>Semua kode di post ini sudah saya jalankan sendiri dan angkanya terverifikasi — bukan hasil tebakan.</p>
<h2>Daftar Isi</h2>
<ul>
<li><p><a href="#problem">Problem: Klasifikasi Spesies Iris</a></p>
</li>
<li><p><a href="#pipeline-ml-singkat">Pipeline ML Singkat</a></p>
</li>
<li><p><a href="#1-load-dataset">1. Load Dataset</a></p>
</li>
<li><p><a href="#2-split-data">2. Split Data</a></p>
</li>
<li><p><a href="#3-model-pertama-logistic-regression">3. Model Pertama: Logistic Regression</a></p>
</li>
<li><p><a href="#4-evaluasi-model-1">4. Evaluasi Model 1</a></p>
</li>
<li><p><a href="#5-model-kedua-decision-tree">5. Model Kedua: Decision Tree</a></p>
</li>
<li><p><a href="#6-evaluasi-model-2">6. Evaluasi Model 2</a></p>
</li>
<li><p><a href="#perbandingan-hasil">Perbandingan Hasil</a></p>
</li>
<li><p><a href="#mini-practice">Mini Practice: Efek Mengubah Parameter</a></p>
</li>
<li><p><a href="#kesalahan-umum">Kesalahan Umum yang Perlu Dihindari</a></p>
</li>
<li><p><a href="#ringkasan">Ringkasan</a></p>
</li>
</ul>
<h2>Problem</h2>
<p><strong>Dataset:</strong> Iris (built-in di scikit-learn) <strong>Task:</strong> klasifikasi spesies bunga — setosa / versicolor / virginica <strong>Input:</strong> ukuran sepal &amp; petal (4 fitur numerik)</p>
<p>Ini adalah contoh klasik <strong>Supervised Learning — Klasifikasi</strong> (bukan regresi), karena target-nya adalah kategori diskrit (spesies bunga), bukan angka kontinu.</p>
<h2>Pipeline ML Singkat</h2>
<pre><code class="language-plaintext">Data → Split → Train → Test → Predict → Evaluate
</code></pre>
<p>X = fitur (ciri) yang jadi input model. y = label (kunci jawaban) yang mau diprediksi.</p>
<h2>1. Load Dataset</h2>
<p>Dataset Iris sudah tersedia built-in di scikit-learn, jadi nggak perlu download manual:</p>
<pre><code class="language-python">from sklearn.datasets import load_iris

iris = load_iris(as_frame=True)
df = iris.frame
df.head()
</code></pre>
<h2>2. Split Data</h2>
<p>Data dibagi jadi training set (buat belajar) dan test set (buat diuji, model belum pernah lihat data ini):</p>
<pre><code class="language-python">from sklearn.model_selection import train_test_split

X = df[iris.feature_names]
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
)
</code></pre>
<p>Dengan <code>test_size=0.2</code> dari total 150 baris data Iris, hasilnya: <strong>120 baris untuk training, 30 baris untuk testing</strong>. Parameter <code>stratify=y</code> penting di sini — ini memastikan proporsi tiap spesies di data train dan test tetap seimbang, bukan asal random.</p>
<h2>3. Model Pertama: Logistic Regression</h2>
<pre><code class="language-python">from sklearn.linear_model import LogisticRegression

clf = LogisticRegression(max_iter=200)
clf.fit(X_train, y_train)
</code></pre>
<p><strong>Intuisi cara kerjanya:</strong></p>
<ul>
<li><p>Model hitung skor: <code>z = w·x + b</code></p>
</li>
<li><p>Skor itu diubah jadi probabilitas lewat fungsi sigmoid: <code>p = sigmoid(z)</code></p>
</li>
<li><p><code>.fit()</code> mencari nilai <code>w</code> dan <code>b</code> terbaik supaya prediksi model semakin mendekati label asli (loss-nya turun)</p>
</li>
</ul>
<h2>4. Evaluasi Model 1</h2>
<pre><code class="language-python">from sklearn.metrics import accuracy_score

pred = clf.predict(X_test)
acc = accuracy_score(y_test, pred)
print(acc)
</code></pre>
<p><strong>Hasil terverifikasi: akurasi Logistic Regression = 0.9667 (96.67%)</strong></p>
<p>Dari 30 data test, model salah memprediksi 1 saja. Confusion matrix-nya:</p>
<pre><code class="language-python">from sklearn.metrics import ConfusionMatrixDisplay

ConfusionMatrixDisplay.from_predictions(
    y_test, pred, display_labels=iris.target_names, cmap="Blues"
)
</code></pre>
<table>
<thead>
<tr>
<th>Realita \ Prediksi</th>
<th>setosa</th>
<th>versicolor</th>
<th>virginica</th>
</tr>
</thead>
<tbody><tr>
<td><strong>setosa</strong></td>
<td>10</td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td><strong>versicolor</strong></td>
<td>0</td>
<td>9</td>
<td>1</td>
</tr>
<tr>
<td><strong>virginica</strong></td>
<td>0</td>
<td>0</td>
<td>10</td>
</tr>
</tbody></table>
<p>Satu-satunya kesalahan: 1 bunga versicolor diprediksi sebagai virginica. Kelas setosa selalu terprediksi sempurna — masuk akal, karena secara morfologi setosa memang paling gampang dibedakan dari dua spesies lainnya.</p>
<h2>5. Model Kedua: Decision Tree</h2>
<pre><code class="language-python">from sklearn.tree import DecisionTreeClassifier

tree = DecisionTreeClassifier(max_depth=3, random_state=42)
tree.fit(X_train, y_train)
</code></pre>
<p><strong>Intuisi cara kerjanya:</strong></p>
<ul>
<li><p>Tree memilih pertanyaan seperti: <code>feature ≤ threshold?</code></p>
</li>
<li><p>Split dipilih untuk "memurnikan" kelas — impurity-nya turun (diukur pakai Gini atau Entropy)</p>
</li>
<li><p><code>max_depth</code> mengontrol kompleksitas tree: terlalu dangkal → underfit, terlalu dalam → overfit</p>
</li>
</ul>
<h2>6. Evaluasi Model 2</h2>
<pre><code class="language-python">pred_tree = tree.predict(X_test)
acc_tree = accuracy_score(y_test, pred_tree)
print(acc_tree)
</code></pre>
<p><strong>Hasil terverifikasi: akurasi Decision Tree = 0.9667 (96.67%)</strong></p>
<p>Menariknya, confusion matrix Decision Tree persis sama dengan Logistic Regression — sama-sama cuma salah di 1 versicolor yang diprediksi virginica. Untuk dataset sesederhana Iris yang batas antar kelasnya cukup jelas, dua algoritma dengan pendekatan berbeda bisa saja "landing" di hasil akhir yang sama.</p>
<h2>Perbandingan Hasil</h2>
<table>
<thead>
<tr>
<th>Model</th>
<th>Akurasi</th>
<th>Kesalahan</th>
</tr>
</thead>
<tbody><tr>
<td>Logistic Regression</td>
<td>96.67%</td>
<td>1 dari 30</td>
</tr>
<tr>
<td>Decision Tree (max_depth=3)</td>
<td>96.67%</td>
<td>1 dari 30</td>
</tr>
</tbody></table>
<p>Untuk kasus ini, keduanya setara. Tapi ini bukan berarti dua algoritma ini selalu setara di dataset lain — di data yang lebih kompleks atau nggak linear, hasilnya bisa jauh berbeda. Makanya proses <strong>model selection</strong> (coba beberapa model, bandingkan metriknya) itu penting, bukan cuma pakai satu algoritma dan berhenti di situ.</p>
<blockquote>
<p><strong>Catatan soal ukuran test set</strong> <em>(ditambahkan setelah</em> <a href="https://hashnode.com/@ahmetozel"><em>Ahmet Özel</em></a> <em>mengingatkan lewat komentar)</em>: test set di sini cuma 30 sampel, jadi 1 sampel yang berpindah sisi = sekitar 3.3 poin persentase akurasi. Itu artinya perbedaan kecil antar model di dataset sesederhana ini — bahkan yang kelihatan "beda" 1-2 poin — biasanya masih dalam rentang noise, bukan bukti satu model beneran lebih baik. Ini juga alasan kenapa cross-validation (dibahas di Mini Practice) lebih bisa dipercaya dibanding satu angka test tunggal.</p>
</blockquote>
<h2>Mini Practice</h2>
<p><strong>Update:</strong> section ini awalnya cuma nunjukin efek ubah <code>test_size</code> dan <code>max_depth</code> dengan lihat langsung akurasi di test set — dan ada masalah metodologi di situ yang perlu diluruskan. Kalau kita memilih hyperparameter "terbaik" berdasarkan angka test set, angka itu berhenti jadi estimasi jujur soal performa di data baru, karena kita sudah diam-diam "mengintip" test set buat bikin keputusan. Ini bentuk data leakage yang lebih halus dibanding sekadar "jangan training pakai data test" — dan gampang banget kelewat. <em>(Terima kasih ke</em> <a href="https://hashnode.com/@ahmetozel"><em>Ahmet Özel</em></a> <em>yang mengangkat poin ini lewat komentar di</em> <a href="https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-dasar-machine-learning"><em>Part 1</em></a> <em>— kritik yang tajam dan layak ditindaklanjuti.)</em></p>
<p>Cara yang benar: pisahkan proses <strong>tuning</strong> (pilih hyperparameter) dari proses <strong>evaluasi final</strong>. Tuning dilakukan pakai cross-validation di dalam training set saja — test set nggak disentuh sama sekali sampai keputusan sudah diambil.</p>
<p><strong>Soal eksperimen</strong> <code>test_size</code> <strong>(0.1 / 0.2 / 0.3):</strong> ini murni ilustrasi bagaimana ukuran test set memengaruhi variasi hasil, bukan sesuatu yang dipilih berdasarkan akurasi mana yang tertinggi. Semakin kecil test set, semakin sedikit sampel buat mengukur performa, jadi angkanya lebih gampang naik-turun karena kebetulan sampel, bukan karena modelnya beneran lebih baik atau lebih buruk.</p>
<table>
<thead>
<tr>
<th>test_size</th>
<th>Akurasi (Logistic Regression)</th>
</tr>
</thead>
<tbody><tr>
<td>0.1</td>
<td>93.33%</td>
</tr>
<tr>
<td>0.2</td>
<td>96.67%</td>
</tr>
<tr>
<td>0.3</td>
<td>93.33%</td>
</tr>
</tbody></table>
<p><strong>Langkah 1 — Tuning</strong> <code>max_depth</code> <strong>pakai cross-validation (cuma di training set):</strong></p>
<pre><code class="language-python">from sklearn.model_selection import StratifiedKFold, cross_val_score

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

for depth in [1, 2, 3, 5, None]:
    tree = DecisionTreeClassifier(max_depth=depth, random_state=42)
    scores = cross_val_score(tree, X_train, y_train, cv=cv)
    print(f"max_depth={depth}: CV accuracy = {scores.mean():.4f}")
</code></pre>
<table>
<thead>
<tr>
<th>max_depth</th>
<th>CV Accuracy (training set saja)</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>66.67%</td>
</tr>
<tr>
<td>2</td>
<td>94.17%</td>
</tr>
<tr>
<td><strong>3</strong></td>
<td><strong>95.83%</strong></td>
</tr>
<tr>
<td>5</td>
<td>95.00%</td>
</tr>
<tr>
<td>None (tanpa batas)</td>
<td>95.00%</td>
</tr>
</tbody></table>
<p>Angka-angka ini sama sekali belum menyentuh test set. Berdasarkan ini, <code>max_depth=3</code> adalah pilihan terbaik.</p>
<p><strong>Langkah 2 — Baru sentuh test set, sekali, buat angka final:</strong></p>
<pre><code class="language-python">final_tree = DecisionTreeClassifier(max_depth=3, random_state=42)
final_tree.fit(X_train, y_train)
final_acc = accuracy_score(y_test, final_tree.predict(X_test))
print(final_acc)
</code></pre>
<p><strong>Hasil: 96.67%</strong> — persis sama dengan angka yang saya laporkan di bagian evaluasi Decision Tree sebelumnya, tapi sekarang didapat lewat proses yang benar: keputusan <code>max_depth=3</code> murni berasal dari training set, dan test set cuma dipakai sekali di akhir buat konfirmasi.</p>
<p>🤔 Coba Tebak Dulu: kenapa akurasi di max_depth=1 jauh lebih rendah (66.67%) dibanding max_depth=3?</p>
<p>Dengan <code>max_depth=1</code>, tree cuma boleh bikin <strong>satu</strong> pertanyaan split sebelum harus memutuskan. Itu cukup buat misahin setosa dari dua spesies lain (karena setosa emang beda jauh), tapi nggak cukup buat misahin versicolor dari virginica yang ciri-cirinya lebih mirip. Model jadi <em>underfit</em> — terlalu simpel buat menangkap pola yang sebenarnya ada di data.</p>
<p>Menariknya, <code>max_depth=None</code> (tree dibiarkan tumbuh sebebas-bebasnya) malah CV accuracy-nya sama dengan <code>max_depth=5</code>, dan sedikit di bawah <code>max_depth=3</code> — tanda kecil <em>overfitting</em> mulai muncul begitu tree dibiarkan terlalu dalam.</p>
<p>🤔 Kenapa proses tuning yang benar tetap penting, padahal hasil akhirnya kebetulan sama?</p>
<p>Di kasus ini, baik cara yang benar (CV di training set) maupun cara yang "mengintip" test set sama-sama menunjuk ke <code>max_depth=3</code>. Tapi itu kebetulan, bukan jaminan. Di dataset lain, dua pendekatan ini bisa saja menunjuk ke hyperparameter yang berbeda. Kalau kita memilih berdasarkan angka test, kita nggak akan pernah tahu apakah akurasi final itu murni performa model, atau sudah diam-diam "disesuaikan" ke test set tanpa sadar. Cuma dengan memisahkan proses tuning dan evaluasi, angka akhir bisa dipercaya sebagai estimasi yang jujur.</p>
<h2>Kesalahan Umum</h2>
<p>Beberapa jebakan yang sering bikin hasil ML jadi menyesatkan:</p>
<ul>
<li><p><strong>Train-test leakage</strong> — ada dua bentuk. Yang jelas: data test ikut "bocor" ke proses training. Yang lebih halus (dan lebih sering kejadian tanpa disadari): memilih hyperparameter, fitur, atau titik berhenti training berdasarkan performa test set — begitu itu terjadi, angka test-nya nggak lagi valid sebagai estimasi jujur (lihat pembahasan lengkap di Mini Practice di atas)</p>
</li>
<li><p><strong>Evaluasi tanpa metrik yang tepat</strong> — cuma pakai accuracy padahal datanya imbalanced (bisa bikin salah kesimpulan, lihat lagi bahasan Precision/Recall di <a href="https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-dasar-machine-learning">Part 1</a>)</p>
</li>
<li><p><strong>Overfitting</strong> — model terlalu "hapal" data training, gagal generalisasi ke data baru (kelihatan di eksperimen <code>max_depth=None</code> di atas)</p>
</li>
</ul>
<blockquote>
<p>Kalau skor model jelek, cek dulu datanya dan cara evaluasinya — jangan buru-buru nyalahin algoritmanya.</p>
</blockquote>
<h2>Ringkasan</h2>
<ul>
<li><p>Load dataset built-in scikit-learn → split dengan <code>stratify</code> → train → predict → evaluate</p>
</li>
<li><p>Logistic Regression dan Decision Tree bisa menghasilkan akurasi yang sama persis (96.67%) di dataset sesederhana Iris</p>
</li>
<li><p>Parameter seperti <code>test_size</code> dan <code>max_depth</code> punya efek nyata ke hasil akhir — nggak ada angka "pasti benar," semua perlu dieksperimenkan</p>
</li>
<li><p>Confusion matrix kasih insight lebih detail dibanding accuracy doang — di kasus ini, kesalahan modelnya konsisten selalu di batas antara versicolor dan virginica</p>
</li>
</ul>
<p>Dari contoh sesederhana ini, kelihatan gimana proses belajar model sebenarnya bekerja: kasih contoh, minimalkan error, uji ke data baru, ukur, lalu iterasi. Itulah inti dari Supervised Learning yang dibahas teorinya di <a href="https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-dasar-machine-learning">Part 1</a>.</p>
<p>Di <a href="https://shaka-ai.hashnode.dev/praktik-unsupervised-learning-clustering-iris-kmeans"><strong>Part 3</strong></a>, saya lanjutkan ke sisi yang belum disentuh: Unsupervised Learning — clustering bunga Iris yang sama, kali ini tanpa dikasih label sama sekali.</p>
<hr />
<p><em>Bagian dari catatan belajar AI Engineering saya. Baca teorinya di</em> <a href="https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-dasar-machine-learning"><em>Part 1</em></a><em>. Lanjut ke</em> <a href="https://shaka-ai.hashnode.dev/praktik-unsupervised-learning-clustering-iris-kmeans"><em>Part 3</em></a> <em>untuk praktik unsupervised learning — source code lengkap ada di</em> <a href="https://github.com/arielshakaramiro/supervised-unsupervised-learning-praktik-arielshakaramiro"><em>GitHub</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Machine Learning from Scratch: Supervised vs Unsupervised Learning (and How to Tell If a Model Actually Learned)]]></title><description><![CDATA[by Muhammad Ariel Shakaramiro
Whenever people hear "Machine Learning," the mental image often jumps straight to robots or AI that "thinks for itself." The reality is much simpler: a computer learns pa]]></description><link>https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-machine-learning-basics</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-machine-learning-basics</guid><category><![CDATA[Machine Learning]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[Supervised learning]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Fri, 04 Sep 2026 03:00:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/d92a4f07-3a9f-48d1-87c0-f7bdf438f4d0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>by Muhammad Ariel Shakaramiro</em></p>
<p>Whenever people hear "Machine Learning," the mental image often jumps straight to robots or AI that "thinks for itself." The reality is much simpler: a computer learns patterns from data, without us writing every rule by hand.</p>
<p>This post is my study notes on the two foundational approaches in Machine Learning — <strong>Supervised</strong> and <strong>Unsupervised Learning</strong> — plus how to actually measure whether a trained model is good, or just memorizing. Part 2 of this series goes hands-on: building a real classifier using the Iris dataset.</p>
<h2>Table of Contents</h2>
<ul>
<li><p><a href="#what-is-machine-learning">What is Machine Learning?</a></p>
</li>
<li><p><a href="#the-big-picture-ml-process">The Big-Picture ML Process</a></p>
</li>
<li><p><a href="#4-categories-of-ml">4 Categories of ML</a></p>
</li>
<li><p><a href="#supervised-learning">Supervised Learning</a></p>
</li>
<li><p><a href="#unsupervised-learning">Unsupervised Learning</a></p>
</li>
<li><p><a href="#strategic-comparison-table">Strategic Comparison Table</a></p>
</li>
<li><p><a href="#model-evaluation">Model Evaluation: How Do We Know a Model Is Good?</a></p>
</li>
<li><p><a href="#real-world-business-case-studies">Real-World Business Case Studies</a></p>
</li>
<li><p><a href="#quick-recall-cheat-sheet">Quick Recall Cheat Sheet</a></p>
</li>
<li><p><a href="#summary">Summary &amp; Next: Part 2</a></p>
</li>
</ul>
<h2>What is Machine Learning?</h2>
<p>Machine Learning (ML) is a branch of Artificial Intelligence that lets computers learn automatically from data — without being explicitly programmed for every case.</p>
<p>With ML, systems can:</p>
<ul>
<li><p>Recognize patterns in historical data</p>
</li>
<li><p>Make predictions on new data</p>
</li>
<li><p>Make decisions based on experience rather than hardcoded rules</p>
</li>
</ul>
<p>Everyday examples: e-commerce product recommendations, face detection on your phone, AI-assisted medical diagnosis, weather prediction systems.</p>
<h2>The Big-Picture ML Process</h2>
<p>Almost every ML project — no matter how simple — follows the same flow:</p>
<pre><code class="language-plaintext">Data Collection &amp; Preparation → Model Training → Model Evaluation → Prediction / Deployment
</code></pre>
<ol>
<li><p><strong>Data Collection &amp; Preparation</strong> — gather and clean data so it's ready to use</p>
</li>
<li><p><strong>Model Training</strong> — train the algorithm to understand relationships between features</p>
</li>
<li><p><strong>Model Evaluation</strong> — measure model performance with specific metrics</p>
</li>
<li><p><strong>Prediction / Deployment</strong> — apply the model to new data to produce real predictions</p>
</li>
</ol>
<h2>4 Categories of ML</h2>
<p>Based on <em>how they learn</em>, ML splits into four major categories:</p>
<table>
<thead>
<tr>
<th>Category</th>
<th>How It Learns</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Supervised Learning</strong></td>
<td>Learns from labeled data</td>
</tr>
<tr>
<td><strong>Semi-Supervised Learning</strong></td>
<td>Combination of labeled and unlabeled data</td>
</tr>
<tr>
<td><strong>Unsupervised Learning</strong></td>
<td>Learns from data with no labels at all</td>
</tr>
<tr>
<td><strong>Reinforcement Learning</strong></td>
<td>Learns from experience through reward &amp; penalty</td>
</tr>
</tbody></table>
<p>This note focuses on the two most commonly encountered when starting out with ML: <strong>Supervised</strong> and <strong>Unsupervised Learning</strong>.</p>
<p>🤔 Guess First: if your data has no "answer key" at all, which category does it fall under?</p>
<p><strong>Unsupervised Learning.</strong> If there's no known label/target output, the model has to find patterns on its own from the structure of the data — not from a "correct answer" it was given.</p>
<h2>Supervised Learning</h2>
<p><strong>Concept:</strong> the model is trained using data that already has labels. Every input has a known output, so the algorithm can learn the relationship between the two to produce accurate predictions on new data.</p>
<p><strong>Analogy:</strong> similar to how a human learns under a teacher's (supervisor's) guidance — the teacher provides example questions with answers, and the student learns the pattern.</p>
<p><strong>Characteristics:</strong></p>
<ul>
<li><p>Training data has labels or targets</p>
</li>
<li><p>The model learns by comparing its predictions against the true labels (<em>error-based learning</em>)</p>
</li>
</ul>
<p><strong>2 Main Problem Types:</strong></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Definition</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Classification</strong></td>
<td>Predicts a discrete category</td>
<td>Spam / not spam</td>
</tr>
<tr>
<td><strong>Regression</strong></td>
<td>Predicts a continuous value</td>
<td>House price, temperature</td>
</tr>
</tbody></table>
<p><strong>Workflow:</strong></p>
<pre><code class="language-plaintext">Labelled Data → Algorithms (trained with Training Data + Desired Output, under Supervisor Intervention) → Process → Output
</code></pre>
<p><strong>Example algorithms:</strong></p>
<ul>
<li><p><em>Classification:</em> Support Vector Machines, Discriminant Analysis, Naive Bayes, Nearest Neighbor</p>
</li>
<li><p><em>Regression:</em> Linear Regression/GLM, SVR/GPR, Ensemble Methods, Decision Trees, Neural Networks</p>
</li>
</ul>
<h2>Unsupervised Learning</h2>
<p><strong>Concept:</strong> used to analyze and discover hidden patterns in data that has <em>no</em> labels. The algorithm automatically groups data based on similarity or structure — without human guidance on "what group this belongs to."</p>
<p><strong>Analogy:</strong> like someone trying to understand something with no direct instructions — just by observing and finding patterns in the information available.</p>
<p><strong>Characteristics:</strong></p>
<ul>
<li><p>Training data has no labels or target output</p>
</li>
<li><p>The model tries to find relationships, similarities, or hidden patterns</p>
</li>
</ul>
<p><strong>2 Main Problem Types:</strong></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Definition</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Clustering</strong></td>
<td>Grouping data based on similarity</td>
<td>Customer segmentation</td>
</tr>
<tr>
<td><strong>Dimensional Reduction</strong></td>
<td>Simplifying features without losing important information</td>
<td>PCA</td>
</tr>
</tbody></table>
<p><strong>Workflow:</strong></p>
<pre><code class="language-plaintext">Raw Data → Interpretation → Algorithms → Process → Output
</code></pre>
<p><strong>Example clustering algorithms:</strong> K-Means, K-Medoids, Fuzzy C-Means, Hierarchical, Gaussian Mixture, Neural Networks, Hidden Markov Model</p>
<h2>Strategic Comparison Table</h2>
<table>
<thead>
<tr>
<th>Aspect</th>
<th>Supervised Learning</th>
<th>Unsupervised Learning</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Definition</strong></td>
<td>Learns from LABELED data, mapping input to known output</td>
<td>Learns from UNLABELED data, finding hidden patterns/structure</td>
</tr>
<tr>
<td><strong>Goal</strong></td>
<td>Predict outcomes for new data based on learned examples</td>
<td>Explore data to discover inherent groupings/relationships</td>
</tr>
<tr>
<td><strong>Applications</strong></td>
<td>Spam filtering, image classification, price prediction</td>
<td>Customer segmentation, anomaly detection, recommendation systems</td>
</tr>
<tr>
<td><strong>Data</strong></td>
<td>Labeled data (input + output)</td>
<td>Unlabeled data (input only)</td>
</tr>
<tr>
<td><strong>Output</strong></td>
<td>Predictive model (classifier or regressor)</td>
<td>Descriptive model (clusters, rules, or embeddings)</td>
</tr>
<tr>
<td><strong>Main Challenge</strong></td>
<td>High labeling cost</td>
<td>Interpretability &amp; validation without ground truth</td>
</tr>
<tr>
<td><strong>Main Risk</strong></td>
<td>Overfitting — model memorizes training data</td>
<td>Patterns that are mathematically valid but not business-relevant</td>
</tr>
</tbody></table>
<h2>Model Evaluation</h2>
<p>Having a model isn't enough — we need to know how good it actually is, and more importantly: whether it's only good on training data, or genuinely reliable on new data.</p>
<p>Evaluation works by comparing the model's predictions against <strong>ground truth</strong> (the actual outcome).</p>
<h3>Confusion Matrix</h3>
<p>A Confusion Matrix compares the model's predictions against reality, producing four possible outcomes:</p>
<table>
<thead>
<tr>
<th></th>
<th>Predicted: Yes</th>
<th>Predicted: No</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Actual: Yes</strong></td>
<td>True Positive (TP)</td>
<td>False Negative (FN)</td>
</tr>
<tr>
<td><strong>Actual: No</strong></td>
<td>False Positive (FP)</td>
<td>True Negative (TN)</td>
</tr>
</tbody></table>
<p>Forest fire detection example:</p>
<ul>
<li><p><strong>TP</strong> — actually a fire, predicted fire ✅</p>
</li>
<li><p><strong>TN</strong> — actually no fire, predicted no fire ✅</p>
</li>
<li><p><strong>FP</strong> — actually no fire, but predicted fire ❌ (<em>Type I Error</em>)</p>
</li>
<li><p><strong>FN</strong> — actually a fire, but predicted no fire ❌ (<em>Type II Error</em>, usually the more dangerous one)</p>
</li>
</ul>
<h3>Accuracy, Precision, Recall</h3>
<p><strong>Accuracy</strong> — percentage of correct predictions out of all observations:</p>
<pre><code class="language-plaintext">Accuracy = (TP + TN) / (TP + FP + TN + FN)
</code></pre>
<p><strong>Precision</strong> — of everything predicted positive, how much was actually positive:</p>
<pre><code class="language-plaintext">Precision = TP / (TP + FP)
</code></pre>
<p><strong>Recall</strong> — of everything actually positive, how much did the model successfully catch:</p>
<pre><code class="language-plaintext">Recall = TP / (TP + FN)
</code></pre>
<p>Quick way to remember: <strong>Precision</strong> is about "how exact am I when I say positive" (avoiding FP). <strong>Recall</strong> is about "how complete am I at catching everything that's actually positive" (avoiding FN).</p>
<p>🤔 Guess First: for cancer detection, which should be prioritized — Precision or Recall?</p>
<p><strong>Recall.</strong> A False Negative (patient is sick but predicted healthy) is far more dangerous than a False Positive (patient is healthy but predicted sick, then re-checked). Better to "over-suspect" than to miss a real case.</p>
<h3>Model Selection</h3>
<p>Several models are tried on the same input data, then each is evaluated using relevant metrics (Model 1 → Metrics 1, Model 2 → Metrics 2, etc.). This evaluation result becomes the basis for choosing the best model to deploy.</p>
<h2>Real-World Business Case Studies</h2>
<p><strong>Supervised — Classification:</strong></p>
<ul>
<li><p><em>Banking fraud detection:</em> historical transaction data labeled "fraud"/"not fraud" is used to predict new transactions. Impact: prevents financial loss in real time.</p>
</li>
<li><p><em>Content moderation:</em> classifying user-uploaded content as "acceptable" vs "policy violation," based on data previously labeled by moderators. Impact: maintains platform quality without manual review of everything.</p>
</li>
</ul>
<p><strong>Supervised — Regression:</strong></p>
<ul>
<li><em>Property price prediction:</em> based on land size, location, number of rooms, building age → continuous numeric output (price). Impact: helps sellers/buyers set fair prices.</li>
</ul>
<p><strong>Unsupervised — Clustering:</strong></p>
<ul>
<li><p><em>Customer segmentation:</em> with no "type A/B/C" label from the start, the model groups customers into segments ("budget shoppers," "high-value loyal customers") based on spending patterns. Impact: more targeted campaigns.</p>
</li>
<li><p><em>Cybersecurity anomaly detection:</em> monitoring login/traffic patterns without an explicit "normal/attack" label — patterns that deviate significantly are flagged as potential threats. Impact: faster prevention of data breaches.</p>
</li>
</ul>
<p><strong>Precision vs Recall in real business:</strong></p>
<ul>
<li><p><em>Credit card fraud</em> needs balance (usually via F1-Score) — low Precision means many good customers complain about blocked transactions; low Recall means a lot of fraud slips through.</p>
</li>
<li><p><em>Product recommendation systems</em> — multiple models (collaborative filtering, content-based, hybrid) are tried, evaluated using metrics like precision@k, then the best one gets deployed.</p>
</li>
</ul>
<h2>Quick Recall Cheat Sheet</h2>
<p>📋 Click to reveal the quick memorization guide</p>
<ul>
<li><p><strong>Supervised = HAS A TEACHER</strong> → has labels, has a supervisor, goal is prediction</p>
</li>
<li><p><strong>Unsupervised = NO TEACHER</strong> → no labels, finds patterns on its own, goal is exploration</p>
</li>
<li><p><strong>Classification</strong> = category (discrete) | <strong>Regression</strong> = number (continuous)</p>
</li>
<li><p><strong>Clustering</strong> = similar groups | <strong>Dimensional Reduction</strong> = simplify features</p>
</li>
<li><p>The keyword <strong>"label"</strong> is the main distinguisher between Supervised and Unsupervised</p>
</li>
<li><p>Confusion Matrix: True/False = correct/incorrect prediction, Positive/Negative = what was predicted</p>
</li>
<li><p><strong>Precision</strong> = "of what I said was positive, how much was actually positive?"</p>
</li>
<li><p><strong>Recall</strong> = "of what was actually positive, how much did I manage to catch?"</p>
</li>
</ul>
<h2>Checklist Before Moving to Practice</h2>
<ul>
<li><p>[ ] Understand the difference between labeled vs unlabeled data</p>
</li>
<li><p>[ ] Can distinguish when to use classification vs regression</p>
</li>
<li><p>[ ] Can distinguish when to use clustering vs dimensional reduction</p>
</li>
<li><p>[ ] Understand why Recall matters more in medical cases, and why both matter in fraud cases</p>
</li>
<li><p>[ ] Understand the TP/TN/FP/FN terms in a confusion matrix</p>
</li>
</ul>
<h2>Summary</h2>
<p>Supervised Learning learns from examples that already have answers — a good fit when the goal is prediction. Unsupervised Learning learns with no answers at all — a good fit when the goal is exploration and discovering structure we didn't already know about.</p>
<p>But theory alone isn't enough to really understand ML. This series continues with <a href="https://shaka-ai.hashnode.dev/hands-on-iris-classification-logistic-regression-vs-decision-tree"><strong>Part 2</strong></a> (hands-on Iris classification — Logistic Regression vs Decision Tree) and <a href="https://shaka-ai.hashnode.dev/hands-on-unsupervised-learning-clustering-iris-kmeans"><strong>Part 3</strong></a> (hands-on clustering with no labels using KMeans, plus follow-up experiments), complete with source code.</p>
<hr />
<p><em>Part of my AI Engineering study notes.</em> <a href="https://shaka-ai.hashnode.dev/hands-on-iris-classification-logistic-regression-vs-decision-tree"><em>Part 2</em></a><em>: hands-on Iris classification.</em> <a href="https://shaka-ai.hashnode.dev/hands-on-unsupervised-learning-clustering-iris-kmeans"><em>Part 3</em></a><em>: hands-on unsupervised learning — full source code on</em> <a href="https://github.com/arielshakaramiro/supervised-unsupervised-learning-praktik-arielshakaramiro"><em>GitHub</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Machine Learning dari Nol: Supervised vs Unsupervised Learning (dan Cara Tahu Model Beneran "Belajar")]]></title><description><![CDATA[by Muhammad Ariel Shakaramiro
Setiap kali orang dengar "Machine Learning," bayangannya sering langsung lompat ke robot atau AI yang "mikir sendiri." Padahal intinya jauh lebih sederhana: komputer bela]]></description><link>https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-dasar-machine-learning</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/supervised-vs-unsupervised-learning-dasar-machine-learning</guid><category><![CDATA[Machine Learning]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[Supervised learning]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Fri, 04 Sep 2026 02:58:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/e495f3bc-7eef-4e86-802c-c5289ab811c7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>by Muhammad Ariel Shakaramiro</em></p>
<p>Setiap kali orang dengar "Machine Learning," bayangannya sering langsung lompat ke robot atau AI yang "mikir sendiri." Padahal intinya jauh lebih sederhana: komputer belajar pola dari data, tanpa kita tulis semua aturannya satu-satu secara manual.</p>
<p>Post ini adalah catatan belajar saya soal dua pendekatan dasar dalam Machine Learning — <strong>Supervised</strong> dan <strong>Unsupervised Learning</strong> — plus cara mengukur apakah model yang kita latih itu beneran bagus atau cuma menghafal. Part 2 dari seri ini akan masuk ke praktik langsung: bikin classifier beneran pakai dataset Iris.</p>
<h2>Daftar Isi</h2>
<ul>
<li><p><a href="#apa-itu-machine-learning">Apa itu Machine Learning?</a></p>
</li>
<li><p><a href="#alur-besar-proses-ml">Alur Besar Proses ML</a></p>
</li>
<li><p><a href="#4-kategori-ml">4 Kategori ML</a></p>
</li>
<li><p><a href="#supervised-learning">Supervised Learning</a></p>
</li>
<li><p><a href="#unsupervised-learning">Unsupervised Learning</a></p>
</li>
<li><p><a href="#tabel-perbandingan-strategis">Tabel Perbandingan Strategis</a></p>
</li>
<li><p><a href="#model-evaluation">Model Evaluation: Gimana Cara Tahu Model Kita Bagus?</a></p>
</li>
<li><p><a href="#studi-kasus-nyata">Studi Kasus Nyata di Dunia Kerja</a></p>
</li>
<li><p><a href="#cheat-sheet-menghafal">Cheat Sheet Menghafal</a></p>
</li>
<li><p><a href="#ringkasan">Ringkasan &amp; Lanjut ke Part 2</a></p>
</li>
</ul>
<h2>Apa itu Machine Learning?</h2>
<p>Machine Learning (ML) adalah cabang dari Artificial Intelligence yang memungkinkan komputer belajar secara otomatis dari data — tanpa perlu diprogram secara eksplisit untuk setiap kasus.</p>
<p>Dengan ML, sistem bisa:</p>
<ul>
<li><p>Mengenali pola dari data historis</p>
</li>
<li><p>Membuat prediksi terhadap data baru</p>
</li>
<li><p>Mengambil keputusan berdasarkan pengalaman, bukan aturan hardcode</p>
</li>
</ul>
<p>Contoh yang paling gampang ditemuin sehari-hari: rekomendasi produk di e-commerce, deteksi wajah di kamera HP, diagnosis medis berbantuan AI, sampai prediksi cuaca.</p>
<h2>Alur Besar Proses ML</h2>
<p>Hampir semua proyek ML — sesederhana apa pun — mengikuti alur yang sama:</p>
<pre><code class="language-plaintext">Data Collection &amp; Preparation → Model Training → Model Evaluation → Prediction / Deployment
</code></pre>
<ol>
<li><p><strong>Data Collection &amp; Preparation</strong> — kumpulin dan bersihin data biar siap dipakai</p>
</li>
<li><p><strong>Model Training</strong> — latih algoritma supaya paham hubungan antar fitur</p>
</li>
<li><p><strong>Model Evaluation</strong> — ukur performa model pakai metrik tertentu</p>
</li>
<li><p><strong>Prediction / Deployment</strong> — pakai model ke data baru buat hasilkan prediksi nyata</p>
</li>
</ol>
<h2>4 Kategori ML</h2>
<p>Berdasarkan <em>cara belajarnya</em>, ML dibagi jadi 4 kategori besar:</p>
<table>
<thead>
<tr>
<th>Kategori</th>
<th>Cara Belajar</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Supervised Learning</strong></td>
<td>Belajar dari data yang sudah berlabel</td>
</tr>
<tr>
<td><strong>Semi-Supervised Learning</strong></td>
<td>Gabungan data berlabel + tidak berlabel</td>
</tr>
<tr>
<td><strong>Unsupervised Learning</strong></td>
<td>Belajar dari data tanpa label sama sekali</td>
</tr>
<tr>
<td><strong>Reinforcement Learning</strong></td>
<td>Belajar dari pengalaman lewat reward &amp; penalty</td>
</tr>
</tbody></table>
<p>Fokus catatan ini ada di dua yang paling sering ditemuin di awal belajar ML: <strong>Supervised</strong> dan <strong>Unsupervised Learning</strong>.</p>
<p>🤔 Coba Tebak Dulu: kalau data kamu nggak punya "jawaban" sama sekali, itu masuk kategori yang mana?</p>
<p>Jawabannya <strong>Unsupervised Learning</strong>. Kalau nggak ada label/target output yang diketahui, model harus cari pola sendiri dari struktur data — bukan dari "jawaban benar" yang dikasih ke dia.</p>
<h2>Supervised Learning</h2>
<p><strong>Konsep:</strong> model dilatih pakai data yang sudah punya label. Tiap data input punya output yang diketahui, jadi algoritma bisa belajar hubungan antara input dan output tersebut untuk menghasilkan prediksi akurat pada data baru.</p>
<p><strong>Analogi:</strong> mirip proses belajar manusia di bawah bimbingan guru (supervisor) — guru kasih contoh soal beserta jawabannya, murid belajar polanya.</p>
<p><strong>Ciri-ciri:</strong></p>
<ul>
<li><p>Data training punya label atau target</p>
</li>
<li><p>Model belajar dengan cara membandingkan prediksi terhadap label asli (<em>error-based learning</em>)</p>
</li>
</ul>
<p><strong>2 Jenis Masalah Utama:</strong></p>
<table>
<thead>
<tr>
<th>Jenis</th>
<th>Definisi</th>
<th>Contoh</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Klasifikasi</strong></td>
<td>Memprediksi kategori diskrit</td>
<td>Spam / not spam</td>
</tr>
<tr>
<td><strong>Regresi</strong></td>
<td>Memprediksi nilai kontinu</td>
<td>Harga rumah, suhu</td>
</tr>
</tbody></table>
<p><strong>Alur kerja:</strong></p>
<pre><code class="language-plaintext">Labelled Data → Algorithms (dilatih dengan Training Data + Desired Output, di bawah Supervisor Intervention) → Process → Output
</code></pre>
<p><strong>Contoh algoritma:</strong></p>
<ul>
<li><p><em>Classification:</em> Support Vector Machines, Discriminant Analysis, Naive Bayes, Nearest Neighbor</p>
</li>
<li><p><em>Regression:</em> Linear Regression/GLM, SVR/GPR, Ensemble Methods, Decision Trees, Neural Networks</p>
</li>
</ul>
<h2>Unsupervised Learning</h2>
<p><strong>Konsep:</strong> dipakai untuk menganalisis dan menemukan pola tersembunyi dari data yang <em>tidak</em> punya label. Algoritma secara otomatis mengelompokkan data berdasarkan kemiripan atau struktur tertentu — tanpa bantuan manusia buat kasih tahu "ini kelompok apa."</p>
<p><strong>Analogi:</strong> mirip orang yang coba memahami sesuatu tanpa petunjuk langsung — cuma dengan mengamati dan mencari pola dari informasi yang ada.</p>
<p><strong>Ciri-ciri:</strong></p>
<ul>
<li><p>Data training tidak punya label atau target output</p>
</li>
<li><p>Model berusaha mencari hubungan, kesamaan, atau pola tersembunyi</p>
</li>
</ul>
<p><strong>2 Jenis Masalah Utama:</strong></p>
<table>
<thead>
<tr>
<th>Jenis</th>
<th>Definisi</th>
<th>Contoh</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Clustering</strong></td>
<td>Pengelompokan data berdasarkan kemiripan</td>
<td>Segmentasi pelanggan</td>
</tr>
<tr>
<td><strong>Dimensional Reduction</strong></td>
<td>Menyederhanakan fitur tanpa kehilangan info penting</td>
<td>PCA</td>
</tr>
</tbody></table>
<p><strong>Alur kerja:</strong></p>
<pre><code class="language-plaintext">Raw Data → Interpretation → Algorithms → Process → Output
</code></pre>
<p><strong>Contoh algoritma clustering:</strong> K-Means, K-Medoids, Fuzzy C-Means, Hierarchical, Gaussian Mixture, Neural Networks, Hidden Markov Model</p>
<h2>Tabel Perbandingan Strategis</h2>
<table>
<thead>
<tr>
<th>Aspek</th>
<th>Supervised Learning</th>
<th>Unsupervised Learning</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Definisi</strong></td>
<td>Belajar dari data BERLABEL, memetakan input ke output yang diketahui</td>
<td>Belajar dari data TANPA LABEL, mencari pola/struktur tersembunyi</td>
</tr>
<tr>
<td><strong>Tujuan</strong></td>
<td>Memprediksi hasil untuk data baru berdasarkan contoh yang sudah dipelajari</td>
<td>Mengeksplorasi data untuk menemukan pengelompokan/relasi yang inheren</td>
</tr>
<tr>
<td><strong>Aplikasi</strong></td>
<td>Spam filtering, klasifikasi gambar, prediksi harga</td>
<td>Segmentasi customer, deteksi anomali, sistem rekomendasi</td>
</tr>
<tr>
<td><strong>Data</strong></td>
<td>Data berlabel (input + output)</td>
<td>Data tanpa label (hanya input)</td>
</tr>
<tr>
<td><strong>Output</strong></td>
<td>Model prediktif (classifier atau regressor)</td>
<td>Model deskriptif (cluster, rules, atau embeddings)</td>
</tr>
<tr>
<td><strong>Tantangan Utama</strong></td>
<td>Biaya labeling tinggi</td>
<td>Interpretability &amp; validasi tanpa ground truth</td>
</tr>
<tr>
<td><strong>Risiko Utama</strong></td>
<td>Overfitting — model menghafal data training</td>
<td>Pola valid secara matematis tapi bisa tidak relevan untuk bisnis</td>
</tr>
</tbody></table>
<h2>Model Evaluation</h2>
<p>Punya model aja nggak cukup — kita harus tahu seberapa bagus model itu, dan lebih penting lagi: apakah dia cuma bagus di data training, atau beneran bisa diandalkan di data baru.</p>
<p>Evaluasi dilakukan dengan membandingkan hasil prediksi model dengan <strong>ground truth</strong> (data sebenarnya).</p>
<h3>Confusion Matrix</h3>
<p>Confusion Matrix membandingkan hasil prediksi model dengan realita, menghasilkan 4 kemungkinan:</p>
<table>
<thead>
<tr>
<th></th>
<th>Prediksi: Yes</th>
<th>Prediksi: No</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Realita: Yes</strong></td>
<td>True Positive (TP)</td>
<td>False Negative (FN)</td>
</tr>
<tr>
<td><strong>Realita: No</strong></td>
<td>False Positive (FP)</td>
<td>True Negative (TN)</td>
</tr>
</tbody></table>
<p>Contoh kasus deteksi kebakaran hutan:</p>
<ul>
<li><p><strong>TP</strong> — realita ada api, diprediksi ada api ✅</p>
</li>
<li><p><strong>TN</strong> — realita tidak ada api, diprediksi tidak ada api ✅</p>
</li>
<li><p><strong>FP</strong> — realita tidak ada api, tapi diprediksi ada api ❌ (<em>Type I Error</em>)</p>
</li>
<li><p><strong>FN</strong> — realita ada api, tapi diprediksi tidak ada api ❌ (<em>Type II Error</em>, biasanya paling berbahaya)</p>
</li>
</ul>
<h3>Accuracy, Precision, Recall</h3>
<p><strong>Accuracy</strong> — persentase prediksi yang benar dari semua pengamatan:</p>
<pre><code class="language-plaintext">Akurasi = (TP + TN) / (TP + FP + TN + FN)
</code></pre>
<p><strong>Precision</strong> — dari semua yang diprediksi positif, berapa persen yang beneran positif:</p>
<pre><code class="language-plaintext">Presisi = TP / (TP + FP)
</code></pre>
<p><strong>Recall</strong> — dari semua yang beneran positif, berapa persen yang berhasil ditangkap model:</p>
<pre><code class="language-plaintext">Recall = TP / (TP + FN)
</code></pre>
<p>Cara gampang bedain: <strong>Presisi</strong> fokus ke "seberapa tepat aku pas bilang positif" (menghindari FP). <strong>Recall</strong> fokus ke "seberapa lengkap aku nangkep semua yang beneran positif" (menghindari FN).</p>
<p>🤔 Coba Tebak Dulu: buat deteksi penyakit kanker, mana yang harus lebih diprioritaskan — Precision atau Recall?</p>
<p><strong>Recall.</strong> False Negative (pasien sakit tapi diprediksi sehat) jauh lebih berbahaya daripada False Positive (pasien sehat tapi diprediksi sakit, lalu dicek ulang). Lebih baik "curiga berlebihan" daripada melewatkan kasus nyata.</p>
<h3>Model Selection</h3>
<p>Beberapa model dicoba di input data yang sama, lalu masing-masing dievaluasi pakai metrik yang relevan (Model 1 → Metrics 1, Model 2 → Metrics 2, dst). Hasil evaluasi ini jadi dasar milih model terbaik yang akan dipakai.</p>
<h2>Studi Kasus Nyata</h2>
<p><strong>Supervised — Klasifikasi:</strong></p>
<ul>
<li><p><em>Fraud Detection perbankan:</em> data transaksi historis yang sudah dilabeli "fraud"/"bukan fraud" dipakai buat prediksi transaksi baru. Dampak: mencegah kerugian finansial secara real-time.</p>
</li>
<li><p><em>Content Moderation:</em> klasifikasi konten upload user sebagai "layak tayang" vs "melanggar kebijakan," berdasarkan data yang sudah dilabeli moderator. Dampak: jaga kualitas platform tanpa cek manual satu-satu.</p>
</li>
</ul>
<p><strong>Supervised — Regresi:</strong></p>
<ul>
<li><em>Prediksi harga properti:</em> berdasarkan luas tanah, lokasi, jumlah kamar, usia bangunan → output angka kontinu (harga). Dampak: bantu penjual/pembeli menentukan harga wajar.</li>
</ul>
<p><strong>Unsupervised — Clustering:</strong></p>
<ul>
<li><p><em>Segmentasi pelanggan:</em> tanpa label "tipe A/B/C" dari awal, model mengelompokkan pelanggan jadi segmen ("pembeli hemat," "pelanggan loyal bernilai tinggi") berdasarkan pola belanja. Dampak: campaign lebih tepat sasaran.</p>
</li>
<li><p><em>Deteksi anomali cybersecurity:</em> memantau pola login/traffic tanpa label "normal/serangan" eksplisit — pola yang menyimpang jauh dianggap potensi ancaman. Dampak: cegah kebocoran data lebih cepat.</p>
</li>
</ul>
<p><strong>Precision vs Recall dalam bisnis nyata:</strong></p>
<ul>
<li><p><em>Fraud kartu kredit</em> butuh keseimbangan (biasanya pakai F1-Score) — Precision rendah = banyak nasabah baik komplain karena transaksinya diblokir; Recall rendah = banyak fraud lolos.</p>
</li>
<li><p><em>Sistem rekomendasi produk</em> — beberapa model (collaborative filtering, content-based, hybrid) dicoba, dievaluasi pakai metrik seperti precision@k, lalu yang terbaik di-deploy.</p>
</li>
</ul>
<h2>Cheat Sheet Menghafal</h2>
<p>📋 Klik untuk lihat cara cepat menghafal</p>
<ul>
<li><p><strong>Supervised = ADA GURU</strong> → ada label, ada supervisor, tujuannya memprediksi</p>
</li>
<li><p><strong>Unsupervised = TANPA GURU</strong> → tanpa label, cari pola sendiri, tujuannya eksplorasi</p>
</li>
<li><p><strong>Klasifikasi</strong> = kategori (diskrit) | <strong>Regresi</strong> = angka (kontinu)</p>
</li>
<li><p><strong>Clustering</strong> = kelompok mirip | <strong>Dimensional Reduction</strong> = sederhanakan fitur</p>
</li>
<li><p>Kata kunci <strong>"label"</strong> adalah pembeda utama antara Supervised dan Unsupervised</p>
</li>
<li><p>Confusion Matrix: True/False = benar/salah prediksi, Positive/Negative = apa yang diprediksi</p>
</li>
<li><p><strong>Presisi</strong> = "dari yang aku bilang positif, berapa yang beneran positif?"</p>
</li>
<li><p><strong>Recall</strong> = "dari yang beneran positif, berapa yang berhasil aku tangkap?"</p>
</li>
</ul>
<h2>Checklist Sebelum Lanjut ke Praktik</h2>
<ul>
<li><p>[ ] Paham bedanya data berlabel vs tanpa label</p>
</li>
<li><p>[ ] Bisa bedain kapan pakai klasifikasi vs regresi</p>
</li>
<li><p>[ ] Bisa bedain kapan pakai clustering vs dimensional reduction</p>
</li>
<li><p>[ ] Ngerti kenapa Recall lebih penting dari Precision di kasus medis, dan kenapa keduanya penting di kasus fraud</p>
</li>
<li><p>[ ] Paham istilah TP/TN/FP/FN di confusion matrix</p>
</li>
</ul>
<h2>Ringkasan</h2>
<p>Supervised Learning belajar dari contoh yang sudah ada jawabannya — cocok kalau tujuan kita memang untuk memprediksi. Unsupervised Learning belajar tanpa jawaban sama sekali — cocok kalau tujuan kita untuk eksplorasi dan menemukan struktur yang belum kita tahu sebelumnya.</p>
<p>Tapi teori doang belum cukup buat ngerti ML. Seri ini berlanjut ke <a href="https://shaka-ai.hashnode.dev/praktik-klasifikasi-iris-logistic-regression-vs-decision-tree"><strong>Part 2</strong></a> (praktik klasifikasi Iris — Logistic Regression vs Decision Tree) dan <a href="https://shaka-ai.hashnode.dev/praktik-unsupervised-learning-clustering-iris-kmeans"><strong>Part 3</strong></a> (praktik clustering tanpa label dengan KMeans, plus eksperimen lanjutan), lengkap dengan source code-nya.</p>
<hr />
<p><em>Bagian dari catatan belajar AI Engineering saya.</em> <a href="https://shaka-ai.hashnode.dev/praktik-klasifikasi-iris-logistic-regression-vs-decision-tree"><em>Part 2</em></a><em>: praktik klasifikasi Iris.</em> <a href="https://shaka-ai.hashnode.dev/praktik-unsupervised-learning-clustering-iris-kmeans"><em>Part 3</em></a><em>: praktik unsupervised learning — source code lengkap ada di</em> <a href="https://github.com/arielshakaramiro/supervised-unsupervised-learning-praktik-arielshakaramiro"><em>GitHub</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Predicting Baldness Probability: From Messy Data to a Working Machine Learning Model]]></title><description><![CDATA[If you think baldness is purely a matter of genetics, here's a plot twist: a few surprisingly sensible patterns show up once you dig into age, smoking habits, and stress levels together.
This post wal]]></description><link>https://shaka-ai.hashnode.dev/predicting-baldness-probability-from-messy-data-to-a-working-machine-learning-model</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/predicting-baldness-probability-from-messy-data-to-a-working-machine-learning-model</guid><category><![CDATA[data cleaning ]]></category><category><![CDATA[eda]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Python]]></category><category><![CDATA[scikit learn]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Mon, 31 Aug 2026 07:07:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/a5fc61e1-6019-4ac8-8725-bd2685e903ae.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<p>If you think baldness is purely a matter of genetics, here's a plot twist: a few surprisingly sensible patterns show up once you dig into age, smoking habits, and stress levels together.</p>
<p>This post walks through the full journey — from a genuinely messy raw dataset to a working regression model that predicts someone's baldness probability — along with the reasoning behind every technical decision made along the way.</p>
<blockquote>
<p>🇮🇩 Baca versi Bahasa Indonesia: <a href="https://shaka-ai.hashnode.dev/memprediksi-probabilitas-kebotakan-dari-data-kotor-sampai-model-machine-learning-terbaik">Memprediksi Probabilitas Kebotakan</a> · 💻 Full code &amp; dataset: <a href="https://github.com/arielshakaramiro/baldness-probability-prediction">GitHub</a></p>
</blockquote>
<h2>Table of Contents</h2>
<ul>
<li><p><a href="#about-the-dataset">About the Dataset</a></p>
</li>
<li><p><a href="#data-cleaning-taming-the-mess">Data Cleaning: Taming the Mess</a></p>
</li>
<li><p><a href="#eda-patterns-hiding-in-the-data">EDA: Patterns Hiding in the Data</a></p>
</li>
<li><p><a href="#outlier-handling-with-iqr">Outlier Handling with IQR</a></p>
</li>
<li><p><a href="#modeling--cross-validation">Modeling &amp; Cross-Validation</a></p>
</li>
<li><p><a href="#best-model--a-sample-prediction">Best Model &amp; a Sample Prediction</a></p>
</li>
<li><p><a href="#cheat-sheet-missing-value-strategy">Cheat Sheet: Missing Value Strategy</a></p>
</li>
<li><p><a href="#quiz-test-yourself">Quiz: Test Yourself</a></p>
</li>
<li><p><a href="#takeaways">Takeaways</a></p>
</li>
</ul>
<h2>About the Dataset</h2>
<p>The dataset (<code>botak_kotor.csv</code>, Indonesian for "dirty baldness data") is <strong>synthetic</strong> — not a real survey or medical dataset — with 7,917 rows across 14 columns: age, gender, occupation, province, salary, marital status, family history of baldness, weight &amp; height, shampoo brand, smoking habit, education level, stress level, and a <code>botak_prob</code> (baldness probability) target column. It was built as a data-cleaning exercise for the <strong>Fullstack Bangalore AI Engineer Bootcamp</strong>, deliberately "dirtied up" with missing values spread across every column (~1% each) and dozens of duplicate rows.</p>
<blockquote>
<p>⚠️ <strong>Important note:</strong> since the data is synthetic, every pattern and correlation discussed here reflects the simulated dataset only — <strong>not a medical or scientific claim</strong> about what actually causes baldness. Treat this as a case study in data cleaning, EDA, and machine learning technique, not health research.</p>
</blockquote>
<p>🤔 Guess first: what percentage of the data do you think is missing?</p>
<p>Missing values turned out to be spread fairly evenly across <strong>all 14 columns</strong>, roughly 1% each. After dropping rows with an empty target, <strong>79 rows</strong> had to go because their <code>botak_prob</code> label was missing — filling a label with a median would just contaminate the training data.</p>
<h2>Data Cleaning: Taming the Mess</h2>
<p>The missing-value strategy was split by column type:</p>
<table>
<thead>
<tr>
<th>Column Type</th>
<th>Examples</th>
<th>Strategy</th>
</tr>
</thead>
<tbody><tr>
<td>Continuous numeric</td>
<td>age, salary, weight, height, stress</td>
<td>Median</td>
</tr>
<tr>
<td>Binary (0/1)</td>
<td>married, family history, smoker</td>
<td>Mode</td>
</tr>
<tr>
<td>Categorical/text</td>
<td>gender, occupation, province, shampoo, education</td>
<td>Mode</td>
</tr>
<tr>
<td>Target (label)</td>
<td>botak_prob</td>
<td><strong>Drop the row</strong>, never impute</td>
</tr>
</tbody></table>
<p>Median was chosen over mean for numeric columns because it's more resistant to outliers — a handful of rows had extreme salary or weight values that would have dragged the mean away from the actual center of the data.</p>
<p>Next steps:</p>
<ul>
<li><p><strong>81 duplicate rows</strong> found and removed</p>
</li>
<li><p>Text columns cleaned up (whitespace stripped, province names title-cased)</p>
</li>
<li><p>Binary columns and age cast back to integers (they'd become floats because of the NaNs)</p>
</li>
</ul>
<p>After this stage, <strong>7,757 rows</strong> remained — ready for exploratory data analysis (EDA), before outlier cleanup.</p>
<h2>EDA: Patterns Hiding in the Data</h2>
<h3>Feature Correlation with Baldness Probability</h3>
<p>Ranking each numeric feature's correlation with <code>botak_prob</code> (strongest first):</p>
<ol>
<li><p><strong>Family history (</strong><code>is_keturunan</code><strong>)</strong> — 0.44</p>
</li>
<li><p><strong>Age</strong> — 0.35</p>
</li>
<li><p><strong>Stress level</strong> — 0.29</p>
</li>
<li><p><strong>Smoking habit</strong> — 0.26</p>
</li>
<li><p>Marital status — 0.12 (weak)</p>
</li>
<li><p>Salary (0.06), height (0.01), weight (near zero) — essentially no correlation</p>
</li>
</ol>
<p>Interestingly, salary and height/weight barely correlate with baldness probability at all — so those columns (along with <code>province</code>, which isn't logically relevant either) were deliberately left out of the model's feature set.</p>
<p>🤔 Guess first: which feature do you think correlates most strongly with baldness probability?</p>
<p>It's <strong>family history (</strong><code>is_keturunan</code><strong>)</strong>, at 0.44 — which tracks with how genetics is commonly associated with baldness patterns (again, this is a pattern in simulated data, not a medical claim).</p>
<h3>The High-Risk Group (Baldness Probability ≥ 70%)</h3>
<p>Out of 7,757 people at this stage, <strong>1,760 (22.7%)</strong> fall into the "high-risk" bucket (<code>botak_prob &gt;= 0.7</code>). Comparing that group to the general population:</p>
<table>
<thead>
<tr>
<th>Indicator</th>
<th>General Population</th>
<th>High-Risk Group</th>
</tr>
</thead>
<tbody><tr>
<td>Median age</td>
<td>39</td>
<td>45</td>
</tr>
<tr>
<td>Smoking rate</td>
<td>49.4%</td>
<td>64.4%</td>
</tr>
<tr>
<td>Married rate</td>
<td>97.9%</td>
<td>99.5%</td>
</tr>
<tr>
<td>Family history rate</td>
<td>20.3%</td>
<td>51.6%</td>
</tr>
</tbody></table>
<p>The pattern lines up with the correlation results: the high-risk group skews older, smokes more, and has more than double the rate of family history compared to the general population.</p>
<h2>Outlier Handling with IQR</h2>
<p>After EDA, outliers in the <code>height</code> and <code>weight</code> columns were cleaned up using the IQR (Interquartile Range) method — values outside the Q1−1.5×IQR to Q3+1.5×IQR range were dropped. This step removed <strong>326 rows</strong>, leaving <strong>7,431 clean rows</strong> for modeling.</p>
<p>Why remove outliers after EDA instead of before? So the data's original patterns (extreme values included) could still be observed during exploration, before deciding what actually needed to go for modeling purposes.</p>
<h2>Modeling &amp; Cross-Validation</h2>
<p>Four regression algorithms were compared: Linear Regression, Decision Tree, Random Forest, and Support Vector Regression (SVR). Numeric features were scaled with <code>MinMaxScaler</code> — but not just once upfront. Instead, scaling was wrapped inside a <code>Pipeline</code> alongside each model, so the scaler gets re-fit on every fold of <strong>5-fold cross-validation</strong>. That matters because it keeps the test fold fully isolated from the fitting process, avoiding data leakage.</p>
<p>Cross-validation results (average RMSE, lower is better):</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>RMSE (Cross-Validation)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Random Forest</strong></td>
<td><strong>0.074</strong></td>
</tr>
<tr>
<td>Support Vector Regression</td>
<td>0.085</td>
</tr>
<tr>
<td>Decision Tree</td>
<td>0.103</td>
</tr>
<tr>
<td>Linear Regression</td>
<td>0.111</td>
</tr>
</tbody></table>
<h2>Best Model &amp; a Sample Prediction</h2>
<p>Random Forest was automatically selected as the model with the lowest cross-validation RMSE. After retraining on the full training set and evaluating on a held-out test set it had never seen:</p>
<ul>
<li><p><strong>RMSE (test set): 0.0675</strong></p>
</li>
<li><p><strong>R² (test set): 0.856</strong> — the model explains roughly 85.6% of the variance in baldness probability using the available features</p>
</li>
</ul>
<p>As an example: for a new data point (40-year-old male, married, private employee, family history present, 65kg, 170cm, master's degree, smoker, stress level 7), the model predicts a <strong>baldness probability of about 64.1%</strong>.</p>
<h2>Cheat Sheet: Missing Value Strategy</h2>
<table>
<thead>
<tr>
<th>Situation</th>
<th>Strategy</th>
<th>Why</th>
</tr>
</thead>
<tbody><tr>
<td>Continuous numeric column</td>
<td>Median</td>
<td>Resistant to outliers, unlike mean</td>
</tr>
<tr>
<td>Binary 0/1 column</td>
<td>Mode</td>
<td>Median can produce weird values like 0.5</td>
</tr>
<tr>
<td>Categorical/text column</td>
<td>Mode</td>
<td>Fill with the most frequent value</td>
</tr>
<tr>
<td>Target/label column</td>
<td><strong>Drop the row</strong></td>
<td>Imputing a label contaminates training</td>
</tr>
</tbody></table>
<h2>Quiz: Test Yourself</h2>
<p>1. Why is scaling done inside a Pipeline instead of once before the split?</p>
<p>So the <code>MinMaxScaler</code> gets re-fit on every cross-validation fold, keeping the test fold genuinely isolated from the fitting process — which prevents data leakage that would otherwise make evaluation results look better than the model's real-world performance.</p>
<p>2. Why were rows with a missing target (botak_prob) dropped instead of imputed with the median?</p>
<p>Because the target is the label the model is trying to learn. Filling it with a synthetic value like the median would contaminate training — the model would end up learning from values that never actually existed in the original data.</p>
<p>3. Which columns were deliberately excluded as model features, and why?</p>
<p><code>salary</code> and <code>province</code> — salary's correlation with <code>botak_prob</code> is only 0.06 (essentially no relationship), and neither is logically relevant as a predictor of baldness anyway.</p>
<h2>Takeaways</h2>
<p>Three things worth carrying forward from this exercise:</p>
<ol>
<li><p><strong>Careful cleaning matters</strong> — even small decisions (median vs. mean, dropping vs. imputing the target) ripple through to model quality down the line.</p>
</li>
<li><p><strong>EDA earns its keep as a feature filter</strong> — near-zero correlations (salary, height, weight) are a solid reason to leave features out rather than force them in.</p>
</li>
<li><p><strong>Cross-validation is more trustworthy</strong> than a single train-test split, especially when choosing between several candidate models.</p>
</li>
</ol>
<p>Random Forest won this particular case study, but the more valuable part is the <em>process</em> that got there: from messy data, to defensible cleaning decisions, to insight from EDA, to rigorous model evaluation.</p>
<hr />
<p>💻 <strong>Full code, dataset, and a runnable notebook</strong> are on GitHub: <a href="https://github.com/arielshakaramiro/baldness-probability-prediction">github.com/arielshakaramiro/baldness-probability-prediction</a></p>
]]></content:encoded></item><item><title><![CDATA[Memprediksi Probabilitas Kebotakan: Dari Data Kotor sampai Model Machine Learning Terbaik]]></title><description><![CDATA[Kalau kamu pikir kebotakan cuma soal gen dari orang tua, coba tunggu dulu — dari eksplorasi data kali ini, muncul beberapa pola menarik (dan cukup masuk akal) dari kombinasi umur, kebiasaan merokok, d]]></description><link>https://shaka-ai.hashnode.dev/memprediksi-probabilitas-kebotakan-dari-data-kotor-sampai-model-machine-learning-terbaik</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/memprediksi-probabilitas-kebotakan-dari-data-kotor-sampai-model-machine-learning-terbaik</guid><category><![CDATA[data cleaning ]]></category><category><![CDATA[eda]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Python]]></category><category><![CDATA[scikit learn]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Mon, 31 Aug 2026 07:05:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/d15239d1-6687-40f4-87d8-117dfcd1209f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<p>Kalau kamu pikir kebotakan cuma soal gen dari orang tua, coba tunggu dulu — dari eksplorasi data kali ini, muncul beberapa pola menarik (dan cukup masuk akal) dari kombinasi umur, kebiasaan merokok, dan tingkat stres.</p>
<p>Tulisan ini merangkum proses penuh dari data mentah yang berantakan sampai model machine learning yang bisa memprediksi probabilitas kebotakan seseorang — lengkap dengan alasan di balik tiap keputusan teknisnya.</p>
<blockquote>
<p>🇬🇧 Baca versi Bahasa Inggris: <a href="https://shaka-ai.hashnode.dev/predicting-baldness-probability-from-messy-data-to-a-working-machine-learning-model">Predicting Baldness Probability</a> · 💻 Kode &amp; dataset lengkap: <a href="https://github.com/arielshakaramiro/baldness-probability-prediction">GitHub</a></p>
</blockquote>
<h2>Daftar Isi</h2>
<ul>
<li><p><a href="#sekilas-tentang-dataset">Sekilas Tentang Dataset</a></p>
</li>
<li><p><a href="#data-cleaning-merapikan-yang-kotor">Data Cleaning: Merapikan yang Kotor</a></p>
</li>
<li><p><a href="#eda-pola-yang-muncul-dari-data">EDA: Pola yang Muncul dari Data</a></p>
</li>
<li><p><a href="#outlier-handling-dengan-iqr">Outlier Handling dengan IQR</a></p>
</li>
<li><p><a href="#modeling--cross-validation">Modeling &amp; Cross-Validation</a></p>
</li>
<li><p><a href="#model-terbaik--contoh-prediksi">Model Terbaik &amp; Contoh Prediksi</a></p>
</li>
<li><p><a href="#cheat-sheet-strategi-isi-missing-value">Cheat Sheet: Strategi Isi Missing Value</a></p>
</li>
<li><p><a href="#quiz-uji-pemahaman">Quiz: Uji Pemahaman</a></p>
</li>
<li><p><a href="#kesimpulan">Kesimpulan</a></p>
</li>
</ul>
<h2>Sekilas Tentang Dataset</h2>
<p>Dataset yang dipakai (<code>botak_kotor.csv</code>) adalah dataset <strong>sintetis</strong> — bukan data survei atau data medis nyata — berisi 7.917 baris dengan 14 kolom: umur, jenis kelamin, pekerjaan, provinsi, gaji, status menikah, riwayat keturunan botak, berat &amp; tinggi badan, merek sampo, kebiasaan merokok, pendidikan, tingkat stres, hingga probabilitas kebotakan (<code>botak_prob</code>) sebagai target. Dataset ini jadi materi latihan data cleaning untuk <strong>Fullstack Bangalore AI Engineer Bootcamp</strong>, sengaja "dikotori" dengan missing value yang tersebar di semua kolom (~1% per kolom) dan puluhan baris duplikat.</p>
<blockquote>
<p>⚠️ <strong>Catatan penting:</strong> Karena datanya sintetis, semua pola dan korelasi yang dibahas di tulisan ini murni pola dari data simulasi — <strong>bukan klaim medis atau ilmiah</strong> tentang penyebab kebotakan di dunia nyata. Anggap ini studi kasus teknik data cleaning, EDA, dan machine learning, bukan riset kesehatan.</p>
</blockquote>
<p>🤔 Coba Tebak Dulu: menurutmu berapa persen data yang hilang (missing value) di dataset ini?</p>
<p>Missing value ternyata tersebar cukup merata di <strong>semua 14 kolom</strong>, masing-masing sekitar 1%. Setelah baris dengan target kosong dibuang, ada <strong>79 baris</strong> yang harus di-drop karena label (<code>botak_prob</code>)-nya kosong — mengisi label dengan median cuma akan mencemari data training.</p>
<h2>Data Cleaning: Merapikan yang Kotor</h2>
<p>Strategi isi missing value dibedakan berdasarkan tipe kolom:</p>
<table>
<thead>
<tr>
<th>Tipe Kolom</th>
<th>Contoh</th>
<th>Strategi</th>
</tr>
</thead>
<tbody><tr>
<td>Numerik kontinu</td>
<td>umur, gaji, berat, tinggi, stress</td>
<td>Median</td>
</tr>
<tr>
<td>Biner (0/1)</td>
<td>is_menikah, is_keturunan, is_merokok</td>
<td>Modus</td>
</tr>
<tr>
<td>Kategorik/teks</td>
<td>jenis_kelamin, pekerjaan, provinsi, sampo, pendidikan</td>
<td>Modus</td>
</tr>
<tr>
<td>Target (label)</td>
<td>botak_prob</td>
<td><strong>Drop baris</strong>, bukan diisi</td>
</tr>
</tbody></table>
<p>Median dipilih untuk kolom numerik (bukan mean) karena lebih tahan terhadap outlier — beberapa baris punya nilai gaji atau berat badan ekstrem yang bisa "menarik" nilai mean menjauh dari pusat data yang sebenarnya.</p>
<p>Langkah selanjutnya:</p>
<ul>
<li><p><strong>81 baris duplikat</strong> ditemukan dan dihapus</p>
</li>
<li><p>Teks dirapikan (strip whitespace, title-case untuk nama provinsi)</p>
</li>
<li><p>Tipe data kolom biner &amp; umur dikembalikan ke integer (sempat berubah jadi float akibat NaN)</p>
</li>
</ul>
<p>Setelah tahap ini, dataset tersisa <strong>7.757 baris</strong> — siap dipakai untuk eksplorasi data (EDA) sebelum masuk ke pembersihan outlier.</p>
<h2>EDA: Pola yang Muncul dari Data</h2>
<h3>Korelasi Fitur terhadap Probabilitas Botak</h3>
<p>Urutan korelasi tiap fitur numerik terhadap <code>botak_prob</code> (dari yang paling kuat):</p>
<ol>
<li><p><strong>Riwayat keturunan (</strong><code>is_keturunan</code><strong>)</strong> — 0,44</p>
</li>
<li><p><strong>Umur</strong> — 0,35</p>
</li>
<li><p><strong>Tingkat stres</strong> — 0,29</p>
</li>
<li><p><strong>Kebiasaan merokok</strong> — 0,26</p>
</li>
<li><p>Status menikah — 0,12 (lemah)</p>
</li>
<li><p>Gaji (0,06), tinggi (0,01), berat (mendekati 0) — praktis tidak berkorelasi</p>
</li>
</ol>
<p>Menariknya, gaji dan tinggi/berat badan nyaris tidak berkorelasi sama sekali dengan probabilitas kebotakan — jadi kedua kolom itu (plus <code>provinsi</code>, yang secara logis juga tidak relevan) sengaja tidak dipakai sebagai fitur model nantinya.</p>
<p>🤔 Coba Tebak Dulu: menurutmu fitur apa yang paling berkorelasi dengan probabilitas botak?</p>
<p>Jawabannya <strong>riwayat keturunan (</strong><code>is_keturunan</code><strong>)</strong>, dengan korelasi 0,44 — cukup masuk akal karena secara umum faktor keturunan sering dikaitkan dengan pola kebotakan (ingat, ini pola dari data simulasi, bukan klaim medis).</p>
<h3>Kelompok Risiko Tinggi (Probabilitas Botak ≥ 70%)</h3>
<p>Dari 7.757 individu pada tahap ini, <strong>1.760 orang (22,7%)</strong> masuk kategori "risiko tinggi" (<code>botak_prob &gt;= 0,7</code>). Beberapa perbandingan populasi umum vs kelompok ini:</p>
<table>
<thead>
<tr>
<th>Indikator</th>
<th>Populasi Umum</th>
<th>Risiko Tinggi</th>
</tr>
</thead>
<tbody><tr>
<td>Median umur</td>
<td>39 tahun</td>
<td>45 tahun</td>
</tr>
<tr>
<td>Proporsi merokok</td>
<td>49,4%</td>
<td>64,4%</td>
</tr>
<tr>
<td>Proporsi sudah menikah</td>
<td>97,9%</td>
<td>99,5%</td>
</tr>
<tr>
<td>Proporsi ada riwayat keturunan</td>
<td>20,3%</td>
<td>51,6%</td>
</tr>
</tbody></table>
<p>Polanya konsisten dengan hasil korelasi: kelompok risiko tinggi cenderung lebih tua, proporsi perokoknya lebih besar, dan proporsi yang punya riwayat keturunan botak lebih dari dua kali lipat dibanding populasi umum.</p>
<h2>Outlier Handling dengan IQR</h2>
<p>Setelah EDA, baru dilakukan pembersihan outlier pada kolom <code>tinggi</code> dan <code>berat</code> memakai metode IQR (Interquartile Range) — nilai di luar rentang Q1−1,5×IQR sampai Q3+1,5×IQR dibuang. Tahap ini menghapus <strong>326 baris</strong>, menyisakan <strong>7.431 baris bersih</strong> yang dipakai untuk modeling.</p>
<p>Kenapa outlier baru dibuang setelah EDA, bukan sebelumnya? Supaya pola asli di data (termasuk nilai ekstrem) masih bisa diamati dulu saat eksplorasi, sebelum diputuskan mana yang benar-benar perlu dibuang untuk keperluan modeling.</p>
<h2>Modeling &amp; Cross-Validation</h2>
<p>Untuk pemodelan, 4 algoritma regresi dibandingkan: Regresi Linear, Decision Tree, Random Forest, dan Support Vector Regression (SVR). Fitur numerik diskalakan dengan <code>MinMaxScaler</code> — tapi bukan sekali di awal, melainkan dibungkus dalam <code>Pipeline</code> bersama tiap model, supaya proses scaling di-<em>fit</em> ulang di setiap fold <strong>5-fold cross-validation</strong>. Pendekatan ini penting supaya data test benar-benar terisolasi dari proses training (menghindari data leakage).</p>
<p>Hasil cross-validation (RMSE rata-rata, makin kecil makin baik):</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>RMSE (Cross-Validation)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Random Forest</strong></td>
<td><strong>0,074</strong></td>
</tr>
<tr>
<td>Support Vector Regression</td>
<td>0,085</td>
</tr>
<tr>
<td>Decision Tree</td>
<td>0,103</td>
</tr>
<tr>
<td>Regresi Linear</td>
<td>0,111</td>
</tr>
</tbody></table>
<h2>Model Terbaik &amp; Contoh Prediksi</h2>
<p>Random Forest otomatis terpilih sebagai model dengan RMSE cross-validation terendah. Setelah di-retrain di seluruh data train dan dievaluasi di data test yang belum pernah dilihat model sebelumnya:</p>
<ul>
<li><p><strong>RMSE (test set): 0,0675</strong></p>
</li>
<li><p><strong>R² (test set): 0,856</strong> — model bisa menjelaskan sekitar 85,6% variasi probabilitas botak dari fitur yang tersedia</p>
</li>
</ul>
<p>Sebagai contoh: untuk data baru (laki-laki, 40 tahun, sudah menikah, pekerja swasta, punya riwayat keturunan, berat 65kg, tinggi 170cm, pendidikan S2, perokok, level stres 7), model memprediksi <strong>probabilitas botak sekitar 64,1%</strong>.</p>
<h2>Cheat Sheet: Strategi Isi Missing Value</h2>
<table>
<thead>
<tr>
<th>Situasi</th>
<th>Strategi</th>
<th>Alasan</th>
</tr>
</thead>
<tbody><tr>
<td>Kolom numerik kontinu</td>
<td>Median</td>
<td>Tahan terhadap outlier, tidak seperti mean</td>
</tr>
<tr>
<td>Kolom biner 0/1</td>
<td>Modus</td>
<td>Median bisa hasilkan nilai aneh seperti 0,5</td>
</tr>
<tr>
<td>Kolom kategorik/teks</td>
<td>Modus</td>
<td>Isi dengan nilai paling sering muncul</td>
</tr>
<tr>
<td>Kolom target/label</td>
<td><strong>Drop baris</strong></td>
<td>Mengisi label akan mencemari proses training</td>
</tr>
</tbody></table>
<h2>Quiz: Uji Pemahaman</h2>
<p>1. Kenapa scaling dilakukan di dalam Pipeline, bukan sekali di awal sebelum split?</p>
<p>Supaya <code>MinMaxScaler</code> di-<em>fit</em> ulang di setiap fold cross-validation, memastikan data test benar-benar terisolasi dari proses fitting — mencegah data leakage yang bisa membuat hasil evaluasi terlihat lebih bagus dari performa sebenarnya.</p>
<p>2. Kenapa baris dengan target (botak_prob) kosong di-drop, bukan diisi median?</p>
<p>Karena target adalah label yang dipelajari model. Mengisi label dengan nilai buatan (median) akan mencemari proses training — model akan belajar dari nilai yang sebenarnya tidak pernah ada di data asli.</p>
<p>3. Kolom apa yang sengaja tidak dipakai sebagai fitur model, dan kenapa?</p>
<p><code>gaji</code> dan <code>provinsi</code> — korelasi gaji terhadap <code>botak_prob</code> cuma 0,06 (praktis tidak ada hubungan), dan keduanya juga secara logis tidak relevan sebagai prediktor kebotakan.</p>
<h2>Kesimpulan</h2>
<p>Dari eksplorasi ini, tiga hal utama yang bisa dibawa pulang:</p>
<ol>
<li><p><strong>Cleaning yang hati-hati itu penting</strong> — keputusan sekecil apa pun (median vs mean, drop vs isi target) bisa memengaruhi kualitas model di ujung proses.</p>
</li>
<li><p><strong>EDA membantu menyaring fitur</strong> sebelum modeling — korelasi yang nyaris nol (gaji, tinggi, berat) jadi alasan kuat untuk tidak memaksakan fitur itu masuk model.</p>
</li>
<li><p><strong>Cross-validation lebih meyakinkan</strong> dibanding satu kali train-test split — terutama saat harus memilih di antara beberapa kandidat model.</p>
</li>
</ol>
<p>Random Forest menang di studi kasus ini, tapi yang lebih penting adalah <em>proses</em> sampai ke sana: dari data kotor, ke keputusan cleaning yang masuk akal, ke insight dari EDA, sampai evaluasi model yang ketat.</p>
<hr />
<p>💻 <strong>Kode lengkap, dataset, dan notebook</strong> yang bisa langsung dijalankan ada di GitHub: <a href="https://github.com/arielshakaramiro/baldness-probability-prediction">github.com/arielshakaramiro/baldness-probability-prediction</a></p>
]]></content:encoded></item><item><title><![CDATA[Hands-On Data Preprocessing with Python: From Missing Values to PCA]]></title><description><![CDATA[Learning notes with code that actually runs — six core data preprocessing techniques you'll reach for before feeding data into a Machine Learning model, backed by real execution output instead of made]]></description><link>https://shaka-ai.hashnode.dev/hands-on-data-preprocessing-with-python-from-missing-values-to-pca</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/hands-on-data-preprocessing-with-python-from-missing-values-to-pca</guid><category><![CDATA[Python]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[pandas]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Mon, 31 Aug 2026 01:26:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/8b838fdf-082c-4d19-aff5-bdae623d0506.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><em>Learning notes with code that actually runs — six core data preprocessing techniques you'll reach for before feeding data into a Machine Learning model, backed by real execution output instead of made-up numbers.</em></p>
</blockquote>
<p>There's a saying that gets repeated a lot in data science: "80% of AI work is data, not the model" (the exact number varies by source, but the general point tends to hold up). What people usually mean is this part of the pipeline: checking for missing values, catching and handling outliers, turning categories and text into numbers, compressing dimensions, and keeping a version history of your data. Every technique below was run with actual Python code (Pandas + Scikit-learn), against two kinds of data — a real 10,000-row customer dataset, and a few small synthetic examples to make the concepts concrete.</p>
<blockquote>
<p>Note on the code blocks: variable names are kept exactly as they were in the original notebook (Indonesian words like <code>nama</code>, <code>umur</code>, <code>gaji</code>, <code>batas_bawah</code>) rather than translated, so the code here is byte-for-byte reproducible against the source. Only the surrounding explanation is in English.</p>
</blockquote>
<blockquote>
<p>📂 The full notebook (already executed end-to-end, dataset included) is on GitHub: <a href="https://github.com/arielshakaramiro/data-preprocessing-praktik-arielshakaramiro">github.com/arielshakaramiro/data-preprocessing-praktik-arielshakaramiro</a></p>
</blockquote>
<h2>Table of Contents</h2>
<ul>
<li><p><a href="#1-data-collection--storage-quick-concept">1. Data Collection &amp; Storage: Quick Concept</a></p>
</li>
<li><p><a href="#2-missing-values-check-first-dont-assume">2. Missing Values: Check First, Don't Assume</a></p>
</li>
<li><p><a href="#3-detecting--handling-outliers-with-iqr">3. Detecting &amp; Handling Outliers with IQR</a></p>
</li>
<li><p><a href="#4-feature-engineering-turning-categories-into-numbers">4. Feature Engineering: Turning Categories into Numbers</a></p>
</li>
<li><p><a href="#5-text-preprocessing-bag-of-words--tf-idf">5. Text Preprocessing: Bag of Words &amp; TF-IDF</a></p>
</li>
<li><p><a href="#6-reducing-dimensions-with-pca">6. Reducing Dimensions with PCA</a></p>
</li>
<li><p><a href="#7-the-data-versioning-concept">7. The Data Versioning Concept</a></p>
</li>
<li><p><a href="#8-summary--next-steps">8. Summary &amp; Next Steps</a></p>
</li>
<li><p><a href="#9-cheat-sheet-preprocessing-checklist">9. Cheat Sheet: Preprocessing Checklist</a></p>
</li>
<li><p><a href="#10-quiz-check">10. Quiz Check</a></p>
</li>
</ul>
<hr />
<h2>1. Data Collection &amp; Storage: Quick Concept</h2>
<p>Before diving into practice, there are two types of data worth knowing:</p>
<ul>
<li><p><strong>Unstructured Data</strong> — data with "free-form" shape, like images, text, or audio. Humans can grasp its meaning directly without any special structure (e.g., we instantly recognize a picture of a cat).</p>
</li>
<li><p><strong>Structured Data</strong> — data neatly organized into rows and columns, like transaction records or customer data. Its meaning depends heavily on business context, so it's not always understandable "at a glance" to a non-expert.</p>
</li>
</ul>
<p>Both types need storage that's <strong>secure, easily accessible, and versionable</strong> (keeps a history of changes) — this versioning concept is covered further in section 7.</p>
<hr />
<h2>2. Missing Values: Check First, Don't Assume</h2>
<p>There are two common ways to handle missing (<code>NaN</code>) values:</p>
<ul>
<li><p><strong>Dropna</strong> — remove rows/columns with missing data. Works well when only a small amount is missing.</p>
</li>
<li><p><strong>Fillna</strong> — fill the gap with a substitute: mean, median, or mode (the most frequent value).</p>
</li>
</ul>
<p><strong>The step beginners skip most often: check first, don't assume the data is dirty.</strong></p>
<pre><code class="language-python">sample_data = pd.read_csv('customers-10000.csv')
print(sample_data.isnull().sum())
</code></pre>
<p><em>(The original code loads the dataset from a Google Drive link; it's written here as a local file for easy reproduction — the content is identical, a public 10,000-row dataset from Datablist.)</em></p>
<p>Real execution result on the customer dataset (10,000 rows, 12 columns — <code>Index</code>, <code>Customer Id</code>, <code>First Name</code>, <code>Last Name</code>, <code>Company</code>, <code>City</code>, <code>Country</code>, <code>Phone 1</code>, <code>Phone 2</code>, <code>Email</code>, <code>Subscription Date</code>, <code>Website</code>):</p>
<pre><code class="language-plaintext">Missing values per column: 0 (every column, 0.0%)
</code></pre>
<blockquote>
<p>🤔 <strong>Guess First:</strong> How much of a public-facing customer dataset like this do you think is typically missing?</p>
<p>Reveal Answer</p>
<p>In this case: <strong>0%</strong> — this is a synthetic dataset generated with Faker for practice purposes, so it was built clean on purpose, with no missing values at all. That's actually the real lesson here: never assume a dataset is "obviously dirty." Always run <code>.isnull().sum()</code> first before deciding on a strategy — if it turns out clean, you just saved yourself time; if something is missing, you know exactly which column needs attention.</p>
</blockquote>
<p>To actually see both handling techniques in action, a tiny synthetic dataset was built with intentional gaps:</p>
<pre><code class="language-python">data = {
    'nama': ['Andi', 'Budi', 'Citra', 'Dewi', 'Eka'],
    'umur': [25, np.nan, 30, 22, np.nan],
    'gaji': [5000000, 6000000, np.nan, 4500000, 5200000]
}
df = pd.DataFrame(data)
</code></pre>
<table>
<thead>
<tr>
<th>nama</th>
<th>umur</th>
<th>gaji</th>
</tr>
</thead>
<tbody><tr>
<td>Andi</td>
<td>25</td>
<td>5,000,000</td>
</tr>
<tr>
<td>Budi</td>
<td>NaN</td>
<td>6,000,000</td>
</tr>
<tr>
<td>Citra</td>
<td>30</td>
<td>NaN</td>
</tr>
<tr>
<td>Dewi</td>
<td>22</td>
<td>4,500,000</td>
</tr>
<tr>
<td>Eka</td>
<td>NaN</td>
<td>5,200,000</td>
</tr>
</tbody></table>
<p>(<code>nama</code> = name, <code>umur</code> = age, <code>gaji</code> = salary — kept in the original language since that's the actual column names in the code.)</p>
<p>Missing count and percentage per column (real output of <code>df.isnull().sum()</code> and <code>df.isnull().sum() / len(df) * 100</code>):</p>
<table>
<thead>
<tr>
<th>Column</th>
<th>Missing Count</th>
<th>Percentage</th>
</tr>
</thead>
<tbody><tr>
<td>nama</td>
<td>0</td>
<td>0%</td>
</tr>
<tr>
<td>umur</td>
<td>2</td>
<td>40%</td>
</tr>
<tr>
<td>gaji</td>
<td>1</td>
<td>20%</td>
</tr>
</tbody></table>
<p><strong>Option 1 —</strong> <code>dropna()</code><strong>:</strong></p>
<pre><code class="language-python">df_dropped = df.dropna()
</code></pre>
<p>Budi, Citra, and Eka all get dropped since each has one empty cell — 5 rows shrink down to 2. Effective when the missing amount is small and those rows aren't critical; wasteful when the data is valuable but just happens to have one blank column.</p>
<p><strong>Option 2 —</strong> <code>fillna()</code> <strong>with mean/median:</strong></p>
<pre><code class="language-python">df_filled = df.copy()
df_filled['umur'] = df_filled['umur'].fillna(df_filled['umur'].mean())
df_filled['gaji'] = df_filled['gaji'].fillna(df_filled['gaji'].median())
</code></pre>
<p>Missing ages get filled with the average of the ones present, and missing salaries get filled with the median. <em>(Added context beyond the original code: median is generally chosen for salary because it's more robust against extreme values than the mean — this will make more sense after the outlier section below.)</em></p>
<hr />
<h2>3. Detecting &amp; Handling Outliers with IQR</h2>
<p>An outlier is a data point that sits far outside the majority. The decision rule:</p>
<ul>
<li><p>Outliers that are <strong>rare and don't impact the business</strong> → safe to drop.</p>
</li>
<li><p>Outliers that <strong>do disrupt business processes</strong> → don't just drop them, handle them more carefully (at production scale, one advanced approach is an <strong>Autoencoder</strong> — a small model that learns the pattern of normal data, so data that deviates far from that pattern produces a large reconstruction error and can be flagged as an outlier).</p>
</li>
</ul>
<p>For beginners, one of the most common and straightforward methods is <strong>IQR (Interquartile Range)</strong>. The simulation: 1,000 normally-distributed salary (<code>gaji</code>) values (mean Rp5,000,000, standard deviation Rp1,000,000), with 1 extreme outlier of Rp50,000,000 injected on top.</p>
<pre><code class="language-python">np.random.seed(42)
gaji = np.random.normal(5000000, 1000000, 1000).tolist()
gaji.append(50000000)  # outlier: a salary far larger than the rest
df_gaji = pd.DataFrame({'gaji': gaji})
</code></pre>
<p>A quick look before computing anything — the boxplot code:</p>
<pre><code class="language-python">plt.figure(figsize=(6, 4))
plt.boxplot(df_gaji['gaji'])
plt.title('Boxplot Gaji (perhatikan ada titik yang jauh di atas)')
plt.ylabel('Gaji')
plt.show()
</code></pre>
<p>(The title/label strings are left in Indonesian, exactly as they were actually run — "Boxplot Gaji (perhatikan ada titik yang jauh di atas)" means "Salary boxplot, notice the point far above.")</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/ce2f390f-1364-4732-b46d-5a380aa9f84f.png" alt="Boxplot of salary showing one extreme outlier point far above the IQR box" style="display:block;margin:0 auto" />

<p>Then the bounds get computed mathematically with IQR:</p>
<pre><code class="language-python">Q1 = df_gaji['gaji'].quantile(0.25)
Q3 = df_gaji['gaji'].quantile(0.75)
IQR = Q3 - Q1
batas_bawah = Q1 - 1.5 * IQR
batas_atas = Q3 + 1.5 * IQR

outliers = df_gaji[(df_gaji['gaji'] &lt; batas_bawah) | (df_gaji['gaji'] &gt; batas_atas)]
</code></pre>
<p>(<code>batas_bawah</code> = lower bound, <code>batas_atas</code> = upper bound.)</p>
<p>Real execution results (with <code>random seed 42</code>, so the numbers are reproducible on a re-run):</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td>Q1 (lower quartile)</td>
<td>Rp4,353,427</td>
</tr>
<tr>
<td>Q3 (upper quartile)</td>
<td>Rp5,648,710</td>
</tr>
<tr>
<td>IQR</td>
<td>Rp1,295,283</td>
</tr>
<tr>
<td>Lower bound</td>
<td>Rp2,410,503</td>
</tr>
<tr>
<td>Upper bound</td>
<td>Rp7,591,634</td>
</tr>
<tr>
<td>Outliers detected</td>
<td><strong>9 out of 1,001 rows</strong></td>
</tr>
</tbody></table>
<blockquote>
<p>🤔 <strong>Guess First:</strong> Only 1 fake outlier (Rp50M) was injected. Do you think IQR flags exactly 1 outlier too, or could it be more?</p>
<p>Reveal Answer</p>
<p>It flagged <strong>9 outliers</strong>, not just 1. Besides the injected Rp50M value, 8 other data points — pure products of the normal distribution, not intentional — happened to land outside the 1.5×IQR range, and they sit on <strong>both sides</strong>: 4 below the lower bound (roughly Rp1.76M–2.38M) and 4 above the upper bound (roughly Rp7.63M–8.85M). The lesson: the IQR method is purely statistical, it has no idea which values are "intentional anomalies" and which are just "extreme by coincidence" due to the natural spread of the data. That's why, before dropping anything IQR flags, it's still worth checking the business context — an Rp8M salary in this dataset could genuinely be a director's valid salary, not a data-entry mistake, and an Rp1.8M salary might just be an intern.</p>
</blockquote>
<p>After removal:</p>
<pre><code class="language-python">df_bersih = df_gaji[(df_gaji['gaji'] &gt;= batas_bawah) &amp; (df_gaji['gaji'] &lt;= batas_atas)]
</code></pre>
<p>The clean remainder: <strong>992 out of 1,001 rows</strong>.</p>
<hr />
<h2>4. Feature Engineering: Turning Categories into Numbers</h2>
<p>Machine Learning models only understand numbers, not text categories like "Apple" or "Chicken." Two common ways to convert them:</p>
<table>
<thead>
<tr>
<th>Technique</th>
<th>How It Works</th>
<th>When to Use</th>
</tr>
</thead>
<tbody><tr>
<td>Label Encoding</td>
<td>Each category gets one unique number</td>
<td>Categories with a natural order/rank (low–medium–high)</td>
</tr>
<tr>
<td>One Hot Encoding</td>
<td>Each category becomes its own 0/1 column</td>
<td>Categories with no order (food names, cities, etc.) — the safer default</td>
</tr>
</tbody></table>
<p>Example data:</p>
<pre><code class="language-python">food = pd.DataFrame({
    'Food Name': ['Apple', 'Chicken', 'Broccoli'],
    'Calories': [95, 231, 50]
})
</code></pre>
<table>
<thead>
<tr>
<th>Food Name</th>
<th>Calories</th>
</tr>
</thead>
<tbody><tr>
<td>Apple</td>
<td>95</td>
</tr>
<tr>
<td>Chicken</td>
<td>231</td>
</tr>
<tr>
<td>Broccoli</td>
<td>50</td>
</tr>
</tbody></table>
<p><strong>Label Encoding</strong> (real <code>LabelEncoder</code> output):</p>
<table>
<thead>
<tr>
<th>Food Name</th>
<th>Calories</th>
<th>Categorical #</th>
</tr>
</thead>
<tbody><tr>
<td>Apple</td>
<td>95</td>
<td>0</td>
</tr>
<tr>
<td>Chicken</td>
<td>231</td>
<td>2</td>
</tr>
<tr>
<td>Broccoli</td>
<td>50</td>
<td>1</td>
</tr>
</tbody></table>
<p>Notice the numbers follow alphabetical order (Apple=0, Broccoli=1, Chicken=2) — not calories or any meaningful ranking. This is a classic source of bugs: applied to categories with no real order, a model can "read" Chicken (2) as somehow twice as large or important as Broccoli (1), when it's really just an alphabetical index.</p>
<p><strong>One Hot Encoding</strong> (real <code>pd.get_dummies</code> output):</p>
<table>
<thead>
<tr>
<th>Calories</th>
<th>Food Name_Apple</th>
<th>Food Name_Broccoli</th>
<th>Food Name_Chicken</th>
</tr>
</thead>
<tbody><tr>
<td>95</td>
<td>1</td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td>231</td>
<td>0</td>
<td>0</td>
<td>1</td>
</tr>
<tr>
<td>50</td>
<td>0</td>
<td>1</td>
<td>0</td>
</tr>
</tbody></table>
<p>No implied ordering — each category gets its own column. The trade-off: with hundreds or thousands of categories (postal codes, for example), the column count can explode.</p>
<hr />
<h2>5. Text Preprocessing: Bag of Words &amp; TF-IDF</h2>
<p>Just like categories, text also needs to become a numeric vector (vectorization). Example corpus (3 short Indonesian sentences):</p>
<pre><code class="language-python">corpus = [
    "rumah ini bagus",        # d1 - "the house is nice"
    "rumah saya makan nasi",  # d2 - "my house, I eat rice"
    "saya makan nasi"         # d3 - "I eat rice"
]
</code></pre>
<p><strong>Bag of Words</strong> — counts how many times each word appears in each document:</p>
<pre><code class="language-python">vectorizer = CountVectorizer()
bow_matrix = vectorizer.fit_transform(corpus)

bow_df = pd.DataFrame(
    bow_matrix.toarray(),
    columns=vectorizer.get_feature_names_out(),
    index=['d1', 'd2', 'd3']
)
</code></pre>
<p>Real execution result (<code>bow_df</code>):</p>
<table>
<thead>
<tr>
<th></th>
<th>bagus</th>
<th>ini</th>
<th>makan</th>
<th>nasi</th>
<th>rumah</th>
<th>saya</th>
</tr>
</thead>
<tbody><tr>
<td>d1</td>
<td>1</td>
<td>1</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>d2</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>d3</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>0</td>
<td>1</td>
</tr>
</tbody></table>
<p>BoW has a weakness: words that appear in <em>every</em> document get counted as heavily as any other word, even though they don't help tell documents apart.</p>
<p><strong>TF-IDF (Term Frequency–Inverse Document Frequency)</strong> fixes this — words that show up across many documents get a lower weight, while rarer, more unique words get a higher weight:</p>
<pre><code class="language-python">tfidf_vectorizer = TfidfVectorizer()
tfidf_matrix = tfidf_vectorizer.fit_transform(corpus)

tfidf_df = pd.DataFrame(
    tfidf_matrix.toarray(),
    columns=tfidf_vectorizer.get_feature_names_out(),
    index=['d1', 'd2', 'd3']
).round(3)
</code></pre>
<p>Real execution result (<code>tfidf_df</code>, rounded to 3 decimals):</p>
<table>
<thead>
<tr>
<th></th>
<th>bagus</th>
<th>ini</th>
<th>makan</th>
<th>nasi</th>
<th>rumah</th>
<th>saya</th>
</tr>
</thead>
<tbody><tr>
<td>d1</td>
<td>0.623</td>
<td>0.623</td>
<td>0.000</td>
<td>0.000</td>
<td>0.474</td>
<td>0.000</td>
</tr>
<tr>
<td>d2</td>
<td>0.000</td>
<td>0.000</td>
<td>0.500</td>
<td>0.500</td>
<td>0.500</td>
<td>0.500</td>
</tr>
<tr>
<td>d3</td>
<td>0.000</td>
<td>0.000</td>
<td>0.577</td>
<td>0.577</td>
<td>0.000</td>
<td>0.577</td>
</tr>
</tbody></table>
<blockquote>
<p>🤔 <strong>Guess First:</strong> "rumah" appears in both d1 and d2, while "bagus" only appears in d1. Do you think the TF-IDF weight of "bagus" in d1 is higher or lower than "rumah" in d1?</p>
<p>Reveal Answer</p>
<p><strong>Higher</strong> (0.623 vs 0.474). "rumah" appears in 2 out of 3 documents, so it's considered less distinctive → its weight gets pulled down. "bagus" only appears in 1 document (d1) → it's considered more distinctive for that document → its weight is higher. That's the core idea behind IDF: the rarer a word is across the whole corpus, the more "distinguishing power" it carries for the document it appears in.</p>
</blockquote>
<blockquote>
<p>💡 <strong>Beyond the core material:</strong> There's a more advanced method called <strong>Word2Vec</strong> — a small neural network that learns word meaning from surrounding words, rather than just counting frequency. The result: words with similar meanings end up with vector positions close to each other (for instance, "King" and "Queen" end up close together along a dimension roughly representing "power/rank"). This is a natural next step after mastering BoW and TF-IDF.</p>
</blockquote>
<hr />
<h2>6. Reducing Dimensions with PCA</h2>
<p>Sometimes data has too many columns/features, making it hard to visualize or slowing a model down. <strong>PCA (Principal Component Analysis)</strong> compresses data into fewer dimensions while preserving as much of the important information as possible.</p>
<p>Example: the Iris flower dataset (4 features — petal/sepal length &amp; width) compressed down to 2 dimensions.</p>
<pre><code class="language-python">iris = load_iris()
X = iris.data   # 4 features: petal/sepal length &amp; width
y = iris.target # flower species

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

plt.figure(figsize=(6, 5))
scatter = plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y, cmap='viridis')
plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.title('Data Iris setelah PCA (4 dimensi -&gt; 2 dimensi)')
plt.legend(handles=scatter.legend_elements()[0], labels=list(iris.target_names))
plt.show()

print(f"Dimensi awal: {X.shape[1]} fitur")
print(f"Dimensi setelah PCA: {X_pca.shape[1]} fitur")
print(f"Informasi yang berhasil dijaga: {pca.explained_variance_ratio_.sum()*100:.1f}%")
</code></pre>
<p>(The plot title and print labels are left exactly as run — "Dimensi awal" = original dimensions, "Dimensi setelah PCA" = dimensions after PCA, "Informasi yang berhasil dijaga" = information preserved.)</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/dd5af023-9785-4ab4-ba7a-d2fd1c588641.png" alt="Scatter plot of Iris data after PCA, 3 flower species visibly clustering apart" style="display:block;margin:0 auto" />

<p>Output of the three <code>print</code> lines above, plus the per-component breakdown (<code>pca.explained_variance_ratio_</code>):</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td>Original dimensions</td>
<td>4 features</td>
</tr>
<tr>
<td>Dimensions after PCA</td>
<td>2 features</td>
</tr>
<tr>
<td>Variance kept by PC1</td>
<td>92.5%</td>
</tr>
<tr>
<td>Variance kept by PC2</td>
<td>5.3%</td>
</tr>
<tr>
<td><strong>Total information preserved</strong></td>
<td><strong>97.8%</strong></td>
</tr>
</tbody></table>
<p>In other words: the original 4 columns get cut down to 2, but only about 2.2% of the information is lost — a trade-off that's usually well worth it for visualization or speeding up model training. The scatter plot also shows it clearly: the <em>setosa</em> species (cyan) is already cleanly separated from the other two even with just 2 dimensions, while <em>versicolor</em> (blue) and <em>virginica</em> (purple) overlap a bit in the middle.</p>
<hr />
<h2>7. The Data Versioning Concept</h2>
<p>A simple analogy: saving a document as <code>report_v1</code>, then <code>report_v2</code> after a revision — so if the new version turns out wrong, you can still go back to the old one. <strong>Data versioning</strong> applies the same idea to datasets.</p>
<p>Why it matters for Machine Learning:</p>
<ul>
<li><p>If a model suddenly gets worse after a data update, you can <strong>roll back</strong> to the previous data version.</p>
</li>
<li><p>Model development becomes easier to <strong>track</strong> — which matters a lot for debugging and audits.</p>
</li>
</ul>
<p>Commonly used real-world tools:</p>
<table>
<thead>
<tr>
<th>Category</th>
<th>Example Tools</th>
</tr>
</thead>
<tbody><tr>
<td>Dataset versioning</td>
<td>DVC, GitLab</td>
</tr>
<tr>
<td>Annotation + versioning</td>
<td>CVAT, Roboflow, SuperAnnotate</td>
</tr>
<tr>
<td>Experiment tracking</td>
<td>MLflow, Neptune.ai, Weights &amp; Biases</td>
</tr>
</tbody></table>
<p>The simplest possible simulation: saving each dataset "version" as a separate file.</p>
<pre><code class="language-python">import os
os.makedirs('data_versions', exist_ok=True)

df_v1 = pd.DataFrame({'nama': ['Apple', 'Chicken'], 'kalori': [95, 231]})
df_v1.to_csv('data_versions/dataset_v1.csv', index=False)

df_v2 = df_v1.copy()
df_v2.loc[len(df_v2)] = ['Broccoli', 50]
df_v2.to_csv('data_versions/dataset_v2.csv', index=False)
</code></pre>
<p>(<code>kalori</code> = calories.)</p>
<p><code>dataset_v1.csv</code> holds 2 rows (Apple, Chicken); <code>dataset_v2.csv</code> holds 3 rows (Apple, Chicken, Broccoli) — a small ("micro") change gets saved as a separate file, so the old version stays accessible at any time.</p>
<blockquote>
<p>⚠️ <strong>Transparency note:</strong> the example above is just a concept simulation using manually separated files — it's not how real versioning tools like DVC actually work under the hood (they use content hashing and storage separate from Git). It's simplified here so the core idea is easy to grasp without extra installation.</p>
</blockquote>
<hr />
<h2>8. Summary &amp; Next Steps</h2>
<table>
<thead>
<tr>
<th>Stage</th>
<th>What Was Covered</th>
</tr>
</thead>
<tbody><tr>
<td>Missing Values</td>
<td><code>dropna()</code> and <code>fillna()</code></td>
</tr>
<tr>
<td>Outliers</td>
<td>IQR detection + boxplot</td>
</tr>
<tr>
<td>Feature Engineering</td>
<td>Label Encoding &amp; One Hot Encoding</td>
</tr>
<tr>
<td>Text Preprocessing</td>
<td>Bag of Words &amp; TF-IDF</td>
</tr>
<tr>
<td>Dimensionality Reduction</td>
<td>PCA</td>
</tr>
<tr>
<td>Data Versioning</td>
<td>The concept of keeping a dataset's version history</td>
</tr>
</tbody></table>
<p>A few next steps worth trying on your own:</p>
<ul>
<li><p>Swap in your own dataset instead of the examples above.</p>
</li>
<li><p>Explore the <code>gensim</code> library to go deeper into Word2Vec.</p>
</li>
<li><p>Try an open-source tool like <strong>DVC</strong> for real, production-grade data versioning.</p>
</li>
</ul>
<hr />
<h2>9. Cheat Sheet: Preprocessing Checklist</h2>
<ul>
<li><p>[ ] <strong>Check for missing values first</strong> with <code>.isnull().sum()</code> before assuming the data is dirty</p>
</li>
<li><p>[ ] Small, non-critical gaps → <code>dropna()</code>; valuable data or large gaps → <code>fillna()</code> (mean/median/mode)</p>
</li>
<li><p>[ ] Detect outliers with IQR, but <strong>check the business context</strong> before dropping — a statistical outlier isn't automatically a data error, and outliers can show up on either side of the distribution</p>
</li>
<li><p>[ ] Ordered categories (low–medium–high) → Label Encoding; unordered categories → One Hot Encoding</p>
</li>
<li><p>[ ] Short/simple text → Bag of Words; need to distinguish "distinctive" words from common ones → TF-IDF</p>
</li>
<li><p>[ ] Too many features → consider PCA, and check how much information is actually retained</p>
</li>
<li><p>[ ] Keep a version history of your data (not just your code) — so you can roll back if a model suddenly gets worse</p>
</li>
</ul>
<hr />
<h2>10. Quiz Check</h2>
<p><strong>1. Why check</strong> <code>.isnull().sum()</code> <strong>first, before jumping straight to dropna/fillna?</strong></p>
<p>Because not every dataset is automatically "dirty" — in the customer dataset example above, the result was 0% missing values across every column. Skipping the check and applying a cleaning strategy anyway risks wasting time (or worse, discarding perfectly fine data) on a problem that didn't actually exist.</p>
<p><strong>2. Why was the missing salary filled with the median instead of the mean?</strong></p>
<p>The median is more robust against extreme values/outliers than the mean. If one salary is far larger than the rest, the mean gets pulled upward and becomes less representative, while the median stays stable near the center of the data.</p>
<p><strong>3. IQR flagged 9 outliers even though only 1 was injected intentionally, and they sat on both sides of the distribution. What does that mean?</strong></p>
<p>Statistical methods like IQR have no way of distinguishing "intentionally anomalous" data from values that just happen to sit at either tail of a normal distribution. Everything outside the 1.5×IQR range gets flagged the same way. So outlier detection results still need to be verified against business context before deciding whether to drop or keep them.</p>
<p><strong>4. When should you avoid Label Encoding for categories like city names or food names?</strong></p>
<p>When the category has no natural order/ranking. Label Encoding assigns sequential numbers (0, 1, 2, ...) that a model can accidentally "read" as a mathematical relationship — for instance, treating category 2 as "greater than" category 1, when that ordering was really just alphabetical coincidence. One Hot Encoding is the safer choice here.</p>
<p><strong>5. Why do words that appear in many documents get a lower TF-IDF weight?</strong></p>
<p>Because TF-IDF's whole purpose is measuring how distinctive a word is for telling one document apart from another. A word that shows up in most or all documents (like "rumah" here) doesn't help distinguish between them much, so its IDF weight is pulled down. A rarer word carries more information about the specific document it appears in, so it gets a higher weight.</p>
<hr />
<p><em>Every number and chart in this post comes from actually running the code (not a manual simulation), with the</em> <code>random seed</code> <em>for the outlier section noted so the results are reproducible.</em></p>
]]></content:encoded></item><item><title><![CDATA[Praktik Data Preprocessing dengan Python: dari Missing Value sampai PCA]]></title><description><![CDATA[Catatan belajar plus kode langsung jalan — enam teknik inti data preprocessing yang paling sering dipakai sebelum data masuk ke model Machine Learning, lengkap dengan angka hasil eksekusi asli, bukan ]]></description><link>https://shaka-ai.hashnode.dev/praktik-data-preprocessing-python-missing-value-pca</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/praktik-data-preprocessing-python-missing-value-pca</guid><category><![CDATA[Python]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[pandas]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Mon, 31 Aug 2026 01:23:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/52583bc9-4614-4049-bbe7-9500478c7d88.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><em>Catatan belajar plus kode langsung jalan — enam teknik inti data preprocessing yang paling sering dipakai sebelum data masuk ke model Machine Learning, lengkap dengan angka hasil eksekusi asli, bukan contoh yang cuma ditempel.</em></p>
</blockquote>
<p>Ada ungkapan yang sering diulang-ulang di dunia data science: "80% kerjaan AI itu ngurusin data, bukan model" (angka pastinya bervariasi tergantung sumber, tapi intinya sering disepakati). Biasanya yang dimaksud ya bagian ini: mengecek data kosong, membuang/menangani outlier, mengubah kategori dan teks jadi angka, memampatkan dimensi, dan menyimpan riwayat versi data. Semua praktik di bawah ini dijalankan langsung dengan Python (Pandas + Scikit-learn), pakai dua jenis data: dataset pelanggan sungguhan berisi 10.000 baris, dan beberapa contoh sintetis untuk memperjelas konsep.</p>
<blockquote>
<p>📂 Notebook lengkapnya (sudah dieksekusi penuh, plus dataset-nya) ada di GitHub: <a href="https://github.com/arielshakaramiro/data-preprocessing-praktik-arielshakaramiro">github.com/arielshakaramiro/data-preprocessing-praktik-arielshakaramiro</a></p>
</blockquote>
<h2>Daftar Isi</h2>
<ul>
<li><p><a href="#1-data-collection--storage-konsep-singkat">1. Data Collection &amp; Storage: Konsep Singkat</a></p>
</li>
<li><p><a href="#2-missing-values-cek-dulu-jangan-asumsi">2. Missing Values: Cek Dulu, Jangan Asumsi</a></p>
</li>
<li><p><a href="#3-mendeteksi--menangani-outlier-dengan-iqr">3. Mendeteksi &amp; Menangani Outlier dengan IQR</a></p>
</li>
<li><p><a href="#4-feature-engineering-kategori-jadi-angka">4. Feature Engineering: Kategori Jadi Angka</a></p>
</li>
<li><p><a href="#5-text-preprocessing-bag-of-words--tf-idf">5. Text Preprocessing: Bag of Words &amp; TF-IDF</a></p>
</li>
<li><p><a href="#6-mengurangi-dimensi-data-dengan-pca">6. Mengurangi Dimensi Data dengan PCA</a></p>
</li>
<li><p><a href="#7-konsep-data-versioning">7. Konsep Data Versioning</a></p>
</li>
<li><p><a href="#8-rangkuman--langkah-selanjutnya">8. Rangkuman &amp; Langkah Selanjutnya</a></p>
</li>
<li><p><a href="#9-cheat-sheet-checklist-preprocessing">9. Cheat Sheet: Checklist Preprocessing</a></p>
</li>
<li><p><a href="#10-quiz-check">10. Quiz Check</a></p>
</li>
</ul>
<hr />
<h2>1. Data Collection &amp; Storage: Konsep Singkat</h2>
<p>Sebelum masuk ke praktik, ada dua jenis data yang perlu dikenali:</p>
<ul>
<li><p><strong>Unstructured Data</strong> — data yang "bentuknya bebas", misalnya gambar, teks, audio. Manusia bisa langsung memahami maknanya tanpa struktur khusus (contoh: kita bisa langsung mengenali gambar kucing).</p>
</li>
<li><p><strong>Structured Data</strong> — data yang rapi dalam bentuk tabel/baris-kolom, misalnya data transaksi atau data pelanggan. Maknanya sangat tergantung konteks bisnis, sehingga tidak selalu bisa dipahami "sekilas lihat" oleh orang awam.</p>
</li>
</ul>
<p>Kedua jenis data ini butuh tempat penyimpanan yang <strong>aman, mudah diakses, dan bisa di-versioning</strong> (disimpan riwayat perubahannya) — konsep versioning ini dibahas lebih lanjut di bagian 7.</p>
<hr />
<h2>2. Missing Values: Cek Dulu, Jangan Asumsi</h2>
<p>Ada dua pendekatan umum menangani nilai kosong (<code>NaN</code>):</p>
<ul>
<li><p><strong>Dropna</strong> — buang baris/kolom yang datanya kosong. Cocok kalau yang hilang sedikit.</p>
</li>
<li><p><strong>Fillna</strong> — isi nilai kosong dengan pengganti: mean, median, atau modus (nilai paling sering muncul).</p>
</li>
</ul>
<p><strong>Langkah pertama yang sering dilewatkan pemula: cek dulu, jangan langsung asumsi datanya kotor.</strong></p>
<pre><code class="language-python">sample_data = pd.read_csv('customers-10000.csv')
print(sample_data.isnull().sum())
</code></pre>
<p><em>(Kode aslinya memuat dataset lewat link Google Drive; di sini ditulis seolah file lokal supaya gampang dicoba ulang — isinya persis sama, dataset publik 10.000 baris dari Datablist.)</em></p>
<p>Hasil eksekusi pada dataset pelanggan (10.000 baris, 12 kolom — <code>Index</code>, <code>Customer Id</code>, <code>First Name</code>, <code>Last Name</code>, <code>Company</code>, <code>City</code>, <code>Country</code>, <code>Phone 1</code>, <code>Phone 2</code>, <code>Email</code>, <code>Subscription Date</code>, <code>Website</code>):</p>
<pre><code class="language-plaintext">Missing value per kolom: 0 (semua kolom, 0.0%)
</code></pre>
<blockquote>
<p>🤔 <strong>Coba Tebak Dulu:</strong> Menurutmu, dataset pelanggan publik seperti ini biasanya berapa persen datanya yang kosong?</p>
<p>Lihat Jawaban</p>
<p>Di kasus ini: <strong>0%</strong> — dataset ini memang dataset sintetis untuk latihan (dibuat dengan Faker), jadi sengaja dibuat rapi tanpa nilai kosong. Ini justru pelajaran penting: jangan pernah asumsikan sebuah dataset "pasti kotor". Selalu jalankan <code>.isnull().sum()</code> dulu sebelum memutuskan strategi apa pun — kalau ternyata bersih, kamu hemat waktu; kalau ternyata ada yang kosong, kamu tahu persis di kolom mana.</p>
</blockquote>
<p>Untuk melihat kedua teknik penanganannya, dipakai data sintetis kecil yang sengaja dibuat berlubang:</p>
<pre><code class="language-python">data = {
    'nama': ['Andi', 'Budi', 'Citra', 'Dewi', 'Eka'],
    'umur': [25, np.nan, 30, 22, np.nan],
    'gaji': [5000000, 6000000, np.nan, 4500000, 5200000]
}
df = pd.DataFrame(data)
</code></pre>
<table>
<thead>
<tr>
<th>nama</th>
<th>umur</th>
<th>gaji</th>
</tr>
</thead>
<tbody><tr>
<td>Andi</td>
<td>25</td>
<td>5.000.000</td>
</tr>
<tr>
<td>Budi</td>
<td>NaN</td>
<td>6.000.000</td>
</tr>
<tr>
<td>Citra</td>
<td>30</td>
<td>NaN</td>
</tr>
<tr>
<td>Dewi</td>
<td>22</td>
<td>4.500.000</td>
</tr>
<tr>
<td>Eka</td>
<td>NaN</td>
<td>5.200.000</td>
</tr>
</tbody></table>
<p>Jumlah &amp; persentase missing value per kolom (hasil eksekusi <code>df.isnull().sum()</code> dan <code>df.isnull().sum() / len(df) * 100</code>):</p>
<table>
<thead>
<tr>
<th>Kolom</th>
<th>Jumlah Kosong</th>
<th>Persentase</th>
</tr>
</thead>
<tbody><tr>
<td>nama</td>
<td>0</td>
<td>0%</td>
</tr>
<tr>
<td>umur</td>
<td>2</td>
<td>40%</td>
</tr>
<tr>
<td>gaji</td>
<td>1</td>
<td>20%</td>
</tr>
</tbody></table>
<p><strong>Opsi 1 —</strong> <code>dropna()</code><strong>:</strong></p>
<pre><code class="language-python">df_dropped = df.dropna()
</code></pre>
<p>Baris Budi, Citra, Eka ikut terbuang karena masing-masing punya satu sel kosong — dari 5 baris tersisa 2 baris. Efektif kalau data yang hilang cuma sedikit dan baris yang hilang tidak penting; boros kalau data berharga tapi kebetulan ada satu kolom bolong.</p>
<p><strong>Opsi 2 —</strong> <code>fillna()</code> <strong>dengan mean/median:</strong></p>
<pre><code class="language-python">df_filled = df.copy()
df_filled['umur'] = df_filled['umur'].fillna(df_filled['umur'].mean())
df_filled['gaji'] = df_filled['gaji'].fillna(df_filled['gaji'].median())
</code></pre>
<p>Umur kosong diisi rata-rata umur yang ada, gaji kosong diisi nilai tengah (median). <em>(Tambahan penjelasan di luar kode aslinya: median dipilih untuk gaji karena secara umum lebih tahan terhadap nilai ekstrem dibanding mean — poin ini akan lebih jelas maknanya setelah bagian outlier di bawah.)</em></p>
<hr />
<h2>3. Mendeteksi &amp; Menangani Outlier dengan IQR</h2>
<p>Outlier adalah data yang nilainya jauh berbeda dari mayoritas. Aturan pengambilan keputusannya:</p>
<ul>
<li><p>Outlier <strong>jarang terjadi dan tidak berdampak ke bisnis</strong> → boleh dibuang.</p>
</li>
<li><p>Outlier <strong>mengganggu proses bisnis</strong> → jangan dibuang begitu saja, perlu ditangani lebih hati-hati (di skala production, salah satu pendekatan lanjutan adalah <em>Autoencoder</em> — model kecil yang belajar pola data normal, sehingga data yang menyimpang jauh dari pola itu punya error rekonstruksi besar dan bisa ditandai sebagai outlier).</p>
</li>
</ul>
<p>Untuk pemula, salah satu metode paling umum dan sederhana adalah <strong>IQR (Interquartile Range)</strong>. Simulasinya: 1000 data gaji berdistribusi normal (rata-rata Rp5.000.000, standar deviasi Rp1.000.000), lalu disisipkan 1 outlier ekstrem sebesar Rp50.000.000.</p>
<pre><code class="language-python"># Membuat contoh data gaji, dengan 1 outlier yang sangat besar
np.random.seed(42)
gaji = np.random.normal(5000000, 1000000, 1000).tolist()
gaji.append(50000000)  # outlier: gaji yang jauh lebih besar dari yang lain
df_gaji = pd.DataFrame({'gaji': gaji})
</code></pre>
<p>Visualisasi cepat sebelum dihitung — kode boxplot-nya:</p>
<pre><code class="language-python">plt.figure(figsize=(6, 4))
plt.boxplot(df_gaji['gaji'])
plt.title('Boxplot Gaji (perhatikan ada titik yang jauh di atas)')
plt.ylabel('Gaji')
plt.show()
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/dba698ff-d045-43e3-8f64-4ff0bdf1b38a.png" alt="Boxplot Gaji menunjukkan satu titik outlier ekstrem jauh di atas kotak IQR" style="display:block;margin:0 auto" />

<p>Baru dihitung batasnya secara matematis dengan IQR:</p>
<pre><code class="language-python"># Deteksi outlier dengan metode IQR
Q1 = df_gaji['gaji'].quantile(0.25)
Q3 = df_gaji['gaji'].quantile(0.75)
IQR = Q3 - Q1
batas_bawah = Q1 - 1.5 * IQR
batas_atas = Q3 + 1.5 * IQR

outliers = df_gaji[(df_gaji['gaji'] &lt; batas_bawah) | (df_gaji['gaji'] &gt; batas_atas)]
</code></pre>
<p>Hasil eksekusi nyata (dengan <code>random seed 42</code>, jadi angkanya konsisten kalau dijalankan ulang):</p>
<table>
<thead>
<tr>
<th>Metrik</th>
<th>Nilai</th>
</tr>
</thead>
<tbody><tr>
<td>Q1 (kuartil bawah)</td>
<td>Rp4.353.427</td>
</tr>
<tr>
<td>Q3 (kuartil atas)</td>
<td>Rp5.648.710</td>
</tr>
<tr>
<td>IQR</td>
<td>Rp1.295.283</td>
</tr>
<tr>
<td>Batas bawah</td>
<td>Rp2.410.503</td>
</tr>
<tr>
<td>Batas atas</td>
<td>Rp7.591.634</td>
</tr>
<tr>
<td>Outlier terdeteksi</td>
<td><strong>9 dari 1.001 data</strong></td>
</tr>
</tbody></table>
<blockquote>
<p>🤔 <strong>Coba Tebak Dulu:</strong> Cuma disisipkan 1 outlier buatan (Rp50 juta). Menurutmu, IQR bakal mendeteksi tepat 1 outlier juga, atau bisa lebih?</p>
<p>Lihat Jawaban</p>
<p>Terdeteksi <strong>9 outlier</strong>, bukan cuma 1. Selain angka Rp50 juta yang memang disisipkan, ada 8 data lain (hasil distribusi normal biasa, bukan disengaja) yang kebetulan jatuh di luar rentang 1,5×IQR — dan posisinya di <strong>dua sisi</strong>: 4 di bawah batas bawah (sekitar Rp1,76–2,38 juta) dan 4 di atas batas atas (sekitar Rp7,63–8,85 juta). Ini pelajaran penting: metode IQR itu murni statistik, dia tidak tahu mana outlier yang "disengaja/anomali" dan mana yang cuma "kebetulan ekstrem karena sebaran data". Makanya sebelum membuang data yang ditandai IQR, tetap perlu dicek konteks bisnisnya — misalnya gaji Rp8 juta di dataset ini mungkin memang gaji direktur yang valid, bukan kesalahan input, dan gaji Rp1,8 juta mungkin memang pegawai magang.</p>
</blockquote>
<p>Setelah dibuang, sisa data bersih:</p>
<pre><code class="language-python">df_bersih = df_gaji[(df_gaji['gaji'] &gt;= batas_bawah) &amp; (df_gaji['gaji'] &lt;= batas_atas)]
</code></pre>
<p><strong>992 dari 1.001 baris</strong> tersisa.</p>
<hr />
<h2>4. Feature Engineering: Kategori Jadi Angka</h2>
<p>Model Machine Learning cuma mengerti angka, bukan teks kategori seperti "Apple" atau "Chicken". Dua cara paling umum mengonversinya:</p>
<table>
<thead>
<tr>
<th>Teknik</th>
<th>Cara Kerja</th>
<th>Kapan Dipakai</th>
</tr>
</thead>
<tbody><tr>
<td>Label Encoding</td>
<td>Setiap kategori diberi 1 angka unik</td>
<td>Kategori yang punya urutan/tingkatan (rendah–sedang–tinggi)</td>
</tr>
<tr>
<td>One Hot Encoding</td>
<td>Setiap kategori jadi kolom sendiri berisi 0/1</td>
<td>Kategori tanpa urutan (nama makanan, kota, dll) — lebih aman secara default</td>
</tr>
</tbody></table>
<p>Data contoh:</p>
<pre><code class="language-python">food = pd.DataFrame({
    'Food Name': ['Apple', 'Chicken', 'Broccoli'],
    'Calories': [95, 231, 50]
})
</code></pre>
<table>
<thead>
<tr>
<th>Food Name</th>
<th>Calories</th>
</tr>
</thead>
<tbody><tr>
<td>Apple</td>
<td>95</td>
</tr>
<tr>
<td>Chicken</td>
<td>231</td>
</tr>
<tr>
<td>Broccoli</td>
<td>50</td>
</tr>
</tbody></table>
<p><strong>Label Encoding</strong> (hasil eksekusi <code>LabelEncoder</code>):</p>
<table>
<thead>
<tr>
<th>Food Name</th>
<th>Calories</th>
<th>Categorical #</th>
</tr>
</thead>
<tbody><tr>
<td>Apple</td>
<td>95</td>
<td>0</td>
</tr>
<tr>
<td>Chicken</td>
<td>231</td>
<td>2</td>
</tr>
<tr>
<td>Broccoli</td>
<td>50</td>
<td>1</td>
</tr>
</tbody></table>
<p>Perhatikan angkanya diurutkan alfabetis (Apple=0, Broccoli=1, Chicken=2) — bukan berdasarkan kalori atau urutan logis apa pun. Ini sumber bug klasik: kalau dipakai untuk kategori tanpa urutan, model bisa salah "mengira" Chicken (2) lebih besar/penting dua kali lipat dari Broccoli (1), padahal itu cuma nomor urut alfabet.</p>
<p><strong>One Hot Encoding</strong> (hasil eksekusi <code>pd.get_dummies</code>):</p>
<table>
<thead>
<tr>
<th>Calories</th>
<th>Food Name_Apple</th>
<th>Food Name_Broccoli</th>
<th>Food Name_Chicken</th>
</tr>
</thead>
<tbody><tr>
<td>95</td>
<td>1</td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td>231</td>
<td>0</td>
<td>0</td>
<td>1</td>
</tr>
<tr>
<td>50</td>
<td>0</td>
<td>1</td>
<td>0</td>
</tr>
</tbody></table>
<p>Tidak ada urutan tersirat — setiap kategori dapat kolom sendiri. Trade-off-nya: kalau kategorinya ratusan/ribuan (misalnya kode pos), jumlah kolom bisa meledak.</p>
<hr />
<h2>5. Text Preprocessing: Bag of Words &amp; TF-IDF</h2>
<p>Sama seperti kategori, teks juga perlu diubah jadi vektor angka (vektorisasi). Korpus contoh (3 kalimat pendek Bahasa Indonesia):</p>
<pre><code class="language-python">corpus = [
    "rumah ini bagus",        # d1
    "rumah saya makan nasi",  # d2
    "saya makan nasi"         # d3
]
</code></pre>
<p><strong>Bag of Words</strong> — menghitung berapa kali tiap kata muncul di tiap dokumen:</p>
<pre><code class="language-python">vectorizer = CountVectorizer()
bow_matrix = vectorizer.fit_transform(corpus)

bow_df = pd.DataFrame(
    bow_matrix.toarray(),
    columns=vectorizer.get_feature_names_out(),
    index=['d1', 'd2', 'd3']
)
</code></pre>
<p>Hasil eksekusi (<code>bow_df</code>):</p>
<table>
<thead>
<tr>
<th></th>
<th>bagus</th>
<th>ini</th>
<th>makan</th>
<th>nasi</th>
<th>rumah</th>
<th>saya</th>
</tr>
</thead>
<tbody><tr>
<td>d1</td>
<td>1</td>
<td>1</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>0</td>
</tr>
<tr>
<td>d2</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>d3</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>0</td>
<td>1</td>
</tr>
</tbody></table>
<p>BoW punya kelemahan: kata yang sering muncul di <em>semua</em> dokumen dianggap "penting" secara nilai, padahal belum tentu informatif untuk membedakan dokumen satu dengan lainnya.</p>
<p><strong>TF-IDF (Term Frequency–Inverse Document Frequency)</strong> memperbaiki ini — kata yang muncul di banyak dokumen diberi bobot lebih rendah, kata yang unik/jarang diberi bobot lebih tinggi:</p>
<pre><code class="language-python">tfidf_vectorizer = TfidfVectorizer()
tfidf_matrix = tfidf_vectorizer.fit_transform(corpus)

tfidf_df = pd.DataFrame(
    tfidf_matrix.toarray(),
    columns=tfidf_vectorizer.get_feature_names_out(),
    index=['d1', 'd2', 'd3']
).round(3)
</code></pre>
<p>Hasil eksekusi (<code>tfidf_df</code>):</p>
<table>
<thead>
<tr>
<th></th>
<th>bagus</th>
<th>ini</th>
<th>makan</th>
<th>nasi</th>
<th>rumah</th>
<th>saya</th>
</tr>
</thead>
<tbody><tr>
<td>d1</td>
<td>0.623</td>
<td>0.623</td>
<td>0.000</td>
<td>0.000</td>
<td>0.474</td>
<td>0.000</td>
</tr>
<tr>
<td>d2</td>
<td>0.000</td>
<td>0.000</td>
<td>0.500</td>
<td>0.500</td>
<td>0.500</td>
<td>0.500</td>
</tr>
<tr>
<td>d3</td>
<td>0.000</td>
<td>0.000</td>
<td>0.577</td>
<td>0.577</td>
<td>0.000</td>
<td>0.577</td>
</tr>
</tbody></table>
<blockquote>
<p>🤔 <strong>Coba Tebak Dulu:</strong> Kata "rumah" muncul di d1 dan d2, sedangkan "bagus" cuma muncul di d1. Menurutmu, bobot TF-IDF kata "bagus" di d1 lebih tinggi atau lebih rendah dibanding bobot "rumah" di d1?</p>
<p>Lihat Jawaban</p>
<p><strong>Lebih tinggi</strong> (0.623 vs 0.474). Karena "rumah" muncul di 2 dari 3 dokumen, ia dianggap kurang khas/kurang membedakan → bobotnya diturunkan. "Bagus" cuma muncul di 1 dokumen (d1) → dianggap lebih khas untuk dokumen itu → bobotnya lebih tinggi. Inilah inti IDF: makin jarang sebuah kata muncul di seluruh korpus, makin tinggi "nilai pembeda"-nya untuk dokumen tempat ia muncul.</p>
</blockquote>
<blockquote>
<p>💡 <strong>Tambahan di luar materi utama:</strong> Ada metode yang lebih canggih bernama <strong>Word2Vec</strong> — neural network kecil yang mempelajari makna kata dari kata-kata di sekitarnya, bukan sekadar menghitung frekuensi. Hasilnya, kata-kata bermakna mirip akan punya posisi vektor yang berdekatan (misalnya "Raja" dan "Ratu" berdekatan pada dimensi tertentu yang merepresentasikan "kekuasaan"). Ini topik lanjutan setelah menguasai BoW dan TF-IDF.</p>
</blockquote>
<hr />
<h2>6. Mengurangi Dimensi Data dengan PCA</h2>
<p>Kadang data punya terlalu banyak kolom/fitur, sehingga sulit divisualisasikan atau bikin model lambat. <strong>PCA (Principal Component Analysis)</strong> memampatkan data ke dimensi lebih kecil sambil mempertahankan informasi paling penting.</p>
<p>Contoh: dataset bunga Iris (4 fitur: panjang &amp; lebar kelopak/mahkota) dimampatkan jadi 2 dimensi.</p>
<pre><code class="language-python">iris = load_iris()
X = iris.data   # 4 fitur: panjang &amp; lebar kelopak/mahkota bunga
y = iris.target # jenis bunga

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

plt.figure(figsize=(6, 5))
scatter = plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y, cmap='viridis')
plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.title('Data Iris setelah PCA (4 dimensi -&gt; 2 dimensi)')
plt.legend(handles=scatter.legend_elements()[0], labels=list(iris.target_names))
plt.show()

print(f"Dimensi awal: {X.shape[1]} fitur")
print(f"Dimensi setelah PCA: {X_pca.shape[1]} fitur")
print(f"Informasi yang berhasil dijaga: {pca.explained_variance_ratio_.sum()*100:.1f}%")
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/a143e63f-4778-4a20-a0a4-7333d823b77f.png" alt="Scatter plot data Iris setelah PCA, 3 spesies bunga terlihat mengelompok terpisah" style="display:block;margin:0 auto" />

<p>Output dari ketiga baris <code>print</code> di atas, plus rincian per komponen (<code>pca.explained_variance_ratio_</code>):</p>
<table>
<thead>
<tr>
<th>Metrik</th>
<th>Nilai</th>
</tr>
</thead>
<tbody><tr>
<td>Dimensi awal</td>
<td>4 fitur</td>
</tr>
<tr>
<td>Dimensi setelah PCA</td>
<td>2 fitur</td>
</tr>
<tr>
<td>Variansi dijaga oleh PC1</td>
<td>92.5%</td>
</tr>
<tr>
<td>Variansi dijaga oleh PC2</td>
<td>5.3%</td>
</tr>
<tr>
<td><strong>Total informasi yang dijaga</strong></td>
<td><strong>97.8%</strong></td>
</tr>
</tbody></table>
<p>Artinya: dari 4 kolom asli dipangkas jadi 2 kolom, tapi cuma kehilangan sekitar 2,2% informasi — trade-off yang biasanya sangat sepadan untuk keperluan visualisasi atau mempercepat training model. Dari scatter plot-nya juga terlihat: spesies <em>setosa</em> (cyan) sudah terpisah jelas dari dua spesies lain bahkan cuma dengan 2 dimensi, sementara <em>versicolor</em> (biru) dan <em>virginica</em> (ungu) sedikit tumpang tindih di tengah.</p>
<hr />
<h2>7. Konsep Data Versioning</h2>
<p>Analogi sederhana: menyimpan dokumen sebagai <code>laporan_v1</code>, lalu <code>laporan_v2</code> setelah direvisi — supaya kalau versi baru ternyata salah, masih bisa kembali ke versi lama. <strong>Data versioning</strong> menerapkan ide yang sama untuk dataset.</p>
<p>Kenapa penting untuk Machine Learning:</p>
<ul>
<li><p>Kalau model tiba-tiba memburuk setelah data diperbarui, bisa <strong>rollback</strong> ke versi data sebelumnya.</p>
</li>
<li><p>Proses pengembangan model jadi lebih <strong>tertrack</strong> (mudah dilacak riwayatnya) — penting untuk debugging dan audit.</p>
</li>
</ul>
<p>Contoh tools yang lazim dipakai di dunia nyata:</p>
<table>
<thead>
<tr>
<th>Kategori</th>
<th>Contoh Tools</th>
</tr>
</thead>
<tbody><tr>
<td>Versioning dataset</td>
<td>DVC, GitLab</td>
</tr>
<tr>
<td>Anotasi + versioning</td>
<td>CVAT, Roboflow, SuperAnnotate</td>
</tr>
<tr>
<td>Tracking eksperimen model</td>
<td>MLflow, Neptune.ai, Weights &amp; Biases</td>
</tr>
</tbody></table>
<p>Simulasi paling sederhana: menyimpan tiap "versi" dataset sebagai file terpisah.</p>
<pre><code class="language-python">import os
os.makedirs('data_versions', exist_ok=True)

df_v1 = pd.DataFrame({'nama': ['Apple', 'Chicken'], 'kalori': [95, 231]})
df_v1.to_csv('data_versions/dataset_v1.csv', index=False)

df_v2 = df_v1.copy()
df_v2.loc[len(df_v2)] = ['Broccoli', 50]
df_v2.to_csv('data_versions/dataset_v2.csv', index=False)
</code></pre>
<p><code>dataset_v1.csv</code> berisi 2 baris (Apple, Chicken), <code>dataset_v2.csv</code> berisi 3 baris (Apple, Chicken, Broccoli) — perubahan kecil ("micro change") tersimpan sebagai file terpisah, sehingga versi lama tetap bisa diakses kapan saja.</p>
<blockquote>
<p>⚠️ <strong>Catatan transparansi:</strong> contoh di atas cuma simulasi konsep dengan menyimpan file terpisah manual — bukan cara kerja tool versioning sungguhan seperti DVC (yang pakai sistem hashing dan penyimpanan terpisah dari Git). Ini disederhanakan supaya intinya mudah dipahami tanpa perlu instalasi tambahan.</p>
</blockquote>
<hr />
<h2>8. Rangkuman &amp; Langkah Selanjutnya</h2>
<table>
<thead>
<tr>
<th>Tahap</th>
<th>Yang Dipelajari</th>
</tr>
</thead>
<tbody><tr>
<td>Missing Values</td>
<td><code>dropna()</code> dan <code>fillna()</code></td>
</tr>
<tr>
<td>Outlier</td>
<td>Deteksi dengan metode IQR + boxplot</td>
</tr>
<tr>
<td>Feature Engineering</td>
<td>Label Encoding &amp; One Hot Encoding</td>
</tr>
<tr>
<td>Text Preprocessing</td>
<td>Bag of Words &amp; TF-IDF</td>
</tr>
<tr>
<td>Dimensionality Reduction</td>
<td>PCA</td>
</tr>
<tr>
<td>Data Versioning</td>
<td>Konsep menyimpan riwayat versi dataset</td>
</tr>
</tbody></table>
<p>Langkah lanjutan yang bisa dicoba sendiri:</p>
<ul>
<li><p>Ganti contoh data di atas dengan dataset sendiri.</p>
</li>
<li><p>Eksplorasi library <code>gensim</code> untuk mempelajari Word2Vec lebih lanjut.</p>
</li>
<li><p>Coba tools open-source seperti <strong>DVC</strong> untuk praktik data versioning yang sesungguhnya.</p>
</li>
</ul>
<hr />
<h2>9. Cheat Sheet: Checklist Preprocessing</h2>
<ul>
<li><p>[ ] <strong>Cek missing value dulu</strong> dengan <code>.isnull().sum()</code> sebelum asumsi data kotor</p>
</li>
<li><p>[ ] Data hilang sedikit &amp; tidak krusial → <code>dropna()</code>; data berharga/hilang banyak → <code>fillna()</code> (mean/median/modus)</p>
</li>
<li><p>[ ] Deteksi outlier dengan IQR, tapi <strong>cek konteks bisnis</strong> sebelum membuang — outlier statistik ≠ otomatis kesalahan data, dan outlier bisa muncul di dua sisi (atas maupun bawah)</p>
</li>
<li><p>[ ] Kategori berurutan (rendah–sedang–tinggi) → Label Encoding; kategori tanpa urutan → One Hot Encoding</p>
</li>
<li><p>[ ] Teks pendek/simpel → Bag of Words; butuh membedakan kata "khas" vs kata umum → TF-IDF</p>
</li>
<li><p>[ ] Fitur terlalu banyak → pertimbangkan PCA, cek berapa persen informasi yang masih terjaga</p>
</li>
<li><p>[ ] Simpan riwayat versi data (bukan cuma versi kode) — supaya bisa rollback kalau model tiba-tiba memburuk</p>
</li>
</ul>
<hr />
<h2>10. Quiz Check</h2>
<p><strong>1. Kenapa mengecek</strong> <code>.isnull().sum()</code> <strong>lebih dulu itu penting, sebelum langsung pakai dropna/fillna?</strong></p>
<p>Karena tidak semua dataset otomatis "kotor" — pada contoh dataset pelanggan di atas, hasilnya 0% missing value di semua kolom. Kalau langsung menjalankan strategi pembersihan tanpa cek dulu, bisa jadi kamu membuang waktu (atau lebih parah, membuang data yang sebenarnya baik-baik saja) untuk masalah yang tidak ada.</p>
<p><strong>2. Kenapa gaji yang kosong diisi dengan median, bukan mean?</strong></p>
<p>Median lebih tahan (robust) terhadap nilai ekstrem/outlier dibanding mean. Kalau ada satu gaji yang jauh lebih besar dari yang lain, mean akan "tertarik" ke atas dan jadi kurang representatif, sedangkan median tetap stabil di tengah data.</p>
<p><strong>3. Metode IQR mendeteksi 9 outlier padahal cuma 1 yang disisipkan sengaja, dan posisinya tersebar di dua sisi. Apa artinya?</strong></p>
<p>Metode statistik seperti IQR tidak tahu mana data yang "sengaja aneh" dan mana yang "kebetulan berada di ujung distribusi normal" — baik di sisi atas maupun bawah. Semua yang berada di luar rentang 1,5×IQR ditandai sama. Karena itu, hasil deteksi outlier tetap perlu diverifikasi dengan pemahaman konteks bisnis sebelum diputuskan dibuang atau dipertahankan.</p>
<p><strong>4. Kapan sebaiknya menghindari Label Encoding untuk kategori seperti nama kota atau nama makanan?</strong></p>
<p>Ketika kategori itu tidak punya urutan/tingkatan alami. Label Encoding memberi angka berurutan (0, 1, 2, ...) yang secara tidak sengaja bisa "dibaca" model sebagai hubungan matematis (misalnya kategori bernomor 2 dianggap "lebih besar" dari kategori bernomor 1), padahal urutan itu cuma kebetulan alfabetis. One Hot Encoding lebih aman untuk kasus ini.</p>
<p><strong>5. Kenapa bobot TF-IDF kata yang muncul di banyak dokumen justru lebih rendah?</strong></p>
<p>Karena tujuan TF-IDF adalah mengukur seberapa "khas" sebuah kata untuk membedakan satu dokumen dari dokumen lain. Kata yang muncul di banyak/semua dokumen (seperti "rumah" di kasus ini) tidak banyak membantu membedakan dokumen, jadi diberi bobot (IDF) lebih rendah. Kata yang jarang muncul justru lebih informatif untuk membedakan dokumen tempat ia muncul, sehingga diberi bobot lebih tinggi.</p>
<hr />
<p><em>Semua angka dan chart pada tulisan ini adalah hasil eksekusi kode secara langsung (bukan simulasi manual), dengan</em> <code>random seed</code> <em>yang dicantumkan pada bagian outlier supaya bisa direproduksi.</em></p>
]]></content:encoded></item><item><title><![CDATA[From Messy Data to Production-Ready: Building an ETL Pipeline with Python, Pandas & pytest]]></title><description><![CDATA[Most data science courses start with a clean dataset. Real-world data doesn't work that way. What you actually get is a CSV with categories written in inconsistent casing, a date column mixing four di]]></description><link>https://shaka-ai.hashnode.dev/etl-pipeline-messy-data-to-production-ready</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/etl-pipeline-messy-data-to-production-ready</guid><category><![CDATA[Python]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Sun, 30 Aug 2026 14:31:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/0e72e5ae-775d-4ca2-8b0c-766ff3a52271.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most data science courses start with a clean dataset. Real-world data doesn't work that way. What you actually get is a CSV with categories written in inconsistent casing, a date column mixing four different formats, and duplicate rows that snuck in from who-knows-where.</p>
<p>This is a case study from a project I built as an <em>Individual Assignment</em> at <strong>Rework Academy — AI Engineering Bootcamp</strong>, in the <em>Data Engineering: Pipeline &amp; Preparation</em> session. I built an ETL pipeline for a deliberately "dirtied" automobile dataset, complete with unit tests and CI. Every number in this post comes straight from running the pipeline — none of it is estimated.</p>
<p>Full source code is on GitHub: <a href="https://github.com/arielshakaramiro/assignment-data-pipeline-arielshakaramiro">assignment-data-pipeline-arielshakaramiro</a></p>
<h2>Table of Contents</h2>
<ul>
<li><p><a href="#the-case">The Case: A "Dirty" Automobile Dataset</a></p>
</li>
<li><p><a href="#stage-1">Stage 1 — Extract &amp; Inspect</a></p>
</li>
<li><p><a href="#stage-2">Stage 2 — Data Cleaning</a></p>
</li>
<li><p><a href="#stage-3">Stage 3 — Data Transformation</a></p>
</li>
<li><p><a href="#pipeline">Wiring It All Into an ETL Pipeline</a></p>
</li>
<li><p><a href="#testing">Testing &amp; CI: Making Sure the Pipeline Doesn't Quietly Break</a></p>
</li>
<li><p><a href="#result">Final Result</a></p>
</li>
<li><p><a href="#checklist">Checklist: Data Cleaning Before Modeling</a></p>
</li>
<li><p><a href="#closing">Closing Thoughts</a></p>
</li>
</ul>
<hr />
<h2>The Case: A "Dirty" Automobile Dataset</h2>
<p>The dataset is <code>automobileEDA_dirty_training.csv</code> — 205 rows of vehicle specs (make, dimensions, engine, price, fuel economy) intentionally corrupted for practice. The rule was simple but strict: <strong>the raw dataset can never be overwritten or edited in place</strong>. Every output has to be a new file, generated automatically by the script — not copied from an "answer key."</p>
<p>Initial inspection turned up 7 distinct data quality issues:</p>
<ol>
<li><p><strong>Missing values</strong> — 17 empty cells spread across 7 columns</p>
</li>
<li><p><strong>Duplicate records</strong> — 4 fully identical rows</p>
</li>
<li><p><strong>Inconsistent categories</strong> — mixed casing across several columns</p>
</li>
<li><p><strong>Excess whitespace</strong> — trailing spaces stuck to some text values</p>
</li>
<li><p><strong>Mixed date formats</strong> — 4 different formats in a single column</p>
</li>
<li><p><strong>Categorical columns that are actually numeric</strong> — numbers spelled out as words</p>
</li>
<li><p><strong>Unscaled numeric ranges</strong> — raw units, ready to bias any model toward large-magnitude features</p>
</li>
</ol>
<hr />
<h2>Stage 1 — Extract &amp; Inspect</h2>
<p>Before cleaning anything, the pipeline looks at the data first. <code>inspect_data()</code> prints the first five rows, row/column counts, dtypes per column, missing value counts, duplicate counts, and unique values for categorical columns — every single time the pipeline runs, not just once during manual exploration.</p>
<p>Here's what inspection found:</p>
<table>
<thead>
<tr>
<th>Aspect</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td>Initial size</td>
<td>205 rows × 30 columns</td>
</tr>
<tr>
<td>Total missing values</td>
<td>17 cells</td>
</tr>
<tr>
<td>Duplicate records</td>
<td>4 rows</td>
</tr>
</tbody></table>
<p><strong>Columns with missing values:</strong></p>
<table>
<thead>
<tr>
<th>Column</th>
<th>Missing</th>
</tr>
</thead>
<tbody><tr>
<td>transaction_date</td>
<td>2</td>
</tr>
<tr>
<td>make</td>
<td>2</td>
</tr>
<tr>
<td>num-of-doors</td>
<td>2</td>
</tr>
<tr>
<td>stroke</td>
<td>4</td>
</tr>
<tr>
<td>horsepower</td>
<td>3</td>
</tr>
<tr>
<td>price</td>
<td>3</td>
</tr>
<tr>
<td>horsepower-binned</td>
<td>1</td>
</tr>
</tbody></table>
<p>The most interesting finding came from checking unique values in categorical columns. The <code>make</code> column, which should only contain 22 brand names, actually had <strong>27 distinct text variants</strong> because of things like:</p>
<p>See the category inconsistencies that were found</p>
<ul>
<li><p><code>make</code>: <code>ALFA-ROMERO</code>, <code>Audi</code>, <code>BMW</code> vs <code>alfa-romero</code>, <code>audi</code>, <code>bmw</code> — plus values like <code>dodge</code> / <code>porsche</code> with trailing whitespace</p>
</li>
<li><p><code>body-style</code>: <code>SEDAN</code>, <code>Sedan</code>, <code>sedan</code> — three different spellings of the same thing</p>
</li>
<li><p><code>drive-wheels</code>: <code>AWD</code>, <code>RWD</code> vs <code>fwd</code>, <code>rwd</code></p>
</li>
<li><p><code>fuel-system</code>: <code>MPFI</code>, <code>Mpfi</code>, <code>mpfi</code></p>
</li>
</ul>
<p>If this gets one-hot encoded without cleaning first, <code>ALFA-ROMERO</code> and <code>alfa-romero</code> get counted as two different brands. The model ends up learning from noise instead of signal.</p>
<pre><code class="language-python">def inspect_data(df: pd.DataFrame) -&gt; None:
    """Print an initial inspection of the dataset to the terminal."""
    print("-- Missing values per column (only &gt; 0) --")
    miss = df.isna().sum()
    print(miss[miss &gt; 0] if (miss &gt; 0).any() else "No missing values")

    print("-- Unique values in relevant categorical columns --")
    for col in ["make", "body-style", "drive-wheels", "fuel-system",
                "num-of-doors", "aspiration", "horsepower-binned"]:
        if col in df.columns:
            uniques = sorted(df[col].dropna().astype(str).unique().tolist())
            print(f"  {col} ({len(uniques)}): {uniques}")
</code></pre>
<hr />
<h2>Stage 2 — Data Cleaning</h2>
<p>Every cleaning decision is documented with a reason — not just "fill in the missing value," but <em>why</em> that specific method was chosen for that specific column.</p>
<table>
<thead>
<tr>
<th>Issue</th>
<th>Column(s)</th>
<th>Method</th>
<th>Reason</th>
</tr>
</thead>
<tbody><tr>
<td>Inconsistent categories &amp; whitespace</td>
<td>all text columns</td>
<td><code>.str.strip().str.lower()</code></td>
<td>Merges equivalent categories so they aren't double-counted during encoding</td>
</tr>
<tr>
<td>Duplicate records</td>
<td>entire rows</td>
<td><code>drop_duplicates()</code></td>
<td>Identical rows add bias without adding information</td>
</tr>
<tr>
<td>Mixed date formats</td>
<td><code>transaction_date</code></td>
<td>Multi-format parsing → <code>YYYY-MM-DD</code></td>
<td>Standardizes the column so it can be analyzed consistently</td>
</tr>
<tr>
<td>Categorical-but-numeric</td>
<td><code>num-of-doors</code>, <code>num-of-cylinders</code></td>
<td>Word→number mapping (two→2, four→4, …)</td>
<td>The values are genuinely ordinal numbers</td>
</tr>
<tr>
<td>Missing numeric</td>
<td>stroke, horsepower, price</td>
<td>Filled with <strong>median</strong></td>
<td>Median resists outliers (car prices are heavily skewed)</td>
</tr>
<tr>
<td>Missing categorical</td>
<td>make, num-of-doors, horsepower-binned</td>
<td>Filled with <strong>mode</strong></td>
<td>Most common value, doesn't introduce a new category</td>
</tr>
<tr>
<td>Missing date</td>
<td><code>transaction_date</code></td>
<td><strong>Left empty</strong></td>
<td>Factual/historical data; imputing it would fabricate a fact</td>
</tr>
</tbody></table>
<p>The date column deserves a bit more explanation. <code>transaction_date</code> mixes four formats in the same column: <code>2025-01-01</code>, <code>02/01/2025</code>, <code>01-03-2025</code>, <code>04-Jan-2025</code>. The pipeline tries each format in sequence until one matches:</p>
<pre><code class="language-python">def _parse_mixed_dates(series: pd.Series) -&gt; pd.Series:
    """Convert a date column with mixed formats into a uniform datetime."""
    formats = ["%Y-%m-%d", "%d/%m/%Y", "%m-%d-%Y", "%d-%b-%Y"]
    parsed = pd.Series(pd.NaT, index=series.index, dtype="datetime64[ns]")
    remaining = series.copy()
    for fmt in formats:
        mask = parsed.isna() &amp; remaining.notna()
        if not mask.any():
            break
        attempt = pd.to_datetime(remaining[mask], format=fmt, errors="coerce")
        parsed.loc[attempt.notna().index[attempt.notna()]] = attempt.dropna()
    return parsed
</code></pre>
<p>🤔 Guess first: of the 7 columns with missing values, which one was <strong>deliberately left empty</strong> after cleaning?</p>
<p>The answer: <code>transaction_date</code> — and that was a deliberate call, not a bug.</p>
<p>Every other numeric column (stroke, horsepower, price) got filled with the median, and categorical columns got filled with the mode. But a transaction date is a different kind of thing — it's <strong>factual, historical data</strong>, not a statistical quantity. A car's price has a "reasonable value" you can approximate from the median. A transaction date has exactly <strong>one correct value</strong>, and there's no statistical pattern that can safely guess it without risking a fabricated fact.</p>
<p>The right data engineering practice for a missing <em>ground-truth field</em> is to <strong>go back to the data owner and confirm</strong> — not to guess through imputation. So those 2 rows were honestly left blank: <em>better empty-but-correct than filled-but-wrong</em>. This column also isn't used as a modeling feature, so it doesn't affect any downstream analysis.</p>
<p><strong>Cleaning result:</strong> 205 → <strong>201 rows</strong> (4 duplicates removed), missing values <strong>17 → 0</strong> (aside from the 2 <code>transaction_date</code> rows left blank on purpose).</p>
<hr />
<h2>Stage 3 — Data Transformation</h2>
<p>Cleaning makes the data <em>correct</em>. Transformation makes it <em>model-ready</em>. Two different things — and beginners mix them up more often than you'd expect.</p>
<table>
<thead>
<tr>
<th>Transformation</th>
<th>Column(s)</th>
<th>Method</th>
<th>Reason</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Normalization</strong></td>
<td>14 numeric columns (wheel-base, curb-weight, engine-size, horsepower, price, etc.)</td>
<td><strong>Min-Max Scaling</strong> <code>(x−min)/(max−min)</code></td>
<td>Puts every feature on a 0–1 range so none dominates by magnitude</td>
</tr>
<tr>
<td><strong>Ordinal encoding</strong></td>
<td><code>horsepower-binned</code> → <code>horsepower_ordinal</code></td>
<td>Low=0, Medium=1, High=2</td>
<td>The category has a genuine order</td>
</tr>
<tr>
<td><strong>One-Hot encoding</strong></td>
<td>body-style, drive-wheels, aspiration, engine-type, engine-location, fuel-system</td>
<td><code>pd.get_dummies()</code></td>
<td>Nominal categories with no order; avoids implying a false ranking</td>
</tr>
<tr>
<td><strong>Frequency encoding</strong></td>
<td><code>make</code> → <code>make_freq</code></td>
<td>Proportion of occurrence per brand</td>
<td>High cardinality (22 brands); one-hot would blow up into too many sparse columns</td>
</tr>
</tbody></table>
<p>That last row is the one people tend to skip: <strong>not every categorical column belongs in one-hot encoding</strong>. If <code>make</code> (22 categories) also got one-hot encoded, that's 22 new columns, most of them zeros for any given row — sparse and wasteful. Frequency encoding is the middle ground: each brand becomes a single number representing how often it shows up in the dataset — one column, still informative.</p>
<p><strong>Real before/after example</strong> (Min-Max Scaling on <code>horsepower</code>):</p>
<table>
<thead>
<tr>
<th>Before</th>
<th>After</th>
</tr>
</thead>
<tbody><tr>
<td>111.0</td>
<td>0.2944</td>
</tr>
<tr>
<td>111.0</td>
<td>0.2944</td>
</tr>
<tr>
<td>154.0</td>
<td>0.4953</td>
</tr>
</tbody></table>
<p>Worth remembering: Min-Max Scaling <strong>only changes the axis range, not the shape of the distribution</strong>. Look at the <code>price</code> histogram before and after — the spread pattern is identical, only the x-axis shifts from raw USD to a 0–1 range.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/276a8389-3909-491e-91f8-592c60699227.png" alt="Distribusi price sebelum vs sesudah scaling" style="display:block;margin:0 auto" />

<p><strong>New columns produced:</strong> <code>horsepower_ordinal</code>, <code>make_freq</code>, and every one-hot column (<code>body-style_*</code>, <code>drive-wheels_*</code>, <code>aspiration_*</code>, <code>engine-type_*</code>, <code>engine-location_*</code>, <code>fuel-system_*</code>). After all this, the column count goes from <strong>30 to 51</strong>.</p>
<hr />
<h2>Wiring It All Into an ETL Pipeline</h2>
<p>Every step above is packaged into 5 separate functions, wired together by one orchestrator:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/11885f6a-782d-4214-9efb-4782901cc17e.png" alt="Data Flow Diagram" style="display:block;margin:0 auto" />

<pre><code class="language-python">def run_pipeline() -&gt; pd.DataFrame:
    """Run the entire ETL pipeline start to finish."""
    df_raw = read_data(RAW_PATH)         # Extract
    inspect_data(df_raw)                 # Transform: inspection
    df_clean = clean_data(df_raw)        # Transform: cleaning
    df_final = transform_data(df_clean)  # Transform: transformation
    save_data(df_final, PROCESSED_PATH)  # Load
    return df_final

if __name__ == "__main__":
    run_pipeline()
</code></pre>
<p>One small but useful detail: file paths are resolved with <code>Path(__file__).resolve().parent.parent</code>, not a path relative to the current working directory. That means the pipeline runs correctly whether it's invoked from the project root or from inside <code>src/</code> — it doesn't depend on where the script happens to be launched from.</p>
<hr />
<h2>Testing &amp; CI: Making Sure the Pipeline Doesn't Quietly Break</h2>
<p>Getting a pipeline to run correctly once is the easy part. The harder part is making sure it's <strong>still correct</strong> six months later, after it's been edited, or after the source data shifts slightly. That's what tests are for.</p>
<p>There are 15 tests in <code>tests/test_pipeline.py</code>, organized around the ETL stages themselves:</p>
<pre><code class="language-python">def test_no_duplicates_after_clean(raw_df):
    cleaned = pipeline.clean_data(raw_df)
    assert cleaned.duplicated().sum() == 0

def test_scaled_columns_within_0_1(processed_df):
    """Every Min-Max scaled column must fall within [0, 1]."""
    for col in pipeline.NUMERIC_TO_SCALE:
        assert processed_df[col].min() &gt;= -1e-9
        assert processed_df[col].max() &lt;= 1 + 1e-9

def test_raw_dataset_untouched(raw_df):
    """Running cleaning must not mutate the original raw object."""
    before = raw_df.shape
    pipeline.clean_data(raw_df)
    assert raw_df.shape == before
</code></pre>
<p>That last test is my favorite: it turns the assignment's own rule ("never modify the raw dataset") into code that can actually fail, instead of just a promise in a README that's easy to break without noticing.</p>
<p>All of this also runs automatically via GitHub Actions on every push or pull request to <code>main</code>:</p>
<pre><code class="language-yaml">- name: Run ETL pipeline
  run: python src/pipeline.py

- name: Verify processed dataset was generated
  run: test -f data/processed/automobileEDA_processed.csv

- name: Run tests
  run: pytest -v
</code></pre>
<p>Here's the actual terminal output (not a mockup) from running the pipeline:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/054257f1-92da-44f3-8065-abd5f272e5d3.png" alt="Terminal output pipeline" style="display:block;margin:0 auto" />

<hr />
<h2>Final Result</h2>
<table>
<thead>
<tr>
<th>Stage</th>
<th>Rows</th>
<th>Columns</th>
</tr>
</thead>
<tbody><tr>
<td>Raw dataset</td>
<td>205</td>
<td>30</td>
</tr>
<tr>
<td>After cleaning</td>
<td>201</td>
<td>30</td>
</tr>
<tr>
<td>Processed dataset (after transform)</td>
<td><strong>201</strong></td>
<td><strong>51</strong></td>
</tr>
</tbody></table>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/ec82a75a-cf55-450a-9a74-33cf386ab0cf.png" alt="Missing Values per Kolom" style="display:block;margin:0 auto" />

<hr />
<h2>Checklist: Data Cleaning Before Modeling</h2>
<p>A short checklist I use myself on any new tabular dataset — feel free to copy it:</p>
<ul>
<li><p>[ ] Check <code>shape</code>, <code>dtypes</code>, and the first 5 rows before doing anything else</p>
</li>
<li><p>[ ] Count missing values <strong>per column</strong>, not just the total</p>
</li>
<li><p>[ ] Check <code>duplicated().sum()</code> before and after cleaning</p>
</li>
<li><p>[ ] For every categorical column: run <code>.unique()</code> and look for casing or whitespace variants</p>
</li>
<li><p>[ ] For date columns: check whether the format is consistent across <em>all</em> rows, not just the first few</p>
</li>
<li><p>[ ] Decide a missing-value strategy <strong>per column</strong>, not one strategy for everything (numeric → median/mean, categorical → mode, <em>ground-truth fields</em> → don't impute unless you're sure)</p>
</li>
<li><p>[ ] Only scale/normalize <strong>after</strong> cleaning, never before</p>
</li>
<li><p>[ ] Choose an encoding method based on cardinality, not one-hot for every categorical column by default</p>
</li>
<li><p>[ ] Write at least a handful of <code>assert</code>/tests that validate the pipeline's output, not just print statements</p>
</li>
</ul>
<hr />
<h2>Closing Thoughts</h2>
<p>The hardest part of this project wasn't the Pandas syntax — <code>drop_duplicates()</code> and <code>pd.get_dummies()</code> are already muscle memory at this point. The hard part was making defensible decisions: why median instead of mean, why one column gets left empty while others get imputed, why one categorical column gets one-hot encoded while another gets frequency encoding instead.</p>
<p>That's what I tried to show here: a good ETL pipeline isn't just code that runs without errors — it's a chain of decisions you can actually defend, one line at a time.</p>
<p>Full source, including the test suite and CI config, is on GitHub: <a href="https://github.com/arielshakaramiro/assignment-data-pipeline-arielshakaramiro"><strong>assignment-data-pipeline-arielshakaramiro</strong></a></p>
<p>If you'd have handled any part of this differently — especially the imputation calls or the encoding choices — the comments are open.</p>
<hr />
<p><em>This project was built as part of the</em> <em><strong>Rework Academy — AI Engineering Bootcamp</strong></em>*, Data Engineering: Pipeline &amp; Preparation session. The automobile dataset (<code>automobileEDA</code>) was provided as practice material for that session.*</p>
]]></content:encoded></item><item><title><![CDATA[Dari Data Kotor ke Siap Pakai: Membangun ETL Pipeline dengan Python, Pandas & pytest]]></title><description><![CDATA[Setiap kelas data science biasanya mulai dari dataset yang sudah bersih. Masalahnya, di dunia nyata tidak ada yang seperti itu. Yang ada adalah CSV dengan kategori yang ditulis dengan huruf besar-keci]]></description><link>https://shaka-ai.hashnode.dev/etl-pipeline-data-kotor-ke-siap-pakai</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/etl-pipeline-data-kotor-ke-siap-pakai</guid><category><![CDATA[Python]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Sun, 30 Aug 2026 14:29:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/cb5ceff2-3481-42c8-9367-b38e9d149217.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Setiap kelas data science biasanya mulai dari dataset yang sudah bersih. Masalahnya, di dunia nyata tidak ada yang seperti itu. Yang ada adalah CSV dengan kategori yang ditulis dengan huruf besar-kecil berbeda, kolom tanggal yang formatnya campur aduk, dan baris duplikat yang menyelinap entah dari mana.</p>
<p>Tulisan ini adalah studi kasus dari proyek yang saya kerjakan sebagai <em>Individual Assignment</em> di <strong>Rework Academy — AI Engineering Bootcamp</strong>, sesi <em>Data Engineering: Pipeline &amp; Preparation</em>. Saya membangun ETL pipeline untuk dataset otomotif yang sengaja "dikotori", lengkap dengan unit test dan CI. Semua angka di tulisan ini diambil langsung dari hasil eksekusi pipeline-nya — bukan estimasi.</p>
<p>Source code lengkap ada di GitHub: <a href="https://github.com/arielshakaramiro/assignment-data-pipeline-arielshakaramiro">assignment-data-pipeline-arielshakaramiro</a></p>
<h2>Daftar Isi</h2>
<ul>
<li><p><a href="#studi-kasus">Studi Kasus: Dataset Otomotif yang "Kotor"</a></p>
</li>
<li><p><a href="#tahap-1">Tahap 1 — Extract &amp; Inspect</a></p>
</li>
<li><p><a href="#tahap-2">Tahap 2 — Data Cleaning</a></p>
</li>
<li><p><a href="#tahap-3">Tahap 3 — Data Transformation</a></p>
</li>
<li><p><a href="#pipeline">Menyusun Semuanya Jadi Pipeline ETL</a></p>
</li>
<li><p><a href="#testing">Testing &amp; CI: Memastikan Pipeline Tidak Diam-Diam Rusak</a></p>
</li>
<li><p><a href="#hasil">Hasil Akhir</a></p>
</li>
<li><p><a href="#checklist">Checklist: Data Cleaning Sebelum Modeling</a></p>
</li>
<li><p><a href="#penutup">Penutup</a></p>
</li>
</ul>
<hr />
<h2>Studi Kasus: Dataset Otomotif yang "Kotor"</h2>
<p>Dataset yang dipakai adalah <code>automobileEDA_dirty_training.csv</code> — 205 baris spesifikasi kendaraan (merek, dimensi, mesin, harga, konsumsi bahan bakar) yang sengaja dibuat kotor untuk latihan. Aturan mainnya sederhana tapi tegas: <strong>dataset mentah tidak boleh ditimpa atau diubah langsung</strong>. Semua hasil harus keluar sebagai file baru, dihasilkan otomatis oleh script — bukan disalin dari file "jawaban".</p>
<p>Setelah pemeriksaan awal, ada 7 masalah data yang harus ditangani:</p>
<ol>
<li><p><strong>Missing values</strong> — 17 sel kosong tersebar di 7 kolom</p>
</li>
<li><p><strong>Duplicate records</strong> — 4 baris identik penuh</p>
</li>
<li><p><strong>Kategori tidak konsisten</strong> — huruf besar/kecil campur di beberapa kolom</p>
</li>
<li><p><strong>Whitespace berlebih</strong> — spasi menempel di beberapa nilai teks</p>
</li>
<li><p><strong>Format tanggal campuran</strong> — 4 format berbeda dalam satu kolom</p>
</li>
<li><p><strong>Kolom kategorikal yang sebenarnya numerik</strong> — angka ditulis sebagai kata</p>
</li>
<li><p><strong>Skala numerik belum seragam</strong> — satuan asli, siap bikin model bias ke fitur dengan angka besar</p>
</li>
</ol>
<hr />
<h2>Tahap 1 — Extract &amp; Inspect</h2>
<p>Sebelum membersihkan apa pun, pipeline dulu <em>melihat</em> datanya. Fungsi <code>inspect_data()</code> menampilkan lima baris pertama, jumlah baris/kolom, tipe data per kolom, jumlah missing values, jumlah duplikat, dan nilai unik pada kolom kategorikal — semuanya dicetak ke terminal setiap kali pipeline dijalankan, bukan cuma sekali waktu eksplorasi manual.</p>
<p>Ini hasil pemeriksaannya:</p>
<table>
<thead>
<tr>
<th>Aspek</th>
<th>Nilai</th>
</tr>
</thead>
<tbody><tr>
<td>Ukuran awal</td>
<td>205 baris × 30 kolom</td>
</tr>
<tr>
<td>Total missing values</td>
<td>17 sel</td>
</tr>
<tr>
<td>Duplicate records</td>
<td>4 baris</td>
</tr>
</tbody></table>
<p><strong>Kolom dengan missing values:</strong></p>
<table>
<thead>
<tr>
<th>Kolom</th>
<th>Missing</th>
</tr>
</thead>
<tbody><tr>
<td>transaction_date</td>
<td>2</td>
</tr>
<tr>
<td>make</td>
<td>2</td>
</tr>
<tr>
<td>num-of-doors</td>
<td>2</td>
</tr>
<tr>
<td>stroke</td>
<td>4</td>
</tr>
<tr>
<td>horsepower</td>
<td>3</td>
</tr>
<tr>
<td>price</td>
<td>3</td>
</tr>
<tr>
<td>horsepower-binned</td>
<td>1</td>
</tr>
</tbody></table>
<p>Yang paling menarik justru dari pengecekan nilai unik kolom kategorikal. Kolom <code>make</code> yang seharusnya cuma berisi 22 nama merek, ternyata punya <strong>27 varian teks</strong> karena hal-hal seperti ini:</p>
<p>Lihat contoh inkonsistensi kategori yang ditemukan</p>
<ul>
<li><p><code>make</code>: <code>ALFA-ROMERO</code>, <code>Audi</code>, <code>BMW</code> vs <code>alfa-romero</code>, <code>audi</code>, <code>bmw</code> — dan beberapa nilai seperti <code>dodge</code> / <code>porsche</code> yang punya spasi menempel di akhir</p>
</li>
<li><p><code>body-style</code>: <code>SEDAN</code>, <code>Sedan</code>, <code>sedan</code> — tiga cara berbeda untuk hal yang sama</p>
</li>
<li><p><code>drive-wheels</code>: <code>AWD</code>, <code>RWD</code> vs <code>fwd</code>, <code>rwd</code></p>
</li>
<li><p><code>fuel-system</code>: <code>MPFI</code>, <code>Mpfi</code>, <code>mpfi</code></p>
</li>
</ul>
<p>Kalau ini langsung di-<em>one-hot encode</em> tanpa dibersihkan dulu, <code>ALFA-ROMERO</code> dan <code>alfa-romero</code> akan dihitung sebagai dua merek berbeda. Model jadi belajar dari noise, bukan dari sinyal.</p>
<pre><code class="language-python">def inspect_data(df: pd.DataFrame) -&gt; None:
    """Menampilkan pemeriksaan awal dataset ke terminal."""
    print("-- Missing values per kolom (hanya &gt; 0) --")
    miss = df.isna().sum()
    print(miss[miss &gt; 0] if (miss &gt; 0).any() else "Tidak ada missing values")

    print("-- Nilai unik kolom kategorikal relevan --")
    for col in ["make", "body-style", "drive-wheels", "fuel-system",
                "num-of-doors", "aspiration", "horsepower-binned"]:
        if col in df.columns:
            uniques = sorted(df[col].dropna().astype(str).unique().tolist())
            print(f"  {col} ({len(uniques)}): {uniques}")
</code></pre>
<hr />
<h2>Tahap 2 — Data Cleaning</h2>
<p>Setiap keputusan cleaning didokumentasikan dengan alasannya — bukan sekadar "hapus missing value", tapi <em>kenapa</em> metode itu yang dipilih untuk kolom itu.</p>
<table>
<thead>
<tr>
<th>Permasalahan</th>
<th>Kolom</th>
<th>Metode</th>
<th>Alasan</th>
</tr>
</thead>
<tbody><tr>
<td>Kategori tidak konsisten &amp; whitespace</td>
<td>semua kolom teks</td>
<td><code>.str.strip().str.lower()</code></td>
<td>Menyatukan kategori yang sama agar tidak dihitung ganda saat encoding</td>
</tr>
<tr>
<td>Duplicate records</td>
<td>seluruh baris</td>
<td><code>drop_duplicates()</code></td>
<td>Baris identik menambah bias &amp; tidak memberi informasi baru</td>
</tr>
<tr>
<td>Format tanggal campuran</td>
<td><code>transaction_date</code></td>
<td>Parsing multi-format → <code>YYYY-MM-DD</code></td>
<td>Menyeragamkan agar dapat dianalisis konsisten</td>
</tr>
<tr>
<td>Kategorikal-numerik</td>
<td><code>num-of-doors</code>, <code>num-of-cylinders</code></td>
<td>Peta kata→angka (two→2, four→4, …)</td>
<td>Nilai memang bersifat numerik ordinal</td>
</tr>
<tr>
<td>Missing numerik</td>
<td>stroke, horsepower, price</td>
<td>Isi dengan <strong>median</strong></td>
<td>Median tahan terhadap outlier (harga mobil sangat skewed)</td>
</tr>
<tr>
<td>Missing kategorikal</td>
<td>make, num-of-doors, horsepower-binned</td>
<td>Isi dengan <strong>modus</strong></td>
<td>Nilai paling umum, tidak menambah kategori baru</td>
</tr>
<tr>
<td>Missing tanggal</td>
<td><code>transaction_date</code></td>
<td><strong>Dibiarkan kosong</strong></td>
<td>Data faktual/historis; tidak diimputasi agar tidak memalsukan fakta</td>
</tr>
</tbody></table>
<p>Bagian kolom tanggal ini butuh sedikit penjelasan. Kolom <code>transaction_date</code> punya 4 format berbeda tercampur dalam satu kolom: <code>2025-01-01</code>, <code>02/01/2025</code>, <code>01-03-2025</code>, <code>04-Jan-2025</code>. Pipeline mencoba setiap format satu per satu sampai salah satu cocok:</p>
<pre><code class="language-python">def _parse_mixed_dates(series: pd.Series) -&gt; pd.Series:
    """Mengubah kolom tanggal dengan format campuran menjadi datetime seragam."""
    formats = ["%Y-%m-%d", "%d/%m/%Y", "%m-%d-%Y", "%d-%b-%Y"]
    parsed = pd.Series(pd.NaT, index=series.index, dtype="datetime64[ns]")
    remaining = series.copy()
    for fmt in formats:
        mask = parsed.isna() &amp; remaining.notna()
        if not mask.any():
            break
        attempt = pd.to_datetime(remaining[mask], format=fmt, errors="coerce")
        parsed.loc[attempt.notna().index[attempt.notna()]] = attempt.dropna()
    return parsed
</code></pre>
<p>🤔 Coba tebak dulu: dari 7 kolom yang punya missing values, kolom mana yang <strong>sengaja dibiarkan tetap kosong</strong> setelah cleaning?</p>
<p>Jawabannya: <code>transaction_date</code>, dan itu keputusan sadar, bukan bug.</p>
<p>Semua kolom numerik lain (stroke, horsepower, price) diisi pakai median, dan kolom kategorikal diisi pakai modus. Tapi tanggal transaksi itu beda karakternya — dia <strong>data faktual/historis</strong>, bukan besaran statistik. Harga mobil punya "nilai yang masuk akal" yang bisa didekati dari median. Tanggal transaksi cuma punya <strong>satu nilai yang benar</strong>, dan itu tidak bisa ditebak dari pola statistik tanpa berisiko memalsukan fakta kejadian.</p>
<p>Praktik data engineering yang tepat untuk <em>ground-truth field</em> yang hilang adalah <strong>mengonfirmasi ulang ke data owner</strong> — bukan menebak lewat imputasi. Jadi 2 baris itu dibiarkan kosong secara jujur: <em>lebih baik kosong-tapi-benar daripada terisi-tapi-salah</em>. Kolom ini juga tidak dipakai sebagai fitur pemodelan, jadi tidak memengaruhi hasil analisis nantinya.</p>
<p><strong>Hasil cleaning:</strong> 205 → <strong>201 baris</strong> (4 duplikat dihapus), missing values <strong>17 → 0</strong> (di luar 2 baris <code>transaction_date</code> yang sengaja dibiarkan).</p>
<hr />
<h2>Tahap 3 — Data Transformation</h2>
<p>Cleaning membuat data <em>benar</em>. Transformation membuat data <em>siap dipakai model</em>. Dua hal yang berbeda, dan sering tertukar oleh pemula.</p>
<table>
<thead>
<tr>
<th>Transformasi</th>
<th>Kolom</th>
<th>Metode</th>
<th>Alasan</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Normalisasi</strong></td>
<td>14 kolom numerik (wheel-base, curb-weight, engine-size, horsepower, price, dll)</td>
<td><strong>Min-Max Scaling</strong> <code>(x−min)/(max−min)</code></td>
<td>Menyeragamkan skala ke rentang 0–1 agar tidak ada fitur yang mendominasi</td>
</tr>
<tr>
<td><strong>Ordinal encoding</strong></td>
<td><code>horsepower-binned</code> → <code>horsepower_ordinal</code></td>
<td>Low=0, Medium=1, High=2</td>
<td>Kategori memiliki urutan (ordinal)</td>
</tr>
<tr>
<td><strong>One-Hot encoding</strong></td>
<td>body-style, drive-wheels, aspiration, engine-type, engine-location, fuel-system</td>
<td><code>pd.get_dummies()</code></td>
<td>Kategori nominal tanpa urutan; menghindari asumsi urutan palsu</td>
</tr>
<tr>
<td><strong>Frequency encoding</strong></td>
<td><code>make</code> → <code>make_freq</code></td>
<td>Proporsi kemunculan tiap merek</td>
<td>Kardinalitas tinggi (22 merek); one-hot akan membuat terlalu banyak kolom</td>
</tr>
</tbody></table>
<p>Poin terakhir ini yang sering dilewatkan orang: <strong>tidak semua kolom kategorikal cocok di-one-hot</strong>. Kalau <code>make</code> (22 kategori) di-one-hot juga, itu nambah 22 kolom baru yang sebagian besar isinya nol — sparse dan boros. Frequency encoding jadi jalan tengah: setiap merek direpresentasikan sebagai proporsi kemunculannya di dataset, tetap 1 kolom, tetap informatif.</p>
<p><strong>Contoh nyata sebelum vs sesudah Min-Max Scaling</strong> (<code>horsepower</code>):</p>
<table>
<thead>
<tr>
<th>Sebelum</th>
<th>Sesudah</th>
</tr>
</thead>
<tbody><tr>
<td>111.0</td>
<td>0.2944</td>
</tr>
<tr>
<td>111.0</td>
<td>0.2944</td>
</tr>
<tr>
<td>154.0</td>
<td>0.4953</td>
</tr>
</tbody></table>
<p>Yang penting dipahami: Min-Max Scaling <strong>hanya mengubah rentang sumbu, bukan bentuk distribusi</strong>. Perhatikan histogram <code>price</code> sebelum dan sesudah — pola sebarannya identik, cuma sumbu-x yang berubah dari satuan USD ke rentang 0–1.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/3b794712-323a-43ad-8f9d-4464b0bdf1d6.png" alt="Distribusi price sebelum vs sesudah scaling" style="display:block;margin:0 auto" />

<p><strong>Kolom baru yang dihasilkan:</strong> <code>horsepower_ordinal</code>, <code>make_freq</code>, dan seluruh kolom one-hot (<code>body-style_*</code>, <code>drive-wheels_*</code>, <code>aspiration_*</code>, <code>engine-type_*</code>, <code>engine-location_*</code>, <code>fuel-system_*</code>). Setelah semua transformasi ini, jumlah kolom naik dari <strong>30 menjadi 51</strong>.</p>
<hr />
<h2>Menyusun Semuanya Jadi Pipeline ETL</h2>
<p>Semua langkah di atas dikemas jadi 5 function terpisah, dirangkai oleh satu orchestrator:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/35efa086-9b7a-4af5-81e6-db171acc96f0.png" alt="Data Flow Diagram" style="display:block;margin:0 auto" />

<pre><code class="language-python">def run_pipeline() -&gt; pd.DataFrame:
    """Menjalankan seluruh pipeline ETL dari awal sampai akhir."""
    df_raw = read_data(RAW_PATH)         # Extract
    inspect_data(df_raw)                 # Transform: inspeksi
    df_clean = clean_data(df_raw)        # Transform: cleaning
    df_final = transform_data(df_clean)  # Transform: transformasi
    save_data(df_final, PROCESSED_PATH)  # Load
    return df_final

if __name__ == "__main__":
    run_pipeline()
</code></pre>
<p>Satu detail kecil yang berguna: path file dihitung pakai <code>Path(__file__).resolve().parent.parent</code>, bukan path relatif ke <em>current working directory</em>. Artinya pipeline tetap jalan benar baik dipanggil dari root folder maupun dari dalam folder <code>src/</code> — tidak bergantung dari mana script dieksekusi.</p>
<hr />
<h2>Testing &amp; CI: Memastikan Pipeline Tidak Diam-Diam Rusak</h2>
<p>Pipeline yang jalan sekali dengan benar itu gampang. Yang lebih sulit: memastikan dia <strong>tetap benar</strong> setelah diubah enam bulan kemudian, atau ketika dataset sumbernya berubah sedikit. Di sinilah test masuk.</p>
<p>Ada 15 test di <code>tests/test_pipeline.py</code>, dibagi mengikuti tahap ETL-nya sendiri:</p>
<pre><code class="language-python">def test_no_duplicates_after_clean(raw_df):
    cleaned = pipeline.clean_data(raw_df)
    assert cleaned.duplicated().sum() == 0

def test_scaled_columns_within_0_1(processed_df):
    """Semua kolom hasil Min-Max Scaling harus berada di rentang [0, 1]."""
    for col in pipeline.NUMERIC_TO_SCALE:
        assert processed_df[col].min() &gt;= -1e-9
        assert processed_df[col].max() &lt;= 1 + 1e-9

def test_raw_dataset_untouched(raw_df):
    """Menjalankan cleaning tidak boleh mengubah objek raw asli."""
    before = raw_df.shape
    pipeline.clean_data(raw_df)
    assert raw_df.shape == before
</code></pre>
<p>Test terakhir itu yang saya suka: dia memvalidasi <em>ketentuan</em> dari assignment-nya sendiri ("dataset mentah tidak boleh diubah") sebagai kode yang bisa gagal, bukan cuma janji di README yang gampang dilanggar tanpa sadar.</p>
<p>Semua test ini juga jalan otomatis lewat GitHub Actions setiap kali ada push atau pull request ke <code>main</code>:</p>
<pre><code class="language-yaml">- name: Run ETL pipeline
  run: python src/pipeline.py

- name: Verify processed dataset was generated
  run: test -f data/processed/automobileEDA_processed.csv

- name: Run tests
  run: pytest -v
</code></pre>
<p>Hasil eksekusi nyata (bukan simulasi) waktu pipeline ini dijalankan:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/8827d0b8-473d-4256-997b-6013cbd8d50f.png" alt="Terminal output pipeline" style="display:block;margin:0 auto" />

<hr />
<h2>Hasil Akhir</h2>
<table>
<thead>
<tr>
<th>Tahap</th>
<th>Baris</th>
<th>Kolom</th>
</tr>
</thead>
<tbody><tr>
<td>Raw dataset</td>
<td>205</td>
<td>30</td>
</tr>
<tr>
<td>Setelah cleaning</td>
<td>201</td>
<td>30</td>
</tr>
<tr>
<td>Processed dataset (setelah transform)</td>
<td><strong>201</strong></td>
<td><strong>51</strong></td>
</tr>
</tbody></table>
<img src="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/4d75bede-c0f1-4dfe-8b49-c99e3a1eb26a.png" alt="Missing Values per Kolom" style="display:block;margin:0 auto" />

<hr />
<h2>Checklist: Data Cleaning Sebelum Modeling</h2>
<p>Checklist ringkas yang saya pakai sendiri setiap kali dapat dataset tabular baru — silakan disalin:</p>
<ul>
<li><p>[ ] Cek <code>shape</code>, <code>dtypes</code>, dan 5 baris pertama sebelum melakukan apa pun</p>
</li>
<li><p>[ ] Hitung missing values <strong>per kolom</strong>, bukan cuma total</p>
</li>
<li><p>[ ] Cek <code>duplicated().sum()</code> sebelum dan sesudah cleaning</p>
</li>
<li><p>[ ] Untuk tiap kolom kategorikal: <code>.unique()</code> dan lihat apakah ada variasi huruf besar/kecil atau whitespace</p>
</li>
<li><p>[ ] Untuk kolom tanggal: cek apakah formatnya konsisten di seluruh baris, bukan cuma di beberapa baris pertama</p>
</li>
<li><p>[ ] Tentukan strategi missing value <strong>per kolom</strong>, bukan satu strategi untuk semua (numerik → median/mean, kategorikal → modus, <em>ground-truth field</em> → jangan diimputasi kalau tidak yakin)</p>
</li>
<li><p>[ ] Baru lakukan scaling/normalisasi <strong>setelah</strong> cleaning, bukan sebelum</p>
</li>
<li><p>[ ] Pilih metode encoding berdasarkan kardinalitas kolom, bukan pakai one-hot untuk semua kolom kategorikal</p>
</li>
<li><p>[ ] Tulis minimal beberapa <code>assert</code>/test yang memvalidasi output pipeline, bukan cuma print statement</p>
</li>
</ul>
<hr />
<h2>Penutup</h2>
<p>Bagian tersulit dari proyek ini bukan di sintaks Pandas-nya — <code>drop_duplicates()</code> dan <code>pd.get_dummies()</code> sudah jadi hal yang dihafal luar kepala. Bagian tersulit adalah membuat keputusan yang bisa dipertanggungjawabkan: kenapa median dan bukan mean, kenapa satu kolom dibiarkan kosong sementara yang lain diimputasi, kenapa satu kolom kategorikal di-one-hot sementara yang lain pakai frequency encoding.</p>
<p>Itu yang saya coba tunjukkan lewat tulisan ini: ETL pipeline yang baik bukan cuma soal kode yang jalan tanpa error, tapi soal reasoning yang bisa dipertanggungjawabkan di setiap baris keputusan.</p>
<p>Source code lengkap, termasuk test suite dan CI config-nya, ada di GitHub: <a href="https://github.com/arielshakaramiro/assignment-data-pipeline-arielshakaramiro"><strong>assignment-data-pipeline-arielshakaramiro</strong></a></p>
<p>Kalau ada bagian yang menurutmu bisa ditangani dengan cara lain — terutama soal keputusan imputasi atau pemilihan metode encoding — komentar di bawah selalu terbuka untuk didiskusikan.</p>
<hr />
<p><em>Proyek ini dikerjakan sebagai bagian dari</em> <em><strong>Rework Academy — AI Engineering Bootcamp</strong></em><em>, sesi Data Engineering: Pipeline &amp; Preparation. Dataset otomotif (</em><code>automobileEDA</code><em>) disediakan sebagai materi latihan pada sesi tersebut.</em></p>
]]></content:encoded></item><item><title><![CDATA[Data Handling & Preprocessing (Part 2): Data Transformation, Versioning, and Best Practices]]></title><description><![CDATA[Part 2 of 2: continuing from Part 1, which covered Data Cleaning. Here we move into Data Transformation, how to save a clean dataset, Data Versioning the MLOps way, Best Practices, and the big-picture]]></description><link>https://shaka-ai.hashnode.dev/data-handling-preprocessing-part-2-data-transformation-versioning-and-best-practices</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/data-handling-preprocessing-part-2-data-transformation-versioning-and-best-practices</guid><category><![CDATA[Data Science]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Python]]></category><category><![CDATA[mlops]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[data analysis]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Sun, 30 Aug 2026 10:36:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/2cfd0a11-d38a-4807-9bd3-c4ab27f59ac8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><em>Part 2 of 2: continuing from</em> <a href="#"><em>Part 1</em></a><em>, which covered Data Cleaning. Here we move into Data Transformation, how to save a clean dataset, Data Versioning the MLOps way, Best Practices, and the big-picture Data Pipeline Flow.</em></p>
</blockquote>
<p>Quick recap: in Part 1 we covered why preprocessing matters (the 80/20 formula), how data is collected &amp; stored, the basics of Pandas, and how to clean data — from missing values and duplicates to outliers and adversarial data. Now it's time to turn that clean data into a format that Machine Learning algorithms can actually "digest."</p>
<h2>Table of Contents</h2>
<ul>
<li><p><a href="#1-data-transformation">1. Data Transformation</a></p>
</li>
<li><p><a href="#2-saving-a-clean-dataset">2. Saving a Clean Dataset</a></p>
</li>
<li><p><a href="#3-data-versioning--management-strategies">3. Data Versioning &amp; Management Strategies</a></p>
</li>
<li><p><a href="#4-best-practices-common-mistakes--error-handling">4. Best Practices, Common Mistakes &amp; Error Handling</a></p>
</li>
<li><p><a href="#5-before-vs-after--data-pipeline-flow">5. Before vs After &amp; Data Pipeline Flow</a></p>
</li>
<li><p><a href="#6-quiz-check--part-2">6. Quiz Check — Part 2</a></p>
</li>
<li><p><a href="#7-preprocessing-checklist-cheat-sheet">7. Preprocessing Checklist Cheat Sheet</a></p>
</li>
</ul>
<hr />
<h2>1. Data Transformation</h2>
<p>Most ML algorithms <strong>only accept numeric data</strong>, so non-numeric data needs to be turned into a numeric representation (<em>vectorization / encoding</em>). This is the core of data transformation.</p>
<h3>1.1 Sorting &amp; Filtering</h3>
<ul>
<li><p><strong>Sorting</strong> — the <strong>"Order before logic"</strong> principle: sorted data is easier for the model to analyze and process.</p>
</li>
<li><p><strong>Filtering</strong> — lets you focus on a specific subset of data, for example only transactions from Asia, or only products priced above Rp 100,000.</p>
</li>
</ul>
<pre><code class="language-python"># Sorting
df_sorted = df.sort_values('price')                    # ascending
df_sorted = df.sort_values('price', ascending=False)    # descending

# Filtering
df_asia = df[df['region'] == 'Asia']
df_filtered = df[(df['region'] == 'Asia') &amp; (df['price'] &gt; 50000)]              # AND
df_filtered = df[(df['region'] == 'Asia') | (df['region'] == 'Europe')]         # OR
df_filtered = df[df['region'].isin(['Asia', 'Europe', 'Africa'])]               # isin()
</code></pre>
<h3>1.2 Encoding Categorical Data</h3>
<blockquote>
<p>🤔 <strong>Guess First:</strong> Clothing size (S, M, L) and hair color are both categories. Do you think both should be encoded the same way?</p>
<p>See the Answer</p>
<p><strong>No.</strong> Clothing size has a ranking order (S &lt; M &lt; L), so it's <strong>ordinal</strong> — it can be converted directly into numbers. Hair color has no order, so it's <strong>nominal</strong> — it's better to use One-Hot Encoding.</p>
</blockquote>
<ul>
<li><p><strong>Nominal</strong>: categories without order/ranking. Examples: hair color, ethnicity, vehicle type, gender → best encoded with <strong>One-Hot Encoding</strong> so the model doesn't assume any order exists.</p>
</li>
<li><p><strong>Ordinal</strong>: categories with a meaningful order/ranking. Examples: clothing size (S, M, L), education level, satisfaction level → can be converted directly into numeric values.</p>
</li>
</ul>
<h3>1.3 Dimensionality Reduction</h3>
<p><strong>Goal:</strong> reduce the number of features without losing important information, to address the <em>Curse of Dimensionality</em> (too many features → slow training, complexity, decreased performance).</p>
<blockquote>
<p><em>Number of Dimensionality</em> = the number of features/variables in a dataset.</p>
</blockquote>
<p><strong>i. Data Transformation Techniques</strong></p>
<ul>
<li><p><strong>PCA (Principal Component Analysis)</strong> — finds the directions with the largest data variation (<em>principal components</em>); widely used on numeric data to compress correlated features.</p>
</li>
<li><p><strong>t-SNE</strong> — preserves proximity between data points (<em>local structure</em>); good for visualizing clusters in high-dimensional data.</p>
</li>
<li><p><strong>UMAP</strong> — similar goal to t-SNE but faster &amp; more scalable for large datasets; can also be used for feature engineering.</p>
</li>
</ul>
<p><strong>ii. Removing Less Relevant Features</strong></p>
<ul>
<li><p><code>.corr()</code> <strong>(Pandas)</strong> — measures the linear correlation between features, or between a feature and the target.</p>
</li>
<li><p><strong>Predictive Power Score (PPS / ppscore)</strong> — measures a feature's ability to predict the target; capable of detecting non-linear relationships (unlike ordinary correlation).</p>
</li>
</ul>
<p><strong>iii. Feature Engineering (Creating New Features)</strong></p>
<ul>
<li><p>Combining two or more features into one new feature.</p>
</li>
<li><p>Creating ratios, differences, or specific transformations from existing features.</p>
</li>
<li><p>Extracting new information from time data (date, hour, day, month, etc.).</p>
</li>
</ul>
<p><strong>iv. Kernelization (Expanding Dimensions)</strong></p>
<ul>
<li><p><strong>Concept</strong>: mapping data into a higher-dimensional space so that data which is originally not linearly separable becomes separable. This can't be done with linear methods — it requires a <em>kernel function</em>.</p>
</li>
<li><p><strong>Kernel functions</strong>: Linear Kernel, Polynomial Kernel, Radial Basis Function (RBF) Kernel, Sigmoid Kernel.</p>
</li>
<li><p><strong>Goal</strong>: make data <em>linearly separable</em> so classification algorithms (Logistic Regression, SVM) can build a better <em>decision boundary</em>.</p>
</li>
</ul>
<p><strong>Kernelization vs TF-IDF — often mixed up, here's the difference</strong></p>
<p>Kernelization is used in ML (e.g., SVM) to map data into higher dimensions, while TF-IDF is used to represent <strong>text</strong> as a weighted vector. Two different techniques with different purposes, even though both "change the representation of data."</p>
<h3>1.4 Normalization / Scaling</h3>
<p><strong>Definition:</strong> the process of rescaling data to a specific <em>range</em> (usually 0–1 or -1 to 1) so that every feature contributes to the model in a balanced way.</p>
<p><strong>Why it's needed:</strong></p>
<ul>
<li><p>AI models are sensitive to data scale.</p>
</li>
<li><p>Features with large values can dominate.</p>
</li>
<li><p>Speeds up <em>convergence</em> during training.</p>
</li>
<li><p>Improves model accuracy.</p>
</li>
</ul>
<table>
<thead>
<tr>
<th>Type</th>
<th>Formula</th>
<th>When to Use</th>
</tr>
</thead>
<tbody><tr>
<td>Min-Max Scaling</td>
<td><code>(x - min) / (max - min)</code> → scales to 0–1</td>
<td>Data has no extreme outliers</td>
</tr>
<tr>
<td>Standardization (Z-Score)</td>
<td><code>(x - mean) / std</code> → mean = 0, std = 1</td>
<td>Data has outliers or a normal distribution</td>
</tr>
<tr>
<td>Robust Scaling</td>
<td>Uses median and IQR, resistant to outliers</td>
<td>Data with a lot of outliers</td>
</tr>
</tbody></table>
<pre><code class="language-python">from sklearn.preprocessing import MinMaxScaler, StandardScaler

scaler = MinMaxScaler()
df['price_scaled'] = scaler.fit_transform(df[['price']])

scaler = StandardScaler()
df['price_std'] = scaler.fit_transform(df[['price']])
</code></pre>
<ul>
<li><p><strong>When normalization is needed</strong>: especially for algorithms sensitive to scale, such as <em>K-Nearest Neighbors</em> (KNN), <em>Neural Networks</em>, and distance-based algorithms.</p>
</li>
<li><p><strong>Benefits for AI</strong>: training speed (the model <em>converges</em> faster), model performance (Neural Networks/KNN/SVM are very scale-sensitive), and fair interpretation (all features contribute in a balanced way without one dominating).</p>
</li>
</ul>
<blockquote>
<p>⚠️ <strong>Don't get the order wrong</strong> — see the <a href="#4-best-practices-common-mistakes--error-handling">Best Practices</a> section below: normalization must be done <strong>after</strong> the train-test split, not before!</p>
</blockquote>
<h3>1.5 Text Vectorization for NLP</h3>
<p>Computers can't directly understand words/sentences, so text needs to be turned into a numeric representation (vectors). Before being vectorized, text is usually cleaned first by removing links/URLs, tags (mentions/hashtags), and stopwords (<em>the, a, an, or, for</em>, etc.) — see <a href="#">Part 1's NLP-Specific Data Cleaning section</a> — because these elements carry no useful information and only add computational load.</p>
<p><strong>a) One-Hot Encoding</strong></p>
<ul>
<li><p><strong>How it works</strong>: every word is turned into a binary vector; each word has its own position (index), with only one value being 1 and the rest 0.</p>
</li>
<li><p><strong>Advantages</strong>: simple, easy to implement, good for a small vocabulary.</p>
</li>
<li><p><strong>Disadvantages</strong>: the vector dimension becomes very large as the number of words grows; doesn't capture relationships/meaning between words.</p>
</li>
</ul>
<blockquote>
<p><strong>Corpus</strong> = the collection of text data that forms the basis for building a vocabulary. In modern NLP, a word is often called a <strong>token</strong>.</p>
</blockquote>
<p>Example corpus (5 words): <em>aku (I), makan (eat), nasi (rice), pakai (with), tempe (tempeh)</em></p>
<table>
<thead>
<tr>
<th>Word</th>
<th>Vector Representation</th>
</tr>
</thead>
<tbody><tr>
<td>aku</td>
<td>[1, 0, 0, 0, 0]</td>
</tr>
<tr>
<td>makan</td>
<td>[0, 1, 0, 0, 0]</td>
</tr>
<tr>
<td>nasi</td>
<td>[0, 0, 1, 0, 0]</td>
</tr>
<tr>
<td>pakai</td>
<td>[0, 0, 0, 1, 0]</td>
</tr>
<tr>
<td>tempe</td>
<td>[0, 0, 0, 0, 1]</td>
</tr>
</tbody></table>
<p>The sentence "aku makan nasi" (I eat rice) → the combined vector of <em>aku</em> + <em>makan</em> + <em>nasi</em>. Each word is treated as standing on its own, without regard to the meaning relationships between words.</p>
<p><strong>b) Bag of Words (BoW)</strong></p>
<p>Represents a document based on how many times each word appears, according to the vocabulary built from all documents.</p>
<p>Example — D1: <em>rumah ini bagus (this house is nice)</em> | D2: <em>rumah saya makan nasi (my house I eat rice)</em> | D3: <em>saya makan nasi (I eat rice)</em></p>
<table>
<thead>
<tr>
<th>Document</th>
<th>rumah (house)</th>
<th>ini (this)</th>
<th>bagus (nice)</th>
<th>saya (I)</th>
<th>makan (eat)</th>
<th>nasi (rice)</th>
</tr>
</thead>
<tbody><tr>
<td>D1</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>0</td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td>D2</td>
<td>1</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>D3</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>1</td>
</tr>
</tbody></table>
<p>Only the count of each word matters; word order within a sentence is ignored. Simple, but doesn't yet understand the sentence's context/meaning.</p>
<p><strong>c) TF-IDF (Term Frequency – Inverse Document Frequency)</strong></p>
<ul>
<li><p><strong>Formula</strong>: <code>TF-IDF = TF × IDF</code></p>
</li>
<li><p><strong>TF (Term Frequency)</strong>: measures how often a word appears in a single document — the more often it appears, the higher its TF value.</p>
</li>
<li><p><strong>IDF (Inverse Document Frequency)</strong>: measures how unique/rare a word is across the entire document corpus. Common words (and, the, of) → low IDF because they're less informative; rare words → high IDF because they better distinguish document content.</p>
</li>
</ul>
<blockquote>
<p>🤔 <strong>Guess First:</strong> If you don't use IDF at all (only TF), what happens to common words like "the" or "of" that appear in ALMOST EVERY document?</p>
<p>See the Answer</p>
<p>Without IDF, words that appear frequently across many documents will <strong>always get a high weight</strong> even though they're not informative, making it hard for the model to distinguish the content between documents. This is exactly why IDF is needed — to "penalize" overly common words.</p>
</blockquote>
<ul>
<li><p><strong>Calculation process</strong>: must use every document in the dataset (the IDF value depends on the entire corpus), then TF and IDF are multiplied for each word in each document.</p>
</li>
<li><p><strong>Final result</strong>: a TF-IDF matrix sized (Number of Documents × Number of Unique Words in the Corpus). Example: 3 documents × 6 unique words → a 3×6 matrix.</p>
</li>
<li><p><strong>Uses</strong>: document classification, <em>information retrieval</em>, <em>search engines</em>, <em>text mining</em>.</p>
</li>
</ul>
<p><strong>d) Word2Vec</strong></p>
<ul>
<li><strong>Concept</strong>: represents words using <em>dense vectors</em> (embeddings) learned by a neural network; capable of capturing semantic relationships between words (words with similar meaning → vectors close together).</li>
</ul>
<blockquote>
<p>🤔 <strong>Guess First:</strong> Complete this famous word-vector equation: <code>King − Man + Woman ≈ ?</code></p>
<p>See the Answer</p>
<p><strong>Queen.</strong> This is the classic example showing that Word2Vec captures semantic relationships between words — not just word occurrence like BoW/TF-IDF.</p>
</blockquote>
<ul>
<li><p><strong>How it works</strong>: the model is trained to predict a missing word (<em>masked word</em>) or the surrounding words based on context — it's not generative, but rather word classification based on the context before &amp; after.</p>
</li>
<li><p><strong>Hidden Dimension</strong>: the representation space (<em>embedding space</em>) that stores each word's features. Example: Hidden Dimension = 512 → each word is represented as a 512-feature vector.</p>
</li>
<li><p><strong>Visualization</strong>: embedding results can be visualized with PCA, t-SNE, or UMAP — words with similar meaning will form clusters (e.g., "King" and "Queen" ending up close together).</p>
</li>
</ul>
<h3>1.6 Images as Numeric Data</h3>
<p>With image data, the computer essentially already receives the data in numeric form.</p>
<p><strong>a) Representing an Image as a Matrix</strong></p>
<ul>
<li><p>Images are stored as binary data, then represented as a pixel matrix.</p>
</li>
<li><p><strong>Grayscale</strong>: a single intensity value (0–255).</p>
</li>
<li><p><strong>RGB</strong>: three values — Red, Green, Blue.</p>
</li>
</ul>
<p><strong>b) CNN as a Feature Extractor</strong></p>
<ul>
<li><p><strong>How it works</strong>: a <em>Convolutional Neural Network</em> (CNN) automatically learns important image characteristics: edges, lines, textures, object shapes, patterns, and object parts.</p>
</li>
<li><p><strong>The deeper the CNN layer</strong>: the more complex the features it learns — early layers recognize lines/edges, deeper layers recognize complex objects (faces, vehicles, animals).</p>
</li>
<li><p><strong>Computer Vision tasks</strong>: Image Classification, Object Detection, Image Segmentation, Face Recognition, Image Retrieval.</p>
</li>
</ul>
<hr />
<h2>2. Saving a Clean Dataset</h2>
<p>Once all the cleaning and transformation steps are done, it's time to save the clean dataset — think of it as the "prepared ingredients" ready to be handed to the AI.</p>
<pre><code class="language-python">df_clean.to_csv('retail_data_clean.csv', index=False)
df_clean.to_excel('retail_data_clean.xlsx', index=False)
df_clean.to_pickle('retail_data_clean.pkl')  # format Python
</code></pre>
<blockquote>
<p>Note: always save the original dataset and the cleaned dataset under <strong>different file names</strong> — never <em>overwrite</em> the original data.</p>
</blockquote>
<hr />
<h2>3. Data Versioning &amp; Management Strategies</h2>
<h3>A. Data Versioning</h3>
<ul>
<li><p><strong>Concept</strong>: one of the important concepts in MLOps.</p>
</li>
<li><p><strong>Goal</strong>: to support the <em>development lifecycle</em> by providing a checkpoint at every stage, so every change can be tracked.</p>
</li>
<li><p><strong>Rollback benefit</strong>: if an error occurs or an experiment's results don't meet expectations, a developer can roll back to a previous version without redoing the entire process from scratch.</p>
</li>
<li><p><strong>End result</strong>: the development process becomes more structured, well-documented, and easy to trace.</p>
</li>
</ul>
<p><strong>Requirements for a Versioning System:</strong></p>
<ul>
<li><p>Able to label every version.</p>
</li>
<li><p>Able to create a <em>checkpoint</em> / <em>freeze progress</em> at every development stage.</p>
</li>
<li><p>Allows a developer to roll back to a specific version at any time.</p>
</li>
</ul>
<h3>B. Version Numbering Scheme</h3>
<ul>
<li><p><strong>vX.X.X</strong> (e.g., v1.0.0): consists of <strong>Major Version</strong> (fundamental/total changes), <strong>Minor Version</strong> (fairly large changes that don't change the whole system), and <strong>Patch/Revision</strong> or <em>Micro Changing</em> (small changes that don't affect the core structure).</p>
</li>
<li><p><strong>vX.X.X.X</strong>: adds one more Patch component, specifically for <em>bug fixing</em> or minor repairs.</p>
</li>
</ul>
<h3>C. Version Change Categories</h3>
<table>
<thead>
<tr>
<th>Category</th>
<th>When It's Used</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td>Major Version</td>
<td>Very significant change</td>
<td>Changing the ML task (Image Classification → Object Detection); a complete overhaul of the system architecture</td>
</tr>
<tr>
<td>Minor Version</td>
<td>A fairly large change, doesn't change the whole system</td>
<td>Adding a new class; a large change to the dataset; adding a major feature that impacts performance</td>
</tr>
<tr>
<td>Micro Changing</td>
<td>A small change</td>
<td>Adding a small amount of data; the number of classes stays the same; a minor change to preprocessing/configuration</td>
</tr>
<tr>
<td>Patch (Bug Fix)</td>
<td>Fixing an error without changing the main behavior</td>
<td>Fixing a bug; adding 1–10 previously incorrect data points; fixing a labeling/configuration error</td>
</tr>
</tbody></table>
<blockquote>
<p>🤔 <strong>Guess First:</strong> You just fixed 5 mislabeled data points in your dataset. Which version category does that fall under — Major, Minor, Micro, or Patch?</p>
<p>See the Answer</p>
<p><strong>Patch (Bug Fix)</strong> — fixing labeling errors is a classic Patch example, since it doesn't change the system's main behavior, it just corrects a small mistake.</p>
</blockquote>
<h3>D. GitLab as a Data Versioning Tool</h3>
<table>
<thead>
<tr>
<th>Advantages</th>
<th>Disadvantages</th>
</tr>
</thead>
<tbody><tr>
<td>Free for basic use</td>
<td>The free tier has limitations</td>
</tr>
<tr>
<td>Easy to use</td>
<td>Needs extra configuration to get dataset versioning working properly</td>
</tr>
<tr>
<td>Good security</td>
<td>No advanced DataOps/MLOps-specific features yet</td>
</tr>
<tr>
<td>Supports a subfolder structure</td>
<td>Not optimal for very large datasets</td>
</tr>
<tr>
<td>Open source</td>
<td></td>
</tr>
</tbody></table>
<h3>E. DataOps and MLOps Tools</h3>
<table>
<thead>
<tr>
<th>Category</th>
<th>Tools</th>
</tr>
</thead>
<tbody><tr>
<td>Annotation Tools (already have dataset versioning features)</td>
<td>CVAT (open source), Roboflow (computer-vision-specific), SuperAnnotate, V7 Labs, Scale AI</td>
</tr>
<tr>
<td>Dataset Versioning &amp; Preprocessing</td>
<td>DVC – Data Version Control (open source), FiftyOne (open source)</td>
</tr>
<tr>
<td>ML Pipelining &amp; Experiment Tracking</td>
<td>Neptune.ai, Weights &amp; Biases (W&amp;B), MLflow (open source)</td>
</tr>
</tbody></table>
<hr />
<h2>4. Best Practices, Common Mistakes &amp; Error Handling</h2>
<h3>A. Do &amp; Don't</h3>
<table>
<thead>
<tr>
<th>❌ Don't Do This</th>
<th>✅ Do This</th>
</tr>
</thead>
<tbody><tr>
<td>Normalize before the train-test split</td>
<td>Split the data first, then normalize</td>
</tr>
<tr>
<td>Drop all rows with missing values without analysis</td>
<td>Analyze the missing value pattern before taking action</td>
</tr>
<tr>
<td>Forget to reset the index after dropping rows</td>
<td>Always <code>reset_index(drop=True)</code> after cleaning</td>
</tr>
<tr>
<td>Normalize the target variable (y) for regression</td>
<td>Keep the target variable at its original scale</td>
</tr>
<tr>
<td>Use test data when fitting the scaler</td>
<td>Fit the scaler only on training data</td>
</tr>
<tr>
<td>Skip checking data quality after transformation</td>
<td>Validate the results with visualization and statistics</td>
</tr>
</tbody></table>
<h3>B. Trust But Verify</h3>
<table>
<thead>
<tr>
<th>Common Mistake</th>
<th>Solution</th>
</tr>
</thead>
<tbody><tr>
<td>Not checking data types before a numeric operation</td>
<td>Always verify with <code>df.dtypes</code> before computing a mean or sum</td>
</tr>
<tr>
<td>Forgetting to handle null values before converting types</td>
<td>Handle them first with <code>dropna()</code> or <code>fillna()</code> before changing the data type</td>
</tr>
<tr>
<td>Overwriting the original dataset</td>
<td>Always keep the original dataset, make a copy for cleaning: <code>df_clean = df.copy()</code></td>
</tr>
<tr>
<td>Not verifying the cleaning results</td>
<td>After cleaning, run <code>df.info()</code> and <code>df.head()</code> to confirm the result is correct</td>
</tr>
<tr>
<td>Normalizing unnecessarily</td>
<td>Not every model needs normalization — understand the context and the algorithm being used</td>
</tr>
</tbody></table>
<blockquote>
<p>Principle: <strong>"Trust but verify"</strong> — always double-check the result of every cleaning step.</p>
</blockquote>
<h3>C. Error Handling</h3>
<table>
<thead>
<tr>
<th>Error</th>
<th>Cause</th>
<th>Solution</th>
</tr>
</thead>
<tbody><tr>
<td><code>KeyError</code></td>
<td>Wrong or nonexistent column name</td>
<td>Check the column name with <code>df.columns</code></td>
</tr>
<tr>
<td><code>ValueError</code></td>
<td>Data type conversion failed</td>
<td>Use the <code>errors='coerce'</code> parameter with <code>pd.to_numeric()</code></td>
</tr>
<tr>
<td><code>FileNotFoundError</code></td>
<td>Wrong file path or file doesn't exist</td>
<td>Make sure the file has been uploaded to the working environment (e.g., Google Colab)</td>
</tr>
<tr>
<td><code>AttributeError</code></td>
<td>Method not available for that particular data type</td>
<td>Check the data type with <code>type()</code> or <code>df.dtypes</code></td>
</tr>
</tbody></table>
<blockquote>
<p>Note: an error is a learning moment, not a failure.</p>
</blockquote>
<hr />
<h2>5. Before vs After &amp; Data Pipeline Flow</h2>
<h3>A. Before vs After: From Chaos to Clarity</h3>
<table>
<thead>
<tr>
<th>Raw Dataset</th>
<th>Clean Dataset</th>
</tr>
</thead>
<tbody><tr>
<td>Missing values scattered across columns</td>
<td>No missing values</td>
</tr>
<tr>
<td>Deceptive duplicate data</td>
<td>Every row is unique</td>
</tr>
<tr>
<td>Inconsistent data types</td>
<td>Data types match the context</td>
</tr>
<tr>
<td>Ambiguous column names</td>
<td>Clear and consistent column names</td>
</tr>
<tr>
<td>Extreme outlier values</td>
<td>Values within a reasonable range</td>
</tr>
<tr>
<td>Non-standard formats</td>
<td>Standardized formats</td>
</tr>
</tbody></table>
<ul>
<li><p><strong>Impact of a raw dataset</strong>: the AI gets confused, errors occur during training, and predictions are inaccurate.</p>
</li>
<li><p><strong>Impact of a clean dataset</strong>: the AI can learn well, training runs smoothly, and predictions are accurate.</p>
</li>
</ul>
<h3>B. Data Pipeline Flow (End-to-End)</h3>
<ol>
<li><p><strong>Data Collection</strong> — gathering data from various sources (database, API, files).</p>
</li>
<li><p><strong>Data Loading</strong> — reading data into the Python environment.</p>
</li>
<li><p><strong>Data Exploration</strong> — understanding the structure, distribution, and issues in the data.</p>
</li>
<li><p><strong>Data Cleaning</strong> — handling missing values, duplicates, and errors.</p>
</li>
<li><p><strong>Data Transformation</strong> — sorting, filtering, normalization, feature engineering.</p>
</li>
<li><p><strong>Data Storage</strong> — saving the clean dataset for the next stage (model training).</p>
</li>
</ol>
<hr />
<h2>6. Quiz Check — Part 2</h2>
<p><strong>Q1. You have a dataset with extreme outliers in the salary column. Which scaling method is the most resistant to outliers: Min-Max, Standardization, or Robust Scaling?</strong></p>
<p><strong>Answer: Robust Scaling.</strong> This method uses the median and IQR (instead of mean/min/max), making it far more resistant to outliers than either Min-Max Scaling or Standardization.</p>
<p><strong>Q2. Why MUST normalization be done after the train-test split, rather than before?</strong></p>
<p><strong>Answer:</strong> Because the scaler (e.g., <code>MinMaxScaler</code>) must be <em>fit</em> only on the training data. If it's fit before the split (on the entire dataset including the test set), information from the test set "leaks" into the training process — this is called <em>data leakage</em>, and it makes the model evaluation dishonest.</p>
<p><strong>Q3. Your code throws a `KeyError` when trying to access `df['price']`. What's the most likely cause and solution?</strong></p>
<p><strong>Answer:</strong> The most likely cause is a <strong>wrong or nonexistent column name</strong> — maybe the actual column name is <code>Price</code> or <code>harga</code> (different capitalization). Solution: check the exact column name with <code>df.columns</code>.</p>
<p><strong>Q4. You just added one new object class to an object detection dataset, plus thousands of new images. What kind of version change is this?</strong></p>
<p><strong>Answer: Minor Version.</strong> Adding a new class and a large change to the dataset both fall under the Minor Version category — a fairly large change, but one that doesn't yet change the whole system/ML task.</p>
<hr />
<h2>7. Preprocessing Checklist Cheat Sheet</h2>
<p>Keep this checklist as a quick reference before your data goes into the training stage:</p>
<p><strong>Exploration</strong></p>
<ul>
<li><p>[ ] Have you run <code>df.head()</code>, <code>df.info()</code>, <code>df.describe()</code>, <code>df.shape</code>?</p>
</li>
<li><p>[ ] Do you know the data type of every column (<code>df.dtypes</code>)?</p>
</li>
</ul>
<p><strong>Cleaning</strong></p>
<ul>
<li><p>[ ] Have missing values been checked (<code>df.isnull().sum()</code>) and handled based on their percentage?</p>
</li>
<li><p>[ ] Have duplicates been checked (<code>df.duplicated().sum()</code>) and removed?</p>
</li>
<li><p>[ ] Have outliers been detected (Boxplot/IQR/Z-Score) and a decision made: remove or treat?</p>
</li>
<li><p>[ ] Have data types been converted correctly (<code>astype</code>, <code>to_numeric</code>, <code>to_datetime</code>)?</p>
</li>
<li><p>[ ] Has text been tidied up (strip, lowercase, replace) and made format-consistent?</p>
</li>
<li><p>[ ] Are column names <code>snake_case</code> and consistent?</p>
</li>
<li><p>[ ] Has the index been reset after dropping rows (<code>reset_index(drop=True)</code>)?</p>
</li>
</ul>
<p><strong>Transformation</strong></p>
<ul>
<li><p>[ ] Has categorical data been encoded according to its type (nominal → One-Hot, ordinal → direct numeric)?</p>
</li>
<li><p>[ ] Has dimensionality reduction been considered if there are too many features?</p>
</li>
<li><p>[ ] Is the scaler fit only on the training data, <strong>after</strong> the train-test split?</p>
</li>
<li><p>[ ] Is the target variable (y) <strong>not</strong> normalized for regression cases?</p>
</li>
</ul>
<p><strong>Saving &amp; Versioning</strong></p>
<ul>
<li><p>[ ] Is the original dataset saved separately from the clean dataset (different file names)?</p>
</li>
<li><p>[ ] Has the clean dataset been given a version (Major/Minor/Micro/Patch) matching the type of change?</p>
</li>
<li><p>[ ] Is the dataset stored somewhere that supports versioning (S3/GCS/DVC/etc.)?</p>
</li>
</ul>
<hr />
<p><em>See</em> <a href="https://shaka-ai.hashnode.dev/data-handling-preprocessing-part-1-en"><em>Part 1</em></a> <em>for the full discussion on Data Collection, Storage, and Data Cleaning.</em></p>
]]></content:encoded></item><item><title><![CDATA[Data Handling & Preprocessing (Part 1): Foundations, Collection, and Data Cleaning]]></title><description><![CDATA[Part 1 of 2: why preprocessing matters, how data is collected & stored, an introduction to Pandas, data exploration, and a deep dive into the cleaning process.

Before an AI model can learn anything, ]]></description><link>https://shaka-ai.hashnode.dev/data-handling-preprocessing-part-1-en</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/data-handling-preprocessing-part-1-en</guid><category><![CDATA[Data Science]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Python]]></category><category><![CDATA[pandas]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[data analysis]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Sun, 30 Aug 2026 10:34:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/0862014a-70d3-44eb-ad84-3bb548225e95.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><em>Part 1 of 2: why preprocessing matters, how data is collected &amp; stored, an introduction to Pandas, data exploration, and a deep dive into the cleaning process.</em></p>
</blockquote>
<p>Before an AI model can learn anything, it has to "eat" data first. The problem is, raw data is almost always messy — some of it is missing, some is duplicated, some is inconsistently formatted. Part 1 of this note covers the foundations of data handling through to the full cleaning process. Part 2 will continue with transformation, versioning, and best practices.</p>
<h2>Table of Contents</h2>
<ul>
<li><p><a href="#1-why-does-data-preprocessing-matter">1. Why Does Data Preprocessing Matter?</a></p>
</li>
<li><p><a href="#2-data-collection--storage">2. Data Collection &amp; Storage</a></p>
</li>
<li><p><a href="#3-pandas-the-main-data-handling-tool">3. Pandas: The Main Data Handling Tool</a></p>
</li>
<li><p><a href="#4-exploring-data-before-cleaning">4. Exploring Data Before Cleaning</a></p>
</li>
<li><p><a href="#5-data-cleaning-deep-dive">5. Data Cleaning (Deep Dive)</a></p>
</li>
<li><p><a href="#6-quiz-check--part-1">6. Quiz Check — Part 1</a></p>
</li>
</ul>
<hr />
<h2>1. Why Does Data Preprocessing Matter?</h2>
<p>Even the most sophisticated AI model won't produce accurate predictions if it's trained on bad data. Preprocessing isn't just a technical step — it's the foundation of an AI project's success.</p>
<p><strong>Golden rule:</strong> <em>Garbage In, Garbage Out</em> (GIGO) — if the data is dirty, the AI's output will keep being wrong.</p>
<blockquote>
<p>🤔 <strong>Guess First:</strong> In your opinion, what percentage of a data science project's total time is spent on data <em>preprocessing</em> versus building the model?</p>
<p>See the Answer</p>
<p><strong>80% of the time</strong> in a project is spent preparing &amp; cleaning data, with only <strong>20%</strong> left for building &amp; training the model. This is the AI success formula that beginners often underestimate.</p>
</blockquote>
<table>
<thead>
<tr>
<th>Statistic</th>
<th>Meaning</th>
</tr>
</thead>
<tbody><tr>
<td>80%</td>
<td>Of total data science project time spent on preprocessing</td>
</tr>
<tr>
<td>70%</td>
<td>Accuracy improvement achievable with good data cleaning</td>
</tr>
<tr>
<td>90%</td>
<td>Success rate of AI projects that apply proper preprocessing</td>
</tr>
</tbody></table>
<blockquote>
<p><em>Note: the figures above (including the "80% unstructured data" statistic in the next section) are commonly-cited figures in data science material, not from a single academic study that can be directly referenced. Read them as an illustration of scale/urgency, not a precise statistic.</em></p>
</blockquote>
<h3>Real Examples: When Bad Data Breaks AI</h3>
<p><strong>Case 1 — A Failed Chatbot</strong></p>
<p>A customer service chatbot was trained on conversation data full of typos &amp; non-standard language → the bot couldn't understand customer questions, and its answers became irrelevant.</p>
<p><strong>Case 2 — A Wrong Price Prediction</strong></p>
<p>A property price prediction model was trained on data with a lot of missing values &amp; duplicates → a house worth Rp 500 million was predicted at Rp 2 billion.</p>
<h3>Key Definitions</h3>
<ul>
<li><p><strong>Data Preprocessing</strong>: the process of preparing data as well and as thoroughly as possible so it's ready to be used by AI.</p>
</li>
<li><p><strong>Data Cleaning</strong>: a part of Data Preprocessing responsible for cleaning data (missing values, duplicates, inconsistent formats, wrong data types, etc.).</p>
</li>
</ul>
<h3>The Data Preprocessing Workflow</h3>
<ol>
<li><p><strong>Raw Data</strong> — a dirty dataset with various problems and inconsistencies.</p>
</li>
<li><p><strong>Cleaning</strong> — removing nulls, fixing formats, handling outliers.</p>
</li>
<li><p><strong>Transformation</strong> — normalization, encoding, feature engineering.</p>
</li>
<li><p><strong>Model Ready</strong> — data ready to be used for training AI.</p>
</li>
</ol>
<hr />
<h2>2. Data Collection &amp; Storage</h2>
<h3>A. Data Collection</h3>
<p>Data Collection is the scheme for gathering data for the purpose of building AI. There are two broad types:</p>
<p><strong>1. Unstructured Data</strong></p>
<ul>
<li><p>Characteristics: humans can understand the meaning of the data (e.g., recognizing an animal species) and have <em>reasoning</em> ability over that data (e.g., understanding the point of a paragraph).</p>
</li>
<li><p>Data sources: <em>generated data</em> is allowed, but <em>real-world data</em> that matches the problem is preferred — make sure the data conditions match real-world conditions.</p>
</li>
<li><p>Examples: text, images, video.</p>
</li>
<li><p>Fact: <strong>around 80% of the world's data</strong> is unstructured data.</p>
</li>
</ul>
<p><strong>2. Structured Data</strong></p>
<ul>
<li><p>Characteristics: ordinary people don't have the reasoning ability to interpret the data's meaning; it's unique to each case — similar cases can use different data (e.g., inventory optimization differs depending on the <em>business objective</em>).</p>
</li>
<li><p>Examples: tabular data, CSV, columns and rows.</p>
</li>
<li><p><strong>Important note</strong>: if an end-to-end system isn't available yet, don't jump straight into using AI.</p>
<ul>
<li><p>Build a <strong>Data Pipeline</strong> first so an end-to-end data flow is established (involve a Data Engineer if needed).</p>
</li>
<li><p>AI should be used as <em>tooling</em>, not as the primary weapon.</p>
</li>
</ul>
</li>
</ul>
<h3>B. Data Storage</h3>
<table>
<thead>
<tr>
<th>Data Type</th>
<th>Storage Medium</th>
<th>Examples</th>
</tr>
</thead>
<tbody><tr>
<td>Unstructured Data</td>
<td>File Storage System — supports versioning, easy access, secure</td>
<td>Amazon S3, Google Cloud Storage (GCS)</td>
</tr>
<tr>
<td>Structured Data</td>
<td>Database (SQL/NoSQL) — ideally with separate data specifically for ML purposes, following the same principles: easy to store, versioned, easy to access, secure</td>
<td>PostgreSQL, MariaDB</td>
</tr>
<tr>
<td>Object Storage</td>
<td>Medium for object-shaped data, generally large files; supports scalability, versioning, good security</td>
<td>Images, videos, documents, ML datasets</td>
</tr>
</tbody></table>
<hr />
<h2>3. Pandas: The Main Data Handling Tool</h2>
<p>Pandas is one of the most popular Python libraries for data manipulation and analysis — think of it as Excel, but with superpowers and coding flexibility.</p>
<ul>
<li><p><strong>Fast</strong> — processes millions of rows in seconds.</p>
</li>
<li><p><strong>Powerful</strong> — complex operations with short code.</p>
</li>
<li><p><strong>Flexible</strong> — can handle various data formats (CSV, Excel, JSON, etc.).</p>
</li>
</ul>
<h3>Other Data Tool Ecosystems</h3>
<p>The data world generally relies on just Python and SQL. Besides Pandas, Python has a few other libraries:</p>
<ul>
<li><p><strong>PySpark</strong> — used when data is stored in Apache Spark.</p>
</li>
<li><p><strong>Polars</strong> — rewritten in Rust so it's faster.</p>
</li>
<li><p><strong>DuckDB</strong> — for <em>online analytical processing</em> (OLAP) databases.</p>
</li>
</ul>
<blockquote>
<p>Note: if your data is still in CSV format, Pandas alone is enough.</p>
</blockquote>
<h3>File Formats Supported by Pandas</h3>
<table>
<thead>
<tr>
<th>Format</th>
<th>Pandas Function</th>
<th>Notes</th>
</tr>
</thead>
<tbody><tr>
<td>CSV</td>
<td><code>pd.read_csv()</code></td>
<td>One of the most common &amp; lightweight formats for tabular data; TSV is similar, differing in the delimiter</td>
</tr>
<tr>
<td>Excel (XLSX/XLS)</td>
<td><code>pd.read_excel()</code></td>
<td>Spreadsheet files</td>
</tr>
<tr>
<td>JSON</td>
<td><code>pd.read_json()</code></td>
<td>Web data format; for unstructured datasets that need labeling</td>
</tr>
<tr>
<td>SQL Database</td>
<td><code>pd.read_sql()</code></td>
<td>Query directly from a database</td>
</tr>
</tbody></table>
<h3>Loading a Dataset</h3>
<p><code>pd</code> is an alias for Pandas, making the code shorter to write. Pandas can read almost every data format commonly used in data science.</p>
<pre><code class="language-python">import pandas as pd

df = pd.read_csv('retail_data_raw.csv')
print(f"Jumlah baris: {len(df)}")
</code></pre>
<p><strong>Practical tips:</strong></p>
<ul>
<li><p>Always check the <em>encoding</em> if you see odd characters.</p>
</li>
<li><p>Use the <code>sep</code> parameter for a custom delimiter.</p>
</li>
<li><p>The <code>nrows</code> parameter for previewing large data.</p>
</li>
<li><p>Pay attention to the delimiter and <em>decimal separator</em>.</p>
</li>
</ul>
<blockquote>
<p>Note: the variable <code>df</code> stands for "DataFrame", Pandas' main data structure shaped like a table with rows &amp; columns.</p>
</blockquote>
<h3>Data Structures: Series vs DataFrame</h3>
<ul>
<li><p><strong>Series</strong>: a 1-dimensional array with an index. Example: <code>pd.Series([1, 2, 3, 4])</code></p>
</li>
<li><p><strong>DataFrame</strong>: a 2-dimensional table with rows and columns — the most commonly used structure.</p>
</li>
</ul>
<hr />
<h2>4. Exploring Data Before Cleaning</h2>
<p>The principle of <strong>"Understand before fix"</strong>: an AI engineer must understand the "face" of their data — column structure, value types, and general patterns — before starting to clean it.</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Function</th>
</tr>
</thead>
<tbody><tr>
<td><code>df.head()</code> / <code>df.head(10)</code></td>
<td>Preview the first rows of the dataset (default 5 rows)</td>
</tr>
<tr>
<td><code>df.tail()</code></td>
<td>Preview the last rows; sometimes there's a different pattern at the end</td>
</tr>
<tr>
<td><code>df.info()</code></td>
<td>Number of rows &amp; columns, column names, data types, non-null counts, memory usage</td>
</tr>
<tr>
<td><code>df.describe()</code></td>
<td>Descriptive statistics for numeric columns: mean, median, min, max, standard deviation</td>
</tr>
<tr>
<td><code>df.shape</code></td>
<td>Dataset size in <code>(rows, columns)</code> format</td>
</tr>
<tr>
<td><code>df.columns</code></td>
<td>List of all column names in the dataset</td>
</tr>
</tbody></table>
<hr />
<h2>5. Data Cleaning (Deep Dive)</h2>
<h3>5.1 Common Problems in a Dataset</h3>
<table>
<thead>
<tr>
<th>Problem</th>
<th>Description</th>
<th>Example / Impact</th>
</tr>
</thead>
<tbody><tr>
<td>Missing Values (Null)</td>
<td>Data that is missing or incomplete</td>
<td>Can cause model errors or biased predictions</td>
</tr>
<tr>
<td>Outliers</td>
<td>Values that are too extreme / don't make sense</td>
<td>Age 250 years, negative salary</td>
</tr>
<tr>
<td>Duplicates</td>
<td>The same row appears multiple times</td>
<td>Can cause the model to overfit to certain data</td>
</tr>
<tr>
<td>Inconsistent Format</td>
<td>Dates in various formats, extra spaces, random capitalization</td>
<td>"Jakarta" vs "jakarta" vs "JAKARTA "</td>
</tr>
<tr>
<td>Wrong Data Type</td>
<td>Numbers stored as text, or categories stored as numbers</td>
<td>A price column typed as object, can't be calculated</td>
</tr>
</tbody></table>
<h3>5.2 Data Types &amp; Conversion</h3>
<p>One classic data cleaning problem is the wrong data type — for example, numbers stored as text, or dates stored as strings. Data types need to be transformed without changing their original meaning (numeric data is ratio/interval in nature, handled differently from categorical data).</p>
<table>
<thead>
<tr>
<th>Data Type</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>int64</code></td>
<td>Integer</td>
</tr>
<tr>
<td><code>float64</code></td>
<td>Decimal number</td>
</tr>
<tr>
<td><code>object</code></td>
<td>Text / String</td>
</tr>
<tr>
<td><code>datetime64</code></td>
<td>Date and time</td>
</tr>
<tr>
<td><code>bool</code></td>
<td>True / False</td>
</tr>
</tbody></table>
<blockquote>
<p>Note: if a price column is stored as <code>object</code> (text), you can't calculate its average or sum — and the AI will get confused too.</p>
</blockquote>
<pre><code class="language-python"># Konversi ke numerik
df['quantity'] = df['quantity'].astype(int)
df['price'] = pd.to_numeric(df['price'], errors='coerce')

# Konversi ke datetime
df['date'] = pd.to_datetime(df['date'], format='%d/%m/%Y')

# Konversi ke string
df['customer_id'] = df['customer_id'].astype(str)
</code></pre>
<blockquote>
<p>Note: the <code>errors='coerce'</code> parameter turns values that fail to convert into <code>NaN</code>, so the code doesn't throw an error.</p>
</blockquote>
<h3>5.3 Cleaning Text Data (Tabular)</h3>
<p>Common string operations for tidying up text columns:</p>
<pre><code class="language-python">df['nama'] = df['nama'].str.strip()              # hapus whitespace
df['kota'] = df['kota'].str.lower()               # lowercase semua
df['telp'] = df['telp'].str.replace('-', '')      # replace karakter
df['nomor'] = df['text'].str.extract(r'(\d+)')    # extract angka saja
</code></pre>
<p><strong>Text cleaning tips:</strong></p>
<ul>
<li><p>Always standardize <em>case</em> (upper/lower).</p>
</li>
<li><p>Strip leading &amp; trailing <em>whitespace</em>.</p>
</li>
<li><p>Remove special characters if unnecessary.</p>
</li>
<li><p>Check for typos with <code>value_counts()</code>.</p>
</li>
<li><p>Stay consistent with format ("Jakarta" vs "jakarta").</p>
</li>
</ul>
<blockquote>
<p>Note: this is text cleaning for table columns (<em>general string cleanup</em>). It's different from preparing text for an NLP model (<em>text vectorization</em>) below, which requires extra steps.</p>
</blockquote>
<h3>5.4 NLP-Specific Data Cleaning</h3>
<p>Before text is vectorized for an NLP model, there are words considered to carry little useful information that need to be removed:</p>
<ul>
<li><p><strong>Links or URLs</strong></p>
</li>
<li><p><strong>Tags</strong> (mentions or hashtags)</p>
</li>
<li><p><strong>Stopwords</strong>, such as: <em>the, a, an, or, for</em>, and other common words</p>
</li>
</ul>
<p><strong>Why:</strong> links/tags aren't relevant to model learning; stopwords appear repeatedly, add computational load, and don't help distinguish one document from another.</p>
<h3>5.5 Missing Values</h3>
<p>Missing values are one of the most common problems in real-world datasets — they can happen due to input errors, broken sensors, or the data simply not existing.</p>
<p><strong>Why they're dangerous:</strong></p>
<ul>
<li><p>AI can't learn from empty data.</p>
</li>
<li><p>Can cause errors during model training.</p>
</li>
<li><p>Reduces prediction accuracy.</p>
</li>
</ul>
<p><strong>Detection:</strong></p>
<ul>
<li><p><code>df.isnull().sum()</code> → number of nulls per column.</p>
</li>
<li><p><code>df.isnull().mean() * 100</code> → percentage of missing values.</p>
</li>
</ul>
<p><strong>Strategies for handling missing values:</strong></p>
<ol>
<li><p><strong>Drop rows</strong> (<code>df.dropna()</code>) — if missing values are few (&lt;5%) and there's still plenty of data.</p>
</li>
<li><p><strong>Drop a column</strong> (<code>df.dropna(axis=1)</code>) — if a column has too many missing values (&gt;50%).</p>
</li>
<li><p><strong>Fill with a specific value</strong> (<code>df.fillna(...)</code>) — use 0, mean, median, or mode depending on context; used when a lot of data is missing but the column is important.</p>
</li>
<li><p><strong>Forward fill / backward fill</strong> — for time series data, fill with the value before/after it (<code>method='ffill'</code>).</p>
</li>
</ol>
<pre><code class="language-python"># 1. Hapus baris dengan null
df_clean = df.dropna()

# 2. Hapus kolom dengan &gt;50% null
threshold = len(df) * 0.5
df_clean = df.dropna(axis=1, thresh=threshold)

# 3. Isi dengan nilai tertentu / mean
df['age'].fillna(0, inplace=True)
df['price'].fillna(df['price'].mean(), inplace=True)

# 4. Forward fill (time series)
df.fillna(method='ffill', inplace=True)
</code></pre>
<blockquote>
<p>Principle: <strong>"No blanks for brains"</strong> — AI needs complete data to learn well.</p>
</blockquote>
<h3>5.6 Duplicate Data</h3>
<p>Duplicate data happens when one observation appears more than once in a dataset — usually caused by input errors or careless dataset merging.</p>
<p><strong>Dangers of duplicates:</strong> the AI model can overfit to certain data, bias in predictions, longer training time, and inaccurate evaluation metrics.</p>
<pre><code class="language-python"># Deteksi duplikat
df.duplicated().sum()
df[df.duplicated()]

# Hapus duplikat
df_clean = df.drop_duplicates()
df_clean = df.drop_duplicates(subset=['customer_id', 'date'])
df_clean = df.drop_duplicates(keep='first')  # atau 'last'
</code></pre>
<blockquote>
<p><strong>Best practice</strong>: always keep the original dataset separate from the cleaned dataset (different file names) — never <em>overwrite</em> the original data.</p>
<p>Principle: <strong>"One truth per row"</strong> — every row must represent one unique observation (<em>unique and clean</em>).</p>
</blockquote>
<h3>5.7 Outliers</h3>
<p><strong>Definition:</strong> data that is anomalous / whose value is far different from most of the other data. Example: employee salaries are generally 5–15 million, but there's one at 500 million — that's an outlier.</p>
<p><strong>Simple detection:</strong> Boxplot, scatter plot, the IQR (Interquartile Range) method, the Z-Score method, or domain knowledge.</p>
<p><strong>When to remove an outlier:</strong> when its likelihood of occurring is very small, or when its presence has no impact on the business process. <strong>If an outlier represents a real business condition, don't remove it right away</strong> — it needs <em>treatment</em> instead.</p>
<p><strong>Advanced Technique: Outlier Detection with a Variational Autoencoder (VAE)</strong></p>
<p><strong>What is an Autoencoder?</strong></p>
<p>A neural network trained with the input and output being the same, so it learns to reconstruct data and understand the distribution of clean (normal) data.</p>
<p><strong>How it works for outlier detection:</strong></p>
<ul>
<li><p>A VAE compares the reconstruction result against the original input.</p>
</li>
<li><p>The bigger the difference (measured with <em>Mean Squared Error</em> / MSE), the more likely the data point is an outlier.</p>
</li>
<li><p>Data outside the learned distribution → the reconstruction result differs greatly → high MSE value → an indicator of outlier data.</p>
</li>
</ul>
<p>Other methods: <strong>Z-Score</strong>, <strong>Interquartile Range (IQR)</strong>, <strong>Isolation Forest</strong>.</p>
<p><strong>Hands-on Implementation — Detecting &amp; Removing Outliers with IQR:</strong></p>
<pre><code class="language-python">Q1 = df['price'].quantile(0.25)
Q3 = df['price'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

df_clean = df[(df['price'] &gt;= lower_bound) &amp; (df['price'] &lt;= upper_bound)]
</code></pre>
<blockquote>
<p>Note: the IQR method is one of the most commonly used ways to detect outliers — but it still needs to be validated against business context (see "When to remove an outlier" above). The number <strong>1.5</strong> is a standard constant, but it can be adjusted as needed.</p>
</blockquote>
<h3>5.8 Data That Confuses the Model</h3>
<p>Beyond numeric outliers, there's a more subtle category of "dirty data": data that actively confuses the model's learning process.</p>
<ul>
<li><p><strong>The goal of cleaning here:</strong> removing data that rarely occurs and doesn't represent actual conditions.</p>
</li>
<li><p><strong>Adversarial attack</strong>: data that has been <strong>deliberately modified</strong> so the AI model produces a wrong prediction.</p>
</li>
<li><p><strong>Important note:</strong> removing a small part of an object within an image is still acceptable if that object is actually lowering the quality of the training data — the goal is to keep the model learning from the correct patterns.</p>
</li>
</ul>
<blockquote>
<p>🤔 <strong>Guess First:</strong> If there's one small, irrelevant object in the corner of a training photo that's actually causing the model to learn incorrectly, is it okay to remove it from the image?</p>
<p>See the Answer</p>
<p><strong>Yes, it's okay.</strong> As long as the goal is to keep the model learning from correct patterns, and the object really is degrading the quality of the training data, removing a small part of an object is still acceptable.</p>
</blockquote>
<h3>5.9 Renaming Columns</h3>
<p><strong>Why it matters:</strong> column names are often inconsistent, spaces &amp; special characters cause hassle, <em>descriptive</em> names are easier to understand, and it standardizes the <em>naming convention</em>.</p>
<p><strong>Best practice:</strong> use <code>snake_case</code> (lowercase letters, underscores for spaces), clear and short names, avoid special characters.</p>
<pre><code class="language-python"># Rename kolom tertentu
df.rename(columns={'Nama Lengkap': 'nama', 'Umur (tahun)': 'usia'}, inplace=True)

# Rename semua kolom sekaligus
df.columns = ['nama', 'usia', 'gaji']

# Lowercase &amp; hilangkan spasi
df.columns = df.columns.str.lower()
df.columns = df.columns.str.replace(' ', '_')
</code></pre>
<hr />
<h2>6. Quiz Check — Part 1</h2>
<p><strong>Q1. Why should a categorical data type like "hair color" be encoded using One-Hot Encoding rather than converted into simple ordered numbers (0, 1, 2, ...)?</strong></p>
<p><strong>Answer:</strong> Because "hair color" is <strong>nominal</strong> data — it has no order/ranking. If it's turned into simple ordered numbers, the model might mistakenly assume there's a ranking relationship between categories (e.g., the number 2 being seen as "bigger" than 1), even though no such order exists. <em>(Full details are covered in Part 2.)</em></p>
<p><strong>Q2. When should you drop an ENTIRE COLUMN instead of just the rows that have missing values?</strong></p>
<p><strong>Answer:</strong> When that column has <strong>more than 50%</strong> missing values. If the missing values are few (under 5%) and there's still plenty of data, it's enough to just drop the rows with <code>df.dropna()</code>.</p>
<p><strong>Q3. An outlier salary of Rp 500 million is found in an HR dataset. Should it automatically be removed?</strong></p>
<p><strong>Answer:</strong> <strong>Not automatically.</strong> You need to check first whether that value represents a real business condition (e.g., it really is a director's salary) or is purely an input error. If it represents a real business condition, don't remove it right away — it needs <em>treatment</em> first.</p>
<p><strong>Q4. What's the difference between "cleaning text in a table column" and "cleaning text for NLP"?</strong></p>
<p><strong>Answer:</strong> Cleaning text in a table column focuses on <em>general string cleanup</em> (stripping whitespace, lowercasing, replacing characters). Cleaning text for NLP requires additional, more specific steps: removing <strong>links/URLs</strong>, <strong>tags</strong> (mentions/hashtags), and <strong>stopwords</strong> — because these elements aren't informative and add extra computational load to the language model.</p>
<hr />
<p><strong>Continue to</strong> <a href="https://shaka-ai.hashnode.dev/data-handling-preprocessing-part-2-en"><strong>Part 2</strong></a> — we'll cover the full Data Transformation process (encoding, dimensionality reduction, normalization, text vectorization, images as numeric data), how to save a clean dataset, Data Versioning &amp; Management Strategies, Best Practices &amp; Error Handling, up to the big-picture end-to-end Data Pipeline Flow.</p>
]]></content:encoded></item><item><title><![CDATA[Data Handling & Preprocessing (Part 2): Data Transformation, Versioning, dan Best Practices]]></title><description><![CDATA[Bagian 2 dari 2: lanjutan dari Part 1 yang membahas Data Cleaning. Di sini kita masuk ke Data Transformation, cara menyimpan dataset bersih, Data Versioning ala MLOps, Best Practices, sampai gambaran ]]></description><link>https://shaka-ai.hashnode.dev/data-handling-preprocessing-part-2-id</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/data-handling-preprocessing-part-2-id</guid><category><![CDATA[Data Science]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Python]]></category><category><![CDATA[mlops]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[data analysis]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Sun, 30 Aug 2026 10:27:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/fb71f96f-a419-4fac-9b60-cb5f846a00e5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><em>Bagian 2 dari 2: lanjutan dari</em> <a href="#"><em>Part 1</em></a> <em>yang membahas Data Cleaning. Di sini kita masuk ke Data Transformation, cara menyimpan dataset bersih, Data Versioning ala MLOps, Best Practices, sampai gambaran besar Data Pipeline Flow.</em></p>
</blockquote>
<p>Sekilas recap: di Part 1 kita sudah tahu kenapa preprocessing itu penting (formula 80/20), bagaimana data dikumpulkan &amp; disimpan, dasar-dasar Pandas, dan cara membersihkan data — mulai dari missing values, duplikat, outlier, sampai adversarial data. Sekarang saatnya mengubah data yang sudah bersih itu menjadi format yang benar-benar bisa "dimakan" oleh algoritma Machine Learning.</p>
<h2>Daftar Isi</h2>
<ul>
<li><p><a href="#1-data-transformation">1. Data Transformation</a></p>
</li>
<li><p><a href="#2-menyimpan-dataset-bersih">2. Menyimpan Dataset Bersih</a></p>
</li>
<li><p><a href="#3-data-versioning--management-strategies">3. Data Versioning &amp; Management Strategies</a></p>
</li>
<li><p><a href="#4-best-practices-kesalahan-umum--error-handling">4. Best Practices, Kesalahan Umum &amp; Error Handling</a></p>
</li>
<li><p><a href="#5-before-vs-after--data-pipeline-flow">5. Before vs After &amp; Data Pipeline Flow</a></p>
</li>
<li><p><a href="#6-quiz-check--part-2">6. Quiz Check — Part 2</a></p>
</li>
<li><p><a href="#7-cheat-sheet-checklist-preprocessing">7. Cheat Sheet Checklist Preprocessing</a></p>
</li>
</ul>
<hr />
<h2>1. Data Transformation</h2>
<p>Sebagian besar algoritma ML <strong>hanya menerima data numerik</strong>, sehingga data non-numerik perlu diubah menjadi representasi angka (<em>vectorization / encoding</em>). Ini inti dari data transformation.</p>
<h3>1.1 Sorting &amp; Filtering</h3>
<ul>
<li><p><strong>Sorting</strong> — prinsip <strong>"Order before logic"</strong>: data yang terurut lebih mudah dianalisis dan diproses oleh model AI.</p>
</li>
<li><p><strong>Filtering</strong> — memungkinkan kita fokus pada subset data tertentu, misalnya hanya transaksi dari Asia, atau hanya produk dengan harga di atas Rp 100.000.</p>
</li>
</ul>
<pre><code class="language-python"># Sorting
df_sorted = df.sort_values('price')                    # ascending
df_sorted = df.sort_values('price', ascending=False)    # descending

# Filtering
df_asia = df[df['region'] == 'Asia']
df_filtered = df[(df['region'] == 'Asia') &amp; (df['price'] &gt; 50000)]              # AND
df_filtered = df[(df['region'] == 'Asia') | (df['region'] == 'Europe')]         # OR
df_filtered = df[df['region'].isin(['Asia', 'Europe', 'Africa'])]               # isin()
</code></pre>
<h3>1.2 Encoding Data Kategorikal</h3>
<blockquote>
<p>🤔 <strong>Coba Tebak Dulu:</strong> Ukuran baju (S, M, L) dan warna rambut, dua-duanya kategori. Menurutmu, apa keduanya harus di-encode dengan cara yang sama?</p>
<p>Lihat Jawaban</p>
<p><strong>Tidak.</strong> Ukuran baju punya urutan tingkatan (S &lt; M &lt; L) sehingga termasuk <strong>ordinal</strong> — bisa langsung dikonversi ke angka. Warna rambut tidak punya urutan sehingga termasuk <strong>nominal</strong> — sebaiknya pakai One-Hot Encoding.</p>
</blockquote>
<ul>
<li><p><strong>Nominal</strong>: kategori tanpa urutan/tingkatan. Contoh: warna rambut, ras, jenis kendaraan, jenis kelamin → sebaiknya pakai <strong>One-Hot Encoding</strong> agar model tidak menganggap ada urutan.</p>
</li>
<li><p><strong>Ordinal</strong>: kategori dengan urutan/tingkatan yang bermakna. Contoh: ukuran baju (S, M, L), tingkat pendidikan, tingkat kepuasan → bisa dikonversi langsung ke nilai numerik.</p>
</li>
</ul>
<h3>1.3 Dimensionality Reduction (Mengubah Dimensi Data)</h3>
<p><strong>Tujuan:</strong> mengurangi jumlah fitur tanpa menghilangkan informasi penting, untuk mengatasi <em>Curse of Dimensionality</em> (fitur terlalu banyak → training lambat, kompleks, performa turun).</p>
<blockquote>
<p><em>Number of Dimensionality</em> = banyaknya fitur/variabel dalam dataset.</p>
</blockquote>
<p><strong>i. Teknik Transformasi Data</strong></p>
<ul>
<li><p><strong>PCA (Principal Component Analysis)</strong> — mencari arah dengan variasi data terbesar (<em>principal components</em>); banyak dipakai pada data numerik untuk mengompresi fitur yang saling berkorelasi.</p>
</li>
<li><p><strong>t-SNE</strong> — mempertahankan kedekatan antar data (<em>local structure</em>); baik untuk visualisasi klaster pada data berdimensi tinggi.</p>
</li>
<li><p><strong>UMAP</strong> — tujuan mirip t-SNE tapi lebih cepat &amp; skalabel untuk dataset besar; juga bisa dipakai untuk feature engineering.</p>
</li>
</ul>
<p><strong>ii. Menghapus Fitur yang Kurang Relevan</strong></p>
<ul>
<li><p><code>.corr()</code> <strong>(Pandas)</strong> — mengukur korelasi linear antar fitur maupun fitur dengan target.</p>
</li>
<li><p><strong>Predictive Power Score (PPS / ppscore)</strong> — mengukur kemampuan fitur memprediksi target; mampu mendeteksi hubungan non-linear (berbeda dari korelasi biasa).</p>
</li>
</ul>
<p><strong>iii. Feature Engineering (Membuat Fitur Baru)</strong></p>
<ul>
<li><p>Menggabungkan dua atau lebih fitur menjadi satu fitur baru.</p>
</li>
<li><p>Membuat rasio, selisih, atau transformasi tertentu dari fitur yang ada.</p>
</li>
<li><p>Mengekstraksi informasi baru dari data waktu (tanggal, jam, hari, bulan, dst).</p>
</li>
</ul>
<p><strong>iv. Kernelization (Membesarkan Dimensi)</strong></p>
<ul>
<li><p><strong>Konsep</strong>: memetakan data ke ruang berdimensi lebih tinggi agar data yang awalnya tidak terpisah secara linear menjadi dapat dipisahkan. Tidak bisa dilakukan dengan metode linear, melainkan pakai <em>kernel function</em>.</p>
</li>
<li><p><strong>Kernel function</strong>: Linear Kernel, Polynomial Kernel, Radial Basis Function (RBF) Kernel, Sigmoid Kernel.</p>
</li>
<li><p><strong>Tujuan</strong>: membuat data <em>linearly separable</em> agar algoritma klasifikasi (Logistic Regression, SVM) bisa membangun <em>decision boundary</em> yang lebih baik.</p>
</li>
</ul>
<p><strong>Kernelization vs TF-IDF — sering ketuker, ini bedanya</strong></p>
<p>Kernelization dipakai di ML (misalnya SVM) untuk memetakan dimensi lebih tinggi, sedangkan TF-IDF dipakai untuk merepresentasikan <strong>teks</strong> menjadi vektor berbobot. Dua teknik yang beda tujuan meskipun sama-sama "mengubah representasi data".</p>
<h3>1.4 Normalisasi / Scaling</h3>
<p><strong>Definisi:</strong> proses mengubah skala data ke <em>range</em> tertentu (biasanya 0–1 atau -1 sampai 1) supaya semua fitur punya kontribusi yang seimbang ke model.</p>
<p><strong>Kenapa perlu:</strong></p>
<ul>
<li><p>Model AI sensitif terhadap skala data.</p>
</li>
<li><p>Fitur dengan nilai besar bisa mendominasi.</p>
</li>
<li><p>Mempercepat <em>convergence</em> saat training.</p>
</li>
<li><p>Meningkatkan akurasi model.</p>
</li>
</ul>
<table>
<thead>
<tr>
<th>Jenis</th>
<th>Rumus</th>
<th>Kapan Dipakai</th>
</tr>
</thead>
<tbody><tr>
<td>Min-Max Scaling</td>
<td><code>(x - min) / (max - min)</code> → skala ke 0–1</td>
<td>Data tidak punya outlier ekstrem</td>
</tr>
<tr>
<td>Standardization (Z-Score)</td>
<td><code>(x - mean) / std</code> → mean = 0, std = 1</td>
<td>Data punya outlier atau distribusi normal</td>
</tr>
<tr>
<td>Robust Scaling</td>
<td>Memakai median dan IQR, tahan outlier</td>
<td>Data dengan banyak outlier</td>
</tr>
</tbody></table>
<pre><code class="language-python">from sklearn.preprocessing import MinMaxScaler, StandardScaler

scaler = MinMaxScaler()
df['price_scaled'] = scaler.fit_transform(df[['price']])

scaler = StandardScaler()
df['price_std'] = scaler.fit_transform(df[['price']])
</code></pre>
<ul>
<li><p><strong>Kapan perlu normalisasi</strong>: terutama untuk algoritma yang sensitif terhadap skala, seperti <em>K-Nearest Neighbors</em> (KNN), <em>Neural Networks</em>, dan algoritma berbasis jarak.</p>
</li>
<li><p><strong>Manfaat untuk AI</strong>: kecepatan training (model <em>converge</em> lebih cepat), performa model (Neural Networks/KNN/SVM sangat sensitif skala), dan interpretasi yang adil (semua fitur berkontribusi seimbang tanpa didominasi satu fitur).</p>
</li>
</ul>
<blockquote>
<p>⚠️ <strong>Jangan sampai salah urutan</strong> — lihat bagian <a href="#4-best-practices-kesalahan-umum--error-handling">Best Practices</a> di bawah: normalisasi harus dilakukan <strong>setelah</strong> train-test split, bukan sebelumnya!</p>
</blockquote>
<h3>1.5 Text Vectorization untuk NLP</h3>
<p>Komputer tidak dapat langsung memahami kata/kalimat, sehingga teks perlu diubah menjadi representasi numerik (vektor). Sebelum divektorisasi, teks biasanya dibersihkan dulu dengan menghapus link/URL, tag (mention/hashtag), dan stopwords (<em>the, a, an, or, for</em>, dll) — lihat <a href="#">Part 1 bagian Data Cleaning Khusus NLP</a> — karena elemen-elemen ini tidak memberi informasi penting dan hanya memperberat komputasi.</p>
<p><strong>a) One-Hot Encoding</strong></p>
<ul>
<li><p><strong>Cara kerja</strong>: setiap kata diubah menjadi vektor biner; setiap kata punya posisi (indeks) sendiri, hanya satu nilai bernilai 1, sisanya 0.</p>
</li>
<li><p><strong>Kelebihan</strong>: sederhana, mudah diimplementasikan, cocok untuk vocabulary kecil.</p>
</li>
<li><p><strong>Kekurangan</strong>: dimensi vektor sangat besar jika jumlah kata banyak; tidak menangkap hubungan/makna antar kata.</p>
</li>
</ul>
<blockquote>
<p><strong>Korpus</strong> = kumpulan data teks yang jadi dasar pembentukan vocabulary. Dalam NLP modern, kata sering disebut <strong>token</strong>.</p>
</blockquote>
<p>Contoh korpus (5 kata): <em>aku, makan, nasi, pakai, tempe</em></p>
<table>
<thead>
<tr>
<th>Kata</th>
<th>Representasi Vektor</th>
</tr>
</thead>
<tbody><tr>
<td>aku</td>
<td>[1, 0, 0, 0, 0]</td>
</tr>
<tr>
<td>makan</td>
<td>[0, 1, 0, 0, 0]</td>
</tr>
<tr>
<td>nasi</td>
<td>[0, 0, 1, 0, 0]</td>
</tr>
<tr>
<td>pakai</td>
<td>[0, 0, 0, 1, 0]</td>
</tr>
<tr>
<td>tempe</td>
<td>[0, 0, 0, 0, 1]</td>
</tr>
</tbody></table>
<p>Kalimat "aku makan nasi" → gabungan vektor <em>aku</em> + <em>makan</em> + <em>nasi</em>. Setiap kata dianggap berdiri sendiri, tidak memperhatikan hubungan makna antar kata.</p>
<p><strong>b) Bag of Words (BoW)</strong></p>
<p>Merepresentasikan dokumen berdasarkan jumlah kemunculan tiap kata sesuai korpus yang terbentuk dari seluruh dokumen.</p>
<p>Contoh — D1: <em>rumah ini bagus</em> | D2: <em>rumah saya makan nasi</em> | D3: <em>saya makan nasi</em></p>
<table>
<thead>
<tr>
<th>Dokumen</th>
<th>rumah</th>
<th>ini</th>
<th>bagus</th>
<th>saya</th>
<th>makan</th>
<th>nasi</th>
</tr>
</thead>
<tbody><tr>
<td>D1</td>
<td>1</td>
<td>1</td>
<td>1</td>
<td>0</td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td>D2</td>
<td>1</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>D3</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td>1</td>
<td>1</td>
<td>1</td>
</tr>
</tbody></table>
<p>Yang diperhatikan hanya jumlah kemunculan kata; urutan kata dalam kalimat diabaikan. Sederhana, tetapi belum memahami konteks/makna kalimat.</p>
<p><strong>c) TF-IDF (Term Frequency – Inverse Document Frequency)</strong></p>
<ul>
<li><p><strong>Rumus</strong>: <code>TF-IDF = TF × IDF</code></p>
</li>
<li><p><strong>TF (Term Frequency)</strong>: mengukur seberapa sering sebuah kata muncul dalam satu dokumen — makin sering muncul, makin tinggi nilai TF-nya.</p>
</li>
<li><p><strong>IDF (Inverse Document Frequency)</strong>: mengukur seberapa unik/jarang suatu kata muncul di seluruh dokumen korpus. Kata umum (dan, yang, di) → IDF rendah karena kurang informatif; kata jarang muncul → IDF tinggi karena lebih membedakan isi dokumen.</p>
</li>
</ul>
<blockquote>
<p>🤔 <strong>Coba Tebak Dulu:</strong> Kalau tidak pakai IDF sama sekali (cuma pakai TF), apa yang terjadi pada kata umum seperti "yang" atau "di" yang sering muncul di HAMPIR SEMUA dokumen?</p>
<p>Lihat Jawaban</p>
<p>Tanpa IDF, kata yang sering muncul di banyak dokumen akan <strong>selalu berbobot tinggi</strong> meski tidak informatif, sehingga model jadi sulit membedakan isi antar dokumen. Inilah alasan IDF dibutuhkan — untuk "menghukum" kata yang terlalu umum.</p>
</blockquote>
<ul>
<li><p><strong>Proses perhitungan</strong>: harus memakai seluruh dokumen dalam dataset (nilai IDF bergantung pada seluruh korpus), lalu TF dan IDF dikalikan untuk tiap kata di tiap dokumen.</p>
</li>
<li><p><strong>Hasil akhir</strong>: matriks TF-IDF berukuran (Jumlah Dokumen × Jumlah Kata dalam Korpus). Contoh: 3 dokumen × 6 kata unik → matriks 3×6.</p>
</li>
<li><p><strong>Kegunaan</strong>: klasifikasi dokumen, <em>information retrieval</em>, <em>search engine</em>, <em>text mining</em>.</p>
</li>
</ul>
<p><strong>d) Word2Vec</strong></p>
<ul>
<li><strong>Konsep</strong>: representasi kata menggunakan <em>dense vector</em> (embedding) yang dipelajari oleh neural network; mampu menangkap hubungan semantik antar kata (kata bermakna mirip → vektor berdekatan).</li>
</ul>
<blockquote>
<p>🤔 <strong>Coba Tebak Dulu:</strong> Lengkapi rumus vektor kata terkenal ini: <code>King − Man + Woman ≈ ?</code></p>
<p>Lihat Jawaban</p>
<p><strong>Queen.</strong> Ini contoh klasik yang menunjukkan Word2Vec menangkap hubungan semantik antar kata — bukan cuma kemunculan kata seperti BoW/TF-IDF.</p>
</blockquote>
<ul>
<li><p><strong>Cara kerja</strong>: model dilatih memprediksi kata yang hilang (<em>masked word</em>) atau kata di sekitarnya berdasarkan konteks — bukan generatif, melainkan klasifikasi kata berdasarkan konteks sebelum &amp; sesudah.</p>
</li>
<li><p><strong>Hidden Dimension</strong>: ruang representasi (<em>embedding space</em>) penyimpan fitur tiap kata. Contoh: Hidden Dimension = 512 → tiap kata direpresentasikan sebagai vektor 512 fitur.</p>
</li>
<li><p><strong>Visualisasi</strong>: hasil embedding bisa divisualisasikan dengan PCA, t-SNE, atau UMAP — kata bermakna serupa akan membentuk cluster (contoh: "Raja" dan "Ratu" saling berdekatan).</p>
</li>
</ul>
<h3>1.6 Image sebagai Data Numerik</h3>
<p>Pada data gambar, komputer pada dasarnya sudah menerima data dalam bentuk numerik.</p>
<p><strong>a) Representasi Gambar sebagai Matriks</strong></p>
<ul>
<li><p>Gambar disimpan sebagai data biner, lalu direpresentasikan sebagai matriks piksel.</p>
</li>
<li><p><strong>Grayscale</strong>: satu nilai intensitas (0–255).</p>
</li>
<li><p><strong>RGB</strong>: tiga nilai — Red, Green, Blue.</p>
</li>
</ul>
<p><strong>b) CNN sebagai Feature Extractor</strong></p>
<ul>
<li><p><strong>Cara kerja</strong>: <em>Convolutional Neural Network</em> (CNN) secara otomatis mempelajari karakteristik penting gambar: tepi (<em>edges</em>), garis, tekstur, bentuk objek, pola, dan bagian-bagian objek.</p>
</li>
<li><p><strong>Semakin dalam layer CNN</strong>: semakin kompleks fitur yang dipelajari — layer awal mengenali garis/tepi, layer dalam mengenali objek kompleks (wajah, kendaraan, hewan).</p>
</li>
<li><p><strong>Tugas Computer Vision</strong>: Image Classification, Object Detection, Image Segmentation, Face Recognition, Image Retrieval.</p>
</li>
</ul>
<hr />
<h2>2. Menyimpan Dataset Bersih</h2>
<p>Setelah semua tahap cleaning dan transformation selesai, saatnya menyimpan dataset bersih — ibarat "bahan masakan" yang siap diberikan ke AI.</p>
<pre><code class="language-python">df_clean.to_csv('retail_data_clean.csv', index=False)
df_clean.to_excel('retail_data_clean.xlsx', index=False)
df_clean.to_pickle('retail_data_clean.pkl')  # format Python
</code></pre>
<blockquote>
<p>Catatan: selalu simpan dataset asli dan dataset yang sudah dibersihkan dengan <strong>nama file berbeda</strong> — jangan <em>overwrite</em> data original.</p>
</blockquote>
<hr />
<h2>3. Data Versioning &amp; Management Strategies</h2>
<h3>A. Data Versioning</h3>
<ul>
<li><p><strong>Konsep</strong>: salah satu konsep penting dalam MLOps.</p>
</li>
<li><p><strong>Tujuan</strong>: membantu <em>development lifecycle</em> dengan menyediakan checkpoint di tiap tahap, sehingga setiap perubahan dapat dilacak.</p>
</li>
<li><p><strong>Manfaat rollback</strong>: jika terjadi error atau hasil eksperimen tidak sesuai harapan, developer bisa rollback ke versi sebelumnya tanpa mengulang seluruh proses dari awal.</p>
</li>
<li><p><strong>Hasil akhir</strong>: proses pengembangan menjadi lebih terstruktur, terdokumentasi, dan mudah ditelusuri.</p>
</li>
</ul>
<p><strong>Syarat Sistem Versioning:</strong></p>
<ul>
<li><p>Bisa memberi label pada setiap versi.</p>
</li>
<li><p>Bisa membuat <em>checkpoint</em> / <em>freeze progress</em> pada setiap tahap pengembangan.</p>
</li>
<li><p>Memungkinkan pengembang rollback ke versi tertentu kapan saja.</p>
</li>
</ul>
<h3>B. Skema Penomoran Versi</h3>
<ul>
<li><p><strong>vX.X.X</strong> (contoh: v1.0.0): terdiri dari <strong>Major Version</strong> (perubahan fundamental/total), <strong>Minor Version</strong> (perubahan besar tapi tidak mengubah keseluruhan sistem), dan <strong>Patch/Revision</strong> atau <em>Micro Changing</em> (perubahan kecil yang tidak memengaruhi struktur utama).</p>
</li>
<li><p><strong>vX.X.X.X</strong>: menambahkan satu komponen Patch lagi, khusus untuk <em>bug fixing</em> atau perbaikan kecil.</p>
</li>
</ul>
<h3>C. Kategori Perubahan Versi</h3>
<table>
<thead>
<tr>
<th>Kategori</th>
<th>Kapan Digunakan</th>
<th>Contoh</th>
</tr>
</thead>
<tbody><tr>
<td>Major Version</td>
<td>Perubahan sangat signifikan</td>
<td>Mengubah task ML (Image Classification → Object Detection); perubahan arsitektur sistem secara menyeluruh</td>
</tr>
<tr>
<td>Minor Version</td>
<td>Perubahan cukup besar, tidak mengubah keseluruhan sistem</td>
<td>Penambahan kelas baru; perubahan dataset dalam jumlah besar; penambahan fitur utama yang berdampak pada performa</td>
</tr>
<tr>
<td>Micro Changing</td>
<td>Perubahan kecil</td>
<td>Penambahan data dalam jumlah sedikit; jumlah kelas tetap; perubahan kecil pada preprocessing/konfigurasi</td>
</tr>
<tr>
<td>Patch (Bug Fix)</td>
<td>Memperbaiki kesalahan tanpa mengubah perilaku utama</td>
<td>Memperbaiki bug; menambahkan 1–10 data yang sebelumnya salah; memperbaiki kesalahan labeling/konfigurasi</td>
</tr>
</tbody></table>
<blockquote>
<p>🤔 <strong>Coba Tebak Dulu:</strong> Kamu baru saja memperbaiki 5 data yang salah label di dataset. Itu masuk kategori versi apa — Major, Minor, Micro, atau Patch?</p>
<p>Lihat Jawaban</p>
<p><strong>Patch (Bug Fix)</strong> — memperbaiki kesalahan labeling termasuk contoh Patch, karena tidak mengubah perilaku utama sistem, hanya membenarkan kesalahan kecil.</p>
</blockquote>
<h3>D. GitLab sebagai Data Versioning</h3>
<table>
<thead>
<tr>
<th>Kelebihan</th>
<th>Kekurangan</th>
</tr>
</thead>
<tbody><tr>
<td>Gratis untuk penggunaan dasar</td>
<td>Versi gratis memiliki keterbatasan</td>
</tr>
<tr>
<td>Mudah digunakan</td>
<td>Perlu konfigurasi tambahan agar versioning dataset berjalan baik</td>
</tr>
<tr>
<td>Keamanan yang baik</td>
<td>Belum ada fitur lanjutan khusus DataOps/MLOps</td>
</tr>
<tr>
<td>Mendukung struktur subfolder</td>
<td>Kurang optimal untuk dataset yang sangat besar</td>
</tr>
<tr>
<td>Bersifat open source</td>
<td></td>
</tr>
</tbody></table>
<h3>E. Tools untuk DataOps dan MLOps</h3>
<table>
<thead>
<tr>
<th>Kategori</th>
<th>Tools</th>
</tr>
</thead>
<tbody><tr>
<td>Annotation Tools (sudah punya fitur dataset versioning)</td>
<td>CVAT (open source), Roboflow (khusus computer vision), SuperAnnotate, V7 Labs, Scale AI</td>
</tr>
<tr>
<td>Dataset Versioning &amp; Preprocessing</td>
<td>DVC – Data Version Control (open source), FiftyOne (open source)</td>
</tr>
<tr>
<td>ML Pipelining &amp; Experiment Tracking</td>
<td>Neptune.ai, Weights &amp; Biases (W&amp;B), MLflow (open source)</td>
</tr>
</tbody></table>
<hr />
<h2>4. Best Practices, Kesalahan Umum &amp; Error Handling</h2>
<h3>A. Do &amp; Don't</h3>
<table>
<thead>
<tr>
<th>❌ Jangan Lakukan Ini</th>
<th>✅ Lakukan Ini</th>
</tr>
</thead>
<tbody><tr>
<td>Normalize sebelum train-test split</td>
<td>Split data dulu, baru normalize</td>
</tr>
<tr>
<td>Drop semua rows dengan missing values tanpa analisis</td>
<td>Analisis pattern missing values sebelum mengambil tindakan</td>
</tr>
<tr>
<td>Lupa reset index setelah drop rows</td>
<td>Selalu <code>reset_index(drop=True)</code> setelah cleaning</td>
</tr>
<tr>
<td>Normalize target variable (y) untuk regression</td>
<td>Keep target variable dalam skala original</td>
</tr>
<tr>
<td>Menggunakan test data saat fit scaler</td>
<td>Fit scaler hanya pada training data</td>
</tr>
<tr>
<td>Tidak cek data quality setelah transformasi</td>
<td>Validate hasil dengan visualisasi dan statistik</td>
</tr>
</tbody></table>
<h3>B. Trust But Verify</h3>
<table>
<thead>
<tr>
<th>Kesalahan Umum</th>
<th>Solusi</th>
</tr>
</thead>
<tbody><tr>
<td>Tidak cek tipe data sebelum operasi numerik</td>
<td>Selalu verifikasi dengan <code>df.dtypes</code> sebelum menghitung mean atau sum</td>
</tr>
<tr>
<td>Lupa handling null values sebelum konversi tipe</td>
<td>Handle dulu dengan <code>dropna()</code> atau <code>fillna()</code> sebelum mengubah tipe data</td>
</tr>
<tr>
<td>Overwrite dataset original</td>
<td>Selalu simpan dataset asli, buat copy untuk cleaning: <code>df_clean = df.copy()</code></td>
</tr>
<tr>
<td>Tidak verifikasi hasil cleaning</td>
<td>Setelah cleaning, jalankan <code>df.info()</code> dan <code>df.head()</code> untuk memastikan hasilnya benar</td>
</tr>
<tr>
<td>Normalisasi tanpa perlu</td>
<td>Tidak semua model butuh normalisasi — pahami konteks dan algoritma yang dipakai</td>
</tr>
</tbody></table>
<blockquote>
<p>Prinsip: <strong>"Trust but verify"</strong> — selalu double-check hasil setiap langkah cleaning.</p>
</blockquote>
<h3>C. Error Handling</h3>
<table>
<thead>
<tr>
<th>Error</th>
<th>Penyebab</th>
<th>Solusi</th>
</tr>
</thead>
<tbody><tr>
<td><code>KeyError</code></td>
<td>Nama kolom salah atau tidak ada</td>
<td>Cek nama kolom dengan <code>df.columns</code></td>
</tr>
<tr>
<td><code>ValueError</code></td>
<td>Konversi tipe data gagal</td>
<td>Gunakan parameter <code>errors='coerce'</code> pada <code>pd.to_numeric()</code></td>
</tr>
<tr>
<td><code>FileNotFoundError</code></td>
<td>File path salah atau file tidak ada</td>
<td>Pastikan file sudah di-<em>upload</em> ke environment kerja (mis. Google Colab)</td>
</tr>
<tr>
<td><code>AttributeError</code></td>
<td>Method tidak tersedia untuk tipe data tertentu</td>
<td>Cek tipe data dengan <code>type()</code> atau <code>df.dtypes</code></td>
</tr>
</tbody></table>
<blockquote>
<p>Catatan: error adalah momen belajar, bukan kegagalan.</p>
</blockquote>
<hr />
<h2>5. Before vs After &amp; Data Pipeline Flow</h2>
<h3>A. Before vs After: From Chaos to Clarity</h3>
<table>
<thead>
<tr>
<th>Dataset Mentah (Raw)</th>
<th>Dataset Bersih (Clean)</th>
</tr>
</thead>
<tbody><tr>
<td>Missing values di berbagai kolom</td>
<td>Tidak ada missing values</td>
</tr>
<tr>
<td>Data duplikat yang menipu</td>
<td>Setiap baris unik</td>
</tr>
<tr>
<td>Tipe data tidak konsisten</td>
<td>Tipe data sesuai konteks</td>
</tr>
<tr>
<td>Nama kolom ambiguous</td>
<td>Nama kolom jelas dan konsisten</td>
</tr>
<tr>
<td>Nilai outlier ekstrem</td>
<td>Nilai dalam rentang wajar</td>
</tr>
<tr>
<td>Format tidak standar</td>
<td>Format terstandarisasi</td>
</tr>
</tbody></table>
<ul>
<li><p><strong>Dampak dataset mentah</strong>: AI akan bingung, error saat training, dan prediksi tidak akurat.</p>
</li>
<li><p><strong>Dampak dataset bersih</strong>: AI bisa belajar dengan baik, training lancar, dan prediksi akurat.</p>
</li>
</ul>
<h3>B. Data Pipeline Flow (End-to-End)</h3>
<ol>
<li><p><strong>Data Collection</strong> — mengumpulkan data dari berbagai sumber (database, API, file).</p>
</li>
<li><p><strong>Data Loading</strong> — membaca data ke dalam environment Python.</p>
</li>
<li><p><strong>Data Exploration</strong> — memahami struktur, distribusi, dan masalah dalam data.</p>
</li>
<li><p><strong>Data Cleaning</strong> — menangani missing values, duplikat, dan error.</p>
</li>
<li><p><strong>Data Transformation</strong> — sorting, filtering, normalisasi, feature engineering.</p>
</li>
<li><p><strong>Data Storage</strong> — menyimpan dataset bersih untuk tahap selanjutnya (training model).</p>
</li>
</ol>
<hr />
<h2>6. Quiz Check — Part 2</h2>
<p><strong>Q1. Kamu punya dataset dengan outlier ekstrem di kolom gaji. Metode scaling mana yang paling tahan terhadap outlier: Min-Max, Standardization, atau Robust Scaling?</strong></p>
<p><strong>Jawaban: Robust Scaling.</strong> Metode ini memakai median dan IQR (bukan mean/min/max), sehingga jauh lebih tahan terhadap outlier dibanding Min-Max Scaling maupun Standardization.</p>
<p><strong>Q2. Kenapa normalisasi HARUS dilakukan setelah train-test split, bukan sebelumnya?</strong></p>
<p><strong>Jawaban:</strong> Karena scaler (misalnya <code>MinMaxScaler</code>) harus di-<em>fit</em> hanya pada training data. Kalau di-fit sebelum split (pada seluruh data termasuk test set), informasi dari test set "bocor" ke proses training — ini disebut <em>data leakage</em> dan membuat evaluasi model jadi tidak jujur.</p>
<p><strong>Q3. Kode kamu error dengan pesan `KeyError` saat mencoba akses `df['harga']`. Apa penyebab paling mungkin dan solusinya?</strong></p>
<p><strong>Jawaban:</strong> Penyebab paling mungkin adalah <strong>nama kolom salah atau tidak ada</strong> — mungkin sebenarnya nama kolomnya <code>price</code> atau <code>Harga</code> (kapital berbeda). Solusi: cek nama kolom persis dengan <code>df.columns</code>.</p>
<p><strong>Q4. Kamu baru saja menambahkan satu kelas objek baru ke dataset object detection, plus menambah ribuan gambar baru. Ini termasuk perubahan versi apa?</strong></p>
<p><strong>Jawaban: Minor Version.</strong> Penambahan kelas baru dan perubahan dataset dalam jumlah besar termasuk kategori Minor Version — perubahan cukup besar tapi belum mengubah keseluruhan sistem/task ML-nya.</p>
<hr />
<h2>7. Cheat Sheet Checklist Preprocessing</h2>
<p>Simpan checklist ini sebagai referensi cepat sebelum data kamu masuk ke tahap training:</p>
<p><strong>Eksplorasi</strong></p>
<ul>
<li><p>[ ] Sudah jalankan <code>df.head()</code>, <code>df.info()</code>, <code>df.describe()</code>, <code>df.shape</code>?</p>
</li>
<li><p>[ ] Sudah tahu tipe data tiap kolom (<code>df.dtypes</code>)?</p>
</li>
</ul>
<p><strong>Cleaning</strong></p>
<ul>
<li><p>[ ] Missing values sudah dicek (<code>df.isnull().sum()</code>) dan ditangani sesuai persentasenya?</p>
</li>
<li><p>[ ] Duplikat sudah dicek (<code>df.duplicated().sum()</code>) dan dihapus?</p>
</li>
<li><p>[ ] Outlier sudah dideteksi (Boxplot/IQR/Z-Score) dan diputuskan: hapus atau treatment?</p>
</li>
<li><p>[ ] Tipe data sudah dikonversi dengan benar (<code>astype</code>, <code>to_numeric</code>, <code>to_datetime</code>)?</p>
</li>
<li><p>[ ] Teks sudah dirapikan (strip, lowercase, replace) dan konsisten formatnya?</p>
</li>
<li><p>[ ] Nama kolom sudah <code>snake_case</code> dan konsisten?</p>
</li>
<li><p>[ ] Index sudah di-reset setelah drop rows (<code>reset_index(drop=True)</code>)?</p>
</li>
</ul>
<p><strong>Transformation</strong></p>
<ul>
<li><p>[ ] Data kategorikal sudah di-encode sesuai jenisnya (nominal → One-Hot, ordinal → numerik langsung)?</p>
</li>
<li><p>[ ] Dimensionality reduction dipertimbangkan jika fitur terlalu banyak?</p>
</li>
<li><p>[ ] Scaler di-<em>fit</em> hanya pada training data, <strong>setelah</strong> train-test split?</p>
</li>
<li><p>[ ] Target variable (y) <strong>tidak</strong> dinormalisasi untuk kasus regression?</p>
</li>
</ul>
<p><strong>Penyimpanan &amp; Versioning</strong></p>
<ul>
<li><p>[ ] Dataset asli disimpan terpisah dari dataset bersih (nama file berbeda)?</p>
</li>
<li><p>[ ] Dataset bersih sudah diberi versi (Major/Minor/Micro/Patch) sesuai jenis perubahan?</p>
</li>
<li><p>[ ] Dataset sudah disimpan di tempat yang mendukung versioning (S3/GCS/DVC/dll)?</p>
</li>
</ul>
<hr />
<p><em>Lihat</em> <a href="https://shaka-ai.hashnode.dev/data-handling-preprocessing-part-1"><em>Part 1</em></a> <em>untuk pembahasan lengkap tentang Data Collection, Storage, dan Data Cleaning.</em></p>
]]></content:encoded></item><item><title><![CDATA[Data Handling & Preprocessing (Part 1): Fondasi, Collection, dan Data Cleaning]]></title><description><![CDATA[Bagian 1 dari 2: kenapa preprocessing penting, cara data dikumpulkan & disimpan, pengenalan Pandas, eksplorasi data, dan proses cleaning secara mendalam.

Sebelum model AI belajar apa pun, ia harus "m]]></description><link>https://shaka-ai.hashnode.dev/data-handling-preprocessing-part-1-id</link><guid isPermaLink="true">https://shaka-ai.hashnode.dev/data-handling-preprocessing-part-1-id</guid><category><![CDATA[Data Science]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Python]]></category><category><![CDATA[pandas]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[data analysis]]></category><dc:creator><![CDATA[Muhammad Ariel Shakaramiro]]></dc:creator><pubDate>Sun, 30 Aug 2026 10:23:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a8ef2e3923670c989379174/276d72c7-0418-4700-9e08-e2a00094eb96.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><em>Bagian 1 dari 2: kenapa preprocessing penting, cara data dikumpulkan &amp; disimpan, pengenalan Pandas, eksplorasi data, dan proses cleaning secara mendalam.</em></p>
</blockquote>
<p>Sebelum model AI belajar apa pun, ia harus "makan" data dulu. Masalahnya, data mentah hampir selalu berantakan — ada yang kosong, ada yang duplikat, ada yang formatnya beda-beda. Bagian 1 catatan ini membahas fondasi data handling sampai tuntas proses cleaning-nya. Bagian 2 akan lanjut ke transformation, versioning, dan best practices.</p>
<h2>Daftar Isi</h2>
<ul>
<li><p><a href="#1-kenapa-data-preprocessing-itu-penting">1. Kenapa Data Preprocessing Itu Penting?</a></p>
</li>
<li><p><a href="#2-data-collection--storage">2. Data Collection &amp; Storage</a></p>
</li>
<li><p><a href="#3-pandas-tools-utama-data-handling">3. Pandas: Tools Utama Data Handling</a></p>
</li>
<li><p><a href="#4-eksplorasi-data-sebelum-cleaning">4. Eksplorasi Data Sebelum Cleaning</a></p>
</li>
<li><p><a href="#5-data-cleaning-deep-dive">5. Data Cleaning (Deep Dive)</a></p>
</li>
<li><p><a href="#6-quiz-check--part-1">6. Quiz Check — Part 1</a></p>
</li>
</ul>
<hr />
<h2>1. Kenapa Data Preprocessing Itu Penting?</h2>
<p>Model AI paling <em>sophisticated</em> sekalipun tidak akan menghasilkan prediksi akurat kalau dilatih dengan data yang buruk. Preprocessing bukan cuma langkah teknis — ini fondasi kesuksesan proyek AI.</p>
<p><strong>Prinsip emas:</strong> <em>Garbage In, Garbage Out</em> (GIGO) — kalau datanya kotor, output AI-nya akan salah terus.</p>
<blockquote>
<p>🤔 <strong>Coba Tebak Dulu:</strong> Menurutmu, dari total waktu sebuah proyek data science, berapa persen yang dihabiskan untuk <em>preprocessing</em> data dibanding membangun model?</p>
<p>Lihat Jawaban</p>
<p><strong>80% waktu</strong> proyek dihabiskan untuk mempersiapkan &amp; membersihkan data, hanya <strong>20%</strong> untuk membangun &amp; melatih model. Ini formula kesuksesan AI yang sering diremehkan pemula.</p>
</blockquote>
<table>
<thead>
<tr>
<th>Statistik</th>
<th>Makna</th>
</tr>
</thead>
<tbody><tr>
<td>80%</td>
<td>Dari total waktu proyek data science dihabiskan untuk preprocessing</td>
</tr>
<tr>
<td>70%</td>
<td>Peningkatan akurasi yang bisa dicapai dengan data cleaning yang baik</td>
</tr>
<tr>
<td>90%</td>
<td>Success rate proyek AI yang menerapkan preprocessing secara proper</td>
</tr>
</tbody></table>
<blockquote>
<p><em>Catatan: angka-angka di atas (termasuk statistik "80% unstructured data" di bagian berikutnya) adalah figur yang umum dikutip di materi data science, bukan dari satu studi akademis tunggal yang bisa dirujuk langsung. Baca sebagai ilustrasi skala/urgensi, bukan statistik presisi.</em></p>
</blockquote>
<h3>Contoh Nyata: Ketika Data Jelek Merusak AI</h3>
<p><strong>Kasus 1 — Chatbot Gagal</strong></p>
<p>Chatbot customer service dilatih dengan data percakapan penuh typo &amp; bahasa tidak baku → bot tidak memahami pertanyaan pelanggan, jawaban jadi tidak relevan.</p>
<p><strong>Kasus 2 — Prediksi Harga Salah</strong></p>
<p>Model prediksi harga properti dilatih dengan data banyak nilai kosong &amp; duplikat → rumah senilai Rp 500 juta diprediksi Rp 2 miliar.</p>
<h3>Definisi Kunci</h3>
<ul>
<li><p><strong>Data Preprocessing</strong>: proses menyiapkan data sebaik dan sebagus mungkin agar siap digunakan oleh AI.</p>
</li>
<li><p><strong>Data Cleaning</strong>: bagian dari Data Preprocessing yang bertugas membersihkan data (missing values, duplikat, format tidak konsisten, tipe data salah, dll).</p>
</li>
</ul>
<h3>Alur Kerja Data Preprocessing</h3>
<ol>
<li><p><strong>Data Mentah</strong> — dataset kotor dengan berbagai masalah dan inkonsistensi.</p>
</li>
<li><p><strong>Pembersihan</strong> — hapus null, perbaiki format, handle outlier.</p>
</li>
<li><p><strong>Transformasi</strong> — normalisasi, encoding, feature engineering.</p>
</li>
<li><p><strong>Model Ready</strong> — data siap dipakai untuk training AI.</p>
</li>
</ol>
<hr />
<h2>2. Data Collection &amp; Storage</h2>
<h3>A. Data Collection</h3>
<p>Data Collection adalah skema untuk mengumpulkan data untuk keperluan pembuatan AI. Ada dua jenis besar:</p>
<p><strong>1. Unstructured Data</strong></p>
<ul>
<li><p>Karakteristik: manusia dapat memahami maksud data (contoh: mengenali jenis hewan) dan punya kemampuan <em>reasoning</em> terhadap data tersebut (contoh: memahami maksud sebuah paragraf).</p>
</li>
<li><p>Sumber data: boleh pakai <em>generated data</em>, tapi lebih baik pakai <em>real-world data</em> yang sesuai masalah — pastikan kondisi data sesuai kondisi di lapangan.</p>
</li>
<li><p>Contoh: teks, gambar, video.</p>
</li>
<li><p>Fakta: <strong>sekitar 80% data di dunia</strong> merupakan unstructured data.</p>
</li>
</ul>
<p><strong>2. Structured Data</strong></p>
<ul>
<li><p>Karakteristik: manusia awam tidak punya kemampuan reasoning terhadap maksud data; bersifat unik pada tiap kasus — kasus yang serupa bisa memakai data berbeda (contoh: optimasi <em>inventory</em> berbeda tergantung <em>business objective</em>).</p>
</li>
<li><p>Contoh: data tabular, CSV, kolom dan baris (row).</p>
</li>
<li><p><strong>Catatan penting</strong>: jika sistem <em>end-to-end</em> belum tersedia, jangan langsung menggunakan AI.</p>
<ul>
<li><p>Bangun <strong>Data Pipeline</strong> terlebih dahulu agar terbentuk alur data end-to-end (libatkan Data Engineer jika perlu).</p>
</li>
<li><p>AI sebaiknya digunakan sebagai <em>tooling</em>, bukan sebagai senjata utama.</p>
</li>
</ul>
</li>
</ul>
<h3>B. Data Storage</h3>
<table>
<thead>
<tr>
<th>Jenis Data</th>
<th>Media Penyimpanan</th>
<th>Contoh</th>
</tr>
</thead>
<tbody><tr>
<td>Unstructured Data</td>
<td>File Storage System — mendukung versioning, mudah diakses, aman</td>
<td>Amazon S3, Google Cloud Storage (GCS)</td>
</tr>
<tr>
<td>Structured Data</td>
<td>Database (SQL/NoSQL) — sebaiknya ada data terpisah khusus keperluan ML dengan prinsip sama: mudah disimpan, versioning, mudah diakses, aman</td>
<td>PostgreSQL, MariaDB</td>
</tr>
<tr>
<td>Object Storage</td>
<td>Media untuk data berbentuk objek, umumnya file besar; mendukung skalabilitas, versioning, keamanan yang baik</td>
<td>Gambar, video, dokumen, dataset ML</td>
</tr>
</tbody></table>
<hr />
<h2>3. Pandas: Tools Utama Data Handling</h2>
<p>Pandas adalah salah satu library Python paling populer untuk manipulasi dan analisis data — ibarat Excel, tapi dengan kekuatan super dan fleksibilitas coding.</p>
<ul>
<li><p><strong>Cepat</strong> — memproses jutaan baris dalam hitungan detik.</p>
</li>
<li><p><strong>Powerful</strong> — operasi kompleks dengan kode singkat.</p>
</li>
<li><p><strong>Fleksibel</strong> — bisa menangani berbagai format data (CSV, Excel, JSON, dll).</p>
</li>
</ul>
<h3>Ekosistem Tools Data Lain</h3>
<p>Dunia data umumnya hanya menggunakan Python dan SQL. Selain Pandas, ada beberapa library lain:</p>
<ul>
<li><p><strong>PySpark</strong> — dipakai jika data disimpan di Apache Spark.</p>
</li>
<li><p><strong>Polars</strong> — ditulis ulang memakai Rust sehingga lebih cepat.</p>
</li>
<li><p><strong>DuckDB</strong> — untuk <em>online analytical processing</em> (OLAP) database.</p>
</li>
</ul>
<blockquote>
<p>Catatan: kalau data masih berupa CSV, Pandas saja sudah cukup.</p>
</blockquote>
<h3>Format File yang Didukung Pandas</h3>
<table>
<thead>
<tr>
<th>Format</th>
<th>Fungsi Pandas</th>
<th>Catatan</th>
</tr>
</thead>
<tbody><tr>
<td>CSV</td>
<td><code>pd.read_csv()</code></td>
<td>Salah satu format paling umum &amp; ringan untuk data tabular; TSV mirip, beda pada delimiter</td>
</tr>
<tr>
<td>Excel (XLSX/XLS)</td>
<td><code>pd.read_excel()</code></td>
<td>File spreadsheet</td>
</tr>
<tr>
<td>JSON</td>
<td><code>pd.read_json()</code></td>
<td>Format data web; untuk dataset unstructured yang butuh labeling</td>
</tr>
<tr>
<td>SQL Database</td>
<td><code>pd.read_sql()</code></td>
<td>Query langsung dari database</td>
</tr>
</tbody></table>
<h3>Load Dataset</h3>
<p><code>pd</code> adalah alias dari Pandas, sehingga penulisan kode lebih singkat. Pandas bisa membaca hampir semua format data yang umum dipakai dalam data science.</p>
<pre><code class="language-python">import pandas as pd

df = pd.read_csv('retail_data_raw.csv')
print(f"Jumlah baris: {len(df)}")
</code></pre>
<p><strong>Tips praktis:</strong></p>
<ul>
<li><p>Selalu cek <em>encoding</em> jika ada karakter aneh.</p>
</li>
<li><p>Gunakan parameter <code>sep</code> untuk delimiter custom.</p>
</li>
<li><p>Parameter <code>nrows</code> untuk preview data besar.</p>
</li>
<li><p>Perhatikan delimiter dan <em>decimal separator</em>.</p>
</li>
</ul>
<blockquote>
<p>Catatan: variabel <code>df</code> adalah singkatan dari "DataFrame", struktur data utama Pandas berbentuk tabel dengan baris &amp; kolom.</p>
</blockquote>
<h3>Struktur Data: Series vs DataFrame</h3>
<ul>
<li><p><strong>Series</strong>: array 1 dimensi dengan index. Contoh: <code>pd.Series([1, 2, 3, 4])</code></p>
</li>
<li><p><strong>DataFrame</strong>: tabel 2 dimensi dengan baris dan kolom — struktur yang paling sering dipakai.</p>
</li>
</ul>
<hr />
<h2>4. Eksplorasi Data Sebelum Cleaning</h2>
<p>Prinsip <strong>"Understand before fix"</strong>: seorang AI engineer harus memahami "wajah" datanya — struktur kolom, jenis nilai, dan pola umum — sebelum mulai membersihkannya.</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Fungsi</th>
</tr>
</thead>
<tbody><tr>
<td><code>df.head()</code> / <code>df.head(10)</code></td>
<td>Preview baris pertama dataset (default 5 baris)</td>
</tr>
<tr>
<td><code>df.tail()</code></td>
<td>Preview baris terakhir; kadang ada pola berbeda di bagian akhir</td>
</tr>
<tr>
<td><code>df.info()</code></td>
<td>Jumlah baris &amp; kolom, nama kolom, tipe data, jumlah non-null, memory usage</td>
</tr>
<tr>
<td><code>df.describe()</code></td>
<td>Statistik deskriptif kolom numerik: mean, median, min, max, standard deviation</td>
</tr>
<tr>
<td><code>df.shape</code></td>
<td>Ukuran dataset dalam format <code>(rows, columns)</code></td>
</tr>
<tr>
<td><code>df.columns</code></td>
<td>Daftar seluruh nama kolom dalam dataset</td>
</tr>
</tbody></table>
<hr />
<h2>5. Data Cleaning (Deep Dive)</h2>
<h3>5.1 Masalah Umum dalam Dataset</h3>
<table>
<thead>
<tr>
<th>Masalah</th>
<th>Deskripsi</th>
<th>Contoh / Dampak</th>
</tr>
</thead>
<tbody><tr>
<td>Missing Values (Null)</td>
<td>Data yang hilang atau tidak lengkap</td>
<td>Bisa bikin model error atau hasil prediksi jadi bias</td>
</tr>
<tr>
<td>Outliers</td>
<td>Nilai yang terlalu ekstrem / tidak masuk akal</td>
<td>Umur 250 tahun, gaji negatif</td>
</tr>
<tr>
<td>Duplikat</td>
<td>Baris yang sama muncul berkali-kali</td>
<td>Bisa membuat model overfitting ke data tertentu</td>
</tr>
<tr>
<td>Format Tidak Konsisten</td>
<td>Tanggal berbagai format, spasi berlebih, kapitalisasi acak</td>
<td>"Jakarta" vs "jakarta" vs "JAKARTA "</td>
</tr>
<tr>
<td>Tipe Data Salah</td>
<td>Angka tersimpan sebagai teks, atau kategori sebagai angka</td>
<td>Kolom harga bertipe object, tidak bisa dihitung</td>
</tr>
</tbody></table>
<h3>5.2 Tipe Data &amp; Konversi</h3>
<p>Salah satu masalah klasik data cleaning adalah tipe data yang salah — misalnya angka tersimpan sebagai teks, atau tanggal tersimpan sebagai string. Tipe data penting ditransformasi tanpa mengubah makna aslinya (data numerik bersifat ratio/interval, beda penanganan dengan data kategorikal).</p>
<table>
<thead>
<tr>
<th>Tipe Data</th>
<th>Keterangan</th>
</tr>
</thead>
<tbody><tr>
<td><code>int64</code></td>
<td>Bilangan bulat</td>
</tr>
<tr>
<td><code>float64</code></td>
<td>Bilangan desimal</td>
</tr>
<tr>
<td><code>object</code></td>
<td>Teks / String</td>
</tr>
<tr>
<td><code>datetime64</code></td>
<td>Tanggal dan waktu</td>
</tr>
<tr>
<td><code>bool</code></td>
<td>True / False</td>
</tr>
</tbody></table>
<blockquote>
<p>Catatan: kalau kolom harga tersimpan sebagai <code>object</code> (teks), kamu tidak bisa menghitung rata-rata atau menjumlahkannya — AI juga akan bingung.</p>
</blockquote>
<pre><code class="language-python"># Konversi ke numerik
df['quantity'] = df['quantity'].astype(int)
df['price'] = pd.to_numeric(df['price'], errors='coerce')

# Konversi ke datetime
df['date'] = pd.to_datetime(df['date'], format='%d/%m/%Y')

# Konversi ke string
df['customer_id'] = df['customer_id'].astype(str)
</code></pre>
<blockquote>
<p>Catatan: parameter <code>errors='coerce'</code> akan mengubah nilai yang gagal dikonversi menjadi <code>NaN</code>, sehingga kode tidak error.</p>
</blockquote>
<h3>5.3 Membersihkan Data Teks (Tabular)</h3>
<p>Operasi string umum untuk merapikan kolom bertipe teks:</p>
<pre><code class="language-python">df['nama'] = df['nama'].str.strip()              # hapus whitespace
df['kota'] = df['kota'].str.lower()               # lowercase semua
df['telp'] = df['telp'].str.replace('-', '')      # replace karakter
df['nomor'] = df['text'].str.extract(r'(\d+)')    # extract angka saja
</code></pre>
<p><strong>Tips text cleaning:</strong></p>
<ul>
<li><p>Selalu standardisasi <em>case</em> (upper/lower).</p>
</li>
<li><p>Hapus <em>whitespace</em> di awal &amp; akhir.</p>
</li>
<li><p>Hapus karakter spesial jika tidak perlu.</p>
</li>
<li><p>Cek typo dengan <code>value_counts()</code>.</p>
</li>
<li><p>Konsisten dengan format ("Jakarta" vs "jakarta").</p>
</li>
</ul>
<blockquote>
<p>Catatan: ini adalah pembersihan teks pada kolom tabel (<em>general string cleanup</em>). Berbeda dengan persiapan teks untuk model NLP (<em>text vectorization</em>) di bawah ini, yang butuh langkah tambahan.</p>
</blockquote>
<h3>5.4 Data Cleaning Khusus NLP</h3>
<p>Sebelum teks divektorisasi untuk model NLP, ada kata-kata yang dianggap kurang memberi informasi penting dan perlu dihilangkan:</p>
<ul>
<li><p><strong>Link atau URL</strong></p>
</li>
<li><p><strong>Tag</strong> (mention atau hashtag)</p>
</li>
<li><p><strong>Stopwords</strong>, seperti: <em>the, a, an, or, for</em>, dan kata umum lainnya</p>
</li>
</ul>
<p><strong>Alasan:</strong> link/tag tidak relevan untuk pembelajaran model; stopwords sering muncul berulang, memperberat komputasi, dan tidak membedakan satu dokumen dengan lainnya.</p>
<h3>5.5 Missing Values</h3>
<p>Missing values adalah salah satu masalah paling umum dalam dataset real-world — bisa terjadi karena error input, sensor rusak, atau data memang tidak ada.</p>
<p><strong>Kenapa berbahaya:</strong></p>
<ul>
<li><p>AI tidak bisa belajar dari data yang kosong.</p>
</li>
<li><p>Bisa menyebabkan error saat training model.</p>
</li>
<li><p>Mengurangi akurasi prediksi.</p>
</li>
</ul>
<p><strong>Deteksi:</strong></p>
<ul>
<li><p><code>df.isnull().sum()</code> → jumlah null per kolom.</p>
</li>
<li><p><code>df.isnull().mean() * 100</code> → persentase missing values.</p>
</li>
</ul>
<p><strong>Strategi menangani missing values:</strong></p>
<ol>
<li><p><strong>Hapus baris</strong> (<code>df.dropna()</code>) — jika missing values sedikit (&lt;5%) dan data masih banyak.</p>
</li>
<li><p><strong>Hapus kolom</strong> (<code>df.dropna(axis=1)</code>) — jika satu kolom punya terlalu banyak missing values (&gt;50%).</p>
</li>
<li><p><strong>Isi dengan nilai tertentu</strong> (<code>df.fillna(...)</code>) — gunakan 0, mean, median, atau mode tergantung konteks; dipakai jika data hilang cukup banyak tapi kolomnya penting.</p>
</li>
<li><p><strong>Forward fill / backward fill</strong> — untuk data time series, isi dengan nilai sebelum/sesudahnya (<code>method='ffill'</code>).</p>
</li>
</ol>
<pre><code class="language-python"># 1. Hapus baris dengan null
df_clean = df.dropna()

# 2. Hapus kolom dengan &gt;50% null
threshold = len(df) * 0.5
df_clean = df.dropna(axis=1, thresh=threshold)

# 3. Isi dengan nilai tertentu / mean
df['age'].fillna(0, inplace=True)
df['price'].fillna(df['price'].mean(), inplace=True)

# 4. Forward fill (time series)
df.fillna(method='ffill', inplace=True)
</code></pre>
<blockquote>
<p>Prinsip: <strong>"No blanks for brains"</strong> — AI butuh data lengkap untuk belajar dengan baik.</p>
</blockquote>
<h3>5.6 Data Duplikat</h3>
<p>Data duplikat terjadi ketika satu observasi muncul lebih dari sekali dalam dataset — biasanya akibat error input atau merge dataset yang tidak hati-hati.</p>
<p><strong>Bahaya duplikat:</strong> model AI bisa overfitting ke data tertentu, bias dalam prediksi, training lebih lama, dan metrics evaluasi jadi tidak akurat.</p>
<pre><code class="language-python"># Deteksi duplikat
df.duplicated().sum()
df[df.duplicated()]

# Hapus duplikat
df_clean = df.drop_duplicates()
df_clean = df.drop_duplicates(subset=['customer_id', 'date'])
df_clean = df.drop_duplicates(keep='first')  # atau 'last'
</code></pre>
<blockquote>
<p><strong>Best practice</strong>: selalu simpan dataset asli terpisah dari dataset yang sudah dibersihkan (nama file berbeda) — jangan <em>overwrite</em> data original.</p>
<p>Prinsip: <strong>"One truth per row"</strong> — setiap baris harus merepresentasikan satu observasi unik (<em>unique and clean</em>).</p>
</blockquote>
<h3>5.7 Outlier</h3>
<p><strong>Definisi:</strong> data yang bersifat anomali / nilainya jauh berbeda dari sebagian besar data lain. Contoh: gaji karyawan umumnya 5–15 juta, tapi ada yang 500 juta — itu outlier.</p>
<p><strong>Deteksi sederhana:</strong> Boxplot, scatter plot, IQR (Interquartile Range) method, Z-Score method, atau domain knowledge.</p>
<p><strong>Kapan outlier dihapus:</strong> ketika kemungkinan kemunculannya sangat kecil, atau ketika keberadaannya tidak berdampak ke proses bisnis. <strong>Jika outlier mewakili kondisi bisnis nyata, jangan langsung dihapus</strong> — perlu di-<em>treatment</em>.</p>
<p><strong>Teknik Advanced: Deteksi Outlier dengan Variational Autoencoder (VAE)</strong></p>
<p><strong>Apa itu Autoencoder?</strong></p>
<p>Neural network yang dilatih dengan input dan output yang sama, sehingga belajar merekonstruksi data dan memahami distribusi data bersih (normal).</p>
<p><strong>Cara kerja untuk deteksi outlier:</strong></p>
<ul>
<li><p>VAE membandingkan hasil rekonstruksi dengan input asli.</p>
</li>
<li><p>Makin besar perbedaannya (diukur dengan <em>Mean Squared Error</em> / MSE), makin besar kemungkinan data tersebut outlier.</p>
</li>
<li><p>Data di luar distribusi yang dipelajari → hasil rekonstruksi berbeda jauh → nilai MSE tinggi → indikator data outlier.</p>
</li>
</ul>
<p>Metode lain: <strong>Z-Score</strong>, <strong>Interquartile Range (IQR)</strong>, <strong>Isolation Forest</strong>.</p>
<p><strong>Implementasi Praktis — Deteksi &amp; Hapus Outlier dengan IQR:</strong></p>
<pre><code class="language-python">Q1 = df['price'].quantile(0.25)
Q3 = df['price'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

df_clean = df[(df['price'] &gt;= lower_bound) &amp; (df['price'] &lt;= upper_bound)]
</code></pre>
<blockquote>
<p>Catatan: IQR method adalah salah satu cara paling umum digunakan untuk deteksi outlier — tapi tetap perlu divalidasi dengan konteks bisnis (lihat poin "Kapan outlier dihapus" di atas). Angka <strong>1.5</strong> adalah konstanta standar, tapi bisa disesuaikan kebutuhan.</p>
</blockquote>
<h3>5.8 Data yang Membingungkan Model</h3>
<p>Selain outlier numerik, ada kategori "data kotor" yang lebih halus: data yang secara aktif membingungkan proses belajar model.</p>
<ul>
<li><p><strong>Tujuan cleaning di sini:</strong> menghilangkan data yang jarang muncul dan tidak merepresentasikan kondisi sebenarnya.</p>
</li>
<li><p><strong>Adversarial attack</strong>: data yang <strong>sengaja dimodifikasi</strong> agar model AI menghasilkan prediksi salah.</p>
</li>
<li><p><strong>Catatan penting:</strong> menghapus sebagian kecil objek dalam sebuah gambar masih bisa diterima jika objek tersebut justru menurunkan kualitas data latih — tujuannya menjaga model belajar dari pola yang benar.</p>
</li>
</ul>
<blockquote>
<p>🤔 <strong>Coba Tebak Dulu:</strong> Kalau ada 1 objek kecil di pojok foto yang tidak relevan dan malah bikin model salah belajar, apakah boleh dihapus dari gambar training?</p>
<p>Lihat Jawaban</p>
<p><strong>Boleh.</strong> Selama tujuannya menjaga model belajar dari pola yang benar dan objek tersebut memang menurunkan kualitas data latih, penghapusan sebagian kecil objek masih bisa diterima.</p>
</blockquote>
<h3>5.9 Rename Kolom</h3>
<p><strong>Kenapa penting:</strong> nama kolom sering tidak konsisten, spasi &amp; karakter spesial bikin ribet, nama yang <em>descriptive</em> lebih mudah dipahami, dan menstandarkan <em>naming convention</em>.</p>
<p><strong>Best practice:</strong> gunakan <code>snake_case</code> (huruf kecil, underscore untuk spasi), nama jelas dan singkat, hindari karakter spesial.</p>
<pre><code class="language-python"># Rename kolom tertentu
df.rename(columns={'Nama Lengkap': 'nama', 'Umur (tahun)': 'usia'}, inplace=True)

# Rename semua kolom sekaligus
df.columns = ['nama', 'usia', 'gaji']

# Lowercase &amp; hilangkan spasi
df.columns = df.columns.str.lower()
df.columns = df.columns.str.replace(' ', '_')
</code></pre>
<hr />
<h2>6. Quiz Check — Part 1</h2>
<p><strong>Q1. Kenapa data tipe kategorikal seperti "warna rambut" sebaiknya di-encode pakai One-Hot Encoding, bukan diubah jadi angka urut biasa (0, 1, 2, ...)?</strong></p>
<p><strong>Jawaban:</strong> Karena "warna rambut" adalah data <strong>nominal</strong> — tidak punya urutan/tingkatan. Jika diubah jadi angka urut biasa, model bisa salah mengira ada hubungan tingkatan antar kategori (misalnya angka 2 dianggap "lebih besar" dari 1), padahal tidak ada makna urutan di sana. <em>(Detail lengkap dibahas di Part 2.)</em></p>
<p><strong>Q2. Kapan sebaiknya kamu menghapus SATU KOLOM penuh, bukan cuma baris yang ada missing value-nya?</strong></p>
<p><strong>Jawaban:</strong> Ketika kolom tersebut punya <strong>lebih dari 50%</strong> nilai missing. Kalau missing values-nya sedikit (di bawah 5%) dan datanya masih banyak, cukup hapus barisnya saja dengan <code>df.dropna()</code>.</p>
<p><strong>Q3. Outlier gaji karyawan sebesar Rp 500 juta ditemukan di dataset HR. Apakah otomatis harus dihapus?</strong></p>
<p><strong>Jawaban:</strong> <strong>Tidak otomatis.</strong> Perlu dicek dulu apakah nilai itu representasi kondisi bisnis nyata (misalnya memang gaji direktur) atau murni error input. Kalau mewakili kondisi bisnis nyata, jangan langsung dihapus — perlu di-<em>treatment</em> terlebih dahulu.</p>
<p><strong>Q4. Apa bedanya "data cleaning teks pada kolom tabel" dengan "data cleaning teks untuk NLP"?</strong></p>
<p><strong>Jawaban:</strong> Cleaning teks pada kolom tabel fokus ke <em>general string cleanup</em> (strip whitespace, lowercase, replace karakter). Cleaning teks untuk NLP butuh langkah tambahan yang lebih spesifik: menghapus <strong>link/URL</strong>, <strong>tag</strong> (mention/hashtag), dan <strong>stopwords</strong> — karena elemen ini tidak informatif dan memperberat komputasi model bahasa.</p>
<hr />
<p><strong>Lanjut ke</strong> <a href="https://shaka-ai.hashnode.dev/data-handling-preprocessing-part-2"><strong>Part 2</strong></a> — kita akan bahas Data Transformation lengkap (encoding, dimensionality reduction, normalisasi, text vectorization, image sebagai data numerik), cara menyimpan dataset bersih, Data Versioning &amp; Management Strategies, Best Practices &amp; Error Handling, sampai gambaran besar Data Pipeline Flow end-to-end.</p>
]]></content:encoded></item></channel></rss>