Skip to content

Commit

Permalink
Implement TimeDeltaType and TimeDeltaColumn. Closes #159.
Browse files Browse the repository at this point in the history
  • Loading branch information
onyxfish committed Aug 31, 2015
1 parent 573c3c6 commit 94a002c
Show file tree
Hide file tree
Showing 17 changed files with 254 additions and 69 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
0.6.0
-----

* 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.
Expand Down
18 changes: 10 additions & 8 deletions agate/aggregations.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

from agate.column_types import BooleanType, NumberType
from agate.columns import BooleanColumn, DateColumn, DateTimeColumn, NumberColumn, TextColumn
from agate.exceptions import NullComputationError, UnsupportedAggregationError
from agate.exceptions import NullCalculationError, UnsupportedAggregationError

class Aggregation(object): #pragma: no cover
"""
Expand Down Expand Up @@ -138,6 +138,8 @@ def get_aggregate_column_type(self, column):
elif isinstance(column, NumberColumn):
return NumberType()

raise UnsupportedAggregationError(self, column)

def run(self, column):
"""
:returns: :class:`datetime.date`
Expand Down Expand Up @@ -204,7 +206,7 @@ def run(self, column):
raise UnsupportedAggregationError(self, column)

if column.has_nulls():
raise NullComputationError
raise NullCalculationError

return column.mean()

Expand All @@ -226,7 +228,7 @@ def run(self, column):
raise UnsupportedAggregationError(self, column)

if column.has_nulls():
raise NullComputationError
raise NullCalculationError

return column.median()

Expand All @@ -245,7 +247,7 @@ def run(self, column):
raise UnsupportedAggregationError(self, column)

if column.has_nulls():
raise NullComputationError
raise NullCalculationError

data = column.get_data()
state = defaultdict(int)
Expand All @@ -270,7 +272,7 @@ def run(self, column):
raise UnsupportedAggregationError(self, column)

if column.has_nulls():
raise NullComputationError
raise NullCalculationError

percentiles = column.percentiles()

Expand All @@ -291,7 +293,7 @@ def run(self, column):
raise UnsupportedAggregationError(self, column)

if column.has_nulls():
raise NullComputationError
raise NullCalculationError

return column.variance()

Expand All @@ -310,7 +312,7 @@ def run(self, column):
raise UnsupportedAggregationError(self, column)

if column.has_nulls():
raise NullComputationError
raise NullCalculationError

return column.variance().sqrt()

Expand Down Expand Up @@ -342,7 +344,7 @@ def run(self, column):
raise UnsupportedAggregationError(self, column)

if column.has_nulls():
raise NullComputationError
raise NullCalculationError

data = column.get_data_sorted()
m = column.percentiles()[50]
Expand Down
30 changes: 30 additions & 0 deletions agate/column_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from decimal import Decimal, InvalidOperation

from dateutil.parser import parse
import pytimeparse
import six

from agate.exceptions import CastError
Expand Down Expand Up @@ -140,6 +141,35 @@ def _create_column(self, table, index):

return DateTimeColumn(table, index)

class TimeDeltaType(ColumnType):
"""
Column type for :class:`datetime.timedelta`.
"""
def cast(self, d):
"""
Cast a single value to :class:`datetime.timedelta`.
:param d: A value to cast.
:returns: :class:`datetime.timedelta` or :code:`None`
"""
if isinstance(d, datetime.timedelta) or d is None:
return d

if isinstance(d, six.string_types):
d = d.strip()

if d.lower() in NULL_VALUES:
return None

seconds = pytimeparse.parse(d)

return datetime.timedelta(seconds=seconds)

def _create_column(self, table, index):
from agate.columns import TimeDeltaColumn

return TimeDeltaColumn(table, index)

class NumberType(ColumnType):
"""
Column type for :class:`NumberColumn`.
Expand Down
1 change: 1 addition & 0 deletions agate/columns/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@
from agate.columns.date_time import *
from agate.columns.number import *
from agate.columns.text import *
from agate.columns.time_delta import *
2 changes: 0 additions & 2 deletions agate/columns/date.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#!/usr/bin/env python

import datetime

from agate.columns.base import Column

class DateColumn(Column):
Expand Down
7 changes: 1 addition & 6 deletions agate/columns/date_time.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
#!/usr/bin/env python

import datetime

from dateutil.parser import parse

from agate.columns.base import Column

class DateTimeColumn(Column):
"""
A column containing :class:`datetime.datetime` data.
"""
def __init__(self, *args, **kwargs):
pass
pass
15 changes: 6 additions & 9 deletions agate/columns/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import six

from agate.columns.base import Column
from agate.exceptions import NullComputationError
from agate.exceptions import NullCalculationError
from agate.utils import memoize

class NumberColumn(Column):
Expand All @@ -15,9 +15,6 @@ class NumberColumn(Column):
All data is represented by the :class:`decimal.Decimal` class.
"""
def __init__(self, *args, **kwargs):
super(NumberColumn, self).__init__(*args, **kwargs)

@memoize
def sum(self):
"""
Expand Down Expand Up @@ -63,10 +60,10 @@ def percentiles(self):
Compute percentiles for this column of data.
:returns: :class:`Percentiles`.
:raises: :exc:`.NullComputationError`
:raises: :exc:`.NullCalculationError`
"""
if self.has_nulls():
raise NullComputationError
raise NullCalculationError

return Percentiles(self)

Expand All @@ -76,7 +73,7 @@ def quartiles(self):
Compute quartiles for this column of data.
:returns: :class:`Quartiles`.
:raises: :exc:`.NullComputationError`
:raises: :exc:`.NullCalculationError`
"""
return Quartiles(self.percentiles())

Expand All @@ -86,7 +83,7 @@ def quintiles(self):
Compute quintiles for this column of data.
:returns: :class:`Quintiles`.
:raises: :exc:`.NullComputationError`
:raises: :exc:`.NullCalculationError`
"""
return Quintiles(self.percentiles())

Expand All @@ -96,7 +93,7 @@ def deciles(self):
Compute deciles for this column of data.
:returns: :class:`Deciles`.
:raises: :exc:`.NullComputationError`
:raises: :exc:`.NullCalculationError`
"""
return Deciles(self.percentiles())

Expand Down
2 changes: 0 additions & 2 deletions agate/columns/text.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#!/usr/bin/env python

import six

from agate.columns.base import Column

class TextColumn(Column):
Expand Down
9 changes: 9 additions & 0 deletions agate/columns/time_delta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env python

from agate.columns.base import Column

class TimeDeltaColumn(Column):
"""
A column containing :class:`datetime.timedelta` data.
"""
pass
79 changes: 58 additions & 21 deletions agate/computations.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@
"""

from agate.aggregations import Mean, StDev
from agate.columns import NumberColumn
from agate.column_types import NumberType
from agate.columns import *
from agate.column_types import *
from agate.exceptions import UnsupportedComputationError
from agate.utils import NullOrder

class Computation(object): #pragma: no cover
"""
Base class for row-wise computations on a :class:`.Table`.
"""
def get_computed_column_type(self):
def get_computed_column_type(self, table):
"""
Returns an instantiated :class:`.ColumnType` which will be appended to
the table.
Expand Down Expand Up @@ -55,7 +55,7 @@ def __init__(self, column_type, func):
self._column_type = column_type
self._func = func

def get_computed_column_type(self):
def get_computed_column_type(self, table):
return self._column_type

def run(self, row):
Expand All @@ -65,33 +65,70 @@ class Change(Computation):
"""
Computes change between two columns.
"""
def __init__(self, before_column, after_column):
self._before_column = before_column
self._after_column = after_column
def __init__(self, before_column_name, after_column_name):
self._before_column_name = before_column_name
self._after_column_name = after_column_name

def get_computed_column_type(self):
return NumberType()
def _validate(self, table):
before_column = table.columns[self._before_column_name]
after_column = table.columns[self._after_column_name]

def prepare(self, table):
before_column = table.columns[self._before_column]
for column_type in (NumberColumn, DateColumn, DateTimeColumn, TimeDeltaColumn):
if isinstance(before_column, column_type):
if not isinstance(after_column, column_type):
raise ValueError('Specified columns must be of the same type')

if not isinstance(before_column, NumberColumn):
raise UnsupportedComputationError(self, before_column)
if before_column.has_nulls():
raise NullCalculationError

after_column = table.columns[self._after_column]
if after_column.has_nulls():
raise NullCalculationError

if not isinstance(after_column, NumberColumn):
raise UnsupportedComputationError(self, after_column)
return (before_column, after_column)

raise UnsupportedComputationError(self, before_column)

def get_computed_column_type(self, table):
before_column, after_column = self._validate(table)

if isinstance(before_column, DateColumn):
return TimeDeltaType()
elif isinstance(before_column, DateTimeColumn):
return TimeDeltaType()
elif isinstance(before_column, TimeDeltaColumn):
return TimeDeltaType()
elif isinstance(before_column, NumberColumn):
return NumberType()

def prepare(self, table):
self._validate(table)

def run(self, row):
return row[self._after_column] - row[self._before_column]
return row[self._after_column_name] - row[self._before_column_name]

class PercentChange(Change):
class PercentChange(Computation):
"""
Computes percent change between two columns.
"""
def __init__(self, before_column_name, after_column_name):
self._before_column_name = before_column_name
self._after_column_name = after_column_name

def get_computed_column_type(self, table):
return NumberType()

def prepare(self, table):
before_column = table.columns[self._before_column_name]
after_column = table.columns[self._after_column_name]

if not isinstance(before_column, NumberColumn):
raise UnsupportedComputationError(self, before_column)

if not isinstance(after_column, NumberColumn):
raise UnsupportedComputationError(self, after_column)

def run(self, row):
return (row[self._after_column] - row[self._before_column]) / row[self._before_column] * 100
return (row[self._after_column_name] - row[self._before_column_name]) / row[self._before_column_name] * 100

class ZScores(Computation):
"""
Expand All @@ -100,7 +137,7 @@ class ZScores(Computation):
def __init__(self, column_name):
self._column_name = column_name

def get_computed_column_type(self):
def get_computed_column_type(self, table):
return NumberType()

def prepare(self, table):
Expand All @@ -125,7 +162,7 @@ class Rank(Computation):
def __init__(self, column_name):
self._column_name = column_name

def get_computed_column_type(self):
def get_computed_column_type(self, table):
return NumberType()

def _null_handler(self, k):
Expand Down
6 changes: 3 additions & 3 deletions agate/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
This module contains various exceptions raised by agate.
"""

class NullComputationError(Exception): # pragma: no cover
class NullCalculationError(Exception): # pragma: no cover
"""
Exception raised if an computation which can not logically
account for null values is attempted on a Column containing
Exception raised if a calculation which can not logically
account for null values is attempted on a :class:`Column containing
nulls.
"""
pass
Expand Down
2 changes: 1 addition & 1 deletion agate/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ def compute(self, computations):
raise ValueError('The second element in pair must be a Computation instance.')

column_names.append(name)
column_types.append(computation.get_computed_column_type())
column_types.append(computation.get_computed_column_type(self))

computation.prepare(self)

Expand Down

0 comments on commit 94a002c

Please sign in to comment.