[{"data":1,"prerenderedAt":2201},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fpivot-text-values-instead-of-numbers-in-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fpivot-text-values-instead-of-numbers-in-pandas":2167,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fpivot-text-values-instead-of-numbers-in-pandas":2176,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fpivot-text-values-instead-of-numbers-in-pandas":2181,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fpivot-text-values-instead-of-numbers-in-pandas":2182},{"id":4,"title":5,"body":6,"dateModified":2132,"datePublished":2132,"description":2133,"extension":2134,"faq":2135,"keyword":2144,"kind":2145,"meta":2146,"navigation":111,"path":2156,"seo":2157,"slug":2160,"stem":2161,"topics":2162,"type":2165,"__hash__":2166},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fpivot-text-values-instead-of-numbers-in-pandas\u002Findex.md","Pivot Text Values Instead of Numbers in pandas",{"type":7,"value":8,"toc":2117},"minimark",[9,33,38,69,79,83,209,221,544,548,554,670,680,684,696,809,837,994,998,1001,1183,1200,1204,1207,1542,1553,1699,1703,1706,1935,1938,1942,1957,1961,1980,1984,2034,2038,2052,2056,2062,2068,2074,2080,2084,2113],[10,11,12,13,17,18,22,23,26,27,32],"p",{},"Pivot tables are usually about sums and counts, but plenty of reports need the ",[14,15,16],"em",{},"text"," itself arranged in a grid: a project status board with projects down the side and weeks across the top showing \"On track\", \"At risk\" or \"Late\"; a rota with employees by day showing shift codes; a matrix of approvers and documents showing who signed what. The data arrives long — one row per project per week — and needs to become wide. Excel's pivot tables cannot place text in the values area at all; pandas can, with ",[19,20,21],"code",{},"pivot"," when each cell has exactly one value, and with ",[19,24,25],{},"pivot_table"," and a text-friendly aggregation when it might have several. This page covers both, and the error that tells you which case you are in. It belongs to ",[28,29,31],"a",{"href":30},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002F","Creating Pivot Tables from Excel Data",".",[34,35,37],"h2",{"id":36},"prerequisites","Prerequisites",[39,40,45],"pre",{"className":41,"code":42,"language":43,"meta":44,"style":44},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl xlsxwriter\n","bash","",[19,46,47],{"__ignoreMap":44},[48,49,52,56,60,63,66],"span",{"class":50,"line":51},"line",1,[48,53,55],{"class":54},"sMTad","pip",[48,57,59],{"class":58},"srMev"," install",[48,61,62],{"class":58}," pandas",[48,64,65],{"class":58}," openpyxl",[48,67,68],{"class":58}," xlsxwriter\n",[10,70,71,74,75,78],{},[19,72,73],{},"status_updates.xlsx"," is a log of weekly status updates for three projects. The warehouse move started in week 38, so it has no week-37 entry, and the website relaunch was updated ",[14,76,77],{},"twice"," in week 39 — first \"At risk\", then \"On track\" after a fix — which is exactly the situation that breaks a naive pivot.",[34,80,82],{"id":81},"step-1-pivot-works-when-each-cell-has-one-value","Step 1 — pivot works when each cell has one value",[39,84,88],{"className":85,"code":86,"language":87,"meta":44,"style":44},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nlog = pd.read_excel(\"status_updates.xlsx\")\nfirst_two_weeks = log[log[\"Week\"] != \"W39\"]\nboard = first_two_weeks.pivot(index=\"Project\", columns=\"Week\", values=\"Status\")\nprint(board)\n","python",[19,89,90,106,113,131,157,199],{"__ignoreMap":44},[48,91,92,96,100,103],{"class":50,"line":51},[48,93,95],{"class":94},"s-kum","import",[48,97,99],{"class":98},"skGVy"," pandas ",[48,101,102],{"class":94},"as",[48,104,105],{"class":98}," pd\n",[48,107,109],{"class":50,"line":108},2,[48,110,112],{"emptyLinePlaceholder":111},true,"\n",[48,114,116,119,122,125,128],{"class":50,"line":115},3,[48,117,118],{"class":98},"log ",[48,120,121],{"class":94},"=",[48,123,124],{"class":98}," pd.read_excel(",[48,126,127],{"class":58},"\"status_updates.xlsx\"",[48,129,130],{"class":98},")\n",[48,132,134,137,139,142,145,148,151,154],{"class":50,"line":133},4,[48,135,136],{"class":98},"first_two_weeks ",[48,138,121],{"class":94},[48,140,141],{"class":98}," log[log[",[48,143,144],{"class":58},"\"Week\"",[48,146,147],{"class":98},"] ",[48,149,150],{"class":94},"!=",[48,152,153],{"class":58}," \"W39\"",[48,155,156],{"class":98},"]\n",[48,158,160,163,165,168,172,174,177,180,183,185,187,189,192,194,197],{"class":50,"line":159},5,[48,161,162],{"class":98},"board ",[48,164,121],{"class":94},[48,166,167],{"class":98}," first_two_weeks.pivot(",[48,169,171],{"class":170},"sa561","index",[48,173,121],{"class":94},[48,175,176],{"class":58},"\"Project\"",[48,178,179],{"class":98},", ",[48,181,182],{"class":170},"columns",[48,184,121],{"class":94},[48,186,144],{"class":58},[48,188,179],{"class":98},[48,190,191],{"class":170},"values",[48,193,121],{"class":94},[48,195,196],{"class":58},"\"Status\"",[48,198,130],{"class":98},[48,200,202,206],{"class":50,"line":201},6,[48,203,205],{"class":204},"sP0c6","print",[48,207,208],{"class":98},"(board)\n",[10,210,211,213,214,217,218,220],{},[19,212,21],{}," reshapes without aggregating: each project\u002Fweek combination becomes one cell holding the status text. The warehouse move has ",[19,215,216],{},"NaN"," for week 37, because there was no update — a gap that should be visible on a status board. ",[19,219,21],{}," works for any data type because it never has to combine values.",[222,223,231,232,231,236,231,240,231,248,231,257,231,261,231,267,231,271,231,275,231,279,231,283,231,287,231,292,231,297,231,301,231,303,231,306,231,308,231,311,231,314,231,318,231,320,231,324,231,326,231,329,231,331,231,334,231,337,231,341,231,343,231,345,231,347,231,350,231,352,231,355,231,358,231,361,231,363,231,366,231,368,231,370,231,372,231,375,231,378,231,382,231,384,231,386,231,388,231,390,231,392,231,394,231,397,231,401,231,403,231,406,231,408,231,410,231,412,231,414,231,417,231,421,231,424,231,428,231,431,231,434,231,437,231,439,231,442,231,444,231,446,231,448,231,450,231,452,231,454,231,456,231,458,231,460,231,462,231,466,231,469,231,471,231,473,231,475,231,477,231,479,231,481,231,483,231,487,231,490,231,492,231,494,231,496,231,498,231,500,231,502,231,506,231,509,231,515,231,519,231,525,231,530,231,538],"svg",{"viewBox":224,"role":225,"ariaLabelledBy":226,"xmlns":229,"style":230},"4 0 752 248","img",[227,228],"pt-pivot-t","pt-pivot-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  ",[233,234,235],"title",{"id":227},"A status log becomes a project-by-week board",[237,238,239],"desc",{"id":228},"On the left the long log with one row per project and week. On the right a grid with projects down the side and weeks across, each cell holding the status text; the Warehouse move has no week 37 status.",[241,242],"rect",{"x":243,"y":244,"width":245,"height":246,"fill":247},"4","0","752","248","#ffffff",[241,249],{"x":250,"y":251,"width":252,"height":253,"rx":244,"fill":254,"stroke":255,"style":256},"20.0","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[241,258],{"x":259,"y":251,"width":260,"height":253,"rx":244,"fill":254,"stroke":255,"style":256},"52.0","64.5",[16,262,266],{"x":263,"y":264,"style":265},"84.3","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[241,268],{"x":269,"y":251,"width":270,"height":253,"rx":244,"fill":254,"stroke":255,"style":256},"116.5","41.2",[16,272,274],{"x":273,"y":264,"style":265},"137.1","B",[241,276],{"x":277,"y":251,"width":278,"height":253,"rx":244,"fill":254,"stroke":255,"style":256},"157.7","52.3",[16,280,282],{"x":281,"y":264,"style":265},"183.8","C",[241,284],{"x":250,"y":285,"width":252,"height":286,"rx":244,"fill":254,"stroke":255,"style":256},"64.0","28.0",[16,288,291],{"x":289,"y":290,"style":265},"36.0","83.0","1",[241,293],{"x":259,"y":285,"width":260,"height":286,"rx":244,"fill":294,"stroke":295,"style":296},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[16,298,300],{"x":263,"y":290,"style":299},"font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Project",[241,302],{"x":269,"y":285,"width":270,"height":286,"rx":244,"fill":294,"stroke":295,"style":296},[16,304,305],{"x":273,"y":290,"style":299},"Week",[241,307],{"x":277,"y":285,"width":278,"height":286,"rx":244,"fill":294,"stroke":295,"style":296},[16,309,310],{"x":281,"y":290,"style":299},"Status",[241,312],{"x":250,"y":313,"width":252,"height":286,"rx":244,"fill":254,"stroke":255,"style":256},"92.0",[16,315,317],{"x":289,"y":316,"style":265},"111.0","2",[241,319],{"x":259,"y":313,"width":260,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,321,323],{"x":263,"y":316,"style":322},"font-size:10.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Website",[241,325],{"x":269,"y":313,"width":270,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,327,328],{"x":273,"y":316,"style":322},"W37",[241,330],{"x":277,"y":313,"width":278,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,332,333],{"x":281,"y":316,"style":322},"On track",[241,335],{"x":250,"y":336,"width":252,"height":286,"rx":244,"fill":254,"stroke":255,"style":256},"120.0",[16,338,340],{"x":289,"y":339,"style":265},"139.0","3",[241,342],{"x":259,"y":336,"width":260,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,344,323],{"x":263,"y":339,"style":322},[241,346],{"x":269,"y":336,"width":270,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,348,349],{"x":273,"y":339,"style":322},"W38",[241,351],{"x":277,"y":336,"width":278,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,353,354],{"x":281,"y":339,"style":322},"At risk",[241,356],{"x":250,"y":357,"width":252,"height":286,"rx":244,"fill":254,"stroke":255,"style":256},"148.0",[16,359,243],{"x":289,"y":360,"style":265},"167.0",[241,362],{"x":259,"y":357,"width":260,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,364,365],{"x":263,"y":360,"style":322},"ERP",[241,367],{"x":269,"y":357,"width":270,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,369,328],{"x":273,"y":360,"style":322},[241,371],{"x":277,"y":357,"width":278,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,373,374],{"x":281,"y":360,"style":322},"Late",[241,376],{"x":250,"y":377,"width":252,"height":286,"rx":244,"fill":254,"stroke":255,"style":256},"176.0",[16,379,381],{"x":289,"y":380,"style":265},"195.0","5",[241,383],{"x":259,"y":377,"width":260,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,385,365],{"x":263,"y":380,"style":322},[241,387],{"x":269,"y":377,"width":270,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,389,349],{"x":273,"y":380,"style":322},[241,391],{"x":277,"y":377,"width":278,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,393,374],{"x":281,"y":380,"style":322},[241,395],{"x":250,"y":396,"width":252,"height":286,"rx":244,"fill":254,"stroke":255,"style":256},"204.0",[16,398,400],{"x":289,"y":399,"style":265},"223.0","6",[241,402],{"x":259,"y":396,"width":260,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,404,405],{"x":263,"y":399,"style":322},"Warehouse",[241,407],{"x":269,"y":396,"width":270,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,409,349],{"x":273,"y":399,"style":322},[241,411],{"x":277,"y":396,"width":278,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,413,333],{"x":281,"y":399,"style":322},[241,415],{"x":416,"y":251,"width":252,"height":253,"rx":244,"fill":254,"stroke":255,"style":256},"538.5",[241,418],{"x":419,"y":251,"width":420,"height":253,"rx":244,"fill":254,"stroke":255,"style":256},"570.5","64.7",[16,422,266],{"x":423,"y":264,"style":265},"602.8",[241,425],{"x":426,"y":251,"width":427,"height":253,"rx":244,"fill":254,"stroke":255,"style":256},"635.2","52.4",[16,429,274],{"x":430,"y":264,"style":265},"661.4",[241,432],{"x":433,"y":251,"width":427,"height":253,"rx":244,"fill":254,"stroke":255,"style":256},"687.6",[16,435,282],{"x":436,"y":264,"style":265},"713.8",[241,438],{"x":416,"y":285,"width":252,"height":286,"rx":244,"fill":254,"stroke":255,"style":256},[16,440,291],{"x":441,"y":290,"style":265},"554.5",[241,443],{"x":419,"y":285,"width":420,"height":286,"rx":244,"fill":294,"stroke":295,"style":296},[16,445,300],{"x":423,"y":290,"style":299},[241,447],{"x":426,"y":285,"width":427,"height":286,"rx":244,"fill":294,"stroke":295,"style":296},[16,449,328],{"x":430,"y":290,"style":299},[241,451],{"x":433,"y":285,"width":427,"height":286,"rx":244,"fill":294,"stroke":295,"style":296},[16,453,349],{"x":436,"y":290,"style":299},[241,455],{"x":416,"y":313,"width":252,"height":286,"rx":244,"fill":254,"stroke":255,"style":256},[16,457,317],{"x":441,"y":316,"style":265},[241,459],{"x":419,"y":313,"width":420,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,461,365],{"x":423,"y":316,"style":322},[241,463],{"x":426,"y":313,"width":427,"height":286,"rx":244,"fill":464,"stroke":465,"style":296},"#fce9e9","var(--danger,#dc2626)",[16,467,374],{"x":430,"y":316,"style":468},"font-size:10.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[241,470],{"x":433,"y":313,"width":427,"height":286,"rx":244,"fill":464,"stroke":465,"style":296},[16,472,374],{"x":436,"y":316,"style":468},[241,474],{"x":416,"y":336,"width":252,"height":286,"rx":244,"fill":254,"stroke":255,"style":256},[16,476,340],{"x":441,"y":339,"style":265},[241,478],{"x":419,"y":336,"width":420,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,480,405],{"x":423,"y":339,"style":322},[241,482],{"x":426,"y":336,"width":427,"height":286,"rx":244,"fill":254,"stroke":255,"style":296},[241,484],{"x":433,"y":336,"width":427,"height":286,"rx":244,"fill":485,"stroke":486,"style":296},"#d9f4f1","var(--teal,#0f9488)",[16,488,333],{"x":436,"y":339,"style":489},"font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[241,491],{"x":416,"y":357,"width":252,"height":286,"rx":244,"fill":254,"stroke":255,"style":256},[16,493,243],{"x":441,"y":360,"style":265},[241,495],{"x":419,"y":357,"width":420,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,497,323],{"x":423,"y":360,"style":322},[241,499],{"x":426,"y":357,"width":427,"height":286,"rx":244,"fill":485,"stroke":486,"style":296},[16,501,333],{"x":430,"y":360,"style":489},[241,503],{"x":433,"y":357,"width":427,"height":286,"rx":244,"fill":504,"stroke":505,"style":296},"#fdefd8","var(--gold,#b4740a)",[16,507,354],{"x":436,"y":360,"style":508},"font-size:10.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[16,510,514],{"x":511,"y":512,"style":513},"115.0","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","status log",[16,516,518],{"x":517,"y":512,"style":513},"639.2","status board",[50,520],{"x1":521,"y1":522,"x2":523,"y2":522,"stroke":295,"style":524},"220.0","108.0","524.5","stroke-width:2px",[526,527],"polygon",{"points":528,"fill":529},"528.5,108.0 519.5,113.0 519.5,103.0","#5b5cf0",[241,531],{"x":532,"y":533,"width":534,"height":535,"rx":536,"fill":247,"stroke":255,"style":537},"258.8","75.0","230.8","22.0","11","stroke-width:1.2px",[16,539,543],{"x":540,"y":541,"style":542},"374.2","90.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","pivot(index, columns, values)",[34,545,547],{"id":546},"step-2-the-duplicate-entries-error","Step 2 — The duplicate entries error",[10,549,550,551,553],{},"Add week 39, with its two updates for the website relaunch, and ",[19,552,21],{}," refuses:",[39,555,557],{"className":85,"code":556,"language":87,"meta":44,"style":44},"try:\n    log.pivot(index=\"Project\", columns=\"Week\", values=\"Status\")\nexcept ValueError as exc:\n    print(\"ValueError:\", exc)\ndupes = log[log.duplicated([\"Project\", \"Week\"], keep=False)]\nprint(dupes.to_string(index=False))\n",[19,558,559,567,596,610,624,654],{"__ignoreMap":44},[48,560,561,564],{"class":50,"line":51},[48,562,563],{"class":94},"try",[48,565,566],{"class":98},":\n",[48,568,569,572,574,576,578,580,582,584,586,588,590,592,594],{"class":50,"line":108},[48,570,571],{"class":98},"    log.pivot(",[48,573,171],{"class":170},[48,575,121],{"class":94},[48,577,176],{"class":58},[48,579,179],{"class":98},[48,581,182],{"class":170},[48,583,121],{"class":94},[48,585,144],{"class":58},[48,587,179],{"class":98},[48,589,191],{"class":170},[48,591,121],{"class":94},[48,593,196],{"class":58},[48,595,130],{"class":98},[48,597,598,601,604,607],{"class":50,"line":115},[48,599,600],{"class":94},"except",[48,602,603],{"class":204}," ValueError",[48,605,606],{"class":94}," as",[48,608,609],{"class":98}," exc:\n",[48,611,612,615,618,621],{"class":50,"line":133},[48,613,614],{"class":204},"    print",[48,616,617],{"class":98},"(",[48,619,620],{"class":58},"\"ValueError:\"",[48,622,623],{"class":98},", exc)\n",[48,625,626,629,631,634,636,638,640,643,646,648,651],{"class":50,"line":159},[48,627,628],{"class":98},"dupes ",[48,630,121],{"class":94},[48,632,633],{"class":98}," log[log.duplicated([",[48,635,176],{"class":58},[48,637,179],{"class":98},[48,639,144],{"class":58},[48,641,642],{"class":98},"], ",[48,644,645],{"class":170},"keep",[48,647,121],{"class":94},[48,649,650],{"class":204},"False",[48,652,653],{"class":98},")]\n",[48,655,656,658,661,663,665,667],{"class":50,"line":201},[48,657,205],{"class":204},[48,659,660],{"class":98},"(dupes.to_string(",[48,662,171],{"class":170},[48,664,121],{"class":94},[48,666,650],{"class":204},[48,668,669],{"class":98},"))\n",[10,671,672,675,676,679],{},[19,673,674],{},"ValueError: Index contains duplicate entries, cannot reshape"," means at least one project\u002Fweek cell would need to hold two values. The error is useful: it forces a decision about ",[14,677,678],{},"which"," value the cell should show. Listing the duplicated combinations, as the second part does, shows exactly where the conflict is — one cell, with two updates two days apart.",[34,681,683],{"id":682},"step-3-pivot_table-with-a-text-aggregation","Step 3 — pivot_table with a text aggregation",[10,685,686,688,689,179,692,695],{},[19,687,25],{}," aggregates, and for text the useful aggregations are ",[19,690,691],{},"\"first\"",[19,693,694],{},"\"last\""," and joining:",[39,697,699],{"className":85,"code":698,"language":87,"meta":44,"style":44},"last_status = log.pivot_table(index=\"Project\", columns=\"Week\", values=\"Status\", aggfunc=\"last\")\nall_statuses = log.pivot_table(index=\"Project\", columns=\"Week\", values=\"Status\",\n                               aggfunc=lambda s: \" → \".join(s))\nprint(last_status)\nprint(all_statuses)\n",[19,700,701,744,778,795,802],{"__ignoreMap":44},[48,702,703,706,708,711,713,715,717,719,721,723,725,727,729,731,733,735,738,740,742],{"class":50,"line":51},[48,704,705],{"class":98},"last_status ",[48,707,121],{"class":94},[48,709,710],{"class":98}," log.pivot_table(",[48,712,171],{"class":170},[48,714,121],{"class":94},[48,716,176],{"class":58},[48,718,179],{"class":98},[48,720,182],{"class":170},[48,722,121],{"class":94},[48,724,144],{"class":58},[48,726,179],{"class":98},[48,728,191],{"class":170},[48,730,121],{"class":94},[48,732,196],{"class":58},[48,734,179],{"class":98},[48,736,737],{"class":170},"aggfunc",[48,739,121],{"class":94},[48,741,694],{"class":58},[48,743,130],{"class":98},[48,745,746,749,751,753,755,757,759,761,763,765,767,769,771,773,775],{"class":50,"line":108},[48,747,748],{"class":98},"all_statuses ",[48,750,121],{"class":94},[48,752,710],{"class":98},[48,754,171],{"class":170},[48,756,121],{"class":94},[48,758,176],{"class":58},[48,760,179],{"class":98},[48,762,182],{"class":170},[48,764,121],{"class":94},[48,766,144],{"class":58},[48,768,179],{"class":98},[48,770,191],{"class":170},[48,772,121],{"class":94},[48,774,196],{"class":58},[48,776,777],{"class":98},",\n",[48,779,780,783,786,789,792],{"class":50,"line":115},[48,781,782],{"class":170},"                               aggfunc",[48,784,785],{"class":94},"=lambda",[48,787,788],{"class":98}," s: ",[48,790,791],{"class":58},"\" → \"",[48,793,794],{"class":98},".join(s))\n",[48,796,797,799],{"class":50,"line":133},[48,798,205],{"class":204},[48,800,801],{"class":98},"(last_status)\n",[48,803,804,806],{"class":50,"line":159},[48,805,205],{"class":204},[48,807,808],{"class":98},"(all_statuses)\n",[10,810,811,814,815,818,819,821,822,824,825,828,829,832,833,836],{},[19,812,813],{},"aggfunc=\"last\""," keeps the last value ",[14,816,817],{},"in row order",", which here happens to be the later update. Joining with an arrow shows the whole history in the cell — \"At risk → On track\" — which is often exactly what a status meeting wants to see. Note that ",[19,820,25],{}," drops all-",[19,823,216],{}," columns and rows by default (",[19,826,827],{},"dropna=True","); pass ",[19,830,831],{},"dropna=False"," to keep a week in which nothing was reported. Counting non-numeric values per cell is also possible with ",[19,834,835],{},"aggfunc=\"count\"",", which turns the grid into a \"how many updates\" table.",[222,838,231,843,231,846,231,849,231,852,231,858,231,869,231,875,231,879,231,883,231,890,231,894,231,899,231,903,231,908,231,911,231,918,231,922,231,926,231,929,231,935,231,938,231,943,231,945,231,950,231,953,231,960,231,965,231,969,231,973,231,978,231,981,231,986,231,989],{"viewBox":839,"role":225,"ariaLabelledBy":840,"xmlns":229,"style":230},"4 0 752 207",[841,842],"pt-agg-t","pt-agg-d",[233,844,845],{"id":841},"Three ways to fill a cell that has two values",[237,847,848],{"id":842},"For the website relaunch in week 39 with updates At risk then On track, aggfunc first shows At risk, last shows On track, and joining shows At risk arrow On track. Choosing by timestamp is safer than relying on row order.",[241,850],{"x":243,"y":244,"width":245,"height":851,"fill":247},"207",[241,853],{"x":250,"y":854,"width":855,"height":856,"rx":857,"fill":254,"stroke":255,"style":524},"16.0","228.0","175.0","14",[859,860,862],"g",{"transform":861},"translate(121.0,29.0) scale(1.083)",[863,864],"path",{"d":865,"fill":866,"stroke":867,"style":868},"M12 3a9 9 0 1 0 .01 0z M12 7v5l3.5 2","none","var(--text,#172033)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[16,870,874],{"x":871,"y":872,"style":873},"134.0","78.0","font-size:14px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","aggfunc=\"first\"",[50,876],{"x1":289,"y1":877,"x2":878,"y2":877,"stroke":255,"style":256},"91.0","232.0",[16,880,354],{"x":881,"y":316,"style":882},"60.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start",[859,884,886],{"transform":885},"translate(38.5,99.5) scale(0.625)",[863,887],{"d":888,"fill":866,"stroke":505,"style":889},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[16,891,893],{"x":881,"y":892,"style":882},"137.0","earliest row wins",[859,895,897],{"transform":896},"translate(38.5,125.5) scale(0.625)",[863,898],{"d":888,"fill":866,"stroke":505,"style":889},[16,900,902],{"x":881,"y":901,"style":882},"163.0","depends on row order",[859,904,906],{"transform":905},"translate(38.5,151.5) scale(0.625)",[863,907],{"d":888,"fill":866,"stroke":505,"style":889},[241,909],{"x":910,"y":854,"width":855,"height":856,"rx":857,"fill":294,"stroke":295,"style":524},"266.0",[859,912,914],{"transform":913},"translate(367.0,29.0) scale(1.083)",[863,915],{"d":916,"fill":866,"stroke":917,"style":868},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7","var(--brand-strong,#4338ca)",[16,919,813],{"x":920,"y":872,"style":921},"380.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[50,923],{"x1":924,"y1":877,"x2":925,"y2":877,"stroke":295,"style":256},"282.0","478.0",[16,927,333],{"x":928,"y":316,"style":882},"306.0",[859,930,932],{"transform":931},"translate(284.5,99.5) scale(0.625)",[863,933],{"d":934,"fill":866,"stroke":486,"style":889},"M5 12.5l4.5 4.5L19 7",[16,936,937],{"x":928,"y":892,"style":882},"latest row wins",[859,939,941],{"transform":940},"translate(284.5,125.5) scale(0.625)",[863,942],{"d":888,"fill":866,"stroke":505,"style":889},[16,944,902],{"x":928,"y":901,"style":882},[859,946,948],{"transform":947},"translate(284.5,151.5) scale(0.625)",[863,949],{"d":888,"fill":866,"stroke":505,"style":889},[241,951],{"x":952,"y":854,"width":855,"height":856,"rx":857,"fill":485,"stroke":486,"style":524},"512.0",[859,954,956],{"transform":955},"translate(613.0,29.0) scale(1.083)",[863,957],{"d":958,"fill":866,"stroke":959,"style":868},"M6 4v5a5 5 0 0 0 5 5h7 M6 20v-5 M15 11l3 3-3 3","var(--teal-ink,#0b6157)",[16,961,964],{"x":962,"y":872,"style":963},"626.0","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","join with →",[50,966],{"x1":967,"y1":877,"x2":968,"y2":877,"stroke":486,"style":256},"528.0","724.0",[16,970,972],{"x":971,"y":316,"style":882},"552.0","At risk → On track",[859,974,976],{"transform":975},"translate(530.5,99.5) scale(0.625)",[863,977],{"d":934,"fill":866,"stroke":486,"style":889},[16,979,980],{"x":971,"y":892,"style":882},"full history in the cell",[859,982,984],{"transform":983},"translate(530.5,125.5) scale(0.625)",[863,985],{"d":934,"fill":866,"stroke":486,"style":889},[16,987,988],{"x":971,"y":901,"style":882},"longer text",[859,990,992],{"transform":991},"translate(530.5,151.5) scale(0.625)",[863,993],{"d":888,"fill":866,"stroke":505,"style":889},[34,995,997],{"id":996},"step-4-the-latest-value-by-timestamp-not-by-row-order","Step 4 — The latest value by timestamp, not by row order",[10,999,1000],{},"\"First\" and \"last\" depend on the order rows happen to be in, which is not guaranteed — an export sorted by project name, or a merge, can reorder them. When there is a timestamp, use it: sort, keep the latest row per cell, then pivot:",[39,1002,1004],{"className":85,"code":1003,"language":87,"meta":44,"style":44},"latest = (log.sort_values(\"Updated\")\n             .drop_duplicates([\"Project\", \"Week\"], keep=\"last\"))\nboard = latest.pivot(index=\"Project\", columns=\"Week\", values=\"Status\")\nprint(board)\nshuffled = log.sample(frac=1, random_state=1)\nboard_again = (shuffled.sort_values(\"Updated\").drop_duplicates([\"Project\", \"Week\"], keep=\"last\")\n                       .pivot(index=\"Project\", columns=\"Week\", values=\"Status\"))\nprint(\"same result after shuffling:\", board.equals(board_again))\n",[19,1005,1006,1021,1042,1075,1081,1109,1140,1170],{"__ignoreMap":44},[48,1007,1008,1011,1013,1016,1019],{"class":50,"line":51},[48,1009,1010],{"class":98},"latest ",[48,1012,121],{"class":94},[48,1014,1015],{"class":98}," (log.sort_values(",[48,1017,1018],{"class":58},"\"Updated\"",[48,1020,130],{"class":98},[48,1022,1023,1026,1028,1030,1032,1034,1036,1038,1040],{"class":50,"line":108},[48,1024,1025],{"class":98},"             .drop_duplicates([",[48,1027,176],{"class":58},[48,1029,179],{"class":98},[48,1031,144],{"class":58},[48,1033,642],{"class":98},[48,1035,645],{"class":170},[48,1037,121],{"class":94},[48,1039,694],{"class":58},[48,1041,669],{"class":98},[48,1043,1044,1046,1048,1051,1053,1055,1057,1059,1061,1063,1065,1067,1069,1071,1073],{"class":50,"line":115},[48,1045,162],{"class":98},[48,1047,121],{"class":94},[48,1049,1050],{"class":98}," latest.pivot(",[48,1052,171],{"class":170},[48,1054,121],{"class":94},[48,1056,176],{"class":58},[48,1058,179],{"class":98},[48,1060,182],{"class":170},[48,1062,121],{"class":94},[48,1064,144],{"class":58},[48,1066,179],{"class":98},[48,1068,191],{"class":170},[48,1070,121],{"class":94},[48,1072,196],{"class":58},[48,1074,130],{"class":98},[48,1076,1077,1079],{"class":50,"line":133},[48,1078,205],{"class":204},[48,1080,208],{"class":98},[48,1082,1083,1086,1088,1091,1094,1096,1098,1100,1103,1105,1107],{"class":50,"line":159},[48,1084,1085],{"class":98},"shuffled ",[48,1087,121],{"class":94},[48,1089,1090],{"class":98}," log.sample(",[48,1092,1093],{"class":170},"frac",[48,1095,121],{"class":94},[48,1097,291],{"class":204},[48,1099,179],{"class":98},[48,1101,1102],{"class":170},"random_state",[48,1104,121],{"class":94},[48,1106,291],{"class":204},[48,1108,130],{"class":98},[48,1110,1111,1114,1116,1119,1121,1124,1126,1128,1130,1132,1134,1136,1138],{"class":50,"line":201},[48,1112,1113],{"class":98},"board_again ",[48,1115,121],{"class":94},[48,1117,1118],{"class":98}," (shuffled.sort_values(",[48,1120,1018],{"class":58},[48,1122,1123],{"class":98},").drop_duplicates([",[48,1125,176],{"class":58},[48,1127,179],{"class":98},[48,1129,144],{"class":58},[48,1131,642],{"class":98},[48,1133,645],{"class":170},[48,1135,121],{"class":94},[48,1137,694],{"class":58},[48,1139,130],{"class":98},[48,1141,1143,1146,1148,1150,1152,1154,1156,1158,1160,1162,1164,1166,1168],{"class":50,"line":1142},7,[48,1144,1145],{"class":98},"                       .pivot(",[48,1147,171],{"class":170},[48,1149,121],{"class":94},[48,1151,176],{"class":58},[48,1153,179],{"class":98},[48,1155,182],{"class":170},[48,1157,121],{"class":94},[48,1159,144],{"class":58},[48,1161,179],{"class":98},[48,1163,191],{"class":170},[48,1165,121],{"class":94},[48,1167,196],{"class":58},[48,1169,669],{"class":98},[48,1171,1173,1175,1177,1180],{"class":50,"line":1172},8,[48,1174,205],{"class":204},[48,1176,617],{"class":98},[48,1178,1179],{"class":58},"\"same result after shuffling:\"",[48,1181,1182],{"class":98},", board.equals(board_again))\n",[10,1184,1185,1186,1189,1190,1192,1193,1195,1196,32],{},"Sorting by ",[19,1187,1188],{},"Updated"," and keeping the last row per project\u002Fweek leaves exactly one value per cell, so plain ",[19,1191,21],{}," works and the website relaunch shows \"On track\" for week 39. Shuffling the input and repeating gives the identical board, which ",[19,1194,813],{}," on unsorted data would not guarantee. The keep-latest pattern is covered in general in ",[28,1197,1199],{"href":1198},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fkeep-the-latest-record-per-key-with-pandas\u002F","Keep the Latest Record per Key with pandas",[34,1201,1203],{"id":1202},"step-5-write-the-board-with-colours-by-value","Step 5 — Write the board with colours by value",[10,1205,1206],{},"A status board is read by colour. Write it with one conditional format per status:",[39,1208,1210],{"className":85,"code":1209,"language":87,"meta":44,"style":44},"order = [\"Website relaunch\", \"ERP upgrade\", \"Warehouse move\"]\nboard = board.reindex(order).fillna(\"—\")\ncolours = {\"On track\": (\"#D9F4F1\", \"#0B6157\"), \"At risk\": (\"#FDEFD8\", \"#7A4E06\"), \"Late\": (\"#FCE9E9\", \"#9C0006\")}\nwith pd.ExcelWriter(\"status_board.xlsx\", engine=\"xlsxwriter\") as xw:\n    board.to_excel(xw, sheet_name=\"Board\")\n    book, ws = xw.book, xw.sheets[\"Board\"]\n    for status, (bg, fg) in colours.items():\n        fmt = book.add_format({\"bg_color\": bg, \"font_color\": fg, \"bold\": True})\n        ws.conditional_format(1, 1, len(board), len(board.columns),\n                              {\"type\": \"cell\", \"criteria\": \"==\", \"value\": f'\"{status}\"', \"format\": fmt})\n    ws.set_column(0, 0, 18)\n    ws.set_column(1, len(board.columns), 11)\nprint(board)\n",[19,1211,1212,1237,1251,1309,1338,1353,1367,1381,1415,1440,1498,1517,1535],{"__ignoreMap":44},[48,1213,1214,1217,1219,1222,1225,1227,1230,1232,1235],{"class":50,"line":51},[48,1215,1216],{"class":98},"order ",[48,1218,121],{"class":94},[48,1220,1221],{"class":98}," [",[48,1223,1224],{"class":58},"\"Website relaunch\"",[48,1226,179],{"class":98},[48,1228,1229],{"class":58},"\"ERP upgrade\"",[48,1231,179],{"class":98},[48,1233,1234],{"class":58},"\"Warehouse move\"",[48,1236,156],{"class":98},[48,1238,1239,1241,1243,1246,1249],{"class":50,"line":108},[48,1240,162],{"class":98},[48,1242,121],{"class":94},[48,1244,1245],{"class":98}," board.reindex(order).fillna(",[48,1247,1248],{"class":58},"\"—\"",[48,1250,130],{"class":98},[48,1252,1253,1256,1258,1261,1264,1267,1270,1272,1275,1278,1281,1283,1286,1288,1291,1293,1296,1298,1301,1303,1306],{"class":50,"line":115},[48,1254,1255],{"class":98},"colours ",[48,1257,121],{"class":94},[48,1259,1260],{"class":98}," {",[48,1262,1263],{"class":58},"\"On track\"",[48,1265,1266],{"class":98},": (",[48,1268,1269],{"class":58},"\"#D9F4F1\"",[48,1271,179],{"class":98},[48,1273,1274],{"class":58},"\"#0B6157\"",[48,1276,1277],{"class":98},"), ",[48,1279,1280],{"class":58},"\"At risk\"",[48,1282,1266],{"class":98},[48,1284,1285],{"class":58},"\"#FDEFD8\"",[48,1287,179],{"class":98},[48,1289,1290],{"class":58},"\"#7A4E06\"",[48,1292,1277],{"class":98},[48,1294,1295],{"class":58},"\"Late\"",[48,1297,1266],{"class":98},[48,1299,1300],{"class":58},"\"#FCE9E9\"",[48,1302,179],{"class":98},[48,1304,1305],{"class":58},"\"#9C0006\"",[48,1307,1308],{"class":98},")}\n",[48,1310,1311,1314,1317,1320,1322,1325,1327,1330,1333,1335],{"class":50,"line":133},[48,1312,1313],{"class":94},"with",[48,1315,1316],{"class":98}," pd.ExcelWriter(",[48,1318,1319],{"class":58},"\"status_board.xlsx\"",[48,1321,179],{"class":98},[48,1323,1324],{"class":170},"engine",[48,1326,121],{"class":94},[48,1328,1329],{"class":58},"\"xlsxwriter\"",[48,1331,1332],{"class":98},") ",[48,1334,102],{"class":94},[48,1336,1337],{"class":98}," xw:\n",[48,1339,1340,1343,1346,1348,1351],{"class":50,"line":159},[48,1341,1342],{"class":98},"    board.to_excel(xw, ",[48,1344,1345],{"class":170},"sheet_name",[48,1347,121],{"class":94},[48,1349,1350],{"class":58},"\"Board\"",[48,1352,130],{"class":98},[48,1354,1355,1358,1360,1363,1365],{"class":50,"line":201},[48,1356,1357],{"class":98},"    book, ws ",[48,1359,121],{"class":94},[48,1361,1362],{"class":98}," xw.book, xw.sheets[",[48,1364,1350],{"class":58},[48,1366,156],{"class":98},[48,1368,1369,1372,1375,1378],{"class":50,"line":1142},[48,1370,1371],{"class":94},"    for",[48,1373,1374],{"class":98}," status, (bg, fg) ",[48,1376,1377],{"class":94},"in",[48,1379,1380],{"class":98}," colours.items():\n",[48,1382,1383,1386,1388,1391,1394,1397,1400,1403,1406,1409,1412],{"class":50,"line":1172},[48,1384,1385],{"class":98},"        fmt ",[48,1387,121],{"class":94},[48,1389,1390],{"class":98}," book.add_format({",[48,1392,1393],{"class":58},"\"bg_color\"",[48,1395,1396],{"class":98},": bg, ",[48,1398,1399],{"class":58},"\"font_color\"",[48,1401,1402],{"class":98},": fg, ",[48,1404,1405],{"class":58},"\"bold\"",[48,1407,1408],{"class":98},": ",[48,1410,1411],{"class":204},"True",[48,1413,1414],{"class":98},"})\n",[48,1416,1418,1421,1423,1425,1427,1429,1432,1435,1437],{"class":50,"line":1417},9,[48,1419,1420],{"class":98},"        ws.conditional_format(",[48,1422,291],{"class":204},[48,1424,179],{"class":98},[48,1426,291],{"class":204},[48,1428,179],{"class":98},[48,1430,1431],{"class":204},"len",[48,1433,1434],{"class":98},"(board), ",[48,1436,1431],{"class":204},[48,1438,1439],{"class":98},"(board.columns),\n",[48,1441,1443,1446,1449,1451,1454,1456,1459,1461,1464,1466,1469,1471,1474,1477,1481,1484,1487,1490,1492,1495],{"class":50,"line":1442},10,[48,1444,1445],{"class":98},"                              {",[48,1447,1448],{"class":58},"\"type\"",[48,1450,1408],{"class":98},[48,1452,1453],{"class":58},"\"cell\"",[48,1455,179],{"class":98},[48,1457,1458],{"class":58},"\"criteria\"",[48,1460,1408],{"class":98},[48,1462,1463],{"class":58},"\"==\"",[48,1465,179],{"class":98},[48,1467,1468],{"class":58},"\"value\"",[48,1470,1408],{"class":98},[48,1472,1473],{"class":94},"f",[48,1475,1476],{"class":58},"'\"",[48,1478,1480],{"class":1479},"sSjpA","{",[48,1482,1483],{"class":98},"status",[48,1485,1486],{"class":1479},"}",[48,1488,1489],{"class":58},"\"'",[48,1491,179],{"class":98},[48,1493,1494],{"class":58},"\"format\"",[48,1496,1497],{"class":98},": fmt})\n",[48,1499,1501,1504,1506,1508,1510,1512,1515],{"class":50,"line":1500},11,[48,1502,1503],{"class":98},"    ws.set_column(",[48,1505,244],{"class":204},[48,1507,179],{"class":98},[48,1509,244],{"class":204},[48,1511,179],{"class":98},[48,1513,1514],{"class":204},"18",[48,1516,130],{"class":98},[48,1518,1520,1522,1524,1526,1528,1531,1533],{"class":50,"line":1519},12,[48,1521,1503],{"class":98},[48,1523,291],{"class":204},[48,1525,179],{"class":98},[48,1527,1431],{"class":204},[48,1529,1530],{"class":98},"(board.columns), ",[48,1532,536],{"class":204},[48,1534,130],{"class":98},[48,1536,1538,1540],{"class":50,"line":1537},13,[48,1539,205],{"class":204},[48,1541,208],{"class":98},[10,1543,1544,1545,1548,1549,32],{},"Each status gets a fill and text colour; \"—\" marks weeks with no update. The criteria value must include the quotation marks (",[19,1546,1547],{},"'\"On track\"'",") because it is compared as a formula expression, and a bare word would be read as a name. Projects are reindexed into a business order rather than alphabetical, as described in ",[28,1550,1552],{"href":1551},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fsort-pivot-rows-in-a-custom-order-with-pandas\u002F","Sort Pivot Rows in a Custom Order with pandas",[222,1554,231,1560,231,1563,231,1566,231,1571,231,1574,231,1578,231,1582,231,1586,231,1589,231,1592,231,1595,231,1598,231,1602,231,1604,231,1608,231,1610,231,1613,231,1615,231,1617,231,1619,231,1621,231,1623,231,1626,231,1629,231,1632,231,1634,231,1638,231,1640,231,1643,231,1645,231,1648,231,1650,231,1652,231,1655,231,1657,231,1659,231,1662,231,1664,231,1667,231,1669,231,1671,231,1673,231,1675,231,1678,231,1681,231,1683,231,1686,231,1688,231,1691,231,1693,231,1695,231,1697],{"viewBox":1555,"role":225,"ariaLabelledBy":1556,"xmlns":229,"style":1559},"178 0 404 168",[1557,1558],"pt-board-t","pt-board-d","width:100%;max-width:404px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[233,1561,1562],{"id":1557},"The finished status board",[237,1564,1565],{"id":1558},"Projects down the side and weeks 37 to 39 across. Website relaunch reads On track, At risk, On track; ERP upgrade Late, Late, At risk; Warehouse move no update, On track, On track. Cells are coloured by status.",[241,1567],{"x":1568,"y":244,"width":1569,"height":1570,"fill":247},"178","404","168",[241,1572],{"x":1573,"y":854,"width":252,"height":253,"rx":244,"fill":254,"stroke":255,"style":256},"194.3",[241,1575],{"x":1576,"y":854,"width":1577,"height":253,"rx":244,"fill":254,"stroke":255,"style":256},"226.3","123.4",[16,1579,266],{"x":1580,"y":1581,"style":265},"288.0","33.0",[241,1583],{"x":1584,"y":854,"width":1585,"height":253,"rx":244,"fill":254,"stroke":255,"style":256},"349.7","72.0",[16,1587,274],{"x":1588,"y":1581,"style":265},"385.7",[241,1590],{"x":1591,"y":854,"width":1585,"height":253,"rx":244,"fill":254,"stroke":255,"style":256},"421.7",[16,1593,282],{"x":1594,"y":1581,"style":265},"457.7",[241,1596],{"x":1597,"y":854,"width":1585,"height":253,"rx":244,"fill":254,"stroke":255,"style":256},"493.7",[16,1599,1601],{"x":1600,"y":1581,"style":265},"529.7","D",[241,1603],{"x":1573,"y":251,"width":252,"height":286,"rx":244,"fill":254,"stroke":255,"style":256},[16,1605,291],{"x":1606,"y":1607,"style":265},"210.3","59.0",[241,1609],{"x":1576,"y":251,"width":1577,"height":286,"rx":244,"fill":294,"stroke":295,"style":296},[16,1611,300],{"x":1580,"y":1607,"style":1612},"font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[241,1614],{"x":1584,"y":251,"width":1585,"height":286,"rx":244,"fill":294,"stroke":295,"style":296},[16,1616,328],{"x":1588,"y":1607,"style":1612},[241,1618],{"x":1591,"y":251,"width":1585,"height":286,"rx":244,"fill":294,"stroke":295,"style":296},[16,1620,349],{"x":1594,"y":1607,"style":1612},[241,1622],{"x":1597,"y":251,"width":1585,"height":286,"rx":244,"fill":294,"stroke":295,"style":296},[16,1624,1625],{"x":1600,"y":1607,"style":1612},"W39",[241,1627],{"x":1573,"y":1628,"width":252,"height":286,"rx":244,"fill":254,"stroke":255,"style":256},"68.0",[16,1630,317],{"x":1606,"y":1631,"style":265},"87.0",[241,1633],{"x":1576,"y":1628,"width":1577,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,1635,1637],{"x":1580,"y":1631,"style":1636},"font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Website relaunch",[241,1639],{"x":1584,"y":1628,"width":1585,"height":286,"rx":244,"fill":485,"stroke":486,"style":296},[16,1641,333],{"x":1588,"y":1631,"style":1642},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[241,1644],{"x":1591,"y":1628,"width":1585,"height":286,"rx":244,"fill":504,"stroke":505,"style":296},[16,1646,354],{"x":1594,"y":1631,"style":1647},"font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[241,1649],{"x":1597,"y":1628,"width":1585,"height":286,"rx":244,"fill":485,"stroke":486,"style":296},[16,1651,333],{"x":1600,"y":1631,"style":1642},[241,1653],{"x":1573,"y":1654,"width":252,"height":286,"rx":244,"fill":254,"stroke":255,"style":256},"96.0",[16,1656,340],{"x":1606,"y":511,"style":265},[241,1658],{"x":1576,"y":1654,"width":1577,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,1660,1661],{"x":1580,"y":511,"style":1636},"ERP upgrade",[241,1663],{"x":1584,"y":1654,"width":1585,"height":286,"rx":244,"fill":464,"stroke":465,"style":296},[16,1665,374],{"x":1588,"y":511,"style":1666},"font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[241,1668],{"x":1591,"y":1654,"width":1585,"height":286,"rx":244,"fill":464,"stroke":465,"style":296},[16,1670,374],{"x":1594,"y":511,"style":1666},[241,1672],{"x":1597,"y":1654,"width":1585,"height":286,"rx":244,"fill":504,"stroke":505,"style":296},[16,1674,354],{"x":1600,"y":511,"style":1647},[241,1676],{"x":1573,"y":1677,"width":252,"height":286,"rx":244,"fill":254,"stroke":255,"style":256},"124.0",[16,1679,243],{"x":1606,"y":1680,"style":265},"143.0",[241,1682],{"x":1576,"y":1677,"width":1577,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,1684,1685],{"x":1580,"y":1680,"style":1636},"Warehouse move",[241,1687],{"x":1584,"y":1677,"width":1585,"height":286,"rx":244,"fill":247,"stroke":255,"style":256},[16,1689,1690],{"x":1588,"y":1680,"style":1636},"—",[241,1692],{"x":1591,"y":1677,"width":1585,"height":286,"rx":244,"fill":485,"stroke":486,"style":296},[16,1694,333],{"x":1594,"y":1680,"style":1642},[241,1696],{"x":1597,"y":1677,"width":1585,"height":286,"rx":244,"fill":485,"stroke":486,"style":296},[16,1698,333],{"x":1600,"y":1680,"style":1642},[34,1700,1702],{"id":1701},"step-6-summarise-the-board-per-project","Step 6 — Summarise the board per project",[10,1704,1705],{},"A status board raises the next question at once: how many weeks has each project spent in each state, and is it getting better? Both come from the same grid:",[39,1707,1709],{"className":85,"code":1708,"language":87,"meta":44,"style":44},"weeks = board.replace(\"—\", pd.NA)\nsummary = weeks.apply(lambda row: row.value_counts(), axis=1).fillna(0).astype(int)\nrank = {\"Late\": 0, \"At risk\": 1, \"On track\": 2}\nlatest_two = weeks.apply(lambda row: row.dropna().tail(2).map(rank).tolist(), axis=1)\nsummary[\"Trend\"] = latest_two.map(lambda r: \"improving\" if len(r) == 2 and r[1] > r[0]\n                                  else \"worsening\" if len(r) == 2 and r[1] \u003C r[0] else \"steady\")\nprint(summary)\n",[19,1710,1711,1731,1767,1801,1828,1885,1928],{"__ignoreMap":44},[48,1712,1713,1716,1718,1721,1723,1726,1729],{"class":50,"line":51},[48,1714,1715],{"class":98},"weeks ",[48,1717,121],{"class":94},[48,1719,1720],{"class":98}," board.replace(",[48,1722,1248],{"class":58},[48,1724,1725],{"class":98},", pd.",[48,1727,1728],{"class":204},"NA",[48,1730,130],{"class":98},[48,1732,1733,1736,1738,1741,1744,1747,1750,1752,1754,1757,1759,1762,1765],{"class":50,"line":108},[48,1734,1735],{"class":98},"summary ",[48,1737,121],{"class":94},[48,1739,1740],{"class":98}," weeks.apply(",[48,1742,1743],{"class":94},"lambda",[48,1745,1746],{"class":98}," row: row.value_counts(), ",[48,1748,1749],{"class":170},"axis",[48,1751,121],{"class":94},[48,1753,291],{"class":204},[48,1755,1756],{"class":98},").fillna(",[48,1758,244],{"class":204},[48,1760,1761],{"class":98},").astype(",[48,1763,1764],{"class":204},"int",[48,1766,130],{"class":98},[48,1768,1769,1772,1774,1776,1778,1780,1782,1784,1786,1788,1790,1792,1794,1796,1798],{"class":50,"line":115},[48,1770,1771],{"class":98},"rank ",[48,1773,121],{"class":94},[48,1775,1260],{"class":98},[48,1777,1295],{"class":58},[48,1779,1408],{"class":98},[48,1781,244],{"class":204},[48,1783,179],{"class":98},[48,1785,1280],{"class":58},[48,1787,1408],{"class":98},[48,1789,291],{"class":204},[48,1791,179],{"class":98},[48,1793,1263],{"class":58},[48,1795,1408],{"class":98},[48,1797,317],{"class":204},[48,1799,1800],{"class":98},"}\n",[48,1802,1803,1806,1808,1810,1812,1815,1817,1820,1822,1824,1826],{"class":50,"line":133},[48,1804,1805],{"class":98},"latest_two ",[48,1807,121],{"class":94},[48,1809,1740],{"class":98},[48,1811,1743],{"class":94},[48,1813,1814],{"class":98}," row: row.dropna().tail(",[48,1816,317],{"class":204},[48,1818,1819],{"class":98},").map(rank).tolist(), ",[48,1821,1749],{"class":170},[48,1823,121],{"class":94},[48,1825,291],{"class":204},[48,1827,130],{"class":98},[48,1829,1830,1833,1836,1838,1840,1843,1845,1848,1851,1854,1857,1860,1863,1866,1869,1872,1874,1876,1879,1881,1883],{"class":50,"line":159},[48,1831,1832],{"class":98},"summary[",[48,1834,1835],{"class":58},"\"Trend\"",[48,1837,147],{"class":98},[48,1839,121],{"class":94},[48,1841,1842],{"class":98}," latest_two.map(",[48,1844,1743],{"class":94},[48,1846,1847],{"class":98}," r: ",[48,1849,1850],{"class":58},"\"improving\"",[48,1852,1853],{"class":94}," if",[48,1855,1856],{"class":204}," len",[48,1858,1859],{"class":98},"(r) ",[48,1861,1862],{"class":94},"==",[48,1864,1865],{"class":204}," 2",[48,1867,1868],{"class":94}," and",[48,1870,1871],{"class":98}," r[",[48,1873,291],{"class":204},[48,1875,147],{"class":98},[48,1877,1878],{"class":94},">",[48,1880,1871],{"class":98},[48,1882,244],{"class":204},[48,1884,156],{"class":98},[48,1886,1887,1890,1893,1895,1897,1899,1901,1903,1905,1907,1909,1911,1914,1916,1918,1920,1923,1926],{"class":50,"line":201},[48,1888,1889],{"class":94},"                                  else",[48,1891,1892],{"class":58}," \"worsening\"",[48,1894,1853],{"class":94},[48,1896,1856],{"class":204},[48,1898,1859],{"class":98},[48,1900,1862],{"class":94},[48,1902,1865],{"class":204},[48,1904,1868],{"class":94},[48,1906,1871],{"class":98},[48,1908,291],{"class":204},[48,1910,147],{"class":98},[48,1912,1913],{"class":94},"\u003C",[48,1915,1871],{"class":98},[48,1917,244],{"class":204},[48,1919,147],{"class":98},[48,1921,1922],{"class":94},"else",[48,1924,1925],{"class":58}," \"steady\"",[48,1927,130],{"class":98},[48,1929,1930,1932],{"class":50,"line":1142},[48,1931,205],{"class":204},[48,1933,1934],{"class":98},"(summary)\n",[10,1936,1937],{},"Counting values along each row gives weeks per status; comparing the last two reported statuses on a simple scale gives a trend. The website relaunch improved from At risk to On track, the ERP upgrade from Late to At risk, and the warehouse move is steady. A trend column next to the board answers the first question in most status meetings before anyone asks it.",[34,1939,1941],{"id":1940},"step-7-when-excels-own-pivot-table-is-not-enough","Step 7 — When Excel's own pivot table is not enough",[10,1943,1944,1945,1948,1949,1952,1953,32],{},"People often try this in Excel first and discover that a pivot table's values area only summarises — it shows counts of text, never the text. The workarounds in Excel (Power Pivot measures with ",[19,1946,1947],{},"CONCATENATEX",", or a grid of ",[19,1950,1951],{},"XLOOKUP"," formulas against the log) work but are fragile when weeks are added. Generating the board in pandas each week and writing it as values, as above, is usually simpler; when the workbook must stay live, write one lookup formula per cell instead, as in ",[28,1954,1956],{"href":1955},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fwrite-lookup-formulas-with-openpyxl\u002F","Write Lookup Formulas (XLOOKUP, VLOOKUP, INDEX\u002FMATCH) with openpyxl",[34,1958,1960],{"id":1959},"performance-notes","Performance notes",[10,1962,1963,1964,1966,1967,1969,1970,1973,1974,1977,1978,32],{},"Pivoting text costs the same as pivoting numbers; the slow part is a Python ",[19,1965,1743],{}," used as ",[19,1968,737],{},", which runs once per cell. For a board of a few hundred projects by fifty weeks that is instant. For millions of cells, prefer the sort-and-drop-duplicates approach, which uses only vectorised operations, and join text with a group-by on the long data (",[19,1971,1972],{},"groupby([...])[\"Status\"].agg(\" → \".join)",") before a final ",[19,1975,1976],{},"unstack",", which is considerably faster than a lambda inside ",[19,1979,25],{},[34,1981,1983],{"id":1982},"common-pitfalls","Common pitfalls",[1985,1986,1987,1995,2009,2018,2028],"ul",{},[1988,1989,1990,1994],"li",{},[1991,1992,1993],"strong",{},"Using pivot on duplicated keys."," It raises \"Index contains duplicate entries\"; decide which value wins.",[1988,1996,1997,2000,2001,2004,2005,2008],{},[1991,1998,1999],{},"Relying on row order."," ",[19,2002,2003],{},"first"," and ",[19,2006,2007],{},"last"," follow the current order; sort by a timestamp first.",[1988,2010,2011,2000,2014,2017],{},[1991,2012,2013],{},"Numeric aggregations on text.",[19,2015,2016],{},"aggfunc=\"sum\""," concatenates strings without separators or fails; use a join.",[1988,2019,2020,2000,2023,2025,2026,32],{},[1991,2021,2022],{},"Disappearing empty weeks.",[19,2024,25],{}," drops all-empty columns unless ",[19,2027,831],{},[1988,2029,2030,2033],{},[1991,2031,2032],{},"Unquoted criteria in conditional formats."," Text values must be wrapped in quotes inside the criteria.",[34,2035,2037],{"id":2036},"conclusion","Conclusion",[10,2039,2040,2041,2043,2044,2046,2047,179,2049,2051],{},"Pivot text with ",[19,2042,21],{}," when every row\u002Fcolumn combination has one value, and treat its \"Index contains duplicate entries\" error as a prompt to decide which value wins. Use ",[19,2045,25],{}," with ",[19,2048,691],{},[19,2050,694],{}," or a join to combine several values, but prefer sorting by a timestamp and keeping the latest row, which does not depend on row order. Keep empty cells visible, put rows in a business order, and colour the cells by value when writing the board to Excel.",[34,2053,2055],{"id":2054},"frequently-asked-questions","Frequently asked questions",[10,2057,2058,2061],{},[1991,2059,2060],{},"How do I pivot text values in pandas?","\nUse df.pivot(index=..., columns=..., values=\"Status\") when each combination appears once. If combinations repeat, use pivot_table with a text aggregation such as aggfunc=\"last\" or aggfunc=\", \".join.",[10,2063,2064,2067],{},[1991,2065,2066],{},"What does \"Index contains duplicate entries, cannot reshape\" mean?","\nAt least one combination of the index and columns appears in more than one row, so pivot cannot put a single value in that cell. Remove the duplicates, for example by keeping the latest row, or use pivot_table with an aggregation.",[10,2069,2070,2073],{},[1991,2071,2072],{},"How do I show the latest status in each cell of a pivot?","\nSort the data by its timestamp, keep the last row per combination with drop_duplicates(keep=\"last\"), then pivot. This is independent of the original row order.",[10,2075,2076,2079],{},[1991,2077,2078],{},"Can an Excel pivot table show text in the values area?","\nNo; Excel's pivot table summarises values, so text appears only as a count. Build the grid in pandas and write it as values, or use lookup formulas against the source data.",[34,2081,2083],{"id":2082},"related","Related",[1985,2085,2086,2092,2099,2106],{},[1988,2087,2088,2089,2091],{},"Up one level: ",[28,2090,31],{"href":30}," — reshaping and summarising.",[1988,2093,2094,2098],{},[28,2095,2097],{"href":2096},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcreate-pivot-table-from-excel-with-pandas\u002F","Create a Pivot Table from Excel with pandas"," — numeric pivots.",[1988,2100,2101,2105],{},[28,2102,2104],{"href":2103},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Funpivot-a-wide-excel-sheet-with-pandas-melt\u002F","Unpivot a Wide Excel Sheet with pandas melt"," — the reverse reshape.",[1988,2107,2108,2112],{},[28,2109,2111],{"href":2110},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Ftextjoin-equivalent-in-pandas\u002F","TEXTJOIN Equivalent in pandas"," — joining text values in groups.",[2114,2115,2116],"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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":44,"searchDepth":108,"depth":108,"links":2118},[2119,2120,2121,2122,2123,2124,2125,2126,2127,2128,2129,2130,2131],{"id":36,"depth":108,"text":37},{"id":81,"depth":108,"text":82},{"id":546,"depth":108,"text":547},{"id":682,"depth":108,"text":683},{"id":996,"depth":108,"text":997},{"id":1202,"depth":108,"text":1203},{"id":1701,"depth":108,"text":1702},{"id":1940,"depth":108,"text":1941},{"id":1959,"depth":108,"text":1960},{"id":1982,"depth":108,"text":1983},{"id":2036,"depth":108,"text":2037},{"id":2054,"depth":108,"text":2055},{"id":2082,"depth":108,"text":2083},"2026-10-02","Turn a long list of statuses, shift codes or comments into a grid — projects by week, employees by day — when the values are text, not numbers: pivot for unique combinations, the \"duplicate entries\" error and how to resolve it, pivot_table with first, last or joined values, picking the latest status by timestamp, and colouring the grid by value in Excel.","md",[2136,2138,2140,2142],{"q":2060,"a":2137},"Use df.pivot(index=..., columns=..., values=\"Status\") when each combination appears once. If combinations repeat, use pivot_table with a text aggregation such as aggfunc=\"last\" or aggfunc=\", \".join.",{"q":2066,"a":2139},"At least one combination of the index and columns appears in more than one row, so pivot cannot put a single value in that cell. Remove the duplicates, for example by keeping the latest row, or use pivot_table with an aggregation.",{"q":2072,"a":2141},"Sort the data by its timestamp, keep the last row per combination with drop_duplicates(keep=\"last\"), then pivot. This is independent of the original row order.",{"q":2078,"a":2143},"No; Excel's pivot table summarises values, so text appears only as a count. Build the grid in pandas and write it as values, or use lookup formulas against the source data.","pivot text values instead of numbers in pandas",null,{"breadcrumb":2147},[2148,2151,2154],{"name":2149,"item":2150},"Home","\u002F",{"name":2152,"item":2153},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":2155,"item":30},"Creating Pivot Tables from Excel Data with Pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fpivot-text-values-instead-of-numbers-in-pandas",{"title":2158,"description":2159},"Pivot Text Values (Not Numbers) with pandas pivot_table","Pivot non-numeric values in pandas: pivot for unique pairs, fixing \"Index contains duplicate entries, cannot reshape\", aggfunc first \u002F last \u002F \", \".join, the latest status per cell by timestamp, and an Excel grid coloured by text value.","pivot-text-values-instead-of-numbers-in-pandas","advanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fpivot-text-values-instead-of-numbers-in-pandas\u002Findex",[2163,2164],"pivots-and-summaries","pandas","how-to","s1lWhPDFA34MzUL0_FhxwTdAh7qLSApgZYwatQCqQj4",[2168,2172],{"title":2169,"path":2170,"stem":2171,"children":-1},"Number Rows Within Each Group in pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fnumber-rows-within-each-group-in-pandas","advanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fnumber-rows-within-each-group-in-pandas\u002Findex",{"title":2173,"path":2174,"stem":2175,"children":-1},"Run a Pareto Analysis on Excel Data with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Frun-a-pareto-analysis-on-excel-data-with-pandas","advanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Frun-a-pareto-analysis-on-excel-data-with-pandas\u002Findex",[2177,2179],{"path":2178,"title":2155},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data",{"path":2180,"title":2152},"\u002Fadvanced-data-transformation-and-cleaning",[],[2183,2186,2189,2192,2195,2198],{"path":2184,"title":2185,"kind":2165},"\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":2187,"title":2188,"kind":2165},"\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":2190,"title":2191,"kind":2165},"\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":2193,"title":2194,"kind":2165},"\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":2196,"title":2197,"kind":2165},"\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":2199,"title":2200,"kind":2165},"\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",1790972086567]