geeViz.fsInsights

Forest Service data services, made usable.

Wraps two public USDA Forest Service APIs that answer complementary halves of the same question and look nothing alike:

FIA (Forest Inventory and Analysis) — a probability sample of forest plots going back to 1984. Answers what is in the forest, and how much, with a standard error. Its API exposes 752 estimate attributes, 96 grouping variables and 1,129 evaluations through one endpoint, and its documentation page still says “under construction”.

LCMS (Landscape Change Monitoring System) — wall-to-wall 30 m maps of land cover, land use, and change from 1985 to 2025. Answers what changed, and where. Small, clean API; almost nobody connects its output to anything else.

Quick start:

from geeViz import fsInsights as fs

fs.find_attributes("carbon")            # what can I estimate?
fs.find_evaluations("Oregon")           # which inventory? -> wc=412022
fs.estimate(wc=412022, snum=2,          # run it
            rselected="County code and name")

fs.lcms_summary(state="Oregon", county="Crook")

Two things worth knowing before trusting a number out of here:

  • FIA estimates always carry their sampling error. estimate() returns se_pct and plots alongside every value and flags cells that are too thin to report. An FIA estimate without its error is not a fact — a real query for white/red/jack pine in Alabama returns 15,748 acres with a 54.9% standard error from four plots.

  • LCMS and FIA are not directly comparable. One is a classified map, the other a probability sample. Comparing map-derived area to a design-based estimate conflates map accuracy with sampling error. This package makes the comparison easy and labels it; it will not hand you a single blended number that hides which is which.

geeViz.fsInsights.find_attributes(query: str = '', *, land_basis: str = '', eval_typ: str = '', limit: int = 25) → Any[source]

Search the 752 FIA estimate attributes.

Parameters:
  • query – Free text matched against description and group, e.g. "carbon", "net growth volume", "mortality".

  • land_basis – Restrict to "Forest land" or "Timberland".

  • eval_typ – Restrict to an evaluation type — EXPCURR, EXPVOL, EXPGROW, EXPMORT, EXPREMV, EXPCHNG, EXPDWM.

  • limit – Maximum rows returned.

Returns:

A pandas.DataFrame with the columns that matter for choosing an attribute — number, description, units, and the evaluation type it requires. The full record is available via get_attribute().

Note

FIA’s own catalog contains duplicate descriptions — 7 of the 752 appear more than once. snum 209 and 956, for instance, are identical across description, estimate group, evaluation type, estimation basis and tree portion. Both are returned rather than silently de-duplicated, because they are distinct attribute numbers upstream and collapsing them would hide a real property of the catalog. Where descriptions match on every field, either number should produce the same estimate.

geeViz.fsInsights.get_attribute(snum: int) → dict | None[source]

Full record for one attribute number, or None.

geeViz.fsInsights.find_groupings(query: str = '', *, limit: int = 25) → Any[source]

Search the 96 FIA grouping variables.

These are what rselected / cselected / pselected accept, and they are passed as display strings — so the exact label in this result is what the query needs, character for character.

geeViz.fsInsights.describe_grouping(label: str) → str[source]

Prose documentation for a grouping variable, codes included.

FIA ships this as PRC_METADATA — an HTML fragment that usually enumerates what each code value means. It is the difference between a column of integers and a column you can interpret, so it is worth surfacing rather than leaving buried in the catalog.

geeViz.fsInsights.find_evaluations(state: str = '', *, most_recent: bool = True, growth_only: bool = False, limit: int = 25) → Any[source]

Search the 1,129 FIA evaluations.

Parameters:
  • state – State name, matched case-insensitively on a prefix.

  • most_recent – Keep only each state’s current evaluation. Almost always what you want; set False to see the back catalog.

  • growth_only – Keep only evaluations with growth accounting (GROWTH_ACCT == 'Y'), which is a prerequisite for every growth, removals, and mortality attribute.

  • limit – Maximum rows returned.

geeViz.fsInsights.get_evaluation(wc: int) → dict | None[source]

Full record for one evaluation group, or None.

geeViz.fsInsights.estimate(wc: int, snum: int, *, rselected: str = '', cselected: str = '', pselected: str = '', sdenom: int | None = None, forest_definition: str = 'FIADEF', str_filter: str = '', max_se_pct: float = 30.0, min_plots: int = 30, validate_first: bool = True) → Any[source]

Run an FIA estimate and return it tidy, with its sampling error.

Parameters:
  • wc – Evaluation group — see find_evaluations().

  • snum – Estimate attribute — see find_attributes().

  • rselected – Row grouping, as the exact display string from find_groupings().

  • cselected – Column grouping. Optional.

  • pselected – Page grouping. Optional.

  • sdenom – Denominator attribute, to produce a ratio estimate.

  • forest_definition –

    "FIADEF" or "RPADEF". Defaults to FIADEF and is always sent explicitly — the API’s own default is RPADEF, and leaving it implicit is how two people pull “the same” number and disagree.

    Caveat, unresolved. In testing, sending FIAorRPA=FIADEF still produced the echo “RPADEF as the forest land definition.” — so the parameter may be ignored, or may need a different spelling than the documentation gives. That is why the returned forest_definition column carries the API’s echo rather than what was requested: whatever the server actually applied is what a saved frame should record. Compare the two before publishing a number that depends on the distinction.

  • str_filter – SQL-style filter passed through as strFilter.

  • max_se_pct – Flag cells whose standard error exceeds this.

  • min_plots – Flag cells resting on fewer plots than this.

  • validate_first – Check attribute/evaluation compatibility locally before sending. Turn off only to probe the API directly.

Returns:

row, column, estimate, se_pct, plots, units, unreliable, unreliable_reason, plus attribute, evaluation and forest_definition for provenance.

Return type:

pandas.DataFrame with one row per cell

Raises:
  • FIAValidationError – The request would fail upstream.

  • UpstreamError / UpstreamUnavailable – The API refused or could not be reached.

geeViz.fsInsights.validate(wc: int, snum: int) → None[source]

Check an attribute is answerable by an evaluation. Raises if not.

Attributes declare the evaluation type they need (EXPCURR, EXPVOL, EXPGROW, EXPMORT, EXPREMV, EXPCHNG, EXPDWM); evaluations advertise whether they support growth accounting. The pairing is knowable before the request leaves.

geeViz.fsInsights.reliable(df) → Any[source]

Drop cells flagged unreliable.

Separate from estimate() on purpose. The data layer returns everything with a reason attached; discarding is the caller’s decision, and a silent drop at fetch time would hide how much of a cross-tabulation is too thin to use.

exception geeViz.fsInsights.FIAValidationError[source]

Bases: ValueError

A request that would fail upstream, caught locally first.

Every check here is cheap and offline. Turning an opaque server error into a specific local one matters most for agents, where a rejected call costs a whole turn.

geeViz.fsInsights.lcms_releases(*, product: str = '', refresh: bool = False) → List[dict][source]

Every published LCMS release, newest first.

Each entry carries VersionNumber, StartYear, EndYear, Products and StudyAreas.

Releases are not interchangeable, in two ways that bite:

  • Products differ. 2025-11 carries Change / Land_Cover / Land_Use; 2025-6 is a tree-canopy release carrying only NLCD_Percent_Tree_Canopy_Cover. “The latest release” is therefore ambiguous unless you say latest of what — see latest_release().

  • Study areas differ, and not monotonically. 2024-10 covers CONUS, AK, HAWAII and PRUSVI; 2025-11 covers only CONUS and AK. Work in Hawaii or Puerto Rico has to pin an older release, which is the opposite of the usual advice.

Note also that 2022-8 reports SummaryAreaCount = 0 — it has no summary areas, so the API cannot answer area queries against it even though it lists products.

Parameters:

product – Keep only releases that publish this product.

geeViz.fsInsights.lcms_products(release: str = '', *, refresh: bool = False) → List[dict][source]

Products in a release, each with its full class list.

Currently Change, Land_Cover and Land_Use.

geeViz.fsInsights.lcms_classes(product: str, release: str = '') → Any[source]

Class table for one product — name, pixel value, and palette hex.

Returns a pandas.DataFrame with class_name, class_value and palette.

Raises:

ValueError – The release does not publish this product, naming the releases that do.

geeViz.fsInsights.lcms_summary_areas(release: str = '', *, type: str = '', refresh: bool = False) → Any[source]

The precomputed areas this API can summarize.

3,643 of them in release 2025-11: 3,137 US counties, 502 ranger districts, and four rollups (political and Forest Service, CONUS and All-Lands).

Parameters:
  • release – Release version; defaults to latest.

  • type – Filter on area type, e.g. "US-COUNTIES" or "RANGER-DISTRICTS".

geeViz.fsInsights.lcms_summary(product: str = 'Land_Cover', *, state: str = '', county: str = '', region: str = '', forest: str = '', district: str = '', year: int | None = None, startyear: int | None = None, endyear: int | None = None, geometry: Any = None, scale: int = 30, release: str = '') → Any[source]

Class areas for an area and period, as a tidy frame.

Dispatches on what it is given:

  • A named area (state/county, or region/forest/ district) goes to the LCMS API — instant, precomputed, and no Earth Engine authentication required.

  • A geometry goes to Earth Engine, computing the same class areas over an arbitrary polygon. Slower, needs EE initialized, and only available when geometry is passed.

The returned frame records which backend produced it, because the two are not guaranteed to agree to the pixel and a reader should not have to guess.

Parameters:
  • product – "Land_Cover", "Land_Use" or "Change".

  • state – State name. Required when county is given.

  • county – County name.

  • region – Forest Service units. district requires forest.

  • forest – Forest Service units. district requires forest.

  • district – Forest Service units. district requires forest.

  • year – Single year. Omit for the full time series — the whole 1985-2025 range is ~64 KB, small enough to fetch eagerly.

  • startyear – Inclusive year range.

  • endyear – Inclusive year range.

  • geometry – An ee.Geometry / ee.Feature / ee.FeatureCollection. Routes to the Earth Engine backend.

  • scale – Reduction scale in meters for the EE backend. LCMS is a 30 m product; coarsening trades accuracy for speed.

  • release – Release version; defaults to latest.

Returns:

pandas.DataFrame with year, class_name, square_meters, acres, hectares, product, area, source.

geeViz.fsInsights.lcms_vis_params(product: str, release: str = '') → dict[source]

Build geeViz/Earth Engine visualization parameters for a product.

Returns a dict with min, max, palette and classLegendDict, ready to hand to Map.addLayer:

Map.addLayer(lcms_change, fs.lcms_vis_params("Change"), "LCMS Change")

Taking the palette from the API rather than a hardcoded constant means the legend cannot drift out of sync with the release — the classes and their colors arrive from the same place as the data.

The palette is emitted as a dense, value-ordered list so it lines up with a contiguous min..``max`` range. Gaps in the class values are filled with a neutral grey rather than silently shifting every subsequent color, which is the failure mode that makes a legend look right and read wrong.

geeViz.fsInsights.latest_release(product: str = '') → str[source]

Resolve to a concrete version string, e.g. 2025-11.

Parameters:

product – Resolve to the newest release carrying this product. Without it you get the newest release overall, which may not publish what you are about to ask for — 2025-6 is newer than 2024-10 but has no Land_Cover.

Pinning matters beyond that. Caching under the key latest means a new release silently changes the answer to a question asked last year; resolving to a version once keeps an old analysis reproducible.

geeViz.fsInsights.release_products(release: str = '') → List[str][source]

Product names published by one release (default: newest overall).

geeViz.fsInsights.refresh_all(*, quiet: bool = False) → Dict[str, int][source]

Force-refresh every catalog. Returns {name: record_count}.

The escape hatch for “a new release just dropped and I do not want to wait out the TTL”. Normal use should never need it.

geeViz.fsInsights.cache_dir() → Path[source]

Directory for refreshed catalogs.

Sits alongside the workload-tag store under ~/.geeViz so geeViz keeps exactly one place where it writes user state. Overridable with GEEVIZ_FSINSIGHTS_CACHE for tests and for locked-down machines where the home directory is not writable.

exception geeViz.fsInsights.FSInsightsError[source]

Bases: RuntimeError

Base for every error raised by this subpackage.

exception geeViz.fsInsights.UpstreamError(message: str, *, url: str = '', provided: dict | None = None)[source]

Bases: FSInsightsError

The API was reached but refused or failed the request.

Carries url and, when the upstream provided one, the parameters it echoed back — LCMS returns those, and they are usually enough to see the mistake without re-reading the call site.

exception geeViz.fsInsights.UpstreamUnavailable[source]

Bases: FSInsightsError

The API could not be reached at all.

Distinct from UpstreamError because callers respond to it differently: a bad parameter needs a code change, an unreachable host needs a retry later or a fall back to cached data.

Modules

align

Putting LCMS and FIA side by side, without pretending they agree.

fia

FIADB-API client — Forest Inventory and Analysis estimates.

lcms

LCMS API client — Landscape Change Monitoring System.

lcms_ee

Earth Engine access to the LCMS products.

vocab

Cached, searchable vocabularies for the FIA and LCMS APIs.