diff --git a/block.py b/block.py index 39944ad..1dc0b25 100644 --- a/block.py +++ b/block.py @@ -20,7 +20,7 @@ def __main__(application_id: str, creditBureau: dict) -> Dict: A dict containing: - prediction: float - grade: str - - reason_description: str or None + - reason_codes: list or None """ record = extract_model_variables(creditBureau) @@ -29,24 +29,27 @@ def __main__(application_id: str, creditBureau: dict) -> Dict: 'application_id': application_id, 'prediction': 0.99, 'grade': 'M14', - 'reason_description': "Lack of account information", + "reason_codes": [{ + "code": None, + "rank": None, + "description": "Lack of account information" + }] } - logger.info(f"final_result (early exit due to missing or empty extracted variables): {final_result}") + logger.info( + f"final_result (early exit due to missing or empty extracted variables): {final_result}") return final_result processed = pre_processing(record) out = processing(processed) - final = post_processing(out) + final = post_processing(out, record) final_result = { 'application_id': application_id, 'prediction': out['prediction'], 'grade': final['grade'], - 'reason_description': final['reason_description'], - # 'tu_credit_report': record, - # 'pre_processed_output': processed + 'reason_codes': final['reason_codes'], } logger.info(f"final_result: {final_result}") @@ -55,7 +58,8 @@ def __main__(application_id: str, creditBureau: dict) -> Dict: if __name__ == "__main__": - import json, sys + import json + import sys with open(sys.argv[1]) as f: data = json.load(f) - __main__(application_id=data["application_id"], creditBureau=data) \ No newline at end of file + __main__(application_id=data["application_id"], creditBureau=data) diff --git a/parse_report.py b/parse_report.py index 79430d0..972d571 100644 --- a/parse_report.py +++ b/parse_report.py @@ -163,8 +163,25 @@ def extract_model_variables(creditBureau: dict) -> dict: # 2b. Also capture score result if present score = safe_get(score_model, "score") - if score and "results" in score: - value_map[(code, "score")] = score["results"] + if score: + # Capture score result + results = score.get("results") + if isinstance(results, str): + value_map[(code, "score")] = results + + # Capture top 4 score factors as list of {code, rank} + raw_factors = score.get("factors", {}).get("factor", []) + if not isinstance(raw_factors, list): + raw_factors = [] + + top_factors = [ + {"code": f.get("code"), "rank": f.get("rank")} + for f in raw_factors[:4] + if isinstance(f, dict) and "code" in f and "rank" in f + ] + if top_factors: + value_map[(code, "factors")] = top_factors + # Step 3.a: Use variable_to_code_map to fetch final vars for var, code in variable_code_map.items(): @@ -176,6 +193,11 @@ def extract_model_variables(creditBureau: dict) -> dict: if value: extracted[key] = value.lstrip("+") + # Extract factor list, if available + factor_list = value_map.get((code, "factors")) + if factor_list: + extracted[f"{key}_factors"] = factor_list + return extracted if __name__ == "__main__": diff --git a/post_processing.py b/post_processing.py index 99b58b5..1bc7d67 100644 --- a/post_processing.py +++ b/post_processing.py @@ -1,7 +1,180 @@ import math -def post_processing(processing_output): - prediction = processing_output["prediction"] +EPD_REASON_MAP = { + "000": "No Adverse Factors", + "001": "Available Credit On Bankcard Accounts Is Too Low", + "006": "Bankcard Account Balances Are Too High In Proportion To Credit Limits", + "010": "Too Many Delinquencies", + "016": "Too Few Satisfactory Accounts", + "020": "Length Of Time Revolving Accounts Have Been Established Is Too Short", + "022": "Too many inquiries", + "023": "Months Since Most Recent Delinquency Is Too Short", + "024": "Too Many Serious Delinquencies", + "026": "Number Of Delinquent Accounts Is Too High In Proportion To Total Number Of Accounts", + "029": "Retail Account Balances Are Too High In Proportion To Credit Limits", + "030": "Not Enough Retail Debt Experience", + "031": "Revolving Account Balances Are Too High In Proportion To Credit Limits", + "035": "Length Of Time Accounts Have Been Established Is Too Short", + "037": "Too Few Bankcard Accounts", + "043": "Too Few Open Revolving Accounts", + "061": "Too Many Recently Opened Accounts", + "066": "Too Many Serious Derogatory Items", + "069": "Not Enough Debt Experience", + "070": "Length Of Time Since Most Recent Bankcard Account Has Been Established Is Too Short", + "074": "Too Few Satisfactory Revolving Accounts", + "076": "Total Amount Past Due Is Too High", + "103": "Not Enough Available Credit", + "105": "Too Few Revolving Accounts", + "117": "Length Of Time Since Most Seriously Delinquent Account Has Been Established Is Too Short", + "132": "Too Few Open Accounts", + "142": "Not Enough Balance Decreases On Active Non-Mortgage Accounts", + "146": "Recency Of A Balance Overlimit On A Bankcard Account", + "154": "Insufficient Payment Activity Over The Last Year", + "155": "Recency Of Max Aggregate Bankcard Balance Over The Last Year", + "158": "Too Few Open Retail Accounts", + "174": "Too Few Open Bankcard Accounts", + "181": "High Recent Balance Range Relative To Previous Balance Range", + "192": "Not Enough Available Credit On Revolving Accounts", + "201": "Length Of Time Since Oldest Auto Account Has Been Established Is Too Short" +} + +VANTAGE_REASON_MAP = { + "10": "Too few accounts paid as agreed", + "11": "Oldest account was opened too recently", + "12": "Delinquent or derogatory status on accounts is too recent", + "13": "Balances on delinquent or derogatory accounts are too high", + "14": "Too high proportion of accounts recently opened", + "15": "Lack of recently reported accounts", + "16": "Total of credit limits and loan amounts is too low", + "17": "No open accounts in your credit file", + "18": "Lack of account information", + "19": "No negative reason code", + "20": "Delinquent or derogatory bankcard", + "21": "Too many bankcards with a high balance", + "22": "Too few bankcards with high credit limit", + "23": "Too high proportion of bankcards recently opened", + "24": "Too many bankcards with high balance compared to credit limit", + "25": "Too high proportion of balances from bankcards", + "26": "Balances on bankcards are too high", + "27": "Delinquent or derogatory status on revolving accounts is too recent", + "28": "Average credit limit on open bankcards is too low", + "29": "Balances on bankcards are too high compared with credit limits", + "30": "Too few open revolving accounts", + "31": "Not enough available credit on revolving accounts", + "32": "Oldest bankcard was opened too recently", + "33": "Not enough balance paid down over time on bankcards", + "34": "Most recently opened revolving account is too new", + "35": "Lack of revolving account information", + "36": "Lack of recently reported revolving accounts", + "37": "No open bankcards in your credit file", + "38": "Lack of bankcard account information", + "39": "Balances on delinquent or derogatory bankcards are too high", + "4": "Balances on accts too high compared to credit limits and loan amounts", + "40": "Too many delinquent or derogatory revolving accounts", + "41": "Average time since revolving accounts opened is too recent", + "42": "Total credit limits on open revolving accounts are too low", + "43": "Too many revolving accounts with high balance compared to credit limit", + "44": "Balances on revolving accts are too high compared with credit limits", + "45": "Not enough balance paid down over time on retail accounts", + "46": "Oldest revolving account was opened too recently", + "47": "No open retail accounts in your credit file", + "48": "Lack of retail account information", + "49": "Not enough balance paid down over time on revolving accounts", + "5": "Too many recent delinquencies", + "50": "Balances on personal installment accts too high compared to loan amts", + "51": "Too few installment accounts recently paid as agreed", + "52": "Delinquent or derogatory installment account", + "53": "Not enough balance paid down over time on installment accounts", + "54": "Delinquent or derogatory status on installment accounts is too recent", + "55": "Lack of recently reported auto accounts", + "56": "Lack of recently reported installment accounts", + "57": "No open installment accounts in your credit file", + "58": "Lack of installment account information", + "59": "Balances on retail cards are too high compared with credit limits", + "6": "Too many accounts recently opened", + "60": "Total delinquent or derogatory balances on real estate loans too high", + "61": "No open first mortgage accounts in your credit file", + "62": "Lack of first mortgage account information", + "63": "Delinquent or derogatory real estate secured loan", + "64": "Not enough balance paid down over time on real estate secured loans", + "65": "Oldest real estate secured loan was opened too recently", + "66": "Delinquent or derogatory status on real estate loans is too recent", + "67": "No open real estate secured loans in your credit file", + "68": "Lack of real estate secured loan information", + "69": "Too high proportion of balances from loans not secured by real estate", + "7": "You have too many delinquent or derogatory accounts", + "70": "Too high proportion of auto accounts are delinquent or derogatory", + "71": "Not enough balance paid down over time on auto accounts", + "72": "Too few auto accounts paid as agreed", + "73": "Delinquent or derogatory auto account", + "74": "Balances on auto accounts are too high compared with loan amounts", + "75": "Payments on auto accounts less than scheduled amount", + "76": "Delinquent or derogatory status on auto accounts is too recent", + "77": "No open auto accounts in your credit file", + "78": "Lack of auto account information", + "79": "No negative reason code", + "8": "Too few accounts recently paid as agreed", + "80": "Delinquent or derogatory student loan", + "81": "Not enough balance paid down over time on student loans", + "82": "Lack of recently reported student loans", + "83": "No negative reason code", + "84": "Number of inquiries was a factor in determining the score", + "85": "Too many inquiries", + "86": "Derogatory public records", + "87": "Unpaid collections", + "88": "Bankruptcy", + "89": "No negative reason code", + "9": "Delinquent or derogatory account", + "90": "No open revolving accounts in your credit file", + "91": "Balances on delinquent or derogatory revolving accounts are too high", + "92": "Delinquent or derogatory first mortgage", + "93": "Not enough balance paid down over time on first mortgage accounts", + "94": "No negative reason code", + "95": "No negative reason code", + "96": "Too few open accounts", + "97": "Too few accounts" +} + +REASON_MAP = { + 'evtg04': "System Generated", + 'eads66': "System Generated", + 's004s': "Length of time on file is too short", + 'mt34s': "Not enough balance decreases on mortgage trades in the past 12 months", + 'ct320': "Insufficient payment activity", + 'us21s': "Length of time since most recent installment account has been established is too short", + 'utlmag02': "Revolving account balances are too high in proportion to credit limits over the last 24 months", + 'trv01': "Recency of a balance overlimit on a bankcard account", + 'us34s': "Not enough balance decreases on installment trades in the past 12 months" +} + + +def generate_reason_codes(score_key, factors): + # fallback to 4 null rows if no factors found + if not isinstance(factors, list) or len(factors) == 0: + return [{"code": None, "rank": None, "description": None} for _ in range(4)] + + reason_map = VANTAGE_REASON_MAP if score_key == "evtg04" else EPD_REASON_MAP if score_key == "eads66" else {} + + reason_codes = [] + for f in factors[:4]: + code = f.get("code") + rank = f.get("rank") + description = reason_map.get(str(code), "") + reason_codes.append({ + "code": code, + "rank": rank, + "description": description + }) + + # pad to 4 + while len(reason_codes) < 4: + reason_codes.append({"code": None, "rank": None, "description": None}) + + return reason_codes + + +def post_processing(processing_output, record): + prediction = processing_output["prediction"] shape_reasoncode = processing_output["shape_reasoncode"] # grade mapping: @@ -16,51 +189,59 @@ def post_processing(processing_output): # if prediction ≤ 0.04, not declined if prediction <= 0.04: return { - "grade": grade, - "reason_description": None + "grade": grade, + "reason_codes": [{ + "code": None, + "rank": None, + "description": None + }] } conditions = { - 'evtg04': lambda x: x < 700, - 'eads66': lambda x: x < 700, - 's004s': lambda x: x < 12, - 'mt34s': lambda x: x > 95, - 'ct320': lambda x: x <= 3, - 'us21s': lambda x: x <= 3, + 'evtg04': lambda x: x < 700, + 'eads66': lambda x: x < 700, + 's004s': lambda x: x < 12, + 'mt34s': lambda x: x > 95, + 'ct320': lambda x: x <= 3, + 'us21s': lambda x: x <= 3, 'utlmag02': lambda x: x > 300, - # 'trv01': lambda x: x > 3, - 'trv01': lambda x: x <= 3, - 'us34s': lambda x: x > 90 + 'trv01': lambda x: x <= 3, + 'us34s': lambda x: x > 90 } - reason_map = { - 'evtg04': "System Generated", - 'eads66': "System Generated", - 's004s': "Length of time on file is too short", - # 'mt34s': "Too high open mortgage credit utilization recently", - 'mt34s': "Not enough balance decreases on mortgage trades in the past 12 months", - 'ct320': "Insufficient payment activity", - 'us21s': "Length of time since most recent installment account has been established is too short", - # 'utlmag02': "Too high revolving credit utilization over the last 24 months", - 'utlmag02': "Revolving account balances are too high in proportion to credit limits over the last 24 months", - 'trv01': "Recency of a balance overlimit on a bankcard account", - # 'us34s': "Too high open unsecured installment credit utilization recently" - 'us34s': "Not enough balance decreases on installment trades in the past 12 months" - } - - for item in shape_reasoncode: - feat = item["feature"] - val = item["value"] + feat = item.get("feature") + val = item.get("value") cond = conditions.get(feat) - if cond and cond(val): - return { - "grade": grade, - "reason_description": reason_map[feat] - } + if cond: + try: + if cond(val): + # If score-type feature (evtg04 or eads66) → full factors-based reason + if feat in ("evtg04", "eads66"): + return { + "grade": grade, + "reason_codes": generate_reason_codes(feat, record.get(f"{feat}_factors", [])) + } + else: + # Other features → only 1 reason code based on REASON_MAP + return { + "grade": grade, + "reason_codes": [{ + "code": feat, + "rank": "1", + "description": REASON_MAP.get(feat, "Reason not mapped") + }] + } + except Exception: + continue + + # Default fallback return { - "grade": grade, - "reason_description": "No suitable Product Offerings found" + "grade": grade, + "reason_codes": [{ + "code": feat, + "rank": "1", + "description": "No suitable Product Offerings found" + }] } - diff --git a/response_schema.json b/response_schema.json index 88814b1..9e1ce66 100644 --- a/response_schema.json +++ b/response_schema.json @@ -14,9 +14,35 @@ "type": "string", "description": "HD Model Grade" }, - "reason_description": { - "type": ["string", "null"], - "description": "Reason for the model decision" + "reason_codes": { + "type": "array", + "description": "List of reason codes explaining the model decision", + "items": { + "type": "object", + "properties": { + "code": { + "type": ["string", "null"], + "description": "Feature or score reason code" + }, + "rank": { + "type": ["string", "null"], + "description": "Rank of importance (1 to 4)" + }, + "description": { + "type": ["string", "null"], + "description": "Human-readable explanation for the reason" + } + }, + "required": ["code", "rank", "description"] + }, + "minItems": 1, + "maxItems": 4 } - } + }, + "required": [ + "application_id", + "prediction", + "grade", + "reason_codes" + ] }