Averaging models

Probably the best way to ensure robust inferences and estimate errors is to have multiple experimental replicates, ideally on different libraries.

Here we describe how to average model fits across libraries and/or replicates.

Split data into replicates

We will use our data for the RBD as an earlier examples, but split it into several libraries / replicates.

Specifically, we will fit two different libraries: avg2muts and avg3muts, which have different barcodes and also different mutation rates (although of course in real life you might sometimes want to average results from different libraries with the same mutation rates). We will also simulate having two replicates for each library just by sampling each library. To make this example faster, we’ll just use one concentration:

[1]:
import pandas as pd

import polyclonal.polyclonal
import polyclonal.polyclonal_collection


# read data
all_data = pd.read_csv("RBD_variants_escape_noisy.csv", na_filter=None)

# split by library and replicates
libraries = ["avg2muts", "avg3muts"]  # the two libraries to use
concentrations = [1]  # use just use this concentration
n_replicates = 2  # number of replicates per library

data_by_replicate = {
    (library, replicate + 1): (
        all_data.query("library == @library")
        .query("concentration in @concentrations")
        .sample(frac=0.3, random_state=replicate)
    )
    for library in libraries
    for replicate in range(n_replicates)
}

Fit models to each replicate

We now fit a Polyclonal model to each replicate using just 2 epitopes, as the data don’t seem sufficient to accurately fit all three epitopes. Then we arrange the models in a data frame:

[2]:
# first create a data frame with all the models
models_by_replicate = {}
for (library, replicate), data in data_by_replicate.items():
    model = polyclonal.Polyclonal(data_to_fit=data, n_epitopes=2)
    models_by_replicate[(library, replicate)] = model
models_df = (
    pd.Series(models_by_replicate, name="model")
    .rename_axis(["library", "replicate"])
    .reset_index()
)

# now fit the models
n_fit, n_failed, models_df["model"] = polyclonal.polyclonal_collection.fit_models(
    models_df["model"],
    n_threads=2,
    reg_escape_weight=0.01,
    reg_uniqueness2_weight=0,
)

Note how the models are arranged in a data frame:

[3]:
# NBVAL_IGNORE_OUTPUT

models_df
[3]:
library replicate model
0 avg2muts 1 <polyclonal.polyclonal.Polyclonal object at 0x...
1 avg2muts 2 <polyclonal.polyclonal.Polyclonal object at 0x...
2 avg3muts 1 <polyclonal.polyclonal.Polyclonal object at 0x...
3 avg3muts 2 <polyclonal.polyclonal.Polyclonal object at 0x...

Average the models

Now we create a PolyclonalAverage model with the models to average. Note that by default the “average” used by PolyclonalAverage is the median rather than the mean between epitopes, although this is a parameter that can also be set to mean.

If your epitopes are too different or poorly defined (e.g., you are trying to fit more epitopes than can be consistently inferred from the data), then you may get an epitope harmonization error:

[4]:
model_avg = polyclonal.PolyclonalAverage(models_df)

Let’s look at the correlation among the escape at each epitope across models:

[5]:
# NBVAL_IGNORE_OUTPUT

model_avg.mut_escape_corr_heatmap()
[5]:

Look at the activities of the epitopes and the rest of the curves. Note how a dark line is shown for the average, and thin lines for individual replicates. It should generally be the case that the epitope with greater activity (more left shifted in plot below) should also be better correlated among replicates (heatmap above) as it can be inferred more reliably:

[6]:
# NBVAL_IGNORE_OUTPUT

model_avg.curves_plot()
[6]:

We can access the average escape values. Note that there is also a column escape_min_magnitude that gives the lowest magnitude value:

[7]:
# NBVAL_IGNORE_OUTPUT

model_avg.mut_escape_df
[7]:
epitope site wildtype mutant mutation escape_mean escape_median escape_min_magnitude escape_std n_models times_seen frac_models
0 1 331 N A N331A -0.066612 -0.031718 0.026679 0.324873 4 17.75 1.0
1 1 331 N D N331D -0.098052 -0.100283 -0.071111 0.022076 4 11.25 1.0
2 1 331 N E N331E -0.061359 -0.008731 -0.001655 0.112576 4 10.25 1.0
3 1 331 N F N331F 0.270243 0.170869 0.014552 0.330315 4 10.00 1.0
4 1 331 N G N331G 0.186271 0.164941 0.031369 0.232272 4 25.00 1.0
... ... ... ... ... ... ... ... ... ... ... ... ...
3859 2 531 T R T531R -0.176471 -0.112467 -0.033899 0.187640 4 27.00 1.0
3860 2 531 T S T531S -0.075136 -0.011741 0.000405 0.155037 4 31.75 1.0
3861 2 531 T V T531V -0.041719 -0.005589 0.012383 0.094055 4 19.50 1.0
3862 2 531 T W T531W 0.169718 0.100366 -0.004133 0.266617 4 5.25 1.0
3863 2 531 T Y T531Y -0.051762 -0.050223 -0.032461 0.020582 4 11.75 1.0

3864 rows × 12 columns

We can do the same for the ICXX values:

[8]:
# NBVAL_IGNORE_OUTPUT

model_avg.mut_icXX_df(
    x=0.9,
    icXX_col="IC90",
    log_fold_change_icXX_col="log2 fold change IC90",
)
[8]:
site wildtype mutant log2 fold change IC90 mean log2 fold change IC90 median log2 fold change IC90 min_magnitude log2 fold change IC90 std n_models times_seen frac_models
0 331 N A -0.159587 -0.206684 -0.135967 0.257279 4 17.75 1.0
1 331 N D -0.086087 -0.054268 -0.002643 0.101813 4 11.25 1.0
2 331 N E 0.034466 0.016051 0.015768 0.067786 4 10.25 1.0
3 331 N F 0.106040 0.142322 0.141842 0.287263 4 10.00 1.0
4 331 N G 0.283214 0.282422 0.088191 0.163483 4 25.00 1.0
... ... ... ... ... ... ... ... ... ... ...
2100 531 T S -0.068568 -0.016541 -0.004995 0.144948 4 31.75 1.0
2101 531 T T 0.000000 0.000000 0.000000 0.000000 4 NaN 1.0
2102 531 T V 0.013326 -0.002602 0.039361 0.236514 4 19.50 1.0
2103 531 T W 0.199249 0.119866 -0.009133 0.287019 4 5.25 1.0
2104 531 T Y -0.021049 -0.048014 -0.044700 0.124192 4 11.75 1.0

2105 rows × 10 columns

Or the per-replicate escape values:

[9]:
# NBVAL_IGNORE_OUTPUT

model_avg.mut_escape_df_replicates
[9]:
epitope site wildtype mutant mutation escape times_seen library replicate
0 1 331 N A N331A -0.090115 19 avg2muts 1
1 1 331 N D N331D -0.110795 10 avg2muts 1
2 1 331 N E N331E -0.001655 11 avg2muts 1
3 1 331 N F N331F 0.037305 10 avg2muts 1
4 1 331 N G N331G 0.031369 18 avg2muts 1
... ... ... ... ... ... ... ... ... ...
15429 2 531 T R T531R -0.033899 24 avg3muts 2
15430 2 531 T S T531S 0.000405 42 avg3muts 2
15431 2 531 T V T531V -0.023562 24 avg3muts 2
15432 2 531 T W T531W 0.532476 6 avg3muts 2
15433 2 531 T Y T531Y -0.074142 9 avg3muts 2

15434 rows × 9 columns

Now let’s plot the escape. See how you can select mutations based not only on how many times they are seen (averaged over all models in average), but also the number of models in which they are seen.

[10]:
# NBVAL_IGNORE_OUTPUT

model_avg.mut_escape_plot(addtl_slider_stats={"times_seen": 2})
[10]:

Here is the same plot plotting the lowest magnitude escape value across libraries for each mutation:

[11]:
# NBVAL_IGNORE_OUTPUT

model_avg.mut_escape_plot(
    addtl_slider_stats={"times_seen": 2}, avg_type="min_magnitude"
)
[11]:

We can also plot the log fold change in IC90 caused by each mutation:

[12]:
# NBVAL_IGNORE_OUTPUT

model_avg.mut_icXX_plot(
    addtl_slider_stats={"times_seen": 2},
    avg_type="median",
)
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[12], line 3
      1 # NBVAL_IGNORE_OUTPUT
----> 3 model_avg.mut_icXX_plot(
      4     addtl_slider_stats={"times_seen": 2},
      5     avg_type="median",
      6 )

File ~/polyclonal/polyclonal/polyclonal_collection.py:1174, in PolyclonalCollection.mut_icXX_plot(self, x, icXX_col, log_fold_change_icXX_col, min_c, max_c, logbase, check_wt_icXX, biochem_order_aas, df_to_merge, positive_color, negative_color, avg_type, init_n_models, per_model_tooltip, scale_stat_col, **kwargs)
   1172     max_escape_std = kwargs["data_df"][std_col].max()
   1173     kwargs["addtl_slider_stats"][std_col] = max_escape_std
-> 1174     kwargs["addtl_slider_stats_as_max"].append(std_col)
   1176 if "heatmap_min_at_least" not in kwargs:
   1177     kwargs["heatmap_min_at_least"] = -2

KeyError: 'addtl_slider_stats_as_max'
[ ]: