import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import geopandas as gpd
import rasterio as rio
import json
import raster_tools as rt
'display.max_columns', 500)
pd.set_option('display.max_rows', 500) pd.set_option(
# for each geojson in data/missoula/missoula_poi, load the geojson into a geodataframe with the name of the file as the name of the geodataframe
# get filenames with glob
import glob
= glob.glob('data/missoula/missoula_poi/*.geojson')
filenames
# create a dictionary to hold the geodataframes
= {}
gdfs
# loop through the filenames
for filename in filenames:
# get the name of the file
= filename.split('/')[-1].split('.')[0]
name # load the geojson into a geodataframe
= gpd.read_file(filename)
gdfs[name] # add a column to the geodataframe with the name of the file
'b_type'] = name gdfs[name][
# combine the poi geodataframes into a single geodataframe, with a new column 'type' that is the name of the original geodataframe
= gpd.GeoDataFrame(pd.concat(gdfs.values(), ignore_index=True))
poi 'b_type'] = poi['b_type'].astype(str)
poi[# poi['b_type'] = poi['b_type'].str.split('_').str[1:2].str[0
# split and keep everything after the first _
'b_type'] = poi['b_type'].str.split('_').str[1:].str.join('_')
poi['lat'] = poi.geometry.y
poi['lon'] = poi.geometry.x
poi[ poi.b_type
# distribution of business types
poi.b_type.value_counts()
# write pois to geojosn
'data/missoula/missoula_poi_all.geojson', driver='GeoJSON') poi.to_file(
=5070, inplace=True) poi.to_crs(epsg
# load missoula_landuse_fixed.geojson
= gpd.read_file('data/missoula/missoula_landuse_fixed.geojson') landuse
import pandas as pd
import folium
from folium.plugins import MarkerCluster
# Load the POI data
# poi = pd.read_csv('path_to_your_poi_data.csv') # Replace with actual path to your POI data
# Create a map centered on Missoula
= folium.Map(location=[46.87, -113.99], zoom_start=12)
m_pois
# Create a color map for business types
= poi['b_type'].unique()
unique_types = {b_type: f'#{hash(b_type) % 0xFFFFFF:06x}' for b_type in unique_types}
color_map
# Create a feature group for all POIs
= folium.FeatureGroup(name="All POIs", show=True)
all_pois
# Add all POIs to the all_pois group
for _, row in poi.iterrows():
folium.CircleMarker(=[row['lat'], row['lon']],
location=5,
radius=f"{row['b_type']}",
popup=color_map[row['b_type']],
color=True,
fill=color_map[row['b_type']]
fillColor
).add_to(all_pois)
all_pois.add_to(m_pois)
# Create a feature group for each business type
for b_type in unique_types:
= folium.FeatureGroup(name=b_type, show=False)
fg = poi[poi['b_type'] == b_type]
type_df
for _, row in type_df.iterrows():
folium.CircleMarker(=[row['lat'], row['lon']],
location=5,
radius=f"{row['b_type']}",
popup=color_map[b_type],
color=True,
fill=color_map[b_type]
fillColor
).add_to(fg)
fg.add_to(m_pois)
# Add layer control to the map
folium.LayerControl().add_to(m_pois)
# Add a legend
= '''
legend_html <div style="position: fixed; bottom: 50px; left: 50px; width: 220px; height: 180px;
border:2px solid grey; z-index:9999; font-size:14px; background-color:white;
overflow-y: auto;">
<p><strong>Business Types:</strong></p>
'''
for b_type, color in color_map.items():
+= f'<p><span style="color:{color};">●</span> {b_type}</p>'
legend_html += '</div>'
legend_html
m_pois.get_root().html.add_child(folium.Element(legend_html))
# Save the map
"maps/missoula_poi_map.html")
m_pois.save(
print("POI map has been saved as 'missoula_poi_map.html'")
import pandas as pd
import folium
from folium.plugins import MarkerCluster
# Load the POI data
# poi = pd.read_csv('path_to_your_poi_data.csv') # Replace with actual path to your POI data
# Create a map centered on Missoula
= folium.Map(location=[46.87, -113.99], zoom_start=12)
m_pois
# Create a color map for business types
= poi['b_type'].unique()
unique_types = {b_type: f'#{hash(b_type) % 0xFFFFFF:06x}' for b_type in unique_types}
color_map
# Create a feature group for all POIs
= folium.FeatureGroup(name="All POIs", show=True)
all_pois
# Add all POIs to the all_pois group
for _, row in poi.iterrows():
folium.CircleMarker(=[row['lat'], row['lon']],
location=5,
radius=f"{row['b_type']}",
popup=color_map[row['b_type']],
color=True,
fill=color_map[row['b_type']]
fillColor
).add_to(all_pois)
all_pois.add_to(m_pois)
# Create a feature group for each business type
for b_type in unique_types:
= folium.FeatureGroup(name=b_type, show=False)
fg = poi[poi['b_type'] == b_type]
type_df
for _, row in type_df.iterrows():
folium.CircleMarker(=[row['lat'], row['lon']],
location=5,
radius=f"{row['b_type']}",
popup=color_map[b_type],
color=True,
fill=color_map[b_type]
fillColor
).add_to(fg)
fg.add_to(m_pois)
# Add layer control to the map
folium.LayerControl().add_to(m_pois)
# Add a legend
= '''
legend_html <div style="position: fixed; bottom: 50px; left: 50px; width: 220px; height: 180px;
border:2px solid grey; z-index:9999; font-size:14px; background-color:white;
overflow-y: auto;">
<p><strong>Business Types:</strong></p>
'''
for b_type, color in color_map.items():
+= f'<p><span style="color:{color};">●</span> {b_type}</p>'
legend_html += '</div>'
legend_html
m_pois.get_root().html.add_child(folium.Element(legend_html))
# Add custom CSS and JavaScript for the layer control label
= """
custom_css <style>
.leaflet-control-layers-list {
margin-top: 30px !important;
}
.layer-control-label {
position: absolute;
top: 10px;
left: 10px;
font-weight: bold;
}
</style>
"""
= """
custom_js <script>
document.addEventListener('DOMContentLoaded', function() {
var layerControl = document.querySelector('.leaflet-control-layers');
if (layerControl) {
var label = document.createElement('div');
label.className = 'layer-control-label';
label.textContent = 'Select POI Type';
layerControl.insertBefore(label, layerControl.firstChild);
}
});
</script>
"""
+ custom_js))
m_pois.get_root().header.add_child(folium.Element(custom_css
# Save the map
"website/maps/missoula_poi_map.html")
m_pois.save(
print("POI map has been saved as 'missoula_poi_map.html'")
# load the 911 calls data into a geodataframe
= gpd.read_file('data/missoula/missoula_911Calls_2023.csv')
calls_911_2023 = gpd.points_from_xy(calls_911_2023.Lon, calls_911_2023.Lat)
calls_911_2023.geometry = gpd.read_file('data/missoula/missoula_911_calls_2024.geojson')
calls_911_2024 = gpd.points_from_xy(calls_911_2024.Lon, calls_911_2024.Lat)
calls_911_2024.geometry # merge the 911 calls data
= pd.concat([calls_911_2023, calls_911_2024], ignore_index=True)
calls_911_2023_2024 # calls_911_2023_2024.to_crs(epsg=5070, inplace=True)
# List of Violent call types
= [
violent_calls 'Assault', 'Assault with a Weapon', 'Domestic Animal', 'Homicide',
'Intimidation', 'Kidnapping', 'Partner Family Member Assault',
'Robbery', 'Sexual Assault', 'Shots Fired', 'Shots Heard',
'Suicidal Person', 'Suicide', 'Weapons - Carry/Possess'
]
# Function to classify call types
def classify_call(call_type):
return 'Violent' if call_type in violent_calls else 'Nonviolent'
# Add new column 'crime_type' to the DataFrame
'crime_type'] = calls_911_2023_2024['Call Type Description'].apply(classify_call) calls_911_2023_2024[
# load missoula_roads_segments.geojson
= gpd.read_file('data/missoula/missoula_roads_clipped.geojson')
missoula_roads # drop roads with roadname DRIVEWAY, ALLEY
# missoula_roads = missoula_roads[~missoula_roads.roadname.isin(['DRIVEWAY', 'ALLEY', 'SERVICE'])]
= missoula_roads[~missoula_roads.roadname.isin(['SERVICE'])]
missoula_roads
missoula_roads.roadname.value_counts()sum()
missoula_roads.isnull().# drop all columns with any missing values
=1, inplace=True)
missoula_roads.dropna(axis=True, inplace=True)
missoula_roads.reset_index(drop
'uniquename'] = missoula_roads['roadname'] + '_' + missoula_roads['fromleft'].astype(str) + '_' + missoula_roads['toleft'].astype(str) + '_' + missoula_roads.index.astype(str)
missoula_roads[
# create n meter buffer around roads
= missoula_roads.copy()
missoula_roads_buffer =5070, inplace=True)
missoula_roads_buffer.to_crs(epsg'geometry'] = missoula_roads_buffer.buffer(30)
missoula_roads_buffer[=True, inplace=True)
missoula_roads_buffer.reset_index(drop missoula_roads_buffer.plot()
= calls_911_2023_2024.copy()
calls_911_2023_2024_buffer =5070, inplace=True) calls_911_2023_2024_buffer.to_crs(epsg
= calls_911_2023_2024_buffer[calls_911_2023_2024_buffer['crime_type'] == 'Violent']
calls_911_2023_2024_buffer_violent = missoula_roads_buffer.merge(missoula_roads_buffer.sjoin(calls_911_2023_2024_buffer_violent).groupby('uniquename').size().rename('n_points').reset_index())
roads_calls_violent = roads_calls_violent[['uniquename', 'geometry', 'n_points', 'shape_Leng']]
roads_calls_fixed_violent # remove buffer for plotting
'geometry'] = roads_calls_fixed_violent.buffer(-20)
roads_calls_fixed_violent[# remove roads starting with '90' in uniquename
= roads_calls_fixed_violent[~roads_calls_fixed_violent.uniquename.str.startswith('90')]
roads_calls_fixed_violent
= missoula_roads_buffer.merge(missoula_roads_buffer.sjoin(calls_911_2023_2024_buffer).groupby('uniquename').size().rename('n_points').reset_index())
roads_calls = roads_calls[['uniquename', 'geometry', 'n_points', 'shape_Leng']]
roads_calls_fixed # remove buffer for plotting
'geometry'] = roads_calls_fixed.buffer(-20)
roads_calls_fixed[# remove roads starting with '90' in uniquename
= roads_calls_fixed[~roads_calls_fixed.uniquename.str.startswith('90')] roads_calls_fixed
# show the roads from roads_calls_fixed with the most 911 calls
'n_points', ascending=False).head(10) roads_calls_fixed.sort_values(
import pandas as pd
import geopandas as gpd
import folium
from folium.features import GeoJsonPopup, GeoJsonTooltip
from branca.colormap import LinearColormap
import numpy as np
# Ensure the GeoDataFrame is in WGS84 (EPSG:4326) for Folium
= roads_calls_fixed.to_crs(epsg=4326)
road_segments
# Create a map centered on Missoula with Esri.WorldStreetMap as default
= folium.Map(location=[46.87, -113.99], zoom_start=13, min_zoom=13, max_bounds=True, tiles='Esri.WorldStreetMap', attr='Esri')
m
# Define the bounding box for the initial view
= 46.77854245661467, 46.97166694574133, -114.17662423519394, -113.85756122668695
min_lat, max_lat, min_lon, max_lon
# Set the max bounds to restrict panning
m.fit_bounds([[min_lat, min_lon], [max_lat, max_lon]])
# Create a color map with percentile-based scaling
= road_segments['n_points'].min()
min_points = road_segments['n_points'].max()
max_points = np.percentile(road_segments['n_points'], 10)
p10 = np.percentile(road_segments['n_points'], 99.5)
p99
= LinearColormap(
colormap = [
colors '#00FF00', # Green
'#90EE90', # Light Green
'#FFA500', # Orange
'#FF8C00', # Dark Orange
'#FF4500', # Red-Orange
'#FF0000', # Red
'#DC143C', # Crimson
'#B22222', # Fire Brick
'#8B0000', # Dark Red
'#000000' # Black
],=p10,
vmin=p99
vmax
)
# Function to get color based on number of points
def get_color(n_points):
if n_points <= p10:
return colormap(p10)
elif n_points >= p99:
return colormap(p99)
else:
return colormap(n_points)
# Add road segments to the map
folium.GeoJson(
road_segments,=lambda feature: {
style_function'fillColor': get_color(feature['properties']['n_points']),
'color': 'black',
'weight': 0,
'fillOpacity': 0.7,
},=GeoJsonTooltip(fields=['uniquename', 'n_points'],
tooltip=['Road Name', 'Number of Points'],
aliases=True),
localize=GeoJsonPopup(fields=['uniquename', 'n_points'],
popup=['Road Name', 'Number of Points'],
aliases=True)
localize
).add_to(m)
# Add a color legend
colormap.add_to(m)= f'Number of Calls near roads (10th-99.5th percentile: {p10:.0f}-{p99:.0f})'
colormap.caption
# Add tile layers
folium.TileLayer(='Esri.WorldImagery',
tiles='Esri',
attr='Esri.WorldImagery',
name=False,
overlay=True
control
).add_to(m)
folium.TileLayer(='Esri.WorldGrayCanvas',
tiles='Esri',
attr='Esri.WorldGrayCanvas',
name=False,
overlay=True
control
).add_to(m)
# Add layer control
folium.LayerControl().add_to(m)
# Save the map
"website/maps/missoula_road_segments_map.html")
m.save(
print("Map of road segments has been saved as 'missoula_road_segments_map.html'")
import pandas as pd
import geopandas as gpd
import folium
from folium.features import GeoJsonPopup, GeoJsonTooltip
from branca.colormap import LinearColormap
import numpy as np
# Ensure the GeoDataFrame is in WGS84 (EPSG:4326) for Folium
= roads_calls_fixed.to_crs(epsg=4326)
road_segments
# Create a map centered on Missoula with Esri.WorldStreetMap as default
= folium.Map(location=[46.87, -113.99], zoom_start=12, min_zoom=12, max_bounds=True, tiles='Esri.WorldStreetMap', attr='Esri')
m
# Define the bounding box for the initial view
= 46.77854245661467, 46.97166694574133, -114.17662423519394, -113.85756122668695
min_lat, max_lat, min_lon, max_lon
# Set the max bounds to restrict panning
m.fit_bounds([[min_lat, min_lon], [max_lat, max_lon]])
# Calculate mean and standard deviation
= road_segments['n_points'].mean()
mean_points = road_segments['n_points'].std()
std_points
# Define color scale range (mean +/- 2 standard deviations)
= (mean_points - 1 * std_points) if (mean_points - 0.5 * std_points) > 0 else 0
vmin = mean_points + 2.0 * std_points
vmax
# Create a diverging color map
= LinearColormap(
colormap = [
colors '#440154', '#482878', '#3E4989', '#31688E', '#26828E',
'#1F9E89', '#35B779', '#6DCD59', '#B4DE2C', '#FDE725'
],=vmin,
vmin=vmax,
vmax=f'Number of Points (Mean: {mean_points:.0f}, Std: {std_points:.0f})'
caption
)
# Function to get color based on number of points
def get_color(n_points):
if n_points < vmin:
return colormap(vmin)
elif n_points > vmax:
return colormap(vmax)
else:
return colormap(n_points)
# Add road segments to the map
folium.GeoJson(
road_segments,=lambda feature: {
style_function'fillColor': get_color(feature['properties']['n_points']),
'color': 'black',
'weight': 0,
'fillOpacity': 0.7,
},=GeoJsonTooltip(fields=['uniquename', 'n_points'],
tooltip=['Road Name', 'Number of Points'],
aliases=True),
localize=GeoJsonPopup(fields=['uniquename', 'n_points'],
popup=['Road Name', 'Number of Points'],
aliases=True)
localize
).add_to(m)
# Add the color scale to the map
colormap.add_to(m)
# Add tile layers
folium.TileLayer(='Esri.WorldImagery',
tiles='Esri',
attr='Esri.WorldImagery',
name=False,
overlay=True
control
).add_to(m)
folium.TileLayer(='Esri.WorldGrayCanvas',
tiles='Esri',
attr='Esri.WorldGrayCanvas',
name=False,
overlay=True
control
).add_to(m)
# Add layer control
folium.LayerControl().add_to(m)
# Save the map
"maps/missoula_road_segments_map_std_scaled.html")
m.save(
print("Map of road segments has been saved as 'missoula_road_segments_map_std_scaled.html'")
import pandas as pd
import geopandas as gpd
import folium
from folium.features import GeoJsonPopup, GeoJsonTooltip
from branca.colormap import LinearColormap
import numpy as np
# Ensure the GeoDataFrame is in WGS84 (EPSG:4326) for Folium
= roads_calls_fixed_violent.to_crs(epsg=4326)
road_segments
# Create a map centered on Missoula with Esri.WorldStreetMap as default
= folium.Map(location=[46.87, -113.99], zoom_start=13, min_zoom=13, max_bounds=True, tiles='Esri.WorldStreetMap', attr='Esri')
m
# Define the bounding box for the initial view
= 46.77854245661467, 46.97166694574133, -114.17662423519394, -113.85756122668695
min_lat, max_lat, min_lon, max_lon
# Set the max bounds to restrict panning
m.fit_bounds([[min_lat, min_lon], [max_lat, max_lon]])
# Create a color map with percentile-based scaling
= road_segments['n_points'].min()
min_points = road_segments['n_points'].max()
max_points = np.percentile(road_segments['n_points'], 10)
p10 = np.percentile(road_segments['n_points'], 99.5)
p99
= LinearColormap(
colormap = [
colors '#00FF00', # Green
'#90EE90', # Light Green
'#FFA500', # Orange
'#FF8C00', # Dark Orange
'#FF4500', # Red-Orange
'#FF0000', # Red
'#DC143C', # Crimson
'#B22222', # Fire Brick
'#8B0000', # Dark Red
'#000000' # Black
],=p10,
vmin=p99
vmax
)
# Function to get color based on number of points
def get_color(n_points):
if n_points <= p10:
return colormap(p10)
elif n_points >= p99:
return colormap(p99)
else:
return colormap(n_points)
# Add road segments to the map
folium.GeoJson(
road_segments,=lambda feature: {
style_function'fillColor': get_color(feature['properties']['n_points']),
'color': 'black',
'weight': 0,
'fillOpacity': 0.7,
},=GeoJsonTooltip(fields=['uniquename', 'n_points'],
tooltip=['Road Name', 'Number of Points'],
aliases=True),
localize=GeoJsonPopup(fields=['uniquename', 'n_points'],
popup=['Road Name', 'Number of Points'],
aliases=True)
localize
).add_to(m)
# Add a color legend
colormap.add_to(m)= f'Number of violent calls near roads (10th-99.5th percentile: {p10:.0f}-{p99:.0f})'
colormap.caption
# Add tile layers
folium.TileLayer(='Esri.WorldImagery',
tiles='Esri',
attr='Esri.WorldImagery',
name=False,
overlay=True
control
).add_to(m)
folium.TileLayer(='Esri.WorldGrayCanvas',
tiles='Esri',
attr='Esri.WorldGrayCanvas',
name=False,
overlay=True
control
).add_to(m)
# Add layer control
folium.LayerControl().add_to(m)
# Save the map
"website/maps/missoula_road_segments_violent_map.html")
m.save(
print("Map of road segments has been saved as 'missoula_road_segments_violent_map.html'")
import pandas as pd
import geopandas as gpd
import folium
from folium.features import GeoJsonPopup, GeoJsonTooltip
from branca.colormap import LinearColormap
import numpy as np
from shapely.ops import unary_union
# Ensure the data is in EPSG:5070 projection
= missoula_roads.to_crs(epsg=5070)
missoula_roads
# Create a smart buffer that fills all space between polygons
def create_smart_buffer(gdf):
= 1
buffer_distance = gdf.buffer(buffer_distance)
buffered
while not unary_union(buffered).is_valid:
+= 1
buffer_distance = gdf.buffer(buffer_distance)
buffered
return gpd.GeoDataFrame(geometry=buffered, crs=gdf.crs)
# Create the smart buffer
= create_smart_buffer(missoula_roads)
missoula_roads_buffer 'uniquename'] = missoula_roads['uniquename']
missoula_roads_buffer[
# Process 911 calls data
= calls_911_2023_2024.copy()
calls_911_2023_2024_buffer =5070, inplace=True)
calls_911_2023_2024_buffer.to_crs(epsg
# Perform spatial join and count points
= missoula_roads_buffer.sjoin(calls_911_2023_2024_buffer).groupby('uniquename').size().rename('n_points').reset_index()
roads_calls = missoula_roads_buffer.merge(roads_calls, on='uniquename', how='left')
roads_calls
# Fill NaN values with 0 for roads with no calls
'n_points'] = roads_calls['n_points'].fillna(0)
roads_calls[
# Keep only necessary columns
= roads_calls[['uniquename', 'geometry', 'n_points']]
roads_calls_fixed
# Remove roads starting with '90' in uniquename
= roads_calls_fixed[~roads_calls_fixed.uniquename.str.startswith('90')]
roads_calls_fixed
# Create a 20m buffer for visualization
'geometry'] = roads_calls_fixed.buffer(20)
roads_calls_fixed[
# Ensure the GeoDataFrame is in WGS84 (EPSG:4326) for Folium
= roads_calls_fixed.to_crs(epsg=4326)
road_segments
# Create a map centered on Missoula
= folium.Map(location=[46.87, -113.99], zoom_start=12)
m
# Create a color map with percentile-based scaling
= road_segments['n_points'].min()
min_points = road_segments['n_points'].max()
max_points = np.percentile(road_segments['n_points'], 10)
p10 = np.percentile(road_segments['n_points'], 99)
p99
= LinearColormap(
colormap = [
colors '#00FF00', # Green
'#90EE90', # Light Green
'#FFA500', # Orange
'#FF8C00', # Dark Orange
'#FF4500', # Red-Orange
'#FF0000', # Red
'#DC143C', # Crimson
'#B22222', # Fire Brick
'#8B0000', # Dark Red
'#000000' # Black
],=p10,
vmin=p99
vmax
)
# Function to get color based on number of points
def get_color(n_points):
if n_points <= p10:
return colormap(p10)
elif n_points >= p99:
return colormap(p99)
else:
return colormap(n_points)
# Add road segments to the map
folium.GeoJson(
road_segments,=lambda feature: {
style_function'fillColor': get_color(feature['properties']['n_points']),
'color': 'black',
'weight': 1,
'fillOpacity': 0.7,
},=GeoJsonTooltip(fields=['uniquename', 'n_points'],
tooltip=['Road Name', 'Number of Points'],
aliases=True),
localize=GeoJsonPopup(fields=['uniquename', 'n_points'],
popup=['Road Name', 'Number of Points'],
aliases=True)
localize
).add_to(m)
# Add a color legend
colormap.add_to(m)= f'Number of Points on Road Segment (10th-90th percentile: {p10:.0f}-{p99:.0f})'
colormap.caption
# Save the map
"maps/missoula_road_segments_map_smartbuffer.html")
m.save(
print("Map of road segments has been saved as 'missoula_road_segments_map_smartbuffer.html'")
import pandas as pd
import geopandas as gpd
import folium
from folium.features import GeoJsonPopup, GeoJsonTooltip
from branca.colormap import LinearColormap
import numpy as np
from shapely.ops import unary_union
# Ensure the data is in EPSG:5070 projection
= missoula_roads.to_crs(epsg=5070)
missoula_roads
# Create a smart buffer that fills all space between polygons
def create_smart_buffer(gdf):
= 1
buffer_distance = gdf.buffer(buffer_distance)
buffered
while not unary_union(buffered).is_valid:
+= 1
buffer_distance = gdf.buffer(buffer_distance)
buffered
return gpd.GeoDataFrame(geometry=buffered, crs=gdf.crs)
# Create the smart buffer
= create_smart_buffer(missoula_roads)
missoula_roads_buffer 'uniquename'] = missoula_roads['uniquename']
missoula_roads_buffer[
# Process 911 calls data
= calls_911_2023_2024.copy()
calls_911_2023_2024_buffer =5070, inplace=True)
calls_911_2023_2024_buffer.to_crs(epsg
# Perform spatial join and count points
= missoula_roads_buffer.sjoin(calls_911_2023_2024_buffer).groupby('uniquename').size().rename('n_points').reset_index()
roads_calls = missoula_roads_buffer.merge(roads_calls, on='uniquename', how='left')
roads_calls
# Fill NaN values with 0 for roads with no calls
'n_points'] = roads_calls['n_points'].fillna(0)
roads_calls[
# Keep only necessary columns
= roads_calls[['uniquename', 'geometry', 'n_points']]
roads_calls_fixed
# Remove roads starting with '90' in uniquename
= roads_calls_fixed[~roads_calls_fixed.uniquename.str.startswith('90')]
roads_calls_fixed
# Create a copy for the 20m buffer
= roads_calls_fixed.copy()
roads_calls_20m 'geometry'] = roads_calls_20m.buffer(20)
roads_calls_20m[
# Ensure the GeoDataFrames are in WGS84 (EPSG:4326) for Folium
= roads_calls_fixed.to_crs(epsg=4326)
road_segments_original = roads_calls_20m.to_crs(epsg=4326)
road_segments_20m
# Create a map centered on Missoula
= folium.Map(location=[46.87, -113.99], zoom_start=12)
m
# Create a color map with percentile-based scaling
= road_segments_original['n_points'].min()
min_points = road_segments_original['n_points'].max()
max_points = np.percentile(road_segments_original['n_points'], 10)
p10 = np.percentile(road_segments_original['n_points'], 99)
p99
= LinearColormap(
colormap = [
colors '#00FF00', # Green
'#90EE90', # Light Green
'#FFA500', # Orange
'#FF8C00', # Dark Orange
'#FF4500', # Red-Orange
'#FF0000', # Red
'#DC143C', # Crimson
'#B22222', # Fire Brick
'#8B0000', # Dark Red
'#000000' # Black
],=p10,
vmin=p99
vmax
)
# Function to get color based on number of points
def get_color(n_points):
if n_points <= p10:
return colormap(p10)
elif n_points >= p99:
return colormap(p99)
else:
return colormap(n_points)
# Add original road segments to the map
folium.GeoJson(
road_segments_original,=lambda feature: {
style_function'fillColor': get_color(feature['properties']['n_points']),
'color': 'black',
'weight': 1,
'fillOpacity': 0.7,
},=GeoJsonTooltip(fields=['uniquename', 'n_points'],
tooltip=['Road Name', 'Number of Points'],
aliases=True),
localize=GeoJsonPopup(fields=['uniquename', 'n_points'],
popup=['Road Name', 'Number of Points'],
aliases=True)
localize
).add_to(m)
# Add 20m buffered road segments to the map
folium.GeoJson(
road_segments_20m,=lambda feature: {
style_function'fillColor': 'none',
'color': 'white',
'weight': 1,
'fillOpacity': 0,
}
).add_to(m)
# Add a color legend
colormap.add_to(m)= f'Number of Points on Road Segment (10th-90th percentile: {p10:.0f}-{p99:.0f})'
colormap.caption
# Save the map
"maps/missoula_road_segments_dual_buffer_map.html")
m.save(
print("Map of road segments with dual buffers has been saved as 'missoula_road_segments_dual_buffer_map.html'")
import pandas as pd
import geopandas as gpd
import folium
from folium.plugins import Draw
import random
import json
from branca.element import MacroElement, Template
# Load the data
= calls_911_2023_2024 # Assuming this is already a GeoDataFrame
gdf
# Function to generate random color
def random_color():
return f'#{random.randint(0, 0xFFFFFF):06x}'
# Create a color map with random colors
= gdf['Call Type Description'].unique()
unique_call_types = {call_type: random_color() for call_type in unique_call_types}
color_dict
# Convert GeoDataFrame to a list of dictionaries for JSON serialization
= gdf.copy()
calls_data 'lat'] = calls_data.geometry.y
calls_data['lon'] = calls_data.geometry.x
calls_data[= calls_data.drop(columns=['geometry']).to_dict('records')
calls_data
# Create the map
= folium.Map(location=[46.87, -114.00], zoom_start=12)
m_911_radius
# Add Draw plugin
= Draw()
draw
draw.add_to(m_911_radius)
# Custom MacroElement to add JavaScript
class ClickForMarker(MacroElement):
def __init__(self, calls_data, color_dict):
super(ClickForMarker, self).__init__()
self._template = Template("""
{% macro script(this, kwargs) %}
var calls_data = {{ this.calls_data }};
var color_dict = {{ this.color_dict }};
var currentMarker = null;
function updateMap(lat, lon) {
// Clear previous update results
{{ this._parent.get_name() }}.eachLayer(function(layer) {
if (layer instanceof L.CircleMarker || layer instanceof L.Circle) {
{{ this._parent.get_name() }}.removeLayer(layer);
}
});
// Remove the current marker if it exists
if (currentMarker) {
{{ this._parent.get_name() }}.removeLayer(currentMarker);
}
// Create a 100m buffer around the clicked point (approximate)
var buffer_size = 0.00090; // Approx. 100m in degrees
// Filter calls within the buffer
var calls_in_buffer = calls_data.filter(function(call) {
var dx = call.lon - lon;
var dy = call.lat - lat;
return Math.sqrt(dx*dx + dy*dy) <= buffer_size;
});
// Add circle to show buffer
L.circle([lat, lon], {
radius: 100,
color: 'red',
fillColor: 'red',
fillOpacity: 0.2
}).addTo({{ this._parent.get_name() }});
// Add points for calls
calls_in_buffer.forEach(function(call) {
L.circleMarker([call.lat, call.lon], {
radius: 5,
color: color_dict[call['Call Type Description']],
fillColor: color_dict[call['Call Type Description']],
fillOpacity: 1
}).bindPopup(call['Call Type Description']).addTo({{ this._parent.get_name() }});
});
// Create popup with top 10 call types
var call_type_counts = {};
calls_in_buffer.forEach(function(call) {
var type = call['Call Type Description'];
call_type_counts[type] = (call_type_counts[type] || 0) + 1;
});
var top_calls = Object.entries(call_type_counts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
var popup_html = "<h4>Top Call Types:</h4>";
top_calls.forEach(function([call_type, count]) {
popup_html += call_type + ": " + count + "<br>";
});
L.popup()
.setLatLng([lat, lon])
.setContent(popup_html)
.openOn({{ this._parent.get_name() }});
}
{{ this._parent.get_name() }}.on('click', function(e) {
var lat = e.latlng.lat.toFixed(6);
var lon = e.latlng.lng.toFixed(6);
// Remove the current marker if it exists
if (currentMarker) {
{{ this._parent.get_name() }}.removeLayer(currentMarker);
}
// Create a new marker and store it in currentMarker
currentMarker = L.marker([lat, lon]).addTo({{ this._parent.get_name() }});
currentMarker.bindPopup('<button onclick="updateMap(' + lat + ',' + lon + ')">Analyze This Area</button>').openPopup();
});
{% endmacro %}
""")
self.calls_data = calls_data
self.color_dict = color_dict
# Add the custom MacroElement to the map
m_911_radius.add_child(ClickForMarker(json.dumps(calls_data), json.dumps(color_dict)))
# Save the map
"maps/911_calls_map.html")
m_911_radius.save(
print("Map saved as 911_calls_map.html")
import pandas as pd
import geopandas as gpd
import folium
from folium.plugins import Draw
import random
import json
from branca.element import MacroElement, Template
from geopy.geocoders import Nominatim
from geopy.exc import GeocoderTimedOut
# Load the data
= calls_911_2023_2024 # Assuming this is already a GeoDataFrame
gdf
# Function to generate random color
def random_color():
return f'#{random.randint(0, 0xFFFFFF):06x}'
# Create a color map with random colors
= gdf['Call Type Description'].unique()
unique_call_types = {call_type: random_color() for call_type in unique_call_types}
color_dict
# Convert GeoDataFrame to a list of dictionaries for JSON serialization
= gdf.copy()
calls_data 'lat'] = calls_data.geometry.y
calls_data['lon'] = calls_data.geometry.x
calls_data[= calls_data.drop(columns=['geometry']).to_dict('records')
calls_data
# Create the map
= folium.Map(location=[46.87, -114.00], zoom_start=12)
m_911_radius
# # Add Draw plugin
# draw = Draw()
# draw.add_to(m_911_radius)
# Custom MacroElement to add JavaScript
class ClickForMarker(MacroElement):
def __init__(self, calls_data, color_dict):
super(ClickForMarker, self).__init__()
self._template = Template("""
{% macro script(this, kwargs) %}
var calls_data = {{ this.calls_data }};
var color_dict = {{ this.color_dict }};
var currentMarker = null;
var currentCircle = null;
function metersToDegreesLat(meters) {
return meters / 111320.0; // Roughly 111,320 meters per degree of latitude
}
function metersToDegreesLon(meters, lat) {
return meters / (111320.0 * Math.cos(lat * Math.PI / 180));
}
function updateMap(lat, lon) {
// Get the radius in meters from the input
var radiusMeters = parseFloat(document.getElementById('radius-input').value) || 100;
var radiusLat = metersToDegreesLat(radiusMeters);
var radiusLon = metersToDegreesLon(radiusMeters, lat);
// Clear previous update results
{{ this._parent.get_name() }}.eachLayer(function(layer) {
if (layer instanceof L.CircleMarker || layer instanceof L.Circle) {
{{ this._parent.get_name() }}.removeLayer(layer);
}
});
// Remove the current marker if it exists
if (currentMarker) {
{{ this._parent.get_name() }}.removeLayer(currentMarker);
}
// Remove the current circle if it exists
if (currentCircle) {
{{ this._parent.get_name() }}.removeLayer(currentCircle);
}
// Filter calls within the buffer
var calls_in_buffer = calls_data.filter(function(call) {
var dx = call.lon - lon;
var dy = call.lat - lat;
return Math.sqrt(dx*dx + dy*dy) <= Math.max(radiusLat, radiusLon);
});
// Add circle to show buffer
currentCircle = L.circle([lat, lon], {
radius: radiusMeters,
color: 'red',
fillColor: 'red',
fillOpacity: 0.2
}).addTo({{ this._parent.get_name() }});
// Add points for calls
calls_in_buffer.forEach(function(call) {
L.circleMarker([call.lat, call.lon], {
radius: 5,
color: color_dict[call['Call Type Description']],
fillColor: color_dict[call['Call Type Description']],
fillOpacity: 1
}).bindPopup(call['Call Type Description']).addTo({{ this._parent.get_name() }});
});
// Create popup with top 10 call types
var call_type_counts = {};
calls_in_buffer.forEach(function(call) {
var type = call['Call Type Description'];
call_type_counts[type] = (call_type_counts[type] || 0) + 1;
});
var top_calls = Object.entries(call_type_counts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
var popup_html = "<h4>Top Call Types:</h4>";
top_calls.forEach(function([call_type, count]) {
popup_html += call_type + ": " + count + "<br>";
});
L.popup()
.setLatLng([lat, lon])
.setContent(popup_html)
.openOn({{ this._parent.get_name() }});
}
{{ this._parent.get_name() }}.on('click', function(e) {
var lat = e.latlng.lat.toFixed(6);
var lon = e.latlng.lng.toFixed(6);
// Remove the current marker if it exists
if (currentMarker) {
{{ this._parent.get_name() }}.removeLayer(currentMarker);
}
// Create a new marker and store it in currentMarker
currentMarker = L.marker([lat, lon]).addTo({{ this._parent.get_name() }});
currentMarker.bindPopup('<button onclick="updateMap(' + lat + ',' + lon + ')">Analyze This Area</button>').openPopup();
});
// Add search and radius control
var searchControl = L.Control.extend({
options: {
position: 'topright'
},
onAdd: function (map) {
var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-custom');
container.style.backgroundColor = 'white';
container.style.padding = '5px';
container.innerHTML = '<input type="text" id="address-input" placeholder="Enter address in Missoula, MT">' +
'<input type="number" id="radius-input" placeholder="Radius (meters)" value="100">' +
'<button id="search-button">Search</button>';
container.style.fontSize = '14px';
return container;
}
});
{{ this._parent.get_name() }}.addControl(new searchControl());
document.getElementById('search-button').addEventListener('click', function() {
var address = document.getElementById('address-input').value + ', Missoula, MT';
fetch('https://nominatim.openstreetmap.org/search?format=json&q=' + encodeURIComponent(address))
.then(response => response.json())
.then(data => {
if (data.length > 0) {
var lat = parseFloat(data[0].lat);
var lon = parseFloat(data[0].lon);
{{ this._parent.get_name() }}.setView([lat, lon], 16);
if (currentMarker) {
{{ this._parent.get_name() }}.removeLayer(currentMarker);
}
currentMarker = L.marker([lat, lon]).addTo({{ this._parent.get_name() }});
currentMarker.bindPopup('<button onclick="updateMap(' + lat + ',' + lon + ')">Analyze This Area</button>').openPopup();
} else {
alert('Address not found');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while searching for the address');
});
});
{% endmacro %}
""")
self.calls_data = calls_data
self.color_dict = color_dict
# Add the custom MacroElement to the map
m_911_radius.add_child(ClickForMarker(json.dumps(calls_data), json.dumps(color_dict)))
# Save the map
"9maps/11_calls_map_2.html")
m_911_radius.save(
print("Map saved as 911_calls_map_2.html")
import pandas as pd
import geopandas as gpd
import folium
from folium.plugins import Draw
import random
import json
from branca.element import MacroElement, Template
# Load the data
= calls_911_2023_2024 # Assuming this is already a GeoDataFrame
gdf = poi.copy()
poi
# Function to generate random color
def random_color():
return f'#{random.randint(0, 0xFFFFFF):06x}'
# Create color maps
= gdf['Call Type Description'].unique()
unique_call_types = {call_type: random_color() for call_type in unique_call_types}
call_color_dict
= poi['b_type'].unique()
unique_poi_types = {b_type: f'#{hash(b_type) % 0xFFFFFF:06x}' for b_type in unique_poi_types}
poi_color_map
# Convert GeoDataFrame to a list of dictionaries for JSON serialization
= gdf.copy()
calls_data 'lat'] = calls_data.geometry.y
calls_data['lon'] = calls_data.geometry.x
calls_data[= calls_data.drop(columns=['geometry']).to_dict('records')
calls_data
# Create the map
= folium.Map(location=[46.87, -114.00], zoom_start=12)
m_combined
# Add Draw plugin
= Draw()
draw
draw.add_to(m_combined)
# Create feature groups for POIs
= folium.FeatureGroup(name="All POIs", show=False)
all_pois for _, row in poi.iterrows():
folium.CircleMarker(=[row['lat'], row['lon']],
location=5,
radius=f"{row['b_type']}",
popup=poi_color_map[row['b_type']],
color=True,
fill=poi_color_map[row['b_type']]
fillColor
).add_to(all_pois)
all_pois.add_to(m_combined)
for b_type in unique_poi_types:
= folium.FeatureGroup(name=f"POI: {b_type}", show=False)
fg = poi[poi['b_type'] == b_type]
type_df for _, row in type_df.iterrows():
folium.CircleMarker(=[row['lat'], row['lon']],
location=5,
radius=f"{row['b_type']}",
popup=poi_color_map[b_type],
color=True,
fill=poi_color_map[b_type]
fillColor
).add_to(fg)
fg.add_to(m_combined)
# Custom MacroElement to add JavaScript
class ClickForMarker(MacroElement):
def __init__(self, calls_data, call_color_dict):
super(ClickForMarker, self).__init__()
self._template = Template("""
{% macro script(this, kwargs) %}
var calls_data = {{ this.calls_data }};
var call_color_dict = {{ this.call_color_dict }};
var currentMarker = null;
var currentCircle = null;
var currentCallMarkers = L.layerGroup().addTo({{ this._parent.get_name() }});
function metersToDegreesLat(meters) {
return meters / 111320.0;
}
function metersToDegreesLon(meters, lat) {
return meters / (111320.0 * Math.cos(lat * Math.PI / 180));
}
function updateMap(lat, lon) {
var radiusMeters = parseInt(document.getElementById('radius-select').value);
var radiusLat = metersToDegreesLat(radiusMeters);
var radiusLon = metersToDegreesLon(radiusMeters, lat);
// change radiusLat and radiusLon based on radiusMeters (i.e. var buffer_size = 0.00090; Approx. 100m in degrees)
if (radiusMeters == 10) {
radiusLat = 0.00010;
radiusLon = 0.00010;
} else if (radiusMeters == 25) {
radiusLat = 0.00025;
radiusLon = 0.00025;
} else if (radiusMeters == 50) {
radiusLat = 0.00050;
radiusLon = 0.00050;
} else if (radiusMeters == 100) {
radiusLat = 0.00100;
radiusLon = 0.00100;
} else if (radiusMeters == 200) {
radiusLat = 0.00200;
radiusLon = 0.00200;;
}
currentCallMarkers.clearLayers();
if (currentCircle) {
{{ this._parent.get_name() }}.removeLayer(currentCircle);
}
currentCircle = L.circle([lat, lon], {
radius: radiusMeters,
color: 'red',
fillColor: 'red',
fillOpacity: 0.2
}).addTo({{ this._parent.get_name() }});
var calls_in_buffer = calls_data.filter(function(call) {
var dx = call.lon - lon;
var dy = call.lat - lat;
return Math.sqrt(dx*dx + dy*dy) <= Math.max(radiusLat, radiusLon);
});
calls_in_buffer.forEach(function(call) {
L.circleMarker([call.lat, call.lon], {
radius: 5,
color: call_color_dict[call['Call Type Description']],
fillColor: call_color_dict[call['Call Type Description']],
fillOpacity: 1
}).bindPopup(call['Call Type Description']).addTo(currentCallMarkers);
});
var call_type_counts = {};
calls_in_buffer.forEach(function(call) {
var type = call['Call Type Description'];
call_type_counts[type] = (call_type_counts[type] || 0) + 1;
});
var top_calls = Object.entries(call_type_counts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
var popup_html = "<h4>Top Call Types:</h4>";
top_calls.forEach(function([call_type, count]) {
popup_html += call_type + ": " + count + "<br>";
});
L.popup()
.setLatLng([lat, lon])
.setContent(popup_html)
.openOn({{ this._parent.get_name() }});
}
{{ this._parent.get_name() }}.on('click', function(e) {
var lat = e.latlng.lat.toFixed(6);
var lon = e.latlng.lng.toFixed(6);
if (currentMarker) {
{{ this._parent.get_name() }}.removeLayer(currentMarker);
}
currentMarker = L.marker([lat, lon]).addTo({{ this._parent.get_name() }});
currentMarker.bindPopup('<button onclick="updateMap(' + lat + ',' + lon + ')">Analyze This Area</button>').openPopup();
});
var searchControl = L.Control.extend({
options: {
position: 'topright'
},
onAdd: function (map) {
var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-custom');
container.style.backgroundColor = 'white';
container.style.padding = '5px';
container.innerHTML = '<input type="text" id="address-input" placeholder="Enter address in Missoula, MT">' +
'<select id="radius-select">' +
'<option value="10">10m</option>' +
'<option value="25">25m</option>' +
'<option value="50">50m</option>' +
'<option value="100" selected>100m</option>' +
'<option value="200">200m</option>' +
'</select>' +
'<button id="search-button">Search</button>';
container.style.fontSize = '14px';
return container;
}
});
{{ this._parent.get_name() }}.addControl(new searchControl());
document.getElementById('search-button').addEventListener('click', function() {
var address = document.getElementById('address-input').value + ', Missoula, MT';
fetch('https://nominatim.openstreetmap.org/search?format=json&q=' + encodeURIComponent(address))
.then(response => response.json())
.then(data => {
if (data.length > 0) {
var lat = parseFloat(data[0].lat);
var lon = parseFloat(data[0].lon);
{{ this._parent.get_name() }}.setView([lat, lon], 16);
if (currentMarker) {
{{ this._parent.get_name() }}.removeLayer(currentMarker);
}
currentMarker = L.marker([lat, lon]).addTo({{ this._parent.get_name() }});
currentMarker.bindPopup('<button onclick="updateMap(' + lat + ',' + lon + ')">Analyze This Area</button>').openPopup();
} else {
alert('Address not found');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while searching for the address');
});
});
{% endmacro %}
""")
self.calls_data = calls_data
self.call_color_dict = call_color_dict
# Add the custom MacroElement to the map
m_combined.add_child(ClickForMarker(json.dumps(calls_data), json.dumps(call_color_dict)))
# Add layer control to the map
folium.LayerControl().add_to(m_combined)
# Add a legend for POIs
= '''
poi_legend_html <div style="position: fixed; bottom: 50px; left: 50px; width: 220px; height: 180px;
border:2px solid grey; z-index:9999; font-size:14px; background-color:white;
overflow-y: auto;">
<p><strong>POI Types:</strong></p>
'''
for b_type, color in poi_color_map.items():
+= f'<p><span style="color:{color};">●</span> {b_type}</p>'
poi_legend_html += '</div>'
poi_legend_html
m_combined.get_root().html.add_child(folium.Element(poi_legend_html))
# Add a legend for 911 call types
= '''
call_legend_html <div style="position: fixed; bottom: 50px; right: 50px; width: 220px; height: 180px;
border:2px solid grey; z-index:9999; font-size:14px; background-color:white;
overflow-y: auto;">
<p><strong>911 Call Types:</strong></p>
'''
for call_type, color in call_color_dict.items():
+= f'<p><span style="color:{color};">●</span> {call_type}</p>'
call_legend_html += '</div>'
call_legend_html
m_combined.get_root().html.add_child(folium.Element(call_legend_html))
# Save the map
"maps/911_poi_combined_map.html")
m_combined.save(
print("Combined map saved as 911_poi_combined_map.html")
= 50 buffer_radius
import pandas as pd
import geopandas as gpd
# Assuming calls_911_2023_2024 and poi are already GeoDataFrames in CRS 5070
= calls_911_2023_2024.to_crs(epsg=5070)
calls_911_gdf = poi
poi_gdf
# Ensure the geometry columns are properly set
= calls_911_gdf.set_geometry('geometry')
calls_911_gdf = poi_gdf.set_geometry('geometry')
poi_gdf
# Count the number of each b_type
= poi_gdf['b_type'].value_counts().reset_index()
b_type_counts = ['b_type', 'POI_Count']
b_type_counts.columns
# Create a buffer around each POI
'buffer'] = poi_gdf.geometry.buffer(buffer_radius)
poi_gdf[= poi_gdf.set_geometry('buffer')
poi_gdf
# Spatial join to find calls within 50 meters of each POI
= gpd.sjoin(calls_911_gdf, poi_gdf[['buffer', 'b_type']], predicate='within', how='inner')
calls_near_poi
# Group by b_type and Call Type Description
= calls_near_poi.groupby(['b_type', 'Call Type Description']).size().unstack(fill_value=0)
grouped
# Calculate total for each b_type and add as a column
'Total'] = grouped.sum(axis=1)
grouped[
# Sort rows by total calls descending
= grouped.sort_values('Total', ascending=False)
poi_911_calls
# Sort columns by total calls descending, keeping 'Total' as the last column
= poi_911_calls.sum().sort_values(ascending=False).index.tolist()
column_order 'Total')
column_order.remove('Total')
column_order.append(= poi_911_calls[column_order]
poi_911_calls
# Add the POI count column
= poi_911_calls.reset_index().merge(b_type_counts, on='b_type', how='left').set_index('b_type')
poi_911_calls
# Reorder columns to put POI_Count right after b_type
= poi_911_calls.columns.tolist()
columns 'POI_Count')
columns.remove(0, 'POI_Count')
columns.insert(= poi_911_calls[columns]
poi_911_calls_by_b_type
# Rename the index to 'b_type'
= 'b_type'
poi_911_calls_by_b_type.index.name
print(poi_911_calls_by_b_type)
# Save to CSV
'data/missoula/poi_911_calls_summary.csv') poi_911_calls_by_b_type.to_csv(
import pandas as pd
import geopandas as gpd
# Assuming calls_911_2023_2024 and poi are already GeoDataFrames in CRS 5070
= calls_911_2023_2024.to_crs(epsg=5070)
calls_911_gdf = poi
poi_gdf
# Ensure the geometry columns are properly set
= calls_911_gdf.set_geometry('geometry')
calls_911_gdf = poi_gdf.set_geometry('geometry')
poi_gdf
# Create a buffer around each POI
'buffer'] = poi_gdf.geometry.buffer(buffer_radius)
poi_gdf[= poi_gdf.set_geometry('buffer')
poi_gdf
# Combine b_types for POIs with the same address
= poi_gdf.groupby('formatted').agg({
poi_grouped 'b_type': lambda x: ', '.join(sorted(set(x))),
'buffer': 'first' # Take the first buffer for each address
}).reset_index()
# set it as a gpd
= gpd.GeoDataFrame(poi_grouped, geometry='buffer')
poi_grouped = poi_gdf.crs
poi_grouped.crs
# Spatial join to find calls within 50 meters of each POI
= gpd.sjoin(calls_911_gdf, poi_grouped[['buffer', 'formatted', 'b_type']], predicate='within', how='inner')
calls_near_poi
# Group by formatted address and Call Type Description
= calls_near_poi.groupby(['formatted', 'Call Type Description']).size().unstack(fill_value=0)
grouped
# Calculate total for each address and add as a column
'Total'] = grouped.sum(axis=1)
grouped[
# Sort rows by total calls descending
= grouped.sort_values('Total', ascending=False)
poi_911_calls
# Sort columns by total calls descending, keeping 'Total' as the last column
= poi_911_calls.sum().sort_values(ascending=False).index.tolist()
column_order 'Total')
column_order.remove('Total')
column_order.append(= poi_911_calls[column_order]
poi_911_calls
# Add the b_type column
= poi_grouped.set_index('formatted')['b_type']
b_type_map 'b_type'] = poi_911_calls.index.map(b_type_map)
poi_911_calls[
# Reorder columns to put b_type right after the index
= poi_911_calls.columns.tolist()
columns 'b_type')
columns.remove(0, 'b_type')
columns.insert(= poi_911_calls[columns]
poi_911_calls_by_poi_address
# Rename the index to 'Address'
= 'Address'
poi_911_calls_by_poi_address.index.name
print(poi_911_calls_by_poi_address)
# Save to CSV
'data/missoula/poi_911_calls_summary_by_address.csv') poi_911_calls_by_poi_address.to_csv(
poi_911_calls_by_poi_address
# plot poi_911_calls for each b_type, sum across all call types
'Total'].sort_values(ascending=False).plot(kind='bar', figsize=(10, 8), color='skyblue')
poi_911_calls_by_b_type.loc[:, 'Total 911 Calls by Business Type within {} Meters'.format(buffer_radius))
plt.title('Total Calls')
plt.ylabel('Business Type')
plt.xlabel( plt.show()
# plot poi_911_calls for each b_type, sum across all call types, scaled by the number of POIs of that type
'Total_per_POI'] = poi_911_calls_by_b_type['Total'] / poi_911_calls_by_b_type['POI_Count']
poi_911_calls_by_b_type['Total_per_POI'].sort_values(ascending=False).plot(kind='bar', figsize=(14, 10), color='skyblue')
poi_911_calls_by_b_type.loc[:, 'Total 911 Calls by Business Type per POI, scaled by number of POIs within {} Meters'.format(buffer_radius))
plt.title('Total Calls per POI')
plt.ylabel('Business Type')
plt.xlabel( plt.show()
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# Load the data (assuming you've already created this CSV in the previous step)
= pd.read_csv('data/missoula/poi_911_calls_summary.csv', index_col='b_type')
poi_911_calls
# Remove 'POI_Count' and 'Total' columns for this analysis
= poi_911_calls.drop(['POI_Count', 'Total'], axis=1)
call_type_data
# Create dropdown menu options
= [{'label': b_type, 'value': b_type} for b_type in call_type_data.index]
dropdown_options
# Function to get top 10 call types for a given b_type
def get_top_10_call_types(b_type):
return call_type_data.loc[b_type].nlargest(10)
# Create the initial plot (we'll use the first b_type as default)
= call_type_data.index[0]
initial_b_type = get_top_10_call_types(initial_b_type)
initial_data
= make_subplots(rows=1, cols=1)
fig
= go.Bar(
bar =initial_data.index,
x=initial_data.values,
y=initial_b_type
name
)
fig.add_trace(bar)
# Update layout
fig.update_layout(='Top 10 Call Types by POI Type within {} meters of POIs'.format(buffer_radius),
title='Call Type',
xaxis_title='Number of Calls',
yaxis_title=[{
updatemenus'buttons': [
{'method': 'update',
'label': b_type,
'args': [{'x': [get_top_10_call_types(b_type).index],
'y': [get_top_10_call_types(b_type).values]},
'title': f'Top 10 Call Types for {b_type} within {buffer_radius} meters of POIs',}]
{for b_type in call_type_data.index
}
],'direction': 'down',
'showactive': True,
}]
)
# Show the plot
fig.show()
# If you want to save the plot as an HTML file for later viewing:
"charts/interactive_911_calls_plot.html") fig.write_html(
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# Load the data
= pd.read_csv('data/missoula/poi_911_calls_summary.csv', index_col='b_type')
poi_911_calls
# Remove 'POI_Count' column for this analysis
= poi_911_calls.drop('POI_Count', axis=1)
call_type_data
# Function to get top 10 call types proportions for a given b_type
def get_top_10_call_types_prop(b_type):
= call_type_data.loc[b_type]
b_type_data = b_type_data['Total']
total_calls = (b_type_data.drop('Total') / total_calls * 100).nlargest(10)
proportions return proportions
# Create dropdown menu options
= [{'label': b_type, 'value': b_type} for b_type in call_type_data.index]
dropdown_options
# Create the initial plot (we'll use the first b_type as default)
= call_type_data.index[0]
initial_b_type = get_top_10_call_types_prop(initial_b_type)
initial_data
= make_subplots(rows=1, cols=1)
fig
= go.Bar(
bar =initial_data.index,
x=initial_data.values,
y=initial_b_type,
name=[f'{val:.2f}%' for val in initial_data.values],
text='auto'
textposition
)
fig.add_trace(bar)
# Update layout
fig.update_layout(=f'Top 10 Call Types for {initial_b_type} (% of Total Calls)',
title='Call Type',
xaxis_title='Percentage of Calls',
yaxis_title=dict(range=[0, 100]), # Set y-axis range from 0 to 100%
yaxis=[{
updatemenus'buttons': [
{'method': 'update',
'label': b_type,
'args': [{'x': [get_top_10_call_types_prop(b_type).index],
'y': [get_top_10_call_types_prop(b_type).values],
'text': [[f'{val:.2f}%' for val in get_top_10_call_types_prop(b_type).values]]},
'title': f'Top 10 Call Types for {b_type} (% of Total Calls)'}]
{for b_type in call_type_data.index
}
],'direction': 'down',
'showactive': True,
}]
)
# Show the plot
fig.show()
# Save the plot as an HTML file for later viewing
"charts/interactive_911_calls_plot_proportional.html") fig.write_html(
import pandas as pd
import plotly.graph_objects as go
# Load the data
= pd.read_csv('data/missoula/poi_911_calls_summary.csv', index_col='b_type')
poi_911_calls
# Remove 'POI_Count' column for this analysis
= poi_911_calls.drop('POI_Count', axis=1)
call_type_data
# Function to get top 10 call types for a given b_type
def get_top_10_call_types(b_type):
return call_type_data.loc[b_type].drop('Total').nlargest(10)
# Function to get top 10 call types proportions for a given b_type
def get_top_10_call_types_prop(b_type):
= call_type_data.loc[b_type]
b_type_data = b_type_data['Total']
total_calls = (b_type_data.drop('Total') / total_calls * 100).nlargest(10)
proportions return proportions
# Create the initial plot (we'll use the first b_type as default)
= call_type_data.index[0]
initial_b_type = get_top_10_call_types(initial_b_type)
initial_data_count = get_top_10_call_types_prop(initial_b_type)
initial_data_prop
= go.Figure()
fig
# Add traces for both count and percentage views
fig.add_trace(go.Bar(=initial_data_count.index,
x=initial_data_count.values,
y='Count',
name=True
visible
))
fig.add_trace(go.Bar(=initial_data_prop.index,
x=initial_data_prop.values,
y='Percentage',
name=[f'{val:.2f}%' for val in initial_data_prop.values],
text='auto',
textposition=False
visible
))
# Update layout
fig.update_layout(=f'Top 10 Call Types for {initial_b_type}',
title='Call Type',
xaxis_title='Number of Calls',
yaxis_title=[
updatemenus# Dropdown for POI type selection
{'buttons': [
{'method': 'update',
'label': b_type,
'args': [
{'x': [get_top_10_call_types(b_type).index, get_top_10_call_types_prop(b_type).index],
'y': [get_top_10_call_types(b_type).values, get_top_10_call_types_prop(b_type).values],
'text': [None, [f'{val:.2f}%' for val in get_top_10_call_types_prop(b_type).values]]
},'title': f'Top 10 Call Types for {b_type}'}
{
]for b_type in call_type_data.index
}
],'direction': 'down',
'showactive': True,
'x': 0.1,
'xanchor': 'left',
'y': 1.15,
'yanchor': 'top'
},# Buttons for toggling between count and percentage views
{'type': 'buttons',
'direction': 'right',
'x': 0.5,
'xanchor': 'center',
'y': 1.15,
'yanchor': 'top',
'buttons': [
{'label': "Count",
'method': 'update',
'args': [
'visible': [True, False]},
{'yaxis': {'title': 'Number of Calls', 'autorange': True}}
{
]
},
{'label': "Percentage",
'method': 'update',
'args': [
'visible': [False, True]},
{'yaxis': {'title': 'Percentage of Calls', 'range': [0, 100]}}
{
]
}
]
}
]
)
# Show the plot
fig.show()
# Save the plot as an HTML file for later viewing
"charts/interactive_911_calls_plot_combined.html") fig.write_html(
import pandas as pd
import plotly.graph_objects as go
# Load the data
= pd.read_csv('data/missoula/poi_911_calls_summary.csv', index_col='b_type')
poi_911_calls
# Remove 'POI_Count' column for this analysis
= poi_911_calls.drop('POI_Count', axis=1)
call_type_data
# Calculate overall top call types
= call_type_data.drop('Total', axis=1).sum().sort_values(ascending=False)
overall_top_calls
# Function to get top 10 call types for a given b_type, ignoring top N overall
def get_top_10_call_types(b_type, ignore_top_n):
= set(overall_top_calls.nlargest(ignore_top_n).index)
ignored_calls = call_type_data.loc[b_type].drop('Total').drop(ignored_calls, errors='ignore')
filtered_data return filtered_data.nlargest(10)
# Function to get top 10 call types proportions for a given b_type, ignoring top N overall
def get_top_10_call_types_prop(b_type, ignore_top_n):
= set(overall_top_calls.nlargest(ignore_top_n).index)
ignored_calls = call_type_data.loc[b_type]
b_type_data = b_type_data['Total']
total_calls = b_type_data.drop('Total').drop(ignored_calls, errors='ignore')
filtered_data = (filtered_data / total_calls * 100).nlargest(10)
proportions return proportions
# Create the initial plot
= call_type_data.index[0]
initial_b_type = 0
initial_ignore_top_n = get_top_10_call_types(initial_b_type, initial_ignore_top_n)
initial_data_count = get_top_10_call_types_prop(initial_b_type, initial_ignore_top_n)
initial_data_prop
= go.Figure()
fig
# Add traces for both count and percentage views
fig.add_trace(go.Bar(=initial_data_count.index,
x=initial_data_count.values,
y='Count',
name=True
visible
))
fig.add_trace(go.Bar(=initial_data_prop.index,
x=initial_data_prop.values,
y='Percentage',
name=[f'{val:.2f}%' for val in initial_data_prop.values],
text='auto',
textposition=False
visible
))
# Function to create slider steps
def create_slider_steps(b_type):
return [
{'method': 'update',
'label': str(n),
'args': [
{'x': [get_top_10_call_types(b_type, n).index,
get_top_10_call_types_prop(b_type, n).index],'y': [get_top_10_call_types(b_type, n).values,
get_top_10_call_types_prop(b_type, n).values],'text': [None, [f'{val:.2f}%' for val in get_top_10_call_types_prop(b_type, n).values]]
},'title': f'Top 10 Call Types for {b_type} (Ignoring top {n} overall)'}
{
]for n in range(0, 11) # Allows ignoring 0 to 10 top overall call types
}
]
# Update layout
fig.update_layout(=f'Top 10 Call Types for {initial_b_type} (Ignoring top {initial_ignore_top_n} overall)',
title='Call Type',
xaxis_title='Number of Calls',
yaxis_title=[
updatemenus# Dropdown for POI type selection
{'buttons': [
{'method': 'update',
'label': b_type,
'args': [
{'x': [get_top_10_call_types(b_type, initial_ignore_top_n).index,
get_top_10_call_types_prop(b_type, initial_ignore_top_n).index],'y': [get_top_10_call_types(b_type, initial_ignore_top_n).values,
get_top_10_call_types_prop(b_type, initial_ignore_top_n).values],'text': [None, [f'{val:.2f}%' for val in get_top_10_call_types_prop(b_type, initial_ignore_top_n).values]]
},
{'title': f'Top 10 Call Types for {b_type} (Ignoring top {initial_ignore_top_n} overall)',
'sliders[0].steps': create_slider_steps(b_type)
}
]for b_type in call_type_data.index
}
],'direction': 'down',
'showactive': True,
'x': 0.5,
'xanchor': 'left',
'y': 1.15,
'yanchor': 'top'
},# Buttons for toggling between count and percentage views
{'type': 'buttons',
'direction': 'right',
'x': 0.8,
'xanchor': 'center',
'y': 1.15,
'yanchor': 'top',
'buttons': [
{'label': "Count",
'method': 'update',
'args': [
'visible': [True, False]},
{'yaxis': {'title': 'Number of Calls', 'autorange': True}}
{
]
},
{'label': "Percentage",
'method': 'update',
'args': [
'visible': [False, True]},
{'yaxis': {'title': 'Percentage of Calls for this POI', 'range': [0, 100]}}
{
]
}
]
}
],# Add a slider to control the number of top overall call types to ignore
=[{
sliders'active': 0,
'currentvalue': {"prefix": "Ignore top N overall: "},
'pad': {"t": 50},
'steps': create_slider_steps(initial_b_type)
}]
)
# Save the plot as an HTML file for later viewing
"charts/interactive_911_calls_plot_with_ignore.html") fig.write_html(
# List of Violent call types
= [
violent_calls 'Assault', 'Assault with a Weapon', 'Domestic Animal', 'Homicide',
'Intimidation', 'Kidnapping', 'Partner Family Member Assault',
'Robbery', 'Sexual Assault', 'Shots Fired', 'Shots Heard',
'Suicidal Person', 'Suicide', 'Weapons - Carry/Possess'
]
# Function to classify call types
def classify_call(call_type):
return 'Violent' if call_type in violent_calls else 'Nonviolent'
# Add new column 'crime_type' to the DataFrame
'crime_type'] = calls_911_2023['Call Type Description'].apply(classify_call)
calls_911_2023[
# Print the first few rows to verify the new column
print(calls_911_2023[['Call Type Description', 'crime_type']].head())
# Print value counts to see the distribution
print(calls_911_2023['crime_type'].value_counts())
# Optional: If you want to see the distribution as percentages
print(calls_911_2023['crime_type'].value_counts(normalize=True))
import pandas as pd
import plotly.graph_objects as go
# Load the data
= calls_911_2023_2024
df 'Call Creation Date and Time'] = df['Call Creation Date and Time'].astype('datetime64[ns]')
df[
# Extract date
'Date'] = df['Call Creation Date and Time'].dt.date
df[
# Create a dataframe with each day as a row and columns for each call type
= df.groupby(['Date', 'Call Type Description']).size().unstack(fill_value=0)
daily_counts
# Add a total column
'Total'] = daily_counts.sum(axis=1)
daily_counts[
# Sort the index to ensure chronological order
= daily_counts.sort_index()
daily_counts
# Function to get top 10 call types for a given date
def get_top_10_call_types(date):
= daily_counts.loc[date].sort_values(ascending=False)
day_data = day_data.head(10)
top_10 return '<br>'.join([f"{call_type}: {count}" for call_type, count in top_10.items() if call_type != 'Total'])
# Create hover text for each day
= daily_counts.index.map(get_top_10_call_types)
hover_text
# Create the plot
= go.Figure()
fig
# Add trace for total calls (visible by default)
fig.add_trace(
go.Scatter(=daily_counts.index,
x=daily_counts['Total'],
y='Total Calls',
name=dict(color='black', width=2),
line='<b>Date</b>: %{x}<br>' +
hovertemplate'<b>Total Calls</b>: %{y}<br><br>' +
'<b>Top 10 Call Types:</b><br>%{text}',
=hover_text
text
)
)
# Add traces for each call type (hidden by default)
for column in daily_counts.columns:
if column != 'Total':
fig.add_trace(
go.Scatter(=daily_counts.index,
x=daily_counts[column],
y=column,
name=False,
visible='<b>Date</b>: %{x}<br>' +
hovertemplatef'<b>{column}</b>: %{{y}}<br><br>' +
'<b>Top 10 Call Types:</b><br>%{text}',
=hover_text
text
)
)
# Create buttons for filtering
= [dict(
buttons ='Total Calls',
label='update',
method=[{'visible': [True] + [False] * (len(daily_counts.columns) - 1)},
args'title': 'Total 911 Calls by Day'}]
{
)]
for i, column in enumerate(daily_counts.columns):
if column != 'Total':
= [False] * len(daily_counts.columns)
visibility + 1] = True # +1 because Total is the first trace
visibility[i dict(
buttons.append(=column,
label='update',
method=[{'visible': visibility},
args'title': f'911 Calls by Day: {column}'}]
{
))
# Update layout
fig.update_layout(='Total 911 Calls by Day',
title='Date',
xaxis_title='Number of Calls',
yaxis_title=[dict(
updatemenus=0,
active=buttons,
buttons="down",
direction={"r": 10, "t": 10},
pad=True,
showactive=0.5,
x="left",
xanchor=1.15,
y="top"
yanchor
)],='closest'
hovermode
)
# Show the plot
fig.show()
# Save the plot as an HTML file
"charts/911_calls_time_series_plot_by_day_with_popup.html") fig.write_html(
import pandas as pd
import plotly.graph_objects as go
# Load the data
= calls_911_2023_2024
df 'Call Creation Date and Time'] = df['Call Creation Date and Time'].astype('datetime64[ns]')
df[= df[df['Call Creation Date and Time'].dt.strftime('%H-%M-%S') != '00-00-00'] # Remove rows with time 00:00:00 (to avoid issues with plotting a lot of calls at midnight)
df
# Extract month and day, ignoring year
'Hour'] = df['Call Creation Date and Time'].dt.hour
df[
# Create a dataframe with each day as a row and columns for each call type
= df.groupby(['Hour', 'Call Type Description']).size().unstack(fill_value=0)
daily_counts
# Add a total column
'Total'] = daily_counts.sum(axis=1)
daily_counts[
# Sort the index to ensure chronological order
= daily_counts.sort_index()
daily_counts
# Create the plot
= go.Figure()
fig
# Add trace for total calls (visible by default)
fig.add_trace(
go.Scatter(=daily_counts.index,
x=daily_counts['Total'],
y='Total Calls',
name=dict(color='black', width=2)
line
)
)
# Add traces for each call type (hidden by default)
for column in daily_counts.columns:
if column != 'Total':
fig.add_trace(
go.Scatter(=daily_counts.index,
x=daily_counts[column],
y=column,
name=False
visible
)
)
# Create buttons for filtering
= [dict(
buttons ='Total Calls',
label='update',
method=[{'visible': [True] + [False] * (len(daily_counts.columns) - 1)},
args'title': 'Total 911 Calls by Hour'}]
{
)]
for i, column in enumerate(daily_counts.columns):
if column != 'Total':
= [False] * len(daily_counts.columns)
visibility + 1] = True # +1 because Total is the first trace
visibility[i dict(
buttons.append(=column,
label='update',
method=[{'visible': visibility},
args'title': f'911 Calls by Hour: {column}'}]
{
))
# Update layout
fig.update_layout(='Total 911 Calls by Hour',
title='Hour',
xaxis_title='Number of Calls',
yaxis_title=[dict(
updatemenus=0,
active=buttons,
buttons="down",
direction={"r": 10, "t": 10},
pad=True,
showactive=0.5,
x="left",
xanchor=1.15,
y="top"
yanchor
)],
)
# Save the plot as an HTML file
"charts/911_calls_time_series_plot_by_hour.html") fig.write_html(
import pandas as pd
import folium
from folium.plugins import HeatMap
# Create DataFrame from the provided data
= calls_911_2023_2024
data = pd.DataFrame(data)
df
# Create a map centered on Missoula
= folium.Map(location=[46.87, -113.99], zoom_start=12)
m
# Create a feature group for all calls
= folium.FeatureGroup(name="All Calls", show=False)
all_calls_group = df[['Lat', 'Lon']].values.tolist()
all_heat_data
HeatMap(all_heat_data).add_to(all_calls_group)
all_calls_group.add_to(m)
# Get unique call types
= df['Call Type Description'].unique()
call_types
# Create a feature group for each call type
for call_type in call_types:
= df[df['Call Type Description'] == call_type]
filtered_data = filtered_data[['Lat', 'Lon']].values.tolist()
heat_data
= folium.FeatureGroup(name=call_type, show=False) # Set show=False to make layers off by default
feature_group = 15, min_opacity = 0.4, blur = 15, gradient = {0.4: 'blue', 0.65: 'lime', 1: 'red'}, max_zoom=18).add_to(feature_group)
HeatMap(heat_data, radius
feature_group.add_to(m)
# Add layer control to the map
folium.LayerControl().add_to(m)
# Save the map
"maps/missoula_911_heatmap_with_all_calls.html")
m.save(
print("Heatmap with 'All Calls' option has been saved as 'missoula_911_heatmap_with_all_calls.html'")
import pandas as pd
import folium
from folium.plugins import HeatMap
# Create DataFrame from the provided data
= calls_911_2023_2024
data = pd.DataFrame(data)
df
# Create a map centered on Missoula
= folium.Map(location=[46.87, -113.99], zoom_start=12)
m
# Create a feature group for all calls
= folium.FeatureGroup(name="All Calls", show=False)
all_calls_group = df[['Lat', 'Lon']].values.tolist()
all_heat_data
HeatMap(all_heat_data).add_to(all_calls_group)
all_calls_group.add_to(m)
# Get unique crime types
= df['crime_type'].unique()
call_types
# Create a feature group for each crime type
for call_type in call_types:
= df[df['crime_type'] == call_type]
filtered_data = filtered_data[['Lat', 'Lon']].values.tolist()
heat_data
= folium.FeatureGroup(name=call_type, show=False) # Set show=False to make layers off by default
feature_group = 15, min_opacity = 0.4, blur = 15, gradient = {0.4: 'blue', 0.65: 'lime', 1: 'red'}, max_zoom=18).add_to(feature_group)
HeatMap(heat_data, radius
feature_group.add_to(m)
# Add layer control to the map
folium.LayerControl().add_to(m)
# Save the map
"maps/missoula_911_heatmap_with_crime_type.html")
m.save(
print("Heatmap with 'All Calls' option has been saved as 'missoula_911_heatmap_with_crime_type.html'")
import pandas as pd
import folium
from folium.plugins import HeatMap
# Create DataFrame from the provided data
= calls_911_2023_2024
data = pd.DataFrame(data)
df
# drop dates with no hms (cant use dt for this)
= df[df['Call Creation Date and Time'].str.contains(':')]
df # Convert the 'Date' column to datetime and extract hour
'Date'] = pd.to_datetime(df['Call Creation Date and Time'])
df['Hour'] = df['Date'].dt.hour
df[
# Create a map centered on Missoula
= folium.Map(location=[46.87, -113.99], zoom_start=12)
m
# Create a feature group for each hour
for hour in range(24):
= df[df['Hour'] == hour]
hour_df = hour_df[['Lat', 'Lon']].values.tolist()
heat_data
# Create a name for the layer (e.g., "12 AM" for hour 0, "1 PM" for hour 13)
= f"{hour % 12 or 12} {'AM' if hour < 12 else 'PM'}"
layer_name
= folium.FeatureGroup(name=layer_name, show=False)
feature_group = 15, min_opacity = 0.4, blur = 15, gradient = {0.4: 'blue', 0.65: 'lime', 1: 'red'}, max_zoom=18).add_to(feature_group)
HeatMap(heat_data, radius
feature_group.add_to(m)
# Add a feature group for all hours
= df[['Lat', 'Lon']].values.tolist()
all_heat_data = folium.FeatureGroup(name="All Hours", show=True)
all_hours_group
HeatMap(all_heat_data).add_to(all_hours_group)
all_hours_group.add_to(m)
# Add layer control to the map
folium.LayerControl().add_to(m)
# Save the map
"maps/missoula_911_heatmap_hourly_layers.html")
m.save(
print("Heatmap with hourly layers has been saved as 'missoula_911_heatmap_hourly_layers.html'")
import rasterio
import numpy as np
import pandas as pd
# Open the TIFF file
= "data/missoula/missoula_pop_density.tif"
tif_file with rasterio.open(tif_file) as src:
# Read the raster data
= src.read(1) # Read the first band (assuming single-band raster)
data
# Replace NaN (no-data) values with 0
= np.nan_to_num(data, nan=0) # Replace NaN with 0
data
# Print out the metadata to verify file details
print("Metadata:", src.meta)
# Check the data shape and some basic statistics
print("Data Shape:", data.shape)
print("Min Value:", np.nanmin(data)) # Should not be NaN anymore
print("Max Value:", np.nanmax(data))
print("Mean Value:", np.nanmean(data))
# Flatten the raster data to create a table (row for each pixel)
= data.flatten()
data_flat
# Create a DataFrame from the flattened raster data
= pd.DataFrame({'Pixel Value': data_flat})
df
# Show the first few rows of the DataFrame
print(df.head())
#------
import rasterio
import geopandas as gpd
from rasterstats import zonal_stats
import numpy as np
# Load the population density TIFF file
= 'data/missoula/missoula_pop_density.tif'
pop_density_file with rasterio.open(pop_density_file) as src:
= src.read(1) # Read the first band (assuming single-band raster)
population_density = src.transform # Affine transformation for pixel coordinates
transform = transform[0] * transform[4] # Get pixel size (e.g., in square kilometers)
pixel_size
# Load the neighborhood boundaries (GeoJSON, Shapefile, etc.)
= gpd.read_file('data/missoula/missoula_neighborhoods_fixed_3.geojson')
n_file
# Ensure both datasets are in the same coordinate reference system (CRS)
if n_file.crs != src.crs:
= n_file.to_crs(src.crs)
n_file
# Perform zonal statistics to sum population density for each neighborhood
= zonal_stats(n_file, pop_density_file, stats="sum", affine=transform)
stats
# Add the population density sums to the neighborhoods GeoDataFrame
'population_density_sum'] = [stat['sum'] for stat in stats]
n_file[
# Calculate the population for each neighborhood
# Assuming the population density is per square kilometer
# n_file['population'] = n_file['population_density_sum'] * pixel_size
# Display the results
# print(n_file) # Assuming 'name' is the neighborhood name column
print(n_file[['FeatureName', 'population_density_sum']])
"pop_dens_sum.geojson", driver="GeoJSON")
n_file.to_file(
#sums pops
= 0
i_sum for i in n_file["population_density_sum"]:
+= i
i_sum print("totaled:", i_sum)
#save into file
#csv:
# n_file[['FeatureName', 'population_density_sum']].to_csv('neighbor_pop_dens.csv', index=False)
#------
n_file
#------
#1
#----------------------
import folium
import branca.colormap as cm
#merge at geometry
= n_file.merge(n_file.sjoin(calls_911_2023_2024).groupby('FeatureName').size().rename('CallAmount').reset_index())
merged
merged#----------------------
#2
#----------------------
"n_p_sum.geojson", driver="GeoJSON")
merged.to_file("FeatureName", "CallAmount"]]
merged[[#----------------------
#3
#----------------------
= folium.Map(location=[46.8721, -113.9940], zoom_start=12)
m
#colormap
= cm.linear.YlOrRd_09.scale(merged["CallAmount"].min(), merged["CallAmount"].max())
colormap = "Call Amount by Neighborhood"
colormap.caption
#color neighs
def style_function(feature):
= feature['properties']['CallAmount']
call_amount return {
'fillOpacity': 0.7,
'weight': 0.5,
'fillColor': colormap(call_amount) if call_amount is not None else '#ffffff', # White if no data
'color': 'black'
}
folium.GeoJson(
merged,=style_function,
style_function=folium.GeoJsonTooltip(fields=['FeatureName', 'CallAmount'], aliases=['Neighborhood:', 'Calls:']),
tooltip
).add_to(m)
colormap.add_to(m)
#save map
'maps/neighborhood_calls_map.html')
m.save(#----------------------
# load zillow data
= pd.read_csv('data/missoula/missoula_zillow_10_2024.csv')
missoula_zillow # drop values where price is less than 3 characters
= missoula_zillow[missoula_zillow['price'].str.len() > 3]
missoula_zillow =True, inplace=True) missoula_zillow.reset_index(drop
missoula_zillow
# drop values where price is less than 3 characters
= missoula_zillow[missoula_zillow['price'].str.len() > 3]
missoula_zillow =True, inplace=True) missoula_zillow.reset_index(drop
3] missoula_zillow.iloc[
# show the first entry image using the image_url
from IPython.display import Image
=missoula_zillow.iloc[3].image_url) Image(url
3].price missoula_zillow.iloc[
# missoula_zillow.address
# write address to a file and open it in a text editor
'missoula_zillow_addresses.txt', index=False) missoula_zillow.address.to_csv(
# open missoula_zillow.csv and merge it with the missoula_zillow dataframe (original_address from missoula_zillow.csv and address from missoula_zillow df)
= pd.read_csv('data/missoula/missoula_zillow.csv')
missoula_zillow_original
missoula_zillow_original
# merge the two dataframes
= pd.merge(missoula_zillow, missoula_zillow_original, left_on='address', right_on='original_address')
missoula_zillow_merged 'missoula_zillow_merged.csv', index=False) missoula_zillow_merged.to_csv(
# format the price column to be just the number
'price'] = missoula_zillow_merged['price'].str.replace('$', '').str.replace(',', '').str.replace('+', '').astype(int)
missoula_zillow_merged['price'] missoula_zillow_merged[
'missoula_zillow_merged.csv', index=False) missoula_zillow_merged.to_csv(
# read merged
= pd.read_csv('missoula_zillow_merged.csv') missoula_zillow_merged
missoula_zillow_merged.price.plot()