andrewammann commited on
Commit
9cb0f21
·
verified ·
1 Parent(s): 5589b38

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +449 -0
app.py ADDED
@@ -0,0 +1,449 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import dash
4
+ from dash import dcc, html, dash_table, callback, Input, Output, State
5
+ import dash_bootstrap_components as dbc
6
+ import pandas as pd
7
+ from datetime import datetime
8
+ import numpy as np
9
+ import plotly.express as px
10
+ import plotly.graph_objects as go
11
+ from geopy.extra.rate_limiter import RateLimiter
12
+ from geopy.geocoders import Nominatim
13
+ from dash.exceptions import PreventUpdate
14
+ from vincenty import vincenty
15
+ import duckdb
16
+ import requests
17
+ import urllib
18
+ from dotenv import load_dotenv
19
+ import time
20
+ from functools import wraps
21
+ import glob
22
+
23
+
24
+ # Load environment variables
25
+ load_dotenv()
26
+
27
+ # Initialize the Dash app
28
+ app = dash.Dash(
29
+ __name__,
30
+ external_stylesheets=[dbc.themes.BOOTSTRAP],
31
+ suppress_callback_exceptions=True
32
+ )
33
+ app.title = "Hail Damage Analyzer"
34
+ server = app.server
35
+
36
+ # Cache functions
37
+ def simple_cache(expire_seconds=300):
38
+ def decorator(func):
39
+ cache = {}
40
+ @wraps(func)
41
+ def wrapper(*args, **kwargs):
42
+ key = (func.__name__, args, frozenset(kwargs.items()))
43
+ current_time = time.time()
44
+ if key in cache:
45
+ result, timestamp = cache[key]
46
+ if current_time - timestamp < expire_seconds:
47
+ return result
48
+ result = func(*args, **kwargs)
49
+ cache[key] = (result, current_time)
50
+ return result
51
+ return wrapper
52
+ return decorator
53
+
54
+ @simple_cache(expire_seconds=300)
55
+ def duck_sql(sql_code):
56
+ con = duckdb.connect()
57
+ con.execute("PRAGMA threads=2")
58
+ con.execute("PRAGMA enable_object_cache")
59
+ return con.execute(sql_code).df()
60
+
61
+ @simple_cache(expire_seconds=300)
62
+ def get_data(lat, lon, date_str):
63
+ data_dir = r"C:/Users/aammann/OneDrive - Great American Insurance Group/Documents/Python Scripts/hail_data"
64
+ parquet_files = glob.glob(f"{data_dir}/hail_*.parquet")
65
+ print("Parquet files found:", parquet_files)
66
+ if not parquet_files:
67
+ raise ValueError("No parquet files found in the specified directory")
68
+
69
+ file_paths = ", ".join([f"'{file}'" for file in parquet_files])
70
+ lat_min, lat_max = lat-1, lat+1
71
+ lon_min, lon_max = lon-1, lon+1
72
+
73
+ code = f"""
74
+ SELECT
75
+ "#ZTIME" as "Date_utc",
76
+ LON,
77
+ LAT,
78
+ MAXSIZE
79
+ FROM read_parquet([{file_paths}], hive_partitioning=1)
80
+ WHERE
81
+ LAT BETWEEN {lat_min} AND {lat_max}
82
+ AND LON BETWEEN {lon_min} AND {lon_max}
83
+ AND "#ZTIME" <= '{date_str}'
84
+ """
85
+ return duck_sql(code)
86
+
87
+ def distance(x):
88
+ left_coords = (x[0], x[1]) # LAT, LON
89
+ right_coords = (x[2], x[3]) # Lat_address, Lon_address
90
+ return vincenty(left_coords, right_coords, miles=True)
91
+
92
+ def geocode(address):
93
+ try:
94
+ try:
95
+ address2 = address.replace(' ', '+').replace(',', '%2C')
96
+ df = pd.read_json(
97
+ f'https://geocoding.geo.census.gov/geocoder/locations/onelineaddress?address={address2}&benchmark=2020&format=json')
98
+ results = df.iloc[0, 0]['results'].iloc[0]['coordinates']
99
+ return results['y'], results['x']
100
+ except:
101
+ geolocator = Nominatim(user_agent="HailDamageAnalyzer")
102
+ geocode = RateLimiter(geolocator.geocode, min_delay_seconds=1)
103
+ location = geolocator.geocode(address)
104
+ if location:
105
+ return location.latitude, location.longitude
106
+ raise Exception("Geocoding failed")
107
+ except:
108
+ try:
109
+ geocode_key = os.getenv("GEOCODE_KEY")
110
+ if not geocode_key:
111
+ raise Exception("Geocode API key not found")
112
+ address_encoded = urllib.parse.quote(address)
113
+ url = f'https://api.geocod.io/v1.7/geocode?q={address_encoded}&api_key={geocode_key}'
114
+ response = requests.get(url, verify=False)
115
+ response.raise_for_status()
116
+ json_response = response.json()
117
+ return json_response['results'][0]['location']['lat'], json_response['results'][0]['location']['lng']
118
+ except Exception as e:
119
+ print(f"Geocoding error: {str(e)}")
120
+ raise Exception("Could not geocode address. Please try again with a different address.")
121
+
122
+ # Layout
123
+ app.layout = html.Div([
124
+ dcc.Store(id="filtered-data-store"),
125
+ dcc.Download(id="download-dataframe-csv"),
126
+ dbc.Button("Download Data as CSV", id="btn-download-csv", color="secondary", className="mb-3"),
127
+
128
+ dbc.Container([
129
+ dbc.Row([
130
+ dbc.Col([
131
+ html.H1("Hail Damage Analyzer", className="text-center my-4"),
132
+ html.P("Analyze historical hail data", className="text-center text-muted"),
133
+ html.Hr()
134
+ ], width=12)
135
+ ]),
136
+
137
+ dbc.Row([
138
+ dbc.Col([
139
+ html.Div([
140
+ html.H5("Search Parameters", className="mb-3"),
141
+ dbc.Form([
142
+ dbc.Label("Address"),
143
+ dbc.Input(id="address-input", type="text", placeholder="Enter address", value="Dallas, TX", className="mb-3"),
144
+ dbc.Label("Maximum Date"),
145
+ dcc.DatePickerSingle(
146
+ id='date-picker',
147
+ min_date_allowed=datetime(2010, 1, 1),
148
+ max_date_allowed=datetime(2025, 7, 5),
149
+ date=datetime(2025, 7, 5),
150
+ className="mb-3 w-100"
151
+ ),
152
+ dbc.Label("Show Data Within"),
153
+ dcc.Dropdown(
154
+ id='distance-dropdown',
155
+ options=[
156
+ {'label': 'All Distances', 'value': 'all'},
157
+ {'label': 'Within 1 Mile', 'value': '1'},
158
+ {'label': 'Within 3 Miles', 'value': '3'},
159
+ {'label': 'Within 5 Miles', 'value': '5'},
160
+ {'label': 'Within 10 Miles', 'value': '10'}
161
+ ],
162
+ value='all',
163
+ className="mb-4"
164
+ ),
165
+ dbc.Button("Search", id="search-button", color="primary", className="w-100 mb-3")
166
+ ]),
167
+ html.Div(id="summary-cards", className="mt-4")
168
+ ], className="p-3 bg-light rounded-3")
169
+ ], md=4),
170
+
171
+ dbc.Col([
172
+ dbc.Row([
173
+ dbc.Col([
174
+ dbc.Card([
175
+ dbc.CardHeader("Hail Data Overview"),
176
+ dbc.CardBody([
177
+ dcc.Loading(
178
+ id="loading-hail-data",
179
+ type="circle",
180
+ children=[
181
+ html.Div(id="hail-data-table"),
182
+ html.Div(id="map-container", className="mt-4")
183
+ ]
184
+ )
185
+ ])
186
+ ])
187
+ ])
188
+ ]),
189
+ dbc.Row([
190
+ dbc.Col([
191
+ dbc.Card([
192
+ dbc.CardHeader("Hail Size Over Time"),
193
+ dbc.CardBody([
194
+ dcc.Loading(
195
+ id="loading-hail-chart",
196
+ type="circle",
197
+ children=[
198
+ dcc.Graph(id="hail-size-chart")
199
+ ]
200
+ )
201
+ ])
202
+ ], className="mt-4")
203
+ ])
204
+ ])
205
+ ], md=8)
206
+ ]),
207
+
208
+ html.Div(id="intermediate-data", style={"display": "none"}),
209
+ dbc.Row([
210
+ dbc.Col([
211
+ html.Hr(),
212
+ html.P("© 2025 Hail Damage Analyzer", className="text-center text-muted small")
213
+ ])
214
+ ], className="mt-4")
215
+ ], fluid=True)
216
+ ])
217
+
218
+ # Main callback
219
+ @app.callback(
220
+ [Output("intermediate-data", "children"),
221
+ Output("summary-cards", "children"),
222
+ Output("hail-data-table", "children"),
223
+ Output("map-container", "children"),
224
+ Output("hail-size-chart", "figure"),
225
+ Output("filtered-data-store", "data")],
226
+ [Input("search-button", "n_clicks"),
227
+ Input("address-input", "n_submit")],
228
+ [State("address-input", "value"),
229
+ State("date-picker", "date"),
230
+ State("distance-dropdown", "value")]
231
+ )
232
+ def update_all(n_clicks, n_submit, address, date_str, distance_filter):
233
+ print("Update all callback triggered") # Debug
234
+ ctx = dash.callback_context
235
+ if not ctx.triggered:
236
+ raise PreventUpdate
237
+
238
+ try:
239
+ lat, lon = geocode(address)
240
+ date_obj = datetime.strptime(date_str.split('T')[0], '%Y-%m-%d')
241
+ date_formatted = date_obj.strftime('%Y%m%d')
242
+ df = get_data(lat, lon, date_formatted)
243
+
244
+ if df.empty:
245
+ error_alert = dbc.Alert("No hail data found for this location and date range.", color="warning")
246
+ return dash.no_update, error_alert, "", "", {}, []
247
+
248
+ df["Lat_address"] = lat
249
+ df["Lon_address"] = lon
250
+ df['Miles to Hail'] = [
251
+ distance(i) for i in df[['LAT', 'LON', 'Lat_address', 'Lon_address']].values
252
+ ]
253
+ df['MAXSIZE'] = df['MAXSIZE'].round(2)
254
+
255
+ if distance_filter != 'all':
256
+ max_distance = float(distance_filter)
257
+ df = df[df['Miles to Hail'] <= max_distance]
258
+
259
+ max_size = df['MAXSIZE'].max()
260
+ last_date = df['Date_utc'].max()
261
+ total_events = len(df)
262
+
263
+ summary_cards = dbc.Row([
264
+ dbc.Col([
265
+ dbc.Card([
266
+ dbc.CardBody([
267
+ html.H6("Max Hail Size (in)", className="card-title"),
268
+ html.H3(f"{max_size:.1f}", className="text-center")
269
+ ])
270
+ ], className="text-center")
271
+ ], md=4, className="mb-3"),
272
+ dbc.Col([
273
+ dbc.Card([
274
+ dbc.CardBody([
275
+ html.H6("Last Hail Event", className="card-title"),
276
+ html.H3(last_date, className="text-center")
277
+ ])
278
+ ], className="text-center")
279
+ ], md=4, className="mb-3"),
280
+ dbc.Col([
281
+ dbc.Card([
282
+ dbc.CardBody([
283
+ html.H6("Total Events", className="card-title"),
284
+ html.H3(f"{total_events}", className="text-center")
285
+ ])
286
+ ], className="text-center")
287
+ ], md=4, className="mb-3")
288
+ ])
289
+
290
+ df_display = df[['Date_utc', 'MAXSIZE', 'Miles to Hail']].copy()
291
+ df_display['Miles to Hail'] = df_display['Miles to Hail'].round(2)
292
+ df_display = df_display.rename(columns={
293
+ 'Date_utc': 'Date',
294
+ 'MAXSIZE': 'Hail Size (in)',
295
+ 'Miles to Hail': 'Distance (miles)'
296
+ })
297
+
298
+ data_table = dash_table.DataTable(
299
+ id='hail-data-table',
300
+ columns=[{"name": i, "id": i} for i in df_display.columns],
301
+ data=df_display.to_dict('records'),
302
+ page_size=10,
303
+ style_table={'overflowX': 'auto'},
304
+ style_cell={
305
+ 'textAlign': 'left',
306
+ 'padding': '8px',
307
+ 'minWidth': '50px', 'width': '100px', 'maxWidth': '180px',
308
+ 'whiteSpace': 'normal'
309
+ },
310
+ style_header={
311
+ 'backgroundColor': 'rgb(230, 230, 230)',
312
+ 'fontWeight': 'bold'
313
+ },
314
+ style_data_conditional=[
315
+ {
316
+ 'if': {
317
+ 'filter_query': '{Hail Size (in)} >= 2',
318
+ 'column_id': 'Hail Size (in)'
319
+ },
320
+ 'backgroundColor': '#ffcccc',
321
+ 'fontWeight': 'bold'
322
+ }
323
+ ]
324
+ )
325
+
326
+ map_fig = go.Figure()
327
+ for _, row in df.iterrows():
328
+ size = row['MAXSIZE']
329
+ map_fig.add_trace(
330
+ go.Scattermapbox(
331
+ lon=[row['LON']],
332
+ lat=[row['LAT']],
333
+ mode='markers',
334
+ marker=go.scattermapbox.Marker(
335
+ size=size * 3,
336
+ color='red',
337
+ opacity=0.7
338
+ ),
339
+ text=f"Size: {size} in Date: {row['Date_utc']}",
340
+ hoverinfo='text',
341
+ showlegend=False
342
+ )
343
+ )
344
+
345
+ if not df.empty:
346
+ center_lat = df['Lat_address'].iloc[0]
347
+ center_lon = df['Lon_address'].iloc[0]
348
+ map_fig.add_trace(
349
+ go.Scattermapbox(
350
+ lon=[center_lon],
351
+ lat=[center_lat],
352
+ mode='markers',
353
+ marker=go.scattermapbox.Marker(
354
+ size=14,
355
+ color='blue',
356
+ symbol='star'
357
+ ),
358
+ text=f"Your Location: {address}",
359
+ hoverinfo='text',
360
+ showlegend=False
361
+ )
362
+ )
363
+
364
+ map_fig.update_layout(
365
+ mapbox_style="open-street-map",
366
+ mapbox=dict(
367
+ center=dict(lat=center_lat, lon=center_lon),
368
+ zoom=10
369
+ ),
370
+ margin={"r":0, "t":0, "l":0, "b":0},
371
+ height=400
372
+ )
373
+
374
+ df_chart = df.copy()
375
+ df_chart['Date'] = pd.to_datetime(df_chart['Date_utc'])
376
+ df_chart = df_chart.sort_values('Date')
377
+
378
+ chart_fig = px.scatter(
379
+ df_chart,
380
+ x='Date',
381
+ y='MAXSIZE',
382
+ color='Miles to Hail',
383
+ size='MAXSIZE',
384
+ hover_data=['Miles to Hail'],
385
+ title='Hail Size Over Time',
386
+ labels={
387
+ 'MAXSIZE': 'Hail Size (in)',
388
+ 'Miles to Hail': 'Distance (miles)'
389
+ }
390
+ )
391
+
392
+ chart_fig.update_traces(
393
+ marker=dict(
394
+ line=dict(width=1, color='DarkSlateGrey'),
395
+ opacity=0.7
396
+ ),
397
+ selector=dict(mode='markers')
398
+ )
399
+
400
+ chart_fig.update_layout(
401
+ xaxis_title='Date',
402
+ yaxis_title='Hail Size (in)',
403
+ plot_bgcolor='rgba(0,0,0,0.02)',
404
+ paper_bgcolor='white',
405
+ hovermode='closest'
406
+ )
407
+
408
+ intermediate_data = df.to_json(date_format='iso', orient='split')
409
+ map_figure = dcc.Graph(figure=map_fig)
410
+ chart_figure = chart_fig
411
+ store_data = df.to_dict('records')
412
+ print("Store data populated:", store_data[:2])
413
+
414
+ return (
415
+ intermediate_data,
416
+ summary_cards,
417
+ data_table,
418
+ map_figure,
419
+ chart_figure,
420
+ store_data
421
+ )
422
+
423
+ except Exception as e:
424
+ error_alert = dbc.Alert(f"Error: {str(e)}", color="danger")
425
+ return dash.no_update, error_alert, "", "", {}, []
426
+
427
+ from dash import callback_context
428
+
429
+ @callback(
430
+ Output("download-dataframe-csv", "data"),
431
+ [Input("btn-download-csv", "n_clicks")],
432
+ [State("filtered-data-store", "data")],
433
+ prevent_initial_call=True
434
+ )
435
+ def download_csv(n_clicks, data):
436
+ if not n_clicks or not data:
437
+ return dash.no_update
438
+
439
+ df = pd.DataFrame(data)
440
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
441
+ filename = f"hail_data_export_{timestamp}.csv"
442
+ csv_string = df.to_csv(index=False, encoding='utf-8')
443
+ return dict(content=csv_string, filename=filename)
444
+
445
+
446
+ if __name__ == '__main__':
447
+ print("🚀 Dash app is running! Open this link in your browser:")
448
+ print("👉 http://127.0.0.1:8050/")
449
+ app.run(debug=True, port=8050)