blob: 85d14569a5a9f70e24b3e8f4b68fecc84300d4b8 [file] [log] [blame] [edit]
# 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"})
# %%