Add enhanced data center cluster map
This commit is contained in:
527
enhanced_data_center_cluster_map.ipynb
Normal file
527
enhanced_data_center_cluster_map.ipynb
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "0",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Enhanced Data Center Cluster Map\n",
|
||||||
|
"\n",
|
||||||
|
"This notebook starts from the spatial clustering outputs created by `spatial_clustering_master_data_centers.ipynb` and adds contextual layers from the demographic/RUCA/energy analysis.\n",
|
||||||
|
"\n",
|
||||||
|
"Current features:\n",
|
||||||
|
"- Loads point and cluster summary CSVs from `output/`.\n",
|
||||||
|
"- Recreates the cluster-colored Folium map.\n",
|
||||||
|
"- Enriches point popups with HUC8 watershed, RUCA, tract demographics, and state energy context where available.\n",
|
||||||
|
"- Adds separate layers for clustered points, isolated/noise points, cluster centroids, HUC8 watersheds, and state IM3 projected demand.\n",
|
||||||
|
"- Saves a standalone HTML map to `output/enhanced_master_data_center_spatial_clusters_map.html`.\n",
|
||||||
|
"\n",
|
||||||
|
"Notes from `output/data_center_demographic_ruca_energy_summary.md`:\n",
|
||||||
|
"- HUC8 watershed join is a recommended next step for water-context analysis.\n",
|
||||||
|
"- `im3_state_projected_moderate_50` is populated and used for state projected demand context.\n",
|
||||||
|
"- `seds_state_msn_year` is checked through the state context export, but it currently has no rows, so SEDS fields are blank until that table is populated.\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "1",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import os\n",
|
||||||
|
"import json\n",
|
||||||
|
"from html import escape\n",
|
||||||
|
"from pathlib import Path\n",
|
||||||
|
"\n",
|
||||||
|
"os.environ.setdefault('MPLCONFIGDIR', '/tmp/matplotlib')\n",
|
||||||
|
"Path(os.environ['MPLCONFIGDIR']).mkdir(parents=True, exist_ok=True)\n",
|
||||||
|
"\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"import folium\n",
|
||||||
|
"from folium import plugins\n",
|
||||||
|
"\n",
|
||||||
|
"print('pandas:', pd.__version__)\n",
|
||||||
|
"print('folium:', folium.__version__)\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "2",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Paths And Display Settings"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "3",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"OUTPUT_DIR = Path('output')\n",
|
||||||
|
"POINTS_CSV = OUTPUT_DIR / 'master_data_center_spatial_cluster_points.csv'\n",
|
||||||
|
"CLUSTERS_CSV = OUTPUT_DIR / 'master_data_center_spatial_cluster_summary.csv'\n",
|
||||||
|
"POINT_CONTEXT_CSV = OUTPUT_DIR / 'master_data_center_map_context.csv'\n",
|
||||||
|
"HUC8_GEOJSON = OUTPUT_DIR / 'master_data_center_huc8_watersheds.geojson'\n",
|
||||||
|
"STATE_ENERGY_CSV = OUTPUT_DIR / 'master_data_center_state_energy_context.csv'\n",
|
||||||
|
"MAP_HTML = OUTPUT_DIR / 'enhanced_master_data_center_spatial_clusters_map.html'\n",
|
||||||
|
"\n",
|
||||||
|
"MAP_CENTER = [39, -98]\n",
|
||||||
|
"MAP_ZOOM = 4\n",
|
||||||
|
"BASE_TILES = 'CartoDB positron'\n",
|
||||||
|
"\n",
|
||||||
|
"MAX_POINTS = None\n",
|
||||||
|
"\n",
|
||||||
|
"CLUSTERED_RADIUS = 5\n",
|
||||||
|
"NOISE_RADIUS = 3\n",
|
||||||
|
"CENTROID_RADIUS = 7\n",
|
||||||
|
"SHOW_CENTROID_P90_CIRCLES = True\n",
|
||||||
|
"SHOW_HUC8_LAYER = True\n",
|
||||||
|
"SHOW_STATE_ENERGY_LAYER = True\n",
|
||||||
|
"\n",
|
||||||
|
"OUTPUT_DIR.mkdir(exist_ok=True)\n",
|
||||||
|
"print('points:', POINTS_CSV)\n",
|
||||||
|
"print('clusters:', CLUSTERS_CSV)\n",
|
||||||
|
"print('point context:', POINT_CONTEXT_CSV)\n",
|
||||||
|
"print('HUC8 GeoJSON:', HUC8_GEOJSON)\n",
|
||||||
|
"print('state energy context:', STATE_ENERGY_CSV)\n",
|
||||||
|
"print('html output:', MAP_HTML)\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "4",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Load Cluster Outputs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "5",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"required_files = [POINTS_CSV, CLUSTERS_CSV]\n",
|
||||||
|
"missing = [str(p) for p in required_files if not p.exists()]\n",
|
||||||
|
"if missing:\n",
|
||||||
|
" raise FileNotFoundError('Missing required cluster output CSV(s): ' + ', '.join(missing))\n",
|
||||||
|
"\n",
|
||||||
|
"points = pd.read_csv(POINTS_CSV)\n",
|
||||||
|
"clusters = pd.read_csv(CLUSTERS_CSV)\n",
|
||||||
|
"point_context = pd.read_csv(POINT_CONTEXT_CSV) if POINT_CONTEXT_CSV.exists() else pd.DataFrame()\n",
|
||||||
|
"state_energy = pd.read_csv(STATE_ENERGY_CSV) if STATE_ENERGY_CSV.exists() else pd.DataFrame()\n",
|
||||||
|
"\n",
|
||||||
|
"if MAX_POINTS is not None:\n",
|
||||||
|
" points = points.head(MAX_POINTS).copy()\n",
|
||||||
|
"\n",
|
||||||
|
"points['cluster_id'] = pd.to_numeric(points['cluster_id'], errors='coerce').fillna(-1).astype(int)\n",
|
||||||
|
"points['is_noise'] = points['cluster_id'].eq(-1)\n",
|
||||||
|
"points['is_clustered'] = ~points['is_noise']\n",
|
||||||
|
"points['name'] = points['name'].fillna('')\n",
|
||||||
|
"points['operator'] = points['operator'].fillna('Unknown').replace('', 'Unknown')\n",
|
||||||
|
"points['city'] = points['city'].fillna('Unknown').replace('', 'Unknown')\n",
|
||||||
|
"points['state'] = points['state'].fillna('UNK').replace('', 'UNK')\n",
|
||||||
|
"points['source'] = points['source'].fillna('unknown').replace('', 'unknown')\n",
|
||||||
|
"\n",
|
||||||
|
"if not point_context.empty:\n",
|
||||||
|
" context_cols = [c for c in point_context.columns if c != 'master_id']\n",
|
||||||
|
" points = points.merge(point_context[['master_id'] + context_cols], on='master_id', how='left')\n",
|
||||||
|
"\n",
|
||||||
|
"if not state_energy.empty:\n",
|
||||||
|
" state_cols = [c for c in state_energy.columns if c != 'state_code']\n",
|
||||||
|
" points = points.merge(state_energy[['state_code'] + state_cols], left_on='state', right_on='state_code', how='left')\n",
|
||||||
|
"\n",
|
||||||
|
"clusters['cluster_id'] = pd.to_numeric(clusters['cluster_id'], errors='coerce').astype(int)\n",
|
||||||
|
"clusters = clusters.sort_values(['point_count', 'radius_km_p90'], ascending=[False, True]).reset_index(drop=True)\n",
|
||||||
|
"clusters['cluster_rank'] = clusters.index + 1\n",
|
||||||
|
"\n",
|
||||||
|
"huc8_geojson = None\n",
|
||||||
|
"if HUC8_GEOJSON.exists():\n",
|
||||||
|
" huc8_geojson = json.loads(HUC8_GEOJSON.read_text())\n",
|
||||||
|
"\n",
|
||||||
|
"n_clusters = points.loc[points['cluster_id'].ne(-1), 'cluster_id'].nunique()\n",
|
||||||
|
"print(f'Loaded {len(points):,} points and {n_clusters:,} clusters')\n",
|
||||||
|
"print('point context columns:', 0 if point_context.empty else len(point_context.columns))\n",
|
||||||
|
"print('HUC8 features:', 0 if huc8_geojson is None else len(huc8_geojson.get('features', [])))\n",
|
||||||
|
"if not state_energy.empty:\n",
|
||||||
|
" seds_available = state_energy['seds_series_count'].notna().sum() if 'seds_series_count' in state_energy.columns else 0\n",
|
||||||
|
" print(f'state energy rows: {len(state_energy):,}; SEDS rows represented: {seds_available:,}')\n",
|
||||||
|
"else:\n",
|
||||||
|
" print('state energy context file not found')\n",
|
||||||
|
"display(points.head())\n",
|
||||||
|
"display(clusters.head(10))\n",
|
||||||
|
"if not state_energy.empty:\n",
|
||||||
|
" display(state_energy.head(10))\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "6",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Map Helpers"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "7",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"CLUSTER_COLORS = [\n",
|
||||||
|
" '#2563eb', '#dc2626', '#16a34a', '#9333ea', '#ea580c', '#0891b2',\n",
|
||||||
|
" '#be123c', '#4f46e5', '#65a30d', '#c026d3', '#0f766e', '#b45309',\n",
|
||||||
|
"]\n",
|
||||||
|
"NOISE_COLOR = '#9ca3af'\n",
|
||||||
|
"CENTROID_COLOR = '#111827'\n",
|
||||||
|
"STATE_ENERGY_COLOR = '#f59e0b'\n",
|
||||||
|
"\n",
|
||||||
|
"cluster_info = clusters.set_index('cluster_id').to_dict('index')\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def clean_value(value):\n",
|
||||||
|
" if pd.isna(value):\n",
|
||||||
|
" return ''\n",
|
||||||
|
" return escape(str(value))\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def fmt_number(value, decimals=0, prefix='', suffix=''):\n",
|
||||||
|
" if pd.isna(value):\n",
|
||||||
|
" return ''\n",
|
||||||
|
" try:\n",
|
||||||
|
" value = float(value)\n",
|
||||||
|
" except (TypeError, ValueError):\n",
|
||||||
|
" return clean_value(value)\n",
|
||||||
|
" return f\"{prefix}{value:,.{decimals}f}{suffix}\"\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def cluster_color(cluster_id):\n",
|
||||||
|
" if cluster_id == -1:\n",
|
||||||
|
" return NOISE_COLOR\n",
|
||||||
|
" info = cluster_info.get(cluster_id, {})\n",
|
||||||
|
" rank = int(info.get('cluster_rank', cluster_id + 1))\n",
|
||||||
|
" return CLUSTER_COLORS[(rank - 1) % len(CLUSTER_COLORS)]\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def cluster_label_and_size(cluster_id):\n",
|
||||||
|
" if cluster_id == -1:\n",
|
||||||
|
" return 'Noise / isolated', '1', ''\n",
|
||||||
|
" info = cluster_info.get(cluster_id, {})\n",
|
||||||
|
" rank = int(info.get('cluster_rank', cluster_id + 1))\n",
|
||||||
|
" point_count = int(info.get('point_count', 0))\n",
|
||||||
|
" return f'Cluster ID {cluster_id}', f'{point_count:,}', f'Rank {rank} of {n_clusters} by size'\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def point_popup(row):\n",
|
||||||
|
" cluster_label, cluster_size, cluster_rank = cluster_label_and_size(row.cluster_id)\n",
|
||||||
|
" nearest = row.nearest_neighbor_km\n",
|
||||||
|
" nearest_text = f'{nearest:.2f} km' if pd.notna(nearest) else ''\n",
|
||||||
|
" title = clean_value(row.name) or clean_value(row.master_id)\n",
|
||||||
|
"\n",
|
||||||
|
" huc8_lines = ''\n",
|
||||||
|
" if hasattr(row, 'huc8') and pd.notna(row.huc8):\n",
|
||||||
|
" huc8_lines = f'''\n",
|
||||||
|
" <hr style=\"margin: 6px 0;\">\n",
|
||||||
|
" <strong>Watershed</strong><br>\n",
|
||||||
|
" HUC8: {clean_value(row.huc8)}<br>\n",
|
||||||
|
" Name: {clean_value(row.huc8_name)}<br>\n",
|
||||||
|
" States: {clean_value(row.huc8_states)}<br>\n",
|
||||||
|
" '''\n",
|
||||||
|
"\n",
|
||||||
|
" ruca_lines = ''\n",
|
||||||
|
" if hasattr(row, 'primary_ruca') and pd.notna(row.primary_ruca):\n",
|
||||||
|
" ruca_lines = f'''\n",
|
||||||
|
" <hr style=\"margin: 6px 0;\">\n",
|
||||||
|
" <strong>RUCA / tract context</strong><br>\n",
|
||||||
|
" RUCA band: {clean_value(row.ruca_band)}<br>\n",
|
||||||
|
" RUCA code: {fmt_number(row.primary_ruca)}<br>\n",
|
||||||
|
" {clean_value(row.primary_ruca_description)}<br>\n",
|
||||||
|
" Median HH income: {fmt_number(row.median_household_income, prefix='$')}<br>\n",
|
||||||
|
" Bachelor's+: {fmt_number(row.bachelor_or_higher_pct, 1, suffix='%')}<br>\n",
|
||||||
|
" Poverty: {fmt_number(row.poverty_rate, 1, suffix='%')}<br>\n",
|
||||||
|
" Non-Hispanic white: {fmt_number(row.non_hispanic_white_pct, 1, suffix='%')}<br>\n",
|
||||||
|
" '''\n",
|
||||||
|
"\n",
|
||||||
|
" energy_lines = ''\n",
|
||||||
|
" if hasattr(row, 'im3_projected_it_power_mw') and pd.notna(row.im3_projected_it_power_mw):\n",
|
||||||
|
" if hasattr(row, 'seds_series_count') and pd.notna(row.seds_series_count):\n",
|
||||||
|
" seds_note = f\"SEDS year: {fmt_number(row.seds_latest_year)}; series: {fmt_number(row.seds_series_count)}<br>\"\n",
|
||||||
|
" else:\n",
|
||||||
|
" seds_note = 'SEDS context: unavailable in seds_state_msn_year<br>'\n",
|
||||||
|
" energy_lines = f'''\n",
|
||||||
|
" <hr style=\"margin: 6px 0;\">\n",
|
||||||
|
" <strong>State energy demand context</strong><br>\n",
|
||||||
|
" IM3 projected IT power: {fmt_number(row.im3_projected_it_power_mw, suffix=' MW')}<br>\n",
|
||||||
|
" IM3 cooling water demand: {fmt_number(row.im3_cooling_water_demand_mgy, 1, suffix=' MGY')}<br>\n",
|
||||||
|
" IM3 water consumption: {fmt_number(row.im3_cooling_water_consumption_mgy, 1, suffix=' MGY')}<br>\n",
|
||||||
|
" IM3 avg siting score: {fmt_number(row.im3_avg_weighted_siting_score, 3)}<br>\n",
|
||||||
|
" {seds_note}\n",
|
||||||
|
" '''\n",
|
||||||
|
"\n",
|
||||||
|
" return folium.Popup(f'''\n",
|
||||||
|
" <div style=\"font-family: system-ui, sans-serif; min-width: 310px; max-width: 420px;\">\n",
|
||||||
|
" <strong>{title}</strong><br>\n",
|
||||||
|
" {clean_value(row.city)}, {clean_value(row.state)}<br>\n",
|
||||||
|
" <hr style=\"margin: 6px 0;\">\n",
|
||||||
|
" <strong>{cluster_label}</strong><br>\n",
|
||||||
|
" {cluster_rank}<br>\n",
|
||||||
|
" Cluster size: {cluster_size} data center(s)<br>\n",
|
||||||
|
" Source: {clean_value(row.source)}<br>\n",
|
||||||
|
" Operator: {clean_value(row.operator)}<br>\n",
|
||||||
|
" Nearest neighbor: {nearest_text}<br>\n",
|
||||||
|
" Master ID: {clean_value(row.master_id)}\n",
|
||||||
|
" {huc8_lines}\n",
|
||||||
|
" {ruca_lines}\n",
|
||||||
|
" {energy_lines}\n",
|
||||||
|
" </div>\n",
|
||||||
|
" ''', max_width=460)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def centroid_popup(row):\n",
|
||||||
|
" return folium.Popup(f'''\n",
|
||||||
|
" <div style=\"font-family: system-ui, sans-serif; min-width: 280px;\">\n",
|
||||||
|
" <strong>Cluster ID {int(row.cluster_id)}</strong><br>\n",
|
||||||
|
" Rank {int(row.cluster_rank)} of {n_clusters} by size<br>\n",
|
||||||
|
" <hr style=\"margin: 6px 0;\">\n",
|
||||||
|
" Points: {int(row.point_count):,}<br>\n",
|
||||||
|
" p50 radius: {row.radius_km_p50:.1f} km<br>\n",
|
||||||
|
" p90 radius: {row.radius_km_p90:.1f} km<br>\n",
|
||||||
|
" Max radius: {row.radius_km_max:.1f} km<br>\n",
|
||||||
|
" States: {clean_value(row.states)}<br>\n",
|
||||||
|
" Cities: {clean_value(row.cities)}<br>\n",
|
||||||
|
" Operators: {clean_value(row.operators)}\n",
|
||||||
|
" </div>\n",
|
||||||
|
" ''', max_width=420)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def huc8_style(feature):\n",
|
||||||
|
" count = feature['properties'].get('data_center_count') or 0\n",
|
||||||
|
" if count >= 100:\n",
|
||||||
|
" fill = '#075985'\n",
|
||||||
|
" elif count >= 50:\n",
|
||||||
|
" fill = '#0284c7'\n",
|
||||||
|
" elif count >= 20:\n",
|
||||||
|
" fill = '#38bdf8'\n",
|
||||||
|
" elif count >= 10:\n",
|
||||||
|
" fill = '#7dd3fc'\n",
|
||||||
|
" else:\n",
|
||||||
|
" fill = '#bae6fd'\n",
|
||||||
|
" return {'fillColor': fill, 'color': '#0369a1', 'weight': 1, 'fillOpacity': 0.22}\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def huc8_popup(feature):\n",
|
||||||
|
" p = feature['properties']\n",
|
||||||
|
" return folium.Popup(f'''\n",
|
||||||
|
" <div style=\"font-family: system-ui, sans-serif; min-width: 280px;\">\n",
|
||||||
|
" <strong>{clean_value(p.get('name'))}</strong><br>\n",
|
||||||
|
" HUC8: {clean_value(p.get('huc8'))}<br>\n",
|
||||||
|
" States: {clean_value(p.get('states'))}<br>\n",
|
||||||
|
" <hr style=\"margin: 6px 0;\">\n",
|
||||||
|
" Data centers: {fmt_number(p.get('data_center_count'))}<br>\n",
|
||||||
|
" Clustered DCs: {fmt_number(p.get('clustered_data_center_count'))}<br>\n",
|
||||||
|
" Distinct clusters: {fmt_number(p.get('cluster_count'))}<br>\n",
|
||||||
|
" Area: {fmt_number(p.get('areasqkm'), 0, suffix=' sq km')}\n",
|
||||||
|
" </div>\n",
|
||||||
|
" ''', max_width=360)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def state_energy_popup(row):\n",
|
||||||
|
" if hasattr(row, 'seds_series_count') and pd.notna(row.seds_series_count):\n",
|
||||||
|
" seds_note = f\"SEDS latest year: {fmt_number(row.seds_latest_year)}; series: {fmt_number(row.seds_series_count)}\"\n",
|
||||||
|
" else:\n",
|
||||||
|
" seds_note = 'SEDS context: unavailable in seds_state_msn_year'\n",
|
||||||
|
" return folium.Popup(f'''\n",
|
||||||
|
" <div style=\"font-family: system-ui, sans-serif; min-width: 280px;\">\n",
|
||||||
|
" <strong>{clean_value(row.state_code)} state energy context</strong><br>\n",
|
||||||
|
" Current data centers: {fmt_number(row.current_data_center_count)}<br>\n",
|
||||||
|
" <hr style=\"margin: 6px 0;\">\n",
|
||||||
|
" IM3 projected sites: {fmt_number(row.im3_project_count)}<br>\n",
|
||||||
|
" IM3 projected IT power: {fmt_number(row.im3_projected_it_power_mw, suffix=' MW')}<br>\n",
|
||||||
|
" IM3 cooling water demand: {fmt_number(row.im3_cooling_water_demand_mgy, 1, suffix=' MGY')}<br>\n",
|
||||||
|
" IM3 water consumption: {fmt_number(row.im3_cooling_water_consumption_mgy, 1, suffix=' MGY')}<br>\n",
|
||||||
|
" IM3 avg siting score: {fmt_number(row.im3_avg_weighted_siting_score, 3)}<br>\n",
|
||||||
|
" {seds_note}\n",
|
||||||
|
" </div>\n",
|
||||||
|
" ''', max_width=380)\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "8",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Build The Map"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "9",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"def build_cluster_map(points_df: pd.DataFrame, clusters_df: pd.DataFrame) -> folium.Map:\n",
|
||||||
|
" m = folium.Map(location=MAP_CENTER, zoom_start=MAP_ZOOM, tiles=BASE_TILES, control_scale=True)\n",
|
||||||
|
" plugins.Fullscreen(position='topleft').add_to(m)\n",
|
||||||
|
" plugins.MeasureControl(position='topleft', primary_length_unit='kilometers').add_to(m)\n",
|
||||||
|
" plugins.MiniMap(toggle_display=True, minimized=True).add_to(m)\n",
|
||||||
|
"\n",
|
||||||
|
" huc8_layer = folium.FeatureGroup(name='HUC8 watersheds with data centers', show=False)\n",
|
||||||
|
" state_energy_layer = folium.FeatureGroup(name='State energy demand context (IM3 / SEDS)', show=False)\n",
|
||||||
|
" clustered_layer = folium.FeatureGroup(name='Data centers: clustered', show=True)\n",
|
||||||
|
" noise_layer = folium.FeatureGroup(name='Data centers: noise / isolated', show=True)\n",
|
||||||
|
" centroid_layer = folium.FeatureGroup(name='Cluster centroids and p90 radius', show=True)\n",
|
||||||
|
"\n",
|
||||||
|
" if SHOW_HUC8_LAYER and huc8_geojson is not None:\n",
|
||||||
|
" folium.GeoJson(\n",
|
||||||
|
" huc8_geojson,\n",
|
||||||
|
" name='HUC8 watersheds with data centers',\n",
|
||||||
|
" style_function=huc8_style,\n",
|
||||||
|
" highlight_function=lambda feature: {'weight': 3, 'fillOpacity': 0.35},\n",
|
||||||
|
" tooltip=folium.GeoJsonTooltip(\n",
|
||||||
|
" fields=['name', 'huc8', 'data_center_count', 'cluster_count'],\n",
|
||||||
|
" aliases=['HUC8', 'Code', 'Data centers', 'Clusters'],\n",
|
||||||
|
" localize=True,\n",
|
||||||
|
" sticky=False,\n",
|
||||||
|
" ),\n",
|
||||||
|
" popup=huc8_popup,\n",
|
||||||
|
" ).add_to(huc8_layer)\n",
|
||||||
|
"\n",
|
||||||
|
" if SHOW_STATE_ENERGY_LAYER and not state_energy.empty:\n",
|
||||||
|
" for row in state_energy.dropna(subset=['map_latitude', 'map_longitude']).itertuples(index=False):\n",
|
||||||
|
" power = getattr(row, 'im3_projected_it_power_mw')\n",
|
||||||
|
" radius = 6 if pd.isna(power) else max(6, min(28, 4 + float(power) ** 0.5 / 2.4))\n",
|
||||||
|
" folium.CircleMarker(\n",
|
||||||
|
" location=[row.map_latitude, row.map_longitude],\n",
|
||||||
|
" radius=radius,\n",
|
||||||
|
" color='#92400e',\n",
|
||||||
|
" fill=True,\n",
|
||||||
|
" fill_color=STATE_ENERGY_COLOR,\n",
|
||||||
|
" fill_opacity=0.55,\n",
|
||||||
|
" weight=1.5,\n",
|
||||||
|
" popup=state_energy_popup(row),\n",
|
||||||
|
" tooltip=f'{row.state_code}: IM3 {fmt_number(power, suffix=\" MW\")}',\n",
|
||||||
|
" ).add_to(state_energy_layer)\n",
|
||||||
|
"\n",
|
||||||
|
" bounds = []\n",
|
||||||
|
" for row in points_df.itertuples(index=False):\n",
|
||||||
|
" cluster_label, cluster_size, _ = cluster_label_and_size(row.cluster_id)\n",
|
||||||
|
" marker = folium.CircleMarker(\n",
|
||||||
|
" location=[row.latitude, row.longitude],\n",
|
||||||
|
" radius=NOISE_RADIUS if row.cluster_id == -1 else CLUSTERED_RADIUS,\n",
|
||||||
|
" color=cluster_color(row.cluster_id),\n",
|
||||||
|
" fill=True,\n",
|
||||||
|
" fill_opacity=0.75,\n",
|
||||||
|
" weight=1,\n",
|
||||||
|
" popup=point_popup(row),\n",
|
||||||
|
" tooltip=f'{cluster_label}; size={cluster_size}',\n",
|
||||||
|
" )\n",
|
||||||
|
" if row.cluster_id == -1:\n",
|
||||||
|
" marker.add_to(noise_layer)\n",
|
||||||
|
" else:\n",
|
||||||
|
" marker.add_to(clustered_layer)\n",
|
||||||
|
" bounds.append([row.latitude, row.longitude])\n",
|
||||||
|
"\n",
|
||||||
|
" for row in clusters_df.itertuples(index=False):\n",
|
||||||
|
" color = cluster_color(int(row.cluster_id))\n",
|
||||||
|
" location = [row.centroid_latitude, row.centroid_longitude]\n",
|
||||||
|
" if SHOW_CENTROID_P90_CIRCLES and pd.notna(row.radius_km_p90):\n",
|
||||||
|
" folium.Circle(\n",
|
||||||
|
" location=location,\n",
|
||||||
|
" radius=float(row.radius_km_p90) * 1000,\n",
|
||||||
|
" color=color,\n",
|
||||||
|
" fill=False,\n",
|
||||||
|
" weight=1,\n",
|
||||||
|
" opacity=0.45,\n",
|
||||||
|
" ).add_to(centroid_layer)\n",
|
||||||
|
" folium.CircleMarker(\n",
|
||||||
|
" location=location,\n",
|
||||||
|
" radius=CENTROID_RADIUS,\n",
|
||||||
|
" color=CENTROID_COLOR,\n",
|
||||||
|
" fill=True,\n",
|
||||||
|
" fill_color=color,\n",
|
||||||
|
" fill_opacity=0.95,\n",
|
||||||
|
" weight=2,\n",
|
||||||
|
" popup=centroid_popup(row),\n",
|
||||||
|
" tooltip=f'Cluster {int(row.cluster_id)} centroid; {int(row.point_count):,} points',\n",
|
||||||
|
" ).add_to(centroid_layer)\n",
|
||||||
|
"\n",
|
||||||
|
" huc8_layer.add_to(m)\n",
|
||||||
|
" state_energy_layer.add_to(m)\n",
|
||||||
|
" clustered_layer.add_to(m)\n",
|
||||||
|
" noise_layer.add_to(m)\n",
|
||||||
|
" centroid_layer.add_to(m)\n",
|
||||||
|
" folium.LayerControl(collapsed=False).add_to(m)\n",
|
||||||
|
" if bounds:\n",
|
||||||
|
" m.fit_bounds(bounds, padding=(20, 20))\n",
|
||||||
|
" return m\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"cluster_map = build_cluster_map(points, clusters)\n",
|
||||||
|
"cluster_map\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "10",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Export HTML"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "11",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"cluster_map.save(MAP_HTML)\n",
|
||||||
|
"print('Wrote:', MAP_HTML.resolve())"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "12",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Feature Staging Area\n",
|
||||||
|
"\n",
|
||||||
|
"Tell me what you want to add next and I will build it here. Good candidates:\n",
|
||||||
|
"- filters by source/operator/state/cluster size\n",
|
||||||
|
"- toggle layers for top-N clusters\n",
|
||||||
|
"- water-stress overlays on top of the HUC8 layer\n",
|
||||||
|
"- generator capacity / fuel mix overlays around each DC\n",
|
||||||
|
"- opposition cases overlay\n",
|
||||||
|
"- cluster labels or summary panels\n",
|
||||||
|
"- downloadable GeoJSON exports\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": ".venv",
|
||||||
|
"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.14.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
64385
output/enhanced_master_data_center_spatial_clusters_map.html
Normal file
64385
output/enhanced_master_data_center_spatial_clusters_map.html
Normal file
File diff suppressed because one or more lines are too long
1
output/master_data_center_huc8_watersheds.geojson
Normal file
1
output/master_data_center_huc8_watersheds.geojson
Normal file
File diff suppressed because one or more lines are too long
1834
output/master_data_center_map_context.csv
Normal file
1834
output/master_data_center_map_context.csv
Normal file
File diff suppressed because it is too large
Load Diff
48
output/master_data_center_state_energy_context.csv
Normal file
48
output/master_data_center_state_energy_context.csv
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
state_code,current_data_center_count,map_latitude,map_longitude,im3_project_count,im3_projected_it_power_mw,im3_cooling_energy_demand_mwh,im3_cooling_water_demand_mgy,im3_cooling_water_consumption_mgy,im3_avg_weighted_siting_score,im3_avg_normalized_locational_cost,im3_avg_normalized_gravity_score,seds_latest_year,seds_series_count,seds_selected_total_value
|
||||||
|
VA,378,38.81180123513644,-77.48445358366453,76.0,2736.0,0.0,3307.495679999999,2645.9966960000033,0.16464524066748473,0.2366938493068311,0.09259663202813818,,,
|
||||||
|
TX,162,31.509920265059563,-97.62641372338256,49.0,1764.0,0.0,2132.4643200000023,1705.9715539999997,0.00350041121207623,0.00018486843693128847,0.00681595398722118,,,
|
||||||
|
CA,147,36.6743835566224,-121.03746691674534,21.0,756.0,0.0,913.91328,731.1306659999999,0.009498709102457833,0.00963112411543682,0.009366294089478828,,,
|
||||||
|
IL,61,41.83977411585855,-87.98518544988711,16.0,576.0,0.0,696.31488,557.051936,0.06235921050437614,0.06746532657235553,0.05725309443639674,,,
|
||||||
|
OR,145,45.4770347075944,-120.9390505417634,14.0,504.0,0.0,609.27552,487.4204439999999,0.016703061443370994,0.010005638753125727,0.023400484133616232,,,
|
||||||
|
AZ,69,33.35143139388246,-111.8599097648615,14.0,504.0,685908.0,293.75784,235.00628579999997,0.22894142277116208,0.2160051752565124,0.24187767028581172,,,
|
||||||
|
IA,65,41.44566071909779,-94.26149737259419,14.0,504.0,0.0,609.27552,487.4204439999999,0.09821083960768505,0.1497288191285527,0.0466928600868174,,,
|
||||||
|
GA,50,33.75917632869643,-84.34900369690746,14.0,504.0,0.0,609.27552,487.4204439999999,0.07573816651050262,0.08431831295122912,0.06715802006977611,,,
|
||||||
|
WA,93,47.40058038575267,-120.65389054931781,11.0,396.0,189216.0,391.67712,313.34171399999997,0.008602897054563028,0.012114925824482491,0.005090868284643536,,,
|
||||||
|
PA,17,40.51861689987747,-77.14067159955545,10.0,360.0,0.0,435.1968,348.15745999999996,0.025611264607888918,0.046432172534256695,0.00479035668152121,,,
|
||||||
|
NJ,62,40.63657863750276,-74.33774092959604,9.0,324.0,0.0,391.67712,313.34171399999997,0.07951105237280956,0.04415559548391192,0.11486650926170719,,,
|
||||||
|
NY,48,42.0986494544888,-75.86702411728405,9.0,324.0,0.0,391.67712,313.34171399999997,0.033844266542779576,0.015562967240912412,0.052125565844646754,,,
|
||||||
|
NE,26,41.19044423571503,-96.21083342167539,8.0,288.0,0.0,348.15744,278.525968,0.023213461680844788,0.00994173426451155,0.036485189097178045,,,
|
||||||
|
ND,3,46.6009985,-97.43026833333334,8.0,288.0,0.0,348.15744,278.525968,0.1754766419137694,0.2637904066656519,0.08716287716188698,,,
|
||||||
|
NV,41,37.52887284950246,-116.95999289168654,7.0,252.0,0.0,304.63776,243.71022199999996,0.09370158681927372,0.13838366134523744,0.049019512293309996,,,
|
||||||
|
NC,31,35.515951937711286,-80.45619313915424,6.0,216.0,0.0,261.11808,208.89447599999997,0.017336791819723883,0.02155215545264995,0.013121428186797816,,,
|
||||||
|
OH,103,40.11289672434338,-82.9632959538231,5.0,180.0,0.0,217.5984,174.07872999999998,0.028173405916738482,0.027854599205723286,0.02849221262775366,,,
|
||||||
|
UT,17,40.27992287878221,-111.9244948339973,5.0,180.0,0.0,217.5984,174.07872999999998,0.071711762460987,0.045980594538791664,0.09744293038318233,,,
|
||||||
|
WY,22,41.2694301778197,-104.88440403454051,4.0,144.0,283824.0,43.51968,34.815746,0.14755687340545556,0.21318301702746534,0.08193072978344579,,,
|
||||||
|
SC,13,33.14909429058469,-80.10080479843208,4.0,144.0,0.0,174.07872,139.262984,0.1016155103641099,0.06996685870746874,0.13326416202075103,,,
|
||||||
|
TN,32,36.04624181258759,-86.94623740179134,3.0,108.0,0.0,130.55904,104.447238,0.10169703586035657,0.18104455608133466,0.02234951563937843,,,
|
||||||
|
FL,29,27.411439250882815,-81.13143447486134,3.0,108.0,283824.0,0.0,0.0,0.08202502654847077,0.1294847364539594,0.0345653166429822,,,
|
||||||
|
CO,27,39.51836862226617,-104.85288139276082,3.0,108.0,141912.0,65.27952,52.223619,0.005559275742971499,0.0064795144918877,0.004639036994055333,,,
|
||||||
|
AL,12,34.30076806872618,-86.35712687934489,3.0,108.0,0.0,130.55904,104.447238,0.0009726334592856833,0.00014293747388154497,0.001802329444689833,,,
|
||||||
|
KY,5,37.94511731391368,-85.43993183815397,3.0,108.0,0.0,130.55904,104.447238,0.0698208727274249,0.023908323736558768,0.11573342171829104,,,
|
||||||
|
MO,17,39.117889133868196,-93.5372434088145,2.0,72.0,0.0,87.03936,69.631492,0.0382817539994971,0.047006409422381296,0.0295570985766129,,,
|
||||||
|
MA,12,42.37838745843559,-71.37421289245025,2.0,72.0,0.0,87.03936,69.631492,0.0727448359176194,0.0192291585456955,0.12626051328954324,,,
|
||||||
|
OK,11,35.77427623773164,-96.24089812456369,2.0,72.0,0.0,87.03936,69.631492,0.05236513716444575,0.0689640790147675,0.035766195314124,,,
|
||||||
|
MN,15,44.980069700569416,-93.17958896920497,1.0,36.0,0.0,43.51968,34.815746,0.1809237101049204,0.2082387823395508,0.1536086378702902,,,
|
||||||
|
MI,13,42.46082727383233,-83.57434337241001,1.0,36.0,0.0,43.51968,34.815746,0.0566642628531991,0.0423349445013257,0.0709935812050726,,,
|
||||||
|
MT,3,46.16069827338828,-109.23879441517472,1.0,36.0,0.0,43.51968,34.815746,0.1285720285331266,0.2250467365641875,0.0320973205020656,,,
|
||||||
|
NM,17,34.84316652063309,-106.64757455359201,,,,,,,,,,,
|
||||||
|
WI,17,43.21578182334325,-88.79371779090025,,,,,,,,,,,
|
||||||
|
MD,16,39.252795209689744,-76.78697385304773,,,,,,,,,,,
|
||||||
|
IN,10,40.603351555794575,-86.52645175699342,,,,,,,,,,,
|
||||||
|
KS,8,38.71977107866478,-95.84686174722735,,,,,,,,,,,
|
||||||
|
CT,7,41.257283034952174,-73.19770117764624,,,,,,,,,,,
|
||||||
|
MS,5,32.39151784524035,-90.32332510585482,,,,,,,,,,,
|
||||||
|
ID,4,43.267254168631325,-113.90666627597611,,,,,,,,,,,
|
||||||
|
NH,4,43.031734736446104,-71.04302199169723,,,,,,,,,,,
|
||||||
|
WV,3,38.77825380000001,-80.518261,,,,,,,,,,,
|
||||||
|
LA,3,30.956981986997324,-91.664033994836,,,,,,,,,,,
|
||||||
|
DC,2,38.89899305000002,-77.032767,,,,,,,,,,,
|
||||||
|
ME,2,43.77645494131252,-70.09226452273563,,,,,,,,,,,
|
||||||
|
PR,2,18.432131450000014,-66.08033840000002,,,,,,,,,,,
|
||||||
|
AR,2,35.002725817804865,-92.0928146845728,,,,,,,,,,,
|
||||||
|
SD,2,43.60344233895939,-96.80197609920154,,,,,,,,,,,
|
||||||
|
Reference in New Issue
Block a user