Merged in feature/adding_contract_admenment_num_pc (pull request #863)
Feature/adding contract admenment num pc * Updated contract amendment * Format code with Black * updated to have letters * format fixes * Adjusted unit testing * Merge remote-tracking branch 'origin/DEV' into feature/adding_contract_admenment_num_pc * apply formatting
This commit is contained in:
@@ -42,6 +42,7 @@ COL_EFF_DATE_RANK = "effective_date_rank"
|
|||||||
COL_FINAL_RANK = "final_rank"
|
COL_FINAL_RANK = "final_rank"
|
||||||
COL_CHILD_RANK = "child_rank"
|
COL_CHILD_RANK = "child_rank"
|
||||||
COL_CHILD_INDEX = "child_index"
|
COL_CHILD_INDEX = "child_index"
|
||||||
|
COL_AMENDMENT_NUM = "AARETE_DERIVED_AMENDMENT_NUM"
|
||||||
|
|
||||||
# Crosswalk columns
|
# Crosswalk columns
|
||||||
XWALK_COL_FILE_NAME = "File Name"
|
XWALK_COL_FILE_NAME = "File Name"
|
||||||
|
|||||||
@@ -172,7 +172,6 @@ if __name__ == "__main__":
|
|||||||
)
|
)
|
||||||
# Generate timestamp for this run
|
# Generate timestamp for this run
|
||||||
run_timestamp = f"run_{datetime.now().strftime('%Y%m%d_%H-%M')}_{config.BATCH_ID}"
|
run_timestamp = f"run_{datetime.now().strftime('%Y%m%d_%H-%M')}_{config.BATCH_ID}"
|
||||||
|
|
||||||
if config.DOCZY_OUTPUT_FOR_PC:
|
if config.DOCZY_OUTPUT_FOR_PC:
|
||||||
results = main(config.DOCZY_OUTPUT_FOR_PC)
|
results = main(config.DOCZY_OUTPUT_FOR_PC)
|
||||||
logging.info(f"PC Mapping complete. Processed {results} files.")
|
logging.info(f"PC Mapping complete. Processed {results} files.")
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ from src.constants.parent_child.bcbs import (
|
|||||||
NON_PARENT_KEYWORDS,
|
NON_PARENT_KEYWORDS,
|
||||||
DATE_PATTERNS,
|
DATE_PATTERNS,
|
||||||
IRS_GROUP_REPLACEMENTS,
|
IRS_GROUP_REPLACEMENTS,
|
||||||
|
COL_AMENDMENT_NUM,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -91,6 +92,7 @@ OUTPUT_COLUMNS = [
|
|||||||
COL_CONTRACT_EFF_DATE,
|
COL_CONTRACT_EFF_DATE,
|
||||||
"File Name",
|
"File Name",
|
||||||
"Provider Type_Consolidated",
|
"Provider Type_Consolidated",
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM",
|
||||||
]
|
]
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -1025,9 +1027,12 @@ def bcbs_main(df_read, xwalk_path=None):
|
|||||||
# Step 28: Add File Name and Provider Type_Consolidated columns
|
# Step 28: Add File Name and Provider Type_Consolidated columns
|
||||||
subset["File Name"] = subset[COL_CONTRACT_NAME]
|
subset["File Name"] = subset[COL_CONTRACT_NAME]
|
||||||
subset["Provider Type_Consolidated"] = subset[COL_CONSOLIDATED_PROVIDER]
|
subset["Provider Type_Consolidated"] = subset[COL_CONSOLIDATED_PROVIDER]
|
||||||
|
if COL_AMENDMENT_NUM in subset.columns:
|
||||||
|
subset["AARETE_DERIVED_AMENDMENT_NUM"] = subset[COL_AMENDMENT_NUM]
|
||||||
|
|
||||||
# Step 29: Select output columns and export
|
# Step 29: Select output columns and export
|
||||||
subset = subset[OUTPUT_COLUMNS]
|
output_cols = [col for col in OUTPUT_COLUMNS if col in subset.columns]
|
||||||
|
subset = subset[output_cols]
|
||||||
subset = subset.sort_values(COL_FOLDER)
|
subset = subset.sort_values(COL_FOLDER)
|
||||||
logger.info(f"Total records: {len(subset)}")
|
logger.info(f"Total records: {len(subset)}")
|
||||||
logger.info(f"Parents: {subset[COL_IS_PARENT].sum()}")
|
logger.info(f"Parents: {subset[COL_IS_PARENT].sum()}")
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class ColumnMapper:
|
|||||||
r"^contract.*title$",
|
r"^contract.*title$",
|
||||||
r"^title$",
|
r"^title$",
|
||||||
r"^contract.*name.*title$",
|
r"^contract.*name.*title$",
|
||||||
|
r"^contract.*name$",
|
||||||
r"^agreement.*title$",
|
r"^agreement.*title$",
|
||||||
r"^doc.*title$",
|
r"^doc.*title$",
|
||||||
],
|
],
|
||||||
@@ -51,6 +52,7 @@ class ColumnMapper:
|
|||||||
r"^provider.*name$",
|
r"^provider.*name$",
|
||||||
r"^prov.*name$",
|
r"^prov.*name$",
|
||||||
r"^group.*name$",
|
r"^group.*name$",
|
||||||
|
r"^multiple.*irs.*names$",
|
||||||
],
|
],
|
||||||
"PROV_GROUP_TIN": [
|
"PROV_GROUP_TIN": [
|
||||||
r"^prov.*group.*tin$",
|
r"^prov.*group.*tin$",
|
||||||
@@ -105,6 +107,10 @@ class ColumnMapper:
|
|||||||
r"^plan.*state$",
|
r"^plan.*state$",
|
||||||
r"^health.*plan.*state$",
|
r"^health.*plan.*state$",
|
||||||
],
|
],
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": [
|
||||||
|
r"aarete_derived_amendment_num",
|
||||||
|
r"^amendment.*number$",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Optional columns (won't fail if not found)
|
# Optional columns (won't fail if not found)
|
||||||
@@ -113,6 +119,7 @@ class ColumnMapper:
|
|||||||
"SERVICE_TERM",
|
"SERVICE_TERM",
|
||||||
"PROV_GROUP_TIN",
|
"PROV_GROUP_TIN",
|
||||||
"PROV_GROUP_NPI",
|
"PROV_GROUP_NPI",
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, df: pd.DataFrame):
|
def __init__(self, df: pd.DataFrame):
|
||||||
|
|||||||
@@ -771,35 +771,45 @@ def assign_child_ranks(df, grouper_col="grouping_key"):
|
|||||||
children.at[idx, "_parent_identity"] = f"unknown_{assigned_rank}"
|
children.at[idx, "_parent_identity"] = f"unknown_{assigned_rank}"
|
||||||
|
|
||||||
# Group by parent identity and assign sequential ranks
|
# Group by parent identity and assign sequential ranks
|
||||||
|
has_amendment_col = "AARETE_DERIVED_AMENDMENT_NUM" in children.columns
|
||||||
|
|
||||||
for parent_id, grp_df in children.groupby("_parent_identity"):
|
for parent_id, grp_df in children.groupby("_parent_identity"):
|
||||||
if parent_id == "orphan":
|
if parent_id == "orphan":
|
||||||
base_rank = "0.0"
|
parent_rank_str = "0"
|
||||||
else:
|
else:
|
||||||
# Extract parent_rank from the child's assigned_parent_rank
|
# Extract parent_rank from the child's assigned_parent_rank
|
||||||
assigned_rank = grp_df.iloc[0]["assigned_parent_rank"]
|
assigned_rank = grp_df.iloc[0]["assigned_parent_rank"]
|
||||||
# Convert to int if it's a float (2.0 -> 2), then add .0
|
# Convert to int if it's a float (2.0 -> 2)
|
||||||
if pd.notna(assigned_rank) and assigned_rank != ASSIGNMENT_NO_PARENT:
|
if pd.notna(assigned_rank) and assigned_rank != ASSIGNMENT_NO_PARENT:
|
||||||
try:
|
try:
|
||||||
if isinstance(assigned_rank, float):
|
if isinstance(assigned_rank, float):
|
||||||
base_rank = f"{int(assigned_rank)}.0"
|
parent_rank_str = str(int(assigned_rank))
|
||||||
else:
|
else:
|
||||||
base_rank = f"{assigned_rank}.0"
|
parent_rank_str = str(assigned_rank)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"Could not convert rank '{assigned_rank}' to int: {e}"
|
f"Could not convert rank '{assigned_rank}' to int: {e}"
|
||||||
)
|
)
|
||||||
base_rank = f"{assigned_rank}.0"
|
parent_rank_str = str(assigned_rank)
|
||||||
else:
|
else:
|
||||||
base_rank = "0.0"
|
parent_rank_str = "0"
|
||||||
|
|
||||||
# Sort by effective date (already converted to datetime at function start)
|
# Sort by effective date (already converted to datetime at function start)
|
||||||
grp_df = grp_df.copy()
|
grp_df = grp_df.copy()
|
||||||
grp_df["sort_date"] = grp_df["fixed_effective_date"].fillna(pd.Timestamp.max)
|
grp_df["sort_date"] = grp_df["fixed_effective_date"].fillna(pd.Timestamp.max)
|
||||||
grp_df = grp_df.sort_values("sort_date")
|
grp_df = grp_df.sort_values("sort_date")
|
||||||
|
|
||||||
# Assign sequential ranks
|
# Assign sequential ranks using amendment number instead of 0
|
||||||
for i, idx in enumerate(grp_df.index, start=1):
|
for i, idx in enumerate(grp_df.index, start=1):
|
||||||
rank_val = f"{base_rank}.{i}"
|
amendment_val = 0
|
||||||
|
if has_amendment_col:
|
||||||
|
raw = grp_df.at[idx, "AARETE_DERIVED_AMENDMENT_NUM"]
|
||||||
|
if pd.notna(raw) and str(raw).strip() != "":
|
||||||
|
try:
|
||||||
|
amendment_val = int(float(raw))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
amendment_val = raw
|
||||||
|
rank_val = f"{parent_rank_str}.{amendment_val}.{i}"
|
||||||
children.loc[idx, "child_rank"] = rank_val
|
children.loc[idx, "child_rank"] = rank_val
|
||||||
|
|
||||||
# Clean up temp column
|
# Clean up temp column
|
||||||
@@ -1044,6 +1054,7 @@ def parent_child_mapping(
|
|||||||
cols_order = [
|
cols_order = [
|
||||||
"grouping_key",
|
"grouping_key",
|
||||||
"PROV_GROUP_NAME_FULL_cleaned",
|
"PROV_GROUP_NAME_FULL_cleaned",
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM",
|
||||||
"consolidated_lob",
|
"consolidated_lob",
|
||||||
"fixed_effective_date",
|
"fixed_effective_date",
|
||||||
"parent_child_flag",
|
"parent_child_flag",
|
||||||
@@ -1110,6 +1121,7 @@ def parent_child_mapping(
|
|||||||
"PROV_GROUP_NPI",
|
"PROV_GROUP_NPI",
|
||||||
"payer_name_cleaned",
|
"payer_name_cleaned",
|
||||||
"PAYER_STATE",
|
"PAYER_STATE",
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM",
|
||||||
"grouping_key",
|
"grouping_key",
|
||||||
"parent",
|
"parent",
|
||||||
"combined_rank",
|
"combined_rank",
|
||||||
|
|||||||
+39
-5
@@ -1793,6 +1793,7 @@ class ConfigFactory:
|
|||||||
"cnc": ConfigFactory.create_molina, # CNC uses same config as Molina
|
"cnc": ConfigFactory.create_molina, # CNC uses same config as Molina
|
||||||
"caresource": ConfigFactory.create_caresource,
|
"caresource": ConfigFactory.create_caresource,
|
||||||
"bcbs": ConfigFactory.create_bcbs,
|
"bcbs": ConfigFactory.create_bcbs,
|
||||||
|
"bcbsnc": ConfigFactory.create_bcbs,
|
||||||
"clover_health": ConfigFactory.create_clover,
|
"clover_health": ConfigFactory.create_clover,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2020,6 +2021,20 @@ class ParentChildEngine:
|
|||||||
parent_cache = df.loc[df["is_parent"], cols_needed].to_dict("index")
|
parent_cache = df.loc[df["is_parent"], cols_needed].to_dict("index")
|
||||||
return self.child_assigner.assign_children(df, self.cfg, parent_cache)
|
return self.child_assigner.assign_children(df, self.cfg, parent_cache)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_amendment_val(x):
|
||||||
|
"""Convert amendment value to string for rank construction.
|
||||||
|
|
||||||
|
Handles both numeric values (e.g. 3.0 -> '3') and character
|
||||||
|
values (e.g. 'A' -> 'A'). Returns '0' for NaN / empty.
|
||||||
|
"""
|
||||||
|
if pd.notna(x) and str(x).strip() != "":
|
||||||
|
try:
|
||||||
|
return str(int(float(x)))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return str(x).strip()
|
||||||
|
return "0"
|
||||||
|
|
||||||
def prepare_output(self, df: pd.DataFrame) -> pd.DataFrame:
|
def prepare_output(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||||
"""Initialize output columns."""
|
"""Initialize output columns."""
|
||||||
df["parent_child_flag_dest"] = pd.Series([pd.NA] * len(df), dtype="string")
|
df["parent_child_flag_dest"] = pd.Series([pd.NA] * len(df), dtype="string")
|
||||||
@@ -2060,12 +2075,23 @@ class ParentChildEngine:
|
|||||||
parent_idx = tmp["assigned_parent_idx"].astype("int64").values
|
parent_idx = tmp["assigned_parent_idx"].astype("int64").values
|
||||||
parent_rank_str = (
|
parent_rank_str = (
|
||||||
df.loc[parent_idx, "parent_rank"]
|
df.loc[parent_idx, "parent_rank"]
|
||||||
.astype("string")
|
.apply(lambda x: str(int(x)) if pd.notna(x) else "0")
|
||||||
.reset_index(drop=True)
|
.reset_index(drop=True)
|
||||||
.values
|
.values
|
||||||
)
|
)
|
||||||
|
|
||||||
df.loc[tmp.index, "child_rank_dest"] = parent_rank_str + ".0." + seq_str
|
if "AARETE_DERIVED_AMENDMENT_NUM" in df.columns:
|
||||||
|
amendment_str = (
|
||||||
|
df.loc[tmp.index, "AARETE_DERIVED_AMENDMENT_NUM"]
|
||||||
|
.apply(self._normalize_amendment_val)
|
||||||
|
.reset_index(drop=True)
|
||||||
|
.values
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
amendment_str = "0"
|
||||||
|
df.loc[tmp.index, "child_rank_dest"] = (
|
||||||
|
parent_rank_str + "." + amendment_str + "." + seq_str
|
||||||
|
)
|
||||||
df.loc[tmp.index, "combined_rank_dest"] = df.loc[
|
df.loc[tmp.index, "combined_rank_dest"] = df.loc[
|
||||||
tmp.index, "child_rank_dest"
|
tmp.index, "child_rank_dest"
|
||||||
]
|
]
|
||||||
@@ -2079,9 +2105,17 @@ class ParentChildEngine:
|
|||||||
tmp = df.loc[orphan_mask, ["input_row_order"]].copy()
|
tmp = df.loc[orphan_mask, ["input_row_order"]].copy()
|
||||||
tmp = tmp.sort_values(["input_row_order"], kind="mergesort")
|
tmp = tmp.sort_values(["input_row_order"], kind="mergesort")
|
||||||
seq = pd.Series(np.arange(1, len(tmp) + 1, dtype=np.int32), index=tmp.index)
|
seq = pd.Series(np.arange(1, len(tmp) + 1, dtype=np.int32), index=tmp.index)
|
||||||
df.loc[tmp.index, "orphan_rank_dest"] = (
|
if "AARETE_DERIVED_AMENDMENT_NUM" in df.columns:
|
||||||
"0.0." + seq.astype("string")
|
amendment_str = df.loc[tmp.index, "AARETE_DERIVED_AMENDMENT_NUM"].apply(
|
||||||
).values
|
self._normalize_amendment_val
|
||||||
|
)
|
||||||
|
df.loc[tmp.index, "orphan_rank_dest"] = (
|
||||||
|
"0." + amendment_str + "." + seq.astype("string")
|
||||||
|
).values
|
||||||
|
else:
|
||||||
|
df.loc[tmp.index, "orphan_rank_dest"] = (
|
||||||
|
"0.0." + seq.astype("string")
|
||||||
|
).values
|
||||||
df.loc[tmp.index, "combined_rank_dest"] = df.loc[
|
df.loc[tmp.index, "combined_rank_dest"] = df.loc[
|
||||||
tmp.index, "orphan_rank_dest"
|
tmp.index, "orphan_rank_dest"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1552,6 +1552,7 @@ Return NO if EITHER term contains (even if valid payment method exists):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def VALIDATE_REIMBURSEMENTS_PROMPT(service_term: str, reimb_term: str) -> str:
|
def VALIDATE_REIMBURSEMENTS_PROMPT(service_term: str, reimb_term: str) -> str:
|
||||||
"""Returns ONLY dynamic content for validation.
|
"""Returns ONLY dynamic content for validation.
|
||||||
Call VALIDATE_REIMBURSEMENTS_INSTRUCTION() separately for the cached instruction.
|
Call VALIDATE_REIMBURSEMENTS_INSTRUCTION() separately for the cached instruction.
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ from src.parent_child.qc import (
|
|||||||
resolve_column,
|
resolve_column,
|
||||||
TextProcessor,
|
TextProcessor,
|
||||||
ConfigFactory,
|
ConfigFactory,
|
||||||
|
ParentChildEngine,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -526,7 +527,8 @@ class TestAssignChildRanks(unittest.TestCase):
|
|||||||
self.assertTrue(result["child_rank"].iloc[1].startswith("0.0"))
|
self.assertTrue(result["child_rank"].iloc[1].startswith("0.0"))
|
||||||
|
|
||||||
def test_assigned_children_get_parent_prefix(self):
|
def test_assigned_children_get_parent_prefix(self):
|
||||||
"""Children assigned to parent get ranks with parent's rank prefix."""
|
"""Children assigned to parent get ranks with parent's rank prefix.
|
||||||
|
Without AARETE_DERIVED_AMENDMENT_NUM column, amendment defaults to 0."""
|
||||||
df = pd.DataFrame(
|
df = pd.DataFrame(
|
||||||
{
|
{
|
||||||
"grouping_key": ["TIN:123", "TIN:123", "TIN:123"],
|
"grouping_key": ["TIN:123", "TIN:123", "TIN:123"],
|
||||||
@@ -540,7 +542,7 @@ class TestAssignChildRanks(unittest.TestCase):
|
|||||||
|
|
||||||
# Parent should have combined_rank "1"
|
# Parent should have combined_rank "1"
|
||||||
self.assertEqual(result["combined_rank"].iloc[0], "1")
|
self.assertEqual(result["combined_rank"].iloc[0], "1")
|
||||||
# Children should have ranks like "1.0.1", "1.0.2"
|
# Children should have ranks like "1.0.1", "1.0.2" (0 = default amendment)
|
||||||
child_ranks = result[result["parent"] == False]["child_rank"].tolist()
|
child_ranks = result[result["parent"] == False]["child_rank"].tolist()
|
||||||
self.assertTrue(all(r.startswith("1.0.") for r in child_ranks))
|
self.assertTrue(all(r.startswith("1.0.") for r in child_ranks))
|
||||||
|
|
||||||
@@ -637,5 +639,372 @@ class TestConfigFactory(unittest.TestCase):
|
|||||||
self.assertIsNotNone(config)
|
self.assertIsNotNone(config)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAssignChildRanksWithAmendmentNum(unittest.TestCase):
|
||||||
|
"""Tests for assign_child_ranks with AARETE_DERIVED_AMENDMENT_NUM column."""
|
||||||
|
|
||||||
|
def test_children_use_amendment_num_in_rank(self):
|
||||||
|
"""Children with amendment number get parent_rank.amendment_num.seq format."""
|
||||||
|
df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"grouping_key": ["TIN:123", "TIN:123", "TIN:123"],
|
||||||
|
"parent": [True, False, False],
|
||||||
|
"parent_rank": [1.0, np.nan, np.nan],
|
||||||
|
"assigned_parent_rank": [np.nan, 1.0, 1.0],
|
||||||
|
"fixed_effective_date": ["2024-01-01", "2024-03-01", "2024-02-01"],
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 5, 3],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = assign_child_ranks(df)
|
||||||
|
|
||||||
|
child_df = result[result["parent"] == False].sort_values("fixed_effective_date")
|
||||||
|
ranks = child_df["child_rank"].tolist()
|
||||||
|
# Earlier date (index 2, amendment=3) sorted first: 1.3.1
|
||||||
|
# Later date (index 1, amendment=5) sorted second: 1.5.2
|
||||||
|
self.assertEqual(ranks[0], "1.3.1")
|
||||||
|
self.assertEqual(ranks[1], "1.5.2")
|
||||||
|
|
||||||
|
def test_children_with_nan_amendment_default_to_0(self):
|
||||||
|
"""Children with NaN/empty amendment number default to 0."""
|
||||||
|
df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"grouping_key": ["TIN:123", "TIN:123", "TIN:123"],
|
||||||
|
"parent": [True, False, False],
|
||||||
|
"parent_rank": [1.0, np.nan, np.nan],
|
||||||
|
"assigned_parent_rank": [np.nan, 1.0, 1.0],
|
||||||
|
"fixed_effective_date": ["2024-01-01", "2024-03-01", "2024-02-01"],
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": [np.nan, np.nan, ""],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = assign_child_ranks(df)
|
||||||
|
|
||||||
|
child_df = result[result["parent"] == False].sort_values("fixed_effective_date")
|
||||||
|
ranks = child_df["child_rank"].tolist()
|
||||||
|
# Both should default to 0 for amendment part
|
||||||
|
self.assertEqual(ranks[0], "1.0.1")
|
||||||
|
self.assertEqual(ranks[1], "1.0.2")
|
||||||
|
|
||||||
|
def test_orphans_use_amendment_num_in_rank(self):
|
||||||
|
"""Orphans with amendment number get 0.amendment_num.seq format."""
|
||||||
|
df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"grouping_key": ["TIN:123", "TIN:123"],
|
||||||
|
"parent": [False, False],
|
||||||
|
"assigned_parent_rank": ["no_parent", "no_parent"],
|
||||||
|
"fixed_effective_date": ["2024-01-01", "2024-02-01"],
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": [2, 7],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = assign_child_ranks(df)
|
||||||
|
|
||||||
|
ranks = result["child_rank"].tolist()
|
||||||
|
self.assertEqual(ranks[0], "0.2.1")
|
||||||
|
self.assertEqual(ranks[1], "0.7.2")
|
||||||
|
|
||||||
|
def test_mixed_amendment_values_in_same_group(self):
|
||||||
|
"""Different amendment numbers within same parent group."""
|
||||||
|
df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"grouping_key": ["TIN:123", "TIN:123", "TIN:123", "TIN:123"],
|
||||||
|
"parent": [True, False, False, False],
|
||||||
|
"parent_rank": [1.0, np.nan, np.nan, np.nan],
|
||||||
|
"assigned_parent_rank": [np.nan, 1.0, 1.0, 1.0],
|
||||||
|
"fixed_effective_date": [
|
||||||
|
"2024-01-01",
|
||||||
|
"2024-02-01",
|
||||||
|
"2024-03-01",
|
||||||
|
"2024-04-01",
|
||||||
|
],
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 3, np.nan, 5],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = assign_child_ranks(df)
|
||||||
|
|
||||||
|
child_df = result[result["parent"] == False].sort_values("fixed_effective_date")
|
||||||
|
ranks = child_df["child_rank"].tolist()
|
||||||
|
self.assertEqual(ranks[0], "1.3.1") # amendment=3
|
||||||
|
self.assertEqual(ranks[1], "1.0.2") # amendment=NaN -> 0
|
||||||
|
self.assertEqual(ranks[2], "1.5.3") # amendment=5
|
||||||
|
|
||||||
|
def test_no_amendment_column_backward_compatible(self):
|
||||||
|
"""Without AARETE_DERIVED_AMENDMENT_NUM column, uses 0 (backward compat)."""
|
||||||
|
df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"grouping_key": ["TIN:123", "TIN:123"],
|
||||||
|
"parent": [True, False],
|
||||||
|
"parent_rank": [1.0, np.nan],
|
||||||
|
"assigned_parent_rank": [np.nan, 1.0],
|
||||||
|
"fixed_effective_date": ["2024-01-01", "2024-03-01"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = assign_child_ranks(df)
|
||||||
|
|
||||||
|
child_rank = result[result["parent"] == False]["child_rank"].iloc[0]
|
||||||
|
self.assertEqual(child_rank, "1.0.1")
|
||||||
|
|
||||||
|
def test_float_amendment_num_converted_to_int(self):
|
||||||
|
"""Float amendment numbers like 3.0 are converted to int 3."""
|
||||||
|
df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"grouping_key": ["TIN:123", "TIN:123"],
|
||||||
|
"parent": [True, False],
|
||||||
|
"parent_rank": [1.0, np.nan],
|
||||||
|
"assigned_parent_rank": [np.nan, 1.0],
|
||||||
|
"fixed_effective_date": ["2024-01-01", "2024-03-01"],
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 3.0],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = assign_child_ranks(df)
|
||||||
|
|
||||||
|
child_rank = result[result["parent"] == False]["child_rank"].iloc[0]
|
||||||
|
self.assertEqual(child_rank, "1.3.1")
|
||||||
|
|
||||||
|
def test_parent_combined_rank_unchanged(self):
|
||||||
|
"""Parent combined_rank should still be just the parent rank number."""
|
||||||
|
df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"grouping_key": ["TIN:123", "TIN:123"],
|
||||||
|
"parent": [True, False],
|
||||||
|
"parent_rank": [1.0, np.nan],
|
||||||
|
"assigned_parent_rank": [np.nan, 1.0],
|
||||||
|
"fixed_effective_date": ["2024-01-01", "2024-03-01"],
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 5],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = assign_child_ranks(df)
|
||||||
|
|
||||||
|
parent_rank = result[result["parent"] == True]["combined_rank"].iloc[0]
|
||||||
|
self.assertEqual(parent_rank, "1")
|
||||||
|
|
||||||
|
def test_children_use_character_amendment_num_in_rank(self):
|
||||||
|
"""Children with character amendment number (A, B, C) use it in rank."""
|
||||||
|
df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"grouping_key": ["TIN:123", "TIN:123", "TIN:123"],
|
||||||
|
"parent": [True, False, False],
|
||||||
|
"parent_rank": [1.0, np.nan, np.nan],
|
||||||
|
"assigned_parent_rank": [np.nan, 1.0, 1.0],
|
||||||
|
"fixed_effective_date": ["2024-01-01", "2024-03-01", "2024-02-01"],
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": [np.nan, "B", "A"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = assign_child_ranks(df)
|
||||||
|
|
||||||
|
child_df = result[result["parent"] == False].sort_values("fixed_effective_date")
|
||||||
|
ranks = child_df["child_rank"].tolist()
|
||||||
|
self.assertEqual(ranks[0], "1.A.1") # Amendment A, sequence 1
|
||||||
|
self.assertEqual(ranks[1], "1.B.2") # Amendment B, sequence 2
|
||||||
|
|
||||||
|
def test_orphans_use_character_amendment_num_in_rank(self):
|
||||||
|
"""Orphans with character amendment number get 0.amendment.seq format."""
|
||||||
|
df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"grouping_key": ["TIN:123", "TIN:123"],
|
||||||
|
"parent": [False, False],
|
||||||
|
"assigned_parent_rank": ["no_parent", "no_parent"],
|
||||||
|
"fixed_effective_date": ["2024-01-01", "2024-02-01"],
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": ["A", "C"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = assign_child_ranks(df)
|
||||||
|
|
||||||
|
ranks = result["child_rank"].tolist()
|
||||||
|
self.assertEqual(ranks[0], "0.A.1")
|
||||||
|
self.assertEqual(ranks[1], "0.C.2")
|
||||||
|
|
||||||
|
def test_mixed_numeric_and_character_amendment_nums(self):
|
||||||
|
"""Mix of numeric and character amendment numbers in same group."""
|
||||||
|
df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"grouping_key": ["TIN:123", "TIN:123", "TIN:123", "TIN:123"],
|
||||||
|
"parent": [True, False, False, False],
|
||||||
|
"parent_rank": [1.0, np.nan, np.nan, np.nan],
|
||||||
|
"assigned_parent_rank": [np.nan, 1.0, 1.0, 1.0],
|
||||||
|
"fixed_effective_date": [
|
||||||
|
"2024-01-01",
|
||||||
|
"2024-02-01",
|
||||||
|
"2024-03-01",
|
||||||
|
"2024-04-01",
|
||||||
|
],
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 3, "A", 5],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = assign_child_ranks(df)
|
||||||
|
|
||||||
|
child_df = result[result["parent"] == False].sort_values("fixed_effective_date")
|
||||||
|
ranks = child_df["child_rank"].tolist()
|
||||||
|
self.assertEqual(ranks[0], "1.3.1") # numeric 3
|
||||||
|
self.assertEqual(ranks[1], "1.A.2") # character A
|
||||||
|
self.assertEqual(ranks[2], "1.5.3") # numeric 5
|
||||||
|
|
||||||
|
|
||||||
|
class TestComputeRanksWithAmendmentNum(unittest.TestCase):
|
||||||
|
"""Tests for ParentChildEngine.compute_ranks with AARETE_DERIVED_AMENDMENT_NUM."""
|
||||||
|
|
||||||
|
def _make_engine(self):
|
||||||
|
"""Create a minimal ParentChildEngine for testing compute_ranks."""
|
||||||
|
config = ConfigFactory.get_config("molina")
|
||||||
|
return ParentChildEngine(config.to_dict())
|
||||||
|
|
||||||
|
def _make_base_df(self, extra_cols=None):
|
||||||
|
"""Create a base DataFrame with parent, child, and orphan rows.
|
||||||
|
|
||||||
|
Index 0: Parent (parent_rank=1)
|
||||||
|
Index 1: Child assigned to parent 0
|
||||||
|
Index 2: Child assigned to parent 0
|
||||||
|
Index 3: Orphan
|
||||||
|
"""
|
||||||
|
df = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"is_parent": [True, False, False, False],
|
||||||
|
"parent_rank": [1, np.nan, np.nan, np.nan],
|
||||||
|
"assigned_parent_idx": [pd.NA, 0, 0, pd.NA],
|
||||||
|
"eff_date": pd.to_datetime(
|
||||||
|
["2024-01-01", "2024-03-01", "2024-02-01", "2024-04-01"]
|
||||||
|
),
|
||||||
|
"input_row_order": [0, 1, 2, 3],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
df["assigned_parent_idx"] = df["assigned_parent_idx"].astype("Int64")
|
||||||
|
if extra_cols:
|
||||||
|
for col, vals in extra_cols.items():
|
||||||
|
df[col] = vals
|
||||||
|
|
||||||
|
# Initialize output columns
|
||||||
|
df["parent_child_flag_dest"] = pd.Series([pd.NA] * len(df), dtype="string")
|
||||||
|
df["parent_rank_dest"] = pd.Series([pd.NA] * len(df), dtype="string")
|
||||||
|
df["child_rank_dest"] = pd.Series([pd.NA] * len(df), dtype="string")
|
||||||
|
df["orphan_rank_dest"] = pd.Series([pd.NA] * len(df), dtype="string")
|
||||||
|
df["combined_rank_dest"] = pd.Series([pd.NA] * len(df), dtype="string")
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def test_child_rank_dest_with_amendment_num(self):
|
||||||
|
"""child_rank_dest uses amendment number instead of 0."""
|
||||||
|
engine = self._make_engine()
|
||||||
|
df = self._make_base_df(
|
||||||
|
extra_cols={
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 5, 3, 2],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = engine.compute_ranks(df)
|
||||||
|
|
||||||
|
# Children sorted by (assigned_parent_idx, eff_date, input_row_order)
|
||||||
|
# Index 2 (eff 2024-02-01, amendment=3) comes first -> seq 1
|
||||||
|
# Index 1 (eff 2024-03-01, amendment=5) comes second -> seq 2
|
||||||
|
self.assertEqual(result.at[2, "child_rank_dest"], "1.3.1")
|
||||||
|
self.assertEqual(result.at[1, "child_rank_dest"], "1.5.2")
|
||||||
|
|
||||||
|
def test_child_rank_dest_without_amendment_column(self):
|
||||||
|
"""child_rank_dest defaults to 0 when column is missing.
|
||||||
|
Format: parent_rank.0.seq (backward compatible)."""
|
||||||
|
engine = self._make_engine()
|
||||||
|
df = self._make_base_df() # No amendment column
|
||||||
|
result = engine.compute_ranks(df)
|
||||||
|
|
||||||
|
self.assertEqual(result.at[2, "child_rank_dest"], "1.0.1")
|
||||||
|
self.assertEqual(result.at[1, "child_rank_dest"], "1.0.2")
|
||||||
|
|
||||||
|
def test_child_rank_dest_with_nan_amendment(self):
|
||||||
|
"""child_rank_dest defaults to 0 for NaN amendment values."""
|
||||||
|
engine = self._make_engine()
|
||||||
|
df = self._make_base_df(
|
||||||
|
extra_cols={
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": [np.nan, np.nan, np.nan, np.nan],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = engine.compute_ranks(df)
|
||||||
|
|
||||||
|
# NaN amendment defaults to 0, so same as without the column
|
||||||
|
self.assertEqual(result.at[2, "child_rank_dest"], "1.0.1")
|
||||||
|
self.assertEqual(result.at[1, "child_rank_dest"], "1.0.2")
|
||||||
|
|
||||||
|
def test_orphan_rank_dest_with_amendment_num(self):
|
||||||
|
"""orphan_rank_dest uses amendment number instead of 0."""
|
||||||
|
engine = self._make_engine()
|
||||||
|
df = self._make_base_df(
|
||||||
|
extra_cols={
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 5, 3, 7],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = engine.compute_ranks(df)
|
||||||
|
|
||||||
|
# Index 3 is orphan with amendment=7
|
||||||
|
self.assertEqual(result.at[3, "orphan_rank_dest"], "0.7.1")
|
||||||
|
|
||||||
|
def test_orphan_rank_dest_without_amendment_column(self):
|
||||||
|
"""orphan_rank_dest defaults to 0 when column is missing."""
|
||||||
|
engine = self._make_engine()
|
||||||
|
df = self._make_base_df() # No amendment column
|
||||||
|
result = engine.compute_ranks(df)
|
||||||
|
|
||||||
|
self.assertEqual(result.at[3, "orphan_rank_dest"], "0.0.1")
|
||||||
|
|
||||||
|
def test_combined_rank_dest_matches_child_and_orphan(self):
|
||||||
|
"""combined_rank_dest equals child_rank_dest for children and orphan_rank_dest for orphans."""
|
||||||
|
engine = self._make_engine()
|
||||||
|
df = self._make_base_df(
|
||||||
|
extra_cols={
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 5, 3, 7],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = engine.compute_ranks(df)
|
||||||
|
|
||||||
|
# Children: combined_rank_dest == child_rank_dest
|
||||||
|
self.assertEqual(
|
||||||
|
result.at[1, "combined_rank_dest"], result.at[1, "child_rank_dest"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
result.at[2, "combined_rank_dest"], result.at[2, "child_rank_dest"]
|
||||||
|
)
|
||||||
|
# Orphan: combined_rank_dest == orphan_rank_dest
|
||||||
|
self.assertEqual(
|
||||||
|
result.at[3, "combined_rank_dest"], result.at[3, "orphan_rank_dest"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_child_rank_dest_with_character_amendment_num(self):
|
||||||
|
"""child_rank_dest handles character amendment values (A, B, C)."""
|
||||||
|
engine = self._make_engine()
|
||||||
|
df = self._make_base_df(
|
||||||
|
extra_cols={
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": [np.nan, "B", "A", "C"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = engine.compute_ranks(df)
|
||||||
|
|
||||||
|
# Index 2 (eff 2024-02-01, amendment=A) comes first -> seq 1
|
||||||
|
# Index 1 (eff 2024-03-01, amendment=B) comes second -> seq 2
|
||||||
|
self.assertEqual(result.at[2, "child_rank_dest"], "1.A.1")
|
||||||
|
self.assertEqual(result.at[1, "child_rank_dest"], "1.B.2")
|
||||||
|
|
||||||
|
def test_orphan_rank_dest_with_character_amendment_num(self):
|
||||||
|
"""orphan_rank_dest handles character amendment values."""
|
||||||
|
engine = self._make_engine()
|
||||||
|
df = self._make_base_df(
|
||||||
|
extra_cols={
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": [np.nan, 5, 3, "A"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = engine.compute_ranks(df)
|
||||||
|
|
||||||
|
self.assertEqual(result.at[3, "orphan_rank_dest"], "0.A.1")
|
||||||
|
|
||||||
|
def test_child_rank_dest_with_mixed_numeric_and_character_amendment(self):
|
||||||
|
"""child_rank_dest handles mix of numeric and character amendment values."""
|
||||||
|
engine = self._make_engine()
|
||||||
|
df = self._make_base_df(
|
||||||
|
extra_cols={
|
||||||
|
"AARETE_DERIVED_AMENDMENT_NUM": [np.nan, "A", 3, "B"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = engine.compute_ranks(df)
|
||||||
|
|
||||||
|
# Index 2 (eff 2024-02-01, amendment=3) -> seq 1
|
||||||
|
# Index 1 (eff 2024-03-01, amendment=A) -> seq 2
|
||||||
|
self.assertEqual(result.at[2, "child_rank_dest"], "1.3.1")
|
||||||
|
self.assertEqual(result.at[1, "child_rank_dest"], "1.A.2")
|
||||||
|
# Index 3 is orphan with amendment=B
|
||||||
|
self.assertEqual(result.at[3, "orphan_rank_dest"], "0.B.1")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user