diff --git a/apps/dash-medical-provider-charges/.gitignore b/apps/dash-medical-provider-charges/.gitignore deleted file mode 100644 index 943677076..000000000 --- a/apps/dash-medical-provider-charges/.gitignore +++ /dev/null @@ -1,109 +0,0 @@ -# Created by .ignore support plugin (hsz.mobi) -### Python template -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ - -./idea/ - diff --git a/apps/dash-medical-provider-charges/README.md b/apps/dash-medical-provider-charges/README.md index f16f94e92..e823b604d 100644 --- a/apps/dash-medical-provider-charges/README.md +++ b/apps/dash-medical-provider-charges/README.md @@ -46,7 +46,7 @@ Select state, cost metric and region to visualize average charges or payments(fo ## Screenshot -![Screencast](screenshot.png) +![Screencast](assets/github/screenshot.png) ## Resources * [Dash](https://dash.plot.ly/) diff --git a/apps/dash-medical-provider-charges/app.py b/apps/dash-medical-provider-charges/app.py index 00b474e9c..789a760aa 100644 --- a/apps/dash-medical-provider-charges/app.py +++ b/apps/dash-medical-provider-charges/app.py @@ -1,14 +1,35 @@ import dash -import dash_table -import dash_core_components as dcc -import dash_html_components as html +from dash import ( + html, + Input, + Output, + State, + callback, +) import plotly.graph_objs as go -from dash.dependencies import State, Input, Output -from dash.exceptions import PreventUpdate + import pandas as pd import os +from utils.helper_functions import ( + generate_aggregation, + get_lat_lon_add, + region_dropdown, + checklist, + procedure_stats, +) + +from utils.components import header, build_upper_left_panel, map_card, procedure_card + +from utils.figures import ( + generate_geo_map, + generate_procedure_plot, + hospital_datatable, + update_geo_map, + update_procedure_plot, +) + app = dash.Dash( __name__, meta_tags=[ @@ -18,396 +39,13 @@ } ], ) -app.title = "Medical Provider Charges" +# app.title = "Medical Provider Charges" server = app.server app.config["suppress_callback_exceptions"] = True -# Plotly mapbox token -mapbox_access_token = "pk.eyJ1IjoicGxvdGx5bWFwYm94IiwiYSI6ImNrOWJqb2F4djBnMjEzbG50amg0dnJieG4ifQ.Zme1-Uzoi75IaFbieBDl3A" - -state_map = { - "AK": "Alaska", - "AL": "Alabama", - "AR": "Arkansas", - "AZ": "Arizona", - "CA": "California", - "CO": "Colorado", - "CT": "Connecticut", - "DC": "District of Columbia", - "DE": "Delaware", - "FL": "Florida", - "GA": "Georgia", - "HI": "Hawaii", - "IA": "Iowa", - "ID": "Idaho", - "IL": "Illinois", - "IN": "Indiana", - "KS": "Kansas", - "KY": "Kentucky", - "LA": "Louisiana", - "MA": "Massachusetts", - "MD": "Maryland", - "ME": "Maine", - "MI": "Michigan", - "MN": "Minnesota", - "MO": "Missouri", - "MS": "Mississippi", - "MT": "Montana", - "NC": "North Carolina", - "ND": "North Dakota", - "NE": "Nebraska", - "NH": "New Hampshire", - "NJ": "New Jersey", - "NM": "New Mexico", - "NV": "Nevada", - "NY": "New York", - "OH": "Ohio", - "OK": "Oklahoma", - "OR": "Oregon", - "PA": "Pennsylvania", - "RI": "Rhode Island", - "SC": "South Carolina", - "SD": "South Dakota", - "TN": "Tennessee", - "TX": "Texas", - "UT": "Utah", - "VA": "Virginia", - "VT": "Vermont", - "WA": "Washington", - "WI": "Wisconsin", - "WV": "West Virginia", - "WY": "Wyoming", -} - -state_list = list(state_map.keys()) - -# Load data -data_dict = {} -for state in state_list: - p = os.getcwd().split(os.path.sep) - csv_path = "data/processed/df_{}_lat_lon.csv".format(state) - state_data = pd.read_csv(csv_path) - data_dict[state] = state_data - -# Cost Metric -cost_metric = [ - "Average Covered Charges", - "Average Total Payments", - "Average Medicare Payments", -] - -init_region = data_dict[state_list[1]][ - "Hospital Referral Region (HRR) Description" -].unique() - - -def generate_aggregation(df, metric): - aggregation = { - metric[0]: ["min", "mean", "max"], - metric[1]: ["min", "mean", "max"], - metric[2]: ["min", "mean", "max"], - } - grouped = ( - df.groupby(["Hospital Referral Region (HRR) Description", "Provider Name"]) - .agg(aggregation) - .reset_index() - ) - - grouped["lat"] = grouped["lon"] = grouped["Provider Street Address"] = grouped[ - "Provider Name" - ] - grouped["lat"] = grouped["lat"].apply(lambda x: get_lat_lon_add(df, x)[0]) - grouped["lon"] = grouped["lon"].apply(lambda x: get_lat_lon_add(df, x)[1]) - grouped["Provider Street Address"] = grouped["Provider Street Address"].apply( - lambda x: get_lat_lon_add(df, x)[2] - ) - - return grouped - - -def get_lat_lon_add(df, name): - return [ - df.groupby(["Provider Name"]).get_group(name)["lat"].tolist()[0], - df.groupby(["Provider Name"]).get_group(name)["lon"].tolist()[0], - df.groupby(["Provider Name"]) - .get_group(name)["Provider Street Address"] - .tolist()[0], - ] - - -def build_upper_left_panel(): - return html.Div( - id="upper-left", - className="six columns", - children=[ - html.P( - className="section-title", - children="Choose hospital on the map or procedures from the list below to see costs", - ), - html.Div( - className="control-row-1", - children=[ - html.Div( - id="state-select-outer", - children=[ - html.Label("Select a State"), - dcc.Dropdown( - id="state-select", - options=[{"label": i, "value": i} for i in state_list], - value=state_list[1], - ), - ], - ), - html.Div( - id="select-metric-outer", - children=[ - html.Label("Choose a Cost Metric"), - dcc.Dropdown( - id="metric-select", - options=[{"label": i, "value": i} for i in cost_metric], - value=cost_metric[0], - ), - ], - ), - ], - ), - html.Div( - id="region-select-outer", - className="control-row-2", - children=[ - html.Label("Pick a Region"), - html.Div( - id="checklist-container", - children=dcc.Checklist( - id="region-select-all", - options=[{"label": "Select All Regions", "value": "All"}], - value=[], - ), - ), - html.Div( - id="region-select-dropdown-outer", - children=dcc.Dropdown( - id="region-select", multi=True, searchable=True, - ), - ), - ], - ), - html.Div( - id="table-container", - className="table-container", - children=[ - html.Div( - id="table-upper", - children=[ - html.P("Hospital Charges Summary"), - dcc.Loading(children=html.Div(id="cost-stats-container")), - ], - ), - html.Div( - id="table-lower", - children=[ - html.P("Procedure Charges Summary"), - dcc.Loading( - children=html.Div(id="procedure-stats-container") - ), - ], - ), - ], - ), - ], - ) - - -def generate_geo_map(geo_data, selected_metric, region_select, procedure_select): - filtered_data = geo_data[ - geo_data["Hospital Referral Region (HRR) Description"].isin(region_select) - ] - colors = ["#21c7ef", "#76f2ff", "#ff6969", "#ff1717"] - - hospitals = [] - - lat = filtered_data["lat"].tolist() - lon = filtered_data["lon"].tolist() - average_covered_charges_mean = filtered_data[selected_metric]["mean"].tolist() - regions = filtered_data["Hospital Referral Region (HRR) Description"].tolist() - provider_name = filtered_data["Provider Name"].tolist() - - # Cost metric mapping from aggregated data - - cost_metric_data = {} - cost_metric_data["min"] = filtered_data[selected_metric]["mean"].min() - cost_metric_data["max"] = filtered_data[selected_metric]["mean"].max() - cost_metric_data["mid"] = (cost_metric_data["min"] + cost_metric_data["max"]) / 2 - cost_metric_data["low_mid"] = ( - cost_metric_data["min"] + cost_metric_data["mid"] - ) / 2 - cost_metric_data["high_mid"] = ( - cost_metric_data["mid"] + cost_metric_data["max"] - ) / 2 - - for i in range(len(lat)): - val = average_covered_charges_mean[i] - region = regions[i] - provider = provider_name[i] - - if val <= cost_metric_data["low_mid"]: - color = colors[0] - elif cost_metric_data["low_mid"] < val <= cost_metric_data["mid"]: - color = colors[1] - elif cost_metric_data["mid"] < val <= cost_metric_data["high_mid"]: - color = colors[2] - else: - color = colors[3] - - selected_index = [] - if provider in procedure_select["hospital"]: - selected_index = [0] - - hospital = go.Scattermapbox( - lat=[lat[i]], - lon=[lon[i]], - mode="markers", - marker=dict( - color=color, - showscale=True, - colorscale=[ - [0, "#21c7ef"], - [0.33, "#76f2ff"], - [0.66, "#ff6969"], - [1, "#ff1717"], - ], - cmin=cost_metric_data["min"], - cmax=cost_metric_data["max"], - size=10 - * (1 + (val + cost_metric_data["min"]) / cost_metric_data["mid"]), - colorbar=dict( - x=0.9, - len=0.7, - title=dict( - text="Average Cost", - font={"color": "#737a8d", "family": "Open Sans"}, - ), - titleside="top", - tickmode="array", - tickvals=[cost_metric_data["min"], cost_metric_data["max"]], - ticktext=[ - "${:,.2f}".format(cost_metric_data["min"]), - "${:,.2f}".format(cost_metric_data["max"]), - ], - ticks="outside", - thickness=15, - tickfont={"family": "Open Sans", "color": "#737a8d"}, - ), - ), - opacity=0.8, - selectedpoints=selected_index, - selected=dict(marker={"color": "#ffff00"}), - customdata=[(provider, region)], - hoverinfo="text", - text=provider - + "
" - + region - + "
Average Procedure Cost:" - + " ${:,.2f}".format(val), - ) - hospitals.append(hospital) - - layout = go.Layout( - margin=dict(l=10, r=10, t=20, b=10, pad=5), - plot_bgcolor="#171b26", - paper_bgcolor="#171b26", - clickmode="event+select", - hovermode="closest", - showlegend=False, - mapbox=go.layout.Mapbox( - accesstoken=mapbox_access_token, - bearing=10, - center=go.layout.mapbox.Center( - lat=filtered_data.lat.mean(), lon=filtered_data.lon.mean() - ), - pitch=5, - zoom=5, - style="mapbox://styles/plotlymapbox/cjvppq1jl1ips1co3j12b9hex", - ), - ) - - return {"data": hospitals, "layout": layout} - - -def generate_procedure_plot(raw_data, cost_select, region_select, provider_select): - procedure_data = raw_data[ - raw_data["Hospital Referral Region (HRR) Description"].isin(region_select) - ].reset_index() - - traces = [] - selected_index = procedure_data[ - procedure_data["Provider Name"].isin(provider_select) - ].index - - text = ( - procedure_data["Provider Name"] - + "
" - + "" - + procedure_data["DRG Definition"].map(str) - + "/
" - + "Average Procedure Cost: $ " - + procedure_data[cost_select].map(str) - ) - - provider_trace = go.Box( - y=procedure_data["DRG Definition"], - x=procedure_data[cost_select], - name="", - customdata=procedure_data["Provider Name"], - boxpoints="all", - jitter=0, - pointpos=0, - hoveron="points", - fillcolor="rgba(0,0,0,0)", - line=dict(color="rgba(0,0,0,0)"), - hoverinfo="text", - hovertext=text, - selectedpoints=selected_index, - selected=dict(marker={"color": "#FFFF00", "size": 13}), - unselected=dict(marker={"opacity": 0.2}), - marker=dict( - line=dict(width=1, color="#000000"), - color="#21c7ef", - opacity=0.7, - symbol="square", - size=12, - ), - ) - - traces.append(provider_trace) - - layout = go.Layout( - showlegend=False, - hovermode="closest", - dragmode="select", - clickmode="event+select", - xaxis=dict( - zeroline=False, - automargin=True, - showticklabels=True, - title=dict(text="Procedure Cost", font=dict(color="#737a8d")), - linecolor="#737a8d", - tickfont=dict(color="#737a8d"), - type="log", - ), - yaxis=dict( - automargin=True, - showticklabels=True, - tickfont=dict(color="#737a8d"), - gridcolor="#171b26", - ), - plot_bgcolor="#171b26", - paper_bgcolor="#171b26", - ) - # x : procedure, y: cost, - return {"data": traces, "layout": layout} +# def header(app, header_color, header, subheader=None, header_background_color="transparent"): app.layout = html.Div( @@ -417,8 +55,9 @@ def generate_procedure_plot(raw_data, cost_select, region_select, provider_selec id="banner", className="banner", children=[ - html.H6("Dash Clinical Analytics"), - html.Img(src=app.get_asset_url("plotly_logo_white.png")), + header( + app, "black", "Dash Clinical Analytics", "Medical Provider Charges" + ), ], ), html.Div( @@ -426,309 +65,77 @@ def generate_procedure_plot(raw_data, cost_select, region_select, provider_selec className="row", children=[ build_upper_left_panel(), - html.Div( - id="geo-map-outer", - className="six columns", - children=[ - html.P( - id="map-title", - children="Medicare Provider Charges in the State of {}".format( - state_map[state_list[0]] - ), - ), - html.Div( - id="geo-map-loading-outer", - children=[ - dcc.Loading( - id="loading", - children=dcc.Graph( - id="geo-map", - figure={ - "data": [], - "layout": dict( - plot_bgcolor="#171b26", - paper_bgcolor="#171b26", - ), - }, - ), - ) - ], - ), - ], - ), - ], - ), - html.Div( - id="lower-container", - children=[ - dcc.Graph( - id="procedure-plot", - figure=generate_procedure_plot( - data_dict[state_list[1]], cost_metric[0], init_region, [] - ), - ) + map_card(), ], ), + procedure_card(), ], ) -@app.callback( - [ - Output("region-select", "value"), - Output("region-select", "options"), - Output("map-title", "children"), - ], - [Input("region-select-all", "value"), Input("state-select", "value"),], +@callback( + Output("region-select", "value"), + Output("region-select", "options"), + Output("map-title", "children"), + Input("region-select-all", "value"), + Input("state-select", "value"), ) def update_region_dropdown(select_all, state_select): - state_raw_data = data_dict[state_select] - regions = state_raw_data["Hospital Referral Region (HRR) Description"].unique() - options = [{"label": i, "value": i} for i in regions] - - ctx = dash.callback_context - if ctx.triggered[0]["prop_id"].split(".")[0] == "region-select-all": - if select_all == ["All"]: - value = [i["value"] for i in options] - else: - value = dash.no_update - else: - value = regions[:4] - return ( - value, - options, - "Medicare Provider Charges in the State of {}".format(state_map[state_select]), - ) + return region_dropdown(select_all, state_select) -@app.callback( +@callback( Output("checklist-container", "children"), - [Input("region-select", "value")], - [State("region-select", "options"), State("region-select-all", "value")], + Input("region-select", "value"), + State("region-select", "options"), + State("region-select-all", "value"), ) def update_checklist(selected, select_options, checked): - if len(selected) < len(select_options) and len(checked) == 0: - raise PreventUpdate() - - elif len(selected) < len(select_options) and len(checked) == 1: - return dcc.Checklist( - id="region-select-all", - options=[{"label": "Select All Regions", "value": "All"}], - value=[], - ) - - elif len(selected) == len(select_options) and len(checked) == 1: - raise PreventUpdate() + return checklist(selected, select_options, checked) - return dcc.Checklist( - id="region-select-all", - options=[{"label": "Select All Regions", "value": "All"}], - value=["All"], - ) - -@app.callback( +@callback( Output("cost-stats-container", "children"), - [ - Input("geo-map", "selectedData"), - Input("procedure-plot", "selectedData"), - Input("metric-select", "value"), - Input("state-select", "value"), - ], + Input("geo-map", "selectedData"), + Input("procedure-plot", "selectedData"), + Input("metric-select", "value"), + Input("state-select", "value"), ) def update_hospital_datatable(geo_select, procedure_select, cost_select, state_select): - state_agg = generate_aggregation(data_dict[state_select], cost_metric) - # make table from geo-select - geo_data_dict = { - "Provider Name": [], - "City": [], - "Street Address": [], - "Maximum Cost ($)": [], - "Minimum Cost ($)": [], - } - - ctx = dash.callback_context - if ctx.triggered: - prop_id = ctx.triggered[0]["prop_id"].split(".")[0] - - # make table from procedure-select - if prop_id == "procedure-plot" and procedure_select is not None: - - for point in procedure_select["points"]: - provider = point["customdata"] - - dff = state_agg[state_agg["Provider Name"] == provider] - - geo_data_dict["Provider Name"].append(point["customdata"]) - city = dff["Hospital Referral Region (HRR) Description"].tolist()[0] - geo_data_dict["City"].append(city) - - address = dff["Provider Street Address"].tolist()[0] - geo_data_dict["Street Address"].append(address) - - geo_data_dict["Maximum Cost ($)"].append( - dff[cost_select]["max"].tolist()[0] - ) - geo_data_dict["Minimum Cost ($)"].append( - dff[cost_select]["min"].tolist()[0] - ) - - if prop_id == "geo-map" and geo_select is not None: - - for point in geo_select["points"]: - provider = point["customdata"][0] - dff = state_agg[state_agg["Provider Name"] == provider] + return hospital_datatable(geo_select, procedure_select, cost_select, state_select) - geo_data_dict["Provider Name"].append(point["customdata"][0]) - geo_data_dict["City"].append(point["customdata"][1].split("- ")[1]) - address = dff["Provider Street Address"].tolist()[0] - geo_data_dict["Street Address"].append(address) - - geo_data_dict["Maximum Cost ($)"].append( - dff[cost_select]["max"].tolist()[0] - ) - geo_data_dict["Minimum Cost ($)"].append( - dff[cost_select]["min"].tolist()[0] - ) - - geo_data_df = pd.DataFrame(data=geo_data_dict) - data = geo_data_df.to_dict("rows") - - else: - data = [{}] - - return dash_table.DataTable( - id="cost-stats-table", - columns=[{"name": i, "id": i} for i in geo_data_dict.keys()], - data=data, - filter_action="native", - page_size=5, - style_cell={"background-color": "#242a3b", "color": "#7b7d8d"}, - style_as_list_view=False, - style_header={"background-color": "#1f2536", "padding": "0px 5px"}, - ) - - -@app.callback( +@callback( Output("procedure-stats-container", "children"), - [ - Input("procedure-plot", "selectedData"), - Input("geo-map", "selectedData"), - Input("metric-select", "value"), - ], - [State("state-select", "value")], + Input("procedure-plot", "selectedData"), + Input("geo-map", "selectedData"), + Input("metric-select", "value"), + State("state-select", "value"), ) def update_procedure_stats(procedure_select, geo_select, cost_select, state_select): - procedure_dict = { - "DRG": [], - "Procedure": [], - "Provider Name": [], - "Cost Summary": [], - } - - ctx = dash.callback_context - prop_id = "" - if ctx.triggered: - prop_id = ctx.triggered[0]["prop_id"].split(".")[0] - - if prop_id == "procedure-plot" and procedure_select is not None: - for point in procedure_select["points"]: - procedure_dict["DRG"].append(point["y"].split(" - ")[0]) - procedure_dict["Procedure"].append(point["y"].split(" - ")[1]) - - procedure_dict["Provider Name"].append(point["customdata"]) - procedure_dict["Cost Summary"].append(("${:,.2f}".format(point["x"]))) - - # Display all procedures at selected hospital - provider_select = [] + return procedure_stats(procedure_select, geo_select, cost_select, state_select) - if prop_id == "geo-map" and geo_select is not None: - for point in geo_select["points"]: - provider = point["customdata"][0] - provider_select.append(provider) - state_raw_data = data_dict[state_select] - provider_filtered = state_raw_data[ - state_raw_data["Provider Name"].isin(provider_select) - ] - - for i in range(len(provider_filtered)): - procedure_dict["DRG"].append( - provider_filtered.iloc[i]["DRG Definition"].split(" - ")[0] - ) - procedure_dict["Procedure"].append( - provider_filtered.iloc[i]["DRG Definition"].split(" - ")[1] - ) - procedure_dict["Provider Name"].append( - provider_filtered.iloc[i]["Provider Name"] - ) - procedure_dict["Cost Summary"].append( - "${:,.2f}".format(provider_filtered.iloc[0][cost_select]) - ) - - procedure_data_df = pd.DataFrame(data=procedure_dict) - - return dash_table.DataTable( - id="procedure-stats-table", - columns=[{"name": i, "id": i} for i in procedure_dict.keys()], - data=procedure_data_df.to_dict("rows"), - filter_action="native", - sort_action="native", - style_cell={ - "textOverflow": "ellipsis", - "background-color": "#242a3b", - "color": "#7b7d8d", - }, - sort_mode="multi", - page_size=5, - style_as_list_view=False, - style_header={"background-color": "#1f2536", "padding": "2px 12px 0px 12px"}, - ) - - -@app.callback( +@callback( Output("geo-map", "figure"), - [ - Input("metric-select", "value"), - Input("region-select", "value"), - Input("procedure-plot", "selectedData"), - Input("state-select", "value"), - ], + Input("metric-select", "value"), + Input("region-select", "value"), + Input("procedure-plot", "selectedData"), + Input("state-select", "value"), ) -def update_geo_map(cost_select, region_select, procedure_select, state_select): - # generate geo map from state-select, procedure-select - state_agg_data = generate_aggregation(data_dict[state_select], cost_metric) - - provider_data = {"procedure": [], "hospital": []} - if procedure_select is not None: - for point in procedure_select["points"]: - provider_data["procedure"].append(point["y"]) - provider_data["hospital"].append(point["customdata"]) - - return generate_geo_map(state_agg_data, cost_select, region_select, provider_data) +def return_updated_geo_map(cost_select, region_select, procedure_select, state_select): + return update_geo_map(cost_select, region_select, procedure_select, state_select) -@app.callback( +@callback( Output("procedure-plot", "figure"), - [ - Input("metric-select", "value"), - Input("region-select", "value"), - Input("geo-map", "selectedData"), - Input("state-select", "value"), - ], + Input("metric-select", "value"), + Input("region-select", "value"), + Input("geo-map", "selectedData"), + Input("state-select", "value"), ) -def update_procedure_plot(cost_select, region_select, geo_select, state_select): - # generate procedure plot from selected provider - state_raw_data = data_dict[state_select] - - provider_select = [] - if geo_select is not None: - for point in geo_select["points"]: - provider_select.append(point["customdata"][0]) - return generate_procedure_plot( - state_raw_data, cost_select, region_select, provider_select - ) +def return_updated_procedure_plot(cost_select, region_select, geo_select, state_select): + return update_procedure_plot(cost_select, region_select, geo_select, state_select) if __name__ == "__main__": diff --git a/apps/dash-medical-provider-charges/assets/base.css b/apps/dash-medical-provider-charges/assets/base.css index f6c30a588..a0656709b 100644 --- a/apps/dash-medical-provider-charges/assets/base.css +++ b/apps/dash-medical-provider-charges/assets/base.css @@ -40,81 +40,172 @@ max-width: 960px; margin: 0 auto; padding: 0 20px; - box-sizing: border-box; } + box-sizing: border-box; +} + .column, .columns { width: 100%; float: left; - box-sizing: border-box; } + box-sizing: border-box; +} /* For devices larger than 400px */ @media (min-width: 400px) { .container { width: 85%; - padding: 0; } + padding: 0; + } } /* For devices larger than 550px */ @media (min-width: 550px) { .container { - width: 80%; } + width: 80%; + } + .column, .columns { - margin-left: 2%; } + margin-left: 2%; + } + .column:first-child, .columns:first-child { - margin-left: 2%; } + margin-left: 2%; + } .one.column, - .one.columns { width: 4.66666666667%; } - .two.columns { width: 13.3333333333%; } - .three.columns { width: 22%; } - .four.columns { width: 30.6666666667%; } - .five.columns { width: 39.3333333333%; } - .six.columns { width: 48%; } - .seven.columns { width: 56.6666666667%; } - .eight.columns { width: 65.3333333333%; } - .nine.columns { width: 74.0%; } - .ten.columns { width: 82.6666666667%; } - .eleven.columns { width: 91.3333333333%; } - .twelve.columns { width: 100%; margin-left: 0; } - - .one-third.column { width: 30.6666666667%; } - .two-thirds.column { width: 65.3333333333%; } - - .one-half.column { width: 48%; } + .one.columns { + width: 4.66666666667%; + } + + .two.columns { + width: 13.3333333333%; + } + + .three.columns { + width: 22%; + } + + .four.columns { + width: 30.6666666667%; + } + + .five.columns { + width: 39.3333333333%; + } + + .six.columns { + width: 48%; + } + + .seven.columns { + width: 56.6666666667%; + } + + .eight.columns { + width: 65.3333333333%; + } + + .nine.columns { + width: 74.0%; + } + + .ten.columns { + width: 82.6666666667%; + } + + .eleven.columns { + width: 91.3333333333%; + } + + .twelve.columns { + width: 100%; + margin-left: 0; + } + + .one-third.column { + width: 30.6666666667%; + } + + .two-thirds.column { + width: 65.3333333333%; + } + + .one-half.column { + width: 48%; + } /* Offsets */ .offset-by-one.column, - .offset-by-one.columns { margin-left: 8.66666666667%; } + .offset-by-one.columns { + margin-left: 8.66666666667%; + } + .offset-by-two.column, - .offset-by-two.columns { margin-left: 17.3333333333%; } + .offset-by-two.columns { + margin-left: 17.3333333333%; + } + .offset-by-three.column, - .offset-by-three.columns { margin-left: 26%; } + .offset-by-three.columns { + margin-left: 26%; + } + .offset-by-four.column, - .offset-by-four.columns { margin-left: 34.6666666667%; } + .offset-by-four.columns { + margin-left: 34.6666666667%; + } + .offset-by-five.column, - .offset-by-five.columns { margin-left: 43.3333333333%; } + .offset-by-five.columns { + margin-left: 43.3333333333%; + } + .offset-by-six.column, - .offset-by-six.columns { margin-left: 52%; } + .offset-by-six.columns { + margin-left: 52%; + } + .offset-by-seven.column, - .offset-by-seven.columns { margin-left: 60.6666666667%; } + .offset-by-seven.columns { + margin-left: 60.6666666667%; + } + .offset-by-eight.column, - .offset-by-eight.columns { margin-left: 69.3333333333%; } + .offset-by-eight.columns { + margin-left: 69.3333333333%; + } + .offset-by-nine.column, - .offset-by-nine.columns { margin-left: 78.0%; } + .offset-by-nine.columns { + margin-left: 78.0%; + } + .offset-by-ten.column, - .offset-by-ten.columns { margin-left: 86.6666666667%; } + .offset-by-ten.columns { + margin-left: 86.6666666667%; + } + .offset-by-eleven.column, - .offset-by-eleven.columns { margin-left: 95.3333333333%; } + .offset-by-eleven.columns { + margin-left: 95.3333333333%; + } .offset-by-one-third.column, - .offset-by-one-third.columns { margin-left: 34.6666666667%; } + .offset-by-one-third.columns { + margin-left: 34.6666666667%; + } + .offset-by-two-thirds.column, - .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } + .offset-by-two-thirds.columns { + margin-left: 69.3333333333%; + } .offset-by-one-half.column, - .offset-by-one-half.columns { margin-left: 52%; } + .offset-by-one-half.columns { + margin-left: 52%; + } } @@ -125,30 +216,82 @@ html is set to 62.5% so that all the REM measurements throughout Skeleton are based on 10px sizing. So basically 1.5rem = 15px :) */ html { - font-size: 62.5%; } + font-size: 62.5%; +} + body { - font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ + font-size: 1.5em; + /* currently ems cause chrome bug misinterpreting rems on body element */ line-height: 1.6; font-weight: 400; font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; - color: rgb(50, 50, 50); } + color: rgb(50, 50, 50); +} /* Typography –––––––––––––––––––––––––––––––––––––––––––––––––– */ -h1, h2, h3, h4, h5, h6 { +h1, +h2, +h3, +h4, +h5, +h6 { margin-top: 0; margin-bottom: 0; - font-weight: 300; } -h1 { font-size: 4.5rem; line-height: 1.2; letter-spacing: -.1rem; margin-bottom: 2rem; } -h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; margin-bottom: 1.8rem; margin-top: 1.8rem;} -h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; margin-bottom: 1.5rem; margin-top: 1.5rem;} -h4 { font-size: 2.6rem; line-height: 1.35; letter-spacing: -.08rem; margin-bottom: 1.2rem; margin-top: 1.2rem;} -h5 { font-size: 2.2rem; line-height: 1.5; letter-spacing: -.05rem; margin-bottom: 0.6rem; margin-top: 0.6rem;} -h6 { font-size: 2.0rem; line-height: 1.6; letter-spacing: 0; margin-bottom: 0.75rem; margin-top: 0.75rem;} + font-weight: 300; +} + +h1 { + font-size: 4.5rem; + line-height: 1.2; + letter-spacing: -.1rem; + margin-bottom: 2rem; +} + +h2 { + font-size: 3.6rem; + line-height: 1.25; + letter-spacing: -.1rem; + margin-bottom: 1.8rem; + margin-top: 1.8rem; +} + +h3 { + font-size: 3.0rem; + line-height: 1.3; + letter-spacing: -.1rem; + margin-bottom: 1.5rem; + margin-top: 1.5rem; +} + +h4 { + font-size: 2.6rem; + line-height: 1.35; + letter-spacing: -.08rem; + margin-bottom: 1.2rem; + margin-top: 1.2rem; +} + +h5 { + font-size: 2.2rem; + line-height: 1.5; + letter-spacing: -.05rem; + margin-bottom: 0.6rem; + margin-top: 0.6rem; +} + +h6 { + font-size: 2.0rem; + line-height: 1.6; + letter-spacing: 0; + margin-bottom: 0.75rem; + margin-top: 0.75rem; +} p { - margin-top: 0; } + margin-top: 0; +} /* Blockquotes @@ -167,9 +310,12 @@ blockquote { a { color: #1EAEDB; text-decoration: underline; - cursor: pointer;} + cursor: pointer; +} + a:hover { - color: #0FA0CE; } + color: #0FA0CE; +} /* Buttons @@ -195,7 +341,9 @@ input[type="button"] { border-radius: 4px; border: 1px solid #bbb; cursor: pointer; - box-sizing: border-box; } + box-sizing: border-box; +} + .button:hover, button:hover, input[type="submit"]:hover, @@ -208,7 +356,9 @@ input[type="reset"]:focus, input[type="button"]:focus { color: #333; border-color: #888; - outline: 0; } + outline: 0; +} + .button.button-primary, button.button-primary, input[type="submit"].button-primary, @@ -216,7 +366,9 @@ input[type="reset"].button-primary, input[type="button"].button-primary { color: #FFF; background-color: #33C3F0; - border-color: #33C3F0; } + border-color: #33C3F0; +} + .button.button-primary:hover, button.button-primary:hover, input[type="submit"].button-primary:hover, @@ -229,7 +381,8 @@ input[type="reset"].button-primary:focus, input[type="button"].button-primary:focus { color: #FFF; background-color: #1EAEDB; - border-color: #1EAEDB; } + border-color: #1EAEDB; +} /* Forms @@ -244,14 +397,18 @@ input[type="password"], textarea, select { height: 38px; - padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ + padding: 6px 10px; + /* The 6px vertically centers text on FF, ignored by Webkit */ background-color: #fff; border: 1px solid #D1D1D1; border-radius: 4px; box-shadow: none; box-sizing: border-box; font-family: inherit; - font-size: inherit; /*https://stackoverflow.com/questions/6080413/why-doesnt-input-inherit-the-font-from-body*/} + font-size: inherit; + /*https://stackoverflow.com/questions/6080413/why-doesnt-input-inherit-the-font-from-body*/ +} + /* Removes awkward default styles on some inputs for iOS */ input[type="email"], input[type="number"], @@ -262,12 +419,16 @@ input[type="url"], input[type="password"], textarea { -webkit-appearance: none; - -moz-appearance: none; - appearance: none; } + -moz-appearance: none; + appearance: none; +} + textarea { min-height: 65px; padding-top: 6px; - padding-bottom: 6px; } + padding-bottom: 6px; +} + input[type="email"]:focus, input[type="number"]:focus, input[type="search"]:focus, @@ -282,36 +443,53 @@ textarea:focus, label, legend { display: block; - margin-bottom: 0px; } + margin-bottom: 0px; +} + fieldset { padding: 0; - border-width: 0; } + border-width: 0; +} + input[type="checkbox"], input[type="radio"] { - display: inline; } -label > .label-body { + display: inline; +} + +label>.label-body { display: inline-block; margin-left: .5rem; - font-weight: normal; } + font-weight: normal; +} /* Lists –––––––––––––––––––––––––––––––––––––––––––––––––– */ ul { - list-style: circle inside; } + list-style: circle inside; +} + ol { - list-style: decimal inside; } -ol, ul { + list-style: decimal inside; +} + +ol, +ul { padding-left: 0; - margin-top: 0; } + margin-top: 0; +} + ul ul, ul ol, ol ol, ol ul { margin: 1.5rem 0 1.5rem 3rem; - font-size: 90%; } + font-size: 90%; +} + li { - margin-bottom: 1rem; } + margin-bottom: 1rem; +} /* Tables @@ -319,52 +497,72 @@ li { table { border-collapse: collapse; } + th, td { padding: 12px 15px; text-align: left; - border-bottom: 1px solid #E1E1E1; } + border-bottom: 1px solid #E1E1E1; +} + th:first-child, td:first-child { - padding-left: 0; } + padding-left: 0; +} + th:last-child, td:last-child { - padding-right: 0; } + padding-right: 0; +} /* Spacing –––––––––––––––––––––––––––––––––––––––––––––––––– */ button, .button { - margin-bottom: 0rem; } + margin-bottom: 0rem; +} + input, textarea, select, fieldset { - margin-bottom: 0rem; } + margin-bottom: 0rem; +} + pre, dl, figure, table, form { - margin-bottom: 0rem; } + margin-bottom: 0rem; +} + p, ul, ol { - margin-bottom: 0.75rem; } + margin-bottom: 0.75rem; +} /* Utilities –––––––––––––––––––––––––––––––––––––––––––––––––– */ .u-full-width { width: 100%; - box-sizing: border-box; } + box-sizing: border-box; +} + .u-max-full-width { max-width: 100%; - box-sizing: border-box; } + box-sizing: border-box; +} + .u-pull-right { - float: right; } + float: right; +} + .u-pull-left { - float: left; } + float: left; +} /* Misc @@ -373,7 +571,8 @@ hr { margin-top: 3rem; margin-bottom: 3.5rem; border-width: 0; - border-top: 1px solid #E1E1E1; } + border-top: 1px solid #E1E1E1; +} /* Clearing @@ -385,7 +584,8 @@ hr { .u-cf { content: ""; display: table; - clear: both; } + clear: both; +} /* Media Queries diff --git a/apps/dash-medical-provider-charges/screenshot.png b/apps/dash-medical-provider-charges/assets/github/screenshot.png similarity index 100% rename from apps/dash-medical-provider-charges/screenshot.png rename to apps/dash-medical-provider-charges/assets/github/screenshot.png diff --git a/apps/dash-medical-provider-charges/assets/images/plotly-logo-dark-theme.png b/apps/dash-medical-provider-charges/assets/images/plotly-logo-dark-theme.png new file mode 100644 index 000000000..984dd57ab Binary files /dev/null and b/apps/dash-medical-provider-charges/assets/images/plotly-logo-dark-theme.png differ diff --git a/apps/dash-medical-provider-charges/assets/plotly_logo_white.png b/apps/dash-medical-provider-charges/assets/plotly_logo_white.png deleted file mode 100644 index 489ed362f..000000000 Binary files a/apps/dash-medical-provider-charges/assets/plotly_logo_white.png and /dev/null differ diff --git a/apps/dash-medical-provider-charges/constants.py b/apps/dash-medical-provider-charges/constants.py new file mode 100644 index 000000000..230bef51e --- /dev/null +++ b/apps/dash-medical-provider-charges/constants.py @@ -0,0 +1,80 @@ +import os +import pandas as pd + +# Plotly mapbox token +mapbox_access_token = "pk.eyJ1IjoicGxvdGx5bWFwYm94IiwiYSI6ImNrOWJqb2F4djBnMjEzbG50amg0dnJieG4ifQ.Zme1-Uzoi75IaFbieBDl3A" + +state_map = { + "AK": "Alaska", + "AL": "Alabama", + "AR": "Arkansas", + "AZ": "Arizona", + "CA": "California", + "CO": "Colorado", + "CT": "Connecticut", + "DC": "District of Columbia", + "DE": "Delaware", + "FL": "Florida", + "GA": "Georgia", + "HI": "Hawaii", + "IA": "Iowa", + "ID": "Idaho", + "IL": "Illinois", + "IN": "Indiana", + "KS": "Kansas", + "KY": "Kentucky", + "LA": "Louisiana", + "MA": "Massachusetts", + "MD": "Maryland", + "ME": "Maine", + "MI": "Michigan", + "MN": "Minnesota", + "MO": "Missouri", + "MS": "Mississippi", + "MT": "Montana", + "NC": "North Carolina", + "ND": "North Dakota", + "NE": "Nebraska", + "NH": "New Hampshire", + "NJ": "New Jersey", + "NM": "New Mexico", + "NV": "Nevada", + "NY": "New York", + "OH": "Ohio", + "OK": "Oklahoma", + "OR": "Oregon", + "PA": "Pennsylvania", + "RI": "Rhode Island", + "SC": "South Carolina", + "SD": "South Dakota", + "TN": "Tennessee", + "TX": "Texas", + "UT": "Utah", + "VA": "Virginia", + "VT": "Vermont", + "WA": "Washington", + "WI": "Wisconsin", + "WV": "West Virginia", + "WY": "Wyoming", +} + +state_list = list(state_map.keys()) + +# Load data +data_dict = {} +for state in state_list: + p = os.getcwd().split(os.path.sep) + csv_path = "data/processed/df_{}_lat_lon.csv".format(state) + state_data = pd.read_csv(csv_path) + data_dict[state] = state_data + +# Cost Metric +cost_metric = [ + "Average Covered Charges", + "Average Total Payments", + "Average Medicare Payments", +] + +init_region = data_dict[state_list[1]][ + "Hospital Referral Region (HRR) Description" +].unique() diff --git a/apps/dash-medical-provider-charges/gitignore b/apps/dash-medical-provider-charges/gitignore new file mode 100644 index 000000000..d8e187da3 --- /dev/null +++ b/apps/dash-medical-provider-charges/gitignore @@ -0,0 +1,191 @@ +# .gitignore specifies the files that shouldn't be included +# in version control and therefore shouldn't be included when +# deploying an application to Dash Enterprise +# This is a very exhaustive list! +# This list was based off of https://github.com/github/gitignore + +# Ignore data that is generated during the runtime of an application +# This folder is used by the "Large Data" sample applications +runtime_data/ +data/ + +# Omit SQLite databases that may be produced by dash-snapshots in development +*.db + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + + +# Jupyter Notebook + +.ipynb_checkpoints +*/.ipynb_checkpoints/* + +# IPython +profile_default/ +ipython_config.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + + +# macOS General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# History files +.Rhistory +.Rapp.history + +# Session Data files +.RData + +# User-specific files +.Ruserdata + +# Example code in package build process +*-Ex.R + +# Output files from R CMD check +/*.Rcheck/ + +# RStudio files +.Rproj.user/ + +# produced vignettes +vignettes/*.html +vignettes/*.pdf + +# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 +.httr-oauth + +# knitr and R markdown default cache directories +*_cache/ +/cache/ + +# Temporary files created by R markdown +*.utf8.md +*.knit.md + +# R Environment Variables +.Renviron + +# Linux +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# SublineText +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings \ No newline at end of file diff --git a/apps/dash-medical-provider-charges/requirements.txt b/apps/dash-medical-provider-charges/requirements.txt index d9c4b1df2..980b4ef72 100644 --- a/apps/dash-medical-provider-charges/requirements.txt +++ b/apps/dash-medical-provider-charges/requirements.txt @@ -1,6 +1,5 @@ -dash==1.12.0 -gunicorn>=19.9.0 -numpy==1.16.2 -pandas==0.24.2 +dash==2.4.1 +pandas==1.4.2 +gunicorn==20.1.0 uszipcode==0.2.2 -plotly==4.7.1 + diff --git a/apps/dash-medical-provider-charges/runtime.txt b/apps/dash-medical-provider-charges/runtime.txt new file mode 100644 index 000000000..cfa660c42 --- /dev/null +++ b/apps/dash-medical-provider-charges/runtime.txt @@ -0,0 +1 @@ +python-3.8.0 \ No newline at end of file diff --git a/apps/dash-medical-provider-charges/utils/components.py b/apps/dash-medical-provider-charges/utils/components.py new file mode 100644 index 000000000..afc882c62 --- /dev/null +++ b/apps/dash-medical-provider-charges/utils/components.py @@ -0,0 +1,163 @@ +from dash import html, dcc +from constants import state_list, cost_metric, state_map, data_dict, init_region +from utils.figures import generate_procedure_plot + + +def header( + app, header_color, header, subheader=None, header_background_color="transparent" +): + left_headers = html.Div( + [ + html.Div(header, className="header-title"), + html.Div(subheader, className="subheader-title"), + ], + style={"color": header_color}, + ) + + logo = html.Img(src=app.get_asset_url("images/plotly-logo-dark-theme.png")) + logo_link = html.A(logo, href="https://plotly.com/get-demo/", target="_blank") + demo_link = html.A( + "LEARN MORE", + href="https://plotly.com/dash/", + target="_blank", + className="demo-button", + ) + right_logos = html.Div([demo_link, logo_link], className="header-logos") + + return html.Div( + [left_headers, right_logos], + className="header", + style={"background-color": header_background_color}, + ) + + +def build_upper_left_panel(): + return html.Div( + id="upper-left", + className="six columns", + children=[ + html.P( + className="section-title", + children="Choose hospital on the map or procedures from the list below to see costs", + ), + html.Div( + className="control-row-1", + children=[ + html.Div( + id="state-select-outer", + children=[ + html.Label("Select a State"), + dcc.Dropdown( + id="state-select", + options=[{"label": i, "value": i} for i in state_list], + value=state_list[1], + ), + ], + ), + html.Div( + id="select-metric-outer", + children=[ + html.Label("Choose a Cost Metric"), + dcc.Dropdown( + id="metric-select", + options=[{"label": i, "value": i} for i in cost_metric], + value=cost_metric[0], + ), + ], + ), + ], + ), + html.Div( + id="region-select-outer", + className="control-row-2", + children=[ + html.Label("Pick a Region"), + html.Div( + id="checklist-container", + children=dcc.Checklist( + id="region-select-all", + options=[{"label": "Select All Regions", "value": "All"}], + value=[], + ), + ), + html.Div( + id="region-select-dropdown-outer", + children=dcc.Dropdown( + id="region-select", + multi=True, + searchable=True, + ), + ), + ], + ), + html.Div( + id="table-container", + className="table-container", + children=[ + html.Div( + id="table-upper", + children=[ + html.P("Hospital Charges Summary"), + dcc.Loading(children=html.Div(id="cost-stats-container")), + ], + ), + html.Div( + id="table-lower", + children=[ + html.P("Procedure Charges Summary"), + dcc.Loading( + children=html.Div(id="procedure-stats-container") + ), + ], + ), + ], + ), + ], + ) + + +def map_card(): + return html.Div( + id="geo-map-outer", + className="six columns", + children=[ + html.P( + id="map-title", + children="Medicare Provider Charges in the State of {}".format( + state_map[state_list[0]] + ), + ), + html.Div( + id="geo-map-loading-outer", + children=[ + dcc.Loading( + id="loading", + children=dcc.Graph( + id="geo-map", + figure={ + "data": [], + "layout": dict( + plot_bgcolor="#171b26", + paper_bgcolor="#171b26", + ), + }, + ), + ) + ], + ), + ], + ) + + +def procedure_card(): + return html.Div( + id="lower-container", + children=[ + dcc.Graph( + id="procedure-plot", + figure=generate_procedure_plot( + data_dict[state_list[1]], cost_metric[0], init_region, [] + ), + ) + ], + ) diff --git a/apps/dash-medical-provider-charges/utils/figures.py b/apps/dash-medical-provider-charges/utils/figures.py new file mode 100644 index 000000000..cf583d970 --- /dev/null +++ b/apps/dash-medical-provider-charges/utils/figures.py @@ -0,0 +1,298 @@ +import plotly.graph_objs as go +from constants import mapbox_access_token +import dash +from dash import dash_table +import pandas as pd +from utils.helper_functions import generate_aggregation +from constants import data_dict, cost_metric + + +def generate_geo_map(geo_data, selected_metric, region_select, procedure_select): + filtered_data = geo_data[ + geo_data["Hospital Referral Region (HRR) Description"].isin(region_select) + ] + + colors = ["#21c7ef", "#76f2ff", "#ff6969", "#ff1717"] + + hospitals = [] + + lat = filtered_data["lat"].tolist() + lon = filtered_data["lon"].tolist() + average_covered_charges_mean = filtered_data[selected_metric]["mean"].tolist() + regions = filtered_data["Hospital Referral Region (HRR) Description"].tolist() + provider_name = filtered_data["Provider Name"].tolist() + + # Cost metric mapping from aggregated data + + cost_metric_data = {} + cost_metric_data["min"] = filtered_data[selected_metric]["mean"].min() + cost_metric_data["max"] = filtered_data[selected_metric]["mean"].max() + cost_metric_data["mid"] = (cost_metric_data["min"] + cost_metric_data["max"]) / 2 + cost_metric_data["low_mid"] = ( + cost_metric_data["min"] + cost_metric_data["mid"] + ) / 2 + cost_metric_data["high_mid"] = ( + cost_metric_data["mid"] + cost_metric_data["max"] + ) / 2 + + for i in range(len(lat)): + val = average_covered_charges_mean[i] + region = regions[i] + provider = provider_name[i] + + if val <= cost_metric_data["low_mid"]: + color = colors[0] + elif cost_metric_data["low_mid"] < val <= cost_metric_data["mid"]: + color = colors[1] + elif cost_metric_data["mid"] < val <= cost_metric_data["high_mid"]: + color = colors[2] + else: + color = colors[3] + + selected_index = [] + if provider in procedure_select["hospital"]: + selected_index = [0] + + hospital = go.Scattermapbox( + lat=[lat[i]], + lon=[lon[i]], + mode="markers", + marker=dict( + color=color, + showscale=True, + colorscale=[ + [0, "#21c7ef"], + [0.33, "#76f2ff"], + [0.66, "#ff6969"], + [1, "#ff1717"], + ], + cmin=cost_metric_data["min"], + cmax=cost_metric_data["max"], + size=10 + * (1 + (val + cost_metric_data["min"]) / cost_metric_data["mid"]), + colorbar=dict( + x=0.9, + len=0.7, + title=dict( + text="Average Cost", + font={"color": "#737a8d", "family": "Open Sans"}, + ), + titleside="top", + tickmode="array", + tickvals=[cost_metric_data["min"], cost_metric_data["max"]], + ticktext=[ + "${:,.2f}".format(cost_metric_data["min"]), + "${:,.2f}".format(cost_metric_data["max"]), + ], + ticks="outside", + thickness=15, + tickfont={"family": "Open Sans", "color": "#737a8d"}, + ), + ), + opacity=0.8, + selectedpoints=selected_index, + selected=dict(marker={"color": "#ffff00"}), + customdata=[(provider, region)], + hoverinfo="text", + text=provider + + "
" + + region + + "
Average Procedure Cost:" + + " ${:,.2f}".format(val), + ) + hospitals.append(hospital) + + layout = go.Layout( + margin=dict(l=10, r=10, t=20, b=10, pad=5), + plot_bgcolor="#171b26", + paper_bgcolor="#171b26", + clickmode="event+select", + hovermode="closest", + showlegend=False, + mapbox=go.layout.Mapbox( + accesstoken=mapbox_access_token, + bearing=10, + center=go.layout.mapbox.Center( + lat=filtered_data.lat.mean(), lon=filtered_data.lon.mean() + ), + pitch=5, + zoom=5, + style="mapbox://styles/plotlymapbox/cjvppq1jl1ips1co3j12b9hex", + ), + ) + + return {"data": hospitals, "layout": layout} + + +def generate_procedure_plot(raw_data, cost_select, region_select, provider_select): + procedure_data = raw_data[ + raw_data["Hospital Referral Region (HRR) Description"].isin(region_select) + ].reset_index() + + traces = [] + selected_index = procedure_data[ + procedure_data["Provider Name"].isin(provider_select) + ].index + + text = ( + procedure_data["Provider Name"] + + "
" + + "" + + procedure_data["DRG Definition"].map(str) + + "/
" + + "Average Procedure Cost: $ " + + procedure_data[cost_select].map(str) + ) + + provider_trace = go.Box( + y=procedure_data["DRG Definition"], + x=procedure_data[cost_select], + name="", + customdata=procedure_data["Provider Name"], + boxpoints="all", + jitter=0, + pointpos=0, + hoveron="points", + fillcolor="rgba(0,0,0,0)", + line=dict(color="rgba(0,0,0,0)"), + hoverinfo="text", + hovertext=text, + selectedpoints=selected_index, + selected=dict(marker={"color": "#FFFF00", "size": 13}), + unselected=dict(marker={"opacity": 0.2}), + marker=dict( + line=dict(width=1, color="#000000"), + color="#21c7ef", + opacity=0.7, + symbol="square", + size=12, + ), + ) + + traces.append(provider_trace) + + layout = go.Layout( + showlegend=False, + hovermode="closest", + dragmode="select", + clickmode="event+select", + xaxis=dict( + zeroline=False, + automargin=True, + showticklabels=True, + title=dict(text="Procedure Cost", font=dict(color="#737a8d")), + linecolor="#737a8d", + tickfont=dict(color="#737a8d"), + type="log", + ), + yaxis=dict( + automargin=True, + showticklabels=True, + tickfont=dict(color="#737a8d"), + gridcolor="#171b26", + ), + plot_bgcolor="#171b26", + paper_bgcolor="#171b26", + ) + # x : procedure, y: cost, + return {"data": traces, "layout": layout} + + +def hospital_datatable(geo_select, procedure_select, cost_select, state_select): + state_agg = generate_aggregation(data_dict[state_select], cost_metric) + # make table from geo-select + geo_data_dict = { + "Provider Name": [], + "City": [], + "Street Address": [], + "Maximum Cost ($)": [], + "Minimum Cost ($)": [], + } + + ctx = dash.callback_context + if ctx.triggered: + prop_id = ctx.triggered[0]["prop_id"].split(".")[0] + + # make table from procedure-select + if prop_id == "procedure-plot" and procedure_select is not None: + + for point in procedure_select["points"]: + provider = point["customdata"] + + dff = state_agg[state_agg["Provider Name"] == provider] + + geo_data_dict["Provider Name"].append(point["customdata"]) + city = dff["Hospital Referral Region (HRR) Description"].tolist()[0] + geo_data_dict["City"].append(city) + + address = dff["Provider Street Address"].tolist()[0] + geo_data_dict["Street Address"].append(address) + + geo_data_dict["Maximum Cost ($)"].append( + dff[cost_select]["max"].tolist()[0] + ) + geo_data_dict["Minimum Cost ($)"].append( + dff[cost_select]["min"].tolist()[0] + ) + + if prop_id == "geo-map" and geo_select is not None: + + for point in geo_select["points"]: + provider = point["customdata"][0] + dff = state_agg[state_agg["Provider Name"] == provider] + + geo_data_dict["Provider Name"].append(point["customdata"][0]) + geo_data_dict["City"].append(point["customdata"][1].split("- ")[1]) + + address = dff["Provider Street Address"].tolist()[0] + geo_data_dict["Street Address"].append(address) + + geo_data_dict["Maximum Cost ($)"].append( + dff[cost_select]["max"].tolist()[0] + ) + geo_data_dict["Minimum Cost ($)"].append( + dff[cost_select]["min"].tolist()[0] + ) + + geo_data_df = pd.DataFrame(data=geo_data_dict) + data = geo_data_df.to_dict(orient="records") + + else: + data = [{}] + + return dash_table.DataTable( + id="cost-stats-table", + columns=[{"name": i, "id": i} for i in geo_data_dict.keys()], + data=data, + filter_action="native", + page_size=5, + style_cell={"background-color": "#242a3b", "color": "#7b7d8d"}, + style_as_list_view=False, + style_header={"background-color": "#1f2536", "padding": "0px 5px"}, + ) + + +def update_geo_map(cost_select, region_select, procedure_select, state_select): + # generate geo map from state-select, procedure-select + state_agg_data = generate_aggregation(data_dict[state_select], cost_metric) + + provider_data = {"procedure": [], "hospital": []} + if procedure_select is not None: + for point in procedure_select["points"]: + provider_data["procedure"].append(point["y"]) + provider_data["hospital"].append(point["customdata"]) + + return generate_geo_map(state_agg_data, cost_select, region_select, provider_data) + + +def update_procedure_plot(cost_select, region_select, geo_select, state_select): + # generate procedure plot from selected provider + state_raw_data = data_dict[state_select] + + provider_select = [] + if geo_select is not None: + for point in geo_select["points"]: + provider_select.append(point["customdata"][0]) + return generate_procedure_plot( + state_raw_data, cost_select, region_select, provider_select + ) diff --git a/apps/dash-medical-provider-charges/utils/helper_functions.py b/apps/dash-medical-provider-charges/utils/helper_functions.py new file mode 100644 index 000000000..6b93ceec6 --- /dev/null +++ b/apps/dash-medical-provider-charges/utils/helper_functions.py @@ -0,0 +1,148 @@ +from constants import state_map, data_dict +import dash +from dash.exceptions import PreventUpdate +from dash import dash_table +import pandas as pd + + +def generate_aggregation(df, metric): + aggregation = { + metric[0]: ["min", "mean", "max"], + metric[1]: ["min", "mean", "max"], + metric[2]: ["min", "mean", "max"], + } + grouped = ( + df.groupby(["Hospital Referral Region (HRR) Description", "Provider Name"]) + .agg(aggregation) + .reset_index() + ) + + grouped["lat"] = grouped["lon"] = grouped["Provider Street Address"] = grouped[ + "Provider Name" + ] + grouped["lat"] = grouped["lat"].apply(lambda x: get_lat_lon_add(df, x)[0]) + grouped["lon"] = grouped["lon"].apply(lambda x: get_lat_lon_add(df, x)[1]) + grouped["Provider Street Address"] = grouped["Provider Street Address"].apply( + lambda x: get_lat_lon_add(df, x)[2] + ) + + return grouped + + +def get_lat_lon_add(df, name): + return [ + df.groupby(["Provider Name"]).get_group(name)["lat"].tolist()[0], + df.groupby(["Provider Name"]).get_group(name)["lon"].tolist()[0], + df.groupby(["Provider Name"]) + .get_group(name)["Provider Street Address"] + .tolist()[0], + ] + + +def region_dropdown(select_all, state_select): + state_raw_data = data_dict[state_select] + regions = state_raw_data["Hospital Referral Region (HRR) Description"].unique() + options = [{"label": i, "value": i} for i in regions] + + ctx = dash.callback_context + if ctx.triggered[0]["prop_id"].split(".")[0] == "region-select-all": + if select_all == ["All"]: + value = [i["value"] for i in options] + else: + value = dash.no_update + else: + value = regions[:4] + return ( + value, + options, + "Medicare Provider Charges in the State of {}".format(state_map[state_select]), + ) + + +def checklist(selected, select_options, checked): + if len(selected) < len(select_options) and len(checked) == 0: + raise PreventUpdate() + + elif len(selected) < len(select_options) and len(checked) == 1: + return dcc.Checklist( + id="region-select-all", + options=[{"label": "Select All Regions", "value": "All"}], + value=[], + ) + + elif len(selected) == len(select_options) and len(checked) == 1: + raise PreventUpdate() + + return dcc.Checklist( + id="region-select-all", + options=[{"label": "Select All Regions", "value": "All"}], + value=["All"], + ) + + +def procedure_stats(procedure_select, geo_select, cost_select, state_select): + procedure_dict = { + "DRG": [], + "Procedure": [], + "Provider Name": [], + "Cost Summary": [], + } + + ctx = dash.callback_context + prop_id = "" + if ctx.triggered: + prop_id = ctx.triggered[0]["prop_id"].split(".")[0] + + if prop_id == "procedure-plot" and procedure_select is not None: + for point in procedure_select["points"]: + procedure_dict["DRG"].append(point["y"].split(" - ")[0]) + procedure_dict["Procedure"].append(point["y"].split(" - ")[1]) + + procedure_dict["Provider Name"].append(point["customdata"]) + procedure_dict["Cost Summary"].append(("${:,.2f}".format(point["x"]))) + + # Display all procedures at selected hospital + provider_select = [] + + if prop_id == "geo-map" and geo_select is not None: + for point in geo_select["points"]: + provider = point["customdata"][0] + provider_select.append(provider) + + state_raw_data = data_dict[state_select] + provider_filtered = state_raw_data[ + state_raw_data["Provider Name"].isin(provider_select) + ] + + for i in range(len(provider_filtered)): + procedure_dict["DRG"].append( + provider_filtered.iloc[i]["DRG Definition"].split(" - ")[0] + ) + procedure_dict["Procedure"].append( + provider_filtered.iloc[i]["DRG Definition"].split(" - ")[1] + ) + procedure_dict["Provider Name"].append( + provider_filtered.iloc[i]["Provider Name"] + ) + procedure_dict["Cost Summary"].append( + "${:,.2f}".format(provider_filtered.iloc[0][cost_select]) + ) + + procedure_data_df = pd.DataFrame(data=procedure_dict) + + return dash_table.DataTable( + id="procedure-stats-table", + columns=[{"name": i, "id": i} for i in procedure_dict.keys()], + data=procedure_data_df.to_dict(orient="records"), + filter_action="native", + sort_action="native", + style_cell={ + "textOverflow": "ellipsis", + "background-color": "#242a3b", + "color": "#7b7d8d", + }, + sort_mode="multi", + page_size=5, + style_as_list_view=False, + style_header={"background-color": "#1f2536", "padding": "2px 12px 0px 12px"}, + )