Chapter sebelumnya menyimpulkan bahwa mode API real-time adalah pintu masuk serving yang paling instruktif untuk dipraktikkan. Kini kita bangun API tersebut sungguhan: satu layanan HTTP yang menerima fitur rumah, memvalidasi nilainya, mengembalikan prediksi harga, dan melaporkan status kesehatannya. FastAPI dipilih karena tiga hal yang pas untuk machine learning — validasi masukan otomatis lewat Pydantic, skema respons yang eksplisit, dan TestClient yang memungkinkan seluruh API diuji in-process tanpa server nyata. Syarat terakhir tersebut penting untuk lingkungan belajar: skrip di chapter ini berjalan sebagai program Python biasa, deterministik, dan selesai dalam hitungan detik, tetapi polanya identik dengan deployment produksi.
Dua Proses yang Dipisah Artefak #
Arsitektur serving yang sehat memisahkan proses pelatihan dari proses penyajian. Proses pelatihan membaca data, menjalankan fit(), lalu menuliskan hasilnya sebagai artefak — dalam praktik kita, berkas joblib yang memuat Pipeline scikit-learn secara utuh bersama parameter terlatihnya. Proses penyajian kemudian memuat artefak tersebut dan hanya menjalankan predict(); proses penyajian tidak pernah menyentuh data pelatihan.
Pemisahan ini membawa tiga manfaat konkret. Pertama, latensi: joblib.load() dibayar satu kali saat proses menyala, bukan pada setiap permintaan. Kedua, audit: artefak yang sama dapat diuji ulang di mesin lain dan hasilnya dibandingkan. Ketiga, keamanan operasional: proses penyajian tidak membutuhkan akses ke data pelatihan, sehingga lingkup aksesnya bisa dipersempit. Konsep penyimpanan dan pemuatan model dibahas sebagai chapter tersendiri di Section Pipeline dan Produktivitas; di sini kita pakai bentuk paling ringkasnya — joblib.dump() dan joblib.load().
Skema Permintaan dan Validasi Pydantic #
Endpoint prediksi adalah gerbang antara dunia luar yang berisik dan model yang rapuh. Pydantic v2 memungkinkan kita menyatakan kontrak gerbang tersebut sebagai class:
class FiturRumah(BaseModel):
luas: float = Field(ge=20, le=500, description="luas bangunan dalam meter persegi")
kamar: int = Field(ge=1, le=10, description="jumlah kamar tidur")Deklarasi luas: float = Field(ge=20, le=500) berarti tiga validasi terjadi sebelum handler berjalan: nilai harus bisa dibaca sebagai float, tidak boleh di bawah 20, dan tidak boleh di atas 500. Permintaan yang melanggar salah satu aturan ditolak otomatis dengan status 422 beserta pesan galat yang menyebutkan field dan alasannya — kita tidak menulis satu baris logika validasi pun di dalam handler. Skema respons (PrediksiHarga) bekerja ke arah sebaliknya: skema tersebut menjamin keluaran endpoint selalu berbentuk yang dijanjikan, termasuk versi_model yang mempercepat penelusuran saat ada laporan dari pengguna.
Menyatukan Semuanya: Skrip Serving Lengkap #
Skrip berikut melakukan lima tahap dalam satu proses: membuat data sintetis, melatih pipeline, menyimpan artefak, memuat artefak kembali ke proses serving, lalu menguji API lewat TestClient. Tidak ada server nyata, tidak ada port yang dibuka — TestClient memanggil handler secara langsung di dalam memori.
# Model serving dengan FastAPI: satu proses Python, tanpa server nyata.
# Alur: latih pipeline -> simpan artefak joblib -> muat kembali -> sajikan
# lewat app FastAPI -> uji sepenuhnya in-process dengan TestClient.
import json
import os
import joblib
import numpy as np
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
FOLDER = os.path.dirname(os.path.abspath(__file__))
NAMA_ARTEFAK = "model-rumah.joblib"
JALUR_ARTEFAK = os.path.join(FOLDER, NAMA_ARTEFAK)
VERSI_MODEL = "rumah-v1.0.0"
# 1) Data sintetis: harga rumah (juta rupiah) dari luas dan jumlah kamar.
rng = np.random.default_rng(42)
n_latih = 300
luas = rng.uniform(20.0, 200.0, n_latih)
kamar = rng.integers(1, 7, n_latih)
harga = 3.0 * luas + 10.0 * kamar + rng.normal(0.0, 4.0, n_latih)
X_latih = np.column_stack([luas, kamar])
y_latih = harga
# 2) Latih pipeline lengkap (skala + regresi) lalu simpan sebagai artefak.
pipeline_latih = Pipeline([
("skala", StandardScaler()),
("regresi", LinearRegression()),
])
pipeline_latih.fit(X_latih, y_latih)
joblib.dump(pipeline_latih, JALUR_ARTEFAK)
# 3) Muat artefak kembali: proses serving tidak melatih apa pun.
model = joblib.load(JALUR_ARTEFAK)
app = FastAPI(title="API Prediksi Harga Rumah", version=VERSI_MODEL)
class FiturRumah(BaseModel):
luas: float = Field(ge=20, le=500, description="luas bangunan dalam meter persegi")
kamar: int = Field(ge=1, le=10, description="jumlah kamar tidur")
class PrediksiHarga(BaseModel):
harga_juta: float
versi_model: str
@app.get("/health")
def health():
return {"status": "ok", "versi_model": VERSI_MODEL}
@app.post("/predict")
def predict(fitur: FiturRumah):
X_baru = np.array([[fitur.luas, fitur.kamar]])
harga_prediksi = model.predict(X_baru)
return PrediksiHarga(harga_juta=round(float(harga_prediksi[0]), 3),
versi_model=VERSI_MODEL)
# 4) Uji in-process: TestClient menjalankan app tanpa jaringan nyata.
client = TestClient(app)
def uji(label, metode, jalur, **kwargs):
respons = client.request(metode, jalur, **kwargs)
print(f"{label}")
print(f" status : {respons.status_code}")
if respons.status_code == 200:
print(f" respons: {json.dumps(respons.json(), sort_keys=True)}")
else:
for galat in respons.json()["detail"]:
print(f" galat : {galat['type']} pada "
f"{'.'.join(str(x) for x in galat['loc'])}: {galat['msg']}")
print()
uji("GET /health", "GET", "/health")
uji("POST /predict (valid: luas=120, kamar=3)", "POST", "/predict",
json={"luas": 120, "kamar": 3})
uji("POST /predict (batas bawah: luas=20, kamar=1)", "POST", "/predict",
json={"luas": 20, "kamar": 1})
uji("POST /predict (luas di bawah batas: -5)", "POST", "/predict",
json={"luas": -5, "kamar": 3})
uji("POST /predict (tipe salah: kamar='tiga')", "POST", "/predict",
json={"luas": 120, "kamar": "tiga"})
uji("POST /predict (field hilang: kamar)", "POST", "/predict",
json={"luas": 120})
# 5) Bersihkan artefak sementara agar folder chapter tetap rapi.
os.remove(JALUR_ARTEFAK)
print(f"artefak {NAMA_ARTEFAK} dihapus; proses selesai")Output yang Dihasilkan:
GET /health
status : 200
respons: {"status": "ok", "versi_model": "rumah-v1.0.0"}
POST /predict (valid: luas=120, kamar=3)
status : 200
respons: {"harga_juta": 389.75, "versi_model": "rumah-v1.0.0"}
POST /predict (batas bawah: luas=20, kamar=1)
status : 200
respons: {"harga_juta": 69.505, "versi_model": "rumah-v1.0.0"}
POST /predict (luas di bawah batas: -5)
status : 422
galat : greater_than_equal pada body.luas: Input should be greater than or equal to 20
POST /predict (tipe salah: kamar='tiga')
status : 422
galat : int_parsing pada body.kamar: Input should be a valid integer, unable to parse string as an integer
POST /predict (field hilang: kamar)
status : 422
galat : missing pada body.kamar: Field required
artefak model-rumah.joblib dihapus; proses selesaiMari kita telaah bagian-bagian yang memikul beban utama:
- Baris 17–20 (konstanta):
JALUR_ARTEFAKdibangun dari__file__, bukan dari direktori kerja, sehingga skrip menghasilkan artefak di folder yang sama di mana pun skrip dijalankan.VERSI_MODELakan ikut dalam setiap respons dan endpoint kesehatan. - Baris 23–29 (data sintetis):
rng = np.random.default_rng(42)menjamin data pelatihan — dan akibatnya seluruh prediksi — dapat direproduksi bit demi bit. - Baris 32–37 (latih dan simpan):
PipelineberisiStandardScalerdanLinearRegression; seluruh objek terlatih, termasuk rata-rata dan simpangan hasilfit(), tersimpan ke satu berkas olehjoblib.dump(). - Baris 40 (
model = joblib.load): baris ini adalah batas dua dunia. Setelahnya, variabelmodeladalah salinan dari artefak; kode di bawahnya tidak lagi bergantung pada proses pelatihan sama sekali. - Baris 45–52 (dua skema):
FiturRumahmemvalidasi masukan;PrediksiHargamenjanjikan bentuk keluaran. Kedua class tersebut juga otomatis menjadi dokumentasi interaktif di/docssaat API dijalankan dengan server nyata. - Baris 55–57 dan 60–65 (dua endpoint):
/healthuntuk pemeriksaan kesiapan oleh sistem orkestrasi;/predictmenerima satuFiturRumah, membentuk array satu baris, lalu memanggilmodel.predict(). Nilai prediksi dibulatkan tiga desimal supaya respons stabil dan ringkas. - Baris 69 (
client = TestClient(app)): dari titik ini, permintaan HTTP disimulasikan di dalam memori.client.post("/predict", json=...)menjalankan seluruh tumpukan FastAPI — routing, validasi Pydantic, serialisasi respons — tanpa membuka port. - Baris 72–95 (helper
ujidan enam skenario): helper mencetak status, lalu untuk respons 200 mencetak JSON dengan kunci terurut (sort_keys=True) agar keluaran deterministik, sedangkan untuk 422 mencetaktype,loc, danmsgsetiap galat validasi. Enam skenario menutup jalur bahagia, nilai batas, pelanggaran rentang, tipe salah, dan field hilang. - Baris 98–99 (pembersihan): artefak sementara dihapus agar folder chapter tetap bersih — kebiasaan yang sama yang kita terapkan pada berkas gambar matplotlib di chapter-chapter sebelumnya.
Perhatikan pola yang berulang pada keluaran: seluruh permintaan tidak valid ditolak dengan status 422 sebelum mencapai handler, dan setiap pesan galat menyebutkan field yang bermasalah. Prediksi luas=120, kamar=3 menghasilkan 389,75 — masuk akal terhadap rumus data sintetis (3,0 × 120 + 10,0 × 3 ≈ 390) — dan nilai batas luas=20 diterima karena ge bersifat inklusif.
Membaca Status Code dan Galat Validasi #
Kontrak status code harus disiplin karena sistem pemantau dan pemanggil API bergantung padanya:
| Status | Arti bagi pemanggil | Penyebab umum |
|---|---|---|
| 200 OK | Prediksi sah dan bisa dipakai | Semua validasi lolos, handler selesai |
| 422 Unprocessable Entity | Masukan ditolak sebelum diproses | Pelanggaran Field, tipe salah, field hilang |
| 500 Internal Server Error | Sisi layanan bermasalah; coba lagi nanti | Galat tak tertangani di dalam handler |
Bagi API prediksi, pembeda 422 dan 500 penting: 422 berarti pemanggil yang harus memperbaiki muatannya, sedangkan 500 berarti pemilik layanan yang harus turun tangan. Seluruh kasus pada skenario uji kita jatuh ke kategori pertama, dan itu pertanda kontrak validasi bekerja.
Praktik Operasional di Sekitar Endpoint #
Ketika API ini dipindahkan ke lingkungan nyata, pola yang berubah hanya lapisan terluarnya — handler, skema, dan artefak tetap sama:
uvicorn api-prediksi-rumah:app --host 0.0.0.0 --port 8000Perintah tersebut menjalankan server ASGI yang melayani app dari berkas api-prediksi-rumah.py; setelahnya dokumentasi interaktif tersedia di /docs. Tiga kebiasaan operasional layak dibawa sejak awal. Pertama, muat artefak sekali di level modul atau startup event, bukan di dalam handler. Kedua, catat setiap permintaan dan prediksi ke penyimpanan log — data tersebut menjadi bahan monitoring pada chapter berikutnya. Ketiga, sisertakan versi_model pada respons seperti pada skrip kita; ketika dua laporan pengguna menunjukkan versi berbeda, penelusuran akar masalah memendek drastis.
Dengan API yang melayani satu permintaan satu prediksi, kita sudah menutup kebutuhan interaktif. Namun tidak semua masalah membutuhkan jawaban seketika: jutaan baris skor kredit semalam tidak memerlukan server yang menyala sepanjang waktu. Pola yang jauh lebih murah untuk kebutuhan tersebut adalah pemrosesan massal terjadwal — materi Chapter Batch Inference.
Referensi Resmi #
- Request Body — FastAPI — pola resmi menerima dan memvalidasi data permintaan dengan Pydantic.
- Body - Fields — FastAPI
— dokumentasi resmi atribut
Fieldsepertige,le, dandescription. - Testing — FastAPI
— panduan resmi pengujian in-process dengan
TestClient. - joblib — Persistence
— referensi resmi
dump/loaduntuk artefak model.