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%22Portfolio%20Optimization%22%0A%23%20description%20%3D%20%22Uses%20Pandas%20Dataframes%22%0A%23%20image%20%3D%20%22__marimo__%2Fthumbnail-portfolio.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**Portfolio%20Optimization%20using%20Pandas%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%20stock%20information%20to%20select%20a%20portfolio%20that%20maximizes%20returns%20subject%20to%20a%20wide%20range%20of%20constraints%20including%20sector%2C%20risk%20and%20ESG%20restrictions.%20We%20showcase%20the%20capabilitites%20of%20the%20Xpress%20Python%20API%20regarding%20the%20use%20of%20Pandas%20operations%20to%20generate%20aggregate%20expressions%2C%20as%20well%20as%20vector%20or%20matrix-based%20formulations%20for%20constraints%20and%20the%20objective%20function.%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%20pandas%20matplotlib%20seaborn'%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**portfolio%20optimization**%20problem%2C%20we%20wish%20to%20select%20stocks%20to%20form%20a%20portfolio%20for%20an%20asset%20allocation%20strategy.%20A%20list%20of%20stocks%20is%20available%20to%20be%20selected%20and%20we%20want%20to%20decide%20the%20fraction%20of%20the%20available%20budget%20to%20be%20allocated%20to%20each%20of%20the%20selected%20stocks%20that%20maximizes%20the%20total%20expected%20return.%0A%0A%20%20%20%20Stock%20data%20comprises%20an%20expected%20return%20for%20the%20coming%20investment%20period%2C%20an%20industry%20sector%2C%20an%20ESG%20(Environmental%2C%20Social%2C%20and%20Governance)%20score%20and%20a%20coefficient%20of%20variation%20(CV)%20representing%20the%20risk%20associated%20with%20each%20stock.%20**The%20selected%20portfolio%20must%20satisfy%20the%20following%20conditions**%3A%0A%0A%20%20%20%20*%20If%20a%20particular%20stock%20is%20selected%20then%20the%20total%20investment%20on%20this%20stock%20should%20not%20be%20lower%20than%201%25%20and%20should%20not%20exceed%2020%25%20of%20the%20available%20budget.%0A%20%20%20%20*%20The%20investment%20in%20each%20of%20the%208%20industry%20sectors%20should%20not%20exceed%2025%25%20of%20the%20available%20budget.%0A%20%20%20%20*%20At%20least%2010%20different%20stocks%20must%20be%20purchased.%0A%20%20%20%20*%20The%20ESG%20score%20amongst%20the%20selected%20stocks%2C%20weighted%20by%20fraction%2C%20needs%20to%20be%20at%20least%2070.%0A%20%20%20%20*%20The%20weighted%20average%20CV%20score%20should%20not%20exceed%200.5.%0A%0A%20%20%20%20The%20input%20data%20file%20**%5Bshares100.csv%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Fblob%2Fmain%2Fmodeling_examples%2Fdata%2Fshares100.csv)**%20provides%20data%2C%20in%20tabular%20form%2C%20related%20to%20100%20stocks%20with%20the%20following%20fields%3A%0A%0A%20%20%20%20*%20*Stock*%3A%20Name%20of%20the%20stock.%0A%20%20%20%20*%20*Return*%3A%20The%20expected%20return%20for%20the%20investment%20cycle%20ahead%2C%20per%20unit%20of%20stock.%0A%20%20%20%20*%20*Sector*%3A%20The%20industry%20sector%20the%20stock%20belongs%20to%20(e.g.%2C%20Technology%2C%20Healthcare%2C%20Energy).%0A%20%20%20%20*%20*ESG%20score*%3A%20The%20Environmental%2C%20Social%2C%20and%20Governance%20score%2C%20which%20evaluates%20a%20company's%20sustainability%20and%20ethical%20impact.%0A%20%20%20%20*%20*CV*%3A%20the%20coefficient%20of%20variation%20(CV)%20representing%20the%20volatility%20(risk)%20associated%20with%20each%20stock.%0A%0A%20%20%20%20The%20goal%20is%20to%20select%20the%20**portfolio%20that%20ensures%20maximum%20returns%20while%20satisfying%20all%20constraints**.%20We%20further%20evaluate%20the%20impact%20of%20setting%20a%20range%20of%20different%20thresholds%20on%20the%20maximum%20average%20risk%20and%20minimum%20ESG%20requirements%20on%20the%20optimal%20return.%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%20portfolio%20constraints%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%20minpershare_slider%20%3D%20mo.ui.slider(0.0%2C%200.1%2C%20value%3D0.01%2C%20step%3D0.01%2C%20label%3D%22Min.%20fraction%20of%20capital%20per%20share%22%2C%20show_value%3DTrue)%0A%20%20%20%20maxpershare_slider%20%3D%20mo.ui.slider(0.05%2C%200.5%2C%20value%3D0.2%2C%20step%3D0.05%2C%20label%3D%22Max.%20fraction%20of%20capital%20per%20share%22%2C%20show_value%3DTrue)%0A%20%20%20%20maxpersector_slider%20%3D%20mo.ui.slider(0.1%2C%200.5%2C%20value%3D0.25%2C%20step%3D0.05%2C%20label%3D%22Max.%20fraction%20of%20capital%20per%20sector%22%2C%20show_value%3DTrue)%0A%20%20%20%20minnumstocks_slider%20%3D%20mo.ui.slider(1%2C%2030%2C%20value%3D10%2C%20step%3D1%2C%20label%3D%22Min.%20number%20of%20stocks%20in%20portfolio%22%2C%20show_value%3DTrue)%0A%20%20%20%20minesg_slider%20%3D%20mo.ui.slider(50%2C%2089%2C%20value%3D70%2C%20step%3D1%2C%20label%3D%22Min.%20average%20ESG%20score%22%2C%20show_value%3DTrue)%0A%20%20%20%20maxrisk_slider%20%3D%20mo.ui.slider(0.1%2C%200.75%2C%20value%3D0.5%2C%20step%3D0.05%2C%20label%3D%22Max.%20average%20risk%20(CV)%22%2C%20show_value%3DTrue)%0A%20%20%20%20mo.vstack(%5B%0A%20%20%20%20%20%20%20%20mo.hstack(%5Bminpershare_slider%2C%20maxpershare_slider%2C%20maxpersector_slider%5D)%2C%0A%20%20%20%20%20%20%20%20mo.hstack(%5Bminnumstocks_slider%2C%20minesg_slider%2C%20maxrisk_slider%5D)%2C%0A%20%20%20%20%5D)%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20maxpersector_slider%2C%0A%20%20%20%20%20%20%20%20maxpershare_slider%2C%0A%20%20%20%20%20%20%20%20maxrisk_slider%2C%0A%20%20%20%20%20%20%20%20minesg_slider%2C%0A%20%20%20%20%20%20%20%20minnumstocks_slider%2C%0A%20%20%20%20%20%20%20%20minpershare_slider%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%20%23%23%20Data%20preparation%2C%20analysis%20and%20visualization%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(%60pandas%60%2C%20%60numpy%60)%2C%20and%20visualization%20(%60matplotlib%60%2C%20%60seaborn%60).%0A%0A%20%20%20%20After%20defining%20the%20value%20for%20the%20constants%20needed%20for%20the%20mathematical%20model%20(from%20the%20**Model%20parameters**%20controls%20above)%2C%20we%20load%20the%20dataset%20included%20in%20the%20file%20named%20**%5Bshares100.csv%5D(https%3A%2F%2Fgithub.com%2Ffico-xpress%2Fpython-notebooks%2Fblob%2Fmain%2Fmodeling_examples%2Fdata%2Fshares100.csv)**%20(which%20must%20be%20present%20in%20the%20%22data%22%20directory)%20containing%20stock%20information%20into%20a%20Pandas%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_(%0A%20%20%20%20maxpersector_slider%2C%0A%20%20%20%20maxpershare_slider%2C%0A%20%20%20%20maxrisk_slider%2C%0A%20%20%20%20minesg_slider%2C%0A%20%20%20%20minnumstocks_slider%2C%0A%20%20%20%20minpershare_slider%2C%0A%20%20%20%20mo%2C%0A)%3A%0A%20%20%20%20import%20xpress%20as%20xp%0A%20%20%20%20import%20pandas%20as%20pd%0A%20%20%20%20import%20numpy%20as%20np%0A%20%20%20%20import%20matplotlib.pyplot%20as%20plt%0A%20%20%20%20import%20seaborn%20as%20sns%0A%0A%20%20%20%20MinPerShare%20%3D%20minpershare_slider.value%20%20%20%20%20%20%23%20Minimum%20fraction%20of%20capital%20to%20invest%20in%20a%20single%20share%0A%20%20%20%20MaxPerShare%20%3D%20maxpershare_slider.value%20%20%20%20%20%20%23%20Maximum%20fraction%20of%20capital%20to%20invest%20in%20a%20single%20share%0A%20%20%20%20MaxPerSector%20%3D%20maxpersector_slider.value%20%20%20%20%23%20Maximum%20fraction%20of%20capital%20to%20invest%20in%20a%20single%20sector%0A%20%20%20%20MinNumStocks%20%3D%20minnumstocks_slider.value%20%20%20%20%23%20Minimum%20number%20of%20stocks%20in%20the%20portfolio%0A%20%20%20%20MinESG%20%3D%20minesg_slider.value%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Minimum%20average%20ESG%20score%20allowed%20in%20the%20portfolio%0A%20%20%20%20MaxRisk%20%3D%20maxrisk_slider.value%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Maximum%20average%20risk%20allowed%20in%20the%20portfolio%0A%0A%20%20%20%20%23%20Load%20the%20shares%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%20shares_df%20%3D%20pd.read_csv(mo.notebook_dir()%20%2F%20%22data%22%20%2F%20%22shares100.csv%22)%0A%0A%20%20%20%20%23%20Share%20data%20overview%0A%20%20%20%20mo.show_code(mo.vstack(%5Bmo.md(%22**Data%20sample%20for%20the%20first%205%20rows%3A**%22)%2C%20shares_df.head()%5D)%2C%20position%3D%22above%22)%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20MaxPerSector%2C%0A%20%20%20%20%20%20%20%20MaxPerShare%2C%0A%20%20%20%20%20%20%20%20MaxRisk%2C%0A%20%20%20%20%20%20%20%20MinESG%2C%0A%20%20%20%20%20%20%20%20MinNumStocks%2C%0A%20%20%20%20%20%20%20%20MinPerShare%2C%0A%20%20%20%20%20%20%20%20np%2C%0A%20%20%20%20%20%20%20%20pd%2C%0A%20%20%20%20%20%20%20%20plt%2C%0A%20%20%20%20%20%20%20%20shares_df%2C%0A%20%20%20%20%20%20%20%20sns%2C%0A%20%20%20%20%20%20%20%20xp%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%20code%20below%20creates%20four%20subplots%20with%20stock%20distributions%20using%20Seaborn%20and%20Matplotlib.%20The%20subplots%20in%20the%20first%20row%20show%20the%20distribution%20of%20**Returns**%20and%20**Sectors**%2C%20while%20the%20second%20row%20displays%20the%20distribution%20of%20**ESG%20scores**%20and%20**CV%20scores**.%0A%0A%20%20%20%20Both%20plots%20use%20histograms%20and%20contain%20kernel%20density%20estmation%20curves%20to%20further%20highlight%20the%20shape%20of%20the%20data.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(plt%2C%20shares_df%2C%20sns)%3A%0A%20%20%20%20%23%20Plot%20distributions%0A%20%20%20%20dist_fig%20%3D%20plt.figure(figsize%3D(12%2C%2010))%0A%0A%20%20%20%20num_bins%20%3D%2025%0A%0A%20%20%20%20%23%20Plotting%20the%20distribution%20of%20returns%0A%20%20%20%20plt.subplot(2%2C%202%2C%201)%0A%20%20%20%20sns.histplot(shares_df%5B'Return'%5D%2C%20bins%3Dnum_bins%2C%20kde%3DTrue)%0A%20%20%20%20plt.title('Distribution%20of%20Returns')%0A%0A%20%20%20%20%23%20Plotting%20the%20distribution%20of%20Sector%0A%20%20%20%20plt.subplot(2%2C%202%2C%202)%0A%20%20%20%20plt.xticks(rotation%3D45)%0A%20%20%20%20sns.histplot(shares_df%5B'Sector'%5D%2C%20bins%3Dnum_bins%2C%20kde%3DTrue)%0A%20%20%20%20plt.title('Distribution%20by%20Sector')%0A%0A%20%20%20%20%23%20Plotting%20the%20distribution%20of%20ESG%20scores%0A%20%20%20%20plt.subplot(2%2C%202%2C%203)%0A%20%20%20%20sns.histplot(shares_df%5B'ESG%20score'%5D%2C%20bins%3Dnum_bins%2C%20kde%3DTrue)%0A%20%20%20%20plt.title('Distribution%20of%20ESG%20Scores')%0A%0A%20%20%20%20%23%20Plotting%20the%20distribution%20of%20risk%20scores%0A%20%20%20%20plt.subplot(2%2C%202%2C%204)%0A%20%20%20%20sns.histplot(shares_df%5B'CV'%5D%2C%20bins%3Dnum_bins%2C%20kde%3DTrue)%0A%20%20%20%20plt.title('Distribution%20of%20CV%20scores')%0A%0A%20%20%20%20plt.tight_layout()%0A%20%20%20%20dist_fig%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%20The%20code%20cell%20below%20plots%20a%20**sector-wise%20analysis**%20using%20boxplots%2C%20depicting%20the%20spread%20of%20**returns**%20and%20**risk%20scores**%20across%20different%20sectors.%20It%20shows%20that%20sectors%20with%20higher%20expected%20returns%20are%20also%20the%20most%20volatile%20sectors.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(plt%2C%20shares_df%2C%20sns)%3A%0A%20%20%20%20%23%20Set%20the%20plot%20layout%0A%20%20%20%20sector_fig%20%3D%20plt.figure(figsize%3D(12%2C%205))%0A%0A%20%20%20%20%23%20Boxplot%20for%20Return%20by%20Sector%0A%20%20%20%20plt.subplot(1%2C%202%2C%201)%0A%20%20%20%20sns.boxplot(data%3Dshares_df%2C%20x%3D'Sector'%2C%20y%3D'Return')%0A%20%20%20%20plt.xticks(rotation%3D45)%0A%20%20%20%20plt.title('Return%20distribution%20by%20Sector')%0A%0A%20%20%20%20%23%20Boxplot%20for%20CV%20score%20by%20Sector%0A%20%20%20%20plt.subplot(1%2C%202%2C%202)%0A%20%20%20%20sns.boxplot(data%3Dshares_df%2C%20x%3D'Sector'%2C%20y%3D'CV')%0A%20%20%20%20plt.xticks(rotation%3D45)%0A%20%20%20%20plt.title('Risk%20distribution%20by%20Sector')%0A%0A%20%20%20%20plt.tight_layout()%0A%20%20%20%20sector_fig%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%20printing%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%20defining%20a%20set%20of%20continuous%20decision%20variables%20%24frac_i%24%20that%20represent%20the%20fraction%20(%24%3E%3D%200%2C%20%3C%3D%201%24)%20of%20the%20available%20budget%20to%20be%20allocated%20to%20each%20stock%20%24i%20%5Cin%20%5Cmathcal%7BS%7D%24%2C%20and%20the%20auxiliary%20binary%20variables%20%24buy_i%24%20to%20decide%20whether%20stock%20%24i%24%20is%20included%20in%20the%20portfolio%20(%24%3D1%24)%2C%20or%20not%20(%24%3D0%24).%0A%0A%20%20%20%20To%20take%20advantage%20of%20the%20**improved%20Pandas%20compatibility%20features%20introduced%20in%20Xpress%209.8**%2C%20Pandas%20series%20containing%20Xpress%20variables%20or%20expressions%20should%20have%20the%20data%20type%20set%20to%20%60xpressobj%60%2C%20as%20shown%20by%20the%20code%20below%20where%20the%20two%20sets%20of%20variables%20are%20added%20to%20the%20previously%20created%20Xpress%20problem.%0A%0A%20%20%20%20The%20NumPy%20arrays%20returned%20by%20%5Bproblem.addVariables%5D(https%3A%2F%2Fwww.fico.com%2Ffico-xpress-optimization%2Fdocs%2Flatest%2Fsolver%2Foptimizer%2Fpython%2FHTML%2Fproblem.addVariables.html)%20are%20wrapped%20into%20Pandas%20series%2C%20which%20must%20be%20added%20as%20a%20new%20column%20to%20the%20dataframe%20in%20order%20to%20enable%20performing%20the%20intended%20Pandas%20operations.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo%2C%20pd%2C%20shares_df%2C%20xp)%3A%0A%20%20%20%20%23%20Create%20Xpress%20problem%20and%20variables%0A%20%20%20%20p%20%3D%20xp.problem(%22Portfolio%20Selection%22)%0A%20%20%20%20shares_df%5B'frac'%5D%20%3D%20pd.Series(p.addVariables(len(shares_df)%2C%20vartype%3Dxp.continuous%2C%20name%3D'frac')%2C%20dtype%3D'xpressobj')%0A%20%20%20%20shares_df%5B'buy'%5D%20%3D%20pd.Series(p.addVariables(len(shares_df)%2C%20vartype%3Dxp.binary%2C%20name%3D'buy')%2C%20dtype%3D'xpressobj')%0A%20%20%20%20mo.show_code()%0A%20%20%20%20return%20(p%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%20objective%20of%20maximizing%20the%20expected%20returns%20is%20defined%20the%20sum%20of%20the%20product%20between%20the%20expected%20return%20(%60RET%60)%20of%20each%20stock%20and%20the%20corresponding%20%60frac%60%20variable%3A%20%24%24%5Cmax%20%5Csum_%7Bi%20%5Cin%20%5Cmathcal%7BS%7D%7D%20RET_i%20%5Ccdot%20frac_i%24%24%0A%0A%20%20%20%20As%20shown%20in%20the%20code%20cell%20below%2C%20the%20objective%20expression%20can%20be%20created%20using%20the%20element-wise%20product%20(%60*%60)%20of%20the%20%60Return%60%20and%20%60frac%60%20columns%20followed%20by%20the%20summation%20(%60sum%60)%20of%20the%20resulting%20series%20**using%20Pandas-specific%20methods**.%0A%0A%20%20%20%20The%20call%20to%20%5Bproblem.setObjective%5D(https%3A%2F%2Fwww.fico.com%2Ffico-xpress-optimization%2Fdocs%2Flatest%2Fsolver%2Foptimizer%2Fpython%2FHTML%2Fproblem.setObjective.html)%20then%20adds%20the%20objective%20function%20to%20the%20problem%2C%20specifying%20that%20it%20is%20to%20be%20maximized.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo%2C%20p%2C%20shares_df%2C%20xp)%3A%0A%20%20%20%20%23%20Objective%20function%3A%20minimize%20total%20return%0A%20%20%20%20obj%20%3D%20(shares_df%5B'Return'%5D%20*%20shares_df%5B'frac'%5D).sum()%0A%20%20%20%20p.setObjective(obj%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%202%20constraints%3A%0A%0A%20%20%20%20*%20The%20sum%20of%20the%20portfolio%20stock%20allocations%20should%20be%20equal%20to%201%20(fully%20invested%20portfolio)%3A%0A%20%20%20%20%24%24%5Csum_%7Bi%20%5Cin%20%5Cmathcal%7BS%7D%7D%20frac_i%20%3D%201%24%24%0A%0A%20%20%20%20*%20Diversification%3A%20ensure%20a%20minimum%20number%20of%20assets%3A%0A%20%20%20%20%24%24%5Csum_%7Bi%20%5Cin%20%5Cmathcal%7BS%7D%7D%20buy_i%20%5Cgeq%20%5Ctext%7BMinNumStocks%7D%24%24%0A%0A%20%20%20%20We%20simply%20use%20the%20Pandas%20%60sum%60%20operator%20on%20the%20corresponding%20dataframe%20column%20to%20represent%20the%20left-hand%20side%20of%20each%20constraint.%20The%20call%20to%20%5Bproblem.addConstraint%5D(https%3A%2F%2Fwww.fico.com%2Ffico-xpress-optimization%2Fdocs%2Flatest%2Fsolver%2Foptimizer%2Fpython%2FHTML%2Fproblem.addConstraint.html)%20adds%20the%20constraint%20to%20the%20model.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(MinNumStocks%2C%20mo%2C%20p%2C%20shares_df)%3A%0A%20%20%20%20%23%20Spend%20all%20the%20capital%0A%20%20%20%20p.addConstraint(shares_df%5B'frac'%5D.sum()%20%3D%3D%201)%0A%0A%20%20%20%20%23%20Ensure%20a%20minimum%20total%20number%20of%20assets%0A%20%20%20%20p.addConstraint(shares_df%5B'buy'%5D.sum()%20%3E%3D%20MinNumStocks)%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%20The%20code%20below%20ensures%20that%20if%20a%20share%20is%20selected%20(%24buy%24%20%3D%201)%2C%20then%20the%20fraction%20bought%20must%20be%20at%20least%20%60MinPerShare%60%20and%20not%20exceed%20%60MaxPerShare%60.%20In%20either%20case%2C%20if%20the%20share%20is%20not%20selected%20(%24buy%24%20%3D%200)%2C%20then%20the%20fraction%20must%20be%20zero.%0A%0A%20%20%20%20%24%24%0A%20%20%20%20%5Ctext%7BMinPerShare%7D%20%5Ccdot%20buy_i%20%5Cleq%20frac_i%20%5Cleq%20%5Ctext%7BMaxPerShare%7D%20%5Ccdot%20buy_i%20%5Cquad%20%5Cforall%20i%20%5Cin%20%5Cmathcal%7BS%7D%0A%20%20%20%20%24%24%0A%0A%20%20%20%20We%20model%20those%20constraints%20by%20doing%20an%20element-wise%20product%20between%20the%20corresponding%20scalar%20and%20the%20%60buy%60%20column%20elements%20using%20the%20multiplication%20operator%20(%60*%60)%2C%20and%20equivalently%20by%20using%20the%20%60mul%60%20Pandas%20method.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(MaxPerShare%2C%20MinPerShare%2C%20mo%2C%20p%2C%20shares_df)%3A%0A%20%20%20%20%23%20Linking%20constraints%20defining%20minimum%20and%20maximum%20fraction%20per%20share%0A%20%20%20%20p.addConstraint(shares_df%5B'frac'%5D%20%3E%3D%20MinPerShare%20*%20shares_df%5B'buy'%5D)%0A%20%20%20%20p.addConstraint(shares_df%5B'frac'%5D%20%3C%3D%20shares_df%5B'buy'%5D.mul(MaxPerShare))%20%20%20%20%20%23%20Alternative%20way%20of%20multiplying%20column%20x%20scalar%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%20Just%20like%20with%20the%20objective%20function%20expression%2C%20we%20define%20the%20ESG%20and%20risk%20constraints%20as%3A%0A%0A%20%20%20%20*%20Minimum%20average%20ESG%20score%20constraint%3A%0A%20%20%20%20%24%24%5Csum_%7Bi%20%5Cin%20%5Cmathcal%7BS%7D%7D%20%5Ctext%7BESG%7D_i%20%5Ccdot%20frac_i%20%5Cgeq%20%5Ctext%7BMinESG%7D%24%24%0A%0A%20%20%20%20*%20Maximum%20average%20risk%20(CV)%20constraint%3A%0A%20%20%20%20%24%24%5Csum_%7Bi%20%5Cin%20%5Cmathcal%7BS%7D%7D%20%5Ctext%7BCV%7D_i%20%5Ccdot%20frac_i%20%5Cleq%20%5Ctext%7BMaxRisk%7D%24%24%0A%0A%20%20%20%20These%20constraints%20can%20be%20modeled%20by%20using%20the%20element-wise%20product%20of%20the%20corresponding%20column%20and%20the%20%60frac%60%20column%2C%20followed%20by%20the%20summation%20of%20the%20resulting%20series%20using%20the%20%60sum%60%20method%20to%20build%20the%20left-hand%20side%20of%20the%20constraint.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(MaxRisk%2C%20MinESG%2C%20mo%2C%20p%2C%20shares_df)%3A%0A%20%20%20%20%23%20Average%20ESG%20score%20constraint%3A%20the%20weighted%20average%20ESG%20score%20must%20be%20at%20least%20MinAvgESG%0A%20%20%20%20avg_esg%20%3D%20(shares_df%5B'ESG%20score'%5D%20*%20shares_df%5B'frac'%5D).sum()%20%3E%3D%20MinESG%0A%0A%20%20%20%20%23%20Risk%20constraint%3A%20average%20risk%20(CV)%20must%20be%20less%20than%20or%20equal%20to%20MaxRisk%0A%20%20%20%20avg_risk%20%3D%20(shares_df%5B'CV'%5D%20*%20shares_df%5B'frac'%5D).sum()%20%3C%3D%20MaxRisk%0A%0A%20%20%20%20p.addConstraint(avg_esg%2C%20avg_risk)%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%20The%20constraint%20that%20ensures%20that%20the%20total%20fraction%20of%20shares%20bought%20in%20any%20given%20sector%20does%20not%20exceed%20%60MaxPerSector%60%3A%0A%20%20%20%20%20%24%24%5Csum_%7Bi%20%5Cin%20%5Cmathcal%7BS%7D%3A%20Sector%5Bi%5D%20%3D%20n%7D%20frac_i%20%5Cleq%20%5Ctext%7BMaxPerSector%7D%2C%20%5Cforall%20n%20%5Cin%20SECTORS%24%24%0A%0A%20%20%20%20This%20constraint%20can%20be%20easily%20modeled%20using%20Pandas%20by%20applying%20the%20%60groupby()%60%20method%20to%20group%20shares%20by%20their%20sector%20(e.g.%2C%20Technology%2C%20Healthcare%2C%20etc.)%2C%20and%20then%20calling%20the%20%60sum%60%20function%20to%20sum%20the%20variables%20representing%20fractional%20purchases%20(%60frac%60)%20within%20each%20sector.%20This%20expression%20produces%20a%20series%20of%20constraints%2C%20one%20for%20each%20sector%2C%20which%20are%20then%20added%20to%20the%20problem%20by%20a%20single%20call%20to%20%60addConstraint%60.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(MaxPerSector%2C%20mo%2C%20p%2C%20shares_df)%3A%0A%20%20%20%20%23%20Maximum%20per%20sector%3A%20total%20fraction%20invested%20in%20each%20sector%20does%20not%20exceed%20MaxPerSector%0A%20%20%20%20p.addConstraint(shares_df.groupby('Sector')%5B'frac'%5D.sum()%20%3C%3D%20MaxPerSector)%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%20Lastly%2C%20we%20turn%20off%20the%20solver%20logs%20by%20using%20the%20%5Boutputlog%5D(https%3A%2F%2Fwww.fico.com%2Ffico-xpress-optimization%2Fdocs%2Flatest%2Fsolver%2Foptimizer%2FHTML%2FOUTPUTLOG.html)%20control%2C%20and%20then%20solve%20the%20problem%20by%20calling%20%5Bproblem.optimize%5D(https%3A%2F%2Fwww.fico.com%2Ffico-xpress-optimization%2Fdocs%2Flatest%2Fsolver%2Foptimizer%2Fpython%2FHTML%2Fproblem.optimize.html)%20which%20returns%20a%20solve%20status%20(completed%2C%20stopped%2C%20...)%20and%20solution%20status%20(optimal%2C%20infeasible%2C%20...).%0A%0A%20%20%20%20In%20case%20a%20(feasible%20or%20optimal)%20solution%20has%20been%20found%2C%20we%20display%20the%20computed%20metrics%20and%20plot%20the%20final%20solution%20(i.e.%20the%20portfolio%20composition)%20using%20a%20donut%20plot%20with%20selected%20stocks%20in%20descending%20order%20of%20fraction%20value.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo%2C%20np%2C%20p%2C%20pd%2C%20plt%2C%20shares_df%2C%20xp)%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%2Cxp.SolStatus.FEASIBLE)%3A%0A%20%20%20%20%20%20%20%20%23%20Get%20solution%0A%20%20%20%20%20%20%20%20shares_df%5B%22fraction%22%5D%20%3D%20p.getSolution(shares_df%5B'frac'%5D)%0A%0A%20%20%20%20%20%20%20%20%23%20Compute%20metrics%0A%20%20%20%20%20%20%20%20SummaryValues%20%3D%20pd.Series(%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Expected%20return%22%3A%20p.attributes.objval%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Average%20risk%20(CV)%22%3A%20(shares_df%5B%22CV%22%5D%20*%20shares_df%5B%22fraction%22%5D).sum()%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Average%20ESG%22%3A%20(shares_df%5B%22ESG%20score%22%5D%20*%20shares_df%5B%22fraction%22%5D).sum()%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22%23%20selected%20stocks%22%3A%20(shares_df%5B%22fraction%22%5D%20%3E%200).sum()%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Largest%20position%22%3A%20(shares_df%5B%22fraction%22%5D).max()%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Smallest%20position%22%3A%20shares_df%5Bshares_df%5B%22fraction%22%5D%20%3E%200%5D%5B%22fraction%22%5D.min()%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22MaxPerSector%22%3A%20shares_df.groupby('Sector')%5B'fraction'%5D.sum().max()%2C%0A%20%20%20%20%20%20%20%20%7D)%0A%0A%20%20%20%20%20%20%20%20%23%20Plot%20portfolio%20composition%0A%20%20%20%20%20%20%20%20filtered_df%20%3D%20shares_df%5Bshares_df%5B%22fraction%22%5D%20%3E%3D%200.005%5D%20%23%20Filter%20rows%20where%20fraction%20is%20greater%20than%20or%20equal%20to%200.005%0A%20%20%20%20%20%20%20%20plot_df%20%3D%20filtered_df.sort_values('fraction'%2C%20ascending%3DFalse)%20%23%20Sort%20so%20the%20largest%20slices%20are%20first%0A%0A%20%20%20%20%20%20%20%20sizes%20%3D%20plot_df%5B'fraction'%5D.astype(float).values%20%20%20%20%20%20%20%20%23%20The%20wedge%20sizes%0A%20%20%20%20%20%20%20%20labels%20%3D%20plot_df%5B'Stock'%5D.astype(str).values%20%20%20%20%20%20%20%20%20%20%20%20%23%20The%20labels%20for%20each%20wedge%0A%0A%20%20%20%20%20%20%20%20colors%20%3D%20plt.cm.tab20(np.linspace(0%2C%201%2C%20len(sizes)))%20%20%20%20%23%20Color%20palette%0A%20%20%20%20%20%20%20%20threshold_pct%20%3D%203.0%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20Only%20show%20%25%20on%20wedges%20%3E%3D%20this%20threshold%0A%0A%20%20%20%20%20%20%20%20fig%2C%20ax%20%3D%20plt.subplots(figsize%3D(9%2C%208))%0A%20%20%20%20%20%20%20%20wedges%2C%20_%2C%20autotexts%20%3D%20ax.pie(%0A%20%20%20%20%20%20%20%20%20%20%20%20sizes%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%20startangle%3D90%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20counterclock%3DFalse%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20autopct%3Dlambda%20p%3A%20f'%7Bp%3A.1f%7D%25'%20if%20p%20%3E%3D%20threshold_pct%20else%20''%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pctdistance%3D0.72%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20wedgeprops%3Ddict(width%3D0.55%2C%20edgecolor%3D'white')%2C%20%20%20%20%20%23%20Donut%20style%20(more%20readable)%0A%20%20%20%20%20%20%20%20%20%20%20%20textprops%3Ddict(color%3D'black'%2C%20fontsize%3D10)%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%20%20%20%20ax.legend(%0A%20%20%20%20%20%20%20%20%20%20%20%20wedges%2C%20labels%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20title%3D'Stock'%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20loc%3D'center%20left'%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20bbox_to_anchor%3D(1.0%2C%200.5)%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%20%20%20%20ax.set_title('Selected%20portfolio%20and%20fractions')%0A%20%20%20%20%20%20%20%20ax.set_aspect('equal')%0A%20%20%20%20%20%20%20%20plt.tight_layout()%0A%20%20%20%20%20%20%20%20result%20%3D%20mo.vstack(%5Bmo.as_html(SummaryValues)%2C%20fig%5D)%0A%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20result%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(result%2C%20position%3D%22above%22)%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
c441135322cc450c1d47f0a009950cc0