In [1]:
######## snakemake preamble start (automatically inserted, do not edit) ########
import sys;sys.path.extend(['/fh/fast/bloom_j/software/miniforge3/envs/seqneut-pipeline/lib/python3.13/site-packages', '/fh/fast/bloom_j/computational_notebooks/jbloom/2025/flu-seqneut-2025/seqneut-pipeline', '/fh/fast/bloom_j/computational_notebooks/jbloom/2025/flu-seqneut-2025', '/fh/fast/bloom_j/software/miniforge3/envs/seqneut-pipeline/bin', '/fh/fast/bloom_j/software/miniforge3/envs/seqneut-pipeline/lib/python3.13', '/fh/fast/bloom_j/software/miniforge3/envs/seqneut-pipeline/lib/python3.13/lib-dynload', '/fh/fast/bloom_j/software/miniforge3/envs/seqneut-pipeline/lib/python3.13/site-packages', '/home/jbloom/.cache/snakemake/snakemake/source-cache/runtime-cache/tmppgi9bv6g/file/fh/fast/bloom_j/computational_notebooks/jbloom/2025/flu-seqneut-2025/notebooks', '/fh/fast/bloom_j/computational_notebooks/jbloom/2025/flu-seqneut-2025/notebooks']);import pickle;from snakemake import script;script.snakemake = pickle.loads(b'\x80\x04\x95\xc1A\x00\x00\x00\x00\x00\x00\x8c\x10snakemake.script\x94\x8c\tSnakemake\x94\x93\x94)\x81\x94}\x94(\x8c\x05input\x94\x8c\x0csnakemake.io\x94\x8c\nInputFiles\x94\x93\x94)\x81\x94(\x8c3results/aggregated_analyses/human_sera_metadata.csv\x94\x8c1results/aggregated_analyses/human_sera_titers.csv\x94\x8cBdata/viral_libraries/flu-seqneut-2025-barcode-to-strain_actual.csv\x94\x8c4data/viral_libraries/flu-seqneut-2025_plot_order.csv\x94e}\x94(\x8c\x06_names\x94}\x94(\x8c\x0cmetadata_csv\x94K\x00N\x86\x94\x8c\ntiters_csv\x94K\x01N\x86\x94\x8c\tvirus_csv\x94K\x02N\x86\x94\x8c\x17viral_strain_plot_order\x94K\x03N\x86\x94u\x8c\x12_allowed_overrides\x94]\x94(\x8c\x05index\x94\x8c\x04sort\x94eh\x1bh\x06\x8c\x0eAttributeGuard\x94\x93\x94)\x81\x94}\x94\x8c\x04name\x94h\x1bsbh\x1ch\x1e)\x81\x94}\x94h!h\x1csbh\x11h\nh\x13h\x0bh\x15h\x0ch\x17h\rub\x8c\x06output\x94h\x06\x8c\x0bOutputFiles\x94\x93\x94)\x81\x94(\x8c?results/aggregated_analyses/sera_collection_dates_and_ages.html\x94\x8cNresults/aggregated_analyses/human_sera_titers_H1N1_recent_individual_sera.html\x94\x8cRresults/aggregated_analyses/human_sera_titers_H1N1_recent_interquartile_range.html\x94\x8cPresults/aggregated_analyses/human_sera_titers_H1N1_recent_frac_below_cutoff.html\x94\x8cOresults/aggregated_analyses/human_sera_titers_H1N1_vaccine_individual_sera.html\x94\x8cSresults/aggregated_analyses/human_sera_titers_H1N1_vaccine_interquartile_range.html\x94\x8cQresults/aggregated_analyses/human_sera_titers_H1N1_vaccine_frac_below_cutoff.html\x94\x8cNresults/aggregated_analyses/human_sera_titers_H3N2_recent_individual_sera.html\x94\x8cRresults/aggregated_analyses/human_sera_titers_H3N2_recent_interquartile_range.html\x94\x8cPresults/aggregated_analyses/human_sera_titers_H3N2_recent_frac_below_cutoff.html\x94\x8cOresults/aggregated_analyses/human_sera_titers_H3N2_vaccine_individual_sera.html\x94\x8cSresults/aggregated_analyses/human_sera_titers_H3N2_vaccine_interquartile_range.html\x94\x8cQresults/aggregated_analyses/human_sera_titers_H3N2_vaccine_frac_below_cutoff.html\x94\x8c<results/aggregated_analyses/human_sera_titers_summarized.csv\x94e}\x94(h\x0f}\x94(\x8c!sera_collection_date_and_age_plot\x94K\x00N\x86\x94\x8c\x0bchart_htmls\x94K\x01K\r\x86\x94\x8c\x15summarized_titers_csv\x94K\rN\x86\x94uh\x19]\x94(h\x1bh\x1ceh\x1bh\x1e)\x81\x94}\x94h!h\x1bsbh\x1ch\x1e)\x81\x94}\x94h!h\x1csbh8h(h:h\x06\x8c\tNamedlist\x94\x93\x94)\x81\x94(h)h*h+h,h-h.h/h0h1h2h3h4e}\x94(h\x0f}\x94h\x19]\x94(h\x1bh\x1ceh\x1bh\x1e)\x81\x94}\x94h!h\x1bsbh\x1ch\x1e)\x81\x94}\x94h!h\x1csbubh<h5ub\x8c\r_params_store\x94h\x06\x8c\x06Params\x94\x93\x94)\x81\x94(}\x94(\x8c\x1fA/Croatia/10136RV/2023-egg_H3N2\x94\x8c\x1b2025-2026 egg-based vaccine\x94\x8c!A/DistrictOfColumbia/27/2023_H3N2\x94\x8c\x1c2025-2026 cell-based vaccine\x94\x8c!A/Victoria/4897/2022_IVR-238_H1N1\x94\x8c\x1b2025-2026 egg-based vaccine\x94\x8c\x18A/Wisconsin/67/2022_H1N1\x94\x8c\x1c2025-2026 cell-based vaccine\x94\x8c\x16A/Thailand/8/2022_H3N2\x94\x8c\x1b2024-2025 egg-based vaccine\x94\x8c\x1cA/Massachusetts/18/2022_H3N2\x94\x8c\x1c2024-2025 cell-based vaccine\x94u}\x94(\x8c\x0ctiter_cutoff\x94K\x8c\x8c\x11titer_lower_limit\x94K(\x8c\x10min_frac_strains\x94G?\xec\xcc\xcc\xcc\xcc\xcc\xcd\x8c\rmin_frac_sera\x94G?\xe8\x00\x00\x00\x00\x00\x00\x8c\x0fmin_frac_action\x94\x8c\x05raise\x94u\x8c\x10circulating_2025\x94e}\x94(h\x0f}\x94(\x8c\x16recent_vaccine_strains\x94K\x00N\x86\x94\x8c\x17human_sera_plots_params\x94K\x01N\x86\x94\x8c\x17circulating_strain_type\x94K\x02N\x86\x94uh\x19]\x94(h\x1bh\x1ceh\x1bh\x1e)\x81\x94}\x94h!h\x1bsbh\x1ch\x1e)\x81\x94}\x94h!h\x1csbhhhQhjh^hlheub\x8c\r_params_types\x94}\x94\x8c\twildcards\x94h\x06\x8c\tWildcards\x94\x93\x94)\x81\x94}\x94(h\x0f}\x94h\x19]\x94(h\x1bh\x1ceh\x1bh\x1e)\x81\x94}\x94h!h\x1bsbh\x1ch\x1e)\x81\x94}\x94h!h\x1csbub\x8c\x07threads\x94K\x01\x8c\tresources\x94h\x06\x8c\tResources\x94\x93\x94)\x81\x94(K\x01K\x01\x8c\x15/loc/scratch/31392043\x94e}\x94(h\x0f}\x94(\x8c\x06_cores\x94K\x00N\x86\x94\x8c\x06_nodes\x94K\x01N\x86\x94\x8c\x06tmpdir\x94K\x02N\x86\x94uh\x19]\x94(h\x1bh\x1ceh\x1bh\x1e)\x81\x94}\x94h!h\x1bsbh\x1ch\x1e)\x81\x94}\x94h!h\x1csbh\x88K\x01h\x8aK\x01h\x8ch\x85ub\x8c\x03log\x94h\x06\x8c\x03Log\x94\x93\x94)\x81\x94\x8c8results/aggregated_analyses/plot_human_sera_titers.ipynb\x94a}\x94(h\x0f}\x94\x8c\x08notebook\x94K\x00N\x86\x94sh\x19]\x94(h\x1bh\x1ceh\x1bh\x1e)\x81\x94}\x94h!h\x1bsbh\x1ch\x1e)\x81\x94}\x94h!h\x1csbh\x9ah\x97ub\x8c\x06config\x94}\x94(\x8c\x08subtypes\x94]\x94(\x8c\x04H1N1\x94\x8c\x04H3N2\x94e\x8c\x17circulating_strain_type\x94he\x8c\x16recent_vaccine_strains\x94hQ\x8c\x1chuman_sera_groups_to_exclude\x94]\x94\x8c\x03FCI\x94a\x8c\x15human_sera_to_exclude\x94]\x94(\x8c\x06SCH_19\x94\x8c\x06SCH_22\x94\x8c\x06SCH_26\x94e\x8c\x17human_sera_plots_params\x94h^\x8c\x10seqneut-pipeline\x94\x8c\x10seqneut-pipeline\x94\x8c\x04docs\x94\x8c\x04docs\x94\x8c\x0bdescription\x94XK\x03\x00\x00# Near real-time mapping of the human neutralizing antibody landscape to influenza virus to inform vaccine-strain selection in September 2025\n\nExperiments and analysis performed by Caroline Kikawa in the [Bloom lab](https://jbloomlab.github.io/) using the sequencing-based neutralization assay described in [Loes et al (2024)](https://journals.asm.org/doi/10.1128/jvi.00689-24) and [Kikawa et al (2025)](https://elifesciences.org/reviewed-preprints/106811).\n\nBriefly, this study measured neutralization titers to influenza viruses with HA from seasonal H3N2 and H1N1 viruses representative of those circulating in the summer of 2025 against a set of human sera collected in late 2024 to spring of 2025.\n\nThe numerical data and computer code are at [https://github.com/jbloomlab/flu-seqneut-2025](https://github.com/jbloomlab/flu-seqneut-2025)\n\x94\x8c\x0fviral_libraries\x94}\x94(\x8c\x08designed\x94\x8cDdata/viral_libraries/flu-seqneut-2025-barcode-to-strain_designed.csv\x94\x8c\x06actual\x94\x8cBdata/viral_libraries/flu-seqneut-2025-barcode-to-strain_actual.csv\x94u\x8c\x17viral_strain_plot_order\x94\x8c4data/viral_libraries/flu-seqneut-2025_plot_order.csv\x94\x8c\x12neut_standard_sets\x94}\x94\x8c\x08loes2023\x94\x8c3data/neut_standard_sets/loes2023_neut_standards.csv\x94s\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\x08upstream\x94\x8c\x1cCCTACAATGTCGGATTTGTATTTAATAG\x94\x8c\ndownstream\x94\x8c\x00\x94\x8c\x04minq\x94K\x14\x8c\x11upstream_mismatch\x94K\x04\x8c\x0ebc_orientation\x94\x8c\x02R2\x94u\x8c#default_process_plate_qc_thresholds\x94}\x94(\x8c\x1bavg_barcode_counts_per_well\x94M\xf4\x01\x8c\x1fmin_neut_standard_frac_per_well\x94G?tz\xe1G\xae\x14{\x8c"no_serum_per_viral_barcode_filters\x94}\x94(\x8c\x08min_frac\x94G?\x1a6\xe2\xeb\x1cC-\x8c\x0fmax_fold_change\x94K\x04\x8c\tmax_wells\x94K\x02u\x8c!per_neut_standard_barcode_filters\x94}\x94(\x8c\x08min_frac\x94G?tz\xe1G\xae\x14{\x8c\x0fmax_fold_change\x94K\x04\x8c\tmax_wells\x94K\x02u\x8c min_neut_standard_count_per_well\x94M\xe8\x03\x8c)min_no_serum_count_per_viral_barcode_well\x94Kd\x8c+max_frac_infectivity_per_viral_barcode_well\x94K\x03\x8c)min_dilutions_per_barcode_serum_replicate\x94K\x06u\x8c%default_process_plate_curvefit_params\x94}\x94(\x8c\x18frac_infectivity_ceiling\x94K\x01\x8c\x06fixtop\x94]\x94(G?\xe3333333K\x01e\x8c\tfixbottom\x94K\x00\x8c\x08fixslope\x94]\x94(G?\xe9\x99\x99\x99\x99\x99\x9aK\neu\x8c!default_process_plate_curvefit_qc\x94}\x94(\x8c\x1dmax_frac_infectivity_at_least\x94G\x00\x00\x00\x00\x00\x00\x00\x00\x8c\x0fgoodness_of_fit\x94}\x94(\x8c\x06min_R2\x94G?\xe0\x00\x00\x00\x00\x00\x00\x8c\x08max_RMSD\x94G?\xc3333333u\x8c#serum_replicates_ignore_curvefit_qc\x94]\x94\x8c+barcode_serum_replicates_ignore_curvefit_qc\x94]\x94u\x8c\x16default_serum_titer_as\x94\x8c\x08midpoint\x94\x8c\x1bdefault_serum_qc_thresholds\x94}\x94(\x8c\x0emin_replicates\x94K\x01\x8c\x1bmax_fold_change_from_median\x94K\x06\x8c\x11viruses_ignore_qc\x94]\x94u\x8c\x16sera_override_defaults\x94}\x94\x8c\x06plates\x94}\x94(\x8c\x08plate1-2\x94}\x94(\x8c\x05group\x94\x8c\x04UWMC\x94\x8c\x04date\x94\x8c\x08datetime\x94\x8c\x04date\x94\x93\x94C\x04\x07\xe9\x08\x07\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c#data/plates/2025-08-11_plate1-2.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\x05wells\x94]\x94(\x8c\x02H1\x94\x8c\x02H2\x94\x8c\x02H3\x94\x8c\x02H4\x94\x8c\x02H5\x94\x8c\x02H6\x94\x8c\x02H7\x94\x8c\x02H8\x94\x8c\x02H9\x94\x8c\x03H10\x94\x8c\x03H11\x94\x8c\x03H12\x94es\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06ATCGAT\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x08plate2-2\x94}\x94(\x8c\x05group\x94\x8c\x04UWMC\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\x07\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c#data/plates/2025-08-11_plate2-2.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06TGACGC\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x06plate3\x94}\x94(\x8c\x05group\x94\x8c\x04UWMC\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\x07\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c!data/plates/2025-08-11_plate3.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06CAGTTG\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x06plate4\x94}\x94(\x8c\x05group\x94\x8c\x04UWMC\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\x07\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c!data/plates/2025-08-11_plate4.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06GTCTAA\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x06plate6\x94}\x94(\x8c\x05group\x94\x8c\x03FCI\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\x0c\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c!data/plates/2025-08-12_plate6.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06ATCGAT\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\nplate7_FCI\x94}\x94(\x8c\x05group\x94\x8c\x03FCI\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\x0c\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c%data/plates/2025-08-12_plate7_FCI.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06TGACGC\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\nplate7_SCH\x94}\x94(\x8c\x05group\x94\x8c\x03SCH\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\x0c\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c%data/plates/2025-08-12_plate7_SCH.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06TGACGC\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x06plate8\x94}\x94(\x8c\x05group\x94\x8c\x03SCH\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\x0c\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c!data/plates/2025-08-12_plate8.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06CAGTTG\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x06plate9\x94}\x94(\x8c\x05group\x94\x8c\x03SCH\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\r\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c!data/plates/2025-08-13_plate9.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06GTCTAA\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate10\x94}\x94(\x8c\x05group\x94\x8c\x03SCH\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\r\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-13_plate10.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06ACGCTG\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate11\x94}\x94(\x8c\x05group\x94\x8c\x03SCH\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\r\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-13_plate11.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06TATAGC\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x0bplate12_SCH\x94}\x94(\x8c\x05group\x94\x8c\x03SCH\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\r\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c&data/plates/2025-08-13_plate12_SCH.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06CGAGCT\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\rplate12_EPIHK\x94}\x94(\x8c\x05group\x94\x8c\x05EPIHK\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\r\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c(data/plates/2025-08-13_plate12_EPIHK.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06CGAGCT\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate13\x94}\x94(\x8c\x05group\x94\x8c\x05EPIHK\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\x12\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-18_plate13.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06GCTACA\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate14\x94}\x94(\x8c\x05group\x94\x8c\x05EPIHK\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\x12\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-18_plate14.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06ATCGAT\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate15\x94}\x94(\x8c\x05group\x94\x8c\x05EPIHK\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\x12\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-18_plate15.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06TGACGC\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate16\x94}\x94(\x8c\x05group\x94\x8c\x04NIID\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\x12\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-18_plate16.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06CAGTTG\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate17\x94}\x94(\x8c\x05group\x94\x8c\x04NIID\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\x14\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-20_plate17.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06GTCTAA\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate18\x94}\x94(\x8c\x05group\x94\x8c\x04NIID\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\x14\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-20_plate18.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06ACGCTG\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate19\x94}\x94(\x8c\x05group\x94\x8c\x04NIID\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\x14\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-20_plate19.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06TATAGC\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x07plate20\x94}\x94(\x8c\x05group\x94\x8c\x04NIID\x94\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\x14\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x06actual\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c"data/plates/2025-08-20_plate20.csv\x94\x8c\x0cmanual_drops\x94}\x94\x8c\rqc_thresholds\x94}\x94(h\xd0M\xf4\x01h\xd1G?tz\xe1G\xae\x14{h\xd2}\x94(h\xd4G?\x1a6\xe2\xeb\x1cC-h\xd5K\x04h\xd6K\x02uh\xd7}\x94(h\xd9G?tz\xe1G\xae\x14{h\xdaK\x04h\xdbK\x02uh\xdcM\xe8\x03h\xddKdh\xdeK\x03h\xdfK\x06u\x8c\x0fcurvefit_params\x94}\x94(h\xe2K\x01h\xe3h\xe4h\xe5K\x00h\xe6h\xe7u\x8c\x0bcurvefit_qc\x94}\x94(h\xeaG\x00\x00\x00\x00\x00\x00\x00\x00h\xeb}\x94(h\xedG?\xe0\x00\x00\x00\x00\x00\x00h\xeeG?\xc3333333uh\xefh\xf0h\xf1h\xf2u\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06CGAGCT\x94\x8c\x12upstream2_mismatch\x94K\x01uuu\x8c\x14miscellaneous_plates\x94}\x94(\x8c\x1520250716_initial_pool\x94}\x94(\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x07\x10\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x08designed\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c5data/miscellaneous_plates/2025-07-16_initial_pool.csv\x94\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06GCTACA\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x1620250723_balanced_pool\x94}\x94(\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x07\x17\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x08designed\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c8data/miscellaneous_plates/2025-07-23_balanced_repool.csv\x94\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06GCTACA\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c(20250723_H3_and_partial_H1_balanced_pool\x94}\x94(\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x07\x17\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x08designed\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8cJdata/miscellaneous_plates/2025-07-23_H3_and_partial_H1_balanced_repool.csv\x94\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06GCTACA\x94\x8c\x12upstream2_mismatch\x94K\x01uu\x8c\x1620250807_balanced_pool\x94}\x94(\x8c\x04date\x94j\x06\x01\x00\x00C\x04\x07\xe9\x08\x07\x94\x85\x94R\x94\x8c\rviral_library\x94\x8c\x08designed\x94\x8c\x11neut_standard_set\x94\x8c\x08loes2023\x94\x8c\x0bsamples_csv\x94\x8c8data/miscellaneous_plates/2025-08-07_balanced_repool.csv\x94\x8c\x1eillumina_barcode_parser_params\x94}\x94(\x8c\tupstream2\x94\x8c\x06GCTACA\x94\x8c\x12upstream2_mismatch\x94K\x01uuuu\x8c\x04rule\x94\x8c\x16plot_human_sera_titers\x94\x8c\x0fbench_iteration\x94N\x8c\tscriptdir\x94\x8cO/fh/fast/bloom_j/computational_notebooks/jbloom/2025/flu-seqneut-2025/notebooks\x94ub.');del script;from snakemake.logging import logger;from snakemake.script import snakemake;import os; os.chdir(r'/fh/fast/bloom_j/computational_notebooks/jbloom/2025/flu-seqneut-2025');
######## snakemake preamble end #########

Make summary plots of the titers of different strains against human sera¶

Setup and read data¶

In [2]:
import itertools
import json

import altair as alt

import pandas as pd

_ = alt.data_transformers.disable_max_rows()

Get variables from snakemake:

In [3]:
metadata_csv = snakemake.input.metadata_csv
titers_csv = snakemake.input.titers_csv
virus_csv = snakemake.input.virus_csv
viral_strain_plot_order_csv = snakemake.input.viral_strain_plot_order
recent_vaccine_strains = snakemake.params.recent_vaccine_strains
circulating_strain_type = snakemake.params.circulating_strain_type
human_sera_plots_params = snakemake.params.human_sera_plots_params
summarized_titers_csv = snakemake.output.summarized_titers_csv
sera_collection_date_and_age_plot = snakemake.output.sera_collection_date_and_age_plot
chart_htmls = snakemake.output.chart_htmls
In [4]:
metadata_all = pd.read_csv(metadata_csv)
print(f"Read {len(metadata_all)=} for sera from {metadata_csv=}")

titers_all = pd.read_csv(titers_csv)
print(f"\nRead {len(titers_all)=} titers from {titers_csv=}")
assert set(titers_all["serum"]) == set(metadata_all["serum"])

viruses_all = (
    pd.read_csv(virus_csv)
    [["strain", "subtype", "strain_type", "subclade", "vaccine_type"]]
    .drop_duplicates()
    .rename(columns={"strain": "virus"})
)
assert set(titers_all["virus"]).issubset(viruses_all["virus"])
assert set(recent_vaccine_strains).issubset(viruses_all["virus"])
if not set(viruses_all["strain_type"]).issubset({circulating_strain_type, "vaccine"}):
    raise ValueError(f"{viruses_all['strain_type']=} != ['vaccine', {circulating_strain_type=}]")
viruses_all["strain_type"] = viruses_all["strain_type"].where(
    ~viruses_all["virus"].isin(recent_vaccine_strains), "recent_vaccine"
)
print(f"\nRead {len(viruses_all)=} viruses from {virus_csv=}")

viral_strain_plot_order = pd.read_csv(viral_strain_plot_order_csv)["strain"].tolist()
assert set(viruses_all["virus"]).issubset(viral_strain_plot_order)
Read len(metadata_all)=188 for sera from metadata_csv='results/aggregated_analyses/human_sera_metadata.csv'

Read len(titers_all)=26148 titers from titers_csv='results/aggregated_analyses/human_sera_titers.csv'

Read len(viruses_all)=140 viruses from virus_csv='data/viral_libraries/flu-seqneut-2025-barcode-to-strain_actual.csv'

Get fraction of sera and viruses with titers¶

Look at the fraction of sera and viruses with titers. We generally may want to drop sera that lack titers for many viruses, and viruses that lack titers for many sera. Depending on min_frac_action (specified in configuration), we either raise an error if any sera or viruses are below the fractions specified in the configuration, or drop any sera or titers below these fractions. In general for production runs you may want to raise an error and filter these at an upstream step.

In [5]:
min_frac_action = human_sera_plots_params["min_frac_action"]

titers = titers_all.copy()
metadata = metadata_all.copy()
viruses = viruses_all.copy()

for min_frac_type, vals, frac_vals, col, frac_var in [
    ("min_frac_strains", set(metadata["serum"]), set(viruses["virus"]), "serum", "virus"),
    ("min_frac_sera", set(viruses["virus"]), set(metadata["serum"]), "virus", "serum"),
]:
    min_frac = human_sera_plots_params[min_frac_type]
    print(f"\nChecking for {min_frac_type=} at cutoff of {min_frac=}")
    frac_df = (
        titers
        .groupby(col, as_index=False)
        .aggregate(n_w_titers=pd.NamedAgg(col, "count"))
        .assign(frac_w_titers=lambda x: x["n_w_titers"] / len(frac_vals))
    )
    frac_df = pd.concat(
        [
            frac_df,
            pd.DataFrame(
                {
                    col: [v for v in vals if v not in set(frac_df[col])],
                    "n_w_titers": 0,
                    "frac_w_titers": 0.0,
                }
            )
        ],
        ignore_index=True,
    )
    assert len(frac_df) == len(vals)
    assert (frac_df["frac_w_titers"] <= 1).all()

    frac_df["below_cutoff"] = frac_df["frac_w_titers"] < min_frac

    frac_chart = (
        alt.Chart(frac_df)
        .encode(
            alt.X(
                "frac_w_titers",
                title=f"fraction {frac_var} with titers",
                scale=alt.Scale(domain=[0, 1]),
            ),
            alt.Y(col, sort=alt.SortField("frac_w_titers", order="descending")),
            alt.Fill("below_cutoff", title=f"below cutoff of {min_frac_type} = {min_frac}"),
            tooltip=[col, "n_w_titers", alt.Tooltip("frac_w_titers", format=".3f")],
        )
        .mark_bar()
        .properties(
            height=alt.Step(11),
            width=135,
            title=f"{frac_var} with titers for each {col}",
        )
        .configure_axis(labelLimit=500)
        .configure_legend(titleLimit=500)
    )

    display(frac_chart)
    
    failed = frac_df.query("below_cutoff").sort_values("frac_w_titers").reset_index(drop=True)
    below_frac = set(failed[col])
    print(f"Overall, {len(below_frac)} {col} have titers for less than {min_frac} {frac_var}")
    display(failed)

    if min_frac_action == "raise":
        if not failed.empty:
            raise ValueError(frac_df.query("below_cutoff").sort_values("frac_w_titers").reset_index(drop=True))
    elif min_frac_action == "drop":
        if not failed.empty:
            print(f"Dropping these {col}")
            titers = titers[~titers[col].isin(below_frac)]
            if col == "serum":
                metadata = metadata[~metadata[col].isin(below_frac)]
            if col == "virus":
                viruses = viruses[~viruses[col].isin(below_frac)]
    else:
        raise ValueError(f"invalid {min_frac_action=}")

print(
    f"\nAfter any filtering:"
    f"\n {len(titers)=} / {len(titers_all)=}"
    f"\n {len(viruses)=} / {len(viruses_all)=}"
    f"\n {len(metadata)=} / {len(metadata_all)=}"
)
Checking for min_frac_type='min_frac_strains' at cutoff of min_frac=0.9
Overall, 0 serum have titers for less than 0.9 virus
serum n_w_titers frac_w_titers below_cutoff
Checking for min_frac_type='min_frac_sera' at cutoff of min_frac=0.75
Overall, 0 virus have titers for less than 0.75 serum
virus n_w_titers frac_w_titers below_cutoff
After any filtering:
 len(titers)=26148 / len(titers_all)=26148
 len(viruses)=140 / len(viruses_all)=140
 len(metadata)=188 / len(metadata_all)=188

Plot age and collection distributions of sera¶

In [6]:
sera_base = (
    alt.Chart(
        metadata[["group", "serum", "collection_date_numerical", "age_years"]]
        .assign(
            n=lambda x: x.groupby("group")["serum"].transform("count"),
            group=lambda x: x["group"] + " (n=" + x["n"].astype(str) + ")",
        )
        .drop(columns="n")
    )
    .encode(alt.Row("group", title=None))
    .mark_bar()
)

sera_date_chart = (
    sera_base
    .encode(
        alt.X(
            "yearmonth(collection_date_numerical)",
            title="collection date",
            axis=alt.Axis(format="%b-%Y", labelAngle=270),
            scale=alt.Scale(nice=False, padding=3),
        ),
        alt.Y("count()", title="number of sera", scale=alt.Scale(nice=False, padding=3)),
    )
    .resolve_scale(y="independent")
    .properties(height=80, width=70)
)

sera_age_chart = (
    sera_base
    .encode(
        alt.X(
            "age_years",
            title="age (years)",
            bin=alt.Bin(step=5, anchor=0),
            scale=alt.Scale(nice=False, padding=3),
        ),
        alt.Y("count()", title="number of sera", scale=alt.Scale(nice=False, padding=3)),
    )
    .resolve_scale(y="independent")
    .properties(height=80, width=150)
)

sera_chart = (
    alt.hconcat(sera_date_chart, sera_age_chart)
    .configure_axis(grid=False, titleFontWeight="normal")
    .configure_header(
        title=None, labelOrient="top", labelFontSize=11, labelPadding=2
    )
    .configure_facet(spacing=7)
    .configure_view(stroke="black")
    .properties(
        title=alt.TitleParams(
            "collection dates and subject ages for sera",
            anchor="middle",
        ),
    )
)

print(f"Saving to {sera_collection_date_and_age_plot}")
sera_chart.save(sera_collection_date_and_age_plot)

sera_chart
Saving to results/aggregated_analyses/sera_collection_dates_and_ages.html
Out[6]:

Plot all the titers¶

Assign label colors by subclade / vaccine type¶

Define color mapping from subclade (circulating strains) or vaccine type (vaccine strains) to colors for label coloring, then create an expression that can be passed to altair labelColor:

In [7]:
strain_color_prop = (
    viruses
    .assign(
        strain=lambda x: pd.Categorical(
            x["virus"], viral_strain_plot_order, ordered=True
        ),
        color_prop=lambda x: x["subclade"].where(
            x["strain_type"] == circulating_strain_type, x["vaccine_type"] + " vaccine"
        ),
    )
    .sort_values("strain")
)
assert strain_color_prop["color_prop"].notnull().all()

prop_colors = {
    "cell vaccine": "black",
    "egg vaccine": "gray",
}

# colors assigned to non-vaccine properties, dropping the light yellow
# (too faint) and the gray (too similar to vaccine strains color)
non_vaccine_colors = list(reversed([
    "#8dd3c7",
    # light yellows dropped "#ffffb3",
    "#bebada",
    "#fb8072",
    "#80b1d3",
    "#fdb462",
    "#b3de69",
    "#fccde5",
    # gray dropped "#d9d9d9",
    "#bc80bd",
    "#ccebc5",
    "#ffed6f",
]))

# make a different color map for each subtype as they are plotted separately
for subtype in strain_color_prop["subtype"].unique():
    subtype_color_props = (
        strain_color_prop.query("subtype == @subtype")["color_prop"].unique().tolist()
    )
    props_not_yet_colored = [p for p in subtype_color_props if p not in prop_colors]
    assert len(props_not_yet_colored) <= len(non_vaccine_colors), props_not_yet_colored
    prop_colors.update(dict(zip(props_not_yet_colored, non_vaccine_colors)))

display(pd.Series(prop_colors).rename("color").rename_axis("property").to_frame())

assert set(strain_color_prop["color_prop"]).issubset(prop_colors)
strain_color_prop = strain_color_prop.assign(
    color=lambda x: x["color_prop"].map(prop_colors)
)
color_mapping = (
    strain_color_prop
    .set_index("virus")
    ["color"]
    .to_dict()
)

labelColor_expr = f"({json.dumps(color_mapping)})[datum.label] || 'black'"
color
property
cell vaccine black
egg vaccine gray
D.5 #ffed6f
D.3.1 #ccebc5
D.1 #bc80bd
D #fccde5
C.1.9.4 #b3de69
C.1.9.3 #fdb462
C.1.9.2 #80b1d3
C.1.9.1 #fb8072
C.1.9 #bebada
C.1 #8dd3c7
J.4 #ffed6f
J.2.5 #ccebc5
J.2.4 #bc80bd
J.2.3 #fccde5
J.2.2 #b3de69
J.2.1 #fdb462
J.2 #80b1d3
J.1.1 #fb8072
J #bebada

Chart base and selections¶

In [8]:
assert len(titers) == len(titers.groupby(["serum", "virus"]))
assert viruses["virus"].nunique() == len(viruses.groupby(["virus", "subtype", "strain_type"]))

# for group labels within altair we calculate these to have n too
groups = (
    pd.concat([metadata, metadata.assign(group="All")])
    .groupby("group", as_index=False)
    .aggregate(n=pd.NamedAgg("serum", "nunique"))
    .assign(group=lambda x: x["group"] + " (n=" + x["n"].astype(str) + ")")
    ["group"].tolist()
)
groups = ["All", *sorted(metadata["group"].unique())]
print(f"{groups=}")

virus_selection = alt.selection_point(
    fields=["virus"], on="mouseover", empty=False, clear="mouseout", nearest=False
)

serum_selection = alt.selection_point(
    fields=["serum"], on="mouseover", empty=False, clear="mouseout", nearest=False
)

group_selection = alt.selection_point(
    fields=["group"],
    bind=alt.binding_select(
        name="serum group", options=[None, *groups], labels=["show all", *groups]
    ),
    init=None,
)

max_age = 5 * int(metadata["age_years"].max() // 5) + 5
assert all(metadata["age_years"] <= max_age)
min_age_slider = alt.param(
    value=0,
    bind=alt.binding_range(min=0, max=max_age, step=5, name="minimum subject age (years)"),
)
max_age_slider = alt.param(
    value=max_age,
    bind=alt.binding_range(min=0, max=max_age, step=5, name="maximum subject age (years)"),
)

# make the chart base, using transform_lookup to make it as small as possible
# by looking up serum-specific and virus-specific annotations
titers_base_nolookup = (
    alt.Chart(titers[["serum", "virus", "titer"]])
    .add_params(
        virus_selection,
        serum_selection,
        group_selection,
        min_age_slider,
        max_age_slider,
    )
    .encode(
        alt.Y(
            "virus",
            sort=list(reversed(viral_strain_plot_order)),
            axis=alt.Axis(
                labelLimit=500,
                labelColor={"expr": labelColor_expr},
                labelFontWeight=600,  # make a bit bolder so colors show
                labelExpr="replace(datum.label, regexp('_[^_]*$'), '')",  # remove _H1N1 or _H3N2
            ),
        ),
    )
    .properties(height=alt.Step(11), width=135)
)

# because of scoping issues when layering and faceting charts with
# transform_lookups (faceting must be done before lookups), we add
# this function to do the faceting and lookups
def facet_and_add_lookups(chart):
    return (
        chart
        # facet
        .facet(column=alt.Column("group_n:N", title=None))
        # lookup additional data
        .transform_lookup(
            lookup="serum",
            from_=alt.LookupData(
                data=metadata,
                key="serum",
                fields=["group", "collection_date_string", "age_string", "age_years", "sex"],
            ),
        )
        .transform_lookup(
            lookup="virus",
            from_=alt.LookupData(
                data=viruses,
                key="virus",
                fields=["subtype", "strain_type", "subclade"],
            ),
        )
        # trick to make a new variable with all groups
        .transform_calculate(facet_with_all="[datum.group, 'All']")
        .transform_flatten(["facet_with_all"], as_=["group"])
        # filter by group and age
        .transform_filter(group_selection)
        .transform_filter(alt.datum["age_years"] >= min_age_slider)
        .transform_filter(alt.datum["age_years"] <= max_age_slider)
        # make facet labels w n per group
        .transform_joinaggregate(n_per_group="distinct(serum)", groupby=["group"])
        .transform_calculate(group_n="datum.group + ' (n=' + datum.n_per_group + ')'")
    )
groups=['All', 'EPIHK', 'NIID', 'SCH', 'UWMC']
In [9]:
# set titer scale
titer_lower_limit = human_sera_plots_params["titer_lower_limit"]
print(f"Using {titer_lower_limit=}")
titer_scale = alt.Scale(type="log", nice=False, domainMin=titer_lower_limit, padding=4)
Using titer_lower_limit=40

Median titers chart¶

In [10]:
# make median titer point chart
median_points = (
    titers_base_nolookup
    .transform_aggregate(
        median_titer="median(titer)",
        groupby=["virus", "subtype", "strain_type", "subclade", "group"],
    )
    .encode(
        alt.X("median_titer:Q", title="titer", scale=titer_scale),
        tooltip=["virus", alt.Tooltip("median_titer:Q", format=".1f"), "strain_type:N", "subclade:N"],
        color=alt.condition(virus_selection, alt.value("red"), alt.value("black")),
        size=alt.condition(virus_selection, alt.value(80), alt.value(40)),
    )
    .mark_circle(opacity=1)
)

#facet_and_add_lookups(median_points)

Per-serum line charts¶

In [11]:
# make per-serum lines
serum_lines =  (
    titers_base_nolookup
    .encode(
        alt.X("titer", scale=titer_scale),
        alt.Detail("serum"),
        tooltip=[
            "virus",
            "serum",
            alt.Tooltip("titer", format=".1f"),
            alt.Tooltip("collection_date_string:N", title="serum date"),
            alt.Tooltip("age_string:N", title="age"),
            "sex:N",
        ],
        size=alt.condition(serum_selection, alt.value(3), alt.value(1.5)),
        opacity=alt.condition(serum_selection, alt.value(1), alt.value(0.2)),
    )
    .mark_line()
)

#facet_and_add_lookups(serum_lines + median_points)

Interquartile range chart¶

In [12]:
interquartile_range =  (
    titers_base_nolookup
    .transform_joinaggregate(
        median_titer="median(titer)",
        titer_q1="q1(titer)",
        titer_q3="q3(titer)",
        groupby=["virus"],
    )
    .encode(
        alt.X("titer", scale=titer_scale),
        tooltip=[
            "virus",
            alt.Tooltip("median_titer:Q", format=".1f"),
            alt.Tooltip("titer_q1:Q", format=".1f"),
            alt.Tooltip("titer_q3:Q", format=".1f"),
            "strain_type:N",
            "subclade:N",
        ],
    )
    .mark_errorband(extent="iqr", opacity=0.5, interpolate="linear")
)

#facet_and_add_lookups(interquartile_range + median_points)

Fraction below titer cutoff chart¶

In [13]:
titer_cutoff = human_sera_plots_params["titer_cutoff"]
print(f"Setting initial {titer_cutoff=}")

titer_cutoff_slider = alt.param(
    value=titer_cutoff,
    bind=alt.binding_range(
        min=titer_lower_limit,
        max=1000,
        step=5,
        name="fraction sera below this cutoff",
    ),
)

# make titer cutoff chart
frac_below_cutoff = (
    titers_base_nolookup
    .add_params(titer_cutoff_slider)
    .transform_calculate(below_cutoff=alt.datum["titer"] < titer_cutoff_slider)
    .transform_aggregate(
        n_below_cutoff="sum(below_cutoff)",
        n_total="distinct(serum)",
        groupby=["virus", "subtype", "strain_type", "subclade", "group"],
    )
    .transform_calculate(
        frac_below_cutoff=alt.datum["n_below_cutoff"] / alt.datum["n_total"]
    )
    .encode(
        alt.X("frac_below_cutoff:Q", title="fraction below cutoff"),
        tooltip=["virus", alt.Tooltip("frac_below_cutoff:Q", format=".2f"), "strain_type:N", "subclade:N"],
        color=alt.condition(virus_selection, alt.value("red"), alt.value("black")),
    )
    .mark_bar(opacity=0.8)
)

#facet_and_add_lookups(frac_below_cutoff)
Setting initial titer_cutoff=140

Now make nicely formatted charts and save them¶

We add a strain-color legend below the chart.

In [14]:
made_chart = {c: False for c in chart_htmls}

for subtype, strain_type, (chart_obj, chart_desc, title) in itertools.product(
    viruses["subtype"].unique(),
    ["recent", "vaccine"],
    [
        ((serum_lines + median_points), "individual_sera", "median (points) and per-serum (lines) titers"),
        ((interquartile_range + median_points), "interquartile_range", "median (points) and interquartile range titers"),
        (frac_below_cutoff, "frac_below_cutoff", "fraction sera below titer cutoff"),
    ],
):
    filesuffix = f"{subtype}_{strain_type}_{chart_desc}.html"
    filename = [c for c in chart_htmls if filesuffix in c]
    assert len(filename) == 1, f"did not find one {filesuffix=} in {chart_htmls=}"
    filename = filename[0]
    made_chart[filename] = True

    # strain types to plot
    strain_types = {
        "recent": [circulating_strain_type, "recent_vaccine"],
        "vaccine": ["vaccine", "recent_vaccine"],
    }[strain_type]

    # ---- Make the legend for the colored strain labels ------------------------
    # viruses plotted
    plotted_viruses = (
        viruses
        .query("subtype == @subtype")
        .query("strain_type in @strain_types")
        ["virus"]
        .unique()
        .tolist()
    )

    # get the virus colors plotted for the labels
    plotted_colors = (
        strain_color_prop
        .query("virus in @plotted_viruses")
        [["color_prop", "color"]]
        .drop_duplicates()
    )

    label_color_legend = (
        alt.Chart(plotted_colors)
          .mark_point(size=100, opacity=0)  # invisible mark; we just want the legend
          .encode(
              x=alt.value(0),
              y=alt.value(0),
              fill=alt.Fill(
                  "color_prop",
                  scale=alt.Scale(
                      domain=list(reversed(plotted_colors["color_prop"].tolist())),
                      range=list(reversed(plotted_colors["color"].tolist())),
                  ),
                  legend=alt.Legend(
                      title=None,
                      symbolType="square",
                      symbolStrokeWidth=0,
                      orient="top",
                      labelFontSize=12,
                      columns=12,
                  )
              )
          )
          .properties(width=1, height=1)  # tiny plot; legend renders outside
    )
    # ---- Finished making the legend for the colored strain labels ---------------------

    chart = (
        alt.vconcat(
            facet_and_add_lookups(chart_obj)
            .transform_filter(alt.datum["subtype"] == subtype)
            .transform_filter(alt.FieldOneOfPredicate("strain_type", strain_types)),
            label_color_legend,
            spacing=10,
        )
        .resolve_scale(fill="independent")
        .configure_axis(
            grid=False, titleFontWeight="normal", titleFontSize=13, labelOverlap=True
        )
        .configure_header(
            title=None, labelOrient="top", labelFontSize=13, labelPadding=2
        )
        .configure_view(stroke="black")
        .configure_facet(spacing=8)
        .properties(
            title=alt.TitleParams(
                f"{title} for {subtype} {strain_type} strains",anchor="middle", fontSize=13
            ),
        )
    )
    display(chart)

    print(f"Saving to {filename=}\n")
    chart.save(filename)

assert all(made_chart.values()), f"{made_chart=}"
Saving to filename='results/aggregated_analyses/human_sera_titers_H1N1_recent_individual_sera.html'

Saving to filename='results/aggregated_analyses/human_sera_titers_H1N1_recent_interquartile_range.html'

Saving to filename='results/aggregated_analyses/human_sera_titers_H1N1_recent_frac_below_cutoff.html'

Saving to filename='results/aggregated_analyses/human_sera_titers_H1N1_vaccine_individual_sera.html'

Saving to filename='results/aggregated_analyses/human_sera_titers_H1N1_vaccine_interquartile_range.html'

Saving to filename='results/aggregated_analyses/human_sera_titers_H1N1_vaccine_frac_below_cutoff.html'

Saving to filename='results/aggregated_analyses/human_sera_titers_H3N2_recent_individual_sera.html'

Saving to filename='results/aggregated_analyses/human_sera_titers_H3N2_recent_interquartile_range.html'

Saving to filename='results/aggregated_analyses/human_sera_titers_H3N2_recent_frac_below_cutoff.html'

Saving to filename='results/aggregated_analyses/human_sera_titers_H3N2_vaccine_individual_sera.html'

Saving to filename='results/aggregated_analyses/human_sera_titers_H3N2_vaccine_interquartile_range.html'

Saving to filename='results/aggregated_analyses/human_sera_titers_H3N2_vaccine_frac_below_cutoff.html'

Make data frame / CSV of summarized titers¶

The above plots summarize the per-virus titers with several statistics: medians, interquartile range, and fraction below cutoff. Here we make and write a data frame with these summarized values.

In [15]:
summarized_titers = (
    pd.concat([titers.assign(group="All"), titers])
    .merge(viruses, on=["virus"], how="left", validate="many_to_one")
    .groupby(["subtype", "strain_type", "subclade", "virus", "group"], as_index=False)
    .aggregate(
        median_titer=pd.NamedAgg("titer", "median"),
        titer_q1=pd.NamedAgg("titer", lambda s: s.quantile(0.25)),
        titer_q3=pd.NamedAgg("titer", lambda s: s.quantile(0.75)),
        frac_below_cutoff=pd.NamedAgg(
            "titer", lambda s: (s < titer_cutoff).sum() / len(s)
        ),
    )
    .rename(
        columns={
            "frac_below_cutoff": f"frac_w_titer_below_{titer_cutoff}",
            "group": "serum_group",
        }
    )
)

print(f"Saving summarized titers to {summarized_titers_csv=}")
summarized_titers.to_csv(summarized_titers_csv, float_format="%.3f", index=False)

summarized_titers
Saving summarized titers to summarized_titers_csv='results/aggregated_analyses/human_sera_titers_summarized.csv'
Out[15]:
subtype strain_type subclade virus serum_group median_titer titer_q1 titer_q3 frac_w_titer_below_140
0 H1N1 circulating_2025 C.1 A/Madagascar/00003/2025_H1N1 All 419.80 188.0500 1087.000 0.159574
1 H1N1 circulating_2025 C.1 A/Madagascar/00003/2025_H1N1 EPIHK 334.85 137.8250 787.950 0.261905
2 H1N1 circulating_2025 C.1 A/Madagascar/00003/2025_H1N1 NIID 250.10 166.7500 382.300 0.145455
3 H1N1 circulating_2025 C.1 A/Madagascar/00003/2025_H1N1 SCH 598.10 187.4000 1758.500 0.212766
4 H1N1 circulating_2025 C.1 A/Madagascar/00003/2025_H1N1 UWMC 755.20 418.6000 2684.250 0.022727
... ... ... ... ... ... ... ... ... ...
595 H3N2 recent_vaccine J.2 A/DistrictOfColumbia/27/2023_H3N2 All 230.90 110.5500 439.450 0.336898
596 H3N2 recent_vaccine J.2 A/DistrictOfColumbia/27/2023_H3N2 EPIHK 203.05 105.3000 351.075 0.357143
597 H3N2 recent_vaccine J.2 A/DistrictOfColumbia/27/2023_H3N2 NIID 138.10 104.3000 268.150 0.509091
598 H3N2 recent_vaccine J.2 A/DistrictOfColumbia/27/2023_H3N2 SCH 420.45 99.0825 944.550 0.326087
599 H3N2 recent_vaccine J.2 A/DistrictOfColumbia/27/2023_H3N2 UWMC 308.40 165.9750 470.350 0.113636

600 rows × 9 columns

In [ ]: