Spaces:
Running
Running
Testing PRs
#2
by
maxschulz-COL
- opened
- .gitattributes +0 -1
- .gitignore +0 -144
- README.md +19 -34
- app.py +80 -94
- assets/css/custom.css +52 -38
- images/kpi-dashboard.gif → assets/images/kpi_dashboard.gif +2 -2
- requirements.txt +14 -9
- utils/__init__.py +1 -1
- utils/_charts.py +50 -61
- utils/_helper.py +3 -76
.gitattributes
CHANGED
@@ -34,4 +34,3 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
assets/images/kpi_dashboard.gif filter=lfs diff=lfs merge=lfs -text
|
37 |
-
kpi-dashboard.gif filter=lfs diff=lfs merge=lfs -text
|
|
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
assets/images/kpi_dashboard.gif filter=lfs diff=lfs merge=lfs -text
|
|
.gitignore
DELETED
@@ -1,144 +0,0 @@
|
|
1 |
-
# npm node_modules
|
2 |
-
node_modules/
|
3 |
-
|
4 |
-
# Ipynb
|
5 |
-
ipynb_checkpoints
|
6 |
-
*/.ipynb_checkpoints/*
|
7 |
-
|
8 |
-
# IPython
|
9 |
-
profile_default/
|
10 |
-
ipython_config.py
|
11 |
-
|
12 |
-
# Byte-compiled / optimized / DLL files
|
13 |
-
__pycache__/
|
14 |
-
*.py[cod]
|
15 |
-
*$py.class
|
16 |
-
|
17 |
-
# C extensions
|
18 |
-
*.so
|
19 |
-
|
20 |
-
# Distribution / packaging
|
21 |
-
.Python
|
22 |
-
build/
|
23 |
-
develop-eggs/
|
24 |
-
dist/
|
25 |
-
downloads/
|
26 |
-
eggs/
|
27 |
-
.eggs/
|
28 |
-
lib/
|
29 |
-
lib64/
|
30 |
-
parts/
|
31 |
-
sdist/
|
32 |
-
var/
|
33 |
-
wheels/
|
34 |
-
share/python-wheels/
|
35 |
-
*.egg-info/
|
36 |
-
.installed.cfg
|
37 |
-
*.egg
|
38 |
-
MANIFEST
|
39 |
-
|
40 |
-
# macOS
|
41 |
-
*.DS_Store
|
42 |
-
.DS_Store
|
43 |
-
.AppleDouble
|
44 |
-
.LSOverride
|
45 |
-
.Trashes
|
46 |
-
|
47 |
-
# PyInstaller
|
48 |
-
*.manifest
|
49 |
-
*.spec
|
50 |
-
|
51 |
-
# Installer logs
|
52 |
-
pip-log.txt
|
53 |
-
pip-delete-this-directory.txt
|
54 |
-
|
55 |
-
# Unit test / coverage reports
|
56 |
-
htmlcov/
|
57 |
-
.tox/
|
58 |
-
.nox/
|
59 |
-
.coverage
|
60 |
-
.coverage.*
|
61 |
-
.cache
|
62 |
-
nosetests.xml
|
63 |
-
coverage.xml
|
64 |
-
*.cover
|
65 |
-
*.py,cover
|
66 |
-
.hypothesis/
|
67 |
-
.pytest_cache/
|
68 |
-
cover/
|
69 |
-
|
70 |
-
# Translations
|
71 |
-
*.mo
|
72 |
-
*.pot
|
73 |
-
|
74 |
-
# Django
|
75 |
-
*.log
|
76 |
-
local_settings.py
|
77 |
-
db.sqlite3
|
78 |
-
db.sqlite3-journal
|
79 |
-
|
80 |
-
# Flask
|
81 |
-
instance/
|
82 |
-
.webassets-cache
|
83 |
-
|
84 |
-
# Scrapy
|
85 |
-
.scrapy
|
86 |
-
|
87 |
-
# PyBuilder
|
88 |
-
.pybuilder/
|
89 |
-
target/
|
90 |
-
|
91 |
-
# IntelliJ
|
92 |
-
.idea/
|
93 |
-
*.iml
|
94 |
-
out/
|
95 |
-
.idea_modules/
|
96 |
-
|
97 |
-
# Vscode
|
98 |
-
.vscode/
|
99 |
-
|
100 |
-
# PEP 582
|
101 |
-
__pypackages__/
|
102 |
-
|
103 |
-
# Celery
|
104 |
-
celerybeat-schedule
|
105 |
-
celerybeat.pid
|
106 |
-
|
107 |
-
# SageMath
|
108 |
-
*.sage.py
|
109 |
-
|
110 |
-
# Environments
|
111 |
-
.env
|
112 |
-
.venv
|
113 |
-
env/
|
114 |
-
venv/
|
115 |
-
ENV/
|
116 |
-
env.bak/
|
117 |
-
venv.bak/
|
118 |
-
|
119 |
-
# Spyder
|
120 |
-
.spyderproject
|
121 |
-
.spyproject
|
122 |
-
|
123 |
-
# Rope
|
124 |
-
.ropeproject
|
125 |
-
|
126 |
-
# Mkdocs
|
127 |
-
/site
|
128 |
-
|
129 |
-
# Mypy
|
130 |
-
.mypy_cache/
|
131 |
-
.dmypy.json
|
132 |
-
dmypy.json
|
133 |
-
|
134 |
-
# Pyre
|
135 |
-
.pyre/
|
136 |
-
|
137 |
-
# Pytype
|
138 |
-
.pytype/
|
139 |
-
|
140 |
-
# Cython
|
141 |
-
cython_debug/
|
142 |
-
|
143 |
-
# Ruff
|
144 |
-
.ruff_cache/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
---
|
2 |
-
title: KPI
|
3 |
emoji: 📊
|
4 |
colorFrom: blue
|
5 |
colorTo: blue
|
@@ -9,43 +9,28 @@ license: apache-2.0
|
|
9 |
short_description: Example of a Key Performance Indicator (KPI) dashboard
|
10 |
---
|
11 |
|
12 |
-
# KPI
|
13 |
|
14 |
-
This dashboard provides an example of a Key Performance Indicator (KPI) dashboard, designed to help users get started
|
15 |
-
|
16 |
|
17 |
-
|
18 |
-
|
19 |
-
---
|
20 |
-
|
21 |
-
### 🗓️ Data
|
22 |
-
|
23 |
-
Special thanks to the [#RWFD Real World Fake Data initiative](https://data.world/markbradbourne/rwfd-real-world-fake-data), a community project that
|
24 |
provides high-quality fake data for creating realistic dashboard examples for real-world applications.
|
25 |
|
26 |
-
|
27 |
-
|
28 |
-
### 📊 Plotly resources
|
29 |
-
|
30 |
-
- [Bar charts](https://plotly.com/python/bar-charts/)
|
31 |
-
- [Pie charts](https://plotly.com/python/pie-charts/)
|
32 |
-
- [Choropleth maps](https://plotly.com/python/choropleth-maps/)
|
33 |
-
- [Unstacked area charts](https://plotly.com/python/filled-area-plots/)
|
34 |
|
35 |
-
|
36 |
-
|
37 |
-
- [Vizro tutorial on pages, layouts and dashboards](https://vizro.readthedocs.io/en/stable/pages/tutorials/explore-components/)
|
38 |
-
- [Custom components](https://vizro.readthedocs.io/en/stable/pages/user-guides/custom-components/)
|
39 |
-
- [Custom charts](https://vizro.readthedocs.io/en/stable/pages/user-guides/custom-charts/)
|
40 |
-
- [Custom CSS](https://vizro.readthedocs.io/en/stable/pages/user-guides/assets/)
|
41 |
-
|
42 |
-
### 🖥️ App demo
|
43 |
-
|
44 |
-
<img src="./images/kpi-dashboard.gif" alt="Gif to KPI dashboard" width="600">
|
45 |
-
|
46 |
-
---
|
47 |
|
48 |
-
##
|
49 |
|
50 |
-
|
51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
+
title: KPI Demo
|
3 |
emoji: 📊
|
4 |
colorFrom: blue
|
5 |
colorTo: blue
|
|
|
9 |
short_description: Example of a Key Performance Indicator (KPI) dashboard
|
10 |
---
|
11 |
|
12 |
+
# KPI dashboard
|
13 |
|
14 |
+
This demo dashboard provides an example of a Key Performance Indicator (KPI) dashboard, designed to help users get started and extend further.
|
15 |
+
It uses fictional budget data to demonstrate the capabilities of Vizro using real world applications.
|
16 |
|
17 |
+
Special thanks to the [#RWFD Real World Fake Data initiative](https://opendatainitiative.io/), a community project that
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
provides high-quality fake data for creating realistic dashboard examples for real-world applications.
|
19 |
|
20 |
+
Note: The data has been additionally edited for the purpose of this example.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
|
22 |
+
<img src="./assets/images/kpi_dashboard.gif" alt="Gif to KPI dashboard">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
|
24 |
+
## Possible future iterations
|
25 |
|
26 |
+
- Enable selection of year filter
|
27 |
+
- Enable current year vs. past year comparison
|
28 |
+
- Enable dynamic KPI Cards
|
29 |
+
- Bar - Enable drill-downs from Issue to Sub-issue and Product to Sub-product
|
30 |
+
- Bar - Reformat numbers with commas in bar chart
|
31 |
+
- Bar - Left-align y-axis labels
|
32 |
+
- Bar - Shorten labels
|
33 |
+
- Line - Customize function to always show selected year vs. past year
|
34 |
+
- Table-view - Check why date format does not work on `Date Received`
|
35 |
+
- Table-view - Add icons to `On time?` column
|
36 |
+
- Table-view - Improve speed by applying cache or overcome limitation that entire data set is loaded in
|
app.py
CHANGED
@@ -1,131 +1,114 @@
|
|
1 |
"""Example to show dashboard configuration."""
|
2 |
|
3 |
-
from dash import html, get_asset_url
|
4 |
-
import dash_bootstrap_components as dbc
|
5 |
import pandas as pd
|
6 |
import vizro.models as vm
|
7 |
-
from utils._charts import COLUMN_DEFS,
|
8 |
-
from utils._helper import clean_data_and_add_columns
|
9 |
from vizro import Vizro
|
10 |
from vizro.actions import filter_interaction
|
11 |
-
from vizro.figures import kpi_card_reference
|
12 |
from vizro.tables import dash_ag_grid
|
13 |
|
14 |
# DATA --------------------------------------------------------------------------------------------
|
15 |
df_complaints = pd.read_csv("https://query.data.world/s/glbdstahsuw3hjgunz3zssggk7dsfu?dws=00000")
|
16 |
df_complaints = clean_data_and_add_columns(df_complaints)
|
17 |
-
|
18 |
-
vm.Page.add_type("components", FlexContainer)
|
19 |
-
|
20 |
|
21 |
# SUB-SECTIONS ------------------------------------------------------------------------------------
|
22 |
-
kpi_banner =
|
|
|
|
|
23 |
components=[
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
value_format="{value:.0f}",
|
32 |
-
reference_format="{delta_relative:+.1%} vs. 2018 ({reference:.0f})",
|
33 |
-
icon="person",
|
34 |
-
),
|
35 |
),
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
value_format="{value:.1f}%",
|
43 |
-
reference_format="{delta:+.1f}pp vs. 2018 ({reference:.1f}%)",
|
44 |
-
icon="inventory",
|
45 |
-
)
|
46 |
),
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
value_format="{value:.1f}%",
|
54 |
-
reference_format="{delta:+.1f}pp vs. 2018 ({reference:.1f}%)",
|
55 |
-
icon="timer",
|
56 |
-
)
|
57 |
),
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
value_format="{value:.1f}%",
|
65 |
-
reference_format="{delta:.1f}pp vs. 2018 ({reference:.1f}%)",
|
66 |
-
icon="payments",
|
67 |
-
)
|
68 |
),
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
|
|
|
|
|
|
79 |
),
|
80 |
],
|
81 |
-
classname="kpi-banner",
|
82 |
)
|
83 |
|
84 |
bar_charts_tabbed = vm.Tabs(
|
85 |
tabs=[
|
86 |
vm.Container(
|
87 |
-
title="By
|
88 |
components=[
|
89 |
vm.Graph(
|
90 |
figure=bar(
|
91 |
data_frame=df_complaints,
|
92 |
-
y="
|
93 |
x="Complaint ID",
|
94 |
),
|
95 |
)
|
96 |
],
|
97 |
),
|
98 |
vm.Container(
|
99 |
-
title="By
|
100 |
components=[
|
101 |
vm.Graph(
|
102 |
figure=bar(
|
103 |
data_frame=df_complaints,
|
104 |
-
y="
|
105 |
x="Complaint ID",
|
106 |
),
|
107 |
)
|
108 |
],
|
109 |
),
|
110 |
vm.Container(
|
111 |
-
title="By
|
112 |
components=[
|
113 |
vm.Graph(
|
114 |
figure=bar(
|
115 |
data_frame=df_complaints,
|
116 |
-
y="
|
117 |
x="Complaint ID",
|
118 |
),
|
119 |
)
|
120 |
],
|
121 |
),
|
122 |
vm.Container(
|
123 |
-
title="By
|
124 |
components=[
|
125 |
vm.Graph(
|
126 |
figure=bar(
|
127 |
data_frame=df_complaints,
|
128 |
-
y="
|
129 |
x="Complaint ID",
|
130 |
),
|
131 |
)
|
@@ -139,12 +122,9 @@ page_exec = vm.Page(
|
|
139 |
title="Executive View",
|
140 |
layout=vm.Layout(
|
141 |
grid=[
|
142 |
-
[0, 0],
|
143 |
[0, 0],
|
144 |
[1, 2],
|
145 |
[1, 2],
|
146 |
-
[1, 2],
|
147 |
-
[1, 3],
|
148 |
[1, 3],
|
149 |
[1, 3],
|
150 |
],
|
@@ -152,10 +132,17 @@ page_exec = vm.Page(
|
|
152 |
components=[
|
153 |
kpi_banner,
|
154 |
bar_charts_tabbed,
|
155 |
-
vm.Graph(figure=
|
156 |
vm.Graph(
|
157 |
figure=pie(
|
158 |
data_frame=df_complaints[df_complaints["Company response - Closed"] != "Not closed"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
159 |
values="Complaint ID",
|
160 |
names="Company response - Closed",
|
161 |
title="Closed company responses",
|
@@ -166,15 +153,23 @@ page_exec = vm.Page(
|
|
166 |
|
167 |
page_region = vm.Page(
|
168 |
title="Regional View",
|
169 |
-
layout=vm.Layout(grid=[[0, 1]]),
|
170 |
components=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
vm.Graph(
|
172 |
figure=choropleth(
|
173 |
data_frame=df_complaints,
|
174 |
locations="State",
|
175 |
color="Complaint ID",
|
176 |
-
title="Complaints by State
|
177 |
-
"charts on the right. Refresh the page to deselect.</sup>",
|
178 |
custom_data=["State"],
|
179 |
),
|
180 |
actions=[
|
@@ -186,26 +181,26 @@ page_region = vm.Page(
|
|
186 |
vm.Tabs(
|
187 |
tabs=[
|
188 |
vm.Container(
|
189 |
-
title="By
|
190 |
components=[
|
191 |
vm.Graph(
|
192 |
-
id="regional-
|
193 |
figure=bar(
|
194 |
data_frame=df_complaints,
|
195 |
-
y="
|
196 |
x="Complaint ID",
|
197 |
),
|
198 |
)
|
199 |
],
|
200 |
),
|
201 |
vm.Container(
|
202 |
-
title="By
|
203 |
components=[
|
204 |
vm.Graph(
|
205 |
-
id="regional-
|
206 |
figure=bar(
|
207 |
data_frame=df_complaints,
|
208 |
-
y="
|
209 |
x="Complaint ID",
|
210 |
),
|
211 |
)
|
@@ -237,7 +232,7 @@ page_table = vm.Page(
|
|
237 |
|
238 |
dashboard = vm.Dashboard(
|
239 |
pages=[page_exec, page_region, page_table],
|
240 |
-
title="Cumulus Financial
|
241 |
navigation=vm.Navigation(
|
242 |
nav_selector=vm.NavBar(
|
243 |
items=[
|
@@ -250,16 +245,7 @@ dashboard = vm.Dashboard(
|
|
250 |
)
|
251 |
|
252 |
app = Vizro().build(dashboard)
|
253 |
-
app.dash.layout.children.append(
|
254 |
-
dbc.NavLink(
|
255 |
-
["Made with ", html.Img(src=get_asset_url("images/logo.svg"), id="banner", alt="Vizro logo"), "vizro"],
|
256 |
-
href="https://github.com/mckinsey/vizro",
|
257 |
-
target="_blank",
|
258 |
-
external_link=True,
|
259 |
-
className="anchor-container",
|
260 |
-
)
|
261 |
-
)
|
262 |
server = app.dash.server
|
263 |
|
264 |
-
if __name__ == "__main__":
|
265 |
-
app.run()
|
|
|
1 |
"""Example to show dashboard configuration."""
|
2 |
|
|
|
|
|
3 |
import pandas as pd
|
4 |
import vizro.models as vm
|
5 |
+
from utils._charts import COLUMN_DEFS, KPI, bar, choropleth, line, pie
|
6 |
+
from utils._helper import clean_data_and_add_columns
|
7 |
from vizro import Vizro
|
8 |
from vizro.actions import filter_interaction
|
|
|
9 |
from vizro.tables import dash_ag_grid
|
10 |
|
11 |
# DATA --------------------------------------------------------------------------------------------
|
12 |
df_complaints = pd.read_csv("https://query.data.world/s/glbdstahsuw3hjgunz3zssggk7dsfu?dws=00000")
|
13 |
df_complaints = clean_data_and_add_columns(df_complaints)
|
14 |
+
vm.Container.add_type("components", KPI)
|
|
|
|
|
15 |
|
16 |
# SUB-SECTIONS ------------------------------------------------------------------------------------
|
17 |
+
kpi_banner = vm.Container(
|
18 |
+
id="kpi-banner",
|
19 |
+
title="",
|
20 |
components=[
|
21 |
+
# Note: For some KPIs the icon/sign go in opposite directions as an increase e.g. in complaints is negative
|
22 |
+
KPI(
|
23 |
+
title="Total Complaints",
|
24 |
+
value="75.513",
|
25 |
+
icon="arrow_circle_up",
|
26 |
+
sign="delta-neg",
|
27 |
+
ref_value="6.8% vs. LY",
|
|
|
|
|
|
|
|
|
28 |
),
|
29 |
+
KPI(
|
30 |
+
title="Closed Complaints",
|
31 |
+
value="99.6%",
|
32 |
+
icon="arrow_circle_up",
|
33 |
+
sign="delta-pos",
|
34 |
+
ref_value="+0.2% vs. LY",
|
|
|
|
|
|
|
|
|
35 |
),
|
36 |
+
KPI(
|
37 |
+
title="Open Complaints",
|
38 |
+
value="0.4%",
|
39 |
+
icon="arrow_circle_down",
|
40 |
+
sign="delta-pos",
|
41 |
+
ref_value="-0.2% vs. LY",
|
|
|
|
|
|
|
|
|
42 |
),
|
43 |
+
KPI(
|
44 |
+
title="Timely Response",
|
45 |
+
value="98.1%",
|
46 |
+
icon="arrow_circle_up",
|
47 |
+
sign="delta-pos",
|
48 |
+
ref_value="+10.5% vs. LY",
|
|
|
|
|
|
|
|
|
49 |
),
|
50 |
+
KPI(
|
51 |
+
title="Closed w/o cost",
|
52 |
+
value="84.5%",
|
53 |
+
icon="arrow_circle_down",
|
54 |
+
sign="delta-neg",
|
55 |
+
ref_value="-8.5% vs. LY",
|
56 |
+
),
|
57 |
+
KPI(
|
58 |
+
title="Consumer disputed",
|
59 |
+
value="9.5%",
|
60 |
+
icon="arrow_circle_up",
|
61 |
+
sign="delta-neg",
|
62 |
+
ref_value="+2.3% vs. LY",
|
63 |
),
|
64 |
],
|
|
|
65 |
)
|
66 |
|
67 |
bar_charts_tabbed = vm.Tabs(
|
68 |
tabs=[
|
69 |
vm.Container(
|
70 |
+
title="By Issue",
|
71 |
components=[
|
72 |
vm.Graph(
|
73 |
figure=bar(
|
74 |
data_frame=df_complaints,
|
75 |
+
y="Issue",
|
76 |
x="Complaint ID",
|
77 |
),
|
78 |
)
|
79 |
],
|
80 |
),
|
81 |
vm.Container(
|
82 |
+
title="By Product",
|
83 |
components=[
|
84 |
vm.Graph(
|
85 |
figure=bar(
|
86 |
data_frame=df_complaints,
|
87 |
+
y="Product",
|
88 |
x="Complaint ID",
|
89 |
),
|
90 |
)
|
91 |
],
|
92 |
),
|
93 |
vm.Container(
|
94 |
+
title="By Channel",
|
95 |
components=[
|
96 |
vm.Graph(
|
97 |
figure=bar(
|
98 |
data_frame=df_complaints,
|
99 |
+
y="Channel",
|
100 |
x="Complaint ID",
|
101 |
),
|
102 |
)
|
103 |
],
|
104 |
),
|
105 |
vm.Container(
|
106 |
+
title="By Region",
|
107 |
components=[
|
108 |
vm.Graph(
|
109 |
figure=bar(
|
110 |
data_frame=df_complaints,
|
111 |
+
y="Region",
|
112 |
x="Complaint ID",
|
113 |
),
|
114 |
)
|
|
|
122 |
title="Executive View",
|
123 |
layout=vm.Layout(
|
124 |
grid=[
|
|
|
125 |
[0, 0],
|
126 |
[1, 2],
|
127 |
[1, 2],
|
|
|
|
|
128 |
[1, 3],
|
129 |
[1, 3],
|
130 |
],
|
|
|
132 |
components=[
|
133 |
kpi_banner,
|
134 |
bar_charts_tabbed,
|
135 |
+
vm.Graph(figure=line(data_frame=df_complaints, y="Complaint ID", x="Year-Month Received")),
|
136 |
vm.Graph(
|
137 |
figure=pie(
|
138 |
data_frame=df_complaints[df_complaints["Company response - Closed"] != "Not closed"],
|
139 |
+
custom_order=[
|
140 |
+
"Closed with explanation",
|
141 |
+
"Closed without relief",
|
142 |
+
"Closed with non-monetary relief",
|
143 |
+
"Closed with relief",
|
144 |
+
"Closed with monetary relief",
|
145 |
+
],
|
146 |
values="Complaint ID",
|
147 |
names="Company response - Closed",
|
148 |
title="Closed company responses",
|
|
|
153 |
|
154 |
page_region = vm.Page(
|
155 |
title="Regional View",
|
156 |
+
layout=vm.Layout(grid=[[0, 0]] + [[1, 2]] * 4),
|
157 |
components=[
|
158 |
+
vm.Card(
|
159 |
+
text="""
|
160 |
+
##### Click on a state inside the map to filter the bar charts on the right.
|
161 |
+
|
162 |
+
- Which state has the most complaints?
|
163 |
+
- What are the three biggest issues in California?
|
164 |
+
- What is the product with the most complaints in Texas?
|
165 |
+
"""
|
166 |
+
),
|
167 |
vm.Graph(
|
168 |
figure=choropleth(
|
169 |
data_frame=df_complaints,
|
170 |
locations="State",
|
171 |
color="Complaint ID",
|
172 |
+
title="Complaints by State",
|
|
|
173 |
custom_data=["State"],
|
174 |
),
|
175 |
actions=[
|
|
|
181 |
vm.Tabs(
|
182 |
tabs=[
|
183 |
vm.Container(
|
184 |
+
title="By Issue",
|
185 |
components=[
|
186 |
vm.Graph(
|
187 |
+
id="regional-issue",
|
188 |
figure=bar(
|
189 |
data_frame=df_complaints,
|
190 |
+
y="Issue",
|
191 |
x="Complaint ID",
|
192 |
),
|
193 |
)
|
194 |
],
|
195 |
),
|
196 |
vm.Container(
|
197 |
+
title="By Product",
|
198 |
components=[
|
199 |
vm.Graph(
|
200 |
+
id="regional-product",
|
201 |
figure=bar(
|
202 |
data_frame=df_complaints,
|
203 |
+
y="Product",
|
204 |
x="Complaint ID",
|
205 |
),
|
206 |
)
|
|
|
232 |
|
233 |
dashboard = vm.Dashboard(
|
234 |
pages=[page_exec, page_region, page_table],
|
235 |
+
title="Cumulus Financial Corporation",
|
236 |
navigation=vm.Navigation(
|
237 |
nav_selector=vm.NavBar(
|
238 |
items=[
|
|
|
245 |
)
|
246 |
|
247 |
app = Vizro().build(dashboard)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
248 |
server = app.dash.server
|
249 |
|
250 |
+
if __name__ == "__main__":
|
251 |
+
app.run()
|
assets/css/custom.css
CHANGED
@@ -2,64 +2,78 @@
|
|
2 |
padding-left: 4px;
|
3 |
}
|
4 |
|
5 |
-
.card
|
6 |
-
|
7 |
-
padding: 0.75rem;
|
8 |
}
|
9 |
|
10 |
-
.kpi-
|
11 |
-
|
12 |
-
gap: 1rem;
|
13 |
-
height: 100%;
|
14 |
-
overflow: scroll;
|
15 |
}
|
16 |
|
17 |
-
.kpi-
|
18 |
-
|
19 |
}
|
20 |
|
21 |
-
.kpi-
|
22 |
-
|
|
|
|
|
23 |
}
|
24 |
|
25 |
-
|
26 |
-
|
27 |
-
color: var(--bs-pink);
|
28 |
}
|
29 |
|
30 |
-
|
31 |
-
color:
|
32 |
}
|
33 |
|
34 |
-
|
35 |
-
|
|
|
36 |
}
|
37 |
|
38 |
-
|
39 |
-
|
|
|
|
|
|
|
40 |
}
|
41 |
|
42 |
-
.
|
43 |
align-items: center;
|
44 |
-
|
45 |
-
border-top-left-radius: 8px;
|
46 |
-
bottom: 0;
|
47 |
-
color: var(--text-contrast-primary);
|
48 |
display: flex;
|
49 |
-
|
50 |
-
font-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
}
|
56 |
|
57 |
-
.
|
58 |
-
|
59 |
-
|
60 |
-
|
|
|
|
|
|
|
|
|
61 |
}
|
62 |
|
63 |
-
|
64 |
-
|
|
|
65 |
}
|
|
|
2 |
padding-left: 4px;
|
3 |
}
|
4 |
|
5 |
+
.card {
|
6 |
+
padding: 8px;
|
|
|
7 |
}
|
8 |
|
9 |
+
.kpi-card-ref {
|
10 |
+
min-width: 168px;
|
|
|
|
|
|
|
11 |
}
|
12 |
|
13 |
+
.kpi-card-ref h4 {
|
14 |
+
margin: 0;
|
15 |
}
|
16 |
|
17 |
+
.kpi-card-ref > span {
|
18 |
+
display: flex;
|
19 |
+
font-weight: 600;
|
20 |
+
gap: 4px;
|
21 |
}
|
22 |
|
23 |
+
.kpi-card-ref .delta-pos {
|
24 |
+
color: #1a85ff;
|
|
|
25 |
}
|
26 |
|
27 |
+
.kpi-card-ref .delta-neg {
|
28 |
+
color: #d41159;
|
29 |
}
|
30 |
|
31 |
+
.kpi-card-ref .material-symbols-outlined {
|
32 |
+
font-size: 16px;
|
33 |
+
line-height: 20px;
|
34 |
}
|
35 |
|
36 |
+
.kpi-card-ref > div {
|
37 |
+
display: flex;
|
38 |
+
flex-direction: row;
|
39 |
+
gap: 8px;
|
40 |
+
margin: 0;
|
41 |
}
|
42 |
|
43 |
+
.kpi-card-ref > p {
|
44 |
align-items: center;
|
45 |
+
color: var(--text-secondary);
|
|
|
|
|
|
|
46 |
display: flex;
|
47 |
+
flex-grow: 1;
|
48 |
+
font-size: 1rem;
|
49 |
+
font-size: 3.6vh;
|
50 |
+
font-weight: 600;
|
51 |
+
line-height: unset;
|
52 |
+
}
|
53 |
+
|
54 |
+
.kpi-card-ref:has(.delta-pos) {
|
55 |
+
border-left: 4px solid #1a85ff;
|
56 |
+
}
|
57 |
+
|
58 |
+
.kpi-card-ref:has(.delta-neg) {
|
59 |
+
border-left: 4px solid #d41159;
|
60 |
+
}
|
61 |
+
|
62 |
+
.card ul {
|
63 |
+
margin-bottom: 0;
|
64 |
}
|
65 |
|
66 |
+
#kpi-banner .container__title {
|
67 |
+
display: none;
|
68 |
+
}
|
69 |
+
|
70 |
+
#kpi-banner .grid-layout {
|
71 |
+
display: flex;
|
72 |
+
flex-direction: row;
|
73 |
+
overflow: auto;
|
74 |
}
|
75 |
|
76 |
+
#kpi-banner > ::-webkit-scrollbar-thumb {
|
77 |
+
border: 6px solid;
|
78 |
+
border-color: var(--main-container-bg-color);
|
79 |
}
|
images/kpi-dashboard.gif → assets/images/kpi_dashboard.gif
RENAMED
File without changes
|
requirements.txt
CHANGED
@@ -6,7 +6,7 @@ blinker==1.8.2
|
|
6 |
# via flask
|
7 |
cachelib==0.9.0
|
8 |
# via flask-caching
|
9 |
-
certifi==2024.
|
10 |
# via requests
|
11 |
charset-normalizer==3.3.2
|
12 |
# via requests
|
@@ -37,7 +37,7 @@ flask-caching==2.3.0
|
|
37 |
# via vizro
|
38 |
gunicorn==23.0.0
|
39 |
# via -r requirements.in
|
40 |
-
idna==3.
|
41 |
# via requests
|
42 |
importlib-metadata==8.4.0
|
43 |
# via dash
|
@@ -52,14 +52,16 @@ markupsafe==2.1.5
|
|
52 |
nest-asyncio==1.6.0
|
53 |
# via dash
|
54 |
numpy==2.1.0
|
55 |
-
# via
|
|
|
|
|
56 |
packaging==24.1
|
57 |
# via
|
58 |
# gunicorn
|
59 |
# plotly
|
60 |
pandas==2.2.2
|
61 |
# via vizro
|
62 |
-
plotly==5.
|
63 |
# via dash
|
64 |
pydantic==2.8.2
|
65 |
# via vizro
|
@@ -73,10 +75,12 @@ requests==2.32.3
|
|
73 |
# via dash
|
74 |
retrying==1.3.4
|
75 |
# via dash
|
76 |
-
ruff==0.6.
|
77 |
# via vizro
|
78 |
-
setuptools==
|
79 |
-
# via
|
|
|
|
|
80 |
six==1.16.0
|
81 |
# via
|
82 |
# python-dateutil
|
@@ -92,13 +96,14 @@ tzdata==2024.1
|
|
92 |
# via pandas
|
93 |
urllib3==2.2.2
|
94 |
# via requests
|
95 |
-
vizro==0.1.
|
96 |
# via -r requirements.in
|
97 |
werkzeug==3.0.4
|
98 |
# via
|
99 |
# dash
|
100 |
# flask
|
|
|
101 |
wrapt==1.16.0
|
102 |
# via vizro
|
103 |
-
zipp==3.20.
|
104 |
# via importlib-metadata
|
|
|
6 |
# via flask
|
7 |
cachelib==0.9.0
|
8 |
# via flask-caching
|
9 |
+
certifi==2024.7.4
|
10 |
# via requests
|
11 |
charset-normalizer==3.3.2
|
12 |
# via requests
|
|
|
37 |
# via vizro
|
38 |
gunicorn==23.0.0
|
39 |
# via -r requirements.in
|
40 |
+
idna==3.7
|
41 |
# via requests
|
42 |
importlib-metadata==8.4.0
|
43 |
# via dash
|
|
|
52 |
nest-asyncio==1.6.0
|
53 |
# via dash
|
54 |
numpy==2.1.0
|
55 |
+
# via
|
56 |
+
# pandas
|
57 |
+
# vizro
|
58 |
packaging==24.1
|
59 |
# via
|
60 |
# gunicorn
|
61 |
# plotly
|
62 |
pandas==2.2.2
|
63 |
# via vizro
|
64 |
+
plotly==5.23.0
|
65 |
# via dash
|
66 |
pydantic==2.8.2
|
67 |
# via vizro
|
|
|
75 |
# via dash
|
76 |
retrying==1.3.4
|
77 |
# via dash
|
78 |
+
ruff==0.6.1
|
79 |
# via vizro
|
80 |
+
setuptools==73.0.1
|
81 |
+
# via
|
82 |
+
# dash
|
83 |
+
# vizro
|
84 |
six==1.16.0
|
85 |
# via
|
86 |
# python-dateutil
|
|
|
96 |
# via pandas
|
97 |
urllib3==2.2.2
|
98 |
# via requests
|
99 |
+
vizro==0.1.20
|
100 |
# via -r requirements.in
|
101 |
werkzeug==3.0.4
|
102 |
# via
|
103 |
# dash
|
104 |
# flask
|
105 |
+
# vizro
|
106 |
wrapt==1.16.0
|
107 |
# via vizro
|
108 |
+
zipp==3.20.0
|
109 |
# via importlib-metadata
|
utils/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1 |
-
"""Utils folder to contain helper functions and custom charts/components."""
|
|
|
1 |
+
"""Utils folder to contain helper functions and custom charts/components."""
|
utils/_charts.py
CHANGED
@@ -2,8 +2,8 @@
|
|
2 |
|
3 |
from typing import List, Literal, Optional
|
4 |
|
|
|
5 |
import pandas as pd
|
6 |
-
import plotly.graph_objects as go
|
7 |
import vizro.models as vm
|
8 |
import vizro.plotly.express as px
|
9 |
from dash import html
|
@@ -11,17 +11,32 @@ from vizro.models.types import capture
|
|
11 |
|
12 |
|
13 |
# CUSTOM COMPONENTS -------------------------------------------------------------
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
20 |
|
21 |
def build(self):
|
22 |
-
|
23 |
-
|
24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
)
|
26 |
|
27 |
|
@@ -34,11 +49,8 @@ def bar(
|
|
34 |
top_n: int = 15,
|
35 |
custom_data: Optional[List[str]] = None,
|
36 |
):
|
37 |
-
"""Custom bar chart implementation.
|
38 |
-
|
39 |
-
Based on [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar).
|
40 |
-
"""
|
41 |
df_agg = data_frame.groupby(y).agg({x: "count"}).sort_values(by=x, ascending=False).reset_index()
|
|
|
42 |
fig = px.bar(
|
43 |
data_frame=df_agg.head(top_n),
|
44 |
x=x,
|
@@ -48,38 +60,21 @@ def bar(
|
|
48 |
color_discrete_sequence=["#1A85FF"],
|
49 |
custom_data=custom_data,
|
50 |
)
|
51 |
-
fig.update_layout(xaxis_title="# of Complaints", yaxis=
|
52 |
return fig
|
53 |
|
54 |
|
55 |
@capture("graph")
|
56 |
-
def
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
df_agg_2019 = df_agg[df_agg["Year"] == "2018"]
|
64 |
-
df_agg_2020 = df_agg[df_agg["Year"] == "2019"]
|
65 |
-
|
66 |
-
fig = go.Figure()
|
67 |
-
fig.add_trace(
|
68 |
-
go.Scatter(x=df_agg_2020[x], y=df_agg_2020[y], fill="tozeroy", name="2019", marker={"color": "#1a85ff"})
|
69 |
-
)
|
70 |
-
fig.add_trace(go.Scatter(x=df_agg_2019[x], y=df_agg_2019[y], fill="tonexty", name="2018", marker={"color": "grey"}))
|
71 |
-
fig.update_layout(
|
72 |
title="Complaints over time",
|
73 |
-
xaxis_title="Date Received",
|
74 |
-
yaxis_title="# of Complaints",
|
75 |
-
title_pad_t=4,
|
76 |
-
xaxis={
|
77 |
-
"showgrid": False,
|
78 |
-
"tickmode": "array",
|
79 |
-
"tickvals": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
80 |
-
"ticktext": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
81 |
-
},
|
82 |
)
|
|
|
83 |
return fig
|
84 |
|
85 |
|
@@ -89,29 +84,25 @@ def pie(
|
|
89 |
values: str,
|
90 |
data_frame: pd.DataFrame = None,
|
91 |
title: Optional[str] = None,
|
|
|
92 |
):
|
93 |
-
"""Custom pie chart implementation.
|
94 |
-
|
95 |
-
Based on [px.pie](https://plotly.com/python-api-reference/generated/plotly.express.pie).
|
96 |
-
"""
|
97 |
df_agg = data_frame.groupby(names).agg({values: "count"}).reset_index()
|
|
|
|
|
|
|
|
|
|
|
98 |
fig = px.pie(
|
99 |
-
data_frame=
|
100 |
names=names,
|
101 |
values=values,
|
102 |
-
|
103 |
-
color_discrete_map={
|
104 |
-
"Closed with explanation": "#1a85ff",
|
105 |
-
"Closed with monetary relief": "#d41159",
|
106 |
-
"Closed with non-monetary relief": "#adbedc",
|
107 |
-
"Closed without relief": "#7ea1ee",
|
108 |
-
"Closed with relief": "#df658c",
|
109 |
-
"Closed": "#1a85ff",
|
110 |
-
},
|
111 |
title=title,
|
112 |
hole=0.4,
|
113 |
)
|
114 |
-
|
|
|
|
|
115 |
return fig
|
116 |
|
117 |
|
@@ -123,11 +114,8 @@ def choropleth(
|
|
123 |
title: Optional[str] = None,
|
124 |
custom_data: Optional[List[str]] = None,
|
125 |
):
|
126 |
-
"""Custom choropleth implementation.
|
127 |
-
|
128 |
-
Based on [px.choropleth](https://plotly.com/python-api-reference/generated/plotly.express.choropleth).
|
129 |
-
"""
|
130 |
df_agg = data_frame.groupby(locations).agg({color: "count"}).reset_index()
|
|
|
131 |
fig = px.choropleth(
|
132 |
data_frame=df_agg,
|
133 |
locations=locations,
|
@@ -151,7 +139,8 @@ def choropleth(
|
|
151 |
title=title,
|
152 |
custom_data=custom_data,
|
153 |
)
|
154 |
-
|
|
|
155 |
return fig
|
156 |
|
157 |
|
@@ -201,4 +190,4 @@ COLUMN_DEFS = [
|
|
201 |
"flex": 6,
|
202 |
},
|
203 |
{"field": "Timely response?", "cellRenderer": "markdown", "headerName": "On time?", "flex": 3},
|
204 |
-
]
|
|
|
2 |
|
3 |
from typing import List, Literal, Optional
|
4 |
|
5 |
+
import dash_bootstrap_components as dbc
|
6 |
import pandas as pd
|
|
|
7 |
import vizro.models as vm
|
8 |
import vizro.plotly.express as px
|
9 |
from dash import html
|
|
|
11 |
|
12 |
|
13 |
# CUSTOM COMPONENTS -------------------------------------------------------------
|
14 |
+
# Note: This is a static KPI Card only (it will not be reactive to controls). A new dynamic KPI Card component
|
15 |
+
# is currently in development.
|
16 |
+
class KPI(vm.VizroBaseModel):
|
17 |
+
"""Static custom `KPI` Card."""
|
18 |
+
|
19 |
+
type: Literal["kpi"] = "kpi"
|
20 |
+
title: str
|
21 |
+
value: str
|
22 |
+
icon: str
|
23 |
+
sign: Literal["delta-pos", "delta-neg"]
|
24 |
+
ref_value: str
|
25 |
|
26 |
def build(self):
|
27 |
+
return dbc.Card(
|
28 |
+
[
|
29 |
+
html.H4(self.title),
|
30 |
+
html.P(self.value),
|
31 |
+
html.Span(
|
32 |
+
[
|
33 |
+
html.Span(self.icon, className="material-symbols-outlined"),
|
34 |
+
html.Span(self.ref_value),
|
35 |
+
],
|
36 |
+
className=self.sign,
|
37 |
+
),
|
38 |
+
],
|
39 |
+
className="kpi-card-ref",
|
40 |
)
|
41 |
|
42 |
|
|
|
49 |
top_n: int = 15,
|
50 |
custom_data: Optional[List[str]] = None,
|
51 |
):
|
|
|
|
|
|
|
|
|
52 |
df_agg = data_frame.groupby(y).agg({x: "count"}).sort_values(by=x, ascending=False).reset_index()
|
53 |
+
|
54 |
fig = px.bar(
|
55 |
data_frame=df_agg.head(top_n),
|
56 |
x=x,
|
|
|
60 |
color_discrete_sequence=["#1A85FF"],
|
61 |
custom_data=custom_data,
|
62 |
)
|
63 |
+
fig.update_layout(xaxis_title="# of Complaints", yaxis=dict(title="", autorange="reversed")) # noqa: C408
|
64 |
return fig
|
65 |
|
66 |
|
67 |
@capture("graph")
|
68 |
+
def line(x: str, y: str, data_frame: pd.DataFrame):
|
69 |
+
df_agg = data_frame.groupby(x).agg({y: "count"}).reset_index()
|
70 |
+
fig = px.area(
|
71 |
+
data_frame=df_agg,
|
72 |
+
x=x,
|
73 |
+
y=y,
|
74 |
+
color_discrete_sequence=["#1A85FF"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
title="Complaints over time",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
76 |
)
|
77 |
+
fig.update_layout(xaxis_title="Date Received", yaxis_title="# of Complaints", title_pad_t=4)
|
78 |
return fig
|
79 |
|
80 |
|
|
|
84 |
values: str,
|
85 |
data_frame: pd.DataFrame = None,
|
86 |
title: Optional[str] = None,
|
87 |
+
custom_order: Optional[List[str]] = None,
|
88 |
):
|
|
|
|
|
|
|
|
|
89 |
df_agg = data_frame.groupby(names).agg({values: "count"}).reset_index()
|
90 |
+
|
91 |
+
# Apply custom order so colors are applied correctly to the pie chart
|
92 |
+
order_mapping = {category: index for index, category in enumerate(custom_order)}
|
93 |
+
df_sorted = df_agg.sort_values(by=names, key=lambda names: names.map(order_mapping))
|
94 |
+
|
95 |
fig = px.pie(
|
96 |
+
data_frame=df_sorted,
|
97 |
names=names,
|
98 |
values=values,
|
99 |
+
color_discrete_sequence=["#1a85ff", "#7ea1ee", "#adbedc", "#df658c", "#d41159"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
100 |
title=title,
|
101 |
hole=0.4,
|
102 |
)
|
103 |
+
|
104 |
+
fig.update_layout(legend_x=1, legend_y=1, title_pad_t=2, margin=dict(l=0, r=0, t=60, b=0)) # noqa: C408
|
105 |
+
fig.update_traces(sort=False)
|
106 |
return fig
|
107 |
|
108 |
|
|
|
114 |
title: Optional[str] = None,
|
115 |
custom_data: Optional[List[str]] = None,
|
116 |
):
|
|
|
|
|
|
|
|
|
117 |
df_agg = data_frame.groupby(locations).agg({color: "count"}).reset_index()
|
118 |
+
|
119 |
fig = px.choropleth(
|
120 |
data_frame=df_agg,
|
121 |
locations=locations,
|
|
|
139 |
title=title,
|
140 |
custom_data=custom_data,
|
141 |
)
|
142 |
+
|
143 |
+
fig.update_coloraxes(colorbar={"thickness": 10, "title": {"side": "right"}})
|
144 |
return fig
|
145 |
|
146 |
|
|
|
190 |
"flex": 6,
|
191 |
},
|
192 |
{"field": "Timely response?", "cellRenderer": "markdown", "headerName": "On time?", "flex": 3},
|
193 |
+
]
|
utils/_helper.py
CHANGED
@@ -1,7 +1,5 @@
|
|
1 |
"""Contains helper functions and variables."""
|
2 |
|
3 |
-
from functools import reduce
|
4 |
-
|
5 |
import numpy as np
|
6 |
import pandas as pd
|
7 |
|
@@ -40,14 +38,13 @@ def clean_data_and_add_columns(data: pd.DataFrame):
|
|
40 |
data["Company response - detailed"] = data["Company response - detailed"].replace("Closed", "Closed without relief")
|
41 |
data["State"] = data["State"].replace("UNITED STATES MINOR OUTLYING ISLANDS", "UM")
|
42 |
data["State"] = fill_na_with_random(data, "State")
|
43 |
-
data["Consumer disputed?"] = data["Consumer disputed?"].fillna("No")
|
44 |
|
45 |
# Convert to correct data type
|
46 |
data["Date Received"] = pd.to_datetime(data["Date Received"], format="%m/%d/%y").dt.strftime("%Y-%m-%d")
|
|
|
47 |
|
48 |
# Create additional columns
|
49 |
-
data["Month"] = pd.to_datetime(data["Date Received"], format="%Y-%m-%d").dt.strftime("%m")
|
50 |
-
data["Year"] = pd.to_datetime(data["Date Received"], format="%Y-%m-%d").dt.strftime("%Y")
|
51 |
data["Region"] = data["State"].map(REGION_MAPPING)
|
52 |
data["Company response"] = np.where(
|
53 |
data["Company response - detailed"].str.contains("Closed"), "Closed", data["Company response - detailed"]
|
@@ -55,74 +52,4 @@ def clean_data_and_add_columns(data: pd.DataFrame):
|
|
55 |
data["Company response - Closed"] = np.where(
|
56 |
data["Company response - detailed"].str.contains("Closed"), data["Company response - detailed"], "Not closed"
|
57 |
)
|
58 |
-
|
59 |
-
# Filter 2018 and 2019 only
|
60 |
-
data = data[(data["Year"].isin(["2018", "2019"]))]
|
61 |
-
return data
|
62 |
-
|
63 |
-
|
64 |
-
def create_data_for_kpi_cards(data):
|
65 |
-
"""Formats and aggregates the data for the KPI cards."""
|
66 |
-
total_complaints = (
|
67 |
-
data.groupby("Year")
|
68 |
-
.agg({"Complaint ID": "count"})
|
69 |
-
.rename(columns={"Complaint ID": "Total Complaints"})
|
70 |
-
.reset_index()
|
71 |
-
)
|
72 |
-
closed_complaints = (
|
73 |
-
data[data["Company response"] == "Closed"]
|
74 |
-
.groupby("Year")
|
75 |
-
.agg({"Complaint ID": "count"})
|
76 |
-
.rename(columns={"Complaint ID": "Closed Complaints"})
|
77 |
-
.reset_index()
|
78 |
-
)
|
79 |
-
timely_response = (
|
80 |
-
data[data["Timely response?"] == "Yes"]
|
81 |
-
.groupby("Year")
|
82 |
-
.agg({"Complaint ID": "count"})
|
83 |
-
.rename(columns={"Complaint ID": "Timely response"})
|
84 |
-
.reset_index()
|
85 |
-
)
|
86 |
-
closed_without_cost = (
|
87 |
-
data[data["Company response - Closed"] != "Closed with monetary relief"]
|
88 |
-
.groupby("Year")
|
89 |
-
.agg({"Complaint ID": "count"})
|
90 |
-
.rename(columns={"Complaint ID": "Closed w/o cost"})
|
91 |
-
.reset_index()
|
92 |
-
)
|
93 |
-
consumer_disputed = (
|
94 |
-
data[data["Consumer disputed?"] == "Yes"]
|
95 |
-
.groupby("Year")
|
96 |
-
.agg({"Complaint ID": "count"})
|
97 |
-
.rename(columns={"Complaint ID": "Consumer disputed"})
|
98 |
-
.reset_index()
|
99 |
-
)
|
100 |
-
|
101 |
-
# Merge all data frames into one
|
102 |
-
dfs_to_merge = [total_complaints, closed_complaints, timely_response, closed_without_cost, consumer_disputed]
|
103 |
-
df_kpi = reduce(lambda left, right: pd.merge(left, right, on="Year", how="outer"), dfs_to_merge)
|
104 |
-
|
105 |
-
# Calculate percentages
|
106 |
-
df_kpi.fillna(0, inplace=True)
|
107 |
-
df_kpi["Closed Complaints"] = df_kpi["Closed Complaints"] / df_kpi["Total Complaints"] * 100
|
108 |
-
df_kpi["Open Complaints"] = 100 - df_kpi["Closed Complaints"]
|
109 |
-
df_kpi["Timely response"] = df_kpi["Timely response"] / df_kpi["Total Complaints"] * 100
|
110 |
-
df_kpi["Closed w/o cost"] = df_kpi["Closed w/o cost"] / df_kpi["Total Complaints"] * 100
|
111 |
-
df_kpi["Consumer disputed"] = df_kpi["Consumer disputed"] / df_kpi["Total Complaints"] * 100
|
112 |
-
|
113 |
-
# Pivot the dataframe and flatten
|
114 |
-
df_kpi["index"] = 0
|
115 |
-
df_kpi = df_kpi.pivot(
|
116 |
-
index="index",
|
117 |
-
columns="Year",
|
118 |
-
values=[
|
119 |
-
"Total Complaints",
|
120 |
-
"Closed Complaints",
|
121 |
-
"Open Complaints",
|
122 |
-
"Timely response",
|
123 |
-
"Closed w/o cost",
|
124 |
-
"Consumer disputed",
|
125 |
-
],
|
126 |
-
)
|
127 |
-
df_kpi.columns = [f"{kpi}_{year}" for kpi, year in df_kpi.columns]
|
128 |
-
return df_kpi
|
|
|
1 |
"""Contains helper functions and variables."""
|
2 |
|
|
|
|
|
3 |
import numpy as np
|
4 |
import pandas as pd
|
5 |
|
|
|
38 |
data["Company response - detailed"] = data["Company response - detailed"].replace("Closed", "Closed without relief")
|
39 |
data["State"] = data["State"].replace("UNITED STATES MINOR OUTLYING ISLANDS", "UM")
|
40 |
data["State"] = fill_na_with_random(data, "State")
|
|
|
41 |
|
42 |
# Convert to correct data type
|
43 |
data["Date Received"] = pd.to_datetime(data["Date Received"], format="%m/%d/%y").dt.strftime("%Y-%m-%d")
|
44 |
+
data["Date Submitted"] = pd.to_datetime(data["Date Submitted"], format="%m/%d/%y").dt.strftime("%Y-%m-%d")
|
45 |
|
46 |
# Create additional columns
|
47 |
+
data["Year-Month Received"] = pd.to_datetime(data["Date Received"], format="%Y-%m-%d").dt.strftime("%Y-%m")
|
|
|
48 |
data["Region"] = data["State"].map(REGION_MAPPING)
|
49 |
data["Company response"] = np.where(
|
50 |
data["Company response - detailed"].str.contains("Closed"), "Closed", data["Company response - detailed"]
|
|
|
52 |
data["Company response - Closed"] = np.where(
|
53 |
data["Company response - detailed"].str.contains("Closed"), data["Company response - detailed"], "Not closed"
|
54 |
)
|
55 |
+
return data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|