Skip to content

Instantly share code, notes, and snippets.

@ahaldane
Last active October 12, 2016 16:28
Show Gist options
  • Save ahaldane/7d1873d33d4d0f80ba7a54ccf1052eee to your computer and use it in GitHub Desktop.
Save ahaldane/7d1873d33d4d0f80ba7a54ccf1052eee to your computer and use it in GitHub Desktop.
structure docs

Numpy allows creation of arrays with a "structured" datatype composed of multiple named :term:`fields <field>` of simpler datatype, similar to a "struct" in the C language. For example,

>>> x = np.array([('Rex', 9, 81.0), ('Fido', 3, 27.0)],
...              dtype=[('name', 'S10'), ('age', 'i4'), ('weight', 'f4')])
>>> x
array([('Rex', 9, 81.0), ('Fido', 3, 27.0)],
      dtype=[('name', 'S10'), ('age', '<i4'), ('weight', '<f4')])

Here x is a one-dimensional array length 2, whose datatype is a structure with three fields: A string of length 10 or less named 'name', a 32-bit integer named 'age', and a 32-bit float named 'weight'. If we index x at the second position we get the second structure:

>>> x[1]
('Fido', 3, 27.0)

Individual fields can be accessed and modified by indexing with the field name:

>>> x['age']
array([9, 3], dtype=int32)
>>> x['age'] = 5
>>> x
array([('Rex', 5, 81.0), ('Fido', 5, 27.0)],
      dtype=[('name', 'S10'), ('age', '<i4'), ('weight', '<f4')])

The goal of structured arrays is to allow low-level manipulation of complex structured datatypes, for example for interpreting binary blobs of structured data. For this purpose numpy includes specialized features such as subarrays and nested datatypes, and allows detailed control over the memory layout of the structure. Structured datatypes are designed to mimic 'structs' in the C language, making them useful for interfacing with C code.

Users wishing simply to manipulate tabular data with labelled columns are enouraged to also consider alternative pydata projects such as pandas, xarray or DataArray, which provide convenient and efficient relational operations on labelled multi-type data through a higher-level interface.

Structured datatypes can be thought of as a sequence of bytes of a certain length (the structure's :term:`itemsize`) which is interpreted as a collection of fields, where each field has a name, a datatype, and a byte offset within the structure. The datatype of a field may be any numpy datatype including other structured datatypes, and it may also be a :term:`sub-array` which behaves like an ndarray of a specified shape. The offsets of the fields are arbitrary, and fields may even overlap. These offsets are usually determined automatically by numpy but can also be manually specified.

Structured datatypes may be created using the function :func:`numpy.dtype` with one of 3 alternative forms of specification which vary in flexibility and conciseness. The forms of specification are further documented in the :ref:`Data Type Objects <arrays.dtypes.constructing>` reference page, and in summary they are:

Each tuple has the form (fieldname, datatype, shape) where shape is optional. fieldname is a string, datatype may be any object convertible to a datatype, and shape is a tuple of integers specifying subarray shape.

>>> x = np.zeros(3, dtype=[('x', 'f4'), ('y', np.float32), ('z', 'f4', (2,2))])
>>> x
array([(0.0, 0.0, [[0.0, 0.0], [0.0, 0.0]]),
       (0.0, 0.0, [[0.0, 0.0], [0.0, 0.0]]),
       (0.0, 0.0, [[0.0, 0.0], [0.0, 0.0]])],
      dtype=[('x', '>f4'), ('y', '>f4'), ('z', '>f4', (2, 2))])

If fieldname is the empty string ('') the field will be given a default name of the form f#, where # is the integer index of the field, counting from 0 from the left.

>>> np.dtype([('x', 'f4'),('', 'i4'),('z', 'i8')])
dtype([('x', '<f4'), ('f1', '<i4'), ('z', '<i8')])

The byte offsets of the fields within the structure and the total structure itemsize are determined automatically.

In this shorthand notation any of the :ref:`string dtype specifications <arrays.dtypes.constructing>` may be used in a string, separated by commas. The itemsize and byte offsets of the fields are determined automatically, and the field names are given the default names f0, f1, etc.

>>> np.dtype('i8,f4,S3')
dtype([('f0', '<i8'), ('f1', '<f4'), ('f2', 'S3')])
>>> np.dtype('3int8, float32, (2,3)float64')
dtype([('f0', '|i1', 3), ('f1', '>f4'), ('f2', '>f8', (2, 3))])

This is the most flexible form of specification since it allows control over the byte-offsets of the fields and the itemsize of the structure.

The dictionary has two required keys, 'names' and 'formats', and four optional keys, 'offsets', 'itemsize', 'aligned' and 'titles'. 'names' and 'formats' should respectively correspond to a list of field names and a list of dtype specifications of the same length. The optional 'offsets' key must correspond to a list of integer byte-offsets of each field within the structure, of the same length. If 'offsets' is not given the offsets are determined automatically. The optional 'itemsize' key should correspond to an integer describing the total size in bytes of the dtype, which must be large enough that all the fields are contained.

>>> np.zeros(3, dtype={'names': ['col1', 'col2'], 'formats': ['i4','f4']})
array([(0, 0.0), (0, 0.0), (0, 0.0)],
      dtype=[('col1', '>i4'), ('col2', '>f4')])
>>> np.zeros(3, dtype={'names':    ['col1', 'col2'],
...                    'formats':  ['i4','f4'],
...                    'offsets':  [0, 4],
...                    'itemsize': 12})
array([(0, 0.0), (0, 0.0), (0, 0.0)],
      dtype={'names':['col1','col2'], 'formats':['<i4','<f4'], 'offsets':[0,4], 'itemsize':12})

Offsets may be chosen such that the fields overlap, though this will mean that assigning to one field may clobber any overlapping field's data. As an exception, fields of :class:`numpy.object` type .. (see :ref:`object arrays <arrays.object>`) cannot overlap with other fields, because of the risk of clobbering the internal object pointer and then dereferencing it.

The optional 'aligned' key can be set to true to create an aligned dtype (see :ref:`offsets-and-alignment`), as if the 'align' keyword argument of :func:`numpy.dtype` had been set to True.

The optional 'titles' key should correspond to a list of titles of the same length as 'names'. Use of this feature is discouraged, see :ref:`Obsolete Features <obsolete-features>`.

The list of field names of a structured datatype can be found in the names attribute of the dtype object:

>>> arr = np.zeros(2, dtype([('x', 'i8'), ('y', 'f4')]))
>>> arr.dtype.names
('x', 'y')

The field names may be modified by assigning to the names attribute using a sequence of strings of the same length.

The dtype object also has a dictionary-like attribute, fields, whose keys are the field names and whose values are tuples containing the dtype and byte offset of each field.

>>> arr.dtype.fields
<dictproxy {'a': (dtype('int64'), 0), 'b': (dtype('float32'), 8)}>

Note that the fields dictionary will also contain :term:`titles`, see :ref:`Obsolete Features <obsolete-features>` below.

Both the names and fields attributes will equal None for unstructured arrays.

The string representation of a structured datatype is shown in the "list of tuples" form if possible, otherwise numpy falls back to using the more general dictionary form.

Structured datatypes are implemented in numpy to have base type :class:`numpy.void` by default, but it is possible to interpret other numpy types as structured types using the (base_dtype, dtype) form of dtype specification described in :ref:`Data Type Objects <array.dtypes.constructing>`. Here, base_dtype is the desired underlying dtype, and fields and flags will be copied from dtype. This dtype will behave similarly to a 'union' in C.

Numpy uses one of two methods to automatically determine the field byte offsets and the overall itemsize of a structured type, depending on whether align=True was specified as a keyword argument to :func:`numpy.dtype`.

By default (with align=False), numpy will pack the fields together tightly such that each field starts at the byte offset the previous field ended, and the fields are contiguous in memory.

>>> def print_offsets(d):
...     print("offsets:", [d.fields[name][1] for name in d.names])
...     print("itemsize:", d.itemsize)
>>> print_offsets(np.dtype('u1,u1,i4,u1,i8,u2'))
offsets: [0, 1, 2, 6, 7, 15]
itemsize: 17

If align=True is set, numpy will pad the structure in the same way many C compilers would pad a C-struct. Aligned structures can give a performance improvement in some cases, at the cost of increased datatype size. Padding bytes are inserted between fields such that each field's byte offset will be a multiple of that field's alignment (usually equal to the field's size in bytes for simple datatypes, see http://docs.scipy.org/doc/numpy/reference/c-api.types-and-structures.html#c.PyArray_Descr.alignment). The structure will also have trailing padding added so that its itemsize is a multiple of the largest field's alignment.

>>> print_offsets(np.dtype('u1,u1,i4,u1,i8,u2', align=True))
offsets: [0, 1, 4, 8, 16, 24]
itemsize: 32

Note that although almost all modern C compilers pad in this way by default, padding in C structs is C-implementation-dependent so this memory layout is not guaranteed to exactly match that of a corresponding struct in a C program. Some massaging may be needed either on the numpy side or the C side to obtain exact correspondence.

If offsets were specified manually using the optional offsets key in the dictionary-based dtype specification, setting align=True will check that each field's offset is a multiple of its size and that the itemsize is a multiple of the largest field size, and raise an exception if not.

If the offsets of the fields and itemsize of a structured array satisfy the alignment conditions, the array will have the ALIGNED :ref:`flag <numpy.ndarray.flags>` set.

One can assign values to a structured array from python tuples, non-structured scalars, void structured scalars, and other structured arrays. One can get structures back by indexing with integers.

To create or assign to structured arrays using raw python data each input element should first be converted to a tuple (and not a list or array, as these will trigger numpy's broadcasting rules).

>>> x = np.array([(1,2,3),(4,5,6)], dtype='i8,f4,f8')
>>> x[1] = (7,8,9)

Indexing a single element of a structured array returns a structured scalar, which may be converted to a tuple by calling :func:`ndarray.item`:

>>> x[0], type(x[0])
((1, 2.0, 3.0), numpy.void)
>>> x[0].item(), type(x[0].item())
((1, 2.0, 3.0), tuple)

Tuples are the native python equivalent to numpy's structured types, much like native python integers are the equivalent to numpy's integer types.

A non-structured scalar assigned to a structured element will be assigned to all fields. This happens when a scalar is assigned to a structured array, or when a non-structured array is assigned to a structured array:

>>> x = np.zeros(2, dtype='i8,f4,?,S1')
>>> x[:] = 3
>>> x
array([(3, 3.0, True, '3'), (3, 3.0, True, '3')],
      dtype=[('f0', '<i8'), ('f1', '<f4'), ('f2', '?'), ('f3', 'S1')])
>>> x[:] = arange(2)
>>> x
array([(0, 0.0, False, '0'), (1, 1.0, True, '1')],
      dtype=[('f0', '<i8'), ('f1', '<f4'), ('f2', '?'), ('f3', 'S1')])

Structured elements can be assigned to non-structured elements only if the structured datatype has just a single field:

>>> b = np.array([3,4], dtype='f4')
>>> b[:] = x
TypeError
>>> y = np.zeros(2, dtype=[('foo', 'i8')])
>>> b[:] = y
>>> b
array([ 0.,  0.], dtype=float32)

Assignment between structured arrays occurs as if the source elements had been converted to tuples and then assigned to the destination elements. That is, the first field of the source array is assigned to the first field of the destination array, and the second field likewise, and so on, regardless of field names. Structured arrays with a different number of fields cannot be assigned to each other. Bytes of the destination structure which are not included in any of the fields are unaffected.

>>> a = np.zeros(3, dtype=[('a', 'i8'), ('b', 'f4'), ('c', 'S3')])
>>> b = np.ones(3, dtype=[('x', 'f4'), ('y', 'S3'), ('z', 'O')])
>>> b[:] = a
>>> b
array([(0.0, '0.0', ''), (0.0, '0.0', ''), (0.0, '0.0', '')],
      dtype=[('x', '<f4'), ('y', 'S3'), ('z', 'O')])

Single fields of a structured array may be accessed and modified by indexing the array with the field name.

>>> x = np.array([(1,2),(3,4)], dtype=[('foo', 'i8'), ('bar', 'f4')])
>>> x['foo']
array([1, 3])
>>> x['foo'] = 10
>>> x
array([(10, 2.0), (10, 4.0)],
      dtype=[('foo', '<i8'), ('bar', '<f4')])

The resulting array is a view into the original array. It shares the same memory locations and writing to the view will modify the original array.

>>> y = x['bar']
>>> y[:] = 10
>>> x
array([(10, 5.0), (10, 5.0)],
      dtype=[('foo', '<i8'), ('bar', '<f4')])

This view has the same dtype and itemsize as the indexed field, ie, it is typically a non-structured array (except in the case of nested structures).

>>> y.dtype, y.shape, y.strides
(dtype('float32'), (2,), (12,))

Note that unlike other numpy scalars void structured scalars act like views into the original ndarray. They support access and assignment by field name:

>>> x = np.array([(1,2),(3,4)], dtype=[('foo', 'i8'), ('bar', 'f4')])
>>> s = x[0]
>>> s['bar'] = 100
>>> x
array([(1, 100.0), (3, 4.0)],
      dtype=[('foo', '<i8'), ('bar', '<f4')])

In this sense, 1d structured arrays behave similarly to 2d non-structured arrays, as indexing using a single index returns a view into the array rather than an immutable scalar.

Numpy allows multi-field indices, where the index is a list of field names:

>>> a = np.zeros(3, dtype=[('a', 'i8'), ('b', 'i4'), ('c', 'f8')])
>>> a[['a', 'c']]
array([(0, 0.0), (0, 0.0), (0, 0.0)],
      dtype={'names':['a','c'], 'formats':['<i8','<f8'], 'offsets':[0,11], 'itemsize':19})
>>> a[['a', 'c']] = (2, 3)
>>> a
array([(2, 0, 3.0), (2, 0, 3.0), (2, 0, 3.0)],
      dtype=[('a', '<i8'), ('b', '<i4'), ('c', '<f8')])

The resulting array is a view of the original array, such that assignment to the view modifies the original array. The fields of the view will be in the order they were indexed. Note that unlike for single-field indexing here the view's dtype has the same itemsize as the original array and has fields at the same offsets as in the original array, and unindexed fields are merely missing.

Since this view is a structured array itself, it follows the assignment rules described above. For example, this means that one can swap the values of two fields:

>>> a[['a', 'c']] = a[['c', 'a']]

In order to prevent clobbering of object pointers in fields of :class:`numpy.object` type .. (see :ref:`Object Arrays <arrays.object>`) , numpy currently does not allow views of structured arrays containing objects.

If the dtypes of two structured arrays are equivalent, testing the equality of the arrays will result in a boolean array with the dimension of the original arrays, with elements set to True where all fields of the correspnding structures are equal. Structured dtypes are equivalent if the field names, dtypes and titles are the same, ignoring endianness.

>>> a = np.zeros(2, dtype=[('a', 'i4'), ('b', 'i4')])
>>> b = np.ones(2, dtype=[('a', 'i4'), ('b', 'i4')])
>>> a == b
array([False, False], dtype=bool)

Currently, if the dtypes of two arrays are not equivalent all comparisons will return False. This behavior is deprecated as of numpy 1.10 and may change in the future.

Currently, the < and > operators will always return False when comparing structured arrays. Many other pairwise operators are not supported.

As an optional convenience numpy provides an ndarray subclass, :class:`numpy.recarray`, and associated helper functions in the :mod:`numpy.rec` submodule, which allows access to fields of structured arrays by attribute. Record arrays also use a special datatype, :class:`numpy.record`, which allows field access by attribute on the structured scalars obtained from the array.

The simplest way to create a record array is with :func:`numpy.rec.array`:

>>> recordarr = np.rec.array([(1,2.,'Hello'),(2,3.,"World")],
...                    dtype=[('foo', 'i4'),('bar', 'f4'), ('baz', 'S10')])
>>> recordarr.bar
array([ 2.,  3.], dtype=float32)
>>> recordarr[1:2]
rec.array([(2, 3.0, 'World')],
      dtype=[('foo', '<i4'), ('bar', '<f4'), ('baz', 'S10')])
>>> recordarr[1:2].foo
array([2], dtype=int32)
>>> recordarr.foo[1:2]
array([2], dtype=int32)
>>> recordarr[1].baz
'World'

:func:`numpy.rec.array` can convert a wide variety of arguments into record arrays, including structured arrays:

>>> arr = array([(1,2.,'Hello'),(2,3.,"World")],
...             dtype=[('foo', 'i4'), ('bar', 'f4'), ('baz', 'S10')])
>>> recordarr = np.rec.array(arr)

The :mod:`numpy.rec` module provides a number of other convenience functions for creating record arrays, see :ref:`record array creation routines <routines.array-creation.rec>`.

A record array representation of a structured array can be obtained using the appropriate :ref:`view`:

>>> arr = np.array([(1,2.,'Hello'),(2,3.,"World")],
...                dtype=[('foo', 'i4'),('bar', 'f4'), ('baz', 'a10')])
>>> recordarr = arr.view(dtype=dtype((np.record, arr.dtype)),
...                      type=np.recarray)

For convenience, viewing an ndarray as type :class:`np.recarray` will automatically convert to :class:`np.record` datatype, so the dtype can be left out of the view:

>>> recordarr = arr.view(np.recarray)
>>> recordarr.dtype
dtype((numpy.record, [('foo', '<i4'), ('bar', '<f4'), ('baz', 'S10')]))

To get back to a plain ndarray both the dtype and type must be reset. The following view does so, taking into account the unusual case that the recordarr was not a structured type:

>>> arr2 = recordarr.view(recordarr.dtype.fields or recordarr.dtype, np.ndarray)

Record array fields accessed by index or by attribute are returned as a record array if the field has a structured type but as a plain ndarray otherwise.

>>> recordarr = np.rec.array([('Hello', (1,2)),("World", (3,4))],
...                 dtype=[('foo', 'S6'),('bar', [('A', int), ('B', int)])])
>>> type(recordarr.foo)
<type 'numpy.ndarray'>
>>> type(recordarr.bar)
<class 'numpy.core.records.recarray'>

Note that if a field has the same name as an ndarray attribute, the ndarray attribute takes precedence. Such fields will be inaccessible by attribute but will still be accessible by index.

In addition to field names, fields may also have an associated :term:`title`, an alternate name. Use of this feature is discouraged. To create structured types with title when using the list-of-tuples form of specification, the fieldname may be be given as a tuple of two strings, which will be the field's title and field name respectively. For example:

>>> np.dtype([(('mytitle', 'x'), 'f4')])

The title may be used in place of the field name when indexing. The titles may also be given in the dict form of specification as described above.

Note that the fields attribute of a dtype object will have both the field names and titles as keys, so effectively a field with a title will be represented twice, and furthermore the tuple values for these fields will have a third element, the field title. Because of this, it is recommended to iterate through the fields using the names attribute of the dtype (which will not list titles), as in:

>>> for name in d.names:
...     print(d.fields[name][:2])

This is also recommended because names preserves the field order while fields may not.

There is an additional dictionary-based form of dtype specification, though its use is discouraged. Here the keys are field names and values are tuples specifying type, offset and optional title:

>>> x = np.zeros(3, dtype={'col1': ('i1',0,'title 1'), 'col2': ('f4',1,'title 2')})
>>> x
array([(0, 0.0), (0, 0.0), (0, 0.0)],
      dtype=[(('title 1', 'col1'), '|i1'), (('title 2', 'col2'), '>f4')])

However, because Python dictionaries do not preserve order, and the order of the fields in a structured dtype has meaning, this form is discouraged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment