Skip to content

Commit

Permalink
Flow changes. Tab switch changes
Browse files Browse the repository at this point in the history
  • Loading branch information
us8945 committed Jun 3, 2023
1 parent 33661cc commit 1d16fcf
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 161 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,5 @@ dmypy.json
*.iml
*.wave
*.state
out
out
.DS_Store
47 changes: 26 additions & 21 deletions emp-churn-step-by-step/README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,41 @@
# Step by step Apps
# Getting Started with H2O Wave

This project was bootstrapped with `wave init` command.

## Running the app

This is a series of self-contained Wave Apps that build upon each other. We begin with basic concepts and advance to more efficient development patterns for Wave.
Make sure you have activated a Python virtual environment with `h2o-wave` installed.

The Wave App highlights the capabilities of the Wave framework. It loads prediction and Shapley values files, and presents various information about Employee Churn based on the selected employee record and probability cut-off to display Shapley values.
If you haven't created a python env yet, simply run the following command (assuming Python 3.7 is installed properly).

To install and run the code:
For MacOS / Linux:

- Create virtual Python virtual environment - the code was tested with Python 3.8
```sh
python3 -m venv venv
source venv/bin/activate
pip install h2o-wave
```

- Install dependencies from the src/requirements.txt
For Windows:

- To run wave the first App, for the consecutive Apps, replace `app1` with the `app2`, ...`app13`
```sh
python3 -m venv venv
venv\Scripts\activate.bat
pip install h2o-wave
```

```bash
wave run --no-reload src.app1
........................
wave run --no-reload src.app13
```
Once the virtual environment is setup and active, run:

```sh
wave run app.py
```

Which will start a Wave app at <http://localhost:10101>.

Here is a list of the training Apps:
## Interactive examples

- App1 through App8: Step-by-step development of the Employee Churn dashboard
- App9: Adding debugging capabilities
- App10: Refactoring App9 to refresh content only when there is a change due to user interaction. In the previous steps, we have redrawn cards every time, even if there was no change
- App11: Same as App10 but pulling data from Delta Lake storage
- App12: App10 plus table pagination logic to handle large amount of data in the table
- App13: App12 plus tabs and tab switching with no card recreation
If you prefer learning by doing, you can run `wave fetch` command that will download all the existing small Python examples that show Wave in action. The best part is that all these examples are interactive, meaning you can edit their code directly within the browser and observe the changes.

To suppress Wave server messages and facilitate App debugging, define `H2O_WAVE_NO_LOG=1`.
## Learn More

TODO: Implement sorting and filtering logic for the Employee table. The current sorting logic applies only to the data within the current (single) page.
To learn more about H2O Wave, check out the [docs](https://wave.h2o.ai/).
6 changes: 1 addition & 5 deletions emp-churn-step-by-step/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
altair==4.2.0
h2o_wave==0.24
numpy==1.21.1
pandas==1.3.5
databricks-sql-connector==2.2.0
h2o-wave==0.24.0
19 changes: 11 additions & 8 deletions emp-churn-step-by-step/src/app10.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Step 10
# Refresh only relevant components. Stop re-creating cards - just refresh attributes
# Set H2O_WAVE_NO_LOG=1 to avoid printing server (waved) messages
# ---
from h2o_wave import main, app, Q, ui, on, handle_on, data

Expand Down Expand Up @@ -62,20 +63,22 @@ async def serve(q: Q):
q.client.initialized = True
await q.page.save()
else:
action_taken = False
# Several q.args params can be populated at the same time.
# For example: q.args: threshold:0.5, render_employee:['90']
if q.args.threshold is not None and q.client.threshold != q.args.threshold:
log.info("++++++++Threshold Changed+++++++++++++")
q.client.threshold = q.args.threshold
await render_threshold(q)
action_taken = True

if q.args.render_employee is not None and len(q.args.render_employee) == 1 and int(
q.args.render_employee[0]) != q.client.employee_num:
# First two conditions are to check that parameter exists and array is not empty
elif q.args.render_employee is not None and len(q.args.render_employee) == 1 and \
int(q.args.render_employee[0]) != q.client.employee_num:
log.info("++++++++Employee for Shapley details table Changed+++++++++++++")
q.client.employee_num = int(q.args.render_employee[0])
await render_emp_shapley(q)
action_taken = True

if not action_taken:
'''We should never get here, unless user did not change anything'''
else:
# We can get here if user did not change anything.
# For example, clicked on already selected Employee, or moved threshold slider to the same value
log.info("++++++++Unhandled condition or no Change+++++++++++++")
'''Will result in loading spinner going away '''
q.page['non-existent'].items = []
Expand Down
18 changes: 10 additions & 8 deletions emp-churn-step-by-step/src/app11.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,22 @@ async def serve(q: Q):
q.client.initialized = True
await q.page.save()
else:
action_taken = False
# Several q.args params can be populated at the same time.
# For example: q.args: threshold:0.5, render_employee:['90']
if q.args.threshold is not None and q.client.threshold != q.args.threshold:
log.info("++++++++Threshold Changed+++++++++++++")
q.client.threshold = q.args.threshold
await render_threshold(q)
action_taken = True

if q.args.render_employee is not None and len(q.args.render_employee) == 1 and int(
q.args.render_employee[0]) != q.client.employee_num:
# First two conditions is to check that parameter exists and array is not empty
elif q.args.render_employee is not None and len(q.args.render_employee) == 1 and \
int(q.args.render_employee[0]) != q.client.employee_num:
log.info("++++++++Employee for Shapley details table Changed+++++++++++++")
q.client.employee_num = int(q.args.render_employee[0])
await render_emp_shapley(q)
action_taken = True

if not action_taken:
'''We should never get here, unless user did not change anything'''
else:
# We can get here if user did not change anything.
# For example, clicked on already selected Employee, or moved threshold slider to the same value
log.info("++++++++Unhandled condition or no Change+++++++++++++")
'''Will result in loading spinner going away '''
q.page['non-existent'].items = []
Expand Down
59 changes: 28 additions & 31 deletions emp-churn-step-by-step/src/app12.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Step 12
# Employee table pagination - handle large number of records in table
# Set H2O_WAVE_NO_LOG=1 to avoid printing server (waved) messages
# ---
from h2o_wave import main, app, Q, ui, on, handle_on, data

Expand Down Expand Up @@ -62,33 +63,29 @@ async def serve(q: Q):
q.client.initialized = True
await q.page.save()
else:
action_taken = False
# Several q.args params can be populated at the same time.
# For example: q.args: threshold:0.5, render_employee:['90']
if q.args.threshold is not None and q.client.threshold != q.args.threshold:
log.info("========Handle Threshold Change=====")
q.client.threshold = q.args.threshold
await render_threshold(q)
action_taken = True

if q.args.render_employee is not None and len(q.args.render_employee) == 1 and int(
q.args.render_employee[0]) != q.client.employee_num:
# First two conditions are to check that parameter exists and array is not empty
elif q.args.render_employee is not None and len(q.args.render_employee) == 1 and \
int(q.args.render_employee[0]) != q.client.employee_num:
log.info("========Handle Shapley values for selected employee=====")
q.client.employee_num = int(q.args.render_employee[0])
await render_emp_shapley(q)
action_taken = True

if not action_taken and q.events.render_employee and q.events.render_employee.page_change:
elif q.events.render_employee and q.events.render_employee.page_change:
log.info("========Handle page Change=====")
q.client.page_offset = q.events.render_employee.page_change.get('offset', 0)
await render_pagination(q)
action_taken = True

if not action_taken and q.events.render_employee and q.events.render_employee.reset:
elif q.events.render_employee and q.events.render_employee.reset:
log.info("========Handle page Reset=====")
q.client.page_offset = 0
await render_pagination(q)
action_taken = True

if not action_taken:
'''We should never get here, unless user did not change anything'''
else:
# We can get here if user did not change anything.
# For example, clicked on already selected Employee, or moved threshold slider to the same value
log.info("++++++++Unhandled condition or no Change+++++++++++++")
'''Will result in loading spinner going away '''
q.page['non-existent'].items = []
Expand Down Expand Up @@ -229,12 +226,12 @@ async def init(q: Q) -> None:
log.info("==Complete init Function ==")


async def render_threshold(q:Q):
'''
Accept threshold value from slider and Refresh Stats cars and table of employees
async def render_threshold(q: Q):
"""
Accept threshold value from slider and Refresh Stats cards and table of employees
:param q:
:return:
'''
"""
log.info("==Start render_threshold Function ==")

# Get employees for the given threshold
Expand Down Expand Up @@ -262,29 +259,29 @@ async def render_threshold(q:Q):
log.info("==Complete render_threshold Function ==")


async def render_pagination(q:Q):
'''
async def render_pagination(q: Q):
"""
Handle Page change for table of employees
:param q:
:return:
'''
"""
log.info("==Start render_pagination Function ==")
cols = ['EmployeeNumber'] + q.client.varimp_col + ["Prediction"]
# Reset offset to first page
# Set table page offset
offset = q.client.page_offset
q.page['table_card'].items[0].table.rows = [ui.table_row(name=str(row['EmployeeNumber']),
cells=[str(k) for k in row[cols]]) for i, row in
q.client.churned_employees[offset: \
q.client.churned_employees[offset:
(offset + rows_per_page)].iterrows()]
log.info("==Complete render_pagination Function ==")


async def render_emp_shapley(q: Q):
'''
"""
Refresh Shapley values for the selected employee.
:param q:
:return:
'''
"""
log.info("==Start render_emp_shapley Function ==")
q.client.employee_varimp = get_local_varimp(q.app.shapley[q.app.shapley['EmployeeNumber'] == q.client.employee_num])

Expand All @@ -295,12 +292,12 @@ async def render_emp_shapley(q: Q):


def get_varimp(shapley_vals, top_n=5):
'''
"""
Get Global variable importance based on Shapley Values
:param shapley_vals:
:param top_n:
:return:
'''
"""
log.info("==Start get_varimp Function ==")
varimp = shapley_vals[[i for i in shapley_vals.columns if 'contrib' in i and i != 'contrib_bias']]
varimp = varimp.abs().mean().reset_index()
Expand All @@ -314,12 +311,12 @@ def get_varimp(shapley_vals, top_n=5):


def get_local_varimp(shapley_vals, top_n=5):
'''
Format Shapley Values for given employee
"""
Format Shapley Values for a given employee
:param shapley_vals:
:param top_n:
:return:
'''
"""
log.info("==Start get_local_varimp Function ==")
varimp = shapley_vals[[i for i in shapley_vals.columns if 'contrib' in i and i != 'contrib_bias']]
varimp = varimp.iloc[0].reset_index()
Expand Down
Loading

0 comments on commit 1d16fcf

Please # to comment.