Orchestrate

Report an issue

Something wrong, out of date or unclear? Tell us. Corrections are prioritized over new topics. You don't need to sign in.

0 / 2000

Don't include personal or confidential company details. Reports are reviewed with AI assistance.

Is your SAP data ready for AI? A readiness check you can run

Judge whether SAP master data is good enough for one AI use case, find where it lives and how to reach it, and produce a one-page readiness report.

Updated Oct 15, 2026Foundational 8 minDeep 35 min
Foundational layer · 8 min read

The 60-second version

Most AI projects on SAP don't fail because the model is weak. They stall because the data the model needs is incomplete, duplicated, locked away, or full of personal details nobody cleared for that use.

"Is our data ready?" has no yes-or-no answer in general. It has an answer for one use case. Supplier master data can be fine for a monthly spend report and not ready for an assistant that tells a buyer which supplier to call. The report tolerates a few duplicates. The assistant will give the wrong phone number with full confidence.

A readiness check answers five questions for that one use case:

  1. Quality. Are the fields the use case needs filled, valid, consistent and unique?
  2. Location. Which systems hold the data: S/4HANA, ECC, SAP BW, SAP Business Data Cloud, or non-SAP tools?
  3. Access. Is there a supported way to read it, such as a released API or a data product?
  4. Ownership. Who owns each dataset and fixes it when it is wrong?
  5. Personal data. Which records describe people, and may this use case process them?

The output is one page: scores, the top problems, and a verdict of ready, ready after fixes, or not ready.

Why it matters to the business

Gartner predicted in February 2025 that, through 2026, organizations will abandon 60% of AI projects that lack AI-ready data. The same release says 63% of organizations lack, or are unsure they have, the right data management practices for AI. Gartner's point is that data must represent the use case, including its errors and outliers. Clean data in general isn't the goal.

In SAP terms, the cost of skipping the check shows up in familiar places:

  • Procure-to-pay. The same supplier exists twice, created years apart. An AI that drafts payment reminders or ranks suppliers splits the history across two records and gets both wrong. Duplicate suppliers also feed the three-way match exceptions this course keeps returning to.
  • Plan-to-produce. One product's weight is in grams in a field everyone reads as kilograms. A forecasting or logistics model learns from nonsense, and MRP exceptions multiply.
  • Order-to-cash. A blocked-order assistant explains a credit block using a customer record that was already marked for deletion. The answer is fluent and wrong.

The money question is simple. A readiness check costs days. Discovering bad data after the pilot costs the pilot and the sponsor's trust. Fixing data also pays off outside AI, because the same errors already slow down people.

How SAP does it

As of October 2026, SAP's main tool for master data quality is SAP Master Data Governance (MDG). On SAP S/4HANA, MDG frames data quality as a loop of four steps: define, enter, monitor and improve quality. Its main parts, from SAP's MDG courses on learning.sap.com:

  • Data quality rules with a business owner and a data owner. The same rule can check existing data, new change requests, and mass loads.
  • Evaluations that run all rules against active data on a schedule. Scores roll up by dimension, with target and warning thresholds.
  • Rule mining, which uses machine learning to propose rules from your existing product and business partner data. People accept or reject each proposal before it becomes a rule.

SAP's May 2026 news on SAP Business Data Cloud says MDG is now a core part of SAP Business Data Cloud. The same piece says SAP's completed acquisition of Reltio brings multi-domain master data management, with AI-based matching of related records, into SAP Business Data Cloud. Check product scope and licensing with SAP for your landscape, since this is moving.

For access, SAP's clean core guidance says to use released APIs, which SAP keeps stable from release to release. You find them on the SAP Business Accelerator Hub. SAP Business Data Cloud provides curated data products; the SAP Knowledge Graph and Business Data Cloud topic explains them.

The readiness checklist

Use this table in the first workshop. Each row gets one answer and one owner.

Area The question Red flag
Use case Which decision or task will the AI support, and which fields does it read? "All our data", or no named fields
Completeness Are the needed fields filled for the records in scope? Gaps clustered in one country, plant or load batch
Uniqueness Does each supplier, customer or product exist once? Same tax number on two suppliers
Validity and consistency Do values follow formats and agree with each other? Weights without units, net weight above gross
Timeliness Is the data current enough for the decision? Records untouched for years still in scope
Location Which systems hold it: S/4HANA, ECC, BW, Business Data Cloud, non-SAP? Copies in several places, none trusted
Access Is there a released API, released CDS view or data product for it? A table extract or an extraction method SAP restricts
Ownership Who owns each dataset and approves fixes? "IT owns it", or nobody
Personal data Which records describe people, and may this use case process them? Birth dates or private addresses in an extract that doesn't need them
Authorizations Will the AI respect who may see what? One technical user that sees everything

Questions to ask

Ask your team, your SAP partner or a vendor:

  1. Which exact fields will the AI read, and which ones change its answer?
  2. What are today's scores for completeness, duplicates and validity on those fields? Who measured them, and when?
  3. Where is the system of record for each dataset, and are there copies?
  4. Which released API, CDS view or data product will we read from? If it's an extraction tool, which method does it use?
  5. Who owns each dataset, and how does a fix get back into SAP?
  6. Which records are personal data, and has the data protection officer agreed to this purpose?
  7. Are deleted and blocked records excluded before the AI sees them?
  8. Do we have MDG or a similar tool already, and can its rules be reused?

Common misconceptions

  • "AI will clean the data for us." AI can propose rules and match duplicates, as MDG's rule mining and Reltio's matching show. A person still decides what is correct, and the fix still has to reach SAP.
  • "Our data is in S/4HANA, so it's clean." A migration copies old problems into a new system unless someone cleaned them on the way.
  • "One overall score tells us we're ready." An average of 92 can hide a duplicate-supplier problem that breaks the use case. Look at the high-impact rules on their own.
  • "Readiness is a one-time project." Gartner frames AI-ready data as an ongoing practice. New records arrive every day, so rules must run on a schedule.
  • "More data is always better." For personal data the opposite holds. GDPR's data minimisation principle limits personal data to what the purpose needs.

Key terms

All course terms, A to Z, in the glossary

  • Master data: long-lived records that transactions reuse, such as suppliers, customers and products.
  • Data quality rule: a check one field or record must pass, such as "a weight has a weight unit".
  • Dimension: a group of rules that measure one kind of quality, such as completeness or uniqueness.
  • Readiness verdict: ready, ready after fixes, or not ready, for one named use case.
  • Released API: an interface SAP keeps stable across releases; the clean core way to read data.
  • Data product: a curated, documented dataset in SAP Business Data Cloud, meant for analytics and AI.
  • System of record: the one system whose copy of the data counts when copies disagree.
  • Data minimisation: a GDPR principle that personal data must be limited to what the purpose needs.

Check yourself

Pick one answer for each question. The explanation appears after you choose.

1Your CIO asks, "Is our supplier data ready for AI?" What is the most useful first reply?

Answer: A. Readiness depends on the use case. A spend report and a supplier-recommending assistant need different fields and tolerate different errors, so the check starts by naming the use case and its fields.

2According to Gartner's February 2025 prediction, what happens to many AI projects without AI-ready data?

Answer: C. Gartner predicted that organizations will abandon 60% of AI projects unsupported by AI-ready data through 2026. That is why the readiness check comes before the pilot, not after it.

3A supplier dataset scores 92 overall, but 17% of suppliers share a tax number with another supplier. What should you conclude?

Answer: B. An average can hide a blocker. Duplicate suppliers break a supplier assistant directly, so a high-impact rule below target means "ready after fixes" whatever the overall score says.

4Which SAP offering checks master data quality with rules, scores and scheduled evaluations?

Answer: C. SAP Master Data Governance defines data quality rules, runs evaluations against active data and scores them by dimension. Its rule mining can also propose rules from existing data for people to accept.

5Your partner plans to pull S/4HANA data into a non-SAP AI platform with an existing extraction tool. What do you ask?

Answer: C. Summaries of SAP Note 3255746 say SAP restricts ODP over RFC for moving data into non-SAP tools, with a blocking patch expected from June 2026. Knowing the method early avoids a pipeline that stops working mid-project.

6The AI extract includes supplier birth dates, but the use case only needs names, addresses and tax numbers. What should happen?

Answer: B. GDPR's data minimisation principle limits personal data to what the purpose needs. If the use case doesn't use birth dates, they should never reach the AI.

7Who should own fixing the duplicate suppliers that the readiness check found?

Answer: C. The readiness checklist names an owner per dataset. MDG rules carry a business owner and a data owner for the same reason. Without an owner and a way to get fixes into SAP, the same duplicates come back.
Deep layer · 35 min read

Mental model

Readiness is a property of data and a use case together, measured by rules. Write down what "good enough" means as a short list of rules, each tied to a field the use case reads. Run the rules. Each rule gives a score: the share of records that pass. Rules roll up into dimensions, and dimensions into a verdict.

That is the same shape SAP Master Data Governance uses. Once you see it, three things follow:

  • Change the use case and the rules change, so the verdict changes. The data didn't change.
  • A rule that matters more gets more weight, and a failing high-impact rule blocks the verdict no matter what the average says.
  • Profiling only answers quality questions. Location, access, ownership and personal data need answers from people, so the report leaves space for them.

How it works

Six dimensions

Data quality practice usually groups checks into six dimensions. This topic uses them as follows.

Dimension Question SAP example
Completeness Is the field filled where the use case needs it? Supplier without a country or tax number
Validity Does the value follow its format and type? Weight without a weight unit; a German postal code with four digits
Consistency Do values agree within the record or across records? Net weight above gross weight
Uniqueness Does each real-world thing appear once? Two suppliers with the same tax number
Accuracy Does the value match reality? 34,000 kg for a pump that weighs 34 kg
Timeliness Is it current enough for the decision? A product untouched since 2014 still in scope

Accuracy is the hard one. A script can't know a pump's true weight. It can flag a value that sits far from its peers, which is an accuracy question for a person, not a verdict.

Rules, scores and weights

SAP MDG documents a simple scoring scheme, and the script below copies it:

  • A rule returns OK or Not OK for each record in scope. Records where the rule doesn't apply (a weight-unit rule for a product with no weight) are skipped.
  • Rule score = OK / (OK + Not OK), as a percentage.
  • Each rule has an impact: none (0), low (1), medium (2) or high (3). A dimension score is the impact-weighted average of its rule scores. A high-impact rule counts three times as much as a low one.
  • MDG sets target, warning and critical thresholds per dimension. The script uses a target of 90 and a warning of 75 for the overall verdict, and adds one rule of its own: any high-impact rule below target means "ready after fixes".

Scope comes first. Deleted and blocked records are removed before scoring. The AI must not use them, so they shouldn't flatter or punish the score.

Duplicates without a matching engine

Two cheap signals catch most duplicates in a first check:

  1. Same identifier. Two suppliers with the same VAT registration number are either duplicates, a rename, or a branch. Only a person can tell which, but it is always worth a look.
  2. Near-identical names inside a block. Normalize names (lowercase, strip accents, punctuation and legal forms such as GmbH), then compare names only within the same country or product group. Comparing everything with everything is slow and produces noise.

One trap: "Centrifugal pump DN20" and "Centrifugal pump DN40" look 95% alike as text but are different products. The script refuses to call two names duplicates when their numbers differ.

Outliers need units first

You can't compare 25,000 g with 30 kg until both are in kilograms. The script converts weights using the weight unit, then compares each product with others in its product group using the robust modified z-score from Anomaly detection in finance and procurement. A score beyond 3.5 marks a record for a look. Expect false alarms: a small but real seal will be flagged now and then.

Where the data lives and how you reach it

flowchart LR
  S4[S/4HANA] -->|released APIs, released CDS views| APP[Your AI app]
  S4 -->|data products| BDC[Business Data Cloud]
  ECC[ECC] -->|extraction, check method| LAKE[Non-SAP platform]
  BW[SAP BW] -->|extraction, check method| LAKE
  BDC --> APP
  LAKE --> APP
  MDG[MDG rules and scores] -.governs.-> S4

Four access paths matter for readiness:

  • Released APIs (OData and SOAP services on the SAP Business Accelerator Hub). SAP keeps them stable across releases. Best for live lookups by an assistant or agent, as in Calling your first SAP API.
  • Released CDS views. Local APIs inside S/4HANA. SAP's example is I_COUNTRY, which replaces direct access to table T005.
  • Data products in SAP Business Data Cloud, for analytics and model training at volume. See SAP Knowledge Graph and Business Data Cloud.
  • Extraction tools that copy data out. Check which method they use. SAP Note 3255746 restricts ODP over RFC for SAP-to-non-SAP integration, according to industry summaries; read the note itself in SAP for Me before you decide.

Older systems such as ECC have fewer released APIs; AI for SAP ECC customers before they migrate covers what to build on ECC before the move.

The field layout also differs from what people expect. In the Business Partner API, A_Supplier holds the supplier's name, tax numbers and block flags, but the address (Country, CityName, PostalCode) lives on A_BusinessPartnerAddress. An extract that forgets the join scores zero on address completeness. The script reports that as a missing column, which is the right finding.

Personal data

Suppliers and customers can be people: sole traders, freelancers, private customers. A_Supplier has an IsNaturalPerson flag and a BirthDate field. For such records, the name and address are personal data too.

Two rules from GDPR Article 5 drive the readiness decision. Purpose limitation: data collected for paying a supplier is not automatically cleared for training a model. Data minimisation: only the fields the purpose needs go in. The script lists personal-data fields found in the extract and every column the use case doesn't need, so you can drop them. Data security and PII covers masking, blocking and retention.

Build it yourself: a one-page readiness report

You will run one script that profiles product and supplier master data, shaped like SAP's Product and Business Partner APIs, for one use case: a procurement assistant. It scores six dimensions, flags duplicates, odd weights and personal data, and writes readiness_report.md. The sample has problems planted on purpose, so you can see each check catch something.

Before you start: complete Set up your computer for this course and Set up for Unit 7. They give you the orchestrate-course folder, its .venv, the unit07 folder, and requests and python-dotenv. This walkthrough needs no new library: the profiling uses only modules built into Python.

flowchart LR
  D[Products and suppliers CSV] --> SC[Scope: drop deleted and blocked]
  SC --> R[Rules per dimension]
  R --> S[Scores, MDG style]
  S --> V[Verdict]
  R --> F[Duplicates, outliers, personal data]
  V --> REP[readiness_report.md]
  F --> REP

What you need

  • The course folder, .venv and unit07 folder from the two setup topics.
  • About 35 minutes. No account and no internet: --sample builds the data on your computer.
  • Optional: your SAP Business Accelerator Hub key in .env as SAP_API_KEY (from the Unit 1 setup) to profile SAP's sandbox data in Step 7. Free.

Step 1: Open the course folder and turn on the environment

  1. Open VS Code, choose File > Open Folder and open orchestrate-course.

  2. Open a terminal: Terminal > New Terminal.

  3. Turn on the virtual environment if the prompt doesn't start with (.venv):

    • Windows (PowerShell):

      .venv\Scripts\Activate.ps1
    • macOS / Linux:

      source .venv/bin/activate
  4. Check Python (the same command everywhere):

    python --version

    What success looks like: Python 3.10 or later, such as Python 3.12.6.

  5. Go into the Unit 7 folder:

    cd unit07

Step 2: Save the script

  1. In VS Code, right-click the unit07 folder, choose New File, and name it data_readiness.py.
  2. Paste the code below and save (Ctrl+S on Windows, Cmd+S on macOS).
"""Orchestrate course, Unit 7: profile SAP-shaped master data and write a readiness report.

Usage:
  python data_readiness.py --sample                     made-up products and suppliers (no account)
  python data_readiness.py --products p.csv --suppliers s.csv   your own CSV extracts
  python data_readiness.py --sandbox                    SAP Business Accelerator Hub sandbox (SAP_API_KEY in .env)

Writes readiness_report.md next to this script. Built-in modules only, except
--sandbox, which uses requests and python-dotenv from the Unit 1 setup.
"""
import argparse
import csv
import random
import re
import statistics
import sys
import unicodedata
from collections import Counter, defaultdict
from datetime import date
from difflib import SequenceMatcher
from pathlib import Path

HERE = Path(__file__).resolve().parent
DATA = HERE / "readiness_data"
REPORT = HERE / "readiness_report.md"
TODAY = date(2026, 10, 15)  # fixed so sample results are repeatable; use date.today() for real data

# 1. The readiness definition: what "good enough" means for ONE use case.
#    Use case: a procurement assistant that answers questions about products and suppliers.
#    Impact follows SAP MDG's scale: 1 low, 2 medium, 3 high.
USE_CASE = "Procurement assistant: answer questions about products and suppliers"
SPEC = {
    "products": {
        "key": "Product",
        "name": "ProductDescription",
        "complete": {"ProductDescription": 3, "ProductType": 3, "BaseUnit": 3, "ProductGroup": 2,
                     "GrossWeight": 1},
        "needed": ["Product", "ProductDescription", "ProductType", "ProductGroup", "BaseUnit",
                   "GrossWeight", "NetWeight", "WeightUnit", "IsMarkedForDeletion", "LastChangeDate"],
        "stale_years": 5,
    },
    "suppliers": {
        "key": "Supplier",
        "name": "SupplierName",
        "complete": {"SupplierName": 3, "Country": 3, "CityName": 2, "TaxNumber1|VATRegistration": 2,
                     "PostalCode": 1},
        "needed": ["Supplier", "SupplierName", "Country", "CityName", "PostalCode", "TaxNumber1",
                   "VATRegistration", "PurchasingIsBlocked", "PostingIsBlocked", "DeletionIndicator"],
        "personal": ["BirthDate", "IsNaturalPerson"],
    },
}
TO_KG = {"KG": 1.0, "G": 0.001, "LB": 0.45359237, "TO": 1000.0}  # your system's units may differ
TARGET, WARNING = 90.0, 75.0  # our thresholds, set with the business


def blank(value):
    return value is None or str(value).strip() == ""


def truthy(value):
    return str(value).strip().lower() in {"true", "x", "1", "yes"}


def number(value):
    try:
        return float(str(value).replace(",", "."))
    except ValueError:
        return None


def norm_name(text):
    """Lowercase, strip accents, punctuation and legal-form words, for duplicate matching."""
    text = unicodedata.normalize("NFKD", str(text)).encode("ascii", "ignore").decode().lower()
    text = re.sub(r"[^a-z0-9 ]", " ", text.replace(".", ""))
    stop = {"gmbh", "ag", "inc", "ltd", "llc", "co", "kg", "sa", "srl", "bv", "the", "and"}
    return " ".join(w for w in text.split() if w not in stop)


# 2. Sample data: SAP-shaped, made up, with problems planted on purpose.
def make_sample():
    rng = random.Random(7)
    groups = {"PUMPS": ("Centrifugal pump", 12, 60), "VALVES": ("Ball valve", 0.4, 6),
              "SEALS": ("Shaft seal", 0.05, 0.6), "MOTORS": ("Electric motor", 8, 45)}
    products = []
    for i in range(60):
        group = list(groups)[i % 4]
        label, low, high = groups[group]
        gross = round(rng.uniform(low, high), 2)
        products.append({
            "Product": f"TG-{1000 + i}", "ProductType": "FERT" if group != "SEALS" else "HAWA",
            "ProductGroup": group, "BaseUnit": "EA", "ProductDescription": f"{label} DN{20 + 5 * i}",
            "GrossWeight": gross, "NetWeight": round(gross * 0.9, 2), "WeightUnit": "KG",
            "IsMarkedForDeletion": "false",
            "LastChangeDate": f"{rng.randint(2022, 2026)}-{rng.randint(1, 12):02d}-15"})
    p = products
    p[3]["ProductDescription"] = ""                           # missing description
    p[7]["ProductDescription"] = ""
    p[11]["BaseUnit"] = ""                                    # missing base unit
    p[14]["WeightUnit"] = ""                                  # weight without unit
    p[19]["GrossWeight"], p[19]["NetWeight"] = 20.0, 22.5     # net heavier than gross
    p[24]["GrossWeight"], p[24]["NetWeight"], p[24]["WeightUnit"] = 25000, 22500, "G"  # grams: fine once converted
    p[28]["GrossWeight"], p[28]["NetWeight"] = 34000, 30600   # grams typed into a KG field
    p[30]["ProductGroup"] = ""                                # missing group
    p[34]["LastChangeDate"] = "2014-03-02"                    # stale
    p[38]["LastChangeDate"] = "2016-11-20"
    for i in range(2, 60, 8):
        p[i]["GrossWeight"] = p[i]["NetWeight"] = ""         # trading goods often lack weights
    p[42]["IsMarkedForDeletion"] = "true"                     # deleted: out of scope
    dup = dict(p[5], Product="TG-1100", ProductDescription=p[5]["ProductDescription"].upper())
    products.append(dup)                                      # same item created twice

    cities = [("DE", "Hamburg", "20095"), ("DE", "Munich", "80331"), ("US", "Houston", "77002"),
              ("FR", "Lyon", "69002"), ("IN", "Pune", "411001")]
    first = ["Nordwerk", "Baltic", "Gulf", "Rhone", "Deccan", "Alpen", "Lone Star", "Hanse"]
    second = ["Pumpen", "Valves", "Seal Supply", "Moteurs", "Castings"]
    suppliers = []
    for i in range(40):
        country, city, postal = cities[i % 5]
        base = f"{first[i % 8]} {second[i // 8]}"
        suppliers.append({
            "Supplier": f"{17300000 + i}", "SupplierName": base + (" GmbH" if country == "DE" else ""),
            "SupplierAccountGroup": "SUPL", "Country": country, "CityName": city, "PostalCode": postal,
            "TaxNumber1": "", "VATRegistration": f"{country}{rng.randint(100000000, 999999999)}",
            "IsNaturalPerson": "", "BirthDate": "", "PurchasingIsBlocked": "false",
            "PostingIsBlocked": "false", "DeletionIndicator": "false"})
    s = suppliers
    s[4]["Country"] = ""                                      # missing country
    s[9]["CityName"], s[9]["PostalCode"] = "", ""             # missing address
    s[13]["VATRegistration"] = ""                             # no tax number at all
    for i in range(24, 40, 5):
        s[i]["VATRegistration"], s[i]["TaxNumber1"] = "", ""  # one country's suppliers were loaded without tax data
    s[16]["PostalCode"] = "2009"                              # DE postal code too short
    s[21]["PurchasingIsBlocked"] = "true"                     # blocked: out of scope
    s[27]["DeletionIndicator"] = "true"
    s[33].update(SupplierName="Maria Keller", IsNaturalPerson="X", BirthDate="1979-04-12")  # a person
    suppliers.append(dict(s[0], Supplier="17300100", SupplierName="Nordwerk Pumpen G.m.b.H."))  # near duplicate
    suppliers.append(dict(s[18], Supplier="17300101", SupplierName=s[18]["SupplierName"] + " Co"))
    suppliers.append(dict(s[7], Supplier="17300102", SupplierName="Hanse Dichtungstechnik"))  # same tax number,
    # different name: a rename, a branch, or a typo. Only a person can tell.

    DATA.mkdir(exist_ok=True)
    for name, rows in (("products", products), ("suppliers", suppliers)):
        with open(DATA / f"{name}.csv", "w", newline="", encoding="utf-8") as f:
            writer = csv.DictWriter(f, fieldnames=list(rows[0]))
            writer.writeheader()
            writer.writerows(rows)
    return DATA / "products.csv", DATA / "suppliers.csv"


# 3. Optional: read the SAP sandbox (A_Product with descriptions, A_Supplier).
def fetch_sandbox():
    try:
        import requests
        from dotenv import load_dotenv
    except ImportError:
        sys.exit("--sandbox needs requests and python-dotenv: pip install requests python-dotenv")
    import os
    load_dotenv()
    key = os.environ.get("SAP_API_KEY")
    if not key:
        sys.exit("SAP_API_KEY is not set. Add it to .env, or run with --sample.")
    base = "https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap/"
    jobs = {
        "products": ("API_PRODUCT_SRV/A_Product", {
            "$select": "Product,ProductType,ProductGroup,BaseUnit,GrossWeight,NetWeight,WeightUnit,"
                       "IsMarkedForDeletion,LastChangeDate,to_Description/ProductDescription,"
                       "to_Description/Language",
            "$expand": "to_Description"}),
        "suppliers": ("API_BUSINESS_PARTNER/A_Supplier", {
            "$select": "Supplier,SupplierName,SupplierAccountGroup,TaxNumber1,VATRegistration,"
                       "IsNaturalPerson,BirthDate,PurchasingIsBlocked,PostingIsBlocked,DeletionIndicator"}),
    }
    DATA.mkdir(exist_ok=True)
    paths = []
    for name, (path, params) in jobs.items():
        params = {**params, "$top": "200", "$format": "json"}
        try:
            resp = requests.get(base + path, params=params, headers={"APIKey": key, "Accept": "application/json"},
                                timeout=60)
        except requests.RequestException as err:
            sys.exit(f"Could not reach sandbox.api.sap.com ({type(err).__name__}). Try --sample.")
        if resp.status_code != 200:
            sys.exit(f"Sandbox returned HTTP {resp.status_code} for {path}. Check SAP_API_KEY, or try --sample.")
        rows = []
        for item in resp.json()["d"]["results"]:
            item.pop("__metadata", None)
            texts = item.pop("to_Description", None)
            if texts is not None:
                english = [t["ProductDescription"] for t in texts.get("results", []) if t.get("Language") == "EN"]
                item["ProductDescription"] = english[0] if english else ""
            item = {k: ("" if v is None else v) for k, v in item.items()}
            for k, v in item.items():  # OData V2 dates arrive as /Date(ms)/
                match = re.fullmatch(r"/Date\((-?\d+)[^)]*\)/", str(v))
                if match:
                    item[k] = date.fromtimestamp(int(match.group(1)) / 1000).isoformat() if int(match.group(1)) > 0 else ""
            rows.append(item)
        if not rows:
            sys.exit(f"The sandbox returned no {name}.")
        out = DATA / f"sandbox_{name}.csv"
        with open(out, "w", newline="", encoding="utf-8") as f:
            writer = csv.DictWriter(f, fieldnames=list(rows[0]))
            writer.writeheader()
            writer.writerows(rows)
        print(f"Saved {len(rows)} {name} to {out.name}")
        paths.append(out)
    return paths


# 4. Rules. Each rule returns (dimension, impact, rule name, ok keys, not-ok keys).
def load(path):
    try:
        with open(path, newline="", encoding="utf-8-sig") as f:
            return list(csv.DictReader(f))
    except FileNotFoundError:
        sys.exit(f"File not found: {path}")


def check_dataset(kind, rows):
    spec = SPEC[kind]
    key = spec["key"]
    columns = set(rows[0]) if rows else set()
    if key not in columns:
        sys.exit(f"{kind}: the key column {key} is missing. Columns found: {sorted(columns)}")
    findings = {"missing_columns": [], "out_of_scope": [], "outliers": [], "duplicates": [], "personal": {}}

    # Records that are deleted or blocked are excluded before scoring: the assistant must not use them.
    flags = [c for c in ("IsMarkedForDeletion", "DeletionIndicator", "PurchasingIsBlocked", "PostingIsBlocked")
             if c in columns]
    in_scope = []
    for r in rows:
        hit = [c for c in flags if truthy(r.get(c))]
        (findings["out_of_scope"].append((r[key], ", ".join(hit))) if hit else in_scope.append(r))
    rules = []

    def rule(dimension, impact, name, test):
        ok, bad = [], []
        for r in in_scope:
            verdict = test(r)
            if verdict is None:  # rule not applicable to this record
                continue
            (ok if verdict else bad).append(r[key])
        rules.append((dimension, impact, name, ok, bad))

    # Completeness: one rule per field the use case needs. "A|B" means either field counts.
    for field, impact in spec["complete"].items():
        options = field.split("|")
        present = [o for o in options if o in columns]
        if not present:
            findings["missing_columns"].append(field)
            rules.append(("Completeness", impact, f"{field} filled", [], [r[key] for r in in_scope]))
            continue
        rule("Completeness", impact, f"{field} filled", lambda r, p=present: any(not blank(r.get(o)) for o in p))

    if kind == "products":
        rule("Validity", 3, "Weight has a weight unit",
             lambda r: None if blank(r.get("GrossWeight")) else not blank(r.get("WeightUnit")))
        rule("Validity", 2, "Weight unit is known to this script",
             lambda r: None if blank(r.get("WeightUnit")) else r["WeightUnit"].upper() in TO_KG)
        rule("Consistency", 2, "Net weight not above gross weight",
             lambda r: None if None in (number(r.get("NetWeight") or ""), number(r.get("GrossWeight") or ""))
             else number(r["NetWeight"]) <= number(r["GrossWeight"]))
        if "LastChangeDate" in columns:
            limit = TODAY.year - spec["stale_years"]
            rule("Timeliness", 1, f"Changed since {limit}",
                 lambda r: None if blank(r.get("LastChangeDate")) else int(r["LastChangeDate"][:4]) >= limit)
        # Outliers: weight in kg, compared within the product group (robust modified z-score).
        by_group = defaultdict(list)
        for r in in_scope:
            gross, unit = number(r.get("GrossWeight") or ""), str(r.get("WeightUnit", "")).upper()
            if gross is not None and unit in TO_KG:
                by_group[r.get("ProductGroup") or "(none)"].append((r[key], gross * TO_KG[unit]))
        flagged = set()
        for group, items in by_group.items():
            if len(items) < 5:
                continue
            values = [v for _, v in items]
            med = statistics.median(values)
            mad = statistics.median(abs(v - med) for v in values)
            for k, v in items:
                score = 0.6745 * (v - med) / mad if mad else 0.0
                if abs(score) > 3.5:
                    flagged.add(k)
                    findings["outliers"].append((k, group, round(v, 2), round(score, 1)))
        rule("Accuracy", 2, "Weight plausible within its group", lambda r: r[key] not in flagged)

    if kind == "suppliers":
        rule("Validity", 1, "DE postal code has 5 digits",
             lambda r: None if r.get("Country") != "DE" or blank(r.get("PostalCode"))
             else bool(re.fullmatch(r"\d{5}", r["PostalCode"].strip())))
        tax_field = "VATRegistration" if "VATRegistration" in columns else None
        if tax_field:
            counts = Counter(r[tax_field] for r in in_scope if not blank(r.get(tax_field)))
            rule("Uniqueness", 3, "Tax number used by one supplier only",
                 lambda r: None if blank(r.get(tax_field)) else counts[r[tax_field]] == 1)

    # Uniqueness by name: near-identical names inside the same group or country.
    block = "ProductGroup" if kind == "products" else "Country"
    buckets = defaultdict(list)
    for r in in_scope:
        if not blank(r.get(spec["name"])):
            buckets[r.get(block, "")].append((r[key], r[spec["name"]], norm_name(r[spec["name"]])))
    dup_keys = set()
    for items in buckets.values():
        for i in range(len(items)):
            for j in range(i + 1, len(items)):
                if re.findall(r"\d+", items[i][2]) != re.findall(r"\d+", items[j][2]):
                    continue  # "pump DN20" and "pump DN40" are different products
                ratio = SequenceMatcher(None, items[i][2], items[j][2]).ratio()
                if ratio >= 0.9:
                    findings["duplicates"].append((items[i][0], items[j][0], items[i][1], items[j][1], round(ratio, 2)))
                    dup_keys.update({items[i][0], items[j][0]})
    rule("Uniqueness", 3, "Name not a near-duplicate of another record", lambda r: r[key] not in dup_keys)

    # Personal data: which personal fields are present, filled, and needed by the use case.
    for field in spec.get("personal", []):
        if field in columns:
            filled = [r[key] for r in rows if not blank(r.get(field))]
            findings["personal"][field] = (filled, field in spec["needed"])
    findings["unneeded_columns"] = sorted(c for c in columns if c not in spec["needed"])
    return rules, findings, len(rows), len(in_scope)


# 5. Scores, the MDG way: rule score = OK / (OK + Not OK); dimension = impact-weighted average.
def score(rules):
    dims = defaultdict(list)
    for dimension, impact, name, ok, bad in rules:
        if ok or bad:
            dims[dimension].append((impact, 100.0 * len(ok) / (len(ok) + len(bad))))
    result = {}
    for dimension, items in dims.items():
        weight = sum(i for i, _ in items)
        result[dimension] = sum(i * s for i, s in items) / weight if weight else None
    overall = statistics.mean(v for v in result.values() if v is not None) if result else 0.0
    return result, overall


def verdict(value, rules):
    """An average can hide a blocker, so any high-impact rule below target also means 'fix first'."""
    blockers = [name for _, impact, name, ok, bad in rules if impact == 3 and bad
                and 100 * len(ok) / (len(ok) + len(bad)) < TARGET]
    if value < WARNING:
        return "Not ready"
    if value < TARGET or blockers:
        return "Ready after fixes"
    return "Ready"


def write_report(results, source):
    lines = [f"# Data readiness report", "",
             f"Use case: {USE_CASE}  ", f"Data: {source}  ", f"Run: {TODAY.isoformat()}  ",
             f"Thresholds: target {TARGET:.0f}, warning {WARNING:.0f} (agreed with the business)", ""]
    lines += ["## Scores", "", "| Dataset | Records | In scope | " + " | ".join(DIMENSIONS) + " | Overall | Verdict |",
              "| --- | ---: | ---: | " + " | ".join("---:" for _ in DIMENSIONS) + " | ---: | --- |"]
    for kind, (rules, findings, total, scope, dims, overall) in results.items():
        cells = [f"{dims[d]:.0f}" if dims.get(d) is not None else "n/a" for d in DIMENSIONS]
        lines.append(f"| {kind} | {total} | {scope} | " + " | ".join(cells) + f" | {overall:.0f} | {verdict(overall, rules)} |")
    for kind, (rules, findings, total, scope, dims, overall) in results.items():
        lines += ["", f"## {kind.capitalize()}: what to fix", "", "| Rule | Dimension | Impact | Score | Failing records (first 5) |",
                  "| --- | --- | ---: | ---: | --- |"]
        for dimension, impact, name, ok, bad in sorted(rules, key=lambda x: (-x[1], len(x[3]) / max(1, len(x[3]) + len(x[4])))):
            if bad:
                lines.append(f"| {name} | {dimension} | {impact} | {100 * len(ok) / (len(ok) + len(bad)):.0f} | {', '.join(bad[:5])} |")
        notes = []
        if findings["missing_columns"]:
            notes.append(f"Columns the use case needs but the extract lacks: {', '.join(findings['missing_columns'])}.")
        for a, b, na, nb, ratio in findings["duplicates"][:5]:
            notes.append(f"Possible duplicate: {a} \"{na}\" and {b} \"{nb}\" (name similarity {ratio}).")
        for k, group, kg, z in findings["outliers"][:5]:
            notes.append(f"Unusual weight: {k} in group {group} weighs {kg} kg (modified z-score {z}).")
        if findings["out_of_scope"]:
            notes.append(f"Excluded as deleted or blocked: {len(findings['out_of_scope'])} "
                         f"({', '.join(f'{k} [{why}]' for k, why in findings['out_of_scope'][:5])}).")
        for field, (filled, needed) in findings["personal"].items():
            if filled:
                notes.append(f"Personal data: {field} is filled for {len(filled)} record(s) ({', '.join(filled[:5])}). "
                             + ("The use case needs it." if needed else "The use case does not need it: drop it from the extract."))
        if "IsNaturalPerson" in findings["personal"] and findings["personal"]["IsNaturalPerson"][0]:
            notes.append("Records flagged as natural persons: the name and address are personal data too. "
                         "Agree purpose, access and retention with your data protection officer.")
        if findings["unneeded_columns"]:
            notes.append(f"Columns the use case does not need (leave them out of the AI's view): "
                         f"{', '.join(findings['unneeded_columns'])}.")
        if notes:
            lines += [""] + [f"- {n}" for n in notes]
    lines += ["", "## Fill in by hand (profiling can't answer these)", "",
              "| Question | Answer |", "| --- | --- |",
              "| Source system and release (S/4HANA, ECC, BW, other) | |",
              "| Access path (released API, released CDS view, data product, extraction tool) | |",
              "| Business owner of each dataset | |",
              "| Who may see which records (authorizations to respect) | |",
              "| Personal data: purpose, retention and blocking rules | |",
              "| How fixes get back into SAP (MDG, mass change, owner) | |"]
    REPORT.write_text("\n".join(lines) + "\n", encoding="utf-8")


DIMENSIONS = ["Completeness", "Validity", "Consistency", "Uniqueness", "Accuracy", "Timeliness"]


def main():
    parser = argparse.ArgumentParser(description="Profile SAP-shaped master data for one AI use case.")
    parser.add_argument("--sample", action="store_true", help="write and profile made-up sample data")
    parser.add_argument("--sandbox", action="store_true", help="read products and suppliers from the SAP sandbox")
    parser.add_argument("--products", help="CSV with product master data")
    parser.add_argument("--suppliers", help="CSV with supplier master data")
    args = parser.parse_args()

    if args.sample:
        products, suppliers = make_sample()
        source = "made-up sample (readiness_data/)"
    elif args.sandbox:
        products, suppliers = fetch_sandbox()
        source = "SAP Business Accelerator Hub sandbox"
    elif args.products or args.suppliers:
        products, suppliers = args.products, args.suppliers
        source = ", ".join(p for p in (products, suppliers) if p)
    else:
        parser.print_help()
        sys.exit("\nChoose --sample, --sandbox, or give --products and/or --suppliers.")

    results = {}
    for kind, path in (("products", products), ("suppliers", suppliers)):
        if not path:
            continue
        rows = load(path)
        if not rows:
            print(f"{kind}: the file has no rows, nothing to profile.")
            continue
        rules, findings, total, scope = check_dataset(kind, rows)
        dims, overall = score(rules)
        results[kind] = (rules, findings, total, scope, dims, overall)
        print(f"\n{kind}: {total} records, {scope} in scope")
        for d in DIMENSIONS:
            if dims.get(d) is not None:
                print(f"  {d:<13}{dims[d]:6.1f}")
        print(f"  {'Overall':<13}{overall:6.1f}  -> {verdict(overall, rules)}")
    if results:
        write_report(results, source)
        print(f"\nReport written to {REPORT.name}")


if __name__ == "__main__":
    main()

Step 3: Run it on sample data

The same command on Windows and macOS/Linux:

python data_readiness.py --sample

What success looks like:


products: 61 records, 60 in scope
  Completeness   97.5
  Validity       98.9
  Consistency    98.1
  Uniqueness     96.7
  Accuracy       96.7
  Timeliness     96.7
  Overall        97.4  -> Ready

suppliers: 43 records, 41 in scope
  Completeness   96.5
  Validity       93.8
  Uniqueness     86.8
  Overall        92.3  -> Ready after fixes

Report written to readiness_report.md

The script also created a readiness_data folder with products.csv and suppliers.csv. Open them in Excel or VS Code to see what was profiled.

Step 4: Read the report

  1. In VS Code, open readiness_report.md.

  2. Right-click its tab and choose Open Preview to see the tables.

  3. Read it top to bottom:

    • Scores. Products are ready. Suppliers score 92 overall, above the target, and still get "Ready after fixes". The reason is in the next table.
    • Suppliers: what to fix. The top row is "Tax number used by one supplier only", a high-impact rule at 83. Six suppliers share tax numbers. Two pairs are near-duplicate names ("Nordwerk Pumpen GmbH" and "Nordwerk Pumpen G.m.b.H."). The third case is a different name with the same tax number: a rename, a branch or a typo.
    • The completeness row for tax numbers fails five suppliers. Four of them are in India (17300024, 17300029, 17300034, 17300039). A pattern like that points at one load or one process, not five separate mistakes.
    • Unusual weight. TG-1028 weighs 34,000 kg among pumps: grams typed into a kilogram field. TG-1024 is recorded in grams and passes, because the script converts units first. TG-1006 is flagged too, and it is probably just a small seal. A flag is a question.
    • Personal data. Supplier 17300033 is a natural person with a birth date. The use case doesn't need BirthDate, so the report says to drop it.
  4. Fill in the last table by hand for a real system you know, or leave it for the exercise.

Step 5: Change the use case, watch the verdict move

Readiness belongs to a use case. Make products matter more for logistics:

  1. In data_readiness.py, find "GrossWeight": 1} in the SPEC block (the last entry under "products").

  2. Change the 1 to 3, so it reads "GrossWeight": 3}, and save.

  3. Run again:

    python data_readiness.py --sample

What success looks like: the products line now ends with -> Ready after fixes. Weight completeness is 88%, and as a high-impact rule it now blocks. Nothing in the data changed. Set the value back to 1 afterwards.

Step 6: Profile your own extract (optional)

  1. Export product or supplier master data to CSV with column headers that use the API field names: for products Product, ProductDescription, ProductType, ProductGroup, BaseUnit, GrossWeight, NetWeight, WeightUnit, IsMarkedForDeletion, LastChangeDate (as YYYY-MM-DD); for suppliers Supplier, SupplierName, Country, CityName, PostalCode, TaxNumber1, VATRegistration, IsNaturalPerson, BirthDate, PurchasingIsBlocked, PostingIsBlocked, DeletionIndicator.

  2. Save the files in unit07.

  3. Run with either or both:

    python data_readiness.py --products my_products.csv --suppliers my_suppliers.csv

Missing columns aren't an error. They show up in the report as "Columns the use case needs but the extract lacks".

Step 7: Profile SAP's sandbox data (optional)

This reads up to 200 products and 200 suppliers from SAP's public sandbox through API_PRODUCT_SRV and API_BUSINESS_PARTNER.

  1. Check that .env in the course folder has your key on its own line: SAP_API_KEY="...". The setup topic shows where to get it.

  2. Run:

    python data_readiness.py --sandbox

What success looks like: two lines such as Saved 200 products to sandbox_products.csv, then the scores. The suppliers report will say the extract lacks Country, CityName and PostalCode. That is correct: A_Supplier has no address fields, because the address sits on A_BusinessPartnerAddress. Sandbox data is shared demo data, so expect odd values and changing results.

Step 8: Save your work in Git

From the course folder (one level up), the same on Windows and macOS/Linux:

cd ..
git add unit07/data_readiness.py unit07/readiness_report.md
git commit -m "Unit 7: data readiness report"

Leave readiness_data/ out; the script recreates the sample, and real extracts must never go into Git.

What each part of the script does

Part What it does
SPEC The readiness definition: which fields the use case needs, each with an impact from 1 to 3
TO_KG, TARGET, WARNING Unit conversions and the thresholds agreed with the business
norm_name Lowercases names and strips accents, dots, punctuation and legal forms before matching
make_sample Builds 61 products and 43 suppliers with planted problems, each marked with a comment
fetch_sandbox Optional: reads the SAP sandbox with your key and saves CSVs in the same shape
check_dataset Removes deleted and blocked records, then runs completeness, validity, consistency, timeliness, outlier and duplicate rules
rule (inside check_dataset) Applies one test per record and keeps OK and Not OK keys; None means the rule doesn't apply
score Rule score = OK / (OK + Not OK); dimension score = impact-weighted average, as in SAP MDG
verdict Ready, ready after fixes, or not ready; a failing high-impact rule blocks "Ready"
write_report Writes the one-page Markdown report, including the questions profiling can't answer

If something goes wrong

What you see What it means What to do
python: command not found or 'python' is not recognized Python isn't on the path, or the environment is off Turn on .venv (Step 1); on macOS/Linux try python3; see the Unit 1 setup topic
can't open file ... data_readiness.py The terminal is in the wrong folder Run cd unit07 from the course folder
Choose --sample, --sandbox, or give --products... No data source given Add --sample
File not found: ... The CSV isn't in unit07 Move it there, or give the full path
the key column Product is missing Your headers differ from the API names Rename the key column to Product or Supplier (Step 6)
--sandbox needs requests and python-dotenv A library is missing in this environment Turn on .venv, then pip install -r requirements.txt from the course folder
SAP_API_KEY is not set The script can't find your key Check .env is in the course folder with SAP_API_KEY="...", or use --sample
Sandbox returned HTTP 401 The key is wrong or expired Copy the key again from the SAP Business Accelerator Hub
Could not reach sandbox.api.sap.com Your network or a company proxy blocks the site Try another network, or ask IT to allow sandbox.api.sap.com; --sample still works
Every product flagged as a duplicate Your descriptions are generic, such as "Spare part" That is a real finding: descriptions too poor for search or AI

The SAP way

SAP Master Data Governance data quality

As of October 2026, SAP's MDG course for SAP S/4HANA describes data quality management as follows:

  • Rules carry a name, status, checked field and base table, plus business details and contacts such as the rule owner and data owner. The base table sets the level: on MARA you get one result per product, on MARC one per plant.
  • Rules are implemented in BRFplus as a scope expression (which records are in scope) and a condition expression (OK or Not OK). The scope expression plays the role of the script's "not applicable".
  • One rule can serve several usages: evaluating active data, checking change requests, and checking consolidation and mass processing. So the rule that measures readiness also stops new bad records.
  • Evaluations run all rules immediately or on a schedule, such as weekly. The Data Quality Overview app shows the latest score and trend. Evaluation Results lets you filter failing items, for example by plant.
  • Scores use the formula in this topic: rule score = OK / (OK + Not OK), impact-weighted dimension scores, and target, warning and critical thresholds.
  • Statuses move a rule from new through to be implemented, to be tested, approved and disabled. Approval needs active BRFplus expressions, and authorization objects control who may change status.

Rule mining

MDG's rule mining proposes rules from your existing data with machine learning. You define a mining run with a goal, the tables, a focus area (such as product type FERT), the fields to check and the fields for the condition, and a maximum number of rules (100 by default). The run proposes rules with a business view and an evaluation of how well each fits the data. People accept or reject each one, then link accepted proposals to data quality rules. SAP lists Product, Business Partner (including Customer and Supplier) and custom objects as supported.

This is the right division of labor for AI on master data: the machine proposes, a person accepts, and an approved rule does the checking.

Business Data Cloud, MDG and Reltio

SAP's May 2026 Business Data Cloud news says MDG is now a core part of SAP Business Data Cloud. The same article says the completed Reltio acquisition adds multi-domain master data management with AI-based entity resolution, which merges related records across SAP and non-SAP sources. It also says agents will rely on data products checked against business policies. Treat how these fit together in your landscape as a question for SAP, since packaging is new.

Access paths, the clean core way

  • Read released APIs from the SAP Business Accelerator Hub, such as API_BUSINESS_PARTNER and API_PRODUCT_SRV, used in this topic's sandbox step.
  • Inside S/4HANA, use released CDS views. The Released Objects view in ABAP Development Tools lists what is released.
  • For volume, use data products in SAP Business Data Cloud, covered in SAP Knowledge Graph and Business Data Cloud.
  • For extraction tools, confirm the method against SAP Note 3255746. Its summaries describe restrictions on ODP over RFC into non-SAP tools.

Build vs. SAP

Situation Use Why
First readiness check for one use case, days not months A profiling script like this one Fast, cheap, no license; answers "should we start?"
Client already runs MDG MDG data quality rules and evaluations Rules live with the data, have owners and also stop new defects
Rules unknown, lots of history MDG rule mining, then review The machine proposes, people accept
Duplicates across SAP and non-SAP systems SAP's master data management in Business Data Cloud (MDG, Reltio) or an MDM tool Matching at scale needs a matching engine and a golden record
AI reads live records Released APIs Stable across releases, respects authorizations
AI trains or analyzes at volume Data products Curated and documented; no table extracts

A practical sequence: script first to size the problem, then move the rules that matter into MDG so they keep running.

Production concerns

  • Authorizations. A readiness extract is often pulled with a broad technical user. The AI system must not inherit that view. Grounding should respect the user's authorizations, as in Grounding on SAP data with authorizations.
  • Personal data. Get the data protection officer's view on purpose before profiling people's records. Drop unneeded columns at extraction, not later. Never commit extracts to Git.
  • Scheduled re-checks. Run the rules weekly, as MDG evaluations do. Readiness that was true in the pilot decays as new records arrive.
  • Fix at the source. Clean data in a copy, and the copy and SAP drift apart. Fixes go back through MDG, mass change or the data owner.
  • Thresholds are business decisions. Write down who agreed to 90 and 75 and why, so the verdict can be defended.
  • Clean core. Read through released APIs and data products. Avoid custom table reads that break on upgrade.
  • Cost. Profiling is cheap. Cleaning is the cost: estimate hours per failing record type, not per record.

Pitfalls

  • Profiling all fields. You drown in findings that don't matter. Profile only the fields the use case reads.
  • Trusting the average. A score above target can hide a failing high-impact rule.
  • Counting deleted records. Blocked and deleted records distort scores and must not reach the AI.
  • Comparing values in mixed units. Convert first, then look for outliers.
  • Calling every flag an error. Outliers and near-duplicates are questions for a person.
  • Forgetting joins. In the Business Partner API, the supplier address lives on a separate entity. A missing join looks like missing data.
  • Fixing the copy. If the fix never reaches SAP, the next extract brings the problem back.

Exercise

Turn the report into a readiness decision for a real or realistic use case.

  1. Pick one use case from your own work or from Where AI creates value in S/4HANA, such as a blocked-order assistant or supplier search.
  2. Copy data_readiness.py to my_readiness.py. Change USE_CASE to your use case.
  3. In SPEC, keep only the fields your use case reads, and set an impact from 1 to 3 for each. Write one sentence per field explaining the impact.
  4. Add one rule of your own in check_dataset (see the Try it above for the pattern).
  5. Run it with --sample, or on an extract you are allowed to use.
  6. Fill in the "Fill in by hand" table in the report: source system, access path, owner, authorizations, personal data, and fix path.
  7. At the top of the report, add a three-line decision: verdict, the top two fixes and who owns them, and the date of the next check.
  8. Commit my_readiness.py and the report to Git.

Done when your report names one use case, shows scores for at least four dimensions, lists the top fixes with owners, has every "Fill in by hand" row answered, and states a verdict you could defend to a steering committee. You will reuse it when scoping AI engagements in Unit 14.

Check yourself

Pick one answer for each question. The explanation appears after you choose.

1Why does the script remove deleted and blocked records before scoring?

Answer: D. Scope comes first. Deleted and blocked records must not reach the assistant, so counting them would make the score describe data the use case never sees.

2A rule has 45 OK records and 5 Not OK records. What is its score, the MDG way?

Answer: A. SAP MDG defines rule score as OK / (OK + Not OK). 45 / 50 is 90%. Impact weights only apply when rule scores are averaged into a dimension score.

3Two products are named "Centrifugal pump DN20" and "Centrifugal pump DN40". Why doesn't the script flag them as duplicates?

Answer: B. Text similarity is high, but the numbers in the names differ. The script skips pairs whose numbers don't match, because a size or model number usually means a different product.

4Product TG-1024 has a gross weight of 25,000 with weight unit G. Why isn't it flagged as an outlier among pumps?

Answer: A. Outliers only make sense in one unit. The script converts each weight to kilograms with its weight unit, so 25,000 g becomes 25 kg, a normal pump weight. TG-1028, with 34,000 in a kilogram field, is flagged.

5You run --sandbox and the supplier report says Country, CityName and PostalCode are missing. What is the likely cause?

Answer: B. In the Business Partner API, the address belongs to the business partner and is a separate entity. An extract that doesn't join it looks like missing data, which is why checking the access path is part of readiness.

6Raising the GrossWeight impact from 1 to 3 changes the product verdict to "Ready after fixes". What does this show?

Answer: D. Nothing in the data changed. A logistics use case that depends on weights makes weight completeness a high-impact rule, and at 88% it blocks the verdict.

7A client runs MDG and wants readiness checks to keep running after the pilot. What do you recommend?

Answer: D. MDG rules can evaluate active data on a schedule and also check change requests and mass loads, so the same rule measures and prevents defects. The script is for sizing the problem first.

8A partner's pipeline copies S/4HANA tables into a non-SAP lake through an extraction tool. What do you check first for readiness?

Answer: C. Summaries of SAP Note 3255746 say SAP restricts ODP over RFC for SAP-to-non-SAP integration, with a blocking patch expected from June 2026. A pipeline on that method may stop working, so the access path is a readiness risk.

Sources

Sign in to track your progress

We'll email you a one-time sign-in link. No password needed.

or

Tell us a little about you

Optional, every field. It helps us pitch answers to your questions at the right level and decide which topics to write next. It is never shown publicly, and you can change or clear it anytime from the account menu.

SAP areas you work in