Indu Learning Platform
Course/Unit 4 · Data engineering for master data
Lesson 4-1

Python and SQL for data profiling

Profile a business partner or supplier extract in minutes with pandas and SQL, instead of days of Excel pivots.

Watch firstPython Pandas Tutorial (Part 1): Getting Started with Data Analysis - Installation and Loading Data · Corey SchaferOpen on YouTube ↗

Why profiling moves from Excel to code

Excel profiling with COUNTIF and pivots breaks beyond a few hundred thousand rows, is hard to repeat for the next mock load, and nobody can see how a number was produced. A 30-line Python script runs identically on mock 1, mock 2 and cutover, and an AI assistant can read its output and propose the next rules.

ExtractProfileAgree rulesCleanseRe-profile
Repeat each mock cycle until the error rate is below the agreed go-live threshold.

Load the extract without damaging it

If pandas guesses types, supplier 0000100045 becomes 100045 and PIN codes lose leading zeros. Read everything as text with dtype=str, and say explicitly which placeholders (#, N/A, a single space) mean missing, so fill rates are honest. Then print the shape: 119 columns instead of 120 means a delimiter inside a name shifted everything.

Python
import pandas as pd

df = pd.read_csv(
    "bp_extract.csv",
    dtype=str,                      # keep leading zeros and codes as text
    keep_default_na=False,          # do not guess what is missing...
    na_values=["", " ", "#", "N/A", "NA", ".", "-"],  # ...say it explicitly
    encoding="utf-8",
)
df = df.apply(lambda col: col.str.strip())   # trim whitespace

print(df.shape)          # (rows, columns)
print(df.head(3).T)      # transposed: easier to read wide SAP extracts

The column profile in one table

Four numbers per column: fill rate, distinct count, top value and its share. A REGION 99% filled but 60% one value was probably defaulted. A TAX_NUMBER with fewer distinct values than filled rows means duplicates or dummy IDs. Split by country or source system too: that is where you find the one system causing most problems.

NAME_ORG1100%COUNTRY99.6%CITY197.2%POST_CODE191.4%REGION78%TAX_NUMBER (GSTIN)64.3%SMTP_ADDR41.8%
Fill rates on a masked 48,000-row supplier extract.
Python
profile = pd.DataFrame({
    "filled_%": df.notna().mean().mul(100).round(1),
    "distinct": df.nunique(dropna=True),
    "top_value": df.mode(dropna=True).iloc[0],
})
print(profile.sort_values("filled_%"))

# Fill rate of key fields per country
key_cols = ["POST_CODE1", "REGION", "TAX_NUMBER", "SMTP_ADDR"]
print(df.groupby("COUNTRY")[key_cols].agg(lambda s: s.notna().mean()).round(2))

Try it: complete the profiling script

Try it · Fill the blanks

Pick the pandas call for each blank.

df = pd.read_csv("bp.csv", , keep_default_na=False)
fill_rate = df.().mean()
distinct = df.()
pin_ok = df["POST_CODE1"].str.(r"[1-9]\d{5}", na=False)

Pattern checks: filled is not valid

Fill rate says a value exists, not that it is right. Write one tested regex per critical field: an Indian PIN is six digits not starting with 0; a GSTIN is 15 characters (state code, PAN block, entity digit, Z, check character). Then add cross-field rules, such as GSTIN state code against address region. Example values here are synthetic.

CheckFilledInvalidExample failurePIN code (IN)38,9121,20440005 (5 digits)GSTIN shape26,3772,11827ABCDE1234F1E-mail shape20,064612accounts@acmeGSTIN state vs region24,25988729… on a Pune address
Pattern profile on the same extract. Filled fields can still be wrong.

Try it: which check catches it?

Try it · Sort

Sort each finding into the profiling check that reveals it.

Fill rate
Distinct / top value
Pattern check
Cross-field rule
Click an item, then a bucket (or drag it).

Walk through a profiling run

Step through · 1 of 6
Load as textdtype=strNormalise missingna_valuesColumn profilenotna / nuniquePattern checksstr.fullmatchCross-field rulesGSTIN vs regionRule list + ownersCSV / Excel
Read the extract with every column as text so keys, PIN codes and supplier numbers keep their leading zeros.
Click Next to step through one mock-cycle run.

From numbers to cleansing rules

Each finding gets a stable rule ID, an owner and a fix route, so you can report trends across mocks. When data is already in a staging database, the volume metrics are a few lines of SQL: COUNT(post_code1) / COUNT(*) is a fill rate, because COUNT(column) skips NULLs. Use the same rule IDs in SQL and Python.

Profile findingRule and fix routePIN not 6 digitsGSTIN wrong shapeGSTIN state ≠ regionREGION 60% one valueE-mail missingDQ-01 auto-fix in transformDQ-02 business mass correctionDQ-03 steward reviewDQ-04 confirm default withownerDQ-05 accept, enrich aftergo-livepad / rejectnon-blocking
Finding → rule ID → fix route → owner.

Try it: the suspicious fill rate

Try it · What would you do?

Your script says POST_CODE1 is 99.8% filled, but stewards insist hundreds of suppliers have no PIN code. What is the most likely cause?

Remember

  • Read SAP extracts as text and define 'missing' explicitly before measuring anything.
  • Fill rate, distinct count and top-value share expose gaps, defaults and duplicates; patterns and cross-field rules expose invalid values.
  • A repeatable profiling script with stable rule IDs turns every mock cycle into a measurable trend.

Check yourself

Why read an SAP extract with dtype=str in pandas?
What does df.notna().mean() return?
A TAX_NUMBER column is 95% filled but has far fewer distinct values than filled rows. What is the most likely issue?
In SQL, why does COUNT(post_code1) work as a fill-rate numerator?