Skip to content

Feature selection: which one bit should you send?

Sometimes a downstream system can carry only one yes/no field per transaction, and you have eight yes/no risk signals to choose from. This page shows the library's helper for that choice: score each signal by how often it agrees with the outcome you care about, keep the best one, and check it on data it has not seen. It is written for a risk data scientist deciding whether to ship a single signal or train a small model to compress all eight. Read the warning in What the library will and will not claim before relying on the demo: its data is built so that the answer is known in advance.

At a glance

Question Which one of eight 0/1 risk signals should go into a one-bit field, and should it be flipped?
Owner Risk data science; the partner integration or terminal-rule team consumes the result.
Today's habit Train a tiny neural network with a one-bit output.
What this adds A no-training baseline: keep the single signal (or its opposite) that agrees most often with the outcome, and measure it on a hold-out window.
Demo outcome Keeps mismatched_billing_address as-is; agrees with the chargeback label on 77.6 % of hold-out rows (the best possible on this synthetic data is 80 %).
Kind of example A helper, not a decision: it returns configuration, not an act/abstain verdict.
Example id feature_selection (uv run grounded-decision examples feature_selection)

The question today

A payments risk engine computes eight yes/no signals per transaction: new device, foreign IP, velocity spike, mismatched billing address, night-time, high amount, first purchase, disposable email. A partner API, an offline card-terminal rule or a compressed event log has room for one bit. You want the bit that says the most about whether the transaction later becomes a chargeback.

The tempting answer is a small model with a one-bit bottleneck: eight inputs, one output, thresholded. That is more code to train, monitor and explain to the partner, and it can quietly overfit.

Where today's habit goes wrong

A learned one-bit mixer is hard to justify when a single raw signal does as well: it adds a model to the real-time path, it has to be retrained, and the partner cannot read it. The helper gives you the cheap baseline first, so that a trained model has to beat a measured number to earn its place.

What the library will and will not claim

The helper, best_coordinate in grounded_decision.compare, does one thing: for each signal it counts how often the signal equals the label, takes the larger of that share and its opposite (a signal that is almost always wrong is just as useful flipped), and returns the best signal, whether to flip it, and its agreement on the training rows. No training, no model.

The library claims The library does not claim
The chosen signal has the highest agreement with the label among your signals, on the rows you gave it. That a one-bit model could not do better on your data.
On data that meets the theory's assumptions (below), keeping one signal is the best one-bit summary there is. That your data meets those assumptions. Real risk signals are correlated with each other and rarely 50/50.
The hold-out agreement the example measures is an honest out-of-sample number. That a hold-out agreement near 0.5 is worth shipping. It is not; ship nothing.

The demo is circular: it shows the API, not that the method finds signal

The synthetic chargeback label is defined as mismatched_billing_address with 20 % of labels flipped at random, and the other seven signals are independent coin flips. The right answer is therefore known before the helper runs, and any sensible method (a correlation table, a decision stump, a small model) would find it. The demo shows how to call the helper and what it returns. It is not evidence that the helper finds signal in real data, and it is not a test of the theorem below.

The theory behind it, stated carefully. The result the helper is named after concerns a uniformly random vector of bits \(X\) and a noisy copy \(Y\) of the whole vector, each bit flipped independently. The question is which yes/no function of \(X\) carries the most information about \(Y\). The answer claimed is: a single bit of \(X\) (a "dictator" function). That is a different, harder setting from the demo, where the label is a noisy copy of one signal; in the demo's setting, keeping that signal is optimal for elementary reasons.

  • In the wider literature: this is the Courtade–Kumar conjecture (2014). It has been proved in some noise regimes and remains open in general.
  • In this project's math catalog: result 119, "Courtade–Kumar coordinate contraction", is recorded with a machine-checked (Lean) statement. See Keeping one bit (Courtade–Kumar) and Where each result stands for exactly what was checked.

Either way, the result only speaks to uniform, independent bits under independent flips. For real signals the helper is a strong, cheap baseline, not a proof of optimality.

Data you provide

Input Source Refresh Owner Quality bar
Training window: rows of the eight 0/1 signals Risk-engine feature log Each labelled window (for example monthly) Risk data science Old enough that chargebacks have arrived (they take weeks).
Chargeback label per row (1 = became a chargeback) Disputes / chargeback system As disputes close Fraud ops Friendly fraud and undisputed fraud make the label noisy; that is expected.
Hold-out window: same columns, a later period Same Same Same Never overlaps the training window.

Worked example

From uv run grounded-decision examples feature_selection (the --json summary gives the unrounded values):

Item Value
Training rows / hold-out rows / signals 1,500 / 500 / 8
How the label was made mismatched_billing_address, flipped with probability 0.2
Signal kept mismatched_billing_address (index 3), not flipped
Agreement on training rows 0.815
Agreement on hold-out rows 0.776
Best possible agreement on this data 0.80 (one minus the flip probability)
Today's habit train a one-bit network

The helper recovers the generating signal, as it must on this data, and its hold-out agreement sits close to the 0.80 ceiling.

Precise statement and the agreement rule

For column \(j\) with values \(x_{ij}\) and labels \(y_i\) over \(n\) training rows, the helper computes \(a_j = \frac{1}{n}\sum_i [x_{ij} = y_i]\) and scores the column \(\max(a_j, 1 - a_j)\). It returns the arg-max \(j^\star\) and use_complement \(= (a_{j^\star} < 0.5)\).

The Courtade–Kumar statement: let \(X\) be uniform on \(\{0,1\}^d\) and let \(Y\) be \(X\) with each bit flipped independently with probability \(\varepsilon \le 1/2\). For every Boolean function \(f\), \(I(f(X); Y) \le 1 - h_2(\varepsilon)\), where \(h_2\) is the binary entropy; a single coordinate \(f(X) = X_j\) attains it. The helper does not compute mutual information; it uses agreement with a label as a practical stand-in.

What your service does with the result

The result is configuration, not a per-transaction decision:

  1. A scheduled job runs the helper on each new labelled window and measures the hold-out agreement.
  2. If the hold-out agreement is close to 0.5, no single signal is informative: do not ship the one-bit field.
  3. Otherwise persist {"signal", "use_complement", "holdout_agreement"}. The real-time path reads that one signal, flips it if told to, and sends it. No model runs per transaction.
  4. If a decision policy downstream uses the bit, pass it in the event payload; see Write a policy.

Tuning and monitoring

There is nothing to tune in the helper. Monitor two things on each re-run: whether the chosen signal changes (a change is worth a human look, because partners notice), and whether hold-out agreement drifts towards 0.5. If you do train a one-bit model, compare its hold-out agreement with this baseline before shipping it.

Limits and risks

  • Circular demo (see the warning above).
  • Assumptions rarely hold. Real signals are correlated and unbalanced; a combination of signals can beat any single one. The helper does not search combinations.
  • Agreement is not the only cost. If a missed chargeback costs far more than a false flag, weight the agreement accordingly before choosing; the helper treats both errors alike.
  • Label delay. Chargebacks arrive weeks late; a too-recent window understates the label and can change the choice.

Wiring it in

from grounded_decision.compare.courtade_kumar import apply_coordinate, best_coordinate

selection = best_coordinate(training_signals, training_labels)  # (rows, 8) and (rows,)
kept_bit = apply_coordinate(
    holdout_signals, selection["index"], selection["use_complement"]
)
holdout_agreement = float((kept_bit == holdout_labels).mean())

choose_one_bit in src/grounded_decision/examples/feature_selection.py wraps exactly these three lines and adds the signal name. More on the comparison helpers: Compare outcomes and features.

Run it yourself

uv run grounded-decision examples feature_selection
uv run grounded-decision examples feature_selection --json

The report has one "tick" (the selection) and a closing summary. How to read it: Example library: reading a report.

Further reading