[{"data":1,"prerenderedAt":2663},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets":3,"surround:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets":2628,"ancestors:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets":2637,"children:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets":2642,"related:\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets":2643},{"id":4,"title":5,"body":6,"dateModified":2594,"datePublished":2594,"description":2595,"extension":2596,"faq":2597,"keyword":2606,"kind":2607,"meta":2608,"navigation":106,"path":2617,"seo":2618,"slug":2621,"stem":2622,"topics":2623,"type":2626,"__hash__":2627},"docs\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets\u002Findex.md","Build a Monthly KPI Scorecard with Targets",{"type":7,"value":8,"toc":2582},"minimark",[9,24,29,61,71,75,78,220,237,241,248,608,619,789,793,796,1051,1058,1136,1140,1143,1956,1967,2249,2253,2256,2464,2471,2475,2510,2514,2517,2521,2527,2533,2539,2545,2549,2578],[10,11,12,13,17,18,23],"p",{},"The monthly management pack opens with a scorecard: a dozen KPIs, this month's figure, the target, whether it is on track, and which way it is heading. The trap is that each KPI behaves differently. Revenue should be above target, but complaint rate and days sales outstanding should be ",[14,15,16],"em",{},"below"," it; some KPIs add up over the year, others are averages or ratios that must not be summed; and \"on track\" usually means \"within a tolerance\", not \"exactly on target\". Hand-maintained scorecards get these rules subtly wrong in one row and nobody notices. Putting the rules in a definitions table and computing status in pandas makes the scorecard consistent month after month. This page builds one, writes it as a single Excel sheet with coloured status and sparklines, and checks the colours match the rules. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002F","Building Finance and Operations Reports",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl xlsxwriter\n","bash","",[37,38,39],"code",{"__ignoreMap":35},[40,41,44,48,52,55,58],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sMTad","pip",[40,49,51],{"class":50},"srMev"," install",[40,53,54],{"class":50}," pandas",[40,56,57],{"class":50}," openpyxl",[40,59,60],{"class":50}," xlsxwriter\n",[10,62,63,66,67,70],{},[37,64,65],{},"kpi_actuals.xlsx"," is a long table with one row per month and KPI from April to September 2026. ",[37,68,69],{},"kpi_targets.xlsx"," defines each KPI: its monthly target, whether higher or lower is better, the tolerance (as a fraction of target) within which a miss is amber rather than red, and whether the year-to-date figure is a sum or an average.",[25,72,74],{"id":73},"step-1-keep-the-kpi-rules-as-data","Step 1 — Keep the KPI rules as data",[10,76,77],{},"The definitions table is the heart of the scorecard. Every rule that differs between KPIs lives there, not in code:",[30,79,83],{"className":80,"code":81,"language":82,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nactuals = pd.read_excel(\"kpi_actuals.xlsx\")\ndefs = pd.read_excel(\"kpi_targets.xlsx\").set_index(\"KPI\")\nprint(defs.to_string())\n\nmissing = set(actuals[\"KPI\"]) ^ set(defs.index)\nassert not missing, f\"KPIs without definitions or data: {missing}\"\n","python",[37,84,85,101,108,126,147,157,162,189],{"__ignoreMap":35},[40,86,87,91,95,98],{"class":42,"line":43},[40,88,90],{"class":89},"s-kum","import",[40,92,94],{"class":93},"skGVy"," pandas ",[40,96,97],{"class":89},"as",[40,99,100],{"class":93}," pd\n",[40,102,104],{"class":42,"line":103},2,[40,105,107],{"emptyLinePlaceholder":106},true,"\n",[40,109,111,114,117,120,123],{"class":42,"line":110},3,[40,112,113],{"class":93},"actuals ",[40,115,116],{"class":89},"=",[40,118,119],{"class":93}," pd.read_excel(",[40,121,122],{"class":50},"\"kpi_actuals.xlsx\"",[40,124,125],{"class":93},")\n",[40,127,129,132,134,136,139,142,145],{"class":42,"line":128},4,[40,130,131],{"class":93},"defs ",[40,133,116],{"class":89},[40,135,119],{"class":93},[40,137,138],{"class":50},"\"kpi_targets.xlsx\"",[40,140,141],{"class":93},").set_index(",[40,143,144],{"class":50},"\"KPI\"",[40,146,125],{"class":93},[40,148,150,154],{"class":42,"line":149},5,[40,151,153],{"class":152},"sP0c6","print",[40,155,156],{"class":93},"(defs.to_string())\n",[40,158,160],{"class":42,"line":159},6,[40,161,107],{"emptyLinePlaceholder":106},[40,163,165,168,170,173,176,178,181,184,186],{"class":42,"line":164},7,[40,166,167],{"class":93},"missing ",[40,169,116],{"class":89},[40,171,172],{"class":152}," set",[40,174,175],{"class":93},"(actuals[",[40,177,144],{"class":50},[40,179,180],{"class":93},"]) ",[40,182,183],{"class":89},"^",[40,185,172],{"class":152},[40,187,188],{"class":93},"(defs.index)\n",[40,190,192,195,198,201,204,207,211,214,217],{"class":42,"line":191},8,[40,193,194],{"class":89},"assert",[40,196,197],{"class":89}," not",[40,199,200],{"class":93}," missing, ",[40,202,203],{"class":89},"f",[40,205,206],{"class":50},"\"KPIs without definitions or data: ",[40,208,210],{"class":209},"sSjpA","{",[40,212,213],{"class":93},"missing",[40,215,216],{"class":209},"}",[40,218,219],{"class":50},"\"\n",[10,221,222,225,226,229,230,233,234,236],{},[37,223,224],{},"Better"," says which direction is good, ",[37,227,228],{},"Tolerance"," how far from target still counts as amber, and ",[37,231,232],{},"YTD"," how to aggregate across months. The symmetric difference check (",[37,235,183],{},") fails if a KPI appears in the data without a definition or the other way round — adding a KPI then requires deciding its rules, which is exactly the conversation that should happen. Owners can maintain this table in Excel themselves.",[25,238,240],{"id":239},"step-2-red-amber-or-green","Step 2 — Red, amber or green",[10,242,243,244,247],{},"Status depends on direction. Express each actual as a ",[14,245,246],{},"performance"," relative to target where positive is always good, then compare with the tolerance:",[30,249,251],{"className":80,"code":250,"language":82,"meta":35,"style":35},"month = \"2026-09\"\nprev = \"2026-08\"\nwide = actuals.pivot(index=\"KPI\", columns=\"Month\", values=\"Actual\")\n\ndef status(actual, target, better, tol):\n    perf = (actual - target) \u002F target if better == \"higher\" else (target - actual) \u002F target\n    perf = round(perf, 9)                 # 2.0 vs 2.2 must land exactly on a 10% tolerance\n    if perf >= 0:\n        return \"Green\", perf\n    return (\"Amber\" if perf >= -tol else \"Red\"), perf\n\ncard = defs.copy()\ncard[\"Actual\"] = wide[month]\ncard[\"Last month\"] = wide[prev]\ncard[[\"Status\", \"Perf\"]] = [status(a, t, b, tol) for a, t, b, tol in\n                            zip(card[\"Actual\"], card[\"Target\"], card[\"Better\"], card[\"Tolerance\"])]\nprint(card[[\"Actual\", \"Target\", \"Better\", \"Perf\", \"Status\"]].round(3).to_string())\n",[37,252,253,263,273,314,318,330,380,402,419,431,465,470,481,497,512,543,573],{"__ignoreMap":35},[40,254,255,258,260],{"class":42,"line":43},[40,256,257],{"class":93},"month ",[40,259,116],{"class":89},[40,261,262],{"class":50}," \"2026-09\"\n",[40,264,265,268,270],{"class":42,"line":103},[40,266,267],{"class":93},"prev ",[40,269,116],{"class":89},[40,271,272],{"class":50}," \"2026-08\"\n",[40,274,275,278,280,283,287,289,291,294,297,299,302,304,307,309,312],{"class":42,"line":110},[40,276,277],{"class":93},"wide ",[40,279,116],{"class":89},[40,281,282],{"class":93}," actuals.pivot(",[40,284,286],{"class":285},"sa561","index",[40,288,116],{"class":89},[40,290,144],{"class":50},[40,292,293],{"class":93},", ",[40,295,296],{"class":285},"columns",[40,298,116],{"class":89},[40,300,301],{"class":50},"\"Month\"",[40,303,293],{"class":93},[40,305,306],{"class":285},"values",[40,308,116],{"class":89},[40,310,311],{"class":50},"\"Actual\"",[40,313,125],{"class":93},[40,315,316],{"class":42,"line":128},[40,317,107],{"emptyLinePlaceholder":106},[40,319,320,323,327],{"class":42,"line":149},[40,321,322],{"class":89},"def",[40,324,326],{"class":325},"s_Opv"," status",[40,328,329],{"class":93},"(actual, target, better, tol):\n",[40,331,332,335,337,340,343,346,349,352,355,358,361,364,367,370,372,375,377],{"class":42,"line":159},[40,333,334],{"class":93},"    perf ",[40,336,116],{"class":89},[40,338,339],{"class":93}," (actual ",[40,341,342],{"class":89},"-",[40,344,345],{"class":93}," target) ",[40,347,348],{"class":89},"\u002F",[40,350,351],{"class":93}," target ",[40,353,354],{"class":89},"if",[40,356,357],{"class":93}," better ",[40,359,360],{"class":89},"==",[40,362,363],{"class":50}," \"higher\"",[40,365,366],{"class":89}," else",[40,368,369],{"class":93}," (target ",[40,371,342],{"class":89},[40,373,374],{"class":93}," actual) ",[40,376,348],{"class":89},[40,378,379],{"class":93}," target\n",[40,381,382,384,386,389,392,395,398],{"class":42,"line":164},[40,383,334],{"class":93},[40,385,116],{"class":89},[40,387,388],{"class":152}," round",[40,390,391],{"class":93},"(perf, ",[40,393,394],{"class":152},"9",[40,396,397],{"class":93},")                 ",[40,399,401],{"class":400},"s-wDw","# 2.0 vs 2.2 must land exactly on a 10% tolerance\n",[40,403,404,407,410,413,416],{"class":42,"line":191},[40,405,406],{"class":89},"    if",[40,408,409],{"class":93}," perf ",[40,411,412],{"class":89},">=",[40,414,415],{"class":152}," 0",[40,417,418],{"class":93},":\n",[40,420,422,425,428],{"class":42,"line":421},9,[40,423,424],{"class":89},"        return",[40,426,427],{"class":50}," \"Green\"",[40,429,430],{"class":93},", perf\n",[40,432,434,437,440,443,446,448,450,453,456,459,462],{"class":42,"line":433},10,[40,435,436],{"class":89},"    return",[40,438,439],{"class":93}," (",[40,441,442],{"class":50},"\"Amber\"",[40,444,445],{"class":89}," if",[40,447,409],{"class":93},[40,449,412],{"class":89},[40,451,452],{"class":89}," -",[40,454,455],{"class":93},"tol ",[40,457,458],{"class":89},"else",[40,460,461],{"class":50}," \"Red\"",[40,463,464],{"class":93},"), perf\n",[40,466,468],{"class":42,"line":467},11,[40,469,107],{"emptyLinePlaceholder":106},[40,471,473,476,478],{"class":42,"line":472},12,[40,474,475],{"class":93},"card ",[40,477,116],{"class":89},[40,479,480],{"class":93}," defs.copy()\n",[40,482,484,487,489,492,494],{"class":42,"line":483},13,[40,485,486],{"class":93},"card[",[40,488,311],{"class":50},[40,490,491],{"class":93},"] ",[40,493,116],{"class":89},[40,495,496],{"class":93}," wide[month]\n",[40,498,500,502,505,507,509],{"class":42,"line":499},14,[40,501,486],{"class":93},[40,503,504],{"class":50},"\"Last month\"",[40,506,491],{"class":93},[40,508,116],{"class":89},[40,510,511],{"class":93}," wide[prev]\n",[40,513,515,518,521,523,526,529,531,534,537,540],{"class":42,"line":514},15,[40,516,517],{"class":93},"card[[",[40,519,520],{"class":50},"\"Status\"",[40,522,293],{"class":93},[40,524,525],{"class":50},"\"Perf\"",[40,527,528],{"class":93},"]] ",[40,530,116],{"class":89},[40,532,533],{"class":93}," [status(a, t, b, tol) ",[40,535,536],{"class":89},"for",[40,538,539],{"class":93}," a, t, b, tol ",[40,541,542],{"class":89},"in\n",[40,544,546,549,552,554,557,560,562,565,567,570],{"class":42,"line":545},16,[40,547,548],{"class":152},"                            zip",[40,550,551],{"class":93},"(card[",[40,553,311],{"class":50},[40,555,556],{"class":93},"], card[",[40,558,559],{"class":50},"\"Target\"",[40,561,556],{"class":93},[40,563,564],{"class":50},"\"Better\"",[40,566,556],{"class":93},[40,568,569],{"class":50},"\"Tolerance\"",[40,571,572],{"class":93},"])]\n",[40,574,576,578,581,583,585,587,589,591,593,595,597,599,602,605],{"class":42,"line":575},17,[40,577,153],{"class":152},[40,579,580],{"class":93},"(card[[",[40,582,311],{"class":50},[40,584,293],{"class":93},[40,586,559],{"class":50},[40,588,293],{"class":93},[40,590,564],{"class":50},[40,592,293],{"class":93},[40,594,525],{"class":50},[40,596,293],{"class":93},[40,598,520],{"class":50},[40,600,601],{"class":93},"]].round(",[40,603,604],{"class":152},"3",[40,606,607],{"class":93},").to_string())\n",[10,609,610,611,614,615,618],{},"For a higher-is-better KPI, performance is (actual − target) \u002F target; for lower-is-better it is flipped, so beating a complaint-rate target of 2.0% with 1.8% would be positive. September's revenue of 468 against 440 is green; gross margin at 37.2% against 38.0% is 2.1% short with a 2% tolerance, so red; complaint rate at 2.2% against a ceiling of 2.0% is 10% worse, exactly at the tolerance, so amber; days sales outstanding at 47 against 40 is far out — red. The ",[37,612,613],{},"round"," matters more than it looks. Without it, the complaint rate's performance comes out as −0.10000000000000009 in floating point — fractionally ",[14,616,617],{},"beyond"," the −0.10 tolerance — and the KPI turns red although it sits exactly on the amber boundary. Rounding to a sensible precision before comparing makes boundary cases behave as the definitions say. Defining tolerance as a fraction of the target keeps it meaningful across KPIs measured in thousands of euros and in percentage points alike.",[620,621,629,630,629,634,629,638,629,646,629,656,629,667,629,673,629,677,629,681,629,684,629,689,629,698,629,703,629,711,629,718,629,723,629,728,629,730,629,733,629,737,629,740,629,745,629,752,629,756,629,759,629,761,629,764,629,767,629,770,629,775,629,782,629,786],"svg",{"viewBox":622,"role":623,"ariaLabelledBy":624,"xmlns":627,"style":628},"4 0 752 272","img",[625,626],"kpi-rag-t","kpi-rag-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  ",[631,632,633],"title",{"id":625},"How a KPI gets its colour",[635,636,637],"desc",{"id":626},"The actual is turned into a performance figure relative to target, flipped for lower-is-better KPIs so positive is always good. At or above zero is green. Below zero but within the tolerance is amber. Further below is red.",[639,640],"rect",{"x":641,"y":642,"width":643,"height":644,"fill":645},"4","0","752","272","#ffffff",[639,647],{"x":648,"y":649,"width":650,"height":651,"rx":652,"fill":653,"stroke":654,"style":655},"211.9","16.0","336.3","75.5","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[657,658,660],"g",{"transform":659},"translate(368.0,29.0) scale(1.000)",[661,662],"path",{"d":663,"fill":664,"stroke":665,"style":666},"M5 9h14 M4 15h14 M10 4l-2 16 M16 4l-2 16","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[668,669,672],"text",{"x":670,"y":651,"style":671},"380.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Performance vs target (positive = good)",[42,674],{"x1":670,"y1":675,"x2":670,"y2":676,"stroke":654,"style":655},"91.5","111.5",[42,678],{"x1":679,"y1":676,"x2":680,"y2":676,"stroke":654,"style":655},"134.7","625.3",[42,682],{"x1":679,"y1":676,"x2":679,"y2":683,"stroke":654,"style":655},"157.5",[685,686],"polygon",{"points":687,"fill":688},"134.7,161.5 129.7,152.5 139.7,152.5","#5b5cf0",[639,690],{"x":691,"y":692,"width":693,"height":694,"rx":695,"fill":645,"stroke":696,"style":697},"116.1","121.5","37.2","21.0","10","var(--line,#cdd5e6)","stroke-width:1.2px",[668,699,702],{"x":679,"y":700,"style":701},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","≥ 0",[639,704],{"x":705,"y":706,"width":707,"height":708,"rx":652,"fill":709,"stroke":710,"style":655},"20.0","163.5","229.3","93.0","#d9f4f1","var(--teal,#0f9488)",[657,712,714],{"transform":713},"translate(122.7,176.5) scale(1.000)",[661,715],{"d":716,"fill":664,"stroke":717,"style":666},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[668,719,722],{"x":679,"y":720,"style":721},"223.0","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Green",[668,724,727],{"x":679,"y":725,"style":726},"242.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","on or better than target",[42,729],{"x1":670,"y1":676,"x2":670,"y2":683,"stroke":654,"style":655},[685,731],{"points":732,"fill":688},"380.0,161.5 375.0,152.5 385.0,152.5",[639,734],{"x":735,"y":692,"width":736,"height":694,"rx":695,"fill":645,"stroke":696,"style":697},"323.4","113.2",[668,738,739],{"x":670,"y":700,"style":701},"within tolerance",[639,741],{"x":742,"y":706,"width":707,"height":708,"rx":652,"fill":743,"stroke":744,"style":655},"265.3","#fdefd8","var(--gold,#b4740a)",[657,746,748],{"transform":747},"translate(368.0,176.5) scale(1.000)",[661,749],{"d":750,"fill":664,"stroke":751,"style":666},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--gold-ink,#7a4e06)",[668,753,755],{"x":670,"y":720,"style":754},"font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Amber",[668,757,758],{"x":670,"y":725,"style":726},"e.g. complaint rate 2.2 vs 2.0",[42,760],{"x1":680,"y1":676,"x2":680,"y2":683,"stroke":654,"style":655},[685,762],{"points":763,"fill":688},"625.3,161.5 620.3,152.5 630.3,152.5",[639,765],{"x":766,"y":692,"width":692,"height":694,"rx":695,"fill":645,"stroke":696,"style":697},"564.6",[668,768,769],{"x":680,"y":700,"style":701},"beyond tolerance",[639,771],{"x":772,"y":706,"width":707,"height":708,"rx":652,"fill":773,"stroke":774,"style":655},"510.7","#fce9e9","var(--danger,#dc2626)",[657,776,778],{"transform":777},"translate(613.3,176.5) scale(1.000)",[661,779],{"d":780,"fill":664,"stroke":781,"style":666},"M6 6l12 12 M18 6L6 18","var(--accent-ink,#be185d)",[668,783,785],{"x":680,"y":720,"style":784},"font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Red",[668,787,788],{"x":680,"y":725,"style":726},"e.g. DSO 47 vs 40",[25,790,792],{"id":791},"step-3-variance-trend-and-year-to-date","Step 3 — Variance, trend and year to date",[10,794,795],{},"The scorecard also shows the variance against target and last month, and a year-to-date figure that respects how each KPI aggregates:",[30,797,799],{"className":80,"code":798,"language":82,"meta":35,"style":35},"card[\"vs target\"] = card[\"Actual\"] - card[\"Target\"]\ncard[\"vs last month\"] = card[\"Actual\"] - card[\"Last month\"]\n\nytd_months = [m for m in wide.columns if m \u003C= month]\ndef ytd(kpi):\n    values = wide.loc[kpi, ytd_months]\n    return values.sum() if defs.loc[kpi, \"YTD\"] == \"sum\" else values.mean()\n\ncard[\"YTD\"] = [ytd(k) for k in card.index]\ncard[\"YTD target\"] = [defs.loc[k, \"Target\"] * (len(ytd_months) if defs.loc[k, \"YTD\"] == \"sum\" else 1) for k in card.index]\nprint(card[[\"Actual\", \"vs target\", \"vs last month\", \"YTD\", \"YTD target\"]].round(1).to_string())\n",[37,800,801,828,853,857,888,898,908,935,939,962,1020],{"__ignoreMap":35},[40,802,803,805,808,810,812,815,817,819,821,823,825],{"class":42,"line":43},[40,804,486],{"class":93},[40,806,807],{"class":50},"\"vs target\"",[40,809,491],{"class":93},[40,811,116],{"class":89},[40,813,814],{"class":93}," card[",[40,816,311],{"class":50},[40,818,491],{"class":93},[40,820,342],{"class":89},[40,822,814],{"class":93},[40,824,559],{"class":50},[40,826,827],{"class":93},"]\n",[40,829,830,832,835,837,839,841,843,845,847,849,851],{"class":42,"line":103},[40,831,486],{"class":93},[40,833,834],{"class":50},"\"vs last month\"",[40,836,491],{"class":93},[40,838,116],{"class":89},[40,840,814],{"class":93},[40,842,311],{"class":50},[40,844,491],{"class":93},[40,846,342],{"class":89},[40,848,814],{"class":93},[40,850,504],{"class":50},[40,852,827],{"class":93},[40,854,855],{"class":42,"line":110},[40,856,107],{"emptyLinePlaceholder":106},[40,858,859,862,864,867,869,872,875,878,880,882,885],{"class":42,"line":128},[40,860,861],{"class":93},"ytd_months ",[40,863,116],{"class":89},[40,865,866],{"class":93}," [m ",[40,868,536],{"class":89},[40,870,871],{"class":93}," m ",[40,873,874],{"class":89},"in",[40,876,877],{"class":93}," wide.columns ",[40,879,354],{"class":89},[40,881,871],{"class":93},[40,883,884],{"class":89},"\u003C=",[40,886,887],{"class":93}," month]\n",[40,889,890,892,895],{"class":42,"line":149},[40,891,322],{"class":89},[40,893,894],{"class":325}," ytd",[40,896,897],{"class":93},"(kpi):\n",[40,899,900,903,905],{"class":42,"line":159},[40,901,902],{"class":93},"    values ",[40,904,116],{"class":89},[40,906,907],{"class":93}," wide.loc[kpi, ytd_months]\n",[40,909,910,912,915,917,920,923,925,927,930,932],{"class":42,"line":164},[40,911,436],{"class":89},[40,913,914],{"class":93}," values.sum() ",[40,916,354],{"class":89},[40,918,919],{"class":93}," defs.loc[kpi, ",[40,921,922],{"class":50},"\"YTD\"",[40,924,491],{"class":93},[40,926,360],{"class":89},[40,928,929],{"class":50}," \"sum\"",[40,931,366],{"class":89},[40,933,934],{"class":93}," values.mean()\n",[40,936,937],{"class":42,"line":191},[40,938,107],{"emptyLinePlaceholder":106},[40,940,941,943,945,947,949,952,954,957,959],{"class":42,"line":421},[40,942,486],{"class":93},[40,944,922],{"class":50},[40,946,491],{"class":93},[40,948,116],{"class":89},[40,950,951],{"class":93}," [ytd(k) ",[40,953,536],{"class":89},[40,955,956],{"class":93}," k ",[40,958,874],{"class":89},[40,960,961],{"class":93}," card.index]\n",[40,963,964,966,969,971,973,976,978,980,983,985,988,991,993,996,998,1000,1002,1004,1006,1009,1012,1014,1016,1018],{"class":42,"line":433},[40,965,486],{"class":93},[40,967,968],{"class":50},"\"YTD target\"",[40,970,491],{"class":93},[40,972,116],{"class":89},[40,974,975],{"class":93}," [defs.loc[k, ",[40,977,559],{"class":50},[40,979,491],{"class":93},[40,981,982],{"class":89},"*",[40,984,439],{"class":93},[40,986,987],{"class":152},"len",[40,989,990],{"class":93},"(ytd_months) ",[40,992,354],{"class":89},[40,994,995],{"class":93}," defs.loc[k, ",[40,997,922],{"class":50},[40,999,491],{"class":93},[40,1001,360],{"class":89},[40,1003,929],{"class":50},[40,1005,366],{"class":89},[40,1007,1008],{"class":152}," 1",[40,1010,1011],{"class":93},") ",[40,1013,536],{"class":89},[40,1015,956],{"class":93},[40,1017,874],{"class":89},[40,1019,961],{"class":93},[40,1021,1022,1024,1026,1028,1030,1032,1034,1036,1038,1040,1042,1044,1046,1049],{"class":42,"line":467},[40,1023,153],{"class":152},[40,1025,580],{"class":93},[40,1027,311],{"class":50},[40,1029,293],{"class":93},[40,1031,807],{"class":50},[40,1033,293],{"class":93},[40,1035,834],{"class":50},[40,1037,293],{"class":93},[40,1039,922],{"class":50},[40,1041,293],{"class":93},[40,1043,968],{"class":50},[40,1045,601],{"class":93},[40,1047,1048],{"class":152},"1",[40,1050,607],{"class":93},[10,1052,1053,1054,1057],{},"Revenue and new customers add up — six months of revenue total 2,556k against a year-to-date target of 2,640k. Percentages and days outstanding must ",[14,1055,1056],{},"not"," be summed; their year-to-date figure is the average of the months (a weighted average is more accurate when volumes differ — weight margin by revenue, for example — and is worth doing if the definitions table carries the weight column). The month-on-month change gives direction: days sales outstanding worsened by 3 days, on-time delivery improved by 3.2 points.",[620,1059,629,1064,629,1067,629,1070,629,1073,629,1078,629,1084,629,1090,629,1096,629,1101,629,1105,629,1109,629,1112,629,1118,629,1123,629,1127,629,1130,629,1133],{"viewBox":1060,"role":623,"ariaLabelledBy":1061,"xmlns":627,"style":628},"4 0 752 207",[1062,1063],"kpi-ytd-t","kpi-ytd-d",[631,1065,1066],{"id":1062},"Year to date — sum or average?",[635,1068,1069],{"id":1063},"Volume KPIs such as revenue and new customers add up across months, and their year-to-date target is the monthly target times the number of months. Rate KPIs such as margin, complaint rate, days outstanding and on-time delivery are averaged, ideally weighted by volume, against the same monthly target.",[639,1071],{"x":641,"y":642,"width":643,"height":1072,"fill":645},"207",[639,1074],{"x":705,"y":649,"width":1075,"height":1076,"rx":1077,"fill":653,"stroke":654,"style":655},"351.0","175.0","14",[657,1079,1081],{"transform":1080},"translate(182.5,29.0) scale(1.083)",[661,1082],{"d":663,"fill":664,"stroke":665,"style":1083},"stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[668,1085,1089],{"x":1086,"y":1087,"style":1088},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","YTD: sum",[42,1091],{"x1":1092,"y1":1093,"x2":1094,"y2":1093,"stroke":654,"style":1095},"36.0","91.0","355.0","stroke-width:1px",[668,1097,1100],{"x":1086,"y":1098,"style":1099},"111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Revenue (k€): 2,556 vs 2,640",[668,1102,1104],{"x":1086,"y":1103,"style":1099},"137.0","New customers: 255 vs 270",[668,1106,1108],{"x":1086,"y":1107,"style":1099},"163.0","target × months",[639,1110],{"x":1111,"y":649,"width":1075,"height":1076,"rx":1077,"fill":709,"stroke":710,"style":655},"389.0",[657,1113,1115],{"transform":1114},"translate(551.5,29.0) scale(1.083)",[661,1116],{"d":1117,"fill":664,"stroke":717,"style":1083},"M4 20h16 M4 20V4 M6 16l4-5 4 3 6-7",[668,1119,1122],{"x":1120,"y":1087,"style":1121},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","YTD: average",[42,1124],{"x1":1125,"y1":1093,"x2":1126,"y2":1093,"stroke":710,"style":1095},"405.0","724.0",[668,1128,1129],{"x":1120,"y":1098,"style":1099},"Gross margin %: 37.9 vs 38",[668,1131,1132],{"x":1120,"y":1103,"style":1099},"Days sales outstanding: 42.3 vs 40",[668,1134,1135],{"x":1120,"y":1107,"style":1099},"weight by volume if you can",[25,1137,1139],{"id":1138},"step-4-write-a-one-page-scorecard","Step 4 — Write a one-page scorecard",[10,1141,1142],{},"XlsxWriter writes the sheet with a status cell coloured by conditional formatting, number formats per KPI and a sparkline of the last six months on each row:",[30,1144,1146],{"className":80,"code":1145,"language":82,"meta":35,"style":35},"import xlsxwriter\n\nbook = xlsxwriter.Workbook(\"kpi_scorecard_2026-09.xlsx\")\nws = book.add_worksheet(\"Scorecard\")\ndata = book.add_worksheet(\"Trend\")\nbold = book.add_format({\"bold\": True, \"bottom\": 1, \"bg_color\": \"#EEF2FF\"})\ntitle = book.add_format({\"bold\": True, \"font_size\": 16, \"font_color\": \"#4338CA\"})\nnum = book.add_format({\"num_format\": \"#,##0.0\"})\nsigned = book.add_format({\"num_format\": \"+#,##0.0;[Red]-#,##0.0;0.0\"})\nfills = {\"Green\": \"#C6EFCE\", \"Amber\": \"#FFEB9C\", \"Red\": \"#FFC7CE\"}\n\nws.write(\"A1\", \"KPI scorecard — September 2026\", title)\nheaders = [\"KPI\", \"Actual\", \"Target\", \"Status\", \"vs target\", \"vs last month\", \"YTD\", \"YTD target\", \"Trend (6 months)\"]\nws.write_row(2, 0, headers, bold)\ndata.write_row(0, 0, [\"KPI\"] + list(wide.columns))\nfor r, (kpi, row) in enumerate(card.iterrows(), start=3):\n    ws.write(r, 0, kpi)\n    for c, col in enumerate([\"Actual\", \"Target\"], start=1):\n        ws.write_number(r, c, row[col], num)\n    ws.write(r, 3, row[\"Status\"])\n    ws.write_number(r, 4, row[\"vs target\"], signed)\n    ws.write_number(r, 5, row[\"vs last month\"], signed)\n    ws.write_number(r, 6, row[\"YTD\"], num)\n    ws.write_number(r, 7, row[\"YTD target\"], num)\n    data.write(r - 2, 0, kpi)\n    data.write_row(r - 2, 1, list(wide.loc[kpi]))\n    ws.add_sparkline(r, 8, {\"range\": f\"Trend!B{r - 1}:G{r - 1}\", \"markers\": True, \"last_point\": True,\n                            \"series_color\": \"#4338CA\", \"last_color\": \"#B91C1C\"})\nlast = 3 + len(card) - 1\nfor status_name, colour in fills.items():\n    ws.conditional_format(3, 3, last, 3, {\"type\": \"cell\", \"criteria\": \"==\", \"value\": f'\"{status_name}\"',\n                                          \"format\": book.add_format({\"bg_color\": colour, \"bold\": True})})\nws.set_column(\"A:A\", 24); ws.set_column(\"B:H\", 12); ws.set_column(\"I:I\", 20)\nws.set_landscape(); ws.fit_to_pages(1, 1); ws.hide_gridlines(2)\nbook.close()\nprint(\"scorecard rows:\", len(card), \"| status counts:\", card[\"Status\"].value_counts().to_dict())\n",[37,1147,1148,1154,1158,1173,1188,1202,1243,1280,1299,1317,1357,1361,1377,1424,1439,1466,1491,1501,1534,1540,1555,1570,1584,1599,1613,1630,1652,1722,1744,1769,1782,1843,1866,1902,1921,1927],{"__ignoreMap":35},[40,1149,1150,1152],{"class":42,"line":43},[40,1151,90],{"class":89},[40,1153,60],{"class":93},[40,1155,1156],{"class":42,"line":103},[40,1157,107],{"emptyLinePlaceholder":106},[40,1159,1160,1163,1165,1168,1171],{"class":42,"line":110},[40,1161,1162],{"class":93},"book ",[40,1164,116],{"class":89},[40,1166,1167],{"class":93}," xlsxwriter.Workbook(",[40,1169,1170],{"class":50},"\"kpi_scorecard_2026-09.xlsx\"",[40,1172,125],{"class":93},[40,1174,1175,1178,1180,1183,1186],{"class":42,"line":128},[40,1176,1177],{"class":93},"ws ",[40,1179,116],{"class":89},[40,1181,1182],{"class":93}," book.add_worksheet(",[40,1184,1185],{"class":50},"\"Scorecard\"",[40,1187,125],{"class":93},[40,1189,1190,1193,1195,1197,1200],{"class":42,"line":149},[40,1191,1192],{"class":93},"data ",[40,1194,116],{"class":89},[40,1196,1182],{"class":93},[40,1198,1199],{"class":50},"\"Trend\"",[40,1201,125],{"class":93},[40,1203,1204,1207,1209,1212,1215,1218,1221,1223,1226,1228,1230,1232,1235,1237,1240],{"class":42,"line":159},[40,1205,1206],{"class":93},"bold ",[40,1208,116],{"class":89},[40,1210,1211],{"class":93}," book.add_format({",[40,1213,1214],{"class":50},"\"bold\"",[40,1216,1217],{"class":93},": ",[40,1219,1220],{"class":152},"True",[40,1222,293],{"class":93},[40,1224,1225],{"class":50},"\"bottom\"",[40,1227,1217],{"class":93},[40,1229,1048],{"class":152},[40,1231,293],{"class":93},[40,1233,1234],{"class":50},"\"bg_color\"",[40,1236,1217],{"class":93},[40,1238,1239],{"class":50},"\"#EEF2FF\"",[40,1241,1242],{"class":93},"})\n",[40,1244,1245,1248,1250,1252,1254,1256,1258,1260,1263,1265,1268,1270,1273,1275,1278],{"class":42,"line":164},[40,1246,1247],{"class":93},"title ",[40,1249,116],{"class":89},[40,1251,1211],{"class":93},[40,1253,1214],{"class":50},[40,1255,1217],{"class":93},[40,1257,1220],{"class":152},[40,1259,293],{"class":93},[40,1261,1262],{"class":50},"\"font_size\"",[40,1264,1217],{"class":93},[40,1266,1267],{"class":152},"16",[40,1269,293],{"class":93},[40,1271,1272],{"class":50},"\"font_color\"",[40,1274,1217],{"class":93},[40,1276,1277],{"class":50},"\"#4338CA\"",[40,1279,1242],{"class":93},[40,1281,1282,1285,1287,1289,1292,1294,1297],{"class":42,"line":191},[40,1283,1284],{"class":93},"num ",[40,1286,116],{"class":89},[40,1288,1211],{"class":93},[40,1290,1291],{"class":50},"\"num_format\"",[40,1293,1217],{"class":93},[40,1295,1296],{"class":50},"\"#,##0.0\"",[40,1298,1242],{"class":93},[40,1300,1301,1304,1306,1308,1310,1312,1315],{"class":42,"line":421},[40,1302,1303],{"class":93},"signed ",[40,1305,116],{"class":89},[40,1307,1211],{"class":93},[40,1309,1291],{"class":50},[40,1311,1217],{"class":93},[40,1313,1314],{"class":50},"\"+#,##0.0;[Red]-#,##0.0;0.0\"",[40,1316,1242],{"class":93},[40,1318,1319,1322,1324,1327,1330,1332,1335,1337,1339,1341,1344,1346,1349,1351,1354],{"class":42,"line":433},[40,1320,1321],{"class":93},"fills ",[40,1323,116],{"class":89},[40,1325,1326],{"class":93}," {",[40,1328,1329],{"class":50},"\"Green\"",[40,1331,1217],{"class":93},[40,1333,1334],{"class":50},"\"#C6EFCE\"",[40,1336,293],{"class":93},[40,1338,442],{"class":50},[40,1340,1217],{"class":93},[40,1342,1343],{"class":50},"\"#FFEB9C\"",[40,1345,293],{"class":93},[40,1347,1348],{"class":50},"\"Red\"",[40,1350,1217],{"class":93},[40,1352,1353],{"class":50},"\"#FFC7CE\"",[40,1355,1356],{"class":93},"}\n",[40,1358,1359],{"class":42,"line":467},[40,1360,107],{"emptyLinePlaceholder":106},[40,1362,1363,1366,1369,1371,1374],{"class":42,"line":472},[40,1364,1365],{"class":93},"ws.write(",[40,1367,1368],{"class":50},"\"A1\"",[40,1370,293],{"class":93},[40,1372,1373],{"class":50},"\"KPI scorecard — September 2026\"",[40,1375,1376],{"class":93},", title)\n",[40,1378,1379,1382,1384,1387,1389,1391,1393,1395,1397,1399,1401,1403,1405,1407,1409,1411,1413,1415,1417,1419,1422],{"class":42,"line":483},[40,1380,1381],{"class":93},"headers ",[40,1383,116],{"class":89},[40,1385,1386],{"class":93}," [",[40,1388,144],{"class":50},[40,1390,293],{"class":93},[40,1392,311],{"class":50},[40,1394,293],{"class":93},[40,1396,559],{"class":50},[40,1398,293],{"class":93},[40,1400,520],{"class":50},[40,1402,293],{"class":93},[40,1404,807],{"class":50},[40,1406,293],{"class":93},[40,1408,834],{"class":50},[40,1410,293],{"class":93},[40,1412,922],{"class":50},[40,1414,293],{"class":93},[40,1416,968],{"class":50},[40,1418,293],{"class":93},[40,1420,1421],{"class":50},"\"Trend (6 months)\"",[40,1423,827],{"class":93},[40,1425,1426,1429,1432,1434,1436],{"class":42,"line":499},[40,1427,1428],{"class":93},"ws.write_row(",[40,1430,1431],{"class":152},"2",[40,1433,293],{"class":93},[40,1435,642],{"class":152},[40,1437,1438],{"class":93},", headers, bold)\n",[40,1440,1441,1444,1446,1448,1450,1453,1455,1457,1460,1463],{"class":42,"line":514},[40,1442,1443],{"class":93},"data.write_row(",[40,1445,642],{"class":152},[40,1447,293],{"class":93},[40,1449,642],{"class":152},[40,1451,1452],{"class":93},", [",[40,1454,144],{"class":50},[40,1456,491],{"class":93},[40,1458,1459],{"class":89},"+",[40,1461,1462],{"class":152}," list",[40,1464,1465],{"class":93},"(wide.columns))\n",[40,1467,1468,1470,1473,1475,1478,1481,1484,1486,1488],{"class":42,"line":545},[40,1469,536],{"class":89},[40,1471,1472],{"class":93}," r, (kpi, row) ",[40,1474,874],{"class":89},[40,1476,1477],{"class":152}," enumerate",[40,1479,1480],{"class":93},"(card.iterrows(), ",[40,1482,1483],{"class":285},"start",[40,1485,116],{"class":89},[40,1487,604],{"class":152},[40,1489,1490],{"class":93},"):\n",[40,1492,1493,1496,1498],{"class":42,"line":575},[40,1494,1495],{"class":93},"    ws.write(r, ",[40,1497,642],{"class":152},[40,1499,1500],{"class":93},", kpi)\n",[40,1502,1504,1507,1510,1512,1514,1517,1519,1521,1523,1526,1528,1530,1532],{"class":42,"line":1503},18,[40,1505,1506],{"class":89},"    for",[40,1508,1509],{"class":93}," c, col ",[40,1511,874],{"class":89},[40,1513,1477],{"class":152},[40,1515,1516],{"class":93},"([",[40,1518,311],{"class":50},[40,1520,293],{"class":93},[40,1522,559],{"class":50},[40,1524,1525],{"class":93},"], ",[40,1527,1483],{"class":285},[40,1529,116],{"class":89},[40,1531,1048],{"class":152},[40,1533,1490],{"class":93},[40,1535,1537],{"class":42,"line":1536},19,[40,1538,1539],{"class":93},"        ws.write_number(r, c, row[col], num)\n",[40,1541,1543,1545,1547,1550,1552],{"class":42,"line":1542},20,[40,1544,1495],{"class":93},[40,1546,604],{"class":152},[40,1548,1549],{"class":93},", row[",[40,1551,520],{"class":50},[40,1553,1554],{"class":93},"])\n",[40,1556,1558,1561,1563,1565,1567],{"class":42,"line":1557},21,[40,1559,1560],{"class":93},"    ws.write_number(r, ",[40,1562,641],{"class":152},[40,1564,1549],{"class":93},[40,1566,807],{"class":50},[40,1568,1569],{"class":93},"], signed)\n",[40,1571,1573,1575,1578,1580,1582],{"class":42,"line":1572},22,[40,1574,1560],{"class":93},[40,1576,1577],{"class":152},"5",[40,1579,1549],{"class":93},[40,1581,834],{"class":50},[40,1583,1569],{"class":93},[40,1585,1587,1589,1592,1594,1596],{"class":42,"line":1586},23,[40,1588,1560],{"class":93},[40,1590,1591],{"class":152},"6",[40,1593,1549],{"class":93},[40,1595,922],{"class":50},[40,1597,1598],{"class":93},"], num)\n",[40,1600,1602,1604,1607,1609,1611],{"class":42,"line":1601},24,[40,1603,1560],{"class":93},[40,1605,1606],{"class":152},"7",[40,1608,1549],{"class":93},[40,1610,968],{"class":50},[40,1612,1598],{"class":93},[40,1614,1616,1619,1621,1624,1626,1628],{"class":42,"line":1615},25,[40,1617,1618],{"class":93},"    data.write(r ",[40,1620,342],{"class":89},[40,1622,1623],{"class":152}," 2",[40,1625,293],{"class":93},[40,1627,642],{"class":152},[40,1629,1500],{"class":93},[40,1631,1633,1636,1638,1640,1642,1644,1646,1649],{"class":42,"line":1632},26,[40,1634,1635],{"class":93},"    data.write_row(r ",[40,1637,342],{"class":89},[40,1639,1623],{"class":152},[40,1641,293],{"class":93},[40,1643,1048],{"class":152},[40,1645,293],{"class":93},[40,1647,1648],{"class":152},"list",[40,1650,1651],{"class":93},"(wide.loc[kpi]))\n",[40,1653,1655,1658,1661,1664,1667,1669,1671,1674,1676,1679,1681,1683,1685,1688,1690,1692,1694,1696,1698,1701,1703,1706,1708,1710,1712,1715,1717,1719],{"class":42,"line":1654},27,[40,1656,1657],{"class":93},"    ws.add_sparkline(r, ",[40,1659,1660],{"class":152},"8",[40,1662,1663],{"class":93},", {",[40,1665,1666],{"class":50},"\"range\"",[40,1668,1217],{"class":93},[40,1670,203],{"class":89},[40,1672,1673],{"class":50},"\"Trend!B",[40,1675,210],{"class":209},[40,1677,1678],{"class":93},"r ",[40,1680,342],{"class":89},[40,1682,1008],{"class":152},[40,1684,216],{"class":209},[40,1686,1687],{"class":50},":G",[40,1689,210],{"class":209},[40,1691,1678],{"class":93},[40,1693,342],{"class":89},[40,1695,1008],{"class":152},[40,1697,216],{"class":209},[40,1699,1700],{"class":50},"\"",[40,1702,293],{"class":93},[40,1704,1705],{"class":50},"\"markers\"",[40,1707,1217],{"class":93},[40,1709,1220],{"class":152},[40,1711,293],{"class":93},[40,1713,1714],{"class":50},"\"last_point\"",[40,1716,1217],{"class":93},[40,1718,1220],{"class":152},[40,1720,1721],{"class":93},",\n",[40,1723,1725,1728,1730,1732,1734,1737,1739,1742],{"class":42,"line":1724},28,[40,1726,1727],{"class":50},"                            \"series_color\"",[40,1729,1217],{"class":93},[40,1731,1277],{"class":50},[40,1733,293],{"class":93},[40,1735,1736],{"class":50},"\"last_color\"",[40,1738,1217],{"class":93},[40,1740,1741],{"class":50},"\"#B91C1C\"",[40,1743,1242],{"class":93},[40,1745,1747,1750,1752,1755,1758,1761,1764,1766],{"class":42,"line":1746},29,[40,1748,1749],{"class":93},"last ",[40,1751,116],{"class":89},[40,1753,1754],{"class":152}," 3",[40,1756,1757],{"class":89}," +",[40,1759,1760],{"class":152}," len",[40,1762,1763],{"class":93},"(card) ",[40,1765,342],{"class":89},[40,1767,1768],{"class":152}," 1\n",[40,1770,1772,1774,1777,1779],{"class":42,"line":1771},30,[40,1773,536],{"class":89},[40,1775,1776],{"class":93}," status_name, colour ",[40,1778,874],{"class":89},[40,1780,1781],{"class":93}," fills.items():\n",[40,1783,1785,1788,1790,1792,1794,1797,1799,1801,1804,1806,1809,1811,1814,1816,1819,1821,1824,1826,1828,1831,1833,1836,1838,1841],{"class":42,"line":1784},31,[40,1786,1787],{"class":93},"    ws.conditional_format(",[40,1789,604],{"class":152},[40,1791,293],{"class":93},[40,1793,604],{"class":152},[40,1795,1796],{"class":93},", last, ",[40,1798,604],{"class":152},[40,1800,1663],{"class":93},[40,1802,1803],{"class":50},"\"type\"",[40,1805,1217],{"class":93},[40,1807,1808],{"class":50},"\"cell\"",[40,1810,293],{"class":93},[40,1812,1813],{"class":50},"\"criteria\"",[40,1815,1217],{"class":93},[40,1817,1818],{"class":50},"\"==\"",[40,1820,293],{"class":93},[40,1822,1823],{"class":50},"\"value\"",[40,1825,1217],{"class":93},[40,1827,203],{"class":89},[40,1829,1830],{"class":50},"'\"",[40,1832,210],{"class":209},[40,1834,1835],{"class":93},"status_name",[40,1837,216],{"class":209},[40,1839,1840],{"class":50},"\"'",[40,1842,1721],{"class":93},[40,1844,1846,1849,1852,1854,1857,1859,1861,1863],{"class":42,"line":1845},32,[40,1847,1848],{"class":50},"                                          \"format\"",[40,1850,1851],{"class":93},": book.add_format({",[40,1853,1234],{"class":50},[40,1855,1856],{"class":93},": colour, ",[40,1858,1214],{"class":50},[40,1860,1217],{"class":93},[40,1862,1220],{"class":152},[40,1864,1865],{"class":93},"})})\n",[40,1867,1869,1872,1875,1877,1880,1883,1886,1888,1890,1892,1895,1897,1900],{"class":42,"line":1868},33,[40,1870,1871],{"class":93},"ws.set_column(",[40,1873,1874],{"class":50},"\"A:A\"",[40,1876,293],{"class":93},[40,1878,1879],{"class":152},"24",[40,1881,1882],{"class":93},"); ws.set_column(",[40,1884,1885],{"class":50},"\"B:H\"",[40,1887,293],{"class":93},[40,1889,652],{"class":152},[40,1891,1882],{"class":93},[40,1893,1894],{"class":50},"\"I:I\"",[40,1896,293],{"class":93},[40,1898,1899],{"class":152},"20",[40,1901,125],{"class":93},[40,1903,1905,1908,1910,1912,1914,1917,1919],{"class":42,"line":1904},34,[40,1906,1907],{"class":93},"ws.set_landscape(); ws.fit_to_pages(",[40,1909,1048],{"class":152},[40,1911,293],{"class":93},[40,1913,1048],{"class":152},[40,1915,1916],{"class":93},"); ws.hide_gridlines(",[40,1918,1431],{"class":152},[40,1920,125],{"class":93},[40,1922,1924],{"class":42,"line":1923},35,[40,1925,1926],{"class":93},"book.close()\n",[40,1928,1930,1932,1935,1938,1940,1942,1945,1948,1951,1953],{"class":42,"line":1929},36,[40,1931,153],{"class":152},[40,1933,1934],{"class":93},"(",[40,1936,1937],{"class":50},"\"scorecard rows:\"",[40,1939,293],{"class":93},[40,1941,987],{"class":152},[40,1943,1944],{"class":93},"(card), ",[40,1946,1947],{"class":50},"\"| status counts:\"",[40,1949,1950],{"class":93},", card[",[40,1952,520],{"class":50},[40,1954,1955],{"class":93},"].value_counts().to_dict())\n",[10,1957,1958,1959,1962,1963,23],{},"Status is written as text and coloured by three conditional formats, so a reader who prints in black and white still sees the word, and anyone who sorts or filters keeps the colour attached. The sparklines read from a Trend sheet holding six months per KPI, with the latest point marked in red; they make it obvious that days sales outstanding has climbed two months running, from 39 to 47. ",[37,1960,1961],{},"fit_to_pages(1, 1)"," keeps the scorecard on one landscape page when printed or exported to PDF. Sparklines in more depth are covered in ",[19,1964,1966],{"href":1965},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fadd-sparklines-to-an-excel-report-with-xlsxwriter\u002F","Add Sparklines to an Excel Report with XlsxWriter",[620,1968,629,1974,629,1977,629,1980,629,1985,629,1991,629,1995,629,2001,629,2005,629,2009,629,2013,629,2017,629,2021,629,2025,629,2029,629,2033,629,2037,629,2040,629,2043,629,2047,629,2049,629,2052,629,2054,629,2057,629,2059,629,2062,629,2064,629,2067,629,2070,629,2073,629,2075,629,2079,629,2081,629,2084,629,2086,629,2089,629,2091,629,2094,629,2096,629,2099,629,2102,629,2105,629,2107,629,2110,629,2112,629,2114,629,2116,629,2119,629,2121,629,2124,629,2126,629,2129,629,2132,629,2135,629,2137,629,2140,629,2142,629,2145,629,2147,629,2150,629,2152,629,2155,629,2157,629,2160,629,2163,629,2166,629,2168,629,2171,629,2173,629,2176,629,2178,629,2181,629,2183,629,2185,629,2187,629,2190,629,2193,629,2196,629,2198,629,2201,629,2203,629,2206,629,2208,629,2210,629,2212,629,2214,629,2216,629,2219,629,2222,629,2225,629,2227,629,2230,629,2232,629,2235,629,2237,629,2240,629,2242,629,2244,629,2246],{"viewBox":1969,"role":623,"ariaLabelledBy":1970,"xmlns":627,"style":1973},"135 0 489 252",[1971,1972],"kpi-sheet-t","kpi-sheet-d","width:100%;max-width:489px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[631,1975,1976],{"id":1971},"The September scorecard",[635,1978,1979],{"id":1972},"Six KPIs with actual, target and status. Revenue, new customers and on-time delivery are green, complaint rate is amber, gross margin and days sales outstanding are red. Each row ends with a six-month sparkline.",[639,1981],{"x":1982,"y":642,"width":1983,"height":1984,"fill":645},"135","489","252",[639,1986],{"x":1987,"y":649,"width":1988,"height":1989,"rx":642,"fill":1990,"stroke":696,"style":1095},"151.5","32.0","24.0","#e7ebef",[639,1992],{"x":1993,"y":649,"width":1994,"height":1989,"rx":642,"fill":1990,"stroke":696,"style":1095},"183.5","149.6",[668,1996,2000],{"x":1997,"y":1998,"style":1999},"258.3","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[639,2002],{"x":2003,"y":649,"width":2004,"height":1989,"rx":642,"fill":1990,"stroke":696,"style":1095},"333.1","59.0",[668,2006,2008],{"x":2007,"y":1998,"style":1999},"362.6","B",[639,2010],{"x":2011,"y":649,"width":2012,"height":1989,"rx":642,"fill":1990,"stroke":696,"style":1095},"392.2","59.8",[668,2014,2016],{"x":2015,"y":1998,"style":1999},"422.1","C",[639,2018],{"x":2019,"y":649,"width":2020,"height":1989,"rx":642,"fill":1990,"stroke":696,"style":1095},"452.0","60.3",[668,2022,2024],{"x":2023,"y":1998,"style":1999},"482.1","D",[639,2026],{"x":2027,"y":649,"width":2028,"height":1989,"rx":642,"fill":1990,"stroke":696,"style":1095},"512.3","96.2",[668,2030,2032],{"x":2031,"y":1998,"style":1999},"560.4","E",[639,2034],{"x":1987,"y":2035,"width":1988,"height":2036,"rx":642,"fill":1990,"stroke":696,"style":1095},"40.0","28.0",[668,2038,1048],{"x":2039,"y":2004,"style":1999},"167.5",[639,2041],{"x":1993,"y":2035,"width":1994,"height":2036,"rx":642,"fill":653,"stroke":654,"style":2042},"stroke-width:1.5px",[668,2044,2046],{"x":1997,"y":2004,"style":2045},"font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","KPI",[639,2048],{"x":2003,"y":2035,"width":2004,"height":2036,"rx":642,"fill":653,"stroke":654,"style":2042},[668,2050,2051],{"x":2007,"y":2004,"style":2045},"Actual",[639,2053],{"x":2011,"y":2035,"width":2012,"height":2036,"rx":642,"fill":653,"stroke":654,"style":2042},[668,2055,2056],{"x":2015,"y":2004,"style":2045},"Target",[639,2058],{"x":2019,"y":2035,"width":2020,"height":2036,"rx":642,"fill":653,"stroke":654,"style":2042},[668,2060,2061],{"x":2023,"y":2004,"style":2045},"Status",[639,2063],{"x":2027,"y":2035,"width":2028,"height":2036,"rx":642,"fill":653,"stroke":654,"style":2042},[668,2065,2066],{"x":2031,"y":2004,"style":2045},"vs last month",[639,2068],{"x":1987,"y":2069,"width":1988,"height":2036,"rx":642,"fill":1990,"stroke":696,"style":1095},"68.0",[668,2071,1431],{"x":2039,"y":2072,"style":1999},"87.0",[639,2074],{"x":1993,"y":2069,"width":1994,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2076,2078],{"x":1997,"y":2072,"style":2077},"font-size:10.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Revenue (k€)",[639,2080],{"x":2003,"y":2069,"width":2004,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2082,2083],{"x":2007,"y":2072,"style":2077},"468.0",[639,2085],{"x":2011,"y":2069,"width":2012,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2087,2088],{"x":2015,"y":2072,"style":2077},"440.0",[639,2090],{"x":2019,"y":2069,"width":2020,"height":2036,"rx":642,"fill":709,"stroke":710,"style":2042},[668,2092,722],{"x":2023,"y":2072,"style":2093},"font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[639,2095],{"x":2027,"y":2069,"width":2028,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2097,2098],{"x":2031,"y":2072,"style":2077},"+67.0",[639,2100],{"x":1987,"y":2101,"width":1988,"height":2036,"rx":642,"fill":1990,"stroke":696,"style":1095},"96.0",[668,2103,604],{"x":2039,"y":2104,"style":1999},"115.0",[639,2106],{"x":1993,"y":2101,"width":1994,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2108,2109],{"x":1997,"y":2104,"style":2077},"Gross margin %",[639,2111],{"x":2003,"y":2101,"width":2004,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2113,693],{"x":2007,"y":2104,"style":2077},[639,2115],{"x":2011,"y":2101,"width":2012,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2117,2118],{"x":2015,"y":2104,"style":2077},"38.0",[639,2120],{"x":2019,"y":2101,"width":2020,"height":2036,"rx":642,"fill":773,"stroke":774,"style":2042},[668,2122,785],{"x":2023,"y":2104,"style":2123},"font-size:10.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[639,2125],{"x":2027,"y":2101,"width":2028,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2127,2128],{"x":2031,"y":2104,"style":2077},"+0.8",[639,2130],{"x":1987,"y":2131,"width":1988,"height":2036,"rx":642,"fill":1990,"stroke":696,"style":1095},"124.0",[668,2133,641],{"x":2039,"y":2134,"style":1999},"143.0",[639,2136],{"x":1993,"y":2131,"width":1994,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2138,2139],{"x":1997,"y":2134,"style":2077},"New customers",[639,2141],{"x":2003,"y":2131,"width":2004,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2143,2144],{"x":2007,"y":2134,"style":2077},"44.0",[639,2146],{"x":2011,"y":2131,"width":2012,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2148,2149],{"x":2015,"y":2134,"style":2077},"45.0",[639,2151],{"x":2019,"y":2131,"width":2020,"height":2036,"rx":642,"fill":743,"stroke":744,"style":2042},[668,2153,755],{"x":2023,"y":2134,"style":2154},"font-size:10.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[639,2156],{"x":2027,"y":2131,"width":2028,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2158,2159],{"x":2031,"y":2134,"style":2077},"+11.0",[639,2161],{"x":1987,"y":2162,"width":1988,"height":2036,"rx":642,"fill":1990,"stroke":696,"style":1095},"152.0",[668,2164,1577],{"x":2039,"y":2165,"style":1999},"171.0",[639,2167],{"x":1993,"y":2162,"width":1994,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2169,2170],{"x":1997,"y":2165,"style":2077},"Complaint rate %",[639,2172],{"x":2003,"y":2162,"width":2004,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2174,2175],{"x":2007,"y":2165,"style":2077},"2.2",[639,2177],{"x":2011,"y":2162,"width":2012,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2179,2180],{"x":2015,"y":2165,"style":2077},"2.0",[639,2182],{"x":2019,"y":2162,"width":2020,"height":2036,"rx":642,"fill":743,"stroke":744,"style":2042},[668,2184,755],{"x":2023,"y":2165,"style":2154},[639,2186],{"x":2027,"y":2162,"width":2028,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2188,2189],{"x":2031,"y":2165,"style":2077},"-0.4",[639,2191],{"x":1987,"y":2192,"width":1988,"height":2036,"rx":642,"fill":1990,"stroke":696,"style":1095},"180.0",[668,2194,1591],{"x":2039,"y":2195,"style":1999},"199.0",[639,2197],{"x":1993,"y":2192,"width":1994,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2199,2200],{"x":1997,"y":2195,"style":2077},"Days sales outstanding",[639,2202],{"x":2003,"y":2192,"width":2004,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2204,2205],{"x":2007,"y":2195,"style":2077},"47.0",[639,2207],{"x":2011,"y":2192,"width":2012,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2209,2035],{"x":2015,"y":2195,"style":2077},[639,2211],{"x":2019,"y":2192,"width":2020,"height":2036,"rx":642,"fill":773,"stroke":774,"style":2042},[668,2213,785],{"x":2023,"y":2195,"style":2123},[639,2215],{"x":2027,"y":2192,"width":2028,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2217,2218],{"x":2031,"y":2195,"style":2077},"+3.0",[639,2220],{"x":1987,"y":2221,"width":1988,"height":2036,"rx":642,"fill":1990,"stroke":696,"style":1095},"208.0",[668,2223,1606],{"x":2039,"y":2224,"style":1999},"227.0",[639,2226],{"x":1993,"y":2221,"width":1994,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2228,2229],{"x":1997,"y":2224,"style":2077},"On-time delivery %",[639,2231],{"x":2003,"y":2221,"width":2004,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2233,2234],{"x":2007,"y":2224,"style":2077},"96.4",[639,2236],{"x":2011,"y":2221,"width":2012,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2238,2239],{"x":2015,"y":2224,"style":2077},"95.0",[639,2241],{"x":2019,"y":2221,"width":2020,"height":2036,"rx":642,"fill":709,"stroke":710,"style":2042},[668,2243,722],{"x":2023,"y":2224,"style":2093},[639,2245],{"x":2027,"y":2221,"width":2028,"height":2036,"rx":642,"fill":645,"stroke":696,"style":1095},[668,2247,2248],{"x":2031,"y":2224,"style":2077},"+3.2",[25,2250,2252],{"id":2251},"step-5-check-the-colours-match-the-rules","Step 5 — Check the colours match the rules",[10,2254,2255],{},"A scorecard with a wrong colour is worse than none. Read the written file back and recompute every status from the definitions:",[30,2257,2259],{"className":80,"code":2258,"language":82,"meta":35,"style":35},"from openpyxl import load_workbook\n\nws = load_workbook(\"kpi_scorecard_2026-09.xlsx\")[\"Scorecard\"]\nmismatches = []\nfor r in range(4, 4 + len(card)):\n    kpi, actual, target, shown = (ws.cell(r, c).value for c in (1, 2, 3, 4))\n    d = defs.loc[kpi]\n    expected, _ = status(actual, target, d[\"Better\"], d[\"Tolerance\"])\n    if shown != expected:\n        mismatches.append((kpi, shown, expected))\nprint(\"status mismatches:\", mismatches or \"none\")\nprint(\"lower-is-better KPIs:\", list(defs.index[defs[\"Better\"] == \"lower\"]))\n",[37,2260,2261,2274,2278,2296,2306,2333,2369,2379,2398,2411,2416,2436],{"__ignoreMap":35},[40,2262,2263,2266,2269,2271],{"class":42,"line":43},[40,2264,2265],{"class":89},"from",[40,2267,2268],{"class":93}," openpyxl ",[40,2270,90],{"class":89},[40,2272,2273],{"class":93}," load_workbook\n",[40,2275,2276],{"class":42,"line":103},[40,2277,107],{"emptyLinePlaceholder":106},[40,2279,2280,2282,2284,2287,2289,2292,2294],{"class":42,"line":110},[40,2281,1177],{"class":93},[40,2283,116],{"class":89},[40,2285,2286],{"class":93}," load_workbook(",[40,2288,1170],{"class":50},[40,2290,2291],{"class":93},")[",[40,2293,1185],{"class":50},[40,2295,827],{"class":93},[40,2297,2298,2301,2303],{"class":42,"line":128},[40,2299,2300],{"class":93},"mismatches ",[40,2302,116],{"class":89},[40,2304,2305],{"class":93}," []\n",[40,2307,2308,2310,2313,2315,2318,2320,2322,2324,2326,2328,2330],{"class":42,"line":149},[40,2309,536],{"class":89},[40,2311,2312],{"class":93}," r ",[40,2314,874],{"class":89},[40,2316,2317],{"class":152}," range",[40,2319,1934],{"class":93},[40,2321,641],{"class":152},[40,2323,293],{"class":93},[40,2325,641],{"class":152},[40,2327,1757],{"class":89},[40,2329,1760],{"class":152},[40,2331,2332],{"class":93},"(card)):\n",[40,2334,2335,2338,2340,2343,2345,2348,2350,2352,2354,2356,2358,2360,2362,2364,2366],{"class":42,"line":159},[40,2336,2337],{"class":93},"    kpi, actual, target, shown ",[40,2339,116],{"class":89},[40,2341,2342],{"class":93}," (ws.cell(r, c).value ",[40,2344,536],{"class":89},[40,2346,2347],{"class":93}," c ",[40,2349,874],{"class":89},[40,2351,439],{"class":93},[40,2353,1048],{"class":152},[40,2355,293],{"class":93},[40,2357,1431],{"class":152},[40,2359,293],{"class":93},[40,2361,604],{"class":152},[40,2363,293],{"class":93},[40,2365,641],{"class":152},[40,2367,2368],{"class":93},"))\n",[40,2370,2371,2374,2376],{"class":42,"line":164},[40,2372,2373],{"class":93},"    d ",[40,2375,116],{"class":89},[40,2377,2378],{"class":93}," defs.loc[kpi]\n",[40,2380,2381,2384,2386,2389,2391,2394,2396],{"class":42,"line":191},[40,2382,2383],{"class":93},"    expected, _ ",[40,2385,116],{"class":89},[40,2387,2388],{"class":93}," status(actual, target, d[",[40,2390,564],{"class":50},[40,2392,2393],{"class":93},"], d[",[40,2395,569],{"class":50},[40,2397,1554],{"class":93},[40,2399,2400,2402,2405,2408],{"class":42,"line":421},[40,2401,406],{"class":89},[40,2403,2404],{"class":93}," shown ",[40,2406,2407],{"class":89},"!=",[40,2409,2410],{"class":93}," expected:\n",[40,2412,2413],{"class":42,"line":433},[40,2414,2415],{"class":93},"        mismatches.append((kpi, shown, expected))\n",[40,2417,2418,2420,2422,2425,2428,2431,2434],{"class":42,"line":467},[40,2419,153],{"class":152},[40,2421,1934],{"class":93},[40,2423,2424],{"class":50},"\"status mismatches:\"",[40,2426,2427],{"class":93},", mismatches ",[40,2429,2430],{"class":89},"or",[40,2432,2433],{"class":50}," \"none\"",[40,2435,125],{"class":93},[40,2437,2438,2440,2442,2445,2447,2449,2452,2454,2456,2458,2461],{"class":42,"line":472},[40,2439,153],{"class":152},[40,2441,1934],{"class":93},[40,2443,2444],{"class":50},"\"lower-is-better KPIs:\"",[40,2446,293],{"class":93},[40,2448,1648],{"class":152},[40,2450,2451],{"class":93},"(defs.index[defs[",[40,2453,564],{"class":50},[40,2455,491],{"class":93},[40,2457,360],{"class":89},[40,2459,2460],{"class":50}," \"lower\"",[40,2462,2463],{"class":93},"]))\n",[10,2465,2466,2467,2470],{},"openpyxl warns that it does not support the sparkline extension — harmless when only reading, but never ",[14,2468,2469],{},"save"," the scorecard with openpyxl, or the sparklines are dropped. The check reads what the reader will see — the values and the status text in the workbook — and recomputes each status from the definitions independently of the code that wrote it. Lower-is-better KPIs are listed explicitly because they are where hand-built scorecards most often go wrong. Running this in the job before the scorecard is distributed catches a broken rule the month it breaks.",[25,2472,2474],{"id":2473},"common-pitfalls","Common pitfalls",[2476,2477,2478,2486,2492,2498,2504],"ul",{},[2479,2480,2481,2485],"li",{},[2482,2483,2484],"strong",{},"Treating every KPI as higher-is-better."," Store the direction and flip the comparison.",[2479,2487,2488,2491],{},[2482,2489,2490],{},"Summing percentages for year to date."," Average them, ideally weighted.",[2479,2493,2494,2497],{},[2482,2495,2496],{},"Tolerances in absolute units."," A fraction of target works across KPIs of any scale.",[2479,2499,2500,2503],{},[2482,2501,2502],{},"Colour without text."," Write the status word; colour alone fails in print and for colour-blind readers.",[2479,2505,2506,2509],{},[2482,2507,2508],{},"Rules hidden in code."," Keep targets, direction, tolerance and aggregation in a definitions table.",[25,2511,2513],{"id":2512},"conclusion","Conclusion",[10,2515,2516],{},"A reliable KPI scorecard starts with a definitions table — target, direction, tolerance and year-to-date rule per KPI — and computes status from a direction-aware performance figure, so lower-is-better KPIs and amber bands are handled the same way every month. Add variance against target and last month, year-to-date figures that sum or average correctly, and six-month sparklines, write it as one landscape page with status text coloured by conditional formatting, and recheck the written statuses before the pack goes out.",[25,2518,2520],{"id":2519},"frequently-asked-questions","Frequently asked questions",[10,2522,2523,2526],{},[2482,2524,2525],{},"How do I calculate red, amber, green status for KPIs in Python?","\nCompute performance as actual minus target over target, flipped for KPIs where lower is better. Zero or more is green, a shortfall within the tolerance is amber, and anything worse is red.",[10,2528,2529,2532],{},[2482,2530,2531],{},"How do I handle KPIs where lower is better?","\nStore the direction in the KPI definitions and compute performance as target minus actual over target for those KPIs, so a positive result always means better than target.",[10,2534,2535,2538],{},[2482,2536,2537],{},"How do I calculate year-to-date for percentage KPIs?","\nDo not sum them. Average the monthly values, or better, compute a weighted average using the underlying volumes, such as margin weighted by revenue.",[10,2540,2541,2544],{},[2482,2542,2543],{},"How do I add trend sparklines to an Excel scorecard?","\nWith XlsxWriter, write each KPI's monthly values to a data sheet and call add_sparkline on the scorecard row with the range of those values.",[25,2546,2548],{"id":2547},"related","Related",[2476,2550,2551,2557,2564,2571],{},[2479,2552,2553,2554,2556],{},"Up one level: ",[19,2555,22],{"href":21}," — finance and operations reports in Excel.",[2479,2558,2559,2563],{},[19,2560,2562],{"href":2561},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-budget-vs-actual-variance-report\u002F","Build a Budget vs Actual Variance Report"," — variance at account level.",[2479,2565,2566,2570],{},[19,2567,2569],{"href":2568},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fadd-icon-sets-with-openpyxl\u002F","Add Icon Sets with openpyxl"," — another way to show status.",[2479,2572,2573,2577],{},[19,2574,2576],{"href":2575},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-support-ticket-sla-report\u002F","Build a Support Ticket SLA Report"," — the source of service KPIs.",[2579,2580,2581],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":35,"searchDepth":103,"depth":103,"links":2583},[2584,2585,2586,2587,2588,2589,2590,2591,2592,2593],{"id":27,"depth":103,"text":28},{"id":73,"depth":103,"text":74},{"id":239,"depth":103,"text":240},{"id":791,"depth":103,"text":792},{"id":1138,"depth":103,"text":1139},{"id":2251,"depth":103,"text":2252},{"id":2473,"depth":103,"text":2474},{"id":2512,"depth":103,"text":2513},{"id":2519,"depth":103,"text":2520},{"id":2547,"depth":103,"text":2548},"2026-10-02","Build a one-page monthly KPI scorecard in Excel with pandas and XlsxWriter: KPI definitions with targets, direction and tolerance kept as data, red-amber-green status that knows when lower is better, variance against target and last month, year-to-date figures that sum or average correctly, six-month sparklines, and a status check that matches the colours.","md",[2598,2600,2602,2604],{"q":2525,"a":2599},"Compute performance as actual minus target over target, flipped for KPIs where lower is better. Zero or more is green, a shortfall within the tolerance is amber, and anything worse is red.",{"q":2531,"a":2601},"Store the direction in the KPI definitions and compute performance as target minus actual over target for those KPIs, so a positive result always means better than target.",{"q":2537,"a":2603},"Do not sum them. Average the monthly values, or better, compute a weighted average using the underlying volumes, such as margin weighted by revenue.",{"q":2543,"a":2605},"With XlsxWriter, write each KPI's monthly values to a data sheet and call add_sparkline on the scorecard row with the range of those values.","build a monthly kpi scorecard with targets",null,{"breadcrumb":2609},[2610,2612,2615],{"name":2611,"item":348},"Home",{"name":2613,"item":2614},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":2616,"item":21},"Building Finance and Operations Reports in Excel with Python","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets",{"title":2619,"description":2620},"Build a Monthly KPI Scorecard with Targets in Excel and Python","A KPI scorecard from monthly actuals and targets: higher- and lower-is-better RAG rules with tolerance, variance, YTD sums vs averages, XlsxWriter sparklines and colour-coded status in one Excel sheet.","build-a-monthly-kpi-scorecard-with-targets","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-monthly-kpi-scorecard-with-targets\u002Findex",[2624,2625],"report-layouts","conditional-formatting","how-to","2IitVXFipUWXZbB3XXy5H3VNC8YvxxVdRq9pBEqxLBA",[2629,2633],{"title":2630,"path":2631,"stem":2632,"children":-1},"Build a Month-End Close Checklist Tracker","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-month-end-close-checklist-tracker","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-month-end-close-checklist-tracker\u002Findex",{"title":2634,"path":2635,"stem":2636,"children":-1},"Build a Price-Volume-Mix Analysis","\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-price-volume-mix-analysis","automating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-price-volume-mix-analysis\u002Findex",[2638,2640],{"path":2639,"title":2616},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports",{"path":2641,"title":2613},"\u002Fautomating-reporting-workflows",[],[2644,2648,2651,2654,2657,2660],{"path":2645,"title":2646,"kind":2647},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards","Building Multi-Sheet Excel Dashboards","guide",{"path":2649,"title":2650,"kind":2626},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-data-dictionary-sheet-to-an-excel-report","Add a Data Dictionary Sheet to an Excel Report",{"path":2652,"title":2653,"kind":2626},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-kpi-summary-block-to-an-excel-dashboard","Add a KPI Summary Block to an Excel Dashboard",{"path":2655,"title":2656,"kind":2626},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-table-of-contents-sheet-with-hyperlinks-in-excel","Add a Table of Contents Sheet with Hyperlinks in Excel",{"path":2658,"title":2659,"kind":2626},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-slicer-style-filters-to-an-excel-dashboard","Add a Dropdown Filter That Drives an Excel Dashboard",{"path":2661,"title":2662,"kind":2626},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-summary-sheet-to-excel-report-python","Add a Summary Sheet to an Excel Report with Python",1790972123990]