|
import os |
|
import re |
|
import warnings |
|
from collections import Counter |
|
|
|
import matplotlib.pyplot as plt |
|
import nltk |
|
import numpy as np |
|
import pandas as pd |
|
import requests |
|
import seaborn as sns |
|
import torch |
|
from nltk.tokenize import word_tokenize |
|
from nltk.util import ngrams |
|
from transformers import AutoModelForSequenceClassification, AutoTokenizer, pipeline |
|
from wordcloud import WordCloud |
|
|
|
warnings.filterwarnings("ignore") |
|
|
|
nltk.download("stopwords") |
|
nltk.download("punkt") |
|
|
|
|
|
class ReviewAnalyzer: |
|
def __init__(self): |
|
self.turkish_stopwords = self.get_turkish_stopwords() |
|
self.setup_sentiment_model() |
|
self.setup_summary_model() |
|
|
|
|
|
self.logistics_seller_words = { |
|
|
|
"kargo", |
|
"kargocu", |
|
"paket", |
|
"paketleme", |
|
"teslimat", |
|
"teslim", |
|
"gönderi", |
|
"gönderim", |
|
"ulaştı", |
|
"ulaşım", |
|
"geldi", |
|
"kurye", |
|
"dağıtım", |
|
"hasarlı", |
|
"hasar", |
|
"kutu", |
|
"ambalaj", |
|
"zamanında", |
|
"geç", |
|
"hızlı", |
|
"yavaş", |
|
"günde", |
|
"saatte", |
|
|
|
"satıcı", |
|
"mağaza", |
|
"sipariş", |
|
"trendyol", |
|
"tedarik", |
|
"stok", |
|
"garanti", |
|
"fatura", |
|
"iade", |
|
"geri", |
|
"müşteri", |
|
"hizmet", |
|
"destek", |
|
"iletişim", |
|
"şikayet", |
|
"sorun", |
|
"çözüm", |
|
"hediye", |
|
|
|
"fiyat", |
|
"ücret", |
|
"para", |
|
"bedava", |
|
"ücretsiz", |
|
"indirim", |
|
"kampanya", |
|
"taksit", |
|
"ödeme", |
|
"bütçe", |
|
"hesap", |
|
"kur", |
|
|
|
"bugün", |
|
"yarın", |
|
"dün", |
|
"hafta", |
|
"gün", |
|
"saat", |
|
"süre", |
|
"bekleme", |
|
"gecikme", |
|
"erken", |
|
"geç", |
|
} |
|
|
|
def get_turkish_stopwords(self): |
|
"""Genişletilmiş stop words listesini hazırla""" |
|
github_url = "https://raw.githubusercontent.com/sgsinclair/trombone/master/src/main/resources/org/voyanttools/trombone/keywords/stop.tr.turkish-lucene.txt" |
|
stop_words = set() |
|
|
|
try: |
|
response = requests.get(github_url) |
|
if response.status_code == 200: |
|
github_stops = set( |
|
word.strip() for word in response.text.split("\n") if word.strip() |
|
) |
|
stop_words.update(github_stops) |
|
except Exception as e: |
|
print(f"GitHub'dan stop words çekilirken hata oluştu: {e}") |
|
|
|
stop_words.update(set(nltk.corpus.stopwords.words("turkish"))) |
|
|
|
additional_stops = { |
|
"bir", |
|
"ve", |
|
"çok", |
|
"bu", |
|
"de", |
|
"da", |
|
"için", |
|
"ile", |
|
"ben", |
|
"sen", |
|
"o", |
|
"biz", |
|
"siz", |
|
"onlar", |
|
"bu", |
|
"şu", |
|
"ama", |
|
"fakat", |
|
"ancak", |
|
"lakin", |
|
"ki", |
|
"dahi", |
|
"mi", |
|
"mı", |
|
"mu", |
|
"mü", |
|
"var", |
|
"yok", |
|
"olan", |
|
"içinde", |
|
"üzerinde", |
|
"bana", |
|
"sana", |
|
"ona", |
|
"bize", |
|
"size", |
|
"onlara", |
|
"evet", |
|
"hayır", |
|
"tamam", |
|
"oldu", |
|
"olmuş", |
|
"olacak", |
|
"etmek", |
|
"yapmak", |
|
"kez", |
|
"kere", |
|
"defa", |
|
"adet", |
|
} |
|
stop_words.update(additional_stops) |
|
|
|
print(f"Toplam {len(stop_words)} adet stop words yüklendi.") |
|
return stop_words |
|
|
|
def preprocess_text(self, text): |
|
"""Metin ön işleme""" |
|
if isinstance(text, str): |
|
|
|
text = text.lower() |
|
|
|
text = re.sub(r"[^\w\s]", "", text) |
|
|
|
text = re.sub(r"\d+", "", text) |
|
|
|
text = re.sub(r"\s+", " ", text).strip() |
|
|
|
words = text.split() |
|
words = [word for word in words if word not in self.turkish_stopwords] |
|
return " ".join(words) |
|
return "" |
|
|
|
def setup_sentiment_model(self): |
|
"""Sentiment analiz modelini hazırla""" |
|
self.device = "cuda" if torch.cuda.is_available() else "cpu" |
|
print(f"Using device for sentiment: {self.device}") |
|
|
|
model_name = "savasy/bert-base-turkish-sentiment-cased" |
|
self.sentiment_tokenizer = AutoTokenizer.from_pretrained(model_name) |
|
self.sentiment_model = ( |
|
AutoModelForSequenceClassification.from_pretrained(model_name) |
|
.to(self.device) |
|
.to(torch.float32) |
|
) |
|
|
|
def setup_summary_model(self): |
|
"""Özet modelini hazırla""" |
|
print("Loading Trendyol-LLM model...") |
|
model_id = "Trendyol/Trendyol-LLM-8b-chat-v2.0" |
|
|
|
self.summary_pipe = pipeline( |
|
"text-generation", |
|
model=model_id, |
|
torch_dtype="auto", |
|
device_map="auto", |
|
) |
|
|
|
self.terminators = [ |
|
self.summary_pipe.tokenizer.eos_token_id, |
|
self.summary_pipe.tokenizer.convert_tokens_to_ids("<|eot_id|>"), |
|
] |
|
|
|
self.sampling_params = { |
|
"do_sample": True, |
|
"temperature": 0.3, |
|
"top_k": 50, |
|
"top_p": 0.9, |
|
"repetition_penalty": 1.1, |
|
} |
|
|
|
def filter_reviews(self, df): |
|
"""Ürün ile ilgili olmayan yorumları filtrele""" |
|
|
|
def is_product_review(text): |
|
if not isinstance(text, str): |
|
return False |
|
return not any(word in text.lower() for word in self.logistics_seller_words) |
|
|
|
filtered_df = df[df["Yorum"].apply(is_product_review)].copy() |
|
|
|
print(f"\nFiltreleme İstatistikleri:") |
|
print(f"Toplam yorum sayısı: {len(df)}") |
|
print(f"Ürün yorumu sayısı: {len(filtered_df)}") |
|
print(f"Filtrelenen yorum sayısı: {len(df) - len(filtered_df)}") |
|
print( |
|
f"Filtreleme oranı: {((len(df) - len(filtered_df)) / len(df) * 100):.2f}%" |
|
) |
|
|
|
return filtered_df |
|
|
|
def analyze_sentiment(self, df): |
|
"""Sentiment analizi yap""" |
|
|
|
def predict_sentiment(text): |
|
if not isinstance(text, str) or len(text.strip()) == 0: |
|
return {"label": "Nötr", "score": 0.5} |
|
|
|
try: |
|
cleaned_text = self.preprocess_text(text) |
|
|
|
inputs = self.sentiment_tokenizer( |
|
cleaned_text, |
|
return_tensors="pt", |
|
truncation=True, |
|
max_length=512, |
|
padding=True, |
|
).to(self.device) |
|
|
|
with torch.no_grad(): |
|
outputs = self.sentiment_model(**inputs) |
|
probs = torch.nn.functional.softmax(outputs.logits, dim=1) |
|
prediction = probs.cpu().numpy()[0] |
|
|
|
score = float(prediction[1]) |
|
|
|
if score > 0.75: |
|
label = "Pozitif" |
|
elif score < 0.25: |
|
label = "Negatif" |
|
elif score > 0.55: |
|
label = "Pozitif" |
|
elif score < 0.45: |
|
label = "Negatif" |
|
else: |
|
label = "Nötr" |
|
|
|
return {"label": label, "score": score} |
|
|
|
except Exception as e: |
|
print(f"Error in sentiment prediction: {e}") |
|
return {"label": "Nötr", "score": 0.5} |
|
|
|
print("\nSentiment analizi yapılıyor...") |
|
results = [predict_sentiment(text) for text in df["Yorum"]] |
|
|
|
df["sentiment_score"] = [r["score"] for r in results] |
|
df["sentiment_label"] = [r["label"] for r in results] |
|
df["cleaned_text"] = df["Yorum"].apply(self.preprocess_text) |
|
|
|
return df |
|
|
|
def get_key_phrases(self, text_series): |
|
"""En önemli anahtar kelimeleri bul""" |
|
text = " ".join(text_series.astype(str)) |
|
words = self.preprocess_text(text).split() |
|
word_freq = Counter(words) |
|
|
|
return { |
|
word: count |
|
for word, count in word_freq.items() |
|
if count >= 3 and len(word) > 2 |
|
} |
|
|
|
def generate_summary(self, df): |
|
"""Yorumların genel özetini oluştur""" |
|
|
|
high_rated = df[df["Yıldız Sayısı"] >= 4] |
|
low_rated = df[df["Yıldız Sayısı"] <= 2] |
|
|
|
|
|
positive_phrases = self.get_key_phrases(high_rated["cleaned_text"]) |
|
negative_phrases = self.get_key_phrases(low_rated["cleaned_text"]) |
|
|
|
|
|
top_positive = ( |
|
high_rated.sort_values("sentiment_score", ascending=False)["Yorum"] |
|
.head(3) |
|
.tolist() |
|
) |
|
top_negative = ( |
|
low_rated.sort_values("sentiment_score")["Yorum"].head(2).tolist() |
|
) |
|
|
|
|
|
pos_features = ", ".join( |
|
[f"{word} ({count})" for word, count in list(positive_phrases.items())[:5]] |
|
) |
|
neg_features = ", ".join( |
|
[f"{word} ({count})" for word, count in list(negative_phrases.items())[:5]] |
|
) |
|
|
|
summary_prompt = f""" |
|
MacBook Air Kullanıcı Yorumları Analizi: |
|
|
|
İSTATİSTİKLER: |
|
- Toplam Yorum: {len(df)} |
|
- Ortalama Puan: {df['Yıldız Sayısı'].mean():.1f}/5 |
|
- Pozitif Yorum Oranı: {(len(df[df['sentiment_label'] == 'Pozitif']) / len(df) * 100):.1f}% |
|
|
|
SIKÇA KULLANILAN KELİMELER: |
|
Olumlu: {pos_features} |
|
Olumsuz: {neg_features} |
|
|
|
ÖRNEK OLUMLU YORUMLAR: |
|
{' '.join([f"• {yorum[:200]}..." for yorum in top_positive])} |
|
|
|
ÖRNEK OLUMSUZ YORUMLAR: |
|
{' '.join([f"• {yorum[:200]}..." for yorum in top_negative])} |
|
|
|
Lütfen bu veriler ışığında bu ürün için kısa ve öz bir değerlendirme yap. |
|
Özellikle kullanıcıların en çok beğendiği özellikler ve en sık dile getirilen sorunlara odaklan. |
|
Değerlendirmeyi 3 paragrafla sınırla ve somut örnekler kullan. |
|
""" |
|
|
|
messages = [ |
|
{ |
|
"role": "system", |
|
"content": "Sen bir ürün yorumları analiz uzmanısın. Yorumları özetlerken nesnel ve açık ol.", |
|
}, |
|
{"role": "user", "content": summary_prompt}, |
|
] |
|
|
|
outputs = self.summary_pipe( |
|
messages, |
|
max_new_tokens=512, |
|
eos_token_id=self.terminators, |
|
return_full_text=False, |
|
**self.sampling_params, |
|
) |
|
|
|
return outputs[0]["generated_text"] |
|
|
|
|
|
def analyze_reviews(file_path): |
|
df = pd.read_csv(file_path) |
|
|
|
analyzer = ReviewAnalyzer() |
|
|
|
filtered_df = analyzer.filter_reviews(df) |
|
|
|
print("Sentiment analizi başlatılıyor...") |
|
analyzed_df = analyzer.analyze_sentiment(filtered_df) |
|
|
|
analyzed_df.to_csv( |
|
"sentiment_analyzed_reviews.csv", index=False, encoding="utf-8-sig" |
|
) |
|
print("Sentiment analizi tamamlandı ve kaydedildi.") |
|
|
|
print("\nÜrün özeti oluşturuluyor...") |
|
summary = analyzer.generate_summary(analyzed_df) |
|
|
|
with open("urun_ozeti.txt", "w", encoding="utf-8") as f: |
|
f.write(summary) |
|
|
|
print("\nÜrün Özeti:") |
|
print("-" * 50) |
|
print(summary) |
|
print("\nÖzet 'urun_ozeti.txt' dosyasına kaydedildi.") |
|
|
|
|
|
if __name__ == "__main__": |
|
analyze_reviews("data/macbook_product_comments_with_ratings.csv") |
|
|