top of page
Search

AIS-IO™ — Predictive Inventory Optimizer for SAP MM/CO

ree

Extending SAP Joule with prescriptive intelligence.

When SAP Joule appeared, it gave supply-chain teams a new kind of visibility — the ability to talk to their data.A planner can simply ask:


“Which materials in US01 haven’t moved for 180 days?”“What’s the stock value by age band?” and Joule answers instantly from live SAP tables.That’s powerful — but it’s only the first step.


AIS-IO™ (Inventory Optimizer) picks up where Joule leaves off.It adds predictive insight and optimization, recommending what to keep, transfer, or scrap — all within your service-level and budget limits.


What AIS-IO™ Does

AIS-IO™ connects directly to SAP MM and CO, turning static stock data into actionable intelligence.

Function

Description

Risk Scoring

Calculates obsolescence probability from stock aging, velocity, forecast gaps, and cost of capital.

Velocity Clustering

Uses K-means to group materials into Fast, Slow, or Dormant categories.

Optimization Engine

Balances scrap budgets and service-level targets with a linear-programming model.

Decision Output

Exports keep/transfer/scrap recommendations and MRP policy updates as CSVs.

How It Works

AIS-IO™ processes standard SAP data (MARD, MBEW, MSEG, MARC) and, optionally, forecast inputs.

SAP → Extract (CDS / RFC / CSV)
     ↓
AIS-IO Engine
     • Risk scoring
     • Velocity clustering
     • Optimization (budget + service)
     ↓
Output → CSV → SAP Upload

The workflow transforms descriptive analytics from Joule into prescriptive, financially aligned actions.


Streamlit Application (Complete Code)

# AIS-IO™ — Predictive Inventory Optimizer for SAP MM/CO
# Forecasts obsolescence, clusters velocity, and recommends optimal actions.

import numpy as np, pandas as pd, streamlit as st, altair as alt
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.ensemble import IsolationForest
try:
    import pulp; PULP_OK=True
except: PULP_OK=False

st.set_page_config(page_title="AIS-IO™ Inventory Optimizer", layout="wide")
st.title("AIS-IO™ — Predictive Inventory Optimizer for SAP MM/CO")
st.caption("Predict risk, optimize actions, and reduce working capital.")

# ---------- Data Prep ----------
def mock_data(n=600,seed=7):
    r=np.random.default_rng(seed)
    df=pd.DataFrame({
        "Material":[f"MAT{100000+i}"for i in range(n)],
        "Plant":r.choice(["US01","US02","DE01","SG01"],n,p=[.4,.3,.2,.1]),
        "UnrestrictedStock":r.integers(0,5000,n),
        "StdPrice":r.uniform(2,120,n).round(2),
        "DaysSinceLastMove":r.integers(0,720,n),
        "AvgMonthlyDemand":r.poisson(10,n).astype(float)
    })
    df["ForecastMonthlyDemand"]=(df["AvgMonthlyDemand"]*r.uniform(.6,1.3,n)).round(0)
    df["StockValue"]=df["UnrestrictedStock"]*df["StdPrice"]
    return df

def compute(df,cap,carry,wd):
    df=df.copy()
    df["AgeBand"]=pd.cut(df["DaysSinceLastMove"],[-1,90,180,360,9e3],
                         labels=["0-3m","3-6m","6-12m",">12m"])
    df["MOH"]=np.where(df["AvgMonthlyDemand"]>0,
                       df["UnrestrictedStock"]/df["AvgMonthlyDemand"],999)
    df["ForecastGap"]=(df["UnrestrictedStock"]-df["ForecastMonthlyDemand"]).clip(lower=0)
    df["CarryingCost$"]=df["StockValue"]*(carry/100)
    df["CapitalCost$"]=df["StockValue"]*(cap/100)
    rate={k:v/100 for k,v in wd.items()}
    df["WriteDownRate"]=df["AgeBand"].astype(str).map(lambda x:rate.get(x,0))
    df["ExpectedWriteDown$"]=df["StockValue"]*df["WriteDownRate"]
    eps=1e-9
    z=(0.35*df["DaysSinceLastMove"]/720+
       0.30*np.tanh(df["MOH"]/12)+
       0.20*df["ForecastGap"]/(df["ForecastGap"].max()+eps)+
       0.15*df["StockValue"]/(df["StockValue"].max()+eps))
    df["RiskScore"]=z*100
    df["RiskBucket"]=pd.cut(df["RiskScore"],[-1,25,50,75,200],
                            labels=["Low","Med","High","Critical"])
    return df

def cluster(df):
    X=StandardScaler().fit_transform(df[["MOH","DaysSinceLastMove"]].fillna(0))
    km=KMeans(n_clusters=3,n_init=10,random_state=42).fit(X)
    df["VelocityCluster"]=km.labels_
    order=np.argsort(km.cluster_centers_[:,0])
    mapping={order[0]:"Fast",order[1]:"Slow",order[2]:"Dormant"}
    df["VelocityClass"]=df["VelocityCluster"].map(mapping)
    return df

def anomalies(df):
    X=df[["UnrestrictedStock","AvgMonthlyDemand","MOH"]].fillna(0)
    df["AnomalyFlag"]=IsolationForest(random_state=42,contamination=.03)\
                      .fit(X).predict(X)==-1
    return df

def optimize(df,budget,target=1.5):
    if not PULP_OK:
        df["Action"]=np.where((df["VelocityClass"]=="Dormant")&(df["AgeBand"].isin(["6-12m",">12m"]))&
                              (df["ExpectedWriteDown$"]>0),"SCRAP",
                       np.where((df["VelocityClass"]!="Fast")&(df["MOH"]>6),"TRANSFER","KEEP"))
        return df
    df=df.copy()
    m=pulp.LpProblem("AIS-IO",pulp.LpMinimize)
    I=df.index.tolist()
    xk,xt,xs=[pulp.LpVariable.dicts(n,I,0,1,cat="Binary")for n in["k","t","s"]]
    for i in I: m+=xk[i]+xt[i]+xs[i]==1
    m+=pulp.lpSum(xs[i]*df.loc[i,"ExpectedWriteDown$"]for i in I)<=budget
    fast=df[df["VelocityClass"]=="Fast"]
    dem=fast.groupby("Plant")["AvgMonthlyDemand"].sum().to_dict()
    for p,v in dem.items():
        idx=df[(df["Plant"]==p)&(df["VelocityClass"]=="Fast")].index
        m+=pulp.lpSum((xk[i]+xt[i])*df.loc[i,"UnrestrictedStock"]for i in idx)>=target*v
    obj=[]
    for i in I:
        carr,cap,wd=df.loc[i,["CarryingCost$","CapitalCost$","ExpectedWriteDown$"]]
        obj+= [xk[i]*(.08*carr+.08*cap),
               xt[i]*(1+0.05*carr),
               xs[i]*(0.5*wd)]
    m+=pulp.lpSum(obj);m.solve(pulp.PULP_CBC_CMD(msg=False))
    df["Action"]=[ "SCRAP" if pulp.value(xs[i])>.5 else
                   "TRANSFER" if pulp.value(xt[i])>.5 else "KEEP" for i in I]
    return df

# ---- Streamlit execution ----
s=st.sidebar
s.header("Parameters")
cap=s.number_input("Cost of Capital %",0.,30.,10.)
carry=s.number_input("Carrying Cost %",0.,40.,18.)
wd={"0-3m":s.number_input("0–3 m %",0.,100.,0.),
    "3-6m":s.number_input("3–6 m %",0.,100.,5.),
    "6-12m":s.number_input("6–12 m %",0.,100.,20.),
    ">12m":s.number_input(">12 m %",0.,100.,50.)}
budget=s.number_input("Monthly Scrap Budget $",0.,1e6,1e5,1e4)
target=s.number_input("Target MOH Fast",0.,12.,1.5,.5)
up=s.file_uploader("Upload MB52-like CSV",type="csv")
df=pd.read_csv(up) if up else mock_data()
df=compute(df,cap,carry,wd)
df=cluster(df);df=anomalies(df)
with st.spinner("Optimizing…"): df=optimize(df,budget,target)
tabs=st.tabs(["Overview","Clusters","Recommendations"])
with tabs[0]:
    st.metric("Total Stock Value",f"${df['StockValue'].sum():,.0f}")
    st.dataframe(df[["Material","Plant","StockValue","MOH","RiskBucket","VelocityClass","Action"]],
                 use_container_width=True)
with tabs[1]:
    chart=alt.Chart(df).mark_circle().encode(
        x="MOH",y="RiskScore",size="StockValue",color="VelocityClass",
        tooltip=["Material","Plant","RiskBucket","VelocityClass"]).interactive()
    st.altair_chart(chart,use_container_width=True)
with tabs[2]:
    filt=st.multiselect("Action",["KEEP","TRANSFER","SCRAP"],["TRANSFER","SCRAP"])
    st.dataframe(df[df["Action"].isin(filt)],
                 use_container_width=True,height=400)
st.success("Upload SAP extracts to replace mock data.")

SAP Integration

AIS-IO™ connects to SAP through a CDS view or RFC extractor.

CDS View

@AbapCatalog.sqlViewName: 'ZV_INV_MB52'
define view ZCDS_INV_MB52 as select from mard
  inner join mara on mara.matnr = mard.matnr
  left outer join mbew on mbew.matnr = mard.matnr and mbew.bwkey = mard.werks
{
  key mard.matnr as Material,
  key mard.werks as Plant,
      mard.labst as UnrestrictedStock,
      mbew.stprs as StdPrice
}

RFC Extractor

FUNCTION Z_INV_OBS_EXTRACT RFC ENABLED.
  TABLES ET_MB52 TYPE ZINV_MB52_T.
  SELECT mara~matnr mard~werks mard~labst mbew~stprs
    FROM mara INNER JOIN mard ON mard~matnr=mara~matnr
    LEFT JOIN mbew ON mbew~matnr=mara~matnr AND mbew~bwkey=mard~werks
    INTO CORRESPONDING FIELDS OF TABLE et_mb52.
ENDFUNCTION.

Working Together with Joule

Joule:

“Show materials older than 180 days.”

AIS-IO™:

“Within a $100 K scrap budget and 1.5 months-on-hand target, here’s the recommended keep/transfer/scrap plan.”

Joule handles the conversation.AIS-IO™ handles the decision.


Outcomes

  • 20–30 % reduction in carrying cost through targeted disposal

  • Faster decision cycles in MRP and CO

  • Improved service-level compliance

  • A continuous intelligence loop between Joule insights and SAP actions

 
 
 

Comments


Featured Blog Post

bottom of page