financier: modeling compensation packages with Python
Negotiating a job offer is hard. It is often a tense process involving rash emotional decisions. I built financier, a tool to visualize compensation packages and make it easier to compare job offers and take some of the edge of the job negotiation process.
While this is potentially generalizable to all lines of work, it is particularly useful to compare complex compensation packages such as the ones of Startup companies.
Note: I use interchangeably the terms “job offer” and “compensation package” in this article: they mean the same thing in this context.
How to represent a job offer?
I decided to represent a jobOffer
as a python object whose constructor expects a name and list of
components. Components are sources of income, they include:
- Salary
- 401(k) Match
- Stock Grant
- Onetime Bonus
- Periodic Bonus
When designing financier, one of my goals was to make the code as expressive as possible so that it would read like English. See for yourself with the following example:
HOOLI = Offer(
'Hooli',
Salary(yearly_amount=15000),
StockGrant(
amount=10000,
schedule=FourYearsScheduleOneYearCliff(
grant_date=datetime.date(2017, 2, 25)
)
),
OneTimeBonus(amount=10000,
payoff_date=datetime.date(2019, 3, 5)),
Match401k(yearly_income=150000,
match_percentage=0.03,
match_contribution_per_dollar=0.5),
)
NOTE: HOOLI is a fictional company featured in the Silicon Valley HBO show!
Computing income
Once you build an offer you can compute the income over time, for example:
HOOLI.detailed_income(
date_range=two_years_by_month()
)
Will return a pandas dataframe
with the income from the HOOLI offer for the next two
years. detailed_income
returns the income broken down by components:
Hooli Salary Hooli Grant...
Date
2019-02-01 1151 625 ...
2019-03-01 1274 0
2019-04-01 1233 0
2019-05-01 1274 625
2019-06-01 1233 0
2019-07-01 1274 0
...
If you want to get the total cumulative income over four years you can swap the
date range and substitute detailed_income
with cumulative_income
:
HOOLI.cumulative_income(
date_range=four_years_by_month()
)
Here is what the resulting dataframe
looks like:
Hooli Gross Income
Date
2019-02-01 1948.287671
2019-03-01 13413.356164
2019-04-01 14831.164384
2019-05-01 16921.232877
2019-06-01 18339.041096
2019-07-01 19804.109589
...
You can then export these dataframe
easily with the to_csv
method and leverage
a spreadsheet software:
HOOLI.cumulative_income(
date_range=four_years_by_month()
).to_csv("hooli.csv")
Visualizing and comparing income
While you can build your own visualization for the income dataframe
or use a
spreadsheet software, financier
comes with a handy visualization wrapper using
matplotlib
. You can plot the frame above with the following command:
plot_income(HOOLI.cumulative_income(
date_range=four_years_by_month()
), "/tmp/hooli.png")
This is very handy to compare offers visually (here we use income
to get
the total income broken down by period):
HOOLI_INCOME = Offer(
'Hooli',
Salary(yearly_amount=15000),
StockGrant(
amount=10000,
schedule=FourYearsScheduleOneYearCliff(
grant_date=datetime.date(2017, 2, 25)
)
),
OneTimeBonus(amount=10000,
payoff_date=datetime.date(2019, 3, 5)),
Match401k(yearly_income=15_0000,
match_percentage=0.03,
match_contribution_per_dollar=0.5),
).income(
date_range=two_years_by_month()
)
RAVIGA_INCOME = Offer(
'Raviga',
Salary(yearly_amount=5000),
StockGrant(
amount=40000,
schedule=FourYearsScheduleOneYearCliff(
grant_date=datetime.date(2017, 2, 25)
)
),
).income(
date_range=two_years_by_month()
)
# Make the dates look nicer (Month name and year)
plot_filename = "/tmp/hooli_vs_raviga.png"
HOOLI_INCOME.index = HOOLI_INCOME.index.to_series().apply(lambda x:x.strftime("%b %y"))
RAVIGA_INCOME.index = RAVIGA_INCOME.index.to_series().apply(lambda x:x.strftime("%b %y"))
plot_income(HOOLI_INCOME["Hooli Gross Income"].to_frame("Hooli Gross Income").join(
RAVIGA_INCOME["Raviga Gross Income"].to_frame("Raviga Gross Income")
), plot_filename)
NOTE: The astute reader will recognize that Raviga is another company from the Silicon Valley Show
Adding components to financier
If you are trying to model an offer but cannot find a component that fits the bill, you can define your own! The contract to define a new component is simple:
Create a class that has an instance method named
value
taking two dates:start
andend
, a closed interval. Return the value of the component over that interval.
For example, here is the definition of the One Time Bonus component, a bonus paid at one point in time:
class OneTimeBonus:
def __init__(self, amount, payoff_date):
self.amount = amount
self.payoff_date = payoff_date
def value(self, start, end):
if self.payoff_date >= start and self.payoff_date <= end:
return self.amount
return 0
It is as simple as it gets, now let’s look at a more complex example, a stock grant:
class StockGrant(object):
def __init__(self, amount, schedule):
self.amount = amount
self.cliffs = [
(e.date, e.value * amount)
for e in schedule.vesting_events
]
def value(self, start, end):
return sum(amount
for date, amount in self.cliffs
if (date >= start and date <= end))
It isn’t too complicated either. So, if you want to use financier but need to define new components to express complicated compensation packages, it shouldn’t be too hard to get it done!
You can find the code on GitHub