adunare generala
This commit is contained in:
324
scripts/verificare_voturi_AG.py
Normal file
324
scripts/verificare_voturi_AG.py
Normal 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()
|
||||
Reference in New Issue
Block a user