From d5f3f33a83b57b2ee48ff61273034705cd681cac Mon Sep 17 00:00:00 2001 From: MufridaFaraDiani27 Date: Tue, 21 Apr 2026 18:33:47 +0700 Subject: [PATCH] update fitur --- app/Http/Controllers/AnalisisController.php | 2 +- app/Http/Controllers/DashboardController.php | 99 ++++--- .../Controllers/RekomendasiController.php | 2 +- app/Http/Controllers/UlasanController.php | 18 +- resources/views/analisis/index.blade.php | 43 ++- resources/views/dashboard.blade.php | 202 ++++++++------ resources/views/ulasan/index.blade.php | 155 ++++++----- scraper/preprocessing.py | 247 ++++++++++-------- scraper/scraping_pipeline.py | 3 +- 9 files changed, 456 insertions(+), 315 deletions(-) diff --git a/app/Http/Controllers/AnalisisController.php b/app/Http/Controllers/AnalisisController.php index 04bcf50..fd1b0b1 100644 --- a/app/Http/Controllers/AnalisisController.php +++ b/app/Http/Controllers/AnalisisController.php @@ -30,4 +30,4 @@ public function index(Request $request) 'wisataList' )); } -} +} \ No newline at end of file diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 1f90b08..2483567 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -12,89 +12,101 @@ public function index(Request $request) $wisata = $request->wisata; // ============================= - // TOTAL DATA + // TOTAL ULASAN (DARI TABEL ULASAN) // ============================= - $total = DB::table('hasil_analisis') - ->when($wisata, function ($q) use ($wisata) { + $totalUlasan = DB::table('ulasan') + ->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) { $q->where('wisata', $wisata); }) ->count(); // ============================= - // HITUNG SENTIMEN + // CEK ADA ANALISIS ATAU BELUM + // ============================= + $hasAnalisis = DB::table('hasil_analisis')->count() > 0; + + // ============================= + // HITUNG SENTIMEN (DARI HASIL ANALISIS) // ============================= $positif = DB::table('hasil_analisis') ->where('sentimen', 'Positif') - ->when($wisata, function ($q) use ($wisata) { + ->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) { $q->where('wisata', $wisata); }) ->count(); $negatif = DB::table('hasil_analisis') ->where('sentimen', 'Negatif') - ->when($wisata, function ($q) use ($wisata) { + ->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) { $q->where('wisata', $wisata); }) ->count(); $netral = DB::table('hasil_analisis') ->where('sentimen', 'Netral') - ->when($wisata, function ($q) use ($wisata) { + ->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) { $q->where('wisata', $wisata); }) ->count(); // ============================= - // PERSENTASE + // PERSENTASE SENTIMEN // ============================= + $totalSentimen = $positif + $negatif + $netral; + $stats = [ - 'total' => $total, - 'positif_persen' => $total ? round(($positif / $total) * 100) : 0, - 'negatif_persen' => $total ? round(($negatif / $total) * 100) : 0, - 'netral_persen' => $total ? round(($netral / $total) * 100) : 0, + 'total' => $totalUlasan, + 'positif_persen' => $totalSentimen > 0 ? round(($positif / $totalSentimen) * 100) : 0, + 'negatif_persen' => $totalSentimen > 0 ? round(($negatif / $totalSentimen) * 100) : 0, + 'netral_persen' => $totalSentimen > 0 ? round(($netral / $totalSentimen) * 100) : 0, ]; // ============================= // GRAFIK PIE (Distribusi Sentimen) // ============================= - $chartSentimen = DB::table('hasil_analisis') - ->select('sentimen', DB::raw('count(*) as total')) - ->when($wisata, function ($q) use ($wisata) { - $q->where('wisata', $wisata); - }) - ->groupBy('sentimen') - ->get(); + $chartSentimen = $hasAnalisis + ? DB::table('hasil_analisis') + ->select('sentimen', DB::raw('count(*) as total')) + ->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) { + $q->where('wisata', $wisata); + }) + ->groupBy('sentimen') + ->orderByRaw("FIELD(sentimen, 'Positif', 'Negatif', 'Netral')") // 🔥 INI KUNCINYA + ->get() + : collect(); // ============================= - // GRAFIK BAR (per destinasi) + // GRAFIK BAR (Sentimen per Destinasi) // ============================= - $chartDestinasi = DB::table('hasil_analisis') - ->select( - 'wisata', - DB::raw("SUM(CASE WHEN sentimen = 'Positif' THEN 1 ELSE 0 END) as positif"), - DB::raw("SUM(CASE WHEN sentimen = 'Negatif' THEN 1 ELSE 0 END) as negatif"), - DB::raw("SUM(CASE WHEN sentimen = 'Netral' THEN 1 ELSE 0 END) as netral") - ) + $chartDestinasi = $hasAnalisis + ? DB::table('hasil_analisis') + ->select( + 'wisata', + DB::raw("SUM(CASE WHEN sentimen = 'Positif' THEN 1 ELSE 0 END) as positif"), + DB::raw("SUM(CASE WHEN sentimen = 'Negatif' THEN 1 ELSE 0 END) as negatif"), + DB::raw("SUM(CASE WHEN sentimen = 'Netral' THEN 1 ELSE 0 END) as netral") + ) + ->when($wisata && $wisata != 'Semua Destinasi', function ($q) use ($wisata) { + $q->where('wisata', $wisata); + }) + ->groupBy('wisata') + + ->get() + : collect(); + + // ============================= + // TOTAL DATA PER DESTINASI (DARI ULASAN) + // ============================= + $totalPerWisata = DB::table('ulasan') + ->select('wisata', DB::raw('count(*) as total')) ->groupBy('wisata') ->get(); // ============================= - // TOTAL DATA PER DESTINASI + // WORD CLOUD (DARI PREPROCESSING) // ============================= - $totalPerWisata = DB::table('hasil_analisis') - ->select('wisata', DB::raw('count(*) as total')) - ->groupBy('wisata') - ->get() - ->toArray(); - - // ============================= - // WORD CLOUD - // ============================= - $texts = DB::table('preprocessing_data') - ->pluck('stemming') - ->toArray(); // 🔥 WAJIB - - $allText = implode(' ', $texts); + $texts = DB::table('preprocessing_data')->pluck('stemming')->toArray(); + $allText = count($texts) > 0 ? implode(' ', $texts) : ''; // ============================= // LAST UPDATE @@ -109,7 +121,8 @@ public function index(Request $request) 'chartDestinasi', 'totalPerWisata', 'allText', - 'lastUpdate' + 'lastUpdate', + 'hasAnalisis' )); } } \ No newline at end of file diff --git a/app/Http/Controllers/RekomendasiController.php b/app/Http/Controllers/RekomendasiController.php index 69e9c95..377b78d 100644 --- a/app/Http/Controllers/RekomendasiController.php +++ b/app/Http/Controllers/RekomendasiController.php @@ -12,7 +12,7 @@ public function index(Request $request) // dropdown destinasi unik $destinasiList = HasilAnalisis::select('wisata') ->distinct() - ->pluck('scraping_pipeline.py'); + ->pluck('wisata'); // query utama $query = HasilAnalisis::query(); diff --git a/app/Http/Controllers/UlasanController.php b/app/Http/Controllers/UlasanController.php index 35eeb8a..bfc8700 100644 --- a/app/Http/Controllers/UlasanController.php +++ b/app/Http/Controllers/UlasanController.php @@ -25,7 +25,7 @@ public function index(Request $request) $mentah = $query->latest()->paginate(50)->withQueryString(); // preprocessing - $preprocessing = PreprocessingData::latest()->paginate(50); + $preprocessing = PreprocessingData::latest()->paginate(30, ['*'], 'pre_page'); return view('ulasan.index', compact('mentah', 'preprocessing')); } @@ -41,14 +41,14 @@ public function ambilData() ->with('success','Data berhasil diambil'); } - public function analisisData() - { - $pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"'; - $scriptPath = '"C:\\laragon\\www\\Sentara\\scraper\\preprocessing.py"'; + public function analisisData() +{ + $pythonPath = '"C:\\Users\\Mufrida Farah\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"'; + $scriptPath = '"C:\\laragon\\www\\Sentara\\scraper\\preprocessing.py"'; - shell_exec($pythonPath . " " . $scriptPath . " 2>&1"); + $output = shell_exec($pythonPath . " " . $scriptPath . " 2>&1"); - return redirect()->route('ulasan.index') - ->with('success','Analisis berhasil'); - } + return redirect()->route('ulasan.index') + ->with('success','Analisis berhasil'); +} } \ No newline at end of file diff --git a/resources/views/analisis/index.blade.php b/resources/views/analisis/index.blade.php index 3cb34d4..e13f342 100644 --- a/resources/views/analisis/index.blade.php +++ b/resources/views/analisis/index.blade.php @@ -98,9 +98,46 @@ class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm -
- {{ $hasil->links() }} -
+
+ Showing {{ $hasil->firstItem() }} to {{ $hasil->lastItem() }} + of {{ $hasil->total() }} results +
+ +
+ + {{-- PREV --}} + @if ($hasil->onFirstPage()) + ‹ + @else + ‹ + @endif + + @php + $start = max($hasil->currentPage() - 2, 1); + $end = min($hasil->currentPage() + 2, $hasil->lastPage()); + @endphp + + {{-- NUMBER --}} + @for ($i = $start; $i <= $end; $i++) + @if ($i == $hasil->currentPage()) + + {{ $i }} + + @else + + {{ $i }} + + @endif + @endfor + + {{-- NEXT --}} + @if ($hasil->hasMorePages()) + › + @else + › + @endif + +
diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index a7a1c36..c0b7857 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -4,37 +4,12 @@
- {{-- Header --}} -
-
-

- Dashboard -

- - -
- - -
- -
-
+ {{-- ALERT --}} + @if(!$hasAnalisis) +
+ Data ulasan sudah tersedia, tetapi belum dilakukan analisis sentimen. +
+ @endif {{-- Statistik Cards --}}
@@ -69,78 +44,133 @@ class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
- {{-- Chart Section --}} -
+ {{-- Chart Section --}} +
- -
-

- Distribusi Sentimen -

-
- + +
+

Distribusi Sentimen

+
+ +
+ + +
+

Sentimen per Destinasi

+
+ +
+
+
- -
-

- Sentimen per Destinasi -

-
- -
-
+ {{-- Section Bawah --}} +
-
{{-- TUTUP GRID CHART --}} + +
+

+ Total Data per Destinasi +

- -{{-- Section Bawah --}} -
- - -
-

- Total Data per Destinasi -

- -
-
- Pantai Papuma - 120 ulasan -
-
- Pantai Watu Ulo - 85 ulasan -
-
- Teluk Love (Payangan) - 95 ulasan -
-
- Kebun Teh Gunung Gambir - 60 ulasan +
+ @foreach($totalPerWisata as $item) +
+ {{ $item->wisata }} + + {{ $item->total }} ulasan + +
+ @endforeach
-
- -
-

- Word Cloud -

+ +
+

+ Word Cloud +

+
+
-
-
+{{-- ========================= --}} +{{-- SCRIPT SECTION --}} +{{-- ========================= --}} + + -
-
+ @endsection \ No newline at end of file diff --git a/resources/views/ulasan/index.blade.php b/resources/views/ulasan/index.blade.php index 27ffc16..63ae945 100644 --- a/resources/views/ulasan/index.blade.php +++ b/resources/views/ulasan/index.blade.php @@ -12,7 +12,7 @@ - +
@@ -155,82 +155,80 @@

Hasil Preprocessing

-
- - - - - - - - - - @forelse($preprocessing as $row) - - - - - - @empty - - - -
- - -
- Showing {{ $preprocessing->firstItem() }} to {{ $preprocessing->lastItem() }} - of {{ $preprocessing->total() }} results -
+
- -
+ +
+
WisataUlasan MentahUlasan Bersih
{{ $row->wisata }}{{ $row->ulasan_asli }}{{ $row->stemming }}
- Belum ada data -
+ + + + + + + + + @forelse($preprocessing as $row) + + + + + + @empty + + + + @endforelse + +
WisataUlasan MentahUlasan Bersih
{{ $row->wisata }}{{ $row->ulasan_asli }}{{ $row->stemming }}
+ Belum ada data +
+
- {{-- FIRST --}} - @if ($preprocessing->currentPage() > 1) - « - @endif + +
+ Showing {{ $preprocessing->firstItem() }} to {{ $preprocessing->lastItem() }} + of {{ $preprocessing->total() }} results +
- {{-- PREV --}} - @if ($preprocessing->onFirstPage()) - ‹ - @else - ‹ - @endif + +
- @php - $start = max($preprocessing->currentPage() - 2, 1); - $end = min($preprocessing->currentPage() + 2, $preprocessing->lastPage()); - @endphp + {{-- PREV --}} + @if ($preprocessing->onFirstPage()) + ‹ + @else + ‹ + @endif - {{-- NUMBER DINAMIS --}} - @for ($i = $start; $i <= $end; $i++) - @if ($i == $preprocessing->currentPage()) - {{ $i }} - @else - - {{ $i }} - - @endif - @endfor + @php + $start = max($preprocessing->currentPage() - 2, 1); + $end = min($preprocessing->currentPage() + 2, $preprocessing->lastPage()); + @endphp - {{-- NEXT --}} - @if ($preprocessing->hasMorePages()) - › - @else - › - @endif + {{-- NUMBER --}} + @for ($i = $start; $i <= $end; $i++) + @if ($i == $preprocessing->currentPage()) + + {{ $i }} + + @else + + {{ $i }} + + @endif + @endfor - {{-- LAST --}} - @if ($preprocessing->currentPage() < $preprocessing->lastPage()) - » - @endif + {{-- NEXT --}} + @if ($preprocessing->hasMorePages()) + › + @else + › + @endif -
+
-
+
- @endforelse + + + @endsection \ No newline at end of file diff --git a/scraper/preprocessing.py b/scraper/preprocessing.py index 0e9c20e..f32b866 100644 --- a/scraper/preprocessing.py +++ b/scraper/preprocessing.py @@ -1,8 +1,20 @@ +import pandas as pd import mysql.connector import re -import math -from collections import Counter +import string +import matplotlib.pyplot as plt +from sklearn.utils import resample + + from Sastrawi.Stemmer.StemmerFactory import StemmerFactory +from Sastrawi.StopWordRemover.StopWordRemoverFactory import StopWordRemoverFactory + +from sklearn.model_selection import train_test_split +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.naive_bayes import MultinomialNB +from sklearn.metrics import accuracy_score, classification_report + +from wordcloud import WordCloud # ============================= # DATABASE @@ -14,130 +26,161 @@ conn = mysql.connector.connect( password="" ) -cursor = conn.cursor() +query = "SELECT wisata, ulasan, rating FROM ulasan" +df = pd.read_sql(query, conn) + +print("Total data:", len(df)) # ============================= -# STEMMER +# STEMMER & STOPWORD # ============================= factory = StemmerFactory() stemmer = factory.create_stemmer() -# ============================= -# DATA TRAINING -# ============================= -data_train = [ - ("bagus bersih indah nyaman sejuk", "positif"), - ("indah nyaman segar bagus", "positif"), - ("kotor jelek rusak mahal sempit", "negatif"), - ("jalan rusak sempit mahal", "negatif") -] +factory_stop = StopWordRemoverFactory() +stopword = factory_stop.create_stop_word_remover() # ============================= -# TRAIN NAIVE BAYES +# PREPROCESSING # ============================= -def train_nb(data): - word_counts = {"positif": Counter(), "negatif": Counter()} - class_counts = {"positif": 0, "negatif": 0} - total_words = {"positif": 0, "negatif": 0} +# kamus slang +slang_dict = { + "gk": "tidak", + "ga": "tidak", + "bgt": "banget", + "tp": "tapi", + "dr": "dari", + "yg": "yang" +} - for text, label in data: - words = text.split() - class_counts[label] += 1 - word_counts[label].update(words) - total_words[label] += len(words) - - return word_counts, class_counts, total_words - -word_counts, class_counts, total_words = train_nb(data_train) - -# ============================= -# PREDICT -# ============================= -def predict_nb(text): +def normalize_slang(text): words = text.split() + return ' '.join([slang_dict.get(w, w) for w in words]) - vocab = set(word_counts["positif"]).union(set(word_counts["negatif"])) - log_pos = math.log(class_counts["positif"] / sum(class_counts.values())) - log_neg = math.log(class_counts["negatif"] / sum(class_counts.values())) +def preprocess(text): + text = str(text) + text = text.lower() - for word in words: - prob_pos = (word_counts["positif"][word] + 1) / (total_words["positif"] + len(vocab)) - prob_neg = (word_counts["negatif"][word] + 1) / (total_words["negatif"] + len(vocab)) + # cleaning + text = re.sub(r'[^a-zA-Z\s]', '', text) - log_pos += math.log(prob_pos) - log_neg += math.log(prob_neg) + # 🔥 TAMBAHKAN DI SINI + text = normalize_slang(text) - if log_pos > log_neg: - return "Positif", log_pos + # stopword + text = stopword.remove(text) + + # stemming + text = stemmer.stem(text) + + return text + +def preprocess(text): + text = str(text) + text = text.lower() + text = re.sub(r'[^a-zA-Z\s]', '', text) + text = stopword.remove(text) + text = stemmer.stem(text) + return text + +df['clean_text'] = df['ulasan'].apply(preprocess) + +# ============================= +# LABELING +# ============================= +def label_sentimen(rating): + if rating >= 4: + return "positif" + elif rating == 3: + return "negatif" # 🔥 ubah ini else: - return "Negatif", log_neg + return "negatif" + +df['label'] = df['rating'].apply(label_sentimen) + +print("\nDistribusi Label:") +print(df['label'].value_counts()) + # ============================= -# HAPUS DATA LAMA (PENTING!) +# SPLIT DATA # ============================= +X_train, X_test, y_train, y_test = train_test_split( + df['clean_text'], + df['label'], + test_size=0.2, + random_state=42 +) + +# ============================= +# TF-IDF +# ============================= +vectorizer = TfidfVectorizer() +X_train_tfidf = vectorizer.fit_transform(X_train) +X_test_tfidf = vectorizer.transform(X_test) + +# ============================= +# MODEL NAIVE BAYES +# ============================= +model = MultinomialNB() +model.fit(X_train_tfidf, y_train) + +# ============================= +# EVALUASI +# ============================= +y_pred = model.predict(X_test_tfidf) + +print("\nAccuracy:", accuracy_score(y_test, y_pred)) +print("\nClassification Report:\n", classification_report(y_test, y_pred)) + +# ============================= +# PREDIKSI SEMUA DATA +# ============================= +print("\nMulai prediksi semua data...") + +X_all_tfidf = vectorizer.transform(df['clean_text']) +df['prediksi'] = model.predict(X_all_tfidf) + +# ============================= +# SIMPAN KE DATABASE +# ============================= +cursor = conn.cursor() + cursor.execute("DELETE FROM preprocessing_data") cursor.execute("DELETE FROM hasil_analisis") -# ============================= -# AMBIL DATA -# ============================= -cursor.execute("SELECT wisata, ulasan FROM ulasan") -data = cursor.fetchall() - -print("Total ulasan:", len(data)) - -# ============================= -# PROSES -# ============================= -for row in data: - - wisata = row[0] - text = row[1] - - if not text: - continue - - # CLEANING - text_clean = re.sub(r'[^a-zA-Z\s]', '', text) - text_clean = text_clean.lower() - - # TOKENIZING - tokens = text_clean.split() - text_token = ' '.join(tokens) - - # STEMMING - text_stem = stemmer.stem(text_clean) - - # PREDIKSI - sentimen, probabilitas = predict_nb(text_stem) - - # INSERT PREPROCESSING - cursor.execute(""" - INSERT INTO preprocessing_data - (wisata, ulasan_asli, cleaning, tokenizing, stemming) - VALUES (%s,%s,%s,%s,%s) - """, - ( - wisata, - text, - text_clean, - text_token, - text_stem +for i, row in df.iterrows(): + try: + # preprocessing + cursor.execute(""" + INSERT INTO preprocessing_data + (wisata, ulasan_asli, cleaning, tokenizing, stemming, final_text) + VALUES (%s, %s, %s, %s, %s, %s) + """, ( + row['wisata'], + row['ulasan'], + row['clean_text'], + row['clean_text'], + row['clean_text'], + row['clean_text'] )) - # INSERT HASIL ANALISIS - cursor.execute(""" - INSERT INTO hasil_analisis - (wisata, ulasan_terolah, hasil_preprocessing, sentimen, probabilitas) - VALUES (%s,%s,%s,%s,%s) - """, (wisata, text, text_stem, sentimen, probabilitas)) + # hasil analisis + cursor.execute(""" + INSERT INTO hasil_analisis + (wisata, ulasan_terolah, hasil_preprocessing, sentimen, probabilitas) + VALUES (%s, %s, %s, %s, %s) + """, ( + row['wisata'], + row['clean_text'], + row['clean_text'], + row['prediksi'], + 0.0 + )) + + except Exception as e: + print("Error insert:", e) -# ============================= -# COMMIT SEKALI SAJA -# ============================= conn.commit() - -print(" Preprocessing & Analisis Sentimen selesai") - -conn.close() \ No newline at end of file +print(" Data berhasil disimpan") \ No newline at end of file diff --git a/scraper/scraping_pipeline.py b/scraper/scraping_pipeline.py index fac71f1..5c37de6 100644 --- a/scraper/scraping_pipeline.py +++ b/scraper/scraping_pipeline.py @@ -17,7 +17,8 @@ cursor = conn.cursor() # CHROME options = Options() options.add_argument("--start-maximized") - +options.add_argument('--lang=id') +Options.add_argument("accept-language-id-ID,id") driver = webdriver.Chrome(options=options) destinations = {