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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PerSent
3
- Version: 1.0.2
3
+ Version: 1.0.20
4
4
  Summary: Persian Sentiment Analysis Toolkit
5
5
  Home-page: https://github.com/RezaGooner/PerSent
6
6
  Author: RezaGooner
@@ -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
+
@@ -0,0 +1,7 @@
1
+ from .CommentAnalyzer import CommentAnalyzer
2
+ from .WeightedSentimentAnalyzer import WeightedSentimentAnalyzer
3
+
4
+
5
+
6
+ __version__ = "1.0.20"
7
+ __all__ = ['CommentAnalyzer','WeightedSentimentAnalyzer']
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PerSent
3
- Version: 1.0.2
3
+ Version: 1.0.20
4
4
  Summary: Persian Sentiment Analysis Toolkit
5
5
  Home-page: https://github.com/RezaGooner/PerSent
6
6
  Author: RezaGooner
@@ -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.2",
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',
@@ -1,4 +0,0 @@
1
- from .CommentAnalyzer import CommentAnalyzer
2
-
3
- __version__ = "1.0.2"
4
- __all__ = ['CommentAnalyzer']
Binary file
File without changes
File without changes