Course 1 · Lesson 2 Collections, Iteration & Mathematical Series
Python for Actuarial Students · Course 1
Lesson 2

Collections, Iteration & Mathematical Series

Lists, tuples, dictionaries, and sets — and the loops that turn them into series sums, perpetuities, and amortization schedules.

Lesson 2 · OverviewBy the end of this lesson you can…

  • Store sequences in lists and tuples, and slice them with [start:stop:step]
  • Look things up by key with dicts — and explain why that’s fast
  • Use sets for uniqueness and membership
  • Repeat work a known number of times with for and range()
  • Repeat until convergence with while — safely
  • Evaluate series and polynomials for perpetuities and loans
75 min
Concepts & worked examples
75 min
Lab: series, perpetuities, amortization

Companion notebook: lesson-02-collections-iteration.ipynb — worked examples and self-checking exercises.

Part 1

Ordered Sequences

Lists and tuples: many values, one name.

ListsA list is an ordered, changeable sequence

# Mortality rates q_x for ages 40..45
q = [0.0010, 0.0012, 0.0015, 0.0019, 0.0024, 0.0031]

q[0]        # 0.001        first item (index 0!)
q[-1]       # 0.0031       last item
len(q)      # 6

q.append(0.0039)       # add to the end
q[0] = 0.0011          # replace an item
max(q), min(q), sum(q)
sorted(q)              # new sorted list
q.sort()               # sorts q in place
  • Square brackets, comma-separated
  • Indexing starts at 0; negative indexes count from the end
  • Lists are mutable: you can add, remove, and replace items
  • Can hold any mix of types — but keeping one type per list keeps you sane

Index 0 is age 40 here. Off-by-one errors between index and age (or time) are the classic actuarial coding bug — name things carefully.

Slicing[start:stop:step]

q = [0.0010, 0.0012, 0.0015, 0.0019, 0.0024, 0.0031]
#      0       1       2       3       4       5

q[1:3]      # [0.0012, 0.0015]          stop is excluded
q[:2]       # [0.001, 0.0012]           from the start
q[3:]       # [0.0019, 0.0024, 0.0031]  to the end
q[::2]      # [0.001, 0.0015, 0.0024]   every 2nd item
q[::-1]     # reversed
q[-3:]      # last three
  • start is included, stop is excluded — so q[1:3] has 3 − 1 = 2 items
  • Omit start/stop to mean “from the beginning” / “to the end”
  • Slicing always returns a new list
  • Out-of-range slices are fine: q[4:100] just stops at the end

Think of indexes as pointing between items: q[1:3] takes what’s between fence posts 1 and 3.

GotchaTwo names, one list

>>> base = [0.01, 0.02]
>>> stressed = base              # NOT a copy
>>> stressed.append(0.03)
>>> base
[0.01, 0.02, 0.03]               # base changed too!

>>> stressed = base.copy()       # a real copy
>>> stressed.append(0.99)
>>> base
[0.01, 0.02, 0.03]               # untouched

Assignment never copies. Both names point at the same list object.

The classic scenario-testing bug: you “stress” a copy of the base assumptions — and silently stress the base as well.

Copy with .copy(), list(base), or base[:].

TuplesFixed records with tuple

policy = ("P001", 34, "F")      # parentheses
policy[1]                       # 34
policy[1] = 35                  # TypeError: immutable

# Unpacking: one line, several names
policy_id, age, sex = policy

# Functions returning several values return a tuple
def pv_and_duration():
    return 952.38, 4.6

pv, dur = pv_and_duration()
listtuple
Syntax[1, 2](1, 2)
Mutable?YesNo
Typical usemany items of the same kindone record with fixed fields
Dict key?NoYes

Immutability is a feature: a tuple can’t be changed by accident.

Part 2

Key–Value Stores & Sets

Find things by name, not position.

DictionariesLook up values by key

rates = {"TERM": 1.24, "WHOLE_LIFE": 14.50, "UL": 9.10}

rates["TERM"]                 # 1.24
rates["ANNUITY"]              # KeyError!
rates.get("ANNUITY")          # None
rates.get("ANNUITY", 0.0)     # 0.0  (a default)
"UL" in rates                 # True

rates["ANNUITY"] = 6.75       # add or update
del rates["UL"]               # remove

for product, rate in rates.items():
    print(f"{product:<11}{rate:>6.2f}")
  • Curly braces; key: value pairs
  • Keys are unique and must be hashable (strings, numbers, tuples — not lists)
  • Order is preserved as inserted
  • .keys(), .values(), .items() to iterate

Rate tables, assumption sets, product parameters, a policy’s fields — dictionaries are everywhere in actuarial code.

Under the hoodWhy dictionary lookups are fast

"P999999"the key
↓ hash()
a numbertells Python which slot to check
↓
jump straight thereno scanning

Is policy "P999999" among a million IDs?

ids_list = [f"P{i:06d}" for i in range(1_000_000)]
ids_set = set(ids_list)

"P999999" in ids_list    # checks items one by one
"P999999" in ids_set     # hashes, then one check
List≈ 12 msgrows with size: O(n)
Set / dict≈ 40 nsconstant: O(1)

Measured on one laptop: ~300,000× faster. Your timings will differ; the gap won’t.

SetsUnique items and set algebra

products = ["TERM", "UL", "TERM", "TERM"]
set(products)                 # {'TERM', 'UL'}

in_force_q1 = {"P001", "P002", "P003"}
in_force_q2 = {"P002", "P003", "P004"}

in_force_q1 & in_force_q2     # {'P002', 'P003'}   both
in_force_q1 | in_force_q2     # all four            either
in_force_q1 - in_force_q2     # {'P001'}  lapsed or claimed
in_force_q2 - in_force_q1     # {'P004'}  new business
  • Unordered, no duplicates
  • Fast membership tests (hashing again)
  • & intersection, | union, - difference

Set difference between two in-force extracts is a one-line movement analysis: exits and new entrants.

Sets print in arbitrary order — wrap in sorted() for stable output.

Part 3

Iteration

for when you know how many times; while when you know when to stop.

forDefinite iteration

cash_flows = [50, 50, 50, 50, 1050]
i = 0.04

pv = 0.0
for t, cf in enumerate(cash_flows, start=1):
    pv += cf * (1 + i) ** -t
print(f"{pv:,.2f}")            # 1,044.52

list(range(5))                 # [0, 1, 2, 3, 4]
list(range(1, 11, 3))          # [1, 4, 7, 10]
list(range(10, 0, -2))         # [10, 8, 6, 4, 2]

for age, qx in zip(range(40, 46), q):
    print(age, qx)
  • for x in collection: runs the block once per item
  • range(start, stop, step) — same “stop excluded” rule as slicing
  • enumerate() gives (position, item) pairs
  • zip() walks several sequences in step
  • += is shorthand: pv = pv + …

Avoid for k in range(len(cash_flows)) — enumerate is clearer and harder to get wrong.

IteratorsWhat for really does

>>> it = iter([10, 20])
>>> next(it)
10
>>> next(it)
20
>>> next(it)
StopIteration

A for loop asks its collection for an iterator, calls next() repeatedly, and stops at StopIteration.

  • Lists, tuples, strings, dicts, sets, files, and range are all iterable
  • range(1_000_000_000) uses almost no memory: it produces numbers on demand

whileIndefinite iteration and convergence

# Sum 1 + r + r^2 + ... until terms are negligible
r = 0.9
tol = 1e-10
max_iter = 10_000

total, term, k = 0.0, 1.0, 0
while term > tol:
    total += term
    term *= r
    k += 1
    if k >= max_iter:
        raise RuntimeError("did not converge")

print(k, total)       # 219 9.999999999046972
print(1 / (1 - r))    # 10.000000000000002
  • while condition: repeats as long as the condition holds
  • A convergence criterion (tol) decides when we’re close enough
  • A max-iteration guard stops a loop that never converges
  • break exits a loop early; continue skips to the next pass

Forget to update term and the loop runs forever. Ctrl+C in a terminal, or Interrupt kernel in Jupyter.

Actuarial Lab · 75 minSeries, perpetuities, and loans

1 · Geometric series

Approximate an infinite sum with a while loop and compare to the closed form.

2 · Perpetuities

Value a payment of 1 forever by summing vk; confirm it equals 1/i.

3 · Loan polynomials

Evaluate cash flows as a polynomial in v, solve for a mortgage payment, and roll the balance forward.

Work in notebooks/lesson02_lab.ipynb. Explain each result in a Markdown cell: what the numbers mean, and whether they match theory.

Lab · Parts 1–2Infinite sums, finite loops

Geometric series

Starting from the while loop on the earlier slide:

  1. Try r = 0.5, 0.9, 0.99. How many terms does each need for tol = 1e-10?
  2. What happens with r = 1.01? Does your guard catch it?
  3. Print the error abs(total - 1/(1-r)) for each r

Perpetuity-immediate

$$a_{\overline{\infty}|} = \sum_{k=1}^{\infty} v^k = \frac{1}{i}$$

i = 0.05
v = 1 / (1 + i)
total, term, k = 0.0, v, 0
while term > 1e-8:
    total += term
    term *= v
    k += 1
print(k, total, 1 / i)
# 377 19.99999979456978 20.0

Then: a perpetuity-due (theory: 1/d = 21), and one paying 100 at time 1 growing 2% a year (theory: 100/(i − g) ≈ 3,333.33).

Lab · Part 3Cash flows as polynomials in v

Present value of cash flows c0, c1, …, cn is a polynomial:

$$PV = c_0 + c_1 v + c_2 v^2 + \dots + c_n v^n$$

Horner’s method evaluates it with one multiply and one add per coefficient:

coeffs = [0, 50, 50, 50, 50, 1050]   # c_0 ... c_5
v = 1 / 1.04

result = 0.0
for c in reversed(coeffs):
    result = result * v + c
print(result)     # 1044.5182233101614

Same bond as the for-loop slide — same answer, fewer powers.

Mortgage: 200,000 over 30 years at 6% (monthly)

  1. Monthly rate j = 0.06 / 12, n = 360
  2. Build [0] + [1] * n and evaluate it at v → an ≈ 166.7916
  3. Payment = 200,000 / an → 1,199.10
  4. Loop month by month: interest = balance × j; principal = payment − interest
MonthInterestPrincipalBalance
11,000.00199.10199,800.90
2999.00200.10199,600.80
12988.77210.33197,543.98

Check: the balance after 360 payments should be ≈ 0. Is it exactly 0? Why not?

RecapLesson 2 checklist

  • I can create, index, slice, and modify lists
  • I know assignment doesn’t copy — and how to copy
  • I use tuples for fixed records and unpack them
  • I can build and query dictionaries, with .get() defaults
  • I can explain why dict/set lookups are O(1)
  • I use for with range, enumerate, and zip
  • Every while loop I write has a tolerance and a guard
  • I can sum a series to convergence and check it against theory
  • I can evaluate a cash-flow polynomial with Horner’s method
  • I can produce the first rows of an amortization schedule
Up next
Lesson 3

Functional Programming & Core Math Applications

Package your formulas as reusable, tested functions — discount factors, interest conversions, and annuities.