diff --git a/chainladder/core/slice.py b/chainladder/core/slice.py index 96a477995..3364fe13d 100644 --- a/chainladder/core/slice.py +++ b/chainladder/core/slice.py @@ -150,11 +150,20 @@ def __setitem__( raise ValueError('Setting values with sparse backend requires .at or .iat') if isinstance(values, TriangleSlicer): values = values.values + # attempt to make keys contig + contig_key = tuple([_LocBase._contig_slice(x) for x in key]) # Create a slice for any key elements that are integers, otherwise preserve the slice or array. - key = tuple( - [slice(item, item + 1) if isinstance(item, int) else item for item in key] + tuple_key = tuple( + [slice(item, item + 1) if isinstance(item, int) else item for item in contig_key] ) - cast(np.ndarray, cast(object, self.obj.values)).__setitem__(self._normalize_index(key), values) + norm_key = self._normalize_index(tuple_key) + if type(norm_key[2]) != slice or type(norm_key[3]) != slice: + raise ValueError("Setting while fancy indexing on origin/development is not supported.") + if type(norm_key[0]) is slice or type(norm_key[1]) is slice: + cast(np.ndarray, cast(object, self.obj.values)).__setitem__(norm_key, values) + else: + #the getter uses arr[idx,:][:,idx] to get the Cartesian product, using np.ix_ on the setter to match + cast(np.ndarray, cast(object, self.obj.values)).__setitem__(np.ix_(norm_key[0], norm_key[1])+(norm_key[2], norm_key[3]), values) def _normalize_index(self, key: IndexExpression) -> tuple[_AxisKey, _AxisKey, _AxisKey, _AxisKey]: """ @@ -182,7 +191,7 @@ def _normalize_index(self, key: IndexExpression) -> tuple[_AxisKey, _AxisKey, _A start: int | None= i.start if i.start > 0 else None stop: int | None = i.stop if i.stop > -1 else None stop: int | None = None if stop == self.obj.shape[n] else stop - step: int | None = None if start is None and stop is None else i.step + step: int | None = None if start is None and stop is None and (i.step == 1) else i.step l.append(slice(start, stop, step)) else: l.append(i) @@ -431,6 +440,21 @@ def key_to_slice(self, key: _LabelKey) -> tuple[_AxisKey, _AxisKey, _AxisKey, _A return out def __setitem__(self, key: _LabelKey, values: int | float | TriangleSlicer) -> None: + """ + Supports the .loc[] for setting Triangle values. Only supported for numpy backend. + + Parameters + ---------- + key: _LabelKey + Indicates the location of the Triangle you want to set values for. + values: int | float | TriangleSlicer + The value(s) you want to assign to the slice of the Triangle. + + Returns + ------- + None + + """ super().__setitem__(cast(tuple[_AxisKey, _AxisKey, _AxisKey, _AxisKey], self.key_to_slice(key)), values) class Ilocation(_LocBase): diff --git a/chainladder/core/tests/test_slicing.py b/chainladder/core/tests/test_slicing.py index c4c2f5efb..02c7f2738 100644 --- a/chainladder/core/tests/test_slicing.py +++ b/chainladder/core/tests/test_slicing.py @@ -273,7 +273,32 @@ def test_loc_setitem_triangle_value(clrd: Triangle) -> None: tri.loc["Aegis Grp", "comauto"] = sub * 2 assert tri.loc["Aegis Grp", "comauto"] == sub * 2 +def test_loc_setitem_partial_triangles(raa: Triangle) -> None: + """ + Use Triangle.loc to set a few origin or develop period via a TriangleSlicer. + Parameters + ---------- + raa: Triangle + The raa sample data set fixture. + + Returns + ------- + None + + """ + raa2 = raa * 2 + raa_new = raa.copy() + if raa_new.array_backend == "sparse": + pytest.skip("Test is specific to the numpy backend.") + else: + raa_new.loc[:,:,:,:60] = raa2.loc[:,:,:,:60] + raa_new.loc[:,:,:,72:] = raa2.loc[:,:,:,72:] + assert raa_new == raa2 + raa_new = raa.copy() + raa_new.loc[:,:,:'1984',:] = raa2.loc[:,:,:'1984',:] + raa_new.loc[:,:,'1985':,:] = raa2.loc[:,:,'1985':,:] + assert raa_new == raa2 def test_invalid_iloc_sparse_assignment(prism) -> None: """ @@ -328,6 +353,29 @@ def test_get_idx_fancy_origin_raises(raa: Triangle) -> None: with pytest.raises(ValueError, match="Fancy indexing on origin/development is not supported"): _= raa.iloc[0, 0, [0, 1, 5], :] +def test_set_fancy_origin_raises(raa: Triangle) -> None: + """ + Attempt setting with fancy indexing on origin axis, raise an error. + + Parameters + ---------- + raa: Triangle + The raa sample data set fixture. + + Returns + ------- + None + + """ + raa_copy = raa.copy() + if raa.array_backend == "sparse": + pytest.skip("Test is specific to the numpy backend.") + else: + with pytest.raises(ValueError, match="Setting while fancy indexing on origin/development is not supported."): + raa_copy.iloc[0, 0, [0, 1, 5], :] = raa.iloc[0, 0, :3, :] + with pytest.raises(ValueError, match="Setting while fancy indexing on origin/development is not supported."): + raa_copy.loc['Total', 'values', ['1983', '1984', '1986'], :] = raa.iloc[0, 0, :3, :] + def test_get_idx_fancy_development_raises(raa: Triangle) -> None: """ @@ -346,6 +394,29 @@ def test_get_idx_fancy_development_raises(raa: Triangle) -> None: with pytest.raises(ValueError, match="Fancy indexing on origin/development is not supported"): _= raa.iloc[0, 0, :, [0, 1, 5]] +def test_set_fancy_development_raises(raa: Triangle) -> None: + """ + Attempt setting with fancy indexing on development axis, raise an error. + + Parameters + ---------- + raa: Triangle + The raa sample data set fixture. + + Returns + ------- + None + + """ + raa_copy = raa.copy() + if raa.array_backend == "sparse": + pytest.skip("Test is specific to the numpy backend.") + else: + with pytest.raises(ValueError, match="Setting while fancy indexing on origin/development is not supported."): + raa_copy.iloc[0, 0, :, [0, 1, 5]] = raa.iloc[0, 0, :, :3] + with pytest.raises(ValueError, match="Setting while fancy indexing on origin/development is not supported."): + raa_copy.loc['Total', 'values', :, [12, 24, 48]] = raa.iloc[0, 0, :, :3] + def test_get_idx_non_contiguous_index_and_columns(clrd: Triangle) -> None: """ @@ -371,6 +442,77 @@ def test_get_idx_non_contiguous_index_and_columns(clrd: Triangle) -> None: assert result.index.values.tolist() == expected_index assert result.columns.tolist() == ['IncurLoss', 'CumPaidLoss', 'EarnedPremNet'] +def test_loc_setting_non_contiguous_index_and_columns(clrd: Triangle) -> None: + """ + Set lists of non-contiguous indexes and column through Triangle.loc. Check the values + of the assigned triangles. + + Parameters + ---------- + clrd: Triangle + The clrd sample data set fixture. + + Returns + ------- + None + + """ + if clrd.array_backend == "sparse": + pytest.skip("Test is specific to the numpy backend.") + else: + dest_index = [ + ['Adriatic Ins Co', 'othliab'], + ['Adriatic Ins Co', 'ppauto'], + ['Agency Ins Co Of MD Inc', 'ppauto'], + ] + val_index = [ + ['Adriatic Ins Co', 'ppauto'], + ['Aegis Grp', 'comauto'], + ['Agency Ins Co Of MD Inc', 'ppauto'], + ] + dest_col = ['CumPaidLoss', 'BulkLoss', 'EarnedPremNet'] + val_col = ['IncurLoss', 'CumPaidLoss', 'EarnedPremNet'] + clrd_copy = clrd.copy() + clrd_copy.loc[dest_index] = clrd.loc[val_index] + assert clrd_copy.loc[dest_index] == clrd.loc[val_index] + clrd_copy = clrd.copy() + clrd_copy.loc[:,dest_col] = clrd.loc[:,val_col] + assert clrd_copy.loc[:,dest_col] == clrd.loc[:,val_col] + clrd_copy = clrd.copy() + clrd_copy.loc[dest_index,dest_col] = clrd.loc[val_index,val_col] + assert clrd_copy.loc[dest_index,dest_col] == clrd.loc[val_index,val_col] + +def test_iloc_setting_non_contiguous_index_and_columns(clrd: Triangle) -> None: + """ + Set lists of non-contiguous indexes and column through Triangle.iloc. Check the values + of the assigned triangles. + + Parameters + ---------- + clrd: Triangle + The clrd sample data set fixture. + + Returns + ------- + None + + """ + if clrd.array_backend == "sparse": + pytest.skip("Test is specific to the numpy backend.") + else: + dest_index = [0,1,5] + val_index = [1,4,6] + dest_col = [2,3,5] + val_col = [1,2,4] + clrd_copy = clrd.copy() + clrd_copy.iloc[dest_index] = clrd.iloc[val_index] + assert clrd_copy.iloc[dest_index] == clrd.iloc[val_index] + clrd_copy = clrd.copy() + clrd_copy.iloc[:,dest_col] = clrd.iloc[:,val_col] + assert clrd_copy.iloc[:,dest_col] == clrd.iloc[:,val_col] + clrd_copy = clrd.copy() + clrd_copy.iloc[dest_index,dest_col] = clrd.iloc[val_index,val_col] + assert clrd_copy.iloc[dest_index,dest_col] == clrd.iloc[val_index,val_col] def test_sparse_at_iat1(prism): t = prism.copy()