Skip to content

Commit 7e7feb7

Browse files
committed
feat: enhance shelter statistics generation by categorizing distances into ranges and adding new visualizations for distance to shelter and local density within 200m
1 parent 4e9542f commit 7e7feb7

1 file changed

Lines changed: 195 additions & 27 deletions

File tree

scripts/generate_shelter_statistics.py

Lines changed: 195 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,28 +1063,35 @@ def create_local_density_distribution(theme, local_density_data, theme_name='tuf
10631063
if closest_distances_deg is None:
10641064
return
10651065

1066-
# Convert to meters and categorize by distance thresholds
1066+
# Convert to meters and categorize by distance ranges
10671067
distance_thresholds = [100, 150, 200, 250, 300]
10681068
distance_thresholds_deg = [d / 100000 for d in distance_thresholds]
10691069

1070-
# Categorize buildings by closest shelter distance
1070+
# Categorize buildings by closest shelter distance into ranges
10711071
categories = []
10721072
for i, dist_deg in enumerate(closest_distances_deg):
10731073
if dist_deg > distance_thresholds_deg[-1]:
10741074
categories.append('no_shelter')
1075-
else:
1076-
# Find which threshold it falls into (closest one)
1077-
for j, threshold in enumerate(distance_thresholds_deg):
1078-
if dist_deg <= threshold:
1079-
categories.append(f'{distance_thresholds[j]}m')
1080-
break
1075+
elif dist_deg <= distance_thresholds_deg[0]:
1076+
categories.append('<100m')
1077+
elif dist_deg <= distance_thresholds_deg[1]:
1078+
categories.append('100-150m')
1079+
elif dist_deg <= distance_thresholds_deg[2]:
1080+
categories.append('150-200m')
1081+
elif dist_deg <= distance_thresholds_deg[3]:
1082+
categories.append('200-250m')
1083+
else: # dist_deg <= distance_thresholds_deg[4] (300m)
1084+
categories.append('250-300m')
10811085

10821086
categories = np.array(categories)
10831087

10841088
# Separate densities by category
10851089
density_by_category = {}
1086-
for threshold in distance_thresholds:
1087-
density_by_category[f'{threshold}m'] = local_densities[categories == f'{threshold}m']
1090+
density_by_category['<100m'] = local_densities[categories == '<100m']
1091+
density_by_category['100-150m'] = local_densities[categories == '100-150m']
1092+
density_by_category['150-200m'] = local_densities[categories == '150-200m']
1093+
density_by_category['200-250m'] = local_densities[categories == '200-250m']
1094+
density_by_category['250-300m'] = local_densities[categories == '250-300m']
10881095
density_by_category['no_shelter'] = local_densities[categories == 'no_shelter']
10891096

10901097
_, ax = plt.subplots(figsize=(10, 6))
@@ -1116,14 +1123,14 @@ def create_local_density_distribution(theme, local_density_data, theme_name='tuf
11161123

11171124
data_layers_all = [
11181125
density_by_category['no_shelter'],
1119-
density_by_category['300m'],
1120-
density_by_category['250m'],
1121-
density_by_category['200m'],
1122-
density_by_category['150m'],
1123-
density_by_category['100m'],
1126+
density_by_category['250-300m'],
1127+
density_by_category['200-250m'],
1128+
density_by_category['150-200m'],
1129+
density_by_category['100-150m'],
1130+
density_by_category['<100m'],
11241131
]
11251132

1126-
labels_all = ['No shelter', '300m', '250m', '200m', '150m', '100m']
1133+
labels_all = ['No shelter', '250-300m', '200-250m', '150-200m', '100-150m', '<100m']
11271134

11281135
ax.hist(data_layers_all, bins=bins,
11291136
color=colors_all, alpha=0.8, edgecolor='none', stacked=True, label=labels_all)
@@ -1149,11 +1156,11 @@ def create_local_density_distribution(theme, local_density_data, theme_name='tuf
11491156

11501157
# Combine all shelter categories into one "has shelter" category
11511158
has_shelter_densities = np.concatenate([
1152-
density_by_category['100m'],
1153-
density_by_category['150m'],
1154-
density_by_category['200m'],
1155-
density_by_category['250m'],
1156-
density_by_category['300m']
1159+
density_by_category['<100m'],
1160+
density_by_category['100-150m'],
1161+
density_by_category['150-200m'],
1162+
density_by_category['200-250m'],
1163+
density_by_category['250-300m']
11571164
])
11581165

11591166
colors_simple = [
@@ -1193,11 +1200,169 @@ def create_local_density_distribution(theme, local_density_data, theme_name='tuf
11931200
print(f" Median: {np.median(local_densities):.1f}")
11941201
print(f" Max: {np.max(local_densities):.0f}")
11951202
total = len(categories)
1196-
for threshold in [100, 150, 200, 250, 300]:
1197-
count = np.sum(categories == f'{threshold}m')
1198-
print(f" {threshold}m: {count:,} ({count/total*100:.1f}%)")
1199-
no_shelter_count = np.sum(categories == 'no_shelter')
1200-
print(f" No shelter: {no_shelter_count:,} ({no_shelter_count/total*100:.1f}%)")
1203+
for label in ['<100m', '100-150m', '150-200m', '200-250m', '250-300m', 'no_shelter']:
1204+
count = np.sum(categories == label)
1205+
display_label = 'No shelter' if label == 'no_shelter' else label
1206+
print(f" {display_label}: {count:,} ({count/total*100:.1f}%)")
1207+
1208+
1209+
def create_distance_to_shelter_line(theme, local_density_data):
1210+
"""Create line graph showing number of buildings vs distance to nearest shelter"""
1211+
if not local_density_data or not local_density_data['building_coords']:
1212+
return
1213+
1214+
building_coords = local_density_data['building_coords']
1215+
1216+
# Load existing shelters
1217+
try:
1218+
with open('data/shelters.geojson', 'r', encoding='utf-8') as f:
1219+
shelters_data = json.load(f)
1220+
existing_shelters = []
1221+
for feature in shelters_data['features']:
1222+
props = feature['properties']
1223+
status = props.get('status', '').strip()
1224+
if status.startswith('Built'):
1225+
coords = feature['geometry']['coordinates']
1226+
existing_shelters.append([coords[0], coords[1]])
1227+
except FileNotFoundError:
1228+
existing_shelters = []
1229+
1230+
if not existing_shelters:
1231+
return
1232+
1233+
# Calculate closest shelter distance for each building
1234+
print(" Calculating closest shelter distances for line graph...")
1235+
closest_distances_deg = calculate_closest_shelter_distance(building_coords, existing_shelters, max_radius_m=500)
1236+
if closest_distances_deg is None:
1237+
return
1238+
1239+
# Convert to meters
1240+
closest_distances_m = np.array(closest_distances_deg) * 100000
1241+
1242+
# Filter to buildings with shelter within 500m
1243+
has_shelter_mask = closest_distances_m <= 500
1244+
distances_with_shelter = closest_distances_m[has_shelter_mask]
1245+
1246+
# Create bins for distance ranges
1247+
bins = np.arange(0, 501, 10) # 10m bins up to 500m
1248+
1249+
# Count buildings in each bin
1250+
counts, bin_edges = np.histogram(distances_with_shelter, bins=bins)
1251+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
1252+
1253+
_, ax = plt.subplots(figsize=(10, 6))
1254+
1255+
# Create line graph
1256+
ax.plot(bin_centers, counts, color=theme['bar_color'], linewidth=2, marker='o', markersize=3)
1257+
1258+
ax.set_xlabel('Distance to Nearest Shelter (m)')
1259+
ax.set_ylabel('Number of Buildings')
1260+
ax.set_title('Buildings by Distance to Nearest Shelter', pad=15)
1261+
ax.set_xlim(0, 500)
1262+
1263+
setup_tufte_axis(ax)
1264+
ax.grid(True, axis='y', linewidth=0.5)
1265+
ax.set_axisbelow(True)
1266+
1267+
plt.tight_layout()
1268+
plt.savefig(f'output/10_distance_to_shelter_line{theme["suffix"]}.jpg', dpi=300, bbox_inches='tight',
1269+
facecolor=theme['background'], format='jpeg')
1270+
plt.close()
1271+
1272+
# Print statistics
1273+
print(f" Distance to shelter statistics:")
1274+
print(f" Buildings with shelter ≤500m: {len(distances_with_shelter):,}")
1275+
print(f" Mean distance: {np.mean(distances_with_shelter):.1f}m")
1276+
print(f" Median distance: {np.median(distances_with_shelter):.1f}m")
1277+
1278+
1279+
def create_local_density_200m(theme, local_density_data, theme_name='tufte'):
1280+
"""Create stacked histogram for 200m distance (buildings and shelters within 200m)"""
1281+
if not local_density_data or not local_density_data['building_coords']:
1282+
return
1283+
1284+
building_coords = local_density_data['building_coords']
1285+
1286+
# Load existing shelters
1287+
try:
1288+
with open('data/shelters.geojson', 'r', encoding='utf-8') as f:
1289+
shelters_data = json.load(f)
1290+
existing_shelters = []
1291+
for feature in shelters_data['features']:
1292+
props = feature['properties']
1293+
status = props.get('status', '').strip()
1294+
if status.startswith('Built'):
1295+
coords = feature['geometry']['coordinates']
1296+
existing_shelters.append([coords[0], coords[1]])
1297+
except FileNotFoundError:
1298+
existing_shelters = []
1299+
1300+
if not existing_shelters:
1301+
return
1302+
1303+
# Calculate buildings within 200m
1304+
print(" Calculating buildings within 200m...")
1305+
local_densities_200m = calculate_local_building_density(building_coords, radius_m=200)
1306+
if local_densities_200m is None:
1307+
return
1308+
1309+
local_densities_200m = np.array(local_densities_200m)
1310+
1311+
# Calculate closest shelter distance within 200m
1312+
print(" Calculating closest shelter distances within 200m...")
1313+
closest_distances_deg = calculate_closest_shelter_distance(building_coords, existing_shelters, max_radius_m=200)
1314+
if closest_distances_deg is None:
1315+
return
1316+
1317+
# Categorize buildings
1318+
has_shelter = closest_distances_deg <= (200 / 100000)
1319+
densities_with_shelter = local_densities_200m[has_shelter]
1320+
densities_without_shelter = local_densities_200m[~has_shelter]
1321+
1322+
_, ax = plt.subplots(figsize=(10, 6))
1323+
1324+
# Create bins
1325+
max_density = int(np.max(local_densities_200m))
1326+
bins = np.arange(0, max_density + 5, 5)
1327+
1328+
# Create stacked histogram
1329+
colors_simple = [
1330+
theme['existing_color'], # no_shelter (red)
1331+
theme['optimal_color'], # has shelter (green)
1332+
]
1333+
1334+
data_layers = [
1335+
densities_without_shelter,
1336+
densities_with_shelter,
1337+
]
1338+
1339+
labels = ['No shelter', 'Has shelter (≤200m)']
1340+
1341+
ax.hist(data_layers, bins=bins,
1342+
color=colors_simple, alpha=0.8, edgecolor='none', stacked=True, label=labels)
1343+
1344+
ax.set_xlabel('Buildings within 200m')
1345+
ax.set_ylabel('Number of Buildings')
1346+
ax.set_title('Buildings within 200m of Each Building', pad=15)
1347+
1348+
# Add legend
1349+
ax.legend(loc='upper right', fontsize=8, frameon=False)
1350+
1351+
setup_tufte_axis(ax)
1352+
ax.grid(True, axis='y', linewidth=0.5)
1353+
ax.set_axisbelow(True)
1354+
1355+
plt.tight_layout()
1356+
plt.savefig(f'output/09c_local_density_200m{theme["suffix"]}.jpg', dpi=300, bbox_inches='tight',
1357+
facecolor=theme['background'], format='jpeg')
1358+
plt.close()
1359+
1360+
# Print statistics
1361+
total = len(has_shelter)
1362+
print(f" Local density statistics (buildings within 200m):")
1363+
print(f" Mean: {np.mean(local_densities_200m):.1f}")
1364+
print(f" Buildings with shelter ≤200m: {np.sum(has_shelter):,} ({np.sum(has_shelter)/total*100:.1f}%)")
1365+
print(f" Buildings without shelter: {np.sum(~has_shelter):,} ({np.sum(~has_shelter)/total*100:.1f}%)")
12011366

12021367

12031368
def main():
@@ -1241,13 +1406,16 @@ def main():
12411406
create_accessibility_coverage_progression(theme, radius_data, coverage_radii)
12421407
create_density_scatter(theme, density_data)
12431408
create_local_density_distribution(theme, local_density_data, theme_name)
1409+
create_distance_to_shelter_line(theme, local_density_data)
1410+
create_local_density_200m(theme, local_density_data, theme_name)
12441411

12451412
print("\n=== GENERATION COMPLETE ===")
12461413
print("Generated chart files in output/ directory:")
12471414
for chart in ['01_shelter_types', '02_data_sources', '03_coverage_analysis',
12481415
'04_buildings_covered', '05_buildings_per_shelter',
12491416
'06_accessibility_coverage_progression', '07_density_scatter',
1250-
'09_local_density_distribution', '09b_local_density_distribution_simple']:
1417+
'09_local_density_distribution', '09b_local_density_distribution_simple',
1418+
'09c_local_density_200m', '10_distance_to_shelter_line']:
12511419
print(f" - {chart}_tufte.jpg")
12521420
print(f" - {chart}_dark.jpg")
12531421

0 commit comments

Comments
 (0)