A churn table contains nine columns. The modeling team wants to know which columns carry their own information — and which are derived, duplicated, or concentrated. The profiler measures which columns are derivable from others, how their values are distributed, and which columns predict the target. The modeling team can use these results to review candidate features.
import random
import polars as pl
import onceover
rng = random.Random(77)
n = 5_000
tenure = [rng.randrange(0, 60) for _ in range(n)]
tenure_months = [round(t / 12, 2) for t in tenure] # rounded copy
x1 = [rng.uniform(-1, 1) for _ in range(n)]
x2 = [rng.uniform(-1, 1) for _ in range(n)]
score = [round(a * b + rng.uniform(-0.05, 0.05), 4) for a, b in zip(x1, x2)] # interaction
channel = ["web" if rng.random() < 0.98 else "app" for _ in range(n)] # concentrated
segment = [rng.choice(["retail", "b2b"]) for _ in range(n)]
plan = [["starter", "scale"][s == "b2b"] if rng.random() < 0.97
else ["starter", "scale"][1 - (s == "b2b")]
for s in segment] # 3% violations
# new customers churn: tenure under a year, with 5% label noise
churn = [(rng.random() < 0.95) == (t < 12) for t in tenure]
df = pl.DataFrame({
"tenure": tenure,
"tenure_months": tenure_months,
"x1": x1,
"x2": x2,
"score": score,
"channel": channel,
"segment": segment,
"plan": plan,
"churn": churn,
})
report = onceover.profile(df)
print(report.overview["headline"])5000 rows × 9 columns (standard budget); key candidate: x1; 5 functional dependencies, 11 predictive dependencies, 8 conditional rules
The headline reports functional dependencies, predictable columns, and
rules. Its key candidate: x1 needs context: x1 is a random float,
unique and non-null in all 5,000 rows. It meets the observed key criteria
but does not represent an entity identifier.
For feature trimming, inspect dependencies for derived columns, concentration for columns with little variation, and predictive statements for candidate inputs.
print("— dependencies")
for f in report.findings:
if f["category"] == "dependencies":
print(f" [{f['type']}] {f['claim']}")
print()
print("— concentration and roles")
for f in report.findings:
if f["category"] in {"concentration", "column_roles"}:
print(f" {f['claim']}")
print()
print("— rules")
for f in report.findings:
if f["category"] == "conditional_rules":
print(f" {f['claim']}")— dependencies
[functional] score, segment → channel holds on 99.64% of rows (18 violations in 5000 rows)
[functional] plan, score → segment holds on 99.46% of rows (27 violations in 5000 rows)
[functional] segment, score → plan holds on 99.22% of rows (39 violations in 5000 rows)
[predictive] score depends on x2 and x1 jointly: 99.7% of the pair's joint partial-dependence variance is interaction (H² = 1.00); the interaction variance is 0.113265 against a target variance of 0.11732 (96.5%), from a surrogate with holdout R² 0.98 against 0.00 for an additive refit
[predictive] score follows 8 local linear regimes (first split x2 ≤ 0.611857; holdout R² 0.910 versus 0.002 for one linear model)
[predictive] segment is predictable from [plan] (holdout accuracy 0.97 vs 0.51 majority baseline)
[predictive] plan is predictable from [segment] (holdout accuracy 0.97 vs 0.52 majority baseline)
[predictive] tenure_months is predictable from [tenure, x1] (holdout R² = 1.00)
[predictive] tenure is predictable from [tenure_months, x2, score] (holdout R² = 1.00)
[predictive] score is predictable from [tenure, x1, x2, channel, segment, churn] (holdout R² = 0.98)
[predictive] x2 is predictable from [tenure, x1, score, segment, plan, churn] (holdout R² = 0.88)
[predictive] x1 is predictable from [tenure, x2, score, segment] (holdout R² = 0.83)
[predictive] churn is predictable from [tenure, x2, score] (holdout accuracy 0.95 vs 0.77 majority baseline)
[functional] tenure → tenure_months (tenure_months is a function of tenure; 0 violations in 5000 rows)
[functional] tenure_months → tenure (tenure is a function of tenure_months; 0 violations in 5000 rows)
[predictive] churn is predicted from [tenure] with 0.896 cross-validated accuracy (majority baseline 0.769)
— concentration and roles
column channel is highly concentrated: 98.1% 'web' (effective cardinality 1.10)
numeric column tenure behaves like a category (60 distinct values)
numeric column tenure_months behaves like a category (60 distinct values)
column x1 looks like a unique identifier (5000 distinct of 5000 values)
column x2 looks like a unique identifier (5000 distinct of 5000 values)
— rules
When tenure > 1 and tenure <= 3, churn = true holds in 30 of 31 matching diagnostic rows (96.77%; baseline 24.40%)
When tenure > 9 and tenure <= 11, churn = true holds in 38 of 41 matching diagnostic rows (92.68%; baseline 24.40%)
When tenure > 24 and tenure <= 26, churn = false holds in 35 of 35 matching diagnostic rows (100.00%; baseline 75.60%)
When tenure > 29 and tenure <= 31, churn = false holds in 34 of 35 matching diagnostic rows (97.14%; baseline 75.60%)
When plan = "scale", segment = "b2b" holds in 484 of 492 matching diagnostic rows (98.37%; baseline 50.00%)
When segment = "retail", plan = "starter" holds in 496 of 504 matching diagnostic rows (98.41%; baseline 51.19%)
When segment = "b2b", plan = "scale" holds in 484 of 504 matching diagnostic rows (96.03%; baseline 48.81%)
When plan = "starter", segment = "retail" holds in 496 of 516 matching diagnostic rows (96.12%; baseline 50.00%)
The trimming checklist¶
Redundant measurements. tenure and tenure_months are exact
functions of each other: 0 violations in both directions. Because
tenure_months is tenure / 12 rounded to two decimals, every observed
integer month count maps to one year value and back, so the profiler’s
partition test finds a bijection rather than fitting a slope. These two columns contain the same observed information. Keeping both
can divide feature importance between equivalent inputs. Decision: drop tenure_months, keep tenure.
Check what a unique float represents. x1 and x2 both surface as
identifier-role columns, and x1 wins the headline’s key ranking. In a
feature table that is a warning: the column is unique because it is
continuous, not because it identifies an entity. Keep it as a feature;
do not join on it.
Interactions are not redundancy. score is predictable from the rest
of the table at holdout R² 0.98, and the model’s gain is almost entirely
on x2 and x1 (gain shares 0.63 and 0.37, respectively; every other input is below
0.001). The piecewise model makes the same point from another angle:
score follows 8 local linear regimes with holdout R² 0.910, against
0.002 for one global linear model. The low global linear R² misses the interaction used to construct
score. Keep x1 and x2 as candidate inputs and compare models with
an explicit product feature.
Review concentrated columns. channel is 98.1% one
value — effective cardinality 1.10. Most rows therefore have the same encoded value. Exclude it in this
fixture, but check the minority rows and predictive performance before
dropping a similarly concentrated column in a real dataset.
Inspect approximate mappings. The rules from segment to plan
hold in 96–98% of matching rows depending on the segment (b2b → scale 96.03%,
retail → starter 98.41%); the profiler also lists segment, score → plan
at 99.22% with 39 violations. The additional condition needs scrutiny: score is a near-unique float, so putting it in the antecedent
can leave few repeated input combinations. Check support and violations before treating these mappings as
constraints or using them to remove features.
The target is predictable. The model predicts churn mainly from
tenure; x2 and score each contribute less than 0.001 of model gain.
Holdout accuracy is 0.95 against a 0.77 majority baseline. A decision
table on tenure alone reaches cross-validated accuracy 0.896 against
0.769. These results support evaluating tenure as a predictor.
target = [f for f in report.findings
if f["category"] == "dependencies" and f.get("type") == "predictive"
and f["subject"].get("target") == "churn"]
for f in target:
e = f["evidence"][0]
m = e["metrics"]
value = m.get("metric", m.get("cv_accuracy"))
print(f["claim"])
print(" inputs:", f["subject"]["inputs"],
"| score:", round(value, 3),
"| baseline:", round(m["baseline"], 3))churn is predictable from [tenure, x2, score] (holdout accuracy 0.95 vs 0.77 majority baseline)
inputs: ['score', 'tenure', 'x2'] | score: 0.951 | baseline: 0.767
churn is predicted from [tenure] with 0.896 cross-validated accuracy (majority baseline 0.769)
inputs: ['tenure'] | score: 0.896 | baseline: 0.769
The control: shuffle the target¶
As a negative control, shuffle churn and profile the table again. The
predictive findings about churn should disappear. If they remain,
investigate possible leakage in the analysis.
permuted = df.with_columns(pl.col("churn").shuffle(seed=77))
permuted_report = onceover.profile(permuted)
kept = [f for f in permuted_report.findings
if f["category"] == "dependencies" and f.get("type") == "predictive"
and f["subject"].get("target") == "churn"]
print("predictive churn findings after shuffling:", len(kept))predictive churn findings after shuffling: 0
No predictive finding about the shuffled target remains. This control checks that the screen does not report predictability after the planted target relationship has been removed.
Summary¶
| Statement | Evidence class | Trimming decision |
|---|---|---|
tenure and tenure_months are exact functions of each other (0 violations) | measured · full | drop the rounded copy |
score predictable from the rest (holdout R² 0.98; 8 regimes, 0.910 vs 0.002 linear) | estimated · holdout | keep x1, x2; consider an explicit product |
channel 98.1% one value | measured · full | exclude from one-hot |
segment → plan 96–98%; segment, score → plan 99.22% | measured · diagnostic partition | inspect mapping violations and support |
churn from tenure (0.896 CV; holdout 0.95 vs 0.77) | estimated · holdout | evaluate tenure as a predictor |
The groups view links findings that share columns:
for g in report.groups:
print(g["id"], "—", len(g["columns"]), "columns,", len(g["finding_ids"]), "findings")
print(" columns:", ", ".join(g["columns"]))
print(" ", g["description"])group-1 — 9 columns, 27 findings
columns: channel, churn, plan, score, segment, tenure, tenure_months, x1, x2
Connected findings; inspect individual statements and directions before inferring redundancy.
The whole table is one connected component: the exact copy joins tenure
to tenure_months, the predictive statements join score to x1 and
x2, and other shared-column links connect churn and channel.
One derived column can connect otherwise separate sets of findings
into a single component. Use groups to navigate shared columns; assess
redundancy from the individual findings.