Skip to Content
Course content

147: Building a Customer Churn Dashboard

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've likely spent the last few lessons mastering ggplot2 and dplyr, but putting them into a live dashboard using Shiny is where things usually get messy. I see the same mistake constantly when engineers move from static scripts to interactive apps: they treat the server logic like a linear script.

Take a look at this snippet. I'm trying to build a simple churn dashboard where the user can filter the data by "Contract Type" to see how it affects the churn rate. It looks logically sound, right?

# BROKEN CODE
ui <- fluidPage(
  selectInput("contract", "Choose Contract:", choices = c("Month-to-month", "One year", "Two year")),
  plotOutput("churnPlot")
)

server <- function(input, output) {
  # Filtering the data based on the input
  filtered_data <- churn_df %>% filter(Contract == input$contract)
  
  output$churnPlot <- renderPlot({
    ggplot(filtered_data, aes(x = Tenure, fill = Churn)) + 
      geom_histogram()
  })
}

shinyApp(ui, server)

The Dashboard that Refuses to Update

If you run this, you'll notice something frustrating: you can change the dropdown menu all you want, but the plot never changes. It's stuck on whatever the default value was when the app first launched.

Here is why: in Shiny, the server function only runs once when the user session starts. In the broken code above, filtered_data is created exactly once at startup. Because it's a standard R variable and not a "reactive" object, the renderPlot function has no reason to think it needs to re-run when input$contract changes. It just keeps plotting that original, static filtered_data dataframe.

Wrapping Logic in Reactive Expressions

To fix this, we need to tell Shiny that filtered_data depends on an input. We do this using reactive(). This creates a reactive expression—essentially a piece of code that "listens" for changes to any inputs inside it.

# FIXED CODE
server <- function(input, output) {
  # Now this is a reactive expression
  filtered_data <- reactive({
    churn_df %>% filter(Contract == input$contract)
  })
  
  output$churnPlot <- renderPlot({
    # Note: we now call filtered_data() as a function!
    ggplot(filtered_data(), aes(x = Tenure, fill = Churn)) + 
      geom_histogram()
  })
}

The fix is subtle but critical. By wrapping the filter in reactive(), Shiny creates a dependency graph. When input$contract changes, Shiny marks filtered_data as "invalid." Then, because renderPlot calls filtered_data(), it also becomes invalid and triggers a re-render. I cannot stress this enough: if you are transforming data based on a user input, it must be in a reactive block.

Structuring the Churn Layout

A real churn dashboard isn't just one plot; it's a story. You want to show the "What" (the current churn rate), the "Who" (the segments churning), and the "Why" (the correlations). For this, I recommend using shinydashboard instead of the basic fluidPage. It gives you that professional sidebar and header layout that stakeholders expect.

When building your layout, keep your "Value Boxes" at the top. A quick glance at the total churn percentage is more valuable than digging through a histogram. Use valueBox() for the headline metrics and tabBox() to separate your "Customer Demographics" from your "Usage Patterns." This keeps the UI clean and prevents the user from feeling overwhelmed by a wall of charts.

Optimizing Heavy Data Calculations

Churn datasets can get massive. If you find your dashboard lagging every time a user toggles a filter, check if you're doing expensive calculations (like a complex logistic regression or a heavy join) inside your renderPlot.

Never put heavy data processing directly inside the render function. If you do, that calculation runs every single time the plot needs to redraw, even if the underlying data hasn't changed. Move that logic into its own reactive() expression. That way, Shiny caches the result and only re-calculates it when the specific input it depends on actually changes.




📋 Practical Task

Build a Multi-Segment Churn Analysis Tool

You have been provided with a dataset called customer_churn_data. Your task is to expand the existing basic dashboard to include a "Service Filter."

  • The Goal: Create a Shiny app that allows users to filter the churn visualization by both Contract Type and Internet Service (e.g., Fiber optic, DSL, No).
  • Requirement 1: Implement two selectInput filters in the UI.
  • Requirement 2: Create a single reactive() expression that handles the filtering for both inputs simultaneously.
  • Requirement 3: Use a ggplot2 bar chart to display the count of churned vs. non-churned customers based on the filtered data.
  • Constraint: Ensure the plot updates instantly when either dropdown is changed. If the filtered dataset is empty, the app should not crash; instead, ensure your plot handles empty data gracefully.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.