Skip to content

Instantly share code, notes, and snippets.

@louisswarren
Last active December 9, 2022 22:34
Embed
What would you like to do?
Tabulation in python, in place of spreadsheets
class RowRef:
def __init__(self, colmap, rows, idx, default = None):
self.colmap = colmap
self.rows = rows
self.idx = idx
self.default = default
def __getitem__(self, colname):
if not 0 <= self.idx < len(self.rows):
return self.default
return self.rows[self.idx][self.colmap[colname]]
def lag(self, n = 1, default = None):
return RowRef(self.colmap, self.rows, self.idx - n, default)
def lead(self, n = 1, default = None):
return RowRef(self.colmap, self.rows, self.idx + n, default)
def at(self, n):
return RowRef(self.colmap, self.rows, n, self.default)
def first(self):
return self.at(0)
def last(self):
return self.at(len(self.rows) - 1)
class Table:
def __init__(self, data, *colnames, formats = None):
self.rows = [list(r) for r in data]
self.colmap = {colname: i for i, colname in enumerate(colnames)}
self.colfmts = {colname: '{}' for colname in colnames}
if formats is not None:
self.colfmts.update(formats)
def __setitem__(self, colname, value):
fmt = '{}'
if isinstance(value, tuple):
fmt, func = value
else:
func = value
assert(colname not in self.colmap)
self.colmap[colname] = len(self.colmap)
for i, row in enumerate(self.rows):
robj = RowRef(self.colmap, self.rows, i)
row.append(func(robj))
self.colfmts[colname] = fmt
def tabulate(self, override_fmts = None):
bar = ' | '
fmts = {k: v for k, v in self.colfmts.items() if v is not None}
if override_fmts:
for k, v in override_fmts.items():
fmts[k] = v
if v is None:
del fmts[k]
print(fmts)
widths = [len(fmtc) for fmtc in fmts]
outputs = []
for row in self.rows:
v = tuple(
fmts[s].format(row[self.colmap[s]])
if row[self.colmap[s]] is not None
else ''
for s in fmts)
outputs.append(v)
widths = [max(widths[i], len(v[i])) for i in range(len(widths))]
print(bar.join((fmtc.ljust(widths[i]) for i, fmtc in enumerate(fmts))))
print('-' * (sum(widths) + len(bar) * (len(widths) - 1)))
for out in outputs:
print(bar.join((x.rjust(widths[i]) for i, x in enumerate(out))))
class Percent(str):
def format(self, x):
return f'{x * 100:0.0f}%'
Percent = Percent('Percent')
class Dollars(str):
def format(self, x):
x = round(x)
return f'${x:,}'
Dollars = Dollars('Dollars')
class Sign(str):
def format(self, x):
if x > 0:
return '▲'
elif x < 0:
return '▼'
else:
return '●'
Sign = Sign('Sign')
cols = [
(1, 76571,),
(2, 79274,),
(3, 81976,),
(4, 84679,),
(5, 87381,),
(6, 90084,),
(7, 103597,),
]
t = Table(cols, 'Band', 'Salary', formats = {'Salary': Dollars})
# t['Diff'] = lambda r: \
# r['Salary'] - r.lag(1, r.first()['Salary'])['Salary']
#
# t['Diff pct'] = lambda r: \
# r['Diff'] / r.lag(1, r.first()['Salary'])['Salary']
t['Diff'] = Dollars, lambda r: \
r['Salary'] - r.lag()['Salary'] if r.idx > 0 else None
t['Diff pct'] = Percent, lambda r: \
r['Diff'] / r.lag()['Salary'] if r['Diff'] is not None else None
t['Competency'] = Percent, lambda r: \
r['Salary'] / r.at(5)['Salary']
t['Tax'] = Dollars, lambda r: (lambda x:
# PAYE
max(0, min(x, 14000) - 0) * 0.105 +
max(0, min(x, 48000) - 14000) * 0.175 +
max(0, min(x, 70000) - 48000) * 0.300 +
max(0, min(x, 180000) - 70000) * 0.330 +
max(0, x - 180000) * 0.390 +
# ACC
x * 0.0146
)(r['Salary'])
t['After tax'] = Dollars, lambda r: \
r['Salary'] - r['Tax']
t['Kiwisaver'] = Dollars, lambda r: r['Salary'] * 0.03
t['Student loan'] = Dollars, lambda r: max(0, r['Salary'] - 21268) * 0.12
t['Take home'] = Dollars, lambda r: \
r['After tax'] - r['Kiwisaver'] - r['Student loan']
t['Weekly'] = Dollars, lambda r: \
r['Take home'] / 52
t['Paycheck'] = Dollars, lambda r: \
r['Take home'] / 365 * 14
t['Weekly diff'] = Dollars, lambda r: \
r['Weekly'] - r.lag()['Weekly'] if r.idx > 0 else None
t['Deflated'] = Dollars, lambda r: \
r['Salary'] / 1.07 ** r.idx
t['Effect'] = Sign, lambda r: \
r['Deflated'] - r['Salary']
t.tabulate({'Kiwisaver': None, 'Student loan': None})
@louisswarren
Copy link
Author

Output:

Band | Salary   | Diff    | Diff pct | Competency | Tax     | Take home | Weekly | Weekly diff
----------------------------------------------------------------------------------------------
   1 |  $76,571 |         |          |        85% | $16,188 |   $60,383 | $1,161 |            
   2 |  $79,274 |  $2,703 |       4% |        88% | $17,080 |   $62,194 | $1,196 |         $35
   3 |  $81,976 |  $2,702 |       3% |        91% | $17,972 |   $64,004 | $1,231 |         $35
   4 |  $84,679 |  $2,703 |       3% |        94% | $18,864 |   $65,815 | $1,266 |         $35
   5 |  $87,381 |  $2,702 |       3% |        97% | $19,756 |   $67,625 | $1,300 |         $35
   6 |  $90,084 |  $2,703 |       3% |       100% | $20,648 |   $69,436 | $1,335 |         $35
   7 | $103,597 | $13,513 |      15% |       115% | $25,107 |   $78,490 | $1,509 |        $174

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment