1""":mod:`numpy.ma..mrecords` 2 3Defines the equivalent of :class:`numpy.recarrays` for masked arrays, 4where fields can be accessed as attributes. 5Note that :class:`numpy.ma.MaskedArray` already supports structured datatypes 6and the masking of individual fields. 7 8.. moduleauthor:: Pierre Gerard-Marchant 9 10""" 11# We should make sure that no field is called '_mask','mask','_fieldmask', 12# or whatever restricted keywords. An idea would be to no bother in the 13# first place, and then rename the invalid fields with a trailing 14# underscore. Maybe we could just overload the parser function ? 15 16import warnings 17 18import numpy as np 19from numpy import ( 20 bool_, dtype, ndarray, recarray, array as narray 21 ) 22from numpy.core.records import ( 23 fromarrays as recfromarrays, fromrecords as recfromrecords 24 ) 25 26_byteorderconv = np.core.records._byteorderconv 27 28import numpy.ma as ma 29from numpy.ma import ( 30 MAError, MaskedArray, masked, nomask, masked_array, getdata, 31 getmaskarray, filled 32 ) 33 34_check_fill_value = ma.core._check_fill_value 35 36 37__all__ = [ 38 'MaskedRecords', 'mrecarray', 'fromarrays', 'fromrecords', 39 'fromtextfile', 'addfield', 40 ] 41 42reserved_fields = ['_data', '_mask', '_fieldmask', 'dtype'] 43 44 45def _checknames(descr, names=None): 46 """ 47 Checks that field names ``descr`` are not reserved keywords. 48 49 If this is the case, a default 'f%i' is substituted. If the argument 50 `names` is not None, updates the field names to valid names. 51 52 """ 53 ndescr = len(descr) 54 default_names = ['f%i' % i for i in range(ndescr)] 55 if names is None: 56 new_names = default_names 57 else: 58 if isinstance(names, (tuple, list)): 59 new_names = names 60 elif isinstance(names, str): 61 new_names = names.split(',') 62 else: 63 raise NameError(f'illegal input names {names!r}') 64 nnames = len(new_names) 65 if nnames < ndescr: 66 new_names += default_names[nnames:] 67 ndescr = [] 68 for (n, d, t) in zip(new_names, default_names, descr.descr): 69 if n in reserved_fields: 70 if t[0] in reserved_fields: 71 ndescr.append((d, t[1])) 72 else: 73 ndescr.append(t) 74 else: 75 ndescr.append((n, t[1])) 76 return np.dtype(ndescr) 77 78 79def _get_fieldmask(self): 80 mdescr = [(n, '|b1') for n in self.dtype.names] 81 fdmask = np.empty(self.shape, dtype=mdescr) 82 fdmask.flat = tuple([False] * len(mdescr)) 83 return fdmask 84 85 86class MaskedRecords(MaskedArray): 87 """ 88 89 Attributes 90 ---------- 91 _data : recarray 92 Underlying data, as a record array. 93 _mask : boolean array 94 Mask of the records. A record is masked when all its fields are 95 masked. 96 _fieldmask : boolean recarray 97 Record array of booleans, setting the mask of each individual field 98 of each record. 99 _fill_value : record 100 Filling values for each field. 101 102 """ 103 104 def __new__(cls, shape, dtype=None, buf=None, offset=0, strides=None, 105 formats=None, names=None, titles=None, 106 byteorder=None, aligned=False, 107 mask=nomask, hard_mask=False, fill_value=None, keep_mask=True, 108 copy=False, 109 **options): 110 111 self = recarray.__new__(cls, shape, dtype=dtype, buf=buf, offset=offset, 112 strides=strides, formats=formats, names=names, 113 titles=titles, byteorder=byteorder, 114 aligned=aligned,) 115 116 mdtype = ma.make_mask_descr(self.dtype) 117 if mask is nomask or not np.size(mask): 118 if not keep_mask: 119 self._mask = tuple([False] * len(mdtype)) 120 else: 121 mask = np.array(mask, copy=copy) 122 if mask.shape != self.shape: 123 (nd, nm) = (self.size, mask.size) 124 if nm == 1: 125 mask = np.resize(mask, self.shape) 126 elif nm == nd: 127 mask = np.reshape(mask, self.shape) 128 else: 129 msg = "Mask and data not compatible: data size is %i, " + \ 130 "mask size is %i." 131 raise MAError(msg % (nd, nm)) 132 copy = True 133 if not keep_mask: 134 self.__setmask__(mask) 135 self._sharedmask = True 136 else: 137 if mask.dtype == mdtype: 138 _mask = mask 139 else: 140 _mask = np.array([tuple([m] * len(mdtype)) for m in mask], 141 dtype=mdtype) 142 self._mask = _mask 143 return self 144 145 def __array_finalize__(self, obj): 146 # Make sure we have a _fieldmask by default 147 _mask = getattr(obj, '_mask', None) 148 if _mask is None: 149 objmask = getattr(obj, '_mask', nomask) 150 _dtype = ndarray.__getattribute__(self, 'dtype') 151 if objmask is nomask: 152 _mask = ma.make_mask_none(self.shape, dtype=_dtype) 153 else: 154 mdescr = ma.make_mask_descr(_dtype) 155 _mask = narray([tuple([m] * len(mdescr)) for m in objmask], 156 dtype=mdescr).view(recarray) 157 # Update some of the attributes 158 _dict = self.__dict__ 159 _dict.update(_mask=_mask) 160 self._update_from(obj) 161 if _dict['_baseclass'] == ndarray: 162 _dict['_baseclass'] = recarray 163 return 164 165 @property 166 def _data(self): 167 """ 168 Returns the data as a recarray. 169 170 """ 171 return ndarray.view(self, recarray) 172 173 @property 174 def _fieldmask(self): 175 """ 176 Alias to mask. 177 178 """ 179 return self._mask 180 181 def __len__(self): 182 """ 183 Returns the length 184 185 """ 186 # We have more than one record 187 if self.ndim: 188 return len(self._data) 189 # We have only one record: return the nb of fields 190 return len(self.dtype) 191 192 def __getattribute__(self, attr): 193 try: 194 return object.__getattribute__(self, attr) 195 except AttributeError: 196 # attr must be a fieldname 197 pass 198 fielddict = ndarray.__getattribute__(self, 'dtype').fields 199 try: 200 res = fielddict[attr][:2] 201 except (TypeError, KeyError) as e: 202 raise AttributeError(f'record array has no attribute {attr}') from e 203 # So far, so good 204 _localdict = ndarray.__getattribute__(self, '__dict__') 205 _data = ndarray.view(self, _localdict['_baseclass']) 206 obj = _data.getfield(*res) 207 if obj.dtype.names is not None: 208 raise NotImplementedError("MaskedRecords is currently limited to" 209 "simple records.") 210 # Get some special attributes 211 # Reset the object's mask 212 hasmasked = False 213 _mask = _localdict.get('_mask', None) 214 if _mask is not None: 215 try: 216 _mask = _mask[attr] 217 except IndexError: 218 # Couldn't find a mask: use the default (nomask) 219 pass 220 tp_len = len(_mask.dtype) 221 hasmasked = _mask.view((bool, ((tp_len,) if tp_len else ()))).any() 222 if (obj.shape or hasmasked): 223 obj = obj.view(MaskedArray) 224 obj._baseclass = ndarray 225 obj._isfield = True 226 obj._mask = _mask 227 # Reset the field values 228 _fill_value = _localdict.get('_fill_value', None) 229 if _fill_value is not None: 230 try: 231 obj._fill_value = _fill_value[attr] 232 except ValueError: 233 obj._fill_value = None 234 else: 235 obj = obj.item() 236 return obj 237 238 def __setattr__(self, attr, val): 239 """ 240 Sets the attribute attr to the value val. 241 242 """ 243 # Should we call __setmask__ first ? 244 if attr in ['mask', 'fieldmask']: 245 self.__setmask__(val) 246 return 247 # Create a shortcut (so that we don't have to call getattr all the time) 248 _localdict = object.__getattribute__(self, '__dict__') 249 # Check whether we're creating a new field 250 newattr = attr not in _localdict 251 try: 252 # Is attr a generic attribute ? 253 ret = object.__setattr__(self, attr, val) 254 except Exception: 255 # Not a generic attribute: exit if it's not a valid field 256 fielddict = ndarray.__getattribute__(self, 'dtype').fields or {} 257 optinfo = ndarray.__getattribute__(self, '_optinfo') or {} 258 if not (attr in fielddict or attr in optinfo): 259 raise 260 else: 261 # Get the list of names 262 fielddict = ndarray.__getattribute__(self, 'dtype').fields or {} 263 # Check the attribute 264 if attr not in fielddict: 265 return ret 266 if newattr: 267 # We just added this one or this setattr worked on an 268 # internal attribute. 269 try: 270 object.__delattr__(self, attr) 271 except Exception: 272 return ret 273 # Let's try to set the field 274 try: 275 res = fielddict[attr][:2] 276 except (TypeError, KeyError): 277 raise AttributeError(f'record array has no attribute {attr}') 278 279 if val is masked: 280 _fill_value = _localdict['_fill_value'] 281 if _fill_value is not None: 282 dval = _localdict['_fill_value'][attr] 283 else: 284 dval = val 285 mval = True 286 else: 287 dval = filled(val) 288 mval = getmaskarray(val) 289 obj = ndarray.__getattribute__(self, '_data').setfield(dval, *res) 290 _localdict['_mask'].__setitem__(attr, mval) 291 return obj 292 293 def __getitem__(self, indx): 294 """ 295 Returns all the fields sharing the same fieldname base. 296 297 The fieldname base is either `_data` or `_mask`. 298 299 """ 300 _localdict = self.__dict__ 301 _mask = ndarray.__getattribute__(self, '_mask') 302 _data = ndarray.view(self, _localdict['_baseclass']) 303 # We want a field 304 if isinstance(indx, str): 305 # Make sure _sharedmask is True to propagate back to _fieldmask 306 # Don't use _set_mask, there are some copies being made that 307 # break propagation Don't force the mask to nomask, that wreaks 308 # easy masking 309 obj = _data[indx].view(MaskedArray) 310 obj._mask = _mask[indx] 311 obj._sharedmask = True 312 fval = _localdict['_fill_value'] 313 if fval is not None: 314 obj._fill_value = fval[indx] 315 # Force to masked if the mask is True 316 if not obj.ndim and obj._mask: 317 return masked 318 return obj 319 # We want some elements. 320 # First, the data. 321 obj = np.array(_data[indx], copy=False).view(mrecarray) 322 obj._mask = np.array(_mask[indx], copy=False).view(recarray) 323 return obj 324 325 def __setitem__(self, indx, value): 326 """ 327 Sets the given record to value. 328 329 """ 330 MaskedArray.__setitem__(self, indx, value) 331 if isinstance(indx, str): 332 self._mask[indx] = ma.getmaskarray(value) 333 334 def __str__(self): 335 """ 336 Calculates the string representation. 337 338 """ 339 if self.size > 1: 340 mstr = [f"({','.join([str(i) for i in s])})" 341 for s in zip(*[getattr(self, f) for f in self.dtype.names])] 342 return f"[{', '.join(mstr)}]" 343 else: 344 mstr = [f"{','.join([str(i) for i in s])}" 345 for s in zip([getattr(self, f) for f in self.dtype.names])] 346 return f"({', '.join(mstr)})" 347 348 def __repr__(self): 349 """ 350 Calculates the repr representation. 351 352 """ 353 _names = self.dtype.names 354 fmt = "%%%is : %%s" % (max([len(n) for n in _names]) + 4,) 355 reprstr = [fmt % (f, getattr(self, f)) for f in self.dtype.names] 356 reprstr.insert(0, 'masked_records(') 357 reprstr.extend([fmt % (' fill_value', self.fill_value), 358 ' )']) 359 return str("\n".join(reprstr)) 360 361 def view(self, dtype=None, type=None): 362 """ 363 Returns a view of the mrecarray. 364 365 """ 366 # OK, basic copy-paste from MaskedArray.view. 367 if dtype is None: 368 if type is None: 369 output = ndarray.view(self) 370 else: 371 output = ndarray.view(self, type) 372 # Here again. 373 elif type is None: 374 try: 375 if issubclass(dtype, ndarray): 376 output = ndarray.view(self, dtype) 377 dtype = None 378 else: 379 output = ndarray.view(self, dtype) 380 # OK, there's the change 381 except TypeError: 382 dtype = np.dtype(dtype) 383 # we need to revert to MaskedArray, but keeping the possibility 384 # of subclasses (eg, TimeSeriesRecords), so we'll force a type 385 # set to the first parent 386 if dtype.fields is None: 387 basetype = self.__class__.__bases__[0] 388 output = self.__array__().view(dtype, basetype) 389 output._update_from(self) 390 else: 391 output = ndarray.view(self, dtype) 392 output._fill_value = None 393 else: 394 output = ndarray.view(self, dtype, type) 395 # Update the mask, just like in MaskedArray.view 396 if (getattr(output, '_mask', nomask) is not nomask): 397 mdtype = ma.make_mask_descr(output.dtype) 398 output._mask = self._mask.view(mdtype, ndarray) 399 output._mask.shape = output.shape 400 return output 401 402 def harden_mask(self): 403 """ 404 Forces the mask to hard. 405 406 """ 407 self._hardmask = True 408 409 def soften_mask(self): 410 """ 411 Forces the mask to soft 412 413 """ 414 self._hardmask = False 415 416 def copy(self): 417 """ 418 Returns a copy of the masked record. 419 420 """ 421 copied = self._data.copy().view(type(self)) 422 copied._mask = self._mask.copy() 423 return copied 424 425 def tolist(self, fill_value=None): 426 """ 427 Return the data portion of the array as a list. 428 429 Data items are converted to the nearest compatible Python type. 430 Masked values are converted to fill_value. If fill_value is None, 431 the corresponding entries in the output list will be ``None``. 432 433 """ 434 if fill_value is not None: 435 return self.filled(fill_value).tolist() 436 result = narray(self.filled().tolist(), dtype=object) 437 mask = narray(self._mask.tolist()) 438 result[mask] = None 439 return result.tolist() 440 441 def __getstate__(self): 442 """Return the internal state of the masked array. 443 444 This is for pickling. 445 446 """ 447 state = (1, 448 self.shape, 449 self.dtype, 450 self.flags.fnc, 451 self._data.tobytes(), 452 self._mask.tobytes(), 453 self._fill_value, 454 ) 455 return state 456 457 def __setstate__(self, state): 458 """ 459 Restore the internal state of the masked array. 460 461 This is for pickling. ``state`` is typically the output of the 462 ``__getstate__`` output, and is a 5-tuple: 463 464 - class name 465 - a tuple giving the shape of the data 466 - a typecode for the data 467 - a binary string for the data 468 - a binary string for the mask. 469 470 """ 471 (ver, shp, typ, isf, raw, msk, flv) = state 472 ndarray.__setstate__(self, (shp, typ, isf, raw)) 473 mdtype = dtype([(k, bool_) for (k, _) in self.dtype.descr]) 474 self.__dict__['_mask'].__setstate__((shp, mdtype, isf, msk)) 475 self.fill_value = flv 476 477 def __reduce__(self): 478 """ 479 Return a 3-tuple for pickling a MaskedArray. 480 481 """ 482 return (_mrreconstruct, 483 (self.__class__, self._baseclass, (0,), 'b',), 484 self.__getstate__()) 485 486def _mrreconstruct(subtype, baseclass, baseshape, basetype,): 487 """ 488 Build a new MaskedArray from the information stored in a pickle. 489 490 """ 491 _data = ndarray.__new__(baseclass, baseshape, basetype).view(subtype) 492 _mask = ndarray.__new__(ndarray, baseshape, 'b1') 493 return subtype.__new__(subtype, _data, mask=_mask, dtype=basetype,) 494 495mrecarray = MaskedRecords 496 497 498############################################################################### 499# Constructors # 500############################################################################### 501 502 503def fromarrays(arraylist, dtype=None, shape=None, formats=None, 504 names=None, titles=None, aligned=False, byteorder=None, 505 fill_value=None): 506 """ 507 Creates a mrecarray from a (flat) list of masked arrays. 508 509 Parameters 510 ---------- 511 arraylist : sequence 512 A list of (masked) arrays. Each element of the sequence is first converted 513 to a masked array if needed. If a 2D array is passed as argument, it is 514 processed line by line 515 dtype : {None, dtype}, optional 516 Data type descriptor. 517 shape : {None, integer}, optional 518 Number of records. If None, shape is defined from the shape of the 519 first array in the list. 520 formats : {None, sequence}, optional 521 Sequence of formats for each individual field. If None, the formats will 522 be autodetected by inspecting the fields and selecting the highest dtype 523 possible. 524 names : {None, sequence}, optional 525 Sequence of the names of each field. 526 fill_value : {None, sequence}, optional 527 Sequence of data to be used as filling values. 528 529 Notes 530 ----- 531 Lists of tuples should be preferred over lists of lists for faster processing. 532 533 """ 534 datalist = [getdata(x) for x in arraylist] 535 masklist = [np.atleast_1d(getmaskarray(x)) for x in arraylist] 536 _array = recfromarrays(datalist, 537 dtype=dtype, shape=shape, formats=formats, 538 names=names, titles=titles, aligned=aligned, 539 byteorder=byteorder).view(mrecarray) 540 _array._mask.flat = list(zip(*masklist)) 541 if fill_value is not None: 542 _array.fill_value = fill_value 543 return _array 544 545 546def fromrecords(reclist, dtype=None, shape=None, formats=None, names=None, 547 titles=None, aligned=False, byteorder=None, 548 fill_value=None, mask=nomask): 549 """ 550 Creates a MaskedRecords from a list of records. 551 552 Parameters 553 ---------- 554 reclist : sequence 555 A list of records. Each element of the sequence is first converted 556 to a masked array if needed. If a 2D array is passed as argument, it is 557 processed line by line 558 dtype : {None, dtype}, optional 559 Data type descriptor. 560 shape : {None,int}, optional 561 Number of records. If None, ``shape`` is defined from the shape of the 562 first array in the list. 563 formats : {None, sequence}, optional 564 Sequence of formats for each individual field. If None, the formats will 565 be autodetected by inspecting the fields and selecting the highest dtype 566 possible. 567 names : {None, sequence}, optional 568 Sequence of the names of each field. 569 fill_value : {None, sequence}, optional 570 Sequence of data to be used as filling values. 571 mask : {nomask, sequence}, optional. 572 External mask to apply on the data. 573 574 Notes 575 ----- 576 Lists of tuples should be preferred over lists of lists for faster processing. 577 578 """ 579 # Grab the initial _fieldmask, if needed: 580 _mask = getattr(reclist, '_mask', None) 581 # Get the list of records. 582 if isinstance(reclist, ndarray): 583 # Make sure we don't have some hidden mask 584 if isinstance(reclist, MaskedArray): 585 reclist = reclist.filled().view(ndarray) 586 # Grab the initial dtype, just in case 587 if dtype is None: 588 dtype = reclist.dtype 589 reclist = reclist.tolist() 590 mrec = recfromrecords(reclist, dtype=dtype, shape=shape, formats=formats, 591 names=names, titles=titles, 592 aligned=aligned, byteorder=byteorder).view(mrecarray) 593 # Set the fill_value if needed 594 if fill_value is not None: 595 mrec.fill_value = fill_value 596 # Now, let's deal w/ the mask 597 if mask is not nomask: 598 mask = np.array(mask, copy=False) 599 maskrecordlength = len(mask.dtype) 600 if maskrecordlength: 601 mrec._mask.flat = mask 602 elif mask.ndim == 2: 603 mrec._mask.flat = [tuple(m) for m in mask] 604 else: 605 mrec.__setmask__(mask) 606 if _mask is not None: 607 mrec._mask[:] = _mask 608 return mrec 609 610 611def _guessvartypes(arr): 612 """ 613 Tries to guess the dtypes of the str_ ndarray `arr`. 614 615 Guesses by testing element-wise conversion. Returns a list of dtypes. 616 The array is first converted to ndarray. If the array is 2D, the test 617 is performed on the first line. An exception is raised if the file is 618 3D or more. 619 620 """ 621 vartypes = [] 622 arr = np.asarray(arr) 623 if arr.ndim == 2: 624 arr = arr[0] 625 elif arr.ndim > 2: 626 raise ValueError("The array should be 2D at most!") 627 # Start the conversion loop. 628 for f in arr: 629 try: 630 int(f) 631 except (ValueError, TypeError): 632 try: 633 float(f) 634 except (ValueError, TypeError): 635 try: 636 complex(f) 637 except (ValueError, TypeError): 638 vartypes.append(arr.dtype) 639 else: 640 vartypes.append(np.dtype(complex)) 641 else: 642 vartypes.append(np.dtype(float)) 643 else: 644 vartypes.append(np.dtype(int)) 645 return vartypes 646 647 648def openfile(fname): 649 """ 650 Opens the file handle of file `fname`. 651 652 """ 653 # A file handle 654 if hasattr(fname, 'readline'): 655 return fname 656 # Try to open the file and guess its type 657 try: 658 f = open(fname) 659 except IOError: 660 raise IOError(f"No such file: '{fname}'") 661 if f.readline()[:2] != "\\x": 662 f.seek(0, 0) 663 return f 664 f.close() 665 raise NotImplementedError("Wow, binary file") 666 667 668def fromtextfile(fname, delimitor=None, commentchar='#', missingchar='', 669 varnames=None, vartypes=None): 670 """ 671 Creates a mrecarray from data stored in the file `filename`. 672 673 Parameters 674 ---------- 675 fname : {file name/handle} 676 Handle of an opened file. 677 delimitor : {None, string}, optional 678 Alphanumeric character used to separate columns in the file. 679 If None, any (group of) white spacestring(s) will be used. 680 commentchar : {'#', string}, optional 681 Alphanumeric character used to mark the start of a comment. 682 missingchar : {'', string}, optional 683 String indicating missing data, and used to create the masks. 684 varnames : {None, sequence}, optional 685 Sequence of the variable names. If None, a list will be created from 686 the first non empty line of the file. 687 vartypes : {None, sequence}, optional 688 Sequence of the variables dtypes. If None, it will be estimated from 689 the first non-commented line. 690 691 692 Ultra simple: the varnames are in the header, one line""" 693 # Try to open the file. 694 ftext = openfile(fname) 695 696 # Get the first non-empty line as the varnames 697 while True: 698 line = ftext.readline() 699 firstline = line[:line.find(commentchar)].strip() 700 _varnames = firstline.split(delimitor) 701 if len(_varnames) > 1: 702 break 703 if varnames is None: 704 varnames = _varnames 705 706 # Get the data. 707 _variables = masked_array([line.strip().split(delimitor) for line in ftext 708 if line[0] != commentchar and len(line) > 1]) 709 (_, nfields) = _variables.shape 710 ftext.close() 711 712 # Try to guess the dtype. 713 if vartypes is None: 714 vartypes = _guessvartypes(_variables[0]) 715 else: 716 vartypes = [np.dtype(v) for v in vartypes] 717 if len(vartypes) != nfields: 718 msg = "Attempting to %i dtypes for %i fields!" 719 msg += " Reverting to default." 720 warnings.warn(msg % (len(vartypes), nfields), stacklevel=2) 721 vartypes = _guessvartypes(_variables[0]) 722 723 # Construct the descriptor. 724 mdescr = [(n, f) for (n, f) in zip(varnames, vartypes)] 725 mfillv = [ma.default_fill_value(f) for f in vartypes] 726 727 # Get the data and the mask. 728 # We just need a list of masked_arrays. It's easier to create it like that: 729 _mask = (_variables.T == missingchar) 730 _datalist = [masked_array(a, mask=m, dtype=t, fill_value=f) 731 for (a, m, t, f) in zip(_variables.T, _mask, vartypes, mfillv)] 732 733 return fromarrays(_datalist, dtype=mdescr) 734 735 736def addfield(mrecord, newfield, newfieldname=None): 737 """Adds a new field to the masked record array 738 739 Uses `newfield` as data and `newfieldname` as name. If `newfieldname` 740 is None, the new field name is set to 'fi', where `i` is the number of 741 existing fields. 742 743 """ 744 _data = mrecord._data 745 _mask = mrecord._mask 746 if newfieldname is None or newfieldname in reserved_fields: 747 newfieldname = 'f%i' % len(_data.dtype) 748 newfield = ma.array(newfield) 749 # Get the new data. 750 # Create a new empty recarray 751 newdtype = np.dtype(_data.dtype.descr + [(newfieldname, newfield.dtype)]) 752 newdata = recarray(_data.shape, newdtype) 753 # Add the existing field 754 [newdata.setfield(_data.getfield(*f), *f) 755 for f in _data.dtype.fields.values()] 756 # Add the new field 757 newdata.setfield(newfield._data, *newdata.dtype.fields[newfieldname]) 758 newdata = newdata.view(MaskedRecords) 759 # Get the new mask 760 # Create a new empty recarray 761 newmdtype = np.dtype([(n, bool_) for n in newdtype.names]) 762 newmask = recarray(_data.shape, newmdtype) 763 # Add the old masks 764 [newmask.setfield(_mask.getfield(*f), *f) 765 for f in _mask.dtype.fields.values()] 766 # Add the mask of the new field 767 newmask.setfield(getmaskarray(newfield), 768 *newmask.dtype.fields[newfieldname]) 769 newdata._mask = newmask 770 return newdata 771