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