#!/usr/bin/env python3 """ Script pentru verificare voturi AG bleuMarin Constanța 25.11.2025 - Detectează duplicate (păstrează primul vot) - Verifică dacă votanții sunt în lista de membri cu drept de vot (fuzzy matching) - Generează raport unitar cu totaluri pe propuneri """ import pandas as pd from difflib import SequenceMatcher import unicodedata import re from pathlib import Path # Configurare căi INPUT_DIR = Path(__file__).parent.parent / "input" OUTPUT_DIR = Path(__file__).parent.parent / "output" # Fișiere de intrare MEMBRI_FILE = INPUT_DIR / "membri_promisiune.csv" VOTURI_PRINCIPAL_FILE = INPUT_DIR / "AG bleuMarin Constanța 25.11.2025 (Responses) - Form Responses 1.csv" VOTURI_SUPLIMENTAR_FILE = INPUT_DIR / "AG bleuMarin Constanța 25.11.2025 Suplimentar (Responses) - Form Responses 1.csv" # Prag pentru fuzzy matching (60% pentru a prinde variații de nume) FUZZY_THRESHOLD = 0.60 def normalize_name(name): """Normalizează numele pentru comparare""" if pd.isna(name): return "" # Convertește la lowercase name = str(name).lower().strip() # Elimină diacritice name = unicodedata.normalize('NFKD', name) name = ''.join(c for c in name if not unicodedata.combining(c)) # Elimină caractere speciale (păstrează doar litere și spații) name = re.sub(r'[^a-z\s]', '', name) # Elimină spații multiple name = re.sub(r'\s+', ' ', name).strip() return name def fuzzy_match(name1, name2): """Calculează similaritatea între două nume""" n1 = normalize_name(name1) n2 = normalize_name(name2) if not n1 or not n2: return 0.0 # Încearcă potrivire directă if n1 == n2: return 1.0 words1 = set(n1.split()) words2 = set(n2.split()) # Dacă toate cuvintele din numele scurt sunt în numele lung (ex: "Stancu Matei" in "Stancu Matei Stefan") if words1 and words2: if words1.issubset(words2) or words2.issubset(words1): return 0.95 # Verifică dacă au cel puțin 2 cuvinte comune (ex: "Bardi Antonio Alexandru" și "Bardi Alexandru Antonio") common = words1.intersection(words2) if len(common) >= 2: # Verifică dacă numele de familie (primul cuvânt din fiecare) se potrivește list1 = n1.split() list2 = n2.split() if list1[0] == list2[0]: return 0.92 # Același nume familie + prenume comun return 0.85 # Cuvinte comune dar familie diferit # Sortează cuvintele și compară (pentru nume în ordine diferită) sorted1 = ' '.join(sorted(n1.split())) sorted2 = ' '.join(sorted(n2.split())) if sorted1 == sorted2: return 0.98 # Fuzzy matching cu SequenceMatcher base_score = SequenceMatcher(None, n1, n2).ratio() # Bonus dacă cel puțin un cuvânt se potrivește exact if words1.intersection(words2): base_score = min(base_score + 0.15, 0.95) return base_score def find_best_match(name, membri_names): """Găsește cel mai bun match pentru un nume în lista de membri""" best_score = 0 best_match = None for membru in membri_names: score = fuzzy_match(name, membru) if score > best_score: best_score = score best_match = membru return best_match, best_score def remove_duplicates(df, email_col='Email Address', timestamp_col='Timestamp'): """Elimină duplicatele păstrând primul vot (cel mai vechi)""" # Sortează după timestamp df_sorted = df.sort_values(by=timestamp_col) # Găsește duplicatele duplicates = df_sorted[df_sorted.duplicated(subset=[email_col], keep='first')] # Păstrează doar primul vot pentru fiecare email df_unique = df_sorted.drop_duplicates(subset=[email_col], keep='first') return df_unique, duplicates def main(): print("=" * 70) print("Verificare voturi AG bleuMarin Constanța 25.11.2025") print("=" * 70) # Încarcă lista de membri print("\n[1] Încărcare date...") membri_df = pd.read_csv(MEMBRI_FILE) membri_names = membri_df['Nume'].dropna().tolist() print(f" - Membri cu drept de vot: {len(membri_names)}") # Încarcă voturile principale (fără skiprows, headerul este pe linia 1) voturi_principal_df = pd.read_csv(VOTURI_PRINCIPAL_FILE) print(f" - Voturi formular principal: {len(voturi_principal_df)}") # Încarcă voturile suplimentare voturi_suplimentar_df = pd.read_csv(VOTURI_SUPLIMENTAR_FILE) print(f" - Voturi formular suplimentar: {len(voturi_suplimentar_df)}") # [2] Elimină duplicatele print("\n[2] Detectare duplicate (pe email)...") voturi_principal_clean, duplicates_principal = remove_duplicates(voturi_principal_df) voturi_suplimentar_clean, duplicates_suplimentar = remove_duplicates(voturi_suplimentar_df) print(f" - Duplicate formular principal: {len(duplicates_principal)}") if len(duplicates_principal) > 0: for _, row in duplicates_principal.iterrows(): print(f" * {row['Nume și prenume']} ({row['Email Address']}) - {row['Timestamp']}") print(f" - Duplicate formular suplimentar: {len(duplicates_suplimentar)}") if len(duplicates_suplimentar) > 0: for _, row in duplicates_suplimentar.iterrows(): print(f" * {row['Nume și prenume']} ({row['Email Address']}) - {row['Timestamp']}") # Salvează duplicatele all_duplicates = [] for _, row in duplicates_principal.iterrows(): all_duplicates.append({ 'Formular': 'Principal', 'Nume': row['Nume și prenume'], 'Email': row['Email Address'], 'Timestamp': row['Timestamp'] }) for _, row in duplicates_suplimentar.iterrows(): all_duplicates.append({ 'Formular': 'Suplimentar', 'Nume': row['Nume și prenume'], 'Email': row['Email Address'], 'Timestamp': row['Timestamp'] }) duplicates_df = pd.DataFrame(all_duplicates) duplicates_df.to_csv(OUTPUT_DIR / "raport_duplicate.csv", index=False, encoding='utf-8-sig') print(f" -> Salvat: output/raport_duplicate.csv") # [3] Verificare drept de vot cu fuzzy matching print("\n[3] Verificare drept de vot (fuzzy matching, prag {:.0%})...".format(FUZZY_THRESHOLD)) # Combinăm votanții din ambele formulare votanti_principal = voturi_principal_clean[['Nume și prenume', 'Email Address']].copy() votanti_principal['Formular'] = 'Principal' votanti_suplimentar = voturi_suplimentar_clean[['Nume și prenume', 'Email Address']].copy() votanti_suplimentar['Formular'] = 'Suplimentar' all_votanti = pd.concat([votanti_principal, votanti_suplimentar]) all_votanti = all_votanti.drop_duplicates(subset=['Email Address']) neautorizati = [] autorizati_mapping = {} for _, row in all_votanti.iterrows(): nume = row['Nume și prenume'] email = row['Email Address'] best_match, score = find_best_match(nume, membri_names) if score >= FUZZY_THRESHOLD: autorizati_mapping[email] = { 'nume_votat': nume, 'nume_membru': best_match, 'score': score } else: neautorizati.append({ 'Nume votat': nume, 'Email': email, 'Cel mai apropiat match': best_match, 'Scor similaritate': f"{score:.2%}" }) print(f" - Votanți autorizați: {len(autorizati_mapping)}") print(f" - Votanți NEAUTORIZAȚI: {len(neautorizati)}") if neautorizati: print("\n Votanți fără drept de vot găsit în lista de membri:") for v in neautorizati: print(f" * {v['Nume votat']} -> {v['Cel mai apropiat match']} ({v['Scor similaritate']})") neautorizati_df = pd.DataFrame(neautorizati) neautorizati_df.to_csv(OUTPUT_DIR / "raport_neautorizati.csv", index=False, encoding='utf-8-sig') print(f" -> Salvat: output/raport_neautorizati.csv") # Lista de email-uri neautorizate (pentru excludere din totaluri) emails_neautorizati = set(v['Email'] for v in neautorizati) # [4] Calcul totaluri print("\n[4] Calcul totaluri pe propuneri...") # Filtrăm doar voturile autorizate voturi_principal_valid = voturi_principal_clean[~voturi_principal_clean['Email Address'].isin(emails_neautorizati)] voturi_suplimentar_valid = voturi_suplimentar_clean[~voturi_suplimentar_clean['Email Address'].isin(emails_neautorizati)] print(f" - Voturi valide formular principal: {len(voturi_principal_valid)}") print(f" - Voturi valide formular suplimentar: {len(voturi_suplimentar_valid)}") # Găsim coloanele cu propuneri din formularul principal (cele care încep cu cifre) propuneri_cols = [] for col in voturi_principal_valid.columns: # Verifică dacă coloana începe cu un număr urmat de punct if re.match(r'^\d+\.', col): # Exclude coloanele Badge (12.x) if not col.startswith('12.'): propuneri_cols.append(col) print(f" - Propuneri găsite în formular principal: {len(propuneri_cols)}") # Calculăm totalurile totaluri = [] # Titluri scurte pentru propuneri titluri_scurte = { 1: "1. AG ONCR. Bugetul ONCR pe anul 2026", 2: "2. AG ONCR. Acordarea dreptului PJ pentru structurile locale", 3: "3. AG ONCR. Reglementarea transferurilor între Centre Locale", 4: "4. Reprezentant bleuMarin la AG Național (Maria Costache)", 5: "5. Cotizație: 250 lei (150 lei adulți/copii lideri, 175 lei frați)", 6: "6. Program Woodbadge: 20% participant, 80% centru local", 7: "7. Fund-raising patrule: 20% acțiuni sociale, 80% patrulă", 8: "8. Fund-raising membru: 20% acțiuni sociale, 80% membru", 9: "9. Sponsorizări membru: până la 20% pentru evenimente", 10: "10. Telefoane la activități - restricții", 11: "11. Fund-raising/caritate anunțate pe grupul de lideri" } # Propuneri din formularul principal for i, col in enumerate(propuneri_cols, 1): voturi = voturi_principal_valid[col].value_counts() da = voturi.get('Da', 0) nu = voturi.get('Nu', 0) abtinere = voturi.get('Abtinere', 0) total = da + nu + abtinere titlu = titluri_scurte.get(i, f"{i}. {col[:50]}...") totaluri.append({ 'Nr': i, 'Propunere': titlu, 'Da': da, 'Nu': nu, 'Abtinere': abtinere, 'Total': total }) # Propunerea din formularul suplimentar suplimentar_cols = [col for col in voturi_suplimentar_valid.columns if re.match(r'^\d+\.', col)] if suplimentar_cols: suplimentar_col = suplimentar_cols[0] voturi_supl = voturi_suplimentar_valid[suplimentar_col].value_counts() totaluri.append({ 'Nr': 12, 'Propunere': '12. AG ONCR. Înființare CL Sf. Voievod Ștefan cel Mare (desprindere)', 'Da': voturi_supl.get('Da', 0), 'Nu': voturi_supl.get('Nu', 0), 'Abtinere': voturi_supl.get('Abtinere', 0), 'Total': voturi_supl.get('Da', 0) + voturi_supl.get('Nu', 0) + voturi_supl.get('Abtinere', 0) }) # Salvează totalurile totaluri_df = pd.DataFrame(totaluri) totaluri_df.to_csv(OUTPUT_DIR / "raport_voturi_AG.csv", index=False, encoding='utf-8-sig') print(f" -> Salvat: output/raport_voturi_AG.csv") # [5] Afișare rezultate print("\n" + "=" * 70) print("REZULTATE FINALE - Totaluri voturi AG") print("=" * 70) for t in totaluri: print(f"\n{t['Propunere']}") print(f" Da: {t['Da']:3d} | Nu: {t['Nu']:3d} | Abținere: {t['Abtinere']:3d} | Total: {t['Total']:3d}") print("\n" + "=" * 70) print("SUMAR") print("=" * 70) print(f"Total membri cu drept de vot: {len(membri_names)}") print(f"Total voturi unice formular principal: {len(voturi_principal_clean)}") print(f"Total voturi unice formular suplimentar: {len(voturi_suplimentar_clean)}") print(f"Duplicate eliminate: {len(duplicates_principal) + len(duplicates_suplimentar)}") print(f"Votanți neautorizați: {len(neautorizati)}") print(f"Voturi valide formular principal: {len(voturi_principal_valid)}") print(f"Voturi valide formular suplimentar: {len(voturi_suplimentar_valid)}") if __name__ == "__main__": main()