Skip to content

Remeasurement Panels

Create t1/t2 linked panel datasets from FIA remeasurement data for harvest analysis, growth tracking, and change detection.

Overview

The panel() function creates linked datasets where each row represents a measurement pair:

  • t1 (time 1): Previous measurement
  • t2 (time 2): Current measurement

This panel data is essential for:

  • Harvest probability modeling
  • Forest change detection
  • Growth and mortality analysis
  • Land use transition studies
import pyfia

db = pyfia.FIA("data/nc.duckdb")
db.clip_by_state("NC")

# Condition-level panel for harvest analysis
cond_panel = pyfia.panel(db, level="condition", land_type="timber")
print(f"Harvest rate: {cond_panel['HARVEST'].mean():.1%}")

# Tree-level panel for mortality/cut analysis
tree_panel = pyfia.panel(db, level="tree")
print(tree_panel.group_by("TREE_FATE").len())

Function Reference

panel

panel(db: str | FIA, level: Literal['condition', 'tree'] = 'condition', columns: list[str] | None = None, land_type: str = 'forest', tree_type: str = 'gs', tree_domain: str | None = None, area_domain: str | None = None, expand_chains: bool = True, min_remper: float = 0, max_remper: float | None = None, min_invyr: int = 2000, harvest_only: bool = False, expand: bool = False, measure: Literal['tpa', 'volume'] = 'tpa', grp_by: list[str] | None = None, by_fate: bool = False) -> DataFrame

Create a t1/t2 remeasurement panel from FIA data.

Returns a DataFrame where each row represents a measurement pair: - t1 (time 1): Previous measurement - t2 (time 2): Current measurement

This panel data is useful for: - Harvest probability modeling - Forest change detection - Growth and mortality analysis - Land use transition studies

Tree-level panels use GRM (Growth-Removal-Mortality) tables for authoritative tree fate classification, providing consistent definitions aligned with FIA's official estimation methodology.

PARAMETER DESCRIPTION
db

Database connection or path to FIA database.

TYPE: str | FIA

level

Level of panel to create: - 'condition': Condition-level panel for area/harvest analysis. Each row is a condition measured at two time points. - 'tree': Tree-level panel for individual tree tracking. Each row is a tree with GRM component classification.

TYPE: ('condition', 'tree') DEFAULT: 'condition'

columns

Additional columns to include beyond defaults. Useful for adding specific attributes needed for analysis.

TYPE: list of str DEFAULT: None

land_type

Land classification filter: - 'forest': All forest land (COND_STATUS_CD = 1) - 'timber': Timberland (productive, unreserved forest) - 'all': No land type filtering

TYPE: ('forest', 'timber', 'all') DEFAULT: 'forest'

tree_type

Tree type filter (tree-level only). Maps to GRM column suffixes: - 'gs': Growing stock (merchantable trees) - uses GS columns - 'all': All trees - uses GS columns (default GRM behavior) - 'live': All live trees - uses AL columns

TYPE: ('all', 'live', 'gs') DEFAULT: 'all'

tree_domain

SQL-like filter expression for tree-level filtering. Example: "SPCD == 131" (loblolly pine only)

TYPE: str DEFAULT: None

area_domain

SQL-like filter expression for condition-level filtering. Example: "OWNGRPCD == 40" (private land only)

TYPE: str DEFAULT: None

expand_chains

If True and multiple remeasurements exist (t1->t2->t3), creates pairs (t1,t2) and (t2,t3). If False, only returns the most recent pair for each location.

TYPE: bool DEFAULT: True

min_remper

Minimum remeasurement period in years. Filters out pairs with shorter intervals.

TYPE: float DEFAULT: 0

max_remper

Maximum remeasurement period in years. Filters out pairs with longer intervals.

TYPE: float DEFAULT: None

min_invyr

Minimum inventory year for t2 (current measurement). Defaults to 2000 to use only the enhanced annual inventory methodology. FIA transitioned from periodic to annual inventory around 1999-2000, with significant methodology changes. Set to None or 0 to include all years.

TYPE: int DEFAULT: 2000

harvest_only

If True, return only records where harvest was detected. For condition-level: uses TRTCD treatment codes. For tree-level: returns trees with TREE_FATE in ['cut', 'diversion'].

TYPE: bool DEFAULT: False

expand

If True, apply expansion factors to produce per-acre estimates comparable to removals(). Requires level='tree'. Uses three-layer expansion: - TPA_UNADJ: Base trees-per-acre - ADJ_FACTOR: Plot-type adjustment (subplot/microplot/macroplot) - EXPNS: Stratum expansion to total acres When expand=True, returns aggregated per-acre estimates instead of tree-level data.

TYPE: bool DEFAULT: False

measure

Measure to expand (only used when expand=True): - 'tpa': Trees per acre - 'volume': Cubic foot volume per acre

TYPE: ('tpa', 'volume') DEFAULT: 'tpa'

grp_by

Grouping columns for expanded estimates (only used when expand=True). Example: ['SPCD'] for by-species estimates.

TYPE: list of str DEFAULT: None

by_fate

If True and expand=True, include TREE_FATE in grouping columns to get separate estimates for survivors, mortality, cut, etc.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
DataFrame

Panel dataset with columns:

For condition-level: - PLT_CN: Current plot control number - PREV_PLT_CN: Previous plot control number - CONDID: Condition identifier - STATECD, COUNTYCD: Geographic identifiers - INVYR: Current inventory year - REMPER: Remeasurement period (years) - HARVEST: Harvest indicator (1=harvest detected, 0=no harvest) - t1_/t2_: Attributes at time 1 and time 2

For tree-level: - PLT_CN: Plot control number - TRE_CN: Tree control number - TREE_FATE: Tree fate from GRM classification: - 'survivor': Tree alive at both measurements - 'mortality': Tree died naturally - 'cut': Tree removed by harvest - 'diversion': Tree removed due to land use change - 'ingrowth': New tree crossing size threshold - COMPONENT: Raw GRM component (SURVIVOR, CUT1, etc.) - DIA_BEGIN, DIA_MIDPT, DIA_END: Diameter measurements - TPA_UNADJ: Trees per acre expansion factor - t1_/t2_: Tree attributes at time 1 and time 2

See Also

removals : Estimate harvest removals (uses same GRM methodology) mortality : Estimate tree mortality growth : Estimate forest growth area_change : Estimate forest area change

Examples:

Basic condition-level panel for harvest analysis:

>>> from pyfia import FIA, panel
>>> with FIA("path/to/db.duckdb") as db:
...     db.clip_by_state(37)  # North Carolina
...     data = panel(db, level="condition", land_type="timber")
...     print(f"Panel has {len(data)} condition pairs")
...     print(f"Harvest rate: {data['HARVEST'].mean():.1%}")

Tree-level panel with GRM-based fate classification:

>>> with FIA("path/to/db.duckdb") as db:
...     db.clip_by_state(37)
...     trees = panel(db, level="tree", tree_type="gs")
...     fate_counts = trees.group_by("TREE_FATE").len()
...     print(fate_counts)

Get all removals (cut + diversion):

>>> data = panel(
...     db,
...     level="tree",
...     harvest_only=True,  # Returns cut and diversion trees
... )

Filter remeasurement period to 4-8 years:

>>> data = panel(
...     db,
...     level="condition",
...     min_remper=4,
...     max_remper=8,
... )
Notes

Tree-level panels use GRM (Growth-Removal-Mortality) tables:

Tree fate is determined from TREE_GRM_COMPONENT, which provides authoritative classification pre-computed by FIA. This ensures consistency with the removals() function and EVALIDator.

GRM component types: - SURVIVOR: Tree alive at beginning and end of period - MORTALITY1/2: Tree died during measurement period - CUT1/2: Tree harvested during measurement period - DIVERSION1/2: Tree removed due to land use change - INGROWTH: New tree crossing 5" DBH threshold

Condition-level harvest detection uses treatment codes (TRTCD): - 10 = Cutting (harvest) - 20 = Site preparation (implies prior harvest)

Remeasurement availability varies by region: - Southern states typically have 5-7 year remeasurement cycles - Western states may have 10-year cycles - Some plots have 3+ remeasurements (t1->t2->t3)

References

Bechtold & Patterson (2005), "The Enhanced Forest Inventory and Analysis Program", Chapter 4: Change Estimation.

FIA Database User Guide, TREE_GRM_COMPONENT table documentation.

Source code in src/pyfia/estimation/estimators/panel.py
def panel(
    db: str | FIA,
    level: Literal["condition", "tree"] = "condition",
    columns: list[str] | None = None,
    land_type: str = "forest",
    tree_type: str = "gs",
    tree_domain: str | None = None,
    area_domain: str | None = None,
    expand_chains: bool = True,
    min_remper: float = 0,
    max_remper: float | None = None,
    min_invyr: int = 2000,
    harvest_only: bool = False,
    expand: bool = False,
    measure: Literal["tpa", "volume"] = "tpa",
    grp_by: list[str] | None = None,
    by_fate: bool = False,
) -> pl.DataFrame:
    """
    Create a t1/t2 remeasurement panel from FIA data.

    Returns a DataFrame where each row represents a measurement pair:
    - t1 (time 1): Previous measurement
    - t2 (time 2): Current measurement

    This panel data is useful for:
    - Harvest probability modeling
    - Forest change detection
    - Growth and mortality analysis
    - Land use transition studies

    Tree-level panels use GRM (Growth-Removal-Mortality) tables for
    authoritative tree fate classification, providing consistent
    definitions aligned with FIA's official estimation methodology.

    Parameters
    ----------
    db : str | FIA
        Database connection or path to FIA database.
    level : {'condition', 'tree'}, default 'condition'
        Level of panel to create:
        - 'condition': Condition-level panel for area/harvest analysis.
          Each row is a condition measured at two time points.
        - 'tree': Tree-level panel for individual tree tracking.
          Each row is a tree with GRM component classification.
    columns : list of str, optional
        Additional columns to include beyond defaults. Useful for adding
        specific attributes needed for analysis.
    land_type : {'forest', 'timber', 'all'}, default 'forest'
        Land classification filter:
        - 'forest': All forest land (COND_STATUS_CD = 1)
        - 'timber': Timberland (productive, unreserved forest)
        - 'all': No land type filtering
    tree_type : {'all', 'live', 'gs'}, default 'gs'
        Tree type filter (tree-level only). Maps to GRM column suffixes:
        - 'gs': Growing stock (merchantable trees) - uses GS columns
        - 'all': All trees - uses GS columns (default GRM behavior)
        - 'live': All live trees - uses AL columns
    tree_domain : str, optional
        SQL-like filter expression for tree-level filtering.
        Example: "SPCD == 131" (loblolly pine only)
    area_domain : str, optional
        SQL-like filter expression for condition-level filtering.
        Example: "OWNGRPCD == 40" (private land only)
    expand_chains : bool, default True
        If True and multiple remeasurements exist (t1->t2->t3),
        creates pairs (t1,t2) and (t2,t3). If False, only returns
        the most recent pair for each location.
    min_remper : float, default 0
        Minimum remeasurement period in years. Filters out pairs
        with shorter intervals.
    max_remper : float, optional
        Maximum remeasurement period in years. Filters out pairs
        with longer intervals.
    min_invyr : int, default 2000
        Minimum inventory year for t2 (current measurement). Defaults to 2000
        to use only the enhanced annual inventory methodology. FIA transitioned
        from periodic to annual inventory around 1999-2000, with significant
        methodology changes. Set to None or 0 to include all years.
    harvest_only : bool, default False
        If True, return only records where harvest was detected.
        For condition-level: uses TRTCD treatment codes.
        For tree-level: returns trees with TREE_FATE in ['cut', 'diversion'].
    expand : bool, default False
        If True, apply expansion factors to produce per-acre estimates
        comparable to removals(). Requires level='tree'.
        Uses three-layer expansion:
        - TPA_UNADJ: Base trees-per-acre
        - ADJ_FACTOR: Plot-type adjustment (subplot/microplot/macroplot)
        - EXPNS: Stratum expansion to total acres
        When expand=True, returns aggregated per-acre estimates instead of
        tree-level data.
    measure : {'tpa', 'volume'}, default 'tpa'
        Measure to expand (only used when expand=True):
        - 'tpa': Trees per acre
        - 'volume': Cubic foot volume per acre
    grp_by : list of str, optional
        Grouping columns for expanded estimates (only used when expand=True).
        Example: ['SPCD'] for by-species estimates.
    by_fate : bool, default False
        If True and expand=True, include TREE_FATE in grouping columns
        to get separate estimates for survivors, mortality, cut, etc.

    Returns
    -------
    pl.DataFrame
        Panel dataset with columns:

        For condition-level:
        - PLT_CN: Current plot control number
        - PREV_PLT_CN: Previous plot control number
        - CONDID: Condition identifier
        - STATECD, COUNTYCD: Geographic identifiers
        - INVYR: Current inventory year
        - REMPER: Remeasurement period (years)
        - HARVEST: Harvest indicator (1=harvest detected, 0=no harvest)
        - t1_*/t2_*: Attributes at time 1 and time 2

        For tree-level:
        - PLT_CN: Plot control number
        - TRE_CN: Tree control number
        - TREE_FATE: Tree fate from GRM classification:
          - 'survivor': Tree alive at both measurements
          - 'mortality': Tree died naturally
          - 'cut': Tree removed by harvest
          - 'diversion': Tree removed due to land use change
          - 'ingrowth': New tree crossing size threshold
        - COMPONENT: Raw GRM component (SURVIVOR, CUT1, etc.)
        - DIA_BEGIN, DIA_MIDPT, DIA_END: Diameter measurements
        - TPA_UNADJ: Trees per acre expansion factor
        - t1_*/t2_*: Tree attributes at time 1 and time 2

    See Also
    --------
    removals : Estimate harvest removals (uses same GRM methodology)
    mortality : Estimate tree mortality
    growth : Estimate forest growth
    area_change : Estimate forest area change

    Examples
    --------
    Basic condition-level panel for harvest analysis:

    >>> from pyfia import FIA, panel
    >>> with FIA("path/to/db.duckdb") as db:
    ...     db.clip_by_state(37)  # North Carolina
    ...     data = panel(db, level="condition", land_type="timber")
    ...     print(f"Panel has {len(data)} condition pairs")
    ...     print(f"Harvest rate: {data['HARVEST'].mean():.1%}")

    Tree-level panel with GRM-based fate classification:

    >>> with FIA("path/to/db.duckdb") as db:
    ...     db.clip_by_state(37)
    ...     trees = panel(db, level="tree", tree_type="gs")
    ...     fate_counts = trees.group_by("TREE_FATE").len()
    ...     print(fate_counts)

    Get all removals (cut + diversion):

    >>> data = panel(
    ...     db,
    ...     level="tree",
    ...     harvest_only=True,  # Returns cut and diversion trees
    ... )

    Filter remeasurement period to 4-8 years:

    >>> data = panel(
    ...     db,
    ...     level="condition",
    ...     min_remper=4,
    ...     max_remper=8,
    ... )

    Notes
    -----
    Tree-level panels use GRM (Growth-Removal-Mortality) tables:

    Tree fate is determined from TREE_GRM_COMPONENT, which provides
    authoritative classification pre-computed by FIA. This ensures
    consistency with the `removals()` function and EVALIDator.

    GRM component types:
    - SURVIVOR: Tree alive at beginning and end of period
    - MORTALITY1/2: Tree died during measurement period
    - CUT1/2: Tree harvested during measurement period
    - DIVERSION1/2: Tree removed due to land use change
    - INGROWTH: New tree crossing 5" DBH threshold

    Condition-level harvest detection uses treatment codes (TRTCD):
    - 10 = Cutting (harvest)
    - 20 = Site preparation (implies prior harvest)

    Remeasurement availability varies by region:
    - Southern states typically have 5-7 year remeasurement cycles
    - Western states may have 10-year cycles
    - Some plots have 3+ remeasurements (t1->t2->t3)

    References
    ----------
    Bechtold & Patterson (2005), "The Enhanced Forest Inventory and
    Analysis Program", Chapter 4: Change Estimation.

    FIA Database User Guide, TREE_GRM_COMPONENT table documentation.
    """
    # Validate inputs
    if level not in ("condition", "tree"):
        raise ValueError(f"Invalid level '{level}'. Must be 'condition' or 'tree'")

    land_type = validate_land_type(land_type)

    if tree_type not in ("all", "live", "gs"):
        raise ValueError(
            f"Invalid tree_type '{tree_type}'. Must be 'all', 'live', or 'gs'"
        )

    tree_domain = validate_domain_expression(tree_domain, "tree_domain")
    area_domain = validate_domain_expression(area_domain, "area_domain")
    expand_chains = validate_boolean(expand_chains, "expand_chains")
    harvest_only = validate_boolean(harvest_only, "harvest_only")
    expand = validate_boolean(expand, "expand")
    by_fate = validate_boolean(by_fate, "by_fate")

    if expand and level != "tree":
        raise ValueError("expand=True requires level='tree'")

    if measure not in ("tpa", "volume"):
        raise ValueError(f"Invalid measure '{measure}'. Must be 'tpa' or 'volume'")

    if min_remper < 0:
        raise ValueError(f"min_remper must be non-negative, got {min_remper}")
    if max_remper is not None and max_remper < min_remper:
        raise ValueError(
            f"max_remper ({max_remper}) must be >= min_remper ({min_remper})"
        )
    if min_invyr is not None and min_invyr < 0:
        raise ValueError(f"min_invyr must be non-negative, got {min_invyr}")

    # Handle database connection - convert path string to FIA instance
    if isinstance(db, str):
        db = FIA(db)

    # Build config
    config = {
        "level": level,
        "columns": columns or [],
        "land_type": land_type,
        "tree_type": tree_type,
        "tree_domain": tree_domain,
        "area_domain": area_domain,
        "expand_chains": expand_chains,
        "min_remper": min_remper,
        "max_remper": max_remper,
        "min_invyr": min_invyr,
        "harvest_only": harvest_only,
        "expand": expand,
        "measure": measure,
        "grp_by": grp_by or [],
        "by_fate": by_fate,
    }

    builder = PanelBuilder(db, config)
    return builder.build()

Panel Levels

Condition-Level (level="condition")

Each row represents a forest condition measured at two time points. Best for:

  • Area-based harvest probability models
  • Land use change analysis
  • Stand-level attribute tracking

Key columns:

Column Description
PLT_CN Current plot control number
PREV_PLT_CN Previous plot control number
CONDID Condition identifier
INVYR Current inventory year
REMPER Remeasurement period (years)
HARVEST Harvest indicator (1=detected, 0=no)
t1_* / t2_* Attributes at time 1 and time 2

Tree-Level (level="tree")

Each row represents an individual tree measured at two time points. Best for:

  • Individual tree fate analysis
  • Species-specific harvest patterns
  • Mortality vs. harvest separation

Key columns:

Column Description
TRE_CN Current tree control number
PREV_TRE_CN Previous tree control number
TREE_FATE Tree fate classification
t1_* / t2_* Tree attributes at time 1 and time 2

Tree Fate Values:

Fate Description GRM Component
survivor Live at t1 and t2 SURVIVOR
mortality Live at t1, dead at t2 (natural causes) MORTALITY1, MORTALITY2
cut Live at t1, removed/harvested at t2 CUT1, CUT2
diversion Removed due to land use change DIVERSION1, DIVERSION2
ingrowth New tree crossing 5" DBH threshold INGROWTH
other Unrecognized component type -

Harvest Detection

Condition-Level Detection

Harvest is detected using TRTCD (treatment code) fields:

  • TRTCD = 10: Cutting (harvest)
  • TRTCD = 20: Site preparation (implies prior harvest)
# Get harvested conditions
harvested = pyfia.panel(db, level="condition", harvest_only=True)
print(f"Harvested conditions: {len(harvested)}")

Tree-Level Detection (GRM-Based)

Tree-level panels use FIA's GRM (Growth-Removal-Mortality) tables for authoritative fate classification. The TREE_GRM_COMPONENT table provides pre-computed classifications that align with FIA's official estimation methodology:

# Tree fate comes directly from GRM COMPONENT classification
tree_panel = pyfia.panel(db, level="tree")
cut_trees = tree_panel.filter(pl.col("TREE_FATE") == "cut")

# Get all removals (cut + diversion)
removals = pyfia.panel(db, level="tree", harvest_only=True)

This ensures consistency with the removals() function and EVALIDator results.

Examples

Basic Harvest Analysis

import pyfia
import polars as pl

db = pyfia.FIA("data/nc.duckdb")
db.clip_by_state("NC")

# Condition-level harvest rates
panel = pyfia.panel(db, level="condition", land_type="forest")

# Overall harvest rate
harvest_rate = panel["HARVEST"].mean()
remper = panel["REMPER"].mean()
annual_rate = 1 - (1 - harvest_rate) ** (1 / remper)

print(f"Period harvest rate: {harvest_rate:.1%}")
print(f"Annualized rate: {annual_rate:.2%}/year")

Harvest by Ownership

panel = pyfia.panel(db, level="condition")

harvest_by_owner = (
    panel
    .group_by("t2_OWNGRPCD")
    .agg([
        pl.len().alias("n_conditions"),
        pl.col("HARVEST").mean().alias("harvest_rate")
    ])
    .sort("t2_OWNGRPCD")
)

# OWNGRPCD: 10=USFS, 20=Other Fed, 30=State/Local, 40=Private
print(harvest_by_owner)

Tree-Level Cut Analysis

# Get all trees with fate information
tree_panel = pyfia.panel(db, level="tree", tree_type="all")

# Tree fate distribution
fate_dist = tree_panel.group_by("TREE_FATE").len()
print(fate_dist)

# Cut trees only
cut_trees = pyfia.panel(db, level="tree", harvest_only=True)

# Top species cut
species_cut = (
    cut_trees
    .group_by("t1_SPCD")
    .agg([
        pl.len().alias("n_trees"),
        pl.col("t1_DIA").mean().alias("avg_dia")
    ])
    .sort("n_trees", descending=True)
    .head(10)
)
print(species_cut)

Filtering Options

# Timberland only (productive, unreserved forest)
timber_panel = pyfia.panel(db, level="condition", land_type="timber")

# Private land only
private_panel = pyfia.panel(
    db,
    level="condition",
    area_domain="OWNGRPCD == 40"
)

# Specific remeasurement period range
panel_5_10yr = pyfia.panel(
    db,
    level="condition",
    min_remper=5,
    max_remper=10
)

# Loblolly pine only (tree-level)
loblolly_panel = pyfia.panel(
    db,
    level="tree",
    tree_domain="SPCD == 131"
)

Multi-Period Chain Analysis

# Get panel with chain expansion (default)
panel = pyfia.panel(db, level="condition", expand_chains=True)

# Count plots by number of measurement periods
chain_lengths = (
    panel
    .group_by("PLT_CN")
    .len()
    .group_by("len")
    .len()
    .rename({"len": "periods", "len_right": "n_plots"})
    .sort("periods")
)
print("Plots by chain length:")
print(chain_lengths)

Expansion to Per-Acre Estimates

Tree-level panels can be expanded to produce per-acre estimates comparable to removals(). This applies the same three-layer expansion methodology:

  1. TPA_UNADJ: Base trees-per-acre from plot design
  2. ADJ_FACTOR: Plot-type adjustment (subplot/microplot/macroplot)
  3. EXPNS: Stratum expansion to total acres

Basic Expansion

# Get per-acre removals estimates (comparable to removals())
expanded = pyfia.panel(
    db,
    level="tree",
    harvest_only=True,
    expand=True,
    measure="tpa"  # Trees per acre
)
print(f"Total removals: {expanded['PANEL_TOTAL'][0]:,.0f} trees/year")
print(f"Per acre: {expanded['PANEL_ACRE'][0]:,.1f} trees/acre/year")

Expansion by Species

# Removals by species
by_species = pyfia.panel(
    db,
    level="tree",
    harvest_only=True,
    expand=True,
    measure="volume",
    grp_by=["SPCD"]
)
print(by_species.sort("PANEL_TOTAL", descending=True).head(10))

Expansion by Tree Fate

# Compare survivor, mortality, cut volumes
by_fate = pyfia.panel(
    db,
    level="tree",
    expand=True,
    measure="volume",
    by_fate=True  # Groups by TREE_FATE automatically
)
print(by_fate)

Expansion Parameters

Parameter Type Default Description
expand bool False Apply expansion factors for per-acre estimates
measure str "tpa" Measure to expand: "tpa" or "volume"
grp_by list None Grouping columns (e.g., ["SPCD"])
by_fate bool False Include TREE_FATE in grouping

Note: expand=True requires level="tree" and returns aggregated estimates instead of tree-level data.

Harvest Transition Analysis

import polars as pl

panel = pyfia.panel(db, level="condition")

# Find plots with multiple periods
multi_period = panel.filter(
    pl.col("PLT_CN").is_in(
        panel.group_by("PLT_CN").len().filter(pl.col("len") > 1)["PLT_CN"]
    )
).sort(["PLT_CN", "INVYR"])

# Calculate harvest transitions
transitions = (
    multi_period
    .with_columns([
        pl.col("HARVEST").shift(1).over("PLT_CN").alias("PREV_HARVEST")
    ])
    .filter(pl.col("PREV_HARVEST").is_not_null())
    .group_by(["PREV_HARVEST", "HARVEST"])
    .len()
)
print("Harvest transitions:")
print(transitions)

Technical Notes

Inventory Year Filter (min_invyr)

FIA transitioned from periodic to annual inventory methodology around 1999-2000. By default, min_invyr=2000 ensures only post-transition data is used, providing:

  • Consistent methodology across the panel
  • Annual rather than periodic measurements
  • Better tree tracking via PREV_TRE_CN

Set min_invyr=0 to include historical data if needed.

Chain Expansion (expand_chains)

When expand_chains=True (default), plots with multiple remeasurements (t1→t2→t3) generate all consecutive pairs:

  • (t1, t2) and (t2, t3)

This maximizes data utilization for transition modeling.

Data Sources

  • PLOT table: PREV_PLT_CN, REMPER for plot linking
  • COND table: TRTCD1-3 for harvest detection
  • TREE table: PREV_TRE_CN, STATUSCD for tree tracking

See Also