[{"data":1,"prerenderedAt":2195},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcompare-two-periods-side-by-side-in-a-pivot":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcompare-two-periods-side-by-side-in-a-pivot":2161,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcompare-two-periods-side-by-side-in-a-pivot":2170,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcompare-two-periods-side-by-side-in-a-pivot":2175,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcompare-two-periods-side-by-side-in-a-pivot":2176},{"id":4,"title":5,"body":6,"dateModified":2127,"datePublished":2127,"description":2128,"extension":2129,"faq":2130,"keyword":2139,"kind":2140,"meta":2141,"navigation":99,"path":2150,"seo":2151,"slug":2154,"stem":2155,"topics":2156,"type":2159,"__hash__":2160},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcompare-two-periods-side-by-side-in-a-pivot\u002Findex.md","Compare Two Periods Side by Side in a Pivot",{"type":7,"value":8,"toc":2115},"minimark",[9,19,24,53,59,63,71,367,377,381,384,476,483,656,660,663,880,891,895,902,1127,1138,1236,1365,1368,1372,1375,1993,2004,2008,2043,2047,2050,2054,2060,2066,2072,2078,2082,2111],[10,11,12,13,18],"p",{},"\"How did September do compared with last September?\" is the most common question a sales report has to answer, and a pivot with two period columns, a difference and a percentage is the most common answer. The details are where these tables go wrong. Comparing a partial month with a full one makes every figure look like a collapse. A product that did not exist last year has no sensible percentage change, and a product that was discontinued shows a confusing −100%. A list of percentage changes hides what actually moved the total: a small product doubling matters less than a big one slipping 5%. This page builds the comparison properly with pandas — like-for-like periods, a side-by-side pivot, changes that handle new and lost lines, the contribution of each line to the total change — and writes it to Excel. It belongs to ",[14,15,17],"a",{"href":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002F","Creating Pivot Tables from Excel Data",".",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47,50],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sMTad","pip",[35,44,46],{"class":45},"srMev"," install",[35,48,49],{"class":45}," pandas",[35,51,52],{"class":45}," openpyxl\n",[10,54,55,58],{},[32,56,57],{},"daily_sales.xlsx"," holds daily sales by product from August 2025 to September 2026. Most products sold in both years; one (Lantern) was launched in March 2026 and one (Trowel) was discontinued at the end of 2025. The report compares September 2026 with September 2025.",[20,60,62],{"id":61},"step-1-choose-periods-that-are-really-comparable","Step 1 — Choose periods that are really comparable",[10,64,65,66,70],{},"A comparison is only fair if both periods cover the same kind of days. For a closed month, compare with the same month a year earlier; mid-month, compare month-to-date with the ",[67,68,69],"em",{},"same days"," of the comparison month:",[25,72,76],{"className":73,"code":74,"language":75,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nsales = pd.read_excel(\"daily_sales.xlsx\")\n\ndef label_periods(df, current_start, current_end, offset):\n    prior_start, prior_end = current_start - offset, current_end - offset\n    out = df.copy()\n    out[\"Period\"] = None\n    out.loc[out[\"Date\"].between(current_start, current_end), \"Period\"] = \"Current\"\n    out.loc[out[\"Date\"].between(prior_start, prior_end), \"Period\"] = \"Prior\"\n    return out.dropna(subset=[\"Period\"]), (prior_start, prior_end)\n\ncur_start, cur_end = pd.Timestamp(\"2026-09-01\"), pd.Timestamp(\"2026-09-30\")\nlabelled, prior = label_periods(sales, cur_start, cur_end, pd.DateOffset(years=1))\nprint(\"current:\", cur_start.date(), \"→\", cur_end.date(), \"| prior:\", prior[0].date(), \"→\", prior[1].date())\nprint(labelled.groupby(\"Period\")[\"Revenue\"].sum().round(2).to_dict())\n","python",[32,77,78,94,101,119,124,137,159,170,188,209,228,251,256,278,300,342],{"__ignoreMap":30},[35,79,80,84,88,91],{"class":37,"line":38},[35,81,83],{"class":82},"s-kum","import",[35,85,87],{"class":86},"skGVy"," pandas ",[35,89,90],{"class":82},"as",[35,92,93],{"class":86}," pd\n",[35,95,97],{"class":37,"line":96},2,[35,98,100],{"emptyLinePlaceholder":99},true,"\n",[35,102,104,107,110,113,116],{"class":37,"line":103},3,[35,105,106],{"class":86},"sales ",[35,108,109],{"class":82},"=",[35,111,112],{"class":86}," pd.read_excel(",[35,114,115],{"class":45},"\"daily_sales.xlsx\"",[35,117,118],{"class":86},")\n",[35,120,122],{"class":37,"line":121},4,[35,123,100],{"emptyLinePlaceholder":99},[35,125,127,130,134],{"class":37,"line":126},5,[35,128,129],{"class":82},"def",[35,131,133],{"class":132},"s_Opv"," label_periods",[35,135,136],{"class":86},"(df, current_start, current_end, offset):\n",[35,138,140,143,145,148,151,154,156],{"class":37,"line":139},6,[35,141,142],{"class":86},"    prior_start, prior_end ",[35,144,109],{"class":82},[35,146,147],{"class":86}," current_start ",[35,149,150],{"class":82},"-",[35,152,153],{"class":86}," offset, current_end ",[35,155,150],{"class":82},[35,157,158],{"class":86}," offset\n",[35,160,162,165,167],{"class":37,"line":161},7,[35,163,164],{"class":86},"    out ",[35,166,109],{"class":82},[35,168,169],{"class":86}," df.copy()\n",[35,171,173,176,179,182,184],{"class":37,"line":172},8,[35,174,175],{"class":86},"    out[",[35,177,178],{"class":45},"\"Period\"",[35,180,181],{"class":86},"] ",[35,183,109],{"class":82},[35,185,187],{"class":186},"sP0c6"," None\n",[35,189,191,194,197,200,202,204,206],{"class":37,"line":190},9,[35,192,193],{"class":86},"    out.loc[out[",[35,195,196],{"class":45},"\"Date\"",[35,198,199],{"class":86},"].between(current_start, current_end), ",[35,201,178],{"class":45},[35,203,181],{"class":86},[35,205,109],{"class":82},[35,207,208],{"class":45}," \"Current\"\n",[35,210,212,214,216,219,221,223,225],{"class":37,"line":211},10,[35,213,193],{"class":86},[35,215,196],{"class":45},[35,217,218],{"class":86},"].between(prior_start, prior_end), ",[35,220,178],{"class":45},[35,222,181],{"class":86},[35,224,109],{"class":82},[35,226,227],{"class":45}," \"Prior\"\n",[35,229,231,234,237,241,243,246,248],{"class":37,"line":230},11,[35,232,233],{"class":82},"    return",[35,235,236],{"class":86}," out.dropna(",[35,238,240],{"class":239},"sa561","subset",[35,242,109],{"class":82},[35,244,245],{"class":86},"[",[35,247,178],{"class":45},[35,249,250],{"class":86},"]), (prior_start, prior_end)\n",[35,252,254],{"class":37,"line":253},12,[35,255,100],{"emptyLinePlaceholder":99},[35,257,259,262,264,267,270,273,276],{"class":37,"line":258},13,[35,260,261],{"class":86},"cur_start, cur_end ",[35,263,109],{"class":82},[35,265,266],{"class":86}," pd.Timestamp(",[35,268,269],{"class":45},"\"2026-09-01\"",[35,271,272],{"class":86},"), pd.Timestamp(",[35,274,275],{"class":45},"\"2026-09-30\"",[35,277,118],{"class":86},[35,279,281,284,286,289,292,294,297],{"class":37,"line":280},14,[35,282,283],{"class":86},"labelled, prior ",[35,285,109],{"class":82},[35,287,288],{"class":86}," label_periods(sales, cur_start, cur_end, pd.DateOffset(",[35,290,291],{"class":239},"years",[35,293,109],{"class":82},[35,295,296],{"class":186},"1",[35,298,299],{"class":86},"))\n",[35,301,303,306,309,312,315,318,321,324,327,330,333,335,337,339],{"class":37,"line":302},15,[35,304,305],{"class":186},"print",[35,307,308],{"class":86},"(",[35,310,311],{"class":45},"\"current:\"",[35,313,314],{"class":86},", cur_start.date(), ",[35,316,317],{"class":45},"\"→\"",[35,319,320],{"class":86},", cur_end.date(), ",[35,322,323],{"class":45},"\"| prior:\"",[35,325,326],{"class":86},", prior[",[35,328,329],{"class":186},"0",[35,331,332],{"class":86},"].date(), ",[35,334,317],{"class":45},[35,336,326],{"class":86},[35,338,296],{"class":186},[35,340,341],{"class":86},"].date())\n",[35,343,345,347,350,352,355,358,361,364],{"class":37,"line":344},16,[35,346,305],{"class":186},[35,348,349],{"class":86},"(labelled.groupby(",[35,351,178],{"class":45},[35,353,354],{"class":86},")[",[35,356,357],{"class":45},"\"Revenue\"",[35,359,360],{"class":86},"].sum().round(",[35,362,363],{"class":186},"2",[35,365,366],{"class":86},").to_dict())\n",[10,368,369,372,373,376],{},[32,370,371],{},"pd.DateOffset(years=1)"," shifts both ends of the window by exactly one year, so September 2026 is compared with September 2025. For a month-to-date report on 15 September, pass ",[32,374,375],{},"current_end=2026-09-15"," and the prior window becomes 1–15 September 2025 — the same number of days. For weekly retail reporting, an offset of 52 weeks keeps weekdays aligned (a Saturday compared with a Saturday), which often matters more than the calendar date. The labels \"Current\" and \"Prior\" keep the rest of the code independent of which periods were chosen.",[20,378,380],{"id":379},"step-2-pivot-the-two-periods-side-by-side","Step 2 — Pivot the two periods side by side",[10,382,383],{},"With every row labelled, one pivot puts the periods in columns:",[25,385,387],{"className":73,"code":386,"language":75,"meta":30,"style":30},"pivot = labelled.pivot_table(index=\"Product\", columns=\"Period\", values=\"Revenue\", aggfunc=\"sum\")\npivot = pivot.reindex(columns=[\"Prior\", \"Current\"])\nprint(pivot.round(2).to_string())\n",[32,388,389,438,464],{"__ignoreMap":30},[35,390,391,394,396,399,402,404,407,410,413,415,417,419,422,424,426,428,431,433,436],{"class":37,"line":38},[35,392,393],{"class":86},"pivot ",[35,395,109],{"class":82},[35,397,398],{"class":86}," labelled.pivot_table(",[35,400,401],{"class":239},"index",[35,403,109],{"class":82},[35,405,406],{"class":45},"\"Product\"",[35,408,409],{"class":86},", ",[35,411,412],{"class":239},"columns",[35,414,109],{"class":82},[35,416,178],{"class":45},[35,418,409],{"class":86},[35,420,421],{"class":239},"values",[35,423,109],{"class":82},[35,425,357],{"class":45},[35,427,409],{"class":86},[35,429,430],{"class":239},"aggfunc",[35,432,109],{"class":82},[35,434,435],{"class":45},"\"sum\"",[35,437,118],{"class":86},[35,439,440,442,444,447,449,451,453,456,458,461],{"class":37,"line":96},[35,441,393],{"class":86},[35,443,109],{"class":82},[35,445,446],{"class":86}," pivot.reindex(",[35,448,412],{"class":239},[35,450,109],{"class":82},[35,452,245],{"class":86},[35,454,455],{"class":45},"\"Prior\"",[35,457,409],{"class":86},[35,459,460],{"class":45},"\"Current\"",[35,462,463],{"class":86},"])\n",[35,465,466,468,471,473],{"class":37,"line":103},[35,467,305],{"class":186},[35,469,470],{"class":86},"(pivot.round(",[35,472,363],{"class":186},[35,474,475],{"class":86},").to_string())\n",[10,477,478,479,482],{},"Each product has its September 2025 revenue under \"Prior\" and its September 2026 revenue under \"Current\". Notice the missing values: Lantern has no prior revenue because it did not exist, and Trowel has no current revenue because it was discontinued. pandas leaves these as ",[32,480,481],{},"NaN",", which is right — they are not zeros in the same sense as a product that was on sale and sold nothing — and the next step treats them deliberately rather than letting them produce odd percentages.",[484,485,493,494,493,498,493,502,493,509,493,518,493,522,493,529,493,533,493,537,493,541,493,545,493,549,493,553,493,558,493,562,493,564,493,567,493,569,493,572,493,575,493,578,493,580,493,584,493,586,493,589,493,591,493,594,493,597,493,601,493,603,493,606,493,608,493,611,493,613,493,616,493,619,493,623,493,625,493,628,493,632,493,634,493,637,493,640,493,644,493,646,493,649,493,651,493,654],"svg",{"viewBox":486,"role":487,"ariaLabelledBy":488,"xmlns":491,"style":492},"208 0 343 196","img",[489,490],"pp-side-t","pp-side-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:343px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[495,496,497],"title",{"id":489},"Two periods side by side",[499,500,501],"desc",{"id":490},"Products down the side with prior and current revenue. Most products have both. Lantern has no prior value because it is new, and Trowel has no current value because it was discontinued.",[503,504],"rect",{"x":505,"y":329,"width":506,"height":507,"fill":508},"208","343","196","#ffffff",[503,510],{"x":511,"y":512,"width":513,"height":514,"rx":329,"fill":515,"stroke":516,"style":517},"224.7","16.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[503,519],{"x":520,"y":512,"width":521,"height":514,"rx":329,"fill":515,"stroke":516,"style":517},"256.7","66.7",[523,524,528],"text",{"x":525,"y":526,"style":527},"290.1","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[503,530],{"x":531,"y":512,"width":532,"height":514,"rx":329,"fill":515,"stroke":516,"style":517},"323.5","97.9",[523,534,536],{"x":535,"y":526,"style":527},"372.4","B",[503,538],{"x":539,"y":512,"width":540,"height":514,"rx":329,"fill":515,"stroke":516,"style":517},"421.4","113.9",[523,542,544],{"x":543,"y":526,"style":527},"478.3","C",[503,546],{"x":511,"y":547,"width":513,"height":548,"rx":329,"fill":515,"stroke":516,"style":517},"40.0","28.0",[523,550,296],{"x":551,"y":552,"style":527},"240.7","59.0",[503,554],{"x":520,"y":547,"width":521,"height":548,"rx":329,"fill":555,"stroke":556,"style":557},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[523,559,561],{"x":525,"y":552,"style":560},"font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Product",[503,563],{"x":531,"y":547,"width":532,"height":548,"rx":329,"fill":555,"stroke":556,"style":557},[523,565,566],{"x":535,"y":552,"style":560},"Prior (Sep 25)",[503,568],{"x":539,"y":547,"width":540,"height":548,"rx":329,"fill":555,"stroke":556,"style":557},[523,570,571],{"x":543,"y":552,"style":560},"Current (Sep 26)",[503,573],{"x":511,"y":574,"width":513,"height":548,"rx":329,"fill":515,"stroke":516,"style":517},"68.0",[523,576,363],{"x":551,"y":577,"style":527},"87.0",[503,579],{"x":520,"y":574,"width":521,"height":548,"rx":329,"fill":508,"stroke":516,"style":517},[523,581,583],{"x":525,"y":577,"style":582},"font-size:10.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Drill",[503,585],{"x":531,"y":574,"width":532,"height":548,"rx":329,"fill":508,"stroke":516,"style":517},[523,587,588],{"x":535,"y":577,"style":582},"19,866",[503,590],{"x":539,"y":574,"width":540,"height":548,"rx":329,"fill":508,"stroke":516,"style":517},[523,592,593],{"x":543,"y":577,"style":582},"22,962",[503,595],{"x":511,"y":596,"width":513,"height":548,"rx":329,"fill":515,"stroke":516,"style":517},"96.0",[523,598,600],{"x":551,"y":599,"style":527},"115.0","3",[503,602],{"x":520,"y":596,"width":521,"height":548,"rx":329,"fill":508,"stroke":516,"style":517},[523,604,605],{"x":525,"y":599,"style":582},"Hose",[503,607],{"x":531,"y":596,"width":532,"height":548,"rx":329,"fill":508,"stroke":516,"style":517},[523,609,610],{"x":535,"y":599,"style":582},"4,154",[503,612],{"x":539,"y":596,"width":540,"height":548,"rx":329,"fill":508,"stroke":516,"style":517},[523,614,615],{"x":543,"y":599,"style":582},"5,766",[503,617],{"x":511,"y":618,"width":513,"height":548,"rx":329,"fill":515,"stroke":516,"style":517},"124.0",[523,620,622],{"x":551,"y":621,"style":527},"143.0","4",[503,624],{"x":520,"y":618,"width":521,"height":548,"rx":329,"fill":508,"stroke":516,"style":517},[523,626,627],{"x":525,"y":621,"style":582},"Lantern",[503,629],{"x":531,"y":618,"width":532,"height":548,"rx":329,"fill":630,"stroke":631,"style":557},"#fdefd8","var(--gold,#b4740a)",[503,633],{"x":539,"y":618,"width":540,"height":548,"rx":329,"fill":508,"stroke":516,"style":517},[523,635,636],{"x":543,"y":621,"style":582},"506",[503,638],{"x":511,"y":639,"width":513,"height":548,"rx":329,"fill":515,"stroke":516,"style":517},"152.0",[523,641,643],{"x":551,"y":642,"style":527},"171.0","5",[503,645],{"x":520,"y":639,"width":521,"height":548,"rx":329,"fill":508,"stroke":516,"style":517},[523,647,648],{"x":525,"y":642,"style":582},"Trowel",[503,650],{"x":531,"y":639,"width":532,"height":548,"rx":329,"fill":508,"stroke":516,"style":517},[523,652,653],{"x":535,"y":642,"style":582},"195",[503,655],{"x":539,"y":639,"width":540,"height":548,"rx":329,"fill":630,"stroke":631,"style":557},[20,657,659],{"id":658},"step-3-changes-that-cope-with-new-and-lost-lines","Step 3 — Changes that cope with new and lost lines",[10,661,662],{},"Compute the absolute change with missing values treated as zero, but give new and lost products a status instead of a meaningless percentage:",[25,664,666],{"className":73,"code":665,"language":75,"meta":30,"style":30},"cmp = pivot.copy()\ncmp[\"Change\"] = cmp[\"Current\"].fillna(0) - cmp[\"Prior\"].fillna(0)\ncmp[\"Change %\"] = (cmp[\"Change\"] \u002F cmp[\"Prior\"] * 100).where(cmp[\"Prior\"].notna() & cmp[\"Current\"].notna())\ncmp[\"Status\"] = \"continuing\"\ncmp.loc[cmp[\"Prior\"].isna(), \"Status\"] = \"new\"\ncmp.loc[cmp[\"Current\"].isna(), \"Status\"] = \"lost\"\nprint(cmp.round(1).sort_values(\"Change\", ascending=False).to_string())\n",[32,667,668,679,721,786,802,827,850],{"__ignoreMap":30},[35,669,670,673,676],{"class":37,"line":38},[35,671,672],{"class":239},"cmp",[35,674,675],{"class":82}," =",[35,677,678],{"class":86}," pivot.copy()\n",[35,680,681,683,685,688,690,692,695,697,699,702,704,707,709,711,713,715,717,719],{"class":37,"line":96},[35,682,672],{"class":239},[35,684,245],{"class":86},[35,686,687],{"class":45},"\"Change\"",[35,689,181],{"class":86},[35,691,109],{"class":82},[35,693,694],{"class":239}," cmp",[35,696,245],{"class":86},[35,698,460],{"class":45},[35,700,701],{"class":86},"].fillna(",[35,703,329],{"class":186},[35,705,706],{"class":86},") ",[35,708,150],{"class":82},[35,710,694],{"class":239},[35,712,245],{"class":86},[35,714,455],{"class":45},[35,716,701],{"class":86},[35,718,329],{"class":186},[35,720,118],{"class":86},[35,722,723,725,727,730,732,734,737,739,741,743,745,748,750,752,754,756,759,762,765,767,769,771,774,777,779,781,783],{"class":37,"line":103},[35,724,672],{"class":239},[35,726,245],{"class":86},[35,728,729],{"class":45},"\"Change %\"",[35,731,181],{"class":86},[35,733,109],{"class":82},[35,735,736],{"class":86}," (",[35,738,672],{"class":239},[35,740,245],{"class":86},[35,742,687],{"class":45},[35,744,181],{"class":86},[35,746,747],{"class":82},"\u002F",[35,749,694],{"class":239},[35,751,245],{"class":86},[35,753,455],{"class":45},[35,755,181],{"class":86},[35,757,758],{"class":82},"*",[35,760,761],{"class":186}," 100",[35,763,764],{"class":86},").where(",[35,766,672],{"class":239},[35,768,245],{"class":86},[35,770,455],{"class":45},[35,772,773],{"class":86},"].notna() ",[35,775,776],{"class":82},"&",[35,778,694],{"class":239},[35,780,245],{"class":86},[35,782,460],{"class":45},[35,784,785],{"class":86},"].notna())\n",[35,787,788,790,792,795,797,799],{"class":37,"line":121},[35,789,672],{"class":239},[35,791,245],{"class":86},[35,793,794],{"class":45},"\"Status\"",[35,796,181],{"class":86},[35,798,109],{"class":82},[35,800,801],{"class":45}," \"continuing\"\n",[35,803,804,806,809,811,813,815,818,820,822,824],{"class":37,"line":126},[35,805,672],{"class":239},[35,807,808],{"class":86},".loc[",[35,810,672],{"class":239},[35,812,245],{"class":86},[35,814,455],{"class":45},[35,816,817],{"class":86},"].isna(), ",[35,819,794],{"class":45},[35,821,181],{"class":86},[35,823,109],{"class":82},[35,825,826],{"class":45}," \"new\"\n",[35,828,829,831,833,835,837,839,841,843,845,847],{"class":37,"line":139},[35,830,672],{"class":239},[35,832,808],{"class":86},[35,834,672],{"class":239},[35,836,245],{"class":86},[35,838,460],{"class":45},[35,840,817],{"class":86},[35,842,794],{"class":45},[35,844,181],{"class":86},[35,846,109],{"class":82},[35,848,849],{"class":45}," \"lost\"\n",[35,851,852,854,856,858,861,863,866,868,870,873,875,878],{"class":37,"line":161},[35,853,305],{"class":186},[35,855,308],{"class":86},[35,857,672],{"class":239},[35,859,860],{"class":86},".round(",[35,862,296],{"class":186},[35,864,865],{"class":86},").sort_values(",[35,867,687],{"class":45},[35,869,409],{"class":86},[35,871,872],{"class":239},"ascending",[35,874,109],{"class":82},[35,876,877],{"class":186},"False",[35,879,475],{"class":86},[10,881,882,883,886,887,890],{},"For the absolute change, a missing value really does contribute zero revenue, so ",[32,884,885],{},"fillna(0)"," is correct: Lantern adds its whole current revenue, Trowel subtracts its whole prior revenue. The ",[67,888,889],{},"percentage"," change is computed only for products present in both periods; for the others it would be infinite or −100%, which says nothing useful and distorts any average. The status column says what happened instead. Sorting by absolute change puts the products that moved the total most at the top — which is usually the question being asked.",[20,892,894],{"id":893},"step-4-contribution-to-the-total-change","Step 4 — Contribution to the total change",[10,896,897,898,901],{},"The total moved by some amount; how much of that is due to each product? Each product's change divided by the total change is its ",[67,899,900],{},"contribution",":",[25,903,905],{"className":73,"code":904,"language":75,"meta":30,"style":30},"totals = cmp[[\"Prior\", \"Current\", \"Change\"]].sum()\ntotal_change_pct = totals[\"Change\"] \u002F totals[\"Prior\"] * 100\ncmp[\"Contribution pts\"] = cmp[\"Change\"] \u002F totals[\"Prior\"] * 100\nprint(f\"total: {totals['Prior']:,.2f} → {totals['Current']:,.2f}  ({total_change_pct:+.1f}%)\")\nprint(cmp[\"Contribution pts\"].round(2).sort_values(ascending=False).to_string())\nprint(\"contributions add up to total change:\", round(cmp[\"Contribution pts\"].sum(), 6) == round(total_change_pct, 6))\n",[32,906,907,932,959,992,1057,1084],{"__ignoreMap":30},[35,908,909,912,914,916,919,921,923,925,927,929],{"class":37,"line":38},[35,910,911],{"class":86},"totals ",[35,913,109],{"class":82},[35,915,694],{"class":239},[35,917,918],{"class":86},"[[",[35,920,455],{"class":45},[35,922,409],{"class":86},[35,924,460],{"class":45},[35,926,409],{"class":86},[35,928,687],{"class":45},[35,930,931],{"class":86},"]].sum()\n",[35,933,934,937,939,942,944,946,948,950,952,954,956],{"class":37,"line":96},[35,935,936],{"class":86},"total_change_pct ",[35,938,109],{"class":82},[35,940,941],{"class":86}," totals[",[35,943,687],{"class":45},[35,945,181],{"class":86},[35,947,747],{"class":82},[35,949,941],{"class":86},[35,951,455],{"class":45},[35,953,181],{"class":86},[35,955,758],{"class":82},[35,957,958],{"class":186}," 100\n",[35,960,961,963,965,968,970,972,974,976,978,980,982,984,986,988,990],{"class":37,"line":103},[35,962,672],{"class":239},[35,964,245],{"class":86},[35,966,967],{"class":45},"\"Contribution pts\"",[35,969,181],{"class":86},[35,971,109],{"class":82},[35,973,694],{"class":239},[35,975,245],{"class":86},[35,977,687],{"class":45},[35,979,181],{"class":86},[35,981,747],{"class":82},[35,983,941],{"class":86},[35,985,455],{"class":45},[35,987,181],{"class":86},[35,989,758],{"class":82},[35,991,958],{"class":186},[35,993,994,996,998,1001,1004,1008,1011,1014,1017,1020,1023,1026,1028,1030,1033,1035,1037,1039,1042,1044,1047,1050,1052,1055],{"class":37,"line":121},[35,995,305],{"class":186},[35,997,308],{"class":86},[35,999,1000],{"class":82},"f",[35,1002,1003],{"class":45},"\"total: ",[35,1005,1007],{"class":1006},"sSjpA","{",[35,1009,1010],{"class":86},"totals[",[35,1012,1013],{"class":45},"'Prior'",[35,1015,1016],{"class":86},"]",[35,1018,1019],{"class":82},":,.2f",[35,1021,1022],{"class":1006},"}",[35,1024,1025],{"class":45}," → ",[35,1027,1007],{"class":1006},[35,1029,1010],{"class":86},[35,1031,1032],{"class":45},"'Current'",[35,1034,1016],{"class":86},[35,1036,1019],{"class":82},[35,1038,1022],{"class":1006},[35,1040,1041],{"class":45},"  (",[35,1043,1007],{"class":1006},[35,1045,1046],{"class":86},"total_change_pct",[35,1048,1049],{"class":82},":+.1f",[35,1051,1022],{"class":1006},[35,1053,1054],{"class":45},"%)\"",[35,1056,118],{"class":86},[35,1058,1059,1061,1063,1065,1067,1069,1072,1074,1076,1078,1080,1082],{"class":37,"line":126},[35,1060,305],{"class":186},[35,1062,308],{"class":86},[35,1064,672],{"class":239},[35,1066,245],{"class":86},[35,1068,967],{"class":45},[35,1070,1071],{"class":86},"].round(",[35,1073,363],{"class":186},[35,1075,865],{"class":86},[35,1077,872],{"class":239},[35,1079,109],{"class":82},[35,1081,877],{"class":186},[35,1083,475],{"class":86},[35,1085,1086,1088,1090,1093,1095,1098,1100,1102,1104,1106,1109,1112,1114,1117,1120,1123,1125],{"class":37,"line":139},[35,1087,305],{"class":186},[35,1089,308],{"class":86},[35,1091,1092],{"class":45},"\"contributions add up to total change:\"",[35,1094,409],{"class":86},[35,1096,1097],{"class":186},"round",[35,1099,308],{"class":86},[35,1101,672],{"class":239},[35,1103,245],{"class":86},[35,1105,967],{"class":45},[35,1107,1108],{"class":86},"].sum(), ",[35,1110,1111],{"class":186},"6",[35,1113,706],{"class":86},[35,1115,1116],{"class":82},"==",[35,1118,1119],{"class":186}," round",[35,1121,1122],{"class":86},"(total_change_pct, ",[35,1124,1111],{"class":186},[35,1126,299],{"class":86},[10,1128,1129,1130,1133,1134,1137],{},"Contribution in ",[67,1131,1132],{},"percentage points"," — each product's change divided by the prior ",[67,1135,1136],{},"total"," — has the neat property that the contributions add up exactly to the total percentage change, so the table explains the headline figure. It also puts percentage changes in proportion: Ladder's revenue grew by 48.8%, but from a small base, so it contributes 4.8 points — half of what the Drill's 15.6% growth on a large base contributes (9.4 points) — and the Saw's 20% fall shows up as the biggest drag at −1.6 points. Together the eight contributions add up to the total growth of 18.2%. This is the column managers actually read.",[484,1139,493,1145,493,1148,493,1151,493,1155,493,1160,493,1166,493,1172,493,1177,493,1180,493,1184,493,1187,493,1191,493,1193,493,1197,493,1200,493,1204,493,1207,493,1211,493,1214,493,1219,493,1222,493,1226,493,1231],{"viewBox":1140,"role":487,"ariaLabelledBy":1141,"xmlns":491,"style":1144},"4 0 744 220",[1142,1143],"pp-contrib-t","pp-contrib-d","width:100%;max-width:744px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[495,1146,1147],{"id":1142},"Contribution of each product to the total change",[499,1149,1150],{"id":1143},"Of the 18.2 percent total growth, the drill contributes 9.43 points from 15.6 percent growth, the hose 4.91 and the ladder 4.81 points despite much larger percentage growth, and the new lantern 1.54. The saw is the largest drag at minus 1.57 points.",[503,1152],{"x":622,"y":329,"width":1153,"height":1154,"fill":508},"744","220",[503,1156],{"x":1157,"y":512,"width":1158,"height":548,"rx":1159,"fill":515,"stroke":516,"style":517},"20.0","138.3","7",[523,1161,1165],{"x":1162,"y":1163,"style":1164},"89.2","34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Drill (+15.6%)",[503,1167],{"x":1168,"y":512,"width":1169,"height":548,"rx":1159,"fill":1170,"stroke":1171,"style":557},"168.3","495.5","#d9f4f1","var(--teal,#0f9488)",[523,1173,1176],{"x":1174,"y":1163,"style":1175},"671.8","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","+9.43 pts",[503,1178],{"x":1157,"y":1179,"width":1158,"height":548,"rx":1159,"fill":515,"stroke":516,"style":517},"56.0",[523,1181,1183],{"x":1162,"y":1182,"style":1164},"74.5","Hose (+38.8%)",[503,1185],{"x":1168,"y":1179,"width":1186,"height":548,"rx":1159,"fill":1170,"stroke":1171,"style":557},"258.0",[523,1188,1190],{"x":1189,"y":1182,"style":1175},"434.3","+4.91 pts",[503,1192],{"x":1157,"y":596,"width":1158,"height":548,"rx":1159,"fill":515,"stroke":516,"style":517},[523,1194,1196],{"x":1162,"y":1195,"style":1164},"114.5","Ladder (+48.8%)",[503,1198],{"x":1168,"y":596,"width":1199,"height":548,"rx":1159,"fill":1170,"stroke":1171,"style":557},"252.7",[523,1201,1203],{"x":1202,"y":1195,"style":1175},"429.1","+4.81 pts",[503,1205],{"x":1157,"y":1206,"width":1158,"height":548,"rx":1159,"fill":515,"stroke":516,"style":517},"136.0",[523,1208,1210],{"x":1162,"y":1209,"style":1164},"154.5","Lantern (new)",[503,1212],{"x":1168,"y":1206,"width":1213,"height":548,"rx":1159,"fill":555,"stroke":556,"style":557},"80.9",[523,1215,1218],{"x":1216,"y":1209,"style":1217},"257.3","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start","+1.54 pts",[503,1220],{"x":1157,"y":1221,"width":1158,"height":548,"rx":1159,"fill":515,"stroke":516,"style":517},"176.0",[523,1223,1225],{"x":1162,"y":1224,"style":1164},"194.5","Saw (−20.4%)",[503,1227],{"x":1168,"y":1221,"width":1228,"height":548,"rx":1159,"fill":1229,"stroke":1230,"style":557},"82.5","#fce9e9","var(--danger,#dc2626)",[523,1232,1235],{"x":1233,"y":1224,"style":1234},"258.8","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","−1.57 pts",[484,1237,493,1243,493,1246,493,1249,493,1253,493,1259,493,1270,493,1276,493,1281,493,1285,493,1290,493,1295,493,1298,493,1305,493,1311,493,1315,493,1319,493,1322,493,1325,493,1332,493,1337,493,1340,493,1344,493,1347,493,1350,493,1357,493,1362],{"viewBox":1238,"role":487,"ariaLabelledBy":1239,"xmlns":491,"style":1242},"4 0 752 142",[1240,1241],"pp-flow-t","pp-flow-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[495,1244,1245],{"id":1240},"Building a period comparison",[499,1247,1248],{"id":1241},"Label each row as current or prior using like-for-like date windows, pivot the two periods side by side, compute change with missing values as zero and percentage change only where both exist, add each line's contribution in percentage points, and write the table with arrow formats.",[503,1250],{"x":622,"y":329,"width":1251,"height":1252,"fill":508},"752","142",[503,1254],{"x":1157,"y":512,"width":1255,"height":1256,"rx":1257,"fill":515,"stroke":516,"style":1258},"153.0","110.5","12","stroke-width:2px",[1260,1261,1263],"g",{"transform":1262},"translate(84.5,29.0) scale(1.000)",[1264,1265],"path",{"d":1266,"fill":1267,"stroke":1268,"style":1269},"M4 5h16v15H4z M4 10h16 M8 3v4 M16 3v4","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[523,1271,1275],{"x":1272,"y":1273,"style":1274},"96.5","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Label periods",[523,1277,1280],{"x":1272,"y":1278,"style":1279},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","same days, offset a",[523,1282,1284],{"x":1272,"y":1283,"style":1279},"112.5","year",[37,1286],{"x1":1287,"y1":1288,"x2":1289,"y2":1288,"stroke":556,"style":1258},"178.0","71.2","200.0",[1291,1292],"polygon",{"points":1293,"fill":1294},"204.0,71.2 195.0,76.2 195.0,66.2","#5b5cf0",[503,1296],{"x":1297,"y":512,"width":1255,"height":1256,"rx":1257,"fill":555,"stroke":556,"style":1258},"209.0",[1260,1299,1301],{"transform":1300},"translate(273.5,37.8) scale(1.000)",[1264,1302],{"d":1303,"fill":1267,"stroke":1304,"style":1269},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","var(--brand-strong,#4338ca)",[523,1306,1310],{"x":1307,"y":1308,"style":1309},"285.5","84.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Pivot",[523,1312,1314],{"x":1307,"y":1313,"style":1279},"103.8","Prior | Current",[37,1316],{"x1":1317,"y1":1288,"x2":1318,"y2":1288,"stroke":556,"style":1258},"367.0","389.0",[1291,1320],{"points":1321,"fill":1294},"393.0,71.2 384.0,76.2 384.0,66.2",[503,1323],{"x":1324,"y":512,"width":1255,"height":1256,"rx":1257,"fill":1170,"stroke":1171,"style":1258},"398.0",[1260,1326,1328],{"transform":1327},"translate(462.5,37.8) scale(1.000)",[1264,1329],{"d":1330,"fill":1267,"stroke":1331,"style":1269},"M5 9h14 M4 15h14 M10 4l-2 16 M16 4l-2 16","var(--teal-ink,#0b6157)",[523,1333,1336],{"x":1334,"y":1308,"style":1335},"474.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Change + status",[523,1338,1339],{"x":1334,"y":1313,"style":1279},"new and lost handled",[37,1341],{"x1":1342,"y1":1288,"x2":1343,"y2":1288,"stroke":556,"style":1258},"556.0","578.0",[1291,1345],{"points":1346,"fill":1294},"582.0,71.2 573.0,76.2 573.0,66.2",[503,1348],{"x":1349,"y":512,"width":1255,"height":1256,"rx":1257,"fill":630,"stroke":631,"style":1258},"587.0",[1260,1351,1353],{"transform":1352},"translate(651.5,37.8) scale(1.000)",[1264,1354],{"d":1355,"fill":1267,"stroke":1356,"style":1269},"M4 20h16 M7 17v-5 M12 17V7 M17 17v-8","var(--gold-ink,#7a4e06)",[523,1358,1361],{"x":1359,"y":1308,"style":1360},"663.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Contribution",[523,1363,1364],{"x":1359,"y":1313,"style":1279},"adds up to the total",[10,1366,1367],{},"The same structure works for any pair of periods — this quarter against the last, this week against the same week last year, year to date against the prior year to date — because only the labelling step knows which dates belong to which period.",[20,1369,1371],{"id":1370},"step-5-write-the-comparison-to-excel","Step 5 — Write the comparison to Excel",[10,1373,1374],{},"The report sheet shows the periods, the change, the percentage, the status and the contribution, with arrows that make direction obvious at a glance:",[25,1376,1378],{"className":73,"code":1377,"language":75,"meta":30,"style":30},"from openpyxl.formatting.rule import CellIsRule\nfrom openpyxl.styles import Font, PatternFill\n\nreport = cmp.sort_values(\"Contribution pts\", ascending=False).reset_index()\nreport.columns = [\"Product\", \"Sep 2025\", \"Sep 2026\", \"Change\", \"Change %\", \"Status\", \"Contribution (pts)\"]\nreport.loc[len(report)] = [\"Total\", totals[\"Prior\"], totals[\"Current\"], totals[\"Change\"], total_change_pct, \"\", total_change_pct]\n\nwith pd.ExcelWriter(\"period_comparison.xlsx\", engine=\"openpyxl\") as xw:\n    report.to_excel(xw, sheet_name=\"Sep vs Sep\", index=False)\n    ws = xw.sheets[\"Sep vs Sep\"]\n    last = ws.max_row\n    for row in ws.iter_rows(min_row=2, max_row=last, min_col=2, max_col=4):\n        for c in row:\n            c.number_format = \"#,##0;[Red]-#,##0\"\n    for row in ws.iter_rows(min_row=2, max_row=last, min_col=5, max_col=7):\n        for c in row:\n            if c.column != 6:\n                c.number_format = '[Color10]\"▲ \"0.0;[Red]\"▼ \"0.0;0.0'\n    for c in ws[last]:\n        c.font = Font(bold=True)\n    ws.conditional_formatting.add(f\"F2:F{last}\", CellIsRule(operator=\"equal\", formula=['\"new\"'], fill=PatternFill(\"solid\", fgColor=\"DDEBF7\")))\n    ws.conditional_formatting.add(f\"F2:F{last}\", CellIsRule(operator=\"equal\", formula=['\"lost\"'], fill=PatternFill(\"solid\", fgColor=\"F2F2F2\")))\n    for col, w in zip(\"ABCDEFG\", (12, 12, 12, 12, 11, 12, 18)):\n        ws.column_dimensions[col].width = w\nprint(report.round(1).to_string(index=False))\n",[32,1379,1380,1393,1405,1409,1434,1476,1517,1521,1549,1572,1586,1596,1646,1659,1669,1709,1719,1737,1748,1760,1781,1852,1909,1961,1972],{"__ignoreMap":30},[35,1381,1382,1385,1388,1390],{"class":37,"line":38},[35,1383,1384],{"class":82},"from",[35,1386,1387],{"class":86}," openpyxl.formatting.rule ",[35,1389,83],{"class":82},[35,1391,1392],{"class":86}," CellIsRule\n",[35,1394,1395,1397,1400,1402],{"class":37,"line":96},[35,1396,1384],{"class":82},[35,1398,1399],{"class":86}," openpyxl.styles ",[35,1401,83],{"class":82},[35,1403,1404],{"class":86}," Font, PatternFill\n",[35,1406,1407],{"class":37,"line":103},[35,1408,100],{"emptyLinePlaceholder":99},[35,1410,1411,1414,1416,1418,1421,1423,1425,1427,1429,1431],{"class":37,"line":121},[35,1412,1413],{"class":86},"report ",[35,1415,109],{"class":82},[35,1417,694],{"class":239},[35,1419,1420],{"class":86},".sort_values(",[35,1422,967],{"class":45},[35,1424,409],{"class":86},[35,1426,872],{"class":239},[35,1428,109],{"class":82},[35,1430,877],{"class":186},[35,1432,1433],{"class":86},").reset_index()\n",[35,1435,1436,1439,1441,1444,1446,1448,1451,1453,1456,1458,1460,1462,1464,1466,1468,1470,1473],{"class":37,"line":126},[35,1437,1438],{"class":86},"report.columns ",[35,1440,109],{"class":82},[35,1442,1443],{"class":86}," [",[35,1445,406],{"class":45},[35,1447,409],{"class":86},[35,1449,1450],{"class":45},"\"Sep 2025\"",[35,1452,409],{"class":86},[35,1454,1455],{"class":45},"\"Sep 2026\"",[35,1457,409],{"class":86},[35,1459,687],{"class":45},[35,1461,409],{"class":86},[35,1463,729],{"class":45},[35,1465,409],{"class":86},[35,1467,794],{"class":45},[35,1469,409],{"class":86},[35,1471,1472],{"class":45},"\"Contribution (pts)\"",[35,1474,1475],{"class":86},"]\n",[35,1477,1478,1481,1484,1487,1489,1491,1494,1497,1499,1502,1504,1506,1508,1511,1514],{"class":37,"line":139},[35,1479,1480],{"class":86},"report.loc[",[35,1482,1483],{"class":186},"len",[35,1485,1486],{"class":86},"(report)] ",[35,1488,109],{"class":82},[35,1490,1443],{"class":86},[35,1492,1493],{"class":45},"\"Total\"",[35,1495,1496],{"class":86},", totals[",[35,1498,455],{"class":45},[35,1500,1501],{"class":86},"], totals[",[35,1503,460],{"class":45},[35,1505,1501],{"class":86},[35,1507,687],{"class":45},[35,1509,1510],{"class":86},"], total_change_pct, ",[35,1512,1513],{"class":45},"\"\"",[35,1515,1516],{"class":86},", total_change_pct]\n",[35,1518,1519],{"class":37,"line":161},[35,1520,100],{"emptyLinePlaceholder":99},[35,1522,1523,1526,1529,1532,1534,1537,1539,1542,1544,1546],{"class":37,"line":172},[35,1524,1525],{"class":82},"with",[35,1527,1528],{"class":86}," pd.ExcelWriter(",[35,1530,1531],{"class":45},"\"period_comparison.xlsx\"",[35,1533,409],{"class":86},[35,1535,1536],{"class":239},"engine",[35,1538,109],{"class":82},[35,1540,1541],{"class":45},"\"openpyxl\"",[35,1543,706],{"class":86},[35,1545,90],{"class":82},[35,1547,1548],{"class":86}," xw:\n",[35,1550,1551,1554,1557,1559,1562,1564,1566,1568,1570],{"class":37,"line":190},[35,1552,1553],{"class":86},"    report.to_excel(xw, ",[35,1555,1556],{"class":239},"sheet_name",[35,1558,109],{"class":82},[35,1560,1561],{"class":45},"\"Sep vs Sep\"",[35,1563,409],{"class":86},[35,1565,401],{"class":239},[35,1567,109],{"class":82},[35,1569,877],{"class":186},[35,1571,118],{"class":86},[35,1573,1574,1577,1579,1582,1584],{"class":37,"line":211},[35,1575,1576],{"class":86},"    ws ",[35,1578,109],{"class":82},[35,1580,1581],{"class":86}," xw.sheets[",[35,1583,1561],{"class":45},[35,1585,1475],{"class":86},[35,1587,1588,1591,1593],{"class":37,"line":230},[35,1589,1590],{"class":86},"    last ",[35,1592,109],{"class":82},[35,1594,1595],{"class":86}," ws.max_row\n",[35,1597,1598,1601,1604,1607,1610,1613,1615,1617,1619,1622,1624,1627,1630,1632,1634,1636,1639,1641,1643],{"class":37,"line":253},[35,1599,1600],{"class":82},"    for",[35,1602,1603],{"class":86}," row ",[35,1605,1606],{"class":82},"in",[35,1608,1609],{"class":86}," ws.iter_rows(",[35,1611,1612],{"class":239},"min_row",[35,1614,109],{"class":82},[35,1616,363],{"class":186},[35,1618,409],{"class":86},[35,1620,1621],{"class":239},"max_row",[35,1623,109],{"class":82},[35,1625,1626],{"class":86},"last, ",[35,1628,1629],{"class":239},"min_col",[35,1631,109],{"class":82},[35,1633,363],{"class":186},[35,1635,409],{"class":86},[35,1637,1638],{"class":239},"max_col",[35,1640,109],{"class":82},[35,1642,622],{"class":186},[35,1644,1645],{"class":86},"):\n",[35,1647,1648,1651,1654,1656],{"class":37,"line":258},[35,1649,1650],{"class":82},"        for",[35,1652,1653],{"class":86}," c ",[35,1655,1606],{"class":82},[35,1657,1658],{"class":86}," row:\n",[35,1660,1661,1664,1666],{"class":37,"line":280},[35,1662,1663],{"class":86},"            c.number_format ",[35,1665,109],{"class":82},[35,1667,1668],{"class":45}," \"#,##0;[Red]-#,##0\"\n",[35,1670,1671,1673,1675,1677,1679,1681,1683,1685,1687,1689,1691,1693,1695,1697,1699,1701,1703,1705,1707],{"class":37,"line":302},[35,1672,1600],{"class":82},[35,1674,1603],{"class":86},[35,1676,1606],{"class":82},[35,1678,1609],{"class":86},[35,1680,1612],{"class":239},[35,1682,109],{"class":82},[35,1684,363],{"class":186},[35,1686,409],{"class":86},[35,1688,1621],{"class":239},[35,1690,109],{"class":82},[35,1692,1626],{"class":86},[35,1694,1629],{"class":239},[35,1696,109],{"class":82},[35,1698,643],{"class":186},[35,1700,409],{"class":86},[35,1702,1638],{"class":239},[35,1704,109],{"class":82},[35,1706,1159],{"class":186},[35,1708,1645],{"class":86},[35,1710,1711,1713,1715,1717],{"class":37,"line":344},[35,1712,1650],{"class":82},[35,1714,1653],{"class":86},[35,1716,1606],{"class":82},[35,1718,1658],{"class":86},[35,1720,1722,1725,1728,1731,1734],{"class":37,"line":1721},17,[35,1723,1724],{"class":82},"            if",[35,1726,1727],{"class":86}," c.column ",[35,1729,1730],{"class":82},"!=",[35,1732,1733],{"class":186}," 6",[35,1735,1736],{"class":86},":\n",[35,1738,1740,1743,1745],{"class":37,"line":1739},18,[35,1741,1742],{"class":86},"                c.number_format ",[35,1744,109],{"class":82},[35,1746,1747],{"class":45}," '[Color10]\"▲ \"0.0;[Red]\"▼ \"0.0;0.0'\n",[35,1749,1751,1753,1755,1757],{"class":37,"line":1750},19,[35,1752,1600],{"class":82},[35,1754,1653],{"class":86},[35,1756,1606],{"class":82},[35,1758,1759],{"class":86}," ws[last]:\n",[35,1761,1763,1766,1768,1771,1774,1776,1779],{"class":37,"line":1762},20,[35,1764,1765],{"class":86},"        c.font ",[35,1767,109],{"class":82},[35,1769,1770],{"class":86}," Font(",[35,1772,1773],{"class":239},"bold",[35,1775,109],{"class":82},[35,1777,1778],{"class":186},"True",[35,1780,118],{"class":86},[35,1782,1784,1787,1789,1792,1794,1797,1799,1802,1805,1808,1810,1813,1815,1818,1820,1822,1825,1828,1831,1833,1836,1839,1841,1844,1846,1849],{"class":37,"line":1783},21,[35,1785,1786],{"class":86},"    ws.conditional_formatting.add(",[35,1788,1000],{"class":82},[35,1790,1791],{"class":45},"\"F2:F",[35,1793,1007],{"class":1006},[35,1795,1796],{"class":86},"last",[35,1798,1022],{"class":1006},[35,1800,1801],{"class":45},"\"",[35,1803,1804],{"class":86},", CellIsRule(",[35,1806,1807],{"class":239},"operator",[35,1809,109],{"class":82},[35,1811,1812],{"class":45},"\"equal\"",[35,1814,409],{"class":86},[35,1816,1817],{"class":239},"formula",[35,1819,109],{"class":82},[35,1821,245],{"class":86},[35,1823,1824],{"class":45},"'\"new\"'",[35,1826,1827],{"class":86},"], ",[35,1829,1830],{"class":239},"fill",[35,1832,109],{"class":82},[35,1834,1835],{"class":86},"PatternFill(",[35,1837,1838],{"class":45},"\"solid\"",[35,1840,409],{"class":86},[35,1842,1843],{"class":239},"fgColor",[35,1845,109],{"class":82},[35,1847,1848],{"class":45},"\"DDEBF7\"",[35,1850,1851],{"class":86},")))\n",[35,1853,1855,1857,1859,1861,1863,1865,1867,1869,1871,1873,1875,1877,1879,1881,1883,1885,1888,1890,1892,1894,1896,1898,1900,1902,1904,1907],{"class":37,"line":1854},22,[35,1856,1786],{"class":86},[35,1858,1000],{"class":82},[35,1860,1791],{"class":45},[35,1862,1007],{"class":1006},[35,1864,1796],{"class":86},[35,1866,1022],{"class":1006},[35,1868,1801],{"class":45},[35,1870,1804],{"class":86},[35,1872,1807],{"class":239},[35,1874,109],{"class":82},[35,1876,1812],{"class":45},[35,1878,409],{"class":86},[35,1880,1817],{"class":239},[35,1882,109],{"class":82},[35,1884,245],{"class":86},[35,1886,1887],{"class":45},"'\"lost\"'",[35,1889,1827],{"class":86},[35,1891,1830],{"class":239},[35,1893,109],{"class":82},[35,1895,1835],{"class":86},[35,1897,1838],{"class":45},[35,1899,409],{"class":86},[35,1901,1843],{"class":239},[35,1903,109],{"class":82},[35,1905,1906],{"class":45},"\"F2F2F2\"",[35,1908,1851],{"class":86},[35,1910,1912,1914,1917,1919,1922,1924,1927,1930,1932,1934,1936,1938,1940,1942,1944,1946,1949,1951,1953,1955,1958],{"class":37,"line":1911},23,[35,1913,1600],{"class":82},[35,1915,1916],{"class":86}," col, w ",[35,1918,1606],{"class":82},[35,1920,1921],{"class":186}," zip",[35,1923,308],{"class":86},[35,1925,1926],{"class":45},"\"ABCDEFG\"",[35,1928,1929],{"class":86},", (",[35,1931,1257],{"class":186},[35,1933,409],{"class":86},[35,1935,1257],{"class":186},[35,1937,409],{"class":86},[35,1939,1257],{"class":186},[35,1941,409],{"class":86},[35,1943,1257],{"class":186},[35,1945,409],{"class":86},[35,1947,1948],{"class":186},"11",[35,1950,409],{"class":86},[35,1952,1257],{"class":186},[35,1954,409],{"class":86},[35,1956,1957],{"class":186},"18",[35,1959,1960],{"class":86},")):\n",[35,1962,1964,1967,1969],{"class":37,"line":1963},24,[35,1965,1966],{"class":86},"        ws.column_dimensions[col].width ",[35,1968,109],{"class":82},[35,1970,1971],{"class":86}," w\n",[35,1973,1975,1977,1980,1982,1985,1987,1989,1991],{"class":37,"line":1974},25,[35,1976,305],{"class":186},[35,1978,1979],{"class":86},"(report.round(",[35,1981,296],{"class":186},[35,1983,1984],{"class":86},").to_string(",[35,1986,401],{"class":239},[35,1988,109],{"class":82},[35,1990,877],{"class":186},[35,1992,299],{"class":86},[10,1994,1995,1996,1999,2000,18],{},"The custom number format ",[32,1997,1998],{},"[Color10]\"▲ \"0.0;[Red]\"▼ \"0.0;0.0"," shows positive percentages and contributions with a green up-arrow and negative ones with a red down-arrow, while the cell still holds a plain number that can be sorted and summed. New and lost products are shaded so their blank percentage cells are explained. The total row at the bottom carries the headline change, which the contribution column adds up to. Arrow formats are covered in more detail in ",[14,2001,2003],{"href":2002},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-variance-arrows-and-trend-indicators-to-a-dashboard\u002F","Add Variance Arrows and Trend Indicators to a Dashboard",[20,2005,2007],{"id":2006},"common-pitfalls","Common pitfalls",[2009,2010,2011,2019,2025,2031,2037],"ul",{},[2012,2013,2014,2018],"li",{},[2015,2016,2017],"strong",{},"Partial periods compared with full ones."," Compare month-to-date with the same days of the prior period.",[2012,2020,2021,2024],{},[2015,2022,2023],{},"Percent change for new or lost lines."," Show a status instead of infinite or −100%.",[2012,2026,2027,2030],{},[2015,2028,2029],{},"Filling the pivot with zeros before computing percentages."," Absolute change uses zeros; percentages need both values.",[2012,2032,2033,2036],{},[2015,2034,2035],{},"Ranking by percentage change."," Rank by absolute change or contribution; percentages exaggerate small lines.",[2012,2038,2039,2042],{},[2015,2040,2041],{},"Contributions that do not add up."," Divide each change by the prior total, not by each line's own prior value.",[20,2044,2046],{"id":2045},"conclusion","Conclusion",[10,2048,2049],{},"A fair period comparison starts with like-for-like windows — the same month a year earlier, or the same days for month-to-date — labelled Current and Prior and pivoted side by side. Compute absolute change with missing values as zero, percentage change only where both periods have values, and a status for new and lost lines. Add each line's contribution in percentage points so the table explains the headline change, rank by it, and write the result with arrow number formats that keep the cells numeric.",[20,2051,2053],{"id":2052},"frequently-asked-questions","Frequently asked questions",[10,2055,2056,2059],{},[2015,2057,2058],{},"How do I compare this year with last year in a pandas pivot?","\nLabel each row Current or Prior by date window, then pivot_table with the product as index and the period label as columns, and compute the difference and percentage change between the two columns.",[10,2061,2062,2065],{},[2015,2063,2064],{},"How should new products appear in a year-over-year comparison?","\nTheir prior value is missing, so the absolute change equals the current value, but the percentage change should be left blank and the product marked as new.",[10,2067,2068,2071],{},[2015,2069,2070],{},"How do I calculate each product's contribution to total growth?","\nDivide each product's change by the total prior value and multiply by 100. These percentage-point contributions add up to the total percentage change.",[10,2073,2074,2077],{},[2015,2075,2076],{},"How do I compare month-to-date figures fairly?","\nCompare the current month to date with the same day range of the comparison month or year, rather than with the full prior month.",[20,2079,2081],{"id":2080},"related","Related",[2009,2083,2084,2090,2097,2104],{},[2012,2085,2086,2087,2089],{},"Up one level: ",[14,2088,17],{"href":16}," — summaries and crosstabs.",[2012,2091,2092,2096],{},[14,2093,2095],{"href":2094},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Frunning-totals-and-year-over-year-growth-in-pandas\u002F","Running Totals and Year-over-Year Growth in pandas"," — growth over a time series.",[2012,2098,2099,2103],{},[14,2100,2102],{"href":2101},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fcalculate-month-to-date-and-year-to-date-totals-with-pandas\u002F","Calculate Month-to-Date and Year-to-Date Totals with pandas"," — the period windows themselves.",[2012,2105,2106,2110],{},[14,2107,2109],{"href":2108},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-budget-vs-actual-variance-report\u002F","Build a Budget vs Actual Variance Report"," — comparing against a plan instead of a period.",[2112,2113,2114],"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}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}",{"title":30,"searchDepth":96,"depth":96,"links":2116},[2117,2118,2119,2120,2121,2122,2123,2124,2125,2126],{"id":22,"depth":96,"text":23},{"id":61,"depth":96,"text":62},{"id":379,"depth":96,"text":380},{"id":658,"depth":96,"text":659},{"id":893,"depth":96,"text":894},{"id":1370,"depth":96,"text":1371},{"id":2006,"depth":96,"text":2007},{"id":2045,"depth":96,"text":2046},{"id":2052,"depth":96,"text":2053},{"id":2080,"depth":96,"text":2081},"2026-10-02","Build the classic \"this period versus last period\" table from Excel sales data with pandas: choose comparable periods (same month last year, month to date versus the same days last month), pivot them side by side, compute change and percentage change that cope with new and discontinued products, rank the biggest movers, show each line's contribution to the total change, and write it to Excel with coloured arrows.","md",[2131,2133,2135,2137],{"q":2058,"a":2132},"Label each row Current or Prior by date window, then pivot_table with the product as index and the period label as columns, and compute the difference and percentage change between the two columns.",{"q":2064,"a":2134},"Their prior value is missing, so the absolute change equals the current value, but the percentage change should be left blank and the product marked as new.",{"q":2070,"a":2136},"Divide each product's change by the total prior value and multiply by 100. These percentage-point contributions add up to the total percentage change.",{"q":2076,"a":2138},"Compare the current month to date with the same day range of the comparison month or year, rather than with the full prior month.","compare two periods side by side in a pivot",null,{"breadcrumb":2142},[2143,2145,2148],{"name":2144,"item":747},"Home",{"name":2146,"item":2147},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":2149,"item":16},"Creating Pivot Tables from Excel Data with Pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcompare-two-periods-side-by-side-in-a-pivot",{"title":2152,"description":2153},"Compare Two Periods Side by Side in a pandas Pivot","This year vs last year in pandas: like-for-like periods, pivot two periods into columns, absolute and percent change with new and lost products, biggest movers, contribution to total change and an Excel report.","compare-two-periods-side-by-side-in-a-pivot","advanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcompare-two-periods-side-by-side-in-a-pivot\u002Findex",[2157,2158],"pivots-and-summaries","pandas","how-to","PGasOZzRk_Q_QYhIrDJ34u-cC_Meo81-C9_S46adYSA",[2162,2166],{"title":2163,"path":2164,"stem":2165,"children":-1},"Calculate Percent of Total per Group in pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas","advanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas\u002Findex",{"title":2167,"path":2168,"stem":2169,"children":-1},"Count Distinct Values per Group in pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcount-distinct-values-per-group-in-pandas","advanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcount-distinct-values-per-group-in-pandas\u002Findex",[2171,2173],{"path":2172,"title":2149},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data",{"path":2174,"title":2146},"\u002Fadvanced-data-transformation-and-cleaning",[],[2177,2180,2183,2186,2189,2192],{"path":2178,"title":2179,"kind":2159},"\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":2181,"title":2182,"kind":2159},"\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":2184,"title":2185,"kind":2159},"\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":2187,"title":2188,"kind":2159},"\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":2190,"title":2191,"kind":2159},"\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":2193,"title":2194,"kind":2159},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-cohort-retention-table-from-excel-data","Build a Cohort Retention Table from Excel Data",1790972086381]