text: import pandas as pd
  
  import pyranges as pr
  import numpy as np
  
  from pyranges.multithreaded import pyrange_apply
  from sorted_nearest.src.introns import find_introns
  
  __all__ = ["genome_bounds", "tile_genome", "GenomicFeaturesMethods"]
  
  class GenomicFeaturesMethods():
  
      """Namespace for methods using feature information.
  
      Accessed through `gr.features`."""
  
      pr = None
  
      def __init__(self, pr):
  
          self.pr = pr
  
      def tss(self):
  
          """Return the transcription start sites.
  
          Returns the 5' for every interval with feature "transcript".
  
          See Also
          --------
          pyranges.genomicfeatures.GenomicFeaturesMethods.tes : return the transcription end sites
  
          Examples
          --------
  
          >>> gr = pr.data.ensembl_gtf()
          >>> gr
          +--------------+------------+--------------+-----------+-----------+------------+--------------+------------+------------------------------------+-------+
          | Chromosome   | Source     | Feature      | Start     | End       | Score      | Strand       | Frame      | gene_biotype                       | +19   |
          | (category)   | (object)   | (category)   | (int32)   | (int32)   | (object)   | (category)   | (object)   | (object)                           | ...   |
          |--------------+------------+--------------+-----------+-----------+------------+--------------+------------+------------------------------------+-------|
          | 1            | havana     | gene         | 11868     | 14409     | .          | +            | .          | transcribed_unprocessed_pseudogene | ...   |
          | 1            | havana     | transcript   | 11868     | 14409     | .          | +            | .          | transcribed_unprocessed_pseudogene | ...   |
          | 1            | havana     | exon         | 11868     | 12227     | .          | +            | .          | transcribed_unprocessed_pseudogene | ...   |
          | 1            | havana     | exon         | 12612     | 12721     | .          | +            | .          | transcribed_unprocessed_pseudogene | ...   |
          | ...          | ...        | ...          | ...       | ...       | ...        | ...          | ...        | ...                                | ...   |
          | 1            | havana     | gene         | 1173055   | 1179555   | .          | -            | .          | lncRNA                             | ...   |
          | 1            | havana     | transcript   | 1173055   | 1179555   | .          | -            | .          | lncRNA                             | ...   |
          | 1            | havana     | exon         | 1179364   | 1179555   | .          | -            | .          | lncRNA                             | ...   |
          | 1            | havana     | exon         | 1173055   | 1176396   | .          | -            | .          | lncRNA                             | ...   |
          +--------------+------------+--------------+-----------+-----------+------------+--------------+------------+------------------------------------+-------+
          Stranded PyRanges object has 2,446 rows and 28 columns from 1 chromosomes.
          For printing, the PyRanges was sorted on Chromosome and Strand.
          19 hidden columns: gene_id, gene_name, gene_source, gene_version, tag, transcript_biotype, transcript_id, transcript_name, transcript_source, transcript_support_level, ... (+ 9 more.)
  
          >>> gr.features.tss()
          +--------------+------------+------------+-----------+-----------+------------+--------------+------------+------------------------------------+-------+
          | Chromosome   | Source     | Feature    | Start     | End       | Score      | Strand       | Frame      | gene_biotype                       | +19   |
          | (category)   | (object)   | (object)   | (int32)   | (int32)   | (object)   | (category)   | (object)   | (object)                           | ...   |
          |--------------+------------+------------+-----------+-----------+------------+--------------+------------+------------------------------------+-------|
          | 1            | havana     | tss        | 11868     | 11869     | .          | +            | .          | transcribed_unprocessed_pseudogene | ...   |
          | 1            | havana     | tss        | 12009     | 12010     | .          | +            | .          | transcribed_unprocessed_pseudogene | ...   |
          | 1            | havana     | tss        | 29553     | 29554     | .          | +            | .          | lncRNA                             | ...   |
          | 1            | havana     | tss        | 30266     | 30267     | .          | +            | .          | lncRNA                             | ...   |
          | ...          | ...        | ...        | ...       | ...       | ...        | ...          | ...        | ...                                | ...   |
          | 1            | havana     | tss        | 1092813   | 1092814   | .          | -            | .          | protein_coding                     | ...   |
          | 1            | havana     | tss        | 1116087   | 1116088   | .          | -            | .          | protein_coding                     | ...   |
          | 1            | havana     | tss        | 1116089   | 1116090   | .          | -            | .          | protein_coding                     | ...   |
          | 1            | havana     | tss        | 1179555   | 1179556   | .          | -            | .          | lncRNA                             | ...   |
          +--------------+------------+------------+-----------+-----------+------------+--------------+------------+------------------------------------+-------+
          Stranded PyRanges object has 280 rows and 28 columns from 1 chromosomes.
          For printing, the PyRanges was sorted on Chromosome and Strand.
          19 hidden columns: gene_id, gene_name, gene_source, gene_version, tag, transcript_biotype, transcript_id, transcript_name, transcript_source, transcript_support_level, ... (+ 9 more.)
          """
  
          pr = self.pr
  
          if not pr.stranded:
              raise Exception(
                  "Cannot compute TSSes or TESes without strand info. Perhaps use extend() or subsequence() or spliced_subsequence() instead?"
              )
  
          pr = pr[pr.Feature == "transcript"]
          pr = pr.apply(lambda df: _tss(df))
  
          pr.Feature = "tss"
  
          return pr
  
      def tes(self, slack=0):
  
          """Return the transcription end sites.
  
          Returns the 3' for every interval with feature "transcript".
  
          See Also
          --------
          pyranges.genomicfeatures.GenomicFeaturesMethods.tss : return the transcription start sites
  
          Examples
          --------
  
          >>> gr = pr.data.ensembl_gtf()
          >>> gr
          +--------------+------------+--------------+-----------+-----------+------------+--------------+------------+------------------------------------+-------+
          | Chromosome   | Source     | Feature      | Start     | End       | Score      | Strand       | Frame      | gene_biotype                       | +19   |
          | (category)   | (object)   | (category)   | (int32)   | (int32)   | (object)   | (category)   | (object)   | (object)                           | ...   |
          |--------------+------------+--------------+-----------+-----------+------------+--------------+------------+------------------------------------+-------|
          | 1            | havana     | gene         | 11868     | 14409     | .          | +            | .          | transcribed_unprocessed_pseudogene | ...   |
          | 1            | havana     | transcript   | 11868     | 14409     | .          | +            | .          | transcribed_unprocessed_pseudogene | ...   |
          | 1            | havana     | exon         | 11868     | 12227     | .          | +            | .          | transcribed_unprocessed_pseudogene | ...   |
          | 1            | havana     | exon         | 12612     | 12721     | .          | +            | .          | transcribed_unprocessed_pseudogene | ...   |
          | ...          | ...        | ...          | ...       | ...       | ...        | ...          | ...        | ...                                | ...   |
          | 1            | havana     | gene         | 1173055   | 1179555   | .          | -            | .          | lncRNA                             | ...   |
          | 1            | havana     | transcript   | 1173055   | 1179555   | .          | -            | .          | lncRNA                             | ...   |
          | 1            | havana     | exon         | 1179364   | 1179555   | .          | -            | .          | lncRNA                             | ...   |
          | 1            | havana     | exon         | 1173055   | 1176396   | .          | -            | .          | lncRNA                             | ...   |
          +--------------+------------+--------------+-----------+-----------+------------+--------------+------------+------------------------------------+-------+
          Stranded PyRanges object has 2,446 rows and 28 columns from 1 chromosomes.
          For printing, the PyRanges was sorted on Chromosome and Strand.
          19 hidden columns: gene_id, gene_name, gene_source, gene_version, tag, transcript_biotype, transcript_id, transcript_name, transcript_source, transcript_support_level, ... (+ 9 more.)
  
          >>> gr.features.tes()
          +--------------+------------+------------+-----------+-----------+------------+--------------+------------+------------------------------------+-------+
          | Chromosome   | Source     | Feature    | Start     | End       | Score      | Strand       | Frame      | gene_biotype                       | +19   |
          | (category)   | (object)   | (object)   | (int32)   | (int32)   | (object)   | (category)   | (object)   | (object)                           | ...   |
          |--------------+------------+------------+-----------+-----------+------------+--------------+------------+------------------------------------+-------|
          | 1            | havana     | tes        | 14409     | 14410     | .          | +            | .          | transcribed_unprocessed_pseudogene | ...   |
          | 1            | havana     | tes        | 13670     | 13671     | .          | +            | .          | transcribed_unprocessed_pseudogene | ...   |
          | 1            | havana     | tes        | 31097     | 31098     | .          | +            | .          | lncRNA                             | ...   |
          | 1            | havana     | tes        | 31109     | 31110     | .          | +            | .          | lncRNA                             | ...   |
          | ...          | ...        | ...        | ...       | ...       | ...        | ...          | ...        | ...                                | ...   |
          | 1            | havana     | tes        | 1092813   | 1092814   | .          | -            | .          | protein_coding                     | ...   |
          | 1            | havana     | tes        | 1116087   | 1116088   | .          | -            | .          | protein_coding                     | ...   |
          | 1            | havana     | tes        | 1116089   | 1116090   | .          | -            | .          | protein_coding                     | ...   |
          | 1            | havana     | tes        | 1179555   | 1179556   | .          | -            | .          | lncRNA                             | ...   |
          +--------------+------------+------------+-----------+-----------+------------+--------------+------------+------------------------------------+-------+
          Stranded PyRanges object has 280 rows and 28 columns from 1 chromosomes.
          For printing, the PyRanges was sorted on Chromosome and Strand.
          19 hidden columns: gene_id, gene_name, gene_source, gene_version, tag, transcript_biotype, transcript_id, transcript_name, transcript_source, transcript_support_level, ... (+ 9 more.)
          """
  
          pr = self.pr
  
          if not pr.stranded:
              raise Exception(
                  "Cannot compute TSSes or TESes without strand info. Perhaps use extend() or subsequence() or spliced_subsequence() instead?"
              )
  
          pr = pr[pr.Feature == "transcript"]
          pr = pr.apply(lambda df: _tes(df))
  
          pr.Feature = "tes"
  
          return pr
  
  
      def introns(self, by="gene", nb_cpu=1):
  
          """Return the introns.
  
          Parameters
          ----------
          by : str, {"gene", "transcript"}, default "gene"
              Whether to find introns per gene or transcript.
  
          nb_cpu: int, default 1
  
              How many cpus to use. Can at most use 1 per chromosome or chromosome/strand tuple.
              Will only lead to speedups on large datasets.
  
          See Also
          --------
          pyranges.genomicfeatures.GenomicFeaturesMethods.tss : return the transcription start sites
  
          Examples
          --------
  
          >>> gr = pr.data.ensembl_gtf()
          >>> gr
          +--------------+------------+--------------+-----------+-----------+------------+--------------+------------+------------------------------------+-------+
          | Chromosome   | Source     | Feature      | Start     | End       | Score      | Strand       | Frame      | gene_biotype                       | +19   |
          | (category)   | (object)   | (category)   | (int32)   | (int32)   | (object)   | (category)   | (object)   | (object)                           | ...   |
          |--------------+------------+--------------+-----------+-----------+------------+--------------+------------+------------------------------------+-------|
          | 1            | havana     | gene         | 11868     | 14409     | .          | +            | .          | transcribed_unprocessed_pseudogene | ...   |
          | 1            | havana     | transcript   | 11868     | 14409     | .          | +            | .          | transcribed_unprocessed_pseudogene | ...   |
          | 1            | havana     | exon         | 11868     | 12227     | .          | +            | .          | transcribed_unprocessed_pseudogene | ...   |
          | 1            | havana     | exon         | 12612     | 12721     | .          | +            | .          | transcribed_unprocessed_pseudogene | ...   |
          | ...          | ...        | ...          | ...       | ...       | ...        | ...          | ...        | ...                                | ...   |
          | 1            | havana     | gene         | 1173055   | 1179555   | .          | -            | .          | lncRNA                             | ...   |
          | 1            | havana     | transcript   | 1173055   | 1179555   | .          | -            | .          | lncRNA                             | ...   |
          | 1            | havana     | exon         | 1179364   | 1179555   | .          | -            | .          | lncRNA                             | ...   |
          | 1            | havana     | exon         | 1173055   | 1176396   | .          | -            | .          | lncRNA                             | ...   |
          +--------------+------------+--------------+-----------+-----------+------------+--------------+------------+------------------------------------+-------+
          Stranded PyRanges object has 2,446 rows and 28 columns from 1 chromosomes.
          For printing, the PyRanges was sorted on Chromosome and Strand.
          19 hidden columns: gene_id, gene_name, gene_source, gene_version, tag, transcript_biotype, transcript_id, transcript_name, transcript_source, transcript_support_level, ... (+ 9 more.)
  
          >>> gr.features.introns(by="gene")
          +--------------+----------------+------------+-----------+-----------+------------+--------------+------------+-------+
          | Chromosome   | Source         | Feature    | Start     | End       | Score      | Strand       | Frame      | +20   |
          | (object)     | (object)       | (object)   | (int32)   | (int32)   | (object)   | (category)   | (object)   | ...   |
          |--------------+----------------+------------+-----------+-----------+------------+--------------+------------+-------|
          | 1            | ensembl_havana | intron     | 1173926   | 1174265   | .          | +            | .          | ...   |
          | 1            | ensembl_havana | intron     | 1174321   | 1174423   | .          | +            | .          | ...   |
          | 1            | ensembl_havana | intron     | 1174489   | 1174520   | .          | +            | .          | ...   |
          | 1            | ensembl_havana | intron     | 1175034   | 1179188   | .          | +            | .          | ...   |
          | ...          | ...            | ...        | ...       | ...       | ...        | ...          | ...        | ...   |
          | 1            | havana         | intron     | 874591    | 875046    | .          | -            | .          | ...   |
          | 1            | havana         | intron     | 875155    | 875525    | .          | -            | .          | ...   |
          | 1            | havana         | intron     | 875625    | 876526    | .          | -            | .          | ...   |
          | 1            | havana         | intron     | 876611    | 876754    | .          | -            | .          | ...   |
          +--------------+----------------+------------+-----------+-----------+------------+--------------+------------+-------+
          Stranded PyRanges object has 311 rows and 28 columns from 1 chromosomes.
          For printing, the PyRanges was sorted on Chromosome and Strand.
          20 hidden columns: gene_biotype, gene_id, gene_name, gene_source, gene_version, tag, transcript_biotype, transcript_id, transcript_name, ... (+ 11 more.)
  
          >>> gr.features.introns(by="transcript")
          +--------------+----------------+------------+-----------+-----------+------------+--------------+------------+----------------------------------+-------+
          | Chromosome   | Source         | Feature    | Start     | End       | Score      | Strand       | Frame      | gene_biotype                     | +19   |
          | (object)     | (object)       | (object)   | (int32)   | (int32)   | (object)   | (category)   | (object)   | (object)                         | ...   |
          |--------------+----------------+------------+-----------+-----------+------------+--------------+------------+----------------------------------+-------|
          | 1            | havana         | intron     | 818202    | 818722    | .          | +            | .          | lncRNA                           | ...   |
          | 1            | ensembl_havana | intron     | 960800    | 961292    | .          | +            | .          | protein_coding                   | ...   |
          | 1            | ensembl_havana | intron     | 961552    | 961628    | .          | +            | .          | protein_coding                   | ...   |
          | 1            | ensembl_havana | intron     | 961750    | 961825    | .          | +            | .          | protein_coding                   | ...   |
          | ...          | ...            | ...        | ...       | ...       | ...        | ...          | ...        | ...                              | ...   |
          | 1            | havana         | intron     | 732207    | 732980    | .          | -            | .          | transcribed_processed_pseudogene | ...   |
          | 1            | havana_tagene  | intron     | 168165    | 169048    | .          | -            | .          | lncRNA                           | ...   |
          | 1            | havana_tagene  | intron     | 165942    | 167958    | .          | -            | .          | lncRNA                           | ...   |
          | 1            | havana_tagene  | intron     | 168165    | 169048    | .          | -            | .          | lncRNA                           | ...   |
          +--------------+----------------+------------+-----------+-----------+------------+--------------+------------+----------------------------------+-------+
          Stranded PyRanges object has 1,043 rows and 28 columns from 1 chromosomes.
          For printing, the PyRanges was sorted on Chromosome and Strand.
          19 hidden columns: gene_id, gene_name, gene_source, gene_version, tag, transcript_biotype, transcript_id, transcript_name, transcript_source, transcript_support_level, ... (+ 9 more.)
          """
  
          kwargs = {"by": by, "nb_cpu": nb_cpu}
          kwargs = pr.pyranges.fill_kwargs(kwargs)
  
          assert by in ["gene", "transcript"]
  
          id_column = by_to_id[by]
          gr = self.pr.sort(id_column)
  
          if not len(gr):
              return pr.PyRanges()
  
          exons = gr.subset(lambda df: df.Feature == "exon")
          exons = exons.merge(by=id_column)
  
          by_gr = gr.subset(lambda df: df.Feature == by)
  
          result = pyrange_apply(_introns2, by_gr, exons, **kwargs)
  
          return pr.PyRanges(result)
  
  def _outside_bounds(df, **kwargs):
  
      chromsizes = kwargs.get("chromsizes")
  
      if not isinstance(chromsizes, dict):
          size_df = chromsizes.df
          chromsizes = {k: v for k, v in zip(size_df.Chromosome, size_df.End)}
  
      size = int(chromsizes[df.Chromosome.iloc[0]])
      clip = kwargs.get("clip", False)
  
      ends_outside = df.End > size
      if not clip:  # i.e. remove
          df = df[~ends_outside]
      else:
          starts_inside = df.Start <= size
          df.loc[ends_outside & starts_inside, "End"] = size
  
      return df
  
  
  def genome_bounds(gr, chromsizes, clip=False):
  
      """Remove or clip intervals outside of genome bounds.
  
      Parameters
      ----------
      chromsizes : dict or PyRanges
  
          Dict or PyRanges describing the lengths of the chromosomes.
  
      clip : bool, default False
  
          Part of interval within bounds.
  
      Examples
      --------
  
      >>> d = {"Chromosome": [1, 1, 3], "Start": [1, 249250600, 5], "End": [2, 249250640, 7]}
      >>> gr = pr.from_dict(d)
      >>> gr
      +--------------+-----------+-----------+
      |   Chromosome |     Start |       End |
      |   (category) |   (int32) |   (int32) |
      |--------------+-----------+-----------|
      |            1 |         1 |         2 |
      |            1 | 249250600 | 249250640 |
      |            3 |         5 |         7 |
      +--------------+-----------+-----------+
      Unstranded PyRanges object has 3 rows and 3 columns from 2 chromosomes.
      For printing, the PyRanges was sorted on Chromosome.
  
      >>> chromsizes = {"1": 249250621, "3": 500}
      >>> chromsizes
      {'1': 249250621, '3': 500}
  
      >>> pr.gf.genome_bounds(gr, chromsizes)
      +--------------+-----------+-----------+
      |   Chromosome |     Start |       End |
      |   (category) |   (int32) |   (int32) |
      |--------------+-----------+-----------|
      |            1 |         1 |         2 |
      |            3 |         5 |         7 |
      +--------------+-----------+-----------+
      Unstranded PyRanges object has 2 rows and 3 columns from 2 chromosomes.
      For printing, the PyRanges was sorted on Chromosome.
  
      >>> pr.gf.genome_bounds(gr, chromsizes, clip=True)
      +--------------+-----------+-----------+
      |   Chromosome |     Start |       End |
      |   (category) |   (int32) |   (int32) |
      |--------------+-----------+-----------|
      |            1 |         1 |         2 |
      |            1 | 249250600 | 249250621 |
      |            3 |         5 |         7 |
      +--------------+-----------+-----------+
      Unstranded PyRanges object has 3 rows and 3 columns from 2 chromosomes.
      For printing, the PyRanges was sorted on Chromosome.
  
      >>> del chromsizes['3']
      >>> chromsizes
      {'1': 249250621}
  
      >>> pr.gf.genome_bounds(gr, chromsizes)
      Traceback (most recent call last):
      ...
      KeyError: '3'
      """
  
      if not isinstance(chromsizes, dict):
          chromsizes = {
              k: v
              for k, v in zip(chromsizes.Chromosome, chromsizes.End)
          }
  
      return gr.apply(_outside_bounds, chromsizes=chromsizes, clip=clip)
  
  
  
  
  def _last_tile(df, **kwargs):
      # do not need copy, since it is only used internally by
      # tile_genome
      # df = df.copy()
      sizes = kwargs.get("sizes")
      size = sizes[df.Chromosome.iloc[0]].End.iloc[0]
      df.loc[df.tail(1).index, "End"] = size
  
      return df
  
  
  def tile_genome(genome, tile_size, tile_last=False):
  
      """Create a tiled genome.
  
      Parameters
      ----------
      chromsizes : dict or PyRanges
  
          Dict or PyRanges describing the lengths of the chromosomes.
  
      tile_size : int
          Length of the tiles.
  
      tile_last : bool, default False
  
          Use genome length as end of last tile.
  
      See Also
      --------
  
      pyranges.PyRanges.tile : split intervals into adjacent non-overlapping tiles.
  
      Examples
      --------
  
      >>> chromsizes = pr.data.chromsizes()
      >>> chromsizes
      +--------------+-----------+-----------+
      | Chromosome   | Start     | End       |
      | (category)   | (int32)   | (int32)   |
      |--------------+-----------+-----------|
      | chr1         | 0         | 249250621 |
      | chr2         | 0         | 243199373 |
      | chr3         | 0         | 198022430 |
      | chr4         | 0         | 191154276 |
      | ...          | ...       | ...       |
      | chr22        | 0         | 51304566  |
      | chrM         | 0         | 16571     |
      | chrX         | 0         | 155270560 |
      | chrY         | 0         | 59373566  |
      +--------------+-----------+-----------+
      Unstranded PyRanges object has 25 rows and 3 columns from 25 chromosomes.
      For printing, the PyRanges was sorted on Chromosome.
  
      >>> pr.gf.tile_genome(chromsizes, int(1e6))
      +--------------+-----------+-----------+
      | Chromosome   | Start     | End       |
      | (category)   | (int32)   | (int32)   |
      |--------------+-----------+-----------|
      | chr1         | 0         | 1000000   |
      | chr1         | 1000000   | 2000000   |
      | chr1         | 2000000   | 3000000   |
      | chr1         | 3000000   | 4000000   |
      | ...          | ...       | ...       |
      | chrY         | 56000000  | 57000000  |
      | chrY         | 57000000  | 58000000  |
      | chrY         | 58000000  | 59000000  |
      | chrY         | 59000000  | 59373566  |
      +--------------+-----------+-----------+
      Unstranded PyRanges object has 3,114 rows and 3 columns from 25 chromosomes.
      For printing, the PyRanges was sorted on Chromosome.
      """
  
      if isinstance(genome, dict):
          chromosomes, ends = list(genome.keys()), list(genome.values())
          df = pd.DataFrame({"Chromosome": chromosomes, "Start": 0, "End": ends})
          genome = pr.PyRanges(df)
  
      gr = genome.tile(tile_size)
  
      if not tile_last:
          gr = gr.apply(_last_tile, sizes=genome)
  
      return gr
  
  
  def _keep_transcript_with_most_exons(df):
  
      transcripts_with_most_exons = []
  
      for _, gdf in df.groupby("gene_id"):
  
          max_exon = gdf.exon_number.max()
          max_transcript = gdf.loc[gdf.exon_number == max_exon].Transcript.iloc[0]
  
          max_rows = gdf.loc[gdf.Transcript == max_transcript]
  
          transcripts_with_most_exons.append(max_rows)
  
      return pd.concat(transcripts_with_most_exons).reset_index(drop=True)
  
  
  
  def filter_transcripts(df, keep="most_exons"):
  
      return _keep_transcript_with_most_exons(df)
  
  
  def _tss(df, slack=0):
  
      intype = df.Start.dtype
  
      tss_pos = df.loc[df.Strand == "+"]
  
      tss_neg = df.loc[df.Strand == "-"].copy()
  
      # pd.options.mode.chained_assignment = None
      tss_neg.loc[:, "Start"] = tss_neg.End - 1
  
      # pd.options.mode.chained_assignment = "warn"
      tss = pd.concat([tss_pos, tss_neg], sort=False)
      tss["End"] = tss.Start + 1
      tss.End = tss.End + slack
      tss.Start = tss.Start - slack
      tss.loc[tss.Start < 0, "Start"] = 0
  
      tss.index = range(len(tss))
  
      tss[["Start", "End"]] = tss[["Start", "End"]].astype(intype)
  
      return tss
  
  
  def _tes(df, slack=0):
  
      intype = df.Start.dtype
      # df = self.df
  
      tes_pos = df.loc[df.Strand == "+"]
  
      tes_neg = df.loc[df.Strand == "-"].copy()
  
      # pd.options.mode.chained_assignment = None
      tes_neg.loc[:, "End"] = tes_neg.Start + 1
  
      # pd.options.mode.chained_assignment = "warn"
      tes = pd.concat([tes_pos, tes_neg], sort=False)
      tes["Start"] = tes.End - 1
      tes.End = tes.End + slack
      tes.Start = tes.Start - slack
      tes.loc[tes.Start < 0, "Start"] = 0
  
      tes.index = range(len(tes))
  
      tes[["Start", "End"]] = tes[["Start", "End"]].astype(intype)
  
      return tes
  
  
  by_to_id = {"gene": "gene_id", "transcript": "transcript_id"}
  
  
  def _introns2(df, exons, **kwargs):
  
      """TODO: refactor
  
      """
  
      if df.empty or exons.empty:
          return None
  
      original_order = df.columns
      by = kwargs["by"]
      id_column = by_to_id[by]
  
      exons = exons[["Start", "End", id_column]]
      genes = df[["Start", "End", id_column]]
      exons.columns = ["Start", "End", "by_id"]
      genes.columns = ["Start", "End", "by_id"]
  
      intersection = pd.Series(
          np.intersect1d(exons["by_id"], genes["by_id"]))
      if len(intersection) == 0:
          return None
  
      exons = exons[exons["by_id"].isin(intersection)].reset_index(
          drop=True).sort_values(["by_id", "Start"])
      genes = genes[genes["by_id"].isin(intersection)].reset_index(
          drop=True).sort_values(["by_id", "Start"])
      df = df[df[id_column].isin(intersection)].reset_index(
          drop=True)
  
      assert len(genes) == len(
          genes.drop_duplicates("by_id")
      ), "The {id_column}s need to be unique to compute the introns.".format(
          id_column=id_column)
  
      exon_ids = (exons["by_id"].shift() != exons["by_id"])
      by_ids = pd.Series(range(1, len(genes) + 1))
      df.insert(0, "__temp__", by_ids)
  
      if len(exons) > 1 and exons["by_id"].iloc[0] == exons["by_id"].iloc[1]:
          exon_ids.iloc[0] = False
          exon_ids = exon_ids.cumsum() + 1
      else:
          exon_ids = exon_ids.cumsum()
  
      assert (by_ids == exon_ids.drop_duplicates().values).all()
      starts, ends, ids = find_introns(genes.Start.values, genes.End.values,
                                       by_ids.values, exons.Start.values,
                                       exons.End.values, exon_ids.values)
  
      introns = pd.DataFrame(
          data={
              "Chromosome": df.Chromosome.iloc[0],
              "Start": starts,
              "End": ends,
              "by_id": ids
          })
  
      vc = introns["by_id"].value_counts(sort=False).to_frame().reset_index()
      vc.columns = ["by_id", "counts"]
  
      genes_without_introns = pd.DataFrame(
          data={
              "by_id": np.setdiff1d(by_ids.values, vc.by_id.values),
              "counts": 0
          })
  
      vc = pd.concat([vc, genes_without_introns]).sort_values("by_id")
  
      original_ids = np.repeat(vc.by_id, vc.counts).to_frame()
      original_ids = original_ids.merge(
          df[["__temp__", id_column]],
          right_on="__temp__",
          left_on="by_id",
          suffixes=("_drop", ""))
      original_ids = original_ids.drop(
          ["__temp__"] +
          [c for c in original_ids.columns if c.endswith("_drop")],
          axis=1).sort_values("by_id")
      introns.loc[:, "by_id"] = original_ids[id_column].values
      introns = introns.merge(
          df, left_on="by_id", right_on=id_column, suffixes=("", "_dropme"))
      introns = introns.drop(
          [c for c in introns.columns if c.endswith("_dropme")], axis=1)
  
      if introns.Feature.dtype.name == "category" and not "intron" in introns.Feature.cat.categories:
          introns.Feature.cat.add_categories(["intron"], inplace=True)
      introns.loc[:, "Feature"] = "intron"
  
      introns = introns[original_order]
  
      return introns
  