{ "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", " | 2020-02 | \n", "2020-03 | \n", "
---|---|---|
CATHY THE NEW MANAGER | \n", "6187.50 | \n", "8006.25 | \n", "
FRANK THE CONSTULANT | \n", "8050.00 | \n", "3762.50 | \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", " | Seniority | \n", "EU | \n", "US | \n", "UK | \n", "
---|---|---|---|---|
Position | \n", "\n", " | \n", " | \n", " | \n", " |
Unit Head | \n", "SENIOR | \n", "1100 | \n", "1300 | \n", "1200 | \n", "
Program Manager | \n", "EXPERIENCED | \n", "900 | \n", "1000 | \n", "900 | \n", "
Project Manager | \n", "SENIOR | \n", "800 | \n", "800 | \n", "800 | \n", "
... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "
Consultant | \n", "SENIOR | \n", "800 | \n", "800 | \n", "900 | \n", "
Consultant | \n", "EXPERIENCED | \n", "550 | \n", "550 | \n", "550 | \n", "
Consultant | \n", "JUNIOR | \n", "500 | \n", "500 | \n", "500 | \n", "
8 rows × 4 columns
\n", "\n", " | Position | \n", "Seniority | \n", "region | \n", "cost | \n", "
---|---|---|---|---|
0 | \n", "Unit Head | \n", "SENIOR | \n", "EU | \n", "1100 | \n", "
1 | \n", "Program Manager | \n", "EXPERIENCED | \n", "EU | \n", "900 | \n", "
2 | \n", "Project Manager | \n", "SENIOR | \n", "EU | \n", "800 | \n", "
... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "
21 | \n", "Consultant | \n", "SENIOR | \n", "UK | \n", "900 | \n", "
22 | \n", "Consultant | \n", "EXPERIENCED | \n", "UK | \n", "550 | \n", "
23 | \n", "Consultant | \n", "JUNIOR | \n", "UK | \n", "500 | \n", "
24 rows × 4 columns
\n", "\n", " | Name | \n", "Engagement | \n", "Country | \n", "Position | \n", "Seniority | \n", "region | \n", "cost | \n", "
---|---|---|---|---|---|---|---|
0 | \n", "JANE DOE | \n", "STAFF | \n", "US | \n", "Consultant | \n", "EXPERIENCED | \n", "US | \n", "550.00 | \n", "
1 | \n", "JOHN DOE | \n", "STAFF | \n", "FR | \n", "Consultant | \n", "JUNIOR | \n", "NaN | \n", "850.00 | \n", "
2 | \n", "JOHN SMITH | \n", "STAFF | \n", "US | \n", "Consultant | \n", "EXPERIENCED | \n", "US | \n", "550.00 | \n", "
... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "
32 | \n", "JOHN THE CONSULTANT | \n", "STAFF | \n", "FR | \n", "Consultant | \n", "EXPERIENCED | \n", "NaN | \n", "850.00 | \n", "
33 | \n", "JANE THE MANAGER | \n", "CONTRACTOR | \n", "BE | \n", "Project Manager | \n", "SENIOR | \n", "NaN | \n", "850.00 | \n", "
34 | \n", "JACK THE EXPERT | \n", "STAFF | \n", "BE | \n", "Consultant | \n", "SENIOR | \n", "NaN | \n", "850.00 | \n", "
35 rows × 7 columns
\n", "\n", " | Client ID | \n", "Project ID | \n", "Project | \n", "PM | \n", "Region | \n", "Start | \n", "End | \n", "Funding | \n", "Daily Rate | \n", "TCV | \n", "
---|---|---|---|---|---|---|---|---|---|---|
0 | \n", "13154 | \n", "68454 | \n", "Medium consutancy 1 | \n", "SUSAN THE MANAGER | \n", "Europe | \n", "2019-01-16 08:00:00 | \n", "2020-07-22 14:00:00 | \n", "Time and Materials | \n", "1200 | \n", "39,000 USD | \n", "
1 | \n", "2764 | \n", "70285 | \n", "Our biggest project | \n", "EXT 6a8e0ca747 | \n", "Europe | \n", "2019-10-01 08:00:00 | \n", "2021-04-30 11:00:00 | \n", "Time and Materials | \n", "1200 | \n", "1,450,000 USD | \n", "
2 | \n", "12916 | \n", "68093 | \n", "Upgrade to new version | \n", "TIM THE LEAVER | \n", "Europe | \n", "2018-09-28 08:00:00 | \n", "2020-04-27 17:00:00 | \n", "Fixed Fee | \n", "1200 | \n", "127,500 USD | \n", "
... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "
45 | \n", "12899 | \n", "63852 | \n", "Huge consultancy pool | \n", "JACK THE MANAGER | \n", "Europe | \n", "2017-08-01 08:00:00 | \n", "2020-04-27 17:00:00 | \n", "Time and Materials | \n", "1200 | \n", "198,000 USD | \n", "
46 | \n", "12901 | \n", "71248 | \n", "Small upgrade | \n", "JACK THE MANAGER | \n", "Europe | \n", "2020-01-29 08:00:00 | \n", "2020-12-31 17:00:00 | \n", "Time and Materials | \n", "1200 | \n", "108,000 USD | \n", "
47 | \n", "17719 | \n", "70618 | \n", "Small consultancy pool | \n", "SUSAN THE MANAGER | \n", "Europe | \n", "2019-11-06 08:00:00 | \n", "2020-06-30 17:00:00 | \n", "Time and Materials | \n", "1200 | \n", "27,200 USD | \n", "
48 rows × 10 columns
\n", "\n", " | Project | \n", "Activity | \n", "User | \n", "Period Starting | \n", "Effort Type | \n", "Total (Days) | \n", "Sun | \n", "Mon | \n", "Tue | \n", "Wed | \n", "Thu | \n", "Fri | \n", "Sat | \n", "Billable | \n", "
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | \n", "PRJ f74fe3647d | \n", "Consultancy | \n", "EXT 800982ab08 | \n", "2020-02-02 | \n", "Actual | \n", "2.50 | \n", "0.00 | \n", "0.50 | \n", "0.50 | \n", "0.50 | \n", "0.50 | \n", "0.50 | \n", "0.00 | \n", "True | \n", "
1 | \n", "PRJ f74fe3647d | \n", "Consultancy | \n", "EXT c8c92ca432 | \n", "2020-02-02 | \n", "Actual | \n", "2.50 | \n", "0.00 | \n", "0.50 | \n", "0.50 | \n", "0.50 | \n", "0.50 | \n", "0.50 | \n", "0.00 | \n", "True | \n", "
2 | \n", "PRJ f74fe3647d | \n", "Consultancy | \n", "EXT c8c92ca432 | \n", "2020-02-09 | \n", "Actual | \n", "2.50 | \n", "0.00 | \n", "0.50 | \n", "0.50 | \n", "0.50 | \n", "0.50 | \n", "0.50 | \n", "0.00 | \n", "True | \n", "
... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "
620 | \n", "PRJ 67df00a7e9 | \n", "Consultancy | \n", "EXT ca02603616 | \n", "2020-04-12 | \n", "Actual | \n", "1.00 | \n", "0.00 | \n", "0.50 | \n", "0.00 | \n", "0.50 | \n", "0.00 | \n", "0.00 | \n", "0.00 | \n", "True | \n", "
621 | \n", "PRJ 67df00a7e9 | \n", "Consultancy | \n", "EXT ca02603616 | \n", "2020-04-19 | \n", "Actual | \n", "1.00 | \n", "0.00 | \n", "0.50 | \n", "0.00 | \n", "0.00 | \n", "0.50 | \n", "0.00 | \n", "0.00 | \n", "True | \n", "
622 | \n", "PRJ 67df00a7e9 | \n", "Consultancy | \n", "EXT ca02603616 | \n", "2020-04-26 | \n", "Reported | \n", "1.50 | \n", "0.00 | \n", "0.50 | \n", "0.50 | \n", "0.00 | \n", "0.50 | \n", "nan | \n", "nan | \n", "True | \n", "
1792 rows × 14 columns
\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", " | Project | \n", "Activity | \n", "User | \n", "Effort Type | \n", "effort | \n", "Billable | \n", "date | \n", "weekday | \n", "month | \n", "weekend | \n", "workweek | \n", "
---|---|---|---|---|---|---|---|---|---|---|---|
0 | \n", "Upgrade in two contracts - 1 | \n", "Consultancy | \n", "CATHY THE NEW MANAGER | \n", "Actual | \n", "0.00 | \n", "True | \n", "2020-02-16 | \n", "Sun | \n", "2020-02 | \n", "True | \n", "False | \n", "
1 | \n", "Upgrade in two contracts - 2 | \n", "Consultancy | \n", "CATHY THE NEW MANAGER | \n", "Actual | \n", "0.00 | \n", "True | \n", "2020-02-16 | \n", "Sun | \n", "2020-02 | \n", "True | \n", "False | \n", "
2 | \n", "A pilot for huge opportunity | \n", "Consultancy | \n", "CATHY THE NEW MANAGER | \n", "Actual | \n", "0.00 | \n", "True | \n", "2020-02-16 | \n", "Sun | \n", "2020-02 | \n", "True | \n", "False | \n", "
... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "... | \n", "
12541 | \n", "Strategic project in a new region | \n", "Project Management | \n", "TOM THE TEAM LEADER | \n", "Reported | \n", "nan | \n", "True | \n", "2020-05-01 | \n", "Fri | \n", "2020-05 | \n", "False | \n", "True | \n", "
12542 | \n", "Strategic project in a new region 2 | \n", "Consultancy | \n", "TOM THE TEAM LEADER | \n", "Actual | \n", "nan | \n", "False | \n", "2020-05-02 | \n", "Sat | \n", "2020-05 | \n", "True | \n", "False | \n", "
12543 | \n", "Strategic project in a new region | \n", "Project Management | \n", "TOM THE TEAM LEADER | \n", "Reported | \n", "nan | \n", "True | \n", "2020-05-02 | \n", "Sat | \n", "2020-05 | \n", "True | \n", "False | \n", "
12544 rows × 11 columns
\n", "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": [ "month | \n", "2020-02 | \n", "2020-03 | \n", "2020-04 | \n", "2020-05 | \n", "
---|---|---|---|---|
User | \n", "\n", " | \n", " | \n", " | \n", " |
CATHY THE NEW MANAGER | \n", "11550.00 | \n", "19350.00 | \n", "9412.50 | \n", "0.00 | \n", "
FRANK THE CONSTULANT | \n", "15750.00 | \n", "12425.00 | \n", "14875.00 | \n", "0.00 | \n", "
IVAN THE PROGRAMMER | \n", "25062.50 | \n", "29043.75 | \n", "25643.75 | \n", "0.00 | \n", "
JACK THE EXPERT | \n", "29093.75 | \n", "31331.25 | \n", "24843.75 | \n", "0.00 | \n", "
JACK THE MANAGER | \n", "24090.62 | \n", "22318.75 | \n", "18821.88 | \n", "0.00 | \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": [ "month | \n", "2020-02 | \n", "2020-03 | \n", "2020-04 | \n", "2020-05 | \n", "
---|---|---|---|---|
User | \n", "\n", " | \n", " | \n", " | \n", " |
CATHY THE NEW MANAGER | \n", "5362.50 | \n", "11343.75 | \n", "9332.81 | \n", "0.00 | \n", "
FRANK THE CONSTULANT | \n", "7700.00 | \n", "8662.50 | \n", "7768.75 | \n", "0.00 | \n", "
IVAN THE PROGRAMMER | \n", "11481.25 | \n", "12925.00 | \n", "11618.75 | \n", "0.00 | \n", "
JACK THE EXPERT | \n", "19921.88 | \n", "21621.88 | \n", "18168.75 | \n", "0.00 | \n", "
JACK THE MANAGER | \n", "11800.00 | \n", "10900.00 | \n", "9600.00 | \n", "0.00 | \n", "
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": [ "month | \n", "2020-02 | \n", "2020-03 | \n", "2020-04 | \n", "2020-05 | \n", "
---|---|---|---|---|
User | \n", "\n", " | \n", " | \n", " | \n", " |
CATHY THE NEW MANAGER | \n", "6187.50 | \n", "8006.25 | \n", "79.69 | \n", "0.00 | \n", "
FRANK THE CONSTULANT | \n", "8050.00 | \n", "3762.50 | \n", "7106.25 | \n", "0.00 | \n", "
IVAN THE PROGRAMMER | \n", "13581.25 | \n", "16118.75 | \n", "14025.00 | \n", "0.00 | \n", "
JACK THE EXPERT | \n", "9171.88 | \n", "9709.38 | \n", "6675.00 | \n", "0.00 | \n", "
JACK THE MANAGER | \n", "12290.62 | \n", "11418.75 | \n", "9221.88 | \n", "0.00 | \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": [ "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 = $('');\n", " button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +\n", " 'ui-button-icon-only');\n", " button.attr('role', 'button');\n", " button.attr('aria-disabled', 'false');\n", " button.click(method_name, toolbar_event);\n", " button.mouseover(tooltip, toolbar_mouse_event);\n", "\n", " var icon_img = $('');\n", " icon_img.addClass('ui-button-icon-primary ui-icon');\n", " icon_img.addClass(image);\n", " icon_img.addClass('ui-corner-all');\n", "\n", " var tooltip_span = $('');\n", " tooltip_span.addClass('ui-button-text');\n", " tooltip_span.html(tooltip);\n", "\n", " button.append(icon_img);\n", " button.append(tooltip_span);\n", "\n", " nav_element.append(button);\n", " }\n", "\n", " var fmt_picker_span = $('');\n", "\n", " var fmt_picker = $('');\n", " fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');\n", " fmt_picker_span.append(fmt_picker);\n", " nav_element.append(fmt_picker_span);\n", " this.format_dropdown = fmt_picker[0];\n", "\n", " for (var ind in mpl.extensions) {\n", " var fmt = mpl.extensions[ind];\n", " var option = $(\n", " '', {selected: fmt === mpl.default_extension}).html(fmt);\n", " fmt_picker.append(option);\n", " }\n", "\n", " // Add hover states to the ui-buttons\n", " $( \".ui-button\" ).hover(\n", " function() { $(this).addClass(\"ui-state-hover\");},\n", " function() { $(this).removeClass(\"ui-state-hover\");}\n", " );\n", "\n", " var status_bar = $('