Repository: pwwang/datar (language: python)

Issue:
`f.a.mean()` not applied to grouped data
```python
>>> from datar.all import *
>>> df = tibble(g=[1,1,2,2], a=[3,4,5,6])
>>> df >> group_by(f.g) >> mutate(b=f.a.mean())
        g       a         b
  <int64> <int64> <float64>
0       1       3       3.5
1       1       4       3.5
2       2       5       3.5
3       2       6       3.5

[Groups: g (n=2)]
>>> # expected
>>> df >> group_by(f.g) >> mutate(b=mean(f.a))
        g       a         b
  <int64> <int64> <float64>
0       1       3       3.5
1       1       4       3.5
2       2       5       5.5
3       2       6       5.5

[Groups: g (n=2)]
```


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e8d34970..06ed2ca2 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -22,6 +22,7 @@ jobs:
           python -m pip install pylint
           python -m pip install poetry
           poetry install -v
+          poetry run pip install scipy
       # - name: Run pylint
       #   run: pylint datar
       - name: Test with pytest
diff --git a/datar/__init__.py b/datar/__init__.py
index 9b6151fd..f019a4e4 100644
--- a/datar/__init__.py
+++ b/datar/__init__.py
@@ -4,4 +4,4 @@
 from .core import frame_format_patch as _
 from .core.defaults import f
 
-__version__ = '0.1.1'
+__version__ = '0.2.0'
diff --git a/datar/all.py b/datar/all.py
index 415a84e4..12fd1c7e 100644
--- a/datar/all.py
+++ b/datar/all.py
@@ -1,11 +1,38 @@
 """Import all constants, verbs and functions"""
-# pylint: disable=redefined-builtin, wildcard-import, unused-wildcard-import
-# pylint: disable=unused-import
+# pylint: disable=wildcard-import, unused-wildcard-import
+# pylint: disable=unused-import, reimported, invalid-name
 from . import f
+from .base import _no_warn as _
 from .base import *
+from .base import _warn as _
 from .utils import *
 from .stats import *
+from .dplyr import _no_warn as _
 from .dplyr import *
+from .dplyr import _warn as _
 from .tidyr import *
 from .tibble import *
 from .datar import *
+
+from . import base as _base, dplyr as _dplyr
+from .core.warn_builtin_names import warn_builtin_names as _warn_builtin_names
+
+_builtin_names = dict(
+    filter=_dplyr.dfilter,
+    slice=_dplyr.dslice,
+    min=_base.arithmetic,
+    max=_base.arithmetic,
+    sum=_base.arithmetic,
+    abs=_base.arithmetic,
+    round=_base.arithmetic,
+    all=_base.testing,
+    any=_base.testing,
+    re=_base.complex
+)
+__all__ = [name for name in locals() if not name.startswith('_')]
+
+for name in _builtin_names:
+    # let __getattr__ handles the builtins
+    del locals()[name]
+
+__getattr__ = _warn_builtin_names(**_builtin_names)
diff --git a/datar/base/__init__.py b/datar/base/__init__.py
index 16711352..412f740f 100644
--- a/datar/base/__init__.py
+++ b/datar/base/__init__.py
@@ -1,28 +1,85 @@
 """Public APIs from this submodule"""
-# pylint: disable=unused-import,redefined-builtin
+# pylint: disable=unused-import
 from .constants import (
-    NA, NaN, TRUE, FALSE, NULL, pi, Inf, letters, LETTERS,
-    NA_character_,
-    NA_compex_,
-    NA_integer_,
-    NA_real_
+    pi, Inf, letters, LETTERS, month_abb, month_name
 )
-from ..core.options import options, getOption, options_context
+from ..core.options import options, get_option, options_context
 from .verbs import (
     colnames, rownames, dim, nrow, ncol, diag, t, names,
-    intersect, union, setdiff, setequal, duplicated, cov
-)
-from .funcs import (
-    as_date, as_character, as_double, as_factor, as_categorical,
-    as_int, as_integer, as_logical, as_bool, as_numeric,
-    c, ceiling, floor, cummax, cummin, cumprod, cumsum, cut, sample,
-    is_categorical, is_character, is_double, is_factor, is_float,
-    is_int, is_int64, is_integer, is_na, is_numeric,
-    as_int64, unique, Im, Re, is_in, is_element, length, lengths,
-    seq_along, seq_len, seq, abs, round, sqrt, rev, which,
-    droplevels, levels, sin, cos, identity, expandgrid, all, any
-)
-# plain functions
-from .funcs import factor, rep, context
+    intersect, union, setdiff, setequal, duplicated
+)
+from .funs import (
+    cut, identity, expandgrid, data_context
+)
+from .arithmetic import mean, median, pmin, pmax, var, ceiling, floor, sqrt, cov
+from .bessel import bessel_i, bessel_j, bessel_k, bessel_y
+from .casting import as_integer, as_float, as_double, as_int, as_numeric
+from .complex import im, mod, arg, conj, is_complex, as_complex
+from .cum import cumsum, cumprod, cummin, cummax
+from .date import as_date
+from .factor import (
+    factor, droplevels, levels,
+    is_factor, is_categorical, as_factor, as_categorical
+)
+from .logical import (
+    TRUE, FALSE,
+    is_true, is_false,
+    is_bool, is_logical,
+    as_bool, as_logical
+)
+from .na import NA, NaN, is_na, any_na
+from .null import NULL, is_null, as_null
+from .random import set_seed
+from .seq import (
+    c, seq, seq_len, seq_along, rev, rep, lengths, unique, sample, length
+)
+
+from .special import (
+    beta, lbeta, gamma, lgamma, digamma, trigamma, psigamma,
+    choose, lchoose, factorial, lfactorial
+)
+from .string import (
+    is_str, is_string, is_character, as_str, as_string, as_character
+)
 from .table import table
-from .arithmetic import sum, mean, median, min, max, pmin, pmax, var
+from .testing import (
+    is_double, is_float, is_int, is_integer, is_numeric,
+    is_atomic, is_element, is_in
+)
+from .trig_hb import (
+    cos, sin, tan, acos, asin, atan, atan2, cospi, sinpi, tanpi,
+    cosh, sinh, tanh, acosh, asinh, atanh
+)
+from .which import which, which_min, which_max
+
+__all__ = [name for name in locals() if not name.startswith('_')]
+__all__.extend([
+    "min",
+    "max",
+    "sum",
+    "abs",
+    "round",
+    "all",
+    "any",
+    "re",
+])
+
+# warn when builtin names are imported directly
+# pylint: disable=wrong-import-position
+from ..core.warn_builtin_names import warn_builtin_names
+from . import (
+    arithmetic as _arithmetic,
+    testing as _testing,
+    complex as _complex
+)
+
+__getattr__ = warn_builtin_names(
+    min=_arithmetic,
+    max=_arithmetic,
+    sum=_arithmetic,
+    abs=_arithmetic,
+    round=_arithmetic,
+    all=_testing,
+    any=_testing,
+    re=_complex
+)
diff --git a/datar/base/arithmetic.py b/datar/base/arithmetic.py
index 96e70451..90ade74a 100644
--- a/datar/base/arithmetic.py
+++ b/datar/base/arithmetic.py
@@ -1,25 +1,32 @@
-"""Arithmetic functions"""
+"""Arithmetic or math functions"""
 
 from multiprocessing.dummy import Array
-from typing import Callable, Iterable
+from typing import Any, Callable, Iterable, Optional
 
 import numpy
-from pipda import register_func
+from pandas import DataFrame, Series
+from pipda import register_func, register_verb
 
 from ..core.contexts import Context
-from ..core.types import NumericType, is_not_null
-from ..core.utils import Array
+from ..core.types import NumericOrIter, NumericType, is_not_null
+from ..core.utils import Array, register_numpy_func_x, recycle_value, length_of
+from ..core.collections import Collection
 
 # TODO: docstring
+# weighted_mean, sd, cor?, range, quantile, summary, iqr
 
-def _register_arithmetic(name: str, doc: str = "") -> Callable:
+def _register_arithmetic_agg(
+        name: str,
+        np_name: str,
+        doc: str = ""
+) -> Callable:
     """Register an arithmetic function"""
     @register_func(None, context=Context.EVAL)
     def _arithmetric(x: Iterable, na_rm: bool = False):
         """Arithmetric function"""
         if na_rm:
             x = Array(x)[is_not_null(x)]
-        return getattr(numpy, name)(x)
+        return getattr(numpy, np_name)(x)
 
     _arithmetric.__name__ = name
     _arithmetric.__doc__ = doc
@@ -27,19 +34,111 @@ def _arithmetric(x: Iterable, na_rm: bool = False):
 
 # pylint: disable=invalid-name
 # pylint: disable=redefined-builtin
-sum = _register_arithmetic('sum', "Sum of an iterable.")
-mean = _register_arithmetic('mean', 'Calculate mean of an iterable.')
-median = _register_arithmetic('median', 'Calculate median of an iterable.')
-min = _register_arithmetic('min', 'Get min of an iterable.')
-max = _register_arithmetic('max', 'Get max of an iterable.')
-var = _register_arithmetic('var', 'Compute the variance of an iterable')
+sum = _register_arithmetic_agg(
+    'sum',
+    'sum',
+    doc="""Sum of the input.
+
+    Args:
+        x: The input
+        na_rm: Exclude the NAs
+
+    Returns:
+        The sum of the input
+    """
+)
+
+mean = _register_arithmetic_agg(
+    'mean',
+    'mean',
+    doc="""Mean of the input.
+
+    Args:
+        x: The input
+        na_rm: Exclude the NAs
+
+    Returns:
+        The mean of the input
+    """
+)
+
+median = _register_arithmetic_agg(
+    'median',
+    'median',
+    doc="""Median of the input.
+
+    Args:
+        x: The input
+        na_rm: Exclude the NAs
+
+    Returns:
+        The median of the input
+    """
+)
+
+@register_func(None, context=Context.EVAL)
+def min(*x, na_rm: bool = False) -> Any:
+    """Min of the input.
+
+    Args:
+        x: The input
+        na_rm: Exclude the NAs
+
+    Returns:
+        The min of the input
+    """
+    fun = numpy.nanmin if na_rm else numpy.min
+    x = Collection(*x) # flatten
+    return fun(x)
+
+
+@register_func(None, context=Context.EVAL)
+def max(*x, na_rm: bool = False) -> Any:
+    """Max of the input.
+
+    Args:
+        x: The input
+        na_rm: Exclude the NAs
+
+    Returns:
+        The max of the input
+    """
+    fun = numpy.nanmax if na_rm else numpy.max
+    x = Collection(*x) # flatten
+    return fun(x)
+
+@register_func(None, context=Context.EVAL)
+def var(x: Any, na_rm: bool = False, ddof: int = 1):
+    """Variance of the input.
+
+    Args:
+        x: The input
+        na_rm: Exclude the NAs
+        ddof: Delta Degrees of Freedom
+
+    Returns:
+        The variance of the input
+    """
+    fun = numpy.nanvar if na_rm else numpy.var
+    return fun(x, ddof=ddof)
+
 
 @register_func(None, context=Context.EVAL)
 def pmin(
         *x: NumericType,
         na_rm: bool = False
 ) -> Iterable[float]:
-    """Get the min value rowwisely"""
+    """Get the min value rowwisely
+
+    Args:
+        *x: The iterables. Elements will be recycled to the max length
+        na_rm: Whether ignore the NAs
+
+    Returns:
+        The rowwise min of `*x`
+    """
+    maxlen = max(map(length_of, x))
+    x = (recycle_value(elem, maxlen) for elem in x)
     return Array([min(elem, na_rm=na_rm) for elem in zip(*x)])
 
 @register_func(None, context=Context.EVAL)
@@ -47,5 +146,89 @@ def pmax(
         *x: Iterable,
         na_rm: bool = False
 ) -> Iterable[float]:
-    """Get the max value rowwisely"""
+    """Get the max value rowwisely
+    Args:
+        *x: The iterables. Elements will be recycled to the max length
+        na_rm: Whether ignore the NAs
+
+    Returns:
+        The rowwise max of `*x`
+    """
+    maxlen = max(map(length_of, x))
+    x = (recycle_value(elem, maxlen) for elem in x)
     return Array([max(elem, na_rm=na_rm) for elem in zip(*x)])
+
+@register_func(None, context=Context.EVAL)
+def round(
+        x: NumericOrIter,
+        ndigits: int = 0
+) -> NumericOrIter:
+    """Rounding a number"""
+    return numpy.round(x, ndigits)
+
+sqrt = register_numpy_func_x(
+    'sqrt',
+    'sqrt',
+    doc="""Get the square root of a number/numbers
+
+    Args:
+        x: The input
+
+    Returns:
+        The square root of the input
+    """
+)
+
+abs = register_numpy_func_x(
+    'abs',
+    'abs',
+    doc="""Get the absolute value of a number/numbers
+
+    Args:
+        x: The input
+
+    Returns:
+        The absolute values of the input
+    """
+)
+
+ceiling = register_numpy_func_x(
+    'ceiling',
+    'ceil',
+    doc="""Get the ceiling integer of a number/numbers
+
+    Args:
+        x: The input
+
+    Returns:
+        The ceiling integer of the input
+    """
+)
+
+floor = register_numpy_func_x(
+    'floor',
+    'floor',
+    doc="""Get the floor integer of a number/numbers
+
+    Args:
+        x: The input
+
+    Returns:
+        The floor integer of the input
+    """
+)
+
+# pylint: disable=unused-argument
+@register_verb(DataFrame, context=Context.EVAL)
+def cov(x: DataFrame, y: Optional[Iterable] = None, ddof: int = 1) -> DataFrame:
+    """Compute pairwise covariance of dataframe columns,
+    or between two variables
+    """
+    # TODO: support na_rm, use, method. see `?cov` in R
+    return x.cov(ddof=ddof)
+
+@cov.register((numpy.ndarray, Series, list, tuple), context=Context.EVAL)
+def _(x: Iterable, y: Iterable, ddof: int = 1) -> DataFrame:
+    """Compute covariance for two iterables"""
+    # ddof: numpy v1.5+
+    return numpy.cov(x, y, ddof=ddof)[0][1]
diff --git a/datar/base/bessel.py b/datar/base/bessel.py
new file mode 100644
index 00000000..aa72888f
--- /dev/null
+++ b/datar/base/bessel.py
@@ -0,0 +1,148 @@
+"""Bessel function family"""
+
+from typing import Callable, Iterable, Mapping, Optional
+
+from pipda import register_func
+
+from ..core.contexts import Context
+
+# pylint: disable=invalid-name
+
+def _get_special_func_from_scipy(name: str) -> Callable:
+    """Import bessel functions from scipy on the fly
+
+    In order to make scipy dependency optional
+    """
+    try:
+        from scipy import special
+    except ImportError: # pragma: no cover
+        raise ImportError("Bessel functions require scipy to be installed.")
+
+    return getattr(special, name)
+
+def _register_bessel_function(
+        name: str,
+        common_fun: str,
+        faster_fun: Mapping[int, str],
+        exp_fun: Optional[str] = None,
+        doc: str = ""
+) -> Callable:
+    """Register bessel function"""
+
+    if exp_fun is None:
+        @register_func(None, context=Context.EVAL)
+        def bessel_fun(x: Iterable, nu: float) -> Iterable:
+            """Bessel function"""
+            # use faster version for order 0 and 1
+            if nu in faster_fun:
+                fun = _get_special_func_from_scipy(faster_fun[nu])
+                return fun(x)
+
+            fun = _get_special_func_from_scipy(common_fun)
+            return fun(nu, x)
+    else:
+        @register_func(None, context=Context.EVAL)
+        def bessel_fun(
+                x: Iterable,
+                nu: float,
+                expon_scaled: bool = False
+        ) -> Iterable:
+            """Modified bessel function"""
+            # use faster version for order 0 and 1
+            if (nu, expon_scaled) in faster_fun:
+                fun = _get_special_func_from_scipy(
+                    faster_fun[(nu, expon_scaled)]
+                )
+                return fun(x)
+
+            if expon_scaled:
+                fun = _get_special_func_from_scipy(exp_fun)
+                return fun(nu, x)
+
+            fun = _get_special_func_from_scipy(common_fun)
+            return fun(nu, x)
+
+    bessel_fun.__name__ = name
+    bessel_fun.__doc__ = doc
+    return bessel_fun
+
+
+bessel_j = _register_bessel_function(
+    'bessel_j',
+    common_fun='jv',
+    faster_fun={0.0: 'j0', 1.0: 'j1'},
+    doc="""Bessel function of first kind
+
+    Args:
+        x: An iterable with numeric >= 0
+        nu: The order of the bessel function
+
+    Returns:
+        Numeric iterable with the values of the corresponding
+        Bessel function.
+    """
+)
+
+bessel_y = _register_bessel_function(
+    'bessel_y',
+    common_fun='yv',
+    faster_fun={0.0: 'y0', 1.0: 'y1'},
+    doc="""Bessel function of second kind
+
+    Args:
+        x: An iterable with numeric >= 0
+        nu: The order of the bessel function
+
+    Returns:
+        Numeric iterable with the values of the corresponding
+        Bessel function.
+    """
+)
+
+bessel_i = _register_bessel_function(
+    'bessel_i',
+    common_fun='iv',
+    exp_fun='ive',
+    faster_fun={
+        (0.0, False): 'i0',
+        (1.0, False): 'i1',
+        (0.0, True): 'i0e',
+        (1.0, True): 'i1e'
+    },
+    doc="""Modified bessel function of first kind
+
+    Args:
+        x: An iterable with numeric >= 0
+        nu: The order of the bessel function
+        expon_scaled: if TRUE, the results are exponentially scaled
+            in order to avoid overflow
+
+    Returns:
+        Numeric iterable with scaled values of the corresponding
+        Bessel function.
+    """
+)
+
+bessel_k = _register_bessel_function(
+    'bessel_k',
+    common_fun='kv',
+    exp_fun='kve',
+    faster_fun={
+        (0.0, False): 'k0',
+        (1.0, False): 'k1',
+        (0.0, True): 'k0e',
+        (1.0, True): 'k1e'
+    },
+    doc="""Modified bessel function of first kind
+
+    Args:
+        x: An iterable with numeric >= 0
+        nu: The order of the bessel function
+        expon_scaled: if TRUE, the results are exponentially scaled
+            in order to avoid underflow
+
+    Returns:
+        Numeric iterable with scaled values of the corresponding
+        Bessel function.
+    """
+)
diff --git a/datar/base/casting.py b/datar/base/casting.py
new file mode 100644
index 00000000..314b76ee
--- /dev/null
+++ b/datar/base/casting.py
@@ -0,0 +1,119 @@
+"""Cast values between types"""
+
+from typing import Any
+
+import numpy
+from pipda import register_func
+
+from ..core.types import (
+    Dtype, NumericOrIter, DoubleOrIter, IntOrIter,
+    is_scalar, is_categorical
+)
+from ..core.contexts import Context
+from ..core.utils import categorized
+from ..core.types import is_null
+
+from .na import NA
+
+# pylint: disable=invalid-name
+
+def _as_type(x: Any, type_: Dtype, na: Any = None) -> Any:
+    """Convert x or elements of x to certain type"""
+    if is_scalar(x):
+        if is_null(x) and na is not None:
+            return na
+        return type_(x)
+
+    if hasattr(x, 'astype'):
+        if na is None:
+            return x.astype(type_)
+
+        na_mask = is_null(x)
+        out = x.astype(type_)
+
+        try:
+            out[na_mask] = na
+        except (ValueError, TypeError):
+            out = out.astype(object)
+            out[na_mask] = na
+
+        return out
+
+    return type(x)([_as_type(elem, type_=type_, na=na) for elem in x])
+
+
+@register_func(None, context=Context.EVAL)
+def as_double(x: Any) -> DoubleOrIter:
+    """Convert an object or elements of an iterable into double/float
+
+    Args:
+        x: The object
+
+    Returns:
+        Values converted into numpy.float64
+    """
+    return _as_type(x, numpy.double)
+
+@register_func(None, context=Context.EVAL)
+def as_float(x: Any, float_dtype: Dtype = numpy.float_) -> DoubleOrIter:
+    """Convert an object or elements of an iterable into double/float
+
+    Args:
+        x: The object
+
+    Returns:
+        Converted value according to the float dtype
+    """
+    return _as_type(x, float_dtype)
+
+@register_func(None, context=Context.EVAL)
+def as_integer(
+        x: Any,
+        integer_dtype: Dtype = numpy.int_,
+        _keep_na: bool = True
+) -> IntOrIter:
+    """Convert an object or elements of an iterable into int64
+
+    Alias `as_int`
+
+    Args:
+        x: The object
+        integer_dtype: The dtype of the integer. Could be one of:
+            - `numpy.int0`
+            - `numpy.int16`
+            - `numpy.int32`
+            - `numpy.int64`
+            - `numpy.int8`
+            - `numpy.int_`
+            - `numpy.intc`
+            - `numpy.intp`
+        _keep_na: If True, NAs will be kept, then the dtype will be object
+            (interger_dtype ignored)
+
+    Returns:
+        Converted values according to the integer_dtype
+    """
+    if is_categorical(x):
+        return categorized(x).codes
+    return _as_type(x, integer_dtype, na=NA if _keep_na else None)
+
+as_int = as_integer
+
+@register_func(None, context=Context.EVAL)
+def as_numeric(x: Any, _keep_na: bool = True) -> NumericOrIter:
+    """Make elements numeric
+
+    Args:
+        x: The value to convert
+        _keep_na: Whether to keep NAs as is. If True, will try to
+            convert to double.
+
+    Returns:
+        Try `as_integer()` if failed then `as_float()`. If keep_na is True
+    """
+    if _keep_na:
+        return as_double(x)
+    try:
+        return as_integer(x)
+    except (ValueError, TypeError):
+        return as_float(x)
diff --git a/datar/base/complex.py b/datar/base/complex.py
new file mode 100644
index 00000000..38d57561
--- /dev/null
+++ b/datar/base/complex.py
@@ -0,0 +1,113 @@
+"""Functions related to complex numbers"""
+from typing import Any
+
+import numpy
+from pandas.core.dtypes.common import is_complex_dtype
+from pipda import register_func
+
+from ..core.utils import register_numpy_func_x
+from ..core.types import ComplexOrIter
+from ..core.contexts import Context
+# pylint: disable=invalid-name
+# pylint: disable=unused-import
+
+from .testing import _register_type_testing
+from .casting import _as_type
+
+re = register_numpy_func_x(
+    're',
+    'real',
+    doc="""Real part of complex numbers
+
+    Args:
+        x: The complex numbers
+
+    Returns:
+        The real part of the complex numbers
+    """
+)
+
+im = register_numpy_func_x(
+    'im',
+    'imag',
+    doc="""Imaginary part of complex numbers
+
+    Args:
+        x: The complex numbers
+
+    Returns:
+        The imaginary part of the complex numbers
+    """
+)
+
+mod = register_numpy_func_x(
+    'mod',
+    'absolute',
+    doc="""Modulus of complex numbers
+
+    Args:
+        x: The complex numbers
+
+    Returns:
+        The Modulus of the complex numbers
+    """
+)
+
+arg = register_numpy_func_x(
+    'arg',
+    'angle',
+    doc="""Angles of complex numbers
+
+    Args:
+        x: The complex numbers
+
+    Returns:
+        The Angles of the complex numbers
+    """
+)
+
+conj = register_numpy_func_x(
+    'conj',
+    'conj',
+    doc="""conjugate of complex numbers
+
+    Args:
+        x: The complex numbers
+
+    Returns:
+        The conjugate of the complex numbers
+    """
+)
+
+@register_func(None, context=Context.EVAL)
+def as_complex(x: Any, complex_type=numpy.complex_) -> ComplexOrIter:
+    """Convert an object or elements of an iterable into complex
+
+    Args:
+        x: The object
+        complex_type: The type of the complex. Could be one of:
+            - numpy.complex128
+            - numpy.complex256
+            - numpy.complex64
+            - numpy.complex_
+
+    Returns:
+        When x is an array or a series, return x.astype(bool).
+        When x is iterable, convert elements of it into bools
+        Otherwise, convert x to bool.
+    """
+    return _as_type(x, complex_type)
+
+is_complex = _register_type_testing(
+    'is_complex',
+    scalar_types=(complex, numpy.complex_),
+    dtype_checker=is_complex_dtype,
+    doc="""Test if a value is complexes
+
+    Args:
+        x: The value to be checked
+
+    Returns:
+        True if the value is an complex or complexes; False otherwise.
+    """
+)
diff --git a/datar/base/constants.py b/datar/base/constants.py
index 9ac58063..f2343572 100644
--- a/datar/base/constants.py
+++ b/datar/base/constants.py
@@ -3,16 +3,8 @@
 from string import ascii_letters
 
 import numpy
-import pandas
-
-from ..core.defaults import NA_REPR
 
 # pylint: disable=invalid-name
-NA = numpy.nan
-NaN = pandas.NA
-TRUE = True
-FALSE = False
-NULL = None
 
 pi = math.pi
 Inf = numpy.inf
@@ -20,7 +12,11 @@
 letters = numpy.array(list(ascii_letters[:26]))
 LETTERS = numpy.array(list(ascii_letters[26:]))
 
-NA_character_ = NA_REPR
-NA_integer_ = numpy.random.randint(numpy.iinfo(numpy.int32).max)
-NA_real_ = NA
-NA_compex_ = complex(NA_real_, NA_real_)
+month_abb = numpy.array([
+    "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+])
+month_name = numpy.array([
+    "January", "February", "March", "April", "May", "June",
+    "July", "August", "September", "October", "November", "December"
+])
diff --git a/datar/base/cum.py b/datar/base/cum.py
new file mode 100644
index 00000000..722d42e3
--- /dev/null
+++ b/datar/base/cum.py
@@ -0,0 +1,88 @@
+"""Cumulative functions"""
+from typing import Any
+
+import numpy
+from pandas import Series
+from pipda import register_func
+
+from ..core.utils import register_numpy_func_x, Array
+from ..core.contexts import Context
+from ..core.types import is_scalar, is_null
+
+from .na import NA
+
+def _ensure_nas_after_na(x: Any) -> Any:
+    """Ensure NAs after first NA
+
+    Since `cum<x>()` in R produces NAs after any NA appearance, but
+    numpy/pandas just ignores the NAs and continue producing real values.
+    """
+    if is_scalar(x):
+        return x
+    na_indexes = numpy.flatnonzero(is_null(x))
+    if len(na_indexes) == 0:
+        return x
+    na_index = na_indexes[0]
+    x = Array(x)
+    x[na_index:] = NA
+    return x
+
+# pylint: disable=invalid-name
+cumsum = register_numpy_func_x(
+    'cumsum',
+    'cumsum',
+    trans_in=_ensure_nas_after_na,
+    doc="""Cumulative sum of elements.
+
+Args:
+    x: Input array
+
+Returns:
+    An array of cumulative sum of elements in x
+"""
+)
+
+cumprod = register_numpy_func_x(
+    'cumprod',
+    'cumprod',
+    trans_in=_ensure_nas_after_na,
+    doc="""Cumulative product of elements.
+
+Args:
+    x: Input array
+
+Returns:
+    An array of cumulative product of elements in x
+"""
+)
+
+
+@register_func(None, context=Context.EVAL)
+def cummin(x: Any) -> Any:
+    """Cummulative min along elements in x
+
+    Args:
+        x: Input array
+
+    Returns:
+        An array of cumulative min of elements in x
+    """
+    if is_scalar(x):
+        return x
+    x = _ensure_nas_after_na(x)
+    return Series(x).cummin()
+
+@register_func(None, context=Context.EVAL)
+def cummax(x: Any) -> Any:
+    """Cummulative max along elements in x
+
+    Args:
+        x: Input array
+
+    Returns:
+        An array of cumulative max of elements in x
+    """
+    if is_scalar(x):
+        return x
+    x = _ensure_nas_after_na(x)
+    return Series(x).cummax()
diff --git a/datar/base/date.py b/datar/base/date.py
new file mode 100644
index 00000000..aee5928e
--- /dev/null
+++ b/datar/base/date.py
@@ -0,0 +1,174 @@
+"""Date time functions"""
+
+import datetime
+import functools
+from typing import Any, Optional, Union, List, Iterable
+
+import numpy
+from pandas import Series, DataFrame
+from pipda import register_func
+
+from ..core.types import IntType, is_scalar_int, is_scalar
+from ..core.contexts import Context
+from .na import NA
+
+# pylint: disable=invalid-name
+# pylint: disable=redefined-builtin
+
+@functools.singledispatch
+def _as_date_dummy(
+        x: Any,
+        format: Optional[str] = None,
+        try_formats: Optional[List[str]] = None,
+        optional: bool = False,
+        tz: Union[IntType, datetime.timedelta] = 0,
+        origin: Any = None
+) -> datetime.date:
+    """Convert a dummy object to date"""
+    raise ValueError(f'Unable to convert to date with type: {type(x)!r}')
+
+@_as_date_dummy.register(datetime.date)
+def _(
+        x: datetime.date,
+        format: Optional[str] = None,
+        try_formats: Optional[List[str]] = None,
+        optional: bool = False,
+        tz: Union[int, datetime.timedelta] = 0,
+        origin: Any = None
+) -> datetime.date:
+    if is_scalar_int(tz):
+        tz = datetime.timedelta(hours=int(tz))
+
+    return x + tz
+
+@_as_date_dummy.register(datetime.datetime)
+def _(
+        x: datetime.datetime,
+        format: Optional[str] = None,
+        try_formats: Optional[List[str]] = None,
+        optional: bool = False,
+        tz: Union[IntType, datetime.timedelta] = 0,
+        origin: Any = None
+):
+    if is_scalar_int(tz):
+        tz = datetime.timedelta(hours=int(tz))
+
+    return (x + tz).date()
+
+@_as_date_dummy.register(str)
+def _(
+        x: str,
+        format: Optional[str] = None,
+        try_formats: Optional[List[str]] = None,
+        optional: bool = False,
+        tz: Union[IntType, datetime.timedelta] = 0,
+        origin: Any = None
+) -> datetime.date:
+    if is_scalar_int(tz):
+        tz = datetime.timedelta(hours=int(tz))
+
+    try_formats = try_formats or [
+        "%Y-%m-%d",
+        "%Y/%m/%d",
+        "%Y-%m-%d %H:%M:%S",
+        "%Y/%m/%d %H:%M:%S"
+    ]
+    if not format:
+        format = try_formats
+    else:
+        format = [format]
+
+    for fmt in format:
+        try:
+            return (datetime.datetime.strptime(x, fmt) + tz).date()
+        except ValueError:
+            continue
+
+    if optional:
+        return NA
+
+    raise ValueError(
+        "character string is not in a standard unambiguous format"
+    )
+
+
+@_as_date_dummy.register(Series)
+def _(
+        x: Series,
+        format: Optional[str] = None,
+        try_formats: Optional[List[str]] = None,
+        optional: bool = False,
+        tz: Union[IntType, datetime.timedelta] = 0,
+        origin: Any = None
+) -> datetime.date:
+    return _as_date_dummy(
+        x.values[0],
+        format=format,
+        try_formats=try_formats,
+        optional=optional,
+        tz=tz,
+        origin=origin
+    )
+
+@_as_date_dummy.register(int)
+@_as_date_dummy.register(numpy.integer)
+def _(
+        x: IntType,
+        format: Optional[str] = None,
+        try_formats: Optional[List[str]] = None,
+        optional: bool = False,
+        tz: Union[IntType, datetime.timedelta] = 0,
+        origin: Any = None
+) -> datetime.date:
+    if isinstance(tz, (int, numpy.integer)):
+        tz = datetime.timedelta(hours=int(tz))
+
+    if isinstance(origin, str):
+        origin = _as_date_dummy(origin)
+
+    dt = origin + datetime.timedelta(days=int(x)) + tz
+
+    if isinstance(dt, datetime.datetime):
+        return dt.date()
+    return dt
+
+@register_func(None, context=Context.EVAL)
+def as_date(
+        x: DataFrame,
+        format: Optional[str] = None,
+        try_formats: Optional[List[str]] = None,
+        optional: bool = False,
+        tz: Union[IntType, datetime.timedelta] = 0,
+        origin: Any = None
+) -> Iterable[datetime.date]:
+    """Convert an object to a datetime.date object
+
+    See: https://rdrr.io/r/base/as.Date.html
+
+    Args:
+        x: Object that can be converted into a datetime.date object
+        format:  If not specified, it will try try_formats one by one on
+            the first non-NA element, and give an error if none works.
+            Otherwise, the processing is via strptime
+        try_formats: vector of format strings to try if format is not specified.
+        optional: indicating to return NA (instead of signalling an error)
+            if the format guessing does not succeed.
+        origin: a datetime.date/datetime object, or something which can be
+            coerced by as_date(origin, ...) to such an object.
+        tz: a time zone offset or a datetime.timedelta object.
+            Note that time zone name is not supported yet.
+
+    Returns:
+        The datetime.date object
+    """
+    if not isinstance(x, Series):
+        x = Series([x]) if is_scalar(x) else Series(x)
+
+    return x.transform(
+        _as_date_dummy,
+        format=format,
+        try_formats=try_formats,
+        optional=optional,
+        tz=tz,
+        origin=origin
+    )
diff --git a/datar/base/factor.py b/datar/base/factor.py
new file mode 100644
index 00000000..65093354
--- /dev/null
+++ b/datar/base/factor.py
@@ -0,0 +1,118 @@
+"""Factors, implemented with pandas' Categorical
+
+The huge difference:
+R's factors support NAs in levels but Categorical cannot have NAs in categories.
+"""
+
+from typing import Optional, Any, Iterable
+
+from pandas import Categorical
+from pipda import register_func
+
+from ..core.contexts import Context
+from ..core.types import (
+    is_scalar, is_categorical as is_categorical_,
+    ArrayLikeType
+)
+from ..core.utils import categorized
+
+from .na import NA
+
+# factor, ordered, is_factor, is_ordered, as_factor, as_ordered, add_na
+
+# pylint: disable=invalid-name
+
+@register_func(None, context=Context.EVAL)
+def droplevels(x: Categorical) -> Categorical:
+    """drop unused levels from a factor
+
+    Args:
+        x: The categorical data
+
+    Returns:
+        The categorical data with unused categories dropped.
+    """
+    return categorized(x).remove_unused_categories()
+
+@register_func(None, context=Context.EVAL)
+def levels(x: Any) -> Optional[ArrayLikeType]:
+    """Get levels from a factor
+
+    Args:
+        x: The categorical data
+
+    Returns:
+        levels of the categorical data
+    """
+    if not is_categorical_(x):
+        return None
+
+    return categorized(x).categories
+
+def factor(
+        x: Optional[Iterable[Any]] = None,
+        # pylint: disable=redefined-outer-name
+        levels: Optional[Iterable[Any]] = None,
+        exclude: Any = NA,
+        ordered: bool = False
+) -> Categorical:
+    """encode a vector as a factor (the terms ‘category’ and ‘enumerated type’
+    are also used for factors).
+
+    If argument ordered is TRUE, the factor levels are assumed to be ordered
+
+    Args:
+        x: a vector of data
+        levels: an optional vector of the unique values (as character strings)
+            that x might have taken.
+        exclude: a vector of values to be excluded when forming the set of
+            levels. This may be factor with the same level set as x or
+            should be a character
+        ordered: logical flag to determine if the levels should be regarded
+            as ordered (in the order given).
+    """
+    if x is None:
+        x = []
+
+    if is_categorical_(x):
+        x = x.to_numpy()
+    ret = Categorical(
+        x,
+        categories=levels,
+        ordered=ordered
+    )
+    if is_scalar(exclude):
+        exclude = [exclude]
+
+    return ret.remove_categories(exclude)
+
+@register_func(None, context=Context.EVAL)
+def as_factor(x: Iterable) -> Categorical:
+    """Convert an iterable into a pandas.Categorical object
+
+    Args:
+        x: The iterable
+
+    Returns:
+        The converted categorical object
+    """
+    return Categorical(x)
+
+as_categorical = as_factor
+
+
+@register_func(None, context=Context.EVAL)
+def is_categorical(x: Any) -> bool:
+    """Check if x is categorical data
+
+    Alias `is_factor`
+
+    Args:
+        x: The value to be checked
+
+    Returns:
+        True if `x` is categorical else False
+    """
+    return is_categorical_(x)
+
+is_factor = is_categorical
diff --git a/datar/base/funcs.py b/datar/base/funcs.py
deleted file mode 100644
index 971ffd8a..00000000
--- a/datar/base/funcs.py
+++ /dev/null
@@ -1,844 +0,0 @@
-"""Some functions from R-base
-
-If a function uses DataFrame/DataFrameGroupBy as first argument, it may be
-registered by `register_verb` and should be placed in `./verbs.py`
-"""
-import builtins
-import math
-import datetime
-import itertools
-import functools
-from typing import Any, Iterable, List, Optional, Type, Union
-
-import numpy
-import pandas
-from pandas.core.dtypes.common import (
-    is_categorical_dtype, is_float_dtype, is_int64_dtype, is_integer_dtype,
-    is_numeric_dtype, is_string_dtype
-)
-from pandas import Series, Categorical, DataFrame
-from pandas.core.groupby.generic import SeriesGroupBy
-from pipda import Context, register_func
-
-from .constants import NA
-from ..core.utils import categorized, get_option, logger, Array, length_of
-from ..core.middlewares import WithDataEnv
-from ..core.collections import Collection
-from ..core.types import (
-    BoolOrIter, CategoricalLikeType, DoubleOrIter, IntOrIter,
-    NumericOrIter,
-    NumericType, SeriesLikeType, StringOrIter, is_scalar_int, IntType,
-    is_iterable, is_series_like, is_scalar, is_null
-)
-from ..core.contexts import Context
-
-# pylint: disable=redefined-builtin,invalid-name
-
-@functools.singledispatch
-def _as_date_dummy(
-        x: Any,
-        format: Optional[str] = None,
-        try_formats: Optional[List[str]] = None,
-        optional: bool = False,
-        tz: Union[IntType, datetime.timedelta] = 0,
-        origin: Any = None
-) -> datetime.date:
-    """Convert a dummy object to date"""
-    raise ValueError(f'Unable to convert to date with type: {type(x)!r}')
-
-@_as_date_dummy.register(datetime.date)
-def _(
-        x: datetime.date,
-        format: Optional[str] = None,
-        try_formats: Optional[List[str]] = None,
-        optional: bool = False,
-        tz: Union[int, datetime.timedelta] = 0,
-        origin: Any = None
-) -> datetime.date:
-    if is_scalar_int(tz):
-        tz = datetime.timedelta(hours=int(tz))
-
-    return x + tz
-
-@_as_date_dummy.register(datetime.datetime)
-def _(
-        x: datetime.datetime,
-        format: Optional[str] = None,
-        try_formats: Optional[List[str]] = None,
-        optional: bool = False,
-        tz: Union[IntType, datetime.timedelta] = 0,
-        origin: Any = None
-):
-    if is_scalar_int(tz):
-        tz = datetime.timedelta(hours=int(tz))
-
-    return (x + tz).date()
-
-@_as_date_dummy.register(str)
-def _(
-        x: str,
-        format: Optional[str] = None,
-        try_formats: Optional[List[str]] = None,
-        optional: bool = False,
-        tz: Union[IntType, datetime.timedelta] = 0,
-        origin: Any = None
-) -> datetime.date:
-    if is_scalar_int(tz):
-        tz = datetime.timedelta(hours=int(tz))
-
-    try_formats = try_formats or [
-        "%Y-%m-%d",
-        "%Y/%m/%d",
-        "%Y-%m-%d %H:%M:%S",
-        "%Y/%m/%d %H:%M:%S"
-    ]
-    if not format:
-        format = try_formats
-    else:
-        format = [format]
-
-    for fmt in format:
-        try:
-            return (datetime.datetime.strptime(x, fmt) + tz).date()
-        except ValueError:
-            continue
-
-    if optional:
-        return numpy.nan
-
-    raise ValueError(
-        "character string is not in a standard unambiguous format"
-    )
-
-
-@_as_date_dummy.register(Series)
-def _(
-        x: Series,
-        format: Optional[str] = None,
-        try_formats: Optional[List[str]] = None,
-        optional: bool = False,
-        tz: Union[IntType, datetime.timedelta] = 0,
-        origin: Any = None
-) -> datetime.date:
-    return _as_date_dummy(
-        x.values[0],
-        format=format,
-        try_formats=try_formats,
-        optional=optional,
-        tz=tz,
-        origin=origin
-    )
-
-@_as_date_dummy.register(int)
-@_as_date_dummy.register(numpy.integer)
-def _(
-        x: IntType,
-        format: Optional[str] = None,
-        try_formats: Optional[List[str]] = None,
-        optional: bool = False,
-        tz: Union[IntType, datetime.timedelta] = 0,
-        origin: Any = None
-) -> datetime.date:
-    if isinstance(tz, (int, numpy.integer)):
-        tz = datetime.timedelta(hours=int(tz))
-
-    if isinstance(origin, str):
-        origin = _as_date_dummy(origin)
-
-    dt = origin + datetime.timedelta(days=int(x)) + tz
-
-    if isinstance(dt, datetime.datetime):
-        return dt.date()
-    return dt
-
-@register_func(None, context=Context.EVAL)
-def as_date(
-        x: DataFrame,
-        format: Optional[str] = None,
-        try_formats: Optional[List[str]] = None,
-        optional: bool = False,
-        tz: Union[IntType, datetime.timedelta] = 0,
-        origin: Any = None
-) -> Iterable[datetime.date]:
-    """Convert an object to a datetime.date object
-
-    See: https://rdrr.io/r/base/as.Date.html
-
-    Args:
-        x: Object that can be converted into a datetime.date object
-        format:  If not specified, it will try try_formats one by one on
-            the first non-NA element, and give an error if none works.
-            Otherwise, the processing is via strptime
-        try_formats: vector of format strings to try if format is not specified.
-        optional: indicating to return NA (instead of signalling an error)
-            if the format guessing does not succeed.
-        origin: a datetime.date/datetime object, or something which can be
-            coerced by as_date(origin, ...) to such an object.
-        tz: a time zone offset or a datetime.timedelta object.
-            Note that time zone name is not supported yet.
-
-    Returns:
-        The datetime.date object
-    """
-    if not isinstance(x, (Series, SeriesGroupBy)):
-        x = Series([x]) if is_scalar(x) else Series(x)
-
-    return x.transform(
-        _as_date_dummy,
-        format=format,
-        try_formats=try_formats,
-        optional=optional,
-        tz=tz,
-        origin=origin
-    )
-
-def _as_type(x: Any, type_: Type) -> Any:
-    """Convert x or elements of x to certain type"""
-    if is_series_like(x):
-        return x.astype(type_)
-
-    if is_scalar(x):
-        return type_(x)
-
-    return type(x)(map(type_, x))
-
-@register_func(None, context=Context.EVAL)
-def as_character(x: Any) -> StringOrIter:
-    """Convert an object or elements of an iterable into string
-
-    Args:
-        x: The object
-
-    Returns:
-        When x is an array or a series, return x.astype(str).
-        When x is iterable, convert elements of it into strings
-        Otherwise, convert x to string.
-    """
-    return _as_type(x, str)
-
-@register_func(None, context=Context.EVAL)
-def as_double(x: Any) -> DoubleOrIter:
-    """Convert an object or elements of an iterable into double/float
-
-    Args:
-        x: The object
-
-    Returns:
-        When x is an array or a series, return x.astype(float).
-        When x is iterable, convert elements of it into floats
-        Otherwise, convert x to float.
-    """
-    return _as_type(x, numpy.double)
-
-@register_func(None, context=Context.EVAL)
-def as_factor(x: Iterable[Any]) -> Categorical:
-    """Convert an iterable into a pandas.Categorical object
-
-    Args:
-        x: The iterable
-
-    Returns:
-        The converted categorical object
-    """
-    return Categorical(x)
-
-as_categorical = as_factor
-
-@register_func(None, context=Context.EVAL)
-def as_int(x: Any) -> Union[int, Iterable[int]]:
-    """Convert an object or elements of an iterable into int
-
-    Args:
-        x: The object
-
-    Returns:
-        When x is an array or a series, return x.astype(int).
-        When x is iterable, convert elements of it into ints
-        Otherwise, convert x to int.
-    """
-    if is_categorical_dtype(x):
-        return categorized(x).codes
-    return _as_type(x, int)
-
-@register_func(None, context=Context.EVAL)
-def as_integer(x: Any) -> Union[numpy.int64, Iterable[numpy.int64]]:
-    """Convert an object or elements of an iterable into int64
-
-    Args:
-        x: The object
-
-    Returns:
-        When x is an array or a series, return x.astype(numpy.int64).
-        When x is iterable, convert elements of it into numpy.int64s
-        Otherwise, convert x to numpy.int64.
-    """
-    if is_categorical_dtype(x):
-        return categorized(x).codes
-    return _as_type(x, numpy.int64)
-
-as_int64 = as_integer
-
-@register_func(None, context=Context.EVAL)
-def as_numeric(x: Any) -> NumericOrIter:
-    """Make elements numeric"""
-    try:
-        return as_integer(x)
-    except (ValueError, TypeError):
-        return as_double(x)
-
-@register_func(None, context=Context.EVAL)
-def as_logical(x: Any) -> BoolOrIter:
-    """Convert an object or elements of an iterable into bool
-
-    Args:
-        x: The object
-
-    Returns:
-        When x is an array or a series, return x.astype(bool).
-        When x is iterable, convert elements of it into bools
-        Otherwise, convert x to bool.
-    """
-    return _as_type(x, bool)
-
-as_bool = as_logical
-
-@register_func(None, context=Context.EVAL)
-def is_numeric(x: Any) -> BoolOrIter:
-    """Check if x is numeric type"""
-    if is_scalar(x):
-        return isinstance(x, (int, float, complex, numpy.number))
-    if isinstance(x, Series):
-        return is_numeric_dtype(x)
-    return Array([is_numeric(elem) for elem in x])
-
-@register_func(None, context=Context.EVAL)
-def is_character(x: Any) -> BoolOrIter:
-    """Mimic the is.character function in R
-
-    Args:
-        x: The elements to check
-
-    Returns:
-        True if
-    """
-    if is_scalar(x):
-        return isinstance(x, str)
-    return is_string_dtype(x)
-
-@register_func(None, context=Context.EVAL)
-def is_categorical(x: Any) -> bool:
-    """Check if x is categorical data"""
-    return is_categorical_dtype(x)
-
-is_factor = is_categorical
-
-@register_func(None, context=Context.EVAL)
-def is_int64(x: Any) -> BoolOrIter:
-    """Check if x is double/float data"""
-    if is_scalar(x):
-        return isinstance(x, (int, numpy.int64))
-    return is_int64_dtype(x)
-
-@register_func(None, context=Context.EVAL)
-def is_integer(x: Any) -> BoolOrIter:
-    """Check if x is double/float data"""
-    if is_scalar(x):
-        return is_scalar_int(x)
-    return is_integer_dtype(x)
-
-is_int = is_integer
-
-@register_func(None, context=Context.EVAL)
-def is_double(x: Any) -> BoolOrIter:
-    """Check if x is double/float data"""
-    if is_scalar(x):
-        return isinstance(x, (float, numpy.float))
-    return is_float_dtype(x)
-
-is_float = is_double
-
-is_na = register_func(None, context=Context.EVAL)(is_null)
-
-@register_func(None, context=Context.UNSET)
-def c(*elems: Any) -> Collection:
-    """Mimic R's concatenation. Named one is not supported yet
-    All elements passed in will be flattened.
-
-    Args:
-        _data: The data piped in
-        *elems: The elements
-
-    Returns:
-        A collection of elements
-    """
-    return Collection(*elems)
-
-@register_func(None, context=Context.EVAL)
-def seq_along(
-        along_with: Iterable[Any],
-        _base0: Optional[bool] = None
-) -> SeriesLikeType:
-    """Generate sequences along an iterable"""
-    _base0 = get_option('index.base.0', _base0)
-    return Array(range(len(along_with))) + int(not _base0)
-
-@register_func(None, context=Context.EVAL)
-def seq_len(
-        length_out: IntOrIter,
-        _base0: Optional[bool] = None
-) -> SeriesLikeType:
-    """Generate sequences with the length"""
-    _base0 = get_option('index.base.0', _base0)
-    if is_scalar(length_out):
-        return Array(range(int(length_out))) + int(not _base0)
-    if len(length_out) > 1:
-        logger.warning(
-            "In seq_len(%r) : first element used of 'length_out' argument",
-            length_out
-        )
-    length_out = int(list(length_out)[0])
-    return Array(range(length_out)) + int(not _base0)
-
-
-@register_func(None, context=Context.EVAL)
-def seq(
-        from_: IntType = None,
-        to: IntType = None,
-        by: IntType = None,
-        length_out: IntType = None,
-        along_with: IntType = None,
-        _base0: Optional[bool] = None,
-) -> SeriesLikeType:
-    """Generate a sequence
-
-    https://rdrr.io/r/base/seq.html
-
-    Note:
-        This API is consistent with r-base's seq. 1-based and inclusive.
-    """
-    _base0 = get_option('index.base.0', _base0)
-    if along_with is not None:
-        return seq_along(along_with, _base0)
-    if from_ is not None and not is_scalar(from_):
-        return seq_along(from_, _base0)
-    if length_out is not None and from_ is None and to is None:
-        return seq_len(length_out)
-
-    base = int(not _base0)
-
-    if from_ is None:
-        from_ = base
-    elif to is None:
-        from_, to = base, from_
-
-    if length_out is not None:
-        by = (float(to) - float(from_)) / float(length_out - 1)
-
-    elif by is None:
-        by = 1 if to > from_ else -1
-        length_out = to - from_ + base if to > from_ else from_ - to + base
-    else:
-        length_out = (to - from_ + .1 * by + float(base) * by) // by
-    return Array([from_ + n * by for n in range(int(length_out))])
-
-@register_func(None, context=Context.EVAL)
-def abs(x: Any) -> NumericOrIter:
-    """Get the absolute value"""
-    return numpy.abs(x)
-
-@register_func(None, context=Context.EVAL)
-def ceiling(x: NumericOrIter) -> IntOrIter:
-    """Get the ceiling integer of x
-
-    Args:
-        x: The number
-
-    Returns:
-        The ceiling integer of x
-    """
-    if is_scalar(x):
-        return math.ceil(x)
-    return list(map(math.ceil, x))
-
-@register_func(None, context=Context.EVAL)
-def floor(x: NumericOrIter) -> IntOrIter:
-    """Get the floor integer of x
-
-    Args:
-        x: The number
-
-    Returns:
-        The ceiling integer of x
-    """
-    if is_scalar(x):
-        return math.floor(x)
-    return list(map(math.floor, x))
-
-def cumx(x: Any, how: str) -> numpy.ndarray:
-    """Cummulative along the elements in x
-
-    numpy and pandas' cums only put NA at the position of original NA, and skip
-    it for later elements. However, all elements afterwards are NA's in R.
-    """
-    if is_scalar(x):
-        x = [x]
-
-    if len(x) == 0:
-        return Array([])
-
-    out = []
-    out_append = out.append
-    def append(elem):
-        if is_null(out[-1]) or is_null(elem):
-            out_append(NA)
-        elif how == 'sum':
-            out_append(out[-1] + elem)
-        elif how == 'prod':
-            out_append(out[-1] * elem)
-        elif how == 'min':
-            out_append(builtins.min(out[-1], elem))
-        elif how == 'max':
-            out_append(builtins.max(out[-1], elem))
-
-    for i, elem in enumerate(x):
-        if i == 0:
-            out_append(elem)
-        else:
-            append(elem)
-    return Array(out)
-
-@register_func(None, context=Context.EVAL)
-def cumsum(x: Any) -> numpy.ndarray:
-    """Cummulative sum along elements in x"""
-    return cumx(x, 'sum')
-
-@register_func(None, context=Context.EVAL)
-def cumprod(x: Any) -> numpy.ndarray:
-    """Cummulative product along elements in x"""
-    return cumx(x, 'prod')
-
-@register_func(None, context=Context.EVAL)
-def cummin(x: Any) -> numpy.ndarray:
-    """Cummulative min along elements in x"""
-    return cumx(x, 'min')
-
-@register_func(None, context=Context.EVAL)
-def cummax(x: Any) -> numpy.ndarray:
-    """Cummulative max along elements in x"""
-    return cumx(x, 'max')
-
-@register_func(None, context=Context.EVAL)
-def cut(
-        x: Iterable[NumericType],
-        breaks: Any,
-        labels: Optional[Iterable[Any]] = None,
-        include_lowest: bool = False,
-        right: bool = True,
-        precision: int = 2,
-        ordered_result: bool = False
-) -> Categorical:
-    """Divides the range of x into intervals and codes the values in x
-    according to which interval they fall. The leftmost interval corresponds
-    to level one, the next leftmost to level two and so on.
-
-    Args:
-        x: a numeric vector which is to be converted to a factor by cutting.
-        breaks: either a numeric vector of two or more unique cut points or
-            a single number (greater than or equal to 2) giving the number of
-            intervals into which x is to be cut.
-        labels: labels for the levels of the resulting category. By default,
-            labels are constructed using "(a,b]" interval notation.
-            If labels = False, simple integer codes are returned instead
-            of a factor.
-        include_lowest: bool, indicating if an ‘x[i]’ equal to the lowest
-            (or highest, for right = FALSE) ‘breaks’ value should be included.
-        right: bool, indicating if the intervals should be closed on the right
-            (and open on the left) or vice versa.
-        precision:integer which is used when labels are not given. It determines
-            the precision used in formatting the break numbers. Note, this
-            argument is different from R's API, which is dig.lab.
-        ordered_result: bool, should the result be an ordered categorical?
-
-    Returns:
-        A categorical object with the cuts
-    """
-    if labels is None:
-        ordered_result = True
-
-    return pandas.cut(
-        x,
-        breaks,
-        labels=labels,
-        include_lowest=include_lowest,
-        right=right,
-        precision=precision,
-        ordered=ordered_result
-    )
-
-@register_func(None, context=Context.EVAL)
-def sample(
-        x: Union[int, Iterable[Any]],
-        size: Optional[int] = None,
-        replace: bool = False,
-        prob: Optional[Iterable[NumericType]] = None
-) -> Iterable[Any]:
-    """Takes a sample of the specified size from the elements of x using
-    either with or without replacement.
-
-    https://rdrr.io/r/base/sample.html
-
-    Args:
-        x: either a vector of one or more elements from which to choose,
-            or a positive integer.
-        n: a positive number, the number of items to choose from.
-        size: a non-negative integer giving the number of items to choose.
-        replace: should sampling be with replacement?
-        prob: a vector of probability weights for obtaining the elements of
-            the vector being sampled.
-
-    Returns:
-        A vector of length size with elements drawn from either x or from the
-        integers 1:x.
-    """
-    if isinstance(x, str):
-        x = list(x)
-    if size is None:
-        size = len(x) if is_iterable(x) else x
-    return numpy.random.choice(x, int(size), replace=replace, p=prob)
-
-@register_func(None, context=Context.EVAL)
-def round(
-        number: NumericOrIter,
-        ndigits: int = 0
-) -> NumericOrIter:
-    """Rounding a number"""
-    if is_series_like(number):
-        return number.round(ndigits)
-    return builtins.round(number, ndigits)
-
-@register_func(None, context=Context.EVAL)
-def sqrt(x: Any) -> bool:
-    """Get the square root of a number"""
-    return numpy.sqrt(x)
-
-@register_func(None, context=Context.EVAL)
-def sin(x: Any) -> NumericOrIter:
-    """Get the sin of a number"""
-    return numpy.sin(x)
-
-@register_func(None, context=Context.EVAL)
-def cos(x: Any) -> NumericOrIter:
-    """Get the cos of a number"""
-    return numpy.cos(x)
-
-@register_func(None, context=Context.EVAL)
-def droplevels(x: Categorical) -> Categorical:
-    """drop unused levels from a factor
-
-    Args:
-        x: The categorical data
-
-    Returns:
-        The categorical data with unused categories dropped.
-    """
-    return categorized(x).remove_unused_categories()
-
-@register_func(None, context=Context.EVAL)
-def levels(x: CategoricalLikeType) -> Optional[List[Any]]:
-    """Get levels from a factor
-
-    Args:
-        x: The categorical data
-
-    Returns:
-        levels of the categorical data
-    """
-    if not is_categorical_dtype(x):
-        return None
-    if isinstance(x, Series):
-        x = x.cat
-    return x.categories
-
-@register_func(None, context=Context.EVAL)
-def rep(
-        x: Any,
-        times: Union[int, Iterable[int]] = 1,
-        length: Optional[int] = None, # pylint: disable=redefined-outer-name
-        each: int = 1
-) -> numpy.ndarray:
-    """replicates the values in x
-
-    Args:
-        x: a vector or scaler
-        times: number of times to repeat each element if of length len(x),
-            or to repeat the whole vector if of length 1
-        length: non-negative integer. The desired length of the output vector
-        each: non-negative integer. Each element of x is repeated each times.
-
-    Returns:
-        A list of repeated elements in x.
-    """
-    if is_scalar(x):
-        x = [x]
-    if is_iterable(times):
-        if len(times) != len(x):
-            raise ValueError(
-                "Invalid times argument, expect length "
-                f"{len(times)}, got {len(x)}"
-            )
-        if each != 1:
-            raise ValueError(
-                "Unexpected each argument when times is an iterable."
-            )
-
-    if is_scalar_int(times):
-        x = Array([elem for elem in x for _ in range(each)] * int(times))
-    else:
-        x = Array([elem for n, elem in zip(times, x) for _ in range(n)])
-    if length is None:
-        return x
-    repeats = length // len(x) + 1
-    x = numpy.tile(x, repeats)
-    return x[:length]
-
-@register_func(None, context=Context.EVAL)
-def rev(x: Iterable[Any]) -> numpy.ndarray:
-    """Get reversed vector"""
-    dtype = getattr(x, 'dtype', None)
-    return Array(list(reversed(x)), dtype=dtype)
-
-
-@register_func(None, context=Context.EVAL)
-def unique(x: Iterable[Any]) -> numpy.ndarray:
-    """Get unique elements"""
-    # return numpy.unique(x)
-    return pandas.unique(x) # keeps order
-
-which = register_func(None, context=Context.EVAL, func=numpy.flatnonzero)
-
-length = register_func(None, context=Context.EVAL, func=length_of)
-
-@register_func(None, context=Context.EVAL)
-def lengths(x: Any) -> List[int]:
-    """Lengths of elements in x"""
-    if is_scalar(x):
-        return [1]
-    return [length(elem) for elem in x]
-
-# ---------------------------------
-# Plain functions
-# ---------------------------------
-
-def factor(
-        x: Optional[Iterable[Any]] = None,
-        # pylint: disable=redefined-outer-name
-        levels: Optional[Iterable[Any]] = None,
-        exclude: Any = NA,
-        ordered: bool = False
-) -> Categorical:
-    """encode a vector as a factor (the terms ‘category’ and ‘enumerated type’
-    are also used for factors).
-
-    If argument ordered is TRUE, the factor levels are assumed to be ordered
-
-    Args:
-        x: a vector of data
-        levels: an optional vector of the unique values (as character strings)
-            that x might have taken.
-        exclude: a vector of values to be excluded when forming the set of
-            levels. This may be factor with the same level set as x or
-            should be a character
-        ordered: logical flag to determine if the levels should be regarded
-            as ordered (in the order given).
-    """
-    if x is None:
-        x = []
-
-    if is_categorical_dtype(x):
-        x = x.to_numpy()
-    ret = Categorical(
-        x,
-        categories=levels,
-        ordered=ordered
-    )
-    if is_scalar(exclude):
-        exclude = [exclude]
-
-    return ret.remove_categories(exclude)
-
-def context(data: DataFrame) -> Any:
-    """Evaluate verbs, functions in the
-    possibly modifying (a copy of) the original data.
-
-    It mimic the `with` function in R, but you have to write it in a python way,
-    which is using the `with` statement. And you have to use it with `as`, since
-    we need the value returned by `__enter__`.
-
-    Args:
-        data: The data
-        func: A function that is registered by
-            `pipda.register_verb` or `pipda.register_func`.
-        *args: Arguments for func
-        **kwargs: Keyword arguments for func
-
-    Returns:
-        The original or modified data
-    """
-    return WithDataEnv(data)
-
-@register_func(None, context=None)
-def identity(x: Any) -> Any:
-    """Return whatever passed in
-
-    Expression objects are evaluated using parent context
-    """
-    return x
-
-@register_func(None)
-def expandgrid(*args: Iterable[Any], **kwargs: Iterable[Any]) -> DataFrame:
-    """Expand all combinations into a dataframe"""
-    iters = {}
-    for i, arg in enumerate(args):
-        iters[f'Var{i}'] = arg
-    iters.update(kwargs)
-
-    return DataFrame(
-        list(itertools.product(*iters.values())),
-        columns=iters.keys()
-    )
-
-@register_func(None)
-def Re(numbers: NumericOrIter) -> numpy.ndarray:
-    """Real part of complex numbers"""
-    if is_scalar(numbers):
-        return numbers.real
-    return numpy.real(numbers)
-
-@register_func(None)
-def Im(numbers: NumericOrIter) -> numpy.ndarray:
-    """Imaginary part of complex numbers"""
-    if is_scalar(numbers):
-        return numbers.imag
-    return numpy.imag(numbers)
-
-@register_func(None)
-def is_element(elem: Any, elems: Iterable[Any]) -> BoolOrIter:
-    """Alias for R's is.element.
-
-    We can't do `a %in% b` in python (in behaves differently), so
-    use this function instead
-    """
-    if is_scalar(elem):
-        return elem in elems
-    return numpy.isin(elem, elems)
-
-is_in = is_element
-
-# pylint: disable=unnecessary-lambda
-all = register_func(None, context=Context.EVAL)(
-    # can't set attributes to builtins.all, so wrap it.
-    lambda *args, **kwargs: builtins.all(*args, **kwargs)
-)
-any = register_func(None, context=Context.EVAL)(
-    lambda *args, **kwargs: builtins.any(*args, **kwargs)
-)
diff --git a/datar/base/funs.py b/datar/base/funs.py
new file mode 100644
index 00000000..aea17752
--- /dev/null
+++ b/datar/base/funs.py
@@ -0,0 +1,113 @@
+"""Some functions from R-base
+
+If a function uses DataFrame/DataFrameGroupBy as first argument, it may be
+registered by `register_verb` and should be placed in `./verbs.py`
+"""
+import itertools
+from typing import Any, Iterable, Optional
+
+import pandas
+from pandas import Categorical, DataFrame
+from pipda import register_func
+
+from ..core.middlewares import WithDataEnv
+from ..core.types import NumericType
+from ..core.contexts import Context
+
+@register_func(None, context=Context.EVAL)
+def cut(
+        x: Iterable[NumericType],
+        breaks: Any,
+        labels: Optional[Iterable[Any]] = None,
+        include_lowest: bool = False,
+        right: bool = True,
+        precision: int = 2,
+        ordered_result: bool = False
+) -> Categorical:
+    """Divides the range of x into intervals and codes the values in x
+    according to which interval they fall. The leftmost interval corresponds
+    to level one, the next leftmost to level two and so on.
+
+    Args:
+        x: a numeric vector which is to be converted to a factor by cutting.
+        breaks: either a numeric vector of two or more unique cut points or
+            a single number (greater than or equal to 2) giving the number of
+            intervals into which x is to be cut.
+        labels: labels for the levels of the resulting category. By default,
+            labels are constructed using "(a,b]" interval notation.
+            If labels = False, simple integer codes are returned instead
+            of a factor.
+        include_lowest: bool, indicating if an ‘x[i]’ equal to the lowest
+            (or highest, for right = FALSE) ‘breaks’ value should be included.
+        right: bool, indicating if the intervals should be closed on the right
+            (and open on the left) or vice versa.
+        precision:integer which is used when labels are not given. It determines
+            the precision used in formatting the break numbers. Note, this
+            argument is different from R's API, which is dig.lab.
+        ordered_result: bool, should the result be an ordered categorical?
+
+    Returns:
+        A categorical object with the cuts
+    """
+    if labels is None:
+        ordered_result = True
+
+    return pandas.cut(
+        x,
+        breaks,
+        labels=labels,
+        include_lowest=include_lowest,
+        right=right,
+        precision=precision,
+        ordered=ordered_result
+    )
+
+@register_func(None, context=Context.EVAL)
+def identity(x: Any) -> Any:
+    """Return whatever passed in
+
+    Expression objects are evaluated using parent context
+    """
+    return x
+
+@register_func(None, context=Context.EVAL)
+def expandgrid(*args: Iterable[Any], **kwargs: Iterable[Any]) -> DataFrame:
+    """Expand all combinations into a dataframe. R's `expand.grid()`"""
+    iters = {}
+    for i, arg in enumerate(args):
+        name = getattr(
+            arg,
+            'name',
+            getattr(arg, '__name__', f'Var{i}')
+        )
+        iters[name] = arg
+    iters.update(kwargs)
+
+    return DataFrame(
+        list(itertools.product(*iters.values())),
+        columns=iters.keys()
+    )
+
+# ---------------------------------
+# Plain functions
+# ---------------------------------
+
+def data_context(data: DataFrame) -> Any:
+    """Evaluate verbs, functions in the
+    possibly modifying (a copy of) the original data.
+
+    It mimic the `with` function in R, but you have to write it in a python way,
+    which is using the `with` statement. And you have to use it with `as`, since
+    we need the value returned by `__enter__`.
+
+    Args:
+        data: The data
+        func: A function that is registered by
+            `pipda.register_verb` or `pipda.register_func`.
+        *args: Arguments for func
+        **kwargs: Keyword arguments for func
+
+    Returns:
+        The original or modified data
+    """
+    return WithDataEnv(data)
diff --git a/datar/base/logical.py b/datar/base/logical.py
new file mode 100644
index 00000000..738bfb31
--- /dev/null
+++ b/datar/base/logical.py
@@ -0,0 +1,86 @@
+"""Logical/Boolean functions"""
+from typing import Any
+
+import numpy
+from pandas.core.dtypes.common import is_bool_dtype
+from pipda import register_func
+
+from ..core.types import BoolOrIter, is_scalar
+from ..core.contexts import Context
+
+from .testing import _register_type_testing
+from .casting import _as_type
+
+TRUE = True
+FALSE = False
+
+# pylint: disable=invalid-name
+
+@register_func(None, context=Context.EVAL)
+def as_logical(x: Any) -> BoolOrIter:
+    """Convert an object or elements of an iterable into bool
+
+    Args:
+        x: The object
+
+    Returns:
+        When x is an array or a series, return x.astype(bool).
+        When x is iterable, convert elements of it into bools
+        Otherwise, convert x to bool.
+    """
+    return _as_type(x, bool)
+
+as_bool = as_logical
+
+
+is_logical = _register_type_testing(
+    'is_logical',
+    scalar_types=(bool, numpy.bool_),
+    dtype_checker=is_bool_dtype,
+    doc="""Test if a value is booleans
+
+Args:
+    x: The value to be checked
+
+Returns:
+    True if the value is an boolean or booleans; False otherwise.
+"""
+)
+
+is_bool = is_logical
+
+@register_func(None, context=Context.EVAL)
+def is_true(x: Any) -> bool:
+    """Check if a value is a scalar True, like `isTRUE()` in `R`.
+
+    If the value is non-scalar, will return False
+    This does not require exact True, but a value that passes
+    `if` test in python
+
+    Args:
+        x: The value to test
+
+    Returns:
+        True if x is True otherwise False
+    """
+    if not is_scalar(x):
+        return False
+    return bool(x)
+
+@register_func(None, context=Context.EVAL)
+def is_false(x: Any) -> bool:
+    """Check if a value is a scalar False, like `isFALSE()` in `R`.
+
+    If the value is non-scalar, will return False
+    This does not require exact False, but a value that fails
+    `if` test in python
+
+    Args:
+        x: The value to test
+
+    Returns:
+        False if x is False otherwise True
+    """
+    if not is_scalar(x):
+        return False
+    return not bool(x)
diff --git a/datar/base/na.py b/datar/base/na.py
new file mode 100644
index 00000000..c5113eb6
--- /dev/null
+++ b/datar/base/na.py
@@ -0,0 +1,57 @@
+"""NA related constants or functions"""
+
+from typing import Any, Iterable
+
+import numpy
+from pipda import register_func
+
+from ..core.contexts import Context
+from ..core.types import is_null, is_scalar
+from ..core.defaults import NA_REPR
+
+# pylint: disable=invalid-name
+NA = numpy.nan
+NaN = NA
+
+# Just for internal and testing uses
+NA_character_ = NA_REPR
+NA_integer_ = numpy.random.randint(numpy.iinfo(numpy.int32).max)
+NA_real_ = numpy.nan
+NA_compex_ = complex(NA_real_, NA_real_)
+
+@register_func(None, context=Context.EVAL)
+def is_na(x: Any) -> Iterable[bool]:
+    """Test if a value is nullable or elements in the value is nullable
+
+    Args:
+        x: The value to test
+
+    Returns:
+        If `x` is scalar, returns a scalar bool. Otherwise, return an array
+        of bools.
+    """
+    return is_null(x)
+
+@register_func(None, context=Context.EVAL)
+def any_na(x: Any, recursive: bool = False) -> bool:
+    """Check if any element in the value is nullable
+
+    Args:
+        x: The value to check
+        recursive: Whether do a recursive check
+
+    Returns:
+        True if any element is nullable otherwise False
+    """
+    if is_scalar(x):
+        return is_na(x)
+
+    from .logical import is_true
+    if not recursive:
+        return any(is_true(is_na(elem)) for elem in x)
+
+    out = False
+    for elem in x:
+        if any_na(elem, recursive=True):
+            return True
+    return out
diff --git a/datar/base/null.py b/datar/base/null.py
new file mode 100644
index 00000000..29efe891
--- /dev/null
+++ b/datar/base/null.py
@@ -0,0 +1,36 @@
+"""Constants and functions about NULL (none)"""
+
+from typing import Any
+
+from pipda import register_func
+
+from ..core.contexts import Context
+
+# pylint: disable=unused-argument
+
+NULL = None
+
+@register_func(None, context=Context.EVAL)
+def as_null(*args: Any, **kwargs: Any) -> None:
+    """Ignores arguments and returns NULL (None). R's `as.null()`
+
+    Args:
+        *args: and
+        **kwargs: arguments to be ignored
+
+    Returns:
+        `NULL`
+    """
+    return NULL
+
+@register_func(None, context=Context.EVAL)
+def is_null(x: Any) -> bool:
+    """Check if x is exactly NULL (None), same as R's `is.null()`
+
+    Args:
+        x: The value to check
+
+    Returns:
+        True if x is NULL (None) else False
+    """
+    return x is NULL
diff --git a/datar/base/random.py b/datar/base/random.py
new file mode 100644
index 00000000..502a47dd
--- /dev/null
+++ b/datar/base/random.py
@@ -0,0 +1,9 @@
+"""Functions related to randomization"""
+import random as std_random
+
+import numpy
+
+def set_seed(seed: int) -> None:
+    """Set the seed for randomization"""
+    std_random.seed(seed)
+    numpy.random.seed(seed)
diff --git a/datar/base/seq.py b/datar/base/seq.py
new file mode 100644
index 00000000..94959478
--- /dev/null
+++ b/datar/base/seq.py
@@ -0,0 +1,202 @@
+"""Generating and manipulating sequences"""
+
+from typing import Optional, Iterable, Any, Union
+
+import numpy
+import pandas
+from pipda import register_func
+
+from ..core.types import (
+    IntType, IntOrIter, ArrayLikeType, NumericType,
+    is_scalar, is_iterable, is_scalar_int
+)
+from ..core.contexts import Context
+from ..core.utils import Array, get_option, length_of, logger
+from ..core.collections import Collection
+
+@register_func(None, context=Context.EVAL)
+def seq_along(
+        along_with: Iterable[Any],
+        _base0: Optional[bool] = None
+) -> ArrayLikeType:
+    """Generate sequences along an iterable
+
+    Args:
+        along_with: An iterable to seq along with
+        _base0: Whether the generated sequence should be 0-based.
+            If not provided, will use `datar.base.get_option('index.base.0')`
+
+    Returns:
+        The generated sequence.
+    """
+    _base0 = get_option('index.base.0', _base0)
+    return Array(range(len(along_with))) + int(not _base0)
+
+@register_func(None, context=Context.EVAL)
+def seq_len(
+        length_out: IntOrIter,
+        _base0: Optional[bool] = None
+) -> ArrayLikeType:
+    """Generate sequences with the length"""
+    _base0 = get_option('index.base.0', _base0)
+    if is_scalar(length_out):
+        return Array(range(int(length_out))) + int(not _base0)
+    if len(length_out) > 1:
+        logger.warning(
+            "In seq_len(%r) : first element used of 'length_out' argument",
+            length_out
+        )
+    length_out = int(list(length_out)[0])
+    return Array(range(length_out)) + int(not _base0)
+
+
+@register_func(None, context=Context.EVAL)
+def seq(
+        from_: IntType = None,
+        to: IntType = None,
+        by: IntType = None,
+        length_out: IntType = None,
+        along_with: IntType = None,
+        _base0: Optional[bool] = None,
+) -> ArrayLikeType:
+    """Generate a sequence
+
+    https://rdrr.io/r/base/seq.html
+
+    Note that this API is consistent with r-base's seq. 1-based and inclusive.
+    """
+    _base0 = get_option('index.base.0', _base0)
+    if along_with is not None:
+        return seq_along(along_with, _base0)
+    if from_ is not None and not is_scalar(from_):
+        return seq_along(from_, _base0)
+    if length_out is not None and from_ is None and to is None:
+        return seq_len(length_out)
+
+    base = int(not _base0)
+
+    if from_ is None:
+        from_ = base
+    elif to is None:
+        from_, to = base, from_
+
+    if length_out is not None:
+        by = (float(to) - float(from_)) / float(length_out - 1)
+
+    elif by is None:
+        by = 1 if to > from_ else -1
+        length_out = to - from_ + base if to > from_ else from_ - to + base
+    else:
+        length_out = (to - from_ + .1 * by + float(base) * by) // by
+    return Array([from_ + n * by for n in range(int(length_out))])
+
+@register_func(None, context=Context.EVAL)
+def rep(
+        x: Any,
+        times: IntOrIter = 1,
+        length: Optional[int] = None, # pylint: disable=redefined-outer-name
+        each: int = 1
+) -> ArrayLikeType:
+    """replicates the values in x
+
+    Args:
+        x: a vector or scaler
+        times: number of times to repeat each element if of length len(x),
+            or to repeat the whole vector if of length 1
+        length: non-negative integer. The desired length of the output vector
+        each: non-negative integer. Each element of x is repeated each times.
+
+    Returns:
+        A list of repeated elements in x.
+    """
+    if is_scalar(x):
+        x = [x]
+    if is_iterable(times):
+        if len(times) != len(x):
+            raise ValueError(
+                "Invalid times argument, expect length "
+                f"{len(times)}, got {len(x)}"
+            )
+        if each != 1:
+            raise ValueError(
+                "Unexpected each argument when times is an iterable."
+            )
+
+    if is_scalar_int(times):
+        x = Array([elem for elem in x for _ in range(each)] * int(times))
+    else:
+        x = Array([elem for n, elem in zip(times, x) for _ in range(n)])
+    if length is None:
+        return x
+    repeats = length // len(x) + 1
+    x = numpy.tile(x, repeats)
+    return x[:length]
+
+
+@register_func(None, context=Context.EVAL)
+def rev(x: Iterable[Any]) -> numpy.ndarray:
+    """Get reversed vector"""
+    dtype = getattr(x, 'dtype', None)
+    return Array(list(reversed(x)), dtype=dtype)
+
+
+@register_func(None, context=Context.EVAL)
+def unique(x: Iterable[Any]) -> numpy.ndarray:
+    """Get unique elements"""
+    # return numpy.unique(x)
+    return pandas.unique(x) # keeps order
+
+# pylint: disable=invalid-name
+length = register_func(None, context=Context.EVAL, func=length_of)
+
+@register_func(None, context=Context.EVAL)
+def lengths(x: Any) -> IntOrIter:
+    """Lengths of elements in x"""
+    if is_scalar(x):
+        return Array([1], dtype=numpy.int_)
+    return Array([length(elem) for elem in x], dtype=numpy.int_)
+
+@register_func(None, context=Context.EVAL)
+def sample(
+        x: Union[IntType, Iterable[Any]],
+        size: Optional[int] = None,
+        replace: bool = False,
+        prob: Optional[Iterable[NumericType]] = None
+) -> Iterable[Any]:
+    """Takes a sample of the specified size from the elements of x using
+    either with or without replacement.
+
+    https://rdrr.io/r/base/sample.html
+
+    Args:
+        x: either a vector of one or more elements from which to choose,
+            or a positive integer.
+        n: a positive number, the number of items to choose from.
+        size: a non-negative integer giving the number of items to choose.
+        replace: should sampling be with replacement?
+        prob: a vector of probability weights for obtaining the elements of
+            the vector being sampled.
+
+    Returns:
+        A vector of length size with elements drawn from either x or from the
+        integers 1:x.
+    """
+    if isinstance(x, str):
+        x = list(x)
+    if size is None:
+        size = len(x) if is_iterable(x) else x
+    return numpy.random.choice(x, int(size), replace=replace, p=prob)
+
+@register_func(None, context=Context.UNSET)
+def c(*elems: Any) -> Collection:
+    """Mimic R's concatenation. Named one is not supported yet
+    All elements passed in will be flattened.
+
+    Args:
+        _data: The data piped in
+        *elems: The elements
+
+    Returns:
+        A collection of elements
+    """
+    return Collection(*elems)
diff --git a/datar/base/special.py b/datar/base/special.py
new file mode 100644
index 00000000..aa86e596
--- /dev/null
+++ b/datar/base/special.py
@@ -0,0 +1,202 @@
+"""Special mathematical functions related to the beta and gamma functions."""
+
+import numpy
+from pipda import register_func
+
+from ..core.types import FloatOrIter, IntOrIter, NumericOrIter, is_scalar
+from ..core.contexts import Context
+from ..core.utils import Array
+from .bessel import _get_special_func_from_scipy
+from .na import NA
+
+# beta(a, b)              => scipy.special.beta(a,b)
+# lbeta(a, b)             => scipy.special.betaln(a,b)
+
+# gamma(x)                => scipy.special.gamma(x)
+# lgamma(x)               => scipy.special.gammaln(x)
+# psigamma(x, deriv = 0)  => scipy.special.polygamma(deriv, x)
+# digamma(x)              => scipy.special.digamma(x)
+# trigamma(x)             => scipy.special.polygamma(1, x)
+
+# choose(n, k)            => scipy.special.comb(n, k)
+# lchoose(n, k)           => numpy.log(scipy.special.comb(n,k))
+# factorial(x)            => scipy.special.factoral(x)
+# lfactorial(x)           => numpy.log(scipy.special.factoral(x))
+
+# pylint: disable=invalid-name
+
+@register_func(None, context=Context.EVAL)
+def beta(a: FloatOrIter, b: FloatOrIter) -> FloatOrIter:
+    """The beta function
+
+    Note that when both `a` and `b` are iterables, the broadcast mechanism is
+    different from R:
+        >>> a = [1,2]; b = [3, 4, 5] will turn into
+        >>> [(1, 3), (2, 4), (1, 5)] in R but raises error in python
+
+    Args:
+        a: and
+        b: Real-valued arguments
+
+    Returns:
+        Value of the beta function
+    """
+    fun = _get_special_func_from_scipy('beta')
+    return fun(a, b)
+
+@register_func(None, context=Context.EVAL)
+def lbeta(a: FloatOrIter, b: FloatOrIter) -> FloatOrIter:
+    """The natural logarithm of `beta()`
+
+    Args:
+        a: and
+        b: Real-valued arguments
+
+    Returns:
+        The natural logarithm of value of the beta function
+    """
+    return numpy.log(beta(a, b))
+
+@register_func(None, context=Context.EVAL)
+def gamma(x: FloatOrIter) -> FloatOrIter:
+    """The gamma function
+
+    Args:
+        x: Real or complex valued argument
+
+    Returns:
+        Values of the gamma function
+    """
+    fun = _get_special_func_from_scipy('gamma')
+    if is_scalar(x) and x <= 0:
+        return NA
+    if not is_scalar(x):
+        x = Array(x, dtype=float)
+        x[x <= 0.0] = NA
+    return fun(x)
+
+@register_func(None, context=Context.EVAL)
+def lgamma(x: FloatOrIter) -> FloatOrIter:
+    """The nature logarithm of `gamma()`
+
+    Args:
+        x: Real or complex valued argument
+
+    Returns:
+        The natural logarithm of values of the gamma function
+    """
+    return numpy.log(gamma(x))
+
+@register_func(None, context=Context.EVAL)
+def digamma(x: FloatOrIter) -> FloatOrIter:
+    """The digamma function.
+
+    Args:
+        x: Real or complex valued argument
+
+    Returns:
+        Computed values of psi.
+    """
+    fun = _get_special_func_from_scipy('digamma')
+    if x == 0.0:
+        return NA
+    return fun(x)
+
+@register_func(None, context=Context.EVAL)
+def choose(n: FloatOrIter, k: IntOrIter) -> FloatOrIter:
+    """The number of combinations of N things taken k at a time.
+
+    Note that when both `a` and `b` are iterables, the broadcast mechanism is
+    different from R:
+        >>> a = [1,2]; b = [3, 4, 5] will turn into
+        >>> [(1, 3), (2, 4), (1, 5)] in R but raises error in python
+
+    Args:
+        n: Number of things.
+        k: Number of elements taken.
+
+    Returns:
+        The total number of combinations.
+    """
+    fun = _get_special_func_from_scipy('comb')
+    return fun(n, k)
+
+@register_func(None, context=Context.EVAL)
+def lchoose(n: FloatOrIter, k: IntOrIter) -> FloatOrIter:
+    """The natural logarithm of `choose()`
+
+    Note that when both `a` and `b` are iterables, the broadcast mechanism is
+    different from R:
+        >>> a = [1,2]; b = [3, 4, 5] will turn into
+        >>> [(1, 3), (2, 4), (1, 5)] in R but raises error in python
+
+    Args:
+        n: Number of things.
+        k: Number of elements taken.
+
+    Returns:
+        The natural logarithm of the total number of combinations.
+    """
+    return numpy.log(choose(n, k))
+
+@register_func(None, context=Context.EVAL)
+def factorial(x: FloatOrIter) -> FloatOrIter:
+    """The factorial of a number or array of numbers.
+
+    Args:
+        Input values. If x < 0, the return value is NA.
+
+    Returns:
+        Factorial of x
+    """
+    fun = _get_special_func_from_scipy('factorial')
+    out = fun(x)
+
+    if is_scalar(x):
+        out = NA if x < 0 else out
+        return out
+
+    out[Array(x) < 0] = NA
+    return out
+
+@register_func(None, context=Context.EVAL)
+def lfactorial(x: FloatOrIter) -> FloatOrIter:
+    """The natural logarithm of `factorial()`
+
+    Args:
+        Input values. If x < 0, the return value is NA.
+
+    Returns:
+        The natural logarithm of factorial of x
+    """
+    return numpy.log(factorial(x))
+
+@register_func(None, context=Context.EVAL)
+def trigamma(x: FloatOrIter) -> FloatOrIter:
+    """The second derivatives of the logarithm of the gamma function
+
+    Args:
+        x: A numeric value or iterable
+
+    Returns:
+        The value of the 2nd derivatives of the logarithm of the gamma function
+    """
+    fun = _get_special_func_from_scipy('polygamma')
+    return fun(1, x)
+
+@register_func(None, context=Context.EVAL)
+def psigamma(
+        x: FloatOrIter,
+        deriv: NumericOrIter
+) -> FloatOrIter:
+    """The deriv-th derivatives of the logarithm of the gamma function
+
+    Args:
+        x: A numeric value or iterable
+
+    Returns:
+        The value of the 2nd derivatives of the logarithm of the gamma function
+    """
+    fun = _get_special_func_from_scipy('polygamma')
+    deriv = numpy.round(deriv)
+    return fun(deriv, x)
diff --git a/datar/base/string.py b/datar/base/string.py
new file mode 100644
index 00000000..1a2683e2
--- /dev/null
+++ b/datar/base/string.py
@@ -0,0 +1,534 @@
+"""String functions in R"""
+import re
+from typing import Any, Iterable, Optional, Tuple, Union
+
+import numpy
+from pandas.core.dtypes.common import is_string_dtype
+from pipda import register_func
+
+from ..core.contexts import Context
+from ..core.types import IntOrIter, StringOrIter, is_scalar, is_null
+from ..core.utils import (
+    arg_match, get_option, length_of, logger, Array, position_at, recycle_value
+)
+
+from .casting import _as_type
+from .testing import _register_type_testing
+from .na import NA
+
+# agrep, agrepl
+# format, format_info, format_pval, format_c, pretty_num (_format_zeros?)
+#
+
+# pylint: disable=invalid-name
+
+@register_func(None, context=Context.EVAL)
+def as_character(x: Any, _na: Any = NA) -> StringOrIter:
+    """Convert an object or elements of an iterable into string
+
+    Aliases `as_str` and `as_string`
+
+    Args:
+        x: The object
+        _na: How NAs should be casted. Specify NA will keep them unchanged.
+            But the dtype will be object then.
+
+    Returns:
+        When x is an array or a series, return x.astype(str).
+        When x is iterable, convert elements of it into strings
+        Otherwise, convert x to string.
+    """
+    return _as_type(x, str, na=_na)
+
+as_str = as_string = as_character
+
+is_character = _register_type_testing(
+    'is_character',
+    scalar_types=(str, numpy.str_),
+    dtype_checker=is_string_dtype,
+    doc="""Test if a value is characters/string
+
+Alias `is_str` and `is_string`
+
+Args:
+    x: The value to be checked
+
+Returns:
+    True if the value is string; with a string dtype;
+    or all elements are strings
+"""
+)
+
+is_str = is_string = is_character
+
+
+# Grep family -----------------------------------
+
+@register_func(None, context=Context.EVAL)
+def grep(
+        pattern: StringOrIter,
+        x: StringOrIter,
+        ignore_case: bool = False,
+        value: bool = False,
+        fixed: bool = False,
+        invert: bool = False,
+        _base0: Optional[bool] = None
+) -> Iterable[Union[int, str]]:
+    """R's grep, get the element in x matching the pattern
+
+    Args:
+        pattern: The pattern
+        x: A string or an iterable of strings; or those can be coerced to
+        ignore_case: Do case-insensitive matching?
+        value: Return values instead of indices?
+        fixed: Fixed matching (instead of regex matching)?
+        invert: Return elements thata don't match instead?
+        _base0: When return indices, whether return 0-based indices?
+            If not set, will use `datar.base.get_option('which.base.0')`
+
+    Returns:
+        The matched (or unmatched (`invert=True`)) indices
+        (or values (`value=True`)).
+    """
+    if is_scalar(x):
+        x = [x]
+    x = Array(as_character(x), dtype=object)
+    matched = grepl(
+        pattern=pattern,
+        x=x,
+        ignore_case=ignore_case,
+        fixed=fixed,
+        invert=invert
+    )
+
+    if value:
+        return x[matched]
+
+    _base0 = get_option('which.base.0', _base0)
+    return numpy.flatnonzero(matched) + int(not _base0)
+
+@register_func(None, context=Context.EVAL)
+def grepl(
+        pattern: StringOrIter,
+        x: StringOrIter,
+        ignore_case: bool = False,
+        fixed: bool = False,
+        invert: bool = False,
+) -> Iterable[Union[int, str]]:
+    """R's grepl, check whether elements in x matching the pattern
+
+    Args:
+        pattern: The pattern
+        x: A string or an iterable of strings; or those can be coerced to
+        ignore_case: Do case-insensitive matching?
+        fixed: Fixed matching (instead of regex matching)?
+        invert: Return elements thata don't match instead?
+
+    Returns:
+        A bool array indicating whether the elements in x match the pattern
+    """
+    pattern = _warn_more_pat_or_rep(pattern, 'grep')
+    match_fun = lambda text: _match(
+        pattern,
+        text,
+        ignore_case=ignore_case,
+        invert=invert,
+        fixed=fixed
+    )
+    if is_scalar(x):
+        x = [x]
+    x = Array(as_character(x), dtype=object)
+    return Array(list(map(match_fun, x)), dtype=bool)
+
+
+@register_func(None, context=Context.EVAL)
+def sub(
+        pattern: StringOrIter,
+        replacement: StringOrIter,
+        x: StringOrIter,
+        ignore_case: bool = False,
+        fixed: bool = False
+) -> Iterable[str]:
+    """R's sub, replace a pattern with replacement for elements in x,
+    each only once
+
+    Args:
+        pattern: The pattern
+        replacement: The replacement
+        x: A string or an iterable of strings; or those can be coerced to
+        ignore_case: Do case-insensitive matching?
+        fixed: Fixed matching (instead of regex matching)?
+
+    Returns:
+        An array of strings with matched parts replaced.
+    """
+    return _sub(
+        pattern=pattern,
+        replacement=replacement,
+        x=x,
+        ignore_case=ignore_case,
+        fixed=fixed,
+    )
+
+
+@register_func(None, context=Context.EVAL)
+def gsub(
+        pattern: StringOrIter,
+        replacement: StringOrIter,
+        x: StringOrIter,
+        ignore_case: bool = False,
+        fixed: bool = False
+) -> Iterable[str]:
+    """R's gsub, replace a pattern with replacement for elements in x,
+    each for all matched parts
+
+    See Also:
+        [sub()](datar.base.string.sub)
+    """
+    return _sub(
+        pattern=pattern,
+        replacement=replacement,
+        x=x,
+        ignore_case=ignore_case,
+        fixed=fixed,
+        count=0,
+        fun='gsub'
+    )
+
+# Grep family helpers --------------------------------
+
+def _warn_more_pat_or_rep(
+        pattern: StringOrIter,
+        fun: str,
+        arg: str = 'pattern'
+) -> str:
+    """Warn when there are more than one pattern or replacement provided"""
+    if is_scalar(pattern):
+        return pattern
+    if len(pattern) == 1:
+        return pattern[0]
+
+    logger.warning(
+        "In %s(...), argument `%s` has length > 1 and only the "
+        "first element will be used",
+        fun,
+        arg
+    )
+    return pattern[0]
+
+def _match(
+        pattern: str,
+        text: str,
+        ignore_case: bool,
+        invert: bool,
+        fixed: bool
+) -> bool:
+    """Do the regex match"""
+    flags = re.IGNORECASE if ignore_case else 0
+    if fixed:
+        pattern = re.escape(pattern)
+
+    pattern = re.compile(pattern, flags)
+    matched = pattern.search(text)
+    if invert:
+        matched = not bool(matched)
+    return bool(matched)
+
+def _sub(
+        pattern: StringOrIter,
+        replacement: StringOrIter,
+        x: StringOrIter,
+        ignore_case: bool = False,
+        fixed: bool = False,
+        count: int = 1,
+        fun: str = 'sub'
+) -> Iterable[str]:
+    """Replace a pattern with replacement for elements in x,
+    with argument count available
+    """
+    pattern = _warn_more_pat_or_rep(pattern, fun)
+    replacement = _warn_more_pat_or_rep(replacement, fun, 'replacement')
+    if fixed:
+        pattern = re.escape(pattern)
+
+    flags = re.IGNORECASE if ignore_case else 0
+    pattern = re.compile(pattern, flags)
+
+    return Array([
+        pattern.sub(repl=replacement, count=count, string=elem)
+        for elem in x
+    ], dtype=object)
+
+# nchar family -------------------------------
+
+# pylint: disable=redefined-builtin
+
+@register_func(None, context=Context.EVAL)
+def nchar(
+        x: StringOrIter,
+        type: str = 'chars',
+        allow_na: bool = True, # i.e.: '\ud861'
+        keep_na: Optional[bool] = None,
+        _na_len: int = 2
+) -> Iterable[int]:
+    """Get the size of the elements in x"""
+    x, keep_na = _prepare_nchar(
+        x, type, keep_na
+    )
+    out = [
+        _nchar_scalar(
+            elem,
+            retn=type,
+            allow_na=allow_na,
+            keep_na=keep_na,
+            na_len=_na_len
+        )
+        for elem in x
+    ]
+    if is_null(out).any():
+        return Array(out, dtype=object)
+    return Array(out, dtype=int)
+
+@register_func(None, context=Context.EVAL)
+def nzchar(
+        x: StringOrIter,
+        keep_na: bool = False
+) -> Iterable[bool]:
+    """Find out if elements of a character vector are non-empty strings.
+
+    Args:
+        x: Strings to test
+        keep_na: Keep NAs as is?
+
+    Returns:
+        A bool array to tell whether elements in x are non-empty strings
+    """
+    if is_scalar(x):
+        x = [x]
+    x = as_character(x, _na=NA if keep_na else None)
+    out = [
+        NA if is_null(elem) else bool(elem)
+        for elem in x
+    ]
+    if is_null(out).any():
+        return Array(out, dtype=object)
+    return Array(out, dtype=bool)
+
+# nchar helpers --------------------------------
+
+def _prepare_nchar(
+        x: StringOrIter,
+        type: str,
+        keep_na: Optional[bool]
+) -> Tuple[Iterable[str], bool]:
+    """Prepare arguments for n(z)char"""
+    arg_match(type, 'type', ['chars', 'bytes', 'width'])
+    if keep_na is None:
+        keep_na = type != 'width'
+
+    if is_scalar(x):
+        x = [x]
+    x = Array(as_character(x), dtype=object)
+    return x, keep_na
+
+def _nchar_scalar(
+        x: str,
+        retn: str,
+        allow_na: bool,
+        keep_na: bool,
+        na_len: int
+) -> int:
+    """Get the size of a scalar string"""
+    if is_null(x):
+        return NA if keep_na else na_len
+
+    if retn == 'width':
+        from wcwidth import wcswidth
+        return wcswidth(x)
+    if retn == 'chars':
+        return len(x)
+
+    try:
+        x = x.encode('utf-8')
+    except UnicodeEncodeError:
+        if allow_na:
+            return NA
+        raise
+    return len(x)
+
+# paste and paste0 --------------------
+
+def paste(
+        *args: StringOrIter,
+        sep: str = " ",
+        collapse: Optional[str] = None
+) -> StringOrIter:
+    """Concatenate vectors after converting to character.
+
+    Args:
+        *args: strings to be concatenated
+        sep: The separator
+        collapse: The separator to collapse the final string arrays
+
+    Returns:
+        A single string if collapse is given, otherwise an array of strings.
+    """
+    args = [as_character(arg, _na='NA') for arg in args]
+    maxlen = max(map(length_of, args))
+    args = zip(*(recycle_value(arg, maxlen) for arg in args))
+
+    out = [sep.join(arg) for arg in args]
+    if collapse is not None:
+        return collapse.join(out)
+    return Array(out, dtype=str)
+
+@register_func(None, context=Context.EVAL)
+def paste0(
+        *args: StringOrIter,
+        collapse: Optional[str] = None
+) -> StringOrIter:
+    """`paste` with `sep=""`
+
+    See [paste](datar.base.string.paste)
+    """
+    return paste(*args, sep="", collapse=collapse)
+
+# sprintf ----------------------------------------------------------------
+
+@register_func(None, context=Context.EVAL)
+def sprintf(
+        fmt: StringOrIter,
+        *args: StringOrIter
+) -> StringOrIter:
+    """C-style String Formatting
+
+    Args:
+        fmt: The formats
+        *args: The values
+
+    Returns:
+        A scalar string if all fmt, *args are scalar strings, otherwise
+        an array of formatted strings
+    """
+    if is_scalar(fmt) and all(is_scalar(arg) for arg in args):
+        return fmt % args
+
+    # args = [as_character(arg, _na='NA') for arg in args]
+    maxlen = max(map(length_of, args))
+    maxlen = max(maxlen, max(map(length_of, fmt)))
+
+    fmt = recycle_value(fmt, maxlen)
+    args = [recycle_value(arg, maxlen) for arg in args]
+    return Array([
+        fmt[i] % arg
+        for i, arg in enumerate(zip(*args))
+    ], dtype=str)
+
+# substr, substring ----------------------------------
+
+@register_func(None, context=Context.EVAL)
+def substr(
+        x: StringOrIter,
+        start: IntOrIter,
+        stop: IntOrIter,
+        _base0: Optional[bool] = None
+) -> StringOrIter:
+    """Extract substrings in strings.
+
+    Args:
+        x: The strings
+        start: The start positions to extract
+        stop: The stop positions to extract
+        _base0: Whether `start` and `stop` are 0-based
+            If not provided, will use `datar.base.get_option('index.base.0')`
+
+    Returns:
+        The substrings from `x`
+    """
+    if is_scalar(x) and is_scalar(start) and is_scalar(stop):
+        if is_null(x):
+            return NA
+        _base0 = get_option('index.base.0', _base0)
+        x = as_character(x)
+        lenx = len(x)
+        # int() converts numpy.int64 to int
+        start0 = position_at(int(start), lenx, base0=_base0)
+        stop0 = position_at(
+            min(int(stop), lenx - int(_base0)),
+            lenx,
+            base0=_base0
+        )
+        return x[start0:stop0+1]
+
+    if is_scalar(x):
+        x = [x]
+    if is_scalar(start):
+        start = [start]
+    if is_scalar(stop):
+        stop = [stop]
+    maxlen = max(length_of(x), length_of(start), length_of(stop))
+    x = recycle_value(x, maxlen)
+    start = recycle_value(start, maxlen)
+    stop = recycle_value(stop, maxlen)
+    out = [
+        substr(elem, start_, stop_, _base0)
+        for elem, start_, stop_ in zip(x, start, stop)
+    ]
+    if is_null(out).any():
+        return Array(out, dtype=object)
+    return Array(out, dtype=str)
+
+@register_func(None, context=Context.EVAL)
+def substring(
+        x: StringOrIter,
+        first: IntOrIter,
+        last: IntOrIter = 1000000,
+        _base0: Optional[bool] = None
+) -> StringOrIter:
+    """Extract substrings in strings.
+
+    Args:
+        x: The strings
+        start: The start positions to extract
+        stop: The stop positions to extract
+        _base0: Whether `start` and `stop` are 0-based
+            If not provided, will use `datar.base.get_option('index.base.0')`
+
+    Returns:
+        The substrings from `x`
+    """
+    return substr(x, first, last, _base0)
+
+# strsplit --------------------------------
+
+@register_func(None, context=Context.EVAL)
+def strsplit(
+        x: StringOrIter,
+        split: StringOrIter,
+        fixed: bool = False
+) -> Iterable[Union[str, Iterable[str]]]:
+    """Split strings by separator
+
+    Args:
+        x: The strings. Have to be strings, no casting will be done.
+        split: The separators to split
+        fixed: fixed matching (instead of regex matching)?
+
+    Returns:
+        List of split strings of x if both x and split are scalars. Otherwise,
+        an array of split strings
+    """
+    if is_scalar(x) and is_scalar(split):
+        if fixed:
+            split = re.escape(split)
+        split = re.compile(split)
+        return split.split(x)
+
+    maxlen = max(length_of(x), length_of(split))
+    x = recycle_value(x, maxlen)
+    split = recycle_value(split, maxlen)
+    out = [
+        strsplit(elem, splt, fixed=fixed)
+        for elem, splt in zip(x, split)
+    ]
+    return Array(out, dtype=object)
diff --git a/datar/base/table.py b/datar/base/table.py
index 05839cd6..a6d87767 100644
--- a/datar/base/table.py
+++ b/datar/base/table.py
@@ -11,7 +11,7 @@
 from ..core.types import is_iterable, is_scalar, is_categorical, is_null
 from ..core.utils import Array, categorized, fillna_safe
 
-from .constants import NA
+from .na import NA
 
 # pylint: disable=redefined-builtin
 
diff --git a/datar/base/trig_hb.py b/datar/base/trig_hb.py
new file mode 100644
index 00000000..53089d66
--- /dev/null
+++ b/datar/base/trig_hb.py
@@ -0,0 +1,182 @@
+"""Trigonometric and Hyperbolic Functions"""
+
+from typing import Callable
+
+import numpy
+from pipda import register_func
+
+from ..core.contexts import Context
+from ..core.types import FloatOrIter
+from .constants import pi
+
+# pylint: disable=invalid-name
+
+def _register_trig_hb_func(
+        name: str,
+        np_name: str,
+        doc: str
+) -> Callable:
+    """Register trigonometric and hyperbolic function"""
+    np_fun = getattr(numpy, np_name)
+    if name.endswith('pi'):
+        func = lambda x: np_fun(x * pi)
+    else:
+        # ufunc cannot set context
+        func = lambda x: np_fun(x) # pylint: disable=unnecessary-lambda
+
+    func = register_func(
+        None,
+        context=Context.EVAL,
+        func=func
+    )
+    func.__name__ = name
+    func.__doc__ = doc
+    return func
+
+sin = _register_trig_hb_func('sin', 'sin', doc="""The sine function
+
+Args:
+    x: a numeric value or iterable
+
+Returns:
+    The sine value of `x`
+""")
+
+cos = _register_trig_hb_func('cos', 'cos', doc="""The cosine function
+
+Args:
+    x: a numeric value or iterable
+
+Returns:
+    The cosine value of `x`
+""")
+
+tan = _register_trig_hb_func('tan', 'tan', doc="""The tangent function
+
+Args:
+    x: a numeric value or iterable
+
+Returns:
+    The tangent value of `x`
+""")
+
+acos = _register_trig_hb_func('acos', 'arccos', doc="""The arc-cosine function
+
+Args:
+    x: a numeric value or iterable
+
+Returns:
+    The arc-cosine value of `x`
+""")
+
+asin = _register_trig_hb_func('acos', 'arcsin', doc="""The arc-sine function
+
+Args:
+    x: a numeric value or iterable
+
+Returns:
+    The arc-sine value of `x`
+""")
+
+atan = _register_trig_hb_func('acos', 'arctan', doc="""The arc-sine function
+
+Args:
+    x: a numeric value or iterable
+
+Returns:
+    The arc-sine value of `x`
+""")
+
+sinpi = _register_trig_hb_func('sinpi', 'sin', doc="""The sine function
+
+Args:
+    x: a numeric value or iterable, which is the multiple of pi
+
+Returns:
+    The sine value of `x`
+""")
+
+cospi = _register_trig_hb_func('cospi', 'cos', doc="""The cosine function
+
+Args:
+    x: a numeric value or iterable, which is the multiple of pi
+
+Returns:
+    The cosine value of `x`
+""")
+
+tanpi = _register_trig_hb_func('tanpi', 'tan', doc="""The tangent function
+
+Args:
+    x: a numeric value or iterable, which is the multiple of pi
+
+Returns:
+    The tangent value of `x`
+""")
+
+cosh = _register_trig_hb_func('cosh', 'cosh', doc="""Hyperbolic cosine
+
+Args:
+    x: a numeric value or iterable
+
+Returns:
+    The hyperbolic cosine value of `x`
+""")
+
+sinh = _register_trig_hb_func('sinh', 'sinh', doc="""Hyperbolic sine
+
+Args:
+    x: a numeric value or iterable
+
+Returns:
+    The hyperbolic sine value of `x`
+""")
+
+tanh = _register_trig_hb_func('tanh', 'tanh', doc="""Hyperbolic tangent
+
+Args:
+    x: a numeric value or iterable
+
+Returns:
+    The hyperbolic tangent value of `x`
+""")
+
+acosh = _register_trig_hb_func('acosh', 'arccosh', doc="""Hyperbolic arc-cosine
+
+Args:
+    x: a numeric value or iterable
+
+Returns:
+    The hyperbolic arc-cosine value of `x`
+""")
+
+asinh = _register_trig_hb_func('asinh', 'arcsinh', doc="""Hyperbolic arc-sine
+
+Args:
+    x: a numeric value or iterable
+
+Returns:
+    The hyperbolic arc-sine value of `x`
+""")
+
+atanh = _register_trig_hb_func('atanh', 'arctanh', doc="""Hyperbolic arc-tangent
+
+Args:
+    x: a numeric value or iterable
+
+Returns:
+    The hyperbolic arc-tangent value of `x`
+""")
+
+@register_func(None, context=Context.EVAL)
+def atan2(y: FloatOrIter, x: FloatOrIter) -> FloatOrIter:
+    """Calculates the angle between the x-axis and the vector (0,0) -> (x,y)
+
+    Args:
+        y: and
+        x: The end coordinates of the vector
+
+    Returns:
+        The angle between x-axis and vector (0,0) -> (x,y)
+    """
+    return numpy.arctan2(y, x)
diff --git a/datar/base/verbs.py b/datar/base/verbs.py
index 6bdd9d66..37d134f1 100644
--- a/datar/base/verbs.py
+++ b/datar/base/verbs.py
@@ -1,5 +1,4 @@
 """Function from R-base that can be used as verbs"""
-# TODO: add tests
 from typing import (
     Any, Iterable, List, Mapping, Optional, Tuple, Union
 )
@@ -18,46 +17,48 @@
 @register_verb(DataFrame, context=Context.EVAL)
 def colnames(
         df: DataFrame,
-        names: Optional[Iterable[str]] = None,
-        stack: bool = True
+        new: Optional[Iterable[str]] = None,
+        _nested: bool = True
 ) -> Union[List[Any], DataFrame]:
     """Get or set the column names of a dataframe
 
     Args:
         df: The dataframe
-        names: The names to set as column names for the dataframe.
+        new: The new names to set as column names for the dataframe.
 
     Returns:
         A list of column names if names is None, otherwise return the dataframe
         with new column names.
         if the input dataframe is grouped, the structure is kept.
     """
-    from ..stats.verbs import setNames
-    if not stack:
-        if names is not None:
-            return setNames(df, names)
+    from ..stats.verbs import set_names
+    if not _nested:
+        if new is not None:
+            return set_names(df, new)
         return df.columns.tolist()
 
-    if names is not None:
+    if new is not None:
         namei = 0
         newnames = []
+        last_parts0 = None
         for colname in df.columns:
-            parts = colname.split('$', 1)
+            parts = str(colname).split('$', 1)
             if not newnames:
                 if len(parts) < 2:
-                    newnames.append(names[namei])
-                    namei += 1
+                    newnames.append(new[namei])
                 else:
-                    newnames.append(f"{names[namei]}${parts[1]}")
+                    last_parts0 = parts[0]
+                    newnames.append(f"{new[namei]}${parts[1]}")
             elif len(parts) < 2:
-                newnames.append(names[namei])
                 namei += 1
-            elif newnames[-1].startswith(f"{parts[0]}$"):
-                newnames.append(f"{names[namei]}${parts[1]}")
+                newnames.append(new[namei])
+            elif last_parts0 and colname.startswith(f"{last_parts0}$"):
+                newnames.append(f"{new[namei]}${parts[1]}")
             else:
                 namei += 1
-                newnames.append(f"{names[namei]}${parts[1]}")
-        return setNames(df, newnames)
+                newnames.append(f"{new[namei]}${parts[1]}")
+                last_parts0 = parts[0]
+        return set_names(df, newnames)
 
     cols = [
         col.split('$', 1)[0] if isinstance(col, str) else col
@@ -73,13 +74,13 @@ def colnames(
 @register_verb(DataFrame, context=Context.EVAL)
 def rownames(
         df: DataFrame,
-        names: Optional[Iterable[str]] = None
+        new: Optional[Iterable[str]] = None
 ) -> Union[List[Any], DataFrame]:
     """Get or set the row names of a dataframe
 
     Args:
         df: The dataframe
-        names: The names to set as row names for the dataframe.
+        new: The new names to set as row names for the dataframe.
         copy: Whether return a copy of dataframe with new row names
 
     Returns:
@@ -87,25 +88,25 @@ def rownames(
         with new row names.
         if the input dataframe is grouped, the structure is kept.
     """
-    if names is not None:
+    if new is not None:
         df = df.copy()
-        df.index = names
+        df.index = new
         return df
 
     return df.index.tolist()
 
 @register_verb(DataFrame, context=Context.EVAL)
-def dim(x: DataFrame, stack: bool = True) -> Tuple[int]:
+def dim(x: DataFrame, _nested: bool = True) -> Tuple[int]:
     """Retrieve the dimension of a dataframe.
 
     Args:
         x: a dataframe
-        stack: When there is stacked df, count as 1.
+        _nested: When there is _nesteded df, count as 1.
 
     Returns:
         The shape of the dataframe.
     """
-    return (nrow(x), ncol(x, stack))
+    return (nrow(x), ncol(x, _nested))
 
 @register_verb(DataFrame)
 def nrow(_data: DataFrame) -> int:
@@ -120,17 +121,17 @@ def nrow(_data: DataFrame) -> int:
     return _data.shape[0]
 
 @register_verb(DataFrame)
-def ncol(_data: DataFrame, stack: bool = True):
+def ncol(_data: DataFrame, _nested: bool = True):
     """Get the number of columns in a dataframe
 
     Args:
         _data: The dataframe
-        stack: When there is stacked df, count as 1.
+        _nested: When there is _nesteded df, count as 1.
 
     Returns:
         The number of columns in _data
     """
-    if not stack:
+    if not _nested:
         return _data.shape[1]
     cols = set()
     for col in _data.columns:
@@ -177,7 +178,7 @@ def diag(
     x = Array(x)
     try:
         ret = DataFrame(numpy.diag(x), dtype=x.dtype)
-    except TypeError:
+    except TypeError: # pragma: no cover, TODO: add test
         ret = DataFrame(numpy.diag(x), dtype=object)
     return ret.iloc[:nrow, :ncol]
 
@@ -193,7 +194,7 @@ def _(
 
     x = x.copy()
     if nrow is not None:
-        numpy.fill_diagonal(x, nrow)
+        numpy.fill_diagonal(x.values, nrow)
         return x
     return numpy.diag(x)
 
@@ -211,17 +212,27 @@ def t(_data: DataFrame, copy: bool = False) -> DataFrame:
     return _data.transpose(copy=copy)
 
 @register_verb(DataFrame)
-def names(x: DataFrame) -> List[str]:
+def names(
+        x: DataFrame,
+        new: Optional[Iterable[str]] = None,
+        _nested: bool = True
+) -> Union[List[str], DataFrame]:
     """Get the column names of a dataframe"""
-    return x.columns.tolist()
+    return colnames(x, new, _nested)
 
 @names.register(dict)
-def _(x: Mapping[str, Any]) -> List[str]:
+def _(
+        x: Mapping[str, Any],
+        new: Optional[Iterable[str]] = None,
+        _nested: bool = True
+) -> Union[List[str], Mapping[str, Any]]:
     """Get the keys of a dict
 
     dict is like a list in R, mimic `names(<list>)` in R.
     """
-    return list(x)
+    if new is None:
+        return list(x)
+    return dict(zip(new, x.values()))
 
 @register_verb(context=Context.EVAL)
 def setdiff(x: Any, y: Any) -> List[Any]:
@@ -268,14 +279,14 @@ def setequal(x: Any, y: Any) -> List[Any]:
 def duplicated( # pylint: disable=invalid-name
         x: Iterable[Any],
         incomparables: Optional[Iterable[Any]] = None,
-        fromLast: bool = False
+        from_last: bool = False
 ) -> numpy.ndarray:
     """Determine Duplicate Elements
 
     Args:
         x: The iterable to detect duplicates
             Currently, elements in `x` must be hashable.
-        fromLast: Whether start to detect from the last element
+        from_last: Whether start to detect from the last element
 
     Returns:
         A bool array with the same length as `x`
@@ -286,17 +297,17 @@ def duplicated( # pylint: disable=invalid-name
     if incomparables is None:
         incomparables = []
 
-    if fromLast:
+    if from_last:
         x = reversed(x)
     for elem in x:
         if elem in incomparables:
             out_append(False)
-        if elem in dups:
+        elif elem in dups:
             out_append(True)
         else:
             dups.add(elem)
             out_append(False)
-    if fromLast:
+    if from_last:
         out = list(reversed(out))
     return Array(out, dtype=bool)
 
@@ -304,25 +315,11 @@ def duplicated( # pylint: disable=invalid-name
 def _( # pylint: disable=invalid-name,unused-argument
         x: DataFrame,
         incomparables: Optional[Iterable[Any]] = None,
-        fromLast: bool = False
+        from_last: bool = False
 ) -> numpy.ndarray:
     """Check if rows in a data frame are duplicated
 
     `incomparables` not working here
     """
-    keep = 'first' if not fromLast else 'last'
+    keep = 'first' if not from_last else 'last'
     return x.duplicated(keep=keep).values
-
-@register_verb(DataFrame, context=Context.EVAL)
-def cov(x: DataFrame, y: Optional[Iterable] = None, ddof: int = 1) -> DataFrame:
-    """Compute pairwise covariance of dataframe columns,
-    or between two variables
-    """
-    # TODO: support na_rm, use, method. see `?cov` in R
-    return x.cov(ddof=ddof)
-
-@cov.register((numpy.ndarray, Series, list, tuple), context=Context.EVAL)
-def _(x: Iterable, y: Iterable, ddof: int = 1) -> DataFrame:
-    """Compute covariance for two iterables"""
-    # ddof: numpy v1.5+
-    return numpy.cov(x, y, ddof=ddof)[0][1]
diff --git a/datar/base/which.py b/datar/base/which.py
new file mode 100644
index 00000000..2dc8a44f
--- /dev/null
+++ b/datar/base/which.py
@@ -0,0 +1,55 @@
+"""Checking an iterable against itself or another one"""
+from typing import Iterable, Optional
+
+import numpy
+from pipda import register_func
+
+from ..core.utils import get_option
+from ..core.contexts import Context
+
+@register_func(None, context=Context.EVAL)
+def which(x: Iterable[bool], _base0: Optional[bool] = None) -> Iterable[int]:
+    """Convert a bool iterable to indexes
+
+    Args:
+        x: An iterable of bools.
+            Note that non-bool values will be converted into
+        _base0: Whether the returned indexes are 0-based.
+            Controlled by `get_option('which.base.0')` if not provided
+
+    Returns:
+        The indexes
+    """
+    return numpy.flatnonzero(x) + int(not get_option('which.base.0', _base0))
+
+@register_func(None)
+def which_min(x: Iterable, _base0: Optional[bool] = None) -> int:
+    """R's `which.min()`
+
+    Get the index of the element with the maximum value
+
+    Args:
+        x: The iterable
+        _base0: Whether the index to return is 0-based or not.
+            Controlled by `get_option('which.base.0')` if not provided
+
+    Returns:
+        The index of the element with the maximum value
+    """
+    return numpy.argmin(x) + int(not get_option('which.base.0', _base0))
+
+@register_func(None)
+def which_max(x: Iterable, _base0: bool = True) -> int:
+    """R's `which.max()`
+
+    Get the index of the element with the minimum value
+
+    Args:
+        x: The iterable
+        _base0: Whether the index to return is 0-based or not
+            Not that this is not controlled by `get_option('index.base.0')`
+
+    Returns:
+        The index of the element with the minimum value
+    """
+    return numpy.argmax(x) + int(not get_option('which.base.0', _base0))
diff --git a/datar/core/frame_format_patch.py b/datar/core/frame_format_patch.py
index a9cb5b1f..d6e96fff 100644
--- a/datar/core/frame_format_patch.py
+++ b/datar/core/frame_format_patch.py
@@ -48,7 +48,7 @@
 
 # TODO: patch more formatters
 
-class DatarDataFrameFormatter(DataFrameFormatter):
+class DatarDataFrameFormatter(DataFrameFormatter): # pragma: no cover
     """Custom formatter for DataFrame
     """
     @property
@@ -96,12 +96,12 @@ def format_col(self, i: int) -> List[str]:
             leading_space=self.index,
         )
 
-class DatarGenericArrayFormatter(GenericArrayFormatter):
+class DatarGenericArrayFormatter(GenericArrayFormatter): # pragma: no cover
     """Generic Array Formatter to show DataFrame element in a cell in a
     collpased representation
     """
 
-    def _format_strings(self) -> List[str]: # pragma: no cover
+    def _format_strings(self) -> List[str]:
         if self.float_format is None:
             float_format = get_option("display.float_format")
             if float_format is None:
@@ -174,7 +174,7 @@ def _format(x):
 
         return fmt_values
 
-class DatarHTMLFormatter(HTMLFormatter):
+class DatarHTMLFormatter(HTMLFormatter): # pragma: no cover
     """Fix nrows as we added one more row (dtype)"""
 
     def _write_regular_rows(
@@ -429,7 +429,7 @@ def render(self) -> List[str]:
 class DatarNotebookFormatter(DatarHTMLFormatter, NotebookFormatter):
     """Notebook Formatter"""
 
-class DatarStringFormatter(StringFormatter):
+class DatarStringFormatter(StringFormatter): # pragma: no cover
     """String Formatter"""
     def to_string(self) -> str:
         """To string representation"""
@@ -448,7 +448,7 @@ def to_string(self) -> str:
 
         return text
 
-def _patch(option: bool) -> None:
+def _patch(option: bool) -> None: # pragma: no cover
     """Patch pandas?"""
     if option:
         # monkey-patch the formatter
diff --git a/datar/core/names.py b/datar/core/names.py
index dac848ab..174ecc4e 100644
--- a/datar/core/names.py
+++ b/datar/core/names.py
@@ -84,7 +84,7 @@ def _repair_names_check_unique(names: Iterable[str]) -> Iterable[str]:
             raise NameNonUniqueError(f"Names must be unique: {name}")
         if name == "" or name is numpy.nan:
             raise NameNonUniqueError(f"Names can't be empty: {name}")
-        if re.search(r'(?:(?<!_)_{1,2}\d+|(?<!_)__)+$', name):
+        if re.search(r'(?:(?<!_)_{2}\d+|(?<!_)__)+$', name):
             raise NameNonUniqueError(
                 f"Names can't be of the form `__` or `_j`: {name}"
             )
@@ -119,7 +119,7 @@ def repair_names(
                 Function accepts a list of names must annotate the first
                 argument with `typing.Iterable` or `typing.Sequence`.
         _base0: Whether the numeric suffix starts from 0 or not.
-            If not specified, will use `datar.base.getOption('index.base.0')`.
+            If not specified, will use `datar.base.get_option('index.base.0')`.
 
     Examples:
         >>> repair_names([None]*3, repair="minimal")
diff --git a/datar/core/operator.py b/datar/core/operator.py
index 4c48493b..83e2e5d9 100644
--- a/datar/core/operator.py
+++ b/datar/core/operator.py
@@ -91,7 +91,8 @@ def ne(self, left: Any, right: Any) -> BoolOrIter:
             neout = ~out
             # neout[pandas.isna(out)] = numpy.nan
             return neout
-        return not out
+        # out is always a numpy.ndarray
+        return not out # pragma: no cover
 
     def __getattr__(self, name: str) -> Any:
         """Other operators"""
diff --git a/datar/core/options.py b/datar/core/options.py
index ab6e870c..449d4a80 100644
--- a/datar/core/options.py
+++ b/datar/core/options.py
@@ -16,7 +16,11 @@
     # Whether use 0-based numbers when index is involved, acts similar like R
     # Otherwise, like python
     index_base_0=False,
+    # Whether which, which_min, which_max is 0-based
+    which_base_0=True,
     dplyr_summarise_inform=True,
+    # whether warn about importing functions that override builtin ones.
+    warn_builtin_names=True,
     add_option=True,
     # allow 'a.b' to access 'a_b'
     diot_transform=_key_transform
@@ -72,7 +76,7 @@ def options_context(**kwargs: Any) -> None:
     options(opts)
 
 # pylint: disable=invalid-name
-def getOption(x: str, default: Any = None) -> Any:
+def get_option(x: str, default: Any = None) -> Any:
     """Get the current value set for option ‘x’,
     or ‘default’ (which defaults to ‘NULL’) if the option is unset.
 
diff --git a/datar/core/types.py b/datar/core/types.py
index ca0929a7..a65fb466 100644
--- a/datar/core/types.py
+++ b/datar/core/types.py
@@ -1,25 +1,31 @@
 """Types for convenience"""
-from typing import Any, Iterable, Union, List
+from typing import Any, Iterable, Union, List, Type, Tuple
 
 # pylint: disable=unused-import
 import numpy
 
 from pandas import Categorical, Series, isnull as is_null
+from pandas.api.types import is_scalar as is_scalar_, is_list_like
 from pandas.core.dtypes.common import is_categorical_dtype as is_categorical
 from pandas._typing import Dtype
 
 # used for type annotations
 NumericType = Union[int, float, complex, numpy.number]
 IntType = Union[int, numpy.integer]
-FloatType = Union[float, numpy.float64]
-SeriesLikeType = Union[Series, List, numpy.ndarray]
+FloatType = Union[float, numpy.float_]
+ComplexType = Union[complex, numpy.complex_]
+StringType = Union[str, numpy.str_]
+ArrayLikeType = Union[Series, List, Tuple, numpy.ndarray]
 CategoricalLikeType = Union[Series, Categorical]
-StringOrIter = Union[str, Iterable[str]]
+StringOrIter = Union[StringType, Iterable[StringType]]
 IntOrIter = Union[IntType, Iterable[IntType]]
 DoubleOrIter = Union[numpy.double, Iterable[numpy.double]]
 BoolOrIter = Union[bool, Iterable[bool]]
 FloatOrIter = Union[FloatType, Iterable[FloatType]]
 NumericOrIter = Union[NumericType, Iterable[NumericType]]
+ComplexOrIter = Union[ComplexType, Iterable[ComplexType]]
+TypeOrIter = Union[Type, Iterable[Type]]
+DtypeOrIter = Union[Dtype, Iterable[Dtype]]
 
 NoneType = type(None)
 # used for type checks
@@ -27,18 +33,12 @@ def is_scalar_int(x: Any) -> bool:
     """Check if a value is an integer"""
     return isinstance(x, (int, numpy.integer))
 
-def is_series_like(x: Any) -> bool:
-    """Check if a value is series like, which can do something in common.
-    For example: .astype, .cumsum, etc
-    """
-    return isinstance(x, (Series, numpy.ndarray))
-
 def is_scalar(x: Any) -> bool:
     """Check if a value is scalar.
 
     None will be counted as scalar
     """
-    ret = numpy.isscalar(x)
+    ret = is_scalar_(x)
     if ret:
         return ret
     try:
diff --git a/datar/core/utils.py b/datar/core/utils.py
index 1a3a5868..645f9c27 100644
--- a/datar/core/utils.py
+++ b/datar/core/utils.py
@@ -13,14 +13,17 @@
 
 import pandas
 from pandas import Categorical, DataFrame, Series
+from pipda import register_func
 from pipda.symbolic import Reference
 
-from varname import argname
-
 from .exceptions import (
     ColumnNotExistingError, DataUnrecyclable, NameNonUniqueError
 )
-from .types import is_iterable, is_scalar, Dtype, is_categorical, is_null
+from .contexts import Context
+from .types import (
+    StringOrIter, Dtype,
+    is_iterable, is_scalar, is_categorical, is_null
+)
 from .defaults import DEFAULT_COLUMN_PREFIX, NA_REPR
 
 # logger
@@ -48,7 +51,7 @@ def vars_select(
         raise_nonexist: Whether raise exception when column not exists
             in the pool
         base0: Whether indexes are 0-based if columns are selected by indexes.
-            If not given, will use `datar.base.getOption('index.base.0')`
+            If not given, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         The selected indexes for columns
@@ -71,34 +74,6 @@ def vars_select(
         )
     return unique(selected).astype(int)
 
-def series_expandable(
-        df_or_series: Union[DataFrame, Series],
-        series_or_df: Union[DataFrame, Series]
-) -> bool:
-    """Check if a series is expandable"""
-    if (not isinstance(df_or_series, (Series, DataFrame)) or
-            not isinstance(series_or_df, (Series, DataFrame))):
-        return False
-
-    if type(df_or_series) is type(series_or_df):
-        if df_or_series.shape[0] < series_or_df.shape[0]:
-            series, df = df_or_series, series_or_df
-        else:
-            df, series = df_or_series, series_or_df
-    elif isinstance(df_or_series, Series):
-        series, df = df_or_series, series_or_df
-    else:
-        df, series = df_or_series, series_or_df
-
-    return series.index.name in df.columns
-
-def series_expand(series: Union[DataFrame, Series], df: DataFrame):
-    """Expand the series to the scale of a dataframe"""
-    if isinstance(series, DataFrame):
-        #assert series.shape[1] == 1
-        series = series.iloc[:, 0]
-    return df[series.index.name].map(series)
-
 def recycle_value(
         value: Any,
         size: int,
@@ -114,6 +89,7 @@ def recycle_value(
     Returns:
         The recycled value
     """
+    # TODO: follow base R's recycling rule? i.e. size 2 -> 4
     from ..base import NA
 
     if is_scalar(value):
@@ -212,15 +188,20 @@ def to_df(data: Any, name: Optional[str] = None) -> DataFrame:
     return DataFrame({name: data})
 
 @to_df.register(numpy.ndarray)
-def _(data: numpy.ndarray, name: Optional[str] = None) -> DataFrame:
+def _(data: numpy.ndarray, name: Optional[StringOrIter] = None) -> DataFrame:
+    if name is not None and is_scalar(name):
+        name = [name]
+
     if len(data.shape) == 1:
-        return DataFrame(data, columns=[name]) if name else DataFrame(data)
+        return (
+            DataFrame(data, columns=name)
+            if name is not None
+            else DataFrame(data)
+        )
 
     ncols = data.shape[1]
-    if isinstance(name, Iterable) and len(name) == ncols:
+    if name is not None and len(name) == ncols:
         return DataFrame(data, columns=name)
-    if len(name) == 1 and name and isinstance(name, str):
-        return DataFrame(data, columns=[name])
     # ignore the name
     return DataFrame(data)
 
@@ -263,9 +244,9 @@ def dict_insert_at(
     for key, val in container.items():
         if key == poskeys[0]:
             matched = True
-            ret_items.extend(value.items())
             if not remove:
                 ret_items_append((key, val))
+            ret_items.extend(value.items())
         elif matched and key in poskeys:
             if not remove:
                 ret_items_append((key, val))
@@ -322,7 +303,12 @@ def name_mutatable_args(
                 if ret_key == key or ret_key.startswith(f"{key}$")
             ]
             if existing_keys:
-                ret = dict_insert_at(ret, existing_keys, val, remove=True)
+                ret = dict_insert_at(
+                    ret,
+                    existing_keys,
+                    {key: val},
+                    remove=True
+                )
             else:
                 for dkey, dval in val.items():
                     ret[f"{key}${dkey}"] = dval
@@ -330,15 +316,19 @@ def name_mutatable_args(
             ret[key] = val
     return ret
 
-def arg_match(arg: Any, values: Iterable[Any], errmsg=Optional[str]) -> Any:
+def arg_match(
+        arg: Any,
+        argname: str,
+        values: Iterable[Any],
+        errmsg: Optional[str] = None
+) -> Any:
     """Make sure arg is in one of the values.
 
     Mimics `rlang::arg_match`.
     """
     if not errmsg:
         values = list(values)
-        name = argname(arg, pos_only=True)
-        errmsg = f'`{name}` must be one of {values}.'
+        errmsg = f'`{argname}` must be one of {values}.'
     if arg not in values:
         raise ValueError(errmsg)
     return arg
@@ -410,13 +400,13 @@ def get_option(key: str, value: Any = None) -> Any:
     This is a shortcut for:
     >>> if value is not None:
     >>>     return value
-    >>> from datar.base import getOption
-    >>> return getOption(key)
+    >>> from datar.base import get_option
+    >>> return get_option(key)
     """
     if value is not None:
         return value
-    from ..base import getOption
-    return getOption(key)
+    from ..base import get_option as get_option_
+    return get_option_(key)
 
 def apply_dtypes(
         df: DataFrame,
@@ -558,10 +548,11 @@ def df_setitem(
     if isinstance(value, Series):
         value = value.values
 
-    if isinstance(value, tuple):
-        # ('A', array([1,2,3]))
-        # VisibleDeprecationWarning
-        value = list(value)
+    # tuple turned into list in recycle_value
+    # if isinstance(value, tuple):
+    #     # ('A', array([1,2,3]))
+    #     # VisibleDeprecationWarning
+    #     value = list(value)
 
     if not allow_dups:
         df[name] = value
@@ -593,7 +584,9 @@ def fillna_safe(data: Iterable, rep: Any = NA_REPR) -> Iterable:
     Raises:
         ValueError: when `rep` exists in data
     """
-    if rep in data:
+    # elementwise comparison failed; returning scalar instead
+    # if rep in data:
+    if rep in list(data):
         raise ValueError("The value to replace NAs is already present in data.")
 
     if not is_null(data).any():
@@ -640,3 +633,68 @@ def length_of(x: Any) -> int:
         return 1
 
     return len(x)
+
+def dedup_name(name: str, all_names: Iterable[str]):
+    """Check if a name is a duplicated name in all_names,
+    return the deduplicated name.
+
+    In other to support duplicated keyword arguments in R:
+        >>> df %>% mutate(a=1, a=a*2)
+
+    Now you can to it with datar:
+        >>> df >> mutate(a_=1, a=f.a*2)
+
+    Args:
+        name: The name to deduplicate
+        all_names: all names
+
+    Returns:
+        The deduplicated name
+    """
+    if not name.endswith('_') or name[:-1] not in all_names:
+        return name
+
+    # now determine whehter the real name is name[:-1]
+    # because the name could be "a__" ("a_" is for sure in all_names)
+    # we need to check if "a" is also in all_names
+    # otherwise, the realname is "a_"
+    name = name[:-1]
+    while name.endswith('_') and name[:-1] in all_names:
+        name = name[:-1]
+    return name
+
+def register_numpy_func_x(
+        name: str,
+        np_name: str,
+        trans_in: Optional[Callable] = None,
+        trans_out: Optional[Callable] = None,
+        doc: str = ""
+) -> Callable:
+    """Register numpy function with single argument x
+
+    Args:
+        name: The name of the function to be returned
+        np_name: The name of the function from numpy
+        trans_in: Transformation for input before sending to numpy function
+        trans_out: Transformation for output
+        doc: The docstring for the function
+
+    Returns:
+        The registered function
+    """
+    func = getattr(numpy, np_name)
+
+    @register_func(None, context=Context.EVAL)
+    def _func(x: Any) -> Any:
+        """Registered function from numpy"""
+        if trans_in:
+            x = trans_in(x)
+
+        out = func(x)
+        if trans_out: # pragma: no cover
+            out = trans_out(out)
+        return out
+
+    _func.__name__ = name
+    _func.__doc__ = doc
+    return _func
diff --git a/datar/core/warn_builtin_names.py b/datar/core/warn_builtin_names.py
new file mode 100644
index 00000000..4a509e69
--- /dev/null
+++ b/datar/core/warn_builtin_names.py
@@ -0,0 +1,66 @@
+"""Let datar warn when builtin names are tried to be imported"""
+import sys
+from typing import Callable, Any
+from types import ModuleType
+from executing import Source
+
+WARNED = set()
+
+def warn_builtin_names(
+        **names: ModuleType
+) -> Callable[[str], Any]:
+    """Generate __getattr__ function to warn the builtin names"""
+    from .utils import logger, get_option
+
+    # Enables tempoarory warn on or off
+    warn = True
+    def _getattr(name: str):
+        """A route to let us check if the function is imported by
+            >>> from module import func
+        But not
+            >>> import module
+            >>> module.func
+
+        If func is a python built-in function, then it gets overriden by datar.
+
+        The side effects:
+        1. This executes every time when `module.func` is called
+        2. When the source is not avaiable for `module.func`, it will be a
+           false alarm treat this as `from module import func`
+        3. It warns even you do `from module import func as alias`
+
+        Instead, you can do `from module import func_` to access function `func`
+
+        Args:
+            **names: The name-module pairs
+
+        Returns:
+            A function that can be used as `__getattr__` for a module
+        """
+        nonlocal warn
+        if name == '_warn':
+            warn = True
+            return None
+        if name == '_no_warn':
+            warn = False
+            return None
+
+        rname = name[:-1] if name.endswith('_') else name
+
+        if name == '__path__' or rname not in names:
+            raise AttributeError
+
+        if rname != name:
+            return getattr(names[rname], rname)
+
+        if warn and name not in WARNED and get_option('warn.builtin.names'):
+            node = Source.executing(sys._getframe(1)).node
+            if not node:
+                WARNED.add(name)
+                logger.warning(
+                    'Builtin name "%s" has been overriden by datar.',
+                    name
+                )
+        return getattr(names[name], name)
+
+    return _getattr
diff --git a/datar/datar/verbs.py b/datar/datar/verbs.py
index 6cd7dddf..7eb5eeb3 100644
--- a/datar/datar/verbs.py
+++ b/datar/datar/verbs.py
@@ -1,5 +1,4 @@
 """Specific verbs from this package"""
-import builtins
 from typing import Any, List
 
 from pandas import DataFrame
@@ -8,7 +7,7 @@
 
 from ..core.types import is_scalar
 from ..core.contexts import Context
-from ..dplyr import select, slice # pylint: disable=redefined-builtin
+from ..dplyr import select, slice_
 
 @register_verb((DataFrame, DataFrameGroupBy), context=Context.SELECT)
 def get(
@@ -49,12 +48,12 @@ def get(
 
     if rows is not None:
         # slice only support integer index
-        if not isinstance(rows, builtins.slice):
+        if not isinstance(rows, slice):
             if is_scalar(rows):
                 rows = [rows]
             if not isinstance(rows[0], int):
                 rows = data.index.get_indexer_for(rows)
-        data = slice(data, rows)
+        data = slice_(data, rows)
     return data
 
 @register_verb(DataFrame)
diff --git a/datar/datasets/__init__.py b/datar/datasets/__init__.py
index cd472d49..4b6d70ad 100644
--- a/datar/datasets/__init__.py
+++ b/datar/datasets/__init__.py
@@ -3,7 +3,6 @@
 from pathlib import Path
 
 import pandas
-from modkit import install
 
 HERE = Path(__file__).parent
 
@@ -38,12 +37,10 @@ def load_data(name: str) -> pandas.DataFrame:
     data.__dfname__ = name
     return data
 
-__all__ = all_datasets().keys()
+__all__ = []
 
 def __getattr__(name):
     # mkapi accesses quite a lot of attributes starting with _
     if name.startswith('_'):
         raise AttributeError
     return load_data(name)
-
-install(__name__)
diff --git a/datar/dplyr/__init__.py b/datar/dplyr/__init__.py
index 1b33ebe4..53f33f9b 100644
--- a/datar/dplyr/__init__.py
+++ b/datar/dplyr/__init__.py
@@ -1,6 +1,6 @@
 """Public APIs for dplyr"""
 
-# pylint: disable=redefined-builtin, unused-import
+# pylint: disable=unused-import
 from .sets import union_all, union, setdiff, intersect, setequal
 from .funs import (
     cummean, cumall, cumany,
@@ -34,10 +34,9 @@
 )
 from .if_else import if_else, case_when
 from .relocate import relocate
-from .filter import filter
 from .distinct import distinct, n_distinct
-from .slice import  (
-    slice, slice_head, slice_tail, slice_min,
+from .dslice import  (
+    slice_head, slice_tail, slice_min,
     slice_max, slice_sample
 )
 from .rank import (
@@ -53,3 +52,18 @@
 from .recode import recode, recode_factor, recode_categorical
 from .order_by import order_by, with_order
 from .rows import rows_insert, rows_update, rows_patch, rows_upsert, rows_delete
+
+# make sure builtin names are included when
+# from datar.dplyr import *
+__all__ = [name for name in locals() if not name.startswith('_')]
+__all__.extend(['filter', 'slice'])
+
+# warn when builtin names are imported directly
+# pylint: disable=wrong-import-position
+from ..core.warn_builtin_names import warn_builtin_names
+from . import dfilter as _dfilter, dslice as _dslice
+
+__getattr__ = warn_builtin_names(
+    filter=_dfilter,
+    slice=_dslice
+)
diff --git a/datar/dplyr/across.py b/datar/dplyr/across.py
index b8613d81..80ff0db5 100644
--- a/datar/dplyr/across.py
+++ b/datar/dplyr/across.py
@@ -179,7 +179,7 @@ def across(
             To use 1-based index, use `{_fn1}`
         _base0: Indicating whether the columns are 0-based if selected
             by indexes. if not provided, will use
-            `datar.base.getOption('index.base.0')`.
+            `datar.base.get_option('index.base.0')`.
         _fn_context: Defines the context to evaluate the arguments for functions
             if they are plain functions.
             Note that registered functions will use its own context
@@ -212,7 +212,7 @@ def c_across(
         _cols: The columns
         _base0: Indicating whether the columns are 0-based if selected
             by indexes. if not provided, will use
-            `datar.base.getOption('index.base.0')`.
+            `datar.base.get_option('index.base.0')`.
 
     Returns:
         A series
diff --git a/datar/dplyr/bind.py b/datar/dplyr/bind.py
index 5d005766..9bdecbb3 100644
--- a/datar/dplyr/bind.py
+++ b/datar/dplyr/bind.py
@@ -40,7 +40,7 @@ def bind_rows(
         _id: The name of the id columns
         _base0: Whether `_id` starts from 0 or not, if no keys are provided.
             If `_base0` is not provided, will use
-            `datar.base.getOption('index.base.0')`
+            `datar.base.get_option('index.base.0')`
         _copy: If `False`, do not copy data unnecessarily.
             Original API does not support this. This argument will be
             passed by to `pandas.concat()` as `copy` argument.
@@ -152,7 +152,7 @@ def bind_cols(
             - "universal": Make the names unique and syntactic
             - a function: apply custom name repair
         _base0: Whether the numeric suffix starts from 0 or not.
-            If not specified, will use `datar.base.getOption('index.base.0')`.
+            If not specified, will use `datar.base.get_option('index.base.0')`.
         _copy: If `False`, do not copy data unnecessarily.
             Original API does not support this. This argument will be
             passed by to `pandas.concat()` as `copy` argument.
diff --git a/datar/dplyr/count_tally.py b/datar/dplyr/count_tally.py
index 6a049595..165fce57 100644
--- a/datar/dplyr/count_tally.py
+++ b/datar/dplyr/count_tally.py
@@ -13,7 +13,7 @@
 from ..core.utils import copy_attrs, logger
 from ..core.defaults import f
 from ..core.grouped import DataFrameGroupBy
-from ..base import options_context, sum # pylint: disable=redefined-builtin
+from ..base import options_context, sum_
 from .context import n
 from .group_by import group_by_drop_default, group_by
 from .group_data import group_data, group_vars
@@ -145,7 +145,7 @@ def _tally_n(
 
     # If it's Expression, will return a Function object
     # Otherwise, sum of wt
-    return sum(wt)
+    return sum_(wt)
 
 def _check_name(name: Optional[str], invars: Iterable[str]) -> str:
     """Check if count is valid"""
diff --git a/datar/dplyr/desc.py b/datar/dplyr/desc.py
index 4d7c110a..4de91e59 100644
--- a/datar/dplyr/desc.py
+++ b/datar/dplyr/desc.py
@@ -4,7 +4,7 @@
 from pipda import register_func
 
 from ..core.contexts import Context
-from ..base.constants import NA
+from ..base import NA
 
 @register_func(None, context=Context.EVAL)
 def desc(x: Iterable[Any]) -> Series:
diff --git a/datar/dplyr/filter.py b/datar/dplyr/dfilter.py
similarity index 100%
rename from datar/dplyr/filter.py
rename to datar/dplyr/dfilter.py
diff --git a/datar/dplyr/slice.py b/datar/dplyr/dslice.py
similarity index 98%
rename from datar/dplyr/slice.py
rename to datar/dplyr/dslice.py
index c2e178f7..d8545f41 100644
--- a/datar/dplyr/slice.py
+++ b/datar/dplyr/dslice.py
@@ -13,9 +13,8 @@
 from ..core.utils import copy_attrs, reconstruct_tibble
 from ..core.grouped import DataFrameGroupBy
 
-from ..base.constants import NA
-from ..base import unique
-from .filter import _filter_groups
+from ..base import NA, unique
+from .dfilter import _filter_groups
 
 
 @register_verb(DataFrame, context=Context.SELECT)
@@ -49,7 +48,7 @@ def slice( # pylint: disable=redefined-builtin
             recalculated based on the resulting data,
             otherwise the grouping is kept as is.
         _base0: If rows are selected by indexes, whether they are 0-based.
-            If not provided, `datar.base.getOption('index.base.0')` is used.
+            If not provided, `datar.base.get_option('index.base.0')` is used.
 
     Returns:
         The sliced dataframe
diff --git a/datar/dplyr/funs.py b/datar/dplyr/funs.py
index b38a4f23..4ee1d90a 100644
--- a/datar/dplyr/funs.py
+++ b/datar/dplyr/funs.py
@@ -11,7 +11,7 @@
 )
 from ..core.contexts import Context
 from ..core.utils import position_at, Array
-from ..base.constants import NA
+from ..base import NA
 
 @register_func(None, context=Context.EVAL)
 def between(
diff --git a/datar/dplyr/group_by.py b/datar/dplyr/group_by.py
index eae1e582..37ea9e8a 100644
--- a/datar/dplyr/group_by.py
+++ b/datar/dplyr/group_by.py
@@ -83,7 +83,7 @@ def rowwise(
             This is typically a set of variables whose combination
             uniquely identify each row.
         _base0: Whether indexes are 0-based if columns are selected by indexes.
-            If not given, will use `datar.base.getOption('index.base.0')`
+            If not given, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         A row-wise data frame
@@ -137,7 +137,7 @@ def ungroup(
         x: The data frame
         *cols: Variables to remove from the grouping variables.
         _base0: If columns are selected with indexes, whether they are 0-based.
-            If not given, will use `datar.base.getOption('index.base.0')`
+            If not given, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         A data frame with selected columns removed from the grouping variables.
diff --git a/datar/dplyr/if_else.py b/datar/dplyr/if_else.py
index d223665a..fae0de3c 100644
--- a/datar/dplyr/if_else.py
+++ b/datar/dplyr/if_else.py
@@ -12,7 +12,7 @@
 from ..core.contexts import Context
 from ..core.types import is_iterable, is_scalar, is_null
 from ..core.utils import Array
-from ..base.constants import NA
+from ..base import NA
 
 @register_func(None, context=Context.EVAL)
 def if_else(
diff --git a/datar/dplyr/join.py b/datar/dplyr/join.py
index edc989de..69e05cda 100644
--- a/datar/dplyr/join.py
+++ b/datar/dplyr/join.py
@@ -14,7 +14,7 @@
 from ..base import intersect, setdiff, union
 from .group_by import group_by_drop_default
 from .group_data import group_data, group_vars
-from .filter import filter # pylint: disable=redefined-builtin
+from .dfilter import filter as dfilter
 
 def _join(
         x: DataFrame,
@@ -303,7 +303,7 @@ def get_nested_df(row: Series) -> DataFrame:
                 condition = y[on[key]] == row[key]
             else:
                 condition = condition & (y[on[key]] == row[key])
-        df = filter(y, condition)
+        df = dfilter(y, condition)
         if not keep:
             df = df[setdiff(df.columns, on.values())]
 
diff --git a/datar/dplyr/lead_lag.py b/datar/dplyr/lead_lag.py
index a83a6e4f..8cb27a57 100644
--- a/datar/dplyr/lead_lag.py
+++ b/datar/dplyr/lead_lag.py
@@ -10,7 +10,7 @@
 
 from ..core.types import NumericType, is_scalar
 from ..core.contexts import Context
-from ..base.constants import NA
+from ..base import NA
 from .order_by import with_order
 
 @register_func(None, context=Context.EVAL)
diff --git a/datar/dplyr/mutate.py b/datar/dplyr/mutate.py
index 92eac01f..31d698e9 100644
--- a/datar/dplyr/mutate.py
+++ b/datar/dplyr/mutate.py
@@ -9,7 +9,7 @@
 
 from ..core.contexts import Context, ContextEval
 from ..core.utils import (
-    recycle_value, arg_match, df_setitem,
+    dedup_name, recycle_value, arg_match, df_setitem,
     name_mutatable_args, reconstruct_tibble
 )
 from ..core.defaults import DEFAULT_COLUMN_PREFIX
@@ -55,7 +55,7 @@ def mutate(
             (the default is to add to the right hand side).
             See relocate() for more details.
         _base0: Whether `_before` and `_after` are 0-based if given by indexes.
-            If not provided, will use `datar.base.getOption('index.base.0')`
+            If not provided, will use `datar.base.get_option('index.base.0')`
         *args: and
         **kwargs: Name-value pairs. The name gives the name of the column
             in the output. The value can be:
@@ -80,6 +80,7 @@ def mutate(
     """
     keep = arg_match(
         _keep,
+        '_keep',
         ['all', 'unused', 'used', 'none']
     )
 
@@ -204,37 +205,49 @@ def _mutate_cols(
     named_mutatables = name_mutatable_args(*args, **kwargs)
     new_columns = []
     removed = []
+    add_new_name = True
     for name, mutatable in named_mutatables.items():
+        ddp_name = dedup_name(name, list(named_mutatables))
+        # if not a dedup name, it's a new name
+        add_new_name = ddp_name == name
+
         mutatable = evaluate_expr(mutatable, data, context)
         if mutatable is None:
-            if name in data:
-                removed.append(name)
-                data.drop(columns=[name], inplace=True)
+            if ddp_name in data:
+                removed.append(ddp_name)
+                data.drop(columns=[ddp_name], inplace=True)
             # be silent if name doesn't exist
             continue
 
         if isinstance(mutatable, DataFrame):
             if (
                     mutatable.shape[1] == 0 and
-                    not name.startswith(DEFAULT_COLUMN_PREFIX)
+                    not ddp_name.startswith(DEFAULT_COLUMN_PREFIX)
             ):
                 data = df_setitem(
-                    data, name, [NA] * max(mutatable.shape[0], 1)
+                    data, ddp_name, [NA] * max(mutatable.shape[0], 1)
                 )
-                new_columns.append(name)
+                if add_new_name:
+                    new_columns.append(ddp_name)
             else:
                 for col in mutatable.columns:
                     new_name = (
-                        col if name.startswith(DEFAULT_COLUMN_PREFIX)
-                        else f'{name}${col}'
+                        col if ddp_name.startswith(DEFAULT_COLUMN_PREFIX)
+                        else f'{ddp_name}${col}'
+                    )
+                    coldata = recycle_value(
+                        mutatable[col], data.shape[0], ddp_name
                     )
-                    coldata = recycle_value(mutatable[col], data.shape[0], name)
                     data = df_setitem(data, new_name, coldata)
-                    new_columns.append(new_name)
+
+                    if add_new_name:
+                        new_columns.append(new_name)
         else:
-            mutatable = recycle_value(mutatable, data.shape[0], name)
-            data = df_setitem(data, name, mutatable)
-            new_columns.append(name)
+            mutatable = recycle_value(mutatable, data.shape[0], ddp_name)
+            data = df_setitem(data, ddp_name, mutatable)
+
+            if add_new_name:
+                new_columns.append(ddp_name)
 
     # keep column order
     return data[new_columns], removed
diff --git a/datar/dplyr/pull.py b/datar/dplyr/pull.py
index d546d661..61ef8e33 100644
--- a/datar/dplyr/pull.py
+++ b/datar/dplyr/pull.py
@@ -10,7 +10,7 @@
 
 from ..core.contexts import Context
 from ..core.utils import arg_match, df_getitem, position_at
-from ..core.types import StringOrIter, SeriesLikeType, is_scalar
+from ..core.types import StringOrIter, ArrayLikeType, is_scalar
 
 @register_verb(
     DataFrame,
@@ -23,7 +23,7 @@ def pull(
         name: Optional[StringOrIter] = None,
         to: Optional[str] = None,
         _base0: Optional[bool] = None
-) -> Union[DataFrame, SeriesLikeType, Mapping[str, SeriesLikeType]]:
+) -> Union[DataFrame, ArrayLikeType, Mapping[str, ArrayLikeType]]:
     # pylint: disable=too-many-branches
     """Pull a series or a dataframe from a dataframe
 
@@ -53,7 +53,7 @@ def pull(
                 `dict` if `name` provided and has the same length as the pulled
                 single column. Otherwise `frame`.
         _base0: Whether `var` is 0-based if given by index
-            If not provided, `datar.base.getOption('index.base.0')` is used.
+            If not provided, `datar.base.get_option('index.base.0')` is used.
 
     Returns:
         The data according to `to`
@@ -61,7 +61,7 @@ def pull(
     # make sure pull(df, 'x') pulls a dataframe for columns
     # x$a, x$b in df
 
-    to = arg_match(to, ['list', 'array', 'frame', 'series', 'dict', None])
+    to = arg_match(to, 'to', ['list', 'array', 'frame', 'series', 'dict', None])
     if name is not None and is_scalar(name):
         name = [name]
 
diff --git a/datar/dplyr/rank.py b/datar/dplyr/rank.py
index e2d570b0..8be69416 100644
--- a/datar/dplyr/rank.py
+++ b/datar/dplyr/rank.py
@@ -1,6 +1,6 @@
 """Windowed rank functions.
 
-See https://github.com/tidyverse/dplyr/blob/master/R/slice.R
+See https://github.com/tidyverse/dplyr/blob/master/R/rank.R
 """
 from typing import Any, Iterable, Optional
 
@@ -13,7 +13,7 @@
 from ..core.contexts import Context
 from ..core.types import is_null
 from ..core.utils import Array
-from ..base.funcs import NA
+from ..base import NA
 
 @register_func(context=Context.EVAL)
 def row_number(
diff --git a/datar/dplyr/recode.py b/datar/dplyr/recode.py
index 1e011278..484bfea5 100644
--- a/datar/dplyr/recode.py
+++ b/datar/dplyr/recode.py
@@ -12,7 +12,8 @@
 from ..core.contexts import Context
 from ..core.utils import logger, get_option, Array
 from ..core.types import is_not_null, is_scalar, is_null, is_categorical
-from ..base import NA, unique, c, intersect, NA_integer_, NA_character_
+from ..base import NA, unique, c, intersect
+from ..base.na import NA_integer_, NA_character_
 
 def _get_first(x: Iterable[Any]) -> Any:
     """Get first raw item from an iterable"""
diff --git a/datar/dplyr/relocate.py b/datar/dplyr/relocate.py
index c92ce7af..37f2a25b 100644
--- a/datar/dplyr/relocate.py
+++ b/datar/dplyr/relocate.py
@@ -31,7 +31,7 @@ def relocate(
         _after: Destination. Supplying neither will move columns to
             the left-hand side; specifying both is an error.
         _base0: Whether `_before` and `_after` are 0-based if given by indexes.
-            If not provided, will use `datar.base.getOption('index.base.0')`
+            If not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         An object of the same type as .data. The output has the following
diff --git a/datar/dplyr/rename.py b/datar/dplyr/rename.py
index 9669df01..108c0799 100644
--- a/datar/dplyr/rename.py
+++ b/datar/dplyr/rename.py
@@ -26,7 +26,7 @@ def rename(
         _data: The dataframe
         **kwargs: The new_name = old_name pairs
         _base0: Whether the old_name is 0-based if given by indexes.
-            If not provided, will use `datar.base.getOption('index.base.0')`
+            If not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         The dataframe with new names
@@ -75,7 +75,7 @@ def rename_with(
             keyword arguments instead.
         **kwargs: keyword arguments for `_fn`
         _base0: Whether the old_name is 0-based if given by indexes.
-            If not provided, will use `datar.base.getOption('index.base.0')`
+            If not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         The dataframe with new names
diff --git a/datar/dplyr/select.py b/datar/dplyr/select.py
index b3ae69d0..371df317 100644
--- a/datar/dplyr/select.py
+++ b/datar/dplyr/select.py
@@ -38,7 +38,7 @@ def select(
         *columns: The columns to select
         **renamings: The columns to rename and select in new => old column way.
         _base0: Whether the columns are 0-based if given by indexes
-            If not provided, will use `datar.base.getOption('index.base.0')`
+            If not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         The dataframe with select columns
diff --git a/datar/dplyr/summarise.py b/datar/dplyr/summarise.py
index 364d8ec3..eb7ddbc1 100644
--- a/datar/dplyr/summarise.py
+++ b/datar/dplyr/summarise.py
@@ -9,7 +9,7 @@
 from ..core.contexts import Context
 from ..core.utils import (
     length_of, recycle_df, arg_match, check_column_uniqueness, df_setitem,
-    name_mutatable_args, logger, get_option, reconstruct_tibble
+    name_mutatable_args, logger, get_option, reconstruct_tibble, dedup_name
 )
 from ..core.exceptions import ColumnNotExistingError
 from ..core.grouped import DataFrameGroupBy, DataFrameRowwise
@@ -72,7 +72,11 @@ def summarise(
         _data,
         "Can't transform a data frame with duplicate names"
     )
-    _groups = arg_match(_groups, ['drop', 'drop_last', 'keep', 'rowwise', None])
+    _groups = arg_match(
+        _groups,
+        '_groups',
+        ['drop', 'drop_last', 'keep', 'rowwise', None]
+    )
     out = _summarise_build(_data, *args, **kwargs)
     if _groups == 'rowwise':
         return DataFrameRowwise(out, _group_drop=group_by_drop_default(_data))
@@ -86,7 +90,11 @@ def _(
         **kwargs: Any
 ) -> DataFrame:
     # empty
-    _groups = arg_match(_groups, ['drop', 'drop_last', 'keep', 'rowwise', None])
+    _groups = arg_match(
+        _groups,
+        '_groups',
+        ['drop', 'drop_last', 'keep', 'rowwise', None]
+    )
 
     allone = True
     if group_data(_data).shape[0] == 0:
@@ -155,6 +163,9 @@ def _summarise_build(
 
     out = group_keys(_data)
     for key, val in named.items():
+        # support: df %>% muate(a=1, a=a+1)
+        key = dedup_name(key, list(named))
+
         envdata = out
         if out.shape[1] == 0 or (
                 isinstance(val, Function) and
diff --git a/datar/dplyr/tidyselect.py b/datar/dplyr/tidyselect.py
index 2242c1dc..b92cce58 100644
--- a/datar/dplyr/tidyselect.py
+++ b/datar/dplyr/tidyselect.py
@@ -1,14 +1,14 @@
 """Select helpers"""
 import re
+import builtins
 from typing import Callable, Iterable, List, Optional, Union
 
-import numpy
 from pandas import DataFrame
 from pipda import register_func
 from pipda.utils import functype
 
 from ..core.contexts import Context
-from ..core.utils import get_option, vars_select
+from ..core.utils import get_option, vars_select, Array
 from ..core.types import StringOrIter
 from ..base import setdiff, intersect
 from .group_data import group_vars
@@ -200,7 +200,7 @@ def all_of(
         _data: The data piped in
         x: A set of variables to match the columns
         _base0: Whether `x` is 0-based or not.
-            if not provided, will use `datar.base.getOption('index.base.0')`
+            if not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         The matched column names
@@ -239,7 +239,7 @@ def any_of(
         _data: The data piped in
         x: A set of variables to match the columns
         _base0: Whether `x` is 0-based or not.
-            if not provided, will use `datar.base.getOption('index.base.0')`
+            if not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         The matched column names
@@ -258,7 +258,7 @@ def any_of(
 @register_func(None)
 def num_range(
         prefix: str,
-        range_: Iterable[int], # pylint: disable=redefined-builtin
+        range: Iterable[int], # pylint: disable=redefined-builtin
         width: Optional[int] = None,
         _base0: Optional[bool] = None
 ) -> List[str]:
@@ -281,7 +281,10 @@ def num_range(
         if not width
         else str(elem + int(not _base0)).zfill(width)
     )
-    return numpy.array([f"{prefix}{zfill(elem)}" for elem in range(range_)])
+    return Array([
+        f"{prefix}{zfill(elem)}"
+        for elem in builtins.range(range)
+    ])
 
 def _filter_columns(
         all_columns: Iterable[str],
diff --git a/datar/stats/__init__.py b/datar/stats/__init__.py
index 573a5692..69ea1411 100644
--- a/datar/stats/__init__.py
+++ b/datar/stats/__init__.py
@@ -1,4 +1,4 @@
 """APIs for stats"""
 
-from .funcs import rnorm, rpois, runif, quantile, sd, weighted_mean
-from .verbs import setNames
+from .funs import rnorm, rpois, runif, quantile, sd, weighted_mean
+from .verbs import set_names
diff --git a/datar/stats/funcs.py b/datar/stats/funs.py
similarity index 94%
rename from datar/stats/funcs.py
rename to datar/stats/funs.py
index d9940feb..187e0e0e 100644
--- a/datar/stats/funcs.py
+++ b/datar/stats/funs.py
@@ -5,9 +5,11 @@
 from pipda import register_func
 
 from ..core.types import (
-    FloatOrIter, NumericOrIter, NumericType, SeriesLikeType, is_scalar
+    FloatOrIter, NumericOrIter, NumericType, ArrayLikeType, is_scalar
 )
 from ..core.contexts import Context
+from ..core.utils import Array
+from ..base import NA
 
 # pylint: disable=redefined-builtin, redefined-outer-name
 
@@ -59,7 +61,7 @@ def quantile(
         series: Iterable[Any],
         probs: FloatOrIter = (0.0, 0.25, 0.5, 0.75, 1.0),
         na_rm: bool = False
-) -> SeriesLikeType:
+) -> ArrayLikeType:
     """produces sample quantiles corresponding to the given probabilities.
 
     Args:
@@ -101,9 +103,9 @@ def weighted_mean(
         x = [x]
     if w is not None and is_scalar(w):
         w = [w]
-    x = numpy.array(x)
+    x = Array(x)
     if w is not None:
-        w = numpy.array(w)
+        w = Array(w)
         if len(x) != len(w):
             raise ValueError("'x' and 'w' must have the same length")
 
@@ -114,5 +116,5 @@ def weighted_mean(
             w = w[notna]
 
     if w is not None and sum(w) == 0:
-        return numpy.nan
+        return NA
     return numpy.average(x, weights=w)
diff --git a/datar/stats/verbs.py b/datar/stats/verbs.py
index 7c825f10..ec98d285 100644
--- a/datar/stats/verbs.py
+++ b/datar/stats/verbs.py
@@ -5,11 +5,25 @@
 from pipda import register_verb
 
 from ..core.defaults import NA_REPR
-from ..dplyr.rename import rename
+from ..core.grouped import DataFrameGroupBy
 
 # pylint: disable=invalid-name
 @register_verb(DataFrame)
-def setNames(obj: DataFrame, names: Iterable[str]) -> DataFrame:
+def set_names(obj: DataFrame, names: Iterable[str]) -> DataFrame:
     """Set names of a dataframe"""
     names = [NA_REPR if isna(name) else name for name in names]
-    return rename(obj, **dict(zip(names, obj.columns)))
+    obj = obj.copy()
+    obj.columns = names
+    return obj
+
+@set_names.register(DataFrameGroupBy)
+def _(obj: DataFrameGroupBy, names: Iterable[str]) -> DataFrameGroupBy:
+    """Set names of a grouped/rowwise df"""
+    obj = obj.copy()
+    names_dict = dict(zip(obj.columns, names))
+    obj.columns = names
+    gvars = [names_dict[name] for name in obj._group_vars]
+    obj._group_vars = gvars
+    obj._group_data = obj._group_data.copy()
+    obj._group_data.columns = gvars + [obj._group_data.columns[-1]]
+    return obj
diff --git a/datar/tibble/tibble.py b/datar/tibble/tibble.py
index 791219b6..a838e25b 100644
--- a/datar/tibble/tibble.py
+++ b/datar/tibble/tibble.py
@@ -44,7 +44,7 @@ def tibble(
         _rows: Number of rows of a 0-col dataframe when args and kwargs are
             not provided. When args or kwargs are provided, this is ignored.
         _base0: Whether the suffixes of repaired names should be 0-based.
-            If not provided, will use `datar.base.getOption('index.base.0')`.
+            If not provided, will use `datar.base.get_option('index.base.0')`.
 
     Returns:
         A constructed dataframe
@@ -82,16 +82,13 @@ def tibble(
     names.extend(kwargs)
     values.extend(kwargs.values())
 
-    if not names:
-        out = DataFrame()
-    else:
-        out = zibble(
-            names,
-            values,
-            _name_repair=_name_repair,
-            _base0=_base0,
-            _dtypes=_dtypes
-        )
+    out = zibble(
+        names,
+        values,
+        _name_repair=_name_repair,
+        _base0=_base0,
+        _dtypes=_dtypes
+    )
 
     try:
         out.__dfname__ = varname(raise_exc=False)
@@ -125,7 +122,7 @@ def tibble_row(
             - "universal": Make the names unique and syntactic
             - a function: apply custom name repair
         _base0: Whether the suffixes of repaired names should be 0-based.
-            If not provided, will use `datar.base.getOption('index.base.0')`.
+            If not provided, will use `datar.base.get_option('index.base.0')`.
 
     Returns:
         A constructed dataframe
@@ -281,7 +278,7 @@ def zibble(
             - "universal": Make the names unique and syntactic
             - a function: apply custom name repair
         _base0: Whether the suffixes of repaired names should be 0-based.
-            If not provided, will use `datar.base.getOption('index.base.0')`.
+            If not provided, will use `datar.base.get_option('index.base.0')`.
 
     Returns:
         A data frame
diff --git a/datar/tibble/verbs.py b/datar/tibble/verbs.py
index 63f3bff0..200acc34 100644
--- a/datar/tibble/verbs.py
+++ b/datar/tibble/verbs.py
@@ -169,7 +169,7 @@ def add_column(
         _after: Column index or name where to add the new columns
             (default to add after the last column)
         _base0: Whether `_before` and `_after` are 0-based if they are index.
-            if not given, will be determined by `getOption('index_base_0')`,
+            if not given, will be determined by `get_option('index_base_0')`,
             which is `False` by default.
         _dtypes: The dtypes for the new columns, either a uniform dtype or a
             dict of dtypes with keys the column names
diff --git a/datar/tidyr/chop.py b/datar/tidyr/chop.py
index 09845cef..91906057 100644
--- a/datar/tidyr/chop.py
+++ b/datar/tidyr/chop.py
@@ -38,7 +38,7 @@ def chop(
         data: A data frame
         cols: Columns to chop
         _base0: Whether `cols` are 0-based
-            if not provided, will use `datar.base.getOption('index.base.0')`
+            if not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         Data frame with selected columns chopped
@@ -116,7 +116,7 @@ def unchop(
             is used as key, all columns of the nested data frames will be casted
             into that dtype.
         _base0: Whether `cols` are 0-based
-            if not provided, will use `datar.base.getOption('index.base.0')`
+            if not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         A data frame with selected columns unchopped.
@@ -143,9 +143,9 @@ def _vec_split(
     Returns a data frame with columns `key` and `val`. `key` is a stacked column
     with data from by.
     """
-    if isinstance(x, Series):
+    if isinstance(x, Series): # pragma: no cover, always a data frame?
         x = x.to_frame()
-    if isinstance(by, Series):
+    if isinstance(by, Series): # pragma: no cover, always a data frame?
         by = by.to_frame()
 
     df = bind_cols(x, by)
@@ -235,8 +235,9 @@ def _unchopping_df_column(
                 for col in val:
                     # pylint: disable=unsupported-membership-test
                     # pylint: disable=unsupported-delete-operation
+                    # TODO: test
                     if col in dtypes and dtypes[col] != val[col].dtype:
-                        del dtypes[col]
+                        del dtypes[col] # pragma: no cover
 
     sizes = []
     val_data = defaultdict(list)
diff --git a/datar/tidyr/drop_na.py b/datar/tidyr/drop_na.py
index 94d6742b..61af591d 100644
--- a/datar/tidyr/drop_na.py
+++ b/datar/tidyr/drop_na.py
@@ -29,12 +29,12 @@ def drop_na(
             - any: Any columns of `columns` to be `NA`s
             (tidyr doesn't support this argument)
         _base0: Whether `*columns` are 0-based if given by indexes
-            If not provided, will use `datar.base.getOption('index.base.0')`
+            If not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         Dataframe with rows with NAs dropped and indexes dropped
     """
-    arg_match(how, ['any', 'all'])
+    arg_match(how, 'how', ['any', 'all'])
     all_columns = _data.columns
     if columns:
         columns = vars_select(all_columns, *columns, base0=_base0)
diff --git a/datar/tidyr/expand.py b/datar/tidyr/expand.py
index 2ad74e30..202c6f31 100644
--- a/datar/tidyr/expand.py
+++ b/datar/tidyr/expand.py
@@ -45,7 +45,7 @@ def expand_grid(
             - "universal": Make the names unique and syntactic
             - a function: apply custom name repair
         _base0: Whether the suffixes of repaired names should be 0-based.
-            If not provided, will use `datar.base.getOption('index.base.0')`.
+            If not provided, will use `datar.base.get_option('index.base.0')`.
 
     Returns:
         A data frame with one column for each input in `*args` and `**kwargs`.
@@ -118,7 +118,7 @@ def expand(
             - "universal": Make the names unique and syntactic
             - a function: apply custom name repair
         _base0: Whether the suffixes of repaired names should be 0-based.
-            If not provided, will use `datar.base.getOption('index.base.0')`.
+            If not provided, will use `datar.base.get_option('index.base.0')`.
 
     Returns:
         A data frame with all combination of variables.
@@ -202,7 +202,7 @@ def nesting(
             - "universal": Make the names unique and syntactic
             - a function: apply custom name repair
         _base0: Whether the suffixes of repaired names should be 0-based.
-            If not provided, will use `datar.base.getOption('index.base.0')`.
+            If not provided, will use `datar.base.get_option('index.base.0')`.
 
     Returns:
         A data frame with all combinations in data.
@@ -246,7 +246,7 @@ def crossing(
             - "universal": Make the names unique and syntactic
             - a function: apply custom name repair
         _base0: Whether the suffixes of repaired names should be 0-based.
-            If not provided, will use `datar.base.getOption('index.base.0')`.
+            If not provided, will use `datar.base.get_option('index.base.0')`.
 
     Returns:
         A data frame with values deduplicated and sorted.
diff --git a/datar/tidyr/extract.py b/datar/tidyr/extract.py
index cb50d62a..055c8e55 100644
--- a/datar/tidyr/extract.py
+++ b/datar/tidyr/extract.py
@@ -42,7 +42,7 @@ def extract(
         convert: The universal type for the extracted columns or a dict for
             individual ones
         _base0: Whether `col` is 0-based when given by index
-            If not provided, will use `datar.base.getOption('index.base.0')`
+            If not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         Dataframe with extracted columns.
diff --git a/datar/tidyr/fill.py b/datar/tidyr/fill.py
index f8d0c03d..3b1e126d 100644
--- a/datar/tidyr/fill.py
+++ b/datar/tidyr/fill.py
@@ -36,7 +36,7 @@ def fill(
             "downup" (i.e. first down and then up) or
             "updown" (first up and then down).
         _base0: Whether `*columns` are 0-based if given by indexes
-            If not provided, will use `datar.base.getOption('index.base.0')`
+            If not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         The dataframe with NAs being replaced.
diff --git a/datar/tidyr/nest.py b/datar/tidyr/nest.py
index 0b3f7363..aaed353c 100644
--- a/datar/tidyr/nest.py
+++ b/datar/tidyr/nest.py
@@ -39,7 +39,7 @@ def nest(
             together the outer and the inner column names, separated by
             `_names_sep`.
         _base0: Whether `**cols` are 0-based
-            if not provided, will use `datar.base.getOption('index.base.0')`
+            if not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         Nested data frame.
@@ -140,7 +140,7 @@ def unnest(
             - "universal": Make the names unique and syntactic
             - a function: apply custom name repair
         _base0: Whether `cols` are 0-based
-            if not provided, will use `datar.base.getOption('index.base.0')`
+            if not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         Data frame with selected columns unnested.
diff --git a/datar/tidyr/pack.py b/datar/tidyr/pack.py
index e1cfa0f9..dd3341f1 100644
--- a/datar/tidyr/pack.py
+++ b/datar/tidyr/pack.py
@@ -34,7 +34,7 @@ def pack(
             together the outer and the inner column names, separated by
             `_names_sep`.
         _base0: Whether `**cols` are 0-based
-            if not provided, will use `datar.base.getOption('index.base.0')`
+            if not provided, will use `datar.base.get_option('index.base.0')`
     """
     if not cols:
         return _data.copy()
@@ -90,7 +90,7 @@ def unpack(
             - "universal": Make the names unique and syntactic
             - a function: apply custom name repair
         _base0: Whether `cols` are 0-based
-            if not provided, will use `datar.base.getOption('index.base.0')`
+            if not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         Data frame with given columns unpacked.
diff --git a/datar/tidyr/pivot_long.py b/datar/tidyr/pivot_long.py
index d3e13e7a..d07ed505 100644
--- a/datar/tidyr/pivot_long.py
+++ b/datar/tidyr/pivot_long.py
@@ -134,7 +134,7 @@ def pivot_longer(
             - "universal": Make the names unique and syntactic
             - a function: apply custom name repair
         _base0: Whether `cols` are 0-based if given by indexes
-            If not provided, will use `datar.base.getOption('index.base.0')`
+            If not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         The pivoted dataframe.
diff --git a/datar/tidyr/pivot_wide.py b/datar/tidyr/pivot_wide.py
index 8b67c893..c44deeda 100644
--- a/datar/tidyr/pivot_wide.py
+++ b/datar/tidyr/pivot_wide.py
@@ -12,7 +12,7 @@
 from ..core.exceptions import ColumnNotExistingError
 
 from ..base import NA, identity
-from ..base.constants import NA_integer_
+from ..base.na import NA_integer_
 
 ROWID_COLUMN = '_PIVOT_ROWID_'
 
@@ -67,7 +67,7 @@ def pivot_wider(
             If not specified, will be `numpy.mean`
         _base0: Whether `id_cols`, `names_from` and `values_from`
             are 0-based if given by indexes.
-            If not provided, will use `datar.base.getOption('index.base.0')`
+            If not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         The pivoted dataframe.
diff --git a/datar/tidyr/replace_na.py b/datar/tidyr/replace_na.py
index cb628dd7..a6397e20 100644
--- a/datar/tidyr/replace_na.py
+++ b/datar/tidyr/replace_na.py
@@ -8,7 +8,7 @@
 from pandas.core.series import Series
 from pipda import register_verb
 
-from ..core.types import SeriesLikeType, is_scalar
+from ..core.types import ArrayLikeType, is_scalar
 from ..core.contexts import Context
 
 @singledispatch
@@ -21,7 +21,7 @@ def _replace_na(data: Iterable[Any], replace: Any) -> Iterable[Any]:
 
 @_replace_na.register(numpy.ndarray)
 @_replace_na.register(Series)
-def _(data: SeriesLikeType, replace: Any) -> SeriesLikeType:
+def _(data: ArrayLikeType, replace: Any) -> ArrayLikeType:
     """Replace NA for numpy.ndarray or Series"""
     ret = data.copy()
     ret[pandas.isnull(ret)] = replace
diff --git a/datar/tidyr/separate.py b/datar/tidyr/separate.py
index 36ce1077..b2570c45 100644
--- a/datar/tidyr/separate.py
+++ b/datar/tidyr/separate.py
@@ -64,7 +64,7 @@ def separate(
             - "left": fill with missing values on the left
         _base0: Whether `col` is 0-based when given by index and Whether `sep`
             is 0-based if given by position
-            If not provided, will use `datar.base.getOption('index.base.0')`
+            If not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         Dataframe with separated columns.
@@ -144,7 +144,7 @@ def separate_rows(
         convert: The universal type for the extracted columns or a dict for
             individual ones
         _base0: Whether `columns` is 0-based when given by index
-            If not provided, will use `datar.base.getOption('index.base.0')`
+            If not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         Dataframe with rows separated and repeated.
diff --git a/datar/tidyr/uncount.py b/datar/tidyr/uncount.py
index 2ce974ab..5fb22be3 100644
--- a/datar/tidyr/uncount.py
+++ b/datar/tidyr/uncount.py
@@ -32,7 +32,7 @@ def uncount(
         _id: Supply a string to create a new variable which gives a
             unique identifier for each created row (0-based).
         _base0: Whether the generated `_id` columns are 0-based.
-            If not provided, will use `datar.base.getOption('index.base.0')`
+            If not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         dataframe with rows repeated.
diff --git a/datar/tidyr/unite.py b/datar/tidyr/unite.py
index a60d7ac3..781d143f 100644
--- a/datar/tidyr/unite.py
+++ b/datar/tidyr/unite.py
@@ -32,7 +32,7 @@ def unite(
         na_rm: If True, missing values will be remove prior to uniting
             each value.
         _base0: Whether `columns` is 0-based when given by index
-            If not provided, will use `datar.base.getOption('index.base.0')`
+            If not provided, will use `datar.base.get_option('index.base.0')`
 
     Returns:
         The dataframe with selected columns united
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index b663bf43..bc77d172 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,3 +1,20 @@
+## 0.2.0
+Added:
+- Add `base.which`, `base.bessel`, `base.special`, `base.trig_hb` and `base.string` modules
+- Add Support for duplicated keyword arguments for dplyr.mutate/summarise by using `_` as suffix
+- Warn when import python builtin names directly; ; Remove modkit dependency
+
+Fixed:
+- Fixed errors when use`a_1` as names for `"check_unique"` name repairs
+- Fixed #14: `f.a.mean()` not applied to grouped data
+
+Changed:
+- Don't allow `from datar.datasets import *`
+- Remove `modkit` dependency
+- Reset `NaN` to `NA`
+- Rename `base.getOption` to `base.get_option`
+- Rename `stats.setNames` to `stats.set_names`
+
 ## 0.1.1
 - Adopt `pipda` 0.2.8
 - Allow `f.col1[f.col2==max(f.col2)]` like expression
@@ -27,7 +44,7 @@ Changed:
 - Finish tidyr porting.
 
 ## 0.0.6
-- Add `options`, `getOption` and `options_context` to `datar.base` to allow set/get global options
+- Add `options`, `get_option` and `options_context` to `datar.base` to allow set/get global options
 - Add options: `dplyr.summarise.inform`
 - Add `_base0` argument to all related APIs
 - Add `nycflights13` datasets
diff --git a/docs/import.md b/docs/import.md
index 2aeb12a2..30bfaab4 100644
--- a/docs/import.md
+++ b/docs/import.md
@@ -40,12 +40,79 @@ from datar.all import mutate
 
 ## Import datasets from datar
 
-Note that `from datar.all import *` will not import datasets
-
 !!! note
 
-    Dataset has to be imported individually. So that `from datar.datasets import *` won't work.
+    Dataset has to be imported individually. This means  `from datar.datasets import *` won't work (you don't want all datasets to exhaust your memory).
 
 You don't have to worry about other datasets to be imported and take up the memory when you import one. The dataset is only loaded into memory when you explictly import it individually.
 
 See also [datasets](../datasets) for details about available datasets.
+
+## Warn about python reserved names to be overriden by `datar`
+
+Sometimes it will be confusing especially when python builtin functions are overriden by `datar`. There are a couple of datar (`r-base`, `dplyr`) functions with the same name as python builtin functions do. For example: `filter`.
+
+To make you aware of the loss of access to builtin functions or other python preserved names, warnings will be reported when those names are imported directly:
+
+```python
+>>> from datar.dplyr import filter
+[2021-06-18 17:58:23][datar][WARNING] Builtin name "filter" has been overriden by datar.
+>>> from datar.all import *
+2021-06-18 17:58:48][datar][WARNING] Builtin name "min" has been overriden by datar.
+[2021-06-18 17:58:48][datar][WARNING] Builtin name "max" has been overriden by datar.
+[2021-06-18 17:58:48][datar][WARNING] Builtin name "sum" has been overriden by datar.
+[2021-06-18 17:58:48][datar][WARNING] Builtin name "abs" has been overriden by datar.
+[2021-06-18 17:58:48][datar][WARNING] Builtin name "round" has been overriden by datar.
+[2021-06-18 17:58:48][datar][WARNING] Builtin name "all" has been overriden by datar.
+[2021-06-18 17:58:48][datar][WARNING] Builtin name "any" has been overriden by datar.
+[2021-06-18 17:58:48][datar][WARNING] Builtin name "re" has been overriden by datar.
+[2021-06-18 17:58:48][datar][WARNING] Builtin name "slice" has been overriden by datar.
+```
+
+However, when they are not imported directly, no warnings will show:
+
+```python
+from datar import dplyr
+dplyr.filter # ok
+```
+
+There are a couple of ways to disable:
+
+1. Use option: `warn.builtin.names`
+
+```python
+>>> from datar.base import options
+>>> options(warn_builtin_names=False)
+>>> from datar.all import * # ok
+>>> options(warn_builtin_names=True)
+>>> from datar.all import *
+[2021-06-18 18:02:35][datar][WARNING] Builtin name "min" has been overriden by datar.
+[2021-06-18 18:02:35][datar][WARNING] Builtin name "max" has been overriden by datar.
+[2021-06-18 18:02:35][datar][WARNING] Builtin name "sum" has been overriden by datar.
+[2021-06-18 18:02:35][datar][WARNING] Builtin name "abs" has been overriden by datar.
+[2021-06-18 18:02:35][datar][WARNING] Builtin name "round" has been overriden by datar.
+[2021-06-18 18:02:35][datar][WARNING] Builtin name "all" has been overriden by datar.
+[2021-06-18 18:02:35][datar][WARNING] Builtin name "any" has been overriden by datar.
+[2021-06-18 18:02:35][datar][WARNING] Builtin name "re" has been overriden by datar.
+[2021-06-18 18:02:35][datar][WARNING] Builtin name "filter" has been overriden by datar.
+[2021-06-18 18:02:35][datar][WARNING] Builtin name "slice" has been overriden by datar.
+```
+
+2. Import `_no_warn`
+
+```python
+>>> from datar.dplyr import _no_warn
+>>> from datar.dplyr import * # ok
+>>> from datar.dplyr import _warn
+>>> from datar.dplyr import * # warn again
+[2021-06-18 18:03:54][datar][WARNING] Builtin name "filter" has been overriden by datar.
+[2021-06-18 18:03:54][datar][WARNING] Builtin name "slice" has been overriden by datar.
+```
+
+3. Let the `logger` hide the message
+
+```python
+>>> from datar.core import logger
+>>> logger.setLevel(40)
+>>> from datar.dplyr import * # ok
+```
diff --git a/docs/notebooks/across.ipynb b/docs/notebooks/across.ipynb
index 7ee87ec9..84b7f0e6 100644
--- a/docs/notebooks/across.ipynb
+++ b/docs/notebooks/across.ipynb
@@ -32,7 +32,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Apply the same transformation to multiple columns\n\nThe original API:  \nhttps://dplyr.tidyverse.org/reference/across.html  \n\n##### Args:\n&emsp;&emsp;`_data`: The dataframe.  \n&emsp;&emsp;`*args`: If given, the first 2 elements should be columns and functions  \n&emsp;&emsp;&emsp;&emsp;apply to each of the selected columns. The rest of them will be  \n&emsp;&emsp;&emsp;&emsp;the arguments for the functions.  \n\n&emsp;&emsp;`_names`: A glue specification that describes how to name  \n&emsp;&emsp;&emsp;&emsp;the output columns. This can use `{_col}` to stand for the  \n&emsp;&emsp;&emsp;&emsp;selected column name, and `{_fn}` to stand for the name of  \n&emsp;&emsp;&emsp;&emsp;the function being applied.  \n&emsp;&emsp;&emsp;&emsp;The default (None) is equivalent to `{_col}` for the  \n&emsp;&emsp;&emsp;&emsp;single function case and `{_col}_{_fn}` for the case where  \n&emsp;&emsp;&emsp;&emsp;a list is used for _fns. In such a case, `{_fn}` is 0-based.  \n&emsp;&emsp;&emsp;&emsp;To use 1-based index, use `{_fn1}`  \n\n&emsp;&emsp;`_base0`: Indicating whether the columns are 0-based if selected  \n&emsp;&emsp;&emsp;&emsp;by indexes. if not provided, will use  \n&emsp;&emsp;&emsp;&emsp;`datar.base.getOption('index.base.0')`.  \n\n&emsp;&emsp;`**kwargs`: Keyword arguments for the functions  \n\n##### Returns:\n&emsp;&emsp;A dataframe with one column for each column and each function.  \n"
+      "text/markdown": "##### Apply the same transformation to multiple columns\n\nThe original API:  \nhttps://dplyr.tidyverse.org/reference/across.html  \n\n##### Args:\n&emsp;&emsp;`_data`: The dataframe.  \n&emsp;&emsp;`*args`: If given, the first 2 elements should be columns and functions  \n&emsp;&emsp;&emsp;&emsp;apply to each of the selected columns. The rest of them will be  \n&emsp;&emsp;&emsp;&emsp;the arguments for the functions.  \n\n&emsp;&emsp;`_names`: A glue specification that describes how to name  \n&emsp;&emsp;&emsp;&emsp;the output columns. This can use `{_col}` to stand for the  \n&emsp;&emsp;&emsp;&emsp;selected column name, and `{_fn}` to stand for the name of  \n&emsp;&emsp;&emsp;&emsp;the function being applied.  \n&emsp;&emsp;&emsp;&emsp;The default (None) is equivalent to `{_col}` for the  \n&emsp;&emsp;&emsp;&emsp;single function case and `{_col}_{_fn}` for the case where  \n&emsp;&emsp;&emsp;&emsp;a list is used for _fns. In such a case, `{_fn}` is 0-based.  \n&emsp;&emsp;&emsp;&emsp;To use 1-based index, use `{_fn1}`  \n\n&emsp;&emsp;`_base0`: Indicating whether the columns are 0-based if selected  \n&emsp;&emsp;&emsp;&emsp;by indexes. if not provided, will use  \n&emsp;&emsp;&emsp;&emsp;`datar.base.get_option('index.base.0')`.  \n\n&emsp;&emsp;`**kwargs`: Keyword arguments for the functions  \n\n##### Returns:\n&emsp;&emsp;A dataframe with one column for each column and each function.  \n"
      },
      "metadata": {}
     },
@@ -80,7 +80,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Apply the same transformation to multiple columns rowwisely\n\n##### Args:\n&emsp;&emsp;`_data`: The dataframe  \n&emsp;&emsp;`_cols`: The columns  \n&emsp;&emsp;`_base0`: Indicating whether the columns are 0-based if selected  \n&emsp;&emsp;&emsp;&emsp;by indexes. if not provided, will use  \n&emsp;&emsp;&emsp;&emsp;`datar.base.getOption('index.base.0')`.  \n\n##### Returns:\n&emsp;&emsp;A series  \n"
+      "text/markdown": "##### Apply the same transformation to multiple columns rowwisely\n\n##### Args:\n&emsp;&emsp;`_data`: The dataframe  \n&emsp;&emsp;`_cols`: The columns  \n&emsp;&emsp;`_base0`: Indicating whether the columns are 0-based if selected  \n&emsp;&emsp;&emsp;&emsp;by indexes. if not provided, will use  \n&emsp;&emsp;&emsp;&emsp;`datar.base.get_option('index.base.0')`.  \n\n##### Returns:\n&emsp;&emsp;A series  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/bind.ipynb b/docs/notebooks/bind.ipynb
index 4f34bdec..06be91a1 100644
--- a/docs/notebooks/bind.ipynb
+++ b/docs/notebooks/bind.ipynb
@@ -33,7 +33,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Bind rows of give dataframes\n\nOriginal APIs https://dplyr.tidyverse.org/reference/bind.html  \n\n##### Args:\n&emsp;&emsp;`_data`: The seed dataframe to bind others  \n&emsp;&emsp;&emsp;&emsp;Could be a dict or a list, keys/indexes will be used for _id col  \n\n&emsp;&emsp;`*datas`: Other dataframes to combine  \n&emsp;&emsp;`_id`: The name of the id columns  \n&emsp;&emsp;`_base0`: Whether `_id` starts from 0 or not, if no keys are provided.  \n&emsp;&emsp;&emsp;&emsp;If `_base0` is not provided, will use  \n&emsp;&emsp;&emsp;&emsp;`datar.base.getOption('index.base.0')`  \n\n&emsp;&emsp;`**kwargs`: A mapping of dataframe, keys will be used as _id col.  \n\n##### Returns:\n&emsp;&emsp;The combined dataframe  \n"
+      "text/markdown": "##### Bind rows of give dataframes\n\nOriginal APIs https://dplyr.tidyverse.org/reference/bind.html  \n\n##### Args:\n&emsp;&emsp;`_data`: The seed dataframe to bind others  \n&emsp;&emsp;&emsp;&emsp;Could be a dict or a list, keys/indexes will be used for _id col  \n\n&emsp;&emsp;`*datas`: Other dataframes to combine  \n&emsp;&emsp;`_id`: The name of the id columns  \n&emsp;&emsp;`_base0`: Whether `_id` starts from 0 or not, if no keys are provided.  \n&emsp;&emsp;&emsp;&emsp;If `_base0` is not provided, will use  \n&emsp;&emsp;&emsp;&emsp;`datar.base.get_option('index.base.0')`  \n\n&emsp;&emsp;`**kwargs`: A mapping of dataframe, keys will be used as _id col.  \n\n##### Returns:\n&emsp;&emsp;The combined dataframe  \n"
      },
      "metadata": {}
     },
@@ -49,7 +49,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Bind columns of give dataframes\n\nNote that unlike `dplyr`, mismatched dimensions are allowed and  \nmissing rows will be filled with `NA`s  \n\n##### Args:\n&emsp;&emsp;`_data`: The seed dataframe to bind others  \n&emsp;&emsp;&emsp;&emsp;Could be a dict, keys will be used for _id col  \n\n&emsp;&emsp;`*datas`: other dataframes to bind  \n&emsp;&emsp;`_name_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether the numeric suffix starts from 0 or not.  \n&emsp;&emsp;&emsp;&emsp;If not specified, will use `datar.base.getOption('index.base.0')`.  \n\n##### Returns:\n&emsp;&emsp;The combined dataframe  \n"
+      "text/markdown": "##### Bind columns of give dataframes\n\nNote that unlike `dplyr`, mismatched dimensions are allowed and  \nmissing rows will be filled with `NA`s  \n\n##### Args:\n&emsp;&emsp;`_data`: The seed dataframe to bind others  \n&emsp;&emsp;&emsp;&emsp;Could be a dict, keys will be used for _id col  \n\n&emsp;&emsp;`*datas`: other dataframes to bind  \n&emsp;&emsp;`_name_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether the numeric suffix starts from 0 or not.  \n&emsp;&emsp;&emsp;&emsp;If not specified, will use `datar.base.get_option('index.base.0')`.  \n\n##### Returns:\n&emsp;&emsp;The combined dataframe  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/chop.ipynb b/docs/notebooks/chop.ipynb
index 6998e174..ccbd67a3 100644
--- a/docs/notebooks/chop.ipynb
+++ b/docs/notebooks/chop.ipynb
@@ -46,7 +46,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Makes data frame shorter by converting rows within each group\ninto list-columns.  \n\n##### Args:\n&emsp;&emsp;`data`: A data frame  \n&emsp;&emsp;`cols`: Columns to chop  \n&emsp;&emsp;`_base0`: Whether `cols` are 0-based  \n&emsp;&emsp;&emsp;&emsp;if not provided, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;Data frame with selected columns chopped  \n"
+      "text/markdown": "##### Makes data frame shorter by converting rows within each group\ninto list-columns.  \n\n##### Args:\n&emsp;&emsp;`data`: A data frame  \n&emsp;&emsp;`cols`: Columns to chop  \n&emsp;&emsp;`_base0`: Whether `cols` are 0-based  \n&emsp;&emsp;&emsp;&emsp;if not provided, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;Data frame with selected columns chopped  \n"
      },
      "metadata": {}
     },
@@ -62,7 +62,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Makes df longer by expanding list-columns so that each element\nof the list-column gets its own row in the output.  \n\nSee https://tidyr.tidyverse.org/reference/chop.html  \n\nRecycling size-1 elements might be different from `tidyr`  \n&emsp;&emsp;>>> df = tibble(x=[1, [2,3]], y=[[2,3], 1])  \n&emsp;&emsp;>>> df >> unchop([f.x, f.y])  \n&emsp;&emsp;>>> # tibble(x=[1,2,3], y=[2,3,1])  \n&emsp;&emsp;>>> # instead of following in tidyr  \n&emsp;&emsp;>>> # tibble(x=[1,1,2,3], y=[2,3,1,1])  \n\n##### Args:\n&emsp;&emsp;`data`: A data frame.  \n&emsp;&emsp;`cols`: Columns to unchop.  \n&emsp;&emsp;`keep_empty`: By default, you get one row of output for each element  \n&emsp;&emsp;&emsp;&emsp;of the list your unchopping/unnesting.  \n&emsp;&emsp;&emsp;&emsp;This means that if there's a size-0 element  \n&emsp;&emsp;&emsp;&emsp;(like NULL or an empty data frame), that entire row will be  \n&emsp;&emsp;&emsp;&emsp;dropped from the output.  \n&emsp;&emsp;&emsp;&emsp;If you want to preserve all rows, use `keep_empty` = `True` to  \n&emsp;&emsp;&emsp;&emsp;replace size-0 elements with a single row of missing values.  \n\n&emsp;&emsp;`dtypes`: NOT `ptype`. Providing the dtypes for the output columns.  \n&emsp;&emsp;&emsp;&emsp;Could be a single dtype, which will be applied to all columns, or  \n&emsp;&emsp;&emsp;&emsp;a dictionary of dtypes with keys for the columns and values the  \n&emsp;&emsp;&emsp;&emsp;dtypes.  \n&emsp;&emsp;&emsp;&emsp;For nested data frames, we need to specify `col$a` as key. If `col`  \n&emsp;&emsp;&emsp;&emsp;is used as key, all columns of the nested data frames will be casted  \n&emsp;&emsp;&emsp;&emsp;into that dtype.  \n\n&emsp;&emsp;`_base0`: Whether `cols` are 0-based  \n&emsp;&emsp;&emsp;&emsp;if not provided, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;A data frame with selected columns unchopped.  \n"
+      "text/markdown": "##### Makes df longer by expanding list-columns so that each element\nof the list-column gets its own row in the output.  \n\nSee https://tidyr.tidyverse.org/reference/chop.html  \n\nRecycling size-1 elements might be different from `tidyr`  \n&emsp;&emsp;>>> df = tibble(x=[1, [2,3]], y=[[2,3], 1])  \n&emsp;&emsp;>>> df >> unchop([f.x, f.y])  \n&emsp;&emsp;>>> # tibble(x=[1,2,3], y=[2,3,1])  \n&emsp;&emsp;>>> # instead of following in tidyr  \n&emsp;&emsp;>>> # tibble(x=[1,1,2,3], y=[2,3,1,1])  \n\n##### Args:\n&emsp;&emsp;`data`: A data frame.  \n&emsp;&emsp;`cols`: Columns to unchop.  \n&emsp;&emsp;`keep_empty`: By default, you get one row of output for each element  \n&emsp;&emsp;&emsp;&emsp;of the list your unchopping/unnesting.  \n&emsp;&emsp;&emsp;&emsp;This means that if there's a size-0 element  \n&emsp;&emsp;&emsp;&emsp;(like NULL or an empty data frame), that entire row will be  \n&emsp;&emsp;&emsp;&emsp;dropped from the output.  \n&emsp;&emsp;&emsp;&emsp;If you want to preserve all rows, use `keep_empty` = `True` to  \n&emsp;&emsp;&emsp;&emsp;replace size-0 elements with a single row of missing values.  \n\n&emsp;&emsp;`dtypes`: NOT `ptype`. Providing the dtypes for the output columns.  \n&emsp;&emsp;&emsp;&emsp;Could be a single dtype, which will be applied to all columns, or  \n&emsp;&emsp;&emsp;&emsp;a dictionary of dtypes with keys for the columns and values the  \n&emsp;&emsp;&emsp;&emsp;dtypes.  \n&emsp;&emsp;&emsp;&emsp;For nested data frames, we need to specify `col$a` as key. If `col`  \n&emsp;&emsp;&emsp;&emsp;is used as key, all columns of the nested data frames will be casted  \n&emsp;&emsp;&emsp;&emsp;into that dtype.  \n\n&emsp;&emsp;`_base0`: Whether `cols` are 0-based  \n&emsp;&emsp;&emsp;&emsp;if not provided, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;A data frame with selected columns unchopped.  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/drop_na.ipynb b/docs/notebooks/drop_na.ipynb
index 5aedd6e3..145dc142 100644
--- a/docs/notebooks/drop_na.ipynb
+++ b/docs/notebooks/drop_na.ipynb
@@ -33,7 +33,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Drop rows containing missing values\n\nSee https://tidyr.tidyverse.org/reference/drop_na.html  \n\n##### Args:\n&emsp;&emsp;`data`: A data frame.  \n&emsp;&emsp;`*columns`: Columns to inspect for missing values.  \n&emsp;&emsp;`how`: How to select the rows to drop  \n&emsp;&emsp;&emsp;&emsp;- all: All columns of `columns` to be `NA`s\n\n&emsp;&emsp;&emsp;&emsp;- any: Any columns of `columns` to be `NA`s\n\n&emsp;&emsp;&emsp;&emsp;(tidyr doesn't support this argument)  \n\n&emsp;&emsp;`_base0`: Whether `*columns` are 0-based if given by indexes  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;Dataframe with rows with NAs dropped and indexes dropped  \n"
+      "text/markdown": "##### Drop rows containing missing values\n\nSee https://tidyr.tidyverse.org/reference/drop_na.html  \n\n##### Args:\n&emsp;&emsp;`data`: A data frame.  \n&emsp;&emsp;`*columns`: Columns to inspect for missing values.  \n&emsp;&emsp;`how`: How to select the rows to drop  \n&emsp;&emsp;&emsp;&emsp;- all: All columns of `columns` to be `NA`s\n\n&emsp;&emsp;&emsp;&emsp;- any: Any columns of `columns` to be `NA`s\n\n&emsp;&emsp;&emsp;&emsp;(tidyr doesn't support this argument)  \n\n&emsp;&emsp;`_base0`: Whether `*columns` are 0-based if given by indexes  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;Dataframe with rows with NAs dropped and indexes dropped  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/expand.ipynb b/docs/notebooks/expand.ipynb
index 154c8401..aa2076c0 100644
--- a/docs/notebooks/expand.ipynb
+++ b/docs/notebooks/expand.ipynb
@@ -33,7 +33,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Generates all combination of variables found in a dataset.\n\n##### Args:\n&emsp;&emsp;`data`: A data frame  \n&emsp;&emsp;`*args`: and,  \n&emsp;&emsp;`**kwargs`: columns to expand. Columns can be atomic lists.  \n&emsp;&emsp;&emsp;&emsp;- To find all unique combinations of x, y and z, including\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;those not present in the data, supply each variable as a  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;separate argument: `expand(df, x, y, z)`.  \n\n&emsp;&emsp;&emsp;&emsp;- To find only the combinations that occur in the data, use\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`nesting`: `expand(df, nesting(x, y, z))`.  \n\n&emsp;&emsp;&emsp;&emsp;- You can combine the two forms. For example,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`expand(df, nesting(school_id, student_id), date)` would  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;produce a row for each present school-student combination  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;for all possible dates.  \n\n&emsp;&emsp;`_name_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether the suffixes of repaired names should be 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`.  \n\n##### Returns:\n&emsp;&emsp;A data frame with all combination of variables.  \n"
+      "text/markdown": "##### Generates all combination of variables found in a dataset.\n\n##### Args:\n&emsp;&emsp;`data`: A data frame  \n&emsp;&emsp;`*args`: and,  \n&emsp;&emsp;`**kwargs`: columns to expand. Columns can be atomic lists.  \n&emsp;&emsp;&emsp;&emsp;- To find all unique combinations of x, y and z, including\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;those not present in the data, supply each variable as a  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;separate argument: `expand(df, x, y, z)`.  \n\n&emsp;&emsp;&emsp;&emsp;- To find only the combinations that occur in the data, use\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`nesting`: `expand(df, nesting(x, y, z))`.  \n\n&emsp;&emsp;&emsp;&emsp;- You can combine the two forms. For example,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`expand(df, nesting(school_id, student_id), date)` would  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;produce a row for each present school-student combination  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;for all possible dates.  \n\n&emsp;&emsp;`_name_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether the suffixes of repaired names should be 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`.  \n\n##### Returns:\n&emsp;&emsp;A data frame with all combination of variables.  \n"
      },
      "metadata": {}
     },
@@ -49,7 +49,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### A helper that only finds combinations already present in the data.\n\n##### Args:\n&emsp;&emsp;`*args`: and,  \n&emsp;&emsp;`**kwargs`: columns to expand. Columns can be atomic lists.  \n&emsp;&emsp;&emsp;&emsp;- To find all unique combinations of x, y and z, including\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;those not present in the data, supply each variable as a  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;separate argument: `expand(df, x, y, z)`.  \n\n&emsp;&emsp;&emsp;&emsp;- To find only the combinations that occur in the data, use\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`nesting`: `expand(df, nesting(x, y, z))`.  \n\n&emsp;&emsp;&emsp;&emsp;- You can combine the two forms. For example,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`expand(df, nesting(school_id, student_id), date)` would  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;produce a row for each present school-student combination  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;for all possible dates.  \n\n&emsp;&emsp;`_name_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether the suffixes of repaired names should be 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`.  \n\n##### Returns:\n&emsp;&emsp;A data frame with all combinations in data.  \n"
+      "text/markdown": "##### A helper that only finds combinations already present in the data.\n\n##### Args:\n&emsp;&emsp;`*args`: and,  \n&emsp;&emsp;`**kwargs`: columns to expand. Columns can be atomic lists.  \n&emsp;&emsp;&emsp;&emsp;- To find all unique combinations of x, y and z, including\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;those not present in the data, supply each variable as a  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;separate argument: `expand(df, x, y, z)`.  \n\n&emsp;&emsp;&emsp;&emsp;- To find only the combinations that occur in the data, use\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`nesting`: `expand(df, nesting(x, y, z))`.  \n\n&emsp;&emsp;&emsp;&emsp;- You can combine the two forms. For example,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`expand(df, nesting(school_id, student_id), date)` would  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;produce a row for each present school-student combination  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;for all possible dates.  \n\n&emsp;&emsp;`_name_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether the suffixes of repaired names should be 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`.  \n\n##### Returns:\n&emsp;&emsp;A data frame with all combinations in data.  \n"
      },
      "metadata": {}
     },
@@ -65,7 +65,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### A wrapper around `expand_grid()` that de-duplicates and sorts its inputs\n\nWhen values are not specified by literal `list`, they will be sorted.  \n\n##### Args:\n&emsp;&emsp;`*args`: and,  \n&emsp;&emsp;`**kwargs`: columns to expand. Columns can be atomic lists.  \n&emsp;&emsp;&emsp;&emsp;- To find all unique combinations of x, y and z, including\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;those not present in the data, supply each variable as a  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;separate argument: `expand(df, x, y, z)`.  \n\n&emsp;&emsp;&emsp;&emsp;- To find only the combinations that occur in the data, use\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`nesting`: `expand(df, nesting(x, y, z))`.  \n\n&emsp;&emsp;&emsp;&emsp;- You can combine the two forms. For example,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`expand(df, nesting(school_id, student_id), date)` would  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;produce a row for each present school-student combination  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;for all possible dates.  \n\n&emsp;&emsp;`_name_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether the suffixes of repaired names should be 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`.  \n\n##### Returns:\n&emsp;&emsp;A data frame with values deduplicated and sorted.  \n"
+      "text/markdown": "##### A wrapper around `expand_grid()` that de-duplicates and sorts its inputs\n\nWhen values are not specified by literal `list`, they will be sorted.  \n\n##### Args:\n&emsp;&emsp;`*args`: and,  \n&emsp;&emsp;`**kwargs`: columns to expand. Columns can be atomic lists.  \n&emsp;&emsp;&emsp;&emsp;- To find all unique combinations of x, y and z, including\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;those not present in the data, supply each variable as a  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;separate argument: `expand(df, x, y, z)`.  \n\n&emsp;&emsp;&emsp;&emsp;- To find only the combinations that occur in the data, use\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`nesting`: `expand(df, nesting(x, y, z))`.  \n\n&emsp;&emsp;&emsp;&emsp;- You can combine the two forms. For example,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`expand(df, nesting(school_id, student_id), date)` would  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;produce a row for each present school-student combination  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;for all possible dates.  \n\n&emsp;&emsp;`_name_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether the suffixes of repaired names should be 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`.  \n\n##### Returns:\n&emsp;&emsp;A data frame with values deduplicated and sorted.  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/expand_grid.ipynb b/docs/notebooks/expand_grid.ipynb
index 5f699a45..e4555d09 100644
--- a/docs/notebooks/expand_grid.ipynb
+++ b/docs/notebooks/expand_grid.ipynb
@@ -33,7 +33,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Create a tibble from all combinations of inputs\n\n##### Args:\n&emsp;&emsp;`*args`: and  \n&emsp;&emsp;`**kwargs`: name-value pairs.  \n&emsp;&emsp;&emsp;&emsp;For `*args`, names will be inferred from the values and if failed,  \n&emsp;&emsp;&emsp;&emsp;`_Var0`, `_Var1`, etc will be used.  \n\n&emsp;&emsp;`_name_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether the suffixes of repaired names should be 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`.  \n\n##### Returns:\n&emsp;&emsp;A data frame with one column for each input in `*args` and `**kwargs`.  \n&emsp;&emsp;The output will have one row for each combination of the inputs,  \n&emsp;&emsp;i.e. the size be equal to the product of the sizes of the inputs.  \n&emsp;&emsp;This implies that if any input has length 0, the output will have  \n&emsp;&emsp;zero rows.  \n"
+      "text/markdown": "##### Create a tibble from all combinations of inputs\n\n##### Args:\n&emsp;&emsp;`*args`: and  \n&emsp;&emsp;`**kwargs`: name-value pairs.  \n&emsp;&emsp;&emsp;&emsp;For `*args`, names will be inferred from the values and if failed,  \n&emsp;&emsp;&emsp;&emsp;`_Var0`, `_Var1`, etc will be used.  \n\n&emsp;&emsp;`_name_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether the suffixes of repaired names should be 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`.  \n\n##### Returns:\n&emsp;&emsp;A data frame with one column for each input in `*args` and `**kwargs`.  \n&emsp;&emsp;The output will have one row for each combination of the inputs,  \n&emsp;&emsp;i.e. the size be equal to the product of the sizes of the inputs.  \n&emsp;&emsp;This implies that if any input has length 0, the output will have  \n&emsp;&emsp;zero rows.  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/extract.ipynb b/docs/notebooks/extract.ipynb
index 3b75287d..ef42795d 100644
--- a/docs/notebooks/extract.ipynb
+++ b/docs/notebooks/extract.ipynb
@@ -33,7 +33,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Given a regular expression with capturing groups, extract() turns each\ngroup into a new column. If the groups don't match, or the input is NA,  \nthe output will be NA.  \n\nSee https://tidyr.tidyverse.org/reference/extract.html  \n\n##### Args:\n&emsp;&emsp;`data`: The dataframe  \n&emsp;&emsp;`col`: Column name or position.  \n&emsp;&emsp;`into`: Names of new variables to create as character vector.  \n&emsp;&emsp;&emsp;&emsp;Use None to omit the variable in the output.  \n\n&emsp;&emsp;`regex`: a regular expression used to extract the desired values.  \n&emsp;&emsp;&emsp;&emsp;There should be one group (defined by ()) for each element of into.  \n\n&emsp;&emsp;`remove`: If TRUE, remove input column from output data frame.  \n&emsp;&emsp;`convert`: The universal type for the extracted columns or a dict for  \n&emsp;&emsp;&emsp;&emsp;individual ones  \n\n&emsp;&emsp;`_base0`: Whether `col` is 0-based when given by index  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;Dataframe with extracted columns.  \n"
+      "text/markdown": "##### Given a regular expression with capturing groups, extract() turns each\ngroup into a new column. If the groups don't match, or the input is NA,  \nthe output will be NA.  \n\nSee https://tidyr.tidyverse.org/reference/extract.html  \n\n##### Args:\n&emsp;&emsp;`data`: The dataframe  \n&emsp;&emsp;`col`: Column name or position.  \n&emsp;&emsp;`into`: Names of new variables to create as character vector.  \n&emsp;&emsp;&emsp;&emsp;Use None to omit the variable in the output.  \n\n&emsp;&emsp;`regex`: a regular expression used to extract the desired values.  \n&emsp;&emsp;&emsp;&emsp;There should be one group (defined by ()) for each element of into.  \n\n&emsp;&emsp;`remove`: If TRUE, remove input column from output data frame.  \n&emsp;&emsp;`convert`: The universal type for the extracted columns or a dict for  \n&emsp;&emsp;&emsp;&emsp;individual ones  \n\n&emsp;&emsp;`_base0`: Whether `col` is 0-based when given by index  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;Dataframe with extracted columns.  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/fill.ipynb b/docs/notebooks/fill.ipynb
index 19b24386..2179ddd1 100644
--- a/docs/notebooks/fill.ipynb
+++ b/docs/notebooks/fill.ipynb
@@ -33,7 +33,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Fills missing values in selected columns using the next or\nprevious entry.  \n\nSee https://tidyr.tidyverse.org/reference/fill.html  \n\n##### Args:\n&emsp;&emsp;`_data`: A dataframe  \n&emsp;&emsp;`*columns`: Columns to fill  \n&emsp;&emsp;`_direction`: Direction in which to fill missing values.  \n&emsp;&emsp;&emsp;&emsp;Currently either \"down\" (the default), \"up\",  \n&emsp;&emsp;&emsp;&emsp;\"downup\" (i.e. first down and then up) or  \n&emsp;&emsp;&emsp;&emsp;\"updown\" (first up and then down).  \n\n&emsp;&emsp;`_base0`: Whether `*columns` are 0-based if given by indexes  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;The dataframe with NAs being replaced.  \n"
+      "text/markdown": "##### Fills missing values in selected columns using the next or\nprevious entry.  \n\nSee https://tidyr.tidyverse.org/reference/fill.html  \n\n##### Args:\n&emsp;&emsp;`_data`: A dataframe  \n&emsp;&emsp;`*columns`: Columns to fill  \n&emsp;&emsp;`_direction`: Direction in which to fill missing values.  \n&emsp;&emsp;&emsp;&emsp;Currently either \"down\" (the default), \"up\",  \n&emsp;&emsp;&emsp;&emsp;\"downup\" (i.e. first down and then up) or  \n&emsp;&emsp;&emsp;&emsp;\"updown\" (first up and then down).  \n\n&emsp;&emsp;`_base0`: Whether `*columns` are 0-based if given by indexes  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;The dataframe with NAs being replaced.  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/group_by.ipynb b/docs/notebooks/group_by.ipynb
index 7106d70c..4584e446 100644
--- a/docs/notebooks/group_by.ipynb
+++ b/docs/notebooks/group_by.ipynb
@@ -48,7 +48,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Ungroup a grouped data\n\nSee https://dplyr.tidyverse.org/reference/group_by.html  \n\n##### Args:\n&emsp;&emsp;`x`: The data frame  \n&emsp;&emsp;`*cols`: Variables to remove from the grouping variables.  \n&emsp;&emsp;`_base0`: If columns are selected with indexes, whether they are 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not given, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;A data frame with selected columns removed from the grouping variables.  \n"
+      "text/markdown": "##### Ungroup a grouped data\n\nSee https://dplyr.tidyverse.org/reference/group_by.html  \n\n##### Args:\n&emsp;&emsp;`x`: The data frame  \n&emsp;&emsp;`*cols`: Variables to remove from the grouping variables.  \n&emsp;&emsp;`_base0`: If columns are selected with indexes, whether they are 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not given, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;A data frame with selected columns removed from the grouping variables.  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/mutate.ipynb b/docs/notebooks/mutate.ipynb
index 320c4133..f174c64d 100644
--- a/docs/notebooks/mutate.ipynb
+++ b/docs/notebooks/mutate.ipynb
@@ -32,7 +32,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Adds new variables and preserves existing ones\n\nThe original API:  \nhttps://dplyr.tidyverse.org/reference/mutate.html  \n\n##### Args:\n&emsp;&emsp;`_data`: A data frame  \n&emsp;&emsp;`_keep`: allows you to control which columns from _data are retained  \n&emsp;&emsp;&emsp;&emsp;in the output:  \n\n&emsp;&emsp;&emsp;&emsp;- \"all\", the default, retains all variables.\n\n&emsp;&emsp;&emsp;&emsp;- \"used\" keeps any variables used to make new variables;\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;it's useful for checking your work as it displays inputs and  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;outputs side-by-side.  \n\n&emsp;&emsp;&emsp;&emsp;- \"unused\" keeps only existing variables not used to make new\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;variables.  \n\n&emsp;&emsp;&emsp;&emsp;- \"none\", only keeps grouping keys (like transmute()).\n\n&emsp;&emsp;`_before`: and  \n&emsp;&emsp;`_after`: Optionally, control where new columns should appear  \n&emsp;&emsp;&emsp;&emsp;(the default is to add to the right hand side).  \n&emsp;&emsp;&emsp;&emsp;See relocate() for more details.  \n\n&emsp;&emsp;`_base0`: Whether `_before` and `_after` are 0-based if given by indexes.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`  \n\n&emsp;&emsp;`*args`: and  \n&emsp;&emsp;`**kwargs`: Name-value pairs. The name gives the name of the column  \n&emsp;&emsp;&emsp;&emsp;in the output. The value can be:  \n\n&emsp;&emsp;&emsp;&emsp;- A vector of length 1, which will be recycled to the correct\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;length.  \n\n&emsp;&emsp;&emsp;&emsp;- A vector the same length as the current group (or the whole\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;data frame if ungrouped).  \n\n&emsp;&emsp;&emsp;&emsp;- None to remove the column\n\n##### Returns:\n&emsp;&emsp;An object of the same type as _data. The output has the following  \n&emsp;&emsp;properties:  \n&emsp;&emsp;- Rows are not affected.\n&emsp;&emsp;- Existing columns will be preserved according to the _keep\n&emsp;&emsp;&emsp;&emsp;argument. New columns will be placed according to the  \n&emsp;&emsp;&emsp;&emsp;_before and _after arguments. If _keep = \"none\"  \n&emsp;&emsp;&emsp;&emsp;(as in transmute()), the output order is determined only  \n&emsp;&emsp;&emsp;&emsp;by ..., not the order of existing columns.  \n\n&emsp;&emsp;- Columns given value None will be removed\n&emsp;&emsp;- Groups will be recomputed if a grouping variable is mutated.\n&emsp;&emsp;- Data frame attributes are preserved.\n"
+      "text/markdown": "##### Adds new variables and preserves existing ones\n\nThe original API:  \nhttps://dplyr.tidyverse.org/reference/mutate.html  \n\n##### Args:\n&emsp;&emsp;`_data`: A data frame  \n&emsp;&emsp;`_keep`: allows you to control which columns from _data are retained  \n&emsp;&emsp;&emsp;&emsp;in the output:  \n\n&emsp;&emsp;&emsp;&emsp;- \"all\", the default, retains all variables.\n\n&emsp;&emsp;&emsp;&emsp;- \"used\" keeps any variables used to make new variables;\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;it's useful for checking your work as it displays inputs and  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;outputs side-by-side.  \n\n&emsp;&emsp;&emsp;&emsp;- \"unused\" keeps only existing variables not used to make new\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;variables.  \n\n&emsp;&emsp;&emsp;&emsp;- \"none\", only keeps grouping keys (like transmute()).\n\n&emsp;&emsp;`_before`: and  \n&emsp;&emsp;`_after`: Optionally, control where new columns should appear  \n&emsp;&emsp;&emsp;&emsp;(the default is to add to the right hand side).  \n&emsp;&emsp;&emsp;&emsp;See relocate() for more details.  \n\n&emsp;&emsp;`_base0`: Whether `_before` and `_after` are 0-based if given by indexes.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`  \n\n&emsp;&emsp;`*args`: and  \n&emsp;&emsp;`**kwargs`: Name-value pairs. The name gives the name of the column  \n&emsp;&emsp;&emsp;&emsp;in the output. The value can be:  \n\n&emsp;&emsp;&emsp;&emsp;- A vector of length 1, which will be recycled to the correct\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;length.  \n\n&emsp;&emsp;&emsp;&emsp;- A vector the same length as the current group (or the whole\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;data frame if ungrouped).  \n\n&emsp;&emsp;&emsp;&emsp;- None to remove the column\n\n##### Returns:\n&emsp;&emsp;An object of the same type as _data. The output has the following  \n&emsp;&emsp;properties:  \n&emsp;&emsp;- Rows are not affected.\n&emsp;&emsp;- Existing columns will be preserved according to the _keep\n&emsp;&emsp;&emsp;&emsp;argument. New columns will be placed according to the  \n&emsp;&emsp;&emsp;&emsp;_before and _after arguments. If _keep = \"none\"  \n&emsp;&emsp;&emsp;&emsp;(as in transmute()), the output order is determined only  \n&emsp;&emsp;&emsp;&emsp;by ..., not the order of existing columns.  \n\n&emsp;&emsp;- Columns given value None will be removed\n&emsp;&emsp;- Groups will be recomputed if a grouping variable is mutated.\n&emsp;&emsp;- Data frame attributes are preserved.\n"
      },
      "metadata": {}
     },
diff --git a/docs/notebooks/nest.ipynb b/docs/notebooks/nest.ipynb
index e4bac161..26879102 100644
--- a/docs/notebooks/nest.ipynb
+++ b/docs/notebooks/nest.ipynb
@@ -46,7 +46,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Nesting creates a list-column of data frames\n\n##### Args:\n&emsp;&emsp;`_data`: A data frame  \n&emsp;&emsp;`**cols`: Columns to nest  \n&emsp;&emsp;`_names_sep`: If `None`, the default, the names will be left as is.  \n&emsp;&emsp;&emsp;&emsp;Inner names will come from the former outer names  \n&emsp;&emsp;&emsp;&emsp;If a string, the inner and outer names will be used together.  \n&emsp;&emsp;&emsp;&emsp;The names of the new outer columns will be formed by pasting  \n&emsp;&emsp;&emsp;&emsp;together the outer and the inner column names, separated by  \n&emsp;&emsp;&emsp;&emsp;`_names_sep`.  \n\n&emsp;&emsp;`_base0`: Whether `**cols` are 0-based  \n&emsp;&emsp;&emsp;&emsp;if not provided, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;Nested data frame.  \n"
+      "text/markdown": "##### Nesting creates a list-column of data frames\n\n##### Args:\n&emsp;&emsp;`_data`: A data frame  \n&emsp;&emsp;`**cols`: Columns to nest  \n&emsp;&emsp;`_names_sep`: If `None`, the default, the names will be left as is.  \n&emsp;&emsp;&emsp;&emsp;Inner names will come from the former outer names  \n&emsp;&emsp;&emsp;&emsp;If a string, the inner and outer names will be used together.  \n&emsp;&emsp;&emsp;&emsp;The names of the new outer columns will be formed by pasting  \n&emsp;&emsp;&emsp;&emsp;together the outer and the inner column names, separated by  \n&emsp;&emsp;&emsp;&emsp;`_names_sep`.  \n\n&emsp;&emsp;`_base0`: Whether `**cols` are 0-based  \n&emsp;&emsp;&emsp;&emsp;if not provided, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;Nested data frame.  \n"
      },
      "metadata": {}
     },
@@ -62,7 +62,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Flattens list-column of data frames back out into regular columns.\n\n##### Args:\n&emsp;&emsp;`data`: A data frame to flatten.  \n&emsp;&emsp;`*cols`: Columns to unnest.  \n&emsp;&emsp;`keep_empty`: By default, you get one row of output for each element  \n&emsp;&emsp;&emsp;&emsp;of the list your unchopping/unnesting.  \n&emsp;&emsp;&emsp;&emsp;This means that if there's a size-0 element  \n&emsp;&emsp;&emsp;&emsp;(like NULL or an empty data frame), that entire row will be  \n&emsp;&emsp;&emsp;&emsp;dropped from the output.  \n&emsp;&emsp;&emsp;&emsp;If you want to preserve all rows, use `keep_empty` = `True` to  \n&emsp;&emsp;&emsp;&emsp;replace size-0 elements with a single row of missing values.  \n\n&emsp;&emsp;`dtypes`: NOT `ptype`. Providing the dtypes for the output columns.  \n&emsp;&emsp;&emsp;&emsp;Could be a single dtype, which will be applied to all columns, or  \n&emsp;&emsp;&emsp;&emsp;a dictionary of dtypes with keys for the columns and values the  \n&emsp;&emsp;&emsp;&emsp;dtypes.  \n\n&emsp;&emsp;`names_sep`: If `None`, the default, the names will be left as is.  \n&emsp;&emsp;&emsp;&emsp;Inner names will come from the former outer names  \n&emsp;&emsp;&emsp;&emsp;If a string, the inner and outer names will be used together.  \n&emsp;&emsp;&emsp;&emsp;The names of the new outer columns will be formed by pasting  \n&emsp;&emsp;&emsp;&emsp;together the outer and the inner column names, separated by  \n&emsp;&emsp;&emsp;&emsp;`names_sep`.  \n\n&emsp;&emsp;`names_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether `cols` are 0-based  \n&emsp;&emsp;&emsp;&emsp;if not provided, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;Data frame with selected columns unnested.  \n"
+      "text/markdown": "##### Flattens list-column of data frames back out into regular columns.\n\n##### Args:\n&emsp;&emsp;`data`: A data frame to flatten.  \n&emsp;&emsp;`*cols`: Columns to unnest.  \n&emsp;&emsp;`keep_empty`: By default, you get one row of output for each element  \n&emsp;&emsp;&emsp;&emsp;of the list your unchopping/unnesting.  \n&emsp;&emsp;&emsp;&emsp;This means that if there's a size-0 element  \n&emsp;&emsp;&emsp;&emsp;(like NULL or an empty data frame), that entire row will be  \n&emsp;&emsp;&emsp;&emsp;dropped from the output.  \n&emsp;&emsp;&emsp;&emsp;If you want to preserve all rows, use `keep_empty` = `True` to  \n&emsp;&emsp;&emsp;&emsp;replace size-0 elements with a single row of missing values.  \n\n&emsp;&emsp;`dtypes`: NOT `ptype`. Providing the dtypes for the output columns.  \n&emsp;&emsp;&emsp;&emsp;Could be a single dtype, which will be applied to all columns, or  \n&emsp;&emsp;&emsp;&emsp;a dictionary of dtypes with keys for the columns and values the  \n&emsp;&emsp;&emsp;&emsp;dtypes.  \n\n&emsp;&emsp;`names_sep`: If `None`, the default, the names will be left as is.  \n&emsp;&emsp;&emsp;&emsp;Inner names will come from the former outer names  \n&emsp;&emsp;&emsp;&emsp;If a string, the inner and outer names will be used together.  \n&emsp;&emsp;&emsp;&emsp;The names of the new outer columns will be formed by pasting  \n&emsp;&emsp;&emsp;&emsp;together the outer and the inner column names, separated by  \n&emsp;&emsp;&emsp;&emsp;`names_sep`.  \n\n&emsp;&emsp;`names_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether `cols` are 0-based  \n&emsp;&emsp;&emsp;&emsp;if not provided, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;Data frame with selected columns unnested.  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/pack.ipynb b/docs/notebooks/pack.ipynb
index 335eb1d0..7b3b8c3a 100644
--- a/docs/notebooks/pack.ipynb
+++ b/docs/notebooks/pack.ipynb
@@ -46,7 +46,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Makes df narrow by collapsing a set of columns into a single df-column.\n\n##### Args:\n&emsp;&emsp;`_data`: A data frame  \n&emsp;&emsp;`**cols`: Columns to pack  \n&emsp;&emsp;`_names_sep`: If `None`, the default, the names will be left as is.  \n&emsp;&emsp;&emsp;&emsp;Inner names will come from the former outer names  \n&emsp;&emsp;&emsp;&emsp;If a string, the inner and outer names will be used together.  \n&emsp;&emsp;&emsp;&emsp;The names of the new outer columns will be formed by pasting  \n&emsp;&emsp;&emsp;&emsp;together the outer and the inner column names, separated by  \n&emsp;&emsp;&emsp;&emsp;`_names_sep`.  \n\n&emsp;&emsp;`_base0`: Whether `**cols` are 0-based  \n&emsp;&emsp;&emsp;&emsp;if not provided, will use `datar.base.getOption('index.base.0')`  \n"
+      "text/markdown": "##### Makes df narrow by collapsing a set of columns into a single df-column.\n\n##### Args:\n&emsp;&emsp;`_data`: A data frame  \n&emsp;&emsp;`**cols`: Columns to pack  \n&emsp;&emsp;`_names_sep`: If `None`, the default, the names will be left as is.  \n&emsp;&emsp;&emsp;&emsp;Inner names will come from the former outer names  \n&emsp;&emsp;&emsp;&emsp;If a string, the inner and outer names will be used together.  \n&emsp;&emsp;&emsp;&emsp;The names of the new outer columns will be formed by pasting  \n&emsp;&emsp;&emsp;&emsp;together the outer and the inner column names, separated by  \n&emsp;&emsp;&emsp;&emsp;`_names_sep`.  \n\n&emsp;&emsp;`_base0`: Whether `**cols` are 0-based  \n&emsp;&emsp;&emsp;&emsp;if not provided, will use `datar.base.get_option('index.base.0')`  \n"
      },
      "metadata": {}
     },
@@ -62,7 +62,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Makes df wider by expanding df-columns back out into individual columns.\n\n##### Args:\n&emsp;&emsp;`data`: A data frame  \n&emsp;&emsp;`cols`: Columns to unpack  \n&emsp;&emsp;`names_sep`: If `None`, the default, the names will be left as is.  \n&emsp;&emsp;&emsp;&emsp;Inner names will come from the former outer names  \n&emsp;&emsp;&emsp;&emsp;If a string, the inner and outer names will be used together.  \n&emsp;&emsp;&emsp;&emsp;The names of the new outer columns will be formed by pasting  \n&emsp;&emsp;&emsp;&emsp;together the outer and the inner column names, separated by  \n&emsp;&emsp;&emsp;&emsp;`_names_sep`.  \n\n&emsp;&emsp;`name_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether `cols` are 0-based  \n&emsp;&emsp;&emsp;&emsp;if not provided, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;Data frame with given columns unpacked.  \n"
+      "text/markdown": "##### Makes df wider by expanding df-columns back out into individual columns.\n\n##### Args:\n&emsp;&emsp;`data`: A data frame  \n&emsp;&emsp;`cols`: Columns to unpack  \n&emsp;&emsp;`names_sep`: If `None`, the default, the names will be left as is.  \n&emsp;&emsp;&emsp;&emsp;Inner names will come from the former outer names  \n&emsp;&emsp;&emsp;&emsp;If a string, the inner and outer names will be used together.  \n&emsp;&emsp;&emsp;&emsp;The names of the new outer columns will be formed by pasting  \n&emsp;&emsp;&emsp;&emsp;together the outer and the inner column names, separated by  \n&emsp;&emsp;&emsp;&emsp;`_names_sep`.  \n\n&emsp;&emsp;`name_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether `cols` are 0-based  \n&emsp;&emsp;&emsp;&emsp;if not provided, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;Data frame with given columns unpacked.  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/pivot_longer.ipynb b/docs/notebooks/pivot_longer.ipynb
index 57c17e15..570f7d4a 100644
--- a/docs/notebooks/pivot_longer.ipynb
+++ b/docs/notebooks/pivot_longer.ipynb
@@ -33,7 +33,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### \"lengthens\" data, increasing the number of rows and\ndecreasing the number of columns.  \n\nThe row order is a bit different from `tidyr` and `pandas.DataFrame.melt`.  \n&emsp;&emsp;>>> df = tibble(x=f[1:2], y=f[3:4])  \n&emsp;&emsp;>>> pivot_longer(df, f[f.x:f.y])  \n&emsp;&emsp;>>> #    name   value  \n&emsp;&emsp;>>> # 0  x      1  \n&emsp;&emsp;>>> # 1  x      2  \n&emsp;&emsp;>>> # 2  y      3  \n&emsp;&emsp;>>> # 3  y      4  \n\nBut with `tidyr::pivot_longer`, the output will be:  \n&emsp;&emsp;>>> # # A tibble: 4 x 2  \n&emsp;&emsp;>>> # name  value  \n&emsp;&emsp;>>> # <chr> <int>  \n&emsp;&emsp;>>> # 1 x   1  \n&emsp;&emsp;>>> # 2 y   3  \n&emsp;&emsp;>>> # 3 x   2  \n&emsp;&emsp;>>> # 4 y   4  \n\n##### Args:\n&emsp;&emsp;`_data`: A data frame to pivot.  \n&emsp;&emsp;`cols`: Columns to pivot into longer format.  \n&emsp;&emsp;`names_to`: A string specifying the name of the column to create from  \n&emsp;&emsp;&emsp;&emsp;the data stored in the column names of data.  \n&emsp;&emsp;&emsp;&emsp;Can be a character vector, creating multiple columns, if names_sep  \n&emsp;&emsp;&emsp;&emsp;or names_pattern is provided. In this case, there are two special  \n&emsp;&emsp;&emsp;&emsp;values you can take advantage of:  \n\n&emsp;&emsp;&emsp;&emsp;- `None`/`NA`/`NULL` will discard that component of the name.\n\n&emsp;&emsp;&emsp;&emsp;- `.value`/`_value` indicates that component of the name defines\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;the name of the column containing the cell values,  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;overriding values_to.  \n\n&emsp;&emsp;&emsp;&emsp;- Different as `tidyr`: With `.value`/`_value`, if there are other\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;parts of the names to distinguish the groups, they must be  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;captured. For example, use `r'(\\w)_(\\d)'` to match `'a_1'` and  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`['.value', NA]` to discard the suffix, instead of use  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`r'(\\w)_\\d'` to match.  \n\n&emsp;&emsp;`names_prefix`: A regular expression used to remove matching text from  \n&emsp;&emsp;&emsp;&emsp;the start of each variable name.  \n\n&emsp;&emsp;`names_sep`: and  \n&emsp;&emsp;`names_pattern`: If names_to contains multiple values,  \n&emsp;&emsp;&emsp;&emsp;these arguments control how the column name is broken up.  \n&emsp;&emsp;&emsp;&emsp;names_sep takes the same specification as separate(), and  \n&emsp;&emsp;&emsp;&emsp;can either be a numeric vector (specifying positions to break on),  \n&emsp;&emsp;&emsp;&emsp;or a single string (specifying a regular expression to split on).  \n\n&emsp;&emsp;`names_pattern`: takes the same specification as extract(),  \n&emsp;&emsp;&emsp;&emsp;a regular expression containing matching groups (()).  \n\n&emsp;&emsp;`names_ptypes`: and  \n&emsp;&emsp;`values_ptypes`: A list of column name-prototype pairs.  \n&emsp;&emsp;&emsp;&emsp;A prototype (or ptype for short) is a zero-length vector  \n&emsp;&emsp;&emsp;&emsp;(like integer() or numeric()) that defines the type, class, and  \n&emsp;&emsp;&emsp;&emsp;attributes of a vector. Use these arguments if you want to confirm  \n&emsp;&emsp;&emsp;&emsp;that the created columns are the types that you expect.  \n&emsp;&emsp;&emsp;&emsp;Note that if you want to change (instead of confirm) the types  \n&emsp;&emsp;&emsp;&emsp;of specific columns, you should use names_transform or  \n&emsp;&emsp;&emsp;&emsp;values_transform instead.  \n\n&emsp;&emsp;`names_transform`: and  \n&emsp;&emsp;`values_transform`: A list of column name-function pairs.  \n&emsp;&emsp;&emsp;&emsp;Use these arguments if you need to change the types of  \n&emsp;&emsp;&emsp;&emsp;specific columns. For example,  \n&emsp;&emsp;&emsp;&emsp;names_transform = dict(week = as.integer) would convert a  \n&emsp;&emsp;&emsp;&emsp;character variable called week to an integer.  \n&emsp;&emsp;&emsp;&emsp;If not specified, the type of the columns generated from names_to  \n&emsp;&emsp;&emsp;&emsp;will be character, and the type of the variables generated from  \n&emsp;&emsp;&emsp;&emsp;values_to will be the common type of the input columns used to  \n&emsp;&emsp;&emsp;&emsp;generate them.  \n\n&emsp;&emsp;`names_repair`: Not supported yet.  \n&emsp;&emsp;`values_to`: A string specifying the name of the column to create from  \n&emsp;&emsp;&emsp;&emsp;the data stored in cell values. If names_to is a character  \n&emsp;&emsp;&emsp;&emsp;containing the special `.value`/`_value` sentinel, this value  \n&emsp;&emsp;&emsp;&emsp;will be ignored, and the name of the value column will be derived  \n&emsp;&emsp;&emsp;&emsp;from part of the existing column names.  \n\n&emsp;&emsp;`values_drop_na`: If TRUE, will drop rows that contain only NAs in  \n&emsp;&emsp;&emsp;&emsp;the value_to column. This effectively converts explicit missing  \n&emsp;&emsp;&emsp;&emsp;values to implicit missing values, and should generally be used  \n&emsp;&emsp;&emsp;&emsp;only when missing values in data were created by its structure.  \n\n&emsp;&emsp;`names_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether `cols` are 0-based if given by indexes  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;The pivoted dataframe.  \n"
+      "text/markdown": "##### \"lengthens\" data, increasing the number of rows and\ndecreasing the number of columns.  \n\nThe row order is a bit different from `tidyr` and `pandas.DataFrame.melt`.  \n&emsp;&emsp;>>> df = tibble(x=f[1:2], y=f[3:4])  \n&emsp;&emsp;>>> pivot_longer(df, f[f.x:f.y])  \n&emsp;&emsp;>>> #    name   value  \n&emsp;&emsp;>>> # 0  x      1  \n&emsp;&emsp;>>> # 1  x      2  \n&emsp;&emsp;>>> # 2  y      3  \n&emsp;&emsp;>>> # 3  y      4  \n\nBut with `tidyr::pivot_longer`, the output will be:  \n&emsp;&emsp;>>> # # A tibble: 4 x 2  \n&emsp;&emsp;>>> # name  value  \n&emsp;&emsp;>>> # <chr> <int>  \n&emsp;&emsp;>>> # 1 x   1  \n&emsp;&emsp;>>> # 2 y   3  \n&emsp;&emsp;>>> # 3 x   2  \n&emsp;&emsp;>>> # 4 y   4  \n\n##### Args:\n&emsp;&emsp;`_data`: A data frame to pivot.  \n&emsp;&emsp;`cols`: Columns to pivot into longer format.  \n&emsp;&emsp;`names_to`: A string specifying the name of the column to create from  \n&emsp;&emsp;&emsp;&emsp;the data stored in the column names of data.  \n&emsp;&emsp;&emsp;&emsp;Can be a character vector, creating multiple columns, if names_sep  \n&emsp;&emsp;&emsp;&emsp;or names_pattern is provided. In this case, there are two special  \n&emsp;&emsp;&emsp;&emsp;values you can take advantage of:  \n\n&emsp;&emsp;&emsp;&emsp;- `None`/`NA`/`NULL` will discard that component of the name.\n\n&emsp;&emsp;&emsp;&emsp;- `.value`/`_value` indicates that component of the name defines\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;the name of the column containing the cell values,  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;overriding values_to.  \n\n&emsp;&emsp;&emsp;&emsp;- Different as `tidyr`: With `.value`/`_value`, if there are other\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;parts of the names to distinguish the groups, they must be  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;captured. For example, use `r'(\\w)_(\\d)'` to match `'a_1'` and  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`['.value', NA]` to discard the suffix, instead of use  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`r'(\\w)_\\d'` to match.  \n\n&emsp;&emsp;`names_prefix`: A regular expression used to remove matching text from  \n&emsp;&emsp;&emsp;&emsp;the start of each variable name.  \n\n&emsp;&emsp;`names_sep`: and  \n&emsp;&emsp;`names_pattern`: If names_to contains multiple values,  \n&emsp;&emsp;&emsp;&emsp;these arguments control how the column name is broken up.  \n&emsp;&emsp;&emsp;&emsp;names_sep takes the same specification as separate(), and  \n&emsp;&emsp;&emsp;&emsp;can either be a numeric vector (specifying positions to break on),  \n&emsp;&emsp;&emsp;&emsp;or a single string (specifying a regular expression to split on).  \n\n&emsp;&emsp;`names_pattern`: takes the same specification as extract(),  \n&emsp;&emsp;&emsp;&emsp;a regular expression containing matching groups (()).  \n\n&emsp;&emsp;`names_ptypes`: and  \n&emsp;&emsp;`values_ptypes`: A list of column name-prototype pairs.  \n&emsp;&emsp;&emsp;&emsp;A prototype (or ptype for short) is a zero-length vector  \n&emsp;&emsp;&emsp;&emsp;(like integer() or numeric()) that defines the type, class, and  \n&emsp;&emsp;&emsp;&emsp;attributes of a vector. Use these arguments if you want to confirm  \n&emsp;&emsp;&emsp;&emsp;that the created columns are the types that you expect.  \n&emsp;&emsp;&emsp;&emsp;Note that if you want to change (instead of confirm) the types  \n&emsp;&emsp;&emsp;&emsp;of specific columns, you should use names_transform or  \n&emsp;&emsp;&emsp;&emsp;values_transform instead.  \n\n&emsp;&emsp;`names_transform`: and  \n&emsp;&emsp;`values_transform`: A list of column name-function pairs.  \n&emsp;&emsp;&emsp;&emsp;Use these arguments if you need to change the types of  \n&emsp;&emsp;&emsp;&emsp;specific columns. For example,  \n&emsp;&emsp;&emsp;&emsp;names_transform = dict(week = as.integer) would convert a  \n&emsp;&emsp;&emsp;&emsp;character variable called week to an integer.  \n&emsp;&emsp;&emsp;&emsp;If not specified, the type of the columns generated from names_to  \n&emsp;&emsp;&emsp;&emsp;will be character, and the type of the variables generated from  \n&emsp;&emsp;&emsp;&emsp;values_to will be the common type of the input columns used to  \n&emsp;&emsp;&emsp;&emsp;generate them.  \n\n&emsp;&emsp;`names_repair`: Not supported yet.  \n&emsp;&emsp;`values_to`: A string specifying the name of the column to create from  \n&emsp;&emsp;&emsp;&emsp;the data stored in cell values. If names_to is a character  \n&emsp;&emsp;&emsp;&emsp;containing the special `.value`/`_value` sentinel, this value  \n&emsp;&emsp;&emsp;&emsp;will be ignored, and the name of the value column will be derived  \n&emsp;&emsp;&emsp;&emsp;from part of the existing column names.  \n\n&emsp;&emsp;`values_drop_na`: If TRUE, will drop rows that contain only NAs in  \n&emsp;&emsp;&emsp;&emsp;the value_to column. This effectively converts explicit missing  \n&emsp;&emsp;&emsp;&emsp;values to implicit missing values, and should generally be used  \n&emsp;&emsp;&emsp;&emsp;only when missing values in data were created by its structure.  \n\n&emsp;&emsp;`names_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether `cols` are 0-based if given by indexes  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;The pivoted dataframe.  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/pivot_wider.ipynb b/docs/notebooks/pivot_wider.ipynb
index 9f96fc4a..c3dd008c 100644
--- a/docs/notebooks/pivot_wider.ipynb
+++ b/docs/notebooks/pivot_wider.ipynb
@@ -33,7 +33,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### \"widens\" data, increasing the number of columns and decreasing\nthe number of rows.  \n\n##### Args:\n&emsp;&emsp;`_data`: A data frame to pivot.  \n&emsp;&emsp;`id_cols`: A set of columns that uniquely identifies each observation.  \n&emsp;&emsp;&emsp;&emsp;Defaults to all columns in data except for the columns specified  \n&emsp;&emsp;&emsp;&emsp;in names_from and values_from.  \n\n&emsp;&emsp;`names_from`: and  \n&emsp;&emsp;`values_from`: A pair of arguments describing which column  \n&emsp;&emsp;&emsp;&emsp;(or columns) to get the name of the output column (names_from),  \n&emsp;&emsp;&emsp;&emsp;and which column (or columns) to get the cell values from  \n&emsp;&emsp;&emsp;&emsp;(values_from).  \n\n&emsp;&emsp;`names_prefix`: String added to the start of every variable name.  \n&emsp;&emsp;`names_sep`: If names_from or values_from contains multiple variables,  \n&emsp;&emsp;&emsp;&emsp;this will be used to join their values together into a single  \n&emsp;&emsp;&emsp;&emsp;string to use as a column name.  \n\n&emsp;&emsp;`names_glue`: Instead of names_sep and names_prefix, you can supply  \n&emsp;&emsp;&emsp;&emsp;a glue specification that uses the names_from columns  \n&emsp;&emsp;&emsp;&emsp;(and special _value) to create custom column names.  \n\n&emsp;&emsp;`names_sort`: Should the column names be sorted? If FALSE, the default,  \n&emsp;&emsp;&emsp;&emsp;column names are ordered by first appearance.  \n\n&emsp;&emsp;`names_repair`: todo  \n&emsp;&emsp;`values_fill`: Optionally, a (scalar) value that specifies what  \n&emsp;&emsp;&emsp;&emsp;each value should be filled in with when missing.  \n\n&emsp;&emsp;`values_fn`: Optionally, a function applied to the value in each cell  \n&emsp;&emsp;&emsp;&emsp;in the output. You will typically use this when the combination  \n&emsp;&emsp;&emsp;&emsp;of `id_cols` and value column does not uniquely identify  \n&emsp;&emsp;&emsp;&emsp;an observation.  \n&emsp;&emsp;&emsp;&emsp;This can be a dict you want to apply different aggregations to  \n&emsp;&emsp;&emsp;&emsp;different value columns.  \n&emsp;&emsp;&emsp;&emsp;If not specified, will be `numpy.mean`  \n\n&emsp;&emsp;`_base0`: Whether `id_cols`, `names_from` and `values_from`  \n&emsp;&emsp;&emsp;&emsp;are 0-based if given by indexes.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;The pivoted dataframe.  \n"
+      "text/markdown": "##### \"widens\" data, increasing the number of columns and decreasing\nthe number of rows.  \n\n##### Args:\n&emsp;&emsp;`_data`: A data frame to pivot.  \n&emsp;&emsp;`id_cols`: A set of columns that uniquely identifies each observation.  \n&emsp;&emsp;&emsp;&emsp;Defaults to all columns in data except for the columns specified  \n&emsp;&emsp;&emsp;&emsp;in names_from and values_from.  \n\n&emsp;&emsp;`names_from`: and  \n&emsp;&emsp;`values_from`: A pair of arguments describing which column  \n&emsp;&emsp;&emsp;&emsp;(or columns) to get the name of the output column (names_from),  \n&emsp;&emsp;&emsp;&emsp;and which column (or columns) to get the cell values from  \n&emsp;&emsp;&emsp;&emsp;(values_from).  \n\n&emsp;&emsp;`names_prefix`: String added to the start of every variable name.  \n&emsp;&emsp;`names_sep`: If names_from or values_from contains multiple variables,  \n&emsp;&emsp;&emsp;&emsp;this will be used to join their values together into a single  \n&emsp;&emsp;&emsp;&emsp;string to use as a column name.  \n\n&emsp;&emsp;`names_glue`: Instead of names_sep and names_prefix, you can supply  \n&emsp;&emsp;&emsp;&emsp;a glue specification that uses the names_from columns  \n&emsp;&emsp;&emsp;&emsp;(and special _value) to create custom column names.  \n\n&emsp;&emsp;`names_sort`: Should the column names be sorted? If FALSE, the default,  \n&emsp;&emsp;&emsp;&emsp;column names are ordered by first appearance.  \n\n&emsp;&emsp;`names_repair`: todo  \n&emsp;&emsp;`values_fill`: Optionally, a (scalar) value that specifies what  \n&emsp;&emsp;&emsp;&emsp;each value should be filled in with when missing.  \n\n&emsp;&emsp;`values_fn`: Optionally, a function applied to the value in each cell  \n&emsp;&emsp;&emsp;&emsp;in the output. You will typically use this when the combination  \n&emsp;&emsp;&emsp;&emsp;of `id_cols` and value column does not uniquely identify  \n&emsp;&emsp;&emsp;&emsp;an observation.  \n&emsp;&emsp;&emsp;&emsp;This can be a dict you want to apply different aggregations to  \n&emsp;&emsp;&emsp;&emsp;different value columns.  \n&emsp;&emsp;&emsp;&emsp;If not specified, will be `numpy.mean`  \n\n&emsp;&emsp;`_base0`: Whether `id_cols`, `names_from` and `values_from`  \n&emsp;&emsp;&emsp;&emsp;are 0-based if given by indexes.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;The pivoted dataframe.  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/pull.ipynb b/docs/notebooks/pull.ipynb
index ab07f077..ce6a600f 100644
--- a/docs/notebooks/pull.ipynb
+++ b/docs/notebooks/pull.ipynb
@@ -33,7 +33,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Pull a series or a dataframe from a dataframe\n\n##### Args:\n&emsp;&emsp;`_data`: The dataframe  \n&emsp;&emsp;`var`: The column to pull, either the name or the index  \n&emsp;&emsp;`name`: The name of the pulled value  \n&emsp;&emsp;&emsp;&emsp;- If `to` is frame, or the value pulled is data frame, it will be\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;the column names  \n\n&emsp;&emsp;&emsp;&emsp;- If `to` is series, it will be the series name. If multiple names\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;are given, only the first name will be used.  \n\n&emsp;&emsp;&emsp;&emsp;- If `to` is series, but value pulled is a data frame, then a\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;dictionary of series with the series names as keys or given `name`  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;as keys.  \n\n&emsp;&emsp;`to`: Type of data to return.  \n&emsp;&emsp;&emsp;&emsp;Only works when pulling `a` for name `a$b`  \n\n&emsp;&emsp;&emsp;&emsp;- series: Return a pandas Series object\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;Group information will be lost  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;If pulled value is a dataframe, it will return a dict of series,  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;with the series names or the `name` provided.  \n\n&emsp;&emsp;&emsp;&emsp;- array: Return a numpy.ndarray object\n\n&emsp;&emsp;&emsp;&emsp;- frame: Return a DataFrame with that column\n\n&emsp;&emsp;&emsp;&emsp;- list: Return a python list\n\n&emsp;&emsp;&emsp;&emsp;- dict: Return a dict with `name` as keys and pulled value as values\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;Only a single column is allowed to pull  \n\n&emsp;&emsp;&emsp;&emsp;- If not provided: `series` when pulled data has only one columns.\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`dict` if `name` provided and has the same length as the pulled  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;single column. Otherwise `frame`.  \n\n&emsp;&emsp;`_base0`: Whether `var` is 0-based if given by index  \n&emsp;&emsp;&emsp;&emsp;If not provided, `datar.base.getOption('index.base.0')` is used.  \n\n##### Returns:\n&emsp;&emsp;The data according to `to`  \n"
+      "text/markdown": "##### Pull a series or a dataframe from a dataframe\n\n##### Args:\n&emsp;&emsp;`_data`: The dataframe  \n&emsp;&emsp;`var`: The column to pull, either the name or the index  \n&emsp;&emsp;`name`: The name of the pulled value  \n&emsp;&emsp;&emsp;&emsp;- If `to` is frame, or the value pulled is data frame, it will be\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;the column names  \n\n&emsp;&emsp;&emsp;&emsp;- If `to` is series, it will be the series name. If multiple names\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;are given, only the first name will be used.  \n\n&emsp;&emsp;&emsp;&emsp;- If `to` is series, but value pulled is a data frame, then a\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;dictionary of series with the series names as keys or given `name`  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;as keys.  \n\n&emsp;&emsp;`to`: Type of data to return.  \n&emsp;&emsp;&emsp;&emsp;Only works when pulling `a` for name `a$b`  \n\n&emsp;&emsp;&emsp;&emsp;- series: Return a pandas Series object\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;Group information will be lost  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;If pulled value is a dataframe, it will return a dict of series,  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;with the series names or the `name` provided.  \n\n&emsp;&emsp;&emsp;&emsp;- array: Return a numpy.ndarray object\n\n&emsp;&emsp;&emsp;&emsp;- frame: Return a DataFrame with that column\n\n&emsp;&emsp;&emsp;&emsp;- list: Return a python list\n\n&emsp;&emsp;&emsp;&emsp;- dict: Return a dict with `name` as keys and pulled value as values\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;Only a single column is allowed to pull  \n\n&emsp;&emsp;&emsp;&emsp;- If not provided: `series` when pulled data has only one columns.\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;`dict` if `name` provided and has the same length as the pulled  \n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;single column. Otherwise `frame`.  \n\n&emsp;&emsp;`_base0`: Whether `var` is 0-based if given by index  \n&emsp;&emsp;&emsp;&emsp;If not provided, `datar.base.get_option('index.base.0')` is used.  \n\n##### Returns:\n&emsp;&emsp;The data according to `to`  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/relocate.ipynb b/docs/notebooks/relocate.ipynb
index 9e8e3a52..fa2700cf 100644
--- a/docs/notebooks/relocate.ipynb
+++ b/docs/notebooks/relocate.ipynb
@@ -32,7 +32,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### change column positions\n\nSee original API  \nhttps://dplyr.tidyverse.org/reference/relocate.html  \n\n##### Args:\n&emsp;&emsp;`_data`: A data frame  \n&emsp;&emsp;`*args`: and  \n&emsp;&emsp;`**kwargs`: Columns to rename and move  \n&emsp;&emsp;`_before`: and  \n&emsp;&emsp;`_after`: Destination. Supplying neither will move columns to  \n&emsp;&emsp;&emsp;&emsp;the left-hand side; specifying both is an error.  \n\n&emsp;&emsp;`_base0`: Whether `_before` and `_after` are 0-based if given by indexes.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;An object of the same type as .data. The output has the following  \n&emsp;&emsp;properties:  \n&emsp;&emsp;- Rows are not affected.\n&emsp;&emsp;- The same columns appear in the output, but (usually) in a\n&emsp;&emsp;&emsp;&emsp;different place.  \n\n&emsp;&emsp;- Data frame attributes are preserved.\n&emsp;&emsp;- Groups are not affected\n"
+      "text/markdown": "##### change column positions\n\nSee original API  \nhttps://dplyr.tidyverse.org/reference/relocate.html  \n\n##### Args:\n&emsp;&emsp;`_data`: A data frame  \n&emsp;&emsp;`*args`: and  \n&emsp;&emsp;`**kwargs`: Columns to rename and move  \n&emsp;&emsp;`_before`: and  \n&emsp;&emsp;`_after`: Destination. Supplying neither will move columns to  \n&emsp;&emsp;&emsp;&emsp;the left-hand side; specifying both is an error.  \n\n&emsp;&emsp;`_base0`: Whether `_before` and `_after` are 0-based if given by indexes.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;An object of the same type as .data. The output has the following  \n&emsp;&emsp;properties:  \n&emsp;&emsp;- Rows are not affected.\n&emsp;&emsp;- The same columns appear in the output, but (usually) in a\n&emsp;&emsp;&emsp;&emsp;different place.  \n\n&emsp;&emsp;- Data frame attributes are preserved.\n&emsp;&emsp;- Groups are not affected\n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/rename.ipynb b/docs/notebooks/rename.ipynb
index 2966594a..c8ef2a8d 100644
--- a/docs/notebooks/rename.ipynb
+++ b/docs/notebooks/rename.ipynb
@@ -46,7 +46,7 @@
        "&emsp;&emsp;`_data`: The dataframe  \n",
        "&emsp;&emsp;`**kwargs`: The new_name = old_name pairs  \n",
        "&emsp;&emsp;`_base0`: Whether the old_name is 0-based if given by indexes.  \n",
-       "&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`  \n",
+       "&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`  \n",
        "\n",
        "##### Returns:\n",
        "&emsp;&emsp;The dataframe with new names  \n"
@@ -85,7 +85,7 @@
        "\n",
        "&emsp;&emsp;`**kwargs`: keyword arguments for `_fn`  \n",
        "&emsp;&emsp;`_base0`: Whether the old_name is 0-based if given by indexes.  \n",
-       "&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`  \n",
+       "&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`  \n",
        "\n",
        "##### Returns:\n",
        "&emsp;&emsp;The dataframe with new names  \n"
diff --git a/docs/notebooks/rowwise.ipynb b/docs/notebooks/rowwise.ipynb
index 66c42c3a..01262438 100644
--- a/docs/notebooks/rowwise.ipynb
+++ b/docs/notebooks/rowwise.ipynb
@@ -32,7 +32,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Compute on a data frame a row-at-a-time\n\nSee https://dplyr.tidyverse.org/reference/rowwise.html  \n\n##### Args:\n&emsp;&emsp;`_data`: The dataframe  \n&emsp;&emsp;`*columns`: Variables to be preserved when calling summarise().  \n&emsp;&emsp;&emsp;&emsp;This is typically a set of variables whose combination  \n&emsp;&emsp;&emsp;&emsp;uniquely identify each row.  \n\n&emsp;&emsp;`_base0`: Whether indexes are 0-based if columns are selected by indexes.  \n&emsp;&emsp;&emsp;&emsp;If not given, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;A row-wise data frame  \n"
+      "text/markdown": "##### Compute on a data frame a row-at-a-time\n\nSee https://dplyr.tidyverse.org/reference/rowwise.html  \n\n##### Args:\n&emsp;&emsp;`_data`: The dataframe  \n&emsp;&emsp;`*columns`: Variables to be preserved when calling summarise().  \n&emsp;&emsp;&emsp;&emsp;This is typically a set of variables whose combination  \n&emsp;&emsp;&emsp;&emsp;uniquely identify each row.  \n\n&emsp;&emsp;`_base0`: Whether indexes are 0-based if columns are selected by indexes.  \n&emsp;&emsp;&emsp;&emsp;If not given, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;A row-wise data frame  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/select.ipynb b/docs/notebooks/select.ipynb
index 82ddf700..59305e13 100644
--- a/docs/notebooks/select.ipynb
+++ b/docs/notebooks/select.ipynb
@@ -32,7 +32,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Select (and optionally rename) variables in a data frame\n\nSee original API  \nhttps://dplyr.tidyverse.org/reference/select.html  \n\nTo exclude columns use `~` instead of `-`. For example, to exclude last  \n`column`: `select(df, ~c(-1))`.  \n\nTo use column name in slice: `f[f.col1:f.col2]`. If you don't want `col2`  \nto be included: `f[f.col1:f.col2:0]`  \n\n##### Args:\n&emsp;&emsp;`*columns`: The columns to select  \n&emsp;&emsp;`**renamings`: The columns to rename and select in new => old column way.  \n&emsp;&emsp;`_base0`: Whether the columns are 0-based if given by indexes  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;The dataframe with select columns  \n"
+      "text/markdown": "##### Select (and optionally rename) variables in a data frame\n\nSee original API  \nhttps://dplyr.tidyverse.org/reference/select.html  \n\nTo exclude columns use `~` instead of `-`. For example, to exclude last  \n`column`: `select(df, ~c(-1))`.  \n\nTo use column name in slice: `f[f.col1:f.col2]`. If you don't want `col2`  \nto be included: `f[f.col1:f.col2:0]`  \n\n##### Args:\n&emsp;&emsp;`*columns`: The columns to select  \n&emsp;&emsp;`**renamings`: The columns to rename and select in new => old column way.  \n&emsp;&emsp;`_base0`: Whether the columns are 0-based if given by indexes  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;The dataframe with select columns  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/separate.ipynb b/docs/notebooks/separate.ipynb
index d298a57e..61deb00c 100644
--- a/docs/notebooks/separate.ipynb
+++ b/docs/notebooks/separate.ipynb
@@ -33,7 +33,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Given either a regular expression or a vector of character positions,\nturns a single character column into multiple columns.  \n\n##### Args:\n&emsp;&emsp;`data`: The dataframe  \n&emsp;&emsp;`col`: Column name or position.  \n&emsp;&emsp;`into`: Names of new variables to create as character vector.  \n&emsp;&emsp;&emsp;&emsp;Use `None`/`NA`/`NULL` to omit the variable in the output.  \n\n&emsp;&emsp;`sep`: Separator between columns.  \n&emsp;&emsp;&emsp;&emsp;If str, `sep` is interpreted as a regular expression.  \n&emsp;&emsp;&emsp;&emsp;The default value is a regular expression that matches  \n&emsp;&emsp;&emsp;&emsp;any sequence of non-alphanumeric values.  \n&emsp;&emsp;&emsp;&emsp;If int, `sep` is interpreted as character positions to split at.  \n\n&emsp;&emsp;`remove`: If TRUE, remove input column from output data frame.  \n&emsp;&emsp;`convert`: The universal type for the extracted columns or a dict for  \n&emsp;&emsp;&emsp;&emsp;individual ones  \n&emsp;&emsp;&emsp;&emsp;Note that when given `TRUE`, `DataFrame.convert_dtypes()` is called,  \n&emsp;&emsp;&emsp;&emsp;but it will not convert `str` to other types  \n&emsp;&emsp;&emsp;&emsp;(For example, `'1'` to `1`). You have to specify the dtype yourself.  \n\n&emsp;&emsp;`extra`: If sep is a character vector, this controls what happens when  \n&emsp;&emsp;&emsp;&emsp;there are too many pieces. There are three valid options:  \n\n&emsp;&emsp;&emsp;&emsp;- \"warn\" (the default): emit a warning and drop extra values.\n\n&emsp;&emsp;&emsp;&emsp;- \"drop\": drop any extra values without a warning.\n\n&emsp;&emsp;&emsp;&emsp;- \"merge\": only splits at most length(into) times\n\n&emsp;&emsp;`fill`: If sep is a character vector, this controls what happens when  \n&emsp;&emsp;&emsp;&emsp;there are not enough pieces. There are three valid options:  \n\n&emsp;&emsp;&emsp;&emsp;- \"warn\" (the default): emit a warning and fill from the right\n\n&emsp;&emsp;&emsp;&emsp;- \"right\": fill with missing values on the right\n\n&emsp;&emsp;&emsp;&emsp;- \"left\": fill with missing values on the left\n\n&emsp;&emsp;`_base0`: Whether `col` is 0-based when given by index and Whether `sep`  \n&emsp;&emsp;&emsp;&emsp;is 0-based if given by position  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;Dataframe with separated columns.  \n"
+      "text/markdown": "##### Given either a regular expression or a vector of character positions,\nturns a single character column into multiple columns.  \n\n##### Args:\n&emsp;&emsp;`data`: The dataframe  \n&emsp;&emsp;`col`: Column name or position.  \n&emsp;&emsp;`into`: Names of new variables to create as character vector.  \n&emsp;&emsp;&emsp;&emsp;Use `None`/`NA`/`NULL` to omit the variable in the output.  \n\n&emsp;&emsp;`sep`: Separator between columns.  \n&emsp;&emsp;&emsp;&emsp;If str, `sep` is interpreted as a regular expression.  \n&emsp;&emsp;&emsp;&emsp;The default value is a regular expression that matches  \n&emsp;&emsp;&emsp;&emsp;any sequence of non-alphanumeric values.  \n&emsp;&emsp;&emsp;&emsp;If int, `sep` is interpreted as character positions to split at.  \n\n&emsp;&emsp;`remove`: If TRUE, remove input column from output data frame.  \n&emsp;&emsp;`convert`: The universal type for the extracted columns or a dict for  \n&emsp;&emsp;&emsp;&emsp;individual ones  \n&emsp;&emsp;&emsp;&emsp;Note that when given `TRUE`, `DataFrame.convert_dtypes()` is called,  \n&emsp;&emsp;&emsp;&emsp;but it will not convert `str` to other types  \n&emsp;&emsp;&emsp;&emsp;(For example, `'1'` to `1`). You have to specify the dtype yourself.  \n\n&emsp;&emsp;`extra`: If sep is a character vector, this controls what happens when  \n&emsp;&emsp;&emsp;&emsp;there are too many pieces. There are three valid options:  \n\n&emsp;&emsp;&emsp;&emsp;- \"warn\" (the default): emit a warning and drop extra values.\n\n&emsp;&emsp;&emsp;&emsp;- \"drop\": drop any extra values without a warning.\n\n&emsp;&emsp;&emsp;&emsp;- \"merge\": only splits at most length(into) times\n\n&emsp;&emsp;`fill`: If sep is a character vector, this controls what happens when  \n&emsp;&emsp;&emsp;&emsp;there are not enough pieces. There are three valid options:  \n\n&emsp;&emsp;&emsp;&emsp;- \"warn\" (the default): emit a warning and fill from the right\n\n&emsp;&emsp;&emsp;&emsp;- \"right\": fill with missing values on the right\n\n&emsp;&emsp;&emsp;&emsp;- \"left\": fill with missing values on the left\n\n&emsp;&emsp;`_base0`: Whether `col` is 0-based when given by index and Whether `sep`  \n&emsp;&emsp;&emsp;&emsp;is 0-based if given by position  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;Dataframe with separated columns.  \n"
      },
      "metadata": {}
     },
@@ -49,7 +49,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Separates the values and places each one in its own row.\n\n##### Args:\n&emsp;&emsp;`data`: The dataframe  \n&emsp;&emsp;`*columns`: The columns to separate on  \n&emsp;&emsp;`sep`: Separator between columns.  \n&emsp;&emsp;`convert`: The universal type for the extracted columns or a dict for  \n&emsp;&emsp;&emsp;&emsp;individual ones  \n\n&emsp;&emsp;`_base0`: Whether `columns` is 0-based when given by index  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;Dataframe with rows separated and repeated.  \n"
+      "text/markdown": "##### Separates the values and places each one in its own row.\n\n##### Args:\n&emsp;&emsp;`data`: The dataframe  \n&emsp;&emsp;`*columns`: The columns to separate on  \n&emsp;&emsp;`sep`: Separator between columns.  \n&emsp;&emsp;`convert`: The universal type for the extracted columns or a dict for  \n&emsp;&emsp;&emsp;&emsp;individual ones  \n\n&emsp;&emsp;`_base0`: Whether `columns` is 0-based when given by index  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;Dataframe with rows separated and repeated.  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/slice.ipynb b/docs/notebooks/slice.ipynb
index 4a86f603..597bff17 100644
--- a/docs/notebooks/slice.ipynb
+++ b/docs/notebooks/slice.ipynb
@@ -33,7 +33,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Index rows by their (integer) locations\n\nOriginal APIs https://dplyr.tidyverse.org/reference/slice.html  \n\n##### Args:\n&emsp;&emsp;`_data`: The dataframe  \n&emsp;&emsp;`rows`: The indexes  \n&emsp;&emsp;&emsp;&emsp;Ranges can be specified as `f[1:3]`  \n&emsp;&emsp;&emsp;&emsp;Note that the negatives mean differently than in dplyr.  \n&emsp;&emsp;&emsp;&emsp;In dplyr, negative numbers meaning exclusive, but here negative  \n&emsp;&emsp;&emsp;&emsp;numbers are negative indexes like how they act in python indexing.  \n&emsp;&emsp;&emsp;&emsp;For exclusive indexes, you need to use inversion. For example:  \n&emsp;&emsp;&emsp;&emsp;`slice(df, ~f[:3])` excludes first 3 rows. You can also do:  \n&emsp;&emsp;&emsp;&emsp;`slice(df, ~c(f[:3], 6))` to exclude multiple set of rows.  \n&emsp;&emsp;&emsp;&emsp;To exclude a single row, you can't do this directly: `slice(df, ~1)`  \n&emsp;&emsp;&emsp;&emsp;since `~1` is directly compiled into a number. You can do this  \n\n&emsp;&emsp;&emsp;&emsp;`instead`: `slice(df, ~c(1))`  \n\n&emsp;&emsp;&emsp;&emsp;Exclusive and inclusive expressions are allowed to be mixed, unlike  \n&emsp;&emsp;&emsp;&emsp;in `dplyr`. They are expanded in the order they are passed in.  \n\n&emsp;&emsp;`_preserve`: Relevant when the _data input is grouped.  \n&emsp;&emsp;&emsp;&emsp;If _preserve = FALSE (the default), the grouping structure is  \n&emsp;&emsp;&emsp;&emsp;recalculated based on the resulting data,  \n&emsp;&emsp;&emsp;&emsp;otherwise the grouping is kept as is.  \n\n&emsp;&emsp;`_base0`: If rows are selected by indexes, whether they are 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not provided, `datar.base.getOption('index.base.0')` is used.  \n\n##### Returns:\n&emsp;&emsp;The sliced dataframe  \n"
+      "text/markdown": "##### Index rows by their (integer) locations\n\nOriginal APIs https://dplyr.tidyverse.org/reference/slice.html  \n\n##### Args:\n&emsp;&emsp;`_data`: The dataframe  \n&emsp;&emsp;`rows`: The indexes  \n&emsp;&emsp;&emsp;&emsp;Ranges can be specified as `f[1:3]`  \n&emsp;&emsp;&emsp;&emsp;Note that the negatives mean differently than in dplyr.  \n&emsp;&emsp;&emsp;&emsp;In dplyr, negative numbers meaning exclusive, but here negative  \n&emsp;&emsp;&emsp;&emsp;numbers are negative indexes like how they act in python indexing.  \n&emsp;&emsp;&emsp;&emsp;For exclusive indexes, you need to use inversion. For example:  \n&emsp;&emsp;&emsp;&emsp;`slice(df, ~f[:3])` excludes first 3 rows. You can also do:  \n&emsp;&emsp;&emsp;&emsp;`slice(df, ~c(f[:3], 6))` to exclude multiple set of rows.  \n&emsp;&emsp;&emsp;&emsp;To exclude a single row, you can't do this directly: `slice(df, ~1)`  \n&emsp;&emsp;&emsp;&emsp;since `~1` is directly compiled into a number. You can do this  \n\n&emsp;&emsp;&emsp;&emsp;`instead`: `slice(df, ~c(1))`  \n\n&emsp;&emsp;&emsp;&emsp;Exclusive and inclusive expressions are allowed to be mixed, unlike  \n&emsp;&emsp;&emsp;&emsp;in `dplyr`. They are expanded in the order they are passed in.  \n\n&emsp;&emsp;`_preserve`: Relevant when the _data input is grouped.  \n&emsp;&emsp;&emsp;&emsp;If _preserve = FALSE (the default), the grouping structure is  \n&emsp;&emsp;&emsp;&emsp;recalculated based on the resulting data,  \n&emsp;&emsp;&emsp;&emsp;otherwise the grouping is kept as is.  \n\n&emsp;&emsp;`_base0`: If rows are selected by indexes, whether they are 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not provided, `datar.base.get_option('index.base.0')` is used.  \n\n##### Returns:\n&emsp;&emsp;The sliced dataframe  \n"
      },
      "metadata": {}
     },
diff --git a/docs/notebooks/tibble.ipynb b/docs/notebooks/tibble.ipynb
index b7c6d8f4..54eb686b 100644
--- a/docs/notebooks/tibble.ipynb
+++ b/docs/notebooks/tibble.ipynb
@@ -33,7 +33,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Constructs a data frame\n\n##### Args:\n&emsp;&emsp;`*args`: and  \n&emsp;&emsp;`**kwargs`: A set of name-value pairs.  \n&emsp;&emsp;`_name_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_rows`: Number of rows of a 0-col dataframe when args and kwargs are  \n&emsp;&emsp;&emsp;&emsp;not provided. When args or kwargs are provided, this is ignored.  \n\n&emsp;&emsp;`_base0`: Whether the suffixes of repaired names should be 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`.  \n\n##### Returns:\n&emsp;&emsp;A constructed dataframe  \n"
+      "text/markdown": "##### Constructs a data frame\n\n##### Args:\n&emsp;&emsp;`*args`: and  \n&emsp;&emsp;`**kwargs`: A set of name-value pairs.  \n&emsp;&emsp;`_name_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_rows`: Number of rows of a 0-col dataframe when args and kwargs are  \n&emsp;&emsp;&emsp;&emsp;not provided. When args or kwargs are provided, this is ignored.  \n\n&emsp;&emsp;`_base0`: Whether the suffixes of repaired names should be 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`.  \n\n##### Returns:\n&emsp;&emsp;A constructed dataframe  \n"
      },
      "metadata": {}
     },
@@ -49,7 +49,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Constructs a data frame that is guaranteed to occupy one row.\n\nScalar values will be wrapped with `[]`  \n\n##### Args:\n&emsp;&emsp;`*args`: and  \n&emsp;&emsp;`**kwargs`: A set of name-value pairs.  \n&emsp;&emsp;`_name_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether the suffixes of repaired names should be 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`.  \n\n##### Returns:\n&emsp;&emsp;A constructed dataframe  \n"
+      "text/markdown": "##### Constructs a data frame that is guaranteed to occupy one row.\n\nScalar values will be wrapped with `[]`  \n\n##### Args:\n&emsp;&emsp;`*args`: and  \n&emsp;&emsp;`**kwargs`: A set of name-value pairs.  \n&emsp;&emsp;`_name_repair`: treatment of problematic column names:  \n&emsp;&emsp;&emsp;&emsp;- \"minimal\": No name repair or checks, beyond basic existence,\n\n&emsp;&emsp;&emsp;&emsp;- \"unique\": Make sure names are unique and not empty,\n\n&emsp;&emsp;&emsp;&emsp;- \"check_unique\": (default value), no name repair,\n&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;but check they are unique,  \n\n&emsp;&emsp;&emsp;&emsp;- \"universal\": Make the names unique and syntactic\n\n&emsp;&emsp;&emsp;&emsp;- a function: apply custom name repair\n\n&emsp;&emsp;`_base0`: Whether the suffixes of repaired names should be 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`.  \n\n##### Returns:\n&emsp;&emsp;A constructed dataframe  \n"
      },
      "metadata": {}
     },
diff --git a/docs/notebooks/uncount.ipynb b/docs/notebooks/uncount.ipynb
index 61274bc3..27fd6116 100644
--- a/docs/notebooks/uncount.ipynb
+++ b/docs/notebooks/uncount.ipynb
@@ -33,7 +33,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Duplicating rows according to a weighting variable\n\n##### Args:\n&emsp;&emsp;`data`: A data frame  \n&emsp;&emsp;`weights`: A vector of weights. Evaluated in the context of data  \n&emsp;&emsp;`_remove`: If TRUE, and weights is the name of a column in data,  \n&emsp;&emsp;&emsp;&emsp;then this column is removed.  \n\n&emsp;&emsp;`_id`: Supply a string to create a new variable which gives a  \n&emsp;&emsp;&emsp;&emsp;unique identifier for each created row (0-based).  \n\n&emsp;&emsp;`_base0`: Whether the generated `_id` columns are 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;dataframe with rows repeated.  \n"
+      "text/markdown": "##### Duplicating rows according to a weighting variable\n\n##### Args:\n&emsp;&emsp;`data`: A data frame  \n&emsp;&emsp;`weights`: A vector of weights. Evaluated in the context of data  \n&emsp;&emsp;`_remove`: If TRUE, and weights is the name of a column in data,  \n&emsp;&emsp;&emsp;&emsp;then this column is removed.  \n\n&emsp;&emsp;`_id`: Supply a string to create a new variable which gives a  \n&emsp;&emsp;&emsp;&emsp;unique identifier for each created row (0-based).  \n\n&emsp;&emsp;`_base0`: Whether the generated `_id` columns are 0-based.  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;dataframe with rows repeated.  \n"
      },
      "metadata": {}
     }
diff --git a/docs/notebooks/unite.ipynb b/docs/notebooks/unite.ipynb
index 21fae3c4..71f40ac1 100644
--- a/docs/notebooks/unite.ipynb
+++ b/docs/notebooks/unite.ipynb
@@ -33,7 +33,7 @@
      "output_type": "display_data",
      "data": {
       "text/plain": "<IPython.core.display.Markdown object>",
-      "text/markdown": "##### Unite multiple columns into one by pasting strings together\n\n##### Args:\n&emsp;&emsp;`data`: A data frame.  \n&emsp;&emsp;`col`: The name of the new column, as a string or symbol.  \n&emsp;&emsp;`*columns`: Columns to unite  \n&emsp;&emsp;`sep`: Separator to use between values.  \n&emsp;&emsp;`remove`: If True, remove input columns from output data frame.  \n&emsp;&emsp;`na_rm`: If True, missing values will be remove prior to uniting  \n&emsp;&emsp;&emsp;&emsp;each value.  \n\n&emsp;&emsp;`_base0`: Whether `columns` is 0-based when given by index  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.getOption('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;The dataframe with selected columns united  \n"
+      "text/markdown": "##### Unite multiple columns into one by pasting strings together\n\n##### Args:\n&emsp;&emsp;`data`: A data frame.  \n&emsp;&emsp;`col`: The name of the new column, as a string or symbol.  \n&emsp;&emsp;`*columns`: Columns to unite  \n&emsp;&emsp;`sep`: Separator to use between values.  \n&emsp;&emsp;`remove`: If True, remove input columns from output data frame.  \n&emsp;&emsp;`na_rm`: If True, missing values will be remove prior to uniting  \n&emsp;&emsp;&emsp;&emsp;each value.  \n\n&emsp;&emsp;`_base0`: Whether `columns` is 0-based when given by index  \n&emsp;&emsp;&emsp;&emsp;If not provided, will use `datar.base.get_option('index.base.0')`  \n\n##### Returns:\n&emsp;&emsp;The dataframe with selected columns united  \n"
      },
      "metadata": {}
     }
diff --git a/docs/reference-maps/ALL.md b/docs/reference-maps/ALL.md
index a5d47822..669a301f 100644
--- a/docs/reference-maps/ALL.md
+++ b/docs/reference-maps/ALL.md
@@ -1,6 +1,7 @@
 
 |Module|Description|Reference|
 |-|-|-|
+|`base`|APIs ported from `r-base`|[:octicons-cross-reference-16:][5]|
 |#|#|#|
 |`dplyr`|APIs ported from `tidyverse/dplyr`|[:octicons-cross-reference-16:][2]|
 |`tidyr`|APIs ported from `tidyverse/tidyr`|[:octicons-cross-reference-16:][4]|
@@ -8,7 +9,8 @@
 |#|#|#|
 |`datasets`|Datasets collected from `tidyverse` or other related packages|[:octicons-cross-reference-16:][3]|
 
-[1]: ../tibble
-[2]: ../dplyr
-[3]: ../datasets
-[4]: ../tidyr
+[1]: ./tibble
+[2]: ./dplyr
+[3]: ./datasets
+[4]: ./tidyr
+[5]: ./base
diff --git a/docs/reference-maps/base.md b/docs/reference-maps/base.md
new file mode 100644
index 00000000..e59ea7d7
--- /dev/null
+++ b/docs/reference-maps/base.md
@@ -0,0 +1,33 @@
+<style>
+.md-typeset__table {
+   min-width: 100%;
+}
+
+.md-typeset table:not([class]) {
+    display: table;
+    max-width: 80%;
+}
+</style>
+
+## Reference of `datar.base`
+
+<u>**Legend:**</u>
+
+|Sample|Status|
+|---|---|
+|[normal]()|API that is regularly ported|
+|
[strike-through]()
|API that is not ported, or not an API originally|
+|[**bold**]()|API that is unique in `datar`|
+|[_italic_]()|Working in process|
+
+### Reference
+
+|API|Description|Notebook example|
+|---|---|---:|
+|[which()][1]|Which indices are True?||
+|[which_min()][2] [which_max()][3]|Where is the minimum or maximum or first TRUE or FALSE||
+
+
+[1]: ../../api/datar.base.which/#datar.dplyr.which.which
+[2]: ../../api/datar.base.which/#datar.dplyr.which.which_min
+[3]: ../../api/datar.base.which/#datar.dplyr.which.which_max
diff --git a/mkdocs.yml b/mkdocs.yml
index 772696d8..8d521c4c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -28,6 +28,7 @@ nav:
     - 'Home': 'index.md'
     - 'Reference maps':
         'ALL': 'reference-maps/ALL.md'
+        'base': 'reference-maps/base.md'
         'dplyr': 'reference-maps/dplyr.md'
         'tibble': 'reference-maps/tibble.md'
         'tidyr': 'reference-maps/tidyr.md'
diff --git a/pyproject.toml b/pyproject.toml
index 8aa3813d..174714c4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "datar"
-version = "0.1.1"
+version = "0.2.0"
 description = "Port of dplyr and other related R packages in python, using pipda."
 authors = ["pwwang <pwwang@pwwang.com>"]
 readme = "README.md"
@@ -11,9 +11,10 @@ repository = "https://github.com/pwwang/datar"
 [tool.poetry.dependencies]
 python = "^3.7.1" # align with pandas
 pipda = "*"
-modkit = "*"
-diot="*"
+diot = "*"
+executing = "*"
 pandas = "^1.2"
+scipy = { version = "^1.6", optional = true }
 
 [tool.poetry.dev-dependencies]
 pytest = "*"
diff --git a/requirements.txt b/requirements.txt
index 4ad253d9..85ec603b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,7 @@
 diot
-modkit
+executing
 pandas==1.*,>=1.2.0
 pipda
 pytest
 pytest-cov
+scipy==1.*,>=1.6.0
diff --git a/setup.py b/setup.py
index d92c4bb2..2a669dc2 100644
--- a/setup.py
+++ b/setup.py
@@ -24,7 +24,7 @@
 setup(
     long_description=readme,
     name='datar',
-    version='0.1.1',
+    version='0.2.0',
     description='Port of dplyr and other related R packages in python, using pipda.',
     python_requires='==3.*,>=3.7.1',
     project_urls={"homepage": "https://github.com/pwwang/datar",
@@ -36,6 +36,7 @@
               'datar.dplyr', 'datar.stats', 'datar.tibble', 'datar.tidyr', 'datar.utils'],
     package_dir={"": "."},
     package_data={"datar.datasets": ["*.gz"]},
-    install_requires=['diot', 'modkit', 'pandas==1.*,>=1.2.0', 'pipda'],
+    install_requires=['diot', 'executing',
+                      'pandas==1.*,>=1.2.0', 'pipda', 'scipy==1.*,>=1.6.0'],
     extras_require={"dev": ["pytest", "pytest-cov"]},
 )


Test patch:
diff --git a/datar/base/testing.py b/datar/base/testing.py
new file mode 100644
index 00000000..f58f28c9
--- /dev/null
+++ b/datar/base/testing.py
@@ -0,0 +1,134 @@
+"""Testing functions in R"""
+
+# all_equal, all_equal_numeric, ...
+# all, any
+# is_true, is_false
+# is_finite, is_infinite, is_nan
+# is_function, is_atomic, is_vector, is_array, is_character, is_complex,
+# is_data_frame, is_double, is_element, (is_factor, is_ordered in base.factor)
+# is_integer, is_list, is_logical, is_numeric
+# is_unsorted, (is_qr in base.qr)
+import builtins
+from typing import Any, Callable, Iterable
+
+import numpy
+from pandas.core.dtypes.common import (
+    is_integer_dtype,
+    is_float_dtype,
+    is_numeric_dtype
+)
+from pipda import register_func
+
+from ..core.contexts import Context
+from ..core.types import TypeOrIter, BoolOrIter, is_scalar
+
+# pylint: disable=invalid-name
+
+def _register_type_testing(
+        name: str,
+        scalar_types: TypeOrIter,
+        dtype_checker: Callable[[Iterable], bool],
+        doc: str = ""
+) -> Callable:
+    """Register type testing function"""
+    @register_func(None, context=Context.EVAL)
+    def _testing(x: Any) -> bool:
+        """Type testing"""
+        if is_scalar(x):
+            return isinstance(x, scalar_types)
+        if hasattr(x, 'dtype'):
+            return dtype_checker(x)
+        return all(isinstance(elem, scalar_types) for elem in x)
+
+    _testing.__name__ = name
+    _testing.__doc__ = doc
+    return _testing
+
+is_numeric = _register_type_testing(
+    'is_numeric',
+    scalar_types=(int, float, complex, numpy.number),
+    dtype_checker=is_numeric_dtype,
+    doc="""Test if a value is numeric
+
+    Args:
+        x: The value to be checked
+
+    Returns:
+        True if the value is numeric; with a numeric dtype;
+        or all elements are numeric
+    """
+)
+
+is_integer = _register_type_testing(
+    'is_integer',
+    scalar_types=(int, numpy.integer),
+    dtype_checker=is_integer_dtype,
+    doc="""Test if a value is integers
+
+    Alias `is_int`
+
+    Args:
+        x: The value to be checked
+
+    Returns:
+        True if the value is an integer or integers; False otherwise.
+    """
+)
+
+is_int = is_integer
+
+is_double = _register_type_testing(
+    'is_double',
+    scalar_types=(float, numpy.float_),
+    dtype_checker=is_float_dtype,
+    doc="""Test if a value is integers
+
+    Alias `is_float`
+
+    Args:
+        x: The value to be checked
+
+    Returns:
+        True if the value is an integer or integers; False otherwise.
+    """
+)
+
+is_float = is_double
+
+@register_func(None, context=Context.EVAL)
+def is_atomic(x: Any) -> bool:
+    """Check if x is an atomic or scalar value
+
+    Args:
+        x: The value to be checked
+
+    Returns:
+        True if x is atomic otherwise False
+    """
+    return is_scalar(x)
+
+
+@register_func(None)
+def is_element(elem: Any, elems: Iterable[Any]) -> BoolOrIter:
+    """R's `is.element()` or `%in%`.
+
+    Alias `is_in()`
+
+    We can't do `a %in% b` in python (`in` behaves differently), so
+    use this function instead
+    """
+    out = numpy.isin(elem, elems)
+    if is_scalar(elem):
+        return bool(out)
+    return out
+
+is_in = is_element
+
+# pylint: disable=unnecessary-lambda, redefined-builtin
+all = register_func(None, context=Context.EVAL)(
+    # can't set attributes to builtins.all, so wrap it.
+    lambda *args, **kwargs: builtins.all(*args, **kwargs)
+)
+any = register_func(None, context=Context.EVAL)(
+    lambda *args, **kwargs: builtins.any(*args, **kwargs)
+)
diff --git a/tests/conftest.py b/tests/conftest.py
index ccb69751..5e250318 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -27,6 +27,15 @@ def assert_equal(x, y):
 def assert_iterable_equal(x, y, na=8525.8525, approx=False):
     x = [na if isna(elt) else elt for elt in x]
     y = [na if isna(elt) else elt for elt in y]
-    if approx:
+    if approx is True:
         x = pytest.approx(x)
+    elif approx:
+        x = pytest.approx(x, rel=approx)
     assert x == y
+
+def is_installed(pkg):
+    try:
+        __import__(pkg)
+        return True
+    except ImportError:
+        return False
diff --git a/tests/test_base.py b/tests/test_base.py
deleted file mode 100644
index fc93f80c..00000000
--- a/tests/test_base.py
+++ /dev/null
@@ -1,373 +0,0 @@
-import pytest
-import warnings
-import datetime
-
-import numpy
-from pandas import Interval, Categorical, DataFrame
-from datar import stats
-from datar.base import *
-
-from .conftest import assert_equal, assert_iterable_equal
-
-@pytest.fixture(autouse=True)
-def supress_warnings():
-    """Ignore pipda not being able to get node to detect piping"""
-    warnings.simplefilter("ignore")
-    yield
-    warnings.simplefilter("default")
-
-@pytest.mark.parametrize('x,expected', [
-    ([1,2,3], ['1', '2', '3']),
-    (numpy.array([1,2,3]), ['1','2','3']),
-    (1, '1'),
-])
-def test_as_character(x, expected):
-    assert_equal(as_character(x), expected)
-
-@pytest.mark.parametrize('x,format,try_formats,optional,tz,origin,expected', [
-    (["1jan1960", "2jan1960", "31mar1960", "30jul1960"],
-     "%d%b%Y", None, False, 0, None,
-     [datetime.date(1960, 1, 1),
-      datetime.date(1960, 1, 2),
-      datetime.date(1960, 3, 31),
-      datetime.date(1960, 7, 30)]),
-    (["02/27/92", "02/27/92", "01/14/92", "02/28/92", "02/01/92"],
-     "%m/%d/%y", None, False, 0, None,
-     [datetime.date(1992, 2, 27),
-      datetime.date(1992, 2, 27),
-      datetime.date(1992, 1, 14),
-      datetime.date(1992, 2, 28),
-      datetime.date(1992, 2, 1)]),
-    (32768,
-     None, None, False, 0, "1900-01-01",
-     [datetime.date(1989, 9, 19)]),
-    (35981,
-     None, None, False, 0, "1899-12-30",
-     [datetime.date(1998, 7, 5)]),
-    (34519,
-     None, None, False, 0, "1904-01-01",
-     [datetime.date(1998, 7, 5)]),
-    (734373 - 719529,
-     None, None, False, 0, datetime.datetime(1970, 1, 1),
-     [datetime.date(2010, 8, 23)]),
-    ([datetime.date(2010, 4, 13)],
-     None, None, False, 12, None,
-     [datetime.date(2010, 4, 13)]),
-    ([datetime.date(2010, 4, 13)],
-     None, None, False, 12 + 13, None,
-     [datetime.date(2010, 4, 14)]),
-    ([datetime.datetime(2010, 4, 13)],
-     None, None, False, 0 - 10, None,
-     [datetime.date(2010, 4, 12)]),
-])
-def test_as_date(x, format, try_formats, optional, tz, origin, expected):
-    assert_equal(as_date(
-        x,
-        format,
-        try_formats,
-        optional,
-        tz,
-        origin
-    ), expected)
-
-def test_as_date_error():
-    with pytest.raises(ValueError):
-        as_date(1.1)
-
-    with pytest.raises(ValueError):
-        as_date("1990-1-1", "%Y")
-
-    assert as_date("1990-1-1", "Y", optional=True).isna().all()
-
-@pytest.mark.parametrize('x,expected', [
-    ([1,2,3], [1.0, 2.0, 3.0]),
-    (numpy.array(['1','2','3']), [1.0, 2.0, 3.0])
-])
-def test_as_double(x, expected):
-    assert_equal(as_double(x), expected)
-
-@pytest.mark.parametrize('x,expected', [
-    ([1,2,3], Categorical([1,2,3])),
-    (numpy.array(['1','2','3']), Categorical(['1', '2', '3']))
-])
-def test_as_categorical(x, expected):
-    assert_equal(as_categorical(x), expected)
-
-@pytest.mark.parametrize('x,expected', [
-    ([1.0,2.0,3.0], [1,2,3]),
-    (numpy.array(['1','2','3']), [1,2,3]),
-    (factor(['1','2','3']), [0,1,2]),
-])
-def test_as_int(x, expected):
-    assert_equal(as_int(x), expected)
-
-@pytest.mark.parametrize('x,expected', [
-    ([1.0,2.0,3.0], [1,2,3]),
-    (numpy.array(['1','2','3']), [1,2,3])
-])
-def test_as_integer(x, expected):
-    assert_equal(as_integer(x), expected)
-
-@pytest.mark.parametrize('x,expected', [
-    ([1.0,0.0], [True, False]),
-    (numpy.array(['1','0']), [True, False])
-])
-def test_as_logical(x, expected):
-    assert_equal(as_logical(x), expected)
-
-def test_factor():
-    ff = factor(list("statistics"), levels=letters)
-    assert_equal(ff.to_numpy().flatten(), list("statistics"))
-    assert_equal(ff.categories.tolist(), letters)
-
-    ffi = as_integer(ff)
-    assert_equal(ffi, [18,19,0,19,8,18,19,8,2,18])
-
-    ff = factor(ff)
-    assert_equal(ff.categories.tolist(), list('acist'))
-
-    z = factor(LETTERS[-3:], ordered=TRUE)
-    assert "['X' < 'Y' < 'Z']" in str(z)
-
-    x = factor(c(1, 2, NA), exclude=None)
-    assert len(x) == 3
-    # different from R
-    assert_equal(x.categories.tolist(), [1, 2])
-
-    x = factor(z, exclude = "X")
-    assert is_na(x[0])
-    assert len(x.categories) == 2
-
-@pytest.mark.parametrize('x,expected', [
-    (1.4, 2),
-    ([1.1,2.1], [2, 3])
-])
-def test_ceiling(x, expected):
-    assert_equal(ceiling(x), expected)
-
-@pytest.mark.parametrize('x,expected', [
-    (1.4, 1),
-    ([1.1,2.1], [1, 2])
-])
-def test_floor(x, expected):
-    assert_equal(floor(x), expected)
-
-def test_c():
-    assert c(1,2,3) == [1,2,3]
-    assert c(c(1,2), 3) == [1,2,3]
-
-def test_rowcolnames():
-    df = DataFrame(dict(x=[1,2,3]))
-    assert colnames(df) == ['x']
-    assert rownames(df) == [0, 1, 2]
-    df = DataFrame([1,2,3], index=['a', 'b', 'c'])
-    assert colnames(df) == [0]
-    assert rownames(df) == ['a', 'b', 'c']
-
-def test_cumx():
-    #                                  1,2,3,4,5
-    assert_equal(cumsum(range(1,6)),  [1,3,6,10,15])
-    assert_equal(cumprod(range(1,6)), [1,2,6,24,120])
-    assert_equal(cummin([3,2,1]), [3,2,1])
-    assert_equal(cummax([3,2,1]), [3,3,3])
-
-def test_cut():
-    z = stats.rnorm(10000)
-    tab = table(cut(z, breaks=range(-6, 7)))
-    assert tab.shape == (1, 12)
-    assert tab.columns.tolist() == [
-        Interval(-6, -5, closed='right'),
-        Interval(-5, -4, closed='right'),
-        Interval(-4, -3, closed='right'),
-        Interval(-3, -2, closed='right'),
-        Interval(-2, -1, closed='right'),
-        Interval(-1, 0, closed='right'),
-        Interval(0, 1, closed='right'),
-        Interval(1, 2, closed='right'),
-        Interval(2, 3, closed='right'),
-        Interval(3, 4, closed='right'),
-        Interval(4, 5, closed='right'),
-        Interval(5, 6, closed='right'),
-    ]
-    assert sum(tab.values.flatten()) == 10000
-
-    z = cut([1] * 5, 4)
-    assert_equal(z.to_numpy(), [Interval(0.9995, 1.0, closed='right')] * 5)
-    assert_equal(z.categories.to_list(), [
-        Interval(0.999,  0.9995, closed='right'),
-        Interval(0.9995, 1.0, closed='right'),
-        Interval(1.0,    1.0005, closed='right'),
-        Interval(1.0005, 1.001, closed='right'),
-    ])
-
-    z = stats.rnorm(100)
-    tab = table(cut(z, breaks=[pi/3.0*i for i in range(0,4)]))
-    assert str(tab.columns.tolist()[0]) == '(0.0, 1.05]'
-
-    tab = table(cut(z, breaks=[pi/3.0*i for i in range(0,4)], precision=3))
-    assert str(tab.columns.tolist()[0]) == '(0.0, 1.047]'
-
-    aaa = c(1,2,3,4,5,2,3,4,5,6,7)
-    ct = cut(aaa, 3, precision=3, ordered_result=True)
-    assert str(ct[0]) == '(0.994, 3.0]'
-
-def test_diag():
-    assert dim(diag(3)) == (3,3)
-    assert dim(diag(10, 3, 4)) == (3,4)
-    x = diag(c(1j,2j))
-    assert x.iloc[0,0] == 0+1j
-    assert x.iloc[0,1] == 0+0j
-    assert x.iloc[1,0] == 0+0j
-    assert x.iloc[1,1] == 0+2j
-    x = diag(TRUE, 3)
-    assert sum(x.values.flatten()) == 3
-    x = diag(c(2,1), 4)
-    assert_iterable_equal(diag(x), [2,1,2,1])
-
-def test_sample():
-    x = sample(range(1, 13))
-    assert set(x) == set(range(1,13))
-
-    y = sample(x, replace=TRUE)
-    assert len(y) <= 12
-
-    z = sample(c(0,1), 100, replace=TRUE)
-    assert set(z) == {0, 1}
-    assert len(z) == 100
-
-    w = sample('abc', 100, replace=TRUE)
-    assert set(w) == {'a', 'b', 'c'}
-    assert len(z) == 100
-
-def test_table():
-    # https://www.rdocumentation.org/packages/base/versions/3.6.2/topics/table
-    from datar import f
-    from datar.datasets import warpbreaks, state_division, state_region, airquality
-    z = stats.rpois(100, 5)
-    x = table(z)
-    assert sum(x.values.flatten()) == 100
-
-    #-----------------
-    with context(warpbreaks) as _:
-        tab = table(f.wool, f.tension)
-
-    assert tab.columns.tolist() == ['H', 'L', 'M']
-    assert tab.index.tolist() == ['A', 'B']
-    assert_equal(tab.values.flatten(), [9] * 6)
-
-    tab = table(warpbreaks.loc[:, ['wool', 'tension']])
-    assert tab.columns.tolist() == ['H', 'L', 'M']
-    assert tab.index.tolist() == ['A', 'B']
-    assert_equal(tab.values.flatten(), [9] * 6)
-
-    #-----------------
-    tab = table(state_division, state_region)
-    assert tab.loc['New England', 'Northeast'] == 6
-
-    #-----------------
-    # wait for cut
-    # with context(airquality) as _:
-    #     tab = table(cut(f.Temp, stats.quantile(f.Temp)), f.Month)
-
-    # assert tab.iloc[0,0] == 24
-
-    #-----------------
-    a = letters[:3]
-    tab = table(a, sample(a))
-    assert sum(tab.values.flatten()) == 3
-
-    #-----------------
-    tab = table(a, sample(a), dnn=['x', 'y'])
-    assert tab.index.name == 'x'
-    assert tab.columns.name == 'y'
-
-    #-----------------
-    a = c(NA, Inf, (1.0/(i+1) for i in range(3)))
-    a = a * 10
-    # tab = table(a)
-    # assert_equal(tab.values.flatten(), [10] * 4)
-
-    tab = table(a, exclude=None)
-    assert_equal(tab.values.flatten(), [10] * 5)
-
-    #------------------
-    b = as_factor(c("A","B","C") * 10)
-    tab = table(b)
-    assert tab.shape == (1, 3)
-    assert_equal(tab.values.flatten(), [10] * 3)
-
-    tab = table(b, exclude="B")
-    assert tab.shape == (1, 2)
-    assert_equal(tab.values.flatten(), [10] * 2)
-    assert 'B' not in tab.columns
-
-    #-------------------
-    d = factor(rep(c("A","B","C"), 10), levels=c("A","B","C","D","E"))
-    tab = table(d, exclude="B", dnn=['x'])
-    assert_equal(tab.columns.to_list(), ["A", "C", "D", "E"])
-    assert_equal(tab.values.flatten(), [10, 10, 0, 0])
-
-    d2 = factor(rep(c("A","B","C"), 10), levels=c("A","B","C","D","E"))
-    tab = table(d, d2, exclude="B")
-    assert tab.shape == (4, 4)
-
-    tab = table("abc", "cba", dnn='x')
-    assert tab.shape == (3,3)
-    assert sum(tab.values.flatten()) == 3
-
-    with context(airquality) as _:
-        tab = table(f.Ozone, f.Solar_R, exclude=None)
-    assert '<NA>' in tab.columns
-    assert '<NA>' in tab.index
-
-def test_table_error():
-    from datar.datasets import iris, warpbreaks
-    with pytest.raises(ValueError):
-        table(iris)
-    with pytest.raises(ValueError):
-        table(warpbreaks, iris)
-    with pytest.raises(ValueError):
-        table(warpbreaks.wool, iris)
-
-@pytest.mark.parametrize('x, times, length, each, expected', [
-    (range(4), 2, None, 1, [0,1,2,3]*2),
-    (range(4), 1, None, 2, [0,0,1,1,2,2,3,3]),
-    (range(4), [2]*4, None, 1, [0,0,1,1,2,2,3,3]),
-    (range(4), [2,1]*2, None, 1, [0,0,1,2,2,3]),
-    (range(4), 1, 4, 2, [0,0,1,1]),
-    (range(4), 1, 10, 2, [0,0,1,1,2,2,3,3,0,0]),
-    (range(4), 3, None, 2, [0,0,1,1,2,2,3,3] * 3),
-    (1, 7, None, 1, [1,1,1,1,1,1,1]),
-])
-def test_rep(x, times, length, each, expected):
-    assert_iterable_equal(rep(x, times=times, length=length, each=each), expected)
-
-def test_rep_error():
-    with pytest.raises(ValueError):
-        rep(c(1,2,3), c(1,2))
-    with pytest.raises(ValueError):
-        rep(c(1,2,3), c(1,2,3), each=2)
-
-def test_droplevels():
-    x = factor(c(1,2,3), levels=c(1,2,3,4))
-    x = droplevels(x)
-    assert len(x.categories) == 3
-
-@pytest.mark.parametrize('from_, to, by, length_out, along_with, expect', [
-    (0, 1, None, 11, None, [.0,.1,.2,.3,.4,.5,.6,.7,.8,.9,1.]),
-    ([4,2,1,9,3]*4, None, None, None, None, range(1, 21)),
-    (1, 9, 2, None, None, [1,3,5,7,9]),
-    (1, 9, 2, None, None, [1,3,5,7,9]),
-    (1, 6, 3, None, None, [1, 4]),
-    (1.575, 5.125, 0.05, None, None, [
-        1.575, 1.625, 1.675, 1.725, 1.775, 1.825, 1.875, 1.925, 1.975, 2.025, 2.075, 2.125,
-        2.175, 2.225, 2.275, 2.325, 2.375, 2.425, 2.475, 2.525, 2.575, 2.625, 2.675, 2.725,
-        2.775, 2.825, 2.875, 2.925, 2.975, 3.025, 3.075, 3.125, 3.175, 3.225, 3.275, 3.325,
-        3.375, 3.425, 3.475, 3.525, 3.575, 3.625, 3.675, 3.725, 3.775, 3.825, 3.875, 3.925,
-        3.975, 4.025, 4.075, 4.125, 4.175, 4.225, 4.275, 4.325, 4.375, 4.425, 4.475, 4.525,
-        4.575, 4.625, 4.675, 4.725, 4.775, 4.825, 4.875, 4.925, 4.975, 5.025, 5.075, 5.125,
-    ]),
-    (17, None, None, None, None, range(1,18))
-])
-def test_seq(from_, to, by, length_out, along_with, expect):
-    assert_iterable_equal(seq(from_, to, by, length_out, along_with), expect, approx=True)
diff --git a/tests/test_base_arithmetic.py b/tests/test_base_arithmetic.py
new file mode 100644
index 00000000..e4540d2c
--- /dev/null
+++ b/tests/test_base_arithmetic.py
@@ -0,0 +1,75 @@
+import pytest
+
+from pandas.testing import assert_frame_equal
+from datar import f
+from datar.base import NA
+from datar.datar import drop_index
+from datar.tibble import tibble
+from datar.base.arithmetic import *
+from .conftest import assert_iterable_equal
+
+def test_sum():
+    assert sum(1) == 1
+    assert sum([1,2]) == 3
+    assert_iterable_equal([sum([1,2, NA])], [NA])
+    assert sum([1,2,NA], na_rm=True) == 3
+
+def test_mean():
+    assert mean(1) == 1
+    assert mean([1.0, 2.0]) == 1.5
+
+def test_median():
+    assert median(1) == 1
+    assert median([1.0, 20, 3.0]) == 3.0
+
+def test_min():
+    assert min(1) == 1
+    assert min([1,2,3]) == 1
+
+def test_max():
+    assert max(1) == 1
+    assert max([1,2,3]) == 3
+
+def test_var():
+    with pytest.warns(RuntimeWarning):
+        assert_iterable_equal([var(1)], [NA])
+    assert var([1,2,3]) == 1
+
+def test_pmin():
+    assert pmin(1,2,3) == 1
+    assert_iterable_equal(pmin(1,[-1, 2], [0, 3]), [-1, 1])
+
+def test_pmax():
+    assert pmax(1,2,3) == 3
+    assert_iterable_equal(pmax(1,[-1, 2], [0, 3]), [1, 3])
+
+def test_round():
+    assert round(1.23456) == 1.0
+    assert_iterable_equal(round([1.23456, 3.45678]), [1.0, 3.0])
+    assert_iterable_equal(round([1.23456, 3.45678], 1), [1.2, 3.5])
+
+def test_sqrt():
+    assert sqrt(1) == 1
+    with pytest.warns(RuntimeWarning):
+        assert_iterable_equal([sqrt(-1)], [NA])
+
+
+def test_abs():
+    assert abs(1) == 1
+    assert_iterable_equal(abs([-1, 1]), [1, 1])
+
+def test_ceiling():
+    assert ceiling(1.1) == 2
+    assert_iterable_equal(ceiling([-1.1, 1.1]), [-1, 2])
+
+def test_floor():
+    assert floor(1.1) == 1
+    assert_iterable_equal(floor([-1.1, 1.1]), [-2, 1])
+
+def test_cov():
+    df = tibble(x=f[1:3], y=f[4:6])
+    out = cov(df) >> drop_index()
+    assert_frame_equal(out, tibble(x=[1.0,1.0], y=[1.0,1.0]))
+
+    out = cov([1,2,3], [4,5,6])
+    assert out == 1.0
diff --git a/tests/test_base_bessel.py b/tests/test_base_bessel.py
new file mode 100644
index 00000000..15f940c6
--- /dev/null
+++ b/tests/test_base_bessel.py
@@ -0,0 +1,51 @@
+import pytest
+from datar.base.bessel import *
+from datar.base import NA, Inf
+from .conftest import assert_iterable_equal, is_installed
+
+pytestmark = pytest.mark.skipif(
+    not is_installed('scipy'),
+    reason="'scipy' is not installed"
+)
+
+@pytest.mark.parametrize('x,nu,exp', [
+    ([0,1,2,3,NA], 0, [1,0.765198,0.223891,-0.260052,NA]),
+    ([0,1,2,3,NA], 1, [0,0.440051,0.576725,0.339059,NA]),
+    ([0,1,2,3,NA], 2, [0,0.114903,0.352834,0.486091,NA])
+])
+def test_bessel_j(x, nu, exp):
+    out = bessel_j(x, nu)
+    assert_iterable_equal(out, exp, approx=1e-5)
+
+@pytest.mark.parametrize('x,nu,exp', [
+    ([0,1,2,3,NA], 0, [-Inf,0.088257,0.510376,0.376850,NA]),
+    ([0,1,2,3,NA], 1, [-Inf,-0.781213,-0.107032,0.324674,NA]),
+    ([0,1,2,3,NA], 2, [-Inf,-1.650683,-0.617408,-0.160400,NA])
+])
+def test_bessel_y(x, nu, exp):
+    out = bessel_y(x, nu)
+    assert_iterable_equal(out, exp, approx=1e-5)
+
+@pytest.mark.parametrize('x,nu,expon_scaled,exp', [
+    ([0,1,2,3,NA], 0, True, [1,0.46575961,0.30850832,0.2430004,NA]),
+    ([0,1,2,3,NA], 1, True, [0,0.20791042,0.21526929,0.1968267,NA]),
+    ([0,1,2,3,NA], 2, True, [0,0.04993878,0.09323903,0.1117825,NA]),
+    ([0,1,2,3,NA], 0, False, [1,1.2660659,2.2795853,4.880793,NA]),
+    ([0,1,2,3,NA], 1, False, [0,0.5651591,1.5906369,3.953370,NA]),
+    ([0,1,2,3,NA], 2, False, [0,0.1357477,0.6889484,2.245212,NA])
+])
+def test_bessel_i(x, nu, expon_scaled, exp):
+    out = bessel_i(x, nu, expon_scaled)
+    assert_iterable_equal(out, exp, approx=1e-5)
+
+@pytest.mark.parametrize('x,nu,expon_scaled,exp', [
+    ([0,1,2,3,NA], 0, True, [Inf,1.14446307980689,0.841568215070772,0.697761598043852,NA]),
+    ([0,1,2,3,NA], 1, True, [Inf,1.63615348626326,1.03347684706869,0.806563480128787,NA]),
+    ([0,1,2,3,NA], 2, True, [Inf,4.41677005233341,1.87504506213946,1.23547058479638,NA]),
+    ([0,1,2,3,NA], 0, False, [Inf,0.421024438240708,0.113893872749533,0.0347395043862793,NA]),
+    ([0,1,2,3,NA], 1, False, [Inf,0.601907230197235,0.139865881816522,0.0401564311281942,NA]),
+    ([0,1,2,3,NA], 2, False, [Inf,1.62483889863518,0.253759754566056,0.061510458471742,NA])
+])
+def test_bessel_k(x, nu, expon_scaled, exp):
+    out = bessel_k(x, nu, expon_scaled)
+    assert_iterable_equal(out, exp, approx=1e-5)
diff --git a/tests/test_base_casting.py b/tests/test_base_casting.py
new file mode 100644
index 00000000..9ede53c3
--- /dev/null
+++ b/tests/test_base_casting.py
@@ -0,0 +1,29 @@
+import pytest
+
+import numpy
+from datar.base.casting import *
+from datar.base.factor import *
+from .conftest import assert_iterable_equal
+
+def test_as_double():
+    assert isinstance(as_double(1), numpy.double)
+    assert as_double(numpy.array([1,2])).dtype == numpy.double
+    assert numpy.array(as_double([1,2])).dtype == numpy.double
+
+def test_as_float():
+    assert isinstance(as_double(1), numpy.float_)
+
+def test_as_integer():
+    assert isinstance(as_integer(1), numpy.int_)
+    fct = factor(list('abc'))
+    assert_iterable_equal(as_integer(fct), [0,1,2])
+    # NAs kept
+    out = as_integer(NA)
+    assert_iterable_equal([out], [NA])
+    out = as_integer(numpy.array([1.0,NA]))
+    assert out.dtype == object
+
+def test_as_numeric():
+    assert as_numeric('1') == 1
+    assert as_numeric('1.1', _keep_na=False) == 1.1
+    assert_iterable_equal(as_numeric(['1', NA]), [1, NA])
diff --git a/tests/test_base_complex.py b/tests/test_base_complex.py
new file mode 100644
index 00000000..10f89881
--- /dev/null
+++ b/tests/test_base_complex.py
@@ -0,0 +1,15 @@
+import pytest
+
+from datar.base.complex import *
+from .conftest import assert_iterable_equal
+
+def test_complex():
+    x = 1+2j
+    assert re(x) == 1
+    assert im(x) == 2
+    assert mod(x) == pytest.approx(2.236068)
+    assert arg(x) == pytest.approx(1.107149)
+    assert conj(x) == 1-2j
+    assert is_complex(x)
+    assert not is_complex(1)
+    assert as_complex(1) == 1+0j
diff --git a/tests/test_base_cum.py b/tests/test_base_cum.py
new file mode 100644
index 00000000..891060e1
--- /dev/null
+++ b/tests/test_base_cum.py
@@ -0,0 +1,24 @@
+import pytest
+
+from datar.base.cum import *
+from .conftest import assert_iterable_equal
+
+def test_cumsum():
+    assert cumsum(1) == 1
+    assert_iterable_equal(cumsum([1,2,3]), [1,3,6])
+    assert_iterable_equal(cumsum([1,NA,3]), [1,NA,NA])
+
+def test_cumprod():
+    assert cumprod(1) == 1
+    assert_iterable_equal(cumprod([1,2,3]), [1,2,6])
+    assert_iterable_equal(cumprod([1,NA,3]), [1,NA,NA])
+
+def test_cummin():
+    assert cummin(1) == 1
+    assert_iterable_equal(cummin([1,2,3]), [1,1,1])
+    assert_iterable_equal(cummin([1,NA,3]), [1,NA,NA])
+
+def test_cummax():
+    assert cummax(1) == 1
+    assert_iterable_equal(cummax([1,2,3]), [1,2,3])
+    assert_iterable_equal(cummax([1,NA,3]), [1,NA,NA])
diff --git a/tests/test_base_date.py b/tests/test_base_date.py
new file mode 100644
index 00000000..9a7c6081
--- /dev/null
+++ b/tests/test_base_date.py
@@ -0,0 +1,59 @@
+import pytest
+
+from datar.base.date import *
+from .conftest import assert_iterable_equal
+
+@pytest.mark.parametrize('x,format,try_formats,optional,tz,origin,expected', [
+    (["1jan1960", "2jan1960", "31mar1960", "30jul1960"],
+     "%d%b%Y", None, False, 0, None,
+     [datetime.date(1960, 1, 1),
+      datetime.date(1960, 1, 2),
+      datetime.date(1960, 3, 31),
+      datetime.date(1960, 7, 30)]),
+    (["02/27/92", "02/27/92", "01/14/92", "02/28/92", "02/01/92"],
+     "%m/%d/%y", None, False, 0, None,
+     [datetime.date(1992, 2, 27),
+      datetime.date(1992, 2, 27),
+      datetime.date(1992, 1, 14),
+      datetime.date(1992, 2, 28),
+      datetime.date(1992, 2, 1)]),
+    (32768,
+     None, None, False, 0, "1900-01-01",
+     [datetime.date(1989, 9, 19)]),
+    (35981,
+     None, None, False, 0, "1899-12-30",
+     [datetime.date(1998, 7, 5)]),
+    (34519,
+     None, None, False, 0, "1904-01-01",
+     [datetime.date(1998, 7, 5)]),
+    (734373 - 719529,
+     None, None, False, 0, datetime.datetime(1970, 1, 1),
+     [datetime.date(2010, 8, 23)]),
+    ([datetime.date(2010, 4, 13)],
+     None, None, False, 12, None,
+     [datetime.date(2010, 4, 13)]),
+    ([datetime.date(2010, 4, 13)],
+     None, None, False, 12 + 13, None,
+     [datetime.date(2010, 4, 14)]),
+    ([datetime.datetime(2010, 4, 13)],
+     None, None, False, 0 - 10, None,
+     [datetime.date(2010, 4, 12)]),
+])
+def test_as_date(x, format, try_formats, optional, tz, origin, expected):
+    assert_iterable_equal(as_date(
+        x,
+        format,
+        try_formats,
+        optional,
+        tz,
+        origin
+    ), expected)
+
+def test_as_date_error():
+    with pytest.raises(ValueError):
+        as_date(1.1)
+
+    with pytest.raises(ValueError):
+        as_date("1990-1-1", "%Y")
+
+    assert as_date("1990-1-1", "Y", optional=True).isna().all()
diff --git a/tests/test_base_factor.py b/tests/test_base_factor.py
new file mode 100644
index 00000000..84e87d99
--- /dev/null
+++ b/tests/test_base_factor.py
@@ -0,0 +1,36 @@
+import pytest
+
+from datar.base.factor import *
+from .conftest import assert_iterable_equal
+
+def test_droplevels():
+    fct = factor([1,2,3], levels=[1,2,3,4])
+    out = droplevels(fct)
+    assert_iterable_equal(fct, out)
+    assert_iterable_equal(levels(out), [1,2,3])
+
+def test_levels():
+    assert levels(1) is None
+
+def test_factor():
+    out = factor()
+    assert len(out) == 0
+    assert len(levels(out)) == 0
+
+    out = factor([1,2,3], exclude=1)
+    assert_iterable_equal(out, [NA,2,3])
+    assert_iterable_equal(levels(out), [2,3])
+
+    out = factor(out)
+    assert_iterable_equal(out, [NA,2,3])
+    assert_iterable_equal(levels(out), [2,3])
+
+def test_as_facotr():
+    out = as_factor([1,2,3])
+    assert_iterable_equal(out, [1,2,3])
+    assert_iterable_equal(levels(out), [1,2,3])
+
+def test_is_factor():
+    out = as_factor([])
+    assert is_factor(out)
+    assert not is_factor([])
diff --git a/tests/test_base_funs.py b/tests/test_base_funs.py
new file mode 100644
index 00000000..538ad4f0
--- /dev/null
+++ b/tests/test_base_funs.py
@@ -0,0 +1,70 @@
+import pytest
+
+from pandas import Interval, DataFrame
+from pandas.testing import assert_frame_equal
+from datar.base.funs import *
+from datar.base import table, pi
+from datar.stats import rnorm
+from .conftest import assert_iterable_equal
+
+def test_cut():
+    z = rnorm(10000)
+    tab = table(cut(z, breaks=range(-6, 7)))
+    assert tab.shape == (1, 12)
+    assert tab.columns.tolist() == [
+        Interval(-6, -5, closed='right'),
+        Interval(-5, -4, closed='right'),
+        Interval(-4, -3, closed='right'),
+        Interval(-3, -2, closed='right'),
+        Interval(-2, -1, closed='right'),
+        Interval(-1, 0, closed='right'),
+        Interval(0, 1, closed='right'),
+        Interval(1, 2, closed='right'),
+        Interval(2, 3, closed='right'),
+        Interval(3, 4, closed='right'),
+        Interval(4, 5, closed='right'),
+        Interval(5, 6, closed='right'),
+    ]
+    assert sum(tab.values.flatten()) == 10000
+
+    z = cut([1] * 5, 4)
+    assert_iterable_equal(z.to_numpy(), [Interval(0.9995, 1.0, closed='right')] * 5)
+    assert_iterable_equal(z.categories.to_list(), [
+        Interval(0.999,  0.9995, closed='right'),
+        Interval(0.9995, 1.0, closed='right'),
+        Interval(1.0,    1.0005, closed='right'),
+        Interval(1.0005, 1.001, closed='right'),
+    ])
+
+    z = rnorm(100)
+    tab = table(cut(z, breaks=[pi/3.0*i for i in range(0,4)]))
+    assert str(tab.columns.tolist()[0]) == '(0.0, 1.05]'
+
+    tab = table(cut(z, breaks=[pi/3.0*i for i in range(0,4)], precision=3))
+    assert str(tab.columns.tolist()[0]) == '(0.0, 1.047]'
+
+    aaa = [1,2,3,4,5,2,3,4,5,6,7]
+    ct = cut(aaa, 3, precision=3, ordered_result=True)
+    assert str(ct[0]) == '(0.994, 3.0]'
+
+def test_identity():
+    assert identity(1) == 1
+    assert identity(1.23) == 1.23
+
+def test_expandgrid():
+    df = expandgrid([1,2], [3,4])
+    assert_frame_equal(df, DataFrame({
+        'Var0': [1,1,2,2],
+        'Var1': [3,4,3,4],
+    }))
+
+def test_data_context():
+    from datar import f
+    df = DataFrame(dict(a=[1,2], b=[3,4]))
+    with data_context(df) as _:
+        out = expandgrid(f.a, f.b)
+
+    assert_frame_equal(out, DataFrame({
+        'a': [1,1,2,2],
+        'b': [3,4,3,4],
+    }))
diff --git a/tests/test_base_logical.py b/tests/test_base_logical.py
new file mode 100644
index 00000000..3d60fcc1
--- /dev/null
+++ b/tests/test_base_logical.py
@@ -0,0 +1,21 @@
+import pytest
+
+from datar.base.logical import *
+from .conftest import assert_iterable_equal
+
+def test_as_logical():
+    assert as_logical(1) is True
+    assert_iterable_equal(as_logical([2,0]), [True, False])
+
+def test_is_logical():
+    assert not is_logical(1)
+    assert is_logical(True)
+    assert is_logical(numpy.array([True, False]))
+
+def test_is_true():
+    assert is_true(True)
+    assert not is_true([True, True])
+
+def test_is_false():
+    assert is_false(False)
+    assert not is_false([False, False])
diff --git a/tests/test_base_na.py b/tests/test_base_na.py
new file mode 100644
index 00000000..46c9f13e
--- /dev/null
+++ b/tests/test_base_na.py
@@ -0,0 +1,23 @@
+import pytest
+
+from datar.base.na import *
+from datar.base.null import *
+from .conftest import assert_iterable_equal
+
+def test_is_na():
+    assert is_na(NA)
+    assert is_na(NULL) # instead of logical(0) in R
+    assert not is_na(1)
+    assert not is_na('a')
+    assert not is_na('NA')
+    assert not is_na('<NA>')
+    assert_iterable_equal(is_na([1, NA]), [False, True])
+
+def test_any_na():
+    assert any_na(NA)
+    assert not any_na(1)
+    assert not any_na('<NA>')
+    assert any_na([1, NA])
+    assert not any_na([1, [2, NA]])
+    assert any_na([1, [2, NA]], recursive=True)
+    assert not any_na([1, [2, 3]], recursive=True)
diff --git a/tests/test_base_null.py b/tests/test_base_null.py
new file mode 100644
index 00000000..1f6004fe
--- /dev/null
+++ b/tests/test_base_null.py
@@ -0,0 +1,16 @@
+import pytest
+
+from datar.base.null import *
+
+def test_null_is_none():
+    assert NULL is None
+
+def test_as_null():
+    assert as_null() is NULL
+    assert as_null(1) is NULL
+    assert as_null(a=1) is NULL
+    assert as_null(1, a=1) is NULL
+
+def test_is_null():
+    assert is_null(NULL)
+    assert not is_null(1)
diff --git a/tests/test_base_random.py b/tests/test_base_random.py
new file mode 100644
index 00000000..5f520eeb
--- /dev/null
+++ b/tests/test_base_random.py
@@ -0,0 +1,16 @@
+import pytest
+import random
+import numpy
+from datar.base.random import *
+
+def test_standard_random_seed():
+    set_seed(900)
+    assert random.randint(1,6) == 5
+    assert random.randint(1,6) == 2
+    assert random.randint(1,6) == 3
+
+def test_numpy_random_seed():
+    set_seed(900)
+    assert numpy.random.randint(1,6) == 2
+    assert numpy.random.randint(1,6) == 1
+    assert numpy.random.randint(1,6) == 5
diff --git a/tests/test_base_seq.py b/tests/test_base_seq.py
new file mode 100644
index 00000000..4c8f3c28
--- /dev/null
+++ b/tests/test_base_seq.py
@@ -0,0 +1,89 @@
+import pytest
+
+from datar.base.seq import *
+from .conftest import assert_iterable_equal
+
+@pytest.mark.parametrize('from_, to, by, length_out, along_with, expect', [
+    (0, 1, None, 11, None, [.0,.1,.2,.3,.4,.5,.6,.7,.8,.9,1.]),
+    ([4,2,1,9,3]*4, None, None, None, None, range(1, 21)),
+    (1, 9, 2, None, None, [1,3,5,7,9]),
+    (1, 9, 2, None, None, [1,3,5,7,9]),
+    (1, 6, 3, None, None, [1, 4]),
+    (1.575, 5.125, 0.05, None, None, [
+        1.575, 1.625, 1.675, 1.725, 1.775, 1.825, 1.875, 1.925, 1.975, 2.025, 2.075, 2.125,
+        2.175, 2.225, 2.275, 2.325, 2.375, 2.425, 2.475, 2.525, 2.575, 2.625, 2.675, 2.725,
+        2.775, 2.825, 2.875, 2.925, 2.975, 3.025, 3.075, 3.125, 3.175, 3.225, 3.275, 3.325,
+        3.375, 3.425, 3.475, 3.525, 3.575, 3.625, 3.675, 3.725, 3.775, 3.825, 3.875, 3.925,
+        3.975, 4.025, 4.075, 4.125, 4.175, 4.225, 4.275, 4.325, 4.375, 4.425, 4.475, 4.525,
+        4.575, 4.625, 4.675, 4.725, 4.775, 4.825, 4.875, 4.925, 4.975, 5.025, 5.075, 5.125,
+    ]),
+    (17, None, None, None, None, range(1,18))
+])
+def test_seq(from_, to, by, length_out, along_with, expect):
+    assert_iterable_equal(seq(from_, to, by, length_out, along_with), expect, approx=True)
+
+def test_seq_along():
+    assert_iterable_equal(seq_along([]), [])
+    assert_iterable_equal(seq_along([1,2]), [1,2])
+    assert_iterable_equal(seq_along(['a', 'b']), [1,2])
+    assert_iterable_equal(seq_along(['a', 'b'], _base0=True), [0,1])
+
+def test_seq_len(caplog):
+    assert_iterable_equal(seq_len([3,4]), [1,2,3])
+    assert 'first element' in caplog.text
+
+def test_seq_derives():
+    assert_iterable_equal(seq(along_with=['a', 'b']), [1,2])
+    assert_iterable_equal(seq(length_out=2), [1,2])
+    assert_iterable_equal(seq(to=2), [1,2])
+
+@pytest.mark.parametrize('x, times, length, each, expected', [
+    (range(4), 2, None, 1, [0,1,2,3]*2),
+    (range(4), 1, None, 2, [0,0,1,1,2,2,3,3]),
+    (range(4), [2]*4, None, 1, [0,0,1,1,2,2,3,3]),
+    (range(4), [2,1]*2, None, 1, [0,0,1,2,2,3]),
+    (range(4), 1, 4, 2, [0,0,1,1]),
+    (range(4), 1, 10, 2, [0,0,1,1,2,2,3,3,0,0]),
+    (range(4), 3, None, 2, [0,0,1,1,2,2,3,3] * 3),
+    (1, 7, None, 1, [1,1,1,1,1,1,1]),
+])
+def test_rep(x, times, length, each, expected):
+    assert_iterable_equal(rep(x, times=times, length=length, each=each), expected)
+
+def test_rep_error():
+    with pytest.raises(ValueError):
+        rep(c(1,2,3), c(1,2))
+    with pytest.raises(ValueError):
+        rep(c(1,2,3), c(1,2,3), each=2)
+
+def test_sample():
+    x = sample(range(1, 13))
+    assert set(x) == set(range(1,13))
+
+    y = sample(x, replace=True)
+    assert len(y) <= 12
+
+    z = sample(c(0,1), 100, replace=True)
+    assert set(z) == {0, 1}
+    assert len(z) == 100
+
+    w = sample('abc', 100, replace=True)
+    assert set(w) == {'a', 'b', 'c'}
+    assert len(z) == 100
+
+def test_rev():
+    assert_iterable_equal(rev([1,2]), [2,1])
+    a = numpy.array([1,2], dtype=float)
+    out = rev(a)
+    assert_iterable_equal(out, [2.0,1.0])
+    assert out.dtype == float
+
+def test_unique():
+    a = [1,2,2,3]
+    assert_iterable_equal(unique(a), [1,2,3])
+
+def test_length():
+    assert length(1) == 1
+    assert length([1,2]) == 2
+    assert_iterable_equal(lengths(1), [1])
+    assert_iterable_equal(lengths([[1], [2,3]]), [1,2])
diff --git a/tests/test_base_special.py b/tests/test_base_special.py
new file mode 100644
index 00000000..c1ee3d6e
--- /dev/null
+++ b/tests/test_base_special.py
@@ -0,0 +1,77 @@
+from numpy import tri
+import pytest
+
+from datar.base.special import *
+from datar.base import Inf
+from .conftest import assert_iterable_equal, is_installed
+
+pytestmark = pytest.mark.skipif(
+    not is_installed('scipy'),
+    reason="'scipy' is not installed"
+)
+
+@pytest.mark.parametrize('a,b,log,exp', [
+    (1, 2, False, 0.5),
+    ([1,2],2,False,[.5,.1666666666667]),
+    (1,[1,2],False,[1.0, 0.5]),
+    (1,2,True,-0.6931472),
+    ([1,2],2,True,[-0.6931472,-1.7917595]),
+    (1,[1,2],True,[0.0, -0.6931472]),
+])
+def test_beta(a, b, log, exp):
+    fun = lbeta if log else beta
+    out = fun(a, b)
+
+    if is_scalar(exp):
+        out, exp = [out], [exp]
+    assert_iterable_equal(out, exp, approx=True)
+
+@pytest.mark.parametrize('x,fun,exp', [
+    (-1, gamma, NA),
+    (0, gamma, NA),
+    ([0,1,2], gamma, [NA, 1, 1]),
+    (-1, lgamma, NA),
+    (0, lgamma, NA),
+    ([0,1,2], lgamma, [NA, 0, 0]),
+    (1, digamma, -0.5772157),
+    (-1, digamma, NA),
+    (0, digamma, NA),
+    (1, trigamma, 1.644934),
+    (-1, trigamma, Inf),
+    (0, trigamma, Inf),
+    (0, factorial, 1),
+    (-1, factorial, NA),
+    ([-1, 1], factorial, [NA, 1]),
+    (0, lfactorial, 0),
+])
+def test_gamma(x, fun, exp):
+    out = fun(x)
+    if is_scalar(exp):
+        out, exp = [out], [exp]
+    assert_iterable_equal(exp, out, approx=True)
+
+@pytest.mark.parametrize('n,k,log,exp', [
+    (5,2,False,10),
+    ([4,5],2,False,[6,10]),
+    (5,2,True,2.302585),
+    ([4,5],2,True,[1.791759469,2.302585]),
+])
+def test_choose(n,k,log,exp):
+    fun = lchoose if log else choose
+    out = fun(n,k)
+    if is_scalar(exp):
+        out, exp = [out], [exp]
+    assert_iterable_equal(exp, out, approx=True)
+
+@pytest.mark.parametrize('x,deriv,exp',[
+    (1,0,-0.5772157),
+    (2,1,0.6449341),
+    (2,-1,NA),
+    (-2,1,Inf),
+    (1, (0,1), [-0.5772157,1.6449341])
+])
+def test_psigamma(x, deriv, exp):
+    out = psigamma(x, deriv)
+    if is_scalar(exp):
+        out, exp = [out], [exp]
+    assert_iterable_equal(exp, out, approx=True)
diff --git a/tests/test_base_string.py b/tests/test_base_string.py
new file mode 100644
index 00000000..1af6dab2
--- /dev/null
+++ b/tests/test_base_string.py
@@ -0,0 +1,112 @@
+import pytest
+
+from datar.base.string import *
+from datar.base.constants import letters
+from .conftest import assert_iterable_equal
+
+def test_as_character():
+    assert as_character(1) == '1'
+    assert_iterable_equal(as_character([1,2]), ['1', '2'])
+    assert_iterable_equal(as_character([1,NA]), ['1', NA])
+
+def test_is_character():
+    assert not is_character(1)
+    assert not is_character([1, 'a'])
+    assert is_character('111')
+    assert is_character(['111', '222'])
+
+def test_grep():
+    out = grep('[a-z]', 'a')
+    assert_iterable_equal(out, [0])
+    out = grep('[a-z]', 'a', _base0=False)
+    assert_iterable_equal(out, [1])
+    out = grep('[a-z]', 'a', invert=True)
+    assert_iterable_equal(out, [])
+
+    out = grep('[a-z]', letters)
+    assert_iterable_equal(out, list(range(26)))
+
+    txt = ["arm","foot","lefroo", "bafoobar"]
+    out = grep("foo", txt)
+    assert_iterable_equal(out, [1,3])
+
+    out = grep("foo", txt, value=True)
+    assert_iterable_equal(out, ["foot", "bafoobar"])
+
+    # fixed
+    out = grep(".", "a")
+    assert_iterable_equal(out, [0])
+    out = grep(".", "a", fixed=True)
+    assert_iterable_equal(out, [])
+
+def test_grepl(caplog):
+    txt = ["arm","foot","lefroo", "bafoobar"]
+    out = grepl(["foo"], txt)
+    assert_iterable_equal(out, [False, True, False, True])
+
+    assert_iterable_equal(grepl('a', 'a'), [True])
+
+    # warn # patterns > 1
+    grepl(['a', 'b'], 'a')
+    assert 'has length > 1' in caplog.text
+
+def test_sub():
+    txt = ["arm","foot","lefroo", "bafoobarfoo"]
+    out = sub("foo", "bar", txt)
+    assert_iterable_equal(out, ["arm", "bart", "lefroo", "babarbarfoo"])
+
+    # fixed
+    out = sub(".", "a", "b")
+    assert_iterable_equal(out, ["a"])
+    out = sub(".", "a", "b", fixed=True)
+    assert_iterable_equal(out, ["b"])
+
+def test_gsub():
+    txt = ["arm","foot","lefroo", "bafoobarfoo"]
+    out = gsub("foo", "bar", txt)
+    assert_iterable_equal(out, ["arm", "bart", "lefroo", "babarbarbar"])
+
+def test_nchar():
+    s = '\u200b'
+    assert_iterable_equal(nchar(s), [1])
+    assert_iterable_equal(nchar(s, type="bytes"), [3])
+    assert_iterable_equal(nchar(s, type="width"), [0])
+
+    with pytest.raises(UnicodeEncodeError):
+        nchar('\ud861', type="bytes", allow_na=False)
+
+    out = nchar(['\ud861', NA, 'a'], type="bytes")
+    assert_iterable_equal(out, [NA, NA, 1])
+    assert out.dtype == object
+
+def test_nzchar():
+    assert_iterable_equal(nzchar(''), [False])
+    assert_iterable_equal(nzchar('1'), [True])
+    assert_iterable_equal(nzchar(['1', NA]), [True, True])
+    assert_iterable_equal(nzchar(['1', NA], keep_na=True), [True, NA])
+
+def test_paste():
+    assert_iterable_equal(paste("a", "b"), ["a b"])
+    assert_iterable_equal(paste("a", ["b", "c"]), ["a b", "a c"])
+    assert paste("a", ["b", "c"], collapse="_") == "a b_a c"
+    assert paste0("a", ["b", "c"], collapse="_") == "ab_ac"
+
+
+def test_sprintf():
+    assert sprintf("%d", 1.1) == '1'
+    assert_iterable_equal(sprintf(["%d", "%.2f"], [1.1, 2.345]), ["1", "2.35"])
+
+def test_substr():
+    assert substr('abcd', 2, 3) == 'bc'
+    assert_iterable_equal([substr(NA, 2, 3)], [NA])
+    assert_iterable_equal(substr(NA, [2,3], 3), [NA, NA])
+    assert_iterable_equal(substr(['abce', 'efgh'], 2, 3), ['bc', 'fg'])
+    assert_iterable_equal(substr(['abce', 'efgh', NA], 2, 3), ['bc', 'fg', NA])
+    assert substring('abcd', 2) == 'bcd'
+
+def test_strsplit():
+    assert strsplit('a.b.c', '.', fixed=True) == ['a', 'b', 'c']
+    out = strsplit('a.b.c', ['.', 'b'], fixed=True)
+    assert len(out) == 2
+    assert out[0] == ['a', 'b', 'c']
+    assert out[1] == ['a.', '.c']
diff --git a/tests/test_base_table.py b/tests/test_base_table.py
new file mode 100644
index 00000000..bb4463c4
--- /dev/null
+++ b/tests/test_base_table.py
@@ -0,0 +1,104 @@
+import pytest
+
+from datar import stats
+from datar.base import *
+from datar import f
+from datar.datasets import warpbreaks, state_division, state_region, airquality
+
+from .conftest import assert_iterable_equal
+
+def test_table():
+    # https://www.rdocumentation.org/packages/base/versions/3.6.2/topics/table
+    z = stats.rpois(100, 5)
+    x = table(z)
+    assert sum(x.values.flatten()) == 100
+
+    #-----------------
+    with data_context(warpbreaks) as _:
+        tab = table(f.wool, f.tension)
+
+    assert tab.columns.tolist() == ['H', 'L', 'M']
+    assert tab.index.tolist() == ['A', 'B']
+    assert_iterable_equal(tab.values.flatten(), [9] * 6)
+
+    tab = table(warpbreaks.loc[:, ['wool', 'tension']])
+    assert tab.columns.tolist() == ['H', 'L', 'M']
+    assert tab.index.tolist() == ['A', 'B']
+    assert_iterable_equal(tab.values.flatten(), [9] * 6)
+
+    #-----------------
+    tab = table(state_division, state_region)
+    assert tab.loc['New England', 'Northeast'] == 6
+
+    #-----------------
+    # wait for cut
+    # with context(airquality) as _:
+    #     tab = table(cut(f.Temp, stats.quantile(f.Temp)), f.Month)
+
+    # assert tab.iloc[0,0] == 24
+
+    #-----------------
+    a = letters[:3]
+    tab = table(a, sample(a))
+    assert sum(tab.values.flatten()) == 3
+
+    #-----------------
+    tab = table(a, sample(a), dnn=['x', 'y'])
+    assert tab.index.name == 'x'
+    assert tab.columns.name == 'y'
+
+    #-----------------
+    a = c(NA, Inf, (1.0/(i+1) for i in range(3)))
+    a = a * 10
+    # tab = table(a)
+    # assert_iterable_equal(tab.values.flatten(), [10] * 4)
+
+    tab = table(a, exclude=None)
+    assert_iterable_equal(tab.values.flatten(), [10] * 5)
+
+    #------------------
+    b = as_factor(c("A","B","C") * 10)
+    tab = table(b)
+    assert tab.shape == (1, 3)
+    assert_iterable_equal(tab.values.flatten(), [10] * 3)
+
+    tab = table(b, exclude="B")
+    assert tab.shape == (1, 2)
+    assert_iterable_equal(tab.values.flatten(), [10] * 2)
+    assert 'B' not in tab.columns
+
+    #-------------------
+    d = factor(rep(c("A","B","C"), 10), levels=c("A","B","C","D","E"))
+    tab = table(d, exclude="B", dnn=['x'])
+    assert_iterable_equal(tab.columns.to_list(), ["A", "C", "D", "E"])
+    assert_iterable_equal(tab.values.flatten(), [10, 10, 0, 0])
+
+    d2 = factor(rep(c("A","B","C"), 10), levels=c("A","B","C","D","E"))
+    tab = table(d, d2, exclude="B")
+    assert tab.shape == (4, 4)
+
+    tab = table("abc", "cba", dnn='x')
+    assert tab.shape == (3,3)
+    assert sum(tab.values.flatten()) == 3
+
+    with data_context(airquality) as _:
+        tab = table(f.Ozone, f.Solar_R, exclude=None)
+    assert '<NA>' in tab.columns
+    assert '<NA>' in tab.index
+
+def test_table_error():
+    from datar.datasets import iris, warpbreaks
+    with pytest.raises(ValueError):
+        table(iris)
+    with pytest.raises(ValueError):
+        table(warpbreaks, iris)
+    with pytest.raises(ValueError):
+        table(warpbreaks.wool, iris)
+    with pytest.raises(ValueError):
+        table(iris.iloc[:, []])
+    with pytest.raises(ValueError):
+        table(iris.iloc[:, [1,2]], iris)
+    with pytest.raises(ValueError):
+        table(iris.iloc[:, [1]], iris, iris)
+    with pytest.raises(ValueError):
+        table(iris.iloc[:, [1]], iris.iloc[:, []])
diff --git a/tests/test_base_testing.py b/tests/test_base_testing.py
new file mode 100644
index 00000000..2fe99c39
--- /dev/null
+++ b/tests/test_base_testing.py
@@ -0,0 +1,33 @@
+import pytest
+
+from datar.base.testing import *
+from .conftest import assert_iterable_equal
+
+def test_is_numeric():
+    a = numpy.array([1,2,3])
+    assert is_numeric(a)
+
+    assert is_numeric(1)
+    assert is_numeric([1,2,3])
+
+def test_is_integer():
+    a = numpy.array([1,2,3])
+    assert is_integer(a)
+
+    assert is_integer(1)
+
+    a = numpy.array([1,2], dtype=float)
+    assert not is_integer(a)
+
+def test_is_atomic():
+    assert is_atomic(1)
+    assert not is_atomic([1])
+
+def test_is_element():
+    assert is_element(1, [1,2])
+    assert not is_element(0, [1,2])
+    assert_iterable_equal(is_element([0,1], [1,2]), [False, True])
+
+def test_all_any():
+    assert all(is_atomic(x) for x in [1,2,3])
+    assert any(is_atomic(x) for x in [1,[2,3]])
diff --git a/tests/test_base_trig_hb.py b/tests/test_base_trig_hb.py
new file mode 100644
index 00000000..0d2d8757
--- /dev/null
+++ b/tests/test_base_trig_hb.py
@@ -0,0 +1,33 @@
+import pytest
+
+from datar.base import NA, Inf
+from datar.base.trig_hb import *
+from datar.core.types import is_scalar
+from .conftest import assert_iterable_equal
+
+@pytest.mark.parametrize('fun,x,exp', [
+    (cos, 1, 0.5403023),
+    (cos, NA, NA),
+    (sin, 1, 0.841471),
+    (tan, 1, 1.557408),
+    (acos, 1, 0),
+    (asin, 1, 1.570796),
+    (atan, 1, 0.7853982),
+    (cospi, 1, -1),
+    (sinpi, 1, 0),
+    (tanpi, 1, 0),
+    (cosh, 1, 1.543081),
+    (sinh, 1, 1.175201),
+    (tanh, 1, 0.7615942),
+    (acosh, 1, 0),
+    (asinh, 1, 0.8813736),
+    (atanh, .4, 0.4236489),
+])
+def test_func_with_1arg(fun, x, exp):
+    out = fun(x)
+    if is_scalar(exp):
+        out, exp = [out], [exp]
+    assert_iterable_equal(out, exp, approx=True)
+
+def test_atan2():
+    assert pytest.approx(atan2(3,3)) == 0.7853982
diff --git a/tests/test_base_verbs.py b/tests/test_base_verbs.py
new file mode 100644
index 00000000..b21c190d
--- /dev/null
+++ b/tests/test_base_verbs.py
@@ -0,0 +1,94 @@
+import pytest
+from pandas import DataFrame
+from datar.base import *
+from datar.tibble import tibble
+
+from .conftest import assert_iterable_equal
+
+def test_rowcolnames():
+    df = DataFrame(dict(x=[1,2,3]))
+    assert colnames(df) == ['x']
+    assert rownames(df) == [0, 1, 2]
+    df = DataFrame([1,2,3], index=['a', 'b', 'c'])
+    assert colnames(df) == [0]
+    assert rownames(df) == ['a', 'b', 'c']
+
+    df = colnames(df, ['y'])
+    assert_iterable_equal(df.columns, ['y'])
+
+    df = colnames(df, ['y'], _nested=False)
+    assert_iterable_equal(df.columns, ['y'])
+
+    assert_iterable_equal(colnames(df, _nested=False), ['y'])
+
+    df = rownames(df, ['a', 'b', 'c'])
+    assert_iterable_equal(df.index, ['a', 'b', 'c'])
+
+    df = tibble(a=tibble(x=1, y=1), b=tibble(u=2, v=3), z=2)
+    df = df >> colnames(['c', 'd', 'w'], _nested=True)
+    assert_iterable_equal(df.columns, ['c$x', 'c$y', 'd$u', 'd$v', 'w'])
+
+
+def test_diag():
+    assert dim(diag(3)) == (3,3)
+    assert dim(diag(10, 3, 4)) == (3,4)
+    x = diag(c(1j,2j))
+    assert x.iloc[0,0] == 0+1j
+    assert x.iloc[0,1] == 0+0j
+    assert x.iloc[1,0] == 0+0j
+    assert x.iloc[1,1] == 0+2j
+    x = diag(TRUE, 3)
+    assert sum(x.values.flatten()) == 3
+    x = diag(c(2,1), 4)
+    assert_iterable_equal(diag(x), [2,1,2,1])
+
+    with pytest.raises(ValueError):
+        diag(x, 3, 3)
+
+    x = diag(1, 4)
+    assert_iterable_equal(diag(diag(x, 3)), [3,3,3,3])
+
+def test_ncol():
+    df = tibble(x=tibble(a=1, b=2))
+    assert ncol(df) == 1
+    assert ncol(df, _nested=False) == 2
+
+def test_t():
+    df = tibble(x=1, y=2)
+    out = t(df)
+    assert out.shape == (2, 1)
+    assert_iterable_equal(out.index, ['x', 'y'])
+
+def test_names():
+    assert_iterable_equal(names(tibble(x=1)), ['x'])
+    assert_iterable_equal(names({'x': 1}), ['x'])
+    assert names({'x':1}, ['y']) == {'y': 1}
+
+def test_setdiff():
+    assert_iterable_equal(setdiff(1,2), [1])
+    assert_iterable_equal(setdiff([1,2], [2]), [1])
+
+
+def test_intersect():
+    assert_iterable_equal(intersect(1,2), [])
+    assert_iterable_equal(intersect([1,2], [2]), [2])
+
+def test_union():
+    assert_iterable_equal(union(1,2), [1,2])
+    assert_iterable_equal(union([1,2], [2]), [1,2])
+
+def test_setequal():
+    assert setequal([1,2], [2,1])
+    assert setequal(1, 1)
+
+def test_duplicated():
+    assert_iterable_equal(
+        duplicated([1,1,-1,-1,2,2], incomparables=[-1]),
+        [False, True, False, False, False, True]
+    )
+    assert_iterable_equal(
+        duplicated([1,1,2,2], from_last=True),
+        [True, False, True, False]
+    )
+    df = tibble(x=[1,1,2,2])
+    assert_iterable_equal(duplicated(df), [False, True, False, True])
diff --git a/tests/test_base_which.py b/tests/test_base_which.py
new file mode 100644
index 00000000..271864b1
--- /dev/null
+++ b/tests/test_base_which.py
@@ -0,0 +1,14 @@
+import pytest
+
+from datar.base.which import *
+from .conftest import assert_iterable_equal
+
+def test_which():
+    assert_iterable_equal(which([True, False, True]), [0,2])
+    assert_iterable_equal(which([True, False, True], _base0=False), [1,3])
+
+def test_which_min():
+    assert which_min([2,1,3]) == 1
+
+def test_which_max():
+    assert which_max([2,1,3]) == 2
diff --git a/tests/test_core_names.py b/tests/test_core_names.py
index 5b2c0cc7..854bb8a2 100644
--- a/tests/test_core_names.py
+++ b/tests/test_core_names.py
@@ -103,7 +103,7 @@ def test_check_unique():
     with pytest.raises(NameNonUniqueError):
         repair_names(["a", "a"], repair="check_unique")
     with pytest.raises(NameNonUniqueError):
-        repair_names(["_1"], repair="check_unique")
+        repair_names(["__1"], repair="check_unique")
     with pytest.raises(NameNonUniqueError):
         repair_names(["__"], repair="check_unique")
     assert repair_names(["a", "b"], repair="check_unique") == ["a", "b"]
diff --git a/tests/test_core_operator.py b/tests/test_core_operator.py
new file mode 100644
index 00000000..735f2489
--- /dev/null
+++ b/tests/test_core_operator.py
@@ -0,0 +1,21 @@
+import pytest
+
+from pandas.testing import assert_frame_equal
+from datar.all import *
+
+def test_neg():
+    df = tibble(x=[1, 2])
+    out = df >> mutate(y=-f.x)
+    assert_frame_equal(out, tibble(x=[1,2], y=[-1,-2]))
+
+def test_or_():
+    df = tibble(x=1, y=2, z=3)
+    out = df >> select(c(f.x, f.y) | [f.y, f.z])
+    assert_frame_equal(out, tibble(x=1,y=2,z=3))
+
+def test_ne():
+    df = tibble(x=[1,2], y=[1,3])
+    # out = mutate(df, z=f.x!=f.y)
+    # assert_frame_equal(out, tibble(x=[1,2], y=[1,3], z=[False, True]))
+    out = mutate(df, z=f.x.size != f.y.size)
+    assert_frame_equal(out, tibble(x=[1,2], y=[1,3], z=[False, False]))
diff --git a/tests/test_core_options.py b/tests/test_core_options.py
index 14b34ba2..5d2f16d2 100644
--- a/tests/test_core_options.py
+++ b/tests/test_core_options.py
@@ -19,16 +19,16 @@ def test_options_with_names_only_selects_options():
 def test_options_with_names_name_value_pairs_mixed_returns_select_options_and_changes_option():
     out = options('index.base.0', dplyr_summarise_inform=False)
     assert out == {'index_base_0': False, 'dplyr_summarise_inform': True}
-    assert not getOption('dplyr.summarise.inform')
+    assert not get_option('dplyr.summarise.inform')
 
 def test_options_with_dict_updates_options():
     out = options({'index.base.0': True})
-    assert getOption('index.base.0')
+    assert get_option('index.base.0')
     assert not out.index_base_0
 
 def test_options_context():
-    assert not getOption('index.base.0')
+    assert not get_option('index.base.0')
     with options_context(index_base_0=True):
-        assert getOption('index.base.0')
+        assert get_option('index.base.0')
 
-    assert not getOption('index.base.0')
+    assert not get_option('index.base.0')
diff --git a/tests/test_core_utils.py b/tests/test_core_utils.py
new file mode 100644
index 00000000..4ee33584
--- /dev/null
+++ b/tests/test_core_utils.py
@@ -0,0 +1,80 @@
+import pytest
+
+from pandas.testing import assert_frame_equal
+from datar.core.utils import *
+from datar.all import factor, levels, NA, tibble, is_integer
+from .conftest import assert_iterable_equal
+
+def test_na_if_safe():
+    fct = factor([1,2,3])
+    out = na_if_safe(fct, 1)
+    assert_iterable_equal(out, [NA, 2, 3])
+    assert_iterable_equal(levels(out), [2, 3])
+
+def test_fillna_safe():
+    it = [1,2,NA]
+    with pytest.raises(ValueError):
+        fillna_safe(it, 1)
+
+def test_df_setitem():
+    df = tibble(x=1)
+    out = df_setitem(df, 'y', numpy.array([[1,2]], dtype=object))
+    assert_frame_equal(out, tibble(x=1, y=[[1,2]]))
+    out = df_setitem(df, 'z', (1,))
+    assert_frame_equal(out, tibble(x=1, y=[[1,2]], z=1))
+
+def test_keep_column_order():
+    df = tibble(x=1, y=2)
+    with pytest.raises(ValueError):
+        keep_column_order(df, ['x'])
+
+def test_apply_dtypes():
+    df = tibble(x=[1.0, 2.0])
+    apply_dtypes(df, True)
+    assert is_integer(df.x)
+
+def test_arg_match():
+    with pytest.raises(ValueError, match='abc'):
+        arg_match('a', 'a', ['b', 'c'], errmsg='abc')
+    with pytest.raises(ValueError, match='must be one of'):
+        arg_match('a', 'a', ['b', 'c'])
+
+def test_name_mutatable_args():
+    out = name_mutatable_args(tibble(a=1), a={'x':1})
+    assert out == {'a': {'x': 1}}
+
+def test_dict_insert_at():
+    dic = dict(a=1, b=2, c=3)
+    out = dict_insert_at(dic, 'b', {'d': 4})
+    assert out == dict(a=1, d=4, b=2, c=3)
+    out = dict_insert_at(dic, 'b', {'d': 4}, remove=True)
+    assert out == dict(a=1, d=4, c=3)
+    dic = dict(a=1, b=2, c=3)
+    out = dict_insert_at(dic, ['b', 'c'], {'b': 4}, remove=False)
+    assert out == dict(a=1, b=4, c=3)
+
+def test_to_df():
+    df = tibble(x=1, y=2)
+    assert_frame_equal(to_df(df), df)
+
+    out = to_df(df, name='a')
+    assert_iterable_equal(out.columns, ['a$x', 'a$y'])
+
+    d = numpy.array([[1,2,3]])
+    out = to_df(d, ['x', 'y', 'z'])
+    assert_frame_equal(out, tibble(x=1, y=2, z=3))
+
+    out = to_df(d, ['x'])
+    assert_iterable_equal(out.columns, [0,1,2])
+
+    d = numpy.array([[1], [2], [3]])
+    out = to_df(d, 'x')
+    assert_frame_equal(out, tibble(x=[1,2,3]))
+
+    d = {'x': [1]}
+    assert_frame_equal(to_df(d), tibble(x=1))
+
+def test_recycle_value():
+    df = tibble(x=[])
+    out = recycle_value(df, 1)
+    assert_frame_equal(out, tibble(x=NA))
diff --git a/tests/test_core_warn_builtin_names.py b/tests/test_core_warn_builtin_names.py
new file mode 100644
index 00000000..6dfaec8e
--- /dev/null
+++ b/tests/test_core_warn_builtin_names.py
@@ -0,0 +1,38 @@
+import pytest
+import builtins
+from datar.core.options import options
+
+@pytest.fixture(autouse=True)
+def clear_warns():
+    from datar.core import warn_builtin_names
+    oldopt = options('warn.builtin.names')
+    options(warn_builtin_names=True)
+    warn_builtin_names.WARNED.clear()
+    yield
+    options(warn_builtin_names=oldopt)
+
+def test_nonexist_names_donot_exist():
+    with pytest.raises(ImportError):
+        from datar.dplyr import x
+
+    with pytest.raises(ImportError):
+        from datar.all import x
+
+def test_trailing_underscore_bypass_warn(caplog):
+    from datar.dplyr import filter_
+    assert callable(filter_)
+    assert filter_ is not builtins.filter
+    assert caplog.text == ''
+
+def test_direct_import_gets_warning(caplog):
+    from datar.dplyr import filter
+    assert '"filter" has been overriden' in caplog.text
+    caplog.clear()
+
+    # second import won't warn
+    from datar.dplyr import filter
+    assert caplog.text == ''
+
+def test_direct_import_from_all_warns_once(caplog):
+    from datar.all import slice
+    assert caplog.text.count('"slice"') == 1
diff --git a/tests/test_dplyr_across.py b/tests/test_dplyr_across.py
index ccb02613..a54ba06c 100644
--- a/tests/test_dplyr_across.py
+++ b/tests/test_dplyr_across.py
@@ -1,13 +1,13 @@
 """Grabbed from
 https://github.com/tidyverse/dplyr/blob/master/tests/testthat/test-across.R"""
-from datar.core.exceptions import DataUnrecyclable
-from datar.core.grouped import DataFrameRowwise
 import numpy
 from pipda import register_func
 import pytest
 
 from pandas.testing import assert_frame_equal
 from datar.all import *
+from datar.core.exceptions import DataUnrecyclable
+from datar.core.grouped import DataFrameRowwise
 from .conftest import assert_iterable_equal
 
 def test_on_one_column():
diff --git a/tests/test_dplyr_bind.py b/tests/test_dplyr_bind.py
index 1ef86dc7..2474909c 100644
--- a/tests/test_dplyr_bind.py
+++ b/tests/test_dplyr_bind.py
@@ -113,7 +113,7 @@ def test_int_to_float():
     out = df1 >> bind_rows(df2)
     a_type = is_float(out.a)
     assert a_type
-    b_type = is_int64(out.b)
+    b_type = is_int(out.b)
     assert b_type
 
 def test_factor_to_chars():
diff --git a/tests/test_dplyr_filter.py b/tests/test_dplyr_filter.py
index 99957360..c6afc8c7 100644
--- a/tests/test_dplyr_filter.py
+++ b/tests/test_dplyr_filter.py
@@ -81,7 +81,7 @@ def test_complex_vec():
     out = filter(d, f.x < 4)
     assert out.y.tolist() == [i+2j for i in range(1,4)]
 
-    out = d >> filter(Re(f.y) < 4)
+    out = d >> filter(re(f.y) < 4)
     assert out.y.tolist() == [i+2j for i in range(1,4)]
 
 def test_contains():
diff --git a/tests/test_dplyr_group_by.py b/tests/test_dplyr_group_by.py
index 32697f9e..8f2e1820 100644
--- a/tests/test_dplyr_group_by.py
+++ b/tests/test_dplyr_group_by.py
@@ -387,7 +387,7 @@ def test_rowwise_preserved_by_subsetting():
     assert isinstance(out, DataFrameRowwise)
     assert group_vars(out) == ['x']
 
-    out = setNames(rf, [name.upper() for name in names(rf)])
+    out = set_names(rf, [name.upper() for name in names(rf)])
     assert isinstance(out, DataFrameRowwise)
     assert group_vars(out) == ['X']
 
diff --git a/tests/test_dplyr_mutate.py b/tests/test_dplyr_mutate.py
index d88cc437..7ea527af 100644
--- a/tests/test_dplyr_mutate.py
+++ b/tests/test_dplyr_mutate.py
@@ -2,6 +2,7 @@
 # https://github.com/tidyverse/dplyr/blob/master/tests/testthat/test-mutate.r
 from pipda.function import register_func
 import pytest
+from pandas.testing import assert_frame_equal
 from pandas.core.frame import DataFrame
 from datar.all import *
 from datar.core.grouped import DataFrameGroupBy, DataFrameRowwise
@@ -367,3 +368,21 @@ def test_transmute_errors():
         transmute(mtcars, cyl2=f.cyl, _keep='all')
     # transmute(mtcars, cyl2=f.cyl, _before=f.disp)
     # transmute(mtcars, cyl2=f.cyl, _after=f.disp)
+
+def test_dup_keyword_args():
+    df = tibble(a=1)
+    out = df >> mutate(b_=f.a+1, b=f.b*2)
+    assert_frame_equal(out, tibble(a=1, b=4))
+    # order doesn't matter
+    out = df >> mutate(b=f.a+1, b_=f.b*2)
+    assert_frame_equal(out, tibble(a=1, b=4))
+    # support >= 2 dups
+    out = df >> mutate(b__=f.a+1, b_=f.b*2, b=f.b/4.)
+    assert_frame_equal(out, tibble(a=1, b=1.))
+    # has to be consective
+    out = df >> mutate(b__=f.a+1, b_=f.b*2, b=f.b/4.)
+    assert_frame_equal(out, tibble(a=1, b=1.))
+    out = df >> mutate(b__=f.a+1, b_=f.b_*2)
+    assert_frame_equal(out, tibble(a=1, b_=4))
+    out = df >> mutate(b_=f.a+1)
+    assert_frame_equal(out, tibble(a=1, b_=2))
diff --git a/tests/test_dplyr_select.py b/tests/test_dplyr_select.py
index b372e65d..a2cd1a05 100644
--- a/tests/test_dplyr_select.py
+++ b/tests/test_dplyr_select.py
@@ -1,7 +1,7 @@
 # tests grabbed from:
 # https://github.com/tidyverse/dplyr/blob/master/tests/testthat/test-select.r
 from pipda.verb import register_verb
-from datar.stats.verbs import setNames
+from datar.stats.verbs import set_names
 from pandas.core.frame import DataFrame
 import pytest
 from datar.all import *
@@ -28,7 +28,7 @@ def test_non_syntactic_grouping_variable_is_preserved():
 
 def test_select_doesnot_fail_if_some_names_missing():
     df1 = tibble(x=range(1,11), y=range(1,11), z=range(1,11))
-    df2 = setNames(df1, ["x", "y", ""])
+    df2 = set_names(df1, ["x", "y", ""])
 
     out1 = select(df1, f.x)
     assert out1.equals(tibble(x=range(1,11)))
@@ -133,20 +133,20 @@ def test_can_select_with_strings():
     assert out.equals(exp)
 
 def test_works_on_empty_names():
-    df = tibble(x=1, y=2, z=3) >> setNames(c("x", "y", ""))
+    df = tibble(x=1, y=2, z=3) >> set_names(c("x", "y", ""))
     out = select(df, f.x) >> pull(to='list')
     assert out == [1]
 
-    df >>= setNames(c("", "y", "z"))
+    df >>= set_names(c("", "y", "z"))
     out = select(df, f.y) >> pull(to='list')
     assert out == [2]
 
 def test_works_on_na_names():
-    df = tibble(x=1, y=2, z=3) >> setNames(c("x", "y", NA))
+    df = tibble(x=1, y=2, z=3) >> set_names(c("x", "y", NA))
     out = select(df, f.x) >> pull(to='list')
     assert out == [1]
 
-    df >>= setNames(c(NA, "y", "z"))
+    df >>= set_names(c(NA, "y", "z"))
     out = select(df, f.y) >> pull(to='list')
     assert out == [2]
 
diff --git a/tests/test_dplyr_slice.py b/tests/test_dplyr_slice.py
index 235e3570..d96cc8b1 100644
--- a/tests/test_dplyr_slice.py
+++ b/tests/test_dplyr_slice.py
@@ -5,7 +5,7 @@
 import pytest
 from datar.all import *
 from datar.datasets import mtcars
-from datar.dplyr.slice import _n_from_prop
+from datar.dplyr.dslice import _n_from_prop
 
 def test_empty_slice_returns_input():
     df = tibble(x=[1,2,3])
diff --git a/tests/test_dplyr_summarise.py b/tests/test_dplyr_summarise.py
index 7b969d7a..20e8fe04 100644
--- a/tests/test_dplyr_summarise.py
+++ b/tests/test_dplyr_summarise.py
@@ -1,5 +1,6 @@
 # tests grabbed from:
 # https://github.com/tidyverse/dplyr/blob/master/tests/testthat/test-summarise.r
+from tokenize import group
 from datar.core.grouped import DataFrameRowwise
 from pandas.core.frame import DataFrame
 from pandas.testing import assert_frame_equal
@@ -254,8 +255,9 @@ def test_errors(caplog):
 
     # Duplicate column names
     x = 1
+    df = tibble(x, x, _name_repair="minimal")
     with pytest.raises(NameNonUniqueError):
-        tibble(x, x, _name_repair="minimal") >> summarise(f.x)
+        df >> summarise(f.x)
 
 def test_summarise_with_multiple_acrosses():
     """https://stackoverflow.com/questions/63200530/python-pandas-equivalent-to-dplyr-1-0-0-summarizeacross"""
@@ -272,3 +274,13 @@ def test_summarise_with_multiple_acrosses():
         wt=[2.285727, 3.117143, 3.999214]
     )
     assert_frame_equal(out, exp)
+
+def test_dup_keyword_args():
+    df = tibble(g=[1,1], a=[1.0,2.0]) >> group_by(f.g)
+    out = df >> summarise(b_=mean(f.a), b=f.b*2)
+    assert_frame_equal(out, tibble(g=1, b=3.0))
+
+def test_use_pandas_series_func_gh14():
+    df = tibble(g=[1,1,2,2], a=[4,4,8,8]) >> group_by(f.g)
+    out = df >> summarise(a=f.a.mean())
+    assert_frame_equal(out, tibble(g=[1,2], a=[4.0,8.0]))
diff --git a/tests/test_stats_funs.py b/tests/test_stats_funs.py
new file mode 100644
index 00000000..43c6fa28
--- /dev/null
+++ b/tests/test_stats_funs.py
@@ -0,0 +1,14 @@
+import pytest
+
+from datar.stats.funs import *
+from datar.base import NA
+
+from .conftest import assert_iterable_equal
+
+def test_weighted_mean():
+    assert pytest.approx(weighted_mean([1,2,3], [1,2,3])) == 2.3333333
+    assert weighted_mean(1, 1) == 1
+    assert weighted_mean([1, NA], [1, NA], na_rm=True) == 1
+    assert_iterable_equal([weighted_mean([1,2], [-1, 1])], [NA])
+    with pytest.raises(ValueError):
+        weighted_mean([1,2], [1,2,3])
diff --git a/tests/test_tibble_tibble.py b/tests/test_tibble_tibble.py
index 8ed98210..0321263a 100644
--- a/tests/test_tibble_tibble.py
+++ b/tests/test_tibble_tibble.py
@@ -2,13 +2,12 @@
 
 from pandas import DataFrame
 from pandas.testing import assert_frame_equal
-from datar.base.funcs import c
 import pytest
 
 from datar import f
 from datar.core.exceptions import DataUnrecyclable
 from datar.tibble import *
-from datar.base import nrow, rep, dim, sum, diag, NA, letters, LETTERS, NULL, seq
+from datar.base import c, nrow, rep, dim, sum, diag, NA, letters, LETTERS, NULL, seq
 from datar.dplyr import pull, mutate
 from datar.datasets import iris
 from .conftest import assert_iterable_equal
diff --git a/tests/test_tibble_verbs.py b/tests/test_tibble_verbs.py
index a143e754..a28bfdef 100644
--- a/tests/test_tibble_verbs.py
+++ b/tests/test_tibble_verbs.py
@@ -5,7 +5,7 @@
 from datar.base.verbs import ncol
 import pandas
 from pandas.testing import assert_frame_equal
-from datar.base.funcs import as_character, c, levels, rev, seq_along
+from datar.base import as_character, c, levels, rev, seq_along
 import pytest
 
 from datar import f
diff --git a/tests/test_tidyr_drop_na.py b/tests/test_tidyr_drop_na.py
index 5de9f0aa..28beaa97 100644
--- a/tests/test_tidyr_drop_na.py
+++ b/tests/test_tidyr_drop_na.py
@@ -12,8 +12,8 @@ def test_empty_call_drops_every_row():
     assert_frame_equal(drop_na(df), exp)
 
 def test_only_considers_specified_vars():
-    df = tibble(x=c(1,2,NA), y=c("a", NaN, "b"))
-    exp = tibble(x=[1,2], y=c("a", NaN), _dtypes={'x': float})
+    df = tibble(x=c(1,2,NA), y=c("a", None, "b"))
+    exp = tibble(x=[1,2], y=c("a", None), _dtypes={'x': float})
     out = drop_na(df, f.x)
     assert_frame_equal(out, exp)
 
diff --git a/tests/test_tidyr_fill.py b/tests/test_tidyr_fill.py
index 385e7626..c94859c1 100644
--- a/tests/test_tidyr_fill.py
+++ b/tests/test_tidyr_fill.py
@@ -42,7 +42,7 @@ def test_missings_filled_down_for_each_atomic_vector():
         lgl = c(True, NA),
         int = c(1, NA),
         dbl = c(1.0, NA),
-        chr = c("a", NaN),
+        chr = c("a", None),
         lst = [seq(1,5), NULL]
     )
     out = fill(df, everything())
@@ -57,7 +57,7 @@ def test_missings_filled_up_for_each_atomic_vector():
         lgl = c(NA, True),
         int = c(NA, 1),
         dbl = c(NA, 1.0),
-        chr = c(NaN, "a"),
+        chr = c(None, "a"),
         lst = [NULL, seq(1,5)]
     )
     out = fill(df, everything(), _direction="up")