809 views
owned this note
<style>
h3 {
border-bottom: 1px solid #ccc;
}
section {
margin-bottom: 2rem;
padding: 0em 1em 1em 1em;
border-radius: 4px;
background-color: #f7f7f7;
border: 1px solid #ccc;
}
summary {
font-weight: bolder;
}
summary:hover {
text-decoration: underline;
}
.todo {
color: #ff00ff;
border: 2px dashed #ff00ff;
padding: 0em 1em;
border-radius: 5px;
/* display: none; UNCOMMENT TO HIDE TODOs */
}
</style>
# Lab 7: Matrices, Files, and Regex (Spring 2026)
In this lab, we're going to build the core of a support application for processing college admissions applications. We'll use this to illustrate three things: a valuable new data structure, a common programming tool, and an overview of how programs work with a filesystem (you'll need the latter for homework 5).
# Learning Objectives
In this lab, you will learn or practice:
- how programs work with filesystems
- selecting data structures to achieve design features of programs
- what matrices are and why they are one of the core data structures of modern computer science
# Setup
Before beginning this assignment, clone the source code from GitHub to make your personal repository for this lab. Here is the **[GitHub Classroom link](https://classroom.github.com/a/EF96nNIZ)**.
**Be sure to follow the [Python Stencil setup guide](https://docs.cs200.io/s/python-stencil-setup-guide)** for instructions on how to do this.
:::warning
:warning::warning::warning: **Warning**: Since this is our first Python assignment, **be sure to read the [**Python Stencil Setup Guide**](https://docs.cs200.io/s/python-stencil-setup-guide)** for important instructions on how to set up VSCode to use the stencil.
In particular, **make sure you [set up your environment](https://docs.cs200.io/s/python-stencil-setup-guide#Setting-up-your-environment)**--if you don't this, you won't be able to run any tests!
:::
:::info
**Remember**: When creating your team for the lab, be sure to name it starting with "`lab07`"! Team names are shared across all assignments in the course, so you can't pick the same name you used in any other assignment!
:::
:::warning
**Written Answers**: This [Google Form](https://forms.gle/fbZimwbRXUeXQwHK6) has spaces for you to write your answers to some of the tasks. Please fill in the spaces as indicated. Your answers will not be graded (or affect your course grades). This is a relatively new lab and we want to review your answers later to help us determine what parts of it are/are not working. Only one of you have to fill it out (there's a field for your partner's email) and only some questions are required!
:::
# Computing Admissions Scores
Imagine that a university admissions office wants to write a function that helps them rate applicants. The function will take a student's GPA, SAT score, and number of extracurriculars and return a `float` score (not an admissions decision). The score multiplies the gpa by .6, the SAT score by .1 and the extracurriculars by .15. Here's a first version of the code:
```python
class Admissions:
def __init__(self):
pass
def admit_v1(self, gpa: float, sat: float, extracurric: float) -> float:
return (gpa * 0.6) + (sat * 0.1) + (extracurric * 0.15)
```
<section>
### Discuss
Does this formula indicate that SAT scores are a sixth as important as GPAs?
</section>
<section>
### Task 1
Bahar proposes that information for a single student should be passed through a single parameter `student_data` in one data structure, rather than as three separate parameters `gpa`, `sat`, and `extracurric` of the method.
- Do you agree or disagree with Bahar?
- What benefits do you get from each approach?
- Assuming you used Bahar's approach, what data structure would you propose?
Put your answer in the Google Form! Only 1 partner of the pair has to.
</section>
<section>
### Task 2
Add a method named `admit_v2` to the `Admissions` class. It takes in student data as a dictionary (type `Dict[str, float]` with keys `"gpa"`, `"sat"`, and `"extracurric"`). Otherwise, its computation is the same as `admit_v1`. (You can start with a copy of `admit_v1` and edit it.)
</section>
In a realistic admissions system, students aren't given a single score. Instead, there are multiple criteria of interest, such as "likelihood to succeed academically" and "likelihood to contribute to the community." Each of these might weigh the data on a student differently.
<section>
### Task 3
Add two helper methods to the `Admissions` class, one for each of two criteria:
- `likely_succeed`: which weighs gpa at .75, SATs at .25, and extracurriculars at 0
- `likely_contribute`: which weighs gpa at .3, SATs at 0, and extracurriculars at .7
Each method should take a dictionary of student data as input and return a float as output. The returned float computes a score from the student data using the given weights.
</section>
<section>
### Task 4
Use these methods to write `admit_v3`, which takes the same input as `admit_v2` but instead returns a `List[float]` with the results of `likely_succeed` and `likely_contribute`, respectively.
:::info
Uncomment and use `test_admissions.py` to test your `admit_v2`, `admit_v3`, and `admit_v4` as you continue.
:::
</section>
When we're writing programs and applications, it helps to consider which future changes or extensions we might need to support. For example, at some future point, we might consider additional information about individual students. We also might want to look at a larger set of weight-based criteria (such as whether they'd bring regional diversity or their ability to participate in the orchestra).
<section>
### Question #2
If we wanted to use more than 2 weighting functions to the output list, what about `admit_v3` would need to be edited? What design of the function would let someone customize the set of weighting criteria without having to edit the body of the method?
**Actually discuss this before moving on to Task 5 -- this is part of how you practice your design instincts.**
<!-- Put your answer in the Google Form! -->
</section>
:::success
**Checkpoint 1!** Call over a TA to check your work.
:::
<section>
### Task 5
Create `admit_v4` which takes a student data dictionary (as before) and a list of weighting functions and returns a list of the results of those functions.
*Passing in functions can be a confusing concept. If you are struggling to understand the hint, feel free to consult an LLM on how to construct this method. Consider also looking at `test_admit_v4` in the testing file to see example usages.*
<details><summary><b>Hint: Passing functions as arguments in Python</b></summary>
In Python, you can pass a `Callable` type into a method and then test the method call with a lambda function. Here's an example of defining a lambda function of Callable type:
```python
from typing import Callable
my_func: Callable[[int], int] = lambda n: n * 2
print(my_func(5)) # Prints 10
```
Remember that `Callable` is a type hint just like `int` or `str`. This means that you can't make a `Callable` object. As shown, you can use `lambda` to actually define the function. You can use this to create the functions for different weightings. All the functions take the student data dictionary and return a float score.
</details>
</section>
With `admit_v4`, we are moving towards a flexible function. We can customize the set of weighting functions. We could customize the student attributes by adding more keys to the dictionary (we would still need to modify the weighting functions to use any new keys). Let's set additional attribute keys aside for now.
<section>
### Question #3
In `admit_v4` we returned a list of values. Unfortunately, we can't easily see what weighing function corresponds to what value without memorizing the order of functions in the list. Can you see why that is a problem?
What if we wanted to return an object that has that list and also the weighting functions that gave rise to each list element? How might you create a new version (`admit_v5`) that returns labeled weights? What data structures would you use and what changes would you make? Your approach should leave the function flexible with regards to the set of weighting functions that are used.
**Put your answer in the Google Form!**
</section>
<section>
### Task 6
Implement `admit_v5` such that it returns a custom class housing the data structure(s) you described. You can see the note below for a suggestion.
<details><summary><b>Hint: Our approach</b></summary>
Above our version of `admit_v5`, we created a custom class `LabeledResults` that has 2 lists as instance variables. This way, our outputs and labels are connected by the same index in the list. We also need to keep track of what labels cause what computation, so we modified our input function structure to associate labels and functions!
Coding tip: If you need to iterate over the keys of a dictionary, `dictionary.keys()` will return an iterable `dict_keys` object, which provides a set-like view for the keys in the dictionary! You can loop through this set and access each key.
</details>
</section>
:::success
**Checkpoint 2!** Call over a TA to check your work.
:::
## Stepping Back
At this point, let's step back and review what we've done.
- We started with a simple function that would weight data from a student's college application to determine a "score" for that student.
- In order to consider multiple factors for a student, we wrote multiple scoring functions that consider the application data in different ways.
- In order to make it easier to add new factors later, we wrote a more general function that takes student data and a list of scoring functions and returns a list of the results.
In other words, we're setting up our original scoring idea to be more flexible through the use of lists and functions as arguments. We could be using this infrastructure for any number of "scoring" situations, not just college admissions.
So far, this has been about making programs more general. Now, we're going to dive deeper and look at the usability of what we've created. Right now, the user of this program has to set up a bunch of **lambda functions (callables)** with the scoring functions. The idea is good, but the notation is clunky. Can we do anything to address that?
## Usability and Abstraction
Let's see if we can make this scoring program easier for someone to use. To start, let's draw out the computation for a specific student:
Assume we have a student dictionary named `std1` containing a gpa of 3.6, sat of 1450, and extracurric of 3. What computations does our program do for this student?
```
likely_succeed = (std1['gpa'] * 0.75) + (std1['sat'] * 0.25) + (std1['extracurric'] * 0)
likely_contribute = (std1['gpa'] * 0.3) + (std1['sat'] * 0) + (std1['extracurric'] * 0.7)
```
Substitute the concrete values from `std1`. Now we have:
```
likely_succeed: (3.6 * .75) + (1450 * .25) + (3 * 0)
likely_contribute: (3.6 * .3) + (1450 * 0) + (3 * .7)
```
Since both of our scoring functions work with the same data from the student dictionary and apply the same pattern of multiplication and addition, we can summarize the actual weight data that a user must provide as the following table:
| | gpa | sat | extracurric |
| -------- | --------| -------- | -------- |
| likely_succeed | .75 | .25 | 0 |
| likely_contribute | .30 | 0 | .70 |
We can also picture a single student's dictionary as a table with one row:
| gpa | sat | extracurric |
| --------| -------- | -------- |
| 3.6 | 1450 | 3 |
In other words, since the scoring functions have similar internal computations (just with different weights over the student data), we don't have to have the user write the `lambda` objects. Instead, **we could simply have the user provide a table (as a csv file, for example) and move the multiplication and addition parts into our program**.
Furthermore, rather than returning a `List` (which someone in the admissions office might not understand), we could return a table with the same information:
| student | likely_succeed | likely_contribute |
| -------- | -------------- | ----------------- |
| std1 | (3.6 * .75) + (1450 * .25) + (3 * 0) | (3.6 * .3) + (1450 * 0) + (3 * .7) |
which after performing the computations is
| student | likely_succeed | likely_contribute |
| -------- | -------------- | ----------------- |
| std1 | 365.2 | 3.18 |
**If you have already taken linear algebra -- does this table-based computation look familiar?**
The computation we have shown here, in which we take two tables and produce a third table with this pattern of multiplication and addition, is called **matrix multiplication** (you may have seen this in a math class even if you haven't taken linear algebra). Our input tables are both matrices.
We derived this table notation from taking a simple scoring function and making the idea more flexible and the inputs more user-friendly. This is the start of a software-design story for why matrices are so useful in computing. But we're not done yet!
# Matrices: Core Computer Science Data Structure
Based on what we've done, what, then, is a matrix? While we may *see* it as just "a table", a matrix is actually a concise way of representing a function that takes a row (a.k.a, a vector) of student data and returns a row (a.k.a, a vector) of the scores for that student.
<section>
### Question #4:
Is a matrix the same as a 2D array? Why or why not?
<!-- **Write your answer in the Google Form. Then expand the answer below.** -->
<details><summary><b>Answer: </b></summary>
Arrays capture the *numbers* within the matrix, but not the *labels* (the column headers from the table view). The labels are important. Why? Without the labels in the scoring matrix, we can't label the columns in the output from the scoring function. Put differently, the labels are essential to maintaining the meaning of the computation.
A proper data structure for a matrix needs the labels and the data. Thus, a 2D array misses some of the key information.
</details>
</section>
These kinds of functions underlie much of machine learning (which is why you have to take linear algebra before taking most AI courses). Imagine that we had an admissions-scoring matrix, and applications from 100 students. We could use matrix-vector multiplication over each student to build up a table:
| Student | succeed | contribute |
| -------- | -------- | -------- |
| s1 | 302.7 | 3.88 |
| s2 | 400.5 | 2.6 |
| s3 | 333.65 | 5.86 |
| ... | ... | ... |
We could augment this table with information on which students we admitted previously:
| Student | succeed | contribute | admitted |
| -------- | -------- | -------- | -------- |
| s1 | 302.7 | 3.88 | no |
| s2 | 400.5 | 2.6 | yes |
| s3 | 333.65 | 5.86 | yes |
| ... | ... | ... | ... |
Is this starting to look familiar? Remember Decision Tree? This is precisely the sort of data table from which we generated a decision tree to be able to make future decisions. Matrix-vector multiplication is thus one of the underlying tools for machine learning.
Matrices aren't just for machine learning. They also play key roles in graphics (where operations like rotating images are also implemented by matrix operations). In a couple of weeks, we will have a lecture on matrices where we develop an actual data structure for them and look at a couple of these other examples.
For the next section of this lab, we want you to get practice expressing computations as matrices, using a matrix multiplication method that is part of the stencil code.
### Writing Computations as Matrices
<section>
### Task 7
Navigate to the `matrix_practice1` and `matrix_practice2` methods in the `Admissions` class. Your task is to represent the matrices below in code, and check the result of multiplying them.
You will have to code the following 4 matrices:
Matrix 1:
| Student | gpa | sat | extracurric | honors | awards |
| -------- | --- | --- | ----------- | ------ | ------ |
| Walter | 3.5 | 1450 | 4.0 | 0 | 2 |
| Jesse | 3.8 | 1350 | 3.2 | 1 | 1 |
| Gus | 3.0 | 1250 | 5.0 | 0 | 5 |
Matrix 2:
| Attribute | fn1 | fn2 |
| --------- | --- | ---- |
| gpa | 0.4 | 0.1 |
| sat | 0.2 | 0.3 |
| extracurric | 0.1 | 0.4 |
| honors | 0.2 | 0 |
| awards | 0.1 | 0.2 |
Matrix 3:
| Attribute | fn1 | fn2 | fn3 |
| --------- | --- | --- | --- |
| isLeader | 0.5 | 0.7 | 0.1 |
| isVarsity | 0.5 | 0.3 | 0.9 |
Matrix 4:
| Student | isLeader | isVarsity |
| -------- | -------- | -------- |
| Kratos | 1 | 1 |
| Loki | 0 | 0 |
| Odin | 1 | 0 |
| Thor | 0 | 1 |
| Heimdall | 0 | 1 |
Follow the instructions in the file to create the matrix data for each of these matrices. Look in the stencil code for the `LabeledMatrix` class. Look at its constructor to create your matrices (and create them), and then take a look at the matrix multiplication method. Then, use the method to:
- multiply Matrix 1 and Matrix 2 in `matrix_practice1`,
- multiply the other two matrices in `matrix_practice2`.
:::info
Refer to `admissions.py`'s `main` method for examples on how to create a `LabeledMatrix` object.
:::
:::info
Test both of your `matrix_practice` in `test_admissions.py`.
:::
</section>
<section>
### Question #5
Normally, multiplication is *commutative*, meaning that we can provide the numbers in either order (e.g., 2 * 3 is the same as 3 * 2). Is the same true here? Can you multiply Matrix 1 and Matrix 2 in either order? Try it out.
**In the Google Form, try to explain why matrix multiplication is NOT commutative.**
**After answering this question, you may submit the Google Form.**
</section>
Wow -- we are well on our way to having an entire algorithmic decision making system! With this lab, we can set up the scoring criteria and produce a matrix of results for multiple students. We could feed that into our Decision Tree project to make predictions.
From a software development perspective, however, we still have some work to do. Where does the matrix of student data come from to feed into the admissions algorithm? At least some of it should be extracted automatically from files that the students submit with their application ...
:::success
**Checkpoint 3!** Call over a TA to check your work.
:::
# Extracting Data from Files
If we were writing an entire app, where would student transcripts come from? They'd be files on a computer. They would be in a separate directory or folder from the program that processes the admissions decisions, however (because different people maintain the collection of student data and the code/program that processes them). In this section of the lab, we will use this problem as a motivating example to help you understand how files work on your computer (you'll need this for homework 5 after the break).
<section>
### Task 8
Here is a [small zip file of sample student application data](https://drive.google.com/file/d/1dVKaTbvGWQR0_EaNZ8FbbzzLt8ogrMdG/view?usp=sharing). The data has a folder for each student (labeled with their application ID); each folder contains both an essay file and a transcript file for that applicant.
Download the zip file to a location that is NOT inside your Python Project for lab. You could put it in Downloads, or put it in a folder where you keep your CS200 assignments. Either way, make sure you know where you put it.
Double click on the .zip file to unpack it into a single folder called `Data` (make sure there isn't already a file with this name in the folder; if there is, move `Data.zip` into a new subfolder to avoid a clash.
</section>
The rest of this task practices how to move through folders and view information on files through something called the "command line" (or "terminal").
If both of you already know commands like "cd", "dir"/"ls", and "more/less", and know what file paths look like, you can skip ahead to the next section (on Parsing Student Transcripts). Otherwise, continue with this section to learn these commands (again, you will need this for Homework 5).
<section>
### Practicing File Navigation
1. Open a terminal/command line window:
- If you are on **Windows**, open the command terminal (type `cmd` in the search bar) or your other terminal of choice, like PowerShell or Git Bash.
- If you are on **MacOS/Linux**, open the application called `Terminal` on macOS or your preferred Bash terminal on Linux. On MacOS, you can hit spacebar while holding down the command (⌘) key, then type "terminal" in the search box.
2. To see what files and subdirectories are in the current directory, type `dir` (Windows CMD) or `ls` (MacOS/Linux; also works in Git Bash and PowerShell).
- If you want to see the contents of a subdirectory, you can type the subdirectory name after the command (e.g., `dir Documents` or `ls Documents`).
3. Now, we want to use terminal commands to move to the directory where you put the downloaded Data.zip file. To do this, you need to know (roughly) where you put the file.
- In both Windows and MacOS/Linux, the command is `cd` (change directory). If you were to type `cd Downloads`, for example, you would move to the Downloads subdirectory of your current directory (if that exists).
- Remember that you can use `dir` (Windows) or `ls` (MacOS/Linux) to find out what subdirectories are in your current directory. If you know roughly where you downloaded the Data.zip file, use these commands to find the right subdirectory name, then use `cd` to move into the subdirectory. Continue until you are in the `Data` directory.
- Useful tips:
- You can press the `Tab` key to autocomplete subdirectory names that you've started typing
- If you need to back up one subdirectory, use `cd ..`
- If you want to go back to your top-level directory and start over:
- **Windows CMD**: use `cd \` from a drive root or reopen the terminal
- **PowerShell**: `Set-Location ~` or `cd ~`
- **MacOS/Linux**: `cd` (no arguments) returns to your home directory
4. Once you are in the `Data` folder, use `dir` or `ls` to see its contents in your terminal/shell. You should see a collection of subdirectories named with (ID) numbers. Pick any of the applicant IDs and look at what is in that applicant's directory (use `cd`). You should see an essay file and a transcript file.
5. Display the contents of each file in your Shell or Terminal:
- On Windows, use `more [fileName]` (CMD) or `Get-Content [fileName]` (PowerShell).
- On MacOS/Linux, use `cat [fileName]` (full print) or `less [fileName]` (scroll view).
- Within any of these, you can:
- Press `Space` to scroll a screen at a time.
- Press `Enter` to go line by line.
- Press `q` to quit and exit.
6. Just as we talked about "paths" (or routes) in graphs, there is a notion of "path" in a filesystem. The "path" records all of the subdirectories you had to navigate to when reaching the current directory (as directory names separated by slashes). To see the path to your `Data` directory, type:
- `pwd` on MacOS/Linux (prints the current directory). In **PowerShell**, `pwd` also works; in **Windows CMD**, use `cd` to show the current directory.
Congratulations, now you know the basics of the terminal to navigate files manually. Next, we'll do this navigation in code.
</section>
Now that we have a sense of how to move around files, we want to learn a piece of how we would extract information from a student's application materials. Looking at the transcript files, for example, there is a section in each one with the overall GPA, which we want to use to compute the scores. It would be great to write a program to extract the GPA from the file, without a human having to search for it.
There are common programming libraries that help with such tasks. For the rest of this lab, we're going to learn about one called "regular expressions".
<section>
### Task 9
Go back to your `admissions.py` file. Take a look at the method `find_gpa_in_file` that we have provided for you. Given a path to a file, this method will utilize something called a _regular expression_ to find a specific match. Your goal is to provide the path to a transcript file, and see if it correctly displays student's cumulative GPA.
Using the file-navigation skills that you just practiced, add calls to `find_gpa_in_file` to the `main` method in the `Admissions` class and make sure that the correct GPA value is extracted. Here's a template of how to set this up. As shown below, the exact directories in your file path will differ depending on where you put the files on your system and what type of operating system you have.
```
# Windows version
transcript_path1 = r"C:\Users\karav\OneDrive\Documents\CS200TA\data\1093857\transcript.txt"
# MacOS version
transcript_path1 = "/Users/karav/OneDrive/Documents/CS200TA/data/1093857/transcript.txt"
# Either OS
gpa1 = find_gpa_in_file(transcript_path1)
print(gpa1)
```
Try a couple of different transcripts and check that the right answers are being extracted.
</section>
:::success
**Final Checkpoint!** Congratulations! You completed this week's lab. Call a TA over to get checked off.
:::
***
:::warning
Ideally, everyone will get this far in lab. We don't expect most of you to get beyond here, but if you have time, read on to learn more about creating regular expressions, which is a super handy programming skill (in any language).
:::
# Parsing student transcripts: regular expressions (Optional)
Imagine that you were given the text from an email message and asked to find all of the references to Brown course codes (such as `"CSCI0200"`) in the message. How might you do this?
If you were searching for a specific course, you could get the text as a list of strings and loop over all of the strings, checking which ones were equal to `"CSCI0200"`. But we’re asking something different: we’re asking you to identify all strings of a specific _shape_ (here, four capital letters followed by four digits). How would you write _that_ program?
Tasks like this that involve searching (and perhaps replacing) strings based on their shapes are common in programming. In fact, this is so common that languages provide constructs to describe the shape of strings you want, while the language handles checking each string against the expected shape in an efficient way. **These "shapes", which denote what patterns of text should look like, are called _regexes_ or _regexps_ (short for “regular expressions”, a term that arises from the mathematical foundations of computation).**
This part of lab will teach you how to write regular expressions. **You'll have a chance to use them on HW5 if you want.** Regexes also show up in nearly every programming language, and are a super handy tool to know for general programming outside this course!
We’re going to start having you work on [a website](https://regex101.com) designed to teach about writing regular expressions. Afterwards, you’ll write a small program to see how to use regexes within a Python program.
### Warmup Exercises
<section>
### Task 10
1. Go to [regex101.com](https://regex101.com), which is an awesome site for building and testing regular expressions.
2. Put the following string into the "Test String" box
```
The cat in the hat was late, and that was a great surprise
```
3. Enter the pattern string `cat` into the "Regular expression" box, like this:
____
4. At this point, your regex101 window should look like this (see annotations or details)

5. Notice that the _cat_ string is highlighted in the text--**this shows what part of the text matched your regex**! If you don't see `cat` highlighted, make sure your regex101 window looks like the figure above (make sure "Python" and "Match" are checked at the left). If you still have issues, call over a TA for help.
</section>
<section>
### Task 11
Now let’s start building up patterns in which not all characters are fixed.
Let’s say we want look for every 3 letter word in a body of text that contains “at” (e.g. “cat”, “bat”, “sat”, etc). For this, we can use `[a-z]at`. The brackets create a set of characters to search for, and the a-z gives a range of every character between a and z in the ASCII character list.
Put this new regex into the site, and see what matches.
</section>
<section>
### Task 12
Now instead, suppose we want every single word ending in “at”, regardless of length (e.g. “at”, “bat”, “seat”, etc). For this, we can use a _quantifier_--the most common (which you may have seen before, outside of regexes) is `*`.
The `*` quantifier says that there can be **any number, including 0, of the character preceding it** in the string it finds. This means `[a-z]*at` will do what we want: find every string consisting of some number of letters followed by “at”.
Put this new regex into the site, and see what matches.
</section>
<section>
### Task 13
If we want to exclude “at” and only find words with 3 or more letters, we could instead use `[a-z]+at` instead. The `+` quantifier requires there be **one or more** a-z characters.
Put this new regex into the site, and see what matches.
</section>
This regex still has some issues though. For example, if we wanted to use it to find all of the words ending in “at”, we see (by the highlights in the website) that it matches “lat” (as part of “late”). We need some way to specify that the “at” actually be at the end of the word. For this, we can use a “word boundary,” which requires that there be “word characters” (letters, numbers or underscores) on only one side of the boundary. This is represented as `\b` in the regex syntax.
<section>
### Task 14
So our new regex, `[a-z]*at\b`, will match “at”, “cat”, “seat”, and “great”, but not “ate”, “late”, “bat9”, or “at_home`.
Put this new regex into the site, and see what matches.
</section>
If we want to avoid matches like “seat” or “great”, we could add a word boundary to the beginning as well, for a final regex of `\b[a-z]at\b`.
> Tip: `[a-z]` matches lowercase ASCII letters only. To be case-insensitive on regex101, enable the **i** flag or use `[A-Za-z]`.
### More challenging examples
Now that you’ve seen the basics of character ranges, quantifiers, and boundaries, we can build up to more interesting patterns. The bottom of our course [regex guide](https://docs.cs200.io/regex-guide) shows other characters you can use in regexes (to match things like spaces, sets of characters, and more).
<section>
### Task 15
Craft regexes to match on the following criteria using [regex101.com](https://regex101.com) (modify your text string as needed to check your work):
- All space separated words.
- All words that start with a capital letter.
- Any capitalization of the word train (i.e., TRAIN, train, tRAIn, TrAiN, etc,)
- All words of length 4 (i.e., 4 characters)
- All words inside quotes (Hint: think escaped characters!)
</section>
### Regexes in Python
Now we’re going to use regexes in Python. Here's an example of code that would extract a list of all of the courses (like CSCI0200) from a string representing a student transcript (we generated this example using an LLM):
```python
import re
def find_course_codes(text: str) -> list[str]:
# 4 letters + 4 digits, word boundaries
pattern = re.compile(r"\b[A-Za-z]{4}\d{4}\b")
return pattern.findall(text)
if __name__ == "__main__":
sample = "I took CSCI0200 and MATH1000; next is DATA1050."
print("Matches:", find_course_codes(sample)) # ['CSCI0200', 'MATH1000', 'DATA1050']
```
<section>
### Task 16
Read through this generated code, annotating what each part does. What can you infer about the `re` module’s functions from this example (e.g., what does `re.compile` return, how do `findall` vs `finditer` vs `search` differ)? If you try `finditer`, inspect each match’s `group()` and `span()`.
</section>
<section>
### Task 17
Using this code as a starting point, add a method named `extract_gpa` to your `Admissions` class that extracts the GPA from a string containing a transcript. Assume that the GPA will be given in one of the following two formats within the string:
- GPA: 3.6
- GPA 3.6
A Pythonic reference implementation:
```python
import re
from typing import Optional
class Admissions:
@staticmethod
def extract_gpa(transcript_text: str) -> Optional[float]:
# case-insensitive 'GPA', optional ':' or space, number 0.00–4.00 (1–2 decimals allowed)
m = re.search(r"(?i)\bGPA\b\s*[: ]\s*([0-4](?:\.\d{1,2})?)", transcript_text)
return float(m.group(1)) if m else None
```
</section>
:::success
**Checkpoint!** Congratulations! You completed the optional section! Kudos from this lab's creators Kathi and Elijah w/ Bohdan and Kevin, March 2025
:::
## **💡Applications💡**
- **Matrix Operations:** Core to graphics (image transforms), machine learning (data pipelines), and scientific computing.
- **Filesystem Interaction:** Automate tasks like backups, log processing, and batch file conversions by navigating directories in code.
- **Data Structuring:** Choosing the right structure (hashmaps, labeled matrices) boosts performance in databases, search engines, and analytics.
- **Pattern Matching:** Regex powers log parsing, form validation, and automated text extraction in real-world data-processing scripts.
- **Flexible Design:** Passing functions and leveraging Python I/O lets you build configurable tools—key for plugins, ETL jobs, and microservices.
***
_Please let us know if you find any mistakes, inconsistencies, or confusing language in this or any other CSCI0200 document by filling out the_ [_anonymous feedback form_](https://docs.google.com/forms/d/e/1FAIpQLScCb0F4UsIOLZSr0xqPmdgDDWE4afNHXqnzSiyoK38Ce0C3vg/viewform)_._