AIS-IO™ — Predictive Inventory Optimizer for SAP MM/CO
- Walf Sun
- 4 days ago
- 3 min read

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 
.png)



Comments