Skip to content

[BUG] cl.concat(...,axis=2, ignore_index=True) does not follow origin grain #1489

Open

Description

@genedan

Are you on the latest chainladder version?

  • Yes, this bug occurs on the latest version.

Describe the bug in words

When concatenating 2 triangles along the origin index, while setting ignore_index=True, the oldest origin period is set to 1970-01-01T00:00:00.000000000 (Unix epoch) and incremented by 1 nanosecond per period.

This ignores the triangle's original grain (Y, in the example below).

How can the bug be reproduced?

import chainladder as cl


raa = cl.load_sample("raa")
o1 = raa.iloc[:, :, :5, :]
o2 = raa.iloc[:, :, 5:, :]
res2 = cl.concat([o1, o2], axis=2, ignore_index=True)
print(res2)

         12       24       36       48       60       72       84       96       108      120
1970  5012.0   8269.0  10907.0  11805.0  13539.0  16181.0  18009.0  18608.0  18662.0  18834.0
1970   106.0   4285.0   5396.0  10666.0  13782.0  15599.0  15496.0  16169.0  16704.0      NaN
1970  3410.0   8992.0  13873.0  16141.0  18735.0  22214.0  22863.0  23466.0      NaN      NaN
1970  5655.0  11555.0  15766.0  21266.0  23425.0  26083.0  27067.0      NaN      NaN      NaN
1970  1092.0   9565.0  15836.0  22169.0  25955.0  26180.0      NaN      NaN      NaN      NaN
1970  1513.0   6445.0  11702.0  12935.0  15852.0      NaN      NaN      NaN      NaN      NaN
1970   557.0   4020.0  10946.0  12314.0      NaN      NaN      NaN      NaN      NaN      NaN
1970  1351.0   6947.0  13112.0      NaN      NaN      NaN      NaN      NaN      NaN      NaN
1970  3133.0   5395.0      NaN      NaN      NaN      NaN      NaN      NaN      NaN      NaN
1970  2063.0      NaN      NaN      NaN      NaN      NaN      NaN      NaN      NaN      NaN
print(res2.origin_grain)

Y

What is the expected behavior?

Some decisions need to be made, but the goal should be to match the data with the grain.

  • Is the Unix epoch OK as the default starting date?
  • Should the origin grain be changed to match nanoseconds?
  • Or should the default increment be faithful to the original grain?

If we decide to make the increment match the original grain:

         12       24       36       48       60       72       84       96       108      120
1970  5012.0   8269.0  10907.0  11805.0  13539.0  16181.0  18009.0  18608.0  18662.0  18834.0
1971   106.0   4285.0   5396.0  10666.0  13782.0  15599.0  15496.0  16169.0  16704.0      NaN
1972  3410.0   8992.0  13873.0  16141.0  18735.0  22214.0  22863.0  23466.0      NaN      NaN
1973  5655.0  11555.0  15766.0  21266.0  23425.0  26083.0  27067.0      NaN      NaN      NaN
1974  1092.0   9565.0  15836.0  22169.0  25955.0  26180.0      NaN      NaN      NaN      NaN
1975  1513.0   6445.0  11702.0  12935.0  15852.0      NaN      NaN      NaN      NaN      NaN
1976   557.0   4020.0  10946.0  12314.0      NaN      NaN      NaN      NaN      NaN      NaN
1977  1351.0   6947.0  13112.0      NaN      NaN      NaN      NaN      NaN      NaN      NaN
1978  3133.0   5395.0      NaN      NaN      NaN      NaN      NaN      NaN      NaN      NaN
1979  2063.0      NaN      NaN      NaN      NaN      NaN      NaN      NaN      NaN      NaN

Would you be willing to contribute this ticket?

  • Yes, absolutely!
  • Yes, but I would like some help.
  • No.

Activity

  1. added theissue type on Oct 15, 2026
  2. updated Scope Codebase and Priority Medium on Oct 15, 2026
  3. henrydingliu commented on Oct 16, 2026

    @henrydingliu
    Member

    can we first align that we should follow pandas convention wherever it makes sense but also don't blindly force pandas convention everywhere?

    if we can be aligned on the above, then i don't think ignore_index=True should be allowed on axes 2 and 3. your example here is fairly benign, but a gigantic can of worms opens up if o2 = [:, :, 4:, :] instead (i.e. res2 is now a non-triangular polygon that's likely to bug out every estimator we throw it into).

  4. genedan commented on Oct 16, 2026

    @genedan
    MemberAuthor

    can we first align that we should follow pandas convention wherever it makes sense but also don't blindly force pandas convention everywhere?

    Agree. For this example, I think we'd have to take the 2nd route since origin is unique to chainladder. Pandas will discard the dtype and reset the axis to be integers 0, 1, ... I think unless we keep origin as a PeriodIndex, a bunch of actuarial calculations will break.

    i don't think ignore_index=True should be allowed on axes 2 and 3.

    Agree. The design decisions that we'd have to make won't be obvious until somebody proposes a use case. I think we can close this out by adding an exception. Looping in @casact/chainladder-maintainers if anyone has last words.

    I wonder if there is a more serious bug that would happen if a user just overrides the origin/development indexes with whatever values they want. With grain being the same, I would expect things to break.

  5. henrydingliu commented on Oct 16, 2026

    @henrydingliu
    Member

    The design decisions that we'd have to make won't be obvious until somebody proposes a use case

    one use case i can think of: when the claims data cut over (e.g. migrating from one claims system to another) in the middle of an origin period. say we are doing OYDY analysis, and the migration happens on 7/1. and say the new claims system outputs data in a different schema. it is reasonable to want to keep the triangle generation pipeline of the old system separate from a triangle generation pipeline for the new system, and only combine the resulting triangles together. however, for this use case, we do still have __add__ that works perfectly.

    prism = cl.load_sample("prism")["Incurred"]
    o1 = prism.iloc[:, :, :66, :].sum().grain("OYDY")
    o2 = prism.iloc[:, :, 66:, :].sum().grain("OYDY")
    try:
        cl.concat([o1, o2],axis=2)
    except Exception as e:
        print(e)
        print((o1 + o2) - prism.sum().grain("OYDY"))
          12            24   36            48            60   72   84   96   108  120
    2008  NaN           NaN  NaN           NaN           NaN  NaN  NaN  NaN  NaN  NaN
    2009  NaN           NaN  NaN           NaN           NaN  NaN  NaN  NaN  NaN  NaN
    2010  NaN           NaN  NaN           NaN           NaN  NaN  NaN  NaN  NaN  NaN
    2011  NaN           NaN  NaN           NaN           NaN  NaN  NaN  NaN  NaN  NaN
    2012  NaN           NaN  NaN           NaN           NaN  NaN  NaN  NaN  NaN  NaN
    2013  NaN -1.862645e-09  NaN  1.490116e-08  1.164153e-10  NaN  NaN  NaN  NaN  NaN
    2014  NaN           NaN  NaN           NaN           NaN  NaN  NaN  NaN  NaN  NaN
    2015  NaN           NaN  NaN           NaN           NaN  NaN  NaN  NaN  NaN  NaN
    2016  NaN           NaN  NaN           NaN           NaN  NaN  NaN  NaN  NaN  NaN
    2017  NaN           NaN  NaN           NaN           NaN  NaN  NaN  NaN  NaN  NaN

    we should also update the AssertError that ignore_index=False emits. It's not helpful today

  6. added
    Great First Contribution! 🌱Beginner friendly tickets with narrow scope and huge impact. Perfect to get your feet wet!
    and removed on Oct 19, 2026
  7. priyam0k commented on Oct 19, 2026

    @priyam0k
    Contributor

    one use case i can think of: when the claims data cut over (e.g. migrating from one claims system to another) in the middle of an origin period... however, for this use case, we do still have __add__ that works perfectly.

    Because __add__ already handles combining amounts across identical origin and development coordinates, having cl.concat synthesize arbitrary origin or development indices with ignore_index=True provides little practical value and breaks temporal grain invariants. We ran into a similar boundary in #1455 for Styler concatenation: actuarial axes require rigid temporal alignment, and treating them like generic pandas axes quickly creates invalid geometry.

    Disallowing ignore_index=True on axes 2 and 3 cleanly resolves both this and #1490. Replacing the bare assert statements across concat (such as duplicate checks and non-concat axis alignment checks) with descriptive ValueErrors would also make failure modes much clearer. Open to other ideas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Great First Contribution! 🌱Beginner friendly tickets with narrow scope and huge impact. Perfect to get your feet wet!

    Type

    Fields

    Priority

    Medium

    Effort

    Medium

    Scope

    Codebase

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions