Skip to content

Commit b1f2c1d

Browse files
alixdammangdementen
authored andcommitted
fix #782 : implemented AxisCollection.set_labels()
+ moved _guess_axis() from LArray to AxisCollection
1 parent ee8ef74 commit b1f2c1d

File tree

4 files changed

+168
-61
lines changed

4 files changed

+168
-61
lines changed

doc/source/api.rst

+1
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ Modifying/Selecting
202202
AxisCollection.insert
203203
AxisCollection.rename
204204
AxisCollection.replace
205+
AxisCollection.set_labels
205206
AxisCollection.without
206207
AxisCollection.combine_axes
207208
AxisCollection.split_axes

doc/source/changes/version_0_30.rst.inc

+2
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ Miscellaneous improvements
243243
the overall performance improvement is negligible over most of the real-world models using larray that we tested these
244244
changes on.
245245

246+
* implemented :py:obj:`AxisCollection.set_labels()` (closes :issue:`782`).
247+
246248

247249
Fixes
248250
^^^^^

larray/core/array.py

+8-61
Original file line numberDiff line numberDiff line change
@@ -2082,37 +2082,6 @@ def _translate_axis_key(self, axis_key):
20822082
else:
20832083
return self._translate_axis_key_chunk(axis_key)
20842084

2085-
def _guess_axis(self, axis_key):
2086-
if isinstance(axis_key, Group):
2087-
group_axis = axis_key.axis
2088-
if group_axis is not None:
2089-
# we have axis information but not necessarily an Axis object from self.axes
2090-
real_axis = self.axes[group_axis]
2091-
if group_axis is not real_axis:
2092-
axis_key = axis_key.with_axis(real_axis)
2093-
return axis_key
2094-
2095-
# TODO: instead of checking all axes, we should have a big mapping
2096-
# (in AxisCollection or LArray):
2097-
# label -> (axis, index)
2098-
# or possibly (for ambiguous labels)
2099-
# label -> {axis: index}
2100-
# but for Pandas, this wouldn't work, we'd need label -> axis
2101-
valid_axes = []
2102-
for axis in self.axes:
2103-
try:
2104-
axis.index(axis_key)
2105-
valid_axes.append(axis)
2106-
except KeyError:
2107-
continue
2108-
if not valid_axes:
2109-
raise ValueError("%s is not a valid label for any axis" % axis_key)
2110-
elif len(valid_axes) > 1:
2111-
valid_axes = ', '.join(a.name if a.name is not None else '{{{}}}'.format(self.axes.index(a))
2112-
for a in valid_axes)
2113-
raise ValueError('%s is ambiguous (valid in %s)' % (axis_key, valid_axes))
2114-
return valid_axes[0][axis_key]
2115-
21162085
def __getitem__(self, key, collapse_slices=False, translate_key=True):
21172086
data = self.data
21182087
# FIXME: I have a huge problem with boolean axis labels + non points
@@ -2788,13 +2757,13 @@ def to_labelgroup(key, stack_depth=1):
27882757
# new axis, and igroups are not the same that LGroups in this regard (I wonder if ideally it shouldn't
27892758
# be the same???)
27902759
# groups = tuple(self._translate_axis_key(k) for k in key)
2791-
groups = tuple(self._guess_axis(_to_key(k, stack_depth + 1)) for k in key)
2760+
groups = tuple(self.axes._guess_axis(_to_key(k, stack_depth + 1)) for k in key)
27922761
axis = groups[0].axis
27932762
if not all(g.axis.equals(axis) for g in groups[1:]):
27942763
raise ValueError("group with different axes: %s" % str(key))
27952764
return groups
27962765
if isinstance(key, (Group, int, basestring, list, slice)):
2797-
return self._guess_axis(key)
2766+
return self.axes._guess_axis(key)
27982767
else:
27992768
raise NotImplementedError("%s has invalid type (%s) for a group aggregate key"
28002769
% (key, type(key).__name__))
@@ -7146,7 +7115,7 @@ def __array__(self, dtype=None):
71467115

71477116
# TODO: this should be a thin wrapper around a method in AxisCollection
71487117
def set_labels(self, axis=None, labels=None, inplace=False, **kwargs):
7149-
r"""Replaces the labels of an axis of array.
7118+
r"""Replaces the labels of one or several axes of the array.
71507119
71517120
Parameters
71527121
----------
@@ -7170,6 +7139,10 @@ def set_labels(self, axis=None, labels=None, inplace=False, **kwargs):
71707139
LArray
71717140
Array with modified labels.
71727141
7142+
See Also
7143+
--------
7144+
AxisCollection.set_labels
7145+
71737146
Examples
71747147
--------
71757148
>>> a = ndtest('nat=BE,FO;sex=M,F')
@@ -7234,33 +7207,7 @@ def set_labels(self, axis=None, labels=None, inplace=False, **kwargs):
72347207
Belgian 0 1
72357208
FO 2 3
72367209
"""
7237-
if axis is None:
7238-
changes = {}
7239-
elif isinstance(axis, dict):
7240-
changes = axis
7241-
elif isinstance(axis, (basestring, Axis, int)):
7242-
changes = {axis: labels}
7243-
else:
7244-
raise ValueError("Expected None or a string/int/Axis/dict instance for axis argument")
7245-
changes.update(kwargs)
7246-
# TODO: we should implement the non-dict behavior in Axis.replace, so that we can simplify this code to:
7247-
# new_axes = [self.axes[old_axis].replace(axis_changes) for old_axis, axis_changes in changes.items()]
7248-
new_axes = []
7249-
for old_axis, axis_changes in changes.items():
7250-
try:
7251-
real_axis = self.axes[old_axis]
7252-
except KeyError:
7253-
axis_changes = {old_axis: axis_changes}
7254-
real_axis = self._guess_axis(old_axis).axis
7255-
if isinstance(axis_changes, dict):
7256-
new_axis = real_axis.replace(axis_changes)
7257-
elif callable(axis_changes):
7258-
new_axis = real_axis.apply(axis_changes)
7259-
else:
7260-
new_axis = Axis(axis_changes, real_axis.name)
7261-
new_axes.append((real_axis, new_axis))
7262-
axes = self.axes.replace(new_axes)
7263-
7210+
axes = self.axes.set_labels(axis, labels, **kwargs)
72647211
if inplace:
72657212
self.axes = axes
72667213
return self

larray/core/axis.py

+157
Original file line numberDiff line numberDiff line change
@@ -2324,6 +2324,163 @@ def replace(self, axes_to_replace=None, new_axis=None, inplace=False, **kwargs):
23242324
else:
23252325
return AxisCollection(axes)
23262326

2327+
def _guess_axis(self, axis_key):
2328+
if isinstance(axis_key, Group):
2329+
group_axis = axis_key.axis
2330+
if group_axis is not None:
2331+
# we have axis information but not necessarily an Axis object from self.axes
2332+
real_axis = self[group_axis]
2333+
if group_axis is not real_axis:
2334+
axis_key = axis_key.with_axis(real_axis)
2335+
return axis_key
2336+
2337+
# TODO: instead of checking all axes, we should have a big mapping
2338+
# (in AxisCollection or LArray):
2339+
# label -> (axis, index)
2340+
# or possibly (for ambiguous labels)
2341+
# label -> {axis: index}
2342+
# but for Pandas, this wouldn't work, we'd need label -> axis
2343+
valid_axes = []
2344+
for axis in self:
2345+
try:
2346+
axis.index(axis_key)
2347+
valid_axes.append(axis)
2348+
except KeyError:
2349+
continue
2350+
if not valid_axes:
2351+
raise ValueError("%s is not a valid label for any axis" % axis_key)
2352+
elif len(valid_axes) > 1:
2353+
valid_axes = ', '.join(a.name if a.name is not None else '{{{}}}'.format(self.axes.index(a))
2354+
for a in valid_axes)
2355+
raise ValueError('%s is ambiguous (valid in %s)' % (axis_key, valid_axes))
2356+
return valid_axes[0][axis_key]
2357+
2358+
def set_labels(self, axis=None, labels=None, inplace=False, **kwargs):
2359+
r"""Replaces the labels of one or several axes.
2360+
2361+
Parameters
2362+
----------
2363+
axis : string or Axis or dict
2364+
Axis for which we want to replace labels, or mapping {axis: changes} where changes can either be the
2365+
complete list of labels, a mapping {old_label: new_label} or a function to transform labels.
2366+
If there is no ambiguity (two or more axes have the same labels), `axis` can be a direct mapping
2367+
{old_label: new_label}.
2368+
labels : int, str, iterable or mapping or function, optional
2369+
Integer or list of values usable as the collection of labels for an Axis. If this is mapping, it must be
2370+
{old_label: new_label}. If it is a function, it must be a function accepting a single argument (a
2371+
label) and returning a single value. This argument must not be used if axis is a mapping.
2372+
inplace : bool, optional
2373+
Whether or not to modify the original object or return a new AxisCollection and leave the original intact.
2374+
Defaults to False.
2375+
**kwargs :
2376+
`axis`=`labels` for each axis you want to set labels.
2377+
2378+
Returns
2379+
-------
2380+
AxisCollection
2381+
AxisCollection with modified labels.
2382+
2383+
Examples
2384+
--------
2385+
>>> from larray import ndtest
2386+
>>> axes = AxisCollection('nat=BE,FO;sex=M,F')
2387+
>>> axes
2388+
AxisCollection([
2389+
Axis(['BE', 'FO'], 'nat'),
2390+
Axis(['M', 'F'], 'sex')
2391+
])
2392+
>>> axes.set_labels('sex', ['Men', 'Women'])
2393+
AxisCollection([
2394+
Axis(['BE', 'FO'], 'nat'),
2395+
Axis(['Men', 'Women'], 'sex')
2396+
])
2397+
2398+
when passing a single string as labels, it will be interpreted to create the list of labels, so that one can
2399+
use the same syntax than during axis creation.
2400+
2401+
>>> axes.set_labels('sex', 'Men,Women')
2402+
AxisCollection([
2403+
Axis(['BE', 'FO'], 'nat'),
2404+
Axis(['Men', 'Women'], 'sex')
2405+
])
2406+
2407+
to replace only some labels, one must give a mapping giving the new label for each label to replace
2408+
2409+
>>> axes.set_labels('sex', {'M': 'Men'})
2410+
AxisCollection([
2411+
Axis(['BE', 'FO'], 'nat'),
2412+
Axis(['Men', 'F'], 'sex')
2413+
])
2414+
2415+
to transform labels by a function, use any function accepting and returning a single argument:
2416+
2417+
>>> axes.set_labels('nat', str.lower)
2418+
AxisCollection([
2419+
Axis(['be', 'fo'], 'nat'),
2420+
Axis(['M', 'F'], 'sex')
2421+
])
2422+
2423+
to replace labels for several axes at the same time, one should give a mapping giving the new labels for each
2424+
changed axis
2425+
2426+
>>> axes.set_labels({'sex': 'Men,Women', 'nat': 'Belgian,Foreigner'})
2427+
AxisCollection([
2428+
Axis(['Belgian', 'Foreigner'], 'nat'),
2429+
Axis(['Men', 'Women'], 'sex')
2430+
])
2431+
2432+
or use keyword arguments
2433+
2434+
>>> axes.set_labels(sex='Men,Women', nat='Belgian,Foreigner')
2435+
AxisCollection([
2436+
Axis(['Belgian', 'Foreigner'], 'nat'),
2437+
Axis(['Men', 'Women'], 'sex')
2438+
])
2439+
2440+
one can also replace some labels in several axes by giving a mapping of mappings
2441+
2442+
>>> axes.set_labels({'sex': {'M': 'Men'}, 'nat': {'BE': 'Belgian'}})
2443+
AxisCollection([
2444+
Axis(['Belgian', 'FO'], 'nat'),
2445+
Axis(['Men', 'F'], 'sex')
2446+
])
2447+
2448+
when there is no ambiguity (two or more axes have the same labels), it is possible to give a mapping
2449+
between old and new labels
2450+
2451+
>>> axes.set_labels({'M': 'Men', 'BE': 'Belgian'})
2452+
AxisCollection([
2453+
Axis(['Belgian', 'FO'], 'nat'),
2454+
Axis(['Men', 'F'], 'sex')
2455+
])
2456+
"""
2457+
if axis is None:
2458+
changes = {}
2459+
elif isinstance(axis, dict):
2460+
changes = axis
2461+
elif isinstance(axis, (basestring, Axis, int)):
2462+
changes = {axis: labels}
2463+
else:
2464+
raise ValueError("Expected None or a string/int/Axis/dict instance for axis argument")
2465+
changes.update(kwargs)
2466+
# TODO: we should implement the non-dict behavior in Axis.replace, so that we can simplify this code to:
2467+
# new_axes = [self[old_axis].replace(axis_changes) for old_axis, axis_changes in changes.items()]
2468+
new_axes = []
2469+
for old_axis, axis_changes in changes.items():
2470+
try:
2471+
real_axis = self[old_axis]
2472+
except KeyError:
2473+
axis_changes = {old_axis: axis_changes}
2474+
real_axis = self._guess_axis(old_axis).axis
2475+
if isinstance(axis_changes, dict):
2476+
new_axis = real_axis.replace(axis_changes)
2477+
elif callable(axis_changes):
2478+
new_axis = real_axis.apply(axis_changes)
2479+
else:
2480+
new_axis = Axis(axis_changes, real_axis.name)
2481+
new_axes.append((real_axis, new_axis))
2482+
return self.replace(new_axes, inplace=inplace)
2483+
23272484
# TODO: deprecate method (should use __sub__ instead)
23282485
def without(self, axes):
23292486
"""

0 commit comments

Comments
 (0)