The request

From Jordan Reyes, Head of Marketing
Your goal
marketing_spend should have one row per day and channel from 2023-01-01 to 2025-12-31. Set result to a DataFrame with columns channel, missing_days (days in that range with no row for the channel), recorded_total (sum of amount as recorded, rounded to 2) and filled_total (sum after filling each missing day with the previous day's amount for that channel, rounded to 2), one row per channel, sorted by channel A to Z, index reset.
- Use the
marketing_spendDataFramenot done yet - Show exactly
channelmissing_daysrecorded_totalfilled_totalnot done yet - 3 rows, sorted as askednot done yet
Your code
Loading the editor…
Reference solution
| spend_date | channel | amount |
|---|---|---|
| 2023-01-01 | paid | 492.99 |
| 2023-01-01 | 37.38 | |
| 2023-01-01 | social | 200.53 |
| 2023-01-02 | paid | 709.13 |
Mote checks your result
Press Run. Each row that matches the goal becomes a firefly in your lantern.
What Jordan gets, and why
DataFrames
Loaded for you as pandas DataFrames, plus pd and np.
marketing_spend3,276 rows · 3 columns
spend_datedatechannelvarcharamountdecimal
| spend_date | channel | amount |
|---|---|---|
| 2023-01-01 | paid | 492.99 |
| 2023-01-01 | 37.38 | |
| 2023-01-01 | social | 200.53 |
| 2023-01-02 | paid | 709.13 |
| 2023-01-02 | 64.13 |
customers2,018 rows · 9 columns
customer_idintegerfirst_namevarcharlast_namevarcharemailvarcharcityvarcharstatevarcharsignup_datedatesegmentvarcharreferral_sourcevarchar
| customer_id | first_name | last_name | city | state | signup_date | segment | referral_source | |
|---|---|---|---|---|---|---|---|---|
| 1 | Owen | Hudson | owen.hudson@icloud.com | San Francisco | CA | 2022-06-02 | consumer | search |
| 2 | Douglas | Mendoza | dmendoza69@gmail.com | Atlanta | GA | 2022-06-02 | consumer | NaN |
| 3 | Henry | Brooks | hbrooks@lakesidecafe.com | Los Angeles | CA | 2022-06-04 | business | NaN |
| 4 | Brian | Graham | bgraham@bluecedarhomes.com | Seattle | WA | 2022-06-06 | business | |
| 5 | Kyle | Jimenez | kjimenez27@yahoo.com | Indianapolis | IN | 2022-06-07 | consumer | NaN |
products120 rows · 8 columns
product_idintegernamevarcharcategoryvarcharsubcategoryvarcharunit_pricedecimalunit_costdecimallaunched_ondateis_activeboolean
| product_id | name | category | subcategory | unit_price | unit_cost | launched_on | is_active |
|---|---|---|---|---|---|---|---|
| 101 | Vale Velvet Loveseat | Furniture | Sofas | 1659.00 | NaN | 2023-09-11 | true |
| 102 | Alder Oak Coffee Table | Furniture | Tables | 919.00 | 503.50 | 2024-05-16 | true |
| 103 | Orchard Rattan Dining Chair | Furniture | Chairs | 789.00 | 429.87 | 2023-05-27 | true |
| 104 | Upland Oak Bookcase | Furniture | Storage | 429.00 | 214.70 | 2022-12-20 | true |
| 105 | Avery Leather Bed Frame | Furniture | Beds | 449.00 | 232.70 | 2021-08-31 | true |
orders12,000 rows · 7 columns
order_idintegercustomer_idintegerorder_datetimestampstatusvarcharchannelvarcharshipping_feedecimalcoupon_codevarchar
| order_id | customer_id | order_date | status | channel | shipping_fee | coupon_code |
|---|---|---|---|---|---|---|
| 1001 | 125 | 2023-01-01 20:51:19 | completed | web | 0.00 | NaN |
| 1002 | 156 | 2023-01-02 07:58:46 | completed | web | 0.00 | NaN |
| 1003 | 22 | 2023-01-02 10:25:27 | completed | marketplace | 4.99 | WELCOME10 |
| 1004 | 48 | 2023-01-02 11:21:51 | completed | marketplace | 4.99 | WELCOME10 |
| 1005 | 82 | 2023-01-02 11:43:36 | completed | web | 0.00 | WELCOME10 |
order_items30,338 rows · 6 columns
order_item_idintegerorder_idintegerproduct_idintegerquantityintegerunit_pricedecimaldiscount_pctdecimal
| order_item_id | order_id | product_id | quantity | unit_price | discount_pct |
|---|---|---|---|---|---|
| 1 | 1001 | 121 | 9 | 31.99 | 0.00 |
| 2 | 1001 | 154 | 3 | 11.99 | 0.00 |
| 3 | 1002 | 154 | 4 | 11.99 | 0.00 |
| 4 | 1002 | 194 | 1 | 149.00 | 0.00 |
| 5 | 1002 | 193 | 2 | 19.99 | 0.00 |
returns1,673 rows · 5 columns
return_idintegerorder_item_idintegerreturned_ondatereasonvarcharrefund_amountdecimal
| return_id | order_item_id | returned_on | reason | refund_amount |
|---|---|---|---|---|
| 1 | 14 | 2023-01-10 | damaged | 83.69 |
| 2 | 42 | 2023-01-10 | wrong_item | 19.99 |
| 3 | 105 | 2023-01-14 | wrong_item | 10.99 |
| 4 | 24 | 2023-01-16 | changed_mind | 26.99 |
| 5 | 126 | 2023-01-16 | late | 20.99 |
employees60 rows · 8 columns
employee_idintegerfirst_namevarcharlast_namevarchardepartmentvarchartitlevarcharmanager_idintegerhire_datedatesalaryinteger
| employee_id | first_name | last_name | department | title | manager_id | hire_date | salary |
|---|---|---|---|---|---|---|---|
| 1 | Catherine | Hale | Executive | Chief Executive Officer | NaN | 2016-03-01 | 285000 |
| 2 | Robert | Kim | Executive | Chief Operating Officer | 1 | 2017-01-09 | 235000 |
| 3 | Megan | Collins | Sales | Head of Sales | 1 | 2018-04-16 | 172000 |
| 4 | Chloe | Gutierrez | Sales | Account Executive | 3 | 2020-01-21 | 72000 |
| 5 | Frank | Bell | Sales | Account Executive | 3 | 2023-05-21 | 73000 |
web_sessions40,000 rows · 7 columns
session_idintegercustomer_idintegerstarted_attimestampdevicevarcharsourcevarcharpages_viewedintegerconvertedboolean
| session_id | customer_id | started_at | device | source | pages_viewed | converted |
|---|---|---|---|---|---|---|
| 1 | NaN | 2023-01-01 23:02:24 | mobile | paid | 1 | false |
| 2 | 18 | 2023-01-01 15:07:49 | desktop | paid | 1 | false |
| 3 | NaN | 2023-01-01 16:55:46 | desktop | direct | 7 | false |
| 4 | NaN | 2023-01-01 12:43:28 | mobile | organic | 3 | false |
| 5 | NaN | 2023-01-01 13:06:21 | mobile | social | 2 | false |
The lesson
Missing rows are invisible
A missing value you can see is easy: it’s NaN, and isna() finds it. A missing row is harder.
If the tracker had an outage on 28 January 2023, there simply is no paid row for that day, and
nothing in the table tells you so. resample and rolling happily run over the hole, and a
rolling(7) window quietly covers 8 calendar days.
The fix is to put the data on a complete calendar, so every day that should exist has a row, and the missing ones show up as NaN.
asfreq for one series
For a single series with a DatetimeIndex, asfreq("D") does it in one call:
paid = marketing_spend[marketing_spend["channel"] == "paid"].set_index("spend_date")["amount"]
len(paid) # 1091
filled = paid.asfreq("D")
len(filled) # 1096
filled.isna().sum() # 5
asfreq builds a regular daily index from the first to the last date and inserts NaN where there
was no row. Three years is 1,096 days (2024 is a leap year), so the paid channel is missing 5 days.
reindex with date_range for many columns
asfreq only fills between the first and last date present. If the whole first week were missing,
it couldn’t know. pd.date_range lets you state the expected calendar explicitly, and reindex
puts the table on it:
days = pd.date_range("2023-01-01", "2025-12-31", freq="D")
wide = marketing_spend.pivot(index="spend_date", columns="channel", values="amount").reindex(days)
wide.isna().sum()
# email 4, paid 5, social 3
pivot turns the long table into one column per channel (chapter 9 covers it in depth), so all
three channels get checked against the same calendar at once.
Choosing a fill
Once the gaps are visible, you decide what they mean. For paid spend, the daily average comes out differently depending on that choice:
- Skip them (the default for
mean()andsum()): average $814.64 over the 1,091 recorded days. fillna(0): “nothing was spent”. Average $810.93 over 1,096 days. Wrong here: the money was spent, only the tracking failed.ffill(): carry the last known value forward. The total rises from $888,774.13 to $892,327.94.interpolate(): a straight line between the neighbours, total $892,420.97.
Zero is right when a missing day really means zero (no orders on a closed day). Forward fill or interpolation is right when the value existed but wasn’t recorded. Say which one you used.
Common mistakes
- Running
rolling(7)on a series with missing days, so the window is longer than a week. - Using
fillna(0)for an outage, which understates spend and overstates return on ad spend. - Using
asfreqwhen gaps may sit at the very start or end of the range;reindexwith an explicitdate_rangecatches those. - Forgetting that
sum()andmean()skip NaN, so a table with gaps still “adds up”, just to the wrong number.
Lantern lit!
Practised: asfreqdate rangereindexffillfillnamissing dates
- Solved
- No hints
- First run
