afb6d5185d
Feature/lesser table caching refactor hybrid * chore: Remove unused duplicate main.py from shared pipeline * fix: Correct crosswalk paths in aarete_derived.py * chore: Remove unused documentation files from fieldExtraction * docs: Add documentation files to documentation folder * docs: Update README with uv setup, expanded project structure, and branching conventions * docs: Add uv installation steps with Ubuntu/WSL emphasis * Enable prompt caching for all remaining LLM calls - Add _INSTRUCTION() functions for: EXHIBIT_HEADER, EXHIBIT_LINKAGE, EXHIBIT_TITLE_MATCH, DATE_FIX, DERIVED_TERM_DATE, CHECK_PROVIDER_NAME_MATCH, SPECIAL_CASE_ASSIGNMENT - Update all invoke_claude() calls in saas and clover pipelines to use cache=True with corresponding _INSTRUCTION() functions - Add new instructions to get_cacheable_instructions() for cache warming - Update tests for new instruction functions Functions now using caching: - prompt_exhibit_level - prompt_exhibit_lesser (EXHIBIT_LEVEL_LESSER_OF) - prompt_fee_schedule_breakout - prompt_grouper_breakout - prompt_special_case_assignment - prompt_exhibit_linkage - prompt_exhibit_header - prompt_smart_chunked (ONE_TO_ONE templates) - prompt_date_fix - prompt_derived_term_date - prompt_exhibit_title_match - provider_name_match_check 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Reorder * feat: Add bcbs_promise client pipeline with OFFSET_TERM extraction - Add new bcbs_promise client with HSC-based OFFSET_TERM field extraction - Extract full paragraph text of offset/recoupment provisions from contracts - Derive OFFSET_INDICATOR (Y/N) from OFFSET_TERM presence - Fix reorder_columns to preserve extra columns not in COLUMN_ORDER - Update QC/QA output path to outputs/qc_qa/ * fix: Update dev deps and test assertions for QC/QA output path - Add pytest/pytest-mock to dev dependencies for mypy type checking - Update test assertions to expect outputs/qc_qa instead of qa_qc_output * style: Apply black formatting to prompt_templates.py * Merge main, move scripts * Archive some scripts * update py version * remove .py version file * Remove ASCII characters * Restore testbed code * restore tracking * Update testbed metrics * Enable prompt caching for CODE_LAST_CHECK, FILL_BILL_TYPE, DUAL_LOB_CHECK, and GROUPER_BREAKOUT - Add CODE_LAST_CHECK_INSTRUCTION() for service specificity classification - Add FILL_BILL_TYPE_INSTRUCTION() for bill type code determination - Add DUAL_LOB_CHECK_INSTRUCTION() for Medicare/Medicaid classification - Update code_funcs.py to use caching for CODE_LAST_CHECK, FILL_BILL_TYPE, GROUPER_BREAKOUT - Update postprocessing_funcs.py to use caching for DUAL_LOB_CHECK - Add new instructions to get_cacheable_instructions() for cache warming - Add unit tests for new instruction functions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix postprocessing_funcs to remove invalid columns * Merge branch 'main' into feature/lesser-table-caching-refactor-hybrid * Revert prompt caching changes from aed1b73c * update formatting * Update imports Approved-by: Sha Brown Approved-by: Praneel Panchigar
194 lines
6.7 KiB
Python
194 lines
6.7 KiB
Python
import src.config as config
|
|
from datetime import datetime
|
|
import pandas as pd
|
|
import src.utils.string_utils as string_utils
|
|
import io
|
|
|
|
|
|
def find_inv_test_results():
|
|
"""
|
|
Efficiently find the 30 most recent directories containing 'inv-test'
|
|
and list their RESULTS.csv files.
|
|
|
|
Strategy: Only list top-level directories first, filter for 'inv-test',
|
|
then search only those directories for RESULTS.csv files.
|
|
"""
|
|
s3_client = config.S3_CLIENT
|
|
bucket_name = config.S3_OUTPUT_BUCKET
|
|
|
|
print(f"\nSearching for 'inv-test' directories in bucket: {bucket_name}")
|
|
print("=" * 80)
|
|
|
|
# Step 1: Get only top-level directories (much faster than scanning all files)
|
|
print("Step 1: Listing top-level directories...")
|
|
response = s3_client.list_objects_v2(Bucket=bucket_name, Delimiter="/")
|
|
|
|
if "CommonPrefixes" not in response:
|
|
print("No directories found in bucket")
|
|
return []
|
|
|
|
all_top_level_dirs = [
|
|
prefix["Prefix"].rstrip("/") for prefix in response["CommonPrefixes"]
|
|
]
|
|
print(f"Found {len(all_top_level_dirs)} total top-level directories")
|
|
|
|
# Step 2: Filter for 'inv-test' directories only
|
|
inv_test_dirs = [d for d in all_top_level_dirs if "inv-test" in d]
|
|
print(f"Found {len(inv_test_dirs)} directories containing 'inv-test'")
|
|
|
|
if not inv_test_dirs:
|
|
print("No 'inv-test' directories found")
|
|
return []
|
|
|
|
# Step 3: Get timestamps for inv-test directories only (not all files in bucket)
|
|
print("\nStep 2: Getting timestamps for inv-test directories...")
|
|
dir_timestamps = []
|
|
|
|
for dir_name in inv_test_dirs:
|
|
# Get just the first file in each directory to get a timestamp
|
|
response = s3_client.list_objects_v2(
|
|
Bucket=bucket_name, Prefix=f"{dir_name}/", MaxKeys=1
|
|
)
|
|
|
|
if "Contents" in response and len(response["Contents"]) > 0:
|
|
timestamp = response["Contents"][0]["LastModified"]
|
|
dir_timestamps.append((dir_name, timestamp))
|
|
|
|
# Sort by timestamp (most recent first)
|
|
dir_timestamps.sort(key=lambda x: x[1], reverse=True)
|
|
|
|
# Get the 30 most recent
|
|
recent_inv_test_dirs = dir_timestamps[:30]
|
|
|
|
print(
|
|
f"\nStep 3: Searching 30 most recent 'inv-test' directories for RESULTS.csv files:"
|
|
)
|
|
print("=" * 80)
|
|
|
|
total_results_files = 0
|
|
all_results_files = {} # Dictionary to store directory -> list of file paths
|
|
|
|
for i, (dir_name, timestamp) in enumerate(recent_inv_test_dirs, 1):
|
|
print(f"\n[{i}/30] Directory: {dir_name}")
|
|
print(f" Last Modified: {timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
|
|
# Search only this directory for RESULTS.csv files
|
|
results_files = []
|
|
paginator = s3_client.get_paginator("list_objects_v2")
|
|
|
|
for page in paginator.paginate(Bucket=bucket_name, Prefix=f"{dir_name}/"):
|
|
if "Contents" not in page:
|
|
continue
|
|
|
|
for obj in page["Contents"]:
|
|
key = obj["Key"]
|
|
# Ignore files in subdirectories named "individual"
|
|
if "/individual/" in key:
|
|
continue
|
|
if key.endswith("-RESULTS.csv"):
|
|
results_files.append(key)
|
|
|
|
if results_files:
|
|
print(f" Found {len(results_files)} RESULTS.csv file(s):")
|
|
for file_key in results_files:
|
|
print(f" ✓ {file_key}")
|
|
total_results_files += 1
|
|
all_results_files[dir_name] = results_files
|
|
else:
|
|
print(f" ✗ No -RESULTS.csv files found")
|
|
|
|
print("\n" + "=" * 80)
|
|
print(f"Summary:")
|
|
print(f" - Searched 30 most recent 'inv-test' directories")
|
|
print(f" - Total -RESULTS.csv files found: {total_results_files}")
|
|
print("=" * 80)
|
|
|
|
# Step 4: Generate missingness report
|
|
if total_results_files > 0:
|
|
print(f"\nStep 4: Generating missingness report...")
|
|
generate_missingness_report(s3_client, bucket_name, all_results_files)
|
|
|
|
return recent_inv_test_dirs
|
|
|
|
|
|
def generate_missingness_report(s3_client, bucket_name, results_files_dict):
|
|
"""
|
|
Generate a missingness report for all RESULTS.csv files.
|
|
|
|
Args:
|
|
s3_client: S3 client object
|
|
bucket_name: S3 bucket name
|
|
results_files_dict: Dictionary mapping directory names to lists of result file paths
|
|
"""
|
|
print("=" * 80)
|
|
print("Generating Missingness Report")
|
|
print("=" * 80)
|
|
|
|
missingness_data = []
|
|
|
|
for dir_name, file_paths in results_files_dict.items():
|
|
for file_path in file_paths:
|
|
print(f"\nProcessing: {file_path}")
|
|
|
|
try:
|
|
# Read CSV from S3
|
|
response = s3_client.get_object(Bucket=bucket_name, Key=file_path)
|
|
csv_content = response["Body"].read().decode("utf-8")
|
|
df = pd.read_csv(io.StringIO(csv_content))
|
|
|
|
if df.empty:
|
|
print(f" ⚠ File is empty, skipping")
|
|
continue
|
|
|
|
# Calculate missingness for each column
|
|
row_data = {"csv_file": file_path}
|
|
|
|
for column in df.columns:
|
|
empty_mask = string_utils.is_empty(df[column], pd_mask=True)
|
|
missingness_pct = (empty_mask.sum() / len(df)) * 100
|
|
row_data[column] = round(missingness_pct, 2)
|
|
|
|
missingness_data.append(row_data)
|
|
print(f" ✓ Processed {len(df)} rows, {len(df.columns)} columns")
|
|
|
|
except Exception as e:
|
|
print(f" ✗ Error processing file: {e}")
|
|
continue
|
|
|
|
if not missingness_data:
|
|
print("\nNo valid data to create report")
|
|
return
|
|
|
|
# Create missingness DataFrame
|
|
missingness_df = pd.DataFrame(missingness_data)
|
|
|
|
# Set csv_file as index for better readability
|
|
missingness_df.set_index("csv_file", inplace=True)
|
|
|
|
# Save report
|
|
output_filename = (
|
|
f"missingness_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
|
)
|
|
missingness_df.to_csv(output_filename)
|
|
|
|
print("\n" + "=" * 80)
|
|
print(f"Missingness Report Summary:")
|
|
print(f" - Total CSV files processed: {len(missingness_data)}")
|
|
print(f" - Total columns analyzed: {len(missingness_df.columns)}")
|
|
print(f" - Report saved to: {output_filename}")
|
|
print("=" * 80)
|
|
|
|
# Display sample of the report
|
|
print("\nSample of Missingness Report (first 5 columns):")
|
|
print("-" * 80)
|
|
if len(missingness_df.columns) > 5:
|
|
print(missingness_df.iloc[:, :5].to_string())
|
|
print(f"\n... and {len(missingness_df.columns) - 5} more columns")
|
|
else:
|
|
print(missingness_df.to_string())
|
|
print("-" * 80)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
find_inv_test_results()
|