Lab Week 6: Shiny Practice Exercises with Solutions
Part 1: Basic UI and Server Structure
Exercise 1.1: Hello World (Warm-up)
Task: Create a Shiny app that displays: - A heading “Welcome to My App” - A paragraph with your name - A horizontal line - Another paragraph with today’s date
Learning goal: Practice basic UI elements
Click to see solution
from shiny import App, ui
from datetime import date
app_ui = ui.page_fluid(
ui.h2("Welcome to My App"),
ui.p("My name is [Your Name]"),
ui.hr(),
ui.p(f"Today's date: {date.today()}")
)
def server(input, output, session):
pass
app = App(app_ui, server)Exercise 1.2: Echo Your Name
Task: Create an app that: - Has a text input asking “What’s your name?” - Shows output text that says “Hello, [name]!”
Learning goal: Connect one input to one output
Click to see solution
from shiny import App, ui, render
app_ui = ui.page_fluid(
ui.h2("Name Echo"),
ui.input_text("user_name", "What's your name?", ""),
ui.output_text("greeting")
)
def server(input, output, session):
@render.text
def greeting():
name = input.user_name()
if name == "":
return "Please enter your name above."
return f"Hello, {name}!"
app = App(app_ui, server)Common mistakes to avoid: - Forgetting () after input.user_name - Function name greeting must match output ID - Need to import render
Exercise 1.3: Age Calculator
Task: Create an app that: - Has a number slider from 0 to 100 asking for birth year (starting at 2000) - Shows text output calculating how old someone born that year would be in 2025
Learning goal: Use slider input and do simple calculations
Click to see solution
from shiny import App, ui, render
app_ui = ui.page_fluid(
ui.h2("Age Calculator"),
ui.input_slider("birth_year", "What year were you born?",
min=1920, max=2025, value=2000),
ui.output_text("age_text")
)
def server(input, output, session):
@render.text
def age_text():
birth_year = input.birth_year()
age = 2025 - birth_year
return f"You are approximately {age} years old in 2025."
app = App(app_ui, server)Exercise 1.4: Multiple Inputs
Task: Create an app that collects: - First name (text input) - Last name (text input) - City (dropdown: Auckland, Wellington, Christchurch)
Then displays: “Hello [First] [Last] from [City]!”
Learning goal: Multiple inputs, one output
Click to see solution
from shiny import App, ui, render
app_ui = ui.page_fluid(
ui.h2("User Profile"),
ui.input_text("first_name", "First name:", ""),
ui.input_text("last_name", "Last name:", ""),
ui.input_select("city", "City:",
choices=["Auckland", "Wellington", "Christchurch"]),
ui.hr(),
ui.output_text("full_greeting")
)
def server(input, output, session):
@render.text
def full_greeting():
first = input.first_name()
last = input.last_name()
city = input.city()
if first == "" or last == "":
return "Please enter your name."
return f"Hello {first} {last} from {city}!"
app = App(app_ui, server)Exercise 1.5: Simple Data Table
Task: Create an app that: - Has a text input for entering a suburb name - Has a number slider for population - Shows a pandas DataFrame table with these two values
Learning goal: Create table outputs
Click to see solution
from shiny import App, ui, render
import pandas as pd
app_ui = ui.page_fluid(
ui.h2("Suburb Data Entry"),
ui.input_text("suburb", "Suburb name:", ""),
ui.input_slider("population", "Population:",
min=0, max=50000, value=10000, step=1000),
ui.hr(),
ui.output_table("data_table")
)
def server(input, output, session):
@render.table
def data_table():
df = pd.DataFrame({
'Suburb': [input.suburb()],
'Population': [input.population()]
})
return df
app = App(app_ui, server)Part 2: Reactivity Basics
Exercise 2.1: Temperature Converter
Task: Create a temperature converter that: - Has a slider for Celsius (0-100) - Shows Fahrenheit in text output - Shows Kelvin in separate text output - Formula: F = C × 9/5 + 32, K = C + 273.15
Learning goal: One input, multiple outputs
Click to see solution
from shiny import App, ui, render
app_ui = ui.page_fluid(
ui.h2("Temperature Converter"),
ui.input_slider("celsius", "Temperature (°C):",
min=0, max=100, value=20),
ui.hr(),
ui.output_text("fahrenheit"),
ui.output_text("kelvin")
)
def server(input, output, session):
@render.text
def fahrenheit():
c = input.celsius()
f = c * 9/5 + 32
return f"{c}°C = {f:.1f}°F"
@render.text
def kelvin():
c = input.celsius()
k = c + 273.15
return f"{c}°C = {k:.2f}K"
app = App(app_ui, server)Exercise 2.2: Using Reactive Calculations
Task: Create a BMI calculator that: - Has slider for height (cm, 140-220) - Has slider for weight (kg, 40-150) - Uses @reactive.calc to calculate BMI once - Shows BMI value in text - Shows BMI category in separate text (underweight <18.5, normal 18.5-25, overweight >25)
Learning goal: Use @reactive.calc to avoid repeated calculations
Click to see solution
from shiny import App, ui, render, reactive
app_ui = ui.page_fluid(
ui.h2("BMI Calculator"),
ui.input_slider("height", "Height (cm):",
min=140, max=220, value=170),
ui.input_slider("weight", "Weight (kg):",
min=40, max=150, value=70),
ui.hr(),
ui.output_text("bmi_value"),
ui.output_text("bmi_category")
)
def server(input, output, session):
# Reactive calculation - calculates BMI once
@reactive.calc
def calculate_bmi():
height_m = input.height() / 100 # Convert cm to m
weight = input.weight()
bmi = weight / (height_m ** 2)
return bmi
@render.text
def bmi_value():
bmi = calculate_bmi() # Use the reactive calc
return f"Your BMI: {bmi:.1f}"
@render.text
def bmi_category():
bmi = calculate_bmi() # Use the same reactive calc
if bmi < 18.5:
category = "Underweight"
elif bmi < 25:
category = "Normal weight"
else:
category = "Overweight"
return f"Category: {category}"
app = App(app_ui, server)Why use @reactive.calc? Without it, BMI would be calculated twice (once in each output). With it, it’s calculated once and shared.
Exercise 2.3: Filtering Data
Task: Create a suburb filter that: - Creates a small DataFrame with suburb data: python suburbs = pd.DataFrame({ 'name': ['Ponsonby', 'Parnell', 'Mt Eden', 'Newmarket', 'Grey Lynn'], 'population': [12500, 8900, 15300, 11200, 14800] }) - Has slider for minimum population - Uses @reactive.calc to filter suburbs - Shows count of matching suburbs - Shows table of matching suburbs
Learning goal: Filter data reactively
Click to see solution
from shiny import App, ui, render, reactive
import pandas as pd
# Load data once
suburbs = pd.DataFrame({
'name': ['Ponsonby', 'Parnell', 'Mt Eden', 'Newmarket', 'Grey Lynn'],
'population': [12500, 8900, 15300, 11200, 14800]
})
app_ui = ui.page_fluid(
ui.h2("Auckland Suburbs Filter"),
ui.input_slider("min_pop", "Minimum population:",
min=0, max=20000, value=0, step=2000),
ui.hr(),
ui.output_text("count"),
ui.output_table("filtered_table")
)
def server(input, output, session):
# Reactive: filter the data
@reactive.calc
def filtered_suburbs():
min_population = input.min_pop()
return suburbs[suburbs['population'] >= min_population]
@render.text
def count():
data = filtered_suburbs()
return f"Found {len(data)} suburbs"
@render.table
def filtered_table():
return filtered_suburbs()
app = App(app_ui, server)Exercise 2.4: Simple Bar Chart
Task: Extend Exercise 2.3 to add a bar chart showing the populations of filtered suburbs.
Learning goal: Create plot outputs with matplotlib
Click to see solution
from shiny import App, ui, render, reactive
import pandas as pd
import matplotlib.pyplot as plt
suburbs = pd.DataFrame({
'name': ['Ponsonby', 'Parnell', 'Mt Eden', 'Newmarket', 'Grey Lynn'],
'population': [12500, 8900, 15300, 11200, 14800]
})
app_ui = ui.page_fluid(
ui.h2("Auckland Suburbs Dashboard"),
ui.input_slider("min_pop", "Minimum population:",
min=0, max=20000, value=0, step=2000),
ui.hr(),
ui.output_text("count"),
ui.output_table("filtered_table"),
ui.output_plot("pop_chart")
)
def server(input, output, session):
@reactive.calc
def filtered_suburbs():
min_population = input.min_pop()
return suburbs[suburbs['population'] >= min_population]
@render.text
def count():
data = filtered_suburbs()
return f"Found {len(data)} suburbs"
@render.table
def filtered_table():
return filtered_suburbs()
@render.plot
def pop_chart():
data = filtered_suburbs()
# Handle empty data
if len(data) == 0:
fig, ax = plt.subplots(figsize=(8, 4))
ax.text(0.5, 0.5, 'No suburbs to display',
ha='center', va='center', fontsize=14)
ax.axis('off')
return fig
# Create bar chart
fig, ax = plt.subplots(figsize=(8, 5))
ax.barh(data['name'], data['population'], color='steelblue')
ax.set_xlabel('Population')
ax.set_title('Population by Suburb')
plt.tight_layout()
return fig
app = App(app_ui, server)Part 3: Working with Spatial Data
Exercise 3.1: Simple Map (Setup)
Task: Create a basic GeoDataFrame and display it as a map: - Create sample polygon data: ```python from shapely.geometry import box import geopandas as gpd
gdf = gpd.GeoDataFrame({ ‘name’: [‘Area A’, ‘Area B’, ‘Area C’], ‘value’: [10, 20, 15], ‘geometry’: [ box(0, 0, 1, 1), box(1, 0, 2, 1), box(0, 1, 1, 2) ] }) ``` - Show a simple choropleth map colored by ‘value’
Learning goal: Display basic spatial data
Click to see solution
from shiny import App, ui, render
import geopandas as gpd
from shapely.geometry import box
import matplotlib.pyplot as plt
# Create sample spatial data
gdf = gpd.GeoDataFrame({
'name': ['Area A', 'Area B', 'Area C'],
'value': [10, 20, 15],
'geometry': [
box(0, 0, 1, 1),
box(1, 0, 2, 1),
box(0, 1, 1, 2)
]
})
app_ui = ui.page_fluid(
ui.h2("Simple Spatial Data"),
ui.output_plot("simple_map")
)
def server(input, output, session):
@render.plot
def simple_map():
fig, ax = plt.subplots(figsize=(8, 6))
gdf.plot(column='value', ax=ax, legend=True,
cmap='YlOrRd', edgecolor='black')
ax.set_title('Areas by Value')
return fig
app = App(app_ui, server)Exercise 3.2: Interactive Spatial Filter
Task: Building on Exercise 3.1: - Add a slider to filter areas by minimum value - Use @reactive.calc to filter the GeoDataFrame - Show count of areas - Show filtered map
Learning goal: Filter spatial data reactively
Click to see solution
from shiny import App, ui, render, reactive
import geopandas as gpd
from shapely.geometry import box
import matplotlib.pyplot as plt
# Sample spatial data
gdf = gpd.GeoDataFrame({
'name': ['Area A', 'Area B', 'Area C', 'Area D', 'Area E'],
'value': [10, 20, 15, 5, 25],
'geometry': [
box(0, 0, 1, 1),
box(1, 0, 2, 1),
box(0, 1, 1, 2),
box(1, 1, 2, 2),
box(2, 0, 3, 1)
]
})
app_ui = ui.page_fluid(
ui.h2("Interactive Spatial Filter"),
ui.input_slider("min_value", "Minimum value:",
min=0, max=30, value=0, step=5),
ui.hr(),
ui.output_text("area_count"),
ui.output_plot("filtered_map")
)
def server(input, output, session):
@reactive.calc
def filtered_areas():
min_val = input.min_value()
return gdf[gdf['value'] >= min_val]
@render.text
def area_count():
data = filtered_areas()
return f"Showing {len(data)} of {len(gdf)} areas"
@render.plot
def filtered_map():
data = filtered_areas()
if len(data) == 0:
fig, ax = plt.subplots(figsize=(8, 6))
ax.text(0.5, 0.5, 'No areas match filter',
ha='center', va='center', fontsize=14)
ax.axis('off')
return fig
fig, ax = plt.subplots(figsize=(8, 6))
data.plot(column='value', ax=ax, legend=True,
cmap='YlOrRd', edgecolor='black')
ax.set_title(f'Filtered Areas (n={len(data)})')
return fig
app = App(app_ui, server)Part 4: Challenge Exercises
Challenge 4.1: Multi-Filter Dashboard
Task: Create a comprehensive dashboard with: - Data with 3 columns: name, population, income - Two sliders: minimum population, minimum income - Checkbox: “Sort by population” (if checked, sort descending) - Outputs: count text, filtered table, bar chart of population
Learning goal: Combine multiple filters and conditional logic
Click to see solution
from shiny import App, ui, render, reactive
import pandas as pd
import matplotlib.pyplot as plt
# Sample data
data = pd.DataFrame({
'name': ['Ponsonby', 'Parnell', 'Mt Eden', 'Newmarket', 'Grey Lynn', 'Remuera'],
'population': [12500, 8900, 15300, 11200, 14800, 9600],
'income': [75000, 95000, 68000, 82000, 71000, 105000]
})
app_ui = ui.page_fluid(
ui.h2("Multi-Filter Suburb Dashboard"),
ui.layout_sidebar(
ui.sidebar(
ui.h4("Filters"),
ui.input_slider("min_pop", "Min Population:",
min=0, max=20000, value=0, step=2000),
ui.input_slider("min_income", "Min Income ($):",
min=60000, max=110000, value=60000, step=5000),
ui.input_checkbox("sort_pop", "Sort by population", value=False)
),
ui.h4("Results"),
ui.output_text("summary"),
ui.output_table("data_table"),
ui.output_plot("pop_chart")
)
)
def server(input, output, session):
@reactive.calc
def filtered_data():
# Apply both filters
result = data.copy()
result = result[result['population'] >= input.min_pop()]
result = result[result['income'] >= input.min_income()]
# Conditional sorting
if input.sort_pop():
result = result.sort_values('population', ascending=False)
return result
@render.text
def summary():
df = filtered_data()
if len(df) == 0:
return "No suburbs match your criteria."
avg_pop = df['population'].mean()
avg_income = df['income'].mean()
return f"Found {len(df)} suburbs | Avg Pop: {avg_pop:,.0f} | Avg Income: ${avg_income:,.0f}"
@render.table
def data_table():
return filtered_data()
@render.plot
def pop_chart():
df = filtered_data()
if len(df) == 0:
fig, ax = plt.subplots(figsize=(8, 4))
ax.text(0.5, 0.5, 'No data to display',
ha='center', va='center', fontsize=12)
ax.axis('off')
return fig
fig, ax = plt.subplots(figsize=(8, 5))
ax.barh(df['name'], df['population'], color='steelblue')
ax.set_xlabel('Population')
ax.set_title('Population by Suburb')
plt.tight_layout()
return fig
app = App(app_ui, server)Challenge 4.2: Dynamic Suburb Selector
Task: Create an app where: - Dropdown menu lets user select ONE suburb - Shows detailed information about that suburb in a formatted text display - Shows a small map highlighting just that suburb - Use the GeoDataFrame approach
Learning goal: Select and display individual features
Click to see solution
from shiny import App, ui, render, reactive
import geopandas as gpd
from shapely.geometry import box
import matplotlib.pyplot as plt
# Create spatial data with more details
gdf = gpd.GeoDataFrame({
'name': ['Ponsonby', 'Parnell', 'Mt Eden', 'Newmarket'],
'population': [12500, 8900, 15300, 11200],
'area_km2': [2.1, 1.8, 3.4, 2.7],
'geometry': [
box(0, 0, 2, 2),
box(2, 0, 4, 2),
box(0, 2, 2, 4),
box(2, 2, 4, 4)
]
})
# Calculate density
gdf['density'] = gdf['population'] / gdf['area_km2']
app_ui = ui.page_fluid(
ui.h2("Suburb Explorer"),
ui.input_select("suburb", "Select a suburb:",
choices=gdf['name'].tolist()),
ui.hr(),
ui.output_text("details"),
ui.output_plot("suburb_map")
)
def server(input, output, session):
@reactive.calc
def selected_suburb():
suburb_name = input.suburb()
return gdf[gdf['name'] == suburb_name]
@render.text
def details():
suburb = selected_suburb()
if len(suburb) == 0:
return "No suburb selected"
row = suburb.iloc[0]
return f"""
Suburb: {row['name']}
Population: {row['population']:,}
Area: {row['area_km2']:.1f} km²
Density: {row['density']:.0f} people/km²
"""
@render.plot
def suburb_map():
suburb = selected_suburb()
fig, ax = plt.subplots(figsize=(6, 6))
# Plot all suburbs in grey
gdf.plot(ax=ax, color='lightgrey', edgecolor='black', alpha=0.5)
# Highlight selected suburb
if len(suburb) > 0:
suburb.plot(ax=ax, color='steelblue', edgecolor='black')
ax.set_title(f'Location of {input.suburb()}')
ax.axis('off')
return fig
app = App(app_ui, server)Tips for Success
- Start simple: Get one input and one output working before adding complexity
- Test frequently: Run your app after each small change
- Check IDs: The most common error is mismatched IDs between UI and server
- Use (reactive.calc?): When the same calculation is used multiple times
- Load data once: Put data loading outside the server function
- Handle empty data: Always check if filtered data is empty before plotting
- Read error messages: They usually tell you exactly what’s wrong
Common Error Messages and Solutions
Error: NameError: name 'render' is not defined Solution: Add render to your imports: from shiny import App, ui, render
Error: AttributeError: 'function' object has no attribute 'value' Solution: You forgot () after the input. Use input.my_slider() not input.my_slider
Error: Output doesn’t update Solution: Check that the function name matches the output ID exactly
Error: KeyError in DataFrame Solution: Check column names match exactly (case-sensitive)
Next Steps
Once you’re comfortable with these exercises: 1. Try combining multiple concepts in one app 2. Work with your own data 3. Explore layout options (sidebars, tabs, columns) 4. Learn about more advanced reactive patterns 5. Add styling with CSS
Happy coding! 🚀