def scenario_builder(self): """Returns an agent and environment pair.""" env_params = lending_params.DelayedImpactParams( applicant_distribution=lending_params.two_group_credit_clusters( cluster_probabilities=self.cluster_probabilities, group_likelihoods=[self.group_0_prob, 1 - self.group_0_prob]), bank_starting_cash=self.bank_starting_cash, interest_rate=self.interest_rate, cluster_shift_increment=self.cluster_shift_increment, ) env = lending.DelayedImpactEnv(env_params) agent_params = classifier_agents.ScoringAgentParams( feature_keys=['applicant_features'], group_key='group', default_action_fn=(lambda: 1), burnin=self.burnin, convert_one_hot_to_integer=True, threshold_policy=self.threshold_policy, skip_retraining_fn=lambda action, observation: action == 0, cost_matrix=params.CostMatrix( fn=0, fp=-1, tp=env_params.interest_rate, tn=0)) agent = oracle_lending_agent.OracleThresholdAgent( action_space=env.action_space, reward_fn=rewards.BinarizedScalarDeltaReward( 'bank_cash', baseline=env.initial_params.bank_starting_cash), observation_space=env.observation_space, params=agent_params, env=env) agent.seed(100) return env, agent
def test_oracle_maxutil_classifier_is_stable(self): env = lending.DelayedImpactEnv() agent_params = classifier_agents.ScoringAgentParams( feature_keys=['applicant_features'], group_key='group', default_action_fn=(lambda: 1), burnin=1, threshold_policy=threshold_policies.ThresholdPolicy.SINGLE_THRESHOLD, convert_one_hot_to_integer=True, cost_matrix=params.CostMatrix( fn=0, fp=-1, tp=env.initial_params.interest_rate, tn=0)) agent = oracle_lending_agent.OracleThresholdAgent( action_space=env.action_space, observation_space=env.observation_space, reward_fn=rewards.BinarizedScalarDeltaReward('bank_cash'), params=agent_params, env=env) test_util.run_test_simulation(env=env, agent=agent) # Drop 0 threshold associated with burn-in. first_nonzero_threshold = None for thresh in agent.global_threshold_history: if thresh > 0: if first_nonzero_threshold is None: first_nonzero_threshold = thresh self.assertAlmostEqual(first_nonzero_threshold, thresh) # Make sure there is at least one non-zero threshold. self.assertIsNotNone(first_nonzero_threshold)
def test_final_credit_distribution_metric_can_interact_with_lending(self): env = lending.DelayedImpactEnv() env.set_scalar_reward(rewards.NullReward()) # Use step=-1 to get the final credit distribution. final_distribution = lending_metrics.CreditDistribution(env, step=-1) initial_distribution = lending_metrics.CreditDistribution(env, step=0) test_util.run_test_simulation( env=env, metric=[final_distribution, initial_distribution])
def test_cumulative_count(self): env = lending.DelayedImpactEnv() metric = lending_metrics.CumulativeLoans(env) env.seed(100) _ = env.reset() for _ in range(10): env.step(np.asarray(1)) result = metric.measure(env) self.assertEqual(result.shape, (2, 10)) # On the first step, the combined number of loans given out should be 1. self.assertEqual(result[:, 0].sum(), 1) # On the last step, the combined number of loans given out should be 10. self.assertEqual(result[:, -1].sum(), 10)
def test_no_loans_to_group_zero(self): env = lending.DelayedImpactEnv() metric = lending_metrics.CumulativeLoans(env) env.seed(100) obs = env.reset() for _ in range(10): # action is 0 for group 0 and 1 for group 1. action = np.argmax(obs['group']) obs, _, _, _ = env.step(action) result = metric.measure(env) self.assertEqual(result.shape, (2, 10)) # Group 0 gets no loans. self.assertEqual(result[0, -1], 0) # Group 1 gets at least 1 loan. self.assertGreater(result[1, -1], 0)
def test_higher_credit_scores_default_less(self): env = lending.DelayedImpactEnv() high_scores = [] low_scores = [] rng = np.random.RandomState() rng.seed(100) for _ in range(1000): applicant = env.initial_params.applicant_distribution.sample(rng) if np.argmax(applicant.features) > 4: high_scores.append(applicant) else: low_scores.append(applicant) self.assertNotEmpty(high_scores) self.assertNotEmpty(low_scores) self.assertLess( np.mean([applicant.will_default for applicant in high_scores]), np.mean([applicant.will_default for applicant in low_scores]))
def test_oracle_lending_agent_interacts(self): env = lending.DelayedImpactEnv() agent_params = classifier_agents.ScoringAgentParams( feature_keys=['applicant_features'], group_key='group', default_action_fn=(lambda: 1), burnin=1, convert_one_hot_to_integer=True, cost_matrix=params.CostMatrix( fn=0, fp=-1, tp=env.initial_params.interest_rate, tn=0)) agent = oracle_lending_agent.OracleThresholdAgent( action_space=env.action_space, observation_space=env.observation_space, reward_fn=rewards.BinarizedScalarDeltaReward('bank_cash'), params=agent_params, env=env) test_util.run_test_simulation(env=env, agent=agent)
def test_measure_distribution_change_measurement(self): # The lower cluster has a 100% success rate and the upper cluster has a 0% # success rate. This causes applicants to move constantly between clusters. clusters = distributions.Mixture( components=[ lending_params._credit_cluster_builder( group_membership=[1, 0], cluster_probs=[0.1, 0.9], success_probs=[1., 0.])(), lending_params._credit_cluster_builder( group_membership=[0, 1], cluster_probs=[0.8, 0.2], success_probs=[1., 0.])(), ], weights=(0.5, 0.5)) env = lending.DelayedImpactEnv( lending_params.DelayedImpactParams(applicant_distribution=clusters)) initial_distribution = lending_metrics.CreditDistribution(env, 0) final_distribution = lending_metrics.CreditDistribution(env, -1) # Giving a loan should change the distribution. env.step(np.asarray(1)) # Take another step to move current state into history. This step does not # change the distribution because the loan is rejected. env.step(np.asarray(0)) self.assertEqual({ '0': [0.1, 0.9], '1': [0.8, 0.2] }, initial_distribution.measure(env)) self.assertNotEqual({ '0': [0.1, 0.9], '1': [0.8, 0.2] }, final_distribution.measure(env))
def test_credit_score_distributions_change(self): env = lending.DelayedImpactEnv() state = env._get_state() # Candidate is in group 0 state.group = [1, 0] state.group_id = 0 # Candidate will default. state.will_default = 1 # Should move probability mass from clusters 3 to 2. state.applicant_features = [0] * 7 state.applicant_features[3] = 1 lending._CreditShift().update(state, lending.LoanDecision.ACCEPT) def get_cluster_probs(params): return params.applicant_distribution.components[0].weights self.assertLess( get_cluster_probs(state.params)[3], get_cluster_probs(env.initial_params)[3]) self.assertGreater( get_cluster_probs(state.params)[2], get_cluster_probs(env.initial_params)[2])
def test_metric_can_interact_with_lending(self): env = lending.DelayedImpactEnv() metric = value_tracking_metrics.ValueChange(env, 'bank_cash') test_util.run_test_simulation(env=env, metric=metric)
def test_delayed_impact_env_has_multinomial_observation_space(self): env = lending.DelayedImpactEnv() for _ in range(10): features = env.observation_space.sample()['applicant_features'] self.assertEqual(features.sum(), 1) self.assertSameElements(features, {0, 1})
def test_render_fails_for_high_dimensional_environments(self): env = lending.DelayedImpactEnv() test_util.run_test_simulation(env=env) with self.assertRaises(NotImplementedError): env.render()