Generated banner for Matlab Engineer Tabular Features
Generated logo for Matlab Engineer Tabular Features

Matlab Engineer Tabular Features

MathWorksOfficialCreated by MathWorks

  • Skill
  • Workflow
  • Data and AI
  • BSD-2-Clause
  • No issues found

From the author

Use when engineering or selecting the best features for single-response classification or regression in MATLAB, whatever the data's modality — for non-tabular data it routes extraction to a domain skill, then selects, assesses, and delivers on the resulting table. Not for multi-response problems, model training, or raw data acquisition.
— the description by MathWorks

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a rigorous, data-driven workflow for feature engineering and selection in MATLAB. It manages the entire process from initial data intake and screening to feature pool generation, consensus-based selection, and diagnostic assessment. Designed for single-response supervised learning, it handles both tabular and non-tabular data by routing complex extractions to domain-specific skills. The workflow emphasizes train-test discipline, ensuring that feature generation and selection are performed on training data while maintaining a held-out set for unbiased evaluation. It concludes by delivering a self-contained inference script that allows users to reproduce the selected feature set on new data.

What it does

  • Screens raw data to remove degenerate or constant predictors.
  • Generates a candidate feature pool using either SMLT or domain-specific extractors.
  • Performs consensus-based feature selection using a ranker panel and elbow-cut logic.
  • Assesses feature set quality and stability through performance metrics and resampling.
  • Produces a standalone MATLAB inference script for applying the selected features to new data.
  • Generates comprehensive reports on the selection process and model-free feature quality.

When to use it

  • When you need to engineer and select the best predictors for a single-response classification or regression task.
  • When you require a structured, reproducible selection process rather than ad-hoc feature picking.
  • When working with non-tabular data that requires specialized extraction before feature selection.
  • When you need a reliable, self-contained script to apply your feature engineering pipeline to future datasets.

Advantages

  • Enforces strict train-test discipline to prevent data leakage.
  • Provides a consensus-driven approach to feature selection that reduces bias.
  • Offers diagnostic tools to evaluate feature set stability and predictive performance.
  • Delivers a portable inference script that encapsulates the entire transformation logic.

Usage examples

“I have a table of sensor data and want to select the best features for a regression model.”
The agent will initiate the intake phase, confirm your output directory, and guide you through the feature engineering and selection workflow.
“Can you help me select features for my classification task and check if they are stable?”
The agent will perform the selection and then offer to run stability gates to see if the same features are chosen across resampled data.

Install

Documentation by MathWorks

Engineer Tabular Features

A lean, functional pipeline: intake → feature pool → select → assess → deliver → report. There is no shared context object — each phase is a direct call to leaf utilities in scripts/, and the reference file for each phase carries the detail. Your value is the structured, data-driven process and, above all, the consensus selection at its center — not an ad-hoc answer.

This skill bundles the workflow in references/ (per-phase detail read on demand) and scripts/ (the computation and plotting utilities). Do not invoke files in references/ as separate skills — they are loaded only via the Read tool when the phase that needs them runs. The per-phase files call the pipeline's leaf helpers for you; if you ever need a helper's signature, references/internal-helpers.md documents each one's inputs, outputs, and an example call — so you never open a helper's source.

When to Use

  • Engineering or selecting the best predictors for single-response supervised classification or regression on a plain in-memory table.

  • You want a structured, data-driven selection — a ranker panel, a consensus vote, and an elbow cut — rather than an ad-hoc hand-picked feature set.

  • The data is non-tabular (signals, images, battery/machinery telemetry): this skill routes extraction to the matching domain skill, then engineers, selects, assesses, and delivers on the resulting table (see references/domain-routing.md).

When NOT to Use

  • Multi-response problems — this skill is single-response only.

  • Model training, tuning, or deployment — it prepares features and stops. Hand the delivered table to a model-training/classification workflow to fit and compare models.

  • Raw data acquisition. And for the extraction step on non-tabular data, the actual feature computation belongs to the matching domain extraction skill — this skill orchestrates that handoff (see references/domain-routing.md), it does not re-implement it.

Requires the Statistics and Machine Learning Toolbox (SMLT) — gencfeatures/genrfeatures build the pool and the ranker/assessment utilities are SMLT-based. MATLAB Report Generator is optional (enables the PDF report; markdown is always produced).

Running MATLAB

Run all MATLAB through the MATLAB MCP server (mcp__matlab__evaluate_matlab_code, or mcp__matlab__run_matlab_file for scripts). Set project_path to this skill's scripts/ directory so the utilities resolve on the current working folder without any addpath calls. Every utility is a leaf function called directly — there is no initialization step and no context object to construct. Validate any code you author with mcp__matlab__check_matlab_code before running it.

Start each dataset from scratch — but use the live workspace within a run. The MCP session is stateful, so a run's intermediates should live in the workspace: set RawTbl, Splits, FullEng, SelectedNames, Baseline, etc. once and pass them phase-to-phase. Do not round-trip them through save/load .mat files (noise, risks stale reads) and do not addpath. Across different datasets/runs, carry nothing — begin each analysis by setting every variable afresh.

Communication style while running this skill

Talk to the user about their data and results, not the skill's plumbing. Everything under references/ and scripts/ is internal. Rule of thumb: if a sentence would only make sense to someone who has read this skill's source files, don't say it.

  • Never name internal files, helpers, or phase/gating mechanics. runConsensusSelection, GenInfo.BinaryReliant, "the redundancy dimension", etc. are internal — give the outcome ("these features duplicate each other, so I'm keeping the strongest"), not the mechanism. Name an internal only when it is a problem the user can act on. Read reference files silently.

  • Use plain words for each check. The three assessment reads: performance → whether the new features improve predictions (a held-out estimate, or a cross-validated mean ± std); fixed-pool stability → whether the same features get picked when rows are resampled; generation stability → whether the same features get built and picked when the whole pipeline re-runs on resampled rows. Say "the ranking step" not "the borda voter"; "reliably re-selected" not "consensus core".

  • Don't narrate uncertainty or mid-flight course-corrections — settle how a function is called silently, then report only the outcome. Surface a difficulty only when the user must decide on it.

  • Announce cost before long work, one sentence — pool size before selection, expected time before a K-fold, and before either stability gate (both re-run selection many times; the generation gate also re-builds the pool each time). And surface user-facing questions verbatim where a phase specifies one (output directory, wide-input, domain routing).

  • Report what was dropped at every phase (screened predictors, excluded WoE columns, skipped rankers) — a silent shrink reads as data loss. But selection evaluates the pool, it doesn't necessarily shrink it — never call it a reduction.

Output directory — REQUIRED, HARD HALT

Deliverables are written to disk. Always confirm the output directory with the user before writing anything. Do not assume the working directory, do not create one silently.


The pipeline

Follow the phases in order. Each links to its reference; read the reference before executing the phase.

1. Intake — references/intake.md

Ask before running any code. Intake is a required conversation, not a default-fill. Confirm every run parameter with the user before proceeding past the screen — data source, response, dataset name, output directory (hard-halt), domain description, separate-test-set, model family (+ lens if agnostic), evaluation strategy, report opt-out — asked one at a time, in the order pinned in intake.md §1 (never dump the whole list in one message). Every item must be asked; offer a default where one exists, but confirm rather than assume — when the opening request implies an answer, state what you inferred and have the user confirm it. Two are non-negotiable — do not proceed without an explicit answer:

  • Output directory — the disk-write hard-halt (see above).

  • Domain description — the sole input to domain routing. Ask what the data is and where it came from; route to a domain extractor if one fits, else the generic path. Never infer the domain from column names or fall through to generic generation on silence. Verbatim prompt in intake.md.

Then assemble the data into one plain table, briefly confirm what was loaded (shape, response, problem type), and screen degenerate predictors. A timetable/tall/gpuArray/datastore isn't a dead end — it's a signal to run a tabularizing step first (often a domain skill, see domain-routing.md) and then re-enter intake with the resulting table; only halt if no tabular path exists.

[ScreenedTbl, ScreenInfo] = screenPredictors(RawTbl, Response);   % or (X, y)

screenPredictors handles the polymorphic response (a name already in the table, or a separately-supplied vector/table it concatenates) and drops constant and near-empty predictors. Then profile, reserve the user's untouched slice, and split:

Profile = profileForSplit(ScreenedTbl, ScreenInfo.ResponseVar);
[WorkingIdx, UserHeldOutIdx, ReserveInfo] = reserveHoldoutForUser( ...
    ScreenedTbl, Profile.ProblemType, ScreenInfo.ResponseVar, ReserveForUser = HasNoSeparateTest);
[Splits, SplitDecision] = splitStrategy(ScreenedTbl, Profile.ProblemType, ...
    ScreenInfo.ResponseVar, Subset = WorkingIdx, EvaluationStrategy = EvaluationStrategy);

reserveHoldoutForUser sets aside an untouched slice for the user's own testing when they have no separate test set (default 20%, user-settable via HoldoutFraction; else nothing); the rest is the working data all phases run on. When a carve happens, materialize the slice as the table variable's name + _test from the original rows and narrate the split in plain words (fraction, row counts, and method — stratified/random — from ReserveInfo); see intake.md. splitStrategy then sets Splits.TrainIdx/.TestIdx over the working rows — a train/test split under holdout, or all working rows with empty TestIdx under cross_validated. Generation and selection run on TrainIdx only.

2. Feature pool — references/feature-pool.md

Produce the candidate pool. First check whether a domain skill fits the data (references/domain-routing.md); otherwise use the default SMLT path:

Opts.TargetModel = TargetModel;   Opts.Standardization = "auto";
OptArgs = namedargs2cell(Opts);
TrainIdx = Splits.TrainIdx;   TestIdx = Splits.TestIdx;
TrainTbl = ScreenedTbl(TrainIdx, :);                       % fit generation on train rows only
[~, Transformer, GenInfo] = generateFeatures(TrainTbl, Response, ProblemType, OptArgs{:});
Recipe  = Transformer;                                     % SMLT recipe (domain path: the captured struct)
FullEng = transformFeatures(Recipe, ScreenedTbl);          % engineered pool over ALL rows
FullEng.(Response) = ScreenedTbl.(Response);               % transformFeatures returns predictors only

Generate-only (external consensus does the cutting). Mind the wide-input guard (generateFeatures:tooManyPredictors) — hold the wide-input conversation and re-call with Opts.NumFeatures set. FullEng is the canonical pool: engineered over all rows with the response re-attached, train-fit so the held-out rows stay leakage-clean. On the domain path the captured table already spans all rows — use it as FullEng directly. Whichever path runs, downstream reads only the pool contract (Recipe, describeFeatures, transformFeatures) — never the producer. Announce GenInfo.PoolSize.

Set OriginalData/OriginalPredVars here — the baseline's "original" reference is path-dependent (raw columns when they exist, else the full pool). assess.md §1 pins the rule.

3. Select — references/select.md

The heart of the skill. One call runs the ranker panel, the consensus vote, and the elbow cut:

[SelectedNames, VoteTable, PanelInfo] = runConsensusSelection( ...
    FullEng(TrainIdx, :), Response, ProblemType, ExcludeFeatures = GenInfo.BinaryReliant, ...
    TargetModel = TargetModel);

Selection runs on the training rows only — slice FullEng(TrainIdx,:); the held-out rows never enter ranking. TargetModel gates the ranker panel: a declared family runs its own embedded probe (linear→lasso, tree_ensemble→oob, kernel_distance→nca) plus the two model-agnostic rankers; agnostic (the default) keeps the full five-ranker panel. Report PanelInfo.Reasoning. Build a plotSelectionConsensus figure only when the report is on (GenerateReport) — every figure is a report input, so a report opt-out skips all plot calls.

The score elbow alone sets the count (see select.md); when the scores decline too gradually to show a knee the full pool is kept — report a keep-all as "the candidates are comparably useful," not as a failure to select.

4. Assess — references/assess.md

Diagnostic only; never revises the delivered set. The performance read follows the evaluation strategy chosen at intake:

Quality = featureSetQuality(ScreenedTbl, FullEng, TrainIdx, Response, ...
    OriginalPredVars, SelectedNames, ProblemType);          % model-free, always

if Splits.Strategy == "holdout"                              % point estimate on the held slice
    Baseline = baselineComparison(ScreenedTbl, FullEng, TrainIdx, TestIdx, Response, ...
        OriginalPredVars, SelectedNames, ProblemType, TargetModel = TargetModel, InternalCV = false);
else                                                         % cross_validated: folds ARE the estimate
    KFold = assessKFold(ScreenedTbl(TrainIdx, :), Response, ProblemType, CVP, TargetModel = TargetModel);
end

% Stability — two independent opt-in gates, both default OFF (assess.md §3):
rng(0, "twister");
SelStab = assessSelectionStability(StabEng, Response, ProblemType, ...        % Gate 1 (fixed pool)
    ExcludeFeatures = StabExclude, TargetModel = TargetModel, MaxRows = RowBudget);
GenStab = assessGenerationStability(RawTrainTbl, Response, ProblemType, ...    % Gate 2 (pool regenerated)
    TargetModel = TargetModel, MaxRows = RowBudget);

Pass a row budget to both gates as MaxRows only when the user named one ("~1000 patients is fine" → MaxRows = 1000) to keep the audit affordable; else omit it (default 3000) — don't pass an empty value.

Quality (model-free representation quality) and the strategy's performance read are unconditional: under holdout, baselineComparison scores the held TestIdx as a point estimate (InternalCV = false, no error bars); under cross_validated (TestIdx empty), assessKFold is the headline (regenerates per fold, reports mean ± std, performance-only) — skip baselineComparison.

Stability is two independent opt-in gates, both default OFF (see assess.md §3): Gate 1 SelStab (re-selects on a fixed pool) and Gate 2 GenStab (also regenerates the pool per subsample; needs a re-runnable fitter — SMLT by default, a domain path needs Recipe.Fit, §3c). Both are computationally expensive — each re-runs the consensus selection M times, and Gate 2 adds a full regeneration on top — so both stay off unless asked for. Both grade the procedure; neither revises the delivered set. Offer the choice, don't default them on, and announce the cost — ask "Want a stability check — would the same features get picked if the data were resampled? It re-runs selection many times so it takes a while; a deeper version also re-builds the features each time, costing more again." and run only what the user opts into. When neither is requested, add neither and set Results.StabilitySkipped = true (report says "not requested"). Skip a requested gate automatically with that flag when the pool has < 2 features or < 2 subsamples are possible.

5. Deliver — references/deliver.md

Write the artifacts to the confirmed output directory:

[ScriptPath, MatPath] = writeInferenceScript(Recipe, SelectedNames, OutputDir, ...
    DatasetName = DatasetName, ResponseVar = ResponseVar);
SelectedTbl = selectFeatures(Recipe, RawTbl, SelectedNames);   % in-workspace CHECK only — do NOT write to disk

The recipe is the inference recipe — fe_transform_<dataset>.m + its companion .mat reproduce the exact selected set on new raw data with one call. writeInferenceScript is generator-agnostic (SMLT FeatureTransformer or a Kind=="domain" recipe) and inlines its logic, so the deliverable stands alone. SelectedTbl is only a verification value — never write the feature table to disk (features_<dataset>.mat/.csv); the inference script regenerates it on demand, so a materialized dump is redundant. Write the figures' fig_*.svg alongside only when the report is on; under a report opt-out the inference script + state are the whole deliverable.

Also save the full Results struct unconditionally — every run result at full resolution, so nothing a plot or a truncated table only previews is lost:

ResultsMatPath = saveResults(Results, OutputDir, DatasetName = DatasetName);

This fe_results_<dataset>.mat ships even on a report opt-out (assemble Results first — see report.md). Tell the user it exists, and that the same data is live in the Results workspace variable this session.

6. Report — references/report.md

Optional (opt-out, default on) — run only if the user kept the report at intake (GenerateReport == true), the same flag that gates every figure in Select/Assess/Deliver. When run, assemble the plain Results struct from every prior phase and emit:

MdPath = generateFeatureReport(Results, OutputDir);            % always
if hasReportGenerator(); PdfPath = generateFeatureReportPdf(Results, OutputDir); end   % if available
7. Hand off to model training (offer, don't invoke)

This skill stops at features. If the user's ask mentioned training a model (supervised learning, classification, regression, etc.) and an installed skill covers tabular model training, name that candidate once at close-out and offer to hand off — semantic match against the injected skill list, confirm before invoking, no hardcoded skill names (same discovery rule as domain-routing.md). If nothing fits or the ask was features-only, stop silently.


Behavioral contract (all phases)

  • Train/test discipline — generation, selection, and all training-side statistics use the training rows only; the test rows certify the delivered artifact and are never seen before then.

  • Generator-agnostic downstream — after the pool phase, touch generator internals only through describeFeatures / transformFeatures.

  • No hidden truncation — if you cap, sample, or skip anything, say so.

  • Never fabricate a pool — when routing offers a choice, or the data needs a tabularizing step this skill can't perform, presenting the choice or asking ends the turn. Wait for the user before building the pool by any means (domain skill, generic path, or an extractor of your own).

  • Confirm outward-facing actions — writing to disk is confirmed once at intake (the output-dir hard-halt); don't write outside it.

  • MathWorks Coding Guidelines for any MATLAB you author (lowerCamelCase, arguments blocks, ≤6 in / ≤4 out, end terminators; modern APIs).


MATLAB and Simulink are registered trademarks of The MathWorks, Inc. See www.mathworks.com/trademarks for a list of additional trademarks.


Copyright 2026 The MathWorks, Inc.


Files

scripts/printSummary.m

function printSummary(Results)
%printSummary Print a short terminal close-out of the feature-engineering run.
%
%   printSummary(Results) prints a few headline lines to the console over the
%   same plain Results struct the report generators consume. It is a SUMMARY,
%   not a copy of the report: what ran, how the pool was cut, whether the
%   engineered set paid off against the baseline, and where the full detail
%   lives. The written report (generateFeatureReport / generateFeatureReportPdf)
%   carries the per-dimension quality breakdown, CV, macro-F1, stability gloss,
%   and full file list -- this does not repeat them.
%
%   Every line guards on field presence, so a partial Results (baseline skipped,
%   files not yet recorded) prints what it has and omits the rest. It is a
%   pointer to the report, never a pass/fail gate.
%
%   Results fields read (all optional):
%     .DatasetName .Profile .GenInfo .PanelInfo .SelectedNames .Baseline
%     .Quality .KFold .SelStab .GenStab .OutputDir .Deliverables

% Copyright 2026 The MathWorks, Inc.

    arguments
        Results (1,1) struct
    end

    Name = getField(Results, "DatasetName", "dataset");
    fprintf('\n=== Feature Engineering Summary: %s ===\n', char(Name));

    printWhatRan(Results);
    printPoolToSelected(Results);
    printHeadlineResult(Results);
    printKFoldPerformance(Results);
    printModelFree(Results);
    printStability(Results);
    printWhereToLook(Results);

    fprintf('===============================================\n');
end


function printWhatRan(Results)
    P = getField(Results, "Profile", struct());
    if ~isfield(P, 'ProblemType')
        return;
    end
    Rows = "";
    if isfield(P, 'NumRows')
        Rows = sprintf(", %d rows", P.NumRows);
    end
    fprintf('  %s%s\n', char(P.ProblemType), char(Rows));
end


function printPoolToSelected(Results)
    Pool = poolSize(Results);
    NSel = selectedCount(Results);
    if isnan(Pool) && isnan(NSel)
        return;
    end
    if ~isnan(Pool) && ~isnan(NSel)
        fprintf('  Features:  %d candidates -> %d selected\n', Pool, NSel);
    elseif ~isnan(NSel)
        fprintf('  Features:  %d selected\n', NSel);
    end
end


function printHeadlineResult(Results)
    % Baseline (baselineComparison) is unconditional in a real run; the presence
    % guards only protect an early/partial Results assembled before it landed.
    if ~isfield(Results, 'Baseline') || ~isstruct(Results.Baseline)
        return;
    end
    B = Results.Baseline;
    if ~isfield(B, 'Original') || ~isfield(B, 'Engineered')
        return;
    end
    % Print one line per trained family, mirroring the k-fold panel below.
    % Whether engineering beats the originals is model-dependent, so a single
    % headline would misattribute the primary family's result to all of them;
    % each family shows its own engineered-vs-original score and its own secondary
    % metric. The primary (reported) family leads. Under a declared single-family
    % TargetModel the panel is one row. No win/lose verdict is tacked on -- the
    % report's per-family table carries the full picture.
    Panel = baselinePanel(B);
    fprintf('  Baseline (holdout):\n');
    for i = 1:numel(Panel)
        FP = Panel(i);
        Metric = char(getField(FP, "Metric", ""));
        fprintf('    %s: engineered %s = %s vs original %s\n', ...
            char(familyLabel(FP.Family)), Metric, fmtQ(FP.Engineered), fmtQ(FP.Original));
        Secondary = secondaryMetric(FP);
        if strlength(Secondary) > 0
            fprintf('               %s\n', char(Secondary));
        end
    end
end


function Panel = baselinePanel(B)
%baselinePanel The per-family holdout panel, primary family first.
%   Falls back to a one-family panel synthesized from the top-level fields when an
%   older Result carried no .Panel -- the top-level fields are the primary family's.
    if isfield(B, 'Panel') && ~isempty(B.Panel)
        Panel = B.Panel;
        Primary = string(getField(B, "Primary", "bag"));
        Fams = arrayfun(@(P) string(P.Family), Panel);
        Idx = find(Fams == Primary, 1);
        if ~isempty(Idx)
            Panel = Panel([Idx, setdiff(1:numel(Panel), Idx, 'stable')]);
        end
        return;
    end
    Panel = struct(Family = string(getField(B, "Primary", "bag")), ...
        Metric = getField(B, "Metric", ""), ...
        Engineered = B.Engineered, Original = B.Original, ...
        MacroF1 = getField(B, "MacroF1", struct()), ...
        Accuracy = getField(B, "Accuracy", struct()));
end


function printKFoldPerformance(Results)
%printKFoldPerformance One line for the full-pipeline k-fold performance read.
%   The baseline headline above is a single-holdout point estimate. When the
%   opt-in Mode 3 k-fold ran (Results.KFold.Performance), it carries the honest
%   end-to-end mean +/- std over folds -- the number worth paying for -- so give
%   it its own line right under the headline, parallel to how the stability line
%   surfaces the k-fold Nogueira index. Prints only when that read exists.
%
%   The primary metric (AUC or RMSE) leads; each companion metric assessKFold
%   aggregated across folds (classification: macro-F1, and accuracy when balanced)
%   follows on its own sub-line. Under agnostic every trained family (bag/linear/
%   kernel) gets its own block; a declared model prints a single block.
    if ~isfield(Results, 'KFold') || ~isstruct(Results.KFold) ...
            || ~isfield(Results.KFold, 'Performance') || ~isstruct(Results.KFold.Performance)
        return;
    end
    KF = Results.KFold;
    Perf = KF.Performance;
    if ~isfield(Perf, 'Engineered') || ~isstruct(Perf.Engineered) ...
            || ~isfinite(getField(Perf.Engineered, "Mean", NaN))
        return;
    end
    Metric = char(getField(KF, "MetricName", ""));
    K = getField(KF, "K", NaN);
    Folds = "";
    if ~isnan(K)
        Folds = sprintf(" (%d folds)", K);
    end
    fprintf('  K-fold performance%s:\n', char(Folds));
    Panel = kfoldPanel(Perf);
    for i = 1:numel(Panel)
        FP = Panel(i);
        fprintf('    %s: engineered %s %s vs original %s\n', ...
            char(familyLabel(FP.Family)), Metric, ...
            fmtMeanStd(FP.Engineered), fmtMeanStd(FP.Original));
        printKFoldSecondary(getField(FP, "Secondary", ...
            struct('Name', {}, 'Engineered', {}, 'Original', {})));
    end
end


function Panel = kfoldPanel(Perf)
%kfoldPanel The per-family panel, falling back to a one-family primary-only panel
%   when an older Result carried no .Panel.
    if isfield(Perf, 'Panel') && ~isempty(Perf.Panel)
        Panel = Perf.Panel;
        return;
    end
    Panel = struct(Family = "bag", Engineered = Perf.Engineered, ...
        Original = getField(Perf, "Original", struct()), ...
        Secondary = getField(Perf, "Secondary", ...
            struct('Name', {}, 'Engineered', {}, 'Original', {})));
end


function printKFoldSecondary(Secondary)
%printKFoldSecondary Sub-lines for each companion metric aggregated across folds.
    for i = 1:numel(Secondary)
        S = Secondary(i);
        if ~isfinite(getField(S.Engineered, "Mean", NaN))
            continue
        end
        fprintf('               %s: eng %s vs orig %s\n', ...
            char(S.Name), fmtMeanStd(S.Engineered), fmtMeanStd(S.Original));
    end
end


function printModelFree(Results)
%printModelFree One diagnostic line: relevance / redundancy / count, orig->eng.
%   Model-free means no model is fit -- it reads the feature set's own structure
%   (mutual information with the response, pairwise MI between features, size).
%   A one-line headline only; the report carries the per-dimension detail.
%   featureSetQuality is unconditional in a real run; the guard only protects an
%   early/partial Results assembled before it landed.
    if ~isfield(Results, 'Quality') || ~isstruct(Results.Quality)
        return;
    end
    Q = Results.Quality;
    Parts = string.empty(1, 0);
    if isfield(Q, 'Relevance') && isfield(Q.Relevance, 'MaxOriginal')
        Parts(end+1) = sprintf("relevance %s->%s", ...
            fmtQ(Q.Relevance.MaxOriginal), fmtQ(Q.Relevance.MaxEngineered));
    end
    if isfield(Q, 'Redundancy') && isfield(Q.Redundancy, 'MeanPairwiseMIOriginal')
        Parts(end+1) = sprintf("redundancy %s->%s", ...
            fmtQ(Q.Redundancy.MeanPairwiseMIOriginal), fmtQ(Q.Redundancy.MeanPairwiseMIEngineered));
    end
    if isfield(Q, 'Compactness') && isfield(Q.Compactness, 'NumFeaturesOriginal')
        Parts(end+1) = sprintf("%d->%d features", ...
            Q.Compactness.NumFeaturesOriginal, Q.Compactness.NumFeaturesEngineered);
    end
    if isempty(Parts)
        return;
    end
    fprintf('  Model-free:  %s\n', char(join(Parts, " | ")));

    % Under cross_validated, the recipe's engineered-side quality also has a
    % per-fold band (assessKFold) -- print it as a companion sub-line.
    if isfield(Results, 'KFold') && isstruct(Results.KFold) ...
            && isfield(Results.KFold, 'QualityStability')
        QS = Results.KFold.QualityStability;
        fprintf('               across folds: relevance %s | redundancy %s (eng)\n', ...
            fmtMS(QS.MaxRelevance), fmtMS(QS.MeanRedundancy));
    end
end


function Str = fmtMS(S)
%fmtMS Format a meanStd sub-struct as "mean +/- std".
    Str = sprintf('%.3f +/- %.3f', S.Mean, S.Std);
end


function printStability(Results)
%printStability One line per selection-stability read that was computed.
%   Two INDEPENDENT opt-in reads can exist: the fixed-pool read (Results.SelStab)
%   and the generation-variance read (Results.GenStab), each requested on its own.
%   The fixed-pool read is the base, so print it first; print the generation read
%   when it also ran. Name which resampling produced each number so it is not
%   mistaken for a single-split result. Neither requested => nothing prints.
    Shipped = string(getField(Results, "SelectedNames", string.empty(1, 0)));
    Nog = stabNogueira(Results, 'SelStab');
    if ~isnan(Nog)
        fprintf('  Stability:  Nogueira %s %s (fixed-pool resampling)\n', ...
            fmtQ(Nog), char(nogueiraGloss(Nog)));
        printFreqDigest(Results, 'SelStab', Shipped);
    end
    GenNog = stabNogueira(Results, 'GenStab');
    if ~isnan(GenNog)
        fprintf('  Stability:  Nogueira %s %s (generation, pool regenerated)\n', ...
            fmtQ(GenNog), char(nogueiraGloss(GenNog)));
        printFreqDigest(Results, 'GenStab', Shipped);
        printDriftDigest(Results, Shipped);
    end
end


function printDriftDigest(Results, Shipped)
%printDriftDigest One line per shipped feature that drifted out of the regenerated
%   pool -- the actionable slice of the generation read's Drift table. "Drift" is
%   binary pool membership: a feature not generated in every subsample was not always
%   available to re-select, so the "absent from N%" gap is worth surfacing alongside
%   the fragile picks. Prints nothing when no shipped feature drifted (the common,
%   healthy case).
    if ~isfield(Results, 'GenStab') || ~isstruct(Results.GenStab) ...
            || ~isfield(Results.GenStab, 'Drift') || ~istable(Results.GenStab.Drift) ...
            || isempty(Results.GenStab.Drift)
        return;
    end
    Drift = Results.GenStab.Drift;
    Shipped = string(Shipped);
    Feats = string(Drift.Feature);
    IsShipped = ismember(Feats, Shipped);
    if ~any(IsShipped)
        return;
    end
    Idx = find(IsShipped);
    Absent = 1 - double(Drift.PoolCoverage);
    Parts = arrayfun(@(i) sprintf('%s (absent from %.0f%%)', Feats(i), 100 * Absent(i)), ...
        Idx(:)', UniformOutput=false);
    fprintf('              shipped but drifted from pool: %s\n', ...
        char(joinNames(string(Parts))));
end


function printFreqDigest(Results, Field, Shipped)
%printFreqDigest The per-feature re-selection read as a short digest -- the terminal
%   counterpart to the report's frequency table, and it uses the SAME three tiers the
%   report does so the two don't disagree: core (>= the CoreThreshold, default 80%),
%   above the 50% floor but below core (kept, moderate support), and fragile (below
%   the floor -- the single most important thing to eyeball). The full per-feature
%   table lives in the report.
    Freq = stabFreqTable(Results, Field);
    Shipped = string(Shipped);
    if isempty(Freq) || isempty(Shipped)
        return;
    end
    Floor = 0.5;
    Core = coreThreshold(Results, Field);
    Feats = string(Freq.Feature);
    [IsShipped, Loc] = ismember(Shipped, Feats);
    ShippedFreq = nan(size(Shipped));
    ShippedFreq(IsShipped) = double(Freq.Frequency(Loc(IsShipped)));
    NCore = sum(ShippedFreq >= Core);
    MidBand = Shipped(ShippedFreq >= Floor & ShippedFreq(:)' < Core);
    Fragile = Shipped(ShippedFreq(:)' < Floor & ~isnan(ShippedFreq(:)'));
    fprintf('              %d of %d shipped features are core (re-selected in >= %.0f%% of resamples)\n', ...
        NCore, numel(Shipped), 100 * Core);
    if ~isempty(MidBand)
        fprintf('              above the %.0f%% floor but below core (moderate support): %s\n', ...
            100 * Floor, char(joinNames(MidBand)));
    end
    if ~isempty(Fragile)
        fprintf('              fragile (below the %.0f%% floor, rarely re-picked): %s\n', ...
            100 * Floor, char(joinNames(Fragile)));
    end
end


function T = coreThreshold(Results, Field)
%coreThreshold The read's own CoreThreshold (re-selection fraction at/above which a
%   feature is "core"), matching the report; falls back to 0.8 when the field is
%   absent (a Result predating it).
    T = 0.8;
    if isfield(Results, Field) && isstruct(Results.(Field)) ...
            && isfield(Results.(Field), 'CoreThreshold') ...
            && ~isempty(Results.(Field).CoreThreshold)
        T = Results.(Field).CoreThreshold;
    end
end


function Tbl = stabFreqTable(Results, Field)
%stabFreqTable The SelectionFrequency table from a stability read, or [] if absent.
    Tbl = [];
    if isfield(Results, Field) && isstruct(Results.(Field)) ...
            && isfield(Results.(Field), 'SelectionFrequency') ...
            && istable(Results.(Field).SelectionFrequency)
        Tbl = Results.(Field).SelectionFrequency;
    end
end


function Str = joinNames(Names)
%joinNames Comma-joined name list, truncated after a few so the line stays short.
    Names = string(Names);
    Max = 5;
    if numel(Names) > Max
        Str = join(Names(1:Max), ", ") + sprintf(", +%d more", numel(Names) - Max);
    else
        Str = join(Names, ", ");
    end
end


function Val = stabNogueira(Results, Field)
%stabNogueira The Nogueira index from a stability read struct, or NaN when absent.
    Val = NaN;
    if isfield(Results, Field) && isstruct(Results.(Field)) ...
            && isfield(Results.(Field), 'Nogueira')
        Val = Results.(Field).Nogueira;
    end
end


function Gloss = nogueiraGloss(Value)
%nogueiraGloss One-word band for a Nogueira stability index.
    if isnan(Value)
        Gloss = "";
    elseif Value >= 0.75
        Gloss = "(highly stable)";
    elseif Value >= 0.4
        Gloss = "(moderate)";
    else
        Gloss = "(unstable)";
    end
end


function printWhereToLook(Results)
    Files = string(getField(Results, "Deliverables", string.empty(1, 0)));
    Dir = string(getField(Results, "OutputDir", ""));
    if strlength(Dir) > 0
        fprintf('  Written:   %d files to %s\n', numel(Files), char(Dir));
    elseif ~isempty(Files)
        fprintf('  Written:   %d files (see feature_report)\n', numel(Files));
    end

    % Point at the two ways to get the run's numbers at full resolution -- the
    % report only previews the top features, so name the results .mat (every
    % table, unconditionally saved) and the live workspace struct that holds the
    % same thing this session. Print the .mat line only if it was actually saved.
    Res = Files(endsWith(Files, ".mat") & startsWith(Files, "fe_results_"));
    if ~isempty(Res)
        fprintf('  Full data: every table (vote, stability, quality) saved to %s\n', ...
            char(Res(1)));
    end
    fprintf(['             also live in the `Results` struct in your workspace ' ...
        'this session.\n']);
end


% ------------------------------------------------------------- small helpers

function Out = getField(S, Name, Default)
%getField Struct field with a default when absent (string name).
    if isfield(S, Name)
        Out = S.(Name);
    else
        Out = Default;
    end
end


function N = poolSize(Results)
    N = NaN;
    G = getField(Results, "GenInfo", struct());
    if isfield(G, 'PoolSize')
        N = G.PoolSize;
        return;
    end
    PI = getField(Results, "PanelInfo", struct());
    if isfield(PI, 'PoolSize')
        N = PI.PoolSize;
    end
end


function N = selectedCount(Results)
    N = NaN;
    PI = getField(Results, "PanelInfo", struct());
    if isfield(PI, 'NSelect')
        N = PI.NSelect;
        return;
    end
    if isfield(Results, 'SelectedNames')
        N = numel(string(Results.SelectedNames));
    end
end


function Str = secondaryMetric(B)
%secondaryMetric The companion score alongside the primary metric.
%   Classification: macro-F1 (class-balanced companion to AUC), or accuracy when
%   the panel logged it for a balanced problem. Regression's RMSE stands alone,
%   so this is empty there.
    Str = "";
    if isfield(B, 'MacroF1') && isfinite(getField(B.MacroF1, "Engineered", NaN))
        Str = sprintf("macro-F1: eng %s vs orig %s", ...
            fmtQ(B.MacroF1.Engineered), fmtQ(B.MacroF1.Original));
    elseif isfield(B, 'Accuracy') && isfinite(getField(B.Accuracy, "Engineered", NaN))
        Str = sprintf("accuracy: eng %s vs orig %s", ...
            fmtQ(B.Accuracy.Engineered), fmtQ(B.Accuracy.Original));
    end
end


function Label = familyLabel(Family)
%familyLabel Short human-readable name for a model family.
    switch string(Family)
        case "linear"
            Label = "linear";
        case "kernel"
            Label = "kernel";
        case "bag"
            Label = "random forest";
        otherwise
            Label = string(Family);
    end
end


function Str = fmtQ(X)
    if ~isnumeric(X) || ~isscalar(X) || isnan(X)
        Str = 'n/a';
    else
        Str = sprintf('%.3f', X);
    end
end


function Str = fmtMeanStd(MS)
%fmtMeanStd Format a meanStd struct (.Mean/.Std) as "mean +/- std", or n/a.
    if ~isstruct(MS) || ~isfield(MS, 'Mean') || ~isfinite(getField(MS, "Mean", NaN))
        Str = 'n/a';
        return;
    end
    Std = getField(MS, "Std", NaN);
    if isfinite(Std)
        Str = sprintf('%.3f +/- %.3f', MS.Mean, Std);
    else
        Str = sprintf('%.3f', MS.Mean);
    end
end

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an MCP server

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
MathWorks
Publisher
MathWorks (Official)
Source
matlab/matlab-agentic-toolkit · skills-catalog/ai-and-statistics/matlab-engineer-tabular-features · commit 2a7f215
Licence
BSD 2-Clause License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE.md)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by MathWorks.

Get it from the source

8 of this skill’s files are not mirrored here, so there is no ZIP. Get the complete skill from its source: matlab/matlab-agentic-toolkit