Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
53 changes: 39 additions & 14 deletions chainladder/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,16 +175,38 @@ def _set_development(
data: DataFrame,
development: list,
development_format: None | str,
origin_date: Series
origin_date: Series,
origin_grain: str
) -> Series:
"""Initialize development and its grain"""
if development:
development_date: Series = TriangleBase._to_datetime(
development_date: Series | None = TriangleBase._to_datetime(
data=data,
fields=development,
period_end=True,
date_format=development_format
date_format=development_format,
allow_age=True,
)
if development_date is None:
# age in months relative to origin's period start, using the
# constructor's own origin_grain so fiscal-year anchors match
age: Series = pd.to_numeric(data[development[0]]).round().astype(int)
grain_base = origin_grain.split("-")[0]
if grain_base == "2Q":
# no native pandas semiannual period; only calendar Jan/Jul anchors supported
if origin_grain not in ("2Q", "2Q-DEC"):
raise ValueError(
"Development expressed as an age is not yet supported for a "
f"non-calendar semiannual origin grain ({origin_grain})."
)
origin_period_start: Series = origin_date.apply(
lambda d: d.replace(month=((d.month - 1) // 6) * 6 + 1, day=1)
)
else:
origin_period_start = origin_date.dt.to_period(origin_grain).dt.to_timestamp(how="s")
development_date = (
origin_period_start.dt.to_period("M") + (age - 1)
).dt.to_timestamp(how="e")
else:
o_max: Timestamp = pd.Period(
value=origin_date.max(),
Expand All @@ -193,15 +215,6 @@ def _set_development(
development_date: Series = pd.Series([o_max] * len(origin_date))

development_date.name = "__development__"
if (
pd.Series(development_date).dt.year.min()
== pd.Series(development_date).dt.year.max()
== 1970
):
raise ValueError(
"Development lags could not be determined. This may be because development"
"is expressed as an age where a date-like vector is required"
)
return development_date

@staticmethod
Expand Down Expand Up @@ -462,8 +475,9 @@ def _to_datetime(
data: DataFrame,
fields: list,
period_end: bool = False,
date_format: Optional[str] = None
) -> Series:
date_format: Optional[str] = None,
allow_age: bool = False
) -> Series | None:
"""
For tabular form, this will take a set of data
column(s) and return a single date array. This function heavily
Expand Down Expand Up @@ -496,9 +510,11 @@ def _to_datetime(
]

datetime_mapping: None | dict = None
matched_a_format: bool = False
for date_inference in date_inference_list:
try:
datetime_mapping = dict(zip(datetime_arg, pd.to_datetime(**date_inference)))
matched_a_format = "format" in date_inference
break
except ValueError:
pass
Expand All @@ -508,6 +524,15 @@ def _to_datetime(
"Unable to infer datetime for field(s): " + str(fields) +
". Please check the underlying data or any supplied format arguments."
)
if not matched_a_format and pd.api.types.is_numeric_dtype(datetime_arg):
# unformatted numeric input falls through to pandas treating it
# as nanoseconds since epoch, not an actual date
if allow_age:
return None
raise ValueError(
"Development lags could not be determined. This may be because development "
"is expressed as an age where a date-like vector is required"
)
target: Series = target_field.map(datetime_mapping)

return target
Expand Down
70 changes: 54 additions & 16 deletions chainladder/core/tests/test_triangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -2033,30 +2033,68 @@ def test_set_development_no_development_column() -> None:
assert tri.development[0] == str(tri.origin[-1])


def test_set_development_age_instead_of_date_raises() -> None:
"""
Initialize a triangle with incorrect development periods specified. Should raise a ValueError.
def test_set_development_age_in_months() -> None:
"""Development given as an age in months (not a date) resolves to the
valuation date that many months after the origin's period start."""
df = pd.DataFrame(
{
'origin': [1995, 1996],
'development': [12, 24],
'reported': [1.0, 2.0]
}
)
tri = cl.Triangle(
data=df,
origin='origin',
development='development',
columns='reported',
cumulative=True
)
assert list(tri.development) == [12, 24, 36]
frame = tri.to_frame(origin_as_datetime=False)
assert frame.loc["1995", 12] == 1.0
assert frame.loc["1996", 24] == 2.0

Returns
-------
None

"""
def test_set_development_age_respects_mid_period_origin() -> None:
"""Age is relative to the start of the origin's own period, not the
literal recorded origin date."""
df = pd.DataFrame(
{
'origin': [1995, 1996],
'origin': ['2018-06-15', '2018-06-15'],
'development': [12, 24],
'reported': [100.0, 150.0]
}
)
tri = cl.Triangle(
data=df,
origin='origin',
development='development',
columns='reported',
cumulative=True
)
assert list(tri.development) == [12, 24]


def test_set_development_bare_years_unaffected_by_age_support() -> None:
"""A development column that is genuinely a bare calendar year (e.g. the
literal year 1970) must still parse as a date, not get reinterpreted as
an age."""
df = pd.DataFrame(
{
'origin': [1969, 1970],
'development': [1970, 1970],
'reported': [1.0, 2.0]
}
)
with pytest.raises(ValueError, match="Development lags could not be determined"):
cl.Triangle(
data=df,
origin='origin',
development='development',
columns='reported',
cumulative=True
)
tri = cl.Triangle(
data=df,
origin='origin',
development='development',
columns='reported',
cumulative=True
)
assert list(tri.development) == ["1970"]


def test_input_validation_non_numeric_columns_raises() -> None:
Expand Down
1 change: 1 addition & 0 deletions chainladder/core/triangle.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ def __init__(
development=development,
development_format=development_format,
origin_date=origin_date,
origin_grain=self.origin_grain,
)

if len(development_date.unique()) == 1:
Expand Down
Loading