PerSent 0.2.4__tar.gz → 0.10.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,320 @@
1
+ Metadata-Version: 2.4
2
+ Name: PerSent
3
+ Version: 0.10.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
+ Dynamic: author
21
+ Dynamic: author-email
22
+ Dynamic: classifier
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: keywords
27
+ Dynamic: requires-dist
28
+ Dynamic: requires-python
29
+ Dynamic: summary
30
+
31
+ # PerSent (Persian Sentiment Analyzer)
32
+
33
+ [![Persian](https://img.shields.io/badge/Persian-فارسی-blue.svg)](README.fa.md)
34
+
35
+ ![PerSent Logo](https://github.com/user-attachments/assets/6bb1633b-6ed3-47fa-aae2-f97886dc4e22)
36
+
37
+ ## Introduction
38
+ 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:
39
+
40
+ ```bash
41
+ pip install PerSent
42
+ ```
43
+
44
+ Current capabilities include:
45
+
46
+ - Sentiment analysis of opinions/comments
47
+
48
+ - Emotion analysis of texts (happiness, sadness, anger, surprise, fear, disgust, calmness)
49
+
50
+ - Analysis of product/service reviews (recommended/not recommended/no idea)
51
+
52
+ - Both single-text and batch CSV processing
53
+
54
+ - Output displayed in terminal or saved to CSV with summary statistics
55
+
56
+ Initial repository that evolved into this library:
57
+ [Click here](https://github.com/RezaGooner/Sentiment-Survey-Analyzer)
58
+
59
+ We welcome user testing and feedback to improve the library. If you encounter bugs or have suggestions, please:
60
+ [Contribute](#Contribute)
61
+
62
+ For installation issues due to dependency conflicts (especially with mingw-w64), consider using online platforms like DeepNote.com.
63
+
64
+ ## Structure
65
+ ### Comment Analysis Functions
66
+
67
+ $$train(train_csv, test_size=0.2, vector_size=100, window=5)$$
68
+
69
+ Trains the model using a CSV file with columns:
70
+
71
+ 1- body (text content)
72
+
73
+ 2- recommendation_status (must be one of:)
74
+
75
+ - no_idea
76
+
77
+ - recommended
78
+
79
+ - not_recommended
80
+
81
+ Null/NaN values are converted to no_idea, affecting model accuracy. Optional parameters:
82
+
83
+ - test_size: Test data proportion (default 0.2)
84
+
85
+ - vector_size: Word vector dimensions (default 100)
86
+
87
+ - window: Context window size (default 5)
88
+
89
+ Returns test accuracy score.
90
+
91
+ ---
92
+
93
+ $$predict(text)$$
94
+
95
+ The core function that analyzes a text and returns one of:
96
+ "not_recommended", "recommended", or "no_idea".
97
+
98
+ ---
99
+
100
+ $$save_model()$$
101
+
102
+ $$load_model()$$
103
+
104
+ Model persistence functions. Models are saved in the model directory.
105
+
106
+ ---
107
+
108
+ $$csvPredict(input_csv, output_path, summary_path=None, text_column=0)$$
109
+
110
+
111
+ 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:
112
+
113
+ 1- Original text
114
+
115
+ 2- Recommendation status
116
+ Optional summary_path generates statistics:
117
+
118
+ - Total count
119
+
120
+ - Recommended count
121
+
122
+ - Not recommended count
123
+
124
+ - No idea count
125
+
126
+ - Model accuracy (not implemented in current version)
127
+
128
+ Returns a DataFrame and saves results.
129
+
130
+ ---
131
+
132
+ ### Emotion Analysis Functions
133
+
134
+ $$load_weighted_lexicon(csv_file, word_col=0, emotion_col=1, weight_col=2)$$
135
+
136
+ Loads a CSV with three columns:
137
+
138
+ 1- Keywords
139
+
140
+ 2- Emotion (happiness, sadness, anger, fear, disgust, calmness)
141
+
142
+ 3- Emotion weight (defaults to 1 if unspecified, affecting accuracy)
143
+
144
+ Column indices are optional.
145
+
146
+ ---
147
+
148
+ $$train_from_csv(train_csv, text_col='text', emotion_col='sentiment', weight_col='weight')$$
149
+
150
+ Trains the emotion model using a CSV with specified column names (optional).
151
+
152
+ ---
153
+
154
+ $$save_model(model_name='weighted_sentiment_model')$$
155
+
156
+ $$load_model(model_name='weighted_sentiment_model')$$
157
+
158
+ Model persistence functions (saved in model directory).
159
+
160
+ ---
161
+
162
+ $$analyze_text(text)$$
163
+
164
+ Analyzes a single text, returning percentage scores for each emotion.
165
+
166
+ ---
167
+
168
+ $$analyze_csv(input_csv, output_csv, text_col='text', output_col='sentiment_analysis')$$
169
+
170
+ Batch processes texts from CSV. Returns True on success. Requires:
171
+
172
+ - input_csv path
173
+
174
+ - output_csv path
175
+ Optional column names.
176
+
177
+ ---
178
+
179
+ ## Installation
180
+ Install via pip:
181
+
182
+ ```bash
183
+ pip install PerSent
184
+ ```
185
+
186
+ For specific versions:
187
+
188
+ ```bash
189
+ pip install PerSent==<VERSION_NUMBER>
190
+ ```
191
+
192
+ ## Usage
193
+ - Comment Analysis
194
+
195
+ Basic single-text analysis:
196
+
197
+ ```bash
198
+ from PerSent import CommentAnalyzer
199
+
200
+ analyzer = CommentAnalyzer()
201
+
202
+ '''
203
+ Training (if you have data):
204
+ Requires CSV with comments and recommendation status columns
205
+ Status must be: recommended/not_recommended/no_idea
206
+ '''
207
+ analyzer.train("train.csv")
208
+
209
+ # Load pre-trained model
210
+ analyzer.load_model()
211
+
212
+ # Predict
213
+ text = "کیفیت عالی داشت" # "Excellent quality"
214
+ result = analyzer.predict(text)
215
+ print(f"Sentiment: {result}") # Output: Sentiment: recommended
216
+ ```
217
+
218
+ 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):
219
+
220
+ [Download Here](https://github.com/RezaGooner/Sentiment-Survey-Analyzer/tree/main/Dataset/big_train)
221
+
222
+ ---
223
+
224
+ Batch CSV processing:
225
+
226
+ ```bash
227
+ from PerSent import CommentAnalyzer
228
+ analyzer = CommentAnalyzer()
229
+ analyzer.load_model()
230
+
231
+ # Basic usage (single-column CSV)
232
+ analyzer.csvPredict(
233
+ input_csv="comments.csv",
234
+ output_path="results.csv"
235
+ )
236
+
237
+ # Alternative usage patterns:
238
+ # 1. Using column index (0-based)
239
+ analyzer.csvPredict("comments.csv", "results.csv", None, 0)
240
+
241
+ # 2. Negative indices (count from end)
242
+ analyzer.csvPredict("comments.csv", "results.csv", None, -1)
243
+
244
+ # 3. Column name
245
+ analyzer.csvPredict("comments.csv", "results.csv", None, "نظرات") # "Comments" column
246
+
247
+ # 4. With summary (single-column)
248
+ analyzer.csvPredict("comments.csv", "results.csv", "summary.csv")
249
+
250
+ # 5. With summary and column specification
251
+ analyzer.csvPredict("comments.csv", "results.csv", "summary.csv", 2)
252
+ ```
253
+
254
+ - Emotion Analysis
255
+
256
+ Single text analysis with pre-trained model:
257
+
258
+ ```bash
259
+ from PerSent import SentimentAnalyzer
260
+
261
+ analyzer = SentimentAnalyzer()
262
+ analyzer.load_model()
263
+
264
+ sample_text = "امتحانم رو خراب کردم. احساس می‌کنم یک شکست خورده‌ی تمام عیارم."
265
+ # "I failed my exam. I feel like a complete failure."
266
+
267
+ result = analyzer.analyze_text(sample_text)
268
+ for emotion, score in sorted(result.items(), key=lambda x: x[1], reverse=True):
269
+ print(f"{emotion}: {score:.2f}%")
270
+ ```
271
+
272
+ output :
273
+
274
+ ```bash
275
+ غم: 36.00% #Sadness
276
+ عصبانیت: 36.00% #anger
277
+ ترس: 28.00% #fear
278
+ شادی: 0.00% #happiness
279
+ تنفر: 0.00% #disgust
280
+ شگفتی: 0.00% #surprise
281
+ آرامش: 0.00% #calmness
282
+ ```
283
+
284
+ To train your own model:
285
+
286
+ ``` bash
287
+ analyzer.train_from_csv('emotion_dataset.csv')
288
+ ```
289
+
290
+ Required CSV columns:
291
+
292
+ 1- Keywords
293
+
294
+ 2- Emotion (happiness, sadness, anger, disgust, fear, calmness)
295
+
296
+ 3- Emotion weight
297
+
298
+ Model persistence:
299
+
300
+ ```bash
301
+ analyzer.save_model("custom_model_name")
302
+ analyzer.load_model("custom_model_name")
303
+ ```
304
+
305
+ Batch CSV processing:
306
+
307
+ ```bash
308
+ analyzer.analyze_csv("input.csv", "output.csv")
309
+ ```
310
+
311
+ ## Contribution
312
+ As mentioned, this library needs community collaboration. Please share suggestions, bugs, or feedback via:
313
+
314
+ - [Fork Repository & Pull Request](https://github.com/RezaGooner/PerSent/fork)
315
+
316
+ - [Create Issue](https://github.com/RezaGooner/PerSent/issues/new)
317
+
318
+ - Email: ```RezaAsadiProgrammer@gmail.com```
319
+
320
+ - 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