diff --git a/README.md b/README.md index 59a3efc..937bce4 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ -**Hello world!!!** +## Overview +This block (`block.py`) is responsible for preparing and validating inputs for the model. It performs data cleansing and returns a normalized output dictionary. + +## Key Inputs & Outputs +- **Request**: Refer to `request_schema.json` for detailed input fields and validation rules. +- **Response**: Refer to `response_schema.json` for the returned structure and data types. + +## Implementation Details +- All core logic resides in `block.py` within the `__main__` function. +- Example usage and validation are demonstrated in `test_block.py`. diff --git a/block.py b/block.py index 3b227f9..ed22ea7 100644 --- a/block.py +++ b/block.py @@ -1,21 +1,235 @@ -@flowx_block -def example_function(request: dict) -> dict: +import logging +import math - # Processing logic here... +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s - %(message)s", +) +logger = logging.getLogger(__name__) - return { - "meta_info": [ - { - "name": "created_date", - "type": "string", - "value": "2024-11-05" - } - ], - "fields": [ - { - "name": "", - "type": "", - "value": "" - } - ] - } +def __main__(record_counts_revolving_trade_count:int,record_counts_total_trade_count:int,score_results:float,total_amount_high_credit:float,revolving_amount_credit_limit:float,revolving_amount_percent_available_credit:float,revolving_amount_current_balance:float,revolving_amount_monthly_payment:float,revolving_amount_high_credit:float,closed_with_balance_amount_current_balance:float,closed_with_balance_amount_monthly_payment:float,AGG101:float,AGG102:float,AT09S:int,AT20S:int,AT31S:int,BALMAG01:float,BC21S:int,PAYMNT10:float,REV83:float,US01S:int,monthly_income:float,internal_monthly_payment:float,installment_amount_monthly_payment:float,open_amount_current_balance:int,installment_amount_current_balance:int)->dict: + #record_counts_revolving_trade_count treatment + if record_counts_revolving_trade_count is None or math.isnan(record_counts_revolving_trade_count): + record_counts_revolving_trade_count = None + elif 0 <= record_counts_revolving_trade_count <= 999: + record_counts_revolving_trade_count = min(30, max(0, record_counts_revolving_trade_count)) + else: + record_counts_revolving_trade_count = None + + #record_counts_total_trade_count treatment + if record_counts_total_trade_count is None or math.isnan(record_counts_total_trade_count): + record_counts_total_trade_count = None + elif 0 <= record_counts_total_trade_count <= 999: + record_counts_total_trade_count = min(35, max(0, record_counts_total_trade_count)) + else: + record_counts_total_trade_count = None + + #score_results treatment + if score_results is None or math.isnan(score_results): + score_results = None + elif 350.0 <= score_results <= 850.0: + score_results = min(850.0, max(350.0, score_results)) + else: + score_results = None + + #revolving_amount_credit_limit treatment + if revolving_amount_credit_limit is None or math.isnan(revolving_amount_credit_limit): + revolving_amount_credit_limit = None + elif 0.0 <= revolving_amount_credit_limit <= 999999999.0: + revolving_amount_credit_limit = min(20000.0, max(0.0, revolving_amount_credit_limit)) + else: + revolving_amount_credit_limit = None + + #revolving_amount_percent_available_credit treatment + if revolving_amount_percent_available_credit is None or math.isnan(revolving_amount_percent_available_credit): + revolving_amount_percent_available_credit = None + elif 0.0 <= revolving_amount_percent_available_credit <= 100.0: + revolving_amount_percent_available_credit = min(100.0, max(0.0, revolving_amount_percent_available_credit)) + else: + revolving_amount_percent_available_credit = None + + #total_amount_monthly_payment + if (revolving_amount_monthly_payment is None or math.isnan(revolving_amount_monthly_payment)) and (installment_amount_monthly_payment is None or math.isnan(installment_amount_monthly_payment)): + total_amount_monthly_payment = None + else: + total_amount_monthly_payment = sum(value for value in [revolving_amount_monthly_payment, installment_amount_monthly_payment] if value is not None) + + + #total_amount_current_balance + # if (open_amount_current_balance in [None, ""] or pd.isna(open_amount_current_balance)) and (revolving_amount_current_balance in [None, ""] or pd.isna(revolving_amount_current_balance)) and (installment_amount_current_balance in [None, ""] or pd.isna(installment_amount_current_balance)): + # if pd.isna(open_amount_current_balance) and pd.isna(revolving_amount_current_balance) and pd.isna(installment_amount_current_balance): + # total_amount_current_balance = None + # else: + # total_amount_current_balance = sum(filter(pd.notna, [open_amount_current_balance, revolving_amount_current_balance, installment_amount_current_balance])) + if (open_amount_current_balance is None or math.isnan(open_amount_current_balance)) and (revolving_amount_current_balance is None or math.isnan(revolving_amount_current_balance)) and (installment_amount_current_balance is None or math.isnan(installment_amount_current_balance)): + total_amount_current_balance = None + else: + total_amount_current_balance = sum(value for value in [open_amount_current_balance, revolving_amount_current_balance, installment_amount_current_balance] if value is not None and value != "") + + + #total_amount_current_balance treatment + if total_amount_current_balance is None or math.isnan(total_amount_current_balance): + total_amount_current_balance = None + elif 0.0 <= total_amount_current_balance <= 999999999.0: + total_amount_current_balance = min(120000.0, max(0.0, total_amount_current_balance)) + else: + total_amount_current_balance = None + + #revolving_amount_current_balance treatment + if revolving_amount_current_balance is None or math.isnan(revolving_amount_current_balance): + revolving_amount_current_balance = None + elif 0.0 <= revolving_amount_current_balance <= 999999999.0: + revolving_amount_current_balance = min(20000.0, max(0.0, revolving_amount_current_balance)) + else: + revolving_amount_current_balance = None + + #revolving_amount_monthly_payment treatment + if revolving_amount_monthly_payment is None or math.isnan(revolving_amount_monthly_payment): + revolving_amount_monthly_payment = None + elif 0.0 <= revolving_amount_monthly_payment <= 999999999.0: + revolving_amount_monthly_payment = min(500.0, max(0.0, revolving_amount_monthly_payment)) + else: + revolving_amount_monthly_payment = None + + #revolving_amount_high_credit treatment + if revolving_amount_high_credit is None or math.isnan(revolving_amount_high_credit): + revolving_amount_high_credit = None + elif 0.0 <= revolving_amount_high_credit <= 999999999.0: + revolving_amount_high_credit = min(20000.0, max(0.0, revolving_amount_high_credit)) + else: + revolving_amount_high_credit = None + + #AGG101 treatment + if AGG101 is None or math.isnan(AGG101): + AGG101 = None + elif 0.0 <= AGG101 <= 999999999.0: + AGG101 = min(40000.0, max(0.0, AGG101)) + else: + AGG101 = 0.0 + + #AGG102 treatment + if AGG102 is None or math.isnan(AGG102): + AGG102 = None + elif 0.0 <= AGG102 <= 999999999.0: + AGG102= min(60000.0, max(0.0, AGG102)) + else: + AGG102 = 0.0 + + #AT09S treatment + if AT09S is None or math.isnan(AT09S): + AT09S = None + elif 0 <= AT09S <= 999: + AT09S = min(10, max(0, AT09S)) + else: + AT09S = 0 + + #AT20S treatment + if AT20S is None or math.isnan(AT20S): + AT20S = None + elif 0 <= AT20S <= 999: + AT20S = min(500, max(0, AT20S)) + else: + AT20S = 0 + + #AT31S treatment + if AT31S is None or math.isnan(AT31S): + AT31S = None + elif 0 <= AT31S <= 999: + AT31S = min(100, max(0, AT31S)) + else: + AT31S = 0 + + #BALMAG01 treatment + if BALMAG01 is None or math.isnan(BALMAG01): + BALMAG01 = None + elif 0.0 <= BALMAG01 <= 600.0: + BALMAG01 = min(500.0, max(0.0, BALMAG01)) + else: + BALMAG01 = 500.0 + + #BC21S treatment + if BC21S is None or math.isnan(BC21S): + BC21S = None + elif 0 <= BC21S <= 999: + BC21S = min(100, max(0, BC21S)) + else: + BC21S = 100 + + #PAYMNT10 treatment + if PAYMNT10 is None or math.isnan(PAYMNT10): + PAYMNT10 = None + elif 0.0 <= PAYMNT10 <= 999.0: + PAYMNT10 = min(15.0, max(0.0, PAYMNT10)) + else: + PAYMNT10 = 0.0 + + #REV83 treatment + if REV83 is None or math.isnan(REV83): + REV83 = None + elif 0.0 <= REV83 <= 999.0: + REV83 = min(500.0, max(0.0, REV83)) + else: + REV83 = 500.0 + + #US01S treatment + if US01S is None or math.isnan(US01S): + US01S = None + elif 0 <= US01S <= 999: + US01S = min(10, max(0, US01S)) + else: + US01S = 10 + + #internal_monthly_payment treatment + if internal_monthly_payment is None or math.isnan(internal_monthly_payment): + internal_monthly_payment = 92.97 + + #monthly_income treatment + if (monthly_income is not None and monthly_income != "") and not math.isnan(monthly_income): + if 0.0 <= monthly_income <= 999999999.0: + monthly_income = min(8500.0, max(0.0, monthly_income)) + else: + monthly_income = None + + #pti calculation + if (monthly_income not in [None, 0.0] and not math.isnan(monthly_income) and internal_monthly_payment not in [None, ""] and not math.isnan(internal_monthly_payment) and total_amount_monthly_payment not in [None, ""] and not math.isnan(total_amount_monthly_payment)): + pti = (total_amount_monthly_payment + internal_monthly_payment) / monthly_income + elif ((monthly_income != 0.0) and monthly_income not in [None, ""] and monthly_income != monthly_income) and (total_amount_monthly_payment not in [None, ""] and total_amount_monthly_payment != total_amount_monthly_payment): + pti = (total_amount_monthly_payment + 92.97) / monthly_income + else: + pti = None + + #total_amount_high_credit treatment + if total_amount_high_credit is None or math.isnan(total_amount_high_credit): + total_amount_high_credit = None + elif 0.0 <= total_amount_high_credit <= 999999999.0: + total_amount_high_credit = min(150000.0, max(0.0, total_amount_high_credit)) + else: + total_amount_high_credit = None + + #closed_with_balance_amount_current_balance treatment + if closed_with_balance_amount_current_balance is None or math.isnan(closed_with_balance_amount_current_balance): + closed_with_balance_amount_current_balance = None + elif 0.0 <= closed_with_balance_amount_current_balance <= 999999999.0: + closed_with_balance_amount_current_balance = min(15000.0, max(0.0, closed_with_balance_amount_current_balance)) + else: + closed_with_balance_amount_current_balance = None + + #closed_with_balance_amount_monthly_payment treatment + if closed_with_balance_amount_monthly_payment is None or math.isnan(closed_with_balance_amount_monthly_payment): + closed_with_balance_amount_monthly_payment = None + elif 0.0 <= closed_with_balance_amount_monthly_payment <= 999999999.0: + closed_with_balance_amount_monthly_payment = min(300.0, max(0.0, closed_with_balance_amount_monthly_payment)) + else: + closed_with_balance_amount_monthly_payment = None + + output_data = {"pti" : pti,"score_results" : score_results,"BALMAG01" : BALMAG01,"revolving_amount_monthly_payment" : revolving_amount_monthly_payment, + "closed_with_balance_amount_current_balance" : closed_with_balance_amount_current_balance,"AT31S" : AT31S,"AT20S" : AT20S, + "BC21S" : BC21S,"record_counts_revolving_trade_count" : record_counts_revolving_trade_count,"record_counts_total_trade_count" : record_counts_total_trade_count, + "PAYMNT10" : PAYMNT10,"AGG102" : AGG102,"total_amount_high_credit" : total_amount_high_credit,"revolving_amount_current_balance" : revolving_amount_current_balance, + "total_amount_current_balance" : total_amount_current_balance,"REV83" : REV83,"revolving_amount_high_credit" : revolving_amount_high_credit, + "closed_with_balance_amount_monthly_payment" : closed_with_balance_amount_monthly_payment,"revolving_amount_percent_available_credit" : revolving_amount_percent_available_credit, + "AGG101" : AGG101,"revolving_amount_credit_limit" : revolving_amount_credit_limit,"AT09S" : AT09S,"US01S" : US01S} + + logger.info(f"PD V2 Pre processed data: {output_data}") + return output_data \ No newline at end of file diff --git a/request_schema.json b/request_schema.json index 0967ef4..3978809 100644 --- a/request_schema.json +++ b/request_schema.json @@ -1 +1,111 @@ -{} +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "record_counts_revolving_trade_count": { + "type": ["integer", "null"], + "description": "Records in the database related to revolving trade accounts (a credit card account)." + }, + "record_counts_total_trade_count": { + "type": ["integer", "null"], + "description": "Total number of trade-related (transaction) records." + }, + "score_results": { + "type": ["number", "null"], + "description": "TransUnion score." + }, + "total_amount_high_credit": { + "type": ["number", "null"], + "description": "The highest credit amount extended across all credit accounts." + }, + "revolving_amount_credit_limit": { + "type": ["number", "null"], + "description": "The total credit limit on revolving credit accounts." + }, + "revolving_amount_percent_available_credit": { + "type": ["number", "null"], + "description": "The percentage of available credit that has been utilized in revolving credit accounts." + }, + "revolving_amount_current_balance": { + "type": ["number", "null"], + "description": "The current owed balance on revolving credit accounts." + }, + "revolving_amount_monthly_payment": { + "type": ["number", "null"], + "description": "Minimum amount the borrower is required to pay each month to maintain the account in good standing." + }, + "revolving_amount_high_credit": { + "type": ["number", "null"], + "description": "The highest credit amount that has been extended to the borrower in revolving credit accounts." + }, + "closed_with_balance_amount_current_balance": { + "type": ["number", "null"], + "description": "The current balance of closed credit accounts." + }, + "closed_with_balance_amount_monthly_payment": { + "type": ["number", "null"], + "description": "The monthly payment amount for closed credit accounts (loans)." + }, + "AGG101": { + "type": ["number", "null"], + "description": "Aggregate non-mortgage balances for month 1." + }, + "AGG102": { + "type": ["number", "null"], + "description": "Aggregate non-mortgage balances for month 2." + }, + "AT09S": { + "type": ["integer", "null"], + "description": "Number of trades opened in the past 24 months." + }, + "AT20S": { + "type": ["integer", "null"], + "description": "Months since the oldest trade opened." + }, + "AT31S": { + "type": ["integer", "null"], + "description": "Percentage of open trades > 75% of credit line verified in the past 12 months." + }, + "BALMAG01": { + "type": ["number", "null"], + "description": "Non-mortgage balance magnitude." + }, + "BC21S": { + "type": ["integer", "null"], + "description": "Months since the most recent credit card trade opened." + }, + "PAYMNT10": { + "type": ["number", "null"], + "description": "Number of payments in the last quarter." + }, + "REV83": { + "type": ["number", "null"], + "description": "Months since a revolving account last exceeded 75% utilization." + }, + "US01S": { + "type": ["integer", "null"], + "description": "Number of unsecured installment trades." + }, + "monthly_income": { + "type": ["number", "null"], + "description": "User-stated income." + }, + "internal_monthly_payment": { + "type": ["number", "null"], + "description": "If loan sanctioned to customer, expected monthly payment to Kiwi." + }, + "installment_amount_monthly_payment": { + "type": ["number", "null"], + "description": "Amount the borrower is required to pay each month on installment loans." + }, + "open_amount_current_balance": { + "type": ["integer", "null"], + "description": "The current owed balance on all open accounts." + }, + "installment_amount_current_balance": { + "type": ["integer", "null"], + "description": "The current owed balance on installment trades/accounts." + } + }, + "required": [] +} diff --git a/requirements.txt b/requirements.txt index 0967ef4..8c4acbc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -{} +jsonschema==4.23.0 \ No newline at end of file diff --git a/response_schema.json b/response_schema.json index 0967ef4..576b17b 100644 --- a/response_schema.json +++ b/response_schema.json @@ -1 +1,98 @@ -{} +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "pti": { + "type": ["number", "null"], + "description": "External + internal monthly payment to income ratio." + }, + "score_results": { + "type": ["number", "null"], + "description": "TransUnion score." + }, + "BALMAG01": { + "type": ["number", "null"], + "description": "Non-mortgage balance magnitude." + }, + "revolving_amount_monthly_payment": { + "type": ["number", "null"], + "description": "Minimum amount the borrower is required to pay each month to maintain the account in good standing." + }, + "closed_with_balance_amount_current_balance": { + "type": ["number", "null"], + "description": "The current balance of closed credit accounts." + }, + "AT31S": { + "type": ["integer", "null"], + "description": "Percentage of open trades > 75% of credit line verified in past 12 months." + }, + "AT20S": { + "type": ["integer", "null"], + "description": "Months since oldest trade opened." + }, + "BC21S": { + "type": ["integer", "null"], + "description": "Months since most recent credit card trade opened." + }, + "record_counts_revolving_trade_count": { + "type": ["integer", "null"], + "description": "Records in the database related to revolving trade accounts (a credit card account)." + }, + "record_counts_total_trade_count": { + "type": ["integer", "null"], + "description": "Total number of trade-related (transaction) records." + }, + "PAYMNT10": { + "type": ["number", "null"], + "description": "Number of payments in the last quarter." + }, + "AGG102": { + "type": ["number", "null"], + "description": "Aggregate non-mortgage balances for month 2." + }, + "total_amount_high_credit": { + "type": ["number", "null"], + "description": "The highest credit amount extended across all credit accounts." + }, + "revolving_amount_current_balance": { + "type": ["number", "null"], + "description": "The current owed balance on revolving credit accounts." + }, + "total_amount_current_balance": { + "type": ["number", "null"], + "description": "The total current balance across all credit accounts." + }, + "REV83": { + "type": ["number", "null"], + "description": "Months since a revolving account last exceeded 75% utilization." + }, + "revolving_amount_high_credit": { + "type": ["number", "null"], + "description": "The highest credit amount that has been extended to the borrower in revolving credit accounts." + }, + "closed_with_balance_amount_monthly_payment": { + "type": ["number", "null"], + "description": "The monthly payment amount for closed credit accounts (loans)." + }, + "revolving_amount_percent_available_credit": { + "type": ["number", "null"], + "description": "The percentage of available credit that has been utilized in revolving credit accounts." + }, + "AGG101": { + "type": ["number", "null"], + "description": "Aggregate non-mortgage balances for month 1." + }, + "revolving_amount_credit_limit": { + "type": ["number", "null"], + "description": "The total credit limit on revolving credit accounts." + }, + "AT09S": { + "type": ["integer", "null"], + "description": "Number of trades opened in the past 24 months." + }, + "US01S": { + "type": ["integer", "null"], + "description": "Number of unsecured installment trades." + } + } +} diff --git a/test_block.py b/test_block.py new file mode 100644 index 0000000..4f57527 --- /dev/null +++ b/test_block.py @@ -0,0 +1,21 @@ +import unittest +from block import __main__ + +class TestBlock(unittest.TestCase): + + def test_main_success(self): + result = __main__(record_counts_revolving_trade_count= None,record_counts_total_trade_count= 18,score_results= 600,total_amount_high_credit= 53807,revolving_amount_credit_limit= 2000,revolving_amount_percent_available_credit= 18,revolving_amount_current_balance= 1635,revolving_amount_monthly_payment= 56,revolving_amount_high_credit= 1720,closed_with_balance_amount_current_balance= 8411,closed_with_balance_amount_monthly_payment= 0,AGG101= 11043,AGG102= 24994,AT09S= 4,AT20S= 166,AT31S= 71,BALMAG01= 196,BC21S= 4,PAYMNT10= 4,REV83= 0,US01S= 0,open_amount_current_balance= None,installment_amount_current_balance= 36718,monthly_income= None,internal_monthly_payment= None,installment_amount_monthly_payment= 572) + + expected_result = {"pti": None,"score_results": 600.0,"BALMAG01": 196.0,"revolving_amount_monthly_payment": 56.0,"closed_with_balance_amount_current_balance": 8411.0,"AT31S": 71.0,"AT20S": 166.0,"BC21S": 4.0,"record_counts_revolving_trade_count": None,"record_counts_total_trade_count": 18.0,"PAYMNT10": 4.0,"AGG102": 24994.0,"total_amount_high_credit": 53807.0,"revolving_amount_current_balance": 1635.0,"total_amount_current_balance": 38353.0,"REV83": 0.0,"revolving_amount_high_credit": 1720.0,"closed_with_balance_amount_monthly_payment": 0.0,"revolving_amount_percent_available_credit": 18.0,"AGG101": 11043.0,"revolving_amount_credit_limit": 2000.0,"AT09S": 4.0,"US01S": 0.0} + for key, expected_value in expected_result.items(): + if isinstance(expected_value, float): + self.assertAlmostEqual(result[key], expected_value, places=6, msg=f"Mismatch for {key}") + else: + self.assertEqual(result[key], expected_value, msg=f"Mismatch for {key}") + + def test_main_invalid_input(self): + with self.assertRaises(TypeError): + __main__(record_counts_revolving_trade_count= 9,record_counts_total_trade_count= 18,score_results= 600,total_amount_high_credit= 53807,revolving_amount_credit_limit= 2000,revolving_amount_percent_available_credit= 18,revolving_amount_current_balance= 1635,revolving_amount_monthly_payment= 56,revolving_amount_high_credit= 1720,closed_with_balance_amount_current_balance= 8411,closed_with_balance_amount_monthly_payment= 0,AGG101= 11043,AGG102= 24994,AT09S= 4,AT20S= 166,AT31S= 71,BALMAG01= 196,BC21S= 4,PAYMNT10= 4,REV83= 0,US01S= 0,open_amount_current_balance= None,installment_amount_current_balance= 36718,monthly_income= "2200.00",internal_monthly_payment= None,installment_amount_monthly_payment= 572) # Invalid input type (string) + +if __name__ == "__main__": + unittest.main()