Profit Maximization for Developers: Optimizing Pricing, Marketing, and Investment Strategies

building_developer.ipynb Open In Colab Kaggle Gradient Open In SageMaker Studio Lab Hits

1. Introduction

This optimization model is designed for a real estate development project to maximize total revenue across multiple periods by managing construction, sales, advertising, and investment decisions. The project involves a portfolio of buildings under construction, each with unique characteristics such as area, start time, and construction duration.

Key components of the model include:

  • Construction Costs and Sales: The model tracks the progress of construction and associated monthly costs. As buildings progress, the base price per square meter of the area increases. The decision variables related to sales include the amount of area sold each month at different price steps, where price elasticity and seasonal demand impact sales potential. Advertising expenditures also influence demand, with additional square meters sold as a result of effective advertising.

  • Demand Elasticity: The demand for building sales is modeled using both price elasticity and seasonal effects. Price elasticity accounts for how sensitive demand is to changes in price, while seasonal effects represent demand fluctuations over time.

  • Revenue Calculation: Revenue is generated from the sale of square meters in each building, where the selling price depends on the selected price step and base price. The model ensures that prices either remain stable or increase over time. Additionally, advertising can boost demand, subject to a limit of increasing base demand by no more than 50%.

  • Advertising Expenditure: Advertising is modeled as a mechanism to increase demand, with effectiveness varying by building. The relationship between advertising and demand is nonlinear, considering the impact of seasonal demand patterns and the base price of the buildings.

  • Investment in Deposits: The model includes a cash management component, where the company can invest funds in deposit products of various durations (1 month, 3 months, 6 months, and 12 months). Each deposit type has its own terms, interest rates, and conditions for withdrawals and replenishments. Interest on deposits is accrued periodically, and the final balance of each deposit is calculated when it matures. The decision variables related to deposits include the amount of money invested, replenished, or withdrawn over time.

  • Account Balance and Cash Flow: A key constraint ensures that the company’s account balance remains non-negative throughout the planning horizon. The model tracks inflows from sales and returns from maturing deposits, while outflows include construction costs, advertising expenditures, and withdrawals from the company’s account.

  • Profit Maximization: The objective function seeks to maximize total revenue, calculated as the sum of account balances over all periods, taking into account sales revenue, construction costs, advertising expenses, and returns from investments in deposits.

The model also includes several key constraints to ensure feasibility, such as limiting the total area sold to the available building area, restricting the assignment of prices, and setting limits on advertising-induced demand increases. The interplay between sales decisions, advertising, and investment allows the project to maximize revenue while ensuring cash flow stability and optimizing the timing of construction and investment activities.

Partner with the AMPL team to transform complex problems into optimized solutions. AMPL consulting services combine deep technical knowledge with industry-leading insights, helping you unlock the full potential of optimization within your organization.

Tags: Marketing, Price optimization, Profitability, Residential Developer, Piecewise-linear, MIP, ampl-only, cbc

Notebook author: Mikhail Riabtsev <mail@solverytic.com>


2. Problem statement

Model of the ElegantDev's work

ElegantDev is embarking on an ambitious venture to create a residential complex comprising three buildings. The completion of this project is set within a 16-month timeframe as per an agreement with the city administration.

Key Indicators of buildings

The blueprint for the residential buildings, including essential metrics, is outlined in the provided table:

Building
Area of ​​apartments, sq.m.
Construction duration, months
Planned start date of works, months
B1
10000
11
2
B2
7500
8
9
B3
12000
12
4

Financing Schedule

ElegantDev has engaged various contractors for the construction and installation of the residential buildings. In accordance with the contractual obligations, the company is responsible for financing these works according to the next schedule:

Building
Construction period, months
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
B1
90000
135000
180000
270000
720000
675000
675000
675000
675000
540000
540000
B2
114000
256500
456000
456000
427500
427500
370500
342000
B3
129600
194400
259200
712800
712800
712800
712800
712800
648000
648000
518400
518400

Nominal Sales Price

According to current regulations, the residential units can be marketed starting one month prior to construction. Preliminary market research has established the initial sale basePrice $ per square meter, which is detailed in the accompanying table. This basePrice is subject to increase based on the construction progress.

Building
Construction period, months
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
B1
450
450
460
470
480
490
505
520
535
550
565
B2
380
390
400
415
430
445
460
475
490
B3
540
540
550
560
570
580
590
605
615
625
635
645
655

Price Elasticity of Sales

Demand for the properties is influenced by both basePrice and seasonal factors. The Marketing Department anticipates that at the stated basePrice level (gave above), monthly nominal demand will be approximately B1:10%, B2:11%, B3:8% of the total building area.

Price changes significantly affect sales: A 20% basePrice reduction results in a doubling of sales. A 20% basePrice increase leads to a tenfold decrease in sales. Price elasticity indicators are detailed in the provided table.

Price change, %
-20%
-15%
-10%
-5%
+5%
+10%
15%
+20%
Demand change, %
+100%
+80%
+50%
+13%
-20%
-50%
-80%
-90%

Seasonal Elasticity of Sales

Demand also depends on the season (time of year). Usually, the seasonal demand curve has the following form. The demand volume in the 1st month (nominal demand volume) is taken as one.

Period, month
January
February
March
April
May
June
July
August
September
October
November
December
Demand change, %
100%
100%
100%
94%
94%
95%
105%
105%
108%
108%
110%
120%
* Annual seasonal elasticity of demand, % of base value (10%,11%,8%) for B1,B2,B3 respectively.

Advertising Strategy

To boost sales in the event of low demand, ElegantDev plans to leverage various advertising mediums. The efficiency of each medium is as follows:

  • Type A: $9 sales for every $1 spent

  • Type B: $8 sales for every $1 spent

  • Type C: $7 sales for every $1 spent

Note: The effect of advertising will be realized with a one-month delay. Advertising has a limited effect, so demand can be warmed up and increased by no more than 50%

Financial Management and Other Obligations

ElegantDev is also handling other investment projects and has ongoing obligations. A schedule for fund withdrawals to meet these obligations is outlined in the provided schedule:

Costs, $
Construction period, months
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Value
-
12500
3500
3500
3500
3500
3500
36000
3500
3500
3500
3500
3500
3500
3500
3500

Investment Activities

To increase the efficiency of the project, ElegantDev intends to invest available funds in financial instruments (deposits). The terms for opening new deposit accounts are detailed in the accompanying information.

Deposit name
Term of placement, month
Frequency of accrual of % of income, month
Possibility of replenishing the account (yes +, no -)
Minimum placement amount
Interest rate, %
Capitalization of charges, (yes +, no -)
Possibility of withdrawing accrued amounts before the expiration of the deposit, (yes +, no -)
I
1
1
+
500
3.5
+
+
III
3
3
+
1000
4.0
+
+
VI
6
3
+
2000
4.5
+
+
XII
12
6
+
10000
5
-
+

Opening new deposit accounts for different types of deposits is available in the following periods:

Deposit name
Construction period, months
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
I
+
+
+
+
+
+
+
+
+
+
+
+
+
III
+
+
+
+
+
+
+
+
+
+
+
+
+
VI
+
+
+
+
+
+
+
+
+
+
XII
+
+
+
+
+
+
+
+
+
+
+
+
+

Objective

Maximize developer profits by optimizing Pricing, Marketing, and Investment Strategies.


Download Necessary Extensions and Libraries

Let’s start by downloading the necessary extensions and libraries

# Install dependencies
%pip install -q amplpy pandas
import pandas as pd                 # Loading panda to work with pandas.DataFrame objects (https://pandas.pydata.org/)
import numpy as np                  # Loading numpy to perform multidimensional calculations numpy.matrix (https://numpy.org/)
# Google Colab & Kaggle integration
from amplpy import AMPL, ampl_notebook

ampl = ampl_notebook(
    modules=["cbc", "highs"],  # modules to install
    license_uuid="default",  # license to use
)  # instantiate AMPL object and register magics

3. AMPL Model Formulation

Use %%ampl_eval to evaluate AMPL commands and declarations

%%ampl_eval
reset ;
### SETS & PARAMETERS
 ## Construction work
  param T := 16;                            # Planning horizon (in months)
  set BUILD := {'B1','B2','B3'};            # Set of buildings under construction
  set CHAR := {'area','start','duration'};  # Set of characteristics of buildings (area, start time, and construction duration)
  set LINKS within {BUILD, 1..T};           # Set of building-period pairs for calculations

  param buildData {BUILD, CHAR} >= 0;       # Parameters for each building under construction (e.g., area, start, duration)
  param buildCost {LINKS} >= 0;             # Monthly construction costs for each building
  param basePrice {LINKS} >= 0;             # Monthly selling base Price per square meter (basePrice increases with construction progress)
  param NomArea {BUILD} >= 0;               # Maximum nominal sales capacity (in sq.m.) per month for each building
  # Valid combinations of buildings (b) and time periods (t) where advertising demand can be considered
  set LINKS_AD_DEMAND = {b in BUILD, t in buildData[b,'start']-1..(buildData[b,'start'] + buildData[b,'duration'])-1: t > 1};
 
 ## Demand (based on price and seasonal elasticity)
  param nStep integer > 0;                  # Number of Price steps
  param priceStep {1..nStep+1} >= 0;        # Price step values – defining different price levels that can be set for the sale of the building area
  param demandCoef {1..nStep} >= 0;         # Coefficients representing price elasticity of demand for each price step. A higher coefficient means higher demand sensitivity to price changes.
  param SeasonalEffect {1..T} >= 0;         # Coefficient for seasonal elasticity of demand per month, representing the impact of seasonal trends on sales
  
  # Limit on the amount of building area that can be sold in month t at price step n.
  param limit {(b,t) in LINKS, n in 1..nStep} = 
    buildData[b,'area'] * NomArea[b] * SeasonalEffect[t] *  demandCoef[n];

  # The sales rate at which a certain amount of area can be sold, calculated as the product of base price and price step.
  param rate {(b,t) in LINKS, n in 1..nStep} = basePrice[b,t] * priceStep[n] ;
  
  # The incremental rate (or marginal rate) of revenue between consecutive price steps.
  param marg_rate {(b,t) in LINKS, n in 1..nStep-1} =
    ((limit[b,t,n+1]-1) * rate[b,t,n+1] - limit[b,t,n] * rate[b,t,n]) / 
    (limit[b,t,n+1]-limit[b,t,n]) ;

 ## Advertising     
  param adEff {BUILD} >= 0;                 # Effectiveness of advertising in increasing sales per dollar invested

## Deposits
  set DEP := {'I','III','VI','XII'};        # Types of deposit products (I: 1 month, III: 3 months, VI: 6 months, XII: 12 months)
  # Deposit characteristics
  set DEP_ATTR := {'term', 'freq_payment', 'refill', 'first_money', 'interest', 'capit_on'};        
  set AVAIL within {1..T, DEP};             # Availability of deposits for investment in each period
    
  param depData {DEP, DEP_ATTR} >= 0;       # Characteristics for each deposit product
  # Link deposit opening periods to periods within its term
  set DEP_LINKS = {(tp,d) in AVAIL, t in tp+1..tp + depData[d,'term']: t <= T+1};
  # Link deposit opening periods to periods within its term (for replenishment and withdrawals)
  set DEP_LINKS_ = {(tp,d,t) in DEP_LINKS: t <= T and t < tp + depData[d,'term']} ;
  # Number of interest payments during the deposit term       
  param nInterestPayments{d in DEP} = depData[d,'term'] / depData [d, 'freq_payment'];
  # Interest rate per payment period for each deposit type
  param RatePerFreq{d in DEP} = (depData[d,'interest'] / 1200) * depData [d, 'freq_payment']  ; 
  
## Cashflow & Investment schedule
  param MoneyWithdrawn {1..T} >= 0 ;        # Project's funds withdrawal schedule, indicating the amount of money   


### DECISION VARIABLES
  var SqmSold {LINKS, 1..nStep} >= 0;       # Number of square meters sold for a building in a period at a given price step
  var SaleDec {LINKS, 1..nStep} binary;     # Binary variable indicating whether sales occurred at a given price step for a building in a period
  var AdSpent {BUILD, 1..T} >= 0;           # Amount of money spent on advertising for each building in each period
  
  # AdDemand represents the additional demand generated due to advertising
  # The equation multiplies the advertising spent by its effectiveness and divides it by the base price to model how much additional demand is created for each unit of advertising, adjusting for the price sensitivity of the market.
  var AdDemand {(b,t) in LINKS_AD_DEMAND} = 
    (AdSpent[b,t-1] *                       # The amount of money spent on advertising for building b in the previous period (t-1)                       
    adEff[b] /                              # how much demand increases per dollar spent on advertising. A higher value indicates more efficient advertising
    basePrice[b,t]) *                       # The additional demand generated is inversely proportional to the base price. Lower base prices lead to a greater increase in demand for a given amount of advertising expenditure.
    SeasonalEffect[t] ;                     # Coefficient for seasonal elasticity of demand per month, representing the impact of seasonal trends on sales
 
 # Monthly profit from sales for each building in each period, considering price steps   
  var Monthly_Profit_From_Sales{(b,t) in LINKS} = 
    sum{n in 1..nStep-1}                    # Summation over all price steps (n) except the highest (nStep). This accumulates the total profit for each building (b) and period (t).                
    << limit[b,t,n];                        # Maximum amount of square meters that can be sold at price step n for building b in period t.
      rate[b,t,n],                          # The sales rate (price per square meter) at the price step n.
      marg_rate[b,t,n]>>                    # The marginal rate (additional profit contribution) at price step n.
      SqmSold[b,t,n] +                      # Number of square meters sold for a building in a period at a given price step 
      if t > 1 then AdDemand[b,t] * basePrice[b,t]; # Sales from advertising 

## Deposit variables
  var IsDepositOpen {AVAIL} binary;         # Binary variable indicating whether a deposit is opened
  var DepositAmt {AVAIL} >= 0;              # Amount invested in deposits
  var ReplenishAmt {DEP_LINKS_} >= 0 ;      # Amount replenished in the deposit during its term 
  var WithdrawalAmt{DEP_LINKS_} >= 0 ;      # Amount withdrawn from the deposit during its term
  
  # Define the accrued interest variable for each deposit product d starting in period tp.
  var Accrued_Interest {(tp,d,t) in DEP_LINKS} =      # % Income is calculated for each period t0. The range of t values ​​for each t0 is within the boundaries t < t0 <= t + depData[d,'term']
      # Check if the current period 't' is a scheduled interest payment period for deposit 'd'
      if exists {k in 1..nInterestPayments[d]} t = tp + depData[d, 'freq_payment'] * k then 
      RatePerFreq[d] * DepositAmt[tp,d] +             # Calculate interest for the base deposit amount
      # Sum the interest from previous periods ('tt') for which interest was already accrued
      # The sum includes the accrued interest over all periods 'tt' less than the current period 't'
      sum{(tp,d,tt) in DEP_LINKS_: tt < t} RatePerFreq[d] *                                   
        # If the deposit 'd' has compounding (depData[d,'capit_on'] = 1), include interest accrued in previous periods
        # If compounding is disabled, this part of the calculation is ignored (added as 0)
        (if depData[d,'capit_on'] = 1 then Accrued_Interest[tp,d,tt] else 0 +
        # If the previous period 'tt' is within one frequency payment period before 't', 
        # calculate the proportion of replenishment and withdrawal amounts to account for in the interest
        if tt > t - depData[d, 'freq_payment'] then tt/depData[d, 'freq_payment'] * 
        (ReplenishAmt[tp,d,tt]              # The amount replenished during period 'tt'
        - WithdrawalAmt[tp,d,tt])           # The amount withdrawn during period 'tt'
        # For periods 'tt' outside the current interest payment window, simply apply the full replenishment 
        # and withdrawal amounts without fractioning them based on the payment period frequency
        else (ReplenishAmt[tp,d,tt] - WithdrawalAmt[tp,d,tt]))  ;
  
  # Define the total value of the deposit at the time of its closure (when its term ends)
  #   - tp: The period when the deposit was opened
  #   - d: The type of deposit product (e.g., 1-month, 3-month)
  #   - t: The period when the deposit matures (equals tp + deposit term)
  # The condition `t = tp + depData[d,'term']` ensures this variable is only active when the deposit reaches its maturity.
  var DepositClose {(tp,d,t) in DEP_LINKS: t = tp + depData[d,'term']} =
    DepositAmt[tp,d]                                          # Initial amount deposited when the deposit was opened
    + sum {(tp,d,tt) in DEP_LINKS} Accrued_Interest[tp,d,tt]  # Add the total accrued interest over the term of the deposit.
    + sum {(tp,d,tt) in DEP_LINKS_: tt < t}                   # Add the total replenishment minus withdrawals over the term of the deposit, but only up to the period before the deposit closes.
      (ReplenishAmt[tp,d,tt]                                  # The amount replenished to the deposit during period tt.   
      - WithdrawalAmt[tp,d,tt]);      

 ## Account Status for the Current Period
  var Account {t in 1..T+1} =                           # The account balance at time period t, where t ranges from 1 to T+1 (including an extra period for end-of-planning balances)
    sum {(b,t) in LINKS} (                              # Loop over each building and time period (b,t) in the set of valid building-period pairs (LINKS)
      Monthly_Profit_From_Sales[b,t]                    # Add the monthly profit from sales of building b in period t
      - buildCost[b,t])                                 # Subtract the construction cost for building b in period t
      - sum{b in BUILD: t <= T} AdSpent[b,t]# Subtract the advertising spent for building b in period t
      - sum {(t,d) in AVAIL} DepositAmt[t,d]            # Subtract the amount invested in deposits in period t for each deposit d available in that period
      + sum {(tp,d) in AVAIL: tp = t - depData[d,'term']} # Add the deposit amount from closed deposits, where tp is the opening period, and deposits close after depData[d,'term'] periods
        DepositClose[tp,d,t]                            # Add the initial contribution of the deposit that is closing in the current period t
      - sum {(tp,d,t) in DEP_LINKS_}                    # For each active deposit (tp, d), where tp is the opening period and t is the current period
        (ReplenishAmt[tp,d,t]                           # Subtract the amount of money replenished into the deposit in the current period t
        - WithdrawalAmt[tp,d,t])                        # Subtract the amount withdrawn from the deposit in the current period t
      - sum {i in 1..1: t <= T} MoneyWithdrawn[t];      # Subtract any external withdrawals (MoneyWithdrawn) in the current period t (e.g., funds withdrawn from the project for non-investment purposes)

### OBJECTIVE FUNCTION
  maximize Total_revenue: sum{t in 1..T+1} Account[t] ; # Maximize total revenue by summing account balances over all periods


### CONSTRAINTS
 ## 1. Ensure total square footage sold does not exceed the building's area    
  AreaLimit_SqmSold {b in BUILD}: 
  sum {(b,t) in LINKS} (
    (if t > 1 then AdDemand[b,t]) + sum{n in 1..nStep} SqmSold[b,t,n]) <= buildData [b,'area'];
    
 ## 2. Ensure only one price can be assigned for each period and product    
  PriceAssignment_Limit {(b,t) in LINKS}: sum {n in 1..nStep} SaleDec [b,t,n] <= 1;
    
 ## 3a. Constraint for monthly sales volume, considering demand, price elasticity, and seasonal effects
  /*Min_MonthlySalesVolume {(b,t) in LINKS, n in 1..nStep-1}: 
    SqmSold[b,t,n] >=       # SqmSold (square meters sold) <= Demand * Price Elasticity * Seasonal Elasticity
    limit[b,t,n] *          # Limit on the amount of building area that can be sold in month t at price step n
    SaleDec[b,t,n] ;    */    # Decision on how much to sell in period (h,t)
 
 ## 3b. 
 Max_MonthlySalesVolume {(b,t) in LINKS, n in 1..nStep-1}: 
    SqmSold[b,t,n] <=     
    limit[b,t,n+1] *            
    SaleDec[b,t,n];
  
  ## 3c. Advertising can increase demand by no more than 50% of the volume of basic demand. 
  # Link AdDemand & SqmSold with SaleDec
  Max_MonthlySalesVolume_ {(b,t) in LINKS}: 
    (if t > 1 then AdDemand[b,t]) + sum{n in 1..nStep-1}(SqmSold[b,t,n] 
     - 1.5 * limit[b,t,n+1] * SaleDec[b,t,n]) <= 0 ;

 ## 4. Ensure that prices remain stable or increase throughout the sales period
  stablePrice {(b,t) in LINKS: t > buildData[b,'start']-1}: 
    sum {n in 1..nStep} SaleDec[b,t,n] * priceStep[n] * basePrice[b,t] >=   # Current period sales price.
    sum {n in 1..nStep} SaleDec[b,t-1,n] * priceStep[n] * basePrice[b,t-1]; # Previous period sales price.
  
 ## 5. Ensure that the cumulative account balance is non-negative for all periods 
  Bal_Account_Pos {t in 1..T+1}: sum{tt in 1..t} Account[tt] >= 0;
  
  ### Deposit Constraints   
 ## 6. Ensure the deposit amount is at least the initial required amount if a deposit is open
   Min_Deposit_Amount {(t,d) in AVAIL}: DepositAmt[t,d] >= IsDepositOpen[t,d] * depData[d,'first_money'];  

 ## 7. To limit the maximum amount that can be withdrawn from a deposit in a given period (t)
  s.t. Max_Withdrawal{(tp,d,t) in DEP_LINKS_}:  
    WithdrawalAmt[tp,d,t] <=                  # The amount withdrawn at time t should not exceed:
    #DepositAmt[tp,d] +                       # The original deposited amount is usually used as a reference, but it may be excluded here for a specific reason
      sum{(tp,d,tt) in DEP_LINKS_: tt <= t} ( # The sum of accrued interest up to time t, which will be the main component determining how much can be withdrawn
        Accrued_Interest[tp,d,tt]             # The interest accrued on the deposit up to the current period tt
        #+ ReplenishAmt[tp,d,tt]              # Optionally, replenish amounts could be included, depending on whether replenishments are allowed in the withdrawal calculation
        - WithdrawalAmt[tp,d,tt])  ;          # Subtract previous withdrawals up to time tt to ensure the total withdrawn does not exceed the accrued interest and available balance     

4. Load data from *.dat file

%%ampl_eval
data building_developer.dat;

5. Solve problem

Use %%ampl_eval to evaluate AMPL commands and declarations

%%ampl_eval
option solver cbc ;          # Choosing a solver
option cbc_options 'outlev=1 lim:time=30';

# Defining Output Settings 
option show_stats 1 ;       # (1) Show statistical information about the size of the problem. Default 0 (statistics are not displayed)
option display_1col 0 ;     # Data Display Settings
option omit_zero_rows 1 ;   # Hide rows with 0 values. Default (0)
option omit_zero_cols 1 ;   # Hide columns with 0 values. Default (0)

solve;                      # Solve the model
Presolve eliminates 249 constraints and 249 variables.
Substitution eliminates 173 variables.
Adjusted problem:
953 variables:
	349 binary variables
	264 nonlinear variables
	340 linear variables
486 constraints; 6844 nonzeros
	17 nonlinear constraints
	469 linear constraints
	486 inequality constraints
1 nonlinear objective; 481 nonzeros.

cbc 2.10.10: 
  tech:outlev = 1
  lim:time = 30
Welcome to the CBC MILP Solver 
Version: 2.10.10 
Build Date: Apr 18 2023 

command line - Cbc_C_Interface -log 1 -solve -quit (default strategy 1)
Continuous objective value is 3.04088e+06 - 0.01 seconds
Cgl0008I 33 inequality constraints converted to equality constraints
Cgl0005I 33 SOS with 330 members
Cgl0004I processed model has 745 rows, 1348 columns (330 integer (330 of which binary)) and 5947 elements
Cbc0036I Heuristics switched off as 264 branching objects are of wrong type
Cbc0031I 174 added rows had average density of 6.2701149
Cbc0013I At root node, 174 cuts changed objective from 3040881.7 to 2359467.5 in 19 passes
Cbc0014I Cut generator 0 (Probing) - 433 row cuts average 2.6 elements, 0 column cuts (92 active)  in 0.031 seconds - new frequency is 1
Cbc0014I Cut generator 1 (Gomory) - 146 row cuts average 32.2 elements, 0 column cuts (0 active)  in 0.013 seconds - new frequency is 1
Cbc0014I Cut generator 2 (Knapsack) - 93 row cuts average 11.1 elements, 0 column cuts (0 active)  in 0.020 seconds - new frequency is 1
Cbc0014I Cut generator 3 (Clique) - 0 row cuts average 0.0 elements, 0 column cuts (0 active)  in 0.001 seconds - new frequency is -100
Cbc0014I Cut generator 4 (MixedIntegerRounding2) - 183 row cuts average 11.2 elements, 0 column cuts (0 active)  in 0.005 seconds - new frequency is 1
Cbc0014I Cut generator 5 (FlowCover) - 7 row cuts average 8.3 elements, 0 column cuts (0 active)  in 0.015 seconds - new frequency is -100
Cbc0014I Cut generator 6 (TwoMirCuts) - 503 row cuts average 70.5 elements, 0 column cuts (0 active)  in 0.030 seconds - new frequency is 1
Cbc0010I After 0 nodes, 1 on tree, -1e+50 best solution, best possible 2359467.5 (0.23 seconds)
Cbc0016I Integer solution of 2329848.7 found by strong branching after 972 iterations and 3 nodes (0.30 seconds)
Cbc0016I Integer solution of 2342977 found by strong branching after 1014 iterations and 5 nodes (0.33 seconds)
Cbc0016I Integer solution of 2346866 found by strong branching after 1134 iterations and 6 nodes (0.38 seconds)
Cbc0001I Search completed - best objective 2346865.994293246, took 1134 iterations and 6 nodes (0.38 seconds)
Cbc0032I Strong branching done 82 times (933 iterations), fathomed 3 nodes and fixed 6 variables
Cbc0035I Maximum depth 2, 306 variables fixed on reduced cost
Cuts at root node changed objective from 3.04088e+06 to 2.35947e+06
Probing was tried 35 times and created 525 cuts of which 92 were active after adding rounds of cuts (0.038 seconds)
Gomory was tried 35 times and created 157 cuts of which 0 were active after adding rounds of cuts (0.020 seconds)
Knapsack was tried 35 times and created 105 cuts of which 0 were active after adding rounds of cuts (0.032 seconds)
Clique was tried 19 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.001 seconds)
MixedIntegerRounding2 was tried 35 times and created 203 cuts of which 0 were active after adding rounds of cuts (0.014 seconds)
FlowCover was tried 19 times and created 7 cuts of which 0 were active after adding rounds of cuts (0.015 seconds)
TwoMirCuts was tried 35 times and created 618 cuts of which 0 were active after adding rounds of cuts (0.040 seconds)
ZeroHalf was tried 1 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.001 seconds)
ImplicationCuts was tried 16 times and created 6 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
234 bounds tightened after postprocessing


Result - Optimal solution found

Objective value:                2346865.99429325
Enumerated nodes:               6
Total iterations:               1134
Time (CPU seconds):             0.40
Time (Wallclock seconds):       0.40

Total time (CPU seconds):       0.40   (Wallclock seconds):       0.40

cbc 2.10.10: optimal solution; objective 2346865.994
1134 simplex iterations
1134 barrier iterations
6 branching nodes

6. Display the solution

Use %%ampl_eval

%%ampl_eval
display 
SqmSold, basePrice, buildCost, MoneyWithdrawn, AdSpent, Monthly_Profit_From_Sales,
Account, DepositAmt, Accrued_Interest, ReplenishAmt, WithdrawalAmt,DepositClose
;

display {(b,t) in LINKS, n in 1..nStep: SqmSold[b,t,n] > 0} 
    Monthly_Profit_From_Sales[b,t] / SqmSold[b,t,n];

display {(b,t) in LINKS, n in 1..nStep-1: SaleDec[b,t,n] > 0}(
    limit[b,t,n],
    limit[b,t,n+1],           
    SqmSold[b,t,n]
    );  
SqmSold [B1,*,*]
:     3     :=
1    500
2    500
3    500
4    752
5    752
6    760
7    840
8    840
9    864
10   864
11   880

 [B2,*,*]
:      4      :=
8    693
9    712.8
10   712.8
11   726
12   792
13   660
14   660
15   660
16   620.4

 [B3,*,*]
:      4       :=
3    768
4    721.92
5    721.92
6    729.6
7    806.4
8    806.4
9    829.44
10   829.44
11   844.8
12   921.6
13   768
14   768
15   768
;

:     basePrice buildCost    :=
B1 1      450          0
B1 2      450      90000
B1 3      460     135000
B1 4      470     180000
B1 5      480     270000
B1 6      490     720000
B1 7      505     675000
B1 8      520     675000
B1 9      535     675000
B1 10     550     540000
B1 11     565     540000
B2 8      380          0
B2 9      390     114000
B2 10     400     256500
B2 11     415     456000
B2 12     430     456000
B2 13     445     427500
B2 14     460     427500
B2 15     475     370500
B2 16     490     342000
B3 3      540          0
B3 4      540     129600
B3 5      550     194400
B3 6      560     259200
B3 7      570     712800
B3 8      580     712800
B3 9      590     712800
B3 10     605     712800
B3 11     615     712800
B3 12     625     648000
B3 13     635     648000
B3 14     645     518400
B3 15     655     518400
;

MoneyWithdrawn [*] :=
 2 12500    5  3500    8 36000   11  3500   14  3500
 3  3500    6  3500    9  3500   12  3500   15  3500
 4  3500    7  3500   10  3500   13  3500   16  3500
;

:       AdSpent  Monthly_Profit_From_Sales    :=
B1 1        0               247500
B1 2        0               247500
B1 3        0               253000
B1 4        0               370618
B1 5        0               378504
B1 6    11970.4             390506
B1 7    23111.1             558000
B1 8    23777.8             676494
B1 9    24444.4             715910
B1 10   25111.1             735982
B1 11       0               770067
B2 8        0               276507
B2 9        0               291892
B2 10       0               299376
B2 11       0               316354
B2 12       0               357588
B2 13   33206.3             308385
B2 14   34289.1             584430
B2 15    7037.23            603488
B2 16       0               372116
B3 3        0               435456
B3 4        0               409329
B3 5        0               416909
B3 6        0               429005
B3 7        0               482630
B3 8        0               491098
B3 9        0               513838
B3 10       0               526902
B3 11   60000               545530
B3 12       0              1108800
B3 13   21937.4             512064
B3 14   62880               673690
B3 15       0               968352
;

Account [*] :=
 1      -2.91038e-11    8      -4.88944e-09   14  211551
 2      -2.91038e-11    9      -7.45058e-09   15 -211551
 4      -2.91038e-09   10      -3.72529e-09   16       1.39698e-09
 5       4.13274e-09   11      -6.51926e-09   17 2346870
 6      -2.56114e-09   12       6.98492e-10
 7      -1.39698e-09   13      -9.31323e-10
;

DepositAmt [*,*]
:        I           III         VI      :=
1         0        247500        .
2         0        145000        .
3         0        549956        .
4         0        368138      348684
5         0             0      473963
6         0          3956.66   376339
8       167.536         0           0
10    68626.4      310589           0
11   409028             0           0
12        0       1150260           0
;

:         Accrued_Interest ReplenishAmt WithdrawalAmt  DepositClose    :=
1  III 4      2475                .            .         249975
2  III 5      1450                .            .         146450
3  III 6      5499.56             .            .         555456
4  III 7      3681.38             .            .         371820
4  VI  7      3922.69               0      1961.35           .
4  VI  8         0                  0       980.673          .
4  VI  9         0                  0       490.337          .
4  VI  10     3966.82             .            .         353141
4  XII 15        0            2045710         0              .
4  XII 16   127857                .            .        2173570
5  VI  8      5332.08               0      2666.04           .
5  VI  9         0                  0      1333.02           .
5  VI  10        0                  0       666.51           .
5  VI  11     5392.07             .            .         480021
5  XII 16        0            2200190         0              .
5  XII 17   146679                .            .        2346870
6  III 9        39.5666           .            .           3996.23
6  VI  9      4233.81               0      2116.91           .
6  VI  10        0                  0      1058.45           .
6  VI  11        0                  0       529.226          .
6  VI  12     4281.44             .            .         381149
8  I   9         0.488648         .            .            168.025
10 I   11      200.16             .            .          68826.6
10 III 13     3105.89             .            .         313695
11 I   12     1193                .            .         410221
12 III 15    11502.6              .            .        1161760
;

Monthly_Profit_From_Sales[b,t]/SqmSold[b,t,n] [*,*,3] (tr)
:      B1       :=
1    495
2    495
3    506
4    492.844
5    503.33
6    513.823
7    664.285
8    805.35
9    828.6
10   851.832
11   875.076

 [*,*,4] (tr)
:      B2        B3       :=
3       .       567
4       .       567
5       .       577.5
6       .       588
7       .       598.5
8    399        609
9    409.5      619.5
10   420        635.25
11   435.75     645.75
12   451.5     1203.12
13   467.25     666.75
14   885.5      877.2
15   914.375   1260.88
16   599.8         .
;

:       limit[b,t,n] limit[b,t,n + 1] SqmSold[b,t,n]    :=
B1 1  3     500            800             500
B1 2  3     500            800             500
B1 3  3     500            800             500
B1 4  3     470            752             752
B1 5  3     470            752             752
B1 6  3     475            760             760
B1 7  3     525            840             840
B1 8  3     525            840             840
B1 9  3     540            864             864
B1 10 3     540            864             864
B1 11 3     550            880             880
B2 8  4     693            866.25          693
B2 9  4     712.8          891             712.8
B2 10 4     712.8          891             712.8
B2 11 4     726            907.5           726
B2 12 4     792            990             792
B2 13 4     660            825             660
B2 14 4     660            825             660
B2 15 4     660            825             660
B2 16 4     620.4          775.5           620.4
B3 3  4     768            960             768
B3 4  4     721.92         902.4           721.92
B3 5  4     721.92         902.4           721.92
B3 6  4     729.6          912             729.6
B3 7  4     806.4         1008             806.4
B3 8  4     806.4         1008             806.4
B3 9  4     829.44        1036.8           829.44
B3 10 4     829.44        1036.8           829.44
B3 11 4     844.8         1056             844.8
B3 12 4     921.6         1152             921.6
B3 13 4     768            960             768
B3 14 4     768            960             768
B3 15 4     768            960             768
;

7. Retrieve solution in Python and save data in *.json

import json
# Initialize an empty dictionary to store AMPL variable data
amplvar = dict()

# Prepare a list of AMPL variables (assuming ampl.get_variables() is a function that retrieves variable data)
list_of_ampl_variables = [item[0] for item in ampl.get_variables()]

# Iterate over each variable name in the list
for key_ampl in list_of_ampl_variables:
    # Skip certain variables that are not to be processed (these variables won't be included in the output)
    if key_ampl not in ['']:
        # Convert the AMPL variable data to a pandas DataFrame
        df = ampl.var[key_ampl].to_pandas()
        # Filter the DataFrame to include only rows where the variable's value is greater than a small threshold (1e-5)
        filtered_df = df[df[f"{key_ampl}.val"] > 1e-5]
        # Round the values in the DataFrame to two decimal places
        rounded_df = filtered_df.round(2)
        # Convert the filtered DataFrame to a dictionary and add it to the amplvar dictionary
        amplvar[key_ampl] = filtered_df.to_dict(orient='records')
        display (rounded_df)
# Save the entire dictionary to a JSON file
with open('dataframes.json', 'w') as file:
    json.dump(amplvar, file, indent=4)
Account.val
14 211550.54
17 2346865.99
Accrued_Interest.val
index0 index1 index2
1 III 4 2475.00
2 III 5 1450.00
3 III 6 5499.56
4 III 7 3681.38
VI 7 3922.69
10 3966.82
XII 16 127857.12
5 VI 8 5332.08
11 5392.07
XII 17 146679.12
6 III 9 39.57
VI 9 4233.81
12 4281.44
8 I 9 0.49
10 I 11 200.16
III 13 3105.89
11 I 12 1193.00
12 III 15 11502.59
AdDemand.val
index0 index1
B1 7 224.00
8 420.00
9 432.00
10 432.00
11 440.00
B2 14 577.50
15 577.50
16 108.00
B3 12 806.40
14 238.08
15 672.00
AdSpent.val
index0 index1
B1 6 11970.37
7 23111.11
8 23777.78
9 24444.44
10 25111.11
B2 13 33206.25
14 34289.06
15 7037.23
B3 11 60000.00
13 21937.37
14 62880.00
DepositAmt.val
index0 index1
1 III 247500.00
2 III 145000.00
3 III 549956.00
4 III 368138.23
VI 348683.91
5 VI 473962.80
6 III 3956.66
VI 376338.83
8 I 167.54
10 I 68626.45
III 310588.73
11 I 409028.06
12 III 1150258.56
DepositClose.val
index0 index1 index2
1 III 4 249975.00
2 III 5 146450.00
3 III 6 555455.56
4 III 7 371819.61
VI 10 353141.07
XII 16 2173571.07
5 VI 11 480021.38
XII 17 2346865.99
6 III 9 3996.23
VI 12 381149.50
8 I 9 168.03
10 I 11 68826.61
III 13 313694.62
11 I 12 410221.06
12 III 15 1161761.14
IsDepositOpen.val
index0 index1
Monthly_Profit_From_Sales.val
index0 index1
B1 1 247500.00
2 247500.00
3 253000.00
4 370618.50
5 378504.00
6 390505.50
7 557999.75
8 676494.00
9 715910.25
10 735982.50
11 770066.75
B2 8 276507.00
9 291891.60
10 299376.00
11 316354.50
12 357588.00
13 308385.00
14 584430.00
15 603487.50
16 372115.80
B3 3 435456.00
4 409328.64
5 416908.80
6 429004.80
7 482630.40
8 491097.60
9 513838.08
10 526901.76
11 545529.60
12 1108800.00
13 512064.00
14 673689.60
15 968352.00
ReplenishAmt.val
index0 index1 index2
4 XII 15 2045713.95
5 XII 16 2200186.87
SaleDec.val
index0 index1 index2
B1 1 3 1.0
2 3 1.0
3 3 1.0
4 3 1.0
5 3 1.0
6 3 1.0
7 3 1.0
8 3 1.0
9 3 1.0
10 3 1.0
11 3 1.0
B2 8 4 1.0
9 4 1.0
10 4 1.0
11 4 1.0
12 4 1.0
13 4 1.0
14 4 1.0
15 4 1.0
16 4 1.0
B3 3 4 1.0
4 4 1.0
5 4 1.0
6 4 1.0
7 4 1.0
8 4 1.0
9 4 1.0
10 4 1.0
11 4 1.0
12 4 1.0
13 4 1.0
14 4 1.0
15 4 1.0
SqmSold.val
index0 index1 index2
B1 1 3 500.00
2 3 500.00
3 3 500.00
4 3 752.00
5 3 752.00
6 3 760.00
7 3 840.00
8 3 840.00
9 3 864.00
10 3 864.00
11 3 880.00
B2 8 4 693.00
9 4 712.80
10 4 712.80
11 4 726.00
12 4 792.00
13 4 660.00
14 4 660.00
15 4 660.00
16 4 620.40
B3 3 4 768.00
4 4 721.92
5 4 721.92
6 4 729.60
7 4 806.40
8 4 806.40
9 4 829.44
10 4 829.44
11 4 844.80
12 4 921.60
13 4 768.00
14 4 768.00
15 4 768.00
WithdrawalAmt.val
index0 index1 index2
4 VI 7 1961.35
8 980.67
9 490.34
5 VI 8 2666.04
9 1333.02
10 666.51
6 VI 9 2116.91
10 1058.45
11 529.23

8. Enhancements

  • Try to explain why the model chooses in most cases the extreme values ​​of demand (min or max) for the chosen price?

  • What are the reasons for the model choosing intermediate values ​​of demand for the chosen price?