From 1ad191dd8f106b0dd5523c810997c506cbd54cdd Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Fri, 3 Jun 2022 13:43:32 -0400 Subject: [PATCH 1/2] refactoring --- apps/dash-medical-provider-charges/.gitignore | 109 --- apps/dash-medical-provider-charges/README.md | 2 +- apps/dash-medical-provider-charges/app.py | 730 ++---------------- .../assets/base.css | 368 +++++++-- .../{ => assets/github}/screenshot.png | Bin .../assets/images/plotly-logo-dark-theme.png | Bin 0 -> 23021 bytes .../assets/{ => images}/plotly_logo_white.png | Bin .../constants.py | 80 ++ apps/dash-medical-provider-charges/gitignore | 191 +++++ .../requirements.txt | 9 +- .../dash-medical-provider-charges/runtime.txt | 1 + .../utils/components.py | 135 ++++ .../utils/figures.py | 298 +++++++ .../utils/helper_functions.py | 148 ++++ 14 files changed, 1208 insertions(+), 863 deletions(-) delete mode 100644 apps/dash-medical-provider-charges/.gitignore rename apps/dash-medical-provider-charges/{ => assets/github}/screenshot.png (100%) create mode 100644 apps/dash-medical-provider-charges/assets/images/plotly-logo-dark-theme.png rename apps/dash-medical-provider-charges/assets/{ => images}/plotly_logo_white.png (100%) create mode 100644 apps/dash-medical-provider-charges/constants.py create mode 100644 apps/dash-medical-provider-charges/gitignore create mode 100644 apps/dash-medical-provider-charges/runtime.txt create mode 100644 apps/dash-medical-provider-charges/utils/components.py create mode 100644 apps/dash-medical-provider-charges/utils/figures.py create mode 100644 apps/dash-medical-provider-charges/utils/helper_functions.py 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..8ce73da12 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 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=[ @@ -23,393 +44,6 @@ 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} - - app.layout = html.Div( className="container scalable", children=[ @@ -426,309 +60,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 0000000000000000000000000000000000000000..984dd57ab53b7f9fa47df219e25df91a0885613d GIT binary patch literal 23021 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.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..fa762aaa1 --- /dev/null +++ b/apps/dash-medical-provider-charges/utils/components.py @@ -0,0 +1,135 @@ +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 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"}, + ) From 49ec18dbce05465f1ad8841619434ef5dee6abd4 Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Fri, 3 Jun 2022 13:51:50 -0400 Subject: [PATCH 2/2] added header --- apps/dash-medical-provider-charges/app.py | 13 +++++--- .../assets/images/plotly_logo_white.png | Bin 5654 -> 0 bytes .../utils/components.py | 28 ++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) delete mode 100644 apps/dash-medical-provider-charges/assets/images/plotly_logo_white.png diff --git a/apps/dash-medical-provider-charges/app.py b/apps/dash-medical-provider-charges/app.py index 8ce73da12..789a760aa 100644 --- a/apps/dash-medical-provider-charges/app.py +++ b/apps/dash-medical-provider-charges/app.py @@ -20,7 +20,7 @@ procedure_stats, ) -from utils.components import build_upper_left_panel, map_card, procedure_card +from utils.components import header, build_upper_left_panel, map_card, procedure_card from utils.figures import ( generate_geo_map, @@ -39,11 +39,15 @@ } ], ) -app.title = "Medical Provider Charges" +# app.title = "Medical Provider Charges" server = app.server app.config["suppress_callback_exceptions"] = True + +# def header(app, header_color, header, subheader=None, header_background_color="transparent"): + + app.layout = html.Div( className="container scalable", children=[ @@ -51,8 +55,9 @@ 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( diff --git a/apps/dash-medical-provider-charges/assets/images/plotly_logo_white.png b/apps/dash-medical-provider-charges/assets/images/plotly_logo_white.png deleted file mode 100644 index 489ed362fe64147dcf6f890a240960e8ba88933e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5654 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 wH?DB^!yA