leilaghomashchi commited on
Commit
e38ad2b
·
verified ·
1 Parent(s): c79c517

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +355 -234
app.py CHANGED
@@ -1,7 +1,7 @@
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
  """
4
- سیستم کامل benchmark برای ناشناس‌سازی - همه چیز در یک فایل
5
  """
6
 
7
  import pandas as pd
@@ -18,7 +18,23 @@ import numpy as np
18
  logging.basicConfig(level=logging.INFO)
19
  logger = logging.getLogger(__name__)
20
 
21
- # ===== کلاس ناشناس‌ساز (کپی از کد اصلی) =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  class LightweightDataAnonymizer:
23
  def __init__(self):
24
  self.mapping_table = {}
@@ -37,61 +53,56 @@ class LightweightDataAnonymizer:
37
  self.api_key = os.getenv("OPENAI_API_KEY", "")
38
 
39
  def get_improved_patterns(self):
40
- """الگوهای کاملاً اصلاح شده - برای benchmark"""
41
  return {
42
  'COMPANY': [
43
- r'شرکت\s+پتروشیمی\s+[آ-ی‌یٰ-ٹa-zA-Z\s]+?(?=\s+مربوط|\s+در|\s+که|\s+با|\s+را|\s+به|،|\.|\s+$)',
44
- r'شرکت\s+[آ-ی‌یٰ-ٹa-zA-Z\s]*(?:پتروشیمی|نفت|گاز|صنایع|تولید|بانک)[آ-ی‌یٰ-ٹa-zA-Z\s‌]+(?=\s+مربوط|\s+در|\s+که|\s+با|\s+را|\s+به|،|\.|\s+$)',
45
- r'پتروشیمی\s+[آ-ی‌یٰ-ٹa-zA-Z\s‌]+?(?=\s+مربوط|\s+در|\s+که|\s+با|\s+را|\s+به|،|\.|\s+$)',
46
- r'شرکت\s+(?:سبهان|غدیر|شتران|شپنا|پترول|فارس|خارک|پلاسکو|جم|کرمان|مارون|اراک|رازی|شازند|کاوه|بندر|پارس|خوزستان|ماهشهر|عسلویه)(?=\s+مربوط|\s+در|\s+که|\s+با|\s+را|\s+به|،|\.|\s+$)',
47
- r'بانک\s+[آ-ی‌یٰ-ٹa-zA-Z\s‌]+?(?=\s+مربوط|\s+در|\s+که|\s+با|\s+را|\s+به|،|\.|\s+$)',
48
- r'[آ-ی‌یٰ-ٹa-zA-Z\s‌]*(?:پتروشیمی|صنایع|تولید|گاز|نفت|بانک)[آ-ی‌یٰ-ٹa-zA-Z\s‌]*\s+شرکت(?=\s|$|،|\.)',
49
- r'پتروشیمی\s+[آ-ی‌یٰ-ٹa-zA-Z\s‌]+\s+شرکت(?=\s|$|،|\.)',
50
- r'بانک\s+[آ-ی‌یٰ-ٹa-zA-Z\s‌]+\s+شرکت(?=\s|$|،|\.)',
51
  r'[A-Z][a-zA-Z\s]+(?:Inc|Corp|Corporation|Company|Ltd|Limited|LLC)'
52
  ],
53
 
54
  'LOCATION': [
55
- r'بندر\s+[آ-ی‌یٰ-ٹa-zA-Z\s‌]+?(?=\s+برگزار|\s+واقع|\s+در|،|\.|\s+$)',
56
- r'\b(?:تهران|اصفهان|ماهشهر|عسلویه|بندرعباس|اهواز|شیراز|مشهد|تبریز|کرج|قم|رشت|کرمان|یزد|زاهدان|بوشهر|خرمشهر|آبادان|اراک|قزوین)\b(?=\s+برگزار|\s+واقع|\s|$|،|\.)',
57
- r'استان\s+[آ-ی‌\s‌]+?(?=\s+واقع|\s+در|،|\.|\s+$)',
58
- r'شهر\s+[آ-ی‌\s‌]+?(?=\s+واقع|\s+در|،|\.|\s+$)',
59
  r'\b(?:ایران|عراق|کویت|عربستان|امارات|قطر|عمان|بحرین|ترکیه|پاکستان|افغانستان)\b',
60
  r'\b(?:London|Paris|Tokyo|New\s+York|Dubai|Singapore|Hong\s+Kong|Shanghai|Mumbai|Frankfurt|Amsterdam)\b'
61
  ],
62
 
63
  'DATE': [
64
- r'سال\s+مالی\s+منتهی\s+به\s+[٠-٩0-9]{1,2}\s+[آ-ی‌]+\s+[٠-٩0-9]{4}',
65
- r'[٠-٩0-9]{1,2}\s+(?:فروردین|اردیبهشت|خرداد|تیر|مرداد|شهریور|مهر|آبان|آذر|دی|بهمن|اسفند)\s+[٠-٩0-9]{4}',
66
- r'[٠-٩0-9]{1,2}\s+[آ-ی‌]+\s+[٠-٩0-9]{4}',
67
- r'[٠-٩0-9]{4}[/-][٠-٩0-9]{1,2}[/-][٠-٩0-9]{1,2}',
68
- r'[٠-٩0-9]{1,2}[/-][٠-٩0-9]{1,2}[/-][٠-٩0-9]{4}',
69
  r'(?:[0-9]{1,2})\s*(?:January|February|March|April|May|June|July|August|September|October|November|December)\s*(?:[0-9]{4})',
70
  r'(?:13[0-9]{2}|14[0-9]{2}|20[0-9]{2}|19[0-9]{2})(?=\s|$|،|\.)'
71
  ],
72
 
73
  'PERSON': [
74
- r'آقای\s+[آ-ی‌یٰ-ٹa-zA-Z\s]+?(?=\s|،|\.|\s+که|\s+در|$)',
75
- r'خانم\s+[آ-ی‌یٰ-ٹa-zA-Z\s]+?(?=\s|،|\.|\s+که|\s+در|$)',
76
- r'مهندس\s+[آ-ی‌یٰ-ٹa-zA-Z\s]+?(?=\s|،|\.|\s+که|\s+در|$)',
77
- r'دکتر\s+[آ-ی‌یٰ-ٹa-zA-Z\s]+?(?=\s|،|\.|\s+که|\s+در|$)',
78
- r'استاد\s+[آ-ی‌یٰ-ٹa-zA-Z\s]+?(?=\s|،|\.|\s+که|\s+در|$)',
 
 
79
  r'Mr\.\s+[a-zA-Z\s]+?(?=\s|,|\.|$)',
80
  r'Ms\.\s+[a-zA-Z\s]+?(?=\s|,|\.|$)',
81
- r'Dr\.\s+[a-zA-Z\s]+?(?=\s|,|\.|$)',
82
- r'[آ-ی‌یٰ-ٹa-zA-Z\s‌]+?(?:\s|،)\s*مدیرعامل(?=\s|$|،|\.)',
83
- r'مدیرعامل\s+[آ-ی‌یٰ-ٹa-zA-Z\s‌]+?(?=\s|$|،|\.)',
84
- r'رئیس\s+هیأت‌مدیره\s+[آ-ی‌یٰ-ٹa-zA-Z\s‌]+?(?=\s|$|،|\.)'
85
  ],
86
 
87
  'PHONE': [
88
- r'(?:تلفن[\s:]*)?(?:شماره[\s:]*)?(?:0)?(?:[٠-٩0-9]{2,3}[-\s]?)?[٠-٩0-9]{7,8}',
89
- r'(?:تماس[\s:]*)?(?:شماره[\s:]*)?(?:با[\s]*)?(?:0)?(?:[٠-٩0-9]{2,3}[-\s]?)?[٠-٩0-9]{7,8}',
90
- r'(?:موبایل[\s:]*)?(?:شماره[\s:]*)?(?:0)?9[٠-٩0-9]{9}',
91
- r'[٠-٩0-9]{3,4}[-\s][٠-٩0-9]{7,8}',
92
- r'[٠-٩0-9]{11}(?!\d)',
93
- r'(?:\+98|0098)?[٠-٩0-9]{10}',
94
- r'[٠-٩0-9]{3,4}[-\s]?[٠-٩0-9]{3,4}[-\s]?[٠-٩0-9]{3,4}',
95
  r'\+[0-9]{1,3}-[0-9]{3}-[0-9]{3}-[0-9]{4}(?:\s+ext\.\s+[0-9]{3,4})?',
96
  r'\([0-9]{3}\)\s+[0-9]{3}-[0-9]{4}'
97
  ],
@@ -101,93 +112,95 @@ class LightweightDataAnonymizer:
101
  r'ایمیل[\s:]*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
102
  r'email[\s:]*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
103
  r'نشانی[\s]*الکترونیکی[\s:]*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
104
- r'آدرس[\s]*ایمیل[\s:]*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
105
- r'facility\.manager@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
106
  ],
107
 
108
  'AMOUNT': [
109
  r'\d+(?:,\d{3})*\s*(?:میلیون|میلیارد|هزار)\s*تومان',
110
  r'مبلغ\s+\d+(?:,\d{3})*\s*(?:میلیون|میلیارد|هزار)?\s*تومان',
111
- r'\d+\s*تومان(?=\s+به\s+ازای|\s+فروش|،)',
112
  r'\$\d+(?:,\d{3})*(?:\.\d+)?\s*(?:million|billion|thousand|M|B|K)?',
113
  r'\d+(?:,\d{3})*\s*ریال'
114
  ],
115
 
116
  'ACCOUNT': [
117
- r'(?:شماره[\s]*)?(?:حساب[\s]*)?(?:بانکی[\s:]*)?(?:[٠-٩0-9]{1,3}[-\s]?)*[٠-٩0-9]{8,20}',
118
- r'حساب[\s]*(?:شماره[\s:]*)?(?:[٠-٩0-9]{1,3}[-\s]?)*[٠-٩0-9]{8,20}',
119
- r'شماره[\s]*حساب[\s:]*(?:[٠-٩0-9]{1,3}[-\s]?)*[٠-٩0-9]{8,20}',
120
  r'Account[\s]*(?:Number[\s:]*)?(?:[0-9]{1,3}[-\s]?)*[0-9]{8,20}',
121
- r'[٠-٩0-9]{3}[-\s]?[٠-٩0-9]{3}[-\s]?[٠-٩0-9]{6,12}',
122
- r'[٠-٩0-9]{2,4}[-\s]?[٠-٩0-9]{6,12}[-\s]?[٠-٩0-9]{2,4}'
123
- ],
124
-
125
- 'MIXED_NAMES': [
126
- r'\b[A-Z][a-z]+-[A-Z][a-z]+\b',
127
- r"\b[A-Z]'[A-Z][a-z]+\b",
128
- r'Dr\.\s+[A-Z][a-zA-Z\s]+?(?=\s|,|\.|$)'
129
  ]
130
  }
131
 
132
- def is_valid_company_name(self, company_text):
133
- """بررسی ساده - فقط کلمات ممنوع"""
134
- forbidden_words = ['مطرح', 'شد', 'کرد', 'داد', 'است', ود']
 
 
 
 
 
 
 
 
 
 
135
  for word in forbidden_words:
136
- if word in company_text:
137
  return False
 
 
 
 
 
 
 
138
  return True
139
 
140
- # ===== تابع کمکی برای تبدیل numpy/pandas types =====
141
- def convert_to_serializable(obj):
142
- """تبدیل numpy/pandas types به Python native types برای JSON serialization"""
143
- if isinstance(obj, (np.integer, np.int64, np.int32)):
144
- return int(obj)
145
- elif isinstance(obj, (np.floating, np.float64, np.float32)):
146
- return float(obj)
147
- elif isinstance(obj, np.ndarray):
148
- return obj.tolist()
149
- elif isinstance(obj, dict):
150
- return {key: convert_to_serializable(value) for key, value in obj.items()}
151
- elif isinstance(obj, list):
152
- return [convert_to_serializable(item) for item in obj]
153
- else:
154
- return obj
155
-
156
- # ===== کلاس Benchmark =====
157
  class AnonymizationBenchmark:
158
  def __init__(self):
159
  self.anonymizer = LightweightDataAnonymizer()
160
  self.results = []
161
 
162
  def extract_entities_from_text(self, text: str) -> Dict[str, List[str]]:
163
- """استخراج تمام entities موجود در متن اصلی"""
164
  entities = defaultdict(list)
165
 
166
- # استفاده از الگوهای سیستم ناشناس‌سازی
 
 
 
167
  patterns = self.anonymizer.get_improved_patterns()
168
 
169
  for category, pattern_list in patterns.items():
170
- for pattern in pattern_list:
171
  try:
172
- matches = re.finditer(pattern, text, re.IGNORECASE | re.MULTILINE)
 
 
 
173
  for match in matches:
174
  # capture کل match
175
  full_match = match.group(0).strip()
176
 
177
- if len(full_match) >= 3 and not full_match.isspace():
178
- # بررسی خاص ب��ای شرکت‌ها
179
- if category == 'COMPANY':
180
- if self.anonymizer.is_valid_company_name(full_match):
181
- entities[category].append(full_match)
182
- else:
183
- entities[category].append(full_match)
184
  except re.error as e:
185
- logger.error(f"Regex error in pattern {pattern}: {e}")
 
 
 
186
  continue
187
 
188
- # حذف duplicates
189
  for category in entities:
190
- entities[category] = list(set(entities[category]))
191
 
192
  return dict(entities)
193
 
@@ -195,25 +208,39 @@ class AnonymizationBenchmark:
195
  """استخراج کدهای ناشناس‌سازی شده از متن خروجی"""
196
  anonymized_entities = defaultdict(list)
197
 
 
 
 
198
  # الگو برای کدهای ناشناس‌سازی: category_number
199
- pattern = r'([a-z_]+)_(\d{3})'
200
 
201
- matches = re.finditer(pattern, anonymized_text, re.IGNORECASE)
202
- for match in matches:
203
- category = match.group(1).upper()
204
- code = match.group(0)
205
- anonymized_entities[category].append(code)
 
 
 
 
 
 
 
206
 
207
  return dict(anonymized_entities)
208
 
209
  def calculate_metrics_for_text(self, original_text: str, anonymized_text: str) -> Dict:
210
  """محاسبه متریک‌های ارزیابی برای یک جفت متن"""
211
 
 
 
212
  # استخراج entities از متن اصلی
213
  original_entities = self.extract_entities_from_text(original_text)
 
214
 
215
  # استخراج کدهای ناشناس‌سازی شده
216
  anonymized_codes = self.extract_anonymized_codes(anonymized_text)
 
217
 
218
  # محاسبه متریک‌ها برای هر category
219
  category_metrics = {}
@@ -264,6 +291,8 @@ class AnonymizationBenchmark:
264
  total_original = sum(len(entities) for entities in original_entities.values())
265
  accuracy = total_tp / total_original if total_original > 0 else 0
266
 
 
 
267
  return {
268
  'original_entities': original_entities,
269
  'anonymized_codes': anonymized_codes,
@@ -286,15 +315,29 @@ class AnonymizationBenchmark:
286
 
287
  # خواندن فایل CSV
288
  try:
289
- df = pd.read_csv(csv_file_path)
290
- logger.info(f"Loaded CSV file with {len(df)} rows")
 
 
 
 
 
 
 
 
 
291
  except Exception as e:
292
  logger.error(f"Error loading CSV file: {e}")
293
  return None
294
 
295
  # بررسی وجود ستون‌های مورد نیاز
296
  if 'original_text' not in df.columns or 'anonymized_text' not in df.columns:
297
- logger.error("CSV file must contain 'original_text' and 'anonymized_text' columns")
 
 
 
 
 
298
  return None
299
 
300
  results = []
@@ -302,37 +345,50 @@ class AnonymizationBenchmark:
302
  for index, row in df.iterrows():
303
  logger.info(f"Processing row {index + 1}/{len(df)}")
304
 
305
- original_text = str(row['original_text'])
306
- anonymized_text = str(row['anonymized_text'])
307
-
308
- # محاسبه متریک‌ها
309
- metrics = self.calculate_metrics_for_text(original_text, anonymized_text)
310
-
311
- # ذخیره نتایج
312
- result = {
313
- 'row_id': index,
314
- 'original_text': original_text,
315
- 'anonymized_text': anonymized_text,
316
- 'total_original_entities': metrics['overall_metrics']['total_original_entities'],
317
- 'total_anonymized_entities': metrics['overall_metrics']['total_anonymized_entities'],
318
- 'tp': metrics['overall_metrics']['total_tp'],
319
- 'fp': metrics['overall_metrics']['total_fp'],
320
- 'fn': metrics['overall_metrics']['total_fn'],
321
- 'precision': metrics['overall_metrics']['precision'],
322
- 'recall': metrics['overall_metrics']['recall'],
323
- 'f1_score': metrics['overall_metrics']['f1_score'],
324
- 'accuracy': metrics['overall_metrics']['accuracy']
325
- }
326
-
327
- # اضافه کردن متریک‌های category به result
328
- for category, cat_metrics in metrics['category_metrics'].items():
329
- result[f'{category.lower()}_precision'] = cat_metrics['precision']
330
- result[f'{category.lower()}_recall'] = cat_metrics['recall']
331
- result[f'{category.lower()}_f1'] = cat_metrics['f1_score']
332
- result[f'{category.lower()}_original_count'] = cat_metrics['original_count']
333
- result[f'{category.lower()}_anonymized_count'] = cat_metrics['anonymized_count']
334
-
335
- results.append(result)
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
  return pd.DataFrame(results)
338
 
@@ -342,88 +398,98 @@ class AnonymizationBenchmark:
342
  if results_df is None or len(results_df) == 0:
343
  return {}
344
 
345
- summary = {
346
- 'total_texts_processed': len(results_df),
347
- 'average_metrics': {
348
- 'precision': float(results_df['precision'].mean()),
349
- 'recall': float(results_df['recall'].mean()),
350
- 'f1_score': float(results_df['f1_score'].mean()),
351
- 'accuracy': float(results_df['accuracy'].mean())
352
- },
353
- 'total_entities': {
354
- 'original': int(results_df['total_original_entities'].sum()),
355
- 'anonymized': int(results_df['total_anonymized_entities'].sum()),
356
- 'tp': int(results_df['tp'].sum()),
357
- 'fp': int(results_df['fp'].sum()),
358
- 'fn': int(results_df['fn'].sum())
 
 
359
  }
360
- }
361
-
362
- # محاسبه متریک‌های کلی بر اساس مجموع
363
- total_tp = summary['total_entities']['tp']
364
- total_fp = summary['total_entities']['fp']
365
- total_fn = summary['total_entities']['fn']
366
- total_original = summary['total_entities']['original']
367
-
368
- summary['overall_metrics'] = {
369
- 'precision': total_tp / (total_tp + total_fp) if (total_tp + total_fp) > 0 else 0,
370
- 'recall': total_tp / (total_tp + total_fn) if (total_tp + total_fn) > 0 else 0,
371
- 'accuracy': total_tp / total_original if total_original > 0 else 0
372
- }
373
-
374
- # F1-Score کلی
375
- overall_precision = summary['overall_metrics']['precision']
376
- overall_recall = summary['overall_metrics']['recall']
377
- summary['overall_metrics']['f1_score'] = 2 * (overall_precision * overall_recall) / (overall_precision + overall_recall) if (overall_precision + overall_recall) > 0 else 0
378
-
379
- # آمار category-wise
380
- category_columns = [col for col in results_df.columns if col.endswith('_precision')]
381
- categories = [col.replace('_precision', '').upper() for col in category_columns]
382
-
383
- category_summary = {}
384
- for category in categories:
385
- cat_lower = category.lower()
386
- if f'{cat_lower}_precision' in results_df.columns:
387
- # فیلتر کردن ردیف‌هایی که این category دارند
388
- mask = results_df[f'{cat_lower}_original_count'] > 0
389
- if mask.any():
390
- category_summary[category] = {
391
- 'count_texts_with_category': int(mask.sum()),
392
- 'average_precision': float(results_df.loc[mask, f'{cat_lower}_precision'].mean()),
393
- 'average_recall': float(results_df.loc[mask, f'{cat_lower}_recall'].mean()),
394
- 'average_f1': float(results_df.loc[mask, f'{cat_lower}_f1'].mean()),
395
- 'total_original': int(results_df[f'{cat_lower}_original_count'].sum()),
396
- 'total_anonymized': int(results_df[f'{cat_lower}_anonymized_count'].sum())
397
- }
398
-
399
- summary['category_summary'] = category_summary
400
-
401
- # تبدیل همه مقادیر به serializable types
402
- summary = convert_to_serializable(summary)
403
-
404
- return summary
 
 
 
405
 
406
  # ===== رابط گرافیکی =====
407
  def create_sample_csv():
408
  """ایجاد فایل نمونه CSV برای تست"""
409
  sample_data = [
410
  {
411
- 'original_text': 'مجمع عمومی عادی سالیانه و مجمع‌عمومی عادی به طور فوق‌العاده شرکت پتروشیمی کارون مربوط به سال مالی منتهی به ۳۰ اسفند ۱۴۰۳ در محل سالن جلسات این شرکت در بندر ماهشهر برگزار شد.',
412
- 'anonymized_text': 'مجمع عمومی عادی سالیانه و مجمع‌عمومی عادی به طور فوق‌العاده company_001 مربوط به سال مالی منتهی به date_001 در محل سالن جلسات این شرکت در location_001 برگزار شد.'
413
  },
414
  {
415
- 'original_text': 'آقای احمد محمدی مدیرعامل شرکت با شماره تماس 09123456789 و ایمیل ahmad@company.com در تاریخ 15 آذر 1403 قرارداد را امضا کرد.',
416
- 'anonymized_text': 'person_001 مدیرعامل شرکت با شماره تماس phone_001 و ایمیل email_001 در تاریخ date_001 قرارداد را امضا کرد.'
417
  },
418
  {
419
- 'original_text': 'بانک ملی ایران با شماره حساب 123-456-789012 مبلغ 500 میلیون تومان را به حساب شرکت واریز کرد.',
420
- 'anonymized_text': 'company_001 با شماره حساب account_001 مبلغ amount_001 را به حساب شرکت واریز کرد.'
421
  }
422
  ]
423
 
424
  df = pd.DataFrame(sample_data)
425
- df.to_csv('sample_benchmark_data.csv', index=False, encoding='utf-8')
426
- return "فایل نمونه 'sample_benchmark_data.csv' ایجاد شد."
 
 
 
 
 
427
 
428
  def process_csv_file(file):
429
  """پردازش فایل CSV آپلود شده"""
@@ -431,76 +497,105 @@ def process_csv_file(file):
431
  return "لطفاً فایل CSV را آپلود کنید.", None, None
432
 
433
  try:
 
 
 
 
 
 
434
  # خواندن فایل آپلود شده
435
- df = pd.read_csv(file.name, encoding='utf-8')
 
 
 
 
 
 
 
436
 
437
  # بررسی ستون‌ها
438
  if 'original_text' not in df.columns or 'anonymized_text' not in df.columns:
439
- return "فایل CSV باید شامل ستون‌های 'original_text' و 'anonymized_text' باشد.", None, None
440
 
441
  # اجرای benchmark
442
  benchmark = AnonymizationBenchmark()
443
  results_df = benchmark.benchmark_from_csv(file.name)
444
 
445
- if results_df is None:
446
- return "خطا در پردازش فایل CSV!", None, None
 
 
447
 
448
  # تولید گزارش خلاصه
449
  summary = benchmark.generate_summary_report(results_df)
450
 
 
 
 
451
  # آماده‌سازی نتایج برای نمایش
452
  metrics_text = f"""
453
  === نتایج کلی Benchmark ===
454
 
455
- تعداد متون پردازش شده: {summary['total_texts_processed']}
456
 
457
- === متریک‌های کلی ===
458
- • Precision: {summary['overall_metrics']['precision']:.4f}
459
- • Recall: {summary['overall_metrics']['recall']:.4f}
460
- • F1-Score: {summary['overall_metrics']['f1_score']:.4f}
461
- • Accuracy: {summary['overall_metrics']['accuracy']:.4f}
462
 
463
  === آمار کلی Entities ===
464
- • تعداد کل Entities اصلی: {summary['total_entities']['original']}
465
- • تعداد کل Entities ناشناس‌سازی شده: {summary['total_entities']['anonymized']}
466
- • True Positives: {summary['total_entities']['tp']}
467
- • False Positives: {summary['total_entities']['fp']}
468
- • False Negatives: {summary['total_entities']['fn']}
469
 
470
  === متریک‌های میانگین ===
471
- • میانگین Precision: {summary['average_metrics']['precision']:.4f}
472
- • میانگین Recall: {summary['average_metrics']['recall']:.4f}
473
- • میانگین F1-Score: {summary['average_metrics']['f1_score']:.4f}
474
- • میانگین Accuracy: {summary['average_metrics']['accuracy']:.4f}
475
  """
476
 
477
  # اضافه کردن آمار دسته‌بندی‌ها
478
- if 'category_summary' in summary:
479
  metrics_text += "\n=== آمار دسته‌بندی‌ها ===\n"
480
  for category, stats in summary['category_summary'].items():
481
  metrics_text += f"""
482
  {category}:
483
- • تعداد متون دارای این دسته: {stats['count_texts_with_category']}
484
- • میانگین Precision: {stats['average_precision']:.4f}
485
- • میانگین Recall: {stats['average_recall']:.4f}
486
- • میانگین F1-Score: {stats['average_f1']:.4f}
487
- • کل Entities اصلی: {stats['total_original']}
488
- • کل Entities ناشناس‌سازی شده: {stats['total_anonymized']}
489
  """
 
 
490
 
491
  # ذخیره گزارش‌ها
492
- results_df.to_csv("benchmark_results_detailed.csv", index=False, encoding='utf-8')
493
- with open("benchmark_results_summary.json", 'w', encoding='utf-8') as f:
494
- json.dump(summary, f, ensure_ascii=False, indent=2)
 
 
 
 
 
 
 
 
495
 
496
  return (
497
  metrics_text,
498
- results_df[['row_id', 'precision', 'recall', 'f1_score', 'accuracy', 'total_original_entities', 'total_anonymized_entities']],
499
  summary
500
  )
501
 
502
  except Exception as e:
503
- return f"خطا در پردازش: {str(e)}", None, None
 
504
 
505
  def download_results():
506
  """دانلود فایل نتایج"""
@@ -516,7 +611,7 @@ def main():
516
 
517
  gr.HTML("""
518
  <h1 style='text-align: center; color: #2E86AB; margin-bottom: 30px;'>
519
- 📊 سیستم ارزیابی Benchmark ناشناس‌سازی
520
  </h1>
521
  """)
522
 
@@ -527,7 +622,8 @@ def main():
527
  <div style='background: #f0f8ff; padding: 15px; border-radius: 10px; margin-bottom: 15px;'>
528
  <b>فرمت فایل CSV:</b><br>
529
  • ستون اول: <code>original_text</code> (متن اصلی)<br>
530
- • ستون دوم: <code>anonymized_text</code> (متن ناشناس‌سازی شده)
 
531
  </div>
532
  """)
533
 
@@ -537,9 +633,9 @@ def main():
537
  file_count="single"
538
  )
539
 
540
- benchmark_btn = gr.Button("🚀 شروع Benchmark", variant="primary")
541
-
542
- sample_btn = gr.Button("📄 ایجاد فایل نمونه", variant="secondary")
543
 
544
  with gr.Row():
545
  with gr.Column():
@@ -547,8 +643,8 @@ def main():
547
 
548
  metrics_output = gr.Textbox(
549
  label="متریک‌های کلی",
550
- lines=25,
551
- max_lines=30,
552
  interactive=False
553
  )
554
 
@@ -558,7 +654,8 @@ def main():
558
 
559
  results_table = gr.Dataframe(
560
  label="نتایج هر متن",
561
- interactive=False
 
562
  )
563
 
564
  with gr.Row():
@@ -566,15 +663,39 @@ def main():
566
  download_btn = gr.Button("💾 دانلود نتایج کامل", variant="secondary")
567
  download_file = gr.File(label="فایل نتایج", visible=False)
568
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
  # Event handlers
 
 
 
 
 
 
 
 
 
570
  benchmark_btn.click(
571
- fn=process_csv_file,
572
  inputs=[file_input],
573
  outputs=[metrics_output, results_table, gr.State()]
574
  )
575
 
576
  sample_btn.click(
577
- fn=create_sample_csv,
578
  outputs=[gr.Textbox(visible=False)]
579
  )
580
 
@@ -592,7 +713,7 @@ def main():
592
 
593
  demo = main()
594
  if __name__ == "__main__":
595
- port = int(os.getenv("PORT", "7860")) # Hugging Face پورت را در این متغیر می‌گذارد
596
  demo.launch(
597
  share=False,
598
  server_name="0.0.0.0",
 
1
  #!/usr/bin/env python3
2
  # -*- coding: utf-8 -*-
3
  """
4
+ سیستم کامل benchmark برای ناشناس‌سازی - همه چیز در یک فایل - ورژن کاملاً اصلاح شده
5
  """
6
 
7
  import pandas as pd
 
18
  logging.basicConfig(level=logging.INFO)
19
  logger = logging.getLogger(__name__)
20
 
21
+ # ===== تابع کمکی برای تبدیل numpy/pandas types =====
22
+ def convert_to_serializable(obj):
23
+ """تبدیل numpy/pandas types به Python native types برای JSON serialization"""
24
+ if isinstance(obj, (np.integer, np.int64, np.int32)):
25
+ return int(obj)
26
+ elif isinstance(obj, (np.floating, np.float64, np.float32)):
27
+ return float(obj)
28
+ elif isinstance(obj, np.ndarray):
29
+ return obj.tolist()
30
+ elif isinstance(obj, dict):
31
+ return {key: convert_to_serializable(value) for key, value in obj.items()}
32
+ elif isinstance(obj, list):
33
+ return [convert_to_serializable(item) for item in obj]
34
+ else:
35
+ return obj
36
+
37
+ # ===== کلاس ناشناس‌ساز بهبود یافته =====
38
  class LightweightDataAnonymizer:
39
  def __init__(self):
40
  self.mapping_table = {}
 
53
  self.api_key = os.getenv("OPENAI_API_KEY", "")
54
 
55
  def get_improved_patterns(self):
56
+ """الگوهای کاملاً بهبود یافته و تست شده"""
57
  return {
58
  'COMPANY': [
59
+ r'شرکت\s+پتروشیمی\s+[\u0600-\u06FF\u0750-\u077F\s\u200C]+?(?=\s|$|،|\.)',
60
+ r'شرکت\s+[\u0600-\u06FF\u0750-\u077F\s\u200C]*(?:پتروشیمی|نفت|گاز|صنایع|تولید)[\u0600-\u06FF\u0750-\u077F\s\u200C]*',
61
+ r'بانک\s+[\u0600-\u06FF\u0750-\u077F\s\u200C]+',
62
+ r'شرکت\s+[\u0600-\u06FF\u0750-\u077F\s\u200C]+',
63
+ r'(?:پتروشیمی|بانک)\s+[\u0600-\u06FF\u0750-\u077F\s\u200C]+',
 
 
 
64
  r'[A-Z][a-zA-Z\s]+(?:Inc|Corp|Corporation|Company|Ltd|Limited|LLC)'
65
  ],
66
 
67
  'LOCATION': [
68
+ r'بندر\s+[\u0600-\u06FF\u0750-\u077F\s\u200C]+',
69
+ r'شهر\s+[\u0600-\u06FF\u0750-\u077F\s\u200C]+',
70
+ r'استان\s+[\u0600-\u06FF\u0750-\u077F\s\u200C]+',
71
+ r'\b(?:تهران|اصفهان|ماهشهر|عسلویه|بندرعباس|اهواز|شیراز|مشهد|تبریز|کرج|قم|رشت|کرمان|یزد|زاهدان|بوشهر|خرمشهر|آبادان|اراک|قزوین)\b',
72
  r'\b(?:ایران|عراق|کویت|عربستان|امارات|قطر|عمان|بحرین|ترکیه|پاکستان|افغانستان)\b',
73
  r'\b(?:London|Paris|Tokyo|New\s+York|Dubai|Singapore|Hong\s+Kong|Shanghai|Mumbai|Frankfurt|Amsterdam)\b'
74
  ],
75
 
76
  'DATE': [
77
+ r'سال\s+مالی\s+منتهی\s+به\s+[\u06F0-\u06F90-9]{1,2}\s+[\u0600-\u06FF\u0750-\u077F]+\s+[\u06F0-\u06F90-9]{4}',
78
+ r'[\u06F0-\u06F90-9]{1,2}\s+(?:فروردین|اردیبهشت|خرداد|تیر|مرداد|شهریور|مهر|آبان|آذر|دی|بهمن|اسفند)\s+[\u06F0-\u06F90-9]{4}',
79
+ r'[\u06F0-\u06F90-9]{1,2}\s+[\u0600-\u06FF\u0750-\u077F]+\s+[\u06F0-\u06F90-9]{4}',
80
+ r'[\u06F0-\u06F90-9]{4}[/-][\u06F0-\u06F90-9]{1,2}[/-][\u06F0-\u06F90-9]{1,2}',
81
+ r'[\u06F0-\u06F90-9]{1,2}[/-][\u06F0-\u06F90-9]{1,2}[/-][\u06F0-\u06F90-9]{4}',
82
  r'(?:[0-9]{1,2})\s*(?:January|February|March|April|May|June|July|August|September|October|November|December)\s*(?:[0-9]{4})',
83
  r'(?:13[0-9]{2}|14[0-9]{2}|20[0-9]{2}|19[0-9]{2})(?=\s|$|،|\.)'
84
  ],
85
 
86
  'PERSON': [
87
+ r'آقای\s+[\u0600-\u06FF\u0750-\u077F\s\u200C]+?(?=\s|$|،|\.)',
88
+ r'خانم\s+[\u0600-\u06FF\u0750-\u077F\s\u200C]+?(?=\s|$|،|\.)',
89
+ r'مهندس\s+[\u0600-\u06FF\u0750-\u077F\s\u200C]+?(?=\s|$|،|\.)',
90
+ r'دکتر\s+[\u0600-\u06FF\u0750-\u077F\s\u200C]+?(?=\s|$|،|\.)',
91
+ r'استاد\s+[\u0600-\u06FF\u0750-\u077F\s\u200C]+?(?=\s|$|،|\.)',
92
+ r'مدیرعامل\s+[\u0600-\u06FF\u0750-\u077F\s\u200C]+?(?=\s|$|،|\.)',
93
+ r'[\u0600-\u06FF\u0750-\u077F\s\u200C]+\s+مدیرعامل(?=\s|$|،|\.)',
94
  r'Mr\.\s+[a-zA-Z\s]+?(?=\s|,|\.|$)',
95
  r'Ms\.\s+[a-zA-Z\s]+?(?=\s|,|\.|$)',
96
+ r'Dr\.\s+[a-zA-Z\s]+?(?=\s|,|\.|$)'
 
 
 
97
  ],
98
 
99
  'PHONE': [
100
+ r'(?:تلفن[\s:]*)?(?:شماره[\s:]*)?(?:0)?(?:[\u06F0-\u06F90-9]{2,3}[-\s]?)?[\u06F0-\u06F90-9]{7,8}',
101
+ r'(?:تماس[\s:]*)?(?:شماره[\s:]*)?(?:با[\s]*)?(?:0)?(?:[\u06F0-\u06F90-9]{2,3}[-\s]?)?[\u06F0-\u06F90-9]{7,8}',
102
+ r'(?:موبایل[\s:]*)?(?:شماره[\s:]*)?(?:0)?9[\u06F0-\u06F90-9]{9}',
103
+ r'[\u06F0-\u06F90-9]{3,4}[-\s][\u06F0-\u06F90-9]{7,8}',
104
+ r'[\u06F0-\u06F90-9]{11}(?!\d)',
105
+ r'09[\u06F0-\u06F90-9]{9}',
 
106
  r'\+[0-9]{1,3}-[0-9]{3}-[0-9]{3}-[0-9]{4}(?:\s+ext\.\s+[0-9]{3,4})?',
107
  r'\([0-9]{3}\)\s+[0-9]{3}-[0-9]{4}'
108
  ],
 
112
  r'ایمیل[\s:]*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
113
  r'email[\s:]*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
114
  r'نشانی[\s]*الکترونیکی[\s:]*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
115
+ r'آدرس[\s]*ایمیل[\s:]*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
 
116
  ],
117
 
118
  'AMOUNT': [
119
  r'\d+(?:,\d{3})*\s*(?:میلیون|میلیارد|هزار)\s*تومان',
120
  r'مبلغ\s+\d+(?:,\d{3})*\s*(?:میلیون|میلیارد|هزار)?\s*تومان',
121
+ r'\d+\s*تومان',
122
  r'\$\d+(?:,\d{3})*(?:\.\d+)?\s*(?:million|billion|thousand|M|B|K)?',
123
  r'\d+(?:,\d{3})*\s*ریال'
124
  ],
125
 
126
  'ACCOUNT': [
127
+ r'(?:شماره[\s]*)?(?:حساب[\s]*)?(?:بانکی[\s:]*)?(?:[\u06F0-\u06F90-9]{1,3}[-\s]?)*[\u06F0-\u06F90-9]{8,20}',
128
+ r'حساب[\s]*(?:شماره[\s:]*)?(?:[\u06F0-\u06F90-9]{1,3}[-\s]?)*[\u06F0-\u06F90-9]{8,20}',
129
+ r'شماره[\s]*حساب[\s:]*(?:[\u06F0-\u06F90-9]{1,3}[-\s]?)*[\u06F0-\u06F90-9]{8,20}',
130
  r'Account[\s]*(?:Number[\s:]*)?(?:[0-9]{1,3}[-\s]?)*[0-9]{8,20}',
131
+ r'[\u06F0-\u06F90-9]{3}[-\s]?[\u06F0-\u06F90-9]{3}[-\s]?[\u06F0-\u06F90-9]{6,12}'
 
 
 
 
 
 
 
132
  ]
133
  }
134
 
135
+ def is_valid_entity(self, entity_text, category):
136
+ """بررسی معتبر بودن entity"""
137
+ # کلمات ممنوع عمومی
138
+ forbidden_words = ['شد', 'کرد', 'داد', 'است', 'بود', 'در', 'که', 'با', 'از', 'به', 'را', 'و', 'یا']
139
+
140
+ # حذف فاصله‌های اضافی
141
+ entity_text = re.sub(r'\s+', ' ', entity_text.strip())
142
+
143
+ # بررسی طول کافی
144
+ if len(entity_text) < 3:
145
+ return False
146
+
147
+ # بررسی کلمات ممنوع
148
  for word in forbidden_words:
149
+ if entity_text.endswith(' ' + word) or entity_text.startswith(word + ' '):
150
  return False
151
+
152
+ # بررسی‌های خاص بر اساس دسته‌بندی
153
+ if category == 'COMPANY':
154
+ # شرکت نباید فقط کلمات عمومی باشد
155
+ if entity_text in ['شرکت', 'بانک', 'پتروشیمی']:
156
+ return False
157
+
158
  return True
159
 
160
+ # ===== کلاس Benchmark بهبود یافته =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  class AnonymizationBenchmark:
162
  def __init__(self):
163
  self.anonymizer = LightweightDataAnonymizer()
164
  self.results = []
165
 
166
  def extract_entities_from_text(self, text: str) -> Dict[str, List[str]]:
167
+ """استخراج تمام entities موجود در متن اصلی با الگوهای بهبود یافته"""
168
  entities = defaultdict(list)
169
 
170
+ if not text or text.strip() == '':
171
+ return dict(entities)
172
+
173
+ # استفاده از الگوهای بهبود یافته
174
  patterns = self.anonymizer.get_improved_patterns()
175
 
176
  for category, pattern_list in patterns.items():
177
+ for pattern_str in pattern_list:
178
  try:
179
+ # تبدیل string به regex object
180
+ pattern = re.compile(pattern_str, re.IGNORECASE | re.MULTILINE)
181
+
182
+ matches = pattern.finditer(text)
183
  for match in matches:
184
  # capture کل match
185
  full_match = match.group(0).strip()
186
 
187
+ # تمیز کردن match
188
+ full_match = re.sub(r'\s+', ' ', full_match)
189
+ full_match = re.sub(r'\s*(در|که|با|به|از|را)\s*$', '', full_match).strip()
190
+
191
+ if self.anonymizer.is_valid_entity(full_match, category):
192
+ entities[category].append(full_match)
193
+
194
  except re.error as e:
195
+ logger.error(f"Regex error in pattern {pattern_str}: {e}")
196
+ continue
197
+ except Exception as e:
198
+ logger.error(f"Unexpected error in pattern {pattern_str}: {e}")
199
  continue
200
 
201
+ # حذف duplicates و مرتب‌سازی
202
  for category in entities:
203
+ entities[category] = sorted(list(set(entities[category])))
204
 
205
  return dict(entities)
206
 
 
208
  """استخراج کدهای ناشناس‌سازی شده از متن خروجی"""
209
  anonymized_entities = defaultdict(list)
210
 
211
+ if not anonymized_text or anonymized_text.strip() == '':
212
+ return dict(anonymized_entities)
213
+
214
  # الگو برای کدهای ناشناس‌سازی: category_number
215
+ pattern = r'([a-zA-Z_]+)_(\d{3})'
216
 
217
+ try:
218
+ matches = re.finditer(pattern, anonymized_text, re.IGNORECASE)
219
+ for match in matches:
220
+ category = match.group(1).upper()
221
+ code = match.group(0)
222
+ anonymized_entities[category].append(code)
223
+ except Exception as e:
224
+ logger.error(f"Error extracting anonymized codes: {e}")
225
+
226
+ # حذف duplicates
227
+ for category in anonymized_entities:
228
+ anonymized_entities[category] = sorted(list(set(anonymized_entities[category])))
229
 
230
  return dict(anonymized_entities)
231
 
232
  def calculate_metrics_for_text(self, original_text: str, anonymized_text: str) -> Dict:
233
  """محاسبه متریک‌های ارزیابی برای یک جفت متن"""
234
 
235
+ logger.info(f"Processing texts - Original length: {len(original_text)}, Anonymized length: {len(anonymized_text)}")
236
+
237
  # استخراج entities از متن اصلی
238
  original_entities = self.extract_entities_from_text(original_text)
239
+ logger.info(f"Original entities found: {original_entities}")
240
 
241
  # استخراج کدهای ناشناس‌سازی شده
242
  anonymized_codes = self.extract_anonymized_codes(anonymized_text)
243
+ logger.info(f"Anonymized codes found: {anonymized_codes}")
244
 
245
  # محاسبه متریک‌ها برای هر category
246
  category_metrics = {}
 
291
  total_original = sum(len(entities) for entities in original_entities.values())
292
  accuracy = total_tp / total_original if total_original > 0 else 0
293
 
294
+ logger.info(f"Calculated metrics - TP: {total_tp}, FP: {total_fp}, FN: {total_fn}")
295
+
296
  return {
297
  'original_entities': original_entities,
298
  'anonymized_codes': anonymized_codes,
 
315
 
316
  # خواندن فایل CSV
317
  try:
318
+ # تلاش برای خواندن با encoding مختلف
319
+ for encoding in ['utf-8', 'utf-8-sig', 'cp1256', 'windows-1256']:
320
+ try:
321
+ df = pd.read_csv(csv_file_path, encoding=encoding)
322
+ logger.info(f"Successfully loaded CSV with {encoding} encoding. Shape: {df.shape}")
323
+ break
324
+ except UnicodeDecodeError:
325
+ continue
326
+ else:
327
+ raise Exception("Could not read CSV file with any encoding")
328
+
329
  except Exception as e:
330
  logger.error(f"Error loading CSV file: {e}")
331
  return None
332
 
333
  # بررسی وجود ستون‌های مورد نیاز
334
  if 'original_text' not in df.columns or 'anonymized_text' not in df.columns:
335
+ logger.error(f"CSV file must contain 'original_text' and 'anonymized_text' columns. Found columns: {df.columns.tolist()}")
336
+ return None
337
+
338
+ # بررسی اینکه آیا داده موجود است
339
+ if len(df) == 0:
340
+ logger.error("CSV file is empty")
341
  return None
342
 
343
  results = []
 
345
  for index, row in df.iterrows():
346
  logger.info(f"Processing row {index + 1}/{len(df)}")
347
 
348
+ try:
349
+ original_text = str(row['original_text']) if pd.notna(row['original_text']) else ""
350
+ anonymized_text = str(row['anonymized_text']) if pd.notna(row['anonymized_text']) else ""
351
+
352
+ if original_text.strip() == "" and anonymized_text.strip() == "":
353
+ logger.warning(f"Row {index} has empty texts, skipping...")
354
+ continue
355
+
356
+ # محاسبه متریک‌ها
357
+ metrics = self.calculate_metrics_for_text(original_text, anonymized_text)
358
+
359
+ # ذخیره نتایج
360
+ result = {
361
+ 'row_id': int(index),
362
+ 'original_text': original_text,
363
+ 'anonymized_text': anonymized_text,
364
+ 'total_original_entities': int(metrics['overall_metrics']['total_original_entities']),
365
+ 'total_anonymized_entities': int(metrics['overall_metrics']['total_anonymized_entities']),
366
+ 'tp': int(metrics['overall_metrics']['total_tp']),
367
+ 'fp': int(metrics['overall_metrics']['total_fp']),
368
+ 'fn': int(metrics['overall_metrics']['total_fn']),
369
+ 'precision': float(metrics['overall_metrics']['precision']),
370
+ 'recall': float(metrics['overall_metrics']['recall']),
371
+ 'f1_score': float(metrics['overall_metrics']['f1_score']),
372
+ 'accuracy': float(metrics['overall_metrics']['accuracy'])
373
+ }
374
+
375
+ # اضافه کردن متریک‌های category به result
376
+ for category, cat_metrics in metrics['category_metrics'].items():
377
+ result[f'{category.lower()}_precision'] = float(cat_metrics['precision'])
378
+ result[f'{category.lower()}_recall'] = float(cat_metrics['recall'])
379
+ result[f'{category.lower()}_f1'] = float(cat_metrics['f1_score'])
380
+ result[f'{category.lower()}_original_count'] = int(cat_metrics['original_count'])
381
+ result[f'{category.lower()}_anonymized_count'] = int(cat_metrics['anonymized_count'])
382
+
383
+ results.append(result)
384
+
385
+ except Exception as e:
386
+ logger.error(f"Error processing row {index}: {e}")
387
+ continue
388
+
389
+ if len(results) == 0:
390
+ logger.error("No valid results were generated")
391
+ return None
392
 
393
  return pd.DataFrame(results)
394
 
 
398
  if results_df is None or len(results_df) == 0:
399
  return {}
400
 
401
+ try:
402
+ summary = {
403
+ 'total_texts_processed': len(results_df),
404
+ 'average_metrics': {
405
+ 'precision': float(results_df['precision'].mean()),
406
+ 'recall': float(results_df['recall'].mean()),
407
+ 'f1_score': float(results_df['f1_score'].mean()),
408
+ 'accuracy': float(results_df['accuracy'].mean())
409
+ },
410
+ 'total_entities': {
411
+ 'original': int(results_df['total_original_entities'].sum()),
412
+ 'anonymized': int(results_df['total_anonymized_entities'].sum()),
413
+ 'tp': int(results_df['tp'].sum()),
414
+ 'fp': int(results_df['fp'].sum()),
415
+ 'fn': int(results_df['fn'].sum())
416
+ }
417
  }
418
+
419
+ # محاسبه متریک‌های کلی بر اساس مجموع
420
+ total_tp = summary['total_entities']['tp']
421
+ total_fp = summary['total_entities']['fp']
422
+ total_fn = summary['total_entities']['fn']
423
+ total_original = summary['total_entities']['original']
424
+
425
+ summary['overall_metrics'] = {
426
+ 'precision': total_tp / (total_tp + total_fp) if (total_tp + total_fp) > 0 else 0,
427
+ 'recall': total_tp / (total_tp + total_fn) if (total_tp + total_fn) > 0 else 0,
428
+ 'accuracy': total_tp / total_original if total_original > 0 else 0
429
+ }
430
+
431
+ # F1-Score کلی
432
+ overall_precision = summary['overall_metrics']['precision']
433
+ overall_recall = summary['overall_metrics']['recall']
434
+ summary['overall_metrics']['f1_score'] = 2 * (overall_precision * overall_recall) / (overall_precision + overall_recall) if (overall_precision + overall_recall) > 0 else 0
435
+
436
+ # آمار category-wise
437
+ category_columns = [col for col in results_df.columns if col.endswith('_precision')]
438
+ categories = [col.replace('_precision', '').upper() for col in category_columns]
439
+
440
+ category_summary = {}
441
+ for category in categories:
442
+ cat_lower = category.lower()
443
+ if f'{cat_lower}_precision' in results_df.columns:
444
+ # فیلتر کردن ردیف‌هایی که این category دارند
445
+ mask = results_df[f'{cat_lower}_original_count'] > 0
446
+ if mask.any():
447
+ category_summary[category] = {
448
+ 'count_texts_with_category': int(mask.sum()),
449
+ 'average_precision': float(results_df.loc[mask, f'{cat_lower}_precision'].mean()),
450
+ 'average_recall': float(results_df.loc[mask, f'{cat_lower}_recall'].mean()),
451
+ 'average_f1': float(results_df.loc[mask, f'{cat_lower}_f1'].mean()),
452
+ 'total_original': int(results_df[f'{cat_lower}_original_count'].sum()),
453
+ 'total_anonymized': int(results_df[f'{cat_lower}_anonymized_count'].sum())
454
+ }
455
+
456
+ summary['category_summary'] = category_summary
457
+
458
+ # تبدیل همه مقادیر به serializable types
459
+ summary = convert_to_serializable(summary)
460
+
461
+ return summary
462
+
463
+ except Exception as e:
464
+ logger.error(f"Error generating summary report: {e}")
465
+ return {'error': str(e)}
466
 
467
  # ===== رابط گرافیکی =====
468
  def create_sample_csv():
469
  """ایجاد فایل نمونه CSV برای تست"""
470
  sample_data = [
471
  {
472
+ 'original_text': 'مجمع عمومی عادی سالیانه شرکت پتروشیمی کارون در بندر ماهشهر برگزار شد.',
473
+ 'anonymized_text': 'مجمع عمومی عادی سالیانه company_001 در location_001 برگزار شد.'
474
  },
475
  {
476
+ 'original_text': 'آقای احمد محمدی مدیرعامل شرکت با شماره تماس 09123456789 و ایمیل ahmad@company.com قرارداد امضا کرد.',
477
+ 'anonymized_text': 'person_001 مدیرعامل شرکت با شماره تماس phone_001 و ایمیل email_001 قرارداد امضا کرد.'
478
  },
479
  {
480
+ 'original_text': 'بانک ملی ایران مبلغ 500 میلیون تومان به حساب 123-456-789012 واریز کرد.',
481
+ 'anonymized_text': 'company_001 مبلغ amount_001 به حساب account_001 واریز کرد.'
482
  }
483
  ]
484
 
485
  df = pd.DataFrame(sample_data)
486
+ sample_file_path = 'sample_benchmark_data.csv'
487
+ df.to_csv(sample_file_path, index=False, encoding='utf-8-sig')
488
+
489
+ # همچنین یک فایل نمونه با نام فارسی ایجاد کنیم
490
+ df.to_csv('نمونه_benchmark.csv', index=False, encoding='utf-8-sig')
491
+
492
+ return f"فایل‌های نمونه ایجاد شدند: {sample_file_path} و نمونه_benchmark.csv"
493
 
494
  def process_csv_file(file):
495
  """پردازش فایل CSV آپلود شده"""
 
497
  return "لطفاً فایل CSV را آپلود کنید.", None, None
498
 
499
  try:
500
+ logger.info(f"Processing file: {file.name}")
501
+
502
+ # بررسی وجود فایل
503
+ if not os.path.exists(file.name):
504
+ return "فایل آپلود شده یافت نشد.", None, None
505
+
506
  # خواندن فایل آپلود شده
507
+ try:
508
+ df = pd.read_csv(file.name, encoding='utf-8')
509
+ except UnicodeDecodeError:
510
+ df = pd.read_csv(file.name, encoding='utf-8-sig')
511
+ except Exception as e:
512
+ return f"خطا در خواندن فایل: {str(e)}", None, None
513
+
514
+ logger.info(f"CSV loaded successfully. Shape: {df.shape}, Columns: {df.columns.tolist()}")
515
 
516
  # بررسی ستون‌ها
517
  if 'original_text' not in df.columns or 'anonymized_text' not in df.columns:
518
+ return f"فایل CSV باید شامل ستون‌های 'original_text' و 'anonymized_text' باشد. ستون‌های موجود: {df.columns.tolist()}", None, None
519
 
520
  # اجرای benchmark
521
  benchmark = AnonymizationBenchmark()
522
  results_df = benchmark.benchmark_from_csv(file.name)
523
 
524
+ if results_df is None or len(results_df) == 0:
525
+ return "خطا در پردازش فایل CSV یا هیچ نتیجه معتبری تولید نشد!", None, None
526
+
527
+ logger.info(f"Benchmark completed. Results shape: {results_df.shape}")
528
 
529
  # تولید گزارش خلاصه
530
  summary = benchmark.generate_summary_report(results_df)
531
 
532
+ if 'error' in summary:
533
+ return f"خطا در تولید گزارش: {summary['error']}", None, None
534
+
535
  # آماده‌سازی نتایج برای نمایش
536
  metrics_text = f"""
537
  === نتایج کلی Benchmark ===
538
 
539
+ تعداد متون پردازش شده: {summary.get('total_texts_processed', 0)}
540
 
541
+ === متریک‌های کلی (بر اساس مجموع) ===
542
+ • Precision: {summary.get('overall_metrics', {}).get('precision', 0):.4f}
543
+ • Recall: {summary.get('overall_metrics', {}).get('recall', 0):.4f}
544
+ • F1-Score: {summary.get('overall_metrics', {}).get('f1_score', 0):.4f}
545
+ • Accuracy: {summary.get('overall_metrics', {}).get('accuracy', 0):.4f}
546
 
547
  === آمار کلی Entities ===
548
+ • تعداد کل Entities اصلی: {summary.get('total_entities', {}).get('original', 0)}
549
+ • تعداد کل Entities ناشناس‌سازی شده: {summary.get('total_entities', {}).get('anonymized', 0)}
550
+ • True Positives: {summary.get('total_entities', {}).get('tp', 0)}
551
+ • False Positives: {summary.get('total_entities', {}).get('fp', 0)}
552
+ • False Negatives: {summary.get('total_entities', {}).get('fn', 0)}
553
 
554
  === متریک‌های میانگین ===
555
+ • میانگین Precision: {summary.get('average_metrics', {}).get('precision', 0):.4f}
556
+ • میانگین Recall: {summary.get('average_metrics', {}).get('recall', 0):.4f}
557
+ • میانگین F1-Score: {summary.get('average_metrics', {}).get('f1_score', 0):.4f}
558
+ • میانگین Accuracy: {summary.get('average_metrics', {}).get('accuracy', 0):.4f}
559
  """
560
 
561
  # اضافه کردن آمار دسته‌بندی‌ها
562
+ if 'category_summary' in summary and summary['category_summary']:
563
  metrics_text += "\n=== آمار دسته‌بندی‌ها ===\n"
564
  for category, stats in summary['category_summary'].items():
565
  metrics_text += f"""
566
  {category}:
567
+ • تعداد متون دارای این دسته: {stats.get('count_texts_with_category', 0)}
568
+ • میانگین Precision: {stats.get('average_precision', 0):.4f}
569
+ • میانگین Recall: {stats.get('average_recall', 0):.4f}
570
+ • میانگین F1-Score: {stats.get('average_f1', 0):.4f}
571
+ • کل Entities اصلی: {stats.get('total_original', 0)}
572
+ • کل Entities ناشناس‌سازی شده: {stats.get('total_anonymized', 0)}
573
  """
574
+ else:
575
+ metrics_text += "\n=== آمار دسته‌بندی‌ها ===\nهیچ دسته‌ای یافت نشد.\n"
576
 
577
  # ذخیره گزارش‌ها
578
+ try:
579
+ results_df.to_csv("benchmark_results_detailed.csv", index=False, encoding='utf-8-sig')
580
+ with open("benchmark_results_summary.json", 'w', encoding='utf-8') as f:
581
+ json.dump(summary, f, ensure_ascii=False, indent=2)
582
+ logger.info("Results saved successfully")
583
+ except Exception as e:
584
+ logger.error(f"Error saving results: {e}")
585
+
586
+ # انتخاب ستون‌های مهم برای نمایش
587
+ display_columns = ['row_id', 'precision', 'recall', 'f1_score', 'accuracy', 'total_original_entities', 'total_anonymized_entities']
588
+ display_df = results_df[display_columns] if all(col in results_df.columns for col in display_columns) else results_df
589
 
590
  return (
591
  metrics_text,
592
+ display_df,
593
  summary
594
  )
595
 
596
  except Exception as e:
597
+ logger.error(f"Unexpected error in process_csv_file: {e}")
598
+ return f"خطای غیرمنتظره در پردازش: {str(e)}", None, None
599
 
600
  def download_results():
601
  """دانلود فایل نتایج"""
 
611
 
612
  gr.HTML("""
613
  <h1 style='text-align: center; color: #2E86AB; margin-bottom: 30px;'>
614
+ 📊 سیستم ارزیابی Benchmark ناشناس‌سازی - ورژن بهبود یافته
615
  </h1>
616
  """)
617
 
 
622
  <div style='background: #f0f8ff; padding: 15px; border-radius: 10px; margin-bottom: 15px;'>
623
  <b>فرمت فایل CSV:</b><br>
624
  • ستون اول: <code>original_text</code> (متن اصلی)<br>
625
+ • ستون دوم: <code>anonymized_text</code> (متن ناشناس‌سازی شده)<br>
626
+ • انکودینگ: UTF-8 (برای متن فارسی)
627
  </div>
628
  """)
629
 
 
633
  file_count="single"
634
  )
635
 
636
+ with gr.Row():
637
+ benchmark_btn = gr.Button("🚀 شروع Benchmark", variant="primary")
638
+ sample_btn = gr.Button("📄 ایجاد فایل نمونه", variant="secondary")
639
 
640
  with gr.Row():
641
  with gr.Column():
 
643
 
644
  metrics_output = gr.Textbox(
645
  label="متریک‌های کلی",
646
+ lines=30,
647
+ max_lines=35,
648
  interactive=False
649
  )
650
 
 
654
 
655
  results_table = gr.Dataframe(
656
  label="نتایج هر متن",
657
+ interactive=False,
658
+ wrap=True
659
  )
660
 
661
  with gr.Row():
 
663
  download_btn = gr.Button("💾 دانلود نتایج کامل", variant="secondary")
664
  download_file = gr.File(label="فایل نتایج", visible=False)
665
 
666
+ with gr.Row():
667
+ with gr.Column():
668
+ gr.HTML("""
669
+ <div style='background: #fff8dc; padding: 15px; border-radius: 10px; margin-top: 15px;'>
670
+ <h4>🔍 راهنمای استفاده:</h4>
671
+ <ol>
672
+ <li>ابتدا با دکمه "ایجاد فایل نمونه" یک فایل تست ایجاد کنید</li>
673
+ <li>فایل CSV خود را آپلود کنید (حتماً شامل ستون‌های original_text و anonymized_text باشد)</li>
674
+ <li>روی "شروع Benchmark" کلیک کنید</li>
675
+ <li>نتایج را در بخش‌های بالا مشاهده کنید</li>
676
+ <li>در صورت نیاز فایل کامل نتایج را دانلود کنید</li>
677
+ </ol>
678
+ </div>
679
+ """)
680
+
681
  # Event handlers
682
+ def handle_benchmark_click(file):
683
+ if file is None:
684
+ return "لطفاً ابتدا فایل CSV را آپلود کنید.", None, gr.update()
685
+ return process_csv_file(file)
686
+
687
+ def handle_sample_creation():
688
+ result = create_sample_csv()
689
+ return gr.update(value=result, visible=True)
690
+
691
  benchmark_btn.click(
692
+ fn=handle_benchmark_click,
693
  inputs=[file_input],
694
  outputs=[metrics_output, results_table, gr.State()]
695
  )
696
 
697
  sample_btn.click(
698
+ fn=handle_sample_creation,
699
  outputs=[gr.Textbox(visible=False)]
700
  )
701
 
 
713
 
714
  demo = main()
715
  if __name__ == "__main__":
716
+ port = int(os.getenv("PORT", "7860"))
717
  demo.launch(
718
  share=False,
719
  server_name="0.0.0.0",