Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

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_table
Source
# --- 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()
Loading...
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()
Loading...
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)
Loading...

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)")
Loading...

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="")
fig
Loading...

Tenure 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"},
)
Loading...

Questions to Investigate