Я пытаюсь фильтровать данные, используя несколько раскрывающихся списков на информационной панели. Всего существует 5 вариантов раскрывающегося списка. Я хочу, чтобы первые три работали независимо, а последние два должны быть связаны в обе стороны с первыми тремя.
В частности, функции, которые я собираюсь реализовать:
Начальной отправной точкой всегда должно быть значение по умолчанию для всех значений.
Первые 3 опции (Год, Сезон и Месяц) должны действовать независимо. Например, к выводу можно добавить любую комбинацию из этих трех. Если выбран один элемент, выходные данные должны быть обновлены с использованием этих значений. Однако если элемент выбран из другого раскрывающегося списка, эти значения должны быть добавлены в выходные данные. Пример ниже в i).
Варианты 4–5 (временные и точные) должны быть связаны в обе стороны с первыми тремя опциями раскрывающегося списка (Год, Сезон и Месяц). Это должно быть обратимо или в обоих направлениях. Если выбран один из первых трех вариантов раскрывающегося списка, выходные данные таблицы должны быть обновлены этими значениями, а раскрывающиеся списки должны быть уменьшены, чтобы пользователь мог выбирать только из этих значений. Пример ниже в ii).
Привести конкретные примеры;
i) 2012 выбран из года в первом раскрывающемся списке. В выводе таблицы отображаются соответствующие значения. Пользователь должен иметь возможность выбирать любые последующие значения в раскрывающемся списке «Год» (функциональном). Однако, если пользователь хочет также увидеть значения Spr из второго раскрывающегося списка, эти данные следует добавить к выходным данным.
ii) Для 4–5 вариантов раскрывающегося списка, которые должны быть связаны с первыми 3, если в параметре temp выбраны «Горячий» и «Мягкий», а в «prec» выбран «Влажный», тогда раскрывающиеся списки в первых трех вариантах должны быть сокращены до: Год = 2013, 2015 г.; Сезон = Spring, Осень; Месяц = апрель, июнь, октябрь, декабрь.
import pandas as pd
from dash import Dash, dcc, html, Input, Output, dash_table
import dash_bootstrap_components as dbc
from itertools import cycle
import random
Year = cycle(['2012','2013','2014','2015'])
Season = cycle(['Win','Spr','Sum','Fall'])
Month = cycle(['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'])
temp_group = cycle(['Hot','Cold','Mild'])
prec_group = cycle(['Dry','Wet'])
df = pd.DataFrame(index = range(20))
df['option1'] = [next(Year) for count in range(df.shape[0])]
df['option2'] = [next(Season) for count in range(df.shape[0])]
df['option3'] = [next(Month) for count in range(df.shape[0])]
df['option4'] = [next(temp_group) for count in range(df.shape[0])]
df['option5'] = [next(prec_group) for count in range(df.shape[0])]
option1_list = sorted(df['option1'].unique().tolist())
option2_list = df['option2'].unique().tolist()
option3_list = df['option3'].unique().tolist()
option4_list = sorted(df['option4'].unique().tolist())
option5_list = sorted(df['option5'].unique().tolist())
app = Dash(__name__)
app.layout = html.Div([
dbc.Card(
dbc.CardBody([
dbc.Row([
dbc.Col([
html.P("Option 1"),
html.Div([
dcc.Dropdown(id='option1_dropdown',
options=option1_list,
value=[],
placeholder='All',
multi=True,
clearable=True),
],
style = {'width': '100%', 'display': 'inline-block'})
]),
dbc.Col([
html.P("Option 2"),
html.Div([
dcc.Dropdown(id='option2_dropdown',
options=option2_list,
value=[],
placeholder='All',
multi=True,
clearable=True),
],
style = {'width': '100%', 'display': 'inline-block'})
]),
dbc.Col([
html.P("Option 3"),
html.Div([
dcc.Dropdown(id='option3_dropdown',
options=option3_list,
value=[],
placeholder='All',
multi=True,
clearable=True),
],
style = {'width': '100%', 'display': 'inline-block'})
]),
dbc.Col([
html.P("Option 4"),
html.Div([
dcc.Dropdown(id='option4_dropdown',
options=option4_list,
value=[],
placeholder='All',
multi=True,
clearable=True),
],
style = {'width': '100%', 'display': 'inline-block'})
]),
dbc.Col([
html.P("Option 5"),
html.Div([
dcc.Dropdown(id='option5_dropdown',
options=option5_list,
value=[],
placeholder='All',
multi=True,
clearable=True),
],
style = {'width': '100%', 'display': 'inline-block'})
]),
], align='center'),
]), color='dark'
),
dbc.Card(
dbc.CardBody([
dbc.Row([
html.Div([
html.Div(id='dd-output-container')
])
], align='center'),
]), color='dark'
),
dbc.Card(
dbc.CardBody([
dbc.Row([
html.Div([
dash_table.DataTable(
id='table_container',
data=df.to_dict('records')
)
])
], align='center'),
]), color='dark'
)
])
@app.callback(
Output('table_container', 'data'),
[Input('option1_dropdown', 'value'),
Input('option2_dropdown', 'value'),
Input('option3_dropdown', 'value'),
Input('option4_dropdown', 'value'),
Input('option5_dropdown', 'value')
])
def set_dropdown_options(value1, value2, value3, value4, value5):
if not value1 or value1 == 'All':
value1 = option1_list
if not value2 or value2 == 'All':
value2 = option2_list
if not value3 or value3 == 'All':
value3 = option3_list
if not value4 or value4 == 'All':
value4 = option4_list
if not value5 or value5 == 'All':
value5 = option5_list
ddf = df.query('option1 == @value1 and '
'option2 == @value2 and '
'option3 == @value3 and '
'option4 == @value4 and '
'option5 == @value5',
engine='python')
return ddf.to_dict('records')
# ====== Using this as a way to view the selections
@app.callback(
Output('dd-output-container', 'children'),
[Input('option1_dropdown', 'value'),
Input('option2_dropdown', 'value'),
Input('option3_dropdown', 'value'),
Input('option4_dropdown', 'value'),
Input('option5_dropdown', 'value')
])
def selection(value1, value2, value3, value4, value5):
# If value lists are empty or equal to the default of 'All', use the initial df values
if not value1 or value1 == 'All':
value1 = option1_list
if not value2 or value2 == 'All':
value2 = option2_list
if not value3 or value3 == 'All':
value3 = option3_list
if not value4 or value4 == 'All':
value4 = option4_list
if not value5 or value5 == 'All':
value5 = option5_list
ddf = df.query('option1 == @value1 and '
'option2 == @value2 and '
'option3 == @value3 and '
'option4 == @value4 and '
'option5 == @value5',
engine='python')
return
if __name__ == '__main__':
app.run_server(debug=True, dev_tools_hot_reload = False)
Редактировать 2:
Есть ли способ включить исходные имена столбцов без преобразования в целочисленный суффикс?
Year = cycle(["2012", "2013", "2014", "2015"])
Season = cycle(["Win", "Spr", "Sum", "Fall"])
Month = cycle(
["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
)
temp_group = cycle(["Hot", "Cold", "Mild"])
prec_group = cycle(["Dry", "Wet"])
df = pd.DataFrame(index=range(20))
df["Year"] = [next(Year) for count in range(df.shape[0])]
df["Season"] = [next(Season) for count in range(df.shape[0])]
df["Month"] = [next(Month) for count in range(df.shape[0])]
df["Temp"] = [next(temp_group) for count in range(df.shape[0])]
df["Prec"] = [next(prec_group) for count in range(df.shape[0])]
Year_list = sorted(df["Year"].unique().tolist())
Season_list = df["Season"].unique().tolist()
Month_list = df["Month"].unique().tolist()
Temp_list = sorted(df["Temp"].unique().tolist())
Prec_list = sorted(df["Prec"].unique().tolist())
df = df.rename(columns = {'Year':'option1',
'Season':'option2',
'Month':'option3',
'Temp':'option4',
'Prec':'option5'})
app = Dash(__name__)
app.layout = html.Div(
[
dbc.Card(
dbc.CardBody(
[
dbc.Row(
[
dbc.Col(
[
html.P("Year"),
html.Div(
[
dcc.Dropdown(
id = "Year_dropdown",
options=Year_list,
value=[],
placeholder = "All",
multi=True,
clearable=True,
),
],
style = {
"width": "100%",
"display": "inline-block",
},
),
]
),
dbc.Col(
[
html.P("Season"),
html.Div(
[
dcc.Dropdown(
id = "Season_dropdown",
options=Season_list,
value=[],
placeholder = "All",
multi=True,
clearable=True,
),
],
style = {
"width": "100%",
"display": "inline-block",
},
),
]
),
dbc.Col(
[
html.P("Month"),
html.Div(
[
dcc.Dropdown(
id = "Month_dropdown",
options=Month_list,
value=[],
placeholder = "All",
multi=True,
clearable=True,
),
],
style = {
"width": "100%",
"display": "inline-block",
},
),
]
),
dbc.Col(
[
html.P("Temp"),
html.Div(
[
dcc.Dropdown(
id = "Temp_dropdown",
options=Temp_list,
value=[],
placeholder = "All",
multi=True,
clearable=True,
),
],
style = {
"width": "100%",
"display": "inline-block",
},
),
]
),
dbc.Col(
[
html.P("Prec"),
html.Div(
[
dcc.Dropdown(
id = "Prec_dropdown",
options=Prec_list,
value=[],
placeholder = "All",
multi=True,
clearable=True,
),
],
style = {
"width": "100%",
"display": "inline-block",
},
),
]
),
],
align = "center",
),
]
),
color = "dark",
),
dbc.Card(
dbc.CardBody(
[
dbc.Row(
[html.Div([html.Div(id = "dd-output-container")])], align = "center"
),
]
),
color = "dark",
),
dbc.Card(
dbc.CardBody(
[
dbc.Row(
[
html.Div(
[
dash_table.DataTable(
id = "table_container", data=df.to_dict("records")
)
]
)
],
align = "center",
),
]
),
color = "dark",
),
]
)
df = df.rename(columns = {'Year':'option1',
'Season':'option2',
'Month':'option3',
'Temp':'option4',
'Prec':'option5'})
def construct_query(filter_values):
additive_clauses = list()
subtractive_clauses = list()
for i, filter_value in enumerate(filter_values):
if filter_value and filter_value != "All":
clause = f"option{i + 1} == @value{i + 1}"
if i <= 3:
additive_clauses.append(clause)
else:
subtractive_clauses.append(clause)
if len(additive_clauses) > 0 or len(subtractive_clauses) > 0:
additive_section = " or ".join(additive_clauses)
subtractive_clauses = " and ".join(subtractive_clauses)
if additive_section and subtractive_clauses:
query = f"({additive_section}) and {subtractive_clauses}"
else:
query = additive_section or subtractive_clauses
return query
@app.callback(
[
Output("Year_dropdown", "options"),
Output("Season_dropdown", "options"),
Output("Month_dropdown", "options"),
],
[
Input("Temp_dropdown", "value"),
Input("Prec_dropdown", "value"),
],
)
def update_additive_options(value4, value5):
query = None
option4_query = "option4 == @value4"
option5_query = "option5 == @value5"
if value4 and value4 != "All" and value5 and value5 != "All":
query = f"{option4_query} and {option5_query}"
elif value4 and value4 != "All":
query = option4_query
elif value5 and value5 != "All":
query = option5_query
if query:
df_filtered = df.query(
query,
engine = "python",
)
else:
df_filtered = df
return (
sorted(df_filtered["option1"].unique().tolist()),
df_filtered["option2"].unique().tolist(),
df_filtered["option3"].unique().tolist(),
)
@app.callback(
[Output("Temp_dropdown", "options"), Output("Prec_dropdown", "options")],
[
Input("Year_dropdown", "options"),
Input("Season_dropdown", "options"),
Input("Month_dropdown", "options"),
],
)
def update_subtractive_options(value1, value2, value3):
query = None
additive_clauses = []
for i, filter_value in enumerate([value1, value2, value3]):
if filter_value and filter_value != "All":
clause = f"option{i + 1} == @value{i + 1}"
additive_clauses.append(clause)
if len(additive_clauses) > 0:
query = " or ".join(additive_clauses)
if query:
df_filtered = df.query(
query,
engine = "python",
)
else:
df_filtered = df
return (
sorted(df_filtered["option4"].unique().tolist()),
sorted(df_filtered["option5"].unique().tolist()),
)
@app.callback(
Output("table_container", "data"),
[
Input("Year_dropdown", "value"),
Input("Season_dropdown", "value"),
Input("Month_dropdown", "value"),
Input("Temp_dropdown", "value"),
Input("Prec_dropdown", "value"),
],
)
def update_table(value1, value2, value3, value4, value5):
query = construct_query(filter_values=[value1, value2, value3, value4, value5])
if query:
df_filtered = df.query(
query,
engine = "python",
)
else:
df_filtered = df
return df_filtered.to_dict("records")
# ====== Using this as a way to view the selections
@app.callback(
Output("dd-output-container", "children"),
[
Input("Year_dropdown", "value"),
Input("Season_dropdown", "value"),
Input("Month_dropdown", "value"),
Input("Temp_dropdown", "value"),
Input("Prec_dropdown", "value"),
],
)
def selection(value1, value2, value3, value4, value5):
# If value lists are empty or equal to the default of 'All', use the initial df values
if not value1 or value1 == "All":
value1 = Year_list
if not value2 or value2 == "All":
value2 = Season_list
if not value3 or value3 == "All":
value3 = Month_list
if not value4 or value4 == "All":
value4 = Temp_list
if not value5 or value5 == "All":
value5 = Prec_list
ddf = df.query(
"option1 == @value1 and "
"option2 == @value2 and "
"option3 == @value3 and "
"option4 == @value4 and "
"option5 == @value5",
engine = "python",
)
return
if __name__ == "__main__":
app.run_server(debug=True, dev_tools_hot_reload=False)






Этого можно добиться с помощью сюжетного тире. Хитрость заключается в том, чтобы использовать операторы or между первыми тремя фильтрами, чтобы сделать их аддитивными, и операторы and между двумя последними, чтобы сделать их вычитающими. Первые три варианта необходимо решить как один блок, прежде чем включать последние два варианта.
Пример структуры запроса: (option1 == @value1 or option2 == @value2 or option3 == @value3) and option4 == @value4 and option5 == @value5
Я решил построить запрос программно на основе того, какие фильтры имеют значения, чтобы логика or работала правильно. См. функцию construct_query() в примере ниже.
def construct_query(filter_values):
additive_clauses = list()
subtractive_clauses = list()
for i, filter_value in enumerate(filter_values):
if filter_value and filter_value != "All":
clause = f"option{i + 1} == @value{i + 1}"
if i <= 3:
additive_clauses.append(clause)
else:
subtractive_clauses.append(clause)
if len(additive_clauses) > 0 or len(subtractive_clauses) > 0:
additive_section = " or ".join(additive_clauses)
subtractive_clauses = " and ".join(subtractive_clauses)
if additive_section and subtractive_clauses:
query = f"({additive_section}) and {subtractive_clauses}"
else:
query = additive_section or subtractive_clauses
return query
Другая проблема — избежать создания циклических обратных вызовов, которые имеют одинаковые входные и выходные компоненты. Один из способов добиться этого — разбить большие обратные вызовы на несколько отдельных обратных вызовов, чтобы входные и выходные данные не были циклическими. В приведенном ниже примере я разделил обновление первых трех раскрывающихся списков на update_additive_options(), а последних двух раскрывающихся списков на update_subtractive_options().
Plotly также описывает другой способ управления циклическими обратными вызовами в своих приложениях.
Документация по расширенным обратным вызовам с контекстной функциональностью.
Пример я: Пример 2:
Вот полная версия моего кода:
import pandas as pd
from dash import Dash, dcc, html, Input, Output, dash_table
import dash_bootstrap_components as dbc
from itertools import cycle
import random
Year = cycle(["2012", "2013", "2014", "2015"])
Season = cycle(["Win", "Spr", "Sum", "Fall"])
Month = cycle(
["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
)
temp_group = cycle(["Hot", "Cold", "Mild"])
prec_group = cycle(["Dry", "Wet"])
df = pd.DataFrame(index=range(20))
df["option1"] = [next(Year) for count in range(df.shape[0])]
df["option2"] = [next(Season) for count in range(df.shape[0])]
df["option3"] = [next(Month) for count in range(df.shape[0])]
df["option4"] = [next(temp_group) for count in range(df.shape[0])]
df["option5"] = [next(prec_group) for count in range(df.shape[0])]
option1_list = sorted(df["option1"].unique().tolist())
option2_list = df["option2"].unique().tolist()
option3_list = df["option3"].unique().tolist()
option4_list = sorted(df["option4"].unique().tolist())
option5_list = sorted(df["option5"].unique().tolist())
app = Dash(__name__)
app.layout = html.Div(
[
dbc.Card(
dbc.CardBody(
[
dbc.Row(
[
dbc.Col(
[
html.P("Option 1"),
html.Div(
[
dcc.Dropdown(
id = "option1_dropdown",
options=option1_list,
value=[],
placeholder = "All",
multi=True,
clearable=True,
),
],
style = {
"width": "100%",
"display": "inline-block",
},
),
]
),
dbc.Col(
[
html.P("Option 2"),
html.Div(
[
dcc.Dropdown(
id = "option2_dropdown",
options=option2_list,
value=[],
placeholder = "All",
multi=True,
clearable=True,
),
],
style = {
"width": "100%",
"display": "inline-block",
},
),
]
),
dbc.Col(
[
html.P("Option 3"),
html.Div(
[
dcc.Dropdown(
id = "option3_dropdown",
options=option3_list,
value=[],
placeholder = "All",
multi=True,
clearable=True,
),
],
style = {
"width": "100%",
"display": "inline-block",
},
),
]
),
dbc.Col(
[
html.P("Option 4"),
html.Div(
[
dcc.Dropdown(
id = "option4_dropdown",
options=option4_list,
value=[],
placeholder = "All",
multi=True,
clearable=True,
),
],
style = {
"width": "100%",
"display": "inline-block",
},
),
]
),
dbc.Col(
[
html.P("Option 5"),
html.Div(
[
dcc.Dropdown(
id = "option5_dropdown",
options=option5_list,
value=[],
placeholder = "All",
multi=True,
clearable=True,
),
],
style = {
"width": "100%",
"display": "inline-block",
},
),
]
),
],
align = "center",
),
]
),
color = "dark",
),
dbc.Card(
dbc.CardBody(
[
dbc.Row(
[html.Div([html.Div(id = "dd-output-container")])], align = "center"
),
]
),
color = "dark",
),
dbc.Card(
dbc.CardBody(
[
dbc.Row(
[
html.Div(
[
dash_table.DataTable(
id = "table_container", data=df.to_dict("records")
)
]
)
],
align = "center",
),
]
),
color = "dark",
),
]
)
def construct_query(filter_values):
additive_clauses = list()
subtractive_clauses = list()
for i, filter_value in enumerate(filter_values):
if filter_value and filter_value != "All":
clause = f"option{i + 1} == @value{i + 1}"
if i <= 3:
additive_clauses.append(clause)
else:
subtractive_clauses.append(clause)
if len(additive_clauses) > 0 or len(subtractive_clauses) > 0:
additive_section = " or ".join(additive_clauses)
subtractive_clauses = " and ".join(subtractive_clauses)
if additive_section and subtractive_clauses:
query = f"({additive_section}) and {subtractive_clauses}"
else:
query = additive_section or subtractive_clauses
return query
@app.callback(
[
Output("option1_dropdown", "options"),
Output("option2_dropdown", "options"),
Output("option3_dropdown", "options"),
],
[
Input("option4_dropdown", "value"),
Input("option5_dropdown", "value"),
],
)
def update_additive_options(value4, value5):
query = None
option4_query = "option4 == @value4"
option5_query = "option5 == @value5"
if value4 and value4 != "All" and value5 and value5 != "All":
query = f"{option4_query} and {option5_query}"
elif value4 and value4 != "All":
query = option4_query
elif value5 and value5 != "All":
query = option5_query
if query:
df_filtered = df.query(
query,
engine = "python",
)
else:
df_filtered = df
return (
sorted(df_filtered["option1"].unique().tolist()),
df_filtered["option2"].unique().tolist(),
df_filtered["option3"].unique().tolist(),
)
@app.callback(
[Output("option4_dropdown", "options"), Output("option5_dropdown", "options")],
[
Input("option1_dropdown", "value"),
Input("option2_dropdown", "value"),
Input("option3_dropdown", "value"),
],
)
def update_subtractive_options(value1, value2, value3):
query = None
additive_clauses = []
for i, filter_value in enumerate([value1, value2, value3]):
if filter_value and filter_value != "All":
clause = f"option{i + 1} == @value{i + 1}"
additive_clauses.append(clause)
if len(additive_clauses) > 0:
query = " or ".join(additive_clauses)
if query:
df_filtered = df.query(
query,
engine = "python",
)
else:
df_filtered = df
return (
sorted(df_filtered["option4"].unique().tolist()),
sorted(df_filtered["option5"].unique().tolist()),
)
@app.callback(
Output("table_container", "data"),
[
Input("option1_dropdown", "value"),
Input("option2_dropdown", "value"),
Input("option3_dropdown", "value"),
Input("option4_dropdown", "value"),
Input("option5_dropdown", "value"),
],
)
def update_table(value1, value2, value3, value4, value5):
query = construct_query(filter_values=[value1, value2, value3, value4, value5])
if query:
df_filtered = df.query(
query,
engine = "python",
)
else:
df_filtered = df
return df_filtered.to_dict("records")
# ====== Using this as a way to view the selections
@app.callback(
Output("dd-output-container", "children"),
[
Input("option1_dropdown", "value"),
Input("option2_dropdown", "value"),
Input("option3_dropdown", "value"),
Input("option4_dropdown", "value"),
Input("option5_dropdown", "value"),
],
)
def selection(value1, value2, value3, value4, value5):
# If value lists are empty or equal to the default of 'All', use the initial df values
if not value1 or value1 == "All":
value1 = option1_list
if not value2 or value2 == "All":
value2 = option2_list
if not value3 or value3 == "All":
value3 = option3_list
if not value4 or value4 == "All":
value4 = option4_list
if not value5 or value5 == "All":
value5 = option5_list
ddf = df.query(
"option1 == @value1 and "
"option2 == @value2 and "
"option3 == @value3 and "
"option4 == @value4 and "
"option5 == @value5",
engine = "python",
)
return
if __name__ == "__main__":
app.run_server(debug=True, dev_tools_hot_reload=False)
Да, это возможно. Я использовал переменную i для простого примера, поскольку имена столбцов были последовательными. Вместо этого вы можете перебирать имена столбцов в фрейме данных или определить список имен столбцов, которые вы хотите использовать в качестве фильтров.
Можно ли реализовать тот же подход без целого числа i? Например, если опция (x) будет заменена на Год, Сезон, Месяц, Темп. процент?