diff --git a/.github/images/huggingface_collection.png b/.github/images/huggingface_collection.png new file mode 100644 index 000000000..e45d4ef60 Binary files /dev/null and b/.github/images/huggingface_collection.png differ diff --git a/.github/images/vizro_examples_gallery.png b/.github/images/vizro_examples_gallery.png new file mode 100644 index 000000000..1875d1bde Binary files /dev/null and b/.github/images/vizro_examples_gallery.png differ diff --git a/README.md b/README.md index 5c7f79223..035facc56 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ You can see Vizro in action by clicking on the following image or by visiting [t

- +

## Dashboard screenshots diff --git a/vizro-core/changelog.d/20240826_141315_huong_li_nguyen_remove_kpi_dashboard.md b/vizro-core/changelog.d/20240826_141315_huong_li_nguyen_remove_kpi_dashboard.md new file mode 100644 index 000000000..f1f65e73c --- /dev/null +++ b/vizro-core/changelog.d/20240826_141315_huong_li_nguyen_remove_kpi_dashboard.md @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/vizro-core/examples/README.md b/vizro-core/examples/README.md new file mode 100644 index 000000000..72146bfb7 --- /dev/null +++ b/vizro-core/examples/README.md @@ -0,0 +1,20 @@ +## Examples + +Please note that this folder contains only example dashboards that are still **in development**. + +### Vizro examples gallery + +To view a comprehensive list of available demos, please visit our [examples gallery](http://vizro.mckinsey.com/). +There, you can explore a wide range of dashboards and applications created with Vizro. + + + + + +### Huggingface collection + +For a curated list of example dashboards, check out our [dashboard collection on Huggingface](https://huggingface.co/collections/vizro/vizro-official-gallery-66697d414646eeac61eae6de). + + + + diff --git a/vizro-core/examples/kpi/README.md b/vizro-core/examples/kpi/README.md deleted file mode 100644 index 22d3ed4cd..000000000 --- a/vizro-core/examples/kpi/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# KPI Dashboard - -This dashboard provides an example of a Key Performance Indicator (KPI) dashboard, designed to help users get started -and extend further. It uses fictional budget data to demonstrate the capabilities of Vizro using real world applications. - -**Created by:** [Huong Li Nguyen](https://github.com/huong-li-nguyen) - ---- - -### ๐Ÿ—“๏ธ Data - -Special thanks to the [#RWFD Real World Fake Data initiative](https://data.world/markbradbourne/rwfd-real-world-fake-data), a community project that -provides high-quality fake data for creating realistic dashboard examples for real-world applications. - -**Note:** The data has been additionally edited for the purpose of this example. - -### ๐Ÿ“Š Plotly resources - -- [Bar charts](https://plotly.com/python/bar-charts/) -- [Pie charts](https://plotly.com/python/pie-charts/) -- [Choropleth maps](https://plotly.com/python/choropleth-maps/) -- [Unstacked area charts](https://plotly.com/python/filled-area-plots/) - -### ๐Ÿš€ Vizro features applied - -- [Vizro tutorial on pages, layouts and dashboards](https://vizro.readthedocs.io/en/stable/pages/tutorials/explore-components/) -- [Custom components](https://vizro.readthedocs.io/en/stable/pages/user-guides/custom-components/) -- [Custom charts](https://vizro.readthedocs.io/en/stable/pages/user-guides/custom-charts/) -- [Custom CSS](https://vizro.readthedocs.io/en/stable/pages/user-guides/assets/) - -### ๐Ÿ–ฅ๏ธ App demo - -Gif to KPI dashboard - ---- - -## How to run the example locally - -1. Run the `app.py` file with your environment activated where `vizro` is installed. -2. You should now be able to access the app locally via http://127.0.0.1:8050/. diff --git a/vizro-core/examples/kpi/app.py b/vizro-core/examples/kpi/app.py deleted file mode 100644 index d46e76a6d..000000000 --- a/vizro-core/examples/kpi/app.py +++ /dev/null @@ -1,251 +0,0 @@ -"""Example to show dashboard configuration.""" - -import pandas as pd -import vizro.models as vm -from utils._charts import COLUMN_DEFS, FlexContainer, area, bar, choropleth, pie -from utils._helper import clean_data_and_add_columns, create_data_for_kpi_cards -from vizro import Vizro -from vizro.actions import filter_interaction -from vizro.figures import kpi_card_reference -from vizro.tables import dash_ag_grid - -# DATA -------------------------------------------------------------------------------------------- -df_complaints = pd.read_csv("https://query.data.world/s/glbdstahsuw3hjgunz3zssggk7dsfu?dws=00000") -df_complaints = clean_data_and_add_columns(df_complaints) -df_kpi_cards = create_data_for_kpi_cards(df_complaints) -vm.Page.add_type("components", FlexContainer) - - -# SUB-SECTIONS ------------------------------------------------------------------------------------ -kpi_banner = FlexContainer( - components=[ - vm.Figure( - id="kpi-reverse-coloring", - figure=kpi_card_reference( - df_kpi_cards, - value_column="Total Complaints_2019", - reference_column="Total Complaints_2018", - title="Total Complaints", - value_format="{value:.0f}", - reference_format="{delta_relative:+.1%} vs. 2018 ({reference:.0f})", - icon="person", - ), - ), - vm.Figure( - figure=kpi_card_reference( - df_kpi_cards, - value_column="Closed Complaints_2019", - reference_column="Closed Complaints_2018", - title="Closed Complaints", - value_format="{value:.1f}%", - reference_format="{delta:+.1f}pp vs. 2018 ({reference:.1f}%)", - icon="inventory", - ) - ), - vm.Figure( - figure=kpi_card_reference( - df_kpi_cards, - value_column="Timely response_2019", - reference_column="Timely response_2018", - title="Timely Response", - value_format="{value:.1f}%", - reference_format="{delta:+.1f}pp vs. 2018 ({reference:.1f}%)", - icon="timer", - ) - ), - vm.Figure( - figure=kpi_card_reference( - df_kpi_cards, - value_column="Closed w/o cost_2019", - reference_column="Closed w/o cost_2018", - title="Closed w/o cost", - value_format="{value:.1f}%", - reference_format="{delta:.1f}pp vs. 2018 ({reference:.1f}%)", - icon="payments", - ) - ), - vm.Figure( - figure=kpi_card_reference( - df_kpi_cards, - value_column="Consumer disputed_2019", - reference_column="Consumer disputed_2018", - title="Consumer disputed", - value_format="{value:.1f}%", - reference_format="{delta:+.1f}pp vs. 2018 ({reference:.1f}%)", - icon="sentiment_dissatisfied", - ) - ), - ], - classname="kpi-banner", -) - -bar_charts_tabbed = vm.Tabs( - tabs=[ - vm.Container( - title="By Product", - components=[ - vm.Graph( - figure=bar( - data_frame=df_complaints, - y="Product", - x="Complaint ID", - ), - ) - ], - ), - vm.Container( - title="By Channel", - components=[ - vm.Graph( - figure=bar( - data_frame=df_complaints, - y="Channel", - x="Complaint ID", - ), - ) - ], - ), - vm.Container( - title="By Region", - components=[ - vm.Graph( - figure=bar( - data_frame=df_complaints, - y="Region", - x="Complaint ID", - ), - ) - ], - ), - vm.Container( - title="By Issue", - components=[ - vm.Graph( - figure=bar( - data_frame=df_complaints, - y="Issue", - x="Complaint ID", - ), - ) - ], - ), - ], -) - -# PAGES -------------------------------------------------------------------------------------- -page_exec = vm.Page( - title="Executive View", - layout=vm.Layout( - grid=[ - [0, 0], - [0, 0], - [1, 2], - [1, 2], - [1, 2], - [1, 3], - [1, 3], - [1, 3], - ], - ), - components=[ - kpi_banner, - bar_charts_tabbed, - vm.Graph(figure=area(data_frame=df_complaints, y="Complaint ID", x="Month")), - vm.Graph( - figure=pie( - data_frame=df_complaints[df_complaints["Company response - Closed"] != "Not closed"], - values="Complaint ID", - names="Company response - Closed", - title="Closed company responses", - ) - ), - ], -) - -page_region = vm.Page( - title="Regional View", - layout=vm.Layout(grid=[[0, 1]]), - components=[ - vm.Graph( - figure=choropleth( - data_frame=df_complaints, - locations="State", - color="Complaint ID", - title="Complaints by State
โคต Click on a state to filter the " - "charts on the right. Refresh the page to deselect.", - custom_data=["State"], - ), - actions=[ - vm.Action( - function=filter_interaction(targets=["regional-issue", "regional-product"]), - ) - ], - ), - vm.Tabs( - tabs=[ - vm.Container( - title="By Product", - components=[ - vm.Graph( - id="regional-product", - figure=bar( - data_frame=df_complaints, - y="Product", - x="Complaint ID", - ), - ) - ], - ), - vm.Container( - title="By Issue", - components=[ - vm.Graph( - id="regional-issue", - figure=bar( - data_frame=df_complaints, - y="Issue", - x="Complaint ID", - ), - ) - ], - ), - ], - ), - ], - controls=[ - vm.Filter(column="Region", selector=vm.Checklist()), - vm.Filter(column="State"), - vm.Filter(column="Product"), - vm.Filter(column="Issue"), - ], -) - -page_table = vm.Page( - title="List of complaints", - components=[ - vm.AgGrid( - figure=dash_ag_grid( - data_frame=df_complaints, - columnDefs=COLUMN_DEFS, - dashGridOptions={"pagination": True}, - ) - ) - ], -) - -dashboard = vm.Dashboard( - pages=[page_exec, page_region, page_table], - title="Cumulus Financial Corp. - Fiscal Year 2019", - navigation=vm.Navigation( - nav_selector=vm.NavBar( - items=[ - vm.NavLink(label="Executive View", icon="Leaderboard", pages=["Executive View"]), - vm.NavLink(label="Regional View", icon="South America", pages=["Regional View"]), - vm.NavLink(label="Table View", icon="Table View", pages=["List of complaints"]), - ] - ) - ), -) - -if __name__ == "__main__": - Vizro().build(dashboard).run() diff --git a/vizro-core/examples/kpi/assets/css/custom.css b/vizro-core/examples/kpi/assets/css/custom.css deleted file mode 100644 index 1117dcaef..000000000 --- a/vizro-core/examples/kpi/assets/css/custom.css +++ /dev/null @@ -1,40 +0,0 @@ -#page-header { - padding-left: 4px; -} - -.card-kpi { - min-width: 220px; - padding: 0.75rem; -} - -.kpi-banner { - display: flex; - gap: 1rem; - height: 100%; - overflow: scroll; -} - -.kpi-banner .figure-container { - height: unset; -} - -.kpi-banner::-webkit-scrollbar-thumb { - border: 5px solid var(--main-container-bg-color); -} - -/* Apply reverse color coding for one KPI card */ -#kpi-reverse-coloring .card-kpi .color-pos.card-footer { - color: var(--bs-pink); -} - -#kpi-reverse-coloring .card-kpi .color-neg.card-footer { - color: var(--bs-blue); -} - -#kpi-reverse-coloring .card-kpi:has(.color-pos) { - border-left: 4px solid var(--bs-pink); -} - -#kpi-reverse-coloring .card-kpi:has(.color-neg) { - border-left: 4px solid var(--bs-blue); -} diff --git a/vizro-core/examples/kpi/assets/favicon.ico b/vizro-core/examples/kpi/assets/favicon.ico deleted file mode 100644 index 240c9f541..000000000 Binary files a/vizro-core/examples/kpi/assets/favicon.ico and /dev/null differ diff --git a/vizro-core/examples/kpi/assets/images/app.svg b/vizro-core/examples/kpi/assets/images/app.svg deleted file mode 100644 index 9d07d6372..000000000 --- a/vizro-core/examples/kpi/assets/images/app.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/vizro-core/examples/kpi/assets/images/logo.svg b/vizro-core/examples/kpi/assets/images/logo.svg deleted file mode 100644 index 0904b87de..000000000 --- a/vizro-core/examples/kpi/assets/images/logo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/vizro-core/examples/kpi/images/kpi-dashboard.gif b/vizro-core/examples/kpi/images/kpi-dashboard.gif deleted file mode 100644 index 39b0b188d..000000000 Binary files a/vizro-core/examples/kpi/images/kpi-dashboard.gif and /dev/null differ diff --git a/vizro-core/examples/kpi/utils/__init__.py b/vizro-core/examples/kpi/utils/__init__.py deleted file mode 100644 index 11387ccac..000000000 --- a/vizro-core/examples/kpi/utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Utils folder to contain helper functions and custom charts/components.""" diff --git a/vizro-core/examples/kpi/utils/_charts.py b/vizro-core/examples/kpi/utils/_charts.py deleted file mode 100644 index 7b8be2985..000000000 --- a/vizro-core/examples/kpi/utils/_charts.py +++ /dev/null @@ -1,204 +0,0 @@ -"""Contains custom components and charts used inside the dashboard.""" - -from typing import List, Literal, Optional - -import pandas as pd -import plotly.graph_objects as go -import vizro.models as vm -import vizro.plotly.express as px -from dash import html -from vizro.models.types import capture - - -# CUSTOM COMPONENTS ------------------------------------------------------------- -class FlexContainer(vm.Container): - """Custom flex `Container`.""" - - type: Literal["flex_container"] = "flex_container" - title: str = None # Title exists in vm.Container but we don't want to use it here. - classname: str = "d-flex" - - def build(self): - """Returns a flex container.""" - return html.Div( - id=self.id, children=[component.build() for component in self.components], className=self.classname - ) - - -# CUSTOM CHARTS ---------------------------------------------------------------- -@capture("graph") -def bar( - x: str, - y: str, - data_frame: pd.DataFrame, - top_n: int = 15, - custom_data: Optional[List[str]] = None, -): - """Custom bar chart implementation. - - Based on [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar). - """ - df_agg = data_frame.groupby(y).agg({x: "count"}).sort_values(by=x, ascending=False).reset_index() - fig = px.bar( - data_frame=df_agg.head(top_n), - x=x, - y=y, - orientation="h", - text=x, - color_discrete_sequence=["#1A85FF"], - custom_data=custom_data, - ) - fig.update_layout(xaxis_title="# of Complaints", yaxis={"title": "", "autorange": "reversed"}) - return fig - - -@capture("graph") -def area(x: str, y: str, data_frame: pd.DataFrame): - """Custom chart to create unstacked area chart. - - Based on [go.Scatter](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Scatter.html). - - """ - df_agg = data_frame.groupby(["Year", "Month"]).agg({y: "count"}).reset_index() - df_agg_2019 = df_agg[df_agg["Year"] == "2018"] - df_agg_2020 = df_agg[df_agg["Year"] == "2019"] - - fig = go.Figure() - fig.add_trace( - go.Scatter(x=df_agg_2020[x], y=df_agg_2020[y], fill="tozeroy", name="2019", marker={"color": "#1a85ff"}) - ) - fig.add_trace(go.Scatter(x=df_agg_2019[x], y=df_agg_2019[y], fill="tonexty", name="2018", marker={"color": "grey"})) - fig.update_layout( - title="Complaints over time", - xaxis_title="Date Received", - yaxis_title="# of Complaints", - title_pad_t=4, - xaxis={ - "showgrid": False, - "tickmode": "array", - "tickvals": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], - "ticktext": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], - }, - ) - return fig - - -@capture("graph") -def pie( - names: str, - values: str, - data_frame: pd.DataFrame = None, - title: Optional[str] = None, -): - """Custom pie chart implementation. - - Based on [px.pie](https://plotly.com/python-api-reference/generated/plotly.express.pie). - """ - df_agg = data_frame.groupby(names).agg({values: "count"}).reset_index() - fig = px.pie( - data_frame=df_agg, - names=names, - values=values, - color=names, - color_discrete_map={ - "Closed with explanation": "#1a85ff", - "Closed with monetary relief": "#d41159", - "Closed with non-monetary relief": "#adbedc", - "Closed without relief": "#7ea1ee", - "Closed with relief": "#df658c", - "Closed": "#1a85ff", - }, - title=title, - hole=0.4, - ) - fig.update_layout(legend_x=1, legend_y=1, title_pad_t=2, margin={"l": 0, "r": 0, "t": 60, "b": 0}) - return fig - - -@capture("graph") -def choropleth( - locations: str, - color: str, - data_frame: pd.DataFrame = None, - title: Optional[str] = None, - custom_data: Optional[List[str]] = None, -): - """Custom choropleth implementation. - - Based on [px.choropleth](https://plotly.com/python-api-reference/generated/plotly.express.choropleth). - """ - df_agg = data_frame.groupby(locations).agg({color: "count"}).reset_index() - fig = px.choropleth( - data_frame=df_agg, - locations=locations, - color=color, - color_continuous_scale=[ - "#ded6d8", - "#f3bdcb", - "#f7a9be", - "#f894b1", - "#f780a3", - "#f46b94", - "#ee517f", - "#e94777", - "#e43d70", - "#df3168", - "#d92460", - "#d41159", - ], - scope="usa", - locationmode="USA-states", - title=title, - custom_data=custom_data, - ) - fig.update_coloraxes(colorbar={"thickness": 10, "title": {"side": "bottom"}, "orientation": "h", "x": 0.5, "y": 0}) - return fig - - -# TABLE CONFIGURATIONS --------------------------------------------------------- -CELL_STYLE = { - "styleConditions": [ - { - "condition": "params.value == 'Closed with explanation'", - "style": {"backgroundColor": "#1a85ff"}, - }, - { - "condition": "params.value == 'Closed with monetary relief'", - "style": {"backgroundColor": "#d41159"}, - }, - { - "condition": "params.value == 'Closed with non-monetary relief'", - "style": {"backgroundColor": "#adbedc"}, - }, - { - "condition": "params.value == 'Closed without relief'", - "style": {"backgroundColor": "#7ea1ee"}, - }, - { - "condition": "params.value == 'Closed with relief'", - "style": {"backgroundColor": "#df658c"}, - }, - { - "condition": "params.value == 'Closed'", - "style": {"backgroundColor": "#1a85ff"}, - }, - ] -} - - -COLUMN_DEFS = [ - {"field": "Complaint ID", "cellDataType": "text", "headerName": "ID", "flex": 3}, - {"field": "Date Received", "cellDataType": "text", "headerName": "Date", "flex": 3}, - {"field": "Channel", "cellDataType": "text", "flex": 3}, - {"field": "State", "cellDataType": "text", "flex": 2}, - {"field": "Product", "cellDataType": "text", "flex": 5}, - {"field": "Issue", "cellDataType": "text", "flex": 5}, - { - "field": "Company response - detailed", - "cellDataType": "text", - "cellStyle": CELL_STYLE, - "headerName": "Company response", - "flex": 6, - }, - {"field": "Timely response?", "cellRenderer": "markdown", "headerName": "On time?", "flex": 3}, -] diff --git a/vizro-core/examples/kpi/utils/_helper.py b/vizro-core/examples/kpi/utils/_helper.py deleted file mode 100644 index 0157a60ff..000000000 --- a/vizro-core/examples/kpi/utils/_helper.py +++ /dev/null @@ -1,128 +0,0 @@ -"""Contains helper functions and variables.""" - -from functools import reduce - -import numpy as np -import pandas as pd - -REGION_MAPPING = { - **dict.fromkeys(["CT", "ME", "MA", "NH", "RI", "VT", "NJ", "NY", "PA"], "North East"), - **dict.fromkeys( - ["IL", "IN", "MI", "OH", "WI", "IA", "KS", "MN", "MO", "NE", "ND", "SD"], "Mid West" # codespell:ignore - ), - **dict.fromkeys( - ["DE", "FL", "GA", "MD", "NC", "SC", "VA", "WV", "DC", "AL", "KY", "MS", "TN", "AR", "LA"], "South" - ), - **dict.fromkeys(["AZ", "NM", "OK", "TX"], "South West"), - **dict.fromkeys(["CO", "ID", "MT", "NV", "UT", "WY", "AK", "CA", "HI", "OR", "WA"], "West"), - **dict.fromkeys(["UM", "PR", "AP", "VI", "AE", "AS", "GU", "FM", "PW", "MP"], "Other"), -} - - -def fill_na_with_random(df, column): - """Fills missing values in a column with random values from the same column.""" - non_na_values = df[column].dropna().values - df[column] = df[column].apply(lambda x: np.random.choice(non_na_values) if pd.isna(x) else x) - return df[column] - - -def clean_data_and_add_columns(data: pd.DataFrame): - """Tidies the original data set, adds new columns, and changes cell values for the purpose of this example.""" - data = data.rename( - columns={ - "Date Sumbited": "Date Submitted", - "Submitted via": "Channel", - "Company response to consumer": "Company response - detailed", - }, - ) - - # Clean cell values and/or assign different values for the purpose of this example - data["Company response - detailed"] = data["Company response - detailed"].replace("Closed", "Closed without relief") - data["State"] = data["State"].replace("UNITED STATES MINOR OUTLYING ISLANDS", "UM") - data["State"] = fill_na_with_random(data, "State") - data["Consumer disputed?"] = data["Consumer disputed?"].fillna("No") - - # Convert to correct data type - data["Date Received"] = pd.to_datetime(data["Date Received"], format="%m/%d/%y").dt.strftime("%Y-%m-%d") - - # Create additional columns - data["Month"] = pd.to_datetime(data["Date Received"], format="%Y-%m-%d").dt.strftime("%m") - data["Year"] = pd.to_datetime(data["Date Received"], format="%Y-%m-%d").dt.strftime("%Y") - data["Region"] = data["State"].map(REGION_MAPPING) - data["Company response"] = np.where( - data["Company response - detailed"].str.contains("Closed"), "Closed", data["Company response - detailed"] - ) - data["Company response - Closed"] = np.where( - data["Company response - detailed"].str.contains("Closed"), data["Company response - detailed"], "Not closed" - ) - - # Filter 2018 and 2019 only - data = data[(data["Year"].isin(["2018", "2019"]))] - return data - - -def create_data_for_kpi_cards(data): - """Formats and aggregates the data for the KPI cards.""" - total_complaints = ( - data.groupby("Year") - .agg({"Complaint ID": "count"}) - .rename(columns={"Complaint ID": "Total Complaints"}) - .reset_index() - ) - closed_complaints = ( - data[data["Company response"] == "Closed"] - .groupby("Year") - .agg({"Complaint ID": "count"}) - .rename(columns={"Complaint ID": "Closed Complaints"}) - .reset_index() - ) - timely_response = ( - data[data["Timely response?"] == "Yes"] - .groupby("Year") - .agg({"Complaint ID": "count"}) - .rename(columns={"Complaint ID": "Timely response"}) - .reset_index() - ) - closed_without_cost = ( - data[data["Company response - Closed"] != "Closed with monetary relief"] - .groupby("Year") - .agg({"Complaint ID": "count"}) - .rename(columns={"Complaint ID": "Closed w/o cost"}) - .reset_index() - ) - consumer_disputed = ( - data[data["Consumer disputed?"] == "Yes"] - .groupby("Year") - .agg({"Complaint ID": "count"}) - .rename(columns={"Complaint ID": "Consumer disputed"}) - .reset_index() - ) - - # Merge all data frames into one - dfs_to_merge = [total_complaints, closed_complaints, timely_response, closed_without_cost, consumer_disputed] - df_kpi = reduce(lambda left, right: pd.merge(left, right, on="Year", how="outer"), dfs_to_merge) - - # Calculate percentages - df_kpi.fillna(0, inplace=True) - df_kpi["Closed Complaints"] = df_kpi["Closed Complaints"] / df_kpi["Total Complaints"] * 100 - df_kpi["Open Complaints"] = 100 - df_kpi["Closed Complaints"] - df_kpi["Timely response"] = df_kpi["Timely response"] / df_kpi["Total Complaints"] * 100 - df_kpi["Closed w/o cost"] = df_kpi["Closed w/o cost"] / df_kpi["Total Complaints"] * 100 - df_kpi["Consumer disputed"] = df_kpi["Consumer disputed"] / df_kpi["Total Complaints"] * 100 - - # Pivot the dataframe and flatten - df_kpi["index"] = 0 - df_kpi = df_kpi.pivot( - index="index", - columns="Year", - values=[ - "Total Complaints", - "Closed Complaints", - "Open Complaints", - "Timely response", - "Closed w/o cost", - "Consumer disputed", - ], - ) - df_kpi.columns = [f"{kpi}_{year}" for kpi, year in df_kpi.columns] - return df_kpi