How busy was each Congress, really? This chapter compares legislative productivity across the 8th through 20th Philippine Congresses — raw bill volume, the House/Senate mix, bills per member, committee density, and which congresses stand out as unusually active or quiet once we adjust for size.
Source
# --- COLLAPSIBLE SETUP BLOCK ---
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import sys
from pathlib import Path
cwd = Path.cwd().resolve()
sys.path[:0] = [str(cwd), str(cwd.parent)]
from book_utils import fetch_all, searchable_tableSource
# --- COLLAPSIBLE DATA PREPARATION BLOCK ---
# Pull every congress with its aggregate stats. These are full, exact totals
# (no sampling), so every metric below covers the complete 8-20 range.
congresses = pd.DataFrame(fetch_all("congresses", include_stats="true"))
congresses = congresses.sort_values("congress_number")
# Coerce count columns to numbers so arithmetic and ranking behave predictably.
count_cols = [
"total_senators",
"total_representatives",
"total_committees",
"total_bills",
"total_house_bills",
"total_senate_bills",
]
for col in count_cols:
congresses[col] = pd.to_numeric(congresses[col], errors="coerce").fillna(0)
# Derived productivity metrics. np.where guards against divide-by-zero for any
# congress with missing membership or committee counts.
congresses["total_members"] = congresses["total_senators"] + congresses["total_representatives"]
congresses["bills_per_member"] = np.where(
congresses["total_members"] > 0,
congresses["total_bills"] / congresses["total_members"],
np.nan,
)
congresses["bills_per_committee"] = np.where(
congresses["total_committees"] > 0,
congresses["total_bills"] / congresses["total_committees"],
np.nan,
)
congresses["house_bill_share"] = np.where(
congresses["total_bills"] > 0,
congresses["total_house_bills"] / congresses["total_bills"],
np.nan,
)
congresses["senate_bill_share"] = np.where(
congresses["total_bills"] > 0,
congresses["total_senate_bills"] / congresses["total_bills"],
np.nan,
)
congresses["productivity_rank"] = congresses["bills_per_member"].rank(method="min", ascending=False)The overview table below is searchable and sortable — sort by bills_per_member
to see the most productive congresses, or type a year to jump to an era.
Source
# Render the full congress overview as a client-side searchable table.
overview = congresses[
[
"congress_number",
"ordinal",
"year_range",
"total_members",
"total_senators",
"total_representatives",
"total_committees",
"total_bills",
"total_house_bills",
"total_senate_bills",
"bills_per_member",
"bills_per_committee",
]
].copy()
overview["bills_per_member"] = overview["bills_per_member"].round(1)
overview["bills_per_committee"] = overview["bills_per_committee"].round(1)
searchable_table(overview, caption="Congress overview (searchable & sortable)", page_length=13)Pick a Congress: Chamber Profile (8–20)¶
Use the dropdown to select any congress from 8 to 20 and see its House vs. Senate output. This is the direct way to answer “what did the nth Congress look like?” without scrolling a chart. Remember from the completeness audit that congresses 8–12 have no Senate bills recorded, so their Senate bar is zero by data availability, not by inactivity.
Source
# --- COLLAPSIBLE CONGRESS-SELECTOR BLOCK ---
# Build one interactive figure whose dropdown swaps in a different congress.
# We pre-compute a [House, Senate] pair per congress and attach one button each,
# so the whole thing works client-side in the static HTML with no live kernel.
selector = congresses.sort_values("congress_number")
chambers = ["House Bills", "Senate Bills"]
# Default view: the most recent congress.
default_row = selector.iloc[-1]
fig = go.Figure(
go.Bar(
x=chambers,
y=[default_row["total_house_bills"], default_row["total_senate_bills"]],
marker_color=["#2b6cb0", "#c05621"],
text=[int(default_row["total_house_bills"]), int(default_row["total_senate_bills"])],
textposition="outside",
)
)
# One dropdown button per congress updates the bar heights and the title.
buttons = []
for _, row in selector.iterrows():
label = f"{row['ordinal']} ({row['year_range']})"
buttons.append(
dict(
label=label,
method="update",
args=[
{"y": [[row["total_house_bills"], row["total_senate_bills"]]],
"text": [[int(row["total_house_bills"]), int(row["total_senate_bills"])]]},
{"title": f"Bills Filed by Chamber - {label} · "
f"{int(row['total_members'])} members · "
f"{row['bills_per_member']:.1f} bills/member"},
],
)
)
fig.update_layout(
title=f"Bills Filed by Chamber - {default_row['ordinal']} ({default_row['year_range']}) · "
f"{int(default_row['total_members'])} members · "
f"{default_row['bills_per_member']:.1f} bills/member",
yaxis_title="Bills filed",
xaxis_title="Chamber",
updatemenus=[
dict(
buttons=buttons,
direction="down",
showactive=True,
x=1.02,
xanchor="left",
y=1.0,
yanchor="top",
active=len(buttons) - 1, # start on the latest congress
)
],
)
figBill Volume by Chamber¶
Raw volume is the baseline, but it can overstate productivity when membership size or coverage differs across congresses. This chart keeps the raw House/Senate split visible for every congress at once.
Source
# --- COLLAPSIBLE CHART BLOCK ---
# Melt House/Senate totals into long form for a grouped bar chart.
bill_cols = ["total_house_bills", "total_senate_bills"]
activity = congresses.melt(
id_vars=["congress_number", "ordinal", "year_range"],
value_vars=bill_cols,
var_name="bill_type",
value_name="count",
)
activity["bill_type"] = activity["bill_type"].map(
{"total_house_bills": "House Bills", "total_senate_bills": "Senate Bills"}
)
fig = px.bar(
activity,
x="ordinal",
y="count",
color="bill_type",
barmode="group",
title="Bills Filed by Congress and Chamber",
labels={"ordinal": "Congress", "count": "Bills filed", "bill_type": "Bill type"},
hover_data=["year_range"],
)
fig.update_layout(legend_title_text="", xaxis_tickangle=-45)
figThe House out-files the Senate everywhere Senate data exists — supporting H2. The zero Senate bars for the 8th–12th congresses are the coverage gap, not a legislative fact.
Productivity Normalized by Membership¶
This view ranks congresses by bills filed per legislator. It is the fair way to compare a large chamber against an unusually active one.
Source
# --- COLLAPSIBLE CHART BLOCK ---
# Sort ascending so the most productive congress lands at the top of the bar chart.
ranked = congresses.sort_values("bills_per_member", ascending=True)
fig = px.bar(
ranked,
x="bills_per_member",
y="ordinal",
orientation="h",
color="total_bills",
color_continuous_scale="Viridis",
title="Bills Filed per Member by Congress",
labels={
"bills_per_member": "Bills per member",
"ordinal": "Congress",
"total_bills": "Total bills",
},
hover_data=[
"year_range",
"total_bills",
"total_members",
"total_house_bills",
"total_senate_bills",
],
)
fig.update_layout(coloraxis_colorbar_title="Total bills")
figCompare this ordering with the raw-volume chart above: the two rankings differ, which is exactly what H1 predicts — size and per-member effort are not the same story.
Output, Committees, and Members¶
Does more committee infrastructure come with more bills? Each bubble is a congress; bubble size is bills per member.
Source
# --- COLLAPSIBLE CHART BLOCK ---
fig = px.scatter(
congresses,
x="total_committees",
y="total_bills",
size="bills_per_member",
hover_name="ordinal",
hover_data=["year_range", "total_senators", "total_representatives"],
title="Bill Volume, Committee Count, and Bills per Member",
labels={
"total_committees": "Committees",
"total_bills": "Bills filed",
"bills_per_member": "Bills per member",
},
)
fig.update_traces(marker={"line": {"width": 1, "color": "white"}})
figChamber Mix Over Time¶
The House usually files more bills in raw counts. The useful question is how much the split moves from one congress to the next.
Source
# --- COLLAPSIBLE CHART BLOCK ---
# Normalize House/Senate shares to a fraction so the area chart reads as a mix.
mix = congresses.melt(
id_vars=["ordinal", "congress_number", "year_range"],
value_vars=["house_bill_share", "senate_bill_share"],
var_name="bill_share",
value_name="share",
)
mix["bill_share"] = mix["bill_share"].map(
{"house_bill_share": "House Bill share", "senate_bill_share": "Senate Bill share"}
)
fig = px.area(
mix,
x="ordinal",
y="share",
color="bill_share",
groupnorm="fraction",
title="House and Senate Share of Bills Filed",
labels={"ordinal": "Congress", "share": "Share of filed bills", "bill_share": ""},
hover_data=["year_range"],
)
fig.update_layout(yaxis_tickformat=".0%", xaxis_tickangle=-45)
figProductivity Flags¶
The z-score below flags congresses whose bills-per-member rate is far from the historical average in this dataset. These are candidates for closer qualitative review, not automatic judgments about performance.
Source
# --- COLLAPSIBLE OUTLIER-DETECTION BLOCK ---
# Standardize bills_per_member into a z-score, then label anything beyond +/-1 SD.
mean_productivity = congresses["bills_per_member"].mean(skipna=True)
std_productivity = congresses["bills_per_member"].std(skipna=True)
congresses["productivity_z"] = (
congresses["bills_per_member"] - mean_productivity
) / std_productivity
flagged = congresses.assign(
flag=np.select(
[
congresses["productivity_z"] >= 1,
congresses["productivity_z"] <= -1,
],
["High output per member", "Low output per member"],
default="Near average",
)
)
fig = px.scatter(
flagged,
x="congress_number",
y="bills_per_member",
color="flag",
size="total_bills",
hover_name="ordinal",
hover_data=[
"year_range",
"total_members",
"total_committees",
"total_bills",
"productivity_z",
],
title="Productivity Outlier Flags",
labels={
"congress_number": "Congress number",
"bills_per_member": "Bills per member",
"flag": "",
},
)
fig.add_hline(y=mean_productivity, line_dash="dash", line_color="gray", annotation_text="Average")
fig.update_xaxes(dtick=1)
figSource
# Top congresses by per-member productivity, formatted for reading.
summary = congresses.sort_values("bills_per_member", ascending=False)[
[
"ordinal",
"year_range",
"total_bills",
"total_members",
"total_committees",
"bills_per_member",
"bills_per_committee",
"house_bill_share",
"senate_bill_share",
]
].head(10)
summary.style.format(
{
"bills_per_member": "{:.1f}",
"bills_per_committee": "{:.1f}",
"house_bill_share": "{:.0%}",
"senate_bill_share": "{:.0%}",
}
)The flagged outliers here are the congresses that satisfy H3: they stay unusual even after normalizing for membership.
Questions to Investigate¶
Did bill volume rise because there were more legislators, more committees, or higher output per member?
Which congresses have unusually high Senate Bill or House Bill shares?
Are committee counts a useful proxy for the complexity of the legislative agenda?
Which high-output congresses remain high after normalizing by membership?
Are low-output congresses associated with missing data, shorter coverage, or genuine differences in legislative filing behavior?