Code
import pandas as pd
url = 'https://eds-217-essential-python.github.io/data/marine_microplastics.csv'
plastics = pd.read_csv(url)
plastics.shape(16245, 22)
βοΈ Write It Once, Name It, Use It

A lightbulb, in the middle of having an idea. MidJourney 5
In yesterdayβs end-of-day activity, you wrote this line (or something similar!) three times, changing just a single word each time:
Three is something of a magic number in programming: the third time you write something very similar (e.g. that differs by only a single variable), you probably want to start thinking about how to refactor your code to make it more efficient. In addition, repetition of code invites more typos and errors. Copying a line and editing one word is how a good number of data-analysis bugs get written: you change the original, forget to update some of the copies, and now your notebook contains two different answers to the same basic question.
A function is the cure to the tedium and risk of a copy-and-paste lifestyle. You write the work once, give it a name, and then you have a new tool that you and pandas can both use.
By the end of this session you will be able to:
def and hand a value back with return.apply()Create the file. In the Explorer, hover over the EDS217 heading and click New Fileβ¦, then type the name in full, extension included: Session_4B_Functions.ipynb
Check the kernel. The Kernel Selector in the notebookβs action bar should read Python 3.11.15 (Conda: eds217). If it reads anything else, click it, choose Change Kernel, and pick the eds217 entry.
Add a title cell. Click + Markdown in the action bar, and give it this content, with todayβs date:
# Day 4: Session 4B - Write It Once, Name It, Use It
[Session Webpage](https://eds-217-essential-python.github.io/course-materials/interactive-sessions/4b_functions.html)
Date: 09/03/2026Save with Ctrl + S (Cmd + S on macOS), and keep saving as you go.
Read in the microplastics data again, raw. We want the file in the state it arrives in, because the exercise at the end of this session checks your answers against the missing-value counts you ran in 4A, and cleaning it first would turn all of those into zero:
def and returnHere is a whole function:
Four pieces, and they are always in this order:
def announces that a definition is coming.celsius_to_fahrenheit is the name. You choose it, and you should choose it well.(celsius) is the parameter: a name for the value that will be handed in.def line is the body, and return says what comes back.Running that cell does not convert anything. It teaches Python a new word. To use it, call it:
The indentation is not decoration. Python uses it to mark where the body ends, exactly the way it marked which lines belonged to an if block yesterday.
Write a function called meters_to_feet that takes a number of metres and returns the same distance in feet. One metre is 3.28084 feet. Call it on 1000 and check that you get about 3,281.
The name is most of the value. Compare:
They compute the same thing. Only one of them will still make sense to you in November.
The line in triple quotes is a docstring. It is optional, it goes on the first line of the body, and Python will show it back to you when you ask for help:
Help on function ppm_to_ppb in module __main__:
ppm_to_ppb(concentration_ppm)
Convert a concentration from parts per million to parts per billion.
Write one whenever the functionβs name does not fully explain itself.
A parameter can have a default, which is used when the caller does not supply that argument:
Notice what is inside that function: the if/elif/else we learned yesterday, doing exactly what it did yesterday. Functions are not a new kind of code, just a wrapper that lets you name code you already know how to write.
Python needs to match the values you pass to the parameters in order, so every parameter with a default has to come after every parameter without one. The error message says non-default argument follows default argument, which is worth recognizing on sight.
When you call a function, you can name the arguments instead of relying on their order:
Both give the same answer, because naming the arguments makes their order irrelevant. Keyword arguments are something we have been using all week without stopping to name them:
pd.read_csv(url, index_col='entity')
df.sort_values('visitors', ascending=False)
df.dropna(subset=['Measurement'])index_col=, ascending= and subset= are keyword arguments to somebody elseβs functions. Now you know what the equals sign was doing.
Write a function classify_latitude that takes a latitude and returns 'tropical' if it is between -23.5 and 23.5, 'polar' if it is below -66.5 or above 66.5, and 'temperate' otherwise. Test it on 0, 45, and -70.
Write your own version before you read on. The next section defines the same function, so compare the two rather than letting ours replace yours.
.apply()You now have functions that work on one number. Your data has 16,245 numbers in a column.
.apply() runs a function once for every value in a column and gives you back a column of the results:
0 -58.428300
1 -51.308200
2 -51.826667
3 -31.696000
4 6.350000
Name: Latitude, dtype: float64
0 temperate
1 temperate
2 temperate
3 temperate
4 tropical
Name: Latitude, dtype: object
Read the call carefully, because what goes inside the parentheses is easy to get wrong:
The functionβs name goes inside the parentheses, with no parentheses of its own. You are handing pandas the function itself, not the result of calling it. Add parentheses by mistake and Python will try to call your function with no arguments, and raise a TypeError about a missing argument.
.apply() follows the same rule as almost everything else in pandas: it hands back a new column and leaves the original table alone. Assign it to keep it:
zone
temperate 12275
tropical 3914
polar 56
Name: count, dtype: int64
The zone counts are a real result. Three quarters of this archive was sampled in temperate water, and 56 samples out of 16,245 came from the poles, which probably tells you more about where the research ships went than about where the plastic is.
π .apply() hangs off a column, like .isin() and .fillna() do. The function you hand it should take one value and return one value. Pandas takes care of the repetition.
Write a function hemisphere that returns 'northern' when a latitude is above zero and 'southern' otherwise. Apply it to plastics['Latitude'], store the result in a new column called hemisphere, and use .value_counts() to report the split.
Here is yesterdayβs problem, solved. The top-N pattern, written once:
| Measurement | Unit | Oceans | |
|---|---|---|---|
| 12590 | 110480.0 | pieces/m3 | Atlantic Ocean |
| 14159 | 103120.0 | pieces/m3 | Atlantic Ocean |
| 15428 | 85600.0 | pieces/m3 | Atlantic Ocean |
| 6622 | 60160.0 | pieces/m3 | Atlantic Ocean |
| 248 | 57680.0 | pieces/m3 | Atlantic Ocean |
| Latitude | Oceans | zone | |
|---|---|---|---|
| 2444 | 88.9613 | Arctic Ocean | polar |
| 15431 | 88.7435 | Arctic Ocean | polar |
| 8603 | 88.6493 | Arctic Ocean | polar |
| 8912 | 88.6113 | Arctic Ocean | polar |
| 14983 | 88.6113 | Arctic Ocean | polar |
| 3010 | 88.6113 | Arctic Ocean | polar |
| 4142 | 88.6113 | Arctic Ocean | polar |
| 15760 | 88.6113 | Arctic Ocean | polar |
| 10182 | 88.6113 | Arctic Ocean | polar |
| 9489 | 88.6097 | Arctic Ocean | polar |
| Longitude | Oceans | zone | |
|---|---|---|---|
| 880 | 5.596789 | Atlantic Ocean | tropical |
| 15044 | 5.475000 | Atlantic Ocean | temperate |
| 1069 | 5.450600 | Atlantic Ocean | temperate |
Two parameters without defaults, one with, and three calls to a single definition. The three rankings you wrote out longhand in yesterdayβs practice are three lines like these now, and changing your mind about how ranking should work means editing one line instead of three.
Note that top_n hands back a whole table rather than a single labelled column, which is the whole-table form of the two you met yesterday, the one written df.sort_values(...). To get the single-column form back instead, put the column selection inside the function: return df.sort_values(column, ascending=False).head(n)[column].
Write a function count_missing(df, column) that returns the number of missing values in a named column. Use .isnull().sum() inside it. Call it on 'Measurement', 'Oceans', and 'SubRegions', and check your three answers against the first .isnull().sum() you ran in 4A, the one on the raw file, before any cleaning. You should get 5,792, 271 and 15,657.
def name(parameter): opens a function; the indented body is what it does; return is what it hands back. No return means it hands back None.df['col'].apply(function_name) runs your function down a whole column. Pass the name, with no parentheses of its own.