[{"data":1,"prerenderedAt":2294},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fcreate-every-combination-of-two-lists-with-a-cross-join":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fcreate-every-combination-of-two-lists-with-a-cross-join":2260,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fcreate-every-combination-of-two-lists-with-a-cross-join":2269,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fcreate-every-combination-of-two-lists-with-a-cross-join":2274,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fcreate-every-combination-of-two-lists-with-a-cross-join":2275},{"id":4,"title":5,"body":6,"dateModified":2225,"datePublished":2225,"description":2226,"extension":2227,"faq":2228,"keyword":2237,"kind":2238,"meta":2239,"navigation":103,"path":2249,"seo":2250,"slug":2253,"stem":2254,"topics":2255,"type":2258,"__hash__":2259},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fcreate-every-combination-of-two-lists-with-a-cross-join\u002Findex.md","Create Every Combination of Two Lists with a Cross Join in pandas",{"type":7,"value":8,"toc":2210},"minimark",[9,32,37,65,71,75,244,247,537,541,544,672,688,692,695,806,813,984,988,995,1248,1257,1339,1343,1346,1551,1554,1558,1759,1766,1770,1773,2077,2080,2084,2087,2091,2130,2134,2145,2149,2155,2161,2167,2173,2177,2206],[10,11,12,13,17,18,20,21,25,26,31],"p",{},"A sales table only contains the store-and-product pairs that sold something. A report that should show ",[14,15,16],"em",{},"every"," product for ",[14,19,16],{}," store — so that a zero is visible as a zero, and a gap in a range is obvious — needs the combinations that did not happen too. The same shape appears in training records (every employee against every mandatory course), budgets (every cost centre for every month) and price lists (every product in every currency). The tool is a cross join: pair every row of one list with every row of another. pandas does it with ",[22,23,24],"code",{},"merge(how=\"cross\")",", and a left join of the real data onto that grid fills in what actually happened. This page belongs to ",[27,28,30],"a",{"href":29},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002F","Merging and Joining Excel DataFrames",".",[33,34,36],"h2",{"id":35},"prerequisites","Prerequisites",[38,39,44],"pre",{"className":40,"code":41,"language":42,"meta":43,"style":43},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[22,45,46],{"__ignoreMap":43},[47,48,51,55,59,62],"span",{"class":49,"line":50},"line",1,[47,52,54],{"class":53},"sMTad","pip",[47,56,58],{"class":57},"srMev"," install",[47,60,61],{"class":57}," pandas",[47,63,64],{"class":57}," openpyxl\n",[10,66,67,70],{},[22,68,69],{},"stores_products.xlsx"," has three sheets: three stores, four products, and a sales table with only the six store\u002Fproduct pairs that sold anything this month.",[33,72,74],{"id":73},"step-1-every-combination-with-mergehowcross","Step 1 — Every combination with merge(how=\"cross\")",[38,76,80],{"className":77,"code":78,"language":79,"meta":43,"style":43},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nbook = pd.read_excel(\"stores_products.xlsx\", sheet_name=None)\nstores, products, sales = book[\"Stores\"], book[\"Products\"], book[\"Sales\"]\ngrid = stores.merge(products, how=\"cross\")\nprint(grid.to_string(index=False))\nprint(len(stores), \"stores ×\", len(products), \"products =\", len(grid), \"rows\")\n","python",[22,81,82,98,105,136,164,185,205],{"__ignoreMap":43},[47,83,84,88,92,95],{"class":49,"line":50},[47,85,87],{"class":86},"s-kum","import",[47,89,91],{"class":90},"skGVy"," pandas ",[47,93,94],{"class":86},"as",[47,96,97],{"class":90}," pd\n",[47,99,101],{"class":49,"line":100},2,[47,102,104],{"emptyLinePlaceholder":103},true,"\n",[47,106,108,111,114,117,120,123,127,129,133],{"class":49,"line":107},3,[47,109,110],{"class":90},"book ",[47,112,113],{"class":86},"=",[47,115,116],{"class":90}," pd.read_excel(",[47,118,119],{"class":57},"\"stores_products.xlsx\"",[47,121,122],{"class":90},", ",[47,124,126],{"class":125},"sa561","sheet_name",[47,128,113],{"class":86},[47,130,132],{"class":131},"sP0c6","None",[47,134,135],{"class":90},")\n",[47,137,139,142,144,147,150,153,156,158,161],{"class":49,"line":138},4,[47,140,141],{"class":90},"stores, products, sales ",[47,143,113],{"class":86},[47,145,146],{"class":90}," book[",[47,148,149],{"class":57},"\"Stores\"",[47,151,152],{"class":90},"], book[",[47,154,155],{"class":57},"\"Products\"",[47,157,152],{"class":90},[47,159,160],{"class":57},"\"Sales\"",[47,162,163],{"class":90},"]\n",[47,165,167,170,172,175,178,180,183],{"class":49,"line":166},5,[47,168,169],{"class":90},"grid ",[47,171,113],{"class":86},[47,173,174],{"class":90}," stores.merge(products, ",[47,176,177],{"class":125},"how",[47,179,113],{"class":86},[47,181,182],{"class":57},"\"cross\"",[47,184,135],{"class":90},[47,186,188,191,194,197,199,202],{"class":49,"line":187},6,[47,189,190],{"class":131},"print",[47,192,193],{"class":90},"(grid.to_string(",[47,195,196],{"class":125},"index",[47,198,113],{"class":86},[47,200,201],{"class":131},"False",[47,203,204],{"class":90},"))\n",[47,206,208,210,213,216,219,222,224,226,229,232,234,236,239,242],{"class":49,"line":207},7,[47,209,190],{"class":131},[47,211,212],{"class":90},"(",[47,214,215],{"class":131},"len",[47,217,218],{"class":90},"(stores), ",[47,220,221],{"class":57},"\"stores ×\"",[47,223,122],{"class":90},[47,225,215],{"class":131},[47,227,228],{"class":90},"(products), ",[47,230,231],{"class":57},"\"products =\"",[47,233,122],{"class":90},[47,235,215],{"class":131},[47,237,238],{"class":90},"(grid), ",[47,240,241],{"class":57},"\"rows\"",[47,243,135],{"class":90},[10,245,246],{},"A cross join needs no key: each of the three store rows is paired with each of the four product rows, giving twelve rows that carry all columns of both tables — the store's region and the product's category come along for free. The row order is \"all products for the first store, then the next store\", which is usually how a report wants it.",[248,249,257,258,257,262,257,266,257,274,257,283,257,287,257,294,257,298,257,302,257,306,257,311,257,316,257,320,257,322,257,325,257,328,257,332,257,334,257,338,257,340,257,343,257,346,257,350,257,352,257,355,257,357,257,360,257,363,257,367,257,369,257,372,257,374,257,377,257,380,257,384,257,386,257,388,257,391,257,394,257,397,257,400,257,404,257,407,257,409,257,412,257,414,257,416,257,418,257,420,257,422,257,424,257,428,257,431,257,433,257,435,257,437,257,439,257,441,257,443,257,445,257,447,257,449,257,451,257,453,257,455,257,457,257,459,257,461,257,463,257,465,257,467,257,469,257,471,257,474,257,478,257,480,257,482,257,484,257,486,257,489,257,493,257,495,257,498,257,500,257,503,257,509,257,513,257,519,257,524,257,532],"svg",{"viewBox":250,"role":251,"ariaLabelledBy":252,"xmlns":255,"style":256},"84 0 591 276","img",[253,254],"cj-grid-t","cj-grid-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:591px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[259,260,261],"title",{"id":253},"Three stores and four products become twelve combinations",[263,264,265],"desc",{"id":254},"On the left two short lists, three stores and four products. On the right the cross join, twelve rows pairing each store with each product.",[267,268],"rect",{"x":269,"y":270,"width":271,"height":272,"fill":273},"84","0","591","276","#ffffff",[267,275],{"x":276,"y":277,"width":278,"height":279,"rx":270,"fill":280,"stroke":281,"style":282},"100.6","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[267,284],{"x":285,"y":277,"width":286,"height":279,"rx":270,"fill":280,"stroke":281,"style":282},"132.6","58.0",[288,289,293],"text",{"x":290,"y":291,"style":292},"161.6","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[267,295],{"x":296,"y":277,"width":297,"height":279,"rx":270,"fill":280,"stroke":281,"style":282},"190.7","68.8",[288,299,301],{"x":300,"y":291,"style":292},"225.0","B",[267,303],{"x":276,"y":304,"width":278,"height":305,"rx":270,"fill":280,"stroke":281,"style":282},"64.0","28.0",[288,307,310],{"x":308,"y":309,"style":292},"116.6","83.0","1",[267,312],{"x":285,"y":304,"width":286,"height":305,"rx":270,"fill":313,"stroke":314,"style":315},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[288,317,319],{"x":290,"y":309,"style":318},"font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Store",[267,321],{"x":296,"y":304,"width":297,"height":305,"rx":270,"fill":313,"stroke":314,"style":315},[288,323,324],{"x":300,"y":309,"style":318},"Product",[267,326],{"x":276,"y":327,"width":278,"height":305,"rx":270,"fill":280,"stroke":281,"style":282},"92.0",[288,329,331],{"x":308,"y":330,"style":292},"111.0","2",[267,333],{"x":285,"y":327,"width":286,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,335,337],{"x":290,"y":330,"style":336},"font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Leeds",[267,339],{"x":296,"y":327,"width":297,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,341,342],{"x":300,"y":330,"style":336},"Drill",[267,344],{"x":276,"y":345,"width":278,"height":305,"rx":270,"fill":280,"stroke":281,"style":282},"120.0",[288,347,349],{"x":308,"y":348,"style":292},"139.0","3",[267,351],{"x":285,"y":345,"width":286,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,353,354],{"x":290,"y":348,"style":336},"York",[267,356],{"x":296,"y":345,"width":297,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,358,359],{"x":300,"y":348,"style":336},"Saw",[267,361],{"x":276,"y":362,"width":278,"height":305,"rx":270,"fill":280,"stroke":281,"style":282},"148.0",[288,364,366],{"x":308,"y":365,"style":292},"167.0","4",[267,368],{"x":285,"y":362,"width":286,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,370,371],{"x":290,"y":365,"style":336},"Hull",[267,373],{"x":296,"y":362,"width":297,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,375,376],{"x":300,"y":365,"style":336},"Hose",[267,378],{"x":276,"y":379,"width":278,"height":305,"rx":270,"fill":280,"stroke":281,"style":282},"176.0",[288,381,383],{"x":308,"y":382,"style":292},"195.0","5",[267,385],{"x":285,"y":379,"width":286,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[267,387],{"x":296,"y":379,"width":297,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,389,390],{"x":300,"y":382,"style":336},"Rake",[267,392],{"x":393,"y":277,"width":278,"height":279,"rx":270,"fill":280,"stroke":281,"style":282},"488.0",[267,395],{"x":396,"y":277,"width":286,"height":279,"rx":270,"fill":280,"stroke":281,"style":282},"520.0",[288,398,293],{"x":399,"y":291,"style":292},"549.0",[267,401],{"x":402,"y":277,"width":403,"height":279,"rx":270,"fill":280,"stroke":281,"style":282},"578.0","81.4",[288,405,301],{"x":406,"y":291,"style":292},"618.7",[267,408],{"x":393,"y":304,"width":278,"height":305,"rx":270,"fill":280,"stroke":281,"style":282},[288,410,310],{"x":411,"y":309,"style":292},"504.0",[267,413],{"x":396,"y":304,"width":286,"height":305,"rx":270,"fill":313,"stroke":314,"style":315},[288,415,319],{"x":399,"y":309,"style":318},[267,417],{"x":402,"y":304,"width":403,"height":305,"rx":270,"fill":313,"stroke":314,"style":315},[288,419,324],{"x":406,"y":309,"style":318},[267,421],{"x":393,"y":327,"width":278,"height":305,"rx":270,"fill":280,"stroke":281,"style":282},[288,423,331],{"x":411,"y":330,"style":292},[267,425],{"x":396,"y":327,"width":286,"height":305,"rx":270,"fill":426,"stroke":427,"style":315},"#d9f4f1","var(--teal,#0f9488)",[288,429,337],{"x":399,"y":330,"style":430},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[267,432],{"x":402,"y":327,"width":403,"height":305,"rx":270,"fill":426,"stroke":427,"style":315},[288,434,342],{"x":406,"y":330,"style":430},[267,436],{"x":393,"y":345,"width":278,"height":305,"rx":270,"fill":280,"stroke":281,"style":282},[288,438,349],{"x":411,"y":348,"style":292},[267,440],{"x":396,"y":345,"width":286,"height":305,"rx":270,"fill":426,"stroke":427,"style":315},[288,442,337],{"x":399,"y":348,"style":430},[267,444],{"x":402,"y":345,"width":403,"height":305,"rx":270,"fill":426,"stroke":427,"style":315},[288,446,359],{"x":406,"y":348,"style":430},[267,448],{"x":393,"y":362,"width":278,"height":305,"rx":270,"fill":280,"stroke":281,"style":282},[288,450,366],{"x":411,"y":365,"style":292},[267,452],{"x":396,"y":362,"width":286,"height":305,"rx":270,"fill":426,"stroke":427,"style":315},[288,454,337],{"x":399,"y":365,"style":430},[267,456],{"x":402,"y":362,"width":403,"height":305,"rx":270,"fill":426,"stroke":427,"style":315},[288,458,376],{"x":406,"y":365,"style":430},[267,460],{"x":393,"y":379,"width":278,"height":305,"rx":270,"fill":280,"stroke":281,"style":282},[288,462,383],{"x":411,"y":382,"style":292},[267,464],{"x":396,"y":379,"width":286,"height":305,"rx":270,"fill":426,"stroke":427,"style":315},[288,466,337],{"x":399,"y":382,"style":430},[267,468],{"x":402,"y":379,"width":403,"height":305,"rx":270,"fill":426,"stroke":427,"style":315},[288,470,390],{"x":406,"y":382,"style":430},[267,472],{"x":393,"y":473,"width":278,"height":305,"rx":270,"fill":280,"stroke":281,"style":282},"204.0",[288,475,477],{"x":411,"y":476,"style":292},"223.0","6",[267,479],{"x":396,"y":473,"width":286,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,481,354],{"x":399,"y":476,"style":336},[267,483],{"x":402,"y":473,"width":403,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,485,342],{"x":406,"y":476,"style":336},[267,487],{"x":393,"y":488,"width":278,"height":305,"rx":270,"fill":280,"stroke":281,"style":282},"232.0",[288,490,492],{"x":411,"y":491,"style":292},"251.0","7",[267,494],{"x":396,"y":488,"width":286,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,496,497],{"x":399,"y":491,"style":336},"…",[267,499],{"x":402,"y":488,"width":403,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,501,502],{"x":406,"y":491,"style":336},"… 12 rows",[288,504,508],{"x":505,"y":506,"style":507},"180.0","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","two lists",[288,510,512],{"x":511,"y":506,"style":507},"573.7","every pair",[49,514],{"x1":515,"y1":516,"x2":517,"y2":516,"stroke":314,"style":518},"269.4","122.0","474.0","stroke-width:2px",[520,521],"polygon",{"points":522,"fill":523},"478.0,122.0 469.0,127.0 469.0,117.0","#5b5cf0",[267,525],{"x":526,"y":527,"width":528,"height":529,"rx":530,"fill":273,"stroke":281,"style":531},"298.3","89.0","150.8","22.0","11","stroke-width:1.2px",[288,533,24],{"x":534,"y":535,"style":536},"373.7","104.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",[33,538,540],{"id":539},"step-2-fill-in-what-actually-happened","Step 2 — Fill in what actually happened",[10,542,543],{},"Left-join the sales onto the grid, so every combination survives and the ones without sales get zero:",[38,545,547],{"className":77,"code":546,"language":79,"meta":43,"style":43},"full = grid.merge(sales, on=[\"Store\", \"Product\"], how=\"left\", validate=\"one_to_one\")\nfull[\"Units\"] = full[\"Units\"].fillna(0).astype(int)\nprint(full.to_string(index=False))\nprint(\"combinations with no sales:\", int((full[\"Units\"] == 0).sum()))\n",[22,548,549,597,628,643],{"__ignoreMap":43},[47,550,551,554,556,559,562,564,567,570,572,575,578,580,582,585,587,590,592,595],{"class":49,"line":50},[47,552,553],{"class":90},"full ",[47,555,113],{"class":86},[47,557,558],{"class":90}," grid.merge(sales, ",[47,560,561],{"class":125},"on",[47,563,113],{"class":86},[47,565,566],{"class":90},"[",[47,568,569],{"class":57},"\"Store\"",[47,571,122],{"class":90},[47,573,574],{"class":57},"\"Product\"",[47,576,577],{"class":90},"], ",[47,579,177],{"class":125},[47,581,113],{"class":86},[47,583,584],{"class":57},"\"left\"",[47,586,122],{"class":90},[47,588,589],{"class":125},"validate",[47,591,113],{"class":86},[47,593,594],{"class":57},"\"one_to_one\"",[47,596,135],{"class":90},[47,598,599,602,605,608,610,613,615,618,620,623,626],{"class":49,"line":100},[47,600,601],{"class":90},"full[",[47,603,604],{"class":57},"\"Units\"",[47,606,607],{"class":90},"] ",[47,609,113],{"class":86},[47,611,612],{"class":90}," full[",[47,614,604],{"class":57},[47,616,617],{"class":90},"].fillna(",[47,619,270],{"class":131},[47,621,622],{"class":90},").astype(",[47,624,625],{"class":131},"int",[47,627,135],{"class":90},[47,629,630,632,635,637,639,641],{"class":49,"line":107},[47,631,190],{"class":131},[47,633,634],{"class":90},"(full.to_string(",[47,636,196],{"class":125},[47,638,113],{"class":86},[47,640,201],{"class":131},[47,642,204],{"class":90},[47,644,645,647,649,652,654,656,659,661,663,666,669],{"class":49,"line":138},[47,646,190],{"class":131},[47,648,212],{"class":90},[47,650,651],{"class":57},"\"combinations with no sales:\"",[47,653,122],{"class":90},[47,655,625],{"class":131},[47,657,658],{"class":90},"((full[",[47,660,604],{"class":57},[47,662,607],{"class":90},[47,664,665],{"class":86},"==",[47,667,668],{"class":131}," 0",[47,670,671],{"class":90},").sum()))\n",[10,673,674,675,678,679,682,683,687],{},"The left join keeps all twelve rows; the six pairs absent from the sales table get ",[22,676,677],{},"NaN",", filled with 0. ",[22,680,681],{},"validate=\"one_to_one\""," makes pandas raise an error if the sales table has two rows for the same store and product, which would otherwise duplicate grid rows silently — the check explained in ",[27,684,686],{"href":685},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fvalidate-a-merge-to-avoid-duplicate-rows\u002F","Validate a Merge to Avoid Duplicate Rows",". Six of the twelve combinations sold nothing — information that was invisible in the sales table.",[33,689,691],{"id":690},"step-3-present-it-as-a-matrix","Step 3 — Present it as a matrix",[10,693,694],{},"The completed grid pivots cleanly into a store × product matrix, with zeros where nothing sold:",[38,696,698],{"className":77,"code":697,"language":79,"meta":43,"style":43},"matrix = full.pivot(index=\"Store\", columns=\"Product\", values=\"Units\").loc[stores[\"Store\"], products[\"Product\"]]\nprint(matrix)\ngaps = full.loc[full[\"Units\"] == 0, [\"Store\", \"Product\"]]\nprint(gaps.groupby(\"Store\")[\"Product\"].agg(\", \".join))\n",[22,699,700,747,754,783],{"__ignoreMap":43},[47,701,702,705,707,710,712,714,716,718,721,723,725,727,730,732,734,737,739,742,744],{"class":49,"line":50},[47,703,704],{"class":90},"matrix ",[47,706,113],{"class":86},[47,708,709],{"class":90}," full.pivot(",[47,711,196],{"class":125},[47,713,113],{"class":86},[47,715,569],{"class":57},[47,717,122],{"class":90},[47,719,720],{"class":125},"columns",[47,722,113],{"class":86},[47,724,574],{"class":57},[47,726,122],{"class":90},[47,728,729],{"class":125},"values",[47,731,113],{"class":86},[47,733,604],{"class":57},[47,735,736],{"class":90},").loc[stores[",[47,738,569],{"class":57},[47,740,741],{"class":90},"], products[",[47,743,574],{"class":57},[47,745,746],{"class":90},"]]\n",[47,748,749,751],{"class":49,"line":100},[47,750,190],{"class":131},[47,752,753],{"class":90},"(matrix)\n",[47,755,756,759,761,764,766,768,770,772,775,777,779,781],{"class":49,"line":107},[47,757,758],{"class":90},"gaps ",[47,760,113],{"class":86},[47,762,763],{"class":90}," full.loc[full[",[47,765,604],{"class":57},[47,767,607],{"class":90},[47,769,665],{"class":86},[47,771,668],{"class":131},[47,773,774],{"class":90},", [",[47,776,569],{"class":57},[47,778,122],{"class":90},[47,780,574],{"class":57},[47,782,746],{"class":90},[47,784,785,787,790,792,795,797,800,803],{"class":49,"line":138},[47,786,190],{"class":131},[47,788,789],{"class":90},"(gaps.groupby(",[47,791,569],{"class":57},[47,793,794],{"class":90},")[",[47,796,574],{"class":57},[47,798,799],{"class":90},"].agg(",[47,801,802],{"class":57},"\", \"",[47,804,805],{"class":90},".join))\n",[10,807,808,809,812],{},"Reindexing with ",[22,810,811],{},".loc[stores, products]"," keeps the stores and products in their original list order rather than alphabetical. The gap list per store — Hull sold no drills, hoses or rakes — is often the most useful output: a ranging check for merchandisers, a missing-training list for HR. Pivoting without the cross join would also produce a matrix, but any product that no store sold would be missing as a column altogether.",[248,814,257,820,257,823,257,826,257,831,257,835,257,838,257,842,257,846,257,849,257,853,257,857,257,861,257,865,257,869,257,873,257,875,257,879,257,881,257,883,257,885,257,887,257,889,257,891,257,893,257,895,257,897,257,899,257,902,257,905,257,907,257,909,257,911,257,914,257,916,257,919,257,921,257,924,257,928,257,931,257,934,257,937,257,939,257,941,257,943,257,946,257,948,257,950,257,952,257,954,257,956,257,958,257,961,257,964,257,966,257,968,257,970,257,972,257,974,257,976,257,978,257,980,257,982],{"viewBox":815,"role":251,"ariaLabelledBy":816,"xmlns":255,"style":819},"219 0 322 168",[817,818],"cj-matrix-t","cj-matrix-d","width:100%;max-width:322px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[259,821,822],{"id":817},"Units sold per store and product, zeros included",[263,824,825],{"id":818},"A matrix with stores Leeds, York and Hull down the side and products Drill, Saw, Hose and Rake across the top. Leeds sold 14, 9, 30 and 0; York 8, 0, 0 and 11; Hull 0, 4, 0 and 0. Zero cells are highlighted.",[267,827],{"x":828,"y":270,"width":829,"height":830,"fill":273},"219","322","168",[267,832],{"x":833,"y":834,"width":278,"height":279,"rx":270,"fill":280,"stroke":281,"style":282},"235.2","16.0",[267,836],{"x":837,"y":834,"width":286,"height":279,"rx":270,"fill":280,"stroke":281,"style":282},"267.2",[288,839,293],{"x":840,"y":841,"style":292},"296.3","33.0",[267,843],{"x":844,"y":834,"width":845,"height":279,"rx":270,"fill":280,"stroke":281,"style":282},"325.3","46.4",[288,847,301],{"x":848,"y":841,"style":292},"348.5",[267,850],{"x":851,"y":834,"width":852,"height":279,"rx":270,"fill":280,"stroke":281,"style":282},"371.7","48.1",[288,854,856],{"x":855,"y":841,"style":292},"395.8","C",[267,858],{"x":859,"y":834,"width":860,"height":279,"rx":270,"fill":280,"stroke":281,"style":282},"419.8","53.1",[288,862,864],{"x":863,"y":841,"style":292},"446.3","D",[267,866],{"x":867,"y":834,"width":868,"height":279,"rx":270,"fill":280,"stroke":281,"style":282},"472.9","51.9",[288,870,872],{"x":871,"y":841,"style":292},"498.8","E",[267,874],{"x":833,"y":277,"width":278,"height":305,"rx":270,"fill":280,"stroke":281,"style":282},[288,876,310],{"x":877,"y":878,"style":292},"251.2","59.0",[267,880],{"x":837,"y":277,"width":286,"height":305,"rx":270,"fill":313,"stroke":314,"style":315},[288,882,319],{"x":840,"y":878,"style":318},[267,884],{"x":844,"y":277,"width":845,"height":305,"rx":270,"fill":313,"stroke":314,"style":315},[288,886,342],{"x":848,"y":878,"style":318},[267,888],{"x":851,"y":277,"width":852,"height":305,"rx":270,"fill":313,"stroke":314,"style":315},[288,890,359],{"x":855,"y":878,"style":318},[267,892],{"x":859,"y":277,"width":860,"height":305,"rx":270,"fill":313,"stroke":314,"style":315},[288,894,376],{"x":863,"y":878,"style":318},[267,896],{"x":867,"y":277,"width":868,"height":305,"rx":270,"fill":313,"stroke":314,"style":315},[288,898,390],{"x":871,"y":878,"style":318},[267,900],{"x":833,"y":901,"width":278,"height":305,"rx":270,"fill":280,"stroke":281,"style":282},"68.0",[288,903,331],{"x":877,"y":904,"style":292},"87.0",[267,906],{"x":837,"y":901,"width":286,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,908,337],{"x":840,"y":904,"style":336},[267,910],{"x":844,"y":901,"width":845,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,912,913],{"x":848,"y":904,"style":336},"14",[267,915],{"x":851,"y":901,"width":852,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,917,918],{"x":855,"y":904,"style":336},"9",[267,920],{"x":859,"y":901,"width":860,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,922,923],{"x":863,"y":904,"style":336},"30",[267,925],{"x":867,"y":901,"width":868,"height":305,"rx":270,"fill":926,"stroke":927,"style":315},"#fdefd8","var(--gold,#b4740a)",[288,929,270],{"x":871,"y":904,"style":930},"font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[267,932],{"x":833,"y":933,"width":278,"height":305,"rx":270,"fill":280,"stroke":281,"style":282},"96.0",[288,935,349],{"x":877,"y":936,"style":292},"115.0",[267,938],{"x":837,"y":933,"width":286,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,940,354],{"x":840,"y":936,"style":336},[267,942],{"x":844,"y":933,"width":845,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,944,945],{"x":848,"y":936,"style":336},"8",[267,947],{"x":851,"y":933,"width":852,"height":305,"rx":270,"fill":926,"stroke":927,"style":315},[288,949,270],{"x":855,"y":936,"style":930},[267,951],{"x":859,"y":933,"width":860,"height":305,"rx":270,"fill":926,"stroke":927,"style":315},[288,953,270],{"x":863,"y":936,"style":930},[267,955],{"x":867,"y":933,"width":868,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,957,530],{"x":871,"y":936,"style":336},[267,959],{"x":833,"y":960,"width":278,"height":305,"rx":270,"fill":280,"stroke":281,"style":282},"124.0",[288,962,366],{"x":877,"y":963,"style":292},"143.0",[267,965],{"x":837,"y":960,"width":286,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,967,371],{"x":840,"y":963,"style":336},[267,969],{"x":844,"y":960,"width":845,"height":305,"rx":270,"fill":926,"stroke":927,"style":315},[288,971,270],{"x":848,"y":963,"style":930},[267,973],{"x":851,"y":960,"width":852,"height":305,"rx":270,"fill":273,"stroke":281,"style":282},[288,975,366],{"x":855,"y":963,"style":336},[267,977],{"x":859,"y":960,"width":860,"height":305,"rx":270,"fill":926,"stroke":927,"style":315},[288,979,270],{"x":863,"y":963,"style":930},[267,981],{"x":867,"y":960,"width":868,"height":305,"rx":270,"fill":926,"stroke":927,"style":315},[288,983,270],{"x":871,"y":963,"style":930},[33,985,987],{"id":986},"step-4-three-dimensions-and-the-size-check","Step 4 — Three dimensions, and the size check",[10,989,990,991,994],{},"Adding months turns the grid into store × product × month. ",[22,992,993],{},"MultiIndex.from_product"," builds the combinations directly from lists:",[38,996,998],{"className":77,"code":997,"language":79,"meta":43,"style":43},"months = pd.period_range(\"2026-07\", \"2026-09\", freq=\"M\").astype(str)\nidx = pd.MultiIndex.from_product([stores[\"Store\"], products[\"Product\"], months], names=[\"Store\", \"Product\", \"Month\"])\ncube = idx.to_frame(index=False)\nprint(len(cube), \"rows\")\n\ndef cross_size(*lists):\n    n = 1\n    for lst in lists:\n        n *= len(lst)\n    return n\n\nprint(\"estimated rows for 500 stores × 20,000 products × 36 months:\", f\"{cross_size(range(500), range(20000), range(36)):,}\")\n",[22,999,1000,1035,1075,1093,1108,1112,1129,1139,1154,1169,1178,1183],{"__ignoreMap":43},[47,1001,1002,1005,1007,1010,1013,1015,1018,1020,1023,1025,1028,1030,1033],{"class":49,"line":50},[47,1003,1004],{"class":90},"months ",[47,1006,113],{"class":86},[47,1008,1009],{"class":90}," pd.period_range(",[47,1011,1012],{"class":57},"\"2026-07\"",[47,1014,122],{"class":90},[47,1016,1017],{"class":57},"\"2026-09\"",[47,1019,122],{"class":90},[47,1021,1022],{"class":125},"freq",[47,1024,113],{"class":86},[47,1026,1027],{"class":57},"\"M\"",[47,1029,622],{"class":90},[47,1031,1032],{"class":131},"str",[47,1034,135],{"class":90},[47,1036,1037,1040,1042,1045,1047,1049,1051,1054,1057,1059,1061,1063,1065,1067,1069,1072],{"class":49,"line":100},[47,1038,1039],{"class":90},"idx ",[47,1041,113],{"class":86},[47,1043,1044],{"class":90}," pd.MultiIndex.from_product([stores[",[47,1046,569],{"class":57},[47,1048,741],{"class":90},[47,1050,574],{"class":57},[47,1052,1053],{"class":90},"], months], ",[47,1055,1056],{"class":125},"names",[47,1058,113],{"class":86},[47,1060,566],{"class":90},[47,1062,569],{"class":57},[47,1064,122],{"class":90},[47,1066,574],{"class":57},[47,1068,122],{"class":90},[47,1070,1071],{"class":57},"\"Month\"",[47,1073,1074],{"class":90},"])\n",[47,1076,1077,1080,1082,1085,1087,1089,1091],{"class":49,"line":107},[47,1078,1079],{"class":90},"cube ",[47,1081,113],{"class":86},[47,1083,1084],{"class":90}," idx.to_frame(",[47,1086,196],{"class":125},[47,1088,113],{"class":86},[47,1090,201],{"class":131},[47,1092,135],{"class":90},[47,1094,1095,1097,1099,1101,1104,1106],{"class":49,"line":138},[47,1096,190],{"class":131},[47,1098,212],{"class":90},[47,1100,215],{"class":131},[47,1102,1103],{"class":90},"(cube), ",[47,1105,241],{"class":57},[47,1107,135],{"class":90},[47,1109,1110],{"class":49,"line":166},[47,1111,104],{"emptyLinePlaceholder":103},[47,1113,1114,1117,1121,1123,1126],{"class":49,"line":187},[47,1115,1116],{"class":86},"def",[47,1118,1120],{"class":1119},"s_Opv"," cross_size",[47,1122,212],{"class":90},[47,1124,1125],{"class":86},"*",[47,1127,1128],{"class":90},"lists):\n",[47,1130,1131,1134,1136],{"class":49,"line":207},[47,1132,1133],{"class":90},"    n ",[47,1135,113],{"class":86},[47,1137,1138],{"class":131}," 1\n",[47,1140,1142,1145,1148,1151],{"class":49,"line":1141},8,[47,1143,1144],{"class":86},"    for",[47,1146,1147],{"class":90}," lst ",[47,1149,1150],{"class":86},"in",[47,1152,1153],{"class":90}," lists:\n",[47,1155,1157,1160,1163,1166],{"class":49,"line":1156},9,[47,1158,1159],{"class":90},"        n ",[47,1161,1162],{"class":86},"*=",[47,1164,1165],{"class":131}," len",[47,1167,1168],{"class":90},"(lst)\n",[47,1170,1172,1175],{"class":49,"line":1171},10,[47,1173,1174],{"class":86},"    return",[47,1176,1177],{"class":90}," n\n",[47,1179,1181],{"class":49,"line":1180},11,[47,1182,104],{"emptyLinePlaceholder":103},[47,1184,1186,1188,1190,1193,1195,1198,1201,1205,1208,1211,1213,1216,1219,1221,1223,1226,1228,1230,1232,1235,1238,1241,1244,1246],{"class":49,"line":1185},12,[47,1187,190],{"class":131},[47,1189,212],{"class":90},[47,1191,1192],{"class":57},"\"estimated rows for 500 stores × 20,000 products × 36 months:\"",[47,1194,122],{"class":90},[47,1196,1197],{"class":86},"f",[47,1199,1200],{"class":57},"\"",[47,1202,1204],{"class":1203},"sSjpA","{",[47,1206,1207],{"class":90},"cross_size(",[47,1209,1210],{"class":131},"range",[47,1212,212],{"class":90},[47,1214,1215],{"class":131},"500",[47,1217,1218],{"class":90},"), ",[47,1220,1210],{"class":131},[47,1222,212],{"class":90},[47,1224,1225],{"class":131},"20000",[47,1227,1218],{"class":90},[47,1229,1210],{"class":131},[47,1231,212],{"class":90},[47,1233,1234],{"class":131},"36",[47,1236,1237],{"class":90},"))",[47,1239,1240],{"class":86},":,",[47,1242,1243],{"class":1203},"}",[47,1245,1200],{"class":57},[47,1247,135],{"class":90},[10,1249,1250,1253,1254,1256],{},[22,1251,1252],{},"from_product"," and chained cross joins give the same rows; ",[22,1255,1252],{}," is convenient when the inputs are plain lists. The size check is not decoration: the row count is the product of the list lengths, so a realistic retailer with 500 stores, 20,000 products and three years of months produces 360 million rows — far beyond what a laptop or an Excel sheet can hold. Always estimate before building, and narrow the inputs (active products only, one year) when the number is large.",[248,1258,257,1264,257,1267,257,1270,257,1274,257,1278,257,1284,257,1288,257,1293,257,1296,257,1300,257,1302,257,1304,257,1306,257,1310,257,1312,257,1316,257,1319,257,1323,257,1328,257,1333],{"viewBox":1259,"role":251,"ariaLabelledBy":1260,"xmlns":255,"style":1263},"4 0 744 206",[1261,1262],"cj-size-t","cj-size-d","width:100%;max-width:744px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[259,1265,1266],{"id":1261},"How fast a cross join grows",[263,1268,1269],{"id":1262},"Row counts for cross joins of increasing size. Three stores by four products give 12 rows. Adding three months gives 36. Fifty stores by 2,000 products by 12 months give 1.2 million. Five hundred stores by 20,000 products by 36 months give 360 million.",[267,1271],{"x":366,"y":270,"width":1272,"height":1273,"fill":273},"744","206",[267,1275],{"x":1276,"y":834,"width":1277,"height":305,"rx":492,"fill":280,"stroke":281,"style":282},"20.0","149.2",[288,1279,1283],{"x":1280,"y":1281,"style":1282},"94.6","34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","3 × 4",[267,1285],{"x":1286,"y":834,"width":1287,"height":305,"rx":492,"fill":426,"stroke":427,"style":315},"179.2","6.0",[288,1289,1292],{"x":1290,"y":1281,"style":1291},"193.2","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","12",[267,1294],{"x":1276,"y":1295,"width":1277,"height":305,"rx":492,"fill":280,"stroke":281,"style":282},"56.0",[288,1297,1299],{"x":1280,"y":1298,"style":1282},"74.5","3 × 4 × 3 months",[267,1301],{"x":1286,"y":1295,"width":1287,"height":305,"rx":492,"fill":426,"stroke":427,"style":315},[288,1303,1234],{"x":1290,"y":1298,"style":1291},[267,1305],{"x":1276,"y":933,"width":1277,"height":305,"rx":492,"fill":280,"stroke":281,"style":282},[288,1307,1309],{"x":1280,"y":1308,"style":1282},"114.5","50 × 2,000 × 12",[267,1311],{"x":1286,"y":933,"width":1287,"height":305,"rx":492,"fill":926,"stroke":927,"style":315},[288,1313,1315],{"x":1290,"y":1308,"style":1314},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","1.2 million",[267,1317],{"x":1276,"y":1318,"width":1277,"height":305,"rx":492,"fill":280,"stroke":281,"style":282},"136.0",[288,1320,1322],{"x":1280,"y":1321,"style":1282},"154.5","500 × 20,000 × 36",[267,1324],{"x":1286,"y":1318,"width":1325,"height":305,"rx":492,"fill":1326,"stroke":1327,"style":315},"476.3","#fce9e9","var(--danger,#dc2626)",[288,1329,1332],{"x":1330,"y":1321,"style":1331},"663.5","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","360 million",[288,1334,1338],{"x":1335,"y":1336,"style":1337},"380.0","186.0","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","The bars are to scale — the small grids are invisible next to the large one.",[33,1340,1342],{"id":1341},"step-5-remove-combinations-that-cannot-happen","Step 5 — Remove combinations that cannot happen",[10,1344,1345],{},"Not every pair is valid: garden products are not ranged in the city-centre store, a course applies only to some roles. Filter the grid with a rule table, so the zeros that remain are real gaps rather than impossibilities:",[38,1347,1349],{"className":77,"code":1348,"language":79,"meta":43,"style":43},"not_ranged = pd.DataFrame({\"Region\": [\"East\"], \"Category\": [\"Garden\"]})\nvalid = (grid.merge(not_ranged.assign(Excluded=True), on=[\"Region\", \"Category\"], how=\"left\")\n             .query(\"Excluded != True\")\n             .drop(columns=\"Excluded\"))\nvalid_full = valid.merge(sales, on=[\"Store\", \"Product\"], how=\"left\").fillna({\"Units\": 0})\nprint(len(grid), \"combinations,\", len(valid), \"valid\")\nprint(valid_full.loc[valid_full[\"Units\"] == 0, [\"Store\", \"Product\"]].to_string(index=False))\n",[22,1350,1351,1383,1425,1435,1449,1492,1517],{"__ignoreMap":43},[47,1352,1353,1356,1358,1361,1364,1367,1370,1372,1375,1377,1380],{"class":49,"line":50},[47,1354,1355],{"class":90},"not_ranged ",[47,1357,113],{"class":86},[47,1359,1360],{"class":90}," pd.DataFrame({",[47,1362,1363],{"class":57},"\"Region\"",[47,1365,1366],{"class":90},": [",[47,1368,1369],{"class":57},"\"East\"",[47,1371,577],{"class":90},[47,1373,1374],{"class":57},"\"Category\"",[47,1376,1366],{"class":90},[47,1378,1379],{"class":57},"\"Garden\"",[47,1381,1382],{"class":90},"]})\n",[47,1384,1385,1388,1390,1393,1396,1398,1401,1403,1405,1407,1409,1411,1413,1415,1417,1419,1421,1423],{"class":49,"line":100},[47,1386,1387],{"class":90},"valid ",[47,1389,113],{"class":86},[47,1391,1392],{"class":90}," (grid.merge(not_ranged.assign(",[47,1394,1395],{"class":125},"Excluded",[47,1397,113],{"class":86},[47,1399,1400],{"class":131},"True",[47,1402,1218],{"class":90},[47,1404,561],{"class":125},[47,1406,113],{"class":86},[47,1408,566],{"class":90},[47,1410,1363],{"class":57},[47,1412,122],{"class":90},[47,1414,1374],{"class":57},[47,1416,577],{"class":90},[47,1418,177],{"class":125},[47,1420,113],{"class":86},[47,1422,584],{"class":57},[47,1424,135],{"class":90},[47,1426,1427,1430,1433],{"class":49,"line":107},[47,1428,1429],{"class":90},"             .query(",[47,1431,1432],{"class":57},"\"Excluded != True\"",[47,1434,135],{"class":90},[47,1436,1437,1440,1442,1444,1447],{"class":49,"line":138},[47,1438,1439],{"class":90},"             .drop(",[47,1441,720],{"class":125},[47,1443,113],{"class":86},[47,1445,1446],{"class":57},"\"Excluded\"",[47,1448,204],{"class":90},[47,1450,1451,1454,1456,1459,1461,1463,1465,1467,1469,1471,1473,1475,1477,1479,1482,1484,1487,1489],{"class":49,"line":166},[47,1452,1453],{"class":90},"valid_full ",[47,1455,113],{"class":86},[47,1457,1458],{"class":90}," valid.merge(sales, ",[47,1460,561],{"class":125},[47,1462,113],{"class":86},[47,1464,566],{"class":90},[47,1466,569],{"class":57},[47,1468,122],{"class":90},[47,1470,574],{"class":57},[47,1472,577],{"class":90},[47,1474,177],{"class":125},[47,1476,113],{"class":86},[47,1478,584],{"class":57},[47,1480,1481],{"class":90},").fillna({",[47,1483,604],{"class":57},[47,1485,1486],{"class":90},": ",[47,1488,270],{"class":131},[47,1490,1491],{"class":90},"})\n",[47,1493,1494,1496,1498,1500,1502,1505,1507,1509,1512,1515],{"class":49,"line":187},[47,1495,190],{"class":131},[47,1497,212],{"class":90},[47,1499,215],{"class":131},[47,1501,238],{"class":90},[47,1503,1504],{"class":57},"\"combinations,\"",[47,1506,122],{"class":90},[47,1508,215],{"class":131},[47,1510,1511],{"class":90},"(valid), ",[47,1513,1514],{"class":57},"\"valid\"",[47,1516,135],{"class":90},[47,1518,1519,1521,1524,1526,1528,1530,1532,1534,1536,1538,1540,1543,1545,1547,1549],{"class":49,"line":207},[47,1520,190],{"class":131},[47,1522,1523],{"class":90},"(valid_full.loc[valid_full[",[47,1525,604],{"class":57},[47,1527,607],{"class":90},[47,1529,665],{"class":86},[47,1531,668],{"class":131},[47,1533,774],{"class":90},[47,1535,569],{"class":57},[47,1537,122],{"class":90},[47,1539,574],{"class":57},[47,1541,1542],{"class":90},"]].to_string(",[47,1544,196],{"class":125},[47,1546,113],{"class":86},[47,1548,201],{"class":131},[47,1550,204],{"class":90},[10,1552,1553],{},"Hull is in the East region, where garden products are not ranged, so its hose and rake combinations are removed before the gap analysis; its missing drill sales remain flagged, because drills should be stocked there. Keeping exclusions in a small table — rather than conditions in code — lets the business maintain them.",[33,1555,1557],{"id":1556},"step-6-write-the-grid-to-excel","Step 6 — Write the grid to Excel",[38,1559,1561],{"className":77,"code":1560,"language":79,"meta":43,"style":43},"with pd.ExcelWriter(\"store_product_grid.xlsx\", engine=\"xlsxwriter\") as xw:\n    matrix.to_excel(xw, sheet_name=\"Matrix\")\n    valid_full.to_excel(xw, sheet_name=\"Valid pairs\", index=False)\n    ws = xw.sheets[\"Matrix\"]\n    zero = xw.book.add_format({\"bg_color\": \"#FDEFD8\", \"font_color\": \"#7A4E06\"})\n    ws.conditional_format(1, 1, len(matrix), len(matrix.columns), {\"type\": \"cell\", \"criteria\": \"==\", \"value\": 0, \"format\": zero})\nwith pd.ExcelFile(\"store_product_grid.xlsx\") as xf:\n    print(\"written:\", xf.sheet_names)\n",[22,1562,1563,1592,1606,1628,1642,1672,1730,1746],{"__ignoreMap":43},[47,1564,1565,1568,1571,1574,1576,1579,1581,1584,1587,1589],{"class":49,"line":50},[47,1566,1567],{"class":86},"with",[47,1569,1570],{"class":90}," pd.ExcelWriter(",[47,1572,1573],{"class":57},"\"store_product_grid.xlsx\"",[47,1575,122],{"class":90},[47,1577,1578],{"class":125},"engine",[47,1580,113],{"class":86},[47,1582,1583],{"class":57},"\"xlsxwriter\"",[47,1585,1586],{"class":90},") ",[47,1588,94],{"class":86},[47,1590,1591],{"class":90}," xw:\n",[47,1593,1594,1597,1599,1601,1604],{"class":49,"line":100},[47,1595,1596],{"class":90},"    matrix.to_excel(xw, ",[47,1598,126],{"class":125},[47,1600,113],{"class":86},[47,1602,1603],{"class":57},"\"Matrix\"",[47,1605,135],{"class":90},[47,1607,1608,1611,1613,1615,1618,1620,1622,1624,1626],{"class":49,"line":107},[47,1609,1610],{"class":90},"    valid_full.to_excel(xw, ",[47,1612,126],{"class":125},[47,1614,113],{"class":86},[47,1616,1617],{"class":57},"\"Valid pairs\"",[47,1619,122],{"class":90},[47,1621,196],{"class":125},[47,1623,113],{"class":86},[47,1625,201],{"class":131},[47,1627,135],{"class":90},[47,1629,1630,1633,1635,1638,1640],{"class":49,"line":138},[47,1631,1632],{"class":90},"    ws ",[47,1634,113],{"class":86},[47,1636,1637],{"class":90}," xw.sheets[",[47,1639,1603],{"class":57},[47,1641,163],{"class":90},[47,1643,1644,1647,1649,1652,1655,1657,1660,1662,1665,1667,1670],{"class":49,"line":166},[47,1645,1646],{"class":90},"    zero ",[47,1648,113],{"class":86},[47,1650,1651],{"class":90}," xw.book.add_format({",[47,1653,1654],{"class":57},"\"bg_color\"",[47,1656,1486],{"class":90},[47,1658,1659],{"class":57},"\"#FDEFD8\"",[47,1661,122],{"class":90},[47,1663,1664],{"class":57},"\"font_color\"",[47,1666,1486],{"class":90},[47,1668,1669],{"class":57},"\"#7A4E06\"",[47,1671,1491],{"class":90},[47,1673,1674,1677,1679,1681,1683,1685,1687,1690,1692,1695,1698,1700,1703,1705,1708,1710,1713,1715,1718,1720,1722,1724,1727],{"class":49,"line":187},[47,1675,1676],{"class":90},"    ws.conditional_format(",[47,1678,310],{"class":131},[47,1680,122],{"class":90},[47,1682,310],{"class":131},[47,1684,122],{"class":90},[47,1686,215],{"class":131},[47,1688,1689],{"class":90},"(matrix), ",[47,1691,215],{"class":131},[47,1693,1694],{"class":90},"(matrix.columns), {",[47,1696,1697],{"class":57},"\"type\"",[47,1699,1486],{"class":90},[47,1701,1702],{"class":57},"\"cell\"",[47,1704,122],{"class":90},[47,1706,1707],{"class":57},"\"criteria\"",[47,1709,1486],{"class":90},[47,1711,1712],{"class":57},"\"==\"",[47,1714,122],{"class":90},[47,1716,1717],{"class":57},"\"value\"",[47,1719,1486],{"class":90},[47,1721,270],{"class":131},[47,1723,122],{"class":90},[47,1725,1726],{"class":57},"\"format\"",[47,1728,1729],{"class":90},": zero})\n",[47,1731,1732,1734,1737,1739,1741,1743],{"class":49,"line":207},[47,1733,1567],{"class":86},[47,1735,1736],{"class":90}," pd.ExcelFile(",[47,1738,1573],{"class":57},[47,1740,1586],{"class":90},[47,1742,94],{"class":86},[47,1744,1745],{"class":90}," xf:\n",[47,1747,1748,1751,1753,1756],{"class":49,"line":1141},[47,1749,1750],{"class":131},"    print",[47,1752,212],{"class":90},[47,1754,1755],{"class":57},"\"written:\"",[47,1757,1758],{"class":90},", xf.sheet_names)\n",[10,1760,1761,1762,31],{},"The matrix sheet highlights the zero cells; the long sheet lists every valid pair with its units, ready for filtering. Highlighting rules are covered in ",[27,1763,1765],{"href":1764},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-above-a-threshold-with-openpyxl\u002F","Highlight Cells Above a Threshold with openpyxl",[33,1767,1769],{"id":1768},"step-7-the-same-pattern-for-a-training-compliance-grid","Step 7 — The same pattern for a training-compliance grid",[10,1771,1772],{},"Swap stores for employees and products for mandatory courses and the code is unchanged — only the meaning of a gap changes. Here a missing combination is not a zero but a course nobody has completed:",[38,1774,1776],{"className":77,"code":1775,"language":79,"meta":43,"style":43},"staff = pd.DataFrame({\"Employee\": [\"Adaeze\", \"Jonas\", \"Claire\"]})\ncourses = pd.DataFrame({\"Course\": [\"Fire safety\", \"Data protection\", \"First aid\"]})\ncompleted = pd.DataFrame({\"Employee\": [\"Adaeze\", \"Adaeze\", \"Jonas\"],\n                          \"Course\": [\"Fire safety\", \"Data protection\", \"Fire safety\"],\n                          \"Completed\": pd.to_datetime([\"2026-03-02\", \"2025-06-15\", \"2026-08-20\"])})\nstatus = staff.merge(courses, how=\"cross\").merge(completed, on=[\"Employee\", \"Course\"], how=\"left\")\nexpiry = status[\"Completed\"] + pd.DateOffset(years=1)\nstatus[\"Status\"] = \"not done\"\nstatus.loc[status[\"Completed\"].notna(), \"Status\"] = \"valid\"\nstatus.loc[status[\"Completed\"].notna() & (expiry \u003C pd.Timestamp(\"2026-10-02\")), \"Status\"] = \"expired\"\nprint(status.pivot(index=\"Employee\", columns=\"Course\", values=\"Status\"))\n",[22,1777,1778,1807,1836,1862,1881,1905,1946,1976,1991,2010,2046],{"__ignoreMap":43},[47,1779,1780,1783,1785,1787,1790,1792,1795,1797,1800,1802,1805],{"class":49,"line":50},[47,1781,1782],{"class":90},"staff ",[47,1784,113],{"class":86},[47,1786,1360],{"class":90},[47,1788,1789],{"class":57},"\"Employee\"",[47,1791,1366],{"class":90},[47,1793,1794],{"class":57},"\"Adaeze\"",[47,1796,122],{"class":90},[47,1798,1799],{"class":57},"\"Jonas\"",[47,1801,122],{"class":90},[47,1803,1804],{"class":57},"\"Claire\"",[47,1806,1382],{"class":90},[47,1808,1809,1812,1814,1816,1819,1821,1824,1826,1829,1831,1834],{"class":49,"line":100},[47,1810,1811],{"class":90},"courses ",[47,1813,113],{"class":86},[47,1815,1360],{"class":90},[47,1817,1818],{"class":57},"\"Course\"",[47,1820,1366],{"class":90},[47,1822,1823],{"class":57},"\"Fire safety\"",[47,1825,122],{"class":90},[47,1827,1828],{"class":57},"\"Data protection\"",[47,1830,122],{"class":90},[47,1832,1833],{"class":57},"\"First aid\"",[47,1835,1382],{"class":90},[47,1837,1838,1841,1843,1845,1847,1849,1851,1853,1855,1857,1859],{"class":49,"line":107},[47,1839,1840],{"class":90},"completed ",[47,1842,113],{"class":86},[47,1844,1360],{"class":90},[47,1846,1789],{"class":57},[47,1848,1366],{"class":90},[47,1850,1794],{"class":57},[47,1852,122],{"class":90},[47,1854,1794],{"class":57},[47,1856,122],{"class":90},[47,1858,1799],{"class":57},[47,1860,1861],{"class":90},"],\n",[47,1863,1864,1867,1869,1871,1873,1875,1877,1879],{"class":49,"line":138},[47,1865,1866],{"class":57},"                          \"Course\"",[47,1868,1366],{"class":90},[47,1870,1823],{"class":57},[47,1872,122],{"class":90},[47,1874,1828],{"class":57},[47,1876,122],{"class":90},[47,1878,1823],{"class":57},[47,1880,1861],{"class":90},[47,1882,1883,1886,1889,1892,1894,1897,1899,1902],{"class":49,"line":166},[47,1884,1885],{"class":57},"                          \"Completed\"",[47,1887,1888],{"class":90},": pd.to_datetime([",[47,1890,1891],{"class":57},"\"2026-03-02\"",[47,1893,122],{"class":90},[47,1895,1896],{"class":57},"\"2025-06-15\"",[47,1898,122],{"class":90},[47,1900,1901],{"class":57},"\"2026-08-20\"",[47,1903,1904],{"class":90},"])})\n",[47,1906,1907,1910,1912,1915,1917,1919,1921,1924,1926,1928,1930,1932,1934,1936,1938,1940,1942,1944],{"class":49,"line":187},[47,1908,1909],{"class":90},"status ",[47,1911,113],{"class":86},[47,1913,1914],{"class":90}," staff.merge(courses, ",[47,1916,177],{"class":125},[47,1918,113],{"class":86},[47,1920,182],{"class":57},[47,1922,1923],{"class":90},").merge(completed, ",[47,1925,561],{"class":125},[47,1927,113],{"class":86},[47,1929,566],{"class":90},[47,1931,1789],{"class":57},[47,1933,122],{"class":90},[47,1935,1818],{"class":57},[47,1937,577],{"class":90},[47,1939,177],{"class":125},[47,1941,113],{"class":86},[47,1943,584],{"class":57},[47,1945,135],{"class":90},[47,1947,1948,1951,1953,1956,1959,1961,1964,1967,1970,1972,1974],{"class":49,"line":207},[47,1949,1950],{"class":90},"expiry ",[47,1952,113],{"class":86},[47,1954,1955],{"class":90}," status[",[47,1957,1958],{"class":57},"\"Completed\"",[47,1960,607],{"class":90},[47,1962,1963],{"class":86},"+",[47,1965,1966],{"class":90}," pd.DateOffset(",[47,1968,1969],{"class":125},"years",[47,1971,113],{"class":86},[47,1973,310],{"class":131},[47,1975,135],{"class":90},[47,1977,1978,1981,1984,1986,1988],{"class":49,"line":1141},[47,1979,1980],{"class":90},"status[",[47,1982,1983],{"class":57},"\"Status\"",[47,1985,607],{"class":90},[47,1987,113],{"class":86},[47,1989,1990],{"class":57}," \"not done\"\n",[47,1992,1993,1996,1998,2001,2003,2005,2007],{"class":49,"line":1156},[47,1994,1995],{"class":90},"status.loc[status[",[47,1997,1958],{"class":57},[47,1999,2000],{"class":90},"].notna(), ",[47,2002,1983],{"class":57},[47,2004,607],{"class":90},[47,2006,113],{"class":86},[47,2008,2009],{"class":57}," \"valid\"\n",[47,2011,2012,2014,2016,2019,2022,2025,2028,2031,2034,2037,2039,2041,2043],{"class":49,"line":1171},[47,2013,1995],{"class":90},[47,2015,1958],{"class":57},[47,2017,2018],{"class":90},"].notna() ",[47,2020,2021],{"class":86},"&",[47,2023,2024],{"class":90}," (expiry ",[47,2026,2027],{"class":86},"\u003C",[47,2029,2030],{"class":90}," pd.Timestamp(",[47,2032,2033],{"class":57},"\"2026-10-02\"",[47,2035,2036],{"class":90},")), ",[47,2038,1983],{"class":57},[47,2040,607],{"class":90},[47,2042,113],{"class":86},[47,2044,2045],{"class":57}," \"expired\"\n",[47,2047,2048,2050,2053,2055,2057,2059,2061,2063,2065,2067,2069,2071,2073,2075],{"class":49,"line":1180},[47,2049,190],{"class":131},[47,2051,2052],{"class":90},"(status.pivot(",[47,2054,196],{"class":125},[47,2056,113],{"class":86},[47,2058,1789],{"class":57},[47,2060,122],{"class":90},[47,2062,720],{"class":125},[47,2064,113],{"class":86},[47,2066,1818],{"class":57},[47,2068,122],{"class":90},[47,2070,729],{"class":125},[47,2072,113],{"class":86},[47,2074,1983],{"class":57},[47,2076,204],{"class":90},[10,2078,2079],{},"The grid gives nine employee\u002Fcourse pairs; three have a completion date and six do not. Adaeze's data-protection course from June 2025 has expired a year later, so the status distinguishes \"valid\", \"expired\" and \"not done\" — three different actions for HR. Without the cross join, Claire, who has completed nothing, would not appear in the report at all, which is precisely the person it exists to find.",[33,2081,2083],{"id":2082},"performance-notes","Performance notes",[10,2085,2086],{},"A cross join's cost is its output size: building twelve rows is instant, building ten million takes a few seconds and a few hundred megabytes, and anything near a hundred million rows needs a different approach — aggregate first, restrict the lists, or keep the data in long form and only fill gaps within each store. The left join of facts onto the grid is a hash merge and costs about as much as building the grid. Writing a matrix to Excel is limited by the sheet: 16,384 columns and 1,048,576 rows, which a store × product matrix of a large retailer can exceed in the column direction.",[33,2088,2090],{"id":2089},"common-pitfalls","Common pitfalls",[2092,2093,2094,2102,2112,2118,2124],"ul",{},[2095,2096,2097,2101],"li",{},[2098,2099,2100],"strong",{},"Pivoting without a grid."," Combinations — and whole products — with no data disappear.",[2095,2103,2104,2107,2108,2111],{},[2098,2105,2106],{},"Inner joins onto the grid."," They throw the empty combinations away again; use ",[22,2109,2110],{},"how=\"left\""," from the grid.",[2095,2113,2114,2117],{},[2098,2115,2116],{},"Duplicate facts."," Two sales rows for one pair duplicate grid rows; validate the merge.",[2095,2119,2120,2123],{},[2098,2121,2122],{},"Size explosions."," The row count is the product of the list sizes; estimate first.",[2095,2125,2126,2129],{},[2098,2127,2128],{},"Invalid combinations as gaps."," Exclude pairs that cannot happen before reporting zeros.",[33,2131,2133],{"id":2132},"conclusion","Conclusion",[10,2135,2136,2137,2140,2141,2144],{},"A cross join turns two (or more) lists into every combination: ",[22,2138,2139],{},"left.merge(right, how=\"cross\")"," or ",[22,2142,2143],{},"pd.MultiIndex.from_product([...])",". Left-join the real data onto that grid and fill missing values, so zeros and gaps become visible, pivot it into a matrix in list order, exclude combinations that cannot occur, and estimate the size before building, because cross joins grow multiplicatively.",[33,2146,2148],{"id":2147},"frequently-asked-questions","Frequently asked questions",[10,2150,2151,2154],{},[2098,2152,2153],{},"How do I get every combination of two columns or lists in pandas?","\nUse df1.merge(df2, how=\"cross\"), which pairs every row of df1 with every row of df2. For plain lists, build a MultiIndex with from_product from the two lists and convert it with to_frame(index=False) for the same result.",[10,2156,2157,2160],{},[2098,2158,2159],{},"How do I show missing combinations as zero?","\nBuild the full grid with a cross join, left-join the actual data onto it on the key columns, and fill the missing values with 0.",[10,2162,2163,2166],{},[2098,2164,2165],{},"How big will a cross join be?","\nThe number of rows is the product of the input lengths, so 500 stores and 20,000 products make 10 million rows, and adding 36 months makes 360 million. Estimate before building and narrow the inputs if needed.",[10,2168,2169,2172],{},[2098,2170,2171],{},"Is there an Excel equivalent of a cross join?","\nPower Query can add a custom column that references the other table and expand it, and Excel 365 formulas can build combinations with dynamic arrays. For large lists, building the grid in pandas and writing the result is simpler.",[33,2174,2176],{"id":2175},"related","Related",[2092,2178,2179,2185,2192,2199],{},[2095,2180,2181,2182,2184],{},"Up one level: ",[27,2183,30],{"href":29}," — combining data from several sheets and files.",[2095,2186,2187,2191],{},[27,2188,2190],{"href":2189},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-two-excel-files-on-common-column-python\u002F","Merge Two Excel Files on a Common Column with Python"," — key-based joins.",[2095,2193,2194,2198],{},[27,2195,2197],{"href":2196},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbuild-a-crosstab-from-excel-data-with-pandas\u002F","Build a Crosstab from Excel Data with pandas"," — counting combinations that occur.",[2095,2200,2201,2205],{},[27,2202,2204],{"href":2203},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Ffill-missing-dates-in-a-time-series-with-pandas\u002F","Fill Missing Dates in a Time Series from Excel with pandas"," — the same gap-filling idea along a date axis.",[2207,2208,2209],"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":43,"searchDepth":100,"depth":100,"links":2211},[2212,2213,2214,2215,2216,2217,2218,2219,2220,2221,2222,2223,2224],{"id":35,"depth":100,"text":36},{"id":73,"depth":100,"text":74},{"id":539,"depth":100,"text":540},{"id":690,"depth":100,"text":691},{"id":986,"depth":100,"text":987},{"id":1341,"depth":100,"text":1342},{"id":1556,"depth":100,"text":1557},{"id":1768,"depth":100,"text":1769},{"id":2082,"depth":100,"text":2083},{"id":2089,"depth":100,"text":2090},{"id":2132,"depth":100,"text":2133},{"id":2147,"depth":100,"text":2148},{"id":2175,"depth":100,"text":2176},"2026-10-02","Build every store × product, employee × training course or region × month combination with merge(how=\"cross\"), then left-join the actual data so missing combinations show as zero or \"not done\" instead of disappearing. Covers three or more lists, MultiIndex.from_product, filtering impossible pairs, guarding against size explosions, and writing a complete grid to Excel.","md",[2229,2231,2233,2235],{"q":2153,"a":2230},"Use df1.merge(df2, how=\"cross\"), which pairs every row of df1 with every row of df2. For plain lists, build a MultiIndex with from_product from the two lists and convert it with to_frame(index=False) for the same result.",{"q":2159,"a":2232},"Build the full grid with a cross join, left-join the actual data onto it on the key columns, and fill the missing values with 0.",{"q":2165,"a":2234},"The number of rows is the product of the input lengths, so 500 stores and 20,000 products make 10 million rows, and adding 36 months makes 360 million. Estimate before building and narrow the inputs if needed.",{"q":2171,"a":2236},"Power Query can add a custom column that references the other table and expand it, and Excel 365 formulas can build combinations with dynamic arrays. For large lists, building the grid in pandas and writing the result is simpler.","create every combination of two lists with a cross join",null,{"breadcrumb":2240},[2241,2244,2247],{"name":2242,"item":2243},"Home","\u002F",{"name":2245,"item":2246},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":2248,"item":29},"Merging and Joining Excel DataFrames with Pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fcreate-every-combination-of-two-lists-with-a-cross-join",{"title":2251,"description":2252},"Every Combination of Two Excel Lists with a pandas Cross Join","Generate all combinations of stores and products (or staff and courses) with merge(how=\"cross\") or MultiIndex.from_product, fill missing pairs with zero, add a third dimension, exclude invalid pairs, estimate the result size first, and export the grid.","create-every-combination-of-two-lists-with-a-cross-join","advanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fcreate-every-combination-of-two-lists-with-a-cross-join\u002Findex",[2256,2257],"joins-and-lookups","pandas","how-to","pbRWPRV75WRDmnUQdxxBBqWzNhTwlaJMGm1UC6oq2VI",[2261,2265],{"title":2262,"path":2263,"stem":2264,"children":-1},"Concatenate Excel Sheets with Different Columns","\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fconcatenate-excel-sheets-with-different-columns","advanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fconcatenate-excel-sheets-with-different-columns\u002Findex",{"title":2266,"path":2267,"stem":2268,"children":-1},"Find Rows in One Excel File Missing from Another","\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffind-rows-in-one-excel-file-missing-from-another","advanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffind-rows-in-one-excel-file-missing-from-another\u002Findex",[2270,2272],{"path":2271,"title":2245},"\u002Fadvanced-data-transformation-and-cleaning",{"path":2273,"title":2248},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes",[],[2276,2279,2282,2285,2288,2291],{"path":2277,"title":2278,"kind":2258},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcompare-two-excel-files-for-differences-with-python","Compare Two Excel Files for Differences with Python",{"path":2280,"title":2281,"kind":2258},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconvert-multi-currency-transactions-with-an-fx-rate-table","Convert Multi-Currency Transactions with an FX Rate Table",{"path":2283,"title":2284,"kind":2258},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-differences-between-two-columns-with-openpyxl","Highlight Differences Between Two Columns with openpyxl",{"path":2286,"title":2287,"kind":2258},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fmap-free-text-job-titles-to-standard-categories","Map Free-Text Job Titles to Standard Categories",{"path":2289,"title":2290,"kind":2258},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fstandardise-category-values-with-a-mapping-table","Standardise Category Values with a Mapping Table in pandas",{"path":2292,"title":2293,"kind":2258},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fapproximate-match-vlookup-in-pandas","Approximate-Match VLOOKUP for Tiered Rates in pandas",1790972096195]