Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -43,7 +43,7 @@ def load_data():
|
|
| 43 |
# Sanitize Headers (removes hidden spaces)
|
| 44 |
df.columns = df.columns.str.strip()
|
| 45 |
|
| 46 |
-
# Validation
|
| 47 |
required_cols = ['Power (MW)', 'Carbon Intensity', 'Annual Million tCO2']
|
| 48 |
missing = [c for c in required_cols if c not in df.columns]
|
| 49 |
if missing:
|
|
@@ -64,13 +64,28 @@ def load_data():
|
|
| 64 |
df['Carbon Intensity'] = df['Carbon Intensity'].apply(clean_numeric)
|
| 65 |
df['Annual Million tCO2'] = df['Annual Million tCO2'].apply(clean_numeric)
|
| 66 |
|
| 67 |
-
# ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
# Formula: MW * 8760 hours * (Intensity kg/MWh / 1000 to get tonnes) / 1,000,000 to get Million Tonnes
|
| 69 |
# We calculate this to double-check the CSV's reported numbers
|
| 70 |
df['Calculated_Mt'] = (df['Power (MW)'] * 8760 * df['Carbon Intensity']) / 1e9
|
| 71 |
|
| 72 |
# Use the Reported number, but normalize it (Handle the 13,093 vs 13.1 issue)
|
| 73 |
-
# If the number is > 100, it's likely in Kilotonnes, so divide by 1000
|
| 74 |
df['Emissions_Mt'] = df['Annual Million tCO2'].apply(lambda x: x / 1000 if x > 100 else x)
|
| 75 |
|
| 76 |
# --- Geocoding (Manual Overrides for missing Lat/Long) ---
|
|
@@ -86,7 +101,7 @@ def load_data():
|
|
| 86 |
mask = df['Project'].astype(str).str.contains(key, case=False, na=False)
|
| 87 |
df.loc[mask, ['Latitude', 'Longitude']] = coords
|
| 88 |
|
| 89 |
-
# Parse DMS coordinates
|
| 90 |
def dms_to_dd(val):
|
| 91 |
if isinstance(val, str) and '°' in val:
|
| 92 |
try:
|
|
@@ -101,7 +116,6 @@ def load_data():
|
|
| 101 |
df[col] = df[col].apply(dms_to_dd)
|
| 102 |
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 103 |
|
| 104 |
-
# Drop rows that still have no location
|
| 105 |
df = df.dropna(subset=['Latitude', 'Longitude'])
|
| 106 |
|
| 107 |
# --- Enrichment for Tooltip ---
|
|
@@ -128,22 +142,26 @@ df = load_data()
|
|
| 128 |
st.sidebar.header("🌍 Frontier AI Emissions")
|
| 129 |
st.sidebar.markdown("Filter the map to analyze the carbon footprint of planned AI infrastructure.")
|
| 130 |
|
| 131 |
-
# Filters
|
|
|
|
|
|
|
| 132 |
grid_filter = st.sidebar.multiselect(
|
| 133 |
"Connection Type",
|
| 134 |
-
options=
|
| 135 |
-
default=
|
| 136 |
)
|
| 137 |
|
|
|
|
|
|
|
| 138 |
owner_filter = st.sidebar.multiselect(
|
| 139 |
"Owner",
|
| 140 |
-
options=
|
| 141 |
-
default=
|
| 142 |
)
|
| 143 |
|
| 144 |
# Apply filters
|
| 145 |
filtered_df = df[
|
| 146 |
-
(df['
|
| 147 |
(df['Owner'].isin(owner_filter))
|
| 148 |
]
|
| 149 |
|
|
@@ -158,7 +176,7 @@ st.sidebar.markdown("### 📊 Aggregate Impact")
|
|
| 158 |
|
| 159 |
col1, col2 = st.sidebar.columns(2)
|
| 160 |
col1.metric("Total Power", f"{total_power:.1f} GW", help="Total capacity of visible projects")
|
| 161 |
-
col2.metric("Annual Emissions", f"{total_emissions:.1f} Mt", help="Million Tonnes
|
| 162 |
|
| 163 |
st.sidebar.markdown(f"""
|
| 164 |
<div class="metric-card">
|
|
@@ -173,8 +191,8 @@ st.sidebar.markdown(f"**Avg Carbon Intensity:** {avg_intensity:.0f} kg/MWh")
|
|
| 173 |
st.title("The Carbon Footprint of Frontier AI")
|
| 174 |
st.markdown(
|
| 175 |
"This map visualizes the annual emissions of major planned AI data centers. "
|
| 176 |
-
"**Bubble size** represents CO₂ emissions. **Color** indicates grid status "
|
| 177 |
-
"(<span style='color:#FF4136'><b>Red = Off-Grid/
|
| 178 |
unsafe_allow_html=True
|
| 179 |
)
|
| 180 |
|
|
@@ -195,20 +213,19 @@ layer = pdk.Layer(
|
|
| 195 |
get_line_color=[0, 0, 0],
|
| 196 |
)
|
| 197 |
|
| 198 |
-
# Tooltip
|
| 199 |
tooltip = {
|
| 200 |
"html": """
|
| 201 |
-
<div style="font-family: sans-serif; padding:
|
| 202 |
-
<
|
| 203 |
-
<hr style="border-top: 1px solid #555;">
|
| 204 |
<b>Owner:</b> {Owner}<br/>
|
| 205 |
-
<b>Location:</b> {Location}<br/>
|
| 206 |
<b>Power:</b> {Power (MW)} MW<br/>
|
| 207 |
-
<b>Status:</b> {
|
| 208 |
<br/>
|
| 209 |
-
<b style="font-size: 1.1em; color: #ffcccb;">
|
| 210 |
-
<i style="font-size: 0.
|
| 211 |
-
<hr style="border-top: 1px dashed #555;">
|
| 212 |
<b>🚗 Equal to:</b> {Cars_Equivalent_Millions} Million Cars<br/>
|
| 213 |
<b>🏭 Equal to:</b> {Coal_Plants_Equivalent} Coal Power Plants
|
| 214 |
</div>
|
|
@@ -217,13 +234,16 @@ tooltip = {
|
|
| 217 |
"backgroundColor": "#1E1E1E",
|
| 218 |
"border": "1px solid #333",
|
| 219 |
"borderRadius": "8px",
|
| 220 |
-
"color": "white"
|
|
|
|
| 221 |
}
|
| 222 |
}
|
| 223 |
|
| 224 |
# Render Map
|
|
|
|
|
|
|
| 225 |
st.pydeck_chart(pdk.Deck(
|
| 226 |
-
map_style=
|
| 227 |
initial_view_state=pdk.ViewState(
|
| 228 |
latitude=39.8,
|
| 229 |
longitude=-98.6,
|
|
@@ -238,6 +258,6 @@ st.pydeck_chart(pdk.Deck(
|
|
| 238 |
st.markdown("---")
|
| 239 |
st.caption(
|
| 240 |
"**Methodology:** Emissions calculated based on publicly stated power capacity (MW) and regional/source-specific carbon intensity. "
|
| 241 |
-
"Car equivalents assume 4.6 metric tonnes CO₂ per passenger vehicle per year (EPA). "
|
| 242 |
-
"Coal plant equivalent assumes ~4.0 MtCO
|
| 243 |
)
|
|
|
|
| 43 |
# Sanitize Headers (removes hidden spaces)
|
| 44 |
df.columns = df.columns.str.strip()
|
| 45 |
|
| 46 |
+
# Validation
|
| 47 |
required_cols = ['Power (MW)', 'Carbon Intensity', 'Annual Million tCO2']
|
| 48 |
missing = [c for c in required_cols if c not in df.columns]
|
| 49 |
if missing:
|
|
|
|
| 64 |
df['Carbon Intensity'] = df['Carbon Intensity'].apply(clean_numeric)
|
| 65 |
df['Annual Million tCO2'] = df['Annual Million tCO2'].apply(clean_numeric)
|
| 66 |
|
| 67 |
+
# --- CLEAN OWNER NAMES ---
|
| 68 |
+
# Remove "#confident", "#likely", etc.
|
| 69 |
+
if 'Owner' in df.columns:
|
| 70 |
+
df['Owner'] = df['Owner'].astype(str).str.split('#').str[0].str.strip()
|
| 71 |
+
|
| 72 |
+
# --- SIMPLIFY GRID STATUS ---
|
| 73 |
+
# Create a clean category for the filter (Grid vs Off-Grid vs Hybrid)
|
| 74 |
+
def simplify_status(status):
|
| 75 |
+
s = str(status).lower()
|
| 76 |
+
if 'off-grid' in s or 'gas' in s: return "Off-Grid / Fossil"
|
| 77 |
+
elif 'hybrid' in s or 'nuclear' in s: return "Hybrid / Nuclear"
|
| 78 |
+
elif 'grid' in s: return "Grid Connected"
|
| 79 |
+
else: return "Unknown"
|
| 80 |
+
|
| 81 |
+
df['Simple_Connection'] = df['Grid Status'].apply(simplify_status)
|
| 82 |
+
|
| 83 |
+
# --- MATH CHECK ---
|
| 84 |
# Formula: MW * 8760 hours * (Intensity kg/MWh / 1000 to get tonnes) / 1,000,000 to get Million Tonnes
|
| 85 |
# We calculate this to double-check the CSV's reported numbers
|
| 86 |
df['Calculated_Mt'] = (df['Power (MW)'] * 8760 * df['Carbon Intensity']) / 1e9
|
| 87 |
|
| 88 |
# Use the Reported number, but normalize it (Handle the 13,093 vs 13.1 issue)
|
|
|
|
| 89 |
df['Emissions_Mt'] = df['Annual Million tCO2'].apply(lambda x: x / 1000 if x > 100 else x)
|
| 90 |
|
| 91 |
# --- Geocoding (Manual Overrides for missing Lat/Long) ---
|
|
|
|
| 101 |
mask = df['Project'].astype(str).str.contains(key, case=False, na=False)
|
| 102 |
df.loc[mask, ['Latitude', 'Longitude']] = coords
|
| 103 |
|
| 104 |
+
# Parse DMS coordinates
|
| 105 |
def dms_to_dd(val):
|
| 106 |
if isinstance(val, str) and '°' in val:
|
| 107 |
try:
|
|
|
|
| 116 |
df[col] = df[col].apply(dms_to_dd)
|
| 117 |
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 118 |
|
|
|
|
| 119 |
df = df.dropna(subset=['Latitude', 'Longitude'])
|
| 120 |
|
| 121 |
# --- Enrichment for Tooltip ---
|
|
|
|
| 142 |
st.sidebar.header("🌍 Frontier AI Emissions")
|
| 143 |
st.sidebar.markdown("Filter the map to analyze the carbon footprint of planned AI infrastructure.")
|
| 144 |
|
| 145 |
+
# Filters: Connection Type (Simplified)
|
| 146 |
+
# We sort them to ensure consistent order
|
| 147 |
+
connection_options = sorted(df['Simple_Connection'].unique())
|
| 148 |
grid_filter = st.sidebar.multiselect(
|
| 149 |
"Connection Type",
|
| 150 |
+
options=connection_options,
|
| 151 |
+
default=connection_options
|
| 152 |
)
|
| 153 |
|
| 154 |
+
# Filters: Owner (Cleaned)
|
| 155 |
+
owner_options = sorted(df['Owner'].unique())
|
| 156 |
owner_filter = st.sidebar.multiselect(
|
| 157 |
"Owner",
|
| 158 |
+
options=owner_options,
|
| 159 |
+
default=owner_options
|
| 160 |
)
|
| 161 |
|
| 162 |
# Apply filters
|
| 163 |
filtered_df = df[
|
| 164 |
+
(df['Simple_Connection'].isin(grid_filter)) &
|
| 165 |
(df['Owner'].isin(owner_filter))
|
| 166 |
]
|
| 167 |
|
|
|
|
| 176 |
|
| 177 |
col1, col2 = st.sidebar.columns(2)
|
| 178 |
col1.metric("Total Power", f"{total_power:.1f} GW", help="Total capacity of visible projects")
|
| 179 |
+
col2.metric("Annual Emissions", f"{total_emissions:.1f} Mt", help="Million Tonnes CO2e/year")
|
| 180 |
|
| 181 |
st.sidebar.markdown(f"""
|
| 182 |
<div class="metric-card">
|
|
|
|
| 191 |
st.title("The Carbon Footprint of Frontier AI")
|
| 192 |
st.markdown(
|
| 193 |
"This map visualizes the annual emissions of major planned AI data centers. "
|
| 194 |
+
"**Bubble size** represents CO₂e emissions. **Color** indicates grid status "
|
| 195 |
+
"(<span style='color:#FF4136'><b>Red = Off-Grid/Fossil</b></span>, <span style='color:#0074D9'><b>Blue = Grid Connected</b></span>).",
|
| 196 |
unsafe_allow_html=True
|
| 197 |
)
|
| 198 |
|
|
|
|
| 213 |
get_line_color=[0, 0, 0],
|
| 214 |
)
|
| 215 |
|
| 216 |
+
# Tooltip (Updated for Data Center Name & Subscript)
|
| 217 |
tooltip = {
|
| 218 |
"html": """
|
| 219 |
+
<div style="font-family: sans-serif; padding: 8px; color: white; max-width: 250px;">
|
| 220 |
+
<h4 style="margin:0; padding-bottom:5px;">{Data Center Name}</h4>
|
| 221 |
+
<hr style="border-top: 1px solid #555; margin: 5px 0;">
|
| 222 |
<b>Owner:</b> {Owner}<br/>
|
|
|
|
| 223 |
<b>Power:</b> {Power (MW)} MW<br/>
|
| 224 |
+
<b>Status:</b> {Simple_Connection}<br/>
|
| 225 |
<br/>
|
| 226 |
+
<b style="font-size: 1.1em; color: #ffcccb;">Emissions: {Emissions_Mt} MtCO<sub>2</sub>e</b><br/>
|
| 227 |
+
<i style="font-size: 0.8em; color: #ccc;">(Intensity: {Carbon Intensity} kg/MWh)</i>
|
| 228 |
+
<hr style="border-top: 1px dashed #555; margin: 5px 0;">
|
| 229 |
<b>🚗 Equal to:</b> {Cars_Equivalent_Millions} Million Cars<br/>
|
| 230 |
<b>🏭 Equal to:</b> {Coal_Plants_Equivalent} Coal Power Plants
|
| 231 |
</div>
|
|
|
|
| 234 |
"backgroundColor": "#1E1E1E",
|
| 235 |
"border": "1px solid #333",
|
| 236 |
"borderRadius": "8px",
|
| 237 |
+
"color": "white",
|
| 238 |
+
"zIndex": "1000"
|
| 239 |
}
|
| 240 |
}
|
| 241 |
|
| 242 |
# Render Map
|
| 243 |
+
# FIX: Removed 'mapbox://' style to prevent black screen.
|
| 244 |
+
# Using map_style=None uses the default adaptable map.
|
| 245 |
st.pydeck_chart(pdk.Deck(
|
| 246 |
+
map_style=None,
|
| 247 |
initial_view_state=pdk.ViewState(
|
| 248 |
latitude=39.8,
|
| 249 |
longitude=-98.6,
|
|
|
|
| 258 |
st.markdown("---")
|
| 259 |
st.caption(
|
| 260 |
"**Methodology:** Emissions calculated based on publicly stated power capacity (MW) and regional/source-specific carbon intensity. "
|
| 261 |
+
"Car equivalents assume 4.6 metric tonnes CO₂e per passenger vehicle per year (EPA). "
|
| 262 |
+
"Coal plant equivalent assumes ~4.0 MtCO₂e/year for a typical plant."
|
| 263 |
)
|