Data 3-3 Multiple Pandas Dataframes#
Working with multiple dataframes: Concat, Merge, resetting the index, join types
In this unit we will discuss strategies for dealing with multiple dataframes and combing them into a single dataframe.
1import pandas as pd
Concatenation#
Concatenation appends the rows of one or more dataframes together. This is a row-oriented operation so the resulting datafram will be longer.
Example: 50 rows concatendated to 40 rows == 90 rows.
pd.concat(items: list[pd.DataFrame], ignore_index=False) The argument is a list of Dataframes to concat.
We can choose to keep the current index in each dataframe ignore_index=False or create a new index ignore_index=True
1campus_students = pd.read_csv("https://raw.githubusercontent.com/mafudge/datasets/master/delimited/campus-students.csv")
2campus_students
| Name | Grade | Year | |
|---|---|---|---|
| 0 | Helen | NaN | Sophomore |
| 1 | Iris | 10.0 | Senior |
| 2 | Jimmy | 8.0 | Freshman |
| 3 | Karen | NaN | Freshman |
| 4 | Lynne | 10.0 | Sophomore |
| 5 | Mike | 10.0 | Sophomore |
| 6 | Nico | NaN | Junior |
| 7 | Pete | 8.0 | Freshman |
1online_students = pd.read_csv("https://raw.githubusercontent.com/mafudge/datasets/master/delimited/online-students.csv")
2online_students
| Name | Grade | Year | Location | |
|---|---|---|---|---|
| 0 | Abby | 7.0 | Freshman | NY |
| 1 | Bob | 9.0 | Sophomore | CA |
| 2 | Chris | 10.0 | Senior | CA |
| 3 | Dave | 8.0 | Freshman | NY |
| 4 | Ellen | 7.0 | Sophomore | TX |
| 5 | Fran | 10.0 | Senior | FL |
| 6 | Greg | 8.0 | Freshman | NY |
1combined_students = pd.concat([campus_students, online_students])
2combined_students
| Name | Grade | Year | Location | |
|---|---|---|---|---|
| 0 | Helen | NaN | Sophomore | NaN |
| 1 | Iris | 10.0 | Senior | NaN |
| 2 | Jimmy | 8.0 | Freshman | NaN |
| 3 | Karen | NaN | Freshman | NaN |
| 4 | Lynne | 10.0 | Sophomore | NaN |
| 5 | Mike | 10.0 | Sophomore | NaN |
| 6 | Nico | NaN | Junior | NaN |
| 7 | Pete | 8.0 | Freshman | NaN |
| 0 | Abby | 7.0 | Freshman | NY |
| 1 | Bob | 9.0 | Sophomore | CA |
| 2 | Chris | 10.0 | Senior | CA |
| 3 | Dave | 8.0 | Freshman | NY |
| 4 | Ellen | 7.0 | Sophomore | TX |
| 5 | Fran | 10.0 | Senior | FL |
| 6 | Greg | 8.0 | Freshman | NY |
concat() - Ignoring the index#
As you can see from the code above the index from the original DataFrames was used. For example Helen and Abby both share the index 0.
While this is acceptable, there are situations where a new index based on combined values is desirable. We will encounter this later when grouping or pivioting data.
To make this happen include the ignore_index=True named argument. This will create a new index from the output DataFrame.
1students = pd.concat([campus_students, online_students], ignore_index=True)
2students
| Name | Grade | Year | Location | |
|---|---|---|---|---|
| 0 | Helen | NaN | Sophomore | NaN |
| 1 | Iris | 10.0 | Senior | NaN |
| 2 | Jimmy | 8.0 | Freshman | NaN |
| 3 | Karen | NaN | Freshman | NaN |
| 4 | Lynne | 10.0 | Sophomore | NaN |
| 5 | Mike | 10.0 | Sophomore | NaN |
| 6 | Nico | NaN | Junior | NaN |
| 7 | Pete | 8.0 | Freshman | NaN |
| 8 | Abby | 7.0 | Freshman | NY |
| 9 | Bob | 9.0 | Sophomore | CA |
| 10 | Chris | 10.0 | Senior | CA |
| 11 | Dave | 8.0 | Freshman | NY |
| 12 | Ellen | 7.0 | Sophomore | TX |
| 13 | Fran | 10.0 | Senior | FL |
| 14 | Greg | 8.0 | Freshman | NY |
Best practice - data lineage#
When combining datasets, its a really good idea to retain data lineage, or a record of where the data came from. This can be done by added a column to each dataframe before concatenating.
In this example we create a student "type" column to track lineage.
1campus_students = pd.read_csv("https://raw.githubusercontent.com/mafudge/datasets/master/delimited/campus-students.csv")
2campus_students['type'] = 'campus'
3online_students = pd.read_csv("https://raw.githubusercontent.com/mafudge/datasets/master/delimited/online-students.csv")
4online_students['type'] = 'online'
5students = pd.concat([campus_students, online_students], ignore_index=True)
6students
| Name | Grade | Year | type | Location | |
|---|---|---|---|---|---|
| 0 | Helen | NaN | Sophomore | campus | NaN |
| 1 | Iris | 10.0 | Senior | campus | NaN |
| 2 | Jimmy | 8.0 | Freshman | campus | NaN |
| 3 | Karen | NaN | Freshman | campus | NaN |
| 4 | Lynne | 10.0 | Sophomore | campus | NaN |
| 5 | Mike | 10.0 | Sophomore | campus | NaN |
| 6 | Nico | NaN | Junior | campus | NaN |
| 7 | Pete | 8.0 | Freshman | campus | NaN |
| 8 | Abby | 7.0 | Freshman | online | NY |
| 9 | Bob | 9.0 | Sophomore | online | CA |
| 10 | Chris | 10.0 | Senior | online | CA |
| 11 | Dave | 8.0 | Freshman | online | NY |
| 12 | Ellen | 7.0 | Sophomore | online | TX |
| 13 | Fran | 10.0 | Senior | online | FL |
| 14 | Greg | 8.0 | Freshman | online | NY |
Challenge 3-3-1#
Classic use case for concatenation.#
Observe the output from the three cells below. The issue with the JSON data is that there are employees under keys by department "accounting", "sales", "marketing"
This is the classic use-case for pd.concat() as there is no practical way to use pd.json_normalize() to get all the employees under each department.
for each department:
create a dataframe for that department (e.g. from the json under the department)
add lineage to the dataframe (e.g. add the department name)
add the dataframe to a list of departments
concat the list of departments together one dataframe
print dataframe
expected output:
firstName lastName age dept
0 John Doe 23 accounting
1 Mary Smith 32 accounting
2 Sally Green 27 sales
3 Jim Galley 41 sales
4 Tom Brown 28 marketing
1import requests
2response = requests.get("https://raw.githubusercontent.com/mafudge/datasets/refs/heads/master/json-samples/employees-dict.json")
3employees = response.json()
4employees
{'accounting': [{'firstName': 'John', 'lastName': 'Doe', 'age': 23},
{'firstName': 'Mary', 'lastName': 'Smith', 'age': 32}],
'sales': [{'firstName': 'Sally', 'lastName': 'Green', 'age': 27},
{'firstName': 'Jim', 'lastName': 'Galley', 'age': 41}],
'marketing': [{'firstName': 'Tom', 'lastName': 'Brown', 'age': 28}]}
1print("departments", employees.keys())
departments dict_keys(['accounting', 'sales', 'marketing'])
1acct_emp_df =pd.json_normalize(employees, record_path='accounting')
2acct_emp_df
| firstName | lastName | age | |
|---|---|---|---|
| 0 | John | Doe | 23 |
| 1 | Mary | Smith | 32 |
Deduplication#
Sometimes after a pd.conact() you will have duplicate rows.
You can use df.drop_duplicates() to remove repeated rows of data.
Without an argument, this will scan the entire row of data to determine if the row is the same.
If your data has a natural key, you can specify that with the subset= named argument. This will improve performance.
1# example
2o1 = pd.read_csv("https://raw.githubusercontent.com/mafudge/datasets/refs/heads/master/dedupe/orders1.csv")
3o2 = pd.read_csv("https://raw.githubusercontent.com/mafudge/datasets/refs/heads/master/dedupe/orders2.csv")
4orders = pd.concat([o1, o2], ignore_index=True)
5orders.sort_values('orderid')
| orderid | orderdate | custname | custemail | custcountry | orderstatus | ordertotal | ordercreditcard | ordershipvia | shippingtotal | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2 | 2023-03-24 | Frayda Pepperd | fpepperd0@sciencedaily.com | Canada | delivered | 228.39 | Discover | RPS | 12.05 |
| 10 | 2 | 2023-03-24 | Frayda Pepperd | fpepperd0@sciencedaily.com | Canada | delivered | 228.39 | Discover | RPS | 12.05 |
| 11 | 3 | 2020-02-23 | Loy Siberry | lsiberry1@so-net.ne.jp | Canada | delivered | 76.87 | Discover | USPS | 6.27 |
| 12 | 4 | 2022-04-28 | Carree Henworth | NaN | Canada | pending | 152.30 | Discover | USPS | 12.74 |
| 1 | 4 | 2022-04-28 | Carree Henworth | NaN | Canada | pending | 152.30 | Discover | USPS | 12.74 |
| 2 | 5 | 2019-11-22 | Goldina Godsafe | ggodsafe3@dailymail.co.uk | United States | shipped | 182.17 | Amex | UPS | 5.44 |
| 3 | 6 | 2022-05-03 | Marris Chatten | mchatten4@csmonitor.com | Mexico | pending | 208.28 | Discover | RPS | 2.16 |
| 13 | 6 | 2022-05-03 | Marris Chatten | mchatten4@csmonitor.com | Mexico | pending | 208.28 | Discover | RPS | 2.16 |
| 4 | 7 | 2022-12-19 | Logan Jacobsson | ljacobsson5@wufoo.com | United States | delivered | 112.15 | Amex | USPS | 11.52 |
| 14 | 7 | 2022-12-19 | Logan Jacobsson | ljacobsson5@wufoo.com | United States | delivered | 112.15 | Amex | USPS | 11.52 |
| 15 | 8 | 2019-06-05 | Lilli Feares | lfeares6@shop-pro.jp | Mexico | pending | 237.90 | Discover | FedEX | 4.48 |
| 16 | 9 | 2019-02-17 | Lowrance Sigsworth | lsigsworth7@youtube.com | United States | delivered | 141.94 | Discover | USPS | 7.31 |
| 17 | 10 | 2023-04-19 | Libbi Spadari | lspadari8@dot.gov | Mexico | pending | 160.79 | Discover | RPS | 16.52 |
| 5 | 10 | 2023-04-19 | Libbi Spadari | lspadari8@dot.gov | Mexico | pending | 160.79 | Discover | RPS | 16.52 |
| 6 | 11 | 2020-01-20 | Renato Hue | rhue9@un.org | Canada | delivered | 120.52 | Visa | USPS | 5.57 |
| 7 | 12 | 2022-03-03 | Lucky Helstrip | lhelstripa@tmall.com | Mexico | delivered | 202.07 | Amex | UPS | 18.57 |
| 18 | 12 | 2022-03-03 | Lucky Helstrip | lhelstripa@tmall.com | Mexico | delivered | 202.07 | Amex | UPS | 18.57 |
| 8 | 13 | 2021-09-04 | Debi Myrie | dmyrieb@unc.edu | United States | delivered | 131.62 | Amex | UPS | 2.37 |
| 19 | 13 | 2021-09-04 | Debi Myrie | dmyrieb@unc.edu | United States | delivered | 131.62 | Amex | UPS | 2.37 |
| 20 | 14 | 2022-02-27 | Hyacinth Aveyard | haveyardc@ucoz.com | United States | pending | 209.86 | Amex | USPS | 8.69 |
| 9 | 15 | 2019-01-11 | Crin Blanket | cblanketd@newsvine.com | United States | delivered | 85.46 | Visa | UPS | 14.22 |
1# uses the entire row to check for duplicates
2orders.drop_duplicates().sort_values("orderid")
3
4# uses the natural key in the data set to check for duplicates
5orders.drop_duplicates(subset="orderid").sort_values("orderid")
| orderid | orderdate | custname | custemail | custcountry | orderstatus | ordertotal | ordercreditcard | ordershipvia | shippingtotal | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2 | 2023-03-24 | Frayda Pepperd | fpepperd0@sciencedaily.com | Canada | delivered | 228.39 | Discover | RPS | 12.05 |
| 11 | 3 | 2020-02-23 | Loy Siberry | lsiberry1@so-net.ne.jp | Canada | delivered | 76.87 | Discover | USPS | 6.27 |
| 1 | 4 | 2022-04-28 | Carree Henworth | NaN | Canada | pending | 152.30 | Discover | USPS | 12.74 |
| 2 | 5 | 2019-11-22 | Goldina Godsafe | ggodsafe3@dailymail.co.uk | United States | shipped | 182.17 | Amex | UPS | 5.44 |
| 3 | 6 | 2022-05-03 | Marris Chatten | mchatten4@csmonitor.com | Mexico | pending | 208.28 | Discover | RPS | 2.16 |
| 4 | 7 | 2022-12-19 | Logan Jacobsson | ljacobsson5@wufoo.com | United States | delivered | 112.15 | Amex | USPS | 11.52 |
| 15 | 8 | 2019-06-05 | Lilli Feares | lfeares6@shop-pro.jp | Mexico | pending | 237.90 | Discover | FedEX | 4.48 |
| 16 | 9 | 2019-02-17 | Lowrance Sigsworth | lsigsworth7@youtube.com | United States | delivered | 141.94 | Discover | USPS | 7.31 |
| 5 | 10 | 2023-04-19 | Libbi Spadari | lspadari8@dot.gov | Mexico | pending | 160.79 | Discover | RPS | 16.52 |
| 6 | 11 | 2020-01-20 | Renato Hue | rhue9@un.org | Canada | delivered | 120.52 | Visa | USPS | 5.57 |
| 7 | 12 | 2022-03-03 | Lucky Helstrip | lhelstripa@tmall.com | Mexico | delivered | 202.07 | Amex | UPS | 18.57 |
| 8 | 13 | 2021-09-04 | Debi Myrie | dmyrieb@unc.edu | United States | delivered | 131.62 | Amex | UPS | 2.37 |
| 20 | 14 | 2022-02-27 | Hyacinth Aveyard | haveyardc@ucoz.com | United States | pending | 209.86 | Amex | USPS | 8.69 |
| 9 | 15 | 2019-01-11 | Crin Blanket | cblanketd@newsvine.com | United States | delivered | 85.46 | Visa | UPS | 14.22 |
Returning a dataframe of duplicates#
To get a dataframe of just the duplicated values you can use the df.duplicated() boolean index.
This should be appled to the concatenated dataframe.
1dupes = orders.duplicated(subset=['orderid'])
2orders[dupes]
| orderid | orderdate | custname | custemail | custcountry | orderstatus | ordertotal | ordercreditcard | ordershipvia | shippingtotal | |
|---|---|---|---|---|---|---|---|---|---|---|
| 10 | 2 | 2023-03-24 | Frayda Pepperd | fpepperd0@sciencedaily.com | Canada | delivered | 228.39 | Discover | RPS | 12.05 |
| 12 | 4 | 2022-04-28 | Carree Henworth | NaN | Canada | pending | 152.30 | Discover | USPS | 12.74 |
| 13 | 6 | 2022-05-03 | Marris Chatten | mchatten4@csmonitor.com | Mexico | pending | 208.28 | Discover | RPS | 2.16 |
| 14 | 7 | 2022-12-19 | Logan Jacobsson | ljacobsson5@wufoo.com | United States | delivered | 112.15 | Amex | USPS | 11.52 |
| 17 | 10 | 2023-04-19 | Libbi Spadari | lspadari8@dot.gov | Mexico | pending | 160.79 | Discover | RPS | 16.52 |
| 18 | 12 | 2022-03-03 | Lucky Helstrip | lhelstripa@tmall.com | Mexico | delivered | 202.07 | Amex | UPS | 18.57 |
| 19 | 13 | 2021-09-04 | Debi Myrie | dmyrieb@unc.edu | United States | delivered | 131.62 | Amex | UPS | 2.37 |
Merges#
A Merge combines to dataframes based on a common column. The resulting dataframe is wider (has more columns) than the original dataframe.
pd.merge(left: pd.DataFrame, right:pd.Dataframe, how:str, left_on:str, right_on:str)
howspecifies the join operation:"inner"- returns ONLY rows when values ofleft_onmatchright_on"left"- returns ALL rows fromleftand ONLY rows fromrightwhen values ofleft_onmatchright_on"right"- returns ALL rows fromrightand ONLY rows fromleftwhen values ofleft_onmatchright_on"outer"- returns ALL rows fromleftandrightand rows when values ofleft_onmatchright_on
1bbplayers = pd.read_csv("https://raw.githubusercontent.com/mafudge/datasets/refs/heads/master/delimited/bbplayers.csv")
2bbplayers
| player_id | player_name | career_pts | player_team_id | |
|---|---|---|---|---|
| 0 | 101 | Jordan | 32292 | 1.0 |
| 1 | 102 | Pippen | 18940 | 1.0 |
| 2 | 103 | Bryant | 33643 | 2.0 |
| 3 | 104 | O'Neal | 28596 | 2.0 |
| 4 | 105 | Fudge | 0 | NaN |
1bbteams = pd.read_csv("https://raw.githubusercontent.com/mafudge/datasets/refs/heads/master/delimited/bbteams.csv")
2bbteams
| team_id | team_name | team_location | |
|---|---|---|---|
| 0 | 1 | Bulls | Chicago, IL |
| 1 | 2 | Lakers | Los Angeles, CA |
| 2 | 3 | Tropics | Flint, MI |
1# INNER join: ONLY rows that match the bbplayer.player_team_id and bbteam.team_id and bbteam will be included
2# we lose Player 105 and team 3 because there are no matches.
3combined = pd.merge(bbplayers, bbteams, left_on='player_team_id', right_on='team_id', how='inner')
4combined
| player_id | player_name | career_pts | player_team_id | team_id | team_name | team_location | |
|---|---|---|---|---|---|---|---|
| 0 | 101 | Jordan | 32292 | 1.0 | 1 | Bulls | Chicago, IL |
| 1 | 102 | Pippen | 18940 | 1.0 | 1 | Bulls | Chicago, IL |
| 2 | 103 | Bryant | 33643 | 2.0 | 2 | Lakers | Los Angeles, CA |
| 3 | 104 | O'Neal | 28596 | 2.0 | 2 | Lakers | Los Angeles, CA |
1# LEFT join: All rows from the left (every player) and ONLY rows from team that match.
2# we now see Player 105 despite no team match
3combined = pd.merge(bbplayers, bbteams, left_on='player_team_id', right_on='team_id', how='left')
4combined
| player_id | player_name | career_pts | player_team_id | team_id | team_name | team_location | |
|---|---|---|---|---|---|---|---|
| 0 | 101 | Jordan | 32292 | 1.0 | 1.0 | Bulls | Chicago, IL |
| 1 | 102 | Pippen | 18940 | 1.0 | 1.0 | Bulls | Chicago, IL |
| 2 | 103 | Bryant | 33643 | 2.0 | 2.0 | Lakers | Los Angeles, CA |
| 3 | 104 | O'Neal | 28596 | 2.0 | 2.0 | Lakers | Los Angeles, CA |
| 4 | 105 | Fudge | 0 | NaN | NaN | NaN | NaN |
1# RIGHT join: All rows from the right (every team) and ONLY rows from players that match.
2# we now see Team 3 despite no player match
3combined = pd.merge(bbplayers, bbteams, left_on='player_team_id', right_on='team_id', how='right')
4combined
| player_id | player_name | career_pts | player_team_id | team_id | team_name | team_location | |
|---|---|---|---|---|---|---|---|
| 0 | 101.0 | Jordan | 32292.0 | 1.0 | 1 | Bulls | Chicago, IL |
| 1 | 102.0 | Pippen | 18940.0 | 1.0 | 1 | Bulls | Chicago, IL |
| 2 | 103.0 | Bryant | 33643.0 | 2.0 | 2 | Lakers | Los Angeles, CA |
| 3 | 104.0 | O'Neal | 28596.0 | 2.0 | 2 | Lakers | Los Angeles, CA |
| 4 | NaN | NaN | NaN | NaN | 3 | Tropics | Flint, MI |
1# OUTER join: All rows from both tables are included. inner join + non-matching rows from both tables
2# we now see Team 3 and player 105
3combined = pd.merge(bbplayers, bbteams, left_on='player_team_id', right_on='team_id', how='outer')
4combined
| player_id | player_name | career_pts | player_team_id | team_id | team_name | team_location | |
|---|---|---|---|---|---|---|---|
| 0 | 101.0 | Jordan | 32292.0 | 1.0 | 1.0 | Bulls | Chicago, IL |
| 1 | 102.0 | Pippen | 18940.0 | 1.0 | 1.0 | Bulls | Chicago, IL |
| 2 | 103.0 | Bryant | 33643.0 | 2.0 | 2.0 | Lakers | Los Angeles, CA |
| 3 | 104.0 | O'Neal | 28596.0 | 2.0 | 2.0 | Lakers | Los Angeles, CA |
| 4 | NaN | NaN | NaN | NaN | 3.0 | Tropics | Flint, MI |
| 5 | 105.0 | Fudge | 0.0 | NaN | NaN | NaN | NaN |
Challenge 3-3-2#
Who is not buying from minimart?#
Observe the following data: mafudge/datasets
You have been hired to build a UI to display names of customers who did not buy from minimart in any given month.
Provide a Streamlit UI with a drop down selection for the month. (jan, feb, mar, apr)
It then outputs a dataframe of customers who did not buy anything in that month.
HINT
Access the raw data from this base url: https://raw.githubusercontent.com/mafudge/datasets/refs/heads/master/minimart/