Introduction
I'm new to Pandas. I'm trying to write a vectorized converter for the situation described in What is an efficient ways to parse a bar separated usr file in Python . All code presented here is my own and the data are synthetic.
For these data:
HeaderG|Header1|Header2|Header3
A|Entry1|Entry2|Entry3
B|Entry1|Entry2|Entry3
A|Eggs|Sausage|Bacon
B|Bread|Lettuce|Tomato
A|aa|bb|cc
B|dd|ee|ff
A|4aa|4bb|4cc
B|4dd|4ee|4ff
FooterG|Footer1|Footer2|Footer3
The converter is responsible for parsing out the header and footer, which have nearly nothing to do with the body of the data; and then parsing out one "payload" per set of groups (above, the groups being A
and B
). In the above sample there are two groups, three "entry columns", and four payloads.
The groups, headers and footers are parametric but well-known. The converter is responsible for producing maps of the header, footer and groups given some additional metadata. The algorithm roughly goes:
- Deserialize the pipe-separated file into one big dataframe
- Trim off the header and footer
- Validate, then trim off the first group column
- Make a Cartesian-product multi-index frame
- Construct and assign the multi-index
- Iterate over the multi-indexed data body to produce the payloads as plain dictionaries
I am aware of both the to_json
and to_dict
methods of DataFrame
but I was unable to get them working as I wanted to, so I had to roll my own. This code does exactly what it should, but I'm sure there's a better way to use Pandas. I want to optimize for speed first, code simplicity second, and memory basically not at all, given that input files are all less than 10 kB each.
My specific concerns:
make_multi_index
is quite ugly and uses a non-vectorized generator conversion of a dictionary; and also has not made (cannot make?) use ofMultiIndex.from_product
- It smells like it could make use of
np.meshgrid
but there was a catch in the nature of the third axis that prevented me from doing so - There must be a simpler way to assign header and footer names and produce dictionaries
- Heavy
groupby
abuse and lack of vectorization inpayloads
The code
from typing import Iterable
from pprint import pprint
import pandas as pd
import numpy as np
group_names = {'A': ('A1ValueKey', 'A2ValueKey', 'A3ValueKey'),
'B': ('B1ValueKey', 'B2ValueKey', 'B3ValueKey')}
header_names = ('HeaderKeyG', 'HeaderKey1', 'HeaderKey2', 'HeaderKey3')
footer_names = ('FooterKeyG', 'FootKey1', 'FootKey2', 'FootKey3')
n_groups = len(group_names)
n_entries = len(header_names) - 1
def make_multi_index(n_payloads: int) -> pd.MultiIndex:
group_indices = np.tile(
np.array(
[
(k, e)
for k, entries in group_names.items()
for e in entries
],
dtype=object
),
(n_payloads, 1),
)
indices = np.empty(
(group_indices.shape[0], 3),
dtype=object
)
indices[:, 0] = np.repeat(np.arange(n_payloads), n_groups * n_entries)
indices[:, 1:] = group_indices
return pd.MultiIndex.from_frame(
pd.DataFrame(indices),
names=(
'payload',
'group',
'entry',
),
)
def parse(fn: str) -> (pd.Series, pd.Series, pd.DataFrame):
df = pd.read_csv(fn, sep='|', header=None)
n_payloads, leftover = divmod(df.shape[0] - 2, n_groups)
assert leftover == 0
assert n_entries == df.shape[1] - 1
header = df.iloc[0, :]
footer = df.iloc[-1, :]
body = df.iloc[1:-1, :]
assert (
body.iloc[:, 0] == np.tile(
np.array(tuple(group_names.keys())),
n_payloads
)
).all()
body.drop(0, axis=1, inplace=True)
entries = pd.DataFrame(
body.values.flatten(),
index=make_multi_index(n_payloads),
)
return header, footer, entries
def payloads(header: Iterable[str], footer: Iterable[str], entries: pd.DataFrame) -> Iterable[dict]:
base = {
'header': dict(zip(header_names, header)),
'footer': dict(zip(footer_names, footer)),
}
for i_payload, payload in entries.groupby(level=0):
d = dict(base)
d['groups'] = {
groupname: {
g: din.values[0, 0]
for g, din in d.groupby(level=2)
}
for groupname, d in payload.groupby(level=1)
}
yield d
def main():
header, footer, entries = parse('file1.usr')
print('Multi-index entry representation:')
print(entries)
print()
print('Payloads:')
for pay in payloads(header, footer, entries):
pprint(pay)
main()
Output
Multi-index entry representation:
0
payload group entry
0 A A1ValueKey Entry1
A2ValueKey Entry2
A3ValueKey Entry3
B B1ValueKey Entry1
B2ValueKey Entry2
B3ValueKey Entry3
1 A A1ValueKey Eggs
A2ValueKey Sausage
A3ValueKey Bacon
B B1ValueKey Bread
B2ValueKey Lettuce
B3ValueKey Tomato
2 A A1ValueKey aa
A2ValueKey bb
A3ValueKey cc
B B1ValueKey dd
B2ValueKey ee
B3ValueKey ff
3 A A1ValueKey 4aa
A2ValueKey 4bb
A3ValueKey 4cc
B B1ValueKey 4dd
B2ValueKey 4ee
B3ValueKey 4ff
Payloads:
{'footer': {'FootKey1': 'Footer1',
'FootKey2': 'Footer2',
'FootKey3': 'Footer3',
'FooterKeyG': 'FooterG'},
'groups': {'A': {'A1ValueKey': 'Entry1',
'A2ValueKey': 'Entry2',
'A3ValueKey': 'Entry3'},
'B': {'B1ValueKey': 'Entry1',
'B2ValueKey': 'Entry2',
'B3ValueKey': 'Entry3'}},
'header': {'HeaderKey1': 'Header1',
'HeaderKey2': 'Header2',
'HeaderKey3': 'Header3',
'HeaderKeyG': 'HeaderG'}}
{'footer': {'FootKey1': 'Footer1',
'FootKey2': 'Footer2',
'FootKey3': 'Footer3',
'FooterKeyG': 'FooterG'},
'groups': {'A': {'A1ValueKey': 'Eggs',
'A2ValueKey': 'Sausage',
'A3ValueKey': 'Bacon'},
'B': {'B1ValueKey': 'Bread',
'B2ValueKey': 'Lettuce',
'B3ValueKey': 'Tomato'}},
'header': {'HeaderKey1': 'Header1',
'HeaderKey2': 'Header2',
'HeaderKey3': 'Header3',
'HeaderKeyG': 'HeaderG'}}
{'footer': {'FootKey1': 'Footer1',
'FootKey2': 'Footer2',
'FootKey3': 'Footer3',
'FooterKeyG': 'FooterG'},
'groups': {'A': {'A1ValueKey': 'aa', 'A2ValueKey': 'bb', 'A3ValueKey': 'cc'},
'B': {'B1ValueKey': 'dd', 'B2ValueKey': 'ee', 'B3ValueKey': 'ff'}},
'header': {'HeaderKey1': 'Header1',
'HeaderKey2': 'Header2',
'HeaderKey3': 'Header3',
'HeaderKeyG': 'HeaderG'}}
{'footer': {'FootKey1': 'Footer1',
'FootKey2': 'Footer2',
'FootKey3': 'Footer3',
'FooterKeyG': 'FooterG'},
'groups': {'A': {'A1ValueKey': '4aa',
'A2ValueKey': '4bb',
'A3ValueKey': '4cc'},
'B': {'B1ValueKey': '4dd',
'B2ValueKey': '4ee',
'B3ValueKey': '4ff'}},
'header': {'HeaderKey1': 'Header1',
'HeaderKey2': 'Header2',
'HeaderKey3': 'Header3',
'HeaderKeyG': 'HeaderG'}}
A
andB
always alternating in the file, as in your example data? And are there always exactly two classes? Or in other words, how do you know which row if classA
to combine with which row of classB
in the final output? \$\endgroup\$seek
to the footer and back first) a payload whenever you see the groups repeat. \$\endgroup\$