update fitur
This commit is contained in:
parent
bcf6326dee
commit
d5f3f33a83
|
|
@ -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'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -98,9 +98,46 @@ class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
|
|||
</div>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="mt-6">
|
||||
{{ $hasil->links() }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500 mt-4 text-center">
|
||||
Showing {{ $hasil->firstItem() }} to {{ $hasil->lastItem() }}
|
||||
of {{ $hasil->total() }} results
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mt-3 items-center gap-2 text-sm">
|
||||
|
||||
{{-- PREV --}}
|
||||
@if ($hasil->onFirstPage())
|
||||
<span class="text-gray-400">‹</span>
|
||||
@else
|
||||
<a href="{{ $hasil->previousPageUrl() }}">‹</a>
|
||||
@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())
|
||||
<span class="px-3 py-1 bg-blue-600 text-white rounded-full">
|
||||
{{ $i }}
|
||||
</span>
|
||||
@else
|
||||
<a href="{{ $hasil->url($i) }}" class="px-2 hover:underline">
|
||||
{{ $i }}
|
||||
</a>
|
||||
@endif
|
||||
@endfor
|
||||
|
||||
{{-- NEXT --}}
|
||||
@if ($hasil->hasMorePages())
|
||||
<a href="{{ $hasil->nextPageUrl() }}">›</a>
|
||||
@else
|
||||
<span class="text-gray-400">›</span>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,37 +4,12 @@
|
|||
|
||||
<div class="max-w-7xl mx-auto">
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<h2 class="text-2xl font-semibold text-gray-800">
|
||||
Dashboard
|
||||
</h2>
|
||||
|
||||
<!-- <p class="text-sm text-gray-500 mt-1">
|
||||
Terakhir diperbarui: 27 Februari 2026, 22:45
|
||||
</p>
|
||||
|
||||
<p class="text-xs text-gray-400">
|
||||
Periode Data: Februari 2026
|
||||
</p> -->
|
||||
</div>
|
||||
|
||||
<!-- Filter Destinasi -->
|
||||
<form method="GET" action="" class="ml-4">
|
||||
<select name="destinasi"
|
||||
class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
|
||||
focus:ring-2 focus:ring-blue-400 focus:outline-none">
|
||||
|
||||
<option value="">Semua Destinasi</option>
|
||||
<option value="papuma">Pantai Papuma</option>
|
||||
<option value="watuulo">Pantai Watu Ulo</option>
|
||||
<option value="teluklove">Teluk Love (Payangan)</option>
|
||||
<option value="gununggambir">Kebun Teh Gunung Gambir </option>
|
||||
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
{{-- ALERT --}}
|
||||
@if(!$hasAnalisis)
|
||||
<div class="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 mb-6 rounded">
|
||||
Data ulasan sudah tersedia, tetapi belum dilakukan analisis sentimen.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Statistik Cards --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
|
|
@ -69,78 +44,133 @@ class="bg-white border border-gray-300 text-sm rounded-lg px-4 py-2 shadow-sm
|
|||
|
||||
</div>
|
||||
|
||||
{{-- Chart Section --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
{{-- Chart Section --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
|
||||
<!-- Pie Chart -->
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">
|
||||
Distribusi Sentimen
|
||||
</h3>
|
||||
<div class="h-[280px] flex justify-center items-center">
|
||||
<canvas id="pieChart"></canvas>
|
||||
<!-- Pie Chart -->
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">Distribusi Sentimen</h3>
|
||||
<div style="height:300px;">
|
||||
<canvas id="pieChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bar Chart -->
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">Sentimen per Destinasi</h3>
|
||||
<div style="height:300px;">
|
||||
<canvas id="barChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Bar Chart -->
|
||||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h3 class="font-semibold mb-4 text-gray-700">
|
||||
Sentimen per Destinasi
|
||||
</h3>
|
||||
<div class="h-[280px]">
|
||||
<canvas id="barChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
{{-- Section Bawah --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
|
||||
</div> {{-- TUTUP GRID CHART --}}
|
||||
<!-- Total Data per Destinasi -->
|
||||
<div class="bg-white rounded-xl shadow p-6 h-[320px]">
|
||||
<h3 class="font-semibold mb-6 text-gray-700">
|
||||
Total Data per Destinasi
|
||||
</h3>
|
||||
|
||||
|
||||
{{-- Section Bawah --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
|
||||
<!-- Total Data per Destinasi -->
|
||||
<div class="bg-white rounded-xl shadow p-6 h-[320px]">
|
||||
<h3 class="font-semibold mb-6 text-gray-700">
|
||||
Total Data per Destinasi
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4 text-sm text-gray-600">
|
||||
<div class="flex justify-between border-b pb-3">
|
||||
<span>Pantai Papuma</span>
|
||||
<span class="font-semibold text-gray-800">120 ulasan</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b pb-3">
|
||||
<span>Pantai Watu Ulo</span>
|
||||
<span class="font-semibold text-gray-800">85 ulasan</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b pb-3">
|
||||
<span>Teluk Love (Payangan)</span>
|
||||
<span class="font-semibold text-gray-800">95 ulasan</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Kebun Teh Gunung Gambir</span>
|
||||
<span class="font-semibold text-gray-800">60 ulasan</span>
|
||||
<div class="space-y-4 text-sm text-gray-600">
|
||||
@foreach($totalPerWisata as $item)
|
||||
<div class="flex justify-between border-b pb-3">
|
||||
<span>{{ $item->wisata }}</span>
|
||||
<span class="font-semibold text-gray-800">
|
||||
{{ $item->total }} ulasan
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WORD CLOUD -->
|
||||
<div class="bg-white rounded-xl shadow p-6 h-[320px]">
|
||||
<h3 class="font-semibold mb-6 text-gray-700">
|
||||
Word Cloud
|
||||
</h3>
|
||||
<!-- WORD CLOUD -->
|
||||
<div class="bg-white rounded-xl shadow p-6 h-[320px]">
|
||||
<h3 class="font-semibold mb-6 text-gray-700">
|
||||
Word Cloud
|
||||
</h3>
|
||||
<div id="wordCloud" class="w-full h-[240px] overflow-hidden"></div>
|
||||
</div>
|
||||
|
||||
<div id="wordCloud" class="w-full h-[240px]"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{-- ========================= --}}
|
||||
{{-- SCRIPT SECTION --}}
|
||||
{{-- ========================= --}}
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/wordcloud2.js/1.1.2/wordcloud2.min.js"></script>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
// =============================
|
||||
// PIE CHART
|
||||
// =============================
|
||||
const pieData = JSON.parse('{!! json_encode($chartSentimen) !!}');
|
||||
|
||||
const pieLabels = pieData.map(item => item.sentimen);
|
||||
const pieValues = pieData.map(item => item.total);
|
||||
|
||||
new Chart(document.getElementById('pieChart'), {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: pieLabels,
|
||||
datasets: [{
|
||||
data: pieValues
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// =============================
|
||||
// BAR CHART
|
||||
// =============================
|
||||
const barData = JSON.parse('{!! json_encode($chartDestinasi) !!}');
|
||||
|
||||
const labelsBar = barData.map(item => item.wisata);
|
||||
const positif = barData.map(item => item.positif);
|
||||
const negatif = barData.map(item => item.negatif);
|
||||
const netral = barData.map(item => item.netral);
|
||||
|
||||
new Chart(document.getElementById('barChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labelsBar,
|
||||
datasets: [
|
||||
{ label: 'Positif', data: positif },
|
||||
{ label: 'Negatif', data: negatif },
|
||||
{ label: 'Netral', data: netral }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// =============================
|
||||
// WORD CLOUD
|
||||
// =============================
|
||||
const text = `{!! $allText !!}`;
|
||||
|
||||
if(text.length > 0){
|
||||
const words = text.split(" ");
|
||||
|
||||
const wordCount = {};
|
||||
words.forEach(word => {
|
||||
if(word.length > 3){
|
||||
wordCount[word] = (wordCount[word] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
const list = Object.entries(wordCount);
|
||||
|
||||
WordCloud(document.getElementById('wordCloud'), {
|
||||
list: list
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
<option value="">Semua Destinasi</option>
|
||||
<option value="Pantai Papuma" {{ request('wisata')=='Pantai Papuma'?'selected':'' }}>Pantai Papuma</option>
|
||||
<option value="Pantai Watu Ulo" {{ request('wisata')=='Pantai Watu Ulo'?'selected':'' }}>Pantai Watu Ulo</option>
|
||||
<option value="Teluk Love (Payangan)" {{ request('wisata')=='Teluk Love (Payangan)'?'selected':'' }}>Teluk Love</option>
|
||||
<option value="Teluk Love" {{ request('wisata')=='Teluk Love'?'selected':'' }}>Teluk Love</option>
|
||||
<option value="Kebun Teh Gunung Gambir" {{ request('wisata')=='Kebun Teh Gunung Gambir'?'selected':'' }}>Kebun Teh Gunung Gambir</option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -155,82 +155,80 @@
|
|||
<div class="bg-white rounded-xl shadow p-6">
|
||||
<h3 class="font-semibold mb-4">Hasil Preprocessing</h3>
|
||||
|
||||
<div class="max-h-[300px] overflow-y-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-100 sticky top-0">
|
||||
<tr>
|
||||
<th class="px-4 py-2">Wisata</th>
|
||||
<th class="px-4 py-2">Ulasan Mentah</th>
|
||||
<th class="px-4 py-2">Ulasan Bersih</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($preprocessing as $row)
|
||||
<tr class="border-b">
|
||||
<td class="px-4 py-2">{{ $row->wisata }}</td>
|
||||
<td class="px-4 py-2">{{ $row->ulasan_asli }}</td>
|
||||
<td class="px-4 py-2">{{ $row->stemming }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="3" class="text-center py-4 text-gray-400">
|
||||
Belum ada data
|
||||
</td>
|
||||
</tr>
|
||||
<div class="flex justify-center mt-4 text-sm">
|
||||
<div id="preprocessingTabel">
|
||||
|
||||
<!-- INFO -->
|
||||
<div class="text-sm text-gray-500 mt-4 text-center">
|
||||
Showing {{ $preprocessing->firstItem() }} to {{ $preprocessing->lastItem() }}
|
||||
of {{ $preprocessing->total() }} results
|
||||
</div>
|
||||
<!-- TABLE -->
|
||||
<div class="max-h-[300px] overflow-y-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-100 sticky top-0">
|
||||
<tr>
|
||||
<th class="px-4 py-2">Wisata</th>
|
||||
<th class="px-4 py-2">Ulasan Mentah</th>
|
||||
<th class="px-4 py-2">Ulasan Bersih</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($preprocessing as $row)
|
||||
<tr class="border-b">
|
||||
<td class="px-4 py-2">{{ $row->wisata }}</td>
|
||||
<td class="px-4 py-2">{{ $row->ulasan_asli }}</td>
|
||||
<td class="px-4 py-2">{{ $row->stemming }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="3" class="text-center py-4 text-gray-400">
|
||||
Belum ada data
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="flex justify-center mt-6 items-center gap-2 text-sm">
|
||||
<!-- INFO -->
|
||||
<div class="text-sm text-gray-500 mt-4 text-center">
|
||||
Showing {{ $preprocessing->firstItem() }} to {{ $preprocessing->lastItem() }}
|
||||
of {{ $preprocessing->total() }} results
|
||||
</div>
|
||||
|
||||
{{-- FIRST --}}
|
||||
@if ($preprocessing->currentPage() > 1)
|
||||
<a href="{{ $preprocessing->url(1) }}">«</a>
|
||||
@endif
|
||||
<!-- PAGINATION -->
|
||||
<div class="flex justify-center mt-4 items-center gap-2 text-sm">
|
||||
|
||||
{{-- PREV --}}
|
||||
@if ($preprocessing->onFirstPage())
|
||||
<span class="text-gray-400">‹</span>
|
||||
@else
|
||||
<a href="{{ $preprocessing->previousPageUrl() }}">‹</a>
|
||||
@endif
|
||||
{{-- PREV --}}
|
||||
@if ($preprocessing->onFirstPage())
|
||||
<span class="text-gray-400">‹</span>
|
||||
@else
|
||||
<a href="{{ $preprocessing->previousPageUrl() }}">‹</a>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$start = max($preprocessing->currentPage() - 2, 1);
|
||||
$end = min($preprocessing->currentPage() + 2, $preprocessing->lastPage());
|
||||
@endphp
|
||||
@php
|
||||
$start = max($preprocessing->currentPage() - 2, 1);
|
||||
$end = min($preprocessing->currentPage() + 2, $preprocessing->lastPage());
|
||||
@endphp
|
||||
|
||||
{{-- NUMBER DINAMIS --}}
|
||||
@for ($i = $start; $i <= $end; $i++)
|
||||
@if ($i == $preprocessing->currentPage())
|
||||
<span class="px-3 py-1 bg-blue-600 text-white rounded-full">{{ $i }}</span>
|
||||
@else
|
||||
<a href="{{ $preprocessing->url($i) }}" class="px-3 py-1 hover:underline">
|
||||
{{ $i }}
|
||||
</a>
|
||||
@endif
|
||||
@endfor
|
||||
{{-- NUMBER --}}
|
||||
@for ($i = $start; $i <= $end; $i++)
|
||||
@if ($i == $preprocessing->currentPage())
|
||||
<span class="px-3 py-1 bg-blue-600 text-white rounded-full">
|
||||
{{ $i }}
|
||||
</span>
|
||||
@else
|
||||
<a href="{{ $preprocessing->url($i) }}" class="px-3 py-1 hover:underline">
|
||||
{{ $i }}
|
||||
</a>
|
||||
@endif
|
||||
@endfor
|
||||
|
||||
{{-- NEXT --}}
|
||||
@if ($preprocessing->hasMorePages())
|
||||
<a href="{{ $preprocessing->nextPageUrl() }}">›</a>
|
||||
@else
|
||||
<span class="text-gray-400">›</span>
|
||||
@endif
|
||||
{{-- NEXT --}}
|
||||
@if ($preprocessing->hasMorePages())
|
||||
<a href="{{ $preprocessing->nextPageUrl() }}">›</a>
|
||||
@else
|
||||
<span class="text-gray-400">›</span>
|
||||
@endif
|
||||
|
||||
{{-- LAST --}}
|
||||
@if ($preprocessing->currentPage() < $preprocessing->lastPage())
|
||||
<a href="{{ $preprocessing->url($preprocessing->lastPage()) }}">»</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById("filterWisata").addEventListener("change", function () {
|
||||
let wisata = this.value;
|
||||
|
|
@ -244,12 +242,31 @@
|
|||
});
|
||||
});
|
||||
</script>
|
||||
@endforelse
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("click", function(e) {
|
||||
if(e.target.closest("#preprocessingTable a")) {
|
||||
e.preventDefault();
|
||||
|
||||
let url = e.target.closest("a").getAttribute("href");
|
||||
|
||||
fetch(url)
|
||||
.then(res => res.text())
|
||||
.then(html => {
|
||||
let doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
let newContent = doc.querySelector("#preprocessingTable").innerHTML;
|
||||
|
||||
document.querySelector("#preprocessingTable").innerHTML = newContent;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
@ -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()
|
||||
print(" Data berhasil disimpan")
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue