This is a static, read-only snapshot — sliders and other controls will not respond.
Back to notebook list
·
Run it yourself
%23%20%2F%2F%2F%20script%0A%23%20%5Btool.marimo.opengraph%5D%0A%23%20title%20%3D%20%22Campaign%20Conversion%22%0A%23%20description%20%3D%20%22Uses%20Polars%20Dataframes%22%0A%23%20image%20%3D%20%22__marimo__%2Fthumbnail-campaign.svg%22%0A%23%20%2F%2F%2F%0A%0Aimport%20marimo%0A%0A__generated_with%20%3D%20%220.24.0%22%0Aapp%20%3D%20marimo.App()%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20marimo%20as%20mo%0A%0A%20%20%20%20return%20(mo%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%20**Campaign%20conversion%20optimization%20using%20Polars%20dataframes**%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20In%20this%20example%2C%20we%20load%20a%20dataset%20with%20customer%20information%20to%20optimize%20campaign%20channel%20assignments%20that%20maximize%20expected%20value%20subject%20to%20budget%20and%20channel%20capacity%20constraints.%20We%20showcase%20the%20capabilities%20of%20the%20Xpress%20Python%20API%20with%20the%20use%20of%20Polars%20dataframes%20to%20generate%20aggregate%20expressions%20for%20optimization.%0A%0A%20%20%20%20%26copy%3B%20Copyright%202025-2026%20Fair%20Isaac%20Corporation.%20The%20use%20of%20this%20example%20is%20subject%20to%20%5Blegal%20and%20license%20requirements%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%23legal-and-license-requirements).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20%23%20Install%20the%20necessary%20packages%0A%20%20%20%20%23%20'%25pip%20install%20-q%20xpress%20polars%20matplotlib%20seaborn%20pyarrow'%20command%20supported%20automatically%20in%20marimo%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Problem%20description%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20In%20this%20**campaign%20conversion%20optimization**%20problem%2C%20we%20want%20to%20contact%20customers%20through%20different%20channels%20(SMS%2C%20Mail%2C%20Phone)%20to%20maximize%20the%20total%20expected%20value%20from%20contacts.%20Each%20customer%20can%20be%20contacted%20through%20at%20most%20one%20channel%2C%20and%20we%20need%20to%20decide%20which%20channel%20to%20use%20for%20each%20customer.%0A%0A%20%20%20%20Customer%20data%20includes%20expected%20value%20and%20propensity%20to%20convert%20for%20each%20channel.%20Channel%20data%20includes%20contact%20costs%20and%20capacity%20constraints.%20**The%20optimized%20campaign%20must%20satisfy%20the%20following%20conditions**%3A%0A%0A%20%20%20%20*%20Each%20customer%20is%20contacted%20through%20at%20most%20one%20channel.%0A%20%20%20%20*%20The%20total%20campaign%20cost%20must%20not%20exceed%20the%20available%20budget.%0A%20%20%20%20*%20Each%20channel%20has%20a%20capacity%20limit%20(maximum%20percentage%20of%20customers%20that%20can%20be%20contacted).%0A%0A%20%20%20%20The%20input%20data%20file%20**%5Bcustomers1000.csv%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Fblob%2Fmain%2Fmodeling_examples%2Fdata%2Fcustomers1000.csv)**%20provides%20customer%20data%20with%20the%20following%20fields%3A%0A%0A%20%20%20%20*%20*CustomerIds*%3A%20Unique%20customer%20identifier.%0A%20%20%20%20*%20*Name*%3A%20Customer%20name.%0A%20%20%20%20*%20*Value*%3A%20Expected%20customer%20value.%0A%20%20%20%20*%20*PropSMS*%3A%20Propensity%20to%20convert%20via%20SMS.%0A%20%20%20%20*%20*PropMail*%3A%20Propensity%20to%20convert%20via%20Mail.%0A%20%20%20%20*%20*PropPhone*%3A%20Propensity%20to%20convert%20via%20Phone.%0A%0A%20%20%20%20The%20goal%20is%20to%20**maximize%20the%20total%20expected%20value%20while%20satisfying%20budget%20and%20channel%20capacity%20constraints**.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Model%20parameters%0A%0A%20%20%20%20**You%20can%20adjust%20the%20parameters%20below%20to%20change%20the%20problem%20instance%20and%20re-solve%20the%20model%20automatically.**%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20num_customers_slider%20%3D%20mo.ui.slider(200%2C%201000%2C%20value%3D1000%2C%20step%3D100%2C%20label%3D%22Number%20of%20customers%20to%20load%22%2C%20show_value%3DTrue)%0A%20%20%20%20budget_slider%20%3D%20mo.ui.slider(200%2C%203000%2C%20value%3D1000%2C%20step%3D100%2C%20label%3D%22Total%20campaign%20budget%20(%24)%22%2C%20show_value%3DTrue)%0A%20%20%20%20capacity_scale_slider%20%3D%20mo.ui.slider(0.5%2C%202.0%2C%20value%3D1.0%2C%20step%3D0.1%2C%20label%3D%22Channel%20capacity%20scale%20factor%22%2C%20show_value%3DTrue)%0A%20%20%20%20goal_selector%20%3D%20mo.ui.radio(%0A%20%20%20%20%20%20%20%20options%3D%7B%22Maximize%20total%20value%22%3A%20%22value%22%2C%20%22Maximize%20total%20propensity%22%3A%20%22propensity%22%2C%20%22Balanced%20(value%20%2B%20propensity)%22%3A%20%22balanced%22%7D%2C%0A%20%20%20%20%20%20%20%20value%3D%22Maximize%20total%20value%22%2C%0A%20%20%20%20%20%20%20%20label%3D%22Goal%22%0A%20%20%20%20)%0A%20%20%20%20mo.vstack(%5B%0A%20%20%20%20%20%20%20%20mo.hstack(%5Bnum_customers_slider%2C%20budget_slider%2C%20capacity_scale_slider%5D)%2C%0A%20%20%20%20%20%20%20%20goal_selector%2C%0A%20%20%20%20%5D)%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20budget_slider%2C%0A%20%20%20%20%20%20%20%20capacity_scale_slider%2C%0A%20%20%20%20%20%20%20%20goal_selector%2C%0A%20%20%20%20%20%20%20%20num_customers_slider%2C%0A%20%20%20%20)%0A%0A%0A%40app.cell%0Adef%20_(goal_selector%2C%20mo)%3A%0A%20%20%20%20%23%20Only%20used%20when%20Goal%20is%20set%20to%20%22Balanced%22%20-%20disabled%20otherwise%20to%20make%20that%20dependency%20clear%0A%20%20%20%20ratio_balance_slider%20%3D%20mo.ui.slider(%0A%20%20%20%20%20%20%20%200%2C%20100%2C%20value%3D50%2C%20step%3D5%2C%0A%20%20%20%20%20%20%20%20label%3D%22Balance%20ratio%20(%25%20weight%20on%20value)%20-%20applies%20only%20when%20Goal%20is%20set%20to%20Balanced%22%2C%0A%20%20%20%20%20%20%20%20show_value%3DTrue%2C%0A%20%20%20%20%20%20%20%20disabled%3Dgoal_selector.value%20!%3D%20%22balanced%22%2C%0A%20%20%20%20)%0A%20%20%20%20ratio_balance_slider%0A%20%20%20%20return%20(ratio_balance_slider%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Data%20preparation%20and%20analysis%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20We%20start%20by%20importing%20the%20essential%20libraries%20for%20optimization%20(%60xpress%60)%2C%20data%20manipulation%20(%60polars%60)%2C%20and%20visualization%20(%60matplotlib%60%2C%20%60seaborn%60).%0A%0A%20%20%20%20After%20defining%20the%20values%20for%20the%20constants%20needed%20for%20the%20mathematical%20model%2C%20we%20load%20the%20customer%20dataset%20from%20the%20CSV%20file%20into%20a%20**Polars%20dataframe**.%20Then%2C%20we%20display%20the%20first%20five%20rows%20to%20give%20a%20quick%20overview%20of%20the%20data%20structure%20and%20contents.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(budget_slider%2C%20mo%2C%20num_customers_slider)%3A%0A%20%20%20%20import%20xpress%20as%20xp%0A%20%20%20%20import%20polars%20as%20pl%0A%20%20%20%20import%20matplotlib.pyplot%20as%20plt%0A%20%20%20%20import%20seaborn%20as%20sns%0A%0A%20%20%20%20NUM_CUSTOMERS%20%3D%20num_customers_slider.value%20%20%20%20%20%20%23%20Number%20of%20customers%20to%20load%2C%20must%20be%20%3C%3D%201000%0A%20%20%20%20BUDGET%20%3D%20budget_slider.value%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Total%20campaign%20budget%0A%0A%20%20%20%20%23%20Load%20the%20customer%20dataset%20(resolved%20relative%20to%20this%20notebook's%20own%20directory%2C%0A%20%20%20%20%23%20so%20it%20works%20regardless%20of%20the%20current%20working%20directory)%0A%20%20%20%20customers%20%3D%20pl.read_csv(mo.notebook_dir()%20%2F%20%22data%22%20%2F%20%22customers1000.csv%22%2C%20n_rows%3DNUM_CUSTOMERS)%0A%0A%20%20%20%20%23%20Customer%20data%20overview%0A%20%20%20%20mo.show_code(mo.vstack(%5Bmo.md(%22**Data%20sample%20for%20the%20first%205%20rows%3A**%22)%2C%20customers.head()%5D)%2C%20position%3D%22above%22)%0A%20%20%20%20return%20BUDGET%2C%20customers%2C%20pl%2C%20plt%2C%20sns%2C%20xp%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20code%20below%20creates%20channel%20data%20and%20displays%20basic%20statistics%20about%20customers%20and%20their%20propensities%20across%20different%20channels.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(customers%2C%20mo%2C%20pl)%3A%0A%20%20%20%20%23%20Create%20channel%20dataframe%20using%20Polars%0A%20%20%20%20channels%20%3D%20pl.from_dict(%7B%0A%20%20%20%20%20%20%20%20%22ChannelIds%22%3A%20%5B0%2C%201%2C%202%5D%2C%0A%20%20%20%20%20%20%20%20%22Name%22%3A%20%5B%22SMS%22%2C%20%22Mail%22%2C%20%22Phone%22%5D%2C%0A%20%20%20%20%20%20%20%20%22Cost%22%3A%20%5B0.5%2C%201.2%2C%203.0%5D%2C%0A%20%20%20%20%20%20%20%20%22Capacity%22%3A%20%5B100%2C%2050%2C%2040%5D%20%20%20%20%20%20%20%23%20Percentage%20of%20total%20customers%0A%20%20%20%20%7D)%0A%0A%20%20%20%20mo.show_code(mo.vstack(%5B%0A%20%20%20%20%20%20%20%20mo.md(%22**Channel%20information%3A**%22)%2C%0A%20%20%20%20%20%20%20%20channels%2C%0A%20%20%20%20%20%20%20%20mo.md(f%22%22%22%0A%20%20%20%20%20%20%20%20**Customer%20statistics%3A**%0A%0A%20%20%20%20%20%20%20%20Total%20customers%3A%20%7Blen(customers)%7D%20%26nbsp%3B%26nbsp%3B%20Average%20value%3A%20%24%7Bcustomers%5B'Value'%5D.mean()%3A.2f%7D%20%26nbsp%3B%26nbsp%3B%20Min%20value%3A%20%24%7Bcustomers%5B'Value'%5D.min()%3A.2f%7D%20%26nbsp%3B%26nbsp%3B%20Max%20value%3A%20%24%7Bcustomers%5B'Value'%5D.max()%3A.2f%7D%20%26nbsp%3B%26nbsp%3B%20Median%20value%3A%20%24%7Bcustomers%5B'Value'%5D.median()%3A.2f%7D%0A%0A%20%20%20%20%20%20%20%20**Average%20propensities%3A**%20SMS%3A%20%7Bcustomers%5B'PropSMS'%5D.mean()%3A.4f%7D%20%26nbsp%3B%26nbsp%3B%20Mail%3A%20%7Bcustomers%5B'PropMail'%5D.mean()%3A.4f%7D%20%26nbsp%3B%26nbsp%3B%20Phone%3A%20%7Bcustomers%5B'PropPhone'%5D.mean()%3A.4f%7D%0A%20%20%20%20%20%20%20%20%22%22%22)%2C%0A%20%20%20%20%5D)%2C%20position%3D%22above%22)%0A%20%20%20%20return%20(channels%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20charts%20below%20show%20the%20distribution%20of%20values%20and%20propensities%20across%20customers%2C%20before%20the%20optimization.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(customers%2C%20plt%2C%20sns)%3A%0A%20%20%20%20%23%20Convert%20to%20pandas%20for%20visualization%20purposes%0A%20%20%20%20customers_pd%20%3D%20customers.to_pandas()%0A%0A%20%20%20%20%23%20Create%20visualizations%0A%20%20%20%20fig1%2C%20axes1%20%3D%20plt.subplots(2%2C%202%2C%20figsize%3D(14%2C%208))%0A%20%20%20%20fig1.suptitle('Customer%20Data%20Analysis'%2C%20fontsize%3D16%2C%20fontweight%3D'bold')%0A%0A%20%20%20%20%23%20Plot%201%3A%20Distribution%20of%20Customer%20Values%0A%20%20%20%20ax1%20%3D%20axes1%5B0%2C%200%5D%0A%20%20%20%20sns.histplot(customers_pd%5B'Value'%5D%2C%20bins%3D30%2C%20kde%3DTrue%2C%20ax%3Dax1%2C%20color%3D'%233498db')%0A%20%20%20%20ax1.set_title('Distribution%20of%20Customer%20Values'%2C%20fontweight%3D'bold')%0A%20%20%20%20ax1.set_xlabel('Customer%20Value%20(%24)')%0A%20%20%20%20ax1.set_ylabel('Frequency')%0A%20%20%20%20ax1.axvline(customers_pd%5B'Value'%5D.mean()%2C%20color%3D'red'%2C%20linestyle%3D'--'%2C%20label%3Df'Mean%3A%20%24%7Bcustomers_pd%5B%22Value%22%5D.mean()%3A.0f%7D')%0A%20%20%20%20ax1.legend()%0A%0A%20%20%20%20%23%20Plot%202%3A%20Distribution%20of%20PropSMS%0A%20%20%20%20ax2%20%3D%20axes1%5B0%2C%201%5D%0A%20%20%20%20sns.histplot(customers_pd%5B'PropSMS'%5D%2C%20bins%3D30%2C%20kde%3DTrue%2C%20ax%3Dax2%2C%20color%3D'%23e74c3c')%0A%20%20%20%20ax2.set_title('Distribution%20of%20SMS%20Propensity'%2C%20fontweight%3D'bold')%0A%20%20%20%20ax2.set_xlabel('Propensity')%0A%20%20%20%20ax2.set_ylabel('Frequency')%0A%0A%20%20%20%20%23%20Plot%203%3A%20Distribution%20of%20PropMail%0A%20%20%20%20ax3%20%3D%20axes1%5B1%2C%200%5D%0A%20%20%20%20sns.histplot(customers_pd%5B'PropMail'%5D%2C%20bins%3D30%2C%20kde%3DTrue%2C%20ax%3Dax3%2C%20color%3D'%239b59b6')%0A%20%20%20%20ax3.set_title('Distribution%20of%20Mail%20Propensity'%2C%20fontweight%3D'bold')%0A%20%20%20%20ax3.set_xlabel('Propensity')%0A%20%20%20%20ax3.set_ylabel('Frequency')%0A%0A%20%20%20%20%23%20Plot%204%3A%20Distribution%20of%20PropPhone%0A%20%20%20%20ax4%20%3D%20axes1%5B1%2C%201%5D%0A%20%20%20%20sns.histplot(customers_pd%5B'PropPhone'%5D%2C%20bins%3D30%2C%20kde%3DTrue%2C%20ax%3Dax4%2C%20color%3D'%232ecc71')%0A%20%20%20%20ax4.set_title('Distribution%20of%20Phone%20Propensity'%2C%20fontweight%3D'bold')%0A%20%20%20%20ax4.set_xlabel('Propensity')%0A%20%20%20%20ax4.set_ylabel('Frequency')%0A%0A%20%20%20%20plt.tight_layout(rect%3D(0%2C%200%2C%201%2C%200.97)%2C%20h_pad%3D3.0)%0A%20%20%20%20fig1%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Model%20implementation%20and%20solution%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20We%20define%20binary%20decision%20variables%20%24contact_%7Bi%2Cj%7D%24%20that%20indicate%20whether%20customer%20%24i%20%5Cin%20%5Cmathcal%7BC%7D%24%20is%20contacted%20through%20channel%20%24j%20%5Cin%20%5Cmathcal%7BCH%7D%24%20(%24%3D1%24)%2C%20or%20not%20(%24%3D0%24).%0A%0A%20%20%20%20Since%20Xpress%20variables%20cannot%20be%20directly%20stored%20in%20Polars%20dataframes%2C%20we%20create%20the%20variables%20using%20standard%20Python%20dictionaries%20indexed%20by%20(customer_id%2C%20channel_id)%20tuples.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(channels%2C%20customers%2C%20mo%2C%20xp)%3A%0A%20%20%20%20%23%20Create%20Xpress%20problem%0A%20%20%20%20p%20%3D%20xp.problem(%22Campaign%20Conversion%22)%0A%0A%20%20%20%20%23%20Extract%20index%20sets%0A%20%20%20%20customer_ids%20%3D%20customers%5B%22CustomerIds%22%5D.to_list()%0A%20%20%20%20channel_ids%20%3D%20channels%5B%22ChannelIds%22%5D.to_list()%0A%20%20%20%20num_customers%20%3D%20len(customer_ids)%0A%0A%20%20%20%20%23%20Decision%20variables%3A%20contact%5Bi%2Cj%5D%20%3D%201%20if%20customer%20i%20contacted%20through%20channel%20j%0A%20%20%20%20contact%20%3D%20p.addVariables(customer_ids%2C%20channel_ids%2C%20vartype%3Dxp.binary%2C%20name%3D'contact')%0A%0A%20%20%20%20%23%20Metric%20tracking%20variables%0A%20%20%20%20MetricTotalCost%20%3D%20p.addVariable(name%3D'MetricTotalCost')%0A%20%20%20%20MetricCostPerChannel%20%3D%20p.addVariables(channel_ids%2C%20vartype%3Dxp.continuous%2C%20name%3D'MetricCostPerChannel')%0A%20%20%20%20MetricContactPerChannel%20%3D%20p.addVariables(channel_ids%2C%20vartype%3Dxp.continuous%2C%20name%3D'MetricContactPerChannel')%0A%0A%20%20%20%20mo.show_code(mo.md(f%22%22%22%0A%20%20%20%20Created%20%7Blen(customer_ids)%20*%20len(channel_ids)%7D%20contact%20decision%20variables%20and%20%7Blen(channel_ids)*2%20%2B%201%7D%20auxiliary%20variables%20for%20metric%20tracking.%0A%20%20%20%20%22%22%22))%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20MetricContactPerChannel%2C%0A%20%20%20%20%20%20%20%20MetricCostPerChannel%2C%0A%20%20%20%20%20%20%20%20MetricTotalCost%2C%0A%20%20%20%20%20%20%20%20channel_ids%2C%0A%20%20%20%20%20%20%20%20contact%2C%0A%20%20%20%20%20%20%20%20customer_ids%2C%0A%20%20%20%20%20%20%20%20num_customers%2C%0A%20%20%20%20%20%20%20%20p%2C%0A%20%20%20%20)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20objective%20is%20to%20maximize%20the%20expected%20value%20from%20customer%20contacts%3A%0A%20%20%20%20%24%24%5Cmax%20%5Csum_%7Bi%20%5Cin%20%5Cmathcal%7BC%7D%7D%20%5Csum_%7Bj%20%5Cin%20%5Cmathcal%7BCH%7D%7D%20Value_i%20%5Ccdot%20Prop_%7Bi%2Cj%7D%20%5Ccdot%20Contact_%7Bi%2Cj%7D%24%24%0A%0A%20%20%20%20We%20build%20this%20objective%20by%20iterating%20through%20customers%20and%20channels%2C%20using%20Polars%20filtering%20to%20extract%20the%20relevant%20propensity%20and%20value%20for%20each%20customer-channel%20pair.%0A%0A%20%20%20%20**Polars%20Operations%20for%20Objective%20Function%20Building%3A**%0A%0A%20%20%20%20The%20objective%20function%20uses%20nested%20loops%20with%20Polars%20%60filter()%60%20operations%3A%0A%20%20%20%201.%20For%20each%20channel%20%60j%60%2C%20we%20extract%20the%20channel%20name%3A%20%60channels.filter(pl.col(%22ChannelIds%22)%20%3D%3D%20j)%5B%22Name%22%5D%5B0%5D%60%0A%20%20%20%202.%20For%20each%20customer%20%60i%60%2C%20we%20dynamically%20access%20the%20right%20propensity%20column%20using%20that%20name%3A%20%60customers.filter(pl.col(%22CustomerIds%22)%20%3D%3D%20i)%5Bf%22Prop%7Bchannel_name%7D%22%5D%5B0%5D%60%0A%20%20%20%203.%20We%20retrieve%20the%20customer%20value%3A%20%60customers.filter(pl.col(%22CustomerIds%22)%20%3D%3D%20i)%5B%22Value%22%5D%5B0%5D%60%0A%0A%20%20%20%20This%20pattern%20demonstrates%20how%20Polars%20can%20be%20integrated%20with%20optimization%20modeling%20using%20efficient%20filtering%20to%20construct%20complex%20mathematical%20expressions.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20channel_ids%2C%0A%20%20%20%20channels%2C%0A%20%20%20%20contact%2C%0A%20%20%20%20customer_ids%2C%0A%20%20%20%20customers%2C%0A%20%20%20%20goal_selector%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20p%2C%0A%20%20%20%20pl%2C%0A%20%20%20%20ratio_balance_slider%2C%0A%20%20%20%20xp%2C%0A)%3A%0A%20%20%20%20%23%20Build%20objective%20expression%20using%20Polars%20filtering%0A%20%20%20%20ObjectiveValue%20%3D%200%0A%20%20%20%20ObjectivePropensity%20%3D%200%0A%0A%20%20%20%20for%20j%20in%20channel_ids%3A%0A%20%20%20%20%20%20%20%20%23%20Get%20channel%20name%20using%20Polars%20filter%0A%20%20%20%20%20%20%20%20channel_name%20%3D%20channels.filter(pl.col(%22ChannelIds%22)%20%3D%3D%20j)%5B%22Name%22%5D%5B0%5D%0A%0A%20%20%20%20%20%20%20%20for%20i%20in%20customer_ids%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20Get%20customer%20propensity%20for%20this%20channel%20using%20Polars%20filter%0A%20%20%20%20%20%20%20%20%20%20%20%20customer_prop%20%3D%20customers.filter(pl.col(%22CustomerIds%22)%20%3D%3D%20i)%5Bf%22Prop%7Bchannel_name%7D%22%5D%5B0%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20customer_value%20%3D%20customers.filter(pl.col(%22CustomerIds%22)%20%3D%3D%20i)%5B%22Value%22%5D%5B0%5D%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20ObjectiveValue%20%2B%3D%20contact%5Bi%2C%20j%5D%20*%20customer_prop%20*%20customer_value%0A%20%20%20%20%20%20%20%20%20%20%20%20ObjectivePropensity%20%2B%3D%20contact%5Bi%2C%20j%5D%20*%20customer_prop%0A%0A%20%20%20%20%23%20Set%20objective%20function%20according%20to%20the%20selected%20Goal%20parameter%0A%20%20%20%20RatioBalance%20%3D%20ratio_balance_slider.value%0A%20%20%20%20if%20goal_selector.value%20%3D%3D%20%22value%22%3A%0A%20%20%20%20%20%20%20%20p.setObjective(ObjectiveValue%2C%20sense%3Dxp.ObjSense.MAXIMIZE)%0A%20%20%20%20elif%20goal_selector.value%20%3D%3D%20%22propensity%22%3A%0A%20%20%20%20%20%20%20%20p.setObjective(ObjectivePropensity%2C%20sense%3Dxp.ObjSense.MAXIMIZE)%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20Balanced%20%3D%20(ObjectiveValue%20%2F%2020%20*%20RatioBalance)%20%2F%20100%20%2B%20ObjectivePropensity%20*%20500%20*%20(100%20-%20RatioBalance)%20%2F%20100%0A%20%20%20%20%20%20%20%20p.setObjective(Balanced%2C%20sense%3Dxp.ObjSense.MAXIMIZE)%0A%20%20%20%20mo.show_code()%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Next%20we%20model%20the%20following%20constraints%3A%0A%0A%20%20%20%20*%20Each%20customer%20must%20be%20contacted%20through%20at%20most%20one%20channel%3A%0A%20%20%20%20%24%24%5Csum_%7Bj%20%5Cin%20%5Cmathcal%7BCH%7D%7D%20contact_%7Bi%2Cj%7D%20%5Cleq%201%20%5Cquad%20%5Cforall%20i%20%5Cin%20%5Cmathcal%7BC%7D%24%24%0A%0A%20%20%20%20*%20Budget%20constraint%3A%20total%20cost%20must%20not%20exceed%20budget%3A%0A%20%20%20%20%24%24%5Csum_%7Bi%20%5Cin%20%5Cmathcal%7BC%7D%7D%20%5Csum_%7Bj%20%5Cin%20%5Cmathcal%7BCH%7D%7D%20Cost_j%20%5Ccdot%20contact_%7Bi%2Cj%7D%20%5Cleq%20Budget%24%24%0A%0A%20%20%20%20*%20Channel%20capacity%3A%20number%20of%20customers%20contacted%20per%20channel%20cannot%20exceed%20capacity%3A%0A%20%20%20%20%24%24%5Csum_%7Bi%20%5Cin%20%5Cmathcal%7BC%7D%7D%20contact_%7Bi%2Cj%7D%20%5Cleq%20Capacity_j%20%5Ccdot%20%7C%5Cmathcal%7BC%7D%7C%20%2F%20100%20%5Cquad%20%5Cforall%20j%20%5Cin%20%5Cmathcal%7BCH%7D%24%24%0A%0A%20%20%20%20**Polars%20Operations%20for%20Constraint%20Building%3A**%0A%0A%20%20%20%20The%20constraints%20building%20process%20leverages%20the%20Polars%20%60filter()%60%20method%20to%20extract%20specific%20values%20from%20dataframes.%20For%20example%3A%0A%0A%20%20%20%20*%20%60channels.filter(pl.col(%22ChannelIds%22)%20%3D%3D%20j)%5B%22Cost%22%5D%5B0%5D%60%20retrieves%20the%20cost%20for%20channel%20%60j%60%0A%20%20%20%20*%20%60channels.filter(pl.col(%22ChannelIds%22)%20%3D%3D%20j)%5B%22Capacity%22%5D%5B0%5D%60%20retrieves%20the%20capacity%20limit%20for%20channel%20%60j%60%0A%0A%20%20%20%20This%20approach%20allows%20us%20to%20dynamically%20look%20up%20channel-specific%20parameters%20while%20building%20the%20optimization%20model%2C%20making%20the%20code%20more%20maintainable%20and%20easier%20to%20understand%20compared%20to%20pre-processing%20all%20data%20into%20lists%20or%20dictionaries.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20BUDGET%2C%0A%20%20%20%20MetricContactPerChannel%2C%0A%20%20%20%20MetricCostPerChannel%2C%0A%20%20%20%20MetricTotalCost%2C%0A%20%20%20%20capacity_scale_slider%2C%0A%20%20%20%20channel_ids%2C%0A%20%20%20%20channels%2C%0A%20%20%20%20contact%2C%0A%20%20%20%20customer_ids%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20num_customers%2C%0A%20%20%20%20p%2C%0A%20%20%20%20pl%2C%0A%20%20%20%20xp%2C%0A)%3A%0A%20%20%20%20%23%20Constraint%201%3A%20Each%20customer%20contacted%20through%20at%20most%20one%20channel%0A%20%20%20%20p.addConstraint(xp.Sum(contact%5Bi%2C%20j%5D%20for%20j%20in%20channel_ids)%20%3C%3D%201%20for%20i%20in%20customer_ids)%0A%0A%20%20%20%20%23%20Constraint%202%3A%20Budget%20constraint%0A%20%20%20%20p.addConstraint(MetricTotalCost%20%3C%3D%20BUDGET)%0A%0A%20%20%20%20%23%20Constraint%203%3A%20Channel%20capacity%20limits%20(using%20Polars%20filter)%0A%20%20%20%20p.addConstraint(%0A%20%20%20%20%20%20%20%20xp.Sum(contact%5Bi%2C%20j%5D%20for%20i%20in%20customer_ids)%20%3C%3D%0A%20%20%20%20%20%20%20%20channels.filter(pl.col(%22ChannelIds%22)%20%3D%3D%20j)%5B%22Capacity%22%5D%5B0%5D%20%2F%20100%20*%20num_customers%20*%20capacity_scale_slider.value%0A%20%20%20%20%20%20%20%20for%20j%20in%20channel_ids%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20Constraint%204%3A%20Total%20contacts%20per%20channel%20(metric%20tracking)%0A%20%20%20%20p.addConstraint(%0A%20%20%20%20%20%20%20%20xp.Sum(contact%5Bi%2C%20j%5D%20for%20i%20in%20customer_ids)%20%3D%3D%20MetricContactPerChannel%5Bj%5D%0A%20%20%20%20%20%20%20%20for%20j%20in%20channel_ids%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20Constraint%205%3A%20Total%20cost%20per%20channel%20(metric%20tracking)%0A%20%20%20%20p.addConstraint(%0A%20%20%20%20%20%20%20%20xp.Sum(contact%5Bi%2C%20j%5D%20*%20channels.filter(pl.col(%22ChannelIds%22)%20%3D%3D%20j)%5B%22Cost%22%5D%5B0%5D%0A%20%20%20%20%20%20%20%20for%20i%20in%20customer_ids)%20%3D%3D%20MetricCostPerChannel%5Bj%5D%0A%20%20%20%20%20%20%20%20for%20j%20in%20channel_ids%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20Constraint%206%3A%20Total%20cost%20equals%20sum%20of%20costs%20per%20channel%20(metric%20tracking)%0A%20%20%20%20p.addConstraint(%0A%20%20%20%20%20%20%20%20xp.Sum(contact%5Bi%2C%20j%5D%20*%20channels.filter(pl.col(%22ChannelIds%22)%20%3D%3D%20j)%5B%22Cost%22%5D%5B0%5D%0A%20%20%20%20%20%20%20%20for%20i%20in%20customer_ids%20for%20j%20in%20channel_ids)%20%3D%3D%20MetricTotalCost%0A%20%20%20%20)%0A%0A%20%20%20%20mo.show_code(mo.md(f%22Model%20has%20%7Bp.attributes.rows%7D%20constraints%20and%20%7Bp.attributes.cols%7D%20variables.%22))%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Now%20we%20solve%20the%20problem%20and%20display%20the%20solution%20metrics%20and%20channel%20allocation.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20MetricContactPerChannel%2C%0A%20%20%20%20MetricCostPerChannel%2C%0A%20%20%20%20MetricTotalCost%2C%0A%20%20%20%20channels%2C%0A%20%20%20%20contact%2C%0A%20%20%20%20customers%2C%0A%20%20%20%20mo%2C%0A%20%20%20%20p%2C%0A%20%20%20%20pl%2C%0A%20%20%20%20xp%2C%0A)%3A%0A%20%20%20%20p.controls.outputlog%20%3D%200%20%20%20%20%20%20%20%20%23%20Suppress%20output%20log%0A%0A%20%20%20%20%23%20Solve%20optimization%20problem%0A%20%20%20%20solvestatus%2C%20solstatus%20%3D%20p.optimize()%0A%0A%20%20%20%20if%20solstatus%20in%20(xp.SolStatus.OPTIMAL%2C%20xp.SolStatus.FEASIBLE)%3A%0A%20%20%20%20%20%20%20%20%23%20Process%20solution%20-%20add%20results%20to%20channels%20dataframe%0A%20%20%20%20%20%20%20%20channels_solution%20%3D%20channels.sort(%22ChannelIds%22)%0A%20%20%20%20%20%20%20%20channels_solution%20%3D%20channels_solution.with_columns(%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.Series(name%3D%22MetricCostPerChannel%22%2C%20values%3Dlist(p.getSolution(MetricCostPerChannel).values())%2C%20dtype%3Dpl.Float64)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.Series(name%3D%22MetricContactPerChannel%22%2C%20values%3Dlist(p.getSolution(MetricContactPerChannel).values())%2C%20dtype%3Dpl.Float64)%0A%20%20%20%20%20%20%20%20%5D)%0A%0A%20%20%20%20%20%20%20%20%23%20Process%20solution%20-%20add%20assigned%20actions%20to%20customers%20dataframe%0A%20%20%20%20%20%20%20%20customers_solution%20%3D%20customers.sort(%22CustomerIds%22)%0A%20%20%20%20%20%20%20%20contact_solution%20%3D%20p.getSolution(contact)%0A%20%20%20%20%20%20%20%20list_actions%20%3D%20%5Bkey%5B1%5D%20for%20key%2C%20value%20in%20contact_solution.items()%20if%20value%20%3E%200.5%5D%0A%0A%20%20%20%20%20%20%20%20customers_solution%20%3D%20customers_solution.with_columns(%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.Series(name%3D%22Action%22%2C%20values%3Dlist_actions%2C%20dtype%3Dpl.Int64)%0A%20%20%20%20%20%20%20%20%5D)%0A%0A%20%20%20%20%20%20%20%20%23%20Add%20action%20names%20by%20mapping%20channel%20IDs%20to%20names%0A%20%20%20%20%20%20%20%20list_action_names%20%3D%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20channels.filter(pl.col(%22ChannelIds%22)%20%3D%3D%20action_id)%5B%22Name%22%5D%5B0%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20action_id%20in%20customers_solution%5B'Action'%5D%0A%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20customers_solution%20%3D%20customers_solution.with_columns(%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.Series(name%3D%22ActionName%22%2C%20values%3Dlist_action_names)%0A%20%20%20%20%20%20%20%20%5D)%0A%0A%20%20%20%20%20%20%20%20channel_display%20%3D%20channels_solution.select(%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Name%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22MetricContactPerChannel%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22MetricCostPerChannel%22%0A%20%20%20%20%20%20%20%20%5D)%0A%0A%20%20%20%20%20%20%20%20assignment_summary%20%3D%20customers_solution.group_by(%22ActionName%22).agg(%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.len().alias(%22Count%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22Value%22).mean().alias(%22AvgValue%22)%0A%20%20%20%20%20%20%20%20%5D).sort(%22Count%22%2C%20descending%3DTrue)%0A%0A%20%20%20%20%20%20%20%20solve_summary%20%3D%20mo.vstack(%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20mo.md(f%22%22%22%0A%20%20%20%20%20%20%20%20%20%20%20%20**Objective%20value%3A**%20%7Bp.attributes.objval%3A%2C.2f%7D%20%26nbsp%3B%26nbsp%3B%20**Total%20cost%3A**%20%24%7Bp.getSolution(MetricTotalCost)%3A%2C.2f%7D%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20**Channel%20allocation%3A**%0A%20%20%20%20%20%20%20%20%20%20%20%20%22%22%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20channel_display%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20mo.md(%22**Customer%20assignment%20summary%3A**%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20assignment_summary%2C%0A%20%20%20%20%20%20%20%20%5D)%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20channels_solution%20%3D%20channels%0A%20%20%20%20%20%20%20%20customers_solution%20%3D%20customers%0A%20%20%20%20%20%20%20%20solve_summary%20%3D%20mo.md(f%22%22%22%0A%20%20%20%20%20%20%20%20**Optimization%20did%20not%20find%20a%20solution.**%20Status%3A%20%7Bp.attributes.solvestatus%7D%0A%20%20%20%20%20%20%20%20%22%22%22)%0A%20%20%20%20mo.show_code(solve_summary%2C%20position%3D%22above%22)%0A%20%20%20%20return%20channels_solution%2C%20customers_solution%2C%20solstatus%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Solution%20Visualization%0A%0A%20%20%20%20Now%20we%20visualize%20the%20campaign%20optimization%20results%20using%20multiple%20plots%20to%20show%20channel%20allocation%2C%20costs%2C%20and%20customer%20distribution.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(channels_solution%2C%20customers_solution%2C%20mo%2C%20pl%2C%20plt%2C%20solstatus%2C%20xp)%3A%0A%20%20%20%20if%20solstatus%20in%20(xp.SolStatus.OPTIMAL%2C%20xp.SolStatus.FEASIBLE)%3A%0A%20%20%20%20%20%20%20%20%23%20Set%20up%20the%20figure%20with%20subplots%0A%20%20%20%20%20%20%20%20fig2%2C%20axes2%20%3D%20plt.subplots(2%2C%202%2C%20figsize%3D(14%2C%2010))%0A%20%20%20%20%20%20%20%20fig2.suptitle('Campaign%20Optimization%20Results'%2C%20fontsize%3D16%2C%20fontweight%3D'bold')%0A%0A%20%20%20%20%20%20%20%20%23%20Prepare%20data%20for%20visualization%0A%20%20%20%20%20%20%20%20channel_names%20%3D%20channels_solution%5B%22Name%22%5D.to_list()%0A%20%20%20%20%20%20%20%20contacts%20%3D%20channels_solution%5B%22MetricContactPerChannel%22%5D.to_list()%0A%20%20%20%20%20%20%20%20costs%20%3D%20channels_solution%5B%22MetricCostPerChannel%22%5D.to_list()%0A%0A%20%20%20%20%20%20%20%20%23%20Color%20scheme%0A%20%20%20%20%20%20%20%20colors%20%3D%20%5B'%233498db'%2C%20'%23e74c3c'%2C%20'%232ecc71'%5D%0A%0A%20%20%20%20%20%20%20%20%23%20Plot%201%3A%20Channel%20Contact%20Distribution%20(Bar%20Chart)%0A%20%20%20%20%20%20%20%20bax1%20%3D%20axes2%5B0%2C%200%5D%0A%20%20%20%20%20%20%20%20bars1%20%3D%20bax1.bar(channel_names%2C%20contacts%2C%20color%3Dcolors%2C%20alpha%3D0.8%2C%20edgecolor%3D'black')%0A%20%20%20%20%20%20%20%20bax1.set_title('Contacts%20per%20Channel'%2C%20fontweight%3D'bold'%2C%20fontsize%3D12)%0A%20%20%20%20%20%20%20%20bax1.set_ylabel('Number%20of%20Contacts'%2C%20fontsize%3D10)%0A%20%20%20%20%20%20%20%20bax1.set_xlabel('Channel'%2C%20fontsize%3D10)%0A%20%20%20%20%20%20%20%20bax1.grid(axis%3D'y'%2C%20alpha%3D0.3%2C%20linestyle%3D'--')%0A%0A%20%20%20%20%20%20%20%20%23%20Add%20value%20labels%20on%20bars%0A%20%20%20%20%20%20%20%20for%20bar%20in%20bars1%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20height%20%3D%20bar.get_height()%0A%20%20%20%20%20%20%20%20%20%20%20%20bax1.text(bar.get_x()%20%2B%20bar.get_width()%2F2.%2C%20height%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f'%7Bint(height)%7D'%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ha%3D'center'%2C%20va%3D'bottom'%2C%20fontweight%3D'bold')%0A%0A%20%20%20%20%20%20%20%20%23%20Plot%202%3A%20Channel%20Cost%20Distribution%20(Bar%20Chart)%0A%20%20%20%20%20%20%20%20bax2%20%3D%20axes2%5B0%2C%201%5D%0A%20%20%20%20%20%20%20%20bars2%20%3D%20bax2.bar(channel_names%2C%20costs%2C%20color%3Dcolors%2C%20alpha%3D0.8%2C%20edgecolor%3D'black')%0A%20%20%20%20%20%20%20%20bax2.set_title('Cost%20per%20Channel'%2C%20fontweight%3D'bold'%2C%20fontsize%3D12)%0A%20%20%20%20%20%20%20%20bax2.set_ylabel('Cost%20(%24)'%2C%20fontsize%3D10)%0A%20%20%20%20%20%20%20%20bax2.set_xlabel('Channel'%2C%20fontsize%3D10)%0A%20%20%20%20%20%20%20%20bax2.grid(axis%3D'y'%2C%20alpha%3D0.3%2C%20linestyle%3D'--')%0A%0A%20%20%20%20%20%20%20%20%23%20Add%20value%20labels%20on%20bars%0A%20%20%20%20%20%20%20%20for%20bar%20in%20bars2%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20height%20%3D%20bar.get_height()%0A%20%20%20%20%20%20%20%20%20%20%20%20bax2.text(bar.get_x()%20%2B%20bar.get_width()%2F2.%2C%20height%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f'%24%7Bheight%3A.1f%7D'%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ha%3D'center'%2C%20va%3D'bottom'%2C%20fontweight%3D'bold')%0A%0A%20%20%20%20%20%20%20%20%23%20Plot%203%3A%20Customer%20Distribution%20by%20Channel%20(Pie%20Chart)%0A%20%20%20%20%20%20%20%20bax3%20%3D%20axes2%5B1%2C%200%5D%0A%20%20%20%20%20%20%20%20wedges%2C%20texts%2C%20autotexts%20%3D%20bax3.pie(%0A%20%20%20%20%20%20%20%20%20%20%20%20contacts%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20labels%3Dchannel_names%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20colors%3Dcolors%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20autopct%3D'%251.1f%25%25'%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20startangle%3D90%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20wedgeprops%3Ddict(edgecolor%3D'white'%2C%20linewidth%3D2)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20bax3.set_title('Customer%20Distribution%20by%20Channel'%2C%20fontweight%3D'bold'%2C%20fontsize%3D12)%0A%0A%20%20%20%20%20%20%20%20%23%20Make%20percentage%20text%20bold%0A%20%20%20%20%20%20%20%20for%20autotext%20in%20autotexts%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20autotext.set_color('white')%0A%20%20%20%20%20%20%20%20%20%20%20%20autotext.set_fontweight('bold')%0A%20%20%20%20%20%20%20%20%20%20%20%20autotext.set_fontsize(10)%0A%0A%20%20%20%20%20%20%20%20%23%20Plot%204%3A%20Average%20Customer%20Value%20by%20Channel%0A%20%20%20%20%20%20%20%20bax4%20%3D%20axes2%5B1%2C%201%5D%0A%0A%20%20%20%20%20%20%20%20%23%20Calculate%20average%20value%20per%20channel%20using%20Polars%0A%20%20%20%20%20%20%20%20avg_values_by_channel%20%3D%20customers_solution.group_by(%22ActionName%22).agg(%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20pl.col(%22Value%22).mean().alias(%22AvgValue%22)%0A%20%20%20%20%20%20%20%20%5D)%0A%0A%20%20%20%20%20%20%20%20%23%20Sort%20to%20match%20channel%20order%0A%20%20%20%20%20%20%20%20channel_avg_values%20%3D%20%5B%5D%0A%20%20%20%20%20%20%20%20for%20name%20in%20channel_names%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20val%20%3D%20avg_values_by_channel.filter(pl.col(%22ActionName%22)%20%3D%3D%20name)%5B%22AvgValue%22%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20len(val)%20%3E%200%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20channel_avg_values.append(val%5B0%5D)%0A%20%20%20%20%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20channel_avg_values.append(0)%0A%0A%20%20%20%20%20%20%20%20bars4%20%3D%20bax4.bar(channel_names%2C%20channel_avg_values%2C%20color%3Dcolors%2C%20alpha%3D0.8%2C%20edgecolor%3D'black')%0A%20%20%20%20%20%20%20%20bax4.set_title('Average%20Customer%20Value%20by%20Channel'%2C%20fontweight%3D'bold'%2C%20fontsize%3D12)%0A%20%20%20%20%20%20%20%20bax4.set_ylabel('Average%20Value%20(%24)'%2C%20fontsize%3D10)%0A%20%20%20%20%20%20%20%20bax4.set_xlabel('Channel'%2C%20fontsize%3D10)%0A%20%20%20%20%20%20%20%20bax4.grid(axis%3D'y'%2C%20alpha%3D0.3%2C%20linestyle%3D'--')%0A%0A%20%20%20%20%20%20%20%20%23%20Add%20value%20labels%20on%20bars%0A%20%20%20%20%20%20%20%20for%20bar%20in%20bars4%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20height%20%3D%20bar.get_height()%0A%20%20%20%20%20%20%20%20%20%20%20%20bax4.text(bar.get_x()%20%2B%20bar.get_width()%2F2.%2C%20height%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f'%24%7Bheight%3A.0f%7D'%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ha%3D'center'%2C%20va%3D'bottom'%2C%20fontweight%3D'bold')%0A%0A%20%20%20%20%20%20%20%20%23%20Adjust%20layout%20to%20prevent%20overlap%0A%20%20%20%20%20%20%20%20plt.tight_layout(rect%3D(0%2C%200%2C%201%2C%200.97))%0A%20%20%20%20%20%20%20%20chart%20%3D%20fig2%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20chart%20%3D%20mo.md(%22No%20results%20to%20plot.%22)%0A%20%20%20%20chart%0A%20%20%20%20return%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
264ebbb586d629fef41efae90f7cef62