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.

python
"""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:

text
indikator_kesehatan.csv siap: 108 baris x 7 kolom (4130 byte)

Hasilnya DataFrame 108 baris — 102 asli plus 6 salinan. Bagian pentingnya:

  1. 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.
  2. Formula per baris: jumlah fasilitas diturunkan dari skala, dikali faktor pertumbuhan tahunan, lalu diberi noise rng.uniform() agar berubah wajar antartahun.
  3. 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.
  4. astype(str) pada jumlah_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.

python
"""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:

text
== 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 sel

Mari kita telaah:

  1. dtype={"kode_provinsi": "str"}: kode wilayah adalah penanda, bukan bilangan, sehingga kolomnya tetap teks sejak awal — praktik pada chapter Parameter Penting read_csv().
  2. df.dtypes: lima kolom teks bertipe str, bawaan pandas 3.0 yang menggantikan object; jumlah_dokter ikut menjadi str karena tanda - — contoh nyata tipe yang tidak sesuai maksud.
  3. Profil tiap kolom: nunique() pada nama_provinsi menghasilkan 46 padahal provinsi hanya 34, bukti 12 bentuk penulisan tambahan; persen_akses_air_bersih kehilangan 9 nilai.
  4. describe(): jumlah_puskesmas ber-min -8.0 dan persen_akses_air_bersih berrentang -5.2 sampai 118.9, keduanya di luar batas wajar; count 99 dari 108 mengonfirmasi nilai kosong.
  5. 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:

MasalahJumlahCara deteksi
Baris duplikat6df.duplicated()
Label provinsi tak seragam12 baris, 46 bentukpembandingan dengan peta NAMA_BAKU
Tipe jumlah_dokter tidak sesuai5 sel '-'df["jumlah_dokter"].dtype
Persen kosong (NaN)9df["persen_akses_air_bersih"].isna()
Persen di luar rentang 0-1004mask boolean < 0 atau > 100
Jumlah negatif2 selpembandingan < 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 .