PerSent 1.0.2__tar.gz → 1.0.20__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of PerSent might be problematic. Click here for more details.
- {persent-1.0.2 → persent-1.0.20}/PKG-INFO +1 -1
- {persent-1.0.2 → persent-1.0.20}/PerSent/CommentAnalyzer.py +1 -1
- persent-1.0.20/PerSent/WeightedSentimentAnalyzer.py +317 -0
- persent-1.0.20/PerSent/__init__.py +7 -0
- {persent-1.0.2 → persent-1.0.20}/PerSent.egg-info/PKG-INFO +1 -1
- {persent-1.0.2 → persent-1.0.20}/PerSent.egg-info/SOURCES.txt +2 -3
- {persent-1.0.2 → persent-1.0.20}/setup.py +2 -2
- persent-1.0.2/PerSent/__init__.py +0 -4
- persent-1.0.2/PerSent/model/classifier.joblib +0 -0
- persent-1.0.2/PerSent/model/word2vec.model +0 -0
- {persent-1.0.2 → persent-1.0.20}/PerSent.egg-info/dependency_links.txt +0 -0
- {persent-1.0.2 → persent-1.0.20}/PerSent.egg-info/requires.txt +0 -0
- {persent-1.0.2 → persent-1.0.20}/PerSent.egg-info/top_level.txt +0 -0
- {persent-1.0.2 → persent-1.0.20}/README.md +0 -0
- {persent-1.0.2 → persent-1.0.20}/setup.cfg +0 -0
|
@@ -11,7 +11,7 @@ import os
|
|
|
11
11
|
import joblib
|
|
12
12
|
|
|
13
13
|
class CommentAnalyzer:
|
|
14
|
-
def __init__(self, model_dir='PerSent/model'):
|
|
14
|
+
def __init__(self, model_dir='PerSent/model/CommentAnalyzer'):
|
|
15
15
|
self.normalizer = Normalizer()
|
|
16
16
|
self.stemmer = Stemmer()
|
|
17
17
|
self.stopwords = set(stopwords_list())
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Persian Weighted Sentiment Analyzer Library
|
|
4
|
+
A comprehensive sentiment analysis tool for Persian text with word weighting support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import csv
|
|
9
|
+
import os
|
|
10
|
+
import joblib
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from hazm import Stemmer, Lemmatizer, Normalizer, word_tokenize, stopwords_list
|
|
13
|
+
from tqdm import tqdm
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
class WeightedSentimentAnalyzer:
|
|
17
|
+
"""A Persian sentiment analyzer that supports word weighting"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, model_dir='PerSent/model/WeightedSentimentAnalyzer'):
|
|
20
|
+
"""
|
|
21
|
+
Initialize the weighted sentiment analyzer
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
model_dir (str): Directory path for saving/loading models
|
|
25
|
+
"""
|
|
26
|
+
self.normalizer = Normalizer()
|
|
27
|
+
self.stemmer = Stemmer()
|
|
28
|
+
self.lemmatizer = Lemmatizer()
|
|
29
|
+
self.stopwords = set(stopwords_list())
|
|
30
|
+
self.model_dir = model_dir
|
|
31
|
+
|
|
32
|
+
os.makedirs(self.model_dir, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
# Emotion mapping dictionary
|
|
35
|
+
self.emotion_map = {
|
|
36
|
+
'شادی': 1, # Happiness
|
|
37
|
+
'غم': 2, # Sadness
|
|
38
|
+
'ترس': 3, # Fear
|
|
39
|
+
'عصبانیت': 4, # Anger
|
|
40
|
+
'تنفر': 5, # Disgust
|
|
41
|
+
'شگفتی': 6, # Surprise
|
|
42
|
+
'آرامش': 7 # Calm
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
self.keywords = defaultdict(list)
|
|
46
|
+
self.word_weights = defaultdict(dict) # Stores word weights for each emotion
|
|
47
|
+
self.model_loaded = False
|
|
48
|
+
|
|
49
|
+
def _normalize_text(self, text):
|
|
50
|
+
"""Normalize Persian text by removing half-spaces and punctuation"""
|
|
51
|
+
text = re.sub(r'[\u200c\s]+', ' ', text).strip()
|
|
52
|
+
text = re.sub(r'[^\w\s\u0600-\u06FF]', ' ', text)
|
|
53
|
+
text = re.sub(r'\d+', '', text)
|
|
54
|
+
return text.lower()
|
|
55
|
+
|
|
56
|
+
def _preprocess_text(self, text):
|
|
57
|
+
"""Preprocess text into stemmed tokens"""
|
|
58
|
+
text = self._normalize_text(text)
|
|
59
|
+
tokens = word_tokenize(text)
|
|
60
|
+
return [self.stemmer.stem(t) for t in tokens if t not in self.stopwords and len(t) > 1]
|
|
61
|
+
|
|
62
|
+
def _generate_word_forms(self, word):
|
|
63
|
+
"""Generate all possible forms of a word (stem, lemma, etc.)"""
|
|
64
|
+
stem = self.stemmer.stem(word)
|
|
65
|
+
lemma = self.lemmatizer.lemmatize(word)
|
|
66
|
+
|
|
67
|
+
forms = {
|
|
68
|
+
word,
|
|
69
|
+
stem,
|
|
70
|
+
lemma,
|
|
71
|
+
self.stemmer.stem(stem),
|
|
72
|
+
self.lemmatizer.lemmatize(stem),
|
|
73
|
+
self.stemmer.stem(lemma),
|
|
74
|
+
self.lemmatizer.lemmatize(lemma)
|
|
75
|
+
}
|
|
76
|
+
return [f for f in forms if f and len(f) > 1]
|
|
77
|
+
|
|
78
|
+
def load_weighted_lexicon(self, csv_file, word_col=0, emotion_col=1, weight_col=2):
|
|
79
|
+
"""
|
|
80
|
+
Load a weighted lexicon from CSV file
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
csv_file (str): Path to CSV file
|
|
84
|
+
word_col (int/str): Column index/name for words (default: 0)
|
|
85
|
+
emotion_col (int/str): Column index/name for emotions (default: 1)
|
|
86
|
+
weight_col (int/str): Column index/name for weights (default: 2)
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
FileNotFoundError: If file doesn't exist
|
|
90
|
+
ValueError: If file format is invalid
|
|
91
|
+
"""
|
|
92
|
+
if not os.path.exists(csv_file):
|
|
93
|
+
raise FileNotFoundError(f"Lexicon file {csv_file} not found")
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
with open(csv_file, mode='r', encoding='utf-8') as file:
|
|
97
|
+
reader = csv.reader(file)
|
|
98
|
+
for row in reader:
|
|
99
|
+
if len(row) < max(word_col, emotion_col, weight_col) + 1:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
# Handle both column names and indices
|
|
103
|
+
word = str(row[word_col]).strip() if isinstance(word_col, int) else row[word_col]
|
|
104
|
+
emotion = str(row[emotion_col]).strip() if isinstance(emotion_col, int) else row[emotion_col]
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
weight = float(row[weight_col]) if isinstance(weight_col, int) else float(row[weight_col])
|
|
108
|
+
except (ValueError, IndexError):
|
|
109
|
+
weight = 1.0 # Default weight if invalid
|
|
110
|
+
|
|
111
|
+
word = self._normalize_text(word)
|
|
112
|
+
forms = self._generate_word_forms(word)
|
|
113
|
+
|
|
114
|
+
if emotion in self.emotion_map:
|
|
115
|
+
numeric_emotion = self.emotion_map[emotion]
|
|
116
|
+
self.keywords[numeric_emotion].extend(forms)
|
|
117
|
+
|
|
118
|
+
# Store weights for each word form
|
|
119
|
+
for form in forms:
|
|
120
|
+
current_weight = self.word_weights.get(form, {}).get(numeric_emotion, 0)
|
|
121
|
+
if weight > current_weight:
|
|
122
|
+
self.word_weights.setdefault(form, {})[numeric_emotion] = weight
|
|
123
|
+
|
|
124
|
+
# Remove duplicates
|
|
125
|
+
for emotion in self.keywords:
|
|
126
|
+
self.keywords[emotion] = list(set(self.keywords[emotion]))
|
|
127
|
+
|
|
128
|
+
self.model_loaded = True
|
|
129
|
+
|
|
130
|
+
except Exception as e:
|
|
131
|
+
raise ValueError(f"Error processing lexicon file: {str(e)}")
|
|
132
|
+
|
|
133
|
+
def train_from_csv(self, train_csv, text_col='text', emotion_col='emotion', weight_col='weight'):
|
|
134
|
+
"""
|
|
135
|
+
Train model from a weighted CSV dataset
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
train_csv (str): Path to training CSV file
|
|
139
|
+
text_col (str/int): Column name/index for text (default: 'text')
|
|
140
|
+
emotion_col (str/int): Column name/index for emotions (default: 'emotion')
|
|
141
|
+
weight_col (str/int): Column name/index for weights (default: 'weight')
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
dict: Training result with status and message
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
FileNotFoundError: If training file doesn't exist
|
|
148
|
+
ValueError: If required columns are missing
|
|
149
|
+
"""
|
|
150
|
+
if not os.path.exists(train_csv):
|
|
151
|
+
raise FileNotFoundError(f"Training file {train_csv} not found")
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
df = pd.read_csv(train_csv)
|
|
155
|
+
|
|
156
|
+
# Handle column names/indices
|
|
157
|
+
text_col = df.columns[text_col] if isinstance(text_col, int) else text_col
|
|
158
|
+
emotion_col = df.columns[emotion_col] if isinstance(emotion_col, int) else emotion_col
|
|
159
|
+
weight_col = df.columns[weight_col] if isinstance(weight_col, int) else weight_col
|
|
160
|
+
|
|
161
|
+
# Check required columns
|
|
162
|
+
missing_cols = [col for col in [text_col, emotion_col, weight_col] if col not in df.columns]
|
|
163
|
+
if missing_cols:
|
|
164
|
+
raise ValueError(f"Missing columns: {', '.join(missing_cols)}")
|
|
165
|
+
|
|
166
|
+
# Train model with weights
|
|
167
|
+
for _, row in tqdm(df.iterrows(), total=len(df), desc="Training model"):
|
|
168
|
+
text = str(row[text_col])
|
|
169
|
+
emotion = str(row[emotion_col])
|
|
170
|
+
weight = float(row[weight_col])
|
|
171
|
+
|
|
172
|
+
words = self._preprocess_text(text)
|
|
173
|
+
forms = set()
|
|
174
|
+
for word in words:
|
|
175
|
+
forms.update(self._generate_word_forms(word))
|
|
176
|
+
|
|
177
|
+
if emotion in self.emotion_map:
|
|
178
|
+
numeric_emotion = self.emotion_map[emotion]
|
|
179
|
+
self.keywords[numeric_emotion].extend(forms)
|
|
180
|
+
|
|
181
|
+
# Update weights
|
|
182
|
+
for form in forms:
|
|
183
|
+
current_weight = self.word_weights.get(form, {}).get(numeric_emotion, 0)
|
|
184
|
+
if weight > current_weight:
|
|
185
|
+
self.word_weights.setdefault(form, {})[numeric_emotion] = weight
|
|
186
|
+
|
|
187
|
+
# Save trained model
|
|
188
|
+
self.save_model()
|
|
189
|
+
self.model_loaded = True
|
|
190
|
+
|
|
191
|
+
return {"status": "success", "message": "Model trained successfully"}
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
return {"status": "error", "message": str(e)}
|
|
195
|
+
|
|
196
|
+
def save_model(self, model_name='weighted_sentiment_model'):
|
|
197
|
+
"""
|
|
198
|
+
Save the current model to disk
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
model_name (str): Base name for model files (without extension)
|
|
202
|
+
"""
|
|
203
|
+
model_path = os.path.join(self.model_dir, f'{model_name}.joblib')
|
|
204
|
+
joblib.dump({
|
|
205
|
+
'emotion_map': self.emotion_map,
|
|
206
|
+
'keywords': dict(self.keywords),
|
|
207
|
+
'word_weights': dict(self.word_weights)
|
|
208
|
+
}, model_path)
|
|
209
|
+
|
|
210
|
+
def load_model(self, model_name='weighted_sentiment_model'):
|
|
211
|
+
"""
|
|
212
|
+
Load a saved model from disk
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
model_name (str): Base name for model files (without extension)
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
FileNotFoundError: If model file doesn't exist
|
|
219
|
+
ValueError: If model file is corrupted
|
|
220
|
+
"""
|
|
221
|
+
model_path = os.path.join(self.model_dir, f'{model_name}.joblib')
|
|
222
|
+
|
|
223
|
+
if not os.path.exists(model_path):
|
|
224
|
+
raise FileNotFoundError(f"Model file {model_path} not found")
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
data = joblib.load(model_path)
|
|
228
|
+
self.emotion_map = data['emotion_map']
|
|
229
|
+
self.keywords = defaultdict(list, data['keywords'])
|
|
230
|
+
self.word_weights = defaultdict(dict, data['word_weights'])
|
|
231
|
+
self.model_loaded = True
|
|
232
|
+
except Exception as e:
|
|
233
|
+
raise ValueError(f"Error loading model: {str(e)}")
|
|
234
|
+
|
|
235
|
+
def analyze_text(self, text):
|
|
236
|
+
"""
|
|
237
|
+
Analyze sentiment of input text with weighted scoring
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
text (str): Text to analyze
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
dict: Emotion percentages with weights
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
Exception: If model is not loaded
|
|
247
|
+
"""
|
|
248
|
+
if not self.model_loaded:
|
|
249
|
+
raise Exception("Model not loaded. Please load or train a model first.")
|
|
250
|
+
|
|
251
|
+
words = self._preprocess_text(text)
|
|
252
|
+
emotion_scores = defaultdict(float)
|
|
253
|
+
total_score = 0.0
|
|
254
|
+
|
|
255
|
+
for word in words:
|
|
256
|
+
forms = self._generate_word_forms(word)
|
|
257
|
+
|
|
258
|
+
for form in forms:
|
|
259
|
+
if form in self.word_weights:
|
|
260
|
+
for emotion_id, weight in self.word_weights[form].items():
|
|
261
|
+
emotion_scores[emotion_id] += weight
|
|
262
|
+
total_score += weight
|
|
263
|
+
|
|
264
|
+
# Calculate weighted percentages
|
|
265
|
+
results = {}
|
|
266
|
+
reverse_map = {v: k for k, v in self.emotion_map.items()}
|
|
267
|
+
|
|
268
|
+
if total_score > 0:
|
|
269
|
+
for emotion_id in self.emotion_map.values():
|
|
270
|
+
score = emotion_scores.get(emotion_id, 0)
|
|
271
|
+
results[reverse_map[emotion_id]] = round(score / total_score * 100, 2)
|
|
272
|
+
|
|
273
|
+
return results or {emotion: 0.0 for emotion in self.emotion_map}
|
|
274
|
+
|
|
275
|
+
def analyze_csv(self, input_csv, output_csv, text_col='text', output_col='sentiment_analysis'):
|
|
276
|
+
"""
|
|
277
|
+
Analyze sentiment for a CSV file and save results
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
input_csv (str): Path to input CSV file
|
|
281
|
+
output_csv (str): Path to save output CSV
|
|
282
|
+
text_col (str/int): Column name/index containing text (default: 'text')
|
|
283
|
+
output_col (str): Column name for output results (default: 'sentiment_analysis')
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
bool: True if successful, False otherwise
|
|
287
|
+
|
|
288
|
+
Raises:
|
|
289
|
+
Exception: If model is not loaded
|
|
290
|
+
"""
|
|
291
|
+
if not self.model_loaded:
|
|
292
|
+
raise Exception("Model not loaded. Please load or train a model first.")
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
df = pd.read_csv(input_csv)
|
|
296
|
+
|
|
297
|
+
# Handle column name/index
|
|
298
|
+
text_col = df.columns[text_col] if isinstance(text_col, int) else text_col
|
|
299
|
+
|
|
300
|
+
if text_col not in df.columns:
|
|
301
|
+
raise ValueError(f"Column '{text_col}' not found in CSV file")
|
|
302
|
+
|
|
303
|
+
tqdm.pandas(desc="Analyzing sentiments")
|
|
304
|
+
df[output_col] = df[text_col].progress_apply(
|
|
305
|
+
lambda x: self.analyze_text(str(x)))
|
|
306
|
+
|
|
307
|
+
# Save results
|
|
308
|
+
df.to_csv(output_csv, index=False, encoding='utf-8-sig')
|
|
309
|
+
return True
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
print(f"Error: {str(e)}")
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
#Github : RezaGooner
|
|
317
|
+
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
setup.py
|
|
3
3
|
PerSent/CommentAnalyzer.py
|
|
4
|
+
PerSent/WeightedSentimentAnalyzer.py
|
|
4
5
|
PerSent/__init__.py
|
|
5
6
|
PerSent.egg-info/PKG-INFO
|
|
6
7
|
PerSent.egg-info/SOURCES.txt
|
|
7
8
|
PerSent.egg-info/dependency_links.txt
|
|
8
9
|
PerSent.egg-info/requires.txt
|
|
9
|
-
PerSent.egg-info/top_level.txt
|
|
10
|
-
PerSent/model/classifier.joblib
|
|
11
|
-
PerSent/model/word2vec.model
|
|
10
|
+
PerSent.egg-info/top_level.txt
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name="PerSent",
|
|
8
|
-
version="1.0.
|
|
8
|
+
version="1.0.20",
|
|
9
9
|
author="RezaGooner",
|
|
10
10
|
author_email="RezaAsadiProgrammer@Gmail.com",
|
|
11
11
|
description="Persian Sentiment Analysis Toolkit",
|
|
@@ -14,7 +14,7 @@ setup(
|
|
|
14
14
|
url="https://github.com/RezaGooner/PerSent",
|
|
15
15
|
packages=find_packages(),
|
|
16
16
|
package_data={
|
|
17
|
-
'PerSent': ['CommentAnalyzer.py', 'model/*'],
|
|
17
|
+
'PerSent': ['CommentAnalyzer.py','WeightedSentimentAnalyzer', 'model/*'],
|
|
18
18
|
},
|
|
19
19
|
install_requires=[
|
|
20
20
|
'hazm>=0.7.0',
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|