This entry is part of a series dedicated to MIES – a miniature insurance economic simulator. The source code for the project is available on GitHub.
Current Status
Up until now, I’ve been able to demonstrate basic consumer behavior and certain market phenomena seen in the insurance industry. However, there’s been a major problem with MIES in that no money has actually changed hands in any of the simulations we’ve seen so far. Consumers have simply switched carriers depending on price, which allowed me to demonstrate adverse selection, but not much else. Consumption decisions involving insurance depend on wealth, but since there was no way to calculate wealth in MIES, its ability to model these decisions was limited.
The next view chapters in Varian place a heavy emphasis on wealth and risk tolerance, so this week, I made the decision to work on incorporating transactions before diving deeper into consumer behavior.
At first glance, transactions might seem like a simple thing to implement, after all, why not just keep a running cash balance for each entity, and then add and subtract payments as needed? The problem with this method is the same problem that leads companies to use double-entry accounting. Transactions are more complicated than simply sending money from one place to another. Loans are generated and capital is invested, which creates liabilities that must be considered when trying to calculate the wealth of an entity. Even something as simple as a premium payment is effectively a loan to an insurance company that needs to have a liability recorded (the unearned premium reserve), in addition to an increase in cash to the insurer.
Therefore, I’ve had to draw on my basic knowledge of accounting, which made me uneasy since I’m not an accountant myself. However, in order to get MIES to model the phenomena I want to model, and to answer the questions I have about insurance, I need to implement double-entry accounting, and eventually, statutory accounting rules. I ran across a post on Hacker News titled, ‘It’s OK for your open source library to be a bit shitty,’ which encouraged me to keep moving forward with the project despite the amount of discomfort I have.
I have certainly found many errors in MIES from past versions and there will likely be many more, including in this post. However, there’s not a lot of open source actuarial stuff out there, or in particular, open source actuarial simulations incorporating both economics concepts and double-entry accounting. Or, if there are packages out there, they aren’t easy to find. Thus, I’ve taken the step to put something out there, awaiting any feedback for things that need to fixed, and then making improvements. If this ever proves to be something useful, younger generations will create even better tools in the future.
The Bank Class
Banks facilitate transactions between insurers, brokers, customers, and other banks. While it may eventually be possible to define more than one bank per simulation, all the examples in the near future will have a single bank at which entities can deposit money and send payments to each other.
Schema
As with all the other entities, each bank comes with its own database:
Here, a bank can accept three types of customers. The customer table represents the customer superclass and references the person, insurer, and bank subclasses. Each customer can have an optional number of accounts. Any of these customers can send transactions to any other customer, including themself. Notice that the transaction table has two relationships to the account table, since the debit and credit account fields for each transaction both point to the account table.
The SQLAlchemy mapping is defined below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import Column, Integer, Date, Float, String from sqlalchemy import ForeignKey from sqlalchemy.orm import relationship Base = declarative_base() class Account(Base): __tablename__ = 'account' account_id = Column( Integer, primary_key=True ) customer_id = Column( Integer, ForeignKey('customer.customer_id') ) account_type = Column(String) transaction_debit = relationship( 'Transaction', primaryjoin='Transaction.debit_account == Account.account_id', back_populates='account_debit' ) transaction_credit = relationship( 'Transaction', primaryjoin='Transaction.credit_account == Account.account_id', back_populates='account_credit' ) def __repr__(self): return "<Account(" \ "customer_id='%s', " \ "account_type='%s', " \ ")>" % ( self.customer_id, self.account_type ) class Transaction(Base): __tablename__ = 'transaction' transaction_id = Column( Integer, primary_key=True ) debit_account = Column( Integer, ForeignKey('account.account_id') ) credit_account = Column( Integer, ForeignKey('account.account_id') ) transaction_date = Column(Date) transaction_amount = Column(Float) account_debit = relationship( "Account", primaryjoin='Transaction.debit_account == Account.account_id', back_populates='transaction_debit' ) account_credit = relationship( "Account", primaryjoin='Transaction.credit_account == Account.account_id', back_populates='transaction_credit' ) def __repr__(self): return "<Transaction(" \ "debit_account='%s', " \ "credit_account='%s', " \ "transaction_date='%s', " \ "transaction_amount='%s'" \ ")>" % ( self.debit_account, self.credit_account, self.transaction_date, self.transaction_amount, ) class Customer(Base): __tablename__ = 'customer' customer_id = Column( Integer, primary_key=True ) customer_type = Column(String) person = relationship( 'Person', primaryjoin='Customer.customer_id == Person.customer_id', back_populates='customer' ) insurer = relationship( 'Insurer', primaryjoin='Customer.customer_id == Insurer.customer_id', back_populates='customer' ) bank = relationship( 'Bank', primaryjoin='Customer.customer_id == Bank.customer_id', back_populates='customer' ) class Person(Base): __tablename__ = 'person' person_id = Column( Integer, primary_key=True ) customer_id = Column( Integer, ForeignKey('customer.customer_id') ) customer = relationship( 'Customer', primaryjoin='Person.customer_id == Customer.customer_id', back_populates='person', uselist=True ) class Insurer(Base): __tablename__ = 'insurer' insurer_id = Column( Integer, primary_key=True ) customer_id = Column( Integer, ForeignKey('customer.customer_id') ) customer = relationship( 'Customer', primaryjoin='Insurer.customer_id == Customer.customer_id', back_populates='insurer', uselist=True ) class Bank(Base): __tablename__ = 'bank' bank_id = Column( Integer, primary_key=True ) customer_id = Column( Integer, ForeignKey('customer.customer_id') ) customer = relationship( 'Customer', primaryjoin='Bank.customer_id == Customer.customer_id', back_populates='bank', uselist=True ) |
Bank Methods
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
import datetime as dt import os import pandas as pd import sqlalchemy as sa from sqlalchemy.orm import sessionmaker import mies.schema.bank as bank from mies.schema.bank import Account, Customer, Insurer, Person, Transaction from mies.schema.bank import Bank as BankTable from mies.utilities.connections import connect_universe from mies.utilities.queries import query_bank_id class Bank: def __init__(self, starting_capital, bank_name, path='db/banks/', date_established=dt.datetime(1, 12, 31)): if not os.path.exists(path): os.makedirs(path) self.engine = sa.create_engine( 'sqlite:///' + path + bank_name + '.db', echo=True ) session = sessionmaker(bind=self.engine) bank.Base.metadata.create_all(self.engine) self.session = session() self.connection = self.engine.connect() self.name = bank_name self.date_established = date_established self.id = self.__register() self.get_customers(self.id, 'bank') self.cash_account = self.assign_account( customer_id=self.id, account_type='cash' ) self.capital_account = self.assign_account(self.id, 'capital') self.liability_account = self.assign_account(self.id, 'liability') self.make_transaction( self.cash_account, self.capital_account, self.date_established, starting_capital ) def __register(self): # populate universe company record insurer_table = pd.DataFrame([[self.name]], columns=['bank_name']) session, connection = connect_universe() insurer_table.to_sql( 'bank', connection, index=False, if_exists='append' ) bank_id = query_bank_id(self.name) return bank_id def get_customers(self, ids, customer_type): new_customers = pd.DataFrame() new_customers[customer_type + '_id'] = pd.Series(ids) new_customers['customer_type'] = customer_type objects = [] for index, row in new_customers.iterrows(): if customer_type == 'person': customer_type_table = Person(person_id=row[customer_type + '_id']) elif customer_type == 'insurer': customer_type_table = Insurer(insurer_id=row[customer_type + '_id']) else: customer_type_table = BankTable(bank_id=row[customer_type + '_id']) customer = Customer( customer_type=customer_type ) customer_type_table.customer.append(customer) objects.append(customer_type_table) self.session.add_all(objects) self.session.commit() def assign_accounts(self, customer_ids, account_type): """ assign multiple accounts given customer ids """ new_accounts = pd.DataFrame() new_accounts['customer_id'] = customer_ids new_accounts['account_type'] = account_type new_accounts.to_sql( 'account', self.connection, index=False, if_exists='append' ) def assign_account(self, customer_id, account_type): """ assign a single account for a customer """ account = Account(customer_id=int(customer_id), account_type=account_type) self.session.add(account) self.session.commit() return account.account_id def make_transaction(self, debit_account, credit_account, transaction_date, transaction_amount): """ make a single transaction """ transaction = Transaction( debit_account=int(debit_account), credit_account=int(credit_account), transaction_date=transaction_date, transaction_amount=transaction_amount ) self.session.add(transaction) self.session.commit() return transaction.transaction_id def make_transactions(self, data: pd.DataFrame): """ accepts a DataFrame to make multiple transactions need debit, credit, transaction date, transaction amount """ data['debit_account'] = data['debit_account'].astype(int) data['credit_account'] = data['credit_account'].astype(int) data.to_sql( 'transaction', self.connection, index=False, if_exists='append' ) |
Upon initialization, a bank will register itself as a customer, this allows it to have its own accounts, as well as to accept deposits from other customers. The reason why it needs to have its own accounts is because each transaction will need to have corresponding debit and credit accounts, and transactions between a bank and its customers will sometimes have one of the bank’s own accounts involved.
The method bank.get_customers() takes a list of IDs, which can be either those of people, insurers, or banks. For each of these entity IDs, the bank will create its own identifier for the customer, which can differ between that customer’s underlying ID. For example, a person with a person ID of 1, and an insurer with an insurer ID of 1, will have separate customer ids.
The method bank.assign_accounts() takes a list of customer IDs, a type of account (such as cash), and then creates that type of account for each customer.
The method bank.make_transactions() will take a list of transactions, each of which have a debit account, credit account, transaction date, and transaction volume defined, and then store them in the transactions table.
Now that we have our bank defined, I’ll walk through the insurance underwriting/claim cycle and show how the transactions that are currently available in MIES work. First, we’ll import the necessary modules, create an environment, make a population of 1000 people, and then create a bank called ‘blargo’ that has 4B in starting capital:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import datetime as dt from mies.entities.bank import Bank from mies.entities.broker import Broker from mies.entities.god import God from mies.entities.insurer import Insurer from mies.utilities.queries import query_population from utilities.queries import query_customers_by_person_id ahura = God() ahura.make_population(1000) blargo = Bank(4000000, 'blargo') |
Starting Wealth
Before we can issue policies to customers, we need to have a starting amount of wealth for each person so that they can actually pay for their policies. Furthermore, we also need each person to have a bank account from which they can issue payments to their insurers. To take care of these two steps, we’ll first send the person IDs to the bank, which will then create corresponding customer IDs and then establish one cash account for each customer:
1 2 3 |
blargo.get_customers(ids=ids, customer_type='person') customer_ids = query_customers_by_person_id(ids, 'blargo') blargo.assign_accounts(customer_ids=customer_ids, account_type='cash') |
This action populates two tables. The person table contains a record of all 1000 people, and the customer table has 1001 records, since each person becomes a customer, but the bank itself is already its own customer:
In the next step, we’ll use a new method called grant_wealth() which the environment uses to give each person a starting amount of wealth. Since wealth is not evenly distributed in society, I’ve drawn these values from the Pareto distribution:
1 |
ahura.grant_wealth(person_ids=ids, bank=blargo, transaction_date=pricing_date) |
The method is defined as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class God: ... def grant_wealth( self, person_ids, bank: Bank, transaction_date ): """ assign an initial amount of starting wealth per person """ accounts = query_accounts_by_person_id( person_ids, bank.name, 'cash' ) accounts['transaction_amount'] = pareto.rvs( b=1, scale=pm.person_params['income'], size=accounts.shape[0], ) accounts = accounts[[ 'account_id', 'transaction_amount' ]] accounts = accounts.rename(columns={ 'account_id': 'debit_account' }) accounts['credit_account'] = bank.liability_account accounts['transaction_date'] = transaction_date bank.make_transactions(accounts) ... |
This action deposits wealth in each person’s cash account. This is marked as the debit side of the transaction, the credit side is the liability that the bank takes on by accepting the deposits, since deposits are loans to the bank:
Policy Inception
Before we issue policies, we need to create two more entities in the simulation. One, a broker called ‘rayon’ and an insurer called ‘company_1.’ Initializing a company now requires two new arguments, a bank with which it associates, and its inception date. These new arguments are used to create bank accounts for the insurer:
1 2 3 4 5 6 7 8 9 |
rayon = Broker() company_1 = Insurer(4000000, blargo, pricing_date, 'company_1') company_1_formula = 'incurred_loss ~ ' \ 'age_class + ' \ 'profession + ' \ 'health_status + ' \ 'education_level' |
We’ll now use the broker rayon to place the customers with the insurer. This method also has a bank as a new argument, which will be used to facilitate the transactions:
1 2 3 4 5 |
rayon.place_business( pricing_date, blargo, company_1 ) |
This action creates a policy record for each insurer, which I won’t show here since I’ve already done so in a previous post. What has changed however, is that each person needs to pay the premium to the insurer up front. The broker is able to tell the bank to create a transaction for each premium payment. This time, the debit account is the insurer’s cash account, and the credit account is the person’s cash account. In the picture below, we see that there are 1000 additional transactions starting with transaction_id 1003 (1002 was used to seed insurer capital). The debit account on all these transactions is account ID 1004, which is the insurer’s cash account:
In reality, a corresponding liability should also be created in the insurer’s accounting system. This is called the unearned premium reserve which is what the insured is entitled to recieve if their policy gets canceled. This feature is not yet implemented in MIES, but it’s an important liability to consider in insurance.
Loss Occurrence
Now that we have our policies issued, we’re ready to simulate some losses:
1 2 3 |
event_date = pricing_date + dt.timedelta(days=1) ahura.smite(event_date) |
This action produced 57 loss events, which can be found in the events table in the universe database:
Claim Reporting
These losses are not considered claims until they are reported to the insurer. Otherwise, the insurer has no knowledge that they occurred:
1 |
rayon.report_claims(event_date) |
This method has changed from last week. The loss amounts are now reported as case reserves, which are estimates made by an insurer on how much they will need to pay for the claim. This is now distinguished from paid losses, which are the actual payments the insurer makes to the insured:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
class Broker: ... def report_claims(self, report_date): # match events to policies in which they are covered events = query_events_by_report_date(report_date) policies = query_all_policies() claims = events.merge( policies, on='person_id', how='left' ) claims = claims.query( 'event_date >= effective_date ' 'and event_date <= expiration_date' ) claims = claims.drop([ 'effective_date', 'expiration_date', 'premium', 'company_id' ], axis=1) companies = get_company_names() for company in companies: # register claims by id reported_claims = claims[claims['company_name'] == company] reported_claims = reported_claims.rename(columns={ 'event_date': 'occurrence_date' }) reported_claims = reported_claims.drop(['company_name'], axis=1) session, connection = connect_company(company) objects = [] for index, row in reported_claims.iterrows(): claim = Claim( policy_id=row['policy_id'], person_id=row['person_id'], event_id=row['event_id'], occurrence_date=row['occurrence_date'], report_date=row['report_date'] ) open_claim = ClaimTransaction( transaction_date=row['report_date'], transaction_type='open claim', transaction_amount=0 ) case_reserve = ClaimTransaction( transaction_date=row['report_date'], transaction_type='set case reserve', transaction_amount=row['ground_up_loss'] ) claim.claim_transaction.append(open_claim) claim.claim_transaction.append(case_reserve) objects.append(claim) session.add_all(objects) session.commit() connection.close() |
This action creates two transactions for each claim. One transaction, called ‘open claim’ signals that a claim has been created. Another transaction, called ‘set case reserve,’ sets a case reserve for each claim. Since there are 57 losses, there are 57 claims, and 114 transactions:
Notice that we have some fairly restrictive assumptions for the simulation. The case reserves are equal to the ground up losses, and all 57 losses are reported to and known by the insurer immediately. This is not the case in the real world, where an insurer does not know how much claims will cost until they are settled, and may not know about claims until many years after they have occurred. We’ll need to revisit this problem in the future, since estimating claim amounts, including those on unreported claims, is a core function of actuarial science.
Claim Settlement
Let’s close these claims by issuing payments:
1 |
company_1.pay_claims(event_date + dt.timedelta(days=1)) |
Insurer.pay_claims() is a new method used to send checks to the insureds for indemnification:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
def pay_claims(self, transaction_date): # send checks to bank case_reserves = query_open_case_reserves(self.company_name) accounts_to_pay = query_accounts_by_person_id( case_reserves['person_id'], self.bank.name, 'cash' ) case_reserves = case_reserves.merge( accounts_to_pay, on='person_id', how='left' ) case_reserves['transaction_date'] = transaction_date case_reserves = case_reserves.rename(columns={ 'case reserve': 'transaction_amount', 'account_id': 'debit_account' }) case_reserves['credit_account'] = self.cash_account payments = case_reserves[[ 'debit_account', 'credit_account', 'transaction_date', 'transaction_amount']].copy() self.bank.make_transactions(payments) # people then use checks to pay their for their own losses payments['credit_account'] = payments['debit_account'] payments['debit_account'] = self.bank.liability_account self.bank.make_transactions(payments) # reduce case reserves objects = [] for index, row in case_reserves.iterrows(): reserve_takedown = ClaimTransaction( claim_id = row['claim_id'], transaction_date=row['transaction_date'], transaction_type='reduce case reserve', transaction_amount=row['transaction_amount'] ) claim_payment = ClaimTransaction( claim_id=row['claim_id'], transaction_date=row['transaction_date'], transaction_type='claim payment', transaction_amount=row['transaction_amount'] ) close_claim = ClaimTransaction( claim_id=row['claim_id'], transaction_date=row['transaction_date'], transaction_type='close claim', transaction_amount=0 ) objects.append(reserve_takedown) objects.append(claim_payment) objects.append(close_claim) self.session.add_all(objects) self.session.commit() |
There’s a lot going on here, so I’ll break it down. The payments to the insureds are handled first. We can see this by going to the transactions table in the bank database:
Notice that there are 114 additional transactions, two for each of the 57 claims. One transaction sends a payment from the insurer to the customer. You can see this since the debit account is the cash account of the customer, and the credit accont (1004) is the cash account of the insurer. The other transaction is a payment from the insured to whomever they owe money to due to the loss. The debit side of the transaction is now a reduction in the bank’s liability account, and the credit amount is a reduction in cash equal to the claim amount for the insured:
Next, three transactions are entered for each claim into the insurer’s database:
- Reduce case reserve
- Claim payment
- Close claim
‘Reduce case reserve’ is a reduction in the claims reserve to zero, signifying that the insurer no longer owes money to the insured. The ‘claim payment’ is a corresponding transaction representing the actual payment, and ‘close claim’ is a transaction that indicates that the claim is now closed. The insurer now has 5 claims transactions for each claim, 57 x 5 = 285 transactions in total. These five transactions are: 1) open claim, 2) set case reserve, 3) reduce case reserve 4) claim payment, and 5) close claim.
One source of confusion I had is that this way of recording transactions does not have the double-entry accounting that you’d see in a general ledger. Indeed, this form is more common for actuarial pricing modelers who do not usually need to get into the finer details of debits and credits. However, I’m leaning towards changing the claims transaction tables to also be double-entry, since doing so also makes things easier to program and avoids questions with negative values. For example, I had to think about whether to record case reserve reductions as a positive or negative value. This confusion is not present if I record them as credit and debit amounts.
Consumer Income
Each person now gets a paycheck during each period. This can be issued by the environment class:
1 |
ahura.send_paychecks(person_ids=ids, bank=blargo, transaction_date=event_date + dt.timedelta(days=1)) |
There’s nothing too special about this method, it just debits each person’s cash account by their income amount:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class God: ... def send_paychecks(self, person_ids, bank: Bank, transaction_date): incomes = query_incomes(person_ids) incomes.columns = ['person_id', 'transaction_amount'] accounts = query_accounts_by_person_id( person_ids, bank.name, 'cash' ) accounts = accounts.merge(incomes, on='person_id', how='left') accounts = accounts.rename(columns={ 'account_id': 'debit_account' }) accounts['credit_account'] = bank.liability_account accounts['transaction_date'] = transaction_date accounts = accounts.drop([ 'person_id', 'customer_id' ], axis=1) bank.make_transactions(accounts) |
An additional 1000 transactions have been recorded, one for each customer. As with wealth, the debit side of the transaction is the person’s cash account, with a corresponding credit to the bank’s liability account:
Policy Pricing
Now that the claims have been reported and settled, the insurer can use this information to recalibrate the premium for each customer. However, unlike last week, claim amounts are not stored as a single column called ‘incurred_loss’, but now must be calculated from the transaction amounts. To handle this, I created a set of queries that can be used to return the case reserves, paid losses, and incurred amounts for each claim:
1 2 3 4 |
from utilities.queries import query_case_by_claim from utilities.queries import query_paid_by_claim from utilities.queries import query_incurred_by_claim from utilities.queries import query_pricing_model_data |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
def query_case_by_claim(company_name): session, connection = connect_company(company_name) claim_policy = session.query( Claim.claim_id, Claim.policy_id ).subquery() case_set = session.query( ClaimTransaction.claim_id, func.sum(ClaimTransaction.transaction_amount).label('set') ).filter(ClaimTransaction.transaction_type == 'set case reserve').group_by(ClaimTransaction.claim_id).subquery() case_takedown = session.query( ClaimTransaction.claim_id, func.sum(ClaimTransaction.transaction_amount).label('takedown') ).filter(ClaimTransaction.transaction_type == 'reduce case reserve').group_by(ClaimTransaction.claim_id).subquery() case_query = session.query( case_set.c.claim_id, (func.ifnull(case_set.c.set, 0) - func.ifnull(case_takedown.c.takedown, 0)).label('case_reserve') ).outerjoin(case_takedown, case_set.c.claim_id == case_takedown.c.claim_id).subquery() claim_case = session.query( claim_policy.c.claim_id, claim_policy.c.policy_id, func.ifnull(case_query.c.case_reserve, 0).label('case_reserve') ).outerjoin(case_query, claim_policy.c.claim_id == case_query.c.claim_id).statement case_reserve = pd.read_sql(claim_case, connection) connection.close() return case_reserve def query_paid_by_claim(company_name): session, connection = connect_company(company_name) claim_policy = session.query( Claim.claim_id, Claim.policy_id ).subquery() payment_query = session.query( ClaimTransaction.claim_id, func.sum(ClaimTransaction.transaction_amount).label('paid_loss') ).filter(ClaimTransaction.transaction_type == 'claim payment').group_by(ClaimTransaction.claim_id).subquery() claim_paid = session.query( claim_policy.c.claim_id, claim_policy.c.policy_id, func.ifnull(payment_query.c.paid_loss, 0).label('paid_loss') ).outerjoin(payment_query, claim_policy.c.claim_id == payment_query.c.claim_id).statement claim_payments = pd.read_sql(claim_paid, connection) connection.close() return claim_payments def query_incurred_by_claim(company_name): case = query_case_by_claim(company_name) case = case.drop(columns=['policy_id'], axis=1) paid = query_paid_by_claim(company_name) incurred = paid.merge(case, on='claim_id', how='left') incurred['incurred_loss'] = incurred['paid_loss'] + incurred['case_reserve'] return incurred def query_pricing_model_data(company_name): session, connection = connect_company(company_name) policy_query = session.query( Policy.policy_id, Policy.person_id, Customer.age_class, Customer.profession, Customer.health_status, Customer.education_level, ).outerjoin( Customer, Policy.person_id == Customer.person_id ).statement policy = pd.read_sql(policy_query, connection) claim = query_incurred_by_claim(company_name) claim = claim.drop(columns=['claim_id', 'paid_loss', 'case_reserve'], axis=1) claim = claim.groupby(['policy_id'])['incurred_loss'].agg('sum') model_set = policy.merge(claim, on='policy_id', how='left') model_set['incurred_loss'] = model_set['incurred_loss'].fillna(0) return model_set |
We mostly need to be concerned about the last one of these, query_pricing_model_data() which uses the first three to combine policy and claim information together, which can be priced with a GLM:
1 |
query_pricing_model_data('company_1') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
Out[15]: policy_id person_id age_class profession health_status education_level \ 0 1 1 E A P H 1 2 2 E B F P 2 3 3 Y C F H 3 4 4 M C F H 4 5 5 M A P P .. ... ... ... ... ... ... 995 996 996 M B P H 996 997 997 E A P H 997 998 998 E B G H 998 999 999 M A G P 999 1000 1000 M C G H incurred_loss 0 0.0 1 0.0 2 0.0 3 0.0 4 0.0 .. ... 995 0.0 996 0.0 997 0.0 998 0.0 999 0.0 [1000 rows x 7 columns] |
Since most people don’t have a claim, most incurred loss amounts are zero. This data set can then be used by the insurer to reprice with a GLM, and the new pricing algorithm is used by the broker to quote and place business:
1 2 3 4 5 6 7 |
company_1.price_book(company_1_formula) rayon.place_business( pricing_date, blargo, company_1 ) |
Further Improvements
Now that I’ve got transactions modeled, I can calculate the wealth for each entity in the simulation. This is key piece required to make further changes to the way consumer preferences work in MIES, which will soon incorporate risk tolerance and wealth.