Upload files to "/"
All checks were successful
Build and Push Docker Image / test (push) Successful in 19s
Build and Push Docker Image / build_and_push (push) Successful in 3m16s

This commit is contained in:
Admin User 2025-06-18 17:12:25 +00:00
parent 848a80fb09
commit 8f7a795573
4 changed files with 287 additions and 54 deletions

View File

@ -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)
__main__(application_id=data["application_id"], creditBureau=data)

View File

@ -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__":

View File

@ -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"
}]
}

View File

@ -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"
]
}