# Set up packages for lecture. Don't worry about understanding this code, but
# make sure to run it if you're following along.
import numpy as np
import babypandas as bpd
import pandas as pd
from matplotlib_inline.backend_inline import set_matplotlib_formats
import matplotlib.pyplot as plt
%reload_ext pandas_tutor
%set_pandas_tutor_options {'projectorMode': True}
set_matplotlib_formats("svg")
plt.style.use('ggplot')
np.set_printoptions(threshold=20, precision=2, suppress=True)
pd.set_option("display.max_rows", 7)
pd.set_option("display.max_columns", 8)
pd.set_option("display.precision", 2)
from IPython.display import display, IFrame
def show_def():
src = "https://docs.google.com/presentation/d/e/2PACX-1vRKMMwGtrQOeLefj31fCtmbNOaJuKY32eBz1VwHi_5ui0AGYV3MoCjPUtQ_4SB1f9x4Iu6gbH0vFvmB/embed?start=false&loop=false&delayms=60000"
width = 960
height = 569
display(IFrame(src, width, height))
Reminder: Use the DSC 10 Reference Sheet. You can also use it on exams!
max
, np.sqrt
, len
) and methods (e.g. .groupby
, .assign
, .plot
). Suppose you drive to a restaurant 🥘 in LA, located exactly 100 miles away.
In segment 1, when you drove 50 miles at 80 miles per hour, you drove for $\frac{50}{80}$ hours:
$$\text{speed}_1 = \frac{\text{distance}_1}{\text{time}_1}$$Similarly, in segment 2, when you drove 50 miles at 60 miles per hour, you drove for $\text{time}_2 = \frac{50}{60} \text{ hours}$.
Then,
$$\text{average speed} = \frac{50 + 50}{\frac{50}{80} + \frac{50}{60}} \text{ miles per hour} $$The harmonic mean ($\text{HM}$) of two positive numbers, $a$ and $b$, is defined as
$$\text{HM} = \frac{2}{\frac{1}{a} + \frac{1}{b}}$$It is often used to find the average of multiple rates.
Finding the harmonic mean of 80 and 60 is not hard:
2 / (1 / 80 + 1 / 60)
68.57142857142857
But what if we want to find the harmonic mean of 80 and 70? 80 and 90? 20 and 40? This would require a lot of copy-pasting, which is prone to error.
It turns out that we can define our own "harmonic mean" function just once, and re-use it multiple times.
def harmonic_mean(a, b):
return 2 / (1 / a + 1 / b)
harmonic_mean(80, 60)
68.57142857142857
harmonic_mean(20, 40)
26.666666666666664
Note that we only had to specify how to calculate the harmonic mean once!
Functions are a way to divide our code into small subparts to prevent us from writing repetitive code. Each time we define our own function in Python, we will use the following pattern.
show_def()
harmonic_mean(20, 40)
26.666666666666664
harmonic_mean(79, 894)
145.17163412127442
harmonic_mean(-2, 4)
-8.0
triple
has one parameter, x
.
def triple(x):
return x * 3
When we call triple
with the argument 5, you can pretend that there's an invisible first line in the body of triple
that says x = 5
.
triple(5)
15
Note that arguments can be of any type!
triple('triton')
'tritontritontriton'
Functions can have any number of arguments. So far, we've created a function that takes two arguments – harmonic_mean
– and a function that takes one argument – triple
.
greeting
takes no arguments!
def greeting():
return 'Hi! 👋'
greeting()
'Hi! 👋'
The body of a function is not run until you use (call) the function.
Here, we can define where_is_the_error
without seeing an error message.
def where_is_the_error(something):
'''You can describe your function within triple quotes. For example, this function
illustrates that errors don't occur until functions are executed (called).'''
return (1 / 0) + something
It is only when we call where_is_the_error
that Python gives us an error message.
where_is_the_error(5)
--------------------------------------------------------------------------- ZeroDivisionError Traceback (most recent call last) /var/folders/pd/w73mdrsj2836_7gp0brr2q7r0000gn/T/ipykernel_71400/3423408763.py in <module> ----> 1 where_is_the_error(5) /var/folders/pd/w73mdrsj2836_7gp0brr2q7r0000gn/T/ipykernel_71400/1703529954.py in where_is_the_error(something) 2 '''You can describe your function within triple quotes. For example, this function 3 illustrates that errors don't occur until functions are executed (called).''' ----> 4 return (1 / 0) + something ZeroDivisionError: division by zero
first_name
¶Let's create a function called first_name
that takes in someone's full name and returns their first name. Example behavior is shown below.
>>> first_name('Pradeep Khosla')
'Pradeep'
Hint: Use the string method .split
.
General strategy for writing functions:
'Pradeep Khosla'.split(' ')[0]
'Pradeep'
def first_name(full_name):
'''Returns the first name given a full name.'''
return full_name.split(' ')[0]
first_name('Pradeep Khosla')
'Pradeep'
# What if there are three names?
first_name('Chancellor Pradeep Khosla')
'Chancellor'
return
keyword specifies what the output of your function should be, i.e. what a call to your function will evaluate to.return
, but using return
is not required.print
and return
work differently!def pythagorean(a, b):
'''Computes the hypotenuse length of a triangle with legs a and b.'''
c = (a ** 2 + b ** 2) ** 0.5
print(c)
x = pythagorean(3, 4)
5.0
# No output – why?
x
# Errors – why?
x + 10
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) /var/folders/pd/w73mdrsj2836_7gp0brr2q7r0000gn/T/ipykernel_71400/3707561498.py in <module> 1 # Errors – why? ----> 2 x + 10 TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
def better_pythagorean(a, b):
'''Computes the hypotenuse length of a triangle with legs a and b, and actually returns the result.'''
c = (a ** 2 + b ** 2) ** 0.5
return c
x = better_pythagorean(3, 4)
x
5.0
x + 10
15.0
Once a function executes a return
statement, it stops running.
def motivational(quote):
return 0
print("Here's a motivational quote:", quote)
motivational('Fall seven times and stand up eight.')
0
The names you choose for a function’s parameters are only known to that function (known as local scope). The rest of your notebook is unaffected by parameter names.
def what_is_awesome(s):
return s + ' is awesome!'
what_is_awesome('data science')
'data science is awesome!'
s
--------------------------------------------------------------------------- NameError Traceback (most recent call last) /var/folders/pd/w73mdrsj2836_7gp0brr2q7r0000gn/T/ipykernel_71400/1028141915.py in <module> ----> 1 s NameError: name 's' is not defined
s = 'DSC 10'
what_is_awesome('data science')
'data science is awesome!'
The DataFrame roster
contains the names and lecture sections of all students enrolled in DSC 10 this quarter. The first names are real, while the last names have been anonymized for privacy.
roster = bpd.read_csv('data/roster-anon.csv')
roster
name | section | |
---|---|---|
0 | Levy Dmxsqj | 11AM |
1 | Aiden Nyozzx | 1PM |
2 | Sruti Fivolq | 12PM |
... | ... | ... |
408 | Leni Hlfjhh | 11AM |
409 | Dory Xaghsk | 1PM |
410 | Laura Xfqwzu | 11AM |
411 rows × 2 columns
What is the most common first name among DSC 10 students? (Any guesses?)
roster
name | section | |
---|---|---|
0 | Levy Dmxsqj | 11AM |
1 | Aiden Nyozzx | 1PM |
2 | Sruti Fivolq | 12PM |
... | ... | ... |
408 | Leni Hlfjhh | 11AM |
409 | Dory Xaghsk | 1PM |
410 | Laura Xfqwzu | 11AM |
411 rows × 2 columns
'name'
column.first_name
function¶Somehow, we need to call first_name
on every student's 'name'
.
roster
name | section | |
---|---|---|
0 | Levy Dmxsqj | 11AM |
1 | Aiden Nyozzx | 1PM |
2 | Sruti Fivolq | 12PM |
... | ... | ... |
408 | Leni Hlfjhh | 11AM |
409 | Dory Xaghsk | 1PM |
410 | Laura Xfqwzu | 11AM |
411 rows × 2 columns
roster.get('name').iloc[0]
'Levy Dmxsqj'
first_name(roster.get('name').iloc[0])
'Levy'
first_name(roster.get('name').iloc[1])
'Aiden'
Ideally, there's a better solution than doing this 411 times...
.apply
¶column_name
in DataFrame df
, usedf.get(column_name).apply(function_name)
.apply
method is a Series method..apply
on Series, not DataFrames..apply
is also a Series..apply(first_name)
..apply(first_name())
.roster.get('name').apply(first_name)
0 Levy 1 Aiden 2 Sruti ... 408 Leni 409 Dory 410 Laura Name: name, Length: 411, dtype: object
%%pt
roster.get('name').apply(first_name)
with_first = roster.assign(
first=roster.get('name').apply(first_name)
)
with_first
name | section | first | |
---|---|---|---|
0 | Levy Dmxsqj | 11AM | Levy |
1 | Aiden Nyozzx | 1PM | Aiden |
2 | Sruti Fivolq | 12PM | Sruti |
... | ... | ... | ... |
408 | Leni Hlfjhh | 11AM | Leni |
409 | Dory Xaghsk | 1PM | Dory |
410 | Laura Xfqwzu | 11AM | Laura |
411 rows × 3 columns
first_counts = with_first.groupby('first').count().sort_values('name', ascending=False).get(['name'])
first_counts
name | |
---|---|
first | |
Ethan | 5 |
Steven | 4 |
Jason | 4 |
... | ... |
Huanchang | 1 |
Housheng | 1 |
Zoya | 1 |
361 rows × 1 columns
Below:
.apply
works with built-in functions, too!¶For instance, to find the length of each name, we might use the len
function:
with_first
name | section | first | |
---|---|---|---|
0 | Levy Dmxsqj | 11AM | Levy |
1 | Aiden Nyozzx | 1PM | Aiden |
2 | Sruti Fivolq | 12PM | Sruti |
... | ... | ... | ... |
408 | Leni Hlfjhh | 11AM | Leni |
409 | Dory Xaghsk | 1PM | Dory |
410 | Laura Xfqwzu | 11AM | Laura |
411 rows × 3 columns
with_first.get('first').apply(len)
0 4 1 5 2 5 .. 408 4 409 4 410 5 Name: first, Length: 411, dtype: int64
We were able to apply first_name
to the 'name'
column because it's a Series. The .apply
method doesn't work on the index, because the index is not a Series.
indexed_by_name = roster.set_index('name')
indexed_by_name
section | |
---|---|
name | |
Levy Dmxsqj | 11AM |
Aiden Nyozzx | 1PM |
Sruti Fivolq | 12PM |
... | ... |
Leni Hlfjhh | 11AM |
Dory Xaghsk | 1PM |
Laura Xfqwzu | 11AM |
411 rows × 1 columns
indexed_by_name.index.apply(first_name)
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) /var/folders/pd/w73mdrsj2836_7gp0brr2q7r0000gn/T/ipykernel_71400/1621495788.py in <module> ----> 1 indexed_by_name.index.apply(first_name) AttributeError: 'Index' object has no attribute 'apply'
.reset_index()
¶Use .reset_index()
to turn the index of a DataFrame into a column, and to reset the index back to the default of 0, 1, 2, 3, and so on.
indexed_by_name.reset_index()
name | section | |
---|---|---|
0 | Levy Dmxsqj | 11AM |
1 | Aiden Nyozzx | 1PM |
2 | Sruti Fivolq | 12PM |
... | ... | ... |
408 | Leni Hlfjhh | 11AM |
409 | Dory Xaghsk | 1PM |
410 | Laura Xfqwzu | 11AM |
411 rows × 2 columns
indexed_by_name.reset_index().get('name').apply(first_name)
0 Levy 1 Aiden 2 Sruti ... 408 Leni 409 Dory 410 Laura Name: name, Length: 411, dtype: object
with_first
name | section | first | |
---|---|---|---|
0 | Levy Dmxsqj | 11AM | Levy |
1 | Aiden Nyozzx | 1PM | Aiden |
2 | Sruti Fivolq | 12PM | Sruti |
... | ... | ... | ... |
408 | Leni Hlfjhh | 11AM | Leni |
409 | Dory Xaghsk | 1PM | Dory |
410 | Laura Xfqwzu | 11AM | Laura |
411 rows × 3 columns
For example, maybe 'Ryan Ufhwdl'
wants to see if there's another 'Ryan'
in their section.
Strategy:
'Ryan Ufhwdl'
in?'Ryan'
?what_section = with_first[with_first.get('name') == 'Ryan Ufhwdl'].get('section').iloc[0]
what_section
'1PM'
how_many = with_first[(with_first.get('section') == what_section) & (with_first.get('first') == 'Ryan')].shape[0]
how_many
2
shared_first_and_section
¶Let's create a function named shared_first_and_section
. It will take in the full name of a student and return the number of students in their section with the same first name and section (including them).
Note: This is the first function we're writing that involves using a DataFrame within the function – this is fine!
def shared_first_and_section(name):
# First, find the row corresponding to that full name in with_first.
# We're assuming that full names are unique.
row = with_first[with_first.get('name') == name]
# Then, get that student's first name and section.
first = row.get('first').iloc[0]
section = row.get('section').iloc[0]
# Now, find all the students with the same first name and section.
shared_info = with_first[(with_first.get('first') == first) & (with_first.get('section') == section)]
# Return the number of such students.
return shared_info.shape[0]
shared_first_and_section('Ryan Ufhwdl')
2
shared_first_and_section('Dory Xaghsk')
1
Now, let's add a column to with_first
that contains the values returned by shared_first_and_section
.
with_first = with_first.assign(shared=with_first.get('name').apply(shared_first_and_section))
with_first
name | section | first | shared | |
---|---|---|---|---|
0 | Levy Dmxsqj | 11AM | Levy | 1 |
1 | Aiden Nyozzx | 1PM | Aiden | 1 |
2 | Sruti Fivolq | 12PM | Sruti | 1 |
... | ... | ... | ... | ... |
408 | Leni Hlfjhh | 11AM | Leni | 1 |
409 | Dory Xaghsk | 1PM | Dory | 1 |
410 | Laura Xfqwzu | 11AM | Laura | 1 |
411 rows × 4 columns
Let's look at all the students who are in a section with someone that has the same first name as them.
with_first[(with_first.get('shared') > 1)].sort_values('shared', ascending=False)
name | section | first | shared | |
---|---|---|---|---|
39 | Ethan Dpcred | 1PM | Ethan | 3 |
352 | Andrew Aspfmf | 10AM | Andrew | 3 |
80 | Samuel Vwwdmu | 1PM | Samuel | 3 |
... | ... | ... | ... | ... |
82 | Ryan Ufhwdl | 1PM | Ryan | 2 |
40 | Justin Plbevg | 11AM | Justin | 2 |
375 | Kevin Sgywid | 10AM | Kevin | 2 |
33 rows × 4 columns
We can narrow this down to a particular lecture section if we'd like.
one_section_only = with_first[(with_first.get('shared') > 1) &
(with_first.get('section') == '10AM')].sort_values('shared', ascending=False)
one_section_only
name | section | first | shared | |
---|---|---|---|---|
88 | Andrew Qgvdmn | 10AM | Andrew | 3 |
117 | Andrew Klhlht | 10AM | Andrew | 3 |
352 | Andrew Aspfmf | 10AM | Andrew | 3 |
28 | Kevin Wphdws | 10AM | Kevin | 2 |
375 | Kevin Sgywid | 10AM | Kevin | 2 |
one_section_only.get('first').unique()
array(['Andrew', 'Kevin'], dtype=object)
While the DataFrames on the previous slide contain the info we were looking for, they're not organized very conveniently. For instance, there are three rows containing the fact that there are 3 'Andrew'
s in the 10AM lecture section.
Wouldn't it be great if we could create a DataFrame like the one below? We'll see how on Friday!
section | first | count | |
---|---|---|---|
0 | 10AM | Andrew | 3 |
1 | 1PM | Ethan | 3 |
2 | 1PM | Samuel | 3 |
3 | 10AM | Kevin | 2 |
4 | 11AM | Connor | 2 |
Find the longest first name in the class that is shared by at least two students in the same section.
Hint: You'll have to use both assign
and apply
.
...
Ellipsis
.apply
method allows us to call a function on every single element of a Series, which usually comes from .get
ting a column of a DataFrame.More advanced DataFrame manipulations!