{ "cells": [ { "cell_type": "markdown", "id": "header", "metadata": {}, "source": [ "# Flow Brush Map API - Interactive Migration Visualization\n", "\n", "This notebook demonstrates the new `flow_brushmap()` API in pytidycensus, which provides a simple, high-level interface for creating interactive migration flow visualizations.\n", "\n", "The `flow_brushmap()` function handles all the complexity of:\n", "- Processing migration flow data\n", "- Converting to GeoArrow format\n", "- Creating arc, source, and target layers\n", "- Configuring the BrushingExtension\n", "- Building an interactive lonboard map\n", "\n", "All you need is data from `get_flows()` with `geometry=True`!" ] }, { "cell_type": "markdown", "id": "installation", "metadata": {}, "source": [ "## Installation\n", "\n", "To use the mapping functions, install pytidycensus with the map extra:\n", "\n", "```bash\n", "pip install pytidycensus[map]\n", "```\n", "\n", "This installs:\n", "- lonboard >= 0.12.1\n", "- pyarrow >= 19.0.0\n", "- folium >= 0.20.0\n", "- contextily >= 1.6.2\n", "- branca >= 0.8.2" ] }, { "cell_type": "markdown", "id": "imports_header", "metadata": {}, "source": [ "## Imports" ] }, { "cell_type": "code", "execution_count": 1, "id": "imports", "metadata": {}, "outputs": [], "source": [ "import pytidycensus as tc\n", "from pytidycensus.mapping import flow_brushmap, quick_flow_map" ] }, { "cell_type": "markdown", "id": "basic_usage_header", "metadata": {}, "source": [ "## Basic Usage\n", "\n", "The simplest way to create a flow brush map is to use `quick_flow_map()`, which fetches data and creates the map in one call:" ] }, { "cell_type": "code", "execution_count": null, "id": "quick_map", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Fetching 2018 county-level migration flows for PA...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/mmann1123/Documents/github/pytidycensus/pytidycensus/flows.py:654: UserWarning: Could not find centroids for 8 GEOIDs: ['09009', '09003', '09005', '09001', '09013'].... These flows will not have geometry data.\n", " warnings.warn(\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Processing 13778 flow records...\n", "Created 2076 arcs, 2503 sources, and 67 targets\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "9f2134fd83a34fc48f9db9be1db36c47", "version_major": 2, "version_minor": 1 }, "text/plain": [ "Map(custom_attribution='', layers=(ScatterplotLayer(brushing_radius=50000.0, extensions=(BrushingExtension(),)…" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Quick one-liner: fetch data and create map\n", "map_ = quick_flow_map(\n", " state=\"PA\", \n", " year=2018,\n", " flow_threshold=50, # Show flows >= 50 people\n", " brushing_radius=50000 # 50km brushing radius\n", ")\n", "map_" ] }, { "cell_type": "markdown", "id": "standard_usage_header", "metadata": {}, "source": [ "## Standard Usage: Two-Step Approach\n", "\n", "For more control, fetch the data first, then create the map:" ] }, { "cell_type": "code", "execution_count": 3, "id": "fetch_data", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Retrieved 36641 flow records\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/mmann1123/Documents/github/pytidycensus/pytidycensus/flows.py:654: UserWarning: Could not find centroids for 97 GEOIDs: ['0900108980', '0900715350', '0900308490', '0900761800', '0900387000'].... These flows will not have geometry data.\n", " warnings.warn(\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
GEOID1GEOID2FULL1_NAMEFULL2_NAMEMOVEDINMOVEDIN_MMOVEDOUTMOVEDOUT_MMOVEDNETMOVEDNET_Mcentroid1centroid2
048001NoneAnderson County, TexasAfrica3852.0NaNNaNNaNNaNPOINT (-95.65236 31.81326)NaN
148001NoneAnderson County, TexasAsia46.0NaNNaNNaNNaNPOINT (-95.65236 31.81326)NaN
248001NoneAnderson County, TexasCentral America23.0NaNNaNNaNNaNPOINT (-95.65236 31.81326)NaN
34800101089Anderson County, TexasMadison County, Alabama1320.00.028.013.020.0POINT (-95.65236 31.81326)POINT (-86.55022579567611 34.762959383197796)
44800102016Anderson County, TexasAleutians West Census Area, Alaska031.07.09.0-7.09.0POINT (-95.65236 31.81326)POINT (-173.77316055538125 52.983281670565866)
\n", "
" ], "text/plain": [ " GEOID1 GEOID2 FULL1_NAME FULL2_NAME \\\n", "0 48001 None Anderson County, Texas Africa \n", "1 48001 None Anderson County, Texas Asia \n", "2 48001 None Anderson County, Texas Central America \n", "3 48001 01089 Anderson County, Texas Madison County, Alabama \n", "4 48001 02016 Anderson County, Texas Aleutians West Census Area, Alaska \n", "\n", " MOVEDIN MOVEDIN_M MOVEDOUT MOVEDOUT_M MOVEDNET MOVEDNET_M \\\n", "0 38 52.0 NaN NaN NaN NaN \n", "1 4 6.0 NaN NaN NaN NaN \n", "2 2 3.0 NaN NaN NaN NaN \n", "3 13 20.0 0.0 28.0 13.0 20.0 \n", "4 0 31.0 7.0 9.0 -7.0 9.0 \n", "\n", " centroid1 centroid2 \n", "0 POINT (-95.65236 31.81326) NaN \n", "1 POINT (-95.65236 31.81326) NaN \n", "2 POINT (-95.65236 31.81326) NaN \n", "3 POINT (-95.65236 31.81326) POINT (-86.55022579567611 34.762959383197796) \n", "4 POINT (-95.65236 31.81326) POINT (-173.77316055538125 52.983281670565866) " ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Step 1: Fetch migration flow data\n", "tx_flows = tc.get_flows(\n", " geography=\"county\",\n", " state=\"TX\",\n", " year=2018,\n", " geometry=True, # Required for mapping!\n", " output=\"wide\"\n", ")\n", "\n", "print(f\"Retrieved {len(tx_flows)} flow records\")\n", "tx_flows.head()" ] }, { "cell_type": "code", "execution_count": 13, "id": "create_map", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Processing 35684 flow records...\n", "Created 2287 arcs, 3049 sources, and 253 targets\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "91660e89273a4f8494446fee78000081", "version_major": 2, "version_minor": 1 }, "text/plain": [ "Map(custom_attribution='', layers=(ScatterplotLayer(brushing_radius=100000.0, extensions=(BrushingExtension(),…" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Step 2: Create interactive brush map\n", "tx_map = flow_brushmap(\n", " tx_flows,\n", " flow_threshold=100, # Only show flows >= 100 people\n", " brushing_radius=100000 # 100km brush radius\n", ")\n", "tx_map" ] }, { "cell_type": "markdown", "id": "customization_header", "metadata": {}, "source": [ "## Customization Options\n", "\n", "The `flow_brushmap()` function provides many customization options:" ] }, { "cell_type": "code", "execution_count": 5, "id": "custom_map", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Processing 35684 flow records...\n", "Created 716 arcs, 1037 sources, and 253 targets\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "170885b9aa1646eb85207d3cd72451a4", "version_major": 2, "version_minor": 1 }, "text/plain": [ "Map(custom_attribution='', layers=(ScatterplotLayer(brushing_radius=150000.0, extensions=(BrushingExtension(),…" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Custom colors, sizes, and interaction settings\n", "custom_map = flow_brushmap(\n", " tx_flows,\n", " flow_threshold=200,\n", " brushing_radius=150000, # 150km radius\n", " source_color=(255, 100, 100), # Light red for outflow\n", " target_color=(100, 100, 255), # Light blue for inflow\n", " arc_opacity=0.6, # More visible arcs\n", " arc_width=2, # Thicker arcs\n", " point_radius_scale=5000, # Larger points\n", " picking_radius=15 # Easier to click\n", ")\n", "custom_map" ] }, { "cell_type": "markdown", "id": "layers_header", "metadata": {}, "source": [ "## Accessing Individual Layers\n", "\n", "For advanced use cases, you can access the individual layers:" ] }, { "cell_type": "code", "execution_count": 6, "id": "get_layers", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Processing 35684 flow records...\n", "Created 2287 arcs, 3049 sources, and 253 targets\n", "Available layers:\n", " Source layer: \n", " Target layer: \n", " Arc layer: \n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "368b1655cf1f455e9cac421cb632bcb0", "version_major": 2, "version_minor": 1 }, "text/plain": [ "Map(custom_attribution='', layers=(ScatterplotLayer(brushing_radius=100000.0, extensions=(BrushingExtension(),…" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Get map and layers separately\n", "map_with_layers, layers = flow_brushmap(\n", " tx_flows,\n", " flow_threshold=100,\n", " return_layers=True\n", ")\n", "\n", "print(\"Available layers:\")\n", "print(f\" Source layer: {type(layers['source'])}\")\n", "print(f\" Target layer: {type(layers['target'])}\")\n", "print(f\" Arc layer: {type(layers['arc'])}\")\n", "\n", "# You can now modify layers or create custom combinations\n", "map_with_layers" ] }, { "cell_type": "markdown", "id": "different_geographies_header", "metadata": {}, "source": [ "## Different Geographic Levels\n", "\n", "The API works with any geography supported by `get_flows()`:" ] }, { "cell_type": "markdown", "id": "msa_header", "metadata": {}, "source": [ "### Metropolitan Statistical Areas" ] }, { "cell_type": "code", "execution_count": 10, "id": "msa_map", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Fetching 2018 county-level migration flows for CA...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/mmann1123/Documents/github/pytidycensus/pytidycensus/flows.py:654: UserWarning: Could not find centroids for 115 GEOIDs: ['0901115910', '0900163480', '0900308490', '0900761800', '0900387000'].... These flows will not have geometry data.\n", " warnings.warn(\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Processing 21910 flow records...\n", "Created 66 arcs, 113 sources, and 58 targets\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "bc1b32a5bee44f3892fa4b05069b4c06", "version_major": 2, "version_minor": 1 }, "text/plain": [ "Map(custom_attribution='', layers=(ScatterplotLayer(brushing_radius=200000.0, extensions=(BrushingExtension(),…" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# County-level flows (requires year >= 2013)\n", "county_map = quick_flow_map(\n", " geography=\"county\",\n", " state=\"CA\", # California\n", " year=2018,\n", " flow_threshold=1000, # Higher threshold for larger areas\n", " brushing_radius=200000 # 200km for larger geographic scale\n", ")\n", "# Note: This will show ALL county flows in the CA, which may be slow\n", "# Consider filtering the data first\n", "county_map" ] }, { "cell_type": "markdown", "id": "filtering_header", "metadata": {}, "source": [ "## Filtering Data Before Mapping\n", "\n", "For large datasets, filter the data to improve performance:" ] }, { "cell_type": "code", "execution_count": 8, "id": "filtering", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Filtered from 22657 to 2604 CA-to-CA flows\n", "Processing 2604 flow records...\n", "Created 428 arcs, 856 sources, and 58 targets\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/home/mmann1123/Documents/github/pytidycensus/pytidycensus/flows.py:654: UserWarning: Could not find centroids for 115 GEOIDs: ['0901115910', '0900163480', '0900308490', '0900761800', '0900387000'].... These flows will not have geometry data.\n", " warnings.warn(\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "e5c313c05ec14c92af2e35975d146f44", "version_major": 2, "version_minor": 1 }, "text/plain": [ "Map(custom_attribution='', layers=(ScatterplotLayer(brushing_radius=100000.0, extensions=(BrushingExtension(),…" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Get California flows\n", "ca_flows = tc.get_flows(\n", " geography=\"county\",\n", " state=\"CA\",\n", " year=2018,\n", " geometry=True,\n", " output=\"wide\"\n", ")\n", "\n", "# Filter to only CA-to-CA flows (exclude international and other states)\n", "ca_to_ca = ca_flows[\n", " ca_flows['GEOID2'].notna() &\n", " ca_flows['GEOID2'].str.startswith('06', na=False) # CA FIPS code\n", "]\n", "\n", "print(f\"Filtered from {len(ca_flows)} to {len(ca_to_ca)} CA-to-CA flows\")\n", "\n", "# Create map with filtered data\n", "ca_map = flow_brushmap(\n", " ca_to_ca,\n", " flow_threshold=100,\n", " brushing_radius=100000\n", ")\n", "ca_map" ] }, { "cell_type": "markdown", "id": "interpretation_header", "metadata": {}, "source": [ "## Interpretation Guide\n", "\n", "### Colors\n", "- **Red**: Outward migration (people leaving)\n", "- **Blue**: Inward migration (people arriving)\n", "\n", "### Layers\n", "1. **Arcs**: Lines showing migration flows\n", " - Color gradient from source (red) to target (blue)\n", " - Only visible near cursor (within brushing_radius)\n", "\n", "2. **Source Points**: Small filled circles\n", " - Origin counties for migrations\n", " - Color indicates direction relative to that flow\n", "\n", "3. **Target Rings**: Hollow circles\n", " - Destination counties\n", " - Ring color shows net migration (red=loss, blue=gain)\n", " - Ring size indicates magnitude of net migration\n" ] }, { "cell_type": "markdown", "id": "performance_header", "metadata": {}, "source": [ "## Performance Tips\n", "\n", "1. **Start with a higher flow_threshold** to reduce number of arcs\n", "2. **Filter to specific regions** before mapping (e.g., single state)\n", "3. **Use smaller brushing_radius** for dense urban areas\n", "4. **For large datasets**, consider aggregating to larger geographies (county → MSA)\n", "\n" ] } ], "metadata": { "kernelspec": { "display_name": "census", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.13" } }, "nbformat": 4, "nbformat_minor": 5 }