Search code examples
pythonpython-3.xpandaswidgetipywidgets

Nested Filtering Using Ipywidgets


I am trying to create a dynamic filtering with Ipywidgets. The word dynamic here refers to: if an option is chosen in one widget, it will affect the choices of the remaining widgets.

Here is a toy dataset for replication purposes.

toy_data = pd.DataFrame({"LETTER": ["A", "B", "A","B","A", "B", "A","B"],
               "PLANT": ["Orange", "Carrots", "Lemon","Potato","Pomelo","Yam","Lime","Radish"],
               "NUMBER": [1,2,3,4,5,6,7,8]})

Creating widgets:

letter_var = widgets.Dropdown(
    description="Letter: ",
    options=toy_data.LETTER.unique(),
    value="A",
)

def letter_filtering(change):
    clear_output(wait = True)
    letter = letter_var.value
    new_toy_data = toy_data[toy_data.LETTER == str(letter)]
    
    plant_var = widgets.Dropdown(description="Plant: ", options=new_toy_data.PLANT.unique(), value="Orange")
    
    return plant_var

the purpose of the letter_filtering function is to filter the choices in the wigets for plants. That is if the letter B has been chosen for letter_var, the choices in plant_var will only be limited to the letter B. but upon implementation,

widgets.HBox([letter_var,letter_filtering])

I am receiving a trait error.

TraitError: The 'children' trait of a HBox instance contains an Instance of a TypedTuple which expected a Widget, not the function 'letter_filtering'.

I think I'm lost on how to go about this.


Solution

  • I am not familiar with the widget mechanism, but you can achieve this via the "interact" functionality, which I think is also simpler:

    from ipywidgets import interact, fixed
    
    toy_data = pd.DataFrame({"LETTER": ["A", "B", "A","B","A", "B", "A","B"],
                   "PLANT": ["Orange", "Carrots", "Lemon","Potato","Pomelo","Yam","Lime","Radish"],
                   "NUMBER": [1,2,3,4,5,6,7,8]})
    
    def inner_fn(plant, df_in):
        df_ = df_in if plant == 'ALL' else df_in[df_in['PLANT'] == plant]
        return df_
    
    def outer_fn(letter):
        df_ = toy_data if letter == 'ALL' else toy_data[toy_data['LETTER'] == letter]
        plants = ['ALL'] + sorted(df_['PLANT'].unique())
        interact(inner_fn, plant=plants, df_in=fixed(df_))
    
    letters = ['ALL'] + sorted(toy_data['LETTER'].unique())
    interact(outer_fn, letter=letters)
    

    This will create a "plant" dropdown underneath the "letter" dropdown, like so:

    Starting point

    And when a letter is chosen, the list of plants will update, like so:

    updated plant list

    The little extra indentation of the second dropdown box is a tip-off that this is a nested mechanism and admittedly makes this technique a bit unsightly.