Outputting Financial Performance#
This notebook is a continuation of template_example.py and demonstrates how the financial performance report is composed using the IFRS variables calculated by running template.py. The output of the financial performance table in this notebook matches the figures shown in Systemorph’s video.
The financial performance report provides a breakdown of the total comprehensive income, as shown below. The main task in this notebook is to map various changes in balance sheet items and incurred cash flows to categories such as Insurance Revenue (IR), Insurance Service Expense (ISE), Insurance Financial Income/Expense (IFIE) or Other Comprehensive Income (OCI) within the breakdown.
Total Comprehensive Income
Profit and Loss
Insurance Service Result
Insurance Revenue (
IR)Insurance Service Expense (
ISE)
Insurance Financial Income/Expense (
IFIE)
Other Comprehensive Income (
OCI)
Each item is futhur broken down as shown in the table below.
Click the badge below to run this notebook online on Google Colab. You need a Google account and need to be logged in to it to run this notebook on Google Colab.
The next code cell below is relevant only when you run this notebook on Google Colab. It installs lifelib and creates a copy of the library for this notebook.
[ ]:
import sys, os
if 'google.colab' in sys.modules:
lib = 'ifrs17a'; lib_dir = '/content/'+ lib
if not os.path.exists(lib_dir):
!pip install lifelib
import lifelib; lifelib.create(lib, lib_dir)
%cd $lib_dir
[1]:
import pandas as pd
from template import ifrsvars, workspace, AocType, EstimateType, PnlVariableType
[2]:
pnl = workspace.database.Query(PnlVariableType, as_df=True)
pnl.loc[pnl['Parent'].isin(['IR', 'ISE', 'IFIE', 'OCI'])][
['Parent', 'SystemName', 'DisplayName']].set_index('Parent')
[2]:
| SystemName | DisplayName | |
|---|---|---|
| Parent | ||
| IR | IR1 | Premiums |
| IR | IR2 | Exc. Investment Components |
| IR | IR3 | CSM Amortization |
| IR | IR4 | Acquistion Expenses Amortization |
| IR | IR5 | Non-Financial LRC Changes (Exc. CSM Amortization) |
| IR | IR6 | Exc. Experience Adjustment on Premiums |
| ISE | ISE1 | Reinsurance Premiums |
| ISE | ISE2 | Claims |
| ISE | ISE3 | Expenses |
| ISE | ISE4 | Commissions |
| ISE | ISE5 | Exc. Investment Components |
| ISE | ISE6 | Acquisition Expenses |
| ISE | ISE7 | Reinsurance CSM Amortization |
| ISE | ISE8 | LoReCo Release |
| ISE | ISE9 | Loss Component Release |
| ISE | ISE10 | Non-Financial Reinsurance LRC Changes (Exc. LC... |
| ISE | ISE11 | Loss Component / LoReCo Changes (Exc. Releases) |
| ISE | ISE12 | Non Financial LIC Changes |
| IFIE | IFIE1 | Financial LRC Changes |
| IFIE | IFIE2 | Financial LIC Changes |
| IFIE | IFIE3 | FX Changes |
| OCI | OCI1 | Financial LRC Changes |
| OCI | OCI2 | Financial LIC Changes |
| OCI | OCI3 | FX Changes |
Processing Raw Data#
When the template module is run, all the IFRS variables become available as a DataFrame named ifrsvars. The all_data variable is then defined by filtering ifrsvars to only include the variables from the first quarter of 2021. deltas is defined to specifically include only those variables that represent increases or decreases in amounts.
[3]:
all_data = ifrsvars.loc[(ifrsvars['Year'] == 2021) & (ifrsvars['Month'] == 3)]
deltas = all_data.loc[((all_data['AocType'] != 'BOP') & (all_data['AocType'] != 'EOP'))
| ((all_data['AocType'] == 'BOP') & (all_data['Novelty'] == 'N'))]
Fulfillment Cashflows (excluding the Loss Component)#
The fulfillment cashflows comprise the best estimate liabilities (BE) and the risk adjustment for non-financial risk (RA).
be_or_ra is a condition to filter deltas and extract variables that represent changes in BE and RA.
Changes in the fulfillment cashflows due to the difference between current discount rates and initial(lock-in) discount rates are recognized either in Profit and Loss or in Other Comprehensive Income depending on whether the Valuation Approach is BBA and the OCI option is selected.
fcf extracts the changes in the fulfillment cashflows that need to be measured using the lock-in discount rates, and also the changes that need to be measured using the current discount rates. current_fcf on the other hand extracts the changes using the current discount rates, regardless of the values of the valuation approaches and OCI options. The difference between fcf and current_fcf are the amounts to be recognized in OCI.
[4]:
be_or_ra = (deltas['EstimateType'] == 'BE') | (deltas['EstimateType'] == 'RA')
as_lockin = (deltas['ValuationApproach'] == 'BBA') & deltas['OciType']
lockin = as_lockin & (deltas['EconomicBasis'] == 'L')
current = ~as_lockin & (deltas['EconomicBasis'] == 'C')
fcf = deltas.loc[be_or_ra & (lockin | current)]
[5]:
current_fcf = deltas.loc[be_or_ra & (deltas['EconomicBasis'] == 'C')]
Non-Financial Component#
The non-financial component in the changes in the fulfillment cashflows is recognized as either Insurance Revenue (IR) or Insurance Service Result (ISR) in the Insurance Service Result section under Profit and Loss.
The changes are identified by the values in the AocType column.
Variables in fcf are split into non_fin_fcf and fin_fcf by looking at their AocType values.
[6]:
fin_fcf_aocs = fcf['AocType'].isin(['IA', 'YCU', 'CRU', 'FX'])
non_fin_fcf_aocs = ~fin_fcf_aocs
non_fin_fcf = fcf.loc[non_fin_fcf_aocs]
non_fin_pnl_vars is a table for mapping the non-financial changes in the fulfillment cashflows to IR and ISE items.
[7]:
non_fin_pnl_vars = pd.DataFrame.from_records(
[['LRC', False, 'IR5'],
['LRC', True, 'ISE10'],
['LIC', False, 'ISE12'],
['LIC', True, 'ISE12']],
columns=['LiabilityType', 'IsReinsurance', 'PnlVariableType']
)
non_fin_pnl_vars
[7]:
| LiabilityType | IsReinsurance | PnlVariableType | |
|---|---|---|---|
| 0 | LRC | False | IR5 |
| 1 | LRC | True | ISE10 |
| 2 | LIC | False | ISE12 |
| 3 | LIC | True | ISE12 |
The code below adds the PnlVariableType column to non_fin_cf by looking up the non_fin_pnl_vars table, then flip the sign of the values so that the decreases in the fulfillment cashflows become positive and vice versa.
[8]:
non_fin_fcf = pd.merge(non_fin_fcf, non_fin_pnl_vars, how='left', on=['LiabilityType', 'IsReinsurance'], sort=False)
non_fin_fcf['Value'] = -1 * non_fin_fcf['Value']
Financial Component#
The non-financial component in the changes in the fulfillment cashflows fin_pnl_vars is a mapping table for the financial component.
[9]:
fin_pnl_vars = pd.DataFrame.from_records(
[['LRC', 'IFIE1'],
['LIC', 'IFIE2']],
columns=['LiabilityType', 'PnlVariableType']
)
fin_pnl_vars
[9]:
| LiabilityType | PnlVariableType | |
|---|---|---|
| 0 | LRC | IFIE1 |
| 1 | LIC | IFIE2 |
fin_fcf is defined by extracting variables that have financial AoC types. The code below adds the PnlVariableType column to fin_fcf by looking up the fin_pnl_vars table, then flip the sign of the values so that the decreases in the fulfillment cashflows become positive and vice versa.
[10]:
fin_fcf = fcf.loc[fin_fcf_aocs]
fin_fcf = pd.merge(fin_fcf, fin_pnl_vars, how='left', on='LiabilityType', sort=False)
fin_fcf['Value'] = -1 * fin_fcf['Value']
OCI Component#
oci_vars is a mapping table for the OCI component in the fulfillment cashflows.
[11]:
oci_vars = pd.DataFrame.from_records(
[['LRC', 'OCI1'],
['LIC', 'OCI2']],
columns=['LiabilityType', 'PnlVariableType']
)
oci_vars
[11]:
| LiabilityType | PnlVariableType | |
|---|---|---|
| 0 | LRC | OCI1 |
| 1 | LIC | OCI2 |
For the groups of contracts with the OCI option, the changes in the fulfillment cashflows due to the difference between current discount rates and initial(lock-in) discount rates are recognized in Other Comprehensive Income.
The code below captures the difference. current_fcf is copied as current_fcf_n and the sings of the values are flipped in current_fcf_n. oci_fin below is created by concatinating current_fcf_n and fcf. PnlVariableType column is added to oci_fin by looking up the oci_vars table.
[12]:
current_fcf_n = current_fcf.copy()
current_fcf_n['Value'] = -1 * current_fcf['Value']
oci_fin = pd.merge(pd.concat([fcf, current_fcf_n]), oci_vars, how='left', on='LiabilityType', sort=False)
Change in Fulfillment Cashflows#
The last step in processing the changes in fulfillment cashflows is to concatinate the 3 components, non_fin_fcf, fin_fcf and oci_fin.
[13]:
fcf_chg_in_est = pd.concat([non_fin_fcf, fin_fcf, oci_fin])
Contractual Service Margin (CSM)#
CSM changes can be extracted from deltas by filtering the EstimateType column for rows marked as C.
[14]:
csm = deltas.loc[deltas['EstimateType'] == 'C']
To map the IFRS variables, which indicate CSM changes, to the Financial Performance items, we first map the AocType values to PnlType using the csm_type table. The PnlType values can be either NF, F, AM, or FX, where NF represents non-financial, F represents financial, AM stands for amortization, and FX means foreign exchange. The changes are then mapped to the Financial Performance items using the csm_vars table based on the PnlType values
and the values in the IsReinsurance column.
Lastly, the signs of the variable values are inverted to ensure decreases in CSM are represented as positive and increases are represented as negative.
[15]:
aoc_to_pnl_type = workspace.database.Query(AocType, as_df=True).rename(
columns={'SystemName': 'AocType'})[['AocType', 'PnlType']]
aoc_to_pnl_type
[15]:
| AocType | PnlType | |
|---|---|---|
| 0 | BOP | NF |
| 1 | MC | NF |
| 2 | PC | NF |
| 3 | RCU | NF |
| 4 | CF | NF |
| 5 | IA | F |
| 6 | AU | NF |
| 7 | FAU | F |
| 8 | YCU | F |
| 9 | CRU | F |
| 10 | EV | NF |
| 11 | WO | NF |
| 12 | CL | NF |
| 13 | EA | NF |
| 14 | AM | AM |
| 15 | FX | FX |
| 16 | EOP | None |
[16]:
csm_vars = pd.DataFrame.from_records(
[['NF', False, 'IR5'],
['NF', True, 'ISE10'],
['F', False, 'IFIE1'],
['F', True, 'IFIE1'],
['AM', False, 'IR3'],
['AM', True, 'ISE7'],
['FX', False, 'IFIE3'],
['FX', True, 'IFIE3']],
columns=['PnlType', 'IsReinsurance', 'PnlVariableType']
)
csm_vars
[16]:
| PnlType | IsReinsurance | PnlVariableType | |
|---|---|---|---|
| 0 | NF | False | IR5 |
| 1 | NF | True | ISE10 |
| 2 | F | False | IFIE1 |
| 3 | F | True | IFIE1 |
| 4 | AM | False | IR3 |
| 5 | AM | True | ISE7 |
| 6 | FX | False | IFIE3 |
| 7 | FX | True | IFIE3 |
[17]:
csm_chg_in_est = pd.merge(
pd.merge(csm, aoc_to_pnl_type, how='left', on='AocType', sort=False),
csm_vars, on=['PnlType', 'IsReinsurance'], sort=False)
csm_chg_in_est['Value'] = -1 * csm_chg_in_est['Value']
Loss Component (LC)#
The changes in Loss Component are mapped by the same steps as CSM:
Fitering
deltasbydeltas['EstimateType'] == 'L'Mapping AocType to PnlType
Mapping PnlType to PnlVariableType
[18]:
lc = deltas.loc[deltas['EstimateType'] == 'L']
lc_vars = pd.DataFrame.from_records(
[['NF','ISE11'],
['F', 'IFIE1'],
['AM','ISE9'],
['FX','IFIE3']],
columns=['PnlType', 'PnlVariableType']
)
lc_vars
[18]:
| PnlType | PnlVariableType | |
|---|---|---|
| 0 | NF | ISE11 |
| 1 | F | IFIE1 |
| 2 | AM | ISE9 |
| 3 | FX | IFIE3 |
[19]:
lc_chg_in_est = pd.merge(
pd.merge(lc, aoc_to_pnl_type, how='left', on='AocType', sort=False),
lc_vars, on='PnlType', sort=False)
lc_chg_in_est['Value'] = -1 * lc_chg_in_est['Value']
Loss Recovery Component (LoReCo)#
The changes in Loss Recovery Component are mapped by the same steps as CSM and Locc Component:
Fitering
deltasbydeltas['EstimateType'] == 'LR'Mapping AocType to PnlType
Mapping PnlType to PnlVariableType
Inverting the values
[20]:
lrc = deltas.loc[deltas['EstimateType'] == 'LR']
lrc_vars = pd.DataFrame.from_records(
[['NF','ISE11'],
['F', 'IFIE1'],
['AM','ISE8'],
['FX','IFIE3']],
columns=['PnlType', 'PnlVariableType']
)
lrc_vars
[20]:
| PnlType | PnlVariableType | |
|---|---|---|
| 0 | NF | ISE11 |
| 1 | F | IFIE1 |
| 2 | AM | ISE8 |
| 3 | FX | IFIE3 |
[21]:
lrc_chg_in_est = pd.merge(pd.merge(lrc, aoc_to_pnl_type, how='left', on='AocType', sort=False), lrc_vars, on='PnlType', sort=False)
lrc_chg_in_est['Value'] = -1 * lrc_chg_in_est['Value']
Incurred Actuals#
Next, variables of actual cashflows are processed according to the following steps:
Variables for incurred cashflows are extracted from
deltasby looking at their EstimateType and AocTypePremium cashflows are extracted as
premiumsand mapped to eitherIRorISENon-investment component claims are extracted as
claims_nicand mapped to Insurance Service ExpenseInvestment-component claims are extracted as
claims_ico_irandclaims_ico_ise, and are then recognized in theIRandISErespectively. The values inclaims_ico_iseare inverted so that they negate each other.expenses,commissionsare also defined and mapped to respective Insurance Service Expense items.incurred_actualsis defined by concatinatingpremiums,claims_nic,claims_ico_ir,claims_ico_ise,expenses, andcommissions.
[22]:
written_cf = (deltas['EstimateType'] == 'A') & (deltas['AocType']=='CF')
advanced_wo = (deltas['EstimateType'] == 'AA') & (deltas['AocType']=='WO')
overdue_wo = (deltas['EstimateType'] == 'OA') & (deltas['AocType']=='WO')
actuals_data = deltas.loc[written_cf | advanced_wo | overdue_wo]
actuals_data.loc[actuals_data['AocType'] == 'WO', 'Value'] *= -1
[23]:
prem_vars = pd.DataFrame.from_records(
[[False,'IR1'],
[True, 'ISE1']],
columns=['IsReinsurance', 'PnlVariableType']
)
prem_vars
[23]:
| IsReinsurance | PnlVariableType | |
|---|---|---|
| 0 | False | IR1 |
| 1 | True | ISE1 |
[24]:
premiums = pd.merge(actuals_data.loc[
(actuals_data['AmountType']=='PR')], prem_vars, how='left', on='IsReinsurance', sort=False)
[25]:
claims_nic = actuals_data.loc[(actuals_data['AmountType']=='NIC')].copy()
claims_nic['PnlVariableType'] = 'ISE2'
[26]:
claims_ico_ir = actuals_data.loc[(actuals_data['AmountType']=='ICO')].copy()
claims_ico_ise = claims_ico_ir.copy()
claims_ico_ir['PnlVariableType'] = 'IR2'
claims_ico_ise['PnlVariableType'] = 'ISE5'
claims_ico_ise['Value'] = -1 * claims_ico_ise['Value']
[27]:
expenses = actuals_data.loc[
(actuals_data['AmountType']=='AEA') | (actuals_data['AmountType']=='AEM')
].copy()
expenses['PnlVariableType'] = 'ISE3'
[28]:
commissions = actuals_data.loc[
(actuals_data['AmountType']=='ACA') | (actuals_data['AmountType']=='ACM')
].copy()
commissions['PnlVariableType'] = 'ISE4'
[29]:
incurred_actuals = pd.concat([
premiums, claims_nic, claims_ico_ir, claims_ico_ise, expenses, commissions])
Incurred Deferrals and Acquisition Expenses#
incurred_deferrals include variables related to acquisition expense amortization. Amortized acquisition expenses are shown both in Insurance Revenue and Insurance Service Expense.
[30]:
amort = deltas.loc[(deltas['EstimateType'] == 'DA') & (deltas['AocType'] == 'AM')]
amort_ir = amort.copy()
amort_ise = amort.copy()
amort_ir['PnlVariableType'] = 'IR4'
amort_ise['PnlVariableType'] = 'ISE6'
amort_ise['Value'] = -1 * amort_ise['Value']
incurred_deferrals = pd.concat([amort_ir, amort_ise])
Experience Adjustment on Premium (allocation to CSM vs P&L recognition)#
Experience adjustments related to premium receipts for current and past periods should be included in insurance revenue. The following code creates exp_adjust_prem, which adjust Insurance Revenue for the experience adjustment on premiums.
[31]:
act_prem_csm = all_data.loc[all_data['EstimateType'] == 'APA'].copy()
act_prem_csm['Value'] = -1 * act_prem_csm['Value']
act_prem_csm['PnlVariableType'] = 'IR6'
est_prem_csm = deltas.loc[deltas['EstimateType'] == 'BEPA'].copy()
est_prem_csm['PnlVariableType'] = 'IR6'
exp_adjust_prem = pd.concat([act_prem_csm, est_prem_csm])
Financial Performance#
Finally, we create the financial_performance table by concatenating all the previously created tables. We then manipulate this combined table through grouping, summing, and reshaping operations to arrive at the final form of the financial performance table.
[32]:
financial_performance = pd.concat([
fcf_chg_in_est,
csm_chg_in_est,
lc_chg_in_est,
lrc_chg_in_est,
incurred_actuals,
incurred_deferrals,
exp_adjust_prem])
financial_performance[
['Value', 'LiabilityType', 'PnlVariableType']
].groupby(
['LiabilityType', 'PnlVariableType']).sum().unstack(level=[0]).fillna(0)
[32]:
| Value | ||
|---|---|---|
| LiabilityType | LIC | LRC |
| PnlVariableType | ||
| IFIE1 | 0.000000 | -0.250282 |
| IFIE2 | -1.240756 | 0.000000 |
| IR1 | 0.000000 | 613.000000 |
| IR2 | 0.000000 | -42.000000 |
| IR3 | 0.000000 | 63.519146 |
| IR4 | 0.000000 | 59.158616 |
| IR5 | 0.000000 | 38.096408 |
| IR6 | 0.000000 | 182.000000 |
| ISE1 | 0.000000 | -183.500000 |
| ISE10 | 0.000000 | 72.607574 |
| ISE11 | 0.000000 | -8.063094 |
| ISE12 | 37.111326 | 0.000000 |
| ISE2 | -82.000000 | -367.500000 |
| ISE3 | 0.000000 | -35.000000 |
| ISE4 | 0.000000 | -70.000000 |
| ISE5 | 0.000000 | 42.000000 |
| ISE6 | 0.000000 | -59.158616 |
| ISE7 | 0.000000 | -44.532586 |
| ISE9 | 0.000000 | 8.128297 |
| OCI1 | 0.000000 | -0.006156 |
| OCI2 | 0.183808 | 0.000000 |