Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
729b2d6
FIX: Fix errors in friedland_uspp_auto_increasing_claim.csv.
genedan Aug 15, 2025
6aeb4ef
Merge branch 'casact:master' into master
genedan Jan 16, 2026
dd96fd1
Merge branch 'casact:master' into master
genedan Jan 21, 2026
13a0451
Merge branch 'casact:master' into master
genedan Jan 22, 2026
4f90122
FEAT: Add validation to TrianglePandas.rename. Also add tests to ensu…
genedan Jan 22, 2026
08b7a95
FEAT; Add test for exception handling in TrianglePandas.rename.
genedan Jan 22, 2026
0a44f0f
DOCS: Add annotations to TrianglePandas.to_frame.
genedan Jan 24, 2026
6011018
DOCS, TEST: Add nbmake to dependencies and update GitHub action to te…
genedan Jan 25, 2026
2a2ae16
Merge branch 'casact:master' into master
genedan Jan 25, 2026
4d0e673
Merge remote-tracking branch 'origin/master'
genedan Jan 25, 2026
0024b13
DOCS: Add jupyter-black to dependencies.
genedan Jan 25, 2026
50841a0
DOCS: Add jupyter-black to dependencies.
genedan Jan 25, 2026
a215318
DOCS: Add jupyter-black to dependencies.
genedan Jan 25, 2026
68a1667
TEST: Add missing statsmodels dependency.
genedan Jan 25, 2026
ee65023
DOCS: Fix incorrect axis in triangle tutorial.
genedan Jan 25, 2026
d1b0e22
TEST: Allow errors in sandbox_workbook_blank.ipynb.
genedan Jan 25, 2026
6dc64db
FIX: Fix bug where you cannot add a reordered triangle back to anothe…
genedan Jan 25, 2026
fa12f3b
FIX: Fix clrd error.
genedan Jan 26, 2026
445c063
FIX: Reverting type hint.
genedan Jan 26, 2026
7e1dd27
FIX: Add jupyter-black to docs dependencies.
genedan Jan 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Install dependencies
run: uv sync --extra test
- name: Run tests
run: uv run pytest --cov=chainladder --cov-report=xml
run: uv run pytest --nbmake --cov=chainladder --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
Expand Down
13 changes: 8 additions & 5 deletions chainladder/core/dunders.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,20 @@ def _prep_columns(self, x, y):
else:
# Find columns to add to each triangle
cols_to_add_to_x = [col for col in y.columns if col not in x.columns]
cols_to_add_to_y = [col for col in x.columns if col not in y.columns]

# Create new columns only if necessary
cols_to_add_to_y = [col for col in x.columns if col not in y.columns]

# Start with case with no new columns, y simply has a different order
new_x_cols = list(x.columns)

# Then, if there are new columns, add them.
if cols_to_add_to_x:
new_x_cols = list(x.columns) + list(cols_to_add_to_x)
x = x.reindex(columns=new_x_cols, fill_value=0)

if cols_to_add_to_y:
new_y_cols = list(y.columns) + list(cols_to_add_to_y)
y = y.reindex(columns=new_y_cols, fill_value=0)

# Ensure both triangles have the same column order
x = x[new_x_cols]
y = y[new_x_cols]
Expand Down
85 changes: 52 additions & 33 deletions chainladder/core/pandas.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
if TYPE_CHECKING:
from chainladder import Triangle
from collections.abc import Callable
from numpy import ndarray
from numpy.typing import ArrayLike
from pandas import (
DataFrame,
Series
)
from types import ModuleType
from typing import (
Literal,
Expand Down Expand Up @@ -52,8 +57,14 @@ def __getitem__(self, key):


class TrianglePandas:
def to_frame(self, origin_as_datetime=True, keepdims=False,
implicit_axis=False, *args, **kwargs):
def to_frame(
self,
origin_as_datetime: bool = True,
keepdims: bool = False,
implicit_axis: bool = False,
*args,
**kwargs
) -> DataFrame | Series:
""" Converts a triangle to a pandas.DataFrame.
Parameters
----------
Expand All @@ -69,55 +80,58 @@ def to_frame(self, origin_as_datetime=True, keepdims=False,
valuation axis in addition to the origin and development.
Returns
-------
pandas.DataFrame representation of the Triangle.
DataFrame or Series representation of the Triangle.
"""
axes = [num for num, item in enumerate(self.shape) if item > 1]

# Identify the axes that increase the dimensionality of the triangle, i.e., those whose length is > 1.
axes: list[int] = [num for num, item in enumerate(self.shape) if item > 1]

# Long format.
if keepdims:
is_val_tri = self.is_val_tri
obj = self.val_to_dev().set_backend("sparse")
out = pd.DataFrame(obj.index.iloc[obj.values.coords[0]])
out["columns"] = obj.columns[obj.values.coords[1]]
missing_cols = list(set(self.columns) - set(out['columns']))
is_val_tri: bool = self.is_val_tri
obj: Triangle = self.val_to_dev().set_backend("sparse")
out: DataFrame = pd.DataFrame(obj.index.iloc[obj.values.coords[0]])
out["columns"]: Series = obj.columns[obj.values.coords[1]]
missing_cols: list = list(set(self.columns) - set(out['columns']))
if origin_as_datetime:
out["origin"] = obj.odims[obj.values.coords[2]]
out["origin"]: Series = obj.odims[obj.values.coords[2]]
else:
out["origin"] = obj.origin[obj.values.coords[2]]
out["development"] = obj.ddims[obj.values.coords[3]]
out["values"] = obj.values.data
out = pd.pivot_table(
out["origin"]: Series = obj.origin[obj.values.coords[2]]
out["development"]: Series = obj.ddims[obj.values.coords[3]]
out["values"]: Series = obj.values.data
out: DataFrame = pd.pivot_table(
out, index=obj.key_labels + ["origin", "development"], columns="columns"
)
out = out.reset_index().set_index(obj.key_labels)
out: DataFrame = out.reset_index().set_index(obj.key_labels)
out.columns = ["origin", "development"] + list(
out.columns.get_level_values(1)[2:]
)

valuation = pd.DataFrame(
valuation: DataFrame = pd.DataFrame(
obj.valuation.values.reshape(obj.shape[-2:], order='F'),
index=obj.odims if origin_as_datetime else obj.origin,
columns=obj.ddims
).unstack().rename('valuation').reset_index().rename(
columns={'level_0': 'development', 'level_1': 'origin'})

val_dict = dict(zip(list(zip(
val_dict: dict = dict(zip(list(zip(
valuation['origin'], valuation['development'])),
valuation['valuation']))
if len(out) > 0:
out['valuation'] = out.apply(
out['valuation']: Series = out.apply(
lambda x: val_dict[(x['origin'], x['development'])], axis=1)
else:
out['valuation'] = self.valuation_date
col_order = list(self.columns)
out['valuation']: Series = self.valuation_date
col_order: list = list(self.columns)
if implicit_axis:
col_order = ['origin', 'development', 'valuation'] + col_order
col_order: list = ['origin', 'development', 'valuation'] + col_order
else:
if is_val_tri:
col_order = ['origin', 'valuation'] + col_order
col_order: list = ['origin', 'valuation'] + col_order
else:
col_order = ['origin', 'development'] + col_order
col_order: list = ['origin', 'development'] + col_order
for col in set(missing_cols) - self.virtual_columns.columns.keys():
out[col] = np.nan
out[col]: Series = np.nan
for col in set(missing_cols).intersection(self.virtual_columns.columns.keys()):
out[col] = out.fillna(0).apply(self.virtual_columns.columns[col], 1)
out.loc[out[col] == 0, col] = np.nan
Expand All @@ -126,35 +140,40 @@ def to_frame(self, origin_as_datetime=True, keepdims=False,

# keepdims = False
else:
# Case when there is a single triangle, for a single segment.
if self.shape[:2] == (1, 1):
return self._repr_format(origin_as_datetime)

# Case when triangle is multidimensional but is of unusual shape, such as a collection of latest diagonals.
elif len(axes) in [1, 2]:
tri = np.squeeze(self.set_backend("numpy").values)
axes_lookup = {
tri: ndarray = np.squeeze(self.set_backend("numpy").values)
axes_lookup: dict = {
0: self.kdims,
1: self.vdims,
2: self.origin,
3: self.development,
}

# Set the index to be key dimension if the key dimension is greater than length 1.
if axes[0] == 0:
idx = self.index.set_index(self.key_labels).index
# Otherwise, find the axis that is greater than length 0 and set that to be the index.
else:
idx = axes_lookup[axes[0]]

if len(axes) == 1:
return pd.Series(tri, index=idx).fillna(0)

elif len(axes) == 2:
# Case len(axes) == 2.
else:
return pd.DataFrame(
tri, index=idx, columns=axes_lookup[axes[1]]
).fillna(0)

# Multidimensional triangles, return DataFrame in long form.
else:
return self.to_frame(
origin_as_datetime=origin_as_datetime, keepdims=True,
implicit_axis=implicit_axis)
origin_as_datetime=origin_as_datetime,
keepdims=True,
implicit_axis=implicit_axis
)

def plot(self, *args, **kwargs):
"""Passthrough of pandas functionality"""
Expand Down Expand Up @@ -410,7 +429,7 @@ def agg_func(
axis: str | int | None = None,
*args,
**kwargs
) -> Triangle | np.ndarray:
) -> Triangle | ndarray:
"""
Applies the aggregation function specified by k from the outer function.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,10 @@
"language": "python",
"name": "python3"
},
"execution": {
"allow_errors": true,
"timeout": 300
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
Expand Down
Loading