Solve Excel-based actuarial exam questions using Python and openpyxl
You are solving an Excel-based actuarial exam paper (e.g. CM1B, CM2B, CS1B, CS2B). These papers provide base data in Excel workbooks and expect candidates to build spreadsheet solutions. Since you cannot interactively use Excel, you will use Python with openpyxl to read the base data, perform all calculations, and write the results to an output workbook.
Your workflow is:
attempt.mdUse the project helper script to inspect and extract data from the exam workbook:
python scripts/excel_helper.py describe "exams/<SUBJECT>/<SITTING>/Answer-Booklet.xlsx"
This prints all sheet names and their dimensions. Then read specific sheets:
python scripts/excel_helper.py read "exams/<SUBJECT>/<SITTING>/Answer-Booklet.xlsx" "Q1 Base"
This outputs the sheet contents as a formatted table. Use this to understand the base data before writing your solution.
Tip: Redirect output to a file if the sheet is large:
python scripts/excel_helper.py read "path/to/file.xlsx" "Q1 Base" > q1_data.txt
For each question, write a standalone Python script that:
openpyxlfrom openpyxl import load_workbook, Workbook
import math
# ── Read base data ──────────────────────────────────────────────
SOURCE = "exams/<SUBJECT>/<SITTING>/Answer-Booklet.xlsx"
wb = load_workbook(SOURCE, data_only=True)
ws = wb["Q1 Base"]
# Extract values into Python data structures
# Example: read a column of mortality rates
data = {}
for row in ws.iter_rows(min_row=2, values_only=True):
if row[0] is not None:
data[row[0]] = row[1]
# ── Perform calculations ───────────────────────────────────────
# Year-by-year loop example:
results = []
for t in range(1, 21):
# ... compute unit fund, non-unit fund, profit vector, etc.
value = 0 # replace with actual calculation
results.append({"year": t, "value": value})
# ── Print results ──────────────────────────────────────────────
print("=== Question 1 Results ===")
for r in results:
print(f"Year {r['year']:2d}: {r['value']:.4f}")
# ── Write to output workbook ───────────────────────────────────
OUTPUT = "results/<MODEL>/<SUBJECT>/<SITTING>/workings.xlsx"
out_wb = Workbook()
out_ws = out_wb.active
out_ws.title = "Q1 Solution"
out_ws.append(["Year", "Value"])
for r in results:
out_ws.append([r["year"], r["value"]])
out_wb.save(OUTPUT)
print(f"\nResults saved to {OUTPUT}")
unit_fund = 0.0
for t in range(1, n_years + 1):
premium = initial_premium * (1 + escalation) ** (t - 1)
alloc_rate = allocation_rates[t]
allocated = premium * alloc_rate
bid_value_new = allocated * (1 - bid_offer_spread)
unit_fund = (unit_fund + bid_value_new) * (1 + growth_rate[t])
mgmt_charge = unit_fund * amc_rate
unit_fund -= mgmt_charge
for t in range(1, n_years + 1):
premium = premiums[t]
allocated = premium * alloc_rate[t]
unallocated = premium - allocated
bos_income = allocated * bid_offer_spread
start_cf = unallocated + bos_income - expenses[t] - commission[t]
interest = start_cf * non_unit_interest
death_cost = q[age + t - 1] * max(0, death_benefit[t] - unit_fund_end[t])
profit[t] = start_cf + interest + mgmt_charge[t] - death_cost
prob_in_force = [1.0]
for t in range(1, n_years + 1):
p_survive = 1 - q[age + t - 1]
p_lapse = lapse_rate[t] if t < n_years else 0
prob_in_force.append(prob_in_force[-1] * p_survive * (1 - p_lapse))
profit_signature = [profit[t] * prob_in_force[t - 1] for t in range(1, n_years + 1)]
npv = sum(ps / (1 + rdr) ** t for t, ps in enumerate(profit_signature, 1))
# Work backwards from the last year
reserve = [0.0] * (n_years + 1)
revised_profit = list(profit) # copy
for t in range(n_years, 0, -1):
if revised_profit[t] < 0:
# Set up reserve at end of previous year
reserve[t - 1] = -revised_profit[t] / ((1 + non_unit_interest) * (1 - q[age + t - 1]))
revised_profit[t] = 0.0
# Adjust previous year's profit for reserve cost
if t > 1:
revised_profit[t - 1] -= reserve[t - 1]
revised_profit[t - 1] += reserve[t - 2] * (1 + non_unit_interest) * (1 - q[age + t - 2]) if t > 1 else 0
def joint_survival(qx_male, qx_female, term):
"""Compute year-by-year joint survival probabilities."""
p_male = [1.0]
p_female = [1.0]
for t in range(term):
p_male.append(p_male[-1] * (1 - qx_male[t]))
p_female.append(p_female[-1] * (1 - qx_female[t]))
p_joint = [p_male[t] * p_female[t] for t in range(term + 1)]
p_at_least_one = [p_male[t] + p_female[t] - p_joint[t] for t in range(term + 1)]
return p_male, p_female, p_joint, p_at_least_one
def annuity_due(qx, interest, max_age=120):
"""Value annuity-due by summing v^t * t_p_x."""
v = 1 / (1 + interest)
result = 0.0
t_px = 1.0
for t in range(max_age):
result += (v ** t) * t_px
if t < len(qx):
t_px *= (1 - qx[t])
else:
break
return result
Run each computation script and capture the output:
python results/<MODEL>/<SUBJECT>/<SITTING>/q1_solution.py
Review the output carefully. If results look wrong, debug and re-run.
All computation scripts should write their results to:
results/<MODEL>/<SUBJECT>/<SITTING>/workings.xlsx
If multiple questions produce output, write to different sheets within the same workbook, or create separate workbooks (e.g. workings_q1.xlsx, workings_q2.xlsx).
In attempt.md, for each question:
CRITICAL: Do not estimate or approximate. Run the computation and report the actual computed values. The whole point of this skill is to replace "≈ £8,557" with exact computed answers.
results/<MODEL>/<SUBJECT>/<SITTING>/
├── attempt.md # Written answers with full working and computed results
├── workings.xlsx # Excel output with computed values
├── q1_solution.py # Python script for Question 1
├── q2_solution.py # Python script for Question 2
├── q3_solution.py # Python script for Question 3
└── ...
The openpyxl package must be installed:
pip install openpyxl