
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)| Column | Field |
|---|---|
| numberarea_m2step 0.1 |
| numberroomsstep 1 |
| categorydistrictoptions "center" · "north" |
| booleanhas_lift |
Code and raw output
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)[
{
"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"
}
]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.
- 1series
- 2onehot-category
- 3boolean
- 4category
- 5date
- 6number
- 7text
infer_schema(df)| Column | Field |
|---|---|
| 7textcustomer |
| 6numberagestep 1 |
| 4categorytieroptions "free" · "pro" |
| 2onehot-categoryplan
|
| 3booleanactive |
| 5datecreated |
| 1seriesreadings
|
Code and raw output
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)[
{
"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.
- 0plan__monthly
- 1plan__yearly
- 2age
| Column | Field |
|---|---|
| numberagemappedTo 2step 1 |
| onehot-categoryplan
|
Code and raw output
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)[
{
"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_schema(df)Columns without names get feature_0, feature_1… as labels and their positions as targets.
| Column | Field |
|---|---|
| numberfeature_0mappedTo 0step 0.1 |
| numberfeature_1mappedTo 1step 0.1 |
| booleanfeature_2mappedTo 2 |
Code and raw output
import pandas as pd
from mlschema import infer_schema
# A NumPy-style matrix: no column names, only input positions.
df = pd.DataFrame([[0.2, 1.4, True], [0.3, 1.1, False], [0.1, 1.7, True]])
schema = infer_schema(df)[
{
"label": "feature_0",
"required": true,
"mappedTo": 0,
"kind": "number",
"step": 0.1
},
{
"label": "feature_1",
"required": true,
"mappedTo": 1,
"kind": "number",
"step": 0.1
},
{
"label": "feature_2",
"required": true,
"mappedTo": 2,
"kind": "boolean"
}
]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.
| Column | Field |
|---|---|
| numberAgemappedTo "age"description "Customer age in years."min 0max 120step 1unit "years" |
| categoryPlanmappedTo "tier"defaultValue "pro"options "free" · "pro" |
Set through overrides; everything else is inferred.
Code and raw output
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)[
{
"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"]
}
]infer_schema(df, builders=[money_builder])Claim a column with an existing kind and add your own metadata.
| Column | Field |
|---|---|
| numberAmountmappedTo "amount_eur"min 0step 0.01unit "EUR" |
Code and raw output
import pandas as pd
from pandas import Series
from mlschema import FieldContext, infer_schema
def money_builder(_series: Series, ctx: FieldContext) -> dict | None:
if ctx.name != "amount_eur":
return None
return {
"kind": "number",
"label": "Amount",
"required": ctx.required,
"mappedTo": ctx.mappedTo,
"step": 0.01,
"unit": "EUR",
"min": 0,
}
df = pd.DataFrame({"amount_eur": [12.5, 80.0, 43.2]})
schema = infer_schema(df, builders=[money_builder])[
{
"label": "Amount",
"required": true,
"mappedTo": "amount_eur",
"kind": "number",
"min": 0.0,
"step": 0.01,
"unit": "EUR"
}
]infer_schema(df, kinds=[kind(model=DurationField, infer=duration_builder)])Register a new kind with its own Pydantic model.
| Column | Field |
|---|---|
| durationwaitunit "seconds"minSeconds 45maxSeconds 300 |
Code and raw output
from typing import Literal
import pandas as pd
from pandas import Series
from mlschema import BaseField, FieldContext, infer_schema, kind
class DurationField(BaseField):
kind: Literal["duration"] = "duration"
unit: Literal["seconds"] = "seconds"
minSeconds: int
maxSeconds: int
def duration_builder(series: Series, ctx: FieldContext) -> dict | None:
if not ctx.dtype.startswith("timedelta64"):
return None
return {
"kind": "duration",
"label": ctx.name,
"required": ctx.required,
"mappedTo": ctx.mappedTo,
"minSeconds": int(series.min().total_seconds()),
"maxSeconds": int(series.max().total_seconds()),
}
df = pd.DataFrame({"wait": pd.to_timedelta([90, 45, 300], unit="s")})
schema = infer_schema(df, kinds=[kind(model=DurationField, infer=duration_builder)])[
{
"label": "wait",
"required": true,
"mappedTo": "wait",
"kind": "duration",
"unit": "seconds",
"minSeconds": 45,
"maxSeconds": 300
}
]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.
Rendered by MLform
Loading MLform…
modelValues
{}Try it on your own DataFrame.
The playground runs MLschema in your browser, with nothing to install.