adunare generala

This commit is contained in:
Marius
2025-12-20 22:23:11 +02:00
parent 90d77704d6
commit 91b9e08e9d
4 changed files with 697 additions and 0 deletions

View File

@@ -0,0 +1,324 @@
#!/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()