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")

Hooli offer cumulative income

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)

Hooli vs Raviga

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 and end, 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