Skip to content

Commit 89ee1ef

Browse files
committed
Add insights page
1 parent 6089585 commit 89ee1ef

8 files changed

Lines changed: 1649 additions & 5 deletions

File tree

data/processed/sample_queries

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
1. Vote split distribution on constitutional cases
2+
3+
SELECT maj_votes || '-' || min_votes AS vote_split, COUNT(*) AS cases
4+
FROM cases WHERE primary_issue_sub_area = 'Constitutional law'
5+
GROUP BY maj_votes, min_votes ORDER BY cases DESC;
6+
Finding: 42% unanimous (7-0), only 6.5% are 4-3 nail-biters. The court reaches very strong consensus on constitutional matters most of the time.
7+
8+
---
9+
2. Most contested constitutional topics by dissent rate
10+
11+
SELECT primary_issue, COUNT(*) AS cases,
12+
ROUND(100.0 * SUM(CASE WHEN min_votes >= 2 THEN 1 ELSE 0 END)/COUNT(*), 1) AS contested_pct
13+
FROM cases WHERE primary_issue_sub_area = 'Constitutional law'
14+
GROUP BY primary_issue HAVING cases >= 5 ORDER BY contested_pct DESC;
15+
Finding: Corporations power is by far the most contested (89% contested), while judicial power and separation of powers are remarkably uncontested (<10%). The
16+
Corporations power data clusters heavily around 2006 — clearly the WorkChoices case cluster generating maximum disagreement.
17+
18+
---
19+
3. Decision direction by Chief Justice era (constitutional cases only)
20+
21+
SELECT chief_argument, COUNT(*) AS cases,
22+
ROUND(100.0 * SUM(CASE WHEN decision_direction='liberal' THEN 1 ELSE 0 END)/COUNT(*),1) AS liberal_pct
23+
FROM cases WHERE primary_issue_sub_area = 'Constitutional law'
24+
GROUP BY chief_argument ORDER BY MIN(year_decision);
25+
Finding: Sharp shift — Brennan court was 69% liberal, Gleeson dropped to 42%, then French/Kiefel hovered around 48-51%. The Gleeson era was distinctly conservative on
26+
constitutional matters.
27+
28+
---
29+
4. Implied freedom of political communication timeline
30+
31+
SELECT year_decision, COUNT(*) AS cases,
32+
SUM(CASE WHEN decision_direction='liberal' THEN 1 ELSE 0 END) AS liberal,
33+
SUM(CASE WHEN decision_direction='conservative' THEN 1 ELSE 0 END) AS conservative
34+
FROM cases WHERE primary_issue LIKE '%political communication%'
35+
GROUP BY year_decision ORDER BY year_decision;
36+
Finding: After the early liberal decisions (1996-97), the court became almost exclusively conservative on implied freedom cases — 2013-2019 saw 11 conservative outcomes
37+
vs 2 liberal. The freedom has been heavily curtailed.
38+
39+
---
40+
5. Panel ideology vs liberal outcome (constitutional cases)
41+
42+
SELECT ROUND(proportion_liberal_panel, 1) AS panel_liberal_prop,
43+
COUNT(*) AS cases,
44+
ROUND(100.0 * SUM(CASE WHEN decision_direction='liberal' THEN 1 ELSE 0 END)/COUNT(*),1) AS liberal_outcome_pct
45+
FROM cases WHERE primary_issue_sub_area = 'Constitutional law'
46+
GROUP BY ROUND(proportion_liberal_panel, 1);
47+
Finding: There's a correlation but it's noisy — panels with 70%+ liberal justices deliver 72% liberal outcomes. But a 30% liberal panel still delivers 50% liberal
48+
outcomes, suggesting constitutional law constrains ideological voting.

scripts/build_insights_data.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
"""
2+
Build constitutional insights data from hca.db for the Insights dashboard tab.
3+
"""
4+
import json
5+
import sqlite3
6+
from pathlib import Path
7+
8+
DB_FILE = "data/processed/hca.db"
9+
OUTPUT = "website/public/insights.json"
10+
11+
12+
def strip_prefix(issue):
13+
"""Strip 'Public Law—Federal constitutional law—' prefix."""
14+
for prefix in [
15+
"Public Law—Federal constitutional law—",
16+
"Public Law—Federal constitutional law— ",
17+
]:
18+
if issue.startswith(prefix):
19+
return issue[len(prefix):].strip()
20+
return issue.strip()
21+
22+
23+
def get_insights_data():
24+
conn = sqlite3.connect(DB_FILE)
25+
conn.row_factory = sqlite3.Row
26+
c = conn.cursor()
27+
28+
# Unnamed cases (no hca_citation) are unmatched sub-matters of multi-matter decisions
29+
# that are already represented by the named lead case. Exclude them from all stats
30+
# so we count distinct decisions rather than inflating counts with duplicate sub-matters.
31+
CITED = "hca_citation IS NOT NULL AND hca_citation != ''"
32+
33+
# 1. Vote split distribution on constitutional cases
34+
c.execute(f"""
35+
SELECT
36+
maj_votes || '-' || min_votes AS split,
37+
COUNT(*) AS count
38+
FROM cases
39+
WHERE primary_issue_sub_area = 'Constitutional law'
40+
AND {CITED}
41+
GROUP BY maj_votes, min_votes
42+
ORDER BY (maj_votes + min_votes) DESC, maj_votes DESC
43+
""")
44+
vote_splits = [{'split': r['split'], 'count': r['count']} for r in c.fetchall()]
45+
46+
# 2. Topic breakdown table
47+
c.execute(f"""
48+
SELECT
49+
primary_issue AS topic,
50+
COUNT(*) AS cases,
51+
ROUND(100.0 * SUM(CASE WHEN min_votes = 0 THEN 1 ELSE 0 END)/COUNT(*), 1) AS unanimous_pct,
52+
ROUND(100.0 * SUM(CASE WHEN min_votes >= 2 THEN 1 ELSE 0 END)/COUNT(*), 1) AS contested_pct,
53+
ROUND(100.0 * SUM(CASE WHEN decision_direction='liberal' THEN 1 ELSE 0 END)/COUNT(*), 1) AS liberal_pct,
54+
ROUND(100.0 * SUM(CASE WHEN decision_direction='conservative' THEN 1 ELSE 0 END)/COUNT(*), 1) AS conservative_pct,
55+
ROUND(100.0 * SUM(CASE WHEN party_winning LIKE 'appealing%' THEN 1 ELSE 0 END)/COUNT(*), 1) AS appellant_win_pct,
56+
ROUND(AVG(CAST(maj_votes AS REAL) / (maj_votes + min_votes)), 2) AS avg_majority_share
57+
FROM cases
58+
WHERE primary_issue_sub_area = 'Constitutional law'
59+
AND {CITED}
60+
GROUP BY primary_issue
61+
HAVING cases >= 3
62+
ORDER BY cases DESC
63+
""")
64+
topic_breakdown = []
65+
for r in c.fetchall():
66+
topic_breakdown.append({
67+
'topic': strip_prefix(r['topic']),
68+
'cases': r['cases'],
69+
'unanimous_pct': r['unanimous_pct'],
70+
'contested_pct': r['contested_pct'],
71+
'liberal_pct': r['liberal_pct'],
72+
'conservative_pct': r['conservative_pct'],
73+
'appellant_win_pct': r['appellant_win_pct'],
74+
'avg_majority_share': r['avg_majority_share'],
75+
})
76+
77+
# 3. Decision direction by Chief Justice era (constitutional cases only)
78+
c.execute(f"""
79+
SELECT
80+
chief_argument AS chief,
81+
COUNT(*) AS cases,
82+
ROUND(100.0 * SUM(CASE WHEN decision_direction='liberal' THEN 1 ELSE 0 END)/COUNT(*), 1) AS liberal_pct,
83+
ROUND(100.0 * SUM(CASE WHEN decision_direction='conservative' THEN 1 ELSE 0 END)/COUNT(*), 1) AS conservative_pct,
84+
ROUND(100.0 * SUM(CASE WHEN decision_direction='unspecifiable' THEN 1 ELSE 0 END)/COUNT(*), 1) AS unspecifiable_pct,
85+
ROUND(100.0 * SUM(CASE WHEN party_winning LIKE 'appealing%' THEN 1 ELSE 0 END)/COUNT(*), 1) AS appellant_win_pct,
86+
MIN(year_decision) AS from_year,
87+
MAX(year_decision) AS to_year
88+
FROM cases
89+
WHERE primary_issue_sub_area = 'Constitutional law'
90+
AND chief_argument IS NOT NULL
91+
AND {CITED}
92+
GROUP BY chief_argument
93+
ORDER BY MIN(year_decision)
94+
""")
95+
direction_by_era = [dict(r) for r in c.fetchall()]
96+
97+
# 4. Implied freedom of political communication — year-by-year
98+
c.execute(f"""
99+
SELECT
100+
year_decision AS year,
101+
COUNT(*) AS cases,
102+
SUM(CASE WHEN decision_direction='liberal' THEN 1 ELSE 0 END) AS liberal,
103+
SUM(CASE WHEN decision_direction='conservative' THEN 1 ELSE 0 END) AS conservative,
104+
SUM(CASE WHEN decision_direction='unspecifiable' THEN 1 ELSE 0 END) AS unspecifiable,
105+
GROUP_CONCAT(
106+
CASE WHEN case_name IS NOT NULL AND case_name != ''
107+
THEN case_name
108+
ELSE hca_citation
109+
END,
110+
' | '
111+
) AS case_names,
112+
GROUP_CONCAT(hca_citation, ' | ') AS citations,
113+
GROUP_CONCAT(maj_votes || '-' || min_votes, ' | ') AS vote_splits,
114+
GROUP_CONCAT(decision_direction, ' | ') AS directions
115+
FROM cases
116+
WHERE primary_issue LIKE '%political communication%'
117+
AND {CITED}
118+
GROUP BY year_decision
119+
ORDER BY year_decision
120+
""")
121+
implied_freedom = []
122+
for r in c.fetchall():
123+
implied_freedom.append({
124+
'year': r['year'],
125+
'cases': r['cases'],
126+
'liberal': r['liberal'],
127+
'conservative': r['conservative'],
128+
'unspecifiable': r['unspecifiable'],
129+
'case_names': r['case_names'].split(' | ') if r['case_names'] else [],
130+
'citations': r['citations'].split(' | ') if r['citations'] else [],
131+
'vote_splits': r['vote_splits'].split(' | ') if r['vote_splits'] else [],
132+
'directions': r['directions'].split(' | ') if r['directions'] else [],
133+
})
134+
135+
# 5. Corporations power case list
136+
c.execute(f"""
137+
SELECT
138+
year_decision AS year,
139+
CASE WHEN case_name IS NOT NULL AND case_name != '' THEN case_name ELSE hca_citation END AS name,
140+
hca_citation AS citation,
141+
maj_votes,
142+
min_votes,
143+
decision_direction AS direction,
144+
party_winning
145+
FROM cases
146+
WHERE primary_issue LIKE '%Corporations power%'
147+
AND {CITED}
148+
ORDER BY year_decision
149+
""")
150+
corporations_cases = [dict(r) for r in c.fetchall()]
151+
152+
# Summary stats for callout cards
153+
c.execute(f"SELECT COUNT(*) AS n FROM cases WHERE primary_issue_sub_area='Constitutional law' AND {CITED}")
154+
total_const = c.fetchone()['n']
155+
156+
c.execute(f"SELECT COUNT(*) AS n FROM cases WHERE primary_issue_sub_area='Constitutional law' AND min_votes=0 AND {CITED}")
157+
unanimous_const = c.fetchone()['n']
158+
159+
c.execute(f"SELECT COUNT(*) AS n FROM cases WHERE primary_issue LIKE '%political communication%' AND decision_direction='conservative' AND {CITED}")
160+
freedom_conservative = c.fetchone()['n']
161+
162+
c.execute(f"SELECT COUNT(*) AS n FROM cases WHERE primary_issue LIKE '%political communication%' AND {CITED}")
163+
freedom_total = c.fetchone()['n']
164+
165+
c.execute(f"""
166+
SELECT ROUND(100.0*SUM(CASE WHEN min_votes>=2 THEN 1 ELSE 0 END)/COUNT(*),1) AS pct
167+
FROM cases WHERE primary_issue LIKE '%Corporations power%' AND {CITED}
168+
""")
169+
corps_contested = c.fetchone()['pct']
170+
171+
conn.close()
172+
173+
return {
174+
'summary': {
175+
'total_constitutional_cases': total_const,
176+
'unanimous_pct': round(100.0 * unanimous_const / total_const, 1),
177+
'freedom_total': freedom_total,
178+
'freedom_conservative_pct': round(100.0 * freedom_conservative / freedom_total, 1),
179+
'corporations_contested_pct': corps_contested,
180+
},
181+
'vote_splits': vote_splits,
182+
'topic_breakdown': topic_breakdown,
183+
'direction_by_era': direction_by_era,
184+
'implied_freedom': implied_freedom,
185+
'corporations_cases': corporations_cases,
186+
}
187+
188+
189+
def main():
190+
print("Building insights data...")
191+
data = get_insights_data()
192+
193+
Path(OUTPUT).parent.mkdir(parents=True, exist_ok=True)
194+
with open(OUTPUT, 'w') as f:
195+
json.dump(data, f, indent=2)
196+
197+
print(f"✅ Wrote {OUTPUT}")
198+
s = data['summary']
199+
print(f" {s['total_constitutional_cases']} constitutional cases")
200+
print(f" {s['unanimous_pct']}% unanimous")
201+
print(f" {len(data['topic_breakdown'])} topics")
202+
print(f" {len(data['implied_freedom'])} implied freedom data points")
203+
204+
205+
if __name__ == "__main__":
206+
main()

0 commit comments

Comments
 (0)