######## 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¶
import itertools
import json
import altair as alt
import pandas as pd
_ = alt.data_transformers.disable_max_rows()
Get variables from snakemake
:
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
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.
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¶
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
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¶
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']
# 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¶
# 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¶
# 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¶
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¶
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.
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.
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'
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