How to create arrays of strings#
Awkward Arrays can contain strings, although these strings are just a special view of lists of uint8
numbers. As such, the variable-length data are efficiently stored.
NumPy’s strings are padded to have equal width, and Pandas’s strings are Python objects. Awkward Array doesn’t have nearly as many functions for manipulating arrays of strings as NumPy and Pandas, though.
import awkward as ak
import numpy as np
From Python strings#
The ak.Array
constructor and ak.from_iter()
recognize strings, and strings are returned by ak.to_list()
.
ak.Array(["one", "two", "three"])
['one', 'two', 'three'] ---------------- type: 3 * string
They may be nested within anything.
ak.Array([["one", "two"], [], ["three"]])
[['one', 'two'], [], ['three']] ---------------------- type: 3 * var * string
From NumPy arrays#
NumPy strings are also recognized by ak.from_numpy()
and ak.to_numpy()
.
numpy_array = np.array(["one", "two", "three", "four"])
numpy_array
array(['one', 'two', 'three', 'four'], dtype='<U5')
awkward_array = ak.Array(numpy_array)
awkward_array
['one', 'two', 'three', 'four'] ---------------- type: 4 * string
Operations with strings#
Since strings are really just lists, some of the list operations “just work” on strings.
ak.num(awkward_array)
[3, 3, 5, 4] --------------- type: 4 * int64
awkward_array[:, 1:]
['ne', 'wo', 'hree', 'our'] ---------------- type: 4 * string
Others had to be specially overloaded for the string case, such as string-equality. The default meaning for ==
would be to descend to the lowest level and compare numbers (characters, in this case).
awkward_array == "three"
[False, False, True, False] -------------- type: 4 * bool
awkward_array == ak.Array(["ONE", "TWO", "three", "four"])
[False, False, True, True] -------------- type: 4 * bool
Similarly, ak.sort()
and ak.argsort()
sort strings lexicographically, not individual characters.
ak.sort(awkward_array)
['four', 'one', 'three', 'two'] ---------------- type: 4 * string
Still other operations had to be inhibited, since they wouldn’t make sense for strings.
np.sqrt(awkward_array)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[11], line 1
----> 1 np.sqrt(awkward_array)
File ~/micromamba/envs/awkward-docs/lib/python3.11/site-packages/awkward/highlevel.py:1511, in Array.__array_ufunc__(self, ufunc, method, *inputs, **kwargs)
1509 name = f"{type(ufunc).__module__}.{ufunc.__name__}.{method!s}"
1510 with ak._errors.OperationErrorContext(name, inputs, kwargs):
-> 1511 return ak._connect.numpy.array_ufunc(ufunc, method, inputs, kwargs)
File ~/micromamba/envs/awkward-docs/lib/python3.11/site-packages/awkward/_connect/numpy.py:458, in array_ufunc(ufunc, method, inputs, kwargs)
450 raise TypeError(
451 "no {}.{} overloads for custom types: {}".format(
452 type(ufunc).__module__, ufunc.__name__, ", ".join(error_message)
453 )
454 )
456 return None
--> 458 out = ak._broadcasting.broadcast_and_apply(
459 inputs, action, allow_records=False, function_name=ufunc.__name__
460 )
462 if len(out) == 1:
463 return wrap_layout(out[0], behavior=behavior, attrs=attrs)
File ~/micromamba/envs/awkward-docs/lib/python3.11/site-packages/awkward/_broadcasting.py:1027, in broadcast_and_apply(inputs, action, depth_context, lateral_context, allow_records, left_broadcast, right_broadcast, numpy_to_regular, regular_to_jagged, function_name, broadcast_parameters_rule)
1025 backend = backend_of(*inputs, coerce_to_common=False)
1026 isscalar = []
-> 1027 out = apply_step(
1028 backend,
1029 broadcast_pack(inputs, isscalar),
1030 action,
1031 0,
1032 depth_context,
1033 lateral_context,
1034 {
1035 "allow_records": allow_records,
1036 "left_broadcast": left_broadcast,
1037 "right_broadcast": right_broadcast,
1038 "numpy_to_regular": numpy_to_regular,
1039 "regular_to_jagged": regular_to_jagged,
1040 "function_name": function_name,
1041 "broadcast_parameters_rule": broadcast_parameters_rule,
1042 },
1043 )
1044 assert isinstance(out, tuple)
1045 return tuple(broadcast_unpack(x, isscalar) for x in out)
File ~/micromamba/envs/awkward-docs/lib/python3.11/site-packages/awkward/_broadcasting.py:1005, in apply_step(backend, inputs, action, depth, depth_context, lateral_context, options)
1003 return result
1004 elif result is None:
-> 1005 return continuation()
1006 else:
1007 raise AssertionError(result)
File ~/micromamba/envs/awkward-docs/lib/python3.11/site-packages/awkward/_broadcasting.py:974, in apply_step.<locals>.continuation()
972 # Any non-string list-types?
973 elif any(x.is_list and not is_string_like(x) for x in contents):
--> 974 return broadcast_any_list()
976 # Any RecordArrays?
977 elif any(x.is_record for x in contents):
File ~/micromamba/envs/awkward-docs/lib/python3.11/site-packages/awkward/_broadcasting.py:630, in apply_step.<locals>.broadcast_any_list()
627 nextinputs.append(x)
628 nextparameters.append(NO_PARAMETERS)
--> 630 outcontent = apply_step(
631 backend,
632 nextinputs,
633 action,
634 depth + 1,
635 copy.copy(depth_context),
636 lateral_context,
637 options,
638 )
639 assert isinstance(outcontent, tuple)
640 parameters = parameters_factory(nextparameters, len(outcontent))
File ~/micromamba/envs/awkward-docs/lib/python3.11/site-packages/awkward/_broadcasting.py:987, in apply_step(backend, inputs, action, depth, depth_context, lateral_context, options)
980 else:
981 raise ValueError(
982 "cannot broadcast: {}{}".format(
983 ", ".join(repr(type(x)) for x in inputs), in_function(options)
984 )
985 )
--> 987 result = action(
988 inputs,
989 depth=depth,
990 depth_context=depth_context,
991 lateral_context=lateral_context,
992 continuation=continuation,
993 backend=backend,
994 options=options,
995 )
997 if isinstance(result, tuple) and all(isinstance(x, Content) for x in result):
998 if any(content.backend is not backend for content in result):
File ~/micromamba/envs/awkward-docs/lib/python3.11/site-packages/awkward/_connect/numpy.py:394, in array_ufunc.<locals>.action(inputs, **ignore)
389 # Do we have all-strings? If so, we can't proceed
390 if all(
391 x.is_list and x.parameter("__array__") in ("string", "bytestring")
392 for x in contents
393 ):
--> 394 raise TypeError(
395 f"{type(ufunc).__module__}.{ufunc.__name__} is not implemented for string types. "
396 "To register an implementation, add a name to these string(s) and register a behavior overload"
397 )
399 if ufunc is numpy.matmul:
400 raise NotImplementedError(
401 "matrix multiplication (`@` or `np.matmul`) is not yet implemented for Awkward Arrays"
402 )
TypeError: numpy.sqrt is not implemented for string types. To register an implementation, add a name to these string(s) and register a behavior overload
This error occurred while calling
numpy.sqrt.__call__(
<Array ['one', 'two', 'three', 'four'] type='4 * string'>
)
Categorical strings#
A large set of strings with few unique values are more efficiently manipulated as integers than as strings. In Pandas, this is categorical data, in R, it’s called a factor, and in Arrow and Parquet, it’s dictionary encoding.
The ak.str.to_categorical()
(requires PyArrow) function makes Awkward Arrays categorical in this sense. ak.to_arrow()
and ak.to_parquet()
recognize categorical data and convert it to the corresponding Arrow and Parquet types.
uncategorized = ak.Array(["three", "one", "two", "two", "three", "one", "one", "one"])
uncategorized
['three', 'one', 'two', 'two', 'three', 'one', 'one', 'one'] ---------------- type: 8 * string
categorized = ak.str.to_categorical(uncategorized)
categorized
['three', 'one', 'two', 'two', 'three', 'one', 'one', 'one'] ---------------------------------- type: 8 * categorical[type=string]
Internally, the data now have an index that selects from a set of unique strings.
categorized.layout.index
<Index dtype='int64' len='8'>[0 1 2 2 0 1 1 1]</Index>
ak.Array(categorized.layout.content)
['three', 'one', 'two'] ---------------- type: 3 * string
The main advantage to Awkward categorical data (other than proper conversions to Arrow and Parquet) is that equality is performed using the index integers.
categorized == "one"
[False, True, False, False, False, True, True, True] -------------- type: 8 * bool
With ArrayBuilder#
ak.ArrayBuilder()
is described in more detail in this tutorial, but you can add strings by calling the string
method or simply appending them.
(This is what ak.from_iter()
uses internally to accumulate data.)
builder = ak.ArrayBuilder()
builder.string("one")
builder.append("two")
builder.append("three")
array = builder.snapshot()
array
['one', 'two', 'three'] ---------------- type: 3 * string