fix 1652
Browse files- app.py +108 -331
- requirements.txt +14 -15
- scrape/trendyol_scraper.py +58 -86
- scripts/data_prp_eda.py +357 -0
- scripts/review_summarizer.py +256 -0
- scripts/sentiment_bert_model.py +166 -0
app.py
CHANGED
@@ -1,331 +1,108 @@
|
|
1 |
-
import gradio as gr
|
2 |
-
import pandas as pd
|
3 |
-
|
4 |
-
|
5 |
-
import
|
6 |
-
import
|
7 |
-
|
8 |
-
import
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
#
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
#
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
def predict_sentiment(self, text):
|
110 |
-
"""Tek bir yorum için sentiment analizi yap"""
|
111 |
-
# Önce metni temizle
|
112 |
-
text = self.preprocess_text(text)
|
113 |
-
|
114 |
-
if not text:
|
115 |
-
return {"label": "nötr", "score": 0.5}
|
116 |
-
|
117 |
-
inputs = self.sentiment_tokenizer(text, return_tensors="pt", truncation=True, max_length=512)
|
118 |
-
inputs = {k: v.to(self.device) for k, v in inputs.items()}
|
119 |
-
|
120 |
-
with torch.no_grad():
|
121 |
-
outputs = self.sentiment_model(**inputs)
|
122 |
-
scores = torch.nn.functional.softmax(outputs.logits, dim=1)
|
123 |
-
|
124 |
-
positive_score = scores[0][1].item()
|
125 |
-
label = "pozitif" if positive_score > 0.5 else "negatif"
|
126 |
-
|
127 |
-
return {"label": label, "score": positive_score}
|
128 |
-
|
129 |
-
def analyze_reviews(self, df):
|
130 |
-
"""Tüm yorumları analiz et"""
|
131 |
-
print("\nSentiment analizi başlatılıyor...")
|
132 |
-
|
133 |
-
# Önce ürün ile ilgili olmayan yorumları filtrele
|
134 |
-
df = self.filter_product_reviews(df)
|
135 |
-
|
136 |
-
# Sentiment analizi
|
137 |
-
results = []
|
138 |
-
for text in tqdm(df['Yorum'], desc="Yorumlar analiz ediliyor"):
|
139 |
-
sentiment = self.predict_sentiment(text)
|
140 |
-
results.append(sentiment)
|
141 |
-
|
142 |
-
df['sentiment_score'] = [r['score'] for r in results]
|
143 |
-
df['sentiment_label'] = [r['label'] for r in results]
|
144 |
-
|
145 |
-
return df
|
146 |
-
|
147 |
-
def generate_summary(self, df):
|
148 |
-
"""İstatistiksel özet ve Gemini ile detaylı analiz"""
|
149 |
-
# Temel istatistikler
|
150 |
-
avg_rating = df['Yıldız Sayısı'].mean()
|
151 |
-
total_reviews = len(df)
|
152 |
-
|
153 |
-
# Sentiment bazlı gruplandırma
|
154 |
-
positive_comments = df[df['sentiment_label'] == 'pozitif']['Yorum'].tolist()
|
155 |
-
negative_comments = df[df['sentiment_label'] == 'negatif']['Yorum'].tolist()
|
156 |
-
positive_count = len(positive_comments)
|
157 |
-
negative_count = len(negative_comments)
|
158 |
-
|
159 |
-
# Yıldız dağılımı
|
160 |
-
star_dist = df['Yıldız Sayısı'].value_counts().sort_index()
|
161 |
-
star_dist_text = "\n".join([f"{star} yıldız: {count} yorum" for star, count in star_dist.items()])
|
162 |
-
|
163 |
-
# En sık kelimeler
|
164 |
-
all_words = []
|
165 |
-
for text in df['Yorum']:
|
166 |
-
cleaned_text = self.preprocess_text(text)
|
167 |
-
if cleaned_text:
|
168 |
-
all_words.extend(cleaned_text.split())
|
169 |
-
|
170 |
-
from collections import Counter
|
171 |
-
word_freq = Counter(all_words).most_common(10)
|
172 |
-
frequent_words = ", ".join([f"{word} ({count} kez)" for word, count in word_freq])
|
173 |
-
|
174 |
-
# İstatistiksel özet metni
|
175 |
-
stats_summary = f"""📊 ÜRÜN ANALİZ RAPORU
|
176 |
-
|
177 |
-
⭐ Ortalama Puan: {avg_rating:.1f}/5
|
178 |
-
📝 Toplam Yorum: {total_reviews}
|
179 |
-
✅ Pozitif Yorum: {positive_count}
|
180 |
-
❌ Negatif Yorum: {negative_count}
|
181 |
-
|
182 |
-
📈 YILDIZ DAĞILIMI:
|
183 |
-
{star_dist_text}
|
184 |
-
|
185 |
-
🔍 EN SIK KULLANILAN KELİMELER:
|
186 |
-
{frequent_words}
|
187 |
-
|
188 |
-
💬 ÖRNEK YORUMLAR:
|
189 |
-
✅ Pozitif Yorumlar:
|
190 |
-
{' | '.join(positive_comments[:3])}
|
191 |
-
|
192 |
-
❌ Negatif Yorumlar:
|
193 |
-
{' | '.join(negative_comments[:3])}"""
|
194 |
-
|
195 |
-
# Gemini ile detaylı analiz
|
196 |
-
if self.gemini_model:
|
197 |
-
try:
|
198 |
-
prompt = f"""Aşağıdaki ürün yorumları verilerine dayanarak detaylı bir analiz yap:
|
199 |
-
|
200 |
-
1. İstatistikler:
|
201 |
-
- Toplam {total_reviews} yorum
|
202 |
-
- Ortalama puan: {avg_rating:.1f}/5
|
203 |
-
- {positive_count} pozitif, {negative_count} negatif yorum
|
204 |
-
|
205 |
-
2. Örnek Pozitif Yorumlar:
|
206 |
-
{' | '.join(positive_comments[:3])}
|
207 |
-
|
208 |
-
3. Örnek Negatif Yorumlar:
|
209 |
-
{' | '.join(negative_comments[:3])}
|
210 |
-
|
211 |
-
4. En Sık Kullanılan Kelimeler:
|
212 |
-
{frequent_words}
|
213 |
-
|
214 |
-
Lütfen şu başlıklar altında bir değerlendirme yap:
|
215 |
-
1. Ürünün güçlü yönleri
|
216 |
-
2. Ürünün zayıf yönleri
|
217 |
-
3. Genel kullanıcı memnuniyeti
|
218 |
-
4. Potansiyel alıcılar için öneriler
|
219 |
-
|
220 |
-
Yanıtını Türkçe olarak ver ve mümkün olduğunca özlü tut."""
|
221 |
-
|
222 |
-
response = self.gemini_model.generate_content(prompt)
|
223 |
-
ai_analysis = response.text
|
224 |
-
|
225 |
-
# İstatistiksel özet ve AI analizini birleştir
|
226 |
-
return f"{stats_summary}\n\n🤖 YAPAY ZEKA ANALİZİ:\n{ai_analysis}"
|
227 |
-
|
228 |
-
except Exception as e:
|
229 |
-
print(f"Gemini API hatası: {str(e)}")
|
230 |
-
return stats_summary
|
231 |
-
|
232 |
-
return stats_summary
|
233 |
-
|
234 |
-
def analyze_url(self, url):
|
235 |
-
try:
|
236 |
-
# Temizlik
|
237 |
-
if os.path.exists("data"):
|
238 |
-
shutil.rmtree("data")
|
239 |
-
|
240 |
-
# Yorumları çek
|
241 |
-
df = scrape_reviews(url)
|
242 |
-
|
243 |
-
if df.empty:
|
244 |
-
return "Yorumlar çekilemedi. Lütfen URL'yi kontrol edin.", None, None, None
|
245 |
-
|
246 |
-
# Sentiment analizi yap
|
247 |
-
analyzed_df = self.analyze_reviews(df)
|
248 |
-
|
249 |
-
# Özet oluştur
|
250 |
-
summary = self.generate_summary(analyzed_df)
|
251 |
-
|
252 |
-
# Grafikleri oluştur
|
253 |
-
fig1 = self.create_sentiment_distribution(analyzed_df)
|
254 |
-
fig2 = self.create_rating_distribution(analyzed_df)
|
255 |
-
fig3 = self.create_sentiment_by_rating(analyzed_df)
|
256 |
-
|
257 |
-
return summary, fig1, fig2, fig3
|
258 |
-
|
259 |
-
except Exception as e:
|
260 |
-
return f"Bir hata oluştu: {str(e)}", None, None, None
|
261 |
-
|
262 |
-
finally:
|
263 |
-
# Temizlik
|
264 |
-
if os.path.exists("data"):
|
265 |
-
shutil.rmtree("data")
|
266 |
-
|
267 |
-
def create_sentiment_distribution(self, df):
|
268 |
-
fig = px.pie(df,
|
269 |
-
names='sentiment_label',
|
270 |
-
title='Duygu Analizi Dağılımı')
|
271 |
-
return fig
|
272 |
-
|
273 |
-
def create_rating_distribution(self, df):
|
274 |
-
fig = px.bar(df['Yıldız Sayısı'].value_counts().sort_index(),
|
275 |
-
title='Yıldız Dağılımı')
|
276 |
-
fig.update_layout(xaxis_title='Yıldız Sayısı',
|
277 |
-
yaxis_title='Yorum Sayısı')
|
278 |
-
return fig
|
279 |
-
|
280 |
-
def create_sentiment_by_rating(self, df):
|
281 |
-
avg_sentiment = df.groupby('Yıldız Sayısı')['sentiment_score'].mean()
|
282 |
-
fig = px.line(avg_sentiment,
|
283 |
-
title='Yıldız Sayısına Göre Ortalama Sentiment Skoru')
|
284 |
-
fig.update_layout(xaxis_title='Yıldız Sayısı',
|
285 |
-
yaxis_title='Ortalama Sentiment Skoru')
|
286 |
-
return fig
|
287 |
-
|
288 |
-
def create_interface():
|
289 |
-
app = ReviewAnalysisApp()
|
290 |
-
|
291 |
-
with gr.Blocks(theme=gr.themes.Soft()) as interface:
|
292 |
-
gr.Markdown("# Trendyol Yorum Analizi")
|
293 |
-
|
294 |
-
with gr.Row():
|
295 |
-
url_input = gr.Textbox(
|
296 |
-
label="Trendyol Ürün Yorumları URL'si",
|
297 |
-
placeholder="https://www.trendyol.com/..."
|
298 |
-
)
|
299 |
-
|
300 |
-
analyze_btn = gr.Button("Analiz Et")
|
301 |
-
|
302 |
-
with gr.Row():
|
303 |
-
with gr.Column(scale=1):
|
304 |
-
summary_output = gr.Textbox(
|
305 |
-
label="Özet",
|
306 |
-
lines=10
|
307 |
-
)
|
308 |
-
|
309 |
-
with gr.Column(scale=2):
|
310 |
-
with gr.Tab("Duygu Analizi"):
|
311 |
-
sentiment_dist = gr.Plot()
|
312 |
-
with gr.Tab("Yıldız Dağılımı"):
|
313 |
-
rating_dist = gr.Plot()
|
314 |
-
with gr.Tab("Sentiment-Yıldız İlişkisi"):
|
315 |
-
sentiment_rating = gr.Plot()
|
316 |
-
|
317 |
-
analyze_btn.click(
|
318 |
-
fn=app.analyze_url,
|
319 |
-
inputs=[url_input],
|
320 |
-
outputs=[summary_output, sentiment_dist, rating_dist, sentiment_rating]
|
321 |
-
)
|
322 |
-
|
323 |
-
return interface
|
324 |
-
|
325 |
-
if __name__ == "__main__":
|
326 |
-
interface = create_interface()
|
327 |
-
interface.launch(
|
328 |
-
server_name="0.0.0.0", # Dış bağlantılara izin ver
|
329 |
-
share=True, # Public link oluştur
|
330 |
-
server_port=7860 # Space'in varsayılan portu
|
331 |
-
)
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import pandas as pd
|
3 |
+
from scrape.trendyol_scraper import scrape_reviews
|
4 |
+
from scripts.review_summarizer import ReviewAnalyzer
|
5 |
+
import plotly.express as px
|
6 |
+
import plotly.graph_objects as go
|
7 |
+
import os
|
8 |
+
import subprocess
|
9 |
+
|
10 |
+
# ChromeDriver kurulumu için fonksiyon
|
11 |
+
def setup_chrome():
|
12 |
+
# Chrome kurulumu
|
13 |
+
os.system('apt-get update && apt-get install -y wget gnupg')
|
14 |
+
os.system('wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -')
|
15 |
+
os.system('echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list')
|
16 |
+
os.system('apt-get update && apt-get install -y google-chrome-stable')
|
17 |
+
|
18 |
+
# ChromeDriver kurulumu
|
19 |
+
chrome_version = subprocess.check_output(['google-chrome', '--version']).decode().strip().split()[2].split('.')[0]
|
20 |
+
os.system(f'wget -q "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_{chrome_version}" -O chrome_version')
|
21 |
+
os.system('wget -q "https://chromedriver.storage.googleapis.com/$(cat chrome_version)/chromedriver_linux64.zip"')
|
22 |
+
os.system('unzip chromedriver_linux64.zip && mv chromedriver /usr/local/bin/ && chmod +x /usr/local/bin/chromedriver')
|
23 |
+
|
24 |
+
# Ana uygulama başlamadan önce Chrome kurulumunu yap
|
25 |
+
setup_chrome()
|
26 |
+
|
27 |
+
class ReviewAnalysisApp:
|
28 |
+
def __init__(self):
|
29 |
+
self.analyzer = ReviewAnalyzer()
|
30 |
+
|
31 |
+
def analyze_url(self, url):
|
32 |
+
# Yorumları çek
|
33 |
+
df = scrape_reviews(url)
|
34 |
+
|
35 |
+
# Sentiment analizi yap
|
36 |
+
analyzed_df = self.analyzer.analyze_reviews(df)
|
37 |
+
|
38 |
+
# Özet oluştur
|
39 |
+
summary = self.analyzer.generate_summary(analyzed_df)
|
40 |
+
|
41 |
+
# Grafikleri oluştur
|
42 |
+
fig1 = self.create_sentiment_distribution(analyzed_df)
|
43 |
+
fig2 = self.create_rating_distribution(analyzed_df)
|
44 |
+
fig3 = self.create_sentiment_by_rating(analyzed_df)
|
45 |
+
|
46 |
+
return summary, fig1, fig2, fig3
|
47 |
+
|
48 |
+
def create_sentiment_distribution(self, df):
|
49 |
+
fig = px.pie(df,
|
50 |
+
names='sentiment_label',
|
51 |
+
title='Duygu Analizi Dağılımı')
|
52 |
+
return fig
|
53 |
+
|
54 |
+
def create_rating_distribution(self, df):
|
55 |
+
fig = px.bar(df['Yıldız Sayısı'].value_counts().sort_index(),
|
56 |
+
title='Yıldız Dağılımı')
|
57 |
+
fig.update_layout(xaxis_title='Yıldız Sayısı',
|
58 |
+
yaxis_title='Yorum Sayısı')
|
59 |
+
return fig
|
60 |
+
|
61 |
+
def create_sentiment_by_rating(self, df):
|
62 |
+
avg_sentiment = df.groupby('Yıldız Sayısı')['sentiment_score'].mean()
|
63 |
+
fig = px.line(avg_sentiment,
|
64 |
+
title='Yıldız Sayısına Göre Ortalama Sentiment Skoru')
|
65 |
+
fig.update_layout(xaxis_title='Yıldız Sayısı',
|
66 |
+
yaxis_title='Ortalama Sentiment Skoru')
|
67 |
+
return fig
|
68 |
+
|
69 |
+
def create_interface():
|
70 |
+
app = ReviewAnalysisApp()
|
71 |
+
|
72 |
+
with gr.Blocks(theme=gr.themes.Soft()) as interface:
|
73 |
+
gr.Markdown("# Trendyol Yorum Analizi")
|
74 |
+
|
75 |
+
with gr.Row():
|
76 |
+
url_input = gr.Textbox(
|
77 |
+
label="Trendyol Ürün Yorumları URL'si",
|
78 |
+
placeholder="https://www.trendyol.com/..."
|
79 |
+
)
|
80 |
+
|
81 |
+
analyze_btn = gr.Button("Analiz Et")
|
82 |
+
|
83 |
+
with gr.Row():
|
84 |
+
with gr.Column(scale=1):
|
85 |
+
summary_output = gr.Textbox(
|
86 |
+
label="Özet",
|
87 |
+
lines=10
|
88 |
+
)
|
89 |
+
|
90 |
+
with gr.Column(scale=2):
|
91 |
+
with gr.Tab("Duygu Analizi"):
|
92 |
+
sentiment_dist = gr.Plot()
|
93 |
+
with gr.Tab("Yıldız Dağılımı"):
|
94 |
+
rating_dist = gr.Plot()
|
95 |
+
with gr.Tab("Sentiment-Yıldız İlişkisi"):
|
96 |
+
sentiment_rating = gr.Plot()
|
97 |
+
|
98 |
+
analyze_btn.click(
|
99 |
+
fn=app.analyze_url,
|
100 |
+
inputs=[url_input],
|
101 |
+
outputs=[summary_output, sentiment_dist, rating_dist, sentiment_rating]
|
102 |
+
)
|
103 |
+
|
104 |
+
return interface
|
105 |
+
|
106 |
+
if __name__ == "__main__":
|
107 |
+
interface = create_interface()
|
108 |
+
interface.launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
@@ -1,15 +1,14 @@
|
|
1 |
-
pandas
|
2 |
-
numpy
|
3 |
-
torch
|
4 |
-
transformers
|
5 |
-
nltk
|
6 |
-
plotly
|
7 |
-
gradio
|
8 |
-
selenium
|
9 |
-
|
10 |
-
tqdm
|
11 |
-
regex
|
12 |
-
scikit-learn
|
13 |
-
|
14 |
-
python-dotenv
|
15 |
-
google-generativeai
|
|
|
1 |
+
pandas
|
2 |
+
numpy
|
3 |
+
torch
|
4 |
+
transformers
|
5 |
+
nltk
|
6 |
+
plotly
|
7 |
+
gradio
|
8 |
+
selenium
|
9 |
+
webdriver-manager
|
10 |
+
tqdm
|
11 |
+
regex
|
12 |
+
scikit-learn
|
13 |
+
google-generativeai
|
14 |
+
python-dotenv
|
|
scrape/trendyol_scraper.py
CHANGED
@@ -3,136 +3,108 @@ from selenium.webdriver.chrome.service import Service
|
|
3 |
from selenium.webdriver.common.by import By
|
4 |
from selenium.webdriver.support.ui import WebDriverWait
|
5 |
from selenium.webdriver.support import expected_conditions as EC
|
6 |
-
from selenium.webdriver.chrome.options import Options
|
7 |
import time
|
8 |
import pandas as pd
|
9 |
import os
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
|
12 |
def scrape_reviews(url):
|
13 |
-
|
14 |
-
|
15 |
data_directory = "data"
|
16 |
if not os.path.exists(data_directory):
|
17 |
os.makedirs(data_directory)
|
18 |
|
19 |
-
|
20 |
-
|
21 |
-
last_height = driver.execute_script("return document.body.scrollHeight")
|
22 |
-
scroll_count = 0
|
23 |
-
while True:
|
24 |
-
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
|
25 |
-
time.sleep(3)
|
26 |
-
new_height = driver.execute_script("return document.body.scrollHeight")
|
27 |
-
scroll_count += 1
|
28 |
-
print(f"Scroll {scroll_count}: {new_height}")
|
29 |
-
if new_height == last_height:
|
30 |
-
break
|
31 |
-
last_height = new_height
|
32 |
-
print("Sayfa kaydırma tamamlandı")
|
33 |
-
|
34 |
-
chrome_options = Options()
|
35 |
chrome_options.add_argument('--headless')
|
|
|
36 |
chrome_options.add_argument('--no-sandbox')
|
37 |
chrome_options.add_argument('--disable-dev-shm-usage')
|
38 |
-
chrome_options.add_argument('--disable-gpu')
|
39 |
-
chrome_options.add_argument('--lang=tr')
|
40 |
-
chrome_options.add_argument('--disable-notifications')
|
41 |
chrome_options.add_argument("--window-size=1920,1080")
|
42 |
-
|
43 |
try:
|
44 |
-
|
45 |
-
service = Service()
|
46 |
driver = webdriver.Chrome(service=service, options=chrome_options)
|
47 |
|
48 |
-
print(f"URL'ye gidiliyor: {url}")
|
49 |
driver.get(url)
|
50 |
-
time.sleep(5) # Sayfa yüklenme süresini artırdık
|
51 |
-
|
52 |
-
# Sayfa kaynağını kontrol et
|
53 |
-
page_source = driver.page_source
|
54 |
-
print(f"Sayfa uzunluğu: {len(page_source)}")
|
55 |
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
)
|
61 |
-
cookie_button.click()
|
62 |
-
print("Çerez popup'ı kapatıldı")
|
63 |
-
except Exception as e:
|
64 |
-
print(f"Çerez popup'ı işlemi: {str(e)}")
|
65 |
|
66 |
comprehensive_scroll(driver)
|
67 |
-
|
68 |
-
print("Yorum elementleri aranıyor...")
|
69 |
-
# Önce yorum container'ını bul
|
70 |
-
review_container = driver.find_element(By.CLASS_NAME, "pr-rnr-com-w")
|
71 |
-
print("Yorum container'ı bulundu")
|
72 |
|
73 |
-
|
74 |
-
comment_elements = review_container.find_elements(By.CLASS_NAME, "comment-cards-item")
|
75 |
total_comments = len(comment_elements)
|
76 |
-
|
77 |
-
if total_comments == 0:
|
78 |
-
print("Alternatif yorum elementi aranıyor...")
|
79 |
-
comment_elements = driver.find_elements(By.CSS_SELECTOR, "div.comment-cards-item")
|
80 |
-
total_comments = len(comment_elements)
|
81 |
-
|
82 |
-
print(f"Bulunan yorum sayısı: {total_comments}")
|
83 |
-
|
84 |
-
if total_comments == 0:
|
85 |
-
print("Hiç yorum bulunamadı!")
|
86 |
-
return pd.DataFrame()
|
87 |
-
|
88 |
data = []
|
89 |
-
for i
|
|
|
90 |
try:
|
91 |
-
|
|
|
92 |
except:
|
93 |
username = "N/A"
|
94 |
-
print(f"Kullanıcı adı alınamadı: {i}")
|
95 |
|
96 |
try:
|
97 |
-
|
|
|
98 |
except:
|
99 |
comment = "N/A"
|
100 |
-
print(f"Yorum metni alınamadı: {i}")
|
101 |
|
102 |
try:
|
103 |
-
|
|
|
104 |
except:
|
105 |
date = "N/A"
|
106 |
-
print(f"Tarih alınamadı: {i}")
|
107 |
|
|
|
108 |
try:
|
109 |
-
|
|
|
110 |
except:
|
111 |
-
|
112 |
-
print(f"Yıldız sayısı alınamadı: {i}")
|
113 |
|
114 |
data.append({
|
115 |
-
"Kullanıcı_id":
|
116 |
"Kullanıcı Adı": username,
|
117 |
"Yorum": comment,
|
118 |
"Tarih": date,
|
119 |
-
"Yıldız Sayısı":
|
120 |
})
|
121 |
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
|
|
127 |
|
128 |
except Exception as e:
|
129 |
-
print(f"
|
130 |
-
|
131 |
-
|
132 |
-
print(driver.page_source[:500]) # İlk 500 karakteri göster
|
133 |
-
return pd.DataFrame()
|
134 |
-
|
135 |
finally:
|
136 |
-
|
137 |
-
|
138 |
-
|
|
|
|
3 |
from selenium.webdriver.common.by import By
|
4 |
from selenium.webdriver.support.ui import WebDriverWait
|
5 |
from selenium.webdriver.support import expected_conditions as EC
|
|
|
6 |
import time
|
7 |
import pandas as pd
|
8 |
import os
|
9 |
+
|
10 |
+
def comprehensive_scroll(driver):
|
11 |
+
# Scroll until no more new content is loaded
|
12 |
+
last_height = driver.execute_script("return document.body.scrollHeight")
|
13 |
+
while True:
|
14 |
+
# Scroll to bottom
|
15 |
+
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
|
16 |
+
time.sleep(3) # Wait for potential content loading
|
17 |
+
|
18 |
+
# Calculate new scroll height
|
19 |
+
new_height = driver.execute_script("return document.body.scrollHeight")
|
20 |
+
|
21 |
+
# Check if bottom has been reached
|
22 |
+
if new_height == last_height:
|
23 |
+
break
|
24 |
+
|
25 |
+
last_height = new_height
|
26 |
|
27 |
def scrape_reviews(url):
|
28 |
+
"""URL'den yorumları çeken fonksiyon"""
|
29 |
+
# Data directory oluştur
|
30 |
data_directory = "data"
|
31 |
if not os.path.exists(data_directory):
|
32 |
os.makedirs(data_directory)
|
33 |
|
34 |
+
# Chrome options ayarları
|
35 |
+
chrome_options = webdriver.ChromeOptions()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
chrome_options.add_argument('--headless')
|
37 |
+
chrome_options.add_argument('--disable-gpu')
|
38 |
chrome_options.add_argument('--no-sandbox')
|
39 |
chrome_options.add_argument('--disable-dev-shm-usage')
|
|
|
|
|
|
|
40 |
chrome_options.add_argument("--window-size=1920,1080")
|
41 |
+
|
42 |
try:
|
43 |
+
# Linux için ChromeDriver ayarı
|
44 |
+
service = Service('chromedriver') # Linux'ta path belirtmeye gerek yok
|
45 |
driver = webdriver.Chrome(service=service, options=chrome_options)
|
46 |
|
|
|
47 |
driver.get(url)
|
|
|
|
|
|
|
|
|
|
|
48 |
|
49 |
+
# Çerez popup'ını kabul et
|
50 |
+
WebDriverWait(driver, 10).until(
|
51 |
+
EC.element_to_be_clickable((By.ID, 'onetrust-accept-btn-handler'))
|
52 |
+
).click()
|
|
|
|
|
|
|
|
|
|
|
53 |
|
54 |
comprehensive_scroll(driver)
|
|
|
|
|
|
|
|
|
|
|
55 |
|
56 |
+
comment_elements = driver.find_elements(By.XPATH, '/html/body/div[1]/div[4]/div/div/div/div/div[3]/div/div/div[3]/div[2]/div')
|
|
|
57 |
total_comments = len(comment_elements)
|
58 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
data = []
|
60 |
+
for i in range(1, total_comments + 1):
|
61 |
+
kullanıcı_id = i
|
62 |
try:
|
63 |
+
username_xpath = f'/html/body/div[1]/div[4]/div/div/div/div/div[3]/div/div/div[3]/div[2]/div[{i}]/div[1]/div[2]/div[1]'
|
64 |
+
username = driver.find_element(By.XPATH, username_xpath).text
|
65 |
except:
|
66 |
username = "N/A"
|
|
|
67 |
|
68 |
try:
|
69 |
+
comment_xpath = f'/html/body/div[1]/div[4]/div/div/div/div/div[3]/div/div/div[3]/div[2]/div[{i}]/div[2]/p'
|
70 |
+
comment = driver.find_element(By.XPATH, comment_xpath).text
|
71 |
except:
|
72 |
comment = "N/A"
|
|
|
73 |
|
74 |
try:
|
75 |
+
date_xpath = f'/html/body/div[1]/div[4]/div/div/div/div/div[3]/div/div/div[3]/div[2]/div[{i}]/div[1]/div[2]/div[2]'
|
76 |
+
date = driver.find_element(By.XPATH, date_xpath).text
|
77 |
except:
|
78 |
date = "N/A"
|
|
|
79 |
|
80 |
+
star_xpath_base = f'/html/body/div[1]/div[4]/div/div/div/div/div[3]/div/div/div[3]/div[2]/div[{i}]/div[1]/div[1]/div'
|
81 |
try:
|
82 |
+
full_stars = driver.find_elements(By.XPATH, f"{star_xpath_base}/div[@class='star-w']/div[@class='full'][@style='width: 100%; max-width: 100%;']")
|
83 |
+
star_count = len(full_stars)
|
84 |
except:
|
85 |
+
star_count = 0
|
|
|
86 |
|
87 |
data.append({
|
88 |
+
"Kullanıcı_id": kullanıcı_id,
|
89 |
"Kullanıcı Adı": username,
|
90 |
"Yorum": comment,
|
91 |
"Tarih": date,
|
92 |
+
"Yıldız Sayısı": star_count
|
93 |
})
|
94 |
|
95 |
+
# Geçici dosya olarak kaydet
|
96 |
+
temp_file = os.path.join(data_directory, 'temp_comments.csv')
|
97 |
+
df = pd.DataFrame(data)
|
98 |
+
df.to_csv(temp_file, index=False, encoding='utf-8-sig')
|
99 |
+
|
100 |
+
return df
|
101 |
|
102 |
except Exception as e:
|
103 |
+
print(f"Hata oluştu: {str(e)}")
|
104 |
+
return pd.DataFrame() # Boş DataFrame döndür
|
105 |
+
|
|
|
|
|
|
|
106 |
finally:
|
107 |
+
driver.quit()
|
108 |
+
# Geçici dosyayı sil
|
109 |
+
if os.path.exists(os.path.join(data_directory, 'temp_comments.csv')):
|
110 |
+
os.remove(os.path.join(data_directory, 'temp_comments.csv'))
|
scripts/data_prp_eda.py
ADDED
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
import numpy as np
|
3 |
+
import matplotlib.pyplot as plt
|
4 |
+
import seaborn as sns
|
5 |
+
from wordcloud import WordCloud
|
6 |
+
import re
|
7 |
+
from collections import Counter
|
8 |
+
from datetime import datetime
|
9 |
+
import warnings
|
10 |
+
from textblob import TextBlob
|
11 |
+
import nltk
|
12 |
+
from nltk.corpus import stopwords
|
13 |
+
from nltk.tokenize import word_tokenize
|
14 |
+
from nltk.util import ngrams
|
15 |
+
import requests
|
16 |
+
import os
|
17 |
+
warnings.filterwarnings('ignore')
|
18 |
+
plt.style.use('seaborn')
|
19 |
+
|
20 |
+
nltk.download('stopwords')
|
21 |
+
nltk.download('punkt')
|
22 |
+
|
23 |
+
class ReviewAnalyzer:
|
24 |
+
def __init__(self, file_path):
|
25 |
+
self.df = pd.read_csv(file_path)
|
26 |
+
self.turkish_stopwords = self.get_turkish_stopwords()
|
27 |
+
|
28 |
+
# Lojistik ve satıcı ile ilgili kelimeleri genişletilmiş liste ile tanımla
|
29 |
+
self.logistics_seller_words = {
|
30 |
+
# Kargo ve teslimat ile ilgili
|
31 |
+
'kargo', 'kargocu', 'paket', 'paketleme', 'teslimat', 'teslim',
|
32 |
+
'gönderi', 'gönderim', 'ulaştı', 'ulaşım', 'geldi', 'kurye',
|
33 |
+
'dağıtım', 'hasarlı', 'hasar', 'kutu', 'ambalaj', 'zamanında',
|
34 |
+
'geç', 'hızlı', 'yavaş', 'günde', 'saatte',
|
35 |
+
|
36 |
+
# Satıcı ve mağaza ile ilgili
|
37 |
+
'satıcı', 'mağaza', 'sipariş', 'trendyol', 'tedarik', 'stok',
|
38 |
+
'garanti', 'fatura', 'iade', 'geri', 'müşteri', 'hizmet',
|
39 |
+
'destek', 'iletişim', 'şikayet', 'sorun', 'çözüm', 'hediye',
|
40 |
+
|
41 |
+
# Fiyat ve ödeme ile ilgili
|
42 |
+
'fiyat', 'ücret', 'para', 'bedava', 'ücretsiz', 'indirim',
|
43 |
+
'kampanya', 'taksit', 'ödeme', 'bütçe', 'hesap', 'kur',
|
44 |
+
|
45 |
+
# Zaman ile ilgili teslimat kelimeleri
|
46 |
+
'bugün', 'yarın', 'dün', 'hafta', 'gün', 'saat', 'süre',
|
47 |
+
'bekleme', 'gecikme', 'erken', 'geç'
|
48 |
+
}
|
49 |
+
|
50 |
+
# Sentiment analizi için kelimeler
|
51 |
+
self.positive_words = {
|
52 |
+
'güzel', 'harika', 'mükemmel', 'süper', 'iyi', 'muhteşem',
|
53 |
+
'teşekkür', 'memnun', 'başarılı', 'kaliteli', 'kusursuz',
|
54 |
+
'özgün', 'şahane', 'enfes', 'ideal'
|
55 |
+
}
|
56 |
+
|
57 |
+
self.negative_words = {
|
58 |
+
'kötü', 'berbat', 'rezalet', 'yetersiz', 'başarısız', 'vasat',
|
59 |
+
'korkunç', 'düşük', 'zayıf', 'çöp', 'pişman', 'kırık', 'bozuk'
|
60 |
+
}
|
61 |
+
|
62 |
+
# Türkçe-İngilizce ay çevirisi
|
63 |
+
self.month_map = {
|
64 |
+
'Ocak': 'January', 'Şubat': 'February', 'Mart': 'March',
|
65 |
+
'Nisan': 'April', 'Mayıs': 'May', 'Haziran': 'June',
|
66 |
+
'Temmuz': 'July', 'Ağustos': 'August', 'Eylül': 'September',
|
67 |
+
'Ekim': 'October', 'Kasım': 'November', 'Aralık': 'December'
|
68 |
+
}
|
69 |
+
|
70 |
+
def get_turkish_stopwords(self):
|
71 |
+
"""Türkçe stop words listesini oluştur"""
|
72 |
+
turkish_stops = set(stopwords.words('turkish'))
|
73 |
+
|
74 |
+
github_url = "https://raw.githubusercontent.com/sgsinclair/trombone/master/src/main/resources/org/voyanttools/trombone/keywords/stop.tr.turkish-lucene.txt"
|
75 |
+
try:
|
76 |
+
response = requests.get(github_url)
|
77 |
+
if response.status_code == 200:
|
78 |
+
github_stops = set(word.strip() for word in response.text.split('\n') if word.strip())
|
79 |
+
turkish_stops.update(github_stops)
|
80 |
+
except Exception as e:
|
81 |
+
print(f"GitHub'dan stop words çekilirken hata oluştu: {e}")
|
82 |
+
|
83 |
+
custom_stops = {'bir', 've', 'çok', 'bu', 'de', 'da', 'için', 'ile', 'ben',
|
84 |
+
'sen', 'o', 'biz', 'siz', 'onlar', 'bu', 'şu', 'ama', 'fakat',
|
85 |
+
'ancak', 'lakin', 'ki', 'dahi', 'mi', 'mı', 'mu', 'mü'}
|
86 |
+
turkish_stops.update(custom_stops)
|
87 |
+
|
88 |
+
return turkish_stops
|
89 |
+
|
90 |
+
def filter_product_reviews(self):
|
91 |
+
"""Salt ürün yorumlarını filtrele"""
|
92 |
+
def is_pure_product_review(text):
|
93 |
+
if not isinstance(text, str):
|
94 |
+
return False
|
95 |
+
|
96 |
+
text_lower = text.lower()
|
97 |
+
return not any(word in text_lower for word in self.logistics_seller_words)
|
98 |
+
|
99 |
+
# Filtrelenmiş DataFrame
|
100 |
+
original_count = len(self.df)
|
101 |
+
self.df = self.df[self.df['Yorum'].apply(is_pure_product_review)]
|
102 |
+
filtered_count = len(self.df)
|
103 |
+
|
104 |
+
print(f"\nFiltreleme İstatistikleri:")
|
105 |
+
print(f"Orijinal yorum sayısı: {original_count}")
|
106 |
+
print(f"Salt ürün yorumu sayısı: {filtered_count}")
|
107 |
+
print(f"Çıkarılan yorum sayısı: {original_count - filtered_count}")
|
108 |
+
print(f"Filtreleme oranı: {((original_count - filtered_count) / original_count * 100):.2f}%")
|
109 |
+
|
110 |
+
print("\nÖrnek Salt Ürün Yorumları:")
|
111 |
+
sample_reviews = self.df['Yorum'].sample(min(3, len(self.df)))
|
112 |
+
for idx, review in enumerate(sample_reviews, 1):
|
113 |
+
print(f"{idx}. {review[:100]}...")
|
114 |
+
|
115 |
+
def convert_turkish_date(self, date_str):
|
116 |
+
"""Türkçe tarihleri İngilizce'ye çevir"""
|
117 |
+
try:
|
118 |
+
day, month, year = date_str.split()
|
119 |
+
english_month = self.month_map[month]
|
120 |
+
return f"{day} {english_month} {year}"
|
121 |
+
except:
|
122 |
+
return None
|
123 |
+
|
124 |
+
def preprocess_text(self, text):
|
125 |
+
"""Metin ön işleme"""
|
126 |
+
if isinstance(text, str):
|
127 |
+
text = text.lower()
|
128 |
+
text = re.sub(r'[^\w\s]', '', text)
|
129 |
+
text = re.sub(r'\d+', '', text)
|
130 |
+
text = re.sub(r'\s+', ' ', text).strip()
|
131 |
+
return text
|
132 |
+
return ''
|
133 |
+
|
134 |
+
|
135 |
+
def analyze_timestamps(self):
|
136 |
+
"""Zaman bazlı analizler"""
|
137 |
+
# Tarihleri dönüştür
|
138 |
+
self.df['Tarih'] = self.df['Tarih'].apply(self.convert_turkish_date)
|
139 |
+
self.df['Tarih'] = pd.to_datetime(self.df['Tarih'], format='%d %B %Y')
|
140 |
+
|
141 |
+
# Günlük dağılım
|
142 |
+
plt.figure(figsize=(12, 6))
|
143 |
+
plt.hist(self.df['Tarih'], bins=20, edgecolor='black')
|
144 |
+
plt.title('Yorumların Zaman İçindeki Dağılımı')
|
145 |
+
plt.xlabel('Tarih')
|
146 |
+
plt.ylabel('Yorum Sayısı')
|
147 |
+
plt.xticks(rotation=45)
|
148 |
+
plt.tight_layout()
|
149 |
+
plt.savefig('images/yorum_zaman_dagilimi.png')
|
150 |
+
plt.close()
|
151 |
+
|
152 |
+
# Aylık dağılım
|
153 |
+
monthly_reviews = self.df.groupby(self.df['Tarih'].dt.to_period('M')).size()
|
154 |
+
plt.figure(figsize=(12, 6))
|
155 |
+
monthly_reviews.plot(kind='bar')
|
156 |
+
plt.title('Aylık Yorum Dağılımı')
|
157 |
+
plt.xlabel('Ay')
|
158 |
+
plt.ylabel('Yorum Sayısı')
|
159 |
+
plt.xticks(rotation=45)
|
160 |
+
plt.tight_layout()
|
161 |
+
plt.savefig('images/aylik_yorum_dagilimi.png')
|
162 |
+
plt.close()
|
163 |
+
|
164 |
+
# Mevsimsel analiz
|
165 |
+
self.df['Mevsim'] = self.df['Tarih'].dt.month.map({
|
166 |
+
12: 'Kış', 1: 'Kış', 2: 'Kış',
|
167 |
+
3: 'İlkbahar', 4: 'İlkbahar', 5: 'İlkbahar',
|
168 |
+
6: 'Yaz', 7: 'Yaz', 8: 'Yaz',
|
169 |
+
9: 'Sonbahar', 10: 'Sonbahar', 11: 'Sonbahar'
|
170 |
+
})
|
171 |
+
seasonal_reviews = self.df.groupby('Mevsim').size()
|
172 |
+
plt.figure(figsize=(10, 6))
|
173 |
+
seasonal_reviews.plot(kind='bar')
|
174 |
+
plt.title('Mevsimsel Yorum Dağılımı')
|
175 |
+
plt.xlabel('Mevsim')
|
176 |
+
plt.ylabel('Yorum Sayısı')
|
177 |
+
plt.tight_layout()
|
178 |
+
plt.savefig('images/mevsimsel_dagilim.png')
|
179 |
+
plt.close()
|
180 |
+
|
181 |
+
def analyze_ratings(self):
|
182 |
+
"""Yıldız bazlı analizler"""
|
183 |
+
plt.figure(figsize=(10, 6))
|
184 |
+
sns.countplot(data=self.df, x='Yıldız Sayısı')
|
185 |
+
plt.title('Yıldız Dağılımı')
|
186 |
+
plt.xlabel('Yıldız Sayısı')
|
187 |
+
plt.ylabel('Yorum Sayısı')
|
188 |
+
plt.savefig('images/yildiz_dagilimi.png')
|
189 |
+
plt.close()
|
190 |
+
|
191 |
+
return {
|
192 |
+
'Ortalama Yıldız': self.df['Yıldız Sayısı'].mean(),
|
193 |
+
'Medyan Yıldız': self.df['Yıldız Sayısı'].median(),
|
194 |
+
'Mod Yıldız': self.df['Yıldız Sayısı'].mode()[0],
|
195 |
+
'Standart Sapma': self.df['Yıldız Sayısı'].std()
|
196 |
+
}
|
197 |
+
|
198 |
+
def create_wordcloud(self):
|
199 |
+
"""Kelime bulutu oluştur"""
|
200 |
+
all_comments = ' '.join([self.preprocess_text(str(comment))
|
201 |
+
for comment in self.df['Yorum']])
|
202 |
+
|
203 |
+
words = word_tokenize(all_comments)
|
204 |
+
filtered_words = [word for word in words
|
205 |
+
if word not in self.turkish_stopwords]
|
206 |
+
clean_text = ' '.join(filtered_words)
|
207 |
+
|
208 |
+
wordcloud = WordCloud(
|
209 |
+
width=800, height=400,
|
210 |
+
background_color='white',
|
211 |
+
max_words=100,
|
212 |
+
font_path='C:/Windows/Fonts/arial.ttf' # Windows varsayılan font
|
213 |
+
).generate(clean_text)
|
214 |
+
|
215 |
+
plt.figure(figsize=(15,8))
|
216 |
+
plt.imshow(wordcloud, interpolation='bilinear')
|
217 |
+
plt.axis('off')
|
218 |
+
plt.savefig('images/wordcloud.png')
|
219 |
+
plt.close()
|
220 |
+
|
221 |
+
def analyze_ngrams(self, max_n=3, top_n=10):
|
222 |
+
"""N-gram analizi"""
|
223 |
+
all_texts = []
|
224 |
+
for comment in self.df['Yorum']:
|
225 |
+
if isinstance(comment, str):
|
226 |
+
words = self.preprocess_text(comment).split()
|
227 |
+
filtered_words = [word for word in words
|
228 |
+
if word not in self.turkish_stopwords]
|
229 |
+
all_texts.extend(filtered_words)
|
230 |
+
|
231 |
+
for n in range(1, max_n + 1):
|
232 |
+
print(f"\n{n}-gram Analizi:")
|
233 |
+
|
234 |
+
if n == 1:
|
235 |
+
ngrams_list = all_texts
|
236 |
+
else:
|
237 |
+
ngrams_list = list(ngrams(all_texts, n))
|
238 |
+
|
239 |
+
ngram_freq = Counter(ngrams_list).most_common(top_n)
|
240 |
+
|
241 |
+
if n == 1:
|
242 |
+
labels = [item[0] for item in ngram_freq]
|
243 |
+
else:
|
244 |
+
labels = [' '.join(item[0]) for item in ngram_freq]
|
245 |
+
|
246 |
+
values = [item[1] for item in ngram_freq]
|
247 |
+
|
248 |
+
plt.figure(figsize=(12, 6))
|
249 |
+
bars = plt.barh(range(len(values)), values)
|
250 |
+
plt.yticks(range(len(labels)), labels)
|
251 |
+
plt.title(f'En Sık Kullanılan {n}-gramlar')
|
252 |
+
plt.xlabel('Frekans')
|
253 |
+
|
254 |
+
for i, bar in enumerate(bars):
|
255 |
+
width = bar.get_width()
|
256 |
+
plt.text(width, bar.get_y() + bar.get_height()/2,
|
257 |
+
f'{int(width)}',
|
258 |
+
ha='left', va='center', fontweight='bold')
|
259 |
+
|
260 |
+
plt.tight_layout()
|
261 |
+
plt.savefig(f'images/{n}gram_analizi.png')
|
262 |
+
plt.close()
|
263 |
+
|
264 |
+
print(f"\nEn sık kullanılan {n}-gramlar:")
|
265 |
+
for ngram, freq in ngram_freq:
|
266 |
+
if n == 1:
|
267 |
+
print(f"{ngram}: {freq}")
|
268 |
+
else:
|
269 |
+
print(f"{' '.join(ngram)}: {freq}")
|
270 |
+
|
271 |
+
def analyze_sentiment(self):
|
272 |
+
"""Duygu analizi"""
|
273 |
+
def count_sentiment_words(text):
|
274 |
+
if not isinstance(text, str):
|
275 |
+
return 0, 0
|
276 |
+
|
277 |
+
text_lower = text.lower()
|
278 |
+
words = text_lower.split()
|
279 |
+
positive_count = sum(1 for word in words if word in self.positive_words)
|
280 |
+
negative_count = sum(1 for word in words if word in self.negative_words)
|
281 |
+
return positive_count, negative_count
|
282 |
+
|
283 |
+
sentiment_counts = self.df['Yorum'].apply(count_sentiment_words)
|
284 |
+
self.df['Pozitif_Kelime_Sayisi'] = [count[0] for count in sentiment_counts]
|
285 |
+
self.df['Negatif_Kelime_Sayisi'] = [count[1] for count in sentiment_counts]
|
286 |
+
self.df['Sentiment_Skor'] = self.df['Pozitif_Kelime_Sayisi'] - self.df['Negatif_Kelime_Sayisi']
|
287 |
+
|
288 |
+
plt.figure(figsize=(10, 6))
|
289 |
+
sns.boxplot(data=self.df, x='Yıldız Sayısı', y='Sentiment_Skor')
|
290 |
+
plt.title('Yıldız Sayısı ve Sentiment Skoru İlişkisi')
|
291 |
+
plt.savefig('images/sentiment_yildiz_iliskisi.png')
|
292 |
+
plt.close()
|
293 |
+
|
294 |
+
plt.figure(figsize=(10, 6))
|
295 |
+
plt.hist(self.df['Sentiment_Skor'], bins=20)
|
296 |
+
plt.title('Sentiment Skor Dağılımı')
|
297 |
+
plt.xlabel('Sentiment Skoru')
|
298 |
+
plt.ylabel('Yorum Sayısı')
|
299 |
+
plt.savefig('images/sentiment_dagilimi.png')
|
300 |
+
plt.close()
|
301 |
+
|
302 |
+
def analyze_comment_lengths(self):
|
303 |
+
"""Yorum uzunluğu analizi"""
|
304 |
+
self.df['Yorum_Uzunlugu'] = self.df['Yorum'].str.len()
|
305 |
+
|
306 |
+
plt.figure(figsize=(10, 6))
|
307 |
+
plt.hist(self.df['Yorum_Uzunlugu'].dropna(), bins=30)
|
308 |
+
plt.title('Yorum Uzunluğu Dağılımı')
|
309 |
+
plt.xlabel('Karakter Sayısı')
|
310 |
+
plt.ylabel('Yorum Sayısı')
|
311 |
+
plt.savefig('images/yorum_uzunluk_dagilimi.png')
|
312 |
+
plt.close()
|
313 |
+
|
314 |
+
plt.figure(figsize=(10, 6))
|
315 |
+
sns.boxplot(data=self.df, x='Yıldız Sayısı', y='Yorum_Uzunlugu')
|
316 |
+
plt.title('Yıldız Sayısı ve Yorum Uzunluğu İlişkisi')
|
317 |
+
plt.xlabel('Yıldız')
|
318 |
+
plt.ylabel('Yorum Uzunluğu (Karakter)')
|
319 |
+
plt.savefig('images/yildiz_uzunluk_iliskisi.png')
|
320 |
+
plt.close()
|
321 |
+
|
322 |
+
def run_analysis(self):
|
323 |
+
"""Ana analiz fonksiyonu"""
|
324 |
+
print("Analiz başlatılıyor...")
|
325 |
+
|
326 |
+
if not os.path.exists('images'):
|
327 |
+
os.makedirs('images')
|
328 |
+
|
329 |
+
print("\nÜrün odaklı yorum filtresi uygulanıyor...")
|
330 |
+
self.filter_product_reviews()
|
331 |
+
|
332 |
+
print("\n1. Yorum Uzunluğu Analizi")
|
333 |
+
self.analyze_comment_lengths()
|
334 |
+
|
335 |
+
print("\n2. Zaman Analizi")
|
336 |
+
self.analyze_timestamps()
|
337 |
+
|
338 |
+
print("\n3. Yıldız Analizi")
|
339 |
+
rating_stats = self.analyze_ratings()
|
340 |
+
print("\nYıldız İstatistikleri:")
|
341 |
+
for key, value in rating_stats.items():
|
342 |
+
print(f"{key}: {value:.2f}")
|
343 |
+
|
344 |
+
print("\n4. Kelime Bulutu Oluşturuluyor")
|
345 |
+
self.create_wordcloud()
|
346 |
+
|
347 |
+
print("\n5. N-gram Analizleri")
|
348 |
+
self.analyze_ngrams(max_n=3, top_n=10)
|
349 |
+
|
350 |
+
print("\n6. Duygu Analizi")
|
351 |
+
self.analyze_sentiment()
|
352 |
+
|
353 |
+
print("\nAnaliz tamamlandı! Tüm görseller 'images' klasörüne kaydedildi.")
|
354 |
+
|
355 |
+
if __name__ == "__main__":
|
356 |
+
analyzer = ReviewAnalyzer('data/macbook_product_comments_with_ratings.csv')
|
357 |
+
analyzer.run_analysis()
|
scripts/review_summarizer.py
ADDED
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
import numpy as np
|
3 |
+
import matplotlib.pyplot as plt
|
4 |
+
import seaborn as sns
|
5 |
+
from transformers import (
|
6 |
+
AutoTokenizer,
|
7 |
+
AutoModelForSequenceClassification,
|
8 |
+
pipeline
|
9 |
+
)
|
10 |
+
import torch
|
11 |
+
import os
|
12 |
+
import requests
|
13 |
+
from collections import Counter
|
14 |
+
import warnings
|
15 |
+
from nltk.tokenize import word_tokenize
|
16 |
+
from nltk.util import ngrams
|
17 |
+
import nltk
|
18 |
+
from wordcloud import WordCloud
|
19 |
+
import re
|
20 |
+
import google.generativeai as genai
|
21 |
+
from dotenv import load_dotenv
|
22 |
+
warnings.filterwarnings('ignore')
|
23 |
+
|
24 |
+
nltk.download('stopwords')
|
25 |
+
nltk.download('punkt')
|
26 |
+
|
27 |
+
class ReviewAnalyzer:
|
28 |
+
def __init__(self):
|
29 |
+
# Load environment variables
|
30 |
+
load_dotenv()
|
31 |
+
|
32 |
+
# Configure Gemini API
|
33 |
+
genai.configure(api_key=os.getenv('GOOGLE_API_KEY'))
|
34 |
+
self.model = genai.GenerativeModel('gemini-pro')
|
35 |
+
|
36 |
+
# Diğer model kurulumları (sentiment analizi için)
|
37 |
+
self.setup_sentiment_model()
|
38 |
+
|
39 |
+
self.turkish_stopwords = self.get_turkish_stopwords()
|
40 |
+
|
41 |
+
# Lojistik ve satıcı ile ilgili kelimeleri tanımla
|
42 |
+
self.logistics_seller_words = {
|
43 |
+
# Kargo ve teslimat ile ilgili
|
44 |
+
'kargo', 'kargocu', 'paket', 'paketleme', 'teslimat', 'teslim',
|
45 |
+
'gönderi', 'gönderim', 'ulaştı', 'ulaşım', 'geldi', 'kurye',
|
46 |
+
'dağıtım', 'hasarlı', 'hasar', 'kutu', 'ambalaj', 'zamanında',
|
47 |
+
'geç', 'hızlı', 'yavaş', 'günde', 'saatte',
|
48 |
+
|
49 |
+
# Satıcı ve mağaza ile ilgili
|
50 |
+
'satıcı', 'mağaza', 'sipariş', 'trendyol', 'tedarik', 'stok',
|
51 |
+
'garanti', 'fatura', 'iade', 'geri', 'müşteri', 'hizmet',
|
52 |
+
'destek', 'iletişim', 'şikayet', 'sorun', 'çözüm', 'hediye',
|
53 |
+
|
54 |
+
# Fiyat ve ödeme ile ilgili
|
55 |
+
'fiyat', 'ücret', 'para', 'bedava', 'ücretsiz', 'indirim',
|
56 |
+
'kampanya', 'taksit', 'ödeme', 'bütçe', 'hesap', 'kur',
|
57 |
+
|
58 |
+
# Zaman ile ilgili teslimat kelimeleri
|
59 |
+
'bugün', 'yarın', 'dün', 'hafta', 'gün', 'saat', 'süre',
|
60 |
+
'bekleme', 'gecikme', 'erken', 'geç'
|
61 |
+
}
|
62 |
+
|
63 |
+
def get_turkish_stopwords(self):
|
64 |
+
"""Genişletilmiş stop words listesini hazırla"""
|
65 |
+
github_url = "https://raw.githubusercontent.com/sgsinclair/trombone/master/src/main/resources/org/voyanttools/trombone/keywords/stop.tr.turkish-lucene.txt"
|
66 |
+
stop_words = set()
|
67 |
+
|
68 |
+
try:
|
69 |
+
response = requests.get(github_url)
|
70 |
+
if response.status_code == 200:
|
71 |
+
github_stops = set(word.strip() for word in response.text.split('\n') if word.strip())
|
72 |
+
stop_words.update(github_stops)
|
73 |
+
except Exception as e:
|
74 |
+
print(f"GitHub'dan stop words çekilirken hata oluştu: {e}")
|
75 |
+
|
76 |
+
stop_words.update(set(nltk.corpus.stopwords.words('turkish')))
|
77 |
+
|
78 |
+
additional_stops = {'bir', 've', 'çok', 'bu', 'de', 'da', 'için', 'ile', 'ben', 'sen',
|
79 |
+
'o', 'biz', 'siz', 'onlar', 'bu', 'şu', 'ama', 'fakat', 'ancak',
|
80 |
+
'lakin', 'ki', 'dahi', 'mi', 'mı', 'mu', 'mü', 'var', 'yok',
|
81 |
+
'olan', 'içinde', 'üzerinde', 'bana', 'sana', 'ona', 'bize',
|
82 |
+
'size', 'onlara', 'evet', 'hayır', 'tamam', 'oldu', 'olmuş',
|
83 |
+
'olacak', 'etmek', 'yapmak', 'kez', 'kere', 'defa', 'adet'}
|
84 |
+
stop_words.update(additional_stops)
|
85 |
+
|
86 |
+
print(f"Toplam {len(stop_words)} adet stop words yüklendi.")
|
87 |
+
return stop_words
|
88 |
+
|
89 |
+
def preprocess_text(self, text):
|
90 |
+
"""Metin ön işleme"""
|
91 |
+
if isinstance(text, str):
|
92 |
+
# Küçük harfe çevir
|
93 |
+
text = text.lower()
|
94 |
+
# Özel karakterleri temizle
|
95 |
+
text = re.sub(r'[^\w\s]', '', text)
|
96 |
+
# Sayıları temizle
|
97 |
+
text = re.sub(r'\d+', '', text)
|
98 |
+
# Fazla boşlukları temizle
|
99 |
+
text = re.sub(r'\s+', ' ', text).strip()
|
100 |
+
# Stop words'leri çıkar
|
101 |
+
words = text.split()
|
102 |
+
words = [word for word in words if word not in self.turkish_stopwords]
|
103 |
+
return ' '.join(words)
|
104 |
+
return ''
|
105 |
+
|
106 |
+
def setup_sentiment_model(self):
|
107 |
+
"""Sentiment analiz modelini hazırla"""
|
108 |
+
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
109 |
+
print(f"Using device for sentiment: {self.device}")
|
110 |
+
|
111 |
+
model_name = "savasy/bert-base-turkish-sentiment-cased"
|
112 |
+
self.sentiment_tokenizer = AutoTokenizer.from_pretrained(model_name)
|
113 |
+
self.sentiment_model = (
|
114 |
+
AutoModelForSequenceClassification.from_pretrained(model_name)
|
115 |
+
.to(self.device)
|
116 |
+
.to(torch.float32)
|
117 |
+
)
|
118 |
+
|
119 |
+
def filter_reviews(self, df):
|
120 |
+
"""Ürün ile ilgili olmayan yorumları filtrele"""
|
121 |
+
def is_product_review(text):
|
122 |
+
if not isinstance(text, str):
|
123 |
+
return False
|
124 |
+
return not any(word in text.lower() for word in self.logistics_seller_words)
|
125 |
+
|
126 |
+
filtered_df = df[df['Yorum'].apply(is_product_review)].copy()
|
127 |
+
|
128 |
+
print(f"\nFiltreleme İstatistikleri:")
|
129 |
+
print(f"Toplam yorum sayısı: {len(df)}")
|
130 |
+
print(f"Ürün yorumu sayısı: {len(filtered_df)}")
|
131 |
+
print(f"Filtrelenen yorum sayısı: {len(df) - len(filtered_df)}")
|
132 |
+
print(f"Filtreleme oranı: {((len(df) - len(filtered_df)) / len(df) * 100):.2f}%")
|
133 |
+
|
134 |
+
return filtered_df
|
135 |
+
|
136 |
+
def analyze_sentiment(self, df):
|
137 |
+
"""Sentiment analizi yap"""
|
138 |
+
def predict_sentiment(text):
|
139 |
+
if not isinstance(text, str) or len(text.strip()) == 0:
|
140 |
+
return {"label": "Nötr", "score": 0.5}
|
141 |
+
|
142 |
+
try:
|
143 |
+
cleaned_text = self.preprocess_text(text)
|
144 |
+
|
145 |
+
inputs = self.sentiment_tokenizer(
|
146 |
+
cleaned_text,
|
147 |
+
return_tensors="pt",
|
148 |
+
truncation=True,
|
149 |
+
max_length=512,
|
150 |
+
padding=True
|
151 |
+
).to(self.device)
|
152 |
+
|
153 |
+
with torch.no_grad():
|
154 |
+
outputs = self.sentiment_model(**inputs)
|
155 |
+
probs = torch.nn.functional.softmax(outputs.logits, dim=1)
|
156 |
+
prediction = probs.cpu().numpy()[0]
|
157 |
+
|
158 |
+
score = float(prediction[1])
|
159 |
+
|
160 |
+
if score > 0.75:
|
161 |
+
label = "Pozitif"
|
162 |
+
elif score < 0.25:
|
163 |
+
label = "Negatif"
|
164 |
+
elif score > 0.55:
|
165 |
+
label = "Pozitif"
|
166 |
+
elif score < 0.45:
|
167 |
+
label = "Negatif"
|
168 |
+
else:
|
169 |
+
label = "Nötr"
|
170 |
+
|
171 |
+
return {"label": label, "score": score}
|
172 |
+
|
173 |
+
except Exception as e:
|
174 |
+
print(f"Error in sentiment prediction: {e}")
|
175 |
+
return {"label": "Nötr", "score": 0.5}
|
176 |
+
|
177 |
+
print("\nSentiment analizi yapılıyor...")
|
178 |
+
results = [predict_sentiment(text) for text in df['Yorum']]
|
179 |
+
|
180 |
+
df['sentiment_score'] = [r['score'] for r in results]
|
181 |
+
df['sentiment_label'] = [r['label'] for r in results]
|
182 |
+
df['cleaned_text'] = df['Yorum'].apply(self.preprocess_text)
|
183 |
+
|
184 |
+
return df
|
185 |
+
|
186 |
+
def get_key_phrases(self, text_series):
|
187 |
+
"""En önemli anahtar kelimeleri bul"""
|
188 |
+
text = ' '.join(text_series.astype(str))
|
189 |
+
words = self.preprocess_text(text).split()
|
190 |
+
word_freq = Counter(words)
|
191 |
+
# En az 3 kez geçen kelimeleri al
|
192 |
+
return {word: count for word, count in word_freq.items()
|
193 |
+
if count >= 3 and len(word) > 2}
|
194 |
+
|
195 |
+
def generate_summary(self, df):
|
196 |
+
"""Yorumları özetle"""
|
197 |
+
# Yorumları ve yıldızları birleştir
|
198 |
+
reviews_with_ratings = [
|
199 |
+
f"Yıldız: {row['Yıldız Sayısı']}, Yorum: {row['Yorum']}"
|
200 |
+
for _, row in df.iterrows()
|
201 |
+
]
|
202 |
+
|
203 |
+
# Prompt hazırla
|
204 |
+
prompt = f"""
|
205 |
+
Aşağıdaki ürün yorumlarını analiz edip özet çıkar:
|
206 |
+
|
207 |
+
{reviews_with_ratings[:50]} # İlk 50 yorumu al (API limiti için)
|
208 |
+
|
209 |
+
Lütfen şu başlıklar altında özetle:
|
210 |
+
1. Genel Değerlendirme
|
211 |
+
2. Olumlu Yönler
|
212 |
+
3. Olumsuz Yönler
|
213 |
+
4. Öneriler
|
214 |
+
|
215 |
+
Önemli: Yanıtını Türkçe olarak ver ve madde madde listele.
|
216 |
+
"""
|
217 |
+
|
218 |
+
try:
|
219 |
+
response = self.model.generate_content(prompt)
|
220 |
+
summary = response.text
|
221 |
+
except Exception as e:
|
222 |
+
summary = f"Özet oluşturulurken hata oluştu: {str(e)}"
|
223 |
+
|
224 |
+
return summary
|
225 |
+
|
226 |
+
def analyze_reviews(self, df):
|
227 |
+
"""Tüm yorumları analiz et"""
|
228 |
+
# Mevcut analiz fonksiyonu aynen kalabilir
|
229 |
+
pass
|
230 |
+
|
231 |
+
def analyze_reviews(file_path):
|
232 |
+
df = pd.read_csv(file_path)
|
233 |
+
|
234 |
+
analyzer = ReviewAnalyzer()
|
235 |
+
|
236 |
+
filtered_df = analyzer.filter_reviews(df)
|
237 |
+
|
238 |
+
print("Sentiment analizi başlatılıyor...")
|
239 |
+
analyzed_df = analyzer.analyze_sentiment(filtered_df)
|
240 |
+
|
241 |
+
analyzed_df.to_csv('sentiment_analyzed_reviews.csv', index=False, encoding='utf-8-sig')
|
242 |
+
print("Sentiment analizi tamamlandı ve kaydedildi.")
|
243 |
+
|
244 |
+
print("\nÜrün özeti oluşturuluyor...")
|
245 |
+
summary = analyzer.generate_summary(analyzed_df)
|
246 |
+
|
247 |
+
with open('urun_ozeti.txt', 'w', encoding='utf-8') as f:
|
248 |
+
f.write(summary)
|
249 |
+
|
250 |
+
print("\nÜrün Özeti:")
|
251 |
+
print("-" * 50)
|
252 |
+
print(summary)
|
253 |
+
print("\nÖzet 'urun_ozeti.txt' dosyasına kaydedildi.")
|
254 |
+
|
255 |
+
if __name__ == "__main__":
|
256 |
+
analyze_reviews('data/macbook_product_comments_with_ratings.csv')
|
scripts/sentiment_bert_model.py
ADDED
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
import numpy as np
|
3 |
+
import matplotlib.pyplot as plt
|
4 |
+
import seaborn as sns
|
5 |
+
from transformers import AutoTokenizer, AutoModelForSequenceClassification
|
6 |
+
import torch
|
7 |
+
import os
|
8 |
+
import warnings
|
9 |
+
warnings.filterwarnings('ignore')
|
10 |
+
|
11 |
+
class TurkishSentimentAnalyzer:
|
12 |
+
def __init__(self):
|
13 |
+
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
14 |
+
print(f"Using device: {self.device}")
|
15 |
+
|
16 |
+
# sentiment model
|
17 |
+
model_name = "savasy/bert-base-turkish-sentiment-cased"
|
18 |
+
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
|
19 |
+
self.model = AutoModelForSequenceClassification.from_pretrained(model_name).to(self.device)
|
20 |
+
|
21 |
+
# Lojistik ve satıcı kelimeleri
|
22 |
+
self.logistics_seller_words = {
|
23 |
+
'kargo', 'kargocu', 'paket', 'paketleme', 'teslimat', 'teslim',
|
24 |
+
'gönderi', 'gönderim', 'ulaştı', 'ulaşım', 'geldi', 'kurye',
|
25 |
+
'satıcı', 'mağaza', 'sipariş', 'trendyol', 'tedarik', 'stok',
|
26 |
+
'fiyat', 'ücret', 'para', 'bedava', 'indirim', 'kampanya',
|
27 |
+
'havale', 'ödeme', 'garanti', 'fatura'
|
28 |
+
}
|
29 |
+
|
30 |
+
def predict_sentiment(self, text):
|
31 |
+
"""Tek bir metin için sentiment tahmini yap"""
|
32 |
+
if not isinstance(text, str) or len(text.strip()) == 0:
|
33 |
+
return {"label": "Nötr", "score": 0.5}
|
34 |
+
|
35 |
+
try:
|
36 |
+
inputs = self.tokenizer(text, return_tensors="pt", truncation=True,
|
37 |
+
max_length=512, padding=True).to(self.device)
|
38 |
+
|
39 |
+
with torch.no_grad():
|
40 |
+
outputs = self.model(**inputs)
|
41 |
+
probs = torch.nn.functional.softmax(outputs.logits, dim=1)
|
42 |
+
prediction = probs.cpu().numpy()[0]
|
43 |
+
|
44 |
+
# İki sınıflı model için (positive/negative)
|
45 |
+
score = float(prediction[1]) # Pozitif sınıfın olasılığı
|
46 |
+
|
47 |
+
# Daha hassas skor eşikleri
|
48 |
+
if score > 0.75: # Yüksek güvenle pozitif
|
49 |
+
label = "Pozitif"
|
50 |
+
elif score < 0.25: # Yüksek güvenle negatif
|
51 |
+
label = "Negatif"
|
52 |
+
elif score > 0.55: # Hafif pozitif eğilim
|
53 |
+
label = "Pozitif"
|
54 |
+
elif score < 0.45: # Hafif negatif eğilim
|
55 |
+
label = "Negatif"
|
56 |
+
else:
|
57 |
+
label = "Nötr"
|
58 |
+
|
59 |
+
return {"label": label, "score": score}
|
60 |
+
|
61 |
+
except Exception as e:
|
62 |
+
print(f"Error in sentiment prediction: {e}")
|
63 |
+
return {"label": "Nötr", "score": 0.5}
|
64 |
+
|
65 |
+
def filter_product_reviews(self, df):
|
66 |
+
"""Ürün ile ilgili olmayan yorumları filtrele"""
|
67 |
+
def is_product_review(text):
|
68 |
+
if not isinstance(text, str):
|
69 |
+
return False
|
70 |
+
return not any(word in text.lower() for word in self.logistics_seller_words)
|
71 |
+
|
72 |
+
filtered_df = df[df['Yorum'].apply(is_product_review)].copy()
|
73 |
+
|
74 |
+
print(f"\nFiltreleme İstatistikleri:")
|
75 |
+
print(f"Toplam yorum sayısı: {len(df)}")
|
76 |
+
print(f"Ürün yorumu sayısı: {len(filtered_df)}")
|
77 |
+
print(f"Filtrelenen yorum sayısı: {len(df) - len(filtered_df)}")
|
78 |
+
print(f"Filtreleme oranı: {((len(df) - len(filtered_df)) / len(df) * 100):.2f}%")
|
79 |
+
|
80 |
+
return filtered_df
|
81 |
+
|
82 |
+
def analyze_reviews(self, df):
|
83 |
+
"""Tüm yorumları analiz et"""
|
84 |
+
print("\nSentiment analizi başlatılıyor...")
|
85 |
+
|
86 |
+
filtered_df = self.filter_product_reviews(df)
|
87 |
+
|
88 |
+
# Sentiment analizi
|
89 |
+
results = []
|
90 |
+
for text in filtered_df['Yorum']:
|
91 |
+
sentiment = self.predict_sentiment(text)
|
92 |
+
results.append(sentiment)
|
93 |
+
|
94 |
+
filtered_df['sentiment_score'] = [r['score'] for r in results]
|
95 |
+
filtered_df['sentiment_label'] = [r['label'] for r in results]
|
96 |
+
|
97 |
+
return filtered_df
|
98 |
+
|
99 |
+
def create_visualizations(self, df):
|
100 |
+
"""Analiz sonuçlarını görselleştir"""
|
101 |
+
if not os.path.exists('images'):
|
102 |
+
os.makedirs('images')
|
103 |
+
|
104 |
+
# 1. Sentiment Dağılımı
|
105 |
+
plt.figure(figsize=(12, 6))
|
106 |
+
sns.countplot(data=df, x='sentiment_label',
|
107 |
+
order=['Pozitif', 'Nötr', 'Negatif'])
|
108 |
+
plt.title('Sentiment Dağılımı')
|
109 |
+
plt.tight_layout()
|
110 |
+
plt.savefig('images/sentiment_distribution.png', bbox_inches='tight', dpi=300)
|
111 |
+
plt.close()
|
112 |
+
|
113 |
+
# 2. Yıldız-Sentiment İlişkisi
|
114 |
+
plt.figure(figsize=(12, 6))
|
115 |
+
df_mean = df.groupby('Yıldız Sayısı')['sentiment_score'].mean().reset_index()
|
116 |
+
sns.barplot(data=df_mean, x='Yıldız Sayısı', y='sentiment_score')
|
117 |
+
plt.title('Yıldız Sayısına Göre Ortalama Sentiment Skoru')
|
118 |
+
plt.tight_layout()
|
119 |
+
plt.savefig('images/star_sentiment_relation.png', bbox_inches='tight', dpi=300)
|
120 |
+
plt.close()
|
121 |
+
|
122 |
+
# 3. Sentiment Score Dağılımı
|
123 |
+
plt.figure(figsize=(12, 6))
|
124 |
+
sns.histplot(data=df, x='sentiment_score', bins=30)
|
125 |
+
plt.title('Sentiment Score Dağılımı')
|
126 |
+
plt.tight_layout()
|
127 |
+
plt.savefig('images/sentiment_score_distribution.png', bbox_inches='tight', dpi=300)
|
128 |
+
plt.close()
|
129 |
+
|
130 |
+
def print_statistics(self, df):
|
131 |
+
"""Analiz istatistiklerini yazdır"""
|
132 |
+
print("\nSentiment Analizi Sonuçları:")
|
133 |
+
print("-" * 50)
|
134 |
+
|
135 |
+
sentiment_counts = df['sentiment_label'].value_counts()
|
136 |
+
total_reviews = len(df)
|
137 |
+
|
138 |
+
for label, count in sentiment_counts.items():
|
139 |
+
percentage = (count / total_reviews) * 100
|
140 |
+
print(f"{label}: {count} yorum ({percentage:.2f}%)")
|
141 |
+
|
142 |
+
print("\nYıldız Bazlı Sentiment Skorları:")
|
143 |
+
print("-" * 50)
|
144 |
+
star_means = df.groupby('Yıldız Sayısı')['sentiment_score'].mean()
|
145 |
+
for star, score in star_means.items():
|
146 |
+
print(f"{star} Yıldız ortalama sentiment skoru: {score:.3f}")
|
147 |
+
|
148 |
+
def main():
|
149 |
+
df = pd.read_csv('data/macbook_product_comments_with_ratings.csv')
|
150 |
+
|
151 |
+
analyzer = TurkishSentimentAnalyzer()
|
152 |
+
|
153 |
+
print("Analiz başlatılıyor...")
|
154 |
+
analyzed_df = analyzer.analyze_reviews(df)
|
155 |
+
|
156 |
+
print("\nGörselleştirmeler oluşturuluyor...")
|
157 |
+
analyzer.create_visualizations(analyzed_df)
|
158 |
+
|
159 |
+
analyzer.print_statistics(analyzed_df)
|
160 |
+
|
161 |
+
output_file = 'sentiment_analyzed_reviews.csv'
|
162 |
+
analyzed_df.to_csv(output_file, index=False, encoding='utf-8-sig')
|
163 |
+
print(f"\nSonuçlar '{output_file}' dosyasına kaydedildi.")
|
164 |
+
|
165 |
+
if __name__ == "__main__":
|
166 |
+
main()
|