bperraudin Nov 17, 2023 | flag | on: List of Parameters for ActionRwD functions

I completely agree with what was said in the previous comment: stochastic optimization will definitely enable to tackle this issue (and more) the right way.
Meanwhile, here is a piece of code that you could use to tackle the order multiplier problem, following the logic I described in my previous comment:


///## ACTION REWARD FUNCTION
///### Conversion to account for customer Order Multipliers
Skus.CustomerOrderMultiplier = 2
Skus.StockOnHandInLots = floor(Skus.StockOnHand/Skus.CustomerOrderMultiplier) //could also use round(), business assumption to be taken according to the pb treated
PO.OrderQtyInLots = floor(PO.OrderQty/Skus.CustomerOrderMultiplier)
CatalogPeriods.BaselineInLots = floor(CatalogPeriods.Baseline/Skus.CustomerOrderMultiplier)

///### Action reward call

Skus.WOOUncovDemandInLots, Skus.HoldingTime = actionrwd.reward(
  TimeIndex: CatalogPeriods.N
  Baseline: CatalogPeriods.BaselineInLots
  Dispersion: Skus.Dispersion ///assume Dispersion is adapted to Order Multiplier change. This should be handled in previous script
  Alpha: 0.05
  StockOnHand: Skus.StockOnHandInLots
  ArrivalTime: dirac(PO.POTimeIndex)
  StockOnOrder: PO.OrderQtyInLots
  LeadTime: Skus.SLT
  StepOfReorder: Skus.RLT)

Skus.WOOUncovDemand = Skus.WOOUncovDemandInLots * Skus.CustomerOrderMultiplier
Skus.SellThrough = (1-cdf(Skus.WOOUncovDemand + 1)) * uniform.right(1)

Regarding the customer MOQ, provided that the customers tend to always order the MOQ or a few units above the MOQ, you could use the same logic as an approximation and treat the MOQ as an Order multiplier. I can't offer a cleaner way to cope with this unfortunately, as it would require to dedicate too much time to the problem.

bperraudin Nov 17, 2023 | flag | on: List of Parameters for ActionRwD functions

Ok now I understand better, sorry about my first irrelevant answer.
Unfortunately, action reward is not designed for these use cases. It assumes under the hood that the demand follows a negative binomial distribution defined by the mean and distribution in inputs.
For order multipliers, you could work you way around this limitation by converting every input of action reward (stock available, stock on order, baseline) in number of multipliers and then multiply the outputs by the size of the multiplier. This is far from perfect as you'll have to make rounding approximations during the conversions.
For MOQ, I'd say that action reward is not built for such use cases.

bperraudin Nov 16, 2023 | flag | on: Output of actionrwd function

There are 2 outputs for the function actionrwd.rwd (see the documentation here: https://docs.lokad.com/reference/abc/actionrwd.reward/):

- Demand: it estimates the probability distribution of the demand that is not covered yet by existing stock and that happens within the coverage timespan. This distribution may have non-zero probabilities in the negative values: this simply means that even if no order is placed, the entire demand might still be covered, with stock remaining at the end of the coverage timespan.
- Holding Time: estimates, for each extra unit that could be purchased, the average time it will stay in stock before being sold.

From your description, I assume you are talking of the Demand output. To complement the explanation of the documentation, you can also see this demand output as the probability to need x additional units in stock to satisfy the future demand. If x is negative, you get the probability of reaching the end of the timespan with abs(x) units in stock without making any additional order.

I hope this helps,

bperraudin Nov 16, 2023 | flag | on: List of Parameters for ActionRwD functions

Hello,

The exhaustive list of parameters is available in the documentation page of the function actionrwd.reward, in the section function signature: https://docs.lokad.com/reference/abc/actionrwd.reward/.

However, this will not help you to integrate MOQs or Order Multipliers. These constraints cannot and should not be treated using actionrwd. Indeed, the main output of actionrwd is the probability distribution of the customer demand that is not covered yet by existing stock (on hand or on order). This has no reason to be affected by MOQ/Multiplier constraints.
However, once the demand left to satisfy is obtained through actionrwd, an economic optimization must be performed to determine the best decision to take, given that demand and other parameters/constraints. It is in this optimization that the MOQ/Mulitplier constraints must be taken into account.

If a product with a demand left to satisfy on the time period considered of 5 units has a MOQ of 50 units, the decision of whether or not you should actually purchase the MOQ completely depends on economical factors. If the product is expensive and has a low margin you might be reluctant to purchase it, if it is cheap and very profitable you might want to do it. This also possibly depends on other parameters as well like budget or storage limitation which can be integrated in an economical optimization.

I hope this helps,

bperraudin Nov 13, 2023 | flag | on: Implement Forecast at Monthly level

Hello,

You'll find below an example of code that can help you generate a daily and a monthly forecast from an existing weekly forecast. I hope this will help.

The methodology I followed fits what was described in previous comments:
1. Compute the weight of each day of the week in the whole horizon considers for each group of products.
2. Multiply this weight to an already computed weekly baseline to get a daily baseline.
3. Aggregate at month level.

This is only example to be adapted to your specific use case, here are the assumptions I made:
1. The weight might differ on the seasonality group: if not the granularity can be changed or the weight can simply be computed for the whole dataset. For categories with very few sales, this logic might overfit
2. The horizon contains only full weeks or is sufficiently big for considering that having 1 extra occurrence of a given week day is negligible.
3. The weight of the days is constant over the whole horizon. In particular we completely neglect here the impact of events like Black Friday.


///Create necessary tables
table WeekDays = extend.range(7)
WeekDays.DayNum = WeekDays.N - 1 //DayNum between 0 and 6
table GroupsWeekDays = cross(Groups,WeekDays) //Groups being an existing table with 1 line per seasonality group

Sales.DayNum = Sales.Date - monday(Sales.Date)
GroupsWeekDays.DemandQty = sum(Sales.DeliveryQty) by [Items.SeasonalityGroup,Sales.DayNum] at [Groups.SeasonalityGroup,WeekDays.DayNum]
GroupsWeekDays.WeightDay = GroupsWeekDays.DemandQty /. sum(GroupsWeekDays.DemandQty) by GroupsWeekDays.SeasonalityGroup

///Compute daily forecast
table ItemsDay = cross(Items,Day)
Day.DayNum = Day.Date - monday(Day.Date)
ItemsDay.Baseline = ItemsWeek.Baseline * single(GroupsWeekDays.WeightDay) by [Groups.SeasonalityGroup,WeekDays.DayNum] at [Items.SeasonalityGroup,Day.DayNum]
ItemsDay.DemandQty = sum(Sales.DeliveryQty)

///Compute monthly forecast
table ItemsMonth = cross(Items,Month)
ItemsMonth.DemandQty = sum(Sales.DeliveryQty)
ItemsMonth.Baseline = sum(ItemsDay.Baseline) //mind partial months when analyzing the results