Workshop 1, Algorithms and Data Analysis

Algorithmic thinking and the core of Python

Author

Alberto Dorantes, Ph.D.

Published

September 21, 2026

Abstract
This is an INDIVIDUAL workshop. We learn how to think algorithmically and how to translate an algorithm into Python code. We cover variables, conditionals, loops, lists, dictionaries and functions, and we apply them to classic finance problems: paying off a mortgage, pricing a bond, building an amortization table, and finding the yield of a bond with a search algorithm.

1 General directions for each workshop

You will work in Google Colab for all the workshops. Log in with your @tec.mx account and create a new Colab notebook for each workshop. Rename your notebook (click on the default name, “Untitled.ipynb”) as:

W1-Algorithms-YourFirstName-YourLastName

When you finish the Workshop, download it as Jupyter Notebook (as a .ipynb file), and then submit that .ipynb file through Canvas.

1.1 How to work in this workshop

Read each section and re-type and run the code yourself. Do not only copy and paste: when you type the code, you notice what each line does. Add your own notes in text cells, as if this notebook were your personal study guide for the Evidence (in-class exam).

In this course we follow a simple learning cycle for every piece of code:

  1. Predict: before running a code chunk, write in a text cell what you think it will print.
  2. Run: run the chunk and compare with your prediction.
  3. Investigate: if your prediction was wrong, find out why. This is where most of the learning happens.
  4. Modify: change one thing (a number, a condition, a variable) and see what happens.
  5. Make: write your own code for the challenges.

1.2 Using an LLM (Gemini, ChatGPT, Claude, etc.)

You are allowed, and encouraged, to use an LLM to help you write code. However, the goal of this course is that you understand Python well enough to read and check any code, including code generated by an LLM. Then, for every challenge:

  • Write your own algorithm (pseudocode) first, in a text cell. Then use it as your prompt.
  • If you used an LLM, paste your prompt in a text cell (in quotes).
  • Verify the result with an independent check: Excel, a closed-form formula, or a simple assert statement. Code that runs without errors is not necessarily correct!
  • Be ready to explain any line of your code. You will be asked to do so in the Canvas quizzes and in the Evidence.

1.3 Grading of this workshop

Your workshop grade has two parts:

Part Weight What is evaluated
Notebook (Jupyter Notebook file (.ipynb file) submitted in Canvas) 60% Completeness: all code replicated, all challenges attempted, your algorithms (pseudocode), your notes and your own explanations
Canvas Quiz W1 40% Automatically graded: you enter key results from your notebook and answer short code-reading questions

The Canvas Quiz W1 is due on the same date as the workshop. You have 2 attempts; Canvas keeps your highest score. Numeric answers accept a small tolerance, so round as indicated in each question.

For the notebook part:

  • Complete (100%): ORIGINAL and COMPLETE notebook, with all the activities, your notes and your OWN answers to the questions.
  • Incomplete (75%): ORIGINAL notebook with all the activities, but you did not answer some of the questions.
  • Very incomplete (10%-70%): you completed between 10% and 75% of the workshop, or parts of your work were copied from other students.
  • Not submitted (0%).

Before submitting, run all the cells from top to bottom (in Colab: Runtime → Run all, or Ctrl+F9) to make sure that your notebook runs without errors. And then, download your Notebook as .iypnb file, and then submit that file.

2 What is an algorithm?

An algorithm is a finite sequence of clear, unambiguous steps that transforms some inputs into a desired output. A cooking recipe is an algorithm: the ingredients are the inputs, the instructions are the steps, and the dish is the output.

In Finance, we use algorithms all the time, even if we do not call them that way. When you build an amortization table in Excel, you are executing an algorithm: for each month, calculate the interest, subtract the payment, and move to the next month.

2.1 How to design an algorithm

We will follow a 5-step design recipe for every problem in this course:

  1. State the problem in your own words, with short and clear sentences.
  2. Identify the inputs: what information do you need to solve the problem?
  3. Identify the output: what is the final result you expect? In what units?
  4. Write the general approach: 3 to 5 big steps in sequential order.
  5. Detail each step: specify calculations, conditions (if … then …) and repetitions (repeat until …). This detailed version is your pseudocode.

2.2 Example: how many years to double an investment?

1. Problem. I invest $100 at 10% annual interest. How many years do I need to wait until my balance is at least twice my initial investment?

2. Inputs. Initial investment ($100), annual interest rate (10%), target multiple (2).

3. Output. The number of years (an integer), and the balance at that year.

4. General approach. Start with the initial investment. Grow the balance one year at a time. Stop when the balance reaches the target.

5. Pseudocode.

SET balance = initial investment
SET year = 0
REPEAT WHILE balance < multiple × initial investment:
    year = year + 1
    balance = balance × (1 + rate)
END REPEAT
DISPLAY year and balance

Notice that the pseudocode is not written in any programming language. It uses plain words, but it is precise: there is only one way to interpret each line. Anybody (or any LLM) can translate this pseudocode into Python, R or Excel.

2.3 Pseudocode vs programming code

The same algorithm in Python:

initial_investment = 100
rate = 0.10
multiple = 2

balance = initial_investment
year = 0
while balance < multiple * initial_investment:
    year = year + 1
    balance = balance * (1 + rate)

print(f"You need {year} years. Your balance will be ${balance:,.2f}")
You need 8 years. Your balance will be $214.36

Compare the pseudocode and the Python code line by line. They are almost the same! This is the main idea of this course: if you can write a clear algorithm, you can write (or check) the code.

2.4 How to use an LLM to learn to think algorithmically

LLMs are very good at translating a clear algorithm into code, and very good at explaining code line by line. They are much less reliable when the prompt is vague. Some good practices:

  • Give the LLM your inputs, output and steps, not only the problem statement.
  • Ask for simple code (“use a while loop, do not use external libraries”) so you can understand it.
  • Ask the LLM to explain the code line by line, and then check whether the explanation matches what the code does.
  • Never accept a result without an independent check.

3 Python foundations

A programming language is a set of instructions that the computer executes to automate tasks. In Economics and Finance we use programming languages mainly to:

  1. collect data,
  2. clean data,
  3. transform and merge data,
  4. store data,
  5. analyze data with descriptive statistics and predictive models, and
  6. deliver information: reports with tables and graphs.

Python and R are free, open-source languages, and they are the two most popular languages for Data Science in the world. In this course we use Python.

Any programming language has the following building blocks:

  • Variables and variable types
  • Conditionals (decisions) using comparison operators
  • Loops to do repetitive tasks
  • Data structures: lists, dictionaries, arrays, data frames
  • Functions: reusable blocks of code with inputs and outputs

We now review each one.

3.1 Variables and types

A variable is a name that refers to a value stored in memory. We create a variable with the assignment operator =:

loan = 3000000        # an integer (int)
apr = 0.11            # a decimal number (float)
bank = "Banorte"      # text (str, string)
approved = True       # a logical value (bool): True or False

print(type(loan), type(apr), type(bank), type(approved))
<class 'int'> <class 'float'> <class 'str'> <class 'bool'>

Important: the = sign does not mean equality as in mathematics. It means “compute the right-hand side, and store the result in the name on the left”. Then, this is perfectly valid:

balance = 100
balance = balance * 1.10   # take the current balance, multiply it, and store it again
print(balance)
110.00000000000001

Arithmetic operators: +, -, *, /, ** (power), // (integer division) and % (remainder):

print(2 ** 3)      # power
print(17 // 5)     # integer division
print(17 % 5)      # remainder
8
3
2

3.1.1 Formatting output with f-strings

We display results with f-strings: a string that starts with f and contains variables inside curly braces {}. After a colon we can specify the format:

payment = 40000.456
print(f"The payment is {payment}")
print(f"The payment is ${payment:,.2f}")   # thousands separator and 2 decimals
print(f"The rate is {apr:.2%}")            # as a percentage with 2 decimals
The payment is 40000.456
The payment is $40,000.46
The rate is 11.00%

3.2 Conditionals

A conditional executes a block of code only if a condition is True. Comparison operators are: == (equal), != (different), <, <=, >, >=. We combine conditions with and, or, not.

Indentation matters in Python. The lines that belong to an if block must be indented (4 spaces). Python uses indentation, not braces or END statements, to know where a block ends.

Example: a bond is traded at a premium if its price is higher than its face value, at par if it is equal, and at a discount if it is lower:

price = 1050
face_value = 1000

if price > face_value:
    status = "premium"
elif price == face_value:
    status = "par"
else:
    status = "discount"

print(f"The bond trades at a {status}")
The bond trades at a premium
WarningCommon error: = vs ==

= assigns a value to a variable; == compares two values. Writing if price = face_value: produces a SyntaxError.

3.3 Loops

A loop repeats a block of code. Python has two types of loops:

  • for loops repeat a block a known number of times, or once for each element of a collection.
  • while loops repeat a block while a condition is True. We use them when we do not know in advance how many iterations we need (as in the “double my investment” example).

The function range(start, stop) generates integers from start up to stop - 1:

for month in range(1, 4):
    print("Month", month)
Month 1
Month 2
Month 3

3.3.1 The accumulator pattern

Many financial calculations follow the accumulator pattern: start a variable at zero (or at an initial value) before the loop, and update it inside the loop.

Example: I save $1,000 at the end of each month in an account that pays 12% annual interest compounded monthly. What is my balance after 6 months?

monthly_rate = 0.12 / 12
deposit = 1000
balance = 0                      # accumulator starts at 0

for month in range(1, 7):
    balance = balance * (1 + monthly_rate) + deposit
    print(f"Month {month}: balance = {balance:,.2f}")
Month 1: balance = 1,000.00
Month 2: balance = 2,010.00
Month 3: balance = 3,030.10
Month 4: balance = 4,060.40
Month 5: balance = 5,101.01
Month 6: balance = 6,152.02

3.3.2 Tracing a loop by hand

A trace table is a table where you write the value of each variable after each iteration. It is the best tool to understand (and debug) a loop. Here is the trace table for the first 3 iterations of the previous loop:

month balance before interest (1%) deposit balance after
1 0.00 0.00 1,000 1,000.00
2 1,000.00 10.00 1,000 2,010.00
3 2,010.00 20.10 1,000 3,030.10

You will build trace tables in some challenges and in the quizzes. Get used to them!

3.4 Lists

A list is an ordered collection of values, written with square brackets. Lists are very useful to store cash flows, prices or names:

cash_flows = [-100, 30, 40, 50, 20]    # year 0 to year 4
print(len(cash_flows))    # number of elements
print(cash_flows[0])      # first element: Python starts counting at 0!
print(cash_flows[-1])     # last element
print(cash_flows[1:3])    # a slice: elements 1 and 2 (the stop is NOT included)
5
-100
20
[30, 40]
WarningPython counts from 0

The first element of a list is cash_flows[0], not cash_flows[1]. A slice [1:3] includes position 1 but excludes position 3. This is the most common source of “off-by-one” errors.

We can add elements with the append method, and we can loop over the elements of a list directly:

prices = []                 # an empty list
prices.append(10.5)
prices.append(11.2)
prices.append(10.9)

for p in prices:
    print(p)
10.5
11.2
10.9

When we need both the position and the value, we use enumerate. Example: the Net Present Value (NPV) of the previous cash flows at a 10% discount rate:

NPV=\sum_{t=0}^{T}\frac{CF_{t}}{(1+r)^{t}}

rate = 0.10
npv = 0
for t, cf in enumerate(cash_flows):    # t = 0, 1, 2, ...; cf = the cash flow
    npv = npv + cf / (1 + rate) ** t
print(f"NPV = {npv:.4f}")
NPV = 11.5566

3.4.1 List comprehensions

A list comprehension builds a new list from another list in one line. It is a compact version of a for loop that appends values:

# Present value of each cash flow, in one line:
pv_flows = [cf / (1 + rate) ** t for t, cf in enumerate(cash_flows)]
print(pv_flows)
print(f"NPV = {sum(pv_flows):.4f}")
[-100.0, 27.27272727272727, 33.05785123966942, 37.565740045078876, 13.66026910730141]
NPV = 11.5566

Read it as: “for each t, cf in enumerate(cash_flows), compute cf / (1+rate)**t, and collect the results in a list”. You will see list comprehensions very often in advanced Finance courses, so learn to read them now.

3.5 Dictionaries

A dictionary stores key: value pairs, written with curly braces. It is ideal to store the characteristics of an object, such as a bond or a loan:

bond = {"face_value": 1000, "coupon_rate": 0.06, "years": 10, "frequency": 2}

print(bond["coupon_rate"])      # access a value by its key
bond["issuer"] = "CEMEX"        # add a new key
for key, value in bond.items():
    print(key, "->", value)
0.06
face_value -> 1000
coupon_rate -> 0.06
years -> 10
frequency -> 2
issuer -> CEMEX

A list of dictionaries is a very natural way to store a table: each dictionary is a row, and the keys are the column names. The pandas library converts it into a data frame (a table) in one line:

import pandas as pd

rows = []
for year in range(1, 4):
    rows.append({"year": year, "balance": 100 * 1.10 ** year})

table = pd.DataFrame(rows)
table
year balance
0 1 110.0
1 2 121.0
2 3 133.1

We will use this pattern to build an amortization table in Challenge 3.

3.6 Functions

If we need to run the same algorithm many times with different inputs, we write a function. A function receives parameters (inputs), executes its steps, and returns an output.

def npv(rate, cash_flows):
    """Net present value of a list of cash flows.
    cash_flows[0] happens today (t=0), cash_flows[1] at t=1, and so on."""
    total = 0
    for t, cf in enumerate(cash_flows):
        total = total + cf / (1 + rate) ** t
    return total

print(npv(0.10, [-100, 30, 40, 50, 20]))
print(npv(0.15, [-100, 30, 40, 50, 20]))
11.55658766477697
0.6435797470706639

Some important details:

  • The text between triple quotes """ is the docstring: it documents what the function does. Always write one.
  • Variables created inside a function (like total) only exist inside the function.
  • return sends the result back. A function without return returns None.

3.6.1 Default values and multiple outputs

Parameters can have default values, so the user only needs to specify them when they are different from the default. A function can also return several values (Python packs them in a tuple), and we can unpack them into several variables:

def future_value(pv, rate, years, frequency=1):
    """Future value of pv invested at an annual rate compounded `frequency` times a year.
    Returns the future value and the total interest earned."""
    fv = pv * (1 + rate / frequency) ** (years * frequency)
    interest = fv - pv
    return fv, interest

fv1, int1 = future_value(1000, 0.12, 5)                  # annual compounding (default)
fv12, int12 = future_value(1000, 0.12, 5, frequency=12)  # monthly compounding
print(f"Annual: {fv1:,.2f} (interest {int1:,.2f}); monthly: {fv12:,.2f} (interest {int12:,.2f})")
Annual: 1,762.34 (interest 762.34); monthly: 1,816.70 (interest 816.70)

Why is this important? In advanced courses (for example, Hedge Funds), most of the code is organized as small functions with default values, docstrings and multiple outputs, like this one:

def style_analysis(fund, X=X_style):
    """Sharpe (1992) constrained style analysis ..."""
    ...
    return w, 1 - e.var() / fund.var(), e

w_qai, r2_qai, e_qai = style_analysis(Rs["QAI"])

You do not need to understand what this function does yet, but you can already read its structure: one required parameter, one parameter with a default value, a docstring, and 3 outputs unpacked into 3 variables.

3.7 Objects and methods

In Python, everything is an object: a number, a list, a data frame. An object contains data and methods, which are functions that belong to the object. We call a method with a dot:

name = "grupo bimbo"
print(name.upper())          # a method of strings

flows = [30, 40]
flows.append(50)             # a method of lists
print(flows)

print(table.head(2))         # a method of data frames
GRUPO BIMBO
[30, 40, 50]
   year  balance
0     1    110.0
1     2    121.0

When you see something.method(), read it as “apply method to something”. This is all you need to know about Object-Oriented Programming for now.

3.8 Reading error messages

Errors are normal. Read the last line of the error message first: it tells you the type of error and usually the line where it happened. The most common errors are:

Error Typical cause
NameError: name 'x' is not defined You used a variable before creating it, or misspelled it (Python is case-sensitive: Rate ≠ rate)
SyntaxError Missing colon : after if, for, while or def; missing parenthesis; = instead of ==
IndentationError The lines of a block are not aligned
TypeError You combined incompatible types, for example "100" + 5
IndexError: list index out of range You asked for position n in a list of length n (remember: the last position is n-1)
ZeroDivisionError A division by zero, for example a rate of 0 in an annuity formula

A while loop whose condition never becomes False will run forever. If a cell does not finish, stop it (the stop button in Colab) and check the loop condition.

4 CHALLENGE 1: Paying off a mortgage

You have to write an algorithm and the corresponding Python code to calculate the number of months needed to pay off a mortgage loan:

  • Loan amount = $3,000,000.00 pesos
  • APR (annual percentage rate) = 11%, compounded monthly
  • Monthly fixed payment = $40,000.00 (includes interest and capital)

Assume that each month the balance first grows with the monthly interest (APR/12), and then the payment is made at the end of the month. In the last month, you only pay what is left (balance plus that month’s interest), which is less than the fixed payment.

Your program must report: (a) the number of months needed to pay off the loan, (b) the amount of the last payment, and (c) the total amount of interest paid over the life of the loan. Your program must work for any change in the values of the inputs.

Do the following:

  1. Write the 5 steps of the design recipe (problem, inputs, output, general approach, pseudocode) in a text cell.
  2. Build a trace table by hand (in a text cell or in Excel) for the first 3 months.
  3. Write (or generate with an LLM from your pseudocode) the Python code. Compare the first 3 months with your trace table.
  4. Verify your number of months with the closed-form formula for the number of periods of an annuity:

N=\frac{-\ln\left(1-\frac{PV\times r}{PMT}\right)}{\ln(1+r)}

where r is the monthly rate. Your number of months must be the next integer above N.

  1. What happens if the monthly payment is $25,000? Run your code and explain the result. Hint: compare the payment with the interest of the first month.

Hint: if you are familiar with Excel, you can start solving the problem in Excel, and then write your logical steps as pseudocode.

5 CHALLENGE 2: Bond pricing function

The company ABC issued a bond to finance a new technological project, with the following characteristics:

  • Principal (face value): $3,000,000
  • Time to maturity: 20 years
  • Coupon rate: 11% (annual)
  • Coupons are paid semiannually

The price of a bond is the present value of its coupons plus the present value of its face value:

Price=\sum_{t=1}^{N}\frac{C}{(1+i)^{t}}+\frac{FV}{(1+i)^{N}}

where C is the periodic coupon, i is the periodic interest rate (the annual rate divided by the number of payments per year), and N is the total number of periods.

  1. Write the design recipe for a function bond_price that receives the annual market interest rate, the years to maturity, the frequency of payments, the annual coupon rate and the face value, and returns the price of the bond. Use default values for the frequency (2) and the face value (1,000).
  2. Write the function using a loop to discount each coupon (do not use the annuity formula).
  3. Using a list of market rates [0.08, 0.11, 0.13] and a for loop, calculate the price of the ABC bond for each rate. For each rate, use a conditional to display whether the bond trades at a premium, at par or at a discount.
  4. Verify one of your prices with the annuity formula: Price = C\times\frac{1-(1+i)^{-N}}{i}+\frac{FV}{(1+i)^{N}}
  5. Explain with your own words why the price goes down when the market rate goes up.

6 CHALLENGE 3: Amortization table

Using the mortgage of Challenge 1 ($3,000,000, APR = 11%, payment = $40,000):

  1. Build the full amortization table as a list of dictionaries, where each dictionary is a month with the following keys: month, beginning_balance, interest, payment, principal (the part of the payment that reduces the balance) and ending_balance.
  2. Convert the list into a pandas data frame and show the first 5 and the last 5 rows (use the methods .head() and .tail()).
  3. Using the data frame, calculate the total interest paid (hint: table["interest"].sum()), and check with assert that it matches the total interest of Challenge 1 (allow a difference smaller than 0.01), and that the last ending balance is zero.
  4. In which month does the principal part of the payment become larger than the interest part for the first time? Hint: use a condition on the data frame, for example table[table["principal"] > table["interest"]], and look at the first row.

7 CHALLENGE 4: Finding the yield of a bond with a search algorithm

(Optional, it’s a little hard problem)

In Challenge 2 we calculated the price of a bond for a given market rate. In practice, we often have the opposite problem: we observe the price in the market, and we want to know the yield to maturity (YTM), the market rate that makes the price formula equal to the observed price. There is no closed formula for the YTM, so we need an algorithm.

The bisection method is a classic search algorithm. The idea: the bond price goes down when the rate goes up. If we know a rate that is too low (the price is above the observed price) and a rate that is too high (the price is below the observed price), the YTM must be between them. We try the midpoint, and we keep the half where the YTM must be. We repeat until the interval is very small.

SET low = 0.0001, high = 1.0
REPEAT WHILE (high − low) > tolerance:
    mid = (low + high) / 2
    IF bond_price(mid) > observed_price THEN
        low = mid        (the rate is too low: the YTM is in the upper half)
    ELSE
        high = mid       (the rate is too high: the YTM is in the lower half)
    END IF
END REPEAT
RETURN mid
  1. The ABC bond of Challenge 2 is trading at $2,600,000. Write a function ytm_bisection(observed_price, years, coupon_rate, frequency=2, face_value=1000, tolerance=1e-8) that implements this algorithm and uses your bond_price function inside it.
  2. Report the YTM (annual, in %, with 4 decimals) and the number of iterations your algorithm needed.
  3. Verify: plug the YTM into bond_price and check that you get the observed price.
  4. Explain with your own words why this algorithm always finds the answer, and why the number of iterations is small.

8 CHALLENGE 5: Code reading

Without running the code, predict what each piece of code prints. Write your prediction in a text cell. Then run it and explain any difference.

5.1

total = 0
for i in range(1, 5):
    total = total + i * 10
print(total)

5.2

flows = [100, 200, 300, 400, 500]
print(flows[1], flows[-2], flows[2:4], len(flows[1:]))

5.3 The following code should calculate the future value of $1,000 invested for 3 years at 10% annually, but it prints the wrong result. Find the bug and fix it.

balance = 1000
for year in range(1, 3):
    balance = balance * 1.10
print(balance)

5.4

def fee(amount, rate=0.02, minimum=50):
    f = amount * rate
    if f < minimum:
        f = minimum
    return f

print(fee(1000), fee(10000), fee(10000, rate=0.01))

9 Coursera - Google: online courses

You MUST TAKE the Coursera Course Get Started with Python. You have to finish this course before we start Week 3. The successful completion of this course for for your final grade.

10 W1 submission

  1. Submit the .ipnb file of your Google Colab Notebook in Canvas.

  2. Answer the Canvas Quiz W1 before the deadline. Keep your notebook open while you answer it: you will need your results.