{ "cells": [ { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "Xwsupk0kS6d7" }, "source": [ "# Use Data Science to Handle Enterprise Resource Planning Data with Ease \n", "> \"Excel at Python\"\n", "\n", "- author: Dmytro Karabash, Maxim Korotkov \n", "- categories: [management, python, erp, enterprise resource planning, data science]\n", "- image: images/unsplash-scott-graham-5fNmWej4tAA-640.jpg\n", "- permalink: /teamdata/" ] }, { "cell_type": "code", "execution_count": 146, "metadata": { "colab": {}, "colab_type": "code", "id": "-buY_kCoViFl" }, "outputs": [], "source": [ "# hide\n", "# loading functions\n", "\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "import matplotlib.patches as mpatches\n", "import numpy as np\n", "\n", "from IPython.core.interactiveshell import InteractiveShell\n", "InteractiveShell.ast_node_interactivity = \"all\"\n", "\n", "verbosity = 5\n", "\n", "pd.options.display.max_rows = 7\n", "pd.options.display.float_format = '{:.2f}'.format" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "iZ-akZ9Zq_lN" }, "source": [ "## Introduction\n", "\n", "

You’re a boss now. You have a team or a business unit doing consultancy. Probably a bunch of Consultants helping clients, some Project Managers leading your projects in a nice Agile or whatever way. Everyone fills their timesheets (and you cut their bonuses if they don’t) – the time is billed to clients, and you have Fixed Fee projects as well. Maybe smaller teams being parts of your organization also – you can lead ten, a hundred or a thousand people (hey, dude – you run a unit of 1000 employees? Don’t read this article – hire someone who did). You probably even have an ERP (Enterprise Resource Planning) or something and a CFO in a lovely corner office. Do you have an excellent way to estimate how much profit every team member and project brings and ways to predict it with reasonable precision? If you have a decent ERP dashboard which gives you all that – then you’re lucky, and you don’t need this article either. The chances are that you get a simple calculation end of the year – like “let’s take all revenue finance recognized on your projects in a year and man day cost at USD 800 to get your unit profitability”. USD 800 can seem crazily high or unacceptably low, depending on the business you’re in. So that’s the precision you have to count the money your team brings to the firm? Do we need to remind you that this is one of the reasons why your job exists?

\n", "\n", "

What else can you get? All the timesheets projects with their budgets, and you can even get to approximate costs (let’s discuss it later) — a shitload of data for a year, even for a team of 10. What we show is that you don’t need an ERP to do the rest – the notebook in Jupyter will do. Keep this in mind - management starts with measurement. How can you supervise your team and projects if you don’t have their data at your fingers?

\n", "\n", "Why do you need Data Science here? Well, you can have all data at your fingers. Eventually, you would want to calculate your profit like\n", "```python\n", "profit = revenue - cost\n", "```\n", "\n", "Not only as subtracting two numbers, but also on the level of tables - so the output of the above statement can be a table with profit per consultant per month, like this:" ] }, { "cell_type": "code", "execution_count": 147, "metadata": {}, "outputs": [], "source": [ "# hide\n", "profit = pd.DataFrame([[6187.500, 8006.25], [8050.000, 3762.50]],\n", " index=['CATHY THE NEW MANAGER', 'FRANK THE CONSTULANT'],\n", " columns=['2020-02', '2020-03'])\n", "# next a bit of formating\n", "profit = profit.applymap(lambda x: \"{:10.2f}\".format(x))" ] }, { "cell_type": "code", "execution_count": 148, "metadata": { "scrolled": true }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
2020-022020-03
CATHY THE NEW MANAGER6187.508006.25
FRANK THE CONSTULANT8050.003762.50
\n", "
" ], "text/plain": [ " 2020-02 2020-03\n", "CATHY THE NEW MANAGER 6187.50 8006.25\n", "FRANK THE CONSTULANT 8050.00 3762.50" ] }, "execution_count": 148, "metadata": {}, "output_type": "execute_result" } ], "source": [ "profit" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "id": "iZ-akZ9Zq_lN" }, "source": [ "Or get a list of 3 people with most billable hours in March via the following statement\n", "```python\n", "t.where(t['Billable'] & (t['month'] == '2020-03')\n", " ).groupby(['User'])['effort'].sum().nlargest(3)\n", "```\n", "\n", "

The code above is indeed not executable, but we promise to get you there in a few paragraphs. And yes, there is some learning of python required. The huge difference from anything else you’ve been doing yourself (ERP reports, Excel, other tools) is that any analysis stays in the notebook and can be re-applied after your data changes.

" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Data Science\n", "\n", "

So, let’s get this done. First – yes, you need to know a bit of python to get the files in. The basic level will do. If you are a manager in 2020 and can’t write a simple script – hmm, you probably missed something. The objective is not to build an ERP and not even to have an easy-to-use toolkit - we want to show you how to make a toolkit for yourself (but feel free to reuse our code). What you will see is a straightforward set of examples - spend an hour or two to load your files into the notebook and then start playing with it - just make the analysis you want. You can end up checking a few numbers or building your charts and dashboards. It is ok, even if you work for a corporation (that’s where you need it most) - just install Anaconda and download the notebook. So, we first get and transform our input files (if you want to follow text AND all the code - read it on colab).

\t\n", "\n", "

We are loading projects, timesheets, and roles below - more details will follow on what we presumed is inside. Note - files are loaded from the GitHub repository so that they will open in colab or your local machine just the same.

\t" ] }, { "cell_type": "code", "execution_count": 149, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['TS Feb', 'TS Mar', 'TS Apr', 'Projects', 'Employees']" ] }, "execution_count": 149, "metadata": {}, "output_type": "execute_result" } ], "source": [ "data = {}\n", "data_path = 'https://raw.githubusercontent.com/h17/fastreport/master/data/'\n", "url = data_path + 'roles.csv'\n", "data['roles'] = pd.read_csv(url, index_col=0, sep=\";\")\n", "\n", "url = data_path + 'project_data.xlsm'\n", "project_data = pd.ExcelFile(url)\n", "project_data.sheet_names" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Don’t focus too much on the next code block - we have monthly timesheets in separate tabs and need to stack them one on top of another. Plus - we have errors in excel (Sat.1 column) - data cleanup is also required, quite usual for those who touched data science. Collapsed blocks in our notebook contain the code, which is not critical for understanding. If you’d rather read it all, we suggest you switch to either GitHub or colab, so you can also play with it.

" ] }, { "cell_type": "code", "execution_count": 150, "metadata": {}, "outputs": [], "source": [ "# collapse\n", "\n", "timesheets = []\n", "for sheet in project_data.sheet_names:\n", " if 'TS' in sheet:\n", " timesheets += [pd.read_excel(project_data, sheet, header=2)]\n", " else:\n", " tmp = pd.read_excel(project_data, sheet)\n", " data[sheet] = tmp\n", "if 'Sat.1' in timesheets[0]: # cleaning from Sat.1\n", " timesheets[0] = timesheets[0].rename(\n", " columns={'Sat': 'Sun', 'Sat.1': 'Sat'})\n", "data['timesheet'] = pd.concat(timesheets, sort=False)\n", "d = {'Billable': True, 'Non-Billable': False}\n", "data['timesheet']['Billable'] = data['timesheet']['Billing'].replace(d)\n", "data['timesheet'].drop(['Billing'], axis=1, inplace=True)\n", "data['timesheet'] = data['timesheet'][~data['timesheet'].User.isnull()]\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So, let's see what we've got here:" ] }, { "cell_type": "code", "execution_count": 151, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "dict_keys(['roles', 'Projects', 'Employees', 'timesheet'])" ] }, "execution_count": 151, "metadata": {}, "output_type": "execute_result" }, { "name": "stdout", "output_type": "stream", "text": [ "roles shape: 8 rows \t and 4 columns\n", "Projects shape: 48 rows \t and 10 columns\n", "Employees shape: 35 rows \t and 5 columns\n", "timesheet shape: 1792 rows \t and 14 columns\n" ] } ], "source": [ "data.keys()\n", "for key in data.keys():\n", " string_format = \"{:20} shape: {:>10} rows \\t and {:>10} columns\"\n", " print(string_format.format(key, *data[key].shape))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Data we loaded\n", "\n", "Let us summarize it here – most probably if you are a team leader or business unit manager – you can get\n", "* Dictionaries\n", " * Cost per region or default cost for “outside” contributors\n", " * Average revenue per hour for projects not “owned” by you\n", " " ] }, { "cell_type": "code", "execution_count": 152, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
SeniorityEUUSUK
Position
Unit HeadSENIOR110013001200
Program ManagerEXPERIENCED9001000900
Project ManagerSENIOR800800800
...............
ConsultantSENIOR800800900
ConsultantEXPERIENCED550550550
ConsultantJUNIOR500500500
\n", "

8 rows × 4 columns

\n", "
" ], "text/plain": [ " Seniority EU US UK\n", "Position \n", "Unit Head SENIOR 1100 1300 1200\n", "Program Manager EXPERIENCED 900 1000 900\n", "Project Manager SENIOR 800 800 800\n", "... ... ... ... ...\n", "Consultant SENIOR 800 800 900\n", "Consultant EXPERIENCED 550 550 550\n", "Consultant JUNIOR 500 500 500\n", "\n", "[8 rows x 4 columns]" ] }, "execution_count": 152, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# show roles\n", "roles = data[\"roles\"]\n", "roles" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We need to set default values and bring it to format easy to use, which is very easy in python:" ] }, { "cell_type": "code", "execution_count": 153, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
PositionSeniorityregioncost
0Unit HeadSENIOREU1100
1Program ManagerEXPERIENCEDEU900
2Project ManagerSENIOREU800
...............
21ConsultantSENIORUK900
22ConsultantEXPERIENCEDUK550
23ConsultantJUNIORUK500
\n", "

24 rows × 4 columns

\n", "
" ], "text/plain": [ " Position Seniority region cost\n", "0 Unit Head SENIOR EU 1100\n", "1 Program Manager EXPERIENCED EU 900\n", "2 Project Manager SENIOR EU 800\n", ".. ... ... ... ...\n", "21 Consultant SENIOR UK 900\n", "22 Consultant EXPERIENCED UK 550\n", "23 Consultant JUNIOR UK 500\n", "\n", "[24 rows x 4 columns]" ] }, "execution_count": 153, "metadata": {}, "output_type": "execute_result" } ], "source": [ "default_revenue = 1200\n", "default_cost = 850\n", "\n", "# wide to long format\n", "roles_long = pd.melt(roles.reset_index(),\n", " id_vars=['Position', 'Seniority'],\n", " var_name='region',\n", " value_name='cost')\n", "roles_long" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* HeadCount\n", " * List of your employees, with their grades (or costs)\n", " * Engagement model (employee or contractor)" ] }, { "cell_type": "code", "execution_count": 154, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
NameEngagementCountryPositionSeniorityregioncost
0JANE DOESTAFFUSConsultantEXPERIENCEDUS550.00
1JOHN DOESTAFFFRConsultantJUNIORNaN850.00
2JOHN SMITHSTAFFUSConsultantEXPERIENCEDUS550.00
........................
32JOHN THE CONSULTANTSTAFFFRConsultantEXPERIENCEDNaN850.00
33JANE THE MANAGERCONTRACTORBEProject ManagerSENIORNaN850.00
34JACK THE EXPERTSTAFFBEConsultantSENIORNaN850.00
\n", "

35 rows × 7 columns

\n", "
" ], "text/plain": [ " Name Engagement Country Position Seniority \\\n", "0 JANE DOE STAFF US Consultant EXPERIENCED \n", "1 JOHN DOE STAFF FR Consultant JUNIOR \n", "2 JOHN SMITH STAFF US Consultant EXPERIENCED \n", ".. ... ... ... ... ... \n", "32 JOHN THE CONSULTANT STAFF FR Consultant EXPERIENCED \n", "33 JANE THE MANAGER CONTRACTOR BE Project Manager SENIOR \n", "34 JACK THE EXPERT STAFF BE Consultant SENIOR \n", "\n", " region cost \n", "0 US 550.00 \n", "1 NaN 850.00 \n", "2 US 550.00 \n", ".. ... ... \n", "32 NaN 850.00 \n", "33 NaN 850.00 \n", "34 NaN 850.00 \n", "\n", "[35 rows x 7 columns]" ] }, "execution_count": 154, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# show head count\n", "headcount = data[\"Employees\"]\n", "headcount = headcount.merge(\n", " roles_long[['Position', 'Seniority', 'region', 'cost']],\n", " how='left',\n", " left_on=['Seniority', 'Position', 'Country'],\n", " right_on=['Seniority', 'Position', 'region'])\n", "headcount['cost'] = headcount['cost'].fillna(default_cost)\n", "headcount" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* Projects with budgets, effort estimates, dates, types of revenue recognition (Time and Material, Fixed Fee or something else), et cetera" ] }, { "cell_type": "code", "execution_count": 155, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
Client IDProject IDProjectPMRegionStartEndFundingDaily RateTCV
01315468454Medium consutancy 1SUSAN THE MANAGEREurope2019-01-16 08:00:002020-07-22 14:00:00Time and Materials120039,000 USD
1276470285Our biggest projectEXT 6a8e0ca747Europe2019-10-01 08:00:002021-04-30 11:00:00Time and Materials12001,450,000 USD
21291668093Upgrade to new versionTIM THE LEAVEREurope2018-09-28 08:00:002020-04-27 17:00:00Fixed Fee1200127,500 USD
.................................
451289963852Huge consultancy poolJACK THE MANAGEREurope2017-08-01 08:00:002020-04-27 17:00:00Time and Materials1200198,000 USD
461290171248Small upgradeJACK THE MANAGEREurope2020-01-29 08:00:002020-12-31 17:00:00Time and Materials1200108,000 USD
471771970618Small consultancy poolSUSAN THE MANAGEREurope2019-11-06 08:00:002020-06-30 17:00:00Time and Materials120027,200 USD
\n", "

48 rows × 10 columns

\n", "
" ], "text/plain": [ " Client ID Project ID Project PM Region \\\n", "0 13154 68454 Medium consutancy 1 SUSAN THE MANAGER Europe \n", "1 2764 70285 Our biggest project EXT 6a8e0ca747 Europe \n", "2 12916 68093 Upgrade to new version TIM THE LEAVER Europe \n", ".. ... ... ... ... ... \n", "45 12899 63852 Huge consultancy pool JACK THE MANAGER Europe \n", "46 12901 71248 Small upgrade JACK THE MANAGER Europe \n", "47 17719 70618 Small consultancy pool SUSAN THE MANAGER Europe \n", "\n", " Start End Funding Daily Rate \\\n", "0 2019-01-16 08:00:00 2020-07-22 14:00:00 Time and Materials 1200 \n", "1 2019-10-01 08:00:00 2021-04-30 11:00:00 Time and Materials 1200 \n", "2 2018-09-28 08:00:00 2020-04-27 17:00:00 Fixed Fee 1200 \n", ".. ... ... ... ... \n", "45 2017-08-01 08:00:00 2020-04-27 17:00:00 Time and Materials 1200 \n", "46 2020-01-29 08:00:00 2020-12-31 17:00:00 Time and Materials 1200 \n", "47 2019-11-06 08:00:00 2020-06-30 17:00:00 Time and Materials 1200 \n", "\n", " TCV \n", "0 39,000 USD \n", "1 1,450,000 USD \n", "2 127,500 USD \n", ".. ... \n", "45 198,000 USD \n", "46 108,000 USD \n", "47 27,200 USD \n", "\n", "[48 rows x 10 columns]" ] }, "execution_count": 155, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# show projects\n", "\n", "projects = data[\"Projects\"]\n", "projects" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* Timesheets for your team / your projects. It is possible that other teams can contribute to your projects and your team might work in outside projects \n", " * Typically arrive monthly or weekly\n", " * (optional) Planned effort\n", " * There might be none, in one of the next posts we'll talk about autofill" ] }, { "cell_type": "code", "execution_count": 156, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
ProjectActivityUserPeriod StartingEffort TypeTotal (Days)SunMonTueWedThuFriSatBillable
0PRJ f74fe3647dConsultancyEXT 800982ab082020-02-02Actual2.500.000.500.500.500.500.500.00True
1PRJ f74fe3647dConsultancyEXT c8c92ca4322020-02-02Actual2.500.000.500.500.500.500.500.00True
2PRJ f74fe3647dConsultancyEXT c8c92ca4322020-02-09Actual2.500.000.500.500.500.500.500.00True
.............................................
620PRJ 67df00a7e9ConsultancyEXT ca026036162020-04-12Actual1.000.000.500.000.500.000.000.00True
621PRJ 67df00a7e9ConsultancyEXT ca026036162020-04-19Actual1.000.000.500.000.000.500.000.00True
622PRJ 67df00a7e9ConsultancyEXT ca026036162020-04-26Reported1.500.000.500.500.000.50nannanTrue
\n", "

1792 rows × 14 columns

\n", "
" ], "text/plain": [ " Project Activity User Period Starting Effort Type \\\n", "0 PRJ f74fe3647d Consultancy EXT 800982ab08 2020-02-02 Actual \n", "1 PRJ f74fe3647d Consultancy EXT c8c92ca432 2020-02-02 Actual \n", "2 PRJ f74fe3647d Consultancy EXT c8c92ca432 2020-02-09 Actual \n", ".. ... ... ... ... ... \n", "620 PRJ 67df00a7e9 Consultancy EXT ca02603616 2020-04-12 Actual \n", "621 PRJ 67df00a7e9 Consultancy EXT ca02603616 2020-04-19 Actual \n", "622 PRJ 67df00a7e9 Consultancy EXT ca02603616 2020-04-26 Reported \n", "\n", " Total (Days) Sun Mon Tue Wed Thu Fri Sat Billable \n", "0 2.50 0.00 0.50 0.50 0.50 0.50 0.50 0.00 True \n", "1 2.50 0.00 0.50 0.50 0.50 0.50 0.50 0.00 True \n", "2 2.50 0.00 0.50 0.50 0.50 0.50 0.50 0.00 True \n", ".. ... ... ... ... ... ... ... ... ... \n", "620 1.00 0.00 0.50 0.00 0.50 0.00 0.00 0.00 True \n", "621 1.00 0.00 0.50 0.00 0.00 0.50 0.00 0.00 True \n", "622 1.50 0.00 0.50 0.50 0.00 0.50 nan nan True \n", "\n", "[1792 rows x 14 columns]" ] }, "execution_count": 156, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# show timesheets\n", "\n", "timesheet = data[\"timesheet\"]\n", "timesheet" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "EXT users here are outside of our headcount (let’s say we are not getting the real names, but rather some ids from ERP in such case)\n", "\n", "

Why not just have things done by ERP? Your ERP probably does not present you anything nicely in terms of parameters you’d want to control on your level (otherwise, you won’t read this anyway). There may be a project running to improve it by 2025 - maybe one of the Big4 firms is interviewing you for requirements. If you are steering this boat long enough – you might have ended up with quick and dirty calculation, SQL queries to SAP (with field names still in German), or an Excel file. Why? Your bosses don’t care – they already hired you, and a better ERP is coming in 2025 anyway. So they want to know how much money your team makes (preferably - per person, per month, per project, with charts, projections, and comparisons) and to know why it is not so profitable (because it never is).

\n", "\n", "

To simplify your way forward we are going to create a timeseries out of timesheets which is a bit more involved so you can skip it for now and come back to it later, but the point is that at the end you will get a lovely monthly pandas DataFrame that looks like this

" ] }, { "cell_type": "code", "execution_count": 157, "metadata": { "scrolled": false }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
ProjectActivityUserEffort TypeeffortBillabledateweekdaymonthweekendworkweek
0Upgrade in two contracts - 1ConsultancyCATHY THE NEW MANAGERActual0.00True2020-02-16Sun2020-02TrueFalse
1Upgrade in two contracts - 2ConsultancyCATHY THE NEW MANAGERActual0.00True2020-02-16Sun2020-02TrueFalse
2A pilot for huge opportunityConsultancyCATHY THE NEW MANAGERActual0.00True2020-02-16Sun2020-02TrueFalse
....................................
12541Strategic project in a new regionProject ManagementTOM THE TEAM LEADERReportednanTrue2020-05-01Fri2020-05FalseTrue
12542Strategic project in a new region 2ConsultancyTOM THE TEAM LEADERActualnanFalse2020-05-02Sat2020-05TrueFalse
12543Strategic project in a new regionProject ManagementTOM THE TEAM LEADERReportednanTrue2020-05-02Sat2020-05TrueFalse
\n", "

12544 rows × 11 columns

\n", "
" ], "text/plain": [ " Project Activity \\\n", "0 Upgrade in two contracts - 1 Consultancy \n", "1 Upgrade in two contracts - 2 Consultancy \n", "2 A pilot for huge opportunity Consultancy \n", "... ... ... \n", "12541 Strategic project in a new region Project Management \n", "12542 Strategic project in a new region 2 Consultancy \n", "12543 Strategic project in a new region Project Management \n", "\n", " User Effort Type effort Billable date weekday \\\n", "0 CATHY THE NEW MANAGER Actual 0.00 True 2020-02-16 Sun \n", "1 CATHY THE NEW MANAGER Actual 0.00 True 2020-02-16 Sun \n", "2 CATHY THE NEW MANAGER Actual 0.00 True 2020-02-16 Sun \n", "... ... ... ... ... ... ... \n", "12541 TOM THE TEAM LEADER Reported nan True 2020-05-01 Fri \n", "12542 TOM THE TEAM LEADER Actual nan False 2020-05-02 Sat \n", "12543 TOM THE TEAM LEADER Reported nan True 2020-05-02 Sat \n", "\n", " month weekend workweek \n", "0 2020-02 True False \n", "1 2020-02 True False \n", "2 2020-02 True False \n", "... ... ... ... \n", "12541 2020-05 False True \n", "12542 2020-05 True False \n", "12543 2020-05 True False \n", "\n", "[12544 rows x 11 columns]" ] }, "execution_count": 157, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# collapse\n", "\n", "dd = []\n", "timesheet['Period Starting'] = pd.to_datetime(timesheet['Period Starting'])\n", "weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']\n", "for i, weekday in enumerate(weekdays):\n", " columns = [col for col in timesheet if col not in weekdays or col == weekday]\n", " tmp = timesheet[columns].copy()\n", " tmp = tmp.rename(columns={weekday: 'effort'})\n", " tmp['date'] = tmp['Period Starting']+pd.to_timedelta('{}D'.format(i))\n", " tmp['weekday'] = weekday\n", " tmp = tmp.drop(['Period Starting', 'Total (Days)'], axis=1)\n", " dd += [tmp]\n", "\n", "timeseries = pd.concat(dd, sort=False).sort_values(['User', 'date'])\n", "timeseries = timeseries.reset_index().drop('index', axis=1)\n", "timeseries['date'] = pd.to_datetime(timeseries['date']).astype('str')\n", "timeseries['month'] = timeseries['date'].str[:7]\n", "\n", "def isweekend(x): return x in ['Sun', 'Sat']\n", "\n", "timeseries['weekend'] = timeseries['weekday'].apply(isweekend)\n", "timeseries['workweek'] = ~timeseries['weekday'].apply(isweekend)\n", "timeseries" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

It is common in data science to have one big DataFrame (similar to excel tab or SQL table) that you can group and filter easily. It is helpful because it makes things very easy.

" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Profitability\n", "\n", "

Timesheets and project budgets are simplified this way, and you can manipulate them the way you want in a few simple statements. If you studied excel formulas - you could do this also. Now, let’s look at profitability in the most straightforward manner possible. As a manager, we suggest you do that to set benchmarks for your team. So, profit is $$\\mbox{Revenue  –  Cost}$$ and we intend to keep it simple. You can implement the calculations you like.

\n", "\n", "

Revenue: Let’s say that for a Time & Material project, you generate as much revenue as you bill (on agreed rate) up to its budget. We are not doing any Fixed Fee recognition formula. Different companies do it differently, and you’ll either need historical data or actual recognized value depending on how you operate.

\n", "\n", "We also assume that we are interested only by users in our headcount (so we filter out all EXT users)." ] }, { "cell_type": "code", "execution_count": 158, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
month2020-022020-032020-042020-05
User
CATHY THE NEW MANAGER11550.0019350.009412.500.00
FRANK THE CONSTULANT15750.0012425.0014875.000.00
IVAN THE PROGRAMMER25062.5029043.7525643.750.00
JACK THE EXPERT29093.7531331.2524843.750.00
JACK THE MANAGER24090.6222318.7518821.880.00
\n", "
" ], "text/plain": [ "month 2020-02 2020-03 2020-04 2020-05\n", "User \n", "CATHY THE NEW MANAGER 11550.00 19350.00 9412.50 0.00\n", "FRANK THE CONSTULANT 15750.00 12425.00 14875.00 0.00\n", "IVAN THE PROGRAMMER 25062.50 29043.75 25643.75 0.00\n", "JACK THE EXPERT 29093.75 31331.25 24843.75 0.00\n", "JACK THE MANAGER 24090.62 22318.75 18821.88 0.00" ] }, "execution_count": 158, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# revenue calculation for a person for a month \n", "# (SUM REV of all timesheet records * corresp project rates)\n", "revenue_df = timeseries[['User', 'Project',\n", " 'Billable', 'effort', 'date', 'month']]\n", "revenue_df = revenue_df.merge(projects[['Project', 'Daily Rate']],\n", " how='left',\n", " on='Project')\n", "revenue_df['Daily Rate'].fillna(default_revenue, inplace=True)\n", "revenue_df = revenue_df[(revenue_df['User'].str[:3] != 'EXT') &\n", " (revenue_df['Billable'])]\n", "\n", "revenue_df['daily_revenue'] = revenue_df['Daily Rate'] * \\\n", " revenue_df['effort']\n", "revenue = revenue_df.groupby(['User', 'month'])[\n", " 'daily_revenue'].sum().unstack().fillna(0)\n", "revenue = revenue[revenue.sum(1) > 0]\n", "revenue.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We got revenue per month per person. Not too sophisticated, right?\n", "\n", "

Cost: Let’s start with the fact that just using the “default cost rate” is not enough - whenever margins are under pressure, you should do better. You might have people working in different countries and of totally different levels. Talk to your finance team and get some estimates from them (or make it together). We’d say they owe you that much if you were appointed to run a team or business unit. We will assume below that you managed to get a few grades per Country (see roles). Costs per individual will be nice to have as well. The beauty of this tool (compared to doing Excel by yourself) is that you can just add it - it will be a few lines of code. Let’s calculate direct cost per month: here we check the cost of effort posted on timesheets, presuming they are full - can check per number of working days in a month also and compare. We are not interested in external resources, so we’ll filter them out again.

" ] }, { "cell_type": "code", "execution_count": 160, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
month2020-022020-032020-042020-05
User
CATHY THE NEW MANAGER5362.5011343.759332.810.00
FRANK THE CONSTULANT7700.008662.507768.750.00
IVAN THE PROGRAMMER11481.2512925.0011618.750.00
JACK THE EXPERT19921.8821621.8818168.750.00
JACK THE MANAGER11800.0010900.009600.000.00
\n", "
" ], "text/plain": [ "month 2020-02 2020-03 2020-04 2020-05\n", "User \n", "CATHY THE NEW MANAGER 5362.50 11343.75 9332.81 0.00\n", "FRANK THE CONSTULANT 7700.00 8662.50 7768.75 0.00\n", "IVAN THE PROGRAMMER 11481.25 12925.00 11618.75 0.00\n", "JACK THE EXPERT 19921.88 21621.88 18168.75 0.00\n", "JACK THE MANAGER 11800.00 10900.00 9600.00 0.00" ] }, "execution_count": 160, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# cost calculation for a project \n", "# (SUM COST of all timesheet records * corresp cost rates - see roles)\n", "cost_df = timeseries[['User', 'Project', 'effort', 'date', 'month']]\n", "cost_df = cost_df.merge(headcount[['Name', 'cost']],\n", " how='left', left_on='User', right_on='Name')\n", "cost_df = cost_df[cost_df['User'].str[:3] != 'EXT']\n", "cost_df['daily_cost'] = cost_df['cost'] * cost_df['effort']\n", "cost = cost_df.groupby(['User', 'month'])['daily_cost'].sum()\n", "cost = cost.unstack().fillna(0)\n", "cost = cost[cost.sum(1) > 0]\n", "cost.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Now, we can get to profit per user per month using operations on dataframes. And here it bears some fruit. Profit = revenue - cost. Indeed, it required some data cleanup first - but not too much

" ] }, { "cell_type": "code", "execution_count": 161, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
month2020-022020-032020-042020-05
User
CATHY THE NEW MANAGER6187.508006.2579.690.00
FRANK THE CONSTULANT8050.003762.507106.250.00
IVAN THE PROGRAMMER13581.2516118.7514025.000.00
JACK THE EXPERT9171.889709.386675.000.00
JACK THE MANAGER12290.6211418.759221.880.00
\n", "
" ], "text/plain": [ "month 2020-02 2020-03 2020-04 2020-05\n", "User \n", "CATHY THE NEW MANAGER 6187.50 8006.25 79.69 0.00\n", "FRANK THE CONSTULANT 8050.00 3762.50 7106.25 0.00\n", "IVAN THE PROGRAMMER 13581.25 16118.75 14025.00 0.00\n", "JACK THE EXPERT 9171.88 9709.38 6675.00 0.00\n", "JACK THE MANAGER 12290.62 11418.75 9221.88 0.00" ] }, "execution_count": 161, "metadata": {}, "output_type": "execute_result" } ], "source": [ "profit = revenue - cost\n", "profit.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That's what we've promised, right? Ok, second one - people who entered most Billable hours in March" ] }, { "cell_type": "code", "execution_count": 162, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "User\n", "TOM THE EXPERT 26.75\n", "JACK THE EXPERT 25.31\n", "PHILIP THE EXPERT 24.88\n", "Name: effort, dtype: float64" ] }, "execution_count": 162, "metadata": {}, "output_type": "execute_result" } ], "source": [ "t = timeseries # concatenating just in case you are reading from phone\n", "t.where(t['Billable'] & (t['month'] == '2020-03')\n", " ).groupby(['User'])['effort'].sum().nlargest(3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## What else?\n", "\n", "

Now let’s look at how to apply some python and data science techniques (we will get to more details in our next posts) to data you’ve seen above and how to visualize it nicely.

\n", "\n", "First, let’s take a PM and visualize revenue on his/her projects per month." ] }, { "cell_type": "code", "execution_count": 163, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "%matplotlib inline\n", "\n", "pm_selected = \"CATHY THE NEW MANAGER\"\n", "drawdt = revenue.loc[pm_selected].T\n", "\n", "plt.bar(range(len(drawdt.index)), drawdt.values, color=\"green\",\n", " width=1, align='center', edgecolor='black');\n", "plt.xticks(range(len(drawdt.index)), drawdt.index);\n", "plt.ylabel(\"Revenue / month: \"+pm_selected);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

That was simple. Then, some fun for those who know slightly more advanced python - you can make an interactive chart in few lines, e.g., here we want to make it visual in terms of if the value is above or below the benchmark (works if you copy notebook, is not clickable on the blog)

" ] }, { "cell_type": "code", "execution_count": 164, "metadata": { "scrolled": true }, "outputs": [ { "data": { "application/javascript": [ "/* Put everything inside the global mpl namespace */\n", "window.mpl = {};\n", "\n", "\n", "mpl.get_websocket_type = function() {\n", " if (typeof(WebSocket) !== 'undefined') {\n", " return WebSocket;\n", " } else if (typeof(MozWebSocket) !== 'undefined') {\n", " return MozWebSocket;\n", " } else {\n", " alert('Your browser does not have WebSocket support. ' +\n", " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", " 'Firefox 4 and 5 are also supported but you ' +\n", " 'have to enable WebSockets in about:config.');\n", " };\n", "}\n", "\n", "mpl.figure = function(figure_id, websocket, ondownload, parent_element) {\n", " this.id = figure_id;\n", "\n", " this.ws = websocket;\n", "\n", " this.supports_binary = (this.ws.binaryType != undefined);\n", "\n", " if (!this.supports_binary) {\n", " var warnings = document.getElementById(\"mpl-warnings\");\n", " if (warnings) {\n", " warnings.style.display = 'block';\n", " warnings.textContent = (\n", " \"This browser does not support binary websocket messages. \" +\n", " \"Performance may be slow.\");\n", " }\n", " }\n", "\n", " this.imageObj = new Image();\n", "\n", " this.context = undefined;\n", " this.message = undefined;\n", " this.canvas = undefined;\n", " this.rubberband_canvas = undefined;\n", " this.rubberband_context = undefined;\n", " this.format_dropdown = undefined;\n", "\n", " this.image_mode = 'full';\n", "\n", " this.root = $('
');\n", " this._root_extra_style(this.root)\n", " this.root.attr('style', 'display: inline-block');\n", "\n", " $(parent_element).append(this.root);\n", "\n", " this._init_header(this);\n", " this._init_canvas(this);\n", " this._init_toolbar(this);\n", "\n", " var fig = this;\n", "\n", " this.waiting = false;\n", "\n", " this.ws.onopen = function () {\n", " fig.send_message(\"supports_binary\", {value: fig.supports_binary});\n", " fig.send_message(\"send_image_mode\", {});\n", " if (mpl.ratio != 1) {\n", " fig.send_message(\"set_dpi_ratio\", {'dpi_ratio': mpl.ratio});\n", " }\n", " fig.send_message(\"refresh\", {});\n", " }\n", "\n", " this.imageObj.onload = function() {\n", " if (fig.image_mode == 'full') {\n", " // Full images could contain transparency (where diff images\n", " // almost always do), so we need to clear the canvas so that\n", " // there is no ghosting.\n", " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", " }\n", " fig.context.drawImage(fig.imageObj, 0, 0);\n", " };\n", "\n", " this.imageObj.onunload = function() {\n", " fig.ws.close();\n", " }\n", "\n", " this.ws.onmessage = this._make_on_message_function(this);\n", "\n", " this.ondownload = ondownload;\n", "}\n", "\n", "mpl.figure.prototype._init_header = function() {\n", " var titlebar = $(\n", " '
');\n", " var titletext = $(\n", " '
');\n", " titlebar.append(titletext)\n", " this.root.append(titlebar);\n", " this.header = titletext[0];\n", "}\n", "\n", "\n", "\n", "mpl.figure.prototype._canvas_extra_style = function(canvas_div) {\n", "\n", "}\n", "\n", "\n", "mpl.figure.prototype._root_extra_style = function(canvas_div) {\n", "\n", "}\n", "\n", "mpl.figure.prototype._init_canvas = function() {\n", " var fig = this;\n", "\n", " var canvas_div = $('
');\n", "\n", " canvas_div.attr('style', 'position: relative; clear: both; outline: 0');\n", "\n", " function canvas_keyboard_event(event) {\n", " return fig.key_event(event, event['data']);\n", " }\n", "\n", " canvas_div.keydown('key_press', canvas_keyboard_event);\n", " canvas_div.keyup('key_release', canvas_keyboard_event);\n", " this.canvas_div = canvas_div\n", " this._canvas_extra_style(canvas_div)\n", " this.root.append(canvas_div);\n", "\n", " var canvas = $('');\n", " canvas.addClass('mpl-canvas');\n", " canvas.attr('style', \"left: 0; top: 0; z-index: 0; outline: 0\")\n", "\n", " this.canvas = canvas[0];\n", " this.context = canvas[0].getContext(\"2d\");\n", "\n", " var backingStore = this.context.backingStorePixelRatio ||\n", "\tthis.context.webkitBackingStorePixelRatio ||\n", "\tthis.context.mozBackingStorePixelRatio ||\n", "\tthis.context.msBackingStorePixelRatio ||\n", "\tthis.context.oBackingStorePixelRatio ||\n", "\tthis.context.backingStorePixelRatio || 1;\n", "\n", " mpl.ratio = (window.devicePixelRatio || 1) / backingStore;\n", "\n", " var rubberband = $('');\n", " rubberband.attr('style', \"position: absolute; left: 0; top: 0; z-index: 1;\")\n", "\n", " var pass_mouse_events = true;\n", "\n", " canvas_div.resizable({\n", " start: function(event, ui) {\n", " pass_mouse_events = false;\n", " },\n", " resize: function(event, ui) {\n", " fig.request_resize(ui.size.width, ui.size.height);\n", " },\n", " stop: function(event, ui) {\n", " pass_mouse_events = true;\n", " fig.request_resize(ui.size.width, ui.size.height);\n", " },\n", " });\n", "\n", " function mouse_event_fn(event) {\n", " if (pass_mouse_events)\n", " return fig.mouse_event(event, event['data']);\n", " }\n", "\n", " rubberband.mousedown('button_press', mouse_event_fn);\n", " rubberband.mouseup('button_release', mouse_event_fn);\n", " // Throttle sequential mouse events to 1 every 20ms.\n", " rubberband.mousemove('motion_notify', mouse_event_fn);\n", "\n", " rubberband.mouseenter('figure_enter', mouse_event_fn);\n", " rubberband.mouseleave('figure_leave', mouse_event_fn);\n", "\n", " canvas_div.on(\"wheel\", function (event) {\n", " event = event.originalEvent;\n", " event['data'] = 'scroll'\n", " if (event.deltaY < 0) {\n", " event.step = 1;\n", " } else {\n", " event.step = -1;\n", " }\n", " mouse_event_fn(event);\n", " });\n", "\n", " canvas_div.append(canvas);\n", " canvas_div.append(rubberband);\n", "\n", " this.rubberband = rubberband;\n", " this.rubberband_canvas = rubberband[0];\n", " this.rubberband_context = rubberband[0].getContext(\"2d\");\n", " this.rubberband_context.strokeStyle = \"#000000\";\n", "\n", " this._resize_canvas = function(width, height) {\n", " // Keep the size of the canvas, canvas container, and rubber band\n", " // canvas in synch.\n", " canvas_div.css('width', width)\n", " canvas_div.css('height', height)\n", "\n", " canvas.attr('width', width * mpl.ratio);\n", " canvas.attr('height', height * mpl.ratio);\n", " canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');\n", "\n", " rubberband.attr('width', width);\n", " rubberband.attr('height', height);\n", " }\n", "\n", " // Set the figure to an initial 600x600px, this will subsequently be updated\n", " // upon first draw.\n", " this._resize_canvas(600, 600);\n", "\n", " // Disable right mouse context menu.\n", " $(this.rubberband_canvas).bind(\"contextmenu\",function(e){\n", " return false;\n", " });\n", "\n", " function set_focus () {\n", " canvas.focus();\n", " canvas_div.focus();\n", " }\n", "\n", " window.setTimeout(set_focus, 100);\n", "}\n", "\n", "mpl.figure.prototype._init_toolbar = function() {\n", " var fig = this;\n", "\n", " var nav_element = $('
');\n", " nav_element.attr('style', 'width: 100%');\n", " this.root.append(nav_element);\n", "\n", " // Define a callback function for later on.\n", " function toolbar_event(event) {\n", " return fig.toolbar_button_onclick(event['data']);\n", " }\n", " function toolbar_mouse_event(event) {\n", " return fig.toolbar_button_onmouseover(event['data']);\n", " }\n", "\n", " for(var toolbar_ind in mpl.toolbar_items) {\n", " var name = mpl.toolbar_items[toolbar_ind][0];\n", " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", " var image = mpl.toolbar_items[toolbar_ind][2];\n", " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", "\n", " if (!name) {\n", " // put a spacer in here.\n", " continue;\n", " }\n", " var button = $('