Merged in bugfix/all_cnc_hotfixes (pull request #302)

DRAFT: Bugfix/all cnc hotfixes

* Pipeline fix - health plan state

* Update for prompt vs non-prompt fixes

* Revert utils.find_regex_matches()

* Remove prints

* Non-prompt run

* Health Plan State, IRS, NPI, LOB, Lesser, Prov 2, Default, Rate Standard, Contract Effective Date, Term Group

* Merged in bugfix/agreement_name (pull request #294)

contract title fixed

* contract title fixed

* Merged bugfix/all_cnc_hotfixes into bugfix/agreement_name


Approved-by: Katon Minhas

* All hotfixes added

* Fixed utils

* fix for clean_contract_effective_date
* Merged in default-fix (pull request #296)

moved default funcs to the top, dropped intermediate columns

* moved default funcs to the top, dropped intermediate columns


Approved-by: Katon Minhas

* quick fix to dropping extra columns

* Merged in bugfix/effective_date_meridian (pull request #297)

fixed effective date for meridian contracts

* fixed effective date for meridian contracts


Approved-by: Katon Minhas

* Default rate and Lesser fix

* Merged in hotfix/irs_npi_updated (pull request #298)

updated irs and npi hotfix funcs

* updated irs and npi hotfix funcs

* Merged bugfix/all_cnc_hotfixes into hotfix/irs_npi_updated


Approved-by: Katon Minhas

* non-functional commit - default only

* debug commit - premerge

* Batch 1 Dup File Reconciliation

* Default rate fix

* Full CNC 1A Run Config

* Standard column refactor

* Working stitching for 1A

* Merged in Alex-Galarce/cnc_hotfix_effective_date_utilspy-edited-1732659983687 (pull request #301)

Remove "license" from effective date smart chunking

* Remove "license" from effective date smart chunking
* Merged bugfix/all_cnc_hotfixes into Alex-Galarce/cnc_hotfix_effective_date_utilspy-edited-1732659983687


Approved-by: Katon Minhas

* Merged in hotfix/irs_batch2_feedback (pull request #300)

IRS fix for feedback on batch2

* IRS fix for feedback on batch2

* Merged bugfix/all_cnc_hotfixes into hotfix/irs_batch2_feedback


Approved-by: Katon Minhas

* Merged main into bugfix/all_cnc_hotfixes


Approved-by: Katon Minhas
This commit is contained in:
Alex Galarce
2024-11-27 15:08:04 +00:00
committed by Katon Minhas
parent 88a57f1e1f
commit 56e3fdd1f8
32 changed files with 3061 additions and 2338 deletions
+17 -1
View File
@@ -85,6 +85,21 @@ texts/
*.json
myenv/
*.env
<<<<<<< HEAD
*.txt
!requirements.txt
*.pdf
subset/
output/
docs/
allfiles/
CNC_testruns_AC/
cnc-abc-output/
humana-input/
humana-output/
output_consolidated/
=======
subset/
output/
docs/
@@ -94,4 +109,5 @@ docs/
__pycache__/
.env
new/
new/
>>>>>>> main
-163
View File
@@ -1,163 +0,0 @@
import pandas as pd
from difflib import SequenceMatcher
import re
import time
import prompts
import claude_funcs
def get_closest_substring_match(val, valid_values):
"""Returns the first match from valid_values using a case-insensitive substring match."""
if pd.isna(val):
return None
val = val.strip().upper()
for valid_value in valid_values:
if valid_value.upper() in val:
return valid_value
return None
def get_best_carveout_from_claude(carveout_list, service):
print("using claude to get best carveout...")
prompt = f"Given the service '{service}', please choose the best matching carveout from the following list: {', '.join(carveout_list)}. ONLY SELECT ONE FROM THE LIST. DO NOT RETURN A SENTENCE"
while True:
try:
response = claude_funcs.invoke_claude_3(prompt, max_tokens=100)
return response.strip()
except Exception as e:
print(f"Error occurred: {e}. Waiting for 60 seconds before retrying...")
time.sleep(60)
def check_prov_type_similarity(prov_type, carveout):
print("checking similarity between prov_type and carveout with claude...")
prompt = f"Do the provider type '{prov_type}' and the carveout '{carveout}' mean the same thing or are they very similar? ONLY ANSWER WITH 'True' OR 'False'"
while True:
try:
response = claude_funcs.invoke_claude_3(prompt, max_tokens=10)
return response.strip()
except Exception as e:
print(f"Error occurred: {e}. Waiting for 60 seconds before retrying...")
time.sleep(60)
def label_services(filepath, carveout_list, output_filepath):
# Read the CSV file
df = pd.read_csv(filepath)
# Select the first 50 rows of the DataFrame
df = df.head(500)
# Define primary service terms
primary_terms = [
"Covered Services",
"Inpatient Services",
"Outpatient Services",
"Physician Services",
"Inpatient",
"Outpatient",
"Physician",
"Medical",
"Surgical",
"Diagnostic",
"Therapeutic",
]
# Initialize columns for the results
df["carveout_matched"] = ""
df["label"] = ""
df["IS_CARVEOUT"] = ""
# Initialize a nested dictionary to track occurrences for each Filename and TD_LOB
occurrence_tracker = {}
carveout_count = 0
# Iterate through the rows in the DataFrame
for index, row in df.iterrows():
service = str(row["SERVICE"])
prov_type = str(row["PROV_TYPE"]) if pd.notna(row["PROV_TYPE"]) else ""
td_lob = row["TD_LOB"]
filename = row["Filename"]
# Initialize the nested dictionary for the filename and TD_LOB if not present
if filename not in occurrence_tracker:
occurrence_tracker[filename] = {}
if td_lob not in occurrence_tracker[filename]:
occurrence_tracker[filename][td_lob] = {term: 0 for term in primary_terms}
# Check if the service is a primary or similar to a primary
primary = get_closest_substring_match(service, primary_terms)
if primary:
# Ensure the primary term is initialized in the occurrence tracker
if primary not in occurrence_tracker[filename][td_lob]:
occurrence_tracker[filename][td_lob][primary] = 0
# Determine the label based on the count of this primary term for the given Filename and TD_LOB
count = occurrence_tracker[filename][td_lob][primary]
if count == 0:
df.at[index, "label"] = "primary"
elif count == 1:
df.at[index, "label"] = "secondary"
elif count == 2:
df.at[index, "label"] = "tertiary"
else:
df.at[index, "label"] = "additional"
occurrence_tracker[filename][td_lob][primary] += 1
df.at[index, "IS_CARVEOUT"] = "N"
else:
# If PROV_TYPE is blank, continue with carveout determination
best_carveout_response = get_best_carveout_from_claude(
carveout_list, service
)
matched_carveout = best_carveout_response # Use the model response directly
df.at[index, "carveout_matched"] = matched_carveout
if matched_carveout:
# Check similarity between PROV_TYPE and carveout
prov_type_similarity_score = SequenceMatcher(
None, prov_type, matched_carveout
).ratio()
if prov_type_similarity_score < 0.8:
prov_type_similarity = check_prov_type_similarity(
prov_type, matched_carveout
)
else:
prov_type_similarity = "True"
if re.search(r"\b(True|Yes)\b", prov_type_similarity, re.IGNORECASE):
if matched_carveout not in occurrence_tracker[filename][td_lob]:
print(
"claude returned a match between provider type and service"
)
occurrence_tracker[filename][td_lob][matched_carveout] = 0
count = occurrence_tracker[filename][td_lob][matched_carveout]
if count == 0:
df.at[index, "label"] = "primary"
elif count == 1:
df.at[index, "label"] = "secondary"
elif count == 2:
df.at[index, "label"] = "tertiary"
else:
df.at[index, "label"] = "additional"
occurrence_tracker[filename][td_lob][matched_carveout] += 1
df.at[index, "IS_CARVEOUT"] = "N"
else:
df.at[index, "IS_CARVEOUT"] = "Y"
else:
df.at[index, "IS_CARVEOUT"] = "Y"
carveout_count += 1
# Save the updated DataFrame to a new CSV file after every 10 carveouts
if carveout_count % 10 == 0:
df.to_csv(output_filepath, index=False)
print(f"Saved progress after processing {carveout_count} carveouts.")
# Final save of the updated DataFrame to a new CSV file
df.to_csv(output_filepath, index=False)
label_services("service.csv", prompts.get_carveout_list(), "carveouts50.csv")
@@ -1,228 +0,0 @@
import concurrent.futures
import traceback
import csv
import time
import sys
import random
random.seed(42)
import pandas as pd
import os
import re
import utils
import config
import claude_funcs
import file_processing
import tracking
import consolidate_output
import top_down_funcs
import valid
import ac_funcs
import preprocess
import prompts
import postprocess
import postprocessing_funcs
import preprocessing_funcs
individual_path = "output_individual/cnc_3_ac1"
clean_path = "reference_files/CNC-Batch 3 Output_File1.xlsx"
AC_DICT = {
"CONTRACT_EFFECTIVE_DT": "What is the contract effective date mentioned in any of the following locations: the signatory section or the preamble of the agreement, or the start of the amendment ? Return this date converted to YYYY-MM-DD format.",
"PROV_GROUP_NPI": "What is the group provider's national provider identifier number mentioned in the contract? This may be found on the signature page or on the roster. It consists of 10 digits. Only return these 10 digits.",
"PROV_GROUP_TIN": "What is the Group provider's taxpayer identification number stated in the contract? This is a 9 digit long number typically with a hyphen after the first 2 digits. Return only this 9 digit number with a hyphen after the first 2 digits. Do not elaborate or add context.",
}
combined_mapping = {
"Contract_Name": "Filename",
"Agreement_Name_Contract_Title": "CONTRACT_TITLE",
"Payer_Name": "PAYER_NAME",
"Health_Plan_State": "HEALTH_PLAN_STATE",
"Affiliate_Y_N": "AFFILIATION_CLAUSE_IND",
"Credentialing_Application_Indicator": "CREDENTIALING_APP_IND",
"Term_Clause": "TERM_CLAUSE",
"Contract_Auto_Renewal_Indicator": "CONTRACT_AUTO_RENEWAL_IND",
"Termination_Date": "CONTRACT_TERMINATION_DT",
"Termination_Upon_Notice_Days": "TERMINATION_UPON_NOTICE",
"Termination_With_Cause_Days": "TERMINATION_UPON_CAUSE",
"Amend_Contract_Upon_notice_Flag_Y_N": "AMEND_CONTRACT_NOTICE_IND",
"Timeframe_to_Object_Days": "TIME_TO_OBJECT",
"Assignments_Clause_Y_N": "ASSIGNMENTS_CLAUSE_IND",
"Contract_Effective_Date": "CONTRACT_EFFECTIVE_DT",
"IRS_Num": "PROV_GROUP_TIN",
"PROV_GROUP_NAME": "PROV_GROUP_NAME",
"NPI_10_digits": "PROV_GROUP_NPI",
"NPI_NAME": "NPI_NAME",
"PROV_GROUP_TIN_SIGNATORY": "PROV_GROUP_TIN_SIGNATORY",
"PROV_TIN_OTHER": "PROV_TIN_OTHER",
"PROV_NPI_OTHER": "PROV_NPI_OTHER",
"Notice_to_Provider_Name": "NOTICE_PROVIDER_NAME",
"Notice_to_Provider_Address": "NOTICE_PROVIDER_ADDRESS",
"Sequestration_Language": "SEQUESTRATION_LANGUAGE",
"Sequestration_Reductions_included_Medicare_only_Y_N": "SEQUESTRATION_REDUCTIONS_IND",
"Parent_Agreement_Code": "Parent Agreement Code",
"Pages": "Pages",
"Page_num": "page_num",
"Attachment_Exhibit": "EXHIBIT",
"Line_of_Business": "CONTRACT_LOB",
"Provider_Type": "PROV_TYPE",
"Provider_Type_Level_2": "PROV_TYPE_LEVEL_2",
"IP_OP": "IP_OP",
"Service_Type": "FULL_SERVICE",
"Plan_Type": "CONTRACT_PROGRAM",
"Lesser_of_Logic_Language_included_Y_N": "LESSER",
"Lesser_of_Rate": "LESSER_RATE",
"Reimb_Methodology": "FULL_METHODOLOGY",
"Reimb_Methodology_short": "SHORT_METHODOLOGY",
"If_rate_is_percent_of_Payer_or_MCR_STANDARD": "RATE_STANDARD",
"If_rate_is_percent_of_Payer_or_MCR_STANDARD_Short": "RATE_SHORT",
"Flat_Fee": "FLAT_FEE_STANDARD",
"Default_Term": "DEFAULT_TERM",
"Default_Rate": "DEFAULT_RATE",
"Inclusion_of_essential_RBRVS_Fee_Source_Language_Y_N": 'Inclusion of essential RBRVS "Fee Source" Language (Y/N)',
"CDM_Language_included_Y_N": "CDM_IND",
"CHARGEMASTER": "CHARGEMASTER",
"IP_DSH_IME_UC_included_Y_N": "IP - DSH/IME/UC, included (Y/N)",
"IP_Stoploss_Catastrophic_Threshold": "IP - Stoploss Catastrophic Threshold",
"EXCLUSIONS": "EXCLUSIONS",
"Not_to_Exceed": "NOT_TO_EXCEED",
"Escalator_or_COLA_Y_N": "RATE_ESCALATOR_IND",
"Escalator_I_Eff_Date": "RATE_ESCALATOR_DT",
}
import ac_funcs
import config
import postprocess
import tracking
def ac_postprocess(final_df):
final_df = postprocessing_funcs.clean_ac_fields(final_df)
final_df = final_df.rename(columns=valid.AC_MAPPING)
final_df = final_df[[col for col in valid.ABC_COLUMNS if col in final_df.columns]]
return final_df
def run_ac_prompts(file_object):
################## INITIATE PROCESSING ##################
filename, contract_text = file_object
print(f"Processing AC for {filename}...")
# print(f"Total contract word count: {len(contract_text.split())}")
################## PREPROCESS ##################
text_dict, exhibit_pages, num_pages, ac_chunks = preprocess.preprocess(
contract_text, filename, fields="ac"
)
# print(f"AC Preprocessing Complete - {filename}")
################## RUN FULL CONTEXT PROMPTS ##################
ac_dict = {}
log_data = []
# Process 'full_context' fields together
full_context_fields = [field for field in AC_DICT]
full_context_questions = AC_DICT
if full_context_questions:
full_context_prompt = ac_funcs.create_prompt(
contract_text[0 : min(len(contract_text), 600000)],
question=full_context_questions,
)
# print(f"High accuracy prompt word count: {len(high_accuracy_prompt.split())}")
try:
full_context_answers = ac_funcs.get_ac_answer(
full_context_prompt,
filename,
fields=list(full_context_questions.keys()),
)
ac_dict.update(full_context_answers)
for field, answer in full_context_answers.items():
log_data.append(
{
"Field": field,
"Prompt": f"Question: {full_context_questions[field]}\n\nContext: {contract_text}", # Include full context
"Context Type": "Full Context (High Accuracy)",
"Response": answer,
}
)
except Exception as e:
# print(f"Error processing high accuracy fields: {str(e)}")
for field in full_context_questions.keys():
ac_dict[field] = f"Error: {str(e)}"
log_data.append(
{
"Field": field,
"Prompt": f"Question: {full_context_questions[field]}\n\nContext: {contract_text}", # Include full context
"Context Type": "Full Context (High Accuracy)",
"Response": f"Error: {str(e)}",
}
)
################## CREATE OUTPUT DIRECTORIES ##################
base_filename = os.path.splitext(filename)[0].strip()
output_dir = os.path.join(individual_path, base_filename)
os.makedirs(output_dir, exist_ok=True)
################## POSTPROCESS ##################
ac_dict["Contract Name"] = filename
ac_df = pd.DataFrame([ac_dict])
ac_df = ac_postprocess(ac_df)
################## WRITE TO OUTPUT ##################
ac_df.to_csv(f"{output_dir}/ac_output.csv", index=False)
print(f"AC Output written: {filename}")
def process_ac_adhoc(item):
filename, contract_text = item
try:
results = run_ac_prompts(item)
if results is not None:
tracking.update_results_csv(item, results)
return item, results
except Exception as e:
print(f"Error processing item {filename}: {e}")
traceback.print_exc()
########################## Process Starts Here ##########################
# Clean
abc = pd.read_excel(clean_path)
abc.rename(columns=combined_mapping, inplace=True)
abc_names = list(abc["Contract Name"].unique())
abc_dicts = abc.to_dict(orient="records")
# Input
input_dict = utils.read_input("data_cnc/batch3A")
# Current Output
output_dirs = list(os.listdir(individual_path))
# Run only files that are not in abc, are in input, and are not in output
input_dict = {
k: v
for k, v in input_dict.items()
if k.split(".txt")[0] not in abc_names and k.split(".txt")[0] not in output_dirs
}
print("Input dict to run: ", len(input_dict))
with concurrent.futures.ThreadPoolExecutor(max_workers=config.MAX_WORKERS) as executor:
if "a" in config.FIELDS and "c" in config.FIELDS:
futures = [
executor.submit(process_ac_adhoc, item) for item in input_dict.items()
]
for future in concurrent.futures.as_completed(futures):
try:
result = future.result()
except Exception as e:
print(f"Error in future: {e}")
traceback.print_exc()
# for item in input_dict.items():
# process_ac_adhoc(item)
-584
View File
@@ -1,584 +0,0 @@
import concurrent.futures
import traceback
import csv
import time
import sys
import random
random.seed(42)
import pandas as pd
import os
import re
import utils
import config
import claude_funcs
import file_processing
import tracking
import consolidate_output
import top_down_funcs
import valid
import ac_funcs
import preprocess
import prompts
import postprocess
import postprocessing_funcs
import preprocessing_funcs
# Ad-Hoc runs of sections of the script
"""
Batch 1 and 2
Fields to Run:
AC
- All Chunked Fields
- All Part 2 Fields
- Postprocess AC
B
- All B Postprocessing
750 additional
- Run all ABC
"""
dup_mapping = {
"Payer Name": "PAYER_NAME",
"IRS Name": "IRS_Name",
"NPI NAME": "NPI_NAME",
"Sequestration Reductions, included [Medicare only] (Y/N)": "SEQUESTRATION_REDUCTIONS_IND",
"Page_num": "page_num",
"Lesser of Logic Language, included (Y/N)": "LESSER",
"Reimb. Methodology_short": "SHORT_METHODOLOGY",
"If rate is % of Payer or MCR [STANDARD]": "RATE_STANDARD",
"If rate is % of Payer or MCR [STANDARD]_Short": "RATE_SHORT",
"Flat Fee": "FLAT_FEE_STANDARD",
"CDM Language, included (Y/N)": "CDM_IND",
"CHARGEMASTER": "CONTRACT_CHARGEMASTER_PROTECTION_LANGUAGE",
"EXCLUSIONS": "Exclusions",
}
combined_mapping = {
"Contract_Name": "Filename",
"Agreement_Name_Contract_Title": "CONTRACT_TITLE",
"Payer_Name": "PAYER_NAME",
"Health_Plan_State": "HEALTH_PLAN_STATE",
"Affiliate_Y_N": "AFFILIATION_CLAUSE_IND",
"Credentialing_Application_Indicator": "CREDENTIALING_APP_IND",
"Term_Clause": "TERM_CLAUSE",
"Contract_Auto_Renewal_Indicator": "CONTRACT_AUTO_RENEWAL_IND",
"Termination_Date": "CONTRACT_TERMINATION_DT",
"Termination_Upon_Notice_Days": "TERMINATION_UPON_NOTICE",
"Termination_With_Cause_Days": "TERMINATION_UPON_CAUSE",
"Amend_Contract_Upon_notice_Flag_Y_N": "AMEND_CONTRACT_NOTICE_IND",
"Timeframe_to_Object_Days": "TIME_TO_OBJECT",
"Assignments_Clause_Y_N": "ASSIGNMENTS_CLAUSE_IND",
"Contract_Effective_Date": "CONTRACT_EFFECTIVE_DT",
"IRS_Num": "PROV_GROUP_TIN",
"PROV_GROUP_NAME": "PROV_GROUP_NAME",
"NPI_10_digits": "PROV_GROUP_NPI",
"NPI_NAME": "NPI_NAME",
"PROV_GROUP_TIN_SIGNATORY": "PROV_GROUP_TIN_SIGNATORY",
"PROV_TIN_OTHER": "PROV_TIN_OTHER",
"PROV_NPI_OTHER": "PROV_NPI_OTHER",
"Notice_to_Provider_Name": "NOTICE_PROVIDER_NAME",
"Notice_to_Provider_Address": "NOTICE_PROVIDER_ADDRESS",
"Sequestration_Language": "SEQUESTRATION_LANGUAGE",
"Sequestration_Reductions_included_Medicare_only_Y_N": "SEQUESTRATION_REDUCTIONS_IND",
"Parent_Agreement_Code": "Parent Agreement Code",
"Pages": "Pages",
"Page_num": "page_num",
"Attachment_Exhibit": "EXHIBIT",
"Line_of_Business": "CONTRACT_LOB",
"Provider_Type": "PROV_TYPE",
"Provider_Type_Level_2": "PROV_TYPE_LEVEL_2",
"IP_OP": "IP_OP",
"Service_Type": "FULL_SERVICE",
"Plan_Type": "CONTRACT_PROGRAM",
"Lesser_of_Logic_Language_included_Y_N": "LESSER",
"Lesser_of_Rate": "LESSER_RATE",
"Reimb_Methodology": "FULL_METHODOLOGY",
"Reimb_Methodology_short": "SHORT_METHODOLOGY",
"If_rate_is_percent_of_Payer_or_MCR_STANDARD": "RATE_STANDARD",
"If_rate_is_percent_of_Payer_or_MCR_STANDARD_Short": "RATE_SHORT",
"Flat_Fee": "FLAT_FEE_STANDARD",
"Default_Term": "DEFAULT_TERM",
"Default_Rate": "DEFAULT_RATE",
"Inclusion_of_essential_RBRVS_Fee_Source_Language_Y_N": 'Inclusion of essential RBRVS "Fee Source" Language (Y/N)',
"CDM_Language_included_Y_N": "CDM_IND",
"CHARGEMASTER": "CHARGEMASTER",
"IP_DSH_IME_UC_included_Y_N": "IP - DSH/IME/UC, included (Y/N)",
"IP_Stoploss_Catastrophic_Threshold": "IP - Stoploss Catastrophic Threshold",
"EXCLUSIONS": "EXCLUSIONS",
"Not_to_Exceed": "NOT_TO_EXCEED",
"Escalator_or_COLA_Y_N": "RATE_ESCALATOR_IND",
"Escalator_I_Eff_Date": "RATE_ESCALATOR_DT",
}
AC_DICT = {
"ACCESS_TO_MEDICAL_RECORDS": "Access clause contains information regarding access to medical records. It may be found in section 4.2 of the contract. Extract the Access clause present in the contract. If it is not present, answer N/A.",
"CARVEOUT_VENDORS": "Carve-Out Vendors clause may be found in section 2.9 of the contract. Extract the Carve-Out Vendors clause present in the contract. If it is not present, answer N/A.",
"CLAIMS_EDITING_LANGUAGE": "Claims Editing Language is typically found in the claims section in the beginning parts of the contract. Extract Claims Editing Language present in the contract. If it is not present, answer N/A.",
"CLEAN_CLAIM": 'Extract the "Clean Claim" subsection present in definition section of the contract. If it is not present, answer N/A.',
"CONFLICTS_BETWEEN_CERTAIN_DOCUMENTS_LANGUAGE": "Extract the Conflicts Between Certain Documents clause present in the contract. If it is not present, answer N/A.",
"COST_SETTLEMENT_LANGUAGE": "Cost settlement language may be present in disputes and arbitration subsection of the contract. It usually contains keywords like settlementor cost settlement. Extract the cost settlement language present in the contract. If it is not present, answer N/A.",
"DEEMER_AMENDMENT": "Deemer Amendment clause may be found in section 8.7.2 of the contract. Extract the Deemer Amendment clause present in the contract. If it is not present, answer N/A.",
"DELEGATED_TERMS": "Extract Delegated Terms present in the contract. If it is not present, answer N/A.",
"ECM": 'What is the "ECM" number mentioned in the contract? Only Answer the ECM number, do not provide any context. Do not provide ICM number. If ECM number is not present, answer N/A.',
"EXCLUSIVITY_REQUIREMENT_LANGUAGE": "Extract Exclusivity requirement clause present in the contract. If it is not present, answer N/A.",
"GUARANTEE_OF_PROVIDER_YIELD_LANGUAGE": "Extract Guarantee of Provider Yield Language present in the contract. If it is not present, answer N/A.",
"HCBS_SERVICES": 'Extract HCBS services clause present either in the compensation or exhibit section of the contract. It may contain keywords like "for covered HCBS services, plan shall pay". Do not include definitions of HCBS. If it is not present, answer N/A.',
"INDEMNIFICATION": "Indemnification clause may be found in section 5.2 of the contract. Extract the Indemnification clause present in the contract. If it is not present, answer N/A.",
"INDEPENDENT_REVIEW_LANGUAGE": "Extract Independent Review language present in the contract. If it is not present, answer N/A.",
"INVOICE_PRICING_LANGUAGE": 'Extract table with heading "INVOICED SERVICES" if it is present in compensation schedule of the contract. If it is not present, answer N/A',
"LATE_PAID_CLAIMS_LANGUAGE": "Extract Late Paid Claims language present in the contract. This will be a sentence or paragraph describing late paid claims, interest payments or similar. Here is an example of a correct response: 'Any Clean Claim, as defined in 42 C.F.R. § 422.500, shall be paid within thirty (30) days of receipt by Plan at such address as may be designated by Plan, and Plan shall pay interest on any Clean Claim not paid within thirty (30) days of such receipt by Plan at the rate of interest required by law, or as otherwise set forth in the Provider Manual.'. Here is an example of an incorrect response: 'Any Clean Claim, as defined in 42 C.F.R. 422.500, shall be paid within thirty (30) days of receipt by Health Plan, Payor or (if Provider contracts with Downstream Entities) Provider, as applicable, as designated by Provider or such Downstream Entity, as applicable.'. If no valid answer is present, answer N/A.",
"MEMBER_CONFINEMENT_DAYS_LANGUAGE": "Extract Member Confinement Days Language present in the contract. If it is not present, answer N/A.",
"NATIONAL_AGREEMENT_IND": "Are more than one states covered in the contract? Answer with a Yes or No, do not provide any other context.",
"NETWORK_ACCESS_FEES_LANGUAGE": 'Extract "Network Access Fee" verbiage present in the contract. If it is not present, answer N/A.',
"NONSTANDARD_APPEALS_PROCESS_LANGUAGE": "Extract Nonstandard Appeals Process language present in the contract. If it is not present, answer N/A.",
"PAYMENT_IN_ADVANCE_OF_CLAIMS_SUBMISSION_LANGUAGE": "Extract Payment in Advance of Claims Submission language present in the contract. If it is not present, answer N/A.",
"PAYOR": '"Payor" is usually found in section 1.12 of the contract. Extract the "Payor" subsection present in definition section of the contract. If it is not present, answer N/A.',
"PMPM": "What is the PMPM rate mentioned in the contract? Only return the rate. do not provide any context. If it is not present, answer N/A.",
"PREAUTHORIZATION": "Preauthorization clause may be found in section 2.7 of the contract. Extract the Preauthorization clause present in the contract. If it is not present, answer N/A.",
"PRODUCT_REMOVAL": "Product removal may be found in the products attachment, stating a product is being removed. Extract product removal language present in the contract. If it is not present, answer N/A.",
"PROVIDER_BASED_BILLING_EXCLUSION_LANGUAGE": "Provider-based Billing Exclusion is often listed under the Additional Provisions section. Extract the Provider-based Billing Exclusion language mentioned in the contract. If it is not present, answer N/A.",
"RECOVERY_RIGHTS": "Recovery Rights clause may be found in section 3.5 of the contract. Extract the entire Recovery Rights sentence or paragraph present in the contract. If it is not present, answer N/A.",
"TEMPLATE": "Is this contract in standard Centene template containing definition, terms clauses and other standard sections. Answer with a Yes or No, do not provide any other context.",
"ARBITRATION_AND_DISPUTES": "Dispute Resolution clause is usually found in section 6.1 of the contract. Arbitration clause is usually found in section 6.2 of the contract. Extract both the Dispute Resolution and the arbitration clause present in the contract. In case one of them is present, extract that one. If none of them are present, answer N/A.",
"DISPARAGEMENT_PROHIBITION_IND": "Is Disparagement Prohibition clause present in the contract? It may be found in section 2.1 of the contract. Answer with a Yes or No, do not provide any other context.",
"DISPARAGEMENT_PROHIBITION_LANGUAGE": "Disparagement Prohibition clause may be found in section 2.1 of the contract. Extract the Disparagement Prohibition clause present in the contract. If it is not present, answer N/A.",
"ELIGIBILITY_VERIFICATION": "Eligibility Verification or Determination clause may be found in section 2.6 of the contract. Extract the Eligibility Verification or Determination clause present in the contract. If it is not present, answer N/A.",
"INSURANCE_REQUIREMENT": "Extract Insurance requirement mentioned in the contract. If it is not present, answer N/A.",
"PARTICIPATION_IN_PRODUCTS": "Participation in Products clause may be found in section 2.2.2 of the contract. Extract the Participation in Products clause present in the contract. If it is not present, answer N/A.",
"POLICIES_AND_PROCEDURES": "Extract the entire Policies and Procedures sentence or paragraph present in the contract. Do not extract language about conflicts and construction. Do not extract language about PP (Preferred Provider) nor PPG (Preferred Provider Group). If it is not present, answer N/A.",
"REGULATORY_REQUIREMENTS": 'Extract the entire Regulatory Requirements paragraph present in the contract. It usually contains the keyword "Regulatory Requirements". If it is not present, answer N/A.',
"RELATIONSHIP_OF_PARTIES_LANGUAGE": "Extract the entire Relationship of Parties passage present in the contract. If it is not present, answer N/A.",
"TERM_CLAUSE": """Extract the subsection or paragraph labeled Term. It may also be labeled Termination. If there are no such sections, extract the paragraph the most closely resembles this label. If there is still no correct answer, simply return 'N/A'.""",
"LATE_PAID_CLAIMS_LANGUAGE": "Extract Late Paid Claims language present in the contract. This will be a sentence or paragraph describing late paid claims, interest payments or similar. Here is an example of a correct response: 'Any Clean Claim, as defined in 42 C.F.R. § 422.500, shall be paid within thirty (30) days of receipt by Plan at such address as may be designated by Plan, and Plan shall pay interest on any Clean Claim not paid within thirty (30) days of such receipt by Plan at the rate of interest required by law, or as otherwise set forth in the Provider Manual.'. Here is an example of an incorrect response: 'Any Clean Claim, as defined in 42 C.F.R. 422.500, shall be paid within thirty (30) days of receipt by Health Plan, Payor or (if Provider contracts with Downstream Entities) Provider, as applicable, as designated by Provider or such Downstream Entity, as applicable.'. If no valid answer is present, answer N/A.",
"NON_RENEWAL_LANGUAGE": "Extract the section of the contract that describes non-renewal obligations, especially the number of days notice that must be given. Write the full relevant sentence or paragraph. Ensure that the exact term 'non-renewal' is present in the answer. Do NOT return an answer that does not have 'non-renewal'.",
"HCBS_SERVICES": 'Extract HCBS services clause present either in the compensation or exhibit section of the contract. It may contain keywords like "for covered HCBS services, plan shall pay". Do not include definitions of HCBS. If it is not present, answer N/A.',
}
KEYWORD_MAPPINGS = {
"TERM_CLAUSE": {
"methodology": "hierarchy",
"keywords": ["Term", "term", "duration", "agreement period"],
},
"LATE_PAID_CLAIMS_LANGUAGE": {
"methodology": "hierarchy",
"keywords": [
"late paid",
"late payment",
"interest",
"late fee",
"interest shall be paid",
"interest payment",
],
},
"NON_RENEWAL_LANGUAGE": {
"methodology": "or",
"keywords": ["not to renew", "non-renewal", "non renewal", "nonrenewal"],
},
# "NON_RENEWAL_DAYS": {'methodology': 'or', 'keywords': ["not to renew", "non-renewal", "non renewal", "nonrenewal"]},
"HCBS_SERVICES": {
"methodology": "hierarchy",
"keywords": ["%", "Plan shall pay", "hcbs"],
},
"POLICIES_AND_PROCEDURES": {
"methodology": "or",
"keywords": ["2.4", "policies", "procedures"],
},
"REGULATORY_REQUIREMENTS": {
"methodology": "or",
"keywords": ["8.3", "6.4", "regulatory requirements", "regulatory"],
},
"RECOVERY_RIGHTS": {"methodology": "or", "keywords": ["3.5", "recovery rights"]},
"RELATIONSHIP_OF_PARTIES_LANGUAGE": {
"methodology": "or",
"keywords": ["8.1", "relationship of parties"],
},
}
def run_ac_prompts(file_object):
################## INITIATE PROCESSING ##################
filename, contract_text = file_object
print(f"Processing AC for {filename}...")
# print(f"Total contract word count: {len(contract_text.split())}")
################## PREPROCESS ##################
text_dict, exhibit_pages, num_pages, ac_chunks = preprocess.preprocess(
contract_text, filename, fields="ac"
)
# print(f"AC Preprocessing Complete - {filename}")
################## RUN FULL CONTEXT PROMPTS ##################
ac_dict = {}
log_data = []
# Process 'full_context' fields together
full_context_fields = [field for field in AC_DICT if field not in KEYWORD_MAPPINGS]
full_context_questions = {
field: prompts.AC_DICT.get(field) for field in full_context_fields
}
if full_context_questions:
full_context_prompt = ac_funcs.create_prompt(
contract_text[0 : min(len(contract_text), 600000)],
question=full_context_questions,
)
# print(f"High accuracy prompt word count: {len(high_accuracy_prompt.split())}")
try:
full_context_answers = ac_funcs.get_ac_answer(
full_context_prompt,
filename,
fields=list(full_context_questions.keys()),
)
ac_dict.update(full_context_answers)
for field, answer in full_context_answers.items():
log_data.append(
{
"Field": field,
"Prompt": f"Question: {full_context_questions[field]}\n\nContext: {contract_text}", # Include full context
"Context Type": "Full Context (High Accuracy)",
"Response": answer,
}
)
except Exception as e:
# print(f"Error processing high accuracy fields: {str(e)}")
for field in full_context_questions.keys():
ac_dict[field] = f"Error: {str(e)}"
log_data.append(
{
"Field": field,
"Prompt": f"Question: {full_context_questions[field]}\n\nContext: {contract_text}", # Include full context
"Context Type": "Full Context (High Accuracy)",
"Response": f"Error: {str(e)}",
}
)
################## RUN CHUNKED PROMPTS ##################
all_fields = set(prompts.AC_DICT.keys())
chunked_fields = [field for field in KEYWORD_MAPPINGS]
for field in chunked_fields:
if field in prompts.AC_DICT:
question = prompts.AC_DICT[field]
else:
# print(f"Field {field} not found in prompts. Skipping...")
continue
# Use chunk if available and different from full context, otherwise use full context
if (
field in ac_chunks
and ac_chunks[field].strip()
and ac_chunks[field] != contract_text
):
context = ac_chunks[field]
context_type = "Smart Chunking"
else:
context = contract_text
context_type = "Full Context"
prompt = prompts.AC_SINGLE_FIELD_TEMPLATE(context, question)
print(
f"Field {field} prompt word count: {len(prompt.split())} ({context_type})"
)
try:
field_answer = claude_funcs.invoke_claude(
prompt, config.MODEL_ID_CLAUDE35_SONNET, filename, 8192
)
ac_dict[field] = field_answer
log_data.append(
{
"Field": field,
"Prompt": f"Question: {question}\n\nContext: {context}", # Include full context
"Context Type": context_type,
"Response": field_answer,
}
)
except Exception as e:
# print(f"Error processing field {field}: {str(e)}")
ac_dict[field] = f"Error: {str(e)}"
log_data.append(
{
"Field": field,
"Prompt": f"Question: {question}\n\nContext: {context}", # Include full context
"Context Type": context_type,
"Response": f"Error: {str(e)}",
}
)
################## AC CONDITIONAL PROMPTS ##################
## Non-Renewal - Days
nrd_prompt = prompts.AC_SINGLE_FIELD_TEMPLATE(
ac_dict["NON_RENEWAL_LANGUAGE"], prompts.AC_DICT["NON_RENEWAL_DAYS"]
)
nrd_answer = claude_funcs.invoke_claude(
nrd_prompt, config.MODEL_ID_CLAUDE35_SONNET, filename, 8192
)
ac_dict["NON_RENEWAL_DAYS"] = nrd_answer
################## ENSURE ALL FIELDS ARE PRESENT ##################
ac_dict = {k: v for k, v in ac_dict.items() if k in valid.AC_MAPPING}
for field in all_fields:
if field not in ac_dict:
# print(f"Field not found in results. {field}")
# ac_dict[field] = "N/A"
log_data.append(
{
"Field": field,
"Prompt": "N/A",
"Context Type": "N/A",
"Response": "N/A",
}
)
################## CREATE OUTPUT DIRECTORIES ##################
base_filename = os.path.splitext(filename)[0].strip()
output_dir = os.path.join(config.OUTPUT_DIRECTORY, base_filename)
os.makedirs(output_dir, exist_ok=True)
################## POSTPROCESS ##################
ac_dict["Contract Name"] = filename
ac_df = pd.DataFrame([ac_dict])
ac_df = postprocess.ac_postprocess(ac_df)
################## WRITE TO OUTPUT ##################
ac_df.to_csv(f"{output_dir}/ac_output.csv", index=False)
print(f"AC Output written: {filename}")
################## WRITE LOG TO CSV ##################
# log_file_path = os.path.join(output_dir, f"{base_filename}_prompt_log.csv")
# with open(log_file_path, 'w', newline='', encoding='utf-8') as log_file:
# fieldnames = ['Field', 'Prompt', 'Context Type', 'Response']
# writer = csv.DictWriter(log_file, fieldnames=fieldnames)
# writer.writeheader()
# writer.writerows(log_data)
# print(f"Prompt log written to {log_file_path}")
return ac_dict
def process_ac_adhoc(item):
filename, contract_text = item
try:
results = run_ac_prompts(item)
if results is not None:
tracking.update_results_csv(item, results)
return item, results
except Exception as e:
print(f"Error processing item {filename}: {e}")
traceback.print_exc()
def preprocess_ad_hoc(contract_text, filename, fields=config.FIELDS):
contract_text = preprocessing_funcs.clean_newlines(contract_text)
contract_text = preprocessing_funcs.clean_law_symbols(contract_text)
text_dict = preprocessing_funcs.split_text(
contract_text
) # return a dictionary with keys - page_num (str), values as the page_text
num_pages = len(text_dict.keys()) - 1
text_dict = preprocessing_funcs.filter_quick_review(text_dict)
if fields == "b":
exhibit_pages = top_down_funcs.get_exhibit_pages(
text_dict, filename
) # All pages with exhibit headers
# text_dict = table_funcs.align_and_format_tables(text_dict, filename)
text_dict = preprocessing_funcs.chunk_consecutive(text_dict, exhibit_pages)
ac_chunks = ""
if fields == "ac":
exhibit_pages = []
ac_chunks = preprocessing_funcs.smart_chunk_ac(text_dict)
return text_dict, exhibit_pages, num_pages, ac_chunks
def get_add_on(final_dicts, text_dict):
add_on_dict = {}
for td_page in list(set([d["page_num"] for d in final_dicts])):
td_page = str(td_page)
# TD page is the original result top down page, it is not necessarily in the new text_dict.
# Find add_on_page = the key of the new text_dict that points to the correct page of the old text dict
if td_page in text_dict.keys():
add_on_page = td_page
else:
add_on_page = [
str(page) for page in text_dict.keys() if int(page) > int(td_page)
][0]
if not add_on_page:
add_on_dict[td_page] = {
"ADD_ON_REIMBURSEMENT_LANGUAGE": "N/A",
"ADD_ON_REIMBURSEMENT_IND": "N",
}
continue
print(f"TD Page: {td_page} | Add On Page: {add_on_page}")
ad = {}
add_on_prompt = f"## Page Start ## {text_dict[str(add_on_page)]} ## Page End ## . Extract the Add On Reimbursement language present in the page above. If there is no Add On language present, answer N/A. Write only the answer, with no additional commentary or explanation."
add_on_answer = claude_funcs.invoke_claude(
add_on_prompt, config.MODEL_ID_CLAUDE35_SONNET, "", 256
)
ad["ADD_ON_REIMBURSEMENT_LANGUAGE"] = add_on_answer
if "N/A" in ad["ADD_ON_REIMBURSEMENT_LANGUAGE"]:
ad["ADD_ON_REIMBURSEMENT_IND"] = "N"
else:
ad["ADD_ON_REIMBURSEMENT_IND"] = "Y"
add_on_dict[str(td_page)] = ad
return add_on_dict
def process_b_adhoc(item):
filename, contract_text = item
print(f"Processing {filename}")
merged_dicts = [d for d in abc_dicts if d["Filename"] + ".txt" == filename]
print(len(merged_dicts))
if len(merged_dicts) > 0:
text_dict, exhibit_pages, num_pages, ac_chunks = preprocess_ad_hoc(
contract_text, filename
)
print(f"Preprocessing {filename}")
# Exclusions
exclusion_dict = top_down_funcs.get_td_dict(
text_dict, filename, prompts.TOP_DOWN_EXCLUSIONS, valid.EXCLUSION_TERMS
)
final_dicts = top_down_funcs.add_exclusions(merged_dicts, exclusion_dict)
print(f"Exclusions - {filename}")
# Medically Necessary
medically_necessary_dict = top_down_funcs.get_td_dict(
text_dict,
filename,
prompts.TOP_DOWN_MEDICALLY_NECESSARY,
valid.VALID_MEDICALLY_NECESSARY,
)
medically_necessary_dict = postprocessing_funcs.filter_dict(
medically_necessary_dict,
pattern=re.compile(
"|".join(
re.escape(keyword) for keyword in valid.INVALID_MEDICALLY_NECESSARY
),
re.IGNORECASE,
),
)
final_dicts = top_down_funcs.add_medically_necessary(
final_dicts, medically_necessary_dict
)
print(f"Medically Necessary - {filename}")
# Add On
add_on_dict = get_add_on(final_dicts, text_dict)
print(f"Add On Results: {add_on_dict}")
print(f"Add On - {filename}")
for td_page in add_on_dict.keys():
for d in final_dicts:
if str(d["page_num"]) == str(td_page):
print("Add On Match")
d.update(add_on_dict[str(td_page)])
# print(len(final_dicts))
output_df = pd.DataFrame(final_dicts)
print(f"B finished -{filename} - output {output_df.shape}")
print(output_df.columns)
base_filename = os.path.splitext(filename)[0].strip()
output_dir = os.path.join(config.OUTPUT_DIRECTORY, base_filename)
os.makedirs(output_dir, exist_ok=True)
output_df.to_csv(f"{output_dir}/b_output.csv")
########################## Process Starts Here ##########################
individual_path = "output_individual/cnc_batch1_b"
clean_path = "reference_files/CNC-Batch 1 Output_File1.xlsx"
run_path = "reference_files/Batch 1_082024.xlsx"
dup_path = "reference_files/Batch 1 Duplicates_10092024.xlsx"
# Run List
run_list = pd.read_excel(run_path)
run_list = list(run_list[run_list.Status == "1A"]["Filename"])
print(f"Run List Length: {len(run_list)}")
# Input
input_dict = utils.read_input()
print(f"Input Dict Length: {len(input_dict)}")
# Current Output
output_dirs = list(os.listdir(individual_path))
print(f"Current Outputs: {len(output_dirs)}")
# Need to Run
input_dict = {
filename: page_text
for filename, page_text in input_dict.items()
if filename.split(".txt")[0] in run_list
} # and filename.split('.txt')[0] not in config.OUTPUT_DIRECTORY}
print(f"Need to Run: {len(input_dict)}")
# Clean ABC
abc = pd.read_excel(clean_path)
abc.rename(columns=combined_mapping, inplace=True)
abc_names = list(abc["Filename"].unique())
abc_dicts = abc.to_dict(orient="records")
print(f"ABC Clean File Length (B): {len(abc_names)}")
# # ABC Dups
abc_dups = pd.read_excel(dup_path)
abc_dups.rename(columns={v: k for k, v in valid.B_MAPPING.items()}, inplace=True)
abc_dups.rename(columns={v: k for k, v in valid.AC_MAPPING.items()}, inplace=True)
abc_dups.rename(columns=dup_mapping, inplace=True)
abc_dup_names = list(abc_dups["Filename"].unique())
print(f"ABC Dup File Length (B): {len(abc_dup_names)}")
# # Concat ABC with Dups
abc = pd.concat([abc, abc_dups], axis=0, ignore_index=True)
abc_names = list(abc["Filename"].unique())
abc_dicts = abc.to_dict(orient="records")
print(f"Final ABC File Length (B+Dups): {len(abc_names)}")
# Run only files that are in abc, are in input, and are not in output
input_dict = {
k: v for k, v in input_dict.items() if k.split(".txt")[0] in abc_dup_names
}
input_dict = {
k: v
for k, v in input_dict.items()
if k
== "04-3122933-Dentsply IH, Inc. dba Wellspect Healthcare-ICMProviderAgreement_72694(2).txt"
}
print("Input dict to run: ", len(input_dict))
# with concurrent.futures.ThreadPoolExecutor(max_workers=config.MAX_WORKERS) as executor:
# if 'a' in config.FIELDS and 'c' in config.FIELDS:
# futures = [executor.submit(process_ac_adhoc, item) for item in input_dict.items()]
# if 'b' in config.FIELDS:
# futures = [executor.submit(process_b_adhoc, item) for item in input_dict.items()]
# for future in concurrent.futures.as_completed(futures):
# try:
# result = future.result()
# except Exception as e:
# print(f"Error in future: {e}")
# traceback.print_exc()
for item in input_dict.items():
process_b_adhoc(item)
@@ -1,591 +0,0 @@
import concurrent.futures
import traceback
import csv
import time
import sys
import random
random.seed(42)
import pandas as pd
import os
import re
import utils
import config
import claude_funcs
import file_processing
import tracking
import consolidate_output
import top_down_funcs
import valid
import ac_funcs
import preprocess
import prompts
import postprocess
import postprocessing_funcs
import preprocessing_funcs
input_path = "data_cnc/batch3"
ac2_output_path = "output_individual/cnc_3_ac2"
b_output_path = "output_individual/cnc_3_b"
clean_path = "reference_files/CNC-Batch 3 Output_File1.xlsx"
run_path = "reference_files/Batch 3_091124.xlsx"
# Ad-Hoc runs of sections of the script
"""
Batch 1 and 2
Fields to Run:
AC
- All Chunked Fields
- All Part 2 Fields
- Postprocess AC
B
- All B Postprocessing
750 additional
- Run all ABC
"""
dup_mapping = {
"Payer Name": "PAYER_NAME",
"IRS Name": "IRS_Name",
"NPI NAME": "NPI_NAME",
"Sequestration Reductions, included [Medicare only] (Y/N)": "SEQUESTRATION_REDUCTIONS_IND",
"Page_num": "page_num",
"Lesser of Logic Language, included (Y/N)": "LESSER",
"Reimb. Methodology_short": "SHORT_METHODOLOGY",
"If rate is % of Payer or MCR [STANDARD]": "RATE_STANDARD",
"If rate is % of Payer or MCR [STANDARD]_Short": "RATE_SHORT",
"Flat Fee": "FLAT_FEE_STANDARD",
"CDM Language, included (Y/N)": "CDM_IND",
"CHARGEMASTER": "CONTRACT_CHARGEMASTER_PROTECTION_LANGUAGE",
"EXCLUSIONS": "Exclusions",
}
combined_mapping = {
"Contract_Name": "Filename",
"Agreement_Name_Contract_Title": "CONTRACT_TITLE",
"Payer_Name": "PAYER_NAME",
"Health_Plan_State": "HEALTH_PLAN_STATE",
"Affiliate_Y_N": "AFFILIATION_CLAUSE_IND",
"Credentialing_Application_Indicator": "CREDENTIALING_APP_IND",
"Term_Clause": "TERM_CLAUSE",
"Contract_Auto_Renewal_Indicator": "CONTRACT_AUTO_RENEWAL_IND",
"Termination_Date": "CONTRACT_TERMINATION_DT",
"Termination_Upon_Notice_Days": "TERMINATION_UPON_NOTICE",
"Termination_With_Cause_Days": "TERMINATION_UPON_CAUSE",
"Amend_Contract_Upon_notice_Flag_Y_N": "AMEND_CONTRACT_NOTICE_IND",
"Timeframe_to_Object_Days": "TIME_TO_OBJECT",
"Assignments_Clause_Y_N": "ASSIGNMENTS_CLAUSE_IND",
"Contract_Effective_Date": "CONTRACT_EFFECTIVE_DT",
"IRS_Num": "PROV_GROUP_TIN",
"PROV_GROUP_NAME": "PROV_GROUP_NAME",
"NPI_10_digits": "PROV_GROUP_NPI",
"NPI_NAME": "NPI_NAME",
"PROV_GROUP_TIN_SIGNATORY": "PROV_GROUP_TIN_SIGNATORY",
"PROV_TIN_OTHER": "PROV_TIN_OTHER",
"PROV_NPI_OTHER": "PROV_NPI_OTHER",
"Notice_to_Provider_Name": "NOTICE_PROVIDER_NAME",
"Notice_to_Provider_Address": "NOTICE_PROVIDER_ADDRESS",
"Sequestration_Language": "SEQUESTRATION_LANGUAGE",
"Sequestration Reductions, included [Medicare only] (Y/N)": "SEQUESTRATION_REDUCTIONS_IND",
"Parent_Agreement_Code": "Parent Agreement Code",
"Pages": "Pages",
"Page_num": "page_num",
"Attachment_Exhibit": "EXHIBIT",
"Line_of_Business": "CONTRACT_LOB",
"Provider_Type": "PROV_TYPE",
"Provider_Type_Level_2": "PROV_TYPE_LEVEL_2",
"IP_OP": "IP_OP",
"Service_Type": "FULL_SERVICE",
"Plan_Type": "CONTRACT_PROGRAM",
"Lesser_of_Logic_Language_included_Y_N": "LESSER",
"Lesser_of_Rate": "LESSER_RATE",
"Reimb_Methodology": "FULL_METHODOLOGY",
"Reimb_Methodology_short": "SHORT_METHODOLOGY",
"If rate is % of Payer or MCR [STANDARD]": "RATE_STANDARD",
"If rate is % of Payer or MCR [STANDARD]_Short": "RATE_SHORT",
"Flat Fee": "FLAT_FEE_STANDARD",
"Default_Term": "DEFAULT_TERM",
"Default_Rate": "DEFAULT_RATE",
"IP - DSH/IME/UC, included (Y/N)": 'Inclusion of essential RBRVS "Fee Source" Language (Y/N)',
"CDM_Language_included_Y_N": "CDM_IND",
"CHARGEMASTER": "CHARGEMASTER",
"IP_DSH_IME_UC_included_Y_N": "IP - DSH/IME/UC, included (Y/N)",
"IP_Stoploss_Catastrophic_Threshold": "IP - Stoploss Catastrophic Threshold",
"EXCLUSIONS": "EXCLUSIONS",
"Not_to_Exceed": "NOT_TO_EXCEED",
"Escalator_or_COLA_Y_N": "RATE_ESCALATOR_IND",
"Escalator_I_Eff_Date": "RATE_ESCALATOR_DT",
}
AC_DICT = {
"ACCESS_TO_MEDICAL_RECORDS": "Access clause contains information regarding access to medical records. It may be found in section 4.2 of the contract. Extract the Access clause present in the contract. If it is not present, answer N/A.",
"CARVEOUT_VENDORS": "Carve-Out Vendors clause may be found in section 2.9 of the contract. Extract the Carve-Out Vendors clause present in the contract. If it is not present, answer N/A.",
"CLAIMS_EDITING_LANGUAGE": "Claims Editing Language is typically found in the claims section in the beginning parts of the contract. Extract Claims Editing Language present in the contract. If it is not present, answer N/A.",
"CLEAN_CLAIM": 'Extract the "Clean Claim" subsection present in definition section of the contract. If it is not present, answer N/A.',
"CONFLICTS_BETWEEN_CERTAIN_DOCUMENTS_LANGUAGE": "Extract the Conflicts Between Certain Documents clause present in the contract. If it is not present, answer N/A.",
"COST_SETTLEMENT_LANGUAGE": "Cost settlement language may be present in disputes and arbitration subsection of the contract. It usually contains keywords like settlementor cost settlement. Extract the cost settlement language present in the contract. If it is not present, answer N/A.",
"DEEMER_AMENDMENT": "Deemer Amendment clause may be found in section 8.7.2 of the contract. Extract the Deemer Amendment clause present in the contract. If it is not present, answer N/A.",
"DELEGATED_TERMS": "Extract Delegated Terms present in the contract. If it is not present, answer N/A.",
"ECM": 'What is the "ECM" number mentioned in the contract? Only Answer the ECM number, do not provide any context. Do not provide ICM number. If ECM number is not present, answer N/A.',
"EXCLUSIVITY_REQUIREMENT_LANGUAGE": "Extract Exclusivity requirement clause present in the contract. If it is not present, answer N/A.",
"GUARANTEE_OF_PROVIDER_YIELD_LANGUAGE": "Extract Guarantee of Provider Yield Language present in the contract. If it is not present, answer N/A.",
"HCBS_SERVICES": 'Extract HCBS services clause present either in the compensation or exhibit section of the contract. It may contain keywords like "for covered HCBS services, plan shall pay". Do not include definitions of HCBS. If it is not present, answer N/A.',
"INDEMNIFICATION": "Indemnification clause may be found in section 5.2 of the contract. Extract the Indemnification clause present in the contract. If it is not present, answer N/A.",
"INDEPENDENT_REVIEW_LANGUAGE": "Extract Independent Review language present in the contract. If it is not present, answer N/A.",
"INVOICE_PRICING_LANGUAGE": 'Extract table with heading "INVOICED SERVICES" if it is present in compensation schedule of the contract. If it is not present, answer N/A',
"LATE_PAID_CLAIMS_LANGUAGE": "Extract Late Paid Claims language present in the contract. This will be a sentence or paragraph describing late paid claims, interest payments or similar. Here is an example of a correct response: 'Any Clean Claim, as defined in 42 C.F.R. § 422.500, shall be paid within thirty (30) days of receipt by Plan at such address as may be designated by Plan, and Plan shall pay interest on any Clean Claim not paid within thirty (30) days of such receipt by Plan at the rate of interest required by law, or as otherwise set forth in the Provider Manual.'. Here is an example of an incorrect response: 'Any Clean Claim, as defined in 42 C.F.R. 422.500, shall be paid within thirty (30) days of receipt by Health Plan, Payor or (if Provider contracts with Downstream Entities) Provider, as applicable, as designated by Provider or such Downstream Entity, as applicable.'. If no valid answer is present, answer N/A.",
"MEMBER_CONFINEMENT_DAYS_LANGUAGE": "Extract Member Confinement Days Language present in the contract. If it is not present, answer N/A.",
"NATIONAL_AGREEMENT_IND": "Are more than one states covered in the contract? Answer with a Yes or No, do not provide any other context.",
"NETWORK_ACCESS_FEES_LANGUAGE": 'Extract "Network Access Fee" verbiage present in the contract. If it is not present, answer N/A.',
"NONSTANDARD_APPEALS_PROCESS_LANGUAGE": "Extract Nonstandard Appeals Process language present in the contract. If it is not present, answer N/A.",
"PAYMENT_IN_ADVANCE_OF_CLAIMS_SUBMISSION_LANGUAGE": "Extract Payment in Advance of Claims Submission language present in the contract. If it is not present, answer N/A.",
"PAYOR": '"Payor" is usually found in section 1.12 of the contract. Extract the "Payor" subsection present in definition section of the contract. If it is not present, answer N/A.',
"PMPM": "What is the PMPM rate mentioned in the contract? Only return the rate. do not provide any context. If it is not present, answer N/A.",
"PREAUTHORIZATION": "Preauthorization clause may be found in section 2.7 of the contract. Extract the Preauthorization clause present in the contract. If it is not present, answer N/A.",
"PRODUCT_REMOVAL": "Product removal may be found in the products attachment, stating a product is being removed. Extract product removal language present in the contract. If it is not present, answer N/A.",
"PROVIDER_BASED_BILLING_EXCLUSION_LANGUAGE": "Provider-based Billing Exclusion is often listed under the Additional Provisions section. Extract the Provider-based Billing Exclusion language mentioned in the contract. If it is not present, answer N/A.",
"RECOVERY_RIGHTS": "Recovery Rights clause may be found in section 3.5 of the contract. Extract the entire Recovery Rights sentence or paragraph present in the contract. If it is not present, answer N/A.",
"TEMPLATE": "Is this contract in standard Centene template containing definition, terms clauses and other standard sections. Answer with a Yes or No, do not provide any other context.",
"ARBITRATION_AND_DISPUTES": "Dispute Resolution clause is usually found in section 6.1 of the contract. Arbitration clause is usually found in section 6.2 of the contract. Extract both the Dispute Resolution and the arbitration clause present in the contract. In case one of them is present, extract that one. If none of them are present, answer N/A.",
"DISPARAGEMENT_PROHIBITION_IND": "Is Disparagement Prohibition clause present in the contract? It may be found in section 2.1 of the contract. Answer with a Yes or No, do not provide any other context.",
"DISPARAGEMENT_PROHIBITION_LANGUAGE": "Disparagement Prohibition clause may be found in section 2.1 of the contract. Extract the Disparagement Prohibition clause present in the contract. If it is not present, answer N/A.",
"ELIGIBILITY_VERIFICATION": "Eligibility Verification or Determination clause may be found in section 2.6 of the contract. Extract the Eligibility Verification or Determination clause present in the contract. If it is not present, answer N/A.",
"INSURANCE_REQUIREMENT": "Extract Insurance requirement mentioned in the contract. If it is not present, answer N/A.",
"PARTICIPATION_IN_PRODUCTS": "Participation in Products clause may be found in section 2.2.2 of the contract. Extract the Participation in Products clause present in the contract. If it is not present, answer N/A.",
"POLICIES_AND_PROCEDURES": "Extract the entire Policies and Procedures sentence or paragraph present in the contract. Do not extract language about conflicts and construction. Do not extract language about PP (Preferred Provider) nor PPG (Preferred Provider Group). If it is not present, answer N/A.",
"REGULATORY_REQUIREMENTS": 'Extract the entire Regulatory Requirements paragraph present in the contract. It usually contains the keyword "Regulatory Requirements". If it is not present, answer N/A.',
"RELATIONSHIP_OF_PARTIES_LANGUAGE": "Extract the entire Relationship of Parties passage present in the contract. If it is not present, answer N/A.",
"TERM_CLAUSE": """Extract the subsection or paragraph labeled Term. It may also be labeled Termination. If there are no such sections, extract the paragraph the most closely resembles this label. If there is still no correct answer, simply return 'N/A'.""",
"LATE_PAID_CLAIMS_LANGUAGE": "Extract Late Paid Claims language present in the contract. This will be a sentence or paragraph describing late paid claims, interest payments or similar. Here is an example of a correct response: 'Any Clean Claim, as defined in 42 C.F.R. § 422.500, shall be paid within thirty (30) days of receipt by Plan at such address as may be designated by Plan, and Plan shall pay interest on any Clean Claim not paid within thirty (30) days of such receipt by Plan at the rate of interest required by law, or as otherwise set forth in the Provider Manual.'. Here is an example of an incorrect response: 'Any Clean Claim, as defined in 42 C.F.R. 422.500, shall be paid within thirty (30) days of receipt by Health Plan, Payor or (if Provider contracts with Downstream Entities) Provider, as applicable, as designated by Provider or such Downstream Entity, as applicable.'. If no valid answer is present, answer N/A.",
"NON_RENEWAL_LANGUAGE": "Extract the section of the contract that describes non-renewal obligations, especially the number of days notice that must be given. Write the full relevant sentence or paragraph. Ensure that the exact term 'non-renewal' is present in the answer. Do NOT return an answer that does not have 'non-renewal'.",
"HCBS_SERVICES": 'Extract HCBS services clause present either in the compensation or exhibit section of the contract. It may contain keywords like "for covered HCBS services, plan shall pay". Do not include definitions of HCBS. If it is not present, answer N/A.',
}
KEYWORD_MAPPINGS = {
"TERM_CLAUSE": {
"methodology": "hierarchy",
"keywords": ["Term", "term", "duration", "agreement period"],
},
"LATE_PAID_CLAIMS_LANGUAGE": {
"methodology": "hierarchy",
"keywords": [
"late paid",
"late payment",
"interest",
"late fee",
"interest shall be paid",
"interest payment",
],
},
"NON_RENEWAL_LANGUAGE": {
"methodology": "or",
"keywords": ["not to renew", "non-renewal", "non renewal", "nonrenewal"],
},
# "NON_RENEWAL_DAYS": {'methodology': 'or', 'keywords': ["not to renew", "non-renewal", "non renewal", "nonrenewal"]},
"HCBS_SERVICES": {
"methodology": "hierarchy",
"keywords": ["%", "Plan shall pay", "hcbs"],
},
"POLICIES_AND_PROCEDURES": {
"methodology": "or",
"keywords": ["2.4", "policies", "procedures"],
},
"REGULATORY_REQUIREMENTS": {
"methodology": "or",
"keywords": ["8.3", "6.4", "regulatory requirements", "regulatory"],
},
"RECOVERY_RIGHTS": {"methodology": "or", "keywords": ["3.5", "recovery rights"]},
"RELATIONSHIP_OF_PARTIES_LANGUAGE": {
"methodology": "or",
"keywords": ["8.1", "relationship of parties"],
},
}
def run_ac2_prompts(file_object):
################## INITIATE PROCESSING ##################
filename, contract_text = file_object
print(f"Processing AC for {filename}...")
# print(f"Total contract word count: {len(contract_text.split())}")
################## PREPROCESS ##################
text_dict, exhibit_pages, num_pages, ac_chunks = preprocess.preprocess(
contract_text, filename, fields="ac"
)
# print(f"AC Preprocessing Complete - {filename}")
################## RUN FULL CONTEXT PROMPTS ##################
ac_dict = {}
log_data = []
# Process 'full_context' fields together
full_context_fields = [field for field in AC_DICT if field not in KEYWORD_MAPPINGS]
full_context_questions = {
field: prompts.AC_DICT.get(field) for field in full_context_fields
}
if full_context_questions:
full_context_prompt = ac_funcs.create_prompt(
contract_text[0 : min(len(contract_text), 600000)],
question=full_context_questions,
)
# print(f"High accuracy prompt word count: {len(high_accuracy_prompt.split())}")
try:
full_context_answers = ac_funcs.get_ac_answer(
full_context_prompt,
filename,
fields=list(full_context_questions.keys()),
)
ac_dict.update(full_context_answers)
for field, answer in full_context_answers.items():
log_data.append(
{
"Field": field,
"Prompt": f"Question: {full_context_questions[field]}\n\nContext: {contract_text}", # Include full context
"Context Type": "Full Context (High Accuracy)",
"Response": answer,
}
)
except Exception as e:
# print(f"Error processing high accuracy fields: {str(e)}")
for field in full_context_questions.keys():
ac_dict[field] = f"Error: {str(e)}"
log_data.append(
{
"Field": field,
"Prompt": f"Question: {full_context_questions[field]}\n\nContext: {contract_text}", # Include full context
"Context Type": "Full Context (High Accuracy)",
"Response": f"Error: {str(e)}",
}
)
################## RUN CHUNKED PROMPTS ##################
all_fields = set(prompts.AC_DICT.keys())
chunked_fields = [field for field in KEYWORD_MAPPINGS]
for field in chunked_fields:
if field in prompts.AC_DICT:
question = prompts.AC_DICT[field]
else:
# print(f"Field {field} not found in prompts. Skipping...")
continue
# Use chunk if available and different from full context, otherwise use full context
if (
field in ac_chunks
and ac_chunks[field].strip()
and ac_chunks[field] != contract_text
):
context = ac_chunks[field]
context_type = "Smart Chunking"
else:
context = contract_text
context_type = "Full Context"
prompt = prompts.AC_SINGLE_FIELD_TEMPLATE(context, question)
# print(f"Field {field} prompt word count: {len(prompt.split())} ({context_type})")
try:
field_answer = claude_funcs.invoke_claude(
prompt, config.MODEL_ID_CLAUDE35_SONNET, filename, 8192
)
print(field, field_answer)
ac_dict[field] = field_answer
log_data.append(
{
"Field": field,
"Prompt": f"Question: {question}\n\nContext: {context}", # Include full context
"Context Type": context_type,
"Response": field_answer,
}
)
except Exception as e:
# print(f"Error processing field {field}: {str(e)}")
ac_dict[field] = f"Error: {str(e)}"
log_data.append(
{
"Field": field,
"Prompt": f"Question: {question}\n\nContext: {context}", # Include full context
"Context Type": context_type,
"Response": f"Error: {str(e)}",
}
)
################## AC CONDITIONAL PROMPTS ##################
## Non-Renewal - Days
nrd_prompt = prompts.AC_SINGLE_FIELD_TEMPLATE(
ac_dict["NON_RENEWAL_LANGUAGE"], prompts.AC_DICT["NON_RENEWAL_DAYS"]
)
nrd_answer = claude_funcs.invoke_claude(
nrd_prompt, config.MODEL_ID_CLAUDE35_SONNET, filename, 8192
)
ac_dict["NON_RENEWAL_DAYS"] = nrd_answer
################## ENSURE ALL FIELDS ARE PRESENT ##################
ac_dict = {k: v for k, v in ac_dict.items() if k in valid.AC_MAPPING}
for field in all_fields:
if field not in ac_dict:
# print(f"Field not found in results. {field}")
# ac_dict[field] = "N/A"
log_data.append(
{
"Field": field,
"Prompt": "N/A",
"Context Type": "N/A",
"Response": "N/A",
}
)
################## CREATE OUTPUT DIRECTORIES ##################
base_filename = os.path.splitext(filename)[0].strip()
output_dir = os.path.join(ac2_output_path, base_filename)
os.makedirs(output_dir, exist_ok=True)
################## POSTPROCESS ##################
ac_dict["Contract Name"] = filename
ac_df = pd.DataFrame([ac_dict])
ac_df = postprocess.ac_postprocess(ac_df)
################## WRITE TO OUTPUT ##################
ac_df.to_csv(f"{output_dir}/ac_output.csv", index=False)
print(f"AC Output written: {filename}")
################## WRITE LOG TO CSV ##################
# log_file_path = os.path.join(output_dir, f"{base_filename}_prompt_log.csv")
# with open(log_file_path, 'w', newline='', encoding='utf-8') as log_file:
# fieldnames = ['Field', 'Prompt', 'Context Type', 'Response']
# writer = csv.DictWriter(log_file, fieldnames=fieldnames)
# writer.writeheader()
# writer.writerows(log_data)
# print(f"Prompt log written to {log_file_path}")
return ac_dict
def process_ac2_adhoc(item):
filename, contract_text = item
try:
results = run_ac2_prompts(item)
if results is not None:
tracking.update_results_csv(item, results)
return item, results
except Exception as e:
print(f"Error processing item {filename}: {e}")
traceback.print_exc()
def preprocess_ad_hoc(contract_text, filename, fields=config.FIELDS):
contract_text = preprocessing_funcs.clean_newlines(contract_text)
contract_text = preprocessing_funcs.clean_law_symbols(contract_text)
text_dict = preprocessing_funcs.split_text(
contract_text
) # return a dictionary with keys - page_num (str), values as the page_text
num_pages = len(text_dict.keys()) - 1
text_dict = preprocessing_funcs.filter_quick_review(text_dict)
if fields == "b":
exhibit_pages = top_down_funcs.get_exhibit_pages(
text_dict, filename
) # All pages with exhibit headers
# text_dict = table_funcs.align_and_format_tables(text_dict, filename)
text_dict = preprocessing_funcs.chunk_consecutive(text_dict, exhibit_pages)
ac_chunks = ""
if fields == "ac":
exhibit_pages = []
ac_chunks = preprocessing_funcs.smart_chunk_ac(text_dict)
return text_dict, exhibit_pages, num_pages, ac_chunks
def get_add_on(final_dicts, text_dict):
add_on_dict = {}
for td_page in list(set([d["page_num"] for d in final_dicts])):
td_page = str(td_page).split(".")[0]
print(f"TD Page : {td_page}")
# TD page is the original result top down page, it is not necessarily in the new text_dict.
# Find add_on_page = the key of the new text_dict that points to the correct page of the old text dict
if str(td_page) in list(text_dict.keys()):
add_on_page = td_page
else:
add_on_page = [
str(page) for page in text_dict.keys() if float(page) > float(td_page)
]
print(add_on_page)
if not add_on_page:
add_on_dict[td_page] = {
"ADD_ON_REIMBURSEMENT_LANGUAGE": "N/A",
"ADD_ON_REIMBURSEMENT_IND": "N",
}
continue
elif not isinstance(add_on_page, str):
add_on_page = add_on_page[0]
ad = {}
add_on_prompt = f"## Page Start ## {text_dict[str(add_on_page)]} ## Page End ## . Extract the Add On Reimbursement language present in the page above. If there is no Add On language present, answer N/A. Write only the answer, with no additional commentary or explanation."
add_on_answer = claude_funcs.invoke_claude(
add_on_prompt, config.MODEL_ID_CLAUDE35_SONNET, "", 256
)
ad["ADD_ON_REIMBURSEMENT_LANGUAGE"] = add_on_answer
if "N/A" in ad["ADD_ON_REIMBURSEMENT_LANGUAGE"]:
ad["ADD_ON_REIMBURSEMENT_IND"] = "N"
else:
ad["ADD_ON_REIMBURSEMENT_IND"] = "Y"
add_on_dict[str(td_page)] = ad
return add_on_dict
def process_b_adhoc(item):
filename, contract_text = item
print(f"Processing {filename}")
merged_dicts = [d for d in abc_dicts if d["Filename"] + ".txt" == filename]
print(len(merged_dicts))
if len(merged_dicts) > 0:
text_dict, exhibit_pages, num_pages, ac_chunks = preprocess_ad_hoc(
contract_text, filename
)
print(f"Preprocessing {filename}")
# Exclusions
exclusion_dict = top_down_funcs.get_td_dict(
text_dict, filename, prompts.TOP_DOWN_EXCLUSIONS, valid.EXCLUSION_TERMS
)
final_dicts = top_down_funcs.add_exclusions(merged_dicts, exclusion_dict)
print(f"Exclusions - {filename}")
# Medically Necessary
medically_necessary_dict = top_down_funcs.get_td_dict(
text_dict,
filename,
prompts.TOP_DOWN_MEDICALLY_NECESSARY,
valid.VALID_MEDICALLY_NECESSARY,
)
medically_necessary_dict = postprocessing_funcs.filter_dict(
medically_necessary_dict,
pattern=re.compile(
"|".join(
re.escape(keyword) for keyword in valid.INVALID_MEDICALLY_NECESSARY
),
re.IGNORECASE,
),
)
final_dicts = top_down_funcs.add_medically_necessary(
final_dicts, medically_necessary_dict
)
print(f"Medically Necessary - {filename}")
# Add On
add_on_dict = get_add_on(final_dicts, text_dict)
print(f"Add On Results: {add_on_dict}")
print(f"Add On - {filename}")
for td_page in add_on_dict.keys():
for d in final_dicts:
if str(d["page_num"]) == str(td_page):
print("Add On Match")
d.update(add_on_dict[str(td_page)])
# print(len(final_dicts))
output_df = pd.DataFrame(final_dicts)
print(f"B finished -{filename} - output {output_df.shape}")
print(output_df.columns)
time.sleep(3)
base_filename = os.path.splitext(filename)[0].strip()
output_dir = os.path.join(b_output_path, base_filename)
os.makedirs(output_dir, exist_ok=True)
output_df.to_csv(f"{output_dir}/b_output.csv")
########################## Process Starts Here ##########################
# Run List
run_list = pd.read_excel(run_path)
run_list = run_list[run_list["Batch"] == "3A"]["Filename"].unique().tolist()
print(f"Run List: {len(run_list)} files")
# Input Dict
input_dict_3a = utils.read_input(local_path=input_path + "A")
input_dict_3b = utils.read_input(local_path=input_path + "B")
input_dict = {**input_dict_3a, **input_dict_3b}
print(f"Input Dict: {len(input_dict)} files")
# Filter Input Dict
input_dict = {
filename: contract_text
for filename, contract_text in input_dict.items()
if filename.split(".txt")[0] in run_list
}
print(f"Input Dict In Run List: {len(input_dict)} files")
# Left to run
if "a" in config.FIELDS:
input_dict_ac2 = {
filename: contract_text
for filename, contract_text in input_dict.items()
if filename.split(".txt")[0] not in os.listdir(ac2_output_path)
}
print(f"Input Dict Left to Run (AC2): {len(input_dict_ac2)} files")
# ABC Dicts
abc_df = pd.read_excel(clean_path)
abc_df.rename(columns={v: k for k, v in valid.B_MAPPING.items()}, inplace=True)
abc_df.rename(columns={v: k for k, v in valid.AC_MAPPING.items()}, inplace=True)
abc_df.rename(columns=combined_mapping, inplace=True)
abc_df.drop(["State from Ben", "Unnamed: 54"], axis=1, inplace=True)
abc_names = list(abc_df["Filename"].unique())
abc_dicts = abc_df.to_dict(orient="records")
print(f"Clean abc : {len(abc_dicts)}")
# Left to run
if "b" in config.FIELDS:
input_dict_b = {
filename: contract_text
for filename, contract_text in input_dict.items()
if filename.split(".txt")[0] in abc_df["Filename"].unique()
}
input_dict_b = {
filename: contract_text
for filename, contract_text in input_dict_b.items()
if filename.split(".txt")[0] not in os.listdir(b_output_path)
}
print(f"Input Dict Left to Run (B): {len(input_dict_b)} files")
with concurrent.futures.ThreadPoolExecutor(max_workers=config.MAX_WORKERS) as executor:
if "a" in config.FIELDS and "c" in config.FIELDS:
futures = [
executor.submit(process_ac2_adhoc, item) for item in input_dict_ac2.items()
]
if "b" in config.FIELDS:
futures = [
executor.submit(process_b_adhoc, item) for item in input_dict_b.items()
]
for future in concurrent.futures.as_completed(futures):
try:
result = future.result()
except Exception as e:
print(f"Error in future: {e}")
traceback.print_exc()
# for item in input_dict_b.items():
# process_b_adhoc(item)
@@ -1,87 +0,0 @@
from utils import read_local
import preprocess
import preprocessing_funcs
import keywords
import argparse
import os
from keywords import KEYWORD_MAPPINGS
from collections import defaultdict
# import tqdm
def parse_arguments():
parser = argparse.ArgumentParser(description="Smart Chunking Tester")
parser.add_argument(
"--input_dir", help="Input directory (local path or S3 URI)", default="src/ip2"
)
parser.add_argument(
"--output_dir", help="Output directory (local)", default="src/output_chunks"
)
parser.add_argument("--keyword", help="Keyword to be used for chunking")
parser.add_argument(
"--case_sensitive",
help="case sensitivity",
action=argparse.BooleanOptionalAction,
)
return parser.parse_args()
def main():
args = parse_arguments()
input_dir = args.input_dir
output_dir = args.output_dir # src/output_chunks
case_sensitive = args.case_sensitive
if case_sensitive is None: # This shouldn't have to be assigned
case_sensitive = False
keyword = args.keyword
keyword = list(map(str, keyword.split(",")))
keyword = [kw.strip() for kw in keyword]
print("Generating Chunks for keywords --> ", keyword)
print("Case sensitive -->", case_sensitive)
print(type(case_sensitive))
kws = KEYWORD_MAPPINGS
retained_rates = []
for file in os.listdir(input_dir):
full_path = os.path.join(input_dir, file)
contract_text = read_local(full_path)
contract_text = preprocessing_funcs.clean_newlines(contract_text)
contract_text = preprocessing_funcs.clean_law_symbols(contract_text)
text_dict = preprocessing_funcs.split_text(
contract_text
) # return a dictionary with keys - page_num (str), values as the page_text
test_chunck = preprocessing_funcs.smart_chunk_ac(
text_dict=text_dict,
keyword_mappings={
"place_holder": {
"methodology": "or",
"keywords": keyword,
"case_sensitive": case_sensitive,
}
},
)
retained_rate = len(test_chunck["place_holder"]) / len(contract_text)
retained_rates.append(retained_rate)
print(
f"\nfrom file {file} {len(contract_text)} characters were retrieved;\n{len(test_chunck['place_holder'])} were retained by smart chunking on keywords:\n{keyword}"
)
print(f"Smart chunking reduced the document by {100*(1-retained_rate):0.2f}%")
with open(f"{output_dir}/{file}_CHUNKED.txt", "w") as f:
f.write(test_chunck["place_holder"])
print("avg reduction:", 1 - (sum(retained_rates) / len(retained_rates)))
print("max retention:", max(retained_rates))
print("min retention:", min(retained_rates))
if __name__ == "__main__":
main()
-41
View File
@@ -1,41 +0,0 @@
import argparse
import os
from utils import read_local
import preprocessing_funcs
import ac_funcs
def parse_arguments():
parser = argparse.ArgumentParser(description="Smart Chunking Tester")
parser.add_argument(
"--input_dir", help="Input directory (local path or S3 URI)", default="src/new/"
)
return parser.parse_args()
def main():
args = parse_arguments()
input_dir = args.input_dir
for file in os.listdir(input_dir):
full_path = os.path.join(input_dir, file)
print("--" * 20)
print(file)
contract_text = read_local(full_path)
contract_text = preprocessing_funcs.clean_newlines(contract_text)
contract_text = preprocessing_funcs.clean_law_symbols(contract_text)
text_dict = preprocessing_funcs.split_text(
contract_text
) # return a dictionary with keys - page_num (str), values as the page_text
irs_answers = ac_funcs.tin_regex(filename=str(file), text_dict=text_dict)
if irs_answers is None:
irs_answers = {}
irs_answers["PROV_GROUP_TIN"] = "N/A"
irs_answers["PROV_TIN_OTHER"] = "N/A"
print(irs_answers)
if __name__ == "__main__":
main()
+107
View File
@@ -1,5 +1,26 @@
import re
"""
Note:
The below pattern will match with the following date formats:
Numeric Formats
MM/DD/YYYY or MM-DD-YYYY (e.g., 11/18/2024, 11-18-2024)
MM/DD/YY or MM-DD-YY (e.g., 11/18/24, 11-18-24)
YYYY/MM/DD or YYYY-MM-DD (e.g., 2024/11/18, 2024-11-18)
DD/MM/YY or DD-MM-YY (e.g., 18/11/24, 18-11-24)
Textual Formats
Month DD, YYYY (e.g., December 1, 2024)
Month D, YYYY (e.g., December 1, 2024 — with a single-digit day)
YYYY Month DD (e.g., 2024 December 1)
Abbreviated Month Formats
Mon DD, YYYY (e.g., Dec 1, 2024)
DD Mon YYYY (e.g., 1 Dec 2024)
YYYY Mon DD (e.g., 2024 Dec 1)
Accounts for month names in lowercase and uppercase
"""
DATE_PATTERN = r'\b((0?[1-9]|1[0-2])[-/](0?[1-9]|[12][0-9]|3[01])[-/](\d{4}|\d{2})|(\d{4}[-/](0?[1-9]|1[0-2])[-/](0?[1-9]|[12][0-9]|3[01]))|((0?[1-9]|[12][0-9]|3[01])[-/](0?[1-9]|1[0-2])[-/](\d{4}|\d{2}))|(?:January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|Jun|Jul|Aug|Sep|Oct|Nov|Dec|january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|jun|jul|aug|sep|oct|nov|dec)\s+\d{1,2},\s+\d{4}|(?:\d{4}\s+(?:January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|Jun|Jul|Aug|Sep|Oct|Nov|Dec|january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|jun|jul|aug|sep|oct|nov|dec)\s+\d{1,2})|(?:\d{1,2}\s+(?:January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|Jun|Jul|Aug|Sep|Oct|Nov|Dec|january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|jun|jul|aug|sep|oct|nov|dec)\s+\d{4}))\b'
def chunk_hierarchical(
text_dict: dict[str, str], keywords: list[str], case_sensitive: bool = False
@@ -198,3 +219,89 @@ def keyword_search(
)
return page_dict_sorted
def chunk_with_include_exclude_keywords(
text_dict: dict[str, str],
include_keywords: list[str],
exclude_keywords: list[str],
case_sensitive: bool = False,
apply_date_filtering: bool = True
) -> list[str]:
"""Look for pages that include any of the include_keywords and exclude any pages that contain any of the exclude_keywords.
Args:
text_dict (dict[str, str]): A dictionary keyed by string page number and valued
by string page contents
include_keywords (list[str]): A list of string keywords to include
exclude_keywords (list[str]): A list of string keywords to exclude
case_sensitive (bool): A flag for converting all keywords and text to lower case,
in order to ignore case altogether.
apply_date_filtering (bool): A flag to filter out the pages not containing a date.
Returns:
list[str]: The sorted list of string page numbers that include all include_keywords and exclude any exclude_keywords
"""
if not case_sensitive:
include_keywords = [keyword.lower() for keyword in include_keywords]
exclude_keywords = [keyword.lower() for keyword in exclude_keywords]
text_dict = {key: value.lower() for key, value in text_dict.items()}
if apply_date_filtering:
pages_containing_date = set()
for page_num, page_content in text_dict.items():
dates_list = re.findall(DATE_PATTERN, page_content)
if len(dates_list) > 0:
pages_containing_date.add(page_num)
pages_containing_date = sorted(pages_containing_date,key=int)
# debug lines for inclusion and exclusion keywords found on each page
# print(f"Pages containing date: {pages_containing_date}")
# for page_num, page_content in text_dict.items():
# if page_num in pages_containing_date:
# inclusion_keywords = set()
# exclusion_keywords = set()
# for keyword in include_keywords:
# if keyword in page_content:
# inclusion_keywords.add(keyword)
# for keyword in exclude_keywords:
# if keyword in page_content:
# exclusion_keywords.add(keyword)
# print(f"Page #: {page_num} contains inclusion keywords: {inclusion_keywords} and contain exclusion keywords: {exclusion_keywords}")
page_list = []
for page in text_dict.keys():
if not apply_date_filtering or (apply_date_filtering and page in pages_containing_date):
if any(keyword in text_dict[page] for keyword in include_keywords) and not any(
keyword in text_dict[page] for keyword in exclude_keywords
):
# if str(int(page) - 1) in text_dict.keys():
# page_list.append(str(int(page) - 1))
# print(f"Taking page #: {str(int(page) - 1)} (as buffer for following page)")
page_list.append(page)
# print(f"Taking page #: {page}")
# if str(int(page) + 1) in text_dict.keys():
# page_list.append(str(int(page) + 1))
# # print(f"Taking page #: {str(int(page) + 1)} (as buffer for previous page)")
page_list_sorted = sorted([int(page_str) for page_str in set(page_list)])
page_list_sorted_str = list(map(str, page_list_sorted))
# print(", ".join([page for page in page_list_sorted_str]))
# print(f"Obtained total pages: {len(page_list_sorted_str)}")
return page_list_sorted_str
+140
View File
@@ -0,0 +1,140 @@
import utils
import config
import cnc_hotfix
import preprocessing_funcs
import keywords
import valid
import pandas as pd
import concurrent.futures
ABC_PATH = '~/centene-national-contracting/doczy.ai/fieldExtraction/clean_output/CNC-1A-ABC.xlsx'
FILEMAP_PATH = '~/centene-national-contracting/doczy.ai/fieldExtraction/reference_files/batch1_duplicates_mapping.csv'
def process_hotfix(abc, contract_text):
"""
Processes a contract by applying a series of hotfixes to the original output data.
Parameters:
abc (DataFrame): The original output DataFrame for one contract. This DataFrame contains 114 columns.
contract_text (str): The text of the contract.
Returns:
DataFrame: The original abc DataFrame augmented with additional columns for each hotfix applied.
"""
filename = abc['Contract Name'].unique()[0]
original_length = len(abc)
if contract_text is not None:
# print(filename, len(contract_text))
# Preprocess
contract_text = preprocessing_funcs.clean_newlines(contract_text)
contract_text = preprocessing_funcs.clean_law_symbols(contract_text)
text_dict = preprocessing_funcs.split_text(
contract_text
) # return a dictionary with keys - page_num (str), values as the page_text
text_dict, top_sheet_dict = preprocessing_funcs.filter_quick_review(text_dict)
ac_chunks = preprocessing_funcs.smart_chunk_ac(text_dict, contract_text, keywords.GROUPED_KEYWORD_MAPPINGS)
######################################### Hot fixes ##########################################
######### Rule/Regex-based #########
# Default
abc = cnc_hotfix.check_default_term(abc)
abc = cnc_hotfix.check_default_rate(abc)
abc = cnc_hotfix.populate_default_term(abc, text_dict)
# # Health Plan State
abc = cnc_hotfix.clean_healthplan_state(abc, filename, text_dict)
# # IRS
abc = cnc_hotfix.clean_irs(abc, filename, text_dict)
# # NPI
abc = cnc_hotfix.clean_npi(abc, filename, text_dict, top_sheet_dict)
# # LOB
abc = cnc_hotfix.clean_lob(abc, text_dict)
# # Lesser
abc = cnc_hotfix.clean_lesser(abc, text_dict)
# # Provider Type Level 2
abc = cnc_hotfix.clean_provider_type_2(abc)
# ######## Prompt-based #########
# Rate Standard
# abc = cnc_hotfix.clean_rate_standard(abc)
# Contract Effective Date
# abc = cnc_hotfix.clean_contract_effective_date(abc, filename, text_dict)
# Term Group
# abc = cnc_hotfix.clean_term_group(abc, filename, text_dict, ac_chunks)
# Agreement Name
# abc = cnc_hotfix.clean_agreement_name(abc, filename, text_dict)
else:
abc["Contract Duplicate Issue"] = "No Contract Found"
return abc
def main():
# Read clean data
print("Reading clean data...")
abc_clean = pd.read_excel(ABC_PATH)
unique_output_files = abc_clean['Contract Name'].unique()
print("ABC Clean: ", abc_clean.shape)
print(len(unique_output_files), 'files in clean output...')
# print(list(abc_clean.columns))
abc_clean.rename(columns=valid.HOTFIX_MAPPING, inplace=True)
# Read input dict
print("Reading input .txt files...")
input_dict = utils.read_input()
input_dict = {filename.strip('.txt') : contract_text for filename, contract_text in input_dict.items()}
print("Input Dict: ", len(input_dict))
input_dict_dups = utils.read_input('data_cnc/CNC-1-Dups')
input_dict_dups = {filename.strip('.txt') : contract_text for filename, contract_text in input_dict_dups.items()}
print("Input Dict Dups: ", len(input_dict_dups))
input_dict_full = input_dict | input_dict_dups
# input_dict_full = input_dict
print(f"{len([file for file in input_dict_full.keys() if file not in unique_output_files])} files in full s3, not in output")
print(f"{len([file for file in unique_output_files if file not in input_dict_full.keys()])} files in output, not in full s3")
# print(f"{[file for file in unique_output_files if file not in input_dict_full.keys()]} files in output, not in full s3")
# Create list of tuples
abc_clean_list = [(group, input_dict_full.get(name)) for name, group in abc_clean.groupby('Contract Name')]
print(f"{len(abc_clean_list)} - files in input")
print(f"{len([f for f in abc_clean_list if f[1]])} - valid files in input")
df_list = []
print("Processing hotfixes...")
for abc_df, contract_text in abc_clean_list:
new_df = process_hotfix(abc_df, contract_text)
df_list.append(new_df)
abc_final = pd.concat(df_list, ignore_index=True)
print(abc_final.shape)
print(list(abc_final.columns))
print("Writing to excel...")
abc_final.to_excel(f'{config.BATCH_ID}-ABC-NonPrompt-HotfixFull.xlsx')
if __name__ == "__main__":
main()
+628 -67
View File
@@ -1,94 +1,655 @@
import utils
import pandas as pd
import itertools
import numpy as np
import re
import pandas as pd
from valid import STATE_MAP
from prompts import BOTTOM_UP_METHODOLOGY_BREAKOUT
from bottom_up_funcs import get_short_methodology
import claude_funcs
from utils import is_empty
import dict_operations
import config
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)
def state_check(states: str | list[str], is_dataframe: bool = False) -> str | list[str]:
# TODO: genericize this function to take in and put out a dataframe and put it into
# postprocessing_funcs.py
keywords_to_detect_invalid_state_name = [
"suggest",
"The Contract Text",
"refers",
"For ",
"Answer",
"Multiple",
"Statewide",
]
if not isinstance(states, str) and not isinstance(states,list):
if is_empty(states):
states = 'N/A'
import ac_smart_chunking
import claude_funcs
import cnc_hotfix_effective_date_utils
import config
import utils
import valid
import table_funcs
from hotfix_helper_funcs import irs_hotfix, clean_output_postprocess, state_check, check_field_for_matches, npi_hotfix, npi_post_process
import keywords
import prompts
import ac_funcs
import postprocessing_funcs
####### Default Term/Rate #######
def concatenate_lists(row):
total = []
for col in ['Line of Business List', 'Provider Type List', 'Provider Type - Level 2 List', 'IP/OP List', 'Service Type List', 'Plan Type List']:
value = row[col]
if isinstance(value, list):
total.extend([str(item) for item in value])
else:
total.append(str(value))
return total
def check_default_term(df: pd.DataFrame):
df['Contains Only'] = df['Default Term'].str.lower().str.contains('only')
if df['Contains Only'].any():
df_only_grouped = df[['Attachment/Exhibit', 'Line of Business', 'Provider Type', 'Provider Type - Level 2', 'IP/OP', 'Service Type', 'Plan Type', 'Default Term']]
df_transformed = df_only_grouped.groupby(['Attachment/Exhibit', 'Default Term'], as_index=False).transform(lambda x: list(set(x.dropna().to_list())))
df_only_grouped[['Line of Business List', 'Provider Type List', 'Provider Type - Level 2 List', 'IP/OP List', 'Service Type List', 'Plan Type List']] = df_transformed
if (
isinstance(states, list) and all(isinstance(state, str) for state in states)
) or isinstance(states, str):
if isinstance(states, str):
if (
not any(
kw.lower() in states.lower()
for kw in keywords_to_detect_invalid_state_name
)
and "," in states
):
states = states.split(",")
else:
states = [states]
df_only_grouped['All Checks'] = df_only_grouped[['Line of Business List', 'Provider Type List', 'Provider Type - Level 2 List', 'IP/OP List', 'Service Type List', 'Plan Type List']].apply(concatenate_lists, axis=1)
df_only_grouped['All Checks'] = df_only_grouped['All Checks'].apply(lambda x: [y.replace('OP','Outpatient').replace('IP', 'Inpatient') for y in x])
df_only_grouped = df_only_grouped[['Attachment/Exhibit', 'Line of Business', 'Provider Type', 'Provider Type - Level 2', 'IP/OP', 'Service Type', 'Plan Type','All Checks']]
df = df.merge(df_only_grouped, on = ['Attachment/Exhibit', 'Line of Business', 'Provider Type', 'Provider Type - Level 2', 'IP/OP', 'Service Type', 'Plan Type'], how = 'left')
new_states = []
for state in states:
state = state.strip()
df['Default Term'].fillna('', inplace=True)
df['All Checks'] = df['All Checks'].apply(lambda d: d if isinstance(d, list) else [])
# fix 1
if state == "Hlorida":
state = "Florida"
df['Default Match?'] = df.apply(lambda row: any([x.lower() in row['Default Term'].lower() for x in row['All Checks']]), axis = 1)
# fix 2 - LLM self-talking
if any(
kw.lower() in state.lower()
for kw in keywords_to_detect_invalid_state_name
):
state = "N/A"
df['Default Rate Corrected Step 1'] = df['Default Rate']
df.loc[(df['Contains Only']==True) & (df['Default Match?']==False), 'Default Rate Corrected Step 1'] = ''
if state in STATE_MAP.keys() or state.upper() in STATE_MAP.keys():
new_states.append(STATE_MAP[state.upper()])
return df
elif state.strip().title() in STATE_MAP.values():
new_states.append(state.strip().title())
else:
df['All Checks'] = ''
df['Default Match?'] = True
df['Default Rate Corrected Step 1'] = df['Default Rate']
return df
else:
new_states.append("N/A")
def check_default_rate(df: pd.DataFrame):
if is_dataframe is False:
return new_states # list
mapping = {
'medicare' : 'MCR',
'medicaid' : 'MCD',
'billed charge' : 'BC',
'allowable charge' : 'AC',
'allowed charge' : 'AC',
'average wholesale price' : 'AWP'
}
return ", ".join(new_states) # concatenated string
def replace_substring(string):
for k, v in mapping.items():
string = re.sub(r'\b' + k + r'\b', v, string, flags=re.IGNORECASE)
return string
raise TypeError(
f"Expected a string or list of strings, got {type(states).__name__}"
)
def safe_extract(pattern, text):
match = re.search(pattern, text)
return match.group(0) if match else ''
df['Default Rate Corrected Step 2'] = df['Default Rate Corrected Step 1']
df_relevant = df.loc[(df['Default Rate Corrected Step 1'].isna()==False) & (df['Default Rate Corrected Step 1'] != '') & ((df['Default Term']=='') | (df['Default Term']!=df['Default Term'])), :].copy()
if not df_relevant.empty:
pct_pattern = r'\d+\% of '
reimb_pattern = r'(medicare|allowed charge|allowable charge|billed charge|medicaid|average wholesale price)'
df_relevant.loc[:, 'Default Rate Shortened'] = df_relevant.loc[:, 'Default Rate Corrected Step 1'].apply(lambda x:
safe_extract(pct_pattern, x.lower()) + safe_extract(reimb_pattern, x.lower())
)
df_relevant.loc[:,'Default Rate Shortened'] = df_relevant.loc[:, 'Default Rate Shortened'].apply(replace_substring)
df_grouped = df_relevant.groupby(['Contract Name', 'Attachment/Exhibit', 'Default Rate Shortened'], as_index = False)[r'If rate is % of Payor or MCR [STANDARD]'].agg(lambda x: list(set(x.dropna().to_list()))).reset_index()
df_grouped = df_grouped.rename(columns = {r'If rate is % of Payor or MCR [STANDARD]' : 'Rate Standard List'})
try:
df_grouped['Remove Default Rate?'] = df_grouped.apply(lambda row: any([x.lower() in row['Default Rate Shortened'].lower() for x in row['Rate Standard List']]), axis=1)
df = df.merge(df_grouped, how = 'left', on = ['Contract Name', 'Attachment/Exhibit'])
df['Default Rate Corrected Step 2'] = df['Default Rate Corrected Step 1']
df.loc[df['Remove Default Rate?'] == True, 'Default Rate Corrected Step 2'] = ''
except ValueError:
pass
#df.drop(columns = ['Default Rate Shortened'], inplace=True)
return df
def populate_default_term(df, text_dict):
file_df = df.loc[(df['Default Rate Corrected Step 2'].isna()==False) & (df['Default Rate Corrected Step 2'] != '') & ((df['Default Term'].isna()==True) | (df['Default Term'] == '')), :]
file_df = file_df.dropna(subset=['Page_Num', 'Attachment/Exhibit'])
if not file_df.empty:
exhibits = file_df['Attachment/Exhibit'].unique()
for exhibit in exhibits:
start_page = file_df.loc[file_df['Attachment/Exhibit'] == exhibit, 'Page_Num'].min()
end_page = file_df.loc[file_df['Attachment/Exhibit'] == exhibit, 'Pages'].min() + 1
extracted_text = ''
for page in range(int(round(start_page)), int(round(end_page))):
exhibit_text = text_dict[f'{page:.0f}']
def_rate = file_df.loc[file_df['Page_Num'] == start_page, 'Default Rate Corrected Step 2']
if isinstance(def_rate, pd.Series):
def_rate = def_rate.min()
elif not isinstance(def_rate, str):
def_rate = str(def_rate)
sentences = exhibit_text.split('.')
pct_pattern = r'\d+\%'
reimb_pattern = r'(medicare|allowed charge|allowable charge|billed charge|medicaid|average wholesale price|fee schedule)'
if (def_rate == def_rate) and (def_rate != '') and (re.search(pct_pattern, def_rate)) and (re.search(reimb_pattern, def_rate.lower())):
def_pct = re.search(pct_pattern, def_rate).group(0)
def_reimb = re.search(reimb_pattern, def_rate.lower()).group(0)
for sentence in sentences:
sentence = sentence.replace('\n', ' ')
if (def_pct in sentence) and (def_reimb in sentence.lower()):
extracted_text = sentence
break
if extracted_text != '':
break
file_df.loc[file_df['Attachment/Exhibit'] == exhibit, 'Default Term Corrected Step 3'] = extracted_text.strip()
file_df = file_df[['Attachment/Exhibit', 'Default Term Corrected Step 3']]
file_df.drop_duplicates(inplace=True)
df = df.merge(file_df, how='left', on=['Attachment/Exhibit'])
else:
file_df['Default Term Corrected Step 3'] = file_df['Default Term']
file_df = file_df[['Page_Num', 'Attachment/Exhibit', 'Default Term Corrected Step 3']]
df = df.merge(file_df, how = 'left', on = ['Page_Num', 'Attachment/Exhibit'])
df['Default Term Corrected Step 3'] = df.apply(lambda x: x['Default Term'] if (x['Default Term Corrected Step 3'] == '') or (x['Default Term Corrected Step 3'] != x['Default Term Corrected Step 3']) else x['Default Term Corrected Step 3'], axis = 1)
for col in ['Contains Only','All Checks','Default Match?','index', 'Default Rate Corrected Step 1','Default Rate Shortened','Rate Standard List','Remove Default Rate?']:
if col in df.columns:
df.drop(col, axis=1, inplace=True)
else:
pass
df.rename(columns={'Default Rate Corrected Step 2' : 'Default Rate Corrected', 'Default Term Corrected Step 3' : 'Default Term Corrected'}, inplace=True)
return df
def apply_state_check_to_df(
df: pd.DataFrame, is_dataframe: bool = True
#### HEALTH PLAN STATE ####
def clean_healthplan_state(
df: pd.DataFrame,
contract_name: str,
text_dict: dict[str, str],
) -> pd.DataFrame:
if not isinstance(df, pd.DataFrame):
raise TypeError(f"Expected a dataframe, got {type(df).__name__}")
# raise TypeError(f"Expected a dataframe, got {type(df).__name__}")
return df
if is_dataframe is False:
raise ValueError(
f"Expected 'is_dataframe' parameter as True, got {is_dataframe}"
)
if contract_name is None:
# raise ValueError(
# f"Expected 'contract_name' parameter, got {contract_name}"
# )
return df
if "Health Plan State" not in df.columns:
raise KeyError(
f"Column 'Health Plan State' not found in DataFrame. Available columns are: {list(df.columns)}"
)
# raise KeyError(
# f"Column 'Health Plan State' not found in DataFrame. Available columns are: {list(df.columns)}"
# )
return df
df["Health Plan State_corrected"] = df["Health Plan State"].apply(
state_check, args=(is_dataframe,)
)
health_plan_state = df['Health Plan State'].unique().tolist()[0]
answer = state_check(health_plan_state, text_dict)
df["Health Plan State_corrected"] = answer
return df
#### LINE OF BUSINESS ####
def clean_lob(df, text_dict):
df['Line of Business'] = df['Line of Business'].str.upper()
incorrect_map = {"MMP PLAN": "", "COMPLETE": "", "ALL OTHER HEALTH PLANS": "", "NAN" : ""}
df['Line of Business'] = df['Line of Business'].replace(incorrect_map)
df['LOB_Correct'] = df['Line of Business']
match_pattern = r'|'.join(valid.VALID_LOBS)
for index, row in df.iterrows():
# Issue 1: Missing
if utils.is_empty(row['Line of Business']) and pd.notna(row['Page_Num']): # Check if 'Line of Business' is missing
# Prepare to search text
page_text = text_dict.get(str(int(row['Page_Num'])), "")
next_page_text = text_dict.get(str(int(row['Page_Num']) + 1), "") # Assuming page_num is a numerical index
second_page_text = text_dict.get(str(int(row['Page_Num']) + 2), "")
# First check in or near exhibit
exhibit_index = page_text.find(str(row['Attachment/Exhibit']).strip())
if exhibit_index != -1 and pd.notna(row['Attachment/Exhibit']):
search_area = page_text[exhibit_index:exhibit_index + 200 + len(row['Attachment/Exhibit'])]
matches = re.findall(match_pattern, search_area, re.IGNORECASE)
if matches:
df.at[index, 'LOB_Correct'] = ', '.join(set([m.upper() for m in matches]))
continue
# Then check on entire page
matches = re.findall(match_pattern, page_text, re.IGNORECASE)
if matches:
df.at[index, 'LOB_Correct'] = ', '.join(set([m.upper() for m in matches]))
continue
# Then check on next page
matches = re.findall(match_pattern, next_page_text, re.IGNORECASE)
if matches:
df.at[index, 'LOB_Correct'] = ', '.join(set([m.upper() for m in matches]))
continue
df.at[index, 'LOB_Correct'] = 'NO MATCH FOUND - REVIEW'
# Issue 2: When Payer..., Medicare and/not Medicaid
elif pd.notna(row["Service Type"]):
service = str(row['Service Type']).lower()
if 'where' in service and ('payer' in service or 'payor' in service):
# Check Exhibit first
matches = re.findall(match_pattern, str(row['Attachment/Exhibit']), re.IGNORECASE)
if matches:
df.at[index, 'LOB_Correct'] = ', '.join(set([m.upper() for m in matches]))
else:
df.at[index, 'LOB_Correct'] = str(row['Line of Business']).upper()
# Non-Issue
else:
df.at[index, 'LOB_Correct'] = str(row['Line of Business']).upper()
return df
#### Rate Standard ####
def reimb_methodology_fix(rm: str):
d = {'FULL_METHODOLOGY' : rm}
prompt = BOTTOM_UP_METHODOLOGY_BREAKOUT(d)
answer = claude_funcs.invoke_claude(
prompt, config.MODEL_ID_CLAUDE35_SONNET, '', max_tokens=512
)
answer_dict = dict_operations.secondary_string_to_dict(answer, '')
d.update(answer_dict)
# Ensure all bottom up methodology keys are present
for key in [
"LESSER",
"RATE_STANDARD",
"RATE_SHORT",
"FLAT_FEE_STANDARD",
"LESSER_RATE",
"NOT_TO_EXCEED",
]:
if key not in d.keys():
d[key] = ""
# SHORT_METHODOLOGY
if "%" in d["RATE_STANDARD"]:
d["SHORT_METHODOLOGY"] = get_short_methodology(d["RATE_STANDARD"])
elif "$" in d["FLAT_FEE_STANDARD"]:
d["SHORT_METHODOLOGY"] = "Flat Fee"
else:
d["SHORT_METHODOLOGY"] = "N/A"
return d
def clean_rate_standard(df: pd.DataFrame) -> pd.DataFrame:
df_relevant = df.loc[(df['Reimb. Methodology'].isna() == False) & (df[r'If rate is % of Payor or MCR [STANDARD]'].isna() == True) & (df['Flat Fee'].isna() == True), :]
if not df_relevant.empty:
rm_list = df_relevant['Reimb. Methodology'].unique()
dict_list = []
for rm in rm_list:
d = reimb_methodology_fix(rm)
dict_list.append(d)
results_df = pd.DataFrame.from_records(dict_list)
results_df = results_df.add_suffix('_fixed')
df = pd.merge(df, results_df, how = 'left', right_on = 'FULL_METHODOLOGY_fixed', left_on = 'Reimb. Methodology')
return df
#### LESSER ####
def clean_lesser(df, text_dict):
df['Lesser_Flag'] = ""
# For Exhibit
all_dfs = []
for page_num, page_df in df.groupby('Page_Num', dropna=False):
if pd.notna(page_num) and str(int(page_num)) in text_dict.keys():
contract_page = str(int(page_num))
try:
contract_text = '\n'.join([text_dict[str(int(contract_page)+i)] for i in range(2)])
except:
contract_text = text_dict[str(int(contract_page))]
output_lesser_count = (page_df['Lesser of Logic language, included (Y/N)'] == 'Y').sum()
row_count = page_df.shape[0]
page_lesser_count = len(re.findall(r"lesser|lessor", contract_text, re.IGNORECASE))
# Likely Incorrect
if output_lesser_count == 1 and row_count > 1 and page_lesser_count == 1:
page_df.loc[:, 'Lesser_Flag'] = 'Likely Incorrect'
# Likely Correct
elif page_lesser_count == output_lesser_count == row_count:
page_df.loc[:, 'Lesser_Flag'] = 'Likely Correct'
# Other
else:
page_df.loc[:, 'Lesser_Flag'] = "No Flag"
else:
page_df.loc[:, 'Lesser_Flag'] = "No Flag"
all_dfs.append(page_df)
try:
final_df = pd.concat(all_dfs, ignore_index=True)
except:
final_df = df.copy()
# Second Column
final_df['Service-Methodology'] = final_df['Service Type'].astype(str) + ' - ' + final_df['Reimb. Methodology'].astype(str)
return final_df
#### PROVIDER TYPE LEVEL 2 ####
def clean_provider_type_2(df):
valid_lists = {
'PROF': [
"Physician", "Primary Care Provider", "Dual Capacity Physician",
"Mid-Level Practicioner", "Specialist", "OB/GYN", "Behavioral Health",
"Therapist", "Surgery", "Independent RHC", "Ambulance Services",
"Home Health", "SNF"
],
'ANC': [
"DME", "Durable Medical Equipment", "Radiology", "Lab", "Home Health",
"Hospice", "Dialysis", "PT/OT/ST", "Urgent Care", "Home Infusion",
"Ambulatory Surgery Center", "ASC", "Ambulance Services", "Home Health"
],
'FAC': [
"Skilled Nursing Facility", "Ambulatory Surgery Center", "ASC",
"Hospital", "Clinic", "Rural Health Clinic", "FQHC", "Long Term Care",
"Community Mental Health Center"
]
}
result_df = df.copy()
result_df['PROV_TYPE_LEVEL_2_Corrected'] = result_df['Provider Type - Level 2']
unique_exhibits = df['Attachment/Exhibit'].unique()
for exhibit in unique_exhibits:
if pd.isna(exhibit):
continue
match, _ = check_field_for_matches(exhibit, valid_lists)
if match:
exhibit_mask = (result_df['Attachment/Exhibit'] == exhibit) & (result_df['PROV_TYPE_LEVEL_2_Corrected'].isna())
if exhibit_mask.any():
result_df.loc[exhibit_mask, 'PROV_TYPE_LEVEL_2_Corrected'] = match
blank_mask = result_df['PROV_TYPE_LEVEL_2_Corrected'].isna()
for idx, row in result_df[blank_mask].iterrows():
for field in df.columns:
if field != 'PROV_TYPE_LEVEL_2_Corrected':
match, _ = check_field_for_matches(row[field], valid_lists)
if match:
result_df.at[idx, 'PROV_TYPE_LEVEL_2_Corrected'] = match
break
return result_df
#### IRS ####
def clean_irs(abc_df, filename, text_dict ):
"""Gets the answers for fixed IRS, TIN Others and merges them with clean output
Args:
abc_df(df): Clean input which is grouped on 'Contract Name'
filename: Name of the contract
text_dict (dict): Dictionary keyed by string page-number and valued by actual textract output page
Returns:
merged_df(df): Left joined [abc_df, irs_hotfix_df] on 'Contract Name' with additional '_corrected' columns
"""
# gets the irs hotfixes in
irs_answers = irs_hotfix(filename, text_dict)
irs_df = pd.DataFrame([irs_answers])
# adds corrected suffix for the hotfixes
irs_df.columns = [col + '_corrected' for col in irs_df.columns]
# join on 'Contract Name'
irs_df["Contract Name"] = str(filename)
df_merged = pd.merge(abc_df, irs_df, on='Contract Name', indicator=True, how='left')
df_merged = clean_output_postprocess(df_merged)
# overwrite the corrected columns with clean output if it already has an answer
df_merged['PROV_GROUP_TIN_corrected'] = df_merged['dummy'].fillna(df_merged['PROV_GROUP_TIN_corrected'])
df_merged['PROV_TIN_OTHER_corrected'] = df_merged['dummy_other'].fillna(df_merged['PROV_TIN_OTHER_corrected'])
df_merged['PROV_TIN_GROUP_SIGNATORY_corrected'] = df_merged['dummy_other_signatory'].fillna(df_merged['PROV_TIN_GROUP_SIGNATORY_corrected'])
df_merged = df_merged.rename(columns={'PROV_GROUP_TIN_corrected': 'IRS_corrected'})
df_merged = df_merged.loc[:, ~df_merged.columns.str.contains('dummy')]
return df_merged
##### CONTRACT EFFECTIVE DATE #####
def contract_effective_date_fix(contract_name: str, text_dict: dict[str, str],is_meridian: bool=False) -> str:
# perform smart chunking
page_list = ac_smart_chunking.chunk_with_include_exclude_keywords(text_dict,
include_keywords = cnc_hotfix_effective_date_utils.included_keyword,
exclude_keywords = cnc_hotfix_effective_date_utils.excluded_keyword
)
context = "\n".join(text_dict[page] for page in page_list)
prompt = cnc_hotfix_effective_date_utils.get_effective_date_prompt(context) # get effective date - this is for both meridian and non-meridian
try:
claude_answer_raw = claude_funcs.invoke_claude(
prompt, config.MODEL_ID_CLAUDE35_SONNET, contract_name, 8192)
claude_answer_extracted = re.findall(pattern=cnc_hotfix_effective_date_utils.regex_backticks, string=claude_answer_raw)[-1]
except Exception as e:
raise
# meridian special case
if is_meridian:
if claude_answer_extracted == "N/A":
# perform smart chunking with signature date pages included
page_list = ac_smart_chunking.chunk_with_include_exclude_keywords(text_dict,
include_keywords = cnc_hotfix_effective_date_utils.included_keyword + ["Signature"],
exclude_keywords = cnc_hotfix_effective_date_utils.excluded_keyword
)
context = "\n".join(text_dict[page] for page in page_list)
prompt = cnc_hotfix_effective_date_utils.get_effective_date_meridian_prompt(context)
try:
claude_answer_raw = claude_funcs.invoke_claude(
prompt, config.MODEL_ID_CLAUDE35_SONNET, contract_name, 8192)
claude_answer_extracted = re.findall(pattern=cnc_hotfix_effective_date_utils.regex_backticks, string=claude_answer_raw)[-1]
except Exception as e:
raise
return claude_answer_extracted
def clean_contract_effective_date(
df: pd.DataFrame,
file_name: str,
text_dict: dict[str, str]
) -> pd.DataFrame:
"""Apply `contract_effective_date_fix` to a grouped input dataframe, `df`. A new column
is added.
Args:
df (pd.DataFrame): Grouped dataframe for one contract. Can have multiple rows because
of B-fields. Contract effective date is an AC-field, so one per document.
file_name (str): Filename of the contract
text_dict (dict[str, str]): String-keyed dictionary valued by contract text
Raises:
TypeError: If a non-dataframe is passed in as `df`
KeyError: If 'Contract Effective Date' is not found in the input `df`
Returns:
pd.DataFrame: A grouped dataframe with an extra column,
`Contract Effective Date_corrected`
"""
if not isinstance(df, pd.DataFrame):
raise TypeError(f"Expected a dataframe, got {type(df).__name__}")
if "Contract Effective Date" not in df.columns:
raise KeyError(
f"Column 'Contract Effective Date' not found in DataFrame. Available columns are: {list(df.columns)}"
)
contract_effective_date = df['Contract Effective Date'].dropna().unique().tolist()[0] if not df['Contract Effective Date'].dropna().empty else None # previous run's contract effective date (if any)
payer_name = df['Payer Name'].dropna().unique().tolist()
if len(payer_name) == 0:
payer_name = ""
else:
payer_name = payer_name[0]
is_meridian = any(re.search(r'\b' + keyword + r'\b', payer_name, re.IGNORECASE) for keyword in ['meridian'])
if utils.is_empty(contract_effective_date): # Try prompting (with smart chunking) when the contract effective date is missing
try:
answer = contract_effective_date_fix(file_name, text_dict,is_meridian)
except Exception as e:
# KATON: Do we want to give out "answer='Error'" in this case?
print(f"Error processing {file_name}, got error: {e}")
else:
answer = contract_effective_date
if answer == "N/A":
df["Contract Effective Date_corrected"] = answer
else:
df["Contract Effective Date_corrected"] = pd.to_datetime(answer, dayfirst=False).strftime('%m/%d/%Y')
return df
#### NPI ####
def clean_npi(abc_df, filename, text_dict, top_sheet_dict ):
npi_answers = npi_hotfix(text_dict=text_dict)
if npi_answers["NPI"] == "N/A" and npi_answers["NPI_other"] == "N/A":
npi_answers = npi_hotfix(top_sheet_dict)
npi_df = pd.DataFrame([npi_answers])
npi_df.columns = [col + '_corrected' for col in npi_df.columns]
npi_df["Contract Name"] = str(filename)
df_merged = pd.merge(abc_df, npi_df, on='Contract Name', how='left')
df_merged = npi_post_process(df_merged)
df_merged['NPI_corrected'] = df_merged['dummy'].fillna(df_merged['NPI_corrected'])
df_merged['NPI_other_corrected'] = df_merged['dummy_other'].fillna(df_merged['NPI_other_corrected'])
df_merged = df_merged.loc[:, ~df_merged.columns.str.contains('dummy')]
return df_merged
#### TERM GROUP ####
def clean_term_group(df: pd.DataFrame,
contract_name: str,
text_dict: dict[str, str],
ac_chunks: dict[str, str]) -> pd.DataFrame:
"""this function takes previously generated file as dataframe and one contract name as input. If value for term is null
it runs the term group prompts and sets term clause_corrected, auto-renewal_corrected and termination date_columns
"""
df["Term Clause_corrected"] = df['Term Clause']
df["Contract Auto-Renewal Indicator_corrected"] = df['Contract Auto-Renewal Indicator']
df["Termination Date_corrected"] = df['Termination Date']
# identify contracts where term is blank
term_df = df[(df['Term Clause'].isna())|(df['Term Clause'].str.len() < 10)]
term_df['Filename'] = term_df['Contract Name'] + '.txt'
contract_list = term_df['Filename'].to_list()
contract_list = list(set(contract_list))
# get term group fields and corresponding prompts
fields = keywords.GROUPED_KEYWORD_MAPPINGS['term_group']["fields"]
fields.sort()
questions = {}
for field in fields:
questions[field] = prompts.AC_DICT[field]
# process the contract
if contract_name in contract_list:
ac_answers_dict = {}
context = ac_chunks['term_group']
context = context[0 : min(100000, len(context)) - 1]
prompt = prompts.AC_MULTI_FIELD_TEMPLATE(context, questions)
field_group_answer_raw = claude_funcs.invoke_claude(
prompt, config.MODEL_ID_CLAUDE35_SONNET, contract_name, 8192
)
field_group_answer_raw = field_group_answer_raw.replace("_DATE", "_DT")
field_group_answer_dict = ac_funcs.json_parsing_search(field_group_answer_raw, fields)
for field in fields:
field_answer = field_group_answer_dict[field]
ac_answers_dict[field] = field_answer
ac_df = pd.DataFrame([ac_answers_dict])
ac_df = postprocessing_funcs.clean_term_clause(ac_df)
ac_df = postprocessing_funcs.clean_auto_renewal_ind(ac_df)
if 'TERM_CLAUSE' in ac_df.columns:
df.loc[df['Contract Name'] == contract_name.replace('.txt',''), 'Term Clause_corrected'] = ac_df['TERM_CLAUSE'].iloc[0]
if 'CONTRACT_AUTO_RENEWAL_IND' in ac_df.columns:
df.loc[df['Contract Name'] == contract_name.replace('.txt',''), 'Contract Auto-Renewal Indicator_corrected'] = ac_df['CONTRACT_AUTO_RENEWAL_IND'].iloc[0]
if 'CONTRACT_TERMINATION_DT' in ac_df.columns:
df.loc[df['Contract Name'] == contract_name.replace('.txt',''), 'Termination Date_corrected'] = ac_df['CONTRACT_TERMINATION_DT'].iloc[0]
return df
#### Agreement Name ####
# ensure that prompt for CONTRACT_TITLE is updated and df contains field Agreement Name (Contract Title)_corrected
def clean_agreement_name(df: pd.DataFrame,
contract_name: str,
text_dict: dict[str, str]) -> pd.DataFrame:
"""this function first runs agreement name prompt on first 5 pages.
If agreement name is not found, it runs on the context selected based on keywords.
"""
contract_title_chunk_dict = dict(itertools.islice(text_dict.items(), 5))
question = prompts.AC_DICT["CONTRACT_TITLE"]
context = "\n".join(
[contract_title_chunk_dict[str(page_num)] for page_num in contract_title_chunk_dict.keys()]
)
prompt = prompts.AC_SINGLE_FIELD_TEMPLATE(context, question)
prompt_answer_raw = claude_funcs.invoke_claude(
prompt, config.MODEL_ID_CLAUDE35_SONNET, contract_name, 8192)
if prompt_answer_raw == "N/A":
agreement_keywords = ["AGREEMENT", "AMENDMENT", "ADDENDUM", "PROVIDER", "CONTRACT", "MEMORANDUM", "LETTER", "REQUEST", "EFFECTIVE"]
page_list = ac_smart_chunking.chunk_hierarchical(
text_dict, agreement_keywords, False
)
context = "\n".join(
[text_dict[str(page_num)] for page_num in page_list]
)
prompt = prompts.AC_SINGLE_FIELD_TEMPLATE(context, question)
prompt_answer_raw = claude_funcs.invoke_claude(
prompt, config.MODEL_ID_CLAUDE35_SONNET, contract_name, 8192)
df.loc[df['Contract Name'] == contract_name.replace('.txt',''), 'Agreement Name (Contract Title)_corrected'] = prompt_answer_raw
return df
@@ -0,0 +1,69 @@
included_keyword = ["Effective Date", "entered into", "shall be effective"]
excluded_keyword = ["%", "$","Permit","Certificat","Registration","Department of Public Health"]
regex_backticks = r"\|([^|]+)\|"
def get_effective_date_prompt(context):
prompt_template = f""""
Please analyze the following contract and extract ONLY the effective date. Follow these precise rules in order:
1. Look for dates that appear after phrases matching these exact patterns:
- "Effective Date:" followed by a date
- "FOR HEALTH PLAN USE ONLY" section containing "Effective Date:"
- "To be completed by Health Plan only:" section containing "Effective Date:"
2. If multiple matches are found:
- Prioritize dates within "FOR HEALTH PLAN USE ONLY" or "To be completed by Health Plan only" sections
- Then prioritize dates that directly follow "Effective Date:"
3. Do not use the date found in the "Signature Date" section.
- If there is nothing next to "Effective Date:", return N/A.
- If there is only one date in the document, don't extract this date unless it is explicitly labeled "effective date". It is okay to return N/A.
4. If partial dates are found, return N/A.
5. Return the date in YYYY-MM-DD format or N/A if date not found. Enclose just your final answer in |pipes|.
Here is the contract to analyze:
## START CONTRACT TEXT ##
{context}
## END CONTRACT TEXT ##
Ensure that you do not use the date found in the "Signature Date" section.
Before returning the output, VERIFY the date format is YYYY-MM-DD. If the date is not found, return N/A.
Finally state "Answer: |YYYY-MM-DD| or |N/A|"
"""
return prompt_template
def get_effective_date_meridian_prompt(context):
prompt_template = f""""
Please analyze the following contract and extract ONLY the Signature Date. Follow these precise rules in order:
1. Look for Signature Date
2. If multiple matches are found:
- Prioritize the latest full date
3. If only partial dates are found, return N/A.
4. Return the date in YYYY-MM-DD format or N/A if date not found. Enclose just your final answer in |pipes|.
Here is the contract to analyze:
## START CONTRACT TEXT ##
{context}
## END CONTRACT TEXT ##
Before returning the output, VERIFY the date format is YYYY-MM-DD. If the date is not found, return N/A.
Finally state "Answer: |YYYY-MM-DD| or |N/A|"
"""
return prompt_template
+2
View File
@@ -164,6 +164,8 @@ TABLE_ANALYSIS_NAME = f"{BATCH_ID}-Table-Analysis.csv"
######################################## AWS SETTINGS #########################################
# RUN_MODE = "local" # for testing purposes only
# Bedrock
if RUN_MODE == "local":
AWS_ACCESS_KEY_ID = get_value_from_env("AWS_ACCESS_KEY_ID")
+457
View File
@@ -0,0 +1,457 @@
import ast
from utils import find_regex_matches, add_hyphen_if_needed
import re
import pandas as pd
from valid import STATE_MAP
from utils import is_empty
import ac_smart_chunking
import cnc_hotfix_effective_date_utils
import claude_funcs
import config
keywords_to_detect_invalid_state_name = [ #This is for health plan state when the LLM talks to itself
"suggest",
"The Contract Text",
"refers",
"For ",
"Answer",
"Multiple",
"Statewide",
]
def convert_to_list(value):
if isinstance(value, str):
# Remove any trailing commas or spaces
value = value.strip().replace(',', ' ') # Replace commas with spaces
# Check if the string is in the form of a list and safely convert it
if value.startswith('[') and value.endswith(']'):
try:
return ast.literal_eval(value) # Safely convert string to list
except:
return value.split() # Fallback to split by spaces if literal_eval fails
else:
# Split by spaces and handle both alphanumeric and numeric values
return value.split() # Just split by spaces, treat everything as a string
elif isinstance(value, list):
return value # If it's already a list, return as is
else:
return [value] # Wrap non-string, non-list types in a list
def list_to_string(lst):
if not lst: # Check if the list is empty
return None
else:
return ", ".join(lst) # Join elements with commas
def remove_alphabets(input_str):
# Check if the string contains any alphabetic characters
if any(char.isalpha() for char in input_str):
return None # Return None if there are alphabets
else:
return input_str # Return the string if no alphabets are present
def clean_output_postprocess(merged_df):
merged_df['dummy'] = merged_df['IRS #']
merged_df['dummy_other'] = merged_df['PROV_TIN_OTHER']
merged_df['dummy_other_signatory'] = merged_df["PROV_GROUP_TIN_SIGNATORY"]
merged_df['dummy'] = merged_df['dummy'].astype(str)
merged_df['dummy_other'] = merged_df['dummy_other'].astype(str)
merged_df['dummy_other_signatory'] = merged_df['dummy_other_signatory'].astype(str)
merged_df['dummy'] = merged_df['dummy'].apply(convert_to_list)
merged_df['dummy_other'] = merged_df['dummy_other'].apply(convert_to_list)
merged_df['dummy_other_signatory'] = merged_df['dummy_other_signatory'].apply(convert_to_list)
merged_df['dummy'] = merged_df['dummy'].apply(lambda lst: [x for x in [remove_alphabets(x) for x in lst] if x is not None])
merged_df['dummy_other'] = merged_df['dummy_other'].apply(lambda lst: [x for x in [remove_alphabets(x) for x in lst] if x is not None])
merged_df['dummy_other_signatory'] = merged_df['dummy_other_signatory'].apply(lambda lst: [x for x in [remove_alphabets(x) for x in lst] if x is not None])
merged_df['dummy'] = merged_df['dummy'].apply(lambda lst: [add_hyphen_if_needed(x) for x in lst])
merged_df['dummy_other'] = merged_df['dummy_other'].apply(lambda lst: [add_hyphen_if_needed(x) for x in lst])
merged_df['dummy_other_signatory'] = merged_df['dummy_other_signatory'].apply(lambda lst: [add_hyphen_if_needed(x) for x in lst])
merged_df['dummy'] = merged_df['dummy'].apply(list_to_string)
merged_df['dummy_other'] = merged_df['dummy_other'].apply(list_to_string)
merged_df['dummy_other_signatory'] = merged_df['dummy_other_signatory'].apply(list_to_string)
return merged_df
def check_tin_in_filename(filename):
"""
Checks for the pattern match in filename
Args:
filename(str): Name of the file
Returns:
tin_from_title(str): regex match for patterns in filename
"""
patterns = [r"\b\d{2}-\d{7}\b", r"\b\d{9}\b"]
for pattern in patterns:
tin_from_title = re.findall(pattern, filename)
if len(tin_from_title) == 1:
tin_from_title = [add_hyphen_if_needed(s) for s in tin_from_title]
return tin_from_title[0]
return 'empty'
def signature_page_irs(text_dict, page_list, matches, pattern, filename):
"""
In case of multiple matches, checks if a TIN is on the Signature page
Args:
text_dict (dict): Dictionary keyed by string page-number and valued by actual textract output page
page_list(list): List of pages where the match was found
matches(list): List of regex match(s)
Returns
provider_tin(str): Provider group TIN - either from signature page or filename
other_tins(str): Remaining TINs
"""
# loops through regex matches and thier pages to check for signature page
tin_on_signature_page = []
for tin in matches:
for page in page_list:
if tin in text_dict[page] and 'signature' in text_dict[page]:
tin_on_signature_page.append(tin)
tin_on_signature_page = list(set(tin_on_signature_page))
#adds hyphen if needed
matches = [add_hyphen_if_needed(s) for s in matches]
tin_on_signature_page = [add_hyphen_if_needed(s) for s in tin_on_signature_page]
#case1 - If 1 TIN found on signature page, return it, put the remaining ones in "TIN Others" have signature_tin = N/A
if len(tin_on_signature_page) == 1:
provider_tin = tin_on_signature_page[0]
signature_tin = "N/A"
matches = [item for item in matches if item not in tin_on_signature_page]
other_tins = ", ".join(matches)
return provider_tin, signature_tin, other_tins
#case2 - No TIN found on the signature page, check in filename
elif len(tin_on_signature_page) == 0:
provider_tin = check_tin_in_filename(filename)
if provider_tin == 'empty':
provider_tin = "N/A"
signature_tin = "N/A"
other_tins = [item for item in matches if item != provider_tin]
other_tins = ", ".join(other_tins)
return provider_tin, signature_tin, other_tins
#case3 - More than 1 TIN on signature page - check for TIN in file name, let the others go in signatory_tins
# and finally check for TINs not on signature page but in "matches" list to add them to "tin_others"
else:
filename_tin_check = check_tin_in_filename(filename)
if filename_tin_check == 'empty':
provider_tin = "N/A"
signature_tin = ", ".join(tin_on_signature_page)
other_tins = "N/A"
return provider_tin, signature_tin, other_tins
else:
provider_tin = filename_tin_check
signature_tin = [item for item in tin_on_signature_page if item != provider_tin]
signature_tin = ", ".join(signature_tin)
if len(matches)> len(tin_on_signature_page):
other_tins = [item for item in matches if item not in tin_on_signature_page and item != provider_tin]
other_tins = ", ".join(other_tins)
return provider_tin, signature_tin, other_tins
else:
other_tins = "N/A"
return provider_tin, signature_tin, other_tins
def irs_hotfix(filename, text_dict: dict[str, str]):
"""Returns dictionery with Provider and other TIN(s) based on regex matches and presence on Signatory page
Args:
filename: Name of the contract
text_dict (dict): Dictionary keyed by string page-number and valued by actual textract output page
Returns:
TIN_dict(dict): A dictionery with keys 'PROV_GROUP_TIN' & 'PROV_TIN_OTHER' and values as respective TIN(s)
"""
TIN_dict = {}
patterns = [r"\b\d{2}-\d{7}\b",
r"\b\d{9}\b"]
for pattern in patterns:
matches, page_list = find_regex_matches(pattern, text_dict)
matches = list(set(matches))
if matches:
if len(matches) == 1:
matches = [add_hyphen_if_needed(s) for s in matches]
TIN_dict["PROV_GROUP_TIN"] = matches[0]
TIN_dict["PROV_TIN_OTHER"] = "N/A"
TIN_dict["PROV_TIN_GROUP_SIGNATORY"] = "N/A"
return TIN_dict
elif len(matches) >= 1:
irs_tin, signatory_tin, other_tins = signature_page_irs(text_dict, page_list, matches, pattern, filename)
TIN_dict["PROV_GROUP_TIN"] = irs_tin
TIN_dict["PROV_TIN_OTHER"] = other_tins
TIN_dict["PROV_TIN_GROUP_SIGNATORY"] = signatory_tin
return TIN_dict
# If no matches were found in the text_dict, check the filename
for pattern in patterns:
tin_from_title = check_tin_in_filename(filename)
TIN_dict["PROV_TIN_OTHER"] = "N/A"
TIN_dict["PROV_TIN_GROUP_SIGNATORY"] = "N/A"
if tin_from_title == "empty":
TIN_dict["PROV_GROUP_TIN"] = "N/A"
else:
TIN_dict["PROV_GROUP_TIN"] = tin_from_title
return TIN_dict
def state_check(states: str | list[str], text_dict: dict[str, str]) -> str:
if not isinstance(states, str) and not isinstance(states,list):
states = 'N/A'
if (
isinstance(states, list) and all(isinstance(state, str) for state in states)
) or isinstance(states, str): # If it's a list of strings or a string...
if isinstance(states, str): # If it's a string, turn it to a list (even a singleton)
if "," in states:
states = states.split(",")
else:
states = [states]
result_states = []
for state in states:
state = state.strip()
# fix 1
if state == "Hlorida":
state = "Florida"
# fix 2 - LLM self-talking
if any(
kw.lower() in state.lower()
for kw in keywords_to_detect_invalid_state_name
):
state = "N/A"
if state.upper() in STATE_MAP.keys(): # Check if `state` is a state abbreviation.
result_states.append(STATE_MAP[state.upper()].title())
elif state.strip().title() in STATE_MAP.values(): # Check if `state` is already an expanded name state from the validation set.
result_states.append(state.strip().title())
else:
result_states.append("N/A")
result_states = set(result_states)
result_states.discard("N/A") # Discard any N/As (so if `result_states` is ['Illinois', 'N/A'], it returns 'Illinois' instead of 'Multiple')
if len(result_states) == 1:
return list(result_states)[0]
elif len(result_states) > 1:
return "Multiple"
elif len(result_states) == 0:
state = get_health_plan_state(text_dict) # search for state(s) in contract. If it's not found, `N/A` will be returned
return state
raise TypeError(
f"Expected a string or list of strings, got {type(states).__name__}"
)
def get_health_plan_state(text_dict: dict[str, str]) -> str:
"""Go through a contract text and extract any mention of any state
Args:
text_dict (dict[str, str]): A string-keyed dictionary valued by contract text
Returns:
str: A single state extracted from the text, `multiple`, or `N/A`
"""
state_names = list(STATE_MAP.values())
pattern = r'\b(?:' + '|'.join(re.escape(state) for state in state_names) + r')\b'
states_set = set()
for page_num, page_content in text_dict.items():
state_matches = re.findall(pattern, page_content, flags=re.IGNORECASE)
state_matches = [state.strip().title() for state in state_matches]
states_set.update(state_matches)
states_list = list(states_set)
if len(states_list) > 1:
state = "Multiple"
elif len(states_list) == 1:
state = states_list[0]
else:
state = "N/A"
return state
def check_field_for_matches(text, valid_lists):
"""
Check a field for matches from the valid lists using case-insensitive regex
Returns tuple of (matched_term, list_type) or (None, None)
"""
if pd.isna(text) or text == "":
return None, None
text = str(text).lower()
for list_type, valid_list in valid_lists.items():
for term in valid_list:
if re.search(rf'\b{re.escape(term.lower())}\b', text):
return term, list_type
return None, None
##### NPI #####
def find_10_digit_numbers(row):
matches = re.findall(r'\b\d{10}\b', row)
return matches
def signature_page_npi(text_dict, page_list, matches):
npi_on_signature_page = []
for npi in matches:
for page in page_list:
if npi in text_dict[page] and 'signature' in text_dict[page]:
npi_on_signature_page.append(npi)
if len(npi_on_signature_page) == 1:
npi = npi_on_signature_page[0]
npi_other = [item for item in matches if item not in npi_on_signature_page]
npi_other = ", ".join(npi_other)
return npi, npi_other
elif len(npi_on_signature_page) == 0:
npi = "N/A"
npi_other = ", ".join(matches)
return npi, npi_other
else:
npi = ", ".join(npi_on_signature_page)
npi_other = [item for item in matches if item not in npi_on_signature_page]
if len(npi_other) == 0:
npi_other = "N/A"
return npi, npi_other
else:
npi_other = ", ".join(npi_other)
return npi, npi_other
def npi_hotfix(text_dict: dict[str, str]):
"""Returns dictionery with Provider and other TIN(s) based on regex matches and presence on Signatory page
Args:
filename: Name of the contract
text_dict (dict): Dictionary keyed by string page-number and valued by actual textract output page
Returns:
TIN_dict(dict): A dictionery with keys 'PROV_GROUP_TIN' & 'PROV_TIN_OTHER' and values as respective TIN(s)
"""
NPI_dict = {}
patterns = [r"\b\d{10}\b"]
for pattern in patterns:
matches, page_list = find_regex_matches(pattern, text_dict)
matches = [s for s in matches if s[0] in ('1', '2')]
matches = list(set(matches))
if len(matches)==1:
NPI_dict["NPI"] = matches[0]
NPI_dict["NPI_other"] = "N/A"
elif len(matches) == 0:
NPI_dict["NPI"] = "N/A"
NPI_dict["NPI_other"] = "N/A"
else:
npi, npi_other = signature_page_npi(text_dict, page_list, matches)
NPI_dict["NPI"] = npi
NPI_dict["NPI_other"] = npi_other
return NPI_dict
##################################################################################################################################
# remove '-' from numbers and drop numbers with more than 2 '-' in them
def remove_dashes(lst):
processed_list = []
for item in lst:
# If the item contains more than 2 dashes, remove it
if item.count('-') > 2:
continue
else:
# Remove dashes and keep the digits only
processed_list.append(item.replace('-', ''))
return processed_list
##################################################################################################################################
def filter_elements_by_length(lst):
return [item for item in lst if 7 <= len(item) <= 10]
##################################################################################################################################
# Function to filter out non-digit elements from a list
def filter_non_digits(lst):
return [item for item in lst if item.isdigit()] # Only keep items that are digits
##################################################################################################################################
def filter_start_with_1_or_2(lst):
return [item for item in lst if item.startswith('1') or item.startswith('2')]
##################################################################################################################################
def convert_to_string(value):
if pd.isna(value):
# If the value is NaN, return it as is
return str(value)
elif isinstance(value, float):
# If it's a float, convert to int and then to string to avoid decimal point
return str(int(value)) if value == value else 'nan'
return str(value)
##################################################################################################################################
def npi_post_process(df_merged):
df_merged['dummy'] = df_merged['NPI (10-digits)']
df_merged['dummy_other'] = df_merged["PROV_NPI_OTHER"]
df_merged['dummy'] = df_merged['dummy'].apply(convert_to_string)
df_merged['dummy_other'] = df_merged['dummy_other'].astype(str)
df_merged['dummy'] = df_merged['dummy'].apply(convert_to_list)
df_merged['dummy_other'] = df_merged['dummy_other'].apply(convert_to_list)
df_merged['dummy'] = df_merged['dummy'].apply(remove_dashes)
df_merged['dummy_other'] = df_merged['dummy_other'].apply(remove_dashes)
df_merged['dummy'] = df_merged['dummy'].apply(filter_elements_by_length)
df_merged['dummy_other'] = df_merged['dummy_other'].apply(filter_elements_by_length)
df_merged['dummy'] = df_merged['dummy'].apply(filter_non_digits)
df_merged['dummy_other'] = df_merged['dummy_other'].apply(filter_non_digits)
df_merged['dummy'] = df_merged['dummy'].apply(filter_start_with_1_or_2)
df_merged['dummy_other'] = df_merged['dummy_other'].apply(filter_start_with_1_or_2)
df_merged['dummy'] = df_merged['dummy'].apply(list_to_string)
df_merged['dummy_other'] = df_merged['dummy_other'].apply(list_to_string)
return df_merged
+61
View File
@@ -0,0 +1,61 @@
import pandas as pd
# # Nonprompt
abc_nonprompt = pd.read_excel('hotfix_output/CNC-1A-ABC-NonPrompt-HotfixFull.xlsx')
print("NonPrompt:", abc_nonprompt.shape)
print(len(abc_nonprompt['Contract Name'].unique()))
# Agreement Name
abc_agreement = pd.read_csv('hotfix_output/CNC-1A-AgreementName-All.csv')
print("Agreement Name:", abc_agreement.shape)
print(len(abc_agreement['Contract Name'].unique()))
# Effective Date
abc_date = pd.read_csv('hotfix_output/CNC-1A-ContractEffectiveDate-All.csv')
abc_date = abc_date[['Contract Name', 'Contract Effective Date_corrected']]
abc_date.drop_duplicates(inplace=True)
print("Effective Date:", abc_date.shape)
print(len(abc_date['Contract Name'].unique()))
# Term Group
abc_term = pd.read_csv('hotfix_output/CNC-1A-TermGroup-All.csv')
abc_term.columns = ['Contract Name', 'Term Clause_corrected', 'Contract Auto-Renewal Indicator_corrected', 'Termination Date_corrected']
print("Term Group:", abc_term.shape)
print(len(abc_term['Contract Name'].unique()))
# Rate
rate_full = pd.read_csv('hotfix_output/CNC-1A-Rate.csv')
rate_468 = pd.read_excel('hotfix_output/CNC-1A-Rate-468.xlsx')
mask = ~rate_full['Contract Name'].isin(rate_468['Contract Name'])
rate_full_filtered = rate_full[mask]
abc_rate = pd.concat([rate_full_filtered, rate_468], ignore_index=True)
abc_rate = abc_rate[['Contract Name', 'Attachment/Exhibit', 'Reimb. Methodology', 'FULL_METHODOLOGY_fixed', 'LESSER_fixed', 'RATE_STANDARD_fixed', 'RATE_SHORT_fixed', 'FLAT_FEE_STANDARD_fixed', 'LESSER_RATE_fixed', 'NOT_TO_EXCEED_fixed', 'SHORT_METHODOLOGY_fixed']]
abc_rate.drop_duplicates(inplace=True)
print("Rate:", abc_rate.shape)
print(len(abc_rate['Contract Name'].unique()))
# Merge Effective Date
abc_final = pd.merge(abc_nonprompt, abc_date, how='left', on='Contract Name')
print("After Date:", abc_final.shape)
print(len(abc_final['Contract Name'].unique()))
# Merge Rate
abc_final = pd.merge(abc_nonprompt, abc_rate, how='left', on=['Contract Name', 'Attachment/Exhibit', 'Reimb. Methodology'])
print("After Rate:", abc_final.shape)
print(len(abc_final['Contract Name'].unique()))
# Merge Agreement Name
abc_final = pd.merge(abc_final, abc_agreement, how='left', on='Contract Name')
print("After Agreement:", abc_final.shape)
print(len(abc_final['Contract Name'].unique()))
# Merge Term Group
abc_final = pd.merge(abc_final, abc_term, how='left', on='Contract Name')
print("After Term (Final):", abc_final.shape)
print(len(abc_final['Contract Name'].unique()))
print(list(abc_final.columns))
abc_final.to_excel('CNC-1A-Hotfix-Final.xlsx')
@@ -74,6 +74,44 @@ def clean_law_symbols(contract_text):
return contract_text
# ORIGINAL
def chunk_consecutive_og(text_dict, exhibit_pages):
# If needed - extend page 1 to page 1 and 2, then cut chunking off after (edge case: compensation terms not found on first page of exhibit)
reimbursement_pages = [page_num for page_num in text_dict.keys() if utils.contains_reimbursement(text_dict, page_num)]
page_dict = {}
current_exhibit = None
for page_num in text_dict.keys():
# Page is Reimbursement AND Exhibit
if page_num in reimbursement_pages and page_num in exhibit_pages:
current_exhibit = page_num
page_dict[page_num] = [page_num]
# Page is Reimbursement NOT Exhibit
elif page_num in reimbursement_pages and page_num not in exhibit_pages:
if current_exhibit:
page_dict[current_exhibit].append(page_num)
else:
page_dict[page_num] = [page_num]
# Page is Exhibit NOT Reimbursement
elif page_num in exhibit_pages and page_num not in reimbursement_pages:
current_exhibit = page_num
page_dict[page_num] = [page_num]
# Page is NOT Exhibit NOT Reimbursment
elif page_num not in exhibit_pages and page_num not in reimbursement_pages:
current_exhibit = None
page_dict[page_num] = [page_num]
final_dict = {page_num : '' for page_num in page_dict.keys()}
for page_num in page_dict.keys():
for p in page_dict[page_num]:
final_dict[page_num] += text_dict[p]
return final_dict
def chunk_consecutive(text_dict, exhibit_pages):
exhibit_pages = set(str(page) for page in exhibit_pages)
+12 -1
View File
@@ -58,7 +58,9 @@ NOT_TO_EXCEED : Only if the methodology contains the specific phrase "Not to exc
For the format 'X% of Y', if Y is in the below list, then apply the indicated abbreviation. If Y is not in the list, then write the full Y value:
Medicare --> MCR
Medicare Advantage --> MCR
Medicaid --> MCD
Medicare Advantage --> MA
Allowable Charges --> AC
Allowed Amount --> AA
Billed Charges --> BC
@@ -90,6 +92,15 @@ Example 5 -
Methodology: "Multiple procedures performed during the same day will be reimbursed at 100% for the primary procedure, 50% for the second procedure, and 50% for the third procedure, subsequent procedures shall not be eligible for reimbursement."
Correct Response: {{'LESSER' : 'N', 'RATE_STANDARD' : '100% of AC / 50% of AC / 50% of AC / 0% of AC', 'RATE_SHORT' : '1 / 0.5 / 0.5 / 0', 'FLAT_FEE_STANDARD' : 'N/A', 'LESSER_RATE' : 'N/A', 'NOT_TO_EXCEED' : 'N/A'.}}
Example 6 -
Methodology: "Provider shall be entitled to the lesser of: (1) Anciliary Provider's billed charges; or (2) the amount payable by Medicare, not including Medicare coinsurance and deductibles, plus the amount payable by Medicaid as a secondary coverage based on the Medicaid fee schedule in effect on the date of service."
Correct Response: {{'LESSER' : 'Y', 'RATE_STANDARD' : '100% of MCR', 'RATE_SHORT' : '1', 'FLAT_FEE_STANDARD' : 'N/A', 'LESSER_RATE' : '100% of BC', 'NOT_TO_EXCEED' : 'N/A'.}}
Example 7 -
Methodology: "The Compensation Schedule for the Medicare Product at any given time is the lesser of (i) the Allowable Charges for the particular Covered Service, or (ii) the appropriate amount for such Covered Service under the Company's fee schedule in effect on the date of service for the Medicare Product."
Correct Response: {{'LESSER' : 'Y', 'RATE_STANDARD' : '100% of MCR', 'RATE_SHORT' : '1', 'FLAT_FEE_STANDARD' : 'N/A', 'LESSER_RATE' : '100% of AC', 'NOT_TO_EXCEED' : 'N/A'.}}
Only return the dictionary, with no other commentary or explanation. Ensure you abide by proper JSON formatting.
"""
@@ -387,7 +398,7 @@ If for any question, no correct answer is found, return 'N/A'.
AC_DICT = {
"CONTRACT_EFFECTIVE_DT": "Extract the contract effective date. This may be mentioned in one of the following locations: the signatory section, the preamble of the agreement, or the start of the amendment. Look for phrases such as /'This amendment is effective/'. Return this date in YYYY-MM-DD format.",
"CONTRACT_TITLE": "What is the title of the contract? It generally contains the word Agreement and is found at the very beginning of the document before a paragraph. This will be filled out if there is an amendment or numbered amendment. It typically contains the words agreement, amendment, or contract. An amendment typically has a number attached to it. The typical structure is the provider name, followed by the phrase with agreement, followed potentially by a number signifying a version of the contract.",
"CONTRACT_TITLE": "What is the complete title of the contract or the document? It is generally written in CAPITAL LETTERS and is found at the beginning of the document before a paragraph containing provider and payer name. It typically contains the words AGREEMENT, AMENDMENT, CONTRACT, ADDENDUM, MEMORANDUM, LETTER, PROVIDER or REQUEST. In case of agreement, typical structure is the provider name, followed by the phrase with agreement, followed potentially by a number signifying a version of the contract. In case of amendment, typical structure is word amendment followed by number, followed by the phrase with agreement. Extract the complete title of the contract or document as accurately as possible. Ensure: All leading characters are included for each word (e.g. 'NETWORK' instead of 'ETWORK'). Spaces between words are preserved in phrases (e.g., 'PROVIDER COLLABORATION' instead of 'PROVIDERCOLLABORATION'). If text appears incomplete or lacks spacing, use logical patterns like capitalization or common formatting rules to infer and correct the output. Replace new lines and line breaks with space.",
"PAYER_NAME": "What is the name of the payer that is a party to the contract as stated in the Preamble?",
"PROV_GROUP_NAME": "What is the name of the Group provider that is a party to the contract? This is generally found near keyword provider. Only return the name of the Group provider along with dba name.",
"PROV_GROUP_NPI": "What is the group provider's national provider identifier number mentioned in the contract? This may be found on the signature page or on the roster. It consists of 10 digits. Only return these 10 digits.",
+68 -2
View File
@@ -5,7 +5,7 @@ import shutil
import time
import sys
import numpy as np
from collections import defaultdict
import config
@@ -214,6 +214,19 @@ def split_and_write_csv(
sub_df.to_csv(path, index=False)
print(f"Written {path}")
def contains_lol(text, page='1'):
if isinstance(text, dict):
return page.isdigit() and (
re.search('les+e*o*r of+', text[page].lower())
)
elif isinstance(text, str):
return (
re.search('les+e*o*r of+', text.lower())
)
else:
print("contains_lol - Invalid data type")
def filter_already_processed(input_dict, input_dict_b):
already_processed_ac, already_processed_b = [], []
@@ -269,9 +282,17 @@ def find_regex_matches(
for page, text in text_dict.items():
found = re.findall(pattern, text)
if found:
matches.extend(found)
matches.extend(found) # make it append
page_list.append(page)
return matches, page_list
# matches = defaultdict(list)
# for page, text in text_dict.items():
# matches_found = re.findall(pattern, text)
# if matches_found:
# matches[page].extend(matches_found)
# return list(matches.values()), list(matches.keys())
def chunk_selected_pages(text_dict: dict[str, str], page_list: list) -> str:
@@ -289,3 +310,48 @@ def chunk_selected_pages(text_dict: dict[str, str], page_list: list) -> str:
page_list = sorted([int(page_str) for page_str in set(page_list)])
context = "\n".join([text_dict[str(page_num)] for page_num in page_list])
return context
def add_hyphen_if_needed(input_str):
"""Checks for specific patterns in the input string and modifies it by adding
a '0' at the start and a hyphen at the 3rd index, or just adds a hyphen at
the 3rd index if it's a 9-digit number.
If the input string is exactly 8 characters long, it adds a '0' at the start
and inserts a hyphen at the 3rd position. If the string matches the pattern
of a 9-digit number (exactly 9 digits), it inserts a hyphen at the 3rd index.
If the input string matches the pattern 'x-xxxxxxx', it adds '0' at the start.
All conditions will return a string in the format 'xx-xxxxxxx'.
Args:
- input_str (str): The input string (IRS).
Returns:
- str: The modified string in the format 'xx-xxxxxxx'.
Example:
- '1-2345678''01-2345678'
- '12345678''01-2345678'
- '123456789''12-3456789'
"""
# Check if the string matches the pattern 'x-xxxxxxx' (1 character, hyphen, 7 characters)
if re.fullmatch(r"\d-\d{7}", input_str):
# Add '0' at the start
input_str = '0' + input_str
# Check if the string is exactly 'xxxxxxxx' (8 characters)
elif bool(re.fullmatch(r"\b\d{8}\b", input_str)):
# Add '0' at the start and insert a hyphen at the 3rd index
input_str = '0' + input_str
input_str = input_str[:2] + '-' + input_str[2:]
# If the string matches the pattern of a 9-digit number, add a hyphen at the 3rd index
elif bool(re.fullmatch(r"\b\d{9}\b", input_str)):
input_str = input_str[:2] + '-' + input_str[2:]
# Ensure the output is in the format 'xx-xxxxxxx'
if not bool(re.fullmatch(r"\b\d{2}-\d{7}\b", input_str)):
input_str = 'N/A'
return input_str
+130 -1
View File
@@ -10,7 +10,6 @@ VALID_LOBS = [
"MARKETPLACE",
"COMMERCIAL",
"COMMERCIAL-EXCHANGE",
"GROUP",
"MEDICAID-MEDICARE",
]
@@ -655,3 +654,133 @@ STATE_MAP = {
"WI": "Wisconsin",
"WY": "Wyoming",
}
HOTFIX_MAPPING = {
"Filename": "Contract Name",
"Parent Agreement Code": "Parent Agreement Code",
"Pages": "Pages",
"page_num": "Page_Num",
"Page Num" : "Page_Num",
"EXHIBIT": "Attachment/Exhibit",
"CONTRACT_LOB": "Line of Business",
"PROV_TYPE": "Provider Type",
"PROV_TYPE_LEVEL_2": "Provider Type - Level 2",
"IP_OP": "IP/OP",
"FULL_SERVICE": "Service Type",
"CONTRACT_PROGRAM": "Plan Type",
'Lesser of Logic Language, Included (Y/N)' : "Lesser of Logic language, included (Y/N)",
"LESSER": "Lesser of Logic language, included (Y/N)",
"LESSER_RATE": "Lesser of Rate",
"FULL_METHODOLOGY": "Reimb. Methodology",
"SHORT_METHODOLOGY": "Reimb. Methodology_Short",
'If Rate is % of Payor or MCR [Standard]' : "If rate is % of Payor or MCR [STANDARD]",
"RATE_STANDARD": "If rate is % of Payor or MCR [STANDARD]",
"RATE_SHORT": "If rate is % of Payor or MCR [STANDARD]_Short",
"FLAT_FEE_STANDARD": "FLAT FEE",
"DEFAULT_TERM": "Default Term",
"DEFAULT_RATE": "Default Rate",
"MEDICAL_NECESSITY_LANGUAGE": "Medical Necessity Language (Language)",
"MEDICAL_NECESSITY_LANGUAGE_IND": "Medical Necessity Language (Y/N)",
'Inclusion of essential RBRVS "Fee Source" Language (Y/N)': 'Inclusion of essential RBRVS "Fee Source" Language (Y/N)',
"CDM_IND": "CDM Neutralization Language, included (Y/N)",
"CHARGEMASTER": "CONTRACT_CHARGEMASTER_PROTECTION_LANGUAGE",
"ADD_ON_REIMBURSEMENT_LANGUAGE": "Add On Reimbursement (Language)",
"ADD_ON_REIMBURSEMENT_IND": "Add On Reimbursement (Y/N)",
"SINGLE_CODE_MULTIPLE_RATES_LANGUAGE": "Single Code Multiple Rates (Language)",
"SINGLE_CODE_MULTIPLE_RATES_IND": "Single Code Multiple Rates (Y/N)",
"IP - DSH/IME/UC, included (Y/N)": "IP - DSH/IME/UC, included (Y/N)",
"IP - Stoploss Catastrophic Threshold": "IP - Stoploss Catastrophic Threshold",
"EXCLUSIONS": "Exclusions",
"NOT_TO_EXCEED": "Not to Exceed",
"RATE_ESCALATOR_IND": "Escalator or COLA (Y/N)",
"RATE_ESCALATOR_DT": "Escalator I, Eff. Date",
"CONTRACT_TITLE": "Agreement_Name (Contract Title)",
"PAYER_NAME": "PAYER NAME",
"AFFILIATION_CLAUSE_IND": "Affiliate (Y/N)",
"TERM_CLAUSE": "Term Clause",
"CONTRACT_AUTO_RENEWAL_IND": "Contract Auto-Renewal Indicator",
"CONTRACT_TERMINATION_DT": "Termination Date",
"TERMINATION_UPON_NOTICE": "Termination Upon Notice - Days",
"TERMINATION_UPON_CAUSE": "Termination With Cause - Days",
"AMEND_CONTRACT_NOTICE_IND": "Amend Contract Upon notice Flag (Y/N)",
"TIME_TO_OBJECT": "Timeframe to Object - Days",
"ASSIGNMENTS_CLAUSE_IND": "Assignments Clause (Y/N)",
"CONTRACT_EFFECTIVE_DT": "Contract Effective Date",
"PROV_GROUP_TIN": "IRS #",
"PROV_GROUP_NAME": "IRS_Name",
"PROV_GROUP_NPI": "NPI (10-digits)",
"PROV_GROUP_TIN_SIGNATORY": "PROV_GROUP_TIN_SIGNATORY",
'Prov Group TIN Signatory' : "PROV_GROUP_TIN_SIGNATORY",
"PROV_TIN_OTHER": "PROV_TIN_OTHER",
"Prov TIN Other" : "PROV_TIN_OTHER",
"PROV_NPI_OTHER": "PROV_NPI_OTHER",
'Prov NPI Other' : "PROV_NPI_OTHER",
"NOTICE_PROVIDER_NAME": "Notice to Provider Name",
"NOTICE_PROVIDER_ADDRESS": "Notice to Provider Address",
"HEALTH_PLAN_STATE": "Health Plan State",
"CREDENTIALING_APP_IND": "Credentialing Application Indicator",
"SEQUESTRATION_LANGUAGE": "Sequestration Language",
"SEQUESTRATION_LANGUAGE_IND": "Sequestration Reductions (Y/N)",
"NPI_NAME": "NPI Name",
"DELEGATED_TERMS_IND": "Delegated Function Indicator",
"DELEGATED_TERMS": "Delegated Terms",
"ECM": "ECM",
"NATIONAL_AGREEMENT_IND": "National Agreement Indicator",
"COST_SETTLEMENT_LANGUAGE_IND": "Cost Settlement (Y/N)",
"COST_SETTLEMENT_LANGUAGE": "Cost Settlement (Language)",
"LATE_PAID_CLAIMS_LANGUAGE_IND": "Late Paid Claims (Y/N)",
"LATE_PAID_CLAIMS_LANGUAGE": "Late Paid Claims (Language)",
"DEEMER_AMENDMENT": "Deemer Amendment",
"REGULATORY_REQUIREMENTS": "Regulatory Requirements",
"RECOVERY_RIGHTS": "Recovery Rights",
"ARBITRATION_AND_DISPUTES": "Arbitration and Disputes",
"EXCLUSIVITY_REQUIREMENT_LANGUAGE_IND": "Exclusivity Requirement (Y/N)",
"EXCLUSIVITY_REQUIREMENT_LANGUAGE": "Exclusivity Requirement (Language)",
"PAYOR": "Payor",
"PARTICIPATION_IN_PRODUCTS": "Participation in Products",
"CLEAN_CLAIM": "Clean Claim",
"INDEPENDENT_REVIEW_LANGUAGE_IND": "Independent Review (Y/N)",
"INDEPENDENT_REVIEW_LANGUAGE": "Independent Review (Language)",
"INDEMNIFICATION": "Indemnification",
"ACCESS_TO_MEDICAL_RECORDS": "Access to Medical Records",
"MEMBER_CONFINEMENT_DAYS_LANGUAGE_IND": "Member Confinement Days Language (Y/N)",
"MEMBER_CONFINEMENT_DAYS_LANGUAGE": "Member Confinement Days Language (Language)",
"NETWORK_ACCESS_FEES_LANGUAGE_IND": "Network Access Fees (Y/N)",
"NETWORK_ACCESS_FEES_LANGUAGE": "Network Access Fees (Language)",
"PAYMENT_IN_ADVANCE_OF_CLAIMS_SUBMISSION_LANGUAGE_IND": "Payment in Advance of Claims Submission Language (Y/N)",
"PAYMENT_IN_ADVANCE_OF_CLAIMS_SUBMISSION_LANGUAGE": "Payment in Advance of Claims Submission Language",
"ELIGIBILITY_VERIFICATION": "Eligibility Verification",
"PREAUTHORIZATION": "Preauthorization",
"POLICIES_AND_PROCEDURES": "Policies and Procedures",
"INSURANCE_REQUIREMENT": "Insurance Requirement",
"CARVEOUT_VENDORS": "Carve-Out Vendors",
"CONFLICTS_BETWEEN_CERTAIN_DOCUMENTS_LANGUAGE_IND": "Conflicts Between Certain Documents (Y/N)",
"CONFLICTS_BETWEEN_CERTAIN_DOCUMENTS_LANGUAGE": "Conflicts Between Certain Documents (Language)",
"RELATIONSHIP_OF_PARTIES_LANGUAGE_IND": "Relationship of Parties (Y/N)",
"RELATIONSHIP_OF_PARTIES_LANGUAGE": "Relationship of Parties (Language)",
"NONSTANDARD_APPEALS_PROCESS_LANGUAGE_IND": "Nonstandard Appeals Process (Y/N)",
"NONSTANDARD_APPEALS_PROCESS_LANGUAGE": "Nonstandard Appeals Process (Language)",
"PRODUCT_REMOVAL": "Product Removal",
"DISPARAGEMENT_PROHIBITION_LANGUAGE_IND": "Disparagement Prohibition (Y/N)",
"DISPARAGEMENT_PROHIBITION_LANGUAGE": "Disparagement Prohibition (Language)",
"CLAIMS_EDITING_LANGUAGE_IND": "Claims Editing Language (Y/N)",
"CLAIMS_EDITING_LANGUAGE": "Claims Editing Language (Language)",
"GUARANTEE_OF_PROVIDER_YIELD_LANGUAGE_IND": "Guarantee of Provider Yield (Y/N)",
"GUARANTEE_OF_PROVIDER_YIELD_LANGUAGE": "Guarantee of Provider Yield (Language)",
"HCBS_SERVICES": "HCBS Services",
"ADD_ON_REIMBURSEMENT_IND": "Add On Reimbursement (Y/N)",
"ADD_ON_REIMBURSEMENT_LANGUAGE": "Add On Reimbursement (Language)",
"PMPM": "PMPM",
"SINGLE_CODE_MULTIPLE_RATES_IND": "Single Code Multiple Rates (Y/N)",
"SINGLE_CODE_MULTIPLE_RATES_LANGUAGE": "Single Code Multiple Rates (Language)",
"INVOICE_PRICING_LANGUAGE_IND": "Invoice Pricing (Y/N)",
"INVOICE_PRICING_LANGUAGE": "Invoice Pricing (Language)",
"TEMPLATE": "Template",
"PROVIDER_BASED_BILLING_EXCLUSION_LANGUAGE_IND": "Provider-based Billing Exclusion (Y/N)",
"PROVIDER_BASED_BILLING_EXCLUSION_LANGUAGE": "Provider-based Billing Exclusion (Language)",
"NON_RENEWAL_LANGUAGE": "Non-Renewal Language",
"NON_RENEWAL_DAYS": "Non-Renewal - Days",
"TIMELY_FILING": "Timely Filing",
}
@@ -1,19 +1,17 @@
import os
import pandas as pd
import logging
import concurrent.futures
logging.getLogger().setLevel("ERROR")
import warnings
warnings.filterwarnings("ignore")
import postprocessing_funcs
import valid
import config
def b_postprocess(filename, combined_df, pages):
if combined_df.shape[0] > 0:
# Metadata fields
@@ -37,15 +35,12 @@ def b_postprocess(filename, combined_df, pages):
combined_df = postprocessing_funcs.clean_lob(combined_df, filename)
# Rename and reorder
combined_df.rename(columns=valid.B_MAPPING, inplace=True)
column_order = [
col for col in valid.B_MAPPING.values() if col in combined_df.columns
]
column_order = [col for col in valid.B_MAPPING.values() if col in combined_df.columns]
final_df = combined_df[column_order]
return final_df
else:
return combined_df
def read_individual(filepath, filename):
df_list = []
for file in os.listdir(filepath):
@@ -54,98 +49,76 @@ def read_individual(filepath, filename):
df_list.append(df)
return pd.concat(df_list, ignore_index=True)
all_column_mappings = valid.B_MAPPING.copy()
all_column_mappings.update(valid.AC_MAPPING)
# For Original ABC -
# For Duplicates -
# For Original ABC -
# For Duplicates -
# For Non-B: All AC1 and AC2 Fields
# TERM_CLAUSE, CONTRACT_EFFECTIVE_DATE, PROV_GROUP_TIN, PROV_GROUP_NPI, DEFAULT_TERM + [ALL PART 2 FIELDS]
# # ############################## READ - Clean ABC Part 1 + 3 New B Fields ##############################
abc = read_individual("output_individual/cnc_batch1_b", "b_output.csv")
abc = read_individual('output_individual/cnc_batch1_b', 'b_output.csv')
abc.drop(["Exclusions"], axis=1, inplace=True)
abc = abc[[col for col in abc.columns if "Unnamed" not in col]]
abc.rename(columns={v: k for k, v in all_column_mappings.items()}, inplace=True)
abc.rename(
columns={
"If rate is % of Payer or MCR [STANDARD]": "RATE_STANDARD",
"If rate is % of Payer or MCR [STANDARD]_Short": "RATE_SHORT",
"Lesser of Logic Language, included (Y/N)": "LESSER",
"Flat Fee": "FLAT_FEE_STANDARD",
"Reimb. Methodology_short": "SHORT_METHODOLOGY",
},
inplace=True,
)
abc["Filename"] = abc["Filename"].str.replace(".txt", "", regex=False)
abc.drop(['Exclusions'], axis=1, inplace=True)
abc = abc[[col for col in abc.columns if 'Unnamed' not in col]]
abc.rename(columns={v : k for k, v in all_column_mappings.items()}, inplace=True)
abc.rename(columns={'If rate is % of Payer or MCR [STANDARD]' : 'RATE_STANDARD', 'If rate is % of Payer or MCR [STANDARD]_Short' : 'RATE_SHORT',
'Lesser of Logic Language, included (Y/N)' : 'LESSER', 'Flat Fee' : 'FLAT_FEE_STANDARD', 'Reimb. Methodology_short' : 'SHORT_METHODOLOGY'}, inplace=True)
abc['Filename'] = abc['Filename'].str.replace('.txt', '', regex=False)
print("Clean ABC Part 1 + 3 New B Fields: ")
print(f"Filenames: {len(abc.Filename.unique())}", abc.Filename[0])
print(list(abc.columns))
print(
f"Invalid Cols: {[col for col in abc.columns if col not in all_column_mappings.keys()]}"
)
print(f"Invalid Cols: {[col for col in abc.columns if col not in all_column_mappings.keys()]}")
print(abc.shape)
# ############################## READ - AC2 For All Contracts ##############################
ac2 = read_individual("output_individual/cnc_batch1_ac", "ac_output.csv")
ac2 = ac2[[col for col in ac2.columns if "Unnamed" not in col]]
ac2.rename(columns={v: k for k, v in all_column_mappings.items()}, inplace=True)
ac2["Filename"] = ac2["Filename"].str.replace(".txt", "", regex=False)
ac2 = read_individual('output_individual/cnc_batch1_ac', 'ac_output.csv')
ac2 = ac2[[col for col in ac2.columns if 'Unnamed' not in col]]
ac2.rename(columns={v : k for k, v in all_column_mappings.items()}, inplace=True)
ac2['Filename'] = ac2['Filename'].str.replace('.txt', '', regex=False)
print("AC2 For All Contracts: ")
print(f"Filenames: {len(ac2.Filename.unique())}", ac2.Filename[0])
print(list(ac2.columns))
print(
f"Invalid Cols: {[col for col in ac2.columns if col not in all_column_mappings.keys()]}"
)
print(f"Invalid Cols: {[col for col in ac2.columns if col not in all_column_mappings.keys()]}")
print(ac2.shape)
# ############################## READ - New AC1 (3 fields) For All Contracts ##############################
ac1 = read_individual("output_individual/cnc_batch1_ac1", "ac_output.csv")
ac1 = read_individual('output_individual/cnc_batch1_ac1', 'ac_output.csv')
# ac1.columns = ['Filename', 'Contract Effective Date', 'IRS #', 'NPI (10-digits)']
ac1.rename(columns={v: k for k, v in all_column_mappings.items()}, inplace=True)
ac1["Filename"] = ac1["Filename"].str.replace(".txt", "", regex=False)
ac1.rename(columns={v : k for k, v in all_column_mappings.items()}, inplace=True)
ac1['Filename'] = ac1['Filename'].str.replace('.txt', '', regex=False)
print("New AC1 For All Contracts: ")
print(f"Filenames: {len(ac1.Filename.unique())}", ac1.Filename[0])
print(list(ac1.columns))
print(ac1.shape)
# ############################## READ - Original AC1 For All Contracts ##############################
ac = pd.read_csv("reference_files/CNC-1-AC.csv")
ac.rename(columns={v: k for k, v in all_column_mappings.items()}, inplace=True)
ac.rename(
columns={
"Evergreen, Fixed or Hard Term": "CONTRACT_AUTO_RENEWAL_IND",
"Sequestration Reductions, included [Medicare only] (Y/N)": "SEQUESTRATION_REDUCTIONS_IND",
},
inplace=True,
)
ac = pd.read_csv('reference_files/CNC-1-AC.csv')
ac.rename(columns={v : k for k, v in all_column_mappings.items()}, inplace=True)
ac.rename(columns={'Evergreen, Fixed or Hard Term' : 'CONTRACT_AUTO_RENEWAL_IND',
'Sequestration Reductions, included [Medicare only] (Y/N)' : 'SEQUESTRATION_REDUCTIONS_IND'}, inplace=True)
print("Original AC1 For Non-B Contracts: ")
print(f"Filenames: {len(ac.Filename.unique())}", ac.Filename[0])
print(list(ac.columns))
print(
f"Invalid Cols: {[col for col in ac.columns if col not in all_column_mappings.keys()]}"
)
print(f"Invalid Cols: {[col for col in ac.columns if col not in all_column_mappings.keys()]}")
print(ac.shape)
############################## MERGE - Original AC1 + 3 New AC1 Fields ##############################
ac.drop(
["CONTRACT_EFFECTIVE_DT", "PROV_GROUP_TIN", "PROV_GROUP_NPI"], axis=1, inplace=True
)
ac = pd.merge(ac, ac1, on="Filename", how="right") # .reset_index(drop=True)
ac.drop(['CONTRACT_EFFECTIVE_DT', 'PROV_GROUP_TIN', 'PROV_GROUP_NPI'], axis=1, inplace=True)
ac = pd.merge(ac, ac1, on='Filename', how='right') # .reset_index(drop=True)
# ac.rename(columns=all_column_mappings, inplace=True)
ac = ac[[col for col in ac.columns if "Unnamed" not in col]]
ac = ac[[col for col in ac.columns if 'Unnamed' not in col]]
print("Final AC1 (with 3 replaced fields)")
print(f"Filenames: {len(ac.Filename.unique())}")
print(list(ac.columns))
print(ac.shape)
############################## FILTER - Only AC1 that are not already in Clean ABC ##############################
ac_new_only = ac[~ac["Filename"].isin(abc.Filename.unique())]
ac_new_only = ac[~ac['Filename'].isin(abc.Filename.unique())]
print("AC New Only (with 3 replaced fields)")
print(f"Filenames: {len(ac_new_only.Filename.unique())}")
print(list(ac_new_only.columns))
@@ -158,41 +131,30 @@ abc = pd.concat([abc, ac_new_only], axis=0)
print("Updated ABC (With non-B AC1 Fields added)")
print(f"Filenames: {len(abc.Filename.unique())}")
print(list(abc.columns))
print(
f"Invalid Cols: {[col for col in abc.columns if col not in all_column_mappings.keys()]}"
)
print(f"Invalid Cols: {[col for col in abc.columns if col not in all_column_mappings.keys()]}")
print(abc.shape)
############################## MERGE - AC2 to abc ##############################
abc = abc[
[
col
for col in abc.columns
if col not in [c for c in ac2.columns if c != "Filename"]
]
]
abc = abc[[col for col in abc.columns if col not in [c for c in ac2.columns if c != 'Filename']]]
# Merge ac2 to abc, keeping the columns from ac2 when there is a conflict. Except for CREDENTIALING_APP_IND
# abc = pd.merge(abc, ac2, on='Filename', how='left')
abc = pd.merge(abc, ac2, on="Filename", how="left", suffixes=("", "_from_ac2"))
abc = pd.merge(abc, ac2, on='Filename', how='left', suffixes=('', '_from_ac2'))
for column in abc.columns:
if column.endswith("_from_ac2"):
if column.endswith('_from_ac2'):
orig_column = column[:-10] # Remove the '_from_ac2' suffix
if orig_column != "CREDENTIALING_APP_IND":
if orig_column != 'CREDENTIALING_APP_IND':
abc[orig_column] = abc[column]
abc.drop(column, axis=1, inplace=True)
print("Updated ABC (with Non-B AC1 fields, new AC2 field, new B fields)")
print(f"Filenames: {len(abc.Filename.unique())}")
print(list(abc.columns))
print(
f"Invalid Cols: {[col for col in abc.columns if col not in all_column_mappings.keys()]}"
)
print(f"Invalid Cols: {[col for col in abc.columns if col not in all_column_mappings.keys()]}")
print(abc.shape)
# abc.rename(columns=all_column_mappings, inplace=True)
# abc.to_csv('output_consolidated/CNC-Batch1-ABC-BeforePostprocessing.csv')
############################## POSTPROCESS - Final Postprocess Step ##############################
def postprocess_ad_hoc(combined_df):
# B Steps
@@ -205,7 +167,7 @@ def postprocess_ad_hoc(combined_df):
print(combined_df.shape)
combined_df = postprocessing_funcs.clean_default_term(combined_df)
print(combined_df.shape)
# combined_df = postprocessing_funcs.clean_msr_lesser(combined_df) # Modify for ad hoc
# combined_df = postprocessing_funcs.clean_prov_2(combined_df) # Modify for ad hoc
# combined_df = postprocessing_funcs.clean_lob(combined_df, "")
@@ -216,11 +178,11 @@ def postprocess_ad_hoc(combined_df):
print(combined_df.shape)
return combined_df
abc_final = postprocess_ad_hoc(abc)
abc_final.rename(columns=all_column_mappings, inplace=True)
abc_final.to_csv("output_consolidated/CNC-1-RERUN-DRAFT4.csv")
abc_final.to_csv('output_consolidated/CNC-1-RERUN-DRAFT4.csv')
print("Final ABC")
print(f"Filenames: {len(abc.Filename.unique())}")
print(list(abc_final.columns))
print(abc_final.shape)
@@ -1,19 +1,17 @@
import os
import pandas as pd
import logging
import concurrent.futures
logging.getLogger().setLevel("ERROR")
import warnings
warnings.filterwarnings("ignore")
import postprocessing_funcs
import valid
import config
def b_postprocess(filename, combined_df, pages):
if combined_df.shape[0] > 0:
# Metadata fields
@@ -37,15 +35,12 @@ def b_postprocess(filename, combined_df, pages):
combined_df = postprocessing_funcs.clean_lob(combined_df, filename)
# Rename and reorder
combined_df.rename(columns=valid.B_MAPPING, inplace=True)
column_order = [
col for col in valid.B_MAPPING.values() if col in combined_df.columns
]
column_order = [col for col in valid.B_MAPPING.values() if col in combined_df.columns]
final_df = combined_df[column_order]
return final_df
else:
return combined_df
def read_individual(filepath, filename):
df_list = []
for file in os.listdir(filepath):
@@ -63,115 +58,92 @@ all_column_mappings.update(valid.AC_MAPPING)
# ############################## READ - Clean AC1 + B + 3 New B ##############################
abc = read_individual("output_individual/cnc_batch2_b", "b_output.csv")
abc.drop(["Exclusions"], axis=1, inplace=True)
abc = abc[[col for col in abc.columns if "Unnamed" not in col]]
abc.rename(columns={v: k for k, v in all_column_mappings.items()}, inplace=True)
abc.rename(
columns={
"If rate is % of Payer or MCR [STANDARD]": "RATE_STANDARD",
"If rate is % of Payer or MCR [STANDARD]_Short": "RATE_SHORT",
"Lesser of Logic Language, included (Y/N)": "LESSER",
"Flat Fee": "FLAT_FEE_STANDARD",
"Reimb. Methodology_short": "SHORT_METHODOLOGY",
},
inplace=True,
)
abc["Filename"] = abc["Filename"].str.replace(".txt", "", regex=False)
abc = read_individual('output_individual/cnc_batch2_b', 'b_output.csv')
abc.drop(['Exclusions'], axis=1, inplace=True)
abc = abc[[col for col in abc.columns if 'Unnamed' not in col]]
abc.rename(columns={v : k for k, v in all_column_mappings.items()}, inplace=True)
abc.rename(columns={'If rate is % of Payer or MCR [STANDARD]' : 'RATE_STANDARD', 'If rate is % of Payer or MCR [STANDARD]_Short' : 'RATE_SHORT',
'Lesser of Logic Language, included (Y/N)' : 'LESSER', 'Flat Fee' : 'FLAT_FEE_STANDARD', 'Reimb. Methodology_short' : 'SHORT_METHODOLOGY'}, inplace=True)
abc['Filename'] = abc['Filename'].str.replace('.txt', '', regex=False)
print("\n\nABC + New B: ")
print(abc.shape)
print(list(abc.columns))
print(
f"Invalid Cols: {[col for col in abc.columns if col not in all_column_mappings.keys()]}"
)
print(f"Invalid Cols: {[col for col in abc.columns if col not in all_column_mappings.keys()]}")
# ############################## READ - New AC2 + Smart Chunked ##############################
ac2 = read_individual("output_individual/cnc_batch2_ac", "ac_output.csv")
ac2 = ac2[[col for col in ac2.columns if "Unnamed" not in col]]
ac2.rename(columns={v: k for k, v in all_column_mappings.items()}, inplace=True)
ac2.drop(["TERM_CLAUSE"], axis=1, inplace=True)
ac2["Filename"] = ac2["Filename"].str.replace(".txt", "", regex=False)
ac2 = read_individual('output_individual/cnc_batch2_ac', 'ac_output.csv')
ac2 = ac2[[col for col in ac2.columns if 'Unnamed' not in col]]
ac2.rename(columns={v : k for k, v in all_column_mappings.items()}, inplace=True)
ac2.drop(['TERM_CLAUSE'], axis=1, inplace=True)
ac2['Filename'] = ac2['Filename'].str.replace('.txt', '', regex=False)
print("\n\nNew AC2: ")
print(ac2.shape)
print(list(ac2.columns))
print(
f"Invalid Cols: {[col for col in ac2.columns if col not in all_column_mappings.keys()]}"
)
print(f"Invalid Cols: {[col for col in ac2.columns if col not in all_column_mappings.keys()]}")
# ############################## READ - New AC1 (3 fields) ##############################
ac1 = read_individual("output_individual/cnc_batch2_ac1", "ac_output.csv")
ac1 = read_individual('output_individual/cnc_batch2_ac1', 'ac_output.csv')
# ac1.columns = ['Filename', 'Contract Effective Date', 'IRS #', 'NPI (10-digits)']
ac1.rename(columns={v: k for k, v in all_column_mappings.items()}, inplace=True)
ac1.rename(columns={v : k for k, v in all_column_mappings.items()}, inplace=True)
print("\n\nNew AC1: ")
print(ac1.shape)
print(list(ac1.columns))
# ############################## READ - Original AC1 ##############################
ac = pd.read_excel("reference_files/CNC-2.0-AC.xlsx")
ac.rename(columns={v: k for k, v in all_column_mappings.items()}, inplace=True)
ac.rename(
columns={
"Evergreen, Fixed or Hard Term": "CONTRACT_AUTO_RENEWAL_IND",
"Sequestration Reductions, included [Medicare only] (Y/N)": "SEQUESTRATION_REDUCTIONS_IND",
},
inplace=True,
)
ac["Filename"] = ac["Filename"].str.replace(".txt", "", regex=False)
ac = ac[ac["Filename"].isin(abc["Filename"])]
ac = pd.read_excel('reference_files/CNC-2.0-AC.xlsx')
ac.rename(columns={v : k for k, v in all_column_mappings.items()}, inplace=True)
ac.rename(columns={'Evergreen, Fixed or Hard Term' : 'CONTRACT_AUTO_RENEWAL_IND',
'Sequestration Reductions, included [Medicare only] (Y/N)' : 'SEQUESTRATION_REDUCTIONS_IND'}, inplace=True)
ac['Filename'] = ac['Filename'].str.replace('.txt', '', regex=False)
ac = ac[ac['Filename'].isin(abc['Filename'])]
print("\n\nOriginal AC1: ")
print(ac.shape)
print(list(ac.columns))
print(
f"Invalid Cols: {[col for col in ac.columns if col not in all_column_mappings.keys()]}"
)
print(f"Invalid Cols: {[col for col in ac.columns if col not in all_column_mappings.keys()]}")
############################## READ - Missing ABC ##############################
base_path = "output_individual/cnc_batch2_b_missing"
base_path = 'output_individual/cnc_batch2_b_missing'
ac_frames = []
b_frames = []
for subdir in os.listdir(base_path):
subdir_path = os.path.join(base_path, subdir)
# Construct file paths
ac_file_path = os.path.join(subdir_path, "test_batch-AC.csv")
b_file_path = os.path.join(subdir_path, "test_batch-B.csv")
ac_file_path = os.path.join(subdir_path, 'test_batch-AC.csv')
b_file_path = os.path.join(subdir_path, 'test_batch-B.csv')
# Check if files exist and then read them
if os.path.exists(ac_file_path):
ac_df = pd.read_csv(ac_file_path)
ac_df.rename(columns={v: k for k, v in valid.AC_MAPPING.items()}, inplace=True)
ac_df.rename(columns={v : k for k, v in valid.AC_MAPPING.items()}, inplace=True)
ac_frames.append(ac_df)
if os.path.exists(b_file_path):
b_df = pd.read_csv(b_file_path)
b_df.rename(columns={v: k for k, v in valid.B_MAPPING.items()}, inplace=True)
b_df.rename(columns={v : k for k, v in valid.B_MAPPING.items()}, inplace=True)
b_frames.append(b_df)
ac_missing = pd.concat(ac_frames, ignore_index=True)
b_missing = pd.concat(b_frames, ignore_index=True)
b_missing = b_missing[[col for col in b_missing.columns if ".1" not in col]]
b_missing = b_missing[[col for col in b_missing.columns if '.1' not in col]]
abc_missing = pd.merge(ac_missing, b_missing, on="Filename", how="left")
abc_missing.rename(columns={v: k for k, v in all_column_mappings.items()}, inplace=True)
abc_missing["Filename"] = abc_missing["Filename"].str.replace(".txt", "", regex=False)
abc_missing = pd.merge(ac_missing, b_missing, on='Filename', how='left')
abc_missing.rename(columns={v : k for k, v in all_column_mappings.items()}, inplace=True)
abc_missing['Filename'] = abc_missing['Filename'].str.replace('.txt', '', regex=False)
print("\n\nABC missing: ")
print(abc_missing.shape)
print(list(abc_missing.columns))
print(
f"Invalid Cols: {[col for col in abc_missing.columns if col not in all_column_mappings.keys()]}"
)
print(f"Invalid Cols: {[col for col in abc_missing.columns if col not in all_column_mappings.keys()]}")
# ############################## MERGE - Original AC + New AC1 ##############################
ac.drop(
["CONTRACT_EFFECTIVE_DT", "PROV_GROUP_TIN", "PROV_GROUP_NPI"], axis=1, inplace=True
)
ac = pd.merge(ac, ac1, on="Filename", how="right") # .reset_index(drop=True)
ac.drop(['CONTRACT_EFFECTIVE_DT', 'PROV_GROUP_TIN', 'PROV_GROUP_NPI'], axis=1, inplace=True)
ac = pd.merge(ac, ac1, on='Filename', how='right') # .reset_index(drop=True)
# ac.rename(columns=all_column_mappings, inplace=True)
ac = ac[[col for col in ac.columns if "Unnamed" not in col]]
ac = ac[[col for col in ac.columns if 'Unnamed' not in col]]
print("\n\nFinal AC1 (with 3 replaced fields)")
print(ac.shape)
print(list(ac.columns))
@@ -184,25 +156,15 @@ abc = pd.concat([abc, ac], axis=0)
print("\n\nUpdated ABC (With non-B AC1 Fields added)")
print(abc.shape)
print(list(abc.columns))
print(
f"Invalid Cols: {[col for col in abc.columns if col not in all_column_mappings.keys()]}"
)
print(f"Invalid Cols: {[col for col in abc.columns if col not in all_column_mappings.keys()]}")
############################## MERGE - AC2 to abc ##############################
abc = abc[
[
col
for col in abc.columns
if col not in [c for c in ac2.columns if c != "Filename"]
]
]
abc = pd.merge(abc, ac2, on="Filename", how="left")
abc = abc[[col for col in abc.columns if col not in [c for c in ac2.columns if c != 'Filename']]]
abc = pd.merge(abc, ac2, on='Filename', how='left')
print("\n\nUpdated ABC (with Non-B AC1 fields, new AC2 field, new B fields)")
print(list(abc.columns))
print(
f"Invalid Cols: {[col for col in abc.columns if col not in all_column_mappings.keys()]}"
)
print(f"Invalid Cols: {[col for col in abc.columns if col not in all_column_mappings.keys()]}")
print(abc.shape)
print(abc.TERM_CLAUSE.value_counts())
@@ -211,14 +173,10 @@ print(abc.TERM_CLAUSE.value_counts())
############################## ADD - ABC missing to abc ##############################
abc = pd.concat([abc, abc_missing], axis=0)
print(
"\n\nFinal ABC (with Non-B AC1 fields, new AC2 fields, new B fields, missing ABC files)"
)
print("\n\nFinal ABC (with Non-B AC1 fields, new AC2 fields, new B fields, missing ABC files)")
print(abc.shape)
print(list(abc.columns))
print(
f"Invalid Cols: {[col for col in abc.columns if col not in all_column_mappings.keys()]}"
)
print(f"Invalid Cols: {[col for col in abc.columns if col not in all_column_mappings.keys()]}")
############################## POSTPROCESS - Final Postprocess Step ##############################
@@ -233,9 +191,9 @@ def postprocess_ad_hoc(combined_df):
print(combined_df.shape)
combined_df = postprocessing_funcs.clean_default_term(combined_df)
print(combined_df.shape)
# combined_df = postprocessing_funcs.clean_msr_lesser(combined_df) # Modify for ad hoc
combined_df = postprocessing_funcs.clean_prov_2(combined_df) # Modify for ad hoc
combined_df = postprocessing_funcs.clean_prov_2(combined_df) # Modify for ad hoc
# combined_df = postprocessing_funcs.clean_lob(combined_df, "")
# AC Steps
@@ -244,24 +202,23 @@ def postprocess_ad_hoc(combined_df):
print(combined_df.shape)
return combined_df
abc_final = postprocess_ad_hoc(abc)
abc_final.rename(columns=all_column_mappings, inplace=True)
print("\n\nFinal ABC")
print(abc_final.shape)
print(f"Unique files: {abc_final['Contract Name'].unique()}")
print(f'Unique files: {abc_final['Contract Name'].unique()}')
print(list(abc_final.columns))
print(
f"Invalid Cols: {[col for col in abc_final.columns if col not in valid.ABC_COLUMNS]}"
)
print(f"Invalid Cols: {[col for col in abc_final.columns if col not in valid.ABC_COLUMNS]}")
abc_final = abc_final[[col for col in valid.ABC_COLUMNS if col in abc_final.columns]]
print("\n\nFinal ABC")
print(abc_final.shape)
print(list(abc_final.columns))
print(
f"Invalid Cols: {[col for col in abc_final.columns if col not in valid.ABC_COLUMNS]}"
)
print(f"Invalid Cols: {[col for col in abc_final.columns if col not in valid.ABC_COLUMNS]}")
abc_final.to_csv('output_consolidated/CNC-2-RERUN-DRAFT4.csv')
abc_final.to_csv("output_consolidated/CNC-2-RERUN-DRAFT4.csv")
@@ -1,16 +1,14 @@
import os
import pandas as pd
pd.set_option("display.max_columns", None)
pd.set_option("display.max_rows", None)
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', None)
import logging
import concurrent.futures
logging.getLogger().setLevel("ERROR")
from concurrent.futures import ThreadPoolExecutor
import warnings
warnings.filterwarnings("ignore")
import postprocessing_funcs
@@ -18,7 +16,6 @@ import valid
import config
import prompts
def b_postprocess(filename, combined_df, pages):
if combined_df.shape[0] > 0:
# Metadata fields
@@ -42,15 +39,12 @@ def b_postprocess(filename, combined_df, pages):
combined_df = postprocessing_funcs.clean_lob(combined_df, filename)
# Rename and reorder
combined_df.rename(columns=valid.B_MAPPING, inplace=True)
column_order = [
col for col in valid.B_MAPPING.values() if col in combined_df.columns
]
column_order = [col for col in valid.B_MAPPING.values() if col in combined_df.columns]
final_df = combined_df[column_order]
return final_df
else:
return combined_df
def read_csv_file(full_path, filename):
"""Helper function to read CSV file."""
try:
@@ -60,7 +54,6 @@ def read_csv_file(full_path, filename):
# print(f"Failed to read {os.path.join(full_path, filename)}: {e}")
return None
def read_individual(filepath, filename):
df_list = []
# Use ThreadPoolExecutor to handle file reading in parallel
@@ -71,13 +64,13 @@ def read_individual(filepath, filename):
full_path = os.path.join(filepath, file)
# Schedule the read_csv_file function to be executed and store the future object
futures.append(executor.submit(read_csv_file, full_path, filename))
# As each future completes, get the result (a DataFrame or None)
for future in futures:
result = future.result()
if result is not None:
df_list.append(result)
# Concatenate all the DataFrames in the list
if df_list:
return pd.concat(df_list, ignore_index=True)
@@ -86,18 +79,18 @@ def read_individual(filepath, filename):
all_column_mappings = valid.B_MAPPING.copy()
all_column_mappings.update(valid.AC_MAPPING)
abc_path = "output_individual/cnc_3_b"
ac1_original_path = "output_consolidated/CNC-3.0-A-AC.csv"
ac1_new_path = "output_individual/cnc_3_ac1"
ac2_path = "output_individual/cnc_3_ac2"
abc_path = 'output_individual/cnc_3_b'
ac1_original_path = 'output_consolidated/CNC-3.0-A-AC.csv'
ac1_new_path = 'output_individual/cnc_3_ac1'
ac2_path = 'output_individual/cnc_3_ac2'
# Read New AC2
# ac2 = read_individual(ac2_path, 'ac_output.csv')
# ac2.rename(columns={v : k for k, v in all_column_mappings.items()}, inplace=True)
# ac2.to_csv('reference_files/ac2_new.csv')
ac2 = pd.read_csv("reference_files/ac2_new.csv")
ac2 = ac2[[col for col in ac2.columns if "Unnamed" not in col]]
ac2["Filename"] = ac2["Filename"].str.replace(".txt", "", regex=False)
ac2 = pd.read_csv('reference_files/ac2_new.csv')
ac2 = ac2[[col for col in ac2.columns if 'Unnamed' not in col]]
ac2['Filename'] = ac2['Filename'].str.replace('.txt', '', regex=False)
print("\n\nNew AC2")
print(f"Unique Filenames: {len(ac2['Filename'].unique())}")
print(ac2.shape)
@@ -115,8 +108,8 @@ print(list(ac2.columns))
# overlapping_columns = [col for col in abc.columns if col in ac2.columns and col != 'Filename']
# abc.drop(overlapping_columns, axis=1, inplace=True)
# abc.to_csv('reference_files/abc_original.csv')
abc = pd.read_csv("reference_files/abc_original.csv")
abc = abc[[col for col in abc.columns if "Unnamed" not in col]]
abc = pd.read_csv('reference_files/abc_original.csv')
abc = abc[[col for col in abc.columns if 'Unnamed' not in col]]
print("\n\nOriginal A1B + 3 New B")
print(f"Unique Filenames: {len(abc.Filename.unique())}")
print(abc.shape)
@@ -132,8 +125,8 @@ print(list(abc.columns))
# overlapping_columns = [col for col in ac1.columns if col in ac2.columns and col != 'Filename']
# ac1.drop(overlapping_columns, axis=1, inplace=True)
# ac1.to_csv('reference_files/ac1_original.csv')
ac1 = pd.read_csv("reference_files/ac1_original.csv")
ac1 = ac1[[col for col in ac1.columns if "Unnamed" not in col]]
ac1 = pd.read_csv('reference_files/ac1_original.csv')
ac1 = ac1[[col for col in ac1.columns if 'Unnamed' not in col]]
print("\n\nOriginal AC1")
print(f"Unique Filenames: {len(ac1['Filename'].unique())}")
print(ac1.shape)
@@ -144,18 +137,16 @@ print(list(ac1.columns))
# ac1_new.rename(columns={v : k for k, v in all_column_mappings.items()}, inplace=True)
# ac1_new['Filename'] = ac1_new['Filename'].str.replace('.txt', '', regex=False)
# ac1_new.to_csv('reference_files/ac1_new.csv')
ac1_new = pd.read_csv("reference_files/ac1_new.csv")
ac1_new = ac1_new[[col for col in ac1_new.columns if "Unnamed" not in col]]
ac1_new = pd.read_csv('reference_files/ac1_new.csv')
ac1_new = ac1_new[[col for col in ac1_new.columns if 'Unnamed' not in col]]
print("\n\n3 New AC1")
print(f"Unique Filenames: {len(ac1_new.Filename.unique())}")
print(ac1_new.shape)
print(list(ac1_new.columns))
# Merge 3 new AC1 to original AC1
ac1.drop(
["CONTRACT_EFFECTIVE_DT", "PROV_GROUP_TIN", "PROV_GROUP_NPI"], axis=1, inplace=True
)
ac1_full = pd.merge(ac1, ac1_new, on="Filename", how="left")
ac1.drop(['CONTRACT_EFFECTIVE_DT', 'PROV_GROUP_TIN', 'PROV_GROUP_NPI'], axis=1, inplace=True)
ac1_full = pd.merge(ac1, ac1_new, on='Filename', how='left')
print("\n\nAC 1 Full")
print(f"Unique Filenames: {len(ac1_full.Filename.unique())}")
print(ac1_full.shape)
@@ -163,13 +154,7 @@ print(list(ac1_full.columns))
# Append Full AC1 to Original ABC
abc = pd.concat([abc, ac1_full], axis=0)
abc = abc[
[
col
for col in abc.columns
if col not in [c for c in ac2.columns if c != "Filename"]
]
]
abc = abc[[col for col in abc.columns if col not in [c for c in ac2.columns if c != 'Filename']]]
print("\n\nFull ABC")
print(f"Unique Filenames: {len(abc['Filename'].unique())}")
print(abc.shape)
@@ -177,18 +162,17 @@ print(list(abc.columns))
# Merge AC2
# Filenames in abc not in ac2
print(abc[~abc["Filename"].isin(ac2["Filename"])]["Filename"].unique())
print(abc[~abc['Filename'].isin(ac2['Filename'])]['Filename'].unique())
# Filenames in ac2 not in abc
print(ac2[~ac2["Filename"].isin(abc["Filename"])]["Filename"].unique())
print(ac2[~ac2['Filename'].isin(abc['Filename'])]['Filename'].unique())
abc = pd.merge(abc, ac2, on="Filename", how="left")
abc = pd.merge(abc, ac2, on='Filename', how='left')
print("\n\nABC Full Final (before postprocessing)")
print(f"Unique Filenames: {len(abc.Filename.unique())}")
print(abc.shape)
print(list(abc.columns))
#### Postprocess ####
def postprocess_ad_hoc(combined_df):
# B Steps
@@ -200,7 +184,7 @@ def postprocess_ad_hoc(combined_df):
print(combined_df.shape)
combined_df = postprocessing_funcs.clean_default_term(combined_df)
print(combined_df.shape)
# combined_df = postprocessing_funcs.clean_msr_lesser(combined_df) # Modify for ad hoc
# combined_df = postprocessing_funcs.clean_prov_2(combined_df) # Modify for ad hoc
# combined_df = postprocessing_funcs.clean_lob(combined_df, "")
@@ -211,7 +195,6 @@ def postprocess_ad_hoc(combined_df):
print(combined_df.shape)
return combined_df
abc_final = postprocess_ad_hoc(abc)
abc_final.rename(columns=all_column_mappings, inplace=True)
print("ABC Final (after postprocessing)")
@@ -225,4 +208,12 @@ print(f"Unique Filenames: {len(abc_final['Contract Name'].unique())}")
print(abc_final.shape)
print(list(abc_final.columns))
abc_final.to_csv("output_consolidated/CNC-3-RERUN-DRAFT6.csv")
abc_final.to_csv('output_consolidated/CNC-3-RERUN-DRAFT6.csv')
@@ -1,7 +1,7 @@
import pandas as pd
pd.set_option("display.max_columns", None)
pd.set_option("display.max_rows", None)
import pandas as pd
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', None)
import numpy as np
from concurrent.futures import ThreadPoolExecutor
import os
@@ -17,17 +17,17 @@ import valid
def clean_prov_2(df):
valid_types = valid.select_valid_prov_2(df["Provider Type"])
target_rows = df[df["Provider Type - Level 2"].apply(is_empty)]
valid_types = valid.select_valid_prov_2(df['Provider Type'])
target_rows = df[df['Provider Type - Level 2'].apply(is_empty)]
def find_exact_match(text):
if pd.isna(text) or text == "":
if pd.isna(text) or text == '':
return None
words = re.findall(r"\b[\w/]+(?:[-\s][\w/]+)*\b", text)
words = re.findall(r'\b[\w/]+(?:[-\s][\w/]+)*\b', text)
for i in range(len(words)):
for j in range(i + 1, len(words) + 1):
phrase = " ".join(words[i:j])
for j in range(i+1, len(words)+1):
phrase = ' '.join(words[i:j])
if phrase in valid_types: # Case-sensitive matching
return phrase
return None
@@ -35,60 +35,55 @@ def clean_prov_2(df):
for index, row in target_rows.iterrows():
match = None
if not is_empty(row["Service Type"]):
match = find_exact_match(str(row["Service Type"]))
if not is_empty(row['Service Type']):
match = find_exact_match(str(row['Service Type']))
if match:
df.at[index, "Provider Type - Level 2"] = match
df.at[index, 'Provider Type - Level 2'] = match
continue
if not is_empty(row["Attachment/Exhibit"]):
match = find_exact_match(str(row["Attachment/Exhibit"]))
if not is_empty(row['Attachment/Exhibit']):
match = find_exact_match(str(row['Attachment/Exhibit']))
if match:
df.at[index, "Provider Type - Level 2"] = match
df.at[index, 'Provider Type - Level 2'] = match
continue
exhibit_rows = df[df["Attachment/Exhibit"] == row["Attachment/Exhibit"]]
exhibit_rows = df[df['Attachment/Exhibit'] == row['Attachment/Exhibit']]
if not exhibit_rows.empty:
for _, exhibit_row in exhibit_rows.iterrows():
if not is_empty(exhibit_row["Provider Type - Level 2"]):
match = find_exact_match(
str(exhibit_row["Provider Type - Level 2"])
)
if not is_empty(exhibit_row['Provider Type - Level 2']):
match = find_exact_match(str(exhibit_row['Provider Type - Level 2']))
if match:
df.at[index, "Provider Type - Level 2"] = match
df.at[index, 'Provider Type - Level 2'] = match
break
elif not is_empty(exhibit_row["Service Type"]):
match = find_exact_match(str(exhibit_row["Service Type"]))
elif not is_empty(exhibit_row['Service Type']):
match = find_exact_match(str(exhibit_row['Service Type']))
if match:
df.at[index, "Provider Type - Level 2"] = match
df.at[index, 'Provider Type - Level 2'] = match
break
# Final check to ensure no invalid values were assigned
invalid_assignments = df[
(~df["Provider Type - Level 2"].isin(valid_types))
& (~df["Provider Type - Level 2"].apply(is_empty))
(~df['Provider Type - Level 2'].isin(valid_types)) &
(~df['Provider Type - Level 2'].apply(is_empty))
]
if not invalid_assignments.empty:
df.loc[invalid_assignments.index, "Provider Type - Level 2"] = ""
df.loc[invalid_assignments.index, 'Provider Type - Level 2'] = ''
return df
def get_new_effective_date(unique_contract_names):
new_dates = {}
for contract_name in unique_contract_names:
if contract_name + ".txt" in input_dict.keys():
contract_text = input_dict[contract_name + ".txt"]
if contract_name+'.txt' in input_dict.keys():
contract_text = input_dict[contract_name+'.txt']
try:
prompt = f"""### Contract Start ### {contract_text} ### Contract End ###
Above is a contract. What is the contract effective date mentioned in any of the following locations: the signatory section, the preamble of the agreement, or the start of the amendment? Look for phrases such as /'This amendment is effective/'.
If there is no clear effective date, return the date from the signature page.
Return the date converted to YYYY-MM-DD format, with no other commentary or explanation.
"""
date_answer = claude_funcs.invoke_claude(
prompt, config.MODEL_ID_CLAUDE35_SONNET, contract_name, 128
)
date_answer = claude_funcs.invoke_claude(prompt, config.MODEL_ID_CLAUDE35_SONNET, contract_name, 128)
print(date_answer)
new_dates[contract_name] = date_answer
except:
@@ -97,37 +92,33 @@ def get_new_effective_date(unique_contract_names):
If there is no clear effective date, return the date from the signature page.
Return the date converted to YYYY-MM-DD format, with no other commentary or explanation.
"""
date_answer = claude_funcs.invoke_claude(
prompt, config.MODEL_ID_CLAUDE35_SONNET, contract_name, 128
)
date_answer = claude_funcs.invoke_claude(prompt, config.MODEL_ID_CLAUDE35_SONNET, contract_name, 128)
new_dates[contract_name] = date_answer
return new_dates
#################### Process Starts Here ###################
abc = pd.read_csv("output_consolidated/CNC-3-RERUN-DRAFT6.csv")
abc = pd.read_csv('output_consolidated/CNC-3-RERUN-DRAFT6.csv')
# null_counts = abc.isnull().sum()
# print(null_counts)
# quit()
input_dict = utils.read_input("data_cnc/batch3A")
input_dict = utils.read_input('data_cnc/batch3A')
# Clean Prov 2
abc_grouped = abc.groupby("Contract Name")
abc_grouped = abc.groupby('Contract Name')
abc_clean = pd.concat([clean_prov_2(group) for name, group in abc_grouped])
# Effective Date
contains_meridian = abc_clean["PAYER NAME"].str.contains(
"meridian", case=False, na=False
)
contains_meridian = abc_clean['PAYER NAME'].str.contains('meridian', case=False, na=False)
# Use your utility function to identify empty dates
empty_dates = abc_clean["Contract Effective Date"].apply(utils.is_empty)
empty_dates = abc_clean['Contract Effective Date'].apply(utils.is_empty)
# Combine filters to find the relevant 'Contract Names'
relevant_contracts = abc_clean[contains_meridian & empty_dates][
"Contract Name"
].unique()
relevant_contracts = abc_clean[contains_meridian & empty_dates]['Contract Name'].unique()
# Get new effective dates for these contracts
new_effective_dates = get_new_effective_date(relevant_contracts)
@@ -135,13 +126,11 @@ new_effective_dates = get_new_effective_date(relevant_contracts)
# Apply the new effective dates to the DataFrame
for contract_name, new_date in new_effective_dates.items():
# Find rows with this 'Contract Name' where dates need replacing
condition = (
(abc_clean["Contract Name"] == contract_name) & contains_meridian & empty_dates
)
abc_clean.loc[condition, "Contract Effective Date"] = new_date
condition = (abc_clean['Contract Name'] == contract_name) & contains_meridian & empty_dates
abc_clean.loc[condition, 'Contract Effective Date'] = new_date
abc_clean.to_csv("output_consolidated/CNC-3-RERUN-DRAFT7.csv")
abc_clean.to_csv('output_consolidated/CNC-3-RERUN-DRAFT7.csv')
print("ABC Full Final (after column renaming)")
print(f"Unique Filenames: {len(abc_clean['Contract Name'].unique())}")
@@ -149,6 +138,7 @@ print(abc_clean.shape)
print(list(abc_clean.columns))
# abc = pd.read_excel('output_consolidated/CNC-3-RERUN-DRAFT6.csv')
# print(abc.shape)
# print(len(abc['Contract Effective Date'].unique()))
@@ -165,4 +155,4 @@ print(list(abc_clean.columns))
# print(len(abc_postprocessed['Contract Effective Date'].unique()))
# print(list(abc_postprocessed['Contract Effective Date'].unique()))
# abc_postprocessed.to_csv('output_consolidated/CNC-1-RERUN-DRAFT7.csv')
# abc_postprocessed.to_csv('output_consolidated/CNC-1-RERUN-DRAFT7.csv')
@@ -9,7 +9,6 @@ import logging
logging.getLogger().setLevel("ERROR")
import warnings
warnings.filterwarnings("ignore")
"""
@@ -17,7 +16,6 @@ This script consolidates a&c fields. Its data sources are `reference_files/CNC-
as well as .csv files from `output_individual/cnc_batch[1/2]_ac/[name of provider]/ac_output.csv.
"""
def b_postprocess(combined_df, filename):
if combined_df.shape[0] > 0:
# Add Single Code, Multiple Rate
@@ -31,7 +29,7 @@ def b_postprocess(combined_df, filename):
# Clean MSR
combined_df = postprocessing_funcs.clean_msr_lesser(combined_df)
# Clean Default - commented out bc DEFAULT_TERM is full of NAs
try:
combined_df = postprocessing_funcs.clean_default_term(combined_df)
@@ -45,16 +43,14 @@ def b_postprocess(combined_df, filename):
try:
combined_df = postprocessing_funcs.clean_lesser_rate(combined_df)
except Exception as e:
pass
pass
# Clean LOB - commented out until we're ready
# combined_df = postprocessing_funcs.clean_lob(combined_df, filename)
# Rename and reorder
combined_df.rename(columns=valid.B_MAPPING, inplace=True)
column_order = [
col for col in valid.B_MAPPING.values() if col in combined_df.columns
]
column_order = [col for col in valid.B_MAPPING.values() if col in combined_df.columns]
final_df = combined_df[column_order]
return final_df
else:
@@ -64,18 +60,16 @@ def b_postprocess(combined_df, filename):
##### Batch 1 #####
# New B Outputs
print("Starting B")
dir_batch_1_individual_b_outputs = "output_individual/cnc_batch1_b/"
dir_batch_1_individual_b_outputs = 'output_individual/cnc_batch1_b/'
fn_b_batch_1_outputs_list = [
dir_batch_1_individual_b_outputs + f + "/b_output.csv"
for f in os.listdir(dir_batch_1_individual_b_outputs)
]
fn_b_batch_1_outputs_list = [dir_batch_1_individual_b_outputs + f + "/b_output.csv"
for f in os.listdir(dir_batch_1_individual_b_outputs)]
pre_concat_list_b_batch_1 = []
print("starting B concatenation for batch 1")
print(str(len(fn_b_batch_1_outputs_list)) + " B files pre-CSV reading")
for fn in fn_b_batch_1_outputs_list:
# try:
# try:
file_b_df = pd.read_csv(fn)
# for colname in file_b_df.columns:
# print(colname)
@@ -84,8 +78,8 @@ for fn in fn_b_batch_1_outputs_list:
# print(fn)
file_b_df = b_postprocess(file_b_df, fn)
pre_concat_list_b_batch_1.append(file_b_df)
# except Exception as e:
# print(f"Appending failed on {fn} with exception {e}")
# except Exception as e:
# print(f"Appending failed on {fn} with exception {e}")
print(str(len(pre_concat_list_b_batch_1)) + " B files post-CSV reading")
abc = pd.concat(pre_concat_list_b_batch_1, axis=1)
@@ -93,7 +87,7 @@ abc = pd.concat(pre_concat_list_b_batch_1, axis=1)
abc.to_csv("src/scratchfiles/b_concat_df.csv", index=False)
abc.drop(["Exclusions", "Unnamed: 0"], axis=1, inplace=True)
abc.drop(['Exclusions', 'Unnamed: 0'], axis=1, inplace=True)
# post = b_postprocess(abc, "")
# print(post.shape)
# print(post.columns)
@@ -116,20 +110,21 @@ quit()
# print("Batch 1 clean shape: " + str(df_batch_1_clean_abc.shape))
# New AC2 Outputs
dir_batch_1_individual_ac2_outputs = "output_individual/cnc_batch1_ac/"
fn_ac2_batch_1_outputs_list = [
dir_batch_1_individual_ac2_outputs + f + "/ac_output.csv"
for f in os.listdir(dir_batch_1_individual_ac2_outputs)
]
dir_batch_1_individual_ac2_outputs = 'output_individual/cnc_batch1_ac/'
fn_ac2_batch_1_outputs_list = [dir_batch_1_individual_ac2_outputs + f + "/ac_output.csv"
for f in os.listdir(dir_batch_1_individual_ac2_outputs)]
pre_concat_list_batch_1 = []
print("starting AC2 concatenation for batch 1")
print(str(len(fn_ac2_batch_1_outputs_list)) + " files pre-CSV reading")
for fn in fn_ac2_batch_1_outputs_list:
try:
pre_concat_list_batch_1.append(pd.read_csv(fn))
except Exception as e:
print(f"Appending failed on {fn} with exception {e}")
try:
pre_concat_list_batch_1.append(pd.read_csv(fn))
except Exception as e:
print(f"Appending failed on {fn} with exception {e}")
print(str(len(pre_concat_list_batch_1)) + " AC2 files post-CSV reading")
df_concat_batch_1_indiv_outputs = ac_postprocess(pd.concat(pre_concat_list_batch_1))
@@ -171,6 +166,12 @@ Just doing:
# batch_1_abc.to_csv("batch_1_merged_abc.csv", index=False)
##### Batch 2 #####
# fn_batch_2_clean_abc = 'reference_files/CNC-Batch 2 Output_File1.xlsx'
@@ -184,8 +185,9 @@ Just doing:
# print("Batch 2 clean shape: " + str(df_batch_2_clean_abc.shape))
# dir_batch_2_individual_ac2_outputs = 'output_individual/cnc_batch2_ac/'
# fn_ac2_batch_2_outputs_list = [dir_batch_2_individual_ac2_outputs + f + "/ac_output.csv"
# fn_ac2_batch_2_outputs_list = [dir_batch_2_individual_ac2_outputs + f + "/ac_output.csv"
# for f in os.listdir(dir_batch_2_individual_ac2_outputs)]
# pre_concat_list_batch_2 = []
@@ -10,7 +10,6 @@ This script consolidates a&c fields. Its data sources are `reference_files/CNC-
as well as .csv files from `output_individual/cnc_batch[1/2]_ac/[name of provider]/ac_output.csv.
"""
def b_postprocess(combined_df, filename):
if combined_df.shape[0] > 0:
# Add Single Code, Multiple Rate
@@ -21,7 +20,7 @@ def b_postprocess(combined_df, filename):
# Clean MSR
combined_df = postprocessing_funcs.clean_msr_lesser(combined_df)
# Clean Default
combined_df = postprocessing_funcs.clean_default_term(combined_df)
@@ -36,9 +35,7 @@ def b_postprocess(combined_df, filename):
# Rename and reorder
combined_df.rename(columns=valid.B_MAPPING, inplace=True)
column_order = [
col for col in valid.B_MAPPING.values() if col in combined_df.columns
]
column_order = [col for col in valid.B_MAPPING.values() if col in combined_df.columns]
final_df = combined_df[column_order]
return final_df
else:
@@ -48,64 +45,65 @@ def b_postprocess(combined_df, filename):
##### Batch 1 #####
# New B Outputs
print("Starting B")
dir_batch_1_individual_b_outputs = "output_individual/cnc_batch1_b/"
dir_batch_1_individual_b_outputs = 'output_individual/cnc_batch1_b/'
fn_b_batch_1_outputs_list = [
dir_batch_1_individual_b_outputs + f + "/b_output.csv"
for f in os.listdir(dir_batch_1_individual_b_outputs)
]
fn_b_batch_1_outputs_list = [dir_batch_1_individual_b_outputs + f + "/b_output.csv"
for f in os.listdir(dir_batch_1_individual_b_outputs)]
pre_concat_list_b_batch_1 = []
print("starting B concatenation for batch 1")
print(str(len(fn_b_batch_1_outputs_list)) + " B files pre-CSV reading")
for fn in fn_b_batch_1_outputs_list:
try:
file_b_df = pd.read_csv(fn)
file_b_df = b_postprocess(file_b_df, fn)
pre_concat_list_b_batch_1.append(file_b_df)
except Exception as e:
print(f"Appending failed on {fn} with exception {e}")
try:
file_b_df = pd.read_csv(fn)
file_b_df = b_postprocess(file_b_df, fn)
pre_concat_list_b_batch_1.append(file_b_df)
except Exception as e:
print(f"Appending failed on {fn} with exception {e}")
print(str(len(pre_concat_list_b_batch_1)) + " B files post-CSV reading")
abc = pd.concat(pre_concat_list_b_batch_1)
abc.drop(["Exclusions", "Unnamed: 0"], axis=1, inplace=True)
abc.drop(['Exclusions', 'Unnamed: 0'], axis=1, inplace=True)
print(abc.shape)
print(abc.columns)
# Original AC1 Outputs - All files
df_batch_1_ac1 = pd.read_csv("reference_files/CNC-1-AC.csv")
df_batch_1_ac1 = pd.read_csv('reference_files/CNC-1-AC.csv')
print(df_batch_1_ac1.shape)
print(df_batch_1_ac1.columns)
# quit()
# Cleaned ABC original
fn_batch_1_clean_abc = "reference_files/CNC-Batch 1 Output_File1.xlsx"
fn_batch_1_clean_abc = 'reference_files/CNC-Batch 1 Output_File1.xlsx'
df_batch_1_clean_abc = pd.read_excel(fn_batch_1_clean_abc)
print("Head of df_batch_1_clean_abc['Contract_Name']:")
print(df_batch_1_clean_abc["Contract_Name"].head())
print(df_batch_1_clean_abc['Contract_Name'].head())
print("Batch 1 clean shape: " + str(df_batch_1_clean_abc.shape))
# New AC2 Outputs
dir_batch_1_individual_ac2_outputs = "output_individual/cnc_batch1_ac/"
fn_ac2_batch_1_outputs_list = [
dir_batch_1_individual_ac2_outputs + f + "/ac_output.csv"
for f in os.listdir(dir_batch_1_individual_ac2_outputs)
]
dir_batch_1_individual_ac2_outputs = 'output_individual/cnc_batch1_ac/'
fn_ac2_batch_1_outputs_list = [dir_batch_1_individual_ac2_outputs + f + "/ac_output.csv"
for f in os.listdir(dir_batch_1_individual_ac2_outputs)]
pre_concat_list_batch_1 = []
print("starting AC2 concatenation for batch 1")
print(str(len(fn_ac2_batch_1_outputs_list)) + " files pre-CSV reading")
for fn in fn_ac2_batch_1_outputs_list:
try:
pre_concat_list_batch_1.append(pd.read_csv(fn))
except Exception as e:
print(f"Appending failed on {fn} with exception {e}")
try:
pre_concat_list_batch_1.append(pd.read_csv(fn))
except Exception as e:
print(f"Appending failed on {fn} with exception {e}")
print(str(len(pre_concat_list_batch_1)) + " AC2 files post-CSV reading")
df_concat_batch_1_indiv_outputs = ac_postprocess(pd.concat(pre_concat_list_batch_1))
"""
1. Read B - create abc - remove old columns (Exclusions, Add-On, MedNes)
2. Updae AC 1 column names - Add AC1 original to the end of abc
@@ -122,30 +120,18 @@ Just doing:
print("starting merging for batch 1")
df_batch_1_clean_abc["Contract_Name"] = df_batch_1_clean_abc["Contract_Name"] + ".txt"
batch_1_merged_ac = pd.merge(
df_batch_1_clean_abc,
df_concat_batch_1_indiv_outputs,
how="left",
left_on="Contract_Name",
right_on="Contract Name",
indicator=True,
)
df_batch_1_clean_abc['Contract_Name'] = df_batch_1_clean_abc['Contract_Name'] + '.txt'
batch_1_merged_ac = pd.merge(df_batch_1_clean_abc, df_concat_batch_1_indiv_outputs, how='left', left_on='Contract_Name',
right_on='Contract Name', indicator=True)
# batch_1_merged.to_csv("output_consolidated/CNC-1-AC2-Added.csv", index=False)
print("Batch1 AC2 merge completed. Merge status counts:")
print(batch_1_merged["_merge"].value_counts())
print(batch_1_merged['_merge'].value_counts())
batch_1_abc = pd.merge(
batch_1_merged_ac,
df_concat_batch_1_indiv_b_outputs,
how="left",
left_on="Contract_Name",
right_on="Contract Name",
indicator=True,
)
batch_1_abc = pd.merge(batch_1_merged_ac, df_concat_batch_1_indiv_b_outputs, how='left', left_on='Contract_Name',
right_on='Contract Name', indicator=True)
print(batch_1_abc.shape)
print("Batch 1 merged and written to CSV")
@@ -153,6 +139,12 @@ print("Batch 1 merged and written to CSV")
batch_1_abc.to_csv("batch_1_merged_abc.csv", index=False)
##### Batch 2 #####
# fn_batch_2_clean_abc = 'reference_files/CNC-Batch 2 Output_File1.xlsx'
@@ -166,8 +158,9 @@ batch_1_abc.to_csv("batch_1_merged_abc.csv", index=False)
# print("Batch 2 clean shape: " + str(df_batch_2_clean_abc.shape))
# dir_batch_2_individual_ac2_outputs = 'output_individual/cnc_batch2_ac/'
# fn_ac2_batch_2_outputs_list = [dir_batch_2_individual_ac2_outputs + f + "/ac_output.csv"
# fn_ac2_batch_2_outputs_list = [dir_batch_2_individual_ac2_outputs + f + "/ac_output.csv"
# for f in os.listdir(dir_batch_2_individual_ac2_outputs)]
# pre_concat_list_batch_2 = []
@@ -7,7 +7,6 @@ import sys
import random
import pandas as pd
import random
random.seed(42)
import utils
@@ -18,6 +17,7 @@ import tracking
import consolidate_output
def process_b(item):
filename, contract_text = item
if utils.contains_reimbursement(contract_text, 0):
@@ -25,80 +25,59 @@ def process_b(item):
results = file_processing.run_b_prompts(item)
if results is not None:
tracking.update_results_csv(item, results)
return item, results
return item, results
except Exception as e:
print(f"Error processing item {filename}: {e}")
traceback.print_exc()
else:
print(f"No reimbursement information found in {filename}. Skipping processing.")
def process_ac(item):
filename, contract_text = item
try:
results = file_processing.run_ac_prompts(item)
results = file_processing.run_new_ac_prompts(item)
if results is not None:
tracking.update_results_csv(item, results)
return item, results
return item, results
except Exception as e:
print(f"Error processing item {filename}: {e}")
traceback.print_exc()
def main():
if config.TEST:
print(
claude_funcs.invoke_claude(
"Write 'test', nothing more.",
model_id=config.MODEL_ID_CLAUDE2,
filename="test",
max_tokens=10,
)
)
print(claude_funcs.invoke_claude("Write 'test', nothing more.", model_id=config.MODEL_ID_CLAUDE2, filename="test", max_tokens=10))
else:
input_dict = (
utils.read_input()
) # keys are contract names, values are full contract text
input_dict = utils.read_input() # keys are contract names, values are full contract text
total_files = len(input_dict)
print(f"Input Files : {len(input_dict)}")
# Filter no reimbursement
input_dict = {k: input_dict[k] for k in list(input_dict)[:100]}
input_dict = {
k: v for k, v in input_dict.items() if utils.contains_reimbursement(str(v))
} # Filter out non-contracts
print(
f"Input Files after reimbursement filter: {len(input_dict)} | {total_files-len(input_dict)} files removed"
)
input_dict = {k : v for k, v in input_dict.items() if utils.contains_reimbursement(str(v))} # Filter out non-contracts
print(f"Input Files after reimbursement filter: {len(input_dict)} | {total_files-len(input_dict)} files removed")
# Filter already processed
if config.FILTER_ALREADY_PROCESSED:
input_dict = utils.filter_already_processed(input_dict)
print(f"Input Files left to be processed : {len(input_dict)}")
batch_results = []
processed_count = 0
with concurrent.futures.ThreadPoolExecutor(
max_workers=config.MAX_WORKERS
) as executor:
if "b" in config.FIELDS:
futures = [
executor.submit(process_b, item) for item in input_dict.items()
]
if "a" in config.FIELDS and "c" in config.FIELDS:
futures = [
executor.submit(process_ac, item) for item in input_dict.items()
]
with concurrent.futures.ThreadPoolExecutor(max_workers=config.MAX_WORKERS) as executor:
if 'b' in config.FIELDS:
futures = [executor.submit(process_b, item) for item in input_dict.items()]
if 'a' in config.FIELDS and 'c' in config.FIELDS:
futures = [executor.submit(process_ac, item) for item in input_dict.items()]
for future in concurrent.futures.as_completed(futures):
try:
result = future.result()
if result:
batch_results.append(result)
processed_count += 1
if processed_count % 5 == 0:
tracking.write_batch_results(batch_results)
batch_results = []
@@ -118,4 +97,4 @@ def main():
if __name__ == "__main__":
main()
main()
@@ -13,10 +13,10 @@
# N = 1
# # Paste contract text here
# text = """The maximum compensation for Covered Services rendered to a Covered Person shall be the "Allowed Amount."
# The Allowed Amount for Covered Services is the lesser of: (i) Allowable Charges; or (ii) as applicable,
# (a) 100% of the Payor's Medicaid fee schedule in effect on the date of service,
# or (b) for those Covered Services included in the Service Categories in Table 1 below,
# text = """The maximum compensation for Covered Services rendered to a Covered Person shall be the "Allowed Amount."
# The Allowed Amount for Covered Services is the lesser of: (i) Allowable Charges; or (ii) as applicable,
# (a) 100% of the Payor's Medicaid fee schedule in effect on the date of service,
# or (b) for those Covered Services included in the Service Categories in Table 1 below,
# the applicable "Contracted Rate" set forth in Table 1 below. Physical, Occupational and Speech Therapy | 80% of the Medicaid Rate in effect on the date of service.
# """
@@ -29,6 +29,6 @@
# answer_dict = dict_operations.secondary_string_to_dict(answer, "")
# print(answer_dict)
# all_dicts.append(answer_dict)
# # answer_df = pd.DataFrame(all_dicts)
# # answer_df.to_csv('full_methodology_unit_test.csv')
+83 -83
View File
@@ -3,116 +3,116 @@ import math
import pandas as pd
from pandas.testing import assert_frame_equal
from cnc_hotfix import state_check, apply_state_check_to_df
from hotfix_helper_funcs import state_check
class TestStateCheck:
# class TestStateCheck:
@pytest.mark.parametrize("input_state,expected_df_output,expected_non_df_output", [
("Multiple", "N/A",["N/A"]),
("Peach State", "N/A",["N/A"]),
("Statewide", "N/A",["N/A"]),
("Sunshine State Health Plan Suggests This Is For Florida, But Since The State Is Not Explicitly Specified, The Most Appropriate Answer Is:\n\nN/A", "N/A", ["N/A"]),
("The Contract Text Indicates This Health Plan Is For The Sunshine State, Which Is Florida. Therefore, The Answer Is:\n\nFlorida->N/A", "N/A", ["N/A"]),
("Sunshine State", "N/A", ["N/A"]),
("The Health Plan Is For The Sunshine State, Which Refers To Florida. However, Since The State Is Not Explicitly Specified, The Most Accurate Answer Based Solely On The Information Provided Is:\n\nN/A", "N/A", ["N/A"]),
])
# @pytest.mark.parametrize("input_state,expected_df_output,expected_non_df_output", [
# ("Multiple", "N/A",["N/A"]),
# ("Peach State", "N/A",["N/A"]),
# ("Statewide", "N/A",["N/A"]),
# ("Sunshine State Health Plan Suggests This Is For Florida, But Since The State Is Not Explicitly Specified, The Most Appropriate Answer Is:\n\nN/A", "N/A", ["N/A"]),
# ("The Contract Text Indicates This Health Plan Is For The Sunshine State, Which Is Florida. Therefore, The Answer Is:\n\nFlorida->N/A", "N/A", ["N/A"]),
# ("Sunshine State", "N/A", ["N/A"]),
# ("The Health Plan Is For The Sunshine State, Which Refers To Florida. However, Since The State Is Not Explicitly Specified, The Most Accurate Answer Based Solely On The Information Provided Is:\n\nN/A", "N/A", ["N/A"]),
# ])
def test_invalid_state_names(self, input_state, expected_df_output, expected_non_df_output):
"""Test invalid inputs"""
# def test_invalid_state_names(self, input_state, expected_df_output, expected_non_df_output):
# """Test invalid inputs"""
assert state_check(input_state,is_dataframe=True) == expected_df_output
assert state_check(input_state,is_dataframe=False) == expected_non_df_output
# assert state_check(input_state,is_dataframe=True) == expected_df_output
# assert state_check(input_state,is_dataframe=False) == expected_non_df_output
def test_hlorida_correction(self):
"""Test fix - Hlorida -> Florida"""
assert state_check("Hlorida",is_dataframe=False) == ["Florida"]
# def test_hlorida_correction(self):
# """Test fix - Hlorida -> Florida"""
# assert state_check("Hlorida",is_dataframe=False) == ["Florida"]
@pytest.mark.parametrize("input_state,expected_df_output,expected_non_df_output", [
("AL", "Alabama",["Alabama"]),
("Florida", "Florida",["Florida"]),
("FLORIDA", "Florida",["Florida"]),
("florida", "Florida",["Florida"]),
])
# @pytest.mark.parametrize("input_state,expected_df_output,expected_non_df_output", [
# ("AL", "Alabama",["Alabama"]),
# ("Florida", "Florida",["Florida"]),
# ("FLORIDA", "Florida",["Florida"]),
# ("florida", "Florida",["Florida"]),
# ])
def test_valid_states(self, input_state, expected_df_output, expected_non_df_output):
"""Test valid inputs"""
assert state_check(input_state,is_dataframe=True) == expected_df_output
assert state_check(input_state,is_dataframe=False) == expected_non_df_output
# def test_valid_states(self, input_state, expected_df_output, expected_non_df_output):
# """Test valid inputs"""
# assert state_check(input_state,is_dataframe=True) == expected_df_output
# assert state_check(input_state,is_dataframe=False) == expected_non_df_output
def test_list_inputs(self):
"""Test a list of input"""
input_states = ["AL", "Florida", "Hlorida", "Multiple"]
# def test_list_inputs(self):
# """Test a list of input"""
# input_states = ["AL", "Florida", "Hlorida", "Multiple"]
expected_df_output = "Alabama, Florida, Florida, N/A"
expected_non_df_output = ["Alabama", "Florida", "Florida", "N/A"]
# expected_df_output = "Alabama, Florida, Florida, N/A"
# expected_non_df_output = ["Alabama", "Florida", "Florida", "N/A"]
assert state_check(input_states,is_dataframe=True) == expected_df_output
assert state_check(input_states,is_dataframe=False) == expected_non_df_output
# assert state_check(input_states,is_dataframe=True) == expected_df_output
# assert state_check(input_states,is_dataframe=False) == expected_non_df_output
@pytest.mark.parametrize("invalid_input", [
123,
{"state": "Florida"},
True
])
# @pytest.mark.parametrize("invalid_input", [
# 123,
# {"state": "Florida"},
# True
# ])
def test_invalid_input_types(self, invalid_input):
"""Test invalid input types"""
# def test_invalid_input_types(self, invalid_input):
# """Test invalid input types"""
with pytest.raises(TypeError):
state_check(invalid_input)
# with pytest.raises(TypeError):
# state_check(invalid_input)
@pytest.mark.parametrize("empty_input,expected_df_output,expected_non_df_output", [
("", "N/A",["N/A"]),
([], "",[]),
([" "], "N/A",["N/A"])
])
# @pytest.mark.parametrize("empty_input,expected_df_output,expected_non_df_output", [
# ("", "N/A",["N/A"]),
# ([], "",[]),
# ([" "], "N/A",["N/A"])
# ])
def test_empty_inputs(self, empty_input, expected_df_output, expected_non_df_output):
"""Test empty inputs"""
assert state_check(empty_input,is_dataframe=True) == expected_df_output
assert state_check(empty_input,is_dataframe=False) == expected_non_df_output
# def test_empty_inputs(self, empty_input, expected_df_output, expected_non_df_output):
# """Test empty inputs"""
# assert state_check(empty_input,is_dataframe=True) == expected_df_output
# assert state_check(empty_input,is_dataframe=False) == expected_non_df_output
def test_multiple_invalid_states(self):
"""Test multiple invalid inputs"""
# def test_multiple_invalid_states(self):
# """Test multiple invalid inputs"""
input_states = [
"Multiple",
"Peach State",
"The Health Plan Is For The Sunshine State, Which Refers To Florida. So The Answer Is:\n\nFlorida->N/A",
"The Health Plan Is For The Florida State, Which Refers To Florida",
"Sunshine State Health Plan Suggests This Is For Florida, But Since The State Is Not Explicitly Specified, The Most Appropriate Answer Is:\n\nN/A->N/A",
"Sunshine State Health Plan->N/A",
"Sunshine State->N/A",
"The Contract Text Indicates This Health Plan Is For Florida, As It Mentions Sunshine State Health Plan, Inc. Which Operates In Florida. However, To Strictly Follow The Instructions, I Will Provide The Answer:\n\nN/A->N/A"
# input_states = [
# "Multiple",
# "Peach State",
# "The Health Plan Is For The Sunshine State, Which Refers To Florida. So The Answer Is:\n\nFlorida->N/A",
# "The Health Plan Is For The Florida State, Which Refers To Florida",
# "Sunshine State Health Plan Suggests This Is For Florida, But Since The State Is Not Explicitly Specified, The Most Appropriate Answer Is:\n\nN/A->N/A",
# "Sunshine State Health Plan->N/A",
# "Sunshine State->N/A",
# "The Contract Text Indicates This Health Plan Is For Florida, As It Mentions Sunshine State Health Plan, Inc. Which Operates In Florida. However, To Strictly Follow The Instructions, I Will Provide The Answer:\n\nN/A->N/A"
]
# ]
expected_df_output = ", ".join(["N/A"]*8)
expected_non_df_output = ["N/A"]*8
# expected_df_output = ", ".join(["N/A"]*8)
# expected_non_df_output = ["N/A"]*8
assert state_check(input_states,is_dataframe=True) == expected_df_output
assert state_check(input_states,is_dataframe=False) == expected_non_df_output
# assert state_check(input_states,is_dataframe=True) == expected_df_output
# assert state_check(input_states,is_dataframe=False) == expected_non_df_output
def test_mixed_states(self):
"""Test mixed input"""
input_states = ["AL", "Multiple", "Florida", "Hlorida", "Florida State Health Plan"]
# def test_mixed_states(self):
# """Test mixed input"""
# input_states = ["AL", "Multiple", "Florida", "Hlorida", "Florida State Health Plan"]
expected_df_output = "Alabama, N/A, Florida, Florida, N/A"
expected_non_df_output = ["Alabama", "N/A", "Florida", "Florida", "N/A"]
# expected_df_output = "Alabama, N/A, Florida, Florida, N/A"
# expected_non_df_output = ["Alabama", "N/A", "Florida", "Florida", "N/A"]
assert state_check(input_states,is_dataframe=True) == expected_df_output
assert state_check(input_states,is_dataframe=False) == expected_non_df_output
# assert state_check(input_states,is_dataframe=True) == expected_df_output
# assert state_check(input_states,is_dataframe=False) == expected_non_df_output
def concatenated_string_states(self):
"""Test concatentated input"""
input_states = ["Arizona, Louisiana","NY, NJ, Connecticut"]
# def concatenated_string_states(self):
# """Test concatentated input"""
# input_states = ["Arizona, Louisiana","NY, NJ, Connecticut"]
expected_df_output = "Arizona, Louisiana, New York, New Jersey"
expected_non_df_output = ["Arizona", "Louisiana", "New York", "New Jersey"]
# expected_df_output = "Arizona, Louisiana, New York, New Jersey"
# expected_non_df_output = ["Arizona", "Louisiana", "New York", "New Jersey"]
assert state_check(input_states,is_dataframe=True) == expected_df_output
assert state_check(input_states,is_dataframe=False) == expected_non_df_output
# assert state_check(input_states,is_dataframe=True) == expected_df_output
# assert state_check(input_states,is_dataframe=False) == expected_non_df_output
# class TestApplyStateCheckToDataFrame:
@@ -0,0 +1,21 @@
import pytest
# from hotfix_helper_funcs import apply_date_formatting_fix
# def test_change_date_formatting():
# assert apply_date_formatting_fix("31/12/2012") == "12/31/2012"
# assert apply_date_formatting_fix("20/07/2025") == "07/20/2025"
# assert apply_date_formatting_fix("13/12/1992") == "12/13/1992"
# def test_date_formatting_no_change():
# assert apply_date_formatting_fix("11/21/2024") == "11/21/2024"
# assert apply_date_formatting_fix("07/20/1992") == "07/20/1992"
# assert apply_date_formatting_fix("01/02/2000") == "01/02/2000"
# def test_date_formatting_na():
# assert apply_date_formatting_fix("N/A") == "N/A"
# def test_invalid_date():
# with pytest.raises(ValueError):
# apply_date_formatting_fix("7/20/1992")
# with pytest.raises(ValueError):
# apply_date_formatting_fix("some string")
@@ -1,29 +1,29 @@
import pandas as pd
import os
fn_batch_1_clean_abc = "reference_files/CNC-Batch 1 Output_File1.xlsx"
fn_batch_1_clean_abc = 'reference_files/CNC-Batch 1 Output_File1.xlsx'
df_batch_1_clean_abc = pd.read_excel(fn_batch_1_clean_abc)
print("Head of df_batch_1_clean_abc['Contract_Name']:")
print(df_batch_1_clean_abc["Contract_Name"].head())
print(df_batch_1_clean_abc['Contract_Name'].head())
fn_batch_2_clean_abc = "reference_files/CNC-Batch 2 Output_File1.xlsx"
fn_batch_2_clean_abc = 'reference_files/CNC-Batch 2 Output_File1.xlsx'
df_batch_2_clean_abc = pd.read_excel(fn_batch_2_clean_abc)
df_batch_2_clean_abc["Contract_Name"] = df_batch_2_clean_abc["Contract Name"]
df_batch_2_clean_abc['Contract_Name'] = df_batch_2_clean_abc['Contract Name']
print("Head of df_batch_2_clean_abc['Contract_Name']:")
print(df_batch_2_clean_abc["Contract_Name"].head())
print(df_batch_2_clean_abc['Contract_Name'].head())
print("Batch 1 clean shape: " + str(df_batch_1_clean_abc.shape))
print("Batch 2 clean shape: " + str(df_batch_2_clean_abc.shape))
print("batch 1 columns: ")
for column in df_batch_1_clean_abc.columns:
print(column)
print(column)
print("\nbatch 2 columns: ")
for column in df_batch_2_clean_abc.columns:
print(column)
print(column)
print("\n columns in batch 2 not in batch 1: ")
b1cols = set(df_batch_1_clean_abc.columns)
@@ -32,17 +32,13 @@ print(b2cols.difference(b1cols))
quit()
dir_batch_1_individual_ac2_outputs = "output_individual/cnc_batch1_ac/"
fn_ac2_batch_1_outputs_list = [
dir_batch_1_individual_ac2_outputs + f + "/ac_output.csv"
for f in os.listdir(dir_batch_1_individual_ac2_outputs)
]
dir_batch_1_individual_ac2_outputs = 'output_individual/cnc_batch1_ac/'
fn_ac2_batch_1_outputs_list = [dir_batch_1_individual_ac2_outputs + f + "/ac_output.csv"
for f in os.listdir(dir_batch_1_individual_ac2_outputs)]
dir_batch_2_individual_ac2_outputs = "output_individual/cnc_batch2_ac/"
fn_ac2_batch_2_outputs_list = [
dir_batch_2_individual_ac2_outputs + f + "/ac_output.csv"
for f in os.listdir(dir_batch_2_individual_ac2_outputs)
]
dir_batch_2_individual_ac2_outputs = 'output_individual/cnc_batch2_ac/'
fn_ac2_batch_2_outputs_list = [dir_batch_2_individual_ac2_outputs + f + "/ac_output.csv"
for f in os.listdir(dir_batch_2_individual_ac2_outputs)]
pre_concat_list_batch_1 = []
@@ -51,10 +47,10 @@ print("starting concatenation for batch 1")
print(str(len(fn_ac2_batch_1_outputs_list)) + " files pre-CSV reading")
for fn in fn_ac2_batch_1_outputs_list:
try:
pre_concat_list_batch_1.append(pd.read_csv(fn))
except Exception as e:
print(f"Appending failed on {fn} with exception {e}")
try:
pre_concat_list_batch_1.append(pd.read_csv(fn))
except Exception as e:
print(f"Appending failed on {fn} with exception {e}")
print(str(len(pre_concat_list_batch_1)) + " files post-CSV reading")
df_concat_batch_1_indiv_outputs = pd.concat(pre_concat_list_batch_1)
@@ -64,10 +60,10 @@ print("batch 1 concatenation complete")
print("starting concatenation for batch 2")
print(str(len(fn_ac2_batch_2_outputs_list)) + " files pre-CSV reading")
for fn in fn_ac2_batch_2_outputs_list:
try:
pre_concat_list_batch_2.append(pd.read_csv(fn))
except Exception as e:
print(f"Appending failed on {fn} with exception {e}")
try:
pre_concat_list_batch_2.append(pd.read_csv(fn))
except Exception as e:
print(f"Appending failed on {fn} with exception {e}")
print(str(len(pre_concat_list_batch_2)) + " files post-CSV reading")
df_concat_batch_2_indiv_outputs = pd.concat(pre_concat_list_batch_2)
@@ -76,31 +72,19 @@ print("batch 2 concatenation complete")
print("starting merging for batch 1")
df_batch_1_clean_abc["Contract_Name"] = df_batch_1_clean_abc["Contract_Name"] + ".txt"
batch_1_merged = pd.merge(
df_batch_1_clean_abc,
df_concat_batch_1_indiv_outputs,
how="left",
left_on="Contract_Name",
right_on="Contract Name",
indicator=True,
)
df_batch_1_clean_abc['Contract_Name'] = df_batch_1_clean_abc['Contract_Name'] + '.txt'
batch_1_merged = pd.merge(df_batch_1_clean_abc, df_concat_batch_1_indiv_outputs, how='left', left_on='Contract_Name',
right_on='Contract Name', indicator=True)
batch_1_merged.to_csv("batch_1_merged.csv", index=False)
print("batch 1 merge completed. Merge status counts:")
print(batch_1_merged["_merge"].value_counts())
print(batch_1_merged['_merge'].value_counts())
print("Batch 1 merged and written to CSV")
print("starting merging for batch 2")
df_batch_2_clean_abc["Contract_Name"] = df_batch_2_clean_abc["Contract_Name"] + ".txt"
batch_2_merged = pd.merge(
df_batch_2_clean_abc,
df_concat_batch_2_indiv_outputs,
how="left",
left_on="Contract_Name",
right_on="Contract Name",
indicator=True,
)
df_batch_2_clean_abc['Contract_Name'] = df_batch_2_clean_abc['Contract_Name'] + '.txt'
batch_2_merged = pd.merge(df_batch_2_clean_abc, df_concat_batch_2_indiv_outputs, how='left', left_on='Contract_Name',
right_on='Contract Name', indicator=True)
batch_2_merged.to_csv("batch_2_merged.csv", index=False)
print("batch 2 merge completed. Merge status counts:")
print(batch_2_merged["_merge"].value_counts())
print(batch_2_merged['_merge'].value_counts())
print("Batch 2 merged and written to CSV")
@@ -8,7 +8,6 @@ from openpyxl.utils.dataframe import dataframe_to_rows
import argparse
import boto3
from botocore.exceptions import ClientError
sys.path.append("src")
import utils
Generated
+927 -46
View File
File diff suppressed because it is too large Load Diff
+8 -7
View File
@@ -1,19 +1,20 @@
[tool.poetry]
name = "pipes"
name = "doczy-smart-chunking"
version = "0.1.0"
description = ""
authors = ["Michael McGuinness <mmcguinness@aarete.com>"]
authors = ["Alex Galarce <agalarce@aarete.com>"]
readme = "README.md"
package-mode = false
[tool.poetry.dependencies]
python = "3.12.7"
requests = "^2.32.3"
python = "^3.12"
pandas = "^2.2.3"
boto3 = "^1.35.40"
anthropic = "^0.36.0"
[tool.poetry.group.dev.dependencies]
pytest = "^8.3.3"
black = "^24.10.0"
mypy = "^1.12.0"
[build-system]
requires = ["poetry-core"]
@@ -26,4 +27,4 @@ target-version = ['py312']
[tool.mypy]
#disable_error_code = []
#install_types = true
#non_interactive = true
#non_interactive = true