dict_keys(['campaign_descriptions', 'coupons', 'promotions', 'campaigns', 'demographics', 'transactions', 'coupon_redemptions', 'products'])
Week 4: Data Wrangling in Python
Quick overview of today’s plan:
Follow along
Open up Colab and load the Complete Journey data: tinyurl.com/bana4080-wk4-lecture
dict_keys(['campaign_descriptions', 'coupons', 'promotions', 'campaigns', 'demographics', 'transactions', 'coupon_redemptions', 'products'])
Note
Complete Journey Docs: bit.ly/completejourney_py
In your group, come up with 2–3 questions you’d like to answer using these datasets. Think about business insights a grocery retailer might want.
Example questions:
Then we’ll take a few responses…
A lot of the insights you’ve just brainstormed will require:
💡 And that’s exactly what we’re going to cover this week!
Our complete journey data is not too bad; however, let’s look at this raw Ames data.
What do you notice?
| Order | PID | MS SubClass | MS Zoning | Lot Frontage | Lot Area | Street | Alley | Lot Shape | Land Contour | ... | Pool Area | Pool QC | Fence | Misc Feature | Misc Val | Mo Sold | Yr Sold | Sale Type | Sale Condition | SalePrice | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 526301100 | 20 | RL | 141.0 | 31770 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 5 | 2010 | WD | Normal | 215000 |
| 1 | 2 | 526350040 | 20 | RH | 80.0 | 11622 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | MnPrv | NaN | 0 | 6 | 2010 | WD | Normal | 105000 |
| 2 | 3 | 526351010 | 20 | RL | 81.0 | 14267 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | NaN | Gar2 | 12500 | 6 | 2010 | WD | Normal | 172000 |
| 3 | 4 | 526353030 | 20 | RL | 93.0 | 11160 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 4 | 2010 | WD | Normal | 244000 |
| 4 | 5 | 527105010 | 60 | RL | 74.0 | 13830 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | MnPrv | NaN | 0 | 3 | 2010 | WD | Normal | 189900 |
5 rows × 82 columns
Order, MS SubClass, SalePrice) - might want to standardize.Order, PID) - might want to drop irrelevant ones.price_per_sqft)We can easily rename specific columns
rename to rename certain columnsdict with {old: new} pairingsinplace=TrueCaution
Great, but we have 82 columns!
| Order | PID | ms_subclass | ms_zoning | Lot Frontage | Lot Area | Street | Alley | Lot Shape | Land Contour | ... | Pool Area | Pool QC | Fence | Misc Feature | Misc Val | Mo Sold | Yr Sold | Sale Type | Sale Condition | SalePrice | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 526301100 | 20 | RL | 141.0 | 31770 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 5 | 2010 | WD | Normal | 215000 |
| 1 | 2 | 526350040 | 20 | RH | 80.0 | 11622 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | MnPrv | NaN | 0 | 6 | 2010 | WD | Normal | 105000 |
| 2 | 3 | 526351010 | 20 | RL | 81.0 | 14267 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | NaN | Gar2 | 12500 | 6 | 2010 | WD | Normal | 172000 |
| 3 | 4 | 526353030 | 20 | RL | 93.0 | 11160 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 4 | 2010 | WD | Normal | 244000 |
| 4 | 5 | 527105010 | 60 | RL | 74.0 | 13830 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | MnPrv | NaN | 0 | 3 | 2010 | WD | Normal | 189900 |
5 rows × 82 columns
😳
Index(['order', 'pid', 'ms_subclass', 'ms_zoning', 'lot_frontage', 'lot_area',
'street', 'alley', 'lot_shape', 'land_contour', 'utilities',
'lot_config', 'land_slope', 'neighborhood', 'condition_1',
'condition_2', 'bldg_type', 'house_style', 'overall_qual',
'overall_cond', 'year_built', 'year_remod/add', 'roof_style',
'roof_matl', 'exterior_1st', 'exterior_2nd', 'mas_vnr_type',
'mas_vnr_area', 'exter_qual', 'exter_cond', 'foundation', 'bsmt_qual',
'bsmt_cond', 'bsmt_exposure', 'bsmtfin_type_1', 'bsmtfin_sf_1',
'bsmtfin_type_2', 'bsmtfin_sf_2', 'bsmt_unf_sf', 'total_bsmt_sf',
'heating', 'heating_qc', 'central_air', 'electrical', '1st_flr_sf',
'2nd_flr_sf', 'low_qual_fin_sf', 'gr_liv_area', 'bsmt_full_bath',
'bsmt_half_bath', 'full_bath', 'half_bath', 'bedroom_abvgr',
'kitchen_abvgr', 'kitchen_qual', 'totrms_abvgrd', 'functional',
'fireplaces', 'fireplace_qu', 'garage_type', 'garage_yr_blt',
'garage_finish', 'garage_cars', 'garage_area', 'garage_qual',
'garage_cond', 'paved_drive', 'wood_deck_sf', 'open_porch_sf',
'enclosed_porch', '3ssn_porch', 'screen_porch', 'pool_area', 'pool_qc',
'fence', 'misc_feature', 'misc_val', 'mo_sold', 'yr_sold', 'sale_type',
'sale_condition', 'saleprice'],
dtype='object')
😎
| order | pid | ms_subclass | ms_zoning | lot_frontage | lot_area | street | alley | lot_shape | land_contour | ... | pool_area | pool_qc | fence | misc_feature | misc_val | mo_sold | yr_sold | sale_type | sale_condition | saleprice | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 526301100 | 20 | RL | 141.0 | 31770 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 5 | 2010 | WD | Normal | 215000 |
| 1 | 2 | 526350040 | 20 | RH | 80.0 | 11622 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | MnPrv | NaN | 0 | 6 | 2010 | WD | Normal | 105000 |
| 2 | 3 | 526351010 | 20 | RL | 81.0 | 14267 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | NaN | Gar2 | 12500 | 6 | 2010 | WD | Normal | 172000 |
| 3 | 4 | 526353030 | 20 | RL | 93.0 | 11160 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 4 | 2010 | WD | Normal | 244000 |
| 4 | 5 | 527105010 | 60 | RL | 74.0 | 13830 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | MnPrv | NaN | 0 | 3 | 2010 | WD | Normal | 189900 |
5 rows × 82 columns
Last week we learned how to select columns of interest.
But we may also want to invert that thinking and just drop columns of disinterest!
Selecting columns of interest
Dropping columns of disinterest
| ms_zoning | lot_frontage | lot_area | street | alley | lot_shape | land_contour | utilities | lot_config | land_slope | ... | pool_area | pool_qc | fence | misc_feature | misc_val | mo_sold | yr_sold | sale_type | sale_condition | saleprice | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | RL | 141.0 | 31770 | Pave | NaN | IR1 | Lvl | AllPub | Corner | Gtl | ... | 0 | NaN | NaN | NaN | 0 | 5 | 2010 | WD | Normal | 215000 |
| 1 | RH | 80.0 | 11622 | Pave | NaN | Reg | Lvl | AllPub | Inside | Gtl | ... | 0 | NaN | MnPrv | NaN | 0 | 6 | 2010 | WD | Normal | 105000 |
| 2 | RL | 81.0 | 14267 | Pave | NaN | IR1 | Lvl | AllPub | Corner | Gtl | ... | 0 | NaN | NaN | Gar2 | 12500 | 6 | 2010 | WD | Normal | 172000 |
| 3 | RL | 93.0 | 11160 | Pave | NaN | Reg | Lvl | AllPub | Corner | Gtl | ... | 0 | NaN | NaN | NaN | 0 | 4 | 2010 | WD | Normal | 244000 |
| 4 | RL | 74.0 | 13830 | Pave | NaN | IR1 | Lvl | AllPub | Inside | Gtl | ... | 0 | NaN | MnPrv | NaN | 0 | 3 | 2010 | WD | Normal | 189900 |
5 rows × 79 columns
Why we might do this:
price_per_sqft, unit_price).1, 2, 3 → "Jan", "Feb", "Mar").Why we might do this:
price_per_sqft, unit_price).1, 2, 3 → "Jan", "Feb", "Mar").| ms_zoning | lot_frontage | lot_area | street | alley | lot_shape | land_contour | utilities | lot_config | land_slope | ... | pool_qc | fence | misc_feature | misc_val | mo_sold | yr_sold | sale_type | sale_condition | saleprice | price_per_sqft | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | RL | 141.0 | 31770 | Pave | NaN | IR1 | Lvl | AllPub | Corner | Gtl | ... | NaN | NaN | NaN | 0 | 5 | 2010 | WD | Normal | 215000 | 129.830918 |
| 1 | RH | 80.0 | 11622 | Pave | NaN | Reg | Lvl | AllPub | Inside | Gtl | ... | NaN | MnPrv | NaN | 0 | 6 | 2010 | WD | Normal | 105000 | 117.187500 |
| 2 | RL | 81.0 | 14267 | Pave | NaN | IR1 | Lvl | AllPub | Corner | Gtl | ... | NaN | NaN | Gar2 | 12500 | 6 | 2010 | WD | Normal | 172000 | 129.420617 |
| 3 | RL | 93.0 | 11160 | Pave | NaN | Reg | Lvl | AllPub | Corner | Gtl | ... | NaN | NaN | NaN | 0 | 4 | 2010 | WD | Normal | 244000 | 115.639810 |
| 4 | RL | 74.0 | 13830 | Pave | NaN | IR1 | Lvl | AllPub | Inside | Gtl | ... | NaN | MnPrv | NaN | 0 | 3 | 2010 | WD | Normal | 189900 | 116.574586 |
5 rows × 80 columns
Why we might do this:
price_per_sqft, unit_price).1, 2, 3 → "Jan", "Feb", "Mar").| ms_zoning | lot_frontage | lot_area | street | alley | lot_shape | land_contour | utilities | lot_config | land_slope | ... | pool_qc | fence | misc_feature | misc_val | mo_sold | yr_sold | sale_type | sale_condition | saleprice | price_per_sqft | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | RL | 141.0 | 31770 | Pave | NaN | IR1 | Lvl | AllPub | Corner | Gtl | ... | NaN | NaN | NaN | 0 | May | 2010 | WD | Normal | 215000 | 129.830918 |
| 1 | RH | 80.0 | 11622 | Pave | NaN | Reg | Lvl | AllPub | Inside | Gtl | ... | NaN | MnPrv | NaN | 0 | Jun | 2010 | WD | Normal | 105000 | 117.187500 |
| 2 | RL | 81.0 | 14267 | Pave | NaN | IR1 | Lvl | AllPub | Corner | Gtl | ... | NaN | NaN | Gar2 | 12500 | Jun | 2010 | WD | Normal | 172000 | 129.420617 |
| 3 | RL | 93.0 | 11160 | Pave | NaN | Reg | Lvl | AllPub | Corner | Gtl | ... | NaN | NaN | NaN | 0 | Apr | 2010 | WD | Normal | 244000 | 115.639810 |
| 4 | RL | 74.0 | 13830 | Pave | NaN | IR1 | Lvl | AllPub | Inside | Gtl | ... | NaN | MnPrv | NaN | 0 | Mar | 2010 | WD | Normal | 189900 | 116.574586 |
5 rows × 80 columns
We can always check for missing values with isnull().
pool_qc 2917
misc_feature 2824
alley 2732
fence 2358
mas_vnr_type 1775
...
yr_sold 0
sale_type 0
sale_condition 0
saleprice 0
price_per_sqft 0
Length: 80, dtype: int64
Warning
Why are there so many pool quality (pool_qc) values missing?
Business Question: Our marketing team wants cleaner product category names for their dashboard. Can we standardize the Complete Journey product categories?
product_category
GREETING CARDS/WRAP/PARTY SPLY 2785
CANDY - PACKAGED 2475
MAKEUP AND TREATMENT 2467
HAIR CARE PRODUCTS 1744
SOFT DRINKS 1704
...
BOUQUET (NON ROSE) 1
MISCELLANEOUS CROUTONS 1
EASTER LILY 1
PKG.SEAFOOD MISC 1
FROZEN PACKAGE MEAT 1
Name: count, Length: 303, dtype: int64
Your turn: Help me clean these category names by:
clean_categoryBusiness Question: Our analytics team wants to calculate unit prices to identify premium vs budget products.
| sales_value | quantity | |
|---|---|---|
| 0 | 0.50 | 1 |
| 1 | 0.99 | 1 |
| 2 | 1.43 | 1 |
| 3 | 1.50 | 1 |
| 4 | 2.78 | 2 |
Last week we saw how we can compute various summary stats for a given column:
Avg Sale Price: $180,796.06
Min & Max Price per Sqft: $15.37 - $276.25
But, when we want to get more complicated and get:
Then we should start using .aggregate() / .agg()
That’s great and all but in many real-world analyses, we’re interested in summarizing within groups rather than across the whole dataset.
Grouped aggregation in Pandas always follows the same three-step process:
| neighborhood | saleprice | ||
|---|---|---|---|
| mean | median | ||
| 0 | Blmngtn | 196661.678571 | 191500.0 |
| 1 | Blueste | 143590.000000 | 130500.0 |
| 2 | BrDale | 105608.333333 | 106000.0 |
| 3 | BrkSide | 124756.250000 | 126750.0 |
| 4 | ClearCr | 208662.090909 | 197500.0 |
| 5 | CollgCr | 201803.434457 | 200000.0 |
| 6 | Crawfor | 207550.834951 | 200624.0 |
| 7 | Edwards | 130843.381443 | 125000.0 |
| 8 | Gilbert | 190646.575758 | 183000.0 |
| 9 | Greens | 193531.250000 | 198000.0 |
| 10 | GrnHill | 280000.000000 | 280000.0 |
| 11 | IDOTRR | 103752.903226 | 106500.0 |
| 12 | Landmrk | 137000.000000 | 137000.0 |
| 13 | MeadowV | 95756.486486 | 88250.0 |
| 14 | Mitchel | 162226.631579 | 153500.0 |
| 15 | NAmes | 145097.349887 | 140000.0 |
| 16 | NPkVill | 140710.869565 | 143750.0 |
| 17 | NWAmes | 188406.908397 | 181000.0 |
| 18 | NoRidge | 330319.126761 | 302000.0 |
| 19 | NridgHt | 322018.265060 | 317750.0 |
| 20 | OldTown | 123991.891213 | 119900.0 |
| 21 | SWISU | 135071.937500 | 136200.0 |
| 22 | Sawyer | 136751.152318 | 135000.0 |
| 23 | SawyerW | 184070.184000 | 180000.0 |
| 24 | Somerst | 229707.324176 | 225500.0 |
| 25 | StoneBr | 324229.196078 | 319000.0 |
| 26 | Timber | 246599.541667 | 232106.5 |
| 27 | Veenker | 248314.583333 | 250250.0 |
| neighborhood | mo_sold | saleprice | |
|---|---|---|---|
| 0 | Blmngtn | Apr | 202745.000000 |
| 1 | Blmngtn | Aug | 186828.333333 |
| 2 | Blmngtn | Feb | 194201.000000 |
| 3 | Blmngtn | Jan | 160000.000000 |
| 4 | Blmngtn | Jun | 167773.333333 |
| ... | ... | ... | ... |
| 281 | Veenker | Jul | 234666.666667 |
| 282 | Veenker | Jun | 205308.333333 |
| 283 | Veenker | Mar | 314000.000000 |
| 284 | Veenker | May | 268800.000000 |
| 285 | Veenker | Nov | 385000.000000 |
286 rows × 3 columns
Important
We can answer so many typical business questions with just this skillset!
Business Question: Which products generate the most revenue? Our merchandising team needs this for inventory planning.
| product_id | sales_value | quantity | |
|---|---|---|---|
| 0 | 1095275 | 0.50 | 1 |
| 1 | 9878513 | 0.99 | 1 |
| 2 | 1041453 | 1.43 | 1 |
| 3 | 1020156 | 1.50 | 1 |
| 4 | 1053875 | 2.78 | 2 |
Business Question: Which stores are performing best? Our operations team wants to understand store-level performance.
store_id
367 41334
406 32602
356 27910
292 26692
381 24398
Name: count, dtype: int64
Your turn: Help me compare total sales and transaction counts by store:
Think back to your brainstorm from earlier - Which of your group’s questions require information from more than one dataset?
transactions + demographics.transactions + products.coupon_redemptions + coupons.Important
Most organizations store data in separate tables for:
Being able to combine datasets is essential to answer more complex questions and see the bigger picture.
In the Complete Journey data:
household_id connects transactions with demographicsproduct_id connects transactions with productscoupon_upc connects coupons with coupon_redemptionsGood key characteristics:
customer_id in a customer table)There are 4 primary types of joins you’ll read about this week:
There are 4 primary types of joins you’ll read about this week:
There are 4 primary types of joins you’ll read about this week:
There are 4 primary types of joins you’ll read about this week:
There are 4 primary types of joins you’ll read about this week:
merge() BasicsWe use merge() to join datasets.
Note
Two general approaches your see. Either is fine.
What is the total sales value for the top 10 selling products?
| product_id | product_category | sales_value | |
|---|---|---|---|
| 42214 | 6534178 | COUPON/MISC ITEMS | 303116.02 |
| 42185 | 6533889 | COUPON/MISC ITEMS | 27467.61 |
| 23017 | 1029743 | FLUID MILK PRODUCTS | 22729.71 |
| 42210 | 6534166 | COUPON/MISC ITEMS | 20477.54 |
| 42178 | 6533765 | FUEL | 19451.66 |
| 27873 | 1082185 | TROPICAL FRUIT | 17219.59 |
| 12489 | 916122 | CHICKEN | 16120.01 |
| 30086 | 1106523 | FLUID MILK PRODUCTS | 15629.95 |
| 19812 | 995242 | FLUID MILK PRODUCTS | 15602.59 |
| 39340 | 5569230 | SOFT DRINKS | 13410.46 |
Business Question: Do families with kids spend more than families without kids? Our marketing team wants to target family-friendly promotions.
| household_id | kids_count | |
|---|---|---|
| 0 | 1 | 0 |
| 1 | 1001 | 0 |
| 2 | 1003 | 0 |
| 3 | 1004 | 0 |
| 4 | 101 | 2 |
Continuing our analysis: Now let’s compare average spending by family type.
Discussion: What does this tell us about family spending patterns?
describe(), groupby(), and aggregationsmerge() (inner/left/right/outer)| Task | Syntax Example |
|---|---|
| Rename columns | df.rename(columns={"old":"new"}, inplace=True) |
| Create a new column | df["unit_price"] = df["sales_value"] / df["quantity"] |
| Drop column(s) | df.drop(columns=["col1","col2"], inplace=True) |
| Fill missing values | df["col"].fillna(0, inplace=True) |
| Group and single aggregation | df.groupby("dept")["sales_value"].sum() |
| Group with multiple aggregations | df.groupby("dept").agg({"sales_value":["sum","mean"], "quantity":"sum"}) |
| Sort results | df.sort_values(["sales_value"], ascending=False) |
| Most frequent items | df["product_id"].value_counts() |
| Join two tables (inner) | pd.merge(left_df, right_df, on="key", how="left) |
.fillna()Important
Be sure to finish the Week 4 readings before Thursday’s lab so you can hit the ground running!
Open floor for any questions regarding…
BANA 4080 | Week 4