Skip to content

EVALIDator API

Client for validating PyFIA estimates against official USFS values.

Overview

EVALIDator is the USFS online tool for FIA estimates. PyFIA includes a client to fetch official estimates for validation.

from pyfia import EVALIDatorClient, validate_pyfia_estimate

# Get official estimate
client = EVALIDatorClient()
official = client.get_forest_area("GA", 2022)

# Compare with PyFIA
validation = validate_pyfia_estimate(pyfia_result, official)
print(f"Difference: {validation.percent_difference:.2f}%")

Client Class

EVALIDatorClient

EVALIDatorClient(timeout: int = 30)

Client for the USFS EVALIDator API.

This client provides methods to retrieve official FIA population estimates for comparison with pyFIA calculations.

PARAMETER DESCRIPTION
timeout

Request timeout in seconds. Default is 30.

TYPE: int DEFAULT: 30

Example

client = EVALIDatorClient() result = client.get_forest_area(state_code=37, year=2023) print(f"Forest area: {result.estimate:,.0f} acres (SE: {result.sampling_error_pct:.1f}%)")

Source code in src/pyfia/evalidator/client.py
def __init__(self, timeout: int = 30):
    self.timeout = timeout
    self.session = requests.Session()
    self.session.headers.update({"User-Agent": "pyFIA/1.0 (validation client)"})

get_forest_area

get_forest_area(state_code: int, year: int, land_type: str = 'forest') -> EVALIDatorEstimate

Get forest area estimate from EVALIDator.

PARAMETER DESCRIPTION
state_code

State FIPS code (e.g., 37 for North Carolina)

TYPE: int

year

Inventory year (e.g., 2023)

TYPE: int

land_type

"forest" for all forestland, "timber" for timberland only

TYPE: str DEFAULT: 'forest'

RETURNS DESCRIPTION
EVALIDatorEstimate

Official estimate with sampling error

Source code in src/pyfia/evalidator/client.py
def get_forest_area(
    self, state_code: int, year: int, land_type: str = "forest"
) -> EVALIDatorEstimate:
    """
    Get forest area estimate from EVALIDator.

    Parameters
    ----------
    state_code : int
        State FIPS code (e.g., 37 for North Carolina)
    year : int
        Inventory year (e.g., 2023)
    land_type : str
        "forest" for all forestland, "timber" for timberland only

    Returns
    -------
    EVALIDatorEstimate
        Official estimate with sampling error
    """
    snum = (
        EstimateType.AREA_TIMBERLAND
        if land_type == "timber"
        else EstimateType.AREA_FOREST
    )

    data = self._make_request(snum=snum, state_code=state_code, year=year)

    return self._parse_njson_response(
        data=data,
        snum=snum,
        state_code=state_code,
        year=year,
        units="acres",
        estimate_type=f"{land_type}_area",
    )

get_volume

get_volume(state_code: int, year: int, vol_type: str = 'net') -> EVALIDatorEstimate

Get volume estimate from EVALIDator.

PARAMETER DESCRIPTION
state_code

State FIPS code

TYPE: int

year

Inventory year

TYPE: int

vol_type

Volume type: "net" for net merchantable, "sawlog" for board feet

TYPE: str DEFAULT: 'net'

RETURNS DESCRIPTION
EVALIDatorEstimate

Official volume estimate

Source code in src/pyfia/evalidator/client.py
def get_volume(
    self, state_code: int, year: int, vol_type: str = "net"
) -> EVALIDatorEstimate:
    """
    Get volume estimate from EVALIDator.

    Parameters
    ----------
    state_code : int
        State FIPS code
    year : int
        Inventory year
    vol_type : str
        Volume type: "net" for net merchantable, "sawlog" for board feet

    Returns
    -------
    EVALIDatorEstimate
        Official volume estimate
    """
    snum_map = {
        "net": EstimateType.VOLUME_NET_GROWINGSTOCK,
        "sawlog": EstimateType.VOLUME_SAWLOG_INTERNATIONAL,
    }
    snum = snum_map.get(vol_type, EstimateType.VOLUME_NET_GROWINGSTOCK)
    units = "board_feet" if vol_type == "sawlog" else "cubic_feet"

    data = self._make_request(snum=snum, state_code=state_code, year=year)

    return self._parse_njson_response(
        data=data,
        snum=snum,
        state_code=state_code,
        year=year,
        units=units,
        estimate_type=f"volume_{vol_type}",
    )

get_biomass

get_biomass(state_code: int, year: int, component: str = 'ag', min_diameter: float = 0.0) -> EVALIDatorEstimate

Get biomass estimate from EVALIDator.

PARAMETER DESCRIPTION
state_code

State FIPS code

TYPE: int

year

Inventory year

TYPE: int

component

"ag" for aboveground, "bg" for belowground, "total" for both

TYPE: str DEFAULT: 'ag'

min_diameter

Minimum DBH threshold. Use 0.0 for all trees, 5.0 for trees >=5" DBH.

TYPE: float DEFAULT: 0.0

RETURNS DESCRIPTION
EVALIDatorEstimate

Official biomass estimate in dry short tons

Source code in src/pyfia/evalidator/client.py
def get_biomass(
    self,
    state_code: int,
    year: int,
    component: str = "ag",
    min_diameter: float = 0.0,
) -> EVALIDatorEstimate:
    """
    Get biomass estimate from EVALIDator.

    Parameters
    ----------
    state_code : int
        State FIPS code
    year : int
        Inventory year
    component : str
        "ag" for aboveground, "bg" for belowground, "total" for both
    min_diameter : float, default 0.0
        Minimum DBH threshold. Use 0.0 for all trees, 5.0 for trees >=5" DBH.

    Returns
    -------
    EVALIDatorEstimate
        Official biomass estimate in dry short tons
    """
    # Select snum based on component and diameter threshold
    if min_diameter >= 5.0:
        snum_map = {
            "ag": EstimateType.BIOMASS_AG_LIVE_5INCH,
            "bg": EstimateType.BIOMASS_BG_LIVE_5INCH,
        }
    else:
        snum_map = {
            "ag": EstimateType.BIOMASS_AG_LIVE,
            "bg": EstimateType.BIOMASS_BG_LIVE,
        }
    snum = snum_map.get(component, snum_map["ag"])

    data = self._make_request(snum=snum, state_code=state_code, year=year)

    return self._parse_njson_response(
        data=data,
        snum=snum,
        state_code=state_code,
        year=year,
        units="dry_short_tons",
        estimate_type=f"biomass_{component}",
    )

get_tree_count

get_tree_count(state_code: int, year: int, min_diameter: float = 1.0, land_type: str = 'forest') -> EVALIDatorEstimate

Get tree count estimate from EVALIDator.

PARAMETER DESCRIPTION
state_code

State FIPS code

TYPE: int

year

Inventory year

TYPE: int

min_diameter

Minimum DBH in inches (1.0 or 5.0). Note: 5" threshold returns growing-stock trees (TREECLCD=2) only, while 1" threshold returns all live trees.

TYPE: float DEFAULT: 1.0

land_type

"forest" for forest land, "timber" for timberland only

TYPE: str DEFAULT: 'forest'

RETURNS DESCRIPTION
EVALIDatorEstimate

Official tree count estimate

Notes

snum values used: - snum=4: Live trees >=1" d.b.h. on forest land (all tree classes) - snum=5: Growing-stock trees >=5" d.b.h. on forest land (TREECLCD=2) - snum=7: Live trees >=1" d.b.h. on timberland (all tree classes) - snum=8: Growing-stock trees >=5" d.b.h. on timberland (TREECLCD=2)

Source code in src/pyfia/evalidator/client.py
def get_tree_count(
    self,
    state_code: int,
    year: int,
    min_diameter: float = 1.0,
    land_type: str = "forest",
) -> EVALIDatorEstimate:
    """
    Get tree count estimate from EVALIDator.

    Parameters
    ----------
    state_code : int
        State FIPS code
    year : int
        Inventory year
    min_diameter : float
        Minimum DBH in inches (1.0 or 5.0).
        Note: 5" threshold returns growing-stock trees (TREECLCD=2) only,
        while 1" threshold returns all live trees.
    land_type : str
        "forest" for forest land, "timber" for timberland only

    Returns
    -------
    EVALIDatorEstimate
        Official tree count estimate

    Notes
    -----
    snum values used:
    - snum=4: Live trees >=1" d.b.h. on forest land (all tree classes)
    - snum=5: Growing-stock trees >=5" d.b.h. on forest land (TREECLCD=2)
    - snum=7: Live trees >=1" d.b.h. on timberland (all tree classes)
    - snum=8: Growing-stock trees >=5" d.b.h. on timberland (TREECLCD=2)
    """
    if land_type == "timber":
        snum = (
            EstimateType.TREE_COUNT_5INCH_TIMBER
            if min_diameter >= 5.0
            else EstimateType.TREE_COUNT_1INCH_TIMBER
        )
    else:
        snum = (
            EstimateType.TREE_COUNT_5INCH_FOREST
            if min_diameter >= 5.0
            else EstimateType.TREE_COUNT_1INCH_FOREST
        )

    data = self._make_request(snum=snum, state_code=state_code, year=year)

    return self._parse_njson_response(
        data=data,
        snum=snum,
        state_code=state_code,
        year=year,
        units="trees",
        estimate_type=f"tree_count_{int(min_diameter)}inch_{land_type}",
    )

Data Classes

EVALIDatorEstimate

Container for EVALIDator API results.

EVALIDatorEstimate dataclass

EVALIDatorEstimate(estimate: float, sampling_error: float, sampling_error_pct: float, variance: float, plot_count: int, units: str, estimate_type: str, state_code: int, year: int, grouping: Optional[Dict[str, Any]] = None, raw_response: Optional[Dict[str, Any]] = None)

Container for an EVALIDator estimate result.

ValidationResult

Container for validation comparison results.

ValidationResult dataclass

ValidationResult(pyfia_estimate: float, pyfia_se: float, evalidator_estimate: float, evalidator_se: float, evalidator_variance: float, evalidator_plot_count: int, pyfia_plot_count: Optional[int], absolute_diff: float, pct_diff: float, within_1se: bool, within_2se: bool, estimate_type: str, state_code: int, year: int, passed: bool, message: str)

Result of comparing pyFIA estimate with EVALIDator.

Estimate Types

EstimateType

Predefined constants for EVALIDator estimate types.

EstimateType

EVALIDator estimate type codes (snum parameter values).

Categories:

Category Constants
Area AREA_FOREST, AREA_TIMBERLAND, AREA_SAMPLED
Volume VOLUME_NET_GROWINGSTOCK, VOLUME_NET_ALLSPECIES, VOLUME_SAWLOG_*
Biomass BIOMASS_AG_LIVE, BIOMASS_BG_LIVE, BIOMASS_AG_LIVE_5INCH
Carbon CARBON_AG_LIVE, CARBON_TOTAL_LIVE, CARBON_POOL_*
Change GROWTH_NET_VOLUME, REMOVALS_VOLUME, MORTALITY_VOLUME

Validation Functions

validate_pyfia_estimate

validate_pyfia_estimate

validate_pyfia_estimate(pyfia_result, state_code: int, year: int, estimate_type: str, client: Optional[EVALIDatorClient] = None, **kwargs) -> ValidationResult

Validate a pyFIA estimate against EVALIDator.

This is a convenience function that fetches the EVALIDator estimate and performs the comparison in one step.

PARAMETER DESCRIPTION
pyfia_result

pyFIA estimation result DataFrame with estimate and SE columns

TYPE: DataFrame

state_code

State FIPS code

TYPE: int

year

Inventory year

TYPE: int

estimate_type

Type of estimate: "area", "volume", "biomass", "carbon", "tpa"

TYPE: str

client

Existing client instance (creates new one if not provided)

TYPE: EVALIDatorClient DEFAULT: None

**kwargs

Additional arguments passed to the EVALIDator API call

DEFAULT: {}

RETURNS DESCRIPTION
ValidationResult

Comparison result

Example

from pyfia import FIA, area from pyfia.evalidator import validate_pyfia_estimate

with FIA("path/to/db.duckdb") as db: ... db.clip_by_state(37) ... db.clip_most_recent(eval_type="EXPALL") ... result = area(db, land_type="forest")

validation = validate_pyfia_estimate( ... result, state_code=37, year=2023, estimate_type="area" ... ) print(validation.message)

Source code in src/pyfia/evalidator/validation.py
def validate_pyfia_estimate(
    pyfia_result,
    state_code: int,
    year: int,
    estimate_type: str,
    client: Optional[EVALIDatorClient] = None,
    **kwargs,
) -> ValidationResult:
    """
    Validate a pyFIA estimate against EVALIDator.

    This is a convenience function that fetches the EVALIDator estimate
    and performs the comparison in one step.

    Parameters
    ----------
    pyfia_result : pl.DataFrame
        pyFIA estimation result DataFrame with estimate and SE columns
    state_code : int
        State FIPS code
    year : int
        Inventory year
    estimate_type : str
        Type of estimate: "area", "volume", "biomass", "carbon", "tpa"
    client : EVALIDatorClient, optional
        Existing client instance (creates new one if not provided)
    **kwargs
        Additional arguments passed to the EVALIDator API call

    Returns
    -------
    ValidationResult
        Comparison result

    Example
    -------
    >>> from pyfia import FIA, area
    >>> from pyfia.evalidator import validate_pyfia_estimate
    >>>
    >>> with FIA("path/to/db.duckdb") as db:
    ...     db.clip_by_state(37)
    ...     db.clip_most_recent(eval_type="EXPALL")
    ...     result = area(db, land_type="forest")
    >>>
    >>> validation = validate_pyfia_estimate(
    ...     result, state_code=37, year=2023, estimate_type="area"
    ... )
    >>> print(validation.message)
    """
    import polars as pl

    if client is None:
        client = EVALIDatorClient()

    # Extract pyFIA values from result DataFrame
    # Assumes standard pyFIA output format with TOTAL and SE columns
    if isinstance(pyfia_result, pl.DataFrame):
        # Look for total/estimate columns
        estimate_cols = [
            c
            for c in pyfia_result.columns
            if "TOTAL" in c.upper() or "ESTIMATE" in c.upper()
        ]
        se_cols = [
            c
            for c in pyfia_result.columns
            if "SE" in c.upper() and "PCT" not in c.upper()
        ]

        if estimate_cols and se_cols:
            pyfia_value = pyfia_result[estimate_cols[0]][0]
            pyfia_se = pyfia_result[se_cols[0]][0]
        else:
            raise ValueError("Could not find estimate and SE columns in pyFIA result")
    else:
        raise TypeError("pyfia_result must be a Polars DataFrame")

    # Fetch EVALIDator estimate based on type
    if estimate_type == "area":
        land_type = kwargs.get("land_type", "forest")
        ev_result = client.get_forest_area(state_code, year, land_type)
    elif estimate_type == "volume":
        vol_type = kwargs.get("vol_type", "net")
        ev_result = client.get_volume(state_code, year, vol_type)
    elif estimate_type == "biomass":
        component = kwargs.get("component", "ag")
        ev_result = client.get_biomass(state_code, year, component)
    elif estimate_type == "carbon":
        pool = kwargs.get("pool", "total")
        ev_result = client.get_carbon(state_code, year, pool)
    elif estimate_type == "tpa":
        min_dia = kwargs.get("min_diameter", 1.0)
        ev_result = client.get_tree_count(state_code, year, min_dia)
    else:
        raise ValueError(f"Unknown estimate_type: {estimate_type}")

    return compare_estimates(pyfia_value, pyfia_se, ev_result)

compare_estimates

compare_estimates

compare_estimates(pyfia_value: float, pyfia_se: float, evalidator_result: EVALIDatorEstimate, tolerance_pct: float = 5.0, pyfia_plot_count: Optional[int] = None) -> ValidationResult

Compare a pyFIA estimate with an EVALIDator official estimate.

PARAMETER DESCRIPTION
pyfia_value

The pyFIA estimate value

TYPE: float

pyfia_se

The pyFIA standard error

TYPE: float

evalidator_result

The EVALIDator official estimate

TYPE: EVALIDatorEstimate

tolerance_pct

Acceptable percentage difference (default 5%)

TYPE: float DEFAULT: 5.0

pyfia_plot_count

Number of plots used by pyFIA (for plot count validation)

TYPE: int DEFAULT: None

RETURNS DESCRIPTION
ValidationResult

Comparison results including pass/fail status

Example

result = compare_estimates( ... pyfia_value=18500000, ... pyfia_se=450000, ... evalidator_result=official_estimate, ... pyfia_plot_count=1500 ... ) print(f"Validation {'PASSED' if result.passed else 'FAILED'}: {result.message}")

Source code in src/pyfia/evalidator/validation.py
def compare_estimates(
    pyfia_value: float,
    pyfia_se: float,
    evalidator_result: EVALIDatorEstimate,
    tolerance_pct: float = 5.0,
    pyfia_plot_count: Optional[int] = None,
) -> ValidationResult:
    """
    Compare a pyFIA estimate with an EVALIDator official estimate.

    Parameters
    ----------
    pyfia_value : float
        The pyFIA estimate value
    pyfia_se : float
        The pyFIA standard error
    evalidator_result : EVALIDatorEstimate
        The EVALIDator official estimate
    tolerance_pct : float
        Acceptable percentage difference (default 5%)
    pyfia_plot_count : int, optional
        Number of plots used by pyFIA (for plot count validation)

    Returns
    -------
    ValidationResult
        Comparison results including pass/fail status

    Example
    -------
    >>> result = compare_estimates(
    ...     pyfia_value=18500000,
    ...     pyfia_se=450000,
    ...     evalidator_result=official_estimate,
    ...     pyfia_plot_count=1500
    ... )
    >>> print(f"Validation {'PASSED' if result.passed else 'FAILED'}: {result.message}")
    """
    ev = evalidator_result

    abs_diff = abs(pyfia_value - ev.estimate)
    pct_diff = (abs_diff / ev.estimate * 100) if ev.estimate != 0 else 0

    # Combined standard error for comparison
    combined_se = (pyfia_se**2 + ev.sampling_error**2) ** 0.5

    within_1se = abs_diff <= combined_se
    within_2se = abs_diff <= 2 * combined_se

    # Pass if within tolerance or within 2 standard errors
    passed = pct_diff <= tolerance_pct or within_2se

    if passed:
        if within_1se:
            message = f"EXCELLENT: Difference ({pct_diff:.2f}%) within 1 SE"
        elif within_2se:
            message = f"GOOD: Difference ({pct_diff:.2f}%) within 2 SE"
        else:
            message = f"ACCEPTABLE: Difference ({pct_diff:.2f}%) within {tolerance_pct}% tolerance"
    else:
        message = f"FAILED: Difference ({pct_diff:.2f}%) exceeds {tolerance_pct}% tolerance and 2 SE"

    return ValidationResult(
        pyfia_estimate=pyfia_value,
        pyfia_se=pyfia_se,
        evalidator_estimate=ev.estimate,
        evalidator_se=ev.sampling_error,
        evalidator_variance=ev.variance,
        evalidator_plot_count=ev.plot_count,
        pyfia_plot_count=pyfia_plot_count,
        absolute_diff=abs_diff,
        pct_diff=pct_diff,
        within_1se=within_1se,
        within_2se=within_2se,
        estimate_type=ev.estimate_type,
        state_code=ev.state_code,
        year=ev.year,
        passed=passed,
        message=message,
    )

Examples

Validate Forest Area

from pyfia import EVALIDatorClient, validate_pyfia_estimate

# Get PyFIA estimate
pyfia_area = pyfia.area(db, land_type="forest")

# Get official estimate
client = EVALIDatorClient()
official = client.get_forest_area("GA", 2022)

# Validate
result = validate_pyfia_estimate(pyfia_area, official)
print(f"PyFIA: {pyfia_area['estimate'][0]:,.0f} acres")
print(f"Official: {official.estimate:,.0f} acres")
print(f"Difference: {result.percent_difference:.2f}%")
print(f"Within CI: {result.within_confidence_interval}")

Validate Volume Estimate

# PyFIA net volume
pyfia_vol = pyfia.volume(db, vol_type="net", tree_type="gs")

# Official growing stock volume
official = client.get_volume("GA", 2022, volume_type="net_growingstock")

result = validate_pyfia_estimate(pyfia_vol, official)

Batch Validation

estimates = [
    ("area", pyfia.area(db), client.get_forest_area("GA", 2022)),
    ("volume", pyfia.volume(db), client.get_volume("GA", 2022)),
    ("biomass", pyfia.biomass(db), client.get_biomass("GA", 2022)),
]

for name, pyfia_est, official in estimates:
    result = validate_pyfia_estimate(pyfia_est, official)
    print(f"{name}: {result.percent_difference:.2f}% diff")