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) |