How long do legislators stay, and do they move between the House and the Senate? This chapter samples legislators and expands their congress memberships to study tenure. It uses a modest sample so local previews stay responsive.
Source
# --- COLLAPSIBLE SETUP BLOCK ---
import pandas as pd
import plotly.express as px
import sys
from pathlib import Path
cwd = Path.cwd().resolve()
sys.path[:0] = [str(cwd), str(cwd.parent)]
from book_utils import api_get, fetch_all, full_name, wrap_label, searchable_tableSource
# --- COLLAPSIBLE ROSTER BLOCK ---
# Pull one page of the legislator roster. full_name is available directly here.
people = pd.DataFrame(fetch_all("people", page_size=100, max_pages=1))
people[["id", "full_name", "first_name", "last_name"]].head()Source
# --- COLLAPSIBLE MEMBERSHIP-EXPANSION BLOCK ---
# For each sampled legislator, fetch their full record with congress memberships,
# then emit one row per (person, congress) served. This "long" shape makes tenure
# and position analysis straightforward.
SAMPLE_SIZE = 40
membership_rows = []
for person_id in people["id"].dropna().head(SAMPLE_SIZE):
person, _ = api_get(f"people/{person_id}", include_congresses="true")
name = full_name(person)
for membership in person.get("congresses_served") or []:
membership_rows.append(
{
"person_id": person_id,
"name": name,
"congress_number": membership.get("congress_number"),
"congress_ordinal": membership.get("congress_ordinal"),
"position": membership.get("position"),
}
)
memberships = pd.DataFrame(membership_rows)
memberships.head()Source
# --- COLLAPSIBLE TENURE BLOCK ---
# Collapse memberships to one row per (person, position), counting the distinct
# congresses served. Sorting descending surfaces the longest-serving members.
tenure = (
memberships.groupby(["person_id", "name", "position"], as_index=False)
.agg(congresses_served=("congress_number", "nunique"))
.sort_values("congresses_served", ascending=False)
)
tenure.head(20)Search the Tenure Table¶
Type a name to find any sampled legislator, or sort by Congresses served to see
the veterans first.
Source
# Client-side searchable tenure table.
tenure_display = tenure.rename(
columns={"name": "Legislator", "position": "Position", "congresses_served": "Congresses served"}
)[["Legislator", "Position", "Congresses served"]]
searchable_table(tenure_display, caption="Sampled legislator tenure (searchable)")Longest-Serving in the Sample¶
Names are wrapped so long compound surnames and suffixes stay readable.
Source
# --- COLLAPSIBLE TOP-TENURE CHART BLOCK ---
# Take the longest-serving members in the sample and wrap their names for the axis.
top_tenure = tenure.head(15).sort_values("congresses_served").copy()
top_tenure["label"] = top_tenure["name"].map(lambda n: wrap_label(n, width=24))
fig = px.bar(
top_tenure,
x="congresses_served",
y="label",
color="position",
orientation="h",
custom_data=["name"],
title="Longest-Serving Legislators (Sampled)",
labels={"congresses_served": "Congresses served", "label": "Legislator", "position": "Position"},
)
fig.update_traces(hovertemplate="%{customdata[0]}<br>Congresses served: %{x}<extra></extra>")
fig.update_layout(margin={"l": 160}, yaxis_title="")
figTenure Distribution¶
The histogram shows how many legislators served how many congresses, split by position. A tall bar at one or two congresses with a thin veteran tail is the right-skew predicted by H1.
Source
# --- COLLAPSIBLE DISTRIBUTION BLOCK ---
px.histogram(
tenure,
x="congresses_served",
color="position",
barmode="group",
title="Sampled Legislator Tenure Distribution",
labels={"congresses_served": "Congresses served", "count": "Legislators", "position": "Position"},
)Questions to Investigate¶
Which legislators have served in the most congresses?
How often do legislators move between House and Senate roles?
Do longer-serving legislators author more bills after adjusting for congress count?