Repository: ibis-project/ibis

Issue:
Add stddev* aggregate functions



Discussion:

Resolving patch:
diff --git a/ibis/expr/api.py b/ibis/expr/api.py
--- a/ibis/expr/api.py
+++ b/ibis/expr/api.py
@@ -716,8 +716,6 @@ def lead(arg, offset=None, default=None):
 last = _unary_op('last', _ops.LastValue)
 rank = _unary_op('rank', _ops.MinRank)
 dense_rank = _unary_op('dense_rank', _ops.DenseRank)
-cumsum = _unary_op('cumsum', _ops.CumulativeSum)
-cummean = _unary_op('cummean', _ops.CumulativeMean)
 cummin = _unary_op('cummin', _ops.CumulativeMin)
 cummax = _unary_op('cummax', _ops.CumulativeMax)
 
@@ -1026,14 +1024,56 @@ def convert_base(arg, from_base, to_base):
 
 
 mean = _agg_function('mean', _ops.Mean, True)
+cummean = _unary_op('cummean', _ops.CumulativeMean)
+
 sum = _agg_function('sum', _ops.Sum, True)
+cumsum = _unary_op('cumsum', _ops.CumulativeSum)
+
+
+def std(arg, where=None, how='sample'):
+    """
+    Compute standard deviation of numeric array
+
+    Parameters
+    ----------
+    how : {'sample', 'pop'}, default 'sample'
+
+    Returns
+    -------
+    stdev : double scalar
+    """
+    expr = _ops.StandardDev(arg, where, how).to_expr()
+    expr = expr.name('std')
+    return expr
+
+
+def variance(arg, where=None, how='sample'):
+    """
+    Compute standard deviation of numeric array
+
+    Parameters
+    ----------
+    how : {'sample', 'pop'}, default 'sample'
+
+    Returns
+    -------
+    stdev : double scalar
+    """
+    expr = _ops.Variance(arg, where, how).to_expr()
+    expr = expr.name('var')
+    return expr
 
 
 _numeric_array_methods = dict(
     mean=mean,
+    cummean=cummean,
+
     sum=sum,
     cumsum=cumsum,
-    cummean=cummean,
+
+    std=std,
+    var=variance,
+
     bucket=bucket,
     histogram=histogram,
     summary=_numeric_summary,
diff --git a/ibis/expr/operations.py b/ibis/expr/operations.py
--- a/ibis/expr/operations.py
+++ b/ibis/expr/operations.py
@@ -651,6 +651,22 @@ class Mean(Reduction):
     output_type = rules.scalar_output(_mean_output_type)
 
 
+class VarianceBase(Reduction):
+
+    input_type = [rules.array, boolean(name='where', optional=True),
+                  rules.string_options(['sample', 'pop'],
+                                       name='how', optional=True)]
+    output_type = rules.scalar_output(_mean_output_type)
+
+
+class StandardDev(VarianceBase):
+    pass
+
+
+class Variance(VarianceBase):
+    pass
+
+
 def _decimal_scalar_ctor(precision, scale):
     out_type = dt.Decimal(precision, scale)
     return ir.DecimalScalar._make_constructor(out_type)
diff --git a/ibis/sql/exprs.py b/ibis/sql/exprs.py
--- a/ibis/sql/exprs.py
+++ b/ibis/sql/exprs.py
@@ -272,19 +272,36 @@ def _unary_op(func_name):
     return _fixed_arity_call(func_name, 1)
 
 
+def _reduction_format(translator, func_name, arg, where):
+    if where is not None:
+        case = where.ifelse(arg, ibis.NA)
+        arg = translator.translate(case)
+    else:
+        arg = translator.translate(arg)
+
+    return '{0!s}({1!s})'.format(func_name, arg)
+
+
 def _reduction(func_name):
     def formatter(translator, expr):
         op = expr.op()
 
-        arg, where = op.args
+        # HACK: support trailing arguments
+        arg, where = op.args[:2]
 
-        if where is not None:
-            case = where.ifelse(arg, ibis.NA)
-            arg = translator.translate(case)
-        else:
-            arg = translator.translate(arg)
+        return _reduction_format(translator, func_name, arg, where)
+    return formatter
 
-        return '{0!s}({1!s})'.format(func_name, arg)
+
+def _variance_like(func_name):
+    func_names = {
+        'sample': func_name,
+        'pop': '{0}_pop'.format(func_name)
+    }
+
+    def formatter(translator, expr):
+        arg, where, how = expr.op().args
+        return _reduction_format(translator, func_names[how], arg, where)
     return formatter
 
 
@@ -933,6 +950,10 @@ def _not_implemented(translator, expr):
     ops.Sum: _reduction('sum'),
     ops.Max: _reduction('max'),
     ops.Min: _reduction('min'),
+
+    ops.StandardDev: _variance_like('stddev'),
+    ops.Variance: _variance_like('variance'),
+
     ops.GroupConcat: _fixed_arity_call('group_concat', 2),
 
     ops.Count: _reduction('count'),


Test patch:
diff --git a/ibis/impala/tests/test_exprs.py b/ibis/impala/tests/test_exprs.py
--- a/ibis/impala/tests/test_exprs.py
+++ b/ibis/impala/tests/test_exprs.py
@@ -481,6 +481,11 @@ def test_aggregations(self):
             d.approx_median(),
             s.group_concat(),
 
+            d.std(),
+            d.std(how='pop'),
+            d.var(),
+            d.var(how='pop'),
+
             table.bool_col.any(),
             table.bool_col.notany(),
             -table.bool_col.any(),
@@ -494,6 +499,8 @@ def test_aggregations(self):
             d.mean(where=cond),
             d.min(where=cond),
             d.max(where=cond),
+            d.std(where=cond),
+            d.var(where=cond),
         ]
 
         agg_exprs = [expr.name('e%d' % i)
diff --git a/ibis/sql/tests/test_exprs.py b/ibis/sql/tests/test_exprs.py
--- a/ibis/sql/tests/test_exprs.py
+++ b/ibis/sql/tests/test_exprs.py
@@ -386,6 +386,10 @@ def test_reduction_where(self):
             (c.mean(where=cond), tmp.format('avg')),
             (c.max(where=cond), tmp.format('max')),
             (c.min(where=cond), tmp.format('min')),
+            (c.std(where=cond), tmp.format('stddev')),
+            (c.std(where=cond, how='pop'), tmp.format('stddev_pop')),
+            (c.var(where=cond), tmp.format('variance')),
+            (c.var(where=cond, how='pop'), tmp.format('variance_pop')),
         ]
         self._check_expr_cases(cases)