Skip to content
Merged
54 changes: 42 additions & 12 deletions chainladder/core/slice.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def get_idx(self, idx: tuple[_AxisKey, _AxisKey, _AxisKey, _AxisKey]) -> Triangl
c_idx: slice | np.ndarray = _LocBase._contig_slice(idx[1])
o_idx: slice | np.ndarray = _LocBase._contig_slice(idx[2])
d_idx: slice | np.ndarray = _LocBase._contig_slice(idx[3])
if type(o_idx) != slice or type(d_idx) != slice:
if type(o_idx) is not slice or type(d_idx) is not slice:
raise ValueError("Fancy indexing on origin/development is not supported.")
if type(i_idx) is slice or type(c_idx) is slice:
obj.values = obj.values[i_idx, c_idx, o_idx, d_idx]
Expand Down Expand Up @@ -150,11 +150,23 @@ 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]) is not slice or type(norm_key[3]) is not slice:
raise ValueError("Setting while fancy indexing on origin/development is not supported.")
Comment thread
cursor[bot] marked this conversation as resolved.
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]:
"""
Expand All @@ -174,19 +186,19 @@ def _normalize_index(self, key: IndexExpression) -> tuple[_AxisKey, _AxisKey, _A
"""
# Apply sparse normalization, fills out the rest of the dimensions using the shape of the Triangle.
key: tuple[_AxisKey, _AxisKey, _AxisKey, _AxisKey] = _slicing.normalize_index(key, self.obj.shape)
l = []
key_list = []
# Preserve start/stop/step boundaries if the user has specified them, otherwise replace them with None.
# None indicates "go-to-boundary" for the slice.
for n, i in enumerate(key):
if isinstance(i, slice):
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
l.append(slice(start, stop, step))
step: int | None = None if start is None and stop is None and (i.step == 1) else i.step
key_list.append(slice(start, stop, step))
else:
l.append(i)
key = tuple(l)
key_list.append(i)
key = tuple(key_list)
return key

def _sparse_setitem(
Expand Down Expand Up @@ -219,7 +231,7 @@ def _sparse_setitem(
(arr.coords[3] == key[3]))
# If it does, index the location and assign the value directly.
if check.max():
data_index = np.where(check == True)[0][0]
data_index = np.where(check)[0][0]
arr.data[data_index] = values
# Otherwise, create a new sparse array with the updated coordinates and data.
else:
Expand Down Expand Up @@ -431,6 +443,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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like I missed filling out a docstring when I annotated the file. Could you fill it out?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


class Ilocation(_LocBase):
Expand All @@ -454,7 +481,10 @@ class TriangleSlicer:
def __getitem__(self: TriangleProtocol, key: pd.Series | np.ndarray | list[str]) -> Triangle: ...
@overload
def __getitem__(self: TriangleProtocol, key: str | int) -> Triangle | pd.Series: ...
def __getitem__(self: TriangleProtocol, key: pd.Series | np.ndarray | str | list[str] | int) -> Triangle | pd.Series:
def __getitem__(
self: TriangleProtocol,
key: pd.Series | np.ndarray | str | list[str] | int
) -> Triangle | pd.Series:
"""
Boolean Slicer functionality.

Expand Down Expand Up @@ -756,7 +786,7 @@ def _check_index(self, key: IndexExpression) -> tuple[int, int, int, int]:
"""
idx = self._normalize_index(key)
types = {type(i) for i in idx}
if len(types) > 1 or list(types)[0] != int:
if len(types) > 1 or list(types)[0] is not int:
raise ValueError('iAt based indexing can only have integer indexers')
return cast("tuple[int, int, int, int]", idx)

Expand Down
142 changes: 142 additions & 0 deletions chainladder/core/tests/test_slicing.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,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:
"""
Expand Down Expand Up @@ -340,6 +365,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:
"""
Expand All @@ -358,6 +406,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:
"""
Expand All @@ -383,6 +454,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()
Expand Down
Loading