Merged in QCQA_Additions (pull request #331)

QCQA Additions

* tested on batch 10, small fixes + change to s3 output

* Merged main into QCQA_Additions

* unit tests for qc qa helpers

* static error fix

* another static fix

* static fix again

* fixed None check

* static fix none

* Merged main into QCQA_Additions

* Small changes based on Katon's review


Approved-by: Katon Minhas
This commit is contained in:
Chris Bull
2024-12-13 19:23:04 +00:00
parent b094542cbe
commit 26c12469bc
12 changed files with 486 additions and 26 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ from qa_qc_helpers import (
from consolidate_output import consolidate_adhoc
file_name_column = config.CONTRACT_FILE_NAME
file_name_column = config.FILE_NAME_COLUMN
"""python scripts/qa_qc.py input_dir=______ output_dir=_______ ac_df=________ b_df=______ read_mode=____ df_read_mode=________
You should have both input and output (ouput is just an empty folder) and then one of ac_df or b_df or both
+1
View File
@@ -225,6 +225,7 @@ ANSWER_TRACKING_DICT = {}
######################################## POSTPROCESSING SETTINGS ########################################
FUZZY_MATCH_THRESHOLD = 0.8
FILE_NAME_COLUMN = 'Contract Name'
BLANKS_THRESHOLD = 0.5
######################################## COMPLEX FLAG SETTINGS ########################################
COMPLEX_MAX_WORKERS = 50
+3 -1
View File
@@ -137,8 +137,10 @@ def consolidate_adhoc(ac_df, b_df):
# If ABC
b_df.drop([col for col in ["Pages", "Parent Agreement Code"] if col in b_df.columns], axis=1, inplace=True)
abc_final_df = pd.merge(
b_df, ac_df, on=[config.FILE_NAME_COLUMN, "Pages", "Parent Agreement Code"], how="right"
b_df, ac_df, on=[config.FILE_NAME_COLUMN], how="right"
)
abc_final_df = abc_final_df[
[col for col in valid.ABC_COLUMNS if col in abc_final_df]
+74 -24
View File
@@ -10,6 +10,7 @@ import preprocessing_funcs
import config
from openpyxl import Workbook
from openpyxl.utils.dataframe import dataframe_to_rows
from hotfix_helper_funcs import keywords_to_detect_invalid_state_name
def detect_date_anomalies(series, field_name, is_merged=False):
total_count = len(series)
@@ -48,17 +49,20 @@ def detect_date_anomalies(series, field_name, is_merged=False):
return result
def tin_check(tin: str) -> tuple[str, bool]:
pattern = r'\b\d{2}-\d{7}\b'
pattern = r'^\b\d{2}-\d{7}\b$'
if not isinstance(tin, str):
try:
tin = str(tin)
except TypeError:
except Exception:
check = False
return tin, check
check=bool(re.match(pattern, tin))
match=re.match(pattern, tin)
check = bool(match)
tin = match.group(0) if match else tin
return tin, check
@@ -68,12 +72,15 @@ def npi_check(npi: str) -> tuple[str, bool]:
if not isinstance(npi, str):
try:
npi = str(npi)
except TypeError:
except Exception:
check = False
return npi, check
check = bool(re.match(pattern, npi))
match = re.match(pattern, npi)
check = bool(match)
npi = match.group(0) if match else npi
return npi, check
@@ -81,9 +88,15 @@ def date_format_check(date: str) -> tuple:
format = '%m/%d/%Y'
if not isinstance(date, str):
try:
date = str(date)
except TypeError:
check = False
date = date
try:
dt = datetime.strptime(date, format)
date = dt.strftime('%m/%d/%Y')
date = dt.strftime('%-m/%-d/%Y')
check = True
except ValueError:
date = date
@@ -112,35 +125,66 @@ def state_check(state: str) -> tuple[str, bool]:
check = False
return state, check
if state in STATE_MAP.keys():
state = STATE_MAP[state]
if state.upper() in STATE_MAP.keys():
state = STATE_MAP[state.upper()]
check = True
elif state.strip().title() in STATE_MAP.values():
check = True
state = state.strip().title()
else:
check = False
return state, check
def pages_pagenum_check(pages: str, pagenum: str) -> bool:
def pages_pagenum_check(pages: int, pagenum: int) -> bool:
if not isinstance(pages, str):
if not isinstance(pages, int):
try:
pages = str(pages)
except TypeError:
pages = int(pages)
except (TypeError, ValueError) as e:
check = False
return check
elif not isinstance(pagenum, str):
try:
pagenum = str(pagenum)
except TypeError:
check = False
return check
else:
check = (pagenum < pages)
return check
return check
if not isinstance(pagenum, int):
try:
pagenum = int(pagenum)
except (TypeError, ValueError) as e:
check = False
return check
check = (pages >= pagenum)
return check
def blanks_check(df: pd.DataFrame, threshold=config.BLANKS_THRESHOLD) -> pd.DataFrame:
highly_blank_cols = []
for col in df.columns:
if df[col].apply(lambda x: utils.is_empty(x)).sum() / len(df[col]) > threshold:
highly_blank_cols.append(col)
return '\n'.join(highly_blank_cols)
def self_talk_check(df: pd.DataFrame):
lang_cols = [col for col in df.columns if ('Language' in col or 'Clause' in col) and ('(Y/N)' not in col)]
selftalkers = []
for col in lang_cols:
df['selftalk'] = df[col].apply(lambda x: any([(kw in x) for kw in keywords_to_detect_invalid_state_name]) if not utils.is_empty(x) else False)
if df['selftalk'].any():
pct = df['selftalk'].sum() / len(df['selftalk'])
selftalkers.append(f'{col} - {pct:.1%}')
return '\n'.join(selftalkers)
def perform_qc_qa_tests_both(ac_df, b_df, merged_df, input_dict, processed_files):
@@ -192,6 +236,8 @@ def perform_qc_qa_tests_both(ac_df, b_df, merged_df, input_dict, processed_files
"Number of duplicate rows in merged DataFrame": merged_df.duplicated().sum(),
"# of fields for AC run" : len(ac_df.columns),
"# of fields for B run" : len(b_df.columns),
"Highly Blank Fields in merged DataFrame" : blanks_check(merged_df),
"Potential 'Self-Talk' columns in merged DataFrame" : self_talk_check(merged_df),
"# of incorrectly formatted Provider Group TINs" : ac_df["IRS #"].apply(lambda x: not tin_check(x)[1] if not utils.is_empty(x) else False).sum(),
"# of incorrectly formatted Provider Group NPIs" : ac_df["NPI (10-digits)"].apply(lambda x: not npi_check(x)[1] if not utils.is_empty(x) else False).sum(),
"# of incorrectly formatted Provider Group Signatory TINs": ac_df["PROV_GROUP_TIN_SIGNATORY"].apply(lambda x: not tin_check(x)[1] if not utils.is_empty(x) else False).sum(),
@@ -199,7 +245,7 @@ def perform_qc_qa_tests_both(ac_df, b_df, merged_df, input_dict, processed_files
"# of incorrectly formatted Provider NPIs (other)": ac_df["PROV_NPI_OTHER"].apply(lambda x: not npi_check(x)[1] if not utils.is_empty(x) else False).sum(),
"# of incorrect Health Plan States" : ac_df['Health Plan State'].apply(lambda x: not state_check(x)[1] if not utils.is_empty(x) else False).sum(),
"# of rows with Pages greater than Page Num" : merged_df.apply(lambda row: pages_pagenum_check(row['Pages'], row['Page_Num']) if (not utils.is_empty(row['Pages']) and utils.is_empty(row['Page_Num'])) else False, axis=1).sum(),
"# of Payer Names found in Notice to Provider Name" : ac_df.apply(lambda row: row['PAYER NAME'] in row['Notice to Provider Name'] if not utils.is_empty(row['Notice to Provider Name']) else False, axis = 1).sum(),
"# of Payer Names found in Notice to Provider Name" : ac_df.apply(lambda row: row['PAYER NAME'] in row['Notice to Provider Name'] if not utils.is_empty(row['Notice to Provider Name']) and not utils.is_empty(row['PAYER NAME']) else False, axis = 1).sum(),
"# of invalid Y/N values for B run" : b_df[yn_cols_b].apply(lambda x: yn_check(x)[1], axis = 1).sum().sum(),
"# of invalid Y/N values for AC run" : ac_df[yn_cols_ac].apply(lambda x: yn_check(x)[1], axis = 1).sum().sum(),
"# of invalid Termination Dates" : ac_df['Termination Date'].apply(lambda x: not date_format_check(x) if not utils.is_empty(x) else x).sum(),
@@ -269,6 +315,8 @@ def perform_qc_qa_tests_ac(ac_df, input_dict, processed_files):
.sum(),
"# of duplicate rows for AC run": ac_df.duplicated().sum(),
"# of fields for AC run" : len(ac_df.columns),
"Highly Blank Fields in AC DataFrame" : blanks_check(ac_df),
"Potential 'Self-Talk' columns in merged DataFrame" : self_talk_check(ac_df),
"# of incorrectly formatted Provider Group TINs" : ac_df["IRS #"].apply(lambda x: not tin_check(x)[1] if not utils.is_empty(x) else False).sum(),
"# of incorrectly formatted Provider Group NPIs" : ac_df["NPI (10-digits)"].apply(lambda x: not npi_check(x)[1] if not utils.is_empty(x) else False).sum(),
"# of incorrectly formatted Provider Group Signatory TINs": ac_df["PROV_GROUP_TIN_SIGNATORY"].apply(lambda x: not tin_check(x)[1] if not utils.is_empty(x) else False).sum(),
@@ -321,6 +369,8 @@ def perform_qc_qa_tests_b(b_df, input_dict, processed_files):
.sum(),
"# of duplicate rows for B run": b_df.duplicated().sum(),
"# of fields for B run" : len(b_df.columns),
"Highly Blank Fields in B DataFrame" : blanks_check(b_df),
"Potential 'Self-Talk' columns in merged DataFrame" : self_talk_check(b_df),
"# of rows with Pages greater than Page Num" : b_df.apply(lambda row: pages_pagenum_check(row['Pages'], row['Page_Num']), axis=1).sum(),
"# of invalid Y/N values for B run" : b_df[yn_cols_b].apply(lambda x: yn_check(x)[1], axis = 1).sum().sum()}
@@ -500,7 +550,7 @@ def save_qcqa_wb(wb, run_timestamp):
# Upload to S3
if config.WRITE_TO_S3:
BUCKET_NAME = config.S3_OUTPUT_BUCKET
print(f"Uploading final outputs to s3://{BUCKET_NAME}/{run_timestamp}")
print(f"Uploading final outputs to s3://{BUCKET_NAME}/qcqa/{run_timestamp}")
# Upload consolidated outputs
+41
View File
@@ -0,0 +1,41 @@
import pytest
import pandas as pd
import sys
sys.path.append('src')
from qa_qc_helpers import blanks_check
def test_blanks_check_no_highly_blank_columns():
# Create a sample DataFrame with no highly blank columns
df = pd.DataFrame({
"A": [1, 2, 3, 4, 5],
"B": ["a", "b", "c", "d", "e"],
"C": [10.5, 11.2, 12.3, 13.4, 14.5]
})
result = blanks_check(df)
assert result == ""
def test_blanks_check_with_highly_blank_columns():
# Create a sample DataFrame with highly blank columns
df = pd.DataFrame({
"A": [1, 2, 3, None, 5, None, 6],
"B": ["a", "b", "", "d", "", "", ""],
"C": [10.5, None, 12.3, None, 14.5, None, None]
})
result = blanks_check(df)
expected_output = "B\nC"
assert result == expected_output
def test_blanks_check_all_columns_highly_blank():
# Create a sample DataFrame where all columns are highly blank
df = pd.DataFrame({
"A": [None, None, None, None, None],
"B": ["", "", "", "", ""],
"C": [None, None, None, None, None]
})
result = blanks_check(df)
expected_output = "A\nB\nC"
assert result == expected_output
@@ -0,0 +1,84 @@
import pytest
from datetime import datetime
import sys
sys.path.append('src')
from qa_qc_helpers import date_format_check
def test_valid_date_formats():
"""Test valid date formats are correctly validated"""
valid_dates = {
"01/01/2024" : '1/1/2024',
"12/31/2023" : '12/31/2023',
"02/29/2024" : '2/29/2024', # Leap year
"07/04/1776" : '7/4/1776',
"09/08/2024" : '9/8/2024'
}
for date in valid_dates.keys():
result, is_valid = date_format_check(date)
assert result == valid_dates[date]
assert is_valid is True
def test_invalid_date_formats():
"""Test various invalid date formats"""
invalid_dates = [
"13/01/2024", # Invalid month
"01/32/2024", # Invalid day
"02/29/2023", # Not a leap year
"00/01/2024", # Zero month
"01/00/2024", # Zero day
"01-01-2024", # Wrong separator
"2024/01/01", # Wrong order
"01/01/24", # Two-digit year
"Jan/01/2024", # Text month
]
for date in invalid_dates:
result, is_valid = date_format_check(date)
assert result == date
assert is_valid is False
def test_non_string_inputs():
"""Test handling of non-string inputs"""
test_cases = [
(20240101, "20240101"),
(None, None),
(1.234, "1.234"),
(["01/01/2024"], "['01/01/2024']"),
({"date": "01/01/2024"}, "{'date': '01/01/2024'}"),
]
for input_val, expected_str in test_cases:
result, is_valid = date_format_check(input_val)
assert result == str(input_val)
assert is_valid is False
def test_edge_cases():
"""Test edge cases with valid-looking but incorrect formats"""
test_cases = [
"01/01/2024 ", # Extra space at end
" 01/01/2024", # Extra space at start
"01/01/2024\n", # Newline at end
"01 /01/2024", # Space in middle
"01/01/ 2024", # Space in middle
]
for date in test_cases:
result, is_valid = date_format_check(date)
assert result == date
assert is_valid is False
def test_special_dates():
"""Test boundary cases for dates"""
test_cases = [
("12/31/9999", True), # Far future
("2/28/2024", True), # Day before leap day
("3/1/2024", True), # Day after leap day
("12/31/2023", True), # Last day of year
("1/1/2024", True), # First day of year
]
for date, expected_valid in test_cases:
result, is_valid = date_format_check(date)
assert result == date
assert is_valid == expected_valid
+67
View File
@@ -0,0 +1,67 @@
import pytest
import sys
sys.path.append('src')
from qa_qc_helpers import npi_check
import re
def test_valid_npi_format():
"""Test that a correctly formatted NPI returns True"""
npi = "1234567890"
result, is_valid = npi_check(npi)
assert result == npi
assert is_valid is True
def test_invalid_npi_format():
"""Test various invalid NPI formats"""
invalid_npis = [
"123456789", # Too few digits
"12345678900", # Too many digits
"12-4567890", # TIN format
"1ab3456789", # Letters instead of numbers
"3234567890", # Doesn't start with 1 or 2
]
for npi in invalid_npis:
result, is_valid = npi_check(npi)
assert result == npi
assert is_valid is False
def test_non_string_inputs():
"""Test handling of non-string inputs"""
test_cases = [
(1234567890, "1234567890", True),
(12.3456789, "12.3456789", False),
(None, 'None', False),
([12, 3456789], "[12, 3456789]", False),
({"npi" : "1234567890"}, "{'npi': '1234567890'}", False),
]
for input_val, expected_str, expected_valid in test_cases:
result, is_valid = npi_check(input_val)
assert result == expected_str
assert is_valid is expected_valid
def test_edge_cases_npi():
"""Test edge cases with valid-looking but incorrect formats"""
test_cases = [
"12345 67890" # Space in middle
]
for npi in test_cases:
result, is_valid = npi_check(npi)
assert result == npi
assert is_valid is False
def test_multiple_tins_in_string():
"""Test strings containing multiple TIN-like patterns"""
test_cases = [
"1234567890 1256789001", # Two TINs with space
"12345678901234567890", # Two TINs without space
"Some text 1234567890", # TIN with preceding text
"1234567890 some text", # TIN with following text
]
for npi in test_cases:
result, is_valid = npi_check(npi)
assert result == npi
assert is_valid is False
@@ -0,0 +1,41 @@
import pytest
import sys
sys.path.append('src')
from qa_qc_helpers import pages_pagenum_check
def test_pages_pagenum_check_valid_inputs():
# Test with valid string inputs
assert pages_pagenum_check("100", "50") is True
assert pages_pagenum_check(50, 10) is True
def test_pages_pagenum_check_invalid_inputs():
# Test with invalid string inputs
assert pages_pagenum_check("abc", "50") is False
assert pages_pagenum_check("50", "xyz") is False
def test_pages_pagenum_check_non_string_inputs():
# Test with non-string inputs for pages
assert pages_pagenum_check(100, "50") is True
assert pages_pagenum_check(50, 10) is True
# Test with non-string inputs for pagenum
assert pages_pagenum_check("100", 50) is True
assert pages_pagenum_check("50", 10) is True
def test_pages_pagenum_check_non_convertible_inputs():
# Test with inputs that cannot be converted to string for pages
def raise_type_error():
raise TypeError("Cannot convert to string")
assert pages_pagenum_check(raise_type_error, "50") is False
# Test with inputs that cannot be converted to string for pagenum
assert pages_pagenum_check("100", raise_type_error) is False
def test_pages_pagenum_check_equality():
# Test when pages and pagenum are equal
assert pages_pagenum_check("100", "100") is True
def test_pages_pagenum_check_pagenum_greater():
# Test when pagenum is greater than pages
assert pages_pagenum_check("50", "100") is False
+42
View File
@@ -0,0 +1,42 @@
import pytest
import pandas as pd
import sys
sys.path.append('src')
from qa_qc_helpers import self_talk_check
from hotfix_helper_funcs import keywords_to_detect_invalid_state_name
def test_self_talk_check_no_self_talk():
# Create a sample DataFrame with no self-talk columns
df = pd.DataFrame({
"Name": ["John", "Jane", "Bob", "Alice", "Tom"],
"Age": [25, 30, 35, 40, 45],
"City": ["New York", "Los Angeles", "Chicago", "Houston", "Miami"]
})
result = self_talk_check(df)
assert result == ""
def test_self_talk_check_with_self_talk():
# Create a sample DataFrame with self-talk columns
df = pd.DataFrame({
"Name": ["John", "Jane", "Bob", "Alice", "Tom"],
"Language": ["English", "Spanish", "The Contract Text suggests Armenian", "French", "German"],
"Clause": ["", "this suggests", "Mike refers to xyz", "The Contract Text says", "My Answer is this: "]
})
result = self_talk_check(df)
expected_output = "Language - 20.0%\nClause - 80.0%"
assert result == expected_output
def test_self_talk_check_all_self_talk():
# Create a sample DataFrame where all language columns have self-talk
df = pd.DataFrame({
"Name": ["John", "Jane", "Bob", "Alice", "Tom"],
"Language": ["Statewide", "Multiple sources imply", "The contract text says", "For this text, it would seem", "This suggests that"],
"Clause": ["The Contract Text contains this", "My Answer Is This: ", "It would seem that multiple clauses say", "I conclude that the exhibit suggests", "For each clause, this is the case"]
})
result = self_talk_check(df)
expected_output = "Language - 100.0%\nClause - 100.0%"
assert result == expected_output
+32
View File
@@ -0,0 +1,32 @@
import pytest
import sys
sys.path.append('src')
from qa_qc_helpers import state_check
def test_state_check_valid_abbreviations():
# Test with valid state abbreviations
assert state_check("CA") == ("California", True)
assert state_check("NY") == ("New York", True)
def test_state_check_valid_full_names():
# Test with valid state full names
assert state_check("California") == ("California", True)
assert state_check("new york") == ("New York", True)
def test_state_check_invalid_input():
# Test with invalid input
assert state_check("ABC") == ("ABC", False)
assert state_check("Invalid State") == ("Invalid State", False)
def test_state_check_non_string_input():
# Test with non-string input
assert state_check(123) == ("123", False)
assert state_check(True) == ("True", False)
def test_state_check_non_convertible_input():
# Test with input that cannot be converted to string
def raise_type_error():
raise TypeError("Cannot convert to string")
assert state_check(raise_type_error)[1] == False
+74
View File
@@ -0,0 +1,74 @@
import pytest
import sys
sys.path.append('src')
from qa_qc_helpers import tin_check
import re
def test_valid_tin_format():
"""Test that a correctly formatted TIN returns True"""
tin = "12-3456789"
result, is_valid = tin_check(tin)
assert result == tin
assert is_valid is True
def test_invalid_tin_format():
"""Test various invalid TIN formats"""
invalid_tins = [
"123-456789", # Too many digits before hyphen
"12-456789", # Too few digits after hyphen
"12-45678901", # Too many digits after hyphen
"12_3456789", # Wrong separator
"ab-3456789", # Letters instead of numbers
"12-abcdefg", # Letters instead of numbers
"123456789", # No hyphen
"-12345678", # Hyphen at start
"12-34567-", # Hyphen at end
]
for tin in invalid_tins:
result, is_valid = tin_check(tin)
assert result == tin
assert is_valid is False
def test_non_string_inputs():
"""Test handling of non-string inputs"""
test_cases = [
(123456789, "123456789", False),
(12.3456789, "12.3456789", False),
(None, 'None', False),
([12, 3456789], "[12, 3456789]", False),
({"tin":"12-3456789"}, "{'tin': '12-3456789'}", False),
]
for input_val, expected_str, expected_valid in test_cases:
result, is_valid = tin_check(input_val)
assert result == expected_str
assert is_valid is expected_valid
def test_edge_cases():
"""Test edge cases with valid-looking but incorrect formats"""
test_cases = [
"12 3456789", #Space instead of dash
"1 2-3456789", # Space at beginning
"12-345 6789", # Space in middle
]
for tin in test_cases:
result, is_valid = tin_check(tin)
assert result == tin
assert is_valid is False
def test_multiple_tins_in_string():
"""Test strings containing multiple TIN-like patterns"""
test_cases = [
"12-3456789 34-5678901", # Two TINs with space
"12-345678912-3456789", # Two TINs without space
"Some text 12-3456789", # TIN with preceding text
"12-3456789 some text", # TIN with following text
]
for tin in test_cases:
result, is_valid = tin_check(tin)
assert result == tin
assert is_valid is False
+26
View File
@@ -0,0 +1,26 @@
import pytest
import sys
sys.path.append('src')
from qa_qc_helpers import yn_check
def test_yn_check_with_valid_string_input():
# Test with valid string inputs
assert yn_check("Y") == ("Y", True)
assert yn_check("N") == ("N", True)
def test_yn_check_with_invalid_string_input():
# Test with invalid string inputs
assert yn_check("a") == ("a", False)
assert yn_check("yes") == ("yes", False)
def test_yn_check_with_non_string_input():
# Test with non-string inputs
assert yn_check(1) == ("1", False)
assert yn_check(True) == ("True", False)
def test_yn_check_with_non_convertible_input():
# Test with inputs that cannot be converted to string
def raise_type_error():
raise TypeError("Cannot convert to string")
assert yn_check(raise_type_error)[1] == False