<jupyter_start><jupyter_text>Illustration of datascience Tables on Open Data from Berkeley**<NAME>**Datascience Table provides a simple, yet powerful data structure for a range of analyses.  The basic concept is an ordered set of named columns.  * It builds on the intuition many develop with *excel* - data is represented as rectangular regions.  But, rather than labeling cells, the column labels really mean something. * Tables embed smoothly in jupyter notebooks, so the user experience is that of a computational document, rather than a spreadsheet. This provides a much clearer sequence of steps from raw data to finished product, at least if they are well constructed.  There is no need to break out into *visual basic* or the like when you need more than the basics.* Tables draws heavily from relational database tables, but there is no separate language (e.g., SQL) required to do relational operations in them.* Tables provide also the concepts associated with *pivot tables* in Excel, which are closely related to relational operations, but often more natural.* Tables can be viewed as simple variants of the *dataframes* in **R** or *Pandas*.  The key is simplicity.  They are simple rectangular structures.  The cells of a table can hold arbitrary values, although columns are homogeneous, so the additional power (and complexity) of higher dimensions are rarely needed.* Tables builds directly on the scipy ecosystem.  Almost any sequence can go in, `numpy.array`s come out.  Thus, it is natural to manipulate data directly from Tables. Basic visualization is provide directly in terms of Tables, so you can go a long ways before reading `matplotlib` documentation.  However, if you need more, a lot of it can be gained through `keyword args` - and if that is not enough, drop into scipy.This notebook illustrates some of the use of Tables using municipal salary data made possible though the City of Berkeley's open data portal.<jupyter_code># This useful nonsense just goes at the top
from datascience import *
import numpy as np
import matplotlib.pyplot as plots
plots.style.use('fivethirtyeight')
%matplotlib inline
# datascience version number of last run of this notebook
version.__version__<jupyter_output><empty_output><jupyter_text>Reading raw data into a Table Let's pull a big wad of City Employee salary data from the Berkeley Open Data portal.  Since this was a trial till June 30, 2015 and you have to mouse around to get csv files, we happen to have pulled a local copy.<jupyter_code>raw_berkeley_sal_2011 = Table.read_table("./data/BerkeleyData/City_of_Berkeley_Employee_Salaries_-_2011.csv")<jupyter_output><empty_output><jupyter_text>Let's take a peek at what we have got.<jupyter_code># Tables are rendered to work in a notebook, even if they are large.  Only part shows.
# You can adjust how much of it you see, but here we'd have ~1500 rows!
raw_berkeley_sal_2011<jupyter_output><empty_output><jupyter_text>Accessing data in a TableA column of Table data is accessed by indexing it by name.  This returns the column as a numpy array.<jupyter_code>raw_berkeley_sal_2011['Base Pay']<jupyter_output><empty_output><jupyter_text>Some prefer the selectors - column and row<jupyter_code>raw_berkeley_sal_2011.column('Overtime Pay')<jupyter_output><empty_output><jupyter_text>Rows in the table can be indexed and sliced.  A row is a little like a record or dict.  It is an tupled, ordered according to the table it comes from and keeping the column names.<jupyter_code>raw_berkeley_sal_2011.rows[0]
raw_berkeley_sal_2011.row(0).item('Base Pay')
raw_berkeley_sal_2011.row(0)[2]
raw_berkeley_sal_2011.row(0).asdict()
raw_berkeley_sal_2011.rows[0:10]<jupyter_output><empty_output><jupyter_text>Converting data in a Table to make it workableWhen we read in data from a csv file we got a bunch of columns filled with a bunch of strings.  As is often the case, we want the data in a column to represent values that we can analyze, whereas we want the printed format of a column to reflect its meaning.  Currency is the most common such situation.  Let's clean up our salary table. We might start by getting ahold of the names of column that we want to clean up<jupyter_code>paylabels = raw_berkeley_sal_2011.labels[2:]
paylabels<jupyter_output><empty_output><jupyter_text>Clean derivatives of raw tablesIt is good hygene to keep the raw data raw and produce distinct, clean derivatives.  Let's start by making a copy of the raw table.  A new name and a new table.<jupyter_code>berkeley_sal_2011 = raw_berkeley_sal_2011.copy()<jupyter_output><empty_output><jupyter_text>Tables allow columns to have customized formattersIn Excel you do this by formatting the cells.  We want to have the data as numbers, keep track of the type, and have it look nice.<jupyter_code>berkeley_sal_2011.set_format(paylabels, CurrencyFormatter)
berkeley_sal_2011<jupyter_output><empty_output><jupyter_text>Now we get values we can compute on - and they still display as currency.<jupyter_code>berkeley_sal_2011['Base Pay']
max(berkeley_sal_2011['Total Pay Benefits'])<jupyter_output><empty_output><jupyter_text>Descriptive Statistics SummaryNow we can try to get a summary of the data with some descriptive statistics.  The `stats` method on Tables computes a list of statistics over each column and creates a new Table containing these statistics.  The default is tailored to the Berkeley Data8 course. Here we provide what you expect from the `summary` operation in **R**<jupyter_code>def firstQtile(x) : return np.percentile(x,25)
def thirdQtile(x) : return np.percentile(x,25)
summary_ops = (min, firstQtile, np.median, np.mean, thirdQtile, max)
berkeley_sal_2011.select(paylabels).stats(ops=summary_ops)<jupyter_output><empty_output><jupyter_text>OK, so it looks like the average salary is about 86k, and it ranges up to 300k with some hefty overtime pay.  Let's see if we can understand what is going on.  Visualizing data* `Table.select` creates a new table consisting of the specified list of columns.* `Table.hist` plots a histogram of each of the columns in a table.  It can either overlay the histograms or show them separately.  Here we have specified the number of bins<jupyter_code>berkeley_sal_2011.select(["Base Pay", 
                          "Overtime Pay", 
                          "Total Pay Benefits"]).hist(overlay=False,normed=False,
                                                      bins=40)<jupyter_output><empty_output><jupyter_text>Interesting.  Base pay is bimodal.  Most employees get no overtime, but there is a looong tale.  Let's look at the individual level.  Who's at the top?<jupyter_code>berkeley_sal_2011.sort('Total Pay Benefits', descending=True)<jupyter_output><empty_output><jupyter_text>So where does the $alary go? First, how many employees?`Table.num_rows` returns just what you'd think. The number of rows.  Which in this case is the number of employees on the city payroll.<jupyter_code>berkeley_sal_2011.num_rows<jupyter_output><empty_output><jupyter_text>Grouping and Sorting Table data* `Table.drop` creates a new Table without some columns.  It is like select, but you don't have to name everythng you want.* `Table.group` aggregates data by grouping all the rows that contain a common value in one (or more) columns.  Here we group in "Job Title" summing the entries in all other columns for each group.  We placed a column full of `1` to get a count, while summing salaries and such.* `Table.sort` sorts the rows in a Table by a column - just like sort in Excel.<jupyter_code># lose the individual names
job_titles = berkeley_sal_2011.drop("Employee Name") 
# Build a handy column full of 1s
job_titles["Title"] = 1
# Group by title summing the number of rows per
by_title = job_titles.group("Job Title", sum)  
# Sort by the number of employees per title
ordered_by_title = by_title.sort('Title sum', descending = True) 
# let's see what we get
ordered_by_title 
ordered_by_title.num_rows<jupyter_output><empty_output><jupyter_text>Wow, 305 Job Titles for 1437 employees!<jupyter_code>"{0:.3} employees per Job Code".format(berkeley_sal_2011.num_rows/ordered_by_title.num_rows)<jupyter_output><empty_output><jupyter_text>Plotting data* `Table.plot` plots each of the columns in a table, either on separate charts or overlayed on a single chart.  Optionally one of the columns can be specified as the horizontal axis and all others plotted against this.<jupyter_code>ordered_by_title.select(['Title sum','Total Pay Benefits sum']).sort('Title sum').plot(overlay=False)<jupyter_output><empty_output><jupyter_text>How about that, a few job categories have most of the employes and most of the spend, but it is far from uniform.  Let's look a little deeper.  Which categories consume most of the budget?<jupyter_code>by_title.sort('Total Pay Benefits sum', descending = True)
by_title.sort('Total Pay Benefits sum', descending = True).row(0)
by_title.select(('Job Title', 'Total Pay Benefits sum')).sort('Total Pay Benefits sum', descending=True)<jupyter_output><empty_output><jupyter_text>As is often the case in the real world, the categorization used for operations is not directly useful for analysis.  We often need to build categories in order to get a handle on what's going on.What do all those job titles look like<jupyter_code>ordered_by_title['Job Title']
categories = {
    'Police': ["POLICE"], 
    'Fire': ["FIRE"], 
    'Animal Control':["ANIMAL"], 
    'Health': ["HEALTH", "PSYCH", "HLTH"],
    'Library': ['LIBRARY','LIBRARIAN'],
    'Offical' : ['MAYOR','COUNCIL', 'COMMISSIONER', 'CITY MANAGER'],
    'Trades' :["ELECTRICIAN","MECHANIC", "ENGINEER"],
    'Parking' : ["PARKING"],
    'Recreation' : ["RECREATION", "AQUATICS"],
    'Gardener' : ["GARDEN"],
    "Labor" : ["LABOR", "JANITOR"],
    'Community': ["COMMUNITY"],
    'Admin' : ["ADMIN"],
    'Traffic' : ["TRAFFIC"],
    'Accounting' : ["ACCOUNT"],
    'Dispatch' : ["DISPATCH"],
    'Waste' : ["WASTE", "SEWER"],
    'Analyst' : ["ANALYS"],
    'Office' : ["OFFICE "],
    'Legal' : ['LEGISLAT', 'ATTORN', 'ATTY'],
    'IT' : ["PROG", "INFORMATION SYSTEMS"],
    'School' : ["SCHOOL"],
    'Architect' : ["ARCHITECT"],
    'Planner' : ["PLANNER", "PERMIT"]
    }
categories<jupyter_output><empty_output><jupyter_text>Applying a function to create a new column* `table.apply`: applies a function to every element in a column.  One of the best examples of high-order functions and tables is in categorizing data. As is often the case, we create a new column with the results<jupyter_code>def categorize (title) : 
    for category, keywords in categories.items():
        for word in keywords :
            if title.find(word) >= 0 : return category
    return 'Other'
berkeley_sal_2011['Category'] = berkeley_sal_2011.apply(categorize, 'Job Title')
berkeley_sal_2011
# lose the individual names
job_categories = berkeley_sal_2011.drop("Employee Name") 
job_categories["Cat"] = 1
by_categories = job_categories.group("Category", sum)
by_categories.sort("Total Pay Benefits sum", descending=True).show()<jupyter_output><empty_output><jupyter_text>As is often the case working with real data, we often need to iterate a bit to get what we want out of it.  With all those titles, a lot of stuff is likely to end up as other.  Here we have a little iterative process to get enough of the job titles categorized<jupyter_code>job_categories.where('Category', 'Other')
job_categories.where('Category', 'Other').group('Job Title',sum).sort('Cat sum', descending=True)
job_categories.where('Category', 'Other').group('Job Title',sum).sort('Total Pay Benefits sum', descending=True)<jupyter_output><empty_output><jupyter_text>So no job title left has more than 10 employees in it, but some have quite a bit of cost.  We could go back and add more entries to our category table and iterate a bit.  The important thing is that we create new tables, we don't clobber old ones.   Well this shows the challenge in managing budget pretty nicely.  Most of the money is spent in a few job categories.  But then there are still over 200 employees in a zillion other categories that are stile the 2 spend.<jupyter_code>by_categories.sort('Total Pay Benefits sum', descending=True).barh('Category', select=['Total Pay Benefits sum', 'Cat sum'], overlay=False)<jupyter_output><empty_output><jupyter_text>So let's try to understand the police category a bit more.<jupyter_code>police = job_categories.where('Category', 'Police')
police<jupyter_output><empty_output><jupyter_text>How do the pay labels spread across the force?We can look at histograms by pay label.  First all toegether and then broken apart.<jupyter_code>police.select(paylabels).hist(bins=30,normed=False)
police.select(paylabels).hist(bins=30,normed=False, overlay=False)<jupyter_output><empty_output><jupyter_text>Base pay seems to chunk into categories, perhaps by job title.  Most members of the force do little overtime, but a few do a lot! How many are in each Job Title?<jupyter_code>police.group('Job Title')
# We can actually get all the data by title
police.select(['Job Title','Base Pay', 'Overtime Pay']).group('Job Title', collect=lambda x:x)<jupyter_output><empty_output><jupyter_text>We can't just pivot by Job Title because we don't have a uniform number of rows, but what we can do is for pivot and bin (or histogram) so we can see the distribution of a column by job title.Sure enough.  Officers cluster around 100-120k, sergeants at 130-140k, but there's a little overlap.<jupyter_code>police.pivot_bin('Job Title', 'Base Pay', bins=np.arange(0,200000,10000)).show()
police.pivot_bin('Job Title', 'Base Pay', bins=np.arange(0,200000,10000)).bar('bin')
police.pivot_bin('Job Title', 'Overtime Pay', bins=np.arange(0,200000,10000)).bar('bin')
police.pivot_bin('Job Title', 'Total Pay Benefits', bins=np.arange(0,420000,10000)).bar('bin')
fire = job_categories.where('Category','Fire')
fire.select(paylabels).hist(bins=30)
fire.group('Job Title')
fire.pivot_bin('Job Title', 'Total Pay Benefits', bins=np.arange(0,420000,10000)).bar('bin')<jupyter_output><empty_output><jupyter_text>Let's compare the 2011 data with more recent 2013 data.<jupyter_code>raw_berkeley_sal_2013 = Table.read_table("./data/BerkeleyData/City_of_Berkeley_Employee_Salaries_-_2013.csv")
raw_berkeley_sal_2013<jupyter_output><empty_output><jupyter_text>Well, the data base changed.  It picked up a few columns over the years. And we need to convert the salary strings to numbers so we can do analysis on them.  All in one go...<jupyter_code>berkeley_sal_2013 = raw_berkeley_sal_2013.drop(['Year','Notes','Agency'])
berkeley_sal_2013
berkeley_sal_2013.set_format(berkeley_sal_2013.labels[2:], CurrencyFormatter)
berkeley_sal_2013["Total Pay & Benefits"]
berkeley_sal_2013.sort('Total Pay & Benefits',descending=True)<jupyter_output><empty_output><jupyter_text>Isn't that interesting.  They seem to have gotten their overtime under control.  Was that management, end of the occupy movement, something else?  Let's do a bit of comparison.First we need to do some clean up and get labels we can deal with.<jupyter_code>b2011 = berkeley_sal_2011.select(["Employee Name", "Job Title", "Total Pay Benefits"])
b2011.relabel('Total Pay Benefits', "Total 2011")
b2011.sort('Total 2011', descending=True)
b2013 = berkeley_sal_2013.select(["Employee Name", "Job Title", "Total Pay & Benefits"])
b2013.relabel('Job Title','Title 2013')
b2013.relabel("Total Pay & Benefits", "Total 2013")
b2013.sort('Total 2013', descending=True)<jupyter_output><empty_output><jupyter_text>Snap! They decided that case was a good idea for proper nouns.  Let's go back to the old way.<jupyter_code>b2013['Employee Name'] = b2013.apply(str.upper, 'Employee Name')
b2013<jupyter_output><empty_output><jupyter_text>Now we can put the two tables together to see what happened with employees who were around in both years.  Here we get to use another powerful operations on tables.* `Table.join`: joins two tables together using a column of each that contains common values.Here we have the employee names in each table.  The join will give us the title and salary in both years for those employees in both tables, i.e., working for the city in both years<jupyter_code>b11_13 = b2011.join('Employee Name', b2013)
b11_13<jupyter_output><empty_output><jupyter_text>Let's add a column with increase in total pay.<jupyter_code>b11_13["Increase"] =b11_13['Total 2013'] - b11_13['Total 2011']
b11_13.sort('Increase', "decr").select('Increase').plot()<jupyter_output><empty_output><jupyter_text>On the tails we have people who joined part way through 2011 or left part way through 2013.<jupyter_code>b11_13.stats(summary_ops)<jupyter_output><empty_output><jupyter_text>Well that's interesting.  Total compensation seems to have dropped.  Did the budget actually go down?<jupyter_code>sum2011 = np.sum(berkeley_sal_2011['Total Pay Benefits'])
"${:,}".format(int(sum2011))
sum2013 = np.sum(berkeley_sal_2013['Total Pay & Benefits'])
"${:,}".format(int(sum2013))
"${:,}".format(int(sum2013-sum2011))
"{:.1%}".format((sum2013-sum2011)/sum2011)<jupyter_output><empty_output><jupyter_text>Look at that.<jupyter_code>np.sum(berkeley_sal_2011['Overtime Pay'])
np.sum(berkeley_sal_2013['Overtime Pay'])<jupyter_output><empty_output><jupyter_text>Let's see who got promoted or demoted<jupyter_code>b11_13.where(b11_13['Job Title'] != b11_13['Title 2013']).sort('Total 2013', descending=True).show()
b11_13.where(b11_13['Job Title'] == b11_13['Title 2013']).sort('Increase', descending=True).show()<jupyter_output><empty_output><jupyter_text>Perhaps we might want to look at the relationship of these two variables.  That leads to another useful operator* `Table.scatter`: does a scatter plot of columns against one columns<jupyter_code>b11_13.scatter('Total 2011', 'Total 2013')<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09519802535622125, 'fraction_numerical': 0.028497699988780434, 'mean_word_length': 5.7195627591405955, 'pattern_counts': {'":': 0, '<': 150, '<?xml version=': 0, '>': 151, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '37', 'dir': 'jupyter-structured-clean-dedup', 'id': '430777', 'license': 'BSD-2-Clause', 'n_tokens_mistral': 5868, 'n_tokens_neox': 5258, 'n_words': 2003, 'path': 'BerkeleySalary.ipynb', 'repo_name': 'deculler/TableDemos', 'size': '17835'}