Files
doczyai-pipelines/src/pipelines/shared/postprocessing/postprocessing_funcs.py
T

850 lines
28 KiB
Python
Raw Normal View History

import ast
import hashlib
import logging
import os
import re
from datetime import datetime
import pandas as pd
from src.utils import string_utils
# Determine the base directory for the project
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Path to the mappings folder
MAPPINGS_DIR = os.path.join(BASE_DIR, "crosswalk", "mappings")
def rename_columns(df):
"""
Use the provided mapping to rename columns in the data
Args:
df (pd.DataFrame): The input DataFrame with columns to be renamed.
Returns:
pd.DataFrame: The DataFrame with renamed columns.
"""
# Check if the DataFrame is empty, return it as is if it is
if df.empty:
return df
# If not empty, proceed with column renaming
rename_map = {
"PROCEDURE_CD": "CPT4_PROC_CD",
"PROCEDURE_CD_DESC": "CPT4_PROC_CD_DESC",
}
# Rename columns using the mapping
df = df.rename(columns=rename_map)
return df
def normalize_indicator_field(value: str) -> str:
"""
Normalize the indicator field value to 'Y' or 'N'.
Args:
value (str): The input value from an _IND field.
Returns:
str: 'Y' if the value is 'Y', otherwise 'N'.
"""
if isinstance(value, str) and value.strip().upper() == "Y":
return "Y"
return "N"
def format_rate_fields_with_commas(value: str) -> str:
"""
Format the rate with commas and two decimal places.
Args:
value (str): A string representing a numeric value.
Returns:
str: The formatted rate string.
"""
if string_utils.is_empty(value):
return ""
try:
numeric_value = float(value)
return f"{numeric_value:,.2f}"
except ValueError:
return ""
2026-02-03 15:06:41 +00:00
def remove_hyphens(value):
"""
2026-02-03 15:06:41 +00:00
Remove hyphens from the input value.
Args:
2026-02-03 15:06:41 +00:00
value : The input might be list or string
Returns:
2026-02-03 15:06:41 +00:00
str: The cleaned value with hyphens removed.
"""
2026-02-03 15:06:41 +00:00
if isinstance(value, str):
return value.replace("-", "")
elif isinstance(value, list):
return [v.replace("-", "") if isinstance(v, str) else v for v in value]
return value
def flatten_singleton_string_list(list_str: str) -> str:
"""Take in a list within a string, and if it has a single element, return that element.
If it has multiple elements, return the list as a string.
Args:
list_str (str): A string representation of a list.
Returns:
str: The single element if the list has one element, otherwise the list as a string.
"""
if string_utils.is_empty(list_str):
return ""
try:
# Convert the string to a list using ast.literal_eval
list_obj = ast.literal_eval(list_str)
# Check if list_obj is actually a list
if not isinstance(list_obj, list):
return list_str
# If the list has one element, return that element
if len(list_obj) == 1:
return list_obj[0]
# Otherwise, return the list as a string
return ", ".join(list_obj)
except (ValueError, SyntaxError):
return list_str
def validate_and_reformat_date(date_str: str) -> str:
"""
Validate if a string is in YYYY/MM/DD format or reformat it to YYYY/MM/DD if possible.
Args:
date_str (str): The input date string.
Returns:
str: The reformatted date string if valid or reformatted, otherwise the original string.
"""
if not isinstance(date_str, str):
return date_str
try:
# Check if the date is already in YYYY/MM/DD format
datetime.strptime(date_str, "%Y/%m/%d")
return date_str
except ValueError:
# Attempt to reformat the date from known formats
known_formats = ["%Y-%m-%d", "%m/%d/%Y", "%d-%b-%Y", "%d/%m/%Y"]
for fmt in known_formats:
try:
return datetime.strptime(date_str, fmt).strftime("%Y/%m/%d")
except ValueError:
continue
return date_str
def normalize_auto_renewal_term(text: str) -> str:
"""
Normalize the auto-renewal term to a standard format.
Args:
text (str): The input auto-renewal term.
Returns:
str: The normalized auto-renewal term.
"""
if not text or not isinstance(text, str):
return ""
# Convert to lowercase and strip whitespace
text = text.lower().strip()
# Remove numbers enclosed in parentheses (e.g., "(1) ", "(12) ")
text = re.sub(r"\(\d+\) ", "", text)
# Define replacements
replacements = {
r"\bone\b": "1",
r"\btwo\b": "2",
r"\bthree\b": "3",
r"\btwelve\b": "12",
r"year to year": "1 year",
r"12 months": "1 year",
r"\btwelve months\b": "1 year",
r"\bone year\b": "1 year",
r"\bone-year\b": "1 year",
r"\b1 year\b": "1 year",
r"\bmonth to month\b": "1 month",
}
# Apply replacements
for pattern, replacement in replacements.items():
text = re.sub(pattern, replacement, text)
# Remove any remaining parentheses or extra spaces
text = text.strip().replace("(", "").replace(")", "")
text = text.replace("-", " ")
if text == "year":
text = "1 year"
return text
def normalize_cpt_fields(value):
"""
Normalize code values to a list format.
Args:
value (Any): Input value which could be a list, string, or other types.
Returns:
list: A normalized list of string codes.
"""
if not value or (isinstance(value, float) and pd.isna(value)): # Handle NaN values
return "" # Empty string
# First normalize to a list
if isinstance(value, (list, tuple)): # If already a list or tuple
result = [str(v).strip() for v in value]
elif isinstance(value, str): # If it's a string
value = value.strip()
if value.startswith("[") and value.endswith(
"]"
): # String representation of a list
try:
# Parse it first
parsed_list = ast.literal_eval(value)
result = []
# Process each item in the parsed list
for item in parsed_list:
item_str = str(item).strip()
# If the item contains commas, split it into multiple items
if "," in item_str:
result.extend([str(v).strip() for v in item_str.split(",")])
else:
result.append(item_str)
except (ValueError, SyntaxError):
result = [value] # If parsing fails, treat it as a single value
elif "," in value: # If it's a comma-separated string
# Split by comma and create a list
result = [str(v).strip() for v in value.split(",")]
elif "-" in value:
# Range format
result = [value]
else: # Single value
result = [value]
elif isinstance(value, (float, int)): # If it's a number
result = [str(int(value)).strip()]
else:
result = [str(value).strip()] # Default case for other types
return str(result) # Return the list directly
def generate_reimb_ids(df: pd.DataFrame) -> pd.DataFrame:
"""Generates REIMB_ID and REIMB_LESSER_OF_ID for each row in the DataFrame based on
the FILE_NAME and other fields.
The REIMB_ID is constructed using the following format:
{FILE_NAME}_exh_pg_{EXHIBIT_PAGE}_{index}_{hash_value}
the REIMB_LESSER_OF_ID is just the {hash_value} itself.
Where:
- FILE_NAME is the name of the file (without extension).
- EXHIBIT_PAGE is the page number of the exhibit.
- index is the index of the row in the group.
- hash_value is the first 8 characters of the MD5 hash of a concatenated string of relevant fields.
The relevant fields are SERVICE_TERM, REIMB_TERM, PROGRAM, PRODUCT, NETWORK, and LOB.
Args:
df (pd.DataFrame): Input DataFrame containing the columns FILE_NAME, EXHIBIT_PAGE, SERVICE_TERM, REIMB_TERM, PROGRAM, PRODUCT, NETWORK, and LOB.
The FILE_NAME column should contain the name of the file (with or without extension).
The EXHIBIT_PAGE column should contain the page number of the exhibit.
Returns:
pd.DataFrame: DataFrame with generated REIMB_IDs and REIMB_LESSER_OF_IDs for each row.
"""
# First sort to ensure consistent ordering
df = (
df.sort_values(by=["FILE_NAME", "EXHIBIT_PAGE"])
if "EXHIBIT_PAGE" in df.columns
else df.sort_values(by=["FILE_NAME"])
)
# Create a temporary copy to avoid SettingWithCopyWarning
df_temp = df.copy()
# Process each file+exhibit_page group to reset counter for each exhibit page
group_cols = (
["FILE_NAME", "EXHIBIT_PAGE"] if "EXHIBIT_PAGE" in df.columns else ["FILE_NAME"]
)
for group_key, indices in df.groupby(group_cols).groups.items():
if isinstance(group_key, tuple):
filename, exhibit_page = group_key
exhibit_page = str(exhibit_page).zfill(3)
else:
filename = group_key
exhibit_page = "000"
clean_filename = str(filename).split("/")[-1].replace(".", "_")
for i, idx in enumerate(indices):
row = df.loc[idx]
# Get fields to include in hash
service_term = str(row.get("SERVICE_TERM", ""))
reimb_term = str(row.get("REIMB_TERM", ""))
program = str(row.get("PROGRAM", ""))
product = str(row.get("PRODUCT", ""))
network = str(row.get("NETWORK", ""))
lob = str(row.get("LOB", ""))
# Create hash string
hash_string = f"{clean_filename}_{service_term}_{reimb_term}_{program}_{product}_{network}_{lob}"
hash_value = hashlib.md5(hash_string.encode()).hexdigest()[:8]
# Generate REIMB_ID and REIMB_LESSER_OF_ID
df_temp.at[idx, "REIMB_ID"] = (
f"{clean_filename}_exh_pg_{exhibit_page}_{i+1:03d}_{hash_value}"
)
df_temp.at[idx, "REIMB_LESSER_OF_ID"] = hash_value
return df_temp
def reorder_columns(df: pd.DataFrame, column_order: list[str]) -> pd.DataFrame:
"""
Reorders the columns of the DataFrame based on the given column order.
Steps:
1. Adds any missing columns from `column_order` to the DataFrame at once, filled with empty strings.
2. Reorders the columns of the DataFrame to match `column_order`.
3. Appends any columns in the DataFrame that are not in `column_order` to the end.
Args:
df (pd.DataFrame): The input DataFrame.
column_order (list[str]): The desired column order.
Returns:
pd.DataFrame: The reordered DataFrame.
"""
# Create a copy to avoid fragmentation
df_copy = df.copy()
# Find missing columns from column_order
missing_columns = [col for col in column_order if col not in df_copy.columns]
# Add all missing columns at once using a dictionary
if missing_columns:
# Create a dictionary of empty columns
empty_cols = {col: [""] * len(df_copy) for col in missing_columns}
# Add all missing columns at once with pd.concat
df_copy = pd.concat(
[df_copy, pd.DataFrame(empty_cols, index=df_copy.index)], axis=1
)
# Reorder columns to match column_order
final_column_order = [col for col in column_order if col in df_copy.columns]
# Return the DataFrame with reordered columns
return df_copy[final_column_order]
def auto_renewal(df: pd.DataFrame) -> pd.DataFrame:
# Normalize the 'AUTO_RENEWAL_TERM' column
if "AUTO_RENEWAL_TERM" in df.columns:
df["AUTO_RENEWAL_TERM"] = df["AUTO_RENEWAL_TERM"].apply(
normalize_auto_renewal_term
)
return df
def update_termination_date_for_conditions(df: pd.DataFrame) -> pd.DataFrame:
"""
Updates the 'AARETE_DERIVED_TERMINATION_DT' column based on specific conditions
involving 'TERMINATION_DT', 'AUTO_RENEWAL_IND', and 'AARETE_DERIVED_TERMINATION_DT'.
Args:
df (pd.DataFrame): The input DataFrame.
Returns:
pd.DataFrame: The updated DataFrame with modified 'AARETE_DERIVED_TERMINATION_DT' values.
"""
if (
"TERMINATION_DT" in df.columns
and "AUTO_RENEWAL_IND" in df.columns
and "AARETE_DERIVED_TERMINATION_DT" in df.columns
):
# Replace Y with 9999/12/31
df.loc[df["AUTO_RENEWAL_IND"] == "Y", "AARETE_DERIVED_TERMINATION_DT"] = (
"9999/12/31"
)
# Replace "N" and "year to year" with 9999/12/31
df.loc[
(df["TERMINATION_DT"] == "year to year")
& (df["AUTO_RENEWAL_IND"] == "N")
& (df["AARETE_DERIVED_TERMINATION_DT"] == "N/A"),
"AARETE_DERIVED_TERMINATION_DT",
] = "9999/12/31"
return df
def standardize_reimb_method_and_fee_schedule(
df: pd.DataFrame, VALID_UNIT_OF_MEASURE: list
) -> pd.DataFrame:
"""
Standardizes the 'AARETE_DERIVED_REIMB_METHOD' column and updates the
'AARETE_DERIVED_FEE_SCHEDULE' column based on specific values.
Args:
df (pd.DataFrame): The input DataFrame.
Returns:
pd.DataFrame: The updated DataFrame with standardized columns.
"""
if (
"AARETE_DERIVED_REIMB_METHOD" in df.columns
and "AARETE_DERIVED_FEE_SCHEDULE" in df.columns
):
# Standardize 'AARETE_DERIVED_REIMB_METHOD' values
# Copy AWP, ASP, WAC to AARETE_DERIVED_FEE_SCHEDULE column for the corresponding row
medrx_fee_schedule_values = ["AWP", "ASP", "WAC"]
df.loc[
df["AARETE_DERIVED_REIMB_METHOD"].isin(medrx_fee_schedule_values),
"AARETE_DERIVED_FEE_SCHEDULE",
] = df["AARETE_DERIVED_REIMB_METHOD"]
# Convert AARETE_DERIVED_REIMB_METHOD values [AWP, ASP, WAC] to 'MedRx'
df["AARETE_DERIVED_REIMB_METHOD"] = df["AARETE_DERIVED_REIMB_METHOD"].replace(
{"AWP": "MedRx", "ASP": "MedRx", "WAC": "MedRx"}
)
if "AARETE_DERIVED_REIMB_METHOD" in df.columns:
# Remove rows where 'AARETE_DERIVED_REIMB_METHOD' is 'DOFR' but 'REIMB_TERM' does not contain 'DOFR'
mask = (df["AARETE_DERIVED_REIMB_METHOD"] == "DOFR") & (
~df["REIMB_TERM"].str.contains("DOFR", na=False)
)
df = df[~mask]
# standardize the 'UNIT_OF_MEASURE' column
if "UNIT_OF_MEASURE" in df.columns:
df["UNIT_OF_MEASURE"] = df["UNIT_OF_MEASURE"].str.replace(
"Per Encounter", "Per Visit"
)
# remove cases of Per Member Per Month (PMPM)
df = df[~df["UNIT_OF_MEASURE"].isin(["Per Member Per Month (PMPM)"])]
2025-08-13 14:51:46 +00:00
# Enhanced logic: Set UNIT_OF_MEASURE as 'Per Unit' for Flat Rate reimbursement method with empty UNIT_OF_MEASURE
# This catches all edge cases including NaN, None, empty strings, etc.
if "AARETE_DERIVED_REIMB_METHOD" in df.columns:
flat_rate_mask = df["AARETE_DERIVED_REIMB_METHOD"] == "Flat Rate"
empty_uom_mask = (
df["UNIT_OF_MEASURE"].isna()
| df["UNIT_OF_MEASURE"].eq("")
| df["UNIT_OF_MEASURE"].apply(string_utils.is_empty)
)
df.loc[flat_rate_mask & empty_uom_mask, "UNIT_OF_MEASURE"] = "Per Unit"
# Ensure UNIT_OF_MEASURE is one of the valid values
df["UNIT_OF_MEASURE"] = df["UNIT_OF_MEASURE"].apply(
lambda x: x if x in VALID_UNIT_OF_MEASURE else ""
)
return df
def process_patient_age_range(df):
"""
Process the 'PATIENT_AGE_RANGE' column into 'PATIENT_AGE_MIN' and 'PATIENT_AGE_MAX'.
If a single number is provided, both min and max are set to that number.
Remove PATIENT_AGE_RANGE column.
Args:
df (pd.DataFrame): The input DataFrame.
Returns:
pd.DataFrame: The updated DataFrame with 'PATIENT_AGE_MIN' and 'PATIENT_AGE_MAX' columns.
"""
if "PATIENT_AGE_RANGE" in df.columns:
try:
# Create temporary columns for processing
df["PATIENT_AGE_MIN"] = df["PATIENT_AGE_RANGE"]
df["PATIENT_AGE_MAX"] = df["PATIENT_AGE_RANGE"]
# Handle hyphenated ranges
mask = df["PATIENT_AGE_RANGE"].str.contains("-", na=False)
if mask.any():
temp_split = df.loc[mask, "PATIENT_AGE_RANGE"].str.split(
"-", expand=True
)
df.loc[mask, "PATIENT_AGE_MIN"] = temp_split[0]
df.loc[mask, "PATIENT_AGE_MAX"] = temp_split[1]
except Exception as e:
# If any error occurs, keep the original values
pass
# Remove the original PATIENT_AGE_RANGE column
df.drop(columns=["PATIENT_AGE_RANGE"], inplace=True, errors="ignore")
return df
def add_aarete_derived_amendment_num(df: pd.DataFrame) -> pd.DataFrame:
"""
Add the 'AARETE_DERIVED_AMENDMENT_NUM' column to the DataFrame.
Args:
df (pd.DataFrame): The input DataFrame.
Returns:
pd.DataFrame: The updated DataFrame with the 'AARETE_DERIVED_AMENDMENT_NUM' column.
"""
if "CONTRACT_AMENDMENT_NUM" in df.columns:
df["AARETE_DERIVED_AMENDMENT_NUM"] = (
df["CONTRACT_AMENDMENT_NUM"]
.str.split("-")
.str[1]
.fillna(df["CONTRACT_AMENDMENT_NUM"])
)
return df
def add_aarete_derived_product(df):
"""
Add AARETE_DERIVED_PRODUCT column to df, based on PRODUCT column.
The new column should be Title Case of the PRODUCT column value.
"""
if "PRODUCT" in df.columns:
2026-02-03 00:07:12 -05:00
df["AARETE_DERIVED_PRODUCT"] = df["PRODUCT"]
return df
def fill_empty_reimb_pct_rate(df):
"""
Fill 'REIMB_PCT_RATE' based on reimbursement method:
- Set 'REIMB_PCT_RATE' to '100' for rows where:
- 'REIMB_PCT_RATE', 'REIMB_FEE_RATE', and 'REIMB_CONVERSION_FACTOR' are all empty.
- Set 'REIMB_PCT_RATE' to 'N/A' for rows where:
- 'AARETE_DERIVED_REIMB_METHOD' contains specific grouper.
"""
required_cols = [
"REIMB_PCT_RATE",
"REIMB_FEE_RATE",
"REIMB_CONVERSION_FACTOR",
"AARETE_DERIVED_REIMB_METHOD",
]
if all(col in df.columns for col in required_cols):
# Fill with '100' when all three rate fields are empty
pct_empty = string_utils.is_empty(df["REIMB_PCT_RATE"])
fee_empty = string_utils.is_empty(df["REIMB_FEE_RATE"])
conversion_empty = string_utils.is_empty(df["REIMB_CONVERSION_FACTOR"])
applicable_methods = ["Billed Charges", "Fee Schedule", "Case Rate", "Cost"]
method_applicable = df["AARETE_DERIVED_REIMB_METHOD"].isin(applicable_methods)
fill_mask = pct_empty & fee_empty & conversion_empty & method_applicable
df.loc[fill_mask, "REIMB_PCT_RATE"] = "100"
# Set to N/A for specific reimbursement methods
na_methods = ["Per Diem", "Flat Rate", "Per Visit"]
na_mask = df["AARETE_DERIVED_REIMB_METHOD"].isin(na_methods)
df.loc[na_mask, "REIMB_PCT_RATE"] = "N/A"
return df
def remove_redundant_reimb_info(df):
"""
Checks if "REIMB_" fields are the same as their corresponding 1:1 fields.
If they are the same, replace the "REIMB_" values with ""
Args:
df (pd.DataFrame): The input DataFrame.
Returns:
pd.DataFrame: The updated DataFrame with redundant reimbursement field values removed.
"""
COLUMN_PAIRS = [
("REIMB_EFFECTIVE_DT", "AARETE_DERIVED_EFFECTIVE_DT"),
("REIMB_TERMINATION_DT", "AARETE_DERIVED_TERMINATION_DT"),
("REIMB_PROV_NAME", "PROV_GROUP_NAME_FULL"),
]
for column_tuple in COLUMN_PAIRS:
reimb_col = column_tuple[0]
one_to_one_col = column_tuple[1]
if all(col in df.columns for col in [reimb_col, one_to_one_col]):
mask = df[reimb_col] == df[one_to_one_col]
df.loc[mask, reimb_col] = ""
return df
def replace_provider_name_from_hybrid_smart_chunking(df):
"""
Replaces PROV_GROUP_NAME_FULL with PROVIDER_NAME for rows where PROVIDER_NAME is not empty/N/A,
then removes the PROVIDER_NAME column.
This function is used in postprocessing after the Hybrid Smart Chunking provider name extraction.
The RAG method may produce a more accurate provider name than the initial extraction,
so we use it to override PROV_GROUP_NAME_FULL when available.
Args:
df (pd.DataFrame): DataFrame containing contract data
Returns:
pd.DataFrame: DataFrame with PROV_GROUP_NAME_FULL updated and PROVIDER_NAME removed
"""
# Check if both columns exist
if "PROVIDER_NAME" not in df.columns:
logging.debug(
"PROVIDER_NAME column not found in DataFrame, skipping replacement"
)
return df
if "PROV_GROUP_NAME_FULL" not in df.columns:
logging.debug("PROV_GROUP_NAME_FULL column not found in DataFrame")
# Just remove PROVIDER_NAME if it exists
df = df.drop(columns=["PROVIDER_NAME"])
return df
# Create a mask for rows where PROVIDER_NAME should replace PROV_GROUP_NAME_FULL
# Replace when PROVIDER_NAME is not empty
mask = ~string_utils.is_empty(df["PROVIDER_NAME"], pd_mask=True)
# Count how many rows will be updated
num_replacements = mask.sum()
if num_replacements > 0:
# Replace PROV_GROUP_NAME_FULL with PROVIDER_NAME for matching rows
df.loc[mask, "PROV_GROUP_NAME_FULL"] = df.loc[mask, "PROVIDER_NAME"]
logging.debug(
f"Replaced PROV_GROUP_NAME_FULL with PROVIDER_NAME for {num_replacements} rows"
)
else:
logging.debug("No valid PROVIDER_NAME values to replace PROV_GROUP_NAME_FULL")
# Remove the PROVIDER_NAME column
df = df.drop(columns=["PROVIDER_NAME"])
return df
def deduplicate_provider_columns(df: pd.DataFrame) -> pd.DataFrame:
"""Deduplicate provider fields in the DataFrame.
This function removes PROV_GROUP_* values from the PROV_OTHER_* columns if they are already present.
In addition, it deduplicates PROV_OTHER_* values.
2026-02-03 15:06:41 +00:00
E.g. If PROV_GROUP_TIN is ["123456789"] and PROV_OTHER_TIN is ["123456789", "987654321", "987654321"],
then PROV_OTHER_TIN will be updated to ["987654321"].
Args:
df (pd.DataFrame): The input DataFrame.
Returns:
pd.DataFrame: The updated DataFrame with deduplicated provider fields.
"""
provider_columns = [
"PROV_GROUP_TIN",
"PROV_GROUP_NPI",
"PROV_GROUP_NAME_FULL",
"PROV_OTHER_TIN",
"PROV_OTHER_NPI",
"PROV_OTHER_NAME_FULL",
]
2026-02-03 15:06:41 +00:00
if not all(col in df.columns for col in provider_columns):
return df
for index, row in df.iterrows():
group_values = {
"PROV_OTHER_TIN": row.get("PROV_GROUP_TIN"),
"PROV_OTHER_NPI": row.get("PROV_GROUP_NPI"),
"PROV_OTHER_NAME_FULL": row.get("PROV_GROUP_NAME_FULL"),
}
for other_col, group_vals in group_values.items():
other_vals = row.get(other_col)
if not other_vals:
# None, NaN, or empty list
df.at[index, other_col] = []
continue
# Normalize group values (list or empty)
group_vals = group_vals or []
# Remove GROUP values
filtered = [
v for v in other_vals if v not in group_vals and v and v != "UNKNOWN"
]
# Deduplicate while preserving order
seen = set()
deduped = []
for v in filtered:
if v not in seen:
seen.add(v)
deduped.append(v)
df.at[index, other_col] = deduped
2025-08-13 14:51:46 +00:00
return df
def update_grouper_base_rate_and_grouper_pct_rate(df):
"""
Update GROUPER_PCT_RATE and GROUPER_BASE_RATE columns based on grouper conditions.
Creates new columns and populates them when:
- AARETE_DERIVED_REIMB_METHOD is 'Grouper', OR
- GROUPER_TYPE is not empty (using string_utils.is_empty) AND not empty array/empty array string, OR
- GROUPER_CD is not empty (using string_utils.is_empty) AND not empty array/empty array string
Args:
df: DataFrame to update
Returns:
DataFrame with updated GROUPER_PCT_RATE and GROUPER_BASE_RATE columns
"""
# Required columns for the operation
required_cols = [
"AARETE_DERIVED_REIMB_METHOD",
"REIMB_PCT_RATE",
"REIMB_CONVERSION_FACTOR",
"GROUPER_TYPE" "GROUPER_CD",
]
# Check if all required columns exist
if not all(col in df.columns for col in required_cols):
# Initialize empty columns and return if required columns are missing
df["GROUPER_PCT_RATE"] = ""
df["GROUPER_BASE_RATE"] = ""
return df
# Create grouper condition mask
grouper_mask = (df["AARETE_DERIVED_REIMB_METHOD"] == "Grouper") & (
(~string_utils.is_empty(df["GROUPER_TYPE"]) & df["GROUPER_TYPE"].ne("[]"))
| (~string_utils.is_empty(df["GROUPER_CD"]) & df["GROUPER_CD"].ne("[]"))
)
# Initialize new columns with empty strings
df["GROUPER_PCT_RATE"] = ""
df["GROUPER_BASE_RATE"] = ""
# Update values where grouper condition is met
df.loc[grouper_mask, "GROUPER_PCT_RATE"] = df.loc[grouper_mask, "REIMB_PCT_RATE"]
df.loc[grouper_mask, "GROUPER_BASE_RATE"] = df.loc[
grouper_mask, "REIMB_CONVERSION_FACTOR"
]
return df
def attach_sid_column(df: pd.DataFrame) -> pd.DataFrame:
"""
Ensures the given AARETE_DERIVED_SID columns exist in the DataFrame, fills missing values,
and creates a unique 'AARETE_DERIVED_SID' identifier column — while keeping all original columns.
Steps:
1. Verify that all columns listed in sid_cols exist in the DataFrame.
- If any are missing, create them and fill with ''.
2. Concatenate all sid_cols values with '__' as a separator to form 'AARETE_DERIVED_SID'.
3. If any value in sid_cols is blank or NaN, replace it with 'xx' in the concatenation.
4. Add the 'AARETE_DERIVED_SID' column to the DataFrame.
Parameters
----------
df : pd.DataFrame
Input DataFrame containing the source columns.
Returns
-------
pd.DataFrame
DataFrame with all original columns plus a new 'AARETE_DERIVED_SID' column.
"""
sid_cols = [
"CLIENT_NAME",
"PAYER_NAME",
"PROV_GROUP_TIN",
"DOCUMENT_TYPE",
"AARETE_DERIVED_AMENDMENT_NUM",
"AARETE_DERIVED_CLAIM_TYPE_CD",
"AARETE_DERIVED_LOB",
"AARETE_DERIVED_EFFECTIVE_DT",
]
# Make a copy to avoid mutating the original DataFrame
df = df.copy()
# Ensure all SID columns exist; create missing ones filled with 'xx'
for col in sid_cols:
if col not in df.columns:
df[col] = ""
# Create the concatenated SID column (replace blanks/NaN only here)
if df.empty:
df["AARETE_DERIVED_SID"] = pd.Series(dtype=str)
else:
sid_df = df[sid_cols].fillna("").astype(str)
df["AARETE_DERIVED_SID"] = sid_df.apply(
lambda row: "__".join(val if val.strip() else "xx" for val in row),
axis=1,
result_type="reduce",
)
return df
def add_aarete_derived_signatory_complete_ind(df: pd.DataFrame) -> pd.DataFrame:
"""
Add a derived indicator column to the DataFrame indicating whether all signatory lines are signed.
Creates column:
AARETE_DERIVED_SIGNATORY_COMPLETE_IND = 'Y' if
NUM_AVAILABLE_SIGNATORY_LINE_COUNT == NUM_SIGNED_SIGNATORY_LINE_COUNT
else 'N'.
Parameters
----------
df : pd.DataFrame
Input DataFrame containing the required columns.
Returns
-------
pd.DataFrame
DataFrame with the new indicator column added.
"""
try:
required_cols = [
"NUM_AVAILABLE_SIGNATORY_LINE_COUNT",
"NUM_SIGNED_SIGNATORY_LINE_COUNT",
]
# Validate required columns
for col in required_cols:
if col not in df.columns:
return df
# Ensure consistent numeric types
for col in required_cols:
df[col] = pd.to_numeric(df[col], errors="coerce")
# Create indicator column
df["AARETE_DERIVED_SIGNATORY_COMPLETE_IND"] = (
df["NUM_AVAILABLE_SIGNATORY_LINE_COUNT"]
.eq(df["NUM_SIGNED_SIGNATORY_LINE_COUNT"])
.map({True: "Y", False: "N"})
)
return df
except Exception:
# Return df unchanged on any exception
return df