Files
doczyai-pipelines/fieldProcessing/src/postprocess_funcs.py
T

462 lines
21 KiB
Python
Raw Normal View History

2024-09-30 10:47:30 +01:00
import pandas as pd
import re
import difflib
import config
def sanitize_value(value):
"""
Sanitizes a given value by handling nulls, lists, and strings to ensure consistency in formatting.
This function performs several cleanliness checks and transformations:
- It leaves null (NaN) values unchanged.
- Converts lists into comma-separated strings.
- Strips unnecessary characters from strings and handles strings that represent lists, ensuring individual elements are properly formatted.
Parameters:
value (varied types): The value to be sanitized, which can be of any type including nulls, lists, or strings.
Returns:
varied types: The sanitized value, which can be a string or the original type if no changes were needed.
"""
try:
if isinstance(value, list):
return ', '.join(str(v) for v in value)
elif isinstance(value, str):
value = value.strip('[]')
return ', '.join([item.strip(" '") for item in value.split(',')])
elif pd.isna(value):
return "N/A"
except:
return value
def exact_match(val, valid_values):
"""
Compares a given string value against a list of valid values to determine if there is an exact match, case-insensitively.
This function processes the input value by trimming whitespace and converting to uppercase for a case-insensitive comparison.
It then checks each item in the list of valid values (also treated case-insensitively) to find an exact match. If a match is found,
the function returns the valid value in its original form as specified in the list. If no match is found, it returns None.
Parameters:
val (str): The value to be checked for an exact match.
valid_values (list of str): A list of valid values against which the input value is compared.
Returns:
str or None: The matching valid value if found, or None if no exact match is found.
"""
val = val.strip().upper()
for valid_val in valid_values:
if val == valid_val.upper():
return valid_val
return None
def clean_columns_combined(df, column_name, valid_values, new_column_name):
"""
Cleans and standardizes entries in a specified column of a DataFrame based on a list of valid values, creating a new column with standardized values.
This function first sanitizes the values in the specified column. It then examines each entry in that column,
looking for any sub-values (separated by commas) that exactly match any of the provided valid values. If a match is found,
it is recorded in a new column. The function also tracks changes and compares the original and cleaned unique values.
Parameters:
df (pandas.DataFrame): The DataFrame containing the data to be cleaned.
column_name (str): The name of the column to be cleaned.
valid_values (list of str): A list of valid values to be used for checking the entries in the specified column.
new_column_name (str): The name of the new column where cleaned, standardized values will be stored.
Returns:
tuple: Contains arrays of original unique values, cleaned unique values, and a dictionary of changes from original to new values.
"""
changes = {}
df[column_name] = df[column_name].apply(sanitize_value)
original_values = df[column_name].unique()
def update_column(entry):
if pd.notna(entry):
terms = entry.split(',')
for term in terms:
match = exact_match(term, valid_values)
if match:
return match
return None
df[new_column_name] = df[column_name].apply(update_column)
cleaned_values = df[new_column_name].unique()
return original_values, cleaned_values, changes
def get_closest_match(val, valid_values, similarity_threshold=0.7):
"""
Finds the closest match for a given string from a list of valid values, using a similarity threshold.
This function cleans the input value and compares it to each cleaned value in the valid values list using a fuzzy
matching technique. It returns the closest match that meets or exceeds the specified similarity threshold. If no
matches meet the threshold, the function returns None.
Parameters:
val (str): The value to be matched.
valid_yvalues (list of str): A list of valid values against which the input value is compared.
similarity_threshold (float): The cutoff similarity score (between 0 and 1) below which matches are not considered.
Returns:
str or None: The closest valid value if a match is found; otherwise, None.
"""
if pd.isna(val):
return None
val = val.strip().upper()
matches = difflib.get_close_matches(val, [v.upper() for v in valid_values], n=1, cutoff=similarity_threshold)
return matches[0] if matches else None
def clean_columns_combined_fuzzy(df, column_name, valid_values, threshold):
"""
Applies fuzzy matching to entries in a specified column of a DataFrame to standardize them according to a list of valid values,
using a defined similarity threshold to determine matches.
The function sanitizes the column values and then processes each entry, splitting it into sub-values if multiple are present.
Each sub-value is compared against the valid values list using a fuzzy matching algorithm. If a close match is found (above the
threshold), it replaces the original sub-value. The function logs any changes made for tracking and verification purposes.
Parameters:
df (pandas.DataFrame): The DataFrame containing the column to be cleaned.
column_name (str): The name of the column to be cleaned.
valid_values (list of str): A list of valid values used for fuzzy matching.
threshold (float): The similarity threshold for accepting matches.
Returns:
tuple: Contains arrays of original unique values, cleaned unique values, and a dictionary of changes (original to new values).
"""
changes = {}
df[column_name] = df[column_name].apply(sanitize_value)
original_values = df[column_name].unique()
def log_and_clean(entry):
if pd.notna(entry):
words = entry.split(',')
cleaned_words = []
for word in words:
cleaned_word = get_closest_match(word.strip(), valid_values, similarity_threshold=threshold)
if cleaned_word and word.strip().upper() != cleaned_word:
changes[word.strip()] = cleaned_word
cleaned_words.append(cleaned_word if cleaned_word else word.strip())
return ', '.join(cleaned_words)
return None
df[column_name] = df[column_name].apply(log_and_clean)
cleaned_values = df[column_name].unique()
return original_values, cleaned_values, changes
def correct_misplaced_values(df, columns, valid_values_dict):
"""
Corrects misplaced values within specified columns of a DataFrame based on a dictionary of valid values for each column.
This function iterates through each row of the DataFrame and checks the values of specified columns. If a value
is found that matches valid entries for a different column (based on the valid_values_dict), it moves the value to
the appropriate column. If the target column already contains a value, it uses fuzzy matching to determine if the
misplaced value should replace the existing one. If a replacement isn't suitable, it logs the correction attempt.
Parameters:
df (pandas.DataFrame): The DataFrame containing the data to be corrected.
columns (list of str): The columns to check for misplaced values.
valid_values_dict (dict): A dictionary mapping column names to lists of valid values for those columns.
Returns:
pandas.DataFrame: The DataFrame with corrected placements of values across the specified columns.
"""
for index, row in df.iterrows():
for col in columns:
if pd.notna(row[col]):
terms = row[col].split(',')
for term in terms:
term = term.strip()
for target_col, valid_values in valid_values_dict.items():
if target_col != col:
match = exact_match(term, valid_values)
if match:
if pd.isna(row[target_col]) or not row[target_col].strip():
df.at[index, target_col] = match
df.at[index, col] = None
else:
current_value = row[target_col].strip()
if get_closest_match(match, [current_value], config.FUZZY_MATCH_THRESHOLD) is None:
df.at[index, 'Corrected_' + target_col] = f"Found {term} in {col} cell"
df.at[index, col] = None
return df
def filter_service_column(d):
"""
Filters out items in a list of dictionaries based on specified keywords within the 'SERVICE' key.
This function uses a list of predefined keywords associated with non-service related terms like 'LIABILITY' or 'RISK'.
It constructs a regular expression pattern from these keywords and removes any dictionary items where the 'SERVICE' key contains
any of these keywords, effectively filtering the list to exclude items that are likely to be irrelevant or misclassified
as services when they pertain to other contractual obligations or conditions.
Parameters:
d (list): A list of dictionaries, each containing a 'SERVICE' key among others.
Returns:
list: A new list of dictionaries with items containing specified keywords in the 'SERVICE' key removed.
"""
keywords = [
'LIABILITY', 'RISK', 'LOBBYING', 'DAMAGES', 'CONFIDENTIALITY', 'ARBITRATION', 'FALSE CLAIMS ACT', 'UNSPECIFIED',
'AUDIT', 'INTEREST', 'N/A', 'BUSINESS', 'COMPLIANCE', 'Medical Assistance Program', 'MATERIAL SUBCONTRACT', 'GIFTS', 'GRATUITIES',
]
pattern = '|'.join(keywords)
regex = re.compile(pattern, re.IGNORECASE)
filtered_list = [item for item in d if not regex.search(item.get('SERVICE', ''))]
return filtered_list
def move_percentage_to_rate(df):
"""
Moves percentage values from the 'REIMBURSEMENT_FLAT_FEE' column to the 'REIMBURSEMENT_RATE' column in a DataFrame.
This function identifies entries in the 'REIMBURSEMENT_FLAT_FEE' column that contain a percentage sign, indicating they are
incorrectly placed. It then transfers these percentage values to the 'REIMBURSEMENT_RATE' column. The original 'REIMBURSEMENT_FLAT_FEE'
entries from which percentages are moved are set to None, correcting the placement of data within the DataFrame.
Parameters:
df (pandas.DataFrame): The DataFrame containing the reimbursement data to be adjusted.
Returns:
pandas.DataFrame: The DataFrame with percentage values moved from the 'REIMBURSEMENT_FLAT_FEE' to the 'REIMBURSEMENT_RATE' column.
"""
def move_percentage(value):
if isinstance(value, str) and '%' in value:
return True
return False
df['REIMBURSEMENT_RATE'] = df.apply(lambda row: row['REIMBURSEMENT_FLAT_FEE'] if move_percentage(row['REIMBURSEMENT_FLAT_FEE']) else row['REIMBURSEMENT_RATE'], axis=1)
df['REIMBURSEMENT_FLAT_FEE'] = df.apply(lambda row: None if move_percentage(row['REIMBURSEMENT_FLAT_FEE']) else row['REIMBURSEMENT_FLAT_FEE'], axis=1)
return df
def set_rate_to_zero_if_not_covered(df):
"""
Sets the 'REIMBURSEMENT_RATE' to zero for entries in the DataFrame where the service is explicitly not covered,
as indicated by the content of the 'FULL_METHODOLOGY' column.
This function checks each row's 'FULL_METHODOLOGY' column for phrases indicating that the service is not covered
(e.g., "not covered"). If such a phrase is found, the 'REIMBURSEMENT_RATE' for that row is set to zero to reflect that
there is no financial reimbursement for these services. This helps ensure that the data accurately represents the terms
of service coverage.
Parameters:
df (pandas.DataFrame): The DataFrame containing the reimbursement and methodology data.
Returns:
pandas.DataFrame: The updated DataFrame with adjusted 'REIMURSEMENT_RATE' values where applicable.
"""
def check_and_set_rate(row):
if pd.notna(row['FULL_METHODOLOGY']) and re.search(r'not covered', row['FULL_METHODOLOGY'], re.IGNORECASE):
return 0
return row['REIMBURSEMENT_RATE']
df['REIMBURSEMENT_RATE'] = df.apply(check_and_set_rate, axis=1)
return df
def adjust_reimbursement_rate(df):
"""
Adjusts the 'REIMBURSEMENT_RATE' in the DataFrame based on specific conditions mentioned in the 'FULL_METHODOLOGY' column.
This function reviews each row's 'FULL_METHODOLOGY' column for mentions of being "case insensitive" as a part of the methodology.
If this condition is noted and the 'REIMBURSEMENT_RATE' is a numerical value, it adjusts the rate by subtracting it from 100,
effectively transforming the rate into a complementary percentage. This modification is intended to correct or recalibrate
reimbursement rates under specific contractual terms.
Parameters:
df (pandas.DataFrame): The DataFrame containing the methodology and reimbursement rate data.
Returns:
pandas.DataFrame: The updated DataFrame with recalibrated 'REIMBURSEMENT_RATE' values based on the specified methodology condition.
"""
def adjust_rate(row):
if pd.notna(row['FULL_METHODOLOGY']) and re.search(r'case insensitive', row['FULL_METHODOLOGY'], re.IGNORECASE):
if pd.notna(row['REIMBURSEMENT_RATE']) and isinstance(row['REIMBURSEMENT_RATE'], (int, float)):
return 100 - row['REIMBURSEMENT_RATE']
return row['REIMBURSEMENT_RATE']
df['REIMBURSEMENT_RATE'] = df.apply(adjust_rate, axis=1)
return df
def clean_td(td):
"""
Cleans the data structure returned from a 'Top Down' analysis by normalizing and formatting its values.
This function iterates through each entry in the list of dictionaries returned from the top down analysis,
modifying data based on specific criteria:
- Dates are converted to lists if not already in list form.
- Comma-separated strings are split into lists.
- 'N/A' values are replaced with empty lists.
- Preserves specific keys as they are (e.g., 'page_num', 'Filename').
Parameters:
td (list of dict): The list of dictionaries containing the results from top down analysis.
Returns:
list of dict: A new list of dictionaries with the cleaned and normalized data.
"""
td_clean = []
for d in td:
new_d = {}
for k, v in d.items():
if 'DATE' in k:
new_d[k] = v if isinstance(v, list) else [v]
elif k not in ['page_num', 'Filename']:
if isinstance(v, str) and ',' in v:
new_d[k] = [item.strip() for item in v.split(',')]
elif v == 'N/A':
new_d[k] = []
else:
new_d[k] = [v] if isinstance(v, str) else v
else:
new_d[k] = v
td_clean.append(new_d)
return td_clean
def sanitize_combined(df):
"""
Sanitizes all columns in a DataFrame by applying a predefined sanitization function to each value.
This function iterates over each column in the DataFrame, applying a sanitization function to each element.
The sanitization function is designed to clean or adjust the data in a way that makes it more consistent or appropriate
for further processing or analysis. This could include operations like trimming whitespace, correcting formats,
or removing unwanted characters.
Parameters:
df (pandas.DataFrame): The DataFrame to be sanitized.
Returns:
pandas.DataFrame: The sanitized DataFrame, with the same structure but cleaned data.
"""
for column in df.columns:
df[column] = df[column].apply(sanitize_value)
return df
def extract_codes_CPT(value):
"""
Extracts and formats CPT codes from a given input value.
This function searches for patterns that match CPT codes within the provided input. CPT codes typically consist
of a letter followed by four digits, or just five digits, and may include a two-letter state code as a suffix.
The function handles cases where the range of codes is specified using 'through' by converting it to a hyphen.
If any codes are found, they are returned as a comma-separated string. If no codes are found or the input is null,
an appropriate value (empty string or null) is returned.
Parameters:
value (str or NaN): The value from which to extract CPT codes, which could potentially be a string or NaN.
Returns:
str: A comma-separated string of the extracted CPT codes, or an empty string if none are found.
If the input is NaN, the function returns the input as is.
"""
if pd.isna(value):
return value
value = re.sub(r'\bthrough\b', '-', str(value), flags=re.IGNORECASE)
pattern = r'\b([A-Z]\d{4}|\d{5})(-[A-Z]{2})?\b'
matches = re.findall(pattern, value)
if matches:
return ', '.join([''.join(match) for match in matches])
else:
return ""
def extract_codes_Diagnosis(value):
"""
Extracts and formats diagnosis codes from a given input value, typically adhering to ICD (International Classification of Diseases) formats.
This function identifies patterns consistent with ICD diagnosis codes, which are generally a letter followed by numbers
and may include additional alphanumeric characters and a period for further specification. The function handles null inputs
gracefully, returning the input itself if null. If valid diagnosis codes are found, they are returned as a comma-separated
string. If no codes are found, the function returns an empty string.
Parameters:
value (str or NaN): The input value from which to extract diagnosis codes, which could be a string or NaN.
Returns:
str: A comma-separated string of the extracted diagnosis codes, or an empty string if none are found.
If the input is NaN, the function returns the input as is.
"""
if pd.isna(value):
return value
pattern = r'\b[A-Z][0-9][A-Z0-9]{1,4}(\.[A-Z0-9]{1,4})?\b'
matches = re.findall(pattern, value)
if matches:
return ', '.join(matches)
else:
return ""
def extract_codes_Revenue(value):
"""
Extracts revenue codes from a given input value. Revenue codes are typically numerical codes of three to four digits.
This function searches for patterns that match revenue codes in the input provided. It handles cases where the input might be null (NaN),
returning the input itself in such cases. If any valid revenue codes are found, they are formatted into a comma-separated string.
If no codes are found, the function returns an empty string, indicating the absence of recognizable codes in the input.
Parameters:
value (str or NaN): The input value from which to extract revenue codes, which could be a string or NaN.
Returns:
str: A comma-separated string of the extracted revenue codes, or an empty string if none are found.
If the input is NaN, the function returns the input as is.
"""
if pd.isna(value):
return value
pattern = r'\b\d{3,4}\b'
matches = re.findall(pattern, value)
if matches:
return ', '.join(matches)
else:
return ""
def clean_code_column(df, column_name, extract_function):
"""
Applies a specified function to clean and extract codes from a specific column in a DataFrame.
This function takes a DataFrame and applies a given extraction function to each element of a specified column.
The extraction function is expected to format or extract codes (e.g., CPT, ICD, Revenue codes) from the column's values.
This cleaned and extracted data replaces the original values in the column. The function is flexible and can be used
with any function designed to process and return cleaned values from a single input.
Parameters:
df (pandas.DataFrame): The DataFrame containing the data to be cleaned.
column_name (str): The name of the column to be cleaned.
extract_function (function): The function to apply to each value in the specified column, which processes and returns the cleaned value.
Returns:
pandas.DataFrame: The DataFrame with the specified column updated with cleaned and processed codes.
"""
df[column_name] = df[column_name].apply(extract_function)
return df
def clean_dates(df):
for idx, row in df.iterrows():
if row['LOB_PRICING_TERMS_EFFECTIVE_DATE'] == row['LOB_PRICING_TERMS_TERMINATION_DATE']:
df.loc[idx, 'LOB_PRICING_TERMS_TERMINATION_DATE'] = 'N/A'
return df
def add_PG_columns(results_dicts):
for key in list(results_dicts[0].keys()):
if key != 'page_num':
for d in results_dicts:
d[f"{key}_PG"] = d['page_num']
return results_dicts