Kolom teks adalah tempat favorit masalah data bersembunyi. Begitu isian diketik manusia — pemesan menulis nama menu sendiri di formulir daring — spasi ekstra, kapital yang tak seragam, dan karakter pengganti mulai bermunculan. Dua nilai yang sama maknanya terhitung berbeda oleh komputer, dan laporan penjualan per menu jadi terpecah. Pada chapter ini kita merapikan kolom teks dengan keluarga metode .str milik pandas: strip untuk tepi, lower untuk kapital, replace untuk karakter, split untuk memecah nilai gabungan, dan extract untuk mengambil pola dari teks.

Formulir Bebas dan Penyamaran Teks #

Formulir pemesanan daring Toko Kopi Kita membiarkan pelanggan menulis nama menu secara bebas. Delapan pesanan masuk, dan sekilas semuanya memesan salah satu dari lima menu. Sekilas itu keliru — mari kita hitung dengan nunique() dan value_counts().

python
import pandas as pd

pesanan = pd.DataFrame({
    "menu": ["  Kopi Susu", "kopi  susu", "Matcha_Latte", "TEH TAWAR",
             "teh tawar ", "Es Kopi Susu", "matcha latte", "AMERICANO"],
})

print(pesanan)
print()
print("Jumlah nilai unik:", pesanan["menu"].nunique())
print()
print(pesanan["menu"].value_counts())

Output yang Dihasilkan:

text
           menu
0     Kopi Susu
1    kopi  susu
2  Matcha_Latte
3     TEH TAWAR
4    teh tawar 
5  Es Kopi Susu
6  matcha latte
7     AMERICANO

Jumlah nilai unik: 8

menu
  Kopi Susu     1
kopi  susu      1
Matcha_Latte    1
TEH TAWAR       1
teh tawar       1
Es Kopi Susu    1
matcha latte    1
AMERICANO       1
Name: count, dtype: int64

Hasilnya janggal: nunique() melaporkan delapan nilai unik dari delapan pesanan — seolah-olah toko menjual delapan menu berbeda. Padahal Kopi Susu dan kopi susu hanyalah kopi susu yang ditulis dua gaya, dan TEH TAWAR dengan teh tawar adalah menu yang sama. Laporan penjualan per menu akan memuat delapan baris yang sebagian besar berisi satu pesanan.

Merapikan Tepi dan Kapital dengan strip dan lower #

Dua masalah paling umum diselesaikan dua metode pertama. str.strip() membuang spasi di awal dan akhir teks; str.lower() menyeragamkan seluruh huruf menjadi kecil — bentuk yang paling cocok untuk analisis karena menghapus perbedaan kapital. Keduanya dirantai langsung pada kolom.

python
import pandas as pd

menu = ["  Kopi Susu", "kopi  susu", "Matcha_Latte", "TEH TAWAR",
        "teh tawar ", "Es Kopi Susu", "matcha latte", "AMERICANO"]

rapikan = pd.Series(menu).str.strip().str.lower()
print(rapikan.tolist())
print()
print(rapikan.value_counts())
print()
print("Untuk tampilan:", rapikan.str.title().tolist())

Output yang Dihasilkan:

text
['kopi susu', 'kopi  susu', 'matcha_latte', 'teh tawar', 'teh tawar', 'es kopi susu', 'matcha latte', 'americano']

teh tawar       2
kopi  susu      1
kopi susu       1
matcha_latte    1
es kopi susu    1
matcha latte    1
americano       1
Name: count, dtype: int64

Untuk tampilan: ['Kopi Susu', 'Kopi  Susu', 'Matcha_Latte', 'Teh Tawar', 'Teh Tawar', 'Es Kopi Susu', 'Matcha Latte', 'Americano']

Mari kita telaah kemajuannya:

  1. Baris 6 (rantai .str.strip().str.lower()): daftar hasil memperlihatkan spasi tepi hilang dan seluruh huruf kecil. TEH TAWAR dan teh tawar kini sama-sama menjadi teh tawar — dua pesanan itu akhirnya terhitung satu menu.
  2. value_counts(): jumlah nilai unik turun dari delapan menjadi tujuh. Kemajuan nyata, tetapi kopi susu (spasi ganda) dan matcha_latte (garis bawah) masih menyamar sebagai menu tersendiri.
  3. Baris 11 (str.title()): kapital di tiap kata dipulihkan menjadi Kopi Susu — tetapi hanya untuk tampilan laporan. Untuk analisis, tetap pakai bentuk kecil yang seragam.

Karakter Pengganti dan Spasi Ganda #

Sisa dua penyamar — garis bawah dan spasi ganda — butuh str.replace. Pola spasi berulang ditangkap regex \s+ yang berarti “satu atau lebih karakter spasi”; menggantinya dengan satu spasi meratakan semua spasi ganda, tiga, dan seterusnya. Rantai lengkapnya menjadi satu alur pembersihan yang bisa dibaca dari atas ke bawah.

python
import pandas as pd

menu = ["  Kopi Susu", "kopi  susu", "Matcha_Latte", "TEH TAWAR",
        "teh tawar ", "Es Kopi Susu", "matcha latte", "AMERICANO"]

bersih = (pd.Series(menu)
          .str.strip()
          .str.lower()
          .str.replace("_", " ", regex=False)
          .str.replace(r"\s+", " ", regex=True))

perbandingan = pd.DataFrame({"asli": menu, "bersih": bersih})
print(perbandingan)
print()
print(bersih.value_counts())
print()
print("Menu yang memuat kata kopi:")
print(bersih[bersih.str.contains("kopi")].tolist())

Output yang Dihasilkan:

text
           asli        bersih
0     Kopi Susu     kopi susu
1    kopi  susu     kopi susu
2  Matcha_Latte  matcha latte
3     TEH TAWAR     teh tawar
4    teh tawar      teh tawar
5  Es Kopi Susu  es kopi susu
6  matcha latte  matcha latte
7     AMERICANO     americano

kopi susu       2
matcha latte    2
teh tawar       2
es kopi susu    1
americano       1
Name: count, dtype: int64

Menu yang memuat kata kopi:
['kopi susu', 'kopi susu', 'es kopi susu']

Mari kita telaah rantai pembersihannya:

  1. Baris 9 (.str.replace("_", " ", regex=False)): garis bawah dianggap pembatas kata, diganti spasi — matcha_latte menjadi matcha latte.
  2. Baris 10 (.str.replace(r"\s+", " ", regex=True)): dengan mode regex, \s+ mencocokkan deretan spasi sepanjang apa pun dan menggantinya dengan tepat satu spasi. kopi susu akhirnya menyatu dengan kopi susu.
  3. Hasil akhir: dari delapan nilai unik menjadi lima menu sesungguhnya — kopi susu 2, matcha latte 2, teh tawar 2, es kopi susu 1, americano 1. Urutan operasi juga penting: strip lebih dulu, lalu penggantian, agar spasi hasil ganti garis bawah ikut dirapikan.
  4. Baris 17-18 (str.contains("kopi")): saringan teks — mengambil hanya menu yang memuat kata kopi, berguna untuk analisis per lini produk.

Memecah Nilai Gabungan dengan split #

Satu sel kadang memuat dua informasi sekaligus: nama lengkap Budi Santoso sebenarnya adalah nama depan dan nama belakang yang direkatkan spasi. str.split memecahnya; dengan expand=True hasil pecahannya langsung menjadi kolom-kolom DataFrame baru.

python
import pandas as pd

pesanan = pd.DataFrame({
    "pemesan": ["Budi Santoso", "Siti Rahayu", "Dewi Lestari",
                "Rani Wijaya", "Agus Pratama"],
    "menu": ["kopi susu", "teh tawar", "matcha latte",
             "americano", "es kopi susu"],
})

bagian = pesanan["pemesan"].str.split(r"\s+", regex=True, expand=True)
print(bagian)
print()
pesanan["nama_depan"] = bagian[0]
pesanan["nama_belakang"] = bagian[1]
print(pesanan)

Output yang Dihasilkan:

text
      0        1
0  Budi  Santoso
1  Siti   Rahayu
2  Dewi  Lestari
3  Rani   Wijaya
4  Agus  Pratama

        pemesan          menu nama_depan nama_belakang
0  Budi Santoso     kopi susu       Budi       Santoso
1   Siti Rahayu     teh tawar       Siti        Rahayu
2  Dewi Lestari  matcha latte       Dewi       Lestari
3   Rani Wijaya     americano       Rani        Wijaya
4  Agus Pratama  es kopi susu       Agus       Pratama

Mari kita telaah hasilnya:

  1. Baris 10 (str.split(r"\s+", regex=True, expand=True)): pola pemisah adalah deretan spasi, sehingga nama dengan spasi ganda pun terpecah benar. expand=True menghasilkan DataFrame dengan kolom bernomor 0, 1, dan seterusnya — satu kolom per pecahan.
  2. Baris 13-14 (bagian[0], bagian[1]): kolom pecahan dipindahkan ke pesanan dengan nama yang bermakna. Bila hanya butuh satu bagian, pesanan["pemesan"].str.split().str.get(0) memberi nama depan tanpa membuat kolom tambahan.
  3. Kolom ketiga bila ada: nama dengan tiga kata membuat expand=True menghasilkan kolom 2 yang berisi NaN untuk baris lainnya — pecahan yang tidak seragam tetap terlihat, bukan tersembunyi.

Mengambil Angka dari Teks dengan extract #

Kebalikan dari split: kadang kita hanya ingin mengambil sebagian teks yang mengikuti pola tertentu. Kolom harga hasil tempelan manusia — Rp24.000, Rp 28.000 — menyimpan angka di tengah teks. str.extract dengan regex menangkapnya lewat capture group.

python
import pandas as pd

harga_teks = pd.Series(["Rp24.000", "Rp 28.000", "Rp20.000", "Rp 22.000"])
print(harga_teks.tolist())
print()

tangkap = harga_teks.str.extract(r"Rp\s*([\d\.]+)")
print(tangkap)
print()

harga = pd.to_numeric(tangkap[0].str.replace(".", "", regex=False))
print(harga)

Output yang Dihasilkan:

text
['Rp24.000', 'Rp 28.000', 'Rp20.000', 'Rp 22.000']

        0
0  24.000
1  28.000
2  20.000
3  22.000

0    24000
1    28000
2    20000
3    22000
Name: 0, dtype: int64

Mari kita bedah pola regex r"Rp\s*([\d\.]+)" itu:

  1. Rp: dicocokkan apa adanya — setiap nilai memuat teks Rp di depan.
  2. \s*: nol atau lebih spasi, menangani Rp24.000 dan Rp 28.000 sekaligus.
  3. ([\d\.]+): tanda kurung adalah capture group — bagian yang diambil. Isinya, satu atau lebih karakter angka atau titik, menangkap 24.000 lengkap.
  4. Baris 11 (pd.to_numeric): hasil tangkapan berupa kolom teks 24.000, lalu langkah lama kita mengambil alih: buang titik ribuan dengan str.replace, konversi dengan pd.to_numeric — pola yang sudah kita kuasai pada chapter Memperbaiki Tipe Data . Bila pola memuat lebih dari satu capture group, extract mengembalikan satu kolom per group.

Rangkuman #

  • nunique() dan value_counts() membongkar nilai yang menyamar; delapan “menu” ternyata lima menu dengan gaya penulisan berbeda.
  • str.strip() dan str.lower() merapikan tepi dan kapital untuk analisis; str.title() hanya untuk tampilan.
  • str.replace() menangani karakter pengganti dan — dengan regex \s+ — meratakan spasi ganda; susun operasi sebagai rantai yang terbaca.
  • str.split(..., expand=True) memecah nilai gabungan menjadi kolom; str.extract() mengambil bagian teks yang cocok pola regex.
  • str.contains() menyaring baris berdasar teks; seluruh metode .str menurunkan NaN tetap sebagai NaN.

Normalisasi teks menyelesaikan masalah dalam satu kolom. Tetapi ketika dua tabel harus digabungkan — transaksi di satu sisi, daftar harga di sisi lain — perbedaan penulisan antar tabel menghasilkan kecocokan yang gagal tanpa galat. Kita tangani itu pada chapter berikutnya, Menangani Data Tidak Konsisten .