Aim of the Code:
The aim of the function I'm working on is to take a particular column from a DataFrame that contains multiple <tag><content>
pairs in the form of a string and expand them into individual columns in an efficient way.
An example file can be found here (aprox. 3M entries), which can be loaded as a DataFrame with:
def load_refseq_coordinates(ifile):
# https://m.ensembl.org/info/website/upload/gff3.html
return pd.read_csv(ifile, sep='\t', comment='#',
names=['seqid', 'source', 'type', 'start', 'end', 'score',
'strand', 'phase', 'attributes'])
return df
The loaded DataFrame will have a column named attributes
that has content such as:
attributes |
---|
ID=NC_000001.11:1..248956422;Dbxref=taxon:9606;Name=1;chromosome=1;gbkey=Src;genome=chromosome;mol_type=genomic DNA |
with different pairs separated by ;
and <tag><content>
pairs defined with =
.
The aim would, then to keep the rest of the columns of the DataFrame and get this one converted to:
attributes.ID | attributes.Dbxref | attributes.Name | attributes.chromosome | attributes.gbkey | attributes.genome | attributes.mol_type |
---|---|---|---|---|---|---|
NC_000001.11:1..248956422 | taxon:9606 | 1 | 1 | Src | chromosome | genomic DNA |
And the same for all rows of the DataFrame.
The Issue:
All the solutions I've found so far do not scale proficiently and applying them to the 3M entry file takes just too long.
In the last 3 cases I'll show, performance seems better, as I can test up to 100K in around 4s (with that time is around 10K rows in the first solutions), but it is still poor performance for the 3M rows.
Current Solutions:
Note that:
- I'm running this in a Jupyter Notebook, thus the
%timeit
call. - I'm only checking up to 10k rows to see how it scales (gets too long with more), except the last 3 solutions, as they perform better (thus I time them for 100K rows)
1. Series.apply a function that does the double string split
def expand_info_string1(df, column='info', entity_sep=';', id_sep='='):
def do_expand(cell, entity_seq, id_sep):
data = [x.split(id_sep) for x in str(cell).split(entity_seq)]
return pd.Series(list(zip(*data))[-1], index=list(zip(*data))[0])
info = df[column].apply(do_expand, entity_seq=entity_sep, id_sep=id_sep)
return pd.concat([df.drop(columns=[column]), info.add_prefix(f'{column}.')], axis=1).fillna('')
%timeit dd = expand_info_string1(df.sample(10), 'attributes')
%timeit dd = expand_info_string1(df.sample(100), 'attributes')
%timeit dd = expand_info_string1(df.sample(1000), 'attributes')
%timeit dd = expand_info_string1(df.sample(10000), 'attributes')
330 ms ± 267 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
299 ms ± 18.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
708 ms ± 375 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
4.4 s ± 263 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
2. Series.str.split + Series.apply a function to do 1 split
def expand_info_string3(df, column='info', entity_sep=';', id_sep='='):
def do_expand(cell, id_sep):
data = [str(x).split(id_sep) for x in cell]
return pd.Series(list(zip(*data))[-1], index=list(zip(*data))[0])
info = df[column].str.split(entity_sep).apply(do_expand, id_sep=id_sep)
return pd.concat([df.drop(columns=[column]), info.add_prefix(f'{column}.')], axis=1).fillna('')
%timeit dd = expand_info_string3(df.sample(10), 'attributes')
%timeit dd = expand_info_string3(df.sample(100), 'attributes')
%timeit dd = expand_info_string3(df.sample(1000), 'attributes')
%timeit dd = expand_info_string3(df.sample(10000), 'attributes')
90 ms ± 28.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
338 ms ± 66.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
553 ms ± 13.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
4.55 s ± 379 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
3. Series.str.split(expand=True) + Series.apply a function to do 1 split
def expand_info_string2(df, column='info', entity_sep=';', id_sep='='):
def do_expand(cell):
data = cell.dropna().values
return pd.Series(list(zip(*data))[-1], index=list(zip(*data))[0])
info = df[column].str.split(entity_sep, expand=True).apply(lambda cell: cell.str.split(id_sep), axis=1).apply(do_expand, axis=1)
return pd.concat([df.drop(columns=[column]), info.add_prefix(f'{column}.')], axis=1).fillna('')
%timeit dd = expand_info_string2(df.sample(10), 'attributes')
%timeit dd = expand_info_string2(df.sample(100), 'attributes')
%timeit dd = expand_info_string2(df.sample(1000), 'attributes')
%timeit dd = expand_info_string2(df.sample(10000), 'attributes')
236 ms ± 44.2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
262 ms ± 19.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
1.35 s ± 281 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
11.2 s ± 313 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
4. Series.str.extractall + regex
def expand_info_regex(df, column='info', entity_sep=';', id_sep='='):
info_re = re.compile("([^;]+?)=(?:([^;]+))?")
info= (df[column].str.extractall(info_re).reset_index(level=1, drop=True)
.set_index(0, append=True)[1]
.unstack(level=1))
return pd.concat([df.drop(columns=[column]), info.add_prefix(f'{column}.')], axis=1).fillna('')
%timeit dd = expand_info_regex(df.sample(10), 'attributes')
%timeit dd = expand_info_regex(df.sample(100), 'attributes')
%timeit dd = expand_info_regex(df.sample(1000), 'attributes')
%timeit dd = expand_info_regex(df.sample(10000), 'attributes')
%timeit dd = expand_info_regex(df.sample(100000), 'attributes')
122 ms ± 3.41 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
129 ms ± 3.38 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
157 ms ± 3.03 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
474 ms ± 9.87 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
4.28 s ± 33.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
5. Series.str.split(expand).stack + DataFrame.groupby
def expand_info_stackgroup(df, column='info', entity_sep=';', id_sep='='):
info = (df[column].str.split(entity_sep, expand=True).stack()
.str.split(id_sep, expand=True).reset_index()
.drop(columns='level_1').groupby(['level_0', 0]).first()
.unstack(fill_value='').reset_index(drop=True))
info.columns = [x[1] for x in info.columns]
info.index = df.index
return pd.concat([df.drop(columns=[column]), info.add_prefix(f'{column}.')], axis=1)
%timeit dd = expand_info_stackgroup(df.sample(10), 'attributes')
%timeit dd = expand_info_stackgroup(df.sample(100), 'attributes')
%timeit dd = expand_info_stackgroup(df.sample(1000), 'attributes')
%timeit dd = expand_info_stackgroup(df.sample(10000), 'attributes')
%timeit dd = expand_info_stackgroup(df.sample(100000), 'attributes')
178 ms ± 24.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
269 ms ± 114 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
231 ms ± 87.2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
494 ms ± 21.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
4.51 s ± 99.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
6. Series.str.split(expand).stack + DataFrame.set_index
def expand_info_stackindex(df, column='info', entity_sep=';', id_sep='='):
info = (df[column].str.split(entity_sep, expand=True).stack()
.str.split(id_sep, expand=True).reset_index()
.drop(columns='level_1').set_index(['level_0', 0])
.unstack(fill_value='').reset_index(drop=True))
info.columns = [x[1] for x in info.columns]
info.index = df.index
return pd.concat([df.drop(columns=[column]), info.add_prefix(f'{column}.')], axis=1)
%timeit dd = expand_info_stackindex(df.sample(10), 'attributes')
%timeit dd = expand_info_stackindex(df.sample(100), 'attributes')
%timeit dd = expand_info_stackindex(df.sample(1000), 'attributes')
%timeit dd = expand_info_stackindex(df.sample(10000), 'attributes')
%timeit dd = expand_info_stackindex(df.sample(100000), 'attributes')
200 ms ± 91.7 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
133 ms ± 4.39 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
158 ms ± 4.02 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
467 ms ± 16.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
4.16 s ± 252 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Any help in trying to optimise this so that is doable for file with high number of entries would be really appreciated. Thanks!