docs: def predict_from_mutation_effects(
              self,
              effects,
              transcript_expression_dict=None,
              gene_expression_dict=None):
          """Given a Varcode.EffectCollection of predicted protein effects,
          return predicted epitopes around each mutation.
  
          Parameters
          ----------
          effects : Varcode.EffectCollection
  
          transcript_expression_dict : dict
              Dictionary mapping transcript IDs to RNA expression estimates. Used
              both for transcript expression filtering and for selecting the
              most abundant transcript for a particular variant. If omitted then
              transcript selection is done using priority of variant effects and
              transcript length.
  
          gene_expression_dict : dict, optional
              Dictionary mapping gene IDs to RNA expression estimates
  
          Returns DataFrame with the following columns:
              - variant
              - gene
              - gene_id
              - transcript_id
              - transcript_name
              - effect
              - effect_type
              - peptide
              - peptide_offset
              - peptide_length
              - allele
              - affinity
              - percentile_rank
              - prediction_method_name
              - contains_mutant_residues
              - mutation_start_in_peptide
              - mutation_end_in_peptide
  
          Optionall will also include the following columns if corresponding
          expression dictionary inputs are provided:
              - gene_expression
              - transcript_expression
          """
  
          # we only care about effects which impact the coding sequence of a
          # protein
          effects = filter_silent_and_noncoding_effects(effects)
  
          effects = apply_effect_expression_filters(
              effects,
              transcript_expression_dict=transcript_expression_dict,
              transcript_expression_threshold=self.min_transcript_expression,
              gene_expression_dict=gene_expression_dict,
              gene_expression_threshold=self.min_gene_expression)
  
          # group by variants, so that we end up with only one mutant
          # sequence per mutation
          variant_effect_groups = effects.groupby_variant()
  
          if len(variant_effect_groups) == 0:
              logging.warn("No candidates for MHC binding prediction")
              return []
  
          if transcript_expression_dict:
              # if expression data is available, then for each variant
              # keep the effect annotation for the most abundant transcript
              top_effects = [
                  variant_effects.top_expression_effect(
                      transcript_expression_dict)
                  for variant_effects in variant_effect_groups.values()
              ]
          else:
              # if no transcript abundance data is available, then
              # for each variant keep the effect with the most significant
              # predicted effect on the protein sequence, along with using
              # transcript/CDS length as a tie-breaker for effects with the same
              # priority.
              top_effects = [
                  variant_effects.top_priority_effect()
                  for variant_effects in variant_effect_groups.values()
              ]
  
          # 1) dictionary mapping varcode effect objects to subsequences
          #    around each mutation
          # 2) dictionary mapping varcode effect to start offset of subsequence
          #    within the full mutant protein sequence
          effect_to_subsequence_dict, effect_to_offset_dict = \
              protein_subsequences_around_mutations(
                  effects=top_effects,
                  padding_around_mutation=self.padding_around_mutation)
  
          # since we know that each set of variant effects has been
          # reduced to a single 'top priority' effect, we can uniquely
          # identify each variant sequence by its original genomic variant
          variant_string_to_effect_dict = {
              effect.variant.short_description: effect
              for effect in effect_to_subsequence_dict.keys()
          }
          variant_string_to_subsequence_dict = {
              effect.variant.short_description: subseq
              for (effect, subseq) in effect_to_subsequence_dict.items()
          }
          variant_string_to_offset_dict = {
              effect.variant.short_description: subseq_offset
              for (effect, subseq_offset) in effect_to_offset_dict.items()
          }
          df = self.predict_from_named_sequences(variant_string_to_subsequence_dict)
          logging.info("MHC predictor returned %d peptide binding predictions" % (
              len(df)))
  
          # since we used variant descrptions as the name of each sequence
          # let's rename that column to be more informative
          df = df.rename(columns={"source_sequence_name": "variant"})
  
          # adjust offset to be relative to start of protein, rather
          # than whatever subsequence we used for prediction
          def compute_peptide_offset_relative_to_protein(row):
              subsequence_offset = variant_string_to_offset_dict[row.variant]
              return row.peptide_offset + subsequence_offset
  
          df["peptide_offset"] = df.apply(
              compute_peptide_offset_relative_to_protein,
              axis=1)
  
          if self.ic50_cutoff:
              df = df[df.affinity <= self.ic50_cutoff]
              logging.info("Kept %d predictions after filtering affinity <= %f" % (
                  len(df), self.ic50_cutoff))
  
          if self.percentile_cutoff:
              df = df[df.percentile_rank <= self.percentile_cutoff]
              logging.info("Kept %d predictions after filtering percentile <= %f" % (
                  len(df), self.percentile_cutoff))
  
          extra_columns = OrderedDict([
              ('gene', []),
              ('gene_id', []),
              ('transcript_id', []),
              ('transcript_name', []),
              ('effect', []),
              ('effect_type', []),
              ('contains_mutant_residues', []),
              ('mutation_start_in_peptide', []),
              ('mutation_end_in_peptide', []),
          ])
          if gene_expression_dict is not None:
              extra_columns["gene_expression"] = []
          if transcript_expression_dict is not None:
              extra_columns["transcript_expression"] = []
  
          for _, row in df.iterrows():
              effect = variant_string_to_effect_dict[row.variant]
              mutation_start_in_protein = effect.aa_mutation_start_offset
              mutation_end_in_protein = effect.aa_mutation_end_offset
              peptide_length = len(row.peptide)
              is_mutant = contains_mutant_residues(
                  peptide_start_in_protein=row.peptide_offset,
                  peptide_length=peptide_length,
                  mutation_start_in_protein=mutation_start_in_protein,
                  mutation_end_in_protein=mutation_end_in_protein)
              if is_mutant:
                  mutation_start_in_peptide, mutation_end_in_peptide = peptide_mutation_interval(
                      peptide_start_in_protein=row.peptide_offset,
                      peptide_length=peptide_length,
                      mutation_start_in_protein=mutation_start_in_protein,
                      mutation_end_in_protein=mutation_end_in_protein)
              else:
                  mutation_start_in_peptide = mutation_end_in_peptide = None
  
              extra_columns["gene"].append(effect.gene_name)
              gene_id = effect.gene_id
              extra_columns["gene_id"].append(gene_id)
              if gene_expression_dict is not None:
                  extra_columns["gene_expression"].append(
                      gene_expression_dict.get(gene_id, 0.0))
  
              transcript_id = effect.transcript_id
              extra_columns["transcript_id"].append(transcript_id)
              extra_columns["transcript_name"].append(effect.transcript_name)
              if transcript_expression_dict is not None:
                  extra_columns["transcript_expression"].append(
                      transcript_expression_dict.get(transcript_id, 0.0))
  
              extra_columns["effect"].append(effect.short_description)
              extra_columns["effect_type"].append(effect.__class__.__name__)
  
              extra_columns["contains_mutant_residues"].append(is_mutant)
              extra_columns["mutation_start_in_peptide"].append(mutation_start_in_peptide)
              extra_columns["mutation_end_in_peptide"].append(mutation_end_in_peptide)
  
          for col, values in extra_columns.items():
              df[col] = values
  
          # TODO: add extra boolean field
          #   novel = is_mutant | not_in_reference
          # Requires keeping a quick lookup structure for all peptides in
          # the reference proteome
          if self.only_novel_epitopes:
              df = df[df.contains_mutant_residues]
  
          return df