| # Copyright 2022 The ChromiumOS Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| # To add a new cell, type '# %%' | |
| # To add a new markdown cell, type '# %% [markdown]' | |
| # %% | |
| # Kaleido is needed for plotly SVG export. | |
| # | |
| # You must use at least version 5.8.0 of plotly to fix go.Figure | |
| # type annotation. See https://github.com/plotly/plotly.py/pull/3425 . | |
| # | |
| #! %pip install pandas numpy matplotlib scipy tqdm ipympl jinja2 | |
| #! %pip install 'plotly>=5.8.0' kaleido nbformat | |
| # Djlint is a Jinja2 template formatter. | |
| #! %pip install djlint | |
| # For report_*.py | |
| #! %pip install requests | |
| # NOTE: Sometimes this autoreload/autoimport feature messes up the environment. | |
| # If you see odd issues, like assert statements failing for TestCases, | |
| # you need to restart your kernel. This is probably because it redefines | |
| # the enums, which makes previous save enum values not equal to the new | |
| # enum value. | |
| #! %load_ext autoreload | |
| #! %autoreload 1 | |
| from __future__ import annotations | |
| import os | |
| import pathlib | |
| from typing import Optional | |
| import bootstrap | |
| from experiment import Experiment | |
| from fpc_bet_results import FPCBETResults | |
| import fpsutils | |
| from IPython.display import display | |
| from IPython.display import HTML | |
| from IPython.display import Markdown | |
| import matplotlib | |
| from matplotlib import pyplot as plt | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| import pandas as pd | |
| import plotly | |
| import plotly.express as px | |
| import plotly.figure_factory as ff | |
| import plotly.graph_objs as go | |
| import plotly.tools as tls | |
| from report_pandoc import Figure | |
| from report_pandoc import Report2 | |
| import scipy.stats as st | |
| from scipy.stats import norm | |
| import simulate_fpstudy | |
| from test_case import TestCase | |
| from tqdm.autonotebook import tqdm # Auto detect notebook or console. | |
| pd.options.plotting.backend = "plotly" | |
| # import tqdm | |
| # import tqdm.notebook as tqdm | |
| #! %aimport bootstrap, fpsutils, report_pandoc, simulate_fpstudy, experiment, fpc_bet_results | |
| # https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-matplotlib | |
| # To open matplotlib in interactive mode | |
| # %matplotlib qt5 | |
| # %matplotlib notebook | |
| # For VSCode Interactive | |
| # get_ipython().run_line_magic('matplotlib', "widget") | |
| #! %matplotlib widget | |
| # %matplotlib gui | |
| # For VSCode No Interactive | |
| # %matplotlib inline | |
| # %matplotlib --list | |
| def print_far_value( | |
| val: float, k_comparisons: list[int] = [20, 50, 100, 500] | |
| ) -> str: | |
| """Print the science notation""" | |
| str = f"{val:.4e}" | |
| for c in k_comparisons: | |
| str += f" = {val * 100 * c*1000:.3f}% of 1/{c}k" | |
| # TODO: Make nice output like "1 in 234k". | |
| return str | |
| # %% | |
| # | |
| # The expected file layout for the data/ symbolic link to dir is the following: | |
| # - report-100k/ (dir containing the decision metadata) | |
| # - orig-data/ (dir contain the collected samples) | |
| # - analysis/ (dir created by this tool) | |
| rpt = Report2(pathlib.Path("data/analysis")) | |
| # %% Load Data | |
| # USE_SIMULATED_DATA = True | |
| USE_SIMULATED_DATA = False | |
| # USE_SIMULATED_PROB = 1/100000.0 | |
| USE_SIMULATED_PROB = 1 / 155000.0 | |
| # USE_SIMULATED_PROB = 1/175000.0 | |
| # USE_SIMULATED_PROB = 1/200000.0 | |
| exp: Optional[Experiment] = None | |
| test_cases: list[TestCase] = list() | |
| if USE_SIMULATED_DATA: | |
| class SimulatedTestCase(TestCase): | |
| SimulatedDataset = ("Simulated dataset.",) | |
| # Load simulated data | |
| print("# Loading Simulated Data") | |
| sim_far_decisions = simulate_fpstudy.GenerateFARResults( | |
| num_users=72, | |
| num_fingers=6, | |
| num_verify_samples=80, | |
| user_groups=["A", "B", "C", "D", "E", "F"], | |
| prob=USE_SIMULATED_PROB, | |
| verbose=True, | |
| ) | |
| exp = Experiment( | |
| num_verification=60, | |
| num_fingers=6, | |
| num_users=72, | |
| far_decisions=sim_far_decisions, | |
| ) | |
| test_cases = [SimulatedTestCase.SimulatedDataset] | |
| exps = { | |
| test_cases[0]: exp, | |
| } | |
| DISPLAY_TC = SimulatedTestCase.SimulatedDataset | |
| else: | |
| # Find Data | |
| # We expect a symbolic link named `data` to point to the directory that | |
| # contains the FPC BET reports and raw collection. | |
| print("# Discover Root Data Directory") | |
| data_root = pathlib.Path(os.readlink("data")) | |
| if not data_root.is_dir(): | |
| raise Exception("Data root doesn't exist") | |
| print(f"Using root {data_root}") | |
| # BET_REPORT_DIR = str(data_root.joinpath('report2')) | |
| BET_REPORT_DIR = data_root.joinpath("report-100k") | |
| COLLECTION_DIR = data_root.joinpath("orig-data") | |
| # %% Import Data From BET Results | |
| bet = FPCBETResults(BET_REPORT_DIR) | |
| # FIXME: Only enable one test case for speed of testing. | |
| # test_cases = [FPCBETResults.TestCase.TUDisabled] | |
| test_cases = FPCBETResults.TestCase.all() | |
| far_decisions = bet.read_files( | |
| list( | |
| zip( | |
| test_cases, | |
| [FPCBETResults.TableType.FAR_Decision] * len(test_cases), | |
| ) | |
| ) | |
| ) | |
| frr_decisions = bet.read_files( | |
| list( | |
| zip( | |
| test_cases, | |
| [FPCBETResults.TableType.FRR_Decision] * len(test_cases), | |
| ) | |
| ) | |
| ) | |
| exps = { | |
| test_cases[i]: Experiment( | |
| num_verification=80, | |
| num_fingers=6, | |
| num_users=72, | |
| far_decisions=far, | |
| frr_decisions=frr, | |
| ) | |
| for i, (far, frr) in enumerate(zip(far_decisions, frr_decisions)) | |
| } | |
| for tc in exps: | |
| exps[tc].add_groups_from_collection_dir(COLLECTION_DIR) | |
| # exp = Experiment(num_verification=60, | |
| # num_fingers=6, | |
| # num_users=72, | |
| # far_decisions=far_decisions[0], | |
| # frr_decisions=frr_decisions[0], | |
| # ) | |
| # exp.add_groups_from_collection_dir(COLLECTION_DIR) | |
| # exp._tbl_far_decisions = exp._tbl_far_decisions[exp._tbl_far_decisions['VerifyGroup'] == 'A'] | |
| # exp._tbl_far_decisions = exp._tbl_far_decisions[exp._tbl_far_decisions['VerifyGroup'] == 'B'] | |
| # exp._tbl_far_decisions = exp._tbl_far_decisions[exp._tbl_far_decisions['VerifyGroup'] == 'C'] | |
| # exp._tbl_far_decisions = exp._tbl_far_decisions[exp._tbl_far_decisions['VerifyGroup'] == 'D'] | |
| # exp._tbl_far_decisions = exp._tbl_far_decisions[exp._tbl_far_decisions['VerifyGroup'] == 'E'] | |
| DISPLAY_TC = FPCBETResults.TestCase.TUDisabled | |
| exp = exps[DISPLAY_TC] | |
| # %% Generate Report Test cases | |
| rpt_tc = {tc: rpt.test_case_add(str(tc), tc.description()) for tc in test_cases} | |
| # %% Check | |
| description = """\ | |
| This shows the number of false accepts as a function of each parameter. | |
| This helps to show if there is some enrollment user, verification user, finger, | |
| or sample that has an unusually high false acceptable rate. | |
| Recall that the results of the FAR experiment is a table of accept/reject | |
| decisions based on the following parameters: | |
| * Enrolled User | |
| * Enrolled Finger | |
| * Verification User | |
| * Verification Finger | |
| * Verification Sample | |
| The Enrolled User and Verification User are further categorized by their | |
| participant groups, Enrolled Group and Verification Group, respectively. | |
| """ | |
| display( | |
| Markdown( | |
| f"""\ | |
| # Distribution of False Accepts | |
| {description} | |
| """ | |
| ) | |
| ) | |
| # rpt.add_text(description) | |
| fa_table = exp.fa_table() | |
| display(fa_table) | |
| # fpsutils.plot_pd_hist_discrete(fa_table, title_prefix='False Accepts') | |
| # rpt.add_plot('Data Distributions') | |
| # %% | |
| #! %%time | |
| # Ultimately, we want to do a histogram over the entire FAR/FRR Decision | |
| # dataset, but doing so directly with the large DataFrame is much too slow | |
| # and will actually hang plotly. We are mimicking the following histogram | |
| # operation: | |
| # go.Histogram(histfunc="sum", x=far['EnrollUser'], y=far['Decision']) | |
| # | |
| # A similar method might be using the following, which runs in about 300ms: | |
| # far[['EnrollUser', 'Decision']].groupby(['EnrollUser']).sum() | |
| # | |
| # The fastest method is by reverse constructing the complete counts table | |
| # by using the pre-aggregated fa_table. This runs in about 66ms, which is | |
| # primarily the time to run exp.user_list(). | |
| def fa_count_figure( | |
| exp: Experiment, | |
| cols: list[Experiment.TableCol], | |
| title: str, | |
| xaxis_title: str, | |
| ) -> go.Figure: | |
| fa_counts = pd.DataFrame({c.value: exp.fa_counts_by(c) for c in cols}) | |
| fa_counts.rename( | |
| columns={c.value: f"FA Counts by {c.value}" for c in cols}, inplace=True | |
| ) | |
| non_zero_labels = fa_counts.loc[(fa_counts > 0).any(axis=1)].index | |
| # non_zero_labels = list(non_zero_counts_enroll.index) + \ | |
| # list(non_zero_counts_verify.index) | |
| # non_zero_labels.sort() | |
| # It is nice to keep the blank space between non-zero bars to be able to | |
| # identify possible abnormal clusters of high false acceptance users. | |
| fig = px.bar( | |
| fa_counts, | |
| # pattern_shape='variable', pattern_shape_sequence=['/', '\\'], | |
| # text_auto=True, | |
| # labels={'EnrollUser': 'FA Counts by EnrollUser', | |
| # 'VerifyUser': 'FA Counts by VerifyUser'} | |
| # orientation='h', | |
| ) | |
| # fig.update_xaxes(type='category') | |
| # fig.update_layout(barmode='overlay') | |
| # fig.update_layout(barmode='group') | |
| # Reduce opacity to see both histograms | |
| # fig.update_traces(opacity=0.75) | |
| # fig.update_traces(opacity=0.50) | |
| # fig.update_traces(marker_size=10) | |
| # fig.update_traces(marker_line_color = 'blue', marker_line_width = 0.25) | |
| fig.update_layout( | |
| # title_text='False Accepts by User ID of Enroll and Verify', | |
| title=title, | |
| # xaxis_title_text='User ID', | |
| xaxis_title=xaxis_title, | |
| yaxis_title_text="False Accept Count", | |
| legend_title="", | |
| barmode="group", | |
| # barmode='overlay', | |
| # bargap=0.2, # gap between bars of adjacent location coordinates | |
| # bargroupgap=0.1 # gap between bars of the same location coordinates | |
| bargap=0.0, | |
| bargroupgap=0.0, | |
| xaxis=dict(type="category", tickmode="array", tickvals=non_zero_labels), | |
| ) | |
| fig.update_layout( | |
| legend=dict( | |
| # orientation='h', | |
| yanchor="top", | |
| y=0.99, | |
| xanchor="left", | |
| x=0.01, | |
| ) | |
| ) | |
| return fig | |
| def fr_count_figure( | |
| exp: Experiment, | |
| cols: list[Experiment.TableCol], | |
| title: str, | |
| xaxis_title: str, | |
| ) -> go.Figure: | |
| fr_counts = pd.DataFrame({c.value: exp.fr_counts_by(c) for c in cols}) | |
| fr_counts.rename( | |
| columns={c.value: f"FR Counts by {c.value}" for c in cols}, inplace=True | |
| ) | |
| non_zero_labels = fr_counts.loc[(fr_counts > 0).any(axis=1)].index | |
| # non_zero_labels = list(non_zero_counts_enroll.index) + \ | |
| # list(non_zero_counts_verify.index) | |
| # non_zero_labels.sort() | |
| fr_tests_total = exp.fr_trials_count() | |
| fr_category_size = len(fr_counts.index) | |
| fr_per_category = float(fr_tests_total) / float(fr_category_size) | |
| percents = ( | |
| np.array(fr_counts.values, dtype=float) / fr_per_category | |
| ) * 100.0 | |
| # It is nice to keep the blank space between non-zero bars to be able to | |
| # identify possible abnormal clusters of high false acceptance users. | |
| # fig = go.Figure() | |
| # fig.add_trace(go.Bar(x=fr_counts.index, y=fr_counts.values[:, 0])) | |
| fig: go.Figure = px.bar( | |
| fr_counts, | |
| # pattern_shape='variable', pattern_shape_sequence=['/', '\\'], | |
| # text_auto=True, | |
| # labels={'EnrollUser': 'FA Counts by EnrollUser', | |
| # 'VerifyUser': 'FA Counts by VerifyUser'} | |
| # orientation='h', | |
| hover_data={ | |
| "percent": (":.3f", percents[:, 0]), | |
| }, | |
| ) | |
| # fr_tests_total = exp.fr_trials_count() | |
| # fr_category_size = len(fr_counts.index) | |
| # fr_per_category = float(fr_tests_total) / float(fr_category_size) | |
| # percents = (np.array(fr_counts.values, dtype=float) / fr_per_category) * 100.0 | |
| # fr_percents = pd.Series(percents, index=fr_counts.index, name='Percents') | |
| # fig.add_trace(go.Bar(x=fr_counts.index, y=percents[:, 0], yaxis='y2')) | |
| # fig.update_xaxes(type='category') | |
| # fig.update_layout(barmode='overlay') | |
| # fig.update_layout(barmode='group') | |
| # Reduce opacity to see both histograms | |
| # fig.update_traces(opacity=0.75) | |
| # fig.update_traces(opacity=0.50) | |
| # fig.update_traces(marker_size=10) | |
| # fig.update_traces(marker_line_color = 'blue', marker_line_width = 0.25) | |
| fig.update_layout( | |
| # title_text='False Rejects by User ID of Enroll and Verify', | |
| title=title, | |
| # xaxis_title_text='User ID', | |
| xaxis_title=xaxis_title, | |
| # yaxis_title_text='False Rejects Count', | |
| legend_title="", | |
| barmode="group", | |
| # barmode='overlay', | |
| # bargap=0.2, # gap between bars of adjacent location coordinates | |
| # bargroupgap=0.1 # gap between bars of the same location coordinates | |
| bargap=0.0, | |
| bargroupgap=0.0, | |
| xaxis=dict(type="category", tickmode="array", tickvals=non_zero_labels), | |
| yaxis=dict( | |
| title="False Rejects Count", | |
| showline=True, | |
| ), | |
| yaxis2=dict( | |
| title="False Reject Percent", | |
| side="right", | |
| showline=True, | |
| ), | |
| ) | |
| fig.update_layout( | |
| legend=dict( | |
| # orientation='h', | |
| yanchor="top", | |
| y=0.99, | |
| xanchor="left", | |
| x=0.01, | |
| ) | |
| ) | |
| return fig | |
| #### Histograms #### | |
| for tc in test_cases: | |
| exp = exps[tc] | |
| section = rpt_tc[tc].add_subsection("hist") | |
| # A high FA count for an EnrollUser would indicate some template(s) for a given | |
| # user allows more false accepts from other users. | |
| # A high FA count for a VerifyUser would indicate that some match attempts | |
| # with this user's fingers yields more false accepts. | |
| # User | |
| fig = fa_count_figure( | |
| exp, | |
| [Experiment.TableCol.Enroll_User, Experiment.TableCol.Verify_User], | |
| "False Accepts by User ID of Enroll and Verify", | |
| "User ID", | |
| ) | |
| section.add_figure( | |
| "FA_by_User", "False Accepts by User ID of Enroll and Verify.", fig | |
| ) | |
| fig = fr_count_figure( | |
| exp, | |
| [Experiment.TableCol.Verify_User], | |
| "False Rejects by User ID", | |
| "User ID", | |
| ) | |
| section.add_figure("FR_by_User", "False Rejects by User ID.", fig) | |
| # Finger | |
| fig = fa_count_figure( | |
| exp, | |
| [Experiment.TableCol.Enroll_Finger, Experiment.TableCol.Verify_Finger], | |
| "False Accepts by Finger ID of Enroll and Verify", | |
| "Finger ID", | |
| ) | |
| section.add_figure( | |
| "FA_by_Finger", "False Accepts by Finger ID of Enroll and Verify.", fig | |
| ) | |
| fig = fr_count_figure( | |
| exp, | |
| [Experiment.TableCol.Verify_Finger], | |
| "False Rejects by Finger ID", | |
| "Finger ID", | |
| ) | |
| section.add_figure("FR_by_Finger", "False Rejects by Finger ID.", fig) | |
| # Sample | |
| # Keep in mind that different test cases may select different samples | |
| # for verification. | |
| fig = fa_count_figure( | |
| exp, | |
| [Experiment.TableCol.Verify_Sample], | |
| "False Accepts by Verify Sample ID", | |
| "Sample ID", | |
| ) | |
| section.add_figure( | |
| "FA_by_Sample", "False Accepts by Verify Sample ID.", fig | |
| ) | |
| fig = fr_count_figure( | |
| exp, | |
| [Experiment.TableCol.Verify_Sample], | |
| "False Rejects by Sample ID. " | |
| "Keep in mind that different test cases may use " | |
| "different samples for verification.", | |
| "Sample ID", | |
| ) | |
| fr_counts = exp.fr_counts_by(Experiment.TableCol.Verify_Sample) | |
| line = st.linregress(fr_counts.index, fr_counts.values) | |
| line_x = np.array(fr_counts.index) | |
| line = st.linregress(line_x, np.array(fr_counts.values)) | |
| line_y = line.slope * line_x + line.intercept | |
| fig.add_trace(go.Line(x=line_x, y=line_y, name="Linear Regression")) | |
| section.add_figure("FR_by_Sample", "False Rejects by Sample ID.", fig) | |
| # Group | |
| fig = fa_count_figure( | |
| exp, | |
| [Experiment.TableCol.Enroll_Group, Experiment.TableCol.Verify_Group], | |
| "False Accepts by Group of Enroll and Verify", | |
| "Group", | |
| ) | |
| section.add_figure( | |
| "FA_by_Group", "False Accepts by Group of Enroll and Verify.", fig | |
| ) | |
| fig = fr_count_figure( | |
| exp, | |
| [Experiment.TableCol.Verify_Group], | |
| "False Rejects by Group", | |
| "Group", | |
| ) | |
| section.add_figure("FR_by_Group", "False Rejects by Group.", fig) | |
| rpt_tc[DISPLAY_TC].display(display) | |
| ################################################################# | |
| # %% | |
| ### FA_by_User | |
| s1 = { | |
| "EnrollUser_" | |
| + tc.name: exps[tc].fa_counts_by(Experiment.TableCol.Enroll_User) | |
| for tc in test_cases | |
| } | |
| s2 = { | |
| "VerifyUser_" | |
| + tc.name: exps[tc].fa_counts_by(Experiment.TableCol.Verify_User) | |
| for tc in test_cases | |
| } | |
| df = pd.DataFrame(s1 | s2) | |
| # [tc.name for tc in test_cases] | |
| fig = px.bar(df) | |
| fig.update_layout( | |
| title="False Accepts by User", | |
| xaxis_title="User", | |
| yaxis_title_text="Count", | |
| legend_title="Category + Test Case", | |
| barmode="group", | |
| # height=2049, | |
| # barmode='overlay', | |
| # bargap=0.2, # gap between bars of adjacent location coordinates | |
| # bargroupgap=0.1 # gap between bars of the same location coordinates | |
| bargap=0.0, | |
| bargroupgap=0.0, | |
| xaxis=dict( | |
| type="category", | |
| # tickmode='array', | |
| # tickvals=non_zero_labels, | |
| ), | |
| ) | |
| fig.update_layout( | |
| legend=dict( | |
| # orientation='h', | |
| yanchor="top", | |
| y=0.99, | |
| xanchor="left", | |
| x=0.01, | |
| ) | |
| ) | |
| fig.show() | |
| rpt.overall_section().add_figure("FA_by_User", "False Accepts by User", fig) | |
| ### FR_by_User | |
| df = pd.DataFrame( | |
| { | |
| tc.name: exps[tc].fr_counts_by(Experiment.TableCol.Verify_User) | |
| for tc in test_cases | |
| } | |
| ) | |
| # [tc.name for tc in test_cases] | |
| fig = px.bar( | |
| df, | |
| # orientation='h', | |
| ) | |
| fig.update_layout( | |
| title="False Rejects by User", | |
| xaxis_title="User", | |
| yaxis_title_text="Count", | |
| legend_title="Test Case", | |
| barmode="group", | |
| # height=2049, | |
| # barmode='overlay', | |
| # bargap=0.2, # gap between bars of adjacent location coordinates | |
| # bargroupgap=0.1 # gap between bars of the same location coordinates | |
| bargap=0.0, | |
| bargroupgap=0.0, | |
| xaxis=dict( | |
| type="category", | |
| # tickmode='array', | |
| # tickvals=non_zero_labels, | |
| ), | |
| ) | |
| fig.update_layout( | |
| legend=dict( | |
| # orientation='h', | |
| yanchor="top", | |
| y=0.99, | |
| xanchor="left", | |
| x=0.01, | |
| ) | |
| ) | |
| fig.show() | |
| rpt.overall_section().add_figure("FR_by_User", "False Rejects by User", fig) | |
| ### FA_by_Sample | |
| df = pd.DataFrame( | |
| { | |
| tc.name: exps[tc].fa_counts_by(Experiment.TableCol.Verify_Sample) | |
| for tc in test_cases | |
| } | |
| ) | |
| # [tc.name for tc in test_cases] | |
| fig = px.bar( | |
| df, | |
| # orientation='h', | |
| ) | |
| fig.update_layout( | |
| # title_text='False Rejects by User ID of Enroll and Verify', | |
| title="False Accepts by Sample", | |
| xaxis_title="Group", | |
| yaxis_title_text="Count", | |
| legend_title="Test Case", | |
| barmode="group", | |
| # height=2049, | |
| # barmode='overlay', | |
| # bargap=0.2, # gap between bars of adjacent location coordinates | |
| # bargroupgap=0.1 # gap between bars of the same location coordinates | |
| bargap=0.0, | |
| bargroupgap=0.0, | |
| # xaxis=dict(type='category', | |
| # tickmode='array', | |
| # tickvals=non_zero_labels), | |
| xaxis=dict( | |
| type="category", | |
| # tickmode='array', | |
| # tickvals=non_zero_labels, | |
| ), | |
| ) | |
| # fig.update_layout( | |
| # # title_text='False Accepts by User ID of Enroll and Verify', | |
| # title=title, | |
| # # xaxis_title_text='User ID', | |
| # xaxis_title=xaxis_title, | |
| # yaxis_title_text='False Accept Count', | |
| # legend_title='', | |
| # barmode='group', | |
| # # barmode='overlay', | |
| # # bargap=0.2, # gap between bars of adjacent location coordinates | |
| # # bargroupgap=0.1 # gap between bars of the same location coordinates | |
| # bargap=0.0, | |
| # bargroupgap=0.0, | |
| # xaxis=dict(type='category', | |
| # tickmode='array', | |
| # tickvals=non_zero_labels), | |
| # ) | |
| fig.update_layout( | |
| legend=dict( | |
| # orientation='h', | |
| yanchor="top", | |
| y=0.99, | |
| xanchor="left", | |
| x=0.01, | |
| ) | |
| ) | |
| fig.show() | |
| rpt.overall_section().add_figure("FA_by_Sample", "False Accepts by Sample", fig) | |
| ### FA_by_Finger | |
| s1 = { | |
| "EnrollFinger_" | |
| + tc.name: exps[tc].fa_counts_by(Experiment.TableCol.Enroll_Finger) | |
| for tc in test_cases | |
| } | |
| s2 = { | |
| "VerifyFinger_" | |
| + tc.name: exps[tc].fa_counts_by(Experiment.TableCol.Verify_Finger) | |
| for tc in test_cases | |
| } | |
| df = pd.DataFrame(s1 | s2) | |
| # [tc.name for tc in test_cases] | |
| fig = px.bar(df) | |
| fig.update_layout( | |
| title="False Accepts by Finger", | |
| xaxis_title="Finger", | |
| yaxis_title_text="Count", | |
| legend_title="Category + Test Case", | |
| barmode="group", | |
| ) | |
| fig.show() | |
| rpt.overall_section().add_figure("FA_by_Finger", "False Accepts by Finger", fig) | |
| ### FR_by_Finger | |
| df = pd.DataFrame( | |
| { | |
| tc.name: exps[tc].fr_counts_by(Experiment.TableCol.Verify_Finger) | |
| for tc in test_cases | |
| } | |
| ) | |
| # [tc.name for tc in test_cases] | |
| fig = px.bar(df) | |
| fig.update_layout( | |
| title="False Rejects by Finger", | |
| xaxis_title="Finger", | |
| yaxis_title_text="Count", | |
| legend_title="Test Case", | |
| barmode="group", | |
| ) | |
| fig.show() | |
| rpt.overall_section().add_figure("FR_by_Finger", "False Rejects by Finger", fig) | |
| ### FA_by_Group | |
| s1 = { | |
| "EnrollGroup_" | |
| + tc.name: exps[tc].fa_counts_by(Experiment.TableCol.Enroll_Group) | |
| for tc in test_cases | |
| } | |
| s2 = { | |
| "VerifyGroup_" | |
| + tc.name: exps[tc].fa_counts_by(Experiment.TableCol.Verify_Group) | |
| for tc in test_cases | |
| } | |
| df = pd.DataFrame(s1 | s2) | |
| # [tc.name for tc in test_cases] | |
| fig = px.bar(df) | |
| fig.update_layout( | |
| title="False Accepts by Group", | |
| xaxis_title="Group", | |
| yaxis_title_text="Count", | |
| legend_title="Category + Test Case", | |
| barmode="group", | |
| ) | |
| fig.show() | |
| rpt.overall_section().add_figure("FA_by_Group", "False Accepts by Group", fig) | |
| ### FR_by_Group | |
| df = pd.DataFrame( | |
| { | |
| tc.name: exps[tc].fr_counts_by(Experiment.TableCol.Verify_Group) | |
| for tc in test_cases | |
| } | |
| ) | |
| # [tc.name for tc in test_cases] | |
| fig = px.bar(df) | |
| fig.update_layout( | |
| # title_text='False Rejects by User ID of Enroll and Verify', | |
| title="False Rejects by Group", | |
| xaxis_title="Group", | |
| yaxis_title_text="Count", | |
| legend_title="Test Case", | |
| barmode="group", | |
| ) | |
| fig.show() | |
| rpt.overall_section().add_figure("FR_by_Group", "False Rejects by Group", fig) | |
| ### | |
| # rpt.generate({ | |
| # # 'pdf', | |
| # # 'md', | |
| # 'html', | |
| # }) | |
| # %% Bootstrap Sampling | |
| # 1000 samples is 95% | |
| # 5000 samples is 99% | |
| # BOOTSTRAP_SAMPLES = 1000 | |
| # BOOTSTRAP_SAMPLES = 5000 # 5000 samples in 2 seconds (128 cores) | |
| BOOTSTRAP_SAMPLES = 100000 # 100000 samples in 16 seconds (128 cores) | |
| CONFIDENCE_PERCENT = 95 | |
| PARALLEL_BOOTSTRAP = True | |
| FAR_THRESHOLD = 1 / 100000.0 | |
| FRR_THRESHOLD = 10 / 100.0 | |
| #### FAR #### | |
| far_boot_results: dict[TestCase, bootstrap.BootstrapResults] = dict() | |
| frr_boot_results: dict[TestCase, bootstrap.BootstrapResults] = dict() | |
| far_figures: dict[TestCase, go.Figure] = dict() | |
| frr_figures: dict[TestCase, go.Figure] = dict() | |
| for tc in test_cases: | |
| print(f"Running Test Case {tc}.") | |
| exp = exps[tc] | |
| section = rpt_tc[tc].add_subsection("score") | |
| info = section.add_data("Info") | |
| #### FAR #### | |
| # Run FAR bootstrap | |
| boot = bootstrap.BootstrapFullFARHierarchy(exp, verbose=True) | |
| # boot = bootstrap.BootstrapFARFlat(exp, verbose=True) | |
| boot_results = boot.run( | |
| num_samples=BOOTSTRAP_SAMPLES, | |
| num_proc=0, | |
| progress=lambda it, total: tqdm(it, total=total), | |
| ) | |
| far_boot_results[tc] = boot_results | |
| # Showing raw values works because we take som many bootstrap samples, | |
| # which fills in a lot of gaps in a "unique" value (bin_size=1) histogram. | |
| bins, counts = fpsutils.discrete_hist(boot_results.samples()) | |
| df = pd.DataFrame( | |
| { | |
| # X-Axes | |
| "False Accepts in Bootstrap Sample": bins, | |
| "FAR": np.array(bins, dtype=float) / exp.fa_trials_count(), | |
| # Y-Axes | |
| "Number of Bootstrap Samples Observed": counts, | |
| } | |
| ) | |
| fig = px.bar( | |
| df, | |
| x="FAR", | |
| y="Number of Bootstrap Samples Observed", | |
| hover_data=["False Accepts in Bootstrap Sample"], | |
| title="Frequency of FAR in Bootstrap Samples", | |
| ) | |
| fig.update_layout(hovermode="x unified") | |
| ci_lower, ci_upper = boot_results.confidence_interval() | |
| frr_ci_lower = ci_lower / exp.fa_trials_count() | |
| frr_ci_upper = ci_upper / exp.fa_trials_count() | |
| frr_mean = np.mean(boot_results.samples()) / exp.fa_trials_count() | |
| frr_std = np.std(boot_results.samples()) / exp.fa_trials_count() | |
| fig.add_vline( | |
| x=frr_ci_lower, | |
| annotation_text=f"lower {CONFIDENCE_PERCENT}%", | |
| annotation_position="top right", | |
| line_width=1, | |
| line_dash="dash", | |
| line_color="yellow", | |
| ) | |
| fig.add_vline( | |
| x=frr_ci_upper, | |
| annotation_text=f"upper {CONFIDENCE_PERCENT}%", | |
| annotation_position="top left", | |
| line_width=1, | |
| line_dash="dash", | |
| line_color="yellow", | |
| ) | |
| fig.add_vline( | |
| x=frr_mean, | |
| annotation_text="mean", | |
| annotation_position="top left", | |
| line_width=1, | |
| line_dash="dash", | |
| line_color="yellow", | |
| ) | |
| # fig.add_vline(x=1/50000.0, annotation_text='1/50', | |
| # line_width=2, line_dash='dash', line_color='red') | |
| fig.add_vline( | |
| x=1 / 100000.0, | |
| annotation_text="1/100k", | |
| line_width=1, | |
| line_dash="dash", | |
| line_color="red", | |
| ) | |
| fig.add_vline( | |
| x=1 / 200000.0, | |
| annotation_text="1/200k", | |
| line_width=1, | |
| line_dash="dash", | |
| line_color="red", | |
| ) | |
| far_figures[tc] = fig | |
| section.add_figure( | |
| "FAR_Bootstrap", | |
| "The hierarchical FAR bootstrap sampling histogram.", | |
| fig, | |
| ) | |
| info.set("FAR_Confidence", CONFIDENCE_PERCENT) | |
| info.set("FAR_Trials", exp.fa_trials_count()) | |
| info.set("FAR_False_Accepts", exp.fa_count()) | |
| info.set("FAR_CI_Lower", frr_ci_lower) | |
| info.set("FAR_CI_Upper", frr_ci_upper) | |
| info.set("FAR_Mean", frr_mean) | |
| info.set("FAR_Std", frr_std) | |
| info.set("FAR_Threshold", f"1/{1 / (FAR_THRESHOLD*1000)}k") | |
| info.set("FAR_Pass", frr_ci_upper < FAR_THRESHOLD) | |
| #### FRR #### | |
| # Run FRR bootstrap | |
| boot = bootstrap.BootstrapFullFRRHierarchy(exp, verbose=True) | |
| # boot = bootstrap.BootstrapFARFlat(exp, verbose=True) | |
| boot_results = boot.run( | |
| num_samples=BOOTSTRAP_SAMPLES, | |
| num_proc=0, | |
| progress=lambda it, total: tqdm(it, total=total), | |
| ) | |
| frr_boot_results[tc] = boot_results | |
| # Showing raw values works because we take som many bootstrap samples, | |
| # which fills in a lot of gaps in a "unique" value (bin_size=1) histogram. | |
| bins, counts = fpsutils.discrete_hist(boot_results.samples()) | |
| df = pd.DataFrame( | |
| { | |
| # X-Axes | |
| "False Accepts in Bootstrap Sample": bins, | |
| "FRR": np.array(bins, dtype=float) / exp.fr_trials_count(), | |
| # Y-Axes | |
| "Number of Bootstrap Samples Observed": counts, | |
| } | |
| ) | |
| fig = px.bar( | |
| df, | |
| x="FRR", | |
| y="Number of Bootstrap Samples Observed", | |
| hover_data=["False Accepts in Bootstrap Sample"], | |
| title="Frequency of FRR in Bootstrap Samples", | |
| ) | |
| fig.update_layout( | |
| hovermode="x unified", | |
| xaxis=dict(tickformat="%"), | |
| bargap=0.0, | |
| bargroupgap=0.0, | |
| ) | |
| ci_lower, ci_upper = boot_results.confidence_interval() | |
| frr_ci_lower = ci_lower / exp.fr_trials_count() | |
| frr_ci_upper = ci_upper / exp.fr_trials_count() | |
| frr_mean = np.mean(boot_results.samples()) / exp.fr_trials_count() | |
| frr_std = np.std(boot_results.samples()) / exp.fr_trials_count() | |
| fig.add_vline( | |
| x=frr_ci_lower, | |
| annotation_text=f"lower {CONFIDENCE_PERCENT}%", | |
| annotation_position="top right", | |
| line_width=1, | |
| line_dash="dash", | |
| line_color="yellow", | |
| ) | |
| fig.add_vline( | |
| x=frr_ci_upper, | |
| annotation_text=f"upper {CONFIDENCE_PERCENT}%", | |
| annotation_position="top left", | |
| line_width=1, | |
| line_dash="dash", | |
| line_color="yellow", | |
| ) | |
| fig.add_vline( | |
| x=frr_mean, | |
| annotation_text="mean", | |
| annotation_position="top left", | |
| line_width=1, | |
| line_dash="dash", | |
| line_color="yellow", | |
| ) | |
| fig.add_vline( | |
| x=10 / 100, | |
| annotation_text="10%", | |
| line_width=2, | |
| line_dash="dash", | |
| line_color="red", | |
| ) | |
| frr_figures[tc] = fig | |
| section.add_figure( | |
| "FRR_Bootstrap", | |
| "The hierarchical FRR bootstrap sampling histogram.", | |
| fig, | |
| ) | |
| info.set("FRR_Confidence", CONFIDENCE_PERCENT) | |
| info.set("FRR_Trials", exp.fr_trials_count()) | |
| info.set("FRR_False_Accepts", exp.fr_count()) | |
| info.set("FRR_CI_Lower", frr_ci_lower) | |
| info.set("FRR_CI_Upper", frr_ci_upper) | |
| info.set("FRR_Mean", frr_mean) | |
| info.set("FRR_Std", frr_std) | |
| info.set("FRR_Threshold", f"{FRR_THRESHOLD * 100}%") | |
| info.set("FRR_Pass", frr_ci_upper < FRR_THRESHOLD) | |
| far_figures[DISPLAY_TC].show() | |
| frr_figures[DISPLAY_TC].show() | |
| # %% | |
| rpt_tc[DISPLAY_TC].display(display) | |
| # %% | |
| # fr_counts = pd.DataFrame({c.value: exp.fr_counts_by(c) for c in cols}) | |
| # fr_counts.rename( | |
| # columns={c.value: f'FR Counts by {c.value}' for c in cols}, inplace=True) | |
| # non_zero_labels = fr_counts.loc[(fr_counts > 0).any(axis=1)].index | |
| # fig = ff.create_distplot( | |
| # [exps[tc].fr_counts_by(Experiment.TableCol.Enroll_Group) | |
| # for tc in test_cases], | |
| # [tc.name for tc in test_cases], | |
| # bin_size=1, | |
| # curve_type='normal', | |
| # show_curve=True, | |
| # ) | |
| # fig.show() | |
| # fig = ff.create_distplot( | |
| # [far_boot_results[tc].samples() for tc in test_cases], | |
| # [tc.name for tc in test_cases], | |
| # bin_size=10, | |
| # curve_type='normal', | |
| # show_curve=True, | |
| # ) | |
| # fig.show() | |
| # fig.write_html('big_image.html') | |
| # %% | |
| rpt.generate( | |
| { | |
| # 'pdf', | |
| # 'md', | |
| "html", | |
| } | |
| ) | |
| # %% | |
| # Upload to x20 website | |
| # https://hesling.users.x20web.corp.google.com/www/fpstudy-demo/output.pdf | |
| # https://hesling.users.x20web.corp.google.com/www/fpstudy-demo/index.html | |
| os.system("rm -rf -v /google/data/rw/users/he/hesling/www/fpstudy-demo") | |
| os.system( | |
| "cp -r -v data/analysis /google/data/rw/users/he/hesling/www/fpstudy-demo" | |
| ) | |
| # %% | |
| # Upload to g3docs for bloonchipper | |
| # /google/src/cloud/hesling/fpc1025-qual/google3 | |
| # https://cider.corp.google.com/?ws=hesling/fpc1025-qual | |
| g3docs_path = ( | |
| "/google/src/cloud/hesling/fpc1025-qual/company/teams/" | |
| "chromeos-fingerprint/development/firmware/testing/Qualification_Results" | |
| "/bloonchipper/" | |
| "fpstudy" | |
| ) | |
| os.system(f"cp -r -v data/analysis {g3docs_path}") | |
| ############################################################# | |
| ############################################################# | |
| ############################################################# | |
| ############################################################# | |
| ############################################################# | |
| ############################################################# | |
| ############################################################# | |
| ############################################################# | |
| ############################################################# | |
| # %% | |
| # 1000 samples is 95% | |
| # 5000 samples is 99% | |
| # BOOTSTRAP_SAMPLES = 1000 | |
| # BOOTSTRAP_SAMPLES = 5000 # 5000 samples in 2 seconds (128 cores) | |
| BOOTSTRAP_SAMPLES = 100000 # 100000 samples in 16 seconds (128 cores) | |
| # CONFIDENCE_PERCENT = 95 | |
| CONFIDENCE_PERCENT = 99 | |
| PARALLEL_BOOTSTRAP = True | |
| # %% Run FAR bootstrap | |
| boot = bootstrap.BootstrapFullFARHierarchy(exp, verbose=True) | |
| # boot = bootstrap.BootstrapFARFlat(exp, verbose=True) | |
| boot_results = boot.run( | |
| num_samples=BOOTSTRAP_SAMPLES, | |
| num_proc=0, | |
| progress=lambda it, total: tqdm(it, total=total), | |
| ) | |
| df = pd.DataFrame({"Sample Means": boot_results.samples()}, dtype=int) | |
| print(df) | |
| print(df.describe()) | |
| # %% | |
| display( | |
| Markdown( | |
| """ | |
| # Raw Bootstrap Samples | |
| """ | |
| ) | |
| ) | |
| print("Number Bootstrap Samples:", len(boot_results.samples())) | |
| df = pd.DataFrame({"Bootstrap Counts": boot_results.samples()}, dtype=int) | |
| print(df.describe()) | |
| plt.figure() | |
| plt.title(f"Histogram of {len(boot_results.samples())} Bootstrap Samples") | |
| # fpsutils.plt_discrete_hist(boot_means) | |
| fpsutils.plt_discrete_hist2(boot_results.samples()) | |
| plt.xlabel("Observed Bootstrap Sample Count") | |
| plt.ylabel("Frequency of Observation") | |
| plt.show() | |
| # Showing raw values works because we take som many bootstrap samples, | |
| # which fills in a lot of gaps in a "unique" value (bin_size=1) histogram. | |
| bins, counts = fpsutils.discrete_hist(boot_results.samples()) | |
| fig = px.bar(x=bins, y=counts) | |
| fig.show() | |
| df = pd.DataFrame( | |
| { | |
| # X-Axes | |
| "False Accepts in Bootstrap Sample": bins, | |
| "FAR": np.array(bins, dtype=float) / exp.fa_trials_count(), | |
| # Y-Axes | |
| "Number of Bootstrap Samples Observed": counts, | |
| } | |
| ) | |
| fig = px.bar( | |
| df, | |
| x="FAR", | |
| y="Number of Bootstrap Samples Observed", | |
| hover_data=["False Accepts in Bootstrap Sample"], | |
| title="Frequency of FAR in Bootstrap Samples", | |
| ) | |
| fig.update_layout(hovermode="x unified") | |
| ci_lower, ci_upper = boot_results.confidence_interval() | |
| frr_ci_lower = ci_lower / exp.fa_trials_count() | |
| frr_ci_upper = ci_upper / exp.fa_trials_count() | |
| frr_mean = np.mean(boot_results.samples()) / exp.fa_trials_count() | |
| fig.add_vline( | |
| x=frr_ci_lower, | |
| annotation_text=f"lower {CONFIDENCE_PERCENT}%", | |
| annotation_position="top right", | |
| line_width=1, | |
| line_dash="dash", | |
| line_color="yellow", | |
| ) | |
| fig.add_vline( | |
| x=frr_ci_upper, | |
| annotation_text=f"upper {CONFIDENCE_PERCENT}%", | |
| annotation_position="top left", | |
| line_width=1, | |
| line_dash="dash", | |
| line_color="yellow", | |
| ) | |
| fig.add_vline( | |
| x=frr_mean, | |
| annotation_text="mean", | |
| annotation_position="top left", | |
| line_width=1, | |
| line_dash="dash", | |
| line_color="yellow", | |
| ) | |
| # fig.add_vline(x=1/50000.0, annotation_text='1/50', | |
| # line_width=2, line_dash='dash', line_color='red') | |
| fig.add_vline( | |
| x=1 / 100000.0, | |
| annotation_text="1/100k", | |
| line_width=1, | |
| line_dash="dash", | |
| line_color="red", | |
| ) | |
| fig.add_vline( | |
| x=1 / 200000.0, | |
| annotation_text="1/200k", | |
| line_width=1, | |
| line_dash="dash", | |
| line_color="red", | |
| ) | |
| fig.show() | |
| # %% | |
| display( | |
| Markdown( | |
| """ | |
| # FAR Bootstrap Analysis | |
| """ | |
| ) | |
| ) | |
| print(f"Total Cross Matches: {exp.fa_trials_count()}") | |
| print(f"Total Cross False Accepts: {exp.fa_count()}") | |
| boot_mean = np.mean(boot_results.samples()) # bootstrapped sample means | |
| print("Mean:", boot_mean) | |
| boot_std = np.std(boot_results.samples()) # bootstrapped std | |
| print("Std:", boot_std) | |
| ci_percent_lower = (100 - CONFIDENCE_PERCENT) / 2 | |
| ci_percent_upper = 100 - ci_percent_lower | |
| # 95% C.I. | |
| boot_limits = np.percentile( | |
| boot_results.samples(), [ci_percent_lower, ci_percent_upper] | |
| ) | |
| print("Limits:", boot_limits) | |
| limit_diff = boot_limits[1] - boot_limits[0] | |
| print("Limits Diff:", limit_diff) | |
| ################# | |
| boot_means_ratio = np.divide(boot_results.samples(), exp.fa_trials_count()) | |
| # far_limits = np.array([ | |
| # boot_limits[0] / float(exp.FARMatches()), | |
| # boot_limits[1] / float(exp.FARMatches()), | |
| # ]) | |
| far_limits = np.divide(boot_limits, exp.fa_trials_count()) | |
| frr_mean = np.divide(boot_mean, exp.fa_trials_count()) | |
| frr_std = np.divide(boot_std, exp.fa_trials_count()) | |
| plt.figure() | |
| # fpsutils.plt_discrete_hist(np.divide(boot_means, exp.FARMatches())) | |
| # Fit a normal distribution to the data: | |
| # mean and standard deviation | |
| mu, std = norm.fit(boot_means_ratio) | |
| # Plot the histogram. | |
| bins = 25 | |
| # bins=50 | |
| plt.hist(boot_means_ratio, bins=bins, density=True, alpha=0.6, color="b") | |
| # Plot the PDF. | |
| xmin, xmax = plt.xlim() | |
| x = np.linspace(xmin, xmax, 100) | |
| p = norm.pdf(x, mu, std) | |
| plt.plot(x, p, "k", linewidth=2) | |
| plt.title(f"Fit Values: {mu} and {std}") | |
| plt.axvline(x=far_limits[0], color="blue") | |
| plt.axvline(x=far_limits[1], color="blue") | |
| plt.axvline(x=1 / 100000.0, color="red") | |
| plt.text(1 / 100000.0, 100000, "1/100k", rotation=90, color="red") | |
| plt.axvline(x=1 / 200000.0, color="red") | |
| plt.text(1 / 200000.0, 100000, "1/200k", rotation=90, color="red") | |
| plt.show() | |
| print("FAR Mean:", frr_mean) | |
| print("FAR Std:", frr_std) | |
| print(f"FAR Limits: {far_limits} <= 1/100k = {far_limits <= 1/100000.0}") | |
| print( | |
| "FAR Limits:", | |
| [ | |
| print_far_value(far_limits[0], [100]), | |
| print_far_value(far_limits[1], [100]), | |
| ], | |
| ) | |
| print(f"FAR Limits 1/{1/far_limits}") | |
| print(f"FAR Limits {far_limits <= 1/100000.0}") | |
| # rpt.add_text(f'FAR Mean: {far_mean}') | |
| # rpt.add_text(f'FAR Std: {far_std}') | |
| # rpt.add_text( | |
| # f'FAR Limits: {far_limits} <= 1/100k = {far_limits <= 1/100000.0}') | |
| # rpt.add_text( | |
| # f'FAR Limits: {[print_far_value(far_limits[0], [100]),print_far_value(far_limits[1], [100])]}') | |
| # rpt.add_text(f'FAR Limits 1/{1/far_limits}') | |
| # rpt.add_text(f'FAR Limits {far_limits <= 1/100000.0}') | |
| ############################################################################### | |
| # %% | |
| rpt.generate({"pdf", "md", "html"}) | |
| # %% |