docs: def merge_sample(sample, sample_subann,
                   data_sources=None, derived_attributes=None):
      """
      Use merge table (subannotation) data to augment/modify Sample.
  
      :param Sample sample: sample to modify via merge table data
      :param sample_subann: data with which to alter Sample
      :param Mapping data_sources: collection of named paths to data locations,
          optional
      :param Iterable[str] derived_attributes: names of attributes for which
          corresponding Sample attribute's value is data-derived, optional
      :return Set[str]: names of columns/attributes that were merged
      """
  
      merged_attrs = {}
  
      if sample_subann is None:
          _LOGGER.log(5, "No data for sample merge, skipping")
          return merged_attrs
  
      if SAMPLE_NAME_COLNAME not in sample_subann.columns:
          raise KeyError(
              "Merge table requires a column named '{}'.".
                  format(SAMPLE_NAME_COLNAME))
  
      _LOGGER.debug("Merging Sample with data sources: {}".
                    format(data_sources))
  
      # Hash derived columns for faster lookup in case of many samples/columns.
      derived_attributes = set(derived_attributes or [])
      _LOGGER.debug("Merging Sample with derived attributes: {}".
                    format(derived_attributes))
  
      sample_name = getattr(sample, SAMPLE_NAME_COLNAME)
      sample_indexer = sample_subann[SAMPLE_NAME_COLNAME] == sample_name
      this_sample_rows = sample_subann[sample_indexer]
      if len(this_sample_rows) == 0:
          _LOGGER.debug("No merge rows for sample '%s', skipping", sample.name)
          return merged_attrs
      _LOGGER.log(5, "%d rows to merge", len(this_sample_rows))
      _LOGGER.log(5, "Merge rows dict: {}".format(this_sample_rows.to_dict()))
  
      # For each row in the merge table of this sample:
      # 1) populate any derived columns
      # 2) derived columns --> space-delimited strings
      # 3) update the sample values with the merge table
      # Keep track of merged cols,
      # so we don't re-derive them later.
      merged_attrs = {key: "" for key in this_sample_rows.columns}
      subsamples = []
      _LOGGER.debug(this_sample_rows)
      for subsample_row_id, row in this_sample_rows.iterrows():
          try:
              row['subsample_name']
          except KeyError:
              # default to a numeric count on subsamples if they aren't named
              row['subsample_name'] = str(subsample_row_id)
          subann_unit = Subsample(row)
          subsamples.append(subann_unit)
          _LOGGER.debug(subsamples)
          rowdata = row.to_dict()
  
          # Iterate over column names to avoid Python3 RuntimeError for
          # during-iteration change of dictionary size.
          for attr_name in this_sample_rows.columns:
              if attr_name == SAMPLE_NAME_COLNAME or \
                              attr_name not in derived_attributes:
                  _LOGGER.log(5, "Skipping merger of attribute '%s'", attr_name)
                  continue
  
              attr_value = rowdata[attr_name]
  
              # Initialize key in parent dict.
              col_key = attr_name + COL_KEY_SUFFIX
              merged_attrs[col_key] = ""
              rowdata[col_key] = attr_value
              data_src_path = sample.locate_data_source(
                  data_sources, attr_name, source_key=rowdata[attr_name],
                  extra_vars=rowdata)  # 1)
              rowdata[attr_name] = data_src_path
  
          _LOGGER.log(5, "Adding derived attributes")
  
          for attr in derived_attributes:
  
              # Skip over any attributes that the sample lacks or that are
              # covered by the data from the current (row's) data.
              if not hasattr(sample, attr) or attr in rowdata:
                  _LOGGER.log(5, "Skipping column: '%s'", attr)
                  continue
  
              # Map key to sample's value for the attribute given by column name.
              col_key = attr + COL_KEY_SUFFIX
              rowdata[col_key] = getattr(sample, attr)
              # Map the col/attr name itself to the populated data source 
              # template string.
              rowdata[attr] = sample.locate_data_source(
                  data_sources, attr, source_key=getattr(sample, attr),
                  extra_vars=rowdata)
  
          # TODO: this (below) is where we could maintain grouped values
          # TODO (cont.): as a collection and defer the true merger.
  
          # Since we are now jamming multiple (merged) entries into a single
          # attribute on a Sample, we have to join the individual items into a
          # space-delimited string and then use that value as the Sample
          # attribute. The intended use case for this sort of merge is for
          # multiple data source paths associated with a single Sample, hence
          # the choice of space-delimited string as the joined-/merged-entry
          # format--it's what's most amenable to use in building up an argument
          # string for a pipeline command.
          for attname, attval in rowdata.items():
              if attname == SAMPLE_NAME_COLNAME or not attval:
                  _LOGGER.log(5, "Skipping KV: {}={}".format(attname, attval))
                  continue
              _LOGGER.log(5, "merge: sample '%s'; '%s'='%s'",
                          str(sample.name), str(attname), str(attval))
              if attname not in merged_attrs:
                  new_attval = str(attval).rstrip()
              else:
                  new_attval = "{} {}".format(merged_attrs[attname],
                                              str(attval)).strip()
              merged_attrs[attname] = new_attval  # 2)
              _LOGGER.log(5, "Stored '%s' as value for '%s' in merged_attrs",
                          new_attval, attname)
  
      # If present, remove sample name from the data with which to update sample.
      merged_attrs.pop(SAMPLE_NAME_COLNAME, None)
  
      _LOGGER.log(5, "Updating Sample {}: {}".format(sample.name, merged_attrs))
      sample.update(merged_attrs)  # 3)
      sample.merged_cols = merged_attrs
      sample.merged = True
      sample.subsamples = subsamples
  
      return sample