agate(1)
| AGATE(1) | agate | AGATE(1) |
NAME
agate - agate 1.7.1 Build statusPyPI downloadsVersionLicenseSupport Python versions
agate is a Python data analysis library that is optimized for humans instead of machines. It is an alternative to numpy and pandas that solves real-world problems with readable code.
agate was previously known as journalism.
Important links:
- Documentation: http://agate.rtfd.org
- Repository: https://github.com/wireservice/agate
- Issues: https://github.com/wireservice/agate/issues
ABOUT AGATE
Why agate?
- A readable and user-friendly API.
- A complete set of SQL-like operations.
- Unicode support everywhere.
- Decimal precision everywhere.
- Exhaustive user documentation.
- Pluggable extensions that add SQL integration, Excel support, and more.
- Designed with iPython, Jupyter and atom/hydrogen in mind.
- Pure Python. No C dependencies to compile.
- Exhaustive test coverage.
- MIT licensed and free for all purposes.
- Zealously zen.
- Made with love.
Principles
agate is a intended to fill a very particular programming niche. It should not be allowed to become as complex as numpy or pandas. Please bear in mind the following principles when considering a new feature:
- Humans have less time than computers. Optimize for humans.
- Most datasets are small. Don't optimize for "big data".
- Text is data. It must always be a first-class citizen.
- Python gets it right. Make it work like Python does.
- Humans lives are nasty, brutish and short. Make it easy.
- Mutability leads to confusion. Processes that alter data must create new copies.
- Extensions are the way. Don't add it to core unless everybody needs it.
INSTALLATION
Users
To use agate install it with pip:
pip install agate
For non-English locale support, install PyICU.
Developers
If you are a developer that also wants to hack on agate, install it from git:
git clone git://github.com/wireservice/agate.git cd agate mkvirtualenv agate pip install -e .[test] python setup.py develop
NOTE:
pytest --cov agate
Supported platforms
agate supports the following versions of Python:
- Python 2.7
- Python 3.5+
- PyPy versions >= 4.0.0
It is tested primarily on OSX, but due to its minimal dependencies it should work perfectly on both Linux and Windows.
NOTE:
TUTORIAL
The agate tutorial is now available in new-and-improved Jupyter Notebook format.
Find it 1.7.1
COOKBOOK
Welcome to the agate cookbook, a source of how-to's and use cases.
Creating tables
From data in memory
From a list of lists.
column_names = ['letter', 'number'] column_types = [agate.Text(), agate.Number()] rows = [
('a', 1),
('b', 2),
('c', None) ] table = agate.Table(rows, column_names, column_types)
From a list of dictionaries.
rows = [
dict(letter='a', number=1),
dict(letter='b', number=2),
dict(letter='c', number=None) ] table = agate.Table.from_object(rows)
From a CSV
By default, loading a table from a CSV will use agate's builtin TypeTester to infer column types:
table = agate.Table.from_csv('filename.csv')
Override type inference
In some cases agate's TypeTester may guess incorrectly. To override the type for some columns and use TypeTester for the rest, pass a dictionary to the column_types argument.
specified_types = {
'column_name_one': agate.Text(),
'column_name_two': agate.Number()
}
table = agate.Table.from_csv('filename.csv', column_types=specified_types)
This will use a generic TypeTester and override your specified columns with TypeTester.force.
Limit type inference
For large datasets TypeTester may be unreasonably slow. In order to limit the amount of data it uses you can specify the limit argument. Note that if data after the limit invalidates the TypeTester's inference you may get errors when the data is loaded.
tester = agate.TypeTester(limit=100)
table = agate.Table.from_csv('filename.csv', column_types=tester)
Manually specify columns
If you know the types of your data you may find it more efficient to manually specify the names and types of your columns. This also gives you an opportunity to rename columns when you load them.
text_type = agate.Text()
number_type = agate.Number()
column_names = ['city', 'area', 'population']
column_types = [text_type, number_type, number_type]
table = agate.Table.from_csv('population.csv', column_names, column_types)
Or, you can use this method to load data from a file that does not have a header row:
table = agate.Table.from_csv('population.csv', column_names, column_types, header=False)
From a unicode CSV
You don't have to do anything special. It just works!
From a latin1 CSV
table = agate.Table.from_csv('census.csv', encoding='latin1')
From a semicolon delimited CSV
Normally, agate will automatically guess the delimiter of your CSV, but if that guess fails you can specify it manually:
table = agate.Table.from_csv('filename.csv', delimiter=';')
From a TSV (tab-delimited CSV)
This is the same as the previous example, but in this case we specify that the delimiter is a tab:
table = agate.Table.from_csv('filename.csv', delimiter='\t')
From JSON
table = agate.Table.from_json('filename.json')
From newline-delimited JSON
table = agate.Table.from_json('filename.json', newline=True)
From a SQL database
Use the agate-sql extension.
import agatesql
agatesql.patch()
table = agate.Table.from_sql('postgresql:///database', 'input_table')
From an Excel spreadsheet
Use the agate-excel extension. It supports both .xls and .xlsx files.
import agateexcel
agateexcel.patch()
table = agate.Table.from_xls('test.xls', sheet='data')
table2 = agate.Table.from_xlsx('test.xlsx', sheet='data')
From a DBF table
DBF is the file format used to hold tabular data for ArcGIS shapefiles. agate-dbf extension.
import agatedbf
agatedbf.patch()
table = agate.Table.from_dbf('test.dbf')
From a remote file
Use the agate-remote extension.
import agateremote
agateremote.patch()
table = agate.Table.from_url('https://raw.githubusercontent.com/wireservice/agate/master/examples/test.csv')
agate-remote also let’s you create an Archive, which is a reference to a group of tables with a known path structure.
archive = agateremote.Archive('https://github.com/vincentarelbundock/Rdatasets/raw/master/csv/')
table = archive.get_table('sandwich/PublicSchools.csv')
Save a table
To a CSV
table.to_csv('filename.csv')
To JSON
table.to_json('filename.json')
To newline-delimited JSON
table.to_json('filename.json', newline=True)
To a SQL database
Use the agate-sql extension.
import agatesql
table.to_sql('postgresql:///database', 'output_table')
Remove columns
Include specific columns
Create a new table with only a specific set of columns:
include_columns = ['column_name_one', 'column_name_two'] new_table = table.select(include_columns)
Exclude specific columns
Create a new table without a specific set of columns:
exclude_columns = ['column_name_one', 'column_name_two'] new_table = table.exclude(exclude_columns)
Filter rows
By regex
You can use Python's builtin re module to introduce a regular expression into a Table.where() query.
For example, here we find all states that start with "C".
import re
new_table = table.where(lambda row: re.match('^C', str(row['state'])))
This can also be useful for finding values that don't match your expectations. For example, finding all values in the "phone number" column that don't look like phone numbers:
new_table = table.where(lambda row: not re.match('\d{3}-\d{3}-\d{4}', str(row['phone'])))
By glob
Hate regexes? You can use glob (fnmatch) syntax too!
from fnmatch import fnmatch
new_table = table.where(lambda row: fnmatch('C*', row['state']))
Values within a range
This snippet filters the dataset to incomes between 100,000 and 200,000.
new_table = table.where(lambda row: 100000 < row['income'] < 200000)
Dates within a range
This snippet filters the dataset to events during the summer of 2015:
import datetime new_table = table.where(lambda row: datetime.datetime(2015, 6, 1) <= row['date'] <= datetime.datetime(2015, 8, 31))
If you want to filter to events during the summer of any year:
new_table = table.where(lambda row: 6 <= row['date'].month <= 8)
Top N percent
To filter a dataset to the top 10% percent of values we first compute the percentiles for the column and then use the result in the Table.where() truth test:
percentiles = table.aggregate(agate.Percentiles('salary'))
top_ten_percent = table.where(lambda r: r['salary'] >= percentiles[90])
Random sample
By combining a random sort with limiting, we can effectively get a random sample from a table.
import random randomized = table.order_by(lambda row: random.random()) sampled = table.limit(10)
Ordered sample
With can also get an ordered sample by simply using the step parameter of the Table.limit() method to get every Nth row.
sampled = table.limit(step=10)
Distinct values
You can retrieve a distinct list of values in a column using Column.values_distinct() or Table.distinct().
Table.distinct() returns the entire row so it's necessary to chain a select on the specific column.
columns = ('value',)
rows = ([1],[2],[2],[5])
new_table = agate.Table(rows, columns)
new_table.columns['value'].values_distinct()
# or
new_table.distinct('value').columns['value'].values()
(Decimal('1'), Decimal('2'), Decimal('5'))
Sort
Alphabetical
Order a table by the last_name column:
new_table = table.order_by('last_name')
Numerical
Order a table by the cost column:
new_table = table.order_by('cost')
By date
Order a table by the birth_date column:
new_table = table.order_by('birth_date')
Reverse order
The order of any sort can be reversed by using the reverse keyword:
new_table = table.order_by('birth_date', reverse=True)
Multiple columns
Because Python's internal sorting works natively with sequences, we can implement multi-column sort by returning a tuple from the key function.
new_table = table.order_by(lambda row: (row['last_name'], row['first_name']))
This table will now be ordered by last_name, then first_name.
Random order
import random new_table = table.order_by(lambda row: random.random())
Search
Exact search
Find all individuals with the last_name "Groskopf":
family = table.where(lambda r: r['last_name'] == 'Groskopf')
Fuzzy search by edit distance
By leveraging an existing Python library for computing the Levenshtein edit distance it is trivially easy to implement a fuzzy string search.
For example, to find all names within 2 edits of "Groskopf":
from Levenshtein import distance fuzzy_family = table.where(lambda r: distance(r['last_name'], 'Groskopf') <= 2)
These results will now include all those "Grosskopfs" and "Groskoffs" whose mail I am always getting.
Fuzzy search by phonetic similarity
By using Fuzzy to calculate phonetic similarity, it is possible to implement a fuzzy phonetic search.
For example to find all rows with first_name phonetically similar to "Catherine":
import fuzzy
dmetaphone = fuzzy.DMetaphone(4)
phonetic_search = dmetaphone('Catherine')
def phonetic_match(r):
return any(x in dmetaphone(r['first_name']) for x in phonetic_search)
phonetic_family = table.where(lambda r: phonetic_match(r))
Standardize names and values
Standardize row and columns names
The Table.rename() method has arguments to convert row or column names to slugs and append unique identifiers to duplicate values.
Using an existing table object:
# Convert column names to unique slugs table.rename(slug_columns=True) # Convert row names to unique slugs table.rename(slug_rows=True) # Convert both column and row names to unique slugs table.rename(slug_columns=True, slug_rows=True)
Standardize column values
agate has a Slug computation that can be used to also standardize text column values. The computation has an option to also append unique identifiers to duplicate values.
Using an existing table object:
# Convert the values in column 'title' to slugs new_table = table.compute([
('title-slug', agate.Slug('title')) ]) # Convert the values in column 'title' to unique slugs new_table = table.compute([
('title-slug', agate.Slug('title', ensure_unique=True)) ])
Statistics
Common descriptive and aggregate statistics are included with the core agate library. For additional statistical methods beyond the scope of agate consider using the agate-stats extension or integrating with scipy.
Descriptive statistics
agate includes a full set of standard descriptive statistics that can be applied to any column containing Number data.
table.aggregate(agate.Sum('salary'))
table.aggregate(agate.Min('salary'))
table.aggregate(agate.Max('salary'))
table.aggregate(agate.Mean('salary'))
table.aggregate(agate.Median('salary'))
table.aggregate(agate.Mode('salary'))
table.aggregate(agate.Variance('salary'))
table.aggregate(agate.StDev('salary'))
table.aggregate(agate.MAD('salary'))
Or, get several at once:
table.aggregate([
('salary_min', agate.Min('salary')),
('salary_ave', agate.Mean('salary')),
('salary_max', agate.Max('salary')), ])
Aggregate statistics
You can also generate aggregate statistics for subsets of data (sometimes referred to as "rolling up"):
doctors = patients.group_by('doctor')
patient_ages = doctors.aggregate([
('patient_count', agate.Count()),
('age_mean', agate.Mean('age')),
('age_median', agate.Median('age'))
])
The resulting table will have four columns: doctor, patient_count, age_mean and age_median.
You can roll up by multiple columns by chaining agate's Table.group_by() method.
doctors_by_state = patients.group_by("state").group_by('doctor')
Distribution by count (frequency)
Counting the number of each unique value in a column can be accomplished with the Table.pivot() method:
# Counts of a single column's values
table.pivot('doctor')
# Counts of all combinations of more than one column's values
table.pivot(['doctor', 'hospital'])
The resulting tables will have a column for each key column and another Count column counting the number of instances of each value.
Distribution by percent
Table.pivot() can also be used to calculate the distribution of values as a percentage of the total number:
# Percents of a single column's values
table.pivot('doctor', computation=agate.Percent('Count'))
# Percents of all combinations of more than one column's values
table.pivot(['doctor', 'hospital'], computation=agate.Percent('Count'))
The output table will be the same format as the previous example, except the value column will be named Percent.
Identify outliers
The agate-stats extension adds methods for finding outliers.
import agatestats
agatestats.patch()
outliers = table.stdev_outliers('salary', deviations=3, reject=False)
By specifying reject=True you can instead return a table including only those values not identified as outliers.
not_outliers = table.stdev_outliers('salary', deviations=3, reject=True)
The second, more robust, method for identifying outliers is by identifying values which are more than some number of "median absolute deviations" from the median (typically 3).
outliers = table.mad_outliers('salary', deviations=3, reject=False)
As with the first example, you can specify reject=True to exclude outliers in the resulting table.
Custom statistics
You can also generate custom aggregated statistics for your data by defining your own 'summary' aggregation. This might be especially useful for performing calculations unique to your data. Here's a simple example:
# Create a custom summary aggregation with agate.Summary
# Input a column name, a return data type and a function to apply on the column
count_millionaires = agate.Summary('salary', agate.Number(), lambda r: sum(salary > 1000000 for salary in r.values()))
table.aggregate([
count_millionaires
])
Your custom aggregation can be used to determine both descriptive and aggregate statistics shown above.
Compute new values
Change
new_table = table.compute([
('2000_change', agate.Change('2000', '2001')),
('2001_change', agate.Change('2001', '2002')),
('2002_change', agate.Change('2002', '2003')) ])
Or, better yet, compute the whole decade using a loop:
computations = [] for year in range(2000, 2010):
change = agate.Change(year, year + 1)
computations.append(('%i_change' % year, change)) new_table = table.compute(computations)
Percent
Calculate the percentage for each value in a column with Percent. Values are divided into the sum of the column by default.
columns = ('value',)
rows = ([1],[2],[2],[5])
new_table = agate.Table(rows, columns)
new_table = new_table.compute([
('percent', agate.Percent('value'))
])
new_table.print_table()
| value | percent |
| ----- | ------- |
| 1 | 10 |
| 2 | 20 |
| 2 | 20 |
| 5 | 50 |
Override the denominator with a keyword argument.
new_table = new_table.compute([
('percent', agate.Percent('value', 5)) ]) new_table.print_table() | value | percent | | ----- | ------- | | 1 | 20 | | 2 | 40 | | 2 | 40 | | 5 | 100 |
Percent change
Want percent change instead of value change? Just swap out the Computation:
computations = [] for year in range(2000, 2010):
change = agate.PercentChange(year, year + 1)
computations.append(('%i_change' % year, change)) new_table = table.compute(computations)
Indexed/cumulative change
Need your change indexed to a starting year? Just fix the first argument:
computations = [] for year in range(2000, 2010):
change = agate.Change(2000, year + 1)
computations.append(('%i_change' % year, change)) new_table = table.compute(computations)
Of course you can also use PercentChange if you need percents rather than values.
Round to two decimal places
agate stores numerical values using Python's decimal.Decimal type. This data type ensures numerical precision beyond what is supported by the native float() type, however, because of this we can not use Python's builtin round() function. Instead we must use decimal.Decimal.quantize().
We can use Table.compute() to apply the quantize to generate a rounded column from an existing one:
from decimal import Decimal number_type = agate.Number() def round_price(row):
return row['price'].quantize(Decimal('0.01')) new_table = table.compute([
('price_rounded', agate.Formula(number_type, round_price)) ])
To round to one decimal place you would simply change 0.01 to 0.1.
Difference between dates
Calculating the difference between dates (or dates and times) works exactly the same as it does for numbers:
new_table = table.compute([
('age_at_death', agate.Change('born', 'died')) ])
Levenshtein edit distance
The Levenshtein edit distance is a common measure of string similarity. It can be used, for instance, to check for typos between manually-entered names and a version that is known to be spelled correctly.
Implementing Levenshtein requires writing a custom Computation. To save ourselves building the whole thing from scratch, we will lean on the python-Levenshtein library for the actual algorithm.
import agate from Levenshtein import distance class LevenshteinDistance(agate.Computation):
"""
Computes Levenshtein edit distance between the column and a given string.
"""
def __init__(self, column_name, compare_string):
self._column_name = column_name
self._compare_string = compare_string
def get_computed_data_type(self, table):
"""
The return value is a numerical distance.
"""
return agate.Number()
def validate(self, table):
"""
Verify the column is text.
"""
column = table.columns[self._column_name]
if not isinstance(column.data_type, agate.Text):
raise agate.DataTypeError('Can only be applied to Text data.')
def run(self, table):
"""
Find the distance, returning null when the input column was null.
"""
new_column = []
for row in table.rows:
val = row[self._column_name]
if val is None:
new_column.append(None)
else:
new_column.append(distance(val, self._compare_string))
return new_column
This code can now be applied to any Table just as any other Computation would be:
new_table = table.compute([
('distance', LevenshteinDistance('column_name', 'string to compare')) ])
The resulting column will contain an integer measuring the edit distance between the value in the column and the comparison string.
USA Today Diversity Index
The USA Today Diversity Index is a widely cited method for evaluating the racial diversity of a given area. Using a custom Computation makes it simple to calculate.
Assuming that your data has a column for the total population, another for the population of each race and a final column for the hispanic population, you can implement the diversity index like this:
class USATodayDiversityIndex(agate.Computation):
def get_computed_data_type(self, table):
return agate.Number()
def run(self, table):
new_column = []
for row in table.rows:
race_squares = 0
for race in ['white', 'black', 'asian', 'american_indian', 'pacific_islander']:
race_squares += (row[race] / row['population']) ** 2
hispanic_squares = (row['hispanic'] / row['population']) ** 2
hispanic_squares += (1 - (row['hispanic'] / row['population'])) ** 2
new_column.append((1 - (race_squares * hispanic_squares)) * 100)
return new_column
We apply the diversity index like any other computation:
with_index = table.compute([
('diversity_index', USATodayDiversityIndex()) ])
Simple Moving Average
A simple moving average is the average of some number of prior values in a series. It is typically used to smooth out variation in time series data.
The following custom Computation will compute a simple moving average. This example assumes your data is already sorted.
class SimpleMovingAverage(agate.Computation):
"""
Computes the simple moving average of a column over some interval.
"""
def __init__(self, column_name, interval):
self._column_name = column_name
self._interval = interval
def get_computed_data_type(self, table):
"""
The return value is a numerical average.
"""
return agate.Number()
def validate(self, table):
"""
Verify the column is numerical.
"""
column = table.columns[self._column_name]
if not isinstance(column.data_type, agate.Number):
raise agate.DataTypeError('Can only be applied to Number data.')
def run(self, table):
new_column = []
for i, row in enumerate(table.rows):
if i < self._interval:
new_column.append(None)
else:
values = tuple(r[self._column_name] for r in table.rows[i - self._interval:i])
if None in values:
new_column.append(None)
else:
new_column.append(sum(values) / self._interval)
return new_column
You would use the simple moving average like so:
with_average = table.compute([
('six_month_moving_average', SimpleMovingAverage('price', 6)) ])
Dates and times
Specify a date format
By default agate will attempt to guess the format of a Date or DateTime column. In some cases, it may not be possible to automatically figure out the format of a date. In this case you can specify a datetime.datetime.strptime() formatting string to specify how the dates should be parsed. For example, if your dates were formatted as "15-03-15" (March 15th, 2015) then you could specify:
date_type = agate.Date('%d-%m-%y')
Another use for this feature is if you have a column that contains extraneous data. For instance, imagine that your column contains hours and minutes, but they are always zero. It would make more sense to load that data as type Date and ignore the extra time information:
date_type = agate.Date('%m/%d/%Y 00:00')
Specify a timezone
Timezones are hard. Under normal circumstances (no arguments specified), agate will not try to parse timezone information, nor will it apply a timezone to the datetime.datetime instances it creates. (They will be naive in Python parlance.) There are two ways to force timezone data into your agate columns.
The first is to use a format string, as shown above, and specify a pattern for timezone information:
datetime_type = agate.DateTime('%Y-%m-%d %H:%M:%S%z')
The second way is to specify a timezone as an argument to the type constructor:
import pytz
eastern = pytz.timezone('US/Eastern')
datetime_type = agate.DateTime(timezone=eastern)
In this case all timezones that are processed will be set to have the Eastern timezone. Note, the timezone will be set, not converted. You cannot use this method to convert your timezones from UTC to another timezone. To do that see Convert timezones.
Calculate a time difference
See Difference between dates.
Sort by date
See By date.
Convert timezones
If you load data from a spreadsheet in one timezone and you need to convert it to another, you can do this using a Formula. Your datetime column must have timezone data for the following example to work. See Specify a timezone.
import pytz
us_eastern = pytz.timezone('US/Eastern')
datetime_type = agate.DateTime(timezone=us_eastern)
column_names = ['what', 'when']
column_types = [text_type, datetime_type]
table = agate.Table.from_csv('events.csv', columns)
rome = timezone('Europe/Rome')
timezone_shifter = agate.Formula(lambda r: r['when'].astimezone(rome))
table = agate.Table.compute([
('when_in_rome', timezone_shifter)
])
Emulate SQL
agate's command structure is very similar to SQL. The primary difference between agate and SQL is that commands like SELECT and WHERE explicitly create new tables. You can chain them together as you would with SQL, but be aware each command is actually creating a new table.
NOTE:
If you want to read and write data from SQL, see From a SQL database.
SELECT
SQL:
SELECT state, total FROM table;
agate:
new_table = table.select(['state', 'total'])
WHERE
SQL:
SELECT * FROM table WHERE LOWER(state) = 'california';
agate:
new_table = table.where(lambda row: row['state'].lower() == 'california')
ORDER BY
SQL:
SELECT * FROM table ORDER BY total DESC;
agate:
new_table = table.order_by(lambda row: row['total'], reverse=True)
DISTINCT
SQL:
SELECT DISTINCT ON (state) * FROM table;
agate:
new_table = table.distinct('state')
NOTE:
INNER JOIN
SQL (two ways):
SELECT * FROM patient, doctor WHERE patient.doctor = doctor.id; SELECT * FROM patient INNER JOIN doctor ON (patient.doctor = doctor.id);
agate:
joined = patients.join(doctors, 'doctor', 'id', inner=True)
LEFT OUTER JOIN
SQL:
SELECT * FROM patient LEFT OUTER JOIN doctor ON (patient.doctor = doctor.id);
agate:
joined = patients.join(doctors, 'doctor', 'id')
FULL OUTER JOIN
SQL:
SELECT * FROM patient FULL OUTER JOIN doctor ON (patient.doctor = doctor.id);
agate:
joined = patients.join(doctors, 'doctor', 'id', full_outer=True)
GROUP BY
agate's Table.group_by() works slightly different than SQLs. It does not require an aggregate function. Instead it returns TableSet. To see how to perform the equivalent of a SQL aggregate, see below.
doctors = patients.group_by('doctor')
You can group by two or more columns by chaining the command.
doctors_by_state = patients.group_by('state').group_by('doctor')
HAVING
agate's TableSet.having() works very similar to SQL's keyword of the same name.
doctors = patients.group_by('doctor')
popular_doctors = doctors.having([
('patient_count', Count())
], lambda t: t['patient_count'] > 100)
This filters to only those doctors whose table includes at least 100 results. Can add as many aggregations as you want to the list and each will be available, by name in the test function you pass.
For example, here we filter to popular doctors with more an average review of at least three stars:
doctors = patients.group_by('doctor')
popular_doctors = doctors.having([
('patient_count', Count()),
('average_stars', Average('stars'))
], lambda t: t['patient_count'] > 100 and t['average_stars'] >= 3)
Chain commands together
SQL:
SELECT state, total FROM table WHERE LOWER(state) = 'california' ORDER BY total DESC;
agate:
new_table = table \
.select(['state', 'total']) \
.where(lambda row: row['state'].lower() == 'california') \
.order_by('total', reverse=True)
NOTE:
Aggregate functions
SQL:
SELECT mean(age), median(age) FROM patients GROUP BY doctor;
agate:
doctors = patients.group_by('doctor')
patient_ages = doctors.aggregate([
('patient_count', agate.Count()),
('age_mean', agate.Mean('age')),
('age_median', agate.Median('age'))
])
The resulting table will have four columns: doctor, patient_count, age_mean and age_median.
Emulate Excel
One of agate's most powerful assets is that instead of a wimpy "formula" language, you have the entire Python language at your disposal. Here are examples of how to translate a few common Excel operations.
Simple formulas
If you need to simulate a simple Excel formula you can use the Formula class to apply an arbitrary function.
Excel:
=($A1 + $B1) / $C1
agate:
def f(row):
return (row['a'] + row['b']) / row['c'] new_table = table.compute([
('new_column', agate.Formula(agate.Number(), f)) ])
If this still isn't enough flexibility, you can also create your own subclass of Computation.
SUM
number_type = agate.Number() def five_year_total(row):
columns = ('2009', '2010', '2011', '2012', '2013')
return sum(tuple(row[c] for c in columns)] formula = agate.Formula(number_type, five_year_total) new_table = table.compute([
('five_year_total', formula) ])
TRIM
new_table = table.compute([
('name_stripped', agate.Formula(text_type, lambda r: r['name'].strip())) ])
CONCATENATE
new_table = table.compute([
('full_name', agate.Formula(text_type, lambda r: '%(first_name)s %(middle_name)s %(last_name)s' % r)) ])
IF
new_table = table.compute([
('mvp_candidate', agate.Formula(boolean_type, lambda r: row['batting_average'] > 0.3)) ])
VLOOKUP
There are two ways to get the equivalent of Excel's VLOOKUP with agate. If your lookup source is another agate Table, then you'll want to use the Table.join() method:
new_table = mvp_table.join(states, 'state_abbr')
This will add all the columns from the states table to the mvp_table, where their state_abbr columns match.
If your lookup source is a Python dictionary or some other object you can implement the lookup using a Formula computation:
states = {
'AL': 'Alabama',
'AK': 'Alaska',
'AZ': 'Arizona',
...
}
new_table = table.compute([
('mvp_candidate', agate.Formula(text_type, lambda r: states[row['state_abbr']]))
])
Pivot tables as cross-tabulations
Pivot tables in Excel implement a tremendous range of functionality. Agate divides this functionality into a few different methods.
If what you want is to convert rows to columns to create a "crosstab", then you'll want to use the Table.pivot() method:
jobs_by_state_and_year = employees.pivot('state', 'year')
This will generate a table with a row for each value in the state column and a column for each value in the year column. The intersecting cells will contains the counts grouped by state and year. You can pass the aggregation keyword to aggregate some other value, such as Mean or Median.
Pivot tables as summaries
On the other hand, if what you want is to summarize your table with descriptive statistics, then you'll want to use Table.group_by() and TableSet.aggregate():
jobs = employees.group_by('job_title')
summary = jobs.aggregate([
('employee_count', agate.Count()),
('salary_mean', agate.Mean('salary')),
('salary_median', agate.Median('salary'))
])
The resulting summary table will have four columns: job_title, employee_count, salary_mean and salary_median.
You may also want to look at the Table.normalize() and Table.denormalize() methods for examples of functionality frequently accomplished with Excel's pivot tables.
Emulate R
c()
Agate's Table.select() and Table.exclude() are the equivalent of R's c for selecting columns.
R:
selected <- data[c("last_name", "first_name", "age")]
excluded <- data[!c("last_name", "first_name", "age")]
agate:
selected = table.select(['last_name', 'first_name', 'age']) excluded = table.exclude(['last_name', 'first_name', 'age'])
subset
Agate's Table.where() is the equivalent of R's subset.
R:
newdata <- subset(data, age >= 20 | age < 10)
agate:
new_table = table.where(lambda row: row['age'] >= 20 or row['age'] < 10)
order
Agate's Table.order_by() is the equivalent of R's order.
R:
newdata <- employees[order(last_name),]
agate:
new_table = employees.order_by('last_name')
merge
Agate's Table.join() is the equivalent of R's merge.
R:
joined <- merge(employees, states, by="usps")
agate:
joined = employees.join(states, 'usps')
rbind
Agate's Table.merge() is the equivalent of R's rbind.
R:
merged <- rbind(first_year, second_year)
agate:
merged = agate.Table.merge(first_year, second_year)
aggregate
Agate's Table.group_by() and TableSet.aggregate() can be used to recreate the functionality of R's aggregate.
R:
aggregates = aggregate(employees$salary, list(job = employees$job), mean)
agate:
jobs = employees.group_by('job')
aggregates = jobs.aggregate([
('mean', agate.Mean('salary'))
])
melt
Agate's Table.normalize() is the equivalent of R's melt.
R:
melt(employees, id=c("last_name", "first_name"))
agate:
employees.normalize(['last_name', 'first_name'])
cast
Agate's Table.denormalize() is the equivalent of R's cast.
R:
melted = melt(employees, id=c("name"))
casted = cast(melted, name~variable, mean)
agate:
normalized = employees.normalize(['name'])
denormalized = normalized.denormalize('name')
Emulate underscore.js
filter
agate's Table.where() functions exactly like Underscore's filter.
new_table = table.where(lambda row: row['state'] == 'Texas')
reject
To simulate Underscore's reject, simply negate the return value of the function you pass into agate's Table.where().
new_table = table.where(lambda row: not (row['state'] == 'Texas'))
find
agate's Table.find() works exactly like Underscore's find.
row = table.find(lambda row: row['state'].startswith('T'))
any
The Any aggregation works like Underscore's any.
true_or_false = table.aggregate(Any('salaries', lambda d: d > 100000))
You can also use Table.where() to filter to columns that pass the truth test.
all
The All aggregation works like Underscore's all.
true_or_false = table.aggregate(All('salaries', lambda d: d > 100000))
Homogenize rows
Fill in missing rows in a series. This can be used, for instance, to add rows for missing years in a time series.
Create rows for missing values
We can insert a default row for each value that is missing in a table from a given sequence of values.
Starting with a table like this, we can fill in rows for all missing years:
| year | female_count | male_count |
| 1997 | 2 | 1 |
| 2000 | 4 | 3 |
| 2002 | 4 | 5 |
| 2003 | 1 | 2 |
key = 'year' expected_values = (1997, 1998, 1999, 2000, 2001, 2002, 2003) # Your default row should specify column values not in `key` default_row = (0, 0) new_table = table.homogenize(key, expected_values, default_row)
The result will be:
| year | female_count | male_count |
| 1997 | 2 | 1 |
| 1998 | 0 | 0 |
| 1999 | 0 | 0 |
| 2000 | 4 | 3 |
| 2001 | 0 | 0 |
| 2002 | 4 | 5 |
| 2003 | 1 | 2 |
Create dynamic rows based on missing values
We can also specify new row values with a value-generating function:
key = 'year' expected_values = (1997, 1998, 1999, 2000, 2001, 2002, 2003) # If default row is a function, it should return a full row def default_row(missing_value):
return (missing_value, missing_value-1997, missing_value-1997) new_table = table.homogenize(key, expected_values, default_row)
The new table will be:
| year | female_count | male_count |
| 1997 | 2 | 1 |
| 1998 | 1 | 1 |
| 1999 | 2 | 2 |
| 2000 | 4 | 3 |
| 2001 | 4 | 4 |
| 2002 | 4 | 5 |
| 2003 | 1 | 2 |
Renaming and reordering columns
Rename columns
You can rename the columns in a table by using the Table.rename() method and specifying the new column names as an array or dictionary mapping old column names to new ones.
table = Table(rows, column_names = ['a', 'b', 'c'])
new_table = table.rename(column_names = ['one', 'two', 'three'])
# or
new_table = table.rename(column_names = {'a': 'one', 'b': 'two', 'c': 'three'})
Reorder columns
You can reorder the columns in a table by using the Table.select() method and specifying the column names in the order you want:
new_table = table.select(['3rd_column_name', '1st_column_name', '2nd_column_name'])
Transform
Pivot by a single column
The Table.pivot() method is a general process for grouping data by row and, optionally, by column, and then calculating some aggregation for each group. Consider the following table:
| name | race | gender | age |
| Joe | white | female | 20 |
| Jane | asian | male | 20 |
| Jill | black | female | 20 |
| Jim | latino | male | 25 |
| Julia | black | female | 25 |
| Joan | asian | female | 25 |
In the very simplest case, this table can be pivoted to count the number occurences of values in a column:
transformed = table.pivot('race')
Result:
| race | pivot |
| white | 1 |
| asian | 2 |
| black | 2 |
| latino | 1 |
Pivot by multiple columns
You can pivot by multiple columns either as additional row-groups, or as intersecting columns. For example, given the table in the previous example:
transformed = table.pivot(['race', 'gender'])
Result:
| race | gender | pivot |
| white | female | 1 |
| asian | male | 1 |
| black | female | 2 |
| latino | male | 1 |
| asian | female | 1 |
For the column, version you would do:
transformed = table.pivot('race', 'gender')
Result:
| race | male | female |
| white | 0 | 1 |
| asian | 1 | 1 |
| black | 0 | 2 |
| latino | 1 | 0 |
Pivot to sum
The default pivot aggregation is Count but you can also supply other operations. For example, to aggregate each group by Sum of their ages:
transformed = table.pivot('race', 'gender', aggregation=agate.Sum('age'))
| race | male | female |
| white | 0 | 20 |
| asian | 20 | 25 |
| black | 0 | 45 |
| latino | 25 | 0 |
Pivot to percent of total
Pivot allows you to apply a Computation to each row of aggregated results prior to returning the table. Use the stringified name of the aggregation as the column argument to your computation:
transformed = table.pivot('race', 'gender', aggregation=agate.Sum('age'), computation=agate.Percent('sum'))
| race | male | female |
| white | 0 | 14.8 |
| asian | 14.8 | 18.4 |
| black | 0 | 33.3 |
| latino | 18.4 | 0 |
Note: actual computed percentages will be much more precise.
It's helpful when constructing these cases to think of all the cells in the pivot table as a single sequence.
Denormalize key/value columns into separate columns
It's common for very large datasets to be distributed in a "normalized" format, such as:
| name | property | value |
| Jane | gender | female |
| Jane | race | black |
| Jane | age | 24 |
| ... | ... | ... |
The Table.denormalize() method can be used to transform the table so that each unique property has its own column.
transformed = table.denormalize('name', 'property', 'value')
Result:
| name | gender | race | age |
| Jane | female | black | 24 |
| Jack | male | white | 35 |
| Joe | male | black | 28 |
Normalize separate columns into key/value columns
Sometimes you have a dataset where each property has its own column, but your analysis would be easier if all properties were stored together. Consider this table:
| name | gender | race | age |
| Jane | female | black | 24 |
| Jack | male | white | 35 |
| Joe | male | black | 28 |
The Table.normalize() method can be used to transform the table so that all the properties and their values share two columns.
transformed = table.normalize('name', ['gender', 'race', 'age'])
Result:
| name | property | value |
| Jane | gender | female |
| Jane | race | black |
| Jane | age | 24 |
| ... | ... | ... |
Locales
agate strives to work equally well for users from all parts of the world. This means properly handling foreign currencies, date formats, etc. To facilitate this, agate makes a hard distinction between your locale and the locale of the data you are working with. This allows you to work seamlessly with data from other countries.
Set your locale
Setting your locale will change how numbers are displayed when you print an agate Table or serialize it to, for example, a CSV file. This works the same as it does for any other Python module. See the locale documentation for details. Changing your locale will not affect how they are parsed from the files you are using. To change how data is parsed see Specify locale of numbers.
Specify locale of numbers
To correctly parse numbers from non-US locales, you must pass a locale parameter to the Number constructor. For example, to parse Dutch numbers (which use a period to separate thousands and a comma to separate fractions):
dutch_numbers = agate.Number(locale='nl_NL')
column_names = ['city', 'population']
column_types = [text_type, dutch_numbers]
table = agate.Table.from_csv('dutch_cities.csv', columns)
Rank
There are many ways to rank a sequence of values. agate strives to find a balance between simple, intuitive ranking and flexibility when you need it.
Competition rank
The basic rank supported by agate is standard "competition ranking". In this model the values [3, 4, 4, 5] would be ranked [1, 2, 2, 4]. You can apply competition ranking using the Rank computation:
new_table = table.compute([
('rank', agate.Rank('value')) ])
Rank descending
Descending competition ranking is specified using the reverse argument.
new_table = table.compute([
('rank', agate.Rank('value', reverse=True)) ])
Rank change
You can compute the change from one rank to another by combining the Rank and Change computations:
new_table = table.compute([
('rank2014', agate.Rank('value2014')),
('rank2015', agate.Rank('value2015')) ]) new_table2 = new_table.compute([
('rank_change', agate.Change('rank2014', 'rank2015')) ])
Percentile rank
"Percentile rank" is a bit of a misnomer. Really, this is the percentile in which each value in a column is located. This column can be computed for your data using the PercentileRank computation:
new_table = table.compute([
('percentile_rank', agate.PercentileRank('value')) ])
Note that there is no entirely standard method for computing percentiles. The percentiles computed in this manner may not agree precisely with those generated by other software. See the Percentiles class documentation for implementation details.
Charts
Agate offers two kinds of built in charting: very simple text bar charts and SVG charting via leather. Both are intended for efficiently exploring data, rather than producing publication-ready charts.
Text-based bar chart
agate has a builtin text-based bar-chart generator:
table.limit(10).print_bars('State Name', 'TOTAL', width=80)
State Name TOTAL ALABAMA 19,582 ▓░░░░░░░░░░░░░ ALASKA 2,705 ▓░░ ARIZONA 46,743 ▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ARKANSAS 7,932 ▓░░░░░ CALIFORNIA 76,639 ▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ COLORADO 21,485 ▓░░░░░░░░░░░░░░░ CONNECTICUT 4,350 ▓░░░ DELAWARE 1,904 ▓░ DIST. OF COLUMBIA 2,185 ▓░ FLORIDA 59,519 ▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
+-------------+------------+------------+-------------+
0 20,000 40,000 60,000 80,000
Text-based histogram
Table.print_bars() can be combined with Table.pivot() or Table.bins() to produce fast histograms:
table.bins('TOTAL', start=0, end=100000).print_bars('TOTAL', width=80)
TOTAL Count [0 - 10,000) 30 ▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ [10,000 - 20,000) 12 ▓░░░░░░░░░░░░░░░░░░░░░░ [20,000 - 30,000) 7 ▓░░░░░░░░░░░░░ [30,000 - 40,000) 1 ▓░░ [40,000 - 50,000) 2 ▓░░░░ [50,000 - 60,000) 1 ▓░░ [60,000 - 70,000) 1 ▓░░ [70,000 - 80,000) 1 ▓░░ [80,000 - 90,000) 0 ▓ [90,000 - 100,000] 0 ▓
+-------------+------------+------------+-------------+
0.0 7.5 15.0 22.5 30.0
SVG bar chart
table.limit(10).bar_chart('State Name', 'TOTAL', 'docs/images/bar_chart.svg')
SVG column chart
table.limit(10).column_chart('State Name', 'TOTAL', 'docs/images/column_chart.svg')
SVG line chart
by_year_exonerated = table.group_by('exonerated')
counts = by_year_exonerated.aggregate([
('count', agate.Count())
])
counts.order_by('exonerated').line_chart('exonerated', 'count', 'docs/images/line_chart.svg')
SVG dots chart
table.scatterplot('exonerated', 'age', 'docs/images/dots_chart.svg')
SVG lattice chart
top_crimes = table.group_by('crime').having([
('count', agate.Count())
], lambda t: t['count'] > 100)
by_year = top_crimes.group_by('exonerated')
counts = by_year.aggregate([
('count', agate.Count())
])
by_crime = counts.group_by('crime')
by_crime.order_by('exonerated').line_chart('exonerated', 'count', 'docs/images/lattice.svg')
Using matplotlib
If you need to make more complex charts, you can always use agate with matplotlib.
Here is an example of how you might generate a line chart:
import pylab
pylab.plot(table.columns['homeruns'], table.columns['wins'])
pylab.xlabel('Homeruns')
pylab.ylabel('Wins')
pylab.title('How homeruns correlate to wins')
pylab.show()
Lookup
Generate new columns by mapping existing data to common lookup tables.
CPI deflation
The agate-lookup extension adds a lookup method to agate's Table class.
Starting with a table that looks like this:
| year | cost |
| 1995 | 2.0 |
| 1997 | 2.2 |
| 1996 | 2.3 |
| 2003 | 4.0 |
| 2007 | 5.0 |
| 2005 | 6.0 |
We can map the year column to its annual CPI index in one lookup call.
import agatelookup
agatelookup.patch()
join_year_cpi = table.lookup('year', 'cpi')
The return table will have now have a new column:
| year | cost | cpi |
| 1995 | 2.0 | 152.383 |
| 1997 | 2.2 | 160.525 |
| 1996 | 2.3 | 156.858 |
| 2003 | 4.0 | 184.000 |
| 2007 | 5.0 | 207.344 |
| 2005 | 6.0 | 195.267 |
A simple computation tacked on to this lookup can then get the 2015 equivalent values of each cost:
cpi_2015 = Decimal(216.909) def cpi_adjust_2015(row):
return (row['cost'] * (cpi_2015 / row['cpi'])).quantize(Decimal('0.01')) cost_2015 = join_year_cpi.compute([
('cost_2015', agate.Formula(agate.Number(), cpi_adjust_2015)) ])
And the final table will look like this:
| year | cost | cpi | cost_2015 |
| 1995 | 2.0 | 152.383 | 2.85 |
| 1997 | 2.2 | 160.525 | 2.97 |
| 1996 | 2.3 | 156.858 | 3.18 |
| 2003 | 4.0 | 184.000 | 4.72 |
| 2007 | 5.0 | 207.344 | 5.23 |
| 2005 | 6.0 | 195.267 | 6.66 |
Basics
- Creating tables from various data types
- Saving data to various data types
- Removing columns from a table
- Filtering rows of data
- Sorting rows of data
- Searching through a table
- Standardize names and values
- Calculating statistics
- Computing new columns
- Handling dates and times
Coming from other tools
- SQL
- Excel
- R
- Underscore.js
- Pandas (coming soon!)
Advanced techniques
- Filling missing rows in a dataset
- Renaming and reordering columns
- Transforming data (pivot/normalize/denormalize)
- Setting your locale and working with foreign data
- Ranking a sequence of data
- Creating simple charts
- Mapping columns to common lookup tables
Have a common use case that isn't covered? Please submit an issue on the GitHub repository.
EXTENSIONS
The core agate library is designed rely on as few dependencies as possible. However, in the real world you're often going to want to interface with more specialized tools, or with other formats, such as SQL or Excel.
Using extensions
agate support's plugin-style extensions using a monkey-patching pattern. Libraries can be created that add new methods onto Table and TableSet. For example, agate-sql adds the ability to read and write tables from a SQL database:
import agate
import agatesql
# After calling patch the from_sql and to_sql methods are now part of the Table class
table = agate.Table.from_sql('postgresql:///database', 'input_table')
table.to_sql('postgresql:///database', 'output_table')
List of extensions
Here is a list of agate extensions that are known to be actively maintained:
- agate-sql: Read and write tables in SQL databases
- agate-stats: Additional statistical methods
- agate-excel: Read excel tables (xls and xlsx)
- agate-dbf: Read dbf tables (from shapefiles)
- agate-remote: Read from remote files
- agate-lookup: Instantly join to hosted lookup tables.
Writing your own extensions
Writing your own extensions is straightforward. Create a function that acts as your "patch" and then dynamically add it to Table or TableSet.
import agate def new_method(self):
print('I do something to a Table when you call me.') agate.Table.new_method = new_method
You can also create new classmethods:
def new_class_method(cls):
print('I make Tables when you call me.') agate.Table.new_method = classmethod(new_method)
These methods can now be called on Table class in your code:
>>> import agate >>> import myextension >>> table = agate.Table(rows, column_names, column_types) >>> table.new_method() 'I do something to a Table when you call me.' >>> agate.Table.new_class_method() 'I make Tables when you call me.'
The same pattern also works for adding methods to TableSet.
API
Table
Properties
Creating
Saving
Basic processing
Calculating new data
Advanced processing
Previewing
Charting
Detailed list
TableSet
Properties
Creating
Saving
Processing
Previewing
Charting
Table Proxy Methods
Detailed list
Columns and rows
Data types
Supported types
Detailed list
Type inference
Aggregations
Basic aggregations
Statistical aggregations
Text aggregations
Detailed list
Computations
Mathematical computations
Detailed list
CSV reader and writer
Agate contains CSV readers and writers that are intended to be used as a drop-in replacement for csv. These versions add unicode support for Python 2 and several other minor features.
Agate methods will use these version automatically. If you would like to use them in your own code, you can import them, like this:
from agate import csv
Due to nuanced differences between the versions, these classes are implemented seperately for Python 2 and Python 3. The documentation for both versions is provided below, but only the one for your version of Python is imported with the above code.
Python 3
| agate.csv_py3.reader | A replacement for Python's csv.reader() that uses csv_py3.Reader. |
| agate.csv_py3.writer | A replacement for Python's csv.writer() that uses csv_py3.Writer. |
| agate.csv_py3.Reader | A wrapper around Python 3's builtin csv.reader(). |
| agate.csv_py3.Writer | A wrapper around Python 3's builtin csv.writer(). |
| agate.csv_py3.DictReader | A wrapper around Python 3's builtin csv.DictReader. |
| agate.csv_py3.DictWriter | A wrapper around Python 3's builtin csv.DictWriter. |
Python 2
Python 3 details
- agate.csv_py3.reader(*args, **kwargs)
- A replacement for Python's csv.reader() that uses csv_py3.Reader.
- agate.csv_py3.writer(*args, **kwargs)
- A replacement for Python's csv.writer() that uses csv_py3.Writer.
- class agate.csv_py3.Reader(f, field_size_limit=None, line_numbers=False, header=True, **kwargs)
- A wrapper around Python 3's builtin csv.reader().
- class agate.csv_py3.Writer(f, line_numbers=False, **kwargs)
- A wrapper around Python 3's builtin csv.writer().
- class agate.csv_py3.DictReader(f, fieldnames=None, restkey=None, restval=None, dialect='excel', *args, **kwds)
- A wrapper around Python 3's builtin csv.DictReader.
- class agate.csv_py3.DictWriter(f, fieldnames, line_numbers=False, **kwargs)
- A wrapper around Python 3's builtin csv.DictWriter.
Python 2 details
Fixed-width reader
Agate contains a fixed-width file reader that is designed to work like Python's csv.
These readers work with CSV-formatted schemas, such as those maintained at wireservice/ffs.
Detailed list
Miscellaneous
Exceptions
Warnings
Unit testing helpers
Config
CONTRIBUTING
agate actively encourages contributions from people of all genders, races, ethnicities, ages, creeds, nationalities, persuasions, alignments, sizes, shapes, and journalistic affiliations. You are welcome here.
We seek contributions from developers and non-developers of all skill levels. We will typically accept bug fixes, documentation updates, and new cookbook recipes with minimal fuss. If you want to work on a larger feature—great! The maintainers will be happy to provide feedback and code review on your implementation.
Before making any changes or additions to agate, please be sure to read about the principles of agate in the About section of the documentation.
Process for documentation
Not a developer? That's fine! As long as you can use git (there are many tutorials) then you can contribute to agate. Please follow this process:
- 1.
- Fork the project on GitHub.
- 2.
- If you don't have a specific task in mind, check out the issue tracker and find a documentation ticket that needs to be done.
- 3.
- Comment on the ticket letting everyone know you're going to be working on it so that nobody duplicates your effort.
- 4.
- Write the documentation. Documentation files live in the docs directory and are in Restructured Text Format.
- 5.
- Add yourself to the AUTHORS file if you aren't already there.
- 6.
- Once your contribution is complete, submit a pull request on GitHub.
- 7.
- Wait for it to either be merged by a maintainer or to receive feedback about what needs to be revised.
- 8.
- Rejoice!
Process for code
Hacker? We'd love to have you hack with us. Please follow this process to make your contribution:
- 1.
- Fork the project on GitHub.
- 2.
- If you don't have a specific task in mind, check out the issue tracker and find a task that needs to be done and is of a scope you can realistically expect to complete in a few days. Don't worry about the priority of the issues at first, but try to choose something you'll enjoy. You're much more likely to finish something to the point it can be merged if it's something you really enjoy hacking on.
- 3.
- If you already have a task you know you want to work on, open a ticket or comment on the existing ticket letting everyone know you're going to be working on it. It's also good practice to provide some general idea of how you plan on resolving the issue so that other developers can make suggestions.
- 4.
- Write tests for the feature you're building. Follow the format of the existing tests in the test directory to see how this works. You can run all the tests with the command pytest.
- 5.
- Write the code. Try to stay consistent with the style and organization of the existing codebase. A good patch won't be refused for stylistic reasons, but large parts of it may be rewritten and nobody wants that.
- 6.
- As you are coding, periodically merge in work from the master branch and verify you haven't broken anything by running the test suite.
- 7.
- Write documentation. This means docstrings on all classes and methods, including parameter explanations. It also means, when relevant, cookbook recipes and updates to the agate user tutorial.
- 8.
- Add yourself to the AUTHORS file if you aren't already there.
- 9.
- Once your contribution is complete, tested, and has documentation, submit a pull request on GitHub.
- 10.
- Wait for it to either be merged by a maintainer or to receive feedback about what needs to be revisited.
- 11.
- Rejoice!
Licensing
To the extent that they care, contributors should keep in mind that the source of agate and therefore of any contributions are licensed under the permissive MIT license. By submitting a patch or pull request you are agreeing to release your code under this license. You will be acknowledged in the AUTHORS list, the commit history and the hearts and minds of journalists everywhere.
RELEASE PROCESS
If substantial changes were made to the code:
- 1.
- Ensure any new modules have been added to setup.py's packages list
- 2.
- Ensure any new public interfaces have been added to the documentation
- 3.
- Ensure TableSet proxy methods have been added for new Table methods
Then:
- 1.
- All tests pass on continuous integration
- 2.
- The changelog is up-to-date and dated
- 3.
- The version number is correct in:
- setup.py
- docs/conf.py
- 4.
- Check for new authors: git log --invert-grep --author='James McKinney'
- 5.
- Run python charts.py to update images in the documentation
- 6.
- Tag the release: git tag -a x.y.z -m 'x.y.z release.'; git push --follow-tags
- 7.
- Upload to PyPI: rm -rf dist; python setup.py sdist bdist_wheel; twine upload dist/*
- 8.
- Build the documentation on ReadTheDocs manually
LICENSE
The MIT License
Copyright (c) 2017 Christopher Groskopf and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
CHANGELOG
1.7.1 - Jan 4, 2023
- •
- Allow parsedatetime 2.6.
1.7.0 - Jan 3, 2023
- Add Python 3.11 support.
- Add Python 3.10 support.
- Drop Python 3.6 support (end-of-life was December 23, 2021).
- Drop Python 2.7 support (end-of-life was January 1, 2020).
1.6.3 - July 15, 2021
- feat: Table.from_csv() accepts a row_limit keyword argument. (#740)
- feat: Table.from_json() accepts an encoding keyword argument. (#734)
- feat: Table.print_html() accepts a max_precision keyword argument, like Table.print_table(). (#753)
- feat: TypeTester accepts a null_values keyword argument, like individual data types. (#745)
- feat: Min, Max and Sum (#735) work with TimeDelta.
- feat: FieldSizeLimitError includes the line number in the error message. (#681)
- feat: csv.Sniffer warns on error while sniffing CSV dialect.
- fix: Table.normalize() works with basic processing methods. (#691)
- fix: Table.homogenize() works with basic processing methods. (#756)
- fix: Table.homogenize() casts compare_values and default_row. (#700)
- fix: Table.homogenize() accepts tuples. (#710)
- fix: TableSet.group_by() accepts input with no rows. (#703)
- fix: TypeTester warns if a column specified by the force argument is not in the table, instead of raising an error. (#747)
- fix: Aggregations return None if all values are None, instead of raising an error. Note that Sum, MaxLength and MaxPrecision continue to return 0 if all values are None. (#706)
- fix: Ensure files are closed when errors occur. (#734)
- build: Make PyICU an optional dependency.
- Drop Python 3.5 support (end-of-life was September 13, 2020).
- Drop Python 3.4 support (end-of-life was March 18, 2019).
1.6.2 - March 10, 2021
- feat: Date.__init__() and DateTime.__init__() accepts a locale keyword argument (e.g. en_US) for parsing formatted dates. (#730)
- feat: Number.cast() casts True to 1 and False to 0. (#733)
- fix: utils.max_precision() ignores infinity when calculating precision. (#726)
- fix: Date.cast() catches OverflowError when type testing. (#720)
- Included examples in Python package. (#716)
1.6.1 - March 11, 2018
- feat: Table.to_json() can use Decimal as keys. (#696)
- fix: Date.cast() and DateTime.cast() no longer parse non-date strings that contain date sub-strings as dates. (#705)
- docs: Link to tutorial now uses version through Sphinx to avoid bad links on future releases. (#682)
1.6.0 - February 28, 2017
This update should not cause any breaking changes, however, it is being classified as major release because the dependency on awesome-slugify, which is licensed with GPLv3, has been replaced with python-slugify, which is licensed with MIT.
- Suppress warning from babel about Time Zone expressions on Python 3.6. (#665)
- Reimplemented slugify with python-slugify instead of awesome-slugify. (#660)
- Slugify renaming of duplicate values is now consistent with Table.init(). (#615)
1.5.5 - December 29, 2016
- Added a "full outer join" example to the SQL section of the cookbook. (#658)
- Warnings are now more explicit when column names are missing. (#652)
- Date.cast() will no longer parse strings like 05_leslie3d_base as dates. (#653)
- Text.cast() will no longer strip leading or trailing whitespace. (#654)
- Fixed 'NoneType' object has no attribute 'groupdict' error in TimeDelta.cast(). (#656)
1.5.4 - December 27, 2016
- Cleaned up handling of warnings in tests.
- Blank column names are not treated as unspecified (letter names will be generated).
1.5.3 - December 26, 2016
This is a minor release that adds one feature: sequential joins (by row number). It also fixes several small bugs blocking a downstream release of csvkit.
- Fixed empty Table column names would be intialized as list instead of tuple.
- Table.join() can now join by row numbers—a sequential join.
- Table.join() now supports full outer joins via the full_outer keyword.
- Table.join() can now accept column indicies instead of column names.
- Table.from_csv() now buffers input files to prevent issues with using STDIN as an input.
1.5.2 - December 24, 2016
- •
- Improved handling of non-ascii encoded CSV files under Python 2.
1.5.1 - December 23, 2016
This is a minor release fixing several small bugs that were blocking a downstream release of csvkit.
- Documented differing behavior of MaxLength under Python 2. (#649)
- agate is now tested against Python 3.6. (#650)
- Fix bug when MaxLength was called on an all-null column.
- Update extensions documentation to match new API. (#645)
- Fix bug in Change and PercentChange where 0 values could cause None to be returned incorrectly.
1.5.0 - November 16, 2016
This release adds SVG charting via the leather charting library. Charts methods have been added for both Table and TableSet. (The latter create lattice plots.) See the revised tutorial and new cookbook entries for examples. Leather is still an early library. Please report any bugs.
Also in this release are a Slugify computation and a variety of small fixes and improvements.
The complete list of changes is as follows:
- Remove support for monkey-patching of extensions. (#594)
- TableSet methods which proxy Table methods now appear in the API docs. (#640)
- Any and All aggregations no longer behave differently for boolean data. (#636)
- Any and All aggregations now accept a single value as a test argument, in addition to a function.
- Any and All aggregations now require a test argument.
- Tables rendered by Table.print_table() are now GitHub Flavored Markdown (GFM) compatible. (#626)
- The agate tutorial has been converted to a Jupyter Notebook.
- Table now supports len as a proxy for len(table.rows).
- Simple SVG charting is now integrated via leather.
- Added First computation. (#634)
- Table.print_table() now has a max_precision argument to limit Number precision. (#544)
- Slug computation now accepts an array of column names to merge. (#617)
- Cookbook: standardize column values with Slugify computation. (#613)
- Cookbook: slugify/standardize row and column names. (#612)
- Fixed condition that prevents integer row names to allow bools in Table.__init__(). (#627)
- PercentChange is now null-safe, returns None for null values. (#623)
- Table can now be iterated, yielding Row instances. (Previously it was necessarily to iterate table.rows.)
1.4.0 - May 26, 2016
This release adds several new features, fixes numerous small bug-fixes, and improves performance for common use cases. There are some minor breaking changes, but few user are likely to encounter them. The most important changes in this release are:
- 1.
- There is now a TableSet.having() method, which behaves similarly to SQL's HAVING keyword.
- 2.
- Table.from_csv() is much faster. In particular, the type inference routines for parsing numbers have been optimized.
- 3.
- The Table.compute() method now accepts a replace keyword which allows new columns to replace existing columns "in place."" (As with all agate operations, a new table is still created.)
- 4.
- There is now a Slug computation which can be used to compute a column of slugs. The Table.rename() method has also added new options for slugifying column and row names.
The complete list of changes is as follows:
- Added a deprecation warning for patch methods. New extensions should not use it. (#594)
- Added Slug computation (#466)
- Added slug_columns and slug_rows arguments to Table.rename(). (#466)
- Added utils.slugify() to standardize a sequence of strings. (#466)
- Table.__init__() now prints row and column on CastError. (#593)
- Fix null sorting in Table.order_by() when ordering by multiple columns. (#607)
- Implemented configuration system.
- Fixed bug in Table.print_bars() when value_column contains None (#608)
- Table.print_table() now restricts header on max_column_width. (#605)
- Cookbook: filling gaps in a dataset with Table.homogenize. (#538)
- Reduced memory usage and improved performance of Table.from_csv().
- Table.from_csv() no longer accepts a sequence of row ids for skip_lines.
- Number.cast() is now three times as fast.
- Number now accepts group_symbol, decimal_symbol and currency_symbols arguments. (#224)
- Tutorial: clean up state data under computing columns (#570)
- Table.__init__() now explicitly checks that row_names are not ints. (#322)
- Cookbook: CPI deflation, agate-lookup. (#559)
- Table.bins() now includes values outside start or end in computed column_names. (#596)
- Fixed bug in Table.bins() where start or end arguments were ignored when specified alone. (#599)
- Table.compute() now accepts a replace argument that allows columns to be overwritten. (#597)
- Table.from_fixed() now creates an agate table from a fixed-width file. (#358)
- fixed now implements a general-purpose fixed-width file reader. (#358)
- TypeTester now correctly parses negative currency values as Number. (#595)
- Cookbook: removing a column (select and exclude). (#592)
- Cookbook: overriding specific column types. (#591)
- TableSet now has a TableSet._fork() method used internally for deriving new tables.
- Added an example of SQL's HAVING to the cookbook.
- Table.aggregate() interface has been revised to be more similar to TableSet.aggregate().
- TableSet.having() is now implemented. (#587)
- There is now a better error when a forced column name does not exist. (#591)
- Arguments to Table.print_html() now mirror Table.print_table().
1.3.1 - March 30, 2016
The major feature of this release is new API documentation. Several minor features and bug fixes are also included. There are no major breaking changes in this release.
Internally, the agate codebase has been reorganized to be more modular, but this should be invisible to most users.
- The MaxLength aggregation now returns a Decimal object. (#574)
- Fixed an edge case where datetimes were parsed as dates. (#568)
- Fixed column alignment in tutorial tables. (#572)
- Table.print_table() now defaults to printing 20 rows and 6 columns. (#589)
- Added Eli Murray to AUTHORS.
- Table.__init__() now accepts a dict to specify partial column types. (#580)
- Table.from_csv() now accepts a skip_lines argument. (#581)
- Moved every Aggregation and Computation into their own modules. (#565)
- Column and Row are now importable from agate.
- Completely reorgnized the API documentation.
- Moved unit tests into modules to match new code organization.
- Moved major Table and TableSet methods into their own modules.
- Fixed bug when using non-unicode encodings with Table.from_csv(). (#560)
- Table.homogenize() now accepts an array of values as compare values if key is a single column name. (#539)
1.3.0 - February 28, 2016
This version implements several new features and includes two major breaking changes.
Please take note of the following breaking changes:
- 1.
- There is no longer a Length aggregation. The more obvious Count is now used instead.
- 2.
- Agate's replacements for Python's CSV reader and writer have been moved to the agate.csv namespace. To use as a drop-in replacement: from agate import csv.
The major new features in this release are primarly related to transforming (reshaping) tables. They are:
- 1.
- Table.normalize() for converting columns to rows.
- 2.
- Table.denormalize() for converting rows to columns.
- 3.
- Table.pivot() for generating "crosstabs".
- 4.
- Table.homogenize() for filling gaps in data series.
Please see the following complete list of changes for a variety of other bug fixes and improvements.
- Moved CSV reader/writer to agate.csv namespace.
- Added numerous new examples to the R section of the cookbook. (#529-#535)
- Updated Excel cookbook entry for pivot tables. (#536)
- Updated Excel cookbook entry for VLOOKUP. (#537)
- Fix number rendering in Table.print_table() on Windows. (#528)
- Added cookbook examples of using Table.pivot() to count frequency/distribution.
- Table.bins() now has smarter output column names. (#524)
- Table.bins() is now a wrapper around pivot. (#522)
- Table.counts() has been removed. Use Table.pivot() instead. (#508)
- Count can now count non-null values in a column.
- Removed Length. Count now works without any arguments. (#520)
- Table.pivot() implemented. (#495)
- Table.denormalize() implemented. (#493)
- Added columns argument to Table.join(). (#479)
- Cookbook: Custom statistics/agate.Summary
- Added Kevin Schaul to AUTHORS.
- Quantiles.locate() now correctly returns Decimal instances. (#509)
- Cookbook: Filter for distinct values of a column (#498)
- Added Column.values_distinct() (#498)
- Cookbook: Fuzzy phonetic search example. (#207)
- Cookbook: Create a table from a remote file. (#473)
- Added printable argument to Table.print_bars() to use only printable characters. (#500)
- MappedSequence now throws an explicit error on __setitem__. (#499)
- Added require_match argument to Table.join(). (#480)
- Cookbook: Rename columns in a table. (#469)
- Table.normalize() implemented. (#487)
- Added Percent computation with example in Cookbook. (#490)
- Added Ben Welsh to AUTHORS.
- Table.__init__() now throws a warning if auto-generated columns are used. (#483)
- Table.__init__() no longer fails on duplicate columns. Instead it renames them and throws a warning. (#484)
- Table.merge() now takes a column_names argument to specify columns included in new table. (#481)
- Table.select() now accepts a single column name as a key.
- Table.exclude() now accepts a single column name as a key.
- Added Table.homogenize() to find gaps in a table and fill them with default rows. (#407)
- Table.distinct() now accepts sequences of column names as a key.
- Table.join() now accepts sequences of column names as either a left or right key. (#475)
- Table.order_by() now accepts a sequence of column names as a key.
- Table.distinct() now accepts a sequence of column names as a key.
- Table.join() now accepts a sequence of column names as either a left or right key. (#475)
- Cookbook: Create a table from a DBF file. (#472)
- Cookbook: Create a table from an Excel spreadsheet.
- Added explicit error if a filename is passed to the Table constructor. (#438)
1.2.2 - February 5, 2016
This release adds several minor features. The only breaking change is that default column names will now be lowercase instead of uppercase. If you depended on these names in your scripts you will need to update them accordingly.
- TypeTester no longer takes a locale argument. Use types instead.
- TypeTester now takes a types argument that is a list of possible types to test. (#461)
- Null conversion can now be disabled for Text by passing cast_nulls=False. (#460)
- Default column names are now lowercase letters instead of uppercase. (#464)
- Table.merge() can now merge tables with different columns or columns in a different order. (#465)
- MappedSequence.get() will no longer raise KeyError if a default is not provided. (#467)
- Number can now test/cast the long type on Python 2.
1.2.1 - February 5, 2016
This release implements several new features and bug fixes. There are no significant breaking changes.
Special thanks to Neil Bedi for his extensive contributions to this release.
- Added a max_column_width argument to Table.print_table(). Defaults to 20. (#442)
- Table.from_json() now defers most functionality to Table.from_object().
- Implemented Table.from_object() for parsing JSON-like Python objects.
- Fixed a bug that prevented Table.order_by() on empty table. (#454)
- Table.from_json() and TableSet.from_json() now have column_types as an optional argument. (#451)
- csv.Reader now has line_numbers and header options to add column for line numbers (#447)
- Renamed maxfieldsize to field_size_limit in csv.Reader for consistency (#447)
- Table.from_csv() now has a sniff_limit option to use csv.Sniffer (#444)
- csv.Sniffer implemented. (#444)
- Table.__init__() no longer fails on empty rows. (#445)
- TableSet.from_json() implemented. (#373)
- Fixed a bug that breaks TypeTester.run() on variable row length. (#440)
- Added TableSet.__str__() to display Table keys and row counts. (#418)
- Fixed a bug that incorrectly checked for column_types equivalence in Table.merge() and TableSet.__init__(). (#435)
- TableSet.merge() now has the ability to specify grouping factors with group, group_name and group_type. (#406)
- Table can now be constructed with None for some column names. Those columns will receive letter names. (#432)
- Slightly changed the parsing of dates and datetimes from strings.
- Numbers are now written to CSV without extra zeros after the decimal point. (#429)
- Made it possible for datetime.date instances to be considered valid DateTime inputs. (#427)
- Changed preference order in type testing so Date is preferred to DateTime.
- Removed float_precision argument from Number. (#428)
- AgateTestCase is now available as agate.AgateTestCase. (#426)
- TableSet.to_json() now has an indent option for use with nested.
- TableSet.to_json() now has a nested option for writing a single, nested JSON file. (#417)
- TestCase.assertRowNames() and TestCase.assertColumnNames() now validate the row and column instance keys.
- Fixed a bug that prevented Table.rename() from renaming column names in Row instances. (#423)
1.2.0 - January 18, 2016
This version introduces one breaking change, which is only relevant if you are using custom Computation subclasses.
- 1.
- Computation has been modified so that Computation.run() takes a Table instance as its argument, rather than a single row. It must return a sequence of values to use for a new column. In addition, the Computation._prepare() method has been renamed to Computation.validate() to more accurately describe it's function. These changes were made to facilitate computing moving averages, streaks and other values that require data for the full column.
- Existing Aggregation subclasses have been updated to use Aggregate.validate(). (This brings a noticeable performance boost.)
- Aggregation now has a Aggregation.validate() method that functions identically to Computation.validate(). (#421)
- Change.validate() now correctly raises DataTypeError.
- Added a SimpleMovingAverage implementation to the cookbook's examples of custom Computation classes.
- Computation._prepare() has been renamed to Computation.validate().
- Computation.run() now takes a Table instance as an argument. (#415)
- Fix a bug in Python 2 where printing a table could raise decimal.InvalidOperation. (#412)
- Fix Rank so it returns Decimal. (#411)
- Added Taurus Olson to AUTHORS.
- Printing a table will now print the table's structure.
- Table.print_structure() implemented. (#393)
- Added Geoffrey Hing to AUTHORS.
- Table.print_html() implemented. (#408)
- Instances of Date and DateTime can now be pickled. (#362)
- AgateTestCase is available as agate.testcase.AgateTestCase for extensions to use. (#384)
- Table.exclude() implemented. Opposite of Table.select(). (#388)
- Table.merge() now accepts a row_names argument. (#403)
- Formula now automatically casts computed values to specified data type unless cast is set to False. (#398)
- Added Neil Bedi to AUTHORS.
- Table.rename() is implemented. (#389)
- TableSet.to_json() is implemented. (#374)
- Table.to_csv() and Table.to_json() will now create the target directory if it does not exist. (#392)
- Boolean will now correctly cast numerical 0 and 1. (#386)
- Table.merge() now consistently maps column names to rows. (#402)
1.1.0 - November 4, 2015
This version of agate introduces three major changes.
- 1.
- Table, Table.from_csv() and TableSet.from_csv() now all take column_names and column_types as separate arguments instead of as a sequence of tuples. This was done to enable more flexible type inference and to streamline the API.
- 2.
- The interfaces for TableSet.aggregate() and Table.compute() have been changed. In both cases the new column name now comes first. Aggregations have also been modified so that the input column name is an argument to the aggregation class, rather than a third element in the tuple.
- 3.
- This version drops support for Python 2.6. Testing and bug-fixing for this version was taking substantial time with no evidence that anyone was actually using it. Also, multiple dependencies claim to not support 2.6, even though agate's tests were passing.
- DataType's now have DataType.csvify() and DataType.jsonify() methods for serializing native values.
- Added a dependency on isodate for handling ISO8601 formatted dates. (#233)
- Aggregation results are no longer cached. (#378)
- Removed Column.aggregate method. Use Table.aggregate() instead. (#378)
- Added Table.aggregate() for aggregating single column results. (#378)
- Aggregation subclasses now take column names as their first argument. (#378)
- TableSet.aggregate() and Table.compute() now take the new column name as the first argument. (#378)
- Remove support for Python 2.6.
- Table.to_json() is implemented. (#345)
- Table.from_json() is implemented. (#344, #347)
- Date and DateTime type testing now takes specified format into account. (#361)
- Number data type now takes a float_precision argument.
- Number data types now work with native float values. (#370)
- TypeTester can now validate Python native types (not just strings). (#367)
- TypeTester can now be used with the Table constructor, not just Table.from_csv(). (#350)
- Table, Table.from_csv() and TableSet.from_csv() now take column_names and column_types as separate parameters. (#350)
- DEFAULT_NULL_VALUES (the list of strings that mean null) is now importable from agate.
- Table.from_csv() and Table.to_csv() are now unicode-safe without separately importing csvkit.
- agate can now be used as a drop-in replacement for Python's csv module.
- Migrated csvkit's unicode CSV reading/writing support into agate. (#354)
1.0.1 - October 29, 2015
- TypeTester now takes a "limit" arg that restricts how many rows it tests. (#332)
- Table.from_csv now supports CSVs with neither headers nor manual column names.
- Tables can now be created with automatically generated column names. (#331)
- File handles passed to Table.to_csv are now left open. (#330)
- Added Table.print_csv method. (#307, #339)
- Fixed stripping currency symbols when casting Numbers from strings. (#333)
- Fixed two major join issues. (#336)
1.0.0 - October 22, 2015
- Table.from_csv now defaults to TypeTester() if column_info is not provided. (#324)
- New tutorial section: "Navigating table data" (#315)
- 100% test coverage reached. (#312)
- NullCalculationError is now a warning instead of an error. (#311)
- TableSet is now a subclass of MappedSequence.
- Rows and Columns are now subclasses of MappedSequence.
- Add Column.values_without_nulls_sorted().
- Column.get_data_without_nulls() is now Column.values_without_nulls().
- Column.get_data_sorted() is now Column.values_sorted().
- Column.get_data() is now Column.values().
- Columns can now be sliced.
- Columns can now be indexed by row name. (#301)
- Added support for Python 3.5.
- Row objects can now be sliced. (#303)
- Replaced RowSequence and ColumnSequence with MappedSequence.
- Replace RowDoesNotExistError with KeyError.
- Replaced ColumnDoesNotExistError with IndexError.
- Removed unnecessary custom RowIterator, ColumnIterator and CellIterator.
- Performance improvements for Table "forks". (where, limit, etc)
- TableSet keys are now converted to row names during aggregation. (#291)
- Removed fancy __repr__ implementations. Use __str__ instead. (#290)
- Rows can now be accessed by name as well as index. (#282)
- Added row_names argument to Table constructor. (#282)
- Removed Row.table and Row.index properties. (#287)
- Columns can now be accessed by index as well as name. (#281)
- Added column name and type validation to Table constructor. (#285)
- Table now supports variable-length rows during construction. (#39)
- aggregations.Summary implemented for generic aggregations. (#181)
- Fix TableSet.key_type being lost after proxying Table methods. (#278)
- Massive performance increases for joins. (#277)
- Added join benchmark. (#73)
0.11.0 - October 6, 2015
- Implemented __repr__ for Table, TableSet, Column and Row. (#261)
- Row.index property added.
- Column constructor no longer takes a data_type argument.
- Column.index and Column.name properties added.
- Table.counts implemented. (#271)
- Table.bins implemented. (#267, #227)
- Table.join now raises ColumnDoesNotExistError. (#264)
- Table.select now raises ColumnDoesNotExistError.
- computations.ZScores moved into agate-stats.
- computations.Rank cmp argument renamed comparer.
- aggregations.MaxPrecision added. (#265)
- Table.print_bars added.
- Table.pretty_print renamed Table.print_table.
- Reimplement Table method proxying via @allow_tableset_proxy decorator. (#263)
- Add agate-stats references to docs.
- Move stdev_outliers, mad_outliers and pearson_correlation into agate-stats. (#260)
- Prevent issues with applying patches multiple times. (#258)
0.10.0 - September 22, 2015
- Add reverse and cmp arguments to Rank computation. (#248)
- Document how to use agate-sql to read/write SQL tables. (#238, #241)
- Document how to write extensions.
- Add monkeypatching extensibility pattern via utils.Patchable.
- Reversed order of argument pairs for Table.compute. (#249)
- TableSet.merge method can be used to ungroup data. (#253)
- Columns with identical names are now suffixed "2" after a Table.join.
- Duplicate key columns are no longer included in the result of a Table.join. (#250)
- Table.join right_key no longer necessary if identical to left_key. (#254)
- Table.inner_join is now more. Use inner keyword to Table.join.
- Table.left_outer_join is now Table.join.
0.9.0 - September 14, 2015
- Add many missing unit tests. Up to 99% coverage.
- Add property accessors for TableSet.key_name and TableSet.key_type. (#247)
- Table.rows and Table.columns are now behind properties. (#247)
- Column.data_type is now a property. (#247)
- Table[Set].get_column_types() is now the Table[Set].column_types property. (#247)
- Table[Set].get_column_names() is now the Table[Set].column_names property. (#247)
- Table.pretty_print now displays consistent decimal places for each Number column.
- Discrete data types (Number, Date etc) are now right-aligned in Table.pretty_print.
- Implement aggregation result caching. (#245)
- Reimplement Percentiles, Quartiles, etc as aggregations.
- UnsupportedAggregationError is now used to disable TableSet aggregations.
- Replaced several exceptions with more general DataTypeError.
- Column type information can now be accessed as Column.data_type.
- Eliminated Column subclasses. Restructured around DataType classes.
- Table.merge implemented. (#9)
- Cookbook: guess column types. (#230)
- Fix issue where all group keys were being cast to text. (#235)
- Table.group_by will now default key_type to the type of the grouping column. (#234)
- Add Matt Riggott to AUTHORS. (#231)
- Support file-like objects in Table.to_csv and Table.from_csv. (#229)
- Fix bug when applying multiple computations with Table.compute.
0.8.0 - September 9, 2015
- Cookbook: dealing with locales. (#220)
- Cookbook: working with dates and times.
- Add timezone support to DateTimeType.
- Use pytimeparse instead of python-dateutil. (#221)
- Handle percents and currency symbols when casting numbers. (#217)
- Table.format is now Table.pretty_print. (#223)
- Rename TextType to Text, NumberType to Number, etc.
- Rename agate.ColumnType to agate.DataType (#216)
- Rename agate.column_types to agate.data_types.
- Implement locale support for number parsing. (#116)
- Cookbook: ranking. (#110)
- Cookbook: date change and date ranking. (#113)
- Add tests for unicode support. (#138)
- Fix computations.ZScores calculation. (#123)
- Differentiate sample and population variance and stdev. (#208)
- Support for overriding column inference with "force".
- Competition ranking implemented as default. (#125)
- TypeTester: robust type inference. (#210)
0.7.0 - September 3, 2015
- Cookbook: USA Today diversity index.
- Cookbook: filter to top x%. (#47)
- Cookbook: fuzzy string search example. (#176)
- Values to coerce to true/false can now be overridden for BooleanType.
- Values to coerce to null can now be overridden for all ColumnType subclasses. (#206)
- Add key_type argument to TableSet and Table.group_by. (#205)
- Nested TableSet's and multi-dimensional aggregates. (#204)
- TableSet.aggregate will now use key_name as the group column name. (#203)
- Added key_name argument to TableSet and Table.group_by.
- Added Length aggregation and removed count from TableSet.aggregate output. (#203)
- Fix error messages for RowDoesNotExistError and ColumnDoesNotExistError.
0.6.0 - September 1, 2015
- Fix missing package definition in setup.py.
- Split Analysis off into the proof library.
- Change computation now works with DateType, DateTimeType and TimeDeltaType. (#159)
- TimeDeltaType and TimeDeltaColumn implemented.
- NonNullAggregation class removed.
- Some private Column methods made public. (#183)
- Rename agate.aggegators to agate.aggregations.
- TableSet.to_csv implemented. (#195)
- TableSet.from_csv implemented. (#194)
- Table.to_csv implemented (#169)
- Table.from_csv implemented. (#168)
- Added Table.format method for pretty-printing tables. (#191)
- Analysis class now implements a caching workflow. (#171)
0.5.0 - August 28, 2015
- Table now takes (column_name, column_type) pairs. (#180)
- Renamed the library to agate. (#179)
- Results of common column operations are now cached using a common memoize decorator. (#162)
- ated support for Python version 3.2.
- Added support for Python wheel packaging. (#127)
- Add PercentileRank computation and usage example to cookbook. (#152)
- Add indexed change example to cookbook. (#151)
- Add annual change example to cookbook. (#150)
- Column.aggregate now invokes Aggregations.
- Column.any, NumberColumn.sum, etc. converted to Aggregations.
- Implement Aggregation and subclasses. (#155)
- Move ColumnType subclasses and ColumnOperation subclasses into new modules.
- Table.percent_change, Table.rank and Table.zscores reimplemented as Computers.
- Computer implemented. Table.compute reimplemented. (#147)
- NumberColumn.iqr (inter-quartile range) implemented. (#102)
- Remove Column.counts as it is not the best way.
- Implement ColumnOperation and subclasses.
- Table.aggregate migrated to TableSet.aggregate.
- Table.group_by now supports grouping by a key function. (#140)
- NumberColumn.deciles implemented.
- NumberColumn.quintiles implemented. (#46)
- NumberColumn.quartiles implemented. (#45)
- Added robust test case for NumberColumn.percentiles. (#129)
- NumberColumn.percentiles reimplemented using new method. (#130)
- Reorganized and modularized column implementations.
- Table.group_by now returns a TableSet.
- Implement TableSet object. (#141)
0.4.0 - September 27, 2014
- Upgrade to python-dateutil 2.2. (#134)
- Wrote introductory tutorial. (#133)
- Reorganize documentation (#132)
- Add John Heasly to AUTHORS.
- Implement percentile. (#35)
- no_null_computations now accepts args. (#122)
- Table.z_scores implemented. (#123)
- DateTimeColumn implemented. (#23)
- Column.counts now returns dict instead of Table. (#109)
- ColumnType.create_column renamed _create_column. (#118)
- Added Mick O'Brien to AUTHORS. (#121)
- Pearson correlation implemented. (#103)
0.3.0
- DateType.date_format implemented. (#112)
- Create ColumnType classes to simplify data parsing.
- DateColumn implemented. (#7)
- Cookbook: Excel pivot tables. (#41)
- Cookbook: statistics, including outlier detection. (#82)
- Cookbook: emulating Underscore's any and all. (#107)
- Parameter documention for method parameters. (#108)
- Table.rank now accepts a column name or key function.
- Optionally use cdecimal for improved performance. (#106)
- Smart naming of aggregate columns.
- Duplicate columns names are now an error. (#92)
- BooleanColumn implemented. (#6)
- TextColumn.max_length implemented. (#95)
- Table.find implemented. (#14)
- Better error handling in Table.__init__. (#38)
- Collapse IntColumn and FloatColumn into NumberColumn. (#64)
- Table.mad_outliers implemented. (#93)
- Column.mad implemented. (#93)
- Table.stdev_outliers implemented. (#86)
- Table.group_by implemented. (#3)
- Cookbook: emulating R. (#81)
- Table.left_outer_join now accepts column names or key functions. (#80)
- Table.inner_join now accepts column names or key functions. (#80)
- Table.distinct now accepts a column name or key function. (#80)
- Table.order_by now accepts a column name or key function. (#80)
- Table.rank implemented. (#15)
- Reached 100% test coverage. (#76)
- Tests for Column._cast methods. (#20)
- Table.distinct implemented. (#83)
- Use assertSequenceEqual in tests. (#84)
- Docs: features section. (#87)
- Cookbook: emulating SQL. (#79)
- Table.left_outer_join implemented. (#11)
- Table.inner_join implemented. (#11)
0.2.0
- Python 3.2, 3.3 and 3.4 support. (#52)
- Documented supported platforms.
- Cookbook: csvkit. (#36)
- Cookbook: glob syntax. (#28)
- Cookbook: filter to values in range. (#30)
- RowDoesNotExistError implemented. (#70)
- ColumnDoesNotExistError implemented. (#71)
- Cookbook: percent change. (#67)
- Cookbook: sampleing. (#59)
- Cookbook: random sort order. (#68)
- Eliminate Table.get_data.
- Use tuples everywhere. (#66)
- Fixes for Python 2.6 compatibility. (#53)
- Cookbook: multi-column sorting. (#13)
- Cookbook: simple sorting.
- Destructive Table ops now deepcopy row data. (#63)
- Non-destructive Table ops now share row data. (#63)
- Table.sort_by now accepts a function. (#65)
- Cookbook: pygal.
- Cookbook: Matplotlib.
- Cookbook: VLOOKUP. (#40)
- Cookbook: Excel formulas. (#44)
- Cookbook: Rounding to two decimal places. (#49)
- Better repr for Column and Row. (#56)
- Cookbook: Filter by regex. (#27)
- Cookbook: Underscore filter & reject. (#57)
- Table.limit implemented. (#58)
- Cookbook: writing a CSV. (#51)
- Kill Table.filter and Table.reject. (#55)
- Column.map removed. (#43)
- Column instance & data caching implemented. (#42)
- Table.select implemented. (#32)
- Eliminate repeated column index lookups. (#25)
- Precise DecimalColumn tests.
- Use Decimal type everywhere internally.
- FloatColumn converted to DecimalColumn. (#17)
- Added Eric Sagara to AUTHORS. (#48)
- NumberColumn.variance implemented. (#1)
- Cookbook: loading a CSV. (#37)
- Table.percent_change implemented. (#16)
- Table.compute implemented. (#31)
- Table.filter and Table.reject now take funcs. (#24)
- Column.count implemented. (#12)
- Column.counts implemented. (#8)
- Column.all implemented. (#5)
- Column.any implemented. (#4)
- Added Jeff Larson to AUTHORS. (#18)
- NumberColumn.mode implmented. (#18)
0.1.0
- •
- Initial prototype
SHOW ME DOCS
- About - why you should use agate and the principles that guide its development
- Install - how to install for users and developers
- Tutorial - a step-by-step guide to start using agate
- Cookbook - sample code showing how to accomplish dozens of common tasks, including comparisons to SQL, R, etc.
- Extensions - a list of libraries that extend agate functionality and how to build your own
- API - technical documentation for every agate feature
- Changelog - a record of every change made to agate for each release
SHOW ME CODE
import agate
purchases = agate.Table.from_csv('examples/realdata/ks_1033_data.csv')
by_county = purchases.group_by('county')
totals = by_county.aggregate([
('county_cost', agate.Sum('total_cost'))
])
totals = totals.order_by('county_cost', reverse=True)
totals.limit(10).print_bars('county', 'county_cost', width=80)
county county_cost SEDGWICK 977,174.45 ▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ COFFEY 691,749.03 ▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ MONTGOMERY 447,581.20 ▓░░░░░░░░░░░░░░░░░░░░░░░░░ JOHNSON 420,628.00 ▓░░░░░░░░░░░░░░░░░░░░░░░░ SALINE 245,450.24 ▓░░░░░░░░░░░░░░ FINNEY 171,862.20 ▓░░░░░░░░░░ BROWN 145,254.96 ▓░░░░░░░░ KIOWA 97,974.00 ▓░░░░░ WILSON 74,747.10 ▓░░░░ FORD 70,780.00 ▓░░░░
+-------------+-------------+-------------+-------------+
0 250,000 500,000 750,000 1,000,000
This example, along with detailed comments, are available as a Jupyter notebook.
JOIN US
- Contributing - guidance for developers who want to contribute to agate
- Release process - the process for maintainers to publish new releases
- License - a copy of the MIT open source license covering agate
WHO WE ARE
agate is made by a community. The following individuals have contributed code, documentation, or expertise to agate:
- Christopher Groskopf
- Jeff Larson
- Eric Sagara
- John Heasly
- Mick O'Brien
- David Eads
- Nikhil Sonnad
- Matt Riggott
- Tyler Fisher
- William P. Davis
- Ryan Murphy
- Raphael Deem
- Robin Linderborg
- Chris Keller
- Neil Bedi
- Geoffrey Hing
- Taurus Olson
- Danny Page
- James McKinney
- Tony Papousek
- Mila Frerichs
- Paul Fitzpatrick
- Ben Welsh
- Kevin Schaul
- sandyp
- Lexie Heinle
- Will Skora
- Joe Germuska
- Eli Murray
- Derek Swingley
- Or Sharir
- Anthony DeBarros
- Apoorv Anand
- Ghislain Antony Vaillant
- Neil MartinsenBurrell
- Aliaksei Urbanski
- Forest Gregg
- Robert Schütz
- Wouter de Vries
- Kartik Agaram
- Loïc Corbasson
- Danny Sepler
- brian-from-quantrocket
- mathdesc
- Tim Gates
INDICES AND TABLES
- Index
- Module Index
- Search Page
AUTHOR
unknown
COPYRIGHT
2023, Christopher Groskopf
| April 24, 2023 | 1.7.1 |
