CoolFace
Apppublic

DavidBeltran/data_visualization

sourceHugging Faceupdated 29d agoView on Hugging Face
0likes
app.py273 linesDownload Raw Back to root
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