From 171005f4820bee6cf5e79afa20989b9bf4ad8de5 Mon Sep 17 00:00:00 2001 From: Ted Yun Date: Fri, 7 Jun 2024 14:02:07 -0400 Subject: [PATCH 1/4] Add analysis_replication.md file --- regle/analysis/analysis_replication.md | 208 +++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 regle/analysis/analysis_replication.md diff --git a/regle/analysis/analysis_replication.md b/regle/analysis/analysis_replication.md new file mode 100644 index 0000000..114b238 --- /dev/null +++ b/regle/analysis/analysis_replication.md @@ -0,0 +1,208 @@ +# Replicates all main analyses in the REGLE paper + +## Analysis of the embeddings + +1. See `embedding_interpretability.ipynb`. + + +## Principal component analysis (PCA) and spline fitting + +See `pca_and_spline_fitting.ipynb`. + + +## GWAS + +1. GWAS on all phenotypes via [BOLT-LMM](https://alkesgroup.broadinstitute.org/BOLT-LMM/BOLT-LMM_manual.html): + + ```[bash] + PHENO_NAME="..." + PHENO_FILE="..." + BOLT_LDSC_DIR="..." + UKB_GENOTYPED_DIR="..." + UKB_IMPUTED_DIR="..." + UKB_BGEN_DIR="..." + bolt \ + --numThreads 64 \ + --LDscoresFile "${BOLT_LDSC_DIR}/LDSCORE.1000G_EUR.tab.gz" \ + --LDscoresMatchBp \ + --covarFile "${PHENO_FILE}" \ + --phenoFile "${PHENO_FILE}" \ + --phenoCol "${PHENO_NAME}" \ + --statsFile /tmp/tmp_result_experiment1 \ + --fam "${UKB_GENOTYPED_DIR}/all_samples.fam" \ + --sampleFile "${UKB_IMPUTED_DIR}/ukb.sample" \ + --predBetasFile /tmp/genotyped_variants.betas \ + --remove "${UKB_GENOTYPED_DIR}/nonoverlapping_samples.txt" \ + --lmmForceNonInf \ + --bgenMinMAF 9.999999747378752e-05 \ + --bgenMinINFO 0.6000000238418579 \ + --bgenFile "${UKB_BGEN_DIR}/ukb_imp_chr10_v3_mininfo_0.6.bgen" \ + --statsFileBgenSnps /tmp/tmp_bgen_result_experiment1 \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr10_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr11_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr12_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr13_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr14_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr15_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr16_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr17_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr18_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr19_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr1_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr20_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr21_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr22_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr2_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr3_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr4_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr5_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr6_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr7_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr8_v2.bed" \ + --bed "${UKB_GENOTYPED_DIR}/ukb_cal_chr9_v2.bed" \ + --qCovarCol age \ + --qCovarCol age_x_age \ + --qCovarCol age_x_sex \ + --qCovarCol bmi \ + --qCovarCol genotyping_array \ + --qCovarCol height_cm \ + --qCovarCol height_cm_x_height_cm \ + --qCovarCol model_fold \ + --qCovarCol occasional_smoker \ + --qCovarCol pc1 \ + --qCovarCol pc10 \ + --qCovarCol pc11 \ + --qCovarCol pc12 \ + --qCovarCol pc13 \ + --qCovarCol pc14 \ + --qCovarCol pc15 \ + --qCovarCol pc2 \ + --qCovarCol pc3 \ + --qCovarCol pc4 \ + --qCovarCol pc5 \ + --qCovarCol pc6 \ + --qCovarCol pc7 \ + --qCovarCol pc8 \ + --qCovarCol pc9 \ + --qCovarCol sex \ + --qCovarCol smoker \ + --qCovarCol smoking_pack_per_year \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr10_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr11_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr12_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr13_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr14_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr15_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr16_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr17_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr18_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr19_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr1_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr20_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr21_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr22_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr2_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr3_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr4_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr5_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr6_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr7_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr8_v2.bim" \ + --bim "${UKB_GENOTYPED_DIR}/ukb_cal_chr9_v2.bim" + ``` + + +## [LDSC](https://github.com/bulik/ldsc) + +1. Run munge: + + ```[bash] + BOLT_GWAS_FILE="..." + LDSC_INPUT_DIR="..." + LDSC_OUTPUT_DIR="..." + source activate ldsc && python /opt/ldsc/munge_sumstats.py \ + --sumstats "${BOLT_GWAS_FILE}" \ + --merge-alleles "${LDSC_INPUT_DIR}/w_hm3.snplist" \ + --out "%{LDSC_OUTPUT_DIR}/munge \ + --chunksize 500000 + ``` + +1. Run S-LDSC: + + ```[bash] + source activate ldsc && python /opt/ldsc/ldsc.py \ + --h2 "${LDSC_OUTPUT_DIR}/munge.sumstats.gz" \ + --ref-ld-chr "${LDSC_INPUT_DIR}/baselineLD." \ + --w-ld-chr "${LDSC_INPUT_DIR}/weight." \ + --out "${LDSC_OUTPUT_DIR}/ldsc" + ``` + +## [GARFIELD](https://www.ebi.ac.uk/birney-srv/GARFIELD/) + +1. For each chromosome run: + ```[bash] + GARFIELD_INPUT_DIR="..." + GARFIELD_OUTPUT_DIR="..." + ANNOTATION_LIKE_FILE="..." + INPUT_FILE_P="..." + ./garfield/garfield-prep-chr \ + -ptags "${GARFIELD_INPUT_DIR}/tags/r01/*"\ + -ctags "${GARFIELD_INPUT_DIR}/tags/r08/*" \ + -maftss "${GARFIELD_INPUT_DIR}/maftssd/*"\ + -pval "${INPUT_FILE_P}"\ + -ann "${GARFIELD_INPUT_DIR}/annotation/*"\ + -excl -1\ + -chr "${CHR}" \ + -o "${GARFIELD_OUTPUT_DIR}/tmp_prep_out" + ``` + +1. For each chromosome run: + ```[bash] + Rscript garfield-Meff-Padj.R \ + -i "${GARFIELD_OUTPUT_DIR}/tmp_prep_out"\ + -o "${GARFIELD_OUTPUT_DIR}/tmp_meff_out" + ``` + +1. To compute enrichment: + ```[bash] + Rscript garfield-test.R \ + -i "${GARFIELD_OUTPUT_DIR}/tmp_prep_out" \ + -o "${GARFIELD_OUTPUT_DIR}/tmp_test_out" \ + -l "${ANNOTATION_LIKE_FILE}" \ + -pt 1e-5,1e-8\ + -b m5,n5,t5\ + -s 1-1005 \ + -c 0 + ``` + +1. Plotting + ```[bash] + Rscript garfield-plot.R \ + -i "${GARFIELD_OUTPUT_DIR}/tmp_prep_out" \ + -o "${GARFIELD_OUTPUT_DIR}/tmp_plot_out" \ + -l "${ANNOTATION_LIKE_FILE}" \ + -t " "\ + -f 10 \ + -padj "${PVAL_ADJ}" + ``` + + +## Polygenic risk score (PRS) analysis + +Given the effect sizes computed by BOLT-LMM or by "pruning and thresholding" as +described in the paper, we generated each individual's polygenic risk scores +(PRS) using [PLINK](https://www.cog-genomics.org/plink/2.0/) as follows. + +1. We ran PLINK to compute the PRS by the following command: + ```[bash] + plink \ + --bed $BED_FILE \ + --bim $BIM_FILE \ + --fam $FAM_FILE \ + --read-freq $VARIANT_FREQ_FILE \ + --score ${MODEL_FILE} header sum double-dosage \ + --out $PLINK_OUT + ``` + +1. See `prs_analysis.ipynb` to compute various PRS metrics we use in the paper +using (paired) bootstrapping. From 3dd72efa9588dc6e07058be5ab9087e00a0fc825 Mon Sep 17 00:00:00 2001 From: Taedong Yun Date: Fri, 7 Jun 2024 14:24:04 -0400 Subject: [PATCH 2/4] Add REGLE analysis Colabs --- regle/analysis/embedding_interpretability.ipynb | 1 + regle/analysis/pca_and_spline_fitting.ipynb | 1 + regle/analysis/prs_analysis.ipynb | 1 + 3 files changed, 3 insertions(+) create mode 100644 regle/analysis/embedding_interpretability.ipynb create mode 100644 regle/analysis/pca_and_spline_fitting.ipynb create mode 100644 regle/analysis/prs_analysis.ipynb diff --git a/regle/analysis/embedding_interpretability.ipynb b/regle/analysis/embedding_interpretability.ipynb new file mode 100644 index 0000000..f90420b --- /dev/null +++ b/regle/analysis/embedding_interpretability.ipynb @@ -0,0 +1 @@ +{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"provenance":[],"authorship_tag":"ABX9TyMFWnmmZWzOiBWzQbJD8MHZ"},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"markdown","metadata":{"id":"TQe5CETGcdwz"},"source":["# Download Keras checkpoints from our GitHub repo"]},{"cell_type":"code","execution_count":1,"metadata":{"id":"a1RXc2pKYPtM","colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"status":"ok","timestamp":1717783515535,"user_tz":240,"elapsed":3133,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}},"outputId":"b49a78be-1307-470a-85a8-f25c6d48dfc6"},"outputs":[{"output_type":"stream","name":"stdout","text":["--2024-06-07 18:05:13-- https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/saved_model.pb\n","Resolving github.com (github.com)... 140.82.113.4\n","Connecting to github.com (github.com)|140.82.113.4|:443... connected.\n","HTTP request sent, awaiting response... 302 Found\n","Location: https://raw.githubusercontent.com/Google-Health/genomics-research/main/regle/saved_models/rspincs/saved_model.pb [following]\n","--2024-06-07 18:05:13-- https://raw.githubusercontent.com/Google-Health/genomics-research/main/regle/saved_models/rspincs/saved_model.pb\n","Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.111.133, 185.199.109.133, ...\n","Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected.\n","HTTP request sent, awaiting response... 200 OK\n","Length: 1227084 (1.2M) [application/octet-stream]\n","Saving to: ‘rspincs/saved_model.pb’\n","\n","saved_model.pb 100%[===================>] 1.17M --.-KB/s in 0.06s \n","\n","2024-06-07 18:05:13 (18.6 MB/s) - ‘rspincs/saved_model.pb’ saved [1227084/1227084]\n","\n","--2024-06-07 18:05:13-- https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/keras_metadata.pb\n","Resolving github.com (github.com)... 140.82.114.4\n","Connecting to github.com (github.com)|140.82.114.4|:443... connected.\n","HTTP request sent, awaiting response... 302 Found\n","Location: https://raw.githubusercontent.com/Google-Health/genomics-research/main/regle/saved_models/rspincs/keras_metadata.pb [following]\n","--2024-06-07 18:05:14-- https://raw.githubusercontent.com/Google-Health/genomics-research/main/regle/saved_models/rspincs/keras_metadata.pb\n","Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.108.133, 185.199.111.133, ...\n","Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.\n","HTTP request sent, awaiting response... 200 OK\n","Length: 97736 (95K) [text/plain]\n","Saving to: ‘rspincs/keras_metadata.pb’\n","\n","keras_metadata.pb 100%[===================>] 95.45K --.-KB/s in 0.02s \n","\n","2024-06-07 18:05:14 (4.34 MB/s) - ‘rspincs/keras_metadata.pb’ saved [97736/97736]\n","\n","--2024-06-07 18:05:14-- https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/variables/variables.data-00000-of-00001\n","Resolving github.com (github.com)... 140.82.113.4\n","Connecting to github.com (github.com)|140.82.113.4|:443... connected.\n","HTTP request sent, awaiting response... 302 Found\n","Location: https://raw.githubusercontent.com/Google-Health/genomics-research/main/regle/saved_models/rspincs/variables/variables.data-00000-of-00001 [following]\n","--2024-06-07 18:05:14-- https://raw.githubusercontent.com/Google-Health/genomics-research/main/regle/saved_models/rspincs/variables/variables.data-00000-of-00001\n","Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ...\n","Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected.\n","HTTP request sent, awaiting response... 200 OK\n","Length: 6589814 (6.3M) [application/octet-stream]\n","Saving to: ‘rspincs/variables/variables.data-00000-of-00001’\n","\n","variables.data-0000 100%[===================>] 6.28M --.-KB/s in 0.1s \n","\n","2024-06-07 18:05:15 (44.0 MB/s) - ‘rspincs/variables/variables.data-00000-of-00001’ saved [6589814/6589814]\n","\n","--2024-06-07 18:05:15-- https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/variables/variables.index\n","Resolving github.com (github.com)... 140.82.113.4\n","Connecting to github.com (github.com)|140.82.113.4|:443... connected.\n","HTTP request sent, awaiting response... 302 Found\n","Location: https://raw.githubusercontent.com/Google-Health/genomics-research/main/regle/saved_models/rspincs/variables/variables.index [following]\n","--2024-06-07 18:05:15-- https://raw.githubusercontent.com/Google-Health/genomics-research/main/regle/saved_models/rspincs/variables/variables.index\n","Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ...\n","Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected.\n","HTTP request sent, awaiting response... 200 OK\n","Length: 2223 (2.2K) [application/octet-stream]\n","Saving to: ‘rspincs/variables/variables.index’\n","\n","variables.index 100%[===================>] 2.17K --.-KB/s in 0s \n","\n","2024-06-07 18:05:15 (23.3 MB/s) - ‘rspincs/variables/variables.index’ saved [2223/2223]\n","\n"]}],"source":["!mkdir -p rspincs/variables\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/saved_model.pb -P rspincs/\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/keras_metadata.pb -P rspincs/\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/variables/variables.data-00000-of-00001 -P rspincs/variables/\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/variables/variables.index -P rspincs/variables/"]},{"cell_type":"markdown","metadata":{"id":"hjRXNyKwcy8T"},"source":["# Imports and functions"]},{"cell_type":"code","execution_count":2,"metadata":{"id":"w6MpGCYoSOgt","executionInfo":{"status":"ok","timestamp":1717783528618,"user_tz":240,"elapsed":13086,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["from typing import Optional\n","\n","import matplotlib as mpl\n","import matplotlib.pyplot as plt\n","import numpy as np\n","import tensorflow as tf"]},{"cell_type":"code","execution_count":3,"metadata":{"id":"CTCzhsgYVt3A","executionInfo":{"status":"ok","timestamp":1717783528620,"user_tz":240,"elapsed":14,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["# The example values for the 5 (standardized) spigrogram EDFs:\n","# 'blow_fev1', 'blow_fvc', 'blow_pef', 'blow_ratio', 'blow_fef25_75'\n","EDF_VALUE_EXAMPLE = [-1.8, -1.8, -1.4, -0.7, -1.5]\n","\n","# Note we use 0, 1, ..., 999 for the volume values in flow-volume curves,\n","# which were interpolated between 0 and 6.58.\n","VOLUME_SCALE_FACTOR = 6.58 / 1000\n","\n","\n","def _draw_double_arrow(\n"," ax: mpl.axes.Axes,\n"," x1: float,\n"," x2: float,\n"," y: float,\n"," arrow_color: str = '#d62728',\n","):\n"," \"\"\"Draw an arrow pointing both sides between (x1, y) and (x2, y).\"\"\"\n"," ax.arrow(\n"," x1,\n"," y,\n"," x2 - x1,\n"," 0,\n"," fc=arrow_color,\n"," ec=arrow_color,\n"," width=0.04,\n"," head_width=0.15,\n"," head_length=0.05,\n"," zorder=100,\n"," )\n"," ax.arrow(\n"," x2,\n"," y,\n"," x1 - x2,\n"," 0,\n"," fc=arrow_color,\n"," ec=arrow_color,\n"," width=0.04,\n"," head_width=0.15,\n"," head_length=0.05,\n"," zorder=100,\n"," )\n","\n","\n","def generate_rspincs_reconstruction_plot(\n"," vae_model: tf.keras.Model,\n"," latent_dim: int,\n"," fpath_noext: Optional[str] = None,\n"," dpi=300,\n",") -> None:\n"," \"\"\"Generate reconstructed spirograms while varying each RSPINCs coordinate.\n","\n"," Args:\n"," row: A row of the SPINCs DF from which we'll get the values of manual\n"," features.\n"," vae_model: The VAE model to be used to reconstruct spirograms.\n"," latent_dim: The latent dimension.\n"," fpath_noext: The path to the output image file without extension.\n"," dpi: DPI of the image.\n"," \"\"\"\n"," cmap = plt.get_cmap('viridis')\n"," num_injected_features = 5\n"," radius = 1.5\n"," single_encodings = np.linspace(-radius, radius, num=21)\n"," decoder = vae_model.get_layer(f'{vae_model.name}_decoder')\n"," colorbar_width = 0.2\n","\n"," rescaled_volume = np.arange(1000) * VOLUME_SCALE_FACTOR\n"," _, axs = plt.subplots(\n"," 1,\n"," latent_dim + 1,\n"," figsize=(4 * latent_dim + colorbar_width, 3),\n"," width_ratios=[4] * latent_dim + [colorbar_width],\n"," )\n","\n"," for latent_idx in range(latent_dim):\n"," ax = axs[latent_idx]\n"," for img_idx, single_encoding in enumerate(single_encodings):\n"," # This value should be in [0, 1].\n"," color_val = single_encoding / (radius * 2) + 0.5\n"," encoding = np.zeros(latent_dim)\n"," encoding[latent_idx] = single_encoding\n"," encoding_input = np.expand_dims(encoding, axis=0)\n"," edf_input = np.expand_dims(np.array(EDF_VALUE_EXAMPLE), axis=0)\n"," vae_input = np.concatenate((encoding_input, edf_input), axis=-1)\n"," assert vae_input.shape == (1, latent_dim + num_injected_features)\n"," reconstructed = decoder(vae_input)[0].numpy()[:, 0]\n"," assert len(rescaled_volume) == len(reconstructed)\n"," ax.plot(\n"," rescaled_volume,\n"," reconstructed,\n"," color=cmap(color_val),\n"," alpha=0.9,\n"," linewidth=0.8,\n"," )\n"," ax.set_xlim((-20 * VOLUME_SCALE_FACTOR, 350 * VOLUME_SCALE_FACTOR))\n"," ax.set_ylim((-0.1, 4.2))\n"," ax.set_xlabel('Volume (L)')\n"," # Custom annotation for RSPINCs with dim = 2:\n"," if latent_idx == 0:\n"," ax.set_ylabel('Flow (L/s)')\n"," _draw_double_arrow(\n"," ax, 50 * VOLUME_SCALE_FACTOR, 140 * VOLUME_SCALE_FACTOR, 3\n"," )\n"," elif latent_idx == 1:\n"," _draw_double_arrow(\n"," ax, 5 * VOLUME_SCALE_FACTOR, 40 * VOLUME_SCALE_FACTOR, 3\n"," )\n"," ax.set_title('$\\mathrm{RSPINC}_' + f'{latent_idx + 1}$')\n"," # Draw a color palette on the last axis.\n"," cbar = plt.colorbar(\n"," mpl.cm.ScalarMappable(\n"," norm=mpl.colors.Normalize(vmin=-radius, vmax=radius), cmap=cmap\n"," ),\n"," cax=axs[-1],\n"," )\n"," cbar.ax.set_xlabel('Coordinate\\nValue')\n"," plt.tight_layout()\n"," plt.show()"]},{"cell_type":"markdown","metadata":{"id":"ols2RVM8c1sh"},"source":["# Load model and generate spirograms from embedding coordinate perturbation"]},{"cell_type":"code","execution_count":4,"metadata":{"id":"BX0g763-ZrLr","executionInfo":{"status":"ok","timestamp":1717783532267,"user_tz":240,"elapsed":3657,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["rspincs_model = tf.keras.models.load_model('rspincs')"]},{"cell_type":"code","execution_count":5,"metadata":{"id":"_2nYHVXhr6uT","colab":{"base_uri":"https://localhost:8080/","height":307},"executionInfo":{"status":"ok","timestamp":1717783534569,"user_tz":240,"elapsed":2324,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}},"outputId":"f6714e48-aa52-49ec-8860-e4e011dd3235"},"outputs":[{"output_type":"display_data","data":{"text/plain":["
"],"image/png":"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rxf/+te/Gvwc/la99M477whN08S6devExo0bG2yHogewWfeoRKNRlixZwogRI3bvU1WVESNGsHDhwt88Z+HChQ2OBxg5cuQej2/K9qf8sGtF0VatWpGfn8/o0aNZvXr1oYjbJDSn7/+B6N27Nzk5OZx44onMnz8/0XEazX8Wo0pNTd3jMfJnYN8c6fUsyLp2fzS3n4H9JevapvkzkJqaysiRI3nxxRd/s1eopqaGLl26UFRURFFR0e79a9asoaamhq5duwLQpUuXX31f58+fv/vre9KlSxcWLVrUYN9/BvDvryVLlmBZFk8++SRHH300HTt2ZOfOnQ2OsdlsmKbZYF+fPn0wTZOysjLat2/fYMvOzj6gTHujWTdUKioqME2TrKysBvuzsrIoKSn5zXNKSkr26fimbH/K36lTJ1599VU++eQT3njjDSzLYvDgwWzfvv1QRE64PX3//X4/oVAoQakOnZycHF566SU+/PBDPvzwQ/Lz8xk+fDhLly5NdLQDZlkWEyZMYMiQIXTv3n2PxzWnOuBQONLrWZB17f6Qda2sa5t6PfDiiy9imiYDBgzgww8/ZOPGjaxdu5bnnnuOQYMGMWLECHr06MHFF1/M0qVLWbx4MWPHjuXYY4/d/SrXrbfeymuvvcbkyZPZuHEjTz31FNOmTds9SH1PbrzxRl599VWmTJnChg0buPfeew/4Dxnt27cnFovx/PPPs3nzZv7973/z0ksvNTimdevW1NfXM2vWLCoqKggGg3Ts2JGLL76YsWPHMm3aNLZs2cLixYuZNGkSX3zxxQFl2htyHRWpgUGDBjV4D3Lw4MF06dKFl19+mQceeCCByaRDoVOnTnTq1Gn358GDB1NQUMDTTz/9qykVDzfXXnstq1atYt68eYmOIkmyrj3Cybq26Wvbti1Lly7loYce4uabb6a4uJiMjAz69evH5MmTURSFTz75hOuvv55hw4ahqiqjRo3i+eef332NMWPG8Oyzz/LEE09w44030qZNG6ZMmcLw4cN/997nn38+BQUF3HbbbYTDYc4++2yuvvrqAxrD1atXL5566ikeffRR7rzzToYNG8akSZMYO3bs7mMGDx7MVVddxfnnn09lZSX33nsv9913H1OmTOHBBx/k5ptvZseOHaSnp3P00Udz2mmn7XeevdWsGyrp6elomvarOa1LS0v32F2VnZ29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\n"},"metadata":{}}],"source":["generate_rspincs_reconstruction_plot(\n"," vae_model=rspincs_model,\n"," latent_dim=2,\n",")"]}]} \ No newline at end of file diff --git a/regle/analysis/pca_and_spline_fitting.ipynb b/regle/analysis/pca_and_spline_fitting.ipynb new file mode 100644 index 0000000..b7f90a8 --- /dev/null +++ b/regle/analysis/pca_and_spline_fitting.ipynb @@ -0,0 +1 @@ +{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"provenance":[],"authorship_tag":"ABX9TyOXk/XH0+SqGWRKkccIsj6v"},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"code","execution_count":1,"metadata":{"id":"pa_dhHReC5dH","executionInfo":{"status":"ok","timestamp":1717783677268,"user_tz":240,"elapsed":2316,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["import numpy as np\n","import pandas as pd\n","import scipy\n","from sklearn import decomposition"]},{"cell_type":"markdown","metadata":{"id":"BVm0PPlJHCjX"},"source":["# PCA"]},{"cell_type":"markdown","metadata":{"id":"XsedyAXiHgDM"},"source":["For PCA we require population-level data. We assume `data_matrix` is a Pandas dataframe whose rows correspond to individuals and columns correspond to data points. We simulate this data in this notebook as we don't have access to the real population-level data."]},{"cell_type":"code","execution_count":2,"metadata":{"id":"eJFBpnleHBqS","executionInfo":{"status":"ok","timestamp":1717783677917,"user_tz":240,"elapsed":654,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["np.random.seed(42)\n","data_matrix = pd.DataFrame(np.random.normal(size=(10000, 1000)))"]},{"cell_type":"code","execution_count":3,"metadata":{"id":"AFbcJIqiHyg7","executionInfo":{"status":"ok","timestamp":1717783677919,"user_tz":240,"elapsed":10,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["def standardize_df(df: pd.DataFrame) -> pd.DataFrame:\n"," \"\"\"Standardizes a dataframe (mean=0, var=1).\"\"\"\n"," return (df - df.mean()) / df.std(ddof=0)\n","\n","\n","def generate_pc(\n"," data_matrix: pd.DataFrame, num_pc: int, standardize: bool = True\n",") -> pd.DataFrame:\n"," \"\"\"Generates principal components (PCs) of the given data matrix.\n","\n"," Args:\n"," data_matrix: The data matrix.\n"," num_pc: The number of PCs to compute.\n"," standardize: True to standardize the data matrix before computing PCs.\n","\n"," Returns:\n"," A matrix of PCs of the data matrix.\n"," \"\"\"\n"," original_shape = data_matrix.shape\n"," if standardize:\n"," data_matrix = standardize_df(data_matrix)\n"," # Replace NaN values with 0 (this can happen when some col has var=0).\n"," data_matrix.fillna(0, inplace=True)\n"," assert data_matrix.shape == original_shape\n"," pca = decomposition.PCA(num_pc)\n"," pc_np = pca.fit_transform(data_matrix)\n"," print('PCA explained variance:', pca.explained_variance_)\n"," print(\n"," 'PCA explained variance (proportion):',\n"," pca.explained_variance_ / np.sum(pca.explained_variance_),\n"," )\n"," assert pc_np.shape == (original_shape[0], num_pc)\n"," return pd.DataFrame(pc_np)"]},{"cell_type":"code","execution_count":4,"metadata":{"colab":{"height":241,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":2381,"status":"ok","timestamp":1717783680293,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"zlBLtbM4IQ53","outputId":"51512bd5-3814-467a-fcda-8aad45ac220f"},"outputs":[{"output_type":"stream","name":"stdout","text":["PCA explained variance: [1.63972209 1.63070323 1.62260396 1.61134043 1.590792 ]\n","PCA explained variance (proportion): [0.20255582 0.20144171 0.2004412 0.19904981 0.19651145]\n"]},{"output_type":"execute_result","data":{"text/plain":[" 0 1 2 3 4\n","0 -2.371899 -0.643403 -0.397528 0.505243 -1.672120\n","1 -0.389563 -0.316097 -0.054947 -1.539366 -0.998421\n","2 -0.278895 -1.904815 0.019068 -0.700896 0.973568\n","3 3.261174 -0.036879 2.362755 -1.733982 0.587677\n","4 0.172324 0.537071 -0.351281 -1.236673 1.708548"],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
01234
0-2.371899-0.643403-0.3975280.505243-1.672120
1-0.389563-0.316097-0.054947-1.539366-0.998421
2-0.278895-1.9048150.019068-0.7008960.973568
33.261174-0.0368792.362755-1.7339820.587677
40.1723240.537071-0.351281-1.2366731.708548
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n"," \n","\n","\n","\n"," \n","
\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","variable_name":"pc_dataframe","summary":"{\n \"name\": \"pc_dataframe\",\n \"rows\": 10000,\n \"fields\": [\n {\n \"column\": 0,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2805163386985488,\n \"min\": -4.643045981269673,\n \"max\": 5.017698894439442,\n \"num_unique_values\": 10000,\n \"samples\": [\n -0.3224716522127656,\n 0.6031338243822927,\n -1.2993299471423263\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 1,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2769899114607324,\n \"min\": -4.448045764841815,\n \"max\": 5.101647474079014,\n \"num_unique_values\": 10000,\n \"samples\": [\n 0.286864855151227,\n -0.6597526194886669,\n -0.4896683064067677\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 2,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.273814728228805,\n \"min\": -4.328973725102052,\n \"max\": 4.872664420026113,\n \"num_unique_values\": 10000,\n \"samples\": [\n -0.6794583220950966,\n 1.9140526678288383,\n -0.4004464395670121\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 3,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2693858467445038,\n \"min\": -4.939769834236929,\n \"max\": 4.99450956625324,\n \"num_unique_values\": 10000,\n \"samples\": [\n 2.225402267644631,\n -0.9588695150842595,\n 1.2924768168268101\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 4,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2612660288538398,\n \"min\": -5.007116466188265,\n \"max\": 5.3472410625736035,\n \"num_unique_values\": 10000,\n \"samples\": [\n 0.19752305167345738,\n -1.0272444388147874,\n -0.010101932326369557\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":4}],"source":["pc_dataframe = generate_pc(\n"," data_matrix,\n"," num_pc=5)\n","\n","pc_dataframe.head()"]},{"cell_type":"markdown","metadata":{"id":"j9tneSsvG5vg"},"source":["# Spline fitting"]},{"cell_type":"code","execution_count":5,"metadata":{"id":"dALiJbUGDghc","executionInfo":{"status":"ok","timestamp":1717783680294,"user_tz":240,"elapsed":15,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["def compute_spline_coefficients(\n"," arr: np.ndarray, knot_position: int\n",") -> np.ndarray:\n"," \"\"\"Gets cubic spline coefficients with a single knot.\n","\n"," We use a single knot which is padded by 4 (= k + 1) boundaries on each side,\n"," where k=3 (cubic) is the degree in this case.\n","\n"," The results are 5 coefficients padded by 4 zeros at the end. We remove the\n"," last 4 zeros.\n","\n"," For more details, see https://en.wikipedia.org/wiki/B-spline and\n"," https://docs.scipy.org/doc/scipy/tutorial/interpolate/smoothing_splines.html#procedural-splrep\n","\n"," Args:\n"," arr: The target numpy array for 1D spline fitting.\n"," knot_position: The position of the single knot.\n","\n"," Returns:\n"," A numpy array of 5 cubic spline coefficients.\n"," \"\"\"\n"," num_points = len(arr)\n"," assert arr.shape == (num_points,)\n"," assert 0 < knot_position < num_points - 1\n"," spline = scipy.interpolate.splrep(\n"," x=np.arange(num_points),\n"," y=arr,\n"," k=3,\n"," task=-1,\n"," t=[knot_position],\n"," )\n"," bspline_coefficients = spline[1]\n"," assert np.array_equal(bspline_coefficients[5:], np.array([0, 0, 0, 0]))\n"," return bspline_coefficients[:5]"]},{"cell_type":"code","execution_count":6,"metadata":{"id":"JPYKbetRCGs5","executionInfo":{"status":"ok","timestamp":1717783680294,"user_tz":240,"elapsed":13,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["MAX_NUM_POINTS = 1000\n","VOLUME_SCALE_FACTOR = 0.001\n","KNOT_POSITION = 199"]},{"cell_type":"markdown","metadata":{"id":"l7XaODNrEXgU"},"source":["`example_curve` variable below should be a 1D numpy array that contains a single curve, such as a spirogram.\n","\n","Here we use an example curve copied from a UK Biobank example at https://biobank.ctsu.ox.ac.uk/crystal/ukb/examples/eg_spiro_3066.dat"]},{"cell_type":"code","execution_count":7,"metadata":{"id":"Dur9LHMQD_B3","executionInfo":{"status":"ok","timestamp":1717783680294,"user_tz":240,"elapsed":12,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["example_curve_txt = '0,0,0,0,3,10,25,54,101,169,258,363,478,589,689,785,879,970,1059,1147,1234,1320,1403,1486,1569,1650,1730,1809,1888,1965,2040,2116,2188,2261,2331,2400,2465,2532,2595,2658,2720,2780,2838,2894,2948,3001,3052,3102,3151,3197,3243,3287,3329,3371,3412,3451,3490,3527,3564,3600,3635,3670,3703,3736,3769,3800,3831,3861,3890,3918,3947,3974,4001,4028,4054,4080,4105,4130,4154,4179,4202,4226,4249,4271,4292,4312,4332,4351,4371,4390,4408,4426,4444,4461,4478,4495,4512,4528,4544,4560,4575,4590,4604,4619,4633,4647,4661,4675,4689,4703,4716,4729,4742,4755,4767,4779,4791,4802,4812,4822,4831,4840,4849,4857,4866,4874,4882,4890,4898,4906,4914,4921,4929,4936,4944,4951,4958,4966,4973,4980,4987,4994,5000,5007,5013,5020,5026,5033,5039,5045,5051,5057,5063,5069,5075,5081,5087,5092,5098,5104,5109,5114,5119,5125,5130,5134,5139,5144,5148,5153,5157,5161,5166,5170,5174,5178,5182,5186,5190,5194,5198,5202,5205,5209,5213,5216,5220,5223,5226,5230,5233,5236,5240,5243,5246,5250,5253,5256,5259,5262,5264,5267,5270,5273,5276,5279,5283,5286,5289,5292,5295,5298,5300,5303,5306,5308,5311,5314,5316,5319,5321,5323,5326,5328,5331,5333,5335,5338,5340,5343,5345,5348,5350,5352,5355,5357,5360,5362,5365,5367,5369,5372,5374,5377,5379,5381,5384,5386,5388,5390,5391,5393,5395,5397,5399,5401,5403,5404,5406,5408,5410,5412,5413,5415,5417,5419,5420,5422,5424,5426,5427,5429,5431,5432,5434,5436,5438,5439,5441,5443,5444,5446,5447,5449,5450,5452,5453,5455,5456,5457,5459,5460,5461,5462,5463,5464,5466,5467,5468,5470,5471,5473,5474,5476,5477,5478,5480,5481,5482,5484,5485,5486,5487,5489,5490,5491,5492,5493,5494,5496,5497,5498,5499,5500,5501,5502,5503,5504,5505,5506,5507,5508,5509,5510,5510,5511,5512,5513,5514,5515,5515,5516,5517,5519,5520,5521,5523,5524,5525,5527,5529,5530,5532,5533,5535,5536,5537,5539,5540,5541,5543,5544,5545,5545,5546,5547,5548,5549,5549,5550,5551,5552,5552,5553,5554,5554,5555,5556,5557,5557,5558,5559,5560,5560,5561,5562,5562,5563,5564,5564,5565,5565,5566,5567,5567,5568,5569,5570,5571,5572,5573,5574,5576,5577,5578,5579,5580,5582,5583,5584,5585,5587,5588,5589,5590,5591,5591,5592,5593,5594,5595,5596,5596,5597,5598,5598,5599,5600,5601,5601,5602,5603,5603,5604,5605,5606,5606,5607,5608,5608,5609,5609,5609,5610,5611,5611,5612,5613,5613,5614,5615,5616,5616,5617,5618,5618,5619,5620,5621,5622,5623,5624,5624,5625,5626,5626,5627,5628,5628,5629,5629,5630,5630,5631,5632,5632,5633,5633,5634,5635,5635,5636,5637,5637,5638,5639,5639,5640,5641,5642,5642,5643,5644,5645,5645,5646,5647,5647,5648,5649,5649,5650,5651,5651,5652,5652,5653,5654,5654,5655,5656,5656,5657,5658,5658,5659,5660,5660,5661,5661,5662,5663,5663,5664,5664,5665,5665,5666,5666,5667,5667,5668,5668,5669,5669,5670,5670,5670,5671,5671,5672,5672,5672,5673,5673,5673,5673,5674,5674,5674,5675,5676,5676,5677,5677,5678,5678,5679,5679,5680,5681,5681,5682,5683,5683,5684,5684,5685,5686,5686,5687,5687,5688,5688,5688,5689,5689,5690,5690,5690,5691,5691,5692,5692,5692,5693,5693,5694,5694,5694,5695,5695,5695,5696,5696,5696,5696,5696,5696,5697,5697,5698,5698,5698,5699,5699,5699,5699,5700,5700,5700,5701,5701,5702,5702,5703,5703,5704,5704,5705,5705,5706,5706,5707,5707,5708,5709,5709,5710,5710,5711,5711,5712,5712,5712,5713,5713,5713,5714,5714,5714,5715,5715,5716,5716,5716,5717,5717,5717,5718,5718,5719,5719,5720,5720,5721,5721,5721,5722,5722,5722,5723,5723,5723,5723,5724,5724,5724,5725,5725,5725,5726,5726,5726,5727,5727,5728,5728,5729,5729,5729,5730,5730,5731,5732,5732,5733,5733,5734,5735,5735,5735,5736,5736,5736,5737,5737,5737,5738,5738,5738,5739,5739,5739,5739,5740,5740,5740,5741,5741,5741,5741,5741,5741,5742,5742,5742,5742,5742,5742,5742,5742,5742,5742,5741,5741,5740,5740,5740,5740,5739,5739,5739,5739,5739,5739,5740,5740,5740,5741,5742,5742,5743,5743,5744,5745,5745,5745,5746,5746,5747,5747,5748,5748,5748,5748,5748,5748,5749,5749,5749,5749,5749,5749,5749,5750,5750,5750,5750,5750,5751,5751,5751,5752,5752,5753,5753,5754,5754,5754,5755,5755,5756,5756,5756,5757,5757,5757,5758,5758,5758,5758,5759,5759,5759,5759,5759,5759,5759,5759,5759,5760,5760,5760,5761,5761,5761,5762,5762,5763,5763,5763,5764,5764,5764,5765,5765,5766,5766,5766,5767,5767,5767,5767,5767,5768,5768,5768,5768,5769,5769,5769,5770,5770,5770,5770,5770,5771,5771,5771,5771,5771,5772,5772,5772,5773,5773,5773,5774,5774,5774,5775,5775,5775,5776,5776,5777,5777,5777,5778,5778,5778,5778,5779,5779,5779,5779,5779,5779,5779,5779,5779,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5780,5780,5780,5780,5781,5781,5781,5782,5782,5782,5783,5783,5783,5784,5784,5784,5785,5785,5785,5785,5785,5786,5786,5786,5786,5786,5786,5786,5787,5787,5787,5788,5788,5788,5789,5789,5789,5790,5790,5790,5791,5791,5792,5792,5792,5793,5793,5793,5794,5794,5795,5795,5795,5796,5796,5796,5797,5797,5798,5798,5798,5798,5798,5799,5799,5799,5799,5800,5800,5800,5801,5801,5801,5801,5802,5802,5802,5802,5803,5803,5803,5803,5803,5803,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5803,5804,5804,5804,5804,5804,5805,5805,5805,5805,5806,5806,5806,5806,5806,5806,5806,5806,5806,5806,5807,5807,5807,5807,5808,5808,5809,5809,5809,5810,5810,5810,5811,5811,5812,5812,5813,5813,5813,5814,5814,5815,5815,5815,5815,5816,5816,5816,5816,5817,5817,5817,5817,5817,5817,5817,5818,5818,5818,5818,5818,5818,5818,5819,5819,5819,5819,5819,5819,5819,5819,5819,5819,5820,5820,5820,5820,5820,5820,5820,5820,5820,5819,5820,5820,5820,5820,5820,5820,5820,5820,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5820,5820,5820,5819,5819,5818,5818,5818,5817,5817,5817,5816,5816,5816,5816,5815,5815,5815,5816,5816,5816,5817,5817,5818,5819,5819,5820,5821,5822,5823,5823,5824,5825,5826,5827,5827,5828,5828,5829,5829,5829,5830,5830,5831,5831,5831,5831,5831,5832,5831,5832,5832,5832,5832,5832,5832,5832,5833,5833,5833,5833,5833,5833,5833,5834,5834,5834,5834,5834,5835,5835,5835,5835,5835,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5835,5835,5835,5835,5834,5834,5834,5834,5833,5833,5833,5833,5833,5832,5832,5832,5832,5832,5832,5832,5832,5831'\n","example_curve = (\n"," np.array(example_curve_txt.split(',')[:MAX_NUM_POINTS], dtype=np.float32)\n"," * VOLUME_SCALE_FACTOR\n",")"]},{"cell_type":"markdown","metadata":{"id":"YHiRGraVEhBf"},"source":["The following code generates the 5 spline coefficients the this curve."]},{"cell_type":"code","execution_count":8,"metadata":{"executionInfo":{"elapsed":13,"status":"ok","timestamp":1717783680295,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"Emoh7tdNCQPv","outputId":"40138b4d-87f6-42b2-ce87-37d4e9cfec99","colab":{"base_uri":"https://localhost:8080/"}},"outputs":[{"output_type":"stream","name":"stdout","text":["[-0.08101105 5.14773236 5.63775992 5.81692895 5.78074777]\n"]}],"source":["print(\n"," compute_spline_coefficients(arr=example_curve, knot_position=KNOT_POSITION)\n",")"]}]} \ No newline at end of file diff --git a/regle/analysis/prs_analysis.ipynb b/regle/analysis/prs_analysis.ipynb new file mode 100644 index 0000000..960c327 --- /dev/null +++ b/regle/analysis/prs_analysis.ipynb @@ -0,0 +1 @@ +{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"provenance":[],"authorship_tag":"ABX9TyMyYVUUHcnCAGY5yxymQ6+C"},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"markdown","metadata":{"id":"VbyGa_IhXRgk"},"source":["# Preparation\n","\n","This section includes imports and functions."]},{"cell_type":"code","execution_count":1,"metadata":{"id":"otMyZHIW0Fqs","executionInfo":{"status":"ok","timestamp":1717783770114,"user_tz":240,"elapsed":2320,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["import dataclasses\n","from typing import Dict, List, Optional, Sequence, Union\n","\n","import abc\n","from typing import Callable\n","\n","import numpy as np\n","import pandas as pd\n","import scipy.stats\n","import sklearn\n","import sklearn.metrics\n","from sklearn import metrics"]},{"cell_type":"code","execution_count":2,"metadata":{"id":"J8pr2zMLzmDH","executionInfo":{"status":"ok","timestamp":1717783770322,"user_tz":240,"elapsed":211,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["# A function that computes a numeric outcome from label and prediction arrays.\n","BootstrappableFn = Callable[[np.ndarray, np.ndarray], float]\n","\n","# Constants denoting the expected case and control values for binary encodings.\n","BINARY_LABEL_CONTROL = 0\n","BINARY_LABEL_CASE = 1\n","\n","class Metric(abc.ABC):\n"," \"\"\"Represents a callable wrapper class for a named metric function.\n","\n"," Attributes:\n"," name: The metric's name.\n"," \"\"\"\n","\n"," def __init__(self, name: str, fn: BootstrappableFn) -> None:\n"," \"\"\"Initializes the metric.\n","\n"," Args:\n"," name: The metric's name.\n"," fn: A function that computes an outcome from label and prediction arrays.\n"," The function's signature should accept a `y_true` label array and a\n"," `y_pred` model prediction array. This function is invoked when the\n"," `Metric` instance is called.\n"," \"\"\"\n"," self._name: str = name\n"," self._fn: BootstrappableFn = fn\n","\n"," @property\n"," def name(self) -> str:\n"," \"\"\"The `Metric`'s name.\"\"\"\n"," return self._name\n","\n"," @abc.abstractmethod\n"," def _validate(self, y_true: np.ndarray, y_pred: np.ndarray) -> None:\n"," \"\"\"Validates the `y_true` labels and `y_pred` predictions.\n","\n"," Note: Each prediction subarray `y_pred[i, ...]` at index `i` should\n"," correspond to the `y_true[i]` label.\n","\n"," Args:\n"," y_true: The ground truth label targets.\n"," y_pred: The target predictions.\n","\n"," Raises:\n"," ValueError: If the first dimension of `y_true` and `y_pred` do not match.\n"," \"\"\"\n"," if y_true.shape[0] != y_pred.shape[0]:\n"," raise ValueError('`y_true` and `y_pred` first dimension mismatch: '\n"," f'{y_true.shape[0]} != {y_pred.shape[0]}')\n","\n"," def __call__(self, y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Invokes the `Metric`'s function.\n","\n"," Args:\n"," y_true: The ground truth label values.\n"," y_pred: The target predictions.\n","\n"," Returns:\n"," The result of the `Metric.fn(y_true, y_pred)`.\n"," \"\"\"\n"," self._validate(y_true, y_pred)\n"," return self._fn(y_true, y_pred)\n","\n"," def __str__(self) -> str:\n"," return self.name\n","\n","\n","class ContinuousMetric(Metric):\n"," \"\"\"Represents a callable wrapper class for a named continuous label function.\n","\n"," Attributes:\n"," name: The metric's name.\n"," \"\"\"\n","\n"," # Note: This is a useful delegation since _validate is an @abc.abstractmethod.\n"," def _validate( # pylint: disable=useless-super-delegation\n"," self,\n"," y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," ) -> None:\n"," \"\"\"Validates the `y_true` labels and `y_pred` predictions.\n","\n"," Args:\n"," y_true: The ground truth label values.\n"," y_pred: The target predictions.\n","\n"," Raises:\n"," ValueError: If the first dimension of `y_true` and `y_pred` do not match.\n"," \"\"\"\n"," super()._validate(y_true, y_pred)\n","\n","\n","class BinaryMetric(Metric):\n"," \"\"\"Represents a callable wrapper class for a named binary label function.\n","\n"," This class asserts that the provided `y_true` labels are binary targets in\n"," `{0, 1}` and that `y_true` contains at least one element in each class, i.e.,\n"," not all samples are from the same class.\n","\n"," Attributes:\n"," name: The metric's name.\n"," \"\"\"\n","\n"," def _validate(self, y_true: np.ndarray, y_pred: np.ndarray) -> None:\n"," \"\"\"Validates the `y_true` labels and `y_pred` predictions.\n","\n"," Args:\n"," y_true: The ground truth label values.\n"," y_pred: The target predictions.\n","\n"," Raises:\n"," ValueError: If the first dimension of `y_true` and `y_pred` do not match.\n"," ValueError: If `y_true` labels are nonbinary, i.e., not all values are in\n"," `{BINARY_LABEL_CONTROL, BINARY_LABEL_CASE}` or if `y_true` does not\n"," contain at least one element from each class.\n"," \"\"\"\n"," super()._validate(y_true, y_pred)\n"," if not is_valid_binary_label(y_true):\n"," raise ValueError('`y_true` labels must be in `{BINARY_LABEL_CONTROL, '\n"," 'BINARY_LABEL_CASE}` and have at least one element from '\n"," f'each class; found: {y_true}')\n","\n","\n","def is_binary(metric: Metric) -> bool:\n"," \"\"\"Whether `metric` is a metric computed with binary `y_true` labels.\"\"\"\n"," return isinstance(metric, BinaryMetric)\n","\n","\n","def is_valid_binary_label(array: np.ndarray) -> bool:\n"," \"\"\"Whether `array` is a \"valid\" binary label array for bootstrapping.\n","\n"," We define a valid binary label array as an array that contains only binary\n"," values, i.e., `{BINARY_LABEL_CONTROL, BINARY_LABEL_CASE}`, and contains at\n"," least one value from each class.\n","\n"," Args:\n"," array: A numpy array.\n","\n"," Returns:\n"," Whether `array` is a \"valid\" binary label array.\n"," \"\"\"\n"," is_case_mask = array == BINARY_LABEL_CASE\n"," is_control_mask = array == BINARY_LABEL_CONTROL\n"," return (np.any(is_case_mask) and np.any(is_control_mask) and\n"," np.all(np.logical_or(is_case_mask, is_control_mask)))\n","\n","\n","def pearsonr(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the Pearson R correlation coefficient.\"\"\"\n"," # Note: We ignore the returned p value.\n"," r, _ = scipy.stats.pearsonr(y_true, y_pred)\n"," return r\n","\n","\n","def pearsonr_squared(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the square of the Pearson correlation coefficient.\"\"\"\n"," return pearsonr(y_true, y_pred)**2\n","\n","\n","def spearmanr(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the Spearman R correlation coefficient.\"\"\"\n"," # Note: We ignore the returned p value.\n"," r, _ = scipy.stats.spearmanr(y_true, y_pred)\n"," return r\n","\n","\n","def count(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the number of samples in `y_true`.\"\"\"\n"," if y_true.shape[0] != y_pred.shape[0]:\n"," raise ValueError('`y_true` and `y_pred` first dimension mismatch: '\n"," f'{y_true.shape[0]} != {y_pred.shape[0]}')\n"," return len(y_true)\n","\n","\n","def frequency_between(y_true: np.ndarray, y_pred: np.ndarray,\n"," percentile_lower: int, percentile_upper: int) -> float:\n"," \"\"\"Computes the positive class frequency within a percentile interval.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," percentile_lower: The lower bound (inclusive) of percentile. 0 to include\n"," all samples.\n"," percentile_upper: The upper bound (inclusive for 100, exclusive for all\n"," other values) of percentile. 100 to include all samples.\n","\n"," Returns:\n"," A [0.0, 1.0] float corresponding to the positive class frequency within\n"," the percentile interval.\n","\n"," Raises:\n"," ValueError: Invalid percentile range.\n"," \"\"\"\n"," if not 0 <= percentile_lower < 100:\n"," raise ValueError('`percentile_lower` must be in range `[0, 100)`: '\n"," f'{percentile_lower}')\n"," if not 0 < percentile_upper <= 100:\n"," raise ValueError('`percentile_upper` must be in range `(0, 100]`: '\n"," f'{percentile_upper}')\n","\n"," pred_lower_percentile, pred_upper_percentile = np.percentile(\n"," a=y_pred, q=[percentile_lower, percentile_upper])\n"," lower_mask = (y_pred >= pred_lower_percentile)\n"," if percentile_upper == 100:\n"," mask = lower_mask\n"," else:\n"," upper_mask = (y_pred < pred_upper_percentile)\n"," mask = lower_mask & upper_mask\n"," assert len(mask) == len(y_true)\n"," return np.mean(y_true[mask])\n","\n","\n","def frequency(y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," top_percentile: int = 100) -> float:\n"," \"\"\"Computes the positive class frequency within the top prediction percentile.\n","\n"," We select the subset of `y_true` labels corresponding to `y_pred`'s\n"," `top_percentile`-th prediction percetile and return the positive class\n"," frequency within this subset. `top_percentile=100` indicates the frequency for\n"," all samples.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," top_percentile: Determines the set of examples considered in the frequency\n"," calculation. The top percentile represents the top percentile by\n"," prediction risk. 100 indicates using all samples.\n","\n"," Returns:\n"," A [0.0, 1.0] float corresponding to the positive class frequency in the top\n"," percentile.\n","\n"," Raises:\n"," ValueError: `top_percentile` is not in range `(0, 100]`.\n"," \"\"\"\n"," if not 0 < top_percentile <= 100:\n"," raise ValueError('`top_percentile` must be in range `(0, 100]`: '\n"," f'{top_percentile}')\n","\n"," return frequency_between(\n"," y_true,\n"," y_pred,\n"," percentile_lower=100 - top_percentile,\n"," percentile_upper=100)\n","\n","\n","def frequency_fn(top_percentile: int) -> BootstrappableFn:\n"," \"\"\"Returns a function that computes `frequency` at `top_percentile`.\"\"\"\n","\n"," def _frequency(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," return frequency(y_true, y_pred, top_percentile)\n","\n"," return _frequency\n","\n","\n","def frequency_between_fn(percentile_lower: int,\n"," percentile_upper: int) -> BootstrappableFn:\n"," \"\"\"Returns a function that computes `frequency` in a percentile interval.\"\"\"\n","\n"," def _freq_between(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," return frequency_between(\n"," y_true,\n"," y_pred,\n"," percentile_lower=percentile_lower,\n"," percentile_upper=percentile_upper)\n","\n"," return _freq_between"]},{"cell_type":"code","execution_count":3,"metadata":{"id":"M33VPEMF0sGd","executionInfo":{"status":"ok","timestamp":1717783770322,"user_tz":240,"elapsed":4,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["# Represents a numpy array of indices for a single bootstrap sample.\n","IndexSample = np.ndarray\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class NamedArray:\n"," \"\"\"Represents a named numpy array.\n","\n"," Attributes:\n"," name: The array name.\n"," values: A numpy array.\n"," \"\"\"\n","\n"," name: str\n"," values: np.ndarray\n","\n"," def __post_init__(self):\n"," if not self.name:\n"," raise ValueError('`name` must be specified.')\n","\n"," def __len__(self) -> int:\n"," return len(self.values)\n","\n"," def __str__(self) -> str:\n"," return f'{self.__class__.__name__}({self.name})'\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class Label(NamedArray):\n"," \"\"\"Represents a named numpy array of ground truth label targets.\n","\n"," Attributes:\n"," name: The label name.\n"," values: A numpy array containing ground truth label targets.\n"," \"\"\"\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class Prediction(NamedArray):\n"," \"\"\"Represents a named numpy array of target predictions.\n","\n"," Attributes:\n"," model_name: The name of the model that generated the predictions.\n"," name: The name of the predictions (e.g., the prediction column).\n"," values: A numpy array containing model predictions.\n"," \"\"\"\n","\n"," model_name: str\n","\n"," def __post_init__(self):\n"," super().__post_init__()\n"," if not self.model_name:\n"," raise ValueError('`model_name` must be specified.')\n","\n"," def __str__(self) -> str:\n"," return f'{self.__class__.__name__}({self.model_name}.{self.name})'\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class SampleMean:\n"," \"\"\"Represents an estimate of the population mean for a given sample.\n","\n"," Attributes:\n"," mean: The mean of a given sample.\n"," stddev: The standard deviation of the sample mean.\n"," num_samples: The number of samples used to calculate `mean` and `stddev`.\n","\n"," Raises:\n"," ValueError: If `num_samples` is not >= `1`.\n"," ValueError: If `stddev` is not `0` when `num_samples` is `1`.\n"," \"\"\"\n","\n"," mean: float\n"," stddev: float\n"," num_samples: int\n","\n"," def __post_init__(self):\n"," # Ensure we have a valid number of samples.\n"," if self.num_samples < 1:\n"," raise ValueError(f'`num_samples` must be >= `1`: {self.num_samples}')\n","\n"," # Ensure the standard deviation is 0 given a single sample.\n"," if self.num_samples == 1 and self.stddev != 0.0:\n"," raise ValueError(\n"," f'`stddev` must be `0` if `num_samples` is `1`: {self.stddev:0.4f}'\n"," )\n","\n"," def __str__(self) -> str:\n"," return f'{self.mean:0.4f} (SD={self.stddev:0.4f}, n={self.num_samples})'\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class ConfidenceInterval(SampleMean):\n"," \"\"\"Represents a confidence interval (CI) for a sample mean.\n","\n"," Attributes:\n"," mean: The mean of a given sample.\n"," stddev: The standard deviation of the sample mean.\n"," num_samples: The number of samples used to calculate `mean` and `stddev`.\n"," level: The confidence level at which the CI is calculated (e.g., 95).\n"," ci_lower: The lower limit of the `level` confidence interval.\n"," ci_upper: The upper limit of the `level` confidence interval.\n","\n"," Raises:\n"," ValueError: If `num_samples` is not >= `1`.\n"," ValueError: If `stddev` is not `0` when `num_samples` is `1`.\n"," ValueError: If `level` is not in range (0, 100].\n"," ValueError: If `ci_lower` or `ci_upper` does not match not `mean` when\n"," `num_samples` is `1`.\n"," \"\"\"\n","\n"," level: float\n"," ci_lower: float\n"," ci_upper: float\n","\n"," def __post_init__(self):\n"," super().__post_init__()\n"," # Ensure we have a valid confidence level.\n"," if not 0 < self.level <= 100:\n"," raise ValueError(f'`level` must be in range (0, 100]: {self.level:0.2f}')\n","\n"," # Ensure confidence intervals match the sample mean given a single sample.\n"," if self.num_samples == 1:\n"," if (self.ci_lower != self.mean) or (self.ci_upper != self.mean):\n"," raise ValueError(\n"," '`ci_lower` and `ci_upper` must match `mean` if `num_samples` is '\n"," f'1: mean={self.mean:0.4f}, ci_lower={self.ci_lower:0.4f}, '\n"," f'ci_upper={self.ci_upper:0.4f}'\n"," )\n","\n"," def __str__(self) -> str:\n"," return (\n"," f'{self.mean:0.4f} (SD={self.stddev:0.4f}, n={self.num_samples}, '\n"," f'{self.level:0>6.2f}% CI=[{self.ci_lower:0.4f}, '\n"," f'{self.ci_upper:0.4f}])'\n"," )\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class Result:\n"," \"\"\"Represents a bootstrapped metric result for an individual model.\n","\n"," Attributes:\n"," model_name: The model's name.\n"," prediction_name: The model's prediction name (e.g., the model head's name or\n"," the label name used in training).\n"," metric_name: The metric's name.\n"," ci: A confidence interval describing the distribution of metric samples.\n"," \"\"\"\n","\n"," model_name: str\n"," prediction_name: str\n"," metric_name: str\n"," ci: ConfidenceInterval\n","\n"," def __post_init__(self):\n"," # Ensure model, prediction, and metric names are specified.\n"," if not self.model_name:\n"," raise ValueError('`model_name` must be specified.')\n"," if not self.prediction_name:\n"," raise ValueError('`prediction_name` must be specified.')\n"," if not self.metric_name:\n"," raise ValueError('`metric_name` must be specified.')\n","\n"," def __str__(self) -> str:\n"," return (\n"," f'{self.model_name}.{self.prediction_name}: '\n"," f'{self.metric_name}: {self.ci}'\n"," )\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class PairedResult:\n"," \"\"\"Represents a paired bootstrapped metric result for two models.\n","\n"," Attributes:\n"," model_name_a: The first model's name.\n"," prediction_name_a: The first model's prediction name (e.g., the model head's\n"," name or the label name used in training).\n"," model_name_b: The second model's name.\n"," prediction_name_b: The second model's prediction name (e.g., the model\n"," head's name or the label name used in training).\n"," metric_name: The metric's name.\n"," ci: A confidence interval describing the distribution of differences between\n"," the first and second models' metric samples.\n"," \"\"\"\n","\n"," model_name_a: str\n"," prediction_name_a: str\n"," model_name_b: str\n"," prediction_name_b: str\n"," metric_name: str\n"," ci: ConfidenceInterval\n","\n"," def __post_init__(self):\n"," # Ensure model, prediction, and metric names are specified.\n"," if not self.model_name_a:\n"," raise ValueError('`model_name_a` must be specified.')\n"," if not self.prediction_name_a:\n"," raise ValueError('`prediction_name_a` must be specified.')\n"," if not self.model_name_b:\n"," raise ValueError('`model_name_b` must be specified.')\n"," if not self.prediction_name_b:\n"," raise ValueError('`prediction_name_b` must be specified.')\n"," if not self.metric_name:\n"," raise ValueError('`metric_name` must be specified.')\n","\n"," def __str__(self) -> str:\n"," return (\n"," f'({self.model_name_a}.{self.prediction_name_a} - '\n"," f'{self.model_name_b}.{self.prediction_name_b}): '\n"," f'{self.metric_name}: {self.ci}'\n"," )\n","\n","\n","def _reverse_paired_result(paired_result: PairedResult) -> PairedResult:\n"," \"\"\"Returns the \"(b - a)\" inverse of an \"(a - b)\" `PairedResult`.\"\"\"\n"," reversed_ci = ConfidenceInterval(\n"," mean=(paired_result.ci.mean * -1),\n"," stddev=paired_result.ci.stddev,\n"," num_samples=paired_result.ci.num_samples,\n"," level=paired_result.ci.level,\n"," ci_upper=(paired_result.ci.ci_lower * -1),\n"," ci_lower=(paired_result.ci.ci_upper * -1),\n"," )\n"," reversed_paired_result = PairedResult(\n"," model_name_a=paired_result.model_name_b,\n"," prediction_name_a=paired_result.prediction_name_b,\n"," model_name_b=paired_result.model_name_a,\n"," prediction_name_b=paired_result.prediction_name_a,\n"," metric_name=paired_result.metric_name,\n"," ci=reversed_ci,\n"," )\n"," return reversed_paired_result\n","\n","\n","def _compute_confidence_interval(\n"," samples: np.ndarray,\n"," ci_level: float,\n",") -> ConfidenceInterval:\n"," \"\"\"Computes the mean, standard deviation, and confidence interval for samples.\n","\n"," Args:\n"," samples: A boostrapped array of observed sample values.\n"," ci_level: The confidence level/width of the desired confidence interval.\n","\n"," Returns:\n"," A `Result` containing the mean, standard deviation, and the `ci_level`%\n"," confidence interval for the observed sample values.\n"," \"\"\"\n"," sample_mean = np.mean(samples, axis=0)\n"," sample_std = np.std(samples, axis=0)\n","\n"," lower_percentile = (100 - ci_level) / 2\n"," upper_percentile = 100 - lower_percentile\n"," percentiles = [lower_percentile, upper_percentile]\n"," ci_lower, ci_upper = np.percentile(a=samples, q=percentiles, axis=0)\n","\n"," ci = ConfidenceInterval(\n"," mean=sample_mean,\n"," stddev=sample_std,\n"," num_samples=len(samples),\n"," level=ci_level,\n"," ci_lower=ci_lower,\n"," ci_upper=ci_upper,\n"," )\n","\n"," return ci\n","\n","\n","def _generate_sample_indices(\n"," label: Label,\n"," is_binary: bool,\n"," num_bootstrap: int,\n"," seed: int,\n",") -> List[IndexSample]:\n"," \"\"\"Returns a list of `num_bootstrap` randomly sampled bootstrap indices.\n","\n"," Args:\n"," label: The ground truth label targets.\n"," is_binary: Whether to generate valid binary samples; i.e., each index sample\n"," contains at least one index corresponding to a label from each class.\n"," num_bootstrap: The number of bootstrap indices to generate.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A list of `num_bootstrap` bootstrap sample indices.\n"," \"\"\"\n"," rng = np.random.default_rng(seed)\n"," num_observations = len(label)\n"," sample_indices = []\n"," while len(sample_indices) < num_bootstrap:\n"," index = rng.integers(0, high=num_observations, size=num_observations)\n"," sample_true = label.values[index]\n"," # If computing a binary metric, skip indices that result in invalid labels.\n"," if is_binary and not is_valid_binary_label(sample_true):\n"," continue\n"," sample_indices.append(index)\n"," return sample_indices\n","\n","\n","def _compute_metric_samples(\n"," metric: Metric,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," sample_indices: Sequence[np.ndarray],\n",") -> Dict[str, np.ndarray]:\n"," \"\"\"Generates `num_bootstrap` metric samples for each `Prediction`.\n","\n"," Note: This method assumes that label and prediction values are orded so that\n"," the value at index `i` in a given `Prediction` corresponds to the label value\n"," at index `i` in `label`. Both the `Label` and `Prediction` arrays are indexed\n"," using the given `sample_indices`.\n","\n"," Args:\n"," metric: An instance of a bootstrappable `Metric`; used to compute samples.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," sample_indices: An array of bootstrap sample indices. If empty, returns the\n"," single value computed on the entire dataset for each prediction.\n","\n"," Returns:\n"," A mapping of model names to the corresponding metric samples array.\n"," \"\"\"\n"," if not sample_indices:\n"," metric_samples = {}\n"," for prediction in predictions:\n"," value = metric(label.values, prediction.values)\n"," metric_samples[prediction.model_name] = np.asarray([value])\n"," return metric_samples\n","\n"," metric_samples = {prediction.model_name: [] for prediction in predictions}\n"," for index in sample_indices:\n"," sample_true = label.values[index]\n"," for prediction in predictions:\n"," sample_value = metric(sample_true, prediction.values[index])\n"," metric_samples[prediction.model_name].append(sample_value)\n","\n"," metric_samples = {\n"," name: np.asarray(samples) for name, samples in metric_samples.items()\n"," }\n","\n"," return metric_samples\n","\n","\n","def _compute_all_metric_samples(\n"," metrics: Sequence[Metric],\n"," contains_binary_metric: bool,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," num_bootstrap: int,\n"," seed: int,\n",") -> Dict[str, Dict[str, np.ndarray]]:\n"," \"\"\"Generates `num_bootstrap` samples for each `Prediction` and `Metric`.\n","\n"," Args:\n"," metrics: A sequence of a bootstrappable `Metric` instances.\n"," contains_binary_metric: Whether the set of metrics contains a binary metric.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," num_bootstrap: The number of bootstrap iterations.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A mapping of metric names to model-sample dictionaries.\n"," \"\"\"\n"," sample_indices = _generate_sample_indices(\n"," label,\n"," contains_binary_metric,\n"," num_bootstrap,\n"," seed,\n"," )\n"," metric_samples = []\n"," for metric in metrics:\n"," metric_samples.append(\n"," _compute_metric_samples(\n"," metric=metric,\n"," label=label,\n"," predictions=predictions,\n"," sample_indices=sample_indices,\n"," )\n"," )\n","\n"," return {\n"," metric.name: metric_sample\n"," for metric, metric_sample in zip(metrics, metric_samples)\n"," }\n","\n","\n","def _process_metric_samples(\n"," metric: Metric,\n"," predictions: Sequence[Prediction],\n"," model_names_to_metric_samples: Dict[str, np.ndarray],\n"," ci_level: float,\n",") -> List[Result]:\n"," \"\"\"Compute `ConfidenceInterval`s for metric samples across predictions.\"\"\"\n"," results = []\n"," for prediction in predictions:\n"," metric_samples = model_names_to_metric_samples[prediction.model_name]\n"," ci = _compute_confidence_interval(metric_samples, ci_level)\n"," result = Result(prediction.model_name, prediction.name, metric.name, ci)\n"," results.append(result)\n"," return results\n","\n","\n","def _process_metric_samples_paired(\n"," metric: Metric,\n"," predictions: Sequence[Prediction],\n"," model_names_to_metric_samples: Dict[str, np.ndarray],\n"," ci_level: float,\n",") -> List[PairedResult]:\n"," \"\"\"Compute `ConfidenceInterval`s for paired samples across predictions.\"\"\"\n"," results = []\n"," for i, prediction_a in enumerate(predictions[:-1]):\n"," for prediction_b in predictions[i + 1 :]:\n"," # Compute the result of `prediction_a - prediction_b`.\n"," metric_samples_a = model_names_to_metric_samples[prediction_a.model_name]\n"," metric_samples_b = model_names_to_metric_samples[prediction_b.model_name]\n"," metric_samples_diff = metric_samples_a - metric_samples_b\n"," ci = _compute_confidence_interval(metric_samples_diff, ci_level)\n"," result = PairedResult(\n"," prediction_a.model_name,\n"," prediction_a.name,\n"," prediction_b.model_name,\n"," prediction_b.name,\n"," metric.name,\n"," ci,\n"," )\n"," results.append(result)\n"," # Derive and include the result of `prediction_b - prediction_a`.\n"," results.append(_reverse_paired_result(result))\n"," return results\n","\n","\n","def _bootstrap(\n"," metrics: Sequence[Metric],\n"," contains_binary_metric: bool,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," num_bootstrap: int,\n"," ci_level: float,\n"," seed: int,\n",") -> Dict[str, List[Result]]:\n"," \"\"\"Performs bootstrapping for all models using the given metrics.\n","\n"," Args:\n"," metrics: A sequence of a bootstrappable `Metric` instances.\n"," contains_binary_metric: Whether the set of metrics contains a binary metric.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," num_bootstrap: The number of bootstrap iterations.\n"," ci_level: The confidence level/width of the desired confidence interval.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A dictionary mapping metric names to a list of `Result`s containing the mean\n"," metric values of each model over `num_bootstrap` bootstrapping iterations.\n"," \"\"\"\n"," metric_to_model_to_samples = _compute_all_metric_samples(\n"," metrics,\n"," contains_binary_metric,\n"," label,\n"," predictions,\n"," num_bootstrap,\n"," seed,\n"," )\n"," metric_samples = []\n"," for metric in metrics:\n"," metric_samples.append(\n"," _process_metric_samples(\n"," metric=metric,\n"," predictions=predictions,\n"," model_names_to_metric_samples=metric_to_model_to_samples[\n"," metric.name\n"," ],\n"," ci_level=ci_level,\n"," )\n"," )\n","\n"," return {\n"," metric.name: metric_sample\n"," for metric, metric_sample in zip(metrics, metric_samples)\n"," }\n","\n","\n","def _paired_bootstrap(\n"," metrics: Sequence[Metric],\n"," contains_binary_metric: bool,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," num_bootstrap: int,\n"," ci_level: float,\n"," seed: int,\n",") -> Dict[str, List[PairedResult]]:\n"," \"\"\"Performs paired bootstrapping for all models using the given metrics.\n","\n"," Args:\n"," metrics: A sequence of a bootstrappable `Metric` instances.\n"," contains_binary_metric: Whether the set of metrics contains a binary metric.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," num_bootstrap: The number of bootstrap iterations.\n"," ci_level: The confidence level/width of the desired confidence interval.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A dictionary mapping metric names to `PairedResult`s containing the mean\n"," metric difference between models over `num_bootstrap` bootstrapping\n"," iterations.\n"," \"\"\"\n"," metric_to_model_to_samples = _compute_all_metric_samples(\n"," metrics,\n"," contains_binary_metric,\n"," label,\n"," predictions,\n"," num_bootstrap,\n"," seed,\n"," )\n"," metric_samples = []\n"," for metric in metrics:\n"," metric_samples.append(\n"," _process_metric_samples_paired(\n"," metric=metric,\n"," predictions=predictions,\n"," model_names_to_metric_samples=metric_to_model_to_samples[\n"," metric.name\n"," ],\n"," ci_level=ci_level,\n"," )\n"," )\n","\n"," return {\n"," metric.name: metric_sample\n"," for metric, metric_sample in zip(metrics, metric_samples)\n"," }\n","\n","\n","def _default_binary_metrics() -> List[BinaryMetric]:\n"," \"\"\"Returns `PerformanceMetrics`'s default metrics for binary target.\"\"\"\n"," metrics = [\n"," BinaryMetric('num', count),\n"," BinaryMetric('auc', sklearn.metrics.roc_auc_score),\n"," BinaryMetric('auprc', sklearn.metrics.average_precision_score),\n"," ]\n"," for percentile in [100, 10, 5, 1]:\n"," metrics.append(\n"," BinaryMetric(\n"," f'freq@{percentile:>03}%',\n"," frequency_fn(percentile),\n"," )\n"," )\n"," return metrics\n","\n","\n","def _default_continuous_metrics() -> List[ContinuousMetric]:\n"," \"\"\"Returns `PerformanceMetrics`'s default metrics for continuous target.\"\"\"\n"," metrics = [\n"," ContinuousMetric('num', count),\n"," ContinuousMetric('pearson', pearsonr),\n"," ContinuousMetric('pearsonr_squared', pearsonr_squared),\n"," ContinuousMetric('spearman', spearmanr),\n"," ContinuousMetric('mse', sklearn.metrics.mean_squared_error),\n"," ContinuousMetric('mae', sklearn.metrics.mean_absolute_error),\n"," ]\n"," return metrics\n","\n","\n","def _default_metrics(binary_targets: bool) -> List[Metric]:\n"," \"\"\"Returns `PerformanceMetrics`'s default set of metrics for the target type.\n","\n"," Args:\n"," binary_targets: Whether the target labels are binary. If false, the returned\n"," metrics assume continuous labels.\n","\n"," Returns:\n"," The default set of binary or continuous `bootstrap_metrics.Metric`s.\n"," \"\"\"\n"," if binary_targets:\n"," return _default_binary_metrics()\n"," return _default_continuous_metrics()\n","\n","\n","class PerformanceMetrics:\n"," \"\"\"A named collection of invocable, bootstrapable `Metric`s.\n","\n"," Initializes a class that applies the given `Metric` functions to new ground\n"," truth labels and predictions. `Metric`s can be evaluated with and without\n"," bootstrapping.\n","\n"," The default metrics are number of samples, auc, auprc, and frequency\n"," calculations for the top 100/10/5/1 top percentiles, if `default_metrics` is\n"," 'binary'. If `default_metrics` is 'continuous', the default metrics are\n"," Pearson and Spearman correlations, the square of the Pearson correlation, mean\n"," squared error (MSE) and mean absolute error (MAE).\n","\n"," TODO(b/199452239): Refactor `PerformanceMetrics` so that the default metric\n"," set is not parameterized with a string.\n","\n"," Raises:\n"," ValueError: if an item in `metrics` is not of type `Metric`.\n"," \"\"\"\n","\n"," def __init__(\n"," self,\n"," name: str,\n"," default_metrics: Optional[str] = None,\n"," metrics: Optional[List[Metric]] = None,\n"," ) -> None:\n","\n"," if metrics is None:\n"," if default_metrics is None:\n"," raise ValueError('`default_metrics` is None and no metric is provided.')\n"," elif default_metrics == 'binary':\n"," metrics = _default_metrics(binary_targets=True)\n"," elif default_metrics == 'continuous':\n"," metrics = _default_metrics(binary_targets=False)\n"," else:\n"," raise ValueError(\n"," 'unknown `default_metrics`: {}'.format(default_metrics)\n"," )\n","\n"," for metric in metrics:\n"," if not isinstance(metric, Metric):\n"," raise ValueError('Invalid metric value: must be of class `Metric`.')\n","\n"," if len(metrics) != len({metric.name for metric in metrics}):\n"," raise ValueError(f'Metric names must be unique: {metrics}')\n","\n"," self.name = name\n"," self.metrics = metrics\n"," self.contains_binary = any(is_binary(m) for m in metrics)\n","\n"," def compute(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," ) -> Dict[str, Result]:\n"," \"\"\"Evaluates all metrics using the given labels and predictions.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," mask: A boolean mask; applied to `y_true` and `y_pred`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n","\n"," Returns:\n"," A dictionary of bootstrapped metrics keyed on metric name with\n"," `Result` values.\n","\n"," Raises:\n"," ValueError: If the dimensions of `y_true`, `y_pred`, or `mask` do not\n"," match, or labels are not in {0 , 1}.\n"," \"\"\"\n"," if len(y_true) != len(y_pred):\n"," raise ValueError('Label and prediction dimensions do not match.')\n","\n"," if mask is not None and len(mask) != len(y_pred):\n"," raise ValueError('Label and prediction dimensions do not match mask.')\n","\n"," if mask is not None:\n"," y_true = y_true[mask]\n"," y_pred = y_pred[mask]\n","\n"," # TODO(b/197539434): Pipe through non-empty names after public api refactor.\n"," label_name = 'label'\n"," label = Label(label_name, y_true)\n"," predictions = [Prediction(label_name, y_pred, 'model')]\n","\n"," metric_results = _bootstrap(\n"," self.metrics,\n"," contains_binary_metric=self.contains_binary,\n"," label=label,\n"," predictions=predictions,\n"," num_bootstrap=n_bootstrap,\n"," ci_level=conf_interval,\n"," seed=seed,\n"," )\n","\n"," # TODO(b/197539434): Remove temporary asserts after public api refactor.\n"," final_results = {}\n"," for metric_name, results in metric_results.items():\n"," assert len(results) == 1\n"," final_results[metric_name] = results[0]\n","\n"," return final_results\n","\n"," def compute_paired(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred_a: np.ndarray,\n"," y_pred_b: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," ) -> Dict[str, PairedResult]:\n"," \"\"\"Computes a paired bootstrap value for each metric.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred_a: Target predictions from model A; compared to `y_pred_b`.\n"," y_pred_b: Target predictions from model B; compared to `y_pred_a`.\n"," mask: A boolean mask; applied to `y_true`, `y_pred_a`, and `y_pred_b`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n","\n"," Returns:\n"," A dictionary of paired bootstrapped metrics keyed on metric name with\n"," `PairedResult` values.\n","\n"," Raises:\n"," ValueError: If the dimensions of `y_true`, `y_pred_a`, `y_pred_b` or\n"," `mask` do not match, or labels are not in {0 , 1}.\n"," \"\"\"\n"," if (len(y_true) != len(y_pred_a)) or (len(y_true) != len(y_pred_b)):\n"," raise ValueError('Label and prediction dimensions do not match.')\n","\n"," if mask is not None and len(mask) != len(y_pred_a):\n"," raise ValueError('Label and prediction dimensions do not match mask.')\n","\n"," if mask is not None:\n"," y_true = y_true[mask]\n"," y_pred_a = y_pred_a[mask]\n"," y_pred_b = y_pred_b[mask]\n","\n"," # TODO(b/197539434): Pipe through non-empty names after public api refactor.\n"," label_name = 'label'\n"," label = Label(label_name, y_true)\n"," first_model_name = 'model_a'\n"," predictions = [\n"," Prediction(label_name, y_pred_a, first_model_name),\n"," Prediction(label_name, y_pred_b, 'model_b'),\n"," ]\n","\n"," metric_results = _paired_bootstrap(\n"," self.metrics,\n"," contains_binary_metric=self.contains_binary,\n"," label=label,\n"," predictions=predictions,\n"," num_bootstrap=n_bootstrap,\n"," ci_level=conf_interval,\n"," seed=seed,\n"," )\n","\n"," # TODO(b/197539434): Remove temporary asserts after public api refactor.\n"," final_results = {}\n"," for metric_name, results in metric_results.items():\n"," assert len(results) == 2\n"," assert results[0].model_name_a == first_model_name\n"," final_results[metric_name] = results[0]\n","\n"," return final_results\n","\n"," def _print_results(\n"," self,\n"," title: str,\n"," results: Dict[str, Union[Result, PairedResult]],\n"," ) -> None:\n"," \"\"\"Prints each result object under the current name and given title.\"\"\"\n"," print(f'{self.name}: {title}')\n"," for _, result in sorted(results.items()):\n"," print(f'\\t{result}')\n","\n"," def compute_and_print(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," title: str = '',\n"," ) -> None:\n"," \"\"\"Evaluates and pretty-prints metrics using given labels and predictions.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," mask: A boolean mask; applied to `y_true` and `y_pred`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n"," title: A title appended to the printed evaluation metrics.\n","\n"," Raises:\n"," ValueError: If any of `y_true`, `y_pred`, or `mask` are not of type\n"," numpy.array of if their dimensions do not match.\n"," \"\"\"\n"," results = self.compute(\n"," y_true,\n"," y_pred,\n"," mask=mask,\n"," n_bootstrap=n_bootstrap,\n"," conf_interval=conf_interval,\n"," seed=seed,\n"," )\n"," self._print_results(title, results)\n","\n"," def compute_paired_and_print(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred_a: np.ndarray,\n"," y_pred_b: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," title: str = '',\n"," **kwargs,\n"," ) -> None:\n"," \"\"\"Evaluates and pretty-prints paired metrics.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred_a: Target predictions from model A; compared to `y_pred_b`.\n"," y_pred_b: Target predictions from model B; compared to `y_pred_a`.\n"," mask: A boolean mask; applied to `y_true`, `y_pred_a`, and `y_pred_b`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n"," title: A title appended to the printed evaluation metrics.\n"," **kwargs: Additional keyword arguments passed to each Metric's `func`.\n"," \"\"\"\n"," results = self.compute_paired(\n"," y_true,\n"," y_pred_a,\n"," y_pred_b,\n"," mask=mask,\n"," n_bootstrap=n_bootstrap,\n"," conf_interval=conf_interval,\n"," seed=seed,\n"," **kwargs,\n"," )\n"," self._print_results(title, results)"]},{"cell_type":"code","execution_count":4,"metadata":{"id":"x4222NTc0xpR","executionInfo":{"status":"ok","timestamp":1717783770586,"user_tz":240,"elapsed":18,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["N_BOOTSTRAP = 300\n","BOOTSTRAP_METRICS_LIST = [\n"," BinaryMetric('roc_auc', metrics.roc_auc_score),\n"," BinaryMetric('pr_auc', metrics.average_precision_score),\n"," ContinuousMetric('pearsonr', pearsonr),\n"," BinaryMetric('top10prev', frequency_fn(10)),\n","]\n","\n","def get_prs_eval_info(y_true, y_pred, name, as_dataframe=False):\n"," performance_metrics = PerformanceMetrics(\n"," 'Metrics', metrics=BOOTSTRAP_METRICS_LIST)\n"," performance_metrics_values = performance_metrics.compute(\n"," y_true=y_true,\n"," y_pred=y_pred,\n"," n_bootstrap=N_BOOTSTRAP,\n"," )\n"," # print(performance_metrics_values, flush=True)\n"," roc_auc_ci = performance_metrics_values['roc_auc'].ci\n"," pr_auc_ci = performance_metrics_values['pr_auc'].ci\n"," pearsonr_ci = performance_metrics_values['pearsonr'].ci\n"," top10prev_ci = performance_metrics_values['top10prev'].ci\n"," info = {\n"," 'method': name,\n"," 'pearsonr': pearsonr_ci.mean,\n"," 'pearsonr_std': pearsonr_ci.stddev,\n"," 'pearsonr_lower': pearsonr_ci.ci_lower,\n"," 'pearsonr_upper': pearsonr_ci.ci_upper,\n"," 'roc_auc': roc_auc_ci.mean,\n"," 'roc_auc_std': roc_auc_ci.stddev,\n"," 'roc_auc_lower': roc_auc_ci.ci_lower,\n"," 'roc_auc_upper': roc_auc_ci.ci_upper,\n"," 'pr_auc': pr_auc_ci.mean,\n"," 'pr_auc_std': pr_auc_ci.stddev,\n"," 'pr_auc_lower': pr_auc_ci.ci_lower,\n"," 'pr_auc_upper': pr_auc_ci.ci_upper,\n"," 'top10prev': top10prev_ci.mean,\n"," 'top10prev_std': top10prev_ci.stddev,\n"," 'top10prev_lower': top10prev_ci.ci_lower,\n"," 'top10prev_upper': top10prev_ci.ci_upper,\n"," }\n"," if as_dataframe:\n"," return pd.DataFrame(info, index=[0])\n"," else:\n"," return info\n","\n","\n","def get_prs_paired_eval_info(y_true,\n"," y_pred1,\n"," y_pred2,\n"," name1,\n"," name2,\n"," as_dataframe=False):\n"," performance_metrics = PerformanceMetrics(\n"," 'Metrics', metrics=BOOTSTRAP_METRICS_LIST)\n"," performance_metrics_values_paired = performance_metrics.compute_paired(\n"," y_true=y_true,\n"," y_pred_a=y_pred1,\n"," y_pred_b=y_pred2,\n"," n_bootstrap=N_BOOTSTRAP,\n"," )\n"," # print(performance_metrics_values_paired, flush=True)\n"," roc_auc_ci = performance_metrics_values_paired['roc_auc'].ci\n"," pr_auc_ci = performance_metrics_values_paired['pr_auc'].ci\n"," pearsonr_ci = performance_metrics_values_paired['pearsonr'].ci\n"," top10prev_ci = performance_metrics_values_paired['top10prev'].ci\n"," info = {\n"," 'method_a': name1,\n"," 'method_b': name2,\n"," 'pearsonr': pearsonr_ci.mean,\n"," 'pearsonr_std': pearsonr_ci.stddev,\n"," 'pearsonr_lower': pearsonr_ci.ci_lower,\n"," 'pearsonr_upper': pearsonr_ci.ci_upper,\n"," 'roc_auc': roc_auc_ci.mean,\n"," 'roc_auc_std': roc_auc_ci.stddev,\n"," 'roc_auc_lower': roc_auc_ci.ci_lower,\n"," 'roc_auc_upper': roc_auc_ci.ci_upper,\n"," 'pr_auc': pr_auc_ci.mean,\n"," 'pr_auc_std': pr_auc_ci.stddev,\n"," 'pr_auc_lower': pr_auc_ci.ci_lower,\n"," 'pr_auc_upper': pr_auc_ci.ci_upper,\n"," 'top10prev': top10prev_ci.mean,\n"," 'top10prev_std': top10prev_ci.stddev,\n"," 'top10prev_lower': top10prev_ci.ci_lower,\n"," 'top10prev_upper': top10prev_ci.ci_upper,\n"," }\n"," if as_dataframe:\n"," return pd.DataFrame(info, index=[0])\n"," else:\n"," return info"]},{"cell_type":"markdown","metadata":{"id":"NOaueJxRPmpG"},"source":["# Simulated data generation\n","\n","In this code example, we generate some simulated data (N=1,000) to demonstrate how to use the above code snippet to compute various metrics in the PRS evaluation part of the paper."]},{"cell_type":"code","execution_count":5,"metadata":{"id":"iXHTm8dxzY2H","executionInfo":{"status":"ok","timestamp":1717783770587,"user_tz":240,"elapsed":16,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["np.random.seed(42)\n","individual_prs1 = np.random.normal(size=(1000,))\n","individual_prs2 = 0.8 * individual_prs1 + 0.2 * np.random.normal(size=(1000,))\n","individual_phenotype = 0.3 * individual_prs1 + 0.7 * np.random.normal(\n"," size=(1000,)\n",")\n","individual_phenotype = (individual_phenotype >= 0).astype(int)\n","\n","data_df = pd.DataFrame({\n"," 'prs1': individual_prs1,\n"," 'prs2': individual_prs2,\n"," 'phenotype': individual_phenotype,\n","})"]},{"cell_type":"code","execution_count":6,"metadata":{"colab":{"height":206,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":16,"status":"ok","timestamp":1717783770588,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"bzdHe1jqULbv","outputId":"d11b16cf-a363-47ad-b819-e306df9990f3"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" prs1 prs2 phenotype\n","0 0.496714 0.677242 0\n","1 -0.138264 0.074315 0\n","2 0.647689 0.530077 0\n","3 1.523030 1.089037 1\n","4 -0.234153 -0.047678 0"],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
prs1prs2phenotype
00.4967140.6772420
1-0.1382640.0743150
20.6476890.5300770
31.5230301.0890371
4-0.234153-0.0476780
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n"," \n","\n","\n","\n"," \n","
\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","variable_name":"data_df","summary":"{\n \"name\": \"data_df\",\n \"rows\": 1000,\n \"fields\": [\n {\n \"column\": \"prs1\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.9792159381796757,\n \"min\": -3.2412673400690726,\n \"max\": 3.852731490654721,\n \"num_unique_values\": 1000,\n \"samples\": [\n 0.543360192379935,\n 0.9826909839455139,\n -1.8408742313316453\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"prs2\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.8005263506410991,\n \"min\": -2.4852626735659844,\n \"max\": 3.4321005411611654,\n \"num_unique_values\": 1000,\n \"samples\": [\n 0.5511076945976712,\n 0.5725922028405726,\n -1.4935892287728105\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"phenotype\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0,\n \"min\": 0,\n \"max\": 1,\n \"num_unique_values\": 2,\n \"samples\": [\n 1,\n 0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":6}],"source":["data_df.head()"]},{"cell_type":"markdown","metadata":{"id":"4LYsbEE3RdeF"},"source":["# PRS evaluation with bootstrapping\n","\n","The following code generates all evaluation metrics, namely Pearson R, AUC-ROC, AUC-PR, top 10% prevalence, and their 95% confidence intervals using bootstrapping. Note that, from the way we generated the simulated data, we expect the Pearson R of ~0.3 for `prs1` and we expect `prs1` to have higher correlation with the phenotype than `prs2`."]},{"cell_type":"code","execution_count":7,"metadata":{"colab":{"height":101,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":15212,"status":"ok","timestamp":1717783785790,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"WVJnK7BAPi33","outputId":"5b371f81-bc64-40ef-ed75-d9d080bd8475"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" method pearsonr pearsonr_std pearsonr_lower pearsonr_upper roc_auc \\\n","0 prs1 0.333455 0.027456 0.277529 0.387433 0.69263 \n","\n"," roc_auc_std roc_auc_lower roc_auc_upper pr_auc pr_auc_std \\\n","0 0.016445 0.65976 0.725288 0.675271 0.022152 \n","\n"," pr_auc_lower pr_auc_upper top10prev top10prev_std top10prev_lower \\\n","0 0.632141 0.715912 0.770216 0.043321 0.688044 \n","\n"," top10prev_upper \n","0 0.85078 "],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
methodpearsonrpearsonr_stdpearsonr_lowerpearsonr_upperroc_aucroc_auc_stdroc_auc_lowerroc_auc_upperpr_aucpr_auc_stdpr_auc_lowerpr_auc_uppertop10prevtop10prev_stdtop10prev_lowertop10prev_upper
0prs10.3334550.0274560.2775290.3874330.692630.0164450.659760.7252880.6752710.0221520.6321410.7159120.7702160.0433210.6880440.85078
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","summary":"{\n \"name\": \")\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"method\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs1\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.3334554859786796,\n \"max\": 0.3334554859786796,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.3334554859786796\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.027455597173908577,\n \"max\": 0.027455597173908577,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.027455597173908577\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.2775293042598108,\n \"max\": 0.2775293042598108,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.2775293042598108\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.38743254268744753,\n \"max\": 0.38743254268744753,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.38743254268744753\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6926303605619311,\n \"max\": 0.6926303605619311,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6926303605619311\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.016445301315729702,\n \"max\": 0.016445301315729702,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.016445301315729702\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.659760150142918,\n \"max\": 0.659760150142918,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.659760150142918\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7252876945992696,\n \"max\": 0.7252876945992696,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7252876945992696\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.675270596876246,\n \"max\": 0.675270596876246,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.675270596876246\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.02215152388674347,\n \"max\": 0.02215152388674347,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.02215152388674347\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6321413648383354,\n \"max\": 0.6321413648383354,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6321413648383354\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7159121917609861,\n \"max\": 0.7159121917609861,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7159121917609861\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7702162426122681,\n \"max\": 0.7702162426122681,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7702162426122681\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.04332125213088804,\n \"max\": 0.04332125213088804,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.04332125213088804\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6880441176470588,\n \"max\": 0.6880441176470588,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6880441176470588\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.8507797029702969,\n \"max\": 0.8507797029702969,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.8507797029702969\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":7}],"source":["get_prs_eval_info(\n"," y_true=data_df['phenotype'],\n"," y_pred=data_df['prs1'],\n"," name='prs1',\n"," as_dataframe=True\n",")"]},{"cell_type":"code","execution_count":8,"metadata":{"colab":{"height":101,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":9709,"status":"ok","timestamp":1717783795493,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"puOfA5wuQeiJ","outputId":"99b92e16-0eb5-497f-f473-26ad3c955948"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" method pearsonr pearsonr_std pearsonr_lower pearsonr_upper roc_auc \\\n","0 prs2 0.319189 0.027899 0.260433 0.373947 0.6837 \n","\n"," roc_auc_std roc_auc_lower roc_auc_upper pr_auc pr_auc_std \\\n","0 0.016604 0.649911 0.717019 0.664467 0.022454 \n","\n"," pr_auc_lower pr_auc_upper top10prev top10prev_std top10prev_lower \\\n","0 0.620486 0.706022 0.764624 0.042396 0.671552 \n","\n"," top10prev_upper \n","0 0.84 "],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
methodpearsonrpearsonr_stdpearsonr_lowerpearsonr_upperroc_aucroc_auc_stdroc_auc_lowerroc_auc_upperpr_aucpr_auc_stdpr_auc_lowerpr_auc_uppertop10prevtop10prev_stdtop10prev_lowertop10prev_upper
0prs20.3191890.0278990.2604330.3739470.68370.0166040.6499110.7170190.6644670.0224540.6204860.7060220.7646240.0423960.6715520.84
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","summary":"{\n \"name\": \")\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"method\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs2\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.3191890184766251,\n \"max\": 0.3191890184766251,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.3191890184766251\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.027898865889530153,\n \"max\": 0.027898865889530153,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.027898865889530153\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.2604328480042442,\n \"max\": 0.2604328480042442,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.2604328480042442\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.3739469506434232,\n \"max\": 0.3739469506434232,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.3739469506434232\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6836996447028457,\n \"max\": 0.6836996447028457,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6836996447028457\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.01660378118234475,\n \"max\": 0.01660378118234475,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.01660378118234475\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6499110741641438,\n \"max\": 0.6499110741641438,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6499110741641438\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7170185826451294,\n \"max\": 0.7170185826451294,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7170185826451294\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6644674946186202,\n \"max\": 0.6644674946186202,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6644674946186202\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.0224540065869167,\n \"max\": 0.0224540065869167,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.0224540065869167\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6204864568922334,\n \"max\": 0.6204864568922334,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6204864568922334\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7060224657169427,\n \"max\": 0.7060224657169427,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7060224657169427\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.764623511500396,\n \"max\": 0.764623511500396,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.764623511500396\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.042396301865302535,\n \"max\": 0.042396301865302535,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.042396301865302535\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6715519801980199,\n \"max\": 0.6715519801980199,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6715519801980199\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.84,\n \"max\": 0.84,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.84\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":8}],"source":["get_prs_eval_info(\n"," y_true=data_df['phenotype'],\n"," y_pred=data_df['prs2'],\n"," name='prs2',\n"," as_dataframe=True\n",")"]},{"cell_type":"markdown","metadata":{"id":"OiLCjqcrSjPg"},"source":["# PRS comparison with paired bootstrapping\n","\n","The following code snippet compares the performance of `prs1` and `prs2` using paired bootstrapping. Note that the difference is statistically significant with 95% paired bootstrapping confidence interval, if the lower and upper end of the confidence interval are both positive (implying `prs1` is significantly better than `prs2`) or both negative (implying `prs2` is significantly better than `prs1`)."]},{"cell_type":"code","execution_count":9,"metadata":{"colab":{"height":101,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":7610,"status":"ok","timestamp":1717783803097,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"oRKgjH_uR2wr","outputId":"8df67f16-31e6-4ae4-c904-b01a91390170"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" method_a method_b pearsonr pearsonr_std pearsonr_lower pearsonr_upper \\\n","0 prs1 prs2 0.014266 0.007112 0.000436 0.027211 \n","\n"," roc_auc roc_auc_std roc_auc_lower roc_auc_upper pr_auc pr_auc_std \\\n","0 0.008931 0.004466 0.000157 0.017171 0.010803 0.005761 \n","\n"," pr_auc_lower pr_auc_upper top10prev top10prev_std top10prev_lower \\\n","0 -0.00061 0.02107 0.005593 0.026971 -0.042589 \n","\n"," top10prev_upper \n","0 0.062382 "],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
method_amethod_bpearsonrpearsonr_stdpearsonr_lowerpearsonr_upperroc_aucroc_auc_stdroc_auc_lowerroc_auc_upperpr_aucpr_auc_stdpr_auc_lowerpr_auc_uppertop10prevtop10prev_stdtop10prev_lowertop10prev_upper
0prs1prs20.0142660.0071120.0004360.0272110.0089310.0044660.0001570.0171710.0108030.005761-0.000610.021070.0055930.026971-0.0425890.062382
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","summary":"{\n \"name\": \" as_dataframe=True)\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"method_a\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs1\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"method_b\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs2\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.014266467502054426,\n \"max\": 0.014266467502054426,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.014266467502054426\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.007111892690604321,\n \"max\": 0.007111892690604321,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.007111892690604321\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.00043626824886599245,\n \"max\": 0.00043626824886599245,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.00043626824886599245\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.027211089302840434,\n \"max\": 0.027211089302840434,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.027211089302840434\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.008930715859085309,\n \"max\": 0.008930715859085309,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.008930715859085309\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.004466363148919537,\n \"max\": 0.004466363148919537,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.004466363148919537\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.00015733124729375172,\n \"max\": 0.00015733124729375172,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.00015733124729375172\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.017170818130808965,\n \"max\": 0.017170818130808965,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.017170818130808965\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.010803102257625864,\n \"max\": 0.010803102257625864,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.010803102257625864\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.005760958016623593,\n \"max\": 0.005760958016623593,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.005760958016623593\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": -0.0006104367572841078,\n \"max\": -0.0006104367572841078,\n \"num_unique_values\": 1,\n \"samples\": [\n -0.0006104367572841078\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.02106968216083579,\n \"max\": 0.02106968216083579,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.02106968216083579\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.005592731111872085,\n \"max\": 0.005592731111872085,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.005592731111872085\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.026971273443313012,\n \"max\": 0.026971273443313012,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.026971273443313012\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": -0.04258910891089107,\n \"max\": -0.04258910891089107,\n \"num_unique_values\": 1,\n \"samples\": [\n -0.04258910891089107\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.062381770529994184,\n \"max\": 0.062381770529994184,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.062381770529994184\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":9}],"source":["get_prs_paired_eval_info(\n"," y_true=data_df['phenotype'],\n"," y_pred1=data_df['prs1'],\n"," y_pred2=data_df['prs2'],\n"," name1='prs1',\n"," name2='prs2',\n"," as_dataframe=True)"]}]} \ No newline at end of file From 1bd27c951c6821822307520381ee58a2540b6a40 Mon Sep 17 00:00:00 2001 From: Taedong Yun Date: Fri, 7 Jun 2024 15:56:12 -0400 Subject: [PATCH 3/4] Add license, fix typo, hide some cell outputs --- regle/analysis/embedding_interpretability.ipynb | 2 +- regle/analysis/pca_and_spline_fitting.ipynb | 2 +- regle/analysis/prs_analysis.ipynb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/regle/analysis/embedding_interpretability.ipynb b/regle/analysis/embedding_interpretability.ipynb index f90420b..16a2377 100644 --- a/regle/analysis/embedding_interpretability.ipynb +++ b/regle/analysis/embedding_interpretability.ipynb @@ -1 +1 @@ -{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"provenance":[],"authorship_tag":"ABX9TyMFWnmmZWzOiBWzQbJD8MHZ"},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"markdown","metadata":{"id":"TQe5CETGcdwz"},"source":["# Download Keras checkpoints from our GitHub repo"]},{"cell_type":"code","execution_count":1,"metadata":{"id":"a1RXc2pKYPtM","colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"status":"ok","timestamp":1717783515535,"user_tz":240,"elapsed":3133,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}},"outputId":"b49a78be-1307-470a-85a8-f25c6d48dfc6"},"outputs":[{"output_type":"stream","name":"stdout","text":["--2024-06-07 18:05:13-- https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/saved_model.pb\n","Resolving github.com (github.com)... 140.82.113.4\n","Connecting to github.com (github.com)|140.82.113.4|:443... connected.\n","HTTP request sent, awaiting response... 302 Found\n","Location: https://raw.githubusercontent.com/Google-Health/genomics-research/main/regle/saved_models/rspincs/saved_model.pb [following]\n","--2024-06-07 18:05:13-- https://raw.githubusercontent.com/Google-Health/genomics-research/main/regle/saved_models/rspincs/saved_model.pb\n","Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.111.133, 185.199.109.133, ...\n","Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected.\n","HTTP request sent, awaiting response... 200 OK\n","Length: 1227084 (1.2M) [application/octet-stream]\n","Saving to: ‘rspincs/saved_model.pb’\n","\n","saved_model.pb 100%[===================>] 1.17M --.-KB/s in 0.06s \n","\n","2024-06-07 18:05:13 (18.6 MB/s) - ‘rspincs/saved_model.pb’ saved [1227084/1227084]\n","\n","--2024-06-07 18:05:13-- https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/keras_metadata.pb\n","Resolving github.com (github.com)... 140.82.114.4\n","Connecting to github.com (github.com)|140.82.114.4|:443... connected.\n","HTTP request sent, awaiting response... 302 Found\n","Location: https://raw.githubusercontent.com/Google-Health/genomics-research/main/regle/saved_models/rspincs/keras_metadata.pb [following]\n","--2024-06-07 18:05:14-- https://raw.githubusercontent.com/Google-Health/genomics-research/main/regle/saved_models/rspincs/keras_metadata.pb\n","Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.108.133, 185.199.111.133, ...\n","Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.\n","HTTP request sent, awaiting response... 200 OK\n","Length: 97736 (95K) [text/plain]\n","Saving to: ‘rspincs/keras_metadata.pb’\n","\n","keras_metadata.pb 100%[===================>] 95.45K --.-KB/s in 0.02s \n","\n","2024-06-07 18:05:14 (4.34 MB/s) - ‘rspincs/keras_metadata.pb’ saved [97736/97736]\n","\n","--2024-06-07 18:05:14-- https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/variables/variables.data-00000-of-00001\n","Resolving github.com (github.com)... 140.82.113.4\n","Connecting to github.com (github.com)|140.82.113.4|:443... connected.\n","HTTP request sent, awaiting response... 302 Found\n","Location: https://raw.githubusercontent.com/Google-Health/genomics-research/main/regle/saved_models/rspincs/variables/variables.data-00000-of-00001 [following]\n","--2024-06-07 18:05:14-- https://raw.githubusercontent.com/Google-Health/genomics-research/main/regle/saved_models/rspincs/variables/variables.data-00000-of-00001\n","Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ...\n","Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected.\n","HTTP request sent, awaiting response... 200 OK\n","Length: 6589814 (6.3M) [application/octet-stream]\n","Saving to: ‘rspincs/variables/variables.data-00000-of-00001’\n","\n","variables.data-0000 100%[===================>] 6.28M --.-KB/s in 0.1s \n","\n","2024-06-07 18:05:15 (44.0 MB/s) - ‘rspincs/variables/variables.data-00000-of-00001’ saved [6589814/6589814]\n","\n","--2024-06-07 18:05:15-- https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/variables/variables.index\n","Resolving github.com (github.com)... 140.82.113.4\n","Connecting to github.com (github.com)|140.82.113.4|:443... connected.\n","HTTP request sent, awaiting response... 302 Found\n","Location: https://raw.githubusercontent.com/Google-Health/genomics-research/main/regle/saved_models/rspincs/variables/variables.index [following]\n","--2024-06-07 18:05:15-- https://raw.githubusercontent.com/Google-Health/genomics-research/main/regle/saved_models/rspincs/variables/variables.index\n","Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ...\n","Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected.\n","HTTP request sent, awaiting response... 200 OK\n","Length: 2223 (2.2K) [application/octet-stream]\n","Saving to: ‘rspincs/variables/variables.index’\n","\n","variables.index 100%[===================>] 2.17K --.-KB/s in 0s \n","\n","2024-06-07 18:05:15 (23.3 MB/s) - ‘rspincs/variables/variables.index’ saved [2223/2223]\n","\n"]}],"source":["!mkdir -p rspincs/variables\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/saved_model.pb -P rspincs/\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/keras_metadata.pb -P rspincs/\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/variables/variables.data-00000-of-00001 -P rspincs/variables/\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/variables/variables.index -P rspincs/variables/"]},{"cell_type":"markdown","metadata":{"id":"hjRXNyKwcy8T"},"source":["# Imports and functions"]},{"cell_type":"code","execution_count":2,"metadata":{"id":"w6MpGCYoSOgt","executionInfo":{"status":"ok","timestamp":1717783528618,"user_tz":240,"elapsed":13086,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["from typing import Optional\n","\n","import matplotlib as mpl\n","import matplotlib.pyplot as plt\n","import numpy as np\n","import tensorflow as tf"]},{"cell_type":"code","execution_count":3,"metadata":{"id":"CTCzhsgYVt3A","executionInfo":{"status":"ok","timestamp":1717783528620,"user_tz":240,"elapsed":14,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["# The example values for the 5 (standardized) spigrogram EDFs:\n","# 'blow_fev1', 'blow_fvc', 'blow_pef', 'blow_ratio', 'blow_fef25_75'\n","EDF_VALUE_EXAMPLE = [-1.8, -1.8, -1.4, -0.7, -1.5]\n","\n","# Note we use 0, 1, ..., 999 for the volume values in flow-volume curves,\n","# which were interpolated between 0 and 6.58.\n","VOLUME_SCALE_FACTOR = 6.58 / 1000\n","\n","\n","def _draw_double_arrow(\n"," ax: mpl.axes.Axes,\n"," x1: float,\n"," x2: float,\n"," y: float,\n"," arrow_color: str = '#d62728',\n","):\n"," \"\"\"Draw an arrow pointing both sides between (x1, y) and (x2, y).\"\"\"\n"," ax.arrow(\n"," x1,\n"," y,\n"," x2 - x1,\n"," 0,\n"," fc=arrow_color,\n"," ec=arrow_color,\n"," width=0.04,\n"," head_width=0.15,\n"," head_length=0.05,\n"," zorder=100,\n"," )\n"," ax.arrow(\n"," x2,\n"," y,\n"," x1 - x2,\n"," 0,\n"," fc=arrow_color,\n"," ec=arrow_color,\n"," width=0.04,\n"," head_width=0.15,\n"," head_length=0.05,\n"," zorder=100,\n"," )\n","\n","\n","def generate_rspincs_reconstruction_plot(\n"," vae_model: tf.keras.Model,\n"," latent_dim: int,\n"," fpath_noext: Optional[str] = None,\n"," dpi=300,\n",") -> None:\n"," \"\"\"Generate reconstructed spirograms while varying each RSPINCs coordinate.\n","\n"," Args:\n"," row: A row of the SPINCs DF from which we'll get the values of manual\n"," features.\n"," vae_model: The VAE model to be used to reconstruct spirograms.\n"," latent_dim: The latent dimension.\n"," fpath_noext: The path to the output image file without extension.\n"," dpi: DPI of the image.\n"," \"\"\"\n"," cmap = plt.get_cmap('viridis')\n"," num_injected_features = 5\n"," radius = 1.5\n"," single_encodings = np.linspace(-radius, radius, num=21)\n"," decoder = vae_model.get_layer(f'{vae_model.name}_decoder')\n"," colorbar_width = 0.2\n","\n"," rescaled_volume = np.arange(1000) * VOLUME_SCALE_FACTOR\n"," _, axs = plt.subplots(\n"," 1,\n"," latent_dim + 1,\n"," figsize=(4 * latent_dim + colorbar_width, 3),\n"," width_ratios=[4] * latent_dim + [colorbar_width],\n"," )\n","\n"," for latent_idx in range(latent_dim):\n"," ax = axs[latent_idx]\n"," for img_idx, single_encoding in enumerate(single_encodings):\n"," # This value should be in [0, 1].\n"," color_val = single_encoding / (radius * 2) + 0.5\n"," encoding = np.zeros(latent_dim)\n"," encoding[latent_idx] = single_encoding\n"," encoding_input = np.expand_dims(encoding, axis=0)\n"," edf_input = np.expand_dims(np.array(EDF_VALUE_EXAMPLE), axis=0)\n"," vae_input = np.concatenate((encoding_input, edf_input), axis=-1)\n"," assert vae_input.shape == (1, latent_dim + num_injected_features)\n"," reconstructed = decoder(vae_input)[0].numpy()[:, 0]\n"," assert len(rescaled_volume) == len(reconstructed)\n"," ax.plot(\n"," rescaled_volume,\n"," reconstructed,\n"," color=cmap(color_val),\n"," alpha=0.9,\n"," linewidth=0.8,\n"," )\n"," ax.set_xlim((-20 * VOLUME_SCALE_FACTOR, 350 * VOLUME_SCALE_FACTOR))\n"," ax.set_ylim((-0.1, 4.2))\n"," ax.set_xlabel('Volume (L)')\n"," # Custom annotation for RSPINCs with dim = 2:\n"," if latent_idx == 0:\n"," ax.set_ylabel('Flow (L/s)')\n"," _draw_double_arrow(\n"," ax, 50 * VOLUME_SCALE_FACTOR, 140 * VOLUME_SCALE_FACTOR, 3\n"," )\n"," elif latent_idx == 1:\n"," _draw_double_arrow(\n"," ax, 5 * VOLUME_SCALE_FACTOR, 40 * VOLUME_SCALE_FACTOR, 3\n"," )\n"," ax.set_title('$\\mathrm{RSPINC}_' + f'{latent_idx + 1}$')\n"," # Draw a color palette on the last axis.\n"," cbar = plt.colorbar(\n"," mpl.cm.ScalarMappable(\n"," norm=mpl.colors.Normalize(vmin=-radius, vmax=radius), cmap=cmap\n"," ),\n"," cax=axs[-1],\n"," )\n"," cbar.ax.set_xlabel('Coordinate\\nValue')\n"," plt.tight_layout()\n"," plt.show()"]},{"cell_type":"markdown","metadata":{"id":"ols2RVM8c1sh"},"source":["# Load model and generate spirograms from embedding coordinate perturbation"]},{"cell_type":"code","execution_count":4,"metadata":{"id":"BX0g763-ZrLr","executionInfo":{"status":"ok","timestamp":1717783532267,"user_tz":240,"elapsed":3657,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["rspincs_model = tf.keras.models.load_model('rspincs')"]},{"cell_type":"code","execution_count":5,"metadata":{"id":"_2nYHVXhr6uT","colab":{"base_uri":"https://localhost:8080/","height":307},"executionInfo":{"status":"ok","timestamp":1717783534569,"user_tz":240,"elapsed":2324,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}},"outputId":"f6714e48-aa52-49ec-8860-e4e011dd3235"},"outputs":[{"output_type":"display_data","data":{"text/plain":["
"],"image/png":"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rxf/+te/Gvwc/la99M477whN08S6devExo0bG2yHogewWfeoRKNRlixZwogRI3bvU1WVESNGsHDhwt88Z+HChQ2OBxg5cuQej2/K9qf8sGtF0VatWpGfn8/o0aNZvXr1oYjbJDSn7/+B6N27Nzk5OZx44onMnz8/0XEazX8Wo0pNTd3jMfJnYN8c6fUsyLp2fzS3n4H9JevapvkzkJqaysiRI3nxxRd/s1eopqaGLl26UFRURFFR0e79a9asoaamhq5duwLQpUuXX31f58+fv/vre9KlSxcWLVrUYN9/BvDvryVLlmBZFk8++SRHH300HTt2ZOfOnQ2OsdlsmKbZYF+fPn0wTZOysjLat2/fYMvOzj6gTHujWTdUKioqME2TrKysBvuzsrIoKSn5zXNKSkr26fimbH/K36lTJ1599VU++eQT3njjDSzLYvDgwWzfvv1QRE64PX3//X4/oVAoQakOnZycHF566SU+/PBDPvzwQ/Lz8xk+fDhLly5NdLQDZlkWEyZMYMiQIXTv3n2PxzWnOuBQONLrWZB17f6Qda2sa5t6PfDiiy9imiYDBgzgww8/ZOPGjaxdu5bnnnuOQYMGMWLECHr06MHFF1/M0qVLWbx4MWPHjuXYY4/d/SrXrbfeymuvvcbkyZPZuHEjTz31FNOmTds9SH1PbrzxRl599VWmTJnChg0buPfeew/4Dxnt27cnFovx/PPPs3nzZv7973/z0ksvNTimdevW1NfXM2vWLCoqKggGg3Ts2JGLL76YsWPHMm3aNLZs2cLixYuZNGkSX3zxxQFl2htyHRWpgUGDBjV4D3Lw4MF06dKFl19+mQceeCCByaRDoVOnTnTq1Gn358GDB1NQUMDTTz/9qykVDzfXXnstq1atYt68eYmOIkmyrj3Cybq26Wvbti1Lly7loYce4uabb6a4uJiMjAz69evH5MmTURSFTz75hOuvv55hw4ahqiqjRo3i+eef332NMWPG8Oyzz/LEE09w44030qZNG6ZMmcLw4cN/997nn38+BQUF3HbbbYTDYc4++2yuvvrqAxrD1atXL5566ikeffRR7rzzToYNG8akSZMYO3bs7mMGDx7MVVddxfnnn09lZSX33nsv9913H1OmTOHBBx/k5ptvZseOHaSnp3P00Udz2mmn7XeevdWsGyrp6elomvarOa1LS0v32F2VnZ29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\n"},"metadata":{}}],"source":["generate_rspincs_reconstruction_plot(\n"," vae_model=rspincs_model,\n"," latent_dim=2,\n",")"]}]} \ No newline at end of file +{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"provenance":[]},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"code","source":["#@title Licensed under the BSD-3 License (the \"License\"); { display-mode: \"form\" }\n","# Copyright 2021 Google LLC.\n","#\n","# Redistribution and use in source and binary forms, with or without modification,\n","# are permitted provided that the following conditions are met:\n","#\n","# 1. Redistributions of source code must retain the above copyright notice, this\n","# list of conditions and the following disclaimer.\n","#\n","# 2. Redistributions in binary form must reproduce the above copyright notice,\n","# this list of conditions and the following disclaimer in the documentation\n","# and/or other materials provided with the distribution.\n","#\n","# 3. Neither the name of the copyright holder nor the names of its contributors\n","# may be used to endorse or promote products derived from this software without\n","# specific prior written permission.\n","#\n","# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n","# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n","# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n","# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\n","# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n","# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n","# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\n","# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n","# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n","# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."],"metadata":{"id":"r2mwcs7BPN7G","executionInfo":{"status":"ok","timestamp":1717789843829,"user_tz":240,"elapsed":8,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"execution_count":1,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"TQe5CETGcdwz"},"source":["# Download Keras checkpoints from our GitHub repo"]},{"cell_type":"code","execution_count":null,"metadata":{"id":"a1RXc2pKYPtM"},"outputs":[],"source":["!mkdir -p rspincs/variables\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/saved_model.pb -P rspincs/\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/keras_metadata.pb -P rspincs/\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/variables/variables.data-00000-of-00001 -P rspincs/variables/\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/variables/variables.index -P rspincs/variables/"]},{"cell_type":"markdown","metadata":{"id":"hjRXNyKwcy8T"},"source":["# Imports and functions"]},{"cell_type":"code","execution_count":3,"metadata":{"id":"w6MpGCYoSOgt","executionInfo":{"status":"ok","timestamp":1717789860399,"user_tz":240,"elapsed":14126,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["from typing import Optional\n","\n","import matplotlib as mpl\n","import matplotlib.pyplot as plt\n","import numpy as np\n","import tensorflow as tf"]},{"cell_type":"code","execution_count":4,"metadata":{"id":"CTCzhsgYVt3A","executionInfo":{"status":"ok","timestamp":1717789860641,"user_tz":240,"elapsed":245,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["# The example values for the 5 (standardized) spirogram EDFs:\n","# 'blow_fev1', 'blow_fvc', 'blow_pef', 'blow_ratio', 'blow_fef25_75'\n","EDF_VALUE_EXAMPLE = [-1.8, -1.8, -1.4, -0.7, -1.5]\n","\n","# Note we use 0, 1, ..., 999 for the volume values in flow-volume curves,\n","# which were interpolated between 0 and 6.58.\n","VOLUME_SCALE_FACTOR = 6.58 / 1000\n","\n","\n","def _draw_double_arrow(\n"," ax: mpl.axes.Axes,\n"," x1: float,\n"," x2: float,\n"," y: float,\n"," arrow_color: str = '#d62728',\n","):\n"," \"\"\"Draw an arrow pointing both sides between (x1, y) and (x2, y).\"\"\"\n"," ax.arrow(\n"," x1,\n"," y,\n"," x2 - x1,\n"," 0,\n"," fc=arrow_color,\n"," ec=arrow_color,\n"," width=0.04,\n"," head_width=0.15,\n"," head_length=0.05,\n"," zorder=100,\n"," )\n"," ax.arrow(\n"," x2,\n"," y,\n"," x1 - x2,\n"," 0,\n"," fc=arrow_color,\n"," ec=arrow_color,\n"," width=0.04,\n"," head_width=0.15,\n"," head_length=0.05,\n"," zorder=100,\n"," )\n","\n","\n","def generate_rspincs_reconstruction_plot(\n"," vae_model: tf.keras.Model,\n"," latent_dim: int,\n"," fpath_noext: Optional[str] = None,\n"," dpi=300,\n",") -> None:\n"," \"\"\"Generate reconstructed spirograms while varying each RSPINCs coordinate.\n","\n"," Args:\n"," row: A row of the SPINCs DF from which we'll get the values of manual\n"," features.\n"," vae_model: The VAE model to be used to reconstruct spirograms.\n"," latent_dim: The latent dimension.\n"," fpath_noext: The path to the output image file without extension.\n"," dpi: DPI of the image.\n"," \"\"\"\n"," cmap = plt.get_cmap('viridis')\n"," num_injected_features = 5\n"," radius = 1.5\n"," single_encodings = np.linspace(-radius, radius, num=21)\n"," decoder = vae_model.get_layer(f'{vae_model.name}_decoder')\n"," colorbar_width = 0.2\n","\n"," rescaled_volume = np.arange(1000) * VOLUME_SCALE_FACTOR\n"," _, axs = plt.subplots(\n"," 1,\n"," latent_dim + 1,\n"," figsize=(4 * latent_dim + colorbar_width, 3),\n"," width_ratios=[4] * latent_dim + [colorbar_width],\n"," )\n","\n"," for latent_idx in range(latent_dim):\n"," ax = axs[latent_idx]\n"," for img_idx, single_encoding in enumerate(single_encodings):\n"," # This value should be in [0, 1].\n"," color_val = single_encoding / (radius * 2) + 0.5\n"," encoding = np.zeros(latent_dim)\n"," encoding[latent_idx] = single_encoding\n"," encoding_input = np.expand_dims(encoding, axis=0)\n"," edf_input = np.expand_dims(np.array(EDF_VALUE_EXAMPLE), axis=0)\n"," vae_input = np.concatenate((encoding_input, edf_input), axis=-1)\n"," assert vae_input.shape == (1, latent_dim + num_injected_features)\n"," reconstructed = decoder(vae_input)[0].numpy()[:, 0]\n"," assert len(rescaled_volume) == len(reconstructed)\n"," ax.plot(\n"," rescaled_volume,\n"," reconstructed,\n"," color=cmap(color_val),\n"," alpha=0.9,\n"," linewidth=0.8,\n"," )\n"," ax.set_xlim((-20 * VOLUME_SCALE_FACTOR, 350 * VOLUME_SCALE_FACTOR))\n"," ax.set_ylim((-0.1, 4.2))\n"," ax.set_xlabel('Volume (L)')\n"," # Custom annotation for RSPINCs with dim = 2:\n"," if latent_idx == 0:\n"," ax.set_ylabel('Flow (L/s)')\n"," _draw_double_arrow(\n"," ax, 50 * VOLUME_SCALE_FACTOR, 140 * VOLUME_SCALE_FACTOR, 3\n"," )\n"," elif latent_idx == 1:\n"," _draw_double_arrow(\n"," ax, 5 * VOLUME_SCALE_FACTOR, 40 * VOLUME_SCALE_FACTOR, 3\n"," )\n"," ax.set_title('$\\mathrm{RSPINC}_' + f'{latent_idx + 1}$')\n"," # Draw a color palette on the last axis.\n"," cbar = plt.colorbar(\n"," mpl.cm.ScalarMappable(\n"," norm=mpl.colors.Normalize(vmin=-radius, vmax=radius), cmap=cmap\n"," ),\n"," cax=axs[-1],\n"," )\n"," cbar.ax.set_xlabel('Coordinate\\nValue')\n"," plt.tight_layout()\n"," plt.show()"]},{"cell_type":"markdown","metadata":{"id":"ols2RVM8c1sh"},"source":["# Load model and generate spirograms from embedding coordinate perturbation"]},{"cell_type":"code","execution_count":5,"metadata":{"id":"BX0g763-ZrLr","executionInfo":{"status":"ok","timestamp":1717789871265,"user_tz":240,"elapsed":10626,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["rspincs_model = tf.keras.models.load_model('rspincs')"]},{"cell_type":"code","execution_count":6,"metadata":{"id":"_2nYHVXhr6uT","colab":{"base_uri":"https://localhost:8080/","height":307},"executionInfo":{"status":"ok","timestamp":1717789873484,"user_tz":240,"elapsed":2232,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}},"outputId":"ff216c3b-79e7-4a39-9438-8035534ea568"},"outputs":[{"output_type":"display_data","data":{"text/plain":["
"],"image/png":"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rxf/+te/Gvwc/la99M477whN08S6devExo0bG2yHogewWfeoRKNRlixZwogRI3bvU1WVESNGsHDhwt88Z+HChQ2OBxg5cuQej2/K9qf8sGtF0VatWpGfn8/o0aNZvXr1oYjbJDSn7/+B6N27Nzk5OZx44onMnz8/0XEazX8Wo0pNTd3jMfJnYN8c6fUsyLp2fzS3n4H9JevapvkzkJqaysiRI3nxxRd/s1eopqaGLl26UFRURFFR0e79a9asoaamhq5duwLQpUuXX31f58+fv/vre9KlSxcWLVrUYN9/BvDvryVLlmBZFk8++SRHH300HTt2ZOfOnQ2OsdlsmKbZYF+fPn0wTZOysjLat2/fYMvOzj6gTHujWTdUKioqME2TrKysBvuzsrIoKSn5zXNKSkr26fimbH/K36lTJ1599VU++eQT3njjDSzLYvDgwWzfvv1QRE64PX3//X4/oVAoQakOnZycHF566SU+/PBDPvzwQ/Lz8xk+fDhLly5NdLQDZlkWEyZMYMiQIXTv3n2PxzWnOuBQONLrWZB17f6Qda2sa5t6PfDiiy9imiYDBgzgww8/ZOPGjaxdu5bnnnuOQYMGMWLECHr06MHFF1/M0qVLWbx4MWPHjuXYY4/d/SrXrbfeymuvvcbkyZPZuHEjTz31FNOmTds9SH1PbrzxRl599VWmTJnChg0buPfeew/4Dxnt27cnFovx/PPPs3nzZv7973/z0ksvNTimdevW1NfXM2vWLCoqKggGg3Ts2JGLL76YsWPHMm3aNLZs2cLixYuZNGkSX3zxxQFl2htyHRWpgUGDBjV4D3Lw4MF06dKFl19+mQceeCCByaRDoVOnTnTq1Gn358GDB1NQUMDTTz/9qykVDzfXXnstq1atYt68eYmOIkmyrj3Cybq26Wvbti1Lly7loYce4uabb6a4uJiMjAz69evH5MmTURSFTz75hOuvv55hw4ahqiqjRo3i+eef332NMWPG8Oyzz/LEE09w44030qZNG6ZMmcLw4cN/997nn38+BQUF3HbbbYTDYc4++2yuvvrqAxrD1atXL5566ikeffRR7rzzToYNG8akSZMYO3bs7mMGDx7MVVddxfnnn09lZSX33nsv9913H1OmTOHBBx/k5ptvZseOHaSnp3P00Udz2mmn7XeevdWsGyrp6elomvarOa1LS0v32F2VnZ29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\n"},"metadata":{}}],"source":["generate_rspincs_reconstruction_plot(\n"," vae_model=rspincs_model,\n"," latent_dim=2,\n",")"]}]} \ No newline at end of file diff --git a/regle/analysis/pca_and_spline_fitting.ipynb b/regle/analysis/pca_and_spline_fitting.ipynb index b7f90a8..ce1e0a6 100644 --- a/regle/analysis/pca_and_spline_fitting.ipynb +++ b/regle/analysis/pca_and_spline_fitting.ipynb @@ -1 +1 @@ -{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"provenance":[],"authorship_tag":"ABX9TyOXk/XH0+SqGWRKkccIsj6v"},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"code","execution_count":1,"metadata":{"id":"pa_dhHReC5dH","executionInfo":{"status":"ok","timestamp":1717783677268,"user_tz":240,"elapsed":2316,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["import numpy as np\n","import pandas as pd\n","import scipy\n","from sklearn import decomposition"]},{"cell_type":"markdown","metadata":{"id":"BVm0PPlJHCjX"},"source":["# PCA"]},{"cell_type":"markdown","metadata":{"id":"XsedyAXiHgDM"},"source":["For PCA we require population-level data. We assume `data_matrix` is a Pandas dataframe whose rows correspond to individuals and columns correspond to data points. We simulate this data in this notebook as we don't have access to the real population-level data."]},{"cell_type":"code","execution_count":2,"metadata":{"id":"eJFBpnleHBqS","executionInfo":{"status":"ok","timestamp":1717783677917,"user_tz":240,"elapsed":654,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["np.random.seed(42)\n","data_matrix = pd.DataFrame(np.random.normal(size=(10000, 1000)))"]},{"cell_type":"code","execution_count":3,"metadata":{"id":"AFbcJIqiHyg7","executionInfo":{"status":"ok","timestamp":1717783677919,"user_tz":240,"elapsed":10,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["def standardize_df(df: pd.DataFrame) -> pd.DataFrame:\n"," \"\"\"Standardizes a dataframe (mean=0, var=1).\"\"\"\n"," return (df - df.mean()) / df.std(ddof=0)\n","\n","\n","def generate_pc(\n"," data_matrix: pd.DataFrame, num_pc: int, standardize: bool = True\n",") -> pd.DataFrame:\n"," \"\"\"Generates principal components (PCs) of the given data matrix.\n","\n"," Args:\n"," data_matrix: The data matrix.\n"," num_pc: The number of PCs to compute.\n"," standardize: True to standardize the data matrix before computing PCs.\n","\n"," Returns:\n"," A matrix of PCs of the data matrix.\n"," \"\"\"\n"," original_shape = data_matrix.shape\n"," if standardize:\n"," data_matrix = standardize_df(data_matrix)\n"," # Replace NaN values with 0 (this can happen when some col has var=0).\n"," data_matrix.fillna(0, inplace=True)\n"," assert data_matrix.shape == original_shape\n"," pca = decomposition.PCA(num_pc)\n"," pc_np = pca.fit_transform(data_matrix)\n"," print('PCA explained variance:', pca.explained_variance_)\n"," print(\n"," 'PCA explained variance (proportion):',\n"," pca.explained_variance_ / np.sum(pca.explained_variance_),\n"," )\n"," assert pc_np.shape == (original_shape[0], num_pc)\n"," return pd.DataFrame(pc_np)"]},{"cell_type":"code","execution_count":4,"metadata":{"colab":{"height":241,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":2381,"status":"ok","timestamp":1717783680293,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"zlBLtbM4IQ53","outputId":"51512bd5-3814-467a-fcda-8aad45ac220f"},"outputs":[{"output_type":"stream","name":"stdout","text":["PCA explained variance: [1.63972209 1.63070323 1.62260396 1.61134043 1.590792 ]\n","PCA explained variance (proportion): [0.20255582 0.20144171 0.2004412 0.19904981 0.19651145]\n"]},{"output_type":"execute_result","data":{"text/plain":[" 0 1 2 3 4\n","0 -2.371899 -0.643403 -0.397528 0.505243 -1.672120\n","1 -0.389563 -0.316097 -0.054947 -1.539366 -0.998421\n","2 -0.278895 -1.904815 0.019068 -0.700896 0.973568\n","3 3.261174 -0.036879 2.362755 -1.733982 0.587677\n","4 0.172324 0.537071 -0.351281 -1.236673 1.708548"],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
01234
0-2.371899-0.643403-0.3975280.505243-1.672120
1-0.389563-0.316097-0.054947-1.539366-0.998421
2-0.278895-1.9048150.019068-0.7008960.973568
33.261174-0.0368792.362755-1.7339820.587677
40.1723240.537071-0.351281-1.2366731.708548
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n"," \n","\n","\n","\n"," \n","
\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","variable_name":"pc_dataframe","summary":"{\n \"name\": \"pc_dataframe\",\n \"rows\": 10000,\n \"fields\": [\n {\n \"column\": 0,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2805163386985488,\n \"min\": -4.643045981269673,\n \"max\": 5.017698894439442,\n \"num_unique_values\": 10000,\n \"samples\": [\n -0.3224716522127656,\n 0.6031338243822927,\n -1.2993299471423263\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 1,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2769899114607324,\n \"min\": -4.448045764841815,\n \"max\": 5.101647474079014,\n \"num_unique_values\": 10000,\n \"samples\": [\n 0.286864855151227,\n -0.6597526194886669,\n -0.4896683064067677\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 2,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.273814728228805,\n \"min\": -4.328973725102052,\n \"max\": 4.872664420026113,\n \"num_unique_values\": 10000,\n \"samples\": [\n -0.6794583220950966,\n 1.9140526678288383,\n -0.4004464395670121\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 3,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2693858467445038,\n \"min\": -4.939769834236929,\n \"max\": 4.99450956625324,\n \"num_unique_values\": 10000,\n \"samples\": [\n 2.225402267644631,\n -0.9588695150842595,\n 1.2924768168268101\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 4,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2612660288538398,\n \"min\": -5.007116466188265,\n \"max\": 5.3472410625736035,\n \"num_unique_values\": 10000,\n \"samples\": [\n 0.19752305167345738,\n -1.0272444388147874,\n -0.010101932326369557\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":4}],"source":["pc_dataframe = generate_pc(\n"," data_matrix,\n"," num_pc=5)\n","\n","pc_dataframe.head()"]},{"cell_type":"markdown","metadata":{"id":"j9tneSsvG5vg"},"source":["# Spline fitting"]},{"cell_type":"code","execution_count":5,"metadata":{"id":"dALiJbUGDghc","executionInfo":{"status":"ok","timestamp":1717783680294,"user_tz":240,"elapsed":15,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["def compute_spline_coefficients(\n"," arr: np.ndarray, knot_position: int\n",") -> np.ndarray:\n"," \"\"\"Gets cubic spline coefficients with a single knot.\n","\n"," We use a single knot which is padded by 4 (= k + 1) boundaries on each side,\n"," where k=3 (cubic) is the degree in this case.\n","\n"," The results are 5 coefficients padded by 4 zeros at the end. We remove the\n"," last 4 zeros.\n","\n"," For more details, see https://en.wikipedia.org/wiki/B-spline and\n"," https://docs.scipy.org/doc/scipy/tutorial/interpolate/smoothing_splines.html#procedural-splrep\n","\n"," Args:\n"," arr: The target numpy array for 1D spline fitting.\n"," knot_position: The position of the single knot.\n","\n"," Returns:\n"," A numpy array of 5 cubic spline coefficients.\n"," \"\"\"\n"," num_points = len(arr)\n"," assert arr.shape == (num_points,)\n"," assert 0 < knot_position < num_points - 1\n"," spline = scipy.interpolate.splrep(\n"," x=np.arange(num_points),\n"," y=arr,\n"," k=3,\n"," task=-1,\n"," t=[knot_position],\n"," )\n"," bspline_coefficients = spline[1]\n"," assert np.array_equal(bspline_coefficients[5:], np.array([0, 0, 0, 0]))\n"," return bspline_coefficients[:5]"]},{"cell_type":"code","execution_count":6,"metadata":{"id":"JPYKbetRCGs5","executionInfo":{"status":"ok","timestamp":1717783680294,"user_tz":240,"elapsed":13,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["MAX_NUM_POINTS = 1000\n","VOLUME_SCALE_FACTOR = 0.001\n","KNOT_POSITION = 199"]},{"cell_type":"markdown","metadata":{"id":"l7XaODNrEXgU"},"source":["`example_curve` variable below should be a 1D numpy array that contains a single curve, such as a spirogram.\n","\n","Here we use an example curve copied from a UK Biobank example at https://biobank.ctsu.ox.ac.uk/crystal/ukb/examples/eg_spiro_3066.dat"]},{"cell_type":"code","execution_count":7,"metadata":{"id":"Dur9LHMQD_B3","executionInfo":{"status":"ok","timestamp":1717783680294,"user_tz":240,"elapsed":12,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["example_curve_txt = '0,0,0,0,3,10,25,54,101,169,258,363,478,589,689,785,879,970,1059,1147,1234,1320,1403,1486,1569,1650,1730,1809,1888,1965,2040,2116,2188,2261,2331,2400,2465,2532,2595,2658,2720,2780,2838,2894,2948,3001,3052,3102,3151,3197,3243,3287,3329,3371,3412,3451,3490,3527,3564,3600,3635,3670,3703,3736,3769,3800,3831,3861,3890,3918,3947,3974,4001,4028,4054,4080,4105,4130,4154,4179,4202,4226,4249,4271,4292,4312,4332,4351,4371,4390,4408,4426,4444,4461,4478,4495,4512,4528,4544,4560,4575,4590,4604,4619,4633,4647,4661,4675,4689,4703,4716,4729,4742,4755,4767,4779,4791,4802,4812,4822,4831,4840,4849,4857,4866,4874,4882,4890,4898,4906,4914,4921,4929,4936,4944,4951,4958,4966,4973,4980,4987,4994,5000,5007,5013,5020,5026,5033,5039,5045,5051,5057,5063,5069,5075,5081,5087,5092,5098,5104,5109,5114,5119,5125,5130,5134,5139,5144,5148,5153,5157,5161,5166,5170,5174,5178,5182,5186,5190,5194,5198,5202,5205,5209,5213,5216,5220,5223,5226,5230,5233,5236,5240,5243,5246,5250,5253,5256,5259,5262,5264,5267,5270,5273,5276,5279,5283,5286,5289,5292,5295,5298,5300,5303,5306,5308,5311,5314,5316,5319,5321,5323,5326,5328,5331,5333,5335,5338,5340,5343,5345,5348,5350,5352,5355,5357,5360,5362,5365,5367,5369,5372,5374,5377,5379,5381,5384,5386,5388,5390,5391,5393,5395,5397,5399,5401,5403,5404,5406,5408,5410,5412,5413,5415,5417,5419,5420,5422,5424,5426,5427,5429,5431,5432,5434,5436,5438,5439,5441,5443,5444,5446,5447,5449,5450,5452,5453,5455,5456,5457,5459,5460,5461,5462,5463,5464,5466,5467,5468,5470,5471,5473,5474,5476,5477,5478,5480,5481,5482,5484,5485,5486,5487,5489,5490,5491,5492,5493,5494,5496,5497,5498,5499,5500,5501,5502,5503,5504,5505,5506,5507,5508,5509,5510,5510,5511,5512,5513,5514,5515,5515,5516,5517,5519,5520,5521,5523,5524,5525,5527,5529,5530,5532,5533,5535,5536,5537,5539,5540,5541,5543,5544,5545,5545,5546,5547,5548,5549,5549,5550,5551,5552,5552,5553,5554,5554,5555,5556,5557,5557,5558,5559,5560,5560,5561,5562,5562,5563,5564,5564,5565,5565,5566,5567,5567,5568,5569,5570,5571,5572,5573,5574,5576,5577,5578,5579,5580,5582,5583,5584,5585,5587,5588,5589,5590,5591,5591,5592,5593,5594,5595,5596,5596,5597,5598,5598,5599,5600,5601,5601,5602,5603,5603,5604,5605,5606,5606,5607,5608,5608,5609,5609,5609,5610,5611,5611,5612,5613,5613,5614,5615,5616,5616,5617,5618,5618,5619,5620,5621,5622,5623,5624,5624,5625,5626,5626,5627,5628,5628,5629,5629,5630,5630,5631,5632,5632,5633,5633,5634,5635,5635,5636,5637,5637,5638,5639,5639,5640,5641,5642,5642,5643,5644,5645,5645,5646,5647,5647,5648,5649,5649,5650,5651,5651,5652,5652,5653,5654,5654,5655,5656,5656,5657,5658,5658,5659,5660,5660,5661,5661,5662,5663,5663,5664,5664,5665,5665,5666,5666,5667,5667,5668,5668,5669,5669,5670,5670,5670,5671,5671,5672,5672,5672,5673,5673,5673,5673,5674,5674,5674,5675,5676,5676,5677,5677,5678,5678,5679,5679,5680,5681,5681,5682,5683,5683,5684,5684,5685,5686,5686,5687,5687,5688,5688,5688,5689,5689,5690,5690,5690,5691,5691,5692,5692,5692,5693,5693,5694,5694,5694,5695,5695,5695,5696,5696,5696,5696,5696,5696,5697,5697,5698,5698,5698,5699,5699,5699,5699,5700,5700,5700,5701,5701,5702,5702,5703,5703,5704,5704,5705,5705,5706,5706,5707,5707,5708,5709,5709,5710,5710,5711,5711,5712,5712,5712,5713,5713,5713,5714,5714,5714,5715,5715,5716,5716,5716,5717,5717,5717,5718,5718,5719,5719,5720,5720,5721,5721,5721,5722,5722,5722,5723,5723,5723,5723,5724,5724,5724,5725,5725,5725,5726,5726,5726,5727,5727,5728,5728,5729,5729,5729,5730,5730,5731,5732,5732,5733,5733,5734,5735,5735,5735,5736,5736,5736,5737,5737,5737,5738,5738,5738,5739,5739,5739,5739,5740,5740,5740,5741,5741,5741,5741,5741,5741,5742,5742,5742,5742,5742,5742,5742,5742,5742,5742,5741,5741,5740,5740,5740,5740,5739,5739,5739,5739,5739,5739,5740,5740,5740,5741,5742,5742,5743,5743,5744,5745,5745,5745,5746,5746,5747,5747,5748,5748,5748,5748,5748,5748,5749,5749,5749,5749,5749,5749,5749,5750,5750,5750,5750,5750,5751,5751,5751,5752,5752,5753,5753,5754,5754,5754,5755,5755,5756,5756,5756,5757,5757,5757,5758,5758,5758,5758,5759,5759,5759,5759,5759,5759,5759,5759,5759,5760,5760,5760,5761,5761,5761,5762,5762,5763,5763,5763,5764,5764,5764,5765,5765,5766,5766,5766,5767,5767,5767,5767,5767,5768,5768,5768,5768,5769,5769,5769,5770,5770,5770,5770,5770,5771,5771,5771,5771,5771,5772,5772,5772,5773,5773,5773,5774,5774,5774,5775,5775,5775,5776,5776,5777,5777,5777,5778,5778,5778,5778,5779,5779,5779,5779,5779,5779,5779,5779,5779,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5780,5780,5780,5780,5781,5781,5781,5782,5782,5782,5783,5783,5783,5784,5784,5784,5785,5785,5785,5785,5785,5786,5786,5786,5786,5786,5786,5786,5787,5787,5787,5788,5788,5788,5789,5789,5789,5790,5790,5790,5791,5791,5792,5792,5792,5793,5793,5793,5794,5794,5795,5795,5795,5796,5796,5796,5797,5797,5798,5798,5798,5798,5798,5799,5799,5799,5799,5800,5800,5800,5801,5801,5801,5801,5802,5802,5802,5802,5803,5803,5803,5803,5803,5803,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5803,5804,5804,5804,5804,5804,5805,5805,5805,5805,5806,5806,5806,5806,5806,5806,5806,5806,5806,5806,5807,5807,5807,5807,5808,5808,5809,5809,5809,5810,5810,5810,5811,5811,5812,5812,5813,5813,5813,5814,5814,5815,5815,5815,5815,5816,5816,5816,5816,5817,5817,5817,5817,5817,5817,5817,5818,5818,5818,5818,5818,5818,5818,5819,5819,5819,5819,5819,5819,5819,5819,5819,5819,5820,5820,5820,5820,5820,5820,5820,5820,5820,5819,5820,5820,5820,5820,5820,5820,5820,5820,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5820,5820,5820,5819,5819,5818,5818,5818,5817,5817,5817,5816,5816,5816,5816,5815,5815,5815,5816,5816,5816,5817,5817,5818,5819,5819,5820,5821,5822,5823,5823,5824,5825,5826,5827,5827,5828,5828,5829,5829,5829,5830,5830,5831,5831,5831,5831,5831,5832,5831,5832,5832,5832,5832,5832,5832,5832,5833,5833,5833,5833,5833,5833,5833,5834,5834,5834,5834,5834,5835,5835,5835,5835,5835,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5835,5835,5835,5835,5834,5834,5834,5834,5833,5833,5833,5833,5833,5832,5832,5832,5832,5832,5832,5832,5832,5831'\n","example_curve = (\n"," np.array(example_curve_txt.split(',')[:MAX_NUM_POINTS], dtype=np.float32)\n"," * VOLUME_SCALE_FACTOR\n",")"]},{"cell_type":"markdown","metadata":{"id":"YHiRGraVEhBf"},"source":["The following code generates the 5 spline coefficients the this curve."]},{"cell_type":"code","execution_count":8,"metadata":{"executionInfo":{"elapsed":13,"status":"ok","timestamp":1717783680295,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"Emoh7tdNCQPv","outputId":"40138b4d-87f6-42b2-ce87-37d4e9cfec99","colab":{"base_uri":"https://localhost:8080/"}},"outputs":[{"output_type":"stream","name":"stdout","text":["[-0.08101105 5.14773236 5.63775992 5.81692895 5.78074777]\n"]}],"source":["print(\n"," compute_spline_coefficients(arr=example_curve, knot_position=KNOT_POSITION)\n",")"]}]} \ No newline at end of file +{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"provenance":[],"authorship_tag":"ABX9TyOWuQ668bwnB28rOF2BEzg+"},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"code","source":["#@title Licensed under the BSD-3 License (the \"License\"); { display-mode: \"form\" }\n","# Copyright 2021 Google LLC.\n","#\n","# Redistribution and use in source and binary forms, with or without modification,\n","# are permitted provided that the following conditions are met:\n","#\n","# 1. Redistributions of source code must retain the above copyright notice, this\n","# list of conditions and the following disclaimer.\n","#\n","# 2. Redistributions in binary form must reproduce the above copyright notice,\n","# this list of conditions and the following disclaimer in the documentation\n","# and/or other materials provided with the distribution.\n","#\n","# 3. Neither the name of the copyright holder nor the names of its contributors\n","# may be used to endorse or promote products derived from this software without\n","# specific prior written permission.\n","#\n","# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n","# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n","# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n","# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\n","# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n","# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n","# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\n","# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n","# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n","# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."],"metadata":{"id":"SqQ7C3xXPfn7","executionInfo":{"status":"ok","timestamp":1717789955106,"user_tz":240,"elapsed":18,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"execution_count":1,"outputs":[]},{"cell_type":"code","execution_count":2,"metadata":{"id":"pa_dhHReC5dH","executionInfo":{"status":"ok","timestamp":1717789958175,"user_tz":240,"elapsed":3082,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["import numpy as np\n","import pandas as pd\n","import scipy\n","from sklearn import decomposition"]},{"cell_type":"markdown","metadata":{"id":"BVm0PPlJHCjX"},"source":["# PCA"]},{"cell_type":"markdown","metadata":{"id":"XsedyAXiHgDM"},"source":["For PCA we require population-level data. We assume `data_matrix` is a Pandas dataframe whose rows correspond to individuals and columns correspond to data points. We simulate this data in this notebook as we don't have access to the real population-level data."]},{"cell_type":"code","execution_count":3,"metadata":{"id":"eJFBpnleHBqS","executionInfo":{"status":"ok","timestamp":1717789959746,"user_tz":240,"elapsed":1574,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["np.random.seed(42)\n","data_matrix = pd.DataFrame(np.random.normal(size=(10000, 1000)))"]},{"cell_type":"code","execution_count":4,"metadata":{"id":"AFbcJIqiHyg7","executionInfo":{"status":"ok","timestamp":1717789959747,"user_tz":240,"elapsed":5,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["def standardize_df(df: pd.DataFrame) -> pd.DataFrame:\n"," \"\"\"Standardizes a dataframe (mean=0, var=1).\"\"\"\n"," return (df - df.mean()) / df.std(ddof=0)\n","\n","\n","def generate_pc(\n"," data_matrix: pd.DataFrame, num_pc: int, standardize: bool = True\n",") -> pd.DataFrame:\n"," \"\"\"Generates principal components (PCs) of the given data matrix.\n","\n"," Args:\n"," data_matrix: The data matrix.\n"," num_pc: The number of PCs to compute.\n"," standardize: True to standardize the data matrix before computing PCs.\n","\n"," Returns:\n"," A matrix of PCs of the data matrix.\n"," \"\"\"\n"," original_shape = data_matrix.shape\n"," if standardize:\n"," data_matrix = standardize_df(data_matrix)\n"," # Replace NaN values with 0 (this can happen when some col has var=0).\n"," data_matrix.fillna(0, inplace=True)\n"," assert data_matrix.shape == original_shape\n"," pca = decomposition.PCA(num_pc)\n"," pc_np = pca.fit_transform(data_matrix)\n"," print('PCA explained variance:', pca.explained_variance_)\n"," print(\n"," 'PCA explained variance (proportion):',\n"," pca.explained_variance_ / np.sum(pca.explained_variance_),\n"," )\n"," assert pc_np.shape == (original_shape[0], num_pc)\n"," return pd.DataFrame(pc_np)"]},{"cell_type":"code","execution_count":5,"metadata":{"colab":{"height":241,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":3135,"status":"ok","timestamp":1717789962878,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"zlBLtbM4IQ53","outputId":"67fb9819-43f5-4182-bf21-e6e84b84d2a4"},"outputs":[{"output_type":"stream","name":"stdout","text":["PCA explained variance: [1.63972209 1.63070323 1.62260396 1.61134043 1.590792 ]\n","PCA explained variance (proportion): [0.20255582 0.20144171 0.2004412 0.19904981 0.19651145]\n"]},{"output_type":"execute_result","data":{"text/plain":[" 0 1 2 3 4\n","0 -2.371899 -0.643403 -0.397528 0.505243 -1.672120\n","1 -0.389563 -0.316097 -0.054947 -1.539366 -0.998421\n","2 -0.278895 -1.904815 0.019068 -0.700896 0.973568\n","3 3.261174 -0.036879 2.362755 -1.733982 0.587677\n","4 0.172324 0.537071 -0.351281 -1.236673 1.708548"],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
01234
0-2.371899-0.643403-0.3975280.505243-1.672120
1-0.389563-0.316097-0.054947-1.539366-0.998421
2-0.278895-1.9048150.019068-0.7008960.973568
33.261174-0.0368792.362755-1.7339820.587677
40.1723240.537071-0.351281-1.2366731.708548
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n"," \n","\n","\n","\n"," \n","
\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","variable_name":"pc_dataframe","summary":"{\n \"name\": \"pc_dataframe\",\n \"rows\": 10000,\n \"fields\": [\n {\n \"column\": 0,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2805163386985488,\n \"min\": -4.643045981269673,\n \"max\": 5.017698894439442,\n \"num_unique_values\": 10000,\n \"samples\": [\n -0.3224716522127656,\n 0.6031338243822927,\n -1.2993299471423263\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 1,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2769899114607324,\n \"min\": -4.448045764841815,\n \"max\": 5.101647474079014,\n \"num_unique_values\": 10000,\n \"samples\": [\n 0.286864855151227,\n -0.6597526194886669,\n -0.4896683064067677\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 2,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.273814728228805,\n \"min\": -4.328973725102052,\n \"max\": 4.872664420026113,\n \"num_unique_values\": 10000,\n \"samples\": [\n -0.6794583220950966,\n 1.9140526678288383,\n -0.4004464395670121\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 3,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2693858467445038,\n \"min\": -4.939769834236929,\n \"max\": 4.99450956625324,\n \"num_unique_values\": 10000,\n \"samples\": [\n 2.225402267644631,\n -0.9588695150842595,\n 1.2924768168268101\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 4,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2612660288538398,\n \"min\": -5.007116466188265,\n \"max\": 5.3472410625736035,\n \"num_unique_values\": 10000,\n \"samples\": [\n 0.19752305167345738,\n -1.0272444388147874,\n -0.010101932326369557\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":5}],"source":["pc_dataframe = generate_pc(\n"," data_matrix,\n"," num_pc=5)\n","\n","pc_dataframe.head()"]},{"cell_type":"markdown","metadata":{"id":"j9tneSsvG5vg"},"source":["# Spline fitting"]},{"cell_type":"code","execution_count":6,"metadata":{"id":"dALiJbUGDghc","executionInfo":{"status":"ok","timestamp":1717789962878,"user_tz":240,"elapsed":26,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["def compute_spline_coefficients(\n"," arr: np.ndarray, knot_position: int\n",") -> np.ndarray:\n"," \"\"\"Gets cubic spline coefficients with a single knot.\n","\n"," We use a single knot which is padded by 4 (= k + 1) boundaries on each side,\n"," where k=3 (cubic) is the degree in this case.\n","\n"," The results are 5 coefficients padded by 4 zeros at the end. We remove the\n"," last 4 zeros.\n","\n"," For more details, see https://en.wikipedia.org/wiki/B-spline and\n"," https://docs.scipy.org/doc/scipy/tutorial/interpolate/smoothing_splines.html#procedural-splrep\n","\n"," Args:\n"," arr: The target numpy array for 1D spline fitting.\n"," knot_position: The position of the single knot.\n","\n"," Returns:\n"," A numpy array of 5 cubic spline coefficients.\n"," \"\"\"\n"," num_points = len(arr)\n"," assert arr.shape == (num_points,)\n"," assert 0 < knot_position < num_points - 1\n"," spline = scipy.interpolate.splrep(\n"," x=np.arange(num_points),\n"," y=arr,\n"," k=3,\n"," task=-1,\n"," t=[knot_position],\n"," )\n"," bspline_coefficients = spline[1]\n"," assert np.array_equal(bspline_coefficients[5:], np.array([0, 0, 0, 0]))\n"," return bspline_coefficients[:5]"]},{"cell_type":"code","execution_count":7,"metadata":{"id":"JPYKbetRCGs5","executionInfo":{"status":"ok","timestamp":1717789962879,"user_tz":240,"elapsed":24,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["MAX_NUM_POINTS = 1000\n","VOLUME_SCALE_FACTOR = 0.001\n","KNOT_POSITION = 199"]},{"cell_type":"markdown","metadata":{"id":"l7XaODNrEXgU"},"source":["`example_curve` variable below should be a 1D numpy array that contains a single curve, such as a spirogram.\n","\n","Here we use an example curve copied from a UK Biobank example at https://biobank.ctsu.ox.ac.uk/crystal/ukb/examples/eg_spiro_3066.dat"]},{"cell_type":"code","execution_count":8,"metadata":{"id":"Dur9LHMQD_B3","executionInfo":{"status":"ok","timestamp":1717789962879,"user_tz":240,"elapsed":22,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["example_curve_txt = '0,0,0,0,3,10,25,54,101,169,258,363,478,589,689,785,879,970,1059,1147,1234,1320,1403,1486,1569,1650,1730,1809,1888,1965,2040,2116,2188,2261,2331,2400,2465,2532,2595,2658,2720,2780,2838,2894,2948,3001,3052,3102,3151,3197,3243,3287,3329,3371,3412,3451,3490,3527,3564,3600,3635,3670,3703,3736,3769,3800,3831,3861,3890,3918,3947,3974,4001,4028,4054,4080,4105,4130,4154,4179,4202,4226,4249,4271,4292,4312,4332,4351,4371,4390,4408,4426,4444,4461,4478,4495,4512,4528,4544,4560,4575,4590,4604,4619,4633,4647,4661,4675,4689,4703,4716,4729,4742,4755,4767,4779,4791,4802,4812,4822,4831,4840,4849,4857,4866,4874,4882,4890,4898,4906,4914,4921,4929,4936,4944,4951,4958,4966,4973,4980,4987,4994,5000,5007,5013,5020,5026,5033,5039,5045,5051,5057,5063,5069,5075,5081,5087,5092,5098,5104,5109,5114,5119,5125,5130,5134,5139,5144,5148,5153,5157,5161,5166,5170,5174,5178,5182,5186,5190,5194,5198,5202,5205,5209,5213,5216,5220,5223,5226,5230,5233,5236,5240,5243,5246,5250,5253,5256,5259,5262,5264,5267,5270,5273,5276,5279,5283,5286,5289,5292,5295,5298,5300,5303,5306,5308,5311,5314,5316,5319,5321,5323,5326,5328,5331,5333,5335,5338,5340,5343,5345,5348,5350,5352,5355,5357,5360,5362,5365,5367,5369,5372,5374,5377,5379,5381,5384,5386,5388,5390,5391,5393,5395,5397,5399,5401,5403,5404,5406,5408,5410,5412,5413,5415,5417,5419,5420,5422,5424,5426,5427,5429,5431,5432,5434,5436,5438,5439,5441,5443,5444,5446,5447,5449,5450,5452,5453,5455,5456,5457,5459,5460,5461,5462,5463,5464,5466,5467,5468,5470,5471,5473,5474,5476,5477,5478,5480,5481,5482,5484,5485,5486,5487,5489,5490,5491,5492,5493,5494,5496,5497,5498,5499,5500,5501,5502,5503,5504,5505,5506,5507,5508,5509,5510,5510,5511,5512,5513,5514,5515,5515,5516,5517,5519,5520,5521,5523,5524,5525,5527,5529,5530,5532,5533,5535,5536,5537,5539,5540,5541,5543,5544,5545,5545,5546,5547,5548,5549,5549,5550,5551,5552,5552,5553,5554,5554,5555,5556,5557,5557,5558,5559,5560,5560,5561,5562,5562,5563,5564,5564,5565,5565,5566,5567,5567,5568,5569,5570,5571,5572,5573,5574,5576,5577,5578,5579,5580,5582,5583,5584,5585,5587,5588,5589,5590,5591,5591,5592,5593,5594,5595,5596,5596,5597,5598,5598,5599,5600,5601,5601,5602,5603,5603,5604,5605,5606,5606,5607,5608,5608,5609,5609,5609,5610,5611,5611,5612,5613,5613,5614,5615,5616,5616,5617,5618,5618,5619,5620,5621,5622,5623,5624,5624,5625,5626,5626,5627,5628,5628,5629,5629,5630,5630,5631,5632,5632,5633,5633,5634,5635,5635,5636,5637,5637,5638,5639,5639,5640,5641,5642,5642,5643,5644,5645,5645,5646,5647,5647,5648,5649,5649,5650,5651,5651,5652,5652,5653,5654,5654,5655,5656,5656,5657,5658,5658,5659,5660,5660,5661,5661,5662,5663,5663,5664,5664,5665,5665,5666,5666,5667,5667,5668,5668,5669,5669,5670,5670,5670,5671,5671,5672,5672,5672,5673,5673,5673,5673,5674,5674,5674,5675,5676,5676,5677,5677,5678,5678,5679,5679,5680,5681,5681,5682,5683,5683,5684,5684,5685,5686,5686,5687,5687,5688,5688,5688,5689,5689,5690,5690,5690,5691,5691,5692,5692,5692,5693,5693,5694,5694,5694,5695,5695,5695,5696,5696,5696,5696,5696,5696,5697,5697,5698,5698,5698,5699,5699,5699,5699,5700,5700,5700,5701,5701,5702,5702,5703,5703,5704,5704,5705,5705,5706,5706,5707,5707,5708,5709,5709,5710,5710,5711,5711,5712,5712,5712,5713,5713,5713,5714,5714,5714,5715,5715,5716,5716,5716,5717,5717,5717,5718,5718,5719,5719,5720,5720,5721,5721,5721,5722,5722,5722,5723,5723,5723,5723,5724,5724,5724,5725,5725,5725,5726,5726,5726,5727,5727,5728,5728,5729,5729,5729,5730,5730,5731,5732,5732,5733,5733,5734,5735,5735,5735,5736,5736,5736,5737,5737,5737,5738,5738,5738,5739,5739,5739,5739,5740,5740,5740,5741,5741,5741,5741,5741,5741,5742,5742,5742,5742,5742,5742,5742,5742,5742,5742,5741,5741,5740,5740,5740,5740,5739,5739,5739,5739,5739,5739,5740,5740,5740,5741,5742,5742,5743,5743,5744,5745,5745,5745,5746,5746,5747,5747,5748,5748,5748,5748,5748,5748,5749,5749,5749,5749,5749,5749,5749,5750,5750,5750,5750,5750,5751,5751,5751,5752,5752,5753,5753,5754,5754,5754,5755,5755,5756,5756,5756,5757,5757,5757,5758,5758,5758,5758,5759,5759,5759,5759,5759,5759,5759,5759,5759,5760,5760,5760,5761,5761,5761,5762,5762,5763,5763,5763,5764,5764,5764,5765,5765,5766,5766,5766,5767,5767,5767,5767,5767,5768,5768,5768,5768,5769,5769,5769,5770,5770,5770,5770,5770,5771,5771,5771,5771,5771,5772,5772,5772,5773,5773,5773,5774,5774,5774,5775,5775,5775,5776,5776,5777,5777,5777,5778,5778,5778,5778,5779,5779,5779,5779,5779,5779,5779,5779,5779,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5780,5780,5780,5780,5781,5781,5781,5782,5782,5782,5783,5783,5783,5784,5784,5784,5785,5785,5785,5785,5785,5786,5786,5786,5786,5786,5786,5786,5787,5787,5787,5788,5788,5788,5789,5789,5789,5790,5790,5790,5791,5791,5792,5792,5792,5793,5793,5793,5794,5794,5795,5795,5795,5796,5796,5796,5797,5797,5798,5798,5798,5798,5798,5799,5799,5799,5799,5800,5800,5800,5801,5801,5801,5801,5802,5802,5802,5802,5803,5803,5803,5803,5803,5803,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5803,5804,5804,5804,5804,5804,5805,5805,5805,5805,5806,5806,5806,5806,5806,5806,5806,5806,5806,5806,5807,5807,5807,5807,5808,5808,5809,5809,5809,5810,5810,5810,5811,5811,5812,5812,5813,5813,5813,5814,5814,5815,5815,5815,5815,5816,5816,5816,5816,5817,5817,5817,5817,5817,5817,5817,5818,5818,5818,5818,5818,5818,5818,5819,5819,5819,5819,5819,5819,5819,5819,5819,5819,5820,5820,5820,5820,5820,5820,5820,5820,5820,5819,5820,5820,5820,5820,5820,5820,5820,5820,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5820,5820,5820,5819,5819,5818,5818,5818,5817,5817,5817,5816,5816,5816,5816,5815,5815,5815,5816,5816,5816,5817,5817,5818,5819,5819,5820,5821,5822,5823,5823,5824,5825,5826,5827,5827,5828,5828,5829,5829,5829,5830,5830,5831,5831,5831,5831,5831,5832,5831,5832,5832,5832,5832,5832,5832,5832,5833,5833,5833,5833,5833,5833,5833,5834,5834,5834,5834,5834,5835,5835,5835,5835,5835,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5835,5835,5835,5835,5834,5834,5834,5834,5833,5833,5833,5833,5833,5832,5832,5832,5832,5832,5832,5832,5832,5831'\n","example_curve = (\n"," np.array(example_curve_txt.split(',')[:MAX_NUM_POINTS], dtype=np.float32)\n"," * VOLUME_SCALE_FACTOR\n",")"]},{"cell_type":"markdown","metadata":{"id":"YHiRGraVEhBf"},"source":["The following code generates the 5 spline coefficients the this curve."]},{"cell_type":"code","execution_count":9,"metadata":{"executionInfo":{"elapsed":278,"status":"ok","timestamp":1717789963136,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"Emoh7tdNCQPv","outputId":"e8bafe1b-4a0f-460c-8a7c-438a21fdfa69","colab":{"base_uri":"https://localhost:8080/"}},"outputs":[{"output_type":"stream","name":"stdout","text":["[-0.08101105 5.14773236 5.63775992 5.81692895 5.78074777]\n"]}],"source":["print(\n"," compute_spline_coefficients(arr=example_curve, knot_position=KNOT_POSITION)\n",")"]}]} \ No newline at end of file diff --git a/regle/analysis/prs_analysis.ipynb b/regle/analysis/prs_analysis.ipynb index 960c327..2daa9dc 100644 --- a/regle/analysis/prs_analysis.ipynb +++ b/regle/analysis/prs_analysis.ipynb @@ -1 +1 @@ -{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"provenance":[],"authorship_tag":"ABX9TyMyYVUUHcnCAGY5yxymQ6+C"},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"markdown","metadata":{"id":"VbyGa_IhXRgk"},"source":["# Preparation\n","\n","This section includes imports and functions."]},{"cell_type":"code","execution_count":1,"metadata":{"id":"otMyZHIW0Fqs","executionInfo":{"status":"ok","timestamp":1717783770114,"user_tz":240,"elapsed":2320,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["import dataclasses\n","from typing import Dict, List, Optional, Sequence, Union\n","\n","import abc\n","from typing import Callable\n","\n","import numpy as np\n","import pandas as pd\n","import scipy.stats\n","import sklearn\n","import sklearn.metrics\n","from sklearn import metrics"]},{"cell_type":"code","execution_count":2,"metadata":{"id":"J8pr2zMLzmDH","executionInfo":{"status":"ok","timestamp":1717783770322,"user_tz":240,"elapsed":211,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["# A function that computes a numeric outcome from label and prediction arrays.\n","BootstrappableFn = Callable[[np.ndarray, np.ndarray], float]\n","\n","# Constants denoting the expected case and control values for binary encodings.\n","BINARY_LABEL_CONTROL = 0\n","BINARY_LABEL_CASE = 1\n","\n","class Metric(abc.ABC):\n"," \"\"\"Represents a callable wrapper class for a named metric function.\n","\n"," Attributes:\n"," name: The metric's name.\n"," \"\"\"\n","\n"," def __init__(self, name: str, fn: BootstrappableFn) -> None:\n"," \"\"\"Initializes the metric.\n","\n"," Args:\n"," name: The metric's name.\n"," fn: A function that computes an outcome from label and prediction arrays.\n"," The function's signature should accept a `y_true` label array and a\n"," `y_pred` model prediction array. This function is invoked when the\n"," `Metric` instance is called.\n"," \"\"\"\n"," self._name: str = name\n"," self._fn: BootstrappableFn = fn\n","\n"," @property\n"," def name(self) -> str:\n"," \"\"\"The `Metric`'s name.\"\"\"\n"," return self._name\n","\n"," @abc.abstractmethod\n"," def _validate(self, y_true: np.ndarray, y_pred: np.ndarray) -> None:\n"," \"\"\"Validates the `y_true` labels and `y_pred` predictions.\n","\n"," Note: Each prediction subarray `y_pred[i, ...]` at index `i` should\n"," correspond to the `y_true[i]` label.\n","\n"," Args:\n"," y_true: The ground truth label targets.\n"," y_pred: The target predictions.\n","\n"," Raises:\n"," ValueError: If the first dimension of `y_true` and `y_pred` do not match.\n"," \"\"\"\n"," if y_true.shape[0] != y_pred.shape[0]:\n"," raise ValueError('`y_true` and `y_pred` first dimension mismatch: '\n"," f'{y_true.shape[0]} != {y_pred.shape[0]}')\n","\n"," def __call__(self, y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Invokes the `Metric`'s function.\n","\n"," Args:\n"," y_true: The ground truth label values.\n"," y_pred: The target predictions.\n","\n"," Returns:\n"," The result of the `Metric.fn(y_true, y_pred)`.\n"," \"\"\"\n"," self._validate(y_true, y_pred)\n"," return self._fn(y_true, y_pred)\n","\n"," def __str__(self) -> str:\n"," return self.name\n","\n","\n","class ContinuousMetric(Metric):\n"," \"\"\"Represents a callable wrapper class for a named continuous label function.\n","\n"," Attributes:\n"," name: The metric's name.\n"," \"\"\"\n","\n"," # Note: This is a useful delegation since _validate is an @abc.abstractmethod.\n"," def _validate( # pylint: disable=useless-super-delegation\n"," self,\n"," y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," ) -> None:\n"," \"\"\"Validates the `y_true` labels and `y_pred` predictions.\n","\n"," Args:\n"," y_true: The ground truth label values.\n"," y_pred: The target predictions.\n","\n"," Raises:\n"," ValueError: If the first dimension of `y_true` and `y_pred` do not match.\n"," \"\"\"\n"," super()._validate(y_true, y_pred)\n","\n","\n","class BinaryMetric(Metric):\n"," \"\"\"Represents a callable wrapper class for a named binary label function.\n","\n"," This class asserts that the provided `y_true` labels are binary targets in\n"," `{0, 1}` and that `y_true` contains at least one element in each class, i.e.,\n"," not all samples are from the same class.\n","\n"," Attributes:\n"," name: The metric's name.\n"," \"\"\"\n","\n"," def _validate(self, y_true: np.ndarray, y_pred: np.ndarray) -> None:\n"," \"\"\"Validates the `y_true` labels and `y_pred` predictions.\n","\n"," Args:\n"," y_true: The ground truth label values.\n"," y_pred: The target predictions.\n","\n"," Raises:\n"," ValueError: If the first dimension of `y_true` and `y_pred` do not match.\n"," ValueError: If `y_true` labels are nonbinary, i.e., not all values are in\n"," `{BINARY_LABEL_CONTROL, BINARY_LABEL_CASE}` or if `y_true` does not\n"," contain at least one element from each class.\n"," \"\"\"\n"," super()._validate(y_true, y_pred)\n"," if not is_valid_binary_label(y_true):\n"," raise ValueError('`y_true` labels must be in `{BINARY_LABEL_CONTROL, '\n"," 'BINARY_LABEL_CASE}` and have at least one element from '\n"," f'each class; found: {y_true}')\n","\n","\n","def is_binary(metric: Metric) -> bool:\n"," \"\"\"Whether `metric` is a metric computed with binary `y_true` labels.\"\"\"\n"," return isinstance(metric, BinaryMetric)\n","\n","\n","def is_valid_binary_label(array: np.ndarray) -> bool:\n"," \"\"\"Whether `array` is a \"valid\" binary label array for bootstrapping.\n","\n"," We define a valid binary label array as an array that contains only binary\n"," values, i.e., `{BINARY_LABEL_CONTROL, BINARY_LABEL_CASE}`, and contains at\n"," least one value from each class.\n","\n"," Args:\n"," array: A numpy array.\n","\n"," Returns:\n"," Whether `array` is a \"valid\" binary label array.\n"," \"\"\"\n"," is_case_mask = array == BINARY_LABEL_CASE\n"," is_control_mask = array == BINARY_LABEL_CONTROL\n"," return (np.any(is_case_mask) and np.any(is_control_mask) and\n"," np.all(np.logical_or(is_case_mask, is_control_mask)))\n","\n","\n","def pearsonr(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the Pearson R correlation coefficient.\"\"\"\n"," # Note: We ignore the returned p value.\n"," r, _ = scipy.stats.pearsonr(y_true, y_pred)\n"," return r\n","\n","\n","def pearsonr_squared(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the square of the Pearson correlation coefficient.\"\"\"\n"," return pearsonr(y_true, y_pred)**2\n","\n","\n","def spearmanr(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the Spearman R correlation coefficient.\"\"\"\n"," # Note: We ignore the returned p value.\n"," r, _ = scipy.stats.spearmanr(y_true, y_pred)\n"," return r\n","\n","\n","def count(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the number of samples in `y_true`.\"\"\"\n"," if y_true.shape[0] != y_pred.shape[0]:\n"," raise ValueError('`y_true` and `y_pred` first dimension mismatch: '\n"," f'{y_true.shape[0]} != {y_pred.shape[0]}')\n"," return len(y_true)\n","\n","\n","def frequency_between(y_true: np.ndarray, y_pred: np.ndarray,\n"," percentile_lower: int, percentile_upper: int) -> float:\n"," \"\"\"Computes the positive class frequency within a percentile interval.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," percentile_lower: The lower bound (inclusive) of percentile. 0 to include\n"," all samples.\n"," percentile_upper: The upper bound (inclusive for 100, exclusive for all\n"," other values) of percentile. 100 to include all samples.\n","\n"," Returns:\n"," A [0.0, 1.0] float corresponding to the positive class frequency within\n"," the percentile interval.\n","\n"," Raises:\n"," ValueError: Invalid percentile range.\n"," \"\"\"\n"," if not 0 <= percentile_lower < 100:\n"," raise ValueError('`percentile_lower` must be in range `[0, 100)`: '\n"," f'{percentile_lower}')\n"," if not 0 < percentile_upper <= 100:\n"," raise ValueError('`percentile_upper` must be in range `(0, 100]`: '\n"," f'{percentile_upper}')\n","\n"," pred_lower_percentile, pred_upper_percentile = np.percentile(\n"," a=y_pred, q=[percentile_lower, percentile_upper])\n"," lower_mask = (y_pred >= pred_lower_percentile)\n"," if percentile_upper == 100:\n"," mask = lower_mask\n"," else:\n"," upper_mask = (y_pred < pred_upper_percentile)\n"," mask = lower_mask & upper_mask\n"," assert len(mask) == len(y_true)\n"," return np.mean(y_true[mask])\n","\n","\n","def frequency(y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," top_percentile: int = 100) -> float:\n"," \"\"\"Computes the positive class frequency within the top prediction percentile.\n","\n"," We select the subset of `y_true` labels corresponding to `y_pred`'s\n"," `top_percentile`-th prediction percetile and return the positive class\n"," frequency within this subset. `top_percentile=100` indicates the frequency for\n"," all samples.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," top_percentile: Determines the set of examples considered in the frequency\n"," calculation. The top percentile represents the top percentile by\n"," prediction risk. 100 indicates using all samples.\n","\n"," Returns:\n"," A [0.0, 1.0] float corresponding to the positive class frequency in the top\n"," percentile.\n","\n"," Raises:\n"," ValueError: `top_percentile` is not in range `(0, 100]`.\n"," \"\"\"\n"," if not 0 < top_percentile <= 100:\n"," raise ValueError('`top_percentile` must be in range `(0, 100]`: '\n"," f'{top_percentile}')\n","\n"," return frequency_between(\n"," y_true,\n"," y_pred,\n"," percentile_lower=100 - top_percentile,\n"," percentile_upper=100)\n","\n","\n","def frequency_fn(top_percentile: int) -> BootstrappableFn:\n"," \"\"\"Returns a function that computes `frequency` at `top_percentile`.\"\"\"\n","\n"," def _frequency(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," return frequency(y_true, y_pred, top_percentile)\n","\n"," return _frequency\n","\n","\n","def frequency_between_fn(percentile_lower: int,\n"," percentile_upper: int) -> BootstrappableFn:\n"," \"\"\"Returns a function that computes `frequency` in a percentile interval.\"\"\"\n","\n"," def _freq_between(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," return frequency_between(\n"," y_true,\n"," y_pred,\n"," percentile_lower=percentile_lower,\n"," percentile_upper=percentile_upper)\n","\n"," return _freq_between"]},{"cell_type":"code","execution_count":3,"metadata":{"id":"M33VPEMF0sGd","executionInfo":{"status":"ok","timestamp":1717783770322,"user_tz":240,"elapsed":4,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["# Represents a numpy array of indices for a single bootstrap sample.\n","IndexSample = np.ndarray\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class NamedArray:\n"," \"\"\"Represents a named numpy array.\n","\n"," Attributes:\n"," name: The array name.\n"," values: A numpy array.\n"," \"\"\"\n","\n"," name: str\n"," values: np.ndarray\n","\n"," def __post_init__(self):\n"," if not self.name:\n"," raise ValueError('`name` must be specified.')\n","\n"," def __len__(self) -> int:\n"," return len(self.values)\n","\n"," def __str__(self) -> str:\n"," return f'{self.__class__.__name__}({self.name})'\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class Label(NamedArray):\n"," \"\"\"Represents a named numpy array of ground truth label targets.\n","\n"," Attributes:\n"," name: The label name.\n"," values: A numpy array containing ground truth label targets.\n"," \"\"\"\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class Prediction(NamedArray):\n"," \"\"\"Represents a named numpy array of target predictions.\n","\n"," Attributes:\n"," model_name: The name of the model that generated the predictions.\n"," name: The name of the predictions (e.g., the prediction column).\n"," values: A numpy array containing model predictions.\n"," \"\"\"\n","\n"," model_name: str\n","\n"," def __post_init__(self):\n"," super().__post_init__()\n"," if not self.model_name:\n"," raise ValueError('`model_name` must be specified.')\n","\n"," def __str__(self) -> str:\n"," return f'{self.__class__.__name__}({self.model_name}.{self.name})'\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class SampleMean:\n"," \"\"\"Represents an estimate of the population mean for a given sample.\n","\n"," Attributes:\n"," mean: The mean of a given sample.\n"," stddev: The standard deviation of the sample mean.\n"," num_samples: The number of samples used to calculate `mean` and `stddev`.\n","\n"," Raises:\n"," ValueError: If `num_samples` is not >= `1`.\n"," ValueError: If `stddev` is not `0` when `num_samples` is `1`.\n"," \"\"\"\n","\n"," mean: float\n"," stddev: float\n"," num_samples: int\n","\n"," def __post_init__(self):\n"," # Ensure we have a valid number of samples.\n"," if self.num_samples < 1:\n"," raise ValueError(f'`num_samples` must be >= `1`: {self.num_samples}')\n","\n"," # Ensure the standard deviation is 0 given a single sample.\n"," if self.num_samples == 1 and self.stddev != 0.0:\n"," raise ValueError(\n"," f'`stddev` must be `0` if `num_samples` is `1`: {self.stddev:0.4f}'\n"," )\n","\n"," def __str__(self) -> str:\n"," return f'{self.mean:0.4f} (SD={self.stddev:0.4f}, n={self.num_samples})'\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class ConfidenceInterval(SampleMean):\n"," \"\"\"Represents a confidence interval (CI) for a sample mean.\n","\n"," Attributes:\n"," mean: The mean of a given sample.\n"," stddev: The standard deviation of the sample mean.\n"," num_samples: The number of samples used to calculate `mean` and `stddev`.\n"," level: The confidence level at which the CI is calculated (e.g., 95).\n"," ci_lower: The lower limit of the `level` confidence interval.\n"," ci_upper: The upper limit of the `level` confidence interval.\n","\n"," Raises:\n"," ValueError: If `num_samples` is not >= `1`.\n"," ValueError: If `stddev` is not `0` when `num_samples` is `1`.\n"," ValueError: If `level` is not in range (0, 100].\n"," ValueError: If `ci_lower` or `ci_upper` does not match not `mean` when\n"," `num_samples` is `1`.\n"," \"\"\"\n","\n"," level: float\n"," ci_lower: float\n"," ci_upper: float\n","\n"," def __post_init__(self):\n"," super().__post_init__()\n"," # Ensure we have a valid confidence level.\n"," if not 0 < self.level <= 100:\n"," raise ValueError(f'`level` must be in range (0, 100]: {self.level:0.2f}')\n","\n"," # Ensure confidence intervals match the sample mean given a single sample.\n"," if self.num_samples == 1:\n"," if (self.ci_lower != self.mean) or (self.ci_upper != self.mean):\n"," raise ValueError(\n"," '`ci_lower` and `ci_upper` must match `mean` if `num_samples` is '\n"," f'1: mean={self.mean:0.4f}, ci_lower={self.ci_lower:0.4f}, '\n"," f'ci_upper={self.ci_upper:0.4f}'\n"," )\n","\n"," def __str__(self) -> str:\n"," return (\n"," f'{self.mean:0.4f} (SD={self.stddev:0.4f}, n={self.num_samples}, '\n"," f'{self.level:0>6.2f}% CI=[{self.ci_lower:0.4f}, '\n"," f'{self.ci_upper:0.4f}])'\n"," )\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class Result:\n"," \"\"\"Represents a bootstrapped metric result for an individual model.\n","\n"," Attributes:\n"," model_name: The model's name.\n"," prediction_name: The model's prediction name (e.g., the model head's name or\n"," the label name used in training).\n"," metric_name: The metric's name.\n"," ci: A confidence interval describing the distribution of metric samples.\n"," \"\"\"\n","\n"," model_name: str\n"," prediction_name: str\n"," metric_name: str\n"," ci: ConfidenceInterval\n","\n"," def __post_init__(self):\n"," # Ensure model, prediction, and metric names are specified.\n"," if not self.model_name:\n"," raise ValueError('`model_name` must be specified.')\n"," if not self.prediction_name:\n"," raise ValueError('`prediction_name` must be specified.')\n"," if not self.metric_name:\n"," raise ValueError('`metric_name` must be specified.')\n","\n"," def __str__(self) -> str:\n"," return (\n"," f'{self.model_name}.{self.prediction_name}: '\n"," f'{self.metric_name}: {self.ci}'\n"," )\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class PairedResult:\n"," \"\"\"Represents a paired bootstrapped metric result for two models.\n","\n"," Attributes:\n"," model_name_a: The first model's name.\n"," prediction_name_a: The first model's prediction name (e.g., the model head's\n"," name or the label name used in training).\n"," model_name_b: The second model's name.\n"," prediction_name_b: The second model's prediction name (e.g., the model\n"," head's name or the label name used in training).\n"," metric_name: The metric's name.\n"," ci: A confidence interval describing the distribution of differences between\n"," the first and second models' metric samples.\n"," \"\"\"\n","\n"," model_name_a: str\n"," prediction_name_a: str\n"," model_name_b: str\n"," prediction_name_b: str\n"," metric_name: str\n"," ci: ConfidenceInterval\n","\n"," def __post_init__(self):\n"," # Ensure model, prediction, and metric names are specified.\n"," if not self.model_name_a:\n"," raise ValueError('`model_name_a` must be specified.')\n"," if not self.prediction_name_a:\n"," raise ValueError('`prediction_name_a` must be specified.')\n"," if not self.model_name_b:\n"," raise ValueError('`model_name_b` must be specified.')\n"," if not self.prediction_name_b:\n"," raise ValueError('`prediction_name_b` must be specified.')\n"," if not self.metric_name:\n"," raise ValueError('`metric_name` must be specified.')\n","\n"," def __str__(self) -> str:\n"," return (\n"," f'({self.model_name_a}.{self.prediction_name_a} - '\n"," f'{self.model_name_b}.{self.prediction_name_b}): '\n"," f'{self.metric_name}: {self.ci}'\n"," )\n","\n","\n","def _reverse_paired_result(paired_result: PairedResult) -> PairedResult:\n"," \"\"\"Returns the \"(b - a)\" inverse of an \"(a - b)\" `PairedResult`.\"\"\"\n"," reversed_ci = ConfidenceInterval(\n"," mean=(paired_result.ci.mean * -1),\n"," stddev=paired_result.ci.stddev,\n"," num_samples=paired_result.ci.num_samples,\n"," level=paired_result.ci.level,\n"," ci_upper=(paired_result.ci.ci_lower * -1),\n"," ci_lower=(paired_result.ci.ci_upper * -1),\n"," )\n"," reversed_paired_result = PairedResult(\n"," model_name_a=paired_result.model_name_b,\n"," prediction_name_a=paired_result.prediction_name_b,\n"," model_name_b=paired_result.model_name_a,\n"," prediction_name_b=paired_result.prediction_name_a,\n"," metric_name=paired_result.metric_name,\n"," ci=reversed_ci,\n"," )\n"," return reversed_paired_result\n","\n","\n","def _compute_confidence_interval(\n"," samples: np.ndarray,\n"," ci_level: float,\n",") -> ConfidenceInterval:\n"," \"\"\"Computes the mean, standard deviation, and confidence interval for samples.\n","\n"," Args:\n"," samples: A boostrapped array of observed sample values.\n"," ci_level: The confidence level/width of the desired confidence interval.\n","\n"," Returns:\n"," A `Result` containing the mean, standard deviation, and the `ci_level`%\n"," confidence interval for the observed sample values.\n"," \"\"\"\n"," sample_mean = np.mean(samples, axis=0)\n"," sample_std = np.std(samples, axis=0)\n","\n"," lower_percentile = (100 - ci_level) / 2\n"," upper_percentile = 100 - lower_percentile\n"," percentiles = [lower_percentile, upper_percentile]\n"," ci_lower, ci_upper = np.percentile(a=samples, q=percentiles, axis=0)\n","\n"," ci = ConfidenceInterval(\n"," mean=sample_mean,\n"," stddev=sample_std,\n"," num_samples=len(samples),\n"," level=ci_level,\n"," ci_lower=ci_lower,\n"," ci_upper=ci_upper,\n"," )\n","\n"," return ci\n","\n","\n","def _generate_sample_indices(\n"," label: Label,\n"," is_binary: bool,\n"," num_bootstrap: int,\n"," seed: int,\n",") -> List[IndexSample]:\n"," \"\"\"Returns a list of `num_bootstrap` randomly sampled bootstrap indices.\n","\n"," Args:\n"," label: The ground truth label targets.\n"," is_binary: Whether to generate valid binary samples; i.e., each index sample\n"," contains at least one index corresponding to a label from each class.\n"," num_bootstrap: The number of bootstrap indices to generate.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A list of `num_bootstrap` bootstrap sample indices.\n"," \"\"\"\n"," rng = np.random.default_rng(seed)\n"," num_observations = len(label)\n"," sample_indices = []\n"," while len(sample_indices) < num_bootstrap:\n"," index = rng.integers(0, high=num_observations, size=num_observations)\n"," sample_true = label.values[index]\n"," # If computing a binary metric, skip indices that result in invalid labels.\n"," if is_binary and not is_valid_binary_label(sample_true):\n"," continue\n"," sample_indices.append(index)\n"," return sample_indices\n","\n","\n","def _compute_metric_samples(\n"," metric: Metric,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," sample_indices: Sequence[np.ndarray],\n",") -> Dict[str, np.ndarray]:\n"," \"\"\"Generates `num_bootstrap` metric samples for each `Prediction`.\n","\n"," Note: This method assumes that label and prediction values are orded so that\n"," the value at index `i` in a given `Prediction` corresponds to the label value\n"," at index `i` in `label`. Both the `Label` and `Prediction` arrays are indexed\n"," using the given `sample_indices`.\n","\n"," Args:\n"," metric: An instance of a bootstrappable `Metric`; used to compute samples.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," sample_indices: An array of bootstrap sample indices. If empty, returns the\n"," single value computed on the entire dataset for each prediction.\n","\n"," Returns:\n"," A mapping of model names to the corresponding metric samples array.\n"," \"\"\"\n"," if not sample_indices:\n"," metric_samples = {}\n"," for prediction in predictions:\n"," value = metric(label.values, prediction.values)\n"," metric_samples[prediction.model_name] = np.asarray([value])\n"," return metric_samples\n","\n"," metric_samples = {prediction.model_name: [] for prediction in predictions}\n"," for index in sample_indices:\n"," sample_true = label.values[index]\n"," for prediction in predictions:\n"," sample_value = metric(sample_true, prediction.values[index])\n"," metric_samples[prediction.model_name].append(sample_value)\n","\n"," metric_samples = {\n"," name: np.asarray(samples) for name, samples in metric_samples.items()\n"," }\n","\n"," return metric_samples\n","\n","\n","def _compute_all_metric_samples(\n"," metrics: Sequence[Metric],\n"," contains_binary_metric: bool,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," num_bootstrap: int,\n"," seed: int,\n",") -> Dict[str, Dict[str, np.ndarray]]:\n"," \"\"\"Generates `num_bootstrap` samples for each `Prediction` and `Metric`.\n","\n"," Args:\n"," metrics: A sequence of a bootstrappable `Metric` instances.\n"," contains_binary_metric: Whether the set of metrics contains a binary metric.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," num_bootstrap: The number of bootstrap iterations.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A mapping of metric names to model-sample dictionaries.\n"," \"\"\"\n"," sample_indices = _generate_sample_indices(\n"," label,\n"," contains_binary_metric,\n"," num_bootstrap,\n"," seed,\n"," )\n"," metric_samples = []\n"," for metric in metrics:\n"," metric_samples.append(\n"," _compute_metric_samples(\n"," metric=metric,\n"," label=label,\n"," predictions=predictions,\n"," sample_indices=sample_indices,\n"," )\n"," )\n","\n"," return {\n"," metric.name: metric_sample\n"," for metric, metric_sample in zip(metrics, metric_samples)\n"," }\n","\n","\n","def _process_metric_samples(\n"," metric: Metric,\n"," predictions: Sequence[Prediction],\n"," model_names_to_metric_samples: Dict[str, np.ndarray],\n"," ci_level: float,\n",") -> List[Result]:\n"," \"\"\"Compute `ConfidenceInterval`s for metric samples across predictions.\"\"\"\n"," results = []\n"," for prediction in predictions:\n"," metric_samples = model_names_to_metric_samples[prediction.model_name]\n"," ci = _compute_confidence_interval(metric_samples, ci_level)\n"," result = Result(prediction.model_name, prediction.name, metric.name, ci)\n"," results.append(result)\n"," return results\n","\n","\n","def _process_metric_samples_paired(\n"," metric: Metric,\n"," predictions: Sequence[Prediction],\n"," model_names_to_metric_samples: Dict[str, np.ndarray],\n"," ci_level: float,\n",") -> List[PairedResult]:\n"," \"\"\"Compute `ConfidenceInterval`s for paired samples across predictions.\"\"\"\n"," results = []\n"," for i, prediction_a in enumerate(predictions[:-1]):\n"," for prediction_b in predictions[i + 1 :]:\n"," # Compute the result of `prediction_a - prediction_b`.\n"," metric_samples_a = model_names_to_metric_samples[prediction_a.model_name]\n"," metric_samples_b = model_names_to_metric_samples[prediction_b.model_name]\n"," metric_samples_diff = metric_samples_a - metric_samples_b\n"," ci = _compute_confidence_interval(metric_samples_diff, ci_level)\n"," result = PairedResult(\n"," prediction_a.model_name,\n"," prediction_a.name,\n"," prediction_b.model_name,\n"," prediction_b.name,\n"," metric.name,\n"," ci,\n"," )\n"," results.append(result)\n"," # Derive and include the result of `prediction_b - prediction_a`.\n"," results.append(_reverse_paired_result(result))\n"," return results\n","\n","\n","def _bootstrap(\n"," metrics: Sequence[Metric],\n"," contains_binary_metric: bool,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," num_bootstrap: int,\n"," ci_level: float,\n"," seed: int,\n",") -> Dict[str, List[Result]]:\n"," \"\"\"Performs bootstrapping for all models using the given metrics.\n","\n"," Args:\n"," metrics: A sequence of a bootstrappable `Metric` instances.\n"," contains_binary_metric: Whether the set of metrics contains a binary metric.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," num_bootstrap: The number of bootstrap iterations.\n"," ci_level: The confidence level/width of the desired confidence interval.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A dictionary mapping metric names to a list of `Result`s containing the mean\n"," metric values of each model over `num_bootstrap` bootstrapping iterations.\n"," \"\"\"\n"," metric_to_model_to_samples = _compute_all_metric_samples(\n"," metrics,\n"," contains_binary_metric,\n"," label,\n"," predictions,\n"," num_bootstrap,\n"," seed,\n"," )\n"," metric_samples = []\n"," for metric in metrics:\n"," metric_samples.append(\n"," _process_metric_samples(\n"," metric=metric,\n"," predictions=predictions,\n"," model_names_to_metric_samples=metric_to_model_to_samples[\n"," metric.name\n"," ],\n"," ci_level=ci_level,\n"," )\n"," )\n","\n"," return {\n"," metric.name: metric_sample\n"," for metric, metric_sample in zip(metrics, metric_samples)\n"," }\n","\n","\n","def _paired_bootstrap(\n"," metrics: Sequence[Metric],\n"," contains_binary_metric: bool,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," num_bootstrap: int,\n"," ci_level: float,\n"," seed: int,\n",") -> Dict[str, List[PairedResult]]:\n"," \"\"\"Performs paired bootstrapping for all models using the given metrics.\n","\n"," Args:\n"," metrics: A sequence of a bootstrappable `Metric` instances.\n"," contains_binary_metric: Whether the set of metrics contains a binary metric.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," num_bootstrap: The number of bootstrap iterations.\n"," ci_level: The confidence level/width of the desired confidence interval.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A dictionary mapping metric names to `PairedResult`s containing the mean\n"," metric difference between models over `num_bootstrap` bootstrapping\n"," iterations.\n"," \"\"\"\n"," metric_to_model_to_samples = _compute_all_metric_samples(\n"," metrics,\n"," contains_binary_metric,\n"," label,\n"," predictions,\n"," num_bootstrap,\n"," seed,\n"," )\n"," metric_samples = []\n"," for metric in metrics:\n"," metric_samples.append(\n"," _process_metric_samples_paired(\n"," metric=metric,\n"," predictions=predictions,\n"," model_names_to_metric_samples=metric_to_model_to_samples[\n"," metric.name\n"," ],\n"," ci_level=ci_level,\n"," )\n"," )\n","\n"," return {\n"," metric.name: metric_sample\n"," for metric, metric_sample in zip(metrics, metric_samples)\n"," }\n","\n","\n","def _default_binary_metrics() -> List[BinaryMetric]:\n"," \"\"\"Returns `PerformanceMetrics`'s default metrics for binary target.\"\"\"\n"," metrics = [\n"," BinaryMetric('num', count),\n"," BinaryMetric('auc', sklearn.metrics.roc_auc_score),\n"," BinaryMetric('auprc', sklearn.metrics.average_precision_score),\n"," ]\n"," for percentile in [100, 10, 5, 1]:\n"," metrics.append(\n"," BinaryMetric(\n"," f'freq@{percentile:>03}%',\n"," frequency_fn(percentile),\n"," )\n"," )\n"," return metrics\n","\n","\n","def _default_continuous_metrics() -> List[ContinuousMetric]:\n"," \"\"\"Returns `PerformanceMetrics`'s default metrics for continuous target.\"\"\"\n"," metrics = [\n"," ContinuousMetric('num', count),\n"," ContinuousMetric('pearson', pearsonr),\n"," ContinuousMetric('pearsonr_squared', pearsonr_squared),\n"," ContinuousMetric('spearman', spearmanr),\n"," ContinuousMetric('mse', sklearn.metrics.mean_squared_error),\n"," ContinuousMetric('mae', sklearn.metrics.mean_absolute_error),\n"," ]\n"," return metrics\n","\n","\n","def _default_metrics(binary_targets: bool) -> List[Metric]:\n"," \"\"\"Returns `PerformanceMetrics`'s default set of metrics for the target type.\n","\n"," Args:\n"," binary_targets: Whether the target labels are binary. If false, the returned\n"," metrics assume continuous labels.\n","\n"," Returns:\n"," The default set of binary or continuous `bootstrap_metrics.Metric`s.\n"," \"\"\"\n"," if binary_targets:\n"," return _default_binary_metrics()\n"," return _default_continuous_metrics()\n","\n","\n","class PerformanceMetrics:\n"," \"\"\"A named collection of invocable, bootstrapable `Metric`s.\n","\n"," Initializes a class that applies the given `Metric` functions to new ground\n"," truth labels and predictions. `Metric`s can be evaluated with and without\n"," bootstrapping.\n","\n"," The default metrics are number of samples, auc, auprc, and frequency\n"," calculations for the top 100/10/5/1 top percentiles, if `default_metrics` is\n"," 'binary'. If `default_metrics` is 'continuous', the default metrics are\n"," Pearson and Spearman correlations, the square of the Pearson correlation, mean\n"," squared error (MSE) and mean absolute error (MAE).\n","\n"," TODO(b/199452239): Refactor `PerformanceMetrics` so that the default metric\n"," set is not parameterized with a string.\n","\n"," Raises:\n"," ValueError: if an item in `metrics` is not of type `Metric`.\n"," \"\"\"\n","\n"," def __init__(\n"," self,\n"," name: str,\n"," default_metrics: Optional[str] = None,\n"," metrics: Optional[List[Metric]] = None,\n"," ) -> None:\n","\n"," if metrics is None:\n"," if default_metrics is None:\n"," raise ValueError('`default_metrics` is None and no metric is provided.')\n"," elif default_metrics == 'binary':\n"," metrics = _default_metrics(binary_targets=True)\n"," elif default_metrics == 'continuous':\n"," metrics = _default_metrics(binary_targets=False)\n"," else:\n"," raise ValueError(\n"," 'unknown `default_metrics`: {}'.format(default_metrics)\n"," )\n","\n"," for metric in metrics:\n"," if not isinstance(metric, Metric):\n"," raise ValueError('Invalid metric value: must be of class `Metric`.')\n","\n"," if len(metrics) != len({metric.name for metric in metrics}):\n"," raise ValueError(f'Metric names must be unique: {metrics}')\n","\n"," self.name = name\n"," self.metrics = metrics\n"," self.contains_binary = any(is_binary(m) for m in metrics)\n","\n"," def compute(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," ) -> Dict[str, Result]:\n"," \"\"\"Evaluates all metrics using the given labels and predictions.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," mask: A boolean mask; applied to `y_true` and `y_pred`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n","\n"," Returns:\n"," A dictionary of bootstrapped metrics keyed on metric name with\n"," `Result` values.\n","\n"," Raises:\n"," ValueError: If the dimensions of `y_true`, `y_pred`, or `mask` do not\n"," match, or labels are not in {0 , 1}.\n"," \"\"\"\n"," if len(y_true) != len(y_pred):\n"," raise ValueError('Label and prediction dimensions do not match.')\n","\n"," if mask is not None and len(mask) != len(y_pred):\n"," raise ValueError('Label and prediction dimensions do not match mask.')\n","\n"," if mask is not None:\n"," y_true = y_true[mask]\n"," y_pred = y_pred[mask]\n","\n"," # TODO(b/197539434): Pipe through non-empty names after public api refactor.\n"," label_name = 'label'\n"," label = Label(label_name, y_true)\n"," predictions = [Prediction(label_name, y_pred, 'model')]\n","\n"," metric_results = _bootstrap(\n"," self.metrics,\n"," contains_binary_metric=self.contains_binary,\n"," label=label,\n"," predictions=predictions,\n"," num_bootstrap=n_bootstrap,\n"," ci_level=conf_interval,\n"," seed=seed,\n"," )\n","\n"," # TODO(b/197539434): Remove temporary asserts after public api refactor.\n"," final_results = {}\n"," for metric_name, results in metric_results.items():\n"," assert len(results) == 1\n"," final_results[metric_name] = results[0]\n","\n"," return final_results\n","\n"," def compute_paired(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred_a: np.ndarray,\n"," y_pred_b: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," ) -> Dict[str, PairedResult]:\n"," \"\"\"Computes a paired bootstrap value for each metric.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred_a: Target predictions from model A; compared to `y_pred_b`.\n"," y_pred_b: Target predictions from model B; compared to `y_pred_a`.\n"," mask: A boolean mask; applied to `y_true`, `y_pred_a`, and `y_pred_b`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n","\n"," Returns:\n"," A dictionary of paired bootstrapped metrics keyed on metric name with\n"," `PairedResult` values.\n","\n"," Raises:\n"," ValueError: If the dimensions of `y_true`, `y_pred_a`, `y_pred_b` or\n"," `mask` do not match, or labels are not in {0 , 1}.\n"," \"\"\"\n"," if (len(y_true) != len(y_pred_a)) or (len(y_true) != len(y_pred_b)):\n"," raise ValueError('Label and prediction dimensions do not match.')\n","\n"," if mask is not None and len(mask) != len(y_pred_a):\n"," raise ValueError('Label and prediction dimensions do not match mask.')\n","\n"," if mask is not None:\n"," y_true = y_true[mask]\n"," y_pred_a = y_pred_a[mask]\n"," y_pred_b = y_pred_b[mask]\n","\n"," # TODO(b/197539434): Pipe through non-empty names after public api refactor.\n"," label_name = 'label'\n"," label = Label(label_name, y_true)\n"," first_model_name = 'model_a'\n"," predictions = [\n"," Prediction(label_name, y_pred_a, first_model_name),\n"," Prediction(label_name, y_pred_b, 'model_b'),\n"," ]\n","\n"," metric_results = _paired_bootstrap(\n"," self.metrics,\n"," contains_binary_metric=self.contains_binary,\n"," label=label,\n"," predictions=predictions,\n"," num_bootstrap=n_bootstrap,\n"," ci_level=conf_interval,\n"," seed=seed,\n"," )\n","\n"," # TODO(b/197539434): Remove temporary asserts after public api refactor.\n"," final_results = {}\n"," for metric_name, results in metric_results.items():\n"," assert len(results) == 2\n"," assert results[0].model_name_a == first_model_name\n"," final_results[metric_name] = results[0]\n","\n"," return final_results\n","\n"," def _print_results(\n"," self,\n"," title: str,\n"," results: Dict[str, Union[Result, PairedResult]],\n"," ) -> None:\n"," \"\"\"Prints each result object under the current name and given title.\"\"\"\n"," print(f'{self.name}: {title}')\n"," for _, result in sorted(results.items()):\n"," print(f'\\t{result}')\n","\n"," def compute_and_print(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," title: str = '',\n"," ) -> None:\n"," \"\"\"Evaluates and pretty-prints metrics using given labels and predictions.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," mask: A boolean mask; applied to `y_true` and `y_pred`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n"," title: A title appended to the printed evaluation metrics.\n","\n"," Raises:\n"," ValueError: If any of `y_true`, `y_pred`, or `mask` are not of type\n"," numpy.array of if their dimensions do not match.\n"," \"\"\"\n"," results = self.compute(\n"," y_true,\n"," y_pred,\n"," mask=mask,\n"," n_bootstrap=n_bootstrap,\n"," conf_interval=conf_interval,\n"," seed=seed,\n"," )\n"," self._print_results(title, results)\n","\n"," def compute_paired_and_print(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred_a: np.ndarray,\n"," y_pred_b: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," title: str = '',\n"," **kwargs,\n"," ) -> None:\n"," \"\"\"Evaluates and pretty-prints paired metrics.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred_a: Target predictions from model A; compared to `y_pred_b`.\n"," y_pred_b: Target predictions from model B; compared to `y_pred_a`.\n"," mask: A boolean mask; applied to `y_true`, `y_pred_a`, and `y_pred_b`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n"," title: A title appended to the printed evaluation metrics.\n"," **kwargs: Additional keyword arguments passed to each Metric's `func`.\n"," \"\"\"\n"," results = self.compute_paired(\n"," y_true,\n"," y_pred_a,\n"," y_pred_b,\n"," mask=mask,\n"," n_bootstrap=n_bootstrap,\n"," conf_interval=conf_interval,\n"," seed=seed,\n"," **kwargs,\n"," )\n"," self._print_results(title, results)"]},{"cell_type":"code","execution_count":4,"metadata":{"id":"x4222NTc0xpR","executionInfo":{"status":"ok","timestamp":1717783770586,"user_tz":240,"elapsed":18,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["N_BOOTSTRAP = 300\n","BOOTSTRAP_METRICS_LIST = [\n"," BinaryMetric('roc_auc', metrics.roc_auc_score),\n"," BinaryMetric('pr_auc', metrics.average_precision_score),\n"," ContinuousMetric('pearsonr', pearsonr),\n"," BinaryMetric('top10prev', frequency_fn(10)),\n","]\n","\n","def get_prs_eval_info(y_true, y_pred, name, as_dataframe=False):\n"," performance_metrics = PerformanceMetrics(\n"," 'Metrics', metrics=BOOTSTRAP_METRICS_LIST)\n"," performance_metrics_values = performance_metrics.compute(\n"," y_true=y_true,\n"," y_pred=y_pred,\n"," n_bootstrap=N_BOOTSTRAP,\n"," )\n"," # print(performance_metrics_values, flush=True)\n"," roc_auc_ci = performance_metrics_values['roc_auc'].ci\n"," pr_auc_ci = performance_metrics_values['pr_auc'].ci\n"," pearsonr_ci = performance_metrics_values['pearsonr'].ci\n"," top10prev_ci = performance_metrics_values['top10prev'].ci\n"," info = {\n"," 'method': name,\n"," 'pearsonr': pearsonr_ci.mean,\n"," 'pearsonr_std': pearsonr_ci.stddev,\n"," 'pearsonr_lower': pearsonr_ci.ci_lower,\n"," 'pearsonr_upper': pearsonr_ci.ci_upper,\n"," 'roc_auc': roc_auc_ci.mean,\n"," 'roc_auc_std': roc_auc_ci.stddev,\n"," 'roc_auc_lower': roc_auc_ci.ci_lower,\n"," 'roc_auc_upper': roc_auc_ci.ci_upper,\n"," 'pr_auc': pr_auc_ci.mean,\n"," 'pr_auc_std': pr_auc_ci.stddev,\n"," 'pr_auc_lower': pr_auc_ci.ci_lower,\n"," 'pr_auc_upper': pr_auc_ci.ci_upper,\n"," 'top10prev': top10prev_ci.mean,\n"," 'top10prev_std': top10prev_ci.stddev,\n"," 'top10prev_lower': top10prev_ci.ci_lower,\n"," 'top10prev_upper': top10prev_ci.ci_upper,\n"," }\n"," if as_dataframe:\n"," return pd.DataFrame(info, index=[0])\n"," else:\n"," return info\n","\n","\n","def get_prs_paired_eval_info(y_true,\n"," y_pred1,\n"," y_pred2,\n"," name1,\n"," name2,\n"," as_dataframe=False):\n"," performance_metrics = PerformanceMetrics(\n"," 'Metrics', metrics=BOOTSTRAP_METRICS_LIST)\n"," performance_metrics_values_paired = performance_metrics.compute_paired(\n"," y_true=y_true,\n"," y_pred_a=y_pred1,\n"," y_pred_b=y_pred2,\n"," n_bootstrap=N_BOOTSTRAP,\n"," )\n"," # print(performance_metrics_values_paired, flush=True)\n"," roc_auc_ci = performance_metrics_values_paired['roc_auc'].ci\n"," pr_auc_ci = performance_metrics_values_paired['pr_auc'].ci\n"," pearsonr_ci = performance_metrics_values_paired['pearsonr'].ci\n"," top10prev_ci = performance_metrics_values_paired['top10prev'].ci\n"," info = {\n"," 'method_a': name1,\n"," 'method_b': name2,\n"," 'pearsonr': pearsonr_ci.mean,\n"," 'pearsonr_std': pearsonr_ci.stddev,\n"," 'pearsonr_lower': pearsonr_ci.ci_lower,\n"," 'pearsonr_upper': pearsonr_ci.ci_upper,\n"," 'roc_auc': roc_auc_ci.mean,\n"," 'roc_auc_std': roc_auc_ci.stddev,\n"," 'roc_auc_lower': roc_auc_ci.ci_lower,\n"," 'roc_auc_upper': roc_auc_ci.ci_upper,\n"," 'pr_auc': pr_auc_ci.mean,\n"," 'pr_auc_std': pr_auc_ci.stddev,\n"," 'pr_auc_lower': pr_auc_ci.ci_lower,\n"," 'pr_auc_upper': pr_auc_ci.ci_upper,\n"," 'top10prev': top10prev_ci.mean,\n"," 'top10prev_std': top10prev_ci.stddev,\n"," 'top10prev_lower': top10prev_ci.ci_lower,\n"," 'top10prev_upper': top10prev_ci.ci_upper,\n"," }\n"," if as_dataframe:\n"," return pd.DataFrame(info, index=[0])\n"," else:\n"," return info"]},{"cell_type":"markdown","metadata":{"id":"NOaueJxRPmpG"},"source":["# Simulated data generation\n","\n","In this code example, we generate some simulated data (N=1,000) to demonstrate how to use the above code snippet to compute various metrics in the PRS evaluation part of the paper."]},{"cell_type":"code","execution_count":5,"metadata":{"id":"iXHTm8dxzY2H","executionInfo":{"status":"ok","timestamp":1717783770587,"user_tz":240,"elapsed":16,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["np.random.seed(42)\n","individual_prs1 = np.random.normal(size=(1000,))\n","individual_prs2 = 0.8 * individual_prs1 + 0.2 * np.random.normal(size=(1000,))\n","individual_phenotype = 0.3 * individual_prs1 + 0.7 * np.random.normal(\n"," size=(1000,)\n",")\n","individual_phenotype = (individual_phenotype >= 0).astype(int)\n","\n","data_df = pd.DataFrame({\n"," 'prs1': individual_prs1,\n"," 'prs2': individual_prs2,\n"," 'phenotype': individual_phenotype,\n","})"]},{"cell_type":"code","execution_count":6,"metadata":{"colab":{"height":206,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":16,"status":"ok","timestamp":1717783770588,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"bzdHe1jqULbv","outputId":"d11b16cf-a363-47ad-b819-e306df9990f3"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" prs1 prs2 phenotype\n","0 0.496714 0.677242 0\n","1 -0.138264 0.074315 0\n","2 0.647689 0.530077 0\n","3 1.523030 1.089037 1\n","4 -0.234153 -0.047678 0"],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
prs1prs2phenotype
00.4967140.6772420
1-0.1382640.0743150
20.6476890.5300770
31.5230301.0890371
4-0.234153-0.0476780
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n"," \n","\n","\n","\n"," \n","
\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","variable_name":"data_df","summary":"{\n \"name\": \"data_df\",\n \"rows\": 1000,\n \"fields\": [\n {\n \"column\": \"prs1\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.9792159381796757,\n \"min\": -3.2412673400690726,\n \"max\": 3.852731490654721,\n \"num_unique_values\": 1000,\n \"samples\": [\n 0.543360192379935,\n 0.9826909839455139,\n -1.8408742313316453\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"prs2\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.8005263506410991,\n \"min\": -2.4852626735659844,\n \"max\": 3.4321005411611654,\n \"num_unique_values\": 1000,\n \"samples\": [\n 0.5511076945976712,\n 0.5725922028405726,\n -1.4935892287728105\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"phenotype\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0,\n \"min\": 0,\n \"max\": 1,\n \"num_unique_values\": 2,\n \"samples\": [\n 1,\n 0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":6}],"source":["data_df.head()"]},{"cell_type":"markdown","metadata":{"id":"4LYsbEE3RdeF"},"source":["# PRS evaluation with bootstrapping\n","\n","The following code generates all evaluation metrics, namely Pearson R, AUC-ROC, AUC-PR, top 10% prevalence, and their 95% confidence intervals using bootstrapping. Note that, from the way we generated the simulated data, we expect the Pearson R of ~0.3 for `prs1` and we expect `prs1` to have higher correlation with the phenotype than `prs2`."]},{"cell_type":"code","execution_count":7,"metadata":{"colab":{"height":101,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":15212,"status":"ok","timestamp":1717783785790,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"WVJnK7BAPi33","outputId":"5b371f81-bc64-40ef-ed75-d9d080bd8475"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" method pearsonr pearsonr_std pearsonr_lower pearsonr_upper roc_auc \\\n","0 prs1 0.333455 0.027456 0.277529 0.387433 0.69263 \n","\n"," roc_auc_std roc_auc_lower roc_auc_upper pr_auc pr_auc_std \\\n","0 0.016445 0.65976 0.725288 0.675271 0.022152 \n","\n"," pr_auc_lower pr_auc_upper top10prev top10prev_std top10prev_lower \\\n","0 0.632141 0.715912 0.770216 0.043321 0.688044 \n","\n"," top10prev_upper \n","0 0.85078 "],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
methodpearsonrpearsonr_stdpearsonr_lowerpearsonr_upperroc_aucroc_auc_stdroc_auc_lowerroc_auc_upperpr_aucpr_auc_stdpr_auc_lowerpr_auc_uppertop10prevtop10prev_stdtop10prev_lowertop10prev_upper
0prs10.3334550.0274560.2775290.3874330.692630.0164450.659760.7252880.6752710.0221520.6321410.7159120.7702160.0433210.6880440.85078
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","summary":"{\n \"name\": \")\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"method\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs1\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.3334554859786796,\n \"max\": 0.3334554859786796,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.3334554859786796\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.027455597173908577,\n \"max\": 0.027455597173908577,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.027455597173908577\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.2775293042598108,\n \"max\": 0.2775293042598108,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.2775293042598108\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.38743254268744753,\n \"max\": 0.38743254268744753,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.38743254268744753\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6926303605619311,\n \"max\": 0.6926303605619311,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6926303605619311\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.016445301315729702,\n \"max\": 0.016445301315729702,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.016445301315729702\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.659760150142918,\n \"max\": 0.659760150142918,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.659760150142918\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7252876945992696,\n \"max\": 0.7252876945992696,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7252876945992696\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.675270596876246,\n \"max\": 0.675270596876246,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.675270596876246\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.02215152388674347,\n \"max\": 0.02215152388674347,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.02215152388674347\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6321413648383354,\n \"max\": 0.6321413648383354,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6321413648383354\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7159121917609861,\n \"max\": 0.7159121917609861,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7159121917609861\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7702162426122681,\n \"max\": 0.7702162426122681,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7702162426122681\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.04332125213088804,\n \"max\": 0.04332125213088804,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.04332125213088804\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6880441176470588,\n \"max\": 0.6880441176470588,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6880441176470588\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.8507797029702969,\n \"max\": 0.8507797029702969,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.8507797029702969\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":7}],"source":["get_prs_eval_info(\n"," y_true=data_df['phenotype'],\n"," y_pred=data_df['prs1'],\n"," name='prs1',\n"," as_dataframe=True\n",")"]},{"cell_type":"code","execution_count":8,"metadata":{"colab":{"height":101,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":9709,"status":"ok","timestamp":1717783795493,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"puOfA5wuQeiJ","outputId":"99b92e16-0eb5-497f-f473-26ad3c955948"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" method pearsonr pearsonr_std pearsonr_lower pearsonr_upper roc_auc \\\n","0 prs2 0.319189 0.027899 0.260433 0.373947 0.6837 \n","\n"," roc_auc_std roc_auc_lower roc_auc_upper pr_auc pr_auc_std \\\n","0 0.016604 0.649911 0.717019 0.664467 0.022454 \n","\n"," pr_auc_lower pr_auc_upper top10prev top10prev_std top10prev_lower \\\n","0 0.620486 0.706022 0.764624 0.042396 0.671552 \n","\n"," top10prev_upper \n","0 0.84 "],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
methodpearsonrpearsonr_stdpearsonr_lowerpearsonr_upperroc_aucroc_auc_stdroc_auc_lowerroc_auc_upperpr_aucpr_auc_stdpr_auc_lowerpr_auc_uppertop10prevtop10prev_stdtop10prev_lowertop10prev_upper
0prs20.3191890.0278990.2604330.3739470.68370.0166040.6499110.7170190.6644670.0224540.6204860.7060220.7646240.0423960.6715520.84
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","summary":"{\n \"name\": \")\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"method\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs2\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.3191890184766251,\n \"max\": 0.3191890184766251,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.3191890184766251\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.027898865889530153,\n \"max\": 0.027898865889530153,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.027898865889530153\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.2604328480042442,\n \"max\": 0.2604328480042442,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.2604328480042442\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.3739469506434232,\n \"max\": 0.3739469506434232,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.3739469506434232\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6836996447028457,\n \"max\": 0.6836996447028457,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6836996447028457\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.01660378118234475,\n \"max\": 0.01660378118234475,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.01660378118234475\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6499110741641438,\n \"max\": 0.6499110741641438,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6499110741641438\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7170185826451294,\n \"max\": 0.7170185826451294,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7170185826451294\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6644674946186202,\n \"max\": 0.6644674946186202,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6644674946186202\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.0224540065869167,\n \"max\": 0.0224540065869167,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.0224540065869167\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6204864568922334,\n \"max\": 0.6204864568922334,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6204864568922334\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7060224657169427,\n \"max\": 0.7060224657169427,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7060224657169427\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.764623511500396,\n \"max\": 0.764623511500396,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.764623511500396\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.042396301865302535,\n \"max\": 0.042396301865302535,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.042396301865302535\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6715519801980199,\n \"max\": 0.6715519801980199,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6715519801980199\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.84,\n \"max\": 0.84,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.84\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":8}],"source":["get_prs_eval_info(\n"," y_true=data_df['phenotype'],\n"," y_pred=data_df['prs2'],\n"," name='prs2',\n"," as_dataframe=True\n",")"]},{"cell_type":"markdown","metadata":{"id":"OiLCjqcrSjPg"},"source":["# PRS comparison with paired bootstrapping\n","\n","The following code snippet compares the performance of `prs1` and `prs2` using paired bootstrapping. Note that the difference is statistically significant with 95% paired bootstrapping confidence interval, if the lower and upper end of the confidence interval are both positive (implying `prs1` is significantly better than `prs2`) or both negative (implying `prs2` is significantly better than `prs1`)."]},{"cell_type":"code","execution_count":9,"metadata":{"colab":{"height":101,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":7610,"status":"ok","timestamp":1717783803097,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"oRKgjH_uR2wr","outputId":"8df67f16-31e6-4ae4-c904-b01a91390170"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" method_a method_b pearsonr pearsonr_std pearsonr_lower pearsonr_upper \\\n","0 prs1 prs2 0.014266 0.007112 0.000436 0.027211 \n","\n"," roc_auc roc_auc_std roc_auc_lower roc_auc_upper pr_auc pr_auc_std \\\n","0 0.008931 0.004466 0.000157 0.017171 0.010803 0.005761 \n","\n"," pr_auc_lower pr_auc_upper top10prev top10prev_std top10prev_lower \\\n","0 -0.00061 0.02107 0.005593 0.026971 -0.042589 \n","\n"," top10prev_upper \n","0 0.062382 "],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
method_amethod_bpearsonrpearsonr_stdpearsonr_lowerpearsonr_upperroc_aucroc_auc_stdroc_auc_lowerroc_auc_upperpr_aucpr_auc_stdpr_auc_lowerpr_auc_uppertop10prevtop10prev_stdtop10prev_lowertop10prev_upper
0prs1prs20.0142660.0071120.0004360.0272110.0089310.0044660.0001570.0171710.0108030.005761-0.000610.021070.0055930.026971-0.0425890.062382
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","summary":"{\n \"name\": \" as_dataframe=True)\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"method_a\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs1\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"method_b\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs2\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.014266467502054426,\n \"max\": 0.014266467502054426,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.014266467502054426\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.007111892690604321,\n \"max\": 0.007111892690604321,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.007111892690604321\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.00043626824886599245,\n \"max\": 0.00043626824886599245,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.00043626824886599245\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.027211089302840434,\n \"max\": 0.027211089302840434,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.027211089302840434\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.008930715859085309,\n \"max\": 0.008930715859085309,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.008930715859085309\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.004466363148919537,\n \"max\": 0.004466363148919537,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.004466363148919537\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.00015733124729375172,\n \"max\": 0.00015733124729375172,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.00015733124729375172\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.017170818130808965,\n \"max\": 0.017170818130808965,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.017170818130808965\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.010803102257625864,\n \"max\": 0.010803102257625864,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.010803102257625864\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.005760958016623593,\n \"max\": 0.005760958016623593,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.005760958016623593\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": -0.0006104367572841078,\n \"max\": -0.0006104367572841078,\n \"num_unique_values\": 1,\n \"samples\": [\n -0.0006104367572841078\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.02106968216083579,\n \"max\": 0.02106968216083579,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.02106968216083579\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.005592731111872085,\n \"max\": 0.005592731111872085,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.005592731111872085\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.026971273443313012,\n \"max\": 0.026971273443313012,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.026971273443313012\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": -0.04258910891089107,\n \"max\": -0.04258910891089107,\n \"num_unique_values\": 1,\n \"samples\": [\n -0.04258910891089107\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.062381770529994184,\n \"max\": 0.062381770529994184,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.062381770529994184\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":9}],"source":["get_prs_paired_eval_info(\n"," y_true=data_df['phenotype'],\n"," y_pred1=data_df['prs1'],\n"," y_pred2=data_df['prs2'],\n"," name1='prs1',\n"," name2='prs2',\n"," as_dataframe=True)"]}]} \ No newline at end of file +{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"provenance":[],"authorship_tag":"ABX9TyMn77gwTOffLNK/j6j2quKt"},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"code","source":["#@title Licensed under the BSD-3 License (the \"License\"); { display-mode: \"form\" }\n","# Copyright 2021 Google LLC.\n","#\n","# Redistribution and use in source and binary forms, with or without modification,\n","# are permitted provided that the following conditions are met:\n","#\n","# 1. Redistributions of source code must retain the above copyright notice, this\n","# list of conditions and the following disclaimer.\n","#\n","# 2. Redistributions in binary form must reproduce the above copyright notice,\n","# this list of conditions and the following disclaimer in the documentation\n","# and/or other materials provided with the distribution.\n","#\n","# 3. Neither the name of the copyright holder nor the names of its contributors\n","# may be used to endorse or promote products derived from this software without\n","# specific prior written permission.\n","#\n","# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n","# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n","# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n","# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\n","# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n","# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n","# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\n","# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n","# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n","# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."],"metadata":{"id":"vdFOGdpqPesl","executionInfo":{"status":"ok","timestamp":1717789979565,"user_tz":240,"elapsed":13,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"execution_count":1,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"VbyGa_IhXRgk"},"source":["# Preparation\n","\n","This section includes imports and functions."]},{"cell_type":"code","execution_count":2,"metadata":{"id":"otMyZHIW0Fqs","executionInfo":{"status":"ok","timestamp":1717789981803,"user_tz":240,"elapsed":2247,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["import dataclasses\n","from typing import Dict, List, Optional, Sequence, Union\n","\n","import abc\n","from typing import Callable\n","\n","import numpy as np\n","import pandas as pd\n","import scipy.stats\n","import sklearn\n","import sklearn.metrics\n","from sklearn import metrics"]},{"cell_type":"code","execution_count":3,"metadata":{"id":"J8pr2zMLzmDH","executionInfo":{"status":"ok","timestamp":1717789981804,"user_tz":240,"elapsed":6,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["# A function that computes a numeric outcome from label and prediction arrays.\n","BootstrappableFn = Callable[[np.ndarray, np.ndarray], float]\n","\n","# Constants denoting the expected case and control values for binary encodings.\n","BINARY_LABEL_CONTROL = 0\n","BINARY_LABEL_CASE = 1\n","\n","class Metric(abc.ABC):\n"," \"\"\"Represents a callable wrapper class for a named metric function.\n","\n"," Attributes:\n"," name: The metric's name.\n"," \"\"\"\n","\n"," def __init__(self, name: str, fn: BootstrappableFn) -> None:\n"," \"\"\"Initializes the metric.\n","\n"," Args:\n"," name: The metric's name.\n"," fn: A function that computes an outcome from label and prediction arrays.\n"," The function's signature should accept a `y_true` label array and a\n"," `y_pred` model prediction array. This function is invoked when the\n"," `Metric` instance is called.\n"," \"\"\"\n"," self._name: str = name\n"," self._fn: BootstrappableFn = fn\n","\n"," @property\n"," def name(self) -> str:\n"," \"\"\"The `Metric`'s name.\"\"\"\n"," return self._name\n","\n"," @abc.abstractmethod\n"," def _validate(self, y_true: np.ndarray, y_pred: np.ndarray) -> None:\n"," \"\"\"Validates the `y_true` labels and `y_pred` predictions.\n","\n"," Note: Each prediction subarray `y_pred[i, ...]` at index `i` should\n"," correspond to the `y_true[i]` label.\n","\n"," Args:\n"," y_true: The ground truth label targets.\n"," y_pred: The target predictions.\n","\n"," Raises:\n"," ValueError: If the first dimension of `y_true` and `y_pred` do not match.\n"," \"\"\"\n"," if y_true.shape[0] != y_pred.shape[0]:\n"," raise ValueError('`y_true` and `y_pred` first dimension mismatch: '\n"," f'{y_true.shape[0]} != {y_pred.shape[0]}')\n","\n"," def __call__(self, y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Invokes the `Metric`'s function.\n","\n"," Args:\n"," y_true: The ground truth label values.\n"," y_pred: The target predictions.\n","\n"," Returns:\n"," The result of the `Metric.fn(y_true, y_pred)`.\n"," \"\"\"\n"," self._validate(y_true, y_pred)\n"," return self._fn(y_true, y_pred)\n","\n"," def __str__(self) -> str:\n"," return self.name\n","\n","\n","class ContinuousMetric(Metric):\n"," \"\"\"Represents a callable wrapper class for a named continuous label function.\n","\n"," Attributes:\n"," name: The metric's name.\n"," \"\"\"\n","\n"," # Note: This is a useful delegation since _validate is an @abc.abstractmethod.\n"," def _validate( # pylint: disable=useless-super-delegation\n"," self,\n"," y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," ) -> None:\n"," \"\"\"Validates the `y_true` labels and `y_pred` predictions.\n","\n"," Args:\n"," y_true: The ground truth label values.\n"," y_pred: The target predictions.\n","\n"," Raises:\n"," ValueError: If the first dimension of `y_true` and `y_pred` do not match.\n"," \"\"\"\n"," super()._validate(y_true, y_pred)\n","\n","\n","class BinaryMetric(Metric):\n"," \"\"\"Represents a callable wrapper class for a named binary label function.\n","\n"," This class asserts that the provided `y_true` labels are binary targets in\n"," `{0, 1}` and that `y_true` contains at least one element in each class, i.e.,\n"," not all samples are from the same class.\n","\n"," Attributes:\n"," name: The metric's name.\n"," \"\"\"\n","\n"," def _validate(self, y_true: np.ndarray, y_pred: np.ndarray) -> None:\n"," \"\"\"Validates the `y_true` labels and `y_pred` predictions.\n","\n"," Args:\n"," y_true: The ground truth label values.\n"," y_pred: The target predictions.\n","\n"," Raises:\n"," ValueError: If the first dimension of `y_true` and `y_pred` do not match.\n"," ValueError: If `y_true` labels are nonbinary, i.e., not all values are in\n"," `{BINARY_LABEL_CONTROL, BINARY_LABEL_CASE}` or if `y_true` does not\n"," contain at least one element from each class.\n"," \"\"\"\n"," super()._validate(y_true, y_pred)\n"," if not is_valid_binary_label(y_true):\n"," raise ValueError('`y_true` labels must be in `{BINARY_LABEL_CONTROL, '\n"," 'BINARY_LABEL_CASE}` and have at least one element from '\n"," f'each class; found: {y_true}')\n","\n","\n","def is_binary(metric: Metric) -> bool:\n"," \"\"\"Whether `metric` is a metric computed with binary `y_true` labels.\"\"\"\n"," return isinstance(metric, BinaryMetric)\n","\n","\n","def is_valid_binary_label(array: np.ndarray) -> bool:\n"," \"\"\"Whether `array` is a \"valid\" binary label array for bootstrapping.\n","\n"," We define a valid binary label array as an array that contains only binary\n"," values, i.e., `{BINARY_LABEL_CONTROL, BINARY_LABEL_CASE}`, and contains at\n"," least one value from each class.\n","\n"," Args:\n"," array: A numpy array.\n","\n"," Returns:\n"," Whether `array` is a \"valid\" binary label array.\n"," \"\"\"\n"," is_case_mask = array == BINARY_LABEL_CASE\n"," is_control_mask = array == BINARY_LABEL_CONTROL\n"," return (np.any(is_case_mask) and np.any(is_control_mask) and\n"," np.all(np.logical_or(is_case_mask, is_control_mask)))\n","\n","\n","def pearsonr(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the Pearson R correlation coefficient.\"\"\"\n"," # Note: We ignore the returned p value.\n"," r, _ = scipy.stats.pearsonr(y_true, y_pred)\n"," return r\n","\n","\n","def pearsonr_squared(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the square of the Pearson correlation coefficient.\"\"\"\n"," return pearsonr(y_true, y_pred)**2\n","\n","\n","def spearmanr(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the Spearman R correlation coefficient.\"\"\"\n"," # Note: We ignore the returned p value.\n"," r, _ = scipy.stats.spearmanr(y_true, y_pred)\n"," return r\n","\n","\n","def count(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the number of samples in `y_true`.\"\"\"\n"," if y_true.shape[0] != y_pred.shape[0]:\n"," raise ValueError('`y_true` and `y_pred` first dimension mismatch: '\n"," f'{y_true.shape[0]} != {y_pred.shape[0]}')\n"," return len(y_true)\n","\n","\n","def frequency_between(y_true: np.ndarray, y_pred: np.ndarray,\n"," percentile_lower: int, percentile_upper: int) -> float:\n"," \"\"\"Computes the positive class frequency within a percentile interval.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," percentile_lower: The lower bound (inclusive) of percentile. 0 to include\n"," all samples.\n"," percentile_upper: The upper bound (inclusive for 100, exclusive for all\n"," other values) of percentile. 100 to include all samples.\n","\n"," Returns:\n"," A [0.0, 1.0] float corresponding to the positive class frequency within\n"," the percentile interval.\n","\n"," Raises:\n"," ValueError: Invalid percentile range.\n"," \"\"\"\n"," if not 0 <= percentile_lower < 100:\n"," raise ValueError('`percentile_lower` must be in range `[0, 100)`: '\n"," f'{percentile_lower}')\n"," if not 0 < percentile_upper <= 100:\n"," raise ValueError('`percentile_upper` must be in range `(0, 100]`: '\n"," f'{percentile_upper}')\n","\n"," pred_lower_percentile, pred_upper_percentile = np.percentile(\n"," a=y_pred, q=[percentile_lower, percentile_upper])\n"," lower_mask = (y_pred >= pred_lower_percentile)\n"," if percentile_upper == 100:\n"," mask = lower_mask\n"," else:\n"," upper_mask = (y_pred < pred_upper_percentile)\n"," mask = lower_mask & upper_mask\n"," assert len(mask) == len(y_true)\n"," return np.mean(y_true[mask])\n","\n","\n","def frequency(y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," top_percentile: int = 100) -> float:\n"," \"\"\"Computes the positive class frequency within the top prediction percentile.\n","\n"," We select the subset of `y_true` labels corresponding to `y_pred`'s\n"," `top_percentile`-th prediction percetile and return the positive class\n"," frequency within this subset. `top_percentile=100` indicates the frequency for\n"," all samples.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," top_percentile: Determines the set of examples considered in the frequency\n"," calculation. The top percentile represents the top percentile by\n"," prediction risk. 100 indicates using all samples.\n","\n"," Returns:\n"," A [0.0, 1.0] float corresponding to the positive class frequency in the top\n"," percentile.\n","\n"," Raises:\n"," ValueError: `top_percentile` is not in range `(0, 100]`.\n"," \"\"\"\n"," if not 0 < top_percentile <= 100:\n"," raise ValueError('`top_percentile` must be in range `(0, 100]`: '\n"," f'{top_percentile}')\n","\n"," return frequency_between(\n"," y_true,\n"," y_pred,\n"," percentile_lower=100 - top_percentile,\n"," percentile_upper=100)\n","\n","\n","def frequency_fn(top_percentile: int) -> BootstrappableFn:\n"," \"\"\"Returns a function that computes `frequency` at `top_percentile`.\"\"\"\n","\n"," def _frequency(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," return frequency(y_true, y_pred, top_percentile)\n","\n"," return _frequency\n","\n","\n","def frequency_between_fn(percentile_lower: int,\n"," percentile_upper: int) -> BootstrappableFn:\n"," \"\"\"Returns a function that computes `frequency` in a percentile interval.\"\"\"\n","\n"," def _freq_between(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," return frequency_between(\n"," y_true,\n"," y_pred,\n"," percentile_lower=percentile_lower,\n"," percentile_upper=percentile_upper)\n","\n"," return _freq_between"]},{"cell_type":"code","execution_count":4,"metadata":{"id":"M33VPEMF0sGd","executionInfo":{"status":"ok","timestamp":1717789982063,"user_tz":240,"elapsed":264,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["# Represents a numpy array of indices for a single bootstrap sample.\n","IndexSample = np.ndarray\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class NamedArray:\n"," \"\"\"Represents a named numpy array.\n","\n"," Attributes:\n"," name: The array name.\n"," values: A numpy array.\n"," \"\"\"\n","\n"," name: str\n"," values: np.ndarray\n","\n"," def __post_init__(self):\n"," if not self.name:\n"," raise ValueError('`name` must be specified.')\n","\n"," def __len__(self) -> int:\n"," return len(self.values)\n","\n"," def __str__(self) -> str:\n"," return f'{self.__class__.__name__}({self.name})'\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class Label(NamedArray):\n"," \"\"\"Represents a named numpy array of ground truth label targets.\n","\n"," Attributes:\n"," name: The label name.\n"," values: A numpy array containing ground truth label targets.\n"," \"\"\"\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class Prediction(NamedArray):\n"," \"\"\"Represents a named numpy array of target predictions.\n","\n"," Attributes:\n"," model_name: The name of the model that generated the predictions.\n"," name: The name of the predictions (e.g., the prediction column).\n"," values: A numpy array containing model predictions.\n"," \"\"\"\n","\n"," model_name: str\n","\n"," def __post_init__(self):\n"," super().__post_init__()\n"," if not self.model_name:\n"," raise ValueError('`model_name` must be specified.')\n","\n"," def __str__(self) -> str:\n"," return f'{self.__class__.__name__}({self.model_name}.{self.name})'\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class SampleMean:\n"," \"\"\"Represents an estimate of the population mean for a given sample.\n","\n"," Attributes:\n"," mean: The mean of a given sample.\n"," stddev: The standard deviation of the sample mean.\n"," num_samples: The number of samples used to calculate `mean` and `stddev`.\n","\n"," Raises:\n"," ValueError: If `num_samples` is not >= `1`.\n"," ValueError: If `stddev` is not `0` when `num_samples` is `1`.\n"," \"\"\"\n","\n"," mean: float\n"," stddev: float\n"," num_samples: int\n","\n"," def __post_init__(self):\n"," # Ensure we have a valid number of samples.\n"," if self.num_samples < 1:\n"," raise ValueError(f'`num_samples` must be >= `1`: {self.num_samples}')\n","\n"," # Ensure the standard deviation is 0 given a single sample.\n"," if self.num_samples == 1 and self.stddev != 0.0:\n"," raise ValueError(\n"," f'`stddev` must be `0` if `num_samples` is `1`: {self.stddev:0.4f}'\n"," )\n","\n"," def __str__(self) -> str:\n"," return f'{self.mean:0.4f} (SD={self.stddev:0.4f}, n={self.num_samples})'\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class ConfidenceInterval(SampleMean):\n"," \"\"\"Represents a confidence interval (CI) for a sample mean.\n","\n"," Attributes:\n"," mean: The mean of a given sample.\n"," stddev: The standard deviation of the sample mean.\n"," num_samples: The number of samples used to calculate `mean` and `stddev`.\n"," level: The confidence level at which the CI is calculated (e.g., 95).\n"," ci_lower: The lower limit of the `level` confidence interval.\n"," ci_upper: The upper limit of the `level` confidence interval.\n","\n"," Raises:\n"," ValueError: If `num_samples` is not >= `1`.\n"," ValueError: If `stddev` is not `0` when `num_samples` is `1`.\n"," ValueError: If `level` is not in range (0, 100].\n"," ValueError: If `ci_lower` or `ci_upper` does not match not `mean` when\n"," `num_samples` is `1`.\n"," \"\"\"\n","\n"," level: float\n"," ci_lower: float\n"," ci_upper: float\n","\n"," def __post_init__(self):\n"," super().__post_init__()\n"," # Ensure we have a valid confidence level.\n"," if not 0 < self.level <= 100:\n"," raise ValueError(f'`level` must be in range (0, 100]: {self.level:0.2f}')\n","\n"," # Ensure confidence intervals match the sample mean given a single sample.\n"," if self.num_samples == 1:\n"," if (self.ci_lower != self.mean) or (self.ci_upper != self.mean):\n"," raise ValueError(\n"," '`ci_lower` and `ci_upper` must match `mean` if `num_samples` is '\n"," f'1: mean={self.mean:0.4f}, ci_lower={self.ci_lower:0.4f}, '\n"," f'ci_upper={self.ci_upper:0.4f}'\n"," )\n","\n"," def __str__(self) -> str:\n"," return (\n"," f'{self.mean:0.4f} (SD={self.stddev:0.4f}, n={self.num_samples}, '\n"," f'{self.level:0>6.2f}% CI=[{self.ci_lower:0.4f}, '\n"," f'{self.ci_upper:0.4f}])'\n"," )\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class Result:\n"," \"\"\"Represents a bootstrapped metric result for an individual model.\n","\n"," Attributes:\n"," model_name: The model's name.\n"," prediction_name: The model's prediction name (e.g., the model head's name or\n"," the label name used in training).\n"," metric_name: The metric's name.\n"," ci: A confidence interval describing the distribution of metric samples.\n"," \"\"\"\n","\n"," model_name: str\n"," prediction_name: str\n"," metric_name: str\n"," ci: ConfidenceInterval\n","\n"," def __post_init__(self):\n"," # Ensure model, prediction, and metric names are specified.\n"," if not self.model_name:\n"," raise ValueError('`model_name` must be specified.')\n"," if not self.prediction_name:\n"," raise ValueError('`prediction_name` must be specified.')\n"," if not self.metric_name:\n"," raise ValueError('`metric_name` must be specified.')\n","\n"," def __str__(self) -> str:\n"," return (\n"," f'{self.model_name}.{self.prediction_name}: '\n"," f'{self.metric_name}: {self.ci}'\n"," )\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class PairedResult:\n"," \"\"\"Represents a paired bootstrapped metric result for two models.\n","\n"," Attributes:\n"," model_name_a: The first model's name.\n"," prediction_name_a: The first model's prediction name (e.g., the model head's\n"," name or the label name used in training).\n"," model_name_b: The second model's name.\n"," prediction_name_b: The second model's prediction name (e.g., the model\n"," head's name or the label name used in training).\n"," metric_name: The metric's name.\n"," ci: A confidence interval describing the distribution of differences between\n"," the first and second models' metric samples.\n"," \"\"\"\n","\n"," model_name_a: str\n"," prediction_name_a: str\n"," model_name_b: str\n"," prediction_name_b: str\n"," metric_name: str\n"," ci: ConfidenceInterval\n","\n"," def __post_init__(self):\n"," # Ensure model, prediction, and metric names are specified.\n"," if not self.model_name_a:\n"," raise ValueError('`model_name_a` must be specified.')\n"," if not self.prediction_name_a:\n"," raise ValueError('`prediction_name_a` must be specified.')\n"," if not self.model_name_b:\n"," raise ValueError('`model_name_b` must be specified.')\n"," if not self.prediction_name_b:\n"," raise ValueError('`prediction_name_b` must be specified.')\n"," if not self.metric_name:\n"," raise ValueError('`metric_name` must be specified.')\n","\n"," def __str__(self) -> str:\n"," return (\n"," f'({self.model_name_a}.{self.prediction_name_a} - '\n"," f'{self.model_name_b}.{self.prediction_name_b}): '\n"," f'{self.metric_name}: {self.ci}'\n"," )\n","\n","\n","def _reverse_paired_result(paired_result: PairedResult) -> PairedResult:\n"," \"\"\"Returns the \"(b - a)\" inverse of an \"(a - b)\" `PairedResult`.\"\"\"\n"," reversed_ci = ConfidenceInterval(\n"," mean=(paired_result.ci.mean * -1),\n"," stddev=paired_result.ci.stddev,\n"," num_samples=paired_result.ci.num_samples,\n"," level=paired_result.ci.level,\n"," ci_upper=(paired_result.ci.ci_lower * -1),\n"," ci_lower=(paired_result.ci.ci_upper * -1),\n"," )\n"," reversed_paired_result = PairedResult(\n"," model_name_a=paired_result.model_name_b,\n"," prediction_name_a=paired_result.prediction_name_b,\n"," model_name_b=paired_result.model_name_a,\n"," prediction_name_b=paired_result.prediction_name_a,\n"," metric_name=paired_result.metric_name,\n"," ci=reversed_ci,\n"," )\n"," return reversed_paired_result\n","\n","\n","def _compute_confidence_interval(\n"," samples: np.ndarray,\n"," ci_level: float,\n",") -> ConfidenceInterval:\n"," \"\"\"Computes the mean, standard deviation, and confidence interval for samples.\n","\n"," Args:\n"," samples: A boostrapped array of observed sample values.\n"," ci_level: The confidence level/width of the desired confidence interval.\n","\n"," Returns:\n"," A `Result` containing the mean, standard deviation, and the `ci_level`%\n"," confidence interval for the observed sample values.\n"," \"\"\"\n"," sample_mean = np.mean(samples, axis=0)\n"," sample_std = np.std(samples, axis=0)\n","\n"," lower_percentile = (100 - ci_level) / 2\n"," upper_percentile = 100 - lower_percentile\n"," percentiles = [lower_percentile, upper_percentile]\n"," ci_lower, ci_upper = np.percentile(a=samples, q=percentiles, axis=0)\n","\n"," ci = ConfidenceInterval(\n"," mean=sample_mean,\n"," stddev=sample_std,\n"," num_samples=len(samples),\n"," level=ci_level,\n"," ci_lower=ci_lower,\n"," ci_upper=ci_upper,\n"," )\n","\n"," return ci\n","\n","\n","def _generate_sample_indices(\n"," label: Label,\n"," is_binary: bool,\n"," num_bootstrap: int,\n"," seed: int,\n",") -> List[IndexSample]:\n"," \"\"\"Returns a list of `num_bootstrap` randomly sampled bootstrap indices.\n","\n"," Args:\n"," label: The ground truth label targets.\n"," is_binary: Whether to generate valid binary samples; i.e., each index sample\n"," contains at least one index corresponding to a label from each class.\n"," num_bootstrap: The number of bootstrap indices to generate.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A list of `num_bootstrap` bootstrap sample indices.\n"," \"\"\"\n"," rng = np.random.default_rng(seed)\n"," num_observations = len(label)\n"," sample_indices = []\n"," while len(sample_indices) < num_bootstrap:\n"," index = rng.integers(0, high=num_observations, size=num_observations)\n"," sample_true = label.values[index]\n"," # If computing a binary metric, skip indices that result in invalid labels.\n"," if is_binary and not is_valid_binary_label(sample_true):\n"," continue\n"," sample_indices.append(index)\n"," return sample_indices\n","\n","\n","def _compute_metric_samples(\n"," metric: Metric,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," sample_indices: Sequence[np.ndarray],\n",") -> Dict[str, np.ndarray]:\n"," \"\"\"Generates `num_bootstrap` metric samples for each `Prediction`.\n","\n"," Note: This method assumes that label and prediction values are orded so that\n"," the value at index `i` in a given `Prediction` corresponds to the label value\n"," at index `i` in `label`. Both the `Label` and `Prediction` arrays are indexed\n"," using the given `sample_indices`.\n","\n"," Args:\n"," metric: An instance of a bootstrappable `Metric`; used to compute samples.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," sample_indices: An array of bootstrap sample indices. If empty, returns the\n"," single value computed on the entire dataset for each prediction.\n","\n"," Returns:\n"," A mapping of model names to the corresponding metric samples array.\n"," \"\"\"\n"," if not sample_indices:\n"," metric_samples = {}\n"," for prediction in predictions:\n"," value = metric(label.values, prediction.values)\n"," metric_samples[prediction.model_name] = np.asarray([value])\n"," return metric_samples\n","\n"," metric_samples = {prediction.model_name: [] for prediction in predictions}\n"," for index in sample_indices:\n"," sample_true = label.values[index]\n"," for prediction in predictions:\n"," sample_value = metric(sample_true, prediction.values[index])\n"," metric_samples[prediction.model_name].append(sample_value)\n","\n"," metric_samples = {\n"," name: np.asarray(samples) for name, samples in metric_samples.items()\n"," }\n","\n"," return metric_samples\n","\n","\n","def _compute_all_metric_samples(\n"," metrics: Sequence[Metric],\n"," contains_binary_metric: bool,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," num_bootstrap: int,\n"," seed: int,\n",") -> Dict[str, Dict[str, np.ndarray]]:\n"," \"\"\"Generates `num_bootstrap` samples for each `Prediction` and `Metric`.\n","\n"," Args:\n"," metrics: A sequence of a bootstrappable `Metric` instances.\n"," contains_binary_metric: Whether the set of metrics contains a binary metric.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," num_bootstrap: The number of bootstrap iterations.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A mapping of metric names to model-sample dictionaries.\n"," \"\"\"\n"," sample_indices = _generate_sample_indices(\n"," label,\n"," contains_binary_metric,\n"," num_bootstrap,\n"," seed,\n"," )\n"," metric_samples = []\n"," for metric in metrics:\n"," metric_samples.append(\n"," _compute_metric_samples(\n"," metric=metric,\n"," label=label,\n"," predictions=predictions,\n"," sample_indices=sample_indices,\n"," )\n"," )\n","\n"," return {\n"," metric.name: metric_sample\n"," for metric, metric_sample in zip(metrics, metric_samples)\n"," }\n","\n","\n","def _process_metric_samples(\n"," metric: Metric,\n"," predictions: Sequence[Prediction],\n"," model_names_to_metric_samples: Dict[str, np.ndarray],\n"," ci_level: float,\n",") -> List[Result]:\n"," \"\"\"Compute `ConfidenceInterval`s for metric samples across predictions.\"\"\"\n"," results = []\n"," for prediction in predictions:\n"," metric_samples = model_names_to_metric_samples[prediction.model_name]\n"," ci = _compute_confidence_interval(metric_samples, ci_level)\n"," result = Result(prediction.model_name, prediction.name, metric.name, ci)\n"," results.append(result)\n"," return results\n","\n","\n","def _process_metric_samples_paired(\n"," metric: Metric,\n"," predictions: Sequence[Prediction],\n"," model_names_to_metric_samples: Dict[str, np.ndarray],\n"," ci_level: float,\n",") -> List[PairedResult]:\n"," \"\"\"Compute `ConfidenceInterval`s for paired samples across predictions.\"\"\"\n"," results = []\n"," for i, prediction_a in enumerate(predictions[:-1]):\n"," for prediction_b in predictions[i + 1 :]:\n"," # Compute the result of `prediction_a - prediction_b`.\n"," metric_samples_a = model_names_to_metric_samples[prediction_a.model_name]\n"," metric_samples_b = model_names_to_metric_samples[prediction_b.model_name]\n"," metric_samples_diff = metric_samples_a - metric_samples_b\n"," ci = _compute_confidence_interval(metric_samples_diff, ci_level)\n"," result = PairedResult(\n"," prediction_a.model_name,\n"," prediction_a.name,\n"," prediction_b.model_name,\n"," prediction_b.name,\n"," metric.name,\n"," ci,\n"," )\n"," results.append(result)\n"," # Derive and include the result of `prediction_b - prediction_a`.\n"," results.append(_reverse_paired_result(result))\n"," return results\n","\n","\n","def _bootstrap(\n"," metrics: Sequence[Metric],\n"," contains_binary_metric: bool,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," num_bootstrap: int,\n"," ci_level: float,\n"," seed: int,\n",") -> Dict[str, List[Result]]:\n"," \"\"\"Performs bootstrapping for all models using the given metrics.\n","\n"," Args:\n"," metrics: A sequence of a bootstrappable `Metric` instances.\n"," contains_binary_metric: Whether the set of metrics contains a binary metric.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," num_bootstrap: The number of bootstrap iterations.\n"," ci_level: The confidence level/width of the desired confidence interval.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A dictionary mapping metric names to a list of `Result`s containing the mean\n"," metric values of each model over `num_bootstrap` bootstrapping iterations.\n"," \"\"\"\n"," metric_to_model_to_samples = _compute_all_metric_samples(\n"," metrics,\n"," contains_binary_metric,\n"," label,\n"," predictions,\n"," num_bootstrap,\n"," seed,\n"," )\n"," metric_samples = []\n"," for metric in metrics:\n"," metric_samples.append(\n"," _process_metric_samples(\n"," metric=metric,\n"," predictions=predictions,\n"," model_names_to_metric_samples=metric_to_model_to_samples[\n"," metric.name\n"," ],\n"," ci_level=ci_level,\n"," )\n"," )\n","\n"," return {\n"," metric.name: metric_sample\n"," for metric, metric_sample in zip(metrics, metric_samples)\n"," }\n","\n","\n","def _paired_bootstrap(\n"," metrics: Sequence[Metric],\n"," contains_binary_metric: bool,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," num_bootstrap: int,\n"," ci_level: float,\n"," seed: int,\n",") -> Dict[str, List[PairedResult]]:\n"," \"\"\"Performs paired bootstrapping for all models using the given metrics.\n","\n"," Args:\n"," metrics: A sequence of a bootstrappable `Metric` instances.\n"," contains_binary_metric: Whether the set of metrics contains a binary metric.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," num_bootstrap: The number of bootstrap iterations.\n"," ci_level: The confidence level/width of the desired confidence interval.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A dictionary mapping metric names to `PairedResult`s containing the mean\n"," metric difference between models over `num_bootstrap` bootstrapping\n"," iterations.\n"," \"\"\"\n"," metric_to_model_to_samples = _compute_all_metric_samples(\n"," metrics,\n"," contains_binary_metric,\n"," label,\n"," predictions,\n"," num_bootstrap,\n"," seed,\n"," )\n"," metric_samples = []\n"," for metric in metrics:\n"," metric_samples.append(\n"," _process_metric_samples_paired(\n"," metric=metric,\n"," predictions=predictions,\n"," model_names_to_metric_samples=metric_to_model_to_samples[\n"," metric.name\n"," ],\n"," ci_level=ci_level,\n"," )\n"," )\n","\n"," return {\n"," metric.name: metric_sample\n"," for metric, metric_sample in zip(metrics, metric_samples)\n"," }\n","\n","\n","def _default_binary_metrics() -> List[BinaryMetric]:\n"," \"\"\"Returns `PerformanceMetrics`'s default metrics for binary target.\"\"\"\n"," metrics = [\n"," BinaryMetric('num', count),\n"," BinaryMetric('auc', sklearn.metrics.roc_auc_score),\n"," BinaryMetric('auprc', sklearn.metrics.average_precision_score),\n"," ]\n"," for percentile in [100, 10, 5, 1]:\n"," metrics.append(\n"," BinaryMetric(\n"," f'freq@{percentile:>03}%',\n"," frequency_fn(percentile),\n"," )\n"," )\n"," return metrics\n","\n","\n","def _default_continuous_metrics() -> List[ContinuousMetric]:\n"," \"\"\"Returns `PerformanceMetrics`'s default metrics for continuous target.\"\"\"\n"," metrics = [\n"," ContinuousMetric('num', count),\n"," ContinuousMetric('pearson', pearsonr),\n"," ContinuousMetric('pearsonr_squared', pearsonr_squared),\n"," ContinuousMetric('spearman', spearmanr),\n"," ContinuousMetric('mse', sklearn.metrics.mean_squared_error),\n"," ContinuousMetric('mae', sklearn.metrics.mean_absolute_error),\n"," ]\n"," return metrics\n","\n","\n","def _default_metrics(binary_targets: bool) -> List[Metric]:\n"," \"\"\"Returns `PerformanceMetrics`'s default set of metrics for the target type.\n","\n"," Args:\n"," binary_targets: Whether the target labels are binary. If false, the returned\n"," metrics assume continuous labels.\n","\n"," Returns:\n"," The default set of binary or continuous `bootstrap_metrics.Metric`s.\n"," \"\"\"\n"," if binary_targets:\n"," return _default_binary_metrics()\n"," return _default_continuous_metrics()\n","\n","\n","class PerformanceMetrics:\n"," \"\"\"A named collection of invocable, bootstrapable `Metric`s.\n","\n"," Initializes a class that applies the given `Metric` functions to new ground\n"," truth labels and predictions. `Metric`s can be evaluated with and without\n"," bootstrapping.\n","\n"," The default metrics are number of samples, auc, auprc, and frequency\n"," calculations for the top 100/10/5/1 top percentiles, if `default_metrics` is\n"," 'binary'. If `default_metrics` is 'continuous', the default metrics are\n"," Pearson and Spearman correlations, the square of the Pearson correlation, mean\n"," squared error (MSE) and mean absolute error (MAE).\n","\n"," TODO(b/199452239): Refactor `PerformanceMetrics` so that the default metric\n"," set is not parameterized with a string.\n","\n"," Raises:\n"," ValueError: if an item in `metrics` is not of type `Metric`.\n"," \"\"\"\n","\n"," def __init__(\n"," self,\n"," name: str,\n"," default_metrics: Optional[str] = None,\n"," metrics: Optional[List[Metric]] = None,\n"," ) -> None:\n","\n"," if metrics is None:\n"," if default_metrics is None:\n"," raise ValueError('`default_metrics` is None and no metric is provided.')\n"," elif default_metrics == 'binary':\n"," metrics = _default_metrics(binary_targets=True)\n"," elif default_metrics == 'continuous':\n"," metrics = _default_metrics(binary_targets=False)\n"," else:\n"," raise ValueError(\n"," 'unknown `default_metrics`: {}'.format(default_metrics)\n"," )\n","\n"," for metric in metrics:\n"," if not isinstance(metric, Metric):\n"," raise ValueError('Invalid metric value: must be of class `Metric`.')\n","\n"," if len(metrics) != len({metric.name for metric in metrics}):\n"," raise ValueError(f'Metric names must be unique: {metrics}')\n","\n"," self.name = name\n"," self.metrics = metrics\n"," self.contains_binary = any(is_binary(m) for m in metrics)\n","\n"," def compute(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," ) -> Dict[str, Result]:\n"," \"\"\"Evaluates all metrics using the given labels and predictions.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," mask: A boolean mask; applied to `y_true` and `y_pred`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n","\n"," Returns:\n"," A dictionary of bootstrapped metrics keyed on metric name with\n"," `Result` values.\n","\n"," Raises:\n"," ValueError: If the dimensions of `y_true`, `y_pred`, or `mask` do not\n"," match, or labels are not in {0 , 1}.\n"," \"\"\"\n"," if len(y_true) != len(y_pred):\n"," raise ValueError('Label and prediction dimensions do not match.')\n","\n"," if mask is not None and len(mask) != len(y_pred):\n"," raise ValueError('Label and prediction dimensions do not match mask.')\n","\n"," if mask is not None:\n"," y_true = y_true[mask]\n"," y_pred = y_pred[mask]\n","\n"," # TODO(b/197539434): Pipe through non-empty names after public api refactor.\n"," label_name = 'label'\n"," label = Label(label_name, y_true)\n"," predictions = [Prediction(label_name, y_pred, 'model')]\n","\n"," metric_results = _bootstrap(\n"," self.metrics,\n"," contains_binary_metric=self.contains_binary,\n"," label=label,\n"," predictions=predictions,\n"," num_bootstrap=n_bootstrap,\n"," ci_level=conf_interval,\n"," seed=seed,\n"," )\n","\n"," # TODO(b/197539434): Remove temporary asserts after public api refactor.\n"," final_results = {}\n"," for metric_name, results in metric_results.items():\n"," assert len(results) == 1\n"," final_results[metric_name] = results[0]\n","\n"," return final_results\n","\n"," def compute_paired(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred_a: np.ndarray,\n"," y_pred_b: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," ) -> Dict[str, PairedResult]:\n"," \"\"\"Computes a paired bootstrap value for each metric.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred_a: Target predictions from model A; compared to `y_pred_b`.\n"," y_pred_b: Target predictions from model B; compared to `y_pred_a`.\n"," mask: A boolean mask; applied to `y_true`, `y_pred_a`, and `y_pred_b`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n","\n"," Returns:\n"," A dictionary of paired bootstrapped metrics keyed on metric name with\n"," `PairedResult` values.\n","\n"," Raises:\n"," ValueError: If the dimensions of `y_true`, `y_pred_a`, `y_pred_b` or\n"," `mask` do not match, or labels are not in {0 , 1}.\n"," \"\"\"\n"," if (len(y_true) != len(y_pred_a)) or (len(y_true) != len(y_pred_b)):\n"," raise ValueError('Label and prediction dimensions do not match.')\n","\n"," if mask is not None and len(mask) != len(y_pred_a):\n"," raise ValueError('Label and prediction dimensions do not match mask.')\n","\n"," if mask is not None:\n"," y_true = y_true[mask]\n"," y_pred_a = y_pred_a[mask]\n"," y_pred_b = y_pred_b[mask]\n","\n"," # TODO(b/197539434): Pipe through non-empty names after public api refactor.\n"," label_name = 'label'\n"," label = Label(label_name, y_true)\n"," first_model_name = 'model_a'\n"," predictions = [\n"," Prediction(label_name, y_pred_a, first_model_name),\n"," Prediction(label_name, y_pred_b, 'model_b'),\n"," ]\n","\n"," metric_results = _paired_bootstrap(\n"," self.metrics,\n"," contains_binary_metric=self.contains_binary,\n"," label=label,\n"," predictions=predictions,\n"," num_bootstrap=n_bootstrap,\n"," ci_level=conf_interval,\n"," seed=seed,\n"," )\n","\n"," # TODO(b/197539434): Remove temporary asserts after public api refactor.\n"," final_results = {}\n"," for metric_name, results in metric_results.items():\n"," assert len(results) == 2\n"," assert results[0].model_name_a == first_model_name\n"," final_results[metric_name] = results[0]\n","\n"," return final_results\n","\n"," def _print_results(\n"," self,\n"," title: str,\n"," results: Dict[str, Union[Result, PairedResult]],\n"," ) -> None:\n"," \"\"\"Prints each result object under the current name and given title.\"\"\"\n"," print(f'{self.name}: {title}')\n"," for _, result in sorted(results.items()):\n"," print(f'\\t{result}')\n","\n"," def compute_and_print(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," title: str = '',\n"," ) -> None:\n"," \"\"\"Evaluates and pretty-prints metrics using given labels and predictions.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," mask: A boolean mask; applied to `y_true` and `y_pred`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n"," title: A title appended to the printed evaluation metrics.\n","\n"," Raises:\n"," ValueError: If any of `y_true`, `y_pred`, or `mask` are not of type\n"," numpy.array of if their dimensions do not match.\n"," \"\"\"\n"," results = self.compute(\n"," y_true,\n"," y_pred,\n"," mask=mask,\n"," n_bootstrap=n_bootstrap,\n"," conf_interval=conf_interval,\n"," seed=seed,\n"," )\n"," self._print_results(title, results)\n","\n"," def compute_paired_and_print(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred_a: np.ndarray,\n"," y_pred_b: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," title: str = '',\n"," **kwargs,\n"," ) -> None:\n"," \"\"\"Evaluates and pretty-prints paired metrics.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred_a: Target predictions from model A; compared to `y_pred_b`.\n"," y_pred_b: Target predictions from model B; compared to `y_pred_a`.\n"," mask: A boolean mask; applied to `y_true`, `y_pred_a`, and `y_pred_b`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n"," title: A title appended to the printed evaluation metrics.\n"," **kwargs: Additional keyword arguments passed to each Metric's `func`.\n"," \"\"\"\n"," results = self.compute_paired(\n"," y_true,\n"," y_pred_a,\n"," y_pred_b,\n"," mask=mask,\n"," n_bootstrap=n_bootstrap,\n"," conf_interval=conf_interval,\n"," seed=seed,\n"," **kwargs,\n"," )\n"," self._print_results(title, results)"]},{"cell_type":"code","execution_count":5,"metadata":{"id":"x4222NTc0xpR","executionInfo":{"status":"ok","timestamp":1717789982063,"user_tz":240,"elapsed":15,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["N_BOOTSTRAP = 300\n","BOOTSTRAP_METRICS_LIST = [\n"," BinaryMetric('roc_auc', metrics.roc_auc_score),\n"," BinaryMetric('pr_auc', metrics.average_precision_score),\n"," ContinuousMetric('pearsonr', pearsonr),\n"," BinaryMetric('top10prev', frequency_fn(10)),\n","]\n","\n","def get_prs_eval_info(y_true, y_pred, name, as_dataframe=False):\n"," performance_metrics = PerformanceMetrics(\n"," 'Metrics', metrics=BOOTSTRAP_METRICS_LIST)\n"," performance_metrics_values = performance_metrics.compute(\n"," y_true=y_true,\n"," y_pred=y_pred,\n"," n_bootstrap=N_BOOTSTRAP,\n"," )\n"," # print(performance_metrics_values, flush=True)\n"," roc_auc_ci = performance_metrics_values['roc_auc'].ci\n"," pr_auc_ci = performance_metrics_values['pr_auc'].ci\n"," pearsonr_ci = performance_metrics_values['pearsonr'].ci\n"," top10prev_ci = performance_metrics_values['top10prev'].ci\n"," info = {\n"," 'method': name,\n"," 'pearsonr': pearsonr_ci.mean,\n"," 'pearsonr_std': pearsonr_ci.stddev,\n"," 'pearsonr_lower': pearsonr_ci.ci_lower,\n"," 'pearsonr_upper': pearsonr_ci.ci_upper,\n"," 'roc_auc': roc_auc_ci.mean,\n"," 'roc_auc_std': roc_auc_ci.stddev,\n"," 'roc_auc_lower': roc_auc_ci.ci_lower,\n"," 'roc_auc_upper': roc_auc_ci.ci_upper,\n"," 'pr_auc': pr_auc_ci.mean,\n"," 'pr_auc_std': pr_auc_ci.stddev,\n"," 'pr_auc_lower': pr_auc_ci.ci_lower,\n"," 'pr_auc_upper': pr_auc_ci.ci_upper,\n"," 'top10prev': top10prev_ci.mean,\n"," 'top10prev_std': top10prev_ci.stddev,\n"," 'top10prev_lower': top10prev_ci.ci_lower,\n"," 'top10prev_upper': top10prev_ci.ci_upper,\n"," }\n"," if as_dataframe:\n"," return pd.DataFrame(info, index=[0])\n"," else:\n"," return info\n","\n","\n","def get_prs_paired_eval_info(y_true,\n"," y_pred1,\n"," y_pred2,\n"," name1,\n"," name2,\n"," as_dataframe=False):\n"," performance_metrics = PerformanceMetrics(\n"," 'Metrics', metrics=BOOTSTRAP_METRICS_LIST)\n"," performance_metrics_values_paired = performance_metrics.compute_paired(\n"," y_true=y_true,\n"," y_pred_a=y_pred1,\n"," y_pred_b=y_pred2,\n"," n_bootstrap=N_BOOTSTRAP,\n"," )\n"," # print(performance_metrics_values_paired, flush=True)\n"," roc_auc_ci = performance_metrics_values_paired['roc_auc'].ci\n"," pr_auc_ci = performance_metrics_values_paired['pr_auc'].ci\n"," pearsonr_ci = performance_metrics_values_paired['pearsonr'].ci\n"," top10prev_ci = performance_metrics_values_paired['top10prev'].ci\n"," info = {\n"," 'method_a': name1,\n"," 'method_b': name2,\n"," 'pearsonr': pearsonr_ci.mean,\n"," 'pearsonr_std': pearsonr_ci.stddev,\n"," 'pearsonr_lower': pearsonr_ci.ci_lower,\n"," 'pearsonr_upper': pearsonr_ci.ci_upper,\n"," 'roc_auc': roc_auc_ci.mean,\n"," 'roc_auc_std': roc_auc_ci.stddev,\n"," 'roc_auc_lower': roc_auc_ci.ci_lower,\n"," 'roc_auc_upper': roc_auc_ci.ci_upper,\n"," 'pr_auc': pr_auc_ci.mean,\n"," 'pr_auc_std': pr_auc_ci.stddev,\n"," 'pr_auc_lower': pr_auc_ci.ci_lower,\n"," 'pr_auc_upper': pr_auc_ci.ci_upper,\n"," 'top10prev': top10prev_ci.mean,\n"," 'top10prev_std': top10prev_ci.stddev,\n"," 'top10prev_lower': top10prev_ci.ci_lower,\n"," 'top10prev_upper': top10prev_ci.ci_upper,\n"," }\n"," if as_dataframe:\n"," return pd.DataFrame(info, index=[0])\n"," else:\n"," return info"]},{"cell_type":"markdown","metadata":{"id":"NOaueJxRPmpG"},"source":["# Simulated data generation\n","\n","In this code example, we generate some simulated data (N=1,000) to demonstrate how to use the above code snippet to compute various metrics in the PRS evaluation part of the paper."]},{"cell_type":"code","execution_count":6,"metadata":{"id":"iXHTm8dxzY2H","executionInfo":{"status":"ok","timestamp":1717789982064,"user_tz":240,"elapsed":14,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["np.random.seed(42)\n","individual_prs1 = np.random.normal(size=(1000,))\n","individual_prs2 = 0.8 * individual_prs1 + 0.2 * np.random.normal(size=(1000,))\n","individual_phenotype = 0.3 * individual_prs1 + 0.7 * np.random.normal(\n"," size=(1000,)\n",")\n","individual_phenotype = (individual_phenotype >= 0).astype(int)\n","\n","data_df = pd.DataFrame({\n"," 'prs1': individual_prs1,\n"," 'prs2': individual_prs2,\n"," 'phenotype': individual_phenotype,\n","})"]},{"cell_type":"code","execution_count":7,"metadata":{"colab":{"height":206,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":13,"status":"ok","timestamp":1717789982064,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"bzdHe1jqULbv","outputId":"f8e850ec-2fdf-45fb-b2be-f4e7ebe5cafa"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" prs1 prs2 phenotype\n","0 0.496714 0.677242 0\n","1 -0.138264 0.074315 0\n","2 0.647689 0.530077 0\n","3 1.523030 1.089037 1\n","4 -0.234153 -0.047678 0"],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
prs1prs2phenotype
00.4967140.6772420
1-0.1382640.0743150
20.6476890.5300770
31.5230301.0890371
4-0.234153-0.0476780
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n"," \n","\n","\n","\n"," \n","
\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","variable_name":"data_df","summary":"{\n \"name\": \"data_df\",\n \"rows\": 1000,\n \"fields\": [\n {\n \"column\": \"prs1\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.9792159381796757,\n \"min\": -3.2412673400690726,\n \"max\": 3.852731490654721,\n \"num_unique_values\": 1000,\n \"samples\": [\n 0.543360192379935,\n 0.9826909839455139,\n -1.8408742313316453\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"prs2\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.8005263506410991,\n \"min\": -2.4852626735659844,\n \"max\": 3.4321005411611654,\n \"num_unique_values\": 1000,\n \"samples\": [\n 0.5511076945976712,\n 0.5725922028405726,\n -1.4935892287728105\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"phenotype\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0,\n \"min\": 0,\n \"max\": 1,\n \"num_unique_values\": 2,\n \"samples\": [\n 1,\n 0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":7}],"source":["data_df.head()"]},{"cell_type":"markdown","metadata":{"id":"4LYsbEE3RdeF"},"source":["# PRS evaluation with bootstrapping\n","\n","The following code generates all evaluation metrics, namely Pearson R, AUC-ROC, AUC-PR, top 10% prevalence, and their 95% confidence intervals using bootstrapping. Note that, from the way we generated the simulated data, we expect the Pearson R of ~0.3 for `prs1` and we expect `prs1` to have higher correlation with the phenotype than `prs2`."]},{"cell_type":"code","execution_count":8,"metadata":{"colab":{"height":101,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":17429,"status":"ok","timestamp":1717789999485,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"WVJnK7BAPi33","outputId":"68161231-112f-4e33-d8d0-0ffc89019139"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" method pearsonr pearsonr_std pearsonr_lower pearsonr_upper roc_auc \\\n","0 prs1 0.333455 0.027456 0.277529 0.387433 0.69263 \n","\n"," roc_auc_std roc_auc_lower roc_auc_upper pr_auc pr_auc_std \\\n","0 0.016445 0.65976 0.725288 0.675271 0.022152 \n","\n"," pr_auc_lower pr_auc_upper top10prev top10prev_std top10prev_lower \\\n","0 0.632141 0.715912 0.770216 0.043321 0.688044 \n","\n"," top10prev_upper \n","0 0.85078 "],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
methodpearsonrpearsonr_stdpearsonr_lowerpearsonr_upperroc_aucroc_auc_stdroc_auc_lowerroc_auc_upperpr_aucpr_auc_stdpr_auc_lowerpr_auc_uppertop10prevtop10prev_stdtop10prev_lowertop10prev_upper
0prs10.3334550.0274560.2775290.3874330.692630.0164450.659760.7252880.6752710.0221520.6321410.7159120.7702160.0433210.6880440.85078
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","summary":"{\n \"name\": \")\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"method\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs1\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.3334554859786796,\n \"max\": 0.3334554859786796,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.3334554859786796\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.027455597173908577,\n \"max\": 0.027455597173908577,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.027455597173908577\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.2775293042598108,\n \"max\": 0.2775293042598108,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.2775293042598108\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.38743254268744753,\n \"max\": 0.38743254268744753,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.38743254268744753\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6926303605619311,\n \"max\": 0.6926303605619311,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6926303605619311\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.016445301315729702,\n \"max\": 0.016445301315729702,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.016445301315729702\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.659760150142918,\n \"max\": 0.659760150142918,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.659760150142918\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7252876945992696,\n \"max\": 0.7252876945992696,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7252876945992696\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.675270596876246,\n \"max\": 0.675270596876246,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.675270596876246\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.02215152388674347,\n \"max\": 0.02215152388674347,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.02215152388674347\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6321413648383354,\n \"max\": 0.6321413648383354,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6321413648383354\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7159121917609861,\n \"max\": 0.7159121917609861,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7159121917609861\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7702162426122681,\n \"max\": 0.7702162426122681,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7702162426122681\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.04332125213088804,\n \"max\": 0.04332125213088804,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.04332125213088804\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6880441176470588,\n \"max\": 0.6880441176470588,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6880441176470588\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.8507797029702969,\n \"max\": 0.8507797029702969,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.8507797029702969\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":8}],"source":["get_prs_eval_info(\n"," y_true=data_df['phenotype'],\n"," y_pred=data_df['prs1'],\n"," name='prs1',\n"," as_dataframe=True\n",")"]},{"cell_type":"code","execution_count":9,"metadata":{"colab":{"height":101,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":9213,"status":"ok","timestamp":1717790008685,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"puOfA5wuQeiJ","outputId":"40a4792a-c897-450c-ee39-aa8ecd72f761"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" method pearsonr pearsonr_std pearsonr_lower pearsonr_upper roc_auc \\\n","0 prs2 0.319189 0.027899 0.260433 0.373947 0.6837 \n","\n"," roc_auc_std roc_auc_lower roc_auc_upper pr_auc pr_auc_std \\\n","0 0.016604 0.649911 0.717019 0.664467 0.022454 \n","\n"," pr_auc_lower pr_auc_upper top10prev top10prev_std top10prev_lower \\\n","0 0.620486 0.706022 0.764624 0.042396 0.671552 \n","\n"," top10prev_upper \n","0 0.84 "],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
methodpearsonrpearsonr_stdpearsonr_lowerpearsonr_upperroc_aucroc_auc_stdroc_auc_lowerroc_auc_upperpr_aucpr_auc_stdpr_auc_lowerpr_auc_uppertop10prevtop10prev_stdtop10prev_lowertop10prev_upper
0prs20.3191890.0278990.2604330.3739470.68370.0166040.6499110.7170190.6644670.0224540.6204860.7060220.7646240.0423960.6715520.84
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","summary":"{\n \"name\": \")\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"method\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs2\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.3191890184766251,\n \"max\": 0.3191890184766251,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.3191890184766251\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.027898865889530153,\n \"max\": 0.027898865889530153,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.027898865889530153\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.2604328480042442,\n \"max\": 0.2604328480042442,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.2604328480042442\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.3739469506434232,\n \"max\": 0.3739469506434232,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.3739469506434232\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6836996447028457,\n \"max\": 0.6836996447028457,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6836996447028457\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.01660378118234475,\n \"max\": 0.01660378118234475,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.01660378118234475\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6499110741641438,\n \"max\": 0.6499110741641438,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6499110741641438\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7170185826451294,\n \"max\": 0.7170185826451294,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7170185826451294\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6644674946186202,\n \"max\": 0.6644674946186202,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6644674946186202\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.0224540065869167,\n \"max\": 0.0224540065869167,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.0224540065869167\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6204864568922334,\n \"max\": 0.6204864568922334,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6204864568922334\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7060224657169427,\n \"max\": 0.7060224657169427,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7060224657169427\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.764623511500396,\n \"max\": 0.764623511500396,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.764623511500396\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.042396301865302535,\n \"max\": 0.042396301865302535,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.042396301865302535\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6715519801980199,\n \"max\": 0.6715519801980199,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6715519801980199\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.84,\n \"max\": 0.84,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.84\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":9}],"source":["get_prs_eval_info(\n"," y_true=data_df['phenotype'],\n"," y_pred=data_df['prs2'],\n"," name='prs2',\n"," as_dataframe=True\n",")"]},{"cell_type":"markdown","metadata":{"id":"OiLCjqcrSjPg"},"source":["# PRS comparison with paired bootstrapping\n","\n","The following code snippet compares the performance of `prs1` and `prs2` using paired bootstrapping. Note that the difference is statistically significant with 95% paired bootstrapping confidence interval, if the lower and upper end of the confidence interval are both positive (implying `prs1` is significantly better than `prs2`) or both negative (implying `prs2` is significantly better than `prs1`)."]},{"cell_type":"code","execution_count":10,"metadata":{"colab":{"height":101,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":6240,"status":"ok","timestamp":1717790014919,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"oRKgjH_uR2wr","outputId":"76474def-1edd-4cbd-c801-6b00f324f288"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" method_a method_b pearsonr pearsonr_std pearsonr_lower pearsonr_upper \\\n","0 prs1 prs2 0.014266 0.007112 0.000436 0.027211 \n","\n"," roc_auc roc_auc_std roc_auc_lower roc_auc_upper pr_auc pr_auc_std \\\n","0 0.008931 0.004466 0.000157 0.017171 0.010803 0.005761 \n","\n"," pr_auc_lower pr_auc_upper top10prev top10prev_std top10prev_lower \\\n","0 -0.00061 0.02107 0.005593 0.026971 -0.042589 \n","\n"," top10prev_upper \n","0 0.062382 "],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
method_amethod_bpearsonrpearsonr_stdpearsonr_lowerpearsonr_upperroc_aucroc_auc_stdroc_auc_lowerroc_auc_upperpr_aucpr_auc_stdpr_auc_lowerpr_auc_uppertop10prevtop10prev_stdtop10prev_lowertop10prev_upper
0prs1prs20.0142660.0071120.0004360.0272110.0089310.0044660.0001570.0171710.0108030.005761-0.000610.021070.0055930.026971-0.0425890.062382
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","summary":"{\n \"name\": \" as_dataframe=True)\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"method_a\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs1\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"method_b\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs2\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.014266467502054426,\n \"max\": 0.014266467502054426,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.014266467502054426\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.007111892690604321,\n \"max\": 0.007111892690604321,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.007111892690604321\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.00043626824886599245,\n \"max\": 0.00043626824886599245,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.00043626824886599245\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.027211089302840434,\n \"max\": 0.027211089302840434,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.027211089302840434\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.008930715859085309,\n \"max\": 0.008930715859085309,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.008930715859085309\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.004466363148919537,\n \"max\": 0.004466363148919537,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.004466363148919537\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.00015733124729375172,\n \"max\": 0.00015733124729375172,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.00015733124729375172\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.017170818130808965,\n \"max\": 0.017170818130808965,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.017170818130808965\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.010803102257625864,\n \"max\": 0.010803102257625864,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.010803102257625864\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.005760958016623593,\n \"max\": 0.005760958016623593,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.005760958016623593\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": -0.0006104367572841078,\n \"max\": -0.0006104367572841078,\n \"num_unique_values\": 1,\n \"samples\": [\n -0.0006104367572841078\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.02106968216083579,\n \"max\": 0.02106968216083579,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.02106968216083579\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.005592731111872085,\n \"max\": 0.005592731111872085,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.005592731111872085\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.026971273443313012,\n \"max\": 0.026971273443313012,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.026971273443313012\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": -0.04258910891089107,\n \"max\": -0.04258910891089107,\n \"num_unique_values\": 1,\n \"samples\": [\n -0.04258910891089107\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.062381770529994184,\n \"max\": 0.062381770529994184,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.062381770529994184\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":10}],"source":["get_prs_paired_eval_info(\n"," y_true=data_df['phenotype'],\n"," y_pred1=data_df['prs1'],\n"," y_pred2=data_df['prs2'],\n"," name1='prs1',\n"," name2='prs2',\n"," as_dataframe=True)"]}]} \ No newline at end of file From 606967862ac4503f8ef3fa33f2a679fc0da61d9a Mon Sep 17 00:00:00 2001 From: Taedong Yun Date: Fri, 7 Jun 2024 16:13:53 -0400 Subject: [PATCH 4/4] fix license year --- regle/analysis/embedding_interpretability.ipynb | 2 +- regle/analysis/pca_and_spline_fitting.ipynb | 2 +- regle/analysis/prs_analysis.ipynb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/regle/analysis/embedding_interpretability.ipynb b/regle/analysis/embedding_interpretability.ipynb index 16a2377..d9fa54c 100644 --- a/regle/analysis/embedding_interpretability.ipynb +++ b/regle/analysis/embedding_interpretability.ipynb @@ -1 +1 @@ -{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"provenance":[]},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"code","source":["#@title Licensed under the BSD-3 License (the \"License\"); { display-mode: \"form\" }\n","# Copyright 2021 Google LLC.\n","#\n","# Redistribution and use in source and binary forms, with or without modification,\n","# are permitted provided that the following conditions are met:\n","#\n","# 1. Redistributions of source code must retain the above copyright notice, this\n","# list of conditions and the following disclaimer.\n","#\n","# 2. Redistributions in binary form must reproduce the above copyright notice,\n","# this list of conditions and the following disclaimer in the documentation\n","# and/or other materials provided with the distribution.\n","#\n","# 3. Neither the name of the copyright holder nor the names of its contributors\n","# may be used to endorse or promote products derived from this software without\n","# specific prior written permission.\n","#\n","# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n","# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n","# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n","# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\n","# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n","# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n","# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\n","# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n","# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n","# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."],"metadata":{"id":"r2mwcs7BPN7G","executionInfo":{"status":"ok","timestamp":1717789843829,"user_tz":240,"elapsed":8,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"execution_count":1,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"TQe5CETGcdwz"},"source":["# Download Keras checkpoints from our GitHub repo"]},{"cell_type":"code","execution_count":null,"metadata":{"id":"a1RXc2pKYPtM"},"outputs":[],"source":["!mkdir -p rspincs/variables\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/saved_model.pb -P rspincs/\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/keras_metadata.pb -P rspincs/\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/variables/variables.data-00000-of-00001 -P rspincs/variables/\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/variables/variables.index -P rspincs/variables/"]},{"cell_type":"markdown","metadata":{"id":"hjRXNyKwcy8T"},"source":["# Imports and functions"]},{"cell_type":"code","execution_count":3,"metadata":{"id":"w6MpGCYoSOgt","executionInfo":{"status":"ok","timestamp":1717789860399,"user_tz":240,"elapsed":14126,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["from typing import Optional\n","\n","import matplotlib as mpl\n","import matplotlib.pyplot as plt\n","import numpy as np\n","import tensorflow as tf"]},{"cell_type":"code","execution_count":4,"metadata":{"id":"CTCzhsgYVt3A","executionInfo":{"status":"ok","timestamp":1717789860641,"user_tz":240,"elapsed":245,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["# The example values for the 5 (standardized) spirogram EDFs:\n","# 'blow_fev1', 'blow_fvc', 'blow_pef', 'blow_ratio', 'blow_fef25_75'\n","EDF_VALUE_EXAMPLE = [-1.8, -1.8, -1.4, -0.7, -1.5]\n","\n","# Note we use 0, 1, ..., 999 for the volume values in flow-volume curves,\n","# which were interpolated between 0 and 6.58.\n","VOLUME_SCALE_FACTOR = 6.58 / 1000\n","\n","\n","def _draw_double_arrow(\n"," ax: mpl.axes.Axes,\n"," x1: float,\n"," x2: float,\n"," y: float,\n"," arrow_color: str = '#d62728',\n","):\n"," \"\"\"Draw an arrow pointing both sides between (x1, y) and (x2, y).\"\"\"\n"," ax.arrow(\n"," x1,\n"," y,\n"," x2 - x1,\n"," 0,\n"," fc=arrow_color,\n"," ec=arrow_color,\n"," width=0.04,\n"," head_width=0.15,\n"," head_length=0.05,\n"," zorder=100,\n"," )\n"," ax.arrow(\n"," x2,\n"," y,\n"," x1 - x2,\n"," 0,\n"," fc=arrow_color,\n"," ec=arrow_color,\n"," width=0.04,\n"," head_width=0.15,\n"," head_length=0.05,\n"," zorder=100,\n"," )\n","\n","\n","def generate_rspincs_reconstruction_plot(\n"," vae_model: tf.keras.Model,\n"," latent_dim: int,\n"," fpath_noext: Optional[str] = None,\n"," dpi=300,\n",") -> None:\n"," \"\"\"Generate reconstructed spirograms while varying each RSPINCs coordinate.\n","\n"," Args:\n"," row: A row of the SPINCs DF from which we'll get the values of manual\n"," features.\n"," vae_model: The VAE model to be used to reconstruct spirograms.\n"," latent_dim: The latent dimension.\n"," fpath_noext: The path to the output image file without extension.\n"," dpi: DPI of the image.\n"," \"\"\"\n"," cmap = plt.get_cmap('viridis')\n"," num_injected_features = 5\n"," radius = 1.5\n"," single_encodings = np.linspace(-radius, radius, num=21)\n"," decoder = vae_model.get_layer(f'{vae_model.name}_decoder')\n"," colorbar_width = 0.2\n","\n"," rescaled_volume = np.arange(1000) * VOLUME_SCALE_FACTOR\n"," _, axs = plt.subplots(\n"," 1,\n"," latent_dim + 1,\n"," figsize=(4 * latent_dim + colorbar_width, 3),\n"," width_ratios=[4] * latent_dim + [colorbar_width],\n"," )\n","\n"," for latent_idx in range(latent_dim):\n"," ax = axs[latent_idx]\n"," for img_idx, single_encoding in enumerate(single_encodings):\n"," # This value should be in [0, 1].\n"," color_val = single_encoding / (radius * 2) + 0.5\n"," encoding = np.zeros(latent_dim)\n"," encoding[latent_idx] = single_encoding\n"," encoding_input = np.expand_dims(encoding, axis=0)\n"," edf_input = np.expand_dims(np.array(EDF_VALUE_EXAMPLE), axis=0)\n"," vae_input = np.concatenate((encoding_input, edf_input), axis=-1)\n"," assert vae_input.shape == (1, latent_dim + num_injected_features)\n"," reconstructed = decoder(vae_input)[0].numpy()[:, 0]\n"," assert len(rescaled_volume) == len(reconstructed)\n"," ax.plot(\n"," rescaled_volume,\n"," reconstructed,\n"," color=cmap(color_val),\n"," alpha=0.9,\n"," linewidth=0.8,\n"," )\n"," ax.set_xlim((-20 * VOLUME_SCALE_FACTOR, 350 * VOLUME_SCALE_FACTOR))\n"," ax.set_ylim((-0.1, 4.2))\n"," ax.set_xlabel('Volume (L)')\n"," # Custom annotation for RSPINCs with dim = 2:\n"," if latent_idx == 0:\n"," ax.set_ylabel('Flow (L/s)')\n"," _draw_double_arrow(\n"," ax, 50 * VOLUME_SCALE_FACTOR, 140 * VOLUME_SCALE_FACTOR, 3\n"," )\n"," elif latent_idx == 1:\n"," _draw_double_arrow(\n"," ax, 5 * VOLUME_SCALE_FACTOR, 40 * VOLUME_SCALE_FACTOR, 3\n"," )\n"," ax.set_title('$\\mathrm{RSPINC}_' + f'{latent_idx + 1}$')\n"," # Draw a color palette on the last axis.\n"," cbar = plt.colorbar(\n"," mpl.cm.ScalarMappable(\n"," norm=mpl.colors.Normalize(vmin=-radius, vmax=radius), cmap=cmap\n"," ),\n"," cax=axs[-1],\n"," )\n"," cbar.ax.set_xlabel('Coordinate\\nValue')\n"," plt.tight_layout()\n"," plt.show()"]},{"cell_type":"markdown","metadata":{"id":"ols2RVM8c1sh"},"source":["# Load model and generate spirograms from embedding coordinate perturbation"]},{"cell_type":"code","execution_count":5,"metadata":{"id":"BX0g763-ZrLr","executionInfo":{"status":"ok","timestamp":1717789871265,"user_tz":240,"elapsed":10626,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["rspincs_model = tf.keras.models.load_model('rspincs')"]},{"cell_type":"code","execution_count":6,"metadata":{"id":"_2nYHVXhr6uT","colab":{"base_uri":"https://localhost:8080/","height":307},"executionInfo":{"status":"ok","timestamp":1717789873484,"user_tz":240,"elapsed":2232,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}},"outputId":"ff216c3b-79e7-4a39-9438-8035534ea568"},"outputs":[{"output_type":"display_data","data":{"text/plain":["
"],"image/png":"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rxf/+te/Gvwc/la99M477whN08S6devExo0bG2yHogewWfeoRKNRlixZwogRI3bvU1WVESNGsHDhwt88Z+HChQ2OBxg5cuQej2/K9qf8sGtF0VatWpGfn8/o0aNZvXr1oYjbJDSn7/+B6N27Nzk5OZx44onMnz8/0XEazX8Wo0pNTd3jMfJnYN8c6fUsyLp2fzS3n4H9JevapvkzkJqaysiRI3nxxRd/s1eopqaGLl26UFRURFFR0e79a9asoaamhq5duwLQpUuXX31f58+fv/vre9KlSxcWLVrUYN9/BvDvryVLlmBZFk8++SRHH300HTt2ZOfOnQ2OsdlsmKbZYF+fPn0wTZOysjLat2/fYMvOzj6gTHujWTdUKioqME2TrKysBvuzsrIoKSn5zXNKSkr26fimbH/K36lTJ1599VU++eQT3njjDSzLYvDgwWzfvv1QRE64PX3//X4/oVAoQakOnZycHF566SU+/PBDPvzwQ/Lz8xk+fDhLly5NdLQDZlkWEyZMYMiQIXTv3n2PxzWnOuBQONLrWZB17f6Qda2sa5t6PfDiiy9imiYDBgzgww8/ZOPGjaxdu5bnnnuOQYMGMWLECHr06MHFF1/M0qVLWbx4MWPHjuXYY4/d/SrXrbfeymuvvcbkyZPZuHEjTz31FNOmTds9SH1PbrzxRl599VWmTJnChg0buPfeew/4Dxnt27cnFovx/PPPs3nzZv7973/z0ksvNTimdevW1NfXM2vWLCoqKggGg3Ts2JGLL76YsWPHMm3aNLZs2cLixYuZNGkSX3zxxQFl2htyHRWpgUGDBjV4D3Lw4MF06dKFl19+mQceeCCByaRDoVOnTnTq1Gn358GDB1NQUMDTTz/9qykVDzfXXnstq1atYt68eYmOIkmyrj3Cybq26Wvbti1Lly7loYce4uabb6a4uJiMjAz69evH5MmTURSFTz75hOuvv55hw4ahqiqjRo3i+eef332NMWPG8Oyzz/LEE09w44030qZNG6ZMmcLw4cN/997nn38+BQUF3HbbbYTDYc4++2yuvvrqAxrD1atXL5566ikeffRR7rzzToYNG8akSZMYO3bs7mMGDx7MVVddxfnnn09lZSX33nsv9913H1OmTOHBBx/k5ptvZseOHaSnp3P00Udz2mmn7XeevdWsGyrp6elomvarOa1LS0v32F2VnZ29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\n"},"metadata":{}}],"source":["generate_rspincs_reconstruction_plot(\n"," vae_model=rspincs_model,\n"," latent_dim=2,\n",")"]}]} \ No newline at end of file +{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"provenance":[]},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"code","source":["#@title Licensed under the BSD-3 License (the \"License\"); { display-mode: \"form\" }\n","# Copyright 2023 Google LLC.\n","#\n","# Redistribution and use in source and binary forms, with or without modification,\n","# are permitted provided that the following conditions are met:\n","#\n","# 1. Redistributions of source code must retain the above copyright notice, this\n","# list of conditions and the following disclaimer.\n","#\n","# 2. Redistributions in binary form must reproduce the above copyright notice,\n","# this list of conditions and the following disclaimer in the documentation\n","# and/or other materials provided with the distribution.\n","#\n","# 3. Neither the name of the copyright holder nor the names of its contributors\n","# may be used to endorse or promote products derived from this software without\n","# specific prior written permission.\n","#\n","# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n","# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n","# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n","# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\n","# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n","# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n","# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\n","# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n","# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n","# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."],"metadata":{"id":"r2mwcs7BPN7G"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"TQe5CETGcdwz"},"source":["# Download Keras checkpoints from our GitHub repo"]},{"cell_type":"code","execution_count":null,"metadata":{"id":"a1RXc2pKYPtM"},"outputs":[],"source":["!mkdir -p rspincs/variables\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/saved_model.pb -P rspincs/\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/keras_metadata.pb -P rspincs/\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/variables/variables.data-00000-of-00001 -P rspincs/variables/\n","!wget https://github.com/Google-Health/genomics-research/raw/main/regle/saved_models/rspincs/variables/variables.index -P rspincs/variables/"]},{"cell_type":"markdown","metadata":{"id":"hjRXNyKwcy8T"},"source":["# Imports and functions"]},{"cell_type":"code","execution_count":null,"metadata":{"id":"w6MpGCYoSOgt"},"outputs":[],"source":["from typing import Optional\n","\n","import matplotlib as mpl\n","import matplotlib.pyplot as plt\n","import numpy as np\n","import tensorflow as tf"]},{"cell_type":"code","execution_count":null,"metadata":{"id":"CTCzhsgYVt3A"},"outputs":[],"source":["# The example values for the 5 (standardized) spirogram EDFs:\n","# 'blow_fev1', 'blow_fvc', 'blow_pef', 'blow_ratio', 'blow_fef25_75'\n","EDF_VALUE_EXAMPLE = [-1.8, -1.8, -1.4, -0.7, -1.5]\n","\n","# Note we use 0, 1, ..., 999 for the volume values in flow-volume curves,\n","# which were interpolated between 0 and 6.58.\n","VOLUME_SCALE_FACTOR = 6.58 / 1000\n","\n","\n","def _draw_double_arrow(\n"," ax: mpl.axes.Axes,\n"," x1: float,\n"," x2: float,\n"," y: float,\n"," arrow_color: str = '#d62728',\n","):\n"," \"\"\"Draw an arrow pointing both sides between (x1, y) and (x2, y).\"\"\"\n"," ax.arrow(\n"," x1,\n"," y,\n"," x2 - x1,\n"," 0,\n"," fc=arrow_color,\n"," ec=arrow_color,\n"," width=0.04,\n"," head_width=0.15,\n"," head_length=0.05,\n"," zorder=100,\n"," )\n"," ax.arrow(\n"," x2,\n"," y,\n"," x1 - x2,\n"," 0,\n"," fc=arrow_color,\n"," ec=arrow_color,\n"," width=0.04,\n"," head_width=0.15,\n"," head_length=0.05,\n"," zorder=100,\n"," )\n","\n","\n","def generate_rspincs_reconstruction_plot(\n"," vae_model: tf.keras.Model,\n"," latent_dim: int,\n"," fpath_noext: Optional[str] = None,\n"," dpi=300,\n",") -> None:\n"," \"\"\"Generate reconstructed spirograms while varying each RSPINCs coordinate.\n","\n"," Args:\n"," row: A row of the SPINCs DF from which we'll get the values of manual\n"," features.\n"," vae_model: The VAE model to be used to reconstruct spirograms.\n"," latent_dim: The latent dimension.\n"," fpath_noext: The path to the output image file without extension.\n"," dpi: DPI of the image.\n"," \"\"\"\n"," cmap = plt.get_cmap('viridis')\n"," num_injected_features = 5\n"," radius = 1.5\n"," single_encodings = np.linspace(-radius, radius, num=21)\n"," decoder = vae_model.get_layer(f'{vae_model.name}_decoder')\n"," colorbar_width = 0.2\n","\n"," rescaled_volume = np.arange(1000) * VOLUME_SCALE_FACTOR\n"," _, axs = plt.subplots(\n"," 1,\n"," latent_dim + 1,\n"," figsize=(4 * latent_dim + colorbar_width, 3),\n"," width_ratios=[4] * latent_dim + [colorbar_width],\n"," )\n","\n"," for latent_idx in range(latent_dim):\n"," ax = axs[latent_idx]\n"," for img_idx, single_encoding in enumerate(single_encodings):\n"," # This value should be in [0, 1].\n"," color_val = single_encoding / (radius * 2) + 0.5\n"," encoding = np.zeros(latent_dim)\n"," encoding[latent_idx] = single_encoding\n"," encoding_input = np.expand_dims(encoding, axis=0)\n"," edf_input = np.expand_dims(np.array(EDF_VALUE_EXAMPLE), axis=0)\n"," vae_input = np.concatenate((encoding_input, edf_input), axis=-1)\n"," assert vae_input.shape == (1, latent_dim + num_injected_features)\n"," reconstructed = decoder(vae_input)[0].numpy()[:, 0]\n"," assert len(rescaled_volume) == len(reconstructed)\n"," ax.plot(\n"," rescaled_volume,\n"," reconstructed,\n"," color=cmap(color_val),\n"," alpha=0.9,\n"," linewidth=0.8,\n"," )\n"," ax.set_xlim((-20 * VOLUME_SCALE_FACTOR, 350 * VOLUME_SCALE_FACTOR))\n"," ax.set_ylim((-0.1, 4.2))\n"," ax.set_xlabel('Volume (L)')\n"," # Custom annotation for RSPINCs with dim = 2:\n"," if latent_idx == 0:\n"," ax.set_ylabel('Flow (L/s)')\n"," _draw_double_arrow(\n"," ax, 50 * VOLUME_SCALE_FACTOR, 140 * VOLUME_SCALE_FACTOR, 3\n"," )\n"," elif latent_idx == 1:\n"," _draw_double_arrow(\n"," ax, 5 * VOLUME_SCALE_FACTOR, 40 * VOLUME_SCALE_FACTOR, 3\n"," )\n"," ax.set_title('$\\mathrm{RSPINC}_' + f'{latent_idx + 1}$')\n"," # Draw a color palette on the last axis.\n"," cbar = plt.colorbar(\n"," mpl.cm.ScalarMappable(\n"," norm=mpl.colors.Normalize(vmin=-radius, vmax=radius), cmap=cmap\n"," ),\n"," cax=axs[-1],\n"," )\n"," cbar.ax.set_xlabel('Coordinate\\nValue')\n"," plt.tight_layout()\n"," plt.show()"]},{"cell_type":"markdown","metadata":{"id":"ols2RVM8c1sh"},"source":["# Load model and generate spirograms from embedding coordinate perturbation"]},{"cell_type":"code","execution_count":null,"metadata":{"id":"BX0g763-ZrLr"},"outputs":[],"source":["rspincs_model = tf.keras.models.load_model('rspincs')"]},{"cell_type":"code","execution_count":null,"metadata":{"id":"_2nYHVXhr6uT","colab":{"base_uri":"https://localhost:8080/","height":307},"executionInfo":{"status":"ok","timestamp":1717789873484,"user_tz":240,"elapsed":2232,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}},"outputId":"ff216c3b-79e7-4a39-9438-8035534ea568"},"outputs":[{"output_type":"display_data","data":{"text/plain":["
"],"image/png":"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rxf/+te/Gvwc/la99M477whN08S6devExo0bG2yHogewWfeoRKNRlixZwogRI3bvU1WVESNGsHDhwt88Z+HChQ2OBxg5cuQej2/K9qf8sGtF0VatWpGfn8/o0aNZvXr1oYjbJDSn7/+B6N27Nzk5OZx44onMnz8/0XEazX8Wo0pNTd3jMfJnYN8c6fUsyLp2fzS3n4H9JevapvkzkJqaysiRI3nxxRd/s1eopqaGLl26UFRURFFR0e79a9asoaamhq5duwLQpUuXX31f58+fv/vre9KlSxcWLVrUYN9/BvDvryVLlmBZFk8++SRHH300HTt2ZOfOnQ2OsdlsmKbZYF+fPn0wTZOysjLat2/fYMvOzj6gTHujWTdUKioqME2TrKysBvuzsrIoKSn5zXNKSkr26fimbH/K36lTJ1599VU++eQT3njjDSzLYvDgwWzfvv1QRE64PX3//X4/oVAoQakOnZycHF566SU+/PBDPvzwQ/Lz8xk+fDhLly5NdLQDZlkWEyZMYMiQIXTv3n2PxzWnOuBQONLrWZB17f6Qda2sa5t6PfDiiy9imiYDBgzgww8/ZOPGjaxdu5bnnnuOQYMGMWLECHr06MHFF1/M0qVLWbx4MWPHjuXYY4/d/SrXrbfeymuvvcbkyZPZuHEjTz31FNOmTds9SH1PbrzxRl599VWmTJnChg0buPfeew/4Dxnt27cnFovx/PPPs3nzZv7973/z0ksvNTimdevW1NfXM2vWLCoqKggGg3Ts2JGLL76YsWPHMm3aNLZs2cLixYuZNGkSX3zxxQFl2htyHRWpgUGDBjV4D3Lw4MF06dKFl19+mQceeCCByaRDoVOnTnTq1Gn358GDB1NQUMDTTz/9qykVDzfXXnstq1atYt68eYmOIkmyrj3Cybq26Wvbti1Lly7loYce4uabb6a4uJiMjAz69evH5MmTURSFTz75hOuvv55hw4ahqiqjRo3i+eef332NMWPG8Oyzz/LEE09w44030qZNG6ZMmcLw4cN/997nn38+BQUF3HbbbYTDYc4++2yuvvrqAxrD1atXL5566ikeffRR7rzzToYNG8akSZMYO3bs7mMGDx7MVVddxfnnn09lZSX33nsv9913H1OmTOHBBx/k5ptvZseOHaSnp3P00Udz2mmn7XeevdWsGyrp6elomvarOa1LS0v32F2VnZ29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\n"},"metadata":{}}],"source":["generate_rspincs_reconstruction_plot(\n"," vae_model=rspincs_model,\n"," latent_dim=2,\n",")"]}]} \ No newline at end of file diff --git a/regle/analysis/pca_and_spline_fitting.ipynb b/regle/analysis/pca_and_spline_fitting.ipynb index ce1e0a6..a1454e2 100644 --- a/regle/analysis/pca_and_spline_fitting.ipynb +++ b/regle/analysis/pca_and_spline_fitting.ipynb @@ -1 +1 @@ -{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"provenance":[],"authorship_tag":"ABX9TyOWuQ668bwnB28rOF2BEzg+"},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"code","source":["#@title Licensed under the BSD-3 License (the \"License\"); { display-mode: \"form\" }\n","# Copyright 2021 Google LLC.\n","#\n","# Redistribution and use in source and binary forms, with or without modification,\n","# are permitted provided that the following conditions are met:\n","#\n","# 1. Redistributions of source code must retain the above copyright notice, this\n","# list of conditions and the following disclaimer.\n","#\n","# 2. Redistributions in binary form must reproduce the above copyright notice,\n","# this list of conditions and the following disclaimer in the documentation\n","# and/or other materials provided with the distribution.\n","#\n","# 3. Neither the name of the copyright holder nor the names of its contributors\n","# may be used to endorse or promote products derived from this software without\n","# specific prior written permission.\n","#\n","# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n","# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n","# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n","# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\n","# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n","# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n","# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\n","# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n","# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n","# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."],"metadata":{"id":"SqQ7C3xXPfn7","executionInfo":{"status":"ok","timestamp":1717789955106,"user_tz":240,"elapsed":18,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"execution_count":1,"outputs":[]},{"cell_type":"code","execution_count":2,"metadata":{"id":"pa_dhHReC5dH","executionInfo":{"status":"ok","timestamp":1717789958175,"user_tz":240,"elapsed":3082,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["import numpy as np\n","import pandas as pd\n","import scipy\n","from sklearn import decomposition"]},{"cell_type":"markdown","metadata":{"id":"BVm0PPlJHCjX"},"source":["# PCA"]},{"cell_type":"markdown","metadata":{"id":"XsedyAXiHgDM"},"source":["For PCA we require population-level data. We assume `data_matrix` is a Pandas dataframe whose rows correspond to individuals and columns correspond to data points. We simulate this data in this notebook as we don't have access to the real population-level data."]},{"cell_type":"code","execution_count":3,"metadata":{"id":"eJFBpnleHBqS","executionInfo":{"status":"ok","timestamp":1717789959746,"user_tz":240,"elapsed":1574,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["np.random.seed(42)\n","data_matrix = pd.DataFrame(np.random.normal(size=(10000, 1000)))"]},{"cell_type":"code","execution_count":4,"metadata":{"id":"AFbcJIqiHyg7","executionInfo":{"status":"ok","timestamp":1717789959747,"user_tz":240,"elapsed":5,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["def standardize_df(df: pd.DataFrame) -> pd.DataFrame:\n"," \"\"\"Standardizes a dataframe (mean=0, var=1).\"\"\"\n"," return (df - df.mean()) / df.std(ddof=0)\n","\n","\n","def generate_pc(\n"," data_matrix: pd.DataFrame, num_pc: int, standardize: bool = True\n",") -> pd.DataFrame:\n"," \"\"\"Generates principal components (PCs) of the given data matrix.\n","\n"," Args:\n"," data_matrix: The data matrix.\n"," num_pc: The number of PCs to compute.\n"," standardize: True to standardize the data matrix before computing PCs.\n","\n"," Returns:\n"," A matrix of PCs of the data matrix.\n"," \"\"\"\n"," original_shape = data_matrix.shape\n"," if standardize:\n"," data_matrix = standardize_df(data_matrix)\n"," # Replace NaN values with 0 (this can happen when some col has var=0).\n"," data_matrix.fillna(0, inplace=True)\n"," assert data_matrix.shape == original_shape\n"," pca = decomposition.PCA(num_pc)\n"," pc_np = pca.fit_transform(data_matrix)\n"," print('PCA explained variance:', pca.explained_variance_)\n"," print(\n"," 'PCA explained variance (proportion):',\n"," pca.explained_variance_ / np.sum(pca.explained_variance_),\n"," )\n"," assert pc_np.shape == (original_shape[0], num_pc)\n"," return pd.DataFrame(pc_np)"]},{"cell_type":"code","execution_count":5,"metadata":{"colab":{"height":241,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":3135,"status":"ok","timestamp":1717789962878,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"zlBLtbM4IQ53","outputId":"67fb9819-43f5-4182-bf21-e6e84b84d2a4"},"outputs":[{"output_type":"stream","name":"stdout","text":["PCA explained variance: [1.63972209 1.63070323 1.62260396 1.61134043 1.590792 ]\n","PCA explained variance (proportion): [0.20255582 0.20144171 0.2004412 0.19904981 0.19651145]\n"]},{"output_type":"execute_result","data":{"text/plain":[" 0 1 2 3 4\n","0 -2.371899 -0.643403 -0.397528 0.505243 -1.672120\n","1 -0.389563 -0.316097 -0.054947 -1.539366 -0.998421\n","2 -0.278895 -1.904815 0.019068 -0.700896 0.973568\n","3 3.261174 -0.036879 2.362755 -1.733982 0.587677\n","4 0.172324 0.537071 -0.351281 -1.236673 1.708548"],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
01234
0-2.371899-0.643403-0.3975280.505243-1.672120
1-0.389563-0.316097-0.054947-1.539366-0.998421
2-0.278895-1.9048150.019068-0.7008960.973568
33.261174-0.0368792.362755-1.7339820.587677
40.1723240.537071-0.351281-1.2366731.708548
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n"," \n","\n","\n","\n"," \n","
\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","variable_name":"pc_dataframe","summary":"{\n \"name\": \"pc_dataframe\",\n \"rows\": 10000,\n \"fields\": [\n {\n \"column\": 0,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2805163386985488,\n \"min\": -4.643045981269673,\n \"max\": 5.017698894439442,\n \"num_unique_values\": 10000,\n \"samples\": [\n -0.3224716522127656,\n 0.6031338243822927,\n -1.2993299471423263\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 1,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2769899114607324,\n \"min\": -4.448045764841815,\n \"max\": 5.101647474079014,\n \"num_unique_values\": 10000,\n \"samples\": [\n 0.286864855151227,\n -0.6597526194886669,\n -0.4896683064067677\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 2,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.273814728228805,\n \"min\": -4.328973725102052,\n \"max\": 4.872664420026113,\n \"num_unique_values\": 10000,\n \"samples\": [\n -0.6794583220950966,\n 1.9140526678288383,\n -0.4004464395670121\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 3,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2693858467445038,\n \"min\": -4.939769834236929,\n \"max\": 4.99450956625324,\n \"num_unique_values\": 10000,\n \"samples\": [\n 2.225402267644631,\n -0.9588695150842595,\n 1.2924768168268101\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 4,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2612660288538398,\n \"min\": -5.007116466188265,\n \"max\": 5.3472410625736035,\n \"num_unique_values\": 10000,\n \"samples\": [\n 0.19752305167345738,\n -1.0272444388147874,\n -0.010101932326369557\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":5}],"source":["pc_dataframe = generate_pc(\n"," data_matrix,\n"," num_pc=5)\n","\n","pc_dataframe.head()"]},{"cell_type":"markdown","metadata":{"id":"j9tneSsvG5vg"},"source":["# Spline fitting"]},{"cell_type":"code","execution_count":6,"metadata":{"id":"dALiJbUGDghc","executionInfo":{"status":"ok","timestamp":1717789962878,"user_tz":240,"elapsed":26,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["def compute_spline_coefficients(\n"," arr: np.ndarray, knot_position: int\n",") -> np.ndarray:\n"," \"\"\"Gets cubic spline coefficients with a single knot.\n","\n"," We use a single knot which is padded by 4 (= k + 1) boundaries on each side,\n"," where k=3 (cubic) is the degree in this case.\n","\n"," The results are 5 coefficients padded by 4 zeros at the end. We remove the\n"," last 4 zeros.\n","\n"," For more details, see https://en.wikipedia.org/wiki/B-spline and\n"," https://docs.scipy.org/doc/scipy/tutorial/interpolate/smoothing_splines.html#procedural-splrep\n","\n"," Args:\n"," arr: The target numpy array for 1D spline fitting.\n"," knot_position: The position of the single knot.\n","\n"," Returns:\n"," A numpy array of 5 cubic spline coefficients.\n"," \"\"\"\n"," num_points = len(arr)\n"," assert arr.shape == (num_points,)\n"," assert 0 < knot_position < num_points - 1\n"," spline = scipy.interpolate.splrep(\n"," x=np.arange(num_points),\n"," y=arr,\n"," k=3,\n"," task=-1,\n"," t=[knot_position],\n"," )\n"," bspline_coefficients = spline[1]\n"," assert np.array_equal(bspline_coefficients[5:], np.array([0, 0, 0, 0]))\n"," return bspline_coefficients[:5]"]},{"cell_type":"code","execution_count":7,"metadata":{"id":"JPYKbetRCGs5","executionInfo":{"status":"ok","timestamp":1717789962879,"user_tz":240,"elapsed":24,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["MAX_NUM_POINTS = 1000\n","VOLUME_SCALE_FACTOR = 0.001\n","KNOT_POSITION = 199"]},{"cell_type":"markdown","metadata":{"id":"l7XaODNrEXgU"},"source":["`example_curve` variable below should be a 1D numpy array that contains a single curve, such as a spirogram.\n","\n","Here we use an example curve copied from a UK Biobank example at https://biobank.ctsu.ox.ac.uk/crystal/ukb/examples/eg_spiro_3066.dat"]},{"cell_type":"code","execution_count":8,"metadata":{"id":"Dur9LHMQD_B3","executionInfo":{"status":"ok","timestamp":1717789962879,"user_tz":240,"elapsed":22,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["example_curve_txt = '0,0,0,0,3,10,25,54,101,169,258,363,478,589,689,785,879,970,1059,1147,1234,1320,1403,1486,1569,1650,1730,1809,1888,1965,2040,2116,2188,2261,2331,2400,2465,2532,2595,2658,2720,2780,2838,2894,2948,3001,3052,3102,3151,3197,3243,3287,3329,3371,3412,3451,3490,3527,3564,3600,3635,3670,3703,3736,3769,3800,3831,3861,3890,3918,3947,3974,4001,4028,4054,4080,4105,4130,4154,4179,4202,4226,4249,4271,4292,4312,4332,4351,4371,4390,4408,4426,4444,4461,4478,4495,4512,4528,4544,4560,4575,4590,4604,4619,4633,4647,4661,4675,4689,4703,4716,4729,4742,4755,4767,4779,4791,4802,4812,4822,4831,4840,4849,4857,4866,4874,4882,4890,4898,4906,4914,4921,4929,4936,4944,4951,4958,4966,4973,4980,4987,4994,5000,5007,5013,5020,5026,5033,5039,5045,5051,5057,5063,5069,5075,5081,5087,5092,5098,5104,5109,5114,5119,5125,5130,5134,5139,5144,5148,5153,5157,5161,5166,5170,5174,5178,5182,5186,5190,5194,5198,5202,5205,5209,5213,5216,5220,5223,5226,5230,5233,5236,5240,5243,5246,5250,5253,5256,5259,5262,5264,5267,5270,5273,5276,5279,5283,5286,5289,5292,5295,5298,5300,5303,5306,5308,5311,5314,5316,5319,5321,5323,5326,5328,5331,5333,5335,5338,5340,5343,5345,5348,5350,5352,5355,5357,5360,5362,5365,5367,5369,5372,5374,5377,5379,5381,5384,5386,5388,5390,5391,5393,5395,5397,5399,5401,5403,5404,5406,5408,5410,5412,5413,5415,5417,5419,5420,5422,5424,5426,5427,5429,5431,5432,5434,5436,5438,5439,5441,5443,5444,5446,5447,5449,5450,5452,5453,5455,5456,5457,5459,5460,5461,5462,5463,5464,5466,5467,5468,5470,5471,5473,5474,5476,5477,5478,5480,5481,5482,5484,5485,5486,5487,5489,5490,5491,5492,5493,5494,5496,5497,5498,5499,5500,5501,5502,5503,5504,5505,5506,5507,5508,5509,5510,5510,5511,5512,5513,5514,5515,5515,5516,5517,5519,5520,5521,5523,5524,5525,5527,5529,5530,5532,5533,5535,5536,5537,5539,5540,5541,5543,5544,5545,5545,5546,5547,5548,5549,5549,5550,5551,5552,5552,5553,5554,5554,5555,5556,5557,5557,5558,5559,5560,5560,5561,5562,5562,5563,5564,5564,5565,5565,5566,5567,5567,5568,5569,5570,5571,5572,5573,5574,5576,5577,5578,5579,5580,5582,5583,5584,5585,5587,5588,5589,5590,5591,5591,5592,5593,5594,5595,5596,5596,5597,5598,5598,5599,5600,5601,5601,5602,5603,5603,5604,5605,5606,5606,5607,5608,5608,5609,5609,5609,5610,5611,5611,5612,5613,5613,5614,5615,5616,5616,5617,5618,5618,5619,5620,5621,5622,5623,5624,5624,5625,5626,5626,5627,5628,5628,5629,5629,5630,5630,5631,5632,5632,5633,5633,5634,5635,5635,5636,5637,5637,5638,5639,5639,5640,5641,5642,5642,5643,5644,5645,5645,5646,5647,5647,5648,5649,5649,5650,5651,5651,5652,5652,5653,5654,5654,5655,5656,5656,5657,5658,5658,5659,5660,5660,5661,5661,5662,5663,5663,5664,5664,5665,5665,5666,5666,5667,5667,5668,5668,5669,5669,5670,5670,5670,5671,5671,5672,5672,5672,5673,5673,5673,5673,5674,5674,5674,5675,5676,5676,5677,5677,5678,5678,5679,5679,5680,5681,5681,5682,5683,5683,5684,5684,5685,5686,5686,5687,5687,5688,5688,5688,5689,5689,5690,5690,5690,5691,5691,5692,5692,5692,5693,5693,5694,5694,5694,5695,5695,5695,5696,5696,5696,5696,5696,5696,5697,5697,5698,5698,5698,5699,5699,5699,5699,5700,5700,5700,5701,5701,5702,5702,5703,5703,5704,5704,5705,5705,5706,5706,5707,5707,5708,5709,5709,5710,5710,5711,5711,5712,5712,5712,5713,5713,5713,5714,5714,5714,5715,5715,5716,5716,5716,5717,5717,5717,5718,5718,5719,5719,5720,5720,5721,5721,5721,5722,5722,5722,5723,5723,5723,5723,5724,5724,5724,5725,5725,5725,5726,5726,5726,5727,5727,5728,5728,5729,5729,5729,5730,5730,5731,5732,5732,5733,5733,5734,5735,5735,5735,5736,5736,5736,5737,5737,5737,5738,5738,5738,5739,5739,5739,5739,5740,5740,5740,5741,5741,5741,5741,5741,5741,5742,5742,5742,5742,5742,5742,5742,5742,5742,5742,5741,5741,5740,5740,5740,5740,5739,5739,5739,5739,5739,5739,5740,5740,5740,5741,5742,5742,5743,5743,5744,5745,5745,5745,5746,5746,5747,5747,5748,5748,5748,5748,5748,5748,5749,5749,5749,5749,5749,5749,5749,5750,5750,5750,5750,5750,5751,5751,5751,5752,5752,5753,5753,5754,5754,5754,5755,5755,5756,5756,5756,5757,5757,5757,5758,5758,5758,5758,5759,5759,5759,5759,5759,5759,5759,5759,5759,5760,5760,5760,5761,5761,5761,5762,5762,5763,5763,5763,5764,5764,5764,5765,5765,5766,5766,5766,5767,5767,5767,5767,5767,5768,5768,5768,5768,5769,5769,5769,5770,5770,5770,5770,5770,5771,5771,5771,5771,5771,5772,5772,5772,5773,5773,5773,5774,5774,5774,5775,5775,5775,5776,5776,5777,5777,5777,5778,5778,5778,5778,5779,5779,5779,5779,5779,5779,5779,5779,5779,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5780,5780,5780,5780,5781,5781,5781,5782,5782,5782,5783,5783,5783,5784,5784,5784,5785,5785,5785,5785,5785,5786,5786,5786,5786,5786,5786,5786,5787,5787,5787,5788,5788,5788,5789,5789,5789,5790,5790,5790,5791,5791,5792,5792,5792,5793,5793,5793,5794,5794,5795,5795,5795,5796,5796,5796,5797,5797,5798,5798,5798,5798,5798,5799,5799,5799,5799,5800,5800,5800,5801,5801,5801,5801,5802,5802,5802,5802,5803,5803,5803,5803,5803,5803,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5803,5804,5804,5804,5804,5804,5805,5805,5805,5805,5806,5806,5806,5806,5806,5806,5806,5806,5806,5806,5807,5807,5807,5807,5808,5808,5809,5809,5809,5810,5810,5810,5811,5811,5812,5812,5813,5813,5813,5814,5814,5815,5815,5815,5815,5816,5816,5816,5816,5817,5817,5817,5817,5817,5817,5817,5818,5818,5818,5818,5818,5818,5818,5819,5819,5819,5819,5819,5819,5819,5819,5819,5819,5820,5820,5820,5820,5820,5820,5820,5820,5820,5819,5820,5820,5820,5820,5820,5820,5820,5820,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5820,5820,5820,5819,5819,5818,5818,5818,5817,5817,5817,5816,5816,5816,5816,5815,5815,5815,5816,5816,5816,5817,5817,5818,5819,5819,5820,5821,5822,5823,5823,5824,5825,5826,5827,5827,5828,5828,5829,5829,5829,5830,5830,5831,5831,5831,5831,5831,5832,5831,5832,5832,5832,5832,5832,5832,5832,5833,5833,5833,5833,5833,5833,5833,5834,5834,5834,5834,5834,5835,5835,5835,5835,5835,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5835,5835,5835,5835,5834,5834,5834,5834,5833,5833,5833,5833,5833,5832,5832,5832,5832,5832,5832,5832,5832,5831'\n","example_curve = (\n"," np.array(example_curve_txt.split(',')[:MAX_NUM_POINTS], dtype=np.float32)\n"," * VOLUME_SCALE_FACTOR\n",")"]},{"cell_type":"markdown","metadata":{"id":"YHiRGraVEhBf"},"source":["The following code generates the 5 spline coefficients the this curve."]},{"cell_type":"code","execution_count":9,"metadata":{"executionInfo":{"elapsed":278,"status":"ok","timestamp":1717789963136,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"Emoh7tdNCQPv","outputId":"e8bafe1b-4a0f-460c-8a7c-438a21fdfa69","colab":{"base_uri":"https://localhost:8080/"}},"outputs":[{"output_type":"stream","name":"stdout","text":["[-0.08101105 5.14773236 5.63775992 5.81692895 5.78074777]\n"]}],"source":["print(\n"," compute_spline_coefficients(arr=example_curve, knot_position=KNOT_POSITION)\n",")"]}]} \ No newline at end of file +{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"provenance":[],"authorship_tag":"ABX9TyNTvrOkUvVBUc5VPWmGwmFh"},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"code","source":["#@title Licensed under the BSD-3 License (the \"License\"); { display-mode: \"form\" }\n","# Copyright 2023 Google LLC.\n","#\n","# Redistribution and use in source and binary forms, with or without modification,\n","# are permitted provided that the following conditions are met:\n","#\n","# 1. Redistributions of source code must retain the above copyright notice, this\n","# list of conditions and the following disclaimer.\n","#\n","# 2. Redistributions in binary form must reproduce the above copyright notice,\n","# this list of conditions and the following disclaimer in the documentation\n","# and/or other materials provided with the distribution.\n","#\n","# 3. Neither the name of the copyright holder nor the names of its contributors\n","# may be used to endorse or promote products derived from this software without\n","# specific prior written permission.\n","#\n","# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n","# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n","# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n","# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\n","# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n","# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n","# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\n","# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n","# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n","# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."],"metadata":{"id":"SqQ7C3xXPfn7"},"execution_count":null,"outputs":[]},{"cell_type":"code","execution_count":null,"metadata":{"id":"pa_dhHReC5dH"},"outputs":[],"source":["import numpy as np\n","import pandas as pd\n","import scipy\n","from sklearn import decomposition"]},{"cell_type":"markdown","metadata":{"id":"BVm0PPlJHCjX"},"source":["# PCA"]},{"cell_type":"markdown","metadata":{"id":"XsedyAXiHgDM"},"source":["For PCA we require population-level data. We assume `data_matrix` is a Pandas dataframe whose rows correspond to individuals and columns correspond to data points. We simulate this data in this notebook as we don't have access to the real population-level data."]},{"cell_type":"code","execution_count":null,"metadata":{"id":"eJFBpnleHBqS"},"outputs":[],"source":["np.random.seed(42)\n","data_matrix = pd.DataFrame(np.random.normal(size=(10000, 1000)))"]},{"cell_type":"code","execution_count":null,"metadata":{"id":"AFbcJIqiHyg7"},"outputs":[],"source":["def standardize_df(df: pd.DataFrame) -> pd.DataFrame:\n"," \"\"\"Standardizes a dataframe (mean=0, var=1).\"\"\"\n"," return (df - df.mean()) / df.std(ddof=0)\n","\n","\n","def generate_pc(\n"," data_matrix: pd.DataFrame, num_pc: int, standardize: bool = True\n",") -> pd.DataFrame:\n"," \"\"\"Generates principal components (PCs) of the given data matrix.\n","\n"," Args:\n"," data_matrix: The data matrix.\n"," num_pc: The number of PCs to compute.\n"," standardize: True to standardize the data matrix before computing PCs.\n","\n"," Returns:\n"," A matrix of PCs of the data matrix.\n"," \"\"\"\n"," original_shape = data_matrix.shape\n"," if standardize:\n"," data_matrix = standardize_df(data_matrix)\n"," # Replace NaN values with 0 (this can happen when some col has var=0).\n"," data_matrix.fillna(0, inplace=True)\n"," assert data_matrix.shape == original_shape\n"," pca = decomposition.PCA(num_pc)\n"," pc_np = pca.fit_transform(data_matrix)\n"," print('PCA explained variance:', pca.explained_variance_)\n"," print(\n"," 'PCA explained variance (proportion):',\n"," pca.explained_variance_ / np.sum(pca.explained_variance_),\n"," )\n"," assert pc_np.shape == (original_shape[0], num_pc)\n"," return pd.DataFrame(pc_np)"]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"height":241,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":3135,"status":"ok","timestamp":1717789962878,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"zlBLtbM4IQ53","outputId":"67fb9819-43f5-4182-bf21-e6e84b84d2a4"},"outputs":[{"output_type":"stream","name":"stdout","text":["PCA explained variance: [1.63972209 1.63070323 1.62260396 1.61134043 1.590792 ]\n","PCA explained variance (proportion): [0.20255582 0.20144171 0.2004412 0.19904981 0.19651145]\n"]},{"output_type":"execute_result","data":{"text/plain":[" 0 1 2 3 4\n","0 -2.371899 -0.643403 -0.397528 0.505243 -1.672120\n","1 -0.389563 -0.316097 -0.054947 -1.539366 -0.998421\n","2 -0.278895 -1.904815 0.019068 -0.700896 0.973568\n","3 3.261174 -0.036879 2.362755 -1.733982 0.587677\n","4 0.172324 0.537071 -0.351281 -1.236673 1.708548"],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
01234
0-2.371899-0.643403-0.3975280.505243-1.672120
1-0.389563-0.316097-0.054947-1.539366-0.998421
2-0.278895-1.9048150.019068-0.7008960.973568
33.261174-0.0368792.362755-1.7339820.587677
40.1723240.537071-0.351281-1.2366731.708548
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n"," \n","\n","\n","\n"," \n","
\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","variable_name":"pc_dataframe","summary":"{\n \"name\": \"pc_dataframe\",\n \"rows\": 10000,\n \"fields\": [\n {\n \"column\": 0,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2805163386985488,\n \"min\": -4.643045981269673,\n \"max\": 5.017698894439442,\n \"num_unique_values\": 10000,\n \"samples\": [\n -0.3224716522127656,\n 0.6031338243822927,\n -1.2993299471423263\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 1,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2769899114607324,\n \"min\": -4.448045764841815,\n \"max\": 5.101647474079014,\n \"num_unique_values\": 10000,\n \"samples\": [\n 0.286864855151227,\n -0.6597526194886669,\n -0.4896683064067677\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 2,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.273814728228805,\n \"min\": -4.328973725102052,\n \"max\": 4.872664420026113,\n \"num_unique_values\": 10000,\n \"samples\": [\n -0.6794583220950966,\n 1.9140526678288383,\n -0.4004464395670121\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 3,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2693858467445038,\n \"min\": -4.939769834236929,\n \"max\": 4.99450956625324,\n \"num_unique_values\": 10000,\n \"samples\": [\n 2.225402267644631,\n -0.9588695150842595,\n 1.2924768168268101\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": 4,\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 1.2612660288538398,\n \"min\": -5.007116466188265,\n \"max\": 5.3472410625736035,\n \"num_unique_values\": 10000,\n \"samples\": [\n 0.19752305167345738,\n -1.0272444388147874,\n -0.010101932326369557\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":5}],"source":["pc_dataframe = generate_pc(\n"," data_matrix,\n"," num_pc=5)\n","\n","pc_dataframe.head()"]},{"cell_type":"markdown","metadata":{"id":"j9tneSsvG5vg"},"source":["# Spline fitting"]},{"cell_type":"code","execution_count":null,"metadata":{"id":"dALiJbUGDghc"},"outputs":[],"source":["def compute_spline_coefficients(\n"," arr: np.ndarray, knot_position: int\n",") -> np.ndarray:\n"," \"\"\"Gets cubic spline coefficients with a single knot.\n","\n"," We use a single knot which is padded by 4 (= k + 1) boundaries on each side,\n"," where k=3 (cubic) is the degree in this case.\n","\n"," The results are 5 coefficients padded by 4 zeros at the end. We remove the\n"," last 4 zeros.\n","\n"," For more details, see https://en.wikipedia.org/wiki/B-spline and\n"," https://docs.scipy.org/doc/scipy/tutorial/interpolate/smoothing_splines.html#procedural-splrep\n","\n"," Args:\n"," arr: The target numpy array for 1D spline fitting.\n"," knot_position: The position of the single knot.\n","\n"," Returns:\n"," A numpy array of 5 cubic spline coefficients.\n"," \"\"\"\n"," num_points = len(arr)\n"," assert arr.shape == (num_points,)\n"," assert 0 < knot_position < num_points - 1\n"," spline = scipy.interpolate.splrep(\n"," x=np.arange(num_points),\n"," y=arr,\n"," k=3,\n"," task=-1,\n"," t=[knot_position],\n"," )\n"," bspline_coefficients = spline[1]\n"," assert np.array_equal(bspline_coefficients[5:], np.array([0, 0, 0, 0]))\n"," return bspline_coefficients[:5]"]},{"cell_type":"code","execution_count":null,"metadata":{"id":"JPYKbetRCGs5"},"outputs":[],"source":["MAX_NUM_POINTS = 1000\n","VOLUME_SCALE_FACTOR = 0.001\n","KNOT_POSITION = 199"]},{"cell_type":"markdown","metadata":{"id":"l7XaODNrEXgU"},"source":["`example_curve` variable below should be a 1D numpy array that contains a single curve, such as a spirogram.\n","\n","Here we use an example curve copied from a UK Biobank example at https://biobank.ctsu.ox.ac.uk/crystal/ukb/examples/eg_spiro_3066.dat"]},{"cell_type":"code","execution_count":null,"metadata":{"id":"Dur9LHMQD_B3"},"outputs":[],"source":["example_curve_txt = '0,0,0,0,3,10,25,54,101,169,258,363,478,589,689,785,879,970,1059,1147,1234,1320,1403,1486,1569,1650,1730,1809,1888,1965,2040,2116,2188,2261,2331,2400,2465,2532,2595,2658,2720,2780,2838,2894,2948,3001,3052,3102,3151,3197,3243,3287,3329,3371,3412,3451,3490,3527,3564,3600,3635,3670,3703,3736,3769,3800,3831,3861,3890,3918,3947,3974,4001,4028,4054,4080,4105,4130,4154,4179,4202,4226,4249,4271,4292,4312,4332,4351,4371,4390,4408,4426,4444,4461,4478,4495,4512,4528,4544,4560,4575,4590,4604,4619,4633,4647,4661,4675,4689,4703,4716,4729,4742,4755,4767,4779,4791,4802,4812,4822,4831,4840,4849,4857,4866,4874,4882,4890,4898,4906,4914,4921,4929,4936,4944,4951,4958,4966,4973,4980,4987,4994,5000,5007,5013,5020,5026,5033,5039,5045,5051,5057,5063,5069,5075,5081,5087,5092,5098,5104,5109,5114,5119,5125,5130,5134,5139,5144,5148,5153,5157,5161,5166,5170,5174,5178,5182,5186,5190,5194,5198,5202,5205,5209,5213,5216,5220,5223,5226,5230,5233,5236,5240,5243,5246,5250,5253,5256,5259,5262,5264,5267,5270,5273,5276,5279,5283,5286,5289,5292,5295,5298,5300,5303,5306,5308,5311,5314,5316,5319,5321,5323,5326,5328,5331,5333,5335,5338,5340,5343,5345,5348,5350,5352,5355,5357,5360,5362,5365,5367,5369,5372,5374,5377,5379,5381,5384,5386,5388,5390,5391,5393,5395,5397,5399,5401,5403,5404,5406,5408,5410,5412,5413,5415,5417,5419,5420,5422,5424,5426,5427,5429,5431,5432,5434,5436,5438,5439,5441,5443,5444,5446,5447,5449,5450,5452,5453,5455,5456,5457,5459,5460,5461,5462,5463,5464,5466,5467,5468,5470,5471,5473,5474,5476,5477,5478,5480,5481,5482,5484,5485,5486,5487,5489,5490,5491,5492,5493,5494,5496,5497,5498,5499,5500,5501,5502,5503,5504,5505,5506,5507,5508,5509,5510,5510,5511,5512,5513,5514,5515,5515,5516,5517,5519,5520,5521,5523,5524,5525,5527,5529,5530,5532,5533,5535,5536,5537,5539,5540,5541,5543,5544,5545,5545,5546,5547,5548,5549,5549,5550,5551,5552,5552,5553,5554,5554,5555,5556,5557,5557,5558,5559,5560,5560,5561,5562,5562,5563,5564,5564,5565,5565,5566,5567,5567,5568,5569,5570,5571,5572,5573,5574,5576,5577,5578,5579,5580,5582,5583,5584,5585,5587,5588,5589,5590,5591,5591,5592,5593,5594,5595,5596,5596,5597,5598,5598,5599,5600,5601,5601,5602,5603,5603,5604,5605,5606,5606,5607,5608,5608,5609,5609,5609,5610,5611,5611,5612,5613,5613,5614,5615,5616,5616,5617,5618,5618,5619,5620,5621,5622,5623,5624,5624,5625,5626,5626,5627,5628,5628,5629,5629,5630,5630,5631,5632,5632,5633,5633,5634,5635,5635,5636,5637,5637,5638,5639,5639,5640,5641,5642,5642,5643,5644,5645,5645,5646,5647,5647,5648,5649,5649,5650,5651,5651,5652,5652,5653,5654,5654,5655,5656,5656,5657,5658,5658,5659,5660,5660,5661,5661,5662,5663,5663,5664,5664,5665,5665,5666,5666,5667,5667,5668,5668,5669,5669,5670,5670,5670,5671,5671,5672,5672,5672,5673,5673,5673,5673,5674,5674,5674,5675,5676,5676,5677,5677,5678,5678,5679,5679,5680,5681,5681,5682,5683,5683,5684,5684,5685,5686,5686,5687,5687,5688,5688,5688,5689,5689,5690,5690,5690,5691,5691,5692,5692,5692,5693,5693,5694,5694,5694,5695,5695,5695,5696,5696,5696,5696,5696,5696,5697,5697,5698,5698,5698,5699,5699,5699,5699,5700,5700,5700,5701,5701,5702,5702,5703,5703,5704,5704,5705,5705,5706,5706,5707,5707,5708,5709,5709,5710,5710,5711,5711,5712,5712,5712,5713,5713,5713,5714,5714,5714,5715,5715,5716,5716,5716,5717,5717,5717,5718,5718,5719,5719,5720,5720,5721,5721,5721,5722,5722,5722,5723,5723,5723,5723,5724,5724,5724,5725,5725,5725,5726,5726,5726,5727,5727,5728,5728,5729,5729,5729,5730,5730,5731,5732,5732,5733,5733,5734,5735,5735,5735,5736,5736,5736,5737,5737,5737,5738,5738,5738,5739,5739,5739,5739,5740,5740,5740,5741,5741,5741,5741,5741,5741,5742,5742,5742,5742,5742,5742,5742,5742,5742,5742,5741,5741,5740,5740,5740,5740,5739,5739,5739,5739,5739,5739,5740,5740,5740,5741,5742,5742,5743,5743,5744,5745,5745,5745,5746,5746,5747,5747,5748,5748,5748,5748,5748,5748,5749,5749,5749,5749,5749,5749,5749,5750,5750,5750,5750,5750,5751,5751,5751,5752,5752,5753,5753,5754,5754,5754,5755,5755,5756,5756,5756,5757,5757,5757,5758,5758,5758,5758,5759,5759,5759,5759,5759,5759,5759,5759,5759,5760,5760,5760,5761,5761,5761,5762,5762,5763,5763,5763,5764,5764,5764,5765,5765,5766,5766,5766,5767,5767,5767,5767,5767,5768,5768,5768,5768,5769,5769,5769,5770,5770,5770,5770,5770,5771,5771,5771,5771,5771,5772,5772,5772,5773,5773,5773,5774,5774,5774,5775,5775,5775,5776,5776,5777,5777,5777,5778,5778,5778,5778,5779,5779,5779,5779,5779,5779,5779,5779,5779,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5780,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5779,5780,5780,5780,5780,5781,5781,5781,5782,5782,5782,5783,5783,5783,5784,5784,5784,5785,5785,5785,5785,5785,5786,5786,5786,5786,5786,5786,5786,5787,5787,5787,5788,5788,5788,5789,5789,5789,5790,5790,5790,5791,5791,5792,5792,5792,5793,5793,5793,5794,5794,5795,5795,5795,5796,5796,5796,5797,5797,5798,5798,5798,5798,5798,5799,5799,5799,5799,5800,5800,5800,5801,5801,5801,5801,5802,5802,5802,5802,5803,5803,5803,5803,5803,5803,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5804,5803,5804,5804,5804,5804,5804,5805,5805,5805,5805,5806,5806,5806,5806,5806,5806,5806,5806,5806,5806,5807,5807,5807,5807,5808,5808,5809,5809,5809,5810,5810,5810,5811,5811,5812,5812,5813,5813,5813,5814,5814,5815,5815,5815,5815,5816,5816,5816,5816,5817,5817,5817,5817,5817,5817,5817,5818,5818,5818,5818,5818,5818,5818,5819,5819,5819,5819,5819,5819,5819,5819,5819,5819,5820,5820,5820,5820,5820,5820,5820,5820,5820,5819,5820,5820,5820,5820,5820,5820,5820,5820,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5821,5820,5820,5820,5819,5819,5818,5818,5818,5817,5817,5817,5816,5816,5816,5816,5815,5815,5815,5816,5816,5816,5817,5817,5818,5819,5819,5820,5821,5822,5823,5823,5824,5825,5826,5827,5827,5828,5828,5829,5829,5829,5830,5830,5831,5831,5831,5831,5831,5832,5831,5832,5832,5832,5832,5832,5832,5832,5833,5833,5833,5833,5833,5833,5833,5834,5834,5834,5834,5834,5835,5835,5835,5835,5835,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5836,5835,5835,5835,5835,5834,5834,5834,5834,5833,5833,5833,5833,5833,5832,5832,5832,5832,5832,5832,5832,5832,5831'\n","example_curve = (\n"," np.array(example_curve_txt.split(',')[:MAX_NUM_POINTS], dtype=np.float32)\n"," * VOLUME_SCALE_FACTOR\n",")"]},{"cell_type":"markdown","metadata":{"id":"YHiRGraVEhBf"},"source":["The following code generates the 5 spline coefficients the this curve."]},{"cell_type":"code","execution_count":null,"metadata":{"executionInfo":{"elapsed":278,"status":"ok","timestamp":1717789963136,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"Emoh7tdNCQPv","outputId":"e8bafe1b-4a0f-460c-8a7c-438a21fdfa69","colab":{"base_uri":"https://localhost:8080/"}},"outputs":[{"output_type":"stream","name":"stdout","text":["[-0.08101105 5.14773236 5.63775992 5.81692895 5.78074777]\n"]}],"source":["print(\n"," compute_spline_coefficients(arr=example_curve, knot_position=KNOT_POSITION)\n",")"]}]} \ No newline at end of file diff --git a/regle/analysis/prs_analysis.ipynb b/regle/analysis/prs_analysis.ipynb index 2daa9dc..12fc4e0 100644 --- a/regle/analysis/prs_analysis.ipynb +++ b/regle/analysis/prs_analysis.ipynb @@ -1 +1 @@ -{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"provenance":[],"authorship_tag":"ABX9TyMn77gwTOffLNK/j6j2quKt"},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"code","source":["#@title Licensed under the BSD-3 License (the \"License\"); { display-mode: \"form\" }\n","# Copyright 2021 Google LLC.\n","#\n","# Redistribution and use in source and binary forms, with or without modification,\n","# are permitted provided that the following conditions are met:\n","#\n","# 1. Redistributions of source code must retain the above copyright notice, this\n","# list of conditions and the following disclaimer.\n","#\n","# 2. Redistributions in binary form must reproduce the above copyright notice,\n","# this list of conditions and the following disclaimer in the documentation\n","# and/or other materials provided with the distribution.\n","#\n","# 3. Neither the name of the copyright holder nor the names of its contributors\n","# may be used to endorse or promote products derived from this software without\n","# specific prior written permission.\n","#\n","# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n","# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n","# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n","# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\n","# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n","# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n","# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\n","# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n","# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n","# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."],"metadata":{"id":"vdFOGdpqPesl","executionInfo":{"status":"ok","timestamp":1717789979565,"user_tz":240,"elapsed":13,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"execution_count":1,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"VbyGa_IhXRgk"},"source":["# Preparation\n","\n","This section includes imports and functions."]},{"cell_type":"code","execution_count":2,"metadata":{"id":"otMyZHIW0Fqs","executionInfo":{"status":"ok","timestamp":1717789981803,"user_tz":240,"elapsed":2247,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["import dataclasses\n","from typing import Dict, List, Optional, Sequence, Union\n","\n","import abc\n","from typing import Callable\n","\n","import numpy as np\n","import pandas as pd\n","import scipy.stats\n","import sklearn\n","import sklearn.metrics\n","from sklearn import metrics"]},{"cell_type":"code","execution_count":3,"metadata":{"id":"J8pr2zMLzmDH","executionInfo":{"status":"ok","timestamp":1717789981804,"user_tz":240,"elapsed":6,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["# A function that computes a numeric outcome from label and prediction arrays.\n","BootstrappableFn = Callable[[np.ndarray, np.ndarray], float]\n","\n","# Constants denoting the expected case and control values for binary encodings.\n","BINARY_LABEL_CONTROL = 0\n","BINARY_LABEL_CASE = 1\n","\n","class Metric(abc.ABC):\n"," \"\"\"Represents a callable wrapper class for a named metric function.\n","\n"," Attributes:\n"," name: The metric's name.\n"," \"\"\"\n","\n"," def __init__(self, name: str, fn: BootstrappableFn) -> None:\n"," \"\"\"Initializes the metric.\n","\n"," Args:\n"," name: The metric's name.\n"," fn: A function that computes an outcome from label and prediction arrays.\n"," The function's signature should accept a `y_true` label array and a\n"," `y_pred` model prediction array. This function is invoked when the\n"," `Metric` instance is called.\n"," \"\"\"\n"," self._name: str = name\n"," self._fn: BootstrappableFn = fn\n","\n"," @property\n"," def name(self) -> str:\n"," \"\"\"The `Metric`'s name.\"\"\"\n"," return self._name\n","\n"," @abc.abstractmethod\n"," def _validate(self, y_true: np.ndarray, y_pred: np.ndarray) -> None:\n"," \"\"\"Validates the `y_true` labels and `y_pred` predictions.\n","\n"," Note: Each prediction subarray `y_pred[i, ...]` at index `i` should\n"," correspond to the `y_true[i]` label.\n","\n"," Args:\n"," y_true: The ground truth label targets.\n"," y_pred: The target predictions.\n","\n"," Raises:\n"," ValueError: If the first dimension of `y_true` and `y_pred` do not match.\n"," \"\"\"\n"," if y_true.shape[0] != y_pred.shape[0]:\n"," raise ValueError('`y_true` and `y_pred` first dimension mismatch: '\n"," f'{y_true.shape[0]} != {y_pred.shape[0]}')\n","\n"," def __call__(self, y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Invokes the `Metric`'s function.\n","\n"," Args:\n"," y_true: The ground truth label values.\n"," y_pred: The target predictions.\n","\n"," Returns:\n"," The result of the `Metric.fn(y_true, y_pred)`.\n"," \"\"\"\n"," self._validate(y_true, y_pred)\n"," return self._fn(y_true, y_pred)\n","\n"," def __str__(self) -> str:\n"," return self.name\n","\n","\n","class ContinuousMetric(Metric):\n"," \"\"\"Represents a callable wrapper class for a named continuous label function.\n","\n"," Attributes:\n"," name: The metric's name.\n"," \"\"\"\n","\n"," # Note: This is a useful delegation since _validate is an @abc.abstractmethod.\n"," def _validate( # pylint: disable=useless-super-delegation\n"," self,\n"," y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," ) -> None:\n"," \"\"\"Validates the `y_true` labels and `y_pred` predictions.\n","\n"," Args:\n"," y_true: The ground truth label values.\n"," y_pred: The target predictions.\n","\n"," Raises:\n"," ValueError: If the first dimension of `y_true` and `y_pred` do not match.\n"," \"\"\"\n"," super()._validate(y_true, y_pred)\n","\n","\n","class BinaryMetric(Metric):\n"," \"\"\"Represents a callable wrapper class for a named binary label function.\n","\n"," This class asserts that the provided `y_true` labels are binary targets in\n"," `{0, 1}` and that `y_true` contains at least one element in each class, i.e.,\n"," not all samples are from the same class.\n","\n"," Attributes:\n"," name: The metric's name.\n"," \"\"\"\n","\n"," def _validate(self, y_true: np.ndarray, y_pred: np.ndarray) -> None:\n"," \"\"\"Validates the `y_true` labels and `y_pred` predictions.\n","\n"," Args:\n"," y_true: The ground truth label values.\n"," y_pred: The target predictions.\n","\n"," Raises:\n"," ValueError: If the first dimension of `y_true` and `y_pred` do not match.\n"," ValueError: If `y_true` labels are nonbinary, i.e., not all values are in\n"," `{BINARY_LABEL_CONTROL, BINARY_LABEL_CASE}` or if `y_true` does not\n"," contain at least one element from each class.\n"," \"\"\"\n"," super()._validate(y_true, y_pred)\n"," if not is_valid_binary_label(y_true):\n"," raise ValueError('`y_true` labels must be in `{BINARY_LABEL_CONTROL, '\n"," 'BINARY_LABEL_CASE}` and have at least one element from '\n"," f'each class; found: {y_true}')\n","\n","\n","def is_binary(metric: Metric) -> bool:\n"," \"\"\"Whether `metric` is a metric computed with binary `y_true` labels.\"\"\"\n"," return isinstance(metric, BinaryMetric)\n","\n","\n","def is_valid_binary_label(array: np.ndarray) -> bool:\n"," \"\"\"Whether `array` is a \"valid\" binary label array for bootstrapping.\n","\n"," We define a valid binary label array as an array that contains only binary\n"," values, i.e., `{BINARY_LABEL_CONTROL, BINARY_LABEL_CASE}`, and contains at\n"," least one value from each class.\n","\n"," Args:\n"," array: A numpy array.\n","\n"," Returns:\n"," Whether `array` is a \"valid\" binary label array.\n"," \"\"\"\n"," is_case_mask = array == BINARY_LABEL_CASE\n"," is_control_mask = array == BINARY_LABEL_CONTROL\n"," return (np.any(is_case_mask) and np.any(is_control_mask) and\n"," np.all(np.logical_or(is_case_mask, is_control_mask)))\n","\n","\n","def pearsonr(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the Pearson R correlation coefficient.\"\"\"\n"," # Note: We ignore the returned p value.\n"," r, _ = scipy.stats.pearsonr(y_true, y_pred)\n"," return r\n","\n","\n","def pearsonr_squared(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the square of the Pearson correlation coefficient.\"\"\"\n"," return pearsonr(y_true, y_pred)**2\n","\n","\n","def spearmanr(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the Spearman R correlation coefficient.\"\"\"\n"," # Note: We ignore the returned p value.\n"," r, _ = scipy.stats.spearmanr(y_true, y_pred)\n"," return r\n","\n","\n","def count(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the number of samples in `y_true`.\"\"\"\n"," if y_true.shape[0] != y_pred.shape[0]:\n"," raise ValueError('`y_true` and `y_pred` first dimension mismatch: '\n"," f'{y_true.shape[0]} != {y_pred.shape[0]}')\n"," return len(y_true)\n","\n","\n","def frequency_between(y_true: np.ndarray, y_pred: np.ndarray,\n"," percentile_lower: int, percentile_upper: int) -> float:\n"," \"\"\"Computes the positive class frequency within a percentile interval.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," percentile_lower: The lower bound (inclusive) of percentile. 0 to include\n"," all samples.\n"," percentile_upper: The upper bound (inclusive for 100, exclusive for all\n"," other values) of percentile. 100 to include all samples.\n","\n"," Returns:\n"," A [0.0, 1.0] float corresponding to the positive class frequency within\n"," the percentile interval.\n","\n"," Raises:\n"," ValueError: Invalid percentile range.\n"," \"\"\"\n"," if not 0 <= percentile_lower < 100:\n"," raise ValueError('`percentile_lower` must be in range `[0, 100)`: '\n"," f'{percentile_lower}')\n"," if not 0 < percentile_upper <= 100:\n"," raise ValueError('`percentile_upper` must be in range `(0, 100]`: '\n"," f'{percentile_upper}')\n","\n"," pred_lower_percentile, pred_upper_percentile = np.percentile(\n"," a=y_pred, q=[percentile_lower, percentile_upper])\n"," lower_mask = (y_pred >= pred_lower_percentile)\n"," if percentile_upper == 100:\n"," mask = lower_mask\n"," else:\n"," upper_mask = (y_pred < pred_upper_percentile)\n"," mask = lower_mask & upper_mask\n"," assert len(mask) == len(y_true)\n"," return np.mean(y_true[mask])\n","\n","\n","def frequency(y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," top_percentile: int = 100) -> float:\n"," \"\"\"Computes the positive class frequency within the top prediction percentile.\n","\n"," We select the subset of `y_true` labels corresponding to `y_pred`'s\n"," `top_percentile`-th prediction percetile and return the positive class\n"," frequency within this subset. `top_percentile=100` indicates the frequency for\n"," all samples.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," top_percentile: Determines the set of examples considered in the frequency\n"," calculation. The top percentile represents the top percentile by\n"," prediction risk. 100 indicates using all samples.\n","\n"," Returns:\n"," A [0.0, 1.0] float corresponding to the positive class frequency in the top\n"," percentile.\n","\n"," Raises:\n"," ValueError: `top_percentile` is not in range `(0, 100]`.\n"," \"\"\"\n"," if not 0 < top_percentile <= 100:\n"," raise ValueError('`top_percentile` must be in range `(0, 100]`: '\n"," f'{top_percentile}')\n","\n"," return frequency_between(\n"," y_true,\n"," y_pred,\n"," percentile_lower=100 - top_percentile,\n"," percentile_upper=100)\n","\n","\n","def frequency_fn(top_percentile: int) -> BootstrappableFn:\n"," \"\"\"Returns a function that computes `frequency` at `top_percentile`.\"\"\"\n","\n"," def _frequency(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," return frequency(y_true, y_pred, top_percentile)\n","\n"," return _frequency\n","\n","\n","def frequency_between_fn(percentile_lower: int,\n"," percentile_upper: int) -> BootstrappableFn:\n"," \"\"\"Returns a function that computes `frequency` in a percentile interval.\"\"\"\n","\n"," def _freq_between(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," return frequency_between(\n"," y_true,\n"," y_pred,\n"," percentile_lower=percentile_lower,\n"," percentile_upper=percentile_upper)\n","\n"," return _freq_between"]},{"cell_type":"code","execution_count":4,"metadata":{"id":"M33VPEMF0sGd","executionInfo":{"status":"ok","timestamp":1717789982063,"user_tz":240,"elapsed":264,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["# Represents a numpy array of indices for a single bootstrap sample.\n","IndexSample = np.ndarray\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class NamedArray:\n"," \"\"\"Represents a named numpy array.\n","\n"," Attributes:\n"," name: The array name.\n"," values: A numpy array.\n"," \"\"\"\n","\n"," name: str\n"," values: np.ndarray\n","\n"," def __post_init__(self):\n"," if not self.name:\n"," raise ValueError('`name` must be specified.')\n","\n"," def __len__(self) -> int:\n"," return len(self.values)\n","\n"," def __str__(self) -> str:\n"," return f'{self.__class__.__name__}({self.name})'\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class Label(NamedArray):\n"," \"\"\"Represents a named numpy array of ground truth label targets.\n","\n"," Attributes:\n"," name: The label name.\n"," values: A numpy array containing ground truth label targets.\n"," \"\"\"\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class Prediction(NamedArray):\n"," \"\"\"Represents a named numpy array of target predictions.\n","\n"," Attributes:\n"," model_name: The name of the model that generated the predictions.\n"," name: The name of the predictions (e.g., the prediction column).\n"," values: A numpy array containing model predictions.\n"," \"\"\"\n","\n"," model_name: str\n","\n"," def __post_init__(self):\n"," super().__post_init__()\n"," if not self.model_name:\n"," raise ValueError('`model_name` must be specified.')\n","\n"," def __str__(self) -> str:\n"," return f'{self.__class__.__name__}({self.model_name}.{self.name})'\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class SampleMean:\n"," \"\"\"Represents an estimate of the population mean for a given sample.\n","\n"," Attributes:\n"," mean: The mean of a given sample.\n"," stddev: The standard deviation of the sample mean.\n"," num_samples: The number of samples used to calculate `mean` and `stddev`.\n","\n"," Raises:\n"," ValueError: If `num_samples` is not >= `1`.\n"," ValueError: If `stddev` is not `0` when `num_samples` is `1`.\n"," \"\"\"\n","\n"," mean: float\n"," stddev: float\n"," num_samples: int\n","\n"," def __post_init__(self):\n"," # Ensure we have a valid number of samples.\n"," if self.num_samples < 1:\n"," raise ValueError(f'`num_samples` must be >= `1`: {self.num_samples}')\n","\n"," # Ensure the standard deviation is 0 given a single sample.\n"," if self.num_samples == 1 and self.stddev != 0.0:\n"," raise ValueError(\n"," f'`stddev` must be `0` if `num_samples` is `1`: {self.stddev:0.4f}'\n"," )\n","\n"," def __str__(self) -> str:\n"," return f'{self.mean:0.4f} (SD={self.stddev:0.4f}, n={self.num_samples})'\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class ConfidenceInterval(SampleMean):\n"," \"\"\"Represents a confidence interval (CI) for a sample mean.\n","\n"," Attributes:\n"," mean: The mean of a given sample.\n"," stddev: The standard deviation of the sample mean.\n"," num_samples: The number of samples used to calculate `mean` and `stddev`.\n"," level: The confidence level at which the CI is calculated (e.g., 95).\n"," ci_lower: The lower limit of the `level` confidence interval.\n"," ci_upper: The upper limit of the `level` confidence interval.\n","\n"," Raises:\n"," ValueError: If `num_samples` is not >= `1`.\n"," ValueError: If `stddev` is not `0` when `num_samples` is `1`.\n"," ValueError: If `level` is not in range (0, 100].\n"," ValueError: If `ci_lower` or `ci_upper` does not match not `mean` when\n"," `num_samples` is `1`.\n"," \"\"\"\n","\n"," level: float\n"," ci_lower: float\n"," ci_upper: float\n","\n"," def __post_init__(self):\n"," super().__post_init__()\n"," # Ensure we have a valid confidence level.\n"," if not 0 < self.level <= 100:\n"," raise ValueError(f'`level` must be in range (0, 100]: {self.level:0.2f}')\n","\n"," # Ensure confidence intervals match the sample mean given a single sample.\n"," if self.num_samples == 1:\n"," if (self.ci_lower != self.mean) or (self.ci_upper != self.mean):\n"," raise ValueError(\n"," '`ci_lower` and `ci_upper` must match `mean` if `num_samples` is '\n"," f'1: mean={self.mean:0.4f}, ci_lower={self.ci_lower:0.4f}, '\n"," f'ci_upper={self.ci_upper:0.4f}'\n"," )\n","\n"," def __str__(self) -> str:\n"," return (\n"," f'{self.mean:0.4f} (SD={self.stddev:0.4f}, n={self.num_samples}, '\n"," f'{self.level:0>6.2f}% CI=[{self.ci_lower:0.4f}, '\n"," f'{self.ci_upper:0.4f}])'\n"," )\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class Result:\n"," \"\"\"Represents a bootstrapped metric result for an individual model.\n","\n"," Attributes:\n"," model_name: The model's name.\n"," prediction_name: The model's prediction name (e.g., the model head's name or\n"," the label name used in training).\n"," metric_name: The metric's name.\n"," ci: A confidence interval describing the distribution of metric samples.\n"," \"\"\"\n","\n"," model_name: str\n"," prediction_name: str\n"," metric_name: str\n"," ci: ConfidenceInterval\n","\n"," def __post_init__(self):\n"," # Ensure model, prediction, and metric names are specified.\n"," if not self.model_name:\n"," raise ValueError('`model_name` must be specified.')\n"," if not self.prediction_name:\n"," raise ValueError('`prediction_name` must be specified.')\n"," if not self.metric_name:\n"," raise ValueError('`metric_name` must be specified.')\n","\n"," def __str__(self) -> str:\n"," return (\n"," f'{self.model_name}.{self.prediction_name}: '\n"," f'{self.metric_name}: {self.ci}'\n"," )\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class PairedResult:\n"," \"\"\"Represents a paired bootstrapped metric result for two models.\n","\n"," Attributes:\n"," model_name_a: The first model's name.\n"," prediction_name_a: The first model's prediction name (e.g., the model head's\n"," name or the label name used in training).\n"," model_name_b: The second model's name.\n"," prediction_name_b: The second model's prediction name (e.g., the model\n"," head's name or the label name used in training).\n"," metric_name: The metric's name.\n"," ci: A confidence interval describing the distribution of differences between\n"," the first and second models' metric samples.\n"," \"\"\"\n","\n"," model_name_a: str\n"," prediction_name_a: str\n"," model_name_b: str\n"," prediction_name_b: str\n"," metric_name: str\n"," ci: ConfidenceInterval\n","\n"," def __post_init__(self):\n"," # Ensure model, prediction, and metric names are specified.\n"," if not self.model_name_a:\n"," raise ValueError('`model_name_a` must be specified.')\n"," if not self.prediction_name_a:\n"," raise ValueError('`prediction_name_a` must be specified.')\n"," if not self.model_name_b:\n"," raise ValueError('`model_name_b` must be specified.')\n"," if not self.prediction_name_b:\n"," raise ValueError('`prediction_name_b` must be specified.')\n"," if not self.metric_name:\n"," raise ValueError('`metric_name` must be specified.')\n","\n"," def __str__(self) -> str:\n"," return (\n"," f'({self.model_name_a}.{self.prediction_name_a} - '\n"," f'{self.model_name_b}.{self.prediction_name_b}): '\n"," f'{self.metric_name}: {self.ci}'\n"," )\n","\n","\n","def _reverse_paired_result(paired_result: PairedResult) -> PairedResult:\n"," \"\"\"Returns the \"(b - a)\" inverse of an \"(a - b)\" `PairedResult`.\"\"\"\n"," reversed_ci = ConfidenceInterval(\n"," mean=(paired_result.ci.mean * -1),\n"," stddev=paired_result.ci.stddev,\n"," num_samples=paired_result.ci.num_samples,\n"," level=paired_result.ci.level,\n"," ci_upper=(paired_result.ci.ci_lower * -1),\n"," ci_lower=(paired_result.ci.ci_upper * -1),\n"," )\n"," reversed_paired_result = PairedResult(\n"," model_name_a=paired_result.model_name_b,\n"," prediction_name_a=paired_result.prediction_name_b,\n"," model_name_b=paired_result.model_name_a,\n"," prediction_name_b=paired_result.prediction_name_a,\n"," metric_name=paired_result.metric_name,\n"," ci=reversed_ci,\n"," )\n"," return reversed_paired_result\n","\n","\n","def _compute_confidence_interval(\n"," samples: np.ndarray,\n"," ci_level: float,\n",") -> ConfidenceInterval:\n"," \"\"\"Computes the mean, standard deviation, and confidence interval for samples.\n","\n"," Args:\n"," samples: A boostrapped array of observed sample values.\n"," ci_level: The confidence level/width of the desired confidence interval.\n","\n"," Returns:\n"," A `Result` containing the mean, standard deviation, and the `ci_level`%\n"," confidence interval for the observed sample values.\n"," \"\"\"\n"," sample_mean = np.mean(samples, axis=0)\n"," sample_std = np.std(samples, axis=0)\n","\n"," lower_percentile = (100 - ci_level) / 2\n"," upper_percentile = 100 - lower_percentile\n"," percentiles = [lower_percentile, upper_percentile]\n"," ci_lower, ci_upper = np.percentile(a=samples, q=percentiles, axis=0)\n","\n"," ci = ConfidenceInterval(\n"," mean=sample_mean,\n"," stddev=sample_std,\n"," num_samples=len(samples),\n"," level=ci_level,\n"," ci_lower=ci_lower,\n"," ci_upper=ci_upper,\n"," )\n","\n"," return ci\n","\n","\n","def _generate_sample_indices(\n"," label: Label,\n"," is_binary: bool,\n"," num_bootstrap: int,\n"," seed: int,\n",") -> List[IndexSample]:\n"," \"\"\"Returns a list of `num_bootstrap` randomly sampled bootstrap indices.\n","\n"," Args:\n"," label: The ground truth label targets.\n"," is_binary: Whether to generate valid binary samples; i.e., each index sample\n"," contains at least one index corresponding to a label from each class.\n"," num_bootstrap: The number of bootstrap indices to generate.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A list of `num_bootstrap` bootstrap sample indices.\n"," \"\"\"\n"," rng = np.random.default_rng(seed)\n"," num_observations = len(label)\n"," sample_indices = []\n"," while len(sample_indices) < num_bootstrap:\n"," index = rng.integers(0, high=num_observations, size=num_observations)\n"," sample_true = label.values[index]\n"," # If computing a binary metric, skip indices that result in invalid labels.\n"," if is_binary and not is_valid_binary_label(sample_true):\n"," continue\n"," sample_indices.append(index)\n"," return sample_indices\n","\n","\n","def _compute_metric_samples(\n"," metric: Metric,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," sample_indices: Sequence[np.ndarray],\n",") -> Dict[str, np.ndarray]:\n"," \"\"\"Generates `num_bootstrap` metric samples for each `Prediction`.\n","\n"," Note: This method assumes that label and prediction values are orded so that\n"," the value at index `i` in a given `Prediction` corresponds to the label value\n"," at index `i` in `label`. Both the `Label` and `Prediction` arrays are indexed\n"," using the given `sample_indices`.\n","\n"," Args:\n"," metric: An instance of a bootstrappable `Metric`; used to compute samples.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," sample_indices: An array of bootstrap sample indices. If empty, returns the\n"," single value computed on the entire dataset for each prediction.\n","\n"," Returns:\n"," A mapping of model names to the corresponding metric samples array.\n"," \"\"\"\n"," if not sample_indices:\n"," metric_samples = {}\n"," for prediction in predictions:\n"," value = metric(label.values, prediction.values)\n"," metric_samples[prediction.model_name] = np.asarray([value])\n"," return metric_samples\n","\n"," metric_samples = {prediction.model_name: [] for prediction in predictions}\n"," for index in sample_indices:\n"," sample_true = label.values[index]\n"," for prediction in predictions:\n"," sample_value = metric(sample_true, prediction.values[index])\n"," metric_samples[prediction.model_name].append(sample_value)\n","\n"," metric_samples = {\n"," name: np.asarray(samples) for name, samples in metric_samples.items()\n"," }\n","\n"," return metric_samples\n","\n","\n","def _compute_all_metric_samples(\n"," metrics: Sequence[Metric],\n"," contains_binary_metric: bool,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," num_bootstrap: int,\n"," seed: int,\n",") -> Dict[str, Dict[str, np.ndarray]]:\n"," \"\"\"Generates `num_bootstrap` samples for each `Prediction` and `Metric`.\n","\n"," Args:\n"," metrics: A sequence of a bootstrappable `Metric` instances.\n"," contains_binary_metric: Whether the set of metrics contains a binary metric.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," num_bootstrap: The number of bootstrap iterations.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A mapping of metric names to model-sample dictionaries.\n"," \"\"\"\n"," sample_indices = _generate_sample_indices(\n"," label,\n"," contains_binary_metric,\n"," num_bootstrap,\n"," seed,\n"," )\n"," metric_samples = []\n"," for metric in metrics:\n"," metric_samples.append(\n"," _compute_metric_samples(\n"," metric=metric,\n"," label=label,\n"," predictions=predictions,\n"," sample_indices=sample_indices,\n"," )\n"," )\n","\n"," return {\n"," metric.name: metric_sample\n"," for metric, metric_sample in zip(metrics, metric_samples)\n"," }\n","\n","\n","def _process_metric_samples(\n"," metric: Metric,\n"," predictions: Sequence[Prediction],\n"," model_names_to_metric_samples: Dict[str, np.ndarray],\n"," ci_level: float,\n",") -> List[Result]:\n"," \"\"\"Compute `ConfidenceInterval`s for metric samples across predictions.\"\"\"\n"," results = []\n"," for prediction in predictions:\n"," metric_samples = model_names_to_metric_samples[prediction.model_name]\n"," ci = _compute_confidence_interval(metric_samples, ci_level)\n"," result = Result(prediction.model_name, prediction.name, metric.name, ci)\n"," results.append(result)\n"," return results\n","\n","\n","def _process_metric_samples_paired(\n"," metric: Metric,\n"," predictions: Sequence[Prediction],\n"," model_names_to_metric_samples: Dict[str, np.ndarray],\n"," ci_level: float,\n",") -> List[PairedResult]:\n"," \"\"\"Compute `ConfidenceInterval`s for paired samples across predictions.\"\"\"\n"," results = []\n"," for i, prediction_a in enumerate(predictions[:-1]):\n"," for prediction_b in predictions[i + 1 :]:\n"," # Compute the result of `prediction_a - prediction_b`.\n"," metric_samples_a = model_names_to_metric_samples[prediction_a.model_name]\n"," metric_samples_b = model_names_to_metric_samples[prediction_b.model_name]\n"," metric_samples_diff = metric_samples_a - metric_samples_b\n"," ci = _compute_confidence_interval(metric_samples_diff, ci_level)\n"," result = PairedResult(\n"," prediction_a.model_name,\n"," prediction_a.name,\n"," prediction_b.model_name,\n"," prediction_b.name,\n"," metric.name,\n"," ci,\n"," )\n"," results.append(result)\n"," # Derive and include the result of `prediction_b - prediction_a`.\n"," results.append(_reverse_paired_result(result))\n"," return results\n","\n","\n","def _bootstrap(\n"," metrics: Sequence[Metric],\n"," contains_binary_metric: bool,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," num_bootstrap: int,\n"," ci_level: float,\n"," seed: int,\n",") -> Dict[str, List[Result]]:\n"," \"\"\"Performs bootstrapping for all models using the given metrics.\n","\n"," Args:\n"," metrics: A sequence of a bootstrappable `Metric` instances.\n"," contains_binary_metric: Whether the set of metrics contains a binary metric.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," num_bootstrap: The number of bootstrap iterations.\n"," ci_level: The confidence level/width of the desired confidence interval.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A dictionary mapping metric names to a list of `Result`s containing the mean\n"," metric values of each model over `num_bootstrap` bootstrapping iterations.\n"," \"\"\"\n"," metric_to_model_to_samples = _compute_all_metric_samples(\n"," metrics,\n"," contains_binary_metric,\n"," label,\n"," predictions,\n"," num_bootstrap,\n"," seed,\n"," )\n"," metric_samples = []\n"," for metric in metrics:\n"," metric_samples.append(\n"," _process_metric_samples(\n"," metric=metric,\n"," predictions=predictions,\n"," model_names_to_metric_samples=metric_to_model_to_samples[\n"," metric.name\n"," ],\n"," ci_level=ci_level,\n"," )\n"," )\n","\n"," return {\n"," metric.name: metric_sample\n"," for metric, metric_sample in zip(metrics, metric_samples)\n"," }\n","\n","\n","def _paired_bootstrap(\n"," metrics: Sequence[Metric],\n"," contains_binary_metric: bool,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," num_bootstrap: int,\n"," ci_level: float,\n"," seed: int,\n",") -> Dict[str, List[PairedResult]]:\n"," \"\"\"Performs paired bootstrapping for all models using the given metrics.\n","\n"," Args:\n"," metrics: A sequence of a bootstrappable `Metric` instances.\n"," contains_binary_metric: Whether the set of metrics contains a binary metric.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," num_bootstrap: The number of bootstrap iterations.\n"," ci_level: The confidence level/width of the desired confidence interval.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A dictionary mapping metric names to `PairedResult`s containing the mean\n"," metric difference between models over `num_bootstrap` bootstrapping\n"," iterations.\n"," \"\"\"\n"," metric_to_model_to_samples = _compute_all_metric_samples(\n"," metrics,\n"," contains_binary_metric,\n"," label,\n"," predictions,\n"," num_bootstrap,\n"," seed,\n"," )\n"," metric_samples = []\n"," for metric in metrics:\n"," metric_samples.append(\n"," _process_metric_samples_paired(\n"," metric=metric,\n"," predictions=predictions,\n"," model_names_to_metric_samples=metric_to_model_to_samples[\n"," metric.name\n"," ],\n"," ci_level=ci_level,\n"," )\n"," )\n","\n"," return {\n"," metric.name: metric_sample\n"," for metric, metric_sample in zip(metrics, metric_samples)\n"," }\n","\n","\n","def _default_binary_metrics() -> List[BinaryMetric]:\n"," \"\"\"Returns `PerformanceMetrics`'s default metrics for binary target.\"\"\"\n"," metrics = [\n"," BinaryMetric('num', count),\n"," BinaryMetric('auc', sklearn.metrics.roc_auc_score),\n"," BinaryMetric('auprc', sklearn.metrics.average_precision_score),\n"," ]\n"," for percentile in [100, 10, 5, 1]:\n"," metrics.append(\n"," BinaryMetric(\n"," f'freq@{percentile:>03}%',\n"," frequency_fn(percentile),\n"," )\n"," )\n"," return metrics\n","\n","\n","def _default_continuous_metrics() -> List[ContinuousMetric]:\n"," \"\"\"Returns `PerformanceMetrics`'s default metrics for continuous target.\"\"\"\n"," metrics = [\n"," ContinuousMetric('num', count),\n"," ContinuousMetric('pearson', pearsonr),\n"," ContinuousMetric('pearsonr_squared', pearsonr_squared),\n"," ContinuousMetric('spearman', spearmanr),\n"," ContinuousMetric('mse', sklearn.metrics.mean_squared_error),\n"," ContinuousMetric('mae', sklearn.metrics.mean_absolute_error),\n"," ]\n"," return metrics\n","\n","\n","def _default_metrics(binary_targets: bool) -> List[Metric]:\n"," \"\"\"Returns `PerformanceMetrics`'s default set of metrics for the target type.\n","\n"," Args:\n"," binary_targets: Whether the target labels are binary. If false, the returned\n"," metrics assume continuous labels.\n","\n"," Returns:\n"," The default set of binary or continuous `bootstrap_metrics.Metric`s.\n"," \"\"\"\n"," if binary_targets:\n"," return _default_binary_metrics()\n"," return _default_continuous_metrics()\n","\n","\n","class PerformanceMetrics:\n"," \"\"\"A named collection of invocable, bootstrapable `Metric`s.\n","\n"," Initializes a class that applies the given `Metric` functions to new ground\n"," truth labels and predictions. `Metric`s can be evaluated with and without\n"," bootstrapping.\n","\n"," The default metrics are number of samples, auc, auprc, and frequency\n"," calculations for the top 100/10/5/1 top percentiles, if `default_metrics` is\n"," 'binary'. If `default_metrics` is 'continuous', the default metrics are\n"," Pearson and Spearman correlations, the square of the Pearson correlation, mean\n"," squared error (MSE) and mean absolute error (MAE).\n","\n"," TODO(b/199452239): Refactor `PerformanceMetrics` so that the default metric\n"," set is not parameterized with a string.\n","\n"," Raises:\n"," ValueError: if an item in `metrics` is not of type `Metric`.\n"," \"\"\"\n","\n"," def __init__(\n"," self,\n"," name: str,\n"," default_metrics: Optional[str] = None,\n"," metrics: Optional[List[Metric]] = None,\n"," ) -> None:\n","\n"," if metrics is None:\n"," if default_metrics is None:\n"," raise ValueError('`default_metrics` is None and no metric is provided.')\n"," elif default_metrics == 'binary':\n"," metrics = _default_metrics(binary_targets=True)\n"," elif default_metrics == 'continuous':\n"," metrics = _default_metrics(binary_targets=False)\n"," else:\n"," raise ValueError(\n"," 'unknown `default_metrics`: {}'.format(default_metrics)\n"," )\n","\n"," for metric in metrics:\n"," if not isinstance(metric, Metric):\n"," raise ValueError('Invalid metric value: must be of class `Metric`.')\n","\n"," if len(metrics) != len({metric.name for metric in metrics}):\n"," raise ValueError(f'Metric names must be unique: {metrics}')\n","\n"," self.name = name\n"," self.metrics = metrics\n"," self.contains_binary = any(is_binary(m) for m in metrics)\n","\n"," def compute(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," ) -> Dict[str, Result]:\n"," \"\"\"Evaluates all metrics using the given labels and predictions.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," mask: A boolean mask; applied to `y_true` and `y_pred`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n","\n"," Returns:\n"," A dictionary of bootstrapped metrics keyed on metric name with\n"," `Result` values.\n","\n"," Raises:\n"," ValueError: If the dimensions of `y_true`, `y_pred`, or `mask` do not\n"," match, or labels are not in {0 , 1}.\n"," \"\"\"\n"," if len(y_true) != len(y_pred):\n"," raise ValueError('Label and prediction dimensions do not match.')\n","\n"," if mask is not None and len(mask) != len(y_pred):\n"," raise ValueError('Label and prediction dimensions do not match mask.')\n","\n"," if mask is not None:\n"," y_true = y_true[mask]\n"," y_pred = y_pred[mask]\n","\n"," # TODO(b/197539434): Pipe through non-empty names after public api refactor.\n"," label_name = 'label'\n"," label = Label(label_name, y_true)\n"," predictions = [Prediction(label_name, y_pred, 'model')]\n","\n"," metric_results = _bootstrap(\n"," self.metrics,\n"," contains_binary_metric=self.contains_binary,\n"," label=label,\n"," predictions=predictions,\n"," num_bootstrap=n_bootstrap,\n"," ci_level=conf_interval,\n"," seed=seed,\n"," )\n","\n"," # TODO(b/197539434): Remove temporary asserts after public api refactor.\n"," final_results = {}\n"," for metric_name, results in metric_results.items():\n"," assert len(results) == 1\n"," final_results[metric_name] = results[0]\n","\n"," return final_results\n","\n"," def compute_paired(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred_a: np.ndarray,\n"," y_pred_b: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," ) -> Dict[str, PairedResult]:\n"," \"\"\"Computes a paired bootstrap value for each metric.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred_a: Target predictions from model A; compared to `y_pred_b`.\n"," y_pred_b: Target predictions from model B; compared to `y_pred_a`.\n"," mask: A boolean mask; applied to `y_true`, `y_pred_a`, and `y_pred_b`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n","\n"," Returns:\n"," A dictionary of paired bootstrapped metrics keyed on metric name with\n"," `PairedResult` values.\n","\n"," Raises:\n"," ValueError: If the dimensions of `y_true`, `y_pred_a`, `y_pred_b` or\n"," `mask` do not match, or labels are not in {0 , 1}.\n"," \"\"\"\n"," if (len(y_true) != len(y_pred_a)) or (len(y_true) != len(y_pred_b)):\n"," raise ValueError('Label and prediction dimensions do not match.')\n","\n"," if mask is not None and len(mask) != len(y_pred_a):\n"," raise ValueError('Label and prediction dimensions do not match mask.')\n","\n"," if mask is not None:\n"," y_true = y_true[mask]\n"," y_pred_a = y_pred_a[mask]\n"," y_pred_b = y_pred_b[mask]\n","\n"," # TODO(b/197539434): Pipe through non-empty names after public api refactor.\n"," label_name = 'label'\n"," label = Label(label_name, y_true)\n"," first_model_name = 'model_a'\n"," predictions = [\n"," Prediction(label_name, y_pred_a, first_model_name),\n"," Prediction(label_name, y_pred_b, 'model_b'),\n"," ]\n","\n"," metric_results = _paired_bootstrap(\n"," self.metrics,\n"," contains_binary_metric=self.contains_binary,\n"," label=label,\n"," predictions=predictions,\n"," num_bootstrap=n_bootstrap,\n"," ci_level=conf_interval,\n"," seed=seed,\n"," )\n","\n"," # TODO(b/197539434): Remove temporary asserts after public api refactor.\n"," final_results = {}\n"," for metric_name, results in metric_results.items():\n"," assert len(results) == 2\n"," assert results[0].model_name_a == first_model_name\n"," final_results[metric_name] = results[0]\n","\n"," return final_results\n","\n"," def _print_results(\n"," self,\n"," title: str,\n"," results: Dict[str, Union[Result, PairedResult]],\n"," ) -> None:\n"," \"\"\"Prints each result object under the current name and given title.\"\"\"\n"," print(f'{self.name}: {title}')\n"," for _, result in sorted(results.items()):\n"," print(f'\\t{result}')\n","\n"," def compute_and_print(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," title: str = '',\n"," ) -> None:\n"," \"\"\"Evaluates and pretty-prints metrics using given labels and predictions.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," mask: A boolean mask; applied to `y_true` and `y_pred`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n"," title: A title appended to the printed evaluation metrics.\n","\n"," Raises:\n"," ValueError: If any of `y_true`, `y_pred`, or `mask` are not of type\n"," numpy.array of if their dimensions do not match.\n"," \"\"\"\n"," results = self.compute(\n"," y_true,\n"," y_pred,\n"," mask=mask,\n"," n_bootstrap=n_bootstrap,\n"," conf_interval=conf_interval,\n"," seed=seed,\n"," )\n"," self._print_results(title, results)\n","\n"," def compute_paired_and_print(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred_a: np.ndarray,\n"," y_pred_b: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," title: str = '',\n"," **kwargs,\n"," ) -> None:\n"," \"\"\"Evaluates and pretty-prints paired metrics.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred_a: Target predictions from model A; compared to `y_pred_b`.\n"," y_pred_b: Target predictions from model B; compared to `y_pred_a`.\n"," mask: A boolean mask; applied to `y_true`, `y_pred_a`, and `y_pred_b`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n"," title: A title appended to the printed evaluation metrics.\n"," **kwargs: Additional keyword arguments passed to each Metric's `func`.\n"," \"\"\"\n"," results = self.compute_paired(\n"," y_true,\n"," y_pred_a,\n"," y_pred_b,\n"," mask=mask,\n"," n_bootstrap=n_bootstrap,\n"," conf_interval=conf_interval,\n"," seed=seed,\n"," **kwargs,\n"," )\n"," self._print_results(title, results)"]},{"cell_type":"code","execution_count":5,"metadata":{"id":"x4222NTc0xpR","executionInfo":{"status":"ok","timestamp":1717789982063,"user_tz":240,"elapsed":15,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["N_BOOTSTRAP = 300\n","BOOTSTRAP_METRICS_LIST = [\n"," BinaryMetric('roc_auc', metrics.roc_auc_score),\n"," BinaryMetric('pr_auc', metrics.average_precision_score),\n"," ContinuousMetric('pearsonr', pearsonr),\n"," BinaryMetric('top10prev', frequency_fn(10)),\n","]\n","\n","def get_prs_eval_info(y_true, y_pred, name, as_dataframe=False):\n"," performance_metrics = PerformanceMetrics(\n"," 'Metrics', metrics=BOOTSTRAP_METRICS_LIST)\n"," performance_metrics_values = performance_metrics.compute(\n"," y_true=y_true,\n"," y_pred=y_pred,\n"," n_bootstrap=N_BOOTSTRAP,\n"," )\n"," # print(performance_metrics_values, flush=True)\n"," roc_auc_ci = performance_metrics_values['roc_auc'].ci\n"," pr_auc_ci = performance_metrics_values['pr_auc'].ci\n"," pearsonr_ci = performance_metrics_values['pearsonr'].ci\n"," top10prev_ci = performance_metrics_values['top10prev'].ci\n"," info = {\n"," 'method': name,\n"," 'pearsonr': pearsonr_ci.mean,\n"," 'pearsonr_std': pearsonr_ci.stddev,\n"," 'pearsonr_lower': pearsonr_ci.ci_lower,\n"," 'pearsonr_upper': pearsonr_ci.ci_upper,\n"," 'roc_auc': roc_auc_ci.mean,\n"," 'roc_auc_std': roc_auc_ci.stddev,\n"," 'roc_auc_lower': roc_auc_ci.ci_lower,\n"," 'roc_auc_upper': roc_auc_ci.ci_upper,\n"," 'pr_auc': pr_auc_ci.mean,\n"," 'pr_auc_std': pr_auc_ci.stddev,\n"," 'pr_auc_lower': pr_auc_ci.ci_lower,\n"," 'pr_auc_upper': pr_auc_ci.ci_upper,\n"," 'top10prev': top10prev_ci.mean,\n"," 'top10prev_std': top10prev_ci.stddev,\n"," 'top10prev_lower': top10prev_ci.ci_lower,\n"," 'top10prev_upper': top10prev_ci.ci_upper,\n"," }\n"," if as_dataframe:\n"," return pd.DataFrame(info, index=[0])\n"," else:\n"," return info\n","\n","\n","def get_prs_paired_eval_info(y_true,\n"," y_pred1,\n"," y_pred2,\n"," name1,\n"," name2,\n"," as_dataframe=False):\n"," performance_metrics = PerformanceMetrics(\n"," 'Metrics', metrics=BOOTSTRAP_METRICS_LIST)\n"," performance_metrics_values_paired = performance_metrics.compute_paired(\n"," y_true=y_true,\n"," y_pred_a=y_pred1,\n"," y_pred_b=y_pred2,\n"," n_bootstrap=N_BOOTSTRAP,\n"," )\n"," # print(performance_metrics_values_paired, flush=True)\n"," roc_auc_ci = performance_metrics_values_paired['roc_auc'].ci\n"," pr_auc_ci = performance_metrics_values_paired['pr_auc'].ci\n"," pearsonr_ci = performance_metrics_values_paired['pearsonr'].ci\n"," top10prev_ci = performance_metrics_values_paired['top10prev'].ci\n"," info = {\n"," 'method_a': name1,\n"," 'method_b': name2,\n"," 'pearsonr': pearsonr_ci.mean,\n"," 'pearsonr_std': pearsonr_ci.stddev,\n"," 'pearsonr_lower': pearsonr_ci.ci_lower,\n"," 'pearsonr_upper': pearsonr_ci.ci_upper,\n"," 'roc_auc': roc_auc_ci.mean,\n"," 'roc_auc_std': roc_auc_ci.stddev,\n"," 'roc_auc_lower': roc_auc_ci.ci_lower,\n"," 'roc_auc_upper': roc_auc_ci.ci_upper,\n"," 'pr_auc': pr_auc_ci.mean,\n"," 'pr_auc_std': pr_auc_ci.stddev,\n"," 'pr_auc_lower': pr_auc_ci.ci_lower,\n"," 'pr_auc_upper': pr_auc_ci.ci_upper,\n"," 'top10prev': top10prev_ci.mean,\n"," 'top10prev_std': top10prev_ci.stddev,\n"," 'top10prev_lower': top10prev_ci.ci_lower,\n"," 'top10prev_upper': top10prev_ci.ci_upper,\n"," }\n"," if as_dataframe:\n"," return pd.DataFrame(info, index=[0])\n"," else:\n"," return info"]},{"cell_type":"markdown","metadata":{"id":"NOaueJxRPmpG"},"source":["# Simulated data generation\n","\n","In this code example, we generate some simulated data (N=1,000) to demonstrate how to use the above code snippet to compute various metrics in the PRS evaluation part of the paper."]},{"cell_type":"code","execution_count":6,"metadata":{"id":"iXHTm8dxzY2H","executionInfo":{"status":"ok","timestamp":1717789982064,"user_tz":240,"elapsed":14,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"}}},"outputs":[],"source":["np.random.seed(42)\n","individual_prs1 = np.random.normal(size=(1000,))\n","individual_prs2 = 0.8 * individual_prs1 + 0.2 * np.random.normal(size=(1000,))\n","individual_phenotype = 0.3 * individual_prs1 + 0.7 * np.random.normal(\n"," size=(1000,)\n",")\n","individual_phenotype = (individual_phenotype >= 0).astype(int)\n","\n","data_df = pd.DataFrame({\n"," 'prs1': individual_prs1,\n"," 'prs2': individual_prs2,\n"," 'phenotype': individual_phenotype,\n","})"]},{"cell_type":"code","execution_count":7,"metadata":{"colab":{"height":206,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":13,"status":"ok","timestamp":1717789982064,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"bzdHe1jqULbv","outputId":"f8e850ec-2fdf-45fb-b2be-f4e7ebe5cafa"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" prs1 prs2 phenotype\n","0 0.496714 0.677242 0\n","1 -0.138264 0.074315 0\n","2 0.647689 0.530077 0\n","3 1.523030 1.089037 1\n","4 -0.234153 -0.047678 0"],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
prs1prs2phenotype
00.4967140.6772420
1-0.1382640.0743150
20.6476890.5300770
31.5230301.0890371
4-0.234153-0.0476780
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n"," \n","\n","\n","\n"," \n","
\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","variable_name":"data_df","summary":"{\n \"name\": \"data_df\",\n \"rows\": 1000,\n \"fields\": [\n {\n \"column\": \"prs1\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.9792159381796757,\n \"min\": -3.2412673400690726,\n \"max\": 3.852731490654721,\n \"num_unique_values\": 1000,\n \"samples\": [\n 0.543360192379935,\n 0.9826909839455139,\n -1.8408742313316453\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"prs2\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.8005263506410991,\n \"min\": -2.4852626735659844,\n \"max\": 3.4321005411611654,\n \"num_unique_values\": 1000,\n \"samples\": [\n 0.5511076945976712,\n 0.5725922028405726,\n -1.4935892287728105\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"phenotype\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0,\n \"min\": 0,\n \"max\": 1,\n \"num_unique_values\": 2,\n \"samples\": [\n 1,\n 0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":7}],"source":["data_df.head()"]},{"cell_type":"markdown","metadata":{"id":"4LYsbEE3RdeF"},"source":["# PRS evaluation with bootstrapping\n","\n","The following code generates all evaluation metrics, namely Pearson R, AUC-ROC, AUC-PR, top 10% prevalence, and their 95% confidence intervals using bootstrapping. Note that, from the way we generated the simulated data, we expect the Pearson R of ~0.3 for `prs1` and we expect `prs1` to have higher correlation with the phenotype than `prs2`."]},{"cell_type":"code","execution_count":8,"metadata":{"colab":{"height":101,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":17429,"status":"ok","timestamp":1717789999485,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"WVJnK7BAPi33","outputId":"68161231-112f-4e33-d8d0-0ffc89019139"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" method pearsonr pearsonr_std pearsonr_lower pearsonr_upper roc_auc \\\n","0 prs1 0.333455 0.027456 0.277529 0.387433 0.69263 \n","\n"," roc_auc_std roc_auc_lower roc_auc_upper pr_auc pr_auc_std \\\n","0 0.016445 0.65976 0.725288 0.675271 0.022152 \n","\n"," pr_auc_lower pr_auc_upper top10prev top10prev_std top10prev_lower \\\n","0 0.632141 0.715912 0.770216 0.043321 0.688044 \n","\n"," top10prev_upper \n","0 0.85078 "],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
methodpearsonrpearsonr_stdpearsonr_lowerpearsonr_upperroc_aucroc_auc_stdroc_auc_lowerroc_auc_upperpr_aucpr_auc_stdpr_auc_lowerpr_auc_uppertop10prevtop10prev_stdtop10prev_lowertop10prev_upper
0prs10.3334550.0274560.2775290.3874330.692630.0164450.659760.7252880.6752710.0221520.6321410.7159120.7702160.0433210.6880440.85078
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","summary":"{\n \"name\": \")\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"method\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs1\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.3334554859786796,\n \"max\": 0.3334554859786796,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.3334554859786796\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.027455597173908577,\n \"max\": 0.027455597173908577,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.027455597173908577\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.2775293042598108,\n \"max\": 0.2775293042598108,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.2775293042598108\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.38743254268744753,\n \"max\": 0.38743254268744753,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.38743254268744753\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6926303605619311,\n \"max\": 0.6926303605619311,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6926303605619311\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.016445301315729702,\n \"max\": 0.016445301315729702,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.016445301315729702\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.659760150142918,\n \"max\": 0.659760150142918,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.659760150142918\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7252876945992696,\n \"max\": 0.7252876945992696,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7252876945992696\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.675270596876246,\n \"max\": 0.675270596876246,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.675270596876246\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.02215152388674347,\n \"max\": 0.02215152388674347,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.02215152388674347\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6321413648383354,\n \"max\": 0.6321413648383354,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6321413648383354\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7159121917609861,\n \"max\": 0.7159121917609861,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7159121917609861\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7702162426122681,\n \"max\": 0.7702162426122681,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7702162426122681\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.04332125213088804,\n \"max\": 0.04332125213088804,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.04332125213088804\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6880441176470588,\n \"max\": 0.6880441176470588,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6880441176470588\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.8507797029702969,\n \"max\": 0.8507797029702969,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.8507797029702969\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":8}],"source":["get_prs_eval_info(\n"," y_true=data_df['phenotype'],\n"," y_pred=data_df['prs1'],\n"," name='prs1',\n"," as_dataframe=True\n",")"]},{"cell_type":"code","execution_count":9,"metadata":{"colab":{"height":101,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":9213,"status":"ok","timestamp":1717790008685,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"puOfA5wuQeiJ","outputId":"40a4792a-c897-450c-ee39-aa8ecd72f761"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" method pearsonr pearsonr_std pearsonr_lower pearsonr_upper roc_auc \\\n","0 prs2 0.319189 0.027899 0.260433 0.373947 0.6837 \n","\n"," roc_auc_std roc_auc_lower roc_auc_upper pr_auc pr_auc_std \\\n","0 0.016604 0.649911 0.717019 0.664467 0.022454 \n","\n"," pr_auc_lower pr_auc_upper top10prev top10prev_std top10prev_lower \\\n","0 0.620486 0.706022 0.764624 0.042396 0.671552 \n","\n"," top10prev_upper \n","0 0.84 "],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
methodpearsonrpearsonr_stdpearsonr_lowerpearsonr_upperroc_aucroc_auc_stdroc_auc_lowerroc_auc_upperpr_aucpr_auc_stdpr_auc_lowerpr_auc_uppertop10prevtop10prev_stdtop10prev_lowertop10prev_upper
0prs20.3191890.0278990.2604330.3739470.68370.0166040.6499110.7170190.6644670.0224540.6204860.7060220.7646240.0423960.6715520.84
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","summary":"{\n \"name\": \")\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"method\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs2\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.3191890184766251,\n \"max\": 0.3191890184766251,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.3191890184766251\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.027898865889530153,\n \"max\": 0.027898865889530153,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.027898865889530153\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.2604328480042442,\n \"max\": 0.2604328480042442,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.2604328480042442\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.3739469506434232,\n \"max\": 0.3739469506434232,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.3739469506434232\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6836996447028457,\n \"max\": 0.6836996447028457,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6836996447028457\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.01660378118234475,\n \"max\": 0.01660378118234475,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.01660378118234475\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6499110741641438,\n \"max\": 0.6499110741641438,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6499110741641438\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7170185826451294,\n \"max\": 0.7170185826451294,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7170185826451294\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6644674946186202,\n \"max\": 0.6644674946186202,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6644674946186202\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.0224540065869167,\n \"max\": 0.0224540065869167,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.0224540065869167\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6204864568922334,\n \"max\": 0.6204864568922334,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6204864568922334\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7060224657169427,\n \"max\": 0.7060224657169427,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7060224657169427\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.764623511500396,\n \"max\": 0.764623511500396,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.764623511500396\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.042396301865302535,\n \"max\": 0.042396301865302535,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.042396301865302535\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6715519801980199,\n \"max\": 0.6715519801980199,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6715519801980199\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.84,\n \"max\": 0.84,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.84\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":9}],"source":["get_prs_eval_info(\n"," y_true=data_df['phenotype'],\n"," y_pred=data_df['prs2'],\n"," name='prs2',\n"," as_dataframe=True\n",")"]},{"cell_type":"markdown","metadata":{"id":"OiLCjqcrSjPg"},"source":["# PRS comparison with paired bootstrapping\n","\n","The following code snippet compares the performance of `prs1` and `prs2` using paired bootstrapping. Note that the difference is statistically significant with 95% paired bootstrapping confidence interval, if the lower and upper end of the confidence interval are both positive (implying `prs1` is significantly better than `prs2`) or both negative (implying `prs2` is significantly better than `prs1`)."]},{"cell_type":"code","execution_count":10,"metadata":{"colab":{"height":101,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":6240,"status":"ok","timestamp":1717790014919,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"oRKgjH_uR2wr","outputId":"76474def-1edd-4cbd-c801-6b00f324f288"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" method_a method_b pearsonr pearsonr_std pearsonr_lower pearsonr_upper \\\n","0 prs1 prs2 0.014266 0.007112 0.000436 0.027211 \n","\n"," roc_auc roc_auc_std roc_auc_lower roc_auc_upper pr_auc pr_auc_std \\\n","0 0.008931 0.004466 0.000157 0.017171 0.010803 0.005761 \n","\n"," pr_auc_lower pr_auc_upper top10prev top10prev_std top10prev_lower \\\n","0 -0.00061 0.02107 0.005593 0.026971 -0.042589 \n","\n"," top10prev_upper \n","0 0.062382 "],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
method_amethod_bpearsonrpearsonr_stdpearsonr_lowerpearsonr_upperroc_aucroc_auc_stdroc_auc_lowerroc_auc_upperpr_aucpr_auc_stdpr_auc_lowerpr_auc_uppertop10prevtop10prev_stdtop10prev_lowertop10prev_upper
0prs1prs20.0142660.0071120.0004360.0272110.0089310.0044660.0001570.0171710.0108030.005761-0.000610.021070.0055930.026971-0.0425890.062382
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","summary":"{\n \"name\": \" as_dataframe=True)\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"method_a\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs1\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"method_b\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs2\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.014266467502054426,\n \"max\": 0.014266467502054426,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.014266467502054426\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.007111892690604321,\n \"max\": 0.007111892690604321,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.007111892690604321\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.00043626824886599245,\n \"max\": 0.00043626824886599245,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.00043626824886599245\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.027211089302840434,\n \"max\": 0.027211089302840434,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.027211089302840434\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.008930715859085309,\n \"max\": 0.008930715859085309,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.008930715859085309\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.004466363148919537,\n \"max\": 0.004466363148919537,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.004466363148919537\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.00015733124729375172,\n \"max\": 0.00015733124729375172,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.00015733124729375172\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.017170818130808965,\n \"max\": 0.017170818130808965,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.017170818130808965\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.010803102257625864,\n \"max\": 0.010803102257625864,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.010803102257625864\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.005760958016623593,\n \"max\": 0.005760958016623593,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.005760958016623593\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": -0.0006104367572841078,\n \"max\": -0.0006104367572841078,\n \"num_unique_values\": 1,\n \"samples\": [\n -0.0006104367572841078\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.02106968216083579,\n \"max\": 0.02106968216083579,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.02106968216083579\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.005592731111872085,\n \"max\": 0.005592731111872085,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.005592731111872085\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.026971273443313012,\n \"max\": 0.026971273443313012,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.026971273443313012\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": -0.04258910891089107,\n \"max\": -0.04258910891089107,\n \"num_unique_values\": 1,\n \"samples\": [\n -0.04258910891089107\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.062381770529994184,\n \"max\": 0.062381770529994184,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.062381770529994184\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":10}],"source":["get_prs_paired_eval_info(\n"," y_true=data_df['phenotype'],\n"," y_pred1=data_df['prs1'],\n"," y_pred2=data_df['prs2'],\n"," name1='prs1',\n"," name2='prs2',\n"," as_dataframe=True)"]}]} \ No newline at end of file +{"nbformat":4,"nbformat_minor":0,"metadata":{"colab":{"provenance":[],"authorship_tag":"ABX9TyNRP3nXabbiT4kQaBguzOs4"},"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"}},"cells":[{"cell_type":"code","source":["#@title Licensed under the BSD-3 License (the \"License\"); { display-mode: \"form\" }\n","# Copyright 2023 Google LLC.\n","#\n","# Redistribution and use in source and binary forms, with or without modification,\n","# are permitted provided that the following conditions are met:\n","#\n","# 1. Redistributions of source code must retain the above copyright notice, this\n","# list of conditions and the following disclaimer.\n","#\n","# 2. Redistributions in binary form must reproduce the above copyright notice,\n","# this list of conditions and the following disclaimer in the documentation\n","# and/or other materials provided with the distribution.\n","#\n","# 3. Neither the name of the copyright holder nor the names of its contributors\n","# may be used to endorse or promote products derived from this software without\n","# specific prior written permission.\n","#\n","# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n","# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n","# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n","# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\n","# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n","# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n","# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\n","# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n","# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n","# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."],"metadata":{"id":"vdFOGdpqPesl"},"execution_count":null,"outputs":[]},{"cell_type":"markdown","metadata":{"id":"VbyGa_IhXRgk"},"source":["# Preparation\n","\n","This section includes imports and functions."]},{"cell_type":"code","execution_count":null,"metadata":{"id":"otMyZHIW0Fqs"},"outputs":[],"source":["import dataclasses\n","from typing import Dict, List, Optional, Sequence, Union\n","\n","import abc\n","from typing import Callable\n","\n","import numpy as np\n","import pandas as pd\n","import scipy.stats\n","import sklearn\n","import sklearn.metrics\n","from sklearn import metrics"]},{"cell_type":"code","execution_count":null,"metadata":{"id":"J8pr2zMLzmDH"},"outputs":[],"source":["# A function that computes a numeric outcome from label and prediction arrays.\n","BootstrappableFn = Callable[[np.ndarray, np.ndarray], float]\n","\n","# Constants denoting the expected case and control values for binary encodings.\n","BINARY_LABEL_CONTROL = 0\n","BINARY_LABEL_CASE = 1\n","\n","class Metric(abc.ABC):\n"," \"\"\"Represents a callable wrapper class for a named metric function.\n","\n"," Attributes:\n"," name: The metric's name.\n"," \"\"\"\n","\n"," def __init__(self, name: str, fn: BootstrappableFn) -> None:\n"," \"\"\"Initializes the metric.\n","\n"," Args:\n"," name: The metric's name.\n"," fn: A function that computes an outcome from label and prediction arrays.\n"," The function's signature should accept a `y_true` label array and a\n"," `y_pred` model prediction array. This function is invoked when the\n"," `Metric` instance is called.\n"," \"\"\"\n"," self._name: str = name\n"," self._fn: BootstrappableFn = fn\n","\n"," @property\n"," def name(self) -> str:\n"," \"\"\"The `Metric`'s name.\"\"\"\n"," return self._name\n","\n"," @abc.abstractmethod\n"," def _validate(self, y_true: np.ndarray, y_pred: np.ndarray) -> None:\n"," \"\"\"Validates the `y_true` labels and `y_pred` predictions.\n","\n"," Note: Each prediction subarray `y_pred[i, ...]` at index `i` should\n"," correspond to the `y_true[i]` label.\n","\n"," Args:\n"," y_true: The ground truth label targets.\n"," y_pred: The target predictions.\n","\n"," Raises:\n"," ValueError: If the first dimension of `y_true` and `y_pred` do not match.\n"," \"\"\"\n"," if y_true.shape[0] != y_pred.shape[0]:\n"," raise ValueError('`y_true` and `y_pred` first dimension mismatch: '\n"," f'{y_true.shape[0]} != {y_pred.shape[0]}')\n","\n"," def __call__(self, y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Invokes the `Metric`'s function.\n","\n"," Args:\n"," y_true: The ground truth label values.\n"," y_pred: The target predictions.\n","\n"," Returns:\n"," The result of the `Metric.fn(y_true, y_pred)`.\n"," \"\"\"\n"," self._validate(y_true, y_pred)\n"," return self._fn(y_true, y_pred)\n","\n"," def __str__(self) -> str:\n"," return self.name\n","\n","\n","class ContinuousMetric(Metric):\n"," \"\"\"Represents a callable wrapper class for a named continuous label function.\n","\n"," Attributes:\n"," name: The metric's name.\n"," \"\"\"\n","\n"," # Note: This is a useful delegation since _validate is an @abc.abstractmethod.\n"," def _validate( # pylint: disable=useless-super-delegation\n"," self,\n"," y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," ) -> None:\n"," \"\"\"Validates the `y_true` labels and `y_pred` predictions.\n","\n"," Args:\n"," y_true: The ground truth label values.\n"," y_pred: The target predictions.\n","\n"," Raises:\n"," ValueError: If the first dimension of `y_true` and `y_pred` do not match.\n"," \"\"\"\n"," super()._validate(y_true, y_pred)\n","\n","\n","class BinaryMetric(Metric):\n"," \"\"\"Represents a callable wrapper class for a named binary label function.\n","\n"," This class asserts that the provided `y_true` labels are binary targets in\n"," `{0, 1}` and that `y_true` contains at least one element in each class, i.e.,\n"," not all samples are from the same class.\n","\n"," Attributes:\n"," name: The metric's name.\n"," \"\"\"\n","\n"," def _validate(self, y_true: np.ndarray, y_pred: np.ndarray) -> None:\n"," \"\"\"Validates the `y_true` labels and `y_pred` predictions.\n","\n"," Args:\n"," y_true: The ground truth label values.\n"," y_pred: The target predictions.\n","\n"," Raises:\n"," ValueError: If the first dimension of `y_true` and `y_pred` do not match.\n"," ValueError: If `y_true` labels are nonbinary, i.e., not all values are in\n"," `{BINARY_LABEL_CONTROL, BINARY_LABEL_CASE}` or if `y_true` does not\n"," contain at least one element from each class.\n"," \"\"\"\n"," super()._validate(y_true, y_pred)\n"," if not is_valid_binary_label(y_true):\n"," raise ValueError('`y_true` labels must be in `{BINARY_LABEL_CONTROL, '\n"," 'BINARY_LABEL_CASE}` and have at least one element from '\n"," f'each class; found: {y_true}')\n","\n","\n","def is_binary(metric: Metric) -> bool:\n"," \"\"\"Whether `metric` is a metric computed with binary `y_true` labels.\"\"\"\n"," return isinstance(metric, BinaryMetric)\n","\n","\n","def is_valid_binary_label(array: np.ndarray) -> bool:\n"," \"\"\"Whether `array` is a \"valid\" binary label array for bootstrapping.\n","\n"," We define a valid binary label array as an array that contains only binary\n"," values, i.e., `{BINARY_LABEL_CONTROL, BINARY_LABEL_CASE}`, and contains at\n"," least one value from each class.\n","\n"," Args:\n"," array: A numpy array.\n","\n"," Returns:\n"," Whether `array` is a \"valid\" binary label array.\n"," \"\"\"\n"," is_case_mask = array == BINARY_LABEL_CASE\n"," is_control_mask = array == BINARY_LABEL_CONTROL\n"," return (np.any(is_case_mask) and np.any(is_control_mask) and\n"," np.all(np.logical_or(is_case_mask, is_control_mask)))\n","\n","\n","def pearsonr(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the Pearson R correlation coefficient.\"\"\"\n"," # Note: We ignore the returned p value.\n"," r, _ = scipy.stats.pearsonr(y_true, y_pred)\n"," return r\n","\n","\n","def pearsonr_squared(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the square of the Pearson correlation coefficient.\"\"\"\n"," return pearsonr(y_true, y_pred)**2\n","\n","\n","def spearmanr(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the Spearman R correlation coefficient.\"\"\"\n"," # Note: We ignore the returned p value.\n"," r, _ = scipy.stats.spearmanr(y_true, y_pred)\n"," return r\n","\n","\n","def count(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," \"\"\"Returns the number of samples in `y_true`.\"\"\"\n"," if y_true.shape[0] != y_pred.shape[0]:\n"," raise ValueError('`y_true` and `y_pred` first dimension mismatch: '\n"," f'{y_true.shape[0]} != {y_pred.shape[0]}')\n"," return len(y_true)\n","\n","\n","def frequency_between(y_true: np.ndarray, y_pred: np.ndarray,\n"," percentile_lower: int, percentile_upper: int) -> float:\n"," \"\"\"Computes the positive class frequency within a percentile interval.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," percentile_lower: The lower bound (inclusive) of percentile. 0 to include\n"," all samples.\n"," percentile_upper: The upper bound (inclusive for 100, exclusive for all\n"," other values) of percentile. 100 to include all samples.\n","\n"," Returns:\n"," A [0.0, 1.0] float corresponding to the positive class frequency within\n"," the percentile interval.\n","\n"," Raises:\n"," ValueError: Invalid percentile range.\n"," \"\"\"\n"," if not 0 <= percentile_lower < 100:\n"," raise ValueError('`percentile_lower` must be in range `[0, 100)`: '\n"," f'{percentile_lower}')\n"," if not 0 < percentile_upper <= 100:\n"," raise ValueError('`percentile_upper` must be in range `(0, 100]`: '\n"," f'{percentile_upper}')\n","\n"," pred_lower_percentile, pred_upper_percentile = np.percentile(\n"," a=y_pred, q=[percentile_lower, percentile_upper])\n"," lower_mask = (y_pred >= pred_lower_percentile)\n"," if percentile_upper == 100:\n"," mask = lower_mask\n"," else:\n"," upper_mask = (y_pred < pred_upper_percentile)\n"," mask = lower_mask & upper_mask\n"," assert len(mask) == len(y_true)\n"," return np.mean(y_true[mask])\n","\n","\n","def frequency(y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," top_percentile: int = 100) -> float:\n"," \"\"\"Computes the positive class frequency within the top prediction percentile.\n","\n"," We select the subset of `y_true` labels corresponding to `y_pred`'s\n"," `top_percentile`-th prediction percetile and return the positive class\n"," frequency within this subset. `top_percentile=100` indicates the frequency for\n"," all samples.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," top_percentile: Determines the set of examples considered in the frequency\n"," calculation. The top percentile represents the top percentile by\n"," prediction risk. 100 indicates using all samples.\n","\n"," Returns:\n"," A [0.0, 1.0] float corresponding to the positive class frequency in the top\n"," percentile.\n","\n"," Raises:\n"," ValueError: `top_percentile` is not in range `(0, 100]`.\n"," \"\"\"\n"," if not 0 < top_percentile <= 100:\n"," raise ValueError('`top_percentile` must be in range `(0, 100]`: '\n"," f'{top_percentile}')\n","\n"," return frequency_between(\n"," y_true,\n"," y_pred,\n"," percentile_lower=100 - top_percentile,\n"," percentile_upper=100)\n","\n","\n","def frequency_fn(top_percentile: int) -> BootstrappableFn:\n"," \"\"\"Returns a function that computes `frequency` at `top_percentile`.\"\"\"\n","\n"," def _frequency(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," return frequency(y_true, y_pred, top_percentile)\n","\n"," return _frequency\n","\n","\n","def frequency_between_fn(percentile_lower: int,\n"," percentile_upper: int) -> BootstrappableFn:\n"," \"\"\"Returns a function that computes `frequency` in a percentile interval.\"\"\"\n","\n"," def _freq_between(y_true: np.ndarray, y_pred: np.ndarray) -> float:\n"," return frequency_between(\n"," y_true,\n"," y_pred,\n"," percentile_lower=percentile_lower,\n"," percentile_upper=percentile_upper)\n","\n"," return _freq_between"]},{"cell_type":"code","execution_count":null,"metadata":{"id":"M33VPEMF0sGd"},"outputs":[],"source":["# Represents a numpy array of indices for a single bootstrap sample.\n","IndexSample = np.ndarray\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class NamedArray:\n"," \"\"\"Represents a named numpy array.\n","\n"," Attributes:\n"," name: The array name.\n"," values: A numpy array.\n"," \"\"\"\n","\n"," name: str\n"," values: np.ndarray\n","\n"," def __post_init__(self):\n"," if not self.name:\n"," raise ValueError('`name` must be specified.')\n","\n"," def __len__(self) -> int:\n"," return len(self.values)\n","\n"," def __str__(self) -> str:\n"," return f'{self.__class__.__name__}({self.name})'\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class Label(NamedArray):\n"," \"\"\"Represents a named numpy array of ground truth label targets.\n","\n"," Attributes:\n"," name: The label name.\n"," values: A numpy array containing ground truth label targets.\n"," \"\"\"\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class Prediction(NamedArray):\n"," \"\"\"Represents a named numpy array of target predictions.\n","\n"," Attributes:\n"," model_name: The name of the model that generated the predictions.\n"," name: The name of the predictions (e.g., the prediction column).\n"," values: A numpy array containing model predictions.\n"," \"\"\"\n","\n"," model_name: str\n","\n"," def __post_init__(self):\n"," super().__post_init__()\n"," if not self.model_name:\n"," raise ValueError('`model_name` must be specified.')\n","\n"," def __str__(self) -> str:\n"," return f'{self.__class__.__name__}({self.model_name}.{self.name})'\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class SampleMean:\n"," \"\"\"Represents an estimate of the population mean for a given sample.\n","\n"," Attributes:\n"," mean: The mean of a given sample.\n"," stddev: The standard deviation of the sample mean.\n"," num_samples: The number of samples used to calculate `mean` and `stddev`.\n","\n"," Raises:\n"," ValueError: If `num_samples` is not >= `1`.\n"," ValueError: If `stddev` is not `0` when `num_samples` is `1`.\n"," \"\"\"\n","\n"," mean: float\n"," stddev: float\n"," num_samples: int\n","\n"," def __post_init__(self):\n"," # Ensure we have a valid number of samples.\n"," if self.num_samples < 1:\n"," raise ValueError(f'`num_samples` must be >= `1`: {self.num_samples}')\n","\n"," # Ensure the standard deviation is 0 given a single sample.\n"," if self.num_samples == 1 and self.stddev != 0.0:\n"," raise ValueError(\n"," f'`stddev` must be `0` if `num_samples` is `1`: {self.stddev:0.4f}'\n"," )\n","\n"," def __str__(self) -> str:\n"," return f'{self.mean:0.4f} (SD={self.stddev:0.4f}, n={self.num_samples})'\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class ConfidenceInterval(SampleMean):\n"," \"\"\"Represents a confidence interval (CI) for a sample mean.\n","\n"," Attributes:\n"," mean: The mean of a given sample.\n"," stddev: The standard deviation of the sample mean.\n"," num_samples: The number of samples used to calculate `mean` and `stddev`.\n"," level: The confidence level at which the CI is calculated (e.g., 95).\n"," ci_lower: The lower limit of the `level` confidence interval.\n"," ci_upper: The upper limit of the `level` confidence interval.\n","\n"," Raises:\n"," ValueError: If `num_samples` is not >= `1`.\n"," ValueError: If `stddev` is not `0` when `num_samples` is `1`.\n"," ValueError: If `level` is not in range (0, 100].\n"," ValueError: If `ci_lower` or `ci_upper` does not match not `mean` when\n"," `num_samples` is `1`.\n"," \"\"\"\n","\n"," level: float\n"," ci_lower: float\n"," ci_upper: float\n","\n"," def __post_init__(self):\n"," super().__post_init__()\n"," # Ensure we have a valid confidence level.\n"," if not 0 < self.level <= 100:\n"," raise ValueError(f'`level` must be in range (0, 100]: {self.level:0.2f}')\n","\n"," # Ensure confidence intervals match the sample mean given a single sample.\n"," if self.num_samples == 1:\n"," if (self.ci_lower != self.mean) or (self.ci_upper != self.mean):\n"," raise ValueError(\n"," '`ci_lower` and `ci_upper` must match `mean` if `num_samples` is '\n"," f'1: mean={self.mean:0.4f}, ci_lower={self.ci_lower:0.4f}, '\n"," f'ci_upper={self.ci_upper:0.4f}'\n"," )\n","\n"," def __str__(self) -> str:\n"," return (\n"," f'{self.mean:0.4f} (SD={self.stddev:0.4f}, n={self.num_samples}, '\n"," f'{self.level:0>6.2f}% CI=[{self.ci_lower:0.4f}, '\n"," f'{self.ci_upper:0.4f}])'\n"," )\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class Result:\n"," \"\"\"Represents a bootstrapped metric result for an individual model.\n","\n"," Attributes:\n"," model_name: The model's name.\n"," prediction_name: The model's prediction name (e.g., the model head's name or\n"," the label name used in training).\n"," metric_name: The metric's name.\n"," ci: A confidence interval describing the distribution of metric samples.\n"," \"\"\"\n","\n"," model_name: str\n"," prediction_name: str\n"," metric_name: str\n"," ci: ConfidenceInterval\n","\n"," def __post_init__(self):\n"," # Ensure model, prediction, and metric names are specified.\n"," if not self.model_name:\n"," raise ValueError('`model_name` must be specified.')\n"," if not self.prediction_name:\n"," raise ValueError('`prediction_name` must be specified.')\n"," if not self.metric_name:\n"," raise ValueError('`metric_name` must be specified.')\n","\n"," def __str__(self) -> str:\n"," return (\n"," f'{self.model_name}.{self.prediction_name}: '\n"," f'{self.metric_name}: {self.ci}'\n"," )\n","\n","\n","@dataclasses.dataclass(eq=False, order=False, frozen=True)\n","class PairedResult:\n"," \"\"\"Represents a paired bootstrapped metric result for two models.\n","\n"," Attributes:\n"," model_name_a: The first model's name.\n"," prediction_name_a: The first model's prediction name (e.g., the model head's\n"," name or the label name used in training).\n"," model_name_b: The second model's name.\n"," prediction_name_b: The second model's prediction name (e.g., the model\n"," head's name or the label name used in training).\n"," metric_name: The metric's name.\n"," ci: A confidence interval describing the distribution of differences between\n"," the first and second models' metric samples.\n"," \"\"\"\n","\n"," model_name_a: str\n"," prediction_name_a: str\n"," model_name_b: str\n"," prediction_name_b: str\n"," metric_name: str\n"," ci: ConfidenceInterval\n","\n"," def __post_init__(self):\n"," # Ensure model, prediction, and metric names are specified.\n"," if not self.model_name_a:\n"," raise ValueError('`model_name_a` must be specified.')\n"," if not self.prediction_name_a:\n"," raise ValueError('`prediction_name_a` must be specified.')\n"," if not self.model_name_b:\n"," raise ValueError('`model_name_b` must be specified.')\n"," if not self.prediction_name_b:\n"," raise ValueError('`prediction_name_b` must be specified.')\n"," if not self.metric_name:\n"," raise ValueError('`metric_name` must be specified.')\n","\n"," def __str__(self) -> str:\n"," return (\n"," f'({self.model_name_a}.{self.prediction_name_a} - '\n"," f'{self.model_name_b}.{self.prediction_name_b}): '\n"," f'{self.metric_name}: {self.ci}'\n"," )\n","\n","\n","def _reverse_paired_result(paired_result: PairedResult) -> PairedResult:\n"," \"\"\"Returns the \"(b - a)\" inverse of an \"(a - b)\" `PairedResult`.\"\"\"\n"," reversed_ci = ConfidenceInterval(\n"," mean=(paired_result.ci.mean * -1),\n"," stddev=paired_result.ci.stddev,\n"," num_samples=paired_result.ci.num_samples,\n"," level=paired_result.ci.level,\n"," ci_upper=(paired_result.ci.ci_lower * -1),\n"," ci_lower=(paired_result.ci.ci_upper * -1),\n"," )\n"," reversed_paired_result = PairedResult(\n"," model_name_a=paired_result.model_name_b,\n"," prediction_name_a=paired_result.prediction_name_b,\n"," model_name_b=paired_result.model_name_a,\n"," prediction_name_b=paired_result.prediction_name_a,\n"," metric_name=paired_result.metric_name,\n"," ci=reversed_ci,\n"," )\n"," return reversed_paired_result\n","\n","\n","def _compute_confidence_interval(\n"," samples: np.ndarray,\n"," ci_level: float,\n",") -> ConfidenceInterval:\n"," \"\"\"Computes the mean, standard deviation, and confidence interval for samples.\n","\n"," Args:\n"," samples: A boostrapped array of observed sample values.\n"," ci_level: The confidence level/width of the desired confidence interval.\n","\n"," Returns:\n"," A `Result` containing the mean, standard deviation, and the `ci_level`%\n"," confidence interval for the observed sample values.\n"," \"\"\"\n"," sample_mean = np.mean(samples, axis=0)\n"," sample_std = np.std(samples, axis=0)\n","\n"," lower_percentile = (100 - ci_level) / 2\n"," upper_percentile = 100 - lower_percentile\n"," percentiles = [lower_percentile, upper_percentile]\n"," ci_lower, ci_upper = np.percentile(a=samples, q=percentiles, axis=0)\n","\n"," ci = ConfidenceInterval(\n"," mean=sample_mean,\n"," stddev=sample_std,\n"," num_samples=len(samples),\n"," level=ci_level,\n"," ci_lower=ci_lower,\n"," ci_upper=ci_upper,\n"," )\n","\n"," return ci\n","\n","\n","def _generate_sample_indices(\n"," label: Label,\n"," is_binary: bool,\n"," num_bootstrap: int,\n"," seed: int,\n",") -> List[IndexSample]:\n"," \"\"\"Returns a list of `num_bootstrap` randomly sampled bootstrap indices.\n","\n"," Args:\n"," label: The ground truth label targets.\n"," is_binary: Whether to generate valid binary samples; i.e., each index sample\n"," contains at least one index corresponding to a label from each class.\n"," num_bootstrap: The number of bootstrap indices to generate.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A list of `num_bootstrap` bootstrap sample indices.\n"," \"\"\"\n"," rng = np.random.default_rng(seed)\n"," num_observations = len(label)\n"," sample_indices = []\n"," while len(sample_indices) < num_bootstrap:\n"," index = rng.integers(0, high=num_observations, size=num_observations)\n"," sample_true = label.values[index]\n"," # If computing a binary metric, skip indices that result in invalid labels.\n"," if is_binary and not is_valid_binary_label(sample_true):\n"," continue\n"," sample_indices.append(index)\n"," return sample_indices\n","\n","\n","def _compute_metric_samples(\n"," metric: Metric,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," sample_indices: Sequence[np.ndarray],\n",") -> Dict[str, np.ndarray]:\n"," \"\"\"Generates `num_bootstrap` metric samples for each `Prediction`.\n","\n"," Note: This method assumes that label and prediction values are orded so that\n"," the value at index `i` in a given `Prediction` corresponds to the label value\n"," at index `i` in `label`. Both the `Label` and `Prediction` arrays are indexed\n"," using the given `sample_indices`.\n","\n"," Args:\n"," metric: An instance of a bootstrappable `Metric`; used to compute samples.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," sample_indices: An array of bootstrap sample indices. If empty, returns the\n"," single value computed on the entire dataset for each prediction.\n","\n"," Returns:\n"," A mapping of model names to the corresponding metric samples array.\n"," \"\"\"\n"," if not sample_indices:\n"," metric_samples = {}\n"," for prediction in predictions:\n"," value = metric(label.values, prediction.values)\n"," metric_samples[prediction.model_name] = np.asarray([value])\n"," return metric_samples\n","\n"," metric_samples = {prediction.model_name: [] for prediction in predictions}\n"," for index in sample_indices:\n"," sample_true = label.values[index]\n"," for prediction in predictions:\n"," sample_value = metric(sample_true, prediction.values[index])\n"," metric_samples[prediction.model_name].append(sample_value)\n","\n"," metric_samples = {\n"," name: np.asarray(samples) for name, samples in metric_samples.items()\n"," }\n","\n"," return metric_samples\n","\n","\n","def _compute_all_metric_samples(\n"," metrics: Sequence[Metric],\n"," contains_binary_metric: bool,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," num_bootstrap: int,\n"," seed: int,\n",") -> Dict[str, Dict[str, np.ndarray]]:\n"," \"\"\"Generates `num_bootstrap` samples for each `Prediction` and `Metric`.\n","\n"," Args:\n"," metrics: A sequence of a bootstrappable `Metric` instances.\n"," contains_binary_metric: Whether the set of metrics contains a binary metric.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," num_bootstrap: The number of bootstrap iterations.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A mapping of metric names to model-sample dictionaries.\n"," \"\"\"\n"," sample_indices = _generate_sample_indices(\n"," label,\n"," contains_binary_metric,\n"," num_bootstrap,\n"," seed,\n"," )\n"," metric_samples = []\n"," for metric in metrics:\n"," metric_samples.append(\n"," _compute_metric_samples(\n"," metric=metric,\n"," label=label,\n"," predictions=predictions,\n"," sample_indices=sample_indices,\n"," )\n"," )\n","\n"," return {\n"," metric.name: metric_sample\n"," for metric, metric_sample in zip(metrics, metric_samples)\n"," }\n","\n","\n","def _process_metric_samples(\n"," metric: Metric,\n"," predictions: Sequence[Prediction],\n"," model_names_to_metric_samples: Dict[str, np.ndarray],\n"," ci_level: float,\n",") -> List[Result]:\n"," \"\"\"Compute `ConfidenceInterval`s for metric samples across predictions.\"\"\"\n"," results = []\n"," for prediction in predictions:\n"," metric_samples = model_names_to_metric_samples[prediction.model_name]\n"," ci = _compute_confidence_interval(metric_samples, ci_level)\n"," result = Result(prediction.model_name, prediction.name, metric.name, ci)\n"," results.append(result)\n"," return results\n","\n","\n","def _process_metric_samples_paired(\n"," metric: Metric,\n"," predictions: Sequence[Prediction],\n"," model_names_to_metric_samples: Dict[str, np.ndarray],\n"," ci_level: float,\n",") -> List[PairedResult]:\n"," \"\"\"Compute `ConfidenceInterval`s for paired samples across predictions.\"\"\"\n"," results = []\n"," for i, prediction_a in enumerate(predictions[:-1]):\n"," for prediction_b in predictions[i + 1 :]:\n"," # Compute the result of `prediction_a - prediction_b`.\n"," metric_samples_a = model_names_to_metric_samples[prediction_a.model_name]\n"," metric_samples_b = model_names_to_metric_samples[prediction_b.model_name]\n"," metric_samples_diff = metric_samples_a - metric_samples_b\n"," ci = _compute_confidence_interval(metric_samples_diff, ci_level)\n"," result = PairedResult(\n"," prediction_a.model_name,\n"," prediction_a.name,\n"," prediction_b.model_name,\n"," prediction_b.name,\n"," metric.name,\n"," ci,\n"," )\n"," results.append(result)\n"," # Derive and include the result of `prediction_b - prediction_a`.\n"," results.append(_reverse_paired_result(result))\n"," return results\n","\n","\n","def _bootstrap(\n"," metrics: Sequence[Metric],\n"," contains_binary_metric: bool,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," num_bootstrap: int,\n"," ci_level: float,\n"," seed: int,\n",") -> Dict[str, List[Result]]:\n"," \"\"\"Performs bootstrapping for all models using the given metrics.\n","\n"," Args:\n"," metrics: A sequence of a bootstrappable `Metric` instances.\n"," contains_binary_metric: Whether the set of metrics contains a binary metric.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," num_bootstrap: The number of bootstrap iterations.\n"," ci_level: The confidence level/width of the desired confidence interval.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A dictionary mapping metric names to a list of `Result`s containing the mean\n"," metric values of each model over `num_bootstrap` bootstrapping iterations.\n"," \"\"\"\n"," metric_to_model_to_samples = _compute_all_metric_samples(\n"," metrics,\n"," contains_binary_metric,\n"," label,\n"," predictions,\n"," num_bootstrap,\n"," seed,\n"," )\n"," metric_samples = []\n"," for metric in metrics:\n"," metric_samples.append(\n"," _process_metric_samples(\n"," metric=metric,\n"," predictions=predictions,\n"," model_names_to_metric_samples=metric_to_model_to_samples[\n"," metric.name\n"," ],\n"," ci_level=ci_level,\n"," )\n"," )\n","\n"," return {\n"," metric.name: metric_sample\n"," for metric, metric_sample in zip(metrics, metric_samples)\n"," }\n","\n","\n","def _paired_bootstrap(\n"," metrics: Sequence[Metric],\n"," contains_binary_metric: bool,\n"," label: Label,\n"," predictions: Sequence[Prediction],\n"," num_bootstrap: int,\n"," ci_level: float,\n"," seed: int,\n",") -> Dict[str, List[PairedResult]]:\n"," \"\"\"Performs paired bootstrapping for all models using the given metrics.\n","\n"," Args:\n"," metrics: A sequence of a bootstrappable `Metric` instances.\n"," contains_binary_metric: Whether the set of metrics contains a binary metric.\n"," label: The ground truth label targets.\n"," predictions: A list of target predictions from a set of models.\n"," num_bootstrap: The number of bootstrap iterations.\n"," ci_level: The confidence level/width of the desired confidence interval.\n"," seed: The random seed; set prior to generating bootstrap indices.\n","\n"," Returns:\n"," A dictionary mapping metric names to `PairedResult`s containing the mean\n"," metric difference between models over `num_bootstrap` bootstrapping\n"," iterations.\n"," \"\"\"\n"," metric_to_model_to_samples = _compute_all_metric_samples(\n"," metrics,\n"," contains_binary_metric,\n"," label,\n"," predictions,\n"," num_bootstrap,\n"," seed,\n"," )\n"," metric_samples = []\n"," for metric in metrics:\n"," metric_samples.append(\n"," _process_metric_samples_paired(\n"," metric=metric,\n"," predictions=predictions,\n"," model_names_to_metric_samples=metric_to_model_to_samples[\n"," metric.name\n"," ],\n"," ci_level=ci_level,\n"," )\n"," )\n","\n"," return {\n"," metric.name: metric_sample\n"," for metric, metric_sample in zip(metrics, metric_samples)\n"," }\n","\n","\n","def _default_binary_metrics() -> List[BinaryMetric]:\n"," \"\"\"Returns `PerformanceMetrics`'s default metrics for binary target.\"\"\"\n"," metrics = [\n"," BinaryMetric('num', count),\n"," BinaryMetric('auc', sklearn.metrics.roc_auc_score),\n"," BinaryMetric('auprc', sklearn.metrics.average_precision_score),\n"," ]\n"," for percentile in [100, 10, 5, 1]:\n"," metrics.append(\n"," BinaryMetric(\n"," f'freq@{percentile:>03}%',\n"," frequency_fn(percentile),\n"," )\n"," )\n"," return metrics\n","\n","\n","def _default_continuous_metrics() -> List[ContinuousMetric]:\n"," \"\"\"Returns `PerformanceMetrics`'s default metrics for continuous target.\"\"\"\n"," metrics = [\n"," ContinuousMetric('num', count),\n"," ContinuousMetric('pearson', pearsonr),\n"," ContinuousMetric('pearsonr_squared', pearsonr_squared),\n"," ContinuousMetric('spearman', spearmanr),\n"," ContinuousMetric('mse', sklearn.metrics.mean_squared_error),\n"," ContinuousMetric('mae', sklearn.metrics.mean_absolute_error),\n"," ]\n"," return metrics\n","\n","\n","def _default_metrics(binary_targets: bool) -> List[Metric]:\n"," \"\"\"Returns `PerformanceMetrics`'s default set of metrics for the target type.\n","\n"," Args:\n"," binary_targets: Whether the target labels are binary. If false, the returned\n"," metrics assume continuous labels.\n","\n"," Returns:\n"," The default set of binary or continuous `bootstrap_metrics.Metric`s.\n"," \"\"\"\n"," if binary_targets:\n"," return _default_binary_metrics()\n"," return _default_continuous_metrics()\n","\n","\n","class PerformanceMetrics:\n"," \"\"\"A named collection of invocable, bootstrapable `Metric`s.\n","\n"," Initializes a class that applies the given `Metric` functions to new ground\n"," truth labels and predictions. `Metric`s can be evaluated with and without\n"," bootstrapping.\n","\n"," The default metrics are number of samples, auc, auprc, and frequency\n"," calculations for the top 100/10/5/1 top percentiles, if `default_metrics` is\n"," 'binary'. If `default_metrics` is 'continuous', the default metrics are\n"," Pearson and Spearman correlations, the square of the Pearson correlation, mean\n"," squared error (MSE) and mean absolute error (MAE).\n","\n"," TODO(b/199452239): Refactor `PerformanceMetrics` so that the default metric\n"," set is not parameterized with a string.\n","\n"," Raises:\n"," ValueError: if an item in `metrics` is not of type `Metric`.\n"," \"\"\"\n","\n"," def __init__(\n"," self,\n"," name: str,\n"," default_metrics: Optional[str] = None,\n"," metrics: Optional[List[Metric]] = None,\n"," ) -> None:\n","\n"," if metrics is None:\n"," if default_metrics is None:\n"," raise ValueError('`default_metrics` is None and no metric is provided.')\n"," elif default_metrics == 'binary':\n"," metrics = _default_metrics(binary_targets=True)\n"," elif default_metrics == 'continuous':\n"," metrics = _default_metrics(binary_targets=False)\n"," else:\n"," raise ValueError(\n"," 'unknown `default_metrics`: {}'.format(default_metrics)\n"," )\n","\n"," for metric in metrics:\n"," if not isinstance(metric, Metric):\n"," raise ValueError('Invalid metric value: must be of class `Metric`.')\n","\n"," if len(metrics) != len({metric.name for metric in metrics}):\n"," raise ValueError(f'Metric names must be unique: {metrics}')\n","\n"," self.name = name\n"," self.metrics = metrics\n"," self.contains_binary = any(is_binary(m) for m in metrics)\n","\n"," def compute(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," ) -> Dict[str, Result]:\n"," \"\"\"Evaluates all metrics using the given labels and predictions.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," mask: A boolean mask; applied to `y_true` and `y_pred`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n","\n"," Returns:\n"," A dictionary of bootstrapped metrics keyed on metric name with\n"," `Result` values.\n","\n"," Raises:\n"," ValueError: If the dimensions of `y_true`, `y_pred`, or `mask` do not\n"," match, or labels are not in {0 , 1}.\n"," \"\"\"\n"," if len(y_true) != len(y_pred):\n"," raise ValueError('Label and prediction dimensions do not match.')\n","\n"," if mask is not None and len(mask) != len(y_pred):\n"," raise ValueError('Label and prediction dimensions do not match mask.')\n","\n"," if mask is not None:\n"," y_true = y_true[mask]\n"," y_pred = y_pred[mask]\n","\n"," # TODO(b/197539434): Pipe through non-empty names after public api refactor.\n"," label_name = 'label'\n"," label = Label(label_name, y_true)\n"," predictions = [Prediction(label_name, y_pred, 'model')]\n","\n"," metric_results = _bootstrap(\n"," self.metrics,\n"," contains_binary_metric=self.contains_binary,\n"," label=label,\n"," predictions=predictions,\n"," num_bootstrap=n_bootstrap,\n"," ci_level=conf_interval,\n"," seed=seed,\n"," )\n","\n"," # TODO(b/197539434): Remove temporary asserts after public api refactor.\n"," final_results = {}\n"," for metric_name, results in metric_results.items():\n"," assert len(results) == 1\n"," final_results[metric_name] = results[0]\n","\n"," return final_results\n","\n"," def compute_paired(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred_a: np.ndarray,\n"," y_pred_b: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," ) -> Dict[str, PairedResult]:\n"," \"\"\"Computes a paired bootstrap value for each metric.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred_a: Target predictions from model A; compared to `y_pred_b`.\n"," y_pred_b: Target predictions from model B; compared to `y_pred_a`.\n"," mask: A boolean mask; applied to `y_true`, `y_pred_a`, and `y_pred_b`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n","\n"," Returns:\n"," A dictionary of paired bootstrapped metrics keyed on metric name with\n"," `PairedResult` values.\n","\n"," Raises:\n"," ValueError: If the dimensions of `y_true`, `y_pred_a`, `y_pred_b` or\n"," `mask` do not match, or labels are not in {0 , 1}.\n"," \"\"\"\n"," if (len(y_true) != len(y_pred_a)) or (len(y_true) != len(y_pred_b)):\n"," raise ValueError('Label and prediction dimensions do not match.')\n","\n"," if mask is not None and len(mask) != len(y_pred_a):\n"," raise ValueError('Label and prediction dimensions do not match mask.')\n","\n"," if mask is not None:\n"," y_true = y_true[mask]\n"," y_pred_a = y_pred_a[mask]\n"," y_pred_b = y_pred_b[mask]\n","\n"," # TODO(b/197539434): Pipe through non-empty names after public api refactor.\n"," label_name = 'label'\n"," label = Label(label_name, y_true)\n"," first_model_name = 'model_a'\n"," predictions = [\n"," Prediction(label_name, y_pred_a, first_model_name),\n"," Prediction(label_name, y_pred_b, 'model_b'),\n"," ]\n","\n"," metric_results = _paired_bootstrap(\n"," self.metrics,\n"," contains_binary_metric=self.contains_binary,\n"," label=label,\n"," predictions=predictions,\n"," num_bootstrap=n_bootstrap,\n"," ci_level=conf_interval,\n"," seed=seed,\n"," )\n","\n"," # TODO(b/197539434): Remove temporary asserts after public api refactor.\n"," final_results = {}\n"," for metric_name, results in metric_results.items():\n"," assert len(results) == 2\n"," assert results[0].model_name_a == first_model_name\n"," final_results[metric_name] = results[0]\n","\n"," return final_results\n","\n"," def _print_results(\n"," self,\n"," title: str,\n"," results: Dict[str, Union[Result, PairedResult]],\n"," ) -> None:\n"," \"\"\"Prints each result object under the current name and given title.\"\"\"\n"," print(f'{self.name}: {title}')\n"," for _, result in sorted(results.items()):\n"," print(f'\\t{result}')\n","\n"," def compute_and_print(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," title: str = '',\n"," ) -> None:\n"," \"\"\"Evaluates and pretty-prints metrics using given labels and predictions.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred: Estimated targets as returned by a classifier.\n"," mask: A boolean mask; applied to `y_true` and `y_pred`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n"," title: A title appended to the printed evaluation metrics.\n","\n"," Raises:\n"," ValueError: If any of `y_true`, `y_pred`, or `mask` are not of type\n"," numpy.array of if their dimensions do not match.\n"," \"\"\"\n"," results = self.compute(\n"," y_true,\n"," y_pred,\n"," mask=mask,\n"," n_bootstrap=n_bootstrap,\n"," conf_interval=conf_interval,\n"," seed=seed,\n"," )\n"," self._print_results(title, results)\n","\n"," def compute_paired_and_print(\n"," self,\n"," y_true: np.ndarray,\n"," y_pred_a: np.ndarray,\n"," y_pred_b: np.ndarray,\n"," mask: Optional[np.ndarray] = None,\n"," n_bootstrap: int = 0,\n"," conf_interval: float = 95,\n"," seed: int = 42,\n"," title: str = '',\n"," **kwargs,\n"," ) -> None:\n"," \"\"\"Evaluates and pretty-prints paired metrics.\n","\n"," Args:\n"," y_true: Ground truth (correct) target values.\n"," y_pred_a: Target predictions from model A; compared to `y_pred_b`.\n"," y_pred_b: Target predictions from model B; compared to `y_pred_a`.\n"," mask: A boolean mask; applied to `y_true`, `y_pred_a`, and `y_pred_b`.\n"," n_bootstrap: An integer denoting the number of bootstrap iterations for\n"," each evaluation metric.\n"," conf_interval: A float denoting the width of confidence interval.\n"," seed: An int denoting the seed for the PRNG.\n"," title: A title appended to the printed evaluation metrics.\n"," **kwargs: Additional keyword arguments passed to each Metric's `func`.\n"," \"\"\"\n"," results = self.compute_paired(\n"," y_true,\n"," y_pred_a,\n"," y_pred_b,\n"," mask=mask,\n"," n_bootstrap=n_bootstrap,\n"," conf_interval=conf_interval,\n"," seed=seed,\n"," **kwargs,\n"," )\n"," self._print_results(title, results)"]},{"cell_type":"code","execution_count":null,"metadata":{"id":"x4222NTc0xpR"},"outputs":[],"source":["N_BOOTSTRAP = 300\n","BOOTSTRAP_METRICS_LIST = [\n"," BinaryMetric('roc_auc', metrics.roc_auc_score),\n"," BinaryMetric('pr_auc', metrics.average_precision_score),\n"," ContinuousMetric('pearsonr', pearsonr),\n"," BinaryMetric('top10prev', frequency_fn(10)),\n","]\n","\n","def get_prs_eval_info(y_true, y_pred, name, as_dataframe=False):\n"," performance_metrics = PerformanceMetrics(\n"," 'Metrics', metrics=BOOTSTRAP_METRICS_LIST)\n"," performance_metrics_values = performance_metrics.compute(\n"," y_true=y_true,\n"," y_pred=y_pred,\n"," n_bootstrap=N_BOOTSTRAP,\n"," )\n"," # print(performance_metrics_values, flush=True)\n"," roc_auc_ci = performance_metrics_values['roc_auc'].ci\n"," pr_auc_ci = performance_metrics_values['pr_auc'].ci\n"," pearsonr_ci = performance_metrics_values['pearsonr'].ci\n"," top10prev_ci = performance_metrics_values['top10prev'].ci\n"," info = {\n"," 'method': name,\n"," 'pearsonr': pearsonr_ci.mean,\n"," 'pearsonr_std': pearsonr_ci.stddev,\n"," 'pearsonr_lower': pearsonr_ci.ci_lower,\n"," 'pearsonr_upper': pearsonr_ci.ci_upper,\n"," 'roc_auc': roc_auc_ci.mean,\n"," 'roc_auc_std': roc_auc_ci.stddev,\n"," 'roc_auc_lower': roc_auc_ci.ci_lower,\n"," 'roc_auc_upper': roc_auc_ci.ci_upper,\n"," 'pr_auc': pr_auc_ci.mean,\n"," 'pr_auc_std': pr_auc_ci.stddev,\n"," 'pr_auc_lower': pr_auc_ci.ci_lower,\n"," 'pr_auc_upper': pr_auc_ci.ci_upper,\n"," 'top10prev': top10prev_ci.mean,\n"," 'top10prev_std': top10prev_ci.stddev,\n"," 'top10prev_lower': top10prev_ci.ci_lower,\n"," 'top10prev_upper': top10prev_ci.ci_upper,\n"," }\n"," if as_dataframe:\n"," return pd.DataFrame(info, index=[0])\n"," else:\n"," return info\n","\n","\n","def get_prs_paired_eval_info(y_true,\n"," y_pred1,\n"," y_pred2,\n"," name1,\n"," name2,\n"," as_dataframe=False):\n"," performance_metrics = PerformanceMetrics(\n"," 'Metrics', metrics=BOOTSTRAP_METRICS_LIST)\n"," performance_metrics_values_paired = performance_metrics.compute_paired(\n"," y_true=y_true,\n"," y_pred_a=y_pred1,\n"," y_pred_b=y_pred2,\n"," n_bootstrap=N_BOOTSTRAP,\n"," )\n"," # print(performance_metrics_values_paired, flush=True)\n"," roc_auc_ci = performance_metrics_values_paired['roc_auc'].ci\n"," pr_auc_ci = performance_metrics_values_paired['pr_auc'].ci\n"," pearsonr_ci = performance_metrics_values_paired['pearsonr'].ci\n"," top10prev_ci = performance_metrics_values_paired['top10prev'].ci\n"," info = {\n"," 'method_a': name1,\n"," 'method_b': name2,\n"," 'pearsonr': pearsonr_ci.mean,\n"," 'pearsonr_std': pearsonr_ci.stddev,\n"," 'pearsonr_lower': pearsonr_ci.ci_lower,\n"," 'pearsonr_upper': pearsonr_ci.ci_upper,\n"," 'roc_auc': roc_auc_ci.mean,\n"," 'roc_auc_std': roc_auc_ci.stddev,\n"," 'roc_auc_lower': roc_auc_ci.ci_lower,\n"," 'roc_auc_upper': roc_auc_ci.ci_upper,\n"," 'pr_auc': pr_auc_ci.mean,\n"," 'pr_auc_std': pr_auc_ci.stddev,\n"," 'pr_auc_lower': pr_auc_ci.ci_lower,\n"," 'pr_auc_upper': pr_auc_ci.ci_upper,\n"," 'top10prev': top10prev_ci.mean,\n"," 'top10prev_std': top10prev_ci.stddev,\n"," 'top10prev_lower': top10prev_ci.ci_lower,\n"," 'top10prev_upper': top10prev_ci.ci_upper,\n"," }\n"," if as_dataframe:\n"," return pd.DataFrame(info, index=[0])\n"," else:\n"," return info"]},{"cell_type":"markdown","metadata":{"id":"NOaueJxRPmpG"},"source":["# Simulated data generation\n","\n","In this code example, we generate some simulated data (N=1,000) to demonstrate how to use the above code snippet to compute various metrics in the PRS evaluation part of the paper."]},{"cell_type":"code","execution_count":null,"metadata":{"id":"iXHTm8dxzY2H"},"outputs":[],"source":["np.random.seed(42)\n","individual_prs1 = np.random.normal(size=(1000,))\n","individual_prs2 = 0.8 * individual_prs1 + 0.2 * np.random.normal(size=(1000,))\n","individual_phenotype = 0.3 * individual_prs1 + 0.7 * np.random.normal(\n"," size=(1000,)\n",")\n","individual_phenotype = (individual_phenotype >= 0).astype(int)\n","\n","data_df = pd.DataFrame({\n"," 'prs1': individual_prs1,\n"," 'prs2': individual_prs2,\n"," 'phenotype': individual_phenotype,\n","})"]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"height":206,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":13,"status":"ok","timestamp":1717789982064,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"bzdHe1jqULbv","outputId":"f8e850ec-2fdf-45fb-b2be-f4e7ebe5cafa"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" prs1 prs2 phenotype\n","0 0.496714 0.677242 0\n","1 -0.138264 0.074315 0\n","2 0.647689 0.530077 0\n","3 1.523030 1.089037 1\n","4 -0.234153 -0.047678 0"],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
prs1prs2phenotype
00.4967140.6772420
1-0.1382640.0743150
20.6476890.5300770
31.5230301.0890371
4-0.234153-0.0476780
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n"," \n","\n","\n","\n"," \n","
\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","variable_name":"data_df","summary":"{\n \"name\": \"data_df\",\n \"rows\": 1000,\n \"fields\": [\n {\n \"column\": \"prs1\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.9792159381796757,\n \"min\": -3.2412673400690726,\n \"max\": 3.852731490654721,\n \"num_unique_values\": 1000,\n \"samples\": [\n 0.543360192379935,\n 0.9826909839455139,\n -1.8408742313316453\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"prs2\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.8005263506410991,\n \"min\": -2.4852626735659844,\n \"max\": 3.4321005411611654,\n \"num_unique_values\": 1000,\n \"samples\": [\n 0.5511076945976712,\n 0.5725922028405726,\n -1.4935892287728105\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"phenotype\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0,\n \"min\": 0,\n \"max\": 1,\n \"num_unique_values\": 2,\n \"samples\": [\n 1,\n 0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":7}],"source":["data_df.head()"]},{"cell_type":"markdown","metadata":{"id":"4LYsbEE3RdeF"},"source":["# PRS evaluation with bootstrapping\n","\n","The following code generates all evaluation metrics, namely Pearson R, AUC-ROC, AUC-PR, top 10% prevalence, and their 95% confidence intervals using bootstrapping. Note that, from the way we generated the simulated data, we expect the Pearson R of ~0.3 for `prs1` and we expect `prs1` to have higher correlation with the phenotype than `prs2`."]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"height":101,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":17429,"status":"ok","timestamp":1717789999485,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"WVJnK7BAPi33","outputId":"68161231-112f-4e33-d8d0-0ffc89019139"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" method pearsonr pearsonr_std pearsonr_lower pearsonr_upper roc_auc \\\n","0 prs1 0.333455 0.027456 0.277529 0.387433 0.69263 \n","\n"," roc_auc_std roc_auc_lower roc_auc_upper pr_auc pr_auc_std \\\n","0 0.016445 0.65976 0.725288 0.675271 0.022152 \n","\n"," pr_auc_lower pr_auc_upper top10prev top10prev_std top10prev_lower \\\n","0 0.632141 0.715912 0.770216 0.043321 0.688044 \n","\n"," top10prev_upper \n","0 0.85078 "],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
methodpearsonrpearsonr_stdpearsonr_lowerpearsonr_upperroc_aucroc_auc_stdroc_auc_lowerroc_auc_upperpr_aucpr_auc_stdpr_auc_lowerpr_auc_uppertop10prevtop10prev_stdtop10prev_lowertop10prev_upper
0prs10.3334550.0274560.2775290.3874330.692630.0164450.659760.7252880.6752710.0221520.6321410.7159120.7702160.0433210.6880440.85078
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","summary":"{\n \"name\": \")\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"method\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs1\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.3334554859786796,\n \"max\": 0.3334554859786796,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.3334554859786796\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.027455597173908577,\n \"max\": 0.027455597173908577,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.027455597173908577\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.2775293042598108,\n \"max\": 0.2775293042598108,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.2775293042598108\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.38743254268744753,\n \"max\": 0.38743254268744753,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.38743254268744753\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6926303605619311,\n \"max\": 0.6926303605619311,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6926303605619311\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.016445301315729702,\n \"max\": 0.016445301315729702,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.016445301315729702\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.659760150142918,\n \"max\": 0.659760150142918,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.659760150142918\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7252876945992696,\n \"max\": 0.7252876945992696,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7252876945992696\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.675270596876246,\n \"max\": 0.675270596876246,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.675270596876246\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.02215152388674347,\n \"max\": 0.02215152388674347,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.02215152388674347\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6321413648383354,\n \"max\": 0.6321413648383354,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6321413648383354\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7159121917609861,\n \"max\": 0.7159121917609861,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7159121917609861\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7702162426122681,\n \"max\": 0.7702162426122681,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7702162426122681\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.04332125213088804,\n \"max\": 0.04332125213088804,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.04332125213088804\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6880441176470588,\n \"max\": 0.6880441176470588,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6880441176470588\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.8507797029702969,\n \"max\": 0.8507797029702969,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.8507797029702969\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":8}],"source":["get_prs_eval_info(\n"," y_true=data_df['phenotype'],\n"," y_pred=data_df['prs1'],\n"," name='prs1',\n"," as_dataframe=True\n",")"]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"height":101,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":9213,"status":"ok","timestamp":1717790008685,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"puOfA5wuQeiJ","outputId":"40a4792a-c897-450c-ee39-aa8ecd72f761"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" method pearsonr pearsonr_std pearsonr_lower pearsonr_upper roc_auc \\\n","0 prs2 0.319189 0.027899 0.260433 0.373947 0.6837 \n","\n"," roc_auc_std roc_auc_lower roc_auc_upper pr_auc pr_auc_std \\\n","0 0.016604 0.649911 0.717019 0.664467 0.022454 \n","\n"," pr_auc_lower pr_auc_upper top10prev top10prev_std top10prev_lower \\\n","0 0.620486 0.706022 0.764624 0.042396 0.671552 \n","\n"," top10prev_upper \n","0 0.84 "],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
methodpearsonrpearsonr_stdpearsonr_lowerpearsonr_upperroc_aucroc_auc_stdroc_auc_lowerroc_auc_upperpr_aucpr_auc_stdpr_auc_lowerpr_auc_uppertop10prevtop10prev_stdtop10prev_lowertop10prev_upper
0prs20.3191890.0278990.2604330.3739470.68370.0166040.6499110.7170190.6644670.0224540.6204860.7060220.7646240.0423960.6715520.84
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","summary":"{\n \"name\": \")\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"method\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs2\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.3191890184766251,\n \"max\": 0.3191890184766251,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.3191890184766251\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.027898865889530153,\n \"max\": 0.027898865889530153,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.027898865889530153\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.2604328480042442,\n \"max\": 0.2604328480042442,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.2604328480042442\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.3739469506434232,\n \"max\": 0.3739469506434232,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.3739469506434232\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6836996447028457,\n \"max\": 0.6836996447028457,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6836996447028457\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.01660378118234475,\n \"max\": 0.01660378118234475,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.01660378118234475\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6499110741641438,\n \"max\": 0.6499110741641438,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6499110741641438\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7170185826451294,\n \"max\": 0.7170185826451294,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7170185826451294\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6644674946186202,\n \"max\": 0.6644674946186202,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6644674946186202\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.0224540065869167,\n \"max\": 0.0224540065869167,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.0224540065869167\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6204864568922334,\n \"max\": 0.6204864568922334,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6204864568922334\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.7060224657169427,\n \"max\": 0.7060224657169427,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.7060224657169427\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.764623511500396,\n \"max\": 0.764623511500396,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.764623511500396\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.042396301865302535,\n \"max\": 0.042396301865302535,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.042396301865302535\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.6715519801980199,\n \"max\": 0.6715519801980199,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.6715519801980199\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.84,\n \"max\": 0.84,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.84\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":9}],"source":["get_prs_eval_info(\n"," y_true=data_df['phenotype'],\n"," y_pred=data_df['prs2'],\n"," name='prs2',\n"," as_dataframe=True\n",")"]},{"cell_type":"markdown","metadata":{"id":"OiLCjqcrSjPg"},"source":["# PRS comparison with paired bootstrapping\n","\n","The following code snippet compares the performance of `prs1` and `prs2` using paired bootstrapping. Note that the difference is statistically significant with 95% paired bootstrapping confidence interval, if the lower and upper end of the confidence interval are both positive (implying `prs1` is significantly better than `prs2`) or both negative (implying `prs2` is significantly better than `prs1`)."]},{"cell_type":"code","execution_count":null,"metadata":{"colab":{"height":101,"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":6240,"status":"ok","timestamp":1717790014919,"user":{"displayName":"Ted Yun","userId":"09506118669803633658"},"user_tz":240},"id":"oRKgjH_uR2wr","outputId":"76474def-1edd-4cbd-c801-6b00f324f288"},"outputs":[{"output_type":"execute_result","data":{"text/plain":[" method_a method_b pearsonr pearsonr_std pearsonr_lower pearsonr_upper \\\n","0 prs1 prs2 0.014266 0.007112 0.000436 0.027211 \n","\n"," roc_auc roc_auc_std roc_auc_lower roc_auc_upper pr_auc pr_auc_std \\\n","0 0.008931 0.004466 0.000157 0.017171 0.010803 0.005761 \n","\n"," pr_auc_lower pr_auc_upper top10prev top10prev_std top10prev_lower \\\n","0 -0.00061 0.02107 0.005593 0.026971 -0.042589 \n","\n"," top10prev_upper \n","0 0.062382 "],"text/html":["\n","
\n","
\n","\n","\n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n"," \n","
method_amethod_bpearsonrpearsonr_stdpearsonr_lowerpearsonr_upperroc_aucroc_auc_stdroc_auc_lowerroc_auc_upperpr_aucpr_auc_stdpr_auc_lowerpr_auc_uppertop10prevtop10prev_stdtop10prev_lowertop10prev_upper
0prs1prs20.0142660.0071120.0004360.0272110.0089310.0044660.0001570.0171710.0108030.005761-0.000610.021070.0055930.026971-0.0425890.062382
\n","
\n","
\n","\n","
\n"," \n","\n"," \n","\n"," \n","
\n","\n","\n","
\n","
\n"],"application/vnd.google.colaboratory.intrinsic+json":{"type":"dataframe","summary":"{\n \"name\": \" as_dataframe=True)\",\n \"rows\": 1,\n \"fields\": [\n {\n \"column\": \"method_a\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs1\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"method_b\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 1,\n \"samples\": [\n \"prs2\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.014266467502054426,\n \"max\": 0.014266467502054426,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.014266467502054426\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.007111892690604321,\n \"max\": 0.007111892690604321,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.007111892690604321\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.00043626824886599245,\n \"max\": 0.00043626824886599245,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.00043626824886599245\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pearsonr_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.027211089302840434,\n \"max\": 0.027211089302840434,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.027211089302840434\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.008930715859085309,\n \"max\": 0.008930715859085309,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.008930715859085309\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.004466363148919537,\n \"max\": 0.004466363148919537,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.004466363148919537\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.00015733124729375172,\n \"max\": 0.00015733124729375172,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.00015733124729375172\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"roc_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.017170818130808965,\n \"max\": 0.017170818130808965,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.017170818130808965\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.010803102257625864,\n \"max\": 0.010803102257625864,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.010803102257625864\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.005760958016623593,\n \"max\": 0.005760958016623593,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.005760958016623593\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": -0.0006104367572841078,\n \"max\": -0.0006104367572841078,\n \"num_unique_values\": 1,\n \"samples\": [\n -0.0006104367572841078\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"pr_auc_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.02106968216083579,\n \"max\": 0.02106968216083579,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.02106968216083579\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.005592731111872085,\n \"max\": 0.005592731111872085,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.005592731111872085\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_std\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.026971273443313012,\n \"max\": 0.026971273443313012,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.026971273443313012\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_lower\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": -0.04258910891089107,\n \"max\": -0.04258910891089107,\n \"num_unique_values\": 1,\n \"samples\": [\n -0.04258910891089107\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"top10prev_upper\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": null,\n \"min\": 0.062381770529994184,\n \"max\": 0.062381770529994184,\n \"num_unique_values\": 1,\n \"samples\": [\n 0.062381770529994184\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"}},"metadata":{},"execution_count":10}],"source":["get_prs_paired_eval_info(\n"," y_true=data_df['phenotype'],\n"," y_pred1=data_df['prs1'],\n"," y_pred2=data_df['prs2'],\n"," name1='prs1',\n"," name2='prs2',\n"," as_dataframe=True)"]}]} \ No newline at end of file