Typed inputs, straight from your data.

Give infer_schema the DataFrame your model learned from. MLschema returns one validated field per feature, typed and pointing at the input it feeds, ready for MLform or any frontend to render.

infer_schema(df)
ColumnField
  • area_m2float6474.5120.056.2
numberarea_m2step 0.1
  • roomsint64352
numberroomsstep 1
  • districtcategorycenternorthcenter
categorydistrictoptions "center" · "north"
  • has_liftboolTrueFalseTrue
booleanhas_lift
Code and raw output
hero.py
import pandas as pd

from mlschema import infer_schema

# The features a house-price model was trained on.
df = pd.DataFrame(
    {
        "area_m2": [74.5, 120.0, 56.2],
        "rooms": [3, 5, 2],
        "district": pd.Categorical(["center", "north", "center"]),
        "has_lift": [True, False, True],
    }
)

schema = infer_schema(df)
schema (JSON)
[
  {
    "label": "area_m2",
    "required": true,
    "mappedTo": "area_m2",
    "kind": "number",
    "step": 0.1
  },
  {
    "label": "rooms",
    "required": true,
    "mappedTo": "rooms",
    "kind": "number",
    "step": 1.0
  },
  {
    "label": "district",
    "required": true,
    "mappedTo": "district",
    "kind": "category",
    "options": ["center", "north"]
  },
  {
    "label": "has_lift",
    "required": true,
    "mappedTo": "has_lift",
    "kind": "boolean"
  }
]
Real output of this example. See the fields rendered by MLform

7 kinds, tried in order.

Each column goes to the first kind that claims it. A column of pairs becomes a series before anything else looks at it, 0/1 columns sharing a prefix become one onehot-category, and object columns nobody claims fall back to text.

  1. 1series
  2. 2onehot-category
  3. 3boolean
  4. 4category
  5. 5date
  6. 6number
  7. 7text
infer_schema(df)
ColumnField
  • customerstrAdaLinusGrace
7textcustomer
  • ageint64425538
6numberagestep 1
  • tiercategoryprofreepro
4categorytieroptions "free" · "pro"
  • plan__monthlyint64100
  • plan__yearlyint64011
2onehot-categoryplan
  • monthlymappedTo "plan__monthly"
  • yearlymappedTo "plan__yearly"
  • activeboolTrueFalseTrue
3booleanactive
  • createddatetime64[us]2024-01-012024-01-02
5datecreated
  • readingsobject(2024-01-01, 23.5)(2024-01-02, 24.1)
1seriesreadings
  • datefield1
  • numberfield2step 0.1
Code and raw output
kinds.py
import pandas as pd

from mlschema import infer_schema

df = pd.DataFrame(
    {
        "customer": ["Ada", "Linus", "Grace"],
        "age": [42, 55, 38],
        "tier": pd.Categorical(["pro", "free", "pro"], categories=["free", "pro"]),
        "plan__monthly": [1, 0, 0],
        "plan__yearly": [0, 1, 1],
        "active": [True, False, True],
        "created": pd.date_range("2024-01-01", periods=3),
        "readings": [
            (pd.Timestamp("2024-01-01"), 23.5),
            (pd.Timestamp("2024-01-02"), 24.1),
            (pd.Timestamp("2024-01-03"), 22.8),
        ],
    }
)

schema = infer_schema(df)
schema (JSON)
[
  {
    "label": "customer",
    "required": true,
    "mappedTo": "customer",
    "kind": "text"
  },
  {
    "label": "age",
    "required": true,
    "mappedTo": "age",
    "kind": "number",
    "step": 1.0
  },
  {
    "label": "tier",
    "required": true,
    "mappedTo": "tier",
    "kind": "category",
    "options": ["free", "pro"]
  },
  {
    "label": "plan",
    "required": true,
    "kind": "onehot-category",
    "options": [
      {
        "label": "monthly",
        "value": "monthly",
        "mappedTo": "plan__monthly"
      },
      {
        "label": "yearly",
        "value": "yearly",
        "mappedTo": "plan__yearly"
      }
    ]
  },
  {
    "label": "active",
    "required": true,
    "mappedTo": "active",
    "kind": "boolean"
  },
  {
    "label": "created",
    "required": true,
    "mappedTo": "created",
    "kind": "date"
  },
  {
    "label": "readings",
    "required": true,
    "mappedTo": "readings",
    "kind": "series",
    "columns": [
      {
        "label": "field1",
        "required": true,
        "kind": "date",
        "id": "field1"
      },
      {
        "label": "field2",
        "required": true,
        "kind": "number",
        "step": 0.1,
        "id": "field2"
      }
    ]
  }
]

Every value knows where it goes.

Labels are for people; mappedTo is for your model. By default it is the column name, and renaming a label never moves it.

When the model reads one array instead, with_positional_mappings swaps names for the positions it was trained with, so every value lands in its slot.

with_positional_mappings(infer_schema(df), feature_names)

feature_names is the order the model was trained with. Unknown or duplicate names raise ValueError.

feature_names
  1. 0plan__monthly
  2. 1plan__yearly
  3. 2age
ColumnField
  • model input 2ageint64425538
numberagemappedTo 2step 1
  • model input 0plan__monthlyint64100
  • model input 1plan__yearlyint64011
onehot-categoryplan
  • monthlymappedTo 0
  • yearlymappedTo 1
Code and raw output
model_order.py
import pandas as pd

from mlschema import infer_schema, with_positional_mappings

df = pd.DataFrame(
    {
        "age": [42, 55, 38],
        "plan__monthly": [1, 0, 0],
        "plan__yearly": [0, 1, 1],
    }
)

# The model reads one array, in the feature order it was trained with.
feature_names = ["plan__monthly", "plan__yearly", "age"]

schema = with_positional_mappings(infer_schema(df), feature_names)
schema (JSON)
[
  {
    "label": "age",
    "required": true,
    "mappedTo": 2,
    "kind": "number",
    "step": 1.0
  },
  {
    "label": "plan",
    "required": true,
    "kind": "onehot-category",
    "options": [
      {
        "label": "monthly",
        "value": "monthly",
        "mappedTo": 0
      },
      {
        "label": "yearly",
        "value": "yearly",
        "mappedTo": 1
      }
    ]
  }
]

Infer the baseline. Refine the rest.

Keep what the data already says and add only what is product-specific. Your builders run first, then custom kinds, then the builtins; the first one to return a field owns the column.

infer_schema(df, overrides=overrides)

Patch labels, bounds, defaults or units after inference.

ColumnField
  • ageint64425538
numberAgemappedTo "age"description "Customer age in years."min 0max 120step 1unit "years"
  • tiercategoryprofreepro
categoryPlanmappedTo "tier"defaultValue "pro"options "free" · "pro"

Set through overrides; everything else is inferred.

Code and raw output
overrides.py
import pandas as pd

from mlschema import infer_schema

df = pd.DataFrame(
    {
        "age": [42, 55, 38],
        "tier": pd.Categorical(["pro", "free", "pro"], categories=["free", "pro"]),
    }
)

overrides = {
    "age": {
        "label": "Age",
        "description": "Customer age in years.",
        "min": 0,
        "max": 120,
        "unit": "years",
    },
    "tier": {"label": "Plan", "defaultValue": "pro"},
}

schema = infer_schema(df, overrides=overrides)
schema (JSON)
[
  {
    "label": "Age",
    "description": "Customer age in years.",
    "required": true,
    "mappedTo": "age",
    "kind": "number",
    "min": 0.0,
    "max": 120.0,
    "step": 1.0,
    "unit": "years"
  },
  {
    "label": "Plan",
    "required": true,
    "mappedTo": "tier",
    "defaultValue": "pro",
    "kind": "category",
    "options": ["free", "pro"]
  }
]

Every field is validated before infer_schema returns. A broken contract raises an exception instead of shipping a half-valid schema.Errors reference

Python owns the contract. The frontend renders it.

MLschema stops at inference and validation; the field list is plain JSON your API can serve. Here MLform renders the fields from the top of this page and assembles the values your model receives, keyed by mappedTo.

Use MLschema with MLform

Rendered by MLform

Loading MLform…

modelValues

{}
Live MLform components in this page. Edit a value and the payload follows; nothing leaves your browser.
  1. PackageMLbundle
  2. DescribeMLschema
  3. RenderMLform
  4. Run & reviewMLsuite

Try it on your own DataFrame.

The playground runs MLschema in your browser, with nothing to install.