Exercises
Exercise 1 - Basic App
Open code/demos/app-01-intro.py file in the repository and run it.
Before doing the exercise make a copy of the file for your solution.
Try out any one of the following changes:
- Add a third input β a checkbox group to filter by species β and connect it to the plot
- Try
ui.input_slider()to filter by one of the numeric columns - Add a
@render.textoutput showing how many points are currently displayed
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 650
from palmerpenguins import load_penguins
from plotnine import aes, geom_point, ggplot, theme_minimal
from shiny.express import input, render, ui
dat = load_penguins().dropna()
num_cols = dat.select_dtypes("float64").columns.tolist()
species = dat["species"].unique().tolist() #<<
ui.input_select("x", "", num_cols, selected="bill_depth_mm")
ui.input_select("y", "", num_cols, selected="body_mass_g")
ui.input_checkbox_group("species", "Species", species, selected=species) #<<
@render.plot
def plot():
sel = dat[dat["species"].isin(input.species())] #<<
return (
ggplot(sel, aes(x=input.x(), y=input.y(), color="species")) #<<
+ geom_point(alpha=0.7)
+ theme_minimal()
)
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 650
from palmerpenguins import load_penguins
from plotnine import aes, geom_point, ggplot, theme_minimal
from shiny.express import input, render, ui
dat = load_penguins().dropna()
num_cols = dat.select_dtypes("float64").columns.tolist()
ui.input_select("x", "", num_cols, selected="bill_depth_mm")
ui.input_select("y", "", num_cols, selected="body_mass_g")
ui.input_slider( #<<
"body_mass", "Body Mass (g)", #<<
min=int(dat["body_mass_g"].min()), #<<
max=int(dat["body_mass_g"].max()), #<<
value=[int(dat["body_mass_g"].min()), int(dat["body_mass_g"].max())], #<<
) #<<
@render.plot
def plot():
sel = dat[ #<<
(dat["body_mass_g"] >= input.body_mass()[0]) #<<
& (dat["body_mass_g"] <= input.body_mass()[1]) #<<
] #<<
return (
ggplot(sel, aes(x=input.x(), y=input.y(), color="species")) #<<
+ geom_point(alpha=0.7)
+ theme_minimal()
)
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 650
from palmerpenguins import load_penguins
from plotnine import aes, geom_point, ggplot, theme_minimal
from shiny import reactive #<<
from shiny.express import input, render, ui
dat = load_penguins().dropna()
num_cols = dat.select_dtypes("float64").columns.tolist()
species = dat["species"].unique().tolist()
ui.input_select("x", "", num_cols, selected="bill_depth_mm")
ui.input_select("y", "", num_cols, selected="body_mass_g")
ui.input_checkbox_group("species", "Species", species, selected=species)
@reactive.calc #<<
def filtered(): #<<
return dat[dat["species"].isin(input.species())] #<<
@render.plot
def plot():
return (
ggplot(filtered(), aes(x=input.x(), y=input.y(), color="species")) #<<
+ geom_point(alpha=0.7)
+ theme_minimal()
)
@render.text #<<
def count(): #<<
return f"{len(filtered())} penguins selected" #<<
Exercise 2 - Reactivity
Part A β open code/demos/app-03-reactivity.py file in the repository.
Before doing the exercise make a copy of the file for your solution.
- Run it and confirm the scatter plot and count update together
- Add a
print("filtering...")insidefiltered()β change Species and verify it prints only once
Part B β open code/demos/app-04-reactivity_chained.py file in the repository.
Before doing the exercise make a copy of the file for your solution.
- Run it and explore all three outputs (plot, count, table)
- Change Y Axis β notice count does not change (filtered is still cached)
- Add a
printto each calc to confirm which ones re-run for each input change
Exercise 3 - Tips
Take a look at the code/exercise/app-ex03-tips.py file in the repository.
Before doing the exercise make a copy of the file for your solution.
- Replace the plotly scatter plot with the altair scatter plot below.
- Wrap the scatter plot in a function,
scatterplot(), - Wse the appropriate output component decorator,
@render_altair
data = ____ # use the reactive calc dataframe here
points = alt.Chart(data).mark_circle().encode(
x=alt.X("total_bill", title="Total bill"),
y=alt.Y("tip", title="Tip"),
tooltip=[
alt.Tooltip("total_bill:Q", title="Total bill", format=".2f"),
alt.Tooltip("tip:Q", title="Tip", format=".2f"),
],
)
trend = points.transform_regression(
"total_bill", "tip", method="linear"
).mark_line()
zoom = alt.selection_interval(bind="scales")
return (points + trend).add_params(zoom).properties(width="container")#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 650
import altair as alt
import seaborn as sns
from shiny.express import input, ui, render
from shiny import reactive
from shinywidgets import render_altair, render_widget
tips = sns.load_dataset("tips")
# title
ui.page_opts(title="Restaurant tipping", fillable=True)
# sidebar
with ui.sidebar(open="desktop"):
ui.input_slider(
id="slider",
label="Bill amount",
min=tips.total_bill.min(),
max=tips.total_bill.max(),
value=[tips.total_bill.min(), tips.total_bill.max()],
)
ui.input_checkbox_group(
id="checkbox_group",
label="Food service",
choices={
"Lunch": "Lunch",
"Dinner": "Dinner",
},
selected=[
"Lunch",
"Dinner",
],
)
@reactive.calc
def filtered_data():
idx1 = tips.total_bill.between(
left=input.slider()[0],
right=input.slider()[1],
inclusive="both",
)
idx2 = tips.time.isin(input.checkbox_group())
tips_filtered = tips[idx1 & idx2]
return tips_filtered
# body of application
# first row of value boxes
with ui.layout_columns(fill=False):
with ui.value_box():
"Total tippers"
@render.text
def total_tippers():
return filtered_data().shape[0]
with ui.value_box():
"Average tip"
@render.text
def average_tip():
perc = filtered_data().tip / filtered_data().total_bill
return f"{perc.mean():.1%}"
with ui.value_box():
"Average bill"
@render.text
def average_bill():
bill = filtered_data().total_bill.mean()
return f"${bill:.2f}"
# second row of cards
with ui.layout_columns(col_widths=[6, 6]):
with ui.card(full_screen=True):
ui.card_header("Tips data")
@render.data_frame
def tips_data():
return filtered_data()
with ui.card(full_screen=True):
ui.card_header("Total bill vs tip")
@render_altair
def scatterplot():
data = filtered_data()
points = alt.Chart(data).mark_circle().encode(
x=alt.X("total_bill", title="Total bill"),
y=alt.Y("tip", title="Tip"),
tooltip=[
alt.Tooltip("total_bill:Q", title="Total bill", format=".2f"),
alt.Tooltip("tip:Q", title="Tip", format=".2f"),
],
)
trend = points.transform_regression(
"total_bill", "tip", method="linear"
).mark_line()
zoom = alt.selection_interval(bind="scales")
return (points + trend).add_params(zoom).properties(width="container")
Exercise 4 - Tips + Button
Take a look at the code/exercises/app-ex03-tips.py file in the repository.
Before doing the exercise make a copy of the file for your solution.
- Add a
ui.input_action_button()component for the action button.- You can use the
"action_button"id.
- You can use the
- Create a
reset_filters()function that uses theui.update_slider()andui.update_checkbox_group()functions to reset the filter values to their default state.
def reset_filters():
ui.update_slider(
"slider",
value=[tips.total_bill.min(), tips.total_bill.max()],
)
ui.update_checkbox_group(
"checkbox_group",
selected=["Lunch", "Dinner"],
)- Decorate the function with a
@reactive.event(input.action_button)call. - Decorate the function again with a
@reactive.effectcall.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 650
import altair as alt
import seaborn as sns
from shiny.express import input, ui, render
from shiny import reactive
from shinywidgets import render_altair, render_widget
tips = sns.load_dataset("tips")
# title
ui.page_opts(title="Restaurant tipping", fillable=True)
# sidebar
with ui.sidebar(open="desktop"):
ui.input_slider(
id="slider",
label="Bill amount",
min=tips.total_bill.min(),
max=tips.total_bill.max(),
value=[tips.total_bill.min(), tips.total_bill.max()],
)
ui.input_checkbox_group(
id="checkbox_group",
label="Food service",
choices={
"Lunch": "Lunch",
"Dinner": "Dinner",
},
selected=[
"Lunch",
"Dinner",
],
)
ui.input_action_button("action_button", "Reset filter")
@reactive.effect
@reactive.event(input.action_button)
def reset_filters():
ui.update_slider(
"slider",
value=[tips.total_bill.min(), tips.total_bill.max()],
)
ui.update_checkbox_group(
"checkbox_group",
selected=["Lunch", "Dinner"],
)
@reactive.calc
def filtered_data():
idx1 = tips.total_bill.between(
left=input.slider()[0],
right=input.slider()[1],
inclusive="both",
)
idx2 = tips.time.isin(input.checkbox_group())
tips_filtered = tips[idx1 & idx2]
return tips_filtered
# body of application
# first row of value boxes
with ui.layout_columns(fill=False):
with ui.value_box():
"Total tippers"
@render.text
def total_tippers():
return filtered_data().shape[0]
with ui.value_box():
"Average tip"
@render.text
def average_tip():
perc = filtered_data().tip / filtered_data().total_bill
return f"{perc.mean():.1%}"
with ui.value_box():
"Average bill"
@render.text
def average_bill():
bill = filtered_data().total_bill.mean()
return f"${bill:.2f}"
# second row of cards
with ui.layout_columns(col_widths=[6, 6]):
with ui.card(full_screen=True):
ui.card_header("Tips data")
@render.data_frame
def tips_data():
return filtered_data()
with ui.card(full_screen=True):
ui.card_header("Total bill vs tip")
@render_altair
def scatterplot():
data = filtered_data()
points = alt.Chart(data).mark_circle().encode(
x=alt.X("total_bill", title="Total bill"),
y=alt.Y("tip", title="Tip"),
tooltip=[
alt.Tooltip("total_bill:Q", title="Total bill", format=".2f"),
alt.Tooltip("tip:Q", title="Tip", format=".2f"),
],
)
trend = points.transform_regression(
"total_bill", "tip", method="linear"
).mark_line()
zoom = alt.selection_interval(bind="scales")
return (points + trend).add_params(zoom).properties(width="container")
Exercise 5 - AI Dashboards
Letβs start from the solution in the previous exercise code/exercise_solutions/app-ex04-button.py. You can find the original code in the repository.
We are going to convert this app from having multiple filters into an AI Chatbot using querychat.
- Import the querychat library and create the
QueryChatobject using the existingtipsdataset
from querychat.express import QueryChat
qc = QueryChat(tips, "tips", client="github/gpt-4.1")- Replace all the sidebar UI elements with the filters (including button) with the querychat sidebar
qc.sidebar()- Finally, replace the filtered dataframe object with the filtered dataframe returned from querychat
qc.df()Will not work in shinylive, run locally.
import altair as alt
import seaborn as sns
from shiny.express import input, ui, render
from shiny import reactive
from shinywidgets import render_altair
from querychat.express import QueryChat
tips = sns.load_dataset("tips")
qc = QueryChat(tips, "tips", client="github/gpt-4.1")
# title
ui.page_opts(title="Restaurant tipping", fillable=True)
# sidebar
qc.sidebar()
@reactive.calc
def filtered_data():
return qc.df()
# body of application
# first row of value boxes
with ui.layout_columns(fill=False):
with ui.value_box():
"Total tippers"
@render.text
def total_tippers():
return filtered_data().shape[0]
with ui.value_box():
"Average tip"
@render.text
def average_tip():
perc = filtered_data().tip / filtered_data().total_bill
return f"{perc.mean():.1%}"
with ui.value_box():
"Average bill"
@render.text
def average_bill():
bill = filtered_data().total_bill.mean()
return f"${bill:.2f}"
# second row of cards
with ui.layout_columns(col_widths=[6, 6]):
with ui.card(full_screen=True):
ui.card_header("Tips data")
@render.data_frame
def tips_data():
return filtered_data()
with ui.card(full_screen=True):
ui.card_header("Total bill vs tip")
@render_altair
def scatterplot():
data = filtered_data()
points = alt.Chart(data).mark_circle().encode(
x=alt.X("total_bill", title="Total bill"),
y=alt.Y("tip", title="Tip"),
tooltip=[
alt.Tooltip("total_bill:Q", title="Total bill", format=".2f"),
alt.Tooltip("tip:Q", title="Tip", format=".2f"),
],
)
trend = points.transform_regression(
"total_bill", "tip", method="linear"
).mark_line()
zoom = alt.selection_interval(bind="scales")
return (points + trend).add_params(zoom).properties(width="container")