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_CHILD_RANK = "child_rank"
|
||||
COL_CHILD_INDEX = "child_index"
|
||||
COL_AMENDMENT_NUM = "AARETE_DERIVED_AMENDMENT_NUM"
|
||||
|
||||
# Crosswalk columns
|
||||
XWALK_COL_FILE_NAME = "File Name"
|
||||
|
||||
@@ -172,7 +172,6 @@ if __name__ == "__main__":
|
||||
)
|
||||
# Generate timestamp for this run
|
||||
run_timestamp = f"run_{datetime.now().strftime('%Y%m%d_%H-%M')}_{config.BATCH_ID}"
|
||||
|
||||
if config.DOCZY_OUTPUT_FOR_PC:
|
||||
results = main(config.DOCZY_OUTPUT_FOR_PC)
|
||||
logging.info(f"PC Mapping complete. Processed {results} files.")
|
||||
|
||||
@@ -57,6 +57,7 @@ from src.constants.parent_child.bcbs import (
|
||||
NON_PARENT_KEYWORDS,
|
||||
DATE_PATTERNS,
|
||||
IRS_GROUP_REPLACEMENTS,
|
||||
COL_AMENDMENT_NUM,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -91,6 +92,7 @@ OUTPUT_COLUMNS = [
|
||||
COL_CONTRACT_EFF_DATE,
|
||||
"File Name",
|
||||
"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
|
||||
subset["File Name"] = subset[COL_CONTRACT_NAME]
|
||||
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
|
||||
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)
|
||||
logger.info(f"Total records: {len(subset)}")
|
||||
logger.info(f"Parents: {subset[COL_IS_PARENT].sum()}")
|
||||
|
||||
@@ -34,6 +34,7 @@ class ColumnMapper:
|
||||
r"^contract.*title$",
|
||||
r"^title$",
|
||||
r"^contract.*name.*title$",
|
||||
r"^contract.*name$",
|
||||
r"^agreement.*title$",
|
||||
r"^doc.*title$",
|
||||
],
|
||||
@@ -51,6 +52,7 @@ class ColumnMapper:
|
||||
r"^provider.*name$",
|
||||
r"^prov.*name$",
|
||||
r"^group.*name$",
|
||||
r"^multiple.*irs.*names$",
|
||||
],
|
||||
"PROV_GROUP_TIN": [
|
||||
r"^prov.*group.*tin$",
|
||||
@@ -105,6 +107,10 @@ class ColumnMapper:
|
||||
r"^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)
|
||||
@@ -113,6 +119,7 @@ class ColumnMapper:
|
||||
"SERVICE_TERM",
|
||||
"PROV_GROUP_TIN",
|
||||
"PROV_GROUP_NPI",
|
||||
"AARETE_DERIVED_AMENDMENT_NUM",
|
||||
]
|
||||
|
||||
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}"
|
||||
|
||||
# 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"):
|
||||
if parent_id == "orphan":
|
||||
base_rank = "0.0"
|
||||
parent_rank_str = "0"
|
||||
else:
|
||||
# Extract parent_rank from the child's 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:
|
||||
try:
|
||||
if isinstance(assigned_rank, float):
|
||||
base_rank = f"{int(assigned_rank)}.0"
|
||||
parent_rank_str = str(int(assigned_rank))
|
||||
else:
|
||||
base_rank = f"{assigned_rank}.0"
|
||||
parent_rank_str = str(assigned_rank)
|
||||
except Exception as e:
|
||||
logging.debug(
|
||||
f"Could not convert rank '{assigned_rank}' to int: {e}"
|
||||
)
|
||||
base_rank = f"{assigned_rank}.0"
|
||||
parent_rank_str = str(assigned_rank)
|
||||
else:
|
||||
base_rank = "0.0"
|
||||
parent_rank_str = "0"
|
||||
|
||||
# Sort by effective date (already converted to datetime at function start)
|
||||
grp_df = grp_df.copy()
|
||||
grp_df["sort_date"] = grp_df["fixed_effective_date"].fillna(pd.Timestamp.max)
|
||||
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):
|
||||
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
|
||||
|
||||
# Clean up temp column
|
||||
@@ -1044,6 +1054,7 @@ def parent_child_mapping(
|
||||
cols_order = [
|
||||
"grouping_key",
|
||||
"PROV_GROUP_NAME_FULL_cleaned",
|
||||
"AARETE_DERIVED_AMENDMENT_NUM",
|
||||
"consolidated_lob",
|
||||
"fixed_effective_date",
|
||||
"parent_child_flag",
|
||||
@@ -1110,6 +1121,7 @@ def parent_child_mapping(
|
||||
"PROV_GROUP_NPI",
|
||||
"payer_name_cleaned",
|
||||
"PAYER_STATE",
|
||||
"AARETE_DERIVED_AMENDMENT_NUM",
|
||||
"grouping_key",
|
||||
"parent",
|
||||
"combined_rank",
|
||||
|
||||
+39
-5
@@ -1793,6 +1793,7 @@ class ConfigFactory:
|
||||
"cnc": ConfigFactory.create_molina, # CNC uses same config as Molina
|
||||
"caresource": ConfigFactory.create_caresource,
|
||||
"bcbs": ConfigFactory.create_bcbs,
|
||||
"bcbsnc": ConfigFactory.create_bcbs,
|
||||
"clover_health": ConfigFactory.create_clover,
|
||||
}
|
||||
|
||||
@@ -2020,6 +2021,20 @@ class ParentChildEngine:
|
||||
parent_cache = df.loc[df["is_parent"], cols_needed].to_dict("index")
|
||||
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:
|
||||
"""Initialize output columns."""
|
||||
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_rank_str = (
|
||||
df.loc[parent_idx, "parent_rank"]
|
||||
.astype("string")
|
||||
.apply(lambda x: str(int(x)) if pd.notna(x) else "0")
|
||||
.reset_index(drop=True)
|
||||
.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[
|
||||
tmp.index, "child_rank_dest"
|
||||
]
|
||||
@@ -2079,9 +2105,17 @@ class ParentChildEngine:
|
||||
tmp = df.loc[orphan_mask, ["input_row_order"]].copy()
|
||||
tmp = tmp.sort_values(["input_row_order"], kind="mergesort")
|
||||
seq = pd.Series(np.arange(1, len(tmp) + 1, dtype=np.int32), index=tmp.index)
|
||||
df.loc[tmp.index, "orphan_rank_dest"] = (
|
||||
"0.0." + seq.astype("string")
|
||||
).values
|
||||
if "AARETE_DERIVED_AMENDMENT_NUM" in df.columns:
|
||||
amendment_str = df.loc[tmp.index, "AARETE_DERIVED_AMENDMENT_NUM"].apply(
|
||||
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[
|
||||
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:
|
||||
"""Returns ONLY dynamic content for validation.
|
||||
Call VALIDATE_REIMBURSEMENTS_INSTRUCTION() separately for the cached instruction.
|
||||
|
||||
@@ -34,6 +34,7 @@ from src.parent_child.qc import (
|
||||
resolve_column,
|
||||
TextProcessor,
|
||||
ConfigFactory,
|
||||
ParentChildEngine,
|
||||
)
|
||||
|
||||
|
||||
@@ -526,7 +527,8 @@ class TestAssignChildRanks(unittest.TestCase):
|
||||
self.assertTrue(result["child_rank"].iloc[1].startswith("0.0"))
|
||||
|
||||
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(
|
||||
{
|
||||
"grouping_key": ["TIN:123", "TIN:123", "TIN:123"],
|
||||
@@ -540,7 +542,7 @@ class TestAssignChildRanks(unittest.TestCase):
|
||||
|
||||
# Parent should have combined_rank "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()
|
||||
self.assertTrue(all(r.startswith("1.0.") for r in child_ranks))
|
||||
|
||||
@@ -637,5 +639,372 @@ class TestConfigFactory(unittest.TestCase):
|
||||
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__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user