Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for np.cov #3345

Merged
merged 69 commits into from
Oct 31, 2018
Merged
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
8020e5f
initial commit
rjenc29 Sep 24, 2018
4146ae1
implement y
rjenc29 Sep 24, 2018
9e28566
tweak travis and tests.sh
rjenc29 Sep 24, 2018
6214f6a
implement rowvar
rjenc29 Sep 24, 2018
ea5d7c7
handle non array inputs
rjenc29 Sep 24, 2018
87bfcec
baseline for CI
rjenc29 Sep 24, 2018
7c184e4
handle negative ddof
rjenc29 Sep 24, 2018
07748c5
minor tweak
rjenc29 Sep 24, 2018
0de4791
handle complex input
rjenc29 Sep 25, 2018
3fffaf4
dtype evaluation
rjenc29 Sep 25, 2018
e24666b
add guard for array dims and associated tests
rjenc29 Sep 25, 2018
794a8ad
require two or more variables
rjenc29 Sep 25, 2018
eb52932
handle 1D array input
rjenc29 Sep 25, 2018
7312260
handle empty array
rjenc29 Sep 25, 2018
8fad984
handle empty array v2
rjenc29 Sep 25, 2018
14303ae
handle empty array m and y
rjenc29 Sep 25, 2018
27e31f0
temporarily remove test case
rjenc29 Sep 25, 2018
03ea0dc
temporarily remove test case
rjenc29 Sep 25, 2018
39f1297
temporarily remove test cases
rjenc29 Sep 25, 2018
a276817
add test back
rjenc29 Sep 26, 2018
9fb51fa
add test back
rjenc29 Sep 26, 2018
9d17159
add test back
rjenc29 Sep 26, 2018
1735006
add test back
rjenc29 Sep 26, 2018
96a710f
verify failing test
rjenc29 Sep 26, 2018
6952b87
verify failing test
rjenc29 Sep 26, 2018
a6c3f1c
handle failing test
rjenc29 Sep 26, 2018
e3fe081
prep for PR
rjenc29 Sep 26, 2018
3d286c5
concatenate bugfix
rjenc29 Sep 26, 2018
b3c1994
revert travis and tests.sh
rjenc29 Sep 26, 2018
838b9b3
update tests
rjenc29 Sep 26, 2018
30f3d25
refactor to reduce branching
rjenc29 Sep 27, 2018
7470953
fix copy paste error
rjenc29 Sep 27, 2018
152aedd
update exception message
rjenc29 Sep 27, 2018
2936be1
minor tweaks
rjenc29 Sep 27, 2018
7b16f0f
revert travis and tests.sh
rjenc29 Sep 27, 2018
54bd3cb
use atleast_2d
rjenc29 Sep 27, 2018
9847bc7
minor refactor to simplify
rjenc29 Sep 28, 2018
3bee5f3
further simplification
rjenc29 Sep 28, 2018
279687a
comment typo
rjenc29 Sep 28, 2018
b996a19
handle scalar input
rjenc29 Sep 28, 2018
c124171
add numpy 1.10 version gate
rjenc29 Sep 28, 2018
6d2caa1
clean up tests
rjenc29 Oct 13, 2018
f36ad97
minor simplification
rjenc29 Oct 13, 2018
9b5ae97
merge master
rjenc29 Oct 16, 2018
63043dc
handle single tuple input and reject 2D array with single row
rjenc29 Oct 16, 2018
8a3e7db
improve handling of Sequence types
rjenc29 Oct 16, 2018
8f36a79
handle array-likes more consistently
rjenc29 Oct 16, 2018
5a01f64
catch array-like y greater than 2D
rjenc29 Oct 16, 2018
e69cd90
require blas
rjenc29 Oct 16, 2018
e4f9e06
rename test fn
rjenc29 Oct 17, 2018
fc154e0
handle type inference for array-likes
rjenc29 Oct 17, 2018
e0783eb
make input pre-processing more consistent
rjenc29 Oct 17, 2018
611b62a
merge master
rjenc29 Oct 17, 2018
b824f7a
fix typo and advertise SciPy requirement in docs
rjenc29 Oct 28, 2018
739c89d
address PEP8 issue
rjenc29 Oct 28, 2018
ce38b07
simplify type check
rjenc29 Oct 28, 2018
9c7d3ad
update exception message
rjenc29 Oct 28, 2018
ab87470
added tests
rjenc29 Oct 28, 2018
8f273fd
validate ddof
rjenc29 Oct 28, 2018
24b9db9
add references to numpy tests
rjenc29 Oct 28, 2018
b3292d9
bugfix tuple handling
rjenc29 Oct 28, 2018
50e2396
additional tests and handle tuple of single tuple
rjenc29 Oct 28, 2018
e9111ee
factor out common data prep
rjenc29 Oct 28, 2018
8921894
tweak to ddof handling
rjenc29 Oct 29, 2018
fc5c55f
Merge branch 'master' into cov
rjenc29 Oct 29, 2018
5903fbd
add required guards to test
rjenc29 Oct 29, 2018
e33f296
revert merge master and reapply test guards
rjenc29 Oct 29, 2018
1451718
change exception types
rjenc29 Oct 29, 2018
e825651
Merge master back in to feature branach
rjenc29 Oct 29, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/reference/numpysupported.rst
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ The following top-level functions are supported:
* :func:`numpy.convolve` (only the 2 first arguments)
* :func:`numpy.copy` (only the first argument)
* :func:`numpy.correlate` (only the 2 first arguments)
* :func:`numpy.cov` (only the 5 first arguments, requires NumPy >= 1.10)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should note requirement of SciPy 0.16+.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

* :func:`numpy.diag`
* :func:`numpy.digitize`
* :func:`numpy.dstack`
Expand Down
177 changes: 177 additions & 0 deletions numba/targets/arraymath.py
Original file line number Diff line number Diff line change
Expand Up @@ -1294,6 +1294,183 @@ def np_vander_seq_impl(x, N=None, increasing=False):
elif isinstance(x, (types.Tuple, types.Sequence)):
return np_vander_seq_impl

#----------------------------------------------------------------------------
# Statistics

@register_jitable
def row_wise_average(a):
assert a.ndim == 2

m, n = a.shape
out = np.empty((m, 1), dtype=a.dtype)

for i in range(m):
out[i, 0] = np.sum(a[i, :]) / n

return out

@register_jitable
def np_cov_impl_inner(X, bias, ddof):

# determine degrees of freedom
if ddof is None:
if bias:
ddof = 0
else:
ddof = 1

# determine the normalization factor
fact = X.shape[1] - ddof

# numpy warns if less than 0 and floors at 0
fact = max(fact, 0.0)

# de-mean
X -= row_wise_average(X)

# calculate result - requires blas
c = np.dot(X, np.conj(X.T))
c *= np.true_divide(1, fact)
return c

def _prepare_cov_input():
pass

@overload(_prepare_cov_input)
def _prepare_cov_input_impl(m, y, rowvar, dtype):
if y in (None, types.none):
def _prepare_cov_input_inner(m, y, rowvar, dtype):
m_arr = np.atleast_2d(_asarray(m))

# transpose if asked to and not a (1, n) vector
if not rowvar and m_arr.shape[0] != 1:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the check for (1, n) need to happen, 1D array.T == 1D array?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, now that the "2D but single row" data shape is not allowed, this check is redundant - have removed it.

m_arr = m_arr.T

return m_arr
else:
def _prepare_cov_input_inner(m, y, rowvar, dtype):
m_arr = np.atleast_2d(_asarray(m))
y_arr = np.atleast_2d(_asarray(y))

# transpose if asked to and not a (1, n) vector - this looks
# wrong as you might end up transposing one and not the other,
# but it's what numpy does
if not rowvar:
if m_arr.shape[0] != 1:
m_arr = m_arr.T
if y_arr.shape[0] != 1:
y_arr = y_arr.T

m_rows, m_cols = m_arr.shape
y_rows, y_cols = y_arr.shape

if m_cols != y_cols:
raise ValueError('m and y must have the same number of variables')
# 'variables' as the constraint on rows or columns depends on
# whether rowvar is True or False...

# allocate and fill output array
out = np.empty((m_rows + y_rows, m_cols), dtype=dtype)
out[:m_rows, :] = m_arr
out[-y_rows:, :] = y_arr

return out

return _prepare_cov_input_inner

@register_jitable
def _handle_m_dim_change(m):
if m.ndim == 2 and m.shape[0] == 1:
msg = ("2D array containing a single row is unsupported due to "
"ambiguity in type inference. To use numpy.cov in this case "
"simply pass the row as a 1D array, i.e. m[0].")
raise RuntimeError(msg)

_handle_m_dim_nop = register_jitable(lambda x:x)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

space after the :

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


def determine_dtype(array_like):
array_like_dt = np.float64
if isinstance(array_like, types.Array):
array_like_dt = as_dtype(array_like.dtype)
elif isinstance(array_like, (types.UniTuple, types.Tuple)):
coltypes = set()
for val in array_like:
if hasattr(val, 'count'):
[coltypes.add(v) for v in val]
else:
coltypes.add(val)
if len(coltypes) > 1:
array_like_dt = np.promote_types(*[as_dtype(ty) for ty in coltypes])
elif len(coltypes) == 1:
array_like_dt = as_dtype(coltypes.pop())

return array_like_dt

def check_dimensions(array_like, name):
if isinstance(array_like, types.Array):
if array_like.ndim > 2:
raise TypeError("{0} has more than 2 dimensions".format(name))
elif isinstance(array_like, types.Sequence):
if isinstance(array_like.key[0], types.Sequence):
if isinstance(array_like.key[0].key[0], types.Sequence):
raise TypeError("{0} has more than 2 dimensions".format(name))

if numpy_version >= (1, 10): # replicate behaviour post numpy 1.10 bugfix release
@overload(np.cov)
def np_cov(m, y=None, rowvar=True, bias=False, ddof=None):

# reject problem if m and / or y are more than 2D
check_dimensions(m, 'm')
check_dimensions(y, 'y')

# special case for 2D array input with 1 row of data - select
# handler function which we'll call later when we have access
# to the shape of the input array
_M_DIM_HANDLER = _handle_m_dim_nop
if isinstance(m, types.Array):
_M_DIM_HANDLER = _handle_m_dim_change

# infer result dtype
m_dt = determine_dtype(m)
y_dt = determine_dtype(y)
dtype = np.result_type(m_dt, y_dt, np.float64)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think ddof needs type checking to be an integer type, non-integral DOFs don't make sense do they? Seems like NumPy will accept floats so long as they are integral value, perhaps we can just accept types.Integer for now and catch this at compile time? A unit test checking this would then be good, thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added something (possibly a bit crude) which should allow int, bool and float if integral value - with some explicit tests. Let me know what you think.

I can't think of a case where anything other than 0 and 1 make sense, in the absence of weights (which I haven't implemented anyway).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks.

def np_cov_impl(m, y=None, rowvar=True, bias=False, ddof=None):
_M_DIM_HANDLER(m)
X = _prepare_cov_input(m, y, rowvar, dtype).astype(dtype)

if np.any(np.array(X.shape) == 0):
return np.full((X.shape[0], X.shape[0]), fill_value=np.nan, dtype=dtype)
else:
return np_cov_impl_inner(X, bias, ddof)

def np_cov_impl_single_variable(m, y=None, rowvar=True, bias=False, ddof=None):
_M_DIM_HANDLER(m)
X = _prepare_cov_input(m, y, rowvar, dtype).astype(dtype)

if np.any(np.array(X.shape) == 0):
variance = np.nan
else:
variance = np_cov_impl_inner(X, bias, ddof).flat[0]

return np.array(variance)

# identify up front if output is 0D
if isinstance(m, types.Array) and m.ndim == 1 or isinstance(m, types.Tuple):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit puzzled by this, a Tuple type could have more than one dimension?

In [24]: print(type(numba.typeof(((0.1, 0.2), (0.11, 0.19), (0.09j, 0.21j)))))
<class 'numba.types.containers.Tuple'>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, Numba has a load of Tuple related types.

In [30]: [x for x in dir(numba.types) if 'tuple' in x.lower()]
Out[30]: 
['BaseAnonymousTuple',
 'BaseNamedTuple',
 'BaseTuple',
 'NamedTuple',
 'NamedTupleClass',
 'NamedUniTuple',
 'Tuple',
 'UniTuple',
 'UniTupleIter']

(ignore the iter and class entries). I think this probably needs to check if m subclasses a BaseTuple and then if the types of the items in the tuple inherit from Number/Boolean, which would indicate a "1D" scenario?

        if isinstance(m, types.BaseTuple):
            if all(isinstance(x, (types.Number, types.Boolean)) for x in m.types):
                if y in (None, types.none):
                    return np_cov_impl_single_variable

might help?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this was a bug on my part. The tests all miraculously passed due to the way the if gates were set up and the various test cases resolving to UniTuple vs Tuple type etc.

Should be sorted now, or at least nearer to being sorted. I added your example explicitly in tests.

if y in (None, types.none):
return np_cov_impl_single_variable

if isinstance(m, (types.Integer, types.Float, types.Complex, types.Boolean)):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think checking inheritance from types.Number in the above condition should cover the numeric types (but not Boolean).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if y in (None, types.none):
return np_cov_impl_single_variable

if isinstance(m, types.Sequence):
if not isinstance(m.key[0], types.Sequence) and y in (None, types.none):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may cause issues at some point, but we can defer concern until sequence type detection is altered.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, there may be a tax to pay - but there are quite a few tests so it should at least be apparent when it's affected and needs a rethink

return np_cov_impl_single_variable

# otherwise assume it's 2D and we're good to go
return np_cov_impl

#----------------------------------------------------------------------------
# Element-wise computations

Expand Down
172 changes: 170 additions & 2 deletions numba/tests/test_np_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from numba.numpy_support import version as np_version
from numba.errors import TypingError
from .support import TestCase, CompilationCache, MemoryLeakMixin
from .matmul_usecase import needs_blas

no_pyobj_flags = Flags()
no_pyobj_flags.set("nrt")
Expand Down Expand Up @@ -88,6 +89,9 @@ def vander(x, N=None, increasing=False):
def partition(a, kth):
return np.partition(a, kth)

def cov(m, y=None, rowvar=True, bias=False, ddof=None):
return np.cov(m, y, rowvar, bias, ddof)

def ediff1d(ary, to_end=None, to_begin=None):
return np.ediff1d(ary, to_end, to_begin)

Expand Down Expand Up @@ -562,10 +566,10 @@ def test_convolve_exceptions(self):
else:
self.assertIn("'v' cannot be empty", str(raises.exception))

def _check_output(self, pyfunc, cfunc, params):
def _check_output(self, pyfunc, cfunc, params, abs_tol=None):
expected = pyfunc(**params)
got = cfunc(**params)
self.assertPreciseEqual(expected, got)
self.assertPreciseEqual(expected, got, abs_tol=abs_tol)

def test_vander_basic(self):
pyfunc = vander
Expand Down Expand Up @@ -1088,6 +1092,170 @@ def test_partition_boolean_inputs(self):
for kth in True, False, -1, 0, 1:
self.partition_sanity_check(pyfunc, cfunc, d, kth)

@unittest.skipUnless(np_version >= (1, 10), "cov needs Numpy 1.10+")
@needs_blas
def test_cov_basic(self):
pyfunc = cov
cfunc = jit(nopython=True)(pyfunc)
_check = partial(self._check_output, pyfunc, cfunc, abs_tol=1e-14)

def m_variations():
# array inputs
yield np.array([[0, 2], [1, 1], [2, 0]]).T
yield self.rnd.randn(100).reshape(5, 20)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add:

            yield np.asfortranarray(np.array([[0, 2], [1, 1], [2, 0]]).T)
            yield self.rnd.randn(100).reshape(5, 20)[:,::2]

for a fortran order and a slice. Note the fortran order specified a few lines below is also C contig by virtue of it being a single "column" of data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done and good spot with the Fortan order blunder.

yield np.array([0.3942, 0.5969, 0.7730, 0.9918, 0.7964])
yield np.full((4, 5), fill_value=True)
yield np.array([np.nan, 0.5969, -np.inf, 0.9918, 0.7964])
yield np.linspace(-3, 3, 33).reshape(33, 1, order='F')

# non-array inputs
yield ((0.1, 0.2), (0.11, 0.19), (0.09, 0.21))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a non-homogeneous tuple in here? Like:

((0.1, 0.2), (0.11, 0.19), (0.09j, 0.21j))

? It should trip the issue described above WRT types.Tuple.

yield (-2.1, -1, 4.3)
yield (1, 2, 3)
yield [4, 5, 6]
yield ((0.1, 0.2, 0.3), (0.1, 0.2, 0.3))
yield [(1, 2, 3), (1, 3, 2)]
yield 3.142

# empty data structures
yield np.array([])
yield np.array([]).reshape(0, 2)
yield np.array([]).reshape(2, 0)
yield ()

# all inputs other than the first are defaulted
for m in m_variations():
_check({'m': m})

@unittest.skipUnless(np_version >= (1, 10), "cov needs Numpy 1.10+")
@needs_blas
def test_cov_explicit_arguments(self):
pyfunc = cov
cfunc = jit(nopython=True)(pyfunc)
_check = partial(self._check_output, pyfunc, cfunc, abs_tol=1e-14)

m = self.rnd.randn(1050).reshape(150, 7)
y_choices = None, m[::-1]
rowvar_choices = False, True
bias_choices = False, True
ddof_choice = None, -1, 0, 1, 3

for y, rowvar, bias, ddof in itertools.product(y_choices, rowvar_choices, bias_choices, ddof_choice):
params = {'m': m, 'y': y, 'ddof': ddof, 'bias': bias, 'rowvar': rowvar}
_check(params)

@unittest.skipUnless(np_version >= (1, 10), "cov needs Numpy 1.10+")
@needs_blas
def test_cov_egde_cases(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/egde/edge/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

pyfunc = cov
cfunc = jit(nopython=True)(pyfunc)
_check = partial(self._check_output, pyfunc, cfunc, abs_tol=1e-14)

# examples borrowed from numpy doc string / unit tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explicit reference to the source location in NumPy please.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

m = np.array([-2.1, -1, 4.3])
y = np.array([3, 1.1, 0.12])
params = {'m': m, 'y': y}
_check(params)

m = np.array([[0, 2], [1, 1], [2, 0]]).T
params = {'m': m, 'ddof': 5}
_check(params)

m = np.array([1, 2, 3]) # test case modified such that m is 1D
y = np.array([[1j, 2j, 3j]])
params = {'m': m, 'y': y}
_check(params)

m = np.array([1, 2, 3])
y = (1j, 2j, 3j)
params = {'m': m, 'y': y}
_check(params)
params = {'m': y, 'y': m} # flip real and complex inputs
_check(params)

m = np.array([1, 2, 3])
y = (1j, 2j, 3) # note last item is not complex
params = {'m': m, 'y': y}
_check(params)
params = {'m': y, 'y': m} # flip real and complex inputs
_check(params)

m = np.array([])
y = np.array([])
params = {'m': m, 'y': y}
_check(params)

m = 1.1
y = 2.2
params = {'m': m, 'y': y}
_check(params)

m = self.rnd.randn(10, 3)
y = np.array([-2.1, -1, 4.3]).reshape(1, 3) / 10
params = {'m': m, 'y': y}
_check(params)

# The following tests pass with numpy version >= 1.10, but fail with 1.9
m = np.array([-2.1, -1, 4.3])
y = np.array([[3, 1.1, 0.12], [3, 1.1, 0.12]])
params = {'m': m, 'y': y}
_check(params)

for rowvar in False, True:
m = np.array([-2.1, -1, 4.3])
y = np.array([[3, 1.1, 0.12], [3, 1.1, 0.12], [4, 1.1, 0.12]])
params = {'m': m, 'y': y, 'rowvar': rowvar}
_check(params)

@unittest.skipUnless(np_version >= (1, 10), "cov needs Numpy 1.10+")
@needs_blas
def test_cov_exceptions(self):
pyfunc = cov
cfunc = jit(nopython=True)(pyfunc)

# Exceptions leak references
self.disable_leak_check()

def _check_m(m):
with self.assertTypingError() as raises:
cfunc(m)
self.assertIn('m has more than 2 dimensions', str(raises.exception))

m = np.ones((5, 6, 7))
_check_m(m)

m = ((((1, 2, 3), (2, 2, 2)),),)
_check_m(m)

m = [[[5, 6, 7]]]
_check_m(m)

def _check_y(m, y):
with self.assertTypingError() as raises:
cfunc(m, y=y)
self.assertIn('y has more than 2 dimensions', str(raises.exception))

m = np.ones((5, 6))
y = np.ones((5, 6, 7))
_check_y(m, y)

m = np.array((1.1, 2.2, 1.1))
y = (((1.2, 2.2, 2.3),),)
_check_y(m, y)

m = np.arange(3)
y = np.arange(4)
with self.assertRaises(ValueError) as raises:
cfunc(m, y=y)
self.assertIn('m and y must have the same number of variables', str(raises.exception))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this should refer to the dimension size mismatch? Is variables a bit generic/ambiguous?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the wording a bit - I meant 'variables' in the statistics sense, but I agree the wording's poor. Hopefully it's now less poor :)

# Numpy raises ValueError: all the input array dimensions except for the
# concatenation axis must match exactly.

m = np.array([-2.1, -1, 4.3]).reshape(1, 3)
with self.assertRaises(RuntimeError) as raises:
cfunc(m)
self.assertIn('2D array containing a single row is unsupported', str(raises.exception))

@unittest.skipUnless(np_version >= (1, 12), "ediff1d needs Numpy 1.12+")
def test_ediff1d_basic(self):
pyfunc = ediff1d
Expand Down