1. Lighthouse

Level 1 · Where do customers come from?

The request

From Jordan Reyes, Head of Marketing

“I want the referral source breakdown for all customers. A third of them never told us, so show those as 'unknown' instead of leaving them out.”

Your goal

Set result to a Series counting customers per referral_source, with missing values replaced by the string 'unknown', as returned by value_counts() (sorted from most to least common).

  • Use the customers DataFramenot done yet
  • Show exactly referral_source countnot done yet
  • 9 rows, sorted as askednot done yet

Your code

Python

Loading the editor…

    ResultPreview of customers · press Run to see yours
    Preview of customers
    customer_idfirst_namelast_nameemailcity
    1OwenHudsonowen.hudson@icloud.comSan Francisco
    2DouglasMendozadmendoza69@gmail.comAtlanta
    3HenryBrookshbrooks@lakesidecafe.comLos Angeles
    4BrianGrahambgraham@bluecedarhomes.comSeattle

    Mote checks your result

    Press Run. Each row that matches the goal becomes a firefly in your lantern.

    DataFrames

    Loaded for you as pandas DataFrames, plus pd and np.

    customers2,018 rows · 9 columns
    • customer_idinteger
    • first_namevarchar
    • last_namevarchar
    • emailvarchar
    • cityvarchar
    • statevarchar
    • signup_datedate
    • segmentvarchar
    • referral_sourcevarchar
    First 5 rows of customers
    customer_idfirst_namelast_nameemailcitystatesignup_datesegmentreferral_source
    1OwenHudsonowen.hudson@icloud.comSan FranciscoCA2022-06-02consumersearch
    2DouglasMendozadmendoza69@gmail.comAtlantaGA2022-06-02consumerNaN
    3HenryBrookshbrooks@lakesidecafe.comLos AngelesCA2022-06-04businessNaN
    4BrianGrahambgraham@bluecedarhomes.comSeattleWA2022-06-06businesspinterest
    5KyleJimenezkjimenez27@yahoo.comIndianapolisIN2022-06-07consumerNaN
    products120 rows · 8 columns
    • product_idinteger
    • namevarchar
    • categoryvarchar
    • subcategoryvarchar
    • unit_pricedecimal
    • unit_costdecimal
    • launched_ondate
    • is_activeboolean
    First 5 rows of products
    product_idnamecategorysubcategoryunit_priceunit_costlaunched_onis_active
    101Vale Velvet LoveseatFurnitureSofas1659.00NaN2023-09-11true
    102Alder Oak Coffee TableFurnitureTables919.00503.502024-05-16true
    103Orchard Rattan Dining ChairFurnitureChairs789.00429.872023-05-27true
    104Upland Oak BookcaseFurnitureStorage429.00214.702022-12-20true
    105Avery Leather Bed FrameFurnitureBeds449.00232.702021-08-31true
    orders12,000 rows · 7 columns
    • order_idinteger
    • customer_idinteger
    • order_datetimestamp
    • statusvarchar
    • channelvarchar
    • shipping_feedecimal
    • coupon_codevarchar
    First 5 rows of orders
    order_idcustomer_idorder_datestatuschannelshipping_feecoupon_code
    10011252023-01-01 20:51:19completedweb0.00NaN
    10021562023-01-02 07:58:46completedweb0.00NaN
    1003222023-01-02 10:25:27completedmarketplace4.99WELCOME10
    1004482023-01-02 11:21:51completedmarketplace4.99WELCOME10
    1005822023-01-02 11:43:36completedweb0.00WELCOME10
    order_items30,338 rows · 6 columns
    • order_item_idinteger
    • order_idinteger
    • product_idinteger
    • quantityinteger
    • unit_pricedecimal
    • discount_pctdecimal
    First 5 rows of order_items
    order_item_idorder_idproduct_idquantityunit_pricediscount_pct
    11001121931.990.00
    21001154311.990.00
    31002154411.990.00
    410021941149.000.00
    51002193219.990.00
    returns1,673 rows · 5 columns
    • return_idinteger
    • order_item_idinteger
    • returned_ondate
    • reasonvarchar
    • refund_amountdecimal
    First 5 rows of returns
    return_idorder_item_idreturned_onreasonrefund_amount
    1142023-01-10damaged83.69
    2422023-01-10wrong_item19.99
    31052023-01-14wrong_item10.99
    4242023-01-16changed_mind26.99
    51262023-01-16late20.99
    employees60 rows · 8 columns
    • employee_idinteger
    • first_namevarchar
    • last_namevarchar
    • departmentvarchar
    • titlevarchar
    • manager_idinteger
    • hire_datedate
    • salaryinteger
    First 5 rows of employees
    employee_idfirst_namelast_namedepartmenttitlemanager_idhire_datesalary
    1CatherineHaleExecutiveChief Executive OfficerNaN2016-03-01285000
    2RobertKimExecutiveChief Operating Officer12017-01-09235000
    3MeganCollinsSalesHead of Sales12018-04-16172000
    4ChloeGutierrezSalesAccount Executive32020-01-2172000
    5FrankBellSalesAccount Executive32023-05-2173000
    web_sessions40,000 rows · 7 columns
    • session_idinteger
    • customer_idinteger
    • started_attimestamp
    • devicevarchar
    • sourcevarchar
    • pages_viewedinteger
    • convertedboolean
    First 5 rows of web_sessions
    session_idcustomer_idstarted_atdevicesourcepages_viewedconverted
    1NaN2023-01-01 23:02:24mobilepaid1false
    2182023-01-01 15:07:49desktoppaid1false
    3NaN2023-01-01 16:55:46desktopdirect7false
    4NaN2023-01-01 12:43:28mobileorganic3false
    5NaN2023-01-01 13:06:21mobilesocial2false
    marketing_spend3,276 rows · 3 columns
    • spend_datedate
    • channelvarchar
    • amountdecimal
    First 5 rows of marketing_spend
    spend_datechannelamount
    2023-01-01paid492.99
    2023-01-01email37.38
    2023-01-01social200.53
    2023-01-02paid709.13
    2023-01-02email64.13

    The lesson

    What missing looks like

    In plain Python a missing value is None. Inside a pandas column it shows up as NaN (for numbers and text loaded from CSV) or NaT (for dates): an empty cell in the CSV becomes NaN. Either way, missing is not zero and not an empty string; it means “we don’t know”.

    Finding them is the first step:

    customers.isna().sum()
    customer_id          0
    ...
    city                14
    ...
    referral_source    666

    isna() returns a True/False table, and .sum() counts the True values per column. notna() is the opposite. customers["referral_source"].isna().mean() gives the share missing, here about a third.

    Missing is information

    Ask why values are missing before deciding what to do. Here, referral_source is missing because many customers skipped the “how did you hear about us?” question. That’s natural, not an error, and it’s a sizeable group, so it belongs in the report as its own category.

    city is missing on 14 rows, likely a data-entry gap. products.unit_cost is missing for 9 products whose cost wasn’t recorded. Each case calls for a different treatment.

    fillna: replace with a value

    customers["referral_source"].fillna("unknown")
    products["unit_cost"].fillna(products["unit_cost"].median())   # an estimate, use with care

    fillna returns a new Series. To change the DataFrame, assign it back to the column: customers["referral_source"] = customers["referral_source"].fillna("unknown"). Avoid fillna(..., inplace=True) on a selected column; it’s a form of chained assignment that may not change the original DataFrame.

    Fill text categories with an explicit label like "unknown". Be much more careful with numbers. Filling missing costs with 0 would make those products look 100% profitable, and filling with an average invents data. Often the honest choice is to leave numbers missing and exclude them from that specific calculation, saying so in a footnote.

    dropna: remove rows

    customers.dropna(subset=["city"])          # drop rows with no city
    products.dropna(subset=["unit_cost"])      # only products with a known cost

    Always pass subset=. Without it, dropna() drops any row with any missing value, and in customers that would throw away a third of the table because of referral source.

    Dropping rows changes your population. A referral breakdown computed after dropna() describes only customers who answered, which overstates every channel’s share.

    How pandas treats NaN in calculations

    • sum, mean, count, max and friends skip NaN by default.
    • value_counts and groupby leave out NaN keys by default; use dropna=False to keep them.
    • Comparisons with NaN are always False, even np.nan == np.nan. Use isna() to test.
    • Arithmetic with NaN gives NaN: unit_price - unit_cost is NaN when the cost is missing.

    So the shortcut for this level is also customers["referral_source"].value_counts(dropna=False), which counts NaN as its own row. Using fillna("unknown") first gives that row a readable label, which is better for anything you’ll show people.

    Common mistakes

    • Reporting shares after value_counts() without dropna=False or fillna.
    • Calling dropna() without subset and losing most of the table.
    • Filling numeric gaps with 0 by reflex.
    • Testing == np.nan, which is never True.

    If you know SQL

    fillna is COALESCE(referral_source, 'unknown'), and isna() is IS NULL.