Skip to content

fw.suggest_paths

Use fw.suggest_paths when a table has many possible dimensions and you want a useful census order. It ranks intermediate prefixes: what a reader sees after the first split, then the second. Different orders can have the same final joint counts and very different reading costs.

The search considers a limited set of paths and gives repeatable results. Its recommendations describe this dataset; they do not establish a causal hierarchy or guarantee the best possible order.

Jump to the worked example or the argument reference.

fw.suggest_paths(
df,
*,
objective='structure',
features=None,
start_with=None,
before=None,
exclude=None,
target=None,
max_dimensions=4,
n_paths=3,
limits=None,
scope=None,
missing=None,
table_id="table",
progress=None,
cancel=None,
timeout=None,
safe_errors=None,
)

This page describes Fieldwork 0.3.2. Import the package with import fieldwork as fw. View the documented source.

Argument Type / default Effect
df pandas.DataFrame; required The source table. Analysis preserves its values and row order; duplicate column labels are rejected.
objective str; 'structure' "structure" favors compact prefixes and coarse-before-fine nesting; "compact" emphasizes display cost; "availability" rewards early separation of availability signatures; "target" rewards early separation of target values; "context" uses structure scoring with a required starting prefix.
features iterable of str or None; None Columns to analyze, in candidate-search order; None selects all columns. Use a short, deliberate list when identifiers or free text would distract from the question.
start_with iterable of str or None; None Required leading dimensions, in this exact order. Required for the context objective; must fit the feature and dimension budgets.
before iterable of two-column pairs or None; None Acyclic ordering constraints, for example [("region", "store")]. Both endpoints must be selected, not excluded, and fit the dimension budget.
exclude iterable of str or None; None Columns removed from candidate paths. A required steering column cannot also be excluded.
target str or None; None Target column for target impurity scoring; required for objective="target". Normally excluded from candidate dimensions unless explicitly required by steering.
max_dimensions positive int; 4 Maximum dimensions per path. Required steering columns must fit. Search exhaustion can leave a shorter path.
n_paths positive int; 3 Maximum diverse final paths returned. Fewer may exist; equivalent alias choices are collapsed for diversity.
limits PathLimits mapping or None; None Search budgets; omitted keys keep their defaults and unknown keys raise TypeError. max_candidates (positive, 200): path extensions evaluated by beam search. max_features (positive, 20): candidate features retained, required features first, then input order; constants are removed unless required. max_pairs (nonnegative, 200): pairs checked for observed nesting and aliases. beam_width (positive, 12): distinct feature sets kept at each depth. display_budget (positive, 40): target prefix-display size used in score penalties and node budget of each saved census preview. Budgets bound work; they never sample rows.
scope Scope or None; None A saved row selection tied to this dataframe; None uses every row. Construct with fw.Scope.from_positions(df, positions). Pass the original dataframe with its scope, rather than a reordered or changed copy.
missing mapping or None; None Column names mapped to lists of sentinel values, for example {"reading": [-999]}. Native nulls are always missing. Sentinels affect this analysis without replacing source values.
table_id str; "table" Nonempty table identity recorded with the evidence. It does not bypass source-fingerprint checks.
progress bool or callable; None Silent by default; True displays progress, or pass a synchronous ProgressEvent callback. See runtime controls.
cancel CancellationToken or None; None Cooperative cancellation token; cancellation raises AnalysisCancelled without returning a partial result.
timeout float or None; None Finite nonnegative seconds from call start; None disables the cooperative deadline. A running pandas/NumPy work item must finish before cancellation.
safe_errors bool or None; None True raises any other failure as AnalysisError, naming the operation, phase and column without echoing the original message, which can quote cell values. See runtime controls.

Returns a fieldwork.Result with kind="paths". paths contains dimensions, measurements, reasons, explanations, and census previews. best is a source-bound Path, or None when none is available; path(i) accesses another recommendation. best.census(df, **options) preserves the original scope and missing conventions. Read coverage, aliases, and nesting alongside the ranking; parameters["limits"] records the budgets the search used.

Use plaintext for a text report, SVG for a static figure, or HTML for a shareable report. to_dict() retains analytical evidence; visualization_data provides the labels and values used to draw a figure. List sections are available as tables through to_frame(section), such as to_frame("paths").

Lower scores are preferred within a search; scores are heuristic costs, not probabilities, percentages, or comparable quality scales across datasets and objectives. Target and availability impurities are row-weighted. Missing values form a category for path scoring.

Contradictory or cyclic steering, missing required target/prefix, or insufficient budgets raise ValueError. A saved path rejects a changed source and context overrides; rerun discovery for a new delivery.

Walkthrough: opening a museum catalog at the right level

Section titled “Walkthrough: opening a museum catalog at the right level”

A collections manager wants a browsable inventory: start with a collection, then find a room, cabinet, and object. The catalog also has accession identifiers. Starting with an identifier can locate a known object, but it makes a poor opening view for someone exploring the shelves.

suggest_paths helps choose that opening order. This example deliberately gives every stored object a unique accession. Every ordering eventually identifies the same objects; the useful difference is what readers see before they reach the last dimension.

Download the runnable example to follow the same steps and save the figure.

There are two collections, each with two rooms. Each room contains two cabinets, and each cabinet holds two objects: 2 × 2 × 2 × 2 = 16 stored objects. A seventeenth catalog row represents an object on loan. Room and cabinet names are globally distinct, so the observed nesting is unambiguous in this sample.

import pandas as pd
import fieldwork as fw
rows = []
for collection, rooms in [
("Ceramics", ["C1", "C2"]),
("Textiles", ["T1", "T2"]),
]:
for room in rooms:
for cabinet in [f"{room}-A", f"{room}-B"]:
for item in [1, 2]:
rows.append(
(
collection,
room,
cabinet,
f"{cabinet}-{item}",
"stored",
)
)
rows.append(("Ceramics", "Loan", "Loan-A", "LOAN-1", "on loan"))
catalog = pd.DataFrame(
rows,
columns=["collection", "room", "cabinet", "accession", "status"],
)
stored_positions = [
i
for i, status in enumerate(catalog["status"])
if status == "stored"
]
stored = fw.Scope.from_positions(
catalog, stored_positions, name="stored objects"
)
print(
f"Catalog: {len(catalog)} rows; stored: {len(stored.positions)} rows"
)
Catalog: 17 rows; stored: 16 rows

The scope selects the stored rows while retaining their original source positions. The loan remains in the source catalog but contributes to none of the path counts. status defines the population; it is not a candidate browsing dimension.

Restrict the candidates to the four inventory dimensions and allow a four-step path. The default structure objective favors inexpensive intermediate prefixes and penalizes reversing supported nesting. The display_budget limit of 40 (the default, spelled out here) participates in scoring and bounds the embedded census preview; it does not change the 16-row population.

paths = fw.suggest_paths(
catalog,
features=["collection", "room", "cabinet", "accession"],
max_dimensions=4,
limits={"display_budget": 40},
scope=stored,
table_id="museum_catalog",
)
summary = paths.to_frame("paths")
print("Recommended:", " > ".join(summary.loc[0, "dimensions"]))
print("Prefix groups:", summary.loc[0, "measurements.prefix_counts"])
print("Score:", summary.loc[0, "measurements.score"])
Recommended: collection > room > cabinet > accession
Prefix groups: [2, 4, 8, 16]
Score: 0.75

The table shows the measurements behind the recommendation. At depth one, there are two collection groups, each containing eight objects. Adding room produces four observed (collection, room) combinations. Cabinets produce eight groups of two objects; accession produces sixteen singleton groups.

The score is a heuristic cost, with lower preferred. Here, no prefix exceeds 40 groups, no step is redundant, and no nesting is reversed. The score is therefore (2 + 4 + 8 + 16) / 40 = 0.75. It is neither a probability nor a measure of catalog accuracy.

Native Fieldwork rendering of the museum path and its prefix counts

This figure is generated directly by fw.render_svg(paths); save it with the export step below. It preserves the explanation alongside the recommendation, including the observed nesting that supports the order.

The search evaluates all four candidate features and all six feature pairs; these counts are recorded in paths["coverage"]. A single alternative is expected here: all four dimensions fit the requested depth, and the search retains the best order per feature set rather than presenting every permutation as an alternative. The unexhausted candidate budget is useful coverage evidence; it does not turn a beam search into a proof of a global optimum.

Change the question: start from an accession

Section titled “Change the question: start from an accession”

A registrar looking up a known object may reasonably insist on an accession-first path. Use start_with to express that workflow, keeping the same population, features, depth, and scoring budget.

forced = fw.suggest_paths(
catalog,
features=["collection", "room", "cabinet", "accession"],
max_dimensions=4,
limits={"display_budget": 40},
scope=stored,
table_id="museum_catalog",
start_with=["accession"],
)
for label, result in [
("Browse shelves", paths),
("Look up accession", forced),
]:
candidate = result["paths"][0]
measures = candidate["measurements"]
print(label + ": " + " > ".join(candidate["dimensions"]))
print(
f" groups={measures['prefix_counts']}; score={measures['score']}; "
f"redundant={measures['redundant_steps']}; "
f"inversions={measures['nesting_inversions']}"
)
joint_groups = (
catalog.iloc[stored_positions]
.groupby(candidate["dimensions"])
.ngroups
)
print(" final joint groups:", joint_groups)
Browse shelves: collection > room > cabinet > accession
groups=[2, 4, 8, 16]; score=0.75; redundant=0; inversions=0
final joint groups: 16
Look up accession: accession > collection > room > cabinet
groups=[16, 16, 16, 16]; score=31.6; redundant=3; inversions=3
final joint groups: 16

The two paths have the same sixteen final combinations. Starting with accession, however, immediately creates sixteen groups. Every subsequent dimension adds descriptive context without splitting any group further. That produces three redundant steps and reverses three supported coarse-to-fine relationships: collection, room, and cabinet would normally precede accession.

Under this objective, the forced path costs 64 / 40 + 3 × 2 + 3 × 8 = 31.6. The high score explains why it is a cumbersome browsing tree. It does not invalidate accession lookup as a task. Steering lets the manager’s intended workflow take precedence over the default preference.

The ordering evidence also has a boundary: these labels nest in this delivery. The search cannot establish that rooms will always belong to one collection, or that an accession is a permanent identifier across future deliveries.

Continue with the selected path and the saved population

Section titled “Continue with the selected path and the saved population”

The recommendation becomes useful when it opens a census. Call best.census with the original catalog, so the stored-object scope, source identity, table name, and missing-value conventions carry forward automatically.

best = paths.best
if best is None:
print(
"No path was found. Try fewer required columns or a larger search budget."
)
else:
tree = best.census(catalog, max_nodes=40)
print(fw.render_plaintext(tree, max_lines=10))
Fieldwork · census
Census (computed, conditional)
Conditional cohort · 16 evaluated / 17 input rows · 0 missing, 1 restricted exclusions
path: collection > room > cabinet > accession
total: 16 rows
collection=Ceramics: 8 rows
room=C1: 4 rows
cabinet=C1-A: 2 rows
accession=C1-A-1: 1 row
... more output not rendered (max_lines)

For this catalog, the report begins with the collection, room, cabinet, and accession groups. The ten-line limit keeps the preview short.

The census preserves the denominator: Ceramics accounts for 8 of the 16 stored rows; room C1 accounts for 4 of those 8 Ceramics rows; cabinet C1-A accounts for 2 of those 4 room rows. The loan is still explicitly accounted for as one restricted row out of the original seventeen.

best.census checks the original source fingerprint. Pass the unchanged catalog, not a separately filtered frame; rerun path discovery when the source or analytical scope changes. Its saved conventions also cannot be replaced through scope, missing, or table_id overrides.

The next useful investigation is to inspect cabinets whose object counts depart from the expected two, then return to the underlying catalog records. The recommendation supplies a navigable view of observed structure; whether an empty or crowded cabinet reflects an inventory problem requires information beyond this synthetic table.

Save the figure or report in your working directory:

from pathlib import Path
Path("suggest_paths.svg").write_text(
fw.render_svg(paths), encoding="utf-8"
)

fw.census, fw.explore, fw.pairs. See results and configuration for serialization and result-method contracts.