Difference between revisions of "Data-cleaning-fisip"

From belajarwiki
Jump to navigation Jump to search
(Created page with "Ada dua bentuk data mentah: 1. `JSON` → salinan respons API, sedekat mungkin dengan data asli dari Bluesky. 2. `CSV` → field penting sudah dijadikan tabel, tetapi **belum...")
 
 
(7 intermediate revisions by the same user not shown)
Line 1: Line 1:
−
Ada dua bentuk data mentah:
+
Dari contoh yang didapat, kita akan melakukan hal berikut
  
−
1. `JSON` → salinan respons API, sedekat mungkin dengan data asli dari Bluesky.
+
<pre>
−
2. `CSV` → field penting sudah dijadikan tabel, tetapi **belum dibersihkan** sehingga cocok untuk latihan mahasiswa.
+
bluesky_climate_change_raw.csv
 +
            ↓
 +
      DATA CLEANING
 +
            ↓
 +
      PREPROCESSING
 +
            ↓
 +
bluesky_climate_change_clean.csv
 +
            ↓
 +
        ANALISIS
 +
</pre>
  
−
Secara resmi, `app.bsky.feed.searchPosts` mewajibkan parameter `q`, mendukung `sort=latest/top`, filter `lang`, dan `limit` maksimum **100** per request. Dokumentasi juga mengingatkan bahwa sintaks pencarian frasa/Boolean tidak sepenuhnya dispesifikasikan, walaupun sintaks Lucene direkomendasikan.
+
Step by step nya adalah
  
−
---
+
==Data Cleaning==
 +
===Import library dan membaca data===
 +
<pre>
 +
import pandas as pd
 +
import re
  
−
# A. Target akhir praktikum
+
df = pd.read_csv(
 +
    "bluesky_climate_change_raw.csv"
 +
)
  
−
Kita akan menghasilkan:
+
df.head()
 +
</pre>
  
−
```text
+
Cek ukuran data
−
BigDataFisip/
+
df.shape
−
│
 
−
├── venvs/
 
−
│  └── jupyter-polsci/
 
−
│
 
−
└── data/
 
−
    ├── raw/
 
−
    │  ├── bluesky_climate_change_raw.json
 
−
    │  └── bluesky_climate_change_raw.csv
 
−
    │
 
−
    └── processed/
 
−
```
 
  
−
Mahasiswa nantinya **tidak mengubah file di `raw/`**.
+
Klo di contoh saya nanti hasilnya adalah (97, 9). Artinya: 97 baris × 9 variabel.
  
−
Hasil cleaning disimpan ke:
+
Cek nama variabel
 +
df.columns
  
−
```text
 
−
data/processed/
 
−
```
 
  
−
---
+
===Cek kondisi data===
 +
df.info()
 +
df.head()
 +
df.sample(5)
 +
df.isna().sum()
  
−
# B. Langkah 1 — Pastikan Jupyter menggunakan environment yang benar
+
Untuk kasus saya, ada 15 posting tidak mempunyai display_name, namun kita cek pakah masih memiliki username
 +
<pre>
 +
df["display_name"] = (
 +
    df["display_name"]
 +
    .fillna(df["username"])
 +
)
 +
</pre>
  
−
Buat cell:
+
Kemudian cek lagi
 +
df["display_name"].isna().sum()
  
−
```python
+
Harus menghasilkan 0
−
import sys
 
  
−
print(sys.executable)
+
Berarti dalam kasus ini display_name tidak terlalu penting karena masih ada username.
−
print(sys.version)
 
−
```
 
  
−
Pada komputer seharusnya kurang lebih:
+
===Cek apakah ada duplicate===
 +
df.duplicated().sum()
  
−
```text
+
Klo pada kasus saya, id unik nya adalah uri,jadi
−
/venvs/jupyter-polsci/bin/python
+
<pre>
 +
df.duplicated(
 +
    subset="uri"
 +
).sum()
 +
</pre>
  
−
3.10.12 ...
+
Kode cleaning nya adalah
−
```
+
<pre>
 +
df = df.drop_duplicates(
 +
    subset="uri"
 +
)
 +
</pre>
  
−
Kalau demikian, environment sudah benar.
+
Kalau satu posting masuk dua kali, analisis kita dapat menghitung satu opini atau satu engagement dua kali.
  
−
---
+
===Rapihkan display_name===
−
 
+
Beberapa nama mungkin memiliki spasi tambahan.
−
# C. Langkah 2 — Import library
+
<pre>
−
 
+
df["display_name"] = (
−
Buat cell baru:
+
    df["display_name"]
−
 
+
    .astype(str)
−
```python
+
    .str.strip()
−
import json
+
)
−
import requests
+
</pre>
−
import pandas as pd
 
−
 
 
−
from pathlib import Path
 
−
```
 
−
 
 
−
Kita menggunakan:
 
−
 
 
−
```text
 
−
requests  → mengambil data API
 
−
json      → menyimpan respons asli
 
−
pandas    → mengubah data menjadi tabel
 
−
Path      → mengatur folder/file
 
−
```
 
−
 
 
−
---
 
−
 
 
−
# D. Langkah 3 — Tentukan folder penyimpanan
 
  
−
```python
+
Kita rapihkan juga username
−
PROJECT = Path(
+
<pre>
−
     "/media/bgoode/Data4/Unpad/Kuliah/BigDataFisip"
+
df["username"] = (
 +
     df["username"]
 +
    .astype(str)
 +
    .str.strip()
 +
    .str.lower()
 
)
 
)
 +
</pre>
  
−
RAW_DIR = PROJECT / "data" / "raw"
+
===Perbaiki type tanggal===
−
PROCESSED_DIR = PROJECT / "data" / "processed"
+
Kita cek tipe datanya
 +
df["created_at"].dtype
  
−
RAW_DIR.mkdir(
+
Jika hasilnya = object, berarti masih dianggap teks, jadi harus diubah ke tipe waktu.
−
     parents=True,
+
<pre>
−
     exist_ok=True
+
df["created_at"] = pd.to_datetime(
 +
     df["created_at"],
 +
    format="mixed",
 +
    utc=True,
 +
     errors="coerce"
 
)
 
)
 +
</pre>
  
−
PROCESSED_DIR.mkdir(
+
===Ubah UTC ke waktu Jakarta===
−
     parents=True,
+
<pre>
−
     exist_ok=True
+
df["created_at_wib"] = (
 +
     df["created_at"]
 +
     .dt.tz_convert("Asia/Jakarta")
 
)
 
)
 +
</pre>
  
−
print(RAW_DIR)
+
Kemudian cek
−
```
+
<pre>
 +
df[
 +
    ["created_at", "created_at_wib"]
 +
].head()
 +
</pre>
  
−
Hasil:
+
===Rapihkan kolom bahasa===
 +
df["language"].value_counts()
  
−
```text
+
Perbaiki
−
/data/raw
+
<pre>
−
```
+
df["language_primary"] = (
 +
    df["language"]
 +
    .astype(str)
 +
    .str.extract(
 +
        r"([A-Za-z]+(?:-[A-Za-z]+)?)",
 +
        expand=False
 +
    )
 +
    .fillna("unknown")
 +
)
 +
</pre>
  
−
---
+
Cek
 +
df["language_primary"].value_counts()
  
−
# E. Langkah 4 — Tentukan topik (misalnya Climate Change)
+
===Pastikan reply, repost dan like benar-benar numerik===
 +
Cek
 +
<pre>
 +
df[
 +
    [
 +
        "reply_count",
 +
        "repost_count",
 +
        "like_count"
 +
    ]
 +
].dtypes
 +
</pre>
  
−
Endpoint:
+
Jika masih belum angka
 +
<pre>
 +
kolom_angka = [
 +
    "reply_count",
 +
    "repost_count",
 +
    "like_count"
 +
]
  
−
```python
+
for kolom in kolom_angka:
−
url = "https://api.bsky.app/xrpc/app.bsky.feed.searchPosts"
+
    df[kolom] = pd.to_numeric(
−
```
+
        df[kolom],
 +
        errors="coerce"
 +
    ).fillna(0).astype(int)
 +
</pre>
  
−
Kemudian query:
+
===Jika ada URL terambil===
 +
Buat variabel, barangkali nanti url tersebut dibutuhkan
 +
<pre>
 +
df["has_url"] = (
 +
    df["text"]
 +
    .str.contains(
 +
        r"https?://|www\.",
 +
        regex=True,
 +
        na=False
 +
    )
 +
)
 +
</pre>
  
−
```python
+
Cek
−
params = {
+
df["has_url"].value_counts()
−
    "q": '"climate change"',
 
−
    "limit": 100,
 
−
    "sort": "latest",
 
−
    "lang": "en"
 
−
}
 
−
```
 
  
−
Artinya:
+
===Deteksi Hastag===
 +
<pre>
 +
df["has_hashtag"] = (
 +
    df["text"]
 +
    .str.contains(
 +
        r"#\w+",
 +
        regex=True,
 +
        na=False
 +
    )
 +
)
 +
</pre>
  
−
```text
+
Cek
−
q
+
df["has_hashtag"].value_counts()
−
↓
 
−
mencari "climate change"
 
  
−
limit
+
==Preprocessing==
−
↓
+
===Buat Clean Text===
−
maksimal 100 posting
+
Kita mulai masuk ke preprocessing
 +
<pre>
 +
df["text_clean"] = (
 +
    df["text"]
 +
    .fillna("")
 +
    .astype(str)
 +
)
 +
</pre>
  
−
sort
+
Hapus URL
−
↓
+
<pre>
−
latest = hasil terbaru
+
df["text_clean"] = (
 +
    df["text_clean"]
 +
    .str.replace(
 +
        r"https?://\S+|www\.\S+",
 +
        " ",
 +
        regex=True
 +
    )
 +
)
 +
</pre>
  
−
lang
+
Nanti hasilnya misal: <b>Climate change is real https://example.com</b> -> <b>Climate change is real</b>
−
↓
 
−
en = bahasa Inggris
 
−
```
 
  
−
Nilai `100` adalah batas maksimum `limit` menurut Lexicon resmi Bluesky.  
+
===Hilangkan mention===
 +
<pre>
 +
df["text_clean"] = (
 +
    df["text_clean"]
 +
    .str.replace(
 +
        r"@[\w.-]+",
 +
        " ",
 +
        regex=True
 +
    )
 +
)
 +
</pre>
  
−
Catatan:
+
Namun mention tidak selalu harus dibuang. Kalau kita ingin melakukan network analysis, mention justru merupakan data yang penting.
  
−
> `limit=100` berarti **meminta maksimal 100**, bukan jaminan server selalu memberikan tepat 100.
+
===Hastag===
−
 
+
Jangan buang katanya, namun tanda "#" saja
−
---
+
<pre>
−
 
+
df["text_clean"] = (
−
# F. Langkah 5 — Kirim request
+
     df["text_clean"]
−
 
+
     .str.replace(
−
```python
+
         r"#(\w+)",
−
response = requests.get(
+
         r"\1",
−
     url,
+
        regex=True
−
     params=params,
+
     )
−
    headers={
 
−
         "Accept": "application/json",
 
−
         "User-Agent": "BigDataFisip-Class/1.0"
 
−
    },
 
−
     timeout=30
 
 
)
 
)
 +
</pre>
  
−
print(
+
===Ubah menjadi lowercase===
−
     "HTTP Status:",
+
<pre>
−
     response.status_code
+
df["text_clean"] = (
 +
     df["text_clean"]
 +
     .str.lower()
 
)
 
)
 +
</pre>
 +
Ini penting agar komputer tidak menganggap kata yang sama namun misal ada huruf kapital, sebagai kata berbeda.
  
−
print(
+
===Rapikan line break dan spasi===
−
     "Content-Type:",
+
<pre>
−
     response.headers.get("content-type")
+
df["text_clean"] = (
 +
     df["text_clean"]
 +
     .str.replace(
 +
        r"\s+",
 +
        " ",
 +
        regex=True
 +
    )
 +
    .str.strip()
 
)
 
)
−
```
+
</pre>
  
−
Harapannya:
+
===Bandingkan sebelum dan sesudah===
 +
<pre>
 +
df[
 +
    [
 +
        "text",
 +
        "text_clean"
 +
    ]
 +
].head(10)
 +
</pre>
  
−
```text
+
===Buat variabel engagement===
−
HTTP Status: 200
+
Misal kita perlu untuk menghitung hubungannya.
−
Content-Type: application/json; charset=utf-8
+
Kita definisikan secara sederhana:Engagement = Reply + Repost + Like
−
```
 
  
−
---
+
<pre>
 +
df["engagement"] = (
 +
    df["reply_count"]
 +
    + df["repost_count"]
 +
    + df["like_count"]
 +
)
 +
</pre>
  
−
# G. Langkah 6 — Cek hasil JSON
+
Kita cek
 +
<pre>
 +
df[
 +
    [
 +
        "reply_count",
 +
        "repost_count",
 +
        "like_count",
 +
        "engagement"
 +
    ]
 +
].head()
 +
</pre>
  
−
Gunakan:
+
Ini adalah definisi operasional untuk latihan, bukan definisi universal engagement. Kalau penelitian sebenarnya, Anda harus menjelaskan mengapa reply, repost, dan like dijumlahkan dan apakah bobotnya memang seharusnya sama.
  
−
```python
+
===Buat jumlah kata===
−
if response.status_code != 200:
+
<pre>
−
 
+
df["word_count"] = (
−
     print("Request gagal")
+
     df["text_clean"]
−
 
+
     .str.findall(
−
     print(
+
         r"\b[\w’'-]+\b"
−
         response.text[:1000]
 
 
     )
 
     )
 +
    .str.len()
 +
)
 +
</pre>
  
−
    raise RuntimeError(
+
Cek
−
        "Data belum berhasil diambil"
+
df["word_count"].describe()
−
    )
 
−
```
 
  
−
Kemudian pastikan hasil memang JSON:
+
===Buat variabel tanggal dan jam===
 +
<pre>
 +
df["date"] = (
 +
    df["created_at_wib"]
 +
    .dt.date
 +
)
  
−
```python
+
df["hour"] = (
−
content_type = response.headers.get(
+
     df["created_at_wib"]
−
     "content-type",
+
     .dt.hour
−
     ""
 
 
)
 
)
 +
</pre>
  
−
if "application/json" not in content_type:
+
Cleaning = memperbaiki data.
 +
Preprocessing = membuat data menjadi lebih siap untuk analisis.
  
−
    raise RuntimeError(
+
===Validasi===
−
        "Respons server bukan JSON"
+
Jumlah data:
−
    )
+
len(df)
−
```
 
  
−
Baru setelah itu:
+
Missing value:
 +
df.isna().sum()
  
−
```python
+
Duplicate:
−
data = response.json()
+
df.duplicated(subset="uri").sum()
−
```
 
  
−
Ini mencegah error:
+
Statistik engagement:
 +
df["engagement"].describe()
  
−
```text
+
Tampilkan hasil:
−
JSONDecodeError
+
df.head()
−
```
 
  
−
ketika server sebenarnya memberikan HTML error.
+
Kode berhasil dijalankan belum berarti data sudah benar. Kita tetap harus memeriksa hasil.
  
−
---
+
===Simpan===
 +
<pre>
 +
df.to_csv(
 +
    "bluesky_climate_change_clean.csv",
 +
    index=False,
 +
    encoding="utf-8"
 +
)
 +
</pre>
  
−
# H. Langkah 7 — Ambil daftar posting
+
==Kode lengkap==
 +
<pre>
 +
import pandas as pd
 +
import re
  
−
```python
+
# ==========================================
−
posts = data.get(
+
# 1. LOAD DATA
−
    "posts",
+
# ==========================================
−
    []
 
−
)
 
  
−
print(
+
df = pd.read_csv(
−
     "Jumlah posting:",
+
     "bluesky_climate_change_raw.csv"
−
    len(posts)
 
 
)
 
)
−
```
 
  
−
Target:
 
  
−
```text
+
# ==========================================
−
Jumlah posting: 100
+
# 2. DUPLICATE
−
```
+
# ==========================================
  
−
Jika hasilnya misalnya:
+
df = df.drop_duplicates(
 +
    subset="uri"
 +
)
  
−
```text
 
−
Jumlah posting: 86
 
−
```
 
  
−
jangan mengubahnya menjadi 100, biarkan saja.
+
# ==========================================
 +
# 3. MISSING DISPLAY NAME
 +
# ==========================================
  
−
Itu berarti API memang hanya mengembalikan 86 pada request tersebut.
+
df["display_name"] = (
 +
    df["display_name"]
 +
    .fillna(df["username"])
 +
    .astype(str)
 +
    .str.strip()
 +
)
  
−
---
+
df["username"] = (
 +
    df["username"]
 +
    .astype(str)
 +
    .str.strip()
 +
    .str.lower()
 +
)
  
−
# I. Langkah 8 — Lihat bentuk JSON
 
  
−
Sekarang:
+
# ==========================================
 +
# 4. DATE / TIME
 +
# ==========================================
  
−
```python
+
df["created_at"] = pd.to_datetime(
−
print(
+
    df["created_at"],
−
     data.keys()
+
    format="mixed",
 +
    utc=True,
 +
     errors="coerce"
 
)
 
)
−
```
 
  
−
Lalu:
+
df["created_at_wib"] = (
−
 
+
     df["created_at"]
−
```python
+
    .dt.tz_convert("Asia/Jakarta")
−
print(
 
−
     posts[0].keys()
 
 
)
 
)
−
```
 
  
−
Dan lihat posting pertama:
 
  
−
```python
+
# ==========================================
−
posts[0]
+
# 5. LANGUAGE
−
```
+
# ==========================================
  
−
Mahasiswa kemungkinan akan melihat struktur yang cukup kompleks.
+
df["language_primary"] = (
 +
    df["language"]
 +
    .astype(str)
 +
    .str.extract(
 +
        r"([A-Za-z]+(?:-[A-Za-z]+)?)",
 +
        expand=False
 +
    )
 +
    .fillna("unknown")
 +
)
  
−
Secara sederhana:
 
  
−
```text
+
# ==========================================
−
post
+
# 6. NUMERIC VARIABLES
−
│
+
# ==========================================
−
├── uri
 
−
├── cid
 
−
│
 
−
├── author
 
−
│  ├── handle
 
−
│  ├── displayName
 
−
│  └── ...
 
−
│
 
−
├── record
 
−
│  ├── text
 
−
│  ├── createdAt
 
−
│  ├── langs
 
−
│  └── ...
 
−
│
 
−
├── replyCount
 
−
├── repostCount
 
−
├── likeCount
 
−
└── ...
 
−
```
 
  
−
> **Data dari API belum tentu berbentuk tabel seperti Excel.**
+
kolom_angka = [
 +
    "reply_count",
 +
    "repost_count",
 +
    "like_count"
 +
]
  
−
Data API sering berbentuk:
+
for kolom in kolom_angka:
  
−
```text
+
    df[kolom] = pd.to_numeric(
−
JSON
+
        df[kolom],
−
     ↓
+
        errors="coerce"
−
nested / bertingkat
+
     ).fillna(0).astype(int)
−
```
 
  
−
---
 
  
−
# J. Langkah 9 — Simpan JSON mentah terlebih dahulu
+
# ==========================================
 +
# 7. INFORMATION FROM ORIGINAL TEXT
 +
# ==========================================
  
−
**Lakukan ini sebelum cleaning.**
+
df["has_url"] = (
−
 
+
     df["text"]
−
```python
+
     .str.contains(
−
json_file = (
+
        r"https?://|www\.",
−
     RAW_DIR /
+
        regex=True,
−
     "bluesky_climate_change_raw.json"
+
        na=False
 +
    )
 
)
 
)
  
−
with open(
+
df["has_hashtag"] = (
−
    json_file,
+
     df["text"]
−
    "w",
+
     .str.contains(
−
     encoding="utf-8"
+
         r"#\w+",
−
) as f:
+
         regex=True,
−
 
+
         na=False
−
     json.dump(
 
−
         data,
 
−
         f,
 
−
         ensure_ascii=False,
 
−
        indent=2
 
 
     )
 
     )
−
 
−
print(
 
−
    "Tersimpan:",
 
−
    json_file
 
 
)
 
)
−
```
 
  
−
Sekarang kita memiliki:
 
  
−
```text
+
# ==========================================
−
data/raw/
+
# 8. TEXT CLEANING
−
└── bluesky_climate_change_raw.json
+
# ==========================================
−
```
 
  
−
File ini **jangan diedit**.
+
df["text_clean"] = (
 +
    df["text"]
 +
    .fillna("")
 +
    .astype(str)
 +
)
  
−
Anggap sebagai:
+
# Remove URL
−
 
+
df["text_clean"] = (
−
> salinan data asli yang diterima dari API.
+
    df["text_clean"]
−
 
+
    .str.replace(
−
---
+
        r"https?://\S+|www\.\S+",
−
 
+
        " ",
−
# K. Langkah 10 — Pilih field yang akan dipelajari mahasiswa
+
        regex=True
−
 
+
    )
−
Sekarang JSON bertingkat kita ubah menjadi tabel.
+
)
−
 
 
−
```python
 
−
rows = []
 
−
 
 
−
for post in posts:
 
  
−
     record = post.get(
+
# Remove mentions
−
         "record",
+
df["text_clean"] = (
−
         {}
+
     df["text_clean"]
 +
    .str.replace(
 +
         r"@[\w.-]+",
 +
         " ",
 +
        regex=True
 
     )
 
     )
 +
)
  
−
     author = post.get(
+
# Keep hashtag word, remove #
−
         "author",
+
df["text_clean"] = (
−
         {}
+
    df["text_clean"]
 +
     .str.replace(
 +
        r"#(\w+)",
 +
         r"\1",
 +
         regex=True
 
     )
 
     )
 +
)
  
−
     rows.append({
+
# Lowercase
 +
df["text_clean"] = (
 +
     df["text_clean"]
 +
    .str.lower()
 +
)
  
−
        "uri":
+
# Remove extra whitespace
−
            post.get("uri"),
+
df["text_clean"] = (
−
 
+
    df["text_clean"]
−
         "created_at":
+
    .str.replace(
−
            record.get("createdAt"),
+
         r"\s+",
−
 
+
        " ",
−
        "username":
+
        regex=True
−
            author.get("handle"),
+
    )
 +
    .str.strip()
 +
)
  
−
        "display_name":
 
−
            author.get("displayName"),
 
  
−
        "text":
+
# ==========================================
−
            record.get("text"),
+
# 9. DERIVED VARIABLES
 +
# ==========================================
  
−
        "language":
+
df["engagement"] = (
−
            record.get("langs"),
+
    df["reply_count"]
−
 
+
    + df["repost_count"]
−
        "reply_count":
+
    + df["like_count"]
−
            post.get(
 
−
                "replyCount",
 
−
                0
 
−
            ),
 
−
 
 
−
        "repost_count":
 
−
            post.get(
 
−
                "repostCount",
 
−
                0
 
−
            ),
 
−
 
 
−
        "like_count":
 
−
            post.get(
 
−
                "likeCount",
 
−
                0
 
−
            )
 
−
    })
 
−
```
 
−
 
 
−
Kemudian:
 
−
 
 
−
```python
 
−
df_raw = pd.DataFrame(
 
−
    rows
 
 
)
 
)
−
```
 
  
−
---
+
df["word_count"] = (
−
 
+
     df["text_clean"]
−
# L. Langkah 11 — Lihat hasil tabel
+
    .str.findall(
−
 
+
        r"\b[\w’'-]+\b"
−
```python
+
    )
−
df_raw.head()
+
    .str.len()
−
```
 
−
 
 
−
Kurang lebih mahasiswa akan memperoleh:
 
−
 
 
−
| created_at | username | text              | language | reply_count | repost_count | like_count |
 
−
| ---------- | -------- | ----------------- | -------- | ----------: | -----------: | ---------: |
 
−
| ...        | user1    | Climate change... | ['en']   |          1 |            3 |        12 |
 
−
| ...        | user2    | New report...     | ['en']   |          0 |            1 |          6 |
 
−
| ...        | user3    | ...              | ['en']   |          2 |            0 |          9 |
 
−
 
 
−
Kemudian:
 
−
 
 
−
```python
 
−
df_raw.shape
 
−
```
 
−
 
 
−
Harapannya:
 
−
 
 
−
```text
 
−
(100, 9)
 
−
```
 
−
 
 
−
---
 
−
 
 
−
# M. Langkah 12 — Periksa kondisi data, tetapi jangan dibersihkan dahulu
 
−
 
 
−
Jalankan:
 
−
 
 
−
```python
 
−
df_raw.info()
 
−
```
 
−
 
 
−
Kemudian:
 
−
 
 
−
```python
 
−
df_raw.isna().sum()
 
−
```
 
−
 
 
−
Cek juga:
 
−
 
 
−
```python
 
−
df_raw.head(10)
 
−
```
 
−
 
 
−
Dan:
 
−
 
 
−
```python
 
−
df_raw["text"].head(10)
 
−
```
 
−
 
 
−
Pada tahap ini mahasiswa hanya **mengamati**.
 
−
 
 
−
Belum:
 
−
 
 
−
```python
 
−
dropna()
 
−
drop_duplicates()
 
−
replace()
 
−
```
 
−
 
 
−
dan sebagainya.
 
−
 
 
−
---
 
−
 
 
−
# N. Langkah 13 — Simpan CSV mentah
 
−
 
 
−
```python
 
−
csv_file = (
 
−
    RAW_DIR /
 
−
    "bluesky_climate_change_raw.csv"
 
 
)
 
)
  
−
df_raw.to_csv(
+
df["date"] = (
−
     csv_file,
+
     df["created_at_wib"]
−
     index=False,
+
     .dt.date
−
    encoding="utf-8-sig"
 
 
)
 
)
  
−
print(
+
df["hour"] = (
−
     "Tersimpan:",
+
     df["created_at_wib"]
−
     csv_file
+
     .dt.hour
 
)
 
)
−
```
 
  
−
Hasil akhirnya:
 
  
−
```text
+
# ==========================================
−
BigDataFisip/
+
# 10. VALIDATION
−
└── data/
+
# ==========================================
−
    │
 
−
    ├── raw/
 
−
    │  ├── bluesky_climate_change_raw.json
 
−
    │  └── bluesky_climate_change_raw.csv
 
−
    │
 
−
    └── processed/
 
−
```
 
  
−
Saya menggunakan:
+
print("Jumlah data:", len(df))
  
−
```python
+
print(
−
encoding="utf-8-sig"
+
    "\nMissing value:"
−
```
+
)
  
−
supaya karakter seperti emoji dan karakter non-ASCII relatif aman jika CSV nantinya dibuka menggunakan Excel.
+
print(
−
 
+
    df.isna().sum()
−
---
 
−
 
 
−
# O. Perbedaan dua file tersebut
 
−
 
 
−
 
 
−
### `raw.json`
 
−
 
 
−
```text
 
−
Bluesky API
 
−
    ↓
 
−
  JSON asli
 
−
```
 
−
 
 
−
Belum kita ubah strukturnya.
 
−
 
 
−
### `raw.csv`
 
−
 
 
−
```text
 
−
JSON
 
−
↓
 
−
pilih field
 
−
↓
 
−
tabel
 
−
```
 
−
 
 
−
Sudah mengalami **transformasi struktur**, tetapi belum mengalami cleaning isi.
 
−
 
 
−
---
 
−
 
 
−
# P. Sekarang mahasiswa mendapatkan dataset untuk cleaning
 
−
 
 
−
Setelah raw CSV tersedia, praktik berikutnya bisa dimulai.
 
−
 
 
−
### 1. Cek tipe data
 
−
 
 
−
```python
 
−
df_raw.info()
 
−
```
 
−
 
 
−
---
 
−
 
 
−
### 2. Cari missing value
 
−
 
 
−
```python
 
−
df_raw.isna().sum()
 
−
```
 
−
 
 
−
Mahasiswa menentukan:
 
−
 
 
−
```text
 
−
mana yang kosong?
 
−
kenapa bisa kosong?
 
−
perlukah dihapus?
 
−
```
 
−
 
 
−
---
 
−
 
 
−
### 3. Periksa duplikasi
 
−
 
 
−
```python
 
−
df_raw.duplicated().sum()
 
−
```
 
−
 
 
−
Lebih baik juga:
 
−
 
 
−
```python
 
−
df_raw.duplicated(
 
−
    subset=["uri"]
 
−
).sum()
 
−
```
 
−
 
 
−
Diskusikan:
 
−
 
 
−
> Mengapa `uri` lebih cocok untuk memeriksa posting yang sama dibandingkan seluruh row?
 
−
 
 
−
---
 
−
 
 
−
### 4. Periksa teks
 
−
 
 
−
```python
 
−
display(
 
−
    df_raw[["text"]]
 
−
    .head(20)
 
 
)
 
)
−
```
 
  
−
Kemungkinan ditemukan:
+
print(
−
 
+
     "\nDuplicate URI:",
−
```text
+
    df.duplicated(
−
URL
+
        subset="uri"
−
emoji
+
    ).sum()
−
hashtag
 
−
newline
 
−
spasi berlebih
 
−
mention
 
−
```
 
−
 
 
−
Itulah bahan untuk text cleaning.
 
−
 
 
−
---
 
−
 
 
−
### 5. Periksa relevansi
 
−
 
 
−
 
 
−
Cari:
 
−
 
 
−
```python
 
−
df_raw[
 
−
     ["text"]
 
−
].head(20)
 
−
```
 
−
 
 
−
Tanyakan:
 
−
 
 
−
> Apakah semuanya benar-benar membahas climate change?
 
−
 
 
−
Tidak boleh otomatis diasumsikan iya, karena dokumentasi Bluesky sendiri menyatakan sintaks dan perilaku query tidak sepenuhnya dispesifikasikan.
 
−
 
 
−
Ini bisa menjadi contoh nyata:
 
−
 
 
−
```text
 
−
Search API
 
−
    ↓
 
−
candidate data
 
−
 
 
−
bukan
 
−
 
 
−
Search API
 
−
    ↓
 
−
ground truth
 
−
```
 
−
 
 
−
---
 
−
 
 
−
# Q. Tugas cleaning
 
−
 
 
−
Mahasiswa diminta menghasilkan:
 
−
 
 
−
```text
 
−
bluesky_climate_change_clean.csv
 
−
```
 
−
 
 
−
dari:
 
−
 
 
−
```text
 
−
bluesky_climate_change_raw.csv
 
−
```
 
−
 
 
−
Dengan tugas:
 
−
 
 
−
**1. Konversi tanggal**
 
−
 
 
−
```text
 
−
created_at
 
−
string → datetime
 
−
```
 
−
 
 
−
**2. Missing value**
 
−
 
 
−
Identifikasi dan tentukan perlakuannya.
 
−
 
 
−
**3. Duplikasi**
 
−
 
 
−
Gunakan `uri` untuk membantu mendeteksi posting yang sama.
 
−
 
 
−
**4. Cleaning text**
 
−
 
 
−
Misalnya:
 
−
 
 
−
```text
 
−
newline
 
−
spasi berlebih
 
−
URL
 
−
```
 
−
 
 
−
Namun mahasiswa harus menjelaskan **mengapa suatu elemen dihapus**, bukan sekadar menghapus semuanya.
 
−
 
 
−
**5. Relevansi topik**
 
−
 
 
−
Periksa apakah posting benar-benar terkait perubahan iklim.
 
−
 
 
−
**6. Engagement**
 
−
 
 
−
Buat variabel:
 
−
 
 
−
```text
 
−
engagement
 
−
=
 
−
reply_count
 
−
+
 
−
repost_count
 
−
+
 
−
like_count
 
−
```
 
−
 
 
−
---
 
−
 
 
−
# R. Kode lengkap pengambilan data
 
−
 
 
−
```python
 
−
import json
 
−
import requests
 
−
import pandas as pd
 
−
 
 
−
from pathlib import Path
 
−
 
 
−
 
 
−
# ========================================
 
−
# 1. Folder project
 
−
# ========================================
 
−
 
 
−
PROJECT = Path(
 
−
    "/media/bgoode/Data4/Unpad/Kuliah/BigDataFisip"
 
 
)
 
)
  
−
RAW_DIR = PROJECT / "data" / "raw"
+
print(
−
 
+
     "\nStatistik engagement:"
−
RAW_DIR.mkdir(
 
−
     parents=True,
 
−
    exist_ok=True
 
 
)
 
)
  
−
 
+
print(
−
# ========================================
+
     df["engagement"].describe()
−
# 2. API
 
−
# ========================================
 
−
 
 
−
url = (
 
−
     "https://api.bsky.app/"
 
−
    "xrpc/app.bsky.feed.searchPosts"
 
 
)
 
)
  
−
params = {
 
−
    "q": '"climate change"',
 
−
    "limit": 100,
 
−
    "sort": "latest",
 
−
    "lang": "en"
 
−
}
 
  
 +
# ==========================================
 +
# 11. SAVE
 +
# ==========================================
  
−
# ========================================
+
df.to_csv(
−
# 3. Request
+
     "bluesky_climate_change_clean.csv",
−
# ========================================
+
     index=False,
−
 
+
     encoding="utf-8"
−
response = requests.get(
 
−
     url,
 
−
     params=params,
 
−
     headers={
 
−
        "Accept":
 
−
            "application/json",
 
−
 
 
−
        "User-Agent":
 
−
            "BigDataFisip-Class/1.0"
 
−
    },
 
−
    timeout=30
 
 
)
 
)
−
 
  
 
print(
 
print(
−
     "HTTP Status:",
+
     "\nData bersih berhasil disimpan."
−
    response.status_code
 
 
)
 
)
 +
</pre>
  
 +
==Analisis yang bisa dilakukan==
 +
===Seberapa tinggi engagement?===
 +
df["engagement"].describe()
  
−
# ========================================
+
===Posting dengan engagement tertinggi===
−
# 4. Validasi response
+
<pre>
−
# ========================================
+
top10 = (
−
 
+
    df.sort_values(
−
if response.status_code != 200:
+
         "engagement",
−
 
+
         ascending=False
−
    raise RuntimeError(
 
−
         f"HTTP {response.status_code}: "
 
−
         f"{response.text[:300]}"
 
 
     )
 
     )
 +
    [
 +
        [
 +
            "username",
 +
            "text_clean",
 +
            "reply_count",
 +
            "repost_count",
 +
            "like_count",
 +
            "engagement"
 +
        ]
 +
    ]
 +
    .head(10)
 +
)
  
 +
top10
 +
</pre>
  
−
data = response.json()
+
===Visualisasi Top 10 Engagement===
 +
<pre>
 +
import matplotlib.pyplot as plt
  
−
posts = data.get(
+
top10_plot = (
−
     "posts",
+
    df.nlargest(
−
     []
+
        10,
 +
        "engagement"
 +
    )
 +
     .sort_values(
 +
        "engagement"
 +
     )
 
)
 
)
  
−
print(
+
top10_plot.plot(
−
     "Jumlah posting:",
+
     x="username",
−
     len(posts)
+
     y="engagement",
 +
    kind="barh",
 +
    legend=False
 
)
 
)
  
 +
plt.title(
 +
    "10 Posting dengan Engagement Tertinggi"
 +
)
  
−
# ========================================
+
plt.xlabel(
−
# 5. Simpan JSON asli
+
    "Engagement"
−
# ========================================
+
)
  
−
json_file = (
+
plt.ylabel(
−
    RAW_DIR /
+
     "Akun"
−
     "bluesky_climate_change_raw.json"
 
 
)
 
)
  
−
with open(
+
plt.show()
−
    json_file,
+
</pre>
−
    "w",
 
−
    encoding="utf-8"
 
−
) as f:
 
  
−
    json.dump(
+
===Akun paling aktif===
−
        data,
+
<pre>
−
        f,
+
df[
−
        ensure_ascii=False,
+
    "username"
−
        indent=2
+
].value_counts().head(10)
−
    )
+
</pre>
  
 +
===Aktivitas berdasarkan jam===
 +
<pre>
 +
posting_per_jam = (
 +
    df["hour"]
 +
    .value_counts()
 +
    .sort_index()
 +
)
  
−
# ========================================
+
posting_per_jam
−
# 6. Ubah ke tabel
+
</pre>
−
# ========================================
 
  
−
rows = []
+
Visualisasi
 +
<pre>
 +
posting_per_jam.plot(
 +
    kind="bar"
 +
)
  
−
for post in posts:
+
plt.title(
 +
    "Jumlah Posting per Jam"
 +
)
  
−
    record = post.get(
+
plt.xlabel(
−
        "record",
+
    "Jam (WIB)"
−
        {}
+
)
−
    )
 
  
−
    author = post.get(
+
plt.ylabel(
−
        "author",
+
    "Jumlah Posting"
−
        {}
+
)
−
    )
 
  
−
    rows.append({
+
plt.show()
 +
</pre>
  
−
        "uri":
+
===Kata yang sering muncul===
−
            post.get("uri"),
+
<pre>
 +
from collections import Counter
  
−
        "created_at":
+
semua_teks = " ".join(
−
            record.get("createdAt"),
+
    df["text_clean"]
 +
)
  
−
        "username":
+
kata = re.findall(
−
            author.get("handle"),
+
    r"[a-zA-Z][a-zA-Z'-]+",
 +
    semua_teks
 +
)
  
−
        "display_name":
+
stopwords = {
−
            author.get("displayName"),
+
    "the", "and", "to", "of",
 +
    "a", "in", "is", "for",
 +
    "that", "it", "on", "with",
 +
    "are", "be", "as", "this",
 +
    "we", "i", "you", "they",
 +
    "have", "has", "was",
 +
    "were", "not", "but",
  
−
        "text":
+
    # kata pencarian
−
            record.get("text"),
+
    "climate",
 +
    "change",
 +
    "climatechange"
 +
}
  
−
        "language":
+
kata_bersih = [
−
            record.get("langs"),
+
    k
 +
    for k in kata
 +
    if k not in stopwords
 +
    and len(k) > 2
 +
]
  
−
        "reply_count":
+
frekuensi = Counter(
−
            post.get(
+
    kata_bersih
−
                "replyCount",
+
)
−
                0
 
−
            ),
 
  
−
        "repost_count":
+
frekuensi.most_common(20)
−
            post.get(
+
</pre>
−
                "repostCount",
 
−
                0
 
−
            ),
 
−
 
 
−
        "like_count":
 
−
            post.get(
 
−
                "likeCount",
 
−
                0
 
−
            )
 
−
    })
 
  
 +
===Visualisasi 15 kata teratas===
 +
<pre>
 +
freq_df = pd.DataFrame(
 +
    frekuensi.most_common(15),
 +
    columns=[
 +
        "word",
 +
        "frequency"
 +
    ]
 +
)
  
−
df_raw = pd.DataFrame(
+
freq_df = freq_df.sort_values(
−
     rows
+
     "frequency"
 
)
 
)
  
−
 
+
freq_df.plot(
−
# ========================================
+
    x="word",
−
# 7. Simpan CSV raw
+
    y="frequency",
−
# ========================================
+
     kind="barh",
−
 
+
     legend=False
−
csv_file = (
 
−
     RAW_DIR /
 
−
     "bluesky_climate_change_raw.csv"
 
 
)
 
)
  
−
df_raw.to_csv(
+
plt.title(
−
     csv_file,
+
     "15 Kata yang Sering Muncul"
−
    index=False,
 
−
    encoding="utf-8-sig"
 
 
)
 
)
  
−
 
+
plt.xlabel(
−
# ========================================
+
     "Frekuensi"
−
# 8. Pemeriksaan
 
−
# ========================================
 
−
 
 
−
print(
 
−
     "Ukuran dataset:",
 
−
    df_raw.shape
 
 
)
 
)
  
−
display(
+
plt.ylabel(
−
     df_raw.head()
+
     "Kata"
 
)
 
)
−
```
 
  
−
---
+
plt.show()
 +
</pre>
  
−
```text
 
−
100 data mentah
 
−
      ↓
 
−
identifikasi masalah
 
−
      ↓
 
−
data cleaning
 
−
      ↓
 
−
data processed
 
−
      ↓
 
−
analisis
 
−
```
 
  
−
[1]: https://bsky.network/docs/api-directory/?utm_source=chatgpt.com "API Hosts and Auth | Bluesky Protocol Services"
+
==Link Terkait==
 +
* [[Big Data Fisip]]

Latest revision as of 14:05, 30 September 2026

Dari contoh yang didapat, kita akan melakukan hal berikut

bluesky_climate_change_raw.csv
            ↓
      DATA CLEANING
            ↓
      PREPROCESSING
            ↓
bluesky_climate_change_clean.csv
            ↓
        ANALISIS

Step by step nya adalah

Data Cleaning

Import library dan membaca data

import pandas as pd
import re

df = pd.read_csv(
    "bluesky_climate_change_raw.csv"
)

df.head()

Cek ukuran data

df.shape

Klo di contoh saya nanti hasilnya adalah (97, 9). Artinya: 97 baris × 9 variabel.

Cek nama variabel

df.columns


Cek kondisi data

df.info()
df.head()
df.sample(5)
df.isna().sum()

Untuk kasus saya, ada 15 posting tidak mempunyai display_name, namun kita cek pakah masih memiliki username

df["display_name"] = (
    df["display_name"]
    .fillna(df["username"])
)

Kemudian cek lagi

df["display_name"].isna().sum()

Harus menghasilkan 0

Berarti dalam kasus ini display_name tidak terlalu penting karena masih ada username.

Cek apakah ada duplicate

df.duplicated().sum()

Klo pada kasus saya, id unik nya adalah uri,jadi

df.duplicated(
    subset="uri"
).sum()

Kode cleaning nya adalah

df = df.drop_duplicates(
    subset="uri"
)

Kalau satu posting masuk dua kali, analisis kita dapat menghitung satu opini atau satu engagement dua kali.

Rapihkan display_name

Beberapa nama mungkin memiliki spasi tambahan.

df["display_name"] = (
    df["display_name"]
    .astype(str)
    .str.strip()
)

Kita rapihkan juga username

df["username"] = (
    df["username"]
    .astype(str)
    .str.strip()
    .str.lower()
)

Perbaiki type tanggal

Kita cek tipe datanya

df["created_at"].dtype

Jika hasilnya = object, berarti masih dianggap teks, jadi harus diubah ke tipe waktu.

df["created_at"] = pd.to_datetime(
    df["created_at"],
    format="mixed",
    utc=True,
    errors="coerce"
)

Ubah UTC ke waktu Jakarta

df["created_at_wib"] = (
    df["created_at"]
    .dt.tz_convert("Asia/Jakarta")
)

Kemudian cek

df[
    ["created_at", "created_at_wib"]
].head()

Rapihkan kolom bahasa

df["language"].value_counts()

Perbaiki

df["language_primary"] = (
    df["language"]
    .astype(str)
    .str.extract(
        r"([A-Za-z]+(?:-[A-Za-z]+)?)",
        expand=False
    )
    .fillna("unknown")
)

Cek

df["language_primary"].value_counts()

Pastikan reply, repost dan like benar-benar numerik

Cek

df[
    [
        "reply_count",
        "repost_count",
        "like_count"
    ]
].dtypes

Jika masih belum angka

kolom_angka = [
    "reply_count",
    "repost_count",
    "like_count"
]

for kolom in kolom_angka:
    df[kolom] = pd.to_numeric(
        df[kolom],
        errors="coerce"
    ).fillna(0).astype(int)

Jika ada URL terambil

Buat variabel, barangkali nanti url tersebut dibutuhkan

df["has_url"] = (
    df["text"]
    .str.contains(
        r"https?://|www\.",
        regex=True,
        na=False
    )
)

Cek

df["has_url"].value_counts()

Deteksi Hastag

df["has_hashtag"] = (
    df["text"]
    .str.contains(
        r"#\w+",
        regex=True,
        na=False
    )
)

Cek

df["has_hashtag"].value_counts()

Preprocessing

Buat Clean Text

Kita mulai masuk ke preprocessing

df["text_clean"] = (
    df["text"]
    .fillna("")
    .astype(str)
)

Hapus URL

df["text_clean"] = (
    df["text_clean"]
    .str.replace(
        r"https?://\S+|www\.\S+",
        " ",
        regex=True
    )
)

Nanti hasilnya misal: Climate change is real https://example.com -> Climate change is real

Hilangkan mention

df["text_clean"] = (
    df["text_clean"]
    .str.replace(
        r"@[\w.-]+",
        " ",
        regex=True
    )
)

Namun mention tidak selalu harus dibuang. Kalau kita ingin melakukan network analysis, mention justru merupakan data yang penting.

Hastag

Jangan buang katanya, namun tanda "#" saja

df["text_clean"] = (
    df["text_clean"]
    .str.replace(
        r"#(\w+)",
        r"\1",
        regex=True
    )
)

Ubah menjadi lowercase

df["text_clean"] = (
    df["text_clean"]
    .str.lower()
)

Ini penting agar komputer tidak menganggap kata yang sama namun misal ada huruf kapital, sebagai kata berbeda.

Rapikan line break dan spasi

df["text_clean"] = (
    df["text_clean"]
    .str.replace(
        r"\s+",
        " ",
        regex=True
    )
    .str.strip()
)

Bandingkan sebelum dan sesudah

df[
    [
        "text",
        "text_clean"
    ]
].head(10)

Buat variabel engagement

Misal kita perlu untuk menghitung hubungannya. Kita definisikan secara sederhana:Engagement = Reply + Repost + Like

df["engagement"] = (
    df["reply_count"]
    + df["repost_count"]
    + df["like_count"]
)

Kita cek

df[
    [
        "reply_count",
        "repost_count",
        "like_count",
        "engagement"
    ]
].head()

Ini adalah definisi operasional untuk latihan, bukan definisi universal engagement. Kalau penelitian sebenarnya, Anda harus menjelaskan mengapa reply, repost, dan like dijumlahkan dan apakah bobotnya memang seharusnya sama.

Buat jumlah kata

df["word_count"] = (
    df["text_clean"]
    .str.findall(
        r"\b[\w’'-]+\b"
    )
    .str.len()
)

Cek

df["word_count"].describe()

Buat variabel tanggal dan jam

df["date"] = (
    df["created_at_wib"]
    .dt.date
)

df["hour"] = (
    df["created_at_wib"]
    .dt.hour
)

Cleaning = memperbaiki data. Preprocessing = membuat data menjadi lebih siap untuk analisis.

Validasi

Jumlah data:

len(df)

Missing value:

df.isna().sum()

Duplicate:

df.duplicated(subset="uri").sum()

Statistik engagement:

df["engagement"].describe()

Tampilkan hasil:

df.head()

Kode berhasil dijalankan belum berarti data sudah benar. Kita tetap harus memeriksa hasil.

Simpan

df.to_csv(
    "bluesky_climate_change_clean.csv",
    index=False,
    encoding="utf-8"
)

Kode lengkap

import pandas as pd
import re

# ==========================================
# 1. LOAD DATA
# ==========================================

df = pd.read_csv(
    "bluesky_climate_change_raw.csv"
)


# ==========================================
# 2. DUPLICATE
# ==========================================

df = df.drop_duplicates(
    subset="uri"
)


# ==========================================
# 3. MISSING DISPLAY NAME
# ==========================================

df["display_name"] = (
    df["display_name"]
    .fillna(df["username"])
    .astype(str)
    .str.strip()
)

df["username"] = (
    df["username"]
    .astype(str)
    .str.strip()
    .str.lower()
)


# ==========================================
# 4. DATE / TIME
# ==========================================

df["created_at"] = pd.to_datetime(
    df["created_at"],
    format="mixed",
    utc=True,
    errors="coerce"
)

df["created_at_wib"] = (
    df["created_at"]
    .dt.tz_convert("Asia/Jakarta")
)


# ==========================================
# 5. LANGUAGE
# ==========================================

df["language_primary"] = (
    df["language"]
    .astype(str)
    .str.extract(
        r"([A-Za-z]+(?:-[A-Za-z]+)?)",
        expand=False
    )
    .fillna("unknown")
)


# ==========================================
# 6. NUMERIC VARIABLES
# ==========================================

kolom_angka = [
    "reply_count",
    "repost_count",
    "like_count"
]

for kolom in kolom_angka:

    df[kolom] = pd.to_numeric(
        df[kolom],
        errors="coerce"
    ).fillna(0).astype(int)


# ==========================================
# 7. INFORMATION FROM ORIGINAL TEXT
# ==========================================

df["has_url"] = (
    df["text"]
    .str.contains(
        r"https?://|www\.",
        regex=True,
        na=False
    )
)

df["has_hashtag"] = (
    df["text"]
    .str.contains(
        r"#\w+",
        regex=True,
        na=False
    )
)


# ==========================================
# 8. TEXT CLEANING
# ==========================================

df["text_clean"] = (
    df["text"]
    .fillna("")
    .astype(str)
)

# Remove URL
df["text_clean"] = (
    df["text_clean"]
    .str.replace(
        r"https?://\S+|www\.\S+",
        " ",
        regex=True
    )
)

# Remove mentions
df["text_clean"] = (
    df["text_clean"]
    .str.replace(
        r"@[\w.-]+",
        " ",
        regex=True
    )
)

# Keep hashtag word, remove #
df["text_clean"] = (
    df["text_clean"]
    .str.replace(
        r"#(\w+)",
        r"\1",
        regex=True
    )
)

# Lowercase
df["text_clean"] = (
    df["text_clean"]
    .str.lower()
)

# Remove extra whitespace
df["text_clean"] = (
    df["text_clean"]
    .str.replace(
        r"\s+",
        " ",
        regex=True
    )
    .str.strip()
)


# ==========================================
# 9. DERIVED VARIABLES
# ==========================================

df["engagement"] = (
    df["reply_count"]
    + df["repost_count"]
    + df["like_count"]
)

df["word_count"] = (
    df["text_clean"]
    .str.findall(
        r"\b[\w’'-]+\b"
    )
    .str.len()
)

df["date"] = (
    df["created_at_wib"]
    .dt.date
)

df["hour"] = (
    df["created_at_wib"]
    .dt.hour
)


# ==========================================
# 10. VALIDATION
# ==========================================

print("Jumlah data:", len(df))

print(
    "\nMissing value:"
)

print(
    df.isna().sum()
)

print(
    "\nDuplicate URI:",
    df.duplicated(
        subset="uri"
    ).sum()
)

print(
    "\nStatistik engagement:"
)

print(
    df["engagement"].describe()
)


# ==========================================
# 11. SAVE
# ==========================================

df.to_csv(
    "bluesky_climate_change_clean.csv",
    index=False,
    encoding="utf-8"
)

print(
    "\nData bersih berhasil disimpan."
)

Analisis yang bisa dilakukan

Seberapa tinggi engagement?

df["engagement"].describe()

Posting dengan engagement tertinggi

top10 = (
    df.sort_values(
        "engagement",
        ascending=False
    )
    [
        [
            "username",
            "text_clean",
            "reply_count",
            "repost_count",
            "like_count",
            "engagement"
        ]
    ]
    .head(10)
)

top10

Visualisasi Top 10 Engagement

import matplotlib.pyplot as plt

top10_plot = (
    df.nlargest(
        10,
        "engagement"
    )
    .sort_values(
        "engagement"
    )
)

top10_plot.plot(
    x="username",
    y="engagement",
    kind="barh",
    legend=False
)

plt.title(
    "10 Posting dengan Engagement Tertinggi"
)

plt.xlabel(
    "Engagement"
)

plt.ylabel(
    "Akun"
)

plt.show()

Akun paling aktif

df[
    "username"
].value_counts().head(10)

Aktivitas berdasarkan jam

posting_per_jam = (
    df["hour"]
    .value_counts()
    .sort_index()
)

posting_per_jam

Visualisasi

posting_per_jam.plot(
    kind="bar"
)

plt.title(
    "Jumlah Posting per Jam"
)

plt.xlabel(
    "Jam (WIB)"
)

plt.ylabel(
    "Jumlah Posting"
)

plt.show()

Kata yang sering muncul

from collections import Counter

semua_teks = " ".join(
    df["text_clean"]
)

kata = re.findall(
    r"[a-zA-Z][a-zA-Z'-]+",
    semua_teks
)

stopwords = {
    "the", "and", "to", "of",
    "a", "in", "is", "for",
    "that", "it", "on", "with",
    "are", "be", "as", "this",
    "we", "i", "you", "they",
    "have", "has", "was",
    "were", "not", "but",

    # kata pencarian
    "climate",
    "change",
    "climatechange"
}

kata_bersih = [
    k
    for k in kata
    if k not in stopwords
    and len(k) > 2
]

frekuensi = Counter(
    kata_bersih
)

frekuensi.most_common(20)

Visualisasi 15 kata teratas

freq_df = pd.DataFrame(
    frekuensi.most_common(15),
    columns=[
        "word",
        "frequency"
    ]
)

freq_df = freq_df.sort_values(
    "frequency"
)

freq_df.plot(
    x="word",
    y="frequency",
    kind="barh",
    legend=False
)

plt.title(
    "15 Kata yang Sering Muncul"
)

plt.xlabel(
    "Frekuensi"
)

plt.ylabel(
    "Kata"
)

plt.show()


Link Terkait