render.data_frame
render.data_frame(self, fn)
Decorator for a function that returns a pandas or polars DataFrame
object to render as an interactive table or grid. Features fast virtualized scrolling, sorting, filtering, and row selection (single or multiple).
Returns
Type | Description |
---|---|
A decorator for a function that returns any of the following: 1. A DataGrid or DataTable object, which can be used to customize the appearance and behavior of the data frame output. 2. A pandas DataFrame object or a polars DataFrame object. This object will be internally upgraded to shiny.render.DataGrid(df) . |
Row Selection
When using the row selection feature, you can access the selected rows by using the <data_frame_renderer>.cell_selection()
method, where <data_frame_renderer>
is the @render.data_frame
function name that corresponds with the id=
used in outout_data_frame
. Internally, <data_frame_renderer>.cell_selection()
retrieves the selected cell information from session’s input.<data_frame_renderer>_cell_selection()
value and upgrades it for consistent subsetting.
For example, to filter your pandas data frame (df
) down to the selected rows you can use:
df.iloc[list(input.<data_frame_renderer>_cell_selection()["rows"])]
df.iloc[list(<data_frame_renderer>.cell_selection()["rows"])]
<data_frame_renderer>.data_view(selected=True)
The last method (.data_view(selected=True)
) will also apply any sorting, filtering, or edits that has been applied by the user.
Editing Cells
When a returned DataTable
or DataGrid
object has editable=True
, app users will be able to edit the cells in the table. After a cell has been edited, the edited value will be sent to the server for processing. The handling methods are set via @<data_frame_renderer>.set_patch_fn
or @<data_frame_renderer>.set_patches_fn
decorators. By default, both decorators will return a string value.
To access the data viewed by the user, use <data_frame_renderer>.data_view()
. This method will sort, filter, and apply any patches to the data frame as viewed by the user within the browser. This is a shallow copy of the original data frame. It is possible that alterations to data_view
could alter the original data
data frame.
To access the original data, use <data_frame_renderer>.data()
. This is a quick reference to the original pandas or polars data frame that was returned from the app’s render function. If it is mutated in place, it will modify the original data.
Note… if the data frame renderer is re-rendered due to reactivity, then the user’s edits, sorting, and filtering will be lost. We hope to improve upon this in the future.
Tip
This decorator should be applied before the @output
decorator (if that decorator is used). Also, the name of the decorated function (or @output(id=...)
) should match the id
of a output_data_frame container (see output_data_frame for example usage).
See Also
- output_data_frame
- DataGrid and DataTable are the objects you can return from the rendering function to specify options.
Examples
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 400
## file: app.py
import pandas # noqa: F401 (this line needed for Shinylive to load plotly.express)
import plotly.express as px
from shinywidgets import output_widget, render_widget
from shiny import App, reactive, render, req, ui
# Load the Gapminder dataset
df = px.data.gapminder()
# Prepare a summary DataFrame
summary_df = (
df.groupby("country")
.agg(
{
"pop": ["min", "max", "mean"],
"lifeExp": ["min", "max", "mean"],
"gdpPercap": ["min", "max", "mean"],
}
)
.reset_index()
)
summary_df.columns = ["_".join(col).strip() for col in summary_df.columns.values]
summary_df.rename(columns={"country_": "country"}, inplace=True)
app_ui = ui.page_fillable(
{"class": "p-3"},
ui.markdown(
"**Instructions**: Select one or more countries in the table below to see more information."
),
ui.layout_columns(
ui.card(ui.output_data_frame("summary_data"), height="400px"),
ui.card(output_widget("country_detail_pop"), height="400px"),
ui.card(output_widget("country_detail_percap"), height="400px"),
col_widths=[12, 6, 6],
),
)
def server(input, output, session):
@render.data_frame
def summary_data():
return render.DataGrid(summary_df.round(2), selection_mode="rows")
@reactive.calc
def filtered_df():
data_selected = summary_data.data_view(selected=True)
req(not data_selected.empty)
countries = data_selected["country"]
# Filter data for selected countries
return df[df["country"].isin(countries)]
@render_widget
def country_detail_pop():
return px.line(
filtered_df(),
x="year",
y="pop",
color="country",
title="Population Over Time",
)
@render_widget
def country_detail_percap():
return px.line(
filtered_df(),
x="year",
y="gdpPercap",
color="country",
title="GDP per Capita Over Time",
)
app = App(app_ui, server)
Attributes
Name | Description |
---|---|
cell_patches | Reactive value of the data frame’s edits provided by the user. |
cell_selection | Reactive value of selected cell information. |
data | Reactive value of the data frame’s output data. |
data_view_rows | Reactive value of the data frame’s user view row numbers. |
filter | Reactive value of the data frame’s column filters. |
selection_modes | Reactive value of the data frame’s possible selection modes. |
sort | Reactive value of the data frame’s column sorting information. |
Methods
Name | Description |
---|---|
data_view | Reactive function that retrieves the data how it is viewed within the browser. |
input_cell_selection | [Deprecated] Reactive value of selected cell information. |
set_patch_fn | Decorator to set the function that updates a single cell in the data frame. |
set_patches_fn | Decorator to set the function that updates a batch of cells in the data frame. |
update_cell_selection | Update the cell selection in the data frame. |
update_filter | Update the column filtering in the data frame. |
update_sort | Update the column sorting in the data frame. |
data_view
render.data_frame.data_view(selected=False)
Reactive function that retrieves the data how it is viewed within the browser.
This function will sort, filter, and apply any patches to the data frame as viewed by the user within the browser.
This is a shallow copy of the original data frame. It is possible that alterations to data_view
could alter the original data
data frame. Please be cautious when using this value directly.
Parameters
selected: bool = False
-
If
True
, subset the viewed data to the selected area. Defaults toFalse
.
Returns
Type | Description |
---|---|
DataFrameLikeT |
A view of the data frame as seen in the browser. Even if the rendered data value was not of type pd.DataFrame or pl.DataFrame , this method currently returns the converted pd.DataFrame . |
See Also
- [
pandas.DataFrame.copy
API documentation]h(ttps://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.copy.html)
Examples
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 400
## file: app.py
from shared import mtcars
from shiny import App, reactive, render, ui
app_ui = ui.page_fillable(
ui.layout_columns(
ui.card(
ui.card_header(
ui.markdown(
"""
##### Editable data frame
* Edit the cells!
* Sort the columns!
"""
)
),
ui.output_data_frame("df_original"),
),
ui.card(
ui.card_header(
ui.markdown(
"""
##### Updated data from the first data frame
* Select the rows!
* Filter and sort the columns!
"""
)
),
ui.output_data_frame("df_edited"),
),
ui.card(
ui.card_header(
ui.markdown(
"""
##### Selected data from the second data frame
* Sort the columns!
"""
)
),
ui.output_data_frame("df_selected"),
),
col_widths=[4, 4, 4],
),
)
def server(input, output, session):
df = reactive.value(mtcars.iloc[:, range(4)])
@render.data_frame
def df_original():
return render.DataGrid(
df(),
editable=True,
)
# Convert edited values to the correct data type
@df_original.set_patch_fn
def _(*, patch: render.CellPatch) -> render.CellValue:
if patch["column_index"] in [0, 2]:
return float(patch["value"])
return int(patch["value"])
@render.data_frame
def df_edited():
return render.DataGrid(
# Reactive value is updated when the user edits the data within `df_original` output
df_original.data_view(),
selection_mode="rows",
filters=True,
)
@render.data_frame
def df_selected():
return render.DataGrid(
# Reactive value is updated when the user selects rows the data within `df_edited` output
df_edited.data_view(selected=True),
selection_mode="rows",
)
app = App(app_ui, server)
## file: mtcars.csv
mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb
21,6,160,110,3.9,2.62,16.46,0,1,4,4
21,6,160,110,3.9,2.875,17.02,0,1,4,4
22.8,4,108,93,3.85,2.32,18.61,1,1,4,1
21.4,6,258,110,3.08,3.215,19.44,1,0,3,1
18.7,8,360,175,3.15,3.44,17.02,0,0,3,2
18.1,6,225,105,2.76,3.46,20.22,1,0,3,1
14.3,8,360,245,3.21,3.57,15.84,0,0,3,4
24.4,4,146.7,62,3.69,3.19,20,1,0,4,2
22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2
19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4
17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4
16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3
17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3
15.2,8,275.8,180,3.07,3.78,18,0,0,3,3
10.4,8,472,205,2.93,5.25,17.98,0,0,3,4
10.4,8,460,215,3,5.424,17.82,0,0,3,4
14.7,8,440,230,3.23,5.345,17.42,0,0,3,4
32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1
30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2
33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1
21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1
15.5,8,318,150,2.76,3.52,16.87,0,0,3,2
15.2,8,304,150,3.15,3.435,17.3,0,0,3,2
13.3,8,350,245,3.73,3.84,15.41,0,0,3,4
19.2,8,400,175,3.08,3.845,17.05,0,0,3,2
27.3,4,79,66,4.08,1.935,18.9,1,1,4,1
26,4,120.3,91,4.43,2.14,16.7,0,1,5,2
30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2
15.8,8,351,264,4.22,3.17,14.5,0,1,5,4
19.7,6,145,175,3.62,2.77,15.5,0,1,5,6
15,8,301,335,3.54,3.57,14.6,0,1,5,8
21.4,4,121,109,4.11,2.78,18.6,1,1,4,2
## file: shared.py
from pathlib import Path
import pandas as pd
app_dir = Path(__file__).parent
mtcars = pd.read_csv(app_dir / "mtcars.csv")
input_cell_selection
render.data_frame.input_cell_selection()
[Deprecated] Reactive value of selected cell information.
Please use ~shiny.render.data_frame
's .cell_selection()
method instead.
set_patch_fn
render.data_frame.set_patch_fn(fn)
Decorator to set the function that updates a single cell in the data frame.
The default patch function returns the value as is.
Parameters
fn:
PatchFn
|PatchFnSync
-
A function that accepts a kwarg
patch
and returns the processedpatch.value
for the cell.
Examples
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 400
## file: app.py
from shared import mtcars
from shiny import App, reactive, render, ui
app_ui = ui.page_fillable(
ui.layout_columns(
ui.card(
ui.card_header(
ui.markdown(
"""
##### Editable data frame
* Edit the cells!
* Sort the columns!
"""
)
),
ui.output_data_frame("df_original"),
),
ui.card(
ui.card_header(
ui.markdown(
"""
##### Updated data from the first data frame
* Select the rows!
* Filter and sort the columns!
"""
)
),
ui.output_data_frame("df_edited"),
),
ui.card(
ui.card_header(
ui.markdown(
"""
##### Selected data from the second data frame
* Sort the columns!
"""
)
),
ui.output_data_frame("df_selected"),
),
col_widths=[4, 4, 4],
),
)
def server(input, output, session):
df = reactive.value(mtcars.iloc[:, range(4)])
@render.data_frame
def df_original():
return render.DataGrid(
df(),
editable=True,
)
# Convert edited values to the correct data type
@df_original.set_patch_fn
def _(*, patch: render.CellPatch) -> render.CellValue:
if patch["column_index"] in [0, 2]:
return float(patch["value"])
return int(patch["value"])
@render.data_frame
def df_edited():
return render.DataGrid(
# Reactive value is updated when the user edits the data within `df_original` output
df_original.data_view(),
selection_mode="rows",
filters=True,
)
@render.data_frame
def df_selected():
return render.DataGrid(
# Reactive value is updated when the user selects rows the data within `df_edited` output
df_edited.data_view(selected=True),
selection_mode="rows",
)
app = App(app_ui, server)
## file: mtcars.csv
mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb
21,6,160,110,3.9,2.62,16.46,0,1,4,4
21,6,160,110,3.9,2.875,17.02,0,1,4,4
22.8,4,108,93,3.85,2.32,18.61,1,1,4,1
21.4,6,258,110,3.08,3.215,19.44,1,0,3,1
18.7,8,360,175,3.15,3.44,17.02,0,0,3,2
18.1,6,225,105,2.76,3.46,20.22,1,0,3,1
14.3,8,360,245,3.21,3.57,15.84,0,0,3,4
24.4,4,146.7,62,3.69,3.19,20,1,0,4,2
22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2
19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4
17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4
16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3
17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3
15.2,8,275.8,180,3.07,3.78,18,0,0,3,3
10.4,8,472,205,2.93,5.25,17.98,0,0,3,4
10.4,8,460,215,3,5.424,17.82,0,0,3,4
14.7,8,440,230,3.23,5.345,17.42,0,0,3,4
32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1
30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2
33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1
21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1
15.5,8,318,150,2.76,3.52,16.87,0,0,3,2
15.2,8,304,150,3.15,3.435,17.3,0,0,3,2
13.3,8,350,245,3.73,3.84,15.41,0,0,3,4
19.2,8,400,175,3.08,3.845,17.05,0,0,3,2
27.3,4,79,66,4.08,1.935,18.9,1,1,4,1
26,4,120.3,91,4.43,2.14,16.7,0,1,5,2
30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2
15.8,8,351,264,4.22,3.17,14.5,0,1,5,4
19.7,6,145,175,3.62,2.77,15.5,0,1,5,6
15,8,301,335,3.54,3.57,14.6,0,1,5,8
21.4,4,121,109,4.11,2.78,18.6,1,1,4,2
## file: shared.py
from pathlib import Path
import pandas as pd
app_dir = Path(__file__).parent
mtcars = pd.read_csv(app_dir / "mtcars.csv")
set_patches_fn
render.data_frame.set_patches_fn(fn)
Decorator to set the function that updates a batch of cells in the data frame.
The default patches function calls the async ._patch_fn()
on each input patch and returns the updated patch values.
There are no checks made on the quantity of patches returned. The user can return more, less, or the same number of patches as the input patches. This allows for the app author to own more control over which columns are updated and how they are updated.
Parameters
fn:
PatchesFn
|PatchesFnSync
-
A function that accepts a kwarg
patches
and returns a list of (possibly updated) patches to apply to the data frame.
Examples
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 400
## file: app.py
from __future__ import annotations
from pathlib import Path
import pandas as pd
from shiny import App, reactive, render, ui
app_ui = ui.page_fillable(
{"class": "p-3"},
ui.markdown(
"""
#### Instructions:
* Run the app locally so that the edits to the underlying CSV file will persist.
* Edit the cells in the table.
#### Note:
The data frame will not be re-rendered as the result of `df()` has not updated.
Once the `df()` is invalidated, all local edits are forgotten, and the data frame will be re-rendered. However, since the edits were saved to the CSV file, the edits will persist between refreshes (when run locally).
"""
),
ui.card(
ui.output_data_frame("my_data_frame"),
),
)
here = Path(__file__).parent
def server(input, output, session):
mtcars_df = reactive.value(pd.read_csv(here / "mtcars.csv").iloc[:, range(4)])
# A copy of the data frame that will store all the edits
edited_df = reactive.value(None)
# Copy mtcars_df to edited_df when mtcars_df changes and on initial load
@reactive.effect
def _sync_mtcars_to_edited_df():
edited_df.set(mtcars_df())
@render.data_frame
def my_data_frame():
return render.DataGrid(
mtcars_df(),
editable=True,
)
# Save the edited values to the data source (ex: the CSV file)
@my_data_frame.set_patches_fn
def _(*, patches: list[render.CellPatch]) -> list[render.CellPatch]:
for patch in patches:
if patch["column_index"] in [0, 2]:
patch["value"] = float(patch["value"])
else:
patch["value"] = int(patch["value"])
# "Save to the database" by writing the edited data to a CSV file
df = edited_df().copy()
for patch in patches:
df.iloc[patch["row_index"], patch["column_index"]] = patch["value"]
edited_df.set(df)
df.to_csv(here / "mtcars.csv", index=False)
print("Saved the edited data to './mtcars.csv'")
return patches
app = App(app_ui, server)
## file: mtcars.csv
mpg,cyl,disp,hp
21.0,6,160.0,110
21.0,6,160.0,110
22.8,4,108.0,93
21.4,6,258.0,110
18.7,8,360.0,175
18.1,6,225.0,105
14.3,8,360.0,245
24.4,4,146.7,62
22.8,4,140.8,95
19.2,6,167.6,123
17.8,6,167.6,123
16.4,8,275.8,180
17.3,8,275.8,180
15.2,8,275.8,180
10.4,8,472.0,205
10.4,8,460.0,215
14.7,8,440.0,230
32.4,4,78.7,66
30.4,4,75.7,52
33.9,4,71.1,65
21.5,4,120.1,97
15.5,8,318.0,150
15.2,8,304.0,150
13.3,8,350.0,245
19.2,8,400.0,175
27.3,4,79.0,66
26.0,4,120.3,91
30.4,4,95.1,113
15.8,8,351.0,264
19.7,6,145.0,175
15.0,8,301.0,335
21.4,4,121.0,109
update_cell_selection
render.data_frame.update_cell_selection(selection)
Update the cell selection in the data frame.
Currently only single ("type": "row"
) or multiple ("type": "rows"
) row selection is supported.
If the current data frame selection mode is "none"
and a non-none selection is provided, a warning will be raised and no rows will be selected. If cells are supposes to be selected, the selection mode returned from the render function must (currently) be set to "row"
or "rows"
.
Parameters
selection:
CellSelection
| Literal[‘all’] | None |BrowserCellSelection
-
The cell selection to apply to the data frame. This can be a
CellSelection
object,"all"
to select all cells (if possible), orNone
to clear the selection.
update_filter
render.data_frame.update_filter(filter)
Update the column filtering in the data frame.
Parameters
filter:
ListOrTuple
[ColumnFilter
] | None-
A list of column filtering information. If
None
, filtering will be removed.
Examples
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 400
## file: app.py
from shared import mtcars
from shiny import App, reactive, render, ui
app_ui = ui.page_fillable(
ui.card(
ui.layout_column_wrap(
ui.input_action_button("btn", "Filter on columns 0, 1, and 3"),
ui.input_action_button("reset", "Reset column filters"),
fill=False,
),
ui.output_data_frame("df"),
),
)
def server(input, output, session):
data = reactive.value(mtcars.iloc[:, range(4)])
@render.data_frame
def df():
return render.DataGrid(data(), filters=True)
@reactive.effect
@reactive.event(input.reset)
async def _():
await df.update_filter(None)
@reactive.effect
@reactive.event(input.btn)
async def _():
await df.update_filter(
[
{"col": 0, "value": [19, 25]},
{"col": 1, "value": [None, 6]},
{"col": 3, "value": [100, None]},
]
)
app = App(app_ui, server, debug=True)
## file: mtcars.csv
mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb
21,6,160,110,3.9,2.62,16.46,0,1,4,4
21,6,160,110,3.9,2.875,17.02,0,1,4,4
22.8,4,108,93,3.85,2.32,18.61,1,1,4,1
21.4,6,258,110,3.08,3.215,19.44,1,0,3,1
18.7,8,360,175,3.15,3.44,17.02,0,0,3,2
18.1,6,225,105,2.76,3.46,20.22,1,0,3,1
14.3,8,360,245,3.21,3.57,15.84,0,0,3,4
24.4,4,146.7,62,3.69,3.19,20,1,0,4,2
22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2
19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4
17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4
16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3
17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3
15.2,8,275.8,180,3.07,3.78,18,0,0,3,3
10.4,8,472,205,2.93,5.25,17.98,0,0,3,4
10.4,8,460,215,3,5.424,17.82,0,0,3,4
14.7,8,440,230,3.23,5.345,17.42,0,0,3,4
32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1
30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2
33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1
21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1
15.5,8,318,150,2.76,3.52,16.87,0,0,3,2
15.2,8,304,150,3.15,3.435,17.3,0,0,3,2
13.3,8,350,245,3.73,3.84,15.41,0,0,3,4
19.2,8,400,175,3.08,3.845,17.05,0,0,3,2
27.3,4,79,66,4.08,1.935,18.9,1,1,4,1
26,4,120.3,91,4.43,2.14,16.7,0,1,5,2
30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2
15.8,8,351,264,4.22,3.17,14.5,0,1,5,4
19.7,6,145,175,3.62,2.77,15.5,0,1,5,6
15,8,301,335,3.54,3.57,14.6,0,1,5,8
21.4,4,121,109,4.11,2.78,18.6,1,1,4,2
## file: shared.py
from pathlib import Path
import pandas as pd
app_dir = Path(__file__).parent
mtcars = pd.read_csv(app_dir / "mtcars.csv")
update_sort
render.data_frame.update_sort(sort)
Update the column sorting in the data frame.
The sort will be applied in reverse order so that the first value has the highest precedence. This mean ties will go to the second sort column (and so on).
Parameters
Examples
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 400
## file: app.py
from shared import mtcars
from shiny import App, reactive, render, ui
app_ui = ui.page_fillable(
ui.card(
ui.layout_column_wrap(
ui.input_action_button("btn", "Sort on columns 1↑ and 3↓"),
ui.input_action_button("reset", "Reset sorting"),
fill=False,
),
ui.output_data_frame("df"),
),
)
def server(input, output, session):
data = reactive.value(mtcars.iloc[:, range(4)])
@render.data_frame
def df():
return render.DataGrid(data())
@reactive.effect
@reactive.event(input.reset)
async def _():
await df.update_sort(None)
@reactive.effect
@reactive.event(input.btn)
async def _():
await df.update_sort([{"col": 1, "desc": False}, {"col": 3, "desc": True}])
app = App(app_ui, server, debug=True)
## file: mtcars.csv
mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb
21,6,160,110,3.9,2.62,16.46,0,1,4,4
21,6,160,110,3.9,2.875,17.02,0,1,4,4
22.8,4,108,93,3.85,2.32,18.61,1,1,4,1
21.4,6,258,110,3.08,3.215,19.44,1,0,3,1
18.7,8,360,175,3.15,3.44,17.02,0,0,3,2
18.1,6,225,105,2.76,3.46,20.22,1,0,3,1
14.3,8,360,245,3.21,3.57,15.84,0,0,3,4
24.4,4,146.7,62,3.69,3.19,20,1,0,4,2
22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2
19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4
17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4
16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3
17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3
15.2,8,275.8,180,3.07,3.78,18,0,0,3,3
10.4,8,472,205,2.93,5.25,17.98,0,0,3,4
10.4,8,460,215,3,5.424,17.82,0,0,3,4
14.7,8,440,230,3.23,5.345,17.42,0,0,3,4
32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1
30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2
33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1
21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1
15.5,8,318,150,2.76,3.52,16.87,0,0,3,2
15.2,8,304,150,3.15,3.435,17.3,0,0,3,2
13.3,8,350,245,3.73,3.84,15.41,0,0,3,4
19.2,8,400,175,3.08,3.845,17.05,0,0,3,2
27.3,4,79,66,4.08,1.935,18.9,1,1,4,1
26,4,120.3,91,4.43,2.14,16.7,0,1,5,2
30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2
15.8,8,351,264,4.22,3.17,14.5,0,1,5,4
19.7,6,145,175,3.62,2.77,15.5,0,1,5,6
15,8,301,335,3.54,3.57,14.6,0,1,5,8
21.4,4,121,109,4.11,2.78,18.6,1,1,4,2
## file: shared.py
from pathlib import Path
import pandas as pd
app_dir = Path(__file__).parent
mtcars = pd.read_csv(app_dir / "mtcars.csv")