docs: def expand_side_table(df):
      """
      Perform equivalent of 'expand side table' in Perseus by folding
      Multiplicity columns down onto duplicate rows
  
      The id is remapped to UID___Multiplicity, which
      is different to Perseus behaviour, but prevents accidental of
      non-matching rows from occurring later in analysis.
  
      :param df:
      :return:
      """
  
      df = df.copy()
  
      idx = df.index.names
      df.reset_index(inplace=True)
  
      def strip_multiplicity(df):
          df.columns = [c[:-4] for c in df.columns]
          return df
          
      def strip_multiple(s):
          for sr in ['___1','___2','___3']:
              if s.endswith(sr):
                  s = s[:-4]
          return s
  
      base = df.filter(regex='.*(?<!___\d)$')
      
      # Remove columns that will match ripped multiplicity columns
      for c in df.columns.values:
          if strip_multiple(c) != c and strip_multiple(c) in list(base.columns.values):
              base.drop(strip_multiple(c), axis=1, inplace=True)
  
      multi1 = df.filter(regex='^.*___1$')
      multi1 = strip_multiplicity(multi1)
      multi1['Multiplicity'] = '___1'
      multi1 = pd.concat([multi1, base], axis=1)
  
      multi2 = df.filter(regex='^.*___2$')
      multi2 = strip_multiplicity(multi2)
      multi2['Multiplicity'] = '___2'
      multi2 = pd.concat([multi2, base], axis=1)
  
      multi3 = df.filter(regex='^.*___3$')
      multi3 = strip_multiplicity(multi3)
      multi3['Multiplicity'] = '___3'
      multi3 = pd.concat([multi3, base], axis=1)
  
      df = pd.concat([multi1, multi2, multi3], axis=0)
      df['id'] = ["%s%s" % (a, b) for a, b in zip(df['id'], df['Multiplicity'])]
  
      if idx[0] is not None:
          df.set_index(idx, inplace=True)
  
      return df