Spaces:
Sleeping
Sleeping
Commit
Β·
ebae247
1
Parent(s):
0c2c1d0
add heatmap layer
Browse files- .gitattributes +1 -0
- .gitignore +1 -2
- app.py +236 -216
- data/IWI_by_admin2.geojson +0 -0
- data/band_1.geojson +3 -0
- data/ground_truth.geojson +0 -0
- data/mean_IWI_by_admin.geojson +0 -0
- data/no_somaliland.geojson +0 -0
- helpers/reduce_precision.py +12 -0
.gitattributes
CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
34 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
35 |
wealth_map.tif filter=lfs diff=lfs merge=lfs -text
|
|
|
|
33 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
34 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
35 |
wealth_map.tif filter=lfs diff=lfs merge=lfs -text
|
36 |
+
*.geojson filter=lfs diff=lfs merge=lfs -text
|
.gitignore
CHANGED
@@ -1,4 +1,3 @@
|
|
1 |
.venv/
|
2 |
__pycache__
|
3 |
-
.DS_Store
|
4 |
-
keys.py
|
|
|
1 |
.venv/
|
2 |
__pycache__
|
3 |
+
.DS_Store
|
|
app.py
CHANGED
@@ -6,11 +6,11 @@ import matplotlib.pyplot as plt
|
|
6 |
import rasterio
|
7 |
from rasterio.plot import show
|
8 |
import geopandas as gpd
|
9 |
-
from ipyleaflet import Map, TileLayer, basemaps, ColorMap, RasterLayer, LegendControl, GeoJSON
|
10 |
from shinywidgets import output_widget, register_widget
|
11 |
import plotnine as p9
|
12 |
-
|
13 |
-
|
14 |
import os
|
15 |
import base64
|
16 |
import tempfile
|
@@ -18,6 +18,11 @@ import json
|
|
18 |
from datetime import datetime
|
19 |
from helpers.fetch_data import fetch_data
|
20 |
from helpers.residuals import get_residual_plot
|
|
|
|
|
|
|
|
|
|
|
21 |
|
22 |
# ------------------------------
|
23 |
# 1. Data & Config
|
@@ -25,21 +30,38 @@ from helpers.residuals import get_residual_plot
|
|
25 |
|
26 |
# Define time periods corresponding to each band in the GeoTIFF
|
27 |
time_periods = ["1990β1992", "1993β1995", "1996β1998", "1999β2001", "2002β2004",
|
28 |
-
|
29 |
|
30 |
# Load GeoTIFF data (multi-band)
|
31 |
-
# Note: In a real application, you'd need to adjust this path
|
32 |
wealth_stack = rasterio.open("wealth_map.tif")
|
33 |
|
34 |
-
|
|
|
35 |
country_json = json.load(a)
|
36 |
-
|
|
|
37 |
IWI_df = pd.read_csv('data/mean_IWI_by_country.csv')
|
38 |
|
|
|
39 |
residual_data = pd.read_csv('data/residual_by_country.csv')
|
40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
|
42 |
|
|
|
|
|
|
|
|
|
43 |
|
44 |
# Function to clean up out-of-range values and get values
|
45 |
def get_clean_values(src, band_idx=1):
|
@@ -48,6 +70,7 @@ def get_clean_values(src, band_idx=1):
|
|
48 |
band_data[(band_data <= 0) | (band_data > 1)] = np.nan
|
49 |
return band_data
|
50 |
|
|
|
51 |
# Get all values across all bands for quantiles
|
52 |
all_vals = []
|
53 |
for i in range(1, wealth_stack.count + 1):
|
@@ -59,7 +82,7 @@ q_breaks_legend = np.quantile(all_vals, np.linspace(0, 1, 6))
|
|
59 |
q_breaks = np.quantile(all_vals, np.linspace(0, 1, 11))
|
60 |
|
61 |
# Get raster bounds for proper positioning on the map
|
62 |
-
bounds = [[wealth_stack.bounds.bottom, wealth_stack.bounds.left],
|
63 |
[wealth_stack.bounds.top, wealth_stack.bounds.right]]
|
64 |
|
65 |
# Load improvement data (change in IWI by state/province)
|
@@ -225,106 +248,129 @@ app_ui = ui.page_fluid(
|
|
225 |
),
|
226 |
ui.page_navbar(
|
227 |
ui.nav_panel("Wealth Map",
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
307 |
ui.nav_panel("Improvement Data",
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
|
|
|
|
|
|
320 |
ui.nav_panel("Trends Over Time",
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
|
|
328 |
title=ui.HTML(
|
329 |
"<span style='font-weight: 600; font-size: 16px;'>"
|
330 |
"<a href='http://aidevlab.org' target='_blank' "
|
@@ -339,9 +385,27 @@ app_ui = ui.page_fluid(
|
|
339 |
# ------------------------------
|
340 |
# 3. Server logic
|
341 |
# ------------------------------
|
|
|
|
|
342 |
def server(input, output, session):
|
343 |
# Initialize the map widget
|
344 |
m = Map(center=(0, 20), zoom=3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
345 |
geo_json = GeoJSON(
|
346 |
data=country_json,
|
347 |
style={
|
@@ -351,189 +415,136 @@ def server(input, output, session):
|
|
351 |
'color': 'white', 'dashArray': '0', 'fillOpacity': 0.5
|
352 |
}
|
353 |
)
|
354 |
-
m.add_layer(geo_json)
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
# Register the map widget with Shiny
|
359 |
-
map_widget = register_widget("
|
360 |
-
|
361 |
# Store clicked point values
|
362 |
clicked_point_vals = reactive.Value(None)
|
363 |
selected_country = reactive.Value(None)
|
364 |
|
365 |
admin_layer = reactive.Value(None)
|
366 |
selected_admin = reactive.Value(None)
|
367 |
-
|
368 |
# Get the currently selected raster layer
|
369 |
@reactive.Calc
|
370 |
def selected_raster():
|
371 |
band_idx = input.time_index()
|
372 |
return get_clean_values(wealth_stack, band_idx)
|
373 |
-
|
374 |
# Display selected time period
|
375 |
@output
|
376 |
@render.text
|
377 |
def current_year_range():
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
#
|
382 |
-
|
383 |
-
# palette_name = input.color_palette()
|
384 |
-
# if palette_name == "blue":
|
385 |
-
# return Blues_9.hex_colors
|
386 |
-
# elif palette_name == "orange":
|
387 |
-
# return OrRd_9.hex_colors
|
388 |
-
# elif palette_name == "red":
|
389 |
-
# return Reds_9.hex_colors
|
390 |
-
# elif palette_name == "purple":
|
391 |
-
# return PuBuGn_9.hex_colors
|
392 |
-
# else: # Spectral
|
393 |
-
# return Spectral_10.hex_colors
|
394 |
-
|
395 |
-
# Create a RasterLayer for the map
|
396 |
-
# @reactive.effect
|
397 |
# @reactive.event(input.time_index, input.color_palette, input.opacity)
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
# temp_path = tmp.name
|
410 |
-
|
411 |
-
# # Create a new GeoTIFF with the selected band
|
412 |
-
# with rasterio.open(
|
413 |
-
# temp_path,
|
414 |
-
# 'w',
|
415 |
-
# driver='GTiff',
|
416 |
-
# height=raster_data.shape[0],
|
417 |
-
# width=raster_data.shape[1],
|
418 |
-
# count=1,
|
419 |
-
# dtype=raster_data.dtype,
|
420 |
-
# crs=wealth_stack.crs,
|
421 |
-
# transform=wealth_stack.transform,
|
422 |
-
# ) as dst:
|
423 |
-
# dst.write(raster_data, 1)
|
424 |
-
|
425 |
-
# # Create a ColorMap for the raster
|
426 |
-
# colormap = ColorMap(
|
427 |
-
# vmin=q_breaks[0],
|
428 |
-
# vmax=q_breaks[-1]
|
429 |
-
# # palette=get_palette()
|
430 |
-
# )
|
431 |
-
|
432 |
-
# # Add the raster layer to the map
|
433 |
-
# raster_layer = RasterLayer(
|
434 |
-
# url=temp_path,
|
435 |
-
# bounds=bounds,
|
436 |
-
# colormap=colormap,
|
437 |
-
# opacity=input.opacity()
|
438 |
-
# )
|
439 |
-
|
440 |
-
# m.add_layer(raster_layer)
|
441 |
-
|
442 |
-
# # Add legend
|
443 |
-
# for ctrl in m.controls:
|
444 |
-
# if isinstance(ctrl, LegendControl):
|
445 |
-
# m.remove_control(ctrl)
|
446 |
-
|
447 |
-
# legend = LegendControl({"IWI": colormap}, position="bottomright")
|
448 |
-
# m.add_control(legend)
|
449 |
-
|
450 |
# Handle map clicks
|
451 |
@reactive.effect
|
452 |
def _():
|
453 |
# Set up click event handler
|
454 |
-
def handle_map_click(event
|
455 |
-
|
456 |
-
|
|
|
457 |
longitudes = [coords[y][0] for y in range(len(coords))]
|
458 |
-
|
459 |
-
|
|
|
|
|
460 |
|
461 |
-
selected_country.set(country_name)
|
462 |
|
463 |
-
|
|
|
464 |
m.center = centroid
|
465 |
m.zoom = 5
|
466 |
-
|
467 |
# Register click handler
|
468 |
geo_json.on_click(handle_map_click)
|
469 |
-
|
470 |
# Display value boxes
|
471 |
@output
|
472 |
@render.text
|
473 |
def highest_iwi():
|
474 |
raster_data = selected_raster()
|
475 |
return f"{np.nanmax(raster_data):.3f}"
|
476 |
-
|
477 |
@output
|
478 |
@render.text
|
479 |
def lowest_iwi():
|
480 |
raster_data = selected_raster()
|
481 |
return f"{np.nanmin(raster_data):.3f}"
|
482 |
-
|
483 |
@output
|
484 |
@render.text
|
485 |
def avg_iwi():
|
486 |
raster_data = selected_raster()
|
487 |
return f"{np.nanmean(raster_data):.3f}"
|
488 |
-
|
489 |
# Generate trend plot for mean IWI across Africa
|
490 |
@output
|
491 |
@render.plot
|
492 |
def trend_plot():
|
493 |
-
fig, ax = plt.subplots(figsize=(
|
494 |
-
ax.plot(range(len(time_periods)), band_means, marker='o',
|
|
|
495 |
ax.set_xticks(range(len(time_periods)))
|
496 |
ax.set_xticklabels(time_periods, rotation=45, ha="right")
|
497 |
ax.set_ylabel("Mean IWI")
|
498 |
ax.set_ylim(0.1, 0.3)
|
499 |
ax.set_title("Average IWI Over Time (Africa)")
|
500 |
ax.grid(True, linestyle='--', alpha=0.7)
|
501 |
-
|
502 |
plt.tight_layout()
|
503 |
return fig
|
504 |
-
|
505 |
# Generate histogram plot
|
506 |
@output
|
507 |
@render.plot
|
508 |
def iwi_residuals():
|
509 |
country_name = selected_country.get()
|
510 |
fig = get_residual_plot(country_name, residual_data)
|
511 |
-
return fig
|
512 |
-
|
513 |
# Plot time series at clicked location
|
514 |
@output
|
515 |
@render.plot
|
516 |
def clicked_ts_plot():
|
517 |
country_name = selected_country.get()
|
518 |
-
|
519 |
fig, ax = plt.subplots(figsize=(10, 4))
|
520 |
-
|
521 |
if country_name is None:
|
522 |
ax.text(0.5, 0.5, "Click on the map to see the IWI time-series here.",
|
523 |
-
|
524 |
-
|
525 |
else:
|
526 |
-
ax.plot(IWI_df['Band_Number'], IWI_df[country_name],
|
527 |
-
|
|
|
528 |
ax.set_xticklabels(time_periods, rotation=45)
|
529 |
ax.set_ylabel("IWI (0 to 1)")
|
530 |
ax.set_ylim(0, 1)
|
531 |
ax.set_title(f"Time Series of IWI in {country_name}")
|
532 |
ax.grid(True, linestyle='--', alpha=0.7)
|
533 |
-
|
534 |
plt.tight_layout()
|
535 |
return fig
|
536 |
-
|
537 |
# Display improvement data table
|
538 |
@output
|
539 |
@render.data_frame
|
@@ -543,15 +554,24 @@ def server(input, output, session):
|
|
543 |
filters=True,
|
544 |
height="800px"
|
545 |
)
|
546 |
-
|
547 |
# Download CSV handler
|
548 |
-
@
|
|
|
549 |
def download_data():
|
550 |
return improvement_data.to_csv(index=False)
|
551 |
-
|
552 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
553 |
|
554 |
# ------------------------------
|
555 |
# 4. Create and run the app
|
556 |
# ------------------------------
|
557 |
-
app = App(app_ui, server)
|
|
|
6 |
import rasterio
|
7 |
from rasterio.plot import show
|
8 |
import geopandas as gpd
|
9 |
+
from ipyleaflet import Map, TileLayer, basemaps, ColorMap, RasterLayer, LegendControl, GeoJSON, MarkerCluster, Marker, DivIcon, Polygon
|
10 |
from shinywidgets import output_widget, register_widget
|
11 |
import plotnine as p9
|
12 |
+
import matplotlib.cm as cm
|
13 |
+
import matplotlib.colors as colors
|
14 |
import os
|
15 |
import base64
|
16 |
import tempfile
|
|
|
18 |
from datetime import datetime
|
19 |
from helpers.fetch_data import fetch_data
|
20 |
from helpers.residuals import get_residual_plot
|
21 |
+
from helpers.reduce_precision import reduce_coordinate_precision
|
22 |
+
from shapely.geometry import shape
|
23 |
+
from shapely.geometry import mapping
|
24 |
+
from shapely.geometry import Polygon as ShapelyPolygon
|
25 |
+
import io
|
26 |
|
27 |
# ------------------------------
|
28 |
# 1. Data & Config
|
|
|
30 |
|
31 |
# Define time periods corresponding to each band in the GeoTIFF
|
32 |
time_periods = ["1990β1992", "1993β1995", "1996β1998", "1999β2001", "2002β2004",
|
33 |
+
"2005β2007", "2008β2010", "2011β2013", "2014β2016", "2017β2019"]
|
34 |
|
35 |
# Load GeoTIFF data (multi-band)
|
|
|
36 |
wealth_stack = rasterio.open("wealth_map.tif")
|
37 |
|
38 |
+
#load country data
|
39 |
+
with open('data/no_somaliland.geojson') as a:
|
40 |
country_json = json.load(a)
|
41 |
+
|
42 |
+
#load IWI by country
|
43 |
IWI_df = pd.read_csv('data/mean_IWI_by_country.csv')
|
44 |
|
45 |
+
#load residual data
|
46 |
residual_data = pd.read_csv('data/residual_by_country.csv')
|
47 |
|
48 |
+
selected_map = reactive.Value(None)
|
49 |
+
|
50 |
+
#load band 1 data by default
|
51 |
+
with open('data/band_1.geojson') as w:
|
52 |
+
band_1_data = json.load(w)
|
53 |
+
band_1_data = reduce_coordinate_precision(band_1_data, precision=5) #reduce precision to aid in rendering
|
54 |
+
|
55 |
+
IWI_values = [feature['properties']['IWI']
|
56 |
+
for feature in band_1_data['features']]
|
57 |
+
norm = colors.Normalize(vmin=min(IWI_values), vmax=max(IWI_values))
|
58 |
+
colormap = cm.get_cmap("plasma")
|
59 |
|
60 |
|
61 |
+
def get_color(iwi):
|
62 |
+
rgba = colormap(norm(iwi)) # Convert to RGBA
|
63 |
+
return colors.to_hex(rgba) # Convert to HEX
|
64 |
+
|
65 |
|
66 |
# Function to clean up out-of-range values and get values
|
67 |
def get_clean_values(src, band_idx=1):
|
|
|
70 |
band_data[(band_data <= 0) | (band_data > 1)] = np.nan
|
71 |
return band_data
|
72 |
|
73 |
+
|
74 |
# Get all values across all bands for quantiles
|
75 |
all_vals = []
|
76 |
for i in range(1, wealth_stack.count + 1):
|
|
|
82 |
q_breaks = np.quantile(all_vals, np.linspace(0, 1, 11))
|
83 |
|
84 |
# Get raster bounds for proper positioning on the map
|
85 |
+
bounds = [[wealth_stack.bounds.bottom, wealth_stack.bounds.left],
|
86 |
[wealth_stack.bounds.top, wealth_stack.bounds.right]]
|
87 |
|
88 |
# Load improvement data (change in IWI by state/province)
|
|
|
248 |
),
|
249 |
ui.page_navbar(
|
250 |
ui.nav_panel("Wealth Map",
|
251 |
+
ui.layout_sidebar(
|
252 |
+
ui.sidebar(
|
253 |
+
ui.h4("Map Controls"),
|
254 |
+
ui.input_switch(
|
255 |
+
"SelectedMap", "Enable Country View", False),
|
256 |
+
ui.input_slider(
|
257 |
+
"time_index",
|
258 |
+
"Select Time Period (Years):",
|
259 |
+
min=1,
|
260 |
+
max=len(time_periods),
|
261 |
+
value=1,
|
262 |
+
step=1,
|
263 |
+
animate=True
|
264 |
+
),
|
265 |
+
ui.strong("Currently Selected: "),
|
266 |
+
ui.output_text("current_year_range", inline=True),
|
267 |
+
ui.input_select(
|
268 |
+
"color_palette",
|
269 |
+
"Select Color Palette:",
|
270 |
+
{
|
271 |
+
"blue": "blue",
|
272 |
+
"red": "red",
|
273 |
+
"orange": "orange",
|
274 |
+
"purple": "purple",
|
275 |
+
"Spectral": "Spectral (Brewer)"
|
276 |
+
},
|
277 |
+
selected="red"
|
278 |
+
),
|
279 |
+
ui.input_slider(
|
280 |
+
"opacity",
|
281 |
+
"Map Opacity:",
|
282 |
+
min=0.2,
|
283 |
+
max=1,
|
284 |
+
value=0.8,
|
285 |
+
step=0.1
|
286 |
+
),
|
287 |
+
ui.accordion(ui.accordion_panel(
|
288 |
+
'How it works', ui.HTML("<p>These wealth-index predictions are AI-generated by a"
|
289 |
+
"sequence-aware neural network trained on 30 years of <em>Demographic and Health Surveys (DHS)</em> ground-truth data.</p"
|
290 |
+
"<ul><li>π 57,100+ geo-referenced survey points from DHS</li> <li>βοΈ Multi-spectral satellite bands & raster-to-vector feature extraction</li><li>π― Calibrated & validated with held-out DHS clusters (1990β2019)</li></ul>")
|
291 |
+
), id="map_instructions", open=False, multiple=False),
|
292 |
+
ui.HTML(share_button_html)
|
293 |
+
),
|
294 |
+
ui.layout_column_wrap(
|
295 |
+
ui.value_box(
|
296 |
+
"Highest IWI",
|
297 |
+
ui.output_text("highest_iwi"),
|
298 |
+
showcase=ui.tags.i(class_="fa fa-arrow-up"),
|
299 |
+
theme="success"
|
300 |
+
),
|
301 |
+
ui.value_box(
|
302 |
+
"Lowest IWI",
|
303 |
+
ui.output_text("lowest_iwi"),
|
304 |
+
showcase=ui.tags.i(class_="fa fa-arrow-down"),
|
305 |
+
theme="danger"
|
306 |
+
),
|
307 |
+
ui.value_box(
|
308 |
+
"Average IWI",
|
309 |
+
ui.output_text("avg_iwi"),
|
310 |
+
showcase=ui.tags.i(
|
311 |
+
class_="fa fa-balance-scale"),
|
312 |
+
theme="primary"
|
313 |
+
),
|
314 |
+
width=1/3
|
315 |
+
),
|
316 |
+
ui.layout_column_wrap(
|
317 |
+
ui.card(
|
318 |
+
ui.card_header(
|
319 |
+
ui.h3("Wealth Map of Africa", class_="title-text")),
|
320 |
+
output_widget("country_map"),
|
321 |
+
ui.p(
|
322 |
+
"Click anywhere on the map to view the time-series of IWI for that specific location (shown below).")
|
323 |
+
),
|
324 |
+
ui.card(
|
325 |
+
ui.card_header(
|
326 |
+
ui.h3("Time Series at Clicked Location", class_="subtitle-text")),
|
327 |
+
ui.output_plot("clicked_ts_plot"),
|
328 |
+
ui.p(
|
329 |
+
"Click on the map to see the full IWI time-series (1990β2019) for that location."),
|
330 |
+
ui.download_button(
|
331 |
+
"download_country_data", "Download CSV", icon="download"),
|
332 |
+
|
333 |
+
)
|
334 |
+
),
|
335 |
+
|
336 |
+
ui.card(
|
337 |
+
ui.card_header(ui.h3(
|
338 |
+
"Ground Truth vs. Prediction Residual Distribution (Selected Country)", class_="subtitle-text")),
|
339 |
+
ui.output_plot("iwi_residuals"),
|
340 |
+
ui.p(
|
341 |
+
"This chart shows the distribution of residuals between ground truth and predicted IWI values based on the selected country."),
|
342 |
+
ui.strong(
|
343 |
+
"Note: wealth estimates for areas without human settlements have been excluded from the analysis."),
|
344 |
+
ui.HTML(
|
345 |
+
"<a href='https://doi.org/10.24963/ijcai.2023/684' target='_blank'>[Paper PDF]</a>")
|
346 |
+
)
|
347 |
+
)
|
348 |
+
),
|
349 |
ui.nav_panel("Improvement Data",
|
350 |
+
ui.layout_columns(
|
351 |
+
ui.card(
|
352 |
+
ui.card_header(
|
353 |
+
ui.h3("Poverty Improvement by State", class_="title-text")),
|
354 |
+
ui.p("This table shows the estimated improvement in mean IWI between 1990β1992 and 2017β2019 for each province in Africa. "
|
355 |
+
"The 'Improvement' column indicates the change in IWI over this period. You can sort or filter the table, "
|
356 |
+
"and use the download button to export the data."),
|
357 |
+
ui.download_button(
|
358 |
+
"download_data", "Download CSV", icon="download"),
|
359 |
+
ui.card(ui.output_data_frame(
|
360 |
+
"improvement_table")),
|
361 |
+
|
362 |
+
)
|
363 |
+
)
|
364 |
+
),
|
365 |
ui.nav_panel("Trends Over Time",
|
366 |
+
ui.card(
|
367 |
+
ui.card_header(
|
368 |
+
ui.h3("Average Wealth Index Across Africa Over Time", class_="title-text")),
|
369 |
+
ui.p("This chart aggregates the mean IWI across all of Africa in each of the ten time periods. "
|
370 |
+
"It provides a high-level view of how wealth (as measured by IWI) has changed over time."),
|
371 |
+
ui.output_plot("trend_plot")
|
372 |
+
)
|
373 |
+
),
|
374 |
title=ui.HTML(
|
375 |
"<span style='font-weight: 600; font-size: 16px;'>"
|
376 |
"<a href='http://aidevlab.org' target='_blank' "
|
|
|
385 |
# ------------------------------
|
386 |
# 3. Server logic
|
387 |
# ------------------------------
|
388 |
+
|
389 |
+
|
390 |
def server(input, output, session):
|
391 |
# Initialize the map widget
|
392 |
m = Map(center=(0, 20), zoom=3)
|
393 |
+
for feature in band_1_data["features"]:
|
394 |
+
iwi = feature["properties"]["IWI"]
|
395 |
+
feature["properties"]["style"] = {
|
396 |
+
"color": get_color(iwi),
|
397 |
+
"fillColor": get_color(iwi), # Fill color based on IWI
|
398 |
+
"fillOpacity": 0.7,
|
399 |
+
"weight": 1
|
400 |
+
}
|
401 |
+
|
402 |
+
band_1_json = GeoJSON(data=band_1_data,
|
403 |
+
style={'radius': 2, 'opacity': 0.8, 'weight': 1.9},
|
404 |
+
point_style={'radius': 3},
|
405 |
+
name='Release'
|
406 |
+
)
|
407 |
+
m.add_layer(band_1_json)
|
408 |
+
|
409 |
geo_json = GeoJSON(
|
410 |
data=country_json,
|
411 |
style={
|
|
|
415 |
'color': 'white', 'dashArray': '0', 'fillOpacity': 0.5
|
416 |
}
|
417 |
)
|
|
|
|
|
|
|
|
|
418 |
# Register the map widget with Shiny
|
419 |
+
map_widget = register_widget("country_map", m)
|
420 |
+
|
421 |
# Store clicked point values
|
422 |
clicked_point_vals = reactive.Value(None)
|
423 |
selected_country = reactive.Value(None)
|
424 |
|
425 |
admin_layer = reactive.Value(None)
|
426 |
selected_admin = reactive.Value(None)
|
427 |
+
|
428 |
# Get the currently selected raster layer
|
429 |
@reactive.Calc
|
430 |
def selected_raster():
|
431 |
band_idx = input.time_index()
|
432 |
return get_clean_values(wealth_stack, band_idx)
|
433 |
+
|
434 |
# Display selected time period
|
435 |
@output
|
436 |
@render.text
|
437 |
def current_year_range():
|
438 |
+
# Adjust for 0-based indexing
|
439 |
+
return time_periods[input.time_index() - 1]
|
440 |
+
|
441 |
+
# Create a Country layer for the map
|
442 |
+
@reactive.effect
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
443 |
# @reactive.event(input.time_index, input.color_palette, input.opacity)
|
444 |
+
def _():
|
445 |
+
if input.SelectedMap() == True:
|
446 |
+
m.remove_layer(band_1_json)
|
447 |
+
m.add_layer(geo_json)
|
448 |
+
return m
|
449 |
+
elif input.SelectedMap() == False:
|
450 |
+
for layer in m.layers:
|
451 |
+
if layer == geo_json:
|
452 |
+
m.remove_layer(layer)
|
453 |
+
m.add_layer(band_1_json)
|
454 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
455 |
# Handle map clicks
|
456 |
@reactive.effect
|
457 |
def _():
|
458 |
# Set up click event handler
|
459 |
+
def handle_map_click(event=None, feature=None, **kwargs):
|
460 |
+
# extract feature coordinates
|
461 |
+
coords = feature['geometry']['coordinates'][0]
|
462 |
+
latitudes = [coords[x][1] for x in range(len(coords))]
|
463 |
longitudes = [coords[y][0] for y in range(len(coords))]
|
464 |
+
# find country name
|
465 |
+
country_name = feature['properties']['sovereignt']
|
466 |
+
# find country abbreviation
|
467 |
+
country_abbrev = feature['properties']['sov_a3']
|
468 |
|
469 |
+
selected_country.set(country_name) # set the country name
|
470 |
|
471 |
+
# lock view position to the country's centroid
|
472 |
+
centroid = (np.mean(latitudes), np.mean(longitudes))
|
473 |
m.center = centroid
|
474 |
m.zoom = 5
|
475 |
+
|
476 |
# Register click handler
|
477 |
geo_json.on_click(handle_map_click)
|
478 |
+
|
479 |
# Display value boxes
|
480 |
@output
|
481 |
@render.text
|
482 |
def highest_iwi():
|
483 |
raster_data = selected_raster()
|
484 |
return f"{np.nanmax(raster_data):.3f}"
|
485 |
+
|
486 |
@output
|
487 |
@render.text
|
488 |
def lowest_iwi():
|
489 |
raster_data = selected_raster()
|
490 |
return f"{np.nanmin(raster_data):.3f}"
|
491 |
+
|
492 |
@output
|
493 |
@render.text
|
494 |
def avg_iwi():
|
495 |
raster_data = selected_raster()
|
496 |
return f"{np.nanmean(raster_data):.3f}"
|
497 |
+
|
498 |
# Generate trend plot for mean IWI across Africa
|
499 |
@output
|
500 |
@render.plot
|
501 |
def trend_plot():
|
502 |
+
fig, ax = plt.subplots(figsize=(10, 4))
|
503 |
+
ax.plot(range(len(time_periods)), band_means, marker='o',
|
504 |
+
color="darkorange", linewidth=2, markersize=6)
|
505 |
ax.set_xticks(range(len(time_periods)))
|
506 |
ax.set_xticklabels(time_periods, rotation=45, ha="right")
|
507 |
ax.set_ylabel("Mean IWI")
|
508 |
ax.set_ylim(0.1, 0.3)
|
509 |
ax.set_title("Average IWI Over Time (Africa)")
|
510 |
ax.grid(True, linestyle='--', alpha=0.7)
|
511 |
+
|
512 |
plt.tight_layout()
|
513 |
return fig
|
514 |
+
|
515 |
# Generate histogram plot
|
516 |
@output
|
517 |
@render.plot
|
518 |
def iwi_residuals():
|
519 |
country_name = selected_country.get()
|
520 |
fig = get_residual_plot(country_name, residual_data)
|
521 |
+
return fig
|
522 |
+
|
523 |
# Plot time series at clicked location
|
524 |
@output
|
525 |
@render.plot
|
526 |
def clicked_ts_plot():
|
527 |
country_name = selected_country.get()
|
528 |
+
|
529 |
fig, ax = plt.subplots(figsize=(10, 4))
|
530 |
+
|
531 |
if country_name is None:
|
532 |
ax.text(0.5, 0.5, "Click on the map to see the IWI time-series here.",
|
533 |
+
horizontalalignment='center', verticalalignment='center',
|
534 |
+
transform=ax.transAxes, fontsize=14)
|
535 |
else:
|
536 |
+
ax.plot(IWI_df['Band_Number'], IWI_df[country_name],
|
537 |
+
marker='o', color="darkorange", linewidth=2, markersize=6)
|
538 |
+
ax.set_xticks(range(1, len(IWI_df['Band_Number'])+1))
|
539 |
ax.set_xticklabels(time_periods, rotation=45)
|
540 |
ax.set_ylabel("IWI (0 to 1)")
|
541 |
ax.set_ylim(0, 1)
|
542 |
ax.set_title(f"Time Series of IWI in {country_name}")
|
543 |
ax.grid(True, linestyle='--', alpha=0.7)
|
544 |
+
|
545 |
plt.tight_layout()
|
546 |
return fig
|
547 |
+
|
548 |
# Display improvement data table
|
549 |
@output
|
550 |
@render.data_frame
|
|
|
554 |
filters=True,
|
555 |
height="800px"
|
556 |
)
|
557 |
+
|
558 |
# Download CSV handler
|
559 |
+
@output
|
560 |
+
@render.download(filename=lambda: f"poverty_improvement_{datetime.now().strftime('%Y-%m-%d')}.csv")
|
561 |
def download_data():
|
562 |
return improvement_data.to_csv(index=False)
|
563 |
+
|
564 |
+
@output
|
565 |
+
@render.download(filename=lambda: f"{selected_country.get()}_IWI.csv")
|
566 |
+
async def download_country_data():
|
567 |
+
country_name = selected_country.get()
|
568 |
+
buf = io.StringIO()
|
569 |
+
country_data = pd.DataFrame(IWI_df[country_name])
|
570 |
+
country_data.to_csv(buf, index=False)
|
571 |
+
yield buf.getvalue()
|
572 |
+
|
573 |
|
574 |
# ------------------------------
|
575 |
# 4. Create and run the app
|
576 |
# ------------------------------
|
577 |
+
app = App(app_ui, server)
|
data/IWI_by_admin2.geojson
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
data/band_1.geojson
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:3622bf0268d2a7141d48495366c4e5c259882c6219cace3d246d336299b567ac
|
3 |
+
size 136321755
|
data/ground_truth.geojson
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
data/mean_IWI_by_admin.geojson
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
data/no_somaliland.geojson
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
helpers/reduce_precision.py
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
|
3 |
+
def reduce_coordinate_precision(geojson_data, precision=5):
|
4 |
+
"""Reduces the decimal precision of coordinates in GeoJSON data."""
|
5 |
+
features = geojson_data.get('features', [])
|
6 |
+
for feature in features:
|
7 |
+
if feature['geometry']['type'] == 'Polygon':
|
8 |
+
coords = feature['geometry']['coordinates']
|
9 |
+
rounded_coords = [[[round(coord, precision) for coord in point] for point in ring] for ring in coords]
|
10 |
+
feature['geometry']['coordinates'] = rounded_coords
|
11 |
+
|
12 |
+
return geojson_data
|