Proyek ini menempuh alur kerja analis data dari titik paling awal: memilih dataset terbuka, memahaminya, lalu menyusun temuan. Studi kasusnya satu berkas dari Portal Data Terbuka Nusantara, portal fiktif penyimulasi situs data terbuka pemerintah, berisi Indikator Layanan Kesehatan Dasar per Provinsi 2022-2024. Karena latihan ini offline, indikator_kesehatan.csv dibangun skrip deterministik sehingga semua pembaca mendapat isi yang sama. Pada bagian ini kita menetapkan kriteria pemilihan, memuat berkasnya dengan pandas, memeriksa struktur dan profil tiap kolom, lalu menyusun daftar masalah sebagai kontrak kerja Bagian 2.
Kriteria Memilih Dataset Terbuka #
Situs data terbuka menyediakan ribuan berkas, dan kemampuan memilih menentukan kualitas analisis. Kita menilai tiga hal. Pertama, relevansi: dataset harus menjawab pertanyaan analisis yang jelas — di sini, pemerataan layanan kesehatan dasar antar provinsi dan perubahannya dari 2022 ke 2024. Kedua, kelengkapan dokumentasi: penyedia serius menyertakan kamus data tiap kolom beserta tipe dan catatan kualitasnya. Ketiga, lisensi terbuka sejenis CC BY, yang membolehkan data dipakai dan diolah selama sumbernya disebutkan.
Ketiga kriteria itu kita cerminkan dalam kode: modul buat_dataset.py menyertakan KAMUS_DATA, dict Python berisi tipe target, deskripsi, dan catatan kualitas tiap kolom — tiruan kamus data yang biasanya menyertai berkas unduhan dan dicetak skrip inspeksi di bawah.
Dataset Latihan yang Dapat Direproduksi #
Dataset proyek dibuat skrip, bukan diunduh dari internet, mengikuti pola Reproducible Data Processing pada section Pengujian dan Debugging. Skrip buat_dataset.py menyusun 102 baris dasar — 34 provinsi dikali 3 tahun — memakai np.random.default_rng(77), menambahkan enam salinan baris sebagai duplikat, lalu mengacak barisnya.
"""Generator dataset simulasi "Indikator Layanan Kesehatan Dasar per Provinsi".
Dataset meniru hasil unduhan satu berkas CSV dari portal data terbuka
(fiktif): Portal Data Terbuka Nusantara. Isi dibuat deterministik
(seed 77) dan sengaja memuat masalah kualitas yang lazim di berkas
ekspor, yaitu:
1. baris duplikat,
2. label nama provinsi tidak seragam,
3. tanda '-' pada kolom jumlah_dokter (nilai tidak tersedia),
4. persen akses air bersih kosong (NaN),
5. persen di luar rentang 0-100,
6. jumlah fasilitas negatif.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
SEED = 77
TAHUN = [2022, 2023, 2024]
JUMLAH_DUPLIKAT = 6
# kode provinsi -> (nama provinsi, skala populasi fiktif 1-10)
PROVINSI = {
"11": ("Aceh", 2),
"12": ("Sumatera Utara", 7),
"13": ("Sumatera Barat", 4),
"14": ("Riau", 4),
"15": ("Jambi", 2),
"16": ("Sumatera Selatan", 5),
"17": ("Bengkulu", 1),
"18": ("Lampung", 4),
"19": ("Kepulauan Bangka Belitung", 1),
"21": ("Kepulauan Riau", 2),
"31": ("DKI Jakarta", 8),
"32": ("Jawa Barat", 10),
"33": ("Jawa Tengah", 9),
"34": ("DI Yogyakarta", 3),
"35": ("Jawa Timur", 10),
"36": ("Banten", 5),
"51": ("Bali", 2),
"52": ("Nusa Tenggara Barat", 3),
"53": ("Nusa Tenggara Timur", 3),
"61": ("Kalimantan Barat", 3),
"62": ("Kalimantan Tengah", 2),
"63": ("Kalimantan Selatan", 3),
"64": ("Kalimantan Timur", 3),
"65": ("Kalimantan Utara", 1),
"71": ("Sulawesi Utara", 2),
"72": ("Sulawesi Tengah", 2),
"73": ("Sulawesi Selatan", 6),
"74": ("Sulawesi Tenggara", 2),
"75": ("Gorontalo", 1),
"76": ("Sulawesi Barat", 1),
"81": ("Maluku", 1),
"82": ("Maluku Utara", 1),
"91": ("Papua Barat", 1),
"94": ("Papua", 2),
}
# nama baku -> dipakai membersihkan label provinsi (kunci dalam huruf kecil)
NAMA_BAKU = {nama.lower(): nama for nama, _ in PROVINSI.values()}
# meta-dataset: kolom -> (tipe target, deskripsi, catatan kualitas)
KAMUS_DATA = {
"kode_provinsi": (
"str",
"kode provinsi dua digit mengikuti pola BPS, mis. 32",
"wajib dibaca sebagai teks, bukan bilangan",
),
"nama_provinsi": (
"str",
"nama resmi provinsi, mis. Jawa Barat",
"ditemukan penulisan tidak seragam",
),
"tahun": (
"int64",
"tahun observasi: 2022, 2023, atau 2024",
"tidak ada catatan",
),
"jumlah_puskesmas": (
"int64",
"banyak puskesmas yang tercatat di provinsi",
"wajib bernilai >= 0",
),
"jumlah_rumah_sakit": (
"int64",
"banyak rumah sakit yang tercatat di provinsi",
"wajib bernilai >= 0",
),
"jumlah_dokter": (
"int64",
"banyak dokter yang terdaftar di provinsi",
"berkas memakai '-' untuk nilai tidak tersedia",
),
"persen_akses_air_bersih": (
"float64",
"persentase rumah tangga berakses air bersih",
"rentang sah 0-100; ada nilai kosong dan di luar rentang",
),
}
KOLOM = [
"kode_provinsi",
"nama_provinsi",
"tahun",
"jumlah_puskesmas",
"jumlah_rumah_sakit",
"jumlah_dokter",
"persen_akses_air_bersih",
]
def buat_dataset() -> pd.DataFrame:
"""Menghasilkan DataFrame mentah 108 baris dengan masalah yang dirancang."""
rng = np.random.default_rng(SEED)
# 34 provinsi x 3 tahun = 102 baris dasar
baris = []
for ti, tahun in enumerate(TAHUN):
for kode, (nama, skala) in PROVINSI.items():
pus = int(
round(55 * skala * (1 + 0.02 * ti) * rng.uniform(0.90, 1.10))
)
rs = int(round(7 * skala * (1 + 0.03 * ti) * rng.uniform(0.88, 1.12)))
dok = int(
round(850 * skala * (1 + 0.04 * ti) * rng.uniform(0.90, 1.10))
)
persen = round(
58 + 2.2 * skala + 0.9 * ti + rng.uniform(-2.5, 2.5), 1
)
baris.append((kode, nama, tahun, pus, rs, dok, persen))
df = pd.DataFrame(baris, columns=KOLOM)
n_asli = len(df) # 102
# semua masalah diambil dari satu permutasi agar kelompoknya saling lepas
urutan = rng.permutation(n_asli)
i_label = urutan[0:12]
i_nan = urutan[12:21]
i_oor = urutan[21:25]
i_str = urutan[25:30]
i_pus_neg = int(urutan[30])
i_dok_neg = int(urutan[31])
i_dup = urutan[32:38]
# 1) label nama provinsi tidak seragam (12 baris, 4 gaya)
gaya = [
lambda s: s.lower(),
lambda s: s.upper(),
lambda s: f" {s} ",
lambda s: f" {s.lower()} ",
]
for urut, i in enumerate(i_label):
df.loc[i, "nama_provinsi"] = gaya[urut % 4](df.loc[i, "nama_provinsi"])
# 2) nilai tidak mungkin: jumlah negatif (1 puskesmas, 1 dokter)
df.loc[i_pus_neg, "jumlah_puskesmas"] = -8
df.loc[i_dok_neg, "jumlah_dokter"] = -245
# 3) tipe tidak sesuai: '-' mengubah kolom dokter menjadi teks
df["jumlah_dokter"] = df["jumlah_dokter"].astype(str)
df.loc[i_str, "jumlah_dokter"] = "-"
# 4) persen kosong (9 baris)
df.loc[i_nan, "persen_akses_air_bersih"] = np.nan
# 5) persen di luar rentang (3 baris >100, 1 baris negatif)
df.loc[i_oor[:3], "persen_akses_air_bersih"] = [104.6, 102.3, 118.9]
df.loc[i_oor[3], "persen_akses_air_bersih"] = -5.2
# 6) enam salinan baris bersih sebagai duplikat, lalu acak urutan
df = pd.concat([df, df.loc[i_dup]], ignore_index=True)
return df.iloc[rng.permutation(len(df))].reset_index(drop=True)
if __name__ == "__main__":
import tempfile
from pathlib import Path
df = buat_dataset()
with tempfile.TemporaryDirectory() as folder:
berkas = Path(folder) / "indikator_kesehatan.csv"
df.to_csv(berkas, index=False)
print(
f"indikator_kesehatan.csv siap: {df.shape[0]} baris x {df.shape[1]} kolom "
f"({berkas.stat().st_size} byte)"
)Output yang Dihasilkan:
indikator_kesehatan.csv siap: 108 baris x 7 kolom (4130 byte)Hasilnya DataFrame 108 baris — 102 asli plus 6 salinan. Bagian pentingnya:
PROVINSI: dict kode provinsi dua digit ke nama dan skala populasi fiktif 1-10; skala inilah yang membuat Jawa Barat tercatat jauh lebih banyak fasilitasnya dibanding Gorontalo.- Formula per baris: jumlah fasilitas diturunkan dari skala, dikali faktor pertumbuhan tahunan, lalu diberi noise
rng.uniform()agar berubah wajar antartahun. urutan = rng.permutation(n_asli): potongan indeks yang saling lepas menjamin masalah tidak saling menimpa, misalnya tidak ada baris yang sekaligus berlabel tak seragam dan berpersen kosong.astype(str)padajumlah_dokter: konversi ke teks dilakukan sebelum tanda-ditanam; inilah penyebab kolom dokter kehilangan tipe numeriknya.
Memuat CSV dan Memeriksa Struktur #
Kini dataset kita perlakukan seperti unduhan sungguhan: tulis ke CSV, muat ulang dengan read_csv(), lalu periksa. Skrip berikut mencetak kamus data, struktur, lima baris pertama, profil tiap kolom, statistik ringkas, dan rekap masalah.
"""Memuat indikator_kesehatan.csv lalu memetakan struktur dan masalahnya."""
from pathlib import Path
import tempfile
import pandas as pd
from buat_dataset import KAMUS_DATA, NAMA_BAKU, buat_dataset
if __name__ == "__main__":
with tempfile.TemporaryDirectory() as folder:
berkas = Path(folder) / "indikator_kesehatan.csv"
buat_dataset().to_csv(berkas, index=False)
df = pd.read_csv(berkas, dtype={"kode_provinsi": "str"})
print("== Kamus data (meta-dataset) ==")
for kolom, (tipe, deskripsi, catatan) in KAMUS_DATA.items():
print(f"{kolom:24s} {tipe:8s} {deskripsi}")
print(f"{'':25s}{'':9s}catatan: {catatan}")
print("\n== Struktur dataset ==")
print(f"bentuk: {df.shape[0]} baris x {df.shape[1]} kolom")
print(df.dtypes.to_string())
print("\n== Lima baris pertama ==")
print(df.head(5).to_string(index=False))
print("\n== Profil tiap kolom ==")
for kolom in df.columns:
print(
f"{kolom:24s} dtype={str(df[kolom].dtype):8s} "
f"unik={df[kolom].nunique():3d} NaN={df[kolom].isna().sum():3d}"
)
print("\n== Statistik ringkas kolom numerik (dibulatkan 1 desimal) ==")
print(
df[["tahun", "jumlah_puskesmas", "jumlah_rumah_sakit",
"persen_akses_air_bersih"]].describe().round(1).to_string()
)
dok = pd.to_numeric(df["jumlah_dokter"], errors="coerce")
peta = df["nama_provinsi"].str.strip().str.lower().map(NAMA_BAKU)
tak_seragam = df["nama_provinsi"] != peta
oor = (df["persen_akses_air_bersih"] < 0) | (
df["persen_akses_air_bersih"] > 100
)
negatif = (df["jumlah_puskesmas"] < 0).sum() + (df["jumlah_rumah_sakit"] < 0).sum()
negatif += (dok < 0).sum()
print("\n== Masalah yang ditemukan ==")
print(f"baris duplikat : {df.duplicated().sum()}")
print(
f"label nama_provinsi tak seragam : {tak_seragam.sum()} baris, "
f"{df['nama_provinsi'].nunique()} bentuk penulisan (seharusnya 34)"
)
print(f"nilai '-' pada jumlah_dokter : {dok.isna().sum()} sel")
print(f"persen kosong (NaN) : {df['persen_akses_air_bersih'].isna().sum()}")
print(f"persen di luar rentang 0-100 : {oor.sum()}")
print(f"jumlah negatif : {negatif} sel")Output yang Dihasilkan:
== Kamus data (meta-dataset) ==
kode_provinsi str kode provinsi dua digit mengikuti pola BPS, mis. 32
catatan: wajib dibaca sebagai teks, bukan bilangan
nama_provinsi str nama resmi provinsi, mis. Jawa Barat
catatan: ditemukan penulisan tidak seragam
tahun int64 tahun observasi: 2022, 2023, atau 2024
catatan: tidak ada catatan
jumlah_puskesmas int64 banyak puskesmas yang tercatat di provinsi
catatan: wajib bernilai >= 0
jumlah_rumah_sakit int64 banyak rumah sakit yang tercatat di provinsi
catatan: wajib bernilai >= 0
jumlah_dokter int64 banyak dokter yang terdaftar di provinsi
catatan: berkas memakai '-' untuk nilai tidak tersedia
persen_akses_air_bersih float64 persentase rumah tangga berakses air bersih
catatan: rentang sah 0-100; ada nilai kosong dan di luar rentang
== Struktur dataset ==
bentuk: 108 baris x 7 kolom
kode_provinsi str
nama_provinsi str
tahun int64
jumlah_puskesmas int64
jumlah_rumah_sakit int64
jumlah_dokter str
persen_akses_air_bersih float64
== Lima baris pertama ==
kode_provinsi nama_provinsi tahun jumlah_puskesmas jumlah_rumah_sakit jumlah_dokter persen_akses_air_bersih
16 Sumatera Selatan 2023 284 36 4442 70.3
72 Sulawesi Tengah 2023 115 13 1776 62.4
11 Aceh 2023 123 14 1879 64.1
91 Papua Barat 2023 53 6 884 NaN
34 DI Yogyakarta 2024 177 21 3012 NaN
== Profil tiap kolom ==
kode_provinsi dtype=str unik= 34 NaN= 0
nama_provinsi dtype=str unik= 46 NaN= 0
tahun dtype=int64 unik= 3 NaN= 0
jumlah_puskesmas dtype=int64 unik= 77 NaN= 0
jumlah_rumah_sakit dtype=int64 unik= 41 NaN= 0
jumlah_dokter dtype=str unik= 97 NaN= 0
persen_akses_air_bersih dtype=float64 unik= 70 NaN= 9
== Statistik ringkas kolom numerik (dibulatkan 1 desimal) ==
tahun jumlah_puskesmas jumlah_rumah_sakit persen_akses_air_bersih
count 108.0 108.0 108.0 99.0
mean 2023.0 187.2 24.3 67.2
std 0.8 144.8 18.8 11.8
min 2022.0 -8.0 6.0 -5.2
25% 2022.0 90.5 13.0 62.4
50% 2023.0 140.0 17.5 66.0
75% 2024.0 243.2 32.0 69.0
max 2024.0 593.0 78.0 118.9
== Masalah yang ditemukan ==
baris duplikat : 6
label nama_provinsi tak seragam : 12 baris, 46 bentuk penulisan (seharusnya 34)
nilai '-' pada jumlah_dokter : 5 sel
persen kosong (NaN) : 9
persen di luar rentang 0-100 : 4
jumlah negatif : 2 selMari kita telaah:
dtype={"kode_provinsi": "str"}: kode wilayah adalah penanda, bukan bilangan, sehingga kolomnya tetap teks sejak awal — praktik pada chapter Parameter Penting read_csv().df.dtypes: lima kolom teks bertipestr, bawaan pandas 3.0 yang menggantikanobject;jumlah_dokterikut menjadistrkarena tanda-— contoh nyata tipe yang tidak sesuai maksud.- Profil tiap kolom:
nunique()padanama_provinsimenghasilkan 46 padahal provinsi hanya 34, bukti 12 bentuk penulisan tambahan;persen_akses_air_bersihkehilangan 9 nilai. describe():jumlah_puskesmasber-min-8.0 danpersen_akses_air_bersihberrentang -5.2 sampai 118.9, keduanya di luar batas wajar;count99 dari 108 mengonfirmasi nilai kosong.pd.to_numeric(errors="coerce"): pratinjau ke Bagian 2 — lima sel'-'menjadi NaN agar bisa dihitung.
Daftar Masalah yang Terdeteksi #
Rekap akhir output merangkum temuan dalam angka pasti:
| Masalah | Jumlah | Cara deteksi |
|---|---|---|
| Baris duplikat | 6 | df.duplicated() |
| Label provinsi tak seragam | 12 baris, 46 bentuk | pembandingan dengan peta NAMA_BAKU |
Tipe jumlah_dokter tidak sesuai | 5 sel '-' | df["jumlah_dokter"].dtype |
| Persen kosong (NaN) | 9 | df["persen_akses_air_bersih"].isna() |
| Persen di luar rentang 0-100 | 4 | mask boolean < 0 atau > 100 |
| Jumlah negatif | 2 sel | pembandingan < 0 setelah to_numeric() |
Enam masalah ini menjadi target kerja Bagian 2: duplikat dihapus, label diseragamkan ke nama baku, tipe numerik dikembalikan lalu diisi, dan persen kosong diperbaiki sampai lolos uji rentang. Teori tiap teknik dibahas pada section Data Cleaning — antara lain Menangani Nilai Duplikat, Memperbaiki Tipe Data, Standardisasi Label Kategori, Mengisi Nilai Kosong, dan Validasi Rentang Nilai — sehingga di sini kita fokus merangkai prosedurnya utuh.
Sumber bacaan resmi: pandas.read_csv , pandas.DataFrame.describe , dan User Guide: IO tools .