Welcome to supplychainpy’s documentation!

Contents:

Change Log

0.0.5

Application

  • [Bug Fix] Using Flask’s web server for the Dashboard on a public route on a standalone server (–host 0.0.0.0)
  • [Bug Fix] Javascript error while loading dashboard.
  • [Bug Fix] Pip install error (Log.txt FileNotFound)
  • [New Feature] Basic ability to run Monte Carlo Simulation and view summarised results in reporting suite.
  • [Update] Load scripts use multi-processing for forecast calculations when processing data file.
  • [Update] Load scripts using batch process.
  • [Update] Debug commandline argument for viewing logging output `–debug’.
  • [Update] Use Chat Bot from commandline with -c flag. EXPERIMENTAL
  • [Update] Recommendation generator takes into account forecasts
  • [Update] Flask Blueprints used for reporting views.

Documentation

  • [New] Wiki started on GitHub for more responsive updates to documentation including changes to source during development.
  • [Update] Tutorial.

0.0.4

Release 0.0.4 has breaking API changes. Namespaces have changed in this release. All the modules previously in the “demand” package are now inside the “inventory” package. If you have been using the “model_inventory” module, then nothing has changed, there will not be any break in contracts.

Application

  • [Update] Explicit internal and public API.
  • [Update] Excess, shortages added to the UncertainDemand order_summary.
  • [Update] Moved abc_xyz.py, analyse_uncertain_demand.py, economic_order_quantity.py and eoq.pyx from “demand” to “inventory” package
  • [Update] “demand” package now contains: evolutionary_algorithms.py, forecast_demand.py and regression.py
  • [Update] retail_price added to model_inventory.analyse_orders.
  • [Update] backlog added to the data format for loading into the analysis.
  • [Update] Unit Tests.
  • [Update] Docstrings.
  • [New Feature] Analytic Hierarchy Process.
  • [New Feature] API supports Pandas DataFrame.
  • [New Feature] Browser based reporting suite, with charts, data summaries and integrated chat bot.
  • [New Feature] Dash Bot, a basic chat bot assistant for the data in the reporting suite. Query data using natural language.
  • [New Feature] Command line interface for processing .csv to database, launching reports and chat bot.
  • [New Feature] “Model_Demand” module containing simple exponential smoothing and holts trend corrected exponential smoothing.
  • [New Feature] Summarise and filter your analysis.
  • [New Feature] Holts Trend Corrected Exponential Smoothing Forecast and optimised variant (evolutionary algorithm for optimised alpha and gamma)
  • [New Feature] Simple Exponential Smoothing (evolutionary algorithm for optimised alpha).
  • [New Feature] Evolutionary Algorithms for Smoothing Level Constants (converges on better smoothing levels using genetic algorithm)
  • [New Feature] SKU and inventory profile recommendations generator.

Documentation

  • [New] Reporting Suite Walk Through.
  • [Update] Tutorial.
  • [Update] Quick Guide.
  • [New] Declare public API explicitly. describe and document each module and function, give an example also add to website tutorial as Jupyter notebook.
  • [New] Docker for supplychainpy quick guide.
  • [New] Analytic Hierarchy Process quick guide.
  • [New] Inventory Modeling.
  • [New] Demand Planning with Pandas.

0.0.3

Application

  • Compiled Cython (eoq and simulation modules) for OS X, Windows and Linux.
  • Removed z_value, file_type, file_path and reorder_cost parameters from simulate.run_monte_carlo.

Documentation

  • Update Quick Guide

0.0.2

Application

  • Added monte carlo simulation and simulation summary using Cython optimisation.
  • Added orders analysis optimisation, based on results of the monte carlo simulation.
  • Added simulate module to api.
  • Added weighted moving average forecast.
  • Added moving average forecast.
  • Added mean absolute deviation.
  • Updated economic order quantity using Cython optimisation.
  • Updated unit tests.

Documentation

  • Updates Quick Guide.
  • Updated Tutorial.
  • Updated README.md
  • Added Formulas and Equations.
  • Updated data.csv.

0.0.1

Application

  • Added inventory analysis for uncertain demand. Analyse orders from .csv, .txt or from dict.
  • Added inventory analysis summary for uncertain demand. ABC XYZ, economic order quantity (EOQ), reorder level (ROL), demand variability and safety stock.

Documentation

  • Added Quick Guide.
  • Added Tutorial.
  • Added Installation.

Options

Currently using the reporting suite feature, requires the command line. Using a nix or PowerShell console, the typical command issued for processing a file and generating a report would be:

$ supplychainpy <filename> -a -loc <absolute-path-to-current-directory> -l

The command line arguments can be viewed by using the –help command in the cli.

  • -l, –launch

    • Launches supplychainpy reporting GUI for setting port and launching the default browser. The reporting suite is hosted inside the browser and defaults to port 5000. The GUI provides the opportunity to change the port if necessary. The -l flag is a boolean flag, and its inclusion is essential if the aim is to launch into the reporting suite.
  • -loc

    • Flag for the aboslute path to the current directory. The path is required to locate a current reporting.db or a store as the new location in the settings.
  • -a, –analyse

    • Initiates the analyisis of the file name supplied as the first argument directly after the supplychainpy command e.g.:

      $ supplychainpy <filename> -a -loc <absolute-path-to-current-directory> -l

  • -lx, –launch-console

    • Launches supplychainpy reporting on the default port, without GUI interface. Uses default port (5000) unless another port is specified. Appropriate for running the reports on a sever. Currently, this is only for testing. The Werkzeug web server that supplied with flask serves the pages for the reports. Werkzeug is not a production web server. It is sufficient as a local web server for the reporting application on a client system. In the coming releases deployment using a more robust web server such as Nginx or Gunicorn will be documented for Server type implementation.
  • -cur

    • The flag Sets the currency for the analysis and should match the raw data. IMPORTANT: Currency conversion does not occur by setting this flag. The default currency is US Dollars (USD).
  • –host

    • Sets the host for the server (defaults 127.0.0.1).
  • –debug

    • Runs in debug mode.
  • -p, –port

    • Specify Port to use for local server e.g. 8080 (default: 5000).
  • -c

    • Enter chat mode with the Dash bot from the command line.

Installation

The easiest way to install supplychainpy is via pip:

pip install supplychainpy
python -m textblob.dowload_copora

The option also exists to install from source. Clone the package from Github.

Python Version

  • Python 3.5

Dependencies

  • Numpy
  • Pandas
  • Flask
  • Flask-Restful
  • Flask-Restless
  • Flask-Script
  • Flask-SqlAlchemy
  • Flask-Uploads
  • Flask-WTF
  • Scipy
  • SqlAlchemy
  • TextBlob

Optional Dependencies

  • matplotlib
  • openpyxl
  • xlwings

Installing the Anaconda package may be preferable as it comes with Python 3.5 and all the dependencies.

Quick Guide

Warning

The library is currently under development and in planning stages. The library should not be used in production at this time.

Overview

Supplychainpy is a Python library for supply chain analysis, modelling and simulation. The library assists a workflow that is reliant on spreadsheets.

This quick guide assumes analysts have the requisite domain knowledge, and predominantly use Excel. Some knowledge of Python or programming is assumed, although those new to data analysis or using Python will likely be able to follow with assistance from other material.

The following guide assumes that the supplychainpy library has already been installed. If not, please use the instructions for Installation.

Up and Running

Typically, inventory analysis requires several formulas, manual processes, possibly some pivot tables and in some cases VBA. Using the supplychainpy library can reduce the time taken and effort made for the same analysis.

A simple analysis for an individual SKU can be carried out by using:

>>> from supplychainpy import model_inventory
>>> yearly_demand = {'jan': 75, 'feb': 75, 'mar': 75, 'apr': 75,
>>>                 'may': 75, 'jun': 75, 'jul': 25,'aug': 25,
>>>                 'sep': 25, 'oct': 25, 'nov': 25, 'dec': 25}
>>> summary = model_inventory.analyse_orders(self._yearly_demand,  sku_id='RX983-90', lead_time= Decimal(3),
>>>                                             unit_cost=Decimal(50.99), reorder_cost=Decimal(400),
>>>                                             z_value=Decimal(1.28), retail_price=Decimal(600), quantity_on_hand=Decimal(390)))
>>> print(summary)
{'revenue': '360000',
'total_orders': '600',
'orders': {'feb': 75, 'dec': 25, 'jan': 75, 'jun': 75, 'may': 75, 'mar': 75, 'aug': 25, 'sep': 25, 'jul': 25, 'oct': 25, 'nov': 25, 'apr': 75},
'shortages': '0',
'reorder_level': '142',
'safety_stock': '55',
'average_orders': '50',
'standard_deviation': '25',
'excess_stock': '161',
'sku': 'RX983-90',
'ABC_XYZ_Classification': '',
'demand_variability': '0.500',
'reorder_quantity': '56',
'quantity_on_hand': '390',
'currency': 'USD',
'unit_cost': '50.99'}

Note

The signature for the analysed_orders function has changed. Moving from release-0.0.3 to release-0.0.4, Retail price and quantity on hand are required arguments.

The same analysis can be made by supplying a pre-formatted .csv, .txt or Pandas DataFrame containing several SKU or entire inventory profile. The format for the file can be found ` here <https://github.com/KevinFasusi/supplychainpy/blob/master/supplychainpy/sample_data/complete_dataset_small.csv>`_ An example using file:

>>> from supplychainpy.model_inventory import analyse
>>> from supplychainpy.sample_data.config import ABS_FILE_PATH
>>> from decimal import Decimal
>>> analysed_data = analyse(file_path=ABS_FILE_PATH['COMPLETE_CSV_SM'],
...                         z_value=Decimal(1.28),
...                         reorder_cost=Decimal(400),
...                         retail_price=Decimal(455),
...                         file_type='csv',
...                         currency='USD')
>>> analysis = [demand.orders_summary() for demand in analysed_data]
{'quantity_on_hand': '1003',
'currency': 'USD',
'orders': {'demand': ('1509', '1855', '2665', '1841', '1231', '2598', '1988', '1988', '2927', '2707', '731', '2598')},
'economic_order_variable_cost': '15708.41',
 'ABC_XYZ_Classification': 'BY',
 'reorder_level': '4069',
 'safety_stock': '1165',
 'shortages': '5969',
 'demand_variability': '0.314',
 'excess_stock': '0',
 'standard_deviation': '644',
 'average_orders': '2053.1667',
 'unit_cost': '1001',
 'economic_order_quantity': '44',
 'reorder_quantity': '13',
 'revenue': '123190000',
 'sku': 'KR202-209',
 'total_orders': '24638'},

The library also supports Pandas using a DataFrame. The following example shows how to use the library to perform an inventory analysis if a DataFrame is the preference:

>>> import pandas as pd
>>> r_df = pd.read_csv(ABS_FILE_PATH['COMPLETE_CSV_SM'])
>>> analyse_kv = dict(
...     df=raw_df,
...     start=1,
...     interval_length=12,
...     interval_type='months',
...     z_value=Decimal(1.28),
...     reorder_cost=Decimal(400),
...     retail_price=Decimal(455),
...     currency='USD'
... )
>>> analysis_df = analyse(**analyse_kv)

Summarising the Analysis

Use the describe_sku method a retrieve a summary for a specific skus:

>>> from supplychainpy.inventory.summarise import Inventory
>>> from supplychainpy.model_inventory import analyse
>>> from supplychainpy.sample_data.config import ABS_FILE_PATH
>>> from decimal import Decimal
>>> analysed_data = analyse(file_path=ABS_FILE_PATH['COMPLETE_CSV_SM'],
...                         z_value=Decimal(1.28),
...                         reorder_cost=Decimal(400),
...                         retail_price=Decimal(455),
...                         file_type='csv',
...                         currency='USD')
>>> filtered_summary = Inventory(processed_orders=analysed_orders)
>>> sku_summary = [summary for summary in filtered_summary.describe_sku('KR202-209')]
>>> print(sku_summary)
{'economic_order_quantity': '44',
'ABC_XYZ_Classification': 'BY',
'sku': 'KR202-209',
'shortages': '5969',
'demand_variability': '0.314',
'reorder_level': '4069',
'reorder_quantity': '13',
'unit_cost': '1001',
'currency': 'UNKNOWN',
'standard_deviation': '644',
'revenue': '123190000',
'average_orders': '2053.1667',
'safety_stock': '1165',
'quantity_on_hand': '1003',
'orders': {'demand': ('1509', '1855', '2665', '1841', '1231', '2598', '1988', '1988', '2927', '2707', '731', '2598')},
'excess_stock': '0',
'economic_order_variable_cost': '15708.41',
'total_orders': '24638'}

For more coverage of the library please take a look at the Jupyter notebooks is available from here . The content of notebooks can be found in Inventory Modeling and Analysis Made Easy with Supplychainpy and Using supplychainpy with Pandas, Jupyter and Matplotlib.

Supplychainpy Reporting Suite

To further indicate the merit of the library in a more direct way and showcase the possibilities offered, release 0.0.4 debuts a reporting feature. The supplychainpy reporting feature allows analysts to get a quick overview of their analysis and provides a tool for communicating their insights very quickly. The reports bring some of the data analysis capabilities of the library to life with a complimentary suite of charts, tables, KPIs and an interactive Bot.

The reports aim to:

  1. provide the ability to visualise data and spot trends, allowing analysts to get a “feel” for their data.
  2. provide a set of generic default reports, to showcase some general uses cases and highlight the capabilities of the library.
  3. identify areas of interest for further exploration.

Launch from Cli

The command line arguments can be viewed using the –help flag. There are several options for launching the reports. Using the the reporting function generates a sqlite database called reporting. To process a CSV file and initiate the reporting suite directly after, navigate to a directory suitable for storing the CSV and resulting database. Use the following commands:

Linux and Mac

supplychainpy filename.csv -a -loc ~/absolute/path/to/current/directory -l -cur EUR

Windows

supplychainpy filename.csv -a -loc drive:\absolute\path\to\current\directory -l -cur EUR

Importantly the the currency flag (-cur) if unspecified will default to USD. Other optional arguments include the host (–host default: 127.0.0.1 ) and port (-p default: 5000) arguments. Setting the host and ports allows the -l arguments can be replaced by the -lx. The -l arguments launch a small intermediary GUI for setting the port before launching the reports in a web browser. The -lx argument start the reporting process but does not launch a GUI or a browser window and instead expects the user to open the browser and navigate to the address hosting the reports as specified in the CLI.

Reporting Suite Walk Through

The reporting suite launches on the ‘Dashboard’ page. The Dashboard is split into three section: Classification Breakdown, Top 10 Shortages and Top 10 Excess. The Dashboard hosts three toggle switches at the top of the screen for toggling each section in and out of view.

The Head up Display (HUD)

The reports use what we like to refer to as a “Heads up Display” (HUD) to highlight key values and KPIs. As seen below, the HUD is a row of boxes (Slates), that sits on top of the main charts, analysis and tabular breakdowns.

_images/hud.png

Classification Breakdown

The classification breakdown summarises the Inventory Profile using Pareto and variance analysis, to indicate the contribution the SKU makes to revenue and the variability in demand respectively. This section shows which classification has the largest excess and shortage as well as breaking down revenue, shortages and excess by category.

_images/classification.jpg

Top 10 Shortages

This section indicates which 10 SKUs are responsible for the most shortages.

_images/shortage.png

Top 10 Excess

This section indicates which 10 SKUs are responsible for the most excess stock based on their unit cost.

_images/excess.jpg

Analysis Cube

The analysis cube page hosts the tabulated data for all data points within the analysis.

_images/cube.jpg

Dash the Bot

The Chat Bot provides a simply method of querying the analysis using natural language.

_images/dash.gif

Recommendations Feed

The recommendation feed for all the auto-generated recommendation for each SKU.

_images/rec_feed.gif

Inventory Modeling and Analysis Made Easy with Supplychainpy

The following is taken from the jupyter notebook title ‘0.0.4-Inventory-Modeling-v1’ found here . For a more interactive experience please retrieve this notebook and run with jupyter.

This workbook assumes some familiarity and proficiency in programming with Python. Understanding list comprehensions, conditional logic, loops and functions are a basic prequisite for continuing with this workbook.

Typically, inventory analysis using Excel requires several formulas, manual processes, possibly some pivot tables and in some cases VBA to achieve. Using the supplychainpy library can reduce the time taken and effort made for the same analysis.

from supplychainpy.model_inventory import analyse
from decimal import Decimal
from supplychainpy.sample_data.config import ABS_FILE_PATH

The first two imports are manditory, the second import is for using the sample data in the supplychainpy library. When working with a different file, supply the file path to the file_path parameter. The data supplied for analysis can be from a csv or a database ETL process.

The sample data is a csv formatted file:

with open(ABS_FILE_PATH['COMPLETE_CSV_SM'],'r') as raw_data:
    for line in raw_data:
        print(line)
Sku,jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec,unit cost,lead-time,retail_price,quantity_on_hand,backlog

KR202-209,1509,1855,2665,1841,1231,2598,1988,1988,2927,2707,731,2598,1001,2,5000,1003,10

KR202-210,1006,206,2588,670,2768,2809,1475,1537,919,2525,440,2691,394,2,1300,3224,10

KR202-211,1840,2284,850,983,2737,1264,2002,1980,235,1489,218,525,434,4,1200,390,10

KR202-212,104,2262,350,528,2570,1216,1101,2755,2856,2381,1867,2743,474,3,10,390,10

KR202-213,489,954,1112,199,919,330,561,2372,921,1587,1532,1512,514,1,2000,2095,10

KR202-214,2416,2010,2527,1409,1059,890,2837,276,987,2228,1095,1396,554,2,1800,55,10

KR202-215,403,1737,753,1982,2775,380,1561,1230,1262,2249,824,743,594,1,2500,4308,10

KR202-216,2908,929,684,2618,1477,1508,765,43,2550,2157,937,1201,634,3,3033,34,10

KR202-217,2799,2197,1647,2263,224,2987,2366,588,1140,869,1707,1180,674,3,5433,390,10

KR202-218,1333,402,804,318,1408,830,1028,534,1871,2730,2022,94,714,2,3034,3535,10

KR202-219,813,969,745,1001,2732,1987,717,599,2722,171,639,2108,754,3,5000,334,10

KR202-220,1481,905,1067,2513,861,1670,650,2630,1245,997,1936,2780,794,3,7500,3434,10

KR202-221,771,2941,1360,2714,1801,1744,1428,1660,436,578,1956,1101,834,2,4938,4433,10

KR202-222,2349,4,345,524,340,2698,2137,1164,498,1583,1241,2965,874,2,4922,3435,10

KR202-223,2045,2055,552,81,2780,176,2316,1475,2566,1678,1553,2745,914,1,4894,34533,10

KR202-224,2482,1887,1911,1446,2939,1241,1281,692,119,627,1941,1383,954,2,2942,33,10

KR202-225,2744,2770,2697,1726,1776,2264,332,2420,2722,1161,1986,2587,994,6,8999,2000,10

KR202-226,2509,914,903,877,1859,2263,383,593,236,189,920,1686,1034,3,4342,4344,10

KR202-227,368,2502,2955,2994,1270,2884,2208,699,854,877,2320,160,1074,3,4920,489,10

KR202-228,1468,1109,2464,2799,948,589,2858,1140,501,2691,93,1060,1114,2,15000,9439,10

KR202-229,2114,198,1479,1249,1475,744,407,2280,226,2285,796,1948,1154,2,13000,8939,10

KR202-230,1023,1150,1672,2026,1590,441,2484,2300,2928,1082,2064,2412,1194,2,10000,349,10

KR202-231,482,546,299,2304,2953,1029,1863,2809,454,927,2488,2341,1234,4,9999,3434,10

KR202-232,614,2138,962,2017,2398,2963,2189,1804,414,2016,1350,2464,1274,2,7500,234,10

KR202-233,2395,2521,2157,728,1028,43,138,826,570,2825,181,787,1314,4,6000,349,10

KR202-234,1336,1478,865,533,1562,422,2287,1302,1230,1059,1153,399,1354,2,20000,324,10

KR202-235,2565,2762,2721,1431,845,2163,2413,2227,1753,740,1139,2300,1394,3,59500,850,10

KR202-236,1912,1726,1569,316,71,2082,108,174,1974,609,2896,566,1434,3,2300,4930,10

KR202-237,2153,1112,16,130,590,2619,2576,2390,2567,1531,842,242,1474,2,4500,9483,10

KR202-238,1417,2044,1981,1936,2377,780,1544,1521,51,1056,1876,1356,1514,3,8000,839,10

KR202-239,2717,2186,2300,677,2157,2328,1917,2519,561,281,1162,1146,1554,2,39000,433,10

KR202-240,1015,741,2754,2925,2302,695,2869,440,406,1083,2334,1015,1594,3,3943,390,10

KR202-241,3050,1507,3637,1112,1963,1675,898,1986,2262,3895,1229,2904,769,5,8007,2125,10

KR202-242,1875,2368,830,823,868,1409,1845,3095,3247,1894,2558,3048,1819,1,13225,1253,10

KR202-243,1717,593,3006,2935,3139,2753,3247,3845,1720,3413,3399,2799,1120,3,14682,1128,10

KR202-244,2383,2046,2487,3827,1674,3118,2849,2233,3888,2566,2216,3817,1067,5,11997,1191,10

KR202-245,1115,2694,3038,3366,1058,2724,2863,1930,1787,838,3087,1565,1623,2,12876,611,10

KR202-246,3108,1197,2472,1264,3179,3638,1268,1581,3456,1630,1788,2288,608,2,6548,2192,10

KR202-247,3439,1854,652,1827,1645,2257,2733,1337,2034,2106,877,2409,1578,2,10463,1017,10

It is probable that getting the data to this format will require ‘extracting’ from a database and ‘transforming’ data before ‘loading’ into the analyse function. This can be achieved with an orm like slqalchemy or using the driver for the database in question. Supplychainpy works with Pandas so performing the transformations using Pandas may be an idea. The DataFrame or file passed as an argument must be identical to the format above (future versions of supplychainpy will be more lenient and attempt to identify if a minimum requirement has been met).

The ETL process is not covered in this workbook but on the ‘roadmap’ for supplychainpy is the automation of this process.

So now that we have the data in the correct format we can proceed with the anlysis.

#%%timeit
analysed_inventory_profile= analyse(file_path=ABS_FILE_PATH['COMPLETE_CSV_SM'],
                                                             z_value=Decimal(1.28),
                                                             reorder_cost=Decimal(400),
                                                             file_type='csv')

The variable analysed_inventory_profile now contains a collection (list) of UncertainDemand objects (one per SKU). Each object contains the analysis for each SKU. The analysis include the following:

  • safety stock
  • total_orders
  • standard_deviation
  • quantity_on_hand’: ‘1003
  • economic_order_variable_cost
  • sku
  • economic_order_quantity
  • unit_cost
  • demand_variability
  • average_orders
  • excess_stock
  • currency
  • ABC_XYZ_Classification
  • shortages
  • reorder_level
  • revenue
  • reorder_quantity
  • safety_stock
  • orders

The listed summary items can be retrieved by calling the method orders_summary() on each object. The quickest way to do this is with a list comprehension.

analysis_summary = [demand.orders_summary() for demand in analysed_inventory_profile]

For the intrepid reader who did not heed the warning about the prerequisites and is now scratching their head wondering “what manner of black magic is this,” here is a quick overview on list comprehensions. In short, the above code is similar to the code below:

analysis_summary =[]
for demand in analysed_inventory_profile:
    analysis_summary.append(demand.orders_summary())

The former is much more readable and in truth quite addictive (hence the warning, the love for list comprehensions runs deep).

Exploring the results

To make sense of the results we can filter our analysis using standard python scripting techniques. For example to retrieve the whole summary for the SKU KR202-209 we can do something like this:

%%timeit
sku_summary = [demand.orders_summary() for demand in analysed_inventory_profile if demand.orders_summary().get('sku')== 'KR202-209']
#print(sku_summary)
1000 loops, best of 3: 885 µs per loop

The inventory classification ABC XYZ denotes the SKUs contribution to revenue and demand volatility. AX SKUs typically exhibit steady demand and contribute 80% of the revenue value for the period being analysed. Further explanation on ABC XYZ analysis can be found here.

As a more traditional way of grouping SKUs by behaviour, it is also likely to be used for generating inventory policies and for further exploration of the inventory profile. To retrive all the summaries for a particular classification, we could do something like this:

ay_classification_summary = [demand.orders_summary() for demand in analysed_inventory_profile if demand.orders_summary().get('ABC_XYZ_Classification')== 'AY']
print(ay_classification_summary)
[{'total_orders': '25185', 'standard_deviation': '721', 'quantity_on_hand': '2000', 'economic_order_variable_cost': '15826.20', 'sku': 'KR202-225', 'economic_order_quantity': '45', 'unit_cost': '994', 'demand_variability': '0.344', 'average_orders': '2098.75', 'excess_stock': '0', 'currency': 'UNKNOWN', 'ABC_XYZ_Classification': 'AY', 'shortages': '10542', 'reorder_level': '7402', 'revenue': '226639815', 'reorder_quantity': '13', 'safety_stock': '2261', 'orders': {'demand': ('2744', '2770', '2697', '1726', '1776', '2264', '332', '2420', '2722', '1161', '1986', '2587')}}, {'total_orders': '15201', 'standard_deviation': '752', 'quantity_on_hand': '8939', 'economic_order_variable_cost': '13248.03', 'sku': 'KR202-229', 'economic_order_quantity': '32', 'unit_cost': '1154', 'demand_variability': '0.594', 'average_orders': '1266.75', 'excess_stock': '3994', 'currency': 'UNKNOWN', 'ABC_XYZ_Classification': 'AY', 'shortages': '0', 'reorder_level': '3153', 'revenue': '197613000', 'reorder_quantity': '9', 'safety_stock': '1362', 'orders': {'demand': ('2114', '198', '1479', '1249', '1475', '744', '407', '2280', '226', '2285', '796', '1948')}}, {'total_orders': '21172', 'standard_deviation': '702', 'quantity_on_hand': '349', 'economic_order_variable_cost': '15903.60', 'sku': 'KR202-230', 'economic_order_quantity': '37', 'unit_cost': '1194', 'demand_variability': '0.398', 'average_orders': '1764.3333', 'excess_stock': '0', 'currency': 'UNKNOWN', 'ABC_XYZ_Classification': 'AY', 'shortages': '5913', 'reorder_level': '3767', 'revenue': '211720000', 'reorder_quantity': '11', 'safety_stock': '1271', 'orders': {'demand': ('1023', '1150', '1672', '2026', '1590', '441', '2484', '2300', '2928', '1082', '2064', '2412')}}, {'total_orders': '21329', 'standard_deviation': '749', 'quantity_on_hand': '234', 'economic_order_variable_cost': '16488.55', 'sku': 'KR202-232', 'economic_order_quantity': '36', 'unit_cost': '1274', 'demand_variability': '0.422', 'average_orders': '1777.4167', 'excess_stock': '0', 'currency': 'UNKNOWN', 'ABC_XYZ_Classification': 'AY', 'shortages': '6150', 'reorder_level': '3870', 'revenue': '159967500', 'reorder_quantity': '11', 'safety_stock': '1356', 'orders': {'demand': ('614', '2138', '962', '2017', '2398', '2963', '2189', '1804', '414', '2016', '1350', '2464')}}, {'total_orders': '13626', 'standard_deviation': '516', 'quantity_on_hand': '324', 'economic_order_variable_cost': '13586.45', 'sku': 'KR202-234', 'economic_order_quantity': '28', 'unit_cost': '1354', 'demand_variability': '0.454', 'average_orders': '1135.5', 'excess_stock': '0', 'currency': 'UNKNOWN', 'ABC_XYZ_Classification': 'AY', 'shortages': '3822', 'reorder_level': '2540', 'revenue': '272520000', 'reorder_quantity': '8', 'safety_stock': '934', 'orders': {'demand': ('1336', '1478', '865', '533', '1562', '422', '2287', '1302', '1230', '1059', '1153', '399')}}, {'total_orders': '23059', 'standard_deviation': '691', 'quantity_on_hand': '850', 'economic_order_variable_cost': '17933.46', 'sku': 'KR202-235', 'economic_order_quantity': '36', 'unit_cost': '1394', 'demand_variability': '0.360', 'average_orders': '1921.5833', 'excess_stock': '0', 'currency': 'UNKNOWN', 'ABC_XYZ_Classification': 'AY', 'shortages': '7339', 'reorder_level': '4861', 'revenue': '1372010500', 'reorder_quantity': '11', 'safety_stock': '1532', 'orders': {'demand': ('2565', '2762', '2721', '1431', '845', '2163', '2413', '2227', '1753', '740', '1139', '2300')}}, {'total_orders': '19951', 'standard_deviation': '811', 'quantity_on_hand': '433', 'economic_order_variable_cost': '17612.47', 'sku': 'KR202-239', 'economic_order_quantity': '32', 'unit_cost': '1554', 'demand_variability': '0.488', 'average_orders': '1662.5833', 'excess_stock': '0', 'currency': 'UNKNOWN', 'ABC_XYZ_Classification': 'AY', 'shortages': '5737', 'reorder_level': '3819', 'revenue': '778089000', 'reorder_quantity': '9', 'safety_stock': '1468', 'orders': {'demand': ('2717', '2186', '2300', '677', '2157', '2328', '1917', '2519', '561', '281', '1162', '1146')}}, {'total_orders': '26118', 'standard_deviation': '950', 'quantity_on_hand': '2125', 'economic_order_variable_cost': '14175.73', 'sku': 'KR202-241', 'economic_order_quantity': '52', 'unit_cost': '769', 'demand_variability': '0.437', 'average_orders': '2176.5', 'excess_stock': '0', 'currency': 'UNKNOWN', 'ABC_XYZ_Classification': 'AY', 'shortages': '10328', 'reorder_level': '7586', 'revenue': '209126826', 'reorder_quantity': '15', 'safety_stock': '2719', 'orders': {'demand': ('3050', '1507', '3637', '1112', '1963', '1675', '898', '1986', '2262', '3895', '1229', '2904')}}, {'total_orders': '23860', 'standard_deviation': '853', 'quantity_on_hand': '1253', 'economic_order_variable_cost': '20838.38', 'sku': 'KR202-242', 'economic_order_quantity': '32', 'unit_cost': '1819', 'demand_variability': '0.429', 'average_orders': '1988.3333', 'excess_stock': '0', 'currency': 'UNKNOWN', 'ABC_XYZ_Classification': 'AY', 'shortages': '0', 'reorder_level': '3080', 'revenue': '315548500', 'reorder_quantity': '9', 'safety_stock': '1092', 'orders': {'demand': ('1875', '2368', '830', '823', '868', '1409', '1845', '3095', '3247', '1894', '2558', '3048')}}, {'total_orders': '32566', 'standard_deviation': '882', 'quantity_on_hand': '1128', 'economic_order_variable_cost': '19103.09', 'sku': 'KR202-243', 'economic_order_quantity': '48', 'unit_cost': '1120', 'demand_variability': '0.325', 'average_orders': '2713.8333', 'excess_stock': '0', 'currency': 'UNKNOWN', 'ABC_XYZ_Classification': 'AY', 'shortages': '10227', 'reorder_level': '6655', 'revenue': '478134012', 'reorder_quantity': '14', 'safety_stock': '1954', 'orders': {'demand': ('1717', '593', '3006', '2935', '3139', '2753', '3247', '3845', '1720', '3413', '3399', '2799')}}, {'total_orders': '33104', 'standard_deviation': '718', 'quantity_on_hand': '1191', 'economic_order_variable_cost': '18799.00', 'sku': 'KR202-244', 'economic_order_quantity': '50', 'unit_cost': '1067', 'demand_variability': '0.260', 'average_orders': '2758.6667', 'excess_stock': '0', 'currency': 'UNKNOWN', 'ABC_XYZ_Classification': 'AY', 'shortages': '13200', 'reorder_level': '8223', 'revenue': '397148688', 'reorder_quantity': '14', 'safety_stock': '2054', 'orders': {'demand': ('2383', '2046', '2487', '3827', '1674', '3118', '2849', '2233', '3888', '2566', '2216', '3817')}}, {'total_orders': '26065', 'standard_deviation': '855', 'quantity_on_hand': '611', 'economic_order_variable_cost': '20573.14', 'sku': 'KR202-245', 'economic_order_quantity': '36', 'unit_cost': '1623', 'demand_variability': '0.394', 'average_orders': '2172.0833', 'excess_stock': '0', 'currency': 'UNKNOWN', 'ABC_XYZ_Classification': 'AY', 'shortages': '7081', 'reorder_level': '4620', 'revenue': '335612940', 'reorder_quantity': '10', 'safety_stock': '1548', 'orders': {'demand': ('1115', '2694', '3038', '3366', '1058', '2724', '2863', '1930', '1787', '838', '3087', '1565')}}, {'total_orders': '26869', 'standard_deviation': '872', 'quantity_on_hand': '2192', 'economic_order_variable_cost': '12784.68', 'sku': 'KR202-246', 'economic_order_quantity': '59', 'unit_cost': '608', 'demand_variability': '0.389', 'average_orders': '2239.0833', 'excess_stock': '0', 'currency': 'UNKNOWN', 'ABC_XYZ_Classification': 'AY', 'shortages': '0', 'reorder_level': '4745', 'revenue': '175938212', 'reorder_quantity': '17', 'safety_stock': '1578', 'orders': {'demand': ('3108', '1197', '2472', '1264', '3179', '3638', '1268', '1581', '3456', '1630', '1788', '2288')}}, {'total_orders': '23170', 'standard_deviation': '735', 'quantity_on_hand': '1017', 'economic_order_variable_cost': '19126.21', 'sku': 'KR202-247', 'economic_order_quantity': '34', 'unit_cost': '1578', 'demand_variability': '0.381', 'average_orders': '1930.8333', 'excess_stock': '0', 'currency': 'UNKNOWN', 'ABC_XYZ_Classification': 'AY', 'shortages': '5776', 'reorder_level': '4062', 'revenue': '242427710', 'reorder_quantity': '10', 'safety_stock': '1331', 'orders': {'demand': ('3439', '1854', '652', '1827', '1645', '2257', '2733', '1337', '2034', '2106', '877', '2409')}}]

Using a built-in feature of the library provides a quicker way to filter the results. For example a quciker way to filter for SKU KR202-209, is through the use of Inventory class in the summarise module.

from supplychainpy.inventory.summarise import Inventory
filtered_summary = Inventory(analysed_inventory_profile)
%%timeit
sku_summary = [summary for summary in filtered_summary.describe_sku('KR202-209')]
#print(sku_summary)
10000 loops, best of 3: 190 µs per loop

Using the import Inventory specifically built to filter the analysis is faster and syntactically cleaner for eaier to read and understand code. The Inventory summary class also provides a more detailed summary of the SKU with additional KPIs and metric in context of the whole inventory profile. The summary ranks and performs some comparative analysis for more insight.

The descriptive summary includes:

  • shortage_rank
  • min_orders
  • excess_units
  • revenue_rank
  • excess_rank
  • average_orders
  • gross_profit_margin
  • markup_percentage
  • max_order
  • shortage_cost
  • quantity_on_hand
  • inventory_turns
  • sku_id
  • retail_price
  • revenue_rank
  • shortage_units
  • unit_cost
  • classification
  • safety_stock_cost
  • safety_stock_units
  • safety_stock_rank
  • percentage_contribution_revenue
  • gross_profit_margin
  • shortage_rank
  • inventory_traffic_light
  • unit_cost_rank
  • excess_cost
  • excess_units
  • markup_percentage
  • revenue

This is a pretty comprehensive list of descriptors to use for further analysis.

Further summaries can be retrieved, for instance summaries at the inventory classification level of detail can be quite useful when exploring inventory policies:

classification_summary =  [summary for summary in filtered_summary.abc_xyz_summary(classification=('AY',), category=('revenue',))]
print(classification_summary)
[{'AY': {'revenue': 5372496600.0}}]

Now we know the total revenue generated by the AY SKU class. There is another, slightly more fun way to arrive at this number using Dash but more on that latter.

top_10_safety_stock_skus =  [summary.get('sku')for summary in filtered_summary.rank_summary(attribute='safety_stock', count=10)]
print(top_10_safety_stock_skus)
['KR202-241', 'KR202-231', 'KR202-233', 'KR202-227', 'KR202-225', 'KR202-212', 'KR202-240', 'KR202-244', 'KR202-236', 'KR202-211', 'KR202-243']

Lets add the safety_stock and create a tuple to see that the results explicitly.

top_10_safety_stock_values =  [(summary.get('sku'), summary.get('safety_stock'))for summary in filtered_summary.rank_summary(attribute='safety_stock', count=10)]
print(top_10_safety_stock_values)
[('KR202-241', '2719'), ('KR202-231', '2484'), ('KR202-233', '2472'), ('KR202-227', '2277'), ('KR202-225', '2261'), ('KR202-212', '2164'), ('KR202-240', '2120'), ('KR202-244', '2054'), ('KR202-236', '2045'), ('KR202-211', '2020'), ('KR202-243', '1954')]

We can then pass back the list of top_10_safety_stock_skus back into the inventory filter and get their breakdown.

top_10_safety_stock_summary = [summary for summary in filtered_summary.describe_sku(*top_10_safety_stock_skus)]
#print(top_10_safety_stock_summary)

We have only covered a few use cases but we have already achieved a significant amount of analysis with relativley few line of code. The equivalent in Excel would require much more work and many more formulas.

Using supplychainpy with Pandas, Jupyter and Matplotlib

The following is taken from the jupyter notebook title ‘0.0.4-Using-Supplychainpy-and-Pandas-v1’ found here . For a more interactive experience please retrieve this notebook and run with jupyter.

To use supplychainpy with Pandas, we first read a csv file to a Pandas DataFrame.

%matplotlib inline

import matplotlib
import pandas as pd

from supplychainpy.model_inventory import analyse
from supplychainpy.model_demand import simple_exponential_smoothing_forecast
from supplychainpy.sample_data.config import ABS_FILE_PATH

raw_df =pd.read_csv(ABS_FILE_PATH['COMPLETE_CSV_SM'])
print(raw_df)
          Sku   jan   feb   mar   apr   may   jun   jul   aug   sep   oct  0   KR202-209  1509  1855  2665  1841  1231  2598  1988  1988  2927  2707
1   KR202-210  1006   206  2588   670  2768  2809  1475  1537   919  2525
2   KR202-211  1840  2284   850   983  2737  1264  2002  1980   235  1489
3   KR202-212   104  2262   350   528  2570  1216  1101  2755  2856  2381
4   KR202-213   489   954  1112   199   919   330   561  2372   921  1587
5   KR202-214  2416  2010  2527  1409  1059   890  2837   276   987  2228
6   KR202-215   403  1737   753  1982  2775   380  1561  1230  1262  2249
7   KR202-216  2908   929   684  2618  1477  1508   765    43  2550  2157
8   KR202-217  2799  2197  1647  2263   224  2987  2366   588  1140   869
9   KR202-218  1333   402   804   318  1408   830  1028   534  1871  2730
10  KR202-219   813   969   745  1001  2732  1987   717   599  2722   171
11  KR202-220  1481   905  1067  2513   861  1670   650  2630  1245   997
12  KR202-221   771  2941  1360  2714  1801  1744  1428  1660   436   578
13  KR202-222  2349     4   345   524   340  2698  2137  1164   498  1583
14  KR202-223  2045  2055   552    81  2780   176  2316  1475  2566  1678
15  KR202-224  2482  1887  1911  1446  2939  1241  1281   692   119   627
16  KR202-225  2744  2770  2697  1726  1776  2264   332  2420  2722  1161
17  KR202-226  2509   914   903   877  1859  2263   383   593   236   189
18  KR202-227   368  2502  2955  2994  1270  2884  2208   699   854   877
19  KR202-228  1468  1109  2464  2799   948   589  2858  1140   501  2691
20  KR202-229  2114   198  1479  1249  1475   744   407  2280   226  2285
21  KR202-230  1023  1150  1672  2026  1590   441  2484  2300  2928  1082
22  KR202-231   482   546   299  2304  2953  1029  1863  2809   454   927
23  KR202-232   614  2138   962  2017  2398  2963  2189  1804   414  2016
24  KR202-233  2395  2521  2157   728  1028    43   138   826   570  2825
25  KR202-234  1336  1478   865   533  1562   422  2287  1302  1230  1059
26  KR202-235  2565  2762  2721  1431   845  2163  2413  2227  1753   740
27  KR202-236  1912  1726  1569   316    71  2082   108   174  1974   609
28  KR202-237  2153  1112    16   130   590  2619  2576  2390  2567  1531
29  KR202-238  1417  2044  1981  1936  2377   780  1544  1521    51  1056
30  KR202-239  2717  2186  2300   677  2157  2328  1917  2519   561   281
31  KR202-240  1015   741  2754  2925  2302   695  2869   440   406  1083
32  KR202-241  3050  1507  3637  1112  1963  1675   898  1986  2262  3895
33  KR202-242  1875  2368   830   823   868  1409  1845  3095  3247  1894
34  KR202-243  1717   593  3006  2935  3139  2753  3247  3845  1720  3413
35  KR202-244  2383  2046  2487  3827  1674  3118  2849  2233  3888  2566
36  KR202-245  1115  2694  3038  3366  1058  2724  2863  1930  1787   838
37  KR202-246  3108  1197  2472  1264  3179  3638  1268  1581  3456  1630
38  KR202-247  3439  1854   652  1827  1645  2257  2733  1337  2034  2106

     nov   dec  unit cost  lead-time  retail_price  quantity_on_hand  backlog
0    731  2598       1001          2          5000              1003       10
1    440  2691        394          2          1300              3224       10
2    218   525        434          4          1200               390       10
3   1867  2743        474          3            10               390       10
4   1532  1512        514          1          2000              2095       10
5   1095  1396        554          2          1800                55       10
6    824   743        594          1          2500              4308       10
7    937  1201        634          3          3033                34       10
8   1707  1180        674          3          5433               390       10
9   2022    94        714          2          3034              3535       10
10   639  2108        754          3          5000               334       10
11  1936  2780        794          3          7500              3434       10
12  1956  1101        834          2          4938              4433       10
13  1241  2965        874          2          4922              3435       10
14  1553  2745        914          1          4894             34533       10
15  1941  1383        954          2          2942                33       10
16  1986  2587        994          6          8999              2000       10
17   920  1686       1034          3          4342              4344       10
18  2320   160       1074          3          4920               489       10
19    93  1060       1114          2         15000              9439       10
20   796  1948       1154          2         13000              8939       10
21  2064  2412       1194          2         10000               349       10
22  2488  2341       1234          4          9999              3434       10
23  1350  2464       1274          2          7500               234       10
24   181   787       1314          4          6000               349       10
25  1153   399       1354          2         20000               324       10
26  1139  2300       1394          3         59500               850       10
27  2896   566       1434          3          2300              4930       10
28   842   242       1474          2          4500              9483       10
29  1876  1356       1514          3          8000               839       10
30  1162  1146       1554          2         39000               433       10
31  2334  1015       1594          3          3943               390       10
32  1229  2904        769          5          8007              2125       10
33  2558  3048       1819          1         13225              1253       10
34  3399  2799       1120          3         14682              1128       10
35  2216  3817       1067          5         11997              1191       10
36  3087  1565       1623          2         12876               611       10
37  1788  2288        608          2          6548              2192       10
38   877  2409       1578          2         10463              1017       10

Passing a Pandas DataFrame as a keyword parameter (df=) returns a DataFrame with the inventory profile analysed. Excluding the import statements this can be achieved in 3 lines of code. There are several columns, so the print statement has been limited to a few.

orders_df = raw_df[['Sku','jan','feb','mar','apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']]
#orders_df.set_index('Sku')
analysis_df = analyse(df=raw_df, start=1, interval_length=12, interval_type='months')
print(analysis_df[['sku','quantity_on_hand', 'excess_stock', 'shortages', 'ABC_XYZ_Classification']])
          sku quantity_on_hand excess_stock shortages ABC_XYZ_Classification
0   KR202-209             1003            0      5969                     BY
1   KR202-210             3224            0         0                     CY
2   KR202-211              390            0      7099                     CY
3   KR202-212              390            0      7759                     CY
4   KR202-213             2095            0         0                     CY
5   KR202-214               55            0      5824                     CY
6   KR202-215             4308          732         0                     CY
7   KR202-216               34            0      6999                     CY
8   KR202-217              390            0      7245                     BY
9   KR202-218             3535            0         0                     CZ
10  KR202-219              334            0      5917                     CZ
11  KR202-220             3434            0         0                     BY
12  KR202-221             4433            0         0                     BY
13  KR202-222             3435            0         0                     CZ
14  KR202-223            34533        30030         0                     BY
15  KR202-224               33            0      5580                     CY
16  KR202-225             2000            0     10542                     AY
17  KR202-226             4344            0         0                     CZ
18  KR202-227              489            0      7587                     BZ
19  KR202-228             9439         3572         0                     AZ
20  KR202-229             8939         3994         0                     AY
21  KR202-230              349            0      5913                     AY
22  KR202-231             3434            0         0                     AZ
23  KR202-232              234            0      6150                     AY
24  KR202-233              349            0      6856                     CZ
25  KR202-234              324            0      3822                     AY
26  KR202-235              850            0      7339                     AY
27  KR202-236             4930            0         0                     CZ
28  KR202-237             9483         3742         0                     CZ
29  KR202-238              839            0      5693                     BY
30  KR202-239              433            0      5737                     AY
31  KR202-240              390            0      7094                     CZ
32  KR202-241             2125            0     10328                     AY
33  KR202-242             1253            0         0                     AY
34  KR202-243             1128            0     10227                     AY
35  KR202-244             1191            0     13200                     AY
36  KR202-245              611            0      7081                     AY
37  KR202-246             2192            0         0                     AY
38  KR202-247             1017            0      5776                     AY

Before we can make a forecast we need to select a SKU from the analysis_df variable, slice the row to retrive only orders data and convert to a Series.

row_ds = raw_df[raw_df['Sku']=='KR202-212'].squeeze()
print(row_ds[1:12])
jan     104
feb    2262
mar     350
apr     528
may    2570
jun    1216
jul    1101
aug    2755
sep    2856
oct    2381
nov    1867
Name: 3, dtype: object

Now that we have a series of orders data fro the SKU KR202-212, we can now perform a forecast using the model_demand module. We can perform a simple_exponential_smoothing_forecast by passing the forecasting function the orders data using the keyword parameter ds=.

ses_df = simple_exponential_smoothing_forecast(ds=row_ds[1:12], length=12, smoothing_level_constant=0.5)
print(ses_df)
{'statistics': {'pvalue': 0.0047852515832242743, 'test_statistic': 3.8634855288615153, 'std_residuals': 4793.7283216530095, 'intercept': 377.59999999999991, 'trend': True, 'slope': 224.4909090909091, 'slope_standard_error': 58.105797838218294}, 'alpha': 0.5, 'forecast_breakdown': [{'squared_error': 2345353.024793389, 'alpha': 0.5, 'demand': 104, 'one_step_forecast': 1635.4545454545455, 't': 1, 'level_estimates': 869.72727272727275, 'forecast_error': -1531.4545454545455}, {'squared_error': 1938423.3471074379, 'alpha': 0.5, 'demand': 2262, 'one_step_forecast': 869.72727272727275, 't': 2, 'level_estimates': 1565.8636363636365, 'forecast_error': 1392.2727272727273}, {'squared_error': 1478324.3822314052, 'alpha': 0.5, 'demand': 350, 'one_step_forecast': 1565.8636363636365, 't': 3, 'level_estimates': 957.93181818181824, 'forecast_error': -1215.8636363636365}, {'squared_error': 184841.36828512402, 'alpha': 0.5, 'demand': 528, 'one_step_forecast': 957.93181818181824, 't': 4, 'level_estimates': 742.96590909090912, 'forecast_error': -429.93181818181824}, {'squared_error': 3338053.5693440083, 'alpha': 0.5, 'demand': 2570, 'one_step_forecast': 742.96590909090912, 't': 5, 'level_estimates': 1656.4829545454545, 'forecast_error': 1827.034090909091}, {'squared_error': 194025.23324509294, 'alpha': 0.5, 'demand': 1216, 'one_step_forecast': 1656.4829545454545, 't': 6, 'level_estimates': 1436.2414772727273, 'forecast_error': -440.4829545454545}, {'squared_error': 112386.84808400051, 'alpha': 0.5, 'demand': 1101, 'one_step_forecast': 1436.2414772727273, 't': 7, 'level_estimates': 1268.6207386363635, 'forecast_error': -335.24147727272725}, {'squared_error': 2209323.3086119094, 'alpha': 0.5, 'demand': 2755, 'one_step_forecast': 1268.6207386363635, 't': 8, 'level_estimates': 2011.8103693181818, 'forecast_error': 1486.3792613636365}, {'squared_error': 712656.13255070464, 'alpha': 0.5, 'demand': 2856, 'one_step_forecast': 2011.8103693181818, 't': 9, 'level_estimates': 2433.905184659091, 'forecast_error': 844.18963068181824}, {'squared_error': 2798.9585638125168, 'alpha': 0.5, 'demand': 2381, 'one_step_forecast': 2433.905184659091, 't': 10, 'level_estimates': 2407.4525923295455, 'forecast_error': -52.905184659090992}, {'squared_error': 292089.0045557259, 'alpha': 0.5, 'demand': 1867, 'one_step_forecast': 2407.4525923295455, 't': 11, 'level_estimates': 2137.226296164773, 'forecast_error': -540.4525923295455}], 'mape': 100.69830747447692, 'forecast': [2137.226296164773, 2137.226296164773, 2137.226296164773, 2137.226296164773, 2137.226296164773]}
print(ses_df.get('forecast', 'UNKNOWN'))
[2137.226296164773, 2137.226296164773, 2137.226296164773, 2137.226296164773, 2137.226296164773]

If we check the statistcs for the forecast we can see whether there is a linear trend and subsequently if the forecast is useful.

print(ses_df.get('statistics', 'UNKNOWN'),'\n mape: {}'.format(ses_df.get('mape', 'UNKNOWN')))
{'pvalue': 0.0047852515832242743, 'test_statistic': 3.8634855288615153, 'std_residuals': 4793.7283216530095, 'intercept': 377.59999999999991, 'trend': True, 'slope': 224.4909090909091, 'slope_standard_error': 58.105797838218294}
 mape: 100.69830747447692

The breakdown of the forecast is also returned with the forecast and statistics.

print(ses_df.get('forecast_breakdown', 'UNKNOWN'))
[{'squared_error': 2345353.024793389, 'alpha': 0.5, 'demand': 104, 'one_step_forecast': 1635.4545454545455, 't': 1, 'level_estimates': 869.72727272727275, 'forecast_error': -1531.4545454545455}, {'squared_error': 1938423.3471074379, 'alpha': 0.5, 'demand': 2262, 'one_step_forecast': 869.72727272727275, 't': 2, 'level_estimates': 1565.8636363636365, 'forecast_error': 1392.2727272727273}, {'squared_error': 1478324.3822314052, 'alpha': 0.5, 'demand': 350, 'one_step_forecast': 1565.8636363636365, 't': 3, 'level_estimates': 957.93181818181824, 'forecast_error': -1215.8636363636365}, {'squared_error': 184841.36828512402, 'alpha': 0.5, 'demand': 528, 'one_step_forecast': 957.93181818181824, 't': 4, 'level_estimates': 742.96590909090912, 'forecast_error': -429.93181818181824}, {'squared_error': 3338053.5693440083, 'alpha': 0.5, 'demand': 2570, 'one_step_forecast': 742.96590909090912, 't': 5, 'level_estimates': 1656.4829545454545, 'forecast_error': 1827.034090909091}, {'squared_error': 194025.23324509294, 'alpha': 0.5, 'demand': 1216, 'one_step_forecast': 1656.4829545454545, 't': 6, 'level_estimates': 1436.2414772727273, 'forecast_error': -440.4829545454545}, {'squared_error': 112386.84808400051, 'alpha': 0.5, 'demand': 1101, 'one_step_forecast': 1436.2414772727273, 't': 7, 'level_estimates': 1268.6207386363635, 'forecast_error': -335.24147727272725}, {'squared_error': 2209323.3086119094, 'alpha': 0.5, 'demand': 2755, 'one_step_forecast': 1268.6207386363635, 't': 8, 'level_estimates': 2011.8103693181818, 'forecast_error': 1486.3792613636365}, {'squared_error': 712656.13255070464, 'alpha': 0.5, 'demand': 2856, 'one_step_forecast': 2011.8103693181818, 't': 9, 'level_estimates': 2433.905184659091, 'forecast_error': 844.18963068181824}, {'squared_error': 2798.9585638125168, 'alpha': 0.5, 'demand': 2381, 'one_step_forecast': 2433.905184659091, 't': 10, 'level_estimates': 2407.4525923295455, 'forecast_error': -52.905184659090992}, {'squared_error': 292089.0045557259, 'alpha': 0.5, 'demand': 1867, 'one_step_forecast': 2407.4525923295455, 't': 11, 'level_estimates': 2137.226296164773, 'forecast_error': -540.4525923295455}]

We can convert the forecast_breakdown back into a DataFrame.

forecast_breakdown_df = pd.DataFrame(ses_df.get('forecast_breakdown', 'UNKNOWN'))
print(forecast_breakdown_df)
    alpha  demand  forecast_error  level_estimates  one_step_forecast  0     0.5     104    -1531.454545       869.727273        1635.454545
1     0.5    2262     1392.272727      1565.863636         869.727273
2     0.5     350    -1215.863636       957.931818        1565.863636
3     0.5     528     -429.931818       742.965909         957.931818
4     0.5    2570     1827.034091      1656.482955         742.965909
5     0.5    1216     -440.482955      1436.241477        1656.482955
6     0.5    1101     -335.241477      1268.620739        1436.241477
7     0.5    2755     1486.379261      2011.810369        1268.620739
8     0.5    2856      844.189631      2433.905185        2011.810369
9     0.5    2381      -52.905185      2407.452592        2433.905185
10    0.5    1867     -540.452592      2137.226296        2407.452592

    squared_error   t
0    2.345353e+06   1
1    1.938423e+06   2
2    1.478324e+06   3
3    1.848414e+05   4
4    3.338054e+06   5
5    1.940252e+05   6
6    1.123868e+05   7
7    2.209323e+06   8
8    7.126561e+05   9
9    2.798959e+03  10
10   2.920890e+05  11

Let’s look at the demand and the one_step_forecast in a chart.

forecast_breakdown_df.plot(x='t', y=['one_step_forecast','demand'])
<matplotlib.axes._subplots.AxesSubplot at 0x10e1be400>
_images/image1.png

Using y = mx + c we can also create the data points for the regression line.

regression = {'regression': [(ses_df.get('statistics')['slope']* i ) + ses_df.get('statistics')['intercept'] for i in range(1,12)]}
print(regression)
{'regression': [602.09090909090901, 826.58181818181811, 1051.0727272727272, 1275.5636363636363, 1500.0545454545454, 1724.5454545454545, 1949.0363636363636, 2173.5272727272727, 2398.0181818181818, 2622.5090909090909, 2847.0]}

We can add the regression data points to the forecast breakdwn DataFrame.

forecast_breakdown_df['regression'] = regression.get('regression')
print(forecast_breakdown_df)
    alpha  demand  forecast_error  level_estimates  one_step_forecast  0     0.5     104    -1531.454545       869.727273        1635.454545
1     0.5    2262     1392.272727      1565.863636         869.727273
2     0.5     350    -1215.863636       957.931818        1565.863636
3     0.5     528     -429.931818       742.965909         957.931818
4     0.5    2570     1827.034091      1656.482955         742.965909
5     0.5    1216     -440.482955      1436.241477        1656.482955
6     0.5    1101     -335.241477      1268.620739        1436.241477
7     0.5    2755     1486.379261      2011.810369        1268.620739
8     0.5    2856      844.189631      2433.905185        2011.810369
9     0.5    2381      -52.905185      2407.452592        2433.905185
10    0.5    1867     -540.452592      2137.226296        2407.452592

    squared_error   t   regression
0    2.345353e+06   1   602.090909
1    1.938423e+06   2   826.581818
2    1.478324e+06   3  1051.072727
3    1.848414e+05   4  1275.563636
4    3.338054e+06   5  1500.054545
5    1.940252e+05   6  1724.545455
6    1.123868e+05   7  1949.036364
7    2.209323e+06   8  2173.527273
8    7.126561e+05   9  2398.018182
9    2.798959e+03  10  2622.509091
10   2.920890e+05  11  2847.000000
forecast_breakdown_df.plot(x='t', y=['one_step_forecast','demand', 'regression'])
<matplotlib.axes._subplots.AxesSubplot at 0x110a83b38>
_images/image2.png

We have a choice now, we can use another alpha and repeat the analysis to reduce the Standard Error or use supplychainpy’s optimise=True parameter to use an evolutionary algorithm and get closer to an optimal solution.

opt_ses_df = simple_exponential_smoothing_forecast(ds=row_ds[1:12], length=12, smoothing_level_constant=0.4,optimise=True)
print(opt_ses_df)
{'statistics': {'pvalue': 0.0047852515832242743, 'test_statistic': 3.8634855288615153, 'std_residuals': 4793.7283216530095, 'intercept': 377.59999999999991, 'trend': True, 'slope': 224.4909090909091, 'slope_standard_error': 58.105797838218294}, 'optimal_alpha': 0.006889829296806371, 'mape': 209.37388042679993, 'standard_error': 1097.3575476759161, 'forecast_breakdown': [{'squared_error': 2345353.024793389, 'alpha': 0.006889829296806371, 'demand': 104, 'one_step_forecast': 1635.4545454545455, 't': 1, 'level_estimates': 1624.9030850605454, 'forecast_error': -1531.4545454545455}, {'squared_error': 405892.47902537062, 'alpha': 0.006889829296806371, 'demand': 2262, 'one_step_forecast': 1624.9030850605454, 't': 2, 'level_estimates': 1629.2925740500002, 'forecast_error': 637.09691493945456}, {'squared_error': 1636589.4900194753, 'alpha': 0.006889829296806371, 'demand': 350, 'one_step_forecast': 1629.2925740500002, 't': 3, 'level_estimates': 1620.4784665941236, 'forecast_error': -1279.2925740500002}, {'squared_error': 1193509.1999718475, 'alpha': 0.006889829296806371, 'demand': 528, 'one_step_forecast': 1620.4784665941236, 't': 4, 'level_estimates': 1612.9514764488533, 'forecast_error': -1092.4784665941236}, {'squared_error': 915941.87643142976, 'alpha': 0.006889829296806371, 'demand': 2570, 'one_step_forecast': 1612.9514764488533, 't': 5, 'level_estimates': 1619.5453774048813, 'forecast_error': 957.04852355114667}, {'squared_error': 162848.87162484805, 'alpha': 0.006889829296806371, 'demand': 1216, 'one_step_forecast': 1619.5453774048813, 't': 6, 'level_estimates': 1616.7650186410463, 'forecast_error': -403.54537740488126}, {'squared_error': 266013.5544537988, 'alpha': 0.006889829296806371, 'demand': 1101, 'one_step_forecast': 1616.7650186410463, 't': 7, 'level_estimates': 1613.2114857053452, 'forecast_error': -515.76501864104625}, {'squared_error': 1303681.0113751951, 'alpha': 0.006889829296806371, 'demand': 2755, 'one_step_forecast': 1613.2114857053452, 't': 8, 'level_estimates': 1621.0782136618895, 'forecast_error': 1141.7885142946548}, {'squared_error': 1525031.8183725097, 'alpha': 0.006889829296806371, 'demand': 2856, 'one_step_forecast': 1621.0782136618895, 't': 9, 'level_estimates': 1629.5866139646664, 'forecast_error': 1234.9217863381105}, {'squared_error': 564622.07671308529, 'alpha': 0.006889829296806371, 'demand': 2381, 'one_step_forecast': 1629.5866139646664, 't': 10, 'level_estimates': 1634.7637239257851, 'forecast_error': 751.41338603533359}, {'squared_error': 53933.687924818943, 'alpha': 0.006889829296806371, 'demand': 1867, 'one_step_forecast': 1634.7637239257851, 't': 11, 'level_estimates': 1636.3637922244625, 'forecast_error': 232.23627607421486}], 'forecast': [1636.3637922244625, 1636.3637922244625, 1636.3637922244625, 1636.3637922244625, 1636.3637922244625]}
print(opt_ses_df.get('statistics', 'UNKNOWN'),'\n mape: {}'.format(opt_ses_df.get('mape', 'UNKNOWN')))
{'pvalue': 0.0047852515832242743, 'test_statistic': 3.8634855288615153, 'std_residuals': 4793.7283216530095, 'intercept': 377.59999999999991, 'trend': True, 'slope': 224.4909090909091, 'slope_standard_error': 58.105797838218294}
 mape: 209.37388042679993
print(opt_ses_df.get('forecast', 'UNKNOWN'))
[1636.3637922244625, 1636.3637922244625, 1636.3637922244625, 1636.3637922244625, 1636.3637922244625]
optimised_regression = {'regression': [(opt_ses_df.get('statistics')['slope']* i ) + opt_ses_df.get('statistics')['intercept'] for i in range(1,12)]}
print(optimised_regression)
{'regression': [602.09090909090901, 826.58181818181811, 1051.0727272727272, 1275.5636363636363, 1500.0545454545454, 1724.5454545454545, 1949.0363636363636, 2173.5272727272727, 2398.0181818181818, 2622.5090909090909, 2847.0]}
opt_forecast_breakdown_df = pd.DataFrame(opt_ses_df.get('forecast_breakdown', 'UNKNOWN'))

We can compare the MAPE of our previous forecast with the optimised simple exponential smoothing forecast to see which is a better forecast.

opt_forecast_breakdown_df['regression'] = optimised_regression.get('regression')
print(opt_forecast_breakdown_df)
      alpha  demand  forecast_error  level_estimates  one_step_forecast  0   0.00689     104    -1531.454545      1624.903085        1635.454545
1   0.00689    2262      637.096915      1629.292574        1624.903085
2   0.00689     350    -1279.292574      1620.478467        1629.292574
3   0.00689     528    -1092.478467      1612.951476        1620.478467
4   0.00689    2570      957.048524      1619.545377        1612.951476
5   0.00689    1216     -403.545377      1616.765019        1619.545377
6   0.00689    1101     -515.765019      1613.211486        1616.765019
7   0.00689    2755     1141.788514      1621.078214        1613.211486
8   0.00689    2856     1234.921786      1629.586614        1621.078214
9   0.00689    2381      751.413386      1634.763724        1629.586614
10  0.00689    1867      232.236276      1636.363792        1634.763724

    squared_error   t   regression
0    2.345353e+06   1   602.090909
1    4.058925e+05   2   826.581818
2    1.636589e+06   3  1051.072727
3    1.193509e+06   4  1275.563636
4    9.159419e+05   5  1500.054545
5    1.628489e+05   6  1724.545455
6    2.660136e+05   7  1949.036364
7    1.303681e+06   8  2173.527273
8    1.525032e+06   9  2398.018182
9    5.646221e+05  10  2622.509091
10   5.393369e+04  11  2847.000000
opt_forecast_breakdown_df.plot(x='t', y=['one_step_forecast','demand', 'regression'])
<matplotlib.axes._subplots.AxesSubplot at 0x110a98f98>
_images/image3.png

Analytic Hierarchy Process

As of release 0.0.4, Supplychainpy will have the facility for computing the AHP of a given set of criteria and alternative options. For an overview of the process, please visit this blog post

Below is a code snippet for the AHP:

>>> from supplychainpy.model_decision import analytical_hierarchy_process
>>> lorry_cost = {'scania': 55000, 'iveco': 79000, 'volvo': 59000, 'navistar': 66000}
>>> criteria = ('style', 'reliability', 'comfort', 'fuel_economy')
>>> criteria_scores = [ (1 / 1, 2 / 1, 7 / 1, 9 / 1), (1 / 2, 1 / 1, 5 / 1, 5 / 1), (1 / 7, 1 / 5, 1 / 1, 5 / 1),(1 / 9, 1 / 5, 1 / 5, 1 / 1)]
>>> options = ('scania', 'iveco', 'navistar', 'volvo' )
>>>     option_scores = {
>>> 'style': [(1 / 1, 1 / 3, 5 / 1, 1 / 5), (3 / 1, 1 / 1, 2 / 1, 3 / 1), (1 / 3, 1 / 5, 1 / 1, 1 / 5), (5 / 1, 1 / 3, 5 / 1, 1 / 1)],
>>> 'reliability': [(1 / 1, 1 / 3, 3 / 1, 1 / 7), (3 / 1, 1 / 1, 5 / 1, 1 / 5), (1 / 3, 1 / 5, 1 / 1, 1 / 5), (7 / 1, 5 / 1, 5 / 1, 1 / 1)],
>>> 'comfort': [(1 / 1, 5 / 1, 5 / 1, 1 / 7), (1 / 5, 1 / 1, 2 / 1, 1 / 7), (1 / 3, 1 / 5, 1 / 1, 1 / 5), (7 / 1, 7 / 1, 5 / 1, 1 / 1)],
>>> 'fuel_economy': (11, 9, 10, 12)}
>>> lorry_decision = analytical_hierarchy_process(criteria=criteria,
...                                          criteria_scores=criteria_scores,
...                                          options=options,
...                                          option_scores=option_scores,
...                                          quantitative_criteria=('fuel_economy',),
...                                          item_cost=lorry_cost)

The results of the AHP:

{'analytical_hierarchy': {'iveco': 0.20541585500041709, 'scania': 0.21539971200341132, 'volvo': 0.5677817531137912, 'navistar': 0.011402679882380324}, 'cost_benefit_ratios': {'iveco': 0.67345198031782316, 'scania': 1.0143368256160643, 'volvo': 2.4924656619741006, 'navistar': 0.044746880144492483}

Monte Carlo Simulation

After analysing the orders, the results for safety stock may not adequately calculate the service level required. The complexity of the supply chain operation may include randomness an analytical model does not capture. A simulation is useful for giving a dynamic view of a complex process. The simulation replicates some of the complexity of the system over time.

The code below returns a transaction report covering the number of periods specified, multiplied by the number of runs requested. The higher the number of runs the more accurately the simulation captures the dynamics of the system, when summarised later. The simulation is limited by the assumptions inherent in the simulations design (detailed in the calculations).

To start we need to analyse the orders again like we did in the inventory analysis above:

>>> from supplychainpy.model_inventory import analyse_orders_abcxyz_from_file
>>> orders_analysis = analyse_orders_abcxyz_from_file(file_path="data.csv", z_value=Decimal(1.28),
>>>                                        reorder_cost=Decimal(5000), file_type="csv")

The orders are then passed as a parameter to the monte carlo simulation:

>>> from supplychainpy.model_inventory import analyse_orders_abcxyz_from_file
>>> from supplychainpy import simulate
>>> orders_analysis = analyse_orders_abcxyz_from_file(file_path="data.csv", z_value=Decimal(1.28),
>>>                                        reorder_cost=Decimal(5000), file_type="csv")
>>>
>>> sim = simulate.run_monte_carlo(orders_analysis=orders_analysis.orders, file_path="data.csv", z_value=Decimal(1.28), runs=100,
>>>                               reorder_cost=Decimal(4000), file_type="csv", period_length=12)
>>> for transaction in sim:
>>>     print(transaction)

The Monte Carlo simulation generates normally distributed random demand, based on the historical data. The demand for each SKU is then used in each period to model a probable transaction history. The output below are the sales for one SKU over the year for 100 runs (1 run shown).

[{'delivery': '0', 'quantity_sold': '1354', 'po_received': '', 'po_quantity': '3630', 'opening_stock': '1446',
'shortage_units': '0', 'closing_stock': '1355', 'revenue': '541946', 'demand': '92', 'index': '1', 'po_raised':
'PO 31', 'period': '1', 'backlog': '0', 'sku_id': 'KR202-209', 'shortage_cost': '0'}]
[{'delivery': '0', 'quantity_sold': '1354', 'po_received': '', 'po_quantity': '6268', 'opening_stock': '1355',
'shortage_units': '1283', 'closing_stock': '0', 'revenue': '541946', 'demand': '2638', 'index': '1', 'po_raised':
'PO 41', 'period': '2', 'backlog': '1283', 'sku_id': 'KR202-209', 'shortage_cost': '154032'}]
[{'delivery': '3630', 'quantity_sold': '1520', 'po_received': 'PO 31', 'po_quantity': '3464', 'opening_stock': '0',
'shortage_units': '0', 'closing_stock': '2805', 'revenue': '608381', 'demand': '826', 'index': '1', 'po_raised':
'PO 51', 'period': '3', 'backlog': '1283', 'sku_id': 'KR202-209', 'shortage_cost': '0'}]
[{'delivery': '6269', 'quantity_sold': '7753', 'po_received': 'PO 41', 'po_quantity': '0', 'opening_stock': '2805',
'shortage_units': '0', 'closing_stock': '7754', 'revenue': '3101401', 'demand': '1320', 'index': '1',
'po_raised': '', 'period': '4', 'backlog': '0', 'sku_id': 'KR202-209', 'shortage_cost': '0'}]
[{'delivery': '3464', 'quantity_sold': '10203', 'po_received': 'PO 51', 'po_quantity': '0', 'opening_stock': '7754',
'shortage_units': '0', 'closing_stock': '10204', 'revenue': '4081460', 'demand': '1014', 'index': '1',
'po_raised': '', 'period': '5', 'backlog': '0', 'sku_id': 'KR202-209', 'shortage_cost': '0'}]
[{'delivery': '0', 'quantity_sold': '8926', 'po_received': '', 'po_quantity': '0', 'opening_stock': '10204',
'shortage_units': '0', 'closing_stock': '8927', 'revenue': '3570654', 'demand': '1277', 'index': '1',
'po_raised': '','period': '6', 'backlog': '0', 'sku_id': 'KR202-209', 'shortage_cost': '0'}]
[{'delivery': '0', 'quantity_sold': '7284', 'po_received': '', 'po_quantity': '0', 'opening_stock': '8927',
'shortage_units': '0', 'closing_stock': '7285', 'revenue': '2913927', 'demand': '1642', 'index': '1',
'po_raised': '','period': '7', 'backlog': '0', 'sku_id': 'KR202-209', 'shortage_cost': '0'}]
[{'delivery': '0', 'quantity_sold': '6387', 'po_received': '', 'po_quantity': '0', 'opening_stock': '7285',
'shortage_units': '0', 'closing_stock': '6387', 'revenue': '2554819', 'demand': '898', 'index': '1',
'po_raised': '','period': '8', 'backlog': '0', 'sku_id': 'KR202-209', 'shortage_cost': '0'}]
[{'delivery': '0', 'quantity_sold': '4708', 'po_received': '', 'po_quantity': '276', 'opening_stock': '6387',
'shortage_units': '0', 'closing_stock': '4709', 'revenue': '1883461', 'demand': '1678', 'index': '1', 'po_raised':
'PO 111', 'period': '9', 'backlog': '0', 'sku_id': 'KR202-209', 'shortage_cost': '0'}]
[{'delivery': '0', 'quantity_sold': '2954', 'po_received': '', 'po_quantity': '2030', 'opening_stock': '4709',
'shortage_units': '0', 'closing_stock': '2955', 'revenue': '1181806', 'demand': '1754', 'index': '1', 'po_raised':
'PO 121', 'period': '10', 'backlog': '0', 'sku_id': 'KR202-209', 'shortage_cost': '0'}]
[{'delivery': '276', 'quantity_sold': '674', 'po_received': 'PO 111', 'po_quantity': '4310',
'opening_stock': '2955', 'shortage_units': '0', 'closing_stock': '674', 'revenue': '269654', 'demand': '2557',
'index': '1', 'po_raised': 'PO 131', 'period': '11', 'backlog': '0', 'sku_id': 'KR202-209', 'shortage_cost': '0'}]
[{'delivery': '2031', 'quantity_sold': '947', 'po_received': 'PO 121', 'po_quantity': '4037',
'opening_stock': '674', 'shortage_units': '0', 'closing_stock': '947', 'revenue': '378903', 'demand': '1757',
'index': '1', 'po_raised': 'PO 141', 'period': '12', 'backlog': '0', 'sku_id': 'KR202-209', 'shortage_cost': '0'}]

After running the Monte Carlo simulation, the results can be passed as a parameter for summary:

>>> from supplychainpy.model_inventory import analyse_orders_abcxyz_from_file
>>> from supplychainpy import simulate
>>> orders_analysis = analyse_orders_abcxyz_from_file(file_path="data.csv", z_value=Decimal(1.28),
>>>                                        reorder_cost=Decimal(5000), file_type="csv")
>>>
>>> sim = simulate.run_monte_carlo(orders_analysis=orders_analysis.orders, runs=100, period_length=12)
>>>
>>> sim_window = simulate.summarize_window(simulation_frame=sim, period_length=12)
>>> for r in i:
>>>         print(r)

The result is a transactions summary for each SKU, over every run (100) requested. It is important to note that each run will have a different randomly generated demand. Due to the randomised demand, the transaction summary for the same SKU will differ over consecutive runs. The spread of data captures the statistically probable distribution of demand the SKU can expect. However the more runs (thousands, tens of thousands), the more useful the result.

{'standard_deviation_backlog': 250.43961347997646, 'variance_quantity_sold': 4045303.0763888955,
'total_shortage_units': 672.0, 'average_closing_stock': 3028.416748046875, 'maximum_opening_stock': 6279.0,
'minimum_closing_stock': 0.0, 'maximum_shortage_units': 672.0, 'variance_backlog': 62720.0,
'average_quantity_sold': 3091.583251953125, 'minimum_backlog': 0.0, 'maximum_backlog': 672.0,
'minimum_opening_stock': 0.0, 'standard_deviation_opening_stock': 2082.4554600412375, 'sku_id': 'KR202-230',
'standard_deviation_revenue': 2011.2938811593137, 'maximum_quantity_sold': 6278.0,
'average_opening_stock': 2994.916748046875, 'minimum_quantity_sold': 537.0, 'maximum_closing_stock': 6279.0,
'stockout_percentage': 0.0833333358168602, 'variance_opening_stock': 4336620.7430555625,
'variance_shortage_units': 34496.0, 'standard_deviation_closing_stock': 2096.713160255569,
'average_backlog': 112.0, 'variance_closing_stock': 4396206.0763888955,
'standard_deviation_shortage_cost': 185.7309882599024, 'minimum_shortage_units': 0.0, 'index': '22'}

The summarize_window returns max, min, averages and standard deviations for the primary values from the transaction summary.

The last method summarises the runs into one transaction summary for each SKU. Similar in content to the previous summary, however, this summary aggregates the simulation runs.

>>> from supplychainpy.model_inventory import analyse_orders_abcxyz_from_file
>>> from supplychainpy import simulate
>>>
>>> orders_analysis = analyse_orders_abcxyz_from_file(file_path="data.csv", z_value=Decimal(1.28),
>>>                                        reorder_cost=Decimal(5000), file_type="csv")
>>>
>>> sim = simulate.run_monte_carlo(orders_analysis=orders_analysis.orders, runs=100, period_length=12)
>>>
>>> sim_window = simulate.summarize_window(simulation_frame=sim, period_length=12)
>>>
>>> sim_frame= simulate.summarise_frame(sim_window)
>>>
>>> for transaction_summary in sim_frame:
>>>         print(transaction_summary)

Below is 1 of 32 results for 32 SKUs ran 100 times.

{'standard_deviation_quantity_sold': '2228', 'average_backlog': '0', 'standard_deviation_closing_stock': '2228',
'maximum_quantity_sold': 7901.0, 'sku_id': 'KR202-209', 'minimum_quantity_sold': 407.0, 'minimum_backlog': 0.0,
'average_closing_stock': '3592', 'average_shortage_units': '0', 'variance_opening_stock': '2287',
'minimum_opening_stock': 407, 'maximum_opening_stock': 7901, 'minimum_closing_stock': 407, 'service_level': '100.00',
'maximum_closing_stock': 7901, 'average_quantity_sold': '3592', 'standard_deviation_backlog': '0',
'maximum_backlog': 0.0}

An optimisation option exists, if after running the Monte Carlo analysis, the behaviour in the transaction summary is not favourable. If most SKUs are not achieving their desired service level or have large quantities of backlog etc., then you can use:

>>> from supplychainpy.model_inventory import analyse_orders_abcxyz_from_file
>>> from supplychainpy import simulate
>>>
>>> orders_analysis = analyse_orders_abcxyz_from_file(file_path="data.csv", z_value=Decimal(1.28),
>>>                                        reorder_cost=Decimal(5000), file_type="csv")
>>>
>>> sim = simulate.run_monte_carlo(orders_analysis=orders_analysis.orders, runs=100, period_length=12)
>>>
>>> sim_window = simulate.summarize_window(simulation_frame=sim, period_length=12)
>>>
>>> sim_frame= simulate.summarise_frame(sim_window)
>>>
>>> optimised_orders = simulate.optimise_service_level(service_level=95.0, frame_summary=sim_frame,
>>>                                            orders_analysis=orders_analysis.orders, runs=100, percentage_increase=1.30)

The optimise_service_level methods take a value for the desired service level, the transaction summary of the Monte Carlo simulation and the original orders analysis. The service level achieved in the Monte Carlo analysis is reviewed and compared with the desired service level. If below a threshold, then the safety stock is increased, and the full Monte Carlo simulation runs again. The supplied variable percentage_increase specifies the growth in safety stock.

This optimisation step will take as long, if not longer, than the first Monte Carlo simulation because the optimisation step runs the simulation again to simulate transactions based on the new safety stock values. Please take this into consideration and adjust your expectation for this optimisation step. This feature is in development as is the whole library but this feature will change in the next release.

For further details on the implementation, please view the deep-dive blog posts for each release.

Supplychainpy with Docker

The docker image for supplychainpy uses the continuumio/anaconda3 image, with a pre-installed version of supplychainpy and all the dependencies.

docker run -ti -v directory/on/client:directory/in/container --name fruit-smoothie -p5000:5000 supplychainpy/suchpy bash

The port, container name and directories can be changed as needed. Use a shared volume (as shown above) to present a CSV to the container for generating the report.

Make sure you specify the host as “0.0.0.0” for the reporting instance running in the container.

supplychainpy data.csv -a -loc / -lx --host 0.0.0.0

Formulas and Equations

The formal expression of the formulas and equations used in the library are detailed here.

Lead-time Demand

\[LD = LT \times D\]
where:

LD = Lead-time Demand

LT = Lead-time

D = Demand

Standard Deviation of Lead-time demand

\[\sigma_{LTD} = \sqrt{LT \times \sigma_{D}^2 + D^2 \times \sigma_{LT}^2}\]
where:

sigma_{LTD} = Standard deviation of lead-time demand

LT = Lead-time

D = Demand

Reorder Level

The formula used for calculating the reorder level is:

\[RL = LT \times D + Z \times \sigma \times \sqrt{LT}\]
where:

Z = service level

LT = Lead-time

D = Demand

Safety Stock

The formula used for safety stock is:

\[SS = Z \times \sigma \times \sqrt{LT}\]
where:

SS = Safety Stock

Z = service level

LT = Lead-time

Economic Order Quantity (eoq)

The economic order quantity is calculated using:

\[eoq_{0} = \sqrt \frac{2 \times R \times D}{HC}\]
where:

R = Reorder Cost

D = Demand

HC = Holding Cost

Indices and tables