Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Output multiple ui elements from one function call #1795

Closed
HarrisonWismer opened this issue Dec 10, 2024 · 2 comments
Closed

Output multiple ui elements from one function call #1795

HarrisonWismer opened this issue Dec 10, 2024 · 2 comments

Comments

@HarrisonWismer
Copy link

HarrisonWismer commented Dec 10, 2024

Hi! This could be extremely obvious but I can't find anything that jumps out explicitly to me in the API reference. I have a single, computationally expensive function that does operations on a large dataframe and returns various slices from it at the end. It currently returns 3 values. At the moment I am calling the function 3 times to output 3 different @reactive_event widgets. I feel like the reactivity itself doesn't handle this but I am curious what the best way to avoid excessive computations is when a single function is returning multiple outputs when the output IDs have to be unique.

Sorry if this is super obvious (probably is :) )

@gadenbuie
Copy link
Collaborator

Are you using the same or different slices of the dataframe in each of the three outputs?

In general, I'd recommend replacing the expensive function with a reactive.calc. This is a reactive function that can use inputs or other reactive values and updates automatically when the inputs change.

If you move the expensive computation into the @reactive.calc, you'll only need to perform the dataframe computation once. Then, I'd likely move the faster operations of slicing up the dataframe as needed into the outputs.

Here's an example that I think mimics your problem. I have an expensive_calculation() function that takes at least 2 second to finish and it's being called in three places. In the live app you can see that it takes about 2*3 seconds for the outputs to update when you run the calculation.

Slow app
from shiny import App, reactive, render, ui
import time
import pandas as pd
import numpy as np

app_ui = ui.page_sidebar(
    ui.sidebar(
        ui.input_slider("n", "Size of Dataset (x1,000)", min = 0, max = 25, value = 10),
        ui.input_action_button("compute", "Compute"),
        ui.p("Click to run expensive computation"),
    ),
    ui.output_text("result1"),
    ui.output_text("result2"),
    ui.output_text("result3"),
)

def expensive_calculation(n = 1000):
    time.sleep(2)
    data = pd.DataFrame({
        'A': np.random.randn(n * 1000),
        'B': np.random.randn(n * 1000)
    })
    
    return {
        'mean_a': data['A'].mean(),
        'mean_b': data['B'].mean(),
        'total_rows': len(data)
    }

def server(input, output, session):
    @render.text
    @reactive.event(input.compute)
    def result1():
        results = expensive_calculation(input.n())
        return f"Mean of A: {results['mean_a']:.2f}"

    @render.text
    @reactive.event(input.compute)
    def result2():
        results = expensive_calculation(input.n())
        return f"Mean of B: {results['mean_b']:.2f}"

    @render.text
    @reactive.event(input.compute)
    def result3():
        results = expensive_calculation(input.n())
        return f"Total rows: {results['total_rows']}"

app = App(app_ui, server)

If we make the expensive_calculation() function a @reactive.calc (that can also use @reactive.event(input.compute) so that it only runs when you click the Compute button), we can still call expensive_calculation() inside the text outputs. But this time, Shiny performs the reactive calc one time and shares the same result with all three outputs. This reduces the wait time to just about 2 seconds and speeds up the app quite a bit

from shiny import App, reactive, render, ui
import time
import pandas as pd
import numpy as np

app_ui = ui.page_sidebar(
    ui.sidebar(
        ui.input_slider("n", "Size of Dataset (x1,000)", min=0, max=25, value=10),
        ui.input_action_button("compute", "Compute"),
        ui.p("Click to run expensive computation"),
    ),
    ui.output_text("result1"),
    ui.output_text("result2"),
    ui.output_text("result3"),
)


def server(input, output, session):
    @reactive.calc
    @reactive.event(input.compute)
    def expensive_calculation():
        n = input.n() * 1000
        time.sleep(2)
        data = pd.DataFrame({"A": np.random.randn(n), "B": np.random.randn(n)})

        return {
            "mean_a": data["A"].mean(),
            "mean_b": data["B"].mean(),
            "total_rows": len(data),
        }

    @render.text
    def result1():
        results = expensive_calculation()
        return f"Mean of A: {results['mean_a']:.2f}"

    @render.text
    def result2():
        results = expensive_calculation()
        return f"Mean of B: {results['mean_b']:.2f}"

    @render.text
    def result3():
        results = expensive_calculation()
        return f"Total rows: {results['total_rows']}"


app = App(app_ui, server)

@HarrisonWismer
Copy link
Author

This is perfect, thanks! The three things being returned aren't all slices from the same dataframe but a substantial amount of time is saved by the reactive calculation!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants