In [2]:
from dsc80_utils import *

Lecture 4 – Simpson's Paradox, Joining, and Transforming¶

DSC 80, Summer 2026¶

Announcements 📣¶

  • Lab 2 due Friday, Project 1 due Saturday.
  • Please come to office hours for help! Find the schedule here and the Zoom link on Canvas.

Agenda¶

  • Pivot tables.
  • Distributions.
  • Simpson's paradox.
  • Merging.
    • Many-to-one & many-to-many joins.
  • Transforming.
    • The price of apply.

Question 🤔

Find the most popular Male and Female baby Name for each Year in baby. Exclude Years where there were fewer than 1 million births recorded.

In [3]:
baby_path = Path('data') / 'baby.csv'
baby = pd.read_csv(baby_path)
baby
Out[3]:
Name Sex Count Year
0 Liam M 20456 2022
1 Noah M 18621 2022
2 Olivia F 16573 2022
... ... ... ... ...
2085155 Wright M 5 1880
2085156 York M 5 1880
2085157 Zachariah M 5 1880

2085158 rows × 4 columns

In [4]:
# approach 1
In [5]:
# approach 2

Pivot tables¶

Pivot tables: an extension of grouping¶

Pivot tables are a compact way to display tables for humans to read:

Sex F M
Year
2018 1698373 1813377
2019 1675139 1790682
2020 1612393 1721588
2021 1635800 1743913
2022 1628730 1733166
  • Notice that each value in the table is a sum over the counts, split by year and sex.
  • You can think of pivot tables as grouping using two columns, then "pivoting" one of the group labels into columns.

pivot_table¶

The pivot_table (not pivot!) DataFrame method aggregates a DataFrame using two columns. To use it:

df.pivot_table(index=index_col,
               columns=columns_col,
               values=values_col,
               aggfunc=func)

The resulting DataFrame will have:

  • One row for every unique value in index_col.
  • One column for every unique value in columns_col.
  • Values determined by applying func on values in values_col.
In [6]:
last_5_years = baby.query('Year >= 2018')
last_5_years
Out[6]:
Name Sex Count Year
0 Liam M 20456 2022
1 Noah M 18621 2022
2 Olivia F 16573 2022
... ... ... ... ...
159444 Zyrie M 5 2018
159445 Zyron M 5 2018
159446 Zzyzx M 5 2018

159447 rows × 4 columns

In [7]:
last_5_years.pivot_table(
    index='Year',
    columns='Sex',
    values='Count',
    aggfunc='sum',
)
Out[7]:
Sex F M
Year
2018 1698373 1813377
2019 1675139 1790682
2020 1612393 1721588
2021 1635800 1743913
2022 1628730 1733166
In [8]:
# Look at the similarity to the snippet above!
(last_5_years
 .groupby(['Year', 'Sex'])
 [['Count']]
 .sum()
)
Out[8]:
Count
Year Sex
2018 F 1698373
M 1813377
2019 F 1675139
... ... ...
2021 M 1743913
2022 F 1628730
M 1733166

10 rows × 1 columns

Question 🤔

Use .pivot_table to find the number of penguins per 'island' and 'species'.

In [9]:
penguins = sns.load_dataset('penguins').dropna()
penguins
Out[9]:
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 Male
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 Female
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 Female
... ... ... ... ... ... ... ...
341 Gentoo Biscoe 50.4 15.7 222.0 5750.0 Male
342 Gentoo Biscoe 45.2 14.8 212.0 5200.0 Female
343 Gentoo Biscoe 49.9 16.1 213.0 5400.0 Male

333 rows × 7 columns

In [ ]:
 

Note that there is a NaN at the intersection of 'Biscoe' and 'Chinstrap', because there were no Chinstrap penguins on Biscoe Island.

We can either use the fillna method afterwards or the fill_value argument to fill in NaNs.

In [ ]:
 

Distributions¶

Example: Penguins¶

Let's start by using the pivot_table method to recreate the DataFrame shown below.

sex Female Male
species
Adelie 73 73
Chinstrap 34 34
Gentoo 58 61

Joint distribution¶

When using aggfunc='count', a pivot table describes the joint distribution of two categorical variables. This is also called a contingency table.

In [10]:
counts = penguins.pivot_table(
    index='species',
    columns='sex',
    values='body_mass_g',
    aggfunc='count',
    fill_value=0,
)
counts
Out[10]:
sex Female Male
species
Adelie 73 73
Chinstrap 34 34
Gentoo 58 61

We can normalize the DataFrame by dividing by the total number of penguins. The resulting numbers can be interpreted as probabilities that a randomly selected penguin from the dataset belongs to a given combination of species and sex.

In [11]:
# How to calculate the total number of penguins?
In [ ]:
 

Marginal probabilities¶

If we sum over one of the axes, we can compute marginal probabilities, i.e. unconditional probabilities.

In [12]:
joint = counts/counts.sum().sum()
joint
Out[12]:
sex Female Male
species
Adelie 0.22 0.22
Chinstrap 0.10 0.10
Gentoo 0.17 0.18
In [13]:
# .sum(axis=0) sums by compressing rows, which computes column sums
joint.sum(axis=0)
Out[13]:
sex
Female    0.5
Male      0.5
dtype: float64
In [14]:
joint.sum(axis=1)
Out[14]:
species
Adelie       0.44
Chinstrap    0.20
Gentoo       0.36
dtype: float64

For instance, the second Series tells us that a randomly selected penguin has a 0.36 chance of being of species 'Gentoo'.

Conditional probabilities¶

Using counts, how might we compute conditional probabilities like $$P(\text{species } = \text{Adelie} \mid \text{sex } = \text{Female})?$$

In [15]:
counts
Out[15]:
sex Female Male
species
Adelie 73 73
Chinstrap 34 34
Gentoo 58 61

$$\begin{align*} P(\text{species} = c \mid \text{sex} = x) &= \frac{\# \: (\text{species} = c \text{ and } \text{sex} = x)}{\# \: (\text{sex} = x)} \end{align*}$$

➡️ Click here to see more of a derivation. $$\begin{align*} P(\text{species} = c \mid \text{sex} = x) &= \frac{P(\text{species} = c \text{ and } \text{sex} = x)}{P(\text{sex = }x)} \\ &= \frac{\frac{\# \: (\text{species } = \: c \text{ and } \text{sex } = \: x)}{N}}{\frac{\# \: (\text{sex } = \: x)}{N}} \\ &= \frac{\# \: (\text{species} = c \text{ and } \text{sex} = x)}{\# \: (\text{sex} = x)} \end{align*}$$

Conditional probabilities¶

To find conditional probabilities of 'species' given 'sex', divide by column sums. To find conditional probabilities of 'sex' given 'species', divide by row sums.

In [16]:
counts.sum(axis=0)
Out[16]:
sex
Female    165
Male      168
dtype: int64

The conditional distribution of 'species' given 'sex' is below. Note that in this new DataFrame, the 'Female' and 'Male' columns each sum to 1.

In [17]:
counts / counts.sum(axis=0)
Out[17]:
sex Female Male
species
Adelie 0.44 0.43
Chinstrap 0.21 0.20
Gentoo 0.35 0.36

For instance, the above DataFrame tells us that the probability that a randomly selected penguin is of 'species' 'Adelie' given that they are of 'sex' 'Female' is 0.442424.

Question 🤔

Find the conditional distribution of 'sex' given 'species'.

Hint: Use .T.

In [ ]:
 

Simpson's paradox¶


No description has been provided for this image

Example: Grades¶

  • Two students, Lisa and Bart, just finished their first year at UCSD. They both took a different number of classes in Fall, Winter, and Spring.

  • Each quarter, Lisa had a higher GPA than Bart.

  • But Bart has a higher overall GPA.

  • How is this possible? 🤔

Grade points¶

The number of "grade points" earned for a course is:

$$\text{grade points} = \text{number of units} \cdot \text{grade point per unit (out of 4)}$$

No description has been provided for this image

Run this cell to create a DataFrame showing Lisa and Bart's grades.

In [18]:
simpsons = pd.DataFrame({'Lisa Units': [20, 18, 5],
                        'Lisa Grade Points': [46.0, 54.0, 20.0],
                        'Lisa GPA': [46/20, 54/18, 20/5],
                        'Bart Units': [5, 5, 22],
                        'Bart Grade Points': [10, 13.5, 82.5],
                        'Bart GPA': [10/5, 13.5/5, 82.5/22],
                        },                        
                        index=['Fall', 'Winter', 'Spring'])
simpsons
Out[18]:
Lisa Units Lisa Grade Points Lisa GPA Bart Units Bart Grade Points Bart GPA
Fall 20 46.0 2.3 5 10.0 2.00
Winter 18 54.0 3.0 5 13.5 2.70
Spring 5 20.0 4.0 22 82.5 3.75

Lisa had a higher GPA in all three quarters.

Question 🤔

Compute Lisa's overall GPA and Bart's overall GPA.

In [19]:
simpsons
Out[19]:
Lisa Units Lisa Grade Points Lisa GPA Bart Units Bart Grade Points Bart GPA
Fall 20 46.0 2.3 5 10.0 2.00
Winter 18 54.0 3.0 5 13.5 2.70
Spring 5 20.0 4.0 22 82.5 3.75
In [ ]:
 
In [ ]:
 

What happened?¶

In [20]:
simpsons
Out[20]:
Lisa Units Lisa Grade Points Lisa GPA Bart Units Bart Grade Points Bart GPA
Fall 20 46.0 2.3 5 10.0 2.00
Winter 18 54.0 3.0 5 13.5 2.70
Spring 5 20.0 4.0 22 82.5 3.75
  • When Lisa and Bart both performed poorly, Lisa took more units than Bart. This brought down 📉 Lisa's overall average.

  • When Lisa and Bart both performed well, Bart took more units than Lisa. This brought up 📈 Bart's overall average.

Simpson's paradox¶

  • Simpson's paradox occurs when grouped data and ungrouped data show opposing trends.

    • It is named after Edward H. Simpson, not Lisa or Bart Simpson.
  • It often happens because there is a hidden factor (i.e. a confounder) within the data that influences results.

  • Question: What is the "correct" way to summarize your data? What if you had to act on these results?

Example: How Berkeley was almost sued for gender discrimination (1973)¶

What do you notice?

No description has been provided for this image

What happened?¶

  • The overall acceptance rate for women (30%) was lower than it was for men (45%).

  • However, most departments (A, B, D, F) had a higher acceptance rate for women.

  • Department A had a 62% acceptance rate for men and an 82% acceptance rate for women!

    • 31% of men applied to Department A.
    • 6% of women applied to Department A.
  • Department F had a 6% acceptance rate for men and a 7% acceptance rate for women!

    • 14% of men applied to Department F.
    • 19% of women applied to Department F.
  • Conclusion: Women tended to apply to departments with a lower acceptance rate; the data don't support the claim that there was major gender discrimination against women.

Takeaways¶

Be skeptical of...

  • Aggregate statistics.
  • People misusing statistics to "prove" that discrimination doesn't exist.
  • Drawing conclusions from individual publications ($p$-hacking, publication bias, narrow focus, etc.).
  • And more!

We need to apply domain knowledge and human judgement calls to decide what to do when Simpson's paradox is present.

Really?¶

To handle Simpson's paradox with rigor, we need some ideas from causal inference which we don't have time to cover in DSC 80. This video has a good example of how to approach Simpson's paradox using a minimal amount of causal inference, if you're curious (not required for DSC 80).

In [21]:
IFrame('https://www.youtube-nocookie.com/embed/zeuW1Z2EtLs?si=l2Dl7P-5RCq3ODpo',
       width=800, height=450)
Out[21]:

Further reading¶

  • Gender Bias in Admission Statistics?
    • Contains a great visualization, but seems to be paywalled now.
  • What is Simpson's Paradox?
  • Understanding Simpson's Paradox
    • Requires more statistics background, but gives a rigorous understanding of when to use aggregated vs. unaggregated data.

Merging¶

Example: Name categories¶

The New York Times article from Lecture 1 claims that certain categories of names are becoming more popular. For example:

  • Forbidden names like Lucifer, Lilith, Kali, and Danger.

  • Evangelical names like Amen, Savior, Canaan, and Creed.

  • Mythological names like Julius, Hera, and Nyx.

  • It also claims that baby boomer names like Susan and Karen are becoming less popular.

Let's see if we can verify these claims using data!

Loading in the data¶

The baby DataFrame has one row for every combination of 'Name', 'Sex', and 'Year'.

In [22]:
baby
Out[22]:
Name Sex Count Year
0 Liam M 20456 2022
1 Noah M 18621 2022
2 Olivia F 16573 2022
... ... ... ... ...
2085155 Wright M 5 1880
2085156 York M 5 1880
2085157 Zachariah M 5 1880

2085158 rows × 4 columns

Our second DataFrame, nyt, contains the names mentioned in the article and their categorization.

In [23]:
nyt_path = Path('data') / 'nyt_names.csv'
nyt = pd.read_csv(nyt_path)
nyt
Out[23]:
nyt_name category
0 Lucifer forbidden
1 Lilith forbidden
2 Danger forbidden
... ... ...
20 Venus celestial
21 Celestia celestial
22 Skye celestial

23 rows × 2 columns

Issue: To find the number of babies born with (for example) forbidden names each year, we need to combine information from both baby and nyt.

Merging¶

  • We want to link rows from baby and nyt together whenever the names match up.
  • This is a merge (pandas term), i.e. a join (SQL term).
  • A merge is appropriate when we have two sources of information about the same individuals that is linked by a common column(s).
  • The common column(s) are called the join key.

Example merge¶

Let's demonstrate on a small subset of baby and nyt.

In [24]:
nyt_small = nyt.iloc[[11, 12, 14]].reset_index(drop=True)

names_to_keep = ['Julius', 'Karen', 'Noah']
baby_small = (baby
 .query("Year == 2020 and Name in @names_to_keep")
 .reset_index(drop=True)
)

dfs_side_by_side(baby_small, nyt_small)
Name Sex Count Year
0 Noah M 18407 2020
1 Julius M 966 2020
2 Karen F 330 2020
3 Noah F 306 2020
4 Karen M 6 2020
nyt_name category
0 Karen boomer
1 Julius mythology
2 Freya mythology
In [25]:
baby_small.merge(nyt_small, left_on='Name', right_on='nyt_name')
Out[25]:
Name Sex Count Year nyt_name category
0 Julius M 966 2020 Julius mythology
1 Karen F 330 2020 Karen boomer
2 Karen M 6 2020 Karen boomer

The merge method¶

  • The merge DataFrame method joins two DataFrames by columns or indexes.

    • As mentioned before, "merge" is just the pandas word for "join."
  • When using the merge method, the DataFrame before merge is the "left" DataFrame, and the DataFrame passed into merge is the "right" DataFrame.

    • In baby_small.merge(nyt_small), baby_small is considered the "left" DataFrame and nyt_small is the "right" DataFrame; the columns from the left DataFrame appear to the left of the columns from right DataFrame.
  • By default:

    • If join keys are not specified, all shared columns between the two DataFrames are used.
    • The "type" of join performed is an inner join. This is the only type of join you saw in DSC 10, but there are more, as we'll now see!

Join types: inner joins¶

In [26]:
baby_small.merge(nyt_small, left_on='Name', right_on='nyt_name')
Out[26]:
Name Sex Count Year nyt_name category
0 Julius M 966 2020 Julius mythology
1 Karen F 330 2020 Karen boomer
2 Karen M 6 2020 Karen boomer
  • Note that 'Noah' and 'Freya' do not appear in the merged DataFrame.
  • This is because there is:
    • no 'Noah' in the right DataFrame (nyt_small), and
    • no 'Freya' in the left DataFrame (baby_small).
  • The default type of join that merge performs is an inner join, which keeps the intersection of the join keys.
No description has been provided for this image

Different join types¶

We can change the type of join performed by changing the how argument in merge. Let's experiment!

In [27]:
# Note the NaNs!
baby_small.merge(nyt_small, left_on='Name', right_on='nyt_name', how='left')
Out[27]:
Name Sex Count Year nyt_name category
0 Noah M 18407 2020 NaN NaN
1 Julius M 966 2020 Julius mythology
2 Karen F 330 2020 Karen boomer
3 Noah F 306 2020 NaN NaN
4 Karen M 6 2020 Karen boomer
In [28]:
baby_small.merge(nyt_small, left_on='Name', right_on='nyt_name', how='right')
Out[28]:
Name Sex Count Year nyt_name category
0 Karen F 330.0 2020.0 Karen boomer
1 Karen M 6.0 2020.0 Karen boomer
2 Julius M 966.0 2020.0 Julius mythology
3 NaN NaN NaN NaN Freya mythology
In [29]:
baby_small.merge(nyt_small, left_on='Name', right_on='nyt_name', how='outer')
Out[29]:
Name Sex Count Year nyt_name category
0 NaN NaN NaN NaN Freya mythology
1 Julius M 966.0 2020.0 Julius mythology
2 Karen F 330.0 2020.0 Karen boomer
3 Karen M 6.0 2020.0 Karen boomer
4 Noah M 18407.0 2020.0 NaN NaN
5 Noah F 306.0 2020.0 NaN NaN

Different join types handle mismatches differently¶

There are four types of joins.

  • Inner: keep only matching keys (intersection).
  • Outer: keep all keys in both DataFrames (union).
  • Left: keep all keys in the left DataFrame, whether or not they are in the right DataFrame.
  • Right: keep all keys in the right DataFrame, whether or not they are in the left DataFrame.
    • Note that a.merge(b, how='left') contains the same information as b.merge(a, how='right'), just in a different order.
No description has been provided for this image

Notes on the merge method¶

  • merge is flexible – you can merge using a combination of columns, or the index of the DataFrame.
  • If the two DataFrames have the same column names, pandas will add _x and _y to the duplicated column names to avoid having columns with the same name (change these using the suffixes argument).
  • There is, in fact, a join method, but it's actually a wrapper around merge with fewer options.
  • As always, the documentation is your friend!

Lots of pandas operations do an implicit outer join!¶

  • pandas will almost always try to match up index values using an outer join.
  • It won't tell you that it's doing an outer join, it'll just throw NaNs in your result!
In [30]:
df1 = pd.DataFrame({'a': [1, 2, 3]}, index=['hello', 'dsc80', 'students'])
df2 = pd.DataFrame({'b': [10, 20, 30]}, index=['dsc80', 'is', 'awesome'])
dfs_side_by_side(df1, df2)
a
hello 1
dsc80 2
students 3
b
dsc80 10
is 20
awesome 30
In [31]:
df1['a'] + df2['b'] #pandas decision to use outer join here
Out[31]:
awesome      NaN
dsc80       12.0
hello        NaN
is           NaN
students     NaN
dtype: float64

Many-to-one & many-to-many joins¶

One-to-one joins¶

  • One-to-one joins are joins where neither the left DataFrame nor the right DataFrame contain any duplicates in the join key.
  • What if there are duplicated join keys, in one or both of the DataFrames we are merging?
In [32]:
# Run this cell to set up the next example.
profs = pd.DataFrame(
[['Sam', 'UCSD', 5],
 ['Janine', 'UCSD', 8],
 ['Marina', 'UIC', 7],
 ['Justin', 'OSU', 5],
 ['Soohyun', 'UCSD', 2],
 ['Peter', 'UW', 2]],
    columns=['Name', 'School', 'Years']
)

schools = pd.DataFrame({
    'Abbr': ['UCSD', 'UCLA', 'UW', 'UIC'],
    'Full': ['University of California San Diego', 'University of California, Los Angeles', 'University of Washington', 'University of Illinois Chicago']
})

programs = pd.DataFrame({
    'uni': ['UCSD', 'UCSD', 'UCSD', 'UW', 'OSU', 'OSU'],
    'dept': ['Math', 'HDSI', 'COGS', 'CS', 'Math', 'CS'],
    'grad_students': [205, 54, 281, 439, 304, 193]
})

Many-to-one joins¶

  • Many-to-one joins are joins where one of the DataFrames contains duplicate values in the join key.
  • The resulting DataFrame will preserve those duplicate entries as appropriate.
In [33]:
dfs_side_by_side(profs, schools)
Name School Years
0 Sam UCSD 5
1 Janine UCSD 8
2 Marina UIC 7
3 Justin OSU 5
4 Soohyun UCSD 2
5 Peter UW 2
Abbr Full
0 UCSD University of California San Diego
1 UCLA University of California, Los Angeles
2 UW University of Washington
3 UIC University of Illinois Chicago
In [34]:
profs.merge(schools, left_on='School', right_on='Abbr', how='left')
Out[34]:
Name School Years Abbr Full
0 Sam UCSD 5 UCSD University of California San Diego
1 Janine UCSD 8 UCSD University of California San Diego
2 Marina UIC 7 UIC University of Illinois Chicago
3 Justin OSU 5 NaN NaN
4 Soohyun UCSD 2 UCSD University of California San Diego
5 Peter UW 2 UW University of Washington

Many-to-many joins¶

Many-to-many joins are joins where both DataFrames have duplicate values in the join key.

In [35]:
dfs_side_by_side(profs, programs)
Name School Years
0 Sam UCSD 5
1 Janine UCSD 8
2 Marina UIC 7
3 Justin OSU 5
4 Soohyun UCSD 2
5 Peter UW 2
uni dept grad_students
0 UCSD Math 205
1 UCSD HDSI 54
2 UCSD COGS 281
3 UW CS 439
4 OSU Math 304
5 OSU CS 193

Before running the following cell, try predicting the number of rows in the output.

In [36]:
#profs.merge(programs, left_on='School', right_on='uni', how='inner')
  • merge stitched together every UCSD row in profs with every UCSD row in programs.
  • Since there were 3 UCSD rows in profs and 3 in programs, there are $3 \cdot 3 = 9$ UCSD rows in the output. The same applies for all other schools.

Question 🤔

Fill in the blank so that the last statement evaluates to True.

df = profs.merge(programs, left_on='School', right_on='uni')
df.shape[0] == (____).sum()

Don't use merge (or join) in your solution!

In [37]:
dfs_side_by_side(profs, programs)
Name School Years
0 Sam UCSD 5
1 Janine UCSD 8
2 Marina UIC 7
3 Justin OSU 5
4 Soohyun UCSD 2
5 Peter UW 2
uni dept grad_students
0 UCSD Math 205
1 UCSD HDSI 54
2 UCSD COGS 281
3 UW CS 439
4 OSU Math 304
5 OSU CS 193
In [ ]:
 

Returning back to our original question¶

Let's find the popularity of baby name categories over time. To start, we'll define a DataFrame that has one row for every combination of 'category' and 'Year'.

In [38]:
category_counts = (
    baby
    .merge(nyt, left_on='Name', right_on='nyt_name')
    .groupby(['category', 'Year'])
    ['Count']
    .sum()
    .reset_index()
)
category_counts
Out[38]:
category Year Count
0 boomer 1880 292
1 boomer 1881 298
2 boomer 1882 326
... ... ... ...
659 mythology 2020 3516
660 mythology 2021 3895
661 mythology 2022 4049

662 rows × 3 columns

In [39]:
# We'll talk about plotting code soon!
import plotly.express as px
fig = px.line(category_counts, x='Year', y='Count',
              facet_col='category', facet_col_wrap=3,
              facet_row_spacing=0.15,
              width=600, height=400)
fig.update_yaxes(matches=None, showticklabels=False)

Transforming¶

Transforming values¶

  • A transformation results from performing some operation on every element in a sequence, e.g. a Series.

  • In DSC 10, you learned how to transform Series using the apply method. apply takes in a function, which itself takes in a single value as input and returns a single value.

In [40]:
baby
Out[40]:
Name Sex Count Year
0 Liam M 20456 2022
1 Noah M 18621 2022
2 Olivia F 16573 2022
... ... ... ... ...
2085155 Wright M 5 1880
2085156 York M 5 1880
2085157 Zachariah M 5 1880

2085158 rows × 4 columns

In [41]:
def number_of_vowels(string):
    return sum(c in 'aeiou' for c in string.lower())

baby['Name'].apply(number_of_vowels)
Out[41]:
0          2
1          2
2          4
          ..
2085155    1
2085156    1
2085157    4
Name: Name, Length: 2085158, dtype: int64
In [42]:
# Built-in functions work with apply, too.
baby['Name'].apply(len)
Out[42]:
0          4
1          4
2          6
          ..
2085155    6
2085156    4
2085157    9
Name: Name, Length: 2085158, dtype: int64

The price of apply¶

Unfortunately, apply runs really slowly!

In [43]:
%%timeit
baby['Name'].apply(number_of_vowels)
1.5 s ± 24.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [44]:
%%timeit
res = []
for name in baby['Name']:
    res.append(number_of_vowels(name))
1.21 s ± 16.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Internally, apply actually just runs a for-loop!

So, when possible – say, when applying arithmetic operations – we should work on Series objects directly and avoid apply!

In [45]:
# Rounds down to the nearest multiple of 10.
baby['Year'] // 10 * 10 
Out[45]:
0          2020
1          2020
2          2020
           ... 
2085155    1880
2085156    1880
2085157    1880
Name: Year, Length: 2085158, dtype: int64
In [46]:
# Does the same, but slower.
baby['Year'].apply(lambda y: y // 10 * 10) 
Out[46]:
0          2020
1          2020
2          2020
           ... 
2085155    1880
2085156    1880
2085157    1880
Name: Year, Length: 2085158, dtype: int64

The .str accessor¶

For string operations, pandas provides a convenient .str accessor.

In [47]:
%%timeit
baby['Name'].str.len()
354 ms ± 1.42 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [48]:
%%timeit
baby['Name'].apply(len)
411 ms ± 5.02 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

It's very convenient and runs at about the same speed as apply.

Other data representations¶

Representations of tabular data¶

  • In DSC 80, we work with DataFrames in pandas.

    • When we say pandas DataFrame, we're talking about the pandas API for its DataFrame objects.
      • API stands for "application programming interface." We'll learn about these more soon.
    • When we say "DataFrame", we're referring to a general way to represent data (rows and columns, with labels for both rows and columns).
  • There many other ways to work with data tables!

    • Examples: R data frames, SQL databases, spreadsheets, or even matrices from linear algebra.
    • When you learn SQL in DSC 100, you'll find many similaries (e.g. slicing columns, filtering rows, grouping, joining, etc.).
    • Relational algebra captures common data operations between many data table systems.
  • Why use DataFrames over something else?

DataFrames vs. spreadsheets¶

  • DataFrames give us a data lineage: the code records down data changes. Not so in spreadsheets!
  • Using a general-purpose programming language gives us the ability to handle much larger datasets, and we can use distributed computing systems to handle massive datasets.

DataFrames vs. matrices¶

\begin{split} \begin{aligned} \mathbf{X} = \begin{bmatrix} 1 & 0 \\ 0 & 4 \\ 0 & 0 \\ \end{bmatrix} \end{aligned} \end{split}

  • Matrices are mathematical objects. They only hold numbers, but have many useful properties (which you've learned about in your linear algebra class, Math 18).
  • Often, we process data from a DataFrame into matrix format for machine learning models. You saw this a bit in DSC 40A, and we'll see this more in DSC 80 in a few weeks.

DataFrames vs. relations¶

  • Relations are the data representation for relational database systems (e.g. MySQL, PostgreSQL, etc.).
  • You'll learn all about these in DSC 100.
  • Database systems are much better than DataFrames at storing many large data tables and handling concurrency (many people reading and writing data at the same time).
  • Common workflow: load a subset of data in from a database system into pandas, then make a plot.
  • Or: load and clean data in pandas, then store it in a database system for others to use.

Summary¶

  • There is no "formula" to automatically resolve Simpson's paradox! Domain knowledge is important.
  • We've covered most of the primary DataFrame operations: subsetting, aggregating, joining, and transforming.

Next time¶

Data cleaning: applying what we've already learned to real-world, messy data!