DavidBeltran/data_visualization
0
1import marimo
2
3__generated_with = "0.24.0"
4app = marimo.App(width="medium")
5
6
7@app.cell
8def _():
9 import marimo as mo
10 import pandas as pd
11 import ast
12 from pathlib import Path
13 import matplotlib.pyplot as plt
14 import matplotlib.patches as patches
15 import numpy as np
16
17 gov_folder = Path("Collected_by_the_Government")
18 journ_folder = Path("Collected_by_the_Journalist")
19
20 def load_participations(folder, source_name):
21 disc = pd.read_csv(folder / "discussion_people_participations.csv")
22 plan = pd.read_csv(folder / "plan_people_participations.csv")
23 disc["record_type"], plan["record_type"] = "discussion", "plan"
24 disc["source"], plan["source"] = source_name, source_name
25 return pd.concat([disc, plan], ignore_index=True)
26
27 all_participations = pd.concat([
28 load_participations(gov_folder, "government"),
29 load_participations(journ_folder, "journalist"),
30 ], ignore_index=True)
31 return (
32 all_participations,
33 ast,
34 gov_folder,
35 journ_folder,
36 mo,
37 np,
38 patches,
39 pd,
40 plt,
41 )
42
43
44@app.cell
45def _(mo):
46 discussion_toggle = mo.ui.checkbox(label="Discussions", value=True)
47 plan_toggle = mo.ui.checkbox(label="Plans", value=True)
48 journalist_toggle = mo.ui.checkbox(label="Add journalist data on top of government")
49 return discussion_toggle, journalist_toggle, plan_toggle
50
51
52@app.cell
53def _(
54 all_participations,
55 ast,
56 discussion_toggle,
57 gov_folder,
58 journalist_toggle,
59 pd,
60 plan_toggle,
61):
62 people_roles = pd.read_csv(gov_folder / "people.csv").set_index("people_id")["role"].to_dict()
63
64 def build_seesaw_data(participations, include_discussions, include_plans, include_journalist):
65 record_types = []
66 if include_discussions:
67 record_types.append("discussion")
68 if include_plans:
69 record_types.append("plan")
70 if not record_types:
71 return pd.DataFrame(columns=["fishing", "tourism"]), pd.Series(dtype=int)
72
73 df = participations[participations["record_type"].isin(record_types)].copy()
74 sources = ["government", "journalist"] if include_journalist else ["government"]
75 df = df[df["source"].isin(sources)]
76 df = df.dropna(subset=["industry"])
77 df["industry_list"] = df["industry"].apply(ast.literal_eval)
78 df = df.explode("industry_list")
79 df["topic_side"] = df["industry_list"].map({
80 "tourism": "tourism", "small vessel": "fishing", "large vessel": "fishing",
81 })
82 df = df.dropna(subset=["topic_side"])
83
84 means = df.groupby(["people_id", "topic_side"])["sentiment"].mean().unstack()
85 counts = df.groupby("people_id").size()
86 return means, counts
87
88 seesaw_data, seesaw_counts = build_seesaw_data(
89 all_participations, discussion_toggle.value, plan_toggle.value, journalist_toggle.value,
90 )
91 return people_roles, seesaw_counts, seesaw_data
92
93
94@app.cell
95def _(np, patches, plt):
96 FISHING_COLOR = "#0F6E56"
97 TOURISM_COLOR = "#993C1D"
98 NEUTRAL = "#5F5E5A"
99 STAND_COLOR = "#888780"
100
101 def plot_scale(ax, name, role, tourism_score, fishing_score, n, n_max, score_range=(-1, 1)):
102 lo, hi = score_range
103 diff = tourism_score - fishing_score
104 angle = np.clip(diff / (hi - lo), -1, 1) * 14
105 scale = np.clip(0.55 + 0.55 * np.sqrt(n / n_max) if n_max > 0 else 0.7, 0.55, 1.05)
106
107 ax.set_xlim(-1.6, 1.6); ax.set_ylim(-1.35, 1.6)
108 ax.set_aspect("equal"); ax.axis("off")
109
110 ax.text(0.5, 1.14, name, transform=ax.transAxes, ha="center", fontsize=12,
111 fontweight="medium", color="#2C2C2A")
112 if role:
113 ax.text(0.5, 1.045, role, transform=ax.transAxes, ha="center", fontsize=9.5,
114 color="#888780")
115
116 ax.plot([-1.4, 1.4], [-1.25, -1.25], color="#D3D1C7", linewidth=1.5, zorder=0)
117
118 pivot_y = 1.0
119 ax.plot([0, 0], [-0.95, pivot_y], color=STAND_COLOR, linewidth=3, zorder=1, solid_capstyle="round")
120 ax.add_patch(patches.Polygon([[-0.18, -0.95], [0.18, -0.95], [0, -0.72]],
121 closed=True, facecolor=STAND_COLOR, zorder=1))
122
123 rad = np.radians(angle)
124 dx, dy = np.cos(rad), np.sin(rad)
125 arm = 1.05 * scale
126 beam_left = (-arm * dx, pivot_y + arm * dy)
127 beam_right = (arm * dx, pivot_y - arm * dy)
128 ax.plot([beam_left[0], beam_right[0]], [beam_left[1], beam_right[1]],
129 color=STAND_COLOR, linewidth=4, solid_capstyle="round", zorder=2)
130 ax.add_patch(patches.Circle((0, pivot_y), 0.05, color=NEUTRAL, zorder=3))
131
132 drop = 0.55
133 pan_w = 0.42 * scale
134
135 def draw_pan(beam_pt, color):
136 px, py = beam_pt
137 pan_y = py - drop
138 ax.plot([px, px - pan_w/2], [py, pan_y], color="#B4B2A9", linewidth=1, zorder=2)
139 ax.plot([px, px + pan_w/2], [py, pan_y], color="#B4B2A9", linewidth=1, zorder=2)
140 theta = np.linspace(np.pi, 2*np.pi, 30)
141 xs = px + (pan_w/2) * np.cos(theta)
142 ys = pan_y + (pan_w*0.275) * np.sin(theta)
143 xs = np.concatenate([xs, [px + pan_w/2, px - pan_w/2]])
144 ys = np.concatenate([ys, [pan_y, pan_y]])
145 ax.fill(xs, ys, color=color, alpha=0.85, zorder=2.5)
146 ax.plot([px - pan_w/2, px + pan_w/2], [pan_y, pan_y], color=color, linewidth=2, zorder=3)
147 return pan_y
148
149 pan_y_fishing = draw_pan(beam_left, FISHING_COLOR)
150 pan_y_tourism = draw_pan(beam_right, TOURISM_COLOR)
151
152 ax.text(beam_left[0], pan_y_fishing - 0.28, f"Fishing\n{fishing_score:.2f}",
153 ha="center", va="top", fontsize=9.5, color=FISHING_COLOR, fontweight="medium")
154 ax.text(beam_right[0], pan_y_tourism - 0.28, f"Tourism\n{tourism_score:.2f}",
155 ha="center", va="top", fontsize=9.5, color=TOURISM_COLOR, fontweight="medium")
156
157 gap = tourism_score - fishing_score
158 if abs(gap) < 0.005:
159 gap_label, gap_color = "Even", "#52514E"
160 elif gap > 0:
161 gap_label, gap_color = f"Favors tourism by {abs(gap):.2f}", TOURISM_COLOR
162 else:
163 gap_label, gap_color = f"Favors fishing by {abs(gap):.2f}", FISHING_COLOR
164 ax.text(0, -1.15, f"{gap_label} · n={n}", ha="center", fontsize=9, fontweight="medium", color=gap_color)
165
166 def plot_all_scales(seesaw_data, seesaw_counts, roles=None, score_range=(-1, 1)):
167 seesaw_data = seesaw_data.fillna(0)
168 n = len(seesaw_data)
169 if n == 0:
170 fig, ax = plt.subplots(figsize=(4, 2))
171 ax.text(0.5, 0.5, "No data for this selection", ha="center")
172 ax.axis("off")
173 return fig
174 n_max = seesaw_counts.max() if len(seesaw_counts) else 1
175 cols = min(3, n); rows = int(np.ceil(n / cols))
176 fig, axes = plt.subplots(rows, cols, figsize=(4*cols, 5.6*rows))
177 fig.patch.set_alpha(0)
178 axes = np.array(axes).reshape(-1)
179 for ax, (name, row) in zip(axes, seesaw_data.iterrows()):
180 role = roles.get(name, "") if roles else ""
181 plot_scale(ax, name, role, row.get("tourism", 0), row.get("fishing", 0),
182 seesaw_counts.get(name, 0), n_max, score_range)
183 for ax in axes[n:]:
184 ax.axis("off")
185 fig.text(0.5, 0.005,
186 "Lower pan = higher (more positive) sentiment, like a heavier weight · pan size = amount of data behind the average",
187 ha="center", fontsize=9.5, color="#52514E", style="italic")
188 fig.tight_layout(rect=[0, 0.03, 1, 0.94])
189 return fig
190
191 return (plot_all_scales,)
192
193
194@app.cell
195def _(
196 discussion_toggle,
197 journalist_toggle,
198 mo,
199 people_roles,
200 plan_toggle,
201 plot_all_scales,
202 seesaw_counts,
203 seesaw_data,
204):
205 mo.vstack([
206 mo.hstack([discussion_toggle, plan_toggle, journalist_toggle], justify="center", gap=1.5),
207 plot_all_scales(seesaw_data, seesaw_counts, roles=people_roles),
208 ])
209 return
210
211
212@app.cell
213def _(
214 ast,
215 discussion_toggle,
216 gov_folder,
217 journ_folder,
218 journalist_toggle,
219 mo,
220 pd,
221 plan_toggle,
222 plot_all_scales,
223):
224 def load_org_participations(folder, source_name):
225 disc = pd.read_csv(folder / "discussion_org_participations.csv")
226 plan = pd.read_csv(folder / "plan_org_participations.csv")
227 disc["record_type"], plan["record_type"] = "discussion", "plan"
228 disc["source"], plan["source"] = source_name, source_name
229 return pd.concat([disc, plan], ignore_index=True)
230
231 all_org_participations = pd.concat([
232 load_org_participations(gov_folder, "government"),
233 load_org_participations(journ_folder, "journalist"),
234 ], ignore_index=True)
235
236 def build_org_seesaw_data(participations, include_discussions, include_plans, include_journalist):
237 record_types = []
238 if include_discussions:
239 record_types.append("discussion")
240 if include_plans:
241 record_types.append("plan")
242 if not record_types:
243 return pd.DataFrame(columns=["fishing", "tourism"]), pd.Series(dtype=int)
244
245 df = participations[participations["record_type"].isin(record_types)].copy()
246 sources = ["government", "journalist"] if include_journalist else ["government"]
247 df = df[df["source"].isin(sources)]
248 df = df.dropna(subset=["industry"])
249 df["industry_list"] = df["industry"].apply(ast.literal_eval)
250 df = df.explode("industry_list")
251 df["topic_side"] = df["industry_list"].map({
252 "tourism": "tourism", "small vessel": "fishing", "large vessel": "fishing",
253 })
254 df = df.dropna(subset=["topic_side"])
255
256 means = df.groupby(["organization_id", "topic_side"])["sentiment"].mean().unstack()
257 counts = df.groupby("organization_id").size()
258 return means, counts
259
260 org_seesaw_data, org_seesaw_counts = build_org_seesaw_data(
261 all_org_participations, discussion_toggle.value, plan_toggle.value, journalist_toggle.value,
262 )
263
264 mo.vstack([
265 mo.md("**Organizations** *(for reference — not directly relevant to Elena's board-bias question)*"),
266 plot_all_scales(org_seesaw_data, org_seesaw_counts, roles=None),
267 ])
268 return
269
270
271if __name__ == "__main__":
272 app.run()
273 