Skip to content

Clean up and unify r_,c_,hstack,vstack etc (Trac #201) #799

@numpy-gitbot

Description

@numpy-gitbot

Original ticket http://projects.scipy.org/numpy/ticket/201 on 2006-07-23 by @baxissimo, assigned to unknown.

== SUMMARY ==

  • make r_ behave like "vstack plus range literals"
  • make column_stack only transpose its 1d inputs.
  • rename r_,c_ to v_,h_ (or something else) to make their connection with vstack and hstack clearer. Maybe vs_ and hs_ would be better?
  • make a new vertsion of 'c_' that acts like column_stack and make r_ an alias for v_ (just as row_stack is an alias for vstack) so that theres a nice parallel v_<=>vstack, h_<=>hstack, c_<=>column_stack, r_<=>row_stack

== DISCUSSION ==
Here's essentially what these different methods do:

vstack(tup):
concatenate( map(atleast_2d,tup), axis=0 )

hstack(tup):
concatenate( map(atleast_1d,tup),axis=1 )

column_stack(tup):
arrays = map( transpose,map(atleast_2d,tup) )
concatenate(arrays,1)

(note that column_stack transposes everything not just 1-d inputs. The current docstring only claims that it works with 1d inputs, so making it do something reasonable with 2d inputs will hopefully not break much code.)

The above 3 are pretty much exactly the code used by numpy. That's
all there is to those 3 functions.
For r_ and c_ I'm summarizing, but effectively they seem to be doing
something like:

r_[args]:
concatenate( map(atleast_1d,args),axis=0 )

c_[args]:
concatenate( map(atleast_1d,args),axis=-1 )

c_ behaves almost exactly like hstack -- with the addition of range
literals being allowed.

r_ is mostly like vstack, but a little different since it effectively
uses atleast_1d, instead of atleast_2d. So you have

>>> numpy.vstack((1,2,3,4))
array([[1],
      [2],
      [3],
      [4]])

but

>>> numpy.r_[1,2,3,4]
array([1, 2, 3, 4])

However for cases like that with just 0-d or 1-d inputs, c_ behaves
identically to r_, so if you wanted to get a 1-d output you could have
just used c_.

The current situation is confusing because r_, and c_ act similar to the existing hstack/vstack methods but different enough to make the distinction hard to remember.
Ideally these changes will make it so that all the user needs to do to change from one of the *stack methods to the equivalent that supports slice literals, or back again, is a simple syntactical transformation:

   hstack( (args) )       <==> h_[ args ]
   vstack( (args) )       <==> v_[ args ]
   column_stack( (args) ) <==> c_[ args ]
   row_stack( (args) )    <==> r_[ args ]

== Code ==
The following is code that implements the above suggestions in a file called altstack.py. Mostly the code is copied from numpy/lib/shape_base.py and numpy/lib/index_tricks.py. I'd be happy to provide a patch against numpy svn with these changes if there's interest.

#!python
import sys
import types
import numpy
import numpy.core.numeric as _nx
from numpy.core.numeric import asarray, ScalarType
import numpy.lib.function_base
import numpy.core.defmatrix as matrix
makemat = matrix.matrix


## ascol helper function to convert only 1d inputs to columns.

def ascol(v):
    """Turns 1-d inputs into a column vectors.
    For all other inputs acts like atleast_2d.

    (Q: Should this also transpose any (1xN)'s to be columns?  The
    current thinking is if you have a 1-d then you haven't really
    decided whether it's a row or col, and this method is asserting
    that it should be a column. But if it's already a 2-d row, then
    it's probably a row for a reason, and you should transpose it
    explicitly if you want a column.)
    """
    arr = numpy.array(v,copy=False,subok=True)
    if arr.ndim<2:
        return numpy.transpose(numpy.atleast_2d(arr))
    else:
        return arr


## Modified column_stack that leaves >2d data alone

def column_stack(tup):
    """ Stack 1D arrays as columns into a 2D array

        Description:
            Take a sequence of 1D and 2D arrays and stack them as columns
            to make a single 2D array.  All arrays in the sequence
            must have the same first dimension.  2D arrays are stacked as-is,
            just like with hstack, 1D arrays are turned into 2D columns first.
        Arguments:
            tup -- sequence of 1D and 2D arrays.  All arrays must have the same
                   first dimension.
        Examples:
            >>> import numpy
            >>> a = array((1,2,3))
            >>> b = array((2,3,4))
            >>> numpy.column_stack((a,b))
            array([[1, 2],
                   [2, 3],
                   [3, 4]])

    """
    arrays = map(ascol,tup)
    return _nx.concatenate(arrays,1)


# Slightly modified version of numpy.lib.index_tricks.concatenator
# Only difference is the addition of 'mapper' parameter and
# self.mapper attribute.  This allows for running the data through a
# given function (e.g. atleast_2d) before calling concatenate().
# Makes it so concatenator can generate results like vstack and
# column_stack

class concatenator(object):
    """Translates slice objects to concatenation along an axis.
    """
    def _retval(self, res):
        if self.matrix:
            oldndim = res.ndim
            res = makemat(res)
            if oldndim == 1 and self.col:
                res = res.T
        self.axis = self._axis
        self.mapping = self._mapping
        self.matrix = self._matrix
        self.col = 0
        return res

    def __init__(self, axis=0, mapping=None, matrix=False):
        self._axis = axis
        self._mapping = mapping
        self._matrix = matrix
        self.axis = axis
        self.mapping = mapping
        self.matrix = matrix
        self.col = 0

    def __getitem__(self,key):
        if isinstance(key, str):
            frame = sys._getframe().f_back
            mymat = matrix.bmat(key,frame.f_globals,frame.f_locals)
            return mymat
        if type(key) is not tuple:
            key = (key,)
        objs = []
        scalars = []
        final_dtypedescr = None
        for k in range(len(key)):
            scalar = False
            if type(key[k]) is slice:
                step = key[k].step
                start = key[k].start
                stop = key[k].stop
                if start is None: start = 0
                if step is None:
                    step = 1
                if type(step) is type(1j):
                    size = int(abs(step))
                    #newobj = function_base.linspace(start, stop, num=size)
                    newobj = numpy.linspace(start, stop, num=size)
                else:
                    newobj = _nx.arange(start, stop, step)
            elif type(key[k]) is str:
                if (key[k] in 'rc'):
                    self.matrix = True
                    self.col = (key[k] == 'c')
                    continue
                try:
                    self.axis = int(key[k])
                    continue
                except (ValueError, TypeError):
                    raise ValueError, "unknown special directive"
            elif type(key[k]) in ScalarType:
                newobj = asarray([key[k]])
                scalars.append(k)
                scalar = True
            else:
                newobj = key[k]
            objs.append(newobj)
            if isinstance(newobj, _nx.ndarray) and not scalar:
                if final_dtypedescr is None:
                    final_dtypedescr = newobj.dtype
                elif newobj.dtype > final_dtypedescr:
                    final_dtypedescr = newobj.dtype
        if final_dtypedescr is not None:
            for k in scalars:
                objs[k] = objs[k].astype(final_dtypedescr)
        if self.mapping is not None:
            objs = map(self.mapping, objs)
        res = _nx.concatenate(tuple(objs),axis=self.axis)
        return self._retval(res)

    def __getslice__(self,i,j):
        res = _nx.arange(i,j)
        return self._retval(res)

    def __len__(self):
        return 0

# separate classes are used here instead of just making r_ = concatentor(0),
# etc. because otherwise we couldn't get the doc string to come out right
# in help(r_)

## The concatenator objects are now:
#  v_ -- like vstack
#  r_ -- like row_stack (but row_stack==vstack, so r_==v_)
#  h_ -- like hstack
#  c_ -- like column_stack (the modified version above)

# Note that r_ was used both for 'making a row' and for 'stacking rows' previously,
# but the old c_ could also be used for this purpose.
# Now if you want to make a row, h_ is the way to do it.


class v_class(concatenator):
    """Translates slice objects to vertical (row-wise) concatenation.  
       Similar to vstack with the additional ability to use slices.

        For example:
        >>> v_[[1,2,3], 3:0:-1, [4,5,6]]
        array([[1, 2, 3],
               [3, 2, 1],
               [4, 5, 6]])
    """
    def __init__(self):
        concatenator.__init__(self, 0, numpy.atleast_2d)

v_ = v_class()
r_ = v_

class h_class(concatenator):
    """Translates slice objects to horizontal concatenation.
       Similar to hstack with the additional ability to use slices.

        For example:
        >>> h_[ 1, 10:7:-1, [2,3,4] ]
        array([ 1, 10,  9,  8,  2,  3,  4])
        >>> h_[[[1],[2],[3]], [[4],[5],[6]]]
        array([[1, 4],
               [2, 5],
               [3, 6]])
    """
    def __init__(self):
        concatenator.__init__(self, 1)

h_ = h_class()

class c_class(concatenator):
    """Translates slice objects to column-wise concatenation.
       Similar to column_stack with the additional ability to use slices.

        For example:
        >>> c_[[1,2,3], [4,5,6]]
        array([[1, 4],
               [2, 5],
               [3, 6]])
        >>> c_[ [1,2], [4,5], [[7,8],[9,10]] ]
        array([[ 1,  4,  7,  8],
               [ 2,  5,  9, 10]])
    """
    def __init__(self):
        concatenator.__init__(self, 1, ascol)

c_ = c_class()

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions