How to Test Whether a Migration Flow Is Really One-Way in Python

On this page

Problem statement

Between any two places, people move both ways. A flow map that draws only the larger direction, or only the net, invites the reader to see a one-way stream. Often, though, the two directions are close, and their difference is smaller than the uncertainty in either estimate. With survey data such as the American Community Survey, the question "do more people move from A to B than from B to A?" has a statistical answer. Compare the difference with its margin of error, which combines the margins of both flows. A related measure, migration efficiency (net divided by gross), says how one-sided an exchange is on a scale from 0 to 100%.

This page tests every county pair that exchanges people with Los Angeles County. It finds which exchanges are significantly one-sided and maps them.

Quick answer

For each pair, take the difference between the two directions, combine their MOEs in quadrature, and call the asymmetry significant when the difference exceeds its MOE:

pairs = us.dropna(subset=["flow", "counter"]).copy()
pairs["diff"] = pairs.counter - pairs.flow                                  # out of LA minus into LA
pairs["diff_moe"] = np.sqrt(pairs.counter_moe.fillna(0) ** 2 + pairs.flow_moe.fillna(0) ** 2)
pairs["significant"] = pairs["diff"].abs() > pairs.diff_moe
pairs["efficiency"] = pairs["diff"] / (pairs.counter + pairs.flow)
print(f"{len(pairs):,} partner counties; significantly one-sided: {pairs.significant.sum():,} "
      f"({pairs.loc[pairs.significant, 'counter'].add(pairs.loc[pairs.significant, 'flow']).sum() / (pairs.counter + pairs.flow).sum():.0%} of all moves between LA and them)")
1,234 partner counties; significantly one-sided: 179 (75% of all moves between LA and them)
Map of the contiguous United States where counties exchanging movers with Los Angeles County are coloured by migration efficiency only if the exchange is significantly one-sided: orange and red counties across California, Arizona, Nevada and the Pacific Northwest receive more from Los Angeles than they send; a few blue counties around New York and elsewhere send more; most partner counties are grey. Map of the contiguous United States where counties exchanging movers with Los Angeles County are coloured by migration efficiency only if the exchange is significantly one-sided: orange and red counties across California, Arizona, Nevada and the Pacific Northwest receive more from Los Angeles than they send; a few blue counties around New York and elsewhere send more; most partner counties are grey.
Which exchanges with Los Angeles County are really one-way. Grey counties exchanged people with Los Angeles, but the difference between the two directions is within its margin of error.

Size dependence, one-sidedness, significant directions

1. See how the test depends on size

Big exchanges are almost always significant: their margins are small relative to the flows. Small ones almost never are, however lopsided they look:

size = pd.cut(pairs.counter + pairs.flow, [0, 100, 1000, 10000, np.inf], labels=["<100", "100–1k", "1k–10k", "10k+"])
print(pairs.groupby(size, observed=True).agg(pairs=("significant", "size"), significant=("significant", "mean")).to_string(
    formatters={"significant": "{:.0%}".format}))
        pairs significant
<100      845          6%
100–1k    312         25%
1k–10k     68         63%
10k+        9        100%

2. Measure how one-sided the big exchanges are

Migration efficiency is net over gross: 0% for a perfectly balanced exchange, 100% for a one-way stream. For Los Angeles's largest partners:

big = pairs.assign(gross=pairs.counter + pairs.flow).nlargest(10, "gross")
print(big[["b_county", "b_state", "counter", "flow", "efficiency", "significant"]].to_string(index=False,
      formatters={"efficiency": "{:+.0%}".format, "counter": "{:,.0f}".format, "flow": "{:,.0f}".format}))
             b_county    b_state counter   flow efficiency  significant
        Orange County California  38,761 23,839       +24%         True
San Bernardino County California  34,920 15,590       +38%         True
     Riverside County California  23,292 10,232       +39%         True
     San Diego County California  10,436  8,898        +8%         True
       Ventura County California  10,187  6,625       +21%         True
         Clark County     Nevada  12,873  3,349       +59%         True
          Kern County California  10,503  5,058       +35%         True
      Maricopa County    Arizona   8,614  2,891       +50%         True
       Alameda County California   6,538  4,404       +20%         True
   Santa Clara County California   5,413  3,900       +16%         True

3. Count the direction of the significant ones

Of the exchanges that are significantly one-sided, how many run out of Los Angeles and how many into it?

sig = pairs[pairs.significant]
print(f"significant and net out of LA: {(sig['diff'] > 0).sum()}; net into LA: {(sig['diff'] < 0).sum()}")
print("largest significant net inflow:", sig.nsmallest(1, "diff")[["b_county", "b_state", "diff"]].to_string(index=False, header=False))
significant and net out of LA: 124; net into LA: 55
largest significant net inflow: Kings County New York -1932

Most significant exchanges run out of Los Angeles. The largest significant one running the other way is with Brooklyn (Kings County, New York): more people moved from Brooklyn to Los Angeles than the reverse.

Scatter of migration efficiency against the size of the exchange: small exchanges spread across the whole range from minus one to one and are almost all grey, not significant; exchanges above about a thousand people are mostly red, significant, with efficiencies between 0.1 and 0.6. Scatter of migration efficiency against the size of the exchange: small exchanges spread across the whole range from minus one to one and are almost all grey, not significant; exchanges above about a thousand people are mostly red, significant, with efficiencies between 0.1 and 0.6.
Small exchanges look extreme but cannot be distinguished from balance; large ones are moderately one-sided and certain.

The workbook's own net margins

The data used on this page

The Census Bureau's county-to-county migration flows, 2016–2020 ACS ("ins, outs, nets, gross" workbook, California sheet) and 2023 county outlines projected to EPSG:5070.

from pathlib import Path

import geopandas as gpd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
SRC = {"county-to-county-2016-2020-ins-outs-nets-gross.xlsx": "https://www2.census.gov/programs-surveys/demo/tables/geographic-mobility/2020/"
       "county-to-county-migration-2016-2020/county-to-county-migration-flows/county-to-county-2016-2020-ins-outs-nets-gross.xlsx",
       "cb_2023_us_county_20m.zip": "https://www2.census.gov/geo/tiger/GENZ2023/shp/cb_2023_us_county_20m.zip"}
for name, url in SRC.items():
    if not Path("data", name).exists():
        Path("data", name).write_bytes(requests.get(url, timeout=600).content)
COLS = ["a_st", "a_cty", "b_st", "b_cty", "a_state", "a_county", "b_state", "b_county",
        "flow", "flow_moe", "counter", "counter_moe", "net", "net_moe", "gross", "gross_moe"]
ca = pd.read_excel("data/county-to-county-2016-2020-ins-outs-nets-gross.xlsx", sheet_name="California",
                   header=None, skiprows=3, names=COLS, dtype=str)
us = ca[(ca.a_cty == "037") & ca.b_st.str.fullmatch(r"\d{3}", na=False)].copy()
for c in COLS[8:]:
    us[c] = pd.to_numeric(us[c], errors="coerce")
us["geoid"] = us.b_st.str[1:] + us.b_cty
COUNTIES = gpd.read_file("data/cb_2023_us_county_20m.zip").query("STUSPS not in ['AK', 'HI', 'PR']").to_crs(5070)
print(f"{len(us):,} US counties exchanging movers with Los Angeles County")
1,234 US counties exchanging movers with Los Angeles County

Example 1: the workbook's own net MOE

check = pairs[["net_moe", "diff_moe"]].dropna()
print(f"quadrature MOE vs the workbook's net MOE: median ratio {np.median(check.diff_moe / check.net_moe):.2f}")
quadrature MOE vs the workbook's net MOE: median ratio 1.00

Two estimates, each with its own uncertainty

A flow between two places is two estimates, one for each direction, and each has its own uncertainty. The difference between them, the net, is only meaningful when it is larger than the combined uncertainty. Every exchange of more than 10,000 people between Los Angeles and another county passes that test, and 63% of those between 1,000 and 10,000 do. Of the 845 exchanges under 100 people, 6% do, about what chance alone would produce. A map of "net flows" that colours them all as gains or losses is mostly showing noise.

Share of Los Angeles County’s exchanges with other counties that are significantly one-sided, by size. only big exchanges can be told apartunder 100 people (845)6%100 to 1,000 (312)25%1,000 to 10,000 (68)63%over 10,000 (9)100%050%100%179 of 1,234 exchanges are one-sided at 90%; they hold 75% of all moves spatialworkflow.io
Significance depends almost entirely on size.

Efficiency adds the second dimension. Two exchanges can have the same net, one as a small difference between two huge flows, the other as a one-way trickle. Efficiency separates them. Los Angeles's exchanges with its neighbouring counties are large, significant, and only moderately one-sided. Its exchanges with distant counties are often strongly one-sided on paper but too small to tell.

Confidence levels, missing counterflows and many tests

Flow asymmetry problems and fixes. asymmetry problemsSymptomFixevery net looks like a streamtest the difference against itsMOEsame net, different storiesefficiency = net / gross~10% flagged by chancemany tests: treat borderline caseswith care95% wanted, MOEs are 90%scale MOEs by 1.96 / 1.645ACS county-to-county flows, Los Angeles County spatialworkflow.io
Asymmetry problems and fixes.
  • 90% level: ACS margins are 90% intervals, so "significant" here means at the 90% level; multiply MOEs by 1.96/1.645 for 95%.
  • Missing counterflows: rows with a blank flow or counterflow cannot be tested and are dropped.
  • Independence: combining MOEs in quadrature assumes the two directions' errors are independent; the workbook's own net MOE is the safer number where given.
  • Many tests: testing a thousand pairs at the 90% level will flag about a hundred by chance among truly balanced pairs; treat borderline cases with care.
  • Zero flows have an MOE but no estimate; a pair with zero in one direction can still be insignificant.

FAQ

How do I test whether two ACS estimates differ?

Take the difference, compute its MOE as the square root of the sum of the squared MOEs, and check whether the difference is larger than that MOE (90% level).

What is migration efficiency?

Net migration divided by gross migration between two places: 0% for a balanced exchange, ±100% for a one-way flow.

Why are most small migration flows not significant?

Their margins of error are as large as the estimates, so the difference between the two directions is usually within the combined margin.

Should a flow map show net or gross flows?

Gross shows how much exchange happens; net shows the imbalance. Show net only where it is significant, and say so.