From 720d70ed61e17b0b471034547f1a915791843962 Mon Sep 17 00:00:00 2001 From: Devansh-Singh-567 Date: Sun, 19 Oct 2025 13:23:04 +0530 Subject: [PATCH 1/3] feat(app): replace picker with searchable dropdown for customer selection --- churn-risk/src/app.py | 63 +++++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/churn-risk/src/app.py b/churn-risk/src/app.py index a6bddba4..b9eda3ac 100644 --- a/churn-risk/src/app.py +++ b/churn-risk/src/app.py @@ -9,12 +9,13 @@ from .churn_predictor import ChurnPredictor -TRAIN_DATASET_PATH='./data/churnTrain.csv' -TEST_DATASET_PATH='./data/churnTest.csv' -TARGET_COLUMN="Churn?" -CATEGORICAL_COLUMNS=['Area Code'] -DROP_COLUMNS=["Phone"] +TRAIN_DATASET_PATH = './data/churnTrain.csv' +TEST_DATASET_PATH = './data/churnTest.csv' +TARGET_COLUMN = "Churn?" +CATEGORICAL_COLUMNS = ['Area Code'] +DROP_COLUMNS = ["Phone"] +# Load test data once at startup df = pd.read_csv(TEST_DATASET_PATH) df.dropna(inplace=True) df['Total Charges'] = (df['Day Charges'] + df['Evening Charges'] + df['Night Charges'] + df['Intl Charges']) @@ -84,8 +85,8 @@ def render_desc_info(q: Q, selected_row_index: Optional[int]): ) total_charges = df['Total Charges'] - charge = total_charges[selected_row_index] if selected_row_index is not None else total_charges.mean(axis=0) - rank = df['Total Charges'].rank(pct=True).values[selected_row_index] if selected_row_index is not None else df['Total Charges'].rank(pct=True).mean(axis=0) + charge = total_charges[selected_row_index] if selected_row_index is not None else total_charges.mean() + rank = df['Total Charges'].rank(pct=True).values[selected_row_index] if selected_row_index is not None else df['Total Charges'].rank(pct=True).mean() q.page['total_charges'] = ui.tall_gauge_stat_card( box='top-stats', title='Total Charges' if selected_row_index else 'Average Total Charges', @@ -104,7 +105,7 @@ def render_charges_breakdown(q: Q, selected_row_index: Optional[int]): if selected_row_index is not None: rows.append((label, df[label][selected_row_index])) else: - rows.append((label, df[label].mean(axis=0))) + rows.append((label, df[label].mean())) color_range = f'{q.client.primary_color} {q.client.secondary_color} {q.client.tertiary_color} #67dde6' q.page['bar_chart'] = ui.plot_card( box=ui.box('top-stats', height='300px'), @@ -115,10 +116,27 @@ def render_charges_breakdown(q: Q, selected_row_index: Optional[int]): def render_analysis(q: Q): - row_phone_no = int(q.args.customers[0]) if q.args.customers else None - q.page['title'].items[0].picker.values = q.args.customers + # Dropdown returns a string (or None), not a list + row_phone_no = int(q.args.customers) if q.args.customers else None + + # Update dropdown selection visually + q.page['title'].items[0].dropdown.value = q.args.customers + + # Find selected index safely + selected_row_index = None + if row_phone_no is not None: + matching = df[df['Phone'] == row_phone_no] + if not matching.empty: + selected_row_index = int(matching.index[0]) + else: + q.page['title'].subtitle = f'Customer: {row_phone_no} (Not found in test data)' + # Clear previous plots to avoid stale data + for card in ['shap_plot', 'top_negative_plot', 'top_positive_plot', 'churn_rate', 'total_charges', 'bar_chart']: + if card in q.page: + del q.page[card] + return + q.page['title'].subtitle = f'Customer: {row_phone_no or "No customer selected"}' - selected_row_index = int(df[df['Phone'] == row_phone_no].index[0]) if row_phone_no else None shap_rows = churn_predictor.get_shap(selected_row_index) render_shap_plot(q, shap_rows, selected_row_index) @@ -172,13 +190,13 @@ def init(q: Q): title='Customer profiles from model predictions', subtitle='Customer: No customer chosen', items=[ - # TODO: Replace with dropdown after https://github.com/h2oai/wave/pull/303 merged. - ui.picker( + ui.dropdown( name='customers', label='Customer Phone Number', choices=[ui.choice(name=str(phone), label=str(phone)) for phone in df['Phone']], - max_choices=1, - trigger=True + trigger=True, + searchable=True, + placeholder='Search by phone number...' ), ui.toggle(name='theme', label='Dark Theme', trigger=True) ] @@ -206,15 +224,14 @@ async def serve(q: Q): q.page['title'].items[1].toggle.value = dark_theme if q.args.code: - del q.page['shap_plot'] - del q.page['top_negative_plot'] - del q.page['top_positive_plot'] - del q.page['total_charges'] - del q.page['bar_chart'] - del q.page['churn_rate'] + # Clean up analysis cards + for card in ['shap_plot', 'top_negative_plot', 'top_positive_plot', 'churn_rate', 'total_charges', 'bar_chart']: + if card in q.page: + del q.page[card] render_code(q) else: - del q.page['code'] + if 'code' in q.page: + del q.page['code'] render_analysis(q) - await q.page.save() + await q.page.save() \ No newline at end of file From fe000bfeea2073ecc3cff172d016867db15da652 Mon Sep 17 00:00:00 2001 From: Devansh-Singh-567 Date: Sun, 19 Oct 2025 13:24:11 +0530 Subject: [PATCH 2/3] feat(app): replace picker with searchable dropdown for customer selection --- churn-risk/100 | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 churn-risk/100 diff --git a/churn-risk/100 b/churn-risk/100 new file mode 100644 index 00000000..e69de29b From 85c488a7ccf4b03ef21b1947b34ad9fd78b337f2 Mon Sep 17 00:00:00 2001 From: Devansh-Singh-567 Date: Sun, 19 Oct 2025 18:02:30 +0530 Subject: [PATCH 3/3] Improve Credit Risk app UI and data handling - Load full test dataset for better model insights. --- credit-risk/src/app.py | 172 ++++++++++++++++++++--------------------- 1 file changed, 85 insertions(+), 87 deletions(-) diff --git a/credit-risk/src/app.py b/credit-risk/src/app.py index 54e9374f..96a6a762 100644 --- a/credit-risk/src/app.py +++ b/credit-risk/src/app.py @@ -5,31 +5,34 @@ TRAIN_CSV = './data/Kaggle/CreditCard-train.csv' -TEST_CSV = './data/Kaggle/CreditCard-train.csv' +TEST_CSV = './data/Kaggle/CreditCard-train.csv' # In practice, use a separate test set ID_COLUMN = 'ID' TARGET_COLUMN = 'default.payment.next.month' -# Recommend rejecting customer's with default probability appove this amount APPROVAL_THRESHOLD = 0.35 def init_app(q: Q): q.app.initialized = True - # build model q.app.model = Model(TRAIN_CSV, ID_COLUMN, TARGET_COLUMN) - # prep df for analyzation - df = pd.read_csv(TEST_CSV).head(20) + + # Load FULL test dataset (not just 20 rows) + df = pd.read_csv(TEST_CSV) + df = df.copy() df.drop(TARGET_COLUMN, axis=1, inplace=True) - # analyze df + + # Generate predictions & contributions q.app.predictions_df = q.app.model.predict(df) q.app.contributions_df = q.app.model.contrib(df) - # prep df for viewing + + # Prepare display DataFrame df['Default Prediction Rate'] = q.app.predictions_df.round(4) - df.insert(loc=0, column='Status', value='Pending', allow_duplicates=True) + df.insert(loc=0, column='Status', value='Pending', allow_duplicates=True) q.app.customer_df = df def init_client(q: Q): q.client.initialized = True + q.client.cards = set() q.page['meta'] = ui.meta_card( box='', title='Credit Risk', @@ -39,15 +42,7 @@ def init_client(q: Q): zones=[ ui.zone('header'), ui.zone('customer_table'), - ui.zone( - 'customer_page', - zones=[ - ui.zone('customer_risk_explanation'), - ui.zone('customer_shap_plot'), - ui.zone('button_group'), - ui.zone('customer_features'), - ] - ) + ui.zone('customer_page', size='800px'), ] ), ui.layout( @@ -55,21 +50,7 @@ def init_client(q: Q): zones=[ ui.zone('header'), ui.zone('customer_table'), - ui.zone( - 'customer_page', - direction=ui.ZoneDirection.ROW, - zones=[ - ui.zone( - 'content', - zones=[ - ui.zone('customer_risk_explanation'), - ui.zone('customer_shap_plot'), - ui.zone('button_group'), - ] - ), - ui.zone('customer_features', size='300px'), - ] - ) + ui.zone('customer_page', size='800px'), ] ) ] @@ -80,149 +61,166 @@ def init_client(q: Q): subtitle='Review customer ability to pay credit card bills', icon='PaymentCard', nav=[ - ui.nav_group( - 'Navigation', - items=[ - ui.nav_item(name='render_customer_selector', label='Customers'), - ] - ), - ui.nav_group( - 'Options', - items=[ - ui.nav_item(name='dark_mode', label='Dark Mode'), - ui.nav_item(name='light_mode', label='Light Mode'), - ] - ) + ui.nav_group('Navigation', items=[ui.nav_item(name='render_customer_selector', label='Customers')]), + ui.nav_group('Options', items=[ + ui.nav_item(name='dark_mode', label='Dark Mode'), + ui.nav_item(name='light_mode', label='Light Mode'), + ]) ] ) def clear_page(q: Q): - if q.client.cards: - for card in q.client.cards: + for card in list(q.client.cards): + if card in q.page: del q.page[card] q.client.cards = set() @on() async def approve(q: Q): - q.app.customer_df.loc[q.app.customer_df[ID_COLUMN] == q.client.selected_customer_id, 'Status'] = 'Approved' + if q.client.selected_customer_id is not None: + q.app.customer_df.loc[q.app.customer_df[ID_COLUMN] == q.client.selected_customer_id, 'Status'] = 'Approved' await render_customer_selector(q) @on() async def reject(q: Q): - q.app.customer_df.loc[q.app.customer_df[ID_COLUMN] == q.client.selected_customer_id, 'Status'] = 'Rejected' + if q.client.selected_customer_id is not None: + q.app.customer_df.loc[q.app.customer_df[ID_COLUMN] == q.client.selected_customer_id, 'Status'] = 'Rejected' await render_customer_selector(q) @on() async def dark_mode(q: Q): q.page['meta'].theme = 'h2o-dark' + await render_customer_selector(q) @on() async def light_mode(q: Q): q.page['meta'].theme = 'default' + await render_customer_selector(q) @on('customer_table') async def render_customer_page(q: Q): - clear_page(q) + # Get selected row index + if not q.args.customer_table: + return row_index = int(q.args.customer_table[0]) - customer_row = q.app.customer_df.loc[row_index] - score = q.app.predictions_df.loc[row_index] - approve = bool(score < APPROVAL_THRESHOLD) - contribs = q.app.contributions_df.loc[row_index].drop('BiasTerm') + customer_row = q.app.customer_df.iloc[row_index] + score = q.app.predictions_df.iloc[row_index] + approve_flag = bool(score < APPROVAL_THRESHOLD) + contribs = q.app.contributions_df.iloc[row_index].drop('BiasTerm') - q.client.selected_customer_id = customer_row['ID'] + q.client.selected_customer_id = customer_row[ID_COLUMN] - # details + # Customer features table q.client.cards.add('customer_features') q.page['customer_features'] = ui.form_card( - box='customer_features', + box='customer_page', items=[ ui.table( name='customer_features', columns=[ - ui.table_column(name='attribute', label='Attribute', sortable=False, searchable=False, max_width='100'), - ui.table_column(name='value', label='Value', sortable=False, searchable=False, max_width='100') + ui.table_column(name='attribute', label='Attribute', max_width='150'), + ui.table_column(name='value', label='Value', max_width='150') ], - rows=[ui.table_row(name=index, cells=[index, row]) for index, row in customer_row.map(str).iteritems()], - groupable=False, - resettable=False, - multiple=False, - height='525px' + rows=[ui.table_row(name=str(i), cells=[str(k), str(v)]) for i, (k, v) in enumerate(customer_row.items())], + height='400px' ) ] ) - # summary - top_feature = contribs.idxmin(axis=0) if approve else contribs.idxmax(axis=0) + # Risk explanation + top_feature = contribs.idxmin() if approve_flag else contribs.idxmax() explanation_data = { - 'will_or_will_not': 'will' if approve else 'will not', + 'will_or_will_not': 'will' if approve_flag else 'will not', 'top_contributing_feature': top_feature, 'value_of_top_contributing_feature': str(customer_row[top_feature]), - 'accept_or_reject': 'approve' if approve else 'reject', + 'accept_or_reject': 'approve' if approve_flag else 'reject', } explanation = ( "- This customer **{{will_or_will_not}}** most probably settle the next month credit card balance.\n" "- Having a **{{top_contributing_feature}}** of **{{value_of_top_contributing_feature}}** is the top reason for that.\n" - "- It's recommended to **{{accept_or_reject}}** this customer." + "- It's recommended to **{{accept_or_reject}}** this customer." ) q.client.cards.add('customer_risk_explanation') q.page['customer_risk_explanation'] = ui.markdown_card( - box='customer_risk_explanation', + box='customer_page', title='Summary on Customer', content='=' + explanation, data=explanation_data, ) - # shap plot - shap_values = list(zip(contribs.index, contribs)) + # SHAP plot + shap_values = [(col, float(val)) for col, val in contribs.items()] shap_values.sort(key=lambda x: x[1]) q.client.cards.add('customer_shap_plot') q.page['customer_shap_plot'] = ui.plot_card( - box='customer_shap_plot', + box='customer_page', title='Effectiveness of each attribute on defaulting next payment', data=data(['label', 'value'], rows=shap_values), plot=ui.plot([ui.mark(type='interval', x='=value', x_title='Feature Contributions', y='=label')]) ) - # approve/reject buttons + # Action buttons q.client.cards.add('button_group') q.page['button_group'] = ui.form_card( - box='button_group', + box='customer_page', items=[ - ui.buttons( - [ - ui.button(name='approve', label='Approve', primary=approve), - ui.button(name='reject', label='Reject', primary=not approve), - ] - ) + ui.buttons([ + ui.button(name='approve', label='Approve', primary=approve_flag), + ui.button(name='reject', label='Reject', primary=not approve_flag), + ]) ] ) async def render_customer_selector(q: Q): - clear_page(q) - columns = [ui.table_column(name=column, label=column, sortable=True, searchable=True) for column in q.app.customer_df.columns] - rows = [ui.table_row(name=str(index), cells=row.tolist()) for index, row in q.app.customer_df.applymap(str).iterrows()] + # Make all columns searchable and sortable + columns = [] + for col in q.app.customer_df.columns: + # Format numeric columns nicely + if col == 'Default Prediction Rate': + columns.append(ui.table_column( + name=col, + label=col, + sortable=True, + searchable=True, + data_type=ui.TableDataType.NUMBER, + precision=4 + )) + else: + columns.append(ui.table_column( + name=col, + label=col, + sortable=True, + searchable=True + )) + + rows = [] + for idx, row in q.app.customer_df.iterrows(): + cells = [str(v) for v in row] + rows.append(ui.table_row(name=str(idx), cells=cells)) + q.client.cards.add('customer_table') q.page['customer_table'] = ui.form_card( box='customer_table', items=[ - ui.message_bar(text='Click "Status" to review a customer', type='info'), + ui.message_bar(text='Click any row to review a customer', type='info'), ui.table( name='customer_table', columns=columns, rows=rows, - groupable=True, multiple=False, + height='500px', + downloadable=True # Optional: allow CSV export ) ] ) @@ -236,4 +234,4 @@ async def serve(q: Q): init_client(q) if not await handle_on(q): await render_customer_selector(q) - await q.page.save() + await q.page.save() \ No newline at end of file