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

Fix reactivity #28

Merged
merged 13 commits into from
Sep 15, 2023
264 changes: 163 additions & 101 deletions binomialbias/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,24 @@
import version as bbv
import main as bbm

T1 = sc.timer() # For debugging


#%% Global variables

# Set the global dictionary defaults
g = sc.objdict()
g.nt = 20 # Number of appointments
g.ne = 10 # Expected number
g.na = 7 # Actual number
g.ntt = g.nt # Number of appointments (text)
g.fe = g.ne/g.nt # Expected fraction
g.fa = g.na/g.nt # Actual fraction
def make_globaldict():

# Set the global dictionary defaults
g = sc.objdict()
g.nt = 20 # Number of appointments
g.ne = 10 # Expected number
g.na = 7 # Actual number
g.ntt = g.nt # Number of appointments (text)
g.fe = g.ne/g.nt # Expected fraction
g.fa = g.na/g.nt # Actual fraction
g.iter = 0 # How many times the value has been updated (the iteration)

return g

# Set the component keys
slider_keys = ['nt', 'ne', 'na']
Expand All @@ -45,30 +52,102 @@
nmax = 100 # Default maximum slider value
slider_max = 1_000_000 # Absolute maximum slider value
width = '50%' # Width of the text entry boxes
delay = 0.3 # Optionally wait for user to finish input before updating
delay = 0.0 # Optionally wait for user to finish input before updating
debug = False


#%% Define the interface

desc = '''
<div>This webapp calculates bias and discrimination in appointment processes, based
on the binomial distribution. It is provided in support of the following paper:<br>
<br>
<b>Quantitative assessment of discrimination in appointments to senior Australian university positions.</b>
Robinson PA, Kerr CC. <i>Under review (2023).</i><br>
<br>
For more information, please see the <a href="https://github.com/thekerrlab/binomialbias">GitHub repository</a>,
the <a href="http://binomialbiaspaper.sciris.org">paper</a> (TBC), or
<a href="mailto:[email protected]">contact us</a>.<br>
<br>
</div>
'''
def make_ui(*args, **kwargs):

desc = '''
<div>This webapp calculates bias and discrimination in appointment processes, based
on the binomial distribution. It is provided in support of the following paper:<br>
<br>
<b>Quantitative assessment of discrimination in appointments to senior Australian university positions.</b>
Robinson PA, Kerr CC. <i>Under review (2023).</i><br>
<br>
For more information, please see the <a href="https://github.com/thekerrlab/binomialbias">GitHub repository</a>,
the <a href="http://binomialbiaspaper.sciris.org">paper</a> (TBC), or
<a href="mailto:[email protected]">contact us</a>.<br>
<br>
</div>
'''

version = f'''
<div><br>
<i>Version: {bbv.__version__} ({bbv.__versiondate__})</i><br>
</div>
'''

nt_str = ui.HTML('Total number of appointments (<i>n<sub>t</sub></i>)')
ne_str = ui.HTML('Expected appointments (<i>n<sub>e</sub></i>)')
na_str = ui.HTML('Actual appointments (<i>n<sub>a</sub></i>)')
ntt_str = '(or type any value)'
fe_str = ui.HTML('Expected fraction (<i>f<sub>e</sub></i>)')
fa_str = ui.HTML('Actual fraction (<i>f<sub>a</sub></i>)')
pagestyle = {"style": "margin-top: 2rem"} # Increase spacing at the top
flexgap = {"style": "display: flex; gap: 2rem"}
flexwrap = {"style": "display: flex; flex-wrap: wrap"}
plotwrap = {'style': 'width: 50vw; min-width: 400px; max-width: 1200px'}

# Define the widgets
g = make_globaldict()
wg = sc.objdict()
wg.nt = ui.input_slider('nt', label=nt_str, min=nmin, max=nmax, value=g.nt)
wg.ne = ui.input_slider('ne', label=ne_str, min=nmin, max=nmax, value=g.ne)
wg.na = ui.input_slider('na', label=na_str, min=nmin, max=nmax, value=g.na)
wg.ntt = ui.input_text('ntt', label=ntt_str, width=width, value=g.ntt)
wg.fe = ui.input_text('fe', label=fe_str, width=width, value=g.fe)
wg.fa = ui.input_text('fa', label=fa_str, width=width, value=g.fa)

# Define the app layout
app_ui = ui.page_fluid(pagestyle,
ui.layout_sidebar(
ui.panel_sidebar(
ui.h2('BinomialBias'),
ui.hr(),
ui.HTML(desc),
ui.input_action_button("instructions", "Instructions", width='200px'),
ui.HTML(version),
ui.hr(),
ui.h4('Inputs'),
ui.div(flexgap, wg.nt, wg.ntt),
ui.div(flexgap, wg.ne, wg.fe),
ui.div(flexgap, wg.na, wg.fa),
ui.div(flexgap,
ui.input_action_button("update", "Update", class_="btn-success", width='80%'),
ui.input_switch("autoupdate", 'Automatic', False, width='150px'),
)
),
ui.panel_main(
ui.div(flexwrap,
ui.div(plotwrap,
ui.panel_conditional("input.show_p",
ui.output_plot('plot_bias', width='100%', height='800px'),
),
ui.div(flexgap,
ui.input_checkbox("show_p", "Show plot", True),
ui.input_checkbox("show_s", "Show statistics", False),
)
),
ui.div(
ui.panel_conditional("input.show_s",
ui.h4('Statistics'),
ui.output_table('stats_table'),
),
),
ui.output_text_verbatim('debug_text'), # Hidden unless debug = True above, but needed for reactivity
),
)
),
title = 'BinomialBias',
)

return app_ui

version = f'''
<div><br>
<i>Version: {bbv.__version__} ({bbv.__versiondate__})</i><br>
</div>
'''

#%% Define the server

instr = ui.HTML('''
This webapp calculates the bias in a selection process, such as
Expand All @@ -85,75 +164,21 @@
(compared to 0.588 had there been 10 women selected). We can also calculate that
the bias against women being selected for this committee is <i>B = 1.86</i>.<br>
<br>
Further information and examples are available in the manuscript.
Further information and examples are available in the manuscript.<br>
<br>
<i>Note:</i> for finer-grained control of the sliders, you can click on the marker and
use the arrow keys rather than clicking and dragging.
''')

nt_str = ui.HTML('Total number of appointments (<i>n<sub>t</sub></i>)')
ne_str = ui.HTML('Expected appointments (<i>n<sub>e</sub></i>)')
na_str = ui.HTML('Actual appointments (<i>n<sub>a</sub></i>)')
ntt_str = '(or type any value)'
fe_str = ui.HTML('Expected fraction (<i>f<sub>e</sub></i>)')
fa_str = ui.HTML('Actual fraction (<i>f<sub>a</sub></i>)')
pagestyle = {"style": "margin-top: 2rem"} # Increase spacing at the top
flexgap = {"style": "display: flex; gap: 2rem"}
flexwrap = {"style": "display: flex; flex-wrap: wrap"}
plotwrap = {'style': 'width: 50vw; min-width: 400px; max-width: 1200px'}

# Define the widgets
wg = sc.objdict()
wg.nt = ui.input_slider('nt', label=nt_str, min=nmin, max=nmax, value=g.nt)
wg.ne = ui.input_slider('ne', label=ne_str, min=nmin, max=nmax, value=g.ne)
wg.na = ui.input_slider('na', label=na_str, min=nmin, max=nmax, value=g.na)
wg.ntt = ui.input_text('ntt', label=ntt_str, width=width, value=g.ntt)
wg.fe = ui.input_text('fe', label=fe_str, width=width, value=g.fe)
wg.fa = ui.input_text('fa', label=fa_str, width=width, value=g.fa)

# Define the app layout
app_ui = ui.page_fluid(pagestyle,
ui.layout_sidebar(
ui.panel_sidebar(
ui.h2('BinomialBias'),
ui.hr(),
ui.HTML(desc),
ui.input_action_button("instructions", "Instructions", width='200px'),
ui.HTML(version),
ui.hr(),
ui.h4('Inputs'),
ui.div(flexgap, wg.nt, wg.ntt),
ui.div(flexgap, wg.ne, wg.fe),
ui.div(flexgap, wg.na, wg.fa),
),
ui.panel_main(
ui.div(flexwrap,
ui.div(plotwrap,
ui.panel_conditional("input.show_p",
ui.output_plot('plot_bias', width='100%', height='800px'),
),
ui.div(flexgap,
ui.input_checkbox("show_p", "Show plot", True),
ui.input_checkbox("show_s", "Show statistics", False),
)
),
ui.div(
ui.panel_conditional("input.show_s",
ui.h4('Statistics'),
ui.output_table('stats_table'),
),
),
)
),
),
title = 'BinomialBias',
)


#%% Define the server

def server(inputdict, output, session):
def server(input, output, session):
""" The PyShiny server, which includes all the update logic """

T2 = sc.timer() # For debugging
g = make_globaldict()
rerender = sh.reactive.Value(0)

@sh.reactive.Effect
@sh.reactive.event(inputdict.instructions)
@sh.reactive.event(input.instructions)
def instructions():
m = ui.modal(instr, title="Instructions", easy_close=True)
return ui.modal_show(m)
Expand All @@ -172,7 +197,7 @@ def get_ui():
u = sc.objdict()
for key in ui_keys:
try:
raw = inputdict[key]()
raw = input[key]()
v = bbm.to_num(raw)
u[key] = v
except:
Expand Down Expand Up @@ -216,6 +241,7 @@ def reconcile_inputs():
# The isolation here avoids a potential infinite loop
with sh.reactive.isolate():
set_ui(u)
g.iter += 1
sc.heading('Done reconciling inputs.')
return

Expand All @@ -236,34 +262,70 @@ def make_bias():
bb = bbm.BinomialBias(n=g.ntt, f_e=g.fe, f_a=g.fa)
return bb

@sh.reactive.Effect
@sh.reactive.event(input.update, ignore_none=False)
def reconcile():
""" Coordinate reconciliation """
reconcile_inputs() # Reconcile inputs here since this gets called before the table
rr = rerender.get()
rerender.set(rr+1)
return

@output
@sh.render.plot(alt='Bias distributions')
@sh.reactive.event(rerender, ignore_none=False)
def plot_bias():
""" Plot the graphs """
reconcile_inputs() # Reconcile inputs here since this gets called before the table
bb = make_bias()
bb.plot(show=False, letters=False, wrap=True)
return
g.iter += 1
fig = bb.plot(show=False, letters=False, wrap=True)
return fig

@output
@sh.render.table
@sh.reactive.event(rerender, ignore_none=False)
def stats_table():
""" Create a dataframe of the results """
show_p = inputdict.show_p()
if show_p: # If we're showing the plot, trigger an event by getting the UI values
get_ui()
else: # If we're not showing the plot, we need to reconcile inputs
reconcile_inputs()
bb = make_bias()
df = bb.to_df(string=True)
return df

@output
@sh.render.text
def debug_text():
""" Debugging -- which also happens to handle the reactivity! """
import os
u = get_ui()

# Handle automatic updates
if input.autoupdate():
with sh.reactive.isolate():
reconcile_inputs()
rr = rerender.get()
rerender.set(rr+1)

s = f'''
user = {sc.getuser()}
pid = {os.getpid()}
cwd = {os.getcwd()}
gid = {id(g)}
elapsed = {T1.tocout()}, {T2.tocout()}
iter = {g.iter}
rerender = {rerender.get()}
update = {input.update.get()}

ui =
{u}'''
out = s if debug else '' # Only render output if we're in debug mode

return out

return


#%% Define and optionally run the app

app = sh.App(app_ui, server, debug=True)
app = sh.App(make_ui, server, debug=True)


def run(**kwargs):
Expand Down
4 changes: 2 additions & 2 deletions binomialbias/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__version__ = '1.1.2'
__versiondate__ = '2023-09-12'
__version__ = '1.2.0'
__versiondate__ = '2023-09-13'