soumalya-iitj commited on
Commit
bc31a5a
Β·
verified Β·
1 Parent(s): d37b74e

Update src/app.py

Browse files
Files changed (1) hide show
  1. src/app.py +224 -28
src/app.py CHANGED
@@ -3,11 +3,18 @@ import pandas as pd
3
  import matplotlib.pyplot as plt
4
  import seaborn as sns
5
  import math
 
 
 
 
6
 
7
  # Initialize session state
8
  if "voters" not in st.session_state:
9
  st.session_state.voters = []
10
 
 
 
 
11
  st.title("🎯 Strategy-Proof Ranked Voting System")
12
 
13
  # 1. Alternatives and scoring vector
@@ -47,25 +54,35 @@ if st.session_state.voters:
47
  st.dataframe(df_votes)
48
 
49
  # --- Function to detect manipulation ---
 
 
50
  def detect_manipulation(voters, alternatives, scoring_vector):
51
  total_profiles = math.factorial(len(alternatives)) ** len(voters)
52
  manipulable_cases = 0
53
  manipulable_info = []
54
 
 
 
 
 
 
55
  original_scores = {alt: 0 for alt in alternatives}
56
  for v in voters:
57
  for i, alt in enumerate(v["ranking"]):
58
  original_scores[alt] += scoring_vector[i]
 
59
  original_winner = max(original_scores, key=original_scores.get)
60
 
61
  for voter in voters:
62
  original_ranking = voter["ranking"]
 
 
63
  if original_ranking[0] == original_winner:
64
- continue # already supports winner
65
 
66
  for alt in alternatives:
67
  if alt == original_ranking[0]:
68
- continue
69
 
70
  new_ranking = original_ranking.copy()
71
  if alt in new_ranking:
@@ -78,17 +95,117 @@ def detect_manipulation(voters, alternatives, scoring_vector):
78
  for i, a in enumerate(rank_list):
79
  simulated_scores[a] += scoring_vector[i]
80
 
 
81
  new_winner = max(simulated_scores, key=simulated_scores.get)
82
- if new_winner != original_winner:
 
 
 
 
 
 
 
 
 
83
  manipulable_cases += 1
84
  manipulable_info.append({
85
  "voter": voter["name"],
86
- "original": original_ranking,
87
- "manipulated": new_ranking
 
 
 
 
88
  })
89
  break
90
 
91
- return manipulable_cases, total_profiles, manipulable_info
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
  # 4. Compute results
94
  if st.button("πŸ“Š Compute Result") and st.session_state.voters:
@@ -143,38 +260,117 @@ if st.button("πŸ“Š Compute Result") and st.session_state.voters:
143
  else:
144
  st.success(f"πŸ… Winner: {top_alts[0]}")
145
 
 
146
  # 5. Manipulation Detection
147
  st.subheader("πŸ•΅οΈ Manipulation Detection")
148
- m_cases, t_cases, m_profiles = detect_manipulation(st.session_state.voters, alternatives, scoring_vector)
149
- st.write(f"Potentially manipulable profiles: **{m_cases} / {t_cases}**")
 
 
 
 
150
 
151
  if m_profiles:
152
  for item in m_profiles:
153
- st.markdown(f"""<div style='margin-bottom:10px;'>
154
- <b>{item['voter']}</b> can manipulate their vote:</div>""", unsafe_allow_html=True)
155
-
156
- original = pd.DataFrame(
157
- [scoring_vector[:len(item['original'])]],
158
- columns=item['original'],
159
- index=["Original"]
160
- )
161
- manipulated = pd.DataFrame(
162
- [scoring_vector[:len(item['manipulated'])]],
163
- columns=item['manipulated'],
164
- index=["Manipulated"]
165
- )
166
-
167
- combined = pd.concat([original, manipulated])
168
-
169
- st.markdown("##### πŸ”„ Original vs Manipulated Preference (Score View)")
170
- fig, ax = plt.subplots(figsize=(8, 2))
171
- sns.heatmap(combined, annot=True, cmap="coolwarm", cbar=False, ax=ax)
172
- st.pyplot(fig)
173
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  else:
175
  st.success("βœ… No manipulable profiles detected.")
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  # Reset
178
  if st.button("πŸ”„ Reset Voters"):
179
  st.session_state.voters = []
 
180
  st.rerun()
 
3
  import matplotlib.pyplot as plt
4
  import seaborn as sns
5
  import math
6
+ from copy import deepcopy
7
+ from itertools import permutations, product
8
+ from collections import defaultdict
9
+
10
 
11
  # Initialize session state
12
  if "voters" not in st.session_state:
13
  st.session_state.voters = []
14
 
15
+ if "scoring_history" not in st.session_state:
16
+ st.session_state.scoring_history = [] # List of tuples: (label, vector, manipulability %)
17
+
18
  st.title("🎯 Strategy-Proof Ranked Voting System")
19
 
20
  # 1. Alternatives and scoring vector
 
54
  st.dataframe(df_votes)
55
 
56
  # --- Function to detect manipulation ---
57
+ import math
58
+
59
  def detect_manipulation(voters, alternatives, scoring_vector):
60
  total_profiles = math.factorial(len(alternatives)) ** len(voters)
61
  manipulable_cases = 0
62
  manipulable_info = []
63
 
64
+ def get_rank(score_dict):
65
+ sorted_alts = sorted(score_dict.items(), key=lambda x: -x[1])
66
+ return {alt: rank + 1 for rank, (alt, _) in enumerate(sorted_alts)}
67
+
68
+ # Original overall ranking
69
  original_scores = {alt: 0 for alt in alternatives}
70
  for v in voters:
71
  for i, alt in enumerate(v["ranking"]):
72
  original_scores[alt] += scoring_vector[i]
73
+ original_ranks = get_rank(original_scores)
74
  original_winner = max(original_scores, key=original_scores.get)
75
 
76
  for voter in voters:
77
  original_ranking = voter["ranking"]
78
+
79
+ # Skip if voter's top choice is already the winner
80
  if original_ranking[0] == original_winner:
81
+ continue
82
 
83
  for alt in alternatives:
84
  if alt == original_ranking[0]:
85
+ continue # skip if same as current top
86
 
87
  new_ranking = original_ranking.copy()
88
  if alt in new_ranking:
 
95
  for i, a in enumerate(rank_list):
96
  simulated_scores[a] += scoring_vector[i]
97
 
98
+ simulated_ranks = get_rank(simulated_scores)
99
  new_winner = max(simulated_scores, key=simulated_scores.get)
100
+
101
+ # Manipulation is beneficial ONLY IF new winner is ranked *higher* than original winner
102
+ if original_ranking.index(new_winner) < original_ranking.index(original_winner):
103
+ rank_improvements = []
104
+ for a in alternatives:
105
+ old_rank = original_ranks[a]
106
+ new_rank = simulated_ranks[a]
107
+ if new_rank < old_rank:
108
+ rank_improvements.append(f"{a}: {old_rank} β†’ {new_rank}")
109
+
110
  manipulable_cases += 1
111
  manipulable_info.append({
112
  "voter": voter["name"],
113
+ "original_ranking": original_ranking,
114
+ "manipulated_ranking": new_ranking,
115
+ "original_winner": original_winner,
116
+ "new_winner": new_winner,
117
+ "rank_improvements": rank_improvements,
118
+ "simulated_ranks": simulated_ranks # βœ… Add this for your DataFrame logic
119
  })
120
  break
121
 
122
+ return manipulable_cases, len(voters), manipulable_info
123
+
124
+ def get_winners(scores):
125
+ max_score = max(scores.values())
126
+ return [alt for alt, score in scores.items() if score == max_score]
127
+
128
+ # --- Function to count manipulable profile ---
129
+ from itertools import permutations, product
130
+ from collections import defaultdict
131
+
132
+ def count_manipulable_profiles(m, n, scoring_vector):
133
+ alternatives = [f"M{i+1}" for i in range(m)]
134
+ all_rankings = list(permutations(alternatives))
135
+ total_profiles = len(all_rankings) ** n
136
+ manipulable_count = 0
137
+
138
+ def get_rank(scores):
139
+ # Returns a ranking dictionary based on scores (highest score first)
140
+ sorted_items = sorted(scores.items(), key=lambda x: -x[1])
141
+ return {alt: i + 1 for i, (alt, _) in enumerate(sorted_items)}
142
+
143
+ # Iterate over all possible profiles
144
+ for profile in product(all_rankings, repeat=n):
145
+ # Calculate the original scores for this profile
146
+ original_scores = defaultdict(int)
147
+ for ranking in profile:
148
+ for i, alt in enumerate(ranking):
149
+ original_scores[alt] += scoring_vector[i]
150
+
151
+ # Identify the original winner (the alternative with the highest score)
152
+ original_winner = max(original_scores, key=original_scores.get)
153
+
154
+ # Calculate the original rank order
155
+ original_ranks = get_rank(original_scores)
156
+
157
+ # Check if the profile can be manipulated by at least one voter
158
+ profile_manipulated = False
159
+
160
+ for voter_idx, ranking in enumerate(profile):
161
+ # If the original winner is already the first choice for this voter, no manipulation is possible
162
+ if ranking[0] == original_winner:
163
+ continue
164
+
165
+ # Find the alternatives this voter prefers over the original winner
166
+ preferred_alts = []
167
+ for alt in ranking:
168
+ if alt == original_winner:
169
+ break
170
+ preferred_alts.append(alt)
171
+
172
+ # Try manipulating the vote by promoting each preferred alternative
173
+ for alt in preferred_alts:
174
+ # Create a manipulated vote where 'alt' is ranked first
175
+ manipulated = list(ranking)
176
+ manipulated.remove(alt)
177
+ manipulated.insert(0, alt)
178
+
179
+ # Recalculate the scores with the manipulated vote
180
+ new_scores = original_scores.copy()
181
+ # Subtract the original vote's scores
182
+ for pos, a in enumerate(ranking):
183
+ new_scores[a] -= scoring_vector[pos]
184
+ # Add the manipulated vote's scores
185
+ for pos, a in enumerate(manipulated):
186
+ new_scores[a] += scoring_vector[pos]
187
+
188
+ # Determine the new winner after manipulation
189
+ new_winner = max(new_scores, key=new_scores.get)
190
+
191
+ # Check if the new winner is one of the preferred alternatives and is better than the original winner
192
+ if new_winner in preferred_alts and new_scores[new_winner] > new_scores[original_winner]:
193
+ manipulable_count += 1
194
+ profile_manipulated = True
195
+ break
196
+
197
+ if profile_manipulated:
198
+ break
199
+
200
+ # Count the profile if at least one voter can manipulate it
201
+ if profile_manipulated:
202
+ manipulable_count += 1
203
+
204
+ # Return manipulable count, total profiles, and manipulability percentage
205
+ manipulability = (manipulable_count / total_profiles) * 100
206
+ return manipulable_count, total_profiles, manipulability
207
+
208
+
209
 
210
  # 4. Compute results
211
  if st.button("πŸ“Š Compute Result") and st.session_state.voters:
 
260
  else:
261
  st.success(f"πŸ… Winner: {top_alts[0]}")
262
 
263
+
264
  # 5. Manipulation Detection
265
  st.subheader("πŸ•΅οΈ Manipulation Detection")
266
+
267
+ m_cases, t_cases, m_profiles = detect_manipulation(
268
+ st.session_state.voters, alternatives, scoring_vector
269
+ )
270
+
271
+ st.markdown(f"πŸ“Š **Voters who can potentially improve their outcome:** `{m_cases} / {t_cases}`")
272
 
273
  if m_profiles:
274
  for item in m_profiles:
275
+ # Corrected rank improvements with accurate old and new positions
276
+ rank_changes = []
277
+ original_ranking = item['original_ranking']
278
+ manipulated_ranking = item['manipulated_ranking']
279
+
280
+ for imp in item['rank_improvements']:
281
+ alt_name, _ = imp.split(':') # Only keep the alternative name, ignore the rank change part
282
+ alt_name = alt_name.strip()
 
 
 
 
 
 
 
 
 
 
 
 
283
 
284
+ # Get original and new rank positions (1-based)
285
+ old = original_ranking.index(alt_name) + 1
286
+ new = manipulated_ranking.index(alt_name) + 1
287
+ benefit = old - new
288
+
289
+ if benefit > 0:
290
+ rank_changes.append(f"βœ… {alt_name}: Rank {old} β†’ {new} (↑ {benefit})")
291
+ elif benefit < 0:
292
+ rank_changes.append(f"⚠️ {alt_name}: Rank {old} β†’ {new} (↓ {abs(benefit)})")
293
+ else:
294
+ rank_changes.append(f"βž– {alt_name}: Rank {old} β†’ {new} (no change)")
295
+
296
+
297
+ # Simulated overall rank table
298
+ sim_ranks_df = pd.DataFrame.from_dict(item["simulated_ranks"], orient="index", columns=["Rank"])
299
+ sim_ranks_df.index.name = "Alternative"
300
+ sim_ranks_df = sim_ranks_df.sort_values("Rank")
301
+
302
+ # Display voter manipulation info
303
+ st.markdown(f"""
304
+ <div style='margin-bottom:15px; border-left: 3px solid #444; padding-left: 10px;'>
305
+ πŸ” <b>{item['voter']}</b> can manipulate:
306
+ <ul>
307
+ <li><b>Original Ranking:</b> {', '.join(original_ranking)}</li>
308
+ <li><b>Manipulated Ranking:</b> {', '.join(manipulated_ranking)}</li>
309
+ <li><b>Original Winner:</b> {item['original_winner']}</li>
310
+ <li><b>New Winner after manipulation:</b> {item['new_winner']}</li>
311
+ <li><b>Rank Improvements:</b>
312
+ <ul>
313
+ {''.join(f"<li>{rc}</li>" for rc in rank_changes)}
314
+ </ul>
315
+ </li>
316
+ </ul>
317
+ </div>
318
+ """, unsafe_allow_html=True)
319
+
320
+ st.markdown("**πŸ“Œ Simulated Ranks after Manipulation:**")
321
+ st.dataframe(sim_ranks_df)
322
  else:
323
  st.success("βœ… No manipulable profiles detected.")
324
 
325
+
326
+
327
+ # 6. Global Manipulability (All Possible Profiles)
328
+ st.subheader("πŸ“š Global Profile Manipulability")
329
+ m = len(alternatives)
330
+ n = len(st.session_state.voters)
331
+
332
+ with st.spinner("Computing over all possible profiles..."):
333
+ manipulable, total,manipulability_perc = count_manipulable_profiles(m, n, scoring_vector)
334
+ st.success(f"Out of {total} total profiles, at least one voter can manipulate in {manipulable} profiles.")
335
+ st.metric("Manipulable Profiles", f"{manipulable} / {total}", delta=f"{round((manipulable/total)*100, 2)}%")
336
+
337
+ current_label = f"Custom {scoring_vector}"
338
+ # After detect_manipulation()
339
+ #m_cases, t_cases, m_profiles = detect_manipulation(st.session_state.voters, alternatives, scoring_vector)
340
+
341
+ # Accurate manipulability % for the current scoring vector
342
+ global_m, global_t, manipulability = count_manipulable_profiles(len(alternatives), len(st.session_state.voters), scoring_vector)
343
+ #manipulability = (global_m / global_t) * 100
344
+
345
+ # Avoid duplicates
346
+ if not any(vector == scoring_vector for _, vector, _, _, _ in st.session_state.scoring_history):
347
+ st.session_state.scoring_history.append((f"Custom {scoring_vector}", scoring_vector.copy(), manipulability, len(alternatives), len(st.session_state.voters)))
348
+
349
+
350
+ # 6. Accumulated Scoring Vector Comparison
351
+ if st.session_state.scoring_history:
352
+ df_hist = pd.DataFrame(
353
+ [(label, m, n, percent) for label, _, percent, m, n in st.session_state.scoring_history],
354
+ columns=["Scoring Vector", "Alternatives", "Voters", "Manipulable %"]
355
+ )
356
+
357
+ st.subheader("πŸ“Š Manipulability of All Used Scoring Vectors")
358
+ fig_hist, ax_hist = plt.subplots(figsize=(10, 6))
359
+ sns.barplot(data=df_hist, x="Manipulable %", y="Scoring Vector", palette="Spectral", ax=ax_hist)
360
+ ax_hist.set_xlim(0, 100)
361
+ ax_hist.set_xlabel("Percentage of Manipulable Profiles")
362
+ ax_hist.set_title("Manipulability by Scoring Vector")
363
+
364
+ # Annotate each bar with m, n and manipulability %
365
+ for i, row in df_hist.iterrows():
366
+ label = f"m={row['Alternatives']}, n={row['Voters']} | {row['Manipulable %']:.2f}%"
367
+ ax_hist.text(row['Manipulable %'] + 1, i, label, va='center', fontsize=9, color='black')
368
+
369
+ st.pyplot(fig_hist)
370
+
371
+
372
  # Reset
373
  if st.button("πŸ”„ Reset Voters"):
374
  st.session_state.voters = []
375
+ st.session_state.scoring_history = []
376
  st.rerun()