Как отфильтровать таблицу, щелкнув сегмент гистограммы?

Я создал панель мониторинга RShiny с помощьюplotly-r, используя набор данных пингвинов Палмера, чтобы, когда я нажимаю на сегмент гистограммы, она использовала эти данные о событиях для фильтрации набора данных.

При наведении:

По клику:

Я хотел создать аналогичную панель мониторинга с использованием Shiny для Python, но не смог заставить customdata работать так же, как в R, и не могу понять, как обрабатывать событие щелчка.

Мне удалось создать объект customdata для сбора нужных категориальных данных для каждого сегмента панели, но мне не удалось найти функцию event_click() вplotly для Python , аналогичную функцииplotly-r .

Может кто-нибудь, пожалуйста, скажите мне, как я могу заставить эту функцию работать? В настоящее время этот код запускает панель управления Shiny для Python, но не реагирует ни на какие события щелчка где-либо на гистограмме.

Я думаю, мне, вероятно, понадобится помощь в части @render_widget функции сервера. Вот мой Исходный код Python :

# Load data and compute static values
from shiny import App, reactive, render, ui
from shinywidgets import output_widget, render_widget, render_plotly
from plotnine import ggplot, aes, geom_bar
import plotly.graph_objects as go
import palmerpenguins
from plotly.callbacks import Points, InputDeviceState
points, state = Points(), InputDeviceState()


df_penguins = palmerpenguins.load_penguins()

dict_category = {'species':'Species','island':'Island','sex':'Gender'}

def filter_shelf():
    return ui.card(
        ui.card_header(
            "Filters", 
            align = "center",
        ),
        # Gender Filter
        ui.input_checkbox_group(
            'sex_filter', 
            label='Gender', 
            choices = {value:(value.capitalize() if (type(value)==str) else value) for value in df_penguins['sex'].unique()},
            selected=list(df_penguins['sex'].unique()),
        ),

        # Species Filter
        ui.input_checkbox_group(
            'species_filter', 
            label='Species', 
            choices=list(df_penguins['species'].unique()),
            selected=list(df_penguins['species'].unique()),
        ),

        # Island Filter
        ui.input_checkbox_group(
            'island_filter', 
            label='Island', 
            choices=list(df_penguins['island'].unique()),
            selected=list(df_penguins['island'].unique()),
        ),
    )

def parameter_shelf():
    return ui.card(
        ui.card_header(
            'Parameters',
            align='center',
        ),

        # Category Selector
        ui.input_radio_buttons(
            'category', 
            label = 'View Penguins by:', 
            choices=dict_category,
            selected = 'species',
        ),
    ),

app_ui = ui.page_fluid(
    ui.h1("Palmer Penguins Analysis"),
    ui.layout_sidebar(
        # Left Sidebar
        ui.sidebar(
            filter_shelf(),
            parameter_shelf(),
            width=250,
        ),
        
        # Main Panel
        ui.card( # Plot
            ui.card_header(ui.output_text('chart_title')),
            output_widget('penguin_plot'),
        ),
        ui.card( # Table
            ui.card_header(ui.output_text('total_rows')),
            ui.column(
                12, #width
                ui.output_table('table_view'),
                style = "height:300px; overflow-y: scroll"
            )
        )
    ),
)

def server (input, output, session):
    
    @reactive.calc
    def category():
        '''This function caches the appropriate Capitalized form of the selected category'''
        return dict_category[input.category()]

    # Dynamic Chart Title
    @render.text
    def chart_title():
        return "Number of Palmer Penguins by Year, colored by "+category()


    @reactive.calc
    def df_filtered_stage1():
        '''This function caches the filtered datframe based on selections in the view'''
        return df_penguins[
            (df_penguins['species'].isin(input.species_filter())) &
            (df_penguins['island'].isin(input.island_filter())) &
            (df_penguins['sex'].isin(input.sex_filter()))]

    @reactive.calc
    def df_filtered_stage2():
        df_filtered_st2 = df_filtered_stage1() 
        # Eventually add additional filters on dataset from segments selected on the visual
        return df_filtered_st2 
    
    @reactive.calc
    def df_summarized():
        return df_filtered_stage2().groupby(['year',input.category()], as_index=False).count().rename({'body_mass_g':"count"},axis=1)[['year',input.category(),'count']]

    @reactive.calc
    def filter_fn():
        print("Clicked!") # This never gets called
        
    @render_widget
    def penguin_plot():
        df_plot = df_summarized()
        bar_columns = list(df_plot['year'].unique()) # x axis column labels
        bar_segments = list(df_plot[input.category()].unique()) # bar segment category labels
        data = [go.Bar(name=segment, x=bar_columns,y=list(df_plot[df_plot[input.category()]==segment]['count'].values), customdata=[input.category()], customdatasrc='A') for segment in bar_segments]
        fig = go.Figure(data)
        fig.update_layout(barmode = "stack")
        fig = go.FigureWidget(fig)

        ##### TRYING TO CAPTURE CLICK EVENT ON A BAR SEGMENT HERE #####
        fig.data[0].on_click(
            filter_fn
            )
        return fig
    
    @render.text
    def total_rows():
        return "Total Rows: "+str(df_filtered_stage1().shape[0])

    @render.table
    def table_view():
        df_this=df_summarized()
        return df_filtered_stage1()

app = App(app_ui, server)

Похоже, я могу фиксировать события кликов только по трассировке. Мне интересно, есть ли лучший способ, чем то, что я делаю выше, потому что fig.data имеет 3 трассировки столбцов во время выполнения при просмотре с помощью «видов» (Gentoo, Chinstrap и Adelie), и кажется, что каждая полоска трассировки - это то, что получает метод on_click().

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
0
111
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Ниже приведен вариант, в котором я изменил несколько деталей, вот самые важные:

  • В вашем примере filter_fn никогда не вызывается, поскольку он не зависит ни от какого reactive выражения. Shiny нет необходимости звонить.

    Вы можете сделать это следующим образом: мы определяем reactive.value под названием filter, который содержит информацию фильтра из события on_click (filter = reactive.value({})) и функцию

    def setClickedFilterValues(trace, points, selector):
      if not points.point_inds:
          return
      filter.set({"Year": points.xs, "Category": points.trace_name})
    

    которое установлено как событие on_click на каждой трассировке:

    for trace in fig.data:
      trace.on_click(setClickedFilterValues)
    

    Предложение if в функции проверяет, находитесь ли вы на следе щелчка, если нет, остановитесь. filter тогда содержит правильные значения. Важным моментом здесь является то, что функция не получает декоратор reactive, такой как @reactive.calc. В этом нет необходимости, поскольку мы только обновляем значение.

  • Я изменил df_filtered_stage2(), чтобы учесть filter, это вычисляет фрейм данных для выходных данных, отображаемых ниже в приложении.

    @reactive.calc
    def df_filtered_stage2():
      if filter.get() == {}:
          return df_filtered_stage1()
      df_filtered_st2 = df_filtered_stage1()[
          (df_filtered_stage1()['year'].isin(filter.get()['Year'])) &
          (df_filtered_stage1()[input.category()].isin([filter.get()['Category']]))
      ] 
      return df_filtered_st2 
    
  • Как и выше, в вашем приложении R можно реализовать событие on_hover, это описано ниже.

Это выглядит так:

# Load data and compute static values
from shiny import App, reactive, render, ui
from shinywidgets import output_widget, render_widget, render_plotly
from plotnine import ggplot, aes, geom_bar
import plotly.graph_objects as go
import palmerpenguins
from plotly.callbacks import Points, InputDeviceState
points, state = Points(), InputDeviceState()


df_penguins = palmerpenguins.load_penguins()

dict_category = {'species':'Species','island':'Island','sex':'Gender'}

def filter_shelf():
    return ui.card(
        ui.card_header(
            "Filters", 
            align = "center",
        ),
        # Gender Filter
        ui.input_checkbox_group(
            'sex_filter', 
            label='Gender', 
            choices = {value:(value.capitalize() if (type(value)==str) else value) for value in df_penguins['sex'].unique()},
            selected=list(df_penguins['sex'].unique()),
        ),

        # Species Filter
        ui.input_checkbox_group(
            'species_filter', 
            label='Species', 
            choices=list(df_penguins['species'].unique()),
            selected=list(df_penguins['species'].unique()),
        ),

        # Island Filter
        ui.input_checkbox_group(
            'island_filter', 
            label='Island', 
            choices=list(df_penguins['island'].unique()),
            selected=list(df_penguins['island'].unique()),
        ),
    )

def parameter_shelf():
    return ui.card(
        ui.card_header(
            'Parameters',
            align='center',
        ),

        # Category Selector
        ui.input_radio_buttons(
            'category', 
            label = 'View Penguins by:', 
            choices=dict_category,
            selected = 'species',
        ),
    ),

app_ui = ui.page_fluid(
    ui.h1("Palmer Penguins Analysis"),
    ui.layout_sidebar(
        # Left Sidebar
        ui.sidebar(
            filter_shelf(),
            parameter_shelf(),
            width=250,
        ),
        
        # Main Panel
        ui.card( # Plot
            ui.card_header(ui.output_text('chart_title')),
            output_widget('penguin_plot'),
        ),
        ui.output_text_verbatim('hoverInfoOutput'),
        ui.card( # Table
            ui.card_header(ui.output_text('total_rows')),
            ui.column(
                12, #width
                ui.output_table('table_view'),
                style = "height:300px; overflow-y: scroll"
            )
        )
    ),
)
   

def server (input, output, session):
    
    filter = reactive.value({})
    hoverInfo = reactive.value({})
    
    @reactive.calc
    def category():
        '''This function caches the appropriate Capitalized form of the selected category'''
        return dict_category[input.category()]

    # Dynamic Chart Title
    @render.text
    def chart_title():
        return "Number of Palmer Penguins by Year, colored by "+category()


    @reactive.calc
    def df_filtered_stage1():
        '''This function caches the filtered datframe based on selections in the view'''
        return df_penguins[
            (df_penguins['species'].isin(input.species_filter())) &
            (df_penguins['island'].isin(input.island_filter())) &
            (df_penguins['sex'].isin(input.sex_filter()))]

    @reactive.calc
    def df_filtered_stage2():
        if filter.get() == {}:
            return df_filtered_stage1()
        df_filtered_st2 = df_filtered_stage1()[
            (df_filtered_stage1()['year'].isin(filter.get()['Year'])) &
            (df_filtered_stage1()[input.category()].isin([filter.get()['Category']]))
        ] 
        return df_filtered_st2 
    
    @reactive.calc
    def df_summarized():
        return df_filtered_stage1().groupby(['year',input.category()], as_index=False).count().rename({'body_mass_g':"count"},axis=1)[['year',input.category(),'count']] 

    def setClickedFilterValues(trace, points, selector):
        if not points.point_inds:
            return
        filter.set({"Year": points.xs, "Category": points.trace_name})
        
    def setHoverValues(trace, points, selector):
        if not points.point_inds:
            return
        hoverInfo.set(points)
    
    @render_widget
    def penguin_plot():
        df_plot = df_summarized()
        bar_columns = list(df_plot['year'].unique()) # x axis column labels
        bar_segments = list(df_plot[input.category()].unique()) # bar segment category labels
        data = [go.Bar(name=segment, x=bar_columns,y=list(df_plot[df_plot[input.category()]==segment]['count'].values), customdata=[input.category()], customdatasrc='A') for segment in bar_segments]
        fig = go.Figure(data)
        fig.update_layout(barmode = "stack")
        fig = go.FigureWidget(fig)
        
        for trace in fig.data:
            trace.on_click(setClickedFilterValues)
            trace.on_hover(setHoverValues)
        
        return fig
    
    @render.text
    def hoverInfoOutput():
        return hoverInfo.get()
    
    @render.text
    def total_rows():
        return "Total Rows: "+str(df_filtered_stage2().shape[0])

    @render.table
    def table_view():
        #df_this=df_summarized()
        return df_filtered_stage2()

app = App(app_ui, server)

У вас все получилось, спасибо! Мне интересно, можете ли вы прояснить мой фундаментальный вопрос о нереактивной функции setClickedFilterValues(). Сначала я попробовал что-то подобное из документации по сюжету: plotly.com/python/click-events , но я не был уверен, как передать реактивность в Shiny. Я получал ошибки, связанные с функцией, которая принимает 3 аргумента (трассировка, точки, селектор), но ни один из них не был предоставлен. Почему ваша строка кода: трассировка.on_click(setClickedFilterValues) неявно знает об объектах трассировки, точках и селекторах?

Dave Guenther 07.07.2024 20:40

Что касается вашего дополнительного вопроса, я думаю, мне нужно более глубоко изучить исходный код, я сделаю это и скажу вам...

Jan 07.07.2024 20:51

Я ценю это! Кстати, даже когда я вытащил строки кода: изplotly.callbacks import Points, точки InputDeviceState, state = Points(), InputDeviceState(), метод трассировки.on_click() все еще, казалось, понимал точки и объекты селектора, даже несмотря на то, что когда Я поставил точку останова на этой строке, ни один из этих объектов не был определен ни в локальной, ни в глобальной области. Я подтвердил это, попытавшись получить к ним доступ в консоли отладки. Я полагаю, что трассировка — это просто экземпляр трассировки, передающий себя в setClickedFilterValues(trace,points,selector)?

Dave Guenther 07.07.2024 21:35

Да, скорее всего, так и будет, хотя на данный момент я не могу подтвердить это в исходном коде. Возможно, я посмотрю позже, но, например. здесь вы видите определение on_click, которое регистрирует только обратный вызов. Можно продолжить исследование того, как определяются _click_callbacks, но я думаю, что следы будут похожи на Точки.

Jan 07.07.2024 21:45

Другие вопросы по теме