def clean_obs_names(data, base='<STR_LIT>', ID_length=<NUM_LIT:12>, copy=False):
def get_base_list(name, base):<EOL><INDENT>base_list = base<EOL>while re.search(base_list + base, name) is not None:<EOL><INDENT>base_list += base<EOL><DEDENT>if len(base_list) == <NUM_LIT:0>:<EOL><INDENT>raise ValueError('<STR_LIT>', name)<EOL><DEDENT>return base_list<EOL><DEDENT>adata = data.copy() if copy else data<EOL>names = adata.obs_names<EOL>base_list = get_base_list(names[<NUM_LIT:0>], base)<EOL>if len(np.unique([len(name) for name in adata.obs_names])) == <NUM_LIT:1>:<EOL><INDENT>start, end = re.search(base_list, names[<NUM_LIT:0>]).span()<EOL>newIDs = [name[start:end] for name in names]<EOL>start, end = <NUM_LIT:0>, len(newIDs[<NUM_LIT:0>])<EOL>for i in range(end - ID_length):<EOL><INDENT>if np.any([ID[i] not in base for ID in newIDs]): start += <NUM_LIT:1><EOL>if np.any([ID[::-<NUM_LIT:1>][i] not in base for ID in newIDs]): end -= <NUM_LIT:1><EOL><DEDENT>newIDs = [ID[start:end] for ID in newIDs]<EOL>prefixes = [names[i].replace(newIDs[i], '<STR_LIT>') for i in range(len(names))]<EOL><DEDENT>else:<EOL><INDENT>prefixes, newIDs = [], []<EOL>for name in names:<EOL><INDENT>match = re.search(base_list, name)<EOL>newID = re.search(get_base_list(name, base), name).group() if match is None else match.group()<EOL>newIDs.append(newID)<EOL>prefixes.append(name.replace(newID, '<STR_LIT>'))<EOL><DEDENT><DEDENT>adata.obs_names = newIDs<EOL>if len(prefixes[<NUM_LIT:0>]) > <NUM_LIT:0> and len(np.unique(prefixes)) > <NUM_LIT:1>:<EOL><INDENT>adata.obs['<STR_LIT>'] = pd.Categorical(prefixes) if len(np.unique(prefixes)) < adata.n_obs else prefixes<EOL><DEDENT>adata.obs_names_make_unique()<EOL>return adata if copy else None<EOL>
Cleans up the obs_names and identifies sample names.
    For example an obs_name 'samlple1_AGTCdate' is changed to 'AGTC' of the sample 'sample1_date'.
    The sample name is then saved in obs['sample_batch'].
    The genetic codes are identified according to according to https://www.neb.com/tools-and-resources/usage-guidelines/the-genetic-code.

    Arguments
    ---------
    adata: :class:`~anndata.AnnData`
        Annotated data matrix.
    base: `str` (default: `[AGTCBDHKMNRSVWY]`)
        Genetic code letters to be identified.
    ID_length: `int` (default: 12)
        Length of the Genetic Codes in the samples.
    copy: `bool` (default: `False`)
        Return a copy instead of writing to adata.

    Returns
    -------
    Returns or updates `adata` with the attributes
    obs_names: list
        updated names of the observations
    sample_batch: `.obs`
        names of the identified sample batches