Spaces:
Sleeping
Sleeping
import os | |
import sys | |
import dash | |
from dash import dcc, html, dash_table, callback, Input, Output, State | |
import dash_bootstrap_components as dbc | |
import pandas as pd | |
from datetime import datetime | |
import numpy as np | |
import plotly.express as px | |
import plotly.graph_objects as go | |
from geopy.extra.rate_limiter import RateLimiter | |
from geopy.geocoders import Nominatim | |
from dash.exceptions import PreventUpdate | |
from vincenty import vincenty | |
import duckdb | |
import requests | |
import urllib | |
from dotenv import load_dotenv | |
import time | |
from functools import wraps | |
import glob | |
# Load environment variables | |
load_dotenv() | |
# Initialize the Dash app | |
app = dash.Dash( | |
__name__, | |
external_stylesheets=[dbc.themes.BOOTSTRAP], | |
suppress_callback_exceptions=True | |
) | |
app.title = "Hail Damage Analyzer" | |
server = app.server | |
# Cache functions | |
def simple_cache(expire_seconds=300): | |
def decorator(func): | |
cache = {} | |
def wrapper(*args, **kwargs): | |
key = (func.__name__, args, frozenset(kwargs.items())) | |
current_time = time.time() | |
if key in cache: | |
result, timestamp = cache[key] | |
if current_time - timestamp < expire_seconds: | |
return result | |
result = func(*args, **kwargs) | |
cache[key] = (result, current_time) | |
return result | |
return wrapper | |
return decorator | |
def duck_sql(sql_code): | |
con = duckdb.connect() | |
con.execute("PRAGMA threads=2") | |
con.execute("PRAGMA enable_object_cache") | |
return con.execute(sql_code).df() | |
def get_data(lat, lon, date_str): | |
data_dir = r"C:/Users/aammann/OneDrive - Great American Insurance Group/Documents/Python Scripts/hail_data" | |
parquet_files = glob.glob(f"{data_dir}/hail_*.parquet") | |
print("Parquet files found:", parquet_files) | |
if not parquet_files: | |
raise ValueError("No parquet files found in the specified directory") | |
file_paths = ", ".join([f"'{file}'" for file in parquet_files]) | |
lat_min, lat_max = lat-1, lat+1 | |
lon_min, lon_max = lon-1, lon+1 | |
code = f""" | |
SELECT | |
"#ZTIME" as "Date_utc", | |
LON, | |
LAT, | |
MAXSIZE | |
FROM read_parquet([{file_paths}], hive_partitioning=1) | |
WHERE | |
LAT BETWEEN {lat_min} AND {lat_max} | |
AND LON BETWEEN {lon_min} AND {lon_max} | |
AND "#ZTIME" <= '{date_str}' | |
""" | |
return duck_sql(code) | |
def distance(x): | |
left_coords = (x[0], x[1]) # LAT, LON | |
right_coords = (x[2], x[3]) # Lat_address, Lon_address | |
return vincenty(left_coords, right_coords, miles=True) | |
def geocode(address): | |
try: | |
try: | |
address2 = address.replace(' ', '+').replace(',', '%2C') | |
df = pd.read_json( | |
f'https://geocoding.geo.census.gov/geocoder/locations/onelineaddress?address={address2}&benchmark=2020&format=json') | |
results = df.iloc[0, 0]['results'].iloc[0]['coordinates'] | |
return results['y'], results['x'] | |
except: | |
geolocator = Nominatim(user_agent="HailDamageAnalyzer") | |
geocode = RateLimiter(geolocator.geocode, min_delay_seconds=1) | |
location = geolocator.geocode(address) | |
if location: | |
return location.latitude, location.longitude | |
raise Exception("Geocoding failed") | |
except: | |
try: | |
geocode_key = os.getenv("GEOCODE_KEY") | |
if not geocode_key: | |
raise Exception("Geocode API key not found") | |
address_encoded = urllib.parse.quote(address) | |
url = f'https://api.geocod.io/v1.7/geocode?q={address_encoded}&api_key={geocode_key}' | |
response = requests.get(url, verify=False) | |
response.raise_for_status() | |
json_response = response.json() | |
return json_response['results'][0]['location']['lat'], json_response['results'][0]['location']['lng'] | |
except Exception as e: | |
print(f"Geocoding error: {str(e)}") | |
raise Exception("Could not geocode address. Please try again with a different address.") | |
# Layout | |
app.layout = html.Div([ | |
dcc.Store(id="filtered-data-store"), | |
dcc.Download(id="download-dataframe-csv"), | |
dbc.Button("Download Data as CSV", id="btn-download-csv", color="secondary", className="mb-3"), | |
dbc.Container([ | |
dbc.Row([ | |
dbc.Col([ | |
html.H1("Hail Damage Analyzer", className="text-center my-4"), | |
html.P("Analyze historical hail data", className="text-center text-muted"), | |
html.Hr() | |
], width=12) | |
]), | |
dbc.Row([ | |
dbc.Col([ | |
html.Div([ | |
html.H5("Search Parameters", className="mb-3"), | |
dbc.Form([ | |
dbc.Label("Address"), | |
dbc.Input(id="address-input", type="text", placeholder="Enter address", value="Dallas, TX", className="mb-3"), | |
dbc.Label("Maximum Date"), | |
dcc.DatePickerSingle( | |
id='date-picker', | |
min_date_allowed=datetime(2010, 1, 1), | |
max_date_allowed=datetime(2025, 7, 5), | |
date=datetime(2025, 7, 5), | |
className="mb-3 w-100" | |
), | |
dbc.Label("Show Data Within"), | |
dcc.Dropdown( | |
id='distance-dropdown', | |
options=[ | |
{'label': 'All Distances', 'value': 'all'}, | |
{'label': 'Within 1 Mile', 'value': '1'}, | |
{'label': 'Within 3 Miles', 'value': '3'}, | |
{'label': 'Within 5 Miles', 'value': '5'}, | |
{'label': 'Within 10 Miles', 'value': '10'} | |
], | |
value='all', | |
className="mb-4" | |
), | |
dbc.Button("Search", id="search-button", color="primary", className="w-100 mb-3") | |
]), | |
html.Div(id="summary-cards", className="mt-4") | |
], className="p-3 bg-light rounded-3") | |
], md=4), | |
dbc.Col([ | |
dbc.Row([ | |
dbc.Col([ | |
dbc.Card([ | |
dbc.CardHeader("Hail Data Overview"), | |
dbc.CardBody([ | |
dcc.Loading( | |
id="loading-hail-data", | |
type="circle", | |
children=[ | |
html.Div(id="hail-data-table"), | |
html.Div(id="map-container", className="mt-4") | |
] | |
) | |
]) | |
]) | |
]) | |
]), | |
dbc.Row([ | |
dbc.Col([ | |
dbc.Card([ | |
dbc.CardHeader("Hail Size Over Time"), | |
dbc.CardBody([ | |
dcc.Loading( | |
id="loading-hail-chart", | |
type="circle", | |
children=[ | |
dcc.Graph(id="hail-size-chart") | |
] | |
) | |
]) | |
], className="mt-4") | |
]) | |
]) | |
], md=8) | |
]), | |
html.Div(id="intermediate-data", style={"display": "none"}), | |
dbc.Row([ | |
dbc.Col([ | |
html.Hr(), | |
html.P("Β© 2025 Hail Damage Analyzer", className="text-center text-muted small") | |
]) | |
], className="mt-4") | |
], fluid=True) | |
]) | |
# Main callback | |
def update_all(n_clicks, n_submit, address, date_str, distance_filter): | |
print("Update all callback triggered") # Debug | |
ctx = dash.callback_context | |
if not ctx.triggered: | |
raise PreventUpdate | |
try: | |
lat, lon = geocode(address) | |
date_obj = datetime.strptime(date_str.split('T')[0], '%Y-%m-%d') | |
date_formatted = date_obj.strftime('%Y%m%d') | |
df = get_data(lat, lon, date_formatted) | |
if df.empty: | |
error_alert = dbc.Alert("No hail data found for this location and date range.", color="warning") | |
return dash.no_update, error_alert, "", "", {}, [] | |
df["Lat_address"] = lat | |
df["Lon_address"] = lon | |
df['Miles to Hail'] = [ | |
distance(i) for i in df[['LAT', 'LON', 'Lat_address', 'Lon_address']].values | |
] | |
df['MAXSIZE'] = df['MAXSIZE'].round(2) | |
if distance_filter != 'all': | |
max_distance = float(distance_filter) | |
df = df[df['Miles to Hail'] <= max_distance] | |
max_size = df['MAXSIZE'].max() | |
last_date = df['Date_utc'].max() | |
total_events = len(df) | |
summary_cards = dbc.Row([ | |
dbc.Col([ | |
dbc.Card([ | |
dbc.CardBody([ | |
html.H6("Max Hail Size (in)", className="card-title"), | |
html.H3(f"{max_size:.1f}", className="text-center") | |
]) | |
], className="text-center") | |
], md=4, className="mb-3"), | |
dbc.Col([ | |
dbc.Card([ | |
dbc.CardBody([ | |
html.H6("Last Hail Event", className="card-title"), | |
html.H3(last_date, className="text-center") | |
]) | |
], className="text-center") | |
], md=4, className="mb-3"), | |
dbc.Col([ | |
dbc.Card([ | |
dbc.CardBody([ | |
html.H6("Total Events", className="card-title"), | |
html.H3(f"{total_events}", className="text-center") | |
]) | |
], className="text-center") | |
], md=4, className="mb-3") | |
]) | |
df_display = df[['Date_utc', 'MAXSIZE', 'Miles to Hail']].copy() | |
df_display['Miles to Hail'] = df_display['Miles to Hail'].round(2) | |
df_display = df_display.rename(columns={ | |
'Date_utc': 'Date', | |
'MAXSIZE': 'Hail Size (in)', | |
'Miles to Hail': 'Distance (miles)' | |
}) | |
data_table = dash_table.DataTable( | |
id='hail-data-table', | |
columns=[{"name": i, "id": i} for i in df_display.columns], | |
data=df_display.to_dict('records'), | |
page_size=10, | |
style_table={'overflowX': 'auto'}, | |
style_cell={ | |
'textAlign': 'left', | |
'padding': '8px', | |
'minWidth': '50px', 'width': '100px', 'maxWidth': '180px', | |
'whiteSpace': 'normal' | |
}, | |
style_header={ | |
'backgroundColor': 'rgb(230, 230, 230)', | |
'fontWeight': 'bold' | |
}, | |
style_data_conditional=[ | |
{ | |
'if': { | |
'filter_query': '{Hail Size (in)} >= 2', | |
'column_id': 'Hail Size (in)' | |
}, | |
'backgroundColor': '#ffcccc', | |
'fontWeight': 'bold' | |
} | |
] | |
) | |
map_fig = go.Figure() | |
for _, row in df.iterrows(): | |
size = row['MAXSIZE'] | |
map_fig.add_trace( | |
go.Scattermapbox( | |
lon=[row['LON']], | |
lat=[row['LAT']], | |
mode='markers', | |
marker=go.scattermapbox.Marker( | |
size=size * 3, | |
color='red', | |
opacity=0.7 | |
), | |
text=f"Size: {size} in Date: {row['Date_utc']}", | |
hoverinfo='text', | |
showlegend=False | |
) | |
) | |
if not df.empty: | |
center_lat = df['Lat_address'].iloc[0] | |
center_lon = df['Lon_address'].iloc[0] | |
map_fig.add_trace( | |
go.Scattermapbox( | |
lon=[center_lon], | |
lat=[center_lat], | |
mode='markers', | |
marker=go.scattermapbox.Marker( | |
size=14, | |
color='blue', | |
symbol='star' | |
), | |
text=f"Your Location: {address}", | |
hoverinfo='text', | |
showlegend=False | |
) | |
) | |
map_fig.update_layout( | |
mapbox_style="open-street-map", | |
mapbox=dict( | |
center=dict(lat=center_lat, lon=center_lon), | |
zoom=10 | |
), | |
margin={"r":0, "t":0, "l":0, "b":0}, | |
height=400 | |
) | |
df_chart = df.copy() | |
df_chart['Date'] = pd.to_datetime(df_chart['Date_utc']) | |
df_chart = df_chart.sort_values('Date') | |
chart_fig = px.scatter( | |
df_chart, | |
x='Date', | |
y='MAXSIZE', | |
color='Miles to Hail', | |
size='MAXSIZE', | |
hover_data=['Miles to Hail'], | |
title='Hail Size Over Time', | |
labels={ | |
'MAXSIZE': 'Hail Size (in)', | |
'Miles to Hail': 'Distance (miles)' | |
} | |
) | |
chart_fig.update_traces( | |
marker=dict( | |
line=dict(width=1, color='DarkSlateGrey'), | |
opacity=0.7 | |
), | |
selector=dict(mode='markers') | |
) | |
chart_fig.update_layout( | |
xaxis_title='Date', | |
yaxis_title='Hail Size (in)', | |
plot_bgcolor='rgba(0,0,0,0.02)', | |
paper_bgcolor='white', | |
hovermode='closest' | |
) | |
intermediate_data = df.to_json(date_format='iso', orient='split') | |
map_figure = dcc.Graph(figure=map_fig) | |
chart_figure = chart_fig | |
store_data = df.to_dict('records') | |
print("Store data populated:", store_data[:2]) | |
return ( | |
intermediate_data, | |
summary_cards, | |
data_table, | |
map_figure, | |
chart_figure, | |
store_data | |
) | |
except Exception as e: | |
error_alert = dbc.Alert(f"Error: {str(e)}", color="danger") | |
return dash.no_update, error_alert, "", "", {}, [] | |
from dash import callback_context | |
def download_csv(n_clicks, data): | |
if not n_clicks or not data: | |
return dash.no_update | |
df = pd.DataFrame(data) | |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
filename = f"hail_data_export_{timestamp}.csv" | |
csv_string = df.to_csv(index=False, encoding='utf-8') | |
return dict(content=csv_string, filename=filename) | |
if __name__ == '__main__': | |
print("π Dash app is running! Open this link in your browser:") | |
print("π http://localhost:7860/") | |
app.run(debug=True, host='0.0.0.0', port=7860) |