Skip to content

get_2010_2020_bound_changes

sdc_census10to20.get_2010_2020_bound_changes

get_2010_2020_bound_changes(res: str = 'tract', geoids: list[str] | None = None, *, state_fips: str = '51') -> pd.DataFrame

Load 2010→2020 relationship data and classify boundary changes.

Parameters:

Name Type Description Default
res str

Resolution: "tract" or "block group".

'tract'
geoids list[str] or None

Optional list of 2010 GEOIDs to filter.

None
state_fips str

State FIPS for the block-group relationship file (default Virginia, "51").

'51'

Returns:

Type Description
DataFrame

Crosswalk with columns geoid20, geoid10, area20, area10, area_part, type_change. type_change is one of:

  • "same" — one-to-one mapping, identical area
  • "split" — one 2010 GEOID divided into multiple 2020 GEOIDs with no boundary movement
  • "moved" — partial overlap; boundary shifted
Source code in packages/sdc-census10to20/src/sdc_census10to20/crosswalk.py
def get_2010_2020_bound_changes(
    res: str = "tract",
    geoids: list[str] | None = None,
    *,
    state_fips: str = "51",
) -> pd.DataFrame:
    """Load 2010→2020 relationship data and classify boundary changes.

    Parameters
    ----------
    res : str
        Resolution: ``"tract"`` or ``"block group"``.
    geoids : list[str] or None
        Optional list of 2010 GEOIDs to filter.
    state_fips : str
        State FIPS for the block-group relationship file (default Virginia, "51").

    Returns
    -------
    pd.DataFrame
        Crosswalk with columns ``geoid20``, ``geoid10``, ``area20``, ``area10``,
        ``area_part``, ``type_change``. ``type_change`` is one of:

        - ``"same"``  — one-to-one mapping, identical area
        - ``"split"`` — one 2010 GEOID divided into multiple 2020 GEOIDs with no
          boundary movement
        - ``"moved"`` — partial overlap; boundary shifted
    """
    crosswalk = _load_relationship(res, state_fips)

    if geoids is not None:
        crosswalk = crosswalk[crosswalk["geoid10"].isin(geoids)]

    crosswalk["count_20"] = crosswalk.groupby("geoid20")["geoid20"].transform("size")
    crosswalk["count_10"] = crosswalk.groupby("geoid10")["geoid10"].transform("size")

    geoid_10_20 = (
        crosswalk[["geoid10", "area20"]]
        .groupby("geoid10", as_index=False)
        .sum()
        .rename(columns={"area20": "match_area"})
    )
    crosswalk = crosswalk.merge(geoid_10_20, on="geoid10", how="left")

    # Match R's case_when first-match-wins semantics: "same" is checked
    # before "split", so apply lower-priority masks first and let higher-
    # priority masks overwrite.
    crosswalk["type_change"] = "moved"
    split_mask = crosswalk["area10"] == crosswalk["match_area"]
    crosswalk.loc[split_mask, "type_change"] = "split"
    same_mask = (crosswalk["count_10"] == 1) & (crosswalk["count_20"] == 1)
    crosswalk.loc[same_mask, "type_change"] = "same"

    crosswalk = crosswalk.drop(columns=["count_10", "count_20", "match_area"])
    return crosswalk