[{"data":1,"prerenderedAt":1898},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-funnel-conversion-table-from-event-data":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-funnel-conversion-table-from-event-data":1865,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-funnel-conversion-table-from-event-data":1874,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-funnel-conversion-table-from-event-data":1879,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-funnel-conversion-table-from-event-data":1880},{"id":4,"title":5,"body":6,"dateModified":1832,"datePublished":1832,"description":1833,"extension":1834,"faq":1835,"keyword":1844,"kind":1845,"meta":1846,"navigation":113,"path":1855,"seo":1856,"slug":1859,"stem":1860,"topics":1861,"type":1863,"__hash__":1864},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-funnel-conversion-table-from-event-data\u002Findex.md","Build a Funnel Conversion Table from Event Data",{"type":7,"value":8,"toc":1819},"minimark",[9,24,29,58,78,82,85,203,206,210,213,313,324,576,580,587,953,956,1038,1042,1045,1270,1273,1277,1600,1612,1695,1699,1710,1714,1749,1753,1756,1760,1766,1772,1778,1784,1788,1815],[10,11,12,13,17,18,23],"p",{},"A product team exports an event log — every visit, sign-up, trial start and purchase, one row per event — and asks the obvious question: of the people who came, how many signed up, how many started a trial, how many bought, and where do we lose them? That is a funnel. Counting distinct users per event type looks like the answer and is not: someone who bought without ever starting a trial, or who signed up a year after their visit, inflates the later steps, and a person with five visits counts five times unless you are careful. A proper funnel follows each user through the steps ",[14,15,16],"em",{},"in order",", within a time window, and counts each user once per step. This page builds that with pandas, adds conversion and drop-off, breaks the funnel down by acquisition channel, measures how long each step takes, and writes the table to Excel. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002F","Creating Pivot Tables from Excel Data",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[37,38,39],"code",{"__ignoreMap":35},[40,41,44,48,52,55],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sMTad","pip",[40,49,51],{"class":50},"srMev"," install",[40,53,54],{"class":50}," pandas",[40,56,57],{"class":50}," openpyxl\n",[10,59,60,63,64,67,68,67,71,67,74,77],{},[37,61,62],{},"events.xlsx"," is an event log of about 10,000 rows for 4,000 users in August and September 2026: user ID, event name (",[37,65,66],{},"visit",", ",[37,69,70],{},"signup",[37,72,73],{},"trial",[37,75,76],{},"purchase","), timestamp and acquisition channel. Like real logs it is unsorted, has repeated visits per user, and contains a few out-of-order events — purchases without a trial, and sign-ups recorded before the first visit.",[25,79,81],{"id":80},"step-1-the-naive-count-and-why-it-misleads","Step 1 — The naive count, and why it misleads",[10,83,84],{},"Counting distinct users per event is quick:",[30,86,90],{"className":87,"code":88,"language":89,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nevents = pd.read_excel(\"events.xlsx\")\nSTEPS = [\"visit\", \"signup\", \"trial\", \"purchase\"]\nnaive = events.groupby(\"Event\")[\"User\"].nunique().reindex(STEPS)\nprint(naive.to_string())\n","python",[37,91,92,108,115,133,167,194],{"__ignoreMap":35},[40,93,94,98,102,105],{"class":42,"line":43},[40,95,97],{"class":96},"s-kum","import",[40,99,101],{"class":100},"skGVy"," pandas ",[40,103,104],{"class":96},"as",[40,106,107],{"class":100}," pd\n",[40,109,111],{"class":42,"line":110},2,[40,112,114],{"emptyLinePlaceholder":113},true,"\n",[40,116,118,121,124,127,130],{"class":42,"line":117},3,[40,119,120],{"class":100},"events ",[40,122,123],{"class":96},"=",[40,125,126],{"class":100}," pd.read_excel(",[40,128,129],{"class":50},"\"events.xlsx\"",[40,131,132],{"class":100},")\n",[40,134,136,140,143,146,149,151,154,156,159,161,164],{"class":42,"line":135},4,[40,137,139],{"class":138},"sP0c6","STEPS",[40,141,142],{"class":96}," =",[40,144,145],{"class":100}," [",[40,147,148],{"class":50},"\"visit\"",[40,150,67],{"class":100},[40,152,153],{"class":50},"\"signup\"",[40,155,67],{"class":100},[40,157,158],{"class":50},"\"trial\"",[40,160,67],{"class":100},[40,162,163],{"class":50},"\"purchase\"",[40,165,166],{"class":100},"]\n",[40,168,170,173,175,178,181,184,187,190,192],{"class":42,"line":169},5,[40,171,172],{"class":100},"naive ",[40,174,123],{"class":96},[40,176,177],{"class":100}," events.groupby(",[40,179,180],{"class":50},"\"Event\"",[40,182,183],{"class":100},")[",[40,185,186],{"class":50},"\"User\"",[40,188,189],{"class":100},"].nunique().reindex(",[40,191,139],{"class":138},[40,193,132],{"class":100},[40,195,197,200],{"class":42,"line":196},6,[40,198,199],{"class":138},"print",[40,201,202],{"class":100},"(naive.to_string())\n",[10,204,205],{},"These numbers count anyone who ever had each event, regardless of order. A user who bought directly through a salesperson appears under \"purchase\" without having started a trial, and a user whose sign-up was logged before their first visit appears under \"signup\" without having come through the front door first. Each such case makes the later steps look better than the journey actually performs. The differences are small here, but in real logs — with imports from other systems, re-used accounts and clock problems — they can be large, and they are invisible in a naive count.",[25,207,209],{"id":208},"step-2-first-time-each-user-reached-each-step","Step 2 — First time each user reached each step",[10,211,212],{},"Reduce the log to one timestamp per user and step — the first time it happened — and spread the steps into columns:",[30,214,216],{"className":87,"code":215,"language":89,"meta":35,"style":35},"first = (events.groupby([\"User\", \"Event\"])[\"Timestamp\"].min()\n               .unstack(\"Event\").reindex(columns=STEPS))\nchannel = events.groupby(\"User\")[\"Channel\"].first()\nprint(first.head(4))\nprint(\"users:\", len(first))\n",[37,217,218,243,264,283,295],{"__ignoreMap":35},[40,219,220,223,225,228,230,232,234,237,240],{"class":42,"line":43},[40,221,222],{"class":100},"first ",[40,224,123],{"class":96},[40,226,227],{"class":100}," (events.groupby([",[40,229,186],{"class":50},[40,231,67],{"class":100},[40,233,180],{"class":50},[40,235,236],{"class":100},"])[",[40,238,239],{"class":50},"\"Timestamp\"",[40,241,242],{"class":100},"].min()\n",[40,244,245,248,250,253,257,259,261],{"class":42,"line":110},[40,246,247],{"class":100},"               .unstack(",[40,249,180],{"class":50},[40,251,252],{"class":100},").reindex(",[40,254,256],{"class":255},"sa561","columns",[40,258,123],{"class":96},[40,260,139],{"class":138},[40,262,263],{"class":100},"))\n",[40,265,266,269,271,273,275,277,280],{"class":42,"line":117},[40,267,268],{"class":100},"channel ",[40,270,123],{"class":96},[40,272,177],{"class":100},[40,274,186],{"class":50},[40,276,183],{"class":100},[40,278,279],{"class":50},"\"Channel\"",[40,281,282],{"class":100},"].first()\n",[40,284,285,287,290,293],{"class":42,"line":135},[40,286,199],{"class":138},[40,288,289],{"class":100},"(first.head(",[40,291,292],{"class":138},"4",[40,294,263],{"class":100},[40,296,297,299,302,305,307,310],{"class":42,"line":169},[40,298,199],{"class":138},[40,300,301],{"class":100},"(",[40,303,304],{"class":50},"\"users:\"",[40,306,67],{"class":100},[40,308,309],{"class":138},"len",[40,311,312],{"class":100},"(first))\n",[10,314,315,316,319,320,323],{},"Each row is now one user and each column the first time they reached a step, or ",[37,317,318],{},"NaT"," if never. Repeated visits collapse to the earliest, so heavy browsers count once. The channel is taken per user (here every event of a user shares a channel; in real data you would choose the ",[14,321,322],{},"first-touch"," channel deliberately). This wide shape makes the ordered checks in the next step one comparison per column.",[325,326,334,335,334,339,334,343,334,350,334,359,334,363,334,370,334,374,334,378,334,382,334,386,334,390,334,395,334,397,334,401,334,403,334,406,334,408,334,411,334,414,334,418,334,420,334,424,334,426,334,428,334,430,334,433,334,436,334,440,334,442,334,444,334,446,334,448,334,450,334,453,334,456,334,459,334,461,334,463,334,465,334,467,334,469,334,472,334,475,334,479,334,481,334,483,334,485,334,487,334,489,334,492,334,496,334,500,334,504,334,507,334,511,334,514,334,518,334,521,334,524,334,527,334,529,334,531,334,533,334,535,334,538,334,541,334,547,334,551,334,557,334,562,334,570],"svg",{"viewBox":327,"role":328,"ariaLabelledBy":329,"xmlns":332,"style":333},"4 0 752 220","img",[330,331],"fn-wide-t","fn-wide-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[336,337,338],"title",{"id":330},"An event log becomes one row per user",[340,341,342],"desc",{"id":331},"A user's three visits, a signup and a trial in the log become one row with the first visit time, the signup time, the trial time and no purchase time.",[344,345],"rect",{"x":292,"y":346,"width":347,"height":348,"fill":349},"0","752","220","#ffffff",[344,351],{"x":352,"y":353,"width":354,"height":355,"rx":346,"fill":356,"stroke":357,"style":358},"20.0","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[344,360],{"x":361,"y":353,"width":362,"height":355,"rx":346,"fill":356,"stroke":357,"style":358},"52.0","49.2",[364,365,369],"text",{"x":366,"y":367,"style":368},"76.6","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[344,371],{"x":372,"y":353,"width":373,"height":355,"rx":346,"fill":356,"stroke":357,"style":358},"101.2","43.6",[364,375,377],{"x":376,"y":367,"style":368},"123.0","B",[344,379],{"x":380,"y":353,"width":381,"height":355,"rx":346,"fill":356,"stroke":357,"style":358},"144.8","68.9",[364,383,385],{"x":384,"y":367,"style":368},"179.2","C",[344,387],{"x":352,"y":388,"width":354,"height":389,"rx":346,"fill":356,"stroke":357,"style":358},"64.0","28.0",[364,391,394],{"x":392,"y":393,"style":368},"36.0","83.0","1",[344,396],{"x":361,"y":388,"width":362,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,398,400],{"x":366,"y":393,"style":399},"font-size:10.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","User",[344,402],{"x":372,"y":388,"width":373,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,404,405],{"x":376,"y":393,"style":399},"Event",[344,407],{"x":380,"y":388,"width":381,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,409,410],{"x":384,"y":393,"style":399},"Time",[344,412],{"x":352,"y":413,"width":354,"height":389,"rx":346,"fill":356,"stroke":357,"style":358},"92.0",[364,415,417],{"x":392,"y":416,"style":368},"111.0","2",[344,419],{"x":361,"y":413,"width":362,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,421,423],{"x":366,"y":416,"style":422},"font-size:10.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","U00042",[344,425],{"x":372,"y":413,"width":373,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,427,66],{"x":376,"y":416,"style":422},[344,429],{"x":380,"y":413,"width":381,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,431,432],{"x":384,"y":416,"style":422},"02 Aug 10:15",[344,434],{"x":352,"y":435,"width":354,"height":389,"rx":346,"fill":356,"stroke":357,"style":358},"120.0",[364,437,439],{"x":392,"y":438,"style":368},"139.0","3",[344,441],{"x":361,"y":435,"width":362,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,443,423],{"x":366,"y":438,"style":422},[344,445],{"x":372,"y":435,"width":373,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,447,66],{"x":376,"y":438,"style":422},[344,449],{"x":380,"y":435,"width":381,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,451,452],{"x":384,"y":438,"style":422},"03 Aug 18:02",[344,454],{"x":352,"y":455,"width":354,"height":389,"rx":346,"fill":356,"stroke":357,"style":358},"148.0",[364,457,292],{"x":392,"y":458,"style":368},"167.0",[344,460],{"x":361,"y":455,"width":362,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,462,423],{"x":366,"y":458,"style":422},[344,464],{"x":372,"y":455,"width":373,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,466,70],{"x":376,"y":458,"style":422},[344,468],{"x":380,"y":455,"width":381,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,470,471],{"x":384,"y":458,"style":422},"02 Aug 21:40",[344,473],{"x":352,"y":474,"width":354,"height":389,"rx":346,"fill":356,"stroke":357,"style":358},"176.0",[364,476,478],{"x":392,"y":477,"style":368},"195.0","5",[344,480],{"x":361,"y":474,"width":362,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,482,423],{"x":366,"y":477,"style":422},[344,484],{"x":372,"y":474,"width":373,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,486,73],{"x":376,"y":477,"style":422},[344,488],{"x":380,"y":474,"width":381,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,490,491],{"x":384,"y":477,"style":422},"04 Aug 09:12",[344,493],{"x":494,"y":353,"width":495,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},"469.5","70.0",[364,497,66],{"x":498,"y":499,"style":399},"504.5","59.0",[344,501],{"x":502,"y":353,"width":503,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},"539.5","72.1",[364,505,70],{"x":506,"y":499,"style":399},"575.5",[344,508],{"x":509,"y":353,"width":510,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},"611.5","72.2",[364,512,73],{"x":513,"y":499,"style":399},"647.7",[344,515],{"x":516,"y":353,"width":517,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},"683.8","56.2",[364,519,76],{"x":520,"y":499,"style":399},"711.9",[344,522],{"x":494,"y":523,"width":495,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},"68.0",[364,525,432],{"x":498,"y":526,"style":422},"87.0",[344,528],{"x":502,"y":523,"width":503,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,530,471],{"x":506,"y":526,"style":422},[344,532],{"x":509,"y":523,"width":510,"height":389,"rx":346,"fill":349,"stroke":357,"style":358},[364,534,491],{"x":513,"y":526,"style":422},[344,536],{"x":516,"y":523,"width":517,"height":389,"rx":346,"fill":356,"stroke":357,"style":537},"stroke-width:1.5px",[364,539,540],{"x":520,"y":526,"style":399},"—",[364,542,546],{"x":543,"y":544,"style":545},"116.8","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","event log",[364,548,550],{"x":549,"y":544,"style":545},"604.7","per user",[42,552],{"x1":553,"y1":523,"x2":554,"y2":523,"stroke":555,"style":556},"223.7","455.5","var(--brand,#5b5cf0)","stroke-width:2px",[558,559],"polygon",{"points":560,"fill":561},"459.5,68.0 450.5,73.0 450.5,63.0","#5b5cf0",[344,563],{"x":564,"y":565,"width":566,"height":567,"rx":568,"fill":349,"stroke":357,"style":569},"255.2","35.0","172.7","22.0","11","stroke-width:1.2px",[364,571,575],{"x":572,"y":573,"style":574},"341.6","50.0","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","groupby min + unstack",[25,577,579],{"id":578},"step-3-an-ordered-funnel-with-a-conversion-window","Step 3 — An ordered funnel with a conversion window",[10,581,582,583,586],{},"A user counts for a step only if they also reached the previous step, ",[14,584,585],{},"after"," it, and within a window — here 30 days from their first visit:",[30,588,590],{"className":87,"code":589,"language":89,"meta":35,"style":35},"WINDOW = pd.Timedelta(days=30)\nreached = pd.DataFrame(index=first.index)\nreached[\"visit\"] = first[\"visit\"].notna()\nprev_time = first[\"visit\"]\nfor step in STEPS[1:]:\n    ok = reached[STEPS[STEPS.index(step) - 1]] & first[step].notna() & (first[step] >= prev_time) \\\n         & (first[step] \u003C= first[\"visit\"] + WINDOW)\n    reached[step] = ok\n    prev_time = first[step].where(ok)\n\nfunnel = pd.DataFrame({\"Users\": reached.sum()})\nfunnel[\"Naive count\"] = naive\nfunnel[\"Step conversion %\"] = (funnel[\"Users\"] \u002F funnel[\"Users\"].shift(1) * 100).round(1)\nfunnel[\"Overall %\"] = (funnel[\"Users\"] \u002F funnel[\"Users\"].iloc[0] * 100).round(1)\nfunnel[\"Lost at this step\"] = (funnel[\"Users\"].shift(1) - funnel[\"Users\"]).fillna(0).astype(int)\nprint(funnel.to_string())\n",[37,591,592,612,630,650,663,685,730,755,766,777,782,799,815,863,904,945],{"__ignoreMap":35},[40,593,594,597,599,602,605,607,610],{"class":42,"line":43},[40,595,596],{"class":138},"WINDOW",[40,598,142],{"class":96},[40,600,601],{"class":100}," pd.Timedelta(",[40,603,604],{"class":255},"days",[40,606,123],{"class":96},[40,608,609],{"class":138},"30",[40,611,132],{"class":100},[40,613,614,617,619,622,625,627],{"class":42,"line":110},[40,615,616],{"class":100},"reached ",[40,618,123],{"class":96},[40,620,621],{"class":100}," pd.DataFrame(",[40,623,624],{"class":255},"index",[40,626,123],{"class":96},[40,628,629],{"class":100},"first.index)\n",[40,631,632,635,637,640,642,645,647],{"class":42,"line":117},[40,633,634],{"class":100},"reached[",[40,636,148],{"class":50},[40,638,639],{"class":100},"] ",[40,641,123],{"class":96},[40,643,644],{"class":100}," first[",[40,646,148],{"class":50},[40,648,649],{"class":100},"].notna()\n",[40,651,652,655,657,659,661],{"class":42,"line":135},[40,653,654],{"class":100},"prev_time ",[40,656,123],{"class":96},[40,658,644],{"class":100},[40,660,148],{"class":50},[40,662,166],{"class":100},[40,664,665,668,671,674,677,680,682],{"class":42,"line":169},[40,666,667],{"class":96},"for",[40,669,670],{"class":100}," step ",[40,672,673],{"class":96},"in",[40,675,676],{"class":138}," STEPS",[40,678,679],{"class":100},"[",[40,681,394],{"class":138},[40,683,684],{"class":100},":]:\n",[40,686,687,690,692,695,697,699,701,704,707,710,713,716,719,721,724,727],{"class":42,"line":196},[40,688,689],{"class":100},"    ok ",[40,691,123],{"class":96},[40,693,694],{"class":100}," reached[",[40,696,139],{"class":138},[40,698,679],{"class":100},[40,700,139],{"class":138},[40,702,703],{"class":100},".index(step) ",[40,705,706],{"class":96},"-",[40,708,709],{"class":138}," 1",[40,711,712],{"class":100},"]] ",[40,714,715],{"class":96},"&",[40,717,718],{"class":100}," first[step].notna() ",[40,720,715],{"class":96},[40,722,723],{"class":100}," (first[step] ",[40,725,726],{"class":96},">=",[40,728,729],{"class":100}," prev_time) \\\n",[40,731,733,736,738,741,743,745,747,750,753],{"class":42,"line":732},7,[40,734,735],{"class":96},"         &",[40,737,723],{"class":100},[40,739,740],{"class":96},"\u003C=",[40,742,644],{"class":100},[40,744,148],{"class":50},[40,746,639],{"class":100},[40,748,749],{"class":96},"+",[40,751,752],{"class":138}," WINDOW",[40,754,132],{"class":100},[40,756,758,761,763],{"class":42,"line":757},8,[40,759,760],{"class":100},"    reached[step] ",[40,762,123],{"class":96},[40,764,765],{"class":100}," ok\n",[40,767,769,772,774],{"class":42,"line":768},9,[40,770,771],{"class":100},"    prev_time ",[40,773,123],{"class":96},[40,775,776],{"class":100}," first[step].where(ok)\n",[40,778,780],{"class":42,"line":779},10,[40,781,114],{"emptyLinePlaceholder":113},[40,783,785,788,790,793,796],{"class":42,"line":784},11,[40,786,787],{"class":100},"funnel ",[40,789,123],{"class":96},[40,791,792],{"class":100}," pd.DataFrame({",[40,794,795],{"class":50},"\"Users\"",[40,797,798],{"class":100},": reached.sum()})\n",[40,800,802,805,808,810,812],{"class":42,"line":801},12,[40,803,804],{"class":100},"funnel[",[40,806,807],{"class":50},"\"Naive count\"",[40,809,639],{"class":100},[40,811,123],{"class":96},[40,813,814],{"class":100}," naive\n",[40,816,818,820,823,825,827,830,832,834,837,840,842,845,847,850,853,856,859,861],{"class":42,"line":817},13,[40,819,804],{"class":100},[40,821,822],{"class":50},"\"Step conversion %\"",[40,824,639],{"class":100},[40,826,123],{"class":96},[40,828,829],{"class":100}," (funnel[",[40,831,795],{"class":50},[40,833,639],{"class":100},[40,835,836],{"class":96},"\u002F",[40,838,839],{"class":100}," funnel[",[40,841,795],{"class":50},[40,843,844],{"class":100},"].shift(",[40,846,394],{"class":138},[40,848,849],{"class":100},") ",[40,851,852],{"class":96},"*",[40,854,855],{"class":138}," 100",[40,857,858],{"class":100},").round(",[40,860,394],{"class":138},[40,862,132],{"class":100},[40,864,866,868,871,873,875,877,879,881,883,885,887,890,892,894,896,898,900,902],{"class":42,"line":865},14,[40,867,804],{"class":100},[40,869,870],{"class":50},"\"Overall %\"",[40,872,639],{"class":100},[40,874,123],{"class":96},[40,876,829],{"class":100},[40,878,795],{"class":50},[40,880,639],{"class":100},[40,882,836],{"class":96},[40,884,839],{"class":100},[40,886,795],{"class":50},[40,888,889],{"class":100},"].iloc[",[40,891,346],{"class":138},[40,893,639],{"class":100},[40,895,852],{"class":96},[40,897,855],{"class":138},[40,899,858],{"class":100},[40,901,394],{"class":138},[40,903,132],{"class":100},[40,905,907,909,912,914,916,918,920,922,924,926,928,930,932,935,937,940,943],{"class":42,"line":906},15,[40,908,804],{"class":100},[40,910,911],{"class":50},"\"Lost at this step\"",[40,913,639],{"class":100},[40,915,123],{"class":96},[40,917,829],{"class":100},[40,919,795],{"class":50},[40,921,844],{"class":100},[40,923,394],{"class":138},[40,925,849],{"class":100},[40,927,706],{"class":96},[40,929,839],{"class":100},[40,931,795],{"class":50},[40,933,934],{"class":100},"]).fillna(",[40,936,346],{"class":138},[40,938,939],{"class":100},").astype(",[40,941,942],{"class":138},"int",[40,944,132],{"class":100},[40,946,948,950],{"class":42,"line":947},16,[40,949,199],{"class":138},[40,951,952],{"class":100},"(funnel.to_string())\n",[10,954,955],{},"Each step requires the previous step to be reached, the event to happen no earlier than the previous one, and within 30 days of the first visit. The ordered counts are lower than the naive ones at every step after the first — 1,085 sign-ups instead of 1,118, and 227 purchases instead of 266. The sign-ups logged before a visit, the purchases without a trial and the purchases that came more than 30 days after the first visit no longer count. The purchase step shrinks by about 15%, which is exactly the kind of overstatement a naive funnel hands to whoever is setting conversion targets. Step conversion divides each step by the one before; overall conversion divides by the number of visitors. The \"lost\" column is what product teams act on — the largest loss is the visit-to-signup step, which is typical: most visitors leave without signing up. The window is a business choice; a longer window counts slow converters, a shorter one gives an earlier, comparable read on recent cohorts.",[325,957,334,963,334,966,334,969,334,973,334,978,334,983,334,988,334,993,334,996,334,999,334,1002,334,1006,334,1009,334,1012,334,1017,334,1022,334,1025,334,1028,334,1033],{"viewBox":958,"role":328,"ariaLabelledBy":959,"xmlns":332,"style":962},"4 0 744 180",[960,961],"fn-bars-t","fn-bars-d","width:100%;max-width:744px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[336,964,965],{"id":960},"Users reaching each step, in order, within 30 days",[340,967,968],{"id":961},"All 4,000 users visited. Roughly a quarter signed up, about half of those started a trial, and around two fifths of trial users purchased. The biggest loss is between visit and signup.",[344,970],{"x":292,"y":346,"width":971,"height":972,"fill":349},"744","180",[344,974],{"x":352,"y":975,"width":976,"height":389,"rx":977,"fill":356,"stroke":357,"style":358},"16.0","88.5","7",[364,979,66],{"x":980,"y":981,"style":982},"64.2","34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[344,984],{"x":985,"y":975,"width":986,"height":389,"rx":977,"fill":987,"stroke":555,"style":537},"118.5","521.9","#f0f4ff",[364,989,992],{"x":990,"y":981,"style":991},"648.4","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start","4,000 · 100%",[344,994],{"x":352,"y":995,"width":976,"height":389,"rx":977,"fill":356,"stroke":357,"style":358},"56.0",[364,997,70],{"x":980,"y":998,"style":982},"74.5",[344,1000],{"x":985,"y":995,"width":1001,"height":389,"rx":977,"fill":987,"stroke":555,"style":537},"141.6",[364,1003,1005],{"x":1004,"y":998,"style":991},"268.1","1,085 · 27.1%",[344,1007],{"x":352,"y":1008,"width":976,"height":389,"rx":977,"fill":356,"stroke":357,"style":358},"96.0",[364,1010,73],{"x":980,"y":1011,"style":982},"114.5",[344,1013],{"x":985,"y":1008,"width":1014,"height":389,"rx":977,"fill":1015,"stroke":1016,"style":537},"75.9","#d9f4f1","var(--teal,#0f9488)",[364,1018,1021],{"x":1019,"y":1011,"style":1020},"202.4","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","582 · 14.6%",[344,1023],{"x":352,"y":1024,"width":976,"height":389,"rx":977,"fill":356,"stroke":357,"style":358},"136.0",[364,1026,76],{"x":980,"y":1027,"style":982},"154.5",[344,1029],{"x":985,"y":1024,"width":1030,"height":389,"rx":977,"fill":1031,"stroke":1032,"style":537},"29.6","#fdefd8","var(--gold,#b4740a)",[364,1034,1037],{"x":1035,"y":1027,"style":1036},"156.1","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","227 · 5.7%",[25,1039,1041],{"id":1040},"step-4-the-funnel-by-channel-and-time-between-steps","Step 4 — The funnel by channel, and time between steps",[10,1043,1044],{},"The same reached-flags grouped by channel show where each channel loses people, and the timestamps give the typical time between steps:",[30,1046,1048],{"className":87,"code":1047,"language":89,"meta":35,"style":35},"by_channel = reached.groupby(channel).sum()[STEPS]\nrates = by_channel.div(by_channel[\"visit\"], axis=0).mul(100).round(1)\nrates.columns = [f\"{s} %\" for s in STEPS]\nprint(pd.concat([by_channel, rates.iloc[:, 1:]], axis=1).to_string())\n\ndef median_gap(a, b):\n    mask = reached[b]\n    return (first.loc[mask, b] - first.loc[mask, a]).median()\n\nfor a, b in zip(STEPS, STEPS[1:]):\n    print(f\"{a} → {b}: median {median_gap(a, b)}\")\n",[37,1049,1050,1064,1098,1138,1159,1163,1175,1185,1198,1202,1229],{"__ignoreMap":35},[40,1051,1052,1055,1057,1060,1062],{"class":42,"line":43},[40,1053,1054],{"class":100},"by_channel ",[40,1056,123],{"class":96},[40,1058,1059],{"class":100}," reached.groupby(channel).sum()[",[40,1061,139],{"class":138},[40,1063,166],{"class":100},[40,1065,1066,1069,1071,1074,1076,1079,1082,1084,1086,1089,1092,1094,1096],{"class":42,"line":110},[40,1067,1068],{"class":100},"rates ",[40,1070,123],{"class":96},[40,1072,1073],{"class":100}," by_channel.div(by_channel[",[40,1075,148],{"class":50},[40,1077,1078],{"class":100},"], ",[40,1080,1081],{"class":255},"axis",[40,1083,123],{"class":96},[40,1085,346],{"class":138},[40,1087,1088],{"class":100},").mul(",[40,1090,1091],{"class":138},"100",[40,1093,858],{"class":100},[40,1095,394],{"class":138},[40,1097,132],{"class":100},[40,1099,1100,1103,1105,1107,1110,1113,1117,1120,1123,1126,1129,1132,1134,1136],{"class":42,"line":117},[40,1101,1102],{"class":100},"rates.columns ",[40,1104,123],{"class":96},[40,1106,145],{"class":100},[40,1108,1109],{"class":96},"f",[40,1111,1112],{"class":50},"\"",[40,1114,1116],{"class":1115},"sSjpA","{",[40,1118,1119],{"class":100},"s",[40,1121,1122],{"class":1115},"}",[40,1124,1125],{"class":50}," %\"",[40,1127,1128],{"class":96}," for",[40,1130,1131],{"class":100}," s ",[40,1133,673],{"class":96},[40,1135,676],{"class":138},[40,1137,166],{"class":100},[40,1139,1140,1142,1145,1147,1150,1152,1154,1156],{"class":42,"line":135},[40,1141,199],{"class":138},[40,1143,1144],{"class":100},"(pd.concat([by_channel, rates.iloc[:, ",[40,1146,394],{"class":138},[40,1148,1149],{"class":100},":]], ",[40,1151,1081],{"class":255},[40,1153,123],{"class":96},[40,1155,394],{"class":138},[40,1157,1158],{"class":100},").to_string())\n",[40,1160,1161],{"class":42,"line":169},[40,1162,114],{"emptyLinePlaceholder":113},[40,1164,1165,1168,1172],{"class":42,"line":196},[40,1166,1167],{"class":96},"def",[40,1169,1171],{"class":1170},"s_Opv"," median_gap",[40,1173,1174],{"class":100},"(a, b):\n",[40,1176,1177,1180,1182],{"class":42,"line":732},[40,1178,1179],{"class":100},"    mask ",[40,1181,123],{"class":96},[40,1183,1184],{"class":100}," reached[b]\n",[40,1186,1187,1190,1193,1195],{"class":42,"line":757},[40,1188,1189],{"class":96},"    return",[40,1191,1192],{"class":100}," (first.loc[mask, b] ",[40,1194,706],{"class":96},[40,1196,1197],{"class":100}," first.loc[mask, a]).median()\n",[40,1199,1200],{"class":42,"line":768},[40,1201,114],{"emptyLinePlaceholder":113},[40,1203,1204,1206,1209,1211,1214,1216,1218,1220,1222,1224,1226],{"class":42,"line":779},[40,1205,667],{"class":96},[40,1207,1208],{"class":100}," a, b ",[40,1210,673],{"class":96},[40,1212,1213],{"class":138}," zip",[40,1215,301],{"class":100},[40,1217,139],{"class":138},[40,1219,67],{"class":100},[40,1221,139],{"class":138},[40,1223,679],{"class":100},[40,1225,394],{"class":138},[40,1227,1228],{"class":100},":]):\n",[40,1230,1231,1234,1236,1238,1240,1242,1244,1246,1249,1251,1254,1256,1259,1261,1264,1266,1268],{"class":42,"line":784},[40,1232,1233],{"class":138},"    print",[40,1235,301],{"class":100},[40,1237,1109],{"class":96},[40,1239,1112],{"class":50},[40,1241,1116],{"class":1115},[40,1243,19],{"class":100},[40,1245,1122],{"class":1115},[40,1247,1248],{"class":50}," → ",[40,1250,1116],{"class":1115},[40,1252,1253],{"class":100},"b",[40,1255,1122],{"class":1115},[40,1257,1258],{"class":50},": median ",[40,1260,1116],{"class":1115},[40,1262,1263],{"class":100},"median_gap(a, b)",[40,1265,1122],{"class":1115},[40,1267,1112],{"class":50},[40,1269,132],{"class":100},[10,1271,1272],{},"Email visitors convert best at every step — 12.2% of them end up purchasing — and Social worst at 2.1% — the kind of difference that changes where marketing money goes. The median time from visit to sign-up is under a day, from sign-up to trial a day or two, and from trial to purchase about a week, which suggests when follow-up emails should be sent. Medians are used because conversion times are heavily skewed: a few users who come back weeks later would drag a mean far from the typical journey.",[25,1274,1276],{"id":1275},"step-5-write-the-funnel-to-excel","Step 5 — Write the funnel to Excel",[30,1278,1280],{"className":87,"code":1279,"language":89,"meta":35,"style":35},"from openpyxl.formatting.rule import DataBarRule\nfrom openpyxl.styles import Font\n\nwith pd.ExcelWriter(\"funnel.xlsx\", engine=\"openpyxl\") as xw:\n    funnel.reset_index(names=\"Step\").to_excel(xw, sheet_name=\"Funnel\", index=False)\n    pd.concat([by_channel, rates.iloc[:, 1:]], axis=1).to_excel(xw, sheet_name=\"By channel\")\n    ws = xw.sheets[\"Funnel\"]\n    ws.conditional_formatting.add(f\"B2:B{len(funnel) + 1}\", DataBarRule(start_type=\"num\", start_value=0,\n                                  end_type=\"max\", color=\"4338CA\", showValue=True))\n    for c in ws[1]:\n        c.font = Font(bold=True)\n    for col, w in zip(\"ABCDEF\", (10, 12, 12, 18, 11, 16)):\n        ws.column_dimensions[col].width = w\nprint(\"written: Funnel and By channel sheets\")\n",[37,1281,1282,1295,1307,1311,1339,1374,1400,1414,1462,1494,1512,1531,1579,1589],{"__ignoreMap":35},[40,1283,1284,1287,1290,1292],{"class":42,"line":43},[40,1285,1286],{"class":96},"from",[40,1288,1289],{"class":100}," openpyxl.formatting.rule ",[40,1291,97],{"class":96},[40,1293,1294],{"class":100}," DataBarRule\n",[40,1296,1297,1299,1302,1304],{"class":42,"line":110},[40,1298,1286],{"class":96},[40,1300,1301],{"class":100}," openpyxl.styles ",[40,1303,97],{"class":96},[40,1305,1306],{"class":100}," Font\n",[40,1308,1309],{"class":42,"line":117},[40,1310,114],{"emptyLinePlaceholder":113},[40,1312,1313,1316,1319,1322,1324,1327,1329,1332,1334,1336],{"class":42,"line":135},[40,1314,1315],{"class":96},"with",[40,1317,1318],{"class":100}," pd.ExcelWriter(",[40,1320,1321],{"class":50},"\"funnel.xlsx\"",[40,1323,67],{"class":100},[40,1325,1326],{"class":255},"engine",[40,1328,123],{"class":96},[40,1330,1331],{"class":50},"\"openpyxl\"",[40,1333,849],{"class":100},[40,1335,104],{"class":96},[40,1337,1338],{"class":100}," xw:\n",[40,1340,1341,1344,1347,1349,1352,1355,1358,1360,1363,1365,1367,1369,1372],{"class":42,"line":169},[40,1342,1343],{"class":100},"    funnel.reset_index(",[40,1345,1346],{"class":255},"names",[40,1348,123],{"class":96},[40,1350,1351],{"class":50},"\"Step\"",[40,1353,1354],{"class":100},").to_excel(xw, ",[40,1356,1357],{"class":255},"sheet_name",[40,1359,123],{"class":96},[40,1361,1362],{"class":50},"\"Funnel\"",[40,1364,67],{"class":100},[40,1366,624],{"class":255},[40,1368,123],{"class":96},[40,1370,1371],{"class":138},"False",[40,1373,132],{"class":100},[40,1375,1376,1379,1381,1383,1385,1387,1389,1391,1393,1395,1398],{"class":42,"line":196},[40,1377,1378],{"class":100},"    pd.concat([by_channel, rates.iloc[:, ",[40,1380,394],{"class":138},[40,1382,1149],{"class":100},[40,1384,1081],{"class":255},[40,1386,123],{"class":96},[40,1388,394],{"class":138},[40,1390,1354],{"class":100},[40,1392,1357],{"class":255},[40,1394,123],{"class":96},[40,1396,1397],{"class":50},"\"By channel\"",[40,1399,132],{"class":100},[40,1401,1402,1405,1407,1410,1412],{"class":42,"line":732},[40,1403,1404],{"class":100},"    ws ",[40,1406,123],{"class":96},[40,1408,1409],{"class":100}," xw.sheets[",[40,1411,1362],{"class":50},[40,1413,166],{"class":100},[40,1415,1416,1419,1421,1424,1426,1428,1431,1433,1435,1437,1439,1442,1445,1447,1450,1452,1455,1457,1459],{"class":42,"line":757},[40,1417,1418],{"class":100},"    ws.conditional_formatting.add(",[40,1420,1109],{"class":96},[40,1422,1423],{"class":50},"\"B2:B",[40,1425,1116],{"class":1115},[40,1427,309],{"class":138},[40,1429,1430],{"class":100},"(funnel) ",[40,1432,749],{"class":96},[40,1434,709],{"class":138},[40,1436,1122],{"class":1115},[40,1438,1112],{"class":50},[40,1440,1441],{"class":100},", DataBarRule(",[40,1443,1444],{"class":255},"start_type",[40,1446,123],{"class":96},[40,1448,1449],{"class":50},"\"num\"",[40,1451,67],{"class":100},[40,1453,1454],{"class":255},"start_value",[40,1456,123],{"class":96},[40,1458,346],{"class":138},[40,1460,1461],{"class":100},",\n",[40,1463,1464,1467,1469,1472,1474,1477,1479,1482,1484,1487,1489,1492],{"class":42,"line":768},[40,1465,1466],{"class":255},"                                  end_type",[40,1468,123],{"class":96},[40,1470,1471],{"class":50},"\"max\"",[40,1473,67],{"class":100},[40,1475,1476],{"class":255},"color",[40,1478,123],{"class":96},[40,1480,1481],{"class":50},"\"4338CA\"",[40,1483,67],{"class":100},[40,1485,1486],{"class":255},"showValue",[40,1488,123],{"class":96},[40,1490,1491],{"class":138},"True",[40,1493,263],{"class":100},[40,1495,1496,1499,1502,1504,1507,1509],{"class":42,"line":779},[40,1497,1498],{"class":96},"    for",[40,1500,1501],{"class":100}," c ",[40,1503,673],{"class":96},[40,1505,1506],{"class":100}," ws[",[40,1508,394],{"class":138},[40,1510,1511],{"class":100},"]:\n",[40,1513,1514,1517,1519,1522,1525,1527,1529],{"class":42,"line":784},[40,1515,1516],{"class":100},"        c.font ",[40,1518,123],{"class":96},[40,1520,1521],{"class":100}," Font(",[40,1523,1524],{"class":255},"bold",[40,1526,123],{"class":96},[40,1528,1491],{"class":138},[40,1530,132],{"class":100},[40,1532,1533,1535,1538,1540,1542,1544,1547,1550,1553,1555,1558,1560,1562,1564,1567,1569,1571,1573,1576],{"class":42,"line":801},[40,1534,1498],{"class":96},[40,1536,1537],{"class":100}," col, w ",[40,1539,673],{"class":96},[40,1541,1213],{"class":138},[40,1543,301],{"class":100},[40,1545,1546],{"class":50},"\"ABCDEF\"",[40,1548,1549],{"class":100},", (",[40,1551,1552],{"class":138},"10",[40,1554,67],{"class":100},[40,1556,1557],{"class":138},"12",[40,1559,67],{"class":100},[40,1561,1557],{"class":138},[40,1563,67],{"class":100},[40,1565,1566],{"class":138},"18",[40,1568,67],{"class":100},[40,1570,568],{"class":138},[40,1572,67],{"class":100},[40,1574,1575],{"class":138},"16",[40,1577,1578],{"class":100},")):\n",[40,1580,1581,1584,1586],{"class":42,"line":817},[40,1582,1583],{"class":100},"        ws.column_dimensions[col].width ",[40,1585,123],{"class":96},[40,1587,1588],{"class":100}," w\n",[40,1590,1591,1593,1595,1598],{"class":42,"line":865},[40,1592,199],{"class":138},[40,1594,301],{"class":100},[40,1596,1597],{"class":50},"\"written: Funnel and By channel sheets\"",[40,1599,132],{"class":100},[10,1601,1602,1603,1607,1608,23],{},"Data bars on the user counts turn the Funnel sheet into a funnel picture without a chart, and the By channel sheet holds the per-channel counts and rates for anyone who wants to dig further. Data bars and other visual conditional formats are covered in ",[19,1604,1606],{"href":1605},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fadd-data-bars-and-colour-scales-with-openpyxl\u002F","Add Data Bars and Colour Scales with openpyxl",", and a chart version in ",[19,1609,1611],{"href":1610},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fcreate-a-funnel-chart-in-excel-with-python\u002F","Create a Funnel Chart in Excel with Python",[325,1613,334,1618,334,1621,334,1624,334,1627,334,1634,334,1645,334,1651,334,1655,334,1659,334,1663,334,1667,334,1670,334,1677,334,1682,334,1686,334,1689,334,1692],{"viewBox":1614,"role":328,"ariaLabelledBy":1615,"xmlns":332,"style":333},"4 0 752 207",[1616,1617],"fn-naive-t","fn-naive-d",[336,1619,1620],{"id":1616},"Naive counts versus an ordered funnel",[340,1622,1623],{"id":1617},"Counting distinct users per event includes people who skipped steps or whose events are out of order, overstating later steps. An ordered funnel counts a user at a step only after the previous step and within the window, so each step is a subset of the one before.",[344,1625],{"x":292,"y":346,"width":347,"height":1626,"fill":349},"207",[344,1628],{"x":352,"y":975,"width":1629,"height":1630,"rx":1631,"fill":1632,"stroke":1633,"style":556},"351.0","175.0","14","#fce9e9","var(--danger,#dc2626)",[1635,1636,1638],"g",{"transform":1637},"translate(182.5,29.0) scale(1.083)",[1639,1640],"path",{"d":1641,"fill":1642,"stroke":1643,"style":1644},"M6 6l12 12 M18 6L6 18","none","var(--accent-ink,#be185d)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[364,1646,1650],{"x":1647,"y":1648,"style":1649},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","distinct users per event",[42,1652],{"x1":392,"y1":1653,"x2":1654,"y2":1653,"stroke":1633,"style":358},"91.0","355.0",[364,1656,1658],{"x":1647,"y":416,"style":1657},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","purchases without a trial count",[364,1660,1662],{"x":1647,"y":1661,"style":1657},"137.0","signups before a visit count",[364,1664,1666],{"x":1647,"y":1665,"style":1657},"163.0","steps are not subsets",[344,1668],{"x":1669,"y":975,"width":1629,"height":1630,"rx":1631,"fill":1015,"stroke":1016,"style":556},"389.0",[1635,1671,1673],{"transform":1672},"translate(551.5,29.0) scale(1.083)",[1639,1674],{"d":1675,"fill":1642,"stroke":1676,"style":1644},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[364,1678,1681],{"x":1679,"y":1648,"style":1680},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","ordered funnel, 30-day window",[42,1683],{"x1":1684,"y1":1653,"x2":1685,"y2":1653,"stroke":1016,"style":358},"405.0","724.0",[364,1687,1688],{"x":1679,"y":416,"style":1657},"previous step required",[364,1690,1691],{"x":1679,"y":1661,"style":1657},"in time order, within window",[364,1693,1694],{"x":1679,"y":1665,"style":1657},"each step ⊆ the one before",[25,1696,1698],{"id":1697},"funnels-over-time","Funnels over time",[10,1700,1701,1702,1705,1706,23],{},"A single funnel answers \"how do we convert overall\"; teams usually also want to know whether conversion is improving. Group the per-user flags by the week or month of the first visit — a ",[14,1703,1704],{},"cohort"," — instead of by channel, and each row becomes the funnel for the visitors who arrived in that period. Read the most recent cohorts with care: visitors from the last few days have not yet had the full 30-day window to sign up and buy, so their conversion looks artificially low. Either leave cohorts out until their window has closed, or show them greyed out with a note. The retention version of this idea is covered in ",[19,1707,1709],{"href":1708},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-cohort-retention-table-from-excel-data\u002F","Build a Cohort Retention Table from Excel Data",[25,1711,1713],{"id":1712},"common-pitfalls","Common pitfalls",[1715,1716,1717,1725,1731,1737,1743],"ul",{},[1718,1719,1720,1724],"li",{},[1721,1722,1723],"strong",{},"Counting events instead of users."," Repeat visits inflate the top of the funnel; take the first per user.",[1718,1726,1727,1730],{},[1721,1728,1729],{},"Ignoring order."," Users who skip or reorder steps make later steps look better.",[1718,1732,1733,1736],{},[1721,1734,1735],{},"No conversion window."," Old visitors keep trickling into later steps; fix a window.",[1718,1738,1739,1742],{},[1721,1740,1741],{},"Comparing recent cohorts too early."," Users who visited yesterday have not had time to convert.",[1718,1744,1745,1748],{},[1721,1746,1747],{},"Means for conversion times."," Use medians; the distribution is skewed.",[25,1750,1752],{"id":1751},"conclusion","Conclusion",[10,1754,1755],{},"A conversion funnel from an event log takes three steps: reduce the log to each user's first timestamp per step, require each step to follow the previous one within a conversion window, and count users per step. From those flags come step and overall conversion, drop-off at each step, a channel breakdown and median times between steps. Write the table with data bars for a quick visual, and remember that recent visitors need time before their conversion rates are comparable.",[25,1757,1759],{"id":1758},"frequently-asked-questions","Frequently asked questions",[10,1761,1762,1765],{},[1721,1763,1764],{},"How do I calculate funnel conversion rates in pandas?","\nTake each user's first timestamp for every step, mark a step as reached only if the previous step was reached earlier and within a time window, sum the flags per step, and divide each step by the previous one and by the first step.",[10,1767,1768,1771],{},[1721,1769,1770],{},"Why is my funnel larger than expected at later steps?","\nCounting distinct users per event includes people who skipped earlier steps or whose events are out of order. Require each step to follow the previous one.",[10,1773,1774,1777],{},[1721,1775,1776],{},"What conversion window should I use?","\nOne that matches your sales cycle, for example 7 days for a checkout and 30 or 90 days for a subscription trial. Apply it consistently so periods can be compared.",[10,1779,1780,1783],{},[1721,1781,1782],{},"How do I compare funnels between channels?","\nGroup the per-user reached flags by channel, sum them, and divide by each channel's number of visitors.",[25,1785,1787],{"id":1786},"related","Related",[1715,1789,1790,1796,1801,1808],{},[1718,1791,1792,1793,1795],{},"Up one level: ",[19,1794,22],{"href":21}," — summaries and crosstabs.",[1718,1797,1798,1800],{},[19,1799,1709],{"href":1708}," — what happens after the purchase.",[1718,1802,1803,1807],{},[19,1804,1806],{"href":1805},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcount-distinct-values-per-group-in-pandas\u002F","Count Distinct Values per Group in pandas"," — the naive count done properly.",[1718,1809,1810,1814],{},[19,1811,1813],{"href":1812},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-sales-pipeline-report\u002F","Build a Sales Pipeline Report"," — the B2B version of a funnel.",[1816,1817,1818],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":35,"searchDepth":110,"depth":110,"links":1820},[1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831],{"id":27,"depth":110,"text":28},{"id":80,"depth":110,"text":81},{"id":208,"depth":110,"text":209},{"id":578,"depth":110,"text":579},{"id":1040,"depth":110,"text":1041},{"id":1275,"depth":110,"text":1276},{"id":1697,"depth":110,"text":1698},{"id":1712,"depth":110,"text":1713},{"id":1751,"depth":110,"text":1752},{"id":1758,"depth":110,"text":1759},{"id":1786,"depth":110,"text":1787},"2026-10-02","Turn a raw event log exported to Excel into a conversion funnel with pandas: one timestamp per user and step, an ordered funnel that only counts steps reached after the previous one within a time window, step-to-step and overall conversion, drop-off counts, a breakdown by channel, median time between steps, and an Excel sheet with data bars.","md",[1836,1838,1840,1842],{"q":1764,"a":1837},"Take each user's first timestamp for every step, mark a step as reached only if the previous step was reached earlier and within a time window, sum the flags per step, and divide each step by the previous one and by the first step.",{"q":1770,"a":1839},"Counting distinct users per event includes people who skipped earlier steps or whose events are out of order. Require each step to follow the previous one.",{"q":1776,"a":1841},"One that matches your sales cycle, for example 7 days for a checkout and 30 or 90 days for a subscription trial. Apply it consistently so periods can be compared.",{"q":1782,"a":1843},"Group the per-user reached flags by channel, sum them, and divide by each channel's number of visitors.","build a funnel conversion table from event data",null,{"breadcrumb":1847},[1848,1850,1853],{"name":1849,"item":836},"Home",{"name":1851,"item":1852},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":1854,"item":21},"Creating Pivot Tables from Excel Data with Pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-funnel-conversion-table-from-event-data",{"title":1857,"description":1858},"Build a Funnel Conversion Table from Event Data in pandas","Funnel analysis from an Excel event log: first event per user and step, ordered funnel with a conversion window, step and overall conversion rates, drop-off, channel breakdown, time to convert and Excel data bars.","build-a-funnel-conversion-table-from-event-data","advanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-funnel-conversion-table-from-event-data\u002Findex",[1862],"pivots-and-summaries","how-to","VZRY42skja_zGeY1_ctnHnyB-8NS47WWaYoIydJ4zxA",[1866,1870],{"title":1867,"path":1868,"stem":1869,"children":-1},"Build a Crosstab from Excel Data with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-crosstab-from-excel-data-with-pandas","advanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-crosstab-from-excel-data-with-pandas\u002Findex",{"title":1871,"path":1872,"stem":1873,"children":-1},"Build a Product Co-occurrence Matrix from Order Lines","\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-product-co-occurrence-matrix-from-order-lines","advanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-product-co-occurrence-matrix-from-order-lines\u002Findex",[1875,1877],{"path":1876,"title":1854},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data",{"path":1878,"title":1851},"\u002Fadvanced-data-transformation-and-cleaning",[],[1881,1884,1887,1890,1893,1896],{"path":1882,"title":1883,"kind":1863},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-customer-churn-report-from-subscription-data","Build a Customer Churn Report from Subscription Data",{"path":1885,"title":1886,"kind":1863},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Frefresh-pivot-tables-and-queries-in-excel-with-python","Refresh Excel Pivot Tables and Queries with Python",{"path":1888,"title":1889,"kind":1863},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fadd-a-native-excel-pivot-table-with-python","Add a Native Excel Pivot Table with Python",{"path":1891,"title":1892,"kind":1863},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fadd-subtotals-and-grand-totals-to-a-pandas-pivot-table","Add Subtotals and Grand Totals to a pandas Pivot Table",{"path":1894,"title":1895,"kind":1863},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbin-values-into-ranges-with-pandas-cut","Bin Values into Ranges Like Excel FREQUENCY with pd.cut",{"path":1897,"title":1709,"kind":1863},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-cohort-retention-table-from-excel-data",1790972086240]