PerSent 0.2.4__tar.gz → 0.299.0__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.
@@ -0,0 +1,365 @@
1
+ Metadata-Version: 2.4
2
+ Name: PerSent
3
+ Version: 0.299.0
4
+ Summary: Persian Sentiment Analysis Toolkit
5
+ Home-page: https://github.com/RezaGooner/PerSent
6
+ Author: RezaGooner
7
+ Author-email: RezaAsadiProgrammer@Gmail.com
8
+ Keywords: persian sentiment analysis nlp
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: hazm>=0.7.0
15
+ Requires-Dist: gensim>=4.0.0
16
+ Requires-Dist: scikit-learn>=1.0.0
17
+ Requires-Dist: pandas>=1.3.0
18
+ Requires-Dist: tqdm>=4.62.0
19
+ Requires-Dist: joblib>=1.1.0
20
+ Requires-Dist: requests>=2.26.0
21
+ Dynamic: author
22
+ Dynamic: author-email
23
+ Dynamic: classifier
24
+ Dynamic: description
25
+ Dynamic: description-content-type
26
+ Dynamic: home-page
27
+ Dynamic: keywords
28
+ Dynamic: requires-dist
29
+ Dynamic: requires-python
30
+ Dynamic: summary
31
+
32
+ # PerSent (Persian Sentiment Analyzer)
33
+
34
+ [![Persian](https://img.shields.io/badge/Persian-فارسی-blue.svg)](README.fa.md)
35
+
36
+ ![PerSent Logo](https://github.com/user-attachments/assets/6bb1633b-6ed3-47fa-aae2-f97886dc4e22)
37
+
38
+ ## Introduction
39
+ PerSent is a practical Python library designed for Persian sentiment analysis. The name stands for "Persian Sentiment Analyzer". Currently in its early testing phase, the library provides basic functionality and is available on [PyPI](https://pypi.org/project/PerSent/). Install it using:
40
+
41
+ ```bash
42
+ pip install PerSent
43
+ ```
44
+
45
+ Current capabilities include:
46
+
47
+ - Sentiment analysis of opinions/comments
48
+
49
+ - Emotion analysis of texts (happiness, sadness, anger, surprise, fear, disgust, calmness)
50
+
51
+ - Analysis of product/service reviews (recommended/not recommended/no idea)
52
+
53
+ - Both single-text and batch CSV processing
54
+
55
+ - Output displayed in terminal or saved to CSV with summary statistics
56
+
57
+ Initial repository that evolved into this library:
58
+ [Click here](https://github.com/RezaGooner/Sentiment-Survey-Analyzer)
59
+
60
+ We welcome user testing and feedback to improve the library. If you encounter bugs or have suggestions, please:
61
+
62
+ - [Contribute](#contribution)
63
+
64
+ For installation issues due to dependency conflicts (especially with mingw-w64), consider using online platforms like DeepNote.com.
65
+
66
+ ## Structure
67
+ ### Comment Analysis Functions
68
+
69
+ ```train(train_csv, test_size=0.2, vector_size=100, window=5)```
70
+
71
+ | Parameter | Data Type | Default Value | Description | Optional/Required |
72
+ |--------------|-----------|---------------|-----------------------------------------------------------------------------|-------------------|
73
+ | `train_csv` | str | - | Path to CSV file containing training data with `body` and `recommendation_status` columns | Required |
74
+ | `test_size` | float | 0.2 | Proportion of test data (between 0.0 and 1.0) | Optional |
75
+ | `vector_size`| int | 100 | Output vector dimension for Word2Vec model (embedding size) | Optional |
76
+ | `window` | int | 5 | Context window size for Word2Vec model | Optional |
77
+
78
+ recommendation_status must be one of:
79
+
80
+ - no_idea
81
+
82
+ - recommended
83
+
84
+ - not_recommended
85
+
86
+ Null/NaN values are converted to no_idea, affecting model accuracy.
87
+
88
+ - Returns test accuracy score.
89
+
90
+ ---
91
+
92
+ ```analyzeText(text)```
93
+
94
+ | Parameter | Data Type | Description | Optional/Required |
95
+ |-----------|-----------|--------------------------------------|-------------------|
96
+ | `text` | str | The Persian text to be analyzed | Required |
97
+
98
+ The core function that analyzes a text and returns one of:
99
+ "not_recommended", "recommended", or "no_idea".
100
+
101
+ ---
102
+
103
+ ```saveModel()```
104
+
105
+ ```loadModel()```
106
+
107
+ Model persistence functions. Models are saved in the model directory.
108
+
109
+ ---
110
+
111
+ ```analyzeCSV(input_csv, output_path, summary_path=None, text_column=0)```
112
+
113
+ | Parameter | Data Type | Default Value | Description | Optional/Required |
114
+ |----------------|-----------------|---------------|-----------------------------------------------------------------------------|-------------------|
115
+ | `input_csv` | str | - | Path to input CSV file containing comments to analyze | Required |
116
+ | `output_path` | str | - | Path where analyzed results CSV will be saved | Required |
117
+ | `summary_path` | str or None | None | Optional path to save summary statistics CSV | Optional |
118
+ | `text_column` | int or str | 0 | Column index (int) or name (str) containing the text to analyze | Optional |
119
+
120
+
121
+ Batch processes comments from a CSV file. For single-column files, text_column isn't needed. Otherwise specify column name/index (0-based, negative indices supported). Output contains:
122
+
123
+ 1- Original text
124
+
125
+ 2- Recommendation status
126
+ Optional summary_path generates statistics:
127
+
128
+ - Total count
129
+
130
+ - Recommended count
131
+
132
+ - Not recommended count
133
+
134
+ - No idea count
135
+
136
+ - Model accuracy (not implemented in current version)
137
+
138
+ Returns a DataFrame and saves results.
139
+
140
+ ---
141
+
142
+ ### Emotion Analysis Functions
143
+
144
+ ```loadLex(csv_file, word_col=0, emotion_col=1, weight_col=2)```
145
+
146
+ | Parameter | Data Type | Default Value | Description | Optional/Required |
147
+ |----------------|-----------------|---------------|-----------------------------------------------------------------------------|-------------------|
148
+ | `csv_file` | str | - | Path to CSV lexicon file | Required |
149
+ | `word_col` | int or str | 0 | Column index (int) or name (str) containing words | Optional |
150
+ | `emotion_col` | int or str | 1 | Column index (int) or name (str) containing emotion labels | Optional |
151
+ | `weight_col` | int or str | 2 | Column index (int) or name (str) containing weight values | Optional |
152
+
153
+ Loads a CSV with three columns:
154
+
155
+ 1- Keywords
156
+
157
+ 2- Emotion (happiness, sadness, anger, fear, disgust, calmness)
158
+
159
+ 3- Emotion weight (defaults to 1 if unspecified, affecting accuracy)
160
+
161
+ Column indices are optional.
162
+
163
+ ---
164
+
165
+ ```train(train_csv, text_col='text', emotion_col='sentiment', weight_col='weight')```
166
+
167
+ | Parameter | Data Type | Default Value | Description | Optional/Required |
168
+ |----------------|-----------------|---------------|-----------------------------------------------------------------------------|-------------------|
169
+ | `train_csv` | str | - | Path to training CSV file | Required |
170
+ | `text_col` | str or int | 'text' | Column name/index containing text data | Optional |
171
+ | `emotion_col` | str or int | 'emotion' | Column name/index containing emotion labels | Optional |
172
+ | `weight_col` | str or int | 'weight' | Column name/index containing weight values | Optional |
173
+
174
+ Trains the emotion model using a CSV with specified column names (optional).
175
+
176
+ ---
177
+
178
+ ```saveModel(model_name='weighted_sentiment_model')```
179
+
180
+ | Parameter | Type | Default Value | Description | Optional/Required |
181
+ |--------------|-------|-----------------------------|-----------------------------------------------------------------------------|-------------------|
182
+ | `model_name` | str | 'weighted_sentiment_model' | Base filename for saving model (without extension) | Optional |
183
+
184
+
185
+ ```loadModel(model_name='weighted_sentiment_model')```
186
+
187
+ | Parameter | Type | Default Value | Description | Optional/Required |
188
+ |--------------|-------|-----------------------------|-----------------------------------------------------------------------------|-------------------|
189
+ | `model_name` | str | 'weighted_sentiment_model' | Base filename of model to load (without extension) | Optional |
190
+
191
+
192
+ Model persistence functions (saved in model directory).
193
+
194
+ ---
195
+
196
+ ```analyzeText(text)```
197
+
198
+ | Parameter | Type | Description | Optional/Required |
199
+ |-----------|------|--------------------------------------|-------------------|
200
+ | `text` | str | Persian text to analyze | Required |
201
+
202
+ Analyzes a single text, returning percentage scores for each emotion.
203
+
204
+ ---
205
+
206
+ ```analyzeCSV(input_csv, output_csv, text_col='text', output_col='sentiment_analysis')```
207
+
208
+ | Parameter | Type | Default Value | Description | Optional/Required |
209
+ |---------------------|---------------|------------------------|-----------------------------------------------------------------------------|-------------------|
210
+ | `input_csv` | str | - | Path to input CSV file containing text to analyze | Required |
211
+ | `output_csv` | str | - | Path to save analyzed results | Required |
212
+ | `text_col` | str/int | 'text' | Column name/index containing text to analyze | Optional |
213
+ | `output_col` | str | 'sentiment_analysis' | Column name for output results | Optional |
214
+
215
+ Batch processes texts from CSV. Returns True on success. Requires:
216
+
217
+ - input_csv path
218
+
219
+ - output_csv path
220
+ Optional column names.
221
+
222
+ ---
223
+
224
+ ## Installation
225
+ Install via pip:
226
+
227
+ ```bash
228
+ pip install PerSent
229
+ ```
230
+
231
+ For specific versions:
232
+
233
+ ```bash
234
+ pip install PerSent==<VERSION_NUMBER>
235
+ ```
236
+
237
+ ## Usage
238
+ - Comment Analysis
239
+
240
+ Basic single-text analysis:
241
+
242
+ ```bash
243
+ from PerSent import CommentAnalyzer
244
+
245
+ analyzer = CommentAnalyzer()
246
+
247
+ '''
248
+ Training (if you have data):
249
+ Requires CSV with comments and recommendation status columns
250
+ Status must be: recommended/not_recommended/no_idea
251
+ '''
252
+ analyzer.train("train.csv")
253
+
254
+ # Load pre-trained model
255
+ analyzer.loadModel()
256
+
257
+ # Predict
258
+ text = "کیفیت عالی داشت" # "Excellent quality"
259
+ result = analyzer.analyzeText(text)
260
+ print(f"Sentiment: {result}") # Output: Sentiment: recommended
261
+ ```
262
+
263
+ The included pre-trained model has ~70% accuracy. For better results, you can train with larger datasets. I've prepared a split dataset (due to size):
264
+
265
+ [Download Here](https://github.com/RezaGooner/Sentiment-Survey-Analyzer/tree/main/Dataset/big_train)
266
+
267
+ ---
268
+
269
+ Batch CSV processing:
270
+
271
+ ```bash
272
+ from PerSent import CommentAnalyzer
273
+ analyzer = CommentAnalyzer()
274
+ analyzer.loadModel()
275
+
276
+ # Basic usage (single-column CSV)
277
+ analyzer.analyzeCSV(
278
+ input_csv="comments.csv",
279
+ output_path="results.csv"
280
+ )
281
+
282
+ # Alternative usage patterns:
283
+ # 1. Using column index (0-based)
284
+ analyzer.analyzeCSV("comments.csv", "results.csv", None, 0)
285
+
286
+ # 2. Negative indices (count from end)
287
+ analyzer.analyzeCSV("comments.csv", "results.csv", None, -1)
288
+
289
+ # 3. Column name
290
+ analyzer.analyzeCSV("comments.csv", "results.csv", None, "نظرات") # "Comments" column
291
+
292
+ # 4. With summary (single-column)
293
+ analyzer.analyzeCSV("comments.csv", "results.csv", "summary.csv")
294
+
295
+ # 5. With summary and column specification
296
+ analyzer.analyzeCSV("comments.csv", "results.csv", "summary.csv", 2)
297
+ ```
298
+
299
+ - Emotion Analysis
300
+
301
+ Single text analysis with pre-trained model:
302
+
303
+ ```bash
304
+ from PerSent import SentimentAnalyzer
305
+
306
+ analyzer = SentimentAnalyzer()
307
+ analyzer.loadModel()
308
+
309
+ sample_text = "امتحانم رو خراب کردم. احساس می‌کنم یک شکست خورده‌ی تمام عیارم."
310
+ # "I failed my exam. I feel like a complete failure."
311
+
312
+ result = analyzer.analyzeText(sample_text)
313
+ for emotion, score in sorted(result.items(), key=lambda x: x[1], reverse=True):
314
+ print(f"{emotion}: {score:.2f}%")
315
+ ```
316
+
317
+ output :
318
+
319
+ ```bash
320
+ غم: 36.00% #Sadness
321
+ عصبانیت: 36.00% #anger
322
+ ترس: 28.00% #fear
323
+ شادی: 0.00% #happiness
324
+ تنفر: 0.00% #disgust
325
+ شگفتی: 0.00% #surprise
326
+ آرامش: 0.00% #calmness
327
+ ```
328
+
329
+ To train your own model:
330
+
331
+ ``` bash
332
+ analyzer.train('emotion_dataset.csv')
333
+ ```
334
+
335
+ Required CSV columns:
336
+
337
+ 1- Keywords
338
+
339
+ 2- Emotion (happiness, sadness, anger, disgust, fear, calmness)
340
+
341
+ 3- Emotion weight
342
+
343
+ Model persistence:
344
+
345
+ ```bash
346
+ analyzer.saveModel("custom_model_name")
347
+ analyzer.loadModel("custom_model_name")
348
+ ```
349
+
350
+ Batch CSV processing:
351
+
352
+ ```bash
353
+ analyzer.analyzeCSV("input.csv", "output.csv")
354
+ ```
355
+
356
+ ## Contribution
357
+ As mentioned, this library needs community collaboration. Please share suggestions, bugs, or feedback via:
358
+
359
+ - [Fork Repository & Pull Request](https://github.com/RezaGooner/PerSent/fork)
360
+
361
+ - [Create Issue](https://github.com/RezaGooner/PerSent/issues/new)
362
+
363
+ - Email: ```RezaAsadiProgrammer@gmail.com```
364
+
365
+ - Telegram: ```@RezaGooner```
@@ -0,0 +1,263 @@
1
+ #import necessary library
2
+ import pandas as pd
3
+ from hazm import Normalizer, word_tokenize, Stemmer, stopwords_list
4
+ import re
5
+ from tqdm import tqdm
6
+ from gensim.models import Word2Vec
7
+ import numpy as np
8
+ from sklearn.linear_model import LogisticRegression
9
+ from sklearn.model_selection import train_test_split
10
+ import os
11
+ import joblib
12
+
13
+ class CommentAnalyzer:
14
+ def __init__(self, model_dir='PerSent/model'):
15
+ self.normalizer = Normalizer()
16
+ self.stemmer = Stemmer()
17
+ self.stopwords = set(stopwords_list())
18
+ self.model_dir = model_dir
19
+ self.vectorizer = None
20
+ self.classifier = None
21
+
22
+ # make /model Directory if not exist
23
+ os.makedirs(self.model_dir, exist_ok=True)
24
+
25
+ def _preprocess_text(self, text):
26
+ """PreProcess Persian Text"""
27
+ try:
28
+ # Normalizing
29
+ text = self.normalizer.normalize(str(text))
30
+
31
+ # remove number and sign
32
+ text = re.sub(r'[!()-\[\]{};:\'",؟<>./?@#$%^&*_~۰-۹\d]+', ' ', text)
33
+ text = re.sub(r'\s+', ' ', text).strip()
34
+
35
+ # tokenize and stemming
36
+ tokens = word_tokenize(text)
37
+ processed_tokens = [
38
+ self.stemmer.stem(token)
39
+ for token in tokens
40
+ if token not in self.stopwords and len(token) > 1
41
+ ]
42
+
43
+ return processed_tokens
44
+ except Exception as e:
45
+ raise Exception(f"Text preprocessing failed: {str(e)}")
46
+
47
+ def _sentence_vector(self, sentence, model):
48
+ """convert sentences to vector by word2vec model"""
49
+ try:
50
+ vectors = []
51
+ for word in sentence:
52
+ try:
53
+ vectors.append(model.wv[word])
54
+ except KeyError:
55
+ vectors.append(np.zeros(100))
56
+ return np.mean(vectors, axis=0) if vectors else np.zeros(100)
57
+ except Exception as e:
58
+ raise Exception(f"Sentence vectorization failed: {str(e)}")
59
+
60
+ def train(self, train_csv, test_size=0.2, vector_size=100, window=5):
61
+ """Train model"""
62
+ try:
63
+ # read data
64
+ if not os.path.exists(train_csv):
65
+ raise FileNotFoundError(f"Training file {train_csv} not found")
66
+
67
+ df = pd.read_csv(train_csv)
68
+
69
+ if 'body' not in df.columns or 'recommendation_status' not in df.columns:
70
+ raise ValueError("CSV file must contain 'body' and 'recommendation_status' columns")
71
+
72
+ df['tokens'] = df['body'].apply(self._preprocess_text)
73
+
74
+ # train Word2Vec model
75
+ self.vectorizer = Word2Vec(
76
+ sentences=df['tokens'],
77
+ vector_size=vector_size,
78
+ window=window,
79
+ min_count=1,
80
+ workers=4
81
+ )
82
+
83
+ # convert sentences to vector
84
+ X = np.array([self._sentence_vector(s, self.vectorizer) for s in df['tokens']])
85
+ y = df['recommendation_status'].map({
86
+ "no_idea": 2,
87
+ "recommended": 1,
88
+ "not_recommended": 0
89
+ }).values
90
+
91
+ # make train and test data
92
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size)
93
+
94
+ self.classifier = LogisticRegression(max_iter=1000)
95
+ self.classifier.fit(X_train, y_train)
96
+
97
+ # save model
98
+ self.saveModel()
99
+
100
+ # evaluation
101
+ accuracy = self.classifier.score(X_test, y_test)
102
+ return accuracy
103
+
104
+ except FileNotFoundError as e:
105
+ raise FileNotFoundError(f"Training file error: {str(e)}")
106
+ except ValueError as e:
107
+ raise ValueError(f"Data validation error: {str(e)}")
108
+ except Exception as e:
109
+ raise Exception(f"Training failed: {str(e)}")
110
+
111
+ def analyzeText(self, text):
112
+ """Predict text sentiment"""
113
+ try:
114
+ if not self.classifier or not self.vectorizer:
115
+ raise Exception("Model not trained! Call train() first or load a pretrained model.")
116
+
117
+ tokens = self._preprocess_text(text)
118
+ vector = self._sentence_vector(tokens, self.vectorizer)
119
+ prediction = self.classifier.predict([vector])[0]
120
+
121
+ return {
122
+ 0: "not_recommended",
123
+ 1: "recommended",
124
+ 2: "no_idea"
125
+ }[prediction]
126
+
127
+ except Exception as e:
128
+ raise Exception(f"Text analysis failed: {str(e)}")
129
+
130
+ def saveModel(self):
131
+ """save trained model"""
132
+ try:
133
+ if not self.classifier or not self.vectorizer:
134
+ raise Exception("No trained model to save")
135
+
136
+ joblib.dump(self.classifier, os.path.join(self.model_dir, 'classifier.joblib'))
137
+ self.vectorizer.save(os.path.join(self.model_dir, 'word2vec.model'))
138
+ except IOError as e:
139
+ raise IOError(f"Failed to save model: {str(e)}")
140
+ except Exception as e:
141
+ raise Exception(f"Error while saving model: {str(e)}")
142
+
143
+ def loadModel(self):
144
+ """reload from file"""
145
+ try:
146
+ classifier_path = os.path.join(self.model_dir, 'classifier.joblib')
147
+ vectorizer_path = os.path.join(self.model_dir, 'word2vec.model')
148
+
149
+ if not os.path.exists(classifier_path) or not os.path.exists(vectorizer_path):
150
+ raise FileNotFoundError("Model files not found in the specified directory")
151
+
152
+ self.classifier = joblib.load(classifier_path)
153
+ self.vectorizer = Word2Vec.load(vectorizer_path)
154
+ except FileNotFoundError as e:
155
+ raise FileNotFoundError(f"Model files not found: {str(e)}")
156
+ except Exception as e:
157
+ raise Exception(f"Failed to load model: {str(e)}")
158
+
159
+ def analyzeCSV(self, input_csv, output_path, summary_path=None, text_column=0):
160
+ """
161
+ Analyze sentiment for comments in a CSV file and save results
162
+
163
+ Parameters:
164
+ input_csv (str): Path to input CSV file
165
+ output_path (str): Path to save output CSV file
166
+ text_column (str/int, optional): Name or index (0-based) of column containing comments.
167
+ Defaults to 0 (first column).
168
+ summary_path (str, optional): Path to save prediction summary report.
169
+ If None, no summary will be saved.
170
+
171
+ Returns:
172
+ pd.DataFrame: DataFrame containing analysis results or None if failed
173
+
174
+ Raises:
175
+ FileNotFoundError: If input file not found
176
+ ValueError: If invalid column specified
177
+ Exception: For other processing errors
178
+ """
179
+ try:
180
+ # Read input CSV
181
+ if not os.path.exists(input_csv):
182
+ raise FileNotFoundError(f"Input file {input_csv} not found")
183
+
184
+ df = pd.read_csv(input_csv)
185
+
186
+ # Determine the correct column
187
+ if isinstance(text_column, int):
188
+ # Handle negative indices
189
+ if text_column < 0:
190
+ text_column = len(df.columns) + text_column
191
+
192
+ if text_column >= len(df.columns) or text_column < 0:
193
+ raise ValueError(f"Column index {text_column} is out of range")
194
+
195
+ column_name = df.columns[text_column]
196
+ else:
197
+ if text_column not in df.columns:
198
+ raise ValueError(f"Column '{text_column}' not found in CSV file")
199
+ column_name = text_column
200
+
201
+ # Analyze each comment
202
+ tqdm.pandas(desc="Analyzing comments")
203
+ df['sentiment'] = df[column_name].progress_apply(self.analyzeText)
204
+
205
+ # Save results
206
+ df.to_csv(output_path, index=False, encoding='utf-8-sig')
207
+ print(f"Results saved to {output_path}")
208
+
209
+ # Generate and save summary if requested
210
+ if summary_path:
211
+ summary = self._generate_summary(df)
212
+ summary.to_csv(summary_path, index=False, encoding='utf-8-sig')
213
+ print(f"Summary report saved to {summary_path}")
214
+
215
+ return df
216
+
217
+ except FileNotFoundError as e:
218
+ print(f"Error: File not found - {str(e)}")
219
+ return None
220
+ except ValueError as e:
221
+ print(f"Error: Invalid column specification - {str(e)}")
222
+ return None
223
+ except Exception as e:
224
+ print(f"Error during CSV analysis: {str(e)}")
225
+ return None
226
+
227
+ def _generate_summary(self, df):
228
+ """Generate prediction summary statistics"""
229
+ try:
230
+ # Count each sentiment
231
+ counts = df['sentiment'].value_counts().to_dict()
232
+
233
+ # Create summary dataframe
234
+ summary = pd.DataFrame({
235
+ 'Category': [
236
+ 'Recommended',
237
+ 'Not Recommended',
238
+ 'No Idea',
239
+ 'Total',
240
+ 'Model Accuracy'
241
+ ],
242
+ 'Count': [
243
+ counts.get('recommended', 0),
244
+ counts.get('not_recommended', 0),
245
+ counts.get('no_idea', 0),
246
+ len(df),
247
+ 'N/A' # Accuracy needs to be calculated during training
248
+ ],
249
+ 'Percentage': [
250
+ f"{100 * counts.get('recommended', 0) / len(df):.2f}%",
251
+ f"{100 * counts.get('not_recommended', 0) / len(df):.2f}%",
252
+ f"{100 * counts.get('no_idea', 0) / len(df):.2f}%",
253
+ '100%',
254
+ 'N/A'
255
+ ]
256
+ })
257
+
258
+ return summary
259
+ except Exception as e:
260
+ raise Exception(f"Failed to generate summary: {str(e)}")
261
+
262
+
263
+ # Github : RezaGooner