示例#1
0
        public void Expand_Two_Markdowns_Two_Weeks()
        {
            var schedule    = SmDenseSchedule.FromInteger(Convert.ToInt32("11", 2), 10, 2);
            var priceLadder = new SmPriceLadder
            {
                Values = new [] { 0.1M, 0.5M, 0.9M },
                Type   = SmPriceLadderType.Percent
            };

            var expected = new[]
            {
                new SmSchedulePricePath {
                    LadderType = SmPriceLadderType.Percent, MarkdownCount = 2, Prices = new decimal?[] { 0.1M, 0.5M }, Weeks = new [] { 10, 11 }
                },
                new SmSchedulePricePath {
                    LadderType = SmPriceLadderType.Percent, MarkdownCount = 2, Prices = new decimal?[] { 0.5M, 0.9M }, Weeks = new [] { 10, 11 }
                },
                new SmSchedulePricePath {
                    LadderType = SmPriceLadderType.Percent, MarkdownCount = 2, Prices = new decimal?[] { 0.1M, 0.9M }, Weeks = new [] { 10, 11 }
                }
            };

            var result = schedule.Expand(priceLadder);

            result.ShouldAllBeEquivalentTo(expected);
        }
示例#2
0
        public void Expand_Two_Markdowns_On_Second_And_Fourth_Week_With_Null_First()
        {
            var schedule    = SmDenseSchedule.FromInteger(Convert.ToInt32("1010", 2), 10, 4);
            var priceLadder = new SmPriceLadder
            {
                Values = new [] { 0.1M, 0.5M, 0.9M },
                Type   = SmPriceLadderType.Percent
            };

            var expected = new[]
            {
                new SmSchedulePricePath {
                    LadderType = SmPriceLadderType.Percent, MarkdownCount = 2, Prices = new [] { (decimal?)null, 0.1M, 0.1M, 0.5M }, Weeks = new [] { 10, 11, 12, 13 }
                },
                new SmSchedulePricePath {
                    LadderType = SmPriceLadderType.Percent, MarkdownCount = 2, Prices = new [] { (decimal?)null, 0.5M, 0.5M, 0.9M }, Weeks = new [] { 10, 11, 12, 13 }
                },
                new SmSchedulePricePath {
                    LadderType = SmPriceLadderType.Percent, MarkdownCount = 2, Prices = new [] { (decimal?)null, 0.1M, 0.1M, 0.9M }, Weeks = new [] { 10, 11, 12, 13 }
                }
            };

            var result = schedule.Expand(priceLadder);

            result.ShouldAllBeEquivalentTo(expected);
        }
示例#3
0
        public void Expand_One_Markdown_One_Week()
        {
            var schedule    = SmDenseSchedule.FromInteger(1, 10, 1);
            var priceLadder = new SmPriceLadder
            {
                Values = new [] { 0.1M, 0.5M, 0.9M },
                Type   = SmPriceLadderType.Percent
            };

            var expected = new[]
            {
                new SmSchedulePricePath {
                    LadderType = SmPriceLadderType.Percent, MarkdownCount = 1, Prices = new decimal?[] { 0.1M }, Weeks = new [] { 10 }
                },
                new SmSchedulePricePath {
                    LadderType = SmPriceLadderType.Percent, MarkdownCount = 1, Prices = new decimal?[] { 0.5M }, Weeks = new [] { 10 }
                },
                new SmSchedulePricePath {
                    LadderType = SmPriceLadderType.Percent, MarkdownCount = 1, Prices = new decimal?[] { 0.9M }, Weeks = new [] { 10 }
                }
            };

            var result = schedule.Expand(priceLadder);

            result.ShouldAllBeEquivalentTo(expected);
        }
示例#4
0
        public void Optimise_Constraints_Are_Always_Markdowns()
        {
            char[,] expected =
            {
                { '_', '_', '_', '_' },
                { '_', '_', '_', '_' },
                { 'x', '_', '_', '_' },
                { 'x', 'x', '_', '_' },
                { 'x', 'x', 'x', '_' },
                { '_', 'x', 'x', 'x' },
                { '_', 'x', 'x', 'x' },
                { '_', '_', '_', 'x' },
                { '_', '_', '_', 'x' }
            };

            var priceLadder = new SmPriceLadder
            {
                Values = new[] { 0.1M, 0.2M, 0.3M, 0.4M, 0.5M, 0.6M, 0.7M, 0.8M, 0.9M },
                Type   = SmPriceLadderType.Percent
            };

            var schedule = SmDenseSchedule.FromInteger(170, 1, 8);

            schedule.Constraints = new[]
            {
                SmWeekConstraint.Range(2, 0.3M, 0.5M),
                SmWeekConstraint.Range(6, 0.5M, 0.7M)
            };

            var result = SmDenseSchedule.Optimise(schedule, priceLadder);

            Simplify(priceLadder, schedule, result)
            .Should()
            .Equal(expected, (left, right) => left == right);
        }
示例#5
0
        public void Optimise_Identifies_All_Potential_Week_Price_Values()
        {
            char[,] expected =
            {
                { 'x', '_', '_', '_' },
                { 'x', 'x', '_', '_' },
                { 'x', 'x', 'x', '_' },
                { 'x', 'x', 'x', 'x' },
                { 'x', 'x', 'x', 'x' },
                { 'x', 'x', 'x', 'x' },
                { '_', 'x', 'x', 'x' },
                { '_', '_', 'x', 'x' },
                { '_', '_', '_', 'x' }
            };

            var priceLadder = new SmPriceLadder
            {
                Values = new [] { 0.1M, 0.2M, 0.3M, 0.4M, 0.5M, 0.6M, 0.7M, 0.8M, 0.9M },
                Type   = SmPriceLadderType.Percent
            };
            var schedule = SmDenseSchedule.FromInteger(170, 1, 8);
            var result   = SmDenseSchedule.Optimise(schedule, priceLadder);

            Simplify(priceLadder, schedule, result)
            .Should()
            .Equal(expected, (left, right) => left == right);
        }
示例#6
0
        private static char[,] Simplify(SmPriceLadder priceLadder, SmDenseSchedule schedule, ICollection <Tuple <int, decimal> > test)
        {
            var result = new char[priceLadder.Values.Length, schedule.MarkdownWeeks.Length];

            for (var y = 0; y < priceLadder.Values.Length; y++)
            {
                for (var x = 0; x < schedule.MarkdownWeeks.Length; x++)
                {
                    result[y, x] = test.Contains(Tuple.Create(schedule.MarkdownWeeks[x], priceLadder.Values[y]))
                        ? 'x'
                        : '_';
                }
            }
            return(result);
        }
示例#7
0
        private static List <SmDenseSchedule> Generate(int firstWeek, int weekCount, int weeksAllowed, int weeksRequired, bool filterConsecutiveWeeks, List <SmWeekConstraint> constraints)
        {
            if (firstWeek < 0)
            {
                throw new ArgumentException("Cannot be < 0", nameof(firstWeek));
            }

            if (weekCount < 1)
            {
                throw new ArgumentException("Cannot be < 1", nameof(weekCount));
            }

            var range = Enumerable.Range(1, (int)Math.Pow(2, weekCount) - 1);

            // Any of these bits are permissible
            if (weeksAllowed > 0)
            {
                range = range.Where(x => x > 0 && (x & ~weeksAllowed) == 0);
            }

            // Any of these bits are must be set
            if (weeksRequired > 0)
            {
                range = range.Where(x => x > 0 && (x & weeksRequired) == weeksRequired);
            }

            // Filter consecutive weeks
            if (filterConsecutiveWeeks)
            {
                FilterConsecutiveWeeks(ref range, weekCount);
            }

            return(range
                   .Select(i => SmDenseSchedule.FromInteger(i, firstWeek, weekCount, constraints))
                   .OrderBy(x => x.MarkdownCount)
                   .ThenBy(x => x.ScheduleNumber)
                   .ToList());
        }
示例#8
0
        public void No_Markdowns()
        {
            var schedule    = SmDenseSchedule.NoMarkdowns(10, 13);
            var priceLadder = new SmPriceLadder
            {
                Values = new [] { 0.1M, 0.5M, 0.9M },
                Type   = SmPriceLadderType.Percent
            };

            var expected = new []
            {
                new SmSchedulePricePath
                {
                    LadderType    = SmPriceLadderType.Percent,
                    MarkdownCount = 0,
                    Prices        = new decimal?[] { null, null, null, null },
                    Weeks         = new[] { 10, 11, 12, 13 }
                }
            };

            var result = schedule.Expand(priceLadder);

            result.ShouldBeEquivalentTo(expected);
        }
示例#9
0
        private async Task <VmRecommendation> Revise(int clientId, SmRecommendationProductSummary product, List <VmScenarioRevision> revisions)
        {
            var constraints = revisions
                              .Take(1)
                              .Concat(revisions.Zip(revisions.Skip(1), (first, second) => second.Week > first.Week && second.Discount > first.Discount ? second : null))
                              .Where(x => x != null)
                              .Select(x => SmWeekConstraint.Fixed(x.Week, (decimal)x.Discount))
                              .ToList();

            if (!constraints.Any())
            {
                throw new HttpStatusCodeException(HttpStatusCode.BadRequest, "No markdowns in the revision model data");
            }

            var scenarioSummary = await _scenarioWebService.Get(clientId, product.ScenarioId);

            if (scenarioSummary == null)
            {
                throw new HttpStatusCodeException(HttpStatusCode.InternalServerError,
                                                  $"Missing scenario for client id ${product.ClientId} with scenario id ${product.ScenarioId}");
            }

            var weeks = Enumerable.Range(scenarioSummary.Scenario.ScheduleWeekMin, scenarioSummary.Scenario.ScheduleWeekMax - scenarioSummary.Scenario.ScheduleWeekMin + 1).ToList();

            if (constraints.Select(x => x.Week).Except(weeks).Any())
            {
                throw new HttpStatusCodeException(HttpStatusCode.BadRequest, "All revision weeks must be within [ScheduleWeekMin..ScheduleWeekMax]");
            }

            var settings = MarkdownFunctionSettings.FromWebApiConfiguration(product.ModelId, 100,
                                                                            product.ScenarioId, _organisationDataProvider.OrganisationId.Value,
                                                                            _organisationDataProvider.UserId.Value, product.PartitionNumber,
                                                                            product.PartitionCount);

            var modelData = await _scenarioService.GetModelData(settings);

            var scenarioData = await _scenarioService.GetScenarioData(settings, settings.PartitionId);

            var productData = scenarioData.Item2.FirstOrDefault(x => x.ProductId == product.ProductId);

            if (productData == null)
            {
                throw new HttpStatusCodeException(HttpStatusCode.InternalServerError, "Missing original product by id " + product.ProductId);
            }

            var revisionValues = revisions.Select(x => (decimal)x.Discount).ToList();

            var constraintValues = constraints
                                   .Select(x => x.Min)
                                   .Concat(constraints.Select(x => x.Max))
                                   .Distinct()
                                   .Where(x => x != null)
                                   .Select(x => (decimal)x)
                                   .ToList();

            if (constraintValues.Except(productData.PriceLadder.Values).Any())
            {
                throw new HttpStatusCodeException(HttpStatusCode.BadRequest, "All revision discounts must be price ladder values");
            }

            var modelId = product.ModelId;

            var revisionId = product.RevisionCount + 1;

            // Caculate the mask passed in
            var revisionMask = weeks
                               .Select((x, i) => new { Week = x, Index = i })
                               .Where(item => constraints.Any(x => x.Week == item.Week))
                               .Aggregate(0, (current, item) => current | 1 << item.Index);

            var schedules = new List <SmDenseSchedule>
            {
                SmDenseSchedule.FromInteger(revisionMask, weeks.First(), weeks.Count, constraints)
            };

            // Calculate
            var result = _markdownService.Calculate(scenarioSummary.Scenario, modelId, revisionId, schedules,
                                                    modelData.DecayHierarchies, modelData.ElasticityHierarchies, productData, revisionValues);

            // Upload
            await _scenarioResultsService.Upload(settings, product, revisionId, result.Recommendations, _organisationDataProvider.UserId.Value);

            // Update state and retrieve
            var finalResult = await _recommendationProductService.Revise(

                clientId : product.ClientId,
                scenarioId : product.ScenarioId,
                productId : product.ProductId);

            return(VmRecommendation.Build(finalResult));
        }
示例#10
0
        public SmCalcProduct Calculate(
            SmScenario scenario,
            int modelId,
            int revisionId,
            List <SmDenseSchedule> schedules,
            Dictionary <int, SmDecayHierarchy> decayHierarchies,
            Dictionary <int, SmElasticityHierarchy> elasticityHierarchies,
            SmProduct product,
            List <decimal> revisedDiscounts     = null,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            // Calculate the current markdown depth and ladder depth
            var depth = GetDiscountLadderDepth(product);

            var result = new SmCalcProduct(scenario, modelId, product, schedules, depth);

            // Return OK if there are no schedules for this product
            if (!schedules.Any())
            {
                _logger.Warning("No schedules for {ProductId}", product.ProductId);
                return(result.Ok(ProductState.NoSchedules));
            }

            // Return Fatal recommendation for bad CSPs
            if (product.CurrentSellingPrice <= 0)
            {
                _logger.Warning("Product {ProductId} CSP is <= 0", product.ProductId);
                return(result.Fatal(ProductState.InvalidCsp));
            }

            // Return Fatal if we can't resolve the decay hierarchy
            if (!decayHierarchies.TryGetValue(product.HierarchyId, out SmDecayHierarchy decayHierarchy))
            {
                _logger.Warning("Can't find Decay Hierarchy by HierarchyId {HierarchyId} for Product {ProductId}", product.HierarchyId, product.ProductId);
                return(result.Fatal(ProductState.InvalidDecayHierarchy));
            }

            // Return Fatal if we can't resolve the elasticity hierarchy
            if (!elasticityHierarchies.TryGetValue(product.HierarchyId, out SmElasticityHierarchy elasticityHierarchy))
            {
                _logger.Warning("Can't find Elasticity Hierarchy by HierarchyId {HierarchyId} for Product {ProductId}", product.HierarchyId, product.ProductId);
                return(result.Fatal(ProductState.InvalidElasticityHierarchy));
            }

            var recommendations = new List <SmCalcRecommendation>();

            foreach (var schedule in schedules)
            {
                // Apply the product mask to this schedule and skip of not aligned
                if (product.ProductScheduleMask != null && (schedule.ScheduleNumber & product.ProductScheduleMask.Value) != 0)
                {
                    result.ScheduleProductMaskFilterCount++;
                    continue;
                }

                // Skip products where the max markdown is exceeded
                if (product.ProductMaxMarkdown != null && product.ProductMaxMarkdown.Value >= schedule.MarkdownCount)
                {
                    result.ScheduleMaxMarkdownFilterCount++;
                    continue;
                }

                if (product.ProductHasExceededFlowlineThreshold == 1)
                {
                    result.ScheduleExceededFlowlineThresholdFilterCount++;
                    continue;
                }

                var crossProduct = GetCrossProduct(schedule, product.PriceLadder);
                result.ScheduleCrossProductCount = crossProduct.Length;

                foreach (var path in crossProduct)
                {
                    var prices         = path.Prices.Select(x => x ?? 0).ToArray();
                    var recommendation = new SmCalcRecommendation(scenario, schedule, prices, revisionId);

                    var calculateResult = CalculatePricePath(
                        product: ref result,
                        recommendation: ref recommendation,
                        scenario: scenario,
                        modelId: modelId,
                        revisionId: revisionId,
                        decayHierarchy: decayHierarchy,
                        elasticityHierarchy: elasticityHierarchy,
                        priceLadder: product.PriceLadder,
                        scheduleId: schedule.ScheduleNumber,
                        schedulePricePath: path);

                    if (calculateResult)
                    {
                        // Get top 10 by Total Revenue
                        recommendations.InsertAfter(recommendation, 10, x => recommendation.TotalRevenue > x.TotalRevenue);
                    }

                    cancellationToken.ThrowIfCancellationRequested();
                }
            }

            // For initial runs, calculate CSP (where revision id = 0)
            if (revisionId == 0)
            {
                var noChangeSchedule = SmDenseSchedule.NoMarkdowns(scenario.ScheduleWeekMin, scenario.ScheduleWeekMax);
                var cspPricePath     = SmSchedulePricePath.Build(noChangeSchedule.WeekMin, noChangeSchedule.WeekMax, noChangeSchedule.MarkdownWeeks, product.PriceLadder.Type, product.PriceLadder.Values);

                var prices            = cspPricePath.Prices.Select(x => x ?? 0).ToArray();
                var cspRecommendation = new SmCalcRecommendation(scenario, noChangeSchedule, prices, revisionId, isCsp: true);

                var cspResult = CalculatePricePath(
                    product: ref result,
                    recommendation: ref cspRecommendation,
                    scenario: scenario,
                    modelId: modelId,
                    revisionId: revisionId,
                    decayHierarchy: decayHierarchy,
                    elasticityHierarchy: elasticityHierarchy,
                    priceLadder: product.PriceLadder,
                    scheduleId: noChangeSchedule.ScheduleNumber,
                    schedulePricePath: cspPricePath);

                if (cspResult)
                {
                    recommendations.InsertAfter(cspRecommendation, recommendations.Count + 1, x => cspRecommendation.TotalRevenue > x.TotalRevenue, true);
                }

                result.ScheduleCount++;
            }

            if (!recommendations.Any())
            {
                _logger.Warning("No recommendations made for {ProductId}", product.ProductId);
                return(result.Ok());
            }

            // Set rank, store and set stats
            var results = new List <SmCalcRecommendation>();

            foreach (var ordered in recommendations.Select((x, i) => new { recomendation = x, index = i }))
            {
                var recommendation = ordered.recomendation;
                recommendation.Rank = recommendations.Count - ordered.index;
                results.Add(recommendation);
            }

            ExpandProjectionWeeks(ref result, ref results, scenario, revisedDiscounts);
            return(result.Ok(results));
        }
示例#11
0
        private SmSchedulePricePath[] GetCrossProduct(SmDenseSchedule schedule, SmProductPriceLadder ladder)
        {
            var constraintId = schedule.Constraints.GetHashCode();
            var key          = new { schedule.MarkdownCount, Mask = schedule.ScheduleNumber, ladder.PriceLadderId, constraintId };

            if (!_ladderPathCache.TryGetValue(key, out SmSchedulePricePath[] result))